"""Gate del CATASTROPHE-CAP auto-finanziato (collar standing) sullo sleeve ETH no-SL. Tesi: lo sleeve ETH no-SL ha la coda da crash (un long-fade puo' perdere -50/-65% in un gap). Un COLLAR standing rollato mensilmente — put lunga ~13% OTM finanziata da call corta ~10% OTM — cappa quella coda a premio netto ~zero (validato sui premi REALI di cerbero-bite: put -13%≈1.0%/m IV55, call +10%≈1.05%/m IV49). Pricing BS calibrato sul reale: skew_put 1.12, skew_call 1.0. Caveat: il collar aggiunge delta SHORT-ETH con dead-zone -p/+c -> cappa anche l'upside; nei mesi tranquilli (ETH dentro la banda) costa ~zero. Il gate dice se aiuta netto. uv run python scripts/analysis/eth_collar_gate.py """ from __future__ import annotations import sys from pathlib import Path import numpy as np import pandas as pd PROJECT_ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(PROJECT_ROOT)) from scripts.analysis.explore_lab import get_df from scripts.analysis.combine_portfolio import IDX, SPLIT, OOS_DATE, metrics, port_returns, _norm from scripts.analysis.option_overlay_lab import dvol_for, bs_put, bs_call from scripts.analysis.mr02eth_port06_gate import ( gen_donchian_base, build_trades, build_trades_exit16, daily_equity, port_metrics, CAPS) from src.portfolio.sleeves import all_sleeve_equities HY = 24 * 365.0 def collar_daily_returns(df, dvol, p_otm=0.13, c_otm=0.10, skew_put=1.12, skew_call=1.0, roll_h=24 * 30) -> pd.Series: """Collar standing rollato ogni roll_h ore. Ritorna la SERIE di rendimenti GIORNALIERI (frazione del notional collar): MTM = d(intrinseco) - theta (premio netto amortizzato).""" c = df["close"].values; ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True) n = len(c) val = np.zeros(n) # valore collar (frac notional) marcato a intrinseco - premio residuo T = roll_h / HY k = 0 while k < n - 1: S0 = c[k]; sig = dvol[k] if not np.isnan(dvol[k]) else 0.6 Kp = S0 * (1 - p_otm); Kc = S0 * (1 + c_otm) prem = bs_put(S0, Kp, T, sig * skew_put) / S0 - bs_call(S0, Kc, T, sig * skew_call) / S0 end = min(k + roll_h, n - 1) span = end - k for j in range(k, end + 1): intr = (max(Kp - c[j], 0.0) - max(c[j] - Kc, 0.0)) / S0 frac_elapsed = (j - k) / span if span else 1.0 val[j] = intr - prem * (1 - frac_elapsed) # premio pagato up-front, amortizzato a 0 a scadenza k = end s = pd.Series(val, index=ts) daily = s.resample("1D").last().reindex(IDX).ffill().bfill() return daily.diff().fillna(0.0) # rendimento giornaliero (frac notional) def combine(fade_eq: pd.Series, collar_dr: pd.Series, hedge_frac: float) -> pd.Series: """sleeve = fade no-SL + hedge_frac * collar. Combina i rendimenti giornalieri.""" fr = fade_eq.pct_change().fillna(0.0) return _norm((1 + fr + hedge_frac * collar_dr).cumprod()) def crash_audit(df, dvol, p_otm, c_otm, hedge_frac): """P&L del collar nei mesi di crollo ETH peggiori (frac notional).""" cr = collar_daily_returns(df, dvol, p_otm=p_otm, c_otm=c_otm) # ETH daily ret mensile cdf = pd.Series(df["close"].values, index=pd.to_datetime(df["timestamp"], unit="ms", utc=True)).resample("1D").last().reindex(IDX).ffill() mret = cdf.resample("30D").last().pct_change() collar_m = (1 + cr).resample("30D").apply(lambda x: x.prod()) - 1 worst = mret.nsmallest(5) print(f" {'mese (fine)':>12}{'ETH 30g%':>10}{'collar P&L%':>13}") for t, r in worst.items(): cm = collar_m.reindex([t], method="nearest").iloc[0] * hedge_frac * 100 print(f" {str(t.date()):>12}{r*100:>9.0f}%{cm:>12.1f}%") def main(): print("=" * 96) print(f" GATE collar standing (catastrophe-cap) sullo sleeve ETH no-SL | OOS da {OOS_DATE}") print("=" * 96) df = get_df("ETH", "1h"); dvol = dvol_for(df, "ETH") eq = dict(all_sleeve_equities()) ids = [k for k in eq if k in {"MR01_BTC","MR02_BTC","MR07_BTC","MR01_ETH","MR02_ETH","MR07_ETH", "DIP01_BTC","TR01_basket","ROT02_rot","PR_ETHBTC","PR_LTCETH","PR_ADAETH","PR_BTCLTC","PR_ETHSOL","TSM01","SH_BTC","SH_ETH"}] base_ents = gen_donchian_base(df, n=20, sl_atr=2.0, trend_max=3.0) nosl_ents = gen_donchian_base(df, n=20, sl_atr=2.0, trend_max=3.0, use_sl=False) def pm(ce): m = dict(eq); m["MR02_ETH"] = ce; return port_metrics(m, ids) f_l, o_l = pm(daily_equity(build_trades_exit16(base_ents, df, sl_confirm=0.5), df)) fade_nosl = daily_equity(build_trades(nosl_ents, df), df) f0, o0 = pm(fade_nosl) print(f"\n {'sleeve ETH':<30s}{'FULL Sh':>8s}{'FULL DD':>8s} |{'OOS Sh':>8s}{'OOS DD':>8s}") print(" " + "-" * 78) print(f" {'LIVE EXIT-16 (rif)':<30s}{f_l['sharpe']:>8.2f}{f_l['dd']:>8.2f} |{o_l['sharpe']:>8.2f}{o_l['dd']:>8.2f}") print(f" {'no-SL nudo':<30s}{f0['sharpe']:>8.2f}{f0['dd']:>8.2f} |{o0['sharpe']:>8.2f}{o0['dd']:>8.2f}") configs = [ ("put13/call10 hf0.45", 0.13, 0.10, 0.45), ("put13/call10 hf0.30", 0.13, 0.10, 0.30), ("put15/call12 hf0.45", 0.15, 0.12, 0.45), ("put20/call15 hf0.45", 0.20, 0.15, 0.45), ("put13/call08 hf0.45", 0.13, 0.08, 0.45), ] rows = [] for name, p, cc, hf in configs: cr = collar_daily_returns(df, dvol, p_otm=p, c_otm=cc) ce = combine(fade_nosl, cr, hf) f_c, o_c = pm(ce) rows.append((name, p, cc, hf, f_c, o_c)) print(f" {'no-SL + '+name:<30s}{f_c['sharpe']:>8.2f}{f_c['dd']:>8.2f} |{o_c['sharpe']:>8.2f}{o_c['dd']:>8.2f}") print("\n " + "=" * 90) print(f" vs LIVE EXIT-16 (FULL {f_l['sharpe']:.2f}/{f_l['dd']:.2f} OOS {o_l['sharpe']:.2f}/{o_l['dd']:.2f}) e vs no-SL") print(" " + "-" * 90) for name, p, cc, hf, f_c, o_c in rows: print(f" {name:<22s} Δ vsEXIT16 FULL {f_c['sharpe']-f_l['sharpe']:+.2f}/{f_c['dd']-f_l['dd']:+.2f} " f"OOS {o_c['sharpe']-o_l['sharpe']:+.2f}/{o_c['dd']-o_l['dd']:+.2f} | " f"Δ vsNoSL DD {f_c['dd']-f0['dd']:+.2f}") print("\n --- audit crash: P&L collar (hf-scaled) nei 5 mesi ETH peggiori (put13/call10 hf0.45) ---") crash_audit(df, dvol, 0.13, 0.10, 0.45) if __name__ == "__main__": main()