Skip to content
SIAT PAPER 2026Open the research page
Cryptoverso

Lab from the book · L16

L16 — Six textbook rules measured together, with the three checks

Lab 16 — Technical analysis, measured

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 "Technical analysis, measured". The chapter's six rules, all written in the same form, applied to the asset and period you choose. Without cherry-picking which ones to show after seeing the results. Then the three checks on the rule that wins: the shape of the surface, the yardstick of chance, and the correction for the number of attempts. Nothing in here is trading advice. It's a way of asking the question.

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

from cvbook import seed_for
from cvbook.dati import carica
from cvbook.metriche import drawdown_massimo
from cvbook.regole import CATALOGO, compra_e_tieni, esegui, rottura

SERIE = "btcusdt"   # ← PROVA / TRY: "ethusdt" · "solusdt" (le tre preparate nel setup)
COSTO = 0.0012      # ← il tuo costo per operazione
                    # PROVA / TRY: raddoppialo (vedi esercizio 1)

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

1. Six textbook rules, all together

The constraint that separates a measurement from a showcase: all of them are shown.

Output

btcusdt, costi 0.12% per operazione

                regola     netto     lordo   oper.   dentro   calo max
  Forza relativa 30/70     0.50x     0.54x      71      47%     -83.4%
       Incrocio 50/200     4.33x     4.42x      18      50%     -66.8%
    Sopra la media 200     6.40x     6.91x      64      49%     -64.2%
        Incrocio 20/50    12.87x    14.03x      72      52%     -74.9%
     Momento a 12 mesi    14.17x    14.62x      26      59%     -63.3%
   Rottura a 20 giorni    35.66x    39.90x      94      52%     -60.9%
        compra e tieni    13.66x    13.68x       1     100%     -83.2%

Six horizontal bars, one per textbook rule, with the final capital per euro on a logarithmic scale from ten to the zero to ten to the one. From the top: relative strength 30/70 at 0.50 times, 50/200 crossover at 4.33, above the 200-day average at 6.40, 20/50 crossover at 12.87, twelve-month momentum at 14.17, twenty-day breakout at 35.66. A black dashed line at 13.7 times marks staying in the market throughout: two rules out of six clear it.

Six rules measured together, and not just the one that worked.Source: Binance Data Vision · Period: 2017-08-17 … 2026-06-30 · Method: Every rule applied to the same daily closes with a cost of 0.12% per trade; the dashed reference is the same series held without ever trading.
2 regole su 6 hanno battuto il non far niente.
Show the script for this step
lab_16_analisi_tecnica.py
python
riferimento = esegui(prezzi, compra_e_tieni(prezzi), costo=COSTO)

righe = []
for nome, regola in CATALOGO.items():
    e = esegui(prezzi, regola(prezzi), costo=COSTO)
    righe.append((nome, e["finale"], e["finale_lordo"], e["operazioni"],
                  e["esposizione"], drawdown_massimo(e["curva"])))
righe.sort(key=lambda x: x[1])

print(f"{SERIE}, costi {COSTO:.2%} per operazione\n")
print(f"{'regola':>22s} {'netto':>9s} {'lordo':>9s} {'oper.':>7s} "
      f"{'dentro':>8s} {'calo max':>10s}")
for nome, netto, lordo, op, esp, dd in righe:
    print(f"{nome:>22s} {netto:8.2f}x {lordo:8.2f}x {op:7.0f} {esp:8.0%} {dd:10.1%}")
print(f"{'compra e tieni':>22s} {riferimento['finale']:8.2f}x "
      f"{riferimento['finale_lordo']:8.2f}x {riferimento['operazioni']:7.0f} "
      f"{riferimento['esposizione']:8.0%} {drawdown_massimo(riferimento['curva']):10.1%}")

with avvio.figura("schermo"):
    fig, ax = plt.subplots(figsize=(9, 4.5))
    y = np.arange(len(righe))
    ax.barh(y, [r[1] for r in righe])
    ax.axvline(riferimento["finale"], linewidth=2, linestyle="--", color="black")
    ax.annotate(f"compra e tieni: {riferimento['finale']:.1f}x",
                xy=(riferimento["finale"], len(righe) - 0.4),
                xytext=(6, 0), textcoords="offset points", va="center")
    ax.set_yticks(y)
    ax.set_yticklabels([r[0] for r in righe])
    ax.set_xscale("log")
    ax.set_xlabel("Capitale finale, per ogni euro investito (scala log)")
    plt.show()

battono = sum(1 for r in righe if r[1] > riferimento["finale"])
print(f"\n{battono} regole su {len(righe)} hanno battuto il non far niente.")

2. Costs decide the ranking

Look at the "net" and "gross" columns of the table. Some rules switch sides.

