sharps stats mirror exits too (SOLD) — one exit model across all three books

smart_money.closed_exits() is now the shared implementation (backtest +
sharps): close time from /closed-positions, exit price reconstructed from
realized P&L. validate_timing's conv/conv30/all-time/realized tallies count
a bet the wallet sold pre-resolution at its exit price (conv_sold /
conv30_sold / all_sold fields) instead of pretending it rode to resolution.
Beyond the ~4000-row data horizon exits fall back to hold-to-res.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jaxperro
2026-07-07 03:18:41 -04:00
parent 44a08cdc0d
commit 72245e4053
3 changed files with 63 additions and 43 deletions
+6 -28
View File
@@ -244,34 +244,12 @@ def window_bets():
return out
def closed_positions(wallet, max_rows=4000):
"""{asset: {ts, exit_p, p, iv, cond, title, outcome}} for the wallet's
FULLY-CLOSED positions with an in-window close time. `ts` is the close
(sell/redeem) timestamp — the same field the cache stores as sell-time.
Exit price is reconstructed from realized P&L over shares bought:
exit_p = avgPrice + realizedPnl / totalBought
(exact for a full single-price exit, share-weighted otherwise). Beyond
max_rows of history the wallet's older exits fall back to hold-to-
resolution — a data-horizon ceiling, counted honest by construction."""
out = {}
for off in range(0, max_rows, 500):
page = sm.get_json("/closed-positions",
{"user": wallet, "limit": 500, "offset": off,
"sortBy": "TIMESTAMP", "sortDirection": "DESC"}) or []
for r in page:
ts = r.get("timestamp") or 0
tb = r.get("totalBought") or 0
avg = r.get("avgPrice") or 0
if not (r.get("asset") and ts and tb and avg):
continue
exit_p = max(0.001, min(0.999, avg + (r.get("realizedPnl") or 0) / tb))
out.setdefault(r["asset"], {
"ts": ts, "exit_p": exit_p, "p": max(0.001, min(0.999, avg)),
"iv": r.get("initialValue") or avg * tb, "cond": r.get("conditionId"),
"title": r.get("title") or "", "outcome": r.get("outcome") or ""})
if len(page) < 500 or (page and (page[-1].get("timestamp") or 0) < START):
break
return out
def closed_positions(wallet):
"""The wallet's fully-closed positions with in-window close times —
shared implementation in smart_money.closed_exits (validate_timing uses
the same one, so the backtest and the sharps stats mirror exits
identically)."""
return sm.closed_exits(wallet, since_ts=START)
def open_bets():
+26 -15
View File
@@ -142,18 +142,27 @@ def display_stats(w):
payouts.ensure({b["cond"] for b in bets} | {r[0] for r in trows})
# ---- ALL-TIME stats over EVERY trusted bet (any size): the dashboard's
# "of every bet placed" columns. Trusted rows only, deduped one-per-market,
# truth-adjusted: refunds (wp=0.5) count as neither won nor lost. ----
# truth-adjusted: refunds (wp=0.5) count as neither won nor lost, and a bet
# the wallet SOLD pre-resolution counts at its exit price (status SOLD) —
# the same exit-mirroring the backtest and live bot use. Exits beyond the
# closed-positions data horizon (~4000 rows) fall back to hold-to-res. ----
exits = sm.closed_exits(w)
tbest = {}
for cond, asset, won, p, res_t, size in trows:
if cond not in tbest or size > tbest[cond][3]:
tbest[cond] = (cond, asset, won, p, size)
tbest[cond] = (cond, asset, won, p, size, res_t)
def tally(rows):
"""(won, lost, refunds, pnl) over (cond, asset, won, p, size) rows."""
w_ = l_ = r_ = 0
"""(won, lost, refunds, sold, pnl) over (cond, asset, won, p, size, res_t)."""
w_ = l_ = r_ = s_ = 0
pnl = 0.0
for cond, asset, won, p, size in rows:
wp = _wp(cond, asset, won)
for cond, asset, won, p, size, res_t in rows:
pc = max(0.001, min(0.999, p or 0))
cx = exits.get(asset)
if cx and res_t and cx["ts"] < res_t - 300: # sold BEFORE resolution
pnl += size * (cx["exit_p"] - pc) / pc
s_ += 1
continue
wp = _wp(cond, asset, won)
pnl += size * (wp - pc) / pc
if wp > 0.5:
w_ += 1
@@ -161,26 +170,28 @@ def display_stats(w):
l_ += 1
else:
r_ += 1
return w_, l_, r_, pnl
all_won, all_lost, all_ref, all_pnl = tally(tbest.values())
return w_, l_, r_, s_, pnl
all_won, all_lost, all_ref, all_sold, all_pnl = tally(tbest.values())
thr = cache.conv_cutoff(b["size"] for b in bets)
conv = [b for b in bets if b["size"] >= thr]
recent = sorted(bets, key=lambda b: b["res_t"] or 0, reverse=True)[:500]
cut30 = time.time() - 30 * 86400
conv30 = [b for b in conv if (b["res_t"] or 0) >= cut30]
brow = lambda bs: [(b["cond"], b.get("asset"), b["won"], b["p"], b["size"]) for b in bs]
cw, cl, cr, cpnl = tally(brow(conv))
c3w, c3l, c3r, c3pnl = tally(brow(conv30))
brow = lambda bs: [(b["cond"], b.get("asset"), b["won"], b["p"], b["size"], b["res_t"])
for b in bs]
cw, cl, cr, cs, cpnl = tally(brow(conv))
c3w, c3l, c3r, c3s, c3pnl = tally(brow(conv30))
out = {
"conv_win": round(100 * cw / (cw + cl), 1) if (cw + cl) else None,
"conv_won": cw, "conv_lost": cl, "conv_ref": cr,
"conv_won": cw, "conv_lost": cl, "conv_ref": cr, "conv_sold": cs,
"conv_pnl": round(cpnl),
"conv30_win": round(100 * c3w / (c3w + c3l), 1) if (c3w + c3l) else None,
"conv30_won": c3w, "conv30_lost": c3l, "conv30_ref": c3r,
"conv30_won": c3w, "conv30_lost": c3l, "conv30_ref": c3r, "conv30_sold": c3s,
"conv30_pnl": round(c3pnl),
"realized_pnl": round(tally(brow(recent))[3]),
"realized_pnl": round(tally(brow(recent))[4]),
"all_win": round(100 * all_won / (all_won + all_lost), 1) if (all_won + all_lost) else None,
"all_won": all_won, "all_lost": all_lost, "all_ref": all_ref, "all_pnl": round(all_pnl),
"all_won": all_won, "all_lost": all_lost, "all_ref": all_ref,
"all_sold": all_sold, "all_pnl": round(all_pnl),
"pm_pnl": _pm_profit(w),
"avg_bet": round(sum(b["size"] for b in conv) / len(conv)) if conv else 0,
"copy_pnl": 0, "held_pnl": 0, "held_won": 0, "held_lost": 0, "sold": 0,
+31
View File
@@ -107,6 +107,37 @@ def leaderboard_candidates(pool):
return ranked[:pool]
def closed_exits(wallet, since_ts=0, max_rows=4000):
"""{asset: {ts, exit_p, p, iv, cond, title, outcome}} for the wallet's
FULLY-CLOSED positions, newest first. `ts` is the close (sell/redeem)
timestamp; the exit price is reconstructed from realized P&L over shares
bought (exit_p = avgPrice + realizedPnl/totalBought exact for a full
single-price exit, share-weighted otherwise). Shared by the backtest
(portfolio.py) and the sharps stats (validate_timing.py) so both books
mirror the signal's exits identically. Beyond max_rows (or before
since_ts) history falls back to hold-to-resolution a data-horizon
ceiling, honest by construction."""
out = {}
for off in range(0, max_rows, 500):
page = get_json("/closed-positions",
{"user": wallet, "limit": 500, "offset": off,
"sortBy": "TIMESTAMP", "sortDirection": "DESC"}) or []
for r in page:
ts = r.get("timestamp") or 0
tb = r.get("totalBought") or 0
avg = r.get("avgPrice") or 0
if not (r.get("asset") and ts and tb and avg):
continue
exit_p = max(0.001, min(0.999, avg + (r.get("realizedPnl") or 0) / tb))
out.setdefault(r["asset"], {
"ts": ts, "exit_p": exit_p, "p": max(0.001, min(0.999, avg)),
"iv": r.get("initialValue") or avg * tb, "cond": r.get("conditionId"),
"title": r.get("title") or "", "outcome": r.get("outcome") or ""})
if len(page) < 500 or (page and (page[-1].get("timestamp") or 0) < since_ts):
break
return out
WIN_WINDOW_DAYS = 90 # measure win rate over resolved bets in this window