Pular para o conteúdo principal
engenhariabacktestingalgoritmos
Por Que os Backtests Mentem: Simular Saídas Baseadas em Indicadores É Mais Difícil do Que Parece

Por Que os Backtests Mentem: Simular Saídas Baseadas em Indicadores É Mais Difícil do Que Parece

Um backtest produz um número, e números são persuasivos. Esse é todo o problema. Quando uma simulação está sutilmente errada, ela não trava e não te avisa. Ela te entrega um resultado ligeiramente melhor do que a realidade e deixa você agir com base nele.

Simular um take-profit fixo é fácil: ou o preço atingiu o seu nível, ou não atingiu. Simular uma entrada ou saída baseada em indicador é onde a coisa fica difícil, porque agora a simulação precisa responder a uma pergunta bem mais espinhosa: o que o bot realmente sabia naquele momento?

Aqui estão três formas pelas quais respondemos essa pergunta de forma errada, e o que cada uma nos ensinou.

Pontos-chave

  • O bug clássico é ler um indicador de timeframe maior antes de sua barra ter fechado. Um RSI de 4 horas consultado na abertura da barra carrega quatro horas de movimento de preço que ainda não tinham acontecido.
  • A correção é aritmética, não heurística: um valor do timeframe T só é legitimamente utilizável depois que T transcorreu, então desloque a consulta para trás em (timeframe do gatilho menos timeframe da simulação).
  • Motores ao vivo têm o mesmo problema com outra fantasia. Um indicador calculado sobre um candle ainda em formação repinta, o que significa que uma condição pode ser verdadeira e depois falsa na mesma barra.
  • Um simulador que pula silenciosamente aquilo que não consegue replicar é mais permissivo do que o bot ao vivo, e a diferença sempre favorece o backtest.
  • A atitude honesta é expor a diferença no resultado em vez de escondê-la. Um backtest que te diz onde está mentindo vale mais do que um que parece impecável.

1. O bug de look-ahead que se esconde em estratégias multi-timeframe

Este é o mais fácil de escrever e o mais difícil de perceber.

Digamos que a simulação avança um candle de 5 minutos por vez, e a estratégia tem uma condição sobre um RSI de 4 horas. A cada passo de 5 minutos, o motor precisa de um valor para esse RSI. A implementação ingênua busca a barra de 4 horas que contém o momento atual e lê o seu valor.

Isso é uma máquina do tempo.

Uma barra de 4 horas que abriu às 12:00 não tem um valor final de RSI até fechar às 16:00. Se a simulação está parada às 12:05 e lê essa barra, ela está lendo um número calculado a partir de quatro horas de movimento de preço que, do ponto de vista das 12:05, ainda não aconteceram. A estratégia parece entrar bem antes de movimentos que ela não poderia ter previsto, e a curva de capital fica excelente.

A correção não é uma heurística. Ela surge naturalmente quando você escreve em que momento um valor realmente se torna conhecível. Uma barra que abre em t em um timeframe de triggerMs só é conhecível em t + triggerMs. A simulação toma sua decisão no fechamento do candle atual, candleOpenTime + simulationMs. Então o valor é legitimamente utilizável exatamente quando:

t + triggerMs <= candleOpenTime + simulationMs
t             <= candleOpenTime - (triggerMs - simulationMs)

O que significa que a consulta correta é a mesma que você já tinha, executada contra um timestamp deslocado para trás em triggerMs - simulationMs.

Vale a pena destacar duas propriedades dessa expressão, porque são elas que a tornam segura para aplicar em todo lugar:

  • Condições do mesmo timeframe recebem um deslocamento de zero. Um indicador de 5 minutos lido no fechamento de um candle de 5 minutos é o seu próprio valor baseado no fechamento, que sempre esteve correto. Apenas timeframes genuinamente maiores se deslocam.
  • O deslocamento é derivado, não configurado. Ele vem do próprio candle, e não de uma configuração que alguém precisa lembrar de definir.

A lição geral é uma que eu aplicaria a qualquer simulação, não só a trading: quando você não tem certeza se tem viés de look-ahead, anote o timestamp em que cada entrada se torna conhecível e compare-o com o timestamp em que você a usa. O viés é o que sobra quando você pula esse passo e confia na intuição, e a intuição é ruim nisso porque ler "a barra atual de 4 horas" soa completamente razoável.

2. O mesmo bug no código ao vivo veste outra fantasia

Você poderia pensar que esse problema pertence ao backtesting. Não pertence. O motor ao vivo também enfrenta esse problema, e lá ele é chamado de repintagem (repainting).

