← Torna agli articoli
March 19, 2026
5 min di lettura

Arbitraggio Statistico e Pairs Trading nei Mercati Crypto: Dalla Cointegrazione al Filtro di Kalman

Arbitraggio Statistico e Pairs Trading nei Mercati Crypto: Dalla Cointegrazione al Filtro di Kalman
#stat arb
#pairs trading
#cointegrazione
#kalman
#arbitraggio
#algo trading
#crypto

Nel 1987, un gruppo di fisici presso Morgan Stanley guadagnò 50 milioni di dollari in un solo anno facendo trading su coppie di azioni con un algoritmo che nessuno di loro riusciva a spiegare completamente al management della banca. Il management non obiettò. Nel 2026, puoi lanciare la stessa idea sulle exchange crypto — con futures perpetui, un mercato 24/7 e liquidità che Nunzio Tartaglia avrebbe invidiato. Ma c'è un ostacolo: ciò che funzionava con le azioni Ford e GM nell'era pre-internet richiede un serio adattamento per un mondo in cui BTC può scendere del 20% in una notte e il funding rate può invertirsi nel giro di un singolo blocco.

Questo articolo è un'analisi completa dell'arbitraggio statistico e del pairs trading per i mercati crypto. Dalla teoria matematica (cointegrazione, processo di Ornstein-Uhlenbeck, filtro di Kalman) al codice Python funzionante che puoi eseguire su dati reali. Lo stile è orientato all'ingegneria: spieghiamo le formule, mostriamo il codice e non nascondiamo le insidie.

1. Una Breve Storia: Dal Gesuita ai Quant

L'arbitraggio statistico nella sua forma moderna nacque nel trading desk di Morgan Stanley a metà degli anni '80. Nunzio Tartaglia — ex sacerdote gesuita con un dottorato in fisica — assemblò un team di matematici, fisici e informatici. L'obiettivo: trovare nei prezzi azionari pattern che i trader tradizionali non riuscivano a vedere.

L'idea era disarmantemente semplice. Se le azioni di Coca-Cola e Pepsi si muovono storicamente insieme (il che ha senso — vendono la stessa acqua zuccherata in bottiglie di colore diverso), allora una divergenza nei loro prezzi è un'anomalia temporanea. Compra il ritardatario, vendi il leader, aspetta la convergenza, incassa il profitto. Una strategia market-neutral: la direzione del mercato non ci riguarda.

Il team di Tartaglia includeva persone che avrebbero poi trasformato l'intera Wall Street:

  • David Shaw — fondò in seguito D.E. Shaw & Co., uno dei più grandi hedge fund quantitativi
  • Peter Muller — fondò PDT Partners, il leggendario gruppo di stat arb all'interno di Morgan Stanley
  • Robert Frey — in seguito si unì a Renaissance Technologies sotto Jim Simons

Il gruppo operava come un laboratorio di ricerca all'interno di una banca d'investimento. L'automazione era all'avanguardia: i cluster VAX generavano segnali e l'esecuzione avveniva tramite terminali. Negli anni migliori (1987-1988), la strategia guadagnò decine di milioni. Poi arrivarono due anni consecutivi in perdita, e nel 1989 Morgan Stanley chiuse il desk.

Ma l'idea si era già diffusa. Gli ex membri del gruppo portarono il concetto di pairs trading su tutta Wall Street. Gatev, Goetzmann e Rouwenhorst pubblicarono il classico paper accademico nel 2006 — "Pairs Trading: Performance of a Relative-Value Arbitrage Rule" — dimostrando che una semplice strategia di pairs trading aveva fornito costantemente rendimenti annuali di circa l'11% dal 1962 al 2002 sulle azioni statunitensi. Era una risposta convincente all'ipotesi di mercato efficiente: il mercato nel suo insieme può essere efficiente, ma le coppie di asset specifici si discostano sistematicamente dall'equilibrio.

Oggi, l'arbitraggio statistico è un settore con centinaia di miliardi di dollari in AUM, e i mercati crypto offrono un terreno particolarmente fertile: liquidità frammentata, microstruttura immatura, trading round-the-clock e futures perpetui con funding rate — uno strumento che semplicemente non esiste nei mercati tradizionali.

2. Il Fondamento Matematico: La Correlazione È una Trappola

Perché la Correlazione Non Funziona

Partiamo dall'errore che fa ogni secondo quant alle prime armi: "BTC ed ETH sono correlati con un coefficiente di 0.85, quindi possiamo fare trading sulla coppia." No. Non puoi. Beh, puoi — ma perderai denaro.

La correlazione misura la relazione lineare tra i rendimenti di due asset. Due asset possono essere perfettamente correlati, eppure i loro prezzi divergono per sempre. L'esempio classico: due random walk con incrementi correlati — divergono indefinitamente nonostante l'elevata correlazione. Aprirai una posizione aspettando una "convergenza" che non arriverà mai.

Cointegration vs correlation

Cointegrazione: L'Approccio Corretto

