Look-Ahead Bias: como um erro de uma vela fabrica um Sharpe de 15 a partir de puro ruído
Parte da série "Backtests Without Illusions".
📄 Este artigo se transformou em um paper de pesquisa. Três fugas sutis de look-ahead são submetidas a um teste controlado contra uma verdade fundamental conhecida (4.000 históricos simulados). Leia o paper online (versão interativa + PDF) em lookahead.marketmaker.cc, código e dados em github.com/suenot/lookahead-inflation.
Há algumas semanas, nosso benchmark de busca de parâmetros estava mentindo para nós, e quase não percebemos.
O motor parecia limpo. Lógica de vela fechada, uma divisão walk-forward rolante honesta, uma busca Sobol/QMC sobre o espaço de parâmetros, uma janela de teste reservada. A busca encontrou configurações que pareciam boas in-sample. O único problema: fora da amostra, quase tudo era negativo. Assumimos que a estratégia era simplesmente fraca.
Então encontramos uma linha. O sinal era decidido no fechamento da vela i, mas a execução era registrada na mesma vela i em vez de na abertura da vela seguinte. Um erro de índice de um (off-by-one) no índice de execução. Movemos a execução para open[i+1] — o único preço pelo qual você poderia realmente transacionar depois de ver o fechamento da vela i — e o resultado fora da amostra inverteu o sinal. A busca Sobol passou de prejuízo para lucro. Nada na estratégia havia mudado. Simplesmente paramos de negociar no passado.
Isso é look-ahead bias, e a parte inquietante é o quão pequeno foi o erro e o quão grande foi a consequência. Este artigo é uma autoauditoria controlada: construímos um simulador em que a verdade fundamental é conhecida por construção, injetamos as fugas sutis uma de cada vez e medimos exatamente o quanto cada uma infla o backtest. A conclusão principal: com nenhum edge real, uma execução na mesma vela fabrica um Sharpe anualizado de +14,8 a partir de puro ruído.
O que é realmente o look-ahead bias

Look-ahead bias é qualquer ponto no seu pipeline em que uma decisão ou medição usa informação que não estaria disponível, em tempo real, no momento em que é usada. Os exemplos de livro-texto são grosseiros — usar o lucro anual completo de uma ação em janeiro, ou uma reformulação que ainda não havia sido publicada. Esses são fáceis de identificar. Os que sobrevivem a uma revisão de código são sutis, e se escondem em três lugares:
- Execução — você decide na vela
ie executa na velai(ou usa a máxima/mínima da velaipara stops na mesma vela que gerou o sinal). Você negocia a um preço que está correlacionado com o que te acionou. - Normalização — você calcula z-score, min-max, ou de outra forma escala uma feature usando estatísticas calculadas sobre toda a série, incluindo o futuro. O scaler "conhece" o conjunto de teste.
- Indicadores / features — você suaviza ou filtra com uma janela que é centralizada (ou que de alguma forma olha para frente), de modo que o valor na vela
ijá contém um pedaço da velai+1.
Todas as três são formas do que a literatura de machine learning chama de leakage: a contaminação de treino/avaliação com informação do futuro do alvo (Kaufman et al., 2012; Kapoor & Narayanan, 2023). Em finanças, o tratamento canônico é Advances in Financial Machine Learning de López de Prado (2018) — validação cruzada purgada, embargo, os perigos do backtesting. A disciplina point-in-time remonta pelo menos a Fama & French (1992), que deliberadamente atrasam os dados contábeis em seis meses para que a variável seja conhecida antes do retorno que ela explica.
A pergunta que este artigo responde é quantitativa: não "o leakage é ruim" (todos concordam), mas "quantos pontos de Sharpe cada forma proporciona, e quais são perigosas?" Sem um número, você não consegue raciocinar sobre isso. Você não consegue saber se uma inflação de +0,3 é ruído ou se uma inflação de +14 é uma prova irrefutável.
Um simulador com verdade fundamental conhecida

