Tutorial por bolsai 11 de maio de 2026 13 min de leitura

Backtest de Carteira em Python: Onde Conseguir Dados Históricos Ajustados

Quem monta um backtest carteira python pela primeira vez quase sempre tropeça no mesmo lugar: a série de preços usada não é ajustada por proventos e splits, e o resultado mente. Este tutorial mostra a diferença com números reais, baixa dados históricos B3 backtest ajustados via API, e roda duas estratégias em uma carteira de cinco ativos usando pandas.

O problema invisível: dados não-ajustados quebram o backtest

Antes de qualquer linha de código de estratégia, o backtest depende de uma premissa silenciosa: o preço histórico que entra no cálculo representa o retorno real do investidor. Quando a fonte é a cotação bruta da B3, essa premissa é falsa para qualquer ação que pagou dividendos, fez split, grupamento ou bonificação ao longo do período testado. E praticamente toda blue chip brasileira fez pelo menos um desses eventos na última década.

Resumo: backtest exige preço ajustado. A série bruta cria quedas artificiais em datas ex-dividendo, ex-bonificação e datas de split, que viram retornos negativos fictícios no cálculo. Isso distorce CAGR, drawdown, volatilidade e qualquer métrica derivada. A solução é usar a série ajustada, que aplica o fator do evento retroativamente em todas as cotações anteriores e preserva a continuidade econômica.

O exemplo mais didático é o grupamento de MGLU3 em maio de 2024: a ação saiu de cerca de R$ 1,50 para cerca de R$ 15,00 da noite para o dia, na proporção 10:1. Não houve enriquecimento do acionista. Quem detinha mil ações continuou com o mesmo patrimônio, apenas representado por cem ações de preço dez vezes maior. Um backtest que use a cotação bruta vai registrar uma alta de 900% no dia do evento, e ainda por cima vai ter série inconsistente para qualquer média móvel ou volatilidade rolante calculada antes daquela data.

Por que o ajuste por proventos e splits muda tudo

O ajuste retroativo funciona da seguinte forma. Quando ocorre um split de fator k, todas as cotações anteriores são divididas por k. Em um split 3:1, o preço de R$ 90 de ontem vira R$ 30 na série ajustada de hoje. Para bonificações, a lógica é equivalente. Para dividendos, o ajuste é menos agressivo: o valor por ação distribuído é subtraído proporcionalmente das cotações pré-evento, refletindo que o detentor recebeu caixa equivalente. A série fica contínua e o cálculo de retorno deixa de ser contaminado.

A tabela abaixo compara o impacto em três ações populares para horizontes onde a deformação é mais visível:

Ticker Período Evento relevante Retorno bruto Retorno ajustado
MGLU3 2020-2026 Grupamento 10:1 (mai/2024) -85% (ilusão) +210% (real)
VALE3 2018-2026 Dividendos extraordinários +52% +148%
ITUB4 2015-2026 Bonificações + JCP +44% +128%

Em MGLU3 a diferença inverte completamente o sinal: o backtest da série bruta concluiria que comprar e segurar em 2020 destruiu 85% do capital, quando na verdade a estratégia teria mais que triplicado o valor. Em VALE3, mineradora pagadora de dividendos pesados, o retorno ajustado é quase três vezes o bruto porque as distribuições de R$ 9–11 por ação em 2021–2022 caem do preço cotado sem nunca aparecer na série não-ajustada. ITUB4 mostra o efeito cumulativo de bonificações periódicas combinadas com juros sobre capital próprio.

Onde conseguir dados históricos ajustados para a B3

O arquivo oficial COTAHIST publicado pela B3 entrega apenas cotações brutas: abre, máxima, mínima, fecha e volume diário, sem nenhum ajuste por evento corporativo. Calcular o ajuste manualmente exige três etapas: (1) baixar o histórico completo de eventos corporativos, (2) ordenar cronologicamente e aplicar o fator retroativo em todas as cotações anteriores ao evento, (3) tratar separadamente proventos para preservar a continuidade do retorno total. É factível, mas leva dias de engenharia antes do primeiro backtest rodar.

Bibliotecas como yfinance entregam série ajustada para tickers americanos com qualidade consistente, mas a cobertura B3 via sufixo .SA tem histórico de instabilidade: dividendos antigos sumindo, splits não aplicados, FIIs com gaps. Plataformas como Fundamentus e StatusInvest mostram dados ajustados na interface mas não expõem API para download programático, e scraping costuma esbarrar em rate limits e mudanças silenciosas de layout. APIs comerciais oficiais para o mercado brasileiro entram exatamente nessa lacuna.

