"""r0724_statarb_deploy_gate — gate di deploy PRE-REGISTRATO per STATARB-RESID (2026-07-24). PERCHE' PRE-REGISTRATO: la regola si fissa a forward-day 25 (oggi), PRIMA di vedere il giorno 90. Decidere "a occhio" dopo aver visto il numero = selection-on-forward, lo stesso bias che il progetto ha gia' pagato (gate SELECTION-ON-HOLDOUT, 2026-06-29). REGOLA (immutabile — ogni modifica va motivata nel diario come violazione): - Data decisione: 2026-09-27 (90 giorni forward dal 2026-06-29). Non prima. - Metrica: Sharpe annualizzato di net_modeled su TUTTA la finestra forward (nessuna sottofinestra), da data/paper_statarb/returns.jsonl. - Esiti: Sharpe >= 0.5 -> CANDIDATO AL DEPLOY: proporre peso {10,15,20}% del book Deribit, giudicato con weights_tilt_null + stima maxDD combinata < 15%. Deploy solo se il tilt-null passa. 0 <= Sh < 0.5 -> ESTENDI una sola volta di altri 90g (decisione 2026-12-26). Alla seconda scadenza: o >=0.5 o RITIRO. Niente terza proroga. Sharpe < 0 -> RITIRO dal forward-monitor (l'edge DSR 0.929 non ha confermato). - Guardie accessorie (tutte richieste per il deploy): * fill-haircut cumulato |MODELED - REAL| < 0.5 pp (eseguibilita' reale a $600); * config invariata (W=45, sgn=+1) — ogni ritocco azzera la finestra; * maxDD forward < 10% (coerenza col profilo di ricerca, DD backtest ~11%). Uso: `uv run python scripts/research/r0724_statarb_deploy_gate.py` (si puo' lanciare in qualsiasi momento: ricorda la data-decisione e valuta lo stato). """ from __future__ import annotations import json from datetime import date from pathlib import Path import numpy as np ROOT = Path(__file__).resolve().parents[2] RETURNS = ROOT / "data" / "paper_statarb" / "returns.jsonl" START = date(2026, 6, 29) # inizio forward (config congelata) DECISION = date(2026, 9, 27) # 90g forward — PRIMA data utile di decisione DECISION_EXT = date(2026, 12, 26) # unica proroga ammessa SH_DEPLOY = 0.5 HAIRCUT_MAX_PP = 0.5 DD_MAX = 0.10 def main() -> None: rows = [json.loads(x) for x in RETURNS.read_text().splitlines() if x.strip()] rm = np.array([r["net_modeled"] for r in rows]) rr = np.array([r["net_real"] for r in rows]) n = len(rm) sh = float(rm.mean() / rm.std() * np.sqrt(365)) if rm.std() > 0 else 0.0 eq = np.cumprod(1 + rm) dd = float(np.max((np.maximum.accumulate(eq) - eq) / np.maximum.accumulate(eq))) haircut_pp = abs(float((np.prod(1 + rm) - np.prod(1 + rr)) * 100)) today = date.today() print("=" * 88) print(" STATARB-RESID — gate di deploy PRE-REGISTRATO (fissato 2026-07-24, forward-day 25)") print("=" * 88) print(f" finestra forward : {rows[0]['dt'][:10]} -> {rows[-1]['dt'][:10]} ({n} barre)") print(f" Sharpe fwd (mod) : {sh:+.2f} (soglia deploy >= {SH_DEPLOY})") print(f" maxDD fwd : {dd:.1%} (guardia < {DD_MAX:.0%})") print(f" fill-haircut cum : {haircut_pp:.2f} pp (guardia < {HAIRCUT_MAX_PP} pp)") print(f" data decisione : {DECISION} (proroga unica: {DECISION_EXT})") if today < DECISION: print(f"\n -> NESSUNA DECISIONE OGGI ({today}): mancano {(DECISION - today).days} giorni.") print(" Il numero corrente NON autorizza deploy anticipato (regola pre-registrata).") else: if sh >= SH_DEPLOY and dd < DD_MAX and haircut_pp < HAIRCUT_MAX_PP: print("\n -> GATE SUPERATO: procedere con weights_tilt_null sui pesi candidati " "{10,15,20}% e stima maxDD combinata (<15%) prima del deploy.") elif sh >= 0: print("\n -> SOTTO SOGLIA ma >=0: estensione unica fino al " f"{DECISION_EXT} (se gia' estesa: RITIRO).") else: print("\n -> Sharpe forward NEGATIVO: RITIRO dal forward-monitor.") if __name__ == "__main__": main()