research(gtaa): il gate (A) non misurava cio' che dichiarava — e TLT ha 13.5 anni in meno

Il test del gate sulla banda GTAA01 aveva smesso di passare senza che il codice fosse
cambiato (data/raw/ e' gitignored, IB rivede ADJUSTED_LAST all'indietro ogni notte).
Invece di allentare la soglia, misurata la risoluzione del criterio.

`rank_in <= rank_oos` lo passano 14/30 celle (47%) PER COSTRUZIONE: la somma dei ranghi
e' la stessa nelle due finestre. E sulla proposta si decideva su 0.00116 di Sharpe contro
uno spread di griglia di 0.3124. Il 27/07 quel criterio passava, e passava per caso.

Criterio sostituito con quello decidibile: la proposta e' una BANDA (la cadenza settimanale
e' gia' produzione), quindi a cadenza fissa la banda scelta sui soli dati pre-2015 e' 25%
= la proposta, margine +0.0151 (13x il vecchio); chi avesse scelto sull'hold-out avrebbe
preso 40%. Controllo positivo incluso. Regge sull'universo coerente a 5 gambe.

TROVATO PER STRADA: TLT parte dal 2016-02-03 invece che dalla quotazione (2002-07-22) →
GTAA01 gira su CINQUE gambe prima del 2016, e quella assente e' la gamba obbligazionaria.
Non e' di oggi (cosi' dal primo giro nel cron log del 24/06, prima della validazione) e non
e' un fetch da rifare: una richiesta retro esplicita a IB ritorna 0 barre.

