Skip to content
SIAT PAPER 2026Open the research page
Cryptoverso

Lab from the book · L03

L03 — Token mortality and survivorship bias

Lab 3 — The graveyard of tokens

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 graveyard of tokens". This notebook contains the real prices of the two dead assets the chapter talks about — one that went to zero in nine days, one whose market was shut down. Keeping that data is harder than it sounds: most convenient sources expose only what's still active. The notebook's value isn't the timeline of the crash. It's the final exercise: rebuilding the same basket looking only at the survivors, and seeing what reassuring answer you get.

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_03_cimitero_token.py
python
import datetime as dt

import matplotlib.pyplot as plt
import numpy as np
import polars as pl

from cvbook.dati import DISCONTINUITA, carica, carica_strumento
from cvbook.metriche import recupero_necessario

1. Nine days

Logarithmic scale: each tick is a factor of ten. On a linear scale this chart would be a vertical line followed by a flat line — and that's exactly how crashes look to whoever watches them happen. The series stops on 13 May 2022: from the 31st the same ticker quotes a different token. Drawing the whole series shows a resurrection that nobody lived.

The closing price of lunausdt in USDT on a logarithmic scale, from 1 April to 13 May 2022, with the vertical axis spanning six orders of magnitude from ten to the minus four up to ten squared. The line starts a little above 100, drifts down towards 80 through April, then falls almost vertically over nine days to five hundred-thousandths and ends there.

Nine days of a collapse on a logarithmic scale: every tick is a factor of ten. The series stops on 13 May, where that ticker stops quoting the same token.Source: Binance Data Vision · Period: 2017-08-17 … 2026-06-30 · Method: Daily closes from 1 April 2022 up to the discontinuity declared in the data registry, on a logarithmic scale.
Show the script for this step
lab_03_cimitero_token.py
python
luna = carica_strumento("lunausdt").sort("data").filter(pl.col("data") >= dt.date(2022, 4, 1))

with avvio.figura("schermo"):
    fig, ax = plt.subplots()
    ax.semilogy(luna["data"].to_list(), luna["chiusura"].to_numpy(), linewidth=1.6)
    ax.set_ylabel("Prezzo di chiusura (USDT, scala log)")
    fig.autofmt_xdate()
    plt.show()

Try it: the chart that says the opposite

Run the cell below. Same series, read raw — reused ticker included — through 30 June.

The same series read raw: the closing price of lunausdt in USDT on a logarithmic scale, from 1 April to 30 June 2022, drawn as a dashed line on the same vertical axis from ten to the minus four up to ten squared. A dotted vertical line marks 13 May. Left of that line is the collapse down to five hundred-thousandths; right of it the line climbs back roughly a hundred thousand times and drifts between 2 and 9 until the end of June.

The chart that says the opposite: without the cut, the same ticker draws a resurrection nobody lived through.Source: Binance Data Vision · Period: 2017-08-17 … 2026-06-30 · Method: Daily closes from 1 April to 30 June 2022 read without applying the discontinuity, with the date of the instrument change marked by a vertical line.

Output

2022-05-05      82.35000 USDT   perdita  0.000%   servirebbe +0%
2022-05-06      77.30000 USDT   perdita  6.132%   servirebbe +7%
2022-05-07      68.13000 USDT   perdita 17.268%   servirebbe +21%
2022-05-08      64.26000 USDT   perdita 21.967%   servirebbe +28%
2022-05-09      30.29000 USDT   perdita 63.218%   servirebbe +172%
2022-05-10      17.46000 USDT   perdita 78.798%   servirebbe +372%
2022-05-11       1.07690 USDT   perdita 98.692%   servirebbe +7,547%
2022-05-12       0.00032 USDT   perdita 100.000%   servirebbe +25,734,275%
2022-05-13       0.00005 USDT   perdita 100.000%   servirebbe +99,999,900%
Show the script for this step
lab_03_cimitero_token.py
python
grezza = carica("lunausdt").sort("data").filter(
    pl.col("data").is_between(dt.date(2022, 4, 1), dt.date(2022, 6, 30))
)

