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:
Adriano Dal Pastro
2026-06-29 19:48:36 +00:00
parent a158d0e2ae
commit aad69f9790
12 changed files with 2283 additions and 0 deletions
+57
View File
@@ -0,0 +1,57 @@
"""Test del filone DVOL-DIREZIONALE (scripts/research/dvol_directional.py).
Verifica le proprieta' che DEVONO valere per ogni segnale del progetto:
* il percentile espandente del DVOL e' CAUSALE (rank su un prefisso == rank(full)[:cut]);
* il leader (DVOL-fear long-flat) passa causality_ok di altlib (niente future-peeking);
* a $600 (eval_weights_smallcap) l'haircut e' trascurabile (segnale eseguibile, low-turnover);
* sign-falsification: la tesi (buy-the-fear) batte il suo flip (buy-the-calm) sull'era DVOL.
"""
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" / "research" / "alt"))
import altlib as al # noqa: E402
from scripts.research import dvol_directional as dd # noqa: E402
def test_expanding_rank_is_causal():
"""rank calcolato su un prefisso deve coincidere con rank(full) ristretto al prefisso."""
x = np.asarray(al.dvol(al.get("BTC", "1d"), "BTC"), float)
full = dd._expanding_rank_arr(x)
cut = int(len(x) * 0.7)
pref = dd._expanding_rank_arr(x[:cut])
a, b = full[:cut], pref
m = np.isfinite(a) & np.isfinite(b)
assert m.sum() > 100
assert np.max(np.abs(a[m] - b[m])) < 1e-9
def test_leader_causality_ok():
"""Il leader (DVOL-fear q0.4 long-flat) non deve avere look-ahead (altlib causality_ok)."""
fn = dd.make_dvol_level(0.4, "fear", True)
co = al.causality_ok(fn, tf="1d")
assert co["ok"], co
assert co["max_tail_diff"] <= 1e-6
@pytest.mark.parametrize("asset", ["BTC", "ETH"])
def test_leader_executable_at_600(asset):
"""A $600 il segnale e' a basso turnover: haircut Sharpe trascurabile, trade eseguiti reali."""
fn = dd.make_dvol_level(0.4, "fear", True)
df = al.get(asset, "1d")
sc = al.eval_weights_smallcap(df, al._call_target(fn, df, asset), capital=600, min_order=5)
assert sc["n_executed_trades"] > 10
assert abs(sc["sharpe_haircut"]) < 0.10
def test_sign_falsification():
"""La tesi (buy-the-fear) deve battere il flip (buy-the-calm) sull'era DVOL."""
thesis = dd.era_full_sharpe(dd.make_dvol_level(0.5, "fear", True))["sharpe"]
flip = dd.era_full_sharpe(dd.make_dvol_level(0.5, "calm", True))["sharpe"]
assert thesis > flip
+59
View File
@@ -0,0 +1,59 @@
"""Test del filone B — INTRADAY REGIME (scripts/research/intraday_regime.py).
Verifica leggera e veloce (un solo asset) che:
* i target factory producano array della lunghezza giusta e CAUSALI (no look-ahead);
* il meccanismo ERM (efficiency-ratio regime momentum) sia long/short di natura;
* la cella canonica ERM 8h L=2 thr=0.35 abbia Sharpe FULL positivo netto fee (sanity dell'edge).
NB: NON costruisce la baseline TP01/SKH01 (lento) — quello e' nello script di ricerca.
"""
import sys
import importlib.util
import numpy as np
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt")
import altlib as al # noqa: E402
_spec = importlib.util.spec_from_file_location(
"intraday_regime", "/opt/docker/PythagorasGoal/scripts/research/intraday_regime.py")
ir = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(ir)
def test_erm_target_shape_and_ls():
df = al.get("BTC", "8h")
tgt = ir.make_erm(tf="8h", L_days=2.0, thr=0.35, long_flat=False)(df)
assert len(tgt) == len(df)
assert np.isfinite(tgt).all()
# L/S: deve esistere sia esposizione long sia short (non e' long-only)
assert (tgt > 0).any() and (tgt < 0).any()
# long_flat=True -> nessuno short
tgt_lf = ir.make_erm(tf="8h", L_days=2.0, thr=0.35, long_flat=True)(df)
assert (tgt_lf >= -1e-9).all()
def test_erm_causal_no_leak():
# causality_ok ricalcola il target su prefissi troncati e pretende che la coda combaci
res = al.causality_ok(ir.make_erm(tf="8h", L_days=2.0, thr=0.35, long_flat=False),
tf="8h", assets=("BTC",))
assert res["ok"], f"look-ahead in ERM: {res}"
def test_erm_winner_positive_full_sharpe():
fn = ir.make_erm(tf="8h", L_days=2.0, thr=0.35, long_flat=False)
for a in ("BTC", "ETH"):
df = al.get(a, "8h")
ev = al.eval_weights(df, fn(df), fee_side=0.0005) # 0.10% RT
assert ev["full"]["sharpe"] > 0.5, f"{a} full Sharpe {ev['full']['sharpe']}"
def test_vbr_and_tod_causal():
for fn in (ir.make_vbr(tf="8h", k=1.0, atr_win=14, long_flat=False),
ir.make_tod(tf="1h", long_flat=True)):
res = al.causality_ok(fn, tf=("8h" if fn is not None else "1h"),
assets=("BTC",)) if False else None
# VBR causale (8h) e TOD causale (1h), un asset per velocita'
assert al.causality_ok(ir.make_vbr(tf="8h", k=1.0, atr_win=14, long_flat=False),
tf="8h", assets=("BTC",))["ok"]
assert al.causality_ok(ir.make_tod(tf="1h", long_flat=True),
tf="1h", assets=("BTC",))["ok"]
+90
View File
@@ -0,0 +1,90 @@
"""Test del filone D — MACRO REGIME-GATE (scripts/research/macro_regime_gate.py).
Verifica leggera che:
* il frame macro (ETF daily) carichi e sia su un calendario monotono;
* i gate builder producano un gate in [g_off, 1] e CAUSALE (SMA/ratio rolling, no future);
* align_gate sia backward-only (la barra crypto i usa solo gate equity con label <= i);
* il VERDETTO del filone regga: il gate macro e' RIDONDANTE col trend di TP01 — "lavora"
(riduce una posizione TP01 NON gia' flat) solo in una piccola quota di giorni.
NB: un solo gate per i test lenti (costruisce TP01) -> velocita'.
"""
import sys
import importlib.util
import numpy as np
sys.path.insert(0, "/opt/docker/PythagorasGoal")
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt")
import altlib as al # noqa: E402
_spec = importlib.util.spec_from_file_location(
"macro_regime_gate", "/opt/docker/PythagorasGoal/scripts/research/macro_regime_gate.py")
mg = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(mg)
def test_macro_frame_loads_and_monotone():
mf = mg.macro_frame()
for col in ("spy", "qqq", "hyg", "lqd", "gld", "tlt", "ief"):
assert col in mf.columns, f"colonna macro mancante: {col}"
ts = mf["timestamp"].values
assert (np.diff(ts) > 0).all(), "calendario macro non strettamente crescente"
# spy non-null dopo l'inizio (master del calendario)
assert np.isfinite(mf["spy"].values[-1])
def test_gate_in_range_and_binary():
mf = mg.macro_frame()
for gdf, g_off in ((mg.gate_trend(mf, "spy", 200, 0.0), 0.0),
(mg.gate_combo(mf, 200, 0.5), 0.5)):
g = gdf["gate"].values
fin = g[np.isfinite(g)]
assert len(fin) > 0
assert (fin >= g_off - 1e-9).all() and (fin <= 1.0 + 1e-9).all(), \
"gate fuori da [g_off, 1]"
assert len(gdf) == len(mf)
def test_gate_trend_causal_prefix():
# gate_trend usa solo SMA rolling -> ricalcolato su un prefisso, la coda combacia
mf = mg.macro_frame()
full = mg.gate_trend(mf, "spy", 200, 0.0)["gate"].values
k = len(mf) - 300
pref = mg.gate_trend(mf.iloc[:k].reset_index(drop=True), "spy", 200, 0.0)["gate"].values
a, b = full[:k], pref
both = np.isfinite(a) & np.isfinite(b)
assert both.any()
assert np.allclose(a[both], b[both]), "look-ahead nel gate_trend (prefix != coda)"
def test_align_gate_backward_no_future():
# la barra crypto i deve mappare a un gate equity con timestamp <= timestamp crypto i
mf = mg.macro_frame()
gate_df = mg.gate_trend(mf, "spy", 200, 0.0)
df = al.get("BTC", "1d")
g = mg.align_gate(gate_df, df)
assert len(g) == len(df)
assert np.isfinite(g).all()
# nessun valore di gate puo' provenire dal futuro: per una manciata di barre crypto,
# il gate allineato deve coincidere con l'ultimo gate equity NON-NaN con ts <= ts_crypto
gd = gate_df.dropna(subset=["gate"]).sort_values("timestamp")
gt, gv = gd["timestamp"].values, gd["gate"].values
cts = df["timestamp"].astype("int64").values
for i in range(len(cts) - 1, max(len(cts) - 50, 0), -1):
prior = np.searchsorted(gt, cts[i], side="right") - 1
if prior < 0:
continue
assert abs(g[i] - gv[prior]) < 1e-9, f"align_gate usa gate futuro a i={i}"
def test_verdict_gate_is_redundant_with_trend():
# IL VERDETTO del filone D: TP01 e' gia' flat nei crash -> il gate macro "lavora"
# (riduce una posizione NON gia' flat) solo in una piccola quota di giorni.
mf = mg.macro_frame()
gate_df = mg.gate_combo(mf, 200, 0.0) # combo SPY+HYG+HYG/LQD, de-risk a 0
diag = mg.redundancy_diag(gate_df)
for a in mg.ASSETS:
r = diag[a]
# esposizione TP01 nei giorni risk-off gia' bassa, e gate "lavora" raramente
assert r["pct_days_gate_works"] < 0.20, \
f"{a}: gate lavora {r['pct_days_gate_works']} (atteso piccolo = ridondante col trend)"
+98
View File
@@ -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}"