Lab from the book · L20
L20 — Random seed, vectorisation and reproducibility
Lab 20 — The basics you actually need
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 basics you actually need". Five cells, one for each idea in the chapter. You don't need to know how to program to follow them: you need to read the comments and change the numbers. The final exercise is removing the seed from the simulation and running it three times. Seeing three different results from the exact same cell is the fastest way to understand why reproducibility isn't a detail.
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 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, rendimentiOne — Reproducibility
A piece of work is reproducible if, run again, it gives the exact same result. Always. On another computer, a year from now, run by someone else. Here the data is frozen: saved once, with an extraction date and a fingerprint that verifies its integrity.
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.
Show the script for this step
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.")Two — The random seed
Computers don't produce truly random numbers: they produce sequences that look random, generated from a starting number called a seed. Same seed, same sequence.
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.Show the script for this step
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.")Three — Vectorization
The intuitive way of processing three thousand days is: take the first, do the math; take the second, do the math. It works and it's excruciatingly slow. The right way is to think of the entire series as a single object.
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.
Show the script for this step
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.")Four — Repeating over many series
It's the step that opens up the interesting questions. Three lines.
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
Show the script for this step
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")Five — Comparing against chance
Generating random paths and placing your own result on top of them. It's perhaps the single most useful skill in the whole book.
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.Show the script for this step
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.")The four skills, and that's it
- Loading data and looking at it. It's ninety percent of the real work.
- Doing a calculation over an entire series. Returns, averages, distances from the peak: three lines each.
- Repeating the calculation over many series. It's the step that opens up the interesting questions.
- Comparing against chance. The cell above.
Nothing else is needed. No classes, no advanced data structures, no machine learning models. The fastest way I know to get started isn't studying the language: it's modifying something that already works and watching what changes. This notebook is built for that.
Reproducibility & downloads
Run on 2026-08-27 from the repository notebook
The notebook
lab_20_basi.ipynb12.9 KB
sha256 931eff3473c8fb6cc4553bbfc6fab4472ec2c70170fa63e0c204b8c3c238416c
lab_20_basi.py9.6 KB
sha256 26f9a220b7598c43ae0d0f921a539bae21116912f8946af98bb9ff78259f413d
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