mirror of
https://github.com/jaxperro/winning-wallet-finder.git
synced 2026-07-28 00:07:47 +00:00
scoring: chain-truth resolutions overlay — refunds no longer count as wins
The cache's won (curPrice>=0.5 at pull) counts 50/50 refunds as wins for BOTH sides — 521 of 2,128 chain-checked follow-set markets (24%) were refunds, which is where the whales' 92-100% displayed win rates came from. - live/payouts.py: resolutions table in cache.duckdb, filled from the CTF contract's payout vectors (batched+paced JSON-RPC, ~12 calls/s free tier; resolved rows immutable, unresolved recheck 6h, RPC failures never cached). truth(cond, asset) -> 1/0/0.5/None; refunds need no asset side. - validate_timing: every displayed stat (conv/conv30/all-time/realized/copy replay) settles at truth; refunds count as neither W nor L, P&L is size*(wp-p)/p; new conv_ref/conv30_ref/all_ref feed fields. - trust.conviction_record: optional truthfn — the selection gates (trust_wr/trust_roi) no longer select on refund inflation. - portfolio.py: replay pays wp (refunds 0.5/share, was 1.0). - conviction_scan: documented as the (refund-inflated) candidate layer; final selection re-judges against truth downstream. Validation: 0x4bFb-whale conv 174-16 91.6% $1.61M -> 34-16 +140ref 68% $214k, and truth-adjusted all-time P&L now sits within ~16% of lb-api's PM P&L (was 7x apart); LSB1 (0 refunds) byte-identical, its P&L matches PM P&L to 0.07%. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,14 @@ Profile gates (on TRAIN conviction bets):
|
|||||||
* median conviction stake >= MIN_MED_STAKE (dust wallets betting $2-$6 clips
|
* median conviction stake >= MIN_MED_STAKE (dust wallets betting $2-$6 clips
|
||||||
aren't followable and their fills aren't reproducible)
|
aren't followable and their fills aren't reproducible)
|
||||||
Then validate forward and count how many keep the profile.
|
Then validate forward and count how many keep the profile.
|
||||||
|
|
||||||
|
NB (2026-07-06): this scan reads the cache's `won` marks, which count 50/50
|
||||||
|
REFUNDS as wins for both sides (28% of resolved markets in the in-play niche)
|
||||||
|
— so it OVER-generates candidates. That's acceptable: this is the candidate
|
||||||
|
layer; final selection (validate_timing.py) re-judges every candidate against
|
||||||
|
chain-truth payouts (payouts.py) and rejects refund-inflated records. Chain-
|
||||||
|
checking all ~19M rows here would take days of RPC; the funnel does it only
|
||||||
|
for the few dozen wallets that survive to the copy replay.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import math, os, time
|
import math, os, time
|
||||||
|
|||||||
+206
@@ -0,0 +1,206 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Chain-truth resolution overlay for cache.duckdb — the 2026-07-06 audit fix.
|
||||||
|
|
||||||
|
The cache's `won` is `curPrice >= 0.5` at pull time, and that lies two ways
|
||||||
|
(see the audit + [[polymarket-resolution-truth]] in project memory):
|
||||||
|
|
||||||
|
* 50/50 REFUNDS (walkovers/abandonments — 28% of the follow set's resolved
|
||||||
|
markets in the 30d sample!) leave curPrice at 0.5, so EVERY holder of
|
||||||
|
EITHER side gets won=True and (1-p)/p phantom profit. The whales' 92-100%
|
||||||
|
displayed win rates were largely this.
|
||||||
|
* operator-resolved in-play markets can leave a stale mark on the losing
|
||||||
|
side (one confirmed both-sides-won market), which resolved=TRUE certifies.
|
||||||
|
|
||||||
|
The fix keeps the cache AS PULLED and overlays the on-chain truth: the CTF
|
||||||
|
contract's payout vector per condition, plus the market's token order so a
|
||||||
|
row's `asset` maps to its payout. Resolved payouts are immutable — fetched
|
||||||
|
once, cached in a `resolutions` table forever. Unresolved conditions recheck
|
||||||
|
after RECHECK_S.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
import payouts
|
||||||
|
payouts.ensure(conds) # batch-backfill (chain + CLOB, cached)
|
||||||
|
wp = payouts.truth(cond, asset) # 1.0 / 0.0 / 0.5 / None (unknown)
|
||||||
|
|
||||||
|
Scoring rule for consumers: truth 1/0 -> real win/loss; 0.5 -> REFUND, count
|
||||||
|
as neither and P&L = size*(0.5-p)/p; None -> fall back to the cache's `won`
|
||||||
|
(legacy NULL-asset rows, unresolved markets, RPC gaps).
|
||||||
|
|
||||||
|
RPC: config.json `alchemy_key` (or ALCHEMY_RPC_URL env). Batched JSON-RPC
|
||||||
|
(BATCH per POST); the CLOB market fetch supplies token order. Selection runs
|
||||||
|
on the Mac, so this module is never needed by the Fly worker.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import ssl
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import urllib.request
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
|
||||||
|
import cache
|
||||||
|
|
||||||
|
_SSL = ssl._create_unverified_context()
|
||||||
|
HERE = os.path.dirname(__file__)
|
||||||
|
CTF = "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045"
|
||||||
|
SEL_DEN = "0xdd34de67" # payoutDenominator(bytes32)
|
||||||
|
SEL_NUM = "0x0504c814" # payoutNumerators(bytes32,uint256)
|
||||||
|
BATCH = 60 # JSON-RPC calls per POST
|
||||||
|
PACE_S = float(os.environ.get("PAYOUTS_PACE_S", 5.0)) # sleep between batches:
|
||||||
|
# free-tier Alchemy sustains ~12 eth_calls/s — an
|
||||||
|
# unpaced backfill just trades 429 retries for gaps
|
||||||
|
RECHECK_S = 6 * 3600 # re-ask the chain about unresolved conds after this
|
||||||
|
|
||||||
|
cache.query("""CREATE TABLE IF NOT EXISTS resolutions(
|
||||||
|
cond TEXT PRIMARY KEY, p0 DOUBLE, p1 DOUBLE,
|
||||||
|
token0 TEXT, token1 TEXT, checked_at BIGINT)""")
|
||||||
|
|
||||||
|
_mem = {} # cond -> row tuple, warm in-process copy
|
||||||
|
_lock = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
|
def _rpc_url():
|
||||||
|
url = os.environ.get("ALCHEMY_RPC_URL")
|
||||||
|
if url:
|
||||||
|
return url
|
||||||
|
try:
|
||||||
|
key = json.load(open(os.path.join(HERE, "..", "config.json")))["alchemy_key"]
|
||||||
|
return f"https://polygon-mainnet.g.alchemy.com/v2/{key}"
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _rpc_batch(calls):
|
||||||
|
"""calls: [hex calldata] -> [int results or None], one batched POST."""
|
||||||
|
url = _rpc_url()
|
||||||
|
if not url:
|
||||||
|
return [None] * len(calls)
|
||||||
|
body = json.dumps([{"jsonrpc": "2.0", "id": i, "method": "eth_call",
|
||||||
|
"params": [{"to": CTF, "data": d}, "latest"]}
|
||||||
|
for i, d in enumerate(calls)]).encode()
|
||||||
|
# retry with backoff — Alchemy 429s a fast backfill after ~2 batches, and a
|
||||||
|
# failed batch must surface as None (skip-this-run), never "unresolved"
|
||||||
|
for attempt in range(4):
|
||||||
|
try:
|
||||||
|
req = urllib.request.Request(url, data=body,
|
||||||
|
headers={"Content-Type": "application/json"})
|
||||||
|
rs = json.loads(urllib.request.urlopen(req, timeout=30, context=_SSL).read())
|
||||||
|
if isinstance(rs, dict): # whole-batch error object
|
||||||
|
raise OSError(str(rs.get("error"))[:80])
|
||||||
|
by_id = {r.get("id"): r for r in rs}
|
||||||
|
out = []
|
||||||
|
for i in range(len(calls)):
|
||||||
|
v = by_id.get(i, {}).get("result")
|
||||||
|
try:
|
||||||
|
# "0x" (revert/empty) must not kill the whole batch — one
|
||||||
|
# bad item used to blank every cond in the chunk
|
||||||
|
out.append(int(v, 16) if v and v != "0x" else None)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
out.append(None)
|
||||||
|
return out
|
||||||
|
except Exception:
|
||||||
|
if attempt == 3:
|
||||||
|
return [None] * len(calls)
|
||||||
|
time.sleep(2 ** attempt)
|
||||||
|
|
||||||
|
|
||||||
|
def _clob_tokens(cond):
|
||||||
|
"""(token0, token1) in the CLOB market's outcome order — payout index order."""
|
||||||
|
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=15, context=_SSL).read())
|
||||||
|
toks = [str(t.get("token_id")) for t in (m.get("tokens") or [])[:2]]
|
||||||
|
return (toks + [None, None])[:2]
|
||||||
|
except Exception:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
def _load(conds):
|
||||||
|
with _lock:
|
||||||
|
missing = [c for c in conds if c and c not in _mem]
|
||||||
|
if missing:
|
||||||
|
rows = cache.query(
|
||||||
|
"SELECT cond, p0, p1, token0, token1, checked_at FROM resolutions "
|
||||||
|
"WHERE cond IN (SELECT UNNEST(?::VARCHAR[]))", [missing])
|
||||||
|
with _lock:
|
||||||
|
for r in rows:
|
||||||
|
_mem[r[0]] = r
|
||||||
|
|
||||||
|
|
||||||
|
def ensure(conds, workers=8):
|
||||||
|
"""Make sure every cond has a resolutions row (fetching chain + CLOB for the
|
||||||
|
unknown ones). Resolved rows are permanent; unresolved recheck after
|
||||||
|
RECHECK_S. Safe to call with thousands of conds — everything is cached."""
|
||||||
|
conds = [c for c in {c for c in conds if c}]
|
||||||
|
_load(conds)
|
||||||
|
now = int(time.time())
|
||||||
|
with _lock:
|
||||||
|
todo = [c for c in conds
|
||||||
|
if c not in _mem
|
||||||
|
or (_mem[c][1] is None and now - (_mem[c][5] or 0) > RECHECK_S)]
|
||||||
|
if not todo:
|
||||||
|
return
|
||||||
|
# chain payouts, batched: den + num0 per cond (binary: num1 = den - num0)
|
||||||
|
payout = {}
|
||||||
|
for i in range(0, len(todo), BATCH // 2):
|
||||||
|
chunk = todo[i:i + BATCH // 2]
|
||||||
|
calls = []
|
||||||
|
for c in chunk:
|
||||||
|
h = c[2:].rjust(64, "0")
|
||||||
|
calls += [SEL_DEN + h,
|
||||||
|
SEL_NUM + h + "0".rjust(64, "0")]
|
||||||
|
res = _rpc_batch(calls)
|
||||||
|
for j, c in enumerate(chunk):
|
||||||
|
den, n0 = res[2 * j], res[2 * j + 1]
|
||||||
|
if den is None:
|
||||||
|
payout[c] = "rpc-fail" # do NOT cache — retry next run
|
||||||
|
elif den and n0 is not None:
|
||||||
|
payout[c] = (n0 / den, (den - n0) / den)
|
||||||
|
else:
|
||||||
|
payout[c] = (None, None) # chain says: not resolved yet
|
||||||
|
if i + BATCH // 2 < len(todo):
|
||||||
|
time.sleep(PACE_S)
|
||||||
|
# token order for the RESOLVED ones (index -> asset mapping); cached forever
|
||||||
|
resolved = [c for c in todo
|
||||||
|
if isinstance(payout.get(c), tuple) and payout[c][0] is not None]
|
||||||
|
tokens = {}
|
||||||
|
with ThreadPoolExecutor(max_workers=workers) as ex:
|
||||||
|
for c, tk in zip(resolved, ex.map(_clob_tokens, resolved)):
|
||||||
|
tokens[c] = tk
|
||||||
|
with _lock:
|
||||||
|
for c in todo:
|
||||||
|
p = payout.get(c)
|
||||||
|
if p == "rpc-fail" or p is None:
|
||||||
|
continue # transient — leave uncached, retry next run
|
||||||
|
t0, t1 = tokens.get(c, (None, None))
|
||||||
|
row = (c, p[0], p[1], t0, t1, now)
|
||||||
|
_mem[c] = row
|
||||||
|
cache.query("INSERT OR REPLACE INTO resolutions VALUES (?,?,?,?,?,?)", list(row))
|
||||||
|
|
||||||
|
|
||||||
|
def truth(cond, asset=None):
|
||||||
|
"""Chain-truth payout of this position: 1.0 / 0.0 / 0.5 / None (unknown).
|
||||||
|
Refunds ([0.5,0.5]) need no asset; decided markets map asset -> index."""
|
||||||
|
_load([cond])
|
||||||
|
r = _mem.get(cond)
|
||||||
|
if not r or r[1] is None:
|
||||||
|
return None
|
||||||
|
p0, p1, t0, t1 = r[1], r[2], r[3], r[4]
|
||||||
|
if p0 == p1: # refund (or exotic even split)
|
||||||
|
return p0
|
||||||
|
if asset is not None:
|
||||||
|
if str(asset) == str(t0):
|
||||||
|
return p0
|
||||||
|
if str(asset) == str(t1):
|
||||||
|
return p1
|
||||||
|
return None # decided, but we can't map the side
|
||||||
|
|
||||||
|
|
||||||
|
def stats():
|
||||||
|
n = cache.query("SELECT count(*), count(p0) FROM resolutions")[0]
|
||||||
|
return {"rows": n[0], "resolved": n[1]}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print(stats())
|
||||||
+29
-9
@@ -31,6 +31,7 @@ import urllib.request
|
|||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
|
||||||
import cache
|
import cache
|
||||||
|
import payouts
|
||||||
import smart_money as sm
|
import smart_money as sm
|
||||||
import trust
|
import trust
|
||||||
|
|
||||||
@@ -179,24 +180,31 @@ def window_bets():
|
|||||||
rows = trust.trusted_wallet_rows(cache.query, w["wallet"], now)
|
rows = trust.trusted_wallet_rows(cache.query, w["wallet"], now)
|
||||||
best = {} # one bet per market: largest-stake token
|
best = {} # one bet per market: largest-stake token
|
||||||
for cond, asset, won, p, res_t, size in rows:
|
for cond, asset, won, p, res_t, size in rows:
|
||||||
if cond not in best or size > best[cond][3]:
|
if cond not in best or size > best[cond][4]:
|
||||||
best[cond] = (won, p, res_t, size)
|
best[cond] = (asset, won, p, res_t, size)
|
||||||
if w.get("class") == "whale":
|
if w.get("class") == "whale":
|
||||||
thr = 0.0
|
thr = 0.0
|
||||||
else:
|
else:
|
||||||
pre = [size for won, p, res_t, size in best.values() if res_t < START]
|
pre = [size for asset, won, p, res_t, size in best.values() if res_t < START]
|
||||||
thr = cache.conv_cutoff(pre if pre else
|
thr = cache.conv_cutoff(pre if pre else
|
||||||
[size for *_, size in best.values()])
|
[size for *_, size in best.values()])
|
||||||
_WALLET_THR[w["wallet"]] = thr
|
_WALLET_THR[w["wallet"]] = thr
|
||||||
for cond, (won, p, res_t, size) in best.items():
|
for cond, (asset, won, p, res_t, size) in best.items():
|
||||||
if size < thr or p > MAX_ENTRY:
|
if size < thr or p > MAX_ENTRY:
|
||||||
continue
|
continue
|
||||||
et = ent.get(cond)
|
et = ent.get(cond)
|
||||||
if not et or et < START: # only in-window entries
|
if not et or et < START: # only in-window entries
|
||||||
continue
|
continue
|
||||||
out.append({"wallet": w["wallet"], "name": w["name"], "cond": cond,
|
out.append({"wallet": w["wallet"], "name": w["name"], "cond": cond,
|
||||||
"cls": w.get("class", "volume"), "their": size,
|
"asset": asset, "cls": w.get("class", "volume"),
|
||||||
"entry_t": et, "p": p, "won": won, "res_t": res_t or 0})
|
"their": size, "entry_t": et, "p": p, "won": won,
|
||||||
|
"res_t": res_t or 0})
|
||||||
|
# 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)
|
||||||
|
payouts.ensure({b["cond"] for b in out})
|
||||||
|
for b in out:
|
||||||
|
b["wp"] = payouts.truth(b["cond"], b.get("asset"))
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
@@ -301,7 +309,12 @@ def main():
|
|||||||
for ft, cost, payoff, rec in held:
|
for ft, cost, payoff, rec in held:
|
||||||
if ft and ft <= upto and rec["kind"] == "res":
|
if ft and ft <= upto and rec["kind"] == "res":
|
||||||
cash += payoff; realized += payoff - cost; perW[rec["wallet"]]["realized"] += payoff - cost
|
cash += payoff; realized += payoff - cost; perW[rec["wallet"]]["realized"] += payoff - cost
|
||||||
perW[rec["wallet"]]["won" if rec["won"] else "lost"] += 1
|
wp = rec.get("wp")
|
||||||
|
won = rec["won"] if wp is None else wp > 0.5
|
||||||
|
perW[rec["wallet"]]["won" if won else "lost"] += 1
|
||||||
|
rec["won"] = won # truth-adjusted for the feed
|
||||||
|
if wp == 0.5:
|
||||||
|
rec["refund"] = True
|
||||||
rec["pnl"] = payoff - cost
|
rec["pnl"] = payoff - cost
|
||||||
resolved.append(rec)
|
resolved.append(rec)
|
||||||
else:
|
else:
|
||||||
@@ -325,7 +338,11 @@ def main():
|
|||||||
cash -= cost; fees_paid += fee; perW[b["wallet"]]["bets"] += 1
|
cash -= cost; fees_paid += fee; perW[b["wallet"]]["bets"] += 1
|
||||||
shares = stake / p_eff # lag-adjusted entry price
|
shares = stake / p_eff # lag-adjusted entry price
|
||||||
if b["kind"] == "res":
|
if b["kind"] == "res":
|
||||||
payoff = shares * (1.0 if b["won"] else 0.0) # redeem is fee-free
|
# chain-truth payout (1/0/0.5) when known, else the cache mark
|
||||||
|
wp = b.get("wp")
|
||||||
|
if wp is None:
|
||||||
|
wp = 1.0 if b["won"] else 0.0
|
||||||
|
payoff = shares * wp # redeem is fee-free
|
||||||
held.append((b["res_t"] or now, cost, payoff, b))
|
held.append((b["res_t"] or now, cost, payoff, b))
|
||||||
else: # currently open -> mark to market, no free yet
|
else: # currently open -> mark to market, no free yet
|
||||||
held.append((None, cost, 0.0, b))
|
held.append((None, cost, 0.0, b))
|
||||||
@@ -356,7 +373,10 @@ def main():
|
|||||||
stake = m.get("stake") or STAKE_MIN
|
stake = m.get("stake") or STAKE_MIN
|
||||||
p_eff, fee, cost = entry_model(m["p"], stake)
|
p_eff, fee, cost = entry_model(m["p"], stake)
|
||||||
if "won" in m:
|
if "won" in m:
|
||||||
return (stake / p_eff) - cost if m["won"] else -cost
|
wp = m.get("wp")
|
||||||
|
if wp is None:
|
||||||
|
wp = 1.0 if m["won"] else 0.0
|
||||||
|
return (stake / p_eff) * wp - cost
|
||||||
return stake * (m.get("cur", p_eff) / p_eff) - cost
|
return stake * (m.get("cur", p_eff) / p_eff) - cost
|
||||||
|
|
||||||
missed.sort(key=lambda m: m.get("res_t") or 0, reverse=True)
|
missed.sort(key=lambda m: m.get("res_t") or 0, reverse=True)
|
||||||
|
|||||||
+28
-9
@@ -103,17 +103,23 @@ def trusted_wallet_rows(runq, wallet, now=None):
|
|||||||
[wallet, now, now])
|
[wallet, now, now])
|
||||||
|
|
||||||
|
|
||||||
def conviction_record(runq, wallet, days=90, pctile=0.80, now=None):
|
def conviction_record(runq, wallet, days=90, pctile=0.80, now=None, truthfn=None):
|
||||||
"""Trailing trusted CONVICTION record for the held-edge gate:
|
"""Trailing trusted CONVICTION record for the held-edge gate:
|
||||||
{n, wr, roi} over the wallet's top-(1-pctile) stake bets resolved in the
|
{n, wr, roi} over the wallet's top-(1-pctile) stake bets resolved in the
|
||||||
last `days`. The conviction cutoff is that wallet's stake p80 over its FULL
|
last `days`. The conviction cutoff is that wallet's stake p80 over its FULL
|
||||||
trusted history (matching cache.conv_cutoff semantics). roi is the flat-
|
trusted history (matching cache.conv_cutoff semantics). roi is the flat-
|
||||||
stake hold-to-resolution copy ROI per bet, fee/slip-free (gates compare it
|
stake hold-to-resolution copy ROI per bet, fee/slip-free (gates compare it
|
||||||
to 0, and fees are already charged in copy_pnl, the other selection leg)."""
|
to 0, and fees are already charged in copy_pnl, the other selection leg).
|
||||||
|
|
||||||
|
truthfn(cond, asset) -> 1/0/0.5/None (payouts.truth): chain-truth payouts.
|
||||||
|
50/50 REFUNDS (28% of the follow-set niche's resolved markets!) then count
|
||||||
|
as NEITHER won nor lost, with roi credited at (0.5-p)/p — the cache alone
|
||||||
|
marks every refund won=True for both sides, which is exactly the inflation
|
||||||
|
these gates must not select on. None -> the cache's won (old behavior)."""
|
||||||
now = int(now or time.time())
|
now = int(now or time.time())
|
||||||
rows = trusted_wallet_rows(runq, wallet, now)
|
rows = trusted_wallet_rows(runq, wallet, now)
|
||||||
if not rows:
|
if not rows:
|
||||||
return dict(n=0, wr=0.0, roi=0.0)
|
return dict(n=0, wr=0.0, roi=0.0, refunds=0)
|
||||||
sizes = sorted(r[5] for r in rows)
|
sizes = sorted(r[5] for r in rows)
|
||||||
k = (len(sizes) - 1) * pctile
|
k = (len(sizes) - 1) * pctile
|
||||||
f = int(k)
|
f = int(k)
|
||||||
@@ -123,11 +129,24 @@ def conviction_record(runq, wallet, days=90, pctile=0.80, now=None):
|
|||||||
best = {}
|
best = {}
|
||||||
for cond, asset, won, p, res_t, size in rows:
|
for cond, asset, won, p, res_t, size in rows:
|
||||||
if size >= thr and res_t >= cut:
|
if size >= thr and res_t >= cut:
|
||||||
if cond not in best or size > best[cond][2]:
|
if cond not in best or size > best[cond][3]:
|
||||||
best[cond] = (won, p, size)
|
best[cond] = (cond, asset, won, p, size)
|
||||||
conv = list(best.values())
|
conv = list(best.values())
|
||||||
if not conv:
|
if not conv:
|
||||||
return dict(n=0, wr=0.0, roi=0.0)
|
return dict(n=0, wr=0.0, roi=0.0, refunds=0)
|
||||||
wins = sum(1 for won, _, _ in conv if won)
|
wins = losses = refunds = 0
|
||||||
roi = sum(((1 - p) / p if won else -1.0) for won, p, _ in conv) / len(conv)
|
roi_sum = 0.0
|
||||||
return dict(n=len(conv), wr=wins / len(conv), roi=roi)
|
for cond, asset, won, p, size in conv:
|
||||||
|
wp = truthfn(cond, asset) if truthfn else None
|
||||||
|
if wp is None:
|
||||||
|
wp = 1.0 if won else 0.0
|
||||||
|
roi_sum += (wp - p) / p
|
||||||
|
if wp > 0.5:
|
||||||
|
wins += 1
|
||||||
|
elif wp < 0.5:
|
||||||
|
losses += 1
|
||||||
|
else:
|
||||||
|
refunds += 1
|
||||||
|
decided = wins + losses
|
||||||
|
return dict(n=decided, wr=(wins / decided) if decided else 0.0,
|
||||||
|
roi=roi_sum / len(conv), refunds=refunds)
|
||||||
|
|||||||
+63
-26
@@ -37,6 +37,7 @@ import urllib.request
|
|||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
|
||||||
import cache
|
import cache
|
||||||
|
import payouts
|
||||||
import smart_money as sm
|
import smart_money as sm
|
||||||
import trust
|
import trust
|
||||||
|
|
||||||
@@ -93,11 +94,25 @@ def _pm_profit(w):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _bet_pnl(b):
|
def _wp(cond, asset, won):
|
||||||
"""Resolved (outcome) P&L of one cache bet: a $size stake at avg price p pays
|
"""Chain-truth payout for a bet (1/0/0.5), falling back to the cache's
|
||||||
size/p if won, else $0 — so P&L = size·(1−p)/p if won else −size."""
|
`won` mark when the chain can't say (unresolved, legacy NULL-asset rows on
|
||||||
|
decided markets, RPC gaps). The fallback keeps old behavior; the truth
|
||||||
|
path kills the two cache lies: 50/50 refunds counted as wins for BOTH
|
||||||
|
sides (28% of the follow set's resolved markets!) and stale both-sides-won
|
||||||
|
marks on operator-resolved markets."""
|
||||||
|
wp = payouts.truth(cond, asset)
|
||||||
|
return (1.0 if won else 0.0) if wp is None else wp
|
||||||
|
|
||||||
|
|
||||||
|
def _bet_pnl(b, wp=None):
|
||||||
|
"""Resolved P&L of one cache bet at payout wp: a $size stake at avg price p
|
||||||
|
returns size·(wp−p)/p — wp=1 win, 0 loss, 0.5 refund ($0.50/share, NOT
|
||||||
|
money-back: flat near coin-flip entries, ruinous for favorites)."""
|
||||||
p = max(0.001, min(0.999, b["p"] or 0))
|
p = max(0.001, min(0.999, b["p"] or 0))
|
||||||
return b["size"] * ((1 - p) / p if b["won"] else -1)
|
if wp is None:
|
||||||
|
wp = _wp(b.get("cond"), b.get("asset"), b["won"])
|
||||||
|
return b["size"] * (wp - p) / p
|
||||||
|
|
||||||
|
|
||||||
def display_stats(w):
|
def display_stats(w):
|
||||||
@@ -121,35 +136,51 @@ def display_stats(w):
|
|||||||
now = time.time()
|
now = time.time()
|
||||||
bets = [b for b in cache.get_bets(w)
|
bets = [b for b in cache.get_bets(w)
|
||||||
if (b["size"] or 0) > 0 and (b["res_t"] or 0) <= now]
|
if (b["size"] or 0) > 0 and (b["res_t"] or 0) <= now]
|
||||||
|
# chain-truth payouts for everything this wallet's stats touch (cached
|
||||||
|
# in the resolutions table — incremental after the first backfill)
|
||||||
|
trows = trust.trusted_wallet_rows(cache.query, 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
|
# ---- ALL-TIME stats over EVERY trusted bet (any size): the dashboard's
|
||||||
# "of every bet placed" columns. Trusted rows only, deduped one-per-market,
|
# "of every bet placed" columns. Trusted rows only, deduped one-per-market,
|
||||||
# so scalper-poisoned cache marks can't inflate them. ----
|
# truth-adjusted: refunds (wp=0.5) count as neither won nor lost. ----
|
||||||
trows = trust.trusted_wallet_rows(cache.query, w)
|
|
||||||
tbest = {}
|
tbest = {}
|
||||||
for cond, asset, won, p, res_t, size in trows:
|
for cond, asset, won, p, res_t, size in trows:
|
||||||
if cond not in tbest or size > tbest[cond][2]:
|
if cond not in tbest or size > tbest[cond][3]:
|
||||||
tbest[cond] = (won, p, size)
|
tbest[cond] = (cond, asset, won, p, size)
|
||||||
all_won = sum(1 for won, _, _ in tbest.values() if won)
|
def tally(rows):
|
||||||
all_lost = len(tbest) - all_won
|
"""(won, lost, refunds, pnl) over (cond, asset, won, p, size) rows."""
|
||||||
all_pnl = sum(size * ((1 - p) / p if won else -1.0)
|
w_ = l_ = r_ = 0
|
||||||
for won, p, size in tbest.values())
|
pnl = 0.0
|
||||||
|
for cond, asset, won, p, size in rows:
|
||||||
|
wp = _wp(cond, asset, won)
|
||||||
|
pc = max(0.001, min(0.999, p or 0))
|
||||||
|
pnl += size * (wp - pc) / pc
|
||||||
|
if wp > 0.5:
|
||||||
|
w_ += 1
|
||||||
|
elif wp < 0.5:
|
||||||
|
l_ += 1
|
||||||
|
else:
|
||||||
|
r_ += 1
|
||||||
|
return w_, l_, r_, pnl
|
||||||
|
all_won, all_lost, all_ref, all_pnl = tally(tbest.values())
|
||||||
thr = cache.conv_cutoff(b["size"] for b in bets)
|
thr = cache.conv_cutoff(b["size"] for b in bets)
|
||||||
conv = [b for b in bets if b["size"] >= thr]
|
conv = [b for b in bets if b["size"] >= thr]
|
||||||
won = sum(1 for b in conv if b["won"])
|
|
||||||
recent = sorted(bets, key=lambda b: b["res_t"] or 0, reverse=True)[:500]
|
recent = sorted(bets, key=lambda b: b["res_t"] or 0, reverse=True)[:500]
|
||||||
cut30 = time.time() - 30 * 86400
|
cut30 = time.time() - 30 * 86400
|
||||||
conv30 = [b for b in conv if (b["res_t"] or 0) >= cut30]
|
conv30 = [b for b in conv if (b["res_t"] or 0) >= cut30]
|
||||||
won30 = sum(1 for b in conv30 if b["won"])
|
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))
|
||||||
out = {
|
out = {
|
||||||
"conv_win": round(100 * won / len(conv), 1) if conv else None,
|
"conv_win": round(100 * cw / (cw + cl), 1) if (cw + cl) else None,
|
||||||
"conv_won": won, "conv_lost": len(conv) - won,
|
"conv_won": cw, "conv_lost": cl, "conv_ref": cr,
|
||||||
"conv_pnl": round(sum(_bet_pnl(b) for b in conv)),
|
"conv_pnl": round(cpnl),
|
||||||
"conv30_win": round(100 * won30 / len(conv30), 1) if conv30 else None,
|
"conv30_win": round(100 * c3w / (c3w + c3l), 1) if (c3w + c3l) else None,
|
||||||
"conv30_won": won30, "conv30_lost": len(conv30) - won30,
|
"conv30_won": c3w, "conv30_lost": c3l, "conv30_ref": c3r,
|
||||||
"conv30_pnl": round(sum(_bet_pnl(b) for b in conv30)),
|
"conv30_pnl": round(c3pnl),
|
||||||
"realized_pnl": round(sum(_bet_pnl(b) for b in recent)),
|
"realized_pnl": round(tally(brow(recent))[3]),
|
||||||
"all_win": round(100 * all_won / (all_won + all_lost), 1) if (all_won + all_lost) else None,
|
"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_pnl": round(all_pnl),
|
"all_won": all_won, "all_lost": all_lost, "all_ref": all_ref, "all_pnl": round(all_pnl),
|
||||||
"pm_pnl": _pm_profit(w),
|
"pm_pnl": _pm_profit(w),
|
||||||
"avg_bet": round(sum(b["size"] for b in conv) / len(conv)) if conv else 0,
|
"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,
|
"copy_pnl": 0, "held_pnl": 0, "held_won": 0, "held_lost": 0, "sold": 0,
|
||||||
@@ -206,13 +237,18 @@ def display_stats(w):
|
|||||||
scalp += sh * pr - STAKE - openp[c]["fee"] - fee_out
|
scalp += sh * pr - STAKE - openp[c]["fee"] - fee_out
|
||||||
sold += 1; del openp[c]
|
sold += 1; del openp[c]
|
||||||
for c, p in openp.items(): # settle held bets at resolution
|
for c, p in openp.items(): # settle held bets at resolution
|
||||||
wv = resmap.get(p["a"])
|
wv = payouts.truth(c, p["a"]) # chain first: refunds pay 0.5
|
||||||
|
if wv is None:
|
||||||
|
wv = resmap.get(p["a"])
|
||||||
if wv is None:
|
if wv is None:
|
||||||
wv = _clob_winner(c, p["a"]) # clob fallback for out-of-pull markets
|
wv = _clob_winner(c, p["a"]) # clob fallback for out-of-pull markets
|
||||||
if wv is None:
|
if wv is None:
|
||||||
continue # not resolved yet -> exclude
|
continue # not resolved yet -> exclude
|
||||||
held += (p["sh"] if wv else 0) - STAKE - p["fee"] # redeem itself is fee-free
|
held += p["sh"] * wv - STAKE - p["fee"] # redeem itself is fee-free
|
||||||
hw += wv; hl += 1 - wv
|
if wv > 0.5:
|
||||||
|
hw += 1
|
||||||
|
elif wv < 0.5:
|
||||||
|
hl += 1
|
||||||
out.update(copy_pnl=round(scalp + held), held_pnl=round(held),
|
out.update(copy_pnl=round(scalp + held), held_pnl=round(held),
|
||||||
held_won=hw, held_lost=hl, sold=sold)
|
held_won=hw, held_lost=hl, sold=sold)
|
||||||
return out
|
return out
|
||||||
@@ -281,8 +317,9 @@ def main():
|
|||||||
# holders are judged on their real resolved sample (the replay's own held leg
|
# holders are judged on their real resolved sample (the replay's own held leg
|
||||||
# is mostly "unresolved" for them and only reported for display).
|
# is mostly "unresolved" for them and only reported for display).
|
||||||
tr = trust.conviction_record(cache.query, c["wallet"], days=TRUST_DAYS,
|
tr = trust.conviction_record(cache.query, c["wallet"], days=TRUST_DAYS,
|
||||||
pctile=cache.CONV_PCTILE)
|
pctile=cache.CONV_PCTILE, truthfn=payouts.truth)
|
||||||
c["trust_n"], c["trust_wr"], c["trust_roi"] = tr["n"], round(tr["wr"], 3), round(tr["roi"], 3)
|
c["trust_n"], c["trust_wr"], c["trust_roi"] = tr["n"], round(tr["wr"], 3), round(tr["roi"], 3)
|
||||||
|
c["trust_refunds"] = tr.get("refunds", 0)
|
||||||
# SELECT a copyable sharp: active, copy-positive (fee-aware replay), and a
|
# SELECT a copyable sharp: active, copy-positive (fee-aware replay), and a
|
||||||
# genuine hold-to-resolution edge — trailing trusted conviction record wins a
|
# genuine hold-to-resolution edge — trailing trusted conviction record wins a
|
||||||
# clear majority with positive flat-stake ROI on a real sample, so the edge
|
# clear majority with positive flat-stake ROI on a real sample, so the edge
|
||||||
|
|||||||
Reference in New Issue
Block a user