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

Lab del libro · L09

L09 — Volatilità mobile, persistenza dei regimi e durata dei periodi agitati

Lab 9 — Il mercato non è sempre lo stesso mercato

Quaderno del capitolo «Il mercato non è sempre lo stesso mercato» di La matematica di chi perde.

La volatilità non è una costante dell'asset: è una serie storica. Qui la calcoli, ne guardi la forma, e verifichi che i periodi agitati durano invece di lampeggiare.

I due esercizi finali valgono più della figura: uno mostra che una finestra lunga nasconde i regimi invece di misurarli, l'altro li fa sparire rimescolando i dati — e vedere sparire una struttura quando la si distrugge di proposito è il modo più diretto di convincersi che c'era.

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

from cvbook.dati import carica
from cvbook.metriche import GIORNI_ANNO, rendimenti

SERIE = "btcusdt"   # ← PROVA / TRY: "ethusdt" · "solusdt" (le tre preparate nel setup)
FINESTRA = 30       # ← giorni della finestra mobile
                    # PROVA / TRY: 10 · 30 · 250 (vedi esercizio 4 qui sotto)

df = carica(SERIE).sort("data")
r = rendimenti(df["chiusura"].to_numpy())
date = df["data"].to_list()[1:]


def volatilita_mobile(rend: np.ndarray, finestra: int) -> np.ndarray:
    """Deviazione standard annualizzata, causale: usa solo il passato."""
    return np.array([
        np.std(rend[i - finestra:i], ddof=1) * np.sqrt(GIORNI_ANNO)
        for i in range(finestra, len(rend) + 1)
    ])


vol = volatilita_mobile(r, FINESTRA)
date_v = date[FINESTRA - 1:]

1. Il numero che descrive un mercato inesistente

La volatilita' annualizzata a 30 giorni di btcusdt dal 2018 al 2026, con l'asse verticale da 25 a oltre 175. La linea oscilla fra un minimo di 17% e un massimo di 184%, undici volte tanto; una riga tratteggiata segna la media di periodo, 62%, e le bande ombreggiate marcano il quarto di tempo piu' agitato. Solo il 39% dei giorni sta entro un quinto dalla media.

La volatilita' non e' un numero: e' una serie che passa poco tempo vicino alla propria media.Fonte: Binance Data Vision · Periodo: 2017-08-17 … 2026-06-30 · Metodo: Deviazione standard delle variazioni giornaliere su finestre mobili di 30 giorni, annualizzata; le bande sono i giorni nel quarto piu' alto della distribuzione.

Output

minimo   16.7%
massimo 183.6%   →  rapporto massimo/minimo: 11.0 volte
media    61.6%
quota di tempo entro il ±20% dalla media: 38.6%

Il mercato passa poco tempo vicino al numero che tutti chiamano «la volatilita' storica».
Espandi lo script di questo passo
lab_09_regimi.py
python
alta = float(np.percentile(vol, 75))
media = float(vol.mean())

with avvio.figura("schermo"):
    fig, ax = plt.subplots(figsize=(10, 4))
    ax.fill_between(date_v, 0, 1, where=vol > alta, transform=ax.get_xaxis_transform(),
                    step="mid", alpha=0.25, label="quarto piu' agitato")
    ax.plot(date_v, vol * 100, linewidth=1.0, label=f"volatilita' a {FINESTRA} giorni")
    ax.axhline(media * 100, linestyle="--", linewidth=1.2,
               label=f"media di periodo ({media:.0%})")
    ax.set_ylabel("Volatilita' annualizzata (%)")
    ax.legend(loc="upper right")
    fig.autofmt_xdate()
    plt.show()

vicino = float(np.mean((vol > media * 0.8) & (vol < media * 1.2)))
print(f"minimo  {vol.min():6.1%}")
print(f"massimo {vol.max():6.1%}   →  rapporto massimo/minimo: {vol.max() / vol.min():.1f} volte")
print(f"media   {media:6.1%}")
print(f"quota di tempo entro il ±20% dalla media: {vicino:.1%}")
print(f"\nIl mercato passa poco tempo vicino al numero che tutti chiamano "
      f"«la volatilita' storica».")

2. La memoria: i regimi persistono

Chiamiamo «agitato» un giorno in cui la volatilità sta nel quarto più alto. Per costruzione capita il 25% delle volte. Ora condizioniamo su com'è oggi.

