Files
winning-wallet-finder/live/trust.py
T
jaxperro 4941818d51 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>
2026-07-07 01:55:50 -04:00

153 lines
7.1 KiB
Python

#!/usr/bin/env python3
"""Trusted-row filtering for cache.duckdb — the 2026-07-03 data-integrity fix.
Two ways a cached bet row can lie (see FINDINGS.md "The holder blind spot"):
1. res_t = ts fallback: when the data-api omits `endDate` on a closed
position, insider.resolved_bets stores the wallet's SELL time as res_t and
`won = curPrice >= 0.5` at pull time. A scalper's sold-at-profit position
then masquerades as a resolved win at a fake resolution time
(ArbTraderRookie's 1,997 legacy rows were 100% this).
2. stale marks: `won` is only authoritative if the wallet was pulled AFTER
the market resolved; rows pulled earlier carry a price mark, and v2 rows
say so via resolved=False, but legacy rows can't.
A row is TRUSTED iff res_t <= now AND either:
* **v2 self-certifying**: resolved = TRUE. resolved is only True when the
pull saw a real endDate AND the market had ended, so res_t is endDate-based
(never the ts fallback) and won was observed post-end. No consensus needed —
important because Polymarket sometimes REWRITES endDate after resolution,
so a freshly-pulled wallet's corrected res_t stops matching the stale
cross-wallet mode (2026-07-04: this wrongly distrusted 374/454 of iohihoo's
rows the morning after his re-pull).
* **legacy consensus** (resolved IS NULL, pre-v2 rows): res_t equals the
market's modal res_t across >= 2 distinct wallets (endDate rows agree;
ts-fallback sell times scatter), and the wallet's pulled_at >= E so won was
observed after resolution. If the wallet is missing from `pulled` (daily.sh
invalidates watchlist wallets by DELETING their pulled row; a transiently
failed re-pull then leaves them absent), fall back to trusting markets
resolved >= 14 days ago — every wallet gets re-pulled at least each
MAX_AGE_DAYS cycle, so its rows' true pull time is at most ~2 weeks old.
(resolved = FALSE rows — price marks on unended markets — are never trusted.)
~13.5M of 19.2M rows pass; what's dropped is exactly the poison that made
scalpers look like 99%-win holders. Selection must only ever score trusted rows.
This module has NO cache.py import (so read-only scripts that open their own
connection can use it without a second in-process connection fighting the
single-writer lock). Callers pass a `runq(sql, params) -> rows` callable.
"""
import time
# CTE fragments: prepend inside `WITH ...` and select from `trusted`.
# {now} must be substituted with an int epoch.
TRUSTED_CTE = """
tr_r AS (SELECT DISTINCT wallet, cond, asset, won, p, res_t, size, src, ts, resolved
FROM bets WHERE res_t > 0 AND size > 0),
tr_cons AS (SELECT cond, res_t AS E FROM (
SELECT cond, res_t, count(DISTINCT wallet) nw,
row_number() OVER (PARTITION BY cond
ORDER BY count(DISTINCT wallet) DESC, count(*) DESC) rn
FROM tr_r GROUP BY cond, res_t) WHERE rn = 1 AND nw >= 2),
trusted AS (
SELECT tr_r.* FROM tr_r
LEFT JOIN tr_cons ON tr_r.cond = tr_cons.cond AND tr_r.res_t = tr_cons.E
LEFT JOIN pulled pl ON pl.wallet = tr_r.wallet
WHERE tr_r.res_t <= {now} AND (
tr_r.resolved = TRUE
OR (tr_r.resolved IS NULL AND tr_cons.E IS NOT NULL
AND tr_cons.E <= COALESCE(pl.pulled_at, {now} - 1209600))))
"""
def cte(now=None):
"""The trusted-rows CTE body with {now} filled in."""
return TRUSTED_CTE.format(now=int(now or time.time()))
def ensure_cons(runq, now=None):
"""Materialize the consensus map once per connection as TEMP TABLE t_cons
(cond, E) so repeated per-wallet queries don't re-scan 19M rows. Temp
tables are allowed on read-only connections."""
have = runq("SELECT count(*) FROM information_schema.tables "
"WHERE table_name = 't_cons'", [])
if have and have[0][0]:
return
runq(f"""CREATE TEMP TABLE t_cons AS
WITH r AS (SELECT DISTINCT wallet, cond, res_t
FROM bets WHERE res_t > 0 AND size > 0)
SELECT cond, res_t AS E FROM (
SELECT cond, res_t, count(DISTINCT wallet) nw,
row_number() OVER (PARTITION BY cond
ORDER BY count(DISTINCT wallet) DESC, count(*) DESC) rn
FROM r GROUP BY cond, res_t) WHERE rn = 1 AND nw >= 2
AND res_t <= {int(now or time.time())}""", [])
def trusted_wallet_rows(runq, wallet, now=None):
"""This wallet's trusted resolved bets as (cond, won, p, res_t, size),
deduped per token. Requires ensure_cons() first."""
now = int(now or time.time())
return runq("""
SELECT DISTINCT b.cond, b.asset, b.won,
least(0.999, greatest(0.001, b.p)) p, b.res_t, b.size
FROM bets b
LEFT JOIN t_cons c ON b.cond = c.cond AND b.res_t = c.E
LEFT JOIN pulled pl ON pl.wallet = b.wallet
WHERE b.wallet = ? AND b.size > 0 AND b.res_t > 0 AND b.res_t <= ?
AND (b.resolved = TRUE
OR (b.resolved IS NULL AND c.E IS NOT NULL
AND c.E <= COALESCE(pl.pulled_at, ? - 1209600)))""",
[wallet, now, now])
def conviction_record(runq, wallet, days=90, pctile=0.80, now=None, truthfn=None):
"""Trailing trusted CONVICTION record for the held-edge gate:
{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
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
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())
rows = trusted_wallet_rows(runq, wallet, now)
if not rows:
return dict(n=0, wr=0.0, roi=0.0, refunds=0)
sizes = sorted(r[5] for r in rows)
k = (len(sizes) - 1) * pctile
f = int(k)
thr = sizes[f] if f + 1 >= len(sizes) else sizes[f] + (sizes[f + 1] - sizes[f]) * (k - f)
cut = now - days * 86400
# one bet per market: keep the largest-stake token row
best = {}
for cond, asset, won, p, res_t, size in rows:
if size >= thr and res_t >= cut:
if cond not in best or size > best[cond][3]:
best[cond] = (cond, asset, won, p, size)
conv = list(best.values())
if not conv:
return dict(n=0, wr=0.0, roi=0.0, refunds=0)
wins = losses = refunds = 0
roi_sum = 0.0
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)