Arbitragem Estatística — Pairs Trading, Cointegração e Mean Reversion
Fundamentos de arbitragem estatística: pairs trading, cointegração, modelos de spread, estratégias de mean reversion e implementação em Python.
Arbitragem estatística é uma classe de estratégias de trading que exploram ineficiências de preços de curto prazo usando modelos estatísticos, sem depender da direção do mercado. É market-neutral: as posições long e short se compensam, reduzindo a exposição ao risco sistemático.
Introduzida pela Morgan Stanley nos anos 1980, tornou-se base de hedge funds quantitativos como Renaissance Technologies e DE Shaw.
Papers de referência: arXiv q-fin.TR — Trading and Market Microstructure | arXiv:2403.12180 — Advanced Statistical Arbitrage with RL
O conceito central: Mean Reversion
Mean reversion é a propriedade de que um processo tende a retornar à sua média ao longo do tempo. Se o spread entre dois ativos relacionados se afasta da média histórica, a estratégia aposta que ele vai convergir.
Spread(t) = Preço_A(t) - β × Preço_B(t)
Se Spread(t) >> média histórica: A está caro, B está barato
→ Vende A (short), Compra B (long)
→ Aposta na convergênciaProblema: como distinguir divergência temporária (oportunidade) de divergência permanente (armadilha)?
Resposta: Cointegração.
Correlação vs Cointegração
Esta distinção é crítica:
| Correlação | Cointegração | |
|---|---|---|
| O que mede | Co-movimento de curto prazo | Equilíbrio de longo prazo |
| Estabilidade | Varia com o regime de mercado | Mais estável estruturalmente |
| Implica | Direção similar | Spread estacionário |
| Para pairs trading | Insuficiente | Necessário |
Exemplo: PETR3 e PETR4 têm alta correlação e cointegração. Duas ações aleatórias podem ter alta correlação temporariamente sem cointegração.
Analogia: um bêbado andando com seu cachorro. Ambos seguem caminhos aleatórios (não estacionários), mas o cachorro nunca fica muito longe do dono — há uma força restauradora (a coleira). Esse é o equilíbrio de longo prazo da cointegração.
Teste de Cointegração
ADF em dois passos (Engle-Granger)
- Estime a relação de equilíbrio:
Preço_A = α + β × Preço_B + ε- Teste se os resíduos ε são estacionários com ADF:
from statsmodels.tsa.stattools import adfuller, coint
import numpy as np
# Teste de Engle-Granger
stat, pvalue, _ = coint(preco_A, preco_B)
print(f"p-valor cointegração: {pvalue:.4f}")
# p < 0.05: rejeita H₀ de não-cointegração → par cointegrado
# Coeficiente de hedge (hedge ratio)
from statsmodels.regression.linear_model import OLS
from statsmodels.tools import add_constant
modelo = OLS(preco_A, add_constant(preco_B)).fit()
beta = modelo.params[1]
print(f"Hedge ratio (β): {beta:.4f}")Teste de Johansen
Para múltiplos ativos (portfólio de cointegração):
from statsmodels.tsa.vector_ar.vecm import coint_johansen
resultado = coint_johansen(precos_df, det_order=0, k_ar_diff=1)
trace_stat = resultado.lr1 # estatísticas de trace
crit_vals = resultado.cvt # valores críticos (90%, 95%, 99%)Modelagem do Spread
Processo de Ornstein-Uhlenbeck (OU)
O spread entre ativos cointegrados é bem modelado pelo processo OU — a formalização matemática de mean reversion:
dX = κ(μ - X) dt + σ dW
Onde:
- X: spread atual
- μ: média de longo prazo do spread
- κ: velocidade de reversão à média
- σ: volatilidade do spread
- dW: BrownianoInterpretação de κ: half-life de reversão = ln(2)/κ. Se κ = 0.05 (diário), o spread leva ~14 dias para percorrer metade da distância até a média.
def fit_ou_process(spread):
"""Estima parâmetros do processo OU por MLE"""
from scipy.optimize import minimize
# Regressão discreta: ΔX_t = κ*(μ - X_{t-1})*dt + σ*ε
X = spread.values
dX = np.diff(X)
X_lag = X[:-1]
# MLE simplificado (regressão OLS na forma discreta)
from statsmodels.regression.linear_model import OLS
from statsmodels.tools import add_constant
modelo = OLS(dX, add_constant(X_lag)).fit()
kappa = -modelo.params[1] # velocidade de reversão
mu = modelo.params[0] / kappa # média de longo prazo
sigma = np.std(modelo.resid) # volatilidade
half_life = np.log(2) / kappa
print(f"κ: {kappa:.4f} | μ: {mu:.4f} | σ: {sigma:.4f}")
print(f"Half-life: {half_life:.1f} dias")
return kappa, mu, sigmaEstratégia de Pairs Trading — implementação completa
import pandas as pd
import numpy as np
from statsmodels.tsa.stattools import coint
import yfinance as yf
class PairsTradingStrategy:
def __init__(self, ticker_A, ticker_B, lookback=252, entry_z=2.0, exit_z=0.5):
self.ticker_A = ticker_A
self.ticker_B = ticker_B
self.lookback = lookback
self.entry_z = entry_z # Entrar quando z-score > ±2
self.exit_z = exit_z # Sair quando z-score < ±0.5
def get_data(self, start, end):
data = yf.download([self.ticker_A, self.ticker_B], start=start, end=end)
return data["Adj Close"]
def calc_spread(self, prices):
from statsmodels.regression.linear_model import OLS
from statsmodels.tools import add_constant
A = prices[self.ticker_A]
B = prices[self.ticker_B]
# Hedge ratio via OLS rolling (janela = lookback)
betas = []
for i in range(self.lookback, len(A)):
modelo = OLS(A.iloc[i-self.lookback:i],
add_constant(B.iloc[i-self.lookback:i])).fit()
betas.append(modelo.params[1])
beta_series = pd.Series(betas, index=A.index[self.lookback:])
spread = A.iloc[self.lookback:] - beta_series * B.iloc[self.lookback:]
return spread, beta_series
def calc_zscore(self, spread, window=20):
"""Z-score rolling do spread"""
mu = spread.rolling(window).mean()
sigma = spread.rolling(window).std()
return (spread - mu) / sigma
def generate_signals(self, zscore):
signals = pd.Series(0, index=zscore.index)
signals[zscore > self.entry_z] = -1 # Spread alto: short A, long B
signals[zscore < -self.entry_z] = 1 # Spread baixo: long A, short B
signals[abs(zscore) < self.exit_z] = 0 # Fechar posição
return signals.ffill() # Manter sinal até condição de saída
# Uso
strategy = PairsTradingStrategy("VALE3.SA", "VALE5.SA")
prices = strategy.get_data("2020-01-01", "2024-12-31")
spread, betas = strategy.calc_spread(prices)
zscore = strategy.calc_zscore(spread)
signals = strategy.generate_signals(zscore)Gestão de risco em pairs trading
Stop loss no spread
Saída obrigatória quando o spread diverge além de um limite:
# Stop se spread divergiu > 3 desvios padrão (sinal falso provável)
stop_z = 3.5
signals[zscore > stop_z] = 0 # Stopout no short
signals[zscore < -stop_z] = 0 # Stopout no longDimension do trade
def position_size(equity, vol_target, spread_vol):
"""
Dimensionamento baseado em volatilidade alvo.
vol_target: % do portfólio de volatilidade diária desejada
"""
dollar_vol = equity * vol_target
notional = dollar_vol / spread_vol
return notionalCustos de transação
def net_pnl(gross_pnl, num_trades, capital, cost_bps=10):
"""
cost_bps: custo por side em basis points
Para um par: 4 lados (entrada long/short, saída long/short)
"""
total_cost = num_trades * 4 * cost_bps/10000 * capital
return gross_pnl - total_costSeleção de pares
Critérios fundamentais
Mesmo setor/indústria: pares fundamentalmente relacionados têm spread mais estável. Evitar correlações espúrias entre setores diferentes.
def selecionar_pares(tickers, p_threshold=0.05):
"""Encontra todos os pares cointegrados dentro de uma lista"""
pares = []
n = len(tickers)
for i in range(n):
for j in range(i+1, n):
_, pvalue, _ = coint(precos[tickers[i]], precos[tickers[j]])
if pvalue < p_threshold:
pares.append((tickers[i], tickers[j], pvalue))
return sorted(pares, key=lambda x: x[2]) # ordenado por p-valorCuidado com data snooping: testar muitos pares aumenta a chance de encontrar cointegração por acaso. Ajustar por múltiplos testes (Bonferroni, FDR).
Além de pairs: portfólios de cointegração
Com Johansen, podemos encontrar combinações lineares de múltiplos ativos que são estacionárias — portfólios de arbitragem com mais ativos.
StatArb de ETFs: explorar desalinhamentos entre ETF e seus componentes (basket arbitrage), especialmente em momentos de stress de mercado.
Arbitragem estatística com Reinforcement Learning
Pesquisa recente (arXiv:2403.12180) combina StatArb com Reinforcement Learning:
- O agente aprende quando abrir/fechar posições baseado no estado do spread
- Supera regras estáticas de z-score em mercados com mudanças de regime
- Pode incorporar custos de transação diretamente na função de recompensa
Conexões com outras seções
- Econometria — cointegração e testes de raiz unitária são conceitos econométricos
- Séries Temporais com ML — modelos de séries para spread e volatilidade
- Aprendizado por Reforço — RL aplicado a estratégias de StatArb
- Otimização de Portfólio — gestão de risco de portfólios de pares
- Automação no TradingView — implementação de pairs trading em Pine Script
Referências:
- Advanced Statistical Arbitrage with RL (arXiv 2403.12180)
- Pairs Trading Basics — QuantInsti
- Cointegration for Pairs Trading — Hudson & Thames
- Statistical Arbitrage — Wikipedia
Aviso Legal: Estratégias de arbitragem estatística envolvem riscos significativos, incluindo falha de cointegração, mudanças de regime e custos de execução. Conteúdo educativo. Não constitui recomendação de investimento.