Files
Adriano Dal Pastro 8f9ce89039 research: imposta sleeve OPZIONI VRP — infrastruttura + prima validazione (LEAD reale, non deploy)
fetch_dvol.py: storia DVOL (IV Deribit) BTC/ETH 2021-2026 -> data/raw/dvol_*. options_vrp_lab.py:
backtest CSP settimanale, premio BS su DVOL reale + calibrazione f (skew/spread), payoff sul path
realizzato, causale; gauntlet (VRP, sweep f/delta, per-anno, worst-weeks, corr+contributo vs TP01).

Esiti (book 50/50 put delta-0.28): VRP reale (BTC IV>RV 78% del tempo). Sharpe DIPENDE da f:
0.71 conservativo (IV-ATM) -> 1.70 a f=1.29 (skew reale calm). CODA severa (DD 30-33%, settimane
-15..-26% su LUNA/FTX/crash; 2022 -9%, 2026-YTD -14%). Scorrelato a TP01 (+0.07) -> migliora il
portafoglio anche a premio conservativo (TP01 70%+OPT 30%: Sh settimanale 0.71->0.97).

VERDETTO: lead reale e diversificante, MA premio modellato (non catena reale) + calibrazione
ottimistica + coda short-vol non catturata nello stress. Regola: mai short-vol da modello in
deploy. NON aggiunto. Portafoglio invariato TP01 70% + XS01 30%. Prossimo: accumulo quote reali
multi-regime + stress crash + daily-MTM + paper testnet. Diario 2026-06-19-options-vrp-lab.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 20:30:08 +00:00

151 lines
7.1 KiB
Python

