Skip to content
SIAT PAPER 2026Open the research page
Cryptoverso

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
calc_02_costi.py
python
import matplotlib.pyplot as plt
import numpy as np

from cvbook.dati import carica
from cvbook.lingua import t
from cvbook.metriche import equity, rendimenti

1. 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
calc_02_costi.py
python
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.

How much you need to gain to cover costs alone, as trading frequency changes.Source: Binance Data Vision · Period: 2017-08-17 … 2026-06-30 · Method: Cost per round trip compounded by the number of round trips a year, three costs compared; the dashed line is 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
calc_02_costi.py
python
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.004x
Show the script for this step
calc_02_costi.py
python
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

  1. 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.
  2. Double COSTO_GIRO and rerun everything. If a strategy only survives at the optimistic cost, that edge belongs to whoever has low costs, not to you.
  3. 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

Back to the lab index