Arbitraggio Statistico e Pairs Trading nei Mercati Crypto: Dalla Cointegrazione al Filtro di Kalman
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.

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:
- Acquista l'asset spot (ad es., 1 BTC)
- Apri una posizione short sul futures perpetuo (1 BTC)
- 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,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:
- Calcola l'hedge ratio β (ad es., β = 1.3)
- Quando z-score > +2: short SOL, long AVAX × β
- Quando z-score < -2: long SOL, short AVAX × β
- 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.

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:
-
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.
-
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.
-
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:
- Dividi i dati in periodi: [train₁ → test₁] → [train₂ → test₂] → ...
- Su ogni periodo di train: stima la cointegrazione, calcola l'hedge ratio, seleziona le soglie z-score
- Sul periodo di test: fai trading con parametri fissi
- Combina tutti i periodi di test per la valutazione finale
Configurazione tipica per crypto: train = 180 giorni, test = 30 giorni, step = 30 giorni.

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 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 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:
- Stop-loss rigoroso: chiudi la posizione quando z-score > 4σ
- Limiti di leva: massimo 2-3x su ogni leg
- Filtro VIX/volatilità: riduci la dimensione della posizione quando la volatilità implicita è alta
- 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)
Autori
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.