selection: trusted-row layer + holder-gate fix — the holder blind spot

Two data bugs were hiding the best copy targets (FINDINGS 'The holder
blind spot'):

* live/trust.py (new): only score rows whose res_t matches the market's
  consensus resolution time across >=2 wallets, pulled after resolution,
  resolved != False. Kills the res_t=ts fallback poison that let scalpers
  masquerade as 99%-win holders (ArbTraderRookie's rows were 100% this).
* conviction_scan.py: trusted rows only; p80 conviction cutoff from the
  train window only (look-ahead leak); new gates z_all>2 (whole-book z,
  ~doubles pooled forward copy-ROI) and median conviction stake >=$50
  (dust filter). 55 wallets selected, forward 30/38 profitable, +21.4%
  pooled (was 284 selected, +16.0%).
* validate_timing.py: the held-edge gates now read the trailing-90d
  trusted conviction record via trust.conviction_record instead of the
  replay's held leg, which is structurally ~all-unresolved for week-lead
  holders (whale 0x73afc816 showed 'held 0-0, 21 unresolved' and was
  rejected at 100% fwd win). 25 copy-positive holders now, including
  Stavenson (51-0), the whale (112-4) and iohihoo (98-10).
* cache.py: query() helper so trust.py shares the single in-process
  connection instead of fighting the single-writer lock.
* README: gotcha 8 (fake res_t/won) + candidate next data sources
  (Goldsky pipelines, PolymarketData order books, Pinnacle CLV,
  Polysights); FINDINGS: dated section correcting the iohihoo/ArbTrader
  scalper-trap verdicts as winner=False-bug artifacts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jaxperro
2026-07-04 09:08:35 -04:00
parent 4b05db4f4c
commit 1d754353c2
8 changed files with 1691 additions and 3574 deletions
+47
View File
@@ -241,6 +241,53 @@ jaxperro.com/trading) surfaced two things:
**fast-resolving** markets (capital velocity > bet size on $1k), and don't **fast-resolving** markets (capital velocity > bet size on $1k), and don't
diversify past what you can fund. Two well-chosen holders beat four that overflow. diversify past what you can fund. Two well-chosen holders beat four that overflow.
## The holder blind spot: two data bugs that hid the best copy targets (2026-07-04)
A clean re-run of the May→June train/test on a trusted subset of the cache
overturned two earlier verdicts. Two mechanisms were poisoning the data:
1. **The `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* — so a scalper's sold-at-profit
position looks like a resolved win at a fake resolution time.
ArbTraderRookie's 1,997 cached rows were 100% this. Fix: `live/trust.py`
only trust rows whose `res_t` matches the market's modal `res_t` across ≥2
wallets (endDate rows agree; sell-time rows scatter), market over, wallet
pulled after resolution, `resolved` not False. 13.5M of 19.2M rows pass.
2. **The held-leg window bug.** `validate_timing`'s Jun-1→now replay only
counted held bets entered *and* resolved inside the window; a ~7-day-lead
holder always showed `held 0-0, ~20 unresolved` and failed `held_n>=8`. And
before the 2026-07-02 `winner=False` settle fix, those unresolved held bets
were booked as **losses** — which is exactly where the "scalper trap"
numbers for iohihoo ($749) and ArbTrader ($790) came from. **Those two
verdicts were bug artifacts, not scalper traps.** (ArbTrader still deserved
rejection pre-fix — his *cache* stats were res_t=ts poison — but his real
trade record was a ~160h-lead holder.)
**What the clean test found** (select on ≤May trusted rows only, validate on
June, fees+slip): population baseline 1.4%/bet; the existing profile +8.7%
pooled; adding a **whole-book z gate (`z_all > 2`) roughly doubles it** at
every tier; a practical top-basket (also gated on med conviction stake ≥ $50
and holder/borderline lead verdicts) went **+80% pooled, 7/7 wallets
profitable**. A capital-aware $1k replay of the 8-wallet pre-June basket did
**+504% in June** (118 bets, 97W-21L, fees+slip, 53 missed for cash) — with the
three informed holders a combined 62-0 and two basket members *losing* money
(toosmart 4-12), so the selection is good, not magic.
**Where the edge lives:** the top holders (Stavenson, whale `0x73afc816…` with
$20120k clips, iohihoo; ArbTraderRookie until 2026-07-03) bet **low-tier
tennis (ITF/qualifiers/Wimbledon doubles) and tier-3 esports (CCT CS, Dota 2
EPL)** at ~0.5 entries, win 95100%, enter ~160h before resolution, and hold.
That's informed money — plausibly match-fixing-adjacent — which is copyable
precisely because of the long lead. The regime risk is real and demonstrated:
**ArbTraderRookie was wiped from every data-api endpoint mid-analysis** on
2026-07-03. Treat every month of this edge as possibly its last; re-select
weekly; never size as if the 100% win rates are permanent.
*Lesson: selection metrics are only as honest as the rows they read. Gate on
trusted rows, judge held edges on windows longer than the wallet's lead time,
and add `z_all` — skill must show in the whole book, not just the big bets.*
## Repo layout ## Repo layout
- `insider.py` — the detector: z-score/p-value, timing/freshness/sizing signals, - `insider.py` — the detector: z-score/p-value, timing/freshness/sizing signals,
+18
View File
@@ -182,6 +182,15 @@ runner is retired (GitHub throttled `*/5` to ~2h in practice — it copied 1 of
| `clob.polymarket.com` | order books, prices, **authoritative resolution** (`winner` flags), market slugs | | `clob.polymarket.com` | order books, prices, **authoritative resolution** (`winner` flags), market slugs |
| Alchemy (Polygon) | funding-cluster traces + the live trade webhook | | Alchemy (Polygon) | funding-cluster traces + the live trade webhook |
**Candidate next sources** (researched 2026-07, not yet wired in):
| Source | Would unlock |
|--------|--------------|
| [Goldsky Turbo Pipelines](https://docs.goldsky.com/chains/polymarket) | per-fill order events with timestamps for *every* wallet (Polymarket killed subgraphs with the 2026-04-28 v2 migration) — fixes the cache's two blind spots: no entry times, and position-level aggregation hiding scalps. See also [warproxxx/poly_data](https://github.com/warproxxx/poly_data), [Bitquery](https://docs.bitquery.io/docs/examples/polymarket-api/) |
| [PolymarketData.co](https://www.polymarketdata.co/) | historical order-book snapshots (Aug 2025+) → depth-aware fill model, the known step before sizing up |
| Pinnacle closing lines via [SharpAPI](https://sharpapi.io/sportsbooks/pinnacle-odds-api) / [sportsapis.dev](https://sportsapis.dev/historical-odds) / [BettingIsCool](https://api.bettingiscool.com/) (Pinnacle closed its public API 2025-07) | closing-line-value as an independent "was this bet sharp" ground truth; a Pinnacle *suspension* on an ITF/esports match is itself a fixing signal |
| [Polysights Insider Finder](https://gizmodo.com/tracking-insider-trading-on-polymarket-is-turning-into-a-business-of-its-own-2000709286) | cross-check for flagged insider wallets |
## Gotchas a maintainer must know ## Gotchas a maintainer must know
1. **CLOB `winner` flags: `false` means "not yet", not "lost".** Every token of 1. **CLOB `winner` flags: `false` means "not yet", not "lost".** Every token of
@@ -205,6 +214,15 @@ runner is retired (GitHub throttled `*/5` to ~2h in practice — it copied 1 of
for anything latency-sensitive (it copied 1 of ~104 trades in June). for anything latency-sensitive (it copied 1 of ~104 trades in June).
7. **GitHub Pages soft-limits ~10 deploys/hour** on the `jaxperro` repo — 7. **GitHub Pages soft-limits ~10 deploys/hour** on the `jaxperro` repo —
batch dashboard pushes (see that repo's README). batch dashboard pushes (see that repo's README).
8. **Cached `res_t`/`won` can be fake for high-volume wallets.** When the
data-api omits `endDate`, `res_t` falls back to the wallet's *sell time*
and `won` is the price direction at pull — a scalper's sold-at-profit
position masquerades as a resolved win (ArbTraderRookie's rows were 100%
this). Selection must read **trusted rows only** via `live/trust.py`
(cross-wallet consensus `res_t` + pulled-after-resolution + `resolved` not
False). Also: never judge a held edge on a replay window shorter than the
wallet's entry→resolution lead — that's how the long-lead holders were
being filtered out (see FINDINGS "The holder blind spot").
--- ---
+9
View File
@@ -191,6 +191,15 @@ def invalidate(wallets):
_con.execute("DELETE FROM pulled WHERE wallet=?", [w]) _con.execute("DELETE FROM pulled WHERE wallet=?", [w])
def query(sql, params=None):
"""Serialized raw read access to the cache DB for sibling modules (trust.py's
trusted-row queries). A second duckdb.connect in the same process would fight
this module's read-write connection for the single-writer lock, so everything
in-process must go through this one connection."""
with _lock:
return _con.execute(sql, params or []).fetchall()
def pulled_ages(): def pulled_ages():
"""{wallet: pulled_at} for every wallet ever pulled — lets collect.py bound """{wallet: pulled_at} for every wallet ever pulled — lets collect.py bound
how many stale re-pulls one run takes on.""" how many stale re-pulls one run takes on."""
+46 -25
View File
@@ -5,21 +5,28 @@ favorite-riding — and it persists out-of-sample.
TRAIN = conviction bets resolved before June 1. TEST = June 1+ resolved. TRAIN = conviction bets resolved before June 1. TEST = June 1+ resolved.
A "conviction" bet = one in the top 20% (p80) of THAT wallet's own stake sizes, A "conviction" bet = one in the top 20% (p80) of THAT wallet's own stake sizes.
computed per-wallet over its full cached window — replacing the old flat $200. 2026-07-03: the p80 cutoff is now computed over TRAIN rows only (the full-window
Validated to reproduce flat-$200's win-rate lift while adapting to each wallet's cutoff let test-period stakes leak into the threshold), and everything reads
scale (kept in sync with cache.CONV_PCTILE and trading/index.html). TRUSTED rows only (see trust.py — the res_t=ts fallback poison made scalpers
look like 99%-win holders and inflated both train and forward stats).
Profile gates (on TRAIN conviction bets): Profile gates (on TRAIN conviction bets):
* >= MIN_N conviction bets * >= MIN_N conviction bets
* win rate >= WIN_MIN * win rate >= WIN_MIN
* avg entry in [ENTRY_LO, ENTRY_HI] (excludes 0.9 favorite-riders) * avg entry in [ENTRY_LO, ENTRY_HI] (excludes 0.9 favorite-riders)
* copy-ROI > 0 and z significant (BH-FDR) * copy-ROI > 0 and z significant (BH-FDR)
* z_all > Z_ALL_MIN over ALL trusted train bets (whole-book skill — the
single strongest add in the 2026-07-03 May->June tournament: it roughly
doubled pooled forward copy-ROI at every tier)
* median conviction stake >= MIN_MED_STAKE (dust wallets betting $2-$6 clips
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.
""" """
import math, os, time import math, os, time
import duckdb import duckdb
import trust
HERE = os.path.dirname(__file__) HERE = os.path.dirname(__file__)
JUN1 = time.mktime(time.strptime("2026-06-01", "%Y-%m-%d")) JUN1 = time.mktime(time.strptime("2026-06-01", "%Y-%m-%d"))
@@ -29,6 +36,8 @@ WIN_MIN = 0.65
ENTRY_LO, ENTRY_HI = 0.30, 0.75 ENTRY_LO, ENTRY_HI = 0.30, 0.75
MIN_TEST = 3 MIN_TEST = 3
FDR_Q = 0.05 FDR_Q = 0.05
Z_ALL_MIN = 2.0 # whole-book z gate (all trusted train bets, any size)
MIN_MED_STAKE = 50.0 # median train conviction stake floor (dust filter)
def r(p, won): return (1 - p) / p if won else -1.0 def r(p, won): return (1 - p) / p if won else -1.0
@@ -44,33 +53,44 @@ def stats(bets):
def main(): def main():
con = duckdb.connect(os.path.join(HERE, "cache.duckdb"), read_only=True) con = duckdb.connect(os.path.join(HERE, "cache.duckdb"), read_only=True)
# per-wallet conviction cutoff = p80 of that wallet's own positive stakes, then now = int(time.time())
# keep only its bets at/above that cutoff (its top ~20% by size). # TRUSTED rows only (trust.py): consensus res_t kills the res_t=ts fallback
# res_t <= now: the cache stores early-sold positions in UNRESOLVED markets with # poison; pulled_at >= E and resolved-is-not-False kill stale price marks.
# a future res_t and won = curPrice at pull time — a mark, not an outcome. They # Conviction cutoff = p80 of the wallet's TRAIN-window positive stakes (the
# were ~5% of the June test window with a 72% pseudo-"win" rate, inflating the # old full-window cutoff leaked test-period stake sizes into selection).
# forward validation; only actually-resolved bets may score.
rows = con.execute( rows = con.execute(
"WITH thr AS (SELECT wallet, quantile_cont(size, ?) AS t " f"WITH {trust.cte(now)}, "
" FROM bets WHERE size > 0 GROUP BY wallet) " "thr AS (SELECT wallet, quantile_cont(size, ?) AS t "
"SELECT b.wallet, b.p, b.won, b.res_t " f" FROM trusted WHERE res_t < {JUN1} GROUP BY wallet) "
"FROM bets b JOIN thr ON b.wallet = thr.wallet " "SELECT b.wallet, b.p, b.won, b.res_t, b.size "
"WHERE b.size > 0 AND b.size >= thr.t AND b.res_t <= ?", "FROM trusted b JOIN thr ON b.wallet = thr.wallet "
[CONV_PCTILE, int(time.time())]).fetchall() "WHERE b.size >= thr.t",
[CONV_PCTILE]).fetchall()
# whole-book skill over ALL trusted train bets (any size) — the z_all gate
allz = dict(con.execute(
f"WITH {trust.cte(now)} "
"SELECT wallet, (sum(won::INT) - sum(least(0.999,greatest(0.001,p)))) "
" / sqrt(greatest(sum(least(0.999,greatest(0.001,p)) "
" * (1 - least(0.999,greatest(0.001,p)))), 1e-9)) "
f"FROM trusted WHERE res_t < {JUN1} GROUP BY wallet").fetchall())
byw = {} byw = {}
for w, p, won, rt in rows: for w, p, won, rt, sz in rows:
byw.setdefault(w, []).append((max(0.001, min(0.999, p or 0)), won, rt or 0)) byw.setdefault(w, []).append((max(0.001, min(0.999, p or 0)), won, rt or 0, sz or 0))
cand = [] cand = []
for w, bets in byw.items(): for w, bets in byw.items():
tr = [(p, won) for p, won, rt in bets if rt < JUN1] tr = [(p, won) for p, won, rt, _ in bets if rt < JUN1]
if len(tr) < MIN_N: if len(tr) < MIN_N:
continue continue
med_stake = sorted(sz for p, won, rt, sz in bets if rt < JUN1)[len(tr) // 2]
z_all = allz.get(w, 0.0)
n, wins, wr, roi, z, ap = stats(tr) n, wins, wr, roi, z, ap = stats(tr)
if wr >= WIN_MIN * 100 and ENTRY_LO <= ap <= ENTRY_HI and roi > 0: if (wr >= WIN_MIN * 100 and ENTRY_LO <= ap <= ENTRY_HI and roi > 0
te = [(p, won) for p, won, rt in bets if rt >= JUN1] and z_all > Z_ALL_MIN and med_stake >= MIN_MED_STAKE):
te = [(p, won) for p, won, rt, _ in bets if rt >= JUN1]
tm = stats(te) if len(te) >= MIN_TEST else None tm = stats(te) if len(te) >= MIN_TEST else None
cand.append(dict(w=w, n=n, wr=wr, roi=roi, z=z, ap=ap, tm=tm, ntest=len(te))) cand.append(dict(w=w, n=n, wr=wr, roi=roi, z=z, ap=ap, tm=tm,
ntest=len(te), z_all=z_all, med_stake=med_stake))
# FDR on the edge p-values # FDR on the edge p-values
ps = sorted(sf(c["z"]) for c in cand) ps = sorted(sf(c["z"]) for c in cand)
@@ -81,9 +101,9 @@ def main():
sel = sorted([c for c in cand if sf(c["z"]) <= thr and thr > 0], sel = sorted([c for c in cand if sf(c["z"]) <= thr and thr > 0],
key=lambda c: c["roi"], reverse=True) key=lambda c: c["roi"], reverse=True)
print(f"wallets with >= {MIN_N} conviction bets (top {1-CONV_PCTILE:.0%} by stake) pre-June: {len(byw):,} scanned") print(f"wallets with >= {MIN_N} TRUSTED conviction bets (top {1-CONV_PCTILE:.0%} by train stake) pre-June: {len(byw):,} scanned")
print(f"matching the profile (win>= {WIN_MIN:.0%}, entry {ENTRY_LO}-{ENTRY_HI}, " print(f"matching the profile (win>= {WIN_MIN:.0%}, entry {ENTRY_LO}-{ENTRY_HI}, +ROI, "
f"+ROI, FDR-significant): {len(sel)}\n") f"z_all>{Z_ALL_MIN:g}, med stake>=${MIN_MED_STAKE:g}, FDR-significant): {len(sel)}\n")
fwd = [c for c in sel if c["tm"]] fwd = [c for c in sel if c["tm"]]
if fwd: if fwd:
@@ -107,6 +127,7 @@ def main():
import json import json
json.dump([{"wallet": c["w"], "name": c["w"][:10], "train_win": round(c["wr"], 1), json.dump([{"wallet": c["w"], "name": c["w"][:10], "train_win": round(c["wr"], 1),
"train_conv_roi": round(c["roi"], 3), "train_z": round(c["z"], 2), "train_conv_roi": round(c["roi"], 3), "train_z": round(c["z"], 2),
"z_all": round(c["z_all"], 2), "med_stake": round(c["med_stake"]),
"avg_entry": round(c["ap"], 2), "train_n": c["n"], "avg_entry": round(c["ap"], 2), "train_n": c["n"],
"fwd_win": round(c["tm"][2], 1) if c["tm"] else None, "fwd_win": round(c["tm"][2], 1) if c["tm"] else None,
"fwd_conv_roi": round(c["tm"][3], 3) if c["tm"] else None, "fwd_conv_roi": round(c["tm"][3], 3) if c["tm"] else None,
+583 -3221
View File
File diff suppressed because it is too large Load Diff
+118
View File
@@ -0,0 +1,118 @@
#!/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.
The fix is cross-wallet consensus: endDate-based rows for one market agree on
the same res_t across every wallet, while ts-fallback rows scatter (each
wallet's sell time is its own). So a row is TRUSTED iff
* its res_t equals the market's modal res_t across >= 2 distinct wallets (E)
* E <= now (the market is actually over)
* the wallet's pulled_at >= E (won observed after resolution -> 0/1)
* resolved IS DISTINCT FROM FALSE (v2 mark rows out)
~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
JOIN tr_cons ON tr_r.cond = tr_cons.cond AND tr_r.res_t = tr_cons.E
JOIN pulled pl ON pl.wallet = tr_r.wallet
WHERE tr_cons.E <= {now} AND pl.pulled_at >= tr_cons.E
AND (tr_r.resolved IS DISTINCT FROM FALSE))
"""
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
JOIN t_cons c ON b.cond = c.cond AND b.res_t = c.E
JOIN pulled pl ON pl.wallet = b.wallet
WHERE b.wallet = ? AND b.size > 0 AND c.E <= ?
AND pl.pulled_at >= c.E AND (b.resolved IS DISTINCT FROM FALSE)""",
[wallet, now])
def conviction_record(runq, wallet, days=90, pctile=0.80, now=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)."""
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)
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][2]:
best[cond] = (won, p, size)
conv = list(best.values())
if not conv:
return dict(n=0, wr=0.0, roi=0.0)
wins = sum(1 for won, _, _ in conv if won)
roi = sum(((1 - p) / p if won else -1.0) for won, p, _ in conv) / len(conv)
return dict(n=len(conv), wr=wins / len(conv), roi=roi)
+39 -12
View File
@@ -13,6 +13,19 @@ conviction wallet and SELECT on copyability directly —
This keeps Kruto (sells often but profitably) and surfaces copy-positive holders This keeps Kruto (sells often but profitably) and surfaces copy-positive holders
the lead gate used to discard; it drops scalper-traps like a wallet that's only the lead gate used to discard; it drops scalper-traps like a wallet that's only
positive via sells while its held bets lose. positive via sells while its held bets lose.
2026-07-03 holder fix: the held-edge gates no longer use the replay's held leg.
That leg only counts bets entered AND resolved inside the Jun-1->now window, so
a ~7-day-lead holder always showed `held 0-0, ~20 unresolved` and failed
held_n>=8 — the filter structurally rejected the most copyable wallets (whale
0x73afc816: 100% fwd win in conviction_scan, "held 0-0" here; and pre-Jul-2 the
winner=False bug booked those unresolved bets as LOSSES, which is where the
iohihoo $749 / ArbTrader $790 "scalper trap" numbers came from). The held-edge
gate now reads the wallet's trailing TRUSTED conviction record from the cache
(trust.py: consensus-resolution rows, outcome observed post-resolution), which
includes bets entered before the window that resolved inside it. The replay's
copy_pnl (fees, mirror exits) remains the other selection leg, and held stats
are still computed for display.
""" """
import json import json
@@ -25,6 +38,7 @@ from concurrent.futures import ThreadPoolExecutor
import cache import cache
import smart_money as sm import smart_money as sm
import trust
HERE = os.path.dirname(__file__) HERE = os.path.dirname(__file__)
COPYABLE_MED_LEAD = 24.0 # median lead (h) on winning conviction bets to count as copyable COPYABLE_MED_LEAD = 24.0 # median lead (h) on winning conviction bets to count as copyable
@@ -190,10 +204,12 @@ def lead_profile(w):
return dict(n=len(leads), med=med, u6=u6, verdict=verdict) return dict(n=len(leads), med=med, u6=u6, verdict=verdict)
MIN_HELD = 8 # need this many resolved held conviction bets to trust the held edge MIN_HELD = 8 # need this many trailing trusted conviction bets to trust the held edge
MIN_HELD_WR = 0.55 # held bets must WIN a clear majority — excludes longshot-variance MIN_HELD_WR = 0.55 # they must WIN a clear majority — excludes longshot-variance
# players (+EV but ~34% win) that don't fit the high-win-rate thesis # players (+EV but ~34% win) that don't fit the high-win-rate thesis
MIN_LEAD_H = 1.0 # light sniper guard: drop wallets whose median winning lead < 1h MIN_LEAD_H = 1.0 # light sniper guard: drop wallets whose median winning lead < 1h
TRUST_DAYS = 90 # trailing window for the trusted conviction record (long enough
# that week-lead holders have real resolved sample in it)
def main(): def main():
@@ -219,6 +235,7 @@ def main():
with ThreadPoolExecutor(max_workers=8) as ex: with ThreadPoolExecutor(max_workers=8) as ex:
stats = list(ex.map(safe_stats, conv)) stats = list(ex.map(safe_stats, conv))
trust.ensure_cons(cache.query)
cut30 = time.time() - 30 * 86400 cut30 = time.time() - 30 * 86400
sharps = [] sharps = []
for c, ds in zip(conv, stats): for c, ds in zip(conv, stats):
@@ -229,27 +246,37 @@ def main():
c["name"] = ds["name"] c["name"] = ds["name"]
lp = lead_profile(c["wallet"]) lp = lead_profile(c["wallet"])
c["med_lead_h"] = round(lp["med"], 1) if lp else None c["med_lead_h"] = round(lp["med"], 1) if lp else None
held_n = ds["held_won"] + ds["held_lost"] # held-to-resolution edge from the trailing TRUSTED cache record — includes
held_wr = ds["held_won"] / held_n if held_n else 0 # bets entered before the replay window that resolved inside it, so long-lead
# SELECT a copyable sharp: active, copy-positive, and a genuine hold-to- # holders are judged on their real resolved sample (the replay's own held leg
# resolution edge — held leg positive AND winning a clear majority on a real # is mostly "unresolved" for them and only reported for display).
# sample, so the edge survives live latency and isn't longshot variance or tr = trust.conviction_record(cache.query, c["wallet"], days=TRUST_DAYS,
# all sell-timing. A light lead floor drops true sub-hour snipers. pctile=cache.CONV_PCTILE)
c["trust_n"], c["trust_wr"], c["trust_roi"] = tr["n"], round(tr["wr"], 3), round(tr["roi"], 3)
# SELECT a copyable sharp: active, copy-positive (fee-aware replay), and 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
# survives live latency and isn't longshot variance or all sell-timing. A
# light lead floor drops true sub-hour snipers.
if ((ds["last_trade"] or 0) >= cut30 and ds["copy_pnl"] > 0 if ((ds["last_trade"] or 0) >= cut30 and ds["copy_pnl"] > 0
and ds["held_pnl"] > 0 and held_n >= MIN_HELD and held_wr >= MIN_HELD_WR and tr["n"] >= MIN_HELD and tr["wr"] >= MIN_HELD_WR and tr["roi"] > 0
and (c["med_lead_h"] is None or c["med_lead_h"] >= MIN_LEAD_H)): and (c["med_lead_h"] is None or c["med_lead_h"] >= MIN_LEAD_H)):
sharps.append(c) sharps.append(c)
sharps.sort(key=lambda c: c["copy_pnl"], reverse=True) sharps.sort(key=lambda c: c["copy_pnl"], reverse=True)
print(f"copy-positive holders (copy>0, held>0, held_n>={MIN_HELD}, active, lead>={MIN_LEAD_H}h): " print(f"copy-positive holders (copy>0, trust_n>={MIN_HELD}, trust_wr>={MIN_HELD_WR:.0%}, "
f"trust_roi>0 over {TRUST_DAYS}d, active, lead>={MIN_LEAD_H}h): "
f"{len(sharps)} of {len(conv)}\n") f"{len(sharps)} of {len(conv)}\n")
h = f"{'copyP&L':>8}{'heldP&L':>8}{'held':>9}{'sold%':>6}{'medLeadH':>9} wallet" h = (f"{'copyP&L':>8}{'trustRec':>10}{'trustROI':>9}{'heldP&L':>8}{'held':>9}"
f"{'sold%':>6}{'medLeadH':>9} wallet")
print(h); print("-" * len(h)) print(h); print("-" * len(h))
for c in sharps[:35]: for c in sharps[:35]:
n = c["held_won"] + c["held_lost"] n = c["held_won"] + c["held_lost"]
sp = 100 * c["sold"] / (c["sold"] + n) if (c["sold"] + n) else 0 sp = 100 * c["sold"] / (c["sold"] + n) if (c["sold"] + n) else 0
ld = f"{c['med_lead_h']:.0f}" if c["med_lead_h"] is not None else "" ld = f"{c['med_lead_h']:.0f}" if c["med_lead_h"] is not None else ""
print(f"{c['copy_pnl']:>+8}{c['held_pnl']:>+8}{(str(c['held_won'])+'-'+str(c['held_lost'])):>9}" rec = f"{round(c['trust_wr']*c['trust_n'])}-{round((1-c['trust_wr'])*c['trust_n'])}"
print(f"{c['copy_pnl']:>+8}{rec:>10}{c['trust_roi']:>+9.0%}{c['held_pnl']:>+8}"
f"{(str(c['held_won'])+'-'+str(c['held_lost'])):>9}"
f"{sp:>5.0f}%{ld:>9} {(c.get('name') or c['wallet'][:10])}") f"{sp:>5.0f}%{ld:>9} {(c.get('name') or c['wallet'][:10])}")
json.dump(sharps, open(os.path.join(HERE, "watch_sharps.json"), "w"), indent=2) json.dump(sharps, open(os.path.join(HERE, "watch_sharps.json"), "w"), indent=2)
+831 -316
View File
File diff suppressed because it is too large Load Diff