From e1ee7d13e7b3f3813f86851739cf068885835963 Mon Sep 17 00:00:00 2001 From: jaxperro Date: Sat, 13 Jun 2026 12:51:28 -0400 Subject: [PATCH] =?UTF-8?q?Add=20insider/sharp=20detector=20(insider.py)?= =?UTF-8?q?=20=E2=80=94=20the=20one=20real=20edge=20signal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replicates the Bubblemaps / 60 Minutes per-wallet insider methodology on the public data API: - Improbability z-score / p-value: wins vs the wins entry odds imply (beating the market's own pricing) — the rigorous edge metric the project was after, unlike biased win-rate or variance-driven PnL - Pre-resolution timing, fresh-wallet (/traded count), sizing signals - Scoring GATED by improbability so losing sports bettors (entering <24h before a game is normal) no longer false-flag - Modes: --scan leaderboard, --market (score a market's traders, the Bubblemaps approach), --wallet deep profile Findings: leaderboard has no extreme insiders (max z~2.3, high-vol sharps); scanning a market's traders surfaces real edges (e.g. arimnestos z=4.0 p~3e-5 over 2205 bets). Funding-cluster linking needs a Polygonscan/Alchemy key (public RPC getLogs capped at 10k blocks). README documents methodology + the project-wide conclusion. Co-Authored-By: Claude Opus 4.8 --- README.md | 41 +++++++++ insider.py | 262 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 303 insertions(+) create mode 100644 insider.py diff --git a/README.md b/README.md index 439f9caa..66ae93c4 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ live), and backtest the strategy. Zero dependencies — Python 3 stdlib only | `lp_screener.py` | Rank reward-eligible markets by risk-adjusted LP yield (pool ÷ competition, penalized by volatility). | | `lp_paper.py` | Paper liquidity-provision loop — simulate quoting on the live book, track **net = rewards − adverse selection**. | | `xarb.py` | Cross-venue scanner — match the same event on Polymarket vs Kalshi and flag price gaps. | +| `insider.py` | Insider/sharp detector — flag wallets winning *above their entry odds* (z-score / p-value), with timing, freshness, and sizing signals. | ## Run the dashboard @@ -269,6 +270,46 @@ 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. +The Bubblemaps **funding-cluster** step (linking an operator's wallets via +who-funded-whom) needs a Polygonscan/Alchemy key — public RPC caps `getLogs` at +10k blocks. + +**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 diff --git a/insider.py b/insider.py new file mode 100644 index 00000000..dfdd3953 --- /dev/null +++ b/insider.py @@ -0,0 +1,262 @@ +#!/usr/bin/env python3 +"""Polymarket insider-pattern detector. + +Replicates the per-wallet methodology behind the Bubblemaps / 60 Minutes work: +flag wallets whose results are too good to be luck and whose behavior fits the +insider fingerprint. All signals come from Polymarket's public data API. + +Signals per wallet (over resolved bets in a recent window): + 1. IMPROBABILITY — wins vs the wins their entry odds imply. Each bet entered + at price p has expected win prob p; observed wins far above Σp is the + "luck alone can't explain this" z-score (and one-sided p-value). + 2. PRE-RESOLUTION TIMING — how long before a market resolved they entered. + Entering minutes/hours before resolution is the classic advance-knowledge + tell. We report median lead time and the share of wins entered <24h out. + 3. FRESH WALLET — account age (first observed trade). New account + big + improbable wins is a strong flag. + 4. SIZING — average / max bet size. + +Composite suspicion 0-10. Funding-cluster linking (the Bubblemaps "who funded +whom" step) needs a Polygonscan/Alchemy key — see cluster_stub(). + + python3 insider.py --scan 40 # score top-40 leaderboard wallets + python3 insider.py --wallet 0xABC… # deep profile one wallet +""" + +import argparse +import math +import time +from concurrent.futures import ThreadPoolExecutor, as_completed + +import smart_money as sm + +WINDOW_DAYS = 120 +WEEK = 7 * 86400 + + +def _parse_end(end): + if not end: + return 0 + end = end.replace("Z", "") + for fmt in ("%Y-%m-%dT%H:%M:%S", "%Y-%m-%d"): + try: + return time.mktime(time.strptime(end, fmt)) + except ValueError: + continue + return 0 + + +def resolved_bets(wallet, cutoff, max_pages=40): + """Resolved bets with entry price, conditionId, resolution time, size.""" + now = time.time() + out = [] + for endpoint in ("/closed-positions", "/positions"): + off = 0 + while off < max_pages * 50: + params = {"user": wallet, "limit": 50, "offset": off} + if endpoint == "/closed-positions": + params.update(sortBy="TIMESTAMP", sortDirection="DESC") + else: + params["sizeThreshold"] = 0.0 + page = sm.get_json(endpoint, params) + if not page: + break + for p in page: + end = _parse_end(p.get("endDate")) + if endpoint == "/closed-positions": + ts = p.get("timestamp", 0) + if ts < cutoff: + continue + res_t = end or ts + else: + if not (cutoff <= end < now): + continue + res_t = end + out.append({ + "won": p.get("curPrice", 0) >= 0.5, + "p": max(0.001, min(0.999, p.get("avgPrice", 0) or 0)), + "cond": p.get("conditionId"), + "res_t": res_t, + "size": p.get("initialValue") or + (p.get("avgPrice", 0) * p.get("totalBought", 0)), + }) + off += 50 + if len(page) < 50: + break + if endpoint == "/closed-positions" and page[-1].get("timestamp", 0) < cutoff: + break + return out + + +def entry_times(wallet, max_pages=20): + """conditionId -> earliest BUY timestamp; plus account-age (first trade).""" + first_buy = {} + earliest = time.time() + off = 0 + while off < max_pages * 500: + page = sm.get_json("/activity", + {"user": wallet, "type": "TRADE", "limit": 500, "offset": off}) + if not page: + break + for t in page: + ts = t.get("timestamp", 0) + earliest = min(earliest, ts) if ts else earliest + if t.get("side") == "BUY" and t.get("conditionId"): + c = t["conditionId"] + if c not in first_buy or ts < first_buy[c]: + first_buy[c] = ts + off += 500 + if len(page) < 500: + break + return first_buy, earliest + + +def norm_sf(z): + """One-sided normal survival P(Z>z) — the 'probability this was luck'.""" + return 0.5 * math.erfc(z / math.sqrt(2)) + + +def analyze(cand): + wallet = cand["wallet"] + cutoff = time.time() - WINDOW_DAYS * 86400 + bets = resolved_bets(wallet, cutoff) + if len(bets) < 15: + return None + first_buy, _ = entry_times(wallet) + total_trades = (sm.get_json("/traded", {"user": wallet}) or {}).get("traded", 0) + + n = len(bets) + wins = sum(1 for b in bets if b["won"]) + exp = sum(b["p"] for b in bets) # expected wins by odds + var = sum(b["p"] * (1 - b["p"]) for b in bets) or 1e-9 + z = (wins - exp) / math.sqrt(var) # wins above odds-implied + pval = norm_sf(z) + + # pre-resolution timing on WINNING bets we can time + leads = [] + for b in bets: + if b["won"] and b["cond"] in first_buy and b["res_t"]: + lead_h = (b["res_t"] - first_buy[b["cond"]]) / 3600 + if lead_h >= 0: + leads.append(lead_h) + median_lead = sorted(leads)[len(leads) // 2] if leads else None + pre24 = (sum(1 for l in leads if l < 24) / len(leads)) if leads else 0 + + avg_size = sum(b["size"] for b in bets) / n + max_size = max(b["size"] for b in bets) + + # IMPROBABILITY GATES the score: a wallet winning at/below its odds cannot + # be an insider regardless of timing or size. Only when wins clearly exceed + # what the entry odds imply (z high) do the other signals amplify. + improb = 0.0 if z < 1 else min(7, (z - 1) * 2.0) # z=1→0, 2→2, 4.5→7 + score = improb + if improb > 0: # amplifiers, gated + score += 1.5 if total_trades < 20 else (0.7 if total_trades < 60 else 0) + score += 1.0 if avg_size > 5000 else (0.5 if avg_size > 1000 else 0) + score += 1.0 if pre24 > 0.5 else 0 # late entry + improbable + score = round(min(10, score), 1) + + return { + "username": cand["username"], "wallet": wallet, + "n": n, "wins": wins, "exp_wins": round(exp, 1), + "z": round(z, 1), "pval": pval, + "med_lead_h": round(median_lead, 1) if median_lead is not None else None, + "pre24_pct": round(pre24 * 100), + "trades": total_trades, "avg_size": round(avg_size), + "max_size": round(max_size), "score": score, + } + + +def market_traders(market, top=40): + """Wallets who traded a market, ranked by notional in it. Start from a + suspicious market and score everyone — the Bubblemaps approach.""" + cond = market + if not market.startswith("0x"): # treat as slug + g = sm.get_json("/markets" if False else None) or None + import urllib.request, json as _j, ssl as _ssl + c = _ssl._create_unverified_context() + req = urllib.request.Request( + f"https://gamma-api.polymarket.com/markets?slug={market}", + headers={"User-Agent": "Mozilla/5.0"}) + gm = _j.loads(urllib.request.urlopen(req, timeout=20, context=c).read()) + cond = gm[0]["conditionId"] if gm else market + notional = {} + names = {} + off = 0 + while off < 8000: + page = sm.get_json("/trades", {"market": cond, "limit": 500, "offset": off}) + if not page: + break + for t in page: + w = t.get("proxyWallet") + if not w: + continue + notional[w] = notional.get(w, 0) + t.get("usdcSize", 0) + names.setdefault(w, t.get("name") or w[:10] + "…") + off += 500 + if len(page) < 500: + break + ranked = sorted(notional, key=notional.get, reverse=True)[:top] + return [{"wallet": w, "username": names[w]} for w in ranked], cond + + +def cluster_stub(): + return ("funding-cluster linking (who-funded-whom, the Bubblemaps step) " + "needs a Polygonscan/Alchemy API key — public RPC caps getLogs at " + "10k blocks. Add a key to enable.") + + +def fmt_p(p): + if p <= 0: + return "<1e-12" + if p < 0.001: + return f"{p:.1e}" + return f"{p:.3f}" + + +def main(): + ap = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--scan", type=int, default=40, help="score top-N leaderboard wallets") + ap.add_argument("--wallet", help="deep-profile a single wallet") + ap.add_argument("--market", help="score the traders of a market (conditionId or slug)") + args = ap.parse_args() + + if args.wallet: + cands = [{"wallet": args.wallet, "username": args.wallet[:12] + "…"}] + elif args.market: + cands, cond = market_traders(args.market) + print(f"market {cond[:20]}… · scoring {len(cands)} top traders by notional\n") + else: + cands = sm.leaderboard_candidates(args.scan) + + rows = [] + with ThreadPoolExecutor(max_workers=10) as ex: + futs = {ex.submit(analyze, c): c for c in cands} + for f in as_completed(futs): + try: + r = f.result() + except Exception: + r = None + if r: + rows.append(r) + rows.sort(key=lambda r: r["score"], reverse=True) + + h = (f"{'susp':>5}{'z':>6}{'p(luck)':>9}{'rec':>11}{'medLead':>8}" + f"{'pre24':>6}{'trades':>7}{'avgSz':>8} trader") + print(h) + print("-" * len(h)) + for r in rows: + rec = f"{r['wins']}/{r['n']}(E{r['exp_wins']:.0f})" + lead = "n/a" if r["med_lead_h"] is None else f"{r['med_lead_h']:.0f}h" + print(f"{r['score']:>5.1f}{r['z']:>6.1f}{fmt_p(r['pval']):>9}{rec:>11}" + f"{lead:>8}{r['pre24_pct']:>5}%{r['trades']:>7}{'$'+format(r['avg_size'],','):>8}" + f" {r['username'][:22]}") + print("-" * len(h)) + print("susp gated by improbability: a wallet must win ABOVE its odds (z>1) to score at all.") + print("z=wins above odds-implied · p(luck)=prob it was chance · pre24=% wins entered <24h out") + print("\nNOTE:", cluster_stub()) + + +if __name__ == "__main__": + main()