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

Lab del libro · L11

L11 — Quante osservazioni servono per distinguere un vantaggio dal caso

Lab 11 — Quante osservazioni servono davvero

Quaderno del capitolo «Quanto serve per sapere se sei bravo» di La matematica di chi perde.

Il conto con i tuoi numeri: quante operazioni servirebbero per stabilire che il vantaggio che pensi di avere non è rumore. E poi la simulazione che consiglio a tutti: venti strategie prive di qualunque vantaggio, testate tutte. In media una supera il test standard. Vederla passare, sapendo che dentro non c'è nulla, vale più di dieci pagine di spiegazioni.

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_11_potere.py
python
from math import ceil, erf, sqrt

import matplotlib.pyplot as plt
import numpy as np

from cvbook import seed_for
from cvbook.dati import carica
from cvbook.metriche import rendimenti

1. Il conto, con i tuoi numeri

Due soli ingredienti: quanto è grande il vantaggio che vuoi dimostrare e quanto oscillano i risultati attorno a esso. Il rapporto fra i due decide tutto. È lo stesso motivo per cui in una stanza silenziosa senti un sussurro e in discoteca devi urlare.

Output

vantaggio da dimostrare: 0.100% per operazione
oscillazione per operazione: 3.5%

operazioni necessarie: 7,574
a 250 operazioni l'anno: 30.3 anni
Espandi lo script di questo passo
lab_11_potere.py
python
VANTAGGIO = 0.001        # ← guadagno medio per operazione, al netto dei costi
                         # PROVA / TRY: il TUO vantaggio stimato (esercizio 1)
OSCILLAZIONE = 0.035     # ← deviazione standard del risultato per operazione
                         # PROVA / TRY: la TUA oscillazione (esercizio 1)
OPERAZIONI_ANNO = 250    # ← quante ne fai in un anno
POTENZA = 0.80           # ← probabilita' di accorgersene, se il vantaggio esiste
ALFA = 0.05              # ← rischio accettato di scambiare rumore per segnale


def quantile_normale(p: float) -> float:
    """Inversa della normale standard, per bisezione: nessuna dipendenza esterna."""
    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


def quante_servono(vantaggio: float, oscillazione: float,
                   potenza: float = POTENZA, alfa: float = ALFA) -> int:
    if vantaggio <= 0:
        raise ValueError("il vantaggio deve essere positivo")
    z_alfa = quantile_normale(1 - alfa)
    z_potenza = quantile_normale(potenza)
    return int(ceil(((z_alfa + z_potenza) * oscillazione / vantaggio) ** 2))


n = quante_servono(VANTAGGIO, OSCILLAZIONE)
print(f"vantaggio da dimostrare: {VANTAGGIO:.3%} per operazione")
print(f"oscillazione per operazione: {OSCILLAZIONE:.1%}\n")
print(f"operazioni necessarie: {n:,}")
print(f"a {OPERAZIONI_ANNO} operazioni l'anno: {n / OPERAZIONI_ANNO:,.1f} anni")

2. La tabella, e la riga che fa male

Output

oscillazione giornaliera misurata su Bitcoin: 3.5%

 vantaggio   operazioni     a 250/anno
     0.05%       30,836         123.3 anni
     0.10%        7,709          30.8 anni
     0.20%        1,928           7.7 anni
     0.50%          309           1.2 anni
     1.00%           78           0.3 anni

Una curva ripidamente discendente su scala logaritmica: in orizzontale il vantaggio medio per operazione, da 0 a 1,2%, in verticale quante operazioni servono per distinguerlo dal caso, da 100 a 100.000. Cinque punti la leggono: con un vantaggio dello 0,05% servono 30.836 operazioni, con lo 0,1% ne servono 7.709, con lo 0,5% 309, con l'1% 78. L'oscillazione giornaliera usata e' 3,5%.

