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>
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user