Skip to content
SIAT PAPER 2026Open the research page
Cryptoverso

Lab from the book · L06

L06 — Fat tails, kurtosis and the twenty days that decide everything

Lab 6 — The lying average

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 "The lying average". Three things, on real data: how wrong the bell curve is on the tails, how few days decide the result, and why the two opposite slogans drawn from that figure are the same statement — and neither is advice.

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_06_code_grasse.py
python
import matplotlib.pyplot as plt
import numpy as np

from cvbook.dati import carica
from cvbook.metriche import rendimenti

SERIE = "btcusdt"  # ← PROVA / TRY: "ethusdt" · "solusdt" (le tre preparate nel setup)

df = carica(SERIE).sort("data")
prezzi = df["chiusura"].to_numpy()
date = df["data"].to_list()
r = rendimenti(prezzi)
mu, sigma = float(np.mean(r)), float(np.std(r, ddof=1))

1. How many extreme days the bell curve predicts, and how many there really are

If returns followed the bell curve, the number of days beyond a certain distance from the mean would be computable. Let's compare it with the real count.

Output

btcusdt: 3239 giorni  ·  media 0.1439%  ·  deviazione standard 3.531%

   oltre   previsti dalla campana    osservati   rapporto
     2σ                  147.375          196        1.3x
     3σ                    8.745           54        6.2x
     4σ                    0.205           18       87.7x
     5σ                    0.002            5     2692.6x
     6σ                    0.000            2   312934.5x

giorno peggiore: -39.5%  →  11.2 deviazioni standard dalla media
Secondo il modello a campana un evento del genere non dovrebbe accadere nemmeno una volta nella storia dell'universo. E' successo, in nove anni.

curtosi: 11.6   (per la curva a campana vale 3)
Show the script for this step
lab_06_code_grasse.py
python
from math import erfc, sqrt

n = len(r)
print(f"{SERIE}: {n} giorni  ·  media {mu:.4%}  ·  deviazione standard {sigma:.3%}\n")
print(f"{'oltre':>8s} {'previsti dalla campana':>24s} {'osservati':>12s} {'rapporto':>10s}")
for k in (2, 3, 4, 5, 6):
    previsti = n * erfc(k / sqrt(2))
    osservati = int(np.sum(np.abs(r - mu) > k * sigma))
    rapporto = osservati / previsti if previsti > 0 else float("inf")
    print(f"{k:>6d}σ {previsti:24.3f} {osservati:12d} {rapporto:10.1f}x")

peggiore = float(np.min(r))
distanza = abs(peggiore - mu) / sigma
print(f"\ngiorno peggiore: {peggiore:.1%}{distanza:.1f} deviazioni standard dalla media")
print("Secondo il modello a campana un evento del genere non dovrebbe accadere "
      "nemmeno una volta nella storia dell'universo. E' successo, in nove anni.")

curtosi = float(np.mean(((r - mu) / sigma) ** 4))
print(f"\ncurtosi: {curtosi:.1f}   (per la curva a campana vale 3)")

2. The shape, drawn

The vertical scale is logarithmic: without it, the difference on the tails — the only part that matters — would be invisible.

A histogram of the daily changes of btcusdt over 200 bins, with the horizontal axis running from minus 0.4 to 0.2 and the day count on a logarithmic scale spanning almost thirty orders of magnitude, from ten to the minus twenty-seven up to ten. Above the histogram runs the bell curve with the same mean and the same standard deviation: in the centre the two agree, while at the edges the bell collapses by tens of orders of magnitude exactly where the days that actually happened are.

What actually happened and what the bell curve predicted, on the same scale.Source: Binance Data Vision · Period: 2017-08-17 … 2026-06-30 · Method: Changes between consecutive closes in 200 bins, with the normal density of equal mean and standard deviation rescaled to the bin width drawn on top; count on a logarithmic scale.
Show the script for this step
lab_06_code_grasse.py
python
from math import exp, pi

griglia = np.linspace(r.min(), r.max(), 400)
campana = n * np.exp(-((griglia - mu) ** 2) / (2 * sigma**2)) / (sigma * np.sqrt(2 * pi))
larghezza = griglia[1] - griglia[0]

