"""VERIFICA SLEEVE OPZIONI su QUOTE REALI Deribit — quanto Sharpe sopravvive a bid/ask + skew. Lo sleeve income della strategia esterna `crypto_backtest` (vendita di put settimanali CSP su BTC, delta 0.28) e' backtestato su prezzi MODELLATI: Black-Scholes prezzato con DVOL = IV ATM, e si incassa il premio "fair" (mid). Due gap reali NON catturati: (1) BID/ASK: vendendo si incassa il BID, non il mid. (2) SKEW: una put OTM (delta 0.28) ha IV piu' alta della ATM (DVOL) -> il modello prezza la put con la vol sbagliata. Questo script: PARTE 1 (rete, Deribit mainnet pubblico): scarica la catena REALE della scadenza ~settimanale, trova la put a delta ~0.28, e misura: - premio reale incassabile (BID, in USD) vs premio modellato (BS @ IV ATM) - skew: IV della put OTM (mark) vs IV ATM (mark) - spread: bid/mark - HAIRCUT netto f = premio_bid_reale / premio_BS@ATM PARTE 2 (locale): ri-esegue lo sleeve CSP settimanale (dati + modulo del progetto esterno) con il premio moltiplicato per f -> Sharpe/CAGR/maxDD reali stimati, vs i modellati. NB ONESTO: e' UNO SNAPSHOT (la catena di oggi). Lo spread si allarga nello stress; lo skew varia. Va ripetuto nel tempo per robustezza. Ma misura direttamente i due gap col mercato vero. uv run python scripts/research/options_real_quote_check.py """ from __future__ import annotations import sys from pathlib import Path import numpy as np import pandas as pd EXT = Path("/home/adriano/crypto_backtest") sys.path.insert(0, str(EXT)) PUT_DELTA = 0.28 CYCLE_DAYS = 7 ANN = 365 def fetch_real_chain(): import ccxt ex = ccxt.deribit({"enableRateLimit": True}) ex.load_markets() puts = [m for m in ex.markets.values() if m.get("option") and m["base"] == "BTC" and m["optionType"] == "put"] calls = [m for m in ex.markets.values() if m.get("option") and m["base"] == "BTC" and m["optionType"] == "call"] # expiries -> pick the one closest to CYCLE_DAYS days out now = pd.Timestamp.utcnow().tz_localize(None) def exp_dt(m): return pd.to_datetime(m["symbol"].split("-")[1], format="%y%m%d") exps = sorted(set(exp_dt(m) for m in puts)) target = now + pd.Timedelta(days=CYCLE_DAYS) expiry = min(exps, key=lambda e: abs((e - target).days)) dte = (expiry - now).days + (expiry - now).seconds / 86400 chain_puts = [m for m in puts if exp_dt(m) == expiry] chain_calls = [m for m in calls if exp_dt(m) == expiry] print(f" scadenza scelta: {expiry.date()} (DTE ~{dte:.1f}g, target {CYCLE_DAYS}g) " f"strikes put: {len(chain_puts)}") def tick(m): try: t = ex.fetch_ticker(m["symbol"]) i = t["info"] g = i.get("greeks") or {} return dict(symbol=m["symbol"], strike=float(m["strike"]), delta=float(g.get("delta", "nan")), mark_iv=float(i.get("mark_iv", "nan")), bid=float(i.get("best_bid_price") or 0), ask=float(i.get("best_ask_price") or 0), mark=float(i.get("mark_price") or 0), S=float(i.get("underlying_price") or i.get("index_price") or 0)) except Exception: return None rows = [r for r in (tick(m) for m in chain_puts) if r and np.isfinite(r["delta"])] callrows = [r for r in (tick(m) for m in chain_calls) if r and np.isfinite(r["delta"])] return expiry, dte, pd.DataFrame(rows), pd.DataFrame(callrows) def bs_put(S, K, T, sigma): from scipy.stats import norm if T <= 0 or sigma <= 0: return max(0.0, K - S) d1 = (np.log(S / K) + 0.5 * sigma ** 2 * T) / (sigma * np.sqrt(T)) d2 = d1 - sigma * np.sqrt(T) return K * norm.cdf(-d2) - S * norm.cdf(-d1) def measure_haircut(dte, puts, calls): S = puts["S"].iloc[0] T = dte / ANN # ATM IV: option with |delta| closest to 0.5 (use calls+puts mark_iv near ATM) allo = pd.concat([puts.assign(typ="P"), calls.assign(typ="C")], ignore_index=True) atm = allo.iloc[(allo["delta"].abs() - 0.5).abs().argsort()[:4]] atm_iv = atm["mark_iv"].mean() / 100.0 # delta-0.28 put (delta negative) p = puts.iloc[(puts["delta"] - (-PUT_DELTA)).abs().argsort()[:1]].iloc[0] K = p["strike"] put_iv = p["mark_iv"] / 100.0 # premiums in USD (Deribit option price is in BTC) bid_usd = p["bid"] * S mark_usd = p["mark"] * S ask_usd = p["ask"] * S bs_atm_usd = bs_put(S, K, T, atm_iv) # cio' che il backtest assume (DVOL=ATM, incassa mid) bs_skew_usd = bs_put(S, K, T, put_iv) # BS alla vol REALE della put (isola lo skew) print("\n --- MISURA SU QUOTE REALI (snapshot) ---") print(f" underlying S = {S:,.0f} strike(delta~-0.28) K = {K:,.0f} ({(1-K/S)*100:.1f}% OTM) delta {p['delta']:.3f}") print(f" IV ATM (DVOL-equivalente) = {atm_iv*100:.1f}% IV put OTM (mark) = {put_iv*100:.1f}% " f"skew +{(put_iv-atm_iv)*100:.1f} pt") print(f" premio put (USD): BID {bid_usd:,.1f} mark {mark_usd:,.1f} ask {ask_usd:,.1f}") print(f" spread bid/mark = {(p['bid']/p['mark']) if p['mark']>0 else float('nan'):.3f} " f"(ask-bid)/mark = {((p['ask']-p['bid'])/p['mark']) if p['mark']>0 else float('nan'):.3f}") print(f" modellato dal backtest BS@IV-ATM = {bs_atm_usd:,.1f} USD (BS@IV-put-reale = {bs_skew_usd:,.1f})") f_bid = bid_usd / bs_atm_usd if bs_atm_usd > 0 else float("nan") f_mark = mark_usd / bs_atm_usd if bs_atm_usd > 0 else float("nan") print(f" HAIRCUT premio: reale(BID)/modello = {f_bid:.3f} | mark/modello = {f_mark:.3f}") print(f" -> lo skew ALZA il premio lordo (+{(bs_skew_usd/bs_atm_usd-1)*100:.0f}% vs ATM), ma il " f"BID/ask lo riporta a {f_bid*100:.0f}% del modello.") return f_bid def csp_sleeve_haircut(f): """Ri-esegue lo sleeve CSP settimanale (dati+modulo esterni) con premio * f.""" import options_deribit as od px = pd.read_csv(EXT / "data/BTCUSDT.csv", parse_dates=["date"]).set_index("date")["close"] dvol = pd.read_csv(EXT / "data/DVOL_BTC.csv", parse_dates=["date"]).set_index("date")["close"] iv = od.build_iv(px, "BTC", dvol) d0 = dvol.index[0] px, iv = px[px.index >= d0], iv[iv.index >= d0] def sim(prem_mult, m=0.63): idx = px.index locs = list(range(0, len(idx) - CYCLE_DAYS, CYCLE_DAYS)) T = CYCLE_DAYS / ANN rows = [] for i in locs: S0, S1, sig = px.iloc[i], px.iloc[i + CYCLE_DAYS], iv.iloc[i] if not (np.isfinite(S0) and np.isfinite(S1) and np.isfinite(sig)): continue Kp = od.strike_for_delta(S0, T, sig, PUT_DELTA, call=False) pp = od.bs_price(S0, Kp, T, sig, call=False) * prem_mult # <-- haircut sul premio fee = od.option_fee(S0, pp) + (od.SETTLE_FEE * S0 if S1 < Kp else 0) pnl = pp - max(Kp - S1, 0.0) - fee rows.append((idx[i + CYCLE_DAYS], m * pnl / S0)) s = pd.Series({d: r for d, r in rows}).sort_index() return s def met(s, name): eq = (1 + s).cumprod() cpy = ANN / CYCLE_DAYS yrs = len(s) / cpy cagr = eq.iloc[-1] ** (1 / yrs) - 1 if eq.iloc[-1] > 0 else -1 sh = s.mean() / s.std() * np.sqrt(cpy) dd = (eq / eq.cummax() - 1).min() print(f" {name:<34s} CAGR {cagr*100:>+6.1f}% Sharpe {sh:>5.2f} maxDD {dd*100:>6.1f}% win {(s>0).mean()*100:>3.0f}%") return sh print("\n --- RI-ESECUZIONE SLEEVE CSP con HAIRCUT REALE (m=0.63, hold-to-expiry) ---") print(f" finestra {px.index[0].date()} -> {px.index[-1].date()} (DVOL reale)") sh_model = met(sim(1.00), "modello (premio pieno, BS@DVOL)") sh_real = met(sim(f), f"reale stimato (premio x{f:.2f} = BID)") # sensitivity for ff in (0.85, 0.70, 0.55): met(sim(ff), f"sensitivity premio x{ff:.2f}") print(f"\n => con haircut reale f={f:.2f}: Sharpe sleeve {sh_model:.2f} -> {sh_real:.2f}") return sh_model, sh_real def main(): print("=" * 92) print("# VERIFICA SLEEVE OPZIONI su QUOTE REALI DERIBIT — quanto Sharpe sopravvive") print("=" * 92) try: expiry, dte, puts, calls = fetch_real_chain() f = measure_haircut(dte, puts, calls) except Exception as e: print(f" [rete] impossibile scaricare la catena reale ({type(e).__name__}: {e})") print(" uso haircut di letteratura f=0.70 (spread+skew tipici su put OTM settimanali)") f = 0.70 f = float(np.clip(f, 0.3, 1.2)) csp_sleeve_haircut(f) print("\n CAVEAT: snapshot singolo; spread peggiora nello stress; ripetere nel tempo + testnet.") if __name__ == "__main__": main()