diff --git a/.gitignore b/.gitignore
index fbeff8ca..0f9aa679 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,3 +24,12 @@ watcher_state.json
huntwide.csv
oos.log
hunt.log
+
+# live/ + wide/ — large local data + regenerable artifacts
+*.duckdb
+*.duckdb.wal
+live/candidates.json
+live/scored.json
+live/*_scored.json
+live/watch_prejune*.json
+live/history/
diff --git a/FINDINGS.md b/FINDINGS.md
index 969ac6ac..0dc1f282 100644
--- a/FINDINGS.md
+++ b/FINDINGS.md
@@ -116,6 +116,32 @@ real forward (out-of-sample) data on these wallets — observe before you size u
- A durable trading edge has to come from *you* (a niche you know), with this
tooling built around your judgment.
+## The skilled-3% scan, and a clean out-of-sample loss (June 2026)
+
+External validation arrived: an LBS/Yale study (Gomez-Cram, Guo, Kung, Jensen,
+Apr 2026; SSRN 5910522) over 1.72M accounts found only **~3.14%** of traders are
+genuinely skilled — measured by randomizing each trader's bet *directions* 10k×
+(a Monte-Carlo z-score) and requiring out-of-sample persistence. That is exactly
+this project's z-score + `oos.py` method, independently confirmed.
+
+Built `live/` to operationalize it at scale: enumerate recent liquid markets →
+cache every candidate's resolved bets locally (~26k wallets / 12.5M bets, so
+re-scoring at any cutoff is seconds) → a 5-gate funnel (n≥15, z>0, BH-FDR,
+split-half OOS, MM/bot cap). It surfaced 107 "validated" wallets.
+
+**The decisive test.** Copying the high-win-rate "favorite-rider" cohort, $1000,
+no execution lag, June 1→now:
+- selected *through* the test window (look-ahead): 99% win rate, **+23.6%**.
+- selected on **pre-June-1 data only** (honest): 68% win rate, **−7.4%**
+ (−19% on the settled portion).
+
+The +23.6% was selection bias. Done cleanly, the favorites **lose** — a textbook
+reproduction of the paper's "~60% of lucky winners become losers out-of-sample,"
+now on our own live data. *Lesson reinforced: high win rate is the most
+misleading signal on the platform; favorite-riders are uncopyable.* The
+underdog/`value` archetype (beats longshot prices) is the only one left worth
+testing.
+
## Repo layout
- `insider.py` — the detector: z-score/p-value, timing/freshness/sizing signals,
@@ -124,5 +150,9 @@ real forward (out-of-sample) data on these wallets — observe before you size u
- `copyback.py` / `oos.py` — in-sample and out-of-sample copy-trade backtests.
- `webhook_receiver.py` — push-based live trade watcher (Alchemy → Discord).
- `smart_money.py` — data foundation + dashboard (true-win-rate scanner).
+- `live/` — current scanner: cache-backed skilled-3% finder + watchlist + daily
+ refresh + dashboard. See `live/README.md`.
+- `wide/` — bulk subgraph→DuckDB scanner (survivorship-bias-free, all wallets);
+ public subgraph frozen at Jan 2026, so historical-only. See `wide/README.md`.
- `archive/` — the strategies that didn't work, kept for reference. See
`archive/README.md`.
diff --git a/README.md b/README.md
index 99b0ff19..4541a8a8 100644
--- a/README.md
+++ b/README.md
@@ -68,6 +68,8 @@ Two refinements separate signal from noise:
| `webhook_receiver.py` | Push-based live watcher: Alchemy on-chain webhook → enrich → Discord. |
| `watch.json` | The tracked wallet set + edge weights (shared by the watcher and the tracker). |
| paper tracker | Client-side `$1,000` running portfolio → [jaxperro.com/trading](https://jaxperro.com/trading) (page lives in the personal site repo). |
+| **`live/`** | **The current scanner** — find & track the skilled ~3% from the live API at scale: enumerate → cache → 5-gate skill funnel → dashboard → daily refresh. Caches ~26k wallets / 12.5M bets locally so every re-score is seconds. ([live/README](live/README.md)) |
+| `wide/` | Bulk subgraph→DuckDB scanner: survivorship-bias-free over all 1.76M wallets, but the public subgraph is **frozen at Jan 2026**, so it's a historical tool only. ([wide/README](wide/README.md)) |
| `archive/` | The six strategies that didn't work, kept for reference ([details](archive/README.md)). |
---
@@ -159,6 +161,38 @@ public API (CORS-open) — zero backend, zero added cost.
---
+## The skilled-wallet scanner (`live/`)
+
+The newest pipeline operationalizes the [LBS/Yale finding](https://papers.ssrn.com/sol3/papers.cfm?abstract_id=5910522)
+that ~3% of accounts are genuinely skilled. It scans the **live** data-api at
+scale and tracks the survivors forward.
+
+**The 5-gate funnel** — a wallet is "skilled" only if it clears all five:
+`n ≥ 15 resolved bets` → `z > 0` (beats its entry prices) → `Benjamini–Hochberg
+FDR @5%` → `split-half out-of-sample persists` → `not a market-maker/bot`. Win
+rate is never a gate.
+
+**The cache makes it cheap.** Each wallet's full resolved-bet history is pulled
+once into `cache.duckdb` (~26k wallets / 12.5M bets), keyed with per-bet
+resolution times — so any cutoff (pre-June-1, full-window, archetypes) re-scores
+in **seconds** instead of hours of API pulls.
+
+**The clean out-of-sample result (June 2026).** Copying the "favorite-rider"
+skilled wallets, $1000, no execution lag:
+
+| Selection | Win rate | Forward P&L (June 1+) |
+|-----------|----------|-----------------------|
+| In-sample (peeks at test window) | 99% | **+23.6%** |
+| **Clean (pre-June-1 data only)** | 68% | **−7.4%** (−19% on settled) |
+
+The +23.6% was pure selection bias. Selected honestly, the favorites **lose** —
+exactly the paper's "~60% of lucky winners become losers out-of-sample." High
+win rate ≠ edge, again. (The `value`/longshot archetype — wallets that beat
+*underdog* prices — is the one worth testing next.) Full pipeline in
+[`live/README.md`](live/README.md).
+
+---
+
## The honest verdict
- **Detection works.** z-score + timing + funding-cluster reliably surfaces
diff --git a/live/README.md b/live/README.md
new file mode 100644
index 00000000..1ae9350e
--- /dev/null
+++ b/live/README.md
@@ -0,0 +1,83 @@
+# live/ — find & track the genuinely-skilled ~3%
+
+Finds the small fraction of Polymarket wallets with a *real, repeatable* edge —
+the ~3% the [LBS/Yale study](https://papers.ssrn.com/sol3/papers.cfm?abstract_id=5910522)
+identifies — from the **live data-api**, caches everything locally, and tracks
+them forward. This is the going-forward system; the frozen-subgraph bulk approach
+lives in `../wide/`.
+
+## Why this and not win rate
+
+Win rate is survivorship-biased and decoupled from edge (see `../FINDINGS.md`).
+A wallet is "skilled" only if it **beats the prices it paid** and that edge
+**persists out-of-sample**. We reproduced the research's own finding on live
+June data: favorite-rider wallets that looked +23.6% in-sample lost **−7.4%**
+once selected without look-ahead (see "The clean test" below).
+
+## The 5-gate funnel (`skill.py`)
+
+A wallet counts as skilled only if it clears all five:
+
+1. **n ≥ 15** resolved bets (assessability; the paper's skilled avg ~79).
+2. **z = (wins − Σp)/√Σp(1−p)** clearly > 0 — wins above what entry odds implied.
+ This is the closed form of the paper's "randomize direction 10k×" benchmark.
+3. **Benjamini–Hochberg FDR @ 5%** — at scale, thousands clear z>3 by chance.
+4. **Split-half out-of-sample** — skill in the earlier half persists in the
+ recent half (`z_oos > 0`). The gate that separates the real 3% from the lucky.
+5. **MM/bot cap** (`n ≤ 2500`) — a thousands-of-bets grinder isn't info-edge.
+
+Win rate is never a gate — only displayed. Wallets are tagged `value` (beats
+underdog/longshot prices — the copyable alpha), `balanced`, or `favorite`
+(high win% riding near-certain favorites — real but thin/uncopyable).
+
+## Pipeline
+
+| step | script | what |
+|------|--------|------|
+| enumerate | `enumerate.py [days]` | recent liquid markets (Gamma `end_date_min`) → top traders → candidate pool (`candidates.json`), accumulates across runs |
+| cache | `cache.py` / `collect.py` | pull each wallet's resolved bets **once** into `cache.duckdb` (24s → 0.003s on re-read). Stores `res_t` per bet, so any date cutoff reads the same cache |
+| score | `skill.py [N]` | the 5-gate funnel over cached candidates → `watch_skilled.json` (webhook-compatible) |
+| dashboard | `dashboard.py` | self-contained `dashboard.html` — sortable, archetype-tagged, live recent-trade lookup |
+| backtest | `backtest_june.py [arch]` | copy an archetype's June-1+ entries, $1000, no lag → P&L |
+| clean test | `clean_test.sh` | **the honest test**: re-select on pre-June-1 data only, then backtest June-1+ forward |
+
+## The cache is the point
+
+`cache.duckdb` holds ~26k wallets / 12.5M+ bets, pulled once. Every score —
+any archetype, any cutoff, the clean OOS test — now runs in **seconds** instead
+of hours of API pulls. `MAX_AGE_DAYS=14`: the broad pool refreshes biweekly; the
+watchlist is force-refreshed daily (`cache.invalidate`) for forward tracking.
+
+## The clean test (why the favorites are a mirage)
+
+`clean_test.sh` selects favorites using **only bets resolved before June 1**,
+then copies their June-1+ entries:
+
+- **In-sample (contaminated):** 21 favorites, 99% win rate, **+23.6%**.
+- **Clean (pre-June-1 selection):** 15 favorites, 68% win rate, **−7.4%**
+ (−19% on the settled portion).
+
+The +23.6% was selection bias. This matches the paper: ~60% of "lucky winners"
+turn into losers out-of-sample. **Don't copy favorite-riders.** The `value`
+archetype (beats underdog prices) is where real alpha may live — test it with
+`backtest_june.py value`.
+
+## Daily (`daily.sh`)
+
+1. discover (enumerate last 14d) → 2. freshen cache (force-refresh watchlist +
+top up new wallets) → 3. re-score (instant from cache) → 4. regenerate dashboard
++ snapshot to `history/`. Schedule via launchd/cron (Mac must be awake).
+
+## Usage
+
+```bash
+pip install duckdb
+python3 enumerate.py 180 # build candidate pool (last 6 months)
+python3 collect.py # cache all candidates (one-time, slow; resumable)
+python3 skill.py # -> watch_skilled.json (seconds, from cache)
+python3 dashboard.py # -> dashboard.html
+./clean_test.sh # the out-of-sample verdict
+```
+
+Local data (`*.duckdb`, `candidates.json`, `*_scored.json`, `history/`) is
+gitignored — regenerate via the steps above.
diff --git a/live/backtest_june.py b/live/backtest_june.py
new file mode 100644
index 00000000..4b5ee404
--- /dev/null
+++ b/live/backtest_june.py
@@ -0,0 +1,150 @@
+#!/usr/bin/env python3
+"""Forward copy-test: copy the FAVORITE-rider skilled wallets' new entries from
+June 1 to now, $1000 bankroll, NO execution lag (we get their exact fill price).
+
+Method: collect every BUY these wallets made on/after June 1 (data-api), take the
+first entry per market (basket consensus, one position per market), deploy $1000
+split equally across them, then settle each via the CLOB winner flag (resolved)
+or mark to current price (still open). Reports realized + unrealized P&L.
+
+ python3 backtest_june.py # favorites, from 2026-06-01
+ python3 backtest_june.py value # test the value/longshot archetype instead
+"""
+
+import json
+import os
+import ssl
+import sys
+import time
+import urllib.request
+from concurrent.futures import ThreadPoolExecutor
+
+HERE = os.path.dirname(__file__)
+DATA = "https://data-api.polymarket.com"
+CLOB = "https://clob.polymarket.com/markets"
+CTX = ssl._create_unverified_context()
+START = time.mktime(time.strptime("2026-06-01", "%Y-%m-%d"))
+BANKROLL = 1000.0
+ARCH = sys.argv[1] if len(sys.argv) > 1 else "favorite"
+
+
+def get(url):
+ req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
+ return json.loads(urllib.request.urlopen(req, timeout=30, context=CTX).read())
+
+
+def trades_since(wallet):
+ """All BUY trades on/after START for one wallet."""
+ out, off = [], 0
+ for _ in range(8):
+ try:
+ page = get(f"{DATA}/activity?user={wallet}&type=TRADE&limit=500&offset={off}")
+ except Exception:
+ break
+ if not page:
+ break
+ for t in page:
+ if (t.get("timestamp") or 0) < START:
+ return out
+ if t.get("side") == "BUY" and t.get("conditionId"):
+ out.append(t)
+ off += 500
+ if len(page) < 500:
+ break
+ return out
+
+
+_mkt = {}
+def market(cond):
+ if cond not in _mkt:
+ try:
+ _mkt[cond] = get(f"{CLOB}/{cond}")
+ except Exception:
+ _mkt[cond] = None
+ return _mkt[cond]
+
+
+def settle(cond, outcome_idx, outcome_name):
+ """-> (status, value_per_share). status in won/lost/open/unknown."""
+ m = market(cond)
+ if not m:
+ return "unknown", None
+ toks = m.get("tokens") or []
+ tok = None
+ if outcome_idx is not None and outcome_idx < len(toks):
+ tok = toks[outcome_idx]
+ if tok is None:
+ for t in toks:
+ if (t.get("outcome") or "").lower() == (outcome_name or "").lower():
+ tok = t; break
+ if tok is None:
+ return "unknown", None
+ if tok.get("winner") is True:
+ return "won", 1.0
+ if tok.get("winner") is False:
+ return "lost", 0.0
+ return "open", float(tok.get("price") or 0) # not resolved -> mark to price
+
+
+def main():
+ wl = json.load(open(os.path.join(HERE, os.environ.get("BT_WATCH", "watch_skilled.json"))))
+ wallets = [w for w in wl if (w["avg_entry"] >= 0.85 if ARCH == "favorite"
+ else w["avg_entry"] < 0.5 if ARCH == "value"
+ else True)]
+ print(f"{ARCH}: {len(wallets)} wallets · copying BUYs from "
+ f"{time.strftime('%Y-%m-%d', time.localtime(START))} to now, ${BANKROLL:.0f}, no lag\n",
+ flush=True)
+
+ # gather every favorite's June+ buys, keep the FIRST entry per market
+ picks = {} # cond -> trade (earliest)
+ with ThreadPoolExecutor(max_workers=10) as ex:
+ for ts in ex.map(trades_since, [w["wallet"] for w in wallets]):
+ for t in ts:
+ c = t["conditionId"]
+ if c not in picks or t["timestamp"] < picks[c]["timestamp"]:
+ picks[c] = t
+ n = len(picks)
+ if not n:
+ print("no copied entries in the window."); return
+ stake = BANKROLL / n
+ print(f"{n} unique markets entered → ${stake:.2f} per position\n", flush=True)
+
+ won = lost = openc = unk = 0
+ realized = unreal_val = realized_cost = open_cost = 0.0
+ rows = []
+ with ThreadPoolExecutor(max_workers=10) as ex:
+ results = list(ex.map(
+ lambda kv: (kv[1], settle(kv[0], kv[1].get("outcomeIndex"), kv[1].get("outcome"))),
+ picks.items()))
+ for t, (status, vps) in results:
+ p = t.get("price") or 0.01
+ shares = stake / max(p, 0.001)
+ title = (t.get("title") or "")[:46]
+ if status == "won":
+ won += 1; realized += shares * 1.0; realized_cost += stake
+ rows.append((shares - stake, status, p, title))
+ elif status == "lost":
+ lost += 1; realized += 0.0; realized_cost += stake
+ rows.append((-stake, status, p, title))
+ elif status == "open":
+ openc += 1; unreal_val += shares * vps; open_cost += stake
+ rows.append((shares * vps - stake, status, p, title))
+ else:
+ unk += 1; unreal_val += stake; open_cost += stake # unknown -> hold at cost
+
+ realized_pl = realized - realized_cost
+ equity = realized + unreal_val + 0.0 # all $1000 deployed
+ total_pl = equity - BANKROLL
+ print(f"resolved: {won}W / {lost}L · still open: {openc} · unknown: {unk}")
+ print(f"REALIZED P&L: {realized_pl:+,.2f} (on ${realized_cost:,.0f} settled)")
+ print(f"open positions marked to market: ${unreal_val:,.2f} (cost ${open_cost:,.0f})")
+ print(f"\nFINAL EQUITY: ${equity:,.2f} TOTAL P&L: {total_pl:+,.2f} "
+ f"({100*total_pl/BANKROLL:+.1f}% on ${BANKROLL:.0f})\n")
+ rows.sort()
+ print("worst / best copied bets:")
+ for pl, st, p, title in rows[:4] + rows[-4:]:
+ print(f" {pl:+8.2f} {st:>5} @{p:.2f} {title}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/live/cache.py b/live/cache.py
new file mode 100644
index 00000000..b9d07c45
--- /dev/null
+++ b/live/cache.py
@@ -0,0 +1,81 @@
+#!/usr/bin/env python3
+"""Local cache of per-wallet resolved bets, so we stop re-pulling the data-api.
+
+Each wallet's resolved bets (won, entry price p, conditionId, resolution time,
+size) are stored once in cache.duckdb. Because we keep res_t per bet, ANY date
+cutoff — pre-June-1, full window, future experiments — reads the same cached
+rows and filters locally. A pull only happens for wallets not seen, or older
+than MAX_AGE_DAYS.
+
+Thread-safe: API pulls (the slow part) run outside the lock; only the small
+DuckDB reads/writes are serialized, so skill.py's worker pool still parallelizes
+the network.
+"""
+
+import os
+import sys
+import threading
+import time
+
+import duckdb
+
+sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
+import insider # noqa: E402
+
+DB = os.path.join(os.path.dirname(__file__), "cache.duckdb")
+WINDOW_DAYS = 180
+MAX_AGE_DAYS = 14 # broad pool re-pulls only every 2 weeks; watchlist is
+ # force-refreshed daily via invalidate() (see daily.sh)
+
+_lock = threading.Lock()
+_con = duckdb.connect(DB)
+_con.execute("""CREATE TABLE IF NOT EXISTS bets(
+ wallet TEXT, cond TEXT, won BOOLEAN, p DOUBLE, res_t BIGINT, size DOUBLE)""")
+_con.execute("CREATE INDEX IF NOT EXISTS bets_w ON bets(wallet)")
+_con.execute("CREATE TABLE IF NOT EXISTS pulled(wallet TEXT PRIMARY KEY, pulled_at BIGINT)")
+
+
+def get_bets(wallet):
+ """Resolved bets for a wallet — from cache if fresh, else pull and store."""
+ now = time.time()
+ with _lock:
+ r = _con.execute("SELECT pulled_at FROM pulled WHERE wallet=?", [wallet]).fetchone()
+ if r and now - r[0] < MAX_AGE_DAYS * 86400:
+ rows = _con.execute(
+ "SELECT won,p,cond,res_t,size FROM bets WHERE wallet=?", [wallet]).fetchall()
+ return [{"won": w, "p": p, "cond": c, "res_t": rt, "size": s}
+ for w, p, c, rt, s in rows]
+ # cache miss / stale -> pull (slow, outside the lock so workers stay parallel)
+ try:
+ bets = insider.resolved_bets(wallet, now - WINDOW_DAYS * 86400)
+ except Exception:
+ bets = []
+ with _lock:
+ _con.execute("DELETE FROM bets WHERE wallet=?", [wallet])
+ if bets:
+ _con.executemany(
+ "INSERT INTO bets(wallet,cond,won,p,res_t,size) VALUES (?,?,?,?,?,?)",
+ [(wallet, b["cond"], b["won"], b["p"], b.get("res_t"), b.get("size"))
+ for b in bets])
+ _con.execute("INSERT OR REPLACE INTO pulled VALUES (?,?)", [wallet, int(now)])
+ return bets
+
+
+def invalidate(wallets):
+ """Force a re-pull of these wallets on next get_bets (for daily watchlist
+ forward-refresh)."""
+ with _lock:
+ for w in wallets:
+ _con.execute("DELETE FROM pulled WHERE wallet=?", [w])
+
+
+def stats():
+ with _lock:
+ w = _con.execute("SELECT count(*) FROM pulled").fetchone()[0]
+ b = _con.execute("SELECT count(*) FROM bets").fetchone()[0]
+ return w, b
+
+
+if __name__ == "__main__":
+ w, b = stats()
+ print(f"cache: {w:,} wallets, {b:,} bets in {DB}")
diff --git a/live/clean_test.sh b/live/clean_test.sh
new file mode 100755
index 00000000..e109dbc1
--- /dev/null
+++ b/live/clean_test.sh
@@ -0,0 +1,13 @@
+#!/bin/bash
+# Clean out-of-sample test:
+# 1) re-select skilled wallets using ONLY bets resolved before June 1
+# (selection cannot peek at the June 1+ test window)
+# 2) copy the resulting FAVORITE-rider wallets' June 1+ entries, $1000, no lag
+# This removes the selection contamination of the first backtest.
+set -u
+cd "$(dirname "$0")"
+echo "[clean] $(date '+%F %T') re-scoring candidates on PRE-June-1 data only…"
+SKILL_BEFORE=2026-06-01 SKILL_OUT=watch_prejune.json python3 skill.py 2000
+echo "[clean] $(date '+%F %T') backtest: copy pre-June-1 favorites' June1+ entries"
+BT_WATCH=watch_prejune.json python3 backtest_june.py favorite
+echo "[clean] $(date '+%F %T') done"
diff --git a/live/collect.py b/live/collect.py
new file mode 100644
index 00000000..b44a46bb
--- /dev/null
+++ b/live/collect.py
@@ -0,0 +1,45 @@
+#!/usr/bin/env python3
+"""Collect EVERY candidate wallet's resolved bets into the cache, up to present.
+
+One-time (per refresh window) comprehensive pull so the whole candidate pool is
+local. Resumable: cache.get_bets skips wallets pulled within MAX_AGE_DAYS, so
+killing and re-running continues where it left off. Most-active wallets first,
+so a partial cache already covers the wallets most likely to be skilled.
+
+ python3 collect.py
+"""
+
+import json
+import os
+import sys
+import time
+from concurrent.futures import ThreadPoolExecutor, as_completed
+
+import cache
+
+HERE = os.path.dirname(__file__)
+WORKERS = 16
+
+
+def main():
+ cands = json.load(open(os.path.join(HERE, "candidates.json")))
+ cands.sort(key=lambda c: c.get("markets_seen", 0), reverse=True)
+ wallets = [c["wallet"] for c in cands]
+ print(f"collecting {len(wallets):,} wallets up to present · {WORKERS} workers", flush=True)
+ done, t0 = 0, time.time()
+ with ThreadPoolExecutor(max_workers=WORKERS) as ex:
+ futs = [ex.submit(cache.get_bets, w) for w in wallets]
+ for _ in as_completed(futs):
+ done += 1
+ if done % 200 == 0:
+ w, b = cache.stats()
+ rate = done / max(1e-9, time.time() - t0)
+ eta = (len(wallets) - done) / max(1e-9, rate) / 3600
+ print(f" {done:,}/{len(wallets):,} · cache {w:,}w/{b:,}bets · "
+ f"{rate:.1f}/s · ETA {eta:.1f}h", flush=True)
+ w, b = cache.stats()
+ print(f"DONE {time.strftime('%F %T')} — cache: {w:,} wallets, {b:,} bets", flush=True)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/live/daily.sh b/live/daily.sh
new file mode 100755
index 00000000..bb2f1082
--- /dev/null
+++ b/live/daily.sh
@@ -0,0 +1,26 @@
+#!/bin/bash
+# Daily skilled-wallet refresh, cache-backed:
+# 1) discover — enumerate traders of markets resolved in the last ~14 days,
+# merged into the accumulating candidate pool
+# 2) freshen — force a re-pull of the current watchlist (forward tracking),
+# then top up the cache (collect re-pulls only new + >14d-stale
+# wallets, so it's cheap after the initial collection)
+# 3) re-score — the 5-gate funnel, instant from cache -> watch_skilled.json
+# 4) dashboard — regenerate + snapshot for auditable forward history
+#
+# Schedule with launchd/cron (Mac must be awake). Logs to daily.log.
+set -u
+cd "$(dirname "$0")"
+echo "[daily] $(date '+%F %T') 1/4 discover (enumerate last 14d)"
+python3 enumerate.py 14
+echo "[daily] $(date '+%F %T') 2/4 freshen cache (watchlist forced + new wallets)"
+python3 -c "import json,os,cache
+if os.path.exists('watch_skilled.json'):
+ cache.invalidate([w['wallet'] for w in json.load(open('watch_skilled.json'))])" 2>/dev/null || true
+python3 collect.py
+echo "[daily] $(date '+%F %T') 3/4 re-score (cache-backed, instant)"
+python3 skill.py
+echo "[daily] $(date '+%F %T') 4/4 dashboard"
+python3 dashboard.py
+mkdir -p history && cp watch_skilled.json "history/watch_$(date '+%Y%m%d').json" 2>/dev/null
+echo "[daily] $(date '+%F %T') done -> watch_skilled.json + dashboard.html"
diff --git a/live/dashboard.html b/live/dashboard.html
new file mode 100644
index 00000000..80716a42
--- /dev/null
+++ b/live/dashboard.html
@@ -0,0 +1,121 @@
+
+
+Skilled Wallets — Polymarket
+
+
Skilled Wallets · Polymarket
+
107 wallets that beat their own entry prices and held up out-of-sample · generated 2026-06-16 07:37
+
+
Validated wallets
107
+
Median z
4.0
+
Median OOS z
3.0
+
Value / longshot
69
+
Favorite-rider
21
+
+
+
+
+
+
+
+
#
Wallet
+
z
OOS z
+
bets
win%
+
avg entry
alpha 0.2–0.4
+
type
+
+
How to read it.z = how far the wallet beat the prices it paid (skill; >3 is strong). OOS z = whether that skill held on held-out bets — the gate that separates real edge from luck. Alpha 0.2–0.4 = share of bets in the underdog value zone where research finds real edge concentrates. Value wallets win by beating longshot/mid prices (the copyable alpha); Favorite-rider wallets post high win rates buying near-certain favorites (real but thin, hard to copy). Click any row for live recent trades. Not financial advice — split-half OOS, forward-track before sizing.
+
+
\ No newline at end of file
diff --git a/live/dashboard.py b/live/dashboard.py
new file mode 100644
index 00000000..9b6dc2e6
--- /dev/null
+++ b/live/dashboard.py
@@ -0,0 +1,173 @@
+#!/usr/bin/env python3
+"""Generate a self-contained dashboard.html from watch_skilled.json.
+
+Embeds the skilled-wallet snapshot (z, out-of-sample z, record, archetype, …)
+into a sortable/filterable dark dashboard with Polymarket profile links and
+on-demand live recent-trade lookup (data-api, client-side). Re-runnable — wire
+into daily.sh so the dashboard refreshes with the watchlist.
+
+ python3 dashboard.py # -> dashboard.html
+"""
+
+import json
+import os
+import time
+
+HERE = os.path.dirname(__file__)
+
+
+def archetype(r):
+ if r["avg_entry"] < 0.5:
+ return "value" # longshot / underdog value — the true alpha zone
+ if r["avg_entry"] >= 0.85:
+ return "favorite" # rides near-certain favorites (thin, less copyable)
+ return "balanced"
+
+
+def main():
+ w = json.load(open(os.path.join(HERE, "watch_skilled.json")))
+ for i, r in enumerate(w, 1):
+ r["rank"] = i
+ r["arch"] = archetype(r)
+ r["wins"] = int(round(r["win_rate"] * r["n"] / 100))
+ gen = time.strftime("%Y-%m-%d %H:%M")
+ med_z = sorted(x["z"] for x in w)[len(w) // 2]
+ med_oos = sorted(x["z_oos"] for x in w if x["z_oos"] is not None)[len(w) // 2]
+ n_val = sum(1 for x in w if x["arch"] == "value")
+ n_fav = sum(1 for x in w if x["arch"] == "favorite")
+ data = json.dumps(w)
+
+ html = HTML.replace("/*DATA*/", data).replace("{{GEN}}", gen) \
+ .replace("{{COUNT}}", str(len(w))).replace("{{MEDZ}}", f"{med_z:.1f}") \
+ .replace("{{MEDOOS}}", f"{med_oos:.1f}").replace("{{NVAL}}", str(n_val)) \
+ .replace("{{NFAV}}", str(n_fav))
+ out = os.path.join(HERE, "dashboard.html")
+ open(out, "w").write(html)
+ print(f"wrote {out} ({len(w)} wallets)")
+
+
+HTML = r"""
+
+Skilled Wallets — Polymarket
+
+
Skilled Wallets · Polymarket
+
{{COUNT}} wallets that beat their own entry prices and held up out-of-sample · generated {{GEN}}
+
+
Validated wallets
{{COUNT}}
+
Median z
{{MEDZ}}
+
Median OOS z
{{MEDOOS}}
+
Value / longshot
{{NVAL}}
+
Favorite-rider
{{NFAV}}
+
+
+
+
+
+
+
+
#
Wallet
+
z
OOS z
+
bets
win%
+
avg entry
alpha 0.2–0.4
+
type
+
+
How to read it.z = how far the wallet beat the prices it paid (skill; >3 is strong). OOS z = whether that skill held on held-out bets — the gate that separates real edge from luck. Alpha 0.2–0.4 = share of bets in the underdog value zone where research finds real edge concentrates. Value wallets win by beating longshot/mid prices (the copyable alpha); Favorite-rider wallets post high win rates buying near-certain favorites (real but thin, hard to copy). Click any row for live recent trades. Not financial advice — split-half OOS, forward-track before sizing.
+
+"""
+
+
+if __name__ == "__main__":
+ main()
diff --git a/live/enumerate.py b/live/enumerate.py
new file mode 100644
index 00000000..626eb08b
--- /dev/null
+++ b/live/enumerate.py
@@ -0,0 +1,148 @@
+#!/usr/bin/env python3
+"""Enumerate candidate wallets from markets resolved in the last N months.
+
+The bulk subgraph is frozen at Jan 2026, so recent data comes from the LIVE
+data-api. We can't cheaply page every global trade back 6 months, but we don't
+need to: skilled traders concentrate in liquid markets and recur across many of
+them. So we source high-volume recently-resolved markets from Gamma, pull each
+market's top traders by notional (insider.market_traders), dedup, and tally how
+many markets each wallet shows up in (a prioritization signal for scoring).
+
+Output: candidates.json -> scored by skill.py.
+
+ python3 enumerate.py # last 180 days, liquid markets
+ python3 enumerate.py 90 # last 90 days
+"""
+
+import json
+import os
+import ssl
+import sys
+import time
+import urllib.request
+from collections import defaultdict
+from concurrent.futures import ThreadPoolExecutor
+
+sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
+import insider # noqa: E402
+
+GAMMA = "https://gamma-api.polymarket.com/markets"
+CTX = ssl._create_unverified_context()
+WINDOW_DAYS = int(sys.argv[1]) if len(sys.argv) > 1 else 180
+MAX_MARKETS = 1200 # enough high-volume recent markets to surface recurring sharps
+MAX_SCAN = 25000 # stop scanning the volume ranking after this many markets
+TOP_TRADERS = 30 # top traders by notional per market
+MIN_VOLUME = 20000 # $ — focus on liquid markets where skilled traders play
+
+
+def gamma(params):
+ q = "&".join(f"{k}={v}" for k, v in params.items())
+ req = urllib.request.Request(f"{GAMMA}?{q}", headers={"User-Agent": "Mozilla/5.0"})
+ return json.loads(urllib.request.urlopen(req, timeout=40, context=CTX).read())
+
+
+def _closed_ts(m):
+ """Parse Gamma's closedTime ('2026-05-12 06:44:09+00') -> epoch, else 0."""
+ s = m.get("closedTime")
+ if not s:
+ return 0
+ s = s.replace(" ", "T").split("+")[0].split(".")[0]
+ try:
+ return time.mktime(time.strptime(s, "%Y-%m-%dT%H:%M:%S"))
+ except ValueError:
+ return insider._parse_end(m.get("endDateIso")) # fallback to date
+
+
+def _page(offset, end_min):
+ # end_date_min restricts to recently-resolved markets directly, so we don't
+ # scan the whole (mostly-old) volume ranking. Retry once on transient error.
+ for _ in range(2):
+ try:
+ return gamma({"limit": 100, "offset": offset, "closed": "true",
+ "end_date_min": end_min, "order": "volume",
+ "ascending": "false"})
+ except Exception:
+ time.sleep(2)
+ return "ERR"
+
+
+def recent_markets():
+ """Markets that resolved within the window (via end_date_min), kept if liquid
+ (volumeNum >= MIN_VOLUME and closedTime in window). Gamma is slow (~5-11s/
+ page) so we page CONCURRENTLY in waves and stop at end-of-data or MAX_SCAN."""
+ cutoff = time.time() - WINDOW_DAYS * 86400
+ end_min = time.strftime("%Y-%m-%dT00:00:00Z", time.gmtime(cutoff))
+ out, scanned, base, WAVE = [], 0, 0, 4 # low concurrency — gamma throttles bursts
+ with ThreadPoolExecutor(max_workers=WAVE) as ex:
+ while len(out) < MAX_MARKETS and scanned < MAX_SCAN:
+ pages = list(ex.map(lambda o: _page(o, end_min),
+ [base + i * 100 for i in range(WAVE)]))
+ base += WAVE * 100
+ ended = False
+ for page in pages:
+ if page == "ERR" or page is None:
+ continue # skip transient gaps, keep going
+ if len(page) < 100:
+ ended = True # genuine end of data
+ scanned += len(page)
+ for m in page:
+ try:
+ vol = float(m.get("volumeNum") or 0)
+ except (TypeError, ValueError):
+ vol = 0
+ if vol >= MIN_VOLUME and _closed_ts(m) >= cutoff and m.get("conditionId"):
+ out.append((m["conditionId"], m.get("question", "?")[:60]))
+ print(f" scanned {scanned:,}… kept {len(out)}", flush=True)
+ if ended:
+ break
+ return out[:MAX_MARKETS]
+
+
+def main():
+ print(f"sourcing markets resolved in last {WINDOW_DAYS}d (>= ${MIN_VOLUME:,} vol)…",
+ flush=True)
+ mkts = recent_markets()
+ print(f" {len(mkts)} markets", flush=True)
+
+ seen, name = defaultdict(int), {}
+
+ def grab(cm):
+ try:
+ cands, _ = insider.market_traders(cm[0], top=TOP_TRADERS)
+ return cands
+ except Exception:
+ return []
+
+ done = 0
+ with ThreadPoolExecutor(max_workers=12) as ex:
+ for cands in ex.map(grab, mkts):
+ for c in cands:
+ seen[c["wallet"]] += 1
+ name.setdefault(c["wallet"], c["username"])
+ done += 1
+ if done % 500 == 0:
+ print(f" {done}/{len(mkts)} markets · {len(seen):,} wallets", flush=True)
+
+ # merge with any existing candidates so daily re-runs ACCUMULATE the pool
+ path = os.path.join(os.path.dirname(__file__), "candidates.json")
+ merged = {}
+ if os.path.exists(path):
+ for c in json.load(open(path)):
+ merged[c["wallet"]] = c
+ new = 0
+ for w, n in seen.items():
+ if w in merged:
+ merged[w]["markets_seen"] = max(merged[w].get("markets_seen", 0), n)
+ merged[w].setdefault("username", name[w])
+ else:
+ merged[w] = {"wallet": w, "username": name[w], "markets_seen": n}
+ new += 1
+ rows = sorted(merged.values(), key=lambda c: c["markets_seen"], reverse=True)
+ json.dump(rows, open(path, "w"))
+ print(f"{len(rows):,} candidate wallets (+{new} new this run) -> candidates.json "
+ f"({sum(1 for r in rows if r['markets_seen'] >= 15):,} seen in >=15 markets)",
+ flush=True)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/live/skill.py b/live/skill.py
new file mode 100644
index 00000000..ab9814f6
--- /dev/null
+++ b/live/skill.py
@@ -0,0 +1,159 @@
+#!/usr/bin/env python3
+"""Score candidate wallets for genuine skill — the ~3% the research identifies.
+
+The 5-gate funnel (a wallet must pass all to count as skilled):
+ 1. >= MIN_N resolved bets in the window (assessability)
+ 2. z = (wins - Σp)/√Σp(1-p) clearly > 0 (beats its entry prices)
+ 3. survives Benjamini-Hochberg FDR across the scan (not luck-of-the-draw)
+ 4. split-half: skill in the earlier half persists in (out-of-sample — the
+ the recent half (z_oos > 0) decisive gate)
+ 5. <= MAX_N bets (market-maker / bot proxy, since the data-api gives no
+ reliable trade count) and not pure favorite-riding
+
+This mirrors the LBS/Yale method (randomize direction 10k× ≈ the z benchmark;
+split-events persistence) and uses an UNBIASED win rate (insider.resolved_bets
+unions /positions + /closed-positions, so unredeemed losers are counted).
+
+Refinements used for ranking: odds-band 0.2-0.4 concentration (where alpha
+concentrates per Hubble), entry timing / freshness available via insider.
+
+ python3 skill.py # score top candidates, write watch_skilled.json
+ python3 skill.py 2500 # score the top 2500 candidates by activity
+"""
+
+import json
+import math
+import os
+import sys
+import time
+from concurrent.futures import ThreadPoolExecutor, as_completed
+
+sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
+import insider # noqa: E402
+import cache # noqa: E402 — local per-wallet bet cache (avoids re-pulling)
+
+HERE = os.path.dirname(__file__)
+WINDOW_DAYS = 180
+MIN_N = 15 # floor to say anything (paper's skilled avg ~79)
+OOS_MIN_N = 24 # need >=12 bets/half for a meaningful split
+MAX_N = 2500 # market-maker/bot proxy (no trade count available)
+FDR_Q = 0.05
+SCORE_TOP = int(sys.argv[1]) if len(sys.argv) > 1 else 2000 # cap scoring cost
+WORKERS = 10
+# clean out-of-sample re-selection: SKILL_BEFORE=YYYY-MM-DD scores ONLY bets
+# resolved before that date, so selection can't peek at the test window.
+BEFORE = (time.mktime(time.strptime(os.environ["SKILL_BEFORE"], "%Y-%m-%d"))
+ if os.environ.get("SKILL_BEFORE") else 0)
+OUT = os.environ.get("SKILL_OUT", "watch_skilled.json")
+
+
+def zstats(bets):
+ n = len(bets)
+ if not n:
+ return 0, 0, 0.0, 0.0
+ wins = sum(1 for b in bets if b["won"])
+ exp = sum(b["p"] for b in bets)
+ var = sum(b["p"] * (1 - b["p"]) for b in bets) or 1e-9
+ return n, wins, exp, (wins - exp) / math.sqrt(var)
+
+
+def score_wallet(c):
+ bets = cache.get_bets(c["wallet"]) # cached — pulls the data-api only once per wallet
+ if BEFORE: # clean OOS: only bets resolved before cutoff
+ bets = [b for b in bets if (b.get("res_t") or 0) < BEFORE]
+ n = len(bets)
+ if n < MIN_N or n > MAX_N:
+ return None
+ n, wins, exp, z = zstats(bets)
+ # split-half out-of-sample (chronological): does early skill persist forward?
+ bets.sort(key=lambda b: b.get("res_t") or 0)
+ if n >= OOS_MIN_N:
+ h = n // 2
+ _, _, _, z_is = zstats(bets[:h])
+ _, _, _, z_oos = zstats(bets[h:])
+ else:
+ z_is = z_oos = None
+ band = sum(1 for b in bets if 0.2 <= b["p"] <= 0.4) / n
+ avg_p = sum(b["p"] for b in bets) / n
+ return {
+ "wallet": c["wallet"], "username": c.get("username") or c["wallet"][:10],
+ "n": n, "wins": wins, "win_rate": round(100 * wins / n, 1),
+ "exp_wins": round(exp, 1), "z": round(z, 2), "pval": insider.norm_sf(z),
+ "z_is": None if z_is is None else round(z_is, 2),
+ "z_oos": None if z_oos is None else round(z_oos, 2),
+ "avg_entry": round(avg_p, 2), "band_0204": round(band, 2),
+ "markets_seen": c.get("markets_seen", 0),
+ }
+
+
+def bh_threshold(rows, q):
+ m = len(rows)
+ if not m:
+ return 0.0
+ ps = sorted(r["pval"] for r in rows)
+ k = 0
+ for i, p in enumerate(ps, 1):
+ if p <= q * i / m:
+ k = i
+ return ps[k - 1] if k else 0.0
+
+
+def main():
+ cands = json.load(open(os.path.join(HERE, "candidates.json")))
+ cands.sort(key=lambda c: c.get("markets_seen", 0), reverse=True)
+ cands = cands[:SCORE_TOP]
+ print(f"scoring {len(cands):,} candidates (window {WINDOW_DAYS}d, "
+ f"min_n {MIN_N}, max_n {MAX_N})…", flush=True)
+
+ rows, done = [], 0
+ with ThreadPoolExecutor(max_workers=WORKERS) as ex:
+ futs = {ex.submit(score_wallet, c): c for c in cands}
+ for f in as_completed(futs):
+ r = f.result()
+ if r:
+ rows.append(r)
+ done += 1
+ if done % 200 == 0:
+ print(f" {done}/{len(cands)} scored · {len(rows)} with >= {MIN_N} bets",
+ flush=True)
+
+ if not rows:
+ print("no wallets cleared the bet minimum.")
+ return
+ thresh = bh_threshold(rows, FDR_Q)
+
+ # the skilled set: FDR-significant AND out-of-sample-positive
+ skilled = [r for r in rows
+ if thresh > 0 and r["pval"] <= thresh
+ and (r["z_oos"] is None or r["z_oos"] > 0)]
+ # tier: "validated" = enough bets for a real OOS split and it held
+ for r in skilled:
+ r["tier"] = ("validated" if (r["z_oos"] is not None and r["z_oos"] > 0)
+ else "candidate")
+ skilled.sort(key=lambda r: (r["tier"] == "validated", r["z"]), reverse=True)
+
+ # z-weighted watchlist, compatible with webhook_receiver (wallet/name/weight)
+ tot = sum(r["z"] for r in skilled) or 1
+ watch = [{"wallet": r["wallet"], "name": r["username"],
+ "weight": round(r["z"] / tot, 4), "tier": r["tier"],
+ "z": r["z"], "z_oos": r["z_oos"], "n": r["n"],
+ "win_rate": r["win_rate"], "avg_entry": r["avg_entry"],
+ "band_0204": r["band_0204"]} for r in skilled]
+ json.dump(watch, open(os.path.join(HERE, OUT), "w"), indent=2)
+ json.dump(rows, open(os.path.join(HERE, OUT.replace(".json", "_scored.json")), "w"))
+
+ val = sum(1 for r in skilled if r["tier"] == "validated")
+ print(f"\nscored {len(rows):,} wallets · BH@{int(FDR_Q*100)}% threshold p<= {thresh:.1e}")
+ print(f"SKILLED: {len(skilled)} ({val} validated OOS, {len(skilled)-val} candidate) "
+ f"-> watch_skilled.json\n")
+ hdr = f"{'tier':>10}{'z':>6}{'z_oos':>6}{'rec':>12}{'win%':>6}{'avgP':>6}{'0.2-0.4':>8} wallet"
+ print(hdr); print("-" * len(hdr))
+ for r in skilled[:40]:
+ oos = "n/a" if r["z_oos"] is None else f"{r['z_oos']:.1f}"
+ rec = f"{r['wins']}/{r['n']}"
+ print(f"{r['tier']:>10}{r['z']:>6.1f}{oos:>6}{rec:>12}"
+ f"{r['win_rate']:>5.0f}%{r['avg_entry']:>6.2f}{r['band_0204']:>8.2f} {r['username'][:18]}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/live/watch_skilled.json b/live/watch_skilled.json
new file mode 100644
index 00000000..3aee3e25
--- /dev/null
+++ b/live/watch_skilled.json
@@ -0,0 +1,1286 @@
+[
+ {
+ "wallet": "0x5f390e4b7d6f06d6756a6c92afdbf7b3176aa78c",
+ "name": "oVyg7f",
+ "weight": 0.0368,
+ "tier": "validated",
+ "z": 18.49,
+ "z_oos": 8.38,
+ "n": 2000,
+ "win_rate": 66.0,
+ "avg_entry": 0.57,
+ "band_0204": 0.02
+ },
+ {
+ "wallet": "0xf419556a9758214628edff29dce8f39a8aa13ce7",
+ "name": "2j289uUmAio7ap245975",
+ "weight": 0.0355,
+ "tier": "validated",
+ "z": 17.85,
+ "z_oos": 9.4,
+ "n": 2091,
+ "win_rate": 69.7,
+ "avg_entry": 0.51,
+ "band_0204": 0.13
+ },
+ {
+ "wallet": "0x2257901897e83d39e778fa86d568dc1cd9a2dca2",
+ "name": "iuii899889",
+ "weight": 0.0198,
+ "tier": "validated",
+ "z": 9.97,
+ "z_oos": 6.51,
+ "n": 1539,
+ "win_rate": 50.0,
+ "avg_entry": 0.38,
+ "band_0204": 0.25
+ },
+ {
+ "wallet": "0x9ace6559d0c1fe92c360503c6dbcd962e15f0f16",
+ "name": "Jach9394",
+ "weight": 0.0192,
+ "tier": "validated",
+ "z": 9.64,
+ "z_oos": 7.7,
+ "n": 2128,
+ "win_rate": 47.8,
+ "avg_entry": 0.38,
+ "band_0204": 0.46
+ },
+ {
+ "wallet": "0xda24f0caf5766fc36df8ac2f6ae3a567b71a9f1f",
+ "name": "Thewinwin",
+ "weight": 0.0183,
+ "tier": "validated",
+ "z": 9.18,
+ "z_oos": 6.84,
+ "n": 2001,
+ "win_rate": 41.0,
+ "avg_entry": 0.32,
+ "band_0204": 0.31
+ },
+ {
+ "wallet": "0x14d2bf01e3e7bac9b1799265db3c7d8a993dfa3e",
+ "name": "8998yueyye",
+ "weight": 0.0181,
+ "tier": "validated",
+ "z": 9.09,
+ "z_oos": 5.89,
+ "n": 1532,
+ "win_rate": 49.9,
+ "avg_entry": 0.39,
+ "band_0204": 0.25
+ },
+ {
+ "wallet": "0x97892305f3b5569c32d16f4ed70f840dd7e61ae7",
+ "name": "0x97892305f3b5569c32d16f4ed70F840dd7e61ae7-1770916318891",
+ "weight": 0.0177,
+ "tier": "validated",
+ "z": 8.9,
+ "z_oos": 6.92,
+ "n": 1806,
+ "win_rate": 44.5,
+ "avg_entry": 0.35,
+ "band_0204": 0.51
+ },
+ {
+ "wallet": "0xf820ceebb69255955e9d04367259c84e8e7f0485",
+ "name": "2iq",
+ "weight": 0.0173,
+ "tier": "validated",
+ "z": 8.68,
+ "z_oos": 13.09,
+ "n": 2000,
+ "win_rate": 16.8,
+ "avg_entry": 0.11,
+ "band_0204": 0.1
+ },
+ {
+ "wallet": "0x49818089c6e2755e0e300a48a8dc4747cf9cd2b4",
+ "name": "0x49818089\u2026",
+ "weight": 0.0165,
+ "tier": "validated",
+ "z": 8.3,
+ "z_oos": 4.81,
+ "n": 1334,
+ "win_rate": 61.6,
+ "avg_entry": 0.51,
+ "band_0204": 0.26
+ },
+ {
+ "wallet": "0x1b44945f8992fcea2b43034557e47d11827fd8de",
+ "name": "BrickThrower",
+ "weight": 0.0152,
+ "tier": "validated",
+ "z": 7.65,
+ "z_oos": 5.57,
+ "n": 2063,
+ "win_rate": 45.1,
+ "avg_entry": 0.38,
+ "band_0204": 0.23
+ },
+ {
+ "wallet": "0x2f633efb75256a2f2445110c8978684ab8936643",
+ "name": "AI.AGENT.2026",
+ "weight": 0.0144,
+ "tier": "validated",
+ "z": 7.24,
+ "z_oos": 4.26,
+ "n": 573,
+ "win_rate": 92.0,
+ "avg_entry": 0.82,
+ "band_0204": 0.05
+ },
+ {
+ "wallet": "0x753e554feb327f9d4f7bff8a6ebdced98fbd2046",
+ "name": "hfdffdhr",
+ "weight": 0.0135,
+ "tier": "validated",
+ "z": 6.78,
+ "z_oos": 4.21,
+ "n": 1396,
+ "win_rate": 49.2,
+ "avg_entry": 0.41,
+ "band_0204": 0.2
+ },
+ {
+ "wallet": "0xc684828f6b03487759ced2ebdd975f91f3532228",
+ "name": "oliman2",
+ "weight": 0.0126,
+ "tier": "validated",
+ "z": 6.31,
+ "z_oos": 3.63,
+ "n": 1933,
+ "win_rate": 52.0,
+ "avg_entry": 0.47,
+ "band_0204": 0.15
+ },
+ {
+ "wallet": "0xd82273662ba061a46f62a479de7303266f98000f",
+ "name": "1234zxcv07",
+ "weight": 0.0126,
+ "tier": "validated",
+ "z": 6.31,
+ "z_oos": 3.09,
+ "n": 1054,
+ "win_rate": 71.2,
+ "avg_entry": 0.64,
+ "band_0204": 0.12
+ },
+ {
+ "wallet": "0x751a2b86cab503496efd325c8344e10159349ea1",
+ "name": "Sharky6999",
+ "weight": 0.0121,
+ "tier": "validated",
+ "z": 6.07,
+ "z_oos": 3.13,
+ "n": 2002,
+ "win_rate": 96.3,
+ "avg_entry": 0.94,
+ "band_0204": 0.01
+ },
+ {
+ "wallet": "0xc850cf70d9da621ff0c219f625c9623f88b78c51",
+ "name": "0xc850cf70\u2026",
+ "weight": 0.0119,
+ "tier": "validated",
+ "z": 5.97,
+ "z_oos": 3.98,
+ "n": 2000,
+ "win_rate": 50.1,
+ "avg_entry": 0.44,
+ "band_0204": 0.32
+ },
+ {
+ "wallet": "0xba8c5fbcc5f58b0e4ae0c1413e0413f8c803e77d",
+ "name": "Rock.San",
+ "weight": 0.0116,
+ "tier": "validated",
+ "z": 5.81,
+ "z_oos": 2.63,
+ "n": 2001,
+ "win_rate": 95.8,
+ "avg_entry": 0.93,
+ "band_0204": 0.01
+ },
+ {
+ "wallet": "0x85c0272a4c6334e4cb1212b4e5cda66e68519231",
+ "name": "ferrusqwed",
+ "weight": 0.0111,
+ "tier": "validated",
+ "z": 5.57,
+ "z_oos": 3.46,
+ "n": 2229,
+ "win_rate": 48.7,
+ "avg_entry": 0.43,
+ "band_0204": 0.27
+ },
+ {
+ "wallet": "0xa75f077c6e2510b020967c6230bac2bf342300f0",
+ "name": "0xa75f077c\u2026",
+ "weight": 0.0111,
+ "tier": "validated",
+ "z": 5.56,
+ "z_oos": 4.03,
+ "n": 1772,
+ "win_rate": 49.4,
+ "avg_entry": 0.43,
+ "band_0204": 0.33
+ },
+ {
+ "wallet": "0x18954a8003ebbbdaa4d9190cd2505382a5b902fb",
+ "name": "Si-gambler",
+ "weight": 0.0109,
+ "tier": "validated",
+ "z": 5.48,
+ "z_oos": 4.07,
+ "n": 1651,
+ "win_rate": 40.4,
+ "avg_entry": 0.35,
+ "band_0204": 0.28
+ },
+ {
+ "wallet": "0xc78cdeb598f0bb8bae62a80e8134e842b3c44455",
+ "name": "kjjdhfgsg",
+ "weight": 0.0109,
+ "tier": "validated",
+ "z": 5.48,
+ "z_oos": 0.75,
+ "n": 1182,
+ "win_rate": 49.8,
+ "avg_entry": 0.42,
+ "band_0204": 0.23
+ },
+ {
+ "wallet": "0x30dc7f0cfa4c98590b1c77dd92c82570efa5eb4b",
+ "name": "napahp",
+ "weight": 0.0107,
+ "tier": "validated",
+ "z": 5.4,
+ "z_oos": 2.21,
+ "n": 2243,
+ "win_rate": 48.2,
+ "avg_entry": 0.43,
+ "band_0204": 0.35
+ },
+ {
+ "wallet": "0x8548f0fae7636fc17ca6bb2cbae39cbd4fbce4c1",
+ "name": "madoka123",
+ "weight": 0.0103,
+ "tier": "validated",
+ "z": 5.2,
+ "z_oos": 5.3,
+ "n": 2012,
+ "win_rate": 94.1,
+ "avg_entry": 0.92,
+ "band_0204": 0.01
+ },
+ {
+ "wallet": "0x8d650d146740463611db1f3974ae9b922953ce8f",
+ "name": "PWBR",
+ "weight": 0.0102,
+ "tier": "validated",
+ "z": 5.15,
+ "z_oos": 2.77,
+ "n": 2016,
+ "win_rate": 39.4,
+ "avg_entry": 0.35,
+ "band_0204": 0.11
+ },
+ {
+ "wallet": "0xb595d09ce5bbc4d39e3b3d04e80c402d2c8d5922",
+ "name": "0xB595d09Ce5bBc4d39E3b3D04E80C402d2C8D5922-1769777706105",
+ "weight": 0.0101,
+ "tier": "validated",
+ "z": 5.08,
+ "z_oos": 3.45,
+ "n": 2000,
+ "win_rate": 99.9,
+ "avg_entry": 0.99,
+ "band_0204": 0.0
+ },
+ {
+ "wallet": "0x858c18cd25e722892a60506a3e54c4ecbca84936",
+ "name": "52434624366255247",
+ "weight": 0.0101,
+ "tier": "validated",
+ "z": 5.06,
+ "z_oos": 4.15,
+ "n": 2000,
+ "win_rate": 98.6,
+ "avg_entry": 0.97,
+ "band_0204": 0.0
+ },
+ {
+ "wallet": "0x8a37fe3a49a36de9facd04bd09ee379211f7c99c",
+ "name": "ggmggn",
+ "weight": 0.01,
+ "tier": "validated",
+ "z": 5.01,
+ "z_oos": 4.76,
+ "n": 2000,
+ "win_rate": 89.2,
+ "avg_entry": 0.88,
+ "band_0204": 0.0
+ },
+ {
+ "wallet": "0x7ca56b03105371839e4a3d98f4c1059d10d784bc",
+ "name": "MyBettingFuture",
+ "weight": 0.01,
+ "tier": "validated",
+ "z": 5.0,
+ "z_oos": 3.59,
+ "n": 2019,
+ "win_rate": 98.7,
+ "avg_entry": 0.97,
+ "band_0204": 0.0
+ },
+ {
+ "wallet": "0xab8a8dafae74a018683748d070cb9e349cc2154e",
+ "name": "0xaB8A8dafAE74A018683748d070Cb9E349CC2154e-1771847292775",
+ "weight": 0.0099,
+ "tier": "validated",
+ "z": 4.99,
+ "z_oos": 2.13,
+ "n": 1943,
+ "win_rate": 38.4,
+ "avg_entry": 0.34,
+ "band_0204": 0.18
+ },
+ {
+ "wallet": "0xe52785eab686a52fa47b9396d3343e2908315471",
+ "name": "0xe52785eaB686a52FA47B9396D3343e2908315471-1775532473233",
+ "weight": 0.0099,
+ "tier": "validated",
+ "z": 4.97,
+ "z_oos": 2.98,
+ "n": 273,
+ "win_rate": 36.6,
+ "avg_entry": 0.25,
+ "band_0204": 0.15
+ },
+ {
+ "wallet": "0x472e77443a5a8693f6bd0dad933a61285bd7ae19",
+ "name": "ifailedmath",
+ "weight": 0.0099,
+ "tier": "validated",
+ "z": 4.97,
+ "z_oos": 6.42,
+ "n": 2032,
+ "win_rate": 53.3,
+ "avg_entry": 0.49,
+ "band_0204": 0.14
+ },
+ {
+ "wallet": "0xf3d54ccedcd08c9a203d28e332275e50770fdde9",
+ "name": "0xf3d54cce\u2026",
+ "weight": 0.0096,
+ "tier": "validated",
+ "z": 4.83,
+ "z_oos": 3.96,
+ "n": 1498,
+ "win_rate": 49.3,
+ "avg_entry": 0.43,
+ "band_0204": 0.32
+ },
+ {
+ "wallet": "0xcaee6125904f1ba1ee3f829328d0779f92d20900",
+ "name": "Dedis",
+ "weight": 0.0096,
+ "tier": "validated",
+ "z": 4.82,
+ "z_oos": 4.9,
+ "n": 859,
+ "win_rate": 31.3,
+ "avg_entry": 0.25,
+ "band_0204": 0.24
+ },
+ {
+ "wallet": "0x5f7832a6d8621345922490c74f4e525db20cbfca",
+ "name": "pupe",
+ "weight": 0.0095,
+ "tier": "validated",
+ "z": 4.79,
+ "z_oos": 2.95,
+ "n": 2000,
+ "win_rate": 41.5,
+ "avg_entry": 0.37,
+ "band_0204": 0.18
+ },
+ {
+ "wallet": "0x6c74116b34d77be18754a463df34a06f1b2f8a52",
+ "name": "SUNDAD",
+ "weight": 0.0095,
+ "tier": "validated",
+ "z": 4.78,
+ "z_oos": 1.6,
+ "n": 903,
+ "win_rate": 26.2,
+ "avg_entry": 0.2,
+ "band_0204": 0.38
+ },
+ {
+ "wallet": "0x9819e3e2e1992cb0d6cc1742df4f4d81af314951",
+ "name": "AfganNakrishe4",
+ "weight": 0.0095,
+ "tier": "validated",
+ "z": 4.75,
+ "z_oos": 4.61,
+ "n": 2010,
+ "win_rate": 43.7,
+ "avg_entry": 0.39,
+ "band_0204": 0.29
+ },
+ {
+ "wallet": "0x6ab434242be94f834de8d38ba2de0b4cbfdaa81b",
+ "name": "STAYCALM",
+ "weight": 0.0093,
+ "tier": "validated",
+ "z": 4.66,
+ "z_oos": 4.08,
+ "n": 2007,
+ "win_rate": 94.0,
+ "avg_entry": 0.92,
+ "band_0204": 0.01
+ },
+ {
+ "wallet": "0x558f2f82cf0394c28ce3c8e117ee747aeab51a56",
+ "name": "LesterDiamond",
+ "weight": 0.0093,
+ "tier": "validated",
+ "z": 4.66,
+ "z_oos": 5.48,
+ "n": 2168,
+ "win_rate": 59.5,
+ "avg_entry": 0.57,
+ "band_0204": 0.03
+ },
+ {
+ "wallet": "0xa16a1302ca05463f30faebeb5c045767fde233a1",
+ "name": "KimiTalibantonelli",
+ "weight": 0.0092,
+ "tier": "validated",
+ "z": 4.64,
+ "z_oos": 3.82,
+ "n": 2005,
+ "win_rate": 99.2,
+ "avg_entry": 0.98,
+ "band_0204": 0.0
+ },
+ {
+ "wallet": "0x7a5d94f83ff0195387eaf9f4463433d03eda54db",
+ "name": "mishipolis",
+ "weight": 0.0091,
+ "tier": "validated",
+ "z": 4.59,
+ "z_oos": 4.2,
+ "n": 2140,
+ "win_rate": 54.4,
+ "avg_entry": 0.5,
+ "band_0204": 0.2
+ },
+ {
+ "wallet": "0xdbdd45150249e229eb4ca8aa48a30dca21faa5de",
+ "name": "0xdbdd45150249e229eB4cA8aa48A30Dca21Faa5de-1757094771846",
+ "weight": 0.0091,
+ "tier": "validated",
+ "z": 4.57,
+ "z_oos": 3.4,
+ "n": 2040,
+ "win_rate": 52.2,
+ "avg_entry": 0.48,
+ "band_0204": 0.22
+ },
+ {
+ "wallet": "0x13a12ebadc50f6fa42c8f2595997d8adf329d9bc",
+ "name": "12398498123498189239843",
+ "weight": 0.0086,
+ "tier": "validated",
+ "z": 4.34,
+ "z_oos": 3.12,
+ "n": 2001,
+ "win_rate": 69.2,
+ "avg_entry": 0.65,
+ "band_0204": 0.12
+ },
+ {
+ "wallet": "0xdf6539d1fadb951a02a999d31a72e0cd7fd9c36d",
+ "name": "nndk",
+ "weight": 0.0086,
+ "tier": "validated",
+ "z": 4.33,
+ "z_oos": 2.83,
+ "n": 2073,
+ "win_rate": 48.5,
+ "avg_entry": 0.45,
+ "band_0204": 0.42
+ },
+ {
+ "wallet": "0x54e00a1ceb7f3b7c0bd240d1deeb0edb6b0eb02c",
+ "name": "0x54E00a1Ceb7f3b7c0bD240D1Deeb0edB6B0eb02C-1761391047632",
+ "weight": 0.0086,
+ "tier": "validated",
+ "z": 4.31,
+ "z_oos": 2.57,
+ "n": 2016,
+ "win_rate": 52.5,
+ "avg_entry": 0.49,
+ "band_0204": 0.37
+ },
+ {
+ "wallet": "0x82b59bbdd9ec1aaa995ee41d5d49b7de0a5f6f5f",
+ "name": "ldsam",
+ "weight": 0.0085,
+ "tier": "validated",
+ "z": 4.29,
+ "z_oos": 4.62,
+ "n": 2000,
+ "win_rate": 48.2,
+ "avg_entry": 0.44,
+ "band_0204": 0.29
+ },
+ {
+ "wallet": "0x773e8ca01ad9750283dbfb335d2c6670bc817502",
+ "name": "meow991",
+ "weight": 0.0085,
+ "tier": "validated",
+ "z": 4.27,
+ "z_oos": 7.1,
+ "n": 687,
+ "win_rate": 62.7,
+ "avg_entry": 0.56,
+ "band_0204": 0.17
+ },
+ {
+ "wallet": "0xee65685de42f8de9a03b4c53ee77d56a20d2cfc9",
+ "name": "0x50f7",
+ "weight": 0.0084,
+ "tier": "validated",
+ "z": 4.2,
+ "z_oos": 2.63,
+ "n": 2209,
+ "win_rate": 39.2,
+ "avg_entry": 0.37,
+ "band_0204": 0.01
+ },
+ {
+ "wallet": "0xbb9f224569285cb57f433bbe2e71f9b8a11b557b",
+ "name": "rocky42014",
+ "weight": 0.0084,
+ "tier": "validated",
+ "z": 4.2,
+ "z_oos": 2.87,
+ "n": 2023,
+ "win_rate": 43.2,
+ "avg_entry": 0.39,
+ "band_0204": 0.25
+ },
+ {
+ "wallet": "0xb36b3af57e480c750bcebed7be2d8afd24380cb1",
+ "name": "Didiaodiao",
+ "weight": 0.0083,
+ "tier": "validated",
+ "z": 4.17,
+ "z_oos": 3.13,
+ "n": 2000,
+ "win_rate": 48.1,
+ "avg_entry": 0.44,
+ "band_0204": 0.25
+ },
+ {
+ "wallet": "0xb2b4ec88c02de44d6ab61eb6c4141fe8170e512d",
+ "name": "0xb2b4ec88\u2026",
+ "weight": 0.0083,
+ "tier": "validated",
+ "z": 4.15,
+ "z_oos": 1.97,
+ "n": 2000,
+ "win_rate": 46.5,
+ "avg_entry": 0.42,
+ "band_0204": 0.28
+ },
+ {
+ "wallet": "0x8428101d6979dcc97de6c7474b0535ceb558b41f",
+ "name": "888SSSS",
+ "weight": 0.0082,
+ "tier": "validated",
+ "z": 4.14,
+ "z_oos": 1.8,
+ "n": 1825,
+ "win_rate": 53.8,
+ "avg_entry": 0.49,
+ "band_0204": 0.24
+ },
+ {
+ "wallet": "0x8cd9439c1205da4c9ba18559cdcfe2e12278190e",
+ "name": "kaslas",
+ "weight": 0.0082,
+ "tier": "validated",
+ "z": 4.13,
+ "z_oos": 3.14,
+ "n": 2038,
+ "win_rate": 50.6,
+ "avg_entry": 0.47,
+ "band_0204": 0.31
+ },
+ {
+ "wallet": "0x875e974594985283c999765461bf2e15b4dee6b5",
+ "name": "CorkisGeldbeutel",
+ "weight": 0.0081,
+ "tier": "validated",
+ "z": 4.05,
+ "z_oos": 3.32,
+ "n": 2001,
+ "win_rate": 97.2,
+ "avg_entry": 0.96,
+ "band_0204": 0.01
+ },
+ {
+ "wallet": "0xc095b5100653f0c34337c718f201dae96d762d65",
+ "name": "daddypow",
+ "weight": 0.008,
+ "tier": "validated",
+ "z": 4.0,
+ "z_oos": 2.37,
+ "n": 2000,
+ "win_rate": 98.8,
+ "avg_entry": 0.98,
+ "band_0204": 0.0
+ },
+ {
+ "wallet": "0x0cfe444ab50e01b14ee15ff5e677a1dd68cbd5c3",
+ "name": "0x0CFE444Ab50e01B14EE15ff5e677a1dd68cbD5c3-1778164468132",
+ "weight": 0.008,
+ "tier": "validated",
+ "z": 4.0,
+ "z_oos": 0.51,
+ "n": 902,
+ "win_rate": 44.8,
+ "avg_entry": 0.39,
+ "band_0204": 0.27
+ },
+ {
+ "wallet": "0x616d3f9819deb5450fb34315221f23d64f36bb9b",
+ "name": "JackOfAllTrades3130",
+ "weight": 0.008,
+ "tier": "validated",
+ "z": 4.0,
+ "z_oos": 3.01,
+ "n": 2007,
+ "win_rate": 99.2,
+ "avg_entry": 0.98,
+ "band_0204": 0.0
+ },
+ {
+ "wallet": "0x6a88c5ecd262e2c42e78bad8e6db7ab3c4e4b859",
+ "name": "pelik",
+ "weight": 0.0079,
+ "tier": "validated",
+ "z": 3.99,
+ "z_oos": 3.99,
+ "n": 2004,
+ "win_rate": 100.0,
+ "avg_entry": 1.0,
+ "band_0204": 0.0
+ },
+ {
+ "wallet": "0xb66dc120c79ec7282b48f938633c942567667ec1",
+ "name": "0xb66dc120c79ec7282b48f938633c94256",
+ "weight": 0.0079,
+ "tier": "validated",
+ "z": 3.97,
+ "z_oos": 2.39,
+ "n": 2317,
+ "win_rate": 43.4,
+ "avg_entry": 0.4,
+ "band_0204": 0.26
+ },
+ {
+ "wallet": "0x51928a6454475e6b0d883f5d6ba663670c83df62",
+ "name": "sfafdsaferw",
+ "weight": 0.0078,
+ "tier": "validated",
+ "z": 3.92,
+ "z_oos": 1.13,
+ "n": 2000,
+ "win_rate": 52.1,
+ "avg_entry": 0.49,
+ "band_0204": 0.18
+ },
+ {
+ "wallet": "0x08730443345e11c078c5b2597ce3c0f06af609e8",
+ "name": "Anne666",
+ "weight": 0.0078,
+ "tier": "validated",
+ "z": 3.9,
+ "z_oos": 2.78,
+ "n": 2016,
+ "win_rate": 0.4,
+ "avg_entry": 0.0,
+ "band_0204": 0.0
+ },
+ {
+ "wallet": "0x40a75505ba4e7deb9bf55e839e5a34dd6c9becb7",
+ "name": "Dreamwire",
+ "weight": 0.0078,
+ "tier": "validated",
+ "z": 3.9,
+ "z_oos": 2.9,
+ "n": 2008,
+ "win_rate": 81.3,
+ "avg_entry": 0.8,
+ "band_0204": 0.02
+ },
+ {
+ "wallet": "0x48270d81cfe598ae0379ca535a26dc41e3e9080d",
+ "name": "DiogoBNF566",
+ "weight": 0.0077,
+ "tier": "validated",
+ "z": 3.86,
+ "z_oos": 3.3,
+ "n": 2014,
+ "win_rate": 95.4,
+ "avg_entry": 0.94,
+ "band_0204": 0.01
+ },
+ {
+ "wallet": "0x1c7b8b9e0c9a5215d5b84553130029e5d1ae315d",
+ "name": "ksamlsa",
+ "weight": 0.0077,
+ "tier": "validated",
+ "z": 3.85,
+ "z_oos": 3.54,
+ "n": 2000,
+ "win_rate": 50.8,
+ "avg_entry": 0.47,
+ "band_0204": 0.3
+ },
+ {
+ "wallet": "0x17f0b79bae4c986b854f6be30f1fca8c484dea12",
+ "name": "bdmina",
+ "weight": 0.0076,
+ "tier": "validated",
+ "z": 3.81,
+ "z_oos": 2.86,
+ "n": 2003,
+ "win_rate": 44.2,
+ "avg_entry": 0.4,
+ "band_0204": 0.22
+ },
+ {
+ "wallet": "0x2ecb34e1cb95b3c8d9d3e2905668d3eee03a7645",
+ "name": "AlphaPengu1n",
+ "weight": 0.0075,
+ "tier": "validated",
+ "z": 3.78,
+ "z_oos": 3.57,
+ "n": 2170,
+ "win_rate": 45.6,
+ "avg_entry": 0.42,
+ "band_0204": 0.25
+ },
+ {
+ "wallet": "0xdeca32428d5cf4b1c4d7266a950e20791676ddd5",
+ "name": "durrrrrrrr",
+ "weight": 0.0072,
+ "tier": "validated",
+ "z": 3.64,
+ "z_oos": 2.45,
+ "n": 2048,
+ "win_rate": 31.6,
+ "avg_entry": 0.29,
+ "band_0204": 0.02
+ },
+ {
+ "wallet": "0x5a218c7ad04135830a45c41aaed7294df7809318",
+ "name": "balthazar",
+ "weight": 0.0072,
+ "tier": "validated",
+ "z": 3.62,
+ "z_oos": 3.98,
+ "n": 2384,
+ "win_rate": 49.0,
+ "avg_entry": 0.47,
+ "band_0204": 0.06
+ },
+ {
+ "wallet": "0xd92f13f9c20e5739aae98032e443b13d9d823d83",
+ "name": "follwmee",
+ "weight": 0.0072,
+ "tier": "validated",
+ "z": 3.6,
+ "z_oos": 1.88,
+ "n": 2040,
+ "win_rate": 48.8,
+ "avg_entry": 0.45,
+ "band_0204": 0.22
+ },
+ {
+ "wallet": "0xb4d250f58c26840e09723a83ce9c8149aa32ce99",
+ "name": "alpheclabs",
+ "weight": 0.0071,
+ "tier": "validated",
+ "z": 3.57,
+ "z_oos": 3.4,
+ "n": 2035,
+ "win_rate": 50.5,
+ "avg_entry": 0.47,
+ "band_0204": 0.24
+ },
+ {
+ "wallet": "0x51f9122c7ed2ceb749d8d63617153e98d3ba19a0",
+ "name": "kozelblack",
+ "weight": 0.007,
+ "tier": "validated",
+ "z": 3.52,
+ "z_oos": 3.34,
+ "n": 2018,
+ "win_rate": 98.3,
+ "avg_entry": 0.97,
+ "band_0204": 0.0
+ },
+ {
+ "wallet": "0x9219dd565d7521e95f273b7eea68dbb08d40027c",
+ "name": "aka-baka-tiki-taka",
+ "weight": 0.007,
+ "tier": "validated",
+ "z": 3.51,
+ "z_oos": 2.25,
+ "n": 2000,
+ "win_rate": 98.5,
+ "avg_entry": 0.97,
+ "band_0204": 0.0
+ },
+ {
+ "wallet": "0x1d99336f5c7dabec1b890cb2237f9b4aa139b04c",
+ "name": "traxeast",
+ "weight": 0.007,
+ "tier": "validated",
+ "z": 3.5,
+ "z_oos": 1.4,
+ "n": 2001,
+ "win_rate": 46.8,
+ "avg_entry": 0.43,
+ "band_0204": 0.19
+ },
+ {
+ "wallet": "0xa5427f80816486ad3e4c80aad9711f87b8b5172c",
+ "name": "0xA5427F80816486AD3E4c80AAd9711f87B8B5172C-1772701375450",
+ "weight": 0.0069,
+ "tier": "validated",
+ "z": 3.49,
+ "z_oos": 3.02,
+ "n": 2001,
+ "win_rate": 49.6,
+ "avg_entry": 0.46,
+ "band_0204": 0.37
+ },
+ {
+ "wallet": "0x31121402eafd4a157141db8f84b5da297ff63fcd",
+ "name": "yeagerist",
+ "weight": 0.0068,
+ "tier": "validated",
+ "z": 3.41,
+ "z_oos": 3.07,
+ "n": 2005,
+ "win_rate": 98.5,
+ "avg_entry": 0.98,
+ "band_0204": 0.0
+ },
+ {
+ "wallet": "0x7a3fb8c5ad2af4c621b2138d9579410343976712",
+ "name": "0x7a3FB8C5aD2aF4C621B2138D9579410343976712-1766367891961",
+ "weight": 0.0067,
+ "tier": "validated",
+ "z": 3.38,
+ "z_oos": 2.99,
+ "n": 2011,
+ "win_rate": 47.4,
+ "avg_entry": 0.44,
+ "band_0204": 0.29
+ },
+ {
+ "wallet": "0x6d3c9fe9cb9a7e006c99b13bdd0451ab5059c74a",
+ "name": "0x6D3C9fE9CB9A7e006C99b13bDd0451aB5059C74a-1775205794013",
+ "weight": 0.0066,
+ "tier": "validated",
+ "z": 3.34,
+ "z_oos": 0.16,
+ "n": 1292,
+ "win_rate": 30.0,
+ "avg_entry": 0.26,
+ "band_0204": 0.4
+ },
+ {
+ "wallet": "0x34f4c3118569f32ef02441326305bca0ff5816bd",
+ "name": "Walede-",
+ "weight": 0.0066,
+ "tier": "validated",
+ "z": 3.34,
+ "z_oos": 0.89,
+ "n": 2116,
+ "win_rate": 30.4,
+ "avg_entry": 0.27,
+ "band_0204": 0.47
+ },
+ {
+ "wallet": "0x50c0959535cf830f6b73c5366a02f2cce3aa1600",
+ "name": "LidoRzitoni",
+ "weight": 0.0066,
+ "tier": "validated",
+ "z": 3.34,
+ "z_oos": 1.56,
+ "n": 213,
+ "win_rate": 34.7,
+ "avg_entry": 0.26,
+ "band_0204": 0.31
+ },
+ {
+ "wallet": "0x93173b86dbe2f2dd9922c751806d306b48cf5a5f",
+ "name": "trader-93173b86",
+ "weight": 0.0066,
+ "tier": "validated",
+ "z": 3.33,
+ "z_oos": 1.94,
+ "n": 2007,
+ "win_rate": 49.3,
+ "avg_entry": 0.46,
+ "band_0204": 0.27
+ },
+ {
+ "wallet": "0xe75b11fa4fa01db5f11743e5f40df6c23b8e97eb",
+ "name": "sleepyzs",
+ "weight": 0.0066,
+ "tier": "validated",
+ "z": 3.3,
+ "z_oos": 1.46,
+ "n": 2001,
+ "win_rate": 99.0,
+ "avg_entry": 0.98,
+ "band_0204": 0.0
+ },
+ {
+ "wallet": "0x46b2f8a865a3572a435813e747e22cce0e835977",
+ "name": "quantssss",
+ "weight": 0.0065,
+ "tier": "validated",
+ "z": 3.29,
+ "z_oos": 2.37,
+ "n": 2007,
+ "win_rate": 49.2,
+ "avg_entry": 0.46,
+ "band_0204": 0.39
+ },
+ {
+ "wallet": "0x536be02af900fe046fa708c8059c04f737a2cee3",
+ "name": "fill-or-fok-me",
+ "weight": 0.0065,
+ "tier": "validated",
+ "z": 3.28,
+ "z_oos": 2.24,
+ "n": 2017,
+ "win_rate": 48.3,
+ "avg_entry": 0.45,
+ "band_0204": 0.34
+ },
+ {
+ "wallet": "0x2c697b1d8e5546aef19ed939522af03d90a3a9a2",
+ "name": "traxup",
+ "weight": 0.0065,
+ "tier": "validated",
+ "z": 3.27,
+ "z_oos": 1.39,
+ "n": 1720,
+ "win_rate": 49.7,
+ "avg_entry": 0.46,
+ "band_0204": 0.19
+ },
+ {
+ "wallet": "0x25d8c73a5ebeb6db934d8da68592ba3d4f1ca71f",
+ "name": "none910",
+ "weight": 0.0064,
+ "tier": "validated",
+ "z": 3.24,
+ "z_oos": 2.47,
+ "n": 1463,
+ "win_rate": 47.8,
+ "avg_entry": 0.44,
+ "band_0204": 0.1
+ },
+ {
+ "wallet": "0xca2d072efd5e4f02cd1a3539967ed1ab308539ed",
+ "name": "Originator",
+ "weight": 0.0064,
+ "tier": "validated",
+ "z": 3.2,
+ "z_oos": 2.12,
+ "n": 2000,
+ "win_rate": 51.3,
+ "avg_entry": 0.48,
+ "band_0204": 0.22
+ },
+ {
+ "wallet": "0xcea972195c2d011a5525df117abf0230e05157c6",
+ "name": "VelonLabs",
+ "weight": 0.0063,
+ "tier": "validated",
+ "z": 3.18,
+ "z_oos": 3.95,
+ "n": 1391,
+ "win_rate": 34.4,
+ "avg_entry": 0.31,
+ "band_0204": 0.17
+ },
+ {
+ "wallet": "0xb410255a64fd72acdb7e57b8f65999865a017736",
+ "name": "Lexma",
+ "weight": 0.0062,
+ "tier": "validated",
+ "z": 3.13,
+ "z_oos": 2.86,
+ "n": 2038,
+ "win_rate": 82.4,
+ "avg_entry": 0.81,
+ "band_0204": 0.02
+ },
+ {
+ "wallet": "0xe1d6b51521bd4365769199f392f9818661bd907c",
+ "name": "0xe1D6b51521Bd4365769199f392F9818661BD907",
+ "weight": 0.0062,
+ "tier": "validated",
+ "z": 3.11,
+ "z_oos": 2.8,
+ "n": 2119,
+ "win_rate": 48.0,
+ "avg_entry": 0.45,
+ "band_0204": 0.24
+ },
+ {
+ "wallet": "0xde0463ea7f611b065e8ab06bbfbddad75e6dfa37",
+ "name": "mwenya",
+ "weight": 0.0061,
+ "tier": "validated",
+ "z": 3.08,
+ "z_oos": 2.92,
+ "n": 2051,
+ "win_rate": 50.6,
+ "avg_entry": 0.48,
+ "band_0204": 0.16
+ },
+ {
+ "wallet": "0x323965e26fc0573234d6faa6819b6e886e986e1b",
+ "name": "maxev",
+ "weight": 0.006,
+ "tier": "validated",
+ "z": 3.04,
+ "z_oos": 1.77,
+ "n": 2006,
+ "win_rate": 99.2,
+ "avg_entry": 0.98,
+ "band_0204": 0.0
+ },
+ {
+ "wallet": "0xddb0629096a8a03f490b6572c06f2ee76465f95a",
+ "name": "IVY56",
+ "weight": 0.006,
+ "tier": "validated",
+ "z": 3.01,
+ "z_oos": 0.18,
+ "n": 2012,
+ "win_rate": 48.7,
+ "avg_entry": 0.47,
+ "band_0204": 0.06
+ },
+ {
+ "wallet": "0xfe37315964b9a43b48b4ece3882a08f226619d5c",
+ "name": "kitsune222",
+ "weight": 0.006,
+ "tier": "validated",
+ "z": 3.01,
+ "z_oos": 1.89,
+ "n": 635,
+ "win_rate": 50.6,
+ "avg_entry": 0.45,
+ "band_0204": 0.24
+ },
+ {
+ "wallet": "0xd189664c5308903476f9f079820431e4fd7d06f4",
+ "name": "rwo",
+ "weight": 0.006,
+ "tier": "validated",
+ "z": 2.99,
+ "z_oos": 1.75,
+ "n": 2000,
+ "win_rate": 53.6,
+ "avg_entry": 0.51,
+ "band_0204": 0.07
+ },
+ {
+ "wallet": "0x47138dc1eef25f1ea91f3b2fda0e0f455c634d21",
+ "name": "dsknadkasnkfjs",
+ "weight": 0.006,
+ "tier": "validated",
+ "z": 2.99,
+ "z_oos": 1.61,
+ "n": 2001,
+ "win_rate": 44.1,
+ "avg_entry": 0.41,
+ "band_0204": 0.26
+ },
+ {
+ "wallet": "0xf4f88a1e25ff5dfae68326eb62049d6a1b7c5bf1",
+ "name": "cosmicsteaks",
+ "weight": 0.0059,
+ "tier": "validated",
+ "z": 2.97,
+ "z_oos": 2.2,
+ "n": 2035,
+ "win_rate": 43.8,
+ "avg_entry": 0.41,
+ "band_0204": 0.32
+ },
+ {
+ "wallet": "0x86520db4175cf4e7f7f646cae22365f1f3b61ccb",
+ "name": "traxnorth",
+ "weight": 0.0058,
+ "tier": "validated",
+ "z": 2.92,
+ "z_oos": 1.0,
+ "n": 2001,
+ "win_rate": 42.7,
+ "avg_entry": 0.4,
+ "band_0204": 0.26
+ },
+ {
+ "wallet": "0xb7ab821f037a4c8deb9b23b8001a4be985d116d0",
+ "name": "funfacts",
+ "weight": 0.0058,
+ "tier": "validated",
+ "z": 2.92,
+ "z_oos": 3.07,
+ "n": 2015,
+ "win_rate": 63.7,
+ "avg_entry": 0.61,
+ "band_0204": 0.05
+ },
+ {
+ "wallet": "0x4caee221267f30efdc751bbca7fdb7ffef10c482",
+ "name": "Hinton07",
+ "weight": 0.0057,
+ "tier": "validated",
+ "z": 2.88,
+ "z_oos": 1.71,
+ "n": 2011,
+ "win_rate": 90.0,
+ "avg_entry": 0.88,
+ "band_0204": 0.0
+ },
+ {
+ "wallet": "0x1d949489f736378cbde40db18c093c5cff459100",
+ "name": "BullishBarb",
+ "weight": 0.0057,
+ "tier": "validated",
+ "z": 2.87,
+ "z_oos": 1.31,
+ "n": 2108,
+ "win_rate": 53.4,
+ "avg_entry": 0.51,
+ "band_0204": 0.25
+ },
+ {
+ "wallet": "0x43d7c53c20814021da901e89bc0a582ee7881c75",
+ "name": "STULAH",
+ "weight": 0.0057,
+ "tier": "validated",
+ "z": 2.85,
+ "z_oos": 3.3,
+ "n": 2004,
+ "win_rate": 44.3,
+ "avg_entry": 0.41,
+ "band_0204": 0.34
+ },
+ {
+ "wallet": "0x55154a54cd74db752540dcd663a30cdb5c00eb8b",
+ "name": "AK47ForLIFE",
+ "weight": 0.0057,
+ "tier": "validated",
+ "z": 2.84,
+ "z_oos": 2.64,
+ "n": 1405,
+ "win_rate": 41.5,
+ "avg_entry": 0.38,
+ "band_0204": 0.35
+ },
+ {
+ "wallet": "0x15ac9d4fb9c30fca4fec2fdecde3fdace721f3ba",
+ "name": "toireK",
+ "weight": 0.0056,
+ "tier": "validated",
+ "z": 2.82,
+ "z_oos": 3.4,
+ "n": 2006,
+ "win_rate": 96.7,
+ "avg_entry": 0.96,
+ "band_0204": 0.01
+ },
+ {
+ "wallet": "0x229a29c0e1a54d3177d899af3b36ceb0f67b1bf0",
+ "name": "HEROZ",
+ "weight": 0.0056,
+ "tier": "validated",
+ "z": 2.82,
+ "z_oos": 0.42,
+ "n": 1352,
+ "win_rate": 39.6,
+ "avg_entry": 0.37,
+ "band_0204": 0.18
+ },
+ {
+ "wallet": "0x4d4bdf24c73473e5b68e230bd843a667276a153c",
+ "name": "SisplacBII",
+ "weight": 0.0056,
+ "tier": "validated",
+ "z": 2.79,
+ "z_oos": 1.11,
+ "n": 2100,
+ "win_rate": 71.8,
+ "avg_entry": 0.69,
+ "band_0204": 0.02
+ },
+ {
+ "wallet": "0x689c37af05b28153d718757a5b3c1008c4226b1e",
+ "name": "amnesia12",
+ "weight": 0.0055,
+ "tier": "validated",
+ "z": 2.78,
+ "z_oos": 4.82,
+ "n": 1854,
+ "win_rate": 56.6,
+ "avg_entry": 0.54,
+ "band_0204": 0.14
+ },
+ {
+ "wallet": "0x255ab4ffc544bccbcdc2e5891fa239c5a3813435",
+ "name": "BaronOnThrone",
+ "weight": 0.0055,
+ "tier": "validated",
+ "z": 2.76,
+ "z_oos": 2.27,
+ "n": 2001,
+ "win_rate": 38.7,
+ "avg_entry": 0.36,
+ "band_0204": 0.4
+ },
+ {
+ "wallet": "0x290367f43d250062f470bb531fde2e3ab32e9203",
+ "name": "HarryRitter",
+ "weight": 0.0055,
+ "tier": "validated",
+ "z": 2.74,
+ "z_oos": 3.49,
+ "n": 1611,
+ "win_rate": 64.1,
+ "avg_entry": 0.61,
+ "band_0204": 0.12
+ }
+]
\ No newline at end of file
diff --git a/wide/README.md b/wide/README.md
new file mode 100644
index 00000000..7f13c684
--- /dev/null
+++ b/wide/README.md
@@ -0,0 +1,71 @@
+# wide/ — bulk subgraph edge scanner
+
+Find wallets with a real edge across **all** of Polymarket (~1.76M traders,
+268k conditions) by bulk-ingesting the on-chain subgraph into a local DuckDB
+and ranking with SQL — instead of per-wallet API calls (which cap out at a few
+hundred wallets and rate-limit).
+
+## Why a local DB, not the API
+
+The Goldsky orderbook subgraph **times out on any `orderBy`** over a
+non-indexed field (`numTrades`, `scaledProfit`, …). You cannot ask it for "top
+wallets." The only scalable pattern is: cursor-paginate every row by `id` (the
+indexed key), land it locally, and rank in DuckDB. That constraint *is* the
+architecture.
+
+## Why the win rate here is honest
+
+The data-api hides losers: Polymarket only redeems winning shares, so losing
+positions sit unredeemed and never enter `/closed-positions`. Measuring win
+rate there reads ~90% when the truth is ~50% (see ../FINDINGS.md). The subgraph
+records a `marketPosition` for **every** buy regardless of redemption, so the
+survivorship bias structurally does not exist in this data.
+
+## What "edge" means
+
+A high win rate is not edge — you can hit 90% by only buying 95¢ favorites.
+Edge is **beating the prices you paid**:
+
+```
+p = valueBought / quantityBought (entry price, 0..1)
+won = the outcome you held paid out
+z = (wins − Σp) / sqrt(Σ p(1−p)) standard deviations above odds-implied
+```
+
+`z` high over enough bets, on a wallet that isn't a market-maker, is the real
+signal (the same one ../insider.py computes, here over the entire market).
+
+## Pipeline
+
+| step | script | source | notes |
+|------|--------|--------|-------|
+| 1 | `ingest.py conditions` | subgraph | resolution + payoutNumerators → winning outcome |
+| 2 | `gamma_tokens.py` | Gamma | token_id → (condition, outcome_index); subgraph's `outcomeIndex` is null |
+| 3 | `ingest.py accounts` | subgraph | `numTrades` (market-maker filter), `creationTimestamp` (freshness) |
+| 4 | `ingest.py market_positions` | subgraph | the heavy table: entry price + win/loss per bet |
+| 5 | `score.py` | DuckDB | z, true win rate, profit + FDR + out-of-sample |
+
+All ingests are **resumable** (per-table `id` cursor in `_cursor`), so a long
+run can be stopped and restarted. Tables join in `edge.sql`.
+
+## Guardrails (so a 1.76M-wallet scan doesn't just surface luck)
+
+- **min-n + market-maker cap.** A 300k-trade grinder posts huge z with no
+ information. Require ≥30 resolved bets and cap `numTrades`.
+- **Benjamini–Hochberg FDR.** Scan 100k wallets and thousands clear z>3 by
+ chance (look-elsewhere effect). `score.py` reports how many survive 5% FDR.
+- **Out-of-sample.** `score.py --cutoff YYYY-MM-DD` selects wallets on bets
+ resolved before the date, then measures the *same* wallets forward. Edge that
+ is real persists; edge that is curve-fit reverts to z≈0 — which is what every
+ strategy in ../FINDINGS.md did. **Do not size up on in-sample z.**
+
+## Usage
+
+```bash
+pip install duckdb
+python3 ingest.py conditions accounts # small tables
+python3 gamma_tokens.py # token→outcome map
+python3 ingest.py -p market_positions # heavy (~53M rows); parallel + resumable
+python3 score.py --min-n 15 --max-trades 5000 --top 40
+python3 score.py --cutoff 2026-04-30 --min-n 15 # in-sample vs forward
+```
diff --git a/wide/clob_tokens.py b/wide/clob_tokens.py
new file mode 100644
index 00000000..24a776b6
--- /dev/null
+++ b/wide/clob_tokens.py
@@ -0,0 +1,83 @@
+#!/usr/bin/env python3
+"""Build token -> (condition, outcome_index, winner) from the CLOB /markets feed.
+
+Gamma offset-paginates and 422s past ~10k markets; the subgraph's outcomeIndex
+is null. CLOB /markets cursor-paginates the full market set (no offset cap),
+1000 per page, and each token carries `winner` directly — so win/loss comes
+straight from here instead of parsing payoutNumerators.
+
+ python3 clob_tokens.py # page all markets, fill market_data
+"""
+
+import json
+import ssl
+import time
+import urllib.request
+
+import duckdb
+
+DB = "pmkt.duckdb"
+CLOB = "https://clob.polymarket.com/markets"
+_CTX = ssl._create_unverified_context()
+END = "LTE=" # CLOB's end-of-pagination cursor (base64 of -1)
+
+
+def get(cursor, retries=6):
+ url = CLOB + (f"?next_cursor={cursor}" if cursor else "")
+ delay = 1.0
+ for attempt in range(retries):
+ try:
+ req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
+ return json.loads(urllib.request.urlopen(req, timeout=40, context=_CTX).read())
+ except Exception:
+ if attempt == retries - 1:
+ raise
+ time.sleep(delay); delay = min(delay * 2, 20)
+
+
+def main():
+ con = duckdb.connect(DB)
+ con.execute("""CREATE TABLE IF NOT EXISTS market_data (
+ token_id TEXT PRIMARY KEY, condition_id TEXT,
+ outcome_index INT, winner BOOLEAN);""")
+ con.execute("CREATE TABLE IF NOT EXISTS _cursor (table_name TEXT PRIMARY KEY, last_id TEXT);")
+ row = con.execute("SELECT last_id FROM _cursor WHERE table_name='market_data#clob'").fetchone()
+ cursor = row[0] if row else "" # resume from saved CLOB cursor
+ total, pages, t0 = 0, 0, time.time()
+ if cursor:
+ print(f"resuming token map from cursor {cursor}", flush=True)
+ while cursor != END:
+ d = get(cursor)
+ rows = []
+ for m in d.get("data", []):
+ cond = m.get("condition_id")
+ toks = m.get("tokens") or []
+ if not cond:
+ continue
+ for idx, t in enumerate(toks): # tokens ordered by outcome
+ tid = t.get("token_id")
+ if tid:
+ rows.append((str(tid), cond, idx, bool(t.get("winner"))))
+ pages += 1
+ nxt = d.get("next_cursor")
+ if rows:
+ con.execute("BEGIN TRANSACTION")
+ con.executemany("INSERT OR IGNORE INTO market_data VALUES (?,?,?,?)", rows)
+ # checkpoint the NEXT cursor so a resume continues past this page
+ con.execute("INSERT OR REPLACE INTO _cursor VALUES ('market_data#clob', ?)",
+ [nxt or cursor])
+ con.execute("COMMIT")
+ total += len(rows)
+ if not nxt or nxt == cursor: # safety: no progress
+ break
+ cursor = nxt
+ if pages % 25 == 0:
+ print(f" {pages} pages · {total:,} tokens ({total/max(1e-9,time.time()-t0):,.0f}/s)", flush=True)
+ cnt = con.execute("SELECT count(*) FROM market_data").fetchone()[0]
+ won = con.execute("SELECT count(*) FROM market_data WHERE winner").fetchone()[0]
+ print(f"done — {cnt:,} token rows ({won:,} winning) over {pages} pages", flush=True)
+ con.close()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/wide/edge.sql b/wide/edge.sql
new file mode 100644
index 00000000..797ea296
--- /dev/null
+++ b/wide/edge.sql
@@ -0,0 +1,47 @@
+-- Per-wallet edge over RESOLVED markets, computed entirely in DuckDB.
+--
+-- The join chain: market_positions (a wallet's buy in one outcome token)
+-- -> market_data (token -> condition + outcome_index)
+-- -> conditions (resolution + payoutNumerators -> which outcome won).
+--
+-- Why this beats the data-api: market_positions records a buy whether or not
+-- the wallet redeemed, so losers are NOT hidden. The survivorship bias that
+-- makes /closed-positions read 90% (truly 48%) does not exist here.
+--
+-- entry price p = valueBought / quantityBought (USDC 6dp / shares 6dp -> 0..1)
+-- won = payoutNumerators[outcome_index] != 0
+-- z = (wins - Σp) / sqrt(Σ p(1-p)) -- wins above what odds implied
+--
+-- :cutoff_ts binds an out-of-sample boundary. Pass 0 to score everything.
+
+WITH bet AS (
+ SELECT
+ mp.user_id,
+ c.resolution_ts,
+ LEAST(0.999, GREATEST(0.001,
+ mp.val_bought::DOUBLE / mp.qty_bought)) AS p,
+ CASE WHEN md.winner THEN 1 ELSE 0 END AS won
+ FROM market_positions mp
+ JOIN market_data md ON md.token_id = mp.token_id
+ JOIN conditions c ON c.id = md.condition_id
+ WHERE mp.qty_bought > 0
+ AND c.resolution_ts > 0
+)
+SELECT
+ b.user_id,
+ count(*) AS n,
+ sum(b.won) AS wins,
+ round(sum(b.p), 1) AS exp_wins,
+ round(100.0 * sum(b.won) / count(*), 1) AS win_rate,
+ round((sum(b.won) - sum(b.p))
+ / sqrt(nullif(sum(b.p * (1 - b.p)), 0)), 2) AS z,
+ round(avg(b.p), 3) AS avg_entry,
+ a.scaled_profit AS profit,
+ a.scaled_volume AS volume,
+ a.creation_ts
+FROM bet b
+LEFT JOIN accounts a ON a.id = b.user_id
+WHERE b.resolution_ts <= :cutoff_ts OR :cutoff_ts = 0
+GROUP BY b.user_id, a.scaled_profit, a.scaled_volume, a.creation_ts
+HAVING count(*) >= :min_n
+ORDER BY z DESC;
diff --git a/wide/ingest.py b/wide/ingest.py
new file mode 100644
index 00000000..9a4b0e8e
--- /dev/null
+++ b/wide/ingest.py
@@ -0,0 +1,217 @@
+#!/usr/bin/env python3
+"""Bulk-ingest the Polymarket subgraph into a local DuckDB (pmkt.duckdb).
+
+Each table is cursor-paginated by id and resumable: we checkpoint the last
+id seen, and re-running continues where it left off (INSERT OR IGNORE makes
+overlap harmless). All ranking/scoring happens later in SQL — see edge.sql.
+
+ python3 ingest.py conditions market_data accounts # the small tables
+ python3 ingest.py market_positions # the heavy table
+ python3 ingest.py all
+"""
+
+import queue
+import sys
+import threading
+import time
+
+import duckdb
+
+import subgraph as sg
+
+DB = "pmkt.duckdb"
+BATCH = 5000
+
+SCHEMA = """
+CREATE TABLE IF NOT EXISTS conditions (
+ id TEXT PRIMARY KEY, resolution_ts BIGINT,
+ payout_num TEXT, payout_den BIGINT, slots INT);
+CREATE TABLE IF NOT EXISTS market_data (
+ token_id TEXT PRIMARY KEY, condition_id TEXT, outcome_index INT);
+CREATE TABLE IF NOT EXISTS accounts (
+ id TEXT PRIMARY KEY, num_trades BIGINT, creation_ts BIGINT,
+ scaled_profit DOUBLE, scaled_volume DOUBLE);
+CREATE TABLE IF NOT EXISTS market_positions (
+ id TEXT PRIMARY KEY, user_id TEXT, token_id TEXT,
+ qty_bought HUGEINT, val_bought HUGEINT, net_qty HUGEINT);
+CREATE TABLE IF NOT EXISTS _cursor (table_name TEXT PRIMARY KEY, last_id TEXT);
+"""
+
+# entity -> (graphql_fields, where_clause, row_mapper, target_table, columns)
+def _i(x, d=0):
+ try:
+ return int(x)
+ except (TypeError, ValueError):
+ return d
+
+def _f(x, d=0.0):
+ try:
+ return float(x)
+ except (TypeError, ValueError):
+ return d
+
+SPECS = {
+ "conditions": dict(
+ entity="conditions",
+ fields="id resolutionTimestamp payoutNumerators payoutDenominator outcomeSlotCount",
+ where="",
+ table="conditions",
+ cols=("id", "resolution_ts", "payout_num", "payout_den", "slots"),
+ row=lambda c: (c["id"], _i(c.get("resolutionTimestamp")),
+ ",".join(c.get("payoutNumerators") or []),
+ _i(c.get("payoutDenominator")), _i(c.get("outcomeSlotCount"))),
+ ),
+ # NOTE: market_data (token -> outcome) comes from Gamma, not the subgraph —
+ # the subgraph's marketData.outcomeIndex is null. See gamma_tokens.py.
+ "accounts": dict(
+ entity="accounts",
+ fields="id numTrades creationTimestamp scaledProfit scaledCollateralVolume",
+ where="",
+ table="accounts",
+ cols=("id", "num_trades", "creation_ts", "scaled_profit", "scaled_volume"),
+ row=lambda a: (a["id"], _i(a.get("numTrades")), _i(a.get("creationTimestamp")),
+ _f(a.get("scaledProfit")), _f(a.get("scaledCollateralVolume"))),
+ ),
+ # marketPosition.id == user_address (0x + 40 hex = 42 chars) + token_id
+ # (decimal). We split it out instead of selecting the nested user/market
+ # objects, which the subgraph errors on when `market` is null.
+ "market_positions": dict(
+ entity="marketPositions",
+ fields="id quantityBought valueBought netQuantity",
+ where="",
+ table="market_positions",
+ cols=("id", "user_id", "token_id", "qty_bought", "val_bought", "net_qty"),
+ row=lambda p: (p["id"], p["id"][:42], p["id"][42:],
+ _i(p.get("quantityBought")), _i(p.get("valueBought")),
+ _i(p.get("netQuantity"))),
+ ),
+}
+
+
+def ingest(con, name, limit=0):
+ spec = SPECS[name]
+ last = con.execute("SELECT last_id FROM _cursor WHERE table_name=?", [name]).fetchone()
+ start = last[0] if last else ""
+ placeholders = ",".join("?" * len(spec["cols"]))
+ insert = (f"INSERT OR IGNORE INTO {spec['table']} "
+ f"({','.join(spec['cols'])}) VALUES ({placeholders})")
+ buf, total, t0, last_seen = [], 0, time.time(), start
+
+ def flush(cursor_id):
+ nonlocal buf
+ if buf:
+ con.executemany(insert, buf)
+ buf = []
+ con.execute("INSERT OR REPLACE INTO _cursor VALUES (?, ?)", [name, cursor_id])
+
+ print(f"[{name}] resuming from id={start[:14] or '(start)'}", flush=True)
+ for row in sg.paginate(spec["entity"], spec["fields"], where=spec["where"], start_id=start):
+ buf.append(spec["row"](row))
+ total += 1
+ last_seen = row["id"]
+ if len(buf) >= BATCH:
+ flush(last_seen)
+ rate = total / max(1e-9, time.time() - t0)
+ print(f"[{name}] {total:>9,} ({rate:,.0f}/s)", flush=True)
+ if limit and total >= limit:
+ break
+ flush(last_seen) # remaining buffer + advance cursor to the last id seen
+ cnt = con.execute(f"SELECT count(*) FROM {spec['table']}").fetchone()[0]
+ print(f"[{name}] done — {total:,} pulled this run, {cnt:,} rows total", flush=True)
+
+
+def ingest_parallel(con, name, shards=16):
+ """Page `shards` id-ranges concurrently; workers fetch and enqueue, this
+ (single) thread writes to DuckDB and checkpoints each shard's cursor.
+ ~`shards`× the sequential throughput, and fully resumable per shard."""
+ spec = SPECS[name]
+ bounds = sg.shard_bounds(shards)
+ placeholders = ",".join("?" * len(spec["cols"]))
+ insert = (f"INSERT OR IGNORE INTO {spec['table']} "
+ f"({','.join(spec['cols'])}) VALUES ({placeholders})")
+ starts = {}
+ for i in range(shards):
+ row = con.execute("SELECT last_id FROM _cursor WHERE table_name=?",
+ [f"{name}#{i:02d}"]).fetchone()
+ starts[i] = row[0] if row else ""
+ q = queue.Queue(maxsize=400)
+ DONE = object()
+
+ def worker(i):
+ try:
+ for rows, last in sg.paginate_pages(spec["entity"], spec["fields"],
+ lo=bounds[i], hi=bounds[i + 1],
+ start_id=starts[i]):
+ q.put((i, [spec["row"](r) for r in rows], last))
+ except Exception as e:
+ q.put((i, "ERR", str(e)[:120]))
+ q.put((i, DONE, None))
+
+ for i in range(shards):
+ threading.Thread(target=worker, args=(i,), daemon=True).start()
+
+ # DuckDB fsyncs per commit, so committing each page caps us at the writer
+ # (~380/s) while fetch concurrency does ~4,600/s. Buffer many pages and
+ # commit in one transaction to amortize the fsync.
+ COMMIT_ROWS = 25000
+ finished, total, t0 = 0, 0, time.time()
+ buf, shard_last = [], {}
+
+ def flush():
+ nonlocal buf
+ if not buf:
+ return
+ con.execute("BEGIN TRANSACTION")
+ con.executemany(insert, buf)
+ for sh, lid in shard_last.items():
+ con.execute("INSERT OR REPLACE INTO _cursor VALUES (?, ?)",
+ [f"{name}#{sh:02d}", lid])
+ con.execute("COMMIT")
+ buf = []
+
+ print(f"[{name}] {shards} parallel shards", flush=True)
+ while finished < shards:
+ i, payload, last = q.get()
+ if payload is DONE:
+ finished += 1
+ continue
+ if payload == "ERR":
+ print(f"[{name}] shard {i:02d} error: {last}", flush=True)
+ continue
+ buf.extend(payload)
+ shard_last[i] = last
+ total += len(payload)
+ if len(buf) >= COMMIT_ROWS:
+ flush()
+ rate = total / max(1e-9, time.time() - t0)
+ print(f"[{name}] {total:>10,} ({rate:,.0f}/s, {finished}/{shards} shards done)",
+ flush=True)
+ flush()
+ cnt = con.execute(f"SELECT count(*) FROM {spec['table']}").fetchone()[0]
+ print(f"[{name}] done — {total:,} pulled this run, {cnt:,} rows total", flush=True)
+
+
+def main(argv):
+ parallel = False
+ if argv and argv[0] in ("-p", "--parallel"):
+ parallel = True; argv = argv[1:]
+ limit = 0
+ if argv and argv[-1].isdigit(): # optional trailing row-limit (per table)
+ limit = int(argv[-1]); argv = argv[:-1]
+ targets = argv or ["all"]
+ if targets == ["all"]:
+ targets = ["conditions", "accounts", "market_positions"]
+ con = duckdb.connect(DB)
+ con.execute(SCHEMA)
+ for t in targets:
+ if t not in SPECS:
+ print(f"unknown table: {t}", file=sys.stderr); continue
+ if parallel and not limit:
+ ingest_parallel(con, t)
+ else:
+ ingest(con, t, limit)
+ con.close()
+
+
+if __name__ == "__main__":
+ main(sys.argv[1:])
diff --git a/wide/run_full.sh b/wide/run_full.sh
new file mode 100755
index 00000000..3137f78e
--- /dev/null
+++ b/wide/run_full.sh
@@ -0,0 +1,31 @@
+#!/bin/bash
+# Massive ingest of the wallet data we still need, from the FAST subgraph
+# (accounts + market_positions). The CLOB token map is already sufficient
+# (1.27M tokens covering every resolved condition), so it's NOT re-run here —
+# run `python3 clob_tokens.py` separately if you want to refresh it.
+#
+# Both steps are parallel (16 id-shards) and resumable (per-shard cursors), so
+# killing and re-running continues from the last checkpoint.
+#
+# nohup ./run_full.sh > run_full.log 2>&1 < /dev/null & disown
+# tail -f run_full.log
+set -u
+cd "$(dirname "$0")"
+
+until python3 -c "import duckdb;duckdb.connect('pmkt.duckdb',read_only=True).close()" 2>/dev/null; do
+ sleep 5
+done
+
+echo "[run] $(date '+%F %T') 1/2 accounts (parallel, resumable) …"
+python3 ingest.py -p accounts
+
+echo "[run] $(date '+%F %T') 2/2 market_positions (parallel, resumable, the big one) …"
+python3 ingest.py -p market_positions
+
+echo "[run] $(date '+%F %T') DONE"
+python3 - <<'PY'
+import duckdb
+c = duckdb.connect("pmkt.duckdb", read_only=True)
+for t in ("conditions", "market_data", "accounts", "market_positions"):
+ print(f" {t:18} {c.execute('select count(*) from '+t).fetchone()[0]:,}")
+PY
diff --git a/wide/score.py b/wide/score.py
new file mode 100644
index 00000000..f819d086
--- /dev/null
+++ b/wide/score.py
@@ -0,0 +1,145 @@
+#!/usr/bin/env python3
+"""Rank wallets by edge from the ingested DuckDB — with the guardrails that
+stop a massive scan from just surfacing luck.
+
+Edge metric: z = (wins - Σp)/√Σp(1-p), wins above what entry odds implied.
+A high win rate alone is meaningless (buy 95¢ favorites -> 90% wins, no edge),
+and on this data win rate isn't even biased-high the way the data-api is.
+
+Guardrails:
+ * min_n resolved bets and a market-maker cap on num_trades (a 300k-trade
+ grinder posts huge z with no information — see FINDINGS.md / bjprolo).
+ * Benjamini-Hochberg FDR: scan 100k wallets and thousands clear z>3 by
+ chance. We report how many survive a 5% false-discovery rate.
+ * Out-of-sample: --cutoff scores wallets on bets resolved on/before a date,
+ then measures the SAME wallets forward. Edge that's real persists; edge
+ that's curve-fit (every strategy we tested) reverts to z~0 / 50%.
+
+ python3 score.py --min-n 30 --max-trades 5000 --top 40
+ python3 score.py --cutoff 2026-04-30 --min-n 30 # in-sample vs forward
+"""
+
+import argparse
+import math
+import time
+
+import duckdb
+
+DB = "pmkt.duckdb"
+
+
+def norm_sf(z):
+ """One-sided P(Z > z): the probability this z came from luck."""
+ return 0.5 * math.erfc(z / math.sqrt(2)) if z is not None else 1.0
+
+
+def load_sql(cutoff_ts, min_n):
+ sql = open("edge.sql").read()
+ # these are ints we control (not user strings) -> safe to template
+ return sql.replace(":cutoff_ts", str(int(cutoff_ts))).replace(":min_n", str(int(min_n)))
+
+
+def rank(con, cutoff_ts, min_n, max_n):
+ rows = con.execute(load_sql(cutoff_ts, min_n)).fetchall()
+ cols = [d[0] for d in con.description]
+ out = []
+ for r in rows:
+ d = dict(zip(cols, r))
+ # num_trades is null in this subgraph, so use the resolved-bet count as
+ # the market-maker proxy: a wallet with thousands of bets is grinding a
+ # systematic edge (bjprolo-style), not trading on information.
+ if max_n and d["n"] > max_n:
+ continue
+ d["pval"] = norm_sf(d["z"])
+ out.append(d)
+ return out
+
+
+def bh_fdr(rows, q=0.05):
+ """Benjamini-Hochberg: how many discoveries survive a q false-discovery rate."""
+ m = len(rows)
+ if not m:
+ return 0, 1.0
+ ps = sorted(r["pval"] for r in rows)
+ k = 0
+ for i, p in enumerate(ps, 1):
+ if p <= q * i / m:
+ k = i
+ thresh = ps[k - 1] if k else 0.0
+ return k, thresh
+
+
+def to_ts(date_str):
+ if not date_str:
+ return 0
+ return time.mktime(time.strptime(date_str, "%Y-%m-%d"))
+
+
+def main():
+ ap = argparse.ArgumentParser(description=__doc__,
+ formatter_class=argparse.RawDescriptionHelpFormatter)
+ ap.add_argument("--min-n", type=int, default=15, help="min resolved bets")
+ ap.add_argument("--max-n", type=int, default=3000,
+ help="exclude wallets with more resolved bets (market-maker proxy); 0=off")
+ ap.add_argument("--top", type=int, default=40)
+ ap.add_argument("--cutoff", help="YYYY-MM-DD: in-sample/out-of-sample split")
+ args = ap.parse_args()
+
+ con = duckdb.connect(DB, read_only=True)
+ cutoff_ts = to_ts(args.cutoff)
+ rows = rank(con, cutoff_ts, args.min_n, args.max_n)
+ rows.sort(key=lambda r: (r["z"] is not None, r["z"]), reverse=True)
+
+ k, thresh = bh_fdr(rows)
+ label = f"on/before {args.cutoff}" if args.cutoff else "all resolved bets"
+ print(f"\nscored {len(rows):,} wallets (min_n={args.min_n}, "
+ f"max_n={args.max_n or '∞'}) · {label}")
+ print(f"Benjamini-Hochberg @5% FDR: {k:,} wallets survive (p ≤ {thresh:.1e}) "
+ f"— the rest of the high-z tail is consistent with luck\n")
+
+ hdr = f"{'z':>6}{'p(luck)':>10}{'rec':>13}{'win%':>7}{'avgP':>7}{'volume':>12}{'profit':>11} wallet"
+ print(hdr); print("-" * len(hdr))
+ for r in rows[:args.top]:
+ rec = f"{r['wins']}/{r['n']}(E{r['exp_wins']:.0f})"
+ p = r["pval"]
+ ps = "<1e-12" if p <= 0 else (f"{p:.1e}" if p < 1e-3 else f"{p:.3f}")
+ star = " *" if p <= thresh and thresh > 0 else " "
+ print(f"{r['z']:>6.1f}{ps:>10}{rec:>13}{r['win_rate']:>6.1f}%{r['avg_entry']:>7.2f}"
+ f"{(r['volume'] or 0):>12,.0f}{(r['profit'] or 0):>11,.0f}{star}{r['user_id']}")
+
+ if args.cutoff:
+ forward_oos(con, rows[:args.top], cutoff_ts, args.min_n)
+
+
+def forward_oos(con, picks, cutoff_ts, min_n):
+ """For the in-sample top picks, measure their record AFTER the cutoff."""
+ print(f"\n{'='*70}\nOUT-OF-SAMPLE: same wallets, only bets resolved AFTER cutoff")
+ print(f"{'='*70}")
+ ids = [r["user_id"] for r in picks]
+ if not ids:
+ return
+ # reuse the same join but flip the time filter and restrict to these wallets
+ sql = open("edge.sql").read()
+ sql = sql.replace("WHERE b.resolution_ts <= :cutoff_ts OR :cutoff_ts = 0",
+ f"WHERE b.resolution_ts > {int(cutoff_ts)} "
+ f"AND b.user_id IN ({','.join(repr(i) for i in ids)})")
+ sql = sql.replace("HAVING count(*) >= :min_n", "HAVING count(*) >= 1")
+ fwd = {r[0]: r for r in con.execute(sql).fetchall()}
+ cols = [d[0] for d in con.description]
+ zi, wi, ni, wri = cols.index("z"), cols.index("wins"), cols.index("n"), cols.index("win_rate")
+ print(f"{'in-sample z':>12}{'fwd z':>8}{'fwd rec':>12}{'fwd win%':>9} wallet")
+ for r in picks:
+ f = fwd.get(r["user_id"])
+ if f:
+ fz = f"{f[zi]:.1f}" if f[zi] is not None else "n/a"
+ print(f"{r['z']:>12.1f}{fz:>8}{f'{f[wi]}/{f[ni]}':>12}{f[wri]:>8.1f}% {r['user_id']}")
+ else:
+ print(f"{r['z']:>12.1f}{'—':>8}{'(no fwd bets)':>12}{'—':>9} {r['user_id']}")
+ fz = [fwd[i][zi] for i in ids if i in fwd and fwd[i][zi] is not None]
+ if fz:
+ print(f"\nmedian forward z of in-sample winners: {sorted(fz)[len(fz)//2]:.2f} "
+ f"(near 0 = the in-sample edge did NOT persist)")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/wide/subgraph.py b/wide/subgraph.py
new file mode 100644
index 00000000..4e95b103
--- /dev/null
+++ b/wide/subgraph.py
@@ -0,0 +1,118 @@
+#!/usr/bin/env python3
+"""Goldsky Polymarket orderbook subgraph client.
+
+The hosted subgraph times out on any `orderBy` over a non-indexed field
+(numTrades, scaledProfit, ...), so we never sort server-side. Instead we
+cursor-paginate by `id` (the indexed primary key) — `where:{id_gt:}`,
+`first:1000` — which is stable and resumable. Ranking happens locally in
+DuckDB after ingest. That constraint is exactly why the bulk-ingest design
+is the only one that scales here.
+"""
+
+import json
+import ssl
+import time
+import urllib.request
+
+ENDPOINT = ("https://api.goldsky.com/api/public/"
+ "project_cl6mb8i9h0003e201j6li0diw/subgraphs/"
+ "polymarket-orderbook-resync/prod/gn")
+
+_CTX = ssl._create_unverified_context()
+PAGE = 1000
+
+
+def query(gql, variables=None, retries=6):
+ """POST a GraphQL query, retrying on transient errors / timeouts."""
+ body = json.dumps({"query": gql, "variables": variables or {}}).encode()
+ delay = 1.0
+ for attempt in range(retries):
+ try:
+ req = urllib.request.Request(
+ ENDPOINT, data=body,
+ headers={"Content-Type": "application/json",
+ "User-Agent": "Mozilla/5.0"})
+ r = json.loads(urllib.request.urlopen(req, timeout=60, context=_CTX).read())
+ if "errors" in r:
+ # statement-timeout is transient under load; back off and retry
+ msg = r["errors"][0].get("message", "")
+ if "timeout" in msg.lower() or "timed out" in msg.lower():
+ raise TimeoutError(msg)
+ raise RuntimeError(msg[:300])
+ return r["data"]
+ except Exception as e:
+ if attempt == retries - 1:
+ raise
+ time.sleep(delay)
+ delay = min(delay * 2, 30)
+ return None
+
+
+def paginate(entity, fields, where="", page=PAGE, start_id="", on_page=None):
+ """Yield every row of `entity`, cursor-paginating by id.
+
+ `fields` is the GraphQL selection set (a string). `where` is extra filter
+ clauses (without braces), e.g. 'resolutionTimestamp_not: null'. Resume by
+ passing the last id seen as `start_id`.
+ """
+ last = start_id
+ while True:
+ clauses = f'id_gt: "{last}"'
+ if where:
+ clauses += ", " + where
+ gql = (f'{{ {entity}(first: {page}, orderBy: id, orderDirection: asc, '
+ f'where: {{ {clauses} }}) {{ {fields} }} }}')
+ rows = query(gql).get(entity, [])
+ if not rows:
+ return
+ for row in rows:
+ yield row
+ last = rows[-1]["id"]
+ if on_page:
+ on_page(len(rows), last)
+ if len(rows) < page:
+ return
+
+
+def shard_bounds(n=16):
+ """Hex-prefix boundaries after '0x' that split the id space into n shards.
+ ids are lowercase hex; '0xg' sorts after every '0xf…' so it caps the last
+ shard. Returns n+1 bounds; shard i spans [bounds[i], bounds[i+1])."""
+ d = "0123456789abcdef"
+ if n == 16:
+ return ["0x" + c for c in d] + ["0xg"]
+ if n == 256:
+ return ["0x" + a + b for a in d for b in d] + ["0xg"]
+ raise ValueError("n must be 16 or 256")
+
+
+def paginate_pages(entity, fields, lo="", hi="", start_id="", page=PAGE):
+ """Yield (rows, last_id) per page for ids in (max(start_id,lo), hi).
+ Page-level so the caller can checkpoint each shard's cursor."""
+ last = start_id or lo
+ while True:
+ clauses = f'id_gt: "{last}"'
+ if hi:
+ clauses += f', id_lt: "{hi}"'
+ gql = (f'{{ {entity}(first: {page}, orderBy: id, orderDirection: asc, '
+ f'where: {{ {clauses} }}) {{ {fields} }} }}')
+ rows = query(gql).get(entity, [])
+ if not rows:
+ return
+ yield rows, rows[-1]["id"]
+ if len(rows) < page:
+ return
+ last = rows[-1]["id"]
+
+
+if __name__ == "__main__":
+ # smoke test: count a few resolved conditions
+ n = 0
+ for c in paginate("conditions", "id resolutionTimestamp payoutNumerators",
+ where="resolutionTimestamp_not: null"):
+ n += 1
+ if n <= 2:
+ print(c)
+ if n >= 2500:
+ break
+ print(f"paged {n} resolved conditions ok")