with avvio.figura("schermo"):
    fig, ax = plt.subplots()
    ax.hist(r, bins=200, label="quello che e' successo")
    ax.plot(griglia, campana * larghezza, linewidth=2, label="quello che prevede la campana")
    ax.set_yscale("log")
    ax.set_xlabel("Variazione giornaliera")
    ax.set_ylabel("Numero di giorni (scala log)")
    ax.legend()
    plt.show()

3. Twenty days out of 3,200

We remove the best days from the series, then the worst, and look at what's left.

Output

tutti i 3239 giorni:                13.68x

   quanti giorni tolti   togliendo i peggiori   togliendo i migliori
                     1                 22.62x                 11.17x
                     5                 48.43x                  5.87x
                    10                102.96x                  2.92x
                    20                381.21x                  0.90x
                    50               8024.89x                  0.04x
Show the script for this step
lab_06_code_grasse.py
python
ordine = np.argsort(r)


def senza(indici_da_togliere: np.ndarray) -> float:
    maschera = np.ones(len(r), dtype=bool)
    maschera[indici_da_togliere] = False
    return float(np.prod(1 + r[maschera]))


base = float(np.prod(1 + r))
print(f"tutti i {len(r)} giorni:            {base:9.2f}x\n")
print(f"{'quanti giorni tolti':>22s} {'togliendo i peggiori':>22s} {'togliendo i migliori':>22s}")
for quanti in (1, 5, 10, 20, 50):  # PROVA / TRY: aggiungi 100 (vedi esercizio 2)
    peggiori = senza(ordine[:quanti])
    migliori = senza(ordine[-quanti:])
    print(f"{quanti:>22d} {peggiori:21.2f}x {migliori:21.2f}x")

4. But they touch

The piece almost every popular book leaves out: the best and the worst days touch each other. Look at March 2020 in the table below — the 12th is the worst of the whole series, the 13th is among the best, and so is the 19th: three of the twenty days that decide nine years, in the same week. The last line measures the general case, and says something more sober: the median distance between a best day and the nearest worst one is two weeks, not one.

Output

i dieci giorni peggiori e i dieci migliori, in ordine di calendario:

  2017-09-14   -19.2%  PEGGIORE
  2017-09-15   +16.0%  migliore
  2017-12-06   +15.8%  migliore
  2017-12-07   +22.5%  migliore
  2017-12-22   -14.0%  PEGGIORE
  2017-12-26   +14.5%  migliore
  2017-12-30   -13.5%  PEGGIORE
  2018-01-16   -19.5%  PEGGIORE
  2018-02-05   -15.2%  PEGGIORE
  2019-04-02   +17.2%  migliore
  2019-10-25   +16.8%  migliore
  2020-03-12   -39.5%  PEGGIORE
  2020-03-13   +16.2%  migliore
  2020-03-19   +14.3%  migliore
  2021-02-08   +19.5%  migliore
  2021-05-19   -14.4%  PEGGIORE
  2022-02-28   +14.5%  migliore
  2022-06-13   -15.4%  PEGGIORE
  2022-11-09   -14.1%  PEGGIORE
  2026-02-05   -14.0%  PEGGIORE

distanza mediana fra un giorno migliore e il peggiore piu' vicino: 15 giorni
Show the script for this step
lab_06_code_grasse.py
python
peggiori_10 = np.sort(ordine[:10])
migliori_10 = np.sort(ordine[-10:])

print("i dieci giorni peggiori e i dieci migliori, in ordine di calendario:\n")
righe = sorted(
    [(date[i + 1], r[i], "PEGGIORE") for i in peggiori_10]
    + [(date[i + 1], r[i], "migliore") for i in migliori_10]
)
for giorno, variazione, tipo in righe:
    print(f"  {giorno}  {variazione:+7.1%}  {tipo}")

distanze = [
    min(abs(int(m) - int(p)) for p in peggiori_10) for m in migliori_10
]
print(f"\ndistanza mediana fra un giorno migliore e il peggiore piu' vicino: "
      f"{int(np.median(distanze))} giorni")

Exercises

  1. Change SERIE to "ethusdt" or "solusdt". Kurtosis stays well above 3 and the concentration of the result in few days remains: it is not a quirk of one asset, it's a property of markets.
  2. In the third cell try removing 100 days. What's left no longer resembles anything real: that's why "avoid the worst days" isn't advice but a description of a world that doesn't exist.
  3. Look at the last table. Try imagining a rule that exits before every worst day and re-enters before every best day: the median distance just printed tells you how much time you'd have to notice.

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

  • 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

Back to the lab index