Calculator from the book · C03
C03 — Time under the peak and position size compatible with your tolerance
Calculator 3 — Risk, measured in time
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 "Risk is not a number". Risk is four different questions, and volatility only answers the first. Here you compute all four, then make the calculation I recommend before opening any position: what size would have been compatible with your tolerance, in the worst historical period. It's usually much smaller than the one you had in mind.
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 drawdown, drawdown_massimo, equity, rendimenti, sharpe, volatilita
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
df = carica(SERIE).sort("data")
prezzi = df["chiusura"].to_numpy()
date = df["data"].to_list()
r = rendimenti(prezzi)
curva = equity(r)
dd = drawdown(curva)1. The four questions
Four different numbers, all called "risk". The first is the one that shows up everywhere; the fourth is the one that decides whether you quit.
Output
btcusdt · 3240 giorni · 2017-08-17 → 2026-06-30 1. Quanto oscilla? volatilita' annualizzata 67.5% 2. Quanto perdo in un colpo? giorno peggiore -39.5% 3. Quanto scendo in totale? calo massimo dal picco -83.2% 4. Per quanto resto sotto? vedi la tabella qui sotto indicatore rendimento/rischio piu' usato al mondo: 0.78
Show the script for this step
print(f"{SERIE} · {len(prezzi)}" + t(" giorni · ", " days · ") + f"{date[0]} → {date[-1]}\n")
print(t("1. Quanto oscilla? volatilita' annualizzata ",
"1. How much does it swing? annualized volatility ") + f"{volatilita(r):>8.1%}")
print(t("2. Quanto perdo in un colpo? giorno peggiore ",
"2. How much in one hit? worst single day ") + f"{r.min():>8.1%}")
print(t("3. Quanto scendo in totale? calo massimo dal picco ",
"3. How far down in total? max drawdown from peak ") + f"{drawdown_massimo(curva):>8.1%}")
print(t("4. Per quanto resto sotto? vedi la tabella qui sotto",
"4. For how long am I down? see the table below"))
print(t("\n indicatore rendimento/rischio piu' usato al mondo: ",
"\n most widely used return/risk indicator in the world: ") + f"{sharpe(r):.2f}")2. Risk measured in time
The question nobody asks, and the one that determines whether a real person actually follows the plan through to the end.
Two stacked panels over 3,240 days of btcusdt, from 17 August 2017 to 30 June 2026. Above, the filled distance from the previous peak: it drops beyond 80 below zero in 2018 and in 2022, and touches zero only a handful of times. Below, the share of time spent at least a given distance from the peak, from 100% on the left down to near zero on the right: 81% of days at least 10 away, 71% at least 20, 40% at least 50, 10% at least 70.
Output
almeno 10% sotto il massimo: 81.3% dei giorni (~7.2 anni su 8.9) almeno 20% sotto il massimo: 71.5% dei giorni (~6.3 anni su 8.9) almeno 50% sotto il massimo: 40.1% dei giorni (~3.6 anni su 8.9) almeno 70% sotto il massimo: 9.6% dei giorni (~0.9 anni su 8.9) almeno 80% sotto il massimo: 2.2% dei giorni (~0.2 anni su 8.9) giorni al proprio massimo storico: 111 su 3240 (3.4% del tempo)
Show the script for this step
with avvio.figura("schermo"):
fig, (a, b) = plt.subplots(2, 1, figsize=(10, 6), height_ratios=[2, 1.4])
a.fill_between(date, dd * 100, 0, step="mid")
a.set_ylabel(t("Distanza dal massimo precedente (%)", "Distance from previous peak (%)"))
soglie = np.arange(0, 0.91, 0.05)
quote = [float((dd <= -s).mean()) * 100 for s in soglie]
b.plot(soglie * 100, quote, marker="o")
b.set_xlabel(t("Almeno questa distanza dal massimo (%)", "At least this far from the peak (%)"))
b.set_ylabel(t("Quota del tempo (%)", "Share of time (%)"))
plt.show()
for s in (0.10, 0.20, 0.50, 0.70, 0.80):
quota = float((dd <= -s).mean())
print(t(f"almeno {s:.0%} sotto il massimo: {quota:6.1%} dei giorni "
f"(~{quota * len(dd) / 365:.1f} anni su {len(dd) / 365:.1f})",
f"at least {s:.0%} below the peak: {quota:6.1%} of the days "
f"(~{quota * len(dd) / 365:.1f} years out of {len(dd) / 365:.1f})"))
sotto_zero = int(np.sum(dd < -0.001))
print(t(f"\ngiorni al proprio massimo storico: {len(dd) - sotto_zero} su {len(dd)} "
f"({1 - sotto_zero / len(dd):.1%} del tempo)",
f"\ndays at their all-time high: {len(dd) - sotto_zero} out of {len(dd)} "
f"({1 - sotto_zero / len(dd):.1%} of the time)"))3. How long it takes to resurface
Not just how far it drops: how long it lasts. It's the statistic missing from every product sheet.
Output
episodi sotto il massimo: 50 durata mediana: 4 giorni durata media: 63 giorni il piu' lungo: 1073 giorni (2.9 anni)
Show the script for this step
picchi = np.maximum.accumulate(curva)
in_calo = curva < picchi - 1e-12
durate, corrente = [], 0
for x in in_calo:
if x:
corrente += 1
elif corrente:
durate.append(corrente)
corrente = 0
if corrente:
durate.append(corrente) # ancora in corso alla fine della serie / still ongoing at the end of the series
durate = np.array(durate)
print(t(f"episodi sotto il massimo: {len(durate)}", f"episodes below the peak: {len(durate)}"))
print(t(f"durata mediana: {np.median(durate):6.0f} giorni", f"median duration: {np.median(durate):6.0f} days"))
print(t(f"durata media: {durate.mean():6.0f} giorni", f"mean duration: {durate.mean():6.0f} days"))
print(t(f"il piu' lungo: {durate.max():6.0f} giorni ({durate.max() / 365:.1f} anni)",
f"the longest: {durate.max():6.0f} days ({durate.max() / 365:.1f} years)"))4. The calculation to do before opening a position
Enter your capital and the loss you truly don't want to exceed. The calculation answers: how much you could put in, if the worst period that already happened repeated itself identically.
Output
calo massimo gia' accaduto su btcusdt: 83.2% perdita accettabile: 3,000 su 20,000 euro (15.0% del capitale) posizione compatibile: 18.0% del capitale, cioe' 3,606 euro E ricordati che il peggio gia' visto NON e' il peggio possibile: e' il peggio di una sola realizzazione. Con un margine del 20% la posizione scende a 3,005 euro.
Show the script for this step
CAPITALE = 20_000.0 # ← il tuo capitale totale, in euro
# PROVA / TRY: il tuo capitale reale
PERDITA_ACCETTABILE = 3_000.0 # ← quanto sei disposto a vedere sparire, in euro
# PROVA / TRY: la tua soglia VERA, non quella
# che diresti a un amico (vedi esercizio 2)
peggiore = abs(drawdown_massimo(curva))
quota_massima = PERDITA_ACCETTABILE / (CAPITALE * peggiore)
print(t(f"calo massimo gia' accaduto su {SERIE}: {peggiore:.1%}",
f"max drawdown already seen on {SERIE}: {peggiore:.1%}"))
print(t(f"perdita accettabile: {PERDITA_ACCETTABILE:,.0f} su {CAPITALE:,.0f} euro "
f"({PERDITA_ACCETTABILE / CAPITALE:.1%} del capitale)\n",
f"acceptable loss: {PERDITA_ACCETTABILE:,.0f} out of {CAPITALE:,.0f} euros "
f"({PERDITA_ACCETTABILE / CAPITALE:.1%} of capital)\n"))
print(t(f"posizione compatibile: {min(quota_massima, 1.0):.1%} del capitale, "
f"cioe' {min(quota_massima, 1.0) * CAPITALE:,.0f} euro",
f"compatible position: {min(quota_massima, 1.0):.1%} of capital, "
f"i.e. {min(quota_massima, 1.0) * CAPITALE:,.0f} euros"))
print(t(f"\nE ricordati che il peggio gia' visto NON e' il peggio possibile: e' il "
f"peggio di una sola realizzazione. Con un margine del 20% la posizione "
f"scende a {min(quota_massima / 1.2, 1.0) * CAPITALE:,.0f} euro.",
f"\nAnd remember that the worst seen so far is NOT the worst possible: it's "
f"the worst of a single realization. With a 20% margin the position "
f"drops to {min(quota_massima / 1.2, 1.0) * CAPITALE:,.0f} euros."))Exercises
- Change
SERIE. The maximum drawdown changes, and with it the compatible position: the same tolerance produces very different sizes on different assets. That's how you compare assets — not by how much they've gone up. - In the fourth cell, enter your real acceptable loss — the one past which you'd actually change behaviour, not the one you'd tell a friend.
- Look at the longest episode from the third cell and ask the chapter's question: how long can I stay down without changing behaviour? If the answer is shorter than that number, the problem isn't the asset: it's the pairing between it and you.
Reproducibility & downloads
Run on 2026-08-27 from the repository notebook
The notebook
calc_03_stop_sizing.ipynb13.2 KB
sha256 3009a508ba5c98464ba4fb78154fbc4b2c4003988499b0ccc8753a605b425f49
calc_03_stop_sizing.py10.2 KB
sha256 234198563772be6199b1c2ca78be5e6704251c5985aa53c53bcf01d079a0fd92
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