"""Test del filone C v3: cross-sectional 'low-risk cousins' (MAX / IVOL / AMIHUD) su Hyperliquid (scripts/research/xsec_v3_lowrisk). Verifica i GATE strutturali, non i numeri esatti (storia corta): - lo script importa ed esegue (catalogo meccanismi costruibile, una cella di engine gira); - i meccanismi sono CAUSALI (prefix-consistency bit-a-bit), incluso AMIHUD che richiede il riallineamento del volume sul prefisso (path piu' delicato); - la selezione 'robust_candidate' RIFIUTA il holdout-fitting (config negativa in-sample con HOLD alto) come prescritto dallo scorer indurito del progetto; - IVOL sui 19 major ha edge in-sample positivo (il LEAD principale). """ from __future__ import annotations import sys from pathlib import Path PROJECT_ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(PROJECT_ROOT)) sys.path.insert(0, str(PROJECT_ROOT / "scripts" / "research")) import numpy as np import pytest import importlib.util _spec = importlib.util.spec_from_file_location( "xsec_v3_lowrisk", PROJECT_ROOT / "scripts" / "research" / "xsec_v3_lowrisk.py") xv3 = importlib.util.module_from_spec(_spec) _spec.loader.exec_module(xv3) xv = xv3.xv # harness collaudato riusato dal modulo v3 from src.portfolio.portfolio import to_daily, metrics @pytest.fixture(scope="module") def majors(): return xv.load_matrix(xv.XS_UNIVERSE) def test_imports_and_builds_mechanisms(majors): """Lo script importa e il catalogo dei 3 'low-risk cousins' (+ entrambi i segni AMIHUD) e' costruibile.""" _, VOL = majors mechs = xv3.build_mechanisms(VOL) assert set(mechs) == {"MAX", "IVOL", "AMIHUD_ILLIQ", "AMIHUD_LIQ"} for mn, (_builder, cfgs) in mechs.items(): assert len(cfgs) == 12 # B{20,30,60} x H{5,10} x k{5,8} def test_engine_executes_a_cell(majors): """Esegue una cella dell'engine (IVOL B30 H5 k8 sui 19 major): serie giornaliera finita, std>0, turnover>0 e Sharpe FULL positivo robusto (il LEAD documentato).""" PX, VOL = majors score_at, warm = xv3.make_ivol(PX, 30) s, turn = xv.xs_engine(PX, VOL, score_at, H=5, k=8, warmup=warm) d = to_daily(s) assert np.isfinite(d.values).all() and d.std() > 0 assert turn > 0 assert metrics(d)["sharpe"] > 0.5 # IVOL 19-major = LEAD (edge in-sample+OOS) @pytest.mark.parametrize("mech,cfg", [ ("MAX", dict(B=60, H=5, k=5)), ("IVOL", dict(B=30, H=5, k=8)), ("AMIHUD_ILLIQ", dict(B=30, H=10, k=5)), # path col riallineamento del volume ]) def test_mechanism_is_causal(majors, mech, cfg): """Nessun look-ahead: ricostruito su un prefisso, la coda combacia bit-a-bit con la run completa. Per AMIHUD verifica anche che il volume sia riallineato al prefisso (non al full-sample).""" PX, VOL = majors builder, _ = xv3.build_mechanisms(VOL)[mech] res = xv.causality_prefix_check(PX, VOL, builder, cfg) assert res["ok"], f"{mech} look-ahead: max_tail_diff={res['max_tail_diff']}" assert res["max_tail_diff"] == 0.0 def test_robust_candidate_rejects_holdout_fit(): """La selezione GIUDICATA scarta il holdout-fitting: una config NEGATIVA in-sample con HOLD alto non e' eleggibile; serve in-sample>=0.5 E HOLD>0. Se nessuna ha edge in-sample -> None.""" rows = [ dict(insample=-1.2, hold=1.0, full=0.1), # holdout-fit -> escluso dict(insample=0.9, hold=0.8, full=0.95), # edge in-sample + OOS -> eleggibile dict(insample=0.6, hold=-0.2, full=0.3), # HOLD<0 -> escluso ] c = xv3.robust_candidate(rows) assert c is not None and c["insample"] == 0.9 assert xv3.robust_candidate([dict(insample=0.1, hold=2.0, full=0.0)]) is None