"""Test del filone C: cross-sectional NON-momentum su Hyperliquid (scripts/research/xsec_v2_nonmom). Verifica i GATE strutturali, non i numeri esatti (storia corta, ricerca): l'engine e' CAUSALE (prefix-consistency, zero look-ahead), le fee MONOTONE (piu' fee -> Sharpe <=), il reversal grezzo e' MORTO (plateau negativo), e il low-vol factor sui 19 major e' positivo in-sample (il LEAD). """ from __future__ import annotations import sys from pathlib import Path PROJECT_ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(PROJECT_ROOT)) import numpy as np import pytest from src.portfolio.portfolio import to_daily, metrics from src.portfolio.sleeves import XS_UNIVERSE import importlib.util _spec = importlib.util.spec_from_file_location( "xsec_v2_nonmom", PROJECT_ROOT / "scripts" / "research" / "xsec_v2_nonmom.py") xv = importlib.util.module_from_spec(_spec) _spec.loader.exec_module(xv) @pytest.fixture(scope="module") def majors(): return xv.load_matrix(XS_UNIVERSE) def test_universe_loads_clean(majors): PX, VOL = majors assert PX.shape[1] == len(XS_UNIVERSE) assert PX.shape[0] > 800 # ~2.5 anni a 1d assert PX.index.is_monotonic_increasing def test_engine_is_causal_no_lookahead(majors): """L'engine NON deve guardare al futuro: ricostruito su un prefisso, la coda combacia bit-a-bit con la run completa (gate #1 della metodologia).""" PX, VOL = majors builder, _ = xv.mechanisms()["LOWVOL"] cfg = dict(B=30, H=10, k=5) res = xv.causality_prefix_check(PX, VOL, builder, cfg) assert res["ok"], f"look-ahead rilevato: max_tail_diff={res['max_tail_diff']}" assert res["max_tail_diff"] == 0.0 # anche un meccanismo residuo (usa beta rolling + mercato) deve essere causale builder_i, _ = xv.mechanisms()["IMOM"] res_i = xv.causality_prefix_check(PX, VOL, builder_i, dict(L=30, H=5, k=8, B=60)) assert res_i["ok"], f"IMOM non causale: {res_i['max_tail_diff']}" def test_fee_is_monotone(majors): """Piu' fee non puo' MAI alzare lo Sharpe (su una config con turnover non nullo).""" PX, VOL = majors builder, _ = xv.mechanisms()["LOWVOL"] score_at, warm = builder(PX, dict(B=30, H=10, k=5)) s0, t0 = xv.xs_engine(PX, VOL, score_at, 10, 5, fee=0.0, warmup=warm) s2, t2 = xv.xs_engine(PX, VOL, score_at, 10, 5, fee=0.002, warmup=warm) assert t0 > 0 assert metrics(to_daily(s0))["sharpe"] >= metrics(to_daily(s2))["sharpe"] - 1e-9 def test_raw_reversal_is_dead(majors): """Reversal cross-sectional grezzo = NESSUN plateau positivo (coerente con la lezione del progetto: la mean-reversion e' artefatto). Almeno meta' delle config dev'essere FULL<=0.""" PX, VOL = majors builder, cfgs = xv.mechanisms()["REV"] neg = 0; tot = 0 for p in cfgs: score_at, warm = builder(PX, p) d = to_daily(xv.xs_engine(PX, VOL, score_at, p["H"], p["k"], warmup=warm)[0]) if d.std() == 0: continue tot += 1 if metrics(d)["sharpe"] <= 0: neg += 1 assert tot > 0 assert neg >= tot / 2, f"reversal inatteso: solo {neg}/{tot} config FULL<=0" def test_lowvol_factor_positive_insample(majors): """Il LEAD: low-vol factor sui 19 major (B30 H10 k5) ha FULL Sharpe positivo e robusto (plateau 100% positivo). Numero non vincolato (ricerca), solo il segno/robustezza.""" PX, VOL = majors builder, cfgs = xv.mechanisms()["LOWVOL"] score_at, warm = builder(PX, dict(B=30, H=10, k=5)) d = to_daily(xv.xs_engine(PX, VOL, score_at, 10, 5, warmup=warm)[0]) assert metrics(d)["sharpe"] > 0.5 # plateau: ogni config LOWVOL deve avere FULL>0 (factor robusto ai parametri in-sample) pos = 0; tot = 0 for p in cfgs: sa, w = builder(PX, p) dd = to_daily(xv.xs_engine(PX, VOL, sa, p["H"], p["k"], warmup=w)[0]) if dd.std() == 0: continue tot += 1; pos += metrics(dd)["sharpe"] > 0 assert pos == tot, f"plateau low-vol non pieno: {pos}/{tot}"