# 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_LAST` e' 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 da `test_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 in `test_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 1. **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. 2. **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. 3. **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. 4. **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. 5. **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. 6. **Provenienza e previsione sono due domande diverse.** Un gate anti-selezione dice da dove viene la scelta, non se funzionera'.