research(crypto): 4 filoni 2026-06-29 — ERM lead sub-daily (forward), 3 scartati/deboli
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>
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
"""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}"
|
||||
Reference in New Issue
Block a user