Lab del libro · L07
L07 — Correlazioni a finestre mobili e quanto salgono nei crolli
Lab 7 — Quando la diversificazione svanisce
Quaderno del capitolo «Quando la diversificazione svanisce» di La matematica di chi perde.
Tre cose. Prima: il modo intuitivo di misurare la correlazione nei crolli dà la risposta sbagliata, e qui lo vedi succedere. Seconda: la correlazione non sta ferma, e sale proprio quando servirebbe che non salisse. Terza: quanto vale davvero avere N asset, con il conto che quasi nessuno fa.
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.
Output
3 serie allineate, 2128 giorni comuni, dal 2020-09-02 al 2026-06-30
Espandi lo script di questo passo
import datetime as dt
import matplotlib.pyplot as plt
import numpy as np
import polars as pl
from cvbook.dati import carica
from cvbook.metriche import drawdown, rendimenti
NOMI = ["btcusdt", "ethusdt", "solusdt"] # ← PROVA / TRY: aggiungi "ftsemib" o "eni"
# (aggiungili anche a avvio.prepara([...]))
DA = dt.date(2020, 9, 1) # PROVA / TRY: sposta la data d'inizio
serie = [
carica(n).filter(pl.col("data") >= DA).select(["data", "chiusura"]).rename({"chiusura": n})
for n in NOMI
]
tabella = serie[0]
for s in serie[1:]:
tabella = tabella.join(s, on="data")
tabella = tabella.sort("data")
date = tabella["data"].to_list()[1:]
M = np.column_stack([rendimenti(tabella[n].to_numpy()) for n in NOMI])
print(f"{len(NOMI)} serie allineate, {len(M)} giorni comuni, dal {date[0]} al {date[-1]}")1. Il metodo intuitivo, e perché è sbagliato
L'idea naturale: prendo i giorni peggiori e calcolo la correlazione lì. Sembra ovvia, ed è un artefatto — selezionando in base a un valore estremo di una variabile se ne restringe la variabilità, e la correlazione risulta distorta verso il basso.
Output
correlazione media su tutto il periodo: 0.675 correlazione media sul 5% dei giorni peggiori: 0.582 Sembra che nei crolli la correlazione SCENDA. E' falso, ed e' il risultato del modo in cui abbiamo selezionato i giorni.
Espandi lo script di questo passo
coppie = np.triu_indices(len(NOMI), 1)
def correlazione_media(blocco: np.ndarray) -> float:
return float(np.corrcoef(blocco.T)[coppie].mean())
primo = M[:, 0]
peggiori = np.argsort(primo)[: len(primo) // 20] # il 5% dei giorni peggiori
# PROVA / TRY: // 5 per il 20% (vedi esercizio 3)
print(f"correlazione media su tutto il periodo: {correlazione_media(M):.3f}")
print(f"correlazione media sul 5% dei giorni peggiori: {correlazione_media(M[peggiori]):.3f}")
print("\nSembra che nei crolli la correlazione SCENDA. E' falso, ed e' il "
"risultato del modo in cui abbiamo selezionato i giorni.")2. Il metodo corretto: finestre temporali
Si misura la correlazione su finestre mobili e poi si guarda in quali periodi è più alta. Nessuna selezione basata sul valore delle variabili.
La correlazione media fra tre serie misurata su finestre mobili di 60 giorni, dal 2021 al 2026, con l'asse verticale da 0 a 1 e una riga punteggiata sulla media del periodo. Le bande ombreggiate segnano i periodi in cui il mercato sta oltre il 30% sotto il massimo: la correlazione oscilla fra 0,17 e 0,94, vale 0,79 di media dentro quelle bande e 0,68 fuori.
Output
minimo 0.17 massimo 0.94 media 0.73 media nei periodi difficili: 0.79 media nel resto del tempo: 0.68
Espandi lo script di questo passo
FINESTRA = 60 # PROVA / TRY: 20 · 60 · 200 (vedi esercizio 2)
corr = np.array([correlazione_media(M[i - FINESTRA:i]) for i in range(FINESTRA, len(M))])
date_c = date[FINESTRA:]
dd = drawdown(np.concatenate([[1.0], np.cumprod(1 + primo)]))[FINESTRA + 1:]
brutti = dd < -0.30
with avvio.figura("schermo"):
fig, ax = plt.subplots(figsize=(10, 4))
ax.fill_between(date_c, 0, 1, where=brutti, step="mid", alpha=0.25,
label="mercato oltre il 30% sotto il massimo")
ax.plot(date_c, corr, linewidth=1.3, label="correlazione media a 60 giorni")
ax.axhline(corr.mean(), linestyle=":", linewidth=1.2)
ax.set_ylim(0, 1)
ax.set_ylabel("Correlazione media")
ax.legend(loc="lower left")
fig.autofmt_xdate()
plt.show()
print(f"minimo {corr.min():.2f} massimo {corr.max():.2f} media {corr.mean():.2f}")
print(f"media nei periodi difficili: {corr[brutti].mean():.2f}")
print(f"media nel resto del tempo: {corr[~brutti].mean():.2f}")3. Quanto vale davvero avere N asset
La quota di oscillazione che resta, rispetto a possederne uno solo: uno diviso il numero degli asset, più la correlazione moltiplicata per tutto il resto. E poi la radice quadrata.
Output
correlazione media delle finestre mobili (quella del capitolo): 0.73
correlazione statica sull'intero periodo: 0.67
asset indipendenti a corr. 0,3 a corr, 0,73 a corr. 0,9
2 70.7% 80.6% 93.1% 97.5%
3 57.7% 73.0% 90.7% 96.6%
4 50.0% 68.9% 89.5% 96.2%
5 44.7% 66.3% 88.7% 95.9%
10 31.6% 60.8% 87.2% 95.4%
15 25.8% 58.9% 86.7% 95.2%
30 18.3% 56.9% 86.2% 95.0%
numero di asset DAVVERO indipendenti equivalenti ai tuoi 3: 1.22Espandi lo script di questo passo
def oscillazione_residua(n: int, rho: float) -> float:
return float(np.sqrt(1 / n + (1 - 1 / n) * rho))
# Due stimatori della stessa parola. `correlazione_media(M)` è la correlazione
# **statica** su tutto il periodo: 0,675. La media delle finestre mobili della
# cella precedente è 0,73, ed è quella che il capitolo usa in ogni suo conto.
# Non sono la stessa cosa e non vanno confuse — è l'errore che il capitolo
# stesso ha dovuto correggere. Qui si usa quella del capitolo.
#
# Two estimators of the same word: the static correlation over the whole period
# (0.675) and the average of the rolling windows (0.73). The chapter's numbers
# all use the second one, so this table uses it too.
rho_misurata = float(corr.mean())
rho_statica = correlazione_media(M)
print(f"correlazione media delle finestre mobili (quella del capitolo): {rho_misurata:.2f}")
print(f"correlazione statica sull'intero periodo: {rho_statica:.2f}\n")
etichetta_misurata = f"a corr. {rho_misurata:.2f}".replace(".", ",")
print(f"{'asset':>6s} {'indipendenti':>14s} {'a corr. 0,3':>14s} "
f"{etichetta_misurata:>14s} {'a corr. 0,9':>14s}")
for n in (2, 3, 4, 5, 10, 15, 30):
valori = "".join(f"{oscillazione_residua(n, rho) * 100:13.1f}%"
for rho in (0.0, 0.3, rho_misurata, 0.9))
print(f"{n:6d} {valori}")
# `len(NOMI)` e non un 3 scritto a mano: la cella sopra invita ad aggiungere
# serie, e con un 3 fisso il numero effettivo restava quello di tre asset
# qualunque cosa il lettore ci mettesse dentro.
quanti = len(NOMI)
n_eff = 1 / (1 / quanti + (1 - 1 / quanti) * rho_misurata)
print(f"\nnumero di asset DAVVERO indipendenti equivalenti ai tuoi {quanti}: {n_eff:.2f}")Esercizi
- Nella terza cella guarda la colonna della correlazione misurata: fra 4 e 15 asset la riduzione cambia di pochissimo. Dal quinto in poi paghi costi e complessità senza comprare protezione.
- Cambia
FINESTRAda 60 a 20 e poi a 200. Con finestre corte la correlazione balla molto di più: quanto della sua instabilità è del mercato e quanto è della misura? - Riesegui la prima cella prendendo il 20% dei giorni peggiori invece del 5%. L'artefatto si attenua. È la dimostrazione che era un effetto della selezione e non un fatto del mercato.
Riproducibilità e download
Eseguito il 2026-08-27 dal quaderno del repository
Il quaderno
lab_07_correlazioni.ipynb11,6 KB
sha256 23abfc4d315b8d34888ba085321b92b4959243c11ba7d4c2c99eeb12af4f961d
lab_07_correlazioni.py8,9 KB
sha256 a57c7a11076a5bb23f77e555fc3407044a3edd3ae2732805344beb26e2ff2d48
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