Strasmore Research
Deep Dives · Matt ConnorBy Matt Connor ·

Como calcular bandas de confiança no backtest

Um Sharpe de 1,4 pode ter intervalo de confiança de 95% amplo o suficiente para incluir zero. Veja como calcular e interpretar o bootstrap do backtest.

O intervalo de confiança de um backtest responde a uma pergunta: quanto da curva de patrimônio vem da estratégia e quanto depende do recorte específico da história em que ela foi executada. O bootstrap de um backtest constrói esse intervalo ao reamostrar a série de retornos muitas vezes, recalcular a estatística em cada nova amostra e observar os percentis dos resultados. Um Sharpe de 1,4 medido ao longo de um único ano de observações diárias tem um intervalo de 95% amplo o suficiente para incluir zero, e os painéis abaixo mostram por quê.

Por que uma única curva de capital é apenas uma amostra

Um backtest fornece um número para cada estatística: um índice de Sharpe, um retorno anualizado, o pior drawdown e uma taxa de acerto. Nenhum deles representa o valor verdadeiro da estratégia. Cada um é uma estimativa baseada em um período finito de pregões. Outro intervalo com a mesma duração teria produzido um resultado diferente.

O painel abaixo elimina completamente a estratégia da análise. Ele mede a posição mais simples possível: manter SPY, um ano-calendário de cada vez, com base nos retornos de preço entre fechamentos.

QueryUma posição, um ano por vez: Sharpe anualizado do SPY por ano-calendário
O SQL exato por trás de cada número
WITH daily AS
(
    SELECT
        date,
        toFloat64(close) / nullIf(lagInFrame(toFloat64(close), 1)
            OVER (ORDER BY date ROWS BETWEEN 1 PRECEDING AND CURRENT ROW), 0) - 1 AS ret
    FROM global_markets.stocks_daily_aggs
    WHERE ticker = 'SPY'
      AND date >= '2011-12-01'
      AND date <  '2026-01-01'
)
SELECT
    toString(toYear(date))                            AS year,
    count()                                           AS obs_count,
    round(avg(ret) * 252 * 100, 2)                    AS ann_return_pct,
    round(stddevSamp(ret) * sqrt(252) * 100, 2)       AS ann_vol_pct,
    round(avg(ret) / stddevSamp(ret) * sqrt(252), 2)  AS sharpe_ratio
FROM daily
WHERE date >= '2012-01-01'
  AND ret IS NOT NULL
GROUP BY year
ORDER BY year
Run this yourself

Cada linha cobre cerca de 250 pregões, próximo do padrão de um ano de negociação. A posição não mudou em nenhum desses períodos. Em 2012, o Sharpe anualizado foi de 1.06. Em 2025, foi de 0.88, com 14 anos representados no gráfico. Manter um índice amplo por um ano altera o número principal em uma magnitude que a maioria dos leitores dificilmente classificaria como ruído. Um backtest de estratégia com a mesma duração incorpora pelo menos essa variabilidade. A convenção de anualização usada na coluna é explicada no nosso guia sobre o índice de Sharpe. A mecânica dos retornos do período é apresentada em como os retornos mensais são medidos.

Qual é a amplitude da faixa em torno de um Sharpe de backtest?

O erro-padrão de uma estimativa de Sharpe cai aproximadamente com a raiz quadrada do número de observações. Em vez de depender dessa fórmula, meça diretamente a dispersão. Divida vinte anos de sessões em janelas não sobrepostas de comprimento fixo, calcule o Sharpe anualizado dentro de cada janela e observe a distância entre os resultados.