A qualquer momento, o candle mais recente ainda está em formação. Um indicador calculado sobre uma janela que termina nesse candle vai mudar conforme o candle muda. Uma condição pode ser verdadeira às 12:01, falsa às 12:03 e verdadeira de novo às 12:04, tudo dentro da mesma barra, sem que o mercado faça nada fora do comum. Se o bot age com base na primeira delas, ele agiu sobre um valor que já não existe mais.

Nosso padrão é descartar o candle ainda em formação e calcular somente sobre barras confirmadas, de modo que um sinal nunca repinte. É isso também que faz o ao vivo e o backtest concordarem, já que a simulação, por construção, trabalha com barras fechadas.

Mas "nunca repintar" não sai de graça. Esperar pela confirmação significa agir mais tarde, e para algumas estratégias esse atraso custa mais do que o ruído que ele evita. Por isso, isso é exposto como uma escolha por condição: avaliar na barra confirmada, ou avaliar ao vivo e aceitar que o valor pode se mover debaixo de você.

O ponto de design é a parte que eu manteria: quando um trade-off é genuinamente um trade-off, não o enterre em um padrão e finja que é um fato. Faça da opção segura o padrão, e então deixe o usuário optar pela outra de forma consciente. O que você não pode fazer é deixar os dois modos discordarem entre ao vivo e backtest, porque aí o usuário está comparando dois bots diferentes.

3. O que o simulador não consegue replicar é mais perigoso do que aquilo que ele erra

Os dois primeiros são bugs de correção. Este é um problema de design, e é o que eu considero o mais subestimado.

Algumas condições não podem ser replicadas. Um sinal externo via webhook é o caso mais claro: não existe registro histórico do que um sistema externo teria enviado às 14:20 em algum dia de março passado. A simulação simplesmente não tem como saber.

Então o que ela deveria fazer? A resposta tentadora é pular essa condição e avaliar o resto. É isso que faz o resultado parecer completo. É também onde o dano acontece, e vale a pena detalhar o mecanismo:

Se uma estratégia exige A E B, e B não pode ser simulado, então pular B deixa A decidindo sozinho. O bot simulado entra em condições nas quais o bot ao vivo teria ficado de fora. A simulação é sistematicamente mais permissiva do que a realidade.

Repare na direção. O erro não é aleatório. Descartar uma condição sob a lógica E só pode produzir mais operações do que a realidade, nunca menos, então o viés aponta para um resultado com aparência melhor todas as vezes. É essa assimetria que o torna perigoso: o ruído aleatório se equilibra ao longo de muitas execuções, mas o viés de uma única direção não.

Escolhemos continuar rodando a simulação em vez de recusá-la, porque um resultado parcial com uma lacuna conhecida ainda é útil. Mas então a lacuna precisa estar visível na saída, não enterrada em um log. O resultado carrega um aviso explícito, no próprio idioma do usuário, com um nome que diz o que aconteceu: "Algumas Condições de Entrada Não Simuladas." Há um equivalente para as saídas.

Essa decisão é a parte deste sistema que eu defenderia com mais afinco. Um simulador que não consegue falhar de forma barulhenta vai falhar de forma silenciosa, e a falha silenciosa em algo que produz números é o pior modo de falha que existe, porque as pessoas agem com base em números e não agem com base em stack traces.

O que levar disso

Se você está construindo qualquer coisa que replica o histórico para produzir um número:

  • Toda entrada precisa de um timestamp de "conhecível em". Se você não consegue defini-lo, não consegue descartar o viés de look-ahead.
  • Verifique a direção dos seus erros, não apenas o tamanho deles. Um erro que sempre aponta para o mesmo lado se acumula; um que aponta aleatoriamente se equilibra. O otimismo de uma única direção é a assinatura do viés de look-ahead.
  • Faça o simulador e o sistema ao vivo compartilharem a mesma lógica de decisão sempre que possível. Duas implementações de "esta condição é verdadeira?" vão divergir, e você vai descobrir por um usuário, e não por um teste.
  • Reporte o que você não conseguiu fazer. A credibilidade de um resultado vem dos limites que ele declara, não da sua aparência impecável.

O resumo desconfortável é que um backtest é uma afirmação sobre um contrafactual, e contrafactuais são fáceis de errar silenciosamente. O objetivo de engenharia não é um simulador que nunca mente. É um que te diz onde está mentindo.


Leitura Relacionada

Crie uma conta

Coloque seu conhecimento em prática e abra uma conta Freya hoje.

Registre-se agora
Este conteúdo é apenas para fins educacionais e não constitui aconselhamento financeiro. O trading de criptomoedas envolve riscos significativos. Nunca invista mais do que você pode se dar ao luxo de perder.