diff --git a/.gitignore b/.gitignore index ac534b6..41f6758 100644 --- a/.gitignore +++ b/.gitignore @@ -73,3 +73,4 @@ data/live/ # dati esterni di ricerca (on-chain CoinMetrics community, F&G) — non certificati, non in git data/external/ +data/options_daily/ diff --git a/docs/diary/2026-07-24-daily-options-0dte.md b/docs/diary/2026-07-24-daily-options-0dte.md new file mode 100644 index 0000000..dddb4b2 --- /dev/null +++ b/docs/diary/2026-07-24-daily-options-0dte.md @@ -0,0 +1,76 @@ +# 2026-07-24 — Dossier 0DTE: economia LIVE delle daily options Deribit (BTC/ETH) + +**Goal.** Quantificare stanotte, da quote pubbliche LIVE Deribit, l'economia del premio a +scadenza DAILY (0-1DTE) — il dossier 0DTE parcheggiato. VRP01 resta deploy-bloccato dalla regola +"niente short-vol da modello"; le daily (365 expiry/anno) accumulerebbero una serie di premi +REALI 7x più in fretta delle weekly. Domanda: il tenor daily è quotato/eseguibile/conveniente, +o muore di fee (lezione Albimarini)? + +**Misura (3 snapshot ~22:35 UTC, ven 24 lug):** catena 0DTE (settle 25JUL 08:00, DTE 9.4h), +daily "fresca" (26JUL, DTE 33.4h), weekly (31JUL, DTE 153h). Fee verificate: taker=maker +0.0003 base/contratto, **cap 12.5% del premio**, daily ESENTI da delivery fee. Min size: +BTC 0.1 contratto, ETH 1.0. Spot 64.1k / 1859. + +## Numeri chiave (put credit spread -0.28d/-0.10d, prezzi al tocco, fee taker 2 gambe) + +| tenor | credito lordo | cross-spread (vs mid) | fee (% credito) | NETTO (% width) | %width/giorno | +|---|---|---|---|---|---| +| BTC 9.4h | $38.5 | 25% | 25.0% | 5.8% | — | +| BTC 33h | $96.2 (0.1 lot: $9.6) | 12% | 20.8% | 7.6% | 5.5%/g | +| BTC weekly | $224.6 | 16% | 17.1% | 9.3% | 1.5%/g | +| ETH 9.4h | $1.12 | 29% | 29.2% | 3.2% | — | +| ETH 33h | $3.53 | 14% | 23.0% | 10.9% | 7.8%/g | +| ETH weekly | $13.39 | 8% | **8.3%** | 12.3% | 1.9%/g | + +- **Il cap 12.5% è quasi sempre BINDING a tenor daily** (premi < 0.0024 base) → fee ≈ 10-12.5% + del premio PER GAMBA, sempre. A weekly la fissa 0.0003 = 3-5% del premio. Il drag fee del + daily è **2-3x** quello weekly per unità di premio: la legge di scala Albimarini vista live + (premio ~ √T, fee ~ costante). +- **Haircut totale mid→netto**: daily 33h ≈ 30-35% del credito mid; 0DTE 9.4h ≈ 45-50%; + weekly ETH ≈ 16%. Richezza di pareggio a tenor daily ≈ **IV/RV ~1.4x**; a weekly ~1.1x. +- **IV vs RV stanotte**: ATM daily mark_iv BTC **12.8%** / ETH **21.5%** ann. contro RV20 + 29.6% / 39.8% → **IV_daily/RV20 = 0.43x / 0.54x: il premio daily era SOTTO la realized** + (VRP negativo al front). Weekly: 33.9% / 44.9% = 1.13-1.14x RV (il VRP vive lì, coerente + con VRP01). DVOL-30d 37.0 / 50.2. Caveat onesto: 1 snapshot, finestra weekend (ven→dom) = + IV bassa in parte razionale; NON generalizzabile — esattamente il perché della cattura. +- **EV check ETH 33h**: credito netto $2.72 < fair-value mid ~$4.1 e < E[payout] alla STESSA + IV implicita (~$4.9) → EV negativo *anche al pricing del mercato*, prima di ogni view di vol. +- **Quote sulle ali**: sì, 2-sided su tutto il ladder anche a 9h; depth MM ETH 400-1150 + contratti al tocco, BTC più sottile (0.4-49). MA la griglia strike daily è GROSSA (step + 1.3% ETH): i delta -0.28/-0.10 non esistono, si salta da -0.36 a -0.12; smile ripidissimo + (vendi bid_iv 13-20, compri ask_iv 22-38); le ali <5d hanno spread mediano 200% del mid = + di fatto non prezzate. Tick 0.0001 base = 3-10% del premio daily: anche la granularità quota. + +## Margine = la realtà che uccide la taglia + +Default Deribit = **standard margin SENZA netting fra gambe** (verificato docs): lo spread +"defined-risk" è margginato come naked short + long pagata intera. Il netting (margine ≈ width) +richiede Portfolio Margin, **min equity ~0.5 BTC ≈ $32k**. Quindi: +- ETH 1-lot PCS daily: margine SM ~$276, credito netto $2.72, max loss ~$22 → *meccanicamente* + eseguibile a $600 (1-2 lot), rendimento su margine ~1%/g SE zero perdite (e l'EV è negativo). +- BTC 0.1-lot: margine SM ~$963 → FUORI a $600, dentro a $2.6k. +- La soglia "$2.6k ETH" vale per strutture long/covered; per credit spread le soglie oneste + sono: $600 = ETH 1-lot (SM), $2.6k = entrambi sottili, **$32k = defined-risk vero (PM)**. + +## Pipeline di cattura — FATTA (non in cron) + +`scripts/research/r0724_daily_opt_snapshot.py`: pubblico/read-only, appende 1 riga JSON per +strumento (+1 meta con spot e DVOL) a `data/options_daily/snapshots.jsonl`; scadenze ≤50h, +moneyness 75-125%, ~160 righe/run, ~40 MB/anno a 2 run/g. Cadenza raccomandata quando la si +cabla: **08:05 UTC** (daily appena listata, ~24h DTE = premio vendibile) + **07:55 UTC** +(pre-settle = chiude il ciclo payoff). Primo snapshot già nel file. Esisteva solo +`options_real_quote_check.py` (weekly, one-shot, dipende da EXT path) e `fetch_dvol.py` (IV +30d, non quote): nessuna cattura daily preesistente. + +## Verdetto: **CAPTURE-AND-WAIT con prior negativo sul daily** + +Il tenor daily NON è morto-di-quotazione (mercato 2-sided, depth reale) ma stanotte era +**morto-di-economia**: haircut fee+spread 2-3x il weekly E IV~1.4x al daily) che oggi non c'è. Nessun deploy, nessun +cambio a book/pesi. Regola di decisione futura: dopo **≥90 giorni di cattura 2x/g**, calcolare +mediana(credito_netto/fair_mid) e mediana(IV_daily/RV_forward); il dossier si riapre SOLO se +il VRP daily mediano netto-haircut > 0 (e poi passa i gate standard). Il f di stress reale +arriva gratis se un crash cade nella finestra di cattura — lo stesso prerequisito che sblocca +VRP01. Fino ad allora: le daily servono da **strumento di misura**, non da strategia. + +Script: `scripts/research/r0724_daily_opt_snapshot.py`. Dati: `data/options_daily/snapshots.jsonl`. diff --git a/docs/diary/2026-07-24-strategy-hunt-web.md b/docs/diary/2026-07-24-strategy-hunt-web.md index 6273d7d..2dc5c84 100644 --- a/docs/diary/2026-07-24-strategy-hunt-web.md +++ b/docs/diary/2026-07-24-strategy-hunt-web.md @@ -105,3 +105,57 @@ capture, rivisita ETF 2028), e 2 regole metodologiche nuove a diario (vintage-ri delle fonti esterne; scettico plateau+lag obbligatorio sugli EARNS_SLOT con DSR<0.95). Book/pesi INVARIATI. + +--- + +## Addendum (sera, goal "un agente per ogni case") — i 3 verdetti + +Tre agenti dedicati, uno per case, tutti su DATI LIVE (nessun claim di seconda mano). +Script: `r0724_pm_deribit_probe.py`, `r0724_hlp_deepdive.py`, `r0724_daily_opt_snapshot.py`. + +### 1. Polymarket ↔ Deribit (con opzioni in copertura) — **SKIP come trade; il lead "11pp" è REFUTATO dalla misura** +34 binarie PM matchate alle daily options Deribit, spec verificata su 66 mercati (PM +risolve su candela 1m Binance USDT alle 16:00 UTC; Deribit TWAP 30m indice USD alle +08:00). Il gap si decompone in: **fonte USDT +9,3bps di basis = +4-5pp di probabilità +ATM**, tempo 8h ∓5pp, e replica ingenua su book ITM morti (la banda sub/super-replica +ATM è [39%,91%] con strike ogni $500 su orizzonti a 1σ≈$350). Residuo spec-adjusted: +ATM −5…+6pp a segno instabile DENTRO la banda di replica; tail +1,8-3,1pp +(favorite-longshot) sotto i costi. Trade coperto reale a size minima: lock netto +$0,1-5, capitale $200-1.000 (il margine SM del leg corto domina: Deribit non netta i +vertical), **P(gambe in conflitto 08→16 UTC) 2-22%** → non è un arb. Più geoblock/ToS +PM per residenti IT. Facoltativo: WATCH a costo zero (cron dello script per la serie +del gap tail). Lezione: anche il "best lead" accademico muore alla riconciliazione di +spec — la parte vera del gap era spec, non alpha. + +### 2. HLP vault — **SKIP come income, WATCH con trigger meccanici; il carry 2026 ex-evento è +0,2%/anno** +Trovata la serie daily vera (wrapper wHLP via CoinGecko, 364 giorni + thunderhead per +i tail di marzo 2025): il daily smaschera la lens 14g — Sharpe 0,69 (non ~2), maxDD +−7,9%, worst day −5,96%; e l'uscita-in-stress via wrapper costa ~5% di sconto NAV. +**Il finding killer: il +12% headline 2026 è UN SOLO periodo evento (feb-2026 +7%); +gli altri 15 periodi sommano +0,10%** → carry ex-evento ~0,2%/anno, PnL-$ piatto vs +TVL (ritorno ∝ 1/TVL, e i depositanti lo sanno: TVL −59% dal picco). Coda JELLY +ricostruita: −4,7% MtM su inventario EREDITATO illimitato, troncato SOLO da un voto +discrezionale dei validator in 2 minuti — il crash-long può morire NEL crash +(pattern is_hedge-che-fallisce). Kelly onesto con code: **f*=0 al carry corrente**. +Trigger di ri-arma meccanici: 90g return-on-AUM ≥8% ann. AND TVL<$350M AND nessun +evento di socializzazione in 180g. Legale: HL accessibile dall'IT ma non autorizzato +MiCA → rischio geoblock non copribile con lock 4g. + +### 3. Opzioni DAILY Deribit (0DTE) — **CAPTURE-AND-WAIT con prior negativo; cattura AVVIATA** +Misura live (22:35 UTC): mercato 2-sided reale anche sulle ali, MA il cap fee 12,5% +è quasi sempre binding a tenor daily → **drag fee 2-3× il weekly** (legge Albimarini +dal vivo: premio ~√T, fee ~costante; haircut mid→netto 0DTE ~45-50%) e stanotte +**IV_daily/RV = 0,43-0,54× (VRP NEGATIVO al front)** mentre il weekly fa 1,13× +(coerente con VRP01: il premio vive lì). Capitale minimo per defined-risk vero +(Portfolio Margin) ≈ $32k; a $600 = 1 lotto ETH a EV negativo. **Pipeline scritta e +attiva**: `r0724_daily_opt_snapshot.py` → `data/options_daily/snapshots.jsonl` +(primo snapshot 160 righe già su disco; raccomandate 2 run/g 08:05+07:55 UTC — da +cablare in cron alla merge, read-only). Regola pre-registrata: riaprire il dossier +solo dopo ≥90g di serie E mediana(IV_daily/RV_forward) netto-haircut > 0. +Diario dedicato: `2026-07-24-daily-options-0dte.md`. + +### Bilancio dei 3 case +0 trade nuovi (ed è il punto: due lead famosi refutati coi numeri, non con le +opinioni), 1 pipeline di cattura reale avviata che tra 90 giorni decide da sola, +regole di ri-arma meccaniche per HLP, e il metodo confermato: ogni claim muore o +sopravvive alla RICONCILIAZIONE DI SPEC prima che al backtest. Book/pesi INVARIATI. diff --git a/scripts/research/r0724_daily_opt_snapshot.py b/scripts/research/r0724_daily_opt_snapshot.py new file mode 100644 index 0000000..9338f3d --- /dev/null +++ b/scripts/research/r0724_daily_opt_snapshot.py @@ -0,0 +1,150 @@ +"""SNAPSHOT catena opzioni DAILY (0-1DTE) Deribit BTC/ETH — cattura quote REALI per il dossier 0DTE. + +CONTESTO (2026-07-24). VRP01 (put credit spread settimanale) e' deploy-bloccato dalla regola +"niente short-vol da modello": serve una serie di premi REALI, non BS-su-DVOL. Le scadenze DAILY +accumulano 365 expiry/anno (7x le weekly): in ~6 mesi di cattura si ottiene una serie di ~180 +premi reali a tenor giornaliero + ~26 weekly, abbastanza per stimare il VRP daily NETTO di +bid/ask e fee (misura live 2026-07-24: haircut mid->netto ~30-50% a tenor daily, fee cap 12.5% +quasi sempre binding perche' i premi daily sono < 0.0024 base ccy). + +COSA FA (sola lettura, API pubblica, nessun ordine, nessun token): + - per BTC e ETH: public/get_instruments (kind=option) -> tutte le scadenze entro --max-dte-h + (default 50h = la daily 0DTE + la daily 1DTE appena listata); + - per ogni strumento nel ladder di moneyness (default 75-125%): public/ticker -> + bid/ask/depth, mark, mark_iv, bid_iv/ask_iv, greche, OI, volume, underlying; + - 1 riga JSON per strumento ("rec":"chain") + 1 riga meta per valuta ("rec":"meta", con + index price e DVOL corrente) APPESE a data/options_daily/snapshots.jsonl. + +CADENZA RACCOMANDATA (quando/se si decide di cablarla — NON e' in cron adesso): + - 08:05 UTC: subito dopo il listing della nuova daily (~24h DTE) = il premio "vendibile"; + - 07:55 UTC: subito prima del settle (08:00 UTC) = chiude il ciclo (payoff realizzato). + Con 2 run/giorno: ~160 strumenti/run, ~350 byte/riga -> ~120 KB/giorno, ~40 MB/anno. Banale. + Ogni run extra (es. 12:00/20:00) aggiunge la dimensione intraday dello spread: opzionale. + +USO: + uv run python scripts/research/r0724_daily_opt_snapshot.py + uv run python scripts/research/r0724_daily_opt_snapshot.py --currencies BTC --max-dte-h 30 + +NB ONESTO: questo script MISURA, non decide. La serie che produce serve a rispondere fra ~6 mesi +a: (1) IV daily vs RV daily netto haircut, (2) quanto spesso il gate IV-rank aprirebbe a tenor +daily, (3) f di stress reale quando capita un crash dentro la finestra di cattura. +""" +from __future__ import annotations + +import argparse +import json +import sys +import time +from pathlib import Path + +import requests + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +OUT_DIR = PROJECT_ROOT / "data" / "options_daily" +OUT_FILE = OUT_DIR / "snapshots.jsonl" +API = "https://www.deribit.com/api/v2/public" + +SESSION = requests.Session() + + +def api(endpoint: str, **params): + """GET pubblico con retry breve. Ritorna result o solleva.""" + last = None + for _ in range(4): + try: + r = SESSION.get(f"{API}/{endpoint}", params=params, timeout=20) + j = r.json() + if "result" in j: + return j["result"] + last = j.get("error") + except Exception as e: # rete/JSON: ritenta + last = str(e) + time.sleep(0.7) + raise RuntimeError(f"Deribit API fail {endpoint} {params}: {last}") + + +def snapshot_currency(cur: str, max_dte_h: float, mny_lo: float, mny_hi: float) -> list[dict]: + now_ms = int(time.time() * 1000) + snap_ts = now_ms + rows: list[dict] = [] + + instruments = api("get_instruments", currency=cur, kind="option", expired="false") + spot = api("get_index_price", index_name=f"{cur.lower()}_usd")["index_price"] + try: + dvol_data = api("get_volatility_index_data", currency=cur, + start_timestamp=now_ms - 3_600_000, end_timestamp=now_ms, + resolution=3600).get("data", []) + dvol = float(dvol_data[-1][4]) if dvol_data else None + except Exception: + dvol = None + + chain = [i for i in instruments + if (i["expiration_timestamp"] - now_ms) / 3.6e6 <= max_dte_h + and mny_lo * spot <= i["strike"] <= mny_hi * spot] + chain.sort(key=lambda i: (i["expiration_timestamp"], i["strike"], i["option_type"])) + + expiries = sorted({i["expiration_timestamp"] for i in chain}) + rows.append({ + "rec": "meta", "snap_ts": snap_ts, "currency": cur, "index_price": spot, + "dvol": dvol, "n_instruments": len(chain), "expiries": expiries, + "max_dte_h": max_dte_h, "moneyness": [mny_lo, mny_hi], + }) + + for inst in chain: + name = inst["instrument_name"] + try: + t = api("ticker", instrument_name=name) + except RuntimeError as e: + print(f" WARN ticker {name}: {e}", file=sys.stderr) + continue + g = t.get("greeks") or {} + st = t.get("stats") or {} + rows.append({ + "rec": "chain", "snap_ts": snap_ts, "currency": cur, "instrument": name, + "expiry_ts": inst["expiration_timestamp"], + "dte_h": round((inst["expiration_timestamp"] - snap_ts) / 3.6e6, 3), + "strike": inst["strike"], "type": inst["option_type"], + "settlement_period": inst.get("settlement_period"), + "min_trade_amount": inst.get("min_trade_amount"), + "taker_comm": inst.get("taker_commission"), + "bid": t.get("best_bid_price"), "ask": t.get("best_ask_price"), + "bid_amount": t.get("best_bid_amount"), "ask_amount": t.get("best_ask_amount"), + "mark": t.get("mark_price"), "mark_iv": t.get("mark_iv"), + "bid_iv": t.get("bid_iv"), "ask_iv": t.get("ask_iv"), + "delta": g.get("delta"), "gamma": g.get("gamma"), + "vega": g.get("vega"), "theta": g.get("theta"), + "oi": t.get("open_interest"), "volume_24h": st.get("volume"), + "underlying": t.get("underlying_price"), "index_price": t.get("index_price"), + }) + time.sleep(0.05) # rate-limit gentile (pubblico: 20 req/s, stiamo larghi) + return rows + + +def main(): + ap = argparse.ArgumentParser(description="Snapshot catena daily-expiry Deribit -> JSONL") + ap.add_argument("--currencies", nargs="+", default=["BTC", "ETH"]) + ap.add_argument("--max-dte-h", type=float, default=50.0, + help="cattura tutte le scadenze entro N ore (default 50 = 0DTE+1DTE)") + ap.add_argument("--moneyness", nargs=2, type=float, default=[0.75, 1.25], + metavar=("LO", "HI"), help="ladder strike in frazione dello spot") + args = ap.parse_args() + + OUT_DIR.mkdir(parents=True, exist_ok=True) + total = 0 + with OUT_FILE.open("a") as f: + for cur in args.currencies: + try: + rows = snapshot_currency(cur, args.max_dte_h, *args.moneyness) + except Exception as e: + print(f"ERRORE {cur}: {e}", file=sys.stderr) + continue + for r in rows: + f.write(json.dumps(r, separators=(",", ":")) + "\n") + total += len(rows) + n_chain = sum(1 for r in rows if r["rec"] == "chain") + print(f"{cur}: {n_chain} strumenti (+1 meta) appesi") + print(f"OK: {total} righe -> {OUT_FILE}") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0724_hlp_deepdive.py b/scripts/research/r0724_hlp_deepdive.py new file mode 100644 index 0000000..09c88c2 --- /dev/null +++ b/scripts/research/r0724_hlp_deepdive.py @@ -0,0 +1,291 @@ +"""r0724_hlp_deepdive — Deep-dive ALLOCATION sul vault HLP di Hyperliquid. + +Contesto (2026-07-24): il probe HLP (89-95 punti ~14g dall'API nativa, vedi +`data/external/hlp_vault.json`) ha mostrato l'unico stream crash-long mai visto dal +progetto (corr col book -0.19, +10.4% nel cascade ott-2025). Questo script risponde +alle domande aperte con dati reali: + +1. GRANULARITA' DAILY — l'API nativa (`vaultDetails`/`portfolio`) e' downsampled: + allTime ~14g (95 pti), month ~10h ma SOLO ultimi 30g, week ~2.4h ultimi 7g + → nessun daily storico ufficiale. Fonti esterne trovate: + - CoinGecko `wrapped-hlp` (wHLP di Hyperbeat, redimibile a NAV): prezzo DAILY + dal 2025-07-25 → oggi. E' un prezzo di mercato (puo' fare sconto sotto stress), + ma e' l'unico mark giornaliero di lungo periodo liberamente accessibile. + - DefiLlama `protocol/hyperliquid-hlp`: TVL DAILY dal 2024-12 (per il modello + di diluizione return-vs-TVL). + - Thunderhead (cloudfront d2v1fiwobg9w6): `hlp_liquidator_pnl` DAILY, ma solo + 2025-03-05 → 2025-07-12 (stantio; copre pero' il 12-mar e JELLY 26-mar-2025) + e `hlp_positions` (esposizione daily per coin dal 2023-06, per la leva). + - MORTI: ASXN api-hyperliquid.asxn.xyz `/hlp_pnl` (daily completo, dietro + Turnstile anti-bot); DefiLlama yields (HLP non ha token → non e' un pool); + stats-data.hyperliquid.xyz espone solo `Mainnet/vaults` (stessi dati downsampled). + +2. CODA — ricostruzione numerica 12-mar-2025 (whale ETH, -$4.28M realizzati in + 1 giorno dal liquidator) e JELLY 26-mar-2025 (unrealized peak ~-$13.5M su TVL + ~$240M ≈ -5.6%, salvato da delist+settlement dei validator a $0.0095 → +$703k + realizzati). Vedi diario per il worst-case ragionato. + +3-5. Fisco/accesso e allocation math: nel diario. Qui i numeri. + +Uso: + uv run python scripts/research/r0724_hlp_deepdive.py # usa cache se c'e' + uv run python scripts/research/r0724_hlp_deepdive.py --refresh # ri-scarica + +I fetch sono salvati in data/external/hlp_deepdive/ (gitignored di fatto: non +committare i dati). Nessuna azione live: SOLO analisi. +""" + +from __future__ import annotations + +import argparse +import json +import sys +import urllib.request +from datetime import datetime, timezone +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +EXT = ROOT / "data" / "external" +CACHE = EXT / "hlp_deepdive" +CACHE.mkdir(parents=True, exist_ok=True) + +VAULT = "0xdfc24b077bc1425ad1dea75bcb6f8158e10df303" + +SOURCES = { + # prezzo daily wHLP (Hyperbeat Wrapped HLP) — mark di mercato del NAV HLP + "whlp_coingecko.json": ( + "https://api.coingecko.com/api/v3/coins/wrapped-hlp/market_chart" + "?vs_currency=usd&days=365&interval=daily" + ), + # TVL daily del vault HLP + "llama_hlp_tvl.json": "https://api.llama.fi/protocol/hyperliquid-hlp", + # PnL daily del liquidator HLP (stantio: 2025-03-05 → 2025-07-12, copre JELLY) + "thunderhead_liq_pnl.json": "https://d2v1fiwobg9w6.cloudfront.net/hlp_liquidator_pnl", +} + + +def fetch(name: str, url: str, refresh: bool) -> dict | list | None: + p = CACHE / name + if p.exists() and not refresh: + return json.loads(p.read_text()) + try: + req = urllib.request.Request(url, headers={"User-Agent": "pythagoras-research/1.0"}) + raw = urllib.request.urlopen(req, timeout=60).read() + p.write_bytes(raw) + return json.loads(raw) + except Exception as e: # noqa: BLE001 + print(f" [WARN] fetch {name} fallito ({e}); uso cache se esiste") + return json.loads(p.read_text()) if p.exists() else None + + +def refresh_vault_details(refresh: bool) -> dict: + """Snapshot vaultDetails dall'API nativa (stesso formato del probe originale).""" + p = EXT / "hlp_vault.json" + if p.exists() and not refresh: + return json.loads(p.read_text()) + body = json.dumps({"type": "vaultDetails", "vaultAddress": VAULT}).encode() + req = urllib.request.Request( + "https://api.hyperliquid.xyz/info", data=body, + headers={"Content-Type": "application/json"}, + ) + raw = urllib.request.urlopen(req, timeout=60).read() + p.write_bytes(raw) + return json.loads(raw) + + +def series_from_period(d: dict, period: str) -> pd.DataFrame: + for per, pdata in d["portfolio"]: + if per == period: + av = pd.DataFrame(pdata["accountValueHistory"], columns=["ts", "av"]) + pnl = pd.DataFrame(pdata["pnlHistory"], columns=["ts", "pnl"]) + df = av.merge(pnl, on="ts") + df["t"] = pd.to_datetime(df["ts"], unit="ms", utc=True) + df["av"] = df["av"].astype(float) + df["pnl"] = df["pnl"].astype(float) # PnL CUMULATIVO + return df.set_index("t")[["av", "pnl"]] + raise KeyError(period) + + +def ret_stats(r: pd.Series, periods_per_year: float, label: str) -> dict: + r = r.replace([np.inf, -np.inf], np.nan).dropna() + mu, sd = r.mean(), r.std() + sharpe = mu / sd * np.sqrt(periods_per_year) if sd > 0 else np.nan + eq = (1 + r).cumprod() + dd = (eq / eq.cummax() - 1).min() + ann = (1 + mu) ** periods_per_year - 1 + out = dict(label=label, n=len(r), ann_ret=ann, sharpe=sharpe, maxdd=dd, + worst=r.min(), worst_t=str(r.idxmin())[:10], best=r.max()) + print(f" {label:28s} n={out['n']:4d} ann~{ann*100:6.1f}% Sh={sharpe:5.2f} " + f"maxDD={dd*100:5.1f}% worst={r.min()*100:6.2f}% ({out['worst_t']})") + return out + + +def main() -> None: + ap = argparse.ArgumentParser() + ap.add_argument("--refresh", action="store_true") + args = ap.parse_args() + + print("=" * 78) + print("HLP DEEP-DIVE — dati al", datetime.now(timezone.utc).isoformat()[:16]) + print("=" * 78) + + d = refresh_vault_details(args.refresh) + print(f"\nVault: {d['name']} APR corrente dichiarato: {d['apr']*100:.3f}%") + print(f"isClosed={d['isClosed']} allowDeposits={d['allowDeposits']} " + f"maxDistributable=${d['maxDistributable']/1e6:.0f}M") + + # ---------------- A. serie nativa 14g (allTime) ---------------- + print("\n[A] Serie NATIVA allTime (~14g/punto) — return su AUM di inizio periodo") + at = series_from_period(d, "allTime") + at["dpnl"] = at["pnl"].diff() + at["ret"] = at["dpnl"] / at["av"].shift(1) + at.loc[at["av"].shift(1) < 5e6, "ret"] = np.nan # AUM<5M: return non significativo + ret14 = at["ret"].iloc[1:] + ret_stats(ret14, 365.25 / 14, "HLP 14g FULL (AUM>5M)") + for y in (2023, 2024, 2025, 2026): + sub = ret14[ret14.index.year == y] + if len(sub) > 3: + ret_stats(sub, 365.25 / 14, f" anno {y}") + print(" Traiettoria 2026 (per periodo 14g):", + " ".join(f"{x*100:+.1f}" for x in ret14[ret14.index.year == 2026].dropna())) + worst5 = ret14.dropna().nsmallest(5) + print(" 5 peggiori periodi 14g:", [(str(i.date()), f"{v*100:+.2f}%") for i, v in worst5.items()]) + + # month nativo (~10h granularita', ultimi 30g) + mo = series_from_period(d, "month") + mo_ret = mo["pnl"].diff() / mo["av"].shift(1) + tot30 = (1 + mo_ret.dropna()).prod() - 1 + print(f" Ultimi 30g (periodo 'month', {len(mo)} pti ~10h): tot {tot30*100:+.2f}%") + + # ---------------- B. wHLP daily (CoinGecko) ---------------- + print("\n[B] wHLP (CoinGecko, prezzo DAILY di mercato ~ NAV; sconto possibile)") + cg = fetch("whlp_coingecko.json", SOURCES["whlp_coingecko.json"], args.refresh) + if cg and cg.get("prices"): + pr = pd.DataFrame(cg["prices"], columns=["ts", "px"]) + pr["t"] = pd.to_datetime(pr["ts"], unit="ms", utc=True).dt.normalize() + pr = pr.drop_duplicates("t").set_index("t")["px"].astype(float) + r1d = pr.pct_change() + ret_stats(r1d, 365.25, "wHLP daily FULL (12 mesi)") + for lbl, a, b in [ + ("cascade 2025-10-09→13", "2025-10-09", "2025-10-13"), + ("feb-2026 (liq event)", "2026-02-01", "2026-02-28"), + ("ultimi 45g", str(pr.index[-1] - pd.Timedelta(days=45))[:10], None), + ]: + w = pr.loc[a:b] if b else pr.loc[a:] + if len(w) > 1: + print(f" {lbl:26s} {w.iloc[0]:.4f} → {w.iloc[-1]:.4f} " + f"({(w.iloc[-1]/w.iloc[0]-1)*100:+.2f}%) min {w.min():.4f}") + peak = pr.cummax() + cur_dd = pr.iloc[-1] / peak.iloc[-1] - 1 + print(f" Drawdown CORRENTE dal max ({pr.idxmax().date()} {pr.max():.4f}): " + f"{cur_dd*100:+.2f}%") + + # ---------------- C. TVL daily (DefiLlama) → diluizione ---------------- + print("\n[C] TVL daily (DefiLlama hyperliquid-hlp) + fit return-vs-TVL") + ll = fetch("llama_hlp_tvl.json", SOURCES["llama_hlp_tvl.json"], args.refresh) + if ll and ll.get("tvl"): + tvl = pd.DataFrame(ll["tvl"]) + tvl["t"] = pd.to_datetime(tvl["date"], unit="s", utc=True).dt.normalize() + tvl = tvl.drop_duplicates("t").set_index("t")["totalLiquidityUSD"].astype(float) + print(f" TVL {tvl.index[0].date()} ${tvl.iloc[0]/1e6:.0f}M → picco " + f"{tvl.idxmax().date()} ${tvl.max()/1e6:.0f}M → oggi ${tvl.iloc[-1]/1e6:.0f}M " + f"({(tvl.iloc[-1]/tvl.max()-1)*100:+.0f}% dal picco)") + # fit sul nativo: PnL$ 14g vs AUM inizio periodo (tutta la storia) + x = at["av"].shift(1).iloc[1:] / 1e6 + y = at["dpnl"].iloc[1:] / 1e6 + ok = x.notna() & y.notna() + b1, b0 = np.polyfit(x[ok], y[ok], 1) + corr_xy = np.corrcoef(x[ok], y[ok])[0, 1] + print(f" Fit PnL$_14g = {b0:+.2f}M {b1:+.4f}·TVL(M) corr={corr_xy:+.2f} " + f"(b1≈0 ⇒ PnL$ NON scala col TVL ⇒ return-on-AUM ∝ 1/TVL = diluizione)") + for lo, hi in [(0, 150), (150, 300), (300, 450), (450, 700)]: + m = (x >= lo) & (x < hi) + if m.sum() >= 4: + impl = (y[m].mean() / x[m].mean()) * (365.25 / 14) * 100 + print(f" TVL {lo:3d}-{hi:3d}M: n={m.sum():3d} PnL medio " + f"${y[m].mean()*1000:+7.0f}k/14g → ~{impl:+.1f}%/anno su AUM") + + # ---------------- D. liquidator daily (thunderhead) — la coda ---------------- + print("\n[D] Liquidator PnL DAILY (thunderhead, 2025-03-05→2025-07-12 — copre JELLY)") + th = fetch("thunderhead_liq_pnl.json", SOURCES["thunderhead_liq_pnl.json"], args.refresh) + if th and th.get("chart_data"): + liq = pd.DataFrame(th["chart_data"]).dropna() + liq["t"] = pd.to_datetime(liq["time"], utc=True) + liq = liq.set_index("t")["total_pnl"].astype(float) + w = liq.sort_values() + print(" 5 peggiori giorni:", [(str(i.date()), f"{v/1e6:+.2f}M") for i, v in w.head(5).items()]) + print(" 5 migliori giorni:", [(str(i.date()), f"{v/1e6:+.2f}M") for i, v in w.tail(5).items()]) + for day, note in [("2025-03-12", "whale ETH 50x"), ("2025-03-26", "JELLY settle")]: + if day in liq.index.strftime("%Y-%m-%d").tolist(): + v = liq[liq.index.strftime("%Y-%m-%d") == day].iloc[0] + print(f" {day} ({note}): {v/1e6:+.2f}M") + + # ---------------- F. decomposizioni oneste ---------------- + print("\n[F] Decomposizioni oneste") + # carry 2026 ex-evento: togli il singolo periodo +7% (evento liquidazione feb-2026) + r26 = ret14[ret14.index.year == 2026].dropna() + if len(r26): + ev = r26.idxmax() + ex = r26.drop(ev) + cum_ex = (1 + ex).prod() - 1 + ann_ex = (1 + cum_ex) ** (365.25 / (14 * len(ex))) - 1 + print(f" 2026: evento {ev.date()} {r26.max()*100:+.1f}% | ex-evento cum " + f"{cum_ex*100:+.2f}% su {len(ex)} periodi ≈ {ann_ex*100:+.1f}%/anno " + f"→ IL CARRY E' MORTO, resta solo il crash-alpha") + # TVL al giorno JELLY (per scalare il -13.5M unrealized) + if ll and ll.get("tvl"): + for day in ("2025-03-12", "2025-03-26", "2025-10-10", "2026-02-08"): + ts = pd.Timestamp(day, tz="UTC") + i = tvl.index.get_indexer([ts], method="nearest")[0] + print(f" TVL {day}: ${tvl.iloc[i]/1e6:.0f}M " + f"(-13.5M JELLY = {-13.5e6/tvl.iloc[i]*100:.1f}% MtM)" if day == "2025-03-26" + else f" TVL {day}: ${tvl.iloc[i]/1e6:.0f}M") + # sconto wrapper: wHLP mercato vs NAV nativo su finestra comune (da max wHLP a oggi) + if cg and cg.get("prices"): + w_from = pr.idxmax() + nav_win = ret14[ret14.index >= w_from].dropna() + nav_chg = (1 + nav_win).prod() - 1 + whlp_chg = pr.iloc[-1] / pr.max() - 1 + print(f" Da {w_from.date()}: wHLP mercato {whlp_chg*100:+.2f}% vs NAV nativo " + f"{nav_chg*100:+.2f}% → sconto wrapper ≈ {(whlp_chg-nav_chg)*100:+.1f}pt " + f"(costo di un exit-in-stress via wrapper)") + + # ---------------- E. allocation math (Kelly log-utility, outcome discreti) ---------------- + print("\n[E] Allocation math — Kelly log-utility su outcome annui discreti") + # Outcome annui dell'ALLOCAZIONE (non del bankroll): base 2 scenari di coda: + # - evento socializzazione stile-JELLY-senza-salvataggio: -20% dell'allocazione + # - morte protocollo/bridge (hack, insolvenza, socializzazione totale): -100% + # mu base = run-rate onesto 2026 (decay in corso: usare 6%, non il 19% del 2025) + scenarios = [ + ("base mu=6%", [(0.06, 0.83), (-0.20, 0.15), (-1.00, 0.02)]), + ("ottimista mu=12%", [(0.12, 0.87), (-0.20, 0.10), (-1.00, 0.03)]), + ("pessimista mu=3%", [(0.03, 0.80), (-0.25, 0.17), (-1.00, 0.03)]), + ] + for name, outs in scenarios: + mu_adj = sum(x * p for x, p in outs) + fgrid = np.linspace(0.001, 0.999, 999) + util = [sum(p * np.log(1 + f * x) for x, p in outs) for f in fgrid] + f_star = float(fgrid[int(np.argmax(util))]) + print(f" {name:20s} mu_adj={mu_adj:+.1%} Kelly pieno f*={f_star:.2f} " + f"0.25·Kelly={0.25*f_star:.1%} del bankroll") + f_use = 0.10 # ~0.25·Kelly dello scenario base, cap operativo + print(f" → allocazione difendibile ~{f_use:.0%} del bankroll (VRP01 fu ~0.27 Kelly):") + for cap in (600, 2000, 5000, 20000): + a = f_use * cap + print(f" capitale ${cap:>6,}: ~${a:,.0f} → atteso ~${a*0.06:,.0f}/anno; " + f"coda -${a*0.20:,.0f} (JELLY-like) / -${a:,.0f} (protocollo)") + + print("\nFonti dati:") + for k, v in SOURCES.items(): + print(f" {k:28s} {v}") + print(" hlp_vault.json https://api.hyperliquid.xyz/info " + '{"type":"vaultDetails","vaultAddress":"' + VAULT + '"}') + print(" MORTI: ASXN /api/hlp_pnl (Turnstile), DefiLlama yields (HLP non-token), " + "stats-data.hyperliquid.xyz (downsampled)") + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/research/r0724_pm_deribit_probe.py b/scripts/research/r0724_pm_deribit_probe.py new file mode 100644 index 0000000..281e6da --- /dev/null +++ b/scripts/research/r0724_pm_deribit_probe.py @@ -0,0 +1,473 @@ +"""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()