9af6693e4f
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>
141 lines
7.2 KiB
Python
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()
|