Calculator from the book · C06
C06 — Tax friction, carried-forward losses and expired losses
Calculator 6 — Tax friction
Code language
The code, its comments and its outputs are in Italian: they are the book’s code, kept identical to what the reader runs.
Notebook for the chapter "The friction no backtest includes". Three numbers on your annual results: final capital paying every year, paying at the end, and losses that expire unused. This notebook is not tax advice. The tax rate and the carry-forward years are two fields to fill in with the ones that apply to you, verified at the source — the mechanism the notebook shows (tax paid early stops compounding) doesn't depend on those numbers. For reference, the Italian framework verified in August 2026 (recheck it, it changes): shares/bonds/ETFs/funds/derivatives 26%, Italian and white-list government bonds 12.5%, PIR investments held ≥5 years 0%, crypto-assets (gains realized from January 1, 2026) 33%. Watch the asymmetry of UCITS ETFs: gains are capital income, losses are "other income", and the two categories don't offset each other.
The lines marked TRY are the ones to change: edit them and rerun to see the effect. Everything else — including lines marked DO NOT CHANGE — exists to keep the result comparable with the one printed in the book.
Show the script for this step
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. It's not the rate, it's the timing
Two identical people, same return, same tax rate. The only difference: the first realizes and pays every year, the second lets it run and pays at the end.
Two side-by-side panels over a 30-year horizon. On the left two curves of net capital starting from 1,000 euros: paying tax only at the end ends above 12,000, paying it every year stays below, and the area between the two is shaded. On the right the extra capital obtained by deferring, for three gross annual rates: at 5% it stays under 20%, at 10% it reaches about 55%, at 20% it passes 175% over thirty years.
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.Show the script for this step
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. Taxable gain is not gain
Year by year, with carry-forward losses and their expiry.
Paired bars for each year from 2017 to 2026, in thousands of euros starting from 100,000: above zero the gross result of the year, below it the tax paid. 2020 is the tallest year, with a gross result above 1,000 thousand euros; 2018, 2022, 2025 and 2026 close in the red and pay nothing, and 2022 reaches about 750 below 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.
Show the script for this step
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. The most useful exercise: shuffle the order
Same sequence of annual results, different order. The overall gross result doesn't change — multiplication is commutative — but the tax does.
Histogram of the tax paid across 2,000 different orderings of the same annual results, with the horizontal axis in thousands of euros from about 500 to 1,800 and the count reaching a little past 400. The mass sits between 550 and 800; a black vertical line marks the ordering that actually happened, at 710 thousand, while the minimum is 535 and the maximum 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.Show the script for this step
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."))Exercises
- Replace
RENDIMENTI_ANNUIwith your own annual results — pairs of(year, return)suffice. It's the calculation no backtesting software does. - Change
ALIQUOTAandANNI_RIPORTOto whatever is in force when you read this. The conclusions change in degree, not in sign. - Set
ANNI_RIPORTO = 0— i.e. no carry-forward — and see how much worse it gets for a highly oscillating strategy. It's the tax cost of the volatility of the annual result, which is a different thing from market volatility and doesn't appear in any standard metric.
Reproducibility & downloads
Run on 2026-08-27 from the repository notebook
The notebook
calc_06_fisco.ipynb21.4 KB
sha256 8eba3264b73332f4430935d516c6ccd7c930f443965a618c1135bd1b726541e6
calc_06_fisco.py17.2 KB
sha256 cc85f8229130c75727bbbaf5863622f460edf50f9360b9d10426583bc2ef4f12
The data
btcusdt.parquet93.2 KB
sha256 ea75ad84e6e981507054df5c622c6b0ec3c8849c1f4dd007721878d4e4c8a329
Source: Binance Data Vision · Period: 2017-08-17 → 2026-06-30 · 3,240 rows · extracted 2026-08-16