Vai al contenuto
PAPER SIAT 2026Apri la pagina della ricerca
Cryptoverso

Lab del libro · L15

L15 — Ottimizzare una regola senza senso, dentro e fuori campione

Lab 15 — Ottimizzare è ingannarsi

Quaderno del capitolo «Ottimizzare è ingannarsi» di La matematica di chi perde.

Qui rifai l'esperimento che smonta il risultato del Lab 12: la stessa regola al variare del suo unico parametro, dentro e fuori campione. Poi provi a trovare un valore che vinca in entrambe le metà. È un esercizio frustrante nel modo giusto.

E infine il pezzo che mi ha convinto più di ogni altro: si prende una regola volutamente senza senso, la si ottimizza, e si guarda che bel risultato in campione. Poi si guarda fuori.

Le righe marcate PROVA sono quelle da cambiare: cambiale e riesegui per vedere l'effetto. Il resto — comprese le righe marcate NON TOCCARE — serve a mantenere il risultato confrontabile con quello stampato nel libro.

Espandi lo script di questo passo
lab_15_ottimizzazione.py
python
import matplotlib.pyplot as plt
import numpy as np
import polars as pl

from cvbook.dati import carica
from cvbook.metriche import drawdown_massimo
from cvbook.regole import compra_e_tieni, esegui, sopra_media

SERIE = "btcusdt"     # ← PROVA / TRY: "ethusdt" · "solusdt" (esercizio 1)
FINESTRE = np.arange(5, 121, 5)  # PROVA / TRY: allarga o restringi il passo
COSTO = 0.0012                   # PROVA / TRY: 0,0006 · 0,0012 · 0,0025

df = carica(SERIE).sort("data")
prezzi = df["chiusura"].to_numpy()
meta = len(prezzi) // 2
prima, seconda = prezzi[:meta], prezzi[meta:]

1. La mappa, dentro e fuori campione

Due linee sulla lunghezza della media, da 5 a 120 giorni, con il capitale finale su scala logaritmica da 10 alla 0 a 10 alla 1. Quella continua, misurata sulla prima meta' della storia — la stessa usata per scegliere — ha un picco isolato a 50 giorni che vale 30,8 volte; quella tratteggiata, misurata sulla seconda meta', in quel punto vale 2,7, ed e' piu' alta a 120 giorni, dove vale 3,3.

Lo stesso parametro visto sulla meta' che lo ha scelto e su quella che non lo ha mai visto.Fonte: Binance Data Vision · Periodo: 2017-08-17 … 2026-06-30 · Metodo: La stessa regola calcolata per ogni lunghezza della media su entrambe le meta' della serie; la scelta del valore migliore avviene solo sulla prima.

Output

il valore migliore sulla PRIMA meta':  50 giorni → 30.81x
lo stesso valore sulla SECONDA meta':  2.69x
restringimento: un fattore 11.5

il valore migliore sulla SECONDA meta': 120 giorni → 3.29x

Il numero che sembrava una scoperta non e' nemmeno quello giusto a posteriori: era una proprieta' di quel campione.
Espandi lo script di questo passo
lab_15_ottimizzazione.py
python
dentro = np.array([esegui(prima, sopra_media(prima, int(f)), costo=COSTO)["finale"]
                   for f in FINESTRE])
fuori = np.array([esegui(seconda, sopra_media(seconda, int(f)), costo=COSTO)["finale"]
                  for f in FINESTRE])

migliore_dentro = int(FINESTRE[np.argmax(dentro)])
migliore_fuori = int(FINESTRE[np.argmax(fuori)])

with avvio.figura("schermo"):
    fig, ax = plt.subplots()
    ax.plot(FINESTRE, dentro, marker="o", linewidth=2, label="prima metà (usata per scegliere)")
    ax.plot(FINESTRE, fuori, marker="s", linewidth=2, linestyle="--",
            label="seconda metà (mai vista)")
    ax.axvline(migliore_dentro, linestyle=":", linewidth=1.2)
    ax.set_yscale("log")
    ax.set_xlabel("Lunghezza della media (giorni)")
    ax.set_ylabel("Capitale finale (volte, scala log)")
    ax.legend()
    plt.show()

scelto = int(np.argmax(dentro))
print(f"il valore migliore sulla PRIMA meta':  {migliore_dentro} giorni "
      f"→ {dentro[scelto]:.2f}x")
print(f"lo stesso valore sulla SECONDA meta':  {fuori[scelto]:.2f}x")
print(f"restringimento: un fattore {dentro[scelto] / fuori[scelto]:.1f}\n")
print(f"il valore migliore sulla SECONDA meta': {migliore_fuori} giorni "
      f"→ {fuori[np.argmax(fuori)]:.2f}x")
