Skip to content
SIAT PAPER 2026Open the research page
Cryptoverso

Lab from the book · L02

L02 — Equity curves generated by chance alone, and the finest among a thousand

Lab 2 — Chance dressed up as skill

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 "Chance dressed up as skill". This notebook generates capital curves that contain nothing: no decisions, no signal, expected edge exactly zero. Then it does what everyone who shows off their results does: keeps the best and throws away the rest. The final exercise is the most useful one in the notebook: plug in a result you've seen around and it tells you what percentage of random curves do better.

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

from cvbook import seed_for
from cvbook.metriche import drawdown_massimo
from cvbook.simulazioni import equity_casuali, migliori_per_caso

1. Twelve curves. Guess which ones are the skilled ones

Look at them before scrolling. Some will look convincing to you — and that's fine, it's exactly the chapter's point.

Twelve panels on four columns, titled from A to L under the heading «Dodici curve senza alcuna abilita' dentro» — twelve curves with no skill inside them: each holds 500 days of capital rebased to 100 with a dotted line on the starting value. The final outcomes written in the titles run from 0.41 to 1.62 times the starting capital, and the vertical scales of the panels reach from 40 up to 180.

Twelve capital curves generated by chance alone, with no edge inside them.Source: — data generated in the notebook · Period: — data generated in the notebook · Method: Five hundred trades per curve, drawn with a typical swing of 1.8% and an edge of exactly zero, with the seed fixed by the notebook.
Show the script for this step
lab_02_equity_casuali.py
python
GIORNI = 500          # ← due anni circa di operatività
                      # PROVA / TRY: 125 (sei mesi) · 500 · 1000
VOLATILITA = 0.018    # ← oscillazione tipica per operazione
                      # PROVA / TRY: 0,01 · 0,018 · 0,04
VANTAGGIO = 0.0       # ← lascialo a zero: è il punto

rng = np.random.default_rng(seed_for("lab-equity-casuali"))
# PROVA / TRY: cambia il nome dentro seed_for(...) per vedere altre dodici
# curve — nessuna delle due versioni contiene più abilità dell'altra
curve = equity_casuali(12, GIORNI, rendimento_atteso=VANTAGGIO,
                       volatilita_periodo=VOLATILITA, rng=rng)

with avvio.figura("schermo"):
    fig, assi = plt.subplots(3, 4, figsize=(11, 6), sharex=True)
    for k, ax in enumerate(assi.flat):
        ax.plot(curve[k] * 100, linewidth=1.2)
        ax.axhline(100, linestyle=":", linewidth=0.8)
        ax.set_title(f"{chr(65 + k)}   {curve[k][-1]:.2f}x", fontsize=9)
        ax.set_xticks([])
    fig.suptitle("Dodici curve senza alcuna abilità dentro")
    plt.show()

There aren't any. They are twelve draws from the same generator, with zero expected edge. The two you liked just had a good week.

2. The five best out of a thousand

Now the operation that everyone who presents their own results performs, without saying so: generate a thousand attempts and show the five best.

Two side-by-side panels. On the left, «Le 5 migliori su 1000» — the best five out of a thousand: five capital curves rebased to 100 over 500 days, all of them rising, the highest above 400. On the right, «Da dove sono state estratte» — where they were drawn from: a histogram of the 1,000 final outcomes, centred just below 100 with a right tail thinning out beyond 300. The median of the thousand is 0.91 times the starting capital.

The best five curves out of a thousand, and the distribution they were picked from.Source: — data generated in the notebook · Period: — data generated in the notebook · Method: A thousand curves generated with zero edge; on the left the five with the highest final capital, on the right the distribution of all one thousand final outcomes.

Output

mediana dei 1000 risultati: 0.907x
la migliore:               4.079x  (calo massimo -14.3%)
quota sopra 1,5x:          10.00%
quota sopra 2x:            2.80%
quota sopra 2x CON calo massimo sotto il 20%: 0.80%
Show the script for this step
lab_02_equity_casuali.py
python
N = 1000  # PROVA / TRY: 200 (veloce) · 1000 · 10000 (il numero del capitolo si stabilizza)
rng = np.random.default_rng(seed_for("migliori-per-caso"))
# NON TOCCARE / DO NOT CHANGE: questa cella mostra le 5 migliori su 1000
# tentativi senza vantaggio — è la messa in scena del p-hacking, non
# l'occasione per cercare un seme che renda le 5 migliori ancora più belle.
# This cell shows the 5 best out of 1000 attempts with no edge — it's the
# staging of p-hacking, not a chance to hunt for a seed that makes the 5
# best look even more impressive.
tutte = equity_casuali(N, GIORNI, rendimento_atteso=VANTAGGIO,
                       volatilita_periodo=VOLATILITA, rng=rng)
