research: verify options sleeve on REAL Deribit quotes (spread+skew haircut)
- options_real_quote_check.py: fetches real weekly BTC put chain, measures premium haircut (bid vs BS@DVOL-ATM), re-runs CSP sleeve with real haircut - KEY FINDING (reverses a prior critique): backtest UNDER-prices the OTM put by using ATM DVOL; real skew (+28% gross) exceeds the ~4% bid/ask spread -> real bid premium = 1.29x modeled. Sleeve premium is conservative at current (calm) quotes. - Real risk SHIFTS to the tail + roll-liquidity in stress (skew = market pricing fat tail), not premium magnitude. Breakpoint: sleeve dies below ~70% premium capture. - updated eval diary with the verification
This commit is contained in:
@@ -81,3 +81,45 @@ e il blend scende di conseguenza.
|
|||||||
|
|
||||||
Coerente con la regola del progetto (lezione v2.0.0): un edge full+OOS robusto su prezzi MODELLATI
|
Coerente con la regola del progetto (lezione v2.0.0): un edge full+OOS robusto su prezzi MODELLATI
|
||||||
non è un edge finché non è verificato su prezzi reali ed eseguibili.
|
non è un edge finché non è verificato su prezzi reali ed eseguibili.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## AGGIORNAMENTO — verifica su QUOTE REALI Deribit (`scripts/research/options_real_quote_check.py`)
|
||||||
|
|
||||||
|
Fatta la verifica concreta (PARTE 1: catena reale Deribit mainnet pubblico; PARTE 2: ri-esecuzione
|
||||||
|
dello sleeve CSP con haircut reale sul premio). **Risultato che RIBALTA una mia critica.**
|
||||||
|
|
||||||
|
Snapshot del 2026-06-19, scadenza settimanale 2026-06-26 (~6.2 DTE), put delta −0.277 (strike 61k,
|
||||||
|
3.1% OTM), underlying 62.965:
|
||||||
|
|
||||||
|
| Grandezza | Valore |
|
||||||
|
|---|---|
|
||||||
|
| IV ATM (≈ DVOL) | 37.2% |
|
||||||
|
| IV put OTM (mark) | 42.1% (**skew +4.8 pt**) |
|
||||||
|
| premio put: BID / mark / ask | 598 / 623 / 630 USD |
|
||||||
|
| spread bid/mark | 0.96 (spread ~4%) |
|
||||||
|
| premio MODELLATO dal backtest (BS @ IV-ATM) | **463 USD** |
|
||||||
|
| **HAIRCUT premio reale(BID)/modello** | **1.29** |
|
||||||
|
|
||||||
|
**Il backtest SOTTOSTIMA il premio, non lo sovrastima.** Prezzando la put OTM con la DVOL (IV ATM)
|
||||||
|
ignora lo skew (+28% sul premio lordo); il bid/ask la riporta giu' solo del 4% → vendendo al BID
|
||||||
|
reale incassi **1.29×** il premio modellato. Lo sleeve modellato (Sharpe 1.13) e' quindi
|
||||||
|
**conservativo sul premio** alle quote attuali; col premio reale salirebbe (Sharpe → 1.83 a f=1.29).
|
||||||
|
|
||||||
|
**Ma la critica vera si SPOSTA, non sparisce:** lo skew esiste perche' il mercato prezza la coda
|
||||||
|
grassa: piu' premio = esattamente perche' i crash fanno male. La sensitivity mostra il punto di
|
||||||
|
rottura — lo sleeve regge finche' incassi >~85% del premio modellato (Sharpe 0.59 a f=0.85), va a
|
||||||
|
zero a f=0.70, negativo a f=0.55. Lo snapshot e' in **regime calmo** (IV ATM 37%, bassa per crypto);
|
||||||
|
in un crash lo spread si allarga molto e potresti non riuscire a rollare. Quindi:
|
||||||
|
|
||||||
|
- ✅ **Concern "premio sovrastimato" = SMENTITO** (alle quote attuali e' anzi sottostimato).
|
||||||
|
- ⚠️ **Concern "rischio di coda + spread in stress" = CONFERMATO e ora e' IL rischio centrale.**
|
||||||
|
Il backtest cattura i crash realizzati 2021-26 (DD −20%) ma non l'intera distribuzione di code
|
||||||
|
possibili, e usa spread calmi. La f reale in settimana di crash e' < 1 e lo spread esplode.
|
||||||
|
|
||||||
|
**Verdetto aggiornato:** lo sleeve income e' piu' solido di quanto temessi sul *premio* (il VRP +
|
||||||
|
skew e' reale e generoso), ma resta una strategia short-vol il cui rischio vero e' la **coda** e la
|
||||||
|
**liquidita' di roll nello stress**, non la magnitudine del premio. Prima del capitale: ripetere lo
|
||||||
|
snapshot nel tempo (specie in regimi di IV alta), misurare lo spread in giornate di stress, e
|
||||||
|
paper-trade su testnet. Il lead per superare il soffitto Sharpe ~1.3 (aggiungere il VRP a TP01)
|
||||||
|
resta valido e ora meglio quantificato.
|
||||||
|
|||||||
@@ -0,0 +1,187 @@
|
|||||||
|
"""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()
|
||||||
Reference in New Issue
Block a user