Skip to content
SIAT PAPER 2026Open the research page
Cryptoverso

Lab from the book · L19

L19 — The five tests to run on your own working environment

Lab 19 — The five tests for your tool

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 toolbox". This notebook does not teach you Python. It shows you in thirty seconds what it means, in practice, to have the chapter's five capabilities — so you can compare them with what your current tool lets you do, instead of trusting my comparison. Five cells, one per test. Run them and then ask yourself, for each: how long would this take me with the tool I use?

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_19_strumenti.py
python
import hashlib
import time

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

from cvbook import seed_for
from cvbook.dati import carica, carica_strumento, leggi_registro
from cvbook.metriche import cagr, drawdown_massimo, rendimenti, sharpe, volatilita
from cvbook.simulazioni import bootstrap_traiettorie

ASSET = ["btcusdt", "ethusdt", "solusdt", "lunausdt", "fttusdt"]
# PROVA / TRY: aggiungi "ftsemib" · "eni" · "enel" · "intesa" · "generali" ·
# "eurusd" (aggiungili anche a avvio.prepara([...]))

Test 1 — The same metric on every asset, in one table

Not five charts to look at one by one: one sortable table. If your tool can't do this, the chapter on the graveyard of tokens is a chapter you couldn't have written.

Output

shape: (5, 9)
┌──────────┬────────┬────────────┬────────────┬───┬─────────┬────────────┬──────────────┬────────┐
│ asset    ┆ giorni ┆ dal        ┆ al         ┆ … ┆ cagr    ┆ volatilita ┆ calo_massimo ┆ sharpe │
│ ---      ┆ ---    ┆ ---        ┆ ---        ┆   ┆ ---     ┆ ---        ┆ ---          ┆ ---    │
│ str      ┆ i64    ┆ str        ┆ str        ┆   ┆ f64     ┆ f64        ┆ f64          ┆ f64    │
╞══════════╪════════╪════════════╪════════════╪═══╪═════════╪════════════╪══════════════╪════════╡
│ lunausdt ┆ 631    ┆ 2020-08-21 ┆ 2022-05-13 ┆ … ┆ -0.9948 ┆ 2.222      ┆ -1.0         ┆ 0.98   │
│ fttusdt  ┆ 1062   ┆ 2019-12-20 ┆ 2022-11-15 ┆ … ┆ -0.1311 ┆ 1.155      ┆ -0.982       ┆ 0.59   │
│ solusdt  ┆ 2150   ┆ 2020-08-11 ┆ 2026-06-30 ┆ … ┆ 0.6948  ┆ 1.162      ┆ -0.963       ┆ 1.03   │
│ ethusdt  ┆ 3240   ┆ 2017-08-17 ┆ 2026-06-30 ┆ … ┆ 0.2043  ┆ 0.87       ┆ -0.94        ┆ 0.65   │
│ btcusdt  ┆ 3240   ┆ 2017-08-17 ┆ 2026-06-30 ┆ … ┆ 0.3429  ┆ 0.675      ┆ -0.832       ┆ 0.78   │
└──────────┴────────┴────────────┴────────────┴───┴─────────┴────────────┴──────────────┴────────┘

tempo impiegato: 0.03 secondi
Show the script for this step
lab_19_strumenti.py
python
inizio = time.perf_counter()

righe = []
for nome in ASSET:
    # `carica_strumento` e non `carica`: LUNAUSDT, dal 31 maggio 2022, quota
    # LUNA 2.0. Rendimento, volatilità e calo massimo di un token morto,
    # calcolati sulla serie grezza, sono le metriche di due strumenti diversi
    # incollati insieme.
    d = carica_strumento(nome).sort("data")
    p = d["chiusura"].to_numpy()
    r = rendimenti(p)
    curva = np.concatenate([[1.0], np.cumprod(1 + r)])
    righe.append({
        "asset": nome,
        "giorni": len(p),
        "dal": str(d["data"][0]),
        "al": str(d["data"][-1]),
        "finale": round(float(p[-1] / p[0]), 3),
        "cagr": round(cagr(curva), 4),
        "volatilita": round(volatilita(r), 3),
        "calo_massimo": round(drawdown_massimo(curva), 3),
        "sharpe": round(sharpe(r), 2),
    })

tabella = pl.DataFrame(righe).sort("calo_massimo")
print(tabella)
print(f"\ntempo impiegato: {time.perf_counter() - inizio:.2f} secondi")

Test 2 — A thousand alternative paths, and where yours falls

It's the operation that turns "that's how it went" into "how it went sits in the worst thirty percent of possible cases". Almost no trading platform does this, and its absence is why almost nobody asks the question.

Histogram of the final capital of 2,000 possible paths, with a logarithmic horizontal axis from ten to the minus two to ten to the fourth times and the count reaching 1,750. The mass is packed around 14.4 times, the median; a black vertical line marks the history that actually happened at 13.7 times, falling at the forty-ninth percentile — that is, in the middle.

Where the history that happened falls among the two thousand that could have.Source: Binance Data Vision · Period: 2017-08-17 … 2026-06-30 · Method: Two thousand paths block-resampled from the changes that actually happened, and the position of the real path inside their distribution.

Output

