Lab del libro · L20
L20 — Seme del caso, vettorizzazione e riproducibilità
Lab 20 — Le basi che ti servono davvero
Quaderno del capitolo «Le basi che ti servono davvero» di La matematica di chi perde.
Cinque celle, una per ciascuna delle idee del capitolo. Non serve saper programmare per seguirle: serve leggere i commenti e cambiare i numeri.
L'esercizio finale è togliere il seme dalla simulazione ed eseguirla tre volte. Vedere tre risultati diversi dalla stessa identica cella è il modo più rapido per capire perché la riproducibilità non è un dettaglio.
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 time
import matplotlib.pyplot as plt
import numpy as np
import polars as pl
from cvbook.dati import carica, leggi_registro
from cvbook.metriche import drawdown, rendimentiUno — La riproducibilità
Un lavoro è riproducibile se, eseguito di nuovo, dà lo stesso identico risultato. Sempre. Su un altro computer, fra un anno, eseguito da un'altra persona.
Qui i dati sono congelati: salvati una volta, con la data di estrazione e un'impronta che ne verifica l'integrità.
Output
serie: btcusdt fonte: Binance Data Vision estratta: 2026-08-16 periodo: 2017-08-17 → 2026-06-30 (3240 righe) impronta: ea75ad84e6e981507054df5c622c6b0ec3c8849c1f4dd007721878d4e4c8a329 Se qualcuno modificasse quel file, `carica()` si rifiuterebbe di eseguire. Non e' pignoleria: e' cio' che rende le figure del libro verificabili fra dieci anni.
Espandi lo script di questo passo
voce = leggi_registro()["btcusdt"]
print(f"serie: {voce.nome}")
print(f"fonte: {voce.fonte}")
print(f"estratta: {voce.estratto}")
print(f"periodo: {voce.dal} → {voce.al} ({voce.righe} righe)")
print(f"impronta: {voce.sha256}")
print("\nSe qualcuno modificasse quel file, `carica()` si rifiuterebbe di "
"eseguire. Non e' pignoleria: e' cio' che rende le figure del libro "
"verificabili fra dieci anni.")Due — Il seme del caso
I computer non producono numeri davvero casuali: producono sequenze che sembrano casuali, generate a partire da un numero iniziale detto seme. Stesso seme, stessa sequenza.
Output
con lo stesso seme, tre esecuzioni:
[ 0.3047 -1.04 0.7505 0.9406 -1.951 ]
[ 0.3047 -1.04 0.7505 0.9406 -1.951 ]
[ 0.3047 -1.04 0.7505 0.9406 -1.951 ]
senza fissare il seme, tre esecuzioni:
[1.1927 0.2206 1.5968 0.2193 1.2435]
[-0.6375 0.4579 0.5312 -0.0172 -1.0695]
[ 0.6577 -0.6453 -0.0103 0.7493 -1.5414]
Quando qualcuno mostra il risultato di una simulazione, CHIEDI se il seme e' fissato e qual e'. Se non lo e', quel risultato non si puo' ricontrollare — e se ha eseguito piu' volte scegliendo l'esecuzione che gli piaceva di piu', e' il capitolo sui test multipli applicato ai numeri casuali.Espandi lo script di questo passo
print("con lo stesso seme, tre esecuzioni:")
for _ in range(3):
rng = np.random.default_rng(42)
print(" ", np.round(rng.normal(size=5), 4))
print("\nsenza fissare il seme, tre esecuzioni:")
for _ in range(3):
rng = np.random.default_rng() # ← nessun seme
print(" ", np.round(rng.normal(size=5), 4))
print("\nQuando qualcuno mostra il risultato di una simulazione, CHIEDI se il "
"seme e' fissato e qual e'. Se non lo e', quel risultato non si puo' "
"ricontrollare — e se ha eseguito piu' volte scegliendo l'esecuzione che "
"gli piaceva di piu', e' il capitolo sui test multipli applicato ai "
"numeri casuali.")Tre — La vettorizzazione
Il modo intuitivo di elaborare tremila giorni è: prendi il primo, fai il conto; prendi il secondo, fai il conto. Funziona ed è lentissimo.
Il modo giusto è pensare all'intera serie come a un oggetto solo.
Output
un giorno alla volta: 1.63 ms tutta la serie insieme: 0.116 ms rapporto: 14 volte risultati identici? True Ma la velocita' non e' il punto vero. Il punto e' che CAMBIA LE DOMANDE che ti vengono in mente: chi pensa per serie intere si chiede naturalmente «quante volte e' successo, su tutti gli asset, in tutti i periodi». Chi pensa un giorno alla volta si ferma prima.
Espandi lo script di questo passo
prezzi = carica("btcusdt").sort("data")["chiusura"].to_numpy()
# Modo intuitivo: un giorno alla volta.
inizio = time.perf_counter()
lento = []
for i in range(1, len(prezzi)):
lento.append(prezzi[i] / prezzi[i - 1] - 1)
tempo_lento = time.perf_counter() - inizio
# Modo vettorizzato: un'istruzione sola, applicata a tutto.
inizio = time.perf_counter()
veloce = prezzi[1:] / prezzi[:-1] - 1
tempo_veloce = time.perf_counter() - inizio
print(f"un giorno alla volta: {tempo_lento * 1000:8.2f} ms")
print(f"tutta la serie insieme:{tempo_veloce * 1000:8.3f} ms")
print(f"rapporto: {tempo_lento / max(tempo_veloce, 1e-9):.0f} volte")
print(f"risultati identici? {np.allclose(lento, veloce)}")
print("\nMa la velocita' non e' il punto vero. Il punto e' che CAMBIA LE DOMANDE "
"che ti vengono in mente: chi pensa per serie intere si chiede "
"naturalmente «quante volte e' successo, su tutti gli asset, in tutti i "
"periodi». Chi pensa un giorno alla volta si ferma prima.")Quattro — Ripetere su molte serie
È il passaggio che apre le domande interessanti. Tre righe.
Output
btcusdt: 40.1% del tempo con meno della meta' del proprio massimo ethusdt: 58.4% del tempo con meno della meta' del proprio massimo solusdt: 50.6% del tempo con meno della meta' del proprio massimo
Espandi lo script di questo passo
risposte = {}
for nome in ("btcusdt", "ethusdt", "solusdt"): # PROVA / TRY: aggiungi un'altra serie preparata
p = carica(nome).sort("data")["chiusura"].to_numpy()
dd = drawdown(np.concatenate([[1.0], np.cumprod(1 + rendimenti(p))]))
risposte[nome] = float((dd < -0.5).mean())
for nome, quota in risposte.items():
print(f"{nome:>10s}: {quota:5.1%} del tempo con meno della meta' del proprio massimo")Cinque — Confrontare con il caso
Generare percorsi casuali e posizionarci sopra il proprio risultato. È forse la singola capacità più utile di tutto il libro.
Output
il risultato vero: 13.68x
con il seme fissato, tre esecuzioni:
0.492
0.492
0.492
senza seme, tre esecuzioni:
0.545
0.485
0.485
E' l'esercizio finale del capitolo: la stessa identica cella, tre risultati diversi. Se un numero cambia a ogni esecuzione, non si puo' ricontrollare — e quindi non e' una prova.Espandi lo script di questo passo
r = rendimenti(prezzi)
reale = float(np.prod(1 + r))
def esperimento(seme: int | None, percorsi: int = 400) -> float: # PROVA / TRY: percorsi=4000
"""Quota di percorsi ricampionati che fanno meglio di quello vero.
Attenzione a una sottigliezza che il capitolo sull'aritmetica ha gia'
incontrato: **rimescolare** i rendimenti non cambia il capitale finale — la
moltiplicazione e' commutativa. Per ottenere storie diverse bisogna
ricampionare **con reinserimento**, cioe' costruire percorsi in cui alcuni
periodi si ripetono e altri mancano.
"""
generatore = np.random.default_rng(seme)
indici = generatore.integers(0, len(r), size=(percorsi, len(r)))
return float((np.prod(1 + r[indici], axis=1) > reale).mean())
print(f"il risultato vero: {reale:.2f}x\n")
print("con il seme fissato, tre esecuzioni:")
for _ in range(3):
print(f" {esperimento(2026):.3f}")
print("\nsenza seme, tre esecuzioni:")
for _ in range(3):
print(f" {esperimento(None):.3f}")
print("\nE' l'esercizio finale del capitolo: la stessa identica cella, tre "
"risultati diversi. Se un numero cambia a ogni esecuzione, non si puo' "
"ricontrollare — e quindi non e' una prova.")Le quattro capacità, e basta
- Caricare dei dati e guardarli. È il novanta per cento del lavoro reale.
- Fare un conto su un'intera serie. Rendimenti, medie, distanze dal massimo: tre righe ciascuna.
- Ripetere il conto su molte serie. È il passaggio che apre le domande interessanti.
- Confrontare con il caso. La cella qui sopra.
Non serve altro. Non servono le classi, le strutture dati avanzate, i modelli di apprendimento automatico. Il modo più rapido che conosco per cominciare non è studiare il linguaggio: è modificare qualcosa che già funziona e guardare cosa cambia. Questo quaderno è fatto per quello.
Riproducibilità e download
Eseguito il 2026-08-27 dal quaderno del repository
Il quaderno
lab_20_basi.ipynb12,9 KB
sha256 931eff3473c8fb6cc4553bbfc6fab4472ec2c70170fa63e0c204b8c3c238416c
lab_20_basi.py9,6 KB
sha256 26f9a220b7598c43ae0d0f921a539bae21116912f8946af98bb9ff78259f413d
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