diff --git a/.gitignore b/.gitignore index abd9cc5c..b536ed32 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,4 @@ live/watch_prejune*.json live/history/ live/slug_cache.json archive/local/ +live/edge_verdict.txt diff --git a/HANDOFF.md b/HANDOFF.md index 1fa74d2f..d668c8f1 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -28,6 +28,17 @@ since 07-11 and since the 07-13 21:30Z swap. Verdict: and −$438 all-time paper — NOT re-added; re-check next Friday. - Guard/floor counterfactuals: re-validated 07-14 by the missed-bets audit (guard skips net-negative would-be on both books) — unchanged. +- **BANKROLL decision instrumented (live/edge.py, in daily.sh + digest + footer)**: parity-era per-signal paper edge vs the MEASURED live fee + hurdle (1.9% of stake — real, not the 3-4% guess; extreme-price copies + keep p(1−p) small) + matched live-paper drag (6.4pp @ $1 stakes). + Decision rule: size up only when edge > hurdle+2pp on n≥30 parity-era + resolved signals (~end of July). history/edge.csv accrues one row/day; + `python3 live/edge.py` any time for the current verdict. RECOMMENDED to + the user (2026-07-16, no decision yet): ~$30-50 coverage top-up now at + most (the free-cash gate is skipping signals at $6 free), big sizing + waits for the measurement; caps bind at ~$60-125 equity anyway and stay + untouched. Deposits are the user's to make — never move funds. ## Shipped since rev 12 (2026-07-15) - **PAPER FAK PARITY**: PaperExecutor BUYs now model live FAK reality — if diff --git a/live/daily.sh b/live/daily.sh index 495cd89b..f78b53b4 100755 --- a/live/daily.sh +++ b/live/daily.sh @@ -95,6 +95,10 @@ with open(path, "a", newline="") as fh: print(f"[daily] calibration: live ${row['live_equity']:,} vs model " f"${row['model_equity']:,} -> {path}") CALIB +echo "[daily] $(date '+%F %T') edge: parity-era per-signal edge vs fee hurdle -> history/edge.csv" +# The bankroll-decision number (HANDOFF rev 13 / 2026-07-16): one row/day; +# the verdict line rides the Discord digest footer below. +python3 edge.py || echo "[daily] edge skipped" echo "[daily] $(date '+%F %T') 6/7 dashboard" python3 dashboard.py mkdir -p history && cp watch_skilled.json "history/watch_$(date '+%Y%m%d').json" 2>/dev/null @@ -118,4 +122,5 @@ echo "[daily] $(date '+%F %T') done -> watch_sharps.json + dashboard.html" # Discord: the daily sharp-list digest (profile links + 30D conviction stats). # The only Discord output in the system — per-trade pings retired 2026-07-04. # Webhook lives in gitignored ../config.json -> daily_webhook. -python3 discord_daily.py "feed: $PUBLISH" || echo "[daily] discord digest failed (non-fatal)" +EDGE_LINE="$(head -1 edge_verdict.txt 2>/dev/null || true)" +python3 discord_daily.py "feed: $PUBLISH${EDGE_LINE:+ · $EDGE_LINE}" || echo "[daily] discord digest failed (non-fatal)" diff --git a/live/edge.py b/live/edge.py new file mode 100644 index 00000000..5c0e6bfa --- /dev/null +++ b/live/edge.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +"""The bankroll-decision number: parity-era per-signal edge vs the fee hurdle. + +One row per run -> history/edge.csv, one verdict line -> edge_verdict.txt +(daily.sh rides it into the Discord digest footer) and stdout. Run ad-hoc any +time: python3 live/edge.py + +What it measures (FINDINGS "The calibration experiment", HANDOFF rev 13): +- EDGE: net per-signal ROI of the PAPER book's resolved copies opened after + PARITY_T0 (2026-07-16 03:49Z, the paper FAK-parity boot — before that the + paper book pretended to fill thin books, so its ROI was flattered). This is + the honest estimate of what the strategy earns per signal after modeled + fees/slippage. +- HURDLE: the LIVE book's realized round-trip fee drag (fees paid / dollars + staked, doubled for the exit side when a position hasn't closed yet is NOT + modeled — we use actual fees over actual stakes, both sides included as + they were really paid). The taker fee is proportional, so this hurdle does + NOT shrink with stake size. +- DRAG: matched-token live-vs-paper ROI gap (execution reality check; the + $1-stake rounding component of this shrinks as stakes grow, the fee + component doesn't). + +Decision rule (agreed 2026-07-16): sizing up needs EDGE comfortably above +HURDLE on n>=30 parity-era resolved signals. Feeds are read from GitHub raw +(freshest truth — the bots commit their own state; a local checkout can lag), +falling back to the local files offline. +""" +import csv +import json +import os +import ssl +import time +import urllib.request + +HERE = os.path.dirname(os.path.abspath(__file__)) +RAW = "https://raw.githubusercontent.com/jaxperro/winning-wallet-finder/main/live" +PARITY_T0 = 1784260140 # 2026-07-16 03:49Z — paper booted on 646139d +MIN_N = 30 # below this, the verdict is "insufficient data" + + +def _feed(name): + try: + ctx = ssl._create_unverified_context() + with urllib.request.urlopen(f"{RAW}/{name}", timeout=30, context=ctx) as r: + return json.loads(r.read()) + except Exception: + return json.load(open(os.path.join(HERE, name))) + + +def main(): + paper = _feed("copybot_live.json") + live = _feed("copybot_live_real.json") + + # EDGE: parity-era resolved paper copies + era = [b for b in paper.get("bets", []) + if (b.get("opened") or 0) >= PARITY_T0 + and b.get("pnl") is not None and b.get("cost")] + n = len(era) + staked = sum(b["cost"] for b in era) + pnl = sum(b["pnl"] for b in era) + edge = pnl / staked if staked else 0.0 + + # HURDLE: live's actual fee drag over everything it ever staked + lbets = [b for b in live.get("bets", []) if b.get("cost")] + lstaked = sum(b["cost"] for b in lbets) + hurdle = (live.get("fees_paid") or 0) / lstaked if lstaked else 0.0 + + # DRAG: matched resolved tokens on both books + def by_tok(feed): + return {b["token"]: b for b in feed.get("bets", []) + if b.get("token") and b.get("pnl") is not None and b.get("cost")} + lb, pb = by_tok(live), by_tok(paper) + common = set(lb) & set(pb) + m_ls = sum(lb[t]["cost"] for t in common) + m_ps = sum(pb[t]["cost"] for t in common) + drag = ((sum(pb[t]["pnl"] for t in common) / m_ps if m_ps else 0.0) + - (sum(lb[t]["pnl"] for t in common) / m_ls if m_ls else 0.0)) + + if n < MIN_N: + verdict = f"MEASURING ({n}/{MIN_N} parity-era signals)" + elif edge > hurdle + 0.02: + verdict = "CLEARS the fee hurdle — sizing up is defensible" + elif edge > 0: + verdict = "positive but BELOW the hurdle — live can't compound" + else: + verdict = "NEGATIVE edge — keep money out" + + line = (f"edge {edge:+.1%}/signal (n={n}, ${staked:,.0f} staked) · " + f"fee hurdle {hurdle:.1%} · live-paper drag {drag*100:.1f}pp " + f"({len(common)} matched) · {verdict}") + print(f"[edge] {line}") + with open(os.path.join(HERE, "edge_verdict.txt"), "w") as fh: + fh.write(line + "\n") + + row = {"date": time.strftime("%F"), "n": n, "staked": round(staked, 2), + "pnl": round(pnl, 2), "edge_pct": round(edge * 100, 2), + "hurdle_pct": round(hurdle * 100, 2), + "drag_pp": round(drag * 100, 2), "matched_n": len(common), + "verdict": verdict} + os.makedirs(os.path.join(HERE, "history"), exist_ok=True) + path = os.path.join(HERE, "history", "edge.csv") + new = not os.path.exists(path) + with open(path, "a", newline="") as fh: + w = csv.DictWriter(fh, fieldnames=list(row)) + if new: + w.writeheader() + w.writerow(row) + + +if __name__ == "__main__": + main()