La cointegrazione è una proprietà delle serie dei prezzi, non dei rendimenti. Due serie non stazionarie X(t) e Y(t) sono dette cointegrate se esiste una combinazione lineare:

S(t) = Y(t) - β · X(t)

che è stazionaria — ovvero ritorna a un valore medio. Il coefficiente β è chiamato hedge ratio e S(t) è lo spread.

Intuizione: BTC ed ETH possono schizzare sulla luna o precipitare in un abisso, ma se la loro differenza (opportunamente scalata) oscilla attorno a un livello fisso — questa è cointegrazione. Ed è esattamente ciò di cui abbiamo bisogno per il trading.

Il Test di Engle-Granger (1987)

Una procedura in due fasi per la quale Robert Engle e Clive Granger hanno ricevuto il Premio Nobel per l'Economia nel 2003:

Passo 1. Regressione OLS: Y(t) = α + β · X(t) + ε(t). Otteniamo l'hedge ratio β e i residui ε(t).

Passo 2. Test ADF (Augmented Dickey-Fuller) sui residui ε(t). Ipotesi nulla: ε(t) ha una radice unitaria (non stazionaria). Se il p-value < 0.05, rifiutiamo H₀ — le serie sono cointegrate.

Importante: per il test di cointegrazione non puoi usare i valori critici ADF standard. I valori critici di Engle-Granger sono stati derivati tramite simulazione Monte Carlo e tengono conto della dipendenza tra le variabili nella regressione OLS. In statsmodels, questo è correttamente implementato nella funzione coint().

Il Test di Johansen

Per sistemi con più di due variabili (ad es., BTC, ETH e SOL simultaneamente), si usa il test di Johansen. Trova tutte le relazioni di cointegrazione nel sistema e consente di costruire portafogli di asset multipli. Il test è basato su un modello VAR (vector autoregression) e usa due criteri: la statistica della traccia e la statistica del massimo autovalore.

Il Processo di Ornstein-Uhlenbeck

Se lo spread è cointegrato, la sua dinamica può essere modellata come un processo di Ornstein-Uhlenbeck (OU):

dS(t) = θ(μ - S(t))dt + σ dW(t)

dove:

  • θ — la velocità di mean reversion
  • μ — il livello medio a lungo termine
  • σ — la volatilità
  • W(t) — un processo di Wiener (moto browniano)

Dai parametri del processo OU si calcola l'emivita della mean reversion:

t½ = ln(2) / θ

L'emivita è una metrica cruciale. Se t½ = 5 giorni, lo spread ritorna alla media in circa 5 giorni. Se t½ = 200 giorni, starai in una posizione per mezzo anno ad aspettare la convergenza. Per le strategie crypto, l'emivita ottimale è 1-30 giorni. Più breve — troppo veloce, le commissioni mangiano il profitto. Più lunga — troppo lenta, rischio di cambiamento strutturale di regime.

In pratica, θ viene stimato tramite regressione:

ΔS(t) = a + b · S(t-1) + ε(t)

dove θ = -b, e t½ = -ln(2) / b.

Normalizzazione Z-Score

Per generare segnali di trading, lo spread viene normalizzato:

z(t) = (S(t) - μ̂) / σ̂

dove μ̂ e σ̂ sono la media mobile e la deviazione standard dello spread. Lo z-score indica di quante deviazioni standard lo spread si è discostato dalla media. Soglie tipiche di ingresso: |z| > 2.0; soglie di uscita: |z| < 0.5.

3. Selezione delle Coppie nel Mercato Crypto

BTC-ETH: Il Classico Che (a Volte) Funziona

BTC ed ETH sono la coppia più ovvia e più liquida. La correlazione dei rendimenti è costantemente superiore a 0.7. Ma la cointegrazione è un'altra storia. Appare e scompare:

  • Durante i mercati laterali del 2023, BTC/ETH erano affidabilmente cointegrati (p-value < 0.01)
  • Durante la divergenza del 2024-2025 (BTC saliva sui flussi ETF, ETH rimaneva indietro), la cointegrazione si è interrotta
  • All'inizio del 2026, dopo il lancio dell'ETH ETF e il recupero del rapporto ETH/BTC, la cointegrazione si è nuovamente stabilizzata

Conclusione: la cointegrazione deve essere monitorata continuamente. I parametri di regressione vengono ricalcolati su una finestra mobile e la strategia si disattiva automaticamente se il p-value del test ADF supera la soglia.

Coppie di Settore

Il mercato crypto è convenientemente segmentato per settore e le coppie intra-settore mostrano spesso una cointegrazione stabile:

Settore Coppie Esempio Caratteristiche
Blockchain L1 SOL/AVAX, NEAR/APT Alta liquidità, emivita 3-10 giorni
Protocolli DeFi AAVE/COMP, UNI/SUSHI Liquidità media, emivita 5-15 giorni
Soluzioni L2 ARB/OP, MATIC/MANTA Alta volatilità dello spread
Memecoins DOGE/SHIB Imprevedibili ma divertenti (sconsigliato)

