diff --git a/live/daily.sh b/live/daily.sh index b3306a26..495cd89b 100755 --- a/live/daily.sh +++ b/live/daily.sh @@ -52,6 +52,15 @@ python3 conviction_scan.py python3 validate_timing.py echo "[daily] $(date '+%F %T') 5/7 floors: pin copy-bot p80 conviction floors -> copybot.paper.json" python3 sync_floors.py || echo "[daily] floor sync skipped" +# parity guard (2026-07-13 audit): class_pct lives in BOTH copybot.paper.json +# and backtest.json with no generator — warn if they silently desync so the +# paper book and backtest don't drift apart on sizing. +python3 - <<'PY' || true +import json +p=json.load(open("copybot.paper.json")).get("follow",{}).get("class_pct") +b=json.load(open("backtest.json")).get("class_pct") +if p!=b: print(f"[daily] ⚠ class_pct DESYNC: paper {p} != backtest {b}") +PY echo "[daily] $(date '+%F %T') portfolio: cache-based \$1k book -> portfolio.json" python3 portfolio.py || echo "[daily] portfolio skipped" echo "[daily] $(date '+%F %T') calibration: live-book vs model, one row/day" diff --git a/live/sync_floors.py b/live/sync_floors.py index 60fda9db..01692d85 100644 --- a/live/sync_floors.py +++ b/live/sync_floors.py @@ -30,6 +30,35 @@ import trust # noqa: E402 HERE = os.path.dirname(os.path.abspath(__file__)) PAPER = os.path.join(HERE, "copybot.paper.json") +BACKTEST = os.path.join(HERE, "backtest.json") + + +def mirror_pins_to_backtest(paper_ws): + """Keep backtest.json's floors on the SAME pins as the paper book so the + three books stay comparable (the audit found the code never did this + despite the comment claiming it — a floor_pin=80 on Kruto in paper vs a + dynamic p80=125.61 in the backtest silently defeated the calibration).""" + pins = {w["wallet"].lower(): w["floor_pin"] + for w in paper_ws if w.get("floor_pin") is not None} + if not pins: + return + try: + bt = json.load(open(BACKTEST)) + except Exception: + return + touched = 0 + for w in bt.get("wallets", []): + p = pins.get(w["wallet"].lower()) + if p is not None: + w["floor_pin"] = round(float(p), 2) + if w.get("floor") != round(float(p), 2): + touched += 1 + w["floor"] = round(float(p), 2) + if touched: + tmp = BACKTEST + ".tmp" + json.dump(bt, open(tmp, "w"), indent=2) + os.replace(tmp, BACKTEST) + print(f"[floors] mirrored {len(pins)} pin(s) into backtest.json") def trusted_p80(wallet, now=None): @@ -71,12 +100,19 @@ def main(): continue # no trusted sized bets — leave as-is new = round(p80, 2) old = w.get("floor") + # a big UNPINNED gap = a hand edit about to be reverted. floor_pin is + # the only protected key; warn loudly so a direct floor edit doesn't + # vanish silently at 08:00 (the trap behind the original Kruto bug). + if old is not None and abs(old - new) > 5 and abs(old - new) / max(new, 1) > 0.15: + print(f"[floors] ⚠ {w.get('name', w['wallet'][:8])} floor {old}→{new} " + f"(p80) — was this a manual edit? add \"floor_pin\": {old} to keep it") w["floor"] = new if old is None or abs((old or 0) - new) > 0.5: changed.append((w.get("name", w["wallet"][:8]), old, new)) tmp = PAPER + ".tmp" json.dump(cfg, open(tmp, "w"), indent=2) os.replace(tmp, PAPER) + mirror_pins_to_backtest(ws) print(f"[floors] pinned {len(ws)} trusted-p80 floors to copybot.paper.json" + (f"; changed: " + ", ".join(f"{n} {o}→{v:g}" for n, o, v in changed) if changed else " (no material change)")) diff --git a/live/validate_timing.py b/live/validate_timing.py index d02e60fe..6c82ca66 100644 --- a/live/validate_timing.py +++ b/live/validate_timing.py @@ -418,7 +418,21 @@ def main(): f"{(str(c['held_won'])+'-'+str(c['held_lost'])):>9}" f"{sp:>5.0f}%{ld:>9} {(c.get('name') or c['wallet'][:10])}") - json.dump(sharps, open(os.path.join(HERE, "watch_sharps.json"), "w"), indent=2) + # GENERATED-ONLY file (2026-07-13 audit): regenerated wholesale nightly — + # any hand edit here is silently clobbered. Manual follow decisions live + # in copybot.paper.json, not here. Log the delta vs the prior file so a + # replacement always leaves an audit trail in daily.log. Format stays a + # plain list (the dashboard + discord_daily read it as one). + path = os.path.join(HERE, "watch_sharps.json") + try: + prev = {s.get("wallet") for s in json.load(open(path))} + cur = {s.get("wallet") for s in sharps} + if cur - prev or prev - cur: + print(f" sharps Δ: +{len(cur - prev)} {[w[:10] for w in cur - prev]} " + f"· -{len(prev - cur)} {[w[:10] for w in prev - cur]}") + except Exception: + pass + json.dump(sharps, open(path, "w"), indent=2) print(f"\n-> watch_sharps.json ({len(sharps)} copy-positive holders)")