Para medir a inflação, você precisa conhecer a verdade. Dados reais nunca revelam a verdade — eles dão uma realização e nenhum oráculo. Então construímos um mercado sintético em que nós definimos o edge.
O processo gerador de dados é estritamente causal e não explosivo:
Aqui é uma deriva latente persistente exógena (um AR(1) com ), e o retorno da vela tem uma pequena deriva que é conhecida uma vela à frente. Como não depende de retornos passados, não há realimentação e nada explode. O parâmetro é o botão que controla quanto edge real existe:
- — o nulo: nenhum edge. Qualquer Sharpe positivo do backtest é 100% artefato.
- — um edge real e negociável: uma regra de momentum honesta realmente ganha dinheiro.
A estratégia é deliberadamente simples — uma regra de sinal de momentum. A feature é a soma dos retornos das últimas velas ( velas), e a posição é seu sinal:
csum = np.concatenate(([0.0], np.cumsum(r))) # csum[k] = sum r[0..k-1]
mom = np.full(n, np.nan)
tt = np.arange(L - 1, n)
mom[tt] = csum[tt + 1] - csum[tt - L + 1]
signal = np.sign(mom) # position for the next bar
Essa feature de momentum é o veículo perfeito para estudar a fuga de mesma vela, porque tem uma propriedade que os indicadores reais compartilham: ela contém mecanicamente a vela atual. mom[t] inclui r[t]. Então, se você registra r[t] como sua operação, está apostando em parte em uma quantidade que já está dentro do seu próprio sinal. Essa é a fuga, tornada concreta.
Configuração: (1% de volatilidade por vela), uma taxa unidirecional de 0,00045 (ida e volta 0,09%, correspondendo ao nosso motor), Sharpe anualizado por (velas horárias), 4.000 históricos independentes de 4.000 velas cada. Tudo é semeado (seeded) e determinístico.
O pipeline honesto (o único negociável)
Decidir no fechamento da vela t, ganhar o retorno da vela seguinte, pagar taxas nas mudanças de posição:
def sharpe(sig, ret_booked):
dpos = np.abs(np.diff(np.concatenate(([0.0], sig))))
pnl = sig * ret_booked - FEE_ONEWAY * dpos
return pnl.mean() / pnl.std() * np.sqrt(8760)
honest = sharpe(signal[idx], r[idx + 1]) # earn r[t+1]: tradable
As três fugas, cada uma uma única mudança cirúrgica
same_bar = sharpe(signal[idx], r[idx])
z_full = (mom - mom[valid].mean()) / mom[valid].std()
norm_full = sharpe(np.sign(z_full[idx]), r[idx + 1])
z_sm = (mom[:-2] + mom[1:-1] + mom[2:]) / 3.0 # uses t-1, t, t+1
indicator = sharpe(np.sign(z_sm[idx]), r[idx + 1])
Cada fuga está a uma linha de distância do pipeline honesto. Esse é o ponto central: não são erros exóticos, são o tipo de coisa que passa por uma revisão de código.
Resultados: a magnitude de cada fuga

Executado em 4.000 sementes (seeds), aqui está o Sharpe anualizado que cada pipeline reporta, sob o nulo (sem edge) e sob um edge real (, ajustado para que o Sharpe honesto seja um crível +1,57):
| Pipeline | Nulo (sem edge) | Edge real |
|---|---|---|
| Honesto (a verdade) | −0,74 | +1,57 |
| Execução na mesma vela | +14,79 | +15,85 |
| Espiada de indicador (1 vela) | +4,76 | +6,62 |
| Normalização de toda a série | −0,84 | +1,46 |
Os intervalos de confiança de 95% entre as sementes são de ±0,05 ou mais estreitos em cada célula; os testes t pareados sobre a inflação são astronomicamente significativos onde o efeito é real (t > 400, p ≈ 0).
Leia primeiro a coluna nula, porque é o experimento mais limpo possível: não há edge, então o pipeline honesto perde dinheiro corretamente (−0,74, o atrito de pagar taxas para negociar ruído). Agora veja o que as fugas fazem com esse mesmo nada:
- Execução na mesma vela: −0,74 → +14,79. Uma estratégia sem nenhum poder preditivo, negociando ruído aleatório, reporta um Sharpe anualizado de quase 15. Isso não é um viés sutil; é uma fabricação. O mecanismo é exatamente o que construímos: a feature de momentum contém
r[t], então registrarr[t]é apostar no próprio sinal. - Espiada de indicador: −0,74 → +4,76. Deixar o suavizador ver uma vela à frente no futuro fabrica um Sharpe próximo de 5 a partir de ruído, porque o valor suavizado em
tagora se correlaciona com or[t+1]que você está prestes a ganhar. - Normalização de toda a série: −0,74 → −0,84. Praticamente nenhuma inflação. Esse é o resultado honesto e não óbvio (mais sobre isso abaixo).
A coluna de edge entrega a mensagem mais insidiosa. Quando um edge real existe (honesto +1,57), as fugas não apenas somam uma constante — elas empurram o Sharpe medido para +15,85 e +6,62, muito acima do +1,57 que você realmente poderia negociar. Então o número medido não consegue distinguir habilidade de fuga. Um +6 vazado e um +6 honesto parecem idênticos no relatório. Você só descobre qual dos dois tinha depois de ter alocado capital.
A fuga é um gradiente, não um interruptor

