Skip to content
SIAT PAPER 2026Open the research page
Cryptoverso

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
lab_01_chi_perde.py
python
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
lab_01_chi_perde.py
python
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
lab_01_chi_perde.py
python
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
lab_01_chi_perde.py
python
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

  1. Set FINE = "2022-12-31" and rerun the cell. The four-year column stops being zero. No data changed: the window did.
  2. 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.
  3. 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

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