Lab del libro · L06
L06 — Code grasse, curtosi e i venti giorni che decidono tutto
Lab 6 — La media che mente
Quaderno del capitolo «La media che mente» di La matematica di chi perde.
Tre cose, sui dati veri: quanto la campana sbaglia sulle code, quanto pochi giorni decidono il risultato, e perché i due slogan opposti che si ricavano da quella figura sono la stessa affermazione — e nessuno dei due è un consiglio.
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.dati import carica
from cvbook.metriche import rendimenti
SERIE = "btcusdt" # ← PROVA / TRY: "ethusdt" · "solusdt" (le tre preparate nel setup)
df = carica(SERIE).sort("data")
prezzi = df["chiusura"].to_numpy()
date = df["data"].to_list()
r = rendimenti(prezzi)
mu, sigma = float(np.mean(r)), float(np.std(r, ddof=1))1. Quanti giorni estremi prevede la campana, e quanti ce ne sono
Se i rendimenti seguissero la curva a campana, il numero di giorni oltre una certa distanza dalla media sarebbe calcolabile. Confrontiamolo con quelli che ci sono davvero.
Output
btcusdt: 3239 giorni · media 0.1439% · deviazione standard 3.531%
oltre previsti dalla campana osservati rapporto
2σ 147.375 196 1.3x
3σ 8.745 54 6.2x
4σ 0.205 18 87.7x
5σ 0.002 5 2692.6x
6σ 0.000 2 312934.5x
giorno peggiore: -39.5% → 11.2 deviazioni standard dalla media
Secondo il modello a campana un evento del genere non dovrebbe accadere nemmeno una volta nella storia dell'universo. E' successo, in nove anni.
curtosi: 11.6 (per la curva a campana vale 3)Espandi lo script di questo passo
from math import erfc, sqrt
n = len(r)
print(f"{SERIE}: {n} giorni · media {mu:.4%} · deviazione standard {sigma:.3%}\n")
print(f"{'oltre':>8s} {'previsti dalla campana':>24s} {'osservati':>12s} {'rapporto':>10s}")
for k in (2, 3, 4, 5, 6):
previsti = n * erfc(k / sqrt(2))
osservati = int(np.sum(np.abs(r - mu) > k * sigma))
rapporto = osservati / previsti if previsti > 0 else float("inf")
print(f"{k:>6d}σ {previsti:24.3f} {osservati:12d} {rapporto:10.1f}x")
peggiore = float(np.min(r))
distanza = abs(peggiore - mu) / sigma
print(f"\ngiorno peggiore: {peggiore:.1%} → {distanza:.1f} deviazioni standard dalla media")
print("Secondo il modello a campana un evento del genere non dovrebbe accadere "
"nemmeno una volta nella storia dell'universo. E' successo, in nove anni.")
curtosi = float(np.mean(((r - mu) / sigma) ** 4))
print(f"\ncurtosi: {curtosi:.1f} (per la curva a campana vale 3)")2. La forma, disegnata
La scala verticale è logaritmica: senza, la differenza sulle code — cioè l'unica parte che conta — sarebbe invisibile.
Un istogramma delle variazioni giornaliere di btcusdt su 200 classi, con l'asse orizzontale da meno 0,4 a 0,2 e il conteggio dei giorni su scala logaritmica che copre quasi trenta ordini di grandezza, da 10 alla meno 27 a 10. Sopra l'istogramma corre la curva a campana con la stessa media e la stessa deviazione standard: al centro le due coincidono, mentre agli estremi la campana precipita di decine di ordini di grandezza dove i giorni realmente accaduti ci sono.
Espandi lo script di questo passo
from math import exp, pi
griglia = np.linspace(r.min(), r.max(), 400)
campana = n * np.exp(-((griglia - mu) ** 2) / (2 * sigma**2)) / (sigma * np.sqrt(2 * pi))
larghezza = griglia[1] - griglia[0]
with avvio.figura("schermo"):
fig, ax = plt.subplots()
ax.hist(r, bins=200, label="quello che e' successo")
ax.plot(griglia, campana * larghezza, linewidth=2, label="quello che prevede la campana")
ax.set_yscale("log")
ax.set_xlabel("Variazione giornaliera")
ax.set_ylabel("Numero di giorni (scala log)")
ax.legend()
plt.show()3. Venti giorni su 3.200
Togliamo dalla serie i giorni migliori, poi i peggiori, e guardiamo cosa resta.
Output
tutti i 3239 giorni: 13.68x
quanti giorni tolti togliendo i peggiori togliendo i migliori
1 22.62x 11.17x
5 48.43x 5.87x
10 102.96x 2.92x
20 381.21x 0.90x
50 8024.89x 0.04xEspandi lo script di questo passo
ordine = np.argsort(r)
def senza(indici_da_togliere: np.ndarray) -> float:
maschera = np.ones(len(r), dtype=bool)
maschera[indici_da_togliere] = False
return float(np.prod(1 + r[maschera]))
base = float(np.prod(1 + r))
print(f"tutti i {len(r)} giorni: {base:9.2f}x\n")
print(f"{'quanti giorni tolti':>22s} {'togliendo i peggiori':>22s} {'togliendo i migliori':>22s}")
for quanti in (1, 5, 10, 20, 50): # PROVA / TRY: aggiungi 100 (vedi esercizio 2)
peggiori = senza(ordine[:quanti])
migliori = senza(ordine[-quanti:])
print(f"{quanti:>22d} {peggiori:21.2f}x {migliori:21.2f}x")4. Ma stanno vicini
Il pezzo che quasi tutti i libri divulgativi omettono: i giorni migliori e i peggiori si toccano. Guarda marzo 2020 nella tabella qui sotto — il 12 è il peggiore di tutta la serie, il 13 è fra i migliori, e il 19 pure: tre dei venti giorni che decidono nove anni, nella stessa settimana.
L'ultima riga misura quanto vale il fenomeno in generale, e va letta per quello che dice: la mediana della distanza fra un giorno migliore e il peggiore più vicino è di due settimane, non di una. Ci sono coppie a un giorno di distanza e coppie a un anno. Chi esce per evitare i giorni peggiori non ha comunque il tempo di accorgersene.
Output
i dieci giorni peggiori e i dieci migliori, in ordine di calendario: 2017-09-14 -19.2% PEGGIORE 2017-09-15 +16.0% migliore 2017-12-06 +15.8% migliore 2017-12-07 +22.5% migliore 2017-12-22 -14.0% PEGGIORE 2017-12-26 +14.5% migliore 2017-12-30 -13.5% PEGGIORE 2018-01-16 -19.5% PEGGIORE 2018-02-05 -15.2% PEGGIORE 2019-04-02 +17.2% migliore 2019-10-25 +16.8% migliore 2020-03-12 -39.5% PEGGIORE 2020-03-13 +16.2% migliore 2020-03-19 +14.3% migliore 2021-02-08 +19.5% migliore 2021-05-19 -14.4% PEGGIORE 2022-02-28 +14.5% migliore 2022-06-13 -15.4% PEGGIORE 2022-11-09 -14.1% PEGGIORE 2026-02-05 -14.0% PEGGIORE distanza mediana fra un giorno migliore e il peggiore piu' vicino: 15 giorni
Espandi lo script di questo passo
peggiori_10 = np.sort(ordine[:10])
migliori_10 = np.sort(ordine[-10:])
print("i dieci giorni peggiori e i dieci migliori, in ordine di calendario:\n")
righe = sorted(
[(date[i + 1], r[i], "PEGGIORE") for i in peggiori_10]
+ [(date[i + 1], r[i], "migliore") for i in migliori_10]
)
for giorno, variazione, tipo in righe:
print(f" {giorno} {variazione:+7.1%} {tipo}")
distanze = [
min(abs(int(m) - int(p)) for p in peggiori_10) for m in migliori_10
]
print(f"\ndistanza mediana fra un giorno migliore e il peggiore piu' vicino: "
f"{int(np.median(distanze))} giorni")Esercizi
- Cambia
SERIEin"ethusdt"o"solusdt". La curtosi resta molto sopra 3 e la concentrazione del risultato in pochi giorni resta: non è una peculiarità di un asset, è una proprietà dei mercati. - Nella terza cella prova a togliere 100 giorni. Quel che resta non assomiglia più a niente di reale: è il motivo per cui «evitare i giorni peggiori» non è un consiglio ma una descrizione di un mondo che non esiste.
- Guarda l'ultima tabella. Prova a immaginare una regola che esca prima di ogni giorno peggiore e rientri prima di ogni giorno migliore: la distanza mediana appena stampata ti dice quanto tempo avresti per accorgertene.
Riproducibilità e download
Eseguito il 2026-08-27 dal quaderno del repository
Il quaderno
lab_06_code_grasse.ipynb11,4 KB
sha256 13a98ffd0f823af447c3e01b46d59b8ba29971f3bbeaa57fb433c6e0582fca20
lab_06_code_grasse.py8,5 KB
sha256 6e9e0b62b2aed0ee0bad2664bc8029c48ec82ffe271ca0007d249f3018099296
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