Lab from the book · L05
L05 — Three views of the same data, and the real-versus-synthetic test
Lab 5 — Looking is not measuring
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 "What measuring means". Three views of the same identical data: price, changes, distribution. They aren't three levels of detail — they are three different questions, and the first step of measuring is knowing which one you're asking. Then there's the exercise worth doing before reading further in the book: telling a real series apart from a randomly generated one. People manage it a bit more than half the time — barely better than a coin toss.
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 import seed_for
from cvbook.dati import carica
from cvbook.metriche import rendimenti, volatilita1. Three views
Change SERIE and rerun: the shape of the first panel changes a lot, the third much less. That's why the third allows comparisons the first doesn't.
Three side-by-side panels over the same 3,240 days of btcusdt, from 17 August 2017 to 30 June 2026. On the left the price on a logarithmic axis running between 10⁴ and 10⁵; in the middle the daily change in percent, its axis graduated from −40 to 20; on the right a histogram of those same daily changes, its count on a logarithmic axis from 10⁰ to 10², with the mass packed around zero.
Output
btcusdt: 3240 giorni, dal 2017-08-17 al 2026-06-30 volatilita' annualizzata dell'intero periodo: 67.5%
Show the script for this step
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)
with avvio.figura("schermo"):
fig, (a, b, c) = plt.subplots(1, 3, figsize=(13, 4))
a.semilogy(date, prezzi, linewidth=1.2)
a.set_title("1. Il prezzo\n«dove siamo arrivati»", fontsize=10)
a.set_ylabel("Prezzo (scala log)")
b.plot(date[1:], r * 100, linewidth=0.5)
b.set_title("2. Le variazioni\n«quanto si muove ogni giorno»", fontsize=10)
b.set_ylabel("Variazione giornaliera (%)")
c.hist(r * 100, bins=120)
c.set_title("3. La distribuzione\n«quanto spesso succede cosa»", fontsize=10)
c.set_xlabel("Variazione giornaliera (%)")
c.set_yscale("log")
for ax in (a, b):
ax.tick_params(axis="x", rotation=30)
plt.show()
print(f"{SERIE}: {len(prezzi)} giorni, dal {date[0]} al {date[-1]}")
print(f"volatilita' annualizzata dell'intero periodo: {volatilita(r):.1%}")In the second panel look at something the first one doesn't show: big shocks come clustered. There are calm periods and turbulent ones, and turbulent days sit close to each other. It's the chapter on regimes.
2. Real or fake?
Six charts. Some are real prices, others are random walks with the same volatility. Write down your answer before running the next cell.
Six panels on two rows, titled "grafico 1" to "grafico 6", each holding 400 days rebased to 100 and carrying no time axis. Three are real windows of btcusdt and three are random walks with the same daily standard deviation; the vertical scales run from 40 in the first panel to 225 in the fifth, while the second and the sixth stay between 60 and 130.
Show the script for this step
rng = np.random.default_rng(seed_for("lab-vero-o-finto"))
# NON TOCCARE / DO NOT CHANGE: scrivi la tua risposta PRIMA di eseguire la
# cella con la soluzione. Cambiare il seme per "azzeccarci di più" vanifica
# l'esercizio invece di misurarlo.
# Write down your answer BEFORE running the cell with the solution. Changing
# the seed to "get it more right" defeats the exercise instead of measuring it.
FINESTRA = 400 # PROVA / TRY: 400 · 1200 (vedi esercizio 2)
sigma = float(np.std(r, ddof=1))
partenze = rng.integers(0, len(prezzi) - FINESTRA, size=6)
etichette = rng.permutation(["vera", "finta", "vera", "finta", "finta", "vera"])
serie_mostrate = []
for k in range(6):
if etichette[k] == "vera":
s = prezzi[partenze[k]: partenze[k] + FINESTRA]
s = s / s[0] * 100
else:
s = 100 * np.cumprod(1 + rng.normal(0.0, sigma, FINESTRA))
serie_mostrate.append(s)
with avvio.figura("schermo"):
fig, assi = plt.subplots(2, 3, figsize=(12, 5))
for k, ax in enumerate(assi.flat):
ax.plot(serie_mostrate[k], linewidth=1.2)
ax.set_title(f"grafico {k + 1}", fontsize=10)
ax.set_xticks([])
plt.show()Output
soluzione: grafico 1: vera grafico 2: finta grafico 3: vera grafico 4: finta grafico 5: vera grafico 6: finta Quasi nessuno supera il caso in questo esercizio. Non e' un limite personale: e' che la percezione trova regolarita' anche nel rumore, ed e' il motivo per cui serve una misura che possa dire di no.
Show the script for this step
print("soluzione:")
for k, e in enumerate(etichette):
print(f" grafico {k + 1}: {e}")
print(
"\nQuasi nessuno supera il caso in questo esercizio. Non e' un limite "
"personale: e' che la percezione trova regolarita' anche nel rumore, ed e' "
"il motivo per cui serve una misura che possa dire di no."
)3. The four questions, in code
Before believing any number you need four answers. Here you watch them change the result one at a time.
Output
Su cosa?
btcusdt: 13.68x nel proprio periodo disponibile
ethusdt: 5.21x nel proprio periodo disponibile
solusdt: 22.33x nel proprio periodo disponibile
Su quale periodo? (stesso asset, finestre diverse)
ultimo anno: 0.55x
ultimi 3 anni: 1.91x
tutto: 13.68x
Con quale rappresentazione? (stessi dati, due misure di 'rendimento medio')
media aritmetica giornaliera: 0.1439% → su un anno 69.0%
composto giornaliero: 0.0808% → su un anno 34.3%
Confrontato con cosa? (la domanda che quasi nessuno pone)
mediana di 200 sotto-campioni casuali della stessa serie: 4.02xShow the script for this step
print("Su cosa?")
for nome in ("btcusdt", "ethusdt", "solusdt"):
p = carica(nome).sort("data")["chiusura"].to_numpy()
print(f" {nome}: {p[-1] / p[0]:8.2f}x nel proprio periodo disponibile")
print("\nSu quale periodo? (stesso asset, finestre diverse)")
p = carica("btcusdt").sort("data")["chiusura"].to_numpy()
for taglio, etichetta in [(365, "ultimo anno"), (1095, "ultimi 3 anni"), (len(p), "tutto")]:
s = p[-taglio:]
print(f" {etichetta:>14s}: {s[-1] / s[0]:8.2f}x")
print("\nCon quale rappresentazione? (stessi dati, due misure di 'rendimento medio')")
r_btc = rendimenti(p)
media = float(np.mean(r_btc))
composto = float(p[-1] / p[0]) ** (1 / len(r_btc)) - 1
print(f" media aritmetica giornaliera: {media:.4%} → su un anno {((1 + media) ** 365 - 1):.1%}")
print(f" composto giornaliero: {composto:.4%} → su un anno {((1 + composto) ** 365 - 1):.1%}")
print("\nConfrontato con cosa? (la domanda che quasi nessuno pone)")
casuali = np.array([
np.prod(1 + rng.permutation(r_btc)[: len(r_btc) // 2]) for _ in range(200)
])
print(f" mediana di 200 sotto-campioni casuali della stessa serie: {np.median(casuali):.2f}x")Exercises
- In the first cell change the series and watch which panel changes the most. The third is the one that makes different assets and eras comparable: that's why all of Part II works there.
- In the second cell, raise
FINESTRAto 1200. With longer series the exercise becomes a bit easier — but much less than you'd expect. - In the third cell, look at the difference between the two "average annual returns". They're the same data. One describes a typical day that doesn't exist, the other what actually happened to whoever was in it.
Reproducibility & downloads
Run on 2026-08-27 from the repository notebook
The notebook
lab_05_misurare.ipynb12.0 KB
sha256 83cc84f9f45968dddcf6a6cb1a781c5e0731b7a80c1b2bb8ee3d404164bac73d
lab_05_misurare.py9.0 KB
sha256 8338433949ead76426d5b059d75900ac82caa994d2776c7736adabb844255ffc
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