research(vrp): buco del tenore chiuso col gate onesto — earns_slot_honest = False

Book, pesi, cron, config INVARIATI.

La griglia strutture del 03/07 si fermava a 10 giorni. Famiglia dichiarata nel
docstring PRIMA di guardare i numeri: 8 tenori (5-35g) x 3 delta corti x 3
lunghi = 72 celle, perche' riaprire il tenore riapre la struttura e i trial si
contano al rialzo.

study_family_honest e' cablato sui candidati direzionali (factory -> target_fn
via candidate_daily) e VRP01 non lo e': usati i suoi tre componenti reali —
selezione in-sample-only, altlib.deflated_sharpe, altlib.marginal_vs_tp01 —
importati e non riscritti (c'e' un test d'identita').

ESITO. Cella scelta al buio 10g -0.28/-0.05 (Sh IS 1.75 / FULL 1.55 / HOLD 1.01)
contro il canonico 7g (1.50/1.32/0.82; rango 17/72 in-sample). Batte il canonico
ma DSR 0.948 < 0.95 FAIL. marginal_vs_tp01 = ADDS per entrambe: il verdetto non
e' 'VRP01 e' rotto', e' che il vantaggio non sopravvive al conto dei trial.

IL BUCO SI CHIUDE SUL CONTENUTO, non sul gate: la regione mai esplorata PERDE.
Miglior cella >10g = 18g, rango 8/72, e solo 3/10 della top-10 sta oltre i 10
giorni -> lo studio conferma il 03/07 invece di ribaltarlo.

IL VINCITORE STA DOVE IL MODELLO SBAGLIA DI PIU': compra l'ala piu' lontana
(delta lungo -0.05), come 5/10 della top-10, cioe' la gamba che il 30/07 ha
misurato sottoprezzata ~2.3x. Sospetto motivato, non dimostrazione: il mediano
non separa -0.10 da -0.05, si separa la coda alta. Ma f non e' misurato fuori
dalla struttura canonica, e la sensibilita' a f uniforme e' la lente sbagliata
per una struttura il cui errore e' concentrato in una gamba sola.

ROBUSTEZZA DEL VERDETTO, PUBBLICATA: DSR 0.983 PASS a N=8, 0.948 FAIL a N=72,
0.909 a N=360. Il verdetto si ribalta col conteggio. La griglia era dichiarata
in anticipo e il conto fatto al rialzo, ma fallisce per 0.002: si cita come
tale, non come refutazione netta. Congelato in un test.

Seguito giusto = una MISURA, non un altro backtest: quanto vale f sul
10g/-0.05 sulle quote vere, ora che la catena la raccogliamo noi ogni ora.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-07-30 22:06:31 +00:00
parent 04cb572535
commit 36dc55748e
4 changed files with 598 additions and 0 deletions
+159
View File
@@ -0,0 +1,159 @@
"""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"]