Files
PythagorasGoal/scripts/research/r0724_statarb_deploy_gate.py
Adriano Dal Pastro 9af6693e4f research: MAT01 e generalizzazione STATARB — 2 refutazioni pulite + addendum al gate 27/09
MAT01 (r0725_mat01_multiasset_trend.py, r0725_mat01b_regime.py) — SCARTATO.
Allargare il trend difensivo da 6 a 18 ETF multi-asset-class a meccanismo GTAA
CONGELATO: MAT18 perde 3/4 finestre disgiunte e, a PARI VOLATILITA', il suo
minor drawdown si INVERTE (-20.6% vs -15.4%) -> era solo de-levering. Terza
occorrenza del null de-levering dopo VRP-DD e TP01xDVOL. Corr->crypto invariata
(0.110 vs 0.116): zero guadagno di diversificazione.
Risultato positivo conservato: la curva d'ampiezza e' monotona (mediana OOS 0.44
a k=1 -> 0.80 a k=18, DD -16.0% -> -6.9%, satura a k~10-12), ma GTAA6 sta gia' al
95° pctl dei 6-subset casuali — e non e' cherry-picked (contiene TLT, il peggiore
dei 18) -> niente piu' ampiezza da raccogliere su quello sleeve.

STATARB-MULTI (r0725_statarb_multi.py) — il meccanismo congelato (W=45, sgn=+1)
su tutte le 50 coppie alt/BTC, out-of-pair-sample. REGGE: 82% Sharpe>0, 0/50
degeneri (mono 53%), perm p<0.05 nel 18% (atteso 5%). E ETH/BTC e' al rango
22/50 (58° pctl): la coppia scopritrice NON e' un outlier -> per il gate 27/09
l'ipotesi "fortuna di una coppia" e' refutata. NON e' uno sleeve: ampiezza
effettiva ~4.5 (corr media 0.204, gamba BTC condivisa), paniere IC95%
[-0.12, 1.72] con t 1.31.
Errore di metodo corretto in-sessione: il primo null statico usava
sign(mean(segnale)) sull'intero campione = look-ahead; rifatto causale + a priori.

STATARB-EQ (r0725_statarb_eq.py) — SCARTATO. Stesso meccanismo su 12 coppie ETF
a priori, 28.5 anni, ampiezza effettiva 9.9: paniere Sharpe -1.00 (t -5.33),
negativo in 4/4 decadi. Ma la lettura sta nel LORDO (-0.17, non -1.00): due terzi
sono drag di turnover -> non esiste strategia specchio. Il segno lordo dice che
sulle azioni il residuo REVERTE mentre sul crypto CONTINUA: meccanismo
plausibilmente crypto-specifico. Il gate 27/09 non puo' appoggiarsi
all'argomento "fenomeno universale".

r0724_statarb_deploy_gate.py: addendum PRE-REGISTRATO oggi (forward-day 26 di 90,
64 giorni prima della decisione) — riporta anche lo Sharpe della statica
"sempre short ETH vs BTC" a pari vol-target sulla stessa finestra. Le soglie del
2026-07-24 NON sono toccate: e' diagnostica non binding. Lettura odierna
(26 barre): STATARB +5.70 vs benchmark -4.98, posizione corrente LONG lo spread
= opposta alla statica.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 09:47:47 +00:00

141 lines
7.2 KiB
Python

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