Nessuna certificazione l'aveva visto perche' tutte guardano DENTRO la serie: una serie
troncata e' integra, senza gap, senza spike, senza duplicati. Cablate due guardie in
fetch_ib_equities.certify — TRONCATO (storia persa vs disco; il file NON viene sovrascritto
ne' fuso, ADJUSTED_LAST e' ri-aggiustato all'indietro e il giunto creerebbe un salto) e
STORIA-CORTA (parte dopo la quotazione, con distinzione dal tetto della richiesta 30Y).
Controlli negativi obbligatori: giro normale, serie al cap, ETF giovane, simbolo fuori
tabella.

Produzione INVARIATA: REBAL_BAND_USD resta $50, GTAA01 resta non deployabile (PRIIPs).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-08-07 19:30:34 +00:00
parent ba5ea2f5c3
commit 963776e5d2
6 changed files with 754 additions and 20 deletions
+71 -12
View File
@@ -4,9 +4,19 @@ Congela i tre esiti del gate e — soprattutto — l'ANNUALIZZAZIONE. Una serie
passata a un annualizzatore a 365 esce con lo Sharpe ×1.20 e il CAGR ×1.45: e' l'errore del
25/07, ed e' l'unico difetto qui che produrrebbe numeri sbagliati ma plausibili.
`test_la_proposta_non_e_selezionata_sull_hold_out` e' il test di metodo: se un giorno la cella
proposta risultasse in rango MIGLIORE sull'hold-out che in-sample, sarebbe la firma della
selezione-sull'hold-out e la validazione andrebbe rifatta.
Il test di METODO e' `test_la_banda_proposta_e_quella_scelta_al_buio` (+ il suo controllo
positivo): se un giorno la banda che si sceglie sui soli dati pre-2015 non fosse piu' il 25%, la
validazione del 27/07 andrebbe rifatta su quella cella.
⚠️ 2026-08-07 — CRITERIO SOSTITUITO. Fino a oggi il gate (A) era congelato come
`rank_in <= rank_oos` sulla proposta, e ha iniziato a fallire (9ª in-sample, 8ª sull'hold-out)
senza che nessuno toccasse il codice: `data/raw/` e' gitignored e IB rivede `ADJUSTED_LAST`
all'indietro ogni notte. Misurato in `scripts/research/r0807_gtaa_gate_resolution.py`, quel
criterio non poteva reggere: lo passa ~meta' della griglia PER COSTRUZIONE (la somma dei ranghi
e' la stessa nelle due finestre) e sulla proposta si decideva su 0.001 di Sharpe contro uno
spread di griglia di 0.31. Il motivo del ritiro e' congelato in
`test_il_confronto_fra_ranghi_e_una_moneta_ed_e_per_questo_che_e_stato_RITIRATO` — non l'esito,
che dipende dai dati e si muove da solo.
"""
from __future__ import annotations
@@ -76,9 +86,8 @@ def test_la_produzione_usa_ancora_la_banda_ASSOLUTA():
# ===========================================================================
# (A) selezione — il gate di metodo
# ===========================================================================
def test_la_proposta_non_e_selezionata_sull_hold_out():
"""Rango in-sample <= rango hold-out: la proposta non migliora passando all'hold-out.
Il contrario sarebbe la firma della selezione-sull'hold-out."""
@pytest.fixture(scope="module")
def griglia_piena():
rows = []
for every in BG.EVERY_GRID:
for frac in BG.FRAC_GRID:
@@ -87,12 +96,62 @@ def test_la_proposta_non_e_selezionata_sull_hold_out():
sh_in=BG.met(s.loc[: BG.HOLDOUT])["sharpe"],
sh_oos=BG.met(s.loc[BG.HOLDOUT:])["sharpe"]))
G = pd.DataFrame(rows)
p = G[(G["every"] == BG.PROPOSTA[0]) & (G["frac"] == BG.PROPOSTA[1])].iloc[0]
rank_in = int((G["sh_in"] > p["sh_in"]).sum()) + 1
rank_oos = int((G["sh_oos"] > p["sh_oos"]).sum()) + 1
assert rank_in <= rank_oos, (
f"la proposta e' {rank_oos}a sull'hold-out ma {rank_in}a in-sample: "
"sta meglio dove non doveva essere guardata")
G["rank_in"] = G["sh_in"].rank(ascending=False, method="min").astype(int)
G["rank_oos"] = G["sh_oos"].rank(ascending=False, method="min").astype(int)
return G
def test_la_banda_proposta_e_quella_scelta_al_buio(griglia_piena):
"""Il gate (A), nella forma decidibile (2026-08-07).
La proposta del 27/07 e' una BANDA: la cadenza settimanale e' gia' `REBAL_EVERY=5` in
produzione e non era in discussione. Quindi la domanda sulla provenienza della scelta e'
«a cadenza di produzione, quale banda si sceglie guardando SOLO il pre-2015?».
"""
G = griglia_piena
riga = G[G["every"] == BG.PROPOSTA[0]]
blind = riga.sort_values("sh_in", ascending=False).iloc[0]
assert blind["frac"] == BG.PROPOSTA[1], (
f"al buio si sceglierebbe la banda {blind['frac']:.0%}, non {BG.PROPOSTA[1]:.0%}: "
"la validazione del 27/07 andrebbe rifatta su quella cella")
def test_chi_guardasse_l_hold_out_sceglierebbe_una_banda_DIVERSA(griglia_piena):
"""CONTROLLO POSITIVO del test precedente. Se le due selezioni coincidessero, «la proposta e'
la scelta in-sample» sarebbe vero anche per una proposta selezionata sull'hold-out, e il gate
non direbbe nulla. Un gate che non puo' fallire e' indistinguibile da uno rotto."""
G = griglia_piena
riga = G[G["every"] == BG.PROPOSTA[0]]
hold = riga.sort_values("sh_oos", ascending=False).iloc[0]
assert hold["frac"] != BG.PROPOSTA[1], (
"in-sample e hold-out scelgono la stessa banda: su questa griglia il gate (A) non ha "
"potenza e non va citato come validazione")
def test_il_margine_del_blind_non_e_un_arrotondamento(griglia_piena):
"""La ragione per cui questo criterio ha sostituito il confronto fra ranghi: decide su
~0.015 di Sharpe invece che su ~0.001. Se il margine scendesse sotto un centesimo, anche
questo criterio smetterebbe di essere una misura e andrebbe ripensato a sua volta."""
G = griglia_piena
riga = G[G["every"] == BG.PROPOSTA[0]]
blind = riga.sort_values("sh_in", ascending=False).iloc[0]
secondo = riga[riga["frac"] != blind["frac"]]["sh_in"].max()
assert blind["sh_in"] - secondo >= 0.01
def test_il_confronto_fra_ranghi_e_una_moneta_ed_e_per_questo_che_e_stato_RITIRATO(griglia_piena):
"""Congela il MOTIVO del ritiro, non l'esito (che dipende dai dati e cambia da solo).
Fino al 2026-08-07 il gate (A) asseriva `rank_in <= rank_oos` sulla proposta. Quel criterio
non puo' distinguere una proposta onesta da una selezionata sull'hold-out: la somma dei
ranghi e' la stessa nelle due finestre, quindi lo passa circa META' della griglia per
costruzione, qualunque cosa contenga. Se qualcuno lo rimettesse, questo test spiega perche' no.
"""
G = griglia_piena
passa = int((G["rank_in"] <= G["rank_oos"]).sum())
assert 0.3 * len(G) <= passa <= 0.7 * len(G), (
f"passa {passa}/{len(G)} celle: se fosse molto lontano da meta', il criterio "
"porterebbe informazione e questa motivazione andrebbe riletta")
def test_l_hold_out_usato_e_quello_documentato_di_gtaa01():