CRITICAL: fix their_prev crash that silently killed all new opens since 07-05
_handle_their_buy referenced their_prev in the fresh-OPEN branch but only assigned it in the is_add branch — every new position crashed with UnboundLocalError the moment the their-bet ceiling landed 2026-07-06. The webhook/heartbeat try/except swallowed it as 'handler error', so the bot logged FOLLOW then silently placed NOTHING (last real fill 07-05 23:17; live Fly logs show FOLLOW ArbTrader ... -> heartbeat error: their_prev). This is the primary cause of 'live bot missing bets'. Fix: hoist their_prev above the if/else so both branches see the signal's prior position. Also (the user's two asks): - copybot on_wallet_activity: a QUALIFYING bet we were too slow to catch (webhook missed / bot down / fast market resolved before we polled) is now recorded as a missed bet 'too slow to follow (Nm late)' instead of silently dropped; below-floor dust stays console-only. Filter runs before the stale gate so we know if it would have qualified. - sync_floors.py rewritten: PIN each paper-bot wallet's floor to the trusted cache p80 (identical to the backtest's conv_thr), written to copybot.paper.json, wired into daily.sh + committed. Kills the boot-time data-api floor drift (fortuneking $1,498 boot vs $892 backtest) that made the live bot filter out bets the backtest kept. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -407,6 +407,13 @@ class CopyTrader:
|
||||
label, title, outcome, event=None, cond=None):
|
||||
mine = self.state["my_pos"].get(token)
|
||||
is_add = mine is not None
|
||||
# the signal's position in this token BEFORE this trade — the their-bet
|
||||
# ceiling needs it in BOTH branches (their_prev + their_size = their
|
||||
# total stake). Defining it only inside the is_add branch made every
|
||||
# fresh OPEN crash with UnboundLocalError since the ceiling landed
|
||||
# 2026-07-06 — swallowed as a webhook "handler error", so the bot
|
||||
# silently placed NO new positions (last fill 2026-07-05, found 07-07).
|
||||
their_prev = self.state["their_pos"].get(wallet, {}).get(token, 0)
|
||||
# don't backfill: never open a position they already held when we
|
||||
# started watching. (A position we built during the run is an ADD;
|
||||
# a brand-new position they opened after start is a fresh OPEN.)
|
||||
@@ -445,7 +452,6 @@ class CopyTrader:
|
||||
# game to 2.15 stakes ($90 on a $42-stake book) when fortuneking
|
||||
# doubled into his own bet; the backtest is one-market-one-stake,
|
||||
# so the bot must be too.
|
||||
their_prev = self.state["their_pos"].get(wallet, {}).get(token, 0)
|
||||
frac = their_size / their_prev if their_prev > 0 else 0
|
||||
want_shares = mine["shares"] * frac
|
||||
room = self.stake_usd(wallet, their_prev + their_size) - mine["cost"]
|
||||
|
||||
+24
-3
@@ -689,10 +689,31 @@ class Copybot:
|
||||
tx = t.get("transactionHash")
|
||||
if not tx or tx in self.engine.seen or tx in self.skipped:
|
||||
continue
|
||||
if not ignore_stale and time.time() - t.get("timestamp", 0) > RECENT_TRADE_WINDOW_S:
|
||||
self.skipped.add(tx) # stale — the webhook is about a newer tx
|
||||
continue
|
||||
# filter FIRST (before the stale gate) so we know whether a trade we
|
||||
# were too slow on WOULD have qualified — a qualifying miss is worth
|
||||
# recording; a below-floor/out-of-band one is a deliberate skip.
|
||||
follow, reason = self.filt.check(wallet, t)
|
||||
stale = not ignore_stale and time.time() - t.get("timestamp", 0) > RECENT_TRADE_WINDOW_S
|
||||
if follow and stale:
|
||||
# a bet we'd have copied but didn't catch in time — webhook missed
|
||||
# it, the bot was down, or the market resolved faster than we poll
|
||||
# (5-min crypto, in-play). Log it as MISSED so it's visible on the
|
||||
# dashboard instead of silently dropped; settle_resolved values it
|
||||
# hypothetically like every other miss. record_miss dedups by token
|
||||
# so reconcile_entries can't double-count the same position.
|
||||
self.skipped.add(tx)
|
||||
if t.get("side") == "BUY" and t.get("asset"):
|
||||
late_m = (time.time() - (t.get("timestamp") or 0)) / 60.0
|
||||
with self.lock:
|
||||
self.engine.record_miss(
|
||||
wallet, t["asset"], t.get("conditionId"),
|
||||
t.get("title") or "", t.get("outcome") or "",
|
||||
t.get("price") or 0, self.engine.stake_usd(wallet),
|
||||
f"too slow to follow ({late_m:.0f}m late)")
|
||||
self.engine.persist()
|
||||
log(f"MISS {name}: {t.get('side')} {t.get('outcome','?')} "
|
||||
f"@ {t.get('price',0):.3f} — too slow ({late_m:.0f}m late)")
|
||||
continue
|
||||
if not follow:
|
||||
self.skipped.add(tx)
|
||||
log(f"skip {name}: {t.get('side')} {t.get('outcome','?')} "
|
||||
|
||||
+8
-7
@@ -9,10 +9,11 @@
|
||||
# 4) sharps — conviction-profile scan + last-minute timing gate ->
|
||||
# conviction_wallets.json + watch_sharps.json (the set the live
|
||||
# trading dashboard reads via raw.githubusercontent)
|
||||
# 5) floors — recompute the copy bot's per-wallet p80 conviction floors from
|
||||
# the (now-fresh) cache into ../config.json, so copybot.py stays
|
||||
# in parity with the dashboard's top-20%-by-stake gate. Local-only
|
||||
# (config.json is gitignored); never touches the curated watchlist.
|
||||
# 5) floors — pin the live paper bot's per-wallet p80 conviction floors
|
||||
# (trusted-cache p80, same as the backtest) into
|
||||
# copybot.paper.json, so the bot and the backtest gate on the
|
||||
# SAME threshold and boots are deterministic (no per-boot
|
||||
# data-api drift). Committed in the publish step.
|
||||
# 6) dashboard — regenerate + snapshot for auditable forward history
|
||||
# 7) publish — commit + push the refreshed outputs so the live dashboard
|
||||
# (jaxperro.com/trading) picks up the new sharp list
|
||||
@@ -49,8 +50,8 @@ python3 skill.py
|
||||
echo "[daily] $(date '+%F %T') 4/7 sharps: conviction scan + last-minute timing gate"
|
||||
python3 conviction_scan.py
|
||||
python3 validate_timing.py
|
||||
echo "[daily] $(date '+%F %T') 5/7 floors: recompute copy-bot p80 conviction floors -> ../config.json"
|
||||
python3 sync_floors.py || echo "[daily] floor sync skipped (no config/watchlist)"
|
||||
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"
|
||||
echo "[daily] $(date '+%F %T') portfolio: cache-based \$1k book -> portfolio.json"
|
||||
python3 portfolio.py || echo "[daily] portfolio skipped"
|
||||
echo "[daily] $(date '+%F %T') 6/7 dashboard"
|
||||
@@ -58,7 +59,7 @@ python3 dashboard.py
|
||||
mkdir -p history && cp watch_skilled.json "history/watch_$(date '+%Y%m%d').json" 2>/dev/null
|
||||
echo "[daily] $(date '+%F %T') 7/7 publish (commit + push refreshed outputs)"
|
||||
PUBLISH="no changes"
|
||||
git add watch_skilled.json watch_sharps.json conviction_wallets.json dashboard.html portfolio.json 2>/dev/null
|
||||
git add watch_skilled.json watch_sharps.json conviction_wallets.json dashboard.html portfolio.json copybot.paper.json 2>/dev/null
|
||||
if git diff --cached --quiet 2>/dev/null; then
|
||||
echo "[daily] no output changes to publish"
|
||||
elif git commit -q -m "live: daily refresh — skilled + sharp wallets [skip ci]"; then
|
||||
|
||||
+52
-32
@@ -1,53 +1,73 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Recompute the copy bot's per-wallet p80 conviction floors from the cache.
|
||||
"""Pin the live paper bot's per-wallet conviction floors to the backtest's.
|
||||
|
||||
Keeps config.json's `follow.per_wallet_min_usd` in exact parity with the
|
||||
dashboard's "top 20% by stake" gate as the watched wallets keep trading — using
|
||||
the same cache.conv_cutoff() (p80) over each wallet's own bet sizes that the
|
||||
pipeline and trading/index.html use. Only the *floors* are rewritten; the
|
||||
curated watchlist itself is never touched.
|
||||
PARITY FIX (2026-07-07): the bot used to derive floors AT BOOT from the
|
||||
data-api's most recent ~500 positions (copybot.derive_floor). The backtest
|
||||
(portfolio.py) uses the TRUSTED cache p80 over ~180 days. When a wallet's
|
||||
recent bets ran bigger than its long-term norm the two diverged hard —
|
||||
fortuneking's boot floor hit $1,498 vs the backtest's $892, so the live bot
|
||||
silently filtered out conviction bets the backtest counted (and the floor
|
||||
drifted every restart). This writes the backtest's floor as a PINNED
|
||||
`floor` on every wallet in copybot.paper.json, so both books gate on the
|
||||
same threshold and boots are deterministic.
|
||||
|
||||
Reads the wallet set from config.json's "watchlist" (falls back to "watch"), so
|
||||
if you re-curate the portfolio the floors follow automatically. config.json is
|
||||
gitignored, so this stays local and is never committed.
|
||||
Uses the exact backtest method: trusted_wallet_rows (trust.py) deduped to
|
||||
one row per market (largest stake), then cache.conv_cutoff (p80). Wired into
|
||||
daily.sh after portfolio.py (cache warm, floors match that run); the publish
|
||||
step commits copybot.paper.json so the next bot restart picks them up.
|
||||
|
||||
python3 sync_floors.py # run standalone; also wired into daily.sh
|
||||
python3 sync_floors.py # writes live/copybot.paper.json floors
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import cache # noqa: E402 — local bet cache + conv_cutoff (p80)
|
||||
import cache # noqa: E402
|
||||
import trust # noqa: E402
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
CFG = os.path.join(HERE, "..", "config.json")
|
||||
PAPER = os.path.join(HERE, "copybot.paper.json")
|
||||
|
||||
|
||||
def trusted_p80(wallet, now=None):
|
||||
"""The wallet's conviction floor = p80 of its TRUSTED bet stakes, one row
|
||||
per market (largest stake) — identical to portfolio.py's conv_thr."""
|
||||
now = int(now or time.time())
|
||||
rows = trust.trusted_wallet_rows(cache.query, wallet, now)
|
||||
best = {}
|
||||
for cond, asset, won, p, res_t, size in rows:
|
||||
if cond not in best or size > best[cond]:
|
||||
best[cond] = size
|
||||
return cache.conv_cutoff(best.values())
|
||||
|
||||
|
||||
def main():
|
||||
if not os.path.exists(CFG):
|
||||
print("[floors] no ../config.json — nothing to do")
|
||||
cfg = json.load(open(PAPER))
|
||||
ws = cfg.get("wallets") or []
|
||||
if not ws:
|
||||
print("[floors] copybot.paper.json has no wallets — nothing to do")
|
||||
return
|
||||
cfg = json.load(open(CFG))
|
||||
wallets = cfg.get("watchlist") or [w["wallet"] for w in cfg.get("watch", [])]
|
||||
if not wallets:
|
||||
print("[floors] config has no watchlist — nothing to do")
|
||||
return
|
||||
|
||||
floors = {}
|
||||
for w in wallets:
|
||||
sizes = [b["size"] for b in cache.get_bets(w) if b.get("size")]
|
||||
p80 = cache.conv_cutoff(sizes) # the dashboard's top-20% threshold
|
||||
if p80 != float("inf"): # skip wallets with no sized bets
|
||||
floors[w.lower()] = round(p80, 2)
|
||||
|
||||
cfg.setdefault("follow", {})["per_wallet_min_usd"] = floors
|
||||
tmp = CFG + ".tmp"
|
||||
trust.ensure_cons(cache.query)
|
||||
now = int(time.time())
|
||||
changed = []
|
||||
for w in ws:
|
||||
p80 = trusted_p80(w["wallet"], now)
|
||||
if p80 == float("inf"):
|
||||
continue # no trusted sized bets — leave as-is
|
||||
new = round(p80, 2)
|
||||
old = w.get("floor")
|
||||
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, CFG) # atomic write
|
||||
print(f"[floors] {len(floors)} p80 conviction floors updated: " +
|
||||
", ".join(f"{w[:8]}…=${v:g}" for w, v in floors.items()))
|
||||
os.replace(tmp, PAPER)
|
||||
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)"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user