"""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%). ADDENDUM PRE-REGISTRATO IL 2026-07-25 (forward-day 26 di 90, 64 giorni PRIMA della decisione — quindi pre-registrazione vera, non selezione a posteriori). Motivo: il test out-of-pair-sample `r0725_statarb_multi.py` ha applicato il meccanismo congelato alle 50 coppie alt/BTC di Hyperliquid e ha trovato che, su 60% delle coppie, una posizione STATICA "sempre short alt vs BTC" a pari vol-target batte il segnale (uplift medio -0.11). Su ETH/BTC il pareggio e' quasi esatto: Sharpe segnale 0.46 vs statica-short 0.48 sulla finestra HL 2024-2026. Tradotto: una parte di cio' che STATARB-RESID guadagna potrebbe essere il beta di regime "ETH sottoperforma BTC", non timing. Si aggiunge quindi UNA misura da riportare alla decisione: lo Sharpe, sulla STESSA finestra forward, della statica sempre-short a pari vol-target (BENCH_SHORT). Non e' una soglia binding: le soglie pre-registrate il 2026-07-24 (Sharpe>=0.5, DD<10%, haircut<0.5pp) restano quelle e NON sono state toccate. E' una diagnostica obbligatoria da leggere insieme, e la decisione se renderla binding spetta esplicitamente all'operatore, non allo script. NB confortante gia' oggi: la posizione forward corrente e' LONG lo spread (+0.61 = long ETH / short BTC), cioe' OPPOSTA alla scommessa statica -> la finestra forward sta testando il segnale in un modo che la statica non spiega. 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 _bench_static_short(rows: list[dict]) -> tuple[float, int] | None: """Addendum 2026-07-25: Sharpe di una posizione STATICA short-spread (short ETH / long BTC) a pari vol-target, sulla STESSA finestra forward. Stessa meccanica di costo del monitor (fee su 2 gambe per |delta pos|), ma qui il turnover viene solo dal vol-target.""" import sys as _sys from pathlib import Path as _P _sys.path.insert(0, str(_P(__file__).resolve().parents[2])) _sys.path.insert(0, str(_P(__file__).resolve().parents[1] / "research")) try: import pandas as pd from orthogonal_signals import build_joint, spread_ret except Exception: return None try: j = build_joint("1d") except Exception: return None ts = {int(r["ts"]) for r in rows} s = spread_ret(j) vol = pd.Series(s).rolling(30, min_periods=30).std().values * np.sqrt(365) scal = np.where((vol > 0) & np.isfinite(vol), 0.20 / vol, 0.0) pos = np.clip(-1.0 * np.nan_to_num(scal), -2.0, 2.0) # SEMPRE short lo spread held = np.concatenate([[0.0], pos[:-1]]) net = held * s - 2.0 * 0.0005 * np.abs(np.diff(held, prepend=0.0)) mask = np.array([int(t) in ts for t in j["timestamp"].values]) r = net[mask] r = r[np.isfinite(r)] if len(r) < 10 or r.std() == 0: return None return float(r.mean() / r.std() * np.sqrt(365)), int(len(r)) 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})") bench = _bench_static_short(rows) if bench is not None: sh_b, n_b = bench print(f"\n [addendum 2026-07-25, diagnostico NON binding]") print(f" benchmark statico 'sempre short ETH vs BTC' a pari vol-target, stessa finestra:") print(f" Sharpe benchmark {sh_b:+.2f} su {n_b} barre -> STATARB {sh:+.2f}, " f"differenza {sh - sh_b:+.2f}") print(" se STATARB non batte questo benchmark, il suo guadagno e' beta di regime") print(" (ETH sottoperforma BTC), non timing. Da leggere PRIMA di deliberare il deploy.") else: print("\n [addendum: benchmark statico non calcolabile — dati BTC/ETH non disponibili]") 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()