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
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
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.
Output
la storia capitata: 13.68x → percentile 49 mediana dei possibili: 14.40x
Show the script for this step
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
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
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
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
lab_19_strumenti.ipynb12.8 KB
sha256 cfa0c835d00b0fabf74fbd438eb448c92ed8825e5fdbefcad2e66a3d074cc7c0
lab_19_strumenti.py9.5 KB
sha256 fe99c53a70fa8541812ec295d733291725b32f2249d8f2e7003ccb8f3071ecb1
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