Nota la precauzione che rende credibile il numero: le due finestre — quella che misura oggi e quella che misura fra un mese — non si sovrappongono.

Tre barre a confronto sulla probabilita' che il mercato sia agitato fra 30 giorni: partendo da un giorno calmo vale 16%, senza memoria varrebbe 25% per costruzione, partendo da un giorno agitato vale 49%. Fra la prima e la terza corrono 3,1 volte.

La stessa probabilita' vista da tre condizioni di partenza diverse.Fonte: Binance Data Vision · Periodo: 2017-08-17 … 2026-06-30 · Metodo: Quota di giorni agitati a 30 giorni di distanza, condizionata allo stato di oggi, con le due finestre di misura che non si sovrappongono.

Output

probabilita' di base:         25.0%
partendo da un giorno agitato: 49.1%
partendo da un giorno calmo:   15.9%
rapporto: 3.1 volte
Espandi lo script di questo passo
lab_09_regimi.py
python
ORIZZONTE = FINESTRA  # non sovrapposte: nessun dato in comune fra le due misure

alto = vol > np.percentile(vol, 75)
oggi, dopo = alto[:-ORIZZONTE], alto[ORIZZONTE:]
base = float(alto.mean())
da_alto = float(dopo[oggi].mean())
da_calmo = float(dopo[~oggi].mean())

with avvio.figura("schermo"):
    fig, ax = plt.subplots(figsize=(6, 4))
    valori = [da_calmo * 100, base * 100, da_alto * 100]
    ax.bar(["oggi calmo", "senza memoria", "oggi agitato"], valori)
    for k, v in enumerate(valori):
        ax.annotate(f"{v:.0f}%", xy=(k, v), xytext=(0, 4), textcoords="offset points",
                    ha="center")
    ax.set_ylabel(f"Agitato fra {ORIZZONTE} giorni (%)")
    plt.show()

print(f"probabilita' di base:         {base:.1%}")
print(f"partendo da un giorno agitato: {da_alto:.1%}")
print(f"partendo da un giorno calmo:   {da_calmo:.1%}")
print(f"rapporto: {da_alto / da_calmo:.1f} volte")

3. Quanto durano i periodi agitati

Il confronto con un mondo in cui i giorni sono indipendenti — stessa percentuale complessiva di giorni agitati, ma sparsi a caso.

Due istogrammi sovrapposti della durata dei periodi agitati, con l'asse orizzontale logaritmico da 1 a oltre 100 giorni. Quello pieno del mercato vero conta 32 episodi, con durata mediana 18 giorni e il piu' lungo di 187; quello tratteggiato di un mondo in cui i giorni sono indipendenti conta 597 episodi, quasi tutti di un giorno solo e mai oltre 7.

Quanto durano i periodi agitati, e quanto durerebbero se i giorni fossero indipendenti.Fonte: Binance Data Vision · Periodo: 2017-08-17 … 2026-06-30 · Metodo: Lunghezza di ogni sequenza consecutiva di giorni agitati, confrontata con quella ottenuta rimescolando gli stessi giorni in ordine casuale.

Output

                         episodi   mediana   il piu lungo
          mercato vero        32        18            187
   giorni indipendenti       597         1              7
Espandi lo script di questo passo
lab_09_regimi.py
python
def sequenze(maschera: np.ndarray) -> np.ndarray:
    lunghezze, corrente = [], 0
    for x in maschera:
        if x:
            corrente += 1
        elif corrente:
            lunghezze.append(corrente)
            corrente = 0
    if corrente:
        lunghezze.append(corrente)
    return np.array(lunghezze)


rng = np.random.default_rng(20260816)
# NON TOCCARE / DO NOT CHANGE: il seme fissa i numeri di episodi/mediana/il
# più lungo citati nel testo qui sotto e riusati nella cella 5.
# The seed fixes the episode/median/longest numbers quoted in the text below
# and reused in cell 5.
vere = sequenze(alto)
finte = sequenze(rng.random(len(alto)) < base)

