"""R0703 VRPIMP-NEWGATES — gate d'ingresso NUOVI (veto additivi) su VRP01 canonico, barra alta. FILONE 4 (ondata 2026-07-03). TRE raffinamenti del gate sono GIA' falliti (2026-07-01, r0701_vrp_refine: sizing sul gap IV-RV, filtro DVOL-momentum, gate di regime TP01) e qui NON si ritestano. Solo angoli mai provati, sempre come VETO ADDITIVO sopra il gate canonico (vrp>0 AND ivr>=0.30 AND ivr<=0.90 — MAI riottimizzato, lezione 2026-07-01): (a) TERM STRUCTURE del DVOL (contango=vendi / backwardation=fuori). PRIMA il dato: la storia per-scadenza NON e' pubblica su Deribit (probe_vol_termstructure 2026-06-29); il logger forward (log_vol_termstructure -> data/raw/vol_term_*.parquet) accumula da fine giugno. Se la storia e' < ~180 giorni il gate NON e' testabile e si DICHIARA. L'unico stand-in ricostruibile dai nostri dati e' TSPROXY = DVOL30 - RV7 (pseudo-slope implied-30g vs realized-7g): dichiaratamente NON una term structure (e' il gate VRP canonico con finestra RV corta) — incluso in griglia solo per completezza, etichettato PROXY. (b) VOL-OF-VOL: std rolling delle variazioni giornaliere del DVOL (k=10/20 gg), messa in percentile espandente CAUSALE; alto = regime vol instabile -> veto (stai fuori). (Diverso dal DVOL-momentum fallito il 2026-07-01: la' era il LIVELLO in salita, qui la VOLATILITA' del DVOL, direzione-agnostica.) (c) ACCELERAZIONE della RV: RV5/RV20 causale > soglia (vol che accelera = precursore di crash) -> veto. Anche in versione percentile espandente. (d) AND pre-dichiarati: canonico + veto(b) + veto(c), 4 combo FISSE scritte nel codice prima di guardare qualunque risultato. METODO (vincolante, regole comuni ondata): - selezione soglie SOLO in-sample (pre-2025); hold-out 2025-26 mai usato per scegliere; - multi-cut: uplift vs baseline a 5 date di taglio (>=4/5 richiesti; >=3 = soglia minima del filone) + 4 finestre DISGIUNTE; - deflated-Sharpe sul TOTALE delle celle provate (18, baseline inclusa) — e comunque anti-conservativo (celle correlate + VRP01 stesso viene da ~20 config storiche); - NULL DEL DE-LEVERING esplicito (lezione TP01xDVOL 2026-06-26): lo Sharpe e' scale-invariante, quindi un veto "vale" solo se batte lo Sharpe del baseline A PARI maxDD; - banda f {0.6, 0.8, 1.0, 1.3} (pricing BS flat su DVOL ATM, skew non modellato); - banda d'ancora obbligatoria (regola 2026-07-02): loop settimanale ancorato a i0=60 -> 7 offset i0 in {60..66} riportati per baseline e cella scelta. Motore: riproduzione ESATTA di options_vrp_v2.vrp_spread_weekly (bridge-check a diff=0 nel main) + veto parametrici. Fee: 12.5% del premio netto (convenzione VRP01). Capitale = strike corto (cash-secured). Regola standing: niente short-vol da modello in deploy — esito massimo = LEAD/conoscenza. Aspettativa dichiarata: FALLIRE (3 precedenti su 3); la mappa dei fallimenti E' il deliverable. uv run python scripts/research/r0703_vrpimp_newgates.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")) from collections import Counter from functools import lru_cache 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 from altlib import deflated_sharpe HOLDOUT = pd.Timestamp("2025-01-01", tz="UTC") WK = 365.25 / 7.0 TENOR = 7 FEE_FRAC = 0.125 # convenzione VRP01: 12.5% del premio netto round-trip F_SWEEP = (0.6, 0.8, 1.0, 1.3) CUTS = [pd.Timestamp(c, tz="UTC") for c in ("2023-01-01", "2023-07-01", "2024-01-01", "2024-07-01", "2025-01-01")] WINDOWS = [("2021-2022", None, "2023-01-01"), ("2023", "2023-01-01", "2024-01-01"), ("2024", "2024-01-01", "2025-01-01"), ("2025+", "2025-01-01", None)] MIN_IS_ACTIVE = 0.20 # baseline ~41% attivo SCRATCH = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/e00896d3-d4bb-4f2a-b471-55a1d88a12ba/scratchpad") # ------------------------------------------------------------------ features causali def _expanding_pctl(x: np.ndarray, min_hist: int = 60) -> np.ndarray: """pctl[i] = quota della storia PASSATA valida x[:i] sotto x[i] (espandente, causale).""" n = len(x); out = np.full(n, np.nan) for i in range(n): if np.isnan(x[i]): continue h = x[:i]; h = h[~np.isnan(h)] if len(h) >= min_hist: out[i] = float((h < x[i]).mean()) return out @lru_cache(maxsize=None) def prep(asset: str): """Tutte le feature usano SOLO dati <= close[i] (sell-date). lr[k]=log(px[k+1]/px[k]) => lr[i-w:i] sono i w rendimenti che finiscono a close[i] (stessa convenzione _rv30 di VRP01).""" J = load_series(asset) px = J["px"].values.astype(float) dv = J["dvol"].values.astype(float) / 100.0 idx = J.index n = len(px) lr = np.diff(np.log(px)) def _rv(i, w): if i < w + 1: return np.nan return float(np.std(lr[i - w:i]) * np.sqrt(365.25)) rv5 = np.array([_rv(i, 5) for i in range(n)]) rv7 = np.array([_rv(i, 7) for i in range(n)]) rv20 = np.array([_rv(i, 20) for i in range(n)]) rv30 = np.array([_rv(i, 30) for i in range(n)]) ivr = np.full(n, np.nan) for i in range(60, n): ivr[i] = float((dv[:i] < dv[i]).mean()) # (b) vol-of-vol: std delle ultime k variazioni giornaliere del DVOL (ultima = dv[i]-dv[i-1]) ddv = np.diff(dv) def _vv(i, k): if i < k + 1: return np.nan return float(np.std(ddv[i - k:i])) vv = {k: np.array([_vv(i, k) for i in range(n)]) for k in (10, 20)} vvp = {k: _expanding_pctl(vv[k]) for k in (10, 20)} # (c) accelerazione RV with np.errstate(invalid="ignore", divide="ignore"): rvacc = rv5 / rv20 rap = _expanding_pctl(rvacc) # (a-proxy) pseudo-slope: DVOL30 - RV7 (PROXY, non term structure) tsproxy = dv - rv7 tsp = _expanding_pctl(tsproxy) return dict(px=px, dv=dv, idx=idx, n=n, rv30=rv30, ivr=ivr, vvp=vvp, rvacc=rvacc, rap=rap, tsproxy=tsproxy, tsp=tsp) # ------------------------------------------------------------------ motore settimanale def _veto_hit(P, i, veto) -> bool: """True se il veto scatta al giorno i (NaN = veto non applicabile -> non scatta).""" kind = veto[0] if kind == "vv": # ("vv", k, pctl_thr) v = P["vvp"][veto[1]][i] return (not np.isnan(v)) and v > veto[2] if kind == "ra": # ("ra", ratio_thr) v = P["rvacc"][i] return (not np.isnan(v)) and v > veto[1] if kind == "rap": # ("rap", pctl_thr) v = P["rap"][i] return (not np.isnan(v)) and v > veto[1] if kind == "ts_bin": # backwardation-proxy: RV7 sopra DVOL30 v = P["tsproxy"][i] return (not np.isnan(v)) and v <= 0.0 if kind == "ts_pctl": # ("ts_pctl", thr): slope-proxy nel fondo percentile v = P["tsp"][i] return (not np.isnan(v)) and v < veto[1] raise ValueError(kind) def vrp_weekly(asset: str, vetoes=(), f: float = 1.0, i0: int = 60, collect: dict | None = None) -> tuple[pd.Series, Counter]: """VRP01 canonico (riproduce vrp_spread_weekly gated ESATTO con vetoes=()) + veto additivi. I veto sono valutati DOPO i gate canonici -> il counter conta l'intervento MARGINALE (settimane altrimenti tradate). Causale: gate/strike/premio a sell-date, payoff a scadenza.""" P = prep(asset) px, dv, idx, n = P["px"], P["dv"], P["idx"], P["n"] T = TENOR / 365.25 rets = {}; st = Counter() i = i0 while i + TENOR < n: st["weeks"] += 1 S0 = px[i]; sig = dv[i] # --- gate canonici (identici a options_vrp_v2, NON riottimizzati) --- rv = P["rv30"][i]; ivr = P["ivr"][i] skip = ((not np.isnan(rv) and (sig - rv) <= 0) or (not np.isnan(ivr) and (ivr < 0.30 or ivr > 0.90))) if skip: st["blk_canon"] += 1 rets[idx[i + TENOR]] = 0.0 i += TENOR continue # --- veto NUOVI (marginali) --- hit = None for v in vetoes: if _veto_hit(P, i, v): hit = v[0] break if hit is not None: st[f"blk_{hit}"] += 1 if collect is not None: collect.setdefault("vetoed_i", []).append((asset, i)) rets[idx[i + TENOR]] = 0.0 i += TENOR continue st["traded"] += 1 # --- struttura canonica: put credit spread -0.28/-0.10, capitale = strike corto --- 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 S1 = px[i + TENOR] payoff = max(0.0, Ks - S1) - max(0.0, Kl - S1) pnl = net_prem - payoff - FEE_FRAC * abs(net_prem) rets[idx[i + TENOR]] = pnl / Ks i += TENOR return pd.Series(rets), st def book(vetoes=(), f: float = 1.0, i0: int = 60) -> tuple[pd.Series, Counter]: rB, sB = vrp_weekly("BTC", vetoes=vetoes, f=f, i0=i0) rE, sE = vrp_weekly("ETH", vetoes=vetoes, f=f, i0=i0) b = pd.concat({"B": rB, "E": rE}, axis=1, join="inner").mean(axis=1).sort_index() return b, sB + sE # ------------------------------------------------------------------ metriche / null def sh_wk(r: pd.Series) -> float: r = r.dropna() if len(r) < 8 or r.std() == 0: return float("nan") return float(r.mean() / r.std() * np.sqrt(WK)) def mstats(r: pd.Series) -> dict: full = m_weekly(r) return dict(sh=full["sh"], dd=full["dd"], cagr=full["cagr"], sh_is=sh_wk(r[r.index < HOLDOUT]), sh_h=sh_wk(r[r.index >= HOLDOUT]), worst=float(r.min()) if len(r) else 0.0, active=float((r != 0).mean())) def maxdd(r: pd.Series) -> float: 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: pd.Series, dd_target: float, hi: float = 2.0) -> float: """Scaling s della size del baseline con maxDD == dd_target (bisezione, Sharpe invariante).""" 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 # ------------------------------------------------------------------ griglia (A PRIORI) def build_grid(): cells = [("BASELINE canonico", ())] for k in (10, 20): for p in (0.70, 0.80, 0.90): cells.append((f"VV k={k} p>{p:.2f}", (("vv", k, p),))) for thr in (1.0, 1.25, 1.5): cells.append((f"RA rv5/rv20>{thr:.2f}", (("ra", thr),))) for p in (0.80, 0.90): cells.append((f"RAP pctl>{p:.2f}", (("rap", p),))) cells.append(("TSPROXY bin (rv7>dvol) [PROXY]", (("ts_bin",),))) cells.append(("TSPROXY pctl<0.20 [PROXY]", (("ts_pctl", 0.20),))) # combo AND pre-dichiarate (scritte prima di guardare i risultati) cells.append(("AND VV(20,.80)+RA(1.25)", (("vv", 20, 0.80), ("ra", 1.25)))) cells.append(("AND VV(20,.80)+RAP(.80)", (("vv", 20, 0.80), ("rap", 0.80)))) cells.append(("AND VV(10,.80)+RA(1.25)", (("vv", 10, 0.80), ("ra", 1.25)))) cells.append(("AND VV(20,.90)+RA(1.50)", (("vv", 20, 0.90), ("ra", 1.50)))) return cells # ------------------------------------------------------------------ main def main(): print("=" * 112) print(" R0703 VRPIMP-NEWGATES — veto d'ingresso nuovi su VRP01 (term-structure / vol-of-vol / RV-accel / AND)") print(" Barra: selezione IS-only + multi-cut >=4/5 + DSR>=0.95 + null de-levering + banda f + banda d'ancora") print("=" * 112) # ---------------- (0) angolo (a): fattibilita' del dato term-structure ---------------- print("\n (0) TERM STRUCTURE DVOL — verifica del dato PRIMA del test") n_days = {} for a in ("btc", "eth"): fp = PROJECT_ROOT / "data" / "raw" / f"vol_term_{a}.parquet" if fp.exists(): df = pd.read_parquet(fp) n_days[a] = len(df) print(f" vol_term_{a}: {len(df)} righe [{df['date'].min().date()} -> {df['date'].max().date()}] " f"slope7-180 medio {df['slope_7_180'].mean():+.1f}pp") else: n_days[a] = 0 print(f" vol_term_{a}: NESSUN FILE") n_ts = min(n_days.values()) if n_days else 0 print(f" -> storia term-structure = {n_ts} GIORNI (logger forward dal 2026-06-26; serve >=~180g +") print(" certificazione). La storia per-scadenza NON e' pubblica (probe 2026-06-29) e NON e'") print(" ricostruibile dai fetch. VERDETTO (a): NON TESTABILE OGGI — rivalutare a ~6-12 mesi di log.") print(" In griglia resta SOLO il proxy TSPROXY = DVOL30-RV7, dichiarato NON-term-structure") print(" (e' il gate VRP canonico a finestra RV corta).") # ---------------- (1) baseline + bridge ---------------- base, st_base = book() ref = pd.concat({"B": vrp_spread_weekly("BTC", defined_risk=True, f=1.0, gate_vrp=True, gate_ivr=0.30, crash_skip=0.90), "E": vrp_spread_weekly("ETH", defined_risk=True, f=1.0, gate_vrp=True, gate_ivr=0.30, crash_skip=0.90)}, axis=1, join="inner").mean(axis=1).sort_index() al_ = base.align(ref, join="inner") bridge = float((al_[0] - al_[1]).abs().max()) mb = mstats(base) print(f"\n (1) BASELINE VRP01 riprodotto: ShF {mb['sh']:.2f} ShH {mb['sh_h']:.2f} Sh-IS {mb['sh_is']:.2f} " f"CAGR {mb['cagr']*100:+.1f}% DD {mb['dd']*100:.1f}% worst {mb['worst']*100:+.1f}% attivo {mb['active']*100:.0f}%") print(f" bridge vs options_vrp_v2.vrp_spread_weekly: max|diff| = {bridge:.2e} " f"({'OK motore ESATTO' if bridge < 1e-12 else '⚠️ DIVERGE — non fidarsi dei numeri'})") # ---------------- (2) diagnostica: i veto rimuovono settimane cattive? (IS only) ---------------- print("\n (2) DIAGNOSTICA IN-SAMPLE (pre-2025): pnl medio del BASELINE nelle settimane che il veto") print(" avrebbe bloccato vs tenuto (per-asset, solo settimane canonicamente tradate):") probes = [("VV k=10 p>.80", ("vv", 10, 0.80)), ("VV k=20 p>.80", ("vv", 20, 0.80)), ("RA >1.25", ("ra", 1.25)), ("RAP p>.80", ("rap", 0.80)), ("TSPROXY bin", ("ts_bin",)), ("TSPROXY p<.20", ("ts_pctl", 0.20))] for label, veto in probes: cut_v, cut_k = [], [] for a in ("BTC", "ETH"): r0, _ = vrp_weekly(a) # baseline per-asset P = prep(a) pos = {ts: k for k, ts in enumerate(P["idx"])} for ts, v in r0.items(): if v == 0.0: continue i_sell = pos[ts] - TENOR if P["idx"][i_sell] >= HOLDOUT: continue (cut_v if _veto_hit(P, i_sell, veto) else cut_k).append(v) mv = np.mean(cut_v) * 100 if cut_v else float("nan") mk = np.mean(cut_k) * 100 if cut_k else float("nan") print(f" {label:<16} bloccate n={len(cut_v):>3} pnl medio {mv:>+6.2f}% | " f"tenute n={len(cut_k):>3} pnl medio {mk:>+6.2f}% " f"({'veto sensato IS' if cut_v and mv < mk else 'veto NON discrimina IS'})") # ---------------- (3) griglia 18 celle ---------------- cells = build_grid() print(f"\n (3) GRIGLIA {len(cells)} celle (tutte nel DSR) — selezione SOLO su Sh-IS (pre-2025):") print(f" {'cella':<32}{'Sh-IS':>7}{'ShF':>7}{'ShH':>7}{'DD':>7}{'worst':>8}{'att.':>6}{'blk-marg':>9}") results = {} for name, vetoes in cells: b, st = book(vetoes=vetoes) m = mstats(b) blk = sum(v for k, v in st.items() if k.startswith("blk_") and k != "blk_canon") results[name] = dict(name=name, vetoes=vetoes, b=b, st=st, blk=blk, **m) print(f" {name:<32}{m['sh_is']:>7.2f}{m['sh']:>7.2f}{m['sh_h']:>7.2f}" f"{m['dd']*100:>6.1f}%{m['worst']*100:>+7.1f}%{m['active']*100:>5.0f}%{blk:>9}") pd.DataFrame([{k: v for k, v in r.items() if k not in ("b", "st", "vetoes")} for r in results.values()]).to_csv(SCRATCH / "r0703_newgates_grid.csv", index=False) bl = results["BASELINE canonico"] ranked = sorted((r for r in results.values() if r["active"] >= MIN_IS_ACTIVE and not np.isnan(r["sh_is"])), key=lambda r: r["sh_is"], reverse=True) cand = ranked[0] is_baseline_best = cand["name"] == bl["name"] n_beat_hold = sum(1 for r in results.values() if r["sh_h"] > bl["sh_h"]) print(f"\n -> cella scelta IN-SAMPLE: [{cand['name']}] Sh-IS {cand['sh_is']:.2f} " f"(baseline {bl['sh_is']:.2f}, Δ {cand['sh_is'] - bl['sh_is']:+.2f})") print(f" [onesta'] celle che battono l'HOLD-OUT del baseline: {n_beat_hold}/{len(results)} — " f"NON selezionabili (selection-on-holdout, gate 2026-06-29).") # ---------------- (4) multi-cut + finestre disgiunte + DSR ---------------- print("\n (4) MULTI-CUT (Sharpe dal taglio a fine storia; uplift = cand - baseline):") pos_cuts = 0 for c in CUTS: sc = sh_wk(cand["b"][cand["b"].index >= c]); sb = sh_wk(bl["b"][bl["b"].index >= c]) u = sc - sb pos_cuts += int(u > 0) print(f" cut {c.date()}: cand {sc:>5.2f} base {sb:>5.2f} uplift {u:>+5.2f}") print(f" -> uplift positivo in {pos_cuts}/{len(CUTS)} tagli (barra alta >=4/5; soglia minima filone >=3)") print(" FINESTRE DISGIUNTE:") pos_win = 0 for wname, lo, hi in WINDOWS: m_ = pd.Series(True, index=cand["b"].index) if lo: m_ &= cand["b"].index >= pd.Timestamp(lo, tz="UTC") if hi: m_ &= cand["b"].index < pd.Timestamp(hi, tz="UTC") sc, sb = sh_wk(cand["b"][m_]), sh_wk(bl["b"][m_]) u = sc - sb pos_win += int(u > 0) print(f" {wname:<10} cand {sc:>5.2f} base {sb:>5.2f} uplift {u:>+5.2f}") print(f" -> uplift positivo in {pos_win}/{len(WINDOWS)} finestre disgiunte") all_sh = [r["sh"] for r in results.values()] dsr_c, null_max = deflated_sharpe(cand["sh"], all_sh, cand["b"].values, dpy=WK) dsr_b, _ = deflated_sharpe(bl["sh"], all_sh, bl["b"].values, dpy=WK) print(f"\n DEFLATED SHARPE (N={len(all_sh)} celle): cand DSR {dsr_c:.3f} (null-max Sh {null_max:.2f}) | " f"baseline DSR {dsr_b:.3f} [PASS >= 0.95; anti-conservativo: celle correlate + ~20 config storiche VRP]") # ---------------- (5) NULL DEL DE-LEVERING ---------------- print("\n (5) NULL DEL DE-LEVERING (lezione TP01xDVOL): baseline scalato allo STESSO maxDD del candidato") s = delever_scale(bl["b"], cand["dd"]) scaled = s * bl["b"] ms = mstats(scaled) print(f" candidato [{cand['name']}]: Sh {cand['sh']:.2f} ShH {cand['sh_h']:.2f} CAGR {cand['cagr']*100:+.1f}% " f"DD {cand['dd']*100:.1f}% worst {cand['worst']*100:+.1f}%") print(f" baseline x{s:.2f} (pari DD): Sh {ms['sh']:.2f} ShH {ms['sh_h']:.2f} CAGR {ms['cagr']*100:+.1f}% " f"DD {ms['dd']*100:.1f}% worst {ms['worst']*100:+.1f}%") survives_delever = (cand["sh"] > ms["sh"]) and (cand["sh_h"] > ms["sh_h"]) print(f" -> il veto {'BATTE' if survives_delever else 'NON batte'} il de-levering " f"(serve Sh full E hold > baseline a pari DD; lo Sharpe e' scale-invariante).") # ---------------- (6) banda f + banda d'ancora ---------------- print("\n (6a) BANDA f (skew non modellato) — uplift cand-baseline per f:") for f in F_SWEEP: bf, _ = book(f=f); cf, _ = book(vetoes=cand["vetoes"], f=f) mbf, mcf = mstats(bf), mstats(cf) print(f" f={f}: base ShF {mbf['sh']:>5.2f}/ShH {mbf['sh_h']:>5.2f} cand ShF {mcf['sh']:>5.2f}/ShH {mcf['sh_h']:>5.2f} " f"uplift full {mcf['sh']-mbf['sh']:>+5.2f} hold {mcf['sh_h']-mbf['sh_h']:>+5.2f}") print("\n (6b) BANDA D'ANCORA (7 offset i0 del ciclo settimanale; regola 2026-07-02):") ups_f, ups_h = [], [] for ph in range(7): bA, _ = book(i0=60 + ph); cA, _ = book(vetoes=cand["vetoes"], i0=60 + ph) mbA, mcA = mstats(bA), mstats(cA) ups_f.append(mcA["sh"] - mbA["sh"]); ups_h.append(mcA["sh_h"] - mbA["sh_h"]) print(f" i0=+{ph}g: base {mbA['sh']:>5.2f}/{mbA['sh_h']:>5.2f} cand {mcA['sh']:>5.2f}/{mcA['sh_h']:>5.2f} " f"uplift {mcA['sh']-mbA['sh']:>+5.2f}/{mcA['sh_h']-mbA['sh_h']:>+5.2f}") print(f" banda uplift FULL [{min(ups_f):+.2f}, {max(ups_f):+.2f}] mediana {np.median(ups_f):+.2f} | " f"HOLD [{min(ups_h):+.2f}, {max(ups_h):+.2f}] mediana {np.median(ups_h):+.2f}") # ---------------- (7) per-anno + verdetto ---------------- print("\n (7) PER-ANNO (ritorno composto):") pyb, pyc = per_year(bl["b"]), per_year(cand["b"]) print(" base: " + " ".join(f"{y}:{v*100:+5.1f}%" for y, v in sorted(pyb.items()))) print(" cand: " + " ".join(f"{y}:{v*100:+5.1f}%" for y, v in sorted(pyc.items()))) print("\n" + "=" * 112) gates = dict( non_e_baseline=not is_baseline_best, batte_IS=cand["sh_is"] > bl["sh_is"], multicut_4su5=pos_cuts >= 4, multicut_min3=pos_cuts >= 3, finestre_disgiunte=pos_win >= 3, hold_migliore=cand["sh_h"] > bl["sh_h"], dsr_pass=bool(dsr_c >= 0.95), batte_delevering=bool(survives_delever), anchor_mediana_pos=float(np.median(ups_h)) > 0, ) improves = all(gates.values()) if is_baseline_best: print(" VERDETTO: NON MIGLIORA — il baseline VRP01 vince gia' la selezione in-sample.") print(" QUARTO fallimento del filone 'raffina il gate': l'alpha resta il binario IV-rank>0.30.") elif improves: print(f" VERDETTO: MIGLIORA — [{cand['name']}] passa TUTTI i gate. Resta sleeve MODELLATO") print(" (DVOL ATM, no skew, f di stress non catturato): MAI deploy, solo aggiornamento del modello.") else: fails = [k for k, v in gates.items() if not v] print(f" VERDETTO: NON MIGLIORA — cella IS-best [{cand['name']}] bocciata su: {', '.join(fails)}.") print(" QUARTO fallimento del filone 'raffina il gate' (dopo sizing/momentum/TP01 del 2026-07-01):") print(" l'alpha del VRP01 resta interamente nel gate binario IV-rank>0.30. Angolo (a) term-structure:") print(f" non testabile ({n_ts}g di storia), il logger forward accumula — rivalutare a ~6-12 mesi.") print(" gates: " + " ".join(f"{k}={'PASS' if v else 'FAIL'}" for k, v in gates.items())) print("=" * 112) if __name__ == "__main__": main()