Vai al contenuto
PAPER SIAT 2026Apri la pagina della ricerca
Cryptoverso

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
lab_14_bias_metodo.py
python
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) / 2

1. 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.

Venti idee senza vantaggio, e quella che passa il test lo stesso.Fonte: Binance Data Vision · Periodo: 2017-08-17 … 2026-06-30 · Metodo: Venti sequenze di 400 osservazioni con vantaggio nullo; per ognuna la statistica t della media, confrontata con la soglia del test a una coda.

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
lab_14_bias_metodo.py
python
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
lab_14_bias_metodo.py
python
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
lab_14_bias_metodo.py
python
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
lab_14_bias_metodo.py
python
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
lab_14_bias_metodo.py
python
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

  1. 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.
  2. Nella prima cella porta IDEE a 500 e conta quante passano. Circa il 5%, come previsto. Ognuna di esse, mostrata da sola, sembrerebbe una scoperta.
  3. 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

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

Torna all’indice dei lab