with avvio.figura("schermo"):
    fig, ax = plt.subplots(figsize=(8, 4))
    bordi = np.logspace(0, np.log10(max(vere.max(), finte.max()) + 1), 16)
    ax.hist(vere, bins=bordi, label="mercato vero")
    ax.hist(finte, bins=bordi, histtype="step", linewidth=1.8, linestyle="--",
            label="giorni indipendenti")
    ax.set_xscale("log")
    ax.set_yscale("log")
    ax.set_xlabel("Durata del periodo agitato (giorni)")
    ax.set_ylabel("Quante volte")
    ax.legend()
    plt.show()

print(f"{'':>22s} {'episodi':>9s} {'mediana':>9s} {'il piu lungo':>14s}")
print(f"{'mercato vero':>22s} {len(vere):9d} {np.median(vere):9.0f} {vere.max():14d}")
print(f"{'giorni indipendenti':>22s} {len(finte):9d} {np.median(finte):9.0f} {finte.max():14d}")

4. Esercizio: la finestra lunga nasconde i regimi

Output

  finestra    minimo   massimo   rapporto   entro ±20%
        10      8.6%    295.7%      34.5x        29.0%
        30     16.7%    183.6%      11.0x        38.6%
        60     26.3%    147.9%       5.6x        46.2%
       120     29.0%    131.9%       4.5x        51.0%
       250     34.8%    113.0%       3.2x        48.3%

Con finestre lunghe l'escursione si comprime e sembra che il mercato sia piu' stabile. Non lo e' diventato: lo stiamo guardando con meno risoluzione.
Espandi lo script di questo passo
lab_09_regimi.py
python
print(f"{'finestra':>10s} {'minimo':>9s} {'massimo':>9s} {'rapporto':>10s} {'entro ±20%':>12s}")
for f in (10, 30, 60, 120, 250):
    v = volatilita_mobile(r, f)
    dentro = float(np.mean((v > v.mean() * 0.8) & (v < v.mean() * 1.2)))
    print(f"{f:10d} {v.min():9.1%} {v.max():9.1%} {v.max() / v.min():9.1f}x {dentro:12.1%}")

print("\nCon finestre lunghe l'escursione si comprime e sembra che il mercato sia "
      "piu' stabile. Non lo e' diventato: lo stiamo guardando con meno risoluzione.")

5. Esercizio: distruggi la struttura e guardala sparire

Output

stessi identici rendimenti, in ordine casuale:

  persistenza a 30 giorni: 31.4% contro 23.2%   (nel mercato vero: 49.1% contro 15.9%)
  episodio agitato piu' lungo: 53 giorni (nel mercato vero: 187)

La struttura non era nei rendimenti presi uno per uno: era nel loro ORDINE. Rimescolarli la distrugge, ed e' la prova che c'era.
Espandi lo script di questo passo
lab_09_regimi.py
python
rimescolati = rng.permutation(r)
vol_finta = volatilita_mobile(rimescolati, FINESTRA)
alto_finto = vol_finta > np.percentile(vol_finta, 75)
seq_finta = sequenze(alto_finto)

oggi_f, dopo_f = alto_finto[:-ORIZZONTE], alto_finto[ORIZZONTE:]

print("stessi identici rendimenti, in ordine casuale:\n")
print(f"  persistenza a {ORIZZONTE} giorni: {float(dopo_f[oggi_f].mean()):.1%} "
      f"contro {float(dopo_f[~oggi_f].mean()):.1%}   (nel mercato vero: "
      f"{da_alto:.1%} contro {da_calmo:.1%})")
print(f"  episodio agitato piu' lungo: {seq_finta.max()} giorni "
      f"(nel mercato vero: {vere.max()})")
print("\nLa struttura non era nei rendimenti presi uno per uno: era nel loro "
      "ORDINE. Rimescolarli la distrugge, ed e' la prova che c'era.")

Attenzione a cosa questo NON dice

La persistenza riguarda quanto il mercato si muoverà, non in che direzione. Sapere che il prossimo mese sarà agitato non ti dice se salirà o scenderà, e chiunque ti presenti la prima informazione facendola passare per la seconda ti sta vendendo qualcosa.

Riproducibilità e download

Eseguito il 2026-08-27 dal quaderno del repository

Il quaderno

  • lab_09_regimi.ipynb13,4 KB

    sha256 ed220b15eadc7c3f1e9517a0e29380c45e2a9aecefa4ddb2fa88c3f441b0c7c5

  • lab_09_regimi.py9,9 KB

    sha256 ca7fce65a4dbcd1848cf41043968bf5efb794d73d537ea5a243b3e187496a003

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