Lab del libro · L14
L14 — Venti strategie senza vantaggio e il correttore per test multipli
Lab 14 — Come mente un backtest: il metodo
Quaderno del capitolo «Come mente un backtest — il metodo» di La matematica di chi perde.
Gli errori del capitolo precedente stanno nel codice e si trovano. Questi no: stanno nel modo in cui hai lavorato, non lasciano traccia e non falliscono nessun controllo automatico.
Qui li rendiamo visibili con delle simulazioni, e alla fine c'è il correttore per test multipli: gli dici quanti tentativi hai fatto e ti restituisce la soglia che avresti dovuto usare.
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
from math import erf, sqrt
import matplotlib.pyplot as plt
import numpy as np
from cvbook import seed_for
from cvbook.dati import carica
from cvbook.regole import compra_e_tieni, esegui, rottura, sopra_media
ALFA = 0.05
def quantile_normale(p: float) -> float:
basso, alto = -10.0, 10.0
for _ in range(200):
mezzo = (basso + alto) / 2
if 0.5 * (1 + erf(mezzo / sqrt(2))) < p:
basso = mezzo
else:
alto = mezzo
return (basso + alto) / 21. Venti idee che non funzionano, e quella che passa
Venti barre, una per idea provata, con la statistica t sull'asse verticale da meno 2 a 1,5 e una riga tratteggiata sulla soglia del test standard, 1,64. Nessuna delle venti idee ha un vantaggio dentro — e' stato messo a zero — eppure una barra supera la soglia.
Output
1 idee su 20 hanno superato il test. Nessuna di esse aveva un vantaggio: era zero, messo li' da noi.
Espandi lo script di questo passo
IDEE = 20 # PROVA / TRY: 500 (vedi esercizio 2)
OSSERVAZIONI = 400
rng = np.random.default_rng(seed_for("lab-metodo-venti"))
soglia = quantile_normale(1 - ALFA)
statistiche = []
for _ in range(IDEE):
campione = rng.normal(0.0, 0.03, OSSERVAZIONI) # vantaggio ESATTAMENTE zero
statistiche.append(campione.mean() / (campione.std(ddof=1) / np.sqrt(OSSERVAZIONI)))
statistiche = np.array(statistiche)
passate = statistiche > soglia
with avvio.figura("schermo"):
fig, ax = plt.subplots(figsize=(9, 4))
ax.bar(np.arange(1, IDEE + 1), statistiche)
ax.axhline(soglia, linestyle="--", linewidth=1.5,
label=f"soglia del test standard ({soglia:.2f})")
ax.set_xlabel("Idea provata")
ax.set_ylabel("Statistica t")
ax.legend()
plt.show()
print(f"{int(passate.sum())} idee su {IDEE} hanno superato il test.")
print("Nessuna di esse aveva un vantaggio: era zero, messo li' da noi.")2. La tabella che cambia il significato di ogni risultato
Output
idee provate prob. che almeno una passi
1 5.0%
5 22.6%
10 40.1%
20 64.2%
50 92.3%
100 99.4%
Chi prova cento configurazioni TROVERA' qualcosa che supera il test. E non sapra' di aver trovato niente, perche' il risultato finale ha esattamente lo stesso aspetto di una scoperta vera.Espandi lo script di questo passo
print(f"{'idee provate':>13s} {'prob. che almeno una passi':>28s}")
for n in (1, 5, 10, 20, 50, 100):
print(f"{n:13d} {1 - (1 - ALFA) ** n:27.1%}")
print("\nChi prova cento configurazioni TROVERA' qualcosa che supera il test. "
"E non sapra' di aver trovato niente, perche' il risultato finale ha "
"esattamente lo stesso aspetto di una scoperta vera.")3. I tentativi che non conti
Il capitolo elenca le scelte che non compaiono in nessun conteggio: quale asset, quale periodo, quale regola, quando fermarsi. Qui le contiamo davvero, su dati veri.
Output
combinazioni provate: 45 (3 serie × 5 regole × 3 date d'inizio) la migliore: rottura a 20 su ethusdt partendo dal giorno 0 → 48.88x, cioe' 9.40 volte il compra-e-tieni quante battono il compra-e-tieni: 38 su 45 (84%) mediana del rapporto: 1.94 Se pubblicassi solo la prima riga, non avrei mentito su nessun numero. Avrei omesso il denominatore.
Espandi lo script di questo passo
SERIE = ["btcusdt", "ethusdt", "solusdt"]
REGOLE = { # PROVA / TRY: aggiungi una regola (vedi esercizio 1)
"sopra la media 50": lambda p: sopra_media(p, 50),
"sopra la media 100": lambda p: sopra_media(p, 100),
"sopra la media 200": lambda p: sopra_media(p, 200),
"rottura a 20": lambda p: rottura(p, 20),
"rottura a 55": lambda p: rottura(p, 55),
}
PARTENZE = [0, 365, 730] # PROVA / TRY: aggiungi una data d'inizio (esercizio 1)
risultati = []
for nome_serie in SERIE:
p_intero = carica(nome_serie).sort("data")["chiusura"].to_numpy()
for nome_regola, regola in REGOLE.items():
for partenza in PARTENZE:
p = p_intero[partenza:]
if len(p) < 400:
continue
finale = esegui(p, regola(p), costo=0.0012)["finale"]
# Il denominatore passa dallo stesso motore delle regole, con gli
# stessi costi: `p[-1] / p[0]` faceva entrare il compra-e-tieni
# senza pagare il proprio ingresso mentre ogni regola pagava.
riferimento = esegui(p, compra_e_tieni(p), costo=0.0012)["finale"]
risultati.append((nome_serie, nome_regola, partenza,
finale, finale / riferimento))
print(f"combinazioni provate: {len(risultati)} "
f"({len(SERIE)} serie × {len(REGOLE)} regole × {len(PARTENZE)} date d'inizio)\n")
rapporti = np.array([r[4] for r in risultati])
migliore = risultati[int(np.argmax(rapporti))]
print(f"la migliore: {migliore[1]} su {migliore[0]} partendo dal giorno {migliore[2]}")
print(f" → {migliore[3]:.2f}x, cioe' {migliore[4]:.2f} volte il compra-e-tieni\n")
print(f"quante battono il compra-e-tieni: {int((rapporti > 1).sum())} su {len(rapporti)} "
f"({(rapporti > 1).mean():.0%})")
print(f"mediana del rapporto: {np.median(rapporti):.2f}")
print("\nSe pubblicassi solo la prima riga, non avrei mentito su nessun numero. "
"Avrei omesso il denominatore.")4. Il correttore per test multipli
Applicalo ai tuoi risultati passati. Con una certa cautela emotiva.
Output
tentativi dichiarati: 45 soglia non corretta: 1.64 soglia corretta: 3.06 il tuo 2.40 NON supera la soglia corretta.
Espandi lo script di questo passo
def soglia_corretta(tentativi: int, alfa: float = ALFA) -> float:
"""Correzione conservativa: il rischio accettato si divide per i tentativi."""
return quantile_normale(1 - alfa / tentativi)
TUOI_TENTATIVI = len(risultati) # ← PROVA / TRY: metti il numero dal TUO registro delle ipotesi
TUA_STATISTICA = 2.4 # ← PROVA / TRY: la statistica t del TUO risultato migliore
print(f"tentativi dichiarati: {TUOI_TENTATIVI}")
print(f"soglia non corretta: {quantile_normale(1 - ALFA):.2f}")
print(f"soglia corretta: {soglia_corretta(TUOI_TENTATIVI):.2f}")
print(f"\nil tuo {TUA_STATISTICA:.2f} " +
("SUPERA" if TUA_STATISTICA > soglia_corretta(TUOI_TENTATIVI) else "NON supera") +
" la soglia corretta.")5. Il registro delle ipotesi
Tutte le difese di questo capitolo si riducono a una pratica sola, che costa dieci minuti e vale più di qualunque tecnica sofisticata. Ecco lo scheletro: copialo in un file di testo e tienilo in ordine cronologico.
Output
data: 2026-08-16 ipotesi: la rottura del massimo a N giorni produce un vantaggio su BTC successo se: supera il 95esimo percentile del metro del caso, con costi 0,25% fallimento se: resta sotto, oppure il risultato dipende da N in modo instabile varianti previste: 24 valori di N, un solo mercato --- esito --- risultato: tentativi effettivi: note: Dopo sei mesi quel file ti dira' una cosa che nessun backtest puo' dirti: QUANTE idee hai provato in tutto. E quel numero e' il moltiplicatore da applicare a ogni tuo risultato positivo.
Espandi lo script di questo passo
MODELLO = """\
data: 2026-08-16
ipotesi: la rottura del massimo a N giorni produce un vantaggio su BTC
successo se: supera il 95esimo percentile del metro del caso, con costi 0,25%
fallimento se: resta sotto, oppure il risultato dipende da N in modo instabile
varianti previste: 24 valori di N, un solo mercato
--- esito ---
risultato:
tentativi effettivi:
note:
"""
print(MODELLO)
print("Dopo sei mesi quel file ti dira' una cosa che nessun backtest puo' dirti: "
"QUANTE idee hai provato in tutto. E quel numero e' il moltiplicatore da "
"applicare a ogni tuo risultato positivo.")Esercizi
- Nella terza cella aggiungi una regola e una data d'inizio. Il numero di combinazioni cresce come il prodotto, non come la somma — ed è quello il numero che conta.
- Nella prima cella porta
IDEEa 500 e conta quante passano. Circa il 5%, come previsto. Ognuna di esse, mostrata da sola, sembrerebbe una scoperta. - Prendi un tuo risultato passato, stima quanti tentativi ci sono stati dietro (contando anche quelli informali) e passalo al correttore. È l'esercizio più scomodo del libro.
Riproducibilità e download
Eseguito il 2026-08-27 dal quaderno del repository
Il quaderno
lab_14_bias_metodo.ipynb13,3 KB
sha256 1f7f24eec11a879da3e44a00cd8bc5a9157015a8bab673c4f23fa8d3f5f4f197
lab_14_bias_metodo.py9,8 KB
sha256 8a162337cee3fd77ee9a569a0d82204e96d9c9cf8ea1e844681bdd2aac9f6b05
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