print("\nIl numero che sembrava una scoperta non e' nemmeno quello giusto a "
      "posteriori: era una proprieta' di quel campione.")

2. Rispetto a cosa? Il confronto onesto sulla seconda metà

Il paragone giusto per un risultato su una finestra è il non far niente su quella stessa finestra, non sull'intero periodo.

Output

seconda meta': 1620 giorni (4.4 anni)

                     finale   calo massimo   tempo dentro
       la regola      2.69x         -44.5%            50%
  compra e tieni      1.62x         -66.7%           100%
Espandi lo script di questo passo
lab_15_ottimizzazione.py
python
riferimento = esegui(seconda, compra_e_tieni(seconda), costo=COSTO)
regola = esegui(seconda, sopra_media(seconda, migliore_dentro), costo=COSTO)

print(f"seconda meta': {len(seconda)} giorni ({len(seconda) / 365:.1f} anni)\n")
print(f"{'':>16s} {'finale':>10s} {'calo massimo':>14s} {'tempo dentro':>14s}")
print(f"{'la regola':>16s} {regola['finale']:9.2f}x {drawdown_massimo(regola['curva']):14.1%} "
      f"{regola['esposizione']:14.0%}")
print(f"{'compra e tieni':>16s} {riferimento['finale']:9.2f}x "
      f"{drawdown_massimo(riferimento['curva']):14.1%} {1.0:14.0%}")

3. Trova un valore che vinca in entrambe le metà

L'esercizio frustrante nel modo giusto.

Output

 finestra  rango dentro  rango fuori
       35             5           11
       40             3            8
       45             2            5
       50             1            6
       55             4            7
      105            22            4
      110            19            2
      115            23            3
      120            24            1

correlazione fra i ranghi delle due meta': -0.02
Se fosse vicina a +1, il parametro migliore sul passato sarebbe anche quello migliore sul futuro. Non lo e'.
Espandi lo script di questo passo
lab_15_ottimizzazione.py
python
ranghi_dentro = np.argsort(np.argsort(-dentro)) + 1
ranghi_fuori = np.argsort(np.argsort(-fuori)) + 1

print(f"{'finestra':>9s} {'rango dentro':>13s} {'rango fuori':>12s}")
for k, f in enumerate(FINESTRE):
    if ranghi_dentro[k] <= 5 or ranghi_fuori[k] <= 5:
        print(f"{f:9d} {ranghi_dentro[k]:13d} {ranghi_fuori[k]:12d}")

correlazione = float(np.corrcoef(ranghi_dentro, ranghi_fuori)[0, 1])
print(f"\ncorrelazione fra i ranghi delle due meta': {correlazione:+.2f}")
print("Se fosse vicina a +1, il parametro migliore sul passato sarebbe anche "
      "quello migliore sul futuro. Non lo e'.")

4. Una regola volutamente senza senso

Compriamo in base al giorno del mese. Non c'è nessuna ragione perché funzioni, e infatti non ce n'è nessuna: ottimizziamola lo stesso.

Output

combinazioni provate: 6,496
la migliore: investire FUORI dal giorno 20 al giorno 23 del mese, saltando il mercoledi
  sulla prima meta':      36.58x   (compra e tieni: 8.17x)
  sulla seconda meta':     2.19x   (compra e tieni: 1.62x)

restringimento dentro→fuori: un fattore 16.7

Dentro campione e' una curva che si mostrerebbe volentieri, e sotto non c'e' alcun meccanismo: l'abbiamo scelta apposta senza senso.
Fuori campione il risultato si restringe di piu' di un ordine di grandezza. Quel poco che avanza NON e' un vantaggio: e' l'effetto di stare fuori dal mercato una parte del tempo, che su un periodo agitato basta a evitare qualche giorno brutto. Il metro del caso del Lab 12 e' li' apposta per separare le due cose — provaci.
Espandi lo script di questo passo
lab_15_ottimizzazione.py
python
date = df["data"].to_list()
giorno_mese = np.array([d.day for d in date])
giorno_settimana = np.array([d.weekday() for d in date])

GIORNI_SETTIMANA = ["lunedi", "martedi", "mercoledi", "giovedi", "venerdi",
                    "sabato", "domenica", "nessuno"]


