P&L: fold abandoned losers into realized (anti-survivorship) — All-Time is the truth, not PM

Resolved-but-unredeemed positions (bets that lost, went to $0, never
redeemed) were sitting in Open P&L, breaking the All-Time+Open=PM reconcile
and — worse — hiding real losses. _open_split now routes decided positions
(curPrice pinned 0/1) into the REALIZED total at their outcome, leaving only
genuinely in-flight positions in Open P&L. This is the project's founding
survivorship insight applied to P&L: PM /profit under-counts abandoned
losers unevenly, so where our All-Time reads lower than PM, PM is biased and
ours is honest. oliman2 $181k->$20k (walked from $161k of losers),
JuiceFarm $380k->$32k; clean wallets unchanged (ewww1 $409k, KBO30 ~PM).
Win% now counts abandoned losers too. Verified: Coteykens/imwalkinghere/KBO30
now ~PM; oliman2/JuiceFarm correctly far below their flattering PM.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jaxperro
2026-07-07 21:19:06 -04:00
parent c3e1c36562
commit c6ca8ad82c
2 changed files with 54 additions and 28 deletions
+19 -15
View File
@@ -290,21 +290,25 @@ runner is retired (GitHub throttled `*/5` to ~2h in practice — it copied 1 of
location doesn't relocate *you* (trade live from Colombia months, paper
from US months).
11. **All-Time / Conv P&L is the wallet's REALIZED track record** — the sum of
Polymarket's own `realizedPnl` per closed position over the wallet's full
history (`cache.closed_exits`, the incremental `/closed-positions` cache).
It's what a copier mirroring their buy/sell/hold actually banks, and it
equals the profile's **PM P&L** except for unrealized marks on positions
still open. This *replaced* the old hold-to-resolution reconstruction
(`won × entry × size`), which diverged from PM by up to 10× and even
flipped signs — four bugs: a 2,000-row pull cap (fixed: full history),
both-sides positions double-dropped by one-per-market dedup (fixed: each
asset is its own realized row), `initialValue = 0` mis-sizing (moot —
realized P&L needs no size), and corrupt near-epoch `res_t` (moot —
realized cash is timestamp-independent). Win % is now the share of closed
positions that *made money*, the mirror lens. (For wallets holding a large
losing open book, realized > PM because PM marks the open losers in;
that gap is unrealized, not an error.)
11. **All-Time / Conv P&L is the wallet's ANTI-SURVIVORSHIP realized track
record.** Every DECIDED position at its actual result: redeemed/sold closed
positions (Polymarket's own `realizedPnl`, `cache.closed_exits`) PLUS
resolved-but-unredeemed positions still sitting in `/positions` — bets that
already won/lost and the wallet never redeemed (mostly **abandoned losers**
at $0). `_open_split` in validate_timing pulls current positions and routes
those decided ones into the realized total (`cashPnl` = their outcome),
leaving only genuinely in-flight positions in the **Open P&L** column. This
is the whole point of the project surfacing: PM `/profit` **under-counts
abandoned losers**, unevenly — Coteykens' PM subtracts them (realized $66k
$52k walked-away = $14k = PM), oliman2's does not ($181k $161k = $20k
true, yet PM says $112k). So where our All-Time reads **lower** than PM, PM
is the survivorship-biased number and **ours is the truth** (oliman2 and
JuiceFarm look elite on redeemed-only P&L, ~$20k/$32k once the losers they
walked from are counted). This *replaced* the old hold-to-resolution
reconstruction (`won × entry × size`), which diverged by up to 10× and
flipped signs — four bugs killed with it: the 2,000-row pull cap, both-sides
double-drop, `initialValue = 0` mis-sizing, and corrupt near-epoch `res_t`.
Win % is the share of decided positions that *made money*.
---
+35 -13
View File
@@ -93,24 +93,41 @@ def _pm_profit(w):
return None
def _open_pnl(w):
"""The wallet's OPEN (unrealized) P&L — sum of cashPnl over every current
position, full paging (the endpoint caps at 50/page). This is the exposure
sitting behind the realized track record: a wallet with a great All-Time
(realized) P&L but a large NEGATIVE open book is selling winners and
holding losers — the realized number flatters what you'd feel following
them live. PM /profit marks this in; our realized All-Time P&L doesn't, so
Open P&L is exactly the reconciling gap between them."""
total = 0.0
def _open_split(w):
"""Split the wallet's current /positions into (open_pnl, resolved).
open_pnl = unrealized P&L (cashPnl) over GENUINELY OPEN positions
(interior price) — real in-flight exposure.
resolved = decided-but-UNREDEEMED positions (curPrice pinned at 0 or 1):
bets that already won/lost and the wallet just never redeemed.
These are NOT open — they're realized outcomes hiding in the
positions endpoint (mostly abandoned LOSERS at $0). They
belong in the REALIZED track record (cashPnl = their decided
P&L: a loser is -cost). Leaving them in "open" is the exact
survivorship blind spot — PM /profit under-counts them, so a
wallet's realized looks better than the bets it walked away
from. Returned as [{realized_pnl, iv, ts}] to fold into
All-Time P&L + win/loss.
"""
open_pnl = 0.0
resolved = []
for off in range(0, 100000, 50):
pg = sm.get_json("/positions", {"user": w, "limit": 50, "offset": off,
"sizeThreshold": 0})
if not pg:
break
total += sum(p.get("cashPnl") or 0 for p in pg)
for p in pg:
cp = p.get("curPrice", 0) or 0
cpnl = p.get("cashPnl") or 0
if cp <= 0.001 or cp >= 0.999: # decided, just unredeemed
resolved.append({"realized_pnl": cpnl,
"iv": p.get("initialValue") or 0,
"ts": p.get("timestamp") or 0})
else:
open_pnl += cpnl
if len(pg) < 50:
break
return round(total)
return round(open_pnl), resolved
def _wp(cond, asset, won):
@@ -177,7 +194,12 @@ def display_stats(w):
scr_ += 1
return won_, lost_, scr_, round(pnl)
allpos = list(exits.values())
# realized universe = redeemed/sold closed positions (exits, with realizedPnl)
# PLUS decided-but-unredeemed positions (resolved losers/winners still sitting
# in /positions). Folding the latter in is the anti-survivorship correction:
# a wallet's true realized record includes the bets it walked away from.
open_pnl, resolved_open = _open_split(w)
allpos = list(exits.values()) + resolved_open
all_won, all_lost, all_scr, all_pnl = rtally(allpos)
# conviction = the wallet's top-20%-by-stake positions (iv); conv30 = those
# closed in the last 30d. Realized P&L over each set.
@@ -199,7 +221,7 @@ def display_stats(w):
"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_scr,
"all_sold": 0, "all_pnl": all_pnl,
"open_pnl": _open_pnl(w),
"open_pnl": open_pnl, # genuinely in-flight only (resolved folded into realized)
"pm_pnl": _pm_profit(w),
"avg_bet": round(sum(e["iv"] for e in conv) / len(conv)) if conv else 0,
"copy_pnl": 0, "held_pnl": 0, "held_won": 0, "held_lost": 0, "sold": 0,