"""ANALISI OPTIONS per BTC/ETH — onesta sui dati REALI disponibili (cerbero-bite mainnet). Dati: Old/data/options (chain per-strike + dvol + market_snapshots). Finestra ~2026-05-01→06-11 (~6 settimane, REGIME UNICO calmo). NON si può validare OOS un edge su opzioni qui; si possono MISURARE i livelli reali (VRP, premi put, skew, liquidità) e ragionare sull'USO delle opzioni per il book BTC/ETH certificato. cerbero-bite è ancora vivo -> la fonte continua ad accumulare. uv run python scripts/analysis/options_analysis.py """ from __future__ import annotations import sys from pathlib import Path PROJECT_ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(PROJECT_ROOT)) import numpy as np import pandas as pd OPT = PROJECT_ROOT / "Old" / "data" / "options" def load(name): return pd.read_parquet(OPT / name) def market_snapshots_analysis(): print("=" * 90) print(" (1) MARKET SNAPSHOTS — VRP, DVOL, funding, dealer-gamma (livelli reali)") print("=" * 90) ms = load("market_snapshots.parquet") t = pd.to_datetime(ms["timestamp"], utc=True, errors="coerce") print(f" copertura: {t.min()} -> {t.max()} ({len(ms)} righe)") for a in ("BTC", "ETH"): d = ms[ms["asset"] == a].dropna(subset=["iv_minus_rv"]) if len(d) == 0: print(f" {a}: nessun dato"); continue vrp = d["iv_minus_rv"].astype(float) dvol = d["dvol"].astype(float) rv = d["realized_vol_30d"].astype(float) fund = d["funding_perp_annualized"].astype(float) if "funding_perp_annualized" in d else pd.Series([np.nan]) gam = d["dealer_net_gamma"].astype(float) if "dealer_net_gamma" in d else pd.Series([np.nan]) print(f"\n {a} (n={len(d)})") print(f" VRP (IV-RV): media {vrp.mean():+.1f} mediana {vrp.median():+.1f} " f">0 nel {100*(vrp>0).mean():.0f}% del tempo [IV-RV in punti di vol annua]") print(f" DVOL: media {dvol.mean():.1f} range [{dvol.min():.1f}, {dvol.max():.1f}]") print(f" Realized30d: media {rv.mean():.1f}") print(f" Funding perp: media {fund.mean():+.1f}% annuo") if gam.notna().any(): print(f" Dealer net-γ: >0 nel {100*(gam>0).mean():.0f}% del tempo (>0 = dealer long gamma = mean-rev)") def chain_analysis(asset): print("\n" + "=" * 90) print(f" (2) CHAIN {asset} — premi put protettivi, skew, liquidità (livelli reali)") print("=" * 90) ch = load(f"{asset.lower()}_chain.parquet") for col in ("strike", "bid", "ask", "mid", "iv", "delta", "gamma"): if col in ch: ch[col] = pd.to_numeric(ch[col], errors="coerce") ch["option_type"] = ch["option_type"].astype(str) dv = load("dvol_history.parquet") dv = dv[dv["asset"] == asset][["timestamp", "spot"]].copy() dv["spot"] = pd.to_numeric(dv["spot"], errors="coerce") # timestamp -> datetime UTC nativo (sono datetime64[tz], NON ms int: to_numeric li romperebbe) ch["t"] = pd.to_datetime(ch["timestamp"], utc=True, errors="coerce") dv["t"] = pd.to_datetime(dv["timestamp"], utc=True, errors="coerce") ch = ch.dropna(subset=["t"]).sort_values("t").reset_index(drop=True) dv = dv.dropna(subset=["t", "spot"]).sort_values("t").reset_index(drop=True) # spot causale per timestamp della chain (merge_asof nearest, tolleranza 1h) ch = pd.merge_asof(ch, dv[["t", "spot"]], on="t", direction="nearest", tolerance=pd.Timedelta("1h")) ch = ch.dropna(subset=["spot", "mid", "strike"]) # days-to-expiry exp = pd.to_datetime(ch["expiry"], utc=True, errors="coerce") ch["dte"] = (exp - ch["t"]).dt.total_seconds() / 86_400.0 ch = ch[(ch["dte"] > 0.5) & (ch["dte"] < 90)] ch["money"] = ch["strike"] / ch["spot"] ch["prem_pct"] = ch["mid"] * 100 # mid è in COIN (frazione del sottostante) -> %-del-notional # NB: iv è GIÀ in percento (35.94 = 35.94%, coerente col DVOL ~40) -> non riscalare ch["spread_pct"] = (ch["ask"] - ch["bid"]) / ch["mid"].replace(0, np.nan) * 100 puts = ch[ch["option_type"].str.lower().str.startswith("p")] calls = ch[ch["option_type"].str.lower().str.startswith("c")] def band(df, mlo, mhi, dlo, dhi): s = df[(df["money"] >= mlo) & (df["money"] <= mhi) & (df["dte"] >= dlo) & (df["dte"] <= dhi)] return s print(" PUT protettive — premio reale (mid/spot) e liquidità per tenor/moneyness:") print(f" {'tenor':<10s}{'moneyness':<14s}{'premio%':>9s}{'/mese%':>9s}{'spread%':>9s}{'n':>7s}{'strike?':>9s}") for dlo, dhi, tn in [(5, 12, "settim."), (18, 45, "mensile")]: for mlo, mhi, ml in [(0.97, 1.03, "ATM"), (0.88, 0.93, "~10% OTM"), (0.83, 0.88, "~15% OTM")]: s = band(puts, mlo, mhi, dlo, dhi) if len(s) == 0: print(f" {tn:<10s}{ml:<14s}{'—':>9s}{'—':>9s}{'—':>9s}{0:>7d}{'NO':>9s}") continue prem = s["prem_pct"].median() permonth = prem * 30.0 / s["dte"].median() print(f" {tn:<10s}{ml:<14s}{prem:>8.2f}%{permonth:>8.2f}%{s['spread_pct'].median():>8.1f}%" f"{len(s):>7d}{'SI':>9s}") # skew: IV put 10% OTM vs IV call 10% OTM (stesso tenor mensile) pv = band(puts, 0.88, 0.93, 12, 50)["iv"].median() cv = band(calls, 1.07, 1.12, 12, 50)["iv"].median() atmv = band(ch, 0.98, 1.02, 12, 50)["iv"].median() if pd.notna(pv) and pd.notna(cv): print(f" SKEW: IV put 10%OTM {pv:.0f}% vs call 10%OTM {cv:.0f}% vs ATM {atmv:.0f}%" f" -> skew put {pv-cv:+.0f} pt vol (>0 = put care = paura del crash prezzata)") def main(): market_snapshots_analysis() for a in ("BTC", "ETH"): chain_analysis(a) print("\n" + "=" * 90) print(" NB: finestra ~6 settimane, REGIME UNICO calmo -> livelli REALI misurabili, ma NESSUN") print(" edge su opzioni è validabile OOS qui. Vedi commento finale.") print("=" * 90) if __name__ == "__main__": main()