Quante osservazioni servono per distinguere un vantaggio dal caso, al variare del vantaggio.Fonte: Binance Data Vision · Periodo: 2017-08-17 … 2026-06-30 · Metodo: Numero di osservazioni necessarie perche' la media campionaria stia a due errori standard dallo zero, data l'oscillazione giornaliera misurata sulla serie.
Un vantaggio dello 0,1% per operazione sarebbe un risultato eccellente — e i costi si mangiano gia' lo 0,12% a giro. Per dimostrarlo servono decenni di operativita' quotidiana.
Espandi lo script di questo passo
lab_11_potere.py
python
oscillazione_btc = float(np.std(rendimenti(
    carica("btcusdt").sort("data")["chiusura"].to_numpy()), ddof=1))
print(f"oscillazione giornaliera misurata su Bitcoin: {oscillazione_btc:.1%}\n")

vantaggi = [0.0005, 0.001, 0.002, 0.005, 0.010]
print(f"{'vantaggio':>10s} {'operazioni':>12s} {'a 250/anno':>14s}")
for v in vantaggi:
    q = quante_servono(v, oscillazione_btc)
    print(f"{v:10.2%} {q:12,d} {q / 250:13.1f} anni")

with avvio.figura("schermo"):
    fig, ax = plt.subplots()
    griglia = np.linspace(0.0003, 0.012, 200)
    ax.plot(griglia * 100, [quante_servono(v, oscillazione_btc) for v in griglia], linewidth=2)
    for v in vantaggi:
        ax.plot([v * 100], [quante_servono(v, oscillazione_btc)], marker="o")
    ax.set_yscale("log")
    ax.set_xlabel("Vantaggio medio per operazione (%)")
    ax.set_ylabel("Operazioni necessarie (scala log)")
    plt.show()

print("\nUn vantaggio dello 0,1% per operazione sarebbe un risultato eccellente — "
      "e i costi si mangiano gia' lo 0,12% a giro. Per dimostrarlo servono "
      "decenni di operativita' quotidiana.")

3. Quaranta operazioni non distinguono nulla da nulla

Con una moneta perfettamente equa, quante volte capita di ottenere 26 teste o più su 40 lanci? Il capitolo dice il 4%. Verifichiamolo invece di crederci.

Output

su 200,000 sequenze di 40 lanci di una moneta EQUA:
  26 vittorie o piu': 4.04% delle volte

E se hai provato piu' di una manciata di strategie prima di trovare questa, quel 4% te lo sei praticamente garantito.
Espandi lo script di questo passo
lab_11_potere.py
python
LANCI = 40
VITTORIE = 26
PROVE = 200_000  # PROVA / TRY: 20000 (veloce) · 200000 (percentuale più precisa)

rng = np.random.default_rng(seed_for("lab-potere-moneta"))
esiti = rng.binomial(LANCI, 0.5, PROVE)
quota = float((esiti >= VITTORIE).mean())

print(f"su {PROVE:,} sequenze di {LANCI} lanci di una moneta EQUA:")
print(f"  {VITTORIE} vittorie o piu': {quota:.2%} delle volte")
print(f"\nE se hai provato piu' di una manciata di strategie prima di trovare "
      f"questa, quel {quota:.0%} te lo sei praticamente garantito.")

4. Venti strategie senza alcun vantaggio, testate tutte

Output

 strategia   risultato medio   statistica t   supera il test?
         1           0.3061%           1.93                SI
         2           0.0795%           0.51                no
         3          -0.0285%          -0.18                no
         4           0.0263%           0.17                no
         5          -0.1789%          -1.17                no
         6           0.3351%           2.13                SI
         7           0.2047%           1.38                no
         8           0.1193%           0.77                no
         9           0.0500%           0.32                no
        10          -0.0574%          -0.39                no
        11           0.0728%           0.49                no
        12          -0.3965%          -2.60                no
        13           0.0611%           0.41                no
        14           0.3027%           1.95                SI
        15           0.0682%           0.44                no
        16          -0.0143%          -0.09                no
        17          -0.1687%          -1.04                no
        18          -0.2347%          -1.44                no
        19           0.2045%           1.34                no
        20           0.0957%           0.63                no