Uma objeção natural: "registrar a vela de sinal inteira é um erro extremo e irrealista." Então varremos a dose — a fração da vela de sinal capturada pela fuga, de 0 (honesto) a 1 (fuga completa na mesma vela):
| Fração capturada | Sharpe nulo | Sharpe com edge |
|---|---|---|
| 0,00 (honesto) | −0,74 | +1,57 |
| 0,25 | +3,90 | +6,41 |
| 0,50 | +9,86 | +12,20 |
| 1,00 (fuga total) | +14,79 | +15,85 |
Capturar apenas um quarto da vela de sinal leva uma estratégia sem edge de −0,74 para +3,90. Você não precisa do erro completo de um índice para ser enganado; uma execução ligeiramente favorável demais — um toque de slippage otimista na vela de sinal, um stop intrabar verificado contra a mesma vela que o acionou — é suficiente para superar a maioria dos limiares "implantáveis". A inflação é suave e monotônica em relação a quanto do presente você se permite negociar.
Com que frequência isso coloca uma estratégia perdedora em produção?
O número que deveria preocupar um praticante é a taxa de implantação falsa: com que frequência uma fuga faz uma configuração verdadeiramente perdedora superar o limiar que você usaria para aprová-la. Usando "Sharpe anualizado ≥ 1,0" como critério de implantação, sob o nulo:
- Execução na mesma vela: 68% das estratégias sem edge parecem implantáveis e realmente perdem dinheiro. Duas em cada três configurações de puro ruído passariam por um portão de Sharpe-≥-1 e perderiam dinheiro ao vivo. (Essa taxa é claramente definida aqui porque a fuga está puramente na execução — o equivalente honesto é o mesmo sinal com uma execução honesta.)
- Espiada de indicador: empurra praticamente toda configuração sem edge acima do limiar de implantação também (99,9% superam Sharpe ≥ 1) — levaria ruído diretamente para produção.
- Normalização de toda a série: 12% superam o limiar — essencialmente a taxa base de ruído, sem prêmio de leakage.
A taxonomia, e como detectar cada uma
As três fugas não são igualmente perigosas, e as diferenças são instrutivas.
1. Leakage de execução (a cara)