migliori = migliori_per_caso(tutte, 5)

with avvio.figura("schermo"):
    fig, (sx, dx) = plt.subplots(1, 2, figsize=(11, 4))
    for curva in migliori:
        sx.plot(curva * 100, linewidth=1.4)
    sx.axhline(100, linestyle=":", linewidth=0.9)
    sx.set_title(f"Le 5 migliori su {N}")
    sx.set_ylabel("Capitale (base 100)")

    dx.hist(tutte[:, -1] * 100, bins=60)
    dx.axvline(100, linestyle=":", linewidth=1.2)
    dx.set_title("Da dove sono state estratte")
    dx.set_xlabel("Capitale finale (base 100)")
    plt.show()

finali = tutte[:, -1]
print(f"mediana dei {N} risultati: {np.median(finali):.3f}x")
print(f"la migliore:               {finali.max():.3f}x  "
      f"(calo massimo {drawdown_massimo(migliori[0]):.1%})")
print(f"quota sopra 1,5x:          {(finali > 1.5).mean():.2%}")
print(f"quota sopra 2x:            {(finali > 2.0).mean():.2%}")
print(f"quota sopra 2x CON calo massimo sotto il 20%: "
      f"{np.mean((finali > 2) & (np.array([drawdown_massimo(c) for c in tutte]) > -0.20)):.2%}")

The last line is the chapter's number: about one in a hundred. Out of ten thousand people who try, a hundred produce a standout two-year run with absolutely nothing inside.

3. And now your own case

Take a result that struck you — a chart, an ad, a curve from some channel — and plug in the three numbers below.

Output

risultato dichiarato: 2.50x su 500 giorni
curve casuali che fanno altrettanto o meglio: 0.76%
su 10.000 persone senza alcuna abilita', ne otterrebbero altrettanto: 76
Show the script for this step
lab_02_equity_casuali.py
python
RISULTATO_DICHIARATO = 2.5   # ← capitale finale dichiarato, in volte (2,5 = +150%)
                             # PROVA / TRY: il risultato che ti ha colpito davvero
DURATA_GIORNI = 500          # ← su quanti giorni di operatività
                             # PROVA / TRY: 125 (vedi esercizio 1) · 500
VOLATILITA_TIPICA = 0.018    # ← oscillazione per operazione, se la conosci
                             # PROVA / TRY: 0,018 · 0,04 (vedi esercizio 2)

rng = np.random.default_rng(seed_for("confronto-personale"))
prova = equity_casuali(5000, DURATA_GIORNI, rendimento_atteso=0.0,
                       volatilita_periodo=VOLATILITA_TIPICA, rng=rng)[:, -1]
meglio = float((prova >= RISULTATO_DICHIARATO).mean())

print(f"risultato dichiarato: {RISULTATO_DICHIARATO:.2f}x su {DURATA_GIORNI} giorni")
print(f"curve casuali che fanno altrettanto o meglio: {meglio:.2%}")
print(f"su 10.000 persone senza alcuna abilita', ne otterrebbero altrettanto: "
      f"{meglio * 10_000:.0f}")

Exercises

  1. Reduce DURATA_GIORNI to 125 (six months) keeping the same result: the percentage collapses. A spectacular result over a short period is harder to get by chance than a spectacular one over a long period.
  2. Raise VOLATILITA_TIPICA to 0.04. The same figure becomes much easier to get by chance: how much it swings changes the meaning of how much it returns.
  3. Set VANTAGGIO = 0.0005 in the first cell. Now there really is an edge. Can you tell it apart by eye from the curves with no edge? Almost nobody can, and that's why the chapter on statistical power exists.

Reproducibility & downloads

Run on 2026-08-27 from the repository notebook

The notebook

Back to the lab index