Calculator from the book · C02
C02 — Cumulative costs over a year of activity, as frequency varies
Calculator 2 — Costs, on your own numbers
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 costs that eat you alive". Three answers, with your own parameters: what costs take out of a year, what gross return you need just to break even on them, and what would have been left trading at that frequency on a real series. The number almost nobody knows is the second one.
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.metriche import equity, rendimenti1. Your three numbers
If you don't know your own round-trip cost, the chapter explains how to measure it: take ten closed trades, compare the price you saw with the price you got, add the commissions. Almost everyone finds themselves closer to 0.25% than to 0.06%.
Output
in un anno paghi: 624.00 euro cioe' il: 6.24% del capitale rendimento lordo per NON perdere nulla: 6.43%
Show the script for this step
CAPITALE = 10_000.0 # ← il capitale impegnato, in euro
# PROVA / TRY: il tuo capitale reale — cambia solo la
# scala, non le percentuali qui sotto
COSTO_GIRO = 0.0012 # ← costo tutto compreso di un giro completo (0,12%)
# PROVA / TRY: 0,0006 (scontato) · 0,0012 · 0,0025 (al dettaglio)
OPERAZIONI_ANNO = 52 # ← quanti giri completi fai in un anno
# PROVA / TRY: la tua frequenza reale, contata sull'estratto conto
speso = CAPITALE * COSTO_GIRO * OPERAZIONI_ANNO
pareggio = (1 + COSTO_GIRO) ** OPERAZIONI_ANNO - 1
print(t("in un anno paghi: ", "in a year you pay: ") + f"{speso:10,.2f}"
+ t(" euro", " euros"))
print(t("cioe' il: ", "that is: ") + f"{speso / CAPITALE:10.2%}"
+ t(" del capitale", " of capital"))
print(t("rendimento lordo per ", "gross return to "))
print(t("NON perdere nulla: ", "lose NOTHING: ") + f"{pareggio:10.2%}")2. The threshold, as frequency varies
The dashed line is, as in the book, the long-run historical average return of a broad equity index. Everything above it is territory where cost eats more than an entire market has ever returned.
Three rising curves over a logarithmic horizontal axis with six ticks — 12, 26, 52, 125, 250 and 500 round trips a year — against how much you must gain, gross, just to cover costs, graduated up to 200%. At 0.06% per round trip the curve stays below 35%, at 0.12% it reaches about 82%, at 0.25% it passes 86% already at 250 round trips and leaves the scale at 500. A dashed grey line at 10% marks the long-run historical average of a broad equity index.
Output
costo 0.06%: 12op→0.7% 26op→1.6% 52op→3.2% 125op→7.8% 250op→16.2% 500op→35.0% costo 0.12%: 12op→1.4% 26op→3.2% 52op→6.4% 125op→16.2% 250op→35.0% 500op→82.1% costo 0.25%: 12op→3.0% 26op→6.7% 52op→13.9% 125op→36.6% 250op→86.7% 500op→248.5%
Show the script for this step
FREQUENZE = np.array([12, 26, 52, 125, 250, 500])
COSTI = [0.0006, 0.0012, 0.0025]
with avvio.figura("schermo"):
fig, ax = plt.subplots(figsize=(9, 5))
for c in COSTI:
soglia = ((1 + c) ** FREQUENZE - 1) * 100
ax.plot(FREQUENZE, soglia, marker="o", label=t(f"costo {c:.2%} a giro", f"cost {c:.2%} per round trip"))
# Riferimento in grigio, con l'etichetta in legenda invece che sul grafico:
# una scritta appoggiata sulle curve e' il modo piu' rapido di rendere
# illeggibile una figura che dice una cosa semplice.
# Grey reference, labelled in the legend rather than on the chart: text
# sitting on top of the curves is the fastest way to make an otherwise
# simple figure unreadable.
ax.axhline(
10,
linestyle="--",
linewidth=1.2,
color="#8C8C8C",
zorder=0,
label=t("~10% annuo: media storica di un indice azionario",
"~10% a year: historical average of an equity index"),
)
ax.set_xscale("log")
ax.set_xticks(FREQUENZE)
ax.set_xticklabels([str(f) for f in FREQUENZE])
ax.set_xlabel(t("Operazioni complete all'anno", "Round trips per year"))
ax.set_ylabel(t("Rendimento lordo necessario per pareggiare (%)", "Gross return needed to break even (%)"))
ax.set_ylim(0, None)
ax.legend(loc="upper left")
plt.show()
for c in COSTI:
print(t(f"costo {c:.2%}: ", f"cost {c:.2%}: ") + " ".join(
f"{f}op→{((1 + c) ** f - 1):.1%}" for f in FREQUENZE))3. What would have been left, on real data
Same exposure, same asset, same period. The only thing that changes is how many times the same position is closed and reopened. No different forecast, no different decision.
Output
btcusdt · 3239 giorni
frequenza 0.06% 0.12% 0.25%
mai (compra e tieni) 13.673x 13.664x 13.645x
una volta l'anno 13.606x 13.532x 13.372x
una volta al mese 12.824x 12.020x 10.445x
una volta a settimana 10.365x 7.852x 4.299x
ogni giorno 1.959x 0.280x 0.004xShow the script for this step
SERIE = "btcusdt" # ← PROVA / TRY: "ethusdt" · "solusdt" (le tre preparate nel setup)
# per un'altra delle 11 serie in codice/dati/registro.json
# aggiungila anche a avvio.prepara([...]) qui sopra
r = rendimenti(carica(SERIE).sort("data")["chiusura"].to_numpy())
n = len(r)
def con_frequenza(rend: np.ndarray, ogni_n_giorni: int | None, costo: float) -> float:
"""Capitale finale restando sempre investiti ma rientrando ogni N giorni."""
operazioni = np.zeros(len(rend))
# L'ingresso del primo giorno si paga sempre, anche a chi non rientrera'
# mai piu': e' un'operazione. Senza questa riga il compra-e-tieni era
# l'unica riga della tabella a viaggiare gratis, e il confronto con le
# altre frequenze partiva gia' truccato di dodici centesimi per mille euro.
# The first day's entry is always paid, even by whoever never re-enters.
operazioni[0] = 1.0
if ogni_n_giorni is not None:
operazioni[::ogni_n_giorni] = 1.0
return float(equity(rend - operazioni * costo)[-1])
print(f"{SERIE} · {n}" + t(" giorni\n", " days\n"))
print(f"{t('frequenza', 'frequency'):>22s} " + " ".join(f"{c:>10.2%}" for c in COSTI))
for ogni, etichetta in [
(None, t("mai (compra e tieni)", "never (buy and hold)")),
(365, t("una volta l'anno", "once a year")),
(30, t("una volta al mese", "once a month")),
(7, t("una volta a settimana", "once a week")),
(1, t("ogni giorno", "every day")),
]:
valori = " ".join(f"{con_frequenza(r, ogni, c):10.3f}x" for c in COSTI)
print(f"{etichetta:>22s} {valori}")Exercises
- Enter your real frequency in
OPERAZIONI_ANNO: count it from last year's statement, not from memory. Then compare the break-even threshold with what you thought you could get. - Double
COSTO_GIROand rerun everything. If a strategy only survives at the optimistic cost, that edge belongs to whoever has low costs, not to you. - In the third cell, change the series. The buy-and-hold column changes a lot; the ratio between the rows barely at all: the cost of frequency doesn't depend on which asset you picked.
Reproducibility & downloads
Run on 2026-08-27 from the repository notebook
The notebook
calc_02_costi.ipynb11.7 KB
sha256 4210a85bb1b4ad856ce6f94cbde408f64623d1da3ca4cdebcfc0a29ca0a41d18
calc_02_costi.py9.0 KB
sha256 58d906d189a4efb9c5e4cefa66630b2fb09b3cc10c4f33d472e2a61504cdb2d1
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
ethusdt.parquet87.0 KB
sha256 c2bd0259da905e0fec87235d7a62295532433fb89657726dd2d19558db7c072a
Source: Binance Data Vision · Period: 2017-08-17 → 2026-06-30 · 3,240 rows · extracted 2026-08-16
solusdt.parquet57.5 KB
sha256 c7ba2368a3e419b898fb31ec6d5345b7212b74784b69079d3d43571c2ac63657
Source: Binance Data Vision · Period: 2020-08-11 → 2026-06-30 · 2,150 rows · extracted 2026-08-16