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>
This commit is contained in:
@@ -0,0 +1,444 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user