"""R0703 VRPIMP-SPXGATE — gate di regime (VIX-rank / term-structure) sulla double diagonal SPX. FILONE 6 (ondata 2026-07-03). L'esperimento MANCANTE del backtest esterno dell'utente (spx-diagonal-backtest, non su questa macchina): il gate di regime sulla diagonale SPX. Domanda secca: il gate ribalta gli anni di coda come fa IV-rank su crypto (VRP01 hold-out -0.25 -> +0.28), o su equity e' solo de-levering? DATI (⚠️ PROVENIENZA): - SPY 1d: data/raw/eq_spy_1d.parquet (1996-2026, gia' in repo dal filone GTAA; research-grade). - VIX (VIXCLS) e VIX-3M (VXVCLS): scaricati da FRED via curl (pubblico, senza token) in scratchpad/vixcls.csv e vxvcls.csv. ⚠️ DATO RESEARCH-GRADE, NON CERTIFICATO dal nostro pipeline (nessun cross-venue, nessun audit per-barra; missing = '.', ffill<=5g). VXV parte dal 2007-12 -> il gate term-structure e' testabile solo dal 2008. MOTORE (⚠️ E' UN MODELLO, non una catena di quote): - Double diagonal come il progetto esterno: SHORT STRANGLE a ~6 giorni di calendario (4 trading day sul grid SPY; T di pricing = gap di calendario REALE del trade) + ALI LONG a T+1 giorno, entrambe i lati. Distanze del corto in DELTA {0.01, 0.02, 0.05} ("1-2-5 delta" del progetto esterno); ali a meta' del delta corto (WING_FRAC=0.5, scelta a priori). - Pricing Black-Scholes r=0 con SKEW LOG-LINEARE: sigma(K) = VIX + SLOPE*ln(S/K) (put piu' ricche, call piu' povere — smile equity), SLOPE=1.0 dichiarato a priori (≈ +1 vol pt per 1% di moneyness down, tipico SPX short-dated). Strike-da-delta risolto a punto fisso con la vol skewata. Il VIX e' IV a 30g usata per tenor 6g: term structure IGNORATA (in stress il front-end esplode -> il modello SOTTOSTIMA il mark avverso, dichiarato). - Ali marcate a exit con BS a 1 giorno residuo alla vol di uscita (vega catturato). - Costi: 5% del premio di ogni gamba (entry 4 gambe + vendita ali residue a exit). Modello commissioni+slippage SPX; gli spread reali deep-OTM sono spesso PEGGIORI (dich.). - Banda f {0.6, 0.8, 1.0, 1.3} moltiplica il premio di ogni gamba (regola comune n.4). - Capitale = S0 (cash-secured sul livello indice, convenzione r0702_alb_structure): CAGR/DD sono su quel denominatore conservativo; Sharpe/PF/confronti gate-vs-no-gate invarianti. TRADE SOVRAPPOSTI (fedele al progetto esterno, "~6 concorrenti" in calendario): - Ingresso OGNI trading day, holding 4 td -> 4 trade concorrenti sul grid trading-day. - Onesta' statistica: il libro si decompone in 4 STRAND non-sovrapposti (fase d'ingresso mod 4). Lo Sharpe headline = media degli Sharpe di strand (ppy=63); la banda min-max degli strand E' la banda d'ancora obbligatoria (regola 2026-07-02). N_eff = N_trade/4; PF/win riportati con lo sconto esplicito. - Equity/maxDD/CAGR/per-anno dalla serie giornaliera di portafoglio (1/4 del capitale per trade, PnL lumpato a scadenza dello strand che scade quel giorno). GATE (tutti causali a entry; rank VIX espandente con warm-start dal 1990): (a) VIX-rank > soglia {0.20,0.30,0.40,0.50} x crash-skip {no, ivr<=0.90}; piu' il GATE CANONICO crypto NON riottimizzato (vrp>0 AND 0.30<=ivr<=0.90, vrp = VIX-RV20). Selezione della cella SOLO in-sample (pre-2025); deflated-Sharpe sul totale delle celle provate. (b) Term-structure: VIX/VXV < 1 (contango) -> vendi; backwardation -> flat. Dal 2008. (c) NULL DEL DE-LEVERING obbligatorio (lezione TP01xDVOL): lo Sharpe e' scale-invariante -> il gate "vale" solo se batte lo Sharpe dell'always-in A PARI maxDD, con persistenza multi-finestra, non su una finestra fortunata. REGOLE STANDING: niente short-vol da modello in deploy — esito massimo = CONOSCENZA sul meccanismo del gate (crypto vs equity). Hold-out 2025-26 mai usato per selezionare. uv run python scripts/research/r0703_vrpimp_spxgate.py """ from __future__ import annotations import sys from bisect import bisect_left, insort 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 scipy.stats import norm from altlib import deflated_sharpe # noqa: E402 (harness condiviso) SCRATCH = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/" "e00896d3-d4bb-4f2a-b471-55a1d88a12ba/scratchpad") HOLDOUT = pd.Timestamp("2025-01-01", tz="UTC") TS_START = pd.Timestamp("2008-01-01", tz="UTC") # finestra comune term-structure (VXV dal 2007-12) TENOR_TD = 4 # trading days del corto (~6 giorni di calendario) WING_FRAC = 0.5 # delta ali = meta' del delta corto (a priori) SLOPE = 1.0 # skew log-lineare (vol per unita' di ln-moneyness) — MODELLO SIG_FLOOR = 0.05 COST_FRAC = 0.05 # 5% del premio per gamba (commissioni+slippage, MODELLO) F_SWEEP = (0.6, 0.8, 1.0, 1.3) DELTAS = (0.01, 0.02, 0.05) # distanze corto del progetto esterno CENTRAL_SD = 0.05 # cella centrale A PRIORI: 5-delta (la piu' campionata in coda; # le 1-2 delta sono il territorio "0 perdite = Sharpe implausibile") DPY_TRADE = 252.0 / TENOR_TD # ~63 trade indipendenti/anno per strand WINDOWS = [("1996-2002", "1996", "2003"), ("2003-2009", "2003", "2010"), ("2010-2016", "2010", "2017"), ("2017-2023", "2017", "2024"), ("2024-2026", "2024", "2027")] TAIL_YEARS = (1998, 2002, 2008, 2011, 2018, 2020, 2022) # ----------------------------------------------------------------------------- pricing (MODELLO) def bs_put(S, K, T, sig): if T <= 0 or sig <= 0: return max(K - S, 0.0) d1 = (np.log(S / K) + 0.5 * sig ** 2 * T) / (sig * np.sqrt(T)) d2 = d1 - sig * np.sqrt(T) return K * norm.cdf(-d2) - S * norm.cdf(-d1) def bs_call(S, K, T, sig): if T <= 0 or sig <= 0: return max(S - K, 0.0) d1 = (np.log(S / K) + 0.5 * sig ** 2 * T) / (sig * np.sqrt(T)) return S * norm.cdf(d1) - K * norm.cdf(d1 - sig * np.sqrt(T)) def sigma_k(sig_atm, S, K): """Skew log-lineare equity: put OTM (K0.""" d1 = norm.ppf(1 - delta) if kind == "put" else norm.ppf(delta) sig = sig_atm K = S for _ in range(8): K = S * np.exp(0.5 * sig * sig * T - d1 * sig * np.sqrt(T)) sig = sigma_k(sig_atm, S, K) return K, sig # ----------------------------------------------------------------------------- dati def load_master(): spy = pd.read_parquet(PROJECT_ROOT / "data" / "raw" / "eq_spy_1d.parquet") px = pd.Series(spy["close"].astype(float).values, index=pd.to_datetime(spy["timestamp"].astype("int64"), unit="ms", utc=True)) def _fred(fn, col): f = SCRATCH / fn if not f.exists(): return None c = pd.read_csv(f, na_values=".") return pd.Series(c[col].astype(float).values, index=pd.to_datetime(c["observation_date"], utc=True)) vix = _fred("vixcls.csv", "VIXCLS") vxv = _fred("vxvcls.csv", "VXVCLS") if vix is None: raise SystemExit("Manca scratchpad/vixcls.csv (curl FRED VIXCLS) — vedi docstring.") # IV-rank espandente CAUSALE sulla serie VIX nativa (warm-start dal 1990, come _ivrank) vals = vix.dropna() hist: list[float] = [] rank = {} for d, v in vals.items(): if len(hist) >= 250: rank[d] = bisect_left(hist, v) / len(hist) insort(hist, v) ivr = pd.Series(rank) df = pd.DataFrame({"px": px}) df["vix"] = vix.reindex(df.index).ffill(limit=5) df["ivr"] = ivr.reindex(df.index).ffill(limit=5) df["vxv"] = vxv.reindex(df.index).ffill(limit=5) if vxv is not None else np.nan lr = np.log(df["px"]).diff() df["rv20"] = lr.rolling(20).std() * np.sqrt(252) * 100.0 # causale: usa ritorni fino a i return df.dropna(subset=["px", "vix", "ivr"]) # ----------------------------------------------------------------------------- motore def run_base(df, sd=CENTRAL_SD, f=1.0): """Vendita della double diagonal OGNI trading day (always-in). Ritorna DataFrame trade (i, entry, exp, pnl su capitale S0, credit). I gate si applicano DOPO come maschera: il pricing e' gate-indipendente -> una sola passata per (sd, f).""" px = df["px"].values vix = df["vix"].values / 100.0 idx = df.index n = len(px) recs = [] for i in range(21, n - TENOR_TD): j = i + TENOR_TD S0 = px[i]; sig0 = vix[i] if not (np.isfinite(S0) and np.isfinite(sig0)) or sig0 <= 0: continue T = max((idx[j] - idx[i]).days, 1) / 365.25 # gap di calendario REALE Tl = T + 1.0 / 365.25 # ali T+1g # gambe corte (strangle) a delta sd; ali long a delta sd*WING_FRAC, scadenza T+1 Kps, sps = strike_skew(S0, T, sig0, sd, "put") Kcs, scs = strike_skew(S0, T, sig0, sd, "call") Kpl, spl = strike_skew(S0, Tl, sig0, sd * WING_FRAC, "put") Kcl, scl = strike_skew(S0, Tl, sig0, sd * WING_FRAC, "call") p_ps = bs_put(S0, Kps, T, sps) / S0 * f p_cs = bs_call(S0, Kcs, T, scs) / S0 * f p_pl = bs_put(S0, Kpl, Tl, spl) / S0 * f p_cl = bs_call(S0, Kcl, Tl, scl) / S0 * f credit = (p_ps + p_cs) - (p_pl + p_cl) # exit a scadenza del corto: corti a intrinseco, ali marcate BS 1g alla vol di uscita S1 = px[j]; sig1 = vix[j] short_pay = (max(0.0, Kps - S1) + max(0.0, S1 - Kcs)) / S0 lp = bs_put(S1, Kpl, 1.0 / 365.25, sigma_k(sig1, S1, Kpl)) / S0 * f lc = bs_call(S1, Kcl, 1.0 / 365.25, sigma_k(sig1, S1, Kcl)) / S0 * f cost = COST_FRAC * (p_ps + p_cs + p_pl + p_cl) + COST_FRAC * (lp + lc) pnl = credit - short_pay + (lp + lc) - cost recs.append((i, idx[i], idx[j], pnl, credit)) tr = pd.DataFrame(recs, columns=["i", "entry", "exp", "pnl", "credit"]) tr["active"] = True return tr def apply_gate(base, mask): """mask: Series booleana su df.index (causale a entry). Trade bloccato -> pnl 0, inattivo.""" tr = base.copy() ok = mask.reindex(tr["entry"]).fillna(False).values.astype(bool) tr["active"] = ok tr.loc[~tr["active"], "pnl"] = 0.0 return tr # ----------------------------------------------------------------------------- metriche def _sh(x, ppy): x = pd.Series(x).dropna() return float(x.mean() / x.std() * np.sqrt(ppy)) if len(x) > 10 and x.std() > 0 else float("nan") def daily_series(tr): return tr.groupby("exp")["pnl"].sum() / TENOR_TD def dd_of(daily): eq = (1 + daily).cumprod() pk = eq.cummax() return float(((pk - eq) / pk).max()) def agg(tr): """Metriche oneste: Sharpe = media degli strand non-sovrapposti (banda = ancora); equity/DD/CAGR dalla serie giornaliera; PF/win sui trade ATTIVI con N_eff = N/4.""" strands = [tr[tr["i"] % TENOR_TD == o]["pnl"] for o in range(TENOR_TD)] shs = [_sh(s, DPY_TRADE) for s in strands] hold = tr[tr["exp"] >= HOLDOUT] isam = tr[tr["exp"] < HOLDOUT] shs_h = [_sh(hold[hold["i"] % TENOR_TD == o]["pnl"], DPY_TRADE) for o in range(TENOR_TD)] shs_i = [_sh(isam[isam["i"] % TENOR_TD == o]["pnl"], DPY_TRADE) for o in range(TENOR_TD)] d = daily_series(tr) eq = (1 + d).cumprod() yrs = len(d) / 252.0 act = tr[tr["active"]]["pnl"] pos = act[act > 0].sum(); neg = -act[act < 0].sum() return dict( sh=float(np.nanmean(shs)), sh_lo=float(np.nanmin(shs)), sh_hi=float(np.nanmax(shs)), sh_is=float(np.nanmean(shs_i)), sh_h=float(np.nanmean(shs_h)), sh_h_lo=float(np.nanmin(shs_h)) if np.isfinite(shs_h).any() else float("nan"), sh_h_hi=float(np.nanmax(shs_h)) if np.isfinite(shs_h).any() else float("nan"), cagr=float(eq.iloc[-1] ** (1 / yrs) - 1) if yrs > 0 and eq.iloc[-1] > 0 else -1.0, dd=dd_of(d), worst5=float(d.rolling(5).sum().min()), pf=float(pos / neg) if neg > 0 else float("inf"), win=float((act > 0).mean()) if len(act) else float("nan"), n_act=int(len(act)), n_eff=int(len(act) / TENOR_TD), act_frac=float(tr["active"].mean()), nloss=int((act < 0).sum())) def per_year(tr): d = daily_series(tr) return {int(y): float((1 + g).prod() - 1) for y, g in d.groupby(d.index.year)} def win_sh(tr, a, b): """Sharpe strand-medio nella finestra [a, b) (su scadenza).""" w = tr[(tr["exp"] >= pd.Timestamp(a, tz="UTC")) & (tr["exp"] < pd.Timestamp(b, tz="UTC"))] return float(np.nanmean([_sh(w[w["i"] % TENOR_TD == o]["pnl"], DPY_TRADE) for o in range(TENOR_TD)])) def delever_k(daily_full, dd_target): lo, hi = 0.0, 1.0 if dd_of(daily_full) <= dd_target: return 1.0 for _ in range(40): k = 0.5 * (lo + hi) if dd_of(k * daily_full) > dd_target: hi = k else: lo = k return lo def row(label, m): pf = f"{m['pf']:5.2f}" if np.isfinite(m["pf"]) else " inf" print(f" {label:<42} {m['sh']:>5.2f} [{m['sh_lo']:>5.2f},{m['sh_hi']:>5.2f}] " f"{m['sh_is']:>6.2f} {m['sh_h']:>6.2f} {m['cagr']*100:>+6.2f}% {m['dd']*100:>5.1f}% " f"{m['worst5']*100:>+6.2f}% {pf} {m['win']*100:>4.0f}% {m['act_frac']*100:>4.0f}% " f"{m['n_eff']:>5}") HDR = (f" {'config':<42} {'Sh':>5} {'[banda ancora]':>14} {'Sh-IS':>6} {'Sh-HO':>6} " f"{'CAGR':>7} {'maxDD':>6} {'w5d':>7} {'PF':>5} {'win%':>5} {'att%':>5} {'Neff':>5}") def main(): print("=" * 118) print(" R0703 VRPIMP-SPXGATE — gate di regime (VIX-rank / term-structure) sulla double diagonal SPX") print(" ⚠️ MODELLO: BS + skew log-lineare su VIX-30g @ tenor 6g; VIX/VXV da FRED research-grade NON certificati.") print(" ⚠️ Trade sovrapposti (~4 concorrenti trading-day, ~6 calendario): Sharpe = media 4 strand indipendenti;") print(" PF/win su trade sovrapposti -> campione effettivo = N/4. Capitale = S0 cash-secured (conv. ALB).") print("=" * 118) df = load_master() print(f" SPY {df.index[0].date()} -> {df.index[-1].date()} ({len(df)} td) | VIX medio {df['vix'].mean():.1f} " f"| RV20 media {df['rv20'].mean():.1f} | VRP(VIX-RV20) medio {(df['vix']-df['rv20']).mean():+.1f} pt, " f">0 nel {((df['vix']-df['rv20'])>0).mean()*100:.0f}% dei giorni | VXV dal " f"{df['vxv'].dropna().index[0].date() if df['vxv'].notna().any() else 'N/A'}") trials = {} # (tag) -> Sharpe FULL strand-medio: conteggio per deflated-Sharpe # ------------------------------------------------------------------ maschere gate (causali) m_vrp = (df["vix"] - df["rv20"]) > 0 m_cs = df["ivr"] <= 0.90 m_canon = m_vrp & (df["ivr"] >= 0.30) & m_cs m_contango = (df["vix"] / df["vxv"]) < 1.0 m_contango = m_contango.fillna(False) # ------------------------------------------------------------------ (1) griglia struttura print(f"\n (1) STRUTTURA always-in, f=1.0 — distanze corto {{1,2,5}}-delta (ali a delta/2, T+1g)") print(HDR) bases = {} for sd in DELTAS: bases[sd] = run_base(df, sd=sd, f=1.0) m = agg(bases[sd]) cr = bases[sd]["credit"] tag = " ⚠️ 0-loss=Sharpe implausibile (CC01)" if m["nloss"] == 0 else "" row(f"diag {sd*100:.0f}Δ always-in (credito {cr.mean()*1e4:+.1f}bps)", m) if tag: print(f" {tag}") trials[f"sd{sd}_always"] = m["sh"] base = bases[CENTRAL_SD] print(f" -> cella centrale A PRIORI: {CENTRAL_SD*100:.0f}Δ (dichiarata nel docstring prima di guardare i numeri)") # ------------------------------------------------------------------ (2) gate VIX-rank print(f"\n (2a) GATE VIX-RANK sulla cella centrale (f=1.0) — soglia scelta SOLO in-sample (pre-2025)") print(HDR) m_always = agg(base) row("ALWAYS-IN (baseline)", m_always) gates = {"vrp>0": m_vrp, "CANONICO crypto (vrp>0 & 0.30<=ivr<=0.90)": m_canon} for th in (0.20, 0.30, 0.40, 0.50): gates[f"ivr>={th:.2f}"] = df["ivr"] >= th gates[f"ivr>={th:.2f} & cs0.90"] = (df["ivr"] >= th) & m_cs res = {} for tag, msk in gates.items(): tr = apply_gate(base, msk) res[tag] = (tr, agg(tr)) row(tag, res[tag][1]) trials[tag] = res[tag][1]["sh"] sel_tag = max(res, key=lambda t: res[t][1]["sh_is"]) sel_tr, sel_m = res[sel_tag] can_tr, can_m = res["CANONICO crypto (vrp>0 & 0.30<=ivr<=0.90)"] print(f" -> cella scelta IN-SAMPLE: '{sel_tag}' (Sh-IS {sel_m['sh_is']:.2f}) | suo hold-out {sel_m['sh_h']:.2f} " f"[{sel_m['sh_h_lo']:.2f},{sel_m['sh_h_hi']:.2f}]") dsr, sr0 = deflated_sharpe(sel_m["sh"], list(trials.values()), sel_tr[sel_tr["i"] % TENOR_TD == 0]["pnl"], dpy=DPY_TRADE) print(f" -> deflated-Sharpe della cella scelta su {len(trials)} celle: DSR={dsr:.3f} " f"(null max atteso {sr0:.2f}) {'PASS' if dsr >= 0.95 else 'FAIL'} (>=0.95)") # ------------------------------------------------------------------ (3) per-anno completo print(f"\n (3) PER-ANNO COMPLETO (f=1.0, ritorni su capitale S0) — * = anno di coda") pa, pc, ps = per_year(base), per_year(can_tr), per_year(sel_tr) print(f" {'anno':<6} {'ALWAYS':>8} {'CANONICO':>9} {'SELECTED':>9} {'anno':<6} {'ALWAYS':>8} {'CANONICO':>9} {'SELECTED':>9}") ys = sorted(pa) half = (len(ys) + 1) // 2 for k in range(half): cells = [] for y in (ys[k], ys[k + half] if k + half < len(ys) else None): if y is None: cells.append(" " * 37) continue star = "*" if y in TAIL_YEARS else " " cells.append(f"{y}{star:<1} {pa[y]*100:>+7.2f}% {pc.get(y,0)*100:>+8.2f}% {ps.get(y,0)*100:>+8.2f}%") print(" " + " ".join(cells)) flips_c = [y for y in TAIL_YEARS if y in pa and pa[y] < 0 and pc.get(y, 0) >= 0] flips_s = [y for y in TAIL_YEARS if y in pa and pa[y] < 0 and ps.get(y, 0) >= 0] worse_c = [y for y in TAIL_YEARS if y in pa and pc.get(y, 0) < min(0.0, pa[y])] print(f" anni di coda RIBALTATI (neg->non-neg): canonico {flips_c or 'NESSUNO'} | selected {flips_s or 'NESSUNO'}") print(f" anni di coda PEGGIORATI dal canonico: {worse_c or 'nessuno'}") # ------------------------------------------------------------------ (4) term structure print(f"\n (4) (2b) GATE TERM-STRUCTURE VIX/VXV — finestra comune dal 2008 (VXV research-grade)") sub = lambda tr: tr[tr["entry"] >= TS_START] # noqa: E731 print(HDR) ts_res = {} cont_tr = apply_gate(base, m_contango) for tag, tr in (("ALWAYS-IN (2008+)", sub(base)), ("contango VIX/VXV<1 (2008+)", sub(cont_tr)), ("CANONICO (2008+)", sub(can_tr)), ("CANONICO & contango (2008+)", sub(apply_gate(base, m_canon & m_contango)))): ts_res[tag] = agg(tr) row(tag, ts_res[tag]) trials[f"ts_{tag}"] = ts_res[tag]["sh"] # la finestra 2008-2009 (esclusa dal multicut per costruzione) e' il banco di prova di coda sa08 = win_sh(base, "2008", "2010"); st08 = win_sh(cont_tr, "2008", "2010") print(f" finestra 2008-2009 (GFC): always {sa08:.2f} vs contango {st08:.2f} (Δ {st08-sa08:+.2f})") # per-anno del contango (2008+) + code ribaltate pt = per_year(sub(cont_tr)); pa8 = {y: v for y, v in per_year(sub(base)).items()} print(f" per-anno 2008+ (ALWAYS vs CONTANGO): " + " ".join( f"{y}{'*' if y in TAIL_YEARS else ''}:{pa8[y]*100:+.2f}/{pt.get(y,0)*100:+.2f}%" for y in sorted(pa8))) flips_t = [y for y in TAIL_YEARS if y in pa8 and pa8[y] < 0 and pt.get(y, 0) >= 0] print(f" anni di coda RIBALTATI dal contango: {flips_t or 'NESSUNO'}") # sovrapposizione maschere (il contango e' un crash-skip travestito?) — giorni 2008+ w8 = df.index >= TS_START for tag, mk in (("ivr<=0.90 (crash-skip)", m_cs), ("vrp>0", m_vrp), ("canonico", m_canon)): a_ = m_contango[w8].values; b_ = mk[w8].values agree = float((a_ == b_).mean()) print(f" accordo giorni contango vs {tag:<22}: {agree*100:.0f}% (contango attivo {a_.mean()*100:.0f}%, " f"{tag.split()[0]} attivo {b_.mean()*100:.0f}%)") dsr_t, sr0_t = deflated_sharpe(ts_res["contango VIX/VXV<1 (2008+)"]["sh"], list(trials.values()), sub(cont_tr)[sub(cont_tr)["i"] % TENOR_TD == 0]["pnl"], dpy=DPY_TRADE) print(f" deflated-Sharpe contango su {len(trials)} celle totali provate: DSR={dsr_t:.3f} " f"{'PASS' if dsr_t >= 0.95 else 'FAIL'} (>=0.95)") print(" ⚠️ CAVEAT MODELLO (decisivo): il pricing usa VIX-30g come IV del tenor 6g. In BACKWARDATION") print(" la IV front-end reale e' >> VIX -> il credito reale nei giorni esclusi dal gate e' maggiore") print(" del modellato: l'uplift del contango e' quindi in parte SOVRASTIMATO per costruzione.") # ------------------------------------------------------------------ (5) null de-levering print(f"\n (5) NULL DEL DE-LEVERING (obbligatorio) — always-in scalato al maxDD del gated:") d_full = daily_series(base) for tag, m_g in (("CANONICO", can_m), (f"SELECTED '{sel_tag}'", sel_m), ("contango (2008+)", ts_res["contango VIX/VXV<1 (2008+)"])): dref = d_full if "2008" not in tag else d_full[d_full.index >= TS_START] sh_ref = m_always["sh"] if "2008" not in tag else ts_res["ALWAYS-IN (2008+)"]["sh"] k = delever_k(dref, m_g["dd"]) eq = (1 + k * dref).cumprod(); yrs = len(dref) / 252.0 cagr_k = float(eq.iloc[-1] ** (1 / yrs) - 1) beat = m_g["sh"] - sh_ref print(f" {tag:<28} gated: Sh {m_g['sh']:.2f} DD {m_g['dd']*100:.1f}% CAGR {m_g['cagr']*100:+.2f}% | " f"de-lever k={k:.2f}: Sh {sh_ref:.2f} (invariante) CAGR {cagr_k:+.2f}% -> " f"ΔSh gate-vs-delever {beat:+.2f} {'(il gate AGGIUNGE)' if beat > 0.1 else '(de-levering basta)'}") # ------------------------------------------------------------------ (6) multi-cut print(f"\n (6) PERSISTENZA MULTI-FINESTRA (uplift Sharpe strand-medio gated - always, finestre disgiunte)") print(f" {'finestra':<12} {'ALWAYS':>7} {'CANON':>7} {'Δcanon':>8} {'SELECT':>7} {'Δsel':>8} {'contango':>9} {'Δcont':>8}") pos_c = pos_s = pos_t = n_t = 0 for wtag, a, b in WINDOWS: sa = win_sh(base, a, b); sc = win_sh(can_tr, a, b); ss = win_sh(sel_tr, a, b) in_ts = pd.Timestamp(a, tz="UTC") >= TS_START st = win_sh(apply_gate(base, m_contango), a, b) if in_ts else float("nan") dc, dsl = sc - sa, ss - sa dt = (st - sa) if in_ts else float("nan") pos_c += dc > 0; pos_s += dsl > 0 if in_ts: pos_t += dt > 0; n_t += 1 print(f" {wtag:<12} {sa:>7.2f} {sc:>7.2f} {dc:>+8.2f} {ss:>7.2f} {dsl:>+8.2f} " f"{st:>9.2f} {dt:>+8.2f}") print(f" -> multicut: canonico {pos_c}/5 | selected {pos_s}/5 | contango {pos_t}/{n_t} (2008+)") # ------------------------------------------------------------------ (7) banda f print(f"\n (7) BANDA f {F_SWEEP} (regola skew n.4) — cella centrale, always vs canonico vs selected") print(HDR) sel_mask = gates[sel_tag] sub8 = lambda tr: tr[tr["entry"] >= TS_START] # noqa: E731 for f in F_SWEEP: bf = bases[CENTRAL_SD] if f == 1.0 else run_base(df, sd=CENTRAL_SD, f=f) row(f"always-in f={f}", agg(bf)) row(f"canonico f={f}", agg(apply_gate(bf, m_canon))) row(f"selected f={f}", agg(apply_gate(bf, sel_mask))) a8 = agg(sub8(bf)); c8 = agg(sub8(apply_gate(bf, m_contango))) print(f" -> contango f={f} (2008+): Sh {c8['sh']:.2f} vs always {a8['sh']:.2f} " f"(Δ {c8['sh']-a8['sh']:+.2f}) DD {c8['dd']*100:.1f}% vs {a8['dd']*100:.1f}%") # ------------------------------------------------------------------ verdetto print("\n DOMANDA SECCA — il gate ribalta gli anni di coda come IV-rank su crypto (HOLD -0.25->+0.28)?") print(f" crypto VRP01: gate ivr>=0.30 = vendi solo vol RICCA -> ribalta l'hold-out.") print(f" SPX qui: always Sh {m_always['sh']:.2f} (HO {m_always['sh_h']:.2f}) vs canonico Sh {can_m['sh']:.2f} " f"(HO {can_m['sh_h']:.2f}); code ribaltate: {flips_c or 'NESSUNA'}; multicut canonico {pos_c}/5; DSR {dsr:.2f}.") print(" ⚠️ Tutto su MODELLO (BS+skew log-lineare, VIX 30g @ 6g, dati FRED non certificati). Regola standing:") print(" niente short-vol da modello in deploy — esito = conoscenza sul meccanismo del gate, non un LEAD.") if __name__ == "__main__": main()