"""Test per r0726_loo_deluck.py — leave-one-out del book de-luckato sulle ancore. Copre i due punti dove questo studio puo' rompersi in silenzio: (a) la REPLICA di fase di GTAA01 — l'unico generatore ancorato scritto nuovo (gli altri quattro sono riusati dagli audit 02/07 e 03/07, gia' verificati bit-exact li'); (b) la STATISTICA APPAIATA — l'invariante che il 26/07 ha ribaltato un verdetto: la mediana delle differenze appaiate NON e' la differenza delle mediane. I test su (b) girano su strutture sintetiche: sono logica, non dati, e devono restare veloci. """ 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_loo_deluck as R # noqa: E402 # =========================================================================== # (a) replica di fase di GTAA01 # =========================================================================== def test_gtaa_fase_canonica_replica_lo_sleeve_di_produzione(): """La fase 0 deve riprodurre BIT-EXACT _gtaa_daily_returns(). Senza questo tutto lo studio misura una strategia diversa da quella del book.""" from src.portfolio.portfolio import to_daily from src.portfolio.sleeves import _gtaa_daily_returns mine = R.gtaa01_at(0) ref = to_daily(_gtaa_daily_returns()) assert len(mine) == len(ref) assert mine.index.equals(ref.index) assert float(np.max(np.abs(mine.values - ref.values))) == 0.0 def test_gtaa_le_fasi_sono_davvero_diverse(): """Controllo POSITIVO: se `ph` non entrasse nel calcolo, il test sopra passerebbe lo stesso e la banda d'ancora di GTAA01 sarebbe cinque copie della stessa serie (fortuna misurata 0 per costruzione). Almeno una fase deve differire dalla canonica.""" base = R.gtaa01_at(0) diffs = [] for ph in (1, 2, 3, 4): other = R.gtaa01_at(ph) J = pd.concat({"a": base, "b": other}, axis=1, join="inner") diffs.append(float(np.max(np.abs(J["a"].values - J["b"].values)))) assert max(diffs) > 1e-9, f"le 5 fasi di GTAA01 sono identiche: {diffs}" def test_gtaa_fase_fuori_range_non_ribilancia_mai(): """`i % 5 == 7` non e' mai vero -> la gamba resta a esposizione 0 per sempre. Serve a dimostrare che e' davvero la CADENZA a essere parametrizzata (e non un no-op).""" close = R.G._close("SPY") leg = R._gtaa_leg_phase("SPY", 1000.0, R.G.REBAL_BAND_USD, R.G.REBAL_EVERY, 7) assert len(leg) == len(close) assert float(np.max(np.abs(leg.values))) == 0.0 # =========================================================================== # (b) statistica appaiata # =========================================================================== def _fake_draws(book_vals, loo_vals, name="TP01_trend_1d"): """Costruisce la struttura che one_config() produce, con Δ = book - loo per estrazione.""" return [{"book": (b, 0.0, 0.0), name: (b - l, 0.0, 0.0)} for b, l in zip(book_vals, loo_vals)] def test_mediana_appaiata_non_e_differenza_delle_mediane(): """L'INVARIANTE del 26/07, con numeri costruiti perche' i due metodi abbiano segno OPPOSTO. book : [1.0, 2.0, 3.0] -> mediana 2.0 senza : [0.5, 2.5, 2.9] -> mediana 2.5 differenza delle mediane = -0.5 (direbbe: lo sleeve fa MALE) mediana delle differenze appaiate = mediana([+0.5, -0.5, +0.1]) = +0.1 (fa BENE) """ book = [1.0, 2.0, 3.0] loo = [0.5, 2.5, 2.9] draws = _fake_draws(book, loo) appaiata = float(np.median(R.paired_deltas(draws, "TP01_trend_1d", 0))) ingenua = float(np.median(book) - np.median(loo)) assert appaiata == pytest.approx(0.1) assert ingenua == pytest.approx(-0.5) assert np.sign(appaiata) != np.sign(ingenua), "il caso di test non dimostra piu' il ribaltamento" def test_paired_deltas_preserva_ordine_e_metrica(): draws = [{"book": (0, 0, 0), "X": (1.0, 10.0, 100.0)}, {"book": (0, 0, 0), "X": (2.0, 20.0, 200.0)}] assert list(R.paired_deltas(draws, "X", 0)) == [1.0, 2.0] assert list(R.paired_deltas(draws, "X", 1)) == [10.0, 20.0] assert list(R.paired_deltas(draws, "X", 2)) == [100.0, 200.0] # =========================================================================== # (c) semantica del leave-one-out sul book # =========================================================================== def _flat_series(vals, start="2020-01-01"): idx = pd.date_range(start, periods=len(vals), freq="1D", tz="UTC") return pd.Series(np.asarray(vals, float), index=idx) def test_one_config_e_appaiato_per_costruzione(): """res[n] deve essere il Δ calcolato alla STESSA configurazione, non a due configurazioni diverse. Si verifica ricalcolando a mano il book completo e quello senza uno sleeve.""" rng = np.random.default_rng(7) n = 400 series = {name: {0: _flat_series(rng.normal(0.0004, 0.01, n)), 1: _flat_series(rng.normal(0.0004, 0.01, n))} for name in R.SPEC} cfg = {name: (i % 2) for i, name in enumerate(R.SPEC)} res = R.one_config(series, cfg) cols = {n_: series[n_][cfg[n_]] for n_ in R.SPEC} bf, bh, bd = R.book_stats(cols) assert res["book"] == (bf, bh, bd) for name in R.SPEC: sub = {k: v for k, v in cols.items() if k != name} f, h, d = R.book_stats(sub) assert res[name][0] == pytest.approx(bf - f) assert res[name][1] == pytest.approx(bh - h) # sul maxDD il segno e' INVERTITO apposta: >0 = togliendolo il drawdown PEGGIORA assert res[name][2] == pytest.approx(d - bd) def test_sleeve_costante_a_zero_non_contribuisce_al_sharpe(): """Controllo positivo sul segno: uno sleeve che non fa nulla deve dare Δ ~0 sul Sharpe, non un contributo positivo per magia di rinormalizzazione.""" rng = np.random.default_rng(11) n = 500 names = list(R.SPEC) series = {nm: {0: _flat_series(rng.normal(0.0005, 0.008, n))} for nm in names} dead = names[-1] series[dead] = {0: _flat_series(np.zeros(n))} cfg = {nm: 0 for nm in names} res = R.one_config(series, cfg) # lo sleeve morto abbassa il Sharpe del book (diluisce): il suo Δ dev'essere <= 0 assert res[dead][0] <= 1e-9 def test_book_stats_usa_i_pesi_di_produzione(): """I pesi devono essere quelli del book attivo, e la somma dei cinque deve fare 1.0: se qualcuno cambia active_sleeves() e non questo modulo, il test lo dice.""" from src.portfolio.sleeves import active_sleeves live = {s.name: round(s.weight, 6) for s in active_sleeves()} assert live == R.WEIGHTS, f"pesi disallineati da active_sleeves(): {live} vs {R.WEIGHTS}" assert sum(R.WEIGHTS.values()) == pytest.approx(1.0) def test_spec_copre_tutti_gli_sleeve_attivi_con_ancora_canonica_valida(): from src.portfolio.sleeves import active_sleeves assert set(R.SPEC) == {s.name for s in active_sleeves()} for name, (_gen, anchors, canon, _prod) in R.SPEC.items(): assert canon in anchors, f"{name}: ancora canonica {canon} fuori dalla griglia" assert len(anchors) >= 5, f"{name}: griglia d'ancora troppo piccola per una banda"