la storia capitata: 13.68x  →  percentile 49
mediana dei possibili: 14.40x
Show the script for this step
lab_19_strumenti.py
python
r = rendimenti(carica("btcusdt").sort("data")["chiusura"].to_numpy())
rng = np.random.default_rng(seed_for("lab-strumenti"))
percorsi = bootstrap_traiettorie(r, n_traiettorie=2000, rng=rng, a_blocchi=20)
# PROVA / TRY: n_traiettorie=500 (veloce) · 2000 · 10000 (coda più precisa)

reale = float(np.prod(1 + r))
finali = percorsi[:, -1]
percentile = float((finali < reale).mean() * 100)

with avvio.figura("schermo"):
    fig, ax = plt.subplots()
    ax.hist(finali, bins=70)
    ax.axvline(reale, linewidth=2.5, color="black")
    ax.set_xscale("log")
    ax.set_xlabel("Capitale finale (volte, scala log)")
    ax.set_ylabel("Su 2.000 percorsi possibili")
    plt.show()

print(f"la storia capitata: {reale:.2f}x  →  percentile {percentile:.0f}")
print(f"mediana dei possibili: {np.median(finali):.2f}x")

Test 3 — Exporting the raw data

Not the chart: the numbers. If you can't, you're delegating to that tool not just the execution but also the verification.

Output

file scritto: esportazione.csv
impronta SHA-256: 2fba4b01a40602f708320c3e6ace15b3…

Da questo momento chiunque puo' verificare che i tuoi numeri siano esattamente questi. Non e' pignoleria: e' la differenza fra un risultato e il ricordo di un risultato.
Show the script for this step
lab_19_strumenti.py
python
percorso = "esportazione.csv"
tabella.write_csv(percorso)
with open(percorso, "rb") as f:
    impronta = hashlib.sha256(f.read()).hexdigest()

print(f"file scritto: {percorso}")
print(f"impronta SHA-256: {impronta[:32]}…")
print("\nDa questo momento chiunque puo' verificare che i tuoi numeri siano "
      "esattamente questi. Non e' pignoleria: e' la differenza fra un risultato "
      "e il ricordo di un risultato.")

Test 4 — Rerunning and getting the exact same number

Can the work from six months ago be redone with one command? If it lives in a sequence of clicks, the answer is no by construction.

Output

     serie   righe   estratta il           impronta
   btcusdt    3240    2026-08-16   ea75ad84e6e98150…
   ethusdt    3240    2026-08-16   c2bd0259da905e0f…
   solusdt    2150    2026-08-16   c7ba2368a3e419b8…
  lunausdt     846    2026-08-16   10fe10357f76eb40…
   fttusdt    1062    2026-08-16   7b235709ebb5ae31…

I dati di questo libro sono congelati e firmati. Se qualcuno modificasse un file, il codice si RIFIUTEREBBE di eseguire — provaci: apri uno snapshot, cambia un byte, e riesegui la prima cella.
Show the script for this step
lab_19_strumenti.py
python
registro = leggi_registro()
print(f"{'serie':>10s} {'righe':>7s} {'estratta il':>13s} {'impronta':>18s}")
for nome in ASSET:
    voce = registro[nome]
    print(f"{nome:>10s} {voce.righe:7d} {voce.estratto:>13s} {voce.sha256[:16]:>18s}…")

print("\nI dati di questo libro sono congelati e firmati. Se qualcuno modificasse "
      "un file, il codice si RIFIUTEREBBE di eseguire — provaci: apri uno "
      "snapshot, cambia un byte, e riesegui la prima cella.")

Test 5 — How long it takes to redo everything changing one parameter

If the answer is "half an hour", you won't run most of the checks you should. If it's "thirty seconds", you'll run them all.

Output

24 varianti complete, con costi, calcolate in 0.22 secondi
peggiore 1.81x   mediana 12.21x   migliore 41.00x

E' questo il punto del capitolo: non la velocita' del computer, ma il fatto che a questo prezzo le verifiche LE FAI. Il numero di verifiche che NON fai e' esattamente cio' che determina quanto ti stai ingannando.
Show the script for this step
lab_19_strumenti.py
python
from cvbook.regole import esegui, rottura

prezzi = carica("btcusdt").sort("data")["chiusura"].to_numpy()

inizio = time.perf_counter()
griglia = {int(f): esegui(prezzi, rottura(prezzi, int(f)), costo=0.0012)["finale"]
           for f in range(5, 121, 5)}
durata = time.perf_counter() - inizio

print(f"{len(griglia)} varianti complete, con costi, calcolate in {durata:.2f} secondi")
print(f"peggiore {min(griglia.values()):.2f}x   mediana "
      f"{np.median(list(griglia.values())):.2f}x   migliore {max(griglia.values()):.2f}x")
print("\nE' questo il punto del capitolo: non la velocita' del computer, ma il "
      "fatto che a questo prezzo le verifiche LE FAI. Il numero di verifiche che "
      "NON fai e' esattamente cio' che determina quanto ti stai ingannando.")

The score

Mentally redo the five tests with your current tool and count how many it passes.

  • Five out of five: keep it. The best choice is the one you already use well.
  • Three or four: you know where the gaps are, and now you also know what they cost.
  • Fewer than three: the problem isn't that you're working worse than you could. It's that there are questions that aren't occurring to you, and by definition you can't notice that from the inside.

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