Le migliori coppie per lo stat arb hanno tre proprietà: (1) cointegrazione stabile su una finestra storica >6 mesi, (2) liquidità sufficiente — volume giornaliero >$10M per asset, (3) emivita ragionevole — da 1 a 30 giorni.

Spot vs Futures Perpetui (Basis)

Una categoria separata di "coppie" è lo stesso asset sui mercati spot e futures. La differenza tra il prezzo dei futures perpetui e il prezzo spot (il basis) è stazionaria per definizione: il meccanismo del funding rate la comprime nuovamente verso zero. Questo rende il basis trading una delle forme più affidabili di stat arb nel crypto.

4. Tre Approcci di Trading

A. Basis Trading: Spot-Futures e Carry del Funding Rate

La forma "più pura" di stat arb nel crypto. La meccanica:

  1. Acquista l'asset spot (ad es., 1 BTC)
  2. Apri una posizione short sul futures perpetuo (1 BTC)
  3. Se il funding rate è positivo (i long pagano gli short) — ricevi funding ogni 8 ore

Con un funding rate medio dello 0.01% ogni 8 ore, si tratta di circa lo 0.03% al giorno o dell'~11% annualizzato senza rischio direzionale. Durante i mercati rialzisti, il funding rate può salire allo 0.05-0.1% ogni 8 ore — già il 55-110% annualizzato.

Rischi: funding negativo (il mercato si inverte), liquidazione della posizione short durante un forte rally del prezzo (necessario buffer di margine) e commissioni di exchange.

Al marzo 2026, il funding rate medio di BTC si è stabilizzato a circa ~0.015% per 8 ore — circa il 50% superiore ai livelli del 2024.

B. Arbitraggio Cross-Exchange

Stessa coin, due exchange, prezzi diversi. Il motivo — differenze di liquidità, composizione dei trader e velocità di aggiornamento del book degli ordini.

Esempio: BTC su Binance: 87,150.BTCsuBybit:87,150. BTC su Bybit: 87,175. Spread: $25 (0.029%).

Strategia: acquista su Binance, vendi su Bybit. Problema: quando entrambi gli ordini vengono eseguiti, lo spread potrebbe essere scomparso. Soluzione: mantenere saldi su entrambe le exchange ed eseguire simultaneamente.

Commissioni tipiche:

  • Binance: ~0.075% taker (con sconto ~0.05%)
  • Bybit: ~0.03% taker (VIP)
  • Totale: ~0.08%

Ciò significa che lo spread deve superare lo 0.08% affinché la strategia sia redditizia. Nel 2026, tali spread si verificano:

  • Su coppie meno liquide (altcoin) — regolarmente
  • Su coppie principali (BTC, ETH) — solo durante momenti di alta volatilità
  • Tra CEX e DEX — più frequentemente, ma con rischio MEV e slippage

Senza co-location, la latenza API è di 10-100 ms. Con reti ottimizzate — ~1 ms. La maggior parte dei trader retail opera nell'intervallo 100-500 ms, sufficiente per molte strategie di arbitraggio ma insufficiente per competere con le istituzioni.

C. Pairs Trading con Leva

Pairs trading classico su due asset diversi usando la leva. Questa è la più complessa delle tre strategie — e quella potenzialmente più redditizia.

Meccanica usando la coppia SOL/AVAX come esempio:

  1. Calcola l'hedge ratio β (ad es., β = 1.3)
  2. Quando z-score > +2: short SOL, long AVAX × β
  3. Quando z-score < -2: long SOL, short AVAX × β
  4. Uscita: |z-score| < 0.5 o timeout (ad es., 30 giorni)

Con 3x di leva su ogni leg e una reversione media dello spread di 2σ → 3σ:

  • Rendimento target per trade: ~3-6%
  • Frequenza media: 2-4 trade al mese per coppia
  • Rendimento annuale atteso: 30-60% (al lordo di commissioni e slippage)

Il rischio principale: la correlazione può interrompersi nel momento peggiore (di solito durante un crollo del mercato). Maggiori dettagli nella sezione 8.

5. Filtro di Kalman per l'Hedge Ratio Adattivo

Perché un Hedge Ratio Statico È un Problema

L'approccio classico: stimare β tramite OLS su una finestra storica e fissarlo. Il problema: β cambia nel tempo. Il mercato crypto è particolarmente non stazionario — i cambiamenti di narrativa (DeFi summer → hype NFT → token AI) alterano le relazioni fondamentali tra gli asset.

Usare OLS rolling (regressione mobile) è una mezza misura. Devi scegliere la lunghezza della finestra: troppo breve — rumore; troppo lunga — lag. Il filtro di Kalman risolve questo problema elegantemente.

Kalman filter

Modello Spazio-Stato

Rappresentiamo la relazione tra Y(t) e X(t) come un modello lineare con coefficienti variabili nel tempo:

Equazione di osservazione:

Y(t) = α(t) + β(t) · X(t) + ε(t),   ε(t) ~ N(0, R)

Equazione di stato:

[α(t+1), β(t+1)]ᵀ = [α(t), β(t)]ᵀ + w(t),   w(t) ~ N(0, Q)