Sintoma: o preço de execução está correlacionado com o sinal porque vêm da mesma vela. Magnitude: enorme (+15 a partir de ruído em dose completa, +3,9 em um quarto de dose). Por que é a pior: seu sinal é, quase por definição, construído a partir da ação de preço recente, então o retorno da vela de sinal é exatamente aquilo com que sua feature está mais correlacionada. Registrá-lo é quase o mesmo que consultar a resposta.
Detecção — o teste de deslocamento de uma vela. Este é o diagnóstico mais valioso deste artigo. Pegue seu backtest e desloque cada execução uma vela mais tarde (decida em i, execute em open[i+1]). Se o resultado mal se mover, sua execução era honesta. Se o resultado desmoronar ou inverter o sinal, você estava negociando no passado. Foi exatamente isso que aconteceu com nossa busca Sobol: desloque as execuções, e um OOS "lucrativo" se revelou uma perda — ou melhor, a relação real emergiu assim que a fuga foi removida.
entry_price = open_[i + 1] # NOT close[i], NOT open[i]
2. Leakage de indicador / feature (a silenciosa)
Sintoma: um indicador na vela i depende de dados de i+1 ou posteriores — uma média móvel centralizada, um filtro sem atraso causal, um rótulo de pico/vale que precisa de velas futuras para confirmar, uma transformação tipo Heikin-Ashi alimentada com candles futuros. Magnitude: grande (+4,8 a partir de ruído). Por que se esconde: a fuga está enterrada dentro de uma chamada de biblioteca. scipy.signal.filtfilt é de fase zero — e fase zero significa não causal. Uma feature "esta vela é um máximo local" é incognoscível até que a próxima vela seja impressa.
Detecção: para cada indicador, pergunte: qual é o maior índice que ele lê? Se calcular o valor em t alguma vez tocar t+1, ele é não causal. Calcule indicadores sobre uma janela causal expansiva/rolante e verifique que o valor na vela t é idêntico independentemente de existirem ou não velas após t no array. (Nossas implementações de HMA/ADX passam neste teste: cada saída em t lê apenas entradas em ≤ t.)
3. Leakage de normalização (a específica do canal)
Sintoma: um scaler (StandardScaler, min-max, um z-score global) é ajustado em todo o conjunto de dados, teste incluído. Os avisos canônicos de ML são explícitos sobre isso — o §7.10.2 de Elements of Statistical Learning de Hastie, Tibshirani & Friedman ("the wrong and right way to do cross-validation"), e o próprio guia de armadilhas comuns do scikit-learn: "the average should be the average of the train subset, not the average of all the data."
Magnitude em nosso teste: ≈ zero (−0,74 → −0,84). Este é o resultado surpreendente e honesto, e vale a pena entender em vez de decorar.
Por que não inflou? Porque nossa estratégia usa a feature apenas através do seu sinal (um limiar zero). O escalonamento por desvio padrão nunca muda um sinal, e a centralização pela média global apenas desloca ligeiramente o cruzamento por zero. Então a padronização de toda a série de uma regra de sinal pura é quase inócua.
Não generalize demais isso. O leakage de normalização é específico do canal. No momento em que sua estratégia usa a magnitude da feature — dimensionamento de posição proporcional a um z-score, um limiar de entrada diferente de zero escolhido observando a distribuição escalonada, uma rede neural que consome entradas padronizadas —, o scaler ciente do futuro começa a importar, e importa mais quanto mais as estatísticas globais diferirem das causais. Nosso resultado não é "o leakage de normalização é seguro." É "a magnitude do leakage depende do canal pelo qual a quantidade vazada entra na decisão, e você deve medi-la em vez de presumi-la." Uma regra de sinal é o único caso em que essa fuga específica é barata.
Onde isso se conecta
Look-ahead bias é o primeiro elo de uma cadeia que esta série vem documentando:
- Corrompe a entrada da validação. Um backtest vazado navegará facilmente por uma divisão walk-forward e parecerá um platô amplo em vez de um pico de overfitting — a fuga é consistente entre os folds, então a validação cruzada não consegue capturá-la. O leakage é o modo de falha anterior ao overfitting, e nenhuma validação honesta posterior vai te salvar.
- Interage com a busca de parâmetros: uma busca de milhares de trials sobre dados vazados encontrará a configuração que explora a fuga mais agressivamente. O "vencedor" é o pior infrator.
- É por isso que a paridade backtest-live diverge. Uma fuga é a explicação mais clara para uma lacuna de 30–50% entre o backtest e o bot, porque o trading ao vivo é, mecanicamente, o único lugar onde você não pode espiar.
A disciplina que captura tudo isso é a mesma que a literatura acadêmica vem defendendo há anos: trate um backtest como um experimento estatístico com uma fronteira de informação estrita. Bailey, Borwein, López de Prado & Zhu mostraram com que facilidade o overfitting fabrica desempenho falso (2014); o protocolo de backtesting de Arnott, Harvey & Markowitz (2019) codifica essa higiene. Look-ahead bias é a fronteira mais básica de todas — a fronteira no tempo — e a mais barata de violar por acidente.
Conclusões

- O look-ahead bias é quantitativamente enorme e qualitativamente invisível. Um único erro de execução de uma vela transformou um Sharpe de −0,74 (puro ruído, perdendo corretamente) em +14,79. O erro é uma linha; a consequência é um histórico fabricado.
- É um gradiente. Capturar apenas 25% da vela de sinal já produz +3,90 do nada. Você não precisa de um bug flagrante — um pouco de otimismo demais em suas execuções basta.
- O número medido não consegue distinguir habilidade de fuga. Quando um edge real existe, as fugas inflam o relatório muito além da verdade negociável. A única defesa é o processo, não a métrica.
- O teste de deslocamento de uma vela é seu diagnóstico mais rápido. Desloque cada execução uma vela mais tarde. Se o desempenho desmoronar, você estava negociando no passado.
- A magnitude do leakage é específica do canal. As espiadas de execução e indicador são devastadoras; a normalização de toda a série de uma regra de sinal é quase gratuita. Meça a fuga através do canal pelo qual ela realmente entra — não presuma.
O estudo controlado completo — as três fugas, a varredura de dose, a análise de implantação falsa, os métodos formais e cada número reproduzível a partir de um único script determinístico — está no paper complementar em lookahead.marketmaker.cc, com código e dados em github.com/suenot/lookahead-inflation.
A estratégia em nosso experimento nulo não tinha nenhum edge. Ainda assim, mostrou um Sharpe de 15. Se seu backtest parece bom demais, a primeira coisa a suspeitar não é do seu gênio — é do seu relógio.
Authors
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.