copybot: feed reconciles my_pos -> bets so open_count matches the visible table

A position can land in my_pos without a bet record when its buy fill is
drained in a context where _record_lag doesn't fire (e.g. a second market on
the same event in one batch): cash-correct (open_count/deployed read my_pos)
but INVISIBLE in the feed table — the header showed '2 open / $75 deployed'
while only 1 bet rendered. write_feed now synthesizes a bet record from any
open my_pos position lacking one, so the count always equals what's shown.
Ledger was unaffected (drift -0.0) — display-only. my_pos entries now carry
'wallet' for proper name attribution on the synthesized record.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jaxperro
2026-07-07 21:05:47 -04:00
parent b2a4a03d5b
commit 7b1e86685a
2 changed files with 24 additions and 1 deletions
+2 -1
View File
@@ -486,7 +486,8 @@ class CopyTrader:
else:
self.state["my_pos"][token] = {
"shares": res["filled_shares"], "cost": spent,
"title": title, "outcome": outcome, "event": event}
"title": title, "outcome": outcome, "event": event,
"wallet": wallet} # attribution for the feed's my_pos->bets safety net
tag = "[PAPER]" if not self.ex.live else "[LIVE]"
self.alert(
f"{kind} {label}{tag} buy {res['filled_shares']:.1f} "
+22
View File
@@ -499,6 +499,28 @@ class Copybot:
st = self.engine.state
bets = st.setdefault("bets", {})
mp = st["my_pos"]
# my_pos -> bets: a position can land in my_pos without a bet record when
# its fill was drained in a context where _record_lag didn't fire (e.g. a
# second market on the same event processed in one batch). It's cash-
# correct (open_exposure/open_count read my_pos) but INVISIBLE in the feed
# table, so the header showed "2 open" while the table showed 1. Synthesize
# the missing record from my_pos so the counts always match what's shown.
for tok, p in mp.items():
b = bets.get(tok)
if not b or b.get("status") != "open":
bets[tok] = {
"token": tok, "wallet": p.get("wallet", ""),
"name": self.names.get((p.get("wallet") or "").lower())
or (b or {}).get("name") or "?",
"outcome": p.get("outcome"), "title": (p.get("title") or "")[:90],
"their_price": None,
"my_price": round(p["cost"] / p["shares"], 4) if p.get("shares") else None,
"slippage_pct": None,
"shares": round(p["shares"], 2), "cost": round(p["cost"], 2),
"fee": (b or {}).get("fee", 0),
"opened": (b or {}).get("opened") or int(time.time()), "status": "open",
"exit_price": None, "pnl": None, "settled": None,
}
for tok, b in bets.items():
if b["status"] == "open" and tok not in mp:
b["status"] = "closed"