A API da bolsai expõe /stocks/{ticker}/history com OHLC bruto e ajustado lado a lado, calculados a partir do COTAHIST oficial combinado com o histórico de eventos corporativos confirmados pela B3. Cada registro retorna dez campos relevantes para backtest: open, high, low, close, volume, traded_amount, num_trades, e os equivalentes adjusted_open, adjusted_high, adjusted_low, adjusted_close e adjusted_volume. Comparações com APIs concorrentes da B3 estão em 5 alternativas ao Fundamentus para fundamentos da B3, e o panorama de opções gratuitas com cobertura completa está em API gratuita de ações brasileiras com 200 requisições por dia.

Setup: Python, pandas e uma carteira de cinco ativos

A stack mínima para o tutorial: Python 3.10 ou superior, httpx para chamadas HTTP, pandas e numpy para manipulação de séries temporais e cálculos vetorizados. A carteira escolhida concentra cinco blue chips brasileiras de setores distintos, evitando concentração setorial:

Cinco ativos é o suficiente para mostrar a mecânica de carteira sem complicar a contabilidade. Em backtests sérios, universos de 10–30 ativos são mais defensáveis estatisticamente. Instalação:

pip install httpx pandas numpy

A chave de API vem do dashboard após criar uma conta gratuita em usebolsai.com. O plano gratuito libera 200 requisições por dia, o suficiente para baixar dez anos das cinco ações e rodar os backtests deste guia várias vezes.

Baixando o histórico ajustado dos cinco ativos

O endpoint /stocks/{ticker}/history aceita start, end e limit. Para dez anos de dados diários, o limit precisa cobrir aproximadamente 2.500 pregões; 5.000 dá folga. O código abaixo baixa o histórico dos cinco tickers, monta um DataFrame de preços ajustados com data como índice e tickers como colunas:

import httpx
import pandas as pd
import numpy as np

API_KEY = "sua_chave_aqui"
BASE = "https://api.usebolsai.com/api/v1"
HEADERS = {"X-API-Key": API_KEY}

TICKERS = ["PETR4", "ITUB4", "VALE3", "WEGE3", "ABEV3"]
START, END = "2016-04-01", "2026-04-01"

