Kaique Mitsuo Silva Yamamoto
Mercado financeiroFinanças Quantitativas

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ência

Problema: 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çãoCointegração
O que medeCo-movimento de curto prazoEquilíbrio de longo prazo
EstabilidadeVaria com o regime de mercadoMais estável estruturalmente
ImplicaDireção similarSpread estacionário
Para pairs tradingInsuficienteNecessá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)

  1. Estime a relação de equilíbrio:
Preço_A = α + β × Preço_B + ε
  1. 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: Browniano

Interpretaçã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, sigma

Estraté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 long

Dimension 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 notional

Custos 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_cost

Seleçã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-valor

Cuidado 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


Referências:

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.

On this page