Rebrand to Winning Wallet Finder; commit validation scripts; dev-friendly docs
- 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 <noreply@anthropic.com>
This commit is contained in:
+53
-2
@@ -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`.
|
||||
|
||||
@@ -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 <slug> # 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) — <market>`.
|
||||
|
||||
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 <conditionId|slug># 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.
|
||||
|
||||
+8
-23
@@ -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
|
||||
}
|
||||
}
|
||||
"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"}
|
||||
]
|
||||
}
|
||||
|
||||
+176
@@ -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()
|
||||
+128
@@ -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()
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user