mirror of
https://github.com/jaxperro/winning-wallet-finder.git
synced 2026-07-27 15:57:47 +00:00
34d02956d8
Moved the 8 tested-and-failed strategy tools into archive/ (copytrade, backtest, edge_research, lookback, table_77, lp_screener, lp_paper, xarb) with an archive/README explaining each. Root now holds the keepers: insider.py (made self-sufficient — dropped the copytrade load_json dependency) and smart_money.py (data foundation). New FINDINGS.md is the honest scorecard: six systematic public-data edges all efficient/illusory, the win-rate survivorship-bias finding, and the one real signal (z-score improbability + funding clustering). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
218 lines
7.9 KiB
Python
218 lines
7.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Polymarket <-> Kalshi cross-venue arbitrage scanner.
|
|
|
|
Pulls live prices from both venues, matches the same event across them, and
|
|
flags executable spreads: buy YES on one + NO on the other for < $1 (net of
|
|
fees) = locked profit regardless of resolution.
|
|
|
|
python3 xarb.py # one-shot scan -> xarb_hits.csv
|
|
python3 xarb.py --min-vol 5000 # only liquid markets
|
|
|
|
Matching is conservative (token overlap + same resolution month) to limit
|
|
false matches — a wrong match isn't an arb, it's two different bets
|
|
(resolution risk). Treat flagged hits as candidates to eyeball, not gospel.
|
|
"""
|
|
|
|
import argparse
|
|
import csv
|
|
import json
|
|
import re
|
|
import ssl
|
|
import urllib.request
|
|
from collections import defaultdict
|
|
|
|
ctx = ssl._create_unverified_context()
|
|
K = "https://api.elections.kalshi.com/trade-api/v2"
|
|
GAMMA = "https://gamma-api.polymarket.com"
|
|
|
|
STOP = {"will", "the", "a", "an", "to", "of", "in", "by", "be", "win", "wins",
|
|
"winner", "2026", "2025", "at", "on", "for", "and", "vs", "game", "match",
|
|
"who", "what", "during", "this", "next", "before", "after", "his", "her"}
|
|
|
|
|
|
def get(url):
|
|
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
|
|
with urllib.request.urlopen(req, timeout=30, context=ctx) as r:
|
|
return json.loads(r.read().decode())
|
|
|
|
|
|
def get_safe(url):
|
|
try:
|
|
return get(url)
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
def fnum(v, d=0.0):
|
|
try:
|
|
return float(v)
|
|
except (TypeError, ValueError):
|
|
return d
|
|
|
|
|
|
def norm(s):
|
|
s = re.sub(r"[^a-z0-9 ]", " ", (s or "").lower())
|
|
return {t for t in s.split() if t not in STOP and len(t) > 1}
|
|
|
|
|
|
def nums(s):
|
|
"""Numeric tokens (thresholds, scores, dates) that must match exactly for
|
|
two markets to be the *same* contract, not just the same event."""
|
|
return set(re.findall(r"\d+", (s or "").lower()))
|
|
|
|
|
|
def kalshi_fee(price):
|
|
"""Kalshi taker fee per $1 contract ≈ 0.07 * P * (1-P)."""
|
|
return 0.07 * price * (1 - price)
|
|
|
|
|
|
# ── data pulls ──────────────────────────────────────────────────────────────
|
|
|
|
def pull_kalshi(min_vol):
|
|
evs, cur = [], ""
|
|
for _ in range(60):
|
|
d = get(K + "/events?limit=200&status=open&with_nested_markets=true"
|
|
+ (f"&cursor={cur}" if cur else ""))
|
|
evs += d.get("events", [])
|
|
cur = d.get("cursor")
|
|
if not cur:
|
|
break
|
|
out = []
|
|
for e in evs:
|
|
for m in e.get("markets", []):
|
|
ya, na = fnum(m.get("yes_ask_dollars")), fnum(m.get("no_ask_dollars"))
|
|
if not (0 < ya < 1 and 0 < na < 1):
|
|
continue
|
|
vol = fnum(m.get("volume_24h_fp"))
|
|
if vol < min_vol:
|
|
continue
|
|
text = f"{e.get('title','')} {m.get('yes_sub_title','')}"
|
|
out.append({
|
|
"venue": "kalshi", "ticker": m["ticker"], "text": text,
|
|
"tokens": norm(text), "end": (m.get("close_time") or "")[:7],
|
|
"yes_ask": ya, "no_ask": na,
|
|
"yes_bid": fnum(m.get("yes_bid_dollars")),
|
|
"no_bid": fnum(m.get("no_bid_dollars")), "vol": vol,
|
|
})
|
|
return out
|
|
|
|
|
|
def pull_polymarket(min_vol):
|
|
out, offset = [], 0
|
|
for _ in range(200):
|
|
g = get_safe(f"{GAMMA}/markets?limit=100&offset={offset}&active=true"
|
|
f"&closed=false&order=volumeNum&ascending=false")
|
|
if not g:
|
|
break
|
|
for m in g:
|
|
try:
|
|
outcomes = json.loads(m.get("outcomes", "[]"))
|
|
except Exception:
|
|
outcomes = []
|
|
if [o.lower() for o in outcomes] != ["yes", "no"]:
|
|
continue
|
|
ask = fnum(m.get("bestAsk"))
|
|
bid = fnum(m.get("bestBid"))
|
|
if not (0 < ask < 1 and 0 < bid < 1):
|
|
continue
|
|
vol = fnum(m.get("volumeNum"))
|
|
if vol < min_vol:
|
|
continue
|
|
q = m.get("question", "")
|
|
out.append({
|
|
"venue": "poly", "text": q, "tokens": norm(q),
|
|
"end": (m.get("endDateIso") or m.get("endDate") or "")[:7],
|
|
"yes_ask": ask, "no_ask": round(1 - bid, 4), # NO ask ≈ 1 - YES bid
|
|
"vol": vol,
|
|
})
|
|
if len(g) < 100:
|
|
break
|
|
offset += 100
|
|
return out
|
|
|
|
|
|
# ── matching + arb ────────────────────────────────────────────────────────
|
|
|
|
def match_and_scan(poly, kalshi, min_sim):
|
|
# inverted index: token -> kalshi markets containing it
|
|
idx = defaultdict(list)
|
|
for k in kalshi:
|
|
for t in k["tokens"]:
|
|
idx[t].append(k)
|
|
hits = []
|
|
for p in poly:
|
|
if len(p["tokens"]) < 2:
|
|
continue
|
|
p_nums = nums(p["text"])
|
|
cand = {id(k): k for t in p["tokens"] for k in idx.get(t, [])}
|
|
best, best_sim = None, 0
|
|
for k in cand.values():
|
|
if p["end"] and k["end"] and p["end"] != k["end"]:
|
|
continue # different resolution month → skip
|
|
if nums(k["text"]) != p_nums:
|
|
continue # different thresholds/scores/dates → not the same contract
|
|
inter = len(p["tokens"] & k["tokens"])
|
|
sim = inter / len(p["tokens"] | k["tokens"])
|
|
if sim > best_sim:
|
|
best, best_sim = k, sim
|
|
if not best or best_sim < min_sim:
|
|
continue
|
|
# two arb directions
|
|
# A: poly YES + kalshi NO
|
|
a_cost = p["yes_ask"] + best["no_ask"]
|
|
a_edge = 1 - a_cost - kalshi_fee(best["no_ask"])
|
|
# B: kalshi YES + poly NO
|
|
b_cost = best["yes_ask"] + p["no_ask"]
|
|
b_edge = 1 - b_cost - kalshi_fee(best["yes_ask"])
|
|
if a_edge >= b_edge:
|
|
edge, leg = a_edge, "poly YES + kalshi NO"
|
|
else:
|
|
edge, leg = b_edge, "kalshi YES + poly NO"
|
|
hits.append({
|
|
"edge_c": round(edge * 100, 2), "sim": round(best_sim, 2),
|
|
"leg": leg, "poly": p["text"][:46], "kalshi": best["text"][:46],
|
|
"p_yes": p["yes_ask"], "p_no": p["no_ask"],
|
|
"k_yes": best["yes_ask"], "k_no": best["no_ask"],
|
|
"min_vol": round(min(p["vol"], best["vol"])),
|
|
})
|
|
hits.sort(key=lambda h: h["edge_c"], reverse=True)
|
|
return hits
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(description=__doc__,
|
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
ap.add_argument("--min-vol", type=float, default=2000)
|
|
ap.add_argument("--min-sim", type=float, default=0.5, help="token-overlap threshold")
|
|
ap.add_argument("--top", type=int, default=30)
|
|
args = ap.parse_args()
|
|
|
|
print("pulling Kalshi...", flush=True)
|
|
kalshi = pull_kalshi(args.min_vol)
|
|
print(f" {len(kalshi)} liquid Kalshi markets", flush=True)
|
|
print("pulling Polymarket...", flush=True)
|
|
poly = pull_polymarket(args.min_vol)
|
|
print(f" {len(poly)} liquid Polymarket markets", flush=True)
|
|
|
|
hits = match_and_scan(poly, kalshi, args.min_sim)
|
|
with open("xarb_hits.csv", "w", newline="") as f:
|
|
w = csv.DictWriter(f, fieldnames=list(hits[0].keys()) if hits else
|
|
["edge_c", "sim", "leg", "poly", "kalshi"])
|
|
w.writeheader()
|
|
w.writerows(hits)
|
|
|
|
arbs = [h for h in hits if h["edge_c"] > 0]
|
|
print(f"\nmatched pairs: {len(hits)} · positive-edge (after fees): {len(arbs)}")
|
|
print(f"\n{'edge¢':>6}{'sim':>5}{'minVol':>9} match (poly ↔ kalshi)")
|
|
print("-" * 92)
|
|
for h in hits[:args.top]:
|
|
print(f"{h['edge_c']:>6.1f}{h['sim']:>5.2f}{h['min_vol']:>9} "
|
|
f"{h['poly'][:34]:34} ↔ {h['kalshi'][:30]}")
|
|
print("-" * 92)
|
|
print(f"saved {len(hits)} matched pairs → xarb_hits.csv (edge>0 = arb after fees)")
|
|
return arbs, hits
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|