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>
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
"""FETCH storia DVOL (Deribit Volatility Index) — input IV per lo sleeve opzioni VRP.
|
||||
|
||||
DVOL = vol implicita 30d annualizzata di Deribit (l'IV "ATM" del mercato). Public API, no auth.
|
||||
Limite 1000 punti/richiesta -> paginazione all'indietro. Salva data/raw/dvol_<asset>.parquet
|
||||
(colonne: timestamp ms, close = DVOL%). Usato come IV per prezzare BS le opzioni nel backtest VRP;
|
||||
la RV viene dai nostri prezzi certificati. VRP = IV - RV.
|
||||
|
||||
uv run python scripts/research/fetch_dvol.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys, time
|
||||
from pathlib import Path
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
import requests, pandas as pd
|
||||
|
||||
URL = "https://www.deribit.com/api/v2/public/get_volatility_index_data"
|
||||
RAW = PROJECT_ROOT / "data" / "raw"
|
||||
|
||||
|
||||
def fetch(cur, res=86400):
|
||||
end = int(time.time() * 1000)
|
||||
floor = int(pd.Timestamp("2020-06-01", tz="UTC").timestamp() * 1000)
|
||||
rows = {}
|
||||
guard = 0
|
||||
while end > floor and guard < 60:
|
||||
guard += 1
|
||||
r = requests.get(URL, params={"currency": cur, "start_timestamp": floor,
|
||||
"end_timestamp": end, "resolution": res}, timeout=40)
|
||||
data = r.json().get("result", {}).get("data", [])
|
||||
if not data:
|
||||
break
|
||||
for ts, o, h, l, c in data:
|
||||
rows[int(ts)] = float(c)
|
||||
earliest = min(int(x[0]) for x in data)
|
||||
if earliest >= end:
|
||||
break
|
||||
end = earliest - 1
|
||||
if not rows:
|
||||
return pd.DataFrame()
|
||||
df = pd.DataFrame(sorted(rows.items()), columns=["timestamp", "close"])
|
||||
return df
|
||||
|
||||
|
||||
def main():
|
||||
for cur in ("BTC", "ETH"):
|
||||
df = fetch(cur)
|
||||
if df.empty:
|
||||
print(f"{cur}: VUOTO"); continue
|
||||
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||
df.to_parquet(RAW / f"dvol_{cur.lower()}.parquet", index=False)
|
||||
print(f"{cur}: {len(df)} giorni [{ts.iloc[0].date()} -> {ts.iloc[-1].date()}] "
|
||||
f"DVOL media {df['close'].mean():.1f} range [{df['close'].min():.1f}, {df['close'].max():.1f}] "
|
||||
f"-> data/raw/dvol_{cur.lower()}.parquet")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,150 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user