Files
winning-wallet-finder/live/README.md
T
jaxperro ecb7a2183a docs: rewrite READMEs around the deployed system (July 2026)
Root README now leads with what actually runs (Mac daily pipeline, Railway
copybot worker, Discord watcher, static dashboard), the July live test, the
realism model (fees/lag/dynamic sizing/missed-bet ledger), a new-developer
quickstart, secrets map, and file map; research story condensed with current
numbers (12 fee-aware sharps, +531% in-sample backfill clearly labeled).
live/README: fee-aware selection, dynamic-sizing portfolio, three dashboard
feeds, copybot now 24/7 on Railway (was "in progress").

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 11:32:37 -04:00

12 KiB
Raw Blame History

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 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(1p) clearly > 0 — wins above what entry odds implied. This is the closed form of the paper's "randomize direction 10k×" benchmark.
  3. BenjaminiHochberg 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)
sharps conviction_scan.py + validate_timing.py conviction-profile scan → copy-positive-holder selection → watch_sharps.json (see "The repeatable find")
portfolio portfolio.py $1k paper book off the cache → portfolio.json (see "Paper portfolio")
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 ~30k wallets / 14M+ 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.

Schema v2 (2026-07-02) — token-keyed, provenance-tagged, archival. bets now carries asset (token id — the position identity), src/ts (endpoint provenance + close time), and resolved (False = early-sold position in a market that hadn't ended at pull time; its won is a curPrice mark, not an outcome — scorers filter these). p is stored raw (0 = avgPrice missing) and clamped to [0.001, 0.999] by get_bets on read, so "missing price" stays distinguishable from a real 0.1¢ longshot. Refresh is an upsert by token (plus superseded legacy rows), not a wallet wipe: each pull still covers the rolling WINDOW_DAYS, but rows that slide out of the window now survive, so per-wallet history accumulates into a permanent archive. The same-asset row from both endpoints (a partially-closed position) is deduped to the larger- stake row instead of double-counting. Failed pulls are returned empty but NOT cached and NOT marked pulled — they retry on the next call instead of masquerading as "no bets" for MAX_AGE_DAYS (pre-v2, an API error could cache a wallet as empty-and-fresh; that bug bit the watchlist in practice). Legacy v1 rows keep NULLs in the new columns until their wallet's next refresh. The migration runs automatically on first open (v1 → v2, exact-duplicate rows merged). Per-endpoint pagination is still capped at ~2k bets (max_pages); ../wide/pmkt.duckdb remains the deep-history subgraph dataset.

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.

Strategy backtests

  • strategy.py — train (pre-May-30) / test (June1+) wallet selection on copy-ROI
    • z + monthly consistency + diversification. → selection.json.
  • followability.py — pull entry timestamps (cached), drop wallets whose edge is in un-followable fast/live markets, re-rank on followable forward bets. → watch_final.json (the execution-realistic list).
  • pnl_basket.py / pnl_focused.py — $1,000 capital-constrained copy sims with missed-trade accounting. Key result: the broad basket loses on $1k (can't follow 1,200 trades), but 12 wallets + a conviction (bet-size) filter clears out-of-sample. See ../FINDINGS.md.

The repeatable find (conviction_scan.py + validate_timing.py)

The best result. Score wallets on their high-conviction bets only — the top 20% by stake size (per-wallet p80, not a flat $200): the edge is wallets that win 7080% on genuinely-uncertain (~0.40.6) markets — real skill, not favorite-riding. The per-wallet cutoff reproduces flat-$200's win-rate lift while adapting to each wallet's scale (a whale's $200 bet isn't conviction; a minnow's is).

  • conviction_scan.py — train pre-June / validate June on conviction bets (resolved-only) → 62/83 profitable forward (p≈0). → conviction_wallets.json.
  • validate_timing.py — the copyability selection, now fee-aware (2026-07-02): for every conviction wallet it runs a flat-$50 copy replay that pays the real taker fee on entries AND mirrored exits, and keeps only the ones genuinely profitable to copy: copy_pnl > 0 and a real hold-to-resolution edge (held_pnl > 0, held win-rate ≥55% over ≥8 resolved held bets), active in 30d, median lead ≥1h (drops sub-hour snipers). It also precomputes every stat the dashboard renders — including copy_pnl, the authoritative fee-adjusted flat-$50 copy P&L (replay entries, mirror exits, settle held bets at CLOB resolution by token_id). → 12 copy-positive holders in watch_sharps.json (read live by jaxperro.com/trading; the table defaults to sorting by Copy P&L).

Copy P&L is the one number that matters for picking copy targets. It replaced an earlier lead-time gate. The lesson (see FINDINGS "the scalper trap"): position win% over-counts scalperscache.won (curPrice ≥ 0.5) scores a sell-at- profit as a "win", so a wallet can show ~100% conviction win% yet lose money when copied (ArbTrader: 99.5% win, $790 copy P&L). Conviction itself = the top 20% of a wallet's own stake sizes, measured at the position level (a wallet's total stake in a market, not per-trade — a scalper splits one position over many small buys). Judge by Copy P&L, never win%.

Paper portfolio (portfolio.pyportfolio.json)

A $1,000 backtest book that mirrors the followed wallets' conviction bets, computed off the cache (not client-side), backfilled from June 1. It runs the same realism model as the live copybot: dynamic stakes (4% of current equity, Kelly-style compounding, halved below 80% of the equity high-water mark, no per-trade cap), the real taker fee on every entry, a +0.5%/~90s lag-slippage haircut, and an optional per-event correlation cap (EVENT_CAP, currently off). The cache stores each bet's resolution time (res_t), so capital recycles at the true resolution moment; unresolved (early-sold) rows never score. Edit the WALLETS list at the top to change who's followed. Output portfolio.json (equity, splits, per-bet stakes, current/resolved/missed tables, fee totals, sizing params) is read by the dashboard in one request; a small live /positions pull supplies the "current open bets" panel.

Caveat carried through the whole stack: fees/lag are modeled, but the wallets were selected on June data, so the June backfill is in-sample by construction (the project's own history: an in-sample copy backtest hit +168% then collapsed out-of-sample). The July forward book — the Railway bot — is the number that counts.

Dashboard feeds (jaxperro.com/trading)

The dashboard (in the jaxperro repo, trading/index.html) is static and reads three precomputed JSONs from this repo via raw.githubusercontent:

  • copybot_live.json — the live bot's book (open/resolved/missed bets with per-fill lag, slippage, fees), committed by the bot itself on change.
  • watch_sharps.json — the sharps table (fee-adjusted Copy P&L, win%/record, avg bet, leads).
  • portfolio.json — the backtest book.

The latter two are committed + pushed by daily.sh, so the page is just a renderer (no per-wallet API calls), with a client-side replay fallback.

Copy execution (../copybot.py — running 24/7 on Railway)

The bot runs as a Railway worker (../host/start.sh: clones this repo with a scoped GITHUB_TOKEN, resumes the last committed state, polls the followed wallets every 60s, and commits state + feed + fills back — no volume needed). Paper mode is the July 2026 forward test; live mode (real money) is gated behind mode:"live" + --live + a typed confirmation phrase — see ../LIVE_TEST.md for the supervised minimum-size runbook and ../preflight_live.py for the read-only credential check. sync_floors.py recomputes each followed wallet's p80 conviction floor from the fresh cache into ../config.json daily, so the bot's entry gate stays in parity with the dashboard's top-20%-by-stake definition (the committed copybot.paper.json floors are refreshed manually — keep them in sync when the follow set changes).

Daily (daily.sh, launchd 10:00)

  1. discover (enumerate.py 14) — recent liquid markets → candidate pool.
  2. freshen — force-refresh the watchlists, then collect.py tops up new/stale.
  3. re-score (skill.py) → watch_skilled.json.
  4. sharps (conviction_scan.py + validate_timing.py) → conviction_wallets.json
    • watch_sharps.json (the copy-positive holders).
  5. floors (sync_floors.py) — copy-bot conviction floors → ../config.json (local).
  6. portfolio (portfolio.py) → portfolio.json (the $1k paper book).
  7. dashboard (dashboard.py) + snapshot to history/.
  8. publish — commit + push the JSON feeds, then ping Discord (daily_webhook in the gitignored ../config.json).

Schedule via launchd/cron (Mac must be awake).

Usage

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.