Files
Adriano Dal Pastro b8c9430bd7 feat(research): XSR01 "Cross-Sectional Residual" — candidato nuovo in forward-monitor
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>
2026-07-25 09:48:11 +00:00

102 lines
4.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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