live bot: record + settle missed bets, publish in feed

Engine records every blocked OPEN (no free cash, event cap, price guard) with
its would-be stake; settle_resolved marks them won/lost at CLOB resolution with
hypothetical P&L (fee-inclusive) - the live counterpart of the backtest's
Missed table. Feed gains missed[] + missed_pnl.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jaxperro
2026-07-02 11:03:32 -04:00
parent c4709df425
commit b398e5d682
2 changed files with 49 additions and 2 deletions
+26 -2
View File
@@ -278,6 +278,21 @@ class CopyTrader:
frac *= self.DD_FACTOR
return frac * eq
def record_miss(self, wallet, token, cond, title, outcome, price, want, reason):
"""A bet the strategy WOULD have copied but the book couldn't take —
free cash gone, event cap, price drifted past the guard. Kept in state so
the live feed can show missed bets and (once the market resolves) their
would-be P&L — the live counterpart of the backtest's Missed table."""
missed = self.state.setdefault("missed", [])
if any(m["token"] == token and m["status"] == "open" for m in missed):
return # already recorded while open
missed.append({"ts": int(time.time()), "wallet": wallet, "token": token,
"cond": cond, "title": title, "outcome": outcome,
"price": round(price or 0, 4), "stake": round(want, 2),
"reason": reason, "status": "open", "pnl": None,
"settled": None})
del missed[:-200] # keep the recent 200
def persist(self):
self.state["seen_tx"] = list(self.seen)[-5000:]
save_json(self.state_path, self.state)
@@ -330,7 +345,8 @@ class CopyTrader:
if side == "BUY":
self._handle_their_buy(wallet, token, their_size, their_price,
label, title, outcome, event=event_key(t))
label, title, outcome, event=event_key(t),
cond=t.get("conditionId"))
their_book[token] = their_prev + their_size
elif side == "SELL":
self._handle_their_sell(token, their_size, their_prev, label)
@@ -352,7 +368,7 @@ class CopyTrader:
return drift <= self.cfg["price_guard_pct"]
def _handle_their_buy(self, wallet, token, their_size, their_price,
label, title, outcome, event=None):
label, title, outcome, event=None, cond=None):
mine = self.state["my_pos"].get(token)
is_add = mine is not None
# don't backfill: never open a position they already held when we
@@ -371,6 +387,8 @@ class CopyTrader:
if held >= cap:
self.log(f"BUY {label} — skip (already {held} positions on this "
f"event, cap {cap})")
self.record_miss(wallet, token, cond, title, outcome, their_price,
self.stake_usd(), f"event cap ({held} held)")
return
price = self._live_price(token, "buy")
@@ -379,6 +397,9 @@ class CopyTrader:
if not self._price_guard_ok(price, their_price):
self.log(f"BUY {label} — skip (price {price:.3f} vs their "
f"{their_price:.3f}, >{self.cfg['price_guard_pct']:.0%})")
self.record_miss(wallet, token, cond, title, outcome, price,
self.stake_usd(),
f"price moved {their_price:.2f}{price:.2f}")
return
if is_add:
@@ -396,6 +417,9 @@ class CopyTrader:
allowed, reason = self.gate_buy(want_usd, price, pos_cost)
if reason:
self.log(f"{kind} {label} — skip ({reason})")
if not is_add: # a blocked OPEN is a missed bet
self.record_miss(wallet, token, cond, title, outcome, price,
want_usd, reason)
return
shares = allowed / price
res = self.ex.buy(token, shares, price, {"title": title})
+23
View File
@@ -375,6 +375,10 @@ class Copybot:
exp = self.engine.open_exposure()
cash = st.get("cash", bank)
lag = st.get("lag", {})
missed = st.get("missed", [])
for m in missed: # display names for the feed
m["name"] = self.names.get((m.get("wallet") or "").lower(),
(m.get("wallet") or "")[:10])
feed = {
"mode": "live" if self.engine.ex.live else "paper",
"bankroll": bank, "stake": round(self.engine.stake_usd(), 2),
@@ -392,6 +396,11 @@ class Copybot:
"bets": sorted(bets.values(),
key=lambda b: b.get("settled") or b.get("opened") or 0,
reverse=True)[:100],
"missed": sorted(missed,
key=lambda m: m.get("settled") or m.get("ts") or 0,
reverse=True)[:60],
"missed_pnl": round(sum(m["pnl"] for m in missed
if m.get("pnl") is not None), 2),
}
# only (re)write — and so only commit — when the meaningful content changed,
# not on every poll. The "updated" stamp advances only on real change, so the
@@ -566,6 +575,20 @@ class Copybot:
discord_text=(f"🏁 **SETTLE** {tag}\n{label}\n"
f"${pos['cost']:.2f} cost -> ${proceeds:.2f} "
f"= **${pnl:+.2f}**"))
# settle MISSED bets hypothetically: what the skipped stake would have
# returned (entry fee included; redeem free) — the live counterpart of
# the backtest's Missed P&L, "the cost of a small bankroll".
for m in self.engine.state.get("missed", []):
if m.get("status") != "open" or not m.get("cond"):
continue
wp = resolution_price(m["token"], m["cond"], m.get("outcome"))
if wp is None:
continue
p = m.get("price") or 0.5
fee = taker_fee(m["stake"] / p, p, self.fee_rate)
pnl = (m["stake"] / p) * wp - m["stake"] - fee
m.update(status=("won" if wp >= 0.5 else "lost"),
pnl=round(pnl, 2), settled=int(time.time()))
self.engine.persist()