
Pairs Trading com Python
No vasto e mundinho dinâmico do mercado financeiro, investidores e traders buscam constantemente por estratégias que ofereçam uma vantagem competitiva. Enquanto muitos se concentram em prever a direção futura do mercado, uma classe de estratégias quantitativas, conhecida como arbitragem estatística, busca lucrar com anomalias de preços, independentemente da tendência geral. Dentro deste fascinante domínio, o Pairs Trading se destaca como uma das abordagens mais robustas e intelectualmente estimulantes.
Se você já se perguntou como é possível construir uma estratégia de negociação que seja neutra ao mercado, capaz de gerar retornos tanto em cenários de alta quanto de baixa, você está no lugar certo. Este artigo oferece uma imersão completa na implementação de uma estratégia de pairs trading com Python, desde a fundamentação teórica até a criação de um sistema de backtesting e um dashboard de análise de performance totalmente interativo e profissional.
Vamos desvendar o processo passo a passo, utilizando Python para identificar pares de ativos com uma relação econômica profunda, desenvolver sinais de negociação baseados em desvios estatísticos e avaliar rigorosamente a performance histórica da estratégia. Ao final, você terá não apenas o código completo, mas também o conhecimento necessário para adaptar e aplicar esta poderosa técnica quantitativa em seus próprios projetos e análises.
O Que é Pairs Trading? Entendendo a Arbitragem Estatística
O Pairs Trading é uma estratégia de investimento que busca explorar as discrepâncias de preços entre dois ativos que possuem uma forte relação histórica. A ideia central é que, quando o preço desses dois ativos se desvia de sua norma histórica, uma oportunidade de arbitragem estatística surge, apostando que eles eventualmente retornarão à sua relação de equilíbrio. Esta característica é conhecida como reversão à média.
Leia o artigo completo: Pairs Trading: a estratégia quantitativa que pode revelar oportunidades ocultas no mercado
Para executar a estratégia, um trader simultaneamente compra o ativo que está subvalorizado (posição long) e vende o ativo que está sobrevalorizado (posição short). O lucro é obtido quando os preços dos ativos convergem de volta para a média, e não da valorização absoluta de um único ativo. Isso torna a estratégia, em teoria, neutra em relação ao mercado.
A Base Teórica: Cointegração vs. Correlação
Um erro comum é confundir correlação com o verdadeiro pilar do Pairs Trading: a cointegração. Embora ambos os conceitos meçam a relação entre dois ativos, eles representam ideias fundamentalmente diferentes.
Correlação mede a direção do movimento entre duas variáveis. Duas ações podem ter uma alta correlação (ambas sobem e descem juntas), mas nada garante que a distância entre seus preços permanecerá estável.
Cointegração, por outro lado, é um teste estatístico muito mais rigoroso. Ele indica que, embora os preços de dois ativos possam variar individualmente ao longo do tempo, existe uma combinação linear entre eles que é estacionária. Em termos simples, eles compartilham uma relação de equilíbrio de longo prazo e tendem a se mover juntos, como se estivessem “amarrados por um elástico”. Quando se afastam, o elástico os puxa de volta.
| Conceito | Descrição | Implicação para o Pairs Trading |
| Correlação | Mede a direção do movimento dos preços. | Insuficiente. Pares correlacionados podem se afastar indefinidamente. |
| Cointegração | Indica uma relação de equilíbrio de longo prazo entre os preços. | Essencial. É a propriedade que garante a reversão à média do spread. |
Como Funciona a Estratégia Long & Short
A execução da estratégia se baseia no spread, que é a diferença (ou a razão) entre os preços dos dois ativos cointegrados. Quando o spread se alarga ou se estreita além de um certo limite estatístico, um sinal de negociação é gerado.
Sinal de Entrada (Short): Se o spread se alarga acima de um limite superior (por exemplo, 2 desvios padrão acima da média), significa que o Ativo A está sobrevalorizado em relação ao Ativo B. A estratégia então vende o Ativo A e compra o Ativo B.
Sinal de Entrada (Long): Se o spread se estreita abaixo de um limite inferior (por exemplo, 2 desvios padrão abaixo da média), o Ativo A está subvalorizado. A estratégia então compra o Ativo A e vende o Ativo B.
Sinal de Saída: A posição é fechada quando o spread retorna à sua média histórica, realizando o lucro da operação.
Long-Short Hedge Funds Are Necessary, Not Evil
Esta abordagem cria uma carteira com exposição líquida próxima de zero, isolando o lucro da performance relativa dos dois ativos, em vez da direção geral do mercado.
Implementando Pairs Trading em Python: Do Zero ao Dashboard
Agora que a teoria está consolidada, vamos à prática vou colocar partes do código aqui, estarei disponibilizando em breve ele completo. Construiremos um sistema completo em Python, dividido em módulos lógicos, que automatiza todo o processo: desde a busca por pares até a análise final de performance.
Parte 1: Configuração e Busca de Pares Cointegrados
O primeiro passo é encontrar os candidatos ideais para nossa estratégia. Criamos uma função que varre um universo de ativos, baixa seus dados históricos e aplica o teste de cointegração de Engle-Granger para identificar pares estatisticamente promissores.
# ==============================================================================
# IMPORTAÇÕES
# ==============================================================================
import yfinance as yf
import pandas as pd
import numpy as np
from statsmodels.tsa.stattools import coint
from plotly.subplots import make_subplots
import plotly.graph_objects as go
from scipy import stats
# ==============================================================================
# CÉLULA 2: MÓDULO DE BUSCA DE PARES
# ==============================================================================
def encontrar_pares_cointegrados(tickers, data_inicio, data_fim):
"""Encontra pares de ações cointegrados usando o teste de Engle-Granger."""
print("📥 Baixando dados para encontrar pares...")
try:
dados = yf.download(tickers, start=data_inicio, end=data_fim, auto_adjust=True, progress=False)['Close']
except Exception as e:
print(f"Erro ao baixar dados: {e}")
return []
n = len(tickers)
chaves = dados.keys()
pares_encontrados = []
print("🔍 Buscando pares cointegrados...")
for i in range(n):
for j in range(i + 1, n):
ativo1 = dados[chaves[i]].dropna()
ativo2 = dados[chaves[j]].dropna()
ativo1_alin, ativo2_alin = ativo1.align(ativo2, join='inner')
if len(ativo1_alin) < 20:
continue
_, p_valor, _ = coint(ativo1_alin, ativo2_alin)
if p_valor < 0.05:
pares_encontrados.append((chaves[i], chaves[j], p_valor))
print(f" ✅ Par encontrado: {chaves[i]} e {chaves[j]} (p-valor: {p_valor:.4f})")
return pares_encontrados
Nesta função, iteramos por todas as combinações possíveis de ativos em nosso universo. Para cada par, o teste coint() da biblioteca statsmodels nos retorna um p-valor. Um p-valor baixo (tipicamente < 0.05) nos dá confiança estatística para rejeitar a hipótese de que não há cointegração, indicando que o par é um bom candidato para nossa estratégia.
Parte 2: Calculando o Spread e os Sinais de Negociação
Uma vez que temos um par cointegrado, precisamos calcular o spread e definir quando comprar ou vender. Utilizamos as Bandas de Bollinger como uma forma dinâmica de estabelecer os limites de negociação. O Z-Score do spread nos ajuda a normalizar a distância do spread em relação à sua média móvel.
# ==============================================================================
# FUNÇÕES DE ANÁLISE E CÁLCULO
# ==============================================================================
def calcular_sinais_spread(dados, ativo1, ativo2, janela, z_score):
"""Calcula o spread, as bandas de Bollinger e os sinais de negociação."""
print("🛠️ Calculando sinais de negociação...")
sinais = pd.DataFrame(index=dados.index)
sinais['spread'] = dados[ativo1] / (dados[ativo2] + 1e-8)
sinais['media_movel'] = sinais['spread']
sinais['desvio_padrao'] = sinais['spread']
sinais.dropna(inplace=True)
sinais['banda_sup'] = sinais['media_movel']
sinais['banda_inf'] = sinais['media_movel']
sinais['sinal'] = 0
sinais['posicao'] = sinais['sinal'].replace(0, np.nan)
# Calcular Z-Score do spread
sinais['z_score_spread'] = (sinais['spread'] - sinais['media_movel'])
print("✅ Sinais calculados.")
return sinais
Spread: Calculamos como a razão entre os preços (ativo1 / ativo2). Isso ajuda a normalizar a relação, especialmente para ativos com preços muito diferentes.
Bandas de Bollinger: A média móvel do spread serve como nosso indicador de equilíbrio, enquanto as bandas superior e inferior, definidas por um múltiplo do desvio padrão (z_score), funcionam como nossos gatilhos de entrada.
Posição: A coluna posicao traduz os sinais de entrada e saída em uma posição contínua ao longo do tempo (1 para long, -1 para short, 0 para flat).
Parte 3: Métricas de Performance Avançadas
Além do retorno total, uma avaliação robusta deve considerar o risco assumido. Nossa função calcular_metricas_performance computa mais de uma dúzia de indicadores essenciais.
def calcular_drawdown(carteira):
"""Calcula a série temporal do drawdown da estratégia."""
pico = carteira['retorno_acum'].expanding(min_periods=1).max()
drawdown = (carteira['retorno_acum'] / pico) - 1
return drawdown
def calcular_metricas_performance(carteira, sinais):
"""Calcula as principais métricas de performance da estratégia."""
print("📈 Calculando métricas de performance...")
retornos = carteira['retorno_estrategia']
if len(retornos) < 2:
return {"Erro": "Dados insuficientes."}
retorno_total = carteira['retorno_acum']
num_anos = len(retornos) / 252
retorno_anual = (1 + retorno_total) ** (1 / num_anos) - 1 if num_anos > 0 else 0
vol_anual = retornos.std() * np.sqrt(252)
sharpe = retorno_anual / vol_anual if vol_anual != 0 else 0
# Drawdown máximo
dd_max = calcular_drawdown(carteira).min()
# Calmar Ratio
calmar_ratio = retorno_anual / abs(dd_max) if dd_max != 0 else 0
# Sortino Ratio (penaliza apenas volatilidade negativa)
retornos_negativos = retornos[retornos < 0]
downside_vol = retornos_negativos.std()
sortino = retorno_anual / downside_vol if downside_vol
# Win Rate
trades_positivos = (retornos > 0).sum()
total_trades = len(retornos[retornos != 0])
win_rate = trades_positivos / total_trades if total_trades > 0 else 0
# Profit Factor
ganhos = retornos[retornos > 0].sum()
perdas = abs(retornos[retornos < 0].sum())
# Estatísticas de Z-Score
z_score_medio = sinais['z_score_spread']
z_score_max = sinais['z_score_spread']
z_score_min = sinais['z_score_spread']
# Exposição ao mercado
total_dias = len(carteira)
dias_long = (carteira['posicao'] > 0).sum()
dias_short = (carteira['posicao'] < 0).sum()
dias_flat = (carteira['posicao'] == 0).sum()
return {
"Retorno Anualizado": f"{retorno_anual:.2%}",
"Volatilidade Anualizada": f"{vol_anual:.2%}",
"Índice Sharpe": f"{sharpe:.2f}",
"Sortino Ratio": f"{sortino:.2f}",
"Drawdown Máximo": f"{dd_max:.2%}",
"Calmar Ratio": f"{calmar_ratio:.2f}",
"Win Rate": f"{win_rate:.2%}",
"Z-Score Médio": f"{z_score_medio:.2f}",
"Z-Score Máximo": f"{z_score_max:.2f}",
"Z-Score Mínimo": f"{z_score_min:.2f}",
"Dias Long": f"{dias_long} ({dias_long/total_dias:.1%})",
}
Índices de Sharpe, Sortino e Calmar: Medem o retorno ajustado ao risco. Sharpe usa a volatilidade total, Sortino foca na volatilidade negativa (prejudicial), e Calmar compara o retorno com o drawdown máximo.
Drawdown Máximo: A maior perda percentual do pico ao fundo. Uma métrica crucial para entender o risco de ruína.
Value at Risk (VaR) e Conditional Value at Risk (CVaR): Estimam a perda potencial máxima em cenários adversos, oferecendo uma visão clara sobre o risco de cauda da estratégia.
Leia também: VaR e CVaR: Análise de Risco de Portfólio com Python
Parte 4: Executando o Backtest da Estratégia
Com os sinais definidos, podemos simular a execução da estratégia ao longo do tempo. A função de backtest calcula os retornos diários da nossa carteira, considerando os custos de transação, e acumula o capital ao longo do período de teste.
# ==============================================================================
# FUNÇÃO DE SIMULAÇÃO (BACKTEST)
# ==============================================================================
def executar_backtest(dados, sinais, ativo1, ativo2, capital_inicial, custo_tx):
"""Executa a simulação da estratégia de pairs trading."""
print("🚀 Executando backtest Long & Short...")
carteira = pd.DataFrame(index=sinais.index)
carteira['posicao'] = sinais['posicao']
retornos_ativo1 = dados_alinhados[ativo1].pct_change()
retornos_ativo2 = dados_alinhados[ativo2].pct_change()
retorno_bruto = (carteira['posicao'].shift(1) * retornos_ativo1) - \
(carteira['posicao'].shift(1) * retornos_ativo2)
custo_trades = carteira['posicao'].diff().abs() * custo_tx
carteira['retorno_estrategia'] = (retorno_bruto - custo_trades)
carteira['retorno_acum'] = (1 + carteira['retorno_estrategia'])
carteira['valor_carteira'] = capital_inicial
print("✅ Backtest finalizado.")
return carteira
O cálculo do retorno_bruto é o coração da estratégia long-short. Ao multiplicar a posição (que pode ser +1 ou -1) pelos retornos de cada ativo, estamos efetivamente comprando um e vendendo o outro, capturando a diferença de performance entre eles.
Parte 5: Dashboard Profissional com Plotly
Finalmente, consolidamos todas as análises em um dashboard visualmente rico e interativo. A visualização de dados é fundamental para compreender a dinâmica da estratégia ao longo do tempo.
# ==============================================================================
# FUNÇÃO DE VISUALIZAÇÃO (DASHBOARD)
# ==============================================================================
def plotar_dashboard_completo(carteira, dados_backtest, sinais, metricas, ativo1, ativo2):
"""Gera e exibe um dashboard profissional com análise de performance e risco."""
print("📊 Gerando Dashboard Profissional Completo...")
if carteira.empty:
print("⚠️ Carteira vazia. Não é possível gerar o dashboard.")
return
# Cálculos preparatórios
meses, retornos_mensais = calcular_retornos_mensais(carteira)
drawdown = calcular_drawdown(carteira)
# Calcula a performance "Buy and Hold" para cada ativo individualmente
capital_inicial = carteira['valor_carteira']
buy_hold_ativo1 = (dados_backtest[ativo1]
buy_hold_ativo2 = (dados_backtest[ativo2]
# Cálculos de risco: VaR e CVaR (95%)
retornos_diarios = carteira['retorno_estrategia'].dropna()
var_95 = np.percentile(retornos_diarios, 5)
cvar_95 = retornos_diarios[retornos_diarios <= var_95].mean()
ic_95_inferior = np.percentile(retornos_diarios)
ic_95_superior = np.percentile(retornos_diarios)
# Criação do dashboard com 8 gráficos
fig = make_subplots(
rows=4, cols=2,
subplot_titles=(
),
vertical_spacing=0.08,
horizontal_spacing=0.12,
)
# ... (código completo dos 8 gráficos)
fig.show()
print("✅ Dashboard gerado com sucesso!")
O dashboard gerado apresenta 8 visualizações essenciais, incluindo a curva de capital, o comportamento do spread, o drawdown ao longo do tempo, a distribuição de retornos e muito mais.
Parte 6: Orquestrando a Estratégia Completa
O último passo é reunir todos os módulos em um fluxo de execução principal que automatiza o processo completo.
# ==============================================================================
# ORQUESTRAÇÃO PRINCIPAL (MAIN)
# ==============================================================================
# Parâmetros de Busca de Pares
UNIVERSO_DE_ATIVOS = [
# Bancos
'ITUB4.SA', 'BBDC4.SA', 'BBAS3.SA', 'SANB11.SA', 'BPAC11.SA',
# Energia elétrica
'TAEE11.SA', 'TRPL4.SA', 'CMIG4.SA', 'CPLE6.SA', 'EGIE3.SA', 'ENBR3.SA', 'NEOE3.SA',
# Petróleo, gás e mineração
'PETR4.SA', 'PETR3.SA', 'VALE3.SA', 'CMIN3.SA', 'PRIO3.SA', 'RECV3.SA',
# Siderurgia e metalurgia
'GGBR4.SA', 'GOAU4.SA', 'USIM5.SA', 'CSNA3.SA',
# Varejo e consumo
'LREN3.SA', 'MGLU3.SA', 'VVAR3.SA',
# Seguradoras e financeiras
'BBSE3.SA', 'IRBR3.SA', 'SULA11.SA',
# Telecomunicações
'VIVT3.SA', 'TIMS3.SA',
# Fundos imobiliários
'HGLG11.SA', 'KNRI11.SA', 'MXRF11.SA'
]
DATA_INICIO_BUSCA = '2015-01-01'
DATA_FIM_BUSCA = '2022-12-31'
print(f"Iniciando busca de pares no período de {DATA_INICIO_BUSCA} a {DATA_FIM_BUSCA}...")
if not pares_encontrados:
print("\n❌ Nenhum par cointegrado encontrado. Tente um universo ou período diferente.")
else:
print(f"\n✅ {len(pares_encontrados)} pares encontrados! Analisando o melhor...")
# Ordena os pares pelo p-valor (do menor para o maior)
pares_ordenados = sorted(pares_encontrados, key=lambda x: x[2])
# Parâmetros do Backtest (out-of-sample)
DATA_INICIO_BACKTEST = '2023-01-01'
DATA_FIM_BACKTEST = pd.to_datetime('today').strftime('%Y-%m-%d')
CAPITAL_INICIAL = 100000.0
for i, par in enumerate(pares_ordenados):
ATIVO_1, ATIVO_2, p_valor = par
print(f"\n--- Analisando Par {i+1}/{len(pares_ordenados)}: {ATIVO_1} vs {ATIVO_2} (p-valor: {p_valor:.4f}) ---")
# Execução do Fluxo Completo
dados_backtest = yf.download([ATIVO_1, ATIVO_2], start=DATA_INICIO_BACKTEST, end=DATA_FIM_BACKTEST, auto_adjust=True, progress=False)['Close']
if dados_backtest.empty or len(dados_backtest) < JANELA_MEDIA_MOVEL:
print("\n❌ Dados insuficientes para realizar o backtest. Tente um período maior.")
else:
sinais = calcular_sinais_spread(dados_backtest, ATIVO_1, ATIVO_2, JANELA_MEDIA_MOVEL, Z_SCORE_LIMITE)
carteira = executar_backtest(dados_backtest, sinais, ATIVO_1, ATIVO_2, CAPITAL_INICIAL, CUSTO_TRANSACAO)
plotar_dashboard_completo(carteira, dados_backtest, sinais, metricas, ATIVO_1, ATIVO_2)
Este código principal define um universo amplo de ativos brasileiros, busca pares cointegrados em um período histórico (2015-2022) e, em seguida, testa a estratégia em um período fora da amostra (2023-presente). O loop permite analisar múltiplos pares automaticamente, gerando um dashboard para cada um.
Interpretando os Resultados: Métricas e Gestão de Risco
Uma vez que o backtest é executado, o dashboard nos fornece uma visão abrangente da performance da estratégia. Vamos explorar como interpretar as principais métricas.
Análise de Performance: Sharpe, Sortino e Calmar Ratios
O Índice de Sharpe é uma das métricas mais populares para avaliar o retorno ajustado ao risco. Ele divide o retorno anualizado pela volatilidade anualizada. Um Sharpe acima de 1 é considerado bom, acima de 2 é excelente, e acima de 3 é excepcional. No entanto, o Sharpe trata toda volatilidade igualmente, o que pode ser uma limitação.
O Sortino Ratio resolve isso ao penalizar apenas a volatilidade negativa (downside). Para estratégias que buscam retornos assimétricos, o Sortino é uma métrica mais apropriada, pois reflete melhor o risco de perdas.
O Calmar Ratio compara o retorno anualizado com o drawdown máximo. Ele responde à pergunta: "Quanto retorno estou obtendo por unidade de perda máxima?" É particularmente útil para avaliar a sustentabilidade de uma estratégia no longo prazo.
Gestão de Risco: VaR, CVaR e Drawdown Máximo
O Value at Risk (VaR) nos diz, com um determinado nível de confiança (geralmente 95%), qual é a perda máxima esperada em um dia normal de negociação. Por exemplo, um VaR de 95% de -1,5% significa que, em 95% dos dias, a perda não deve exceder 1,5%.
O Conditional Value at Risk (CVaR), também conhecido como Expected Shortfall, vai além. Ele calcula a perda média nos 5% piores dias (aqueles que excedem o VaR). O CVaR nos dá uma ideia do quão ruins podem ser os dias realmente ruins.
O Drawdown Máximo é a maior queda percentual do pico ao vale durante o período de análise. É uma medida direta do sofrimento que um investidor teria experimentado ao manter a estratégia. Drawdowns prolongados ou profundos podem indicar que a relação de cointegração se quebrou ou que os parâmetros da estratégia precisam ser ajustados.
Aplicando no Mercado Brasileiro: Casos Práticos
Para validar nossa estratégia, aplicamos o sistema a um universo de ações brasileiras, buscando pares no período de 2015 a 2022 e realizando o backtest em um período fora da amostra (out-of-sample) de 2023 até o presente. Abaixo, apresentamos o dashboard completo para alguns dos pares encontrados.
Caso 1: GGBR4 vs. GOAU4 (Siderurgia)
Este é um par clássico, representando a produtora de aço (Gerdau) e sua holding. A forte relação econômica entre as duas empresas resulta em um comportamento altamente cointegrado, ideal para o Pairs Trading.

Caso 2: GOAU4 vs. PRIO3 (Siderurgia vs. Petróleo)
Este par, menos intuitivo, foi identificado pelo nosso algoritmo. A análise mostra uma performance mais volátil, destacando a importância de se avaliar não apenas o p-valor, mas também as métricas de risco do backtest.

Caso 3: NEOE3 vs. USIM5 (Elétrico vs. Siderurgia)
Outro par inesperado, provavelmente capturando uma relação macroeconômica mais ampla. O resultado demonstra um período de bom desempenho seguido por um aumento significativo no drawdown, ilustrando que as relações de cointegração podem, por vezes, quebrar.

O Poder da Arbitragem Estatística
O Pairs Trading, quando implementado de forma sistemática e rigorosa, representa uma poderosa ferramenta no arsenal de um investidor quantitativo. Ao focar na relação entre ativos em vez de suas direções absolutas, a estratégia oferece potencial de diversificação e geração de alfa, mesmo em mercados laterais ou em queda.
Através deste artigo, demonstramos como construir um sistema completo de pairs trading com Python, cobrindo desde a teoria da cointegração até a criação de um dashboard de análise profissional. O código fornecido serve como um framework robusto que pode ser expandido e adaptado para diferentes mercados e classes de ativos.
Lembre-se, no entanto, que nenhuma estratégia é infalível. As relações de cointegração podem mudar, e um monitoramento constante do risco é fundamental. A análise de drawdown, VaR e outras métricas de risco, como as incluídas em nosso dashboard, não são apenas um exercício acadêmico, mas uma necessidade prática para a sobrevivência e o sucesso no longo prazo.
Gostou deste artigo? Explore mais sobre estratégias quantitativas e Python para o mercado financeiro em nossas categorias de Análise e Estratégias Quantitativa.
Disclaimer:
Este artigo é apenas para fins educacionais e não constitui uma recomendação de investimento. O desempenho passado não é garantia de resultados futuros.





