← Voltar aos artigos
March 6, 2026
5 min read

Monte Carlo Bootstrap: como obter intervalos de confiança para um backtest em 10 linhas de código

#algotrading
#backtest
#Monte Carlo
#bootstrap
#confidence intervals
#risk management
#statistics
🎯
Part 5 of 9 · Collection
Backtesting Without Fooling Yourself

📄 Este artigo deu origem a um artigo científico. Os intervalos de confiança bootstrap descritos aqui passam por um teste controlado de cobertura sob dependência serial (iid vs. bootstrap por trade vs. block bootstrap, 6.000 experimentos com verdade exatamente conhecida). Leia o artigo online (versão interativa + PDF) em bootstrap.marketmaker.cc, código e dados em github.com/suenot/bootstrap-coverage.

Você executou uma estratégia em um backtest. Obteve PnL +42%, Sharpe 1,8, MaxDD -12%. Os resultados parecem excelentes. Você lança o bot em produção, e um mês depois descobre que o drawdown já está em -28% e o PnL está caminhando para zero.

O que deu errado? Não é um bug nem "o mercado mudou". O problema é que você tomou uma decisão baseada em um único número — uma estimativa pontual. Você aprendeu que a estratégia mostrou +42%, mas não aprendeu o quanto podia confiar nesse número.

O problema das estimativas pontuais

A single point estimate versus a full probability distribution Um único ponto de dados (à esquerda) fornece uma imagem enganosa, enquanto a distribuição completa (à direita) revela a verdadeira amplitude dos resultados possíveis.

Um backtest sobre dados históricos é uma única execução ao longo de uma sequência específica de eventos de mercado. O resultado depende da ordem dos trades: a mesma estratégia com os mesmos trades, mas em ordem diferente, pode mostrar um drawdown máximo completamente diferente.

Imagine 491 trades. Cada trade é um evento aleatório com uma determinada distribuição de retornos. O backtest histórico mostra apenas uma realização desse processo. É como jogar um dado uma vez e concluir que ele sempre cai no número quatro.

O que realmente precisamos:

  • Não uma estimativa pontual, mas um intervalo: "com 95% de probabilidade, o PnL final estará entre X e Y"
  • Não um único drawdown máximo, mas uma distribuição: "nos 5% piores cenários, o drawdown ultrapassa Z%"
  • Não a média, mas as caudas: o que acontece se a sorte não estiver do seu lado?

É exatamente para isso que serve o bootstrap Monte Carlo.

O que é o bootstrap Monte Carlo

Monte Carlo bootstrap resampling: thousands of alternative equity paths generated from trade data O bootstrap gera milhares de trajetórias alternativas de equity por meio de reamostragem de trades com reposição a partir do conjunto de dados original.

Bootstrap é um método de reamostragem proposto por Bradley Efron em 1979. A ideia é elegante: se temos uma amostra de dados, podemos gerar milhares de "novas" amostras selecionando aleatoriamente elementos da amostra original com reposição.

No contexto de um backtest, funciona assim:

  1. Você tem um array de retornos para cada trade — por exemplo, 491 valores
  2. Você seleciona aleatoriamente 491 valores desse array com reposição — alguns trades aparecerão duas vezes, outros não aparecerão de forma alguma
  3. Você constrói uma curva de equity a partir dessa nova amostra
  4. Você repete o processo 10.000 vezes
  5. Você obtém uma distribuição de métricas finais, não um único número

Cada iteração é um "cenário alternativo": o que poderia ter acontecido se a ordem e o conjunto de trades tivessem sido ligeiramente diferentes.

Implementação em 10 linhas

Aqui está uma implementação completa e funcional:

import numpy as np

def max_drawdown(equity_curve):
    """Calculate the maximum drawdown of an equity curve."""
    peak = np.maximum.accumulate(equity_curve)
    drawdown = (equity_curve - peak) / peak
    return drawdown.min()

trade_returns = [...]  # 491 values, e.g. [0.012, -0.005, 0.008, ...]

n_simulations = 10000
results = []

