A proposta de um bot de trading no-code é simples: você descreve uma estratégia em um formulário e o software a executa para você. A parte difícil não é o formulário. É que cada simplificação que você oferece ao usuário precisa ser paga em algum lugar do sistema, geralmente num lugar que você não esperava.
Aqui estão três coisas sobre isso que eu avaliei mal, e o que cada uma de fato nos ensinou. Elas estão ordenadas pelo tamanho do meu erro de julgamento.
Pontos-chave
- As APIs das exchanges falham em silêncio com mais frequência do que falham de forma barulhenta. Um HTTP 200 com menos dados do que você pediu é o modo de falha contra o qual você precisa se proteger no projeto.
- Nunca confie no resultado de um backtest até ter testado o próprio simulador. Uma curva de patrimônio de aparência limpa pode vir de uma execução quebrada.
- A aritmética de ponto flutuante sobre quantidades de ordens produz erros de um passo a menos que só aparecem em pares específicos.
- Decidir nunca custodiar os fundos do usuário elimina uma classe inteira de problemas e cria uma menor e mais afiada: seu sistema precisa estar correto sem nunca poder desfazer nada.
- Toda conveniência que você oferece ao usuário vira uma restrição em algum ponto do motor. Essa troca geralmente vale a pena, mas nunca é de graça.
1. Eu presumi que uma requisição falha teria cara de falha
Esse foi o que custou mais tempo, e é sobre ele que eu alertaria qualquer desenvolvedor primeiro.
Buscamos candles históricos nas exchanges para rodar backtests. O modelo mental ingênuo é que uma requisição ou dá certo ou dá erro: você recebe seus dados, ou recebe um código diferente de 200 e o trata. As APIs reais das exchanges não se comportam assim sob carga. Elas retornam HTTP 200 com menos candles do que você pediu, e nenhuma indicação de que algo deu errado.
Aqui vai um caso real dos nossos logs, e os números são exatos. Um backtest de período longo pediu cerca de 536.000 candles. Às 16:34 recebeu 58.000. Status 200. Nenhum campo de erro. Nenhum aviso. A mesma requisição, sem alteração, às 17:54 recebeu os 535.000 completos.
Pense no que isso significa se você não estiver verificando. O motor simula alegremente uma estratégia sobre onze por cento do histórico pretendido, produz uma curva de patrimônio de aparência plausível e reporta um resultado. O usuário lê um número que está errado de um jeito que nada na saída revela. Isso é pior que um crash. Um crash é honesto.
A correção é conceitualmente entediante e era fácil de pular: depois de buscar, verifique se os dados que você recebeu realmente cobrem o período que você pediu, e trate qualquer falta como uma falha, mesmo que o transporte tenha dito que estava tudo bem. Cobertura é uma propriedade que você precisa afirmar, não uma que você pode presumir.
Havia uma lição de segunda ordem escondida atrás da primeira. Assim que começamos a detectar truncamento, o movimento óbvio era falhar a execução. Mas os números acima mostram que esses truncamentos são transitórios: a requisição idêntica uma hora depois retornou dados completos. Falhar de imediato teria queimado a cota do usuário por causa do que não passa de um soluço temporário do lado da exchange. Então o comportamento correto é tentar de novo uma vez antes de falhar, o que exigiu retrabalhar o caminho de falha para que uma condição recuperável não seja marcada permanentemente como falha na saída.
Duas regras saíram disso, e eu levaria as duas para qualquer projeto que conversa com uma API de terceiros:
- Valide o formato de uma resposta bem-sucedida, não apenas o código de status. O código de status diz que a requisição foi concluída. Ele não diz que a resposta é o que você pediu.
- Distinga falhas transitórias das permanentes antes de decidir o que fazer. Elas parecem idênticas no momento em que acontecem e merecem respostas opostas.
2. Eu presumi que o resultado de um backtest era um resultado
Relacionado, mas um erro distinto, e vai mais fundo que o problema do truncamento.
Quando você constrói um simulador, gasta seu esforço de teste com as estratégias. A lógica do DCA se comporta corretamente? As saídas disparam onde deveriam? Essa é a pergunta interessante, então é para lá que a atenção vai naturalmente.
O problema é que um simulador quebrado produz uma saída que se parece exatamente com a de um que funciona. Uma execução que processou silenciosamente uma fração dos dados ainda entrega um número. Uma execução cujas condições de saída nunca foram avaliadas ainda entrega um número, e um número suspeitamente plano. Já vimos resultados voltarem em exatamente +0.00% com zero operações, que é o sinal mais barulhento possível de que algo está errado com o maquinário, e o mais fácil de ler errado como “essa estratégia não faz nada”.
A virada que resolveu isso foi mudar o que testamos. Teste o simulador, não a estratégia. Concretamente, isso significa fazer asserções sobre coisas que a estratégia não deveria ser capaz de mudar: que o número de candles processados bate com o número buscado, que uma execução sobre um período conhecido realmente abrange aquele período, que uma configuração garantida para disparar produz operações. Quando essas invariantes se sustentam, um resultado de aparência estranha é uma descoberta real sobre a estratégia. Quando não se sustentam, o resultado é ruído vestido de terno.
A forma geral disso, para qualquer coisa que simula: um simulador que não consegue falhar de forma barulhenta vai falhar em silêncio, e a falha silenciosa em um sistema que produz números é o pior modo de falha que existe. As pessoas agem com base em números. Elas não agem com base em stack traces.
3. Eu subestimei o quanto o dimensionamento de ordens é, de fato, trabalho de precisão
Esse não é intelectualmente difícil. Ele é apenas muito maior do que parece, e se esconde num lugar onde erros são caros.
As exchanges não aceitam quantidades arbitrárias. Cada par de trading tem um step size para a quantidade e um tick size para o preço, e ordens que não se alinham são rejeitadas. Então toda quantidade que o sistema calcula precisa ser arredondada para baixo até um passo válido antes de sair. Isso soa como uma única função utilitária.
Aí chega o ponto flutuante. Considere arredondar 0.29 para baixo com um passo de 0.01. Multiplique para escalar e o IEEE-754 te dá 28.999999999999996, que arredonda para 28, que volta a 0.28. O valor já estava alinhado ao passo. A aritmética o moveu um passo inteiro para baixo, e fez isso em silêncio, na direção de uma ordem menor. Corrigir exige um epsilon durante a etapa de escala, pequeno o suficiente para que um valor genuinamente abaixo como 0.289999 ainda arredonde para baixo corretamente, grande o suficiente para absorver o ruído de representação.
Essa é uma correção de uma linha que exigiu debugging de verdade para ser encontrada, porque o sintoma não é uma exceção. É uma ordem ligeiramente menor do que o pretendido, em alguns pares e não em outros, dependendo de onde o step size cai em relação à representação binária.
O ponto mais amplo, e a razão de eu listar isso como algo que subestimei: precisão em um sistema de trading não é uma função utilitária, é uma categoria de trabalho. Step sizes e tick sizes diferem por par, por exchange e entre spot e futuros. Os valores mínimos de ordem não são um número único que você consulta e deixa fixo no código. Eles vêm da exchange por símbolo, e em alguns lugares dependem do preço atual. Toda vez que você se pegar escrevendo uma única constante para algo que a exchange define por par, você está escrevendo um bug com pavio de retardo.
Nós mostramos isso aos usuários em vez de esconder, porque a alternativa é uma ordem que é rejeitada por motivos que eles não conseguem ver.
4. A restrição que eu não esperava gostar
O último não é um erro. É uma decisão cujas consequências eu subestimei de um jeito bom.
A Freya nunca custodia os fundos de trading do usuário. Os bots operam por meio de chaves de API da exchange com permissão para ler e negociar, e nunca para sacar. O dinheiro permanece na conta da exchange do usuário o tempo todo.
Eu esperava que isso fosse um argumento de marketing. Acabou sendo um argumento de arquitetura, e deixou o sistema menor. Não há livro-razão de custódia, não há saldo interno para reconciliar contra um externo, não há fila de saques, não há política de cold wallet para o capital de trading. Uma categoria inteira dos problemas mais difíceis do software financeiro simplesmente não existe, porque nunca assumimos aquilo que os cria.
O que entra no lugar é mais estreito e mais afiado: seu sistema precisa estar correto em tempo real, porque não consegue desfazer nada. Com custódia você pode, em princípio, reverter uma transferência interna. Colocar uma ordem na conta de exchange de outra pessoa é definitivo no instante em que é executada. Isso joga todo o peso de engenharia sobre a correção antes de a requisição sair, em vez da reconciliação depois, e essa é uma troca que eu faria de novo.
O que eu diria a mim mesmo no começo
- Presuma que toda API de terceiros dá certo de forma incorreta de vez em quando, e projete a detecção disso antes de precisar dela.
- Escreva testes de invariantes para o seu simulador antes de escrever asserções sobre as estratégias. Os testes de estratégia não valem nada até os do simulador passarem.
- Trate a precisão como um domínio, não como um helper.
- Escolha suas restrições cedo. As que você aceita de antemão são muito mais baratas que as que você descobre depois.
Nenhum desses era um problema exótico. Eram problemas de engenharia comuns que eu arquivei como “pequenos” durante o planejamento, que é exatamente como eles ficaram caros.
