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çaCorrectIncorrectnHit rate
< 50%11910422353,4%
50-65%19920140049,8%
65-80%1079520253,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 sem alterar o fator de risco.

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:

  1. Arm A — o agente LLM (DeepSeek-v3 com sinais + sentimento + Polymarket + precedentes)
  2. Arm B — regra determinística de momentum (24h change + 7d momentum, sem LLM)
  3. 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çoCorrectIncorrectnHit rate
A (LLM)42540082551,5%
B (Regra)34377147,9%
C (Aleatório)13712125853,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 e rótulo abaixo do ruído são duas faces do mesmo problema: a métrica não reflete a realidade que pretende medir. 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%. Do outro lado, 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.

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.

As “cinco fontes de validação” que na prática derivam do mesmo sistema upstream não são independentes. Cinco agentes LLM validando um output não são cinco fontes se todos consultam o mesmo knowledge base e o mesmo modelo. A independência precisa ser estrutural, não nominal.

E o 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 é que o modelo é commodity. O entregável é a medição que diz se ele serve naquele workflow específico, e ela não existe fora do contexto do cliente.

O que eu ainda não sei

  1. 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.
  2. 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.
  3. 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?