"""Test della lente WICK ACCOPPIATA (scripts/research/r0725_prop_coupled.py). Blindano le proprieta' che rendono la lente *onesta* — sono le stesse che, se si rompessero, farebbero tornare silenziosamente il difetto del 25/07 (gap indipendente dal giorno): * il recon XS01 deve riprodurre ESATTAMENTE lo sleeve ufficiale (se diverge, si sta misurando il wick di una strategia che non e' quella in produzione); * il gap dev'essere <= 0 per costruzione (il minimo intraday non puo' stare sopra la chiusura); * la dipendenza dal giorno dev'esserci (gap piu' profondo nei giorni buoni) — e' il fatto su cui poggia tutta la conclusione; * le tre lenti devono ordinarsi close >= coupled >= indip sui breach; * il bound severo su XS01 dev'essere effettivamente piu' severo di quello condiviso. """ from __future__ import annotations import sys from pathlib import Path import numpy as np import pytest ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT / "scripts" / "research")) pytest.importorskip("pandas") import r0725_prop_coupled as pc # noqa: E402 from src.portfolio.sleeves import _xsec_returns # noqa: E402 # --------------------------------------------------------------- fedelta' del recon def test_xsec_recon_riproduce_lo_sleeve_ufficiale(): """Se il recon XS01 divergesse dallo sleeve, misureremmo il wick di un'altra strategia.""" off = _xsec_returns().dropna().sort_index() if off.index.tz is None: off.index = off.index.tz_localize("UTC") rec = pc.xsec_daily_tuples() common = off.index.intersection(rec.index) assert len(common) > 500 assert np.abs(off.reindex(common).values - rec["R"].reindex(common).values).max() < 1e-12 def test_crypto_recon_riproduce_il_book_7525_del_2407(): """La decomposizione per-sleeve, ricombinata a 75/25, deve dare il book di r0724. Tolleranza larga: r0724 fa il recon in un colpo solo, qui si somma dopo aver diviso per i pesi — devono coincidere a meno dell'aritmetica in virgola mobile.""" R, G, I = pc.book_tuples(*pc.CFG_A) assert len(R) > 2000 vol = R.std() * np.sqrt(365.0) assert 0.08 < vol < 0.16, f"vol book live fuori scala: {vol:.3f}" # i percentili del gap devono ritrovare quelli dichiarati dal 24/07 (-0.17 / -1.03 / -2.70) p50, p90 = -np.percentile(-G, 50), -np.percentile(-G, 90) assert -0.004 < p50 < -0.0005, p50 assert -0.020 < p90 < -0.004, p90 # --------------------------------------------------------------- proprieta' del gap @pytest.mark.parametrize("cfg", [pc.CFG_A, pc.CFG_B, pc.CFG_XS]) def test_gap_sempre_non_positivo(cfg): """m <= R per costruzione: il minimo intraday non puo' stare sopra la chiusura.""" _, G, _ = pc.book_tuples(*cfg) assert G.max() <= 1e-15 def test_gap_piu_profondo_nei_giorni_buoni(): """IL fatto su cui poggia la conclusione. Se questo si invertisse, la lente indipendente non sarebbe piu' sbilanciata e il follow-up perderebbe senso.""" R, G, _ = pc.book_tuples(*pc.CFG_A) q = np.quantile(R, [0.1, 0.9]) gap_bad = G[R <= q[0]].mean() gap_good = G[R >= q[1]].mean() assert gap_good < gap_bad, (gap_good, gap_bad) assert gap_good < 2.0 * gap_bad, "il gap nei giorni buoni dev'essere nettamente piu' profondo" def test_una_quota_di_giorni_chiude_sul_minimo(): """Nei giorni brutti la chiusura E' il minimo: e' il motivo per cui il gap indipendente (che non lo sa) aggiunge un tuffo che non c'e'.""" _, G, _ = pc.book_tuples(*pc.CFG_A) frac = float((G > -1e-12).mean()) assert 0.10 < frac < 0.60, frac # --------------------------------------------------------------- ordinamento delle lenti def test_le_lenti_si_ordinano_sui_breach(): """close-only non vede mai il minimo -> meno breach di tutti; l'indipendente ne inventa -> piu' della accoppiata. L'ordine e' la ragion d'essere dell'esercizio.""" R, G, _ = pc.book_tuples(*pc.CFG_A, start=pc.START_WIN) rng = np.random.default_rng(0) vol = R.std() * np.sqrt(365.0) Gi = -np.exp(rng.normal(pc.pl.WICK_MU, pc.pl.WICK_SIGMA, size=(200, len(R)))) * np.clip( vol / pc.pl.WICK_REF_VOL, 0.0, 3.0) thr = 0.04 b_close = float((R < -thr).mean()) b_coup = float((R + G < -thr).mean()) b_indip = float((R[None, :] + Gi < -thr).mean()) assert b_close <= b_coup <= b_indip, (b_close, b_coup, b_indip) assert b_indip > 1.3 * max(b_coup, 1e-9), "l'indipendente deve gonfiare in modo misurabile" def test_bound_severo_xs01_e_davvero_piu_severo(): _, g_sh, _ = pc.book_tuples(*pc.CFG_B, xs_mode="shared") _, g_pl, _ = pc.book_tuples(*pc.CFG_B, xs_mode="perleg") assert g_pl.mean() < g_sh.mean() assert g_pl.min() < g_sh.min() def test_wick_mode_sconosciuto_alza_errore(): with pytest.raises(ValueError): pc.xsec_daily_tuples("nonesiste") # --------------------------------------------------------------- simulazione def test_single_account_close_domina_coupled_domina_indip(): """A parita' di tutto il resto, una lente piu' severa non puo' far sopravvivere di piu'.""" out = {ln: pc.single_account(pc.CFG_A, 1.0, ln, pc.START_WIN, n_paths=800) for ln in ("close", "coupled", "indip")} assert out["close"]["p_alive"] >= out["coupled"]["p_alive"] >= out["indip"]["p_alive"] def test_leva_piu_alta_non_aumenta_la_sopravvivenza(): lo = pc.single_account(pc.CFG_B, 0.50, "coupled", pc.START_WIN, n_paths=800) hi = pc.single_account(pc.CFG_B, 1.00, "coupled", pc.START_WIN, n_paths=800) assert lo["p_alive"] > hi["p_alive"] def test_book_tuples_non_condivide_lo_stato_fra_chiamate(): """La cache restituisce copie: un chiamante che modifica l'array non deve avvelenare il successivo (stesso guardrail messo su r0725_capcurve).""" R1, _, _ = pc.book_tuples(*pc.CFG_A) R1[:] = 999.0 R2, _, _ = pc.book_tuples(*pc.CFG_A) assert not np.allclose(R2, 999.0)