"""Test del gate onesto sul tenore di VRP01 (scripts/research/r0730_vrp_tenor_gate.py). Cosa va congelato, in ordine di importanza: * **La famiglia dichiarata.** Il deflated-Sharpe dipende da quanti trial si dichiarano, e il verdetto di questo studio si RIBALTA fra 8 e 72 celle. La griglia e' stata dichiarata nel docstring PRIMA di guardare i numeri; questi test la congelano, cosi' che un domani nessuno possa farla passare restringendo il conteggio. * **La selezione e' in-sample.** Se qualcuno la cambiasse in "cella a max hold-out" il gate diventerebbe la trappola che esiste per prevenire. * **La coerenza fra i due script del 30/07**: la cella canonica qui deve essere la stessa serie del canonico misurato in `r0730_vrp_profit_take`. * **Il controllo positivo**: un gate che dice sempre FAIL non e' un gate. """ from __future__ import annotations import importlib.util 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" / "alt")) _spec = importlib.util.spec_from_file_location( "r0730_tenor", ROOT / "scripts" / "research" / "r0730_vrp_tenor_gate.py") TG = importlib.util.module_from_spec(_spec) _spec.loader.exec_module(TG) import altlib # noqa: E402 from src.portfolio.sleeves import VRP_CFG # noqa: E402 @pytest.fixture(scope="module") def grid(): rows = [] for tn in TG.TENORS: for sd in TG.SHORT_DELTAS: for ld in TG.LONG_DELTAS: d = TG.cell_daily(tn, sd, ld) ins = d[d.index < TG.HOLDOUT] rows.append(dict(tenor=tn, sd=sd, ld=ld, daily=d, is_sh=TG._sh(ins), full_sh=TG._sh(d), hold_sh=TG._sh(d[d.index >= TG.HOLDOUT]))) return rows # =========================================================================== # la famiglia dichiarata — il numero da cui dipende il verdetto # =========================================================================== def test_la_griglia_dichiarata_e_di_72_celle(): assert len(TG.TENORS) * len(TG.SHORT_DELTAS) * len(TG.LONG_DELTAS) == 72 def test_la_griglia_copre_la_regione_mai_esplorata_dal_0307(): """Il buco era 'oltre i 10 giorni': la griglia 03/07 era (3, 5, 7, 10).""" assert max(TG.TENORS) > 10 assert sum(1 for t in TG.TENORS if t > 10) >= 4 def test_il_canonico_e_dentro_la_griglia(): """Senza il canonico dentro non c'e' un termine di paragone.""" assert TG.CANON == (VRP_CFG["tenor_d"], VRP_CFG["short_delta"], VRP_CFG["long_delta"]) assert TG.CANON[0] in TG.TENORS assert TG.CANON[1] in TG.SHORT_DELTAS assert TG.CANON[2] in TG.LONG_DELTAS # =========================================================================== # coerenza fra i due script del 30/07 # =========================================================================== def test_la_cella_canonica_e_lo_stesso_canonico_dell_altro_studio(): """`r0730_vrp_profit_take.combo(pt=None)` e `cell_daily(*CANON)` devono essere la STESSA serie: se divergono, i due diari raccontano due cose diverse chiamandole uguali.""" a = TG.cell_daily(*TG.CANON) b = TG.PT.combo(pt=None, f=1.0, real_fee=True)[0] j = pd.concat({"a": a, "b": b}, axis=1, join="inner") assert len(j) == len(a) == len(b) assert float((j["a"] - j["b"]).abs().max()) == pytest.approx(0.0, abs=1e-12) # =========================================================================== # la selezione non sbircia l'hold-out # =========================================================================== def test_la_cella_e_scelta_col_solo_sharpe_in_sample(grid): chosen = max(grid, key=lambda r: r["is_sh"]) assert chosen["is_sh"] == max(r["is_sh"] for r in grid) def test_scegliere_sull_hold_out_darebbe_una_cella_DIVERSA(grid): """E' il motivo per cui il gate esiste: la cella a max hold-out non e' quella a max in-sample, quindi 'scegliere guardando l'hold-out' sarebbe una scelta, non una misura.""" a = max(grid, key=lambda r: r["is_sh"]) b = max(grid, key=lambda r: r["hold_sh"]) assert (a["tenor"], a["sd"], a["ld"]) != (b["tenor"], b["sd"], b["ld"]) # =========================================================================== # il risultato: la regione nuova perde # =========================================================================== def test_la_regione_oltre_i_10_giorni_non_produce_il_vincitore(grid): """Il modo in cui il buco si chiude: si e' guardato dove non si era mai guardato, e li' non c'e' il vincitore.""" chosen = max(grid, key=lambda r: r["is_sh"]) assert chosen["tenor"] <= 10 best_new = max((r for r in grid if r["tenor"] > 10), key=lambda r: r["is_sh"]) assert best_new["is_sh"] < chosen["is_sh"] # =========================================================================== # deflated Sharpe: quello di altlib, e il verdetto che ne dipende # =========================================================================== def test_usa_il_deflated_sharpe_di_altlib_non_una_copia(): assert TG.altlib.deflated_sharpe is altlib.deflated_sharpe def test_il_verdetto_dipende_dal_numero_di_trial_dichiarati(grid): """Il fatto piu' importante da non dimenticare: a 8 trial passerebbe, a 72 no. La griglia e' stata dichiarata prima di guardare, e il conteggio si fa al rialzo.""" chosen = max(grid, key=lambda r: r["is_sh"]) all72 = [r["full_sh"] for r in grid] only8 = [r["full_sh"] for r in grid if (r["sd"], r["ld"]) == TG.CANON[1:]] d72, _ = altlib.deflated_sharpe(chosen["full_sh"], all72, chosen["daily"]) d8, _ = altlib.deflated_sharpe(chosen["full_sh"], only8, chosen["daily"]) assert len(only8) == len(TG.TENORS) == 8 assert d8 > 0.95 > d72 # il verdetto si ribalta assert d72 > 0.90 # ...e fallisce per poco: va citato cosi' def test_il_gate_fallisce_sulla_griglia_dichiarata(grid): chosen = max(grid, key=lambda r: r["is_sh"]) dsr, _ = altlib.deflated_sharpe(chosen["full_sh"], [r["full_sh"] for r in grid], chosen["daily"]) assert dsr < 0.95 def test_controllo_positivo_il_deflated_sharpe_sa_promuovere(grid): """Obbligatorio: un gate che risponde sempre FAIL e' indistinguibile da uno rotto. Una serie COSTRUITA molto forte, contro gli stessi 72 trial, deve passare.""" chosen = max(grid, key=lambda r: r["is_sh"]) strong = chosen["daily"] * 1.0 strong = strong + strong.std() * 0.5 # drift grande, stessa forma dsr, _ = altlib.deflated_sharpe(TG._sh(strong), [r["full_sh"] for r in grid], strong) assert dsr >= 0.95 # =========================================================================== # il marginale resta ADDS: il verdetto NON e' "VRP e' rotto" # =========================================================================== def test_il_canonico_resta_ADDS_vs_tp01(): """Il gate boccia un CAMBIO di struttura, non lo sleeve: se questo si rompe, la conclusione del diario va riscritta.""" m = altlib.marginal_vs_tp01(TG.cell_daily(*TG.CANON)) assert m["marginal_verdict"] == "ADDS" assert m["has_insample_edge"] and not m["is_hedge"]