with avvio.figura("schermo"):
    fig, ax = plt.subplots()
    ax.semilogy(grezza["data"].to_list(), grezza["chiusura"].to_numpy(),
                linewidth=1.6, linestyle="--")
    ax.axvline(DISCONTINUITA["lunausdt"], linewidth=1.0, linestyle=":")
    ax.set_ylabel("Prezzo di chiusura (USDT, scala log)")
    ax.set_title("La stessa serie letta grezza: da qui in poi è un altro token")
    fig.autofmt_xdate()
    plt.show()

righe = luna.filter(pl.col("data").is_between(dt.date(2022, 5, 5), dt.date(2022, 5, 13)))
partenza = float(righe["chiusura"][0])
for d, p in zip(righe["data"].to_list(), righe["chiusura"].to_numpy()):
    perdita = 1 - p / partenza
    recupero = recupero_necessario(min(perdita, 0.999999))
    print(f"{d}  {p:12.5f} USDT   perdita {perdita:7.3%}   servirebbe +{recupero:,.0%}")

Look at the second-to-last row: the loss was already 98.7%. The next day the price divided again by three thousand. Even after losing 98%, there was still everything left to lose.

2. When the price stops existing

The second way of disappearing is worse, because it doesn't even leave a price. Note where the line ends: it doesn't reach zero, it breaks off.

The closing price of fttusdt in USDT from 1 October to 15 November 2022, with the vertical axis running from 5 to 25. The line, dotted with one marker per day, hovers around 25 until early November, then drops and stops: the last marker is 15 November at 1.43, and after that date there is no price at all.

The second way of disappearing: the line does not reach zero, it stops.Source: Binance Data Vision · Period: 2017-08-17 … 2026-06-30 · Method: Daily closes up to the last day on which the series has a price; after that date the data does not exist, and the line ends there.

Output

ultimo giorno con un prezzo: 2022-11-15  (1.43 USDT)
dopo quella data non esiste piu' un mercato: non c'e' un prezzo, non c'e' una quotazione, non c'e' modo di vendere.
Show the script for this step
lab_03_cimitero_token.py
python
ftt = carica("fttusdt").sort("data").filter(pl.col("data") >= dt.date(2022, 10, 1))

with avvio.figura("schermo"):
    fig, ax = plt.subplots()
    ax.plot(ftt["data"].to_list(), ftt["chiusura"].to_numpy(), linewidth=1.6, marker="o",
            markersize=2.5)
    ax.set_ylabel("Prezzo di chiusura (USDT)")
    fig.autofmt_xdate()
    plt.show()

print(f"ultimo giorno con un prezzo: {ftt['data'][-1]}  ({float(ftt['chiusura'][-1]):.2f} USDT)")
print("dopo quella data non esiste piu' un mercato: non c'e' un prezzo, non c'e' "
      "una quotazione, non c'e' modo di vendere.")

3. The basket, with and without the dead

An equal-weight basket built on April 1, 2021. We measure it two ways: looking only at what still exists today, and looking at everything that was actually bought.

Two capital curves rebased to 100 from 1 April 2021 to 31 December 2022, with the vertical axis graduated up to 500 and a dotted horizontal line at 100. Both rise to a peak on 6 November 2021 — 564 for the survivors-only curve, 424 for the dashed one that keeps the dead tokens inside — and then fall through the whole of 2022, below where they started: the first ends at 47.1, the second at 29.0, 38.5% lower.

The same basket measured twice: with what still exists, and with everything that was actually held.Source: Binance Data Vision · Period: 2017-08-17 … 2026-06-30 · Method: Equally weighted basket built on 1 April 2021 and held to 31 December 2022, computed once on the survivors alone and once on every series, dead ones included.

Output

paniere dei soli sopravvissuti: 47.1
paniere reale, morti compresi:  29.0
differenza:                     -38.5%
Show the script for this step
lab_03_cimitero_token.py
python
INIZIO = dt.date(2021, 4, 1)  # ← PROVA / TRY: sposta a gennaio 2022 (vedi esercizio 2)
FINE = dt.date(2022, 12, 31)  # ← PROVA / TRY: porta a oggi e guarda cosa cambia
VIVI = ["btcusdt", "ethusdt", "solusdt"]  # ← PROVA / TRY: togli "solusdt" (esercizio 1)
MORTI = ["lunausdt", "fttusdt"]
# NON TOCCARE / DO NOT CHANGE: MORTI deve restare com'è — sono gli unici due
# token defunti congelati nel registro dati; la dimostrazione è proprio che
# le fonti comode non li avrebbero mai lasciati scegliere (vedi esercizio 3)
# MORTI must stay as it is — these are the only two dead tokens frozen in the
# data registry; the whole demonstration is that convenient sources would
# never have let you choose them (see exercise 3)


