Diario 2026-08-07-crescita-fisco-etf-scelta.md + i bullet corrispondenti in CLAUDE.md (fisco d'accumulo, confronto book/ETF, scelta 50/50, simulatore web). Registra anche un buco trovato ricostruendo il piano: i QUATTRO risultati del 27/07 sera (r0727_3k_vs_5k, r0727_orizzonte10, r0727_tasse) non erano ne' in CLAUDE.md ne' in un diario — vivevano solo nei messaggi di commit, e uno di essi cambia il numero di testa del piano. GATE GTAA01 CHE FALLISCE (test_gtaa_band_gate::test_la_proposta_non_e_selezionata _sull_hold_out): la proposta e' 9a/30 in-sample e 8a/30 sull'hold-out contro il 4/30 e 5/30 registrato il 27/07 — cioe' migliora dove non doveva essere guardata. Caratterizzato prima di riportarlo: i due ranghi distano 0.00116 di Sharpe su un'ampiezza di griglia di 0.3124 (0.4%), quindi il criterio non ha mai avuto margine; il calcolo e' deterministico (2 corse, max|diff| = 0.0) e il codice e' invariato dal 27/07 -> e' cambiato il DATO, perche' data/raw/ e' gitignored e i parquet equity sono riscritti ogni giorno dal cron con ADJUSTED_LAST di IB, che e' retroattivo. REGOLA NUOVA: un gate validato su dati sovrascritti ogni giorno non e' ri-verificabile. Il lato cripto non ha il problema (rebuild_history.py ricostruisce da sorgente deterministica), il lato equity si'. Il test NON e' stato toccato: allentare una soglia perche' ha smesso di passare e' proprio cio' che questo progetto vieta, e un xfail silenzierebbe il segnale. Niente di operativo dipende da questo (GTAA01 non e' deployabile per il blocco PRIIPs e non e' nel book live), ma l'affermazione "il rango NON migliora sull'hold-out" oggi e' falsa e la decisione su cosa farne e' dell'operatore. Book, pesi, cron, config/live.json e i gate pre-registrati: INVARIATI. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
13 KiB
2026-08-07 — La crescita del capitale, il fisco che nessuno aveva contato, e se invece fosse un ETF
Richieste dell'operatore, in sequenza: «stato progetto» → «riepiloga piano» → «grafico della crescita per anno con versamento e con guadagno» → «con 800 al mese» → «sistema dinamico dove imposti valore iniziale, mensile e durata, e calcola la best curva fissando il tempo» → «il guadagno come viene calcolato? e se mettessi in MSCI World o ETF SP500?» → «quale è la miglior strategia?».
Esito: book, pesi, cron, config INVARIATI. Nessuna decisione operativa cambia. Cambiano tre numeri pubblicati e cade un'affermazione su GTAA01.
0. Il buco trovato per primo: quattro risultati mai registrati
Ricostruendo il piano per l'operatore ho scoperto che quattro commit del 27/07 sera non sono né
in CLAUDE.md né in un diario: vivono solo nei messaggi di commit e negli script
(r0727_3k_vs_5k.py, r0727_orizzonte10.py, r0727_tasse.py). Uno di questi cambia il numero di
testa del piano — vedi §2. È il motivo per cui questo diario esiste prima ancora del lavoro nuovo.
1. Come si calcola il «guadagno» — la domanda che andava fatta
Nessuna traiettoria del progetto ipotizza un rendimento. Il meccanismo, ora scritto:
- si parte dai ritorni giornalieri veri del book live (TP01 75% + SKH01 25%), 2.692 giorni dal 2019-03-14, già netti di commissioni;
- si toglie la fortuna d'ancora:
r − (1−0.89)·media(r); - si rimescolano a blocchi di 20 giorni (block bootstrap): blocchi, non giorni singoli, per non distruggere il raggruppamento della volatilità;
- si compone:
cap *= (1+r)ogni giorno, deposito ogni 30, imposta a fine anno, su 4.000 percorsi; guadagno = mediana(capitale) − versato.
Drift implicito: 17,1% annuo, vol 10,9%. Non è un'assunzione sul rendimento — è l'assunzione che il futuro somigli a quei 7 anni. Che è esattamente ciò che §3 mette alla prova.
Script: scripts/research/r0807_growth_yearly.py (--lump, --dep, --anni, --json).
2. ⚠️ Il fisco durante l'ACCUMULO — la voce mai contata
TAX_RATE compariva in un solo punto di tutto il progetto: la lordizzazione del bersaglio in
fase di prelievo. L'accumulo componeva al lordo per dieci o vent'anni. Misurato il 27/07
(r0727_tasse.py) e riprodotto oggi su lump €5.000 + €500/mese:
| 5 anni | 10 anni | 15 anni | |
|---|---|---|---|
| costo del fisco d'accumulo | −15,7% | −29,8% | −42,6% |
(27/07, lump €10k: −16,8% / −31% / −44% → replica coerente su lump diverso.)
L'errore è composto, non una tantum. Conseguenza da tenere presente: tutte le tabelle a 15-20 anni pubblicate in CLAUDE.md sono al lordo del fisco d'accumulo e vanno lette con questo sconto sopra. Assunzioni dichiarate (33% plusvalenze, minusvalenze in carry 4 anni, 0,2% annuo sul valore), non un parere fiscale.
Piano a €800/mese, 15 anni: capitale mediano $450.107 netto contro $776.366 lordo.
📌 Risultato contro-intuitivo: versare di più ritarda il sorpasso (l'anno in cui il guadagno cumulato supera tutto il versato): 7º anno a €500/mese, 8º a €800, perché si alza l'asticella che il guadagno deve superare. I €300 in più non comprano il sorpasso, comprano il traguardo: la mediana tocca il bersaglio al 12º anno invece che al 15º, e P(bersaglio) a 15 anni passa da 73,2% a 99,0%.
⚠️ €800/mese non bastano per il vincolo dei 10 anni: P(entro 10 anni) col fisco = 25,5%. Servono ~€880-1.050/mese (27/07), e a quel punto si versano oltre $119.000 per arrivare a $272.061.
3. E se invece fosse un ETF S&P 500 o MSCI World
Script scripts/research/r0807_asset_compare.py. Tre cose andavano rese comparabili:
- la griglia — le azioni hanno ~252 barre/anno, il book 365: portate su calendario con 0,0 a borsa chiusa (convenzione GTAA01). Senza, il Sharpe esce ×1,20 (lezione 25/07);
- il fisco — il book realizza ogni anno al 33%; un UCITS ad accumulazione paga il 26% alla vendita. Il differimento è un vantaggio strutturale dell'ETF, ora nel modello (le curve ETF sono valori di liquidazione, già al netto dell'imposta latente);
- il bersaglio — $272.061 vale per la rendita perpetua del book e per il 33%. Ricalcolato per ciascuno.
| a 15 anni, €5.000 + €500/mese | book | S&P 500 | MSCI World (proxy) |
|---|---|---|---|
| rendita perpetua | 11,67% | 4,16% | 3,09% |
| capitale-rendita necessario | $254.524 | $646.172 | $869.729 |
| lente A — storia piena di ciascuno | $293.823 → 115% | $204.663 → 32% | $188.417 → 22% |
| lente B — stessa finestra 2019-26 | $295.540 → 118% | $343.805 → 110% | $296.707 → 82% |
Il risultato che conta non è quale vince, è che le due lenti danno risposte opposte. Sulla storia piena il book stravince; sulla stessa finestra l'S&P 500 accumula più del book. Il divario della lente A viene interamente dal fatto che i 30 anni dell'indice contengono il 2000 e il 2008, crolli che la strategia non ha mai vissuto.
📌 E in una riga della tabella delle serie c'è il fatto più importante: sulla stessa finestra il rendimento è quasi identico — 17,4% contro 16,8%. Tutta la differenza è nel rischio: vol 11,0% contro 19,6%, maxDD 10,5% contro 33,7%. Il book non guadagna di più, perde di meno — che è precisamente ciò che il progetto scrive di TP01 dal 19/06, qui misurato contro un'alternativa vera. E la rendita perpetua vive sul drawdown: per questo il book ha bisogno di 2,5× meno capitale.
⚠️ MSCI World è un proxy (70% SPY + 30% EFA): URTH/ACWI/VT non sono nell'abbonamento dati del conto IB. Fra 50% e 80% di quota USA il drift si muove di 0,7 punti e il Sharpe di 0,05.
4. Quale strategia: book, ETF o un mix
Script scripts/research/r0807_best_strategy.py.
⚠️ Difetto della prima stesura, corretto: un mix esiste solo dove esistono ENTRAMBE le serie. Confrontavo «storia piena» contro «stessa finestra» come due lenti, ma calcolavo i bersagli del mix sull'intersezione in entrambe — quindi la lente «storia piena» dichiarava trent'anni e ne usava sette (per l'ETF puro dava rendita 8,63% invece del 4,16% vero). Il confronto fra finestre non è disponibile per un portafoglio misto. Rifatto su una finestra sola, con gli scenari espressi come spostamento del drift, applicabile simmetricamente ai due lati.
| quota sul book | base | equity a 30 anni | book dimezzato | entrambi cauti | peggiore | rimpianto max |
|---|---|---|---|---|---|---|
| solo ETF | 68% | 23% | 68% | 23% | 23% | 53% |
| 25% book | 80% | 41% | 60% | 27% | 27% | 34% |
| 50% book | 86% | 58% | 48% | 27% | 27% | 20% |
| 75% book | 84% | 69% | 32% | 23% | 23% | 35% |
| solo book | 75% | 75% | 16% | 16% | 16% | 52% |
Risposta: 50/50, non perché vinca (vince solo nello scenario base) ma perché ha il rimpianto massimo più basso. ⚠️ Il criterio del solo caso peggiore non distingue 25% da 50% (27,09% contro 26,99% = pareggio dentro il rumore): a separarli è il rimpianto.
📌 L'asimmetria fra i due stress È il risultato. Quello sull'equity è misurato — trent'anni esistono e dicono 11,3% invece di 16,5%. Quello sul book è giudiziale — metà del drift, scelto a mano, perché 7,4 anni sono tutta la storia che ha e non c'è nulla con cui stressarlo. Si può discutere la taglia del taglio, non lo si può togliere. La sua conseguenza è brutale: col drift dimezzato il capitale-rendita del book puro passa da $254k a $771.646 (rendita 11,67% → 3,85%).
Perché il mix non è un compromesso: correlazione book↔S&P +0,082 nei giorni di borsa aperta, +0,046 nei ribassi; nel 5% di giornate peggiori dell'indice (media −2,94%) il book fa −0,11%. Il capitale-rendita del 50/50 è $235.769, più basso sia del solo ETF ($311.567) sia del solo book ($254.524): due motori scorrelati abbassano l'asticella.
⚠️ Non c'è il rischio di venue (spingerebbe ancora verso il mix), e la decisione del 26/07 tiene tutto su Deribit fino a $20k: questa analisi è materiale per quella soglia, non un'indicazione di agire adesso.
5. Il simulatore nel browser, e la sua verifica
scripts/web/ — motore di accumulo in JavaScript (engine.js), serie esportata da
export_series.py, pagine assemblate da build.py.
Il motore JS deve dare la stessa risposta del Python, e lo si prova: test_engine.js confronta
mediane e probabilità su due configurazioni già calcolate, verifica che il versato (deterministico)
coincida al centesimo, e include un controllo positivo (un motore col fisco spento dev'essere
rilevato fuori tolleranza — un test che non sa fallire non è un test). Il risolutore («quanto serve
al mese per centrare il bersaglio in N anni») è validato contro dep_necessario di r0727_tasse.py:
scarti −0,4% / −0,6% / −1,1% su tre configurazioni.
Misura che ha cambiato una scelta: la banda del versamento suggerito resta ±1% da 1.200 a 3.000 percorsi → non domina il Monte Carlo ma la granularità della bisezione (~€5). Percorsi tenuti bassi e incertezza dichiarata, invece di pagare tempo per una precisione che non arriva.
6. Quattro errori miei, tutti trovati da un controllo e non a occhio
Sono la parte riutilizzabile del diario.
(a) Dati inventati in un grafico. Le righe degli anni 1-2 di una serie erano scritte a memoria
perché l'output del terminale era troncato da tail. Le ha trovate il confronto riga-per-riga fra
il JSON e i dati incollati nella pagina. Regola: i dati di una pagina si INIETTANO da un file, mai
si trascrivono — ora è build.py a farlo, e il passaggio manuale non esiste più.
(b) «Distorsione sistematica» che non esisteva. Il motore JS sembrava +0,75% sopra il Python su 8 semi, tutti positivi. Ma erano 8 estrazioni contro UN solo punto Python (seme 807), che stava in basso nella propria distribuzione: 8 segni concordi rispetto a un punto rumoroso non sono 8 osservazioni indipendenti. Misurato bene (8 semi per parte): −0,01%, t = −0,04; e i due campionatori cadono entrambi entro 1,7 SE dall'atteso analitico della media di blocco. Regola: un confronto punto-contro-distribuzione non produce evidenza di distorsione.
(c) Un confronto impossibile per costruzione — §4, il mix sull'intersezione.
(d) Una pagina pubblicata rotta. Python scrive le chiavi dei pesi come "0.0"/"1.0", la pagina
le ricostruiva con String(0) → "0": ogni lookup undefined, script morto alla prima riga utile.
I pesi intermedi funzionavano per caso. Il controllo che usavo (node --check) valida solo la
SINTASSI: ora c'è smoke.js, che esegue le pagine con un DOM finto e segnala sia le eccezioni sia
gli elementi riempiti con "undefined". Regola: le chiavi di un dizionario Python si LEGGONO dal
JSON, non si ricostruiscono.
7. ⚠️ Effetto collaterale del check: un gate di GTAA01 ora FALLISCE
tests/test_gtaa_band_gate.py::test_la_proposta_non_e_selezionata_sull_hold_out — la proposta
(banda 25%, cadenza 5g) risulta 9ª/30 in-sample e 8ª/30 sull'hold-out: migliora passando
all'hold-out, cioè la firma che il test intercetta. CLAUDE.md registra 4/30 e 5/30 (27/07).
Caratterizzato prima di riportarlo:
- i due ranghi sono separati da 0,00116 di Sharpe su un'ampiezza di griglia di 0,3124 = lo 0,4%: il criterio non ha mai avuto margine, era una differenza di un rango;
- il calcolo è deterministico (due corse,
max|diff| = 0.0) e il codice del gate è invariato dal 27/07 (nessun commit sur0727_gtaa_band_gate.py,gtaa.py,eqlib.py,eq_splits.py); - quindi è cambiato il dato:
data/raw/è gitignored e i parquet equity vengono riscritti ogni giorno dal cron conADJUSTED_LASTdi IB, che è retroattivo.
📌 La lezione vale più del test: un gate validato su dati sovrascritti ogni giorno non è
ri-verificabile. Il lato cripto non ha questo problema (rebuild_history.py ricostruisce da una
sorgente deterministica); il lato equity sì.
Il test NON è stato toccato — allentare una soglia perché ha smesso di passare è ciò che questo
progetto vieta, e un xfail silenzierebbe esattamente il segnale. Nulla di operativo dipende da
questo (GTAA01 non è deployabile per il blocco PRIIPs e non è nel book live), ma l'affermazione
«il rango NON migliora sull'hold-out» oggi è falsa: decisione dell'operatore se correggere
l'affermazione, ripensare il gate, o congelare uno snapshot dei parquet equity per renderlo
riproducibile.
Cosa NON cambia
Book, pesi, cron, config/live.json, i gate pre-registrati (STATARB 27/09, XSR01 23/10,
DVOLSPREAD 24/10), la decisione di venue (100% Deribit fino a $20k), il muro pubblicato di
$272.061. Gli script di questo diario non toccano la produzione.