Lab from the book · L01
L01 — How many retail accounts close at a loss, and how long it takes
Lab 1 — Who really loses, and how much
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 "Who really loses, and how much". The chapter shows three coordinates without which a result means nothing: on what, over which period, at what cost. Here you watch them move. The book's figure says that, on this asset and in this period, the moment you started matters more than anything else. The notebook redoes that calculation and — above all — takes it apart, exactly as the chapter does: the sample is a single market in a lucky period, and that has to be looked at directly, not hidden. Run the cells top to bottom. The first takes about twenty seconds, the rest are immediate.
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.
Output
serie: Binance Data Vision, estratta il 2026-08-16 periodo: 2017-08-17 → 2026-06-30 (3240 giorni)
Show the script for this step
import matplotlib.pyplot as plt
import numpy as np
from cvbook.dati import carica, citazione
from cvbook.metriche import drawdown_massimo
df = carica("btcusdt").sort("data") # ← PROVA / TRY: "ethusdt" · "solusdt" (vedi esercizio 3)
prezzi = df["chiusura"].to_numpy()
date = df["data"].to_list()
fonte, estratto = citazione("btcusdt")
print(f"serie: {fonte}, estratta il {estratto}")
print(f"periodo: {date[0]} → {date[-1]} ({len(prezzi)} giorni)")1. How many entries end at a loss, by horizon
For every day in the history we ask: if someone had entered that day and exited after N days, how would it have gone? Then we count the share of entries that closed at a loss. It's the calculation almost nobody makes, because it requires looking at every entry day, not just the convenient one.
Output
1 mese: 46.4% degli ingressi in perdita (3210 ingressi) 3 mesi: 45.5% degli ingressi in perdita (3150 ingressi) 1 anno: 33.6% degli ingressi in perdita (2875 ingressi) 2 anni: 24.3% degli ingressi in perdita (2510 ingressi) 4 anni: 0.0% degli ingressi in perdita (1780 ingressi)
Show the script for this step
ORIZZONTI = [(30, "1 mese"), (90, "3 mesi"), (365, "1 anno"), (730, "2 anni"), (1460, "4 anni")]
# PROVA / TRY: aggiungi o cambia un orizzonte, es. (180, "6 mesi")
def quota_in_perdita(p: np.ndarray, giorni: int) -> float:
"""Frazione di giorni d'ingresso che, dopo `giorni`, si trova sotto zero."""
if giorni >= len(p):
return float("nan")
esiti = p[giorni:] / p[:-giorni] - 1.0
return float((esiti < 0).mean())
for giorni, etichetta in ORIZZONTI:
q = quota_in_perdita(prezzi, giorni)
print(f"{etichetta:>8s}: {q:6.1%} degli ingressi in perdita ({len(prezzi) - giorni} ingressi)")2. The first limit: observations overlap
The numbers just printed look like they're based on thousands of cases. They're not. 2,875 twelve-month entries over nine years of history are not 2,875 independent experiments: they're nine years seen from 2,875 angles that almost all overlap with each other. The honest count of independent occasions is closer to this.
Output
1 mese: 3210 righe nel file, ma circa 108 periodi davvero distinti 3 mesi: 3150 righe nel file, ma circa 36 periodi davvero distinti 1 anno: 2875 righe nel file, ma circa 8 periodi davvero distinti 2 anni: 2510 righe nel file, ma circa 4 periodi davvero distinti 4 anni: 1780 righe nel file, ma circa 2 periodi davvero distinti
Show the script for this step
for giorni, etichetta in ORIZZONTI:
sovrapposti = len(prezzi) - giorni
indipendenti = len(prezzi) // giorni
print(
f"{etichetta:>8s}: {sovrapposti:5d} righe nel file, "
f"ma circa {indipendenti:3d} periodi davvero distinti"
)Look at the right-hand column. At four years the independent observations are two. Not a number to build any conclusion on.
3. The second limit: the period is lucky
Change the window and watch what happens to the numbers. The chapter says it explicitly: the four-year column doesn't prove that you don't lose at four years — it proves that in this window it didn't happen.
Output
finestra scelta: 2017-08-17 → 2026-06-30 (3240 giorni) risultato del compra-e-tieni: 13.68x calo massimo attraversato: -83.2% 1 mese: 46.4% in perdita 3 mesi: 45.5% in perdita 1 anno: 33.6% in perdita 2 anni: 24.3% in perdita 4 anni: 0.0% in perdita
Show the script for this step
INIZIO, FINE = "2017-08-17", "2026-06-30"
# PROVA / TRY: FINE = "2022-12-31" · INIZIO = "2021-01-01" (vedi esercizi 1 e 2)
import datetime as dt
maschera = [
dt.date.fromisoformat(INIZIO) <= d <= dt.date.fromisoformat(FINE) for d in date
]
sotto = prezzi[np.array(maschera)]
print(f"finestra scelta: {INIZIO} → {FINE} ({len(sotto)} giorni)")
print(f"risultato del compra-e-tieni: {sotto[-1] / sotto[0]:.2f}x")
print(f"calo massimo attraversato: {drawdown_massimo(sotto):.1%}\n")
for giorni, etichetta in ORIZZONTI:
q = quota_in_perdita(sotto, giorni)
print(f"{etichetta:>8s}: {q:6.1%} in perdita")Exercises
- Set
FINE = "2022-12-31"and rerun the cell. The four-year column stops being zero. No data changed: the window did. - Set
INIZIO = "2021-01-01". Buy-and-hold drops a lot. It's the chapter that says ten months of difference on entry were worth 129 percentage points. - Change
"btcusdt"to"ethusdt"or"solusdt"in the setup cell and in the loading one. Does the conclusion hold? On which horizons?
Takeaway. A result is not a number: it's a number with its period, its sample, and its effective size. If any of these three is missing, the correct answer to "so does it work?" is: I don't know, and neither do you.
Reproducibility & downloads
Run on 2026-08-27 from the repository notebook
The notebook
lab_01_chi_perde.ipynb11.4 KB
sha256 81f4f7a05bee17ca23a90d288c4ea9725f5e7c9ce0d7cced0aa12699d5193129
lab_01_chi_perde.py8.7 KB
sha256 ab9ac092c4b51303cc9e8d3cef8720bec70b4fe09224d5cf7ccf4b2d5ce106e1
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