Direto ao ponto
Consertei um bug no robô de grid e o resultado virou de cabeça para baixo. O campeão da minha lista, que marcava +535 dólares, virou uma perda de 341. E um robô que eu daria como morto, com 1.088 no vermelho, virou +461. No total, o bug tinha inflado o resultado dos 23 robôs em cerca de 36%. A lição é dura: um robô pode ser vencedor não do mercado, mas de um bug.
No capítulo anterior eu montei a espinha do método: robôs descartáveis, disjuntor de drawdown, peso igual. Antes de confiar em qualquer número, fui auditar o robô de grid que entra no portfólio. Achei um bug. E o que veio depois de consertar foi o susto mais instrutivo do projeto inteiro, do tipo que muda a forma como você olha para qualquer backtest.
O bug: uma regra morta no meio da estratégia
A entrada desse grid depende de duas condições ao mesmo tempo. Uma é o sinal, uma sequência de candles na mesma direção. A outra é um tempo mínimo de espera depois de fechar a rodada anterior, antes de abrir uma rodada nova. O bug matou só a segunda. A memória do horário em que a rodada fechava era zerada por engano, então a trava de tempo nunca valia, e o robô reabria uma rodada nova na hora, em vez de esperar as horas definidas no seu parâmetro.
O detalhe importante é que o sinal de candles continuou funcionando 100%, no backtest com bug e no corrigido. Num dos robôs, o EUR-H3, foram 69 entradas, todas com a sequência de candles certa na direção certa, tanto na versão bugada quanto na corrigida. Só o relógio estava quebrado. Isso já é uma lição por si só: auditar um robô não é olhar se as entradas parecem certas. Uma entrada pode ter o gatilho certo, os candles na direção, e ainda assim estar errada por violar outra regra, a frequência. É preciso conferir cada condição do parâmetro contra a ordem real, isolada das outras. Foi assim que eu separei o sinal, que estava bom, da frequência, que estava bugada. Esse é o tipo de erro de backtest de grid que passa despercebido porque a curva continua bonita.
Teve até um susto de auditoria que virou aprendizado. Num dos robôs, a distância entre os aumentos parecia violar o mínimo do parâmetro: dava negativa, menos 0,02%, o que é fisicamente impossível. Eu quase tratei como um segundo bug. Não era. O robô confere a distância no preço de compra, o bid, mas preenche a ordem no preço de venda, o ask, e a diferença entre os dois, o spread, explicava a distância negativa. Era artefato de execução, não erro de lógica. A lição vale para qualquer um que audita robô: nem toda anomalia no dado é bug do seu código, parte é a mecânica real do mercado, e confundir as duas faz você consertar o que não está quebrado.
Com isso resolvido, validei o robô corrigido condição por condição, nos 23, antes de confiar em qualquer número. Conferi a trava de tempo da reentrada e do aumento, o hedge, o número máximo de aumentos, o alvo de lucro e o alinhamento com o timeframe. Zero violação real. O conserto fez exatamente o que tinha que fazer: a reabertura passou a respeitar a espera de cada robô, em vez dos zero hora de antes, e nada mais no comportamento mudou. Só aí o retest virou um teste em que dava para confiar.
O choque: o bug mentia nos dois sentidos
Retestei os 23 robôs aprovados com o robô corrigido, na mesma janela, com os mesmos parâmetros de cada um. Eu esperava uma piora geral. O que veio foi diferente e bem pior de entender: o bug mentia para os dois lados. Ele afundava robôs bons e inflava robôs ruins. Alguns exemplos:
| Robô | Com o bug | Corrigido |
|---|---|---|
| EUR-H3 | −35 | +976 |
| GBP-H3 | −1.088 | +461 |
| EUR-M15 | +237 | +916 |
| JPY-M2a (o “campeão”) | +535 | −341 |
| EUR-M5 | +521 | +0 |
| CAD-M6 | +494 | −266 |
E não era só o resultado. O bug também escondia drawdown. O campeão JPY-M2a tinha um drawdown declarado de 242 dólares que, corrigido, virou 662. O GBP-M2 saltou de 438 para 1.017, e o CAD-M6 de 28 para 464. Ou seja, além de inflar o lucro de alguns, o bug fazia eles parecerem mais seguros do que eram. Somando tudo, os 23 robôs marcavam mais de 6.000 dólares com o bug e cerca de 3.900 corrigidos. O bug inflava em torno de 36% do resultado total.
Por que vencedor virou perdedor
A pergunta que eu fiz na hora foi: como uma estratégia vencedora vira perdedora só de consertar um relógio? A resposta é a lição mais importante deste capítulo. Os robôs nunca foram vencedores de verdade. Eles eram vencedores da versão bugada. A otimização procurou, entre milhares de combinações, justamente as que davam lucro com o robô se comportando de forma bugada, reabrindo na hora, naquele exato pedaço de 2,5 anos. O otimizador não sabe o que o parâmetro significa. Ele guarda qualquer coisa que lucrou na amostra, inclusive o lucro que veio do bug.
Isso é overfitting puro, só que ao bug em vez de ao mercado. O ganho estava grudado em duas coisas ao mesmo tempo, o bug e aquele caminho específico de preços, e nenhuma das duas viaja para o futuro. Por isso consertar o bug funciona como um teste fora da amostra surpresa. Se o comportamento bugado fosse de fato bom, consertar quebraria todos os robôs. Mas consertou uns e quebrou outros, sem direção consistente. Essa falta de direção é a assinatura do ruído: cada robô tinha pescado uma coincidência diferente, não uma vantagem.
Como medir se uma ordenação sobrevive
Existe uma forma formal de olhar isso, e ela conecta com tudo que veio antes na série. Quando eu ordeno os robôs do melhor para o pior antes e depois do conserto, posso medir o quanto as duas ordens concordam com a correlação de postos de Spearman (1904):
\rho = 1 - \frac{6 \sum_{i} d_i^{2}}{n\,(n^{2}-1)}Aqui n é o número de robôs e d_i é a diferença de posição de cada robô entre as duas ordenações, a com bug e a corrigida. Se a ordem fosse uma propriedade real dos robôs, uma mudança que não deveria importar manteria o rho perto de 1. Foi o contrário. O campeão caiu para o fundo e o pior subiu para o topo, então a ordenação se embaralhou e o rho desabou. E aqui está o ponto: quando uma classificação se desmancha diante de uma mudança que não deveria mexer nela, essa classificação nunca foi informação, era ruído. É exatamente a mesma lição do capítulo dois, em que selecionar os melhores robôs não sobrevivia a uma era nova. Lá a mudança foi o tempo. Aqui foi um conserto de software. O resultado é o mesmo: o que se embaralha facilmente nunca foi vantagem.
O que eu mantive, e o que joguei fora
A decisão saiu direto da lógica acima. Mantive os robôs que ficaram melhores depois do conserto, porque eles ganham apesar da mudança, e são os mais prováveis de serem reais. Joguei fora os que viraram negativos, que eram artefato do bug. É contraintuitivo, porque entre os apagados estava o antigo campeão, e entre os mantidos estavam robôs que o relatório antigo dava como perdedores.
Tem uma ressalva honesta que eu faço questão de manter. A janela inclui 2024, o ano do caos no iene, então parte do vermelho dos robôs de iene é estresse de regime, não só o bug. O número limpo, o que mede de fato o efeito do bug, é a diferença na mesma janela, e essa diferença é enorme: só no JPY-M2a, o efeito do bug isolado do mercado foi de −876 dólares. O que esse episódio não permite é confundir as duas coisas, e por isso eu separo sempre o efeito do conserto do efeito do mercado.
E teve um segundo corte, porque sobreviver ao conserto não era suficiente. Entre os 15 que ficaram positivos, apliquei um filtro de qualidade: só mantive os que ganham bem em relação ao risco que correm, com lucro de pelo menos o dobro do drawdown. Usei lucro sobre drawdown, e não Sharpe, de propósito, porque o grid afunda em drawdown aberto antes de fechar a rodada, e o Sharpe não enxerga esse tipo de risco. Sobraram quatro. O CAD-M20, que lucra quase cinco vezes o seu drawdown, o EUR-M15 com 3,3, o EUR-H3 com 3,0 e o GBP-M5b com 2,2. Repare que nenhum dos quatro é de iene. A safra de USDJPY, que era a minha preferida e tinha o maior Sharpe in-sample de todas, 5,22, era também a mais contaminada pelo bug, e evaporou inteira no corte. Os outros onze positivos, de qualidade morna, também foram apagados. Não basta o robô ser real depois do conserto. Ele ainda precisa pagar pelo risco que carrega, senão entra no portfólio só enchendo espaço e drawdown.
A regra que sai disso, e o próximo capítulo
A regra que ficou para o resto do projeto é simples de dizer e trabalhosa de fazer. Antes de confiar em qualquer seleção, valide a aderência entre o parâmetro e a ordem: o que a ordem de fato fez bate com o que o arquivo de configuração mandava? A tentação seguinte é re-otimizar tudo no robô corrigido, mas isso vem depois, nunca antes: re-otimizar em cima de um robô cuja aderência você não validou é só repetir o erro numa escala maior. Eu quase fiz o oposto do certo aqui. Quase deletei os meus melhores robôs, os que o bug estava escondendo, e quase subi para o mercado os campeões falsos. Um detalhe de código separou as duas decisões. No próximo capítulo, outro risco que eu estava medindo errado, e que quase ninguém olha: o drawdown não dos trades fechados, mas das posições que ainda estão abertas.
Acompanhe a série para não perder. O próximo sai em poucos dias.
Perguntas frequentes
O que é “overfit ao bug”?
É quando a otimização guarda um comportamento bugado porque ele deu lucro na amostra. O ganho fica grudado na combinação de bug com aquele período específico, e nenhuma das duas coisas se repete no futuro. Consertar o bug revela isso, porque o lucro que dependia dele desaparece.
Como um bug-fix vira um teste fora da amostra?
Porque um robô com vantagem real deveria ser indiferente a uma mudança que não toca na sua tese. Quando o conserto reordena completamente os resultados, com campeão virando perdedor, isso prova que a ordenação era artefato. É o mesmo efeito de testar numa era nova, só que provocado por software.
Por que manter os robôs que melhoraram com o conserto?
Porque eles lucraram apesar da mudança, e não por causa dela. Robôs que dependiam do bug para parecer bons caem quando ele é corrigido. Robôs que sobem mesmo com a correção têm a maior chance de carregar algo real, não uma coincidência.
Um bug pode quebrar só parte do robô?
Sim. Neste caso ele matou a trava de tempo entre rodadas e deixou intacto o sinal de candles. Por isso é preciso checar cada condição do robô isoladamente contra as ordens reais, e não confiar que tudo está certo só porque as entradas parecem na direção certa.
Referências
- Bailey, D. H.; Borwein, J. M.; López de Prado, M.; Zhu, Q. J. (2014). Pseudo-Mathematics and Financial Charlatanism: The Effects of Backtest Overfitting on Out-of-Sample Performance. SSRN 2308659 / Notices of the AMS.
- Harvey, C. R.; Liu, Y.; Zhu, H. (2016). … and the Cross-Section of Expected Returns. Review of Financial Studies, 29(1), 5-68. O custo de testar muitas hipóteses sobre os mesmos dados.
- Spearman, C. (1904). The Proof and Measurement of Association between Two Things. The American Journal of Psychology, 15(1), 72-101. A correlação de postos usada para medir se a ordenação sobreviveu.
- Taranto, A. (2022). Bidirectional Grid Constrained Stochastic Processes. PhD Thesis, University of Southern Queensland, DOI 10.26192/q7q62. O grid como processo estruturalmente sobrevivível, porém não seguro.
- López de Prado, M. (2018). Advances in Financial Machine Learning. Wiley. Sobre artefatos de backtest e a fragilidade de rankings ajustados à amostra.
Presente para Leitores: Robô de Gradiente Linear Gratuito
Estou liberando o acesso ao meu setup pessoal de Gradiente Linear sem custo nenhum. É só clicar e me pedir o arquivo.
Quem escreve: Flávio Araújo
Trader algorítmico há mais de 13 anos, MBA em Mercado de Capitais e Derivativos. Mantém um laboratório próprio que já rodou mais de 20 milhões de backtests e analisou mais de 20 mil estratégias, e opera um portfólio de robôs acompanhado publicamente no blog. LinkedIn · Instagram · YouTube
