audit fixes: pin-mirror to backtest, unpinned-floor warning, watch_sharps diff, class_pct guard

From the automated-writer clobber audit (3-agent workflow):
- sync_floors now ACTUALLY mirrors floor_pin into backtest.json (the comment
  claimed it; the code never did — so a paper pin diverged from the backtest's
  dynamic p80, defeating calibration). Kruto 80 now consistent across all 3.
- sync_floors warns loudly when about to revert an UNPINNED floor that looks
  hand-edited (>5 and >15%) — floor_pin is the only protected key, so a direct
  edit used to vanish silently at 08:00.
- validate_timing logs the sharps add/drop delta and documents watch_sharps.json
  as generated-only (format kept a plain list — dashboard/discord read it so).
- daily.sh warns if copybot.paper.json vs backtest.json class_pct desync.

Audit verdict otherwise CLEAN: sync_floors is the only pipeline step that
mutates a manual-knob file, and it round-trips the whole JSON touching only
floor — all other knobs (caps, guard, depth_gate, follow set, band) survive.
config.live.example.json is untouched by every automated writer (safe by
neglect). No second floor_pin-class bug exists.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jaxperro
2026-07-13 17:11:17 -04:00
parent 51e769fbf0
commit 57cdbde395
3 changed files with 60 additions and 1 deletions
+9
View File
@@ -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"
+36
View File
@@ -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)"))
+15 -1
View File
@@ -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)")