CRITICAL settle fix: CLOB winner=False means unresolved, not lost
The CLOB reports winner=False on EVERY token of an unresolved market; only a True winner anywhere means resolved. resolution_price treated False as "lost", so the 60s poller settled live in-play positions as instant losses 1-3 min after entry (June's ~2h Actions cadence masked it - markets had genuinely resolved by the next check). Same semantics fixed in _market's resolved-cache gate and validate_timing._clob_winner (unresolved held bets were counted as losses, biasing copy_pnl selection downward). Also: _record_lag now AGGREGATES fills per token (ADD used to overwrite the display record, showing one fill's cost against the whole position's P&L - the Cost $20.84 vs P&L -$57.30 confusion). State repaired: the four July bets all actually WON on-chain (verified by token winner flags) - settled correctly, +$225.40 proceeds restored, book $1,045.19 (+45.19 realized, 4W/0L). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+38
-17
@@ -110,8 +110,10 @@ def _market(cond):
|
||||
m = json.loads(r.read().decode()) or {}
|
||||
except Exception:
|
||||
return None
|
||||
# only cache a resolved market (it won't change); re-check live ones each pass
|
||||
if any(t.get("winner") in (True, False) for t in (m.get("tokens") or [])):
|
||||
# only cache a RESOLVED market (it won't change); re-check live ones each pass.
|
||||
# CLOB semantics: unresolved markets report winner=False on EVERY token —
|
||||
# resolution flips exactly one to True. Only a True winner means resolved.
|
||||
if any(t.get("winner") is True for t in (m.get("tokens") or [])):
|
||||
with _MKT_LOCK:
|
||||
_MKT_CACHE[cond] = m
|
||||
return m
|
||||
@@ -132,14 +134,21 @@ def market_neg_risk(cond):
|
||||
def resolution_price(token_id, cond, outcome=None):
|
||||
"""Settled price of our held token: 1.0 if it won, 0.0 if it lost, None if the
|
||||
market hasn't resolved yet. Matches by outcome first (as the dashboard does),
|
||||
then by token_id."""
|
||||
then by token_id.
|
||||
|
||||
CRITICAL semantics: the CLOB reports winner=False on EVERY token of an
|
||||
UNRESOLVED market — False alone means "not yet", not "lost". A market is
|
||||
resolved only once some token's winner is True. Treating False as lost made
|
||||
the bot settle live in-play markets as instant losses minutes after entry
|
||||
(2026-07-02: four winning bets booked as -$180 of losses)."""
|
||||
toks = market_tokens(cond)
|
||||
if not toks:
|
||||
return None
|
||||
if not any(t.get("winner") is True for t in toks):
|
||||
return None # nobody has won -> not resolved
|
||||
|
||||
def winp(t):
|
||||
w = t.get("winner")
|
||||
return 1.0 if w is True else 0.0 if w is False else None
|
||||
return 1.0 if t.get("winner") is True else 0.0
|
||||
|
||||
if outcome is not None:
|
||||
for t in toks:
|
||||
@@ -345,18 +354,30 @@ class Copybot:
|
||||
lag["n"] += 1
|
||||
lag["sum_s"] += detect_s
|
||||
lag["sum_slip_pct"] += slip_pct
|
||||
# record the placed bet for the live dashboard feed
|
||||
self.engine.state.setdefault("bets", {})[fill["token"]] = {
|
||||
"token": fill["token"], "wallet": wallet,
|
||||
"name": self.names.get(wallet.lower(), wallet[:10]),
|
||||
"outcome": t.get("outcome"), "title": (t.get("title") or "")[:90],
|
||||
"their_price": round(their_p, 4), "my_price": round(my_p, 4),
|
||||
"slippage_pct": round(slip_pct, 4),
|
||||
"shares": round(fill["shares"], 2), "cost": round(fill["shares"] * my_p, 2),
|
||||
"fee": fill.get("fee", 0),
|
||||
"opened": int(their_ts or now), "status": "open",
|
||||
"exit_price": None, "pnl": None, "settled": None,
|
||||
}
|
||||
# record the placed bet for the live dashboard feed. AGGREGATE across
|
||||
# fills: an ADD to an existing open position must accumulate shares/
|
||||
# cost/fees, not overwrite the record with just the last fill — that
|
||||
# made Cost show one fill while P&L settled the whole position.
|
||||
bets = self.engine.state.setdefault("bets", {})
|
||||
prev = bets.get(fill["token"])
|
||||
if prev and prev.get("status") == "open":
|
||||
sh = prev["shares"] + fill["shares"]
|
||||
cost = prev["cost"] + fill["shares"] * my_p
|
||||
prev.update(shares=round(sh, 2), cost=round(cost, 2),
|
||||
my_price=round(cost / sh, 4) if sh else prev["my_price"],
|
||||
fee=round((prev.get("fee") or 0) + fill.get("fee", 0), 4))
|
||||
else:
|
||||
bets[fill["token"]] = {
|
||||
"token": fill["token"], "wallet": wallet,
|
||||
"name": self.names.get(wallet.lower(), wallet[:10]),
|
||||
"outcome": t.get("outcome"), "title": (t.get("title") or "")[:90],
|
||||
"their_price": round(their_p, 4), "my_price": round(my_p, 4),
|
||||
"slippage_pct": round(slip_pct, 4),
|
||||
"shares": round(fill["shares"], 2), "cost": round(fill["shares"] * my_p, 2),
|
||||
"fee": fill.get("fee", 0),
|
||||
"opened": int(their_ts or now), "status": "open",
|
||||
"exit_price": None, "pnl": None, "settled": None,
|
||||
}
|
||||
log(f" ↳ lag {('%.0fs' % detect_s) if detect_s is not None else '?'} · "
|
||||
f"their {their_p:.3f} → mine {my_p:.3f} ({slip_pct:+.1%} slippage)")
|
||||
|
||||
|
||||
+20
-20
@@ -978,7 +978,7 @@
|
||||
"0x41558102a796ba971c7567cad41c307e59f8fa41"
|
||||
],
|
||||
"conds": {},
|
||||
"cash": 819.787881884037,
|
||||
"cash": 1045.19,
|
||||
"lag": {
|
||||
"n": 5,
|
||||
"sum_s": 239.17888164520264,
|
||||
@@ -999,10 +999,10 @@
|
||||
"cost": 50.0,
|
||||
"fee": 0.69,
|
||||
"opened": 1783001516,
|
||||
"status": "lost",
|
||||
"exit_price": 0.0,
|
||||
"pnl": -50.69,
|
||||
"settled": 1783001640
|
||||
"status": "won",
|
||||
"exit_price": 1.0,
|
||||
"pnl": 41.9,
|
||||
"settled": 1783048513
|
||||
},
|
||||
"28393949293851968084595287701797923567435527099133056404275480596406503297469": {
|
||||
"token": "28393949293851968084595287701797923567435527099133056404275480596406503297469",
|
||||
@@ -1017,10 +1017,10 @@
|
||||
"cost": 37.97,
|
||||
"fee": 0.0513,
|
||||
"opened": 1783034410,
|
||||
"status": "lost",
|
||||
"exit_price": 0.0,
|
||||
"pnl": -38.02,
|
||||
"settled": 1783034498
|
||||
"status": "won",
|
||||
"exit_price": 1.0,
|
||||
"pnl": 1.74,
|
||||
"settled": 1783048513
|
||||
},
|
||||
"12184589112253099646841219091162361978388293103010365931849641794603228859112": {
|
||||
"token": "12184589112253099646841219091162361978388293103010365931849641794603228859112",
|
||||
@@ -1031,14 +1031,14 @@
|
||||
"their_price": 0.99,
|
||||
"my_price": 0.99,
|
||||
"slippage_pct": 0.0,
|
||||
"shares": 21.05,
|
||||
"cost": 20.84,
|
||||
"fee": 0.0063,
|
||||
"shares": 57.87,
|
||||
"cost": 57.29,
|
||||
"fee": 0.0172,
|
||||
"opened": 1783039844,
|
||||
"status": "lost",
|
||||
"exit_price": 0.0,
|
||||
"pnl": -57.3,
|
||||
"settled": 1783039964
|
||||
"status": "won",
|
||||
"exit_price": 1.0,
|
||||
"pnl": 0.56,
|
||||
"settled": 1783048513
|
||||
},
|
||||
"94170053339927453082717035943543731636274553439076639716988060790503681603673": {
|
||||
"token": "94170053339927453082717035943543731636274553439076639716988060790503681603673",
|
||||
@@ -1053,10 +1053,10 @@
|
||||
"cost": 34.16,
|
||||
"fee": 0.0297,
|
||||
"opened": 1783040003,
|
||||
"status": "lost",
|
||||
"exit_price": 0.0,
|
||||
"pnl": -34.19,
|
||||
"settled": 1783040090
|
||||
"status": "won",
|
||||
"exit_price": 1.0,
|
||||
"pnl": 0.99,
|
||||
"settled": 1783048513
|
||||
}
|
||||
},
|
||||
"feed_sig": "039a4619e46287904585dd83d0f3bb01",
|
||||
|
||||
+10
-3
@@ -42,15 +42,22 @@ _CLOB = {} # conditionId -> {token_id: winner-price 1/0/None}
|
||||
|
||||
def _clob_winner(cond, token):
|
||||
"""Authoritative resolution for a token: 1 if it won, 0 if it lost, None if the
|
||||
market hasn't resolved. Matched by token_id (exact, no outcome-name guessing)."""
|
||||
market hasn't resolved. Matched by token_id (exact, no outcome-name guessing).
|
||||
|
||||
NB: the CLOB reports winner=False on EVERY token of an UNRESOLVED market —
|
||||
only a present True winner means resolved. Treating False as "lost" counted
|
||||
every unresolved held bet as a loss, biasing copy_pnl (the selection metric)
|
||||
downward."""
|
||||
if cond not in _CLOB:
|
||||
try:
|
||||
req = urllib.request.Request("https://clob.polymarket.com/markets/" + cond,
|
||||
headers={"User-Agent": "Mozilla/5.0"})
|
||||
m = json.loads(urllib.request.urlopen(req, timeout=20, context=_SSL).read())
|
||||
toks = m.get("tokens") or []
|
||||
resolved = any(t.get("winner") is True for t in toks)
|
||||
_CLOB[cond] = {str(t.get("token_id")):
|
||||
(1 if t.get("winner") is True else 0 if t.get("winner") is False else None)
|
||||
for t in (m.get("tokens") or [])}
|
||||
((1 if t.get("winner") is True else 0) if resolved else None)
|
||||
for t in toks}
|
||||
except Exception:
|
||||
_CLOB[cond] = {}
|
||||
return _CLOB[cond].get(str(token))
|
||||
|
||||
Reference in New Issue
Block a user