Output

                regola       0.00%      0.06%      0.12%      0.25%      0.50%
       Incrocio 50/200       4.42x      4.38x      4.33x      4.23x      4.04x
        Incrocio 20/50      14.03x     13.44x     12.87x     11.71x      9.78x
    Sopra la media 200       6.91x      6.65x      6.40x      5.88x      5.01x
   Rottura a 20 giorni      39.90x     37.72x     35.66x     31.57x     24.96x
  Forza relativa 30/70       0.54x      0.52x      0.50x      0.46x      0.38x
     Momento a 12 mesi      14.62x     14.39x     14.17x     13.70x     12.84x
        compra e tieni      13.68x     13.67x     13.66x     13.65x     13.61x

«Quale regola e' migliore» dipende da quanto paghi. Chi pubblica un backtest senza costi non ha mentito su nessun numero: ha omesso una voce, e l'omissione puo' invertire la conclusione.
Show the script for this step
lab_16_analisi_tecnica.py
python
print(f"{'regola':>22s} " + "".join(f"{c:>11.2%}" for c in (0.0, 0.0006, 0.0012, 0.0025, 0.005)))
for nome, regola in CATALOGO.items():
    valori = "".join(f"{esegui(prezzi, regola(prezzi), costo=c)['finale']:10.2f}x"
                     for c in (0.0, 0.0006, 0.0012, 0.0025, 0.005))
    print(f"{nome:>22s} {valori}")
# Anche il compra-e-tieni ha un'operazione — il proprio ingresso — quindi la
# sua riga cambia con la colonna, di poco ma cambia. Stamparla costante era
# comodo e falso: diceva che il metro di confronto non paga i costi.
print(f"{'compra e tieni':>22s} " +
      "".join(f"{esegui(prezzi, compra_e_tieni(prezzi), costo=c)['finale']:10.2f}x"
              for c in (0.0, 0.0006, 0.0012, 0.0025, 0.005)))

print("\n«Quale regola e' migliore» dipende da quanto paghi. Chi pubblica un "
      "backtest senza costi non ha mentito su nessun numero: ha omesso una voce, "
      "e l'omissione puo' invertire la conclusione.")

3. The first check: the shape of the surface

A wide plateau is compatible with a real phenomenon. An isolated peak almost never is.

A dotted line over the breakout window, from 5 to 120 days in steps of 5, with the final capital on a logarithmic scale between 2 and 40 times. The profile is jagged rather than a plateau: the median of the grid is 12.2 times, and 11 values out of 24 clear the black dashed line of staying in the market throughout.

The shape of the surface around the chosen parameter: plateau or isolated peak.Source: Binance Data Vision · Period: 2017-08-17 … 2026-06-30 · Method: The same rule recomputed for every window from 5 to 120 days in steps of 5, on the same series and with the same costs.

Output

valori che battono il compra-e-tieni: 11 su 24
mediana della griglia: 12.21x
Show the script for this step
lab_16_analisi_tecnica.py
python
FINESTRE = np.arange(5, 121, 5)  # PROVA / TRY: allarga o restringi il passo
griglia = np.array([esegui(prezzi, rottura(prezzi, int(f)), costo=COSTO)["finale"]
                    for f in FINESTRE])

with avvio.figura("schermo"):
    fig, ax = plt.subplots()
    ax.plot(FINESTRE, griglia, marker="o", linewidth=2)
    ax.axhline(riferimento["finale"], linestyle="--", linewidth=1.5, color="black",
               label="compra e tieni")
    ax.set_yscale("log")
    ax.set_xlabel("Finestra della rottura (giorni)")
    ax.set_ylabel("Capitale finale (volte, scala log)")
    ax.legend()
    plt.show()

print(f"valori che battono il compra-e-tieni: {int((griglia > riferimento['finale']).sum())} "
      f"su {len(griglia)}")
print(f"mediana della griglia: {np.median(griglia):.2f}x")

4. The second check: the yardstick of chance

Histogram of the final capital of a thousand random positions, with a logarithmic horizontal axis from ten to the minus one to ten squared times and the count reaching 400. The mass is packed around 3.4 times, the median; a black vertical line marks the chosen rule at 35.7 times, and the title says it sits at the ninety-ninth percentile.

Where the chosen rule falls inside a thousand alternatives that spend as long in the market as it does.Source: Binance Data Vision · Period: 2017-08-17 … 2026-06-30 · Method: A thousand random positions with the same number of days in the market and the same number of trades as the rule, and the same costs.

Output

la regola: 35.66x con 94 operazioni
mediana delle casuali: 3.75x
percentile: 98.4
Show the script for this step
lab_16_analisi_tecnica.py
python
N_CASUALI = 1000  # PROVA / TRY: 200 (veloce) · 1000 · 10000 (percentile più preciso)
SCELTA = 20


def posizione_casuale(n: int, n_operazioni: int, rng) -> np.ndarray:
    pos = np.zeros(n)
    punti = np.sort(rng.choice(n - 1, size=n_operazioni, replace=False))
    stato, precedente = 0.0, 0
    for i in punti:
        pos[precedente:i] = stato
        stato, precedente = 1.0 - stato, i
    pos[precedente:] = stato
    return pos