I parametri α(t) e β(t) sono trattati come uno stato nascosto che deriva lentamente (random walk). Il filtro di Kalman stima ottimalmente questo stato nascosto dalle osservazioni rumorose.

  • R (rumore di osservazione) — la varianza del rumore di osservazione. Maggiore è R, più lentamente il filtro risponde ai nuovi dati.
  • Q (rumore di stato) — la matrice di covarianza del rumore di stato. Maggiore è Q, più velocemente il filtro si adatta.

Il rapporto Q/R determina la "fluidità" del filtro — analogamente alla scelta della lunghezza della finestra nell'OLS rolling, ma senza troncamento rigido dei dati.

Vantaggi Rispetto all'OLS Rolling

Gli spread calcolati usando il filtro di Kalman sono significativamente più stazionari e mean-reverting rispetto agli spread della regressione rolling. Il filtro di Kalman usa tutte le osservazioni passate con pesi esponenzialmente decrescenti, anziché troncare i dati a una lunghezza di finestra fissa. Inoltre, il filtro di Kalman non richiede la calibrazione di un parametro "lunghezza finestra" — invece, calibra automaticamente l'equilibrio tra inerzia e adattività attraverso le matrici Q e R.

Implementazione con filterpy

import numpy as np
from filterpy.kalman import KalmanFilter

def create_kalman_filter(
    delta: float = 1e-4,
    obs_noise: float = 1.0
) -> KalmanFilter:
    """
    Crea un filtro di Kalman per la stima adattiva dell'hedge ratio.

    delta: varianza del rumore di stato (Q = delta * I).
           Delta maggiore → adattamento più rapido, più rumore.
    obs_noise: varianza del rumore di osservazione (R).
    """
    kf = KalmanFilter(dim_x=2, dim_z=1)

    kf.x = np.zeros((2, 1))

    kf.F = np.eye(2)

    kf.P = np.eye(2) * 1000

    kf.Q = np.eye(2) * delta

    kf.R = np.array([[obs_noise]])

    return kf