QueryDispersão observada do Sharpe em janelas não sobrepostas do SPY, de 2006 a 2025
O SQL exato por trás de cada número
WITH daily AS
(
    SELECT
        date,
        toFloat64(close) / nullIf(lagInFrame(toFloat64(close), 1)
            OVER (ORDER BY date ROWS BETWEEN 1 PRECEDING AND CURRENT ROW), 0) - 1 AS ret
    FROM global_markets.stocks_daily_aggs
    WHERE ticker = 'SPY'
      AND date >= '2005-12-01'
      AND date <  '2026-01-01'
),
indexed AS
(
    SELECT
        ret,
        row_number() OVER (ORDER BY date) AS i
    FROM daily
    WHERE date >= '2006-01-01'
      AND ret IS NOT NULL
),
blocks AS
(
    SELECT
        w,
        intDiv(i, w)                                            AS blk,
        count()                                                 AS n,
        avg(ret) / nullIf(stddevSamp(ret), 0) * sqrt(252)        AS sharpe
    FROM indexed
    CROSS JOIN (SELECT arrayJoin([21, 63, 126, 252, 504]) AS w) AS ws
    GROUP BY w, blk
    HAVING n = w
)
SELECT
    concat(toString(w), ' sessions')                          AS horizon,
    count()                                                   AS block_count,
    round(quantileDeterministic(0.05)(sharpe, blk), 2)        AS sharpe_p05,
    round(quantileDeterministic(0.50)(sharpe, blk), 2)        AS sharpe_p50,
    round(quantileDeterministic(0.95)(sharpe, blk), 2)        AS sharpe_p95,
    round(quantileDeterministic(0.95)(sharpe, blk)
        - quantileDeterministic(0.05)(sharpe, blk), 2)        AS sharpe_band_width
FROM blocks
WHERE sharpe IS NOT NULL
GROUP BY w
ORDER BY w
Run this yourself

Com 21 sessions por janela, o percentil 5 ao 95 do Sharpe medido vai de -3.37 a 6.97, uma dispersão de 10.34 pontos de Sharpe em 238 janelas. Estendendo cada janela para 504 sessions, a dispersão cai para 1.67 pontos, agora calculada sobre apenas 8 janelas. Duas coisas mudam ao mesmo tempo. A estimativa fica mais precisa com o aumento da amostra, mas a quantidade de amostras independentes que o histórico pode fornecer diminui. Essa tensão é o tema central.

Como fazer bootstrap do intervalo de confiança de um backtest

O procedimento é curto o suficiente para ser apresentado por completo.

  1. Comece pela série de retornos realizados da estratégia, com um valor por período, totalizando N valores.
  2. Extraia N retornos dessa lista aleatoriamente, com reposição. Alguns aparecerão duas vezes; outros, nenhuma.
  3. Recalcule a estatística na amostra reamostrada.
  4. Repita o processo várias milhares de vezes, mantendo todos os valores.
  5. Ordene os valores armazenados e leia os percentis de 2,5% e 97,5% para obter um intervalo de 95%.

Defina a semente do gerador de números aleatórios antes da etapa 2 e registre-a ao lado do intervalo publicado. O bootstrap é uma estimativa de Monte Carlo: duas execuções sem semente coincidem apenas aproximadamente nos últimos dígitos, e um revisor que não consiga reproduzir o intervalo não tem como verificá-lo. Essa é a mesma disciplina descrita em uma configuração reproduzível de backtest.

O retorno médio e o índice de Sharpe passam pela etapa 2 sem problemas, pois ambos tratam a lista de retornos como um conjunto. O drawdown máximo não. O drawdown considera a ordem da trajetória. Uma amostra reamostrada que reorganiza os retornos pode produzir um pior drawdown que não ocorreu em nenhuma ordenação da sequência real de trades. Ainda vale a pena aplicar bootstrap a essa métrica, desde que o resultado seja identificado corretamente: ele representa a distribuição do drawdown em históricos reordenados, não uma previsão do próximo drawdown. A definição está em drawdown máximo.

Por que o bootstrap IID está errado para os retornos do mercado

A Etapa 2 pressupõe que todos os retornos sejam independentes e identicamente distribuídos, a chamada hipótese IID. Os retornos diários violam essa hipótese de uma forma que altera a largura do intervalo. Os retornos com sinal apresentam apenas uma memória de um dia pouco relevante. Já as magnitudes se agrupam: movimentos fortes tendem a ocorrer próximos de outros movimentos fortes, enquanto dias tranquilos aparecem em sequência.