def regola_assurda(mese: np.ndarray, settimana: np.ndarray,
                   dal: int, al: int, escluso: int, invertita: bool) -> np.ndarray:
    """Investito nei giorni del mese fra `dal` e `al`, saltando un giorno della
    settimana. Nessun meccanismo la sostiene: e' scelta apposta perche' non ne ha.
    """
    dentro_finestra = (mese >= dal) & (mese <= al)
    if invertita:
        dentro_finestra = ~dentro_finestra
    segnale = (dentro_finestra & (settimana != escluso)).astype(float)
    posizione = np.zeros(len(segnale))
    posizione[1:] = segnale[:-1]
    return posizione


migliore, tentativi = (None, -np.inf), 0
for dal in range(1, 29):
    for al in range(dal, 29):
        for escluso in range(8):          # 0-6 = un giorno saltato, 7 = nessuno
            for invertita in (False, True):
                tentativi += 1
                valore = esegui(
                    prima,
                    regola_assurda(giorno_mese[:meta], giorno_settimana[:meta],
                                   dal, al, escluso, invertita),
                    costo=COSTO,
                )["finale"]
                if valore > migliore[1]:
                    migliore = ((dal, al, escluso, invertita), valore)

(dal, al, escluso, invertita), valore_dentro = migliore
valore_fuori = esegui(
    seconda,
    regola_assurda(giorno_mese[meta:], giorno_settimana[meta:], dal, al, escluso, invertita),
    costo=COSTO,
)["finale"]

print(f"combinazioni provate: {tentativi:,}")
print(f"la migliore: investire {'FUORI dal' if invertita else 'dal'} giorno {dal} "
      f"al giorno {al} del mese, saltando il {GIORNI_SETTIMANA[escluso]}")
# Il compra-e-tieni fra parentesi passa dallo stesso motore delle regole, con
# gli stessi costi: la cella 2 lo faceva gia', questa lo prendeva grezzo.
print(f"  sulla prima meta':   {valore_dentro:8.2f}x   (compra e tieni: "
      f"{esegui(prima, compra_e_tieni(prima), costo=COSTO)['finale']:.2f}x)")
print(f"  sulla seconda meta': {valore_fuori:8.2f}x   (compra e tieni: "
      f"{esegui(seconda, compra_e_tieni(seconda), costo=COSTO)['finale']:.2f}x)")
print(f"\nrestringimento dentro→fuori: un fattore "
      f"{valore_dentro / max(valore_fuori, 1e-9):.1f}")
print("\nDentro campione e' una curva che si mostrerebbe volentieri, e sotto non "
      "c'e' alcun meccanismo: l'abbiamo scelta apposta senza senso.")
print("Fuori campione il risultato si restringe di piu' di un ordine di grandezza. "
      "Quel poco che avanza NON e' un vantaggio: e' l'effetto di stare fuori dal "
      "mercato una parte del tempo, che su un periodo agitato basta a evitare "
      "qualche giorno brutto. Il metro del caso del Lab 12 e' li' apposta per "
      "separare le due cose — provaci.")

Esercizi

  1. Cambia SERIE. Il valore migliore sulla prima metà cambia da un mercato all'altro. Se fosse una proprietà del mondo, non dovrebbe.
  2. Nella prima cella, invece del massimo prendi il centro dell'altopiano più largo — la regione in cui il risultato resta accettabile. Confronta i due valori fuori campione: di solito il secondo regge meglio.
  3. Nella quarta cella prova a spiegare a te stesso perché «dal giorno X al giorno Y del mese» dovrebbe funzionare. Ci riuscirai: il cervello produce spiegazioni per qualunque cosa, ed è esattamente il punto.

Riproducibilità e download

Eseguito il 2026-08-27 dal quaderno del repository

Il quaderno

I dati

  • btcusdt.parquet93,2 KB

    sha256 ea75ad84e6e981507054df5c622c6b0ec3c8849c1f4dd007721878d4e4c8a329

    Fonte: Binance Data Vision · Periodo: 2017-08-17 → 2026-06-30 · 3240 righe · estratto 2026-08-16

  • ethusdt.parquet87,0 KB

    sha256 c2bd0259da905e0fec87235d7a62295532433fb89657726dd2d19558db7c072a

    Fonte: Binance Data Vision · Periodo: 2017-08-17 → 2026-06-30 · 3240 righe · estratto 2026-08-16

  • solusdt.parquet57,5 KB

    sha256 c7ba2368a3e419b898fb31ec6d5345b7212b74784b69079d3d43571c2ac63657

    Fonte: Binance Data Vision · Periodo: 2020-08-11 → 2026-06-30 · 2150 righe · estratto 2026-08-16

Torna all’indice dei lab