Files
PythagorasGoal/scripts/research/options_vrp_v2.py
T
Adriano 75e1aacd19 research: analisi strategie FinanceOld + VRP v2 (defined-risk spread + gate IV-rank)
Analisi 4 progetti FinanceOld. Solo il filone opzioni-VRP backtestabile sui dati
certificati (funding-arb senza dati storici; Polybot ticks corrotti/3gg/edge=latenza).

VRP v2 porta 3 idee di OptionsAgent nel framework, causale + fee-aware:
- put credit spread (rischio definito): worst-week -16.6%->-7.4%, DD 33%->21%
- gate IV-rank>0.30: ribalta HOLD-OUT da -0.25 a +0.28 (alpha = filtro regime)
- COMBO f=1.0: FULL Sh 1.10, HOLD 0.60, DD 12%, positiva/piatta ogni anno
- blend TP01 70/30 -> Sh 1.00, DD 7% (corr +0.07)

Lead quantificato, non deploy (premio modellato ATM, serve f di stress reale).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 11:16:59 +02:00

181 lines
9.1 KiB
Python

"""OPTIONS VRP v2 — migliora lo sleeve short-vol con le idee di FinanceOld/OptionsAgent.
Baseline (options_vrp_lab): vendita put NUDA settimanale delta -0.28, premio BS su DVOL reale.
f=1.0 -> Sh 0.71, DD 33%, worst-week -26%, HOLD-OUT Sh 0.04 (muore OOS). Il rischio e' la CODA.
OptionsAgent (Bear Call Spread + VIX hedge su IWM) porta 3 idee testabili qui:
(A) RISCHIO DEFINITO: invece della put nuda, PUT CREDIT SPREAD (vendi put delta -0.28, COMPRI put
piu' OTM delta -0.10). Cap la coda: max perdita = width - premio netto. Capitale = width (margine
reale di un defined-risk). Lo Sharpe e' scale-free; DD/worst-week sul width (capitale vero a rischio).
(B) GATE VRP/IV-RANK: vendi vol SOLO quando e' ricca. Gate causale su:
- vrp: DVOL[i] - RV30(causale) > 0 (premio > vol realizzata recente)
- ivr: IV-rank = percentile espandente di DVOL[i] in DVOL[:i] > soglia
"Solo se IV Rank > 30%" e' una delle 5 condizioni d'ingresso di OptionsAgent.
(C) CRASH-SKIP: vai flat se DVOL gia' esploso sopra un percentile alto (vol-spike = NO-GO, come
"VIX>35 -> NO-GO" di OptionsAgent). Evita di vendere nel pieno del crash.
Tutto CAUSALE: strike/premio/gate usano solo dati <= sell-date; payoff realizza a scadenza sui prezzi
certificati. Fee Deribit opzioni: 0.03% del NOTIONAL per gamba (cap 12.5% del premio) -> qui modellate
come costo per-trade sul premio. NON deploy: lead quantificato e onesto.
uv run python scripts/research/options_vrp_v2.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 scripts.research.options_vrp_lab import bs_put, strike_from_delta, load_series, m_weekly, per_year
HOLDOUT = pd.Timestamp("2025-01-01", tz="UTC")
WK_PER_YEAR = 365.25 / 7.0
# fee Deribit opzioni: 0.0003 * spot per contratto, cap 12.5% del premio. Per uno spread sono 2 gambe.
# Modellata come frazione del premio netto incassato (conservativa: usa il cap come ordine di grandezza).
FEE_FRAC_OF_PREMIUM = 0.125 # 12.5% del premio netto, per ROUND-TRIP delle gambe (worst-case del cap)
def _rv30(px: np.ndarray, i: int) -> float:
"""RV annualizzata causale dagli ultimi 30 rendimenti giornalieri (fino a i incluso)."""
if i < 31:
return np.nan
r = np.diff(np.log(px[i - 30:i + 1]))
return float(np.std(r) * np.sqrt(365.25))
def _ivrank(dv: np.ndarray, i: int) -> float:
"""IV-rank causale: percentile di dv[i] nella storia espandente dv[:i]."""
if i < 60:
return np.nan
hist = dv[:i]
return float((hist < dv[i]).mean())
def vrp_spread_weekly(asset, short_delta=-0.28, long_delta=-0.10, f=1.0, tenor_d=7,
defined_risk=True, gate_vrp=False, gate_ivr=0.0, crash_skip=1.01,
with_fee=True):
"""Vendita settimanale di put credit spread (o nuda se defined_risk=False), con gate causali.
Ritorna serie di rendimenti settimanali su CAPITALE A RISCHIO (width per lo spread, K per la nuda)."""
J = load_series(asset)
px = J["px"].values; dv_pct = J["dvol"].values; dv = dv_pct / 100.0; idx = J.index
n = len(px); T = tenor_d / 365.25
rets = {}
i = 60 # serve storia per RV/IV-rank
while i + tenor_d < n:
S0 = px[i]; sig = dv[i]
# --- GATE causali (decisi a sell-date) ---
skip = False
if gate_vrp:
rv = _rv30(px, i)
if not np.isnan(rv) and (sig - rv) <= 0: # VRP non positivo -> non vendere
skip = True
if gate_ivr > 0:
ivr = _ivrank(dv, i)
if not np.isnan(ivr) and ivr < gate_ivr: # IV troppo bassa -> non vendere
skip = True
if crash_skip < 1.0:
ivr = _ivrank(dv, i)
if not np.isnan(ivr) and ivr > crash_skip: # vol gia' esplosa -> NO-GO
skip = True
if skip:
rets[idx[i + tenor_d]] = 0.0 # flat: nessun rischio quella settimana
i += tenor_d
continue
# --- struttura ---
Ks = strike_from_delta(S0, T, sig, short_delta) # put venduta
prem_s = bs_put(S0, Ks, T, sig) * f
S1 = px[i + tenor_d]
if defined_risk:
Kl = strike_from_delta(S0, T, sig, long_delta) # put comprata (piu' OTM, strike piu' basso)
prem_l = bs_put(S0, Kl, T, sig) * f
net_prem = prem_s - prem_l
width = Ks - Kl
payoff = max(0.0, Ks - S1) - max(0.0, Kl - S1) # quanto pago netto a scadenza
pnl = net_prem - payoff
cap = Ks # cash-secured: stesso capitale del baseline nudo -> DD/worst comparabili,
# il long wing CAPPA la coda (la differenza dal nudo e' solo la coda tagliata)
else:
pnl = prem_s - max(0.0, Ks - S1)
cap = Ks
net_prem = prem_s
if with_fee:
pnl -= FEE_FRAC_OF_PREMIUM * abs(net_prem)
rets[idx[i + tenor_d]] = pnl / cap
i += tenor_d
return pd.Series(rets)
def book(fn, **kw):
rB = fn("BTC", **kw); rE = fn("ETH", **kw)
return pd.concat({"B": rB, "E": rE}, axis=1, join="inner").mean(axis=1)
def report(name, b):
full = m_weekly(b); ho = m_weekly(b[b.index >= HOLDOUT])
worst = b.min(); active = float((b != 0).mean())
py = per_year(b)
print(f" {name:<34} FULL Sh {full['sh']:>5.2f} CAGR {full['cagr']*100:>+4.0f}% DD {full['dd']*100:>3.0f}% "
f"worst {worst*100:>+5.1f}% | HOLD Sh {ho['sh']:>5.2f} | attivo {active*100:>3.0f}%")
return full, ho, py
def main():
print("=" * 104)
print(" OPTIONS VRP v2 — defined-risk spread + gate VRP/IV-rank + crash-skip (idee OptionsAgent)")
print("=" * 104)
print(" Fee opzioni Deribit modellate: 12.5%% del premio netto per round-trip (cap del fee reale).\n")
print(" (0) BASELINE — put NUDA delta -0.28 (riproduce options_vrp_lab, ora CON fee)")
report("naked f=1.0 (no gate)", book(vrp_spread_weekly, defined_risk=False, f=1.0))
report("naked f=1.29 (reale-calm)", book(vrp_spread_weekly, defined_risk=False, f=1.29))
print("\n (1) RISCHIO DEFINITO — put credit spread -0.28/-0.10 (cap coda), capitale=width")
for f in (1.0, 1.29):
report(f"spread f={f}", book(vrp_spread_weekly, defined_risk=True, f=f))
print("\n (2) + GATE VRP>0 (vendi solo se DVOL>RV30 causale)")
for f in (1.0, 1.29):
report(f"spread+vrp f={f}", book(vrp_spread_weekly, defined_risk=True, f=f, gate_vrp=True))
print("\n (3) + GATE IV-RANK > 0.30 (vendi solo vol ricca; cond. d'ingresso OptionsAgent)")
for f in (1.0, 1.29):
report(f"spread+ivr30 f={f}", book(vrp_spread_weekly, defined_risk=True, f=f, gate_ivr=0.30))
print("\n (4) + CRASH-SKIP IV-rank>0.90 (NO-GO se vol gia' esplosa)")
for f in (1.0, 1.29):
report(f"spread+crashskip f={f}", book(vrp_spread_weekly, defined_risk=True, f=f, crash_skip=0.90))
print("\n (5) COMBO — spread + vrp + ivr30 + crash-skip (tutti i filtri, f=1.0 conservativo)")
full, ho, py = report("COMBO f=1.0", book(vrp_spread_weekly, defined_risk=True, f=1.0,
gate_vrp=True, gate_ivr=0.30, crash_skip=0.90))
print(" per-anno: " + " ".join(f"{y}:{v*100:+.0f}%" for y, v in py.items()))
full, ho, py = report("COMBO f=1.29", book(vrp_spread_weekly, defined_risk=True, f=1.29,
gate_vrp=True, gate_ivr=0.30, crash_skip=0.90))
print(" per-anno: " + " ".join(f"{y}:{v*100:+.0f}%" for y, v in py.items()))
# contributo al portafoglio TP01
print("\n (6) CORRELAZIONE + CONTRIBUTO vs TP01 (COMBO f=1.0)")
from src.portfolio.sleeves import tp01_sleeve
tp = tp01_sleeve().daily()
tp_wk = (1 + tp).resample("7D").prod() - 1
opt = book(vrp_spread_weekly, defined_risk=True, f=1.0, gate_vrp=True, gate_ivr=0.30, crash_skip=0.90)
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}")
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: capitale=strike corto (cash-secured) per entrambe -> DD/worst comparabili al nudo.")
print(" Il defined-risk CAPPA la coda (-16.6%->-7.4% worst, DD 33%->14-21%) RIDUCENDO la dipendenza")
print(" dal f di stress (la coda e' tagliata per costruzione). Il gate IV-rank e' l'alpha: vendere")
print(" solo vol ricca (58%% delle settimane) ribalta l'HOLD-OUT da -0.25 a +0.28 (f=1.0). Premio")
print(" MODELLATO su DVOL ATM (no skew). Lead quantificato, non deploy (serve catena reale + f di stress).")
if __name__ == "__main__":
main()