def estimate_hedge_ratio(
    prices_y: np.ndarray,
    prices_x: np.ndarray,
    delta: float = 1e-4,
    obs_noise: float = 1.0
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
    """
    Stima l'hedge ratio adattivo usando un filtro di Kalman.

    Restituisce:
        alphas: array di intercette (α)
        betas: array di hedge ratio (β)
        spreads: array di spread Y - α - β*X
    """
    n = len(prices_y)
    kf = create_kalman_filter(delta, obs_noise)

    alphas = np.zeros(n)
    betas = np.zeros(n)
    spreads = np.zeros(n)

    for t in range(n):
        kf.H = np.array([[1.0, prices_x[t]]])

        kf.predict()

        kf.update(np.array([[prices_y[t]]]))

        alphas[t] = kf.x[0, 0]
        betas[t] = kf.x[1, 0]
        spreads[t] = prices_y[t] - kf.x[0, 0] - kf.x[1, 0] * prices_x[t]

    return alphas, betas, spreads

Il parametro delta è fondamentale. Per coppie crypto con alta volatilità (memecoins, alt a bassa capitalizzazione), usa delta = 1e-3. Per coppie stabili (BTC/ETH, SOL/AVAX) — delta = 1e-5.

6. Segnali di Ingresso e Uscita

Soglie Z-Score

Logica del segnale di base:

def generate_signals(
    spreads: np.ndarray,
    lookback: int = 60,
    entry_z: float = 2.0,
    exit_z: float = 0.5,
    stop_z: float = 4.0
) -> np.ndarray:
    """
    Genera segnali di trading basati sullo z-score dello spread.

    Restituisce array: +1 (long spread), -1 (short spread), 0 (flat)
    """
    signals = np.zeros(len(spreads))
    position = 0

    for t in range(lookback, len(spreads)):
        window = spreads[t - lookback:t]
        mu = np.mean(window)
        sigma = np.std(window)

        if sigma < 1e-10:
            continue

        z = (spreads[t] - mu) / sigma

        if position == 0:
            if z > entry_z:
                position = -1  # Short spread (short Y, long X)
            elif z < -entry_z:
                position = 1   # Long spread (long Y, short X)
        else:
            if position == 1 and z > -exit_z:
                position = 0
            elif position == -1 and z < exit_z:
                position = 0
            elif abs(z) > stop_z:
                position = 0

        signals[t] = position

    return signals

Filtri Momentum

Un segnale puro di mean reversion può essere migliorato con dei filtri:

  1. Filtro momentum: non aprire una posizione se lo spread continua a divergere. Aspetta che lo spread inverta prima di entrare. Tecnicamente: lo z-score ha superato la soglia, ma la variazione corrente dello spread è già diretta verso la media.

  2. Filtro volatilità: aumenta la soglia di ingresso durante i periodi di alta volatilità. Quando il mercato entra in panico, lo z-score può rimanere sopra 3σ per settimane.

  3. Filtro cointegrazione: prima di ogni trade, verifica che la cointegrazione sia ancora valida (test ADF rolling). Se p-value > 0.1 — pausa nel trading.

Uscite Basate sul Tempo

Se una posizione è aperta da più di 2× l'emivita e lo spread non è tornato alla media — chiudila forzatamente. Se lo spread non è tornato entro 2× il tempo atteso, la cointegrazione si è probabilmente interrotta e non c'è nulla da aspettare.

7. Backtesting: Farlo nel Modo Giusto

Analisi Walk-Forward

Un backtest standard (addestramento su tutti i dati → test su tutti i dati) è inutile per lo stat arb. I parametri di regressione sono overfittati ai dati e il risultato sarà ottimistico.

Approccio walk-forward:

  1. Dividi i dati in periodi: [train₁ → test₁] → [train₂ → test₂] → ...
  2. Su ogni periodo di train: stima la cointegrazione, calcola l'hedge ratio, seleziona le soglie z-score
  3. Sul periodo di test: fai trading con parametri fissi
  4. Combina tutti i periodi di test per la valutazione finale

Configurazione tipica per crypto: train = 180 giorni, test = 30 giorni, step = 30 giorni.

Spread strategy backtest

Modello dei Costi di Transazione

Per il crypto, devi tenere conto di:

Componente Valore Tipico Commento
Commissione maker 0.02% Ordini limit
Commissione taker 0.05-0.075% Ordini market
Slippage 0.01-0.1% Dipende dalla liquidità
Funding rate ±0.01%/8h Per posizioni futures
Spread (bid-ask) 0.01-0.05% Sulle principali exchange

L'ingresso e l'uscita da una posizione in coppia coinvolge 4 trade (2 leg × ingresso + uscita). Costi totali: ~0.3-0.5% per round trip. Ciò significa che il profitto medio per trade deve superare lo 0.5% per avere un valore atteso positivo.

Modello di Slippage

Modello lineare: slippage = k × (order_size / ADV), dove ADV è il volume giornaliero medio. Per il crypto, k ≈ 0.1 per le prime 10 coin e k ≈ 0.3-0.5 per le altcoin.

Un modello più realistico è l'impatto a radice quadrata: slippage = k × sqrt(order_size / ADV). Riflette meglio la reale microstruttura del mercato.

Metriche

def calculate_metrics(returns: np.ndarray, rf: float = 0.04) -> dict:
    """
    Calcola le metriche chiave della strategia.
    rf: tasso privo di rischio (annuale)
    """
    daily_rf = rf / 365
    excess = returns - daily_rf

    ann_return = np.mean(returns) * 365
    ann_vol = np.std(returns) * np.sqrt(365)

    sharpe = (ann_return - rf) / ann_vol if ann_vol > 0 else 0

    cumulative = np.cumprod(1 + returns)
    running_max = np.maximum.accumulate(cumulative)
    drawdowns = (cumulative - running_max) / running_max
    max_dd = np.min(drawdowns)

    calmar = ann_return / abs(max_dd) if max_dd != 0 else 0

    win_rate = np.mean(returns > 0) if len(returns) > 0 else 0

    gains = returns[returns > 0].sum()
    losses = abs(returns[returns < 0].sum())
    profit_factor = gains / losses if losses > 0 else float('inf')

    return {
        'annual_return': f'{ann_return:.1%}',
        'annual_volatility': f'{ann_vol:.1%}',
        'sharpe_ratio': f'{sharpe:.2f}',
        'max_drawdown': f'{max_dd:.1%}',
        'calmar_ratio': f'{calmar:.2f}',
        'win_rate': f'{win_rate:.1%}',
        'profit_factor': f'{profit_factor:.2f}',
    }

Benchmark per lo stat arb crypto:

  • Sharpe > 1.5 — una buona strategia
  • Max drawdown < 15% — rischio accettabile
  • Calmar > 2.0 — eccellente rapporto rendimento/drawdown
  • Profit factor > 1.5 — vantaggio sostenibile

8. Problemi del Mondo Reale

Slippage e Liquidità

In un backtest, entri istantaneamente al mid-price. Nella realtà — no. Su altcoin con volume giornaliero di 5M,unordineda5M, un ordine da 50K può spostare il prezzo dello 0.2-0.5%. Per una strategia in coppia, quello è slippage raddoppiato (due leg), e può divorare tutto il profitto.

Soluzione: usa ordini limit (maker, non taker), suddividi gli ordini in parti (TWAP/VWAP) e limita rigorosamente la dimensione della posizione rispetto all'ADV (massimo 1-2% del volume giornaliero).

Rischio del Funding Rate

Con il basis trading, ricevi il funding rate, ma può diventare negativo. Nel mercato ribassista di dicembre 2022, il funding rate di BTC era -0.02% ogni 8 ore — se eri in una posizione "long spot + short perp", pagavi 60/giornoper60/giorno per 100K di posizione.

Protezione: monitora il funding rate in tempo reale e chiudi la posizione quando il tasso si inverte. Un approccio più avanzato è l'arbitraggio del funding rate tra exchange (long sull'exchange con funding basso, short sull'exchange con funding alto).

Rottura della Correlazione in una Crisi

Marzo 2020, maggio 2021, novembre 2022, agosto 2024 — in ogni crollo crypto, le correlazioni si rompono. Più precisamente, le correlazioni si intensificano (tutto cade insieme), ma la cointegrazione si interrompe — lo spread può volare a 10σ e non tornare mai.

Questo è il tallone d'Achille del pairs trading. La strategia guadagna piccole cifre costantemente, poi perde una grande somma in un singolo giorno. Il classico profilo "raccogliere centesimi davanti a uno schiacciasassi".

Protezione:

  1. Stop-loss rigoroso: chiudi la posizione quando z-score > 4σ
  2. Limiti di leva: massimo 2-3x su ogni leg
  3. Filtro VIX/volatilità: riduci la dimensione della posizione quando la volatilità implicita è alta
  4. Diversificazione: fai trading su 10-20 coppie simultaneamente, non mettere tutto su una

Requisiti di Capitale

Per lo stat arb crypto serio:

  • Basis trading: da $50K (su una coppia, una exchange)
  • Arbitraggio cross-exchange: da $100K (saldi su due exchange)
  • Portafoglio pairs trading (10 coppie): da $200K
  • Livello istituzionale: da $1M

Con importi inferiori, le commissioni e le dimensioni minime delle posizioni rendono la strategia non praticabile.

9. Implementazione Python End-to-End

Recupero dei Dati

import ccxt
import pandas as pd
import numpy as np
from datetime import datetime, timedelta

def fetch_ohlcv(
    exchange_id: str,
    symbol: str,
    timeframe: str = '1h',
    days: int = 365
) -> pd.DataFrame:
    """Recupera dati OHLCV tramite ccxt."""
    exchange = getattr(ccxt, exchange_id)({
        'enableRateLimit': True,
    })

    since = int((datetime.now() - timedelta(days=days)).timestamp() * 1000)
    all_candles = []

    while True:
        candles = exchange.fetch_ohlcv(
            symbol, timeframe, since=since, limit=1000
        )
        if not candles:
            break
        all_candles.extend(candles)
        since = candles[-1][0] + 1
        if len(candles) < 1000:
            break

    df = pd.DataFrame(
        all_candles,
        columns=['timestamp', 'open', 'high', 'low', 'close', 'volume']
    )
    df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms')
    df.set_index('timestamp', inplace=True)
    return df

sol = fetch_ohlcv('binance', 'SOL/USDT', '1h', 365)
avax = fetch_ohlcv('binance', 'AVAX/USDT', '1h', 365)

prices = pd.DataFrame({
    'SOL': sol['close'],
    'AVAX': avax['close']
}).dropna()

Test di Cointegrazione

from statsmodels.tsa.stattools import coint, adfuller
from statsmodels.regression.linear_model import OLS
from statsmodels.tools import add_constant

def test_cointegration(y: np.ndarray, x: np.ndarray) -> dict:
    """
    Test di cointegrazione completo con diagnostica.
    """
    score, pvalue, crit_values = coint(y, x)

    x_const = add_constant(x)
    model = OLS(y, x_const).fit()
    alpha, beta = model.params
    spread = y - alpha - beta * x

    adf_stat, adf_pvalue, _, _, adf_crit, _ = adfuller(spread, maxlag=20)

    spread_lag = spread[:-1]
    spread_diff = np.diff(spread)
    spread_lag_const = add_constant(spread_lag)
    hl_model = OLS(spread_diff, spread_lag_const).fit()
    theta = -hl_model.params[1]
    half_life = np.log(2) / theta if theta > 0 else np.inf

    return {
        'coint_pvalue': pvalue,
        'cointegrated': pvalue < 0.05,
        'hedge_ratio': beta,
        'intercept': alpha,
        'adf_statistic': adf_stat,
        'adf_pvalue': adf_pvalue,
        'half_life_hours': half_life,
        'half_life_days': half_life / 24,
        'spread_mean': np.mean(spread),
        'spread_std': np.std(spread),
    }

result = test_cointegration(
    prices['SOL'].values,
    prices['AVAX'].values
)
print(f"Cointegrazione: {result['cointegrated']} "
      f"(p-value: {result['coint_pvalue']:.4f})")
print(f"Hedge ratio: {result['hedge_ratio']:.4f}")
print(f"Emivita: {result['half_life_days']:.1f} giorni")

Filtro di Kalman + Backtester

from filterpy.kalman import KalmanFilter

class PairsBacktester:
    """
    Backtester walk-forward per pairs trading
    con filtro di Kalman.
    """

    def __init__(
        self,
        prices_y: np.ndarray,
        prices_x: np.ndarray,
        kalman_delta: float = 1e-4,
        obs_noise: float = 1.0,
        entry_z: float = 2.0,
        exit_z: float = 0.5,
        stop_z: float = 4.0,
        lookback: int = 60,
        fee_rate: float = 0.001,    # 0.1% round trip per leg
        slippage_rate: float = 0.0005,  # 0.05% slippage per leg
    ):
        self.prices_y = prices_y
        self.prices_x = prices_x
        self.n = len(prices_y)
        self.kalman_delta = kalman_delta
        self.obs_noise = obs_noise
        self.entry_z = entry_z
        self.exit_z = exit_z
        self.stop_z = stop_z
        self.lookback = lookback
        self.fee_rate = fee_rate
        self.slippage_rate = slippage_rate

    def run(self) -> pd.DataFrame:
        """Esegui il backtest. Restituisce un DataFrame con i risultati."""
        kf = KalmanFilter(dim_x=2, dim_z=1)
        kf.x = np.zeros((2, 1))
        kf.F = np.eye(2)
        kf.P = np.eye(2) * 1000
        kf.Q = np.eye(2) * self.kalman_delta
        kf.R = np.array([[self.obs_noise]])

        alphas = np.zeros(self.n)
        betas = np.zeros(self.n)
        spreads = np.zeros(self.n)

        for t in range(self.n):
            kf.H = np.array([[1.0, self.prices_x[t]]])
            kf.predict()
            kf.update(np.array([[self.prices_y[t]]]))
            alphas[t] = kf.x[0, 0]
            betas[t] = kf.x[1, 0]
            spreads[t] = (
                self.prices_y[t] - kf.x[0, 0]
                - kf.x[1, 0] * self.prices_x[t]
            )

        positions = np.zeros(self.n)
        z_scores = np.zeros(self.n)
        position = 0

        for t in range(self.lookback, self.n):
            window = spreads[t - self.lookback:t]
            mu = np.mean(window)
            sigma = np.std(window)
            if sigma < 1e-10:
                continue

            z = (spreads[t] - mu) / sigma
            z_scores[t] = z

            if position == 0:
                if z > self.entry_z:
                    position = -1
                elif z < -self.entry_z:
                    position = 1
            else:
                if position == 1 and z > -self.exit_z:
                    position = 0
                elif position == -1 and z < self.exit_z:
                    position = 0
                elif abs(z) > self.stop_z:
                    position = 0

            positions[t] = position

        spread_returns = np.diff(spreads) / np.abs(
            spreads[:-1] + 1e-10
        )
        pnl = np.zeros(self.n)

        for t in range(1, self.n):
            if positions[t - 1] != 0:
                raw_return = positions[t - 1] * spread_returns[t - 1]
                pnl[t] = raw_return

                if positions[t] != positions[t - 1]:
                    total_cost = 2 * (self.fee_rate + self.slippage_rate)
                    pnl[t] -= total_cost

        return pd.DataFrame({
            'price_y': self.prices_y,
            'price_x': self.prices_x,
            'alpha': alphas,
            'beta': betas,
            'spread': spreads,
            'z_score': z_scores,
            'position': positions,
            'pnl': pnl,
            'cumulative_pnl': np.cumsum(pnl),
        })

bt = PairsBacktester(
    prices_y=prices['SOL'].values,
    prices_x=prices['AVAX'].values,
    kalman_delta=1e-4,
    entry_z=2.0,
    exit_z=0.5,
    stop_z=4.0,
    lookback=60,
    fee_rate=0.001,
    slippage_rate=0.0005,
)
results = bt.run()

daily_pnl = results['pnl'].resample('D').sum() if hasattr(
    results.index, 'freq'
) else results['pnl']
metrics = calculate_metrics(daily_pnl.values)
for k, v in metrics.items():
    print(f'{k}: {v}')

Scheletro di Trading Live

import ccxt
import asyncio
import logging

logger = logging.getLogger(__name__)

class LivePairsTrader:
    """
    Scheletro minimale per il live pairs trading.
    Per la produzione: aggiungi logica di retry, monitoraggio,
    alert, riconciliazione del saldo.
    """

    def __init__(
        self,
        exchange_id: str,
        symbol_y: str,
        symbol_x: str,
        api_key: str,
        secret: str,
        position_size_usd: float = 1000.0,
        entry_z: float = 2.0,
        exit_z: float = 0.5,
    ):
        self.exchange = getattr(ccxt, exchange_id)({
            'apiKey': api_key,
            'secret': secret,
            'enableRateLimit': True,
        })
        self.symbol_y = symbol_y
        self.symbol_x = symbol_x
        self.position_size = position_size_usd
        self.entry_z = entry_z
        self.exit_z = exit_z
        self.position = 0  # +1, -1, 0

        self.kf = create_kalman_filter(delta=1e-4)
        self.spread_history = []

    async def update(self):
        """Un ciclo di aggiornamento."""
        ticker_y = self.exchange.fetch_ticker(self.symbol_y)
        ticker_x = self.exchange.fetch_ticker(self.symbol_x)
        price_y = ticker_y['last']
        price_x = ticker_x['last']

        self.kf.H = np.array([[1.0, price_x]])
        self.kf.predict()
        self.kf.update(np.array([[price_y]]))

        alpha = self.kf.x[0, 0]
        beta = self.kf.x[1, 0]
        spread = price_y - alpha - beta * price_x
        self.spread_history.append(spread)

        if len(self.spread_history) < 60:
            logger.info(f"Riscaldamento: {len(self.spread_history)}/60")
            return

        window = np.array(self.spread_history[-60:])
        z = (spread - np.mean(window)) / np.std(window)

        logger.info(
            f"β={beta:.4f} spread={spread:.4f} z={z:.2f} "
            f"pos={self.position}"
        )

        new_position = self.position

        if self.position == 0:
            if z > self.entry_z:
                new_position = -1
            elif z < -self.entry_z:
                new_position = 1
        else:
            if self.position == 1 and z > -self.exit_z:
                new_position = 0
            elif self.position == -1 and z < self.exit_z:
                new_position = 0

        if new_position != self.position:
            await self._execute_trade(
                new_position, price_y, price_x, beta
            )
            self.position = new_position

    async def _execute_trade(
        self, target: int, price_y: float, price_x: float,
        beta: float
    ):
        """Esegui un trade in coppia."""
        if target == 0:
            logger.info("Chiusura posizione")
        elif target == 1:
            size_y = self.position_size / price_y
            size_x = (self.position_size * beta) / price_x
            logger.info(
                f"Long spread: acquista {size_y:.4f} {self.symbol_y}, "
                f"vendi {size_x:.4f} {self.symbol_x}"
            )
        elif target == -1:
            size_y = self.position_size / price_y
            size_x = (self.position_size * beta) / price_x
            logger.info(
                f"Short spread: vendi {size_y:.4f} {self.symbol_y}, "
                f"acquista {size_x:.4f} {self.symbol_x}"
            )

    async def run_loop(self, interval_seconds: int = 60):
        """Loop principale."""
        logger.info(
            f"Avvio live trading: "
            f"{self.symbol_y}/{self.symbol_x}"
        )
        while True:
            try:
                await self.update()
            except Exception as e:
                logger.error(f"Errore nell'aggiornamento: {e}")
            await asyncio.sleep(interval_seconds)

Invece di una Conclusione

L'arbitraggio statistico non è il Santo Graal. È un mestiere. Tra "so cos'è la cointegrazione" e "ho una strategia che funziona in modo coerente" c'è un abisso di dettagli ingegneristici: elaborazione corretta dei dati, backtesting walk-forward corretto, un modello realistico di slippage, monitoraggio in tempo reale.

I mercati delle criptovalute offrono ancora più opportunità per lo stat arb rispetto a quelli tradizionali — liquidità frammentata, infrastruttura di mercato immatura e strumenti unici come i futures perpetui con funding rate creano inefficienze che sono state arbitraggiate fino a zero sul NYSE da tempo.

Ma la finestra si sta chiudendo. I player istituzionali stanno entrando nei mercati crypto, il capitale di arbitraggio sta crescendo (secondo le stime, il volume del capitale di arbitraggio sulle exchange crypto è cresciuto del 215% nel 2025) e i margini si stanno comprimendo. Se hai intenzione di fare stat arb nel crypto — è meglio iniziare adesso.

Tutto il codice in questo articolo è disponibile come punto di partenza. Non eseguirlo in produzione senza test approfonditi. E ricorda: l'unica strategia che è garantita funzionare è la gestione del rischio.


Opere accademiche fondamentali:

  • Engle, R.F. & Granger, C.W.J. (1987). "Co-Integration and Error Correction: Representation, Estimation, and Testing". Econometrica, 55(2), 251-276.
  • Gatev, E., Goetzmann, W.N. & Rouwenhorst, K.G. (2006). "Pairs Trading: Performance of a Relative-Value Arbitrage Rule". The Review of Financial Studies, 19(3), 797-827.
  • Vidyamurthy, G. (2004). Pairs Trading: Quantitative Methods and Analysis. Wiley.
  • Avellaneda, M. & Lee, J.H. (2010). "Statistical Arbitrage in the US Equities Market". Quantitative Finance, 10(7), 761-782.
  • Frontiers (2026). "Deep learning-based pairs trading: real-time forecasting of co-integrated cryptocurrency pairs". Frontiers in Applied Mathematics and Statistics.

Librerie utili:

  • statsmodels — cointegrazione, ADF, OLS
  • filterpy — filtro di Kalman
  • ccxt — API unificata per 100+ exchange
  • arbitragelab — libreria specializzata per pairs trading (OU, Kalman, copule)
Disclaimer: le informazioni fornite in questo articolo hanno solo scopo didattico e informativo e non costituiscono consulenza finanziaria, di investimento o di trading. Il trading di criptovalute comporta un rischio significativo di perdita.

Autori

Eugen Soloviov
Eugen Soloviov

Trading-systems engineer

Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.

Newsletter

Resta un Passo Avanti al Mercato

Iscriviti alla nostra newsletter per approfondimenti esclusivi sul trading con IA, analisi di mercato e aggiornamenti sulla piattaforma.

Rispettiamo la tua privacy. Annulla l'iscrizione in qualsiasi momento.