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:
+6
-28
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user