Isto encerra uma pequena série sobre construir a Freya Finance. Em vez de repetir os artigos, aqui está o que eu de fato diria a mim mesmo no início, ordenado por quanto cada lição custou para ser aprendida.
O fio condutor é incômodo: quase nenhuma dessas falhas se anunciou. Elas produziram números plausíveis, ordens de aparência válida e telas que pareciam corretas. Em um sistema que age sobre o dinheiro de alguém, o bug que lança uma exceção é o tipo amigável.
Pontos-chave
- APIs de terceiros falham silenciosamente. Um HTTP 200 que traz menos dados do que você pediu é o modo de falha contra o qual você deve projetar, não o 500.
- Teste o simulador antes de testar qualquer coisa que ele simula. Um simulador quebrado retorna números idênticos aos de um que funciona.
- Relate o que você não conseguiu simular. Um resultado com uma lacuna declarada vale mais que um resultado de aparência limpa que descartou uma condição em silêncio.
- Precisão é um domínio, não uma função utilitária. Step sizes, tick sizes e mínimos diferem por par, por exchange e por tipo de mercado.
- Escolha as restrições cedo. Decidir nunca custodiar os fundos do usuário apagou uma categoria inteira de problemas em vez de resolvê-los.
- Em uma interface que lida com dinheiro, audite os gestos que a plataforma trata como edições. A roda do mouse sobre um campo numérico focado é um deles.
1. Presuma que toda API às vezes tem sucesso de forma incorreta
A suposição mais cara que fiz foi que uma requisição com falha pareceria uma falha.
Exchanges sob carga retornam HTTP 200 com menos candles do que o solicitado, sem campo de erro, sem aviso. Um dos nossos deep backtests pediu cerca de 536.000 candles e recebeu 58.000. A mesma requisição, uma hora depois, retornou o conjunto completo. Nada na resposta distinguia as duas.
Se você não estiver verificando, o motor simula uma estratégia sobre uma fração do histórico pretendido e informa um número cheio de confiança. Isso é pior que um crash, porque um crash é honesto.
Duas regras surgiram disso: valide o formato de uma resposta bem-sucedida, não apenas o código de status, e distinga falhas transitórias de falhas permanentes antes de decidir o que fazer. Elas parecem idênticas no momento em que acontecem e merecem respostas opostas.
História completa: o que eu subestimei
2. Teste o simulador, não a estratégia
Todo instinto te empurra a testar a lógica da estratégia, porque essa é a parte interessante. A armadilha é que um simulador quebrado produz uma saída indistinguível da de um que funciona. Uma execução que processou silenciosamente um décimo dos dados ainda retorna um número. Uma execução cujas saídas nunca foram avaliadas retorna um número suspeitosamente plano.
A mudança que resolveu isso foi verificar coisas que a estratégia não pode alterar: que os candles processados batem com os candles buscados, que uma execução sobre um período conhecido realmente o cobre, que uma configuração garantida para acionar produz operações.
Quando esses invariantes se sustentam, um resultado estranho é uma descoberta real. Quando não se sustentam, o resultado é ruído vestido de terno.
3. Relate o que você não conseguiu simular
Esta é uma escolha de design, não um bug, e acho que é a mais subestimada.
Algumas condições não podem ser reproduzidas. Um sinal externo de webhook não tem registro histórico. A tentação é ignorá-lo e avaliar o resto, o que faz a saída parecer completa.
Veja o que isso faz com uma estratégia que exige A E B. Se B não pode ser simulado, A decide sozinho, então o bot simulado entra onde o real teria ficado de fora. 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, então o viés aponta para um resultado de melhor aparência todas as vezes.
Continuamos rodando a simulação, mas o resultado carrega um aviso explícito nomeando o que foi ignorado. Um simulador que não consegue falhar de forma ruidosa vai falhar em silêncio, e a falha silenciosa em algo que produz números é o pior modo de falha que existe.
História completa: por que backtests mentem
4. Uma curva de patrimônio suave é uma pergunta, não uma resposta
Relacionado, e vale separar porque engana humanos em vez de código.
Uma escada de ordens de segurança DCA sem stop loss quase nunca realiza um prejuízo. Ela segura e adiciona. Então as operações fechadas são quase todas vencedoras, a taxa de acerto parece excelente e a curva de patrimônio é uma escadinha organizada.
O risco não foi a lugar nenhum. Ele foi adiado, e o adiamento tem um limite rígido: o capital que resta para mais ordens de segurança. A pergunta útil não é “qual foi o drawdown” mas “se o preço continuar indo contra a posição, em que ponto a estratégia fica sem espaço?”
O próprio drawdown precisa do mesmo ceticismo. Ele é medido em relação ao capital alocado no seu pico, então a mesma perda de 200 USDT aparece como 40% numa alocação de 500 USDT e 4% em 5.000. Dois bots operando de forma idêntica podem exibir números radicalmente diferentes.
História completa: entendendo o drawdown
5. Precisão é um domínio, não um auxiliar
Eu classifiquei “arredondar quantidades para steps válidos” como uma única função utilitária. É uma categoria de trabalho, e ela se esconde num lugar onde erros custam dinheiro.
O ponto flutuante começa: escalar 0.29 por um step de 0.01 resulta em 28.999999999999996, que arredonda para baixo um step inteiro a menos, silenciosamente, em direção a uma ordem menor.
Depois, as próprias exchanges discordam sobre as regras em si. Uma verifica o valor da ordem em USDT, outra verifica a contagem de contratos sem nenhuma verificação de valor, outra verifica os dois. Nosso pior bug nesta área veio de inventar um mínimo que uma das exchanges não tem, e então rejeitar ordens válidas com base nele. O sinal, em retrospecto, era que a nossa versão da restrição dependia do preço, enquanto a real não dependia.
Em qualquer lugar onde você escreve uma única constante para algo que a exchange define por par, você está escrevendo um bug com um pavio retardado.
História completa: o ponto flutuante vai te custar dinheiro
6. Escolha suas restrições cedo
A última não é um erro. É uma decisão cujas consequências eu subestimei, no bom sentido.
A Freya nunca custodia os fundos de trading do usuário. As chaves carregam permissão de leitura e de trade, nunca de saque, e uma chave que chega com permissão de saque é recusada, e não aceita e ignorada. Essa distinção importa mais do que parece: “não vamos sacar” é uma afirmação sobre o nosso comportamento que o usuário não pode verificar, enquanto “a chave não pode sacar” é imposta pela exchange e verificável pelo próprio usuário nas configurações dele.
Eu esperava que isso fosse um argumento de marketing. Acabou sendo arquitetural, e deixou o sistema menor: sem livro-razão de custódia, sem reconciliação, sem fila de saques, sem política de cold storage para o capital de trading. Uma classe inteira dos problemas mais difíceis do software financeiro está ausente porque nunca assumimos aquilo que os cria.
O que entra no lugar é mais estreito e mais afiado: o sistema tem que estar correto em tempo real, porque nunca pode desfazer nada. Uma ordem executada na conta de exchange de outra pessoa é definitiva.
História completa: projetando trading não custodial
A lição que eu não esperava precisar
Um bônus, porque foi o que mais me surpreendeu: numa interface que lida com dinheiro, o front-end precisa da mesma paranoia que o motor.
Um <input type="number"> focado incrementa seu valor quando você rola a roda do mouse sobre ele. Num seletor de quantidade isso é um aborrecimento. Na alavancagem, no tamanho da ordem e no preço, isso reescreve silenciosamente o que o usuário escolheu, e o formulário ainda valida como correto. Nada nesse bug é visível em uma captura de tela.
História completa: a roda do mouse está editando seus campos numéricos
Se eu comprimisse tudo em uma linha
Construa para as falhas que não se anunciam. As que lançam exceção vão te encontrar sozinhas.
