36dc55748e
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>
160 lines
7.2 KiB
Python
160 lines
7.2 KiB
Python
"""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"]
|