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
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 rendimenti1. 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
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 anniA 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%.
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
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
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
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
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
- 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.
- In the fourth cell raise
STRATEGIEto 200. How many pass? About 5%, as predicted — and each one, shown on its own, would look like a discovery. - Change
OSSERVAZIONIfrom 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