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>
102 lines
4.0 KiB
Python
102 lines
4.0 KiB
Python
"""Lock del forward-monitor XSR01 (candidato trovato 2026-07-25).
|
||
|
||
Pin degli invarianti che rendono la finestra forward interpretabile. Se uno di questi salta, i
|
||
numeri accumulati non valgono piu' e la finestra va azzerata consapevolmente (--reset), non
|
||
lasciata scorrere con una config diversa.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
import numpy as np
|
||
import pytest
|
||
|
||
ROOT = Path(__file__).resolve().parents[1]
|
||
sys.path.insert(0, str(ROOT))
|
||
sys.path.insert(0, str(ROOT / "scripts" / "live"))
|
||
sys.path.insert(0, str(ROOT / "scripts" / "research"))
|
||
|
||
pytest.importorskip("pandas")
|
||
import pandas as pd # noqa: E402
|
||
|
||
paper_xsr = pytest.importorskip("paper_xsr")
|
||
|
||
|
||
def _panel():
|
||
try:
|
||
return paper_xsr.build_panel()
|
||
except (FileNotFoundError, ValueError):
|
||
pytest.skip("parquet Hyperliquid assenti")
|
||
|
||
|
||
def test_config_congelata():
|
||
"""La config e' il contratto della finestra forward: cambiarla la invalida."""
|
||
from r0725_statarb_multi import CAP, SGN, TARGET_VOL, VOL_WIN, W
|
||
assert (W, SGN, TARGET_VOL, VOL_WIN, CAP) == (45, +1, 0.20, 30, 2.0)
|
||
assert paper_xsr.FEE_LEG == 0.0005
|
||
assert paper_xsr.MIN_ORDER == 5.0
|
||
assert paper_xsr.MODELED_CAPITAL == 2000.0
|
||
|
||
|
||
def test_dollar_neutral_per_costruzione():
|
||
"""Il demeaning DEVE dare somma dei pesi ~0: e' cio' che annulla la gamba BTC comune."""
|
||
_, _, W, _, _, _ = _panel()
|
||
net = np.abs(W.sum(axis=1))
|
||
assert np.nanmax(net) < 1e-12, f"non dollar-neutral: max |somma pesi| = {np.nanmax(net)}"
|
||
|
||
|
||
def test_timestamp_in_millisecondi_non_1970():
|
||
"""Trappola pandas gia' pagata dal progetto: astype/view int64 su indice tz-aware.
|
||
I ts devono essere ms epoch plausibili, non nanosecondi ne' secondi."""
|
||
ts, idx, _, _, _, _ = _panel()
|
||
assert ts.dtype == np.int64
|
||
assert pd.Timestamp(int(ts[-1]), unit="ms", tz="UTC").year >= 2024
|
||
assert pd.Timestamp(int(ts[0]), unit="ms", tz="UTC").year >= 2023
|
||
# coerenza con l'indice da cui derivano
|
||
assert pd.Timestamp(int(ts[-1]), unit="ms", tz="UTC").date() == idx[-1].date()
|
||
|
||
|
||
def test_universo_50_alt_senza_btc():
|
||
_, _, _, _, _, syms = _panel()
|
||
assert len(syms) >= 45, f"attesi ~50 alt certificati, trovati {len(syms)}"
|
||
assert "BTC" not in syms, "BTC e' il riferimento, non una gamba"
|
||
|
||
|
||
def test_step_causale_la_posizione_nuova_non_guadagna_la_barra_corrente():
|
||
"""Il rendimento della barra si calcola sui pesi TENUTI (decisi prima), mai sul target."""
|
||
k = 3
|
||
bk = paper_xsr._book(1000.0, k)
|
||
bk["w"] = [0.10, -0.10, 0.0]
|
||
w_target = np.array([-0.50, 0.50, 0.0]) # ribaltamento totale
|
||
r_alt = np.array([0.10, -0.10, 0.0]) # premia i pesi VECCHI
|
||
net = paper_xsr._step(bk, w_target, r_alt, 0.0, None)
|
||
assert net > 0, "il netto deve riflettere i pesi tenuti, non il nuovo target"
|
||
assert bk["w"] == pytest.approx(list(w_target)) # e dopo si e' ribilanciato
|
||
|
||
|
||
def test_min_order_blocca_le_gambe_piccole():
|
||
"""A capitale piccolo le gambe sub-min-order NON si muovono: e' il punto dei libri REAL."""
|
||
k = 2
|
||
bk = paper_xsr._book(600.0, k)
|
||
w_target = np.array([0.001, -0.001]) # 600*0.001 = $0.60 << $5
|
||
paper_xsr._step(bk, w_target, np.zeros(k), 0.0, paper_xsr.MIN_ORDER)
|
||
assert bk["w"] == [0.0, 0.0], "gamba sub-min-order eseguita: il libro REAL sarebbe fittizio"
|
||
assert bk["n_skip"] == 2
|
||
|
||
big = paper_xsr._book(600.0, k)
|
||
paper_xsr._step(big, np.array([0.20, -0.20]), np.zeros(k), 0.0, paper_xsr.MIN_ORDER)
|
||
assert big["w"] == pytest.approx([0.20, -0.20]) # $120 per gamba: si esegue
|
||
assert big["n_fill"] == 2
|
||
|
||
|
||
def test_fee_addebitata_su_alt_piu_gamba_btc():
|
||
"""La fee copre le gambe alt E la gamba BTC netta: a ribilanciamento nullo, netto = 0."""
|
||
k = 2
|
||
bk = paper_xsr._book(1000.0, k)
|
||
net = paper_xsr._step(bk, np.zeros(k), np.zeros(k), 0.0, None)
|
||
assert net == pytest.approx(0.0)
|
||
bk2 = paper_xsr._book(1000.0, k)
|
||
net2 = paper_xsr._step(bk2, np.array([0.5, -0.5]), np.zeros(k), 0.0, None)
|
||
assert net2 == pytest.approx(-paper_xsr.FEE_LEG * 1.0) # |0.5|+|−0.5| alt, BTC netto 0
|