scelta = esegui(prezzi, rottura(prezzi, SCELTA), costo=COSTO)
n_op = int(scelta["operazioni"])
rng = np.random.default_rng(seed_for("tecnica-verifica"))
# NON TOCCARE / DO NOT CHANGE: è il seme della figura stampata nel capitolo.
# Con quello — stesse mille posizioni casuali, stesse operazioni, stessi costi
# — questa cella ridisegna l'istogramma del libro e stampa il suo percentile.
# Con un seme qualunque il quaderno resta corretto ma risponde 98,9 dove la
# pagina dice 98: due numeri per la stessa domanda, ed è la cosa che questo
# libro promette di non fare.
# This is the seed of the figure printed in the chapter.
casuali = np.array([
    esegui(prezzi, posizione_casuale(len(prezzi), n_op, rng), costo=COSTO)["finale"]
    for _ in range(N_CASUALI)
])
percentile = float((casuali < scelta["finale"]).mean() * 100)

with avvio.figura("schermo"):
    fig, ax = plt.subplots()
    ax.hist(casuali, bins=50)
    ax.axvline(scelta["finale"], linewidth=2.5, color="black")
    ax.set_xscale("log")
    ax.set_xlabel("Capitale finale (volte, scala log)")
    ax.set_ylabel(f"Su {N_CASUALI} posizioni casuali")
    ax.set_title(f"La regola sta al {percentile:.0f}esimo percentile")
    plt.show()

print(f"la regola: {scelta['finale']:.2f}x con {n_op} operazioni")
print(f"mediana delle casuali: {np.median(casuali):.2f}x")
print(f"percentile: {percentile:.1f}")

5. The third check: how many attempts were behind it

The check almost nobody applies to their own numbers.

Output

regole provate: 6
valori del parametro provati: 24
tentativi complessivi (stima prudente): 30

probabilita' che UN tentativo raggiunga questo percentile per caso: 1.6%
probabilita' che almeno uno su 30 lo faccia: 38.4%

I tentativi non sono del tutto indipendenti — finestre vicine danno risultati simili — quindi questa stima e' pessimistica. Ma il punto resta: un percentile, dopo trenta tentativi, non e' piu' quel percentile.
Show the script for this step
lab_16_analisi_tecnica.py
python
regole_provate = len(CATALOGO)
parametri_provati = len(FINESTRE)
tentativi = regole_provate + parametri_provati

p_singolo = 1 - percentile / 100
print(f"regole provate: {regole_provate}")
print(f"valori del parametro provati: {parametri_provati}")
print(f"tentativi complessivi (stima prudente): {tentativi}\n")
print(f"probabilita' che UN tentativo raggiunga questo percentile per caso: {p_singolo:.1%}")
print(f"probabilita' che almeno uno su {tentativi} lo faccia: "
      f"{1 - (1 - p_singolo) ** tentativi:.1%}")
print("\nI tentativi non sono del tutto indipendenti — finestre vicine danno "
      "risultati simili — quindi questa stima e' pessimistica. Ma il punto resta: "
      "un percentile, dopo trenta tentativi, non e' piu' quel percentile.")

6. Other markets are not other trials

Output

   btcusdt: regola    35.66x   compra e tieni    13.66x   rapporto  2.61
   ethusdt: regola    48.88x   compra e tieni     5.20x   rapporto  9.40
   solusdt: regola   187.25x   compra e tieni    22.31x   rapporto  8.39

Tre conferme? No. Il Lab 8 ha mostrato che questi tre mercati contengono poco piu' di UNA scommessa: una sola componente ne spiega quasi l'ottanta per cento dei movimenti. Un metodo che funziona sul fattore comune funziona su tutti e tre — non perche' sia stato confermato tre volte, ma perche' e' stato confermato una volta e conteggiato tre.
Show the script for this step
lab_16_analisi_tecnica.py
python
for altro in ("btcusdt", "ethusdt", "solusdt"):
    p = carica(altro).sort("data")["chiusura"].to_numpy()
    r = esegui(p, rottura(p, SCELTA), costo=COSTO)
    b = esegui(p, compra_e_tieni(p), costo=COSTO)
    print(f"{altro:>10s}: regola {r['finale']:8.2f}x   compra e tieni {b['finale']:8.2f}x   "
          f"rapporto {r['finale'] / b['finale']:5.2f}")

print("\nTre conferme? No. Il Lab 8 ha mostrato che questi tre mercati contengono "
      "poco piu' di UNA scommessa: una sola componente ne spiega quasi l'ottanta "
      "per cento dei movimenti. Un metodo che funziona sul fattore comune "
      "funziona su tutti e tre — non perche' sia stato confermato tre volte, ma "
      "perche' e' stato confermato una volta e conteggiato tre.")

Exercises

  1. Change COSTO and rerun the second cell. Watch the ranking order change: it's the quickest demonstration that "which rule is better" isn't a property of the rule.
  2. Add your own rule to the catalogue, following the pattern in cvbook.regole, and put it through the same three checks. Remember to count it among the attempts.
  3. The most instructive one: run everything on a non-digital market. Watch out for closed trading days, which change the count of periods. If the result repeats there, you have a real confirmation; if it doesn't, you've learned something more useful than a confirmation.

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