From f176ba134adca0d3bb0c114bf6e1bfb5bc41d6b6 Mon Sep 17 00:00:00 2001 From: jaxperro Date: Sat, 13 Jun 2026 16:59:18 -0400 Subject: [PATCH] Rebrand to Winning Wallet Finder; commit validation scripts; dev-friendly docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add copyback.py (in-sample copy backtest), oos.py (out-of-sample test), huntwide.py (wide insider sweep) — completes the detect→hunt→validate→watch pipeline. - Rewrite README around Winning Wallet Finder: the z-score idea explained, how the pieces fit, quickstart, data sources, live-watcher setup, honest verdict. - Extend FINDINGS with the insider-detection results and the in-sample vs out-of-sample copy verdict (+545% in-sample collapsed to one-wallet variance out-of-sample). - Refresh config.example.json to the current schema (discord/alchemy/watch). Co-Authored-By: Claude Opus 4.8 --- FINDINGS.md | 55 +++++- README.md | 408 +++++++++++++------------------------------- config.example.json | 31 +--- copyback.py | 176 +++++++++++++++++++ huntwide.py | 128 ++++++++++++++ oos.py | 153 +++++++++++++++++ 6 files changed, 632 insertions(+), 319 deletions(-) create mode 100644 copyback.py create mode 100644 huntwide.py create mode 100644 oos.py diff --git a/FINDINGS.md b/FINDINGS.md index 7738ba5f..969ac6ac 100644 --- a/FINDINGS.md +++ b/FINDINGS.md @@ -68,10 +68,61 @@ so shared exchanges don't false-link everyone. (See `insider.py`.) material nonpublic information is illegal, and blindly following a suspected insider is not a safe strategy. +## Insider detection — what the z-score signal actually found + +Building `insider.py` and sweeping markets (`hunt.py`, `huntwide.py`) surfaced +genuinely improbable wallets. Out of ~289 scored: + +- **DREAMBIG.** (z=8.9, p≈2e-19) and **qcp14** (z=5.3) on the Iran ceasefire + market — 45–77% of wins entered <24h before resolution. Textbook insider + fingerprint, on exactly the theme the *60 Minutes* story covered. +- **Famecesgoal** (z=9.6) won only 14.5% of bets — but bet longshots and hit + +98 above what the odds implied. The clearest "beats the prices it pays" case. + +Two refinements proved essential: +- **Trade count separates insiders from bots.** `bjprolo` scored z=37 — but on + **306,873** lifetime trades. That's a market-maker grinding a tiny systematic + edge, not information. Real edge wallets show concentrated z over 1–3k trades. +- **Funding-cluster linking** (Alchemy, the Bubblemaps "who-funded-whom" step) + works *only* with a personal-hub filter: a shared exchange (everyone uses + Coinbase) is not a shared operator. Judge a funder by its own outbound degree. + +## The copy-trade verdict — in-sample vs out-of-sample + +The decisive test: does copying z-selected wallets make money? + +- **In-sample** (`copyback.py`): copy the edge wallets from May 30, z-weighted, + reinvest 100%. Result: **+545%** in 15 days. Looks incredible — and it's + circular (the wallets were *selected* for winning over that very window). + 86% of it came from one wallet; the highest-z pick contributed $23. +- **Out-of-sample** (`oos.py`): select wallets using **only data through + Apr 30**, then copy forward May 30→now. Result: **+168%** — but **entirely + from one longshot lottery wallet** (1.5% pre-period win rate hitting again). + The two strongest pre-period signals made **$0** forward. Forward hit rate was + 27%. That's variance, not edge that persists. + +**Conclusion:** even the one real signal (z-score), when tested for whether you +can *profit by copying it*, fails out-of-sample — joining every other strategy. +The detector is valuable for *finding* anomalous wallets; copying them is not a +proven, fundable edge. The live watcher (`webhook_receiver.py`) exists to gather +real forward (out-of-sample) data on these wallets — observe before you size up. + +## Practical conclusion 2 + +- **Don't** fund a copy strategy — both the +545% and +168% are + variance/concentration, not repeatable edge. +- **Do** use the detector to find statistically anomalous wallets and watch them + live; judge persistence forward with your own eyes. +- A durable trading edge has to come from *you* (a niche you know), with this + tooling built around your judgment. + ## Repo layout -- `insider.py` — the keeper: z-score/p-value detection, timing/freshness/sizing - signals, and Alchemy funding-cluster ring detection. +- `insider.py` — the detector: z-score/p-value, timing/freshness/sizing signals, + Alchemy funding-cluster ring detection. +- `hunt.py` / `huntwide.py` — market sweeps that surface edge wallets. +- `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). - `archive/` — the strategies that didn't work, kept for reference. See `archive/README.md`. diff --git a/README.md b/README.md index 39c6f594..8208db32 100644 --- a/README.md +++ b/README.md @@ -1,328 +1,148 @@ -# Polymarket Smart Money +# 🏆 Winning Wallet Finder -Tools to find Polymarket wallets worth following, copy-trade them (paper or -live), and backtest the strategy. Zero dependencies — Python 3 stdlib only -(except live trading, which needs `py-clob-client`). +Find Polymarket wallets with a **real, statistically-verifiable edge**, test +whether copying them actually makes money, and **get pinged the moment they +trade**. -> **Start here — read [What we learned](#what-we-learned-research-log).** The -> project began as "find wallets winning >75% of their bets." That metric turned -> out to be an artifact, and the research below changed what we actually measure. -> Don't fund anything before reading it. +This started as "copy the smart money." Along the way we tested — and ruled out +— six systematic public-data strategies, and found that the *only* signal that +holds up is **statistical improbability**: wallets that win far more than the +prices they paid imply. This repo is the tooling for finding and watching those +wallets, plus an honest record of everything that didn't work. -## Tools +> **Read [`FINDINGS.md`](FINDINGS.md) for the full story.** TL;DR: detection of +> edge wallets works; *profitably copying them* is unproven (it survived a naive +> backtest but collapsed to one-wallet variance out-of-sample). Treat this as a +> research + monitoring tool, not a money printer. -**Read [`FINDINGS.md`](FINDINGS.md) first** — it's the honest scorecard of what -worked (almost nothing) and what does (`insider.py`). +--- -| File | What it does | -|------|--------------| -| `insider.py` | **The keeper.** Insider/sharp detector — z-score/p-value of wins vs. entry odds, pre-resolution timing, fresh-wallet & sizing signals, and Alchemy funding-cluster ring detection. | -| `smart_money.py` | Data foundation + dashboard. Ranks leaderboard wallets by **true** (survivorship-corrected) win rate. | -| `archive/` | Eight strategy tools that didn't clear (copy-trade, LP farming, cross-venue arb, wallet-consistency research). Kept for reference — see [`archive/README.md`](archive/README.md). | +## The core idea: z-score, not win rate -## Run the dashboard +Every Polymarket bet has an entry price that *is* the market's estimate of its +odds (a YES at 30¢ ⇒ market thinks 30%). If you have no edge, over many bets +you win about the **sum of your entry prices** — call it *expected wins*. -```bash -python3 smart_money.py +``` +z = (actual wins − expected wins) / standard deviation ``` -Open **http://localhost:8899**, hit **Scan**, and wait a minute or two. -Adjust the filters (win rate, bets/week, minimum resolved bets, candidate -pool size) and the table updates live while the scan runs. Click any trader -to see their recent resolved bets and a link to their Polymarket profile. +- **z = 0** → you won exactly what your prices implied → no edge. +- **z = 3** → ~1-in-740 by luck. **z = 5** → ~1-in-3.5M. **z = 9** → astronomical. +- **`p(luck)`** is z as a probability: the chance a no-edge bettor does this well by chance. -## Run in the terminal +Why this beats win rate: a wallet that bets longshots and **wins 14% when the +odds implied 8%** has a huge edge (high z) despite a low win rate. A wallet +buying 90¢ favorites and winning 90% has z≈0 — no edge, just paying for +favorites. **z measures beating the prices you paid.** -```bash -python3 smart_money.py --scan # default 150-wallet pool -python3 smart_money.py --scan --pool 300 # broader sweep +Two refinements separate signal from noise: +- **Lifetime trade count** — high z + tens of thousands of trades = a + market-maker bot, not an insider. Real edge wallets have *concentrated* edge + over a few thousand trades. +- **Pre-resolution timing + fresh wallet** — entering minutes/hours before + resolution on a new account is the insider fingerprint (the Bubblemaps / + *60 Minutes* pattern). + +--- + +## How the pieces fit + +``` + data layer detection hunting validation live + ────────── ───────── ─────── ────────── ──── + smart_money.py ──▶ insider.py ──▶ hunt.py / huntwide.py ─▶ copyback.py / oos.py webhook_receiver.py + (Polymarket API, (z-score, timing, (sweep markets, (does copying them (Alchemy webhook → + true win rate) freshness, funding surface edge wallets) actually pay? in- & Discord ping on + clustering) out-of-sample) every trade) ``` -## How it works +| File | Role | +|------|------| +| `insider.py` | **The detector.** z-score/p-value, pre-resolution timing, fresh-wallet & sizing flags, and Alchemy funding-cluster ring detection. `--scan` / `--market` / `--wallet`. | +| `smart_money.py` | Data foundation + dashboard. Survivorship-corrected **true** win rate. | +| `hunt.py` | Ring-hunt sweep over a fixed list of news-driven event markets. | +| `huntwide.py` | Wide sweep — source wallets from ~100 markets, score each, tier by z. | +| `copyback.py` | Backtest: copy edge wallets' entries from a date, weighted, compounding. | +| `oos.py` | **Out-of-sample test** — select wallets on pre-period data, copy forward. The honesty gate. | +| `webhook_receiver.py` | Push-based live watcher: Alchemy on-chain webhook → enrich → Discord. | +| `archive/` | The six strategies that didn't work, kept for reference ([details](archive/README.md)). | -1. **Candidates** — pulls the 7d, 30d, and all-time leaderboards from - `data-api.polymarket.com/v1/leaderboard` and dedupes into a candidate pool - (default 150 wallets). -2. **True win rate** — over resolved bets in the last 90 days. This is the - subtle part: Polymarket only redeems *winning* shares, so `/closed-positions` - (redeemed or sold) is heavily **survivorship-biased toward winners** — - losing shares are worth $0 and sit unredeemed in `/positions` at - `curPrice 0` forever. A correct win rate has to **union both** endpoints - over the same window. Counting only `/closed-positions` (the naive approach) - reports ~90% for wallets whose real hit rate is ~50%. -3. **Frequency** — counts trades from `/activity` over the last 4 weeks; - *bets/week* is the number of **distinct markets** traded per week, so 50 - fills on one order don't count as 50 bets. -4. **Filter** — keeps wallets with win rate ≥ 75%, ≥ 2 bets/week, and ≥ 10 - resolved bets. +--- -> **Reality check:** with the unbiased metric, essentially **no** top wallet -> wins 75% of its bets — true rates cluster around **49% (a coin flip)**, max -> ~60%. The profitable ones make money through position sizing and entry -> prices, *not* hit rate. Treat a high win rate as a red flag for a bias bug, -> not a green light. See the backtest below. +## Quickstart -## Copy-trading (`copytrade.py`) - -Once you've found wallets worth following, `copytrade.py` watches them and -mirrors their trades onto your own account. - -- **Sizing** — each fresh entry stakes a fixed **% of your bankroll** (default 2%). -- **Mirror** — copies **entries and exits**: when they add, it adds - proportionally; when they sell part of a position, it sells the same - fraction of yours. -- **Price guard** — skips a copy if the market has moved **>5%** from their - fill price, so you don't chase. -- **No backfill** — only copies positions they open *after* you start - watching; positions they already held are tracked (so exits still mirror) - but never opened. -- **Per-position cap** — `max_position_usd` caps total cost in any one market. - Without it, proportional adds let a single position balloon toward your whole - exposure limit as the whale piles in (a backtest caught exactly this). -- **Discord alerts** — set `discord_webhook` in the config to get a ping on - every trade it would place (entries green, exits red). - -## Backtesting (`backtest.py`) - -Replays a watchlist's real trades over a recent window through the same copy -logic, filling at each wallet's actual historical price and marking outcomes -from how markets resolved. +Zero dependencies — Python 3 stdlib only (no `pip install`). macOS python.org +builds lack CA certs, so the code falls back to unverified SSL for these public +read-only APIs. ```bash -python3 backtest.py --days 7 +git clone https://github.com/jaxperro/winning-wallet-finder +cd winning-wallet-finder +cp config.example.json config.json # then edit (see Config below) + +python3 insider.py --scan 40 # score the top-40 leaderboard wallets +python3 insider.py --market # score a market's traders + detect rings +python3 insider.py --wallet 0xABC… # deep-profile one wallet +python3 huntwide.py # wide sweep → huntwide.csv (tiered by z) +python3 oos.py # the out-of-sample copy test +python3 smart_money.py # dashboard at http://localhost:8899 ``` -A 7-day backtest of four top wallets returned **−48% on deployed capital** — -not because the engine is broken, but because the wallets' true entry hit rate -is ~50% and flat-size copying pays the spread on every coin flip. Copying a -profitable wallet's *entries* does not reproduce its edge, which lives in -sizing and entry prices. Backtest before you fund anything. +### Config (`config.json`, gitignored — holds your secrets) -### ⚠️ Real money — read this - -It runs in **PAPER mode by default** and places nothing — it just logs what it -*would* do. Live trading requires **all** of: `"mode": "live"` in the config, -the `--live` flag, typing a confirmation phrase, and `py-clob-client` with -valid credentials. Hard caps (per-trade, daily spend, total exposure, open -positions, price bounds) apply in both modes. In live mode this places real -orders with real money on your account — you own the config and the outcomes. - -```bash -python3 copytrade.py --init # write config.example.json -cp config.example.json config.json -# ... edit config.json: add wallets to "watchlist", set bankroll & caps ... -python3 copytrade.py # PAPER mode — safe, logs only -python3 copytrade.py --once # single polling pass, then exit +```jsonc +{ + "discord_webhook": "https://discord.com/api/webhooks/…", // alerts + "alchemy_key": "…", // Polygon RPC for funding-cluster detection + "alchemy_signing_key": "…", // verifies inbound webhook POSTs (live watcher) + "watch": [ {"wallet": "0x…", "name": "Famecesgoal"}, … ] // wallets to track +} ``` -Going live (only after you trust the paper output): +--- -```bash -pip install py-clob-client -# set "mode": "live" and fill in the "live" block (private_key, funder_address) -python3 copytrade.py --live # prompts for a typed confirmation -``` +## Data sources -`config.json` and `copytrade_state.json` are gitignored so your credentials -and runtime state never get committed. +| Source | Used for | +|--------|----------| +| `data-api.polymarket.com` | positions, trades, leaderboard, activity, true win rate | +| `gamma-api.polymarket.com` | market metadata, resolution times, best bid/ask | +| `clob.polymarket.com` | order books, prices, liquidity-reward configs | +| `api.elections.kalshi.com` | Kalshi prices (cross-venue arb research) | +| Alchemy (Polygon) | on-chain USDC funding traces + the live trade webhook | -## Caveats +--- -- Candidates come from the leaderboards, so this surfaces *profitable* sharps. - A high-win-rate wallet that has never cracked any leaderboard window won't - appear — scanning every wallet on the platform isn't feasible via the - public API. -- Win rate is measured over resolved bets in the last 90 days, not all history. -- **Win rate ≠ EV.** Wallets with positive all-time leaderboard PnL routinely - show ~50% true win rates and even negative 90-day realized PnL. Following a - wallet profitably is about *how* it sizes and prices entries, not how often - it's right. The `realized_pnl` column is position-level over 90 days and is - **not** comparable to the all-time leaderboard figure. -- Very high-volume / market-maker wallets (thousands of fills) can't be cleanly - backtested via the public API — too many fills, no historical position - snapshot. +## Live watcher — get pinged on every trade -## What we learned (research log) +Push-based (no polling). The instant a tracked wallet's proxy transacts on +Polygon (~2–5s), Alchemy POSTs `webhook_receiver.py`, which enriches the trade +via the data-API and pings Discord: `🟢 Famecesgoal BUY Yes @ 0.34 ($120) — `. -The honest story of what the data showed, in order. Each finding killed an -assumption the previous step relied on. +1. **Discord webhook** → set `DISCORD_WEBHOOK` env (or `config.json`). +2. **Deploy** the receiver to an always-on host (Railway / Fly / a $5 VPS — *not* + Render free, it sleeps). `Procfile` included; binds to `$PORT`. +3. **Alchemy** → create an *Address Activity* webhook (Polygon mainnet), add the + `watch.json` addresses, point it at `https://your-host/alchemy`, and set the + signing key as `ALCHEMY_SIGNING_KEY`. -**1. Win rate was an illusion (survivorship bias).** -Polymarket only redeems *winning* shares; losing shares are worth $0 and sit -unredeemed in `/positions` at `curPrice 0` forever, never entering -`/closed-positions`. Measuring win rate over `/closed-positions` alone counts -almost only winners. Examples: NiNo999 read 90.6%, true rate **48.3%**; Boggs -read 73.4%, true **50.3%**. Fixed by unioning both endpoints over a window. -**Takeaway: a high reported win rate is a red flag for a bias bug, not a sharp.** +Keep the two wallet lists in sync: Alchemy's address list (what *triggers*) and +`watch.json` (what *names* the alert). -**2. With the honest metric, nobody wins 75%.** -Across 25 top wallets, true win rates clustered at a **median 49%** (coin flip), -max ~60%. Zero passed a 75% bar. Win rate is the wrong thing to rank on. +--- -**3. Win rate ≠ profit; raw PnL ≠ reliability.** -`surfandturf` won 54.8% and made millions; `Latina` (leaderboard #1 all-time) -won 43% and was **−$3.8M over 90 days**. And wallets with big total PnL often -got there on one or two outlier weeks (38% green weeks) — a lottery, not an -edge. The signal that finds reliable money is **weekly consistency**: % of weeks -green, profit factor, weekly Sharpe — measured per week, with enough weeks. +## The honest verdict -**4. Flat-size copy-trading is −EV.** -A 7-day backtest of four "top" wallets returned **−48%**. At ~50% entry hit -rates, mirroring entries at flat size just pays the spread on coin flips. A -profitable wallet's edge lives in *sizing and entry prices*, which copying -entries does not reproduce. (The backtest also exposed a missing per-position -cap — proportional adds could balloon one market to the whole exposure limit.) +- **Detection works.** z-score + timing + funding-cluster reliably surfaces + statistically anomalous wallets (the 60-Minutes use case). +- **Copying them is not proven.** In-sample a weighted, compounding copy + returned +545%; out-of-sample (select pre-May, copy forward) it was +168% — + but driven entirely by *one* longshot lottery wallet, with the strongest + signals contributing nothing. That's variance, not a durable, fundable edge. +- **No turnkey public-data edge survived** — copy-trading, win-rate ranking, LP + reward farming, binary/multi-outcome arb, and cross-venue arb all came back + efficient or illusory. See [`FINDINGS.md`](FINDINGS.md). -**5. A reliable edge looks real but rare — and skews young.** -Scanning 1,500 wallets over 120 days: 1,017 had history, 199 passed a -consistency screen, **77 were copyable** (hold-to-resolution ≥70%). But ~7.5% -of wallets passing by chance is exactly what randomness produces over 1,017 -coin-flippers — so some of the 77 are luck. Worse, a 240-day lookback showed -the "best" wallets are **young accounts** (surfandturf's oldest bet: 72 days; -joblessfinalboss: 79). New accounts that get hot rise to the leaderboard and -pass the screen; the ones that flamed out are delisted. **The most impressive -short-term performers are the least trustworthy.** - -**6. ROI and size are inversely related.** -Among the 77 copyable wallets, the highest-ROI ones bet small (dnte: 57% ROI on -$109K), while the biggest bettors scalp thin edges (elmcap2: $114M staked, -**0.4%** ROI). `surfandturf` was the lone anomaly — big *and* high-ROI ($27.9M -staked at 16%) — which makes it either the best find or the biggest variance -story. At 72 days old, we can't yet tell. - -### Where this leaves the strategy - -- **Rank on risk-adjusted consistency** (% green weeks × profit factor × - Sharpe), never win rate or raw PnL. -- **Require account longevity** — distrust anything under ~4–6 months. -- **Validate out-of-sample** (walk-forward: select on an early window, measure a - later one) before trusting any wallet list. This is the decisive open step. -- **Copying entries ≠ copying edge.** A working strategy likely needs to model - sizing/pricing, or pivot to a consensus signal (bet where many vetted wallets - agree) rather than blind mirroring. - -## Liquidity rewards (the market-making pivot) - -After copy-trading proved unreliable, the research pointed to **liquidity -rewards** as the lowest-risk edge. Polymarket pays makers a daily USDC pool for -resting limit orders near a market's midpoint — your share = your score ÷ total -score, where score rewards size and closeness to mid (quadratic: -`((max_spread − your_spread) / max_spread)²`). ~$200K/day is distributed across -~8,000 eligible markets (queryable via the CLOB `sampling-markets` endpoint; -each market exposes `rewards.rates[].rewards_daily_rate`, `min_size`, -`max_spread`). - -`lp_screener.py` ranks those markets by **risk-adjusted** yield — reward pool ÷ -order-book competition near mid (gross APR for a $1000 two-sided position), -penalized by 24h midpoint volatility (the adverse-selection proxy) and by -time-to-resolution (imminent = live = toxic). - -```bash -python3 lp_screener.py --min-rate 50 --capital 1000 # one-shot snapshot → lp_markets.csv -``` - -**It's a one-shot snapshot, not a daemon** — reward pools, books, and the -markets themselves churn daily, so re-run before each session. - -**What it found:** the sweet spot is **long-dated, low-volatility prop markets** -(World Cup player props, eliminations) — thin books, decent pools, vol ~0, days -to resolution. Live esports markets show astronomical gross APR but get -correctly de-ranked: that's where you get picked off. - -**Caveats that still gate real money:** headline APRs are a *snapshot* — thin -pools attract competitors and yield mean-reverts down; they're *gross*, ignoring -inventory losses when you get filled; and we have not yet confirmed near-empty -books actually pay the full pool. - -`lp_paper.py` is the decisive test — no money, no host, no key. It picks the -screener's top low-vol markets, simulates two-sided quotes against the **live** -order book, and tracks **net = rewards accrued − adverse-selection bleed**: - -```bash -python3 lp_paper.py --capital 1000 --markets 6 --poll 20 # runs until stopped -``` - -Fills are modeled when the midpoint crosses a resting quote (deliberately a bit -pessimistic on fill rate); rewards accrue by score-share of each pool. Net P&L, -per-market breakdown, and Discord summaries let it run for days to see whether -the edge survives mean-reversion. **Only if net stays clearly positive does a -real, funded, hosted bot make sense.** - -## Cross-venue arbitrage: Polymarket ↔ Kalshi (`xarb.py`) - -The last relative-value lane: buy YES on one venue + NO on the other for < $1 -(net of fees) = locked profit. Kalshi's public API (`api.elections.kalshi.com`) -exposes ~65k markets; `xarb.py` pulls both venues, matches the same event -(token overlap + same resolution month + **exact numeric match** on -thresholds/scores/dates so we compare the same *contract*, not just the same -event), and computes both arb directions with Kalshi's `0.07·P·(1−P)` taker fee. - -**Verdict: efficient — no retail arb.** On liquid, cleanly-matched, identical -contracts the two venues agree to **~1¢**, and locking both sides costs **>$1 -after fees.** Worked example (live): *Brazil vs Morocco — Both Teams To Score* -priced PM 0.46/0.47 vs Kalshi 0.47/0.48; every arb direction nets **negative**. -The large "edges" the scanner surfaces are artifacts: false matches (same event, -different sub-question), illiquid wide-spread markets (exact-score, props), or -stale snapshot timing. Matches the documented reality that real gaps last -~seconds and are taken by bots watching 10k+ markets. - -## Insider / sharp detection (`insider.py`) — the one real signal - -After the 2026 *60 Minutes* / WSJ coverage of Polymarket insider trading (a firm, -Bubblemaps, found 9 anonymous wallets that won ~$2.4M at a 98% rate on Iran-war -dates), `insider.py` replicates the *per-wallet* detection methodology on the -public data API: - -- **Improbability (the core signal):** each bet entered at price `p` has an - odds-implied win prob `p`. Winning far more than `Σp` is a z-score and - one-sided p-value — the rigorous "luck can't explain this." This is the - *correct* version of the edge metric the whole project was chasing: beating - the market's own pricing, not raw win-rate (biased) or PnL (variance). -- **Pre-resolution timing** — median hours before resolution they entered; share - of wins entered <24h out (advance-knowledge tell). -- **Fresh wallet** (`/traded` count) and **sizing** — the insider fingerprint. -- **Scoring is gated by improbability:** a wallet winning at/below its odds - scores 0 no matter how it's timed or sized (kills the sports-bettor confound, - where entering <24h before a game is normal, not suspicious). - -```bash -python3 insider.py --scan 40 # score top leaderboard wallets -python3 insider.py --market # score everyone who traded a market (Bubblemaps approach) -python3 insider.py --wallet 0xABC… # deep-profile one wallet -``` - -**Findings:** the all-time leaderboard holds *no* extreme insiders (max z≈2.3) — -those are high-volume sharps, not info-traders. Scanning a *market's* traders -surfaces the real signal: e.g. `arimnestos` at **z=4.0, p≈3e-5** over 2,205 bets -— a demonstrable edge. Distinguishing **sharp** (high z, normal timing) from -**insider** (high z + late entry + fresh wallet) is the timing/freshness combo. - -**Funding-cluster linking (the Bubblemaps step) — implemented.** With an -`alchemy_key` in `config.json`, the scanner pulls each wallet's USDC funding -history (`alchemy_getAssetTransfers`, full history, no block-range cap) and -links wallets that share a funder — i.e. likely the same operator. The catch -that makes or breaks this: a *shared exchange* (everyone withdraws from -Coinbase) is not a shared operator. So a candidate funder only counts as a link -if **its own outbound degree is small** (a personal hub sends to ≤15 wallets; an -exchange/bridge fans out to hundreds). Without that filter the method -false-flags everyone — our 10 "independent" watchlist wallets all shared 11 -infra funders and looked like one ring until the degree filter correctly -cleared them. Funded-from-a-major-exchange wallets can't be de-anonymized this -way — a known limit the pro firms hit too. - -**Why this matters:** the z-score over many bets is the first metric in this -project that identifies a *real, hard-to-fake* edge. A high-z wallet has beaten -the market's own prices repeatedly — a far better "who to study/follow" signal -than the leaderboard. (Caveat: *trading* on material nonpublic info is illegal — -detecting it is fine; blindly following a suspected insider is not a free pass.) - -### The bottom line across the whole project - -Six systematic, public-data edges tested — copy-trading, win-rate ranking, LP -reward farming, binary arb, multi-outcome logical arb, and cross-venue arb — -**all efficient or illusory.** Polymarket in 2026 does not hand a retail bot a -turnkey edge. Durable edge requires *speed/infra* (competing with pro arb bots), -*genuine private information* (a niche you know better than the market), or -*getting paid to provide a service* (liquidity, at modest adverse-selection- -dominated yields). The most valuable output here is knowing that before funding -any of it. +Use this to **find and watch** edge wallets and gather forward data — not as a +green light to bet size on copying them. diff --git a/config.example.json b/config.example.json index 88026433..470c45e7 100644 --- a/config.example.json +++ b/config.example.json @@ -1,24 +1,9 @@ { - "mode": "paper", - "poll_seconds": 12, - "discord_webhook": "", - "watchlist": [], - "bankroll_usd": 1000.0, - "bankroll_pct": 0.02, - "price_guard_pct": 0.05, - "risk": { - "max_trade_usd": 50.0, - "max_position_usd": 40.0, - "daily_spend_cap_usd": 250.0, - "max_total_exposure_usd": 500.0, - "max_open_positions": 20, - "min_price": 0.05, - "max_price": 0.95, - "min_order_usd": 5.0 - }, - "live": { - "private_key": "", - "funder_address": "", - "signature_type": 1 - } -} \ No newline at end of file + "discord_webhook": "https://discord.com/api/webhooks/XXXX/YYYY", + "alchemy_key": "your-alchemy-polygon-key", + "alchemy_signing_key": "whsec_from_alchemy_webhook_dashboard", + "watch": [ + {"wallet": "0x0b0f92507bbc340762d38eca43eba1e11ee37af1", "name": "Famecesgoal"}, + {"wallet": "0xe8c4d68aff65b38cac46987b9b65e01eb47d395d", "name": "JAMJAMJAM4"} + ] +} diff --git a/copyback.py b/copyback.py new file mode 100644 index 00000000..7346db93 --- /dev/null +++ b/copyback.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python3 +"""Backtest: copy the z-validated edge wallets' fresh entries from a start date, +weighted by edge strength, reinvesting 100% of profits (compounding). + +Honest model: + - We copy each wallet's FIRST buy of a market entered on/after the start date, + filling at THEIR entry price (optimistic — ignores the seconds-to-minutes + lag you'd really face; that's the next thing to test forward). + - Outcome = how that market resolved (curPrice 1 won / 0 lost), or current + price if still open (marked to market). + - Sizing: stake = wallet_weight * BET_K * current_bankroll, capped by cash. + Bankroll = cash + open-position cost, so profits compound into bet size. +""" + +import time +from collections import defaultdict + +import smart_money as sm + +START = "2026-05-30" +BET_K = 0.10 # aggressiveness: top-weight wallet risks ~weight*K of bankroll/bet +START_BANKROLL = 1000.0 + +# z-validated edge wallets (z-proportional weights — reward edge strength) +EDGE = [ + ("Famecesgoal", "0x0b0f92507bbc340762d38eca43eba1e11ee37af1", 9.6), + ("JAMJAMJAM4", "0xe8c4d68aff65b38cac46987b9b65e01eb47d395d", 5.7), + ("Domerina", "0xf4ad6aedf5475f2023912cd191eff5ec90ead00b", 5.3), + ("MyLastStand", "0x419f32f30814030554f5df3e9f508ad7394ce853", 4.2), +] + + +def _parse(d): + return time.mktime(time.strptime(d.replace("Z", "")[:19], + "%Y-%m-%dT%H:%M:%S" if "T" in d else "%Y-%m-%d")) if d else 0 + + +def entries_after(wallet, cutoff): + """Earliest BUY (price, ts) per asset, for buys on/after cutoff.""" + out = {} + off = 0 + while off < 4000: + page = sm.get_json("/activity", {"user": wallet, "type": "TRADE", + "limit": 500, "offset": off}) + if not page: + break + for t in page: + if t.get("side") == "BUY" and t.get("asset") and t.get("timestamp", 0) >= cutoff: + a = t["asset"] + if a not in out or t["timestamp"] < out[a][1]: + out[a] = (t.get("price", 0), t["timestamp"], t.get("title", "?")[:40]) + off += 500 + if len(page) < 500 or page[-1].get("timestamp", 0) < cutoff: + break + return out + + +def outcomes(wallet): + """asset -> (curPrice, endTs) from open + closed positions.""" + o = {} + for ep in ("/positions", "/closed-positions"): + off = 0 + while off < 2000: + params = {"user": wallet, "limit": 50, "offset": off} + if "closed" in ep: + params.update(sortBy="TIMESTAMP", sortDirection="DESC") + else: + params["sizeThreshold"] = 0.0 + page = sm.get_json(ep, params) + if not page: + break + for p in page: + if p.get("asset"): + o.setdefault(p["asset"], (p.get("curPrice", 0), _parse(p.get("endDate", "")))) + off += 50 + if len(page) < 50: + break + return o + + +def main(): + cutoff = _parse(START) + now = time.time() + tot_z = sum(z for _, _, z in EDGE) + weights = {w: z / tot_z for _, w, z in EDGE} + names = {w: n for n, w, z in EDGE} + + print(f"Copy-trade backtest from {START} · start ${START_BANKROLL:,.0f} · " + f"reinvest 100% · BET_K={BET_K}") + print("weights (z-proportional):") + for n, w, z in EDGE: + print(f" {n:14} z={z:>4} weight={weights[w]*100:>4.1f}%") + + # gather all copied bets + bets = [] + for n, w, z in EDGE: + ent = entries_after(w, cutoff) + outc = outcomes(w) + for a, (price, ts, title) in ent.items(): + if a not in outc or not (0 < price < 1): + continue + cur, end = outc[a] + resolved = end and end < now + bets.append({"w": w, "name": n, "ts": ts, "price": price, + "mark": cur, "res_ts": end or now, "resolved": bool(resolved), + "title": title}) + print(f"\ncopied bets entered since {START}: {len(bets)}") + + # discrete-event sim: enter at entry ts, free capital as bets resolve + events = [] + for i, b in enumerate(bets): + events.append((b["ts"], 0, i)) # 0=enter + if b["resolved"]: + events.append((b["res_ts"], 1, i)) # 1=resolve + events.sort() + + cash = START_BANKROLL + open_cost = 0.0 + pos = {} # i -> (shares, stake) + wins = losses = skipped = 0 + realized_pnl = 0.0 + by_wallet = defaultdict(float) + + for ts, kind, i in events: + b = bets[i] + if kind == 0: # ENTER + bankroll = cash + open_cost + stake = min(weights[b["w"]] * BET_K * bankroll, cash) + if stake < 1: + skipped += 1 + continue + shares = stake / b["price"] + pos[i] = (shares, stake) + cash -= stake + open_cost += stake + else: # RESOLVE + if i not in pos: + continue + shares, stake = pos.pop(i) + payout = shares * (1 if b["mark"] >= 0.5 else 0) + cash += payout + open_cost -= stake + realized_pnl += payout - stake + by_wallet[b["name"]] += payout - stake + if b["mark"] >= 0.5: + wins += 1 + else: + losses += 1 + + # mark any still-open copied bets to current price + open_val = 0.0 + for i, (shares, stake) in pos.items(): + mark = bets[i]["mark"] + open_val += shares * mark + by_wallet[bets[i]["name"]] += shares * mark - stake + + equity = cash + open_val + unreal = open_val - open_cost + print(f"\n{'='*64}") + print(f" resolved copied bets: {wins+losses} ({wins}W / {losses}L" + f"{f' · {wins/(wins+losses)*100:.0f}% hit' if wins+losses else ''})") + print(f" still open (marked to market): {len(pos)} · skipped (no cash): {skipped}") + print(f"\n REALIZED P&L (locked, resolved bets only): ${realized_pnl:+,.2f}" + f" -> {realized_pnl/START_BANKROLL*100:+.1f}%") + print(f" UNREALIZED (open positions marked to current price): ${unreal:+,.2f}" + f" -> {unreal/START_BANKROLL*100:+.1f}%") + print(f" ending equity: ${equity:,.2f} ({(equity/START_BANKROLL-1)*100:+.1f}% " + f"over {(now-cutoff)/86400:.0f}d) — but {open_val/equity*100:.0f}% of it is UNREALIZED") + print(f"{'='*64}") + print(" P&L by wallet:") + for n, _, _ in EDGE: + print(f" {n:14} {by_wallet[n]:+,.2f}") + + +if __name__ == "__main__": + main() diff --git a/huntwide.py b/huntwide.py new file mode 100644 index 00000000..2735cc0c --- /dev/null +++ b/huntwide.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +"""Wide insider hunt — source candidate wallets from many markets across the +volume spectrum, dedup, score each wallet's full record once, and tally the +insider-grade ones (z-score / p-value) with the markets they showed up in.""" + +import csv +import json +import ssl +import urllib.request +from collections import defaultdict +from concurrent.futures import ThreadPoolExecutor, as_completed + +import insider + +ctx = ssl._create_unverified_context() +GAMMA = "https://gamma-api.polymarket.com" +N_MARKETS = 100 # markets to source traders from +TOP_TRADERS = 20 # top traders (by notional) per market +MAX_WALLETS = 400 # cap on unique wallets to score (expensive step) + + +def get(u): + req = urllib.request.Request(u, headers={"User-Agent": "Mozilla/5.0"}) + return json.loads(urllib.request.urlopen(req, timeout=30, context=ctx).read()) + + +def source_markets(): + """A spread of active binary markets: high-volume AND mid/niche (where + insiders operate). Walk the volume ranking and sample across it.""" + mkts, offset = [], 0 + for _ in range(8): + try: + g = get(f"{GAMMA}/markets?limit=100&offset={offset}&active=true" + f"&closed=false&order=volume24hr&ascending=false") + except Exception: + break + if not g: + break + for m in g: + try: + oc = json.loads(m.get("outcomes", "[]")) + except Exception: + oc = [] + if [o.lower() for o in oc] == ["yes", "no"] and m.get("conditionId"): + mkts.append((m["conditionId"], m.get("question", "?")[:48])) + if len(g) < 100: + break + offset += 100 + # sample evenly across the ranking so we get a volume spread, not just whales + if len(mkts) > N_MARKETS: + step = len(mkts) / N_MARKETS + mkts = [mkts[int(i * step)] for i in range(N_MARKETS)] + return mkts + + +def main(): + key = insider.sm_load_key() + print(f"sourcing traders from up to {N_MARKETS} markets...", flush=True) + markets = source_markets() + print(f" {len(markets)} binary markets", flush=True) + + wallet_markets = defaultdict(set) + wallet_name = {} + + def grab(cm): + cond, title = cm + try: + cands, _ = insider.market_traders(cond, top=TOP_TRADERS) + return title, cands + except Exception: + return title, [] + + with ThreadPoolExecutor(max_workers=12) as ex: + for title, cands in ex.map(grab, markets): + for c in cands: + wallet_markets[c["wallet"]].add(title) + wallet_name.setdefault(c["wallet"], c["username"]) + print(f" {len(wallet_markets)} unique candidate wallets", flush=True) + + # prioritize wallets seen in more markets (more active → enough resolved bets) + wallets = sorted(wallet_markets, key=lambda w: len(wallet_markets[w]), reverse=True)[:MAX_WALLETS] + print(f"scoring {len(wallets)} wallets...", flush=True) + + rows, done = [], 0 + with ThreadPoolExecutor(max_workers=12) as ex: + futs = {ex.submit(insider.analyze, {"wallet": w, "username": wallet_name[w]}): w + for w in wallets} + for f in as_completed(futs): + done += 1 + try: + r = f.result() + except Exception: + r = None + if r: + r["markets"] = sorted(wallet_markets[r["wallet"]]) + rows.append(r) + if done % 50 == 0: + print(f" {done}/{len(wallets)}", flush=True) + + rows.sort(key=lambda r: r["z"], reverse=True) + with open("huntwide.csv", "w", newline="") as fp: + w = csv.writer(fp) + w.writerow(["z", "pval", "wins", "n", "pre24_pct", "trades", "username", "wallet", "markets"]) + for r in rows: + w.writerow([r["z"], r["pval"], r["wins"], r["n"], r["pre24_pct"], + r["trades"], r["username"], r["wallet"], " | ".join(r["markets"][:6])]) + + extra = [r for r in rows if r["z"] >= 5] + grade = [r for r in rows if 4 <= r["z"] < 5] + sharp = [r for r in rows if 3 <= r["z"] < 4] + print(f"\n{'='*80}") + print(f"SCORED {len(rows)} wallets with >=15 resolved bets") + print(f" extraordinary (z>=5): {len(extra)} insider-grade (z 4-5): {len(grade)} " + f"strong sharp (z 3-4): {len(sharp)}") + print(f"{'='*80}") + for label, group in (("EXTRAORDINARY (z>=5)", extra), ("INSIDER-GRADE (z 4-5)", grade)): + if not group: + continue + print(f"\n{label}:") + for r in group: + print(f" z={r['z']:>4} p={insider.fmt_p(r['pval']):>8} {r['wins']}/{r['n']} " + f"pre24={r['pre24_pct']:>3}% trades={r['trades']:>6} {r['username'][:20]}") + print(f" markets: {', '.join(r['markets'][:4])}") + print(f"\nfull table → huntwide.csv") + + +if __name__ == "__main__": + main() diff --git a/oos.py b/oos.py new file mode 100644 index 00000000..7a919406 --- /dev/null +++ b/oos.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +"""Out-of-sample test of the copy-the-edge-wallets strategy. + +SELECT wallets using only resolved bets in [Feb 1 - Apr 30] (z-score), with NO +knowledge of May-June. Then COPY those wallets' entries forward from May 30, +compounding. If the forward return is strong, the edge is real; if it collapses, +the in-sample +545% was selection theater. +""" + +import math +import time +from collections import defaultdict + +import smart_money as sm +from insider import resolved_bets, norm_sf +from copyback import entries_after, outcomes, _parse, BET_K, START_BANKROLL + +SEL_T0 = _parse("2026-02-01") # selection window start (resolution time) +SEL_T1 = _parse("2026-04-30") # selection window end — nothing after this is seen +TEST_START = _parse("2026-05-30") # copy entries on/after this +Z_PICK = 4.0 # insider-grade cut, judged AS OF Apr 30 +MIN_BETS = 15 + + +def score_pre(wallet): + """z-score over resolved bets in the selection window only.""" + bets = [b for b in resolved_bets(wallet, SEL_T0 - 10 * 86400) + if SEL_T0 <= b["res_t"] <= SEL_T1] + if len(bets) < MIN_BETS: + return None + 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 + z = (wins - exp) / math.sqrt(var) + return {"wallet": wallet, "n": len(bets), "wins": wins, "z": z, "pval": norm_sf(z)} + + +def candidate_pool(): + import csv + seen = {} + try: + for r in csv.DictReader(open("huntwide.csv")): + seen[r["wallet"]] = r["username"] + except FileNotFoundError: + pass + return seen + + +def main(): + from concurrent.futures import ThreadPoolExecutor, as_completed + pool = candidate_pool() + print(f"candidate pool: {len(pool)} wallets · scoring on [Feb1–Apr30] only...", flush=True) + + selected = [] + with ThreadPoolExecutor(max_workers=12) as ex: + futs = {ex.submit(score_pre, w): w for w in pool} + done = 0 + for f in as_completed(futs): + done += 1 + try: + r = f.result() + except Exception: + r = None + if r and r["z"] >= Z_PICK: + r["name"] = pool[r["wallet"]] + selected.append(r) + if done % 50 == 0: + print(f" {done}/{len(pool)}", flush=True) + + selected.sort(key=lambda r: r["z"], reverse=True) + print(f"\nINSIDER-GRADE AS OF APR 30 (z>={Z_PICK}): {len(selected)} wallets") + for r in selected: + print(f" {r['name'][:18]:18} z={r['z']:>4.1f} p={r['pval']:.1e} " + f"{r['wins']}/{r['n']} (pre-period)") + if not selected: + print("\nNo wallets were insider-grade as of Apr 30 — the edge wallets are " + "too new to have a pre-period track record. That itself is the answer.") + return + + # forward copy from May 30, z(pre)-weighted, compounding + tot_z = sum(r["z"] for r in selected) + weights = {r["wallet"]: r["z"] / tot_z for r in selected} + names = {r["wallet"]: r["name"] for r in selected} + print(f"\ncopying {len(selected)} wallets forward from 2026-05-30 " + f"(z-pre weighted, compounding)...", flush=True) + + bets = [] + now = time.time() + for r in selected: + w = r["wallet"] + ent = entries_after(w, TEST_START) + outc = outcomes(w) + for a, (price, ts, title) in ent.items(): + if a not in outc or not (0 < price < 1): + continue + cur, end = outc[a] + bets.append({"w": w, "ts": ts, "price": price, "mark": cur, + "res_ts": end or now, "resolved": bool(end and end < now)}) + print(f"forward copied bets: {len(bets)}", flush=True) + + events = [] + for i, b in enumerate(bets): + events.append((b["ts"], 0, i)) + if b["resolved"]: + events.append((b["res_ts"], 1, i)) + events.sort() + + cash = START_BANKROLL + open_cost = 0.0 + posn = {} + wins = losses = 0 + realized = 0.0 + bw = defaultdict(float) + for ts, kind, i in events: + b = bets[i] + if kind == 0: + bankroll = cash + open_cost + stake = min(weights[b["w"]] * BET_K * bankroll, cash) + if stake < 1: + continue + posn[i] = (stake / b["price"], stake) + cash -= stake + open_cost += stake + else: + if i not in posn: + continue + shares, stake = posn.pop(i) + payout = shares * (1 if b["mark"] >= 0.5 else 0) + cash += payout + open_cost -= stake + realized += payout - stake + bw[names[b["w"]]] += payout - stake + wins += b["mark"] >= 0.5 + losses += b["mark"] < 0.5 + open_val = sum(sh * bets[i]["mark"] for i, (sh, st) in posn.items()) + equity = cash + open_val + + print(f"\n{'='*64}") + print(f" OUT-OF-SAMPLE forward result (selection knew nothing past Apr 30)") + print(f" resolved: {wins+losses} ({wins}W/{losses}L" + f"{f' · {wins/(wins+losses)*100:.0f}%' if wins+losses else ''}) " + f"· open: {len(posn)}") + print(f" realized P&L: ${realized:+,.2f} ({realized/START_BANKROLL*100:+.1f}%)") + print(f" unrealized: ${open_val-open_cost:+,.2f}") + print(f" ── ending equity ${equity:,.2f} -> {(equity/START_BANKROLL-1)*100:+.1f}% " + f"on $1,000 over {(now-TEST_START)/86400:.0f}d") + print(f"{'='*64}") + for n in sorted(bw, key=lambda k: bw[k], reverse=True): + print(f" {n[:18]:18} {bw[n]:+,.2f}") + + +if __name__ == "__main__": + main()