3 strategie su 20 hanno superato il test standard.
Dentro non c'era niente. Nessuna di esse aveva un vantaggio: era zero, messo li' da noi.
Espandi lo script di questo passo
lab_11_potere.py
python
STRATEGIE = 20        # PROVA / TRY: 200 (vedi esercizio 2)
OSSERVAZIONI = 500    # PROVA / TRY: 5000 (vedi esercizio 3)

rng = np.random.default_rng(seed_for("lab-potere-multipli"))
soglia = quantile_normale(1 - ALFA)

print(f"{'strategia':>10s} {'risultato medio':>17s} {'statistica t':>14s} {'supera il test?':>17s}")
passate = 0
for k in range(STRATEGIE):
    campione = rng.normal(0.0, OSCILLAZIONE, OSSERVAZIONI)  # vantaggio ESATTAMENTE zero
    t = campione.mean() / (campione.std(ddof=1) / np.sqrt(OSSERVAZIONI))
    supera = t > soglia
    passate += supera
    print(f"{k + 1:10d} {campione.mean():17.4%} {t:14.2f} {'SI' if supera else 'no':>17s}")

print(f"\n{passate} strategie su {STRATEGIE} hanno superato il test standard.")
print("Dentro non c'era niente. Nessuna di esse aveva un vantaggio: era zero, "
      "messo li' da noi.")

5. Il correttore per test multipli

Gli dici quanti tentativi hai fatto e ti restituisce la soglia che avresti dovuto usare. Applicalo ai tuoi risultati passati — con una certa cautela emotiva.

Output

 tentativi   soglia sulla statistica t   prob. che almeno uno passi
         1                        1.64                        5.0%
         5                        2.33                       22.6%
        20                        2.81                       64.2%
        50                        3.09                       92.3%
       100                        3.29                       99.4%
       500                        3.72                      100.0%

Con cento tentativi, trovare qualcosa che supera il test non corretto e' praticamente certo. Non e' un difetto del test: e' la sua definizione.
Espandi lo script di questo passo
lab_11_potere.py
python
def soglia_corretta(tentativi: int, alfa: float = ALFA) -> float:
    """Correzione conservativa: si divide il rischio accettato per i tentativi."""
    return quantile_normale(1 - alfa / tentativi)


print(f"{'tentativi':>10s} {'soglia sulla statistica t':>27s} "
      f"{'prob. che almeno uno passi':>28s}")
for tentativi in (1, 5, 20, 50, 100, 500):
    print(f"{tentativi:10d} {soglia_corretta(tentativi):27.2f} "
          f"{1 - (1 - ALFA) ** tentativi:27.1%}")

print("\nCon cento tentativi, trovare qualcosa che supera il test non corretto e' "
      "praticamente certo. Non e' un difetto del test: e' la sua definizione.")

Esercizi

  1. Nella prima cella metti il tuo vantaggio stimato e la tua oscillazione, calcolati sul tuo registro. Il numero che esce è il tuo orizzonte di verifica reale.
  2. Nella quarta cella porta STRATEGIE a 200. Quante passano? Circa il 5%, come previsto — e ognuna di esse, mostrata da sola, sembrerebbe una scoperta.
  3. Cambia OSSERVAZIONI da 500 a 5000 nella quarta cella. La quota di strategie che passa non cambia: più dati non proteggono dai test multipli. Solo il conteggio dei tentativi protegge.

Riproducibilità e download

Eseguito il 2026-08-27 dal quaderno del repository

Il quaderno

  • lab_11_potere.ipynb13,2 KB

    sha256 3f6fd59cc9f40cedd146e27a8e7906631c769f3a7ad55f6118894416333af93e

  • lab_11_potere.py9,9 KB

    sha256 be7a69c1c279dd60e131c06308a7a12e6e9718c4ba5185ed1c495bfb78735032

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

Torna all’indice dei lab