"""OPTIONS VRP LAB — sleeve income: vendita put settimanali (CSP) che incassa il VRP (IV>RV).
Aggira il muro "niente catena storica gratis" come crypto_backtest: prezza le put con Black-Scholes
sulla DVOL REALE (IV storica Deribit, data/raw/dvol_*.parquet) + CALIBRAZIONE su quote reali
(fattore f: la verifica su quote reali ha trovato premio reale ~1.29x il modellato a IV-ATM per via
dello skew, al netto dello spread). Payoff sul path REALIZZATO dei prezzi certificati. Causale: la
decisione (strike/premio) usa solo dati <= sell-date; il payoff realizza a scadenza.
Onesto: e' SHORT-VOL, il rischio vero e' la CODA (crash). Riporto worst-weeks (LUNA/FTX), per-anno,
sweep su f (sensitivity del premio reale) e delta. NON e' un deploy: e' la prima validazione del lead.
uv run python scripts/research/options_vrp_lab.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, pandas as pd
from scipy.stats import norm
from scripts.analysis.research_lab import load_tf
HOLDOUT = pd.Timestamp("2025-01-01", tz="UTC")
WK_PER_YEAR = 365.25 / 7.0
def bs_put(S, K, T, sig):
if T <= 0 or sig <= 0:
return max(K - S, 0.0)
d1 = (np.log(S / K) + 0.5 * sig ** 2 * T) / (sig * np.sqrt(T))
d2 = d1 - sig * np.sqrt(T)
return K * norm.cdf(-d2) - S * norm.cdf(-d1) # r=0
def strike_from_delta(S, T, sig, target_delta=-0.28):
# delta_put = -N(-d1) = target -> d1 = -N^{-1}(-target)
d1 = -norm.ppf(-target_delta)
return S * np.exp(0.5 * sig ** 2 * T - d1 * sig * np.sqrt(T))
def load_series(asset):
px = load_tf(asset, "1d")
s = pd.Series(px["close"].values.astype(float), index=pd.to_datetime(px["timestamp"], unit="ms", utc=True))
dv = pd.read_parquet(PROJECT_ROOT / "data" / "raw" / f"dvol_{asset.lower()}.parquet")
d = pd.Series(dv["close"].values.astype(float), index=pd.to_datetime(dv["timestamp"], unit="ms", utc=True))
J = pd.concat({"px": s, "dvol": d}, axis=1, join="inner").sort_index().dropna()
return J
def put_sell_weekly(asset, delta=-0.28, f=1.0, tenor_d=7):
"""Vendita CSP settimanale. Ritorna serie di rendimenti SETTIMANALI (su collaterale K) indicizzata
alla data di scadenza. Causale: strike/premio da DVOL e prezzo a sell-date; payoff a scadenza."""
J = load_series(asset)
px = J["px"].values; dv = J["dvol"].values / 100.0; idx = J.index
n = len(px); T = tenor_d / 365.25
rets = {}
i = 30
while i + tenor_d < n:
S0 = px[i]; sig = dv[i]
K = strike_from_delta(S0, T, sig, delta)
prem = bs_put(S0, K, T, sig) * f
S1 = px[i + tenor_d]
pnl = prem - max(0.0, K - S1) # short put: incassi premio, paghi se finisce ITM
rets[idx[i + tenor_d]] = pnl / K # rendimento su collaterale cash-secured
i += tenor_d
return pd.Series(rets)
def m_weekly(r):
r = r.dropna()
if len(r) < 3 or r.std() == 0:
return dict(sh=0, cagr=0, dd=0, n=len(r))
eq = np.cumprod(1 + r.values); pk = np.maximum.accumulate(eq)
yrs = len(r) / WK_PER_YEAR
return dict(sh=float(r.mean() / r.std() * np.sqrt(WK_PER_YEAR)),
cagr=float(eq[-1] ** (1 / yrs) - 1) if yrs > 0 and eq[-1] > 0 else 0,
dd=float(np.max((pk - eq) / pk)), n=len(r))
def per_year(r):
out = {}
for y, g in r.groupby(r.index.year):
eq = np.cumprod(1 + g.values)
out[int(y)] = float(eq[-1] - 1)
return out
def main():
print("=" * 96)
print(" OPTIONS VRP LAB — vendita put settimanali (CSP), premio BS su DVOL reale + calibrazione f")
print("=" * 96)
# contesto VRP: IV (DVOL) vs RV realizzata
for a in ("BTC", "ETH"):
J = load_series(a)
rv = J["px"].pct_change().rolling(30).std() * np.sqrt(365.25) * 100
vrp = (J["dvol"] - rv).dropna()
print(f" {a}: DVOL media {J['dvol'].mean():.0f}% | RV30 media {rv.mean():.0f}% | VRP media {vrp.mean():+.1f} pt, >0 nel {100*(vrp>0).mean():.0f}% del tempo")
print("\n (1) SWEEP CALIBRAZIONE f (delta -0.28, weekly) — book 50/50 BTC+ETH")
print(f" {'f':>6}{'Sh':>7}{'CAGR':>8}{'maxDD':>8}{'worst-wk':>10}")
for f in (0.70, 0.85, 1.0, 1.15, 1.29):
rB = put_sell_weekly("BTC", f=f); rE = put_sell_weekly("ETH", f=f)
book = pd.concat({"B": rB, "E": rE}, axis=1, join="inner").mean(axis=1)
mm = m_weekly(book); worst = book.min()
tag = " <- reale(calm)" if f == 1.29 else (" <- conservativo" if f == 1.0 else "")
print(f" {f:>6.2f}{mm['sh']:>7.2f}{mm['cagr']*100:>+7.0f}%{mm['dd']*100:>7.1f}%{worst*100:>+9.1f}%{tag}")
print("\n (2) SWEEP DELTA (f=1.0 conservativo) — book 50/50")
print(f" {'delta':>7}{'Sh':>7}{'CAGR':>8}{'maxDD':>8}")
for dl in (-0.15, -0.28, -0.40):
rB = put_sell_weekly("BTC", delta=dl); rE = put_sell_weekly("ETH", delta=dl)
book = pd.concat({"B": rB, "E": rE}, axis=1, join="inner").mean(axis=1)
mm = m_weekly(book)
print(f" {dl:>7.2f}{mm['sh']:>7.2f}{mm['cagr']*100:>+7.0f}%{mm['dd']*100:>7.1f}%")
# config centrale: delta -0.28, f=1.0 (conservativo) e f=1.29 (reale misurato)
print("\n (3) PER ANNO + WORST WEEKS (delta -0.28, book 50/50) — il rischio e' la CODA")
for f in (1.0, 1.29):
rB = put_sell_weekly("BTC", f=f); rE = put_sell_weekly("ETH", f=f)
book = pd.concat({"B": rB, "E": rE}, axis=1, join="inner").mean(axis=1)
py = per_year(book)
worst = book.nsmallest(5)
print(f"\n f={f}: per-anno " + " ".join(f"{y}:{v*100:+.0f}%" for y, v in py.items()))
print(f" worst weeks: " + " ".join(f"{d.date()}:{v*100:.0f}%" for d, v in worst.items()))
full = m_weekly(book); ho = m_weekly(book[book.index >= HOLDOUT])
print(f" FULL Sh {full['sh']:.2f} CAGR {full['cagr']*100:+.0f}% DD {full['dd']*100:.0f}% | HOLD-OUT Sh {ho['sh']:.2f}")
# correlazione e contributo vs TP01 (resampling settimanale)
print("\n (4) CORRELAZIONE + CONTRIBUTO vs TP01 (settimanale; f=1.0 conservativo)")
from src.portfolio.sleeves import tp01_sleeve
tp = tp01_sleeve().daily()
tp_wk = (1 + tp).resample("7D").prod() - 1
rB = put_sell_weekly("BTC"); rE = put_sell_weekly("ETH")
opt = pd.concat({"B": rB, "E": rE}, axis=1, join="inner").mean(axis=1)
opt_wk = opt.copy(); opt_wk.index = opt_wk.index.to_period("W").to_timestamp()
tp_wk2 = tp_wk.copy(); tp_wk2.index = tp_wk2.index.to_period("W").to_timestamp()
Jc = pd.concat({"tp": tp_wk2, "opt": opt_wk}, axis=1, join="inner").dropna()
corr = float(Jc["tp"].corr(Jc["opt"])) if len(Jc) > 5 else float("nan")
print(f" corr settimanale opt vs TP01 = {corr:+.2f} (atteso ~0.2)")
for w in (0.3, 0.5):
comb = (1 - w) * Jc["tp"] + w * Jc["opt"]
mt = m_weekly(Jc["tp"]); mc = m_weekly(comb)
print(f" TP01 {1-w:.0%} + OPT {w:.0%}: Sh {mc['sh']:.2f} (TP01-solo {mt['sh']:.2f}) DD {mc['dd']*100:.0f}%")
print("\n NB onesto: short-vol -> guarda i worst-weeks e gli anni di crash. Premio MODELLATO; il")
print(" rischio coda/roll in stress NON e' pienamente catturato. Lead, non deploy.")
if __name__ == "__main__":
main()