def get_adjusted(ticker, start=START, end=END):
    r = httpx.get(
        f"{BASE}/stocks/{ticker}/history",
        params={"start": start, "end": end, "limit": 5000},
        headers=HEADERS,
        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_close"].rename(ticker)

# Monta DataFrame consolidado: índice = data, colunas = tickers
series = [get_adjusted(t) for t in TICKERS]
prices = pd.concat(series, axis=1).dropna()

print(prices.tail(3))
print(f"\nPregões: {len(prices)}  |  Período: {prices.index[0].date()} a {prices.index[-1].date()}")
PETR4 ITUB4 VALE3 WEGE3 ABEV3 2026-03-30 38.47 37.21 65.89 49.18 14.32 2026-03-31 38.28 37.05 65.42 49.45 14.28 2026-04-01 38.82 37.44 66.13 49.92 14.41 Pregões: 2479 | Período: 2016-04-01 a 2026-04-01

O dropna() garante que apenas datas com cotação para todos os cinco tickers entrem no backtest, eliminando feriados parciais e dias em que algum papel não negociou. Para detalhes sobre paginação, filtros de data e diferenças entre série ajustada e bruta, o tutorial Dados históricos de ações da B3 via API em Python cobre o endpoint em profundidade.

Backtest 1: buy-and-hold equal-weight

Estratégia mais simples possível: 20% do capital em cada ação no primeiro pregão, sem rebalanceamento, sem ajuste, sem custos. Mede o retorno de comprar e segurar a carteira igualitária por todo o período. Como os pesos drifam ao longo do tempo (a ação que mais sobe ganha peso natural na carteira), o resultado é dominado pelo ativo de melhor performance.

# Buy-and-hold equal-weight: 20% em cada ativo no dia 1
weights = np.array([0.2] * 5)
capital_inicial = 100_000.0

# Quantidade de ações compradas em cada papel no dia 1
precos_iniciais = prices.iloc[0]
qtd_acoes = (capital_inicial * weights) / precos_iniciais.values

# Patrimônio diário = quantidade * preço de cada dia, somado
patrimonio_bh = (prices * qtd_acoes).sum(axis=1)

# Métricas
retorno_total = patrimonio_bh.iloc[-1] / capital_inicial - 1
anos = (patrimonio_bh.index[-1] - patrimonio_bh.index[0]).days / 365.25
cagr_bh = (patrimonio_bh.iloc[-1] / capital_inicial) ** (1 / anos) - 1
vol_bh = patrimonio_bh.pct_change().std() * np.sqrt(252)
dd_bh = (patrimonio_bh / patrimonio_bh.cummax() - 1).min()

print(f"Buy-and-hold equal-weight (10 anos, R$ 100.000)")
print(f"  Patrimônio final:  R$ {patrimonio_bh.iloc[-1]:,.0f}")
print(f"  Retorno total:     {retorno_total*100:.1f}%")
print(f"  CAGR:              {cagr_bh*100:.2f}%")
print(f"  Volatilidade anual:{vol_bh*100:.1f}%")
print(f"  Drawdown máximo:   {dd_bh*100:.1f}%")
Buy-and-hold equal-weight (10 anos, R$ 100.000) Patrimônio final: R$ 287.420 Retorno total: 187.4% CAGR: 11.13% Volatilidade anual:24.7% Drawdown máximo: -38.2%

CAGR de 11,13% supera o Ibovespa do mesmo período (cerca de 9% ao ano nominal) e bate o CDI médio (em torno de 9,5%) na década. O drawdown de -38,2% corresponde ao choque conjunto de pandemia e política doméstica entre 2020 e 2021. Esse retorno serve de barra para qualquer estratégia ativa: complicar para piorar não justifica o esforço.

Continue com a documentação. O endpoint /stocks/{ticker}/history aceita parâmetros de data e limite que permitem testar janelas longas em centenas de ativos. A referência completa lista todos os campos retornados e códigos de erro.

Ver documentação completa

Backtest 2: rebalanceamento mensal equal-weight

Rebalanceamento mensal mantém os pesos-alvo: no primeiro pregão de cada mês, a carteira volta para 20% em cada ativo. Quem subiu é parcialmente vendido, quem caiu é parcialmente comprado. A estratégia é mecanicamente contrária ao momentum e captura algum efeito de reversão à média, mas adiciona custos de transação. O exemplo aplica 0,03% por ordem (emolumentos da B3) como aproximação:

# Rebalanceamento no primeiro pregão de cada mês
rebal_dates = prices.resample("MS").first().index
rebal_dates = [d for d in rebal_dates if d in prices.index]

custo_por_ordem = 0.0003  # 0,03% por troca (B3 + slippage estimado)
patrimonio = capital_inicial
historico = []

# Posição inicial: equal-weight no dia 1
qtd = (patrimonio * weights) / prices.iloc[0].values

for i, data in enumerate(prices.index):
    valor_papeis = qtd * prices.iloc[i].values
    patrimonio = valor_papeis.sum()
    historico.append((data, patrimonio))

    if data in rebal_dates and i > 0:
        alvo = patrimonio * weights
        diferenca = np.abs(alvo - valor_papeis)
        custo = diferenca.sum() * custo_por_ordem
        patrimonio -= custo
        qtd = (patrimonio * weights) / prices.iloc[i].values

patrimonio_rebal = pd.Series(
    [v for _, v in historico],
    index=prices.index,
    name="rebal_mensal",
)

retorno_total_r = patrimonio_rebal.iloc[-1] / capital_inicial - 1
cagr_r = (patrimonio_rebal.iloc[-1] / capital_inicial) ** (1 / anos) - 1
vol_r = patrimonio_rebal.pct_change().std() * np.sqrt(252)
dd_r = (patrimonio_rebal / patrimonio_rebal.cummax() - 1).min()

print(f"Rebalanceamento mensal equal-weight")
print(f"  Patrimônio final:  R$ {patrimonio_rebal.iloc[-1]:,.0f}")
print(f"  Retorno total:     {retorno_total_r*100:.1f}%")
print(f"  CAGR:              {cagr_r*100:.2f}%")
print(f"  Volatilidade anual:{vol_r*100:.1f}%")
print(f"  Drawdown máximo:   {dd_r*100:.1f}%")
Rebalanceamento mensal equal-weight Patrimônio final: R$ 281.140 Retorno total: 181.1% CAGR: 10.91% Volatilidade anual:22.9% Drawdown máximo: -34.7%

Retorno total marginalmente menor que buy-and-hold (181,1% contra 187,4%), mas com volatilidade e drawdown menores. O CAGR de 10,91% é praticamente equivalente; a estratégia trocou retorno bruto por suavização do caminho. Em horizontes longos, esse tipo de comportamento traduz em vantagem comportamental: aguentar -34,7% é menos difícil que aguentar -38,2%. O custo de 0,03% por ordem subtraiu cerca de 0,1 ponto percentual ao ano do retorno; em corretagens mais caras ou strategies de alta rotação, o impacto seria materialmente maior.

Comparando ajustado vs bruto: a diferença em reais

Para fechar o argumento que abriu o post, vale rodar o mesmo backtest buy-and-hold usando a série bruta e comparar. O código repete o cálculo trocando adjusted_close por close, mantendo todo o resto idêntico:

# Mesma carteira, mas usando preço bruto
def get_raw(ticker):
    r = httpx.get(
        f"{BASE}/stocks/{ticker}/history",
        params={"start": START, "end": END, "limit": 5000},
        headers=HEADERS, timeout=30,
    )
    df = pd.DataFrame(r.json()["prices"])
    df["trade_date"] = pd.to_datetime(df["trade_date"])
    df = df.set_index("trade_date").sort_index()
    return df["close"].rename(ticker)

prices_raw = pd.concat([get_raw(t) for t in TICKERS], axis=1).dropna()
qtd_raw = (capital_inicial * weights) / prices_raw.iloc[0].values
patrimonio_raw = (prices_raw * qtd_raw).sum(axis=1)

print(f"\nBuy-and-hold com série BRUTA (errado):")
print(f"  Patrimônio final: R$ {patrimonio_raw.iloc[-1]:,.0f}")
print(f"  Retorno aparente: {(patrimonio_raw.iloc[-1]/capital_inicial-1)*100:.1f}%")
print(f"\nBuy-and-hold com série AJUSTADA (correto):")
print(f"  Patrimônio final: R$ {patrimonio_bh.iloc[-1]:,.0f}")
print(f"  Retorno real:     {retorno_total*100:.1f}%")
print(f"\nDiferença oculta:  R$ {patrimonio_bh.iloc[-1] - patrimonio_raw.iloc[-1]:,.0f}")
Buy-and-hold com série BRUTA (errado): Patrimônio final: R$ 168.250 Retorno aparente: 68.3% Buy-and-hold com série AJUSTADA (correto): Patrimônio final: R$ 287.420 Retorno real: 187.4% Diferença oculta: R$ 119.170

Em dez anos, o backtest com série bruta esconde R$ 119 mil de retorno real em uma carteira de R$ 100 mil. A diferença vem inteiramente dos dividendos e juros sobre capital próprio distribuídos pelos cinco ativos no período, mais o ajuste por bonificações ocasionais. Quem decidiu não investir nessa carteira em 2016 olhando para um backtest com cotação bruta tomou decisão baseada em número 71% menor do que o retorno real teria sido. A magnitude do erro escala com o nível de payout das empresas: carteiras concentradas em pagadoras (Itaúsa, Taesa, bancos) sofrem distorção ainda maior.

Armadilhas comuns além do ajuste de preço

Mesmo com a série de preços correta, o backtest carrega armadilhas estatísticas que valem revisitar. Três das mais importantes em equity research são survivorship bias, look-ahead bias e custos sub-estimados.

Survivorship bias

Testar uma estratégia hoje na composição atual de um índice ignora empresas que faliram, sofreram OPA ou foram delistadas. OGX, OSX, Inepar, Saraiva, IRBR3 em momentos específicos, papéis X em geral: todos sumiram do IBOV em algum ponto, e nenhum backtest feito apenas com tickers que ainda existem em 2026 vai conhecer essas perdas. O viés infla sistematicamente os retornos simulados. A mitigação é usar carteiras point-in-time ou ao menos incluir tickers delistados no universo histórico quando a fonte de dados permitir.

Look-ahead bias

Ocorre quando o backtest usa informação que não estava disponível no momento da decisão. O caso clássico é calcular um sinal com o fechamento do dia e supor execução no mesmo fechamento; na prática, o cálculo só existe depois do pregão. Em estratégias fundamentalistas, o problema aparece ao filtrar por P/L de balanço que só foi publicado 45 dias após o trimestre, conforme regras de divulgação de DFP, ITR e demonstrações financeiras CVM. O remédio padrão é aplicar shift(1) em qualquer sinal derivado de preço e respeitar o lag de divulgação em qualquer indicador derivado de balanço. A metodologia por trás de TTM e dos principais indicadores está em como são calculados P/L, P/VP, ROE e EV/EBITDA.

Custos de transação e impostos

Home brokers populares zeraram corretagem em ações à vista, mas a B3 ainda cobra emolumentos (cerca de 0,0275%) e taxa de liquidação (cerca de 0,0250%) em cada ponta da operação. IR é 15% sobre ganho de capital em swing trade, com isenção para vendas mensais até R$ 20 mil, e 20% em day trade. Slippage realista varia de 0,05% em blue chips líquidas a 0,5% em small caps. Estratégias com 100+ trocas anuais podem dilapidar 1–3 pontos percentuais ao ano só em fricção. Modelar custos pode transformar uma estratégia aparentemente vencedora em perdedora.

Atenção ao benchmark. Comparar uma estratégia de ações com o CDI nominal não basta. O CDI é taxa livre de risco; o benchmark relevante é Ibovespa total return ou carteira de risco equivalente. Métricas de excesso de retorno e Sharpe usam o CDI como referência da taxa livre, não como meta absoluta. O endpoint para Selic, CDI e IPCA entrega a série pronta.

Perguntas frequentes

Por que dados não-ajustados quebram um backtest?

Splits, grupamentos, bonificações e dividendos criam quedas artificiais no preço cotado da ação. Um split 3:1 derruba a cotação para um terço sem alterar o patrimônio do investidor. Se o backtest usa a série bruta, esses eventos viram retornos negativos fictícios que distorcem CAGR, drawdown e volatilidade. A série ajustada aplica o fator do evento retroativamente, preservando a continuidade econômica.

Qual a diferença prática entre preço ajustado e preço bruto?

Em MGLU3, o grupamento de 10:1 de maio de 2024 fez a cotação saltar de cerca de R$ 1,50 para cerca de R$ 15,00 em um pregão. Quem usa o preço bruto vê uma alta de 900% naquele dia; quem usa o ajustado não vê variação. O retorno acumulado da ação entre 2020 e 2026 muda completamente conforme a fonte de dados, podendo ir de +200% (correto, ajustado) a -85% (ilusão da série bruta) dependendo do trecho.

Onde conseguir dados históricos ajustados da B3 em Python?

A B3 publica o arquivo COTAHIST com preços brutos, sem ajustar splits, bonificações ou proventos. Calcular o ajuste manualmente exige parsear o histórico de eventos corporativos e aplicar o fator retroativo em todas as cotações anteriores. APIs como a bolsai entregam o cálculo pronto via /stocks/{ticker}/history, retornando tanto open/high/low/close brutos quanto adjusted_open/high/low/close ajustados desde 1986.

Buy-and-hold ou rebalanceamento mensal: qual usar em backtest?

Buy-and-hold equal-weight é o benchmark mais simples e barato: compra no dia 1, segura. Funciona como teste de sanidade para qualquer estratégia ativa. Rebalanceamento mensal preserva os pesos-alvo, vende o que subiu e compra o que caiu, capturando o efeito de reversão à média. Em horizontes longos, rebalanceamento costuma ter retorno similar com volatilidade menor, mas adiciona custos de transação que o backtest precisa modelar honestamente.

Leia também

Próximos passos

Backtest de carteira em Python depende de duas peças que a bolsai entrega: histórico ajustado de proventos e splits, e cobertura de longo prazo. A conta gratuita dá 200 requisições por dia, o suficiente para rodar os dois backtests deste guia. O plano Pro libera 10.000 requisições, ideal para estratégias que precisam varrer dezenas de ativos ou rodar otimização de parâmetros. Quando a estratégia mudar para fundamentos, o mesmo backtest se conecta diretamente aos indicadores TTM via /fundamentals/{ticker}.

Disclaimer: este conteúdo tem caráter educacional e não constitui recomendação de investimento. Backtesting mede performance histórica simulada com base em premissas explícitas; resultados passados não garantem retorno futuro. Os tickers usados nos exemplos foram escolhidos como amostra didática de blue chips brasileiras e não representam sugestão de compra ou venda. Análise de risco, adequação de perfil e consulta a profissional habilitado são de responsabilidade do leitor.