Files
PythagorasGoal/scripts/research/r0703_vrpimp_spxgate.py
T
Adriano Dal Pastro 26f8d27a61 research(wave-0703): migliora+proteggi VRP01 — 7 filoni, 0 miglioramenti, anchor-audit VRP01 chiuso (4/4 sleeve)
Goal: migliorare la strategia short-vol (famiglia Albimarini/VRP01) e
proteggerla dai DD. Workflow 26 agenti (7 filoni + 2 lenti avversariali
+ scettico incrociato). Esito: NON migliorabile; la protezione DD si
compra SOLO con la size.

- Griglia 288 strutture: nessuna batte VRP01 (DSR 0.000; meta' griglia
  = 3a occorrenza "0-perdite/Sharpe implausibile" dopo CC01/ALB-A).
- 4 overlay DD (exit-spike/SL-MTM/ala-coda/cooldown): 4/4 REFUTED dal
  null de-levering — la protezione crash vive gia' nel gate IV-rank.
- Gate nuovi: 4o fallimento su 4 (l'alpha e' il binario IV-rank>0.30).
- Sizing: 12% deploy ~ 0.27 Kelly onesto (anti-rovina); disambiguazione
  unita' obbligatoria vs 12% peso book (0.014 Kelly, fattore 19x).
- Gate term-structure VIX/VXV su SPX (dSh +0.90, DSR 0.992) = confound
  di modello al 100% -> nuova regola: riprezzare term-structure-consistent
  prima di credere a un gate vol su strutture BS-flat.
- ANCHOR-AUDIT VRP01 CHIUSO: primo sleeve SENZA luck (fase canonica =
  peggiore delle 7 -> numeri di ammissione conservativi). Audit anchor
  ora completo su 4/4 sleeve ancorati.

Book/pesi INVARIATI. Nessun nuovo sleeve. 168 test verdi.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 07:05:19 +00:00

463 lines
24 KiB
Python

"""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 (K<S) piu' ricche, call OTM piu' povere."""
return max(sig_atm + SLOPE * np.log(S / K), SIG_FLOOR)
def strike_skew(S, T, sig_atm, delta, kind):
"""Strike dal delta CON la vol skewata (punto fisso). kind: 'put'/'call', delta>0."""
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()