Files
PythagorasGoal/scripts/research/r0703_vrpimp_newgates.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

445 lines
23 KiB
Python

"""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()