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>
9.3 KiB
2026-08-07 — Un gate che falliva, un criterio che non misurava, e una gamba con 13 anni in meno
Book, pesi, cron, config: INVARIATI. REBAL_BAND_USD resta $50; GTAA01 resta non deployabile
(PRIIPs) e sotto GTAA_MIN_CAPITAL. Cambia un test, cambia un'affermazione in CLAUDE.md, e si
aggiunge una guardia di certificazione che mancava.
Script: scripts/research/r0807_gtaa_gate_resolution.py.
Test: tests/test_eq_history_guard.py (11) + tests/test_gtaa_band_gate.py (16, criterio (A)
sostituito).
Il punto di partenza
Il 07/08, facendo il giro dei test, test_la_proposta_non_e_selezionata_sull_hold_out falliva:
la proposta e' 8a sull'hold-out ma 9a in-sample: sta meglio dove non doveva essere guardata
Il gate (A) del 27/07 (r0727_gtaa_band_gate.py) era stato registrato cosi': «proposta 4/30
in-sample, 5/30 hold-out → il rango NON migliora sull'hold-out, quindi non e' selection-on-holdout».
Il codice non era stato toccato (git log src/portfolio/gtaa.py fermo al 26/07). Erano cambiati i
dati: data/raw/ e' gitignored e il cron ri-scarica ogni notte i sei ETF con ADJUSTED_LAST,
che IB rivede all'indietro a ogni dividendo.
La tentazione ovvia era allentare la soglia o mettere un xfail. Sarebbe stato mettere a tacere
esattamente il segnale. La domanda giusta e' un'altra: il criterio misura cio' che dichiara?
(0) Il difetto trovato per strada: TLT ha 13.5 anni in meno
Prima ancora di guardare il criterio, la copertura delle sei gambe:
| gamba | prima barra | quotato dal | mancano | barre | pre-2015 | 2015+ |
|---|---|---|---|---|---|---|
| SPY | 1996-08-14 | 1993-01-22 | 3.6a | 7540 | 4625 | 2915 |
| QQQ | 1999-03-10 | 1999-03-10 | 0.0a | 6896 | 3981 | 2915 |
| IWM | 2000-05-26 | 2000-05-22 | 0.0a | 6586 | 3671 | 2915 |
| TLT | 2016-02-03 | 2002-07-22 | 13.5a | 2642 | 0 | 2642 |
| GLD | 2004-11-18 | 2004-11-18 | 0.0a | 5460 | 2545 | 2915 |
| HYG | 2007-04-11 | 2007-04-04 | 0.0a | 4861 | 1946 | 2915 |
(SPY parte dal 1996 perche' la richiesta chiede durationStr="30 Y": e' il tetto, non un
difetto. La distinzione conta, senza di essa una guardia segnalerebbe ogni serie lunga.)
GTAA01 gira su CINQUE gambe prima del 2016, e la gamba assente e' proprio quella che diversifica — le obbligazioni. Conseguenza diretta sul gate (A): l'in-sample (pre-2015) e l'hold-out (2015+) non sono la stessa strategia.
Non e' successo il 07/08: nel logs/cron_daily.log TLT parte dal 2016-02-03 fin dal primo giro
registrato (24/06), quindi da prima della validazione del 27/07. E non e' un fetch da rifare:
una richiesta retro esplicita (endDateTime=2016-01-01, durationStr="5 Y") su questo conto IB
ritorna 0 barre. La storia non c'e'.
Perche' nessuna certificazione l'ha vista. Il feed equity ha guardie su integrita', gap lunghi, spike, split non aggiustati (25/07) e cross-check col gemello UCITS (26/07). Tutte guardano DENTRO la serie. Una serie troncata e' perfettamente integra: non ha buchi, non ha salti, non ha duplicati. E' la terza volta in due settimane che una guardia tarata su una classe di difetto non sorveglia le altre (soglia 50% cieca allo split 2:1; soglia sulla deviazione cieca alla contaminazione EUR/USD; ora: ogni controllo cieco a cio' che la serie ha perso).
Cablato
fetch_ib_equities.certify(sym, df, prev) ora ha due guardie, perche' i due difetti non si
vedono nello stesso modo:
TRONCATO— la serie ha perso storia rispetto al disco (parte >10 giorni dopo, o ha >5 barre in meno). Prende una troncatura il giorno in cui compare; e' cieca a una gia' presente. Il file NON viene sovrascritto — e neppure fuso:ADJUSTED_LASTe' ri-aggiustato all'indietro a ogni dividendo, quindi incollare una vintage vecchia a una nuova creerebbe un salto sul giunto, un difetto peggiore di quello che si voleva evitare.STORIA-CORTA— la serie parte >1 anno dopo la quotazione dello strumento, e non al tetto della richiesta. Prende anche una troncatura presente da sempre, che e' il caso di TLT. Riferimento:PRIMA_QUOTAZIONE, sei simboli, fonte secondaria dichiarata.
Controlli positivi obbligatori nei test: il giro normale di ogni notte non deve scattare, una serie al tetto 30Y non deve scattare, un ETF giovane (HYG) non deve scattare, un simbolo fuori tabella non viene giudicato. Una guardia che non segnala mai e' indistinguibile da una rotta.
(1) Il criterio non aveva risoluzione — misurato, non argomentato
| valore | |
|---|---|
| rango della proposta | 9/30 in-sample · 8/30 hold-out |
| distanza dal rango precedente | 0.00116 di Sharpe in-sample |
| spread dell'intera griglia | 0.3124 |
| il verdetto si decideva su | 0.37% dello spread |
E la misura che chiude la questione — il criterio applicato a ogni cella della griglia passa
14/30 (47%). Non e' un caso: la somma dei ranghi e' la stessa nelle due finestre, quindi
rank_in <= rank_oos e' vero per circa meta' delle celle per costruzione, qualunque cosa la
griglia contenga.
Un gate che una cella a caso passa il 47% delle volte non distingue una proposta onesta da una selezionata sull'hold-out. E' una moneta.
Contorno: lo spostamento tipico fra le due finestre e' di 8 ranghi (massimo 25), e lo Spearman IS/OOS e' +0.05. Su questa griglia il rango in-sample non porta informazione sul rango hold-out — il che rende il confronto fra i due ranghi doppiamente privo di senso.
(2) Il criterio decidibile
La proposta del 27/07 e' una BANDA, non una cadenza. REBAL_EVERY=5 e' gia' la 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?
| banda | Sh in-sample | Sh hold-out |
|---|---|---|
| 0% | 0.5396 | 0.8261 |
| 5% | 0.5714 | 0.8640 |
| 10% | 0.5956 | 0.8629 |
| 25% | 0.6398 ← argmax | 0.8529 |
| 40% | 0.6247 | 0.9081 ← argmax |
| 60% | 0.5874 | 0.7573 |
- banda scelta sui soli dati pre-2015: 25% = la proposta, con margine +0.0151 sulla 2ª;
- banda scelta sull'hold-out: 40% — diversa. (Controllo positivo: se coincidessero, il gate non avrebbe potenza e non andrebbe citato come validazione.)
- cella scelta al buio su tutta la griglia: cadenza 1, banda 25% — stessa banda.
La proposta e' l'esatto contrario di una selezione-sull'hold-out: e' la cella che si sceglie senza guardare l'hold-out, e chi avesse guardato l'hold-out ne avrebbe scelta un'altra. E il margine e' 0.0151 contro i 0.00116 su cui si decideva il criterio a ranghi: 13×.
⚠️ Cio' che questo criterio NON dice: che la banda scelta in-sample sia la migliore sull'hold-out. Non lo e'. Con lo Spearman IS/OOS a ~0, nessuna cella di questa griglia lo sarebbe in modo affidabile. Il gate (A) risponde alla domanda sulla provenienza della scelta, non a quella sulla previsione. Confonderle e' esattamente il modo in cui si finisce a selezionare sull'hold-out credendo di validare.
(3) Robustezza al difetto (0)
Rifatto tutto sull'universo a 5 gambe (senza TLT), coerente fra le due finestre: blind 25%, hold-out 40%, margine +0.0164. Verdetto identico. Il criterio a ranghi resta una moneta anche li' (passa 14/30).
Quindi: il difetto dei dati non e' cio' che decide questo verdetto — ma resta un difetto, e va riparato per suo conto. Che e' cio' che si e' fatto al punto (0).
Cosa cambia
tests/test_gtaa_band_gate.py: il criterio a ranghi e' ritirato e sostituito datest_la_banda_proposta_e_quella_scelta_al_buio+ il suo controllo positivo (test_chi_guardasse_l_hold_out_sceglierebbe_una_banda_DIVERSA) + un test sul margine. Il motivo del ritiro e' congelato intest_il_confronto_fra_ranghi_e_una_moneta_ed_e_per_questo_che_e_stato_RITIRATO— si congela il motivo, non l'esito, che dipende dai dati e si muove da solo.r0727_gtaa_band_gate.py: il gate (A) stampa il criterio decidibile; il confronto fra ranghi resta come descrizione, con la data del ritiro.- CLAUDE.md: l'affermazione «proposta 4/30 in-sample, 5/30 hold-out → il rango NON migliora» e' corretta.
fetch_ib_equities.py: le due guardie sulla storia + il riepilogo finale che le elenca.
Regole
- Un test che fallisce senza che il codice sia cambiato sta segnalando che i dati non sono versionati. Prima di toccarlo, si guarda cosa e' cambiato sotto.
- Un criterio va misurato sulla sua risoluzione prima che sul suo esito. Se decide su una frazione di percento dello spread, il verdetto e' rumore in entrambi i versi — anche quando passa. Il 27/07 quel criterio passava, e passava per caso.
- Un gate si valida contando quante volte lo passa un candidato a caso. Qui: 47%. Il numero si poteva calcolare il 27/07 senza dati nuovi.
- Una certificazione che guarda solo dentro la serie non vede cio' che la serie ha perso. Una serie troncata e' integra, senza gap, senza spike, senza duplicati: passa tutto.
- Distinguere «giovane», «al tetto della richiesta» e «troncato». Sono tre cose diverse e solo la terza e' un difetto; senza la distinzione la guardia segnala sempre e viene ignorata.
- Provenienza e previsione sono due domande diverse. Un gate anti-selezione dice da dove viene la scelta, non se funzionera'.