aad69f9790
Ricerca onesta su BTC/ETH + universo HL, branch separato (nessun impatto live).
Harness condiviso altlib (causale, fee 0.10% RT, marginal vs TP01, day-boundary,
haircut $600). Test 19/19 verdi.
- A DVOL direzionale -> LEAD hedge/DD-dampener, NON sleeve (buy-the-fear; is_hedge).
- B Intraday ERM 8h -> LEAD forte / forward-monitor: earns_slot=True, ADDS oltre
SKH01 (TP01+SKH+ERM 60/25/15 FULL 1.88/HOLD 1.46/DD 8.9%).
Caveat: plateau hold-out single-row, multiple-testing non
deflazionato, exec 8h. Controllo TOD = FAIL atteso.
- C Cross-sectional non-mom (low-vol HL) -> DEBOLE/forward-monitor (deflated-Sh 0.13,
storia 2.5a, non eseguibile $600) STAT-MODE.
- D Macro regime-gate -> RIDONDANTE col trend (corr->TP01 0.989), SCARTATO.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
99 lines
3.9 KiB
Python
99 lines
3.9 KiB
Python
"""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}"
|