mirror of
https://github.com/jaxperro/winning-wallet-finder.git
synced 2026-08-07 21:27:47 +00:00
Add cross-venue scanner (xarb.py); verdict: PM<->Kalshi is efficient
xarb.py pulls Polymarket (Gamma) + Kalshi (elections API, ~65k markets), matches the same contract (token overlap + same resolution month + exact numeric match on thresholds/scores/dates), and computes both arb directions with Kalshi's 0.07*P*(1-P) taker fee. Verified verdict: no retail cross-venue arb. On liquid, identical, cleanly- matched contracts the venues agree to ~1c and locking both sides costs >$1 after fees (worked example: Brazil-Morocco BTTS, PM 0.46/0.47 vs Kalshi 0.47/0.48 -> every direction negative). The big apparent edges are false matches, illiquid wide-spread markets, or stale snapshot timing. README now records the full project conclusion: six systematic public-data edges tested, all efficient/illusory. Durable edge needs speed/infra, private information, or liquidity provision -- not a turnkey public-data scanner. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -15,3 +15,6 @@ copyable_77.csv
|
||||
lp_markets.csv
|
||||
follow_10.json
|
||||
lp_paper_state.json
|
||||
|
||||
# cross-venue scanner output
|
||||
xarb_hits.csv
|
||||
|
||||
@@ -21,6 +21,7 @@ live), and backtest the strategy. Zero dependencies — Python 3 stdlib only
|
||||
| `backtest.py` | Replay a watchlist over a recent window and mark outcomes. |
|
||||
| `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. |
|
||||
|
||||
## Run the dashboard
|
||||
|
||||
@@ -249,3 +250,32 @@ 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.
|
||||
|
||||
### 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.
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user