O sistema reportava 77,1% de acerto — a proporção de decisões que se confirmaram. O acerto direcional real era 20% — uma em cada cinco calls de alta ou baixa. Depois de corrigir os erros de medição por trás dessa distância, o número verdadeiro é 51,5% — uma moeda. E o braço aleatório de controle (escolha direcional por sorteio) acerta 53,1%, estatisticamente idêntico ao LLM (z ≈ 0,45, p ≈ 0,66). O LLM não tem edge sobre cara ou coroa.
Este é o diagnóstico dos quatro erros de medição que produziram cada um desses números, mais um bug de unidade que quase virou o quinto. Nenhum deles é de trading. Todos são de instrumentação, e todos reaparecem em qualquer deployment de AI em empresa.
O que é isso, em um parágrafo
Construí um sistema de paper trading onde um LLM (DeepSeek-v3 via OpenRouter) gera teses de investimento a cada 2 horas para um universo de BTC/ETH/SOL + PETR4/VALE3/ITUB4, executa ordens automaticamente num simulador SQLite com preços reais da Binance/yfinance, e gerencia risco com regras automáticas de saída (stop loss, take profit, trailing stop). Capital simulado: R$10.000. A hipótese primária era de alpha — testar se agentes LLM extraem sinal direcional de contexto de mercado. Ela foi refutada. O que resta é valor arquitetural: o sistema produziu dados honestos sobre os próprios limites. Registro isso como consolação, não como objetivo reescrito depois do resultado.
O que eu achei que estava medindo
A hipótese: um LLM alimentado com sinais de preço (momentum, volatilidade), sentimento (Fear & Greed Index, RSS de CoinDesk/CoinTelegraph), dados on-chain (taxa de financiamento de derivativos, interesse aberto de posições), mercados de previsão (Polymarket) e fundamentos (P/E, ROE, dividend yield para equities) conseguiria emitir direções — bullish (aposta em alta), bearish (aposta em baixa) ou neutral (não espera movimento relevante) — com hit rate (proporção de decisões confirmadas) acima de 50% e PnL (lucro ou prejuízo realizado) positivo após custos. O sistema também incorporaria memory via embedding similarity — decisões passadas resolvidas seriam injetadas como few-shot examples no prompt. Acreditava que o edge viria da síntese de fontes heterogêneas que um trader humano não consegue processar em tempo real.
Constraints que eu não podia mudar
Sem capital real — logo, sem slippage observado, apenas modelado. Rate limit da Binance: 1.200 de peso de requisição por minuto (não 1.200 requisições; um único klines com limit=1000 consome 10 de peso), suficiente para o pipeline mas inviável para replay exaustivo em janelas longas. Histórico: o backtest avalia apenas decisões efetivamente tomadas pelo sistema em múltiplos horizontes — não simula novas decisões sobre dados históricos arbitrários. Orçamento de inferência: ~7 chamadas LLM por ciclo de 2h, cada uma com prompt de ~2-4k tokens. Custo baixo (DeepSeek-v3), mas determinou um único LLM para todo o universo.
Os modos de falha
A análise de calibração que aparece abaixo foi feita sobre um snapshot documentado na primeira semana de operação (início de junho de 2026, 144 decisões, ~21/dia). O sistema continuou rodando via cron por ~62 dias no total (junho-agosto, 3.773 decisões, ~61/dia — a taxa triplicou porque o universo de ativos e a frequência de coleta de sinais foram expandidos após as primeiras duas semanas). O alpha reportado no modo de falha #2 refere-se ao período completo, e os testes estatísticos do #1 usam o dataset integral do braço live (Arm A).
Os três números no lede deste post — 77,1%, 20%, 51,5% — são todos reais. São três medições diferentes do mesmo sistema, cada uma com um erro diferente. 77,1% inclui neutral, onde “acerto” é tautologia. 20% usa threshold fixo de ±0,3%, abaixo do ruído do ativo. 51,5% é o resultado com threshold ATR-scaled, excluindo decisões inconclusive (movimento insuficiente para classificar como certo ou errado). O arco de um para o outro é o assunto deste post.
1. Amostra insuficiente para calibração
Sintoma. No snapshot de 7 dias, confidence bands aparentemente inversas: decisões com confiança < 50% apresentavam 74% de hit rate, enquanto 65-80% de confiança mostravam apenas 21%. A diferença parecia dramática o suficiente para justificar uma recalibração imediata.
Diagnóstico. 144 decisões no snapshot, ~44 direcionais em 3 bands. Parecia pouco. Decidi esperar e acumular dados. Depois de 62 dias com o braço live (Arm A), o quadro mudou:
| Banda de confiança | Correct | Incorrect | n | Hit rate |
|---|---|---|---|---|
| < 50% | 119 | 104 | 223 | 53,4% |
| 50-65% | 199 | 201 | 400 | 49,8% |
| 65-80% | 107 | 95 | 202 | 53,0% |
(Inconclusivos excluídos; n direcional total = 825 no Arm A.)
As bands colapsaram para ~50% — uma moeda. Fisher’s exact test pairwise entre bands retorna p > 0,35 em todos os pares. Chi-square omnibus: χ² = 0,975, 2 graus de liberdade, p = 0,61. O “74% vs 21%” do snapshot era puro ruído de amostra pequena, e os dados completos o demonstram de forma mais contundente do que qualquer argumento teórico: confiança do LLM não tem valor preditivo sobre acurácia direcional.
Isso é um resultado nulo. Não é antissinal (o que seria explorável invertendo as calls); é ausência de sinal, confirmada por um braço de controle aleatório que acerta 53,1% no mesmo período — estatisticamente idêntico ao LLM.
Por que passou despercebido. O dashboard reportava hit rate como métrica pontual sem intervalo de confiança. “74% vs 21%” parece uma diferença dramática quando apresentada como número único; com IC de 95%, ambos os intervalos englobam toda a faixa de 0% a 100%. É um erro de instrumentação, não de estatística — a métrica estava disponível, a incerteza sobre ela não estava.
Correção. B1 (recalibrar bands) foi explicitamente rebaixado para P3. O critério adotado: 30+ amostras direcionais por banda antes de qualquer ajuste. O sizing (quanto capital alocar por decisão) migrou de confidence-based para hit-rate empírico rolling 20 — só aloca se o símbolo tiver > 50% de acerto direcional nas últimas 20 decisões, e nunca mais que 5% do portfólio.
2. Custos ausentes do simulador
Sintoma. Curva de equity positiva no PaperEngine. As métricas de PnL pareciam consistentes e pequenas, mascarando o fato de que o componente dominante do retorno estava ausente.
Diagnóstico. A versão inicial do PaperEngine não modelava taxa ou slippage. Comprava e vendia pelo preço spot exato da Binance. Em estratégia de alta rotatividade (ciclo de 2h, múltiplos símbolos), o custo de transação é da mesma ordem de grandeza do edge esperado — o que significa que a diferença entre lucro e prejuízo estava inteiramente dentro do termo omitido. Depois de adicionar taker fee de 0,10% + slippage de 0,05% por lado (total ~0,15% por round-trip), o PnL do sistema passou de marginalmente positivo para -0,84%, com alpha de -4,21% — ou seja, o sistema perdeu 4,21% em relação a simplesmente comprar os mesmos ativos em partes iguais e não fazer nada (HODL equal-weight) no mesmo período de ~60 dias.
Por que passou despercebido. O simulador foi construído para validar o fluxo de ordens (buy reduz saldo, sell calcula PnL realizado, média de custo funciona), não para estimar retorno. Depois foi lido como se fosse a segunda coisa. Um componente construído para um propósito e lido para outro é o modo de falha mais comum em sistema de AI corporativo: o mock que vira produção, o protótipo que vira produto.
Correção. Modelo de custo adotado: taker fee 0,10% + slippage 0,05% por lado, aplicado em toda buy/sell no PaperEngine. Não tenho as duas curvas de equity (com e sem custo) publicadas lado a lado porque o custo foi adicionado antes da coleta sistemática de dados de performance — o gráfico “antes” seria uma mentira retroativa.
O bug de unidade que quase virou modo de falha
Houve um segundo erro escondido dentro deste: o campo outcome_pnl_pct já era armazenado em porcentagem (ex: -0,96 significava -0,96%), mas a query analítica aplicava AVG() * 100, multiplicando por 100 duas vezes. O diagnóstico inicial mostrava “correct avg PnL -9,09%” quando o real era -0,09%.
Isso é uma classe de erro diferente dos outros quatro. Não é omissão de modelo nem amostra pequena — é erro de unidade em camada analítica. O número produzido (-9,09%) era plausível o suficiente para ninguém questionar: parecia perda real, não bug de query. Em contexto corporativo, é o dashboard de executivo que mostra “redução de 40% no tempo de processamento” quando a unidade original já estava em segundos e a query dividiu por 60 duas vezes. O número parece correto porque está na ordem de grandeza esperada, e ninguém audita a query até alguém perguntar de onde vem.
3. Rótulos abaixo do piso de ruído
Sintoma. O sistema com acurácia aparentemente razoável (77,1% de hit rate geral) e comportamento inconsistente em produção. Hit rate direcional real de apenas 20% — 1 acerto em cada 5 calls de alta (bullish) ou baixa (bearish).
Diagnóstico. Rótulos definidos com threshold fixo de ±0,3% (DIRECTIONAL_THRESHOLD_PCT = 0.3) contra volatilidade de crypto que move 2-5% intraday. O rótulo estava dentro do ruído da própria série — o sistema estava sendo treinado e avaliado sobre uma variável majoritariamente aleatória. Uma call bullish “correta” com +0,4% de movimento não significa que o sistema identificou sinal; significa que o preço subiu dentro do ruído normal. O hit rate geral de 77,1% no snapshot era inflado pela massa de neutral (69% das decisões no snapshot; 42% no dataset completo após o prompt bias forçar mais direcionais), onde “acertar” significava apenas que o preço não se moveu muito — uma tautologia.
Por que passou despercebido. O threshold de ±0,3% foi herdado da primeira versão do resolve() sem validação contra a volatilidade realizada. Parecia um número razoável — pequeno o suficiente para capturar movimento, grande o suficiente para não contar zero. Mas a volatilidade de BTC/ETH/SOL em janelas de 2h faz esse threshold ser puramente aleatório. O erro foi tratar o threshold como parâmetro de design em vez de derivá-lo da estatística do ativo.
Correção. O threshold passou a ser ATR-scaled: uma call direcional é “correta” apenas se o preço mover > 0,5 × ATR(symbol) na direção prevista. O ATR (Average True Range — medida de volatilidade que calcula a amplitude média de movimento por candle) é computado das últimas 14 velas de 1h da Binance, cached por 5 minutos. Para BTC, isso tipicamente resulta em threshold de ~0,8-1,2% em vez de 0,3%. Neutral permanece com threshold fixo de ±0,5%. Decisões que não atingem nenhum dos dois polos são marcadas inconclusive (movimento insuficiente para classificar) e excluídas do denominador do hit rate.
4. Correlação tratada como independência
Sintoma. O check de correlação disparando quase continuamente. Todo trade novo em crypto tinha correlation_warning no log.
Diagnóstico. BTC/ETH/SOL com correlação na casa de 0,8+. Três ativos no universo, mas aproximadamente um fator de risco. Qualquer regra condicionada a “quantos ativos confirmam” degenera para “um ativo confirma, três vezes”. O corte automático de alocação (reduzir em 50% quando |corr| > 0,7 com posição existente) disparava em basicamente toda trade crypto, pois qualquer par de criptos no universo violava o threshold.
Por que passou despercebido. A mecânica de correlation-aware allocation foi implementada corretamente — o problema é que ela foi implementada como se o universo tivesse diversidade real. Com 3 criptos altamente correlacionadas, a regra não reduz risco; reduz allocation em todo trade e mantém o mesmo fator de risco. A proteção virou fricção.
Correção. Limite de grupo correlacionado em 40% do portfólio além do corte individual. Universo mantido intencionalmente (objetivo inclui testar a arquitetura), com a restrição de grupo operando sobre o fator crypto, não sobre ativos individuais.
O que eu deliberadamente NÃO construí
- Execução com capital real. Tentador porque paper trading não gera dor real e portanto não testa psicologia. Não construído porque os modos de falha acima precisavam ser resolvidos primeiro — executar com medição defeituosa é mais perigoso que não executar.
- Otimização de hiperparâmetro sobre a amostra existente. Eu mesmo pedi “ajustar bands de confiança” quando vi 74% vs 21%. Tentador porque parece quick win. Não construído porque a amostra significava que qualquer “otimização” era overfitting. Escrever “p = 0,61, adiar” é mais difícil que rodar um grid search, mas é a decisão correta.
- Forçar binary direction (eliminar neutral). O sistema tinha 69% de decisões neutral e parecia óbvio cortar. Não construído porque o hit rate direcional era 20% — forçar binary sem ter sinal troca neutral por incorrect massivo. A abordagem adotada foi aumentar gradualmente o directional bias no prompt e medir.
O experimento que deveria ter vindo primeiro
Três braços em sombra sobre o mesmo fluxo de dados e a mesma janela:
- Arm A — o agente LLM (DeepSeek-v3 com sinais + sentimento + Polymarket + precedentes)
- Arm B — regra determinística de momentum (24h change + 7d momentum, sem LLM)
- Arm C — escolha aleatória com o mesmo perfil de exposição
Este experimento foi implementado em 8 de julho de 2026 — 37 dias depois do início do sistema. O pipeline gera os três braços a cada ciclo, mas apenas o Arm A executa trades reais no paper engine (live_arm = "A"). Os braços B e C são registrados no mesmo SQLite, resolvidos no mesmo horizonte, mas sem impacto no portfólio.
Resultado medido (threshold ATR-scaled, excluindo inconclusivos):
| Braço | Correct | Incorrect | n | Hit rate |
|---|---|---|---|---|
| A (LLM) | 425 | 400 | 825 | 51,5% |
| B (Regra) | 34 | 37 | 71 | 47,9% |
| C (Aleatório) | 137 | 121 | 258 | 53,1% |
Arm A e Arm C são estatisticamente indistinguíveis (z ≈ 0,45, p ≈ 0,66). Arm B tem n menor (o braço de regra gera mais inconclusive por design) e fica entre ambos. O LLM não tem edge sobre escolha aleatória neste universo e janela.
O experimento deveria ter existido desde o primeiro ciclo. Se existisse, eu teria poupado seis semanas otimizando prompts e sizing para um sistema que performa como uma moeda.
O loop de correção
O sistema implementa três mecanismos encadeados:
1. Circuit-breaker por símbolo. Se o hit rate rolling de 10 trades de um símbolo cai abaixo de 40%, o símbolo é pausado por 24h. (A janela é mais curta que o rolling 20 do sizing — intencional: circuit-breaker precisa reagir rápido, sizing precisa de estabilidade.) Exemplo real: SOLBRL acumulou PnL consistentemente negativo nas primeiras 24h; o circuit-breaker a pausou, evitando mais 4-5 trades no mesmo padrão.
2. Few-shot filtrado por símbolo. A memória semântica (embedding similarity via Ollama mxbai-embed-large) injeta decisões passadas resolvidas como precedentes no prompt. Inicialmente global (top 5 melhores); corrigido para filtrar por símbolo primeiro, fallback global se < 3 exemplos. SOLBRL estava aprendendo com exemplos de BTCBRL, que operam em regime diferente — depois da filtragem, SOL parou de receber precedentes não-representativos.
3. Regime gate. Um classificador de regime (bull/bear/chop baseado em 90d momentum + volatilidade) força neutral no braço live quando detecta chop — mercado sem tendência definida. Reduziu trades direcionais nesses períodos, mas não resolve o problema fundamental: acertar que o mercado está sem tendência não significa que o LLM teria sinal quando não está.
O que isto generaliza para deployment corporativo
Os quatro modos de falha, mais o bug de unidade, não são de trading. São de instrumentação, e reaparecem em qualquer deployment de AI em empresa:
Amostra insuficiente → o piloto de 6 semanas que “funcionou” com 40 casos e não sobrevive ao mês três. O dashboard mostra 87% de acurácia sem intervalo de confiança, e ninguém pergunta quantas amostras estão atrás do número até o modelo entrar em produção e a acurácia colapsar para 52%.
Custo omitido do simulador → o ROI calculado sobre tempo de inferência, ignorando revisão humana, retrabalho e o custo de o output estar errado. “O agente processa 1.000 documentos por $2” ignora que 12% dos outputs exigem correção humana a $40/hora de analista sênior.
Rótulo abaixo do ruído → o eval cujo critério de acerto é mais fino que a variação natural entre dois revisores humanos competentes. Se dois especialistas discordam em 15% dos casos sobre se um output é “correto”, o modelo não pode ser avaliado abaixo dessa margem — qualquer acurácia reportada com threshold mais fino que o desacordo inter-humano é ruído apresentado como métrica.
Correlação como independência → as “cinco fontes de validação” que na prática derivam do mesmo sistema upstream. Cinco agentes LLM validando um output não são cinco fontes independentes se todos consultam o mesmo knowledge base e o mesmo modelo. A independência precisa ser estrutural, não nominal.
Erro de unidade em camada analítica → o dashboard de executivo que mostra “redução de 40% no tempo de processamento” porque a query SQL dividiu segundos por 60 duas vezes. O número parece correto porque está na ordem de grandeza esperada. Ninguém audita a query até alguém perguntar de onde vem — e a pergunta nunca vem porque o número passou no teste do “parece certo”.
A conclusão operacional: o eval é o entregável, não o modelo. O modelo é commodity; a medição que diz se ele serve naquele workflow específico não é, e não existe fora do contexto do cliente.
O que eu ainda não sei
- O LLM tem edge em algum subuniverso específico — ex: apenas stocks, apenas BTC, apenas alta volatilidade? Os 51,5% agregados escondem possíveis heterogeneidades. Sem segmentação, não é possível afirmar que o sinal é zero em toda parte.
- Qual horizonte temporal maximiza o edge (se ele existe)? O backtest das decisões existentes indicou 1h com 79% de hit, degradando após 4h — mas isso pode ser artefato de amostra pequena, e o backtest replaya decisões já tomadas, não simula novas sobre histórico arbitrário.
- Em um universo não-correlacionado (ex: crypto + forex + commodities), o sistema teria melhor performance por diversificação real, ou o sinal do LLM é tão fraco que diversificação apenas dilui sem criar alpha?