mirror of
https://github.com/jaxperro/winning-wallet-finder.git
synced 2026-07-27 15:57:47 +00:00
#24 v1: follow-since windows (config git history -> per-wallet clamp), reject-split buckets (REJECTED vs UNSEEN), reconcile --csv row + drift alarm, live-replica residual
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"0xe8ca3f758c93f44f3ec210542ab78afb7c0bcccb": 1783307453,
|
||||
"0x86c878cde72660ec52f5e6f0f0438b76de8fc867": 1783307453,
|
||||
"0x41558102a796ba971c7567cad41c307e59f8fa41": 1783307453,
|
||||
"0xd96750bf8d941a8186e592b0ae6e096da66aa266": 1783307453,
|
||||
"0xfc81760d44a21acc9fd4b749a5bf9a9b2eeae072": 1783307453,
|
||||
"0xf5fe759cece500f58a431ef8dacea321f6e3e23d": 1783307453,
|
||||
"0x73afc8160c17830c0c7281a7bf570c871455b880": 1783307453,
|
||||
"0x4bfb41d5b570defd03c39a9a4d8de6bd8b8982e": 1783308783,
|
||||
"0x72e1597864456eda62878413cf3e60c332e4a45d": 1783308985,
|
||||
"0x0d42cc5ff0526fbb8f0bb9eafa9ebba3f23df124": 1783476565,
|
||||
"0xbadaf319415c17f28824a43ae0cd912b9d84d874": 1783476565,
|
||||
"0x215adbb63b47d0ca92f849fe2c2dc1adb0f6254c": 1783476565,
|
||||
"0xd7d36345c4aab150e59577e360696b01d01d698b": 1783527507,
|
||||
"0x921433c93558b9a4ba807ec824d02aad7ea2ddbf": 1783527507,
|
||||
"0xa1d57d329227c75b12b09f927fb3d6d6ef8f1343": 1783527507,
|
||||
"0x40ce68f1564f3c751b12d88a393d8cc0651dbf90": 1784823384
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env python3
|
||||
"""#24 v1 — per-wallet follow-start dates from the follow config's own git
|
||||
history (the truth of when each wallet actually entered the book).
|
||||
Writes live/follow_since.json {wallet_lower: epoch}; wallets present in
|
||||
the FIRST recorded config carry that first-commit date (left-censored,
|
||||
same convention as sharp_halflife). Run by daily.sh before the portfolio
|
||||
replays so --follow-only never replays a wallet before we followed it
|
||||
(the JuiceFarm class: 8 'unseen' bets that were simply pre-follow)."""
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.dirname(HERE)
|
||||
|
||||
|
||||
def main():
|
||||
log = subprocess.run(
|
||||
["git", "log", "--reverse", "--format=%at %H",
|
||||
"--", "live/copybot.paper.json"],
|
||||
cwd=ROOT, capture_output=True, text=True).stdout.split()
|
||||
pairs = list(zip(log[0::2], log[1::2]))
|
||||
since = {}
|
||||
for ts, sha in pairs:
|
||||
try:
|
||||
blob = subprocess.run(
|
||||
["git", "show", f"{sha}:live/copybot.paper.json"],
|
||||
cwd=ROOT, capture_output=True, text=True).stdout
|
||||
cfg = json.loads(blob)
|
||||
except Exception:
|
||||
continue
|
||||
for w in cfg.get("wallets", []):
|
||||
a = (w.get("wallet") or "").lower()
|
||||
if a and a not in since:
|
||||
since[a] = int(ts)
|
||||
# wallets in the working tree but not yet committed: follow = now
|
||||
try:
|
||||
cfg = json.load(open(os.path.join(HERE, "copybot.paper.json")))
|
||||
for w in cfg.get("wallets", []):
|
||||
a = (w.get("wallet") or "").lower()
|
||||
if a and a not in since:
|
||||
since[a] = int(time.time())
|
||||
except Exception:
|
||||
pass
|
||||
out = os.path.join(HERE, "follow_since.json")
|
||||
json.dump(since, open(out, "w"), indent=1)
|
||||
print(f"[follow_since] {len(since)} wallets -> follow_since.json")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -127,6 +127,16 @@ if _ARGS.follow_only:
|
||||
WALLETS = [w for w in WALLETS if w["wallet"].lower() in _FSET]
|
||||
except Exception as _e:
|
||||
raise SystemExit(f"--follow-only: cannot read copybot.paper.json ({_e})")
|
||||
# #24 v1: never replay a wallet before we actually followed it (the
|
||||
# JuiceFarm class — pre-follow bets are not copyable reality). Dates
|
||||
# from the follow config's own git history (live/follow_since.py).
|
||||
try:
|
||||
FOLLOW_SINCE = {k.lower(): int(v) for k, v in json.load(
|
||||
open(os.path.join(HERE, "follow_since.json"))).items()}
|
||||
except Exception:
|
||||
FOLLOW_SINCE = {}
|
||||
else:
|
||||
FOLLOW_SINCE = {}
|
||||
if not WALLETS:
|
||||
raise SystemExit("no wallets to replay: create live/backtest.json or pass --wallets")
|
||||
|
||||
@@ -460,6 +470,13 @@ def main():
|
||||
if b["cond"] and (b["cond"] not in by_mkt or b["entry_t"] < by_mkt[b["cond"]]["entry_t"]):
|
||||
b["kind"] = "open"; by_mkt[b["cond"]] = b
|
||||
stream = sorted(by_mkt.values(), key=lambda b: b["entry_t"])
|
||||
if FOLLOW_SINCE:
|
||||
_pre = len(stream)
|
||||
stream = [b for b in stream if b["entry_t"] >=
|
||||
FOLLOW_SINCE.get(b["wallet"].lower(), 0)]
|
||||
if _pre != len(stream):
|
||||
print(f"portfolio: follow-since dropped {_pre - len(stream)} "
|
||||
f"pre-follow bets", flush=True)
|
||||
|
||||
# ONLY_CONDS=<json path>: replay only these markets — {cond: bool} or
|
||||
# us_listable.py's {cond: {"listed": bool, ...}}. Models "same signal, but
|
||||
@@ -707,6 +724,7 @@ def main():
|
||||
"entry_mode": ENTRY_MODE, "exit_mode": EXIT_MODE,
|
||||
"mirror_sell_min_p": MIRROR_SELL_MIN_P,
|
||||
"follow_only": bool(_ARGS.follow_only),
|
||||
"follow_since_applied": bool(FOLLOW_SINCE),
|
||||
}
|
||||
json.dump(out, open(os.path.join(HERE, OUT) if not os.path.isabs(OUT) else OUT, "w"),
|
||||
separators=(",", ":"))
|
||||
|
||||
@@ -23,6 +23,7 @@ honest. Companion input: live/portfolio_follow_bets.json (portfolio.py
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import tape # noqa: E402
|
||||
@@ -77,13 +78,34 @@ def bot_views():
|
||||
return v
|
||||
|
||||
|
||||
def rejects():
|
||||
"""{book: {token,...}} — the reject ledgers (live since 2026-07-24)
|
||||
split UNSEEN into REJECTED (screen: the bot saw and filtered) vs
|
||||
truly unseen (universe)."""
|
||||
out = {"paper": set(), "live": set()}
|
||||
for book, f in (("paper", "copybot_rejects.jsonl"),
|
||||
("live", "copybot_rejects.live.jsonl")):
|
||||
for r in load_jsonl(f):
|
||||
if r.get("token"):
|
||||
out[book].add(str(r["token"]))
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
bets = json.load(open(os.path.join(ROOT, "live",
|
||||
"portfolio_follow_bets.json")))
|
||||
try:
|
||||
fs = {k.lower(): int(v) for k, v in json.load(open(os.path.join(
|
||||
ROOT, "live", "follow_since.json"))).items()}
|
||||
nm2w = {w["name"]: w["wallet"].lower() for w in json.load(open(
|
||||
os.path.join(ROOT, "live", "copybot.paper.json")))["wallets"]}
|
||||
except Exception:
|
||||
fs, nm2w = {}, {}
|
||||
bets = [b for b in bets if b.get("entry_t", 0) >= PARITY_T0
|
||||
and b.get("asset") and b.get("p")]
|
||||
print(f"backtest bets in the parity era with join keys: {len(bets)}",
|
||||
flush=True)
|
||||
and b.get("asset") and b.get("p")
|
||||
and b.get("entry_t", 0) >= fs.get(nm2w.get(b["name"], ""), 0)]
|
||||
print(f"backtest bets in the parity era with join keys "
|
||||
f"(follow-since applied): {len(bets)}", flush=True)
|
||||
db = tape.connect()
|
||||
tape.build_resolved(db)
|
||||
pays = fwd.payouts_for(db, [b["asset"] for b in bets])
|
||||
@@ -100,9 +122,11 @@ def main():
|
||||
print(f"chain-graded (refunds/pending excluded): {len(graded)}\n",
|
||||
flush=True)
|
||||
|
||||
rej = rejects()
|
||||
row = {"date": time.strftime("%Y-%m-%d"), "n": len(graded)}
|
||||
for book in ("paper", "live"):
|
||||
f, mset = v[book]["fills"], v[book]["missed"]
|
||||
buckets = {"FILLED": [], "MISSED": [], "UNSEEN": []}
|
||||
buckets = {"FILLED": [], "MISSED": [], "REJECTED": [], "UNSEEN": []}
|
||||
pg = 0.0
|
||||
for b in graded:
|
||||
a = b["asset"]
|
||||
@@ -114,17 +138,23 @@ def main():
|
||||
pg += b["_ev_bt"] - ev_real
|
||||
elif a in mset:
|
||||
buckets["MISSED"].append(b)
|
||||
elif a in rej[book]:
|
||||
buckets["REJECTED"].append(b) # screen gap, now visible
|
||||
else:
|
||||
buckets["UNSEEN"].append(b)
|
||||
buckets["UNSEEN"].append(b) # universe gap
|
||||
tot_bt = sum(b["_ev_bt"] for b in graded)
|
||||
row[f"{book}_bt_ev"] = round(tot_bt)
|
||||
print(f"== {book.upper()} (n={len(graded)} signals · backtest "
|
||||
f"$100-EV {tot_bt:+,.0f}) ==")
|
||||
for k in ("FILLED", "MISSED", "UNSEEN"):
|
||||
for k in ("FILLED", "MISSED", "REJECTED", "UNSEEN"):
|
||||
bs = buckets[k]
|
||||
ev = sum(b["_ev_bt"] for b in bs)
|
||||
share = 100 * ev / tot_bt if tot_bt else 0
|
||||
print(f" {k:>7}: n={len(bs):>4} · backtest EV in bucket "
|
||||
row[f"{book}_{k.lower()}_n"] = len(bs)
|
||||
row[f"{book}_{k.lower()}_ev"] = round(ev)
|
||||
print(f" {k:>8}: n={len(bs):>4} · backtest EV in bucket "
|
||||
f"{ev:+9.0f} ({share:+5.1f}% of story)")
|
||||
row[f"{book}_fill_gap"] = round(pg)
|
||||
print(f" fill-price gap on FILLED (their_p feeless -> my_p+fee): "
|
||||
f"{pg:+,.0f}")
|
||||
un = buckets["UNSEEN"]
|
||||
@@ -135,8 +165,60 @@ def main():
|
||||
top = sorted(byw.items(), key=lambda kv: -kv[1])[:4]
|
||||
print(f" unseen by wallet: {top}")
|
||||
print(flush=True)
|
||||
json.dump({"n": len(graded)}, open(os.path.join(
|
||||
HERE, "copy_reconcile.json"), "w"))
|
||||
# live-bankroll replica residual: the model at the live book's capital
|
||||
# vs the live book itself (window-aligned numbers recorded raw)
|
||||
try:
|
||||
rep = json.load(open(os.path.join(ROOT, "live",
|
||||
"portfolio_live_replica.json")))
|
||||
lv = json.load(open(os.path.join(ROOT, "live",
|
||||
"copybot_live_real_full.json")))
|
||||
row["replica_pnl"] = round(rep.get("pnl") or 0, 2)
|
||||
row["live_realized"] = round(lv.get("pnl_realized")
|
||||
or lv.get("realized") or 0, 2)
|
||||
print(f"REPLICA: model-at-live-bank 30d P&L {row['replica_pnl']:+.2f}"
|
||||
f" vs live book realized {row['live_realized']:+.2f} "
|
||||
f"(residual {row['replica_pnl'] - row['live_realized']:+.2f})",
|
||||
flush=True)
|
||||
except Exception:
|
||||
pass
|
||||
json.dump(row, open(os.path.join(HERE, "copy_reconcile.json"), "w"))
|
||||
|
||||
if "--csv" in sys.argv:
|
||||
import csv as _csv
|
||||
path = os.path.join(ROOT, "history", "reconcile.csv")
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
rows = []
|
||||
try:
|
||||
rows = list(_csv.DictReader(open(path)))
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
rows = [r for r in rows if r.get("date") != row["date"]]
|
||||
# drift alarm: any bucket-EV share moved >15pp vs the trailing mean
|
||||
if len(rows) >= 5:
|
||||
import statistics as _st
|
||||
for book in ("paper", "live"):
|
||||
for k in ("filled", "missed", "rejected", "unseen"):
|
||||
key = f"{book}_{k}_ev"
|
||||
tot_k = f"{book}_bt_ev"
|
||||
hist = [float(r[key]) / max(1, abs(float(r[tot_k])))
|
||||
for r in rows[-7:] if r.get(key) and r.get(tot_k)]
|
||||
if not hist:
|
||||
continue
|
||||
cur = row[key] / max(1, abs(row[tot_k]))
|
||||
if abs(cur - _st.mean(hist)) > 0.15:
|
||||
print(f"⚠ RECONCILE DRIFT {book}.{k}: share "
|
||||
f"{cur:+.0%} vs 7d mean {_st.mean(hist):+.0%}",
|
||||
flush=True)
|
||||
rows.append({k: str(vv) for k, vv in row.items()})
|
||||
cols = sorted({c for r in rows for c in r}, key=lambda c:
|
||||
(c != "date", c))
|
||||
with open(path, "w", newline="") as fh:
|
||||
w = _csv.DictWriter(fh, fieldnames=cols)
|
||||
w.writeheader()
|
||||
for r in rows:
|
||||
w.writerow(r)
|
||||
print(f"[reconcile.csv] {len(rows)} rows -> history/reconcile.csv",
|
||||
flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user