b8c9430bd7
Primo candidato da molte ondate a superare marginale-ADDS, deflated-Sharpe 0.95, tre null avversariali e il test di lag, con corr ~0 a TUTTI e 5 gli sleeve attivi. COS'E'. Il meccanismo congelato di STATARB-RESID (W=45, sgn=+1, residuo OLS causale su BTC, z-score, tanh, vol-target 20%) applicato ai 50 alt HL, con posizioni DEMEANATE cross-sezionalmente ogni giorno. Il demeaning annulla ALGEBRICAMENTE la gamba BTC comune — sum(p_i-p̄)(r_i-r_btc) = sum(p_i-p̄)r_i — quindi non e' un paniere di coppie ma una strategia cross-sectional dollar-neutral, e l'ampiezza effettiva passa da 4.5 a 37.4. NB: non e' nato da un'idea nuova ma dalla DIAGNOSI di un fallimento (l'ampiezza effettiva 4.5 indicava un fattore comune da togliere). NUMERI (2.6 anni): Sharpe netta 1.82 (lorda 2.70), maxDD -2.6%, vol 2.3%. GATE: marginale ADDS (robust_oos + beats_noise_null + non-hedge + has_insample_edge); deflated-Sharpe 0.985 PASS; corr TP01 0.060 / XS01 -0.019 / VRP01 -0.001 / SKH01 0.001 / GTAA01 0.071; book a w=15% HOLD 2.36 -> 2.51, DD invariato. SCETTICO (r0725_statarb_demean_skeptic.py): 3 null A FEE-NEUTRALE (permutazione cross-sezionale / casuale / temporale a blocchi) centrati a ~0.01, candidato lordo 2.70 sopra il MASSIMO di 300 estrazioni (p<0.004); lag 1.82/1.19/0.81/0.51 a +0/1/2/3g = decadimento dolce di segnale lento, non firma di look-ahead; non ridondante con XS-momentum semplice (corr 0.17-0.29). Nota di metodo: i null vanno confrontati A FEE ZERO — permutare un segnale ne fa esplodere il turnover, quindi a fee piene il null perderebbe per COSTO invece che per assenza di informazione (p-value trionfale e falso). NON NEL BOOK, 3 motivi dichiarati: (a) storia 2.6a monoregime e CRESCENTE (Sh 2024 1.03 / 2025 1.98 / 2026 3.11) -> edge recente; (b) weights_tilt_null FALLITO (gate_pass=False a 10% e 15%); (c) margine di costo sottile — 1.82 a 0.05%/gamba, 0.93 a 0.10%, NEGATIVA a 0.20%, turnover 38.5% del lordo/g su 50 alt anche illiquidi con slippage NON modellato (rischio #1). ESEGUIBILITA': ticket/gamba $1.73 a $600 (sotto min-order $5 -> STAT-MODE oggi), $5.76 a $2000, $14.41 a $5000 -> diventa reale a ~$5k, non ai ~$20k di XS01. CABLATO: scripts/live/paper_xsr.py (config CONGELATA, 3 libri MODELED $2000 / REAL $600 / REAL $5000 con min-order per gamba, stato append-only) in cron_daily.sh; gate PRE-REGISTRATO a forward-day 0 (r0725_xsr_deploy_gate.py): decisione 2026-10-23, deploy solo se Sharpe>=1.0 E haircut di eseguibilita' a $5000 <=40% (se sfonda -> RITIRO a prescindere dallo Sharpe). tests/test_paper_xsr.py: 7 casi che bloccano config, dollar-neutralita', causalita' dello step, min-order e la trappola dei timestamp tz-aware (astype int64 -> epoche 1970, gemella di quella dell'ondata 2026-07-01). Book e pesi INVARIATI. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
101 lines
5.2 KiB
Python
101 lines
5.2 KiB
Python
"""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()
|