backtest: fold in-window abandoned losers into the replay (consistency with sharps)

The replay selected trusted conviction bets + sold round-trips + chain
settlement, but abandoned losers (bets held to a decided outcome, never
redeemed — /positions at curPrice 0, not in /closed-positions, not trusted
rows on operator-resolved markets) fell out entirely, reading optimistic.
resolved_unredeemed() now pulls them; window_bets folds in the in-window,
conviction-size ones at their decided outcome (loser pays 0). Same anti-
survivorship correction as the sharps table's _open_split.

Impact on the follow-set 30d backtest is ~0 ($16,255->$16,260): those 8
wallets are clean, AND abandoned-loser survivorship is an all-time
ACCUMULATION (oliman2: 1123 losers lifetime, only 2 in any 30d window), so a
30d period metric structurally can't surface much of it. The fix guarantees
no in-window loss is dropped and keeps the backtest consistent with the
all-time sharps view where survivorship actually lives. Verified the fold-in
fires (oliman2's 2 in-window conviction losers identified).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jaxperro
2026-07-07 21:44:52 -04:00
parent c647e569c0
commit 09e511cc2e
2 changed files with 61 additions and 1 deletions
+1 -1
View File
File diff suppressed because one or more lines are too long
+60
View File
@@ -235,6 +235,25 @@ def window_bets():
"won": None, "res_t": 0,
"exit_t": cx["ts"], "exit_p": cx["exit_p"],
"title": cx.get("title") or ""})
# abandoned losers/winners: conviction bets entered in-window that the
# signal held to a decided outcome and never redeemed (curPrice 0/1 in
# /positions). Not trusted rows, not in /closed-positions -> the replay
# missed them and read optimistic. Settle at the decided outcome (a loser
# pays 0 = full loss), same fold-in as the sharps table.
for asset, r in resolved_unredeemed(w["wallet"]).items():
cond = r["cond"]
if cond in best or not cond:
continue # already a trusted row / round trip
if (r["iv"] or 0) < thr or r["p"] > MAX_ENTRY:
continue
et = ent.get(cond)
if not et or et < START:
continue
best[cond] = None
out.append({"wallet": w["wallet"], "name": w["name"], "cond": cond,
"asset": asset, "cls": w.get("class", "volume"),
"their": r["iv"], "entry_t": et, "p": r["p"],
"won": r["curp"] >= 0.5, "res_t": r["res_t"]})
# chain-truth payouts for the replayed markets: refunds pay 0.5/share, and
# a cache `won` mark can be wrong on operator-resolved markets — the
# replay must settle at what a redeem actually pays (see payouts.py)
@@ -251,6 +270,47 @@ def closed_positions(wallet):
return cache.closed_exits(wallet)
def _end_ts(s):
if not s:
return 0
s = s.replace("Z", "")
for fmt in ("%Y-%m-%dT%H:%M:%S", "%Y-%m-%d"):
try:
return int(time.mktime(time.strptime(s, fmt)))
except ValueError:
continue
return 0
def resolved_unredeemed(wallet):
"""Conviction bets the signal LOST (or won) and never redeemed — they sit in
/positions at curPrice pinned 0/1, are NOT in /closed-positions, and for
operator-resolved markets are NOT trusted rows either, so the replay never
saw them. Mostly ABANDONED LOSERS. Folding them in is the same anti-
survivorship correction the sharps table uses (_open_split) — without it the
backtest silently drops these losses and reads optimistic.
{asset: {cond, iv, p, curp, res_t}}."""
out = {}
for off in range(0, 100000, 50):
pg = sm.get_json("/positions", {"user": wallet, "limit": 50, "offset": off,
"sizeThreshold": 0})
if not pg:
break
for pos in pg:
cp = pos.get("curPrice", 0) or 0
asset = pos.get("asset")
if not asset or 0.001 < cp < 0.999: # genuinely open -> not decided
continue
out[asset] = {"cond": pos.get("conditionId"),
"iv": pos.get("initialValue") or 0,
"p": max(0.001, min(0.999, pos.get("avgPrice") or 0)),
"curp": cp,
"res_t": _end_ts(pos.get("endDate")) or int(time.time())}
if len(pg) < 50:
break
return out
def open_bets():
"""Currently-held conviction positions (live /positions pull, small) for the
'current bets' panel — the cache only has resolved bets."""