"""R0703 VRPIMP-DDOVERLAY — overlay di protezione DD su VRP01 canonico vs NULL DEL DE-LEVERING. FILONE 2 (ondata 2026-07-03): su VRP01 canonico (put credit spread settimanale -0.28/-0.10, gate vrp>0 + ivr>=0.30 + crash-skip 0.90, riprodotto ESATTO dal motore path qui sotto e bridge-validato vs options_vrp_v2.vrp_spread_weekly) si testano 4 famiglie di overlay: (a) EXIT INTRA-SETTIMANA su spike: DVOL sale >X% dall'ingresso, oppure RV5 causale supera m x DVOL d'ingresso -> chiudi lo spread al close del giorno del trigger (MTM BS). (b) STOP-LOSS sul MTM: esci se il valore dello spread raggiunge mult x credito incassato. (c) ALA DI CODA comprata: q put molto piu' OTM (delta -0.05/-0.03) finanziate col credito (ratio/broken-wing-ish), tenute a scadenza. (d) COOLDOWN: salta c settimane di calendario dopo una settimana attiva in perdita. REGOLA VINCOLANTE (lezione TP01xDVOL 2026-06-26): ogni claim di riduzione DD si confronta col NULL DEL DE-LEVERING — lo scaling s della size del baseline che produce lo STESSO maxDD. Lo Sharpe e' scale-invariante => il de-levered ha SEMPRE lo Sharpe del baseline: un overlay "vale" solo se il suo Sharpe batte quello del baseline A PARI maxDD (e regge banda f + multi-cut + DSR). ⚠️ LENS DICHIARATA (in testa, non in fondo): - Granularita' 1d: i trigger (a)/(b) si valutano al CLOSE giornaliero e si esegue a QUEL close -> il gap-through oltre il livello di stop E' DENTRO il fill (onesto), ma lo spike intraday peggiore del close NON e' modellato (lezione SKH01: a 1d lo stop e' una lente, non un fill). - MTM Black-Scholes FLAT sulla DVOL-30g usata per tenor <=7g: in stress il front-end esplode piu' della 30g e lo skew irripidisce -> il MTM avverso e' SOTTOSTIMATO => gli stop scattano piu' tardi e costano piu' del modellato. Percio' banda f {0.6,0.8,1.0,1.3} obbligatoria. - Booking: il pnl di ogni trade (anche se chiuso prima) e' registrato alla data di SCADENZA -> griglia settimanale identica al baseline, Sharpe confrontabile (lente, non timing reale). - Equity settimanale compounding (convenzione VRP01): DD/worst-week su quella lente. - Banda d'ancora (regola 2026-07-02): loop settimanale ancorato a i0=60 -> 7 offset riportati. Regola standing: niente short-vol da modello in deploy — obiettivo = conoscenza/LEAD. Selezione celle SOLO in-sample (pre-2025); hold-out 2025-26 mai usato per scegliere; DSR sul numero totale di celle (14). uv run python scripts/research/r0703_vrpimp_ddoverlay.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)) sys.path.insert(0, str(PROJECT_ROOT / "scripts" / "research" / "alt")) 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 from scripts.research.options_vrp_v2 import vrp_spread_weekly, _ivrank, _rv30 HOLDOUT = pd.Timestamp("2025-01-01", tz="UTC") WK = 365.25 / 7.0 F_SWEEP = (0.6, 0.8, 1.0, 1.3) FEE_FRAC = 0.125 # convenzione VRP01: 12.5% del premio netto per il round-trip base TENOR = 7 def _rv5(px: np.ndarray, j: int) -> float: """RV annualizzata causale dagli ultimi 5 rendimenti giornalieri (fino a j incluso).""" if j < 6: return np.nan r = np.diff(np.log(px[j - 5:j + 1])) return float(np.std(r) * np.sqrt(365.25)) def vrp_path_weekly(asset, f=1.0, overlay=None, i0=60, collect=None): """VRP01 canonico con path MTM giornaliero. overlay=None riproduce vrp_spread_weekly (defined_risk, gate vrp>0 + ivr>=0.30 + crash_skip 0.90) ESATTO (bridge sotto). overlay: ('spike_dvol', x) | ('spike_rv', m) | ('sl', mult) | ('tail', delta, q) | ('cool', c) Ritorna Series settimanale di rendimenti su capitale = strike corto (convenzione VRP01). """ J = load_series(asset) px = J["px"].values; dv = J["dvol"].values / 100.0; idx = J.index n = len(px); T = TENOR / 365.25 kind = overlay[0] if overlay else None rets = {} cool_left = 0 i = i0 while i + TENOR < n: S0 = px[i]; sig = dv[i] # --- cooldown (d): decisione causale dal risultato del trade precedente gia' chiuso --- if kind == "cool" and cool_left > 0: cool_left -= 1 rets[idx[i + TENOR]] = 0.0 i += TENOR continue # --- gate canonici VRP01 (identici a vrp_spread_weekly, NON riottimizzati) --- skip = False rv = _rv30(px, i) if not np.isnan(rv) and (sig - rv) <= 0: skip = True ivr = _ivrank(dv, i) if not np.isnan(ivr) and (ivr < 0.30 or ivr > 0.90): skip = True if skip: rets[idx[i + TENOR]] = 0.0 i += TENOR continue # --- struttura canonica --- Ks = strike_from_delta(S0, T, sig, -0.28) Kl = strike_from_delta(S0, T, sig, -0.10) net_prem = (bs_put(S0, Ks, T, sig) - bs_put(S0, Kl, T, sig)) * f base_fee = FEE_FRAC * abs(net_prem) # --- ala di coda (c): q put extra-OTM a stessa scadenza, pagata col credito --- tail_cost = tail_fee = 0.0; K_tail = None; q = 0 if kind == "tail": _, dlt, q = overlay K_tail = strike_from_delta(S0, T, sig, dlt) prem_t = bs_put(S0, K_tail, T, sig) * f tail_cost = q * prem_t tail_fee = q * min(0.0003 * S0, 0.125 * max(prem_t, 1e-12)) if collect is not None: collect.setdefault("tail_frac", []).append(tail_cost / max(net_prem, 1e-12)) # --- path MTM giornaliero: exit intra-settimana (a)/(b) al primo trigger, a QUEL close --- exited = False if kind in ("spike_dvol", "spike_rv", "sl"): for j in range(i + 1, i + TENOR): T_rem = (i + TENOR - j) / 365.25 trig = False if kind == "spike_dvol": trig = (dv[j] / sig - 1.0) > overlay[1] elif kind == "spike_rv": r5 = _rv5(px, j) trig = (not np.isnan(r5)) and (r5 > overlay[1] * sig) elif kind == "sl": Vj = (bs_put(px[j], Ks, T_rem, dv[j]) - bs_put(px[j], Kl, T_rem, dv[j])) * f trig = Vj >= overlay[1] * net_prem if trig: Vj = (bs_put(px[j], Ks, T_rem, dv[j]) - bs_put(px[j], Kl, T_rem, dv[j])) * f exit_fee = min(0.0006 * px[j], 0.125 * max(Vj, 1e-12)) # 2 gambe buyback pnl = net_prem - Vj - base_fee - exit_fee rets[idx[i + TENOR]] = pnl / Ks # booking a scadenza (lens dichiarata) if collect is not None: collect.setdefault("exits", []).append((idx[j], (net_prem - Vj) / Ks)) exited = True break if not exited: S1 = px[i + TENOR] payoff = max(0.0, Ks - S1) - max(0.0, Kl - S1) pnl = net_prem - payoff - base_fee if kind == "tail": pnl += q * max(0.0, K_tail - S1) - tail_cost - tail_fee rets[idx[i + TENOR]] = pnl / Ks if kind == "cool" and rets[idx[i + TENOR]] < 0: cool_left = overlay[1] i += TENOR return pd.Series(rets) def book(f=1.0, overlay=None, i0=60, collect=None): rB = vrp_path_weekly("BTC", f=f, overlay=overlay, i0=i0, collect=collect) rE = vrp_path_weekly("ETH", f=f, overlay=overlay, i0=i0, collect=collect) return pd.concat({"B": rB, "E": rE}, axis=1, join="inner").mean(axis=1) def maxdd(r): eq = np.cumprod(1 + r.values); pk = np.maximum.accumulate(eq) return float(np.max((pk - eq) / pk)) if len(r) else 0.0 def delever_scale(r_base, dd_target, hi=5.0): """Scaling s della size del baseline che produce maxDD == dd_target (bisezione).""" lo = 0.0 for _ in range(80): mid = (lo + hi) / 2 if maxdd(mid * r_base) > dd_target: hi = mid else: lo = mid return (lo + hi) / 2 def mstats(r): full = m_weekly(r); ho = m_weekly(r[r.index >= HOLDOUT]); isam = m_weekly(r[r.index < HOLDOUT]) return dict(sh=full["sh"], sh_h=ho["sh"], sh_is=isam["sh"], cagr=full["cagr"], dd=full["dd"], worst=float(r.min()) if len(r) else 0.0) def fmt(name, m, extra=""): print(f" {name:<34} ShF {m['sh']:>5.2f} ShH {m['sh_h']:>5.2f} Sh-IS {m['sh_is']:>5.2f} " f"CAGR {m['cagr']*100:>+5.1f}% DD {m['dd']*100:>4.1f}% worst {m['worst']*100:>+5.1f}%{extra}") OVERLAYS = { "a-spike": [("spike_dvol", 0.15), ("spike_dvol", 0.25), ("spike_dvol", 0.40), ("spike_rv", 1.00), ("spike_rv", 1.25)], "b-stoploss": [("sl", 1.5), ("sl", 2.0), ("sl", 3.0)], "c-tailwing": [("tail", -0.05, 1), ("tail", -0.05, 2), ("tail", -0.03, 1), ("tail", -0.03, 2)], "d-cooldown": [("cool", 1), ("cool", 2)], } def olabel(ov): k = ov[0] if k == "spike_dvol": return f"exit DVOL +{ov[1]*100:.0f}% da entry" if k == "spike_rv": return f"exit RV5 > {ov[1]:.2f}x DVOL entry" if k == "sl": return f"stop-loss MTM {ov[1]:.1f}x credito" if k == "tail": return f"tail put d={ov[1]:+.2f} q={ov[2]}" return f"cooldown {ov[1]} settimana/e" def main(): print("=" * 112) print(" R0703 VRPIMP-DDOVERLAY — overlay di protezione DD su VRP01 vs NULL DEL DE-LEVERING") print(" Lens: MTM BS flat su DVOL-30g, trigger e fill al close 1d (gap-through nel fill, spike") print(" intraday e term-structure di stress NON modellati -> stop piu' lenti/cari del reale).") print(" Booking a scadenza su griglia settimanale. Selezione celle SOLO in-sample (pre-2025).") print("=" * 112) for a in ("BTC", "ETH"): J = load_series(a) print(f" {a}: dati {J.index[0].date()} -> {J.index[-1].date()} ({len(J)} giorni)") print(" Eventi chiave nel campione: 2021-05 (crash Cina), 2022-05 (LUNA), 2022-11 (FTX).") # ------------------------------------------------------------- (0) bridge di validazione print("\n (0) BRIDGE: motore path (overlay=None) vs options_vrp_v2.vrp_spread_weekly (VRP01)") base = {} for f in F_SWEEP: canon = pd.concat({"B": vrp_spread_weekly("BTC", defined_risk=True, f=f, gate_vrp=True, gate_ivr=0.30, crash_skip=0.90), "E": vrp_spread_weekly("ETH", defined_risk=True, f=f, gate_vrp=True, gate_ivr=0.30, crash_skip=0.90)}, axis=1, join="inner").mean(axis=1) mine = book(f=f) Jn = pd.concat({"c": canon, "m": mine}, axis=1, join="inner") dmax = float((Jn["c"] - Jn["m"]).abs().max()) base[f] = mine if f == 1.0: print(f" f={f}: max|diff| = {dmax:.2e} su {len(Jn)} settimane " f"{'OK (riproduzione esatta)' if dmax < 1e-12 else '⚠️ MISMATCH'}") print("\n (1) BASELINE VRP01 canonico — banda f") for f in F_SWEEP: fmt(f"VRP01 f={f}", mstats(base[f])) pyb = per_year(base[1.0]) print(" per-anno f=1.0: " + " ".join(f"{y}:{v*100:+5.1f}%" for y, v in sorted(pyb.items()))) anchor_sh = [m_weekly(book(f=1.0, i0=60 + o))["sh"] for o in range(7)] print(f" banda d'ancora (7 offset del loop settimanale): ShF in " f"[{min(anchor_sh):.2f}, {max(anchor_sh):.2f}] mediana {np.median(anchor_sh):.2f}") # ------------------------------------------------------------- (2) griglia overlay f=1.0 print(f"\n (2) GRIGLIA OVERLAY (f=1.0) — 14 celle, selezione per-famiglia su Sh-IS (pre-2025)") m0 = mstats(base[1.0]) all_full_sh = [m0["sh"]] cells = {} best = {} for fam, ovs in OVERLAYS.items(): for ov in ovs: col = {} r = book(f=1.0, overlay=ov, collect=col) m = mstats(r) cells[ov] = (r, m, col) all_full_sh.append(m["sh"]) n_ex = len(col.get("exits", [])) extra = f" dSh {m['sh']-m0['sh']:+.2f}" if n_ex: extra += f" early-exit n={n_ex}" if col.get("tail_frac"): extra += f" costo ala = {np.mean(col['tail_frac'])*100:.0f}% del credito" fmt(f"[{fam}] {olabel(ov)}", m, extra) if fam not in best or m["sh_is"] > cells[best[fam]][1]["sh_is"]: best[fam] = ov print() # ------------------------------------------------------------- (3) best-IS per famiglia: banda f + NULL de-levering print(" (3) CELLA BEST-IN-SAMPLE PER FAMIGLIA — banda f + CONFRONTO OBBLIGATORIO col null de-levering") print(" (de-lever = s x baseline con lo STESSO maxDD; lo Sharpe del de-levered = Sharpe baseline,") print(" scale-invariante: l'overlay vale SOLO se ShF_overlay > ShF_base a pari DD)") import altlib as al verdicts = {} for fam, ov in best.items(): print(f"\n --- {fam}: {olabel(ov)} ---") print(f" {'f':>4} | {'overlay: ShF':>12} {'ShH':>6} {'DD':>6} {'worst':>7} | " f"{'de-lever s':>10} {'ShF':>6} {'CAGR':>7} {'worst':>7} | {'dSh a pari DD':>13}") wins = 0 for f in F_SWEEP: r = book(f=f, overlay=ov) if f != 1.0 else cells[ov][0] m = mstats(r); mb = mstats(base[f]) s = delever_scale(base[f], m["dd"]) rd = s * base[f]; md = mstats(rd) dsh = m["sh"] - md["sh"] wins += dsh > 0.05 print(f" {f:>4} | {m['sh']:>12.2f} {m['sh_h']:>6.2f} {m['dd']*100:>5.1f}% {m['worst']*100:>+6.1f}% | " f"{s:>10.2f} {md['sh']:>6.2f} {md['cagr']*100:>+6.1f}% {md['worst']*100:>+6.1f}% | {dsh:>+13.2f}") # multi-cut: dSh per finestra annuale disgiunta (f=1.0) — persistenza, non finestra fortunata r1 = cells[ov][0] dcut = {} for y in (2021, 2022, 2023, 2024, 2025): sel = r1.index.year == y if y < 2025 else r1.index >= HOLDOUT selb = base[1.0].index.year == y if y < 2025 else base[1.0].index >= HOLDOUT dcut[y] = m_weekly(r1[sel])["sh"] - m_weekly(base[1.0][selb])["sh"] npos = sum(v > 0 for y, v in dcut.items() if y < 2025) print(" multi-cut dSh: " + " ".join(f"{y}:{v:+.2f}" for y, v in dcut.items()) + f" ({npos}/4 finestre IS positive)") dsr, null_max = al.deflated_sharpe(cells[ov][1]["sh"], all_full_sh, cells[ov][0].values, dpy=WK) print(f" deflated-Sharpe (14 celle + base): DSR {dsr:.3f} (null-max {null_max:.2f}) " f"{'PASS' if dsr >= 0.95 else 'FAIL'}") aband = [m_weekly(book(f=1.0, overlay=ov, i0=60 + o))["sh"] - m_weekly(book(f=1.0, i0=60 + o))["sh"] for o in range(7)] print(f" banda d'ancora dSh (7 offset): [{min(aband):+.2f}, {max(aband):+.2f}] " f"mediana {np.median(aband):+.2f}") survives = (wins == len(F_SWEEP)) and npos >= 3 and dsr >= 0.95 and np.median(aband) > 0 verdicts[fam] = dict(ov=ov, survives=survives, wins_f=int(wins), multicut=dcut, dsr=float(dsr), anchor=(min(aband), max(aband))) print(f" => {'SOPRAVVIVE al null de-levering' if survives else 'NON batte il de-levering'} " f"(pari-DD win {wins}/4 f, multicut {npos}/4, DSR {dsr:.2f})") # ------------------------------------------------------------- (4) per-anno print("\n (4) PER-ANNO (f=1.0) — 2021 (crash 5/2021) e 2022 (LUNA+FTX) sono il banco di prova") rows = [("VRP01 base", base[1.0])] + [(f"{fam} best", cells[best[fam]][0]) for fam in OVERLAYS] for tag, r in rows: py = per_year(r) print(f" {tag:<14} " + " ".join(f"{y}:{v*100:+5.1f}%" for y, v in sorted(py.items()))) print("\n VERDETTO: " + "; ".join( f"{fam} {'PASS' if v['survives'] else 'FAIL'}" for fam, v in verdicts.items())) print(" Se tutti FAIL: la protezione DD su VRP01 si compra meglio con la SIZE (de-levering),") print(" coerente con TP01xDVOL 2026-06-26. Regola standing: niente short-vol da modello in deploy.") if __name__ == "__main__": main()