mirror of
https://github.com/jaxperro/winning-wallet-finder.git
synced 2026-08-02 02:37:46 +00:00
copybot: enrich metadata-less RTDS seeds + repair cond-less books each settle pass (closes #18)
Root cause of the 07-20 CASH≠CHAIN -$1.75: some markets stream RTDS trades with empty title/outcome/conditionId; the seed stored them verbatim, on_wallet_activity's 'tok not in self.conds' guard made the empty value sticky, and settle_resolved's 'if not cond' skip left the position silently unsettleable until the venue auto-redeemed it. Three-layer fix: - RTDS seed: when the payload lacks conditionId/title, enrich from the token id via _token_market (gamma, cached) before dispatch; the copy still goes through on a miss. - conds write: falsy-overwrite instead of key-presence, so a later, richer source repairs an empty entry. - repair_market_meta (called at the top of every settle pass, holding the lock): re-resolves empty conds for my_pos AND open missed entries, backfills position/bet/missed records, 300s per-token lookup backoff, and a once-per-token alarm when a position has sat unsettleable >1h. A repaired token settles in the SAME pass — the Odyssey bet would have self-healed the moment the market resolved. The paper book currently carries 4 empty conds and both books have cond-less open missed entries — the repair pass fixes those on the first cycle after deploy. tests/test_meta_repair.py covers backfill, backoff, the single alarm, recovery, and the end-to-end repair->settle un-stick; full suite 8/8. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+5
-3
@@ -24,8 +24,9 @@ Builder API Key (SDK gasless relay) the runtime bot deliberately lacks —
|
||||
LOW urgency, the platform auto-redeems winners itself. auto_redeem OFF.
|
||||
Open now: #1 Signal A, #2 tape research, #4 redeem builder-key, #13 Fri
|
||||
bench, #14 edge verdict, #16 surge momentum (forward window OPEN), #17
|
||||
oracle fair value (accumulating, nothing frozen), #18 empty-cond copies
|
||||
are unsettleable (2026-07-20 CASH≠CHAIN post-mortem).
|
||||
oracle fair value (accumulating, nothing frozen). #18 (empty-cond copies
|
||||
unsettleable) closed same day: RTDS seed enrichment + falsy-cond repair
|
||||
pass + 1h alarm.
|
||||
|
||||
## Operating boundary (user, 2026-07-13 — standing)
|
||||
**Full autonomy on the bots**; the real-money bot **stays ARMED**. Never
|
||||
@@ -42,7 +43,8 @@ push through.
|
||||
6-wallet Set E rev 4, 4% of working equity/bet, alarm-free after the
|
||||
07-20 reconcile (CASH≠CHAIN −$1.75: empty-cond Odyssey bet the venue
|
||||
auto-redeemed → hand-settled won +$0.68, $0.05 rounding folded; root
|
||||
cause → #18). **wwf-copybot** (paper $1k): same set, FAK-parity fills.
|
||||
cause FIXED same day, #18: RTDS metadata-less rows now enrich at seed,
|
||||
repair_market_meta un-sticks cond-less books each settle pass). **wwf-copybot** (paper $1k): same set, FAK-parity fills.
|
||||
Both on the audit-hardened build (locks, chain-gated sweep, boot-id
|
||||
single-writer guard, TLS'd user-ws — HANDOFF_ARCHIVE rev 16). 2026-07-20:
|
||||
FAK no-match OPENs get one re-quote retry on both bots, PER-NICHE waits
|
||||
|
||||
+63
-1
@@ -778,6 +778,16 @@ class RtdsListener:
|
||||
"outcome": p.get("outcome"), "conditionId": p.get("conditionId"),
|
||||
"eventSlug": p.get("eventSlug") or p.get("slug"),
|
||||
"timestamp": int(ts) if ts else None}
|
||||
# #18: some markets stream with empty title/outcome/conditionId (the
|
||||
# 07-20 Odyssey copy) — a cond-less book entry is UNSETTLEABLE. Enrich
|
||||
# from the token id (gamma, cached) before seeding; on a miss the copy
|
||||
# still goes through and repair_market_meta backfills later.
|
||||
if seed["asset"] and not (seed["conditionId"] and seed["title"]):
|
||||
meta = _token_market(seed["asset"])
|
||||
if meta:
|
||||
seed["title"] = seed["title"] or meta[0]
|
||||
seed["outcome"] = seed["outcome"] or meta[1]
|
||||
seed["conditionId"] = seed["conditionId"] or meta[2]
|
||||
try: # same funnel as the Alchemy push —
|
||||
self.bot.on_wallet_activity(w, seed_trade=seed) # locks internally
|
||||
except Exception as e:
|
||||
@@ -919,6 +929,10 @@ class Copybot:
|
||||
self.shadow_log = cfg.get("shadow_log", "rtds_shadow.jsonl")
|
||||
# persisted across restarts via the engine's state file
|
||||
self.conds = engine.state.setdefault("conds", {}) # token_id -> conditionId (open positions)
|
||||
# #18 empty-cond repair: in-memory backoff/alarm bookkeeping (a reboot
|
||||
# restarts the 1h clock — fine, the repair itself is what matters)
|
||||
self._meta_fail = {} # token -> (first_fail_ts, last_attempt_ts)
|
||||
self._meta_warned = set() # tokens already alarmed as unsettleable
|
||||
engine.state.setdefault("cash", cfg["bankroll_usd"]) # free cash (recycles on sell/resolution)
|
||||
engine.state.setdefault("lag", {"n": 0, "sum_s": 0.0, "sum_slip_pct": 0.0})
|
||||
engine.state.setdefault("fees_paid", 0.0)
|
||||
@@ -2096,7 +2110,9 @@ class Copybot:
|
||||
self.engine.handle_trade(wallet, t) # sizes, gates, places (paper/live)
|
||||
self.engine.seen.update(extras) # component fills of the merged bet
|
||||
tok = t.get("asset")
|
||||
if tok in self.engine.state["my_pos"] and tok not in self.conds:
|
||||
# falsy-overwrite, not key-presence (#18): a seed that stored
|
||||
# cond="" must not block the indexer's richer row from fixing it
|
||||
if tok in self.engine.state["my_pos"] and not self.conds.get(tok):
|
||||
self.conds[tok] = t.get("conditionId") # remember for settling
|
||||
for f in self._drain_fills():
|
||||
if f["token"] == tok: # the fill from this copy
|
||||
@@ -2371,6 +2387,51 @@ class Copybot:
|
||||
f"hypothetical ${m['pnl']:+.2f} (sold)")
|
||||
self.engine.persist()
|
||||
|
||||
META_RETRY_S = 300 # per-token backoff between failed lookups
|
||||
META_WARN_S = 3600 # empty-cond position older than this alarms
|
||||
|
||||
def repair_market_meta(self):
|
||||
"""#18: a copy seeded from a metadata-less RTDS row books with
|
||||
title/outcome/cond == "" and settle_resolved's `if not cond` skip makes
|
||||
it silently UNSETTLEABLE — the 07-20 Odyssey bet sat open while the
|
||||
venue auto-redeemed it on-chain, alarming CASH≠CHAIN -$1.75 until
|
||||
manual state surgery. Each settle pass: re-resolve empty conds from
|
||||
the token id (gamma, cached), backfill the position/bet/missed
|
||||
records, and alarm ONCE per token still unsettleable after an hour.
|
||||
Caller holds self.lock (settle_resolved)."""
|
||||
st = self.engine.state
|
||||
now = time.time()
|
||||
empties = [(t, p) for t, p in st["my_pos"].items() if not self.conds.get(t)]
|
||||
for m in st.get("missed", []): # cond-less misses skip hypothetical
|
||||
if m.get("status") == "open" and not m.get("cond") and m.get("token"):
|
||||
empties.append((m["token"], m))
|
||||
for tok, rec in empties:
|
||||
first, last = self._meta_fail.get(tok, (now, 0.0))
|
||||
if now - last >= self.META_RETRY_S:
|
||||
meta = _token_market(tok)
|
||||
if meta and meta[2]:
|
||||
self._meta_fail.pop(tok, None)
|
||||
self._meta_warned.discard(tok)
|
||||
b = st.get("bets", {}).get(tok)
|
||||
for d in (rec, b) if b is not None else (rec,):
|
||||
d["title"] = d.get("title") or meta[0]
|
||||
d["outcome"] = d.get("outcome") or meta[1]
|
||||
if "cond" in d or d is rec:
|
||||
d["cond"] = d.get("cond") or meta[2]
|
||||
if tok in st["my_pos"]:
|
||||
self.conds[tok] = meta[2]
|
||||
log(f"meta repair (#18): …{tok[-8:]} -> "
|
||||
f"{str(meta[0])[:40]} · {meta[1]}")
|
||||
continue
|
||||
self._meta_fail[tok] = (first, now)
|
||||
if (tok in st["my_pos"] and now - first > self.META_WARN_S
|
||||
and tok not in self._meta_warned):
|
||||
self._meta_warned.add(tok)
|
||||
self.engine.alert(
|
||||
f"⚠ …{tok[-8:]} ({str(rec.get('title') or '?')[:36]}) has had "
|
||||
f"no condition id for {(now - first) / 3600:.1f}h — "
|
||||
f"UNSETTLEABLE until metadata resolves (#18)")
|
||||
|
||||
def settle_resolved(self):
|
||||
# audit 4: this ran on EVERY webhook POST ahead of the copy itself —
|
||||
# dozens of redundant CLOB fetches during clip bursts. 20s throttle;
|
||||
@@ -2383,6 +2444,7 @@ class Copybot:
|
||||
realized P&L. This is the resolution path the engine's sell-only mirror
|
||||
lacks — without it the $1k never recycles for held-to-resolution bets."""
|
||||
with self.lock:
|
||||
self.repair_market_meta() # #18: un-stick cond-less books first
|
||||
mp = self.engine.state["my_pos"]
|
||||
for token in list(mp):
|
||||
cond = self.conds.get(token)
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
"""Stub tests for repair_market_meta (#18): a copy seeded from a
|
||||
metadata-less RTDS row books title/outcome/cond == "" and can never settle —
|
||||
the 2026-07-20 Odyssey bet sat open while the venue auto-redeemed it,
|
||||
alarming CASH≠CHAIN until manual surgery. Covers: backfill of my_pos + bets
|
||||
+ missed records, the falsy-cond overwrite, lookup-failure backoff, the 1h
|
||||
unsettleable alarm (once), and the end-to-end un-stick (repair -> same-pass
|
||||
settle). Run: python3 tests/test_meta_repair.py — no network, no config.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
||||
import copybot # noqa: E402
|
||||
|
||||
TOK = "35358889366059131445607416132477521541386270328042995107702816540034073998102"
|
||||
MISS_TOK = "111"
|
||||
COND = "0xd2a97dd956e70c9c4abda30d2864588ece0c7213013eb3fa664b88509ff829b1"
|
||||
TITLE = 'Will "The Odyssey" Opening Weekend Box Office be greater than 115m?'
|
||||
|
||||
|
||||
class Ex:
|
||||
live = False
|
||||
|
||||
|
||||
class Eng:
|
||||
def __init__(self, st):
|
||||
self.state, self.ex, self.alerts = st, Ex(), []
|
||||
|
||||
def persist(self):
|
||||
pass
|
||||
|
||||
def alert(self, m, discord_text=None):
|
||||
self.alerts.append(m)
|
||||
|
||||
|
||||
def mkbot(st):
|
||||
bot = copybot.Copybot.__new__(copybot.Copybot)
|
||||
bot.engine = Eng(st)
|
||||
bot.lock = threading.Lock()
|
||||
bot.redeemer = None
|
||||
bot.fee_rate = 0.03
|
||||
bot.conds = st.setdefault("conds", {})
|
||||
bot._meta_fail = {}
|
||||
bot._meta_warned = set()
|
||||
return bot
|
||||
|
||||
|
||||
def fresh_state():
|
||||
return {
|
||||
"cash": 10.0, "adjustments": [],
|
||||
"my_pos": {TOK: {"shares": 1.694914, "cost": 1.0, "title": "",
|
||||
"outcome": "", "event": None, "wallet": "0xw",
|
||||
"cond": ""}},
|
||||
"bets": {TOK: {"token": TOK, "wallet": "0xw", "name": "Bikes",
|
||||
"outcome": "", "title": "", "shares": 1.69,
|
||||
"cost": 1.0, "fee": 0.0123, "opened": 1,
|
||||
"status": "open", "exit_price": None, "pnl": None,
|
||||
"settled": None}},
|
||||
"missed": [{"token": MISS_TOK, "cond": "", "status": "open",
|
||||
"outcome": "", "price": 0.5, "stake": 2.0}],
|
||||
"conds": {TOK: ""},
|
||||
}
|
||||
|
||||
|
||||
calls = []
|
||||
|
||||
|
||||
def meta_ok(token):
|
||||
calls.append(token)
|
||||
return (TITLE, "Yes", COND) if token == TOK else ("Miss mkt", "No", "0xm")
|
||||
|
||||
|
||||
def meta_fail(token):
|
||||
calls.append(token)
|
||||
return None
|
||||
|
||||
|
||||
fails = []
|
||||
|
||||
# 1) repair backfills my_pos + bets + conds + missed, and logs no alarm
|
||||
copybot._token_market = meta_ok
|
||||
st = fresh_state()
|
||||
bot = mkbot(st)
|
||||
bot.repair_market_meta()
|
||||
if st["conds"][TOK] != COND:
|
||||
fails.append(f"conds not repaired: {st['conds'][TOK]!r}")
|
||||
if st["my_pos"][TOK]["title"] != TITLE or st["my_pos"][TOK]["cond"] != COND:
|
||||
fails.append(f"my_pos not backfilled: {st['my_pos'][TOK]}")
|
||||
if st["bets"][TOK]["title"] != TITLE or st["bets"][TOK]["outcome"] != "Yes":
|
||||
fails.append(f"bets not backfilled: {st['bets'][TOK]}")
|
||||
if st["missed"][0]["cond"] != "0xm":
|
||||
fails.append(f"missed not repaired: {st['missed'][0]}")
|
||||
if MISS_TOK in st["conds"]:
|
||||
fails.append("missed token leaked into conds (conds is open positions only)")
|
||||
if bot.engine.alerts:
|
||||
fails.append(f"unexpected alarm: {bot.engine.alerts}")
|
||||
|
||||
# 2) lookup failure: tracked, backed off, no crash; second pass inside the
|
||||
# retry window must NOT re-call the API
|
||||
copybot._token_market = meta_fail
|
||||
st = fresh_state()
|
||||
bot = mkbot(st)
|
||||
calls.clear()
|
||||
bot.repair_market_meta()
|
||||
n_first = len(calls)
|
||||
bot.repair_market_meta() # immediate second pass -> backoff, no calls
|
||||
if n_first != 2: # TOK + MISS_TOK
|
||||
fails.append(f"expected 2 lookup calls, got {n_first}")
|
||||
if len(calls) != n_first:
|
||||
fails.append(f"backoff violated: {len(calls) - n_first} extra calls")
|
||||
if st["conds"][TOK] != "":
|
||||
fails.append("failed lookup should leave cond empty")
|
||||
|
||||
# 3) 1h-old failure alarms ONCE (my_pos only, never for missed)
|
||||
now = time.time()
|
||||
bot._meta_fail[TOK] = (now - 3700, now) # old failure, recent attempt
|
||||
bot._meta_fail[MISS_TOK] = (now - 3700, now)
|
||||
bot.repair_market_meta()
|
||||
bot.repair_market_meta()
|
||||
alarms = [a for a in bot.engine.alerts if "UNSETTLEABLE" in a]
|
||||
if len(alarms) != 1:
|
||||
fails.append(f"expected exactly 1 unsettleable alarm, got {bot.engine.alerts}")
|
||||
|
||||
# 4) recovery after failure: lookup succeeds once the market appears -> alarm
|
||||
# bookkeeping cleared
|
||||
copybot._token_market = meta_ok
|
||||
bot._meta_fail[TOK] = (now - 3700, 0.0) # out of the retry window
|
||||
bot.repair_market_meta()
|
||||
if st["conds"][TOK] != COND or TOK in bot._meta_fail or TOK in bot._meta_warned:
|
||||
fails.append("recovery did not clear fail/warned bookkeeping")
|
||||
|
||||
# 5) end-to-end un-stick: repaired cond lets the SAME settle_resolved pass
|
||||
# settle the bet (this is the exact Odyssey failure, inverted)
|
||||
copybot._token_market = meta_ok
|
||||
copybot.resolution_price = lambda token, cond, outcome=None: 1.0
|
||||
st = fresh_state()
|
||||
bot = mkbot(st)
|
||||
bot._settle_last = 0
|
||||
bot.settle_resolved()
|
||||
b = st["bets"][TOK]
|
||||
if b["status"] != "won" or b["pnl"] is None or TOK in st["my_pos"]:
|
||||
fails.append(f"repair+settle did not un-stick the bet: {b}")
|
||||
if abs(st["cash"] - 11.694914) > 1e-9:
|
||||
fails.append(f"settle cash wrong: {st['cash']}")
|
||||
|
||||
print("FAILURES:", fails or "none")
|
||||
sys.exit(1 if fails else 0)
|
||||
Reference in New Issue
Block a user