"""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"]))