Seis semanas depois de executarmos um backtest, voltámos a executá-lo com o mesmo código, configuração e intervalo de datas. O capital final era diferente. Não muito diferente, mas o suficiente para fazer o Sharpe passar de 1.31 para 1.08 e alterar qual conjunto de parâmetros parecia melhor.
A estratégia não tinha mudado. Os dados históricos, sim. Um fornecedor tinha revisto vários dias de transações e barras, e a nossa primeira execução não tinha preservado o snapshot exato dos dados de entrada. Passámos os dias seguintes a tentar perceber se o novo resultado se devia a uma correção dos dados, a um erro no pipeline ou a um processo de investigação que não conseguíamos reproduzir.
Segunda-feira: a nova execução deu um resultado diferente
A primeira pista foi uma pequena discrepância num par spot líquido. A nossa barra original mostrava um máximo de 62,418. A nova execução mostrava 62,431. A diferença parecia inofensiva, até rastrearmos a entrada da estratégia: o limiar ficava entre esses preços. Algumas barras alteradas significavam sinais diferentes e, por isso, posições diferentes vários dias depois.
Comparámos o número diário de transações e descobrimos que a divergência começava numa única data. Depois, comparámos as linhas em bruto que alimentavam essa data. A resposta atual do fornecedor incluía algumas transações corrigidas e uma barra agregada revista. A resposta que tínhamos usado não vinha acompanhada de qualquer registo de alterações.
Este comportamento dos dados é normal. As bolsas corrigem registos; os fornecedores preenchem lacunas, corrigem transações erradas e ajustam a lógica de agregação. Uma consulta aos «dados de janeiro» pode devolver uma resposta diferente em março.
Terça-feira: encontrámos o comprovativo em falta
Tínhamos guardado no registo da execução o nome do fornecedor, o símbolo e o intervalo de datas. Não tínhamos guardado a própria resposta nem um hash do conteúdo de cada partição de entrada. Isso significava que podíamos provar o que tínhamos pedido, mas não que bytes o fornecedor tinha devolvido.
Fomos buscar os ficheiros em cache mais antigos que conseguimos encontrar e comparámo-los com a resposta atual. A diferença não se devia a um ajuste por split nem a uma conversão de fuso horário. Era um pequeno conjunto de registos de transações corrigidos que passavam pelo nosso construtor de barras. Isso eliminou uma hipótese, mas a cache estava incompleta, pelo que continuávamos sem conseguir reconstruir com exatidão todas as barras afetadas.
| O que registámos | A que perguntas permitia responder | O que faltava |
|---|---|---|
| Fornecedor, símbolo, intervalo de datas | Que pedido a execução fez | O conteúdo da resposta |
| Versão do pipeline | Que código construiu as barras | As linhas de entrada exatas |
| Data e hora da execução | Quando executámos a tarefa | Que revisão do fornecedor estava disponível nessa altura |
Tínhamos tratado «mesma consulta» como «mesmos dados». Essa suposição fazia a experiência parecer mais reproduzível do que realmente era.
Quarta-feira: a correção mudou a história
Reproduzimos as datas afetadas com ambas as versões dos dados disponíveis. As barras revistas eliminaram duas entradas e anteciparam uma saída em uma barra. Essas três transações explicavam a maior parte da alteração do Sharpe. A estratégia era sensível a algumas observações próximas do seu gatilho, embora a série de preços global parecesse quase idêntica no gráfico.
Isso não significava que o resultado original fosse fraudulento nem que o resultado revisto fosse automaticamente melhor. Significava que a afirmação precisava de ser delimitada: a execução antiga descrevia o snapshot de dados que tínhamos na altura, enquanto a nova execução descrevia o histórico atual do fornecedor. Não podíamos, honestamente, chamar a uma reprodução da outra.
Quinta-feira: alterámos o registo de investigação
Passámos a incluir snapshots imutáveis dos dados de entrada nas novas execuções de investigação. Cada partição recebe um hash do conteúdo, e o manifesto da execução regista a origem, a hora de obtenção, a versão do esquema, a política de ajustes e a versão do código de transformação. Para conjuntos de dados grandes, guardamos os ficheiros em armazenamento endereçado pelo conteúdo, para que partições idênticas possam ser partilhadas sem substituir versões anteriores.
Quando as regras de armazenamento ou de licenciamento nos impedem de conservar dados em bruto, preservamos o comprovativo mais sólido permitido pelos termos: hashes, parâmetros do pedido, identificadores da revisão da origem e uma pequena amostra das linhas analisadas para diagnóstico. Um hash não permite recuperar os dados, mas pode dizer-nos quando duas execuções não consumiram bytes idênticos.
Um backtest reproduzível precisa de uma identidade dos dados de entrada, não apenas de um URL para os dados. Registe o que chegou, quando chegou e como se transformou nas linhas consumidas pela sua estratégia.
Sexta-feira: o que evitaríamos da próxima vez
Evitaríamos comparar apenas as métricas principais. Uma alteração do Sharpe diz-lhe que algo mudou, mas não identifica a causa. Comece pelos totais de linhas e pelos hashes das partições; depois, encontre a primeira data de entrada divergente. A partir daí, compare, por esta ordem, as linhas transformadas, os sinais, as ordens e as execuções.
Também evitaríamos substituir silenciosamente os dados antigos pelo histórico corrigido. As correções são úteis, mas as execuções antigas devem continuar associadas aos dados de entrada originais. Um novo conjunto de dados deve dar origem a uma nova execução, com uma ligação visível à anterior.
A nossa regra prática é agora simples: se uma nova execução tiver como objetivo reproduzir uma experiência, fixe o snapshot dos dados. Se tiver como objetivo beneficiar de um histórico corrigido, identifique-a como uma nova experiência. Precisámos de alguns dias para aprender esta distinção, que agora ocupa uma linha no manifesto da execução.
← Todos os artigos


