From d45255c48e6771fc855e936f6a12c55d8cba7f58 Mon Sep 17 00:00:00 2001 From: jaxperro Date: Wed, 8 Jul 2026 17:09:46 -0400 Subject: [PATCH] cleanup + calibration tracker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Railway project magnificent-kindness DELETED (soft-delete, purges 2026-07-10) — retired-infra fully gone; requirements.txt loses its Railway-detection rationale (stdlib core, optional extras noted). - tests/test_check_book.py: the Option A orphan-fix regression harness moves into the repo (was session-local); 6 cases, no network/config. - daily.sh: calibration step — one row/day of live-book vs backtest equity to live/history/calibration.csv, the series that sizes real money (day-zero row seeded: live $1,000 vs model $22,027). - config.json.bak (untracked, stale copy of the leaked Alchemy key) deleted locally. Co-Authored-By: Claude Fable 5 --- HANDOFF.md | 1 + live/daily.sh | 32 +++++++ requirements.txt | 7 +- tests/test_check_book.py | 188 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 226 insertions(+), 2 deletions(-) create mode 100644 tests/test_check_book.py diff --git a/HANDOFF.md b/HANDOFF.md index a6e6d10c..34f63f86 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -28,6 +28,7 @@ The repo is authoritative; project memory has deeper history. `jaxperro/trading/copybot-section.js`. `?botFeed=`/`?rmFeed=` query params override feeds for local testing. - **Daily pipeline** (Mac launchd 08:00, fires on wake — typically ~10:00) + appends one live-vs-model row/day to `live/history/calibration.csv` and re-derives everything under the new rules: entries cursors now invalidate daily, `_open_split` folds by the `redeemable` flag, `rtally` splits SOLD. - **Backtest**: `portfolio.py` takes `--bank` (backtest.json `"bank"`). diff --git a/live/daily.sh b/live/daily.sh index b5088fed..b3306a26 100755 --- a/live/daily.sh +++ b/live/daily.sh @@ -54,6 +54,38 @@ echo "[daily] $(date '+%F %T') 5/7 floors: pin copy-bot p80 conviction floors -> python3 sync_floors.py || echo "[daily] floor sync skipped" 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" +# The number that sizes real money (FINDINGS "The calibration experiment"): +# the measured ratio between the live paper book and the backtest of the SAME +# set. Live side from the bot-committed feed on GitHub (freshest truth; the +# local checkout can lag), model side from the portfolio.json just written. +python3 - <<'CALIB' || echo "[daily] calibration row skipped" +import csv, json, os, ssl, time, urllib.request +ctx = ssl._create_unverified_context() +live = json.load(urllib.request.urlopen( + "https://raw.githubusercontent.com/jaxperro/winning-wallet-finder/main/live/copybot_live.json", + timeout=30, context=ctx)) +model = json.load(open("portfolio.json")) +row = { + "date": time.strftime("%F"), + "live_equity": round((live.get("cash") or 0) + (live.get("deployed") or 0) + + (live.get("reserve") or 0), 2), + "live_realized": live.get("realized"), "live_open": live.get("open_count"), + "live_drift": live.get("ledger_drift"), + "model_equity": model.get("equity"), "model_realized": model.get("realized"), + "model_bank": model.get("bank"), "wallets": ",".join(live.get("wallets") or []), +} +os.makedirs("history", exist_ok=True) +path = "history/calibration.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) +print(f"[daily] calibration: live ${row['live_equity']:,} vs model " + f"${row['model_equity']:,} -> {path}") +CALIB 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 diff --git a/requirements.txt b/requirements.txt index 978a75c9..7b554f62 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,5 @@ -# Winning Wallet Finder — no third-party deps, Python 3 stdlib only. -# This file exists so Railway/Nixpacks detects a Python app. +# Core system: Python 3 stdlib only — nothing to install for the finder, +# the daily pipeline, or the PAPER copy bot. +# Optional extras, installed only where used: +# py-clob-client==0.34.6, web3==7.16.0 -> live trading (fly.Dockerfile has them) +# duckdb -> live/ cache + wide/ scanner diff --git a/tests/test_check_book.py b/tests/test_check_book.py new file mode 100644 index 00000000..f83ac0da --- /dev/null +++ b/tests/test_check_book.py @@ -0,0 +1,188 @@ +"""Regression tests for the Option A orphan fix (HANDOFF 2026-07-08): +_record_untracked_buy, _synth_bet, check_book (record/conds self-correct + +guarded boot cash heal). Run: python3 tests/test_check_book.py — needs no +network, no cache, no config; stubs the engine. +""" +import json +import os +import sys +import tempfile + +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) +import copybot # noqa: E402 + +_TMP = tempfile.mkdtemp(prefix="copybot_test_") +FILL_LOG = os.path.join(_TMP, "fills_test.jsonl") +FEED = os.path.join(_TMP, "feed_test.json") + + +class StubEx: + live = False + + def __init__(self): + self.fills = [] + + +class StubEngine: + def __init__(self, state): + self.state = state + self.ex = StubEx() + self.persisted = 0 + + def persist(self): + self.persisted += 1 + + def open_exposure(self): + return sum(p["cost"] for p in self.state["my_pos"].values()) + + +def mkbot(state): + cfg = {"watch": [{"wallet": "0xabc", "name": "TestSharp"}], + "bankroll_usd": 1000.0, + "feed_path": FEED, "fill_log": FILL_LOG} + eng = StubEngine(state) + bot = copybot.Copybot(cfg, eng, filt=None) + bot.here = "" # fill_log paths are absolute already + return bot, eng + + +def base_state(): + return {"my_pos": {}, "their_pos": {}, "bets": {}, "conds": {}, + "adjustments": [], "missed": [], "lag_recent": []} + + +def approx(a, b, tol=0.02): + assert abs(a - b) <= tol, f"{a} !~ {b}" + + +fails = [] + + +def case(name): + def deco(fn): + try: + if os.path.exists(FILL_LOG): + os.remove(FILL_LOG) + fn() + print(f"PASS {name}") + except AssertionError as e: + print(f"FAIL {name}: {e}") + fails.append(name) + return deco + + +@case("drain debits cash; untracked buy gets record+conds+ledger line") +def t1(): + st = base_state() + bot, eng = mkbot(st) + # position exists (engine added it), fill drained under ANOTHER trade's loop + st["my_pos"]["T2"] = {"shares": 80.0, "cost": 40.0, "title": "Orphan Mkt", + "outcome": "Yes", "wallet": "0xabc", "cond": "0xcond2"} + eng.ex.fills.append({"side": "BUY", "token": "T2", "shares": 80.0, "price": 0.5}) + cash0 = st["cash"] + buys = bot._drain_fills() + assert len(buys) == 1 + fee = buys[0]["fee"] + approx(st["cash"], cash0 - 40.0 - fee) + bot._record_untracked_buy(buys[0]) + b = st["bets"]["T2"] + assert b["status"] == "open" and b["their_price"] is None + assert b["name"] == "TestSharp" and b["wallet"] == "0xabc" + assert st["conds"]["T2"] == "0xcond2" + lines = [json.loads(l) for l in open(FILL_LOG)] + assert any(l.get("token") == "T2" and l.get("side") == "BUY" for l in lines) + approx(bot.ledger_drift(), 0.0) + + +@case("check_book synthesizes record + backfills conds, drift stays 0") +def t2(): + st = base_state() + bot, eng = mkbot(st) + st["my_pos"]["T3"] = {"shares": 100.0, "cost": 45.0, "title": "Synth Mkt", + "outcome": "No", "wallet": "0xabc", "cond": "0xcond3"} + # cash WAS debited (cost + the same estimated fee) — drained-but-not-recorded + fee_est = copybot.taker_fee(100.0, 0.45, bot.fee_rate) + st["cash"] -= 45.0 + fee_est + drift = bot.check_book() + b = st["bets"]["T3"] + assert b["status"] == "open" and b["their_price"] is None + approx(b["fee"], fee_est) + assert st["conds"]["T3"] == "0xcond3" + approx(drift, 0.0) + assert eng.persisted > 0 + + +@case("boot heal: never-debited orphan matched by drift -> cash debited, drift 0") +def t3(): + st = base_state() + bot, eng = mkbot(st) + st["my_pos"]["T4"] = {"shares": 72.0, "cost": 36.35, "title": "Hanyi Liu Redux", + "outcome": "Yes", "wallet": "0xabc", "cond": "0xcond4"} + cash0 = st["cash"] # cash NEVER debited — the crash orphan + drift = bot.check_book(heal_cash=True) + fee_est = st["bets"]["T4"]["fee"] + approx(st["cash"], cash0 - 36.35 - fee_est) + approx(drift, 0.0) + lines = [json.loads(l) for l in open(FILL_LOG)] + assert any(l.get("healed") for l in lines) + + +@case("boot heal REFUSES a drift that matches nothing") +def t4(): + st = base_state() + bot, eng = mkbot(st) + st["my_pos"]["T5"] = {"shares": 80.0, "cost": 40.0, "title": "Innocent Mkt", + "outcome": "Yes", "wallet": "0xabc", "cond": "0xcond5"} + fee_est = copybot.taker_fee(80.0, 0.5, bot.fee_rate) + st["cash"] -= 40.0 + fee_est # this position's cash is fine + st["cash"] += 10.0 # unrelated $10 drift from somewhere else + cash0 = st["cash"] + drift = bot.check_book(heal_cash=True) + assert st["cash"] == cash0, "healed cash it should not have touched" + approx(drift, 10.0) + + +@case("real _record_lag record (their_price set) is never a heal candidate") +def t5(): + st = base_state() + bot, eng = mkbot(st) + st["my_pos"]["T6"] = {"shares": 80.0, "cost": 40.0, "title": "Lagged Mkt", + "outcome": "Yes", "wallet": "0xabc", "cond": "0xcond6"} + st["bets"]["T6"] = {"token": "T6", "wallet": "0xabc", "name": "TestSharp", + "outcome": "Yes", "title": "Lagged Mkt", + "their_price": 0.48, "my_price": 0.5, "slippage_pct": 0.04, + "shares": 80.0, "cost": 40.0, "fee": 0.6, + "opened": 1, "status": "open", + "exit_price": None, "pnl": None, "settled": None} + st["cash"] -= 40.6 # this position's cash was properly drained + # fabricate an UNRELATED drift that HAPPENS to equal this bet's cost+fee + st["cash"] += 40.6 + cash0 = st["cash"] + drift = bot.check_book(heal_cash=True) + assert st["cash"] == cash0, "debited a vouched position" + approx(drift, 40.6) + + +@case("two orphans: both healed iteratively? no — only exact-match heals") +def t6(): + st = base_state() + bot, eng = mkbot(st) + # two never-debited orphans of different sizes: drift = sum, matches + # NEITHER individually -> heal must refuse both (visible drift > 0) + st["my_pos"]["T7"] = {"shares": 80.0, "cost": 40.0, "title": "Orphan A", + "outcome": "Yes", "wallet": "0xabc", "cond": "c7"} + st["my_pos"]["T8"] = {"shares": 50.0, "cost": 30.0, "title": "Orphan B", + "outcome": "No", "wallet": "0xabc", "cond": "c8"} + cash0 = st["cash"] + drift = bot.check_book(heal_cash=True) + assert st["cash"] == cash0, "healed on a non-matching drift" + assert drift > 70, f"drift should stay visible, got {drift}" + + +for f in (FILL_LOG, FEED): + if os.path.exists(f): + os.remove(f) + +print() +print("FAILURES:", fails or "none") +sys.exit(1 if fails else 0)