cff5fa2bf5
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>
84 lines
3.5 KiB
Python
84 lines
3.5 KiB
Python
"""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"]))
|