Tutorial por bolsai 19 de abril de 2026 14 min de leitura

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.

Ver documentação completa

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

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.