Backtesting de Ações Brasileiras em Python: Guia Completo com API
Simular uma estratégia de investimento em dez anos de dados exige três coisas: histórico confiável, modelagem honesta das premissas e métricas comparáveis a um benchmark. Este guia cobre backtesting de ações brasileiras em Python usando a API da B3 da bolsai, da coleta de OHLC ajustado ao cálculo de Sharpe contra o CDI.
O que é backtesting e por que é fundamental antes de investir
Backtesting é a simulação de uma estratégia de investimento sobre dados históricos para estimar como ela teria performado no passado. Serve para filtrar ideias ruins antes que capital real seja exposto, calibrar parâmetros e medir risco em termos quantitativos — retorno, volatilidade, drawdown. Resultado positivo em backtest não garante lucro futuro, mas um resultado ruim praticamente garante que a estratégia não funciona.
Para o mercado brasileiro, o problema começa antes da simulação. Onde baixar 10 anos de OHLC ajustado de PETR4? Como obter o CDI diário como taxa livre de risco? Como lidar com tickers que mudaram de nome (VIIA3 era VVAR3) ou saíram de bolsa? A qualidade do backtest depende inteiramente da qualidade dos dados de entrada — e aí mora a primeira armadilha quando se tenta fazer backtesting python ações brasileiras com ferramentas pensadas para o mercado americano.
Limitações de alternativas (yfinance, scraping, MetaTrader)
A biblioteca yfinance é popular por ser gratuita e simples, mas o sufixo .SA para tickers da B3 tem histórico de instabilidade: séries são cortadas sem aviso, ajustes por eventos corporativos ficam defasados e FIIs apresentam dados inconsistentes. Boa parte dos backtests brasileiros em fóruns públicos usa yfinance e depois falha em produção porque o pipeline quebra silenciosamente.
MetaTrader e TradingView servem bem para análise visual e execução, mas não são bibliotecas Python — o fluxo programático é limitado e integrar com pandas ou numpy exige exportações manuais. Scraping de Fundamentus ou StatusInvest, além de frágil, esbarra em termos de uso e captchas. Plataformas como Quantopian (descontinuada) e QuantConnect continuam focadas em tickers dos EUA, com cobertura brasileira rasa.
Sobra a opção de construir a própria infraestrutura a partir de COTAHIST da B3 e DFPs da CVM — viável, mas o parser de largura fixa, o ajuste retroativo por splits e a normalização de TTM consomem semanas antes de qualquer linha de código de estratégia ser escrita. Uma API REST que já entrega preço ajustado e CDI pronto vira o caminho mais curto entre hipótese e número.
Stack: Python + pandas + bolsai API
A stack mínima para um backtest rigoroso no Brasil tem quatro componentes. O primeiro é a fonte de dados: a API da bolsai expõe /stocks/{ticker}/history com OHLC ajustado desde 1986 e /macro/cdi para taxa livre de risco. O segundo é pandas para manipular séries temporais, o terceiro é numpy para cálculos vetorizados, e o quarto é uma biblioteca de backtesting propriamente dita.
Para estratégias simples em um único ativo, backtesting.py oferece uma API declarativa curta e ótima para ensinar. Quando o escopo exige testar centenas de parâmetros ou dezenas de ativos simultaneamente, vectorbt é ordens de magnitude mais rápido graças à vetorização em numpy. Os exemplos a seguir foram escritos com pandas puro para deixar a lógica explícita; plugar em backtesting.py ou vectorbt é trivial depois.
Instalação mínima:
pip install httpx pandas numpy matplotlib
Baixando histórico ajustado de PETR4 para 10 anos
O endpoint /stocks/PETR4/history retorna candles diários ordenados do mais recente para o mais antigo. Usar adjusted_close é obrigatório em backtest: a série bruta cria gaps artificiais em cada data ex-dividendo, o que contamina cálculos de retorno. O código abaixo baixa 10 anos de PETR4, constrói um DataFrame indexado por data e plota a série:
import httpx
import pandas as pd
import matplotlib.pyplot as plt
API_KEY = "sua_chave_aqui"
BASE = "https://api.usebolsai.com/api/v1"
def get_history(ticker, start="2016-04-01", end="2026-04-01"):
r = httpx.get(
f"{BASE}/stocks/{ticker}/history",
params={"start": start, "end": end, "limit": 5000},
headers={"X-API-Key": API_KEY},
timeout=30,
)
r.raise_for_status()
prices = r.json()["prices"]
df = pd.DataFrame(prices)
df["trade_date"] = pd.to_datetime(df["trade_date"])
df = df.set_index("trade_date").sort_index()
return df[["adjusted_open", "adjusted_high",
"adjusted_low", "adjusted_close", "volume"]]
petr4 = get_history("PETR4")
print(f"Registros: {len(petr4)}")
print(f"Período: {petr4.index[0].date()} a {petr4.index[-1].date()}")
print(petr4.tail(3))
petr4["adjusted_close"].plot(title="PETR4 (ajustado) — 10 anos", figsize=(10, 4))
plt.ylabel("R$"); plt.tight_layout(); plt.savefig("petr4.png")
# Registros: 2479
# Período: 2016-04-01 a 2026-04-01
# adjusted_open adjusted_high adjusted_low adjusted_close volume
# 2026-03-30 38.21 38.64 38.02 38.47 42301200
# 2026-03-31 38.49 38.73 38.10 38.28 38127400
# 2026-04-01 38.30 38.95 38.15 38.82 45211300
Para estratégias que rodam sobre múltiplos ativos, basta iterar e concatenar. A guia detalhada sobre histórico de preços aprofunda paginação, start/end e diferenças entre série ajustada e bruta.
Estratégia 1: buy-and-hold como benchmark
Toda estratégia ativa deve ser comparada com a alternativa mais boba possível: comprar no primeiro dia, segurar até o último. Se a estratégia sofisticada não bate buy-and-hold descontado o risco e os custos, ela não justifica complexidade. Código:
# Retorno acumulado buy-and-hold em PETR4
close = petr4["adjusted_close"]
daily_ret = close.pct_change().fillna(0)
bh_equity = (1 + daily_ret).cumprod()
total_return = bh_equity.iloc[-1] - 1
years = (close.index[-1] - close.index[0]).days / 365.25
cagr = bh_equity.iloc[-1] ** (1 / years) - 1
print(f"Buy-and-hold PETR4 (10a)")
print(f" Retorno total: {total_return*100:.1f}%")
print(f" CAGR: {cagr*100:.2f}%")
print(f" Vol anual: {daily_ret.std()*(252**0.5)*100:.2f}%")
# Buy-and-hold PETR4 (10a)
# Retorno total: 187.3%
# CAGR: 11.12%
# Vol anual: 38.41%
PETR4 entregou 11,12% ao ano com volatilidade de 38%. Volatilidade desse patamar é característica de empresas cíclicas — o Ibovespa como um todo roda perto de 22%–26%. Esse número é a barra que qualquer estratégia de timing em PETR4 precisa ultrapassar.
Estratégia 2: cruzamento de médias móveis (SMA 20/50)
O cruzamento de médias é o "hello world" do trading sistemático. A regra: quando a média curta (SMA 20) cruza acima da média longa (SMA 50), comprar; quando cruza abaixo, vender. A premissa subjacente é que tendências tendem a persistir — a chamada momentum. A implementação vetorizada é curta:
import numpy as np
df = petr4.copy()
df["sma_20"] = df["adjusted_close"].rolling(20).mean()
df["sma_50"] = df["adjusted_close"].rolling(50).mean()
# Sinal: 1 se SMA20 > SMA50, senão 0. Shift(1) evita look-ahead bias.
df["signal"] = (df["sma_20"] > df["sma_50"]).astype(int).shift(1)
df["daily_ret"] = df["adjusted_close"].pct_change()
df["strat_ret"] = df["signal"] * df["daily_ret"]
# Aplicar custo estimado de 0,10% por troca de posição
trades = df["signal"].diff().abs().fillna(0)
df["strat_ret"] = df["strat_ret"] - trades * 0.001
df["strat_equity"] = (1 + df["strat_ret"].fillna(0)).cumprod()
df["bh_equity"] = (1 + df["daily_ret"].fillna(0)).cumprod()
strat_total = df["strat_equity"].iloc[-1] - 1
bh_total = df["bh_equity"].iloc[-1] - 1
n_trades = int(trades.sum())
print(f"SMA 20/50 em PETR4")
print(f" Trocas de posição: {n_trades}")
print(f" Retorno estratégia: {strat_total*100:.1f}%")
print(f" Retorno buy-and-hold: {bh_total*100:.1f}%")
# SMA 20/50 em PETR4
# Trocas de posição: 74
# Retorno estratégia: 142.8%
# Retorno buy-and-hold: 187.3%
No caso de PETR4, a estratégia de médias móveis ficou atrás do buy-and-hold mesmo antes de impostos — 142,8% contra 187,3%. O detalhe importante está no shift(1): sem ele, o sinal seria calculado com o fechamento do próprio dia, como se fosse possível decidir e executar ao preço de fechamento simultaneamente. Esse é look-ahead bias, a armadilha estatística mais comum em backtests amadores.
Acelere o próximo passo. A documentação lista todos os parâmetros de /stocks/{ticker}/history, incluindo paginação e filtros por data — útil quando a estratégia precisa rodar em 300 ativos em paralelo.
Estratégia 3: dollar-cost averaging vs aporte único
Investidores pessoa física raramente aportam de uma só vez. O padrão é dollar-cost averaging (DCA): aporte mensal fixo, independente do preço. O backtest do DCA exige lógica ligeiramente diferente — cada mês compra uma quantia em reais, o que gera número fracionário de ações e patrimônio acumulado não trivial. Comparação com aporte único do mesmo total:
# DCA: R$ 1.000 por mês em PETR4 durante 10 anos
monthly = petr4["adjusted_close"].resample("MS").first().dropna()
aporte = 1000.0
shares = (aporte / monthly).cumsum()
capital_aportado = aporte * np.arange(1, len(monthly) + 1)
patrimonio = shares * monthly
dca_total_aportado = float(capital_aportado[-1])
dca_valor_final = float(patrimonio.iloc[-1])
dca_lucro = dca_valor_final - dca_total_aportado
# Aporte único equivalente no início
preco_inicial = petr4["adjusted_close"].iloc[0]
preco_final = petr4["adjusted_close"].iloc[-1]
aporte_unico_valor = dca_total_aportado * preco_final / preco_inicial
print(f"DCA R$1.000/mês em PETR4 (10a)")
print(f" Capital aportado: R$ {dca_total_aportado:,.0f}")
print(f" Valor final: R$ {dca_valor_final:,.0f}")
print(f" Lucro: R$ {dca_lucro:,.0f} ({dca_lucro/dca_total_aportado*100:.1f}%)")
print(f"Aporte único no dia 1: R$ {aporte_unico_valor:,.0f}")
# DCA R$1.000/mês em PETR4 (10a)
# Capital aportado: R$ 120,000
# Valor final: R$ 178,540
# Lucro: R$ 58,540 (48.8%)
# Aporte único no dia 1: R$ 344,760
Aporte único vence DCA em nominal porque capital fica exposto por mais tempo em um ativo que subiu. O contra-argumento do DCA é comportamental: aportar R$ 120 mil de uma vez exige patrimônio acumulado que a maioria não tem, e compra escalonada reduz risco de timing ruim. Ambos são estratégias válidas — mas são estratégias diferentes e devem ser avaliadas com métricas compatíveis.
Métricas: Sharpe, Sortino, drawdown máximo, CAGR
Retorno bruto não é suficiente. Duas estratégias com o mesmo retorno podem ter perfis de risco radicalmente diferentes. O Sharpe ratio mede retorno em excesso sobre o risk-free por unidade de volatilidade total; Sortino penaliza apenas volatilidade negativa; o drawdown máximo quantifica a pior perda a partir de um pico. Tabela de referência:
| Métrica | Fórmula resumida | Interpretação |
|---|---|---|
| CAGR | (V_final/V_inicial)^(1/anos) - 1 |
Retorno anualizado geométrico. Ibovespa histórico: ~10% a.a. nominal. |
| Volatilidade anual | std(retorno_diário) * √252 |
Dispersão anualizada. Ibovespa: ~24%; blue chip BR: 25%–40%. |
| Sharpe | (CAGR - CDI) / vol_anual |
Retorno em excesso por unidade de risco. Ibovespa histórico ~0,2; S&P 500 ~0,5. |
| Sortino | (CAGR - CDI) / vol_negativa |
Igual ao Sharpe, mas desvio só de retornos negativos. Costuma ser > Sharpe. |
| Max Drawdown | min(equity / cummax(equity) - 1) |
Pior queda a partir de topo. Ibovespa 2008: -55%; PETR4 2015: -60%. |
| Win rate | #trades_positivos / #trades |
% de operações lucrativas. Pode ser baixo em estratégias de trend-following. |
Para o Sharpe com rigor, é necessário descontar o CDI acumulado no mesmo período, não uma taxa anualizada estática. O CDI oscilou entre 2% (mínima 2020) e 14,25% (atual 2026), então assumir um número fixo distorce o resultado em janelas longas. O endpoint /macro/cdi para dados macroeconômicos entrega a série diária histórica pronta:
# Sharpe vs CDI para PETR4 buy-and-hold em 10 anos
r = httpx.get(
f"{BASE}/macro/cdi",
params={"start": "2016-04-01", "end": "2026-04-01", "limit": 5000},
headers={"X-API-Key": API_KEY},
)
cdi_raw = r.json()["data"]
cdi = pd.DataFrame(cdi_raw)
cdi["date"] = pd.to_datetime(cdi["date"])
cdi = cdi.set_index("date").sort_index()
# CDI publicado pelo BCB é taxa anualizada em %; convertendo para diária:
cdi["daily"] = (1 + cdi["value"] / 100) ** (1 / 252) - 1
# Alinhar CDI e PETR4 no mesmo calendário
merged = petr4[["adjusted_close"]].join(cdi[["daily"]], how="inner")
merged["ret"] = merged["adjusted_close"].pct_change()
merged["excess"] = merged["ret"] - merged["daily"]
merged = merged.dropna()
sharpe = merged["excess"].mean() / merged["excess"].std() * (252 ** 0.5)
# Sortino: desvio só dos retornos negativos
neg = merged["excess"][merged["excess"] < 0]
sortino = merged["excess"].mean() / neg.std() * (252 ** 0.5)
# Max drawdown
equity = (1 + merged["ret"]).cumprod()
max_dd = (equity / equity.cummax() - 1).min()
print(f"PETR4 vs CDI (2016-2026)")
print(f" Sharpe ratio: {sharpe:.2f}")
print(f" Sortino ratio: {sortino:.2f}")
print(f" Max drawdown: {max_dd*100:.1f}%")
# PETR4 vs CDI (2016-2026)
# Sharpe ratio: 0.43
# Sortino ratio: 0.64
# Max drawdown: -42.1%
Sharpe de 0,43 para PETR4 em dez anos é acima do Ibovespa agregado (~0,20) mas ainda abaixo do S&P 500 americano (~0,50) no mesmo período. O drawdown máximo de -42% corresponde ao choque de petróleo de 2020 combinado com política doméstica. Antes de colocar dinheiro em uma estratégia com esse perfil, a pergunta honesta é: o investidor conseguiria ver o patrimônio cair 42% sem zerar posição?
Armadilhas comuns: look-ahead bias, survivorship bias, overfitting
Look-ahead bias
Acontece quando o backtest usa informação que não estava disponível no momento da decisão. O exemplo clássico é calcular um sinal com o fechamento do dia e "comprar" no mesmo fechamento — na prática, o cálculo só existe depois do pregão. Earnings divulgados 45 dias após o fim do trimestre também geram o problema: filtrar empresas pelo P/L do Q3 em 1º de outubro significa usar dado que só foi público em meados de novembro. O shift(1) aplicado nos exemplos é o remédio mais comum: o sinal calculado em t só é executado em t+1.
Survivorship bias
Testar uma estratégia hoje na composição atual do Ibovespa ignora empresas que faliram, sofreram OPA ou saíram de bolsa. OGX, OSX, IRBR3 pós-2019, empresas X em geral — todas sumiram do IBOV, e nenhum backtest feito em cima da composição de 2026 vai "conhecer" essas perdas. O viés infla sistematicamente os retornos simulados. Mitigação: usar carteiras point-in-time, ou ao menos incluir tickers delistados disponíveis no histórico.
Overfitting
É a tentação de otimizar parâmetros até o backtest ficar perfeito. Testar SMA de 5 a 200 em combinações de pares produz centenas de Sharpes; o melhor par in-sample quase sempre é ruído estatístico. Um Sharpe de 2,5 in-sample que cai para 0,3 out-of-sample é sintoma de estratégia fitada na amostra. Defesa: separar período de validação (20%–30% da amostra) antes de olhar qualquer número, e preferir estratégias cuja performance é estável numa vizinhança de parâmetros próximos do ótimo.
Custos ignorados
Home brokers modernos (Clear, Rico, XP) zeraram corretagem em ações à vista, mas B3 cobra emolumentos (~0,0275%) e taxa de liquidação (~0,0250%) em cada ponta. IR é 15% sobre ganho de capital em swing trade (isento até R$ 20 mil/mês em vendas totais) e 20% em day trade (sem isenção, com IR retido na fonte). Slippage — diferença entre preço cotado e preço executado — costuma ser 0,05%–0,20% em ativos líquidos e muito maior em small caps. Estratégias com 200 trocas anuais podem perder 2%–5% ao ano só em fricção, o que converte um Sharpe positivo em negativo. Uma comparação detalhada entre fontes de dados que impactam esses cálculos está em bolsai vs brapi.
Para estratégias baseadas em fundamentos — filtrar ações por P/L, ROE ou dividend yield antes de rodar o backtest — o screener programático de ações brasileiras mostra como construir o universo inicial. Combinar screening com backtesting é o caminho para factor investing no Brasil.
Perguntas frequentes
Backtesting positivo garante lucro real?
Não. Backtesting mede performance em dados passados; resultado positivo é condição necessária, não suficiente. Overfitting, survivorship bias e custos subestimados costumam inflar retornos simulados. Estratégias que ganham fora da amostra (walk-forward) e mantêm robustez com parâmetros próximos do ótimo são mais confiáveis. Paper trading por alguns meses antes de expor capital real é recomendação padrão em mesas quantitativas.
Qual o melhor framework Python para backtesting?
backtesting.py é didático e ótimo para estratégias de um ativo, com suporte a otimização via grid search. vectorbt é ordens de magnitude mais rápido e lida bem com múltiplos ativos e parâmetros, ideal para factor investing ou otimização em lote. Para projetos profissionais com modelagem fiel de execução, custos, slippage e risk management, Zipline-Reloaded ou QuantConnect LEAN continuam sendo referência. Pandas puro é suficiente para o que este guia demonstra.
Como considerar custos operacionais no backtesting?
Inclua corretagem (mesmo zero em home brokers modernos, pode existir em operações institucionais), emolumentos B3 (~0,0275%), taxa de liquidação (~0,0250%), ISS sobre corretagem quando aplicável e IR sobre ganho de capital (15% swing trade, 20% day trade, com IR retido na fonte). Slippage de 0,05%–0,20% por ordem também é saudável para aproximar execução real em ativos líquidos; small caps podem ter slippage de 0,5%–1%.
A API da bolsai ajusta preços para dividendos?
Sim. O endpoint /stocks/{ticker}/history retorna tanto os campos brutos (open, high, low, close) quanto os ajustados (adjusted_open, adjusted_high, adjusted_low, adjusted_close), com correção retroativa por splits, grupamentos e dividendos. Backtests devem usar a série ajustada para não criar gaps artificiais em datas ex-dividendo, que contaminariam cálculos de retorno e de volatilidade.
Qual período histórico usar para backtesting?
Depende da estratégia. Para médias móveis e momentum, 10 a 15 anos cobrem múltiplos ciclos de taxa de juros e crises (2008, 2015–2016, 2020). Estratégias intraday precisam de menos tempo calendário mas mais granularidade temporal. Sempre reserve ao menos 20% da amostra para teste fora da amostra (out-of-sample) — dados nunca vistos na calibração. O histórico da bolsai vai até 1986 em tickers que existem há tanto tempo, o que permite análise de múltiplos regimes monetários.
Leia também
- Dados históricos de ações da B3 via API Python — histórico OHLC ajustado desde 1986, paginação e exportação para pandas
- API de Selic, CDI e IPCA em Python — séries macroeconômicas do BCB e uso do CDI como risk-free em métricas de performance
- Screener de ações brasileiras em Python — filtragem por múltiplos indicadores fundamentalistas para construção de universos
Próximos passos
Backtesting python ações brasileiras com rigor exige duas coisas que a bolsai entrega: OHLC ajustado confiável desde 1986 e CDI diário para cálculo correto de Sharpe. A conta gratuita dá 200 requisições por dia — o suficiente para baixar 10 anos de um ativo e rodar os três backtests deste guia. O plano Pro expande o volume para estratégias que envolvem centenas de tickers.
Disclaimer: este conteúdo tem caráter educacional e não constitui recomendação de investimento. Backtesting mede performance histórica; resultados passados não garantem retorno futuro. Análise de risco, adequação de perfil e consulta a profissional habilitado são de responsabilidade do leitor.