"""r0724_pm_deribit_probe — Riconciliazione LIVE Polymarket <-> Deribit su digitali BTC/ETH. Domanda: quanto del "gap ~11pp" (arXiv 2606.19517, PM vs prob implicite Deribit) sopravvive OGGI a una riconciliazione onesta che aggiusta i mismatch di SPEC, e il trade hedged (lato cheap su PM + digitale opposta via VERTICAL di opzioni Deribit) ha senso a ~$600? Mismatch di spec quantificati (non assunti): 1. TEMPO: i PM "above $K on " risolvono sulla candela 1m Binance delle 12:00 ET (=16:00 UTC in estate); le daily Deribit scadono 08:00 UTC (settlement TWAP 30m dell'indice) -> 8h di varianza extra lato PM. dP_time = N(d2;T_pm) - N(d2;T_der). 2. FONTE: PM risolve su Binance BTC/USDT; Deribit su indice BTC-USD. Basis misurato live (= sconto USDT/USD): pochi bps di spot MA a orizzonte daily con IV ~15% vale svariati pp di probabilita' ATM. dP_src = N(d2; S*(1+basis)) - N(d2; S). 3. REPLICA: la digitale via vertical ha bound sub/super-replicanti: call-spread [K,K+w] <= 1{S>K} <= call-spread [K-w,K] (identico coi put OTM). Lato ITM i book Deribit sono MORTI -> si replica SEMPRE dal lato OTM (K>=S: call; K payoff 0 o 2): MC bivariato con sigma implicita. Vicino all'ATM e' grande -> il "lock" non esiste. Tutto live, API pubbliche tokenless, zero storage; se un endpoint e' vuoto lo dichiara. Riproducibile: uv run python scripts/research/r0724_pm_deribit_probe.py """ from __future__ import annotations import json import math import random import time import urllib.request from datetime import datetime, timezone UA = {"User-Agent": "Mozilla/5.0 (research probe)"} def get(url: str, timeout: int = 20, retries: int = 2): for k in range(retries + 1): try: req = urllib.request.Request(url, headers=UA) with urllib.request.urlopen(req, timeout=timeout) as r: return json.load(r) except Exception as e: # noqa: BLE001 if k == retries: print(f" [FETCH FAIL] {url[:100]} -> {e}") return None time.sleep(1.0) def ncdf(x: float) -> float: return 0.5 * (1.0 + math.erf(x / math.sqrt(2.0))) def bs_digital(S: float, K: float, sig: float, T_yr: float) -> float: if T_yr <= 0 or sig <= 0: return 1.0 if S > K else 0.0 d2 = (math.log(S / K) - 0.5 * sig * sig * T_yr) / (sig * math.sqrt(T_yr)) return ncdf(d2) # ----------------------------------------------------------------------------- Polymarket PM_EVENTS = [ ("BTC", "bitcoin-above-on-july-25-2026", "25JUL26"), ("ETH", "ethereum-above-on-july-25-2026", "25JUL26"), ("BTC", "bitcoin-above-on-july-26-2026", "26JUL26"), ("ETH", "ethereum-above-on-july-26-2026", "26JUL26"), ("BTC", "bitcoin-above-on-july-27-2026", "27JUL26"), ("ETH", "ethereum-above-on-july-27-2026", "27JUL26"), ] def parse_strike(question: str) -> float | None: import re m = re.search(r"\$([\d,]+(?:\.\d+)?)", question) return float(m.group(1).replace(",", "")) if m else None def fetch_pm(): out = [] for asset, slug, dexp in PM_EVENTS: ev = get(f"https://gamma-api.polymarket.com/events?slug={slug}") if not ev: print(f"[PM] evento {slug}: VUOTO/bloccato") continue ev = ev[0] for m in ev["markets"]: K = parse_strike(m["question"]) if K is None: continue desc = m.get("description", "") tok = json.loads(m.get("clobTokenIds", "[]")) out.append( dict( asset=asset, dexp=dexp, end=ev["endDate"], K=K, yes_bid=float(m["bestBid"]) if m.get("bestBid") else None, yes_ask=float(m["bestAsk"]) if m.get("bestAsk") else None, vol24=float(m.get("volume24hr") or 0), spec_ok=("Binance" in desc and "12:00 in the ET" in desc), yes_token=tok[0] if tok else None, ) ) return out def pm_book_depth(token_id: str): b = get(f"https://clob.polymarket.com/book?token_id={token_id}") if not b or not b.get("bids") or not b.get("asks"): return None bids = [(float(x["price"]), float(x["size"])) for x in b["bids"]] asks = [(float(x["price"]), float(x["size"])) for x in b["asks"]] bb, ba = max(p for p, _ in bids), min(p for p, _ in asks) mid = 0.5 * (bb + ba) d = {} for w in (0.01, 0.02): d[w] = ( sum(s * p for p, s in bids if p >= mid - w), sum(s * p for p, s in asks if p <= mid + w), ) return dict(bb=bb, ba=ba, depth=d) # ------------------------------------------------------------------------------- Deribit def fetch_deribit(ccy: str): bs = get( "https://www.deribit.com/api/v2/public/get_book_summary_by_currency" f"?currency={ccy}&kind=option" ) idx = get( f"https://www.deribit.com/api/v2/public/get_index_price?index_name={ccy.lower()}_usd" ) ins = get( f"https://www.deribit.com/api/v2/public/get_instruments?currency={ccy}" "&kind=option&expired=false" ) if not bs or not idx or not ins: return None books = {row["instrument_name"]: row for row in bs["result"]} expts = {} for i in ins["result"]: expts[i["instrument_name"].split("-")[1]] = i["expiration_timestamp"] / 1000.0 return dict(books=books, index=idx["result"]["index_price"], expts=expts) def _q(books, ccy, dexp, strike, typ, index): """(bid_usd, ask_usd, mark_usd) di uno strumento; None se assente.""" n = f"{ccy}-{dexp}-{int(strike)}-{typ}" r = books.get(n) if not r: return None return dict( n=n, bid=(r["bid_price"] or 0.0) * index, ask=(r["ask_price"] * index) if r["ask_price"] else None, mark=(r.get("mark_price") or 0.0) * index, ) def deribit_digital(books, ccy, dexp, K, index): """Stima onesta di P(S_der > K) via vertical OTM-side. Ritorna: mark centrato, banda [sub_mark, super_mark], eseguibili (exec_buy = costo per COMPRARE la sub-replica dell'above-digitale ai prezzi reali; exec_sell = incasso per VENDERLA), gambe usate. """ typ = "C" if K >= index else "P" strikes = sorted( float(n.split("-")[2]) for n in books if n.split("-")[1] == dexp and n.endswith(f"-{typ}") ) if K not in strikes: return None # tutti gli strike PM qui coincidono con strike listati i = strikes.index(K) if i == 0 or i == len(strikes) - 1: return None Km, Kp = strikes[i - 1], strikes[i + 1] qm, q0, qp = ( _q(books, ccy, dexp, Km, typ, index), _q(books, ccy, dexp, K, typ, index), _q(books, ccy, dexp, Kp, typ, index), ) if not (qm and q0 and qp): return None if typ == "C": centered = (qm["mark"] - qp["mark"]) / (Kp - Km) sub = (q0["mark"] - qp["mark"]) / (Kp - K) # <= P sup = (qm["mark"] - q0["mark"]) / (K - Km) # >= P # comprare sub-replica: buy C(K), sell C(Kp) exec_buy = ( (q0["ask"] - qp["bid"]) / (Kp - K) if q0["ask"] is not None else None ) exec_sell = ( (q0["bid"] - qp["ask"]) / (Kp - K) if qp["ask"] is not None else None ) legs = (q0, qp) else: # P_above = 1 - put_spread/w ; [K,Kp] put spread super-replica il below centered = 1.0 - (qp["mark"] - qm["mark"]) / (Kp - Km) sub = 1.0 - (qp["mark"] - q0["mark"]) / (Kp - K) # <= P_above sup = 1.0 - (q0["mark"] - qm["mark"]) / (K - Km) # >= P_above # comprare (sinteticamente) l'above = VENDERE il put spread [K,Kp]: # incasso bid(Kp)-ask(K); prezzo implicito pagato = 1 - incasso/w exec_buy = ( 1.0 - (qp["bid"] - q0["ask"]) / (Kp - K) if q0["ask"] is not None else None ) exec_sell = ( 1.0 - (qp["ask"] - q0["bid"]) / (Kp - K) if qp["ask"] is not None else None ) legs = (q0, qp) return dict( typ=typ, Km=Km, Kp=Kp, w=Kp - K, centered=centered, sub=sub, sup=sup, exec_buy=exec_buy, exec_sell=exec_sell, legs=legs, ) IV_CACHE: dict = {} def deribit_iv(ccy: str, dexp: str, name: str) -> float | None: if name in IV_CACHE: return IV_CACHE[name] t = get(f"https://www.deribit.com/api/v2/public/ticker?instrument_name={name}") iv = None if t: iv = t["result"].get("mark_iv") iv = iv / 100.0 if iv else None IV_CACHE[name] = iv return iv def p_cross(S, K, sig, T1_yr, T2_yr, n=200_000, seed=7): """MC: P(lati opposti di K a T1 e T2) e P(sopra a T2 ma sotto a T1) ecc.""" rng = random.Random(seed) lo_hi = hi_lo = 0 lnK = math.log(K / S) s1 = sig * math.sqrt(T1_yr) s2x = sig * math.sqrt(max(T2_yr - T1_yr, 1e-12)) for _ in range(n): x1 = -0.5 * s1 * s1 + s1 * rng.gauss(0, 1) x2 = x1 - 0.5 * s2x * s2x + s2x * rng.gauss(0, 1) a, b = x1 > lnK, x2 > lnK if not a and b: lo_hi += 1 elif a and not b: hi_lo += 1 return lo_hi / n, hi_lo / n # ----------------------------------------------------------------------------------- run def main(): now = datetime.now(timezone.utc) print(f"=== PROBE PM<->DERIBIT {now.isoformat(timespec='seconds')} ===\n") pm = fetch_pm() nbad = sum(1 for m in pm if not m["spec_ok"]) print(f"[PM] {len(pm)} mercati above/below; spec Binance/12:00ET non confermata su {nbad}") der = {c: fetch_deribit(c) for c in ("BTC", "ETH")} for c, d in der.items(): if d: print(f"[Deribit] {c} index={d['index']:.2f}") def binance_spot(sym): for host in ("https://api.binance.com", "https://data-api.binance.vision"): r = get(f"{host}/api/v3/ticker/price?symbol={sym}", timeout=10, retries=0) if r and "price" in r: return float(r["price"]) return None binance = {"BTC": binance_spot("BTCUSDT"), "ETH": binance_spot("ETHUSDT")} kr = get("https://api.kraken.com/0/public/Ticker?pair=USDTZUSD", timeout=10, retries=1) usdtusd = None try: usdtusd = float(list(kr["result"].values())[0]["c"][0]) except Exception: # noqa: BLE001 pass print(f"[Binance] BTCUSDT={binance['BTC']} ETHUSDT={binance['ETH']} | USDT/USD={usdtusd}") basis = {} for c in ("BTC", "ETH"): basis[c] = (binance[c] / der[c]["index"] - 1.0) if (der[c] and binance[c]) else 0.0 print(f" basis {c} BinanceUSDT vs indice Deribit: {basis[c]*1e4:+.1f} bps") rows = [] for m in pm: c, d = m["asset"], der[m["asset"]] if not d or m["dexp"] not in d["expts"]: continue S = d["index"] dig = deribit_digital(d["books"], c, m["dexp"], m["K"], S) if dig is None: continue T_der = max(d["expts"][m["dexp"]] - now.timestamp(), 0) / (365.25 * 86400) t_pm = datetime.fromisoformat(m["end"].replace("Z", "+00:00")).timestamp() T_pm = max(t_pm - now.timestamp(), 0) / (365.25 * 86400) near = abs(math.log(m["K"] / S)) < 0.09 sig = deribit_iv(c, m["dexp"], dig["legs"][0]["n"]) if near else None if sig is None: sig = 0.45 dP_time = bs_digital(S, m["K"], sig, T_pm) - bs_digital(S, m["K"], sig, T_der) dP_src = bs_digital(S * (1 + basis[c]), m["K"], sig, T_pm) - bs_digital( S, m["K"], sig, T_pm ) pm_mid = ( 0.5 * (m["yes_bid"] + m["yes_ask"]) if (m["yes_bid"] is not None and m["yes_ask"] is not None) else (m["yes_ask"] or m["yes_bid"]) ) raw = pm_mid - dig["centered"] if pm_mid is not None else None resid = raw - dP_time - dP_src if raw is not None else None depth = pm_book_depth(m["yes_token"]) if (m["yes_token"] and near) else None rows.append( dict( m=m, dig=dig, sig=sig, T_der=T_der, T_pm=T_pm, dP_time=dP_time, dP_src=dP_src, pm_mid=pm_mid, raw=raw, resid=resid, depth=depth, S=S, near=near, ) ) print("\n=== RICONCILIAZIONE (prob %, gap pp; digitale = vertical OTM-side) ===") hdr = ( f"{'mkt':<22}{'PM b/a':>12}{'Der mark[sub,sup]':>20}{'exec b/s':>13}" f"{'raw':>7}{'dT':>6}{'dSrc':>6}{'resid':>7}{'iv%':>5}" ) print(hdr) print("-" * len(hdr)) for r in rows: m, g = r["m"], r["dig"] name = f"{m['asset']} >{int(m['K'])} {m['dexp'][:5]}" pmba = ( f"{(m['yes_bid'] or 0)*100:.1f}/{(m['yes_ask'] or 0)*100:.1f}" if (m["yes_bid"] is not None or m["yes_ask"] is not None) else "n/a" ) ex = ( f"{g['exec_buy']*100:.0f}/{g['exec_sell']*100:.0f}" if (g["exec_buy"] is not None and g["exec_sell"] is not None) else "n/q" ) print( f"{name:<22}{pmba:>12}" f"{g['centered']*100:>8.1f}[{g['sub']*100:.0f},{g['sup']*100:.0f}]".ljust(42) + f"{ex:>13}" f"{(r['raw'] or 0)*100:>+7.1f}{r['dP_time']*100:>+6.1f}" f"{r['dP_src']*100:>+6.1f}{(r['resid'] or 0)*100:>+7.1f}" f"{r['sig']*100:>5.0f}" ) print( "\n Der mark = spread centrato sui mark, [sub,sup] = bound di replica;" " exec b/s = comprare/vendere la sub-replica ai bid/ask REALI." "\n raw = PM_mid - Der_mark; resid = raw - dT - dSrc" " (>0: PM sovraprezza il lato above vs Deribit spec-adjusted)." ) # sistematicita' per bucket di moneyness print("\n=== RESIDUO PER BUCKET (solo quote PM a doppio lato) ===") buckets = {"ITM(KS+1.5%)": []} for r in rows: if r["resid"] is None or r["m"]["yes_bid"] is None or r["m"]["yes_ask"] is None: continue lm = math.log(r["m"]["K"] / r["S"]) b = "ITM(KS+1.5%)" if lm > 0.015 else "ATM(|1.5%|)") buckets[b].append(r["resid"]) for b, v in buckets.items(): if v: pos = sum(1 for x in v if x > 0) print( f" {b:<15} n={len(v):>2} resid medio {sum(v)/len(v)*100:+.1f}pp" f" mediana {sorted(v)[len(v)//2]*100:+.1f}pp >0: {pos}/{len(v)}" ) print("\n=== DEPTH CLOB PM (vicino allo spot) ===") for r in rows: if r["depth"]: m, dp = r["m"], r["depth"] d1, d2 = dp["depth"][0.01], dp["depth"][0.02] print( f" {m['asset']} >{int(m['K'])} {m['dexp']}: book {dp['bb']:.3f}/{dp['ba']:.3f}" f" depth±1c ${d1[0]:,.0f}/${d1[1]:,.0f} ±2c ${d2[0]:,.0f}/${d2[1]:,.0f}" f" vol24h ${m['vol24']:,.0f}" ) # ------------------------------------------------------------------ trade hedged demo print("\n=== TRADE HEDGED a size minima (0.1 BTC / 1 ETH) — numeri VERI ===") min_amt = {"BTC": 0.1, "ETH": 1.0} fee_rate, dlv_rate = 0.0003, 0.00015 for r in rows: m, g = r["m"], r["dig"] if r["resid"] is None or not r["near"] or abs(r["resid"]) < 0.01: continue if g["exec_buy"] is None or g["exec_sell"] is None: continue c, S = m["asset"], r["S"] amt = min_amt[c] W = g["w"] * amt # notional digitale $ if r["resid"] > 0: # PM ricco sul lato above: buy PM NO al (1-yes_bid) + buy digitale-above Deribit pm_px = 1 - (m["yes_bid"] or 0) der_px = g["exec_buy"] side = "BUY PM NO + LONG vertical OTM (sub-replica above)" else: pm_px = m["yes_ask"] or 1 der_px = 1 - g["exec_sell"] side = "BUY PM YES + SHORT vertical OTM (sub-replica below)" cost = (pm_px + der_px) * W lock = W - cost fees = 0.0 for leg in g["legs"]: fees += min(fee_rate * amt * S, 0.125 * leg["mark"] * amt) dlv = min(dlv_rate * amt * S, 0.125 * max(le["mark"] for le in g["legs"]) * amt) otm_frac = abs(g["Kp"] - S) / S mark_short = g["legs"][1]["mark"] / S im = (max(0.15 - otm_frac, 0.10) + mark_short) * amt pc = p_cross(S, m["K"], r["sig"], r["T_der"], r["T_pm"]) capital = cost + im * S net = lock - fees - dlv days = r["T_pm"] * 365.25 print(f"\n {m['asset']} >{int(m['K'])} {m['dexp']} resid {r['resid']*100:+.1f}pp -> {side}") print( f" gambe Deribit {g['legs'][0]['n']}/{g['legs'][1]['n']} amount {amt} {c}" f" -> payout digitale ${W:,.0f}" ) print( f" costo PM ${pm_px*W:,.2f} + Deribit ${der_px*W:,.2f} = ${cost:,.2f};" f" lock lordo ${lock:,.2f}; fee entry ${fees:.2f} + delivery ${dlv:.2f}" f" -> netto ${net:,.2f}" ) print( f" margine leg corto (SM, no netting) ~${im*S:,.0f};" f" capitale impegnato ~${capital:,.0f};" f" ritorno se lock regge: {net/capital*100:.2f}% in {days:.1f}g" f" (~{net/capital*365.25/days*100:.0f}%/anno)" ) print( f" RISCHIO CROSS 08->16 UTC (MC, iv {r['sig']*100:.0f}%):" f" P(sotto@08,sopra@16)={pc[0]*100:.1f}% P(sopra@08,sotto@16)={pc[1]*100:.1f}%" f" -> P(gambe in conflitto)={sum(pc)*100:.1f}%" ) print("\n[NOTE] Deribit settle = TWAP 30m pre-08:00 UTC vs PM candela 1m 16:00 UTC;") print("la sub-replica paga <1 nella rampa [K,K+w] -> il 'lock' e' un bound inferiore") print("solo FUORI dalla rampa; ATM con w=500 (BTC) la rampa e' ~1-1.5 sigma daily.") if __name__ == "__main__": main()