Lab from the book · L16
L16 — Six textbook rules measured together, with the three checks
Lab 16 — Technical analysis, measured
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 "Technical analysis, measured". The chapter's six rules, all written in the same form, applied to the asset and period you choose. Without cherry-picking which ones to show after seeing the results. Then the three checks on the rule that wins: the shape of the surface, the yardstick of chance, and the correction for the number of attempts. Nothing in here is trading advice. It's a way of asking the question.
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
import matplotlib.pyplot as plt
import numpy as np
from cvbook import seed_for
from cvbook.dati import carica
from cvbook.metriche import drawdown_massimo
from cvbook.regole import CATALOGO, compra_e_tieni, esegui, rottura
SERIE = "btcusdt" # ← PROVA / TRY: "ethusdt" · "solusdt" (le tre preparate nel setup)
COSTO = 0.0012 # ← il tuo costo per operazione
# PROVA / TRY: raddoppialo (vedi esercizio 1)
prezzi = carica(SERIE).sort("data")["chiusura"].to_numpy()1. Six textbook rules, all together
The constraint that separates a measurement from a showcase: all of them are shown.
Output
btcusdt, costi 0.12% per operazione
regola netto lordo oper. dentro calo max
Forza relativa 30/70 0.50x 0.54x 71 47% -83.4%
Incrocio 50/200 4.33x 4.42x 18 50% -66.8%
Sopra la media 200 6.40x 6.91x 64 49% -64.2%
Incrocio 20/50 12.87x 14.03x 72 52% -74.9%
Momento a 12 mesi 14.17x 14.62x 26 59% -63.3%
Rottura a 20 giorni 35.66x 39.90x 94 52% -60.9%
compra e tieni 13.66x 13.68x 1 100% -83.2%Six horizontal bars, one per textbook rule, with the final capital per euro on a logarithmic scale from ten to the zero to ten to the one. From the top: relative strength 30/70 at 0.50 times, 50/200 crossover at 4.33, above the 200-day average at 6.40, 20/50 crossover at 12.87, twelve-month momentum at 14.17, twenty-day breakout at 35.66. A black dashed line at 13.7 times marks staying in the market throughout: two rules out of six clear it.
2 regole su 6 hanno battuto il non far niente.
Show the script for this step
riferimento = esegui(prezzi, compra_e_tieni(prezzi), costo=COSTO)
righe = []
for nome, regola in CATALOGO.items():
e = esegui(prezzi, regola(prezzi), costo=COSTO)
righe.append((nome, e["finale"], e["finale_lordo"], e["operazioni"],
e["esposizione"], drawdown_massimo(e["curva"])))
righe.sort(key=lambda x: x[1])
print(f"{SERIE}, costi {COSTO:.2%} per operazione\n")
print(f"{'regola':>22s} {'netto':>9s} {'lordo':>9s} {'oper.':>7s} "
f"{'dentro':>8s} {'calo max':>10s}")
for nome, netto, lordo, op, esp, dd in righe:
print(f"{nome:>22s} {netto:8.2f}x {lordo:8.2f}x {op:7.0f} {esp:8.0%} {dd:10.1%}")
print(f"{'compra e tieni':>22s} {riferimento['finale']:8.2f}x "
f"{riferimento['finale_lordo']:8.2f}x {riferimento['operazioni']:7.0f} "
f"{riferimento['esposizione']:8.0%} {drawdown_massimo(riferimento['curva']):10.1%}")
with avvio.figura("schermo"):
fig, ax = plt.subplots(figsize=(9, 4.5))
y = np.arange(len(righe))
ax.barh(y, [r[1] for r in righe])
ax.axvline(riferimento["finale"], linewidth=2, linestyle="--", color="black")
ax.annotate(f"compra e tieni: {riferimento['finale']:.1f}x",
xy=(riferimento["finale"], len(righe) - 0.4),
xytext=(6, 0), textcoords="offset points", va="center")
ax.set_yticks(y)
ax.set_yticklabels([r[0] for r in righe])
ax.set_xscale("log")
ax.set_xlabel("Capitale finale, per ogni euro investito (scala log)")
plt.show()
battono = sum(1 for r in righe if r[1] > riferimento["finale"])
print(f"\n{battono} regole su {len(righe)} hanno battuto il non far niente.")2. Costs decide the ranking
Look at the "net" and "gross" columns of the table. Some rules switch sides.
Output
regola 0.00% 0.06% 0.12% 0.25% 0.50%
Incrocio 50/200 4.42x 4.38x 4.33x 4.23x 4.04x
Incrocio 20/50 14.03x 13.44x 12.87x 11.71x 9.78x
Sopra la media 200 6.91x 6.65x 6.40x 5.88x 5.01x
Rottura a 20 giorni 39.90x 37.72x 35.66x 31.57x 24.96x
Forza relativa 30/70 0.54x 0.52x 0.50x 0.46x 0.38x
Momento a 12 mesi 14.62x 14.39x 14.17x 13.70x 12.84x
compra e tieni 13.68x 13.67x 13.66x 13.65x 13.61x
«Quale regola e' migliore» dipende da quanto paghi. Chi pubblica un backtest senza costi non ha mentito su nessun numero: ha omesso una voce, e l'omissione puo' invertire la conclusione.Show the script for this step
print(f"{'regola':>22s} " + "".join(f"{c:>11.2%}" for c in (0.0, 0.0006, 0.0012, 0.0025, 0.005)))
for nome, regola in CATALOGO.items():
valori = "".join(f"{esegui(prezzi, regola(prezzi), costo=c)['finale']:10.2f}x"
for c in (0.0, 0.0006, 0.0012, 0.0025, 0.005))
print(f"{nome:>22s} {valori}")
# Anche il compra-e-tieni ha un'operazione — il proprio ingresso — quindi la
# sua riga cambia con la colonna, di poco ma cambia. Stamparla costante era
# comodo e falso: diceva che il metro di confronto non paga i costi.
print(f"{'compra e tieni':>22s} " +
"".join(f"{esegui(prezzi, compra_e_tieni(prezzi), costo=c)['finale']:10.2f}x"
for c in (0.0, 0.0006, 0.0012, 0.0025, 0.005)))
print("\n«Quale regola e' migliore» dipende da quanto paghi. Chi pubblica un "
"backtest senza costi non ha mentito su nessun numero: ha omesso una voce, "
"e l'omissione puo' invertire la conclusione.")3. The first check: the shape of the surface
A wide plateau is compatible with a real phenomenon. An isolated peak almost never is.
A dotted line over the breakout window, from 5 to 120 days in steps of 5, with the final capital on a logarithmic scale between 2 and 40 times. The profile is jagged rather than a plateau: the median of the grid is 12.2 times, and 11 values out of 24 clear the black dashed line of staying in the market throughout.
Output
valori che battono il compra-e-tieni: 11 su 24 mediana della griglia: 12.21x
Show the script for this step
FINESTRE = np.arange(5, 121, 5) # PROVA / TRY: allarga o restringi il passo
griglia = np.array([esegui(prezzi, rottura(prezzi, int(f)), costo=COSTO)["finale"]
for f in FINESTRE])
with avvio.figura("schermo"):
fig, ax = plt.subplots()
ax.plot(FINESTRE, griglia, marker="o", linewidth=2)
ax.axhline(riferimento["finale"], linestyle="--", linewidth=1.5, color="black",
label="compra e tieni")
ax.set_yscale("log")
ax.set_xlabel("Finestra della rottura (giorni)")
ax.set_ylabel("Capitale finale (volte, scala log)")
ax.legend()
plt.show()
print(f"valori che battono il compra-e-tieni: {int((griglia > riferimento['finale']).sum())} "
f"su {len(griglia)}")
print(f"mediana della griglia: {np.median(griglia):.2f}x")4. The second check: the yardstick of chance
Histogram of the final capital of a thousand random positions, with a logarithmic horizontal axis from ten to the minus one to ten squared times and the count reaching 400. The mass is packed around 3.4 times, the median; a black vertical line marks the chosen rule at 35.7 times, and the title says it sits at the ninety-ninth percentile.
Output
la regola: 35.66x con 94 operazioni mediana delle casuali: 3.75x percentile: 98.4
Show the script for this step
N_CASUALI = 1000 # PROVA / TRY: 200 (veloce) · 1000 · 10000 (percentile più preciso)
SCELTA = 20
def posizione_casuale(n: int, n_operazioni: int, rng) -> np.ndarray:
pos = np.zeros(n)
punti = np.sort(rng.choice(n - 1, size=n_operazioni, replace=False))
stato, precedente = 0.0, 0
for i in punti:
pos[precedente:i] = stato
stato, precedente = 1.0 - stato, i
pos[precedente:] = stato
return pos
scelta = esegui(prezzi, rottura(prezzi, SCELTA), costo=COSTO)
n_op = int(scelta["operazioni"])
rng = np.random.default_rng(seed_for("tecnica-verifica"))
# NON TOCCARE / DO NOT CHANGE: è il seme della figura stampata nel capitolo.
# Con quello — stesse mille posizioni casuali, stesse operazioni, stessi costi
# — questa cella ridisegna l'istogramma del libro e stampa il suo percentile.
# Con un seme qualunque il quaderno resta corretto ma risponde 98,9 dove la
# pagina dice 98: due numeri per la stessa domanda, ed è la cosa che questo
# libro promette di non fare.
# This is the seed of the figure printed in the chapter.
casuali = np.array([
esegui(prezzi, posizione_casuale(len(prezzi), n_op, rng), costo=COSTO)["finale"]
for _ in range(N_CASUALI)
])
percentile = float((casuali < scelta["finale"]).mean() * 100)
with avvio.figura("schermo"):
fig, ax = plt.subplots()
ax.hist(casuali, bins=50)
ax.axvline(scelta["finale"], linewidth=2.5, color="black")
ax.set_xscale("log")
ax.set_xlabel("Capitale finale (volte, scala log)")
ax.set_ylabel(f"Su {N_CASUALI} posizioni casuali")
ax.set_title(f"La regola sta al {percentile:.0f}esimo percentile")
plt.show()
print(f"la regola: {scelta['finale']:.2f}x con {n_op} operazioni")
print(f"mediana delle casuali: {np.median(casuali):.2f}x")
print(f"percentile: {percentile:.1f}")5. The third check: how many attempts were behind it
The check almost nobody applies to their own numbers.
Output
regole provate: 6 valori del parametro provati: 24 tentativi complessivi (stima prudente): 30 probabilita' che UN tentativo raggiunga questo percentile per caso: 1.6% probabilita' che almeno uno su 30 lo faccia: 38.4% I tentativi non sono del tutto indipendenti — finestre vicine danno risultati simili — quindi questa stima e' pessimistica. Ma il punto resta: un percentile, dopo trenta tentativi, non e' piu' quel percentile.
Show the script for this step
regole_provate = len(CATALOGO)
parametri_provati = len(FINESTRE)
tentativi = regole_provate + parametri_provati
p_singolo = 1 - percentile / 100
print(f"regole provate: {regole_provate}")
print(f"valori del parametro provati: {parametri_provati}")
print(f"tentativi complessivi (stima prudente): {tentativi}\n")
print(f"probabilita' che UN tentativo raggiunga questo percentile per caso: {p_singolo:.1%}")
print(f"probabilita' che almeno uno su {tentativi} lo faccia: "
f"{1 - (1 - p_singolo) ** tentativi:.1%}")
print("\nI tentativi non sono del tutto indipendenti — finestre vicine danno "
"risultati simili — quindi questa stima e' pessimistica. Ma il punto resta: "
"un percentile, dopo trenta tentativi, non e' piu' quel percentile.")6. Other markets are not other trials
Output
btcusdt: regola 35.66x compra e tieni 13.66x rapporto 2.61 ethusdt: regola 48.88x compra e tieni 5.20x rapporto 9.40 solusdt: regola 187.25x compra e tieni 22.31x rapporto 8.39 Tre conferme? No. Il Lab 8 ha mostrato che questi tre mercati contengono poco piu' di UNA scommessa: una sola componente ne spiega quasi l'ottanta per cento dei movimenti. Un metodo che funziona sul fattore comune funziona su tutti e tre — non perche' sia stato confermato tre volte, ma perche' e' stato confermato una volta e conteggiato tre.
Show the script for this step
for altro in ("btcusdt", "ethusdt", "solusdt"):
p = carica(altro).sort("data")["chiusura"].to_numpy()
r = esegui(p, rottura(p, SCELTA), costo=COSTO)
b = esegui(p, compra_e_tieni(p), costo=COSTO)
print(f"{altro:>10s}: regola {r['finale']:8.2f}x compra e tieni {b['finale']:8.2f}x "
f"rapporto {r['finale'] / b['finale']:5.2f}")
print("\nTre conferme? No. Il Lab 8 ha mostrato che questi tre mercati contengono "
"poco piu' di UNA scommessa: una sola componente ne spiega quasi l'ottanta "
"per cento dei movimenti. Un metodo che funziona sul fattore comune "
"funziona su tutti e tre — non perche' sia stato confermato tre volte, ma "
"perche' e' stato confermato una volta e conteggiato tre.")Exercises
- Change
COSTOand rerun the second cell. Watch the ranking order change: it's the quickest demonstration that "which rule is better" isn't a property of the rule. - Add your own rule to the catalogue, following the pattern in
cvbook.regole, and put it through the same three checks. Remember to count it among the attempts. - The most instructive one: run everything on a non-digital market. Watch out for closed trading days, which change the count of periods. If the result repeats there, you have a real confirmation; if it doesn't, you've learned something more useful than a confirmation.
Reproducibility & downloads
Run on 2026-08-27 from the repository notebook
The notebook
lab_16_analisi_tecnica.ipynb16.0 KB
sha256 c5c04b6347235201a9d3604b3d10b308cce6b38a34b2820231300a52a3a24620
lab_16_analisi_tecnica.py12.0 KB
sha256 261e6881ae847ff1e37bb2a0b9178f4986fd34e071bd909be9c026e946ba9b61
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
ethusdt.parquet87.0 KB
sha256 c2bd0259da905e0fec87235d7a62295532433fb89657726dd2d19558db7c072a
Source: Binance Data Vision · Period: 2017-08-17 → 2026-06-30 · 3,240 rows · extracted 2026-08-16
solusdt.parquet57.5 KB
sha256 c7ba2368a3e419b898fb31ec6d5345b7212b74784b69079d3d43571c2ac63657
Source: Binance Data Vision · Period: 2020-08-11 → 2026-06-30 · 2,150 rows · extracted 2026-08-16