Lab del libro · L18
L18 — Mille traiettorie invece di una, e i tre numeri che decidono
Lab 18 — Mille futuri invece di uno
Quaderno del capitolo «Mille futuri invece di uno» di La matematica di chi perde.
Ogni curva di capitale che hai visto in vita tua è una realizzazione. Qui generi le altre storie possibili, fatte della stessa identica materia prima, e guardi dove cade quella che è capitata davvero.
L'output più utile è una frase sola: «nel 5% dei casi peggiori avresti chiuso a X e attraversato un calo del Y%». Quella frase, guardata prima di aprire una posizione, cambia il dimensionamento più di qualunque ragionamento.
Le righe marcate PROVA sono quelle da cambiare: cambiale e riesegui per vedere l'effetto. Il resto — comprese le righe marcate NON TOCCARE — serve a mantenere il risultato confrontabile con quello stampato nel libro.
Espandi lo script di questo passo
import matplotlib.pyplot as plt
import numpy as np
from cvbook import seed_for
from cvbook.dati import carica
from cvbook.metriche import drawdown_massimo, rendimenti
from cvbook.simulazioni import bootstrap_traiettorie, distribuzione_esiti
SERIE = "btcusdt" # ← PROVA / TRY: "ethusdt" · "solusdt" (vedi esercizio 3)
PERCORSI = 5000 # ← mille bastano per la mediana, per le CODE servono di piu'
# PROVA / TRY: 1000 · 5000 · 20000 (vedi sezione 4 qui sotto)
BLOCCHI = 20 # ← lunghezza dei blocchi ricampionati
# PROVA / TRY: 5 · 20 · 60 (vedi esercizio 2)
r = rendimenti(carica(SERIE).sort("data")["chiusura"].to_numpy())
reale = np.cumprod(1 + r)1. Le altre storie possibili
I rendimenti non vengono modificati né modellati: sono esattamente quelli, con tutte le loro code grasse. Vengono rimescolati a blocchi, non giorno per giorno — rimescolare i singoli giorni distruggerebbe il raggruppamento della volatilità che il Lab 9 ha misurato, e produrrebbe percorsi troppo docili.
Due pannelli affiancati su 3.239 giorni. A sinistra 120 delle 5.000 traiettorie ricampionate, chiare e sovrapposte, su scala logaritmica da 10 alla meno 2 a 10 alla 3, con in nero la storia realmente accaduta che chiude a 13,7 volte il capitale iniziale. A destra l'istogramma del calo massimo di ogni percorso, con l'asse da meno 100 a meno 50: la massa sta fra 70 e 90, e una riga nera segna il calo davvero avvenuto, 83.
Output
btcusdt: 3239 giorni ricampionati a blocchi di 20 la storia capitata: 13.68x calo massimo -83.2% mediana dei percorsi: 15.12x calo mediano -79.9% il 5% peggiore chiude a: 0.47x con cali fino a -95.3% il 5% migliore chiude a: 446.78x percorsi che finiscono sotto il capitale iniziale: 10.3%
Espandi lo script di questo passo
rng = np.random.default_rng(seed_for("lab-montecarlo"))
percorsi = bootstrap_traiettorie(r, n_traiettorie=PERCORSI, rng=rng, a_blocchi=BLOCCHI)
with avvio.figura("schermo"):
fig, (sx, dx) = plt.subplots(1, 2, figsize=(12, 4.5))
for k in range(120):
# `rasterized=True` sul solo strato denso: 120 traiettorie da 3 240
# punti sono ~389 000 vertici, e in un SVG ogni vertice diventa testo.
# Rasterizzando queste strisce — e solo queste — assi, griglia, legenda
# e numeri restano vettoriali e leggibili, la figura non cambia di una
# traiettoria, e il file passa da 2 132 KB a 148 KB.
sx.semilogy(percorsi[k], linewidth=0.5, alpha=0.4, color="#7A8CC7", rasterized=True)
sx.semilogy(reale, linewidth=2.5, color="black", label="la storia capitata")
sx.set_ylabel("Capitale (scala log)")
sx.set_xlabel("Giorni")
sx.legend()
cali = np.array([drawdown_massimo(p) for p in percorsi])
dx.hist(cali * 100, bins=60)
dx.axvline(drawdown_massimo(reale) * 100, linewidth=2.5, color="black")
dx.set_xlabel("Calo massimo del percorso (%)")
dx.set_ylabel(f"Su {PERCORSI} percorsi")
plt.show()
esiti = distribuzione_esiti(percorsi)
print(f"{SERIE}: {len(r)} giorni ricampionati a blocchi di {BLOCCHI}\n")
print(f"la storia capitata: {reale[-1]:8.2f}x calo massimo {drawdown_massimo(reale):7.1%}")
print(f"mediana dei percorsi: {esiti['mediana_finale']:8.2f}x calo mediano "
f"{esiti['drawdown_mediano']:7.1%}")
print(f"il 5% peggiore chiude a: {esiti['peggiore_5pct']:8.2f}x con cali fino a "
f"{esiti['drawdown_peggiore_5pct']:7.1%}")
print(f"il 5% migliore chiude a: {esiti['migliore_5pct']:8.2f}x")
print(f"percorsi che finiscono sotto il capitale iniziale: {esiti['prob_perdita']:.1%}")Rileggi l'ultima riga. Con gli stessi identici rendimenti, una quota non trascurabile dei percorsi finisce in perdita. Non per una decisione sbagliata: per la combinazione in cui sono arrivate le cose.
E nota il calo massimo: quello già visto non è il peggio possibile. È solo il peggio di una realizzazione, cioè di un campione di dimensione uno.
2. Dal grafico alla decisione: tre numeri e una soglia
Metti la tua soglia e guarda se la posizione è troppo grande.
Output
calo al 5esimo percentile dei percorsi possibili: -95.3% mettendoci il 50% del capitale, sul totale fa: 47.7% la tua soglia: 30.0% → la posizione e' TROPPO GRANDE. Compatibile con la tua soglia: 31.5% del capitale, cioe' 6,294 euro. Regola: riduci la posizione finche' il calo al quinto percentile non sta sotto la tua soglia. Non serve altro, e questo unico passaggio fa piu' lavoro di qualunque affinamento della strategia.
Espandi lo script di questo passo
CAPITALE = 20_000.0 # PROVA / TRY: il tuo capitale reale
SOGLIA_PERDITA = 0.30 # ← oltre questo calo cambieresti comportamento
# PROVA / TRY: la tua soglia vera
QUOTA = 0.50 # ← quanta parte del capitale metti in questa posizione
# PROVA / TRY: la quota che stai davvero valutando
calo_5pct = float(np.percentile([drawdown_massimo(p) for p in percorsi], 5))
calo_atteso_sul_totale = abs(calo_5pct) * QUOTA
print(f"calo al 5esimo percentile dei percorsi possibili: {calo_5pct:.1%}")
print(f"mettendoci il {QUOTA:.0%} del capitale, sul totale fa: {calo_atteso_sul_totale:.1%}")
print(f"la tua soglia: {SOGLIA_PERDITA:.1%}\n")
if calo_atteso_sul_totale > SOGLIA_PERDITA:
quota_compatibile = SOGLIA_PERDITA / abs(calo_5pct)
print(f"→ la posizione e' TROPPO GRANDE. Compatibile con la tua soglia: "
f"{quota_compatibile:.1%} del capitale, cioe' "
f"{quota_compatibile * CAPITALE:,.0f} euro.")
else:
print("→ la posizione e' compatibile con la soglia che hai dichiarato.")
print("\nRegola: riduci la posizione finche' il calo al quinto percentile non sta "
"sotto la tua soglia. Non serve altro, e questo unico passaggio fa piu' "
"lavoro di qualunque affinamento della strategia.")3. Perché a blocchi e non giorno per giorno
Il confronto che giustifica l'avvertenza tecnica.
Output
memoria della vol. calo mediano 5% peggiore
giorno per giorno -0.002 -77.2% -94.5%
a blocchi di 20 0.164 -79.6% -94.8%
la storia vera 0.175 -83.2% —
La prima colonna e' quella che decide. Rimescolare giorno per giorno azzera la memoria della volatilita': si ottengono percorsi in cui i giorni agitati sono sparsi, che non e' come si comporta nessun mercato. I blocchi la conservano quasi tutta.
Sulle altre due colonne, invece, la differenza qui e' piccola — e va detto invece di nasconderlo. Su un orizzonte di nove anni il calo massimo e' dominato dall'accumulo, non dal raggruppamento. Il metodo a blocchi resta quello giusto, ma su QUESTE due misure non e' li' che si vede.Espandi lo script di questo passo
def memoria_della_volatilita(percorso: np.ndarray) -> float:
"""Autocorrelazione a un giorno dell'ampiezza dei movimenti.
E' la misura diretta del raggruppamento: se i giorni agitati arrivano in
gruppo, l'ampiezza di oggi somiglia a quella di ieri.
"""
variazioni = np.abs(percorso[1:] / percorso[:-1] - 1.0)
return float(np.corrcoef(variazioni[:-1], variazioni[1:])[0, 1])
rng2 = np.random.default_rng(seed_for("lab-montecarlo-confronto"))
puntuale = bootstrap_traiettorie(r, n_traiettorie=400, rng=rng2, a_blocchi=None)
a_blocchi = bootstrap_traiettorie(r, n_traiettorie=400, rng=rng2, a_blocchi=BLOCCHI)
print(f"{'':>22s} {'memoria della vol.':>20s} {'calo mediano':>14s} {'5% peggiore':>13s}")
for nome, insieme in (("giorno per giorno", puntuale), (f"a blocchi di {BLOCCHI}", a_blocchi)):
memoria = np.median([memoria_della_volatilita(p) for p in insieme])
cali = np.array([drawdown_massimo(p) for p in insieme])
print(f"{nome:>22s} {memoria:20.3f} {np.median(cali):14.1%} "
f"{np.percentile(cali, 5):13.1%}")
print(f"{'la storia vera':>22s} {memoria_della_volatilita(reale):20.3f} "
f"{drawdown_massimo(reale):14.1%} {'—':>13s}")
print("\nLa prima colonna e' quella che decide. Rimescolare giorno per giorno "
"azzera la memoria della volatilita': si ottengono percorsi in cui i giorni "
"agitati sono sparsi, che non e' come si comporta nessun mercato. I blocchi "
"la conservano quasi tutta.")
print("\nSulle altre due colonne, invece, la differenza qui e' piccola — e va "
"detto invece di nasconderlo. Su un orizzonte di nove anni il calo massimo "
"e' dominato dall'accumulo, non dal raggruppamento. Il metodo a blocchi "
"resta quello giusto, ma su QUESTE due misure non e' li' che si vede.")4. Quanti percorsi servono davvero
Mille bastano per la mediana e sono al limite per il quinto percentile. Se il numero che ti serve è una coda — e in questo quaderno è sempre una coda — diecimila costano qualche secondo in più e danno un valore su cui appoggiare una decisione.
Output
percorsi mediana 5% peggiore
100 13.83x 0.44x
500 18.19x 0.54x
1000 14.71x 0.50x
5000 15.18x 0.48x
20000 14.81x 0.47x
La colonna di sinistra si stabilizza subito, quella di destra molto piu' tardi. E' uno dei rari casi, in questo libro, in cui il problema si risolve semplicemente calcolando di piu'.Espandi lo script di questo passo
print(f"{'percorsi':>10s} {'mediana':>10s} {'5% peggiore':>14s}")
for n in (100, 500, 1000, 5000, 20000):
rng3 = np.random.default_rng(seed_for(f"stabilita-{n}"))
campione = bootstrap_traiettorie(r, n_traiettorie=n, rng=rng3, a_blocchi=BLOCCHI)
finali = campione[:, -1]
print(f"{n:10d} {np.median(finali):9.2f}x {np.percentile(finali, 5):13.2f}x")
print("\nLa colonna di sinistra si stabilizza subito, quella di destra molto piu' "
"tardi. E' uno dei rari casi, in questo libro, in cui il problema si "
"risolve semplicemente calcolando di piu'.")Esercizi
- Incolla i tuoi rendimenti al posto di
r(una lista di variazioni percentuali per operazione va benissimo) e leggi la frase del quinto percentile. È la cosa più utile che questo quaderno possa darti. - Cambia
BLOCCHIda 5 a 60. Il calo mediano cresce con la lunghezza dei blocchi: la scelta è un parametro, non una verità, e va dichiarata. - Cambia
SERIE. Su un asset più volatile la distanza fra mediana e quinto percentile si allarga: è la misura di quanto poco la mediana descriva quel mercato.
Riproducibilità e download
Eseguito il 2026-08-27 dal quaderno del repository
Il quaderno
lab_18_montecarlo.ipynb15,5 KB
sha256 ccc0148896e31223fba548dd4978d81a5a72493deb9f93ae15c5ed5f725116e0
lab_18_montecarlo.py12,1 KB
sha256 f1c9771b4454d10dcddedd1649a0ec17fd5e689d3e76a13939ae2e7de175e810
I dati
btcusdt.parquet93,2 KB
sha256 ea75ad84e6e981507054df5c622c6b0ec3c8849c1f4dd007721878d4e4c8a329
Fonte: Binance Data Vision · Periodo: 2017-08-17 → 2026-06-30 · 3240 righe · estratto 2026-08-16
ethusdt.parquet87,0 KB
sha256 c2bd0259da905e0fec87235d7a62295532433fb89657726dd2d19558db7c072a
Fonte: Binance Data Vision · Periodo: 2017-08-17 → 2026-06-30 · 3240 righe · estratto 2026-08-16
solusdt.parquet57,5 KB
sha256 c7ba2368a3e419b898fb31ec6d5345b7212b74784b69079d3d43571c2ac63657
Fonte: Binance Data Vision · Periodo: 2020-08-11 → 2026-06-30 · 2150 righe · estratto 2026-08-16