Files
PythagorasGoal/docs/diary/2026-08-07-crescita-fisco-etf-scelta.md
Adriano Dal Pastro ba5ea2f5c3 docs: diario 07/08 e memoria — e un gate di GTAA01 che ORA FALLISCE
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>
2026-08-07 18:44:36 +00:00

13 KiB
Raw Permalink Blame History

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:

  1. si parte dai ritorni giornalieri veri del book live (TP01 75% + SKH01 25%), 2.692 giorni dal 2019-03-14, già netti di commissioni;
  2. si toglie la fortuna d'ancora: r (10.89)·media(r);
  3. si rimescolano a blocchi di 20 giorni (block bootstrap): blocchi, non giorni singoli, per non distruggere il raggruppamento della volatilità;
  4. si compone: cap *= (1+r) ogni giorno, deposito ogni 30, imposta a fine anno, su 4.000 percorsi;
  5. 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 su r0727_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 con ADJUSTED_LAST di 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.