Add insider/sharp detector (insider.py) — the one real edge signal

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 <conditionId|slug> (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 <noreply@anthropic.com>
This commit is contained in:
jaxperro
2026-06-13 12:51:28 -04:00
parent 7536379023
commit e1ee7d13e7
2 changed files with 303 additions and 0 deletions
+41
View File
@@ -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 <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.
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
+262
View File
@@ -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()