"""Test per r0726_wall_fixedpoint.py — il muro come punto fisso col book giusto alla taglia. Il test che conta e' `test_gtaa_e_troncato_all_era_book`: e' il bug catturato in sessione. `CC.gtaa_banded` ritorna la storia GTAA dal **1996**, mentre lo sleeve di produzione tronca a `GTAA_BOOK_ACTIVATION`. Senza troncamento la rinormalizzazione per-riga di `combine_outer` rende il 75% del campione (1996-2019) **GTAA01 da solo al 100%** — e il "book a 4 sleeve" diventa uno sleeve a basso CAGR: Sharpe 0.95 invece di 1.94, muro $854k invece di $274k. Un numero grottescamente sbagliato e perfettamente plausibile a prima vista. """ from __future__ import annotations import sys from pathlib import Path import numpy as np import pandas as pd import pytest ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT / "scripts" / "research")) sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) import r0726_wall_fixedpoint as WF # noqa: E402 # =========================================================================== # il bug: troncamento di GTAA01 # =========================================================================== def test_gtaa_e_troncato_all_era_book(): from src.portfolio.sleeves import GTAA_BOOK_ACTIVATION g = WF.realistic_book(300_000.0, {"GTAA01": 1.0}) assert g.index[0] >= GTAA_BOOK_ACTIVATION, ( f"GTAA01 parte da {g.index[0]}: storia pre-book inclusa, il book combinato sarebbe " "GTAA01 da solo per la maggior parte del campione") def test_il_book_deployable_non_parte_dal_1996(): """Guardia sull'effetto, non solo sulla causa: se una gamba futura reintroducesse storia antecedente, questo test la prende anche se il test sopra non la copre.""" b = WF.realistic_book(300_000.0, WF.included_at(300_000.0)) assert b.index[0] >= pd.Timestamp("2019-01-01", tz="UTC"), ( f"il book parte da {b.index[0]}: qualche gamba porta storia pre-era-crypto") def test_il_book_a_4_sleeve_ha_sharpe_superiore_al_singolo_gtaa(): """Controllo di plausibilita': un book diversificato non puo' avere lo Sharpe di GTAA01 da solo. Con il bug del troncamento faceva esattamente quello.""" from src.portfolio.portfolio import metrics C = 300_000.0 b = WF.realistic_book(C, WF.included_at(C)) g = WF.realistic_book(C, {"GTAA01": 1.0}) assert metrics(b)["sharpe"] > metrics(g)["sharpe"] + 0.3 # =========================================================================== # soglie e pesi # =========================================================================== def test_soglie_di_sblocco_degli_sleeve(): assert set(WF.included_at(1_000.0)) == {"TP01", "SKH01"} assert set(WF.included_at(50_000.0)) == {"TP01", "SKH01", "GTAA01"} assert set(WF.included_at(300_000.0)) == {"TP01", "SKH01", "GTAA01", "XS01"} @pytest.mark.parametrize("cap", [600.0, 5_000.0, 50_000.0, 300_000.0]) def test_i_pesi_si_rinormalizzano_a_uno(cap): w = WF.included_at(cap) assert sum(w.values()) == pytest.approx(1.0) assert all(v > 0 for v in w.values()) def test_vrp01_e_xsr01_sono_esclusi_per_regola(): """VRP01: 'niente short-vol da modello in deploy'. XSR01: gate pre-registrato 2026-10-23. Se qualcuno li aggiunge qui senza passare dai gate, il test lo dice.""" assert "VRP01" not in WF.W_DEPLOY assert "XSR01" not in WF.W_DEPLOY assert set(WF.W_DEPLOY) == {"TP01", "SKH01", "GTAA01", "XS01"} def test_i_pesi_deployable_derivano_dal_book_attivo(): """W_DEPLOY deve essere il book attivo meno VRP01, rinormalizzato — non pesi inventati.""" from src.portfolio.sleeves import active_sleeves live = {s.name: s.weight for s in active_sleeves()} atteso = { "TP01": live["TP01_trend_1d"], "SKH01": live["SKH01_skyhook"], "GTAA01": live["GTAA01_eq_trend"], "XS01": live["XS01_xsec_hl"]} tot = sum(atteso.values()) for k, v in atteso.items(): assert WF.W_DEPLOY[k] == pytest.approx(v / tot, rel=1e-6), ( f"{k}: peso disallineato da active_sleeves()") # =========================================================================== # fattore d'ancora # =========================================================================== def test_anchor_factor_e_un_haircut_plausibile(): """Deve essere <1 (l'ancora canonica e' un punto alto) ma non assurdo.""" f = WF.anchor_factor(WF.included_at(300_000.0), draws=40) assert 0.6 < f < 1.05, f"fattore d'ancora implausibile: {f}"