diff --git a/arb/calc.py b/arb/calc.py index 93906d865..5694c8fde 100644 --- a/arb/calc.py +++ b/arb/calc.py @@ -22,6 +22,12 @@ all resolution regions and key off the guaranteed worst case, NOT (1 - cost). """ from datetime import date +# Kalshi market statuses that mean the market is no longer live/tradeable. +# A settled/finalized market still returns prices, but they are stale 1.0/0.0 +# sentinels (e.g. a month-end MINMON whose strike already resolved) and must +# not be treated as a real quote. +_DEAD_KALSHI = {"finalized", "settled", "closed", "determined"} + def direction(ticker): return "Below" if "MINMON" in (ticker or "").upper() else "Above" @@ -125,7 +131,8 @@ def evaluate(pair, kq, pq, settings, today=None): if not pq: row["status"] = "NO POLY" return row - if not kq: + if not kq or (kq.get("status") in _DEAD_KALSHI): + # Settled/finalized Kalshi leg: prices are stale sentinels, not a quote. row["status"] = "NO KALSHI" return row @@ -149,7 +156,10 @@ def evaluate(pair, kq, pq, settings, today=None): cands.append(("NO+YES", "NO", kq["no_ask"], k_nsz, "YES", pq["yes_ask"], pq.get("yes_ask_size"))) if not cands: - row["status"] = "NO DATA" + # Both venues returned live quotes, but the opposite-side asks needed to + # build a hedge aren't both offered (e.g. deep-OTM 'below' markets where + # each venue only quotes the cheap YES side). There's data, just no arb. + row["status"] = "NO ARB" return row best = None # (worst_pnl, ...) diff --git a/arb/kalshi.py b/arb/kalshi.py index 6bdfc16ab..015e61bc4 100644 --- a/arb/kalshi.py +++ b/arb/kalshi.py @@ -31,6 +31,14 @@ def _f(v): return None +def _ask(v): + """Like _f, but also drops the '1.0000' sentinel Kalshi returns when there + is no resting offer on that side. Real Kalshi asks are 1-99c, so a $1.00 + ask is never executable and must not be treated as a tradeable quote.""" + x = _f(v) + return x if (x is not None and x < 1.0) else None + + def parse_expiry(ticker): """KX...-26MAY31-7000 -> '2026-05-31', else None.""" m = re.search(r"-(\d{2})([A-Z]{3})(\d{2})-", ticker) @@ -49,9 +57,9 @@ def _quote(mkt): return { "ticker": t, "yes_bid": _f(mkt.get("yes_bid_dollars")), - "yes_ask": _f(mkt.get("yes_ask_dollars")), + "yes_ask": _ask(mkt.get("yes_ask_dollars")), "no_bid": _f(mkt.get("no_bid_dollars")), - "no_ask": _f(mkt.get("no_ask_dollars")), + "no_ask": _ask(mkt.get("no_ask_dollars")), "yes_ask_size": None, "no_ask_size": None, "open_interest": _f(mkt.get("open_interest_fp")) or 0.0, diff --git a/data/pairs.json b/data/pairs.json index f49a2b55f..a124a22f4 100644 --- a/data/pairs.json +++ b/data/pairs.json @@ -316,16 +316,16 @@ "asset": "ETH", "kalshi_ticker": "KXETHMINMON-ETH-26MAY31-75000", "kalshi_strike": 750.0, - "poly_slug": null, - "poly_strike": null + "poly_slug": "will-ethereum-dip-to-800-in-may-2026", + "poly_strike": 800.0 }, { "active": true, "asset": "ETH", "kalshi_ticker": "KXETHMINMON-ETH-26MAY31-50000", "kalshi_strike": 500.0, - "poly_slug": null, - "poly_strike": null + "poly_slug": "will-ethereum-dip-to-400-in-may-2026", + "poly_strike": 400.0 }, { "active": true, @@ -340,32 +340,32 @@ "asset": "ETH", "kalshi_ticker": "KXETHMINMON-ETH-26MAY31-200000", "kalshi_strike": 2000.0, - "poly_slug": "will-ethereum-dip-to-2000-in-may-2026", - "poly_strike": 2000.0 + "poly_slug": "will-ethereum-dip-to-1800-in-may-2026", + "poly_strike": 1800.0 }, { "active": true, "asset": "ETH", "kalshi_ticker": "KXETHMINMON-ETH-26MAY31-175000", "kalshi_strike": 1750.0, - "poly_slug": "will-ethereum-dip-to-2000-in-may-2026", - "poly_strike": 2000.0 + "poly_slug": "will-ethereum-dip-to-1800-in-may-2026", + "poly_strike": 1800.0 }, { "active": true, "asset": "ETH", "kalshi_ticker": "KXETHMINMON-ETH-26MAY31-150000", "kalshi_strike": 1500.0, - "poly_slug": null, - "poly_strike": null + "poly_slug": "will-ethereum-dip-to-1400-in-may-2026", + "poly_strike": 1400.0 }, { "active": true, "asset": "ETH", "kalshi_ticker": "KXETHMINMON-ETH-26MAY31-125000", "kalshi_strike": 1250.0, - "poly_slug": null, - "poly_strike": null + "poly_slug": "will-ethereum-dip-to-1200-in-may-2026", + "poly_strike": 1200.0 }, { "active": true, @@ -564,64 +564,64 @@ "asset": "XRP", "kalshi_ticker": "KXXRPMINMON-XRP-26MAY31-130", "kalshi_strike": 1.3, - "poly_slug": null, - "poly_strike": null + "poly_slug": "will-xrp-dip-to-1pt2-in-may-2026", + "poly_strike": 1.2 }, { "active": true, "asset": "XRP", "kalshi_ticker": "KXXRPMINMON-XRP-26MAY31-120", "kalshi_strike": 1.2, - "poly_slug": null, - "poly_strike": null + "poly_slug": "will-xrp-dip-to-1pt2-in-may-2026", + "poly_strike": 1.2 }, { "active": true, "asset": "XRP", "kalshi_ticker": "KXXRPMINMON-XRP-26MAY31-110", "kalshi_strike": 1.1, - "poly_slug": null, - "poly_strike": null + "poly_slug": "will-xrp-dip-to-1-in-may-2026", + "poly_strike": 1.0 }, { "active": true, "asset": "XRP", "kalshi_ticker": "KXXRPMINMON-XRP-26MAY31-100", "kalshi_strike": 1.0, - "poly_slug": null, - "poly_strike": null + "poly_slug": "will-xrp-dip-to-1-in-may-2026", + "poly_strike": 1.0 }, { "active": true, "asset": "XRP", "kalshi_ticker": "KXXRPMINMON-XRP-26MAY31-090", "kalshi_strike": 0.9, - "poly_slug": null, - "poly_strike": null + "poly_slug": "will-xrp-dip-to-0pt8-in-may-2026", + "poly_strike": 0.8 }, { "active": true, "asset": "XRP", "kalshi_ticker": "KXXRPMINMON-XRP-26MAY31-080", "kalshi_strike": 0.8, - "poly_slug": null, - "poly_strike": null + "poly_slug": "will-xrp-dip-to-0pt8-in-may-2026", + "poly_strike": 0.8 }, { "active": true, "asset": "XRP", "kalshi_ticker": "KXXRPMINMON-XRP-26MAY31-070", "kalshi_strike": 0.7, - "poly_slug": null, - "poly_strike": null + "poly_slug": "will-xrp-dip-to-0pt6-in-may-2026", + "poly_strike": 0.6 }, { "active": true, "asset": "XRP", "kalshi_ticker": "KXXRPMINMON-XRP-26MAY31-060", "kalshi_strike": 0.6, - "poly_slug": null, - "poly_strike": null + "poly_slug": "will-xrp-dip-to-0pt6-in-may-2026", + "poly_strike": 0.6 }, { "active": true, @@ -943,4 +943,4 @@ "poly_slug": "will-bnb-dip-to-500-in-may", "poly_strike": 500.0 } -] \ No newline at end of file +]