for _ in range(n_simulations):
    sampled = np.random.choice(trade_returns, size=len(trade_returns), replace=True)
    equity = np.cumprod(1 + sampled)
    results.append({
        "final_pnl": equity[-1] - 1,
        "max_dd": max_drawdown(equity),
        "sharpe": np.mean(sampled) / np.std(sampled) * np.sqrt(252)
    })

Tempo de execução: ~2 segundos em um laptop comum. 10.000 histórias alternativas da sua estratégia.

Extraindo intervalos de confiança

Confidence intervals for PnL, MaxDD, and Sharpe Ratio with 5th, 50th, and 95th percentiles Intervalos de confiança para as principais métricas da estratégia: PnL, MaxDD e Sharpe Ratio, mostrando as bandas dos percentis 5, 50 e 95.

Agora temos não um número, mas uma distribuição. Veja como extrair informações úteis dela:

import pandas as pd

df = pd.DataFrame(results)

pnl_5 = np.percentile(df['final_pnl'], 5)
pnl_50 = np.percentile(df['final_pnl'], 50)
pnl_95 = np.percentile(df['final_pnl'], 95)

dd_5 = np.percentile(df['max_dd'], 5)    # 5th — worst case
dd_50 = np.percentile(df['max_dd'], 50)
dd_95 = np.percentile(df['max_dd'], 95)  # 95th — best case

print(f"PnL:   {pnl_5:.1%} | {pnl_50:.1%} | {pnl_95:.1%}")
print(f"MaxDD: {dd_5:.1%} | {dd_50:.1%} | {dd_95:.1%}")
print(f"Sharpe: {np.percentile(df['sharpe'], 5):.2f}{np.percentile(df['sharpe'], 95):.2f}")

Exemplo de saída para uma estratégia real:

Metric 5th percentile (worst) Median 95th percentile (best)
PnL +18.3% +41.7% +72.1%
MaxDD -23.4% -12.8% -5.1%
Sharpe 1.12 1.76 2.41

Agora a diferença é evidente:

  • O backtest mostrou PnL +42% — mas nos 5% dos piores cenários, o PnL é de apenas +18,3%
  • O backtest mostrou MaxDD -12% — mas nos 5% dos piores cenários, o drawdown é de -23,4%
  • Sharpe 1,8 — mas o limite inferior é 1,12

O percentil 5 é o seu "pior cenário realista". Se a estratégia deixa de ser lucrativa no percentil 5, lançá-la em produção é arriscado.

Visualização: Fan Chart

O bootstrap Monte Carlo é visualizado naturalmente como um fan chart — um leque de curvas de equity:

import matplotlib.pyplot as plt

fig, axes = plt.subplots(1, 2, figsize=(16, 6))

ax = axes[0]
for i in range(min(500, n_simulations)):
    sampled = np.random.choice(trade_returns, size=len(trade_returns), replace=True)
    equity = np.cumprod(1 + sampled)
    ax.plot(equity, alpha=0.02, color='#4FC3F7')

all_equities = []
for _ in range(n_simulations):
    sampled = np.random.choice(trade_returns, size=len(trade_returns), replace=True)
    equity = np.cumprod(1 + sampled)
    all_equities.append(equity)

all_equities = np.array(all_equities)
p5 = np.percentile(all_equities, 5, axis=0)
p50 = np.percentile(all_equities, 50, axis=0)
p95 = np.percentile(all_equities, 95, axis=0)

ax.fill_between(range(len(p5)), p5, p95, alpha=0.3, color='#7C4DFF', label='90% CI')
ax.plot(p50, color='#E040FB', linewidth=2, label='Median')
ax.set_title('Monte Carlo Bootstrap: Equity Curves')
ax.legend()

ax = axes[1]
ax.hist(df['final_pnl'] * 100, bins=80, color='#4FC3F7', alpha=0.7, edgecolor='#1A237E')
ax.axvline(pnl_5 * 100, color='#FF5252', linestyle='--', label=f'5th: {pnl_5:.1%}')
ax.axvline(pnl_50 * 100, color='#E040FB', linestyle='--', label=f'Median: {pnl_50:.1%}')
ax.axvline(pnl_95 * 100, color='#69F0AE', linestyle='--', label=f'95th: {pnl_95:.1%}')
ax.set_title('Distribution of Final PnL')
ax.set_xlabel('PnL, %')
ax.legend()

