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

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
lab_08_acp.py
python
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.

Quante direzioni indipendenti servono per descrivere tre asset che sembrano tre scommesse.Fonte: Binance Data Vision · Periodo: 2017-08-17 … 2026-06-30 · Metodo: Autovalori della matrice di correlazione delle variazioni giornaliere delle tre serie, espressi come quota della varianza totale e come somma progressiva.

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
lab_08_acp.py
python
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
lab_08_acp.py
python
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
lab_08_acp.py
python
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%.

La stessa misura ripetuta su finestre diverse: quanto e' stabile davvero.Fonte: Binance Data Vision · Periodo: 2017-08-17 … 2026-06-30 · Metodo: Prima componente principale ricalcolata su ogni finestra mobile di 250 giorni, e sua quota di varianza spiegata finestra per finestra.

Output

prima componente: minimo 62.1%, massimo 93.2%, media 82.5%
Espandi lo script di questo passo
lab_08_acp.py
python
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

  1. Togli "solusdt" da NOMI e 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.
  2. Nella quarta cella riduci FINESTRA a 60. La prima componente diventa molto più instabile: quanto di quella instabilità è del mercato e quanto è dell'aver usato meno dati?
  3. 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

Torna all’indice dei lab