QueryAutocorrelação de primeira defasagem: retornos com sinal versus retornos absolutos, de 2016 a 2025
O SQL exato por trás de cada número
WITH daily AS
(
    SELECT
        ticker,
        date,
        toFloat64(close) / nullIf(lagInFrame(toFloat64(close), 1)
            OVER (PARTITION BY ticker ORDER BY date ROWS BETWEEN 1 PRECEDING AND CURRENT ROW), 0) - 1 AS ret
    FROM global_markets.stocks_daily_aggs
    WHERE ticker IN ('SPY', 'MSFT', 'KO', 'XOM', 'JNJ', 'PG')
      AND date >= '2015-11-01'
      AND date <  '2026-01-01'
),
lagged AS
(
    SELECT
        ticker,
        date,
        ret,
        lagInFrame(ret, 1) OVER (PARTITION BY ticker ORDER BY date ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS ret_prev
    FROM daily
    WHERE ret IS NOT NULL
      AND abs(ret) < 0.35
)
SELECT
    ticker,
    count()                                AS obs_count,
    round(corr(ret, ret_prev), 3)          AS return_autocorr,
    round(corr(abs(ret), abs(ret_prev)), 3) AS abs_return_autocorr
FROM lagged
WHERE date >= '2016-01-01'
  AND ret_prev IS NOT NULL
GROUP BY ticker
ORDER BY abs_return_autocorr DESC
Run this yourself

Para SPY, a autocorrelação de primeira defasagem dos retornos diários absolutos foi de 0.366 em 2514 sessões, contra -0.133 para os retornos com sinal nos mesmos dias. Compare as duas barras de qualquer ativo no gráfico. Embaralhar uma série de retornos elimina esse agrupamento, e um bootstrap IID aplicado a esses dados produz um intervalo mais estreito do que o suportado pela amostra. O erro ocorre na direção menos útil: favorece artificialmente a estratégia.

O bootstrap por blocos móveis e a escolha do comprimento do bloco

A correção é reamostrar blocos contíguos em vez de retornos individuais. Escolha um comprimento de bloco L, extraia aleatoriamente, com reposição, blocos de L retornos consecutivos e una-os até que a série sintética tenha N observações. A dependência dentro de cada bloco permanece intacta: os retornos mantêm a ordem original. Apenas as junções entre os blocos são artificiais.

O comprimento do bloco impõe um equilíbrio entre dois erros, e nenhuma configuração elimina ambos. Blocos curtos se comportam como o bootstrap IID e subestimam o intervalo. Isso é viés. Blocos longos preservam mais dependência, mas deixam menos blocos distintos disponíveis para a amostragem. Assim, cada reamostragem repete grandes trechos do mesmo histórico, e o próprio intervalo fica mais instável. Isso é variância. As regras práticas publicadas fazem o comprimento variar com N elevado à potência de um terço. Trate-as como pontos de partida.

Um diagnóstico mais simples é verificar como a variância escala com o horizonte. Sob independência, a variância da soma dos retornos de k dias é igual a k vezes a variância de um dia, e a razão entre as duas fica em 1.

QueryRazão de variância por tamanho do bloco: a variância do SPY escala como a de observações independentes?
O SQL exato por trás de cada número
WITH daily AS
(
    SELECT
        date,
        toFloat64(close) / nullIf(lagInFrame(toFloat64(close), 1)
            OVER (ORDER BY date ROWS BETWEEN 1 PRECEDING AND CURRENT ROW), 0) - 1 AS ret
    FROM global_markets.stocks_daily_aggs
    WHERE ticker = 'SPY'
      AND date >= '2005-12-01'
      AND date <  '2026-01-01'
),
indexed AS
(
    SELECT
        ret,
        row_number() OVER (ORDER BY date) AS i
    FROM daily
    WHERE date >= '2006-01-01'
      AND ret IS NOT NULL
),
base AS
(
    SELECT varSamp(ret) AS var_1d FROM indexed
),
blocks AS
(
    SELECT
        k,
        intDiv(i, k)  AS blk,
        count()       AS n,
        sum(ret)      AS block_ret
    FROM indexed
    CROSS JOIN (SELECT arrayJoin([2, 3, 5, 10, 21, 42, 63]) AS k) AS ks
    GROUP BY k, blk
    HAVING n = k
)
SELECT
    concat(toString(k), ' sessions')                          AS block_length,
    count()                                                   AS block_count,
    round(varSamp(block_ret) / (k * any(var_1d)), 3)          AS variance_ratio
FROM blocks
CROSS JOIN base
GROUP BY k
ORDER BY k
Run this yourself

Em 2 sessions, a razão medida foi 0.844; em 63 sessions, foi 0.584, calculada sobre 78 blocos não sobrepostos. Valores próximos de 1 indicam que a soma escala como ocorreria com observações independentes nesse horizonte. Valores distantes de 1 indicam os horizontes em que a dependência ainda exerce influência. Esse é o intervalo que o comprimento do bloco precisa abranger. Escolha o bloco mais curto que cubra esse intervalo e depois verifique quantos blocos restaram.

Reamostragem por grupos e a contagem ao lado de cada intervalo

A reamostragem dentro de grupos, por regime de volatilidade ou por mês do calendário, preserva a condição que tornou a pergunta relevante. Se a alegação é que uma estratégia obtém seu Sharpe em regimes de alta volatilidade, o intervalo construído apenas com dias de alta volatilidade é o que testa essa alegação. O custo é matemático. Dividir 250 observações em quatro grupos deixa cerca de 60 em cada um, e um bootstrap com 60 observações produz um intervalo aproximadamente duas vezes mais amplo que a versão com a amostra completa.

Por isso, informe a contagem do grupo ao lado de cada intervalo, sempre. A coluna block_count nos painéis acima torna esse procedimento visível: o 5º percentil calculado a partir de nove janelas é diferente daquele calculado a partir de duzentas, mesmo quando ambos são apresentados com duas casas decimais.

O que um bootstrap não pode corrigir

Um bootstrap quantifica uma única coisa: o ruído amostral de uma estatística calculada a partir dos dados disponíveis. Ele não informa se esses dados poderiam ter sido obtidos naquele momento.

Um backtest contaminado por viés de antecipação produz uma série de retornos que nunca poderia ter sido negociada. Um bootstrap dessa série devolve uma faixa estreita e confiante em torno de uma ficção. Um universo formado pelos atuais componentes de um índice carrega viés de sobrevivência, e cada reamostragem desse universo o incorpora. Existe ainda um terceiro problema: o efeito de seleção. Execute 200 variantes, mantenha a melhor, e o intervalo de bootstrap descreverá o ruído amostral dessa única variante, ignorando as 199 extrações usadas para escolhê-la. Faixas honestas são úteis. Elas não substituem dados fora da amostra.

Notas sobre os dados e o método
  • Os retornos são calculados do fechamento ao fechamento com base em barras diárias. Os dividendos são excluídos, o que subestima o nível de todos os retornos e dos índices de Sharpe em aproximadamente o dividend yield. A dispersão analisada neste post é praticamente não afetada.
  • As janelas e os blocos não se sobrepõem. Assim, cada estatística calculada usa um intervalo separado do histórico. Janelas sobrepostas inflariam o número aparente de amostras.
  • Os quantis usam um estimador determinístico. Portanto, a execução repetida de um painel retorna o mesmo percentil, e não uma nova aproximação.
  • O painel de autocorrelação usa seis ações de grande capitalização sem desdobramento dentro da janela. Também exclui qualquer dia com uma variação superior a 35%, mantendo artefatos de ajuste de preços fora da correlação.

Perguntas frequentes

O que um intervalo de confiança bootstrap informa sobre um backtest?

Ele mostra o intervalo de valores que a estatística provavelmente assumiria se o mesmo processo fosse amostrado novamente pelo mesmo número de períodos. Um intervalo de 95% que contém zero indica que a amostra é curta demais para distinguir a estratégia de uma situação sem vantagem.

Quantas reamostragens bootstrap são suficientes?

Para um intervalo de 95%, alguns milhares de reamostragens costumam gerar resultados estáveis, e 10.000 é um padrão comum com baixo custo. Quantis mais extremos exigem mais reamostragens: o percentil de 1% em 1.000 observações é obtido a partir de aproximadamente dez valores.

Qual comprimento de bloco deve ser usado em um moving block bootstrap?

Não existe um comprimento universalmente correto. As regras práticas costumam relacioná-lo ao tamanho da amostra elevado à potência de um terço. Uma verificação útil é observar o horizonte em que a variância deixa de crescer de forma linear, medido pelo painel de razão de variâncias acima. Publique o comprimento usado junto com o intervalo.

É possível aplicar bootstrap ao drawdown máximo?

Sim, mas há uma ressalva relacionada à ordem. O drawdown depende da sequência dos retornos. Por isso, uma reamostragem feita por embaralhamento calcula o drawdown de um histórico reordenado. O block bootstrap preserva sequências curtas e é a ferramenta mais adequada para estatísticas de trajetória.

O bootstrap corrige o overfitting?

Não. Ele mede o ruído amostral dentro de uma única série de retornos. O viés de look-ahead e o efeito de seleção resultante do teste de muitas variantes ficam fora do que o método consegue identificar.


Cada painel aqui inclui o SQL que o produziu. Altere o ticker ou a duração da janela e execute a consulta no terminal Strasmore.

#backtesting#bootstrap#statistics#confidence intervals#sharpe ratio