Refresh stale Polymarket slugs (BTC dip markets rediscovered)

Polymarket appends random hash suffixes when re-publishing markets
(e.g. will-bitcoin-dip-to-70k-in-may-2026 → ...-2026-438-356-919).
Our static pairs.json had the unsuffixed slugs from the original
spreadsheet snapshot, so every BTC 'below' pair silently fell through
to NO POLY despite both venues having liquid markets. User flagged
this looking at BTC <70k May.

- scripts/refresh_pairs.py: walks each asset's 'what-price-will-X-
  hit-in-may' multi-outcome event on Gamma, rebuilds the
  (asset, direction, strike) → slug map, rewrites data/pairs.json.
  Probes HYPE strikes individually since there's no container event
  for that asset. Has --dry-run.
- data/pairs.json: updated to current live slugs. Two BTC arbs that
  were previously hidden now surface (BTC <65k and <70k both ARB).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Casey Judice
2026-05-29 17:12:05 -04:00
parent c3cf16bde3
commit 8cb8d4a9e4
2 changed files with 130 additions and 7 deletions
+7 -7
View File
@@ -196,7 +196,7 @@
"asset": "BTC",
"kalshi_ticker": "KXBTCMINMON-BTC-26MAY31-7250000",
"kalshi_strike": 72500.0,
"poly_slug": "will-bitcoin-dip-to-70k-in-may-2026",
"poly_slug": "will-bitcoin-dip-to-70k-in-may-2026-438-356-919",
"poly_strike": 70000.0
},
{
@@ -204,7 +204,7 @@
"asset": "BTC",
"kalshi_ticker": "KXBTCMINMON-BTC-26MAY31-7000000",
"kalshi_strike": 70000.0,
"poly_slug": "will-bitcoin-dip-to-70k-in-may-2026",
"poly_slug": "will-bitcoin-dip-to-70k-in-may-2026-438-356-919",
"poly_strike": 70000.0
},
{
@@ -212,7 +212,7 @@
"asset": "BTC",
"kalshi_ticker": "KXBTCMINMON-BTC-26MAY31-6750000",
"kalshi_strike": 67500.0,
"poly_slug": "will-bitcoin-dip-to-65k-in-may-2026",
"poly_slug": "will-bitcoin-dip-to-65k-in-may-2026-183-857-425",
"poly_strike": 65000.0
},
{
@@ -220,7 +220,7 @@
"asset": "BTC",
"kalshi_ticker": "KXBTCMINMON-BTC-26MAY31-6500000",
"kalshi_strike": 65000.0,
"poly_slug": "will-bitcoin-dip-to-65k-in-may-2026",
"poly_slug": "will-bitcoin-dip-to-65k-in-may-2026-183-857-425",
"poly_strike": 65000.0
},
{
@@ -228,7 +228,7 @@
"asset": "BTC",
"kalshi_ticker": "KXBTCMINMON-BTC-26MAY31-6250000",
"kalshi_strike": 62500.0,
"poly_slug": "will-bitcoin-dip-to-60k-in-may-2026",
"poly_slug": "will-bitcoin-dip-to-60k-in-may-2026-973-269",
"poly_strike": 60000.0
},
{
@@ -236,7 +236,7 @@
"asset": "BTC",
"kalshi_ticker": "KXBTCMINMON-BTC-26MAY31-6000000",
"kalshi_strike": 60000.0,
"poly_slug": "will-bitcoin-dip-to-60k-in-may-2026",
"poly_slug": "will-bitcoin-dip-to-60k-in-may-2026-973-269",
"poly_strike": 60000.0
},
{
@@ -244,7 +244,7 @@
"asset": "BTC",
"kalshi_ticker": "KXBTCMINMON-BTC-26MAY31-5750000",
"kalshi_strike": 57500.0,
"poly_slug": "will-bitcoin-dip-to-55k-in-may-2026",
"poly_slug": "will-bitcoin-dip-to-55k-in-may-2026-941",
"poly_strike": 55000.0
},
{
+123
View File
@@ -0,0 +1,123 @@
#!/usr/bin/env python3
"""Refresh data/pairs.json Polymarket slugs against the live Gamma API.
Polymarket periodically appends random hash suffixes to market slugs (e.g.
will-bitcoin-dip-to-70k-in-may-2026 -> ...-2026-438-356-919). When that
happens the scanner silently falls through to NO POLY for the affected
pairs even though both venues have liquid markets.
Usage:
python3 scripts/refresh_pairs.py [--dry-run]
Walks each asset's monthly multi-outcome event on Gamma, builds a current
(asset, direction, strike) -> slug map, and rewrites pairs.json. Pairs whose
strike has no current Polymarket counterpart are left as-is (the scanner
will surface them as NO POLY so you can decide whether to remove them).
"""
import json
import re
import sys
import urllib.request
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
PAIRS = ROOT / "data" / "pairs.json"
ASSET_EVENTS = {
"BTC": "what-price-will-bitcoin-hit-in-may-2026",
"ETH": "what-price-will-ethereum-hit-in-may-2026",
"SOL": "what-price-will-solana-hit-in-may-2026",
"XRP": "what-price-will-xrp-hit-in-may-2026",
"DOGE": "what-price-will-dogecoin-hit-in-may-2026",
"BNB": "what-price-will-bnb-hit-in-may",
}
HYPE_STRIKES = [(d, s) for d in ("above", "below")
for s in (52, 48, 44, 38, 32, 28, 24)]
def gj(u):
rq = urllib.request.Request(u, headers={"User-Agent": "Mozilla/5.0"})
try:
with urllib.request.urlopen(rq, timeout=20) as r:
return json.loads(r.read().decode())
except Exception:
return None
def normalize(asset, raw):
raw = float(raw)
return raw * 1000 if asset == "BTC" else raw
def build_remap():
"""Returns {(asset, direction, strike): slug}."""
out = {}
for asset, ev_slug in ASSET_EVENTS.items():
e = gj("https://gamma-api.polymarket.com/events?slug=" + ev_slug)
if not e:
print(" WARN: event not found for %s (%s)" % (asset, ev_slug),
file=sys.stderr)
continue
for mk in e[0].get("markets", []):
if mk.get("closed"):
continue
sl = mk["slug"]
m = re.search(r"-(dip-to|reach)-(\d+(?:pt\d+)?)k?-in-may", sl)
if not m:
continue
direction = "below" if m.group(1) == "dip-to" else "above"
strike = normalize(asset, float(m.group(2).replace("pt", ".")))
out[(asset, direction, strike)] = sl
# HYPE doesn't have a single-event container; probe known strikes directly.
for direc, strike in HYPE_STRIKES:
kw = "reach" if direc == "above" else "dip-to"
for variant in ("will-hyperliquid-%s-%s-in-may" % (kw, strike),
"will-hyperliquid-%s-%s-in-may-2026" % (kw, strike)):
a = gj("https://gamma-api.polymarket.com/markets?slug=" + variant)
if a and not a[0].get("closed"):
out[("HYPE", direc, float(strike))] = variant
break
return out
def main():
dry = "--dry-run" in sys.argv
remap = build_remap()
print("Built remap with %d live (asset, direction, strike) entries." % len(remap))
pairs = json.loads(PAIRS.read_text())
changed = []
orphan = []
for p in pairs:
if not p.get("kalshi_ticker") or not p.get("poly_strike"):
continue
d = "below" if "MIN" in p["kalshi_ticker"] else "above"
key = (p["asset"], d, float(p["poly_strike"]))
new = remap.get(key)
if new is None:
if p.get("poly_slug"):
orphan.append((p["asset"], d, p["poly_strike"]))
continue
if new != p.get("poly_slug"):
changed.append((p["asset"], d, p["poly_strike"],
p["poly_slug"], new))
p["poly_slug"] = new
print(" %d pairs updated, %d pairs have no live Poly counterpart"
% (len(changed), len(orphan)))
for asset, d, s, old, new in changed[:8]:
print(" %s %s $%s %s%s"
% (asset, d, s, (old or "")[:40], new[:55]))
if len(changed) > 8:
print(" ...(%d more)" % (len(changed) - 8))
if dry:
print(" --dry-run; not writing.")
return
PAIRS.write_text(json.dumps(pairs, indent=2) + "\n")
print(" wrote %s (%d pairs total)" % (PAIRS, len(pairs)))
if __name__ == "__main__":
main()