def curva_paniere(nomi: list[str]) -> tuple[list, np.ndarray]:
    """Paniere a peso uguale, ribilanciato una sola volta all'inizio.

    Due scelte, e vanno dette perché sono le stesse della figura stampata nel
    libro — altrimenti questo quaderno risponderebbe a una domanda diversa da
    quella del capitolo, che è esattamente ciò che non deve succedere.

    **Un asset delistato non sparisce: resta all'ultimo valore noto.** È quanto
    vale davvero per chi ce l'ha in portafoglio, e non è zero: FTT il 15 novembre
    2022 valeva ancora 1,43 dollari, semplicemente non si poteva più vendere.

    **Le serie si leggono con `carica_strumento`**, che si ferma dove
    l'identificativo cambia strumento. Con la serie grezza LUNA risaliva al 6,8%
    del valore iniziale e il paniere dei morti chiudeva a 30,3 invece che a 29,0:
    questo quaderno avrebbe sottostimato del 7% proprio l'errore che esiste per
    misurare.
    """
    calendario = [
        d for d in carica("btcusdt").sort("data")["data"].to_list()
        if INIZIO <= d <= FINE
    ]
    quote = []
    for n in nomi:
        s = carica_strumento(n).sort("data").filter(
            pl.col("data").is_between(INIZIO, FINE)
        )
        mappa = dict(zip(s["data"].to_list(), s["chiusura"].to_numpy()))
        base = mappa[min(mappa)]
        ultimo, riempita = base, []
        for d in calendario:
            ultimo = mappa.get(d, ultimo)
            riempita.append(ultimo / base)
        quote.append(riempita)
    return calendario, np.mean(np.array(quote), axis=0)


date_v, solo_vivi = curva_paniere(VIVI)
date_t, tutti = curva_paniere(VIVI + MORTI)

with avvio.figura("schermo"):
    fig, ax = plt.subplots()
    ax.plot(date_v, solo_vivi * 100, linewidth=1.8, label="solo i sopravvissuti")
    ax.plot(date_t, tutti * 100, linewidth=1.8, linestyle="--",
            label="tutti, morti compresi")
    ax.axhline(100, linestyle=":", linewidth=0.9)
    ax.set_ylabel("Capitale (base 100)")
    ax.legend()
    fig.autofmt_xdate()
    plt.show()

print(f"paniere dei soli sopravvissuti: {solo_vivi[-1] * 100:.1f}")
print(f"paniere reale, morti compresi:  {tutti[-1] * 100:.1f}")
print(f"differenza:                     {tutti[-1] / solo_vivi[-1] - 1:+.1%}")

Neither number is wrong: they are answers to two different questions. The first is "how did these three assets do". The second is "how would it have gone for me". Almost everyone asks the first and believes they've answered the second.

Exercises.

  1. Remove "solusdt" from VIVI and rerun: the distance between the two curves changes quite a bit. How much does the conclusion depend on which names you included?
  2. Move INIZIO to January 2022. The real basket does much worse than the survivors-only one: the distortion grows when the period contains more deaths.
  3. The exercise worth the whole chapter: try rebuilding this same figure using any free source of historical data. You'll find the two dead assets aren't there, and you'll get the reassuring curve without even noticing you made a mistake.

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

  • lunausdt.parquet27.6 KB

    sha256 10fe10357f76eb408550f4809ce2a87cb1129164f6f6d074ae7eac730ccb7f15

    Source: Binance Data Vision · Period: 2020-08-21 → 2022-12-31 · 846 rows · extracted 2026-08-16

  • fttusdt.parquet27.5 KB

    sha256 7b235709ebb5ae31df0a9c315bd134b95e5249196a6c84b54763d9e63795fc06

    Source: Binance Data Vision · Period: 2019-12-20 → 2022-11-15 · 1,062 rows · extracted 2026-08-16

Back to the lab index