Lab del libro · L08
L08 — Componenti principali e numero effettivo di scommesse
Lab 8 — Quante cose stai davvero comprando
Quaderno del capitolo «Quante cose stai davvero comprando» di La matematica di chi perde.
L'analisi delle componenti principali risponde a una domanda che nessun'altra misura pone: quante direzioni indipendenti servono per descrivere il movimento di un portafoglio.
L'esercizio che vale il quaderno è il secondo: aggiungere asset uno alla volta e guardare il numero non muoversi. È controintuitivo finché non lo si vede.
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
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 rendimenti
def allinea(nomi: list[str], da: dt.date = dt.date(2020, 9, 1)) -> np.ndarray:
"""Rendimenti giornalieri delle serie, sui soli giorni presenti in tutte."""
pezzi = [
carica(n).filter(pl.col("data") >= da).select(["data", "chiusura"]).rename({"chiusura": n})
for n in nomi
]
base = pezzi[0]
for p in pezzi[1:]:
base = base.join(p, on="data")
base = base.sort("data")
return np.column_stack([rendimenti(base[n].to_numpy()) for n in nomi])1. Le componenti, sui tre asset del capitolo
Si parte dalla matrice di correlazione e se ne prendono gli autovalori: sono le quote di movimento spiegate da ciascuna direzione indipendente. Tre righe di codice, e non serve alcuna libreria specialistica.
Due pannelli affiancati sulle tre componenti principali di btcusdt, ethusdt e solusdt. A sinistra tre barre con la quota di movimento spiegata da ciascuna, etichettate 78%, 15% e 6%. A destra la stessa quota sommata via via, da 78% con una sola componente a 100% con tutte e tre, con una riga punteggiata sul 90%: la si supera con due componenti.
Output
componente 1: 78.5% cumulata 78.5% componente 2: 15.3% cumulata 93.8% componente 3: 6.2% cumulata 100.0% componenti necessarie per arrivare al 90%: 2 giorni usati: 2128 — il calendario delle sole cripto, che non chiudono mai Il capitolo stampa 78,7% perche' misura queste stesse tre serie sui soli giorni in cui e' aperta anche la borsa di Milano: e' l'unico modo di confrontarle con il paniere esteso. Due decimi di punto di differenza, e il numero effettivo di scommesse — 1,55 — e' identico.
Espandi lo script di questo passo
NOMI = ["btcusdt", "ethusdt", "solusdt"]
# PROVA / TRY: togli "solusdt" (esercizio 1) · aggiungi "ftsemib"/"eni"
# (aggiungili anche a avvio.prepara([...]))
M = allinea(NOMI)
C = np.corrcoef(M.T)
autovalori = np.linalg.eigvalsh(C)[::-1]
quote = autovalori / autovalori.sum()
cumulata = np.cumsum(quote)
with avvio.figura("schermo"):
fig, (sx, dx) = plt.subplots(1, 2, figsize=(11, 4))
x = np.arange(1, len(quote) + 1)
sx.bar(x, quote * 100)
for k, q in enumerate(quote):
sx.annotate(f"{q:.0%}", xy=(k + 1, q * 100), xytext=(0, 4),
textcoords="offset points", ha="center")
sx.set_xticks(x)
sx.set_xlabel("Componente")
sx.set_ylabel("Varianza spiegata (%)")
dx.plot(x, cumulata * 100, marker="o")
dx.axhline(90, linestyle=":", linewidth=1.2)
dx.set_xticks(x)
dx.set_xlabel("Componenti usate")
dx.set_ylabel("Varianza cumulata (%)")
plt.show()
for k, (q, c) in enumerate(zip(quote, cumulata), start=1):
print(f"componente {k}: {q:6.1%} cumulata {c:6.1%}")
print(f"\ncomponenti necessarie per arrivare al 90%: {int(np.searchsorted(cumulata, 0.90)) + 1}")
print(f"giorni usati: {len(M)} — il calendario delle sole cripto, che non chiudono mai")
print("Il capitolo stampa 78,7% perche' misura queste stesse tre serie sui soli "
"giorni in cui e' aperta anche la borsa di Milano: e' l'unico modo di "
"confrontarle con il paniere esteso. Due decimi di punto di differenza, "
"e il numero effettivo di scommesse — 1,55 — e' identico.")La prima componente — una sola direzione, cioè sostanzialmente «oggi il settore sale o scende» — spiega la gran parte di tutto ciò che accade. Le differenze fra i tre asset stanno in quel che resta.
2. Il numero effettivo di scommesse
Un numero solo al posto del grafico: si sommano i quadrati delle quote e si prende l'inverso. Se le componenti pesassero tutte uguale darebbe il numero degli asset; se una sola pesasse tutto darebbe uno.
Output
numero effettivo di scommesse (componenti): 1.55 numero effettivo di scommesse (correlazione media): 1.28 Due metodi con assunzioni diverse. Il secondo assume che tutte le coppie abbiano la stessa correlazione; il primo vede la struttura reale. Quando concordano sull'ordine di grandezza, la conclusione e' molto piu' solida.
Espandi lo script di questo passo
def numero_effettivo(nomi: list[str]) -> tuple[float, float]:
m = allinea(nomi)
c = np.corrcoef(m.T)
v = np.linalg.eigvalsh(c)[::-1]
q = v / v.sum()
rho = float(c[np.triu_indices(len(nomi), 1)].mean())
da_correlazione = 1.0 / (1 / len(nomi) + (1 - 1 / len(nomi)) * rho)
return float(1.0 / np.sum(q**2)), da_correlazione
da_componenti, da_correlazione = numero_effettivo(NOMI)
print(f"numero effettivo di scommesse (componenti): {da_componenti:.2f}")
print(f"numero effettivo di scommesse (correlazione media): {da_correlazione:.2f}")
print("\nDue metodi con assunzioni diverse. Il secondo assume che tutte le coppie "
"abbiano la stessa correlazione; il primo vede la struttura reale. Quando "
"concordano sull'ordine di grandezza, la conclusione e' molto piu' solida.")3. L'esercizio: aggiungi asset e guarda il numero non muoversi
Output
portafoglio asset scommesse effettive
btcusdt 1 1.00
btcusdt + ethusdt 2 1.21
btcusdt + ethusdt + solusdt 3 1.55
Aggiungere asset dello stesso tipo non aggiunge dimensioni: aggiunge costi e cose da seguire.Espandi lo script di questo passo
print(f"{'portafoglio':>34s} {'asset':>6s} {'scommesse effettive':>20s}")
for k in range(1, len(NOMI) + 1):
sottoinsieme = NOMI[:k]
if k == 1:
effettivo = 1.0
else:
effettivo, _ = numero_effettivo(sottoinsieme)
print(f"{' + '.join(sottoinsieme):>34s} {k:6d} {effettivo:20.2f}")
print("\nAggiungere asset dello stesso tipo non aggiunge dimensioni: aggiunge "
"costi e cose da seguire.")4. Stabilità nel tempo
I limiti vanno guardati, non nominati. Le componenti calcolate su un periodo tranquillo possono essere diverse da quelle calcolate su un periodo di stress: la buona pratica è calcolarle su più finestre e vedere quanto sono stabili.
Una linea puntinata che segue la varianza spiegata dalla prima componente lungo circa settanta finestre mobili di 250 giorni, con l'asse verticale da poco sotto 70 a 100. Il valore non sta fermo: oscilla fra un minimo di 62% e un massimo di 93%, attorno a una media di 82%.
Output
prima componente: minimo 62.1%, massimo 93.2%, media 82.5%
Espandi lo script di questo passo
FINESTRA = 250 # PROVA / TRY: 60 (vedi esercizio 2) · 250
prime = []
for i in range(FINESTRA, len(M), 25):
blocco = M[i - FINESTRA:i]
v = np.linalg.eigvalsh(np.corrcoef(blocco.T))[::-1]
prime.append(v[0] / v.sum())
prime = np.array(prime)
with avvio.figura("schermo"):
fig, ax = plt.subplots(figsize=(9, 3.5))
ax.plot(prime * 100, marker="o", markersize=3)
ax.set_xlabel(f"Finestre mobili di {FINESTRA} giorni")
ax.set_ylabel("Varianza spiegata dalla prima componente (%)")
ax.set_ylim(0, 100)
plt.show()
print(f"prima componente: minimo {prime.min():.1%}, massimo {prime.max():.1%}, "
f"media {prime.mean():.1%}")Esercizi
- Togli
"solusdt"daNOMIe riesegui: con due soli asset la prima componente spiega ancora di più. Non è un miglioramento della misura, è che con meno serie c'è meno struttura da trovare. - Nella quarta cella riduci
FINESTRAa 60. La prima componente diventa molto più instabile: quanto di quella instabilità è del mercato e quanto è dell'aver usato meno dati? - Applica lo stesso codice ai tuoi indicatori invece che agli asset. Se due o tre componenti spiegano quasi tutto, i tuoi otto indicatori stanno misurando la stessa cosa in modi leggermente diversi.
Riproducibilità e download
Eseguito il 2026-08-27 dal quaderno del repository
Il quaderno
lab_08_acp.ipynb12,9 KB
sha256 cf4cc6adca958b2808a04cfc54b7fb88a4037aa6c5447c9cb32a781ecd5ae412
lab_08_acp.py9,6 KB
sha256 a133b07afe789c2afca16b695cb449e25c85ccc56483454c8adcc797397ac835
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