"""r0725_xsr_deploy_gate — gate di decisione PRE-REGISTRATO per XSR01 (fissato 2026-07-25). PERCHE' PRE-REGISTRATO. La regola si fissa oggi, forward-day 0, PRIMA di vedere qualunque barra out-of-sample. Decidere "a occhio" dopo aver visto i numeri e' selection-on-forward — il bias che questo progetto ha gia' pagato due volte (gate SELECTION-ON-HOLDOUT 2026-06-29; EW-STR refutato come selezione di 2° ordine 2026-07-01). Stesso trattamento gia' dato a STATARB-RESID. CONTESTO. XSR01 e' il candidato del 2026-07-25: cross-sectional residual dollar-neutral sui 50 alt HL. In-sample (2.6 anni) fa Sharpe netta 1.82 / lorda 2.70, maxDD -2.6%, marginale ADDS, deflated-Sharpe 0.985, corr ~0 a tutti e 5 gli sleeve. Sopravvive a tre null fee-neutrali e al test di lag. NON e' nel book per tre motivi dichiarati: storia corta e monotona crescente, `weights_tilt_null` fallito, e margine di costo sottile con slippage NON modellato. REGOLA (immutabile — ogni modifica va motivata nel diario come violazione): - Data decisione: 2026-10-23 (90 giorni forward dal 2026-07-25). Non prima. - Metrica primaria: Sharpe annualizzato di `net_modeled` su TUTTA la finestra forward. - GUARDIA DI COSTO, la piu' importante per questa strategia (turnover 38.5% del lordo/giorno su 50 alt anche illiquidi): il rapporto Sharpe_forward / Sharpe_in_sample_netta(1.82) va letto insieme allo scarto MODELED-vs-REAL$5000. Se l'haircut di eseguibilita' a $5000 supera il 40% del rendimento modellato, la strategia e' un artefatto di costo e va RITIRATA a prescindere dallo Sharpe. - Esiti: Sharpe >= 1.0 AND haircut$5000 <= 40% -> CANDIDATO SLEEVE: proporre peso {5,10}% e giudicarlo con `weights_tilt_null` (che oggi NON passa) + maxDD combinato. Deploy solo se il tilt-null passa E il capitale e' >= $5000. 0.3 <= Sh < 1.0 -> ESTENDI una sola volta di 90g (2027-01-21). Sharpe < 0.3 OR haircut > 40% -> RITIRO dal forward-monitor. - Guardie accessorie: config invariata (il test `tests/test_paper_xsr.py` la blocca); universo costante a 50 gambe (un cambio impone --reset e azzera la finestra). Uso: `uv run python scripts/research/r0725_xsr_deploy_gate.py` """ 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_xsr" / "returns.jsonl" START = date(2026, 7, 25) DECISION = date(2026, 10, 23) DECISION_EXT = date(2027, 1, 21) SH_DEPLOY = 1.0 SH_RETIRE = 0.3 HAIRCUT_MAX = 0.40 IS_SHARPE_NET = 1.82 # riferimento in-sample, per contesto def main() -> None: print("=" * 92) print(" XSR01 — gate di decisione PRE-REGISTRATO (fissato 2026-07-25, forward-day 0)") print("=" * 92) if not RETURNS.exists(): print(f" nessun dato forward ancora: {RETURNS} assente.") print(f" data decisione: {DECISION} (mancano {(DECISION - date.today()).days} giorni)") return rows = [json.loads(x) for x in RETURNS.read_text().splitlines() if x.strip()] if len(rows) < 5: print(f" finestra forward troppo corta ({len(rows)} barre).") print(f" data decisione: {DECISION} (mancano {(DECISION - date.today()).days} giorni)") return rm = np.array([r["net_modeled"] for r in rows]) key5 = "net_REAL-$5000" r5 = np.array([r.get(key5, np.nan) for r in rows]) 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))) tot_m = float(np.prod(1 + rm) - 1) tot_5 = float(np.prod(1 + r5[np.isfinite(r5)]) - 1) if np.isfinite(r5).any() else float("nan") haircut = (tot_m - tot_5) / abs(tot_m) if tot_m not in (0.0,) and np.isfinite(tot_5) else float("nan") today = date.today() print(f" finestra forward : {rows[0]['dt'][:10]} -> {rows[-1]['dt'][:10]} ({len(rows)} barre)") print(f" Sharpe fwd (mod) : {sh:+.2f} (in-sample netta era {IS_SHARPE_NET:.2f})") print(f" maxDD fwd : {dd:.1%}") print(f" ritorno MODELED : {tot_m*100:+.2f}% REAL-$5000: {tot_5*100:+.2f}%") print(f" haircut eseguib. : {haircut*100:.1f}% (guardia <= {HAIRCUT_MAX*100:.0f}%)") 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).") return if np.isfinite(haircut) and haircut > HAIRCUT_MAX: print("\n -> RITIRO: l'haircut di eseguibilita' supera la guardia. E' costo, non edge.") elif sh >= SH_DEPLOY: print("\n -> CANDIDATO SLEEVE: proporre peso {5,10}% e passarlo a weights_tilt_null " "(oggi NON passa) + maxDD combinato. Deploy solo con capitale >= $5000.") elif sh >= SH_RETIRE: print(f"\n -> SOTTO SOGLIA ma >= {SH_RETIRE}: estensione unica fino al {DECISION_EXT}.") else: print("\n -> RITIRO dal forward-monitor.") if __name__ == "__main__": main()