plt.tight_layout()
plt.savefig('monte_carlo_fan_chart.png', dpi=150)
plt.show()

Um fan chart oferece uma compreensão intuitiva da dispersão dos resultados possíveis. Um leque estreito significa que a estratégia é estável. Um leque largo significa que o resultado depende fortemente da "sorte" quanto à ordem dos trades.

Monte Carlo Visualization: Fan Chart and Distribution Histogram O fan chart (à esquerda) mostra a dispersão das possíveis trajetórias de equity, e o histograma (à direita) mostra a distribuição de densidade dos retornos finais com os intervalos de confiança destacados (5%, 50%, 95%).

Análise avançada: probabilidade de ruína

Probability of ruin analysis: equity curves either surviving or falling into ruin Visualização da probabilidade de ruína: os caminhos de equity que sobrevivem (ciano) curvam-se para cima, enquanto os caminhos arruinados (vermelho) caem abaixo do limite de equity zero.

O bootstrap permite responder a uma pergunta crítica: qual é a probabilidade de a estratégia perder X% do capital?

ruin_threshold = -0.20
prob_ruin = (df['max_dd'] < ruin_threshold).mean()
print(f"P(MaxDD < -20%) = {prob_ruin:.1%}")

prob_loss = (df['final_pnl'] < 0).mean()
print(f"P(PnL < 0) = {prob_loss:.1%}")

worst_5pct = df['final_pnl'].quantile(0.05)
cvar = df[df['final_pnl'] <= worst_5pct]['final_pnl'].mean()
print(f"CVaR(5%) = {cvar:.1%}")

Essas métricas são impossíveis de obter a partir de uma única execução de backtest. No entanto, são fundamentais para a decisão de lançar uma estratégia.

Para saber mais sobre por que drawdowns profundos são matematicamente perigosos e como funciona a assimetria de retornos, leia nosso artigo Loss-Profit Asymmetry.

Quando o bootstrap clássico não funciona

O método tem limitações importantes de conhecer.

Autocorrelação dos retornos

O bootstrap clássico assume que os trades são independentes. Na realidade, isso muitas vezes não é o caso — uma estratégia pode ter sequências de ganhos e perdas. Se a autocorrelação for significativa, use o block bootstrap:

def block_bootstrap(returns, block_size=10, n_simulations=10000):
    """Bootstrap preserving local dependency structure."""
    n = len(returns)
    results = []

    for _ in range(n_simulations):
        starts = np.random.randint(0, n - block_size + 1, size=n // block_size + 1)
        sampled = np.concatenate([returns[s:s+block_size] for s in starts])[:n]
        equity = np.cumprod(1 + sampled)
        results.append({
            "final_pnl": equity[-1] - 1,
            "max_dd": max_drawdown(equity),
        })

    return pd.DataFrame(results)

O block bootstrap preserva as dependências locais entre trades consecutivos, fornecendo intervalos de confiança mais realistas para o MaxDD.

Block bootstrap resampling: sequential trade blocks shuffled and recombined O block bootstrap preserva a autocorrelação dentro dos blocos ao particionar a sequência de trades em blocos e reamostrá-los com reposição.

Não estacionariedade do mercado

O bootstrap trabalha com a distribuição original de trades. Se o mercado mudou estruturalmente (por exemplo, a volatilidade caiu ou a liquidez mudou), os trades históricos podem não ser representativos. Para levar isso em conta:

  • Use uma janela móvel: faça bootstrap apenas nos últimos N trades
  • Dê mais peso aos trades recentes: weighted bootstrap
  • Divida os dados por regimes de mercado e faça bootstrap separadamente

Número pequeno de trades

O bootstrap é confiável quando n > 30 trades. Se você tem 10 trades, nenhuma quantidade de reamostragem vai ajudar. 491 trades é uma amostra excelente; pode-se confiar nos resultados.

Comparação de abordagens para avaliar a robustez do backtest

Method What it provides Complexity Time When to use
Single backtest One point estimate Minimal Seconds Never as a final result
Walk-forward Out-of-sample metrics Medium Minutes To check for overfitting
Monte Carlo bootstrap Confidence intervals Minimal ~2 sec Always before production
Monte Carlo path New price paths High Minutes-hours For stress testing
Cross-validation Average metrics across folds Medium Minutes For parameter tuning

O bootstrap Monte Carlo é o único método que, em tempo mínimo, fornece um quadro completo dos riscos.

Checklist: interpretando os resultados

Assim recomendamos interpretar os resultados do bootstrap Monte Carlo:

Lançar em produção se:

  • o PnL no percentil 5 for positivo
  • o MaxDD no percentil 5 for aceitável para seu apetite ao risco
  • a probabilidade de ruína for < 1%
  • o Sharpe no percentil 5 for > 0,5

Precisa de ajustes se:

  • o PnL no percentil 5 estiver próximo de zero
  • o MaxDD no percentil 5 for significativamente pior do que no percentil 50
  • a dispersão do fan chart for ampla — a estratégia é instável

Não lançar se:

  • o PnL no percentil 5 for negativo
  • a probabilidade de ruína for > 5%
  • o intervalo de confiança do Sharpe incluir 0

Nossa experiência na marketmaker.cc

Na marketmaker.cc, desenvolvemos nosso próprio motor de backtest, e o bootstrap Monte Carlo é parte integrante do nosso pipeline. Cada estratégia passa pelo bootstrap automaticamente antes de ser aprovada para negociação ao vivo.

Integramos o bootstrap diretamente no motor de backtest: após uma execução, você obtém não apenas o PnL final, mas um relatório completo com intervalos de confiança, fan chart, probabilidade de ruína e uma comparação entre block bootstrap e bootstrap padrão. Isso leva 2-3 segundos adicionais — um preço insignificante para entender os riscos reais.

Pela nossa experiência: cerca de 30% das estratégias que parecem atraentes pela estimativa pontual são filtradas após o bootstrap Monte Carlo. O PnL delas no percentil 5 se torna negativo ou o MaxDD se mostra inaceitável. Sem o bootstrap, essas estratégias teriam ido para produção e muito provavelmente teriam resultado em perdas.

Conclusão

O bootstrap Monte Carlo são cerca de 10 linhas de código e cerca de 2 segundos de computação. Ele transforma um único número de um backtest em uma distribuição completa com intervalos de confiança. Este é talvez o maior ROI entre todas as ferramentas de análise quantitativa:

  • Custo mínimo: implementação em 30 minutos
  • Retorno máximo: compreensão dos riscos reais da estratégia
  • Sem dependências: apenas NumPy

Se você ainda não usa bootstrap — adicione-o ao seu pipeline hoje mesmo. É a única forma de saber o quanto você pode confiar nos resultados do seu backtest.


References

  1. Efron, B. — Bootstrap Methods: Another Look at the Jackknife (1979)
  2. Davison, A.C., Hinkley, D.V. — Bootstrap Methods and their Application (Cambridge)
  3. Aronson, D.R. — Evidence-Based Technical Analysis: Monte Carlo permutation
  4. QuantStart — Monte Carlo Simulation for Backtest Analysis
  5. Marcos Lopez de Prado — Advances in Financial Machine Learning, Chapter 12: Backtesting
  6. Kevin Davey — Building Winning Algorithmic Trading Systems: Monte Carlo Analysis
  7. NumPy — numpy.random.choice

Citation

@software{soloviov2026montecarlobootstrap,
  author = {Soloviov, Eugen},
  title = {Monte Carlo Bootstrap: How to Get Confidence Intervals for a Backtest in 10 Lines of Code},
  year = {2026},
  url = {https://marketmaker.cc/ru/blog/post/monte-carlo-bootstrap-backtest},
  version = {0.1.0},
  description = {Why a single-point estimate from a backtest is a dangerous illusion. How Monte Carlo bootstrap in 2 seconds of computation gives you a 95\% confidence interval for PnL and MaxDD, and why this is a mandatory step before launching a strategy in production.}
}
blog.disclaimer

Authors

Eugen Soloviov
Eugen Soloviov

Trading-systems engineer

Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.

Newsletter

Fique à frente do mercado

Assine nossa newsletter para insights exclusivos sobre trading com IA, análises de mercado e atualizações da plataforma.

Respeitamos sua privacidade. Cancele a inscrição a qualquer momento.