Calculator from the book · C01
C01 — How much you need to gain to recover a loss
Calculator 1 — How much it takes to recover from a loss
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 arithmetic nobody shows you" of The math of those who lose. Loss and recovery are not symmetric, and the gap grows fast. This calculator puts your own numbers inside that asymmetry: how much it takes to break even, what volatility costs compounded capital, and what happens once you add leverage.
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_massimo, equity, recupero_necessario, rendimenti1. Your number
Change PERDITA (loss) and rerun. It's the only cell you need to touch.
Output
perdita subita: 50.0% ti resta: 50.0% del capitale serve un guadagno del: 100.0% per tornare al punto di partenza
Show the script for this step
PERDITA = 0.50 # ← la perdita subita, in frazione: 0,50 vuol dire meno 50%
# PROVA / TRY: qualunque valore fra 0,01 e 0,99 · guarda cosa
# succede vicino a 0,90 (dove la curva diventa quasi verticale)
recupero = recupero_necessario(PERDITA)
print(t("perdita subita: ", "loss taken: ") + f"{PERDITA:6.1%}")
print(t("ti resta: ", "you have left: ") + f"{1 - PERDITA:6.1%}"
+ t(" del capitale", " of capital"))
print(t("serve un guadagno del: ", "you need a gain of: ") + f"{recupero:6.1%}"
+ t(" per tornare al punto di partenza", " to get back to break even"))2. The full curve
The shape is what matters: up to 30% it's a climb, past 70% it's a wall. No threshold is declared anywhere — it's arithmetic.
A curve that steepens as it climbs: the loss taken runs along the horizontal axis, from 1 to 95%, and the gain needed to get back to the starting capital along the vertical one, graduated up to 1,000%. Four marked points read it off: a 20% loss asks for a 25% gain, a 50% loss for 100%, an 80% loss for 400%, a 90% loss for 900%.
Show the script for this step
perdite = np.linspace(0.01, 0.95, 200)
recuperi = np.array([recupero_necessario(p) for p in perdite])
with avvio.figura("schermo"):
fig, ax = plt.subplots()
ax.plot(perdite * 100, recuperi * 100, linewidth=2)
for p in (0.2, 0.5, 0.8, 0.9):
r = recupero_necessario(p)
ax.plot([p * 100], [r * 100], marker="o")
ax.annotate(
f"-{p:.0%} → +{r:.0%}",
xy=(p * 100, r * 100),
xytext=(-6, 8),
textcoords="offset points",
ha="right",
)
ax.set_xlabel(t("Perdita subita (%)", "Loss taken (%)"))
ax.set_ylabel(t("Guadagno necessario per tornare in pari (%)", "Gain needed to break even (%)"))
ax.set_ylim(0, 1000)
plt.show()3. The volatility drag, on your own parameters
Two strategies with the same average return don't leave you with the same money: the more volatile one leaves less. The drag is worth about half the variance, and the calculation below verifies it instead of just asserting it.
Output
volatilita/giorno media aritm. composto capitale finale
0.500% 0.0793% 0.0781% 2.18x
1.000% 0.1433% 0.1380% 3.97x
2.000% 0.1931% 0.1725% 5.60x
3.500% -0.0079% -0.0708% 0.49x
5.000% 0.1105% -0.0175% 0.84xShow the script for this step
MEDIA_GIORNALIERA = 0.0010 # ← rendimento medio per giorno
# PROVA / TRY: 0,0005 · 0,0010 · 0,0020 — il freno
# dipende dalla volatilità, non da questo valore
GIORNI = 1000 # PROVA / TRY: 250 (un anno) · 1000 · 3000
rng = np.random.default_rng(20260816)
# NON TOCCARE / DO NOT CHANGE: il seme è fisso perché la tabella qui sotto è
# commentata nel testo con questi numeri esatti; cambiarlo dopo aver visto il
# risultato è il p-hacking che il libro smonta altrove.
# The seed is fixed because the table below is discussed in the text with
# these exact numbers; changing it after seeing the result is the p-hacking
# the book takes apart elsewhere.
print(f"{t('volatilita/giorno', 'volatility/day'):>18s} "
f"{t('media aritm.', 'arith. mean'):>14s} "
f"{t('composto', 'compounded'):>12s} "
f"{t('capitale finale', 'final capital'):>16s}")
for vol in (0.005, 0.01, 0.02, 0.035, 0.05):
r = rng.normal(MEDIA_GIORNALIERA, vol, GIORNI)
curva = equity(r)
composto = curva[-1] ** (1 / GIORNI) - 1
print(f"{vol:18.3%} {r.mean():14.4%} {composto:12.4%} {curva[-1]:16.2f}x")The arithmetic-mean column stays put; the compounded one drops. It's the same average return producing different outcomes, and the difference is only volatility.
4. Leverage, which multiplies the drag by its square
With leverage k the expected return is multiplied by k, but the drag by k². That's why there's a point past which more leverage makes everything worse — and on an already-volatile asset that point arrives very soon.
Output
leva capitale finale calo massimo azzerato il
1 13.6811x -83.2% —
2 2.1427x -99.1% —
3 0.0000x -100.0% 2020-03-12
5 0.0000x -100.0% 2020-03-12
10 0.0000x -100.0% 2017-09-14Show the script for this step
df = carica("btcusdt").sort("data")
# PROVA / TRY: per usare un'altra serie aggiungila anche alla cella di setup
# (avvio.prepara([...])) — le 11 disponibili sono in codice/dati/registro.json:
# btcusdt · ethusdt · solusdt · lunausdt · fttusdt · ftsemib · eni · enel ·
# intesa · generali · eurusd
r_reali = rendimenti(df["chiusura"].to_numpy())
def curva_con_leva(rend: np.ndarray, k: float) -> np.ndarray:
"""Capitale con leva `k`, con l'azzeramento che e' definitivo.
Un giorno in cui la posizione perde piu' del capitale non produce un numero
negativo: produce la fine. Senza questo taglio il calcolo restituirebbe cali
superiori al 100%, che non significano niente.
Capital with `k`× leverage, where wipeout is permanent: a day in which the
position loses more than the capital does not produce a negative number —
it produces the end. Without this floor the calculation would return
drawdowns above 100%, which mean nothing.
"""
passi = np.maximum(1.0 + k * rend, 0.0)
return np.concatenate([[1.0], np.cumprod(passi)])
print(f"{t('leva', 'leverage'):>5s} {t('capitale finale', 'final capital'):>16s} "
f"{t('calo massimo', 'max drawdown'):>14s} {t('azzerato il', 'wiped out on'):>14s}")
for k in (1, 2, 3, 5, 10): # PROVA / TRY: aggiungi 4 o 6, come suggerito negli esercizi
curva = curva_con_leva(r_reali, k)
azzerata = np.argmax(curva <= 0.0) if np.any(curva <= 0.0) else None
quando = str(df["data"][int(azzerata)]) if azzerata is not None else "—"
print(f"{k:5d} {curva[-1]:16.4f}x {drawdown_massimo(curva):14.1%} {quando:>14s}")Exercises
- Set
PERDITA = 0.83, the actual maximum drawdown this asset went through in the period. The resulting number is why the risk chapter talks about survival, not return. - In the leverage cell, try
k = 4andk = 6. Find the point where final capital stops growing. No forecast changed — only the size. - In the drag cell, keep
MEDIA_GIORNALIERAfixed and ask yourself which volatility wipes out the compounded return. The answer is the square root of twice the mean — try it.
Reproducibility & downloads
Run on 2026-08-27 from the repository notebook
The notebook
calc_01_recupero_perdite.ipynb12.6 KB
sha256 e659bf07777822fa838a7382876c3f9e0bbc16030554db9847c4a3fcdaf41166
calc_01_recupero_perdite.py9.6 KB
sha256 8473c432aa6dc5f4e53d156045b7f1bc0c8a4d93c67b10c57e8fce96b119cc99
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