research(sweep): 5 thread paralleli — 0 nuovi sleeve, STATARB-RESID LEAD ortogonale+eseguibile
Ricerca onesta su aree inesplorate (harness altlib+xsec_v2_nonmom, tutti i gate incl. study_family_honest anti-selection-on-holdout). Branch main, nessun impatto live, test 143/143. 1 XSEC low-risk cousins (MAX/idio-vol/Amihud) -> 1 LEAD (IVOL), STAT-MODE, DSR 0.37<0.95 2 XSEC momentum-structure vs XS01 -> tutto REDUNDANT (sostituire XS01 distrugge hold) 3 Meta-allocazione dinamica (4 sleeve) -> pesi fissi vincono (gia quasi risk-parity) 4 Segnali ortogonali ETH/BTC (2 gambe) -> STATARB-RESID + DVOLSPREAD LEAD 5 1-gamba a segnale (MACD/RSI/Supertrend/...) -> 0/12 earns_slot (trend=TP01, MR morta, hedge) LEAD principale STATARB-RESID (mean-rev residuo ETH-b*BTC, OLS rolling, 2 gambe): primo stream INSIEME ortogonale (corr->book 0.027, beta-mkt 0.013) ED eseguibile a $600 (haircut ~0, NON STAT-MODE) -> cadono i 2 muri di XS01/opzioni. Resta solo il muro dell'edge (Sharpe 0.84, DSR 0.929 same-sign <0.95). Causalita+fee verificate dal coordinatore. Forward-monitor, non sleeve. Soffitto direzionale ~1.3 riconfermato. Diario 2026-06-29-strategy-search-5threads.md, CLAUDE.md agg. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
"""Test minimali per scripts/research/meta_allocation.py.
|
||||
|
||||
Verifica le proprieta' STRUTTURALI dell'harness di meta-allocazione (non l'edge — quello e' nel
|
||||
report): (1) i pesi-bersaglio + cash sommano a 1 per riga; (2) gli sleeve inattivi pesano 0;
|
||||
(3) lo schema vol-parity e' CAUSALE (un cambio dei rendimenti in t+k non altera i pesi <= t);
|
||||
(4) il cap del momentum e' rispettato; (5) il motore di simulazione conserva (vol nulla -> equity
|
||||
piatta) e il costo di ribilancio aumenta col turnover.
|
||||
"""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import pytest
|
||||
|
||||
from scripts.research import meta_allocation as M
|
||||
|
||||
|
||||
def _toy(n=400, A=4, seed=0):
|
||||
rng = np.random.default_rng(seed)
|
||||
R = rng.normal(0.0005, 0.01, size=(n, A))
|
||||
active = np.ones((n, A), bool)
|
||||
index = pd.date_range("2020-01-01", periods=n, freq="1D", tz="UTC")
|
||||
fixed_w = np.array([0.4125, 0.1875, 0.15, 0.25])
|
||||
return index, R, active, fixed_w
|
||||
|
||||
|
||||
def test_weights_plus_cash_sum_to_one():
|
||||
index, R, active, fixed_w = _toy()
|
||||
for fn in (M.scheme_base, M.scheme_volpar_pure, M.scheme_volpar_tilt,
|
||||
M.scheme_momentum, M.scheme_dd_cash, M.scheme_dd_defensive):
|
||||
W = fn(index, R, active, fixed_w) # ultima colonna = cash
|
||||
s = W.sum(axis=1)
|
||||
assert np.allclose(s, 1.0, atol=1e-9), f"{fn.__name__}: righe non sommano a 1 (max dev {np.abs(s-1).max():.2e})"
|
||||
assert (W >= -1e-12).all(), f"{fn.__name__}: pesi negativi"
|
||||
|
||||
|
||||
def test_inactive_sleeves_get_zero_weight():
|
||||
index, R, active, fixed_w = _toy()
|
||||
active[:, 2] = False # spegni lo sleeve 2 ovunque
|
||||
W = M.scheme_base(index, R, active, fixed_w)
|
||||
assert np.allclose(W[:, 2], 0.0), "uno sleeve inattivo riceve peso non nullo"
|
||||
assert np.allclose(W.sum(axis=1), 1.0)
|
||||
|
||||
|
||||
def test_volparity_is_causal():
|
||||
"""Un cambio dei rendimenti da t0 in poi NON deve alterare i pesi calcolati per t < t0."""
|
||||
index, R, active, fixed_w = _toy(n=400)
|
||||
t0 = 360
|
||||
W1 = M.scheme_volpar_pure(index, R, active, fixed_w)
|
||||
R2 = R.copy(); R2[t0:] *= 50.0 # shock futuro enorme
|
||||
W2 = M.scheme_volpar_pure(index, R2, active, fixed_w)
|
||||
assert np.allclose(W1[:t0], W2[:t0]), "VOL-PARITY non causale: pesi passati dipendono dal futuro"
|
||||
|
||||
|
||||
def test_momentum_respects_cap():
|
||||
index, R, active, fixed_w = _toy()
|
||||
cap = 0.55
|
||||
W = M.scheme_momentum(index, R, active, fixed_w, cap=cap)
|
||||
sleeve_w = W[:, :-1] # escludi cash
|
||||
assert sleeve_w.max() <= cap + 1e-6, f"cap momentum violato: max {sleeve_w.max():.3f} > {cap}"
|
||||
|
||||
|
||||
def test_simulate_flat_when_no_returns():
|
||||
index, R, active, fixed_w = _toy()
|
||||
Rz = np.zeros_like(R)
|
||||
W = M.scheme_base(index, Rz, active, fixed_w)
|
||||
sim = M.simulate(Rz, active, W, cost_rate=0.0)
|
||||
assert np.allclose(sim["daily"].values, 0.0, atol=1e-12), "equity non piatta con rendimenti nulli e costo zero"
|
||||
|
||||
|
||||
def test_rebalance_cost_increases_with_turnover():
|
||||
"""Uno schema ad alto turnover (vol-parity) deve pagare piu' costo del peso-fisso (basso turnover)."""
|
||||
index, R, active, fixed_w = _toy(seed=3)
|
||||
Wb = M.scheme_base(index, R, active, fixed_w)
|
||||
Wv = M.scheme_volpar_pure(index, R, active, fixed_w)
|
||||
tb = M.simulate(R, active, Wb)["turnover_per_year"]
|
||||
tv = M.simulate(R, active, Wv)["turnover_per_year"]
|
||||
assert tv > tb, f"il vol-parity dovrebbe avere turnover > peso-fisso (got {tv:.2f} vs {tb:.2f})"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-q"]))
|
||||
@@ -0,0 +1,83 @@
|
||||
"""Test minimali per orthogonal_signals.py — CAUSALITÀ dollar-neutral + dollar-neutrality (beta~0).
|
||||
|
||||
Lo scopo è blindare le due proprietà su cui poggia tutto il filone relative-value ETH/BTC:
|
||||
1. l'evaluator dollar-neutral è CAUSALE: pos[i] decisa a close[i] è tenuta SOLO durante la
|
||||
barra i+1 -> una decisione presa all'ultima barra non può toccare il backtest (no look-ahead),
|
||||
e il prefix-check sul segnale combacia con la coda del full.
|
||||
2. la fee è caricata su 2 GAMBE (ETH + BTC).
|
||||
3. dollar-neutrality: un segnale temporizzato sul ratio ha beta di mercato ~0 (ortogonale per
|
||||
costruzione) — il cuore della richiesta (stream scorrelato al book direzionale).
|
||||
"""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "scripts" / "research"))
|
||||
import orthogonal_signals as o # noqa: E402
|
||||
|
||||
|
||||
def _synthetic_joint(n: int = 60, seed: int = 0) -> pd.DataFrame:
|
||||
ts = pd.date_range("2022-01-01", periods=n, freq="D", tz="UTC")
|
||||
rng = np.random.default_rng(seed)
|
||||
cb = 100 * np.cumprod(1 + rng.normal(0, 0.02, n))
|
||||
ce = 100 * np.cumprod(1 + rng.normal(0, 0.03, n))
|
||||
j = pd.DataFrame({"timestamp": ts.view("int64") // 10**6, "datetime": ts, "cb": cb, "ce": ce})
|
||||
j["r_btc"] = o.al.simple_returns(cb)
|
||||
j["r_eth"] = o.al.simple_returns(ce)
|
||||
j["log_ratio"] = np.log(ce / cb)
|
||||
return j
|
||||
|
||||
|
||||
def test_position_held_next_bar_only():
|
||||
"""Una posizione nota a close[k] muove SOLO il ritorno della barra k+1 (eseguibile, no leak)."""
|
||||
j = _synthetic_joint()
|
||||
k = 10
|
||||
pos = np.zeros(len(j)); pos[k] = 1.0
|
||||
ev = o.eval_spread(j, pos, fee_side=0.0)
|
||||
nz = np.nonzero(np.abs(ev["net"]) > 1e-12)[0]
|
||||
assert list(nz) == [k + 1], f"posizione a k={k} deve toccare solo k+1, trovato {nz}"
|
||||
expected = j["r_eth"].values[k + 1] - j["r_btc"].values[k + 1]
|
||||
assert abs(ev["net"][k + 1] - expected) < 1e-12
|
||||
|
||||
|
||||
def test_last_bar_decision_cannot_leak():
|
||||
"""Una decisione presa SOLO all'ultima barra non può influenzare il backtest (è tenuta su una
|
||||
barra i+1 che non esiste) -> net identicamente 0. Guardia anti-look-ahead strutturale."""
|
||||
j = _synthetic_joint()
|
||||
pos = np.zeros(len(j)); pos[-1] = 9.0
|
||||
ev = o.eval_spread(j, pos, fee_side=0.0)
|
||||
assert np.allclose(ev["net"], 0.0)
|
||||
|
||||
|
||||
def test_fee_charged_on_two_legs():
|
||||
"""La fee è su 2 gambe: ogni Δpos paga fee_side su ETH E su BTC -> costo totale = fee*2*turnover."""
|
||||
j = _synthetic_joint()
|
||||
pos = np.zeros(len(j)); pos[5] = 1.0 # held: entra a 6 (Δ=1), esce a 7 (Δ=1) -> turnover=2
|
||||
f = 0.001
|
||||
ev0 = o.eval_spread(j, pos, fee_side=0.0)
|
||||
evf = o.eval_spread(j, pos, fee_side=f)
|
||||
total_fee = float((ev0["net"] - evf["net"]).sum())
|
||||
assert abs(total_fee - f * 2 * 2) < 1e-12, total_fee
|
||||
|
||||
|
||||
def test_prefix_causality_real_signal():
|
||||
"""Prefix-check su dati reali: ricostruendo il segnale su un prefisso, la coda combacia col full."""
|
||||
ck = o.causality_spread(o.f_statarb_resid(W=60), tf="1d")
|
||||
assert ck["ok"] and ck["checked"] >= 1, ck
|
||||
ck2 = o.causality_spread(o.f_ratio_mom(L=30), tf="1d")
|
||||
assert ck2["ok"], ck2
|
||||
|
||||
|
||||
def test_dollar_neutral_low_market_beta():
|
||||
"""Dollar-neutrality: un segnale temporizzato sul ratio ha beta di mercato (50/50 BTC+ETH) ~0.
|
||||
È la proprietà 'ortogonale per costruzione' richiesta dallo studio."""
|
||||
j = o.build_joint("1d")
|
||||
pos = o.f_statarb_resid(W=60)(j)
|
||||
daily = o.spread_daily(j, pos)
|
||||
mkt = o.market_daily()
|
||||
beta, corr = o.beta_to(daily, mkt)
|
||||
assert abs(beta) < 0.10, f"beta di mercato non ~0: {beta}"
|
||||
assert abs(corr) < 0.20, f"corr di mercato troppo alta: {corr}"
|
||||
@@ -0,0 +1,50 @@
|
||||
"""Test minimale per scripts/research/signal_inout_1leg.py:
|
||||
- la costruzione del segnale MACD e' CAUSALE (no look-ahead): causality_ok ok, tail-diff ~0;
|
||||
- una cella esegue end-to-end (study_weights ritorna un verdetto valido).
|
||||
Veloce: solo BTC a 1d, una cella.
|
||||
"""
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
|
||||
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt")
|
||||
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research")
|
||||
import altlib as al # noqa: E402
|
||||
import signal_inout_1leg as sig # noqa: E402
|
||||
|
||||
|
||||
def test_macd_target_is_causal():
|
||||
"""Un target MACD costruito con EMA(adjust=False) non deve guardare al futuro."""
|
||||
fn = sig.make_macd("LF")(tf="1d", fast=12, slow=26, sig=9)
|
||||
c = al.causality_ok(fn, tf="1d")
|
||||
assert c["ok"], c
|
||||
assert c["max_tail_diff"] <= 1e-6, c
|
||||
|
||||
|
||||
def test_macd_position_values_and_hold():
|
||||
"""Long-flat in {0,1}; long-short in {-1,1}; nessun NaN."""
|
||||
df = al.get("BTC", "1d")
|
||||
lf = sig.make_macd("LF")(tf="1d", fast=12, slow=26, sig=9)(df)
|
||||
ls = sig.make_macd("LS")(tf="1d", fast=12, slow=26, sig=9)(df)
|
||||
assert len(lf) == len(df) and len(ls) == len(df)
|
||||
assert set(np.unique(lf)).issubset({0.0, 1.0})
|
||||
assert set(np.unique(ls)).issubset({-1.0, 1.0})
|
||||
assert np.isfinite(lf).all() and np.isfinite(ls).all()
|
||||
|
||||
|
||||
def test_one_cell_executes_end_to_end():
|
||||
"""study_weights su una cella MACD-LF deve produrre un verdetto valido."""
|
||||
fn = sig.make_macd("LF")(tf="1d", fast=12, slow=26, sig=9)
|
||||
rep = al.study_weights("MACD-LF-test", fn, tfs=("1d",))
|
||||
assert rep["verdict"]["grade"] in ("PASS", "WEAK", "FAIL")
|
||||
assert rep["cells"] and rep["cells"][0]["per_asset"]
|
||||
|
||||
|
||||
def test_supertrend_and_rsi_targets_run():
|
||||
"""Supertrend (stateful) e RSI (mean-rev) producono posizioni causali eseguibili."""
|
||||
df = al.get("BTC", "1d")
|
||||
st = sig.make_supertrend("LF")(tf="1d", atr_win=14, mult=2.5)(df)
|
||||
rs = sig.make_rsi()(tf="1d", win=14, oversold=30, overbought=65)(df)
|
||||
assert len(st) == len(df) and len(rs) == len(df)
|
||||
assert np.isfinite(st).all() and np.isfinite(rs).all()
|
||||
assert al.causality_ok(sig.make_supertrend("LF")(tf="1d", atr_win=14, mult=2.5), tf="1d")["ok"]
|
||||
@@ -0,0 +1,82 @@
|
||||
"""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
|
||||
@@ -0,0 +1,62 @@
|
||||
"""Test del filone XS-v3: varianti STRUTTURALI di momentum cross-sectional
|
||||
(scripts/research/xsec_v3_momstruct). Verifica i GATE strutturali, non i numeri esatti (storia
|
||||
corta, ricerca): gli engine sono CAUSALI (prefix-consistency, zero look-ahead, anche quello
|
||||
volatility-managed custom) e una cella della griglia ESEGUE producendo una serie finita non degenere.
|
||||
"""
|
||||
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
|
||||
|
||||
from src.portfolio.sleeves import XS_UNIVERSE
|
||||
|
||||
import importlib.util
|
||||
_spec = importlib.util.spec_from_file_location(
|
||||
"xsec_v3_momstruct", PROJECT_ROOT / "scripts" / "research" / "xsec_v3_momstruct.py")
|
||||
v3 = importlib.util.module_from_spec(_spec)
|
||||
_spec.loader.exec_module(v3)
|
||||
xv = v3.xv
|
||||
|
||||
|
||||
def _majors():
|
||||
return xv.load_matrix(XS_UNIVERSE)
|
||||
|
||||
|
||||
def test_std_engine_variants_are_causal():
|
||||
"""RAMOM/ACCEL/FIP usano xs_engine: ricostruiti su un prefisso, la coda combacia bit-a-bit
|
||||
con la run completa (gate #2 della metodologia)."""
|
||||
PX, VOL = _majors()
|
||||
vdefs = v3.variants()
|
||||
for name, cfg in (("RAMOM", dict(L=30, H=10, k=5)),
|
||||
("ACCEL", dict(Ls=30, Ll=60, H=5, k=8)),
|
||||
("FIP", dict(L=60, H=10, k=5))):
|
||||
res = xv.causality_prefix_check(PX, VOL, vdefs[name]["builder"], cfg)
|
||||
assert res["ok"], f"{name} look-ahead: max_tail_diff={res['max_tail_diff']}"
|
||||
assert res["max_tail_diff"] == 0.0
|
||||
|
||||
|
||||
def test_volscaled_engine_is_causal():
|
||||
"""L'engine volatility-managed custom (vol-target sulla vol di MERCATO, shift 1) e' causale."""
|
||||
PX, VOL = _majors()
|
||||
v = v3.variants()["VOLSC"]
|
||||
res = v3.caus_check_mktvol(PX, VOL, v["builder"], dict(L=60, H=5, k=8), B_mkt=v["B_mkt"])
|
||||
assert res["ok"], f"VOLSC look-ahead: max_tail_diff={res['max_tail_diff']}"
|
||||
assert res["max_tail_diff"] == 0.0
|
||||
|
||||
|
||||
def test_grid_cell_executes_finite():
|
||||
"""Una cella di ogni variante esegue e produce una serie GIORNALIERA finita e non degenere."""
|
||||
PX, VOL = _majors()
|
||||
vdefs = v3.variants()
|
||||
for name, cfg in (("RAMOM", dict(L=60, H=10, k=5)),
|
||||
("ACCEL", dict(Ls=30, Ll=90, H=5, k=8)),
|
||||
("FIP", dict(L=90, H=10, k=8)),
|
||||
("VOLSC", dict(L=60, H=5, k=8))):
|
||||
daily, turn = v3.run_variant_cfg(PX, VOL, vdefs[name], cfg)
|
||||
assert len(daily) > 60
|
||||
assert np.isfinite(daily.values).all()
|
||||
assert daily.std() > 0
|
||||
assert turn > 0
|
||||
Reference in New Issue
Block a user