"""VRP01 + GESTIONE ATTIVA (test del doc 'strategia-credit-spread-eth', 2026-06-20). Innesta sul put credit spread di VRP01 le regole di gestione intra-trade del documento: - profit-take 50% del credito - stop-loss stretto 1.5x il credito (debito di chiusura) - VOL-STOP: chiudi se DVOL sale >=10 punti dall'apertura (regola crypto-specifica, NUOVA) - delta-exit: chiudi se |delta| dello short put >= 0.30 (niente rolling/difesa) - time-stop 7 DTE Confronto A/B ONESTO sugli STESSI ingressi gated (VRP>0 + IV-rank>0.30) e dati certificati: BASE = hold-to-expiry (come VRP01) vs MANAGED = stesso trade con la gestione attiva. Il MTM giornaliero dello spread usa BS sul path certificato + DVOL reale (causale: decisione al giorno j con dati <= j). CAVEAT invariato: premio MODELLATO su DVOL ATM (no skew), nessun fill di stress reale -> LEAD, non deploy. Qui misuriamo solo SE la gestione attiva taglia la coda. uv run python scripts/research/options_vrp_managed.py """ from __future__ import annotations import sys from pathlib import Path import numpy as np import pandas as pd from scipy.stats import norm sys.path.insert(0, str(Path(__file__).resolve().parents[2])) from src.data.downloader import load_data from src.strategies.trend_portfolio import resample_1d from src.portfolio.portfolio import to_daily, metrics, HOLDOUT from src.portfolio.sleeves import _bs_put, _strike_from_delta, VRP_CFG, _HL_DIR CFG = dict(VRP_CFG) # short_delta -0.28, long_delta -0.10, f 1.0, gate_ivr 0.30, crash_skip 0.90, fee_frac 0.125 def _put_delta_mag(S, K, T, sig): if T <= 0 or sig <= 0: return 1.0 if S < K else 0.0 d1 = (np.log(S / K) + 0.5 * sig ** 2 * T) / (sig * np.sqrt(T)) return float(norm.cdf(-d1)) # |delta| dello short put (=N(-d1)) def simulate(asset: str, tenor_d: int, mode: str = "hte"): """mode: 'hte' hold-to-expiry | 'full' tutte le regole | 'volstop' solo vol-stop DVOL+10 (+PT50). Ritorna (serie rendimenti per-trade indicizzata alla data di uscita, dict conteggio exit).""" manage = mode != "hte" full = mode == "full" df = resample_1d(load_data(asset, "1h")) s = pd.Series(df["close"].values.astype(float), index=pd.to_datetime(df["datetime"])) if s.index.tz is None: s.index = s.index.tz_localize("UTC") dv = pd.read_parquet(_HL_DIR / 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() px = J["px"].values dvf = J["dvol"].values / 100.0 idx = J.index n = len(px) tn = tenor_d f, fee = CFG["f"], CFG["fee_frac"] rets, exits = {}, {} i = 60 while i + tn < n: S0, sig0 = px[i], dvf[i] # --- gates d'ingresso identici a VRP01 (causali) --- skip = False if i >= 31: rv = np.std(np.diff(np.log(px[i - 30:i + 1]))) * np.sqrt(365.25) if (sig0 - rv) <= 0: # VRP>0 skip = True if not skip and i >= 60: ivr = float((dvf[:i] < dvf[i]).mean()) # IV-rank espandente causale if ivr < CFG["gate_ivr"] or ivr > CFG["crash_skip"]: skip = True if skip: i += tn continue T0 = tn / 365.25 Ks = _strike_from_delta(S0, T0, sig0, CFG["short_delta"]) Kl = _strike_from_delta(S0, T0, sig0, CFG["long_delta"]) net_prem = (_bs_put(S0, Ks, T0, sig0) - _bs_put(S0, Kl, T0, sig0)) * f if net_prem <= 0: i += tn continue reason, pnl, exit_j = None, None, i + tn if manage: for j in range(i + 1, i + tn): # giorni STRETTAMENTE prima della scadenza Trem = (i + tn - j) / 365.25 Sj, sigj = px[j], dvf[j] sval = _bs_put(Sj, Ks, Trem, sigj) - _bs_put(Sj, Kl, Trem, sigj) # MTM dello spread if sval <= 0.5 * net_prem: reason, pnl, exit_j = "PT50", net_prem - sval, j; break if (sigj - sig0) >= 0.10: # VOL-STOP (la regola crypto nuova del doc) reason, pnl, exit_j = "VOLSTOP", net_prem - sval, j; break if full and sval >= 1.5 * net_prem: reason, pnl, exit_j = "SL150", net_prem - sval, j; break if full and _put_delta_mag(Sj, Ks, Trem, sigj) >= 0.30: reason, pnl, exit_j = "DELTA", net_prem - sval, j; break if full and (i + tn - j) <= 7: reason, pnl, exit_j = "TIME7", net_prem - sval, j; break if reason is None: # scadenza S1 = px[i + tn] payoff = max(0.0, Ks - S1) - max(0.0, Kl - S1) pnl, reason, exit_j = net_prem - payoff, "expiry", i + tn pnl -= fee * abs(net_prem) # fee d'ingresso (su entrambe le gambe via net_prem) if reason != "expiry": pnl -= fee * abs(net_prem) # fee di chiusura anticipata (ricompro lo spread) rets[idx[exit_j]] = pnl / Ks exits[reason] = exits.get(reason, 0) + 1 i += tn return pd.Series(rets).sort_index(), exits def daily(series): if series.empty: return series days = pd.date_range(series.index.min().normalize(), series.index.max().normalize(), freq="1D", tz="UTC") out = pd.Series(0.0, index=days) out.loc[series.index.normalize()] = series.values return out def report(label, perTrade): dl = to_daily(daily(perTrade)) m = metrics(dl) mh = metrics(dl[dl.index >= HOLDOUT]) wins = float((perTrade > 0).mean()) * 100 worst = float(perTrade.min()) * 100 print(f" {label:<22s} n={len(perTrade):>3d} win={wins:>4.0f}% ret={m['ret']*100:>+6.0f}% " f"Sh={m['sharpe']:>5.2f} DD={m['maxdd']*100:>4.1f}% HOLD Sh={mh['sharpe']:>+5.2f} " f"worst-trade={worst:>+5.1f}%") return dl def main(): print("=" * 100) print(" VRP01 hold-to-expiry vs GESTIONE ATTIVA (vol-stop DVOL+10, SL 1.5x, PT50, delta-exit, 7DTE)") print(" Stessi ingressi gated (VRP>0 + IV-rank>0.30), dati certificati, premio MODELLATO su DVOL (no skew)") print("=" * 100) combos = {} for asset in ("ETH", "BTC"): print(f"\n--- {asset} ---") report("VRP01 live (7d HtE)", simulate(asset, 7, "hte")[0]) # riferimento live # confronto equo a tenor 14 (range del doc), STESSI ingressi b14, _ = simulate(asset, 14, "hte") v14, exv = simulate(asset, 14, "volstop") # SOLO vol-stop (la regola nuova) m14, exm = simulate(asset, 14, "full") # tutte le regole del doc report("14d hold-to-expiry", b14) report("14d +vol-stop only", v14); print(f" exit volstop: {exv}") report("14d FULL managed", m14); print(f" exit full: {exm}") combos[asset] = dict(base14=daily(b14), vol14=daily(v14), man14=daily(m14)) # combo 50/50 BTC+ETH (come lo sleeve VRP01) — il confronto che conta per il portafoglio print("\n--- COMBO 50/50 BTC+ETH (sleeve-level) ---") for tag, key in (("14d hold-to-expiry", "base14"), ("14d +vol-stop only", "vol14"), ("14d FULL managed", "man14")): J = pd.concat({"B": combos["BTC"][key], "E": combos["ETH"][key]}, axis=1, join="outer").fillna(0.0) combo = to_daily(0.5 * J["B"] + 0.5 * J["E"]) m, mh = metrics(combo), metrics(combo[combo.index >= HOLDOUT]) print(f" {tag:<22s} Sh={m['sharpe']:>5.2f} DD={m['maxdd']*100:>4.1f}% ret={m['ret']*100:>+6.0f}% " f"HOLD Sh={mh['sharpe']:>+5.2f}") print("\n Lettura: la gestione attiva VALE se taglia maxDD e worst-trade SENZA distruggere Sharpe/ritorno.") print(" Caveat invariato: premio modellato su DVOL ATM (no skew) + nessun fill di stress reale -> LEAD, non deploy.") if __name__ == "__main__": main()