Skip to content
SIAT PAPER 2026Open the research page
Cryptoverso

Lab from the book · L11

L11 — How many observations it takes to tell an edge from chance

Lab 11 — How many observations you really need

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 "How much it takes to know if you're good". The calculation with your own numbers: how many trades it would take to establish that the edge you think you have isn't noise. Then the simulation I recommend to everyone: twenty strategies with absolutely no edge, all tested. On average one passes the standard test. Watching it pass, knowing there's nothing inside, is worth more than ten pages of explanation.

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
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. The calculation, with your own numbers

Just two ingredients: how big the edge is you want to prove, and how much the results swing around it. Their ratio decides everything. It's the same reason you hear a whisper in a silent room and have to shout in a nightclub.

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
Show the script for this step
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. The table, and the row that hurts

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

A steeply falling curve on a logarithmic scale: the average edge per trade runs along the horizontal axis, from 0 to 1.2%, and the number of trades needed to tell it apart from chance up the side, from 100 to 100,000. Five points read it off: an edge of 0.05% needs 30,836 trades, 0.1% needs 7,709, 0.5% needs 309, 1% needs 78. The daily swing used is 3.5%.

How many observations it takes to tell an edge from chance, as the edge changes.Source: Binance Data Vision · Period: 2017-08-17 … 2026-06-30 · Method: Number of observations needed for the sample mean to sit two standard errors away from zero, given the daily swing measured on the series.
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.
Show the script for this step
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. Forty trades don't tell anything apart from nothing

With a perfectly fair coin, how often do you get 26 heads or more out of 40 flips? The chapter says 4%. Let's verify it instead of taking it on faith.

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.
Show the script for this step
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. Twenty strategies with no edge at all, all tested

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.
Show the script for this step
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. The multiple-testing corrector

Tell it how many attempts you made and it gives back the threshold you should have used. Apply it to your past results — with some emotional caution.

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.
Show the script for this step
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.")

Exercises

  1. In the first cell, enter your own estimated edge and your own swing, computed from your log. The resulting number is your real verification horizon.
  2. In the fourth cell raise STRATEGIE to 200. How many pass? About 5%, as predicted — and each one, shown on its own, would look like a discovery.
  3. Change OSSERVAZIONI from 500 to 5000 in the fourth cell. The share of strategies that pass doesn't change: more data doesn't protect against multiple testing. Only counting the attempts does.

Reproducibility & downloads

Run on 2026-08-27 from the repository notebook

The notebook

  • lab_11_potere.ipynb13.2 KB

    sha256 3f6fd59cc9f40cedd146e27a8e7906631c769f3a7ad55f6118894416333af93e

  • lab_11_potere.py9.9 KB

    sha256 be7a69c1c279dd60e131c06308a7a12e6e9718c4ba5185ed1c495bfb78735032

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

Back to the lab index