"""Test del gate weights_tilt_null (lezione 2026-07-01: EW-STR refutato come best-of-k). Dati SINTETICI deterministici: 3 sleeve a date d'inizio diverse, di cui uno ("C") con Sharpe gonfiato SOLO nell'hold-out — il tilt verso C deve risultare sospetto (percentile alto fra i tilt casuali, delta_insample ~<=0), mentre un tilt nullo deve essere innocuo. """ import sys from pathlib import Path import numpy as np import pandas as pd import pytest sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from src.portfolio.portfolio import HOLDOUT, StrategyPortfolio, Sleeve, combine_outer, weights_tilt_null def _mk_daily(start: str, n: int, mu: float, sigma: float, seed: int, mu_holdout: float | None = None) -> pd.Series: rng = np.random.default_rng(seed) idx = pd.date_range(start, periods=n, freq="1D", tz="UTC") r = rng.normal(mu, sigma, n) if mu_holdout is not None: m = idx >= HOLDOUT r[m] = rng.normal(mu_holdout, sigma, int(m.sum())) return pd.Series(r, index=idx) @pytest.fixture(scope="module") def cols() -> dict: n = 2600 # ~2019-07 -> 2026-08: copre pre e post hold-out return { "A": _mk_daily("2019-07-01", n, 8e-4, 0.010, seed=1), "B": _mk_daily("2021-01-01", n - 550, 6e-4, 0.012, seed=2), # C: rumore pre-holdout, forte SOLO nell'hold-out (imita lo sleeve selezionato sull'OOS) "C": _mk_daily("2019-07-01", n, 0.0, 0.011, seed=3, mu_holdout=18e-4), } def test_combine_outer_equivale_a_combined_daily(cols): sleeves = [Sleeve(nm, w, daily_fn=(lambda s=cols[nm]: s)) for nm, w in [("A", 0.5), ("B", 0.3), ("C", 0.2)]] port = StrategyPortfolio(sleeves) a = port.combined_daily() b = combine_outer(cols, {"A": 0.5, "B": 0.3, "C": 0.2}) assert np.allclose(a.values, b.values) and a.index.equals(b.index) def test_tilt_identico_e_neutro(cols): w = {"A": 0.5, "B": 0.3, "C": 0.2} rep = weights_tilt_null(cols, w, w, n=100, seed=7) assert rep["delta_hold"] == 0.0 and rep["delta_full"] == 0.0 and rep["delta_insample"] == 0.0 assert rep["n_samples"] == 100 assert 0.0 <= rep["frac_random_beat_hold"] <= 1.0 def test_vincoli_floor_caps_rispettati(cols): rep = weights_tilt_null(cols, {"A": 0.5, "B": 0.3, "C": 0.2}, {"A": 0.4, "B": 0.3, "C": 0.3}, caps={"B": 0.35}, floor=0.05, n=150, seed=11) S = rep["samples"] assert (S >= 0.05 - 1e-12).all() and (S[:, 1] <= 0.35 + 1e-12).all() assert np.allclose(S.sum(axis=1), 1.0) def test_tilt_verso_sleeve_holdout_only_e_sospetto(cols): """Tilt verso C (edge solo hold-out): delta_hold>0 ma insample<=~0 -> gate_pass False.""" w_cur = {"A": 0.5, "B": 0.3, "C": 0.2} w_pro = {"A": 0.30, "B": 0.25, "C": 0.45} rep = weights_tilt_null(cols, w_cur, w_pro, n=300, seed=13, k_seen=15) assert rep["delta_hold"] > 0 # sull'hold-out "vince" (per costruzione) assert rep["delta_insample"] <= 0.05 # ma pre-holdout non c'e' edge assert rep["bestofk_pctl"] == pytest.approx(100 * 15 / 16) assert not rep["gate_pass"] def test_determinismo(cols): w_cur = {"A": 0.5, "B": 0.3, "C": 0.2} w_pro = {"A": 0.4, "B": 0.3, "C": 0.3} r1 = weights_tilt_null(cols, w_cur, w_pro, n=80, seed=42) r2 = weights_tilt_null(cols, w_cur, w_pro, n=80, seed=42) assert r1["pctl_hold"] == r2["pctl_hold"] and np.allclose(r1["samples"], r2["samples"])