Calcolatore del libro · C06
C06 — Attrito fiscale, perdite riportate e perdite scadute
Calcolatore 6 — L'attrito fiscale
Quaderno del capitolo «L'attrito che nessun backtest include» di La matematica di chi perde.
Tre numeri sui tuoi risultati annuali: il capitale finale pagando ogni anno, quello pagando alla fine, e le perdite che scadono senza essere state usate.
Questo quaderno non è consulenza fiscale. L'aliquota e gli anni di riporto sono due caselle da riempire: mettici quelle che ti riguardano, verificate alla fonte. Il meccanismo che il quaderno mostra — l'imposta versata presto smette di comporre — non dipende da quei numeri.
Per orientarti, il quadro italiano verificato ad agosto 2026 — da ricontrollare, perché cambia:
| Su che cosa | Aliquota |
|---|---|
| Azioni, obbligazioni societarie, ETF, fondi, derivati | 26% |
| Titoli di Stato italiani e di Paesi in white list | 12,5% |
| Investimenti in un PIR mantenuti almeno 5 anni | 0% |
| Cripto-attività, plusvalenze realizzate dal 1° gennaio 2026 | 33% |
Attenzione all'asimmetria degli ETF armonizzati: i guadagni sono redditi di capitale, le perdite redditi diversi, e le due categorie non si compensano fra loro.
Le righe marcate PROVA sono quelle da cambiare: cambiale e riesegui per vedere l'effetto. Il resto — comprese le righe marcate NON TOCCARE — serve a mantenere il risultato confrontabile con quello stampato nel libro.
Espandi lo script di questo passo
import matplotlib.pyplot as plt
import numpy as np
from cvbook.dati import carica
from cvbook.lingua import t
from cvbook.regole import esegui, rottura
ALIQUOTA = 0.26 # ← l'aliquota che ti riguarda (0.33 sulle cripto dal 2026)
# PROVA / TRY: 0,26 · 0,33 · quella in vigore quando leggi
ANNI_RIPORTO = 4 # ← per quanti anni si possono riportare le perdite
# PROVA / TRY: 0 (nessun riporto, vedi esercizio 3) · 4
CAPITALE = 100_000.0 # ← il capitale di partenza
# PROVA / TRY: il tuo capitale reale1. Non è l'aliquota, è il momento
Due persone identiche, stesso rendimento, stessa aliquota. L'unica differenza: la prima realizza e paga ogni anno, la seconda lascia correre e paga alla fine.
Due pannelli affiancati su un orizzonte di 30 anni. A sinistra due curve del capitale netto partendo da 1.000 euro: pagando l'imposta solo alla fine si arriva sopra i 12.000, pagandola ogni anno si resta sotto, e l'area fra le due e' ombreggiata. A destra il capitale in piu' ottenuto differendo, per tre tassi lordi annui: con 5% resta sotto 20%, con 10% arriva a circa 55%, con 20% supera 175% a trent'anni.
Output
aliquota usata: 26%
orizzonte 5% lordo 10% lordo 20% lordo
5a 0.4% 1.6% 5.4%
10a 1.9% 6.7% 21.8%
20a 7.5% 25.6% 81.1%
30a 16.3% 54.7% 179.9%
Il divario cresce con l'orizzonte e soprattutto con il RENDIMENTO. Chi ha rendimenti alti e orizzonti lunghi — cioe' esattamente la situazione che ogni strategia attiva promette — e' chi paga di piu' questo attrito.Espandi lo script di questo passo
def confronto(rendimento: float, anni: int, aliquota: float = ALIQUOTA):
annuale = (1 + rendimento * (1 - aliquota)) ** anni
lordo = (1 + rendimento) ** anni
differita = 1 + (lordo - 1) * (1 - aliquota)
return annuale, differita
anni = np.arange(0, 31)
annuale = np.array([confronto(0.10, int(a))[0] for a in anni]) * 1000
differita = np.array([confronto(0.10, int(a))[1] for a in anni]) * 1000
with avvio.figura("schermo"):
fig, (sx, dx) = plt.subplots(1, 2, figsize=(12, 4.5))
sx.plot(anni, differita, linewidth=2, label=t("imposta alla fine", "tax at the end"))
sx.plot(anni, annuale, linewidth=2, linestyle="--", label=t("imposta ogni anno", "tax every year"))
sx.fill_between(anni, annuale, differita, alpha=0.2)
sx.set_xlabel(t("Anni", "Years"))
sx.set_ylabel(t("Capitale netto (euro, da 1.000)", "Net capital (euros, starting from 1,000)"))
sx.legend()
for r in (0.05, 0.10, 0.20):
divario = [(confronto(r, int(a))[1] / confronto(r, int(a))[0] - 1) * 100
for a in anni[1:]]
dx.plot(anni[1:], divario, linewidth=2, label=t(f"{r:.0%} lordo annuo", f"{r:.0%} gross a year"))
dx.set_xlabel(t("Orizzonte (anni)", "Horizon (years)"))
dx.set_ylabel(t("Capitale in piu' differendo (%)", "Extra capital by deferring (%)"))
dx.legend()
plt.show()
print(t(f"aliquota usata: {ALIQUOTA:.0%}\n", f"tax rate used: {ALIQUOTA:.0%}\n"))
print(f"{t('orizzonte', 'horizon'):>10s} " + "".join(
f"{r:>16.0%}" + t(" lordo", " gross") for r in (0.05, 0.10, 0.20)))
for a in (5, 10, 20, 30):
valori = "".join(f"{confronto(r, a)[1] / confronto(r, a)[0] - 1:21.1%}"
for r in (0.05, 0.10, 0.20))
print(f"{a:9d}" + t("a", "y") + f" {valori}")
print(t("\nIl divario cresce con l'orizzonte e soprattutto con il RENDIMENTO. Chi "
"ha rendimenti alti e orizzonti lunghi — cioe' esattamente la situazione "
"che ogni strategia attiva promette — e' chi paga di piu' questo attrito.",
"\nThe gap grows with the horizon and above all with the RETURN. Whoever "
"has high returns and long horizons — exactly the situation every active "
"strategy promises — pays the most for this friction."))2. Il guadagno tassabile non è il guadagno
Anno per anno, con le perdite riportabili e la loro scadenza.
Barre appaiate per ogni anno dal 2017 al 2026, in migliaia di euro a partire da 100.000: sopra lo zero il risultato lordo dell'anno, sotto l'imposta versata. Il 2020 e' l'anno piu' alto, con oltre 1.000 migliaia di euro di risultato lordo; il 2018, il 2022, il 2025 e il 2026 chiudono in perdita e non versano nulla, e il 2022 arriva a circa 750 sotto lo zero.
Output
anno lordo imponibile imposta capitale 2017 183,434 183,434 47,693 235,741 2018 -90,871 0 0 144,870 2019 161,681 70,811 18,411 288,141 2020 1,134,546 1,134,546 294,982 1,127,705 2021 455,166 455,166 118,343 1,464,528 2022 -675,768 0 0 788,760 2023 685,611 9,843 2,559 1,471,811 2024 1,015,488 1,015,488 264,027 2,223,272 2025 -209,113 0 0 2,014,159 2026 -189,684 0 0 1,824,475 capitale finale pagando ogni anno: 1,824,475 capitale finale pagando alla fine: 2,664,982 differenza: 46.1% imposte versate: 746,015 guadagno lordo complessivo: 3,466,193 guadagno realizzato da chi ha pagato: 2,470,490 aliquota EFFETTIVA sul guadagno: 30.2% perdite mai usate (scadute): 398,797 Guarda l'aliquota effettiva: e' PIU' ALTA di quella nominale, e non per un'aliquota diversa. E' l'effetto delle perdite scadute — anni in rosso che non hanno mai trovato un anno positivo entro il termine — sommato al fatto che l'imposta versata presto smette di comporre. Il danno non viene dall'aliquota: viene dal momento.
Espandi lo script di questo passo
def simula_imposta(rendimenti_annui, capitale=CAPITALE, aliquota=ALIQUOTA,
anni_riporto=ANNI_RIPORTO):
"""Imposta anno per anno, con riporto delle perdite e loro scadenza."""
crediti: list[list[float]] = []
righe, imposte_totali = [], 0.0
for anno, r in rendimenti_annui:
lordo = capitale * r
if lordo >= 0:
usato = 0.0
for c in crediti:
if anno - c[0] <= anni_riporto:
quota = min(c[1], lordo - usato)
c[1] -= quota
usato += quota
if usato >= lordo:
break
imponibile = max(lordo - usato, 0.0)
imposta = imponibile * aliquota
else:
crediti.append([anno, -lordo])
imponibile, imposta = 0.0, 0.0
crediti = [c for c in crediti if c[1] > 1e-9 and anno - c[0] < anni_riporto]
imposte_totali += imposta
capitale += lordo - imposta
righe.append({"anno": anno, "lordo": lordo, "imponibile": imponibile,
"imposta": imposta, "capitale": capitale})
return righe, imposte_totali, sum(c[1] for c in crediti)
# I risultati annuali di una regola meccanica su Bitcoin. Sostituiscili con i TUOI.
# Annual results of a mechanical rule on Bitcoin. Replace them with YOUR OWN.
# PROVA / TRY: sostituisci l'intero blocco RENDIMENTI_ANNUI qui sotto con le
# tue coppie (anno, rendimento) — vedi l'esercizio 1
df = carica("btcusdt").sort("data")
prezzi = df["chiusura"].to_numpy()
anni_serie = np.array([d.year for d in df["data"].to_list()])
curva = esegui(prezzi, rottura(prezzi, 20))["curva"]
# Il rendimento di un anno si misura dalla chiusura dell'anno PRECEDENTE, non
# dalla sua prima seduta. Partendo dalla prima seduta si perde il movimento del
# passaggio d'anno — dieci passaggi in dieci anni — e i rendimenti annuali non
# ricompongono piu' il risultato della regola: il prodotto usciva 33,66 dove la
# curva vale 35,66, e con esso tutte le cifre del capitolo (1.740.075 invece di
# 1.824.475). Con questa base il prodotto degli anni coincide con la curva.
# A year's return is measured from the PREVIOUS year's close, not from its own
# first session: otherwise the turn-of-year move is lost and the annual returns
# no longer multiply back to the rule's result.
RENDIMENTI_ANNUI = []
base_anno = float(curva[0])
for a in sorted(set(anni_serie.tolist())):
ultimo = int(np.where(anni_serie == a)[0][-1])
RENDIMENTI_ANNUI.append((int(a), float(curva[ultimo]) / base_anno - 1.0))
base_anno = float(curva[ultimo])
righe, imposte, mai_usate = simula_imposta(RENDIMENTI_ANNUI)
lordo_composto = CAPITALE
for _, r in RENDIMENTI_ANNUI:
lordo_composto *= 1 + r
differito = CAPITALE + (lordo_composto - CAPITALE) * (1 - ALIQUOTA)
with avvio.figura("schermo"):
fig, ax = plt.subplots(figsize=(10, 4.5))
x = np.arange(len(righe))
ax.bar(x - 0.2, [r["lordo"] / 1000 for r in righe], width=0.4,
label=t("risultato lordo", "gross result"))
ax.bar(x + 0.2, [-r["imposta"] / 1000 for r in righe], width=0.4,
label=t("imposta versata", "tax paid"))
ax.axhline(0, linewidth=1, color="black")
ax.set_xticks(x)
ax.set_xticklabels([str(r["anno"]) for r in righe], rotation=45)
ax.set_ylabel(t(f"Migliaia di euro (da {CAPITALE / 1000:.0f}.000 iniziali)",
f"Thousands of euros (starting from {CAPITALE / 1000:.0f},000)"))
ax.legend()
plt.show()
print(f"{t('anno', 'year'):>6s} {t('lordo', 'gross'):>14s} {t('imponibile', 'taxable'):>14s} "
f"{t('imposta', 'tax'):>12s} {t('capitale', 'capital'):>14s}")
for r in righe:
print(f"{r['anno']:6d} {r['lordo']:14,.0f} {r['imponibile']:14,.0f} "
f"{r['imposta']:12,.0f} {r['capitale']:14,.0f}")
print(t(f"\ncapitale finale pagando ogni anno: {righe[-1]['capitale']:14,.0f}",
f"\nfinal capital paying every year: {righe[-1]['capitale']:14,.0f}"))
print(t(f"capitale finale pagando alla fine: {differito:14,.0f}",
f"final capital paying at the end: {differito:14,.0f}"))
print(t(f"differenza: {differito / righe[-1]['capitale'] - 1:14.1%}",
f"difference: {differito / righe[-1]['capitale'] - 1:14.1%}"))
print(t(f"\nimposte versate: {imposte:14,.0f}",
f"\ntax paid: {imposte:14,.0f}"))
# ALIQUOTA EFFETTIVA: numeratore e denominatore devono venire dallo STESSO
# percorso. Dividere le imposte del percorso TASSATO per il guadagno del
# percorso LORDO significa mettere insieme due capitali diversi — chi paga ogni
# anno, da li' in poi, capitalizza su meno — e il rapporto usciva 21,7%, cioe'
# SOTTO l'aliquota nominale. Il guadagno lordo di chi paga lungo la strada e'
# quello che gli resta piu' quello che ha versato: su quello si misura quanta
# parte se n'e' andata, e viene il 30,2%, cioe' SOPRA la nominale.
# Numerator and denominator must come from the SAME path: the gross gain of
# whoever pays along the way is what they keep plus what they paid.
guadagno_realizzato = righe[-1]["capitale"] - CAPITALE + imposte
print(t(f"guadagno lordo complessivo: {lordo_composto - CAPITALE:14,.0f}",
f"total gross gain: {lordo_composto - CAPITALE:14,.0f}"))
print(t(f"guadagno realizzato da chi ha pagato:{guadagno_realizzato:13,.0f}",
f"gain realized by whoever paid: {guadagno_realizzato:14,.0f}"))
print(t(f"aliquota EFFETTIVA sul guadagno: {imposte / guadagno_realizzato:14.1%}",
f"EFFECTIVE rate on the gain: {imposte / guadagno_realizzato:14.1%}"))
print(t(f"perdite mai usate (scadute): {mai_usate:14,.0f}",
f"losses never used (expired): {mai_usate:14,.0f}"))
print(t("\nGuarda l'aliquota effettiva: e' PIU' ALTA di quella nominale, e non "
"per un'aliquota diversa. E' l'effetto delle perdite scadute — anni in "
"rosso che non hanno mai trovato un anno positivo entro il termine — "
"sommato al fatto che l'imposta versata presto smette di comporre. "
"Il danno non viene dall'aliquota: viene dal momento.",
"\nLook at the effective rate: it is HIGHER than the nominal one, and "
"not because of a different rate. It is the effect of expired losses — "
"red years that never met a positive year within the window — plus the "
"fact that tax paid early stops compounding. The damage doesn't come "
"from the rate: it comes from the timing."))3. L'esercizio più utile: mescola l'ordine
Stessa sequenza di risultati annuali, ordine diverso. Il risultato lordo complessivo non cambia — la moltiplicazione è commutativa — ma l'imposta sì.
Istogramma delle imposte versate su 2.000 ordini diversi degli stessi risultati annuali, con l'asse orizzontale in migliaia di euro da circa 500 a 1.800 e il conteggio fino a poco oltre 400. La massa sta fra 550 e 800; una riga verticale nera segna l'ordine realmente accaduto, a 710 migliaia, mentre il minimo e' 535 e il massimo 1.785.
Output
imposte versate nell'ordine reale: 746,015
mescolando l'ordine — minimo: 563,378
mediana: 635,853
massimo: 1,872,862
capitale finale: da 1,474,935 a 1,939,241
Stesso guadagno lordo. Il fisco non tassa il tuo guadagno: tassa il modo in cui e' arrivato.Espandi lo script di questo passo
rng = np.random.default_rng(20260816)
# NON TOCCARE / DO NOT CHANGE: il seme fissa quali dei 2.000 rimescolamenti
# vengono disegnati; l'istogramma e il minimo/mediana/massimo citati nel testo
# restano quelli. Cambiare il seme non falsa la dimostrazione (qualunque
# rimescolamento mostra lo stesso fenomeno), ma sposta i numeri esatti stampati.
# The seed fixes which of the 2,000 shuffles get drawn; the histogram and the
# min/median/max quoted in the text stay those. Changing the seed doesn't
# invalidate the demonstration (any shuffle shows the same phenomenon), but
# it shifts the exact printed numbers.
valori = [r for _, r in RENDIMENTI_ANNUI]
anni_etichette = [a for a, _ in RENDIMENTI_ANNUI]
imposte_mescolate, finali_mescolati = [], []
for _ in range(2000):
ordine = rng.permutation(len(valori))
sequenza = [(anni_etichette[k], valori[ordine[k]]) for k in range(len(valori))]
rr, ii, _ = simula_imposta(sequenza)
imposte_mescolate.append(ii)
finali_mescolati.append(rr[-1]["capitale"])
imposte_mescolate = np.array(imposte_mescolate)
finali_mescolati = np.array(finali_mescolati)
with avvio.figura("schermo"):
fig, ax = plt.subplots()
ax.hist(imposte_mescolate / 1000, bins=60)
ax.axvline(imposte / 1000, linewidth=2.5, color="black")
ax.set_xlabel(t("Imposte versate (migliaia di euro)", "Tax paid (thousands of euros)"))
ax.set_ylabel(t("Su 2.000 ordini diversi", "Out of 2,000 different orderings"))
plt.show()
print(t(f"imposte versate nell'ordine reale: {imposte:12,.0f}",
f"tax paid in the real order: {imposte:12,.0f}"))
print(t(f"mescolando l'ordine — minimo: {imposte_mescolate.min():12,.0f}",
f"shuffling the order — minimum: {imposte_mescolate.min():12,.0f}"))
print(t(f" mediana: {np.median(imposte_mescolate):12,.0f}",
f" median: {np.median(imposte_mescolate):12,.0f}"))
print(t(f" massimo: {imposte_mescolate.max():12,.0f}",
f" maximum: {imposte_mescolate.max():12,.0f}"))
print(t(f"\ncapitale finale: da {finali_mescolati.min():,.0f} a {finali_mescolati.max():,.0f}",
f"\nfinal capital: from {finali_mescolati.min():,.0f} to {finali_mescolati.max():,.0f}"))
print(t("\nStesso guadagno lordo. Il fisco non tassa il tuo guadagno: tassa il modo "
"in cui e' arrivato.",
"\nSame gross gain. The taxman doesn't tax your gain: it taxes how it "
"arrived."))Esercizi
- Sostituisci
RENDIMENTI_ANNUIcon i tuoi risultati annuali — bastano coppie(anno, rendimento). È il conto che nessun software di backtest fa. - Cambia
ALIQUOTAeANNI_RIPORTOcon quelli in vigore quando leggi. Le conclusioni cambiano di grado, non di segno. - Metti
ANNI_RIPORTO = 0— cioè nessun riporto — e guarda quanto peggiora il caso di una strategia molto oscillante. È il costo fiscale della volatilità del risultato annuale, che è una cosa diversa dalla volatilità del mercato e non compare in nessuna metrica standard.
Riproducibilità e download
Eseguito il 2026-08-27 dal quaderno del repository
Il quaderno
calc_06_fisco.ipynb21,4 KB
sha256 8eba3264b73332f4430935d516c6ccd7c930f443965a618c1135bd1b726541e6
calc_06_fisco.py17,2 KB
sha256 cc85f8229130c75727bbbaf5863622f460edf50f9360b9d10426583bc2ef4f12
I dati
btcusdt.parquet93,2 KB
sha256 ea75ad84e6e981507054df5c622c6b0ec3c8849c1f4dd007721878d4e4c8a329
Fonte: Binance Data Vision · Periodo: 2017-08-17 → 2026-06-30 · 3240 righe · estratto 2026-08-16