Enviaste-me o notebook na noite de domingo e, desde então, tenho-no aberto no segundo monitor. Momentum cross-sectional, os 150 perpétuos Binance USDⓈ-M com maior volume em dólares nos últimos 30 dias, rebalanceamento semanal, posição longa no decil superior e curta no inferior, de 2021-01 a 2026-06. Sharpe de 2.31, drawdown máximo de 14.2% e um modelo de custos que me pareceu honesto: taker à entrada e à saída, funding acumulado por intervalo, um termo de slippage que escala com o tamanho da tua ordem face à liquidez do livro. Fizeste bem as partes difíceis. Depois perguntaste-me porque é que seis semanas de paper trading não deram em nada e se a vantagem se tinha dissipado.
Não se dissipou. Nunca esteve no backtest. Vê a célula 4:
info = requests.get(BASE + "/fapi/v1/exchangeInfo").json()
symbols = [s["symbol"] for s in info["symbols"]
if s["status"] == "TRADING" and s["quoteAsset"] == "USDT"]
Chamaste esse endpoint em junho de 2026 e usaste a resposta para definir o que era negociável em março de 2021. Por definição, todos os símbolos dessa lista sobreviveram até junho de 2026. Esse é o problema todo e vale mais do que o resto do teu modelo de custos junto.
O que o endpoint não te diz
Não há histórico em exchangeInfo. Não há parâmetro asOf, arquivo nem registo de alterações. É uma fotografia do momento e a Binance nunca prometeu mais do que isso. Quando um contrato é removido da listagem, a respetiva entrada desaparece da resposta e o ticker deixa de existir, pelo menos para a API. Desde 2019, a Binance já listou mais de 600 perpétuos USDⓈ-M e retirou bem mais de uma centena. BTS, COCOS, TOMO, RAY, FTT, SC e uma longa cauda de contratos da época das altcoins de 2021, que tiveram um trimestre de glória e depois foram perdendo atividade até a plataforma os retirar.
Pergunta a ti próprio de que nomes gosta um filtro de momentum de 30 dias. Não de BTC. Gosta da moeda que acabou de triplicar com um pump de listagem e um ciclo no Twitter. Essa população e a dos ativos que acabam por ser retirados sobrepõem-se bastante, e o teu filtro de universo eliminou essa sobreposição.
Refiz a tua execução com o nosso arquivo de snapshots, um universo point-in-time, o mesmo sinal e os mesmos custos. O Sharpe ficou em 0.74, com um drawdown de 31%. Cerca de dois quintos da diferença vêm de nomes retirados da listagem que nunca poderias ter mantido. Outro quarto resulta do problema inverso, que é mais subtil e que suspeito que te vai agradar menos.
O outro extremo da máquina do tempo: símbolos que ainda não existiam
A tua variável usa retornos de 90 dias. A tua janela móvel tem min_periods=20, porque a definiste uma vez para impedir que o aquecimento arrasasse o primeiro trimestre da amostra e nunca mais a revistaste. Assim, um contrato listado há 21 dias recebe uma pontuação de momentum calculada com apenas três semanas de movimento de preços após a listagem, entra no decil superior quase sempre que a listagem correu bem e acaba na tua carteira.
Essa parte é discutivelmente legítima. O que não é legítimo é isto: o teu fornecedor preencheu retroativamente alguns desses símbolos com dados spot ou de índice anteriores à existência do perpétuo, por isso alguns contratos têm histórico de klines anterior à sua própria onboardDate. Podes confirmar isto em cerca de um minuto. Cruza a tua tabela de preços com as datas de listagem e conta as linhas anteriores à listagem. Nos teus dados há 41 símbolos com barras anteriores à listagem e um deles, no verão de 2023, contribui um ganho de 6% numa única semana para a curva de capital, com uma posição num contrato que só viria a existir nove dias depois.
Uma string de ticker não é um identificador. É um rótulo que a plataforma aluga, por vezes duas vezes.
E isso leva-nos às mudanças de nome. MATICUSDT passou a POLUSDT. FTMUSDT passou a SUSDT numa conversão 1:1. A situação da LUNA em maio de 2022 deixou uma LUNCUSDT e, mais tarde, uma LUNAUSDT totalmente nova, com a mesma raiz de ticker de algo que perdeu praticamente tudo. Se o teu carregador usa a string do símbolo como chave e concatena os ficheiros que encontra, tens pelo menos uma série com uma descontinuidade que não resulta de um movimento de preço. A tua variável de momentum vai interpretar essa descontinuidade como o sinal mais forte da secção transversal.
Tudo o resto que muda sem dares por isso
Depois de aceitares que a lista de símbolos varia ao longo do tempo, o mesmo argumento aplica-se a todos os outros campos dessa resposta. Estás a usar os valores de hoje para todos eles.
| Campo | Como muda | O que falha |
|---|---|---|
| status | TRADING → SETTLING → removido | Viés de sobrevivência; saídas fantasma a um preço de fecho que nunca foi negociado |
| onboardDate | Novas listagens todas as semanas; ausente para símbolos descontinuados | Negociar contratos antes de existirem |
| tickSize / stepSize | Ajustados à medida que os níveis de preço mudam | Arredondamento das ordens e preços limite que teriam sido rejeitados |
| minNotional | Aumentado ao longo do tempo em livros pouco líquidos | Posições pequenas que o teu encaminhador de ordens em produção recusaria |
| fundingIntervalHours | 8h durante anos, depois 4h ou 1h para muitos símbolos | Carry errado por um fator de 2–3× precisamente nas altcoins que o teu filtro mantém |
| leverage brackets | Escalões e margem de manutenção revistos | Modelação de liquidações e capacidade de margem |
No teu caso, o funding é o que mais pesa. O teu ciclo de acumulação assume três pagamentos por dia ao longo de todo o histórico. Uma fatia significativa da carteira de altcoins passou a ter funding de quatro em quatro horas, e é nas posições curtas de nomes com funding elevado que está uma boa parte do teu PnL simulado. Não estás a errar por uma diferença de arredondamento. Estás a errar por um fator.
A retirada da listagem é um evento e não o estás a modelar
Na tua nova execução point-in-time, dei à estratégia uma saída generosa. As retiradas reais seguem um guião: um anúncio, geralmente com sete a catorze dias de antecedência, depois uma janela em que só se pode reduzir a posição e, por fim, liquidação forçada a um preço de marcação. O anúncio é informação pública e podes agir com base nele, por isso a simulação honesta é sair ao fecho do dia do anúncio. Mas, numa retirada típica de uma altcoin, esse fecho já está 10-20% abaixo da semana anterior, o livro tem pouca liquidez e o teu termo de slippage precisa de ter em conta que está a operar num regime diferente. Se o teu motor de execução te dá o preço de liquidação sem impacto, estás discretamente a tornar negociáveis ativos em queda pelo justo valor.
Se nunca arquivaste exchangeInfo, ainda vais a tempo. O dump público em data.binance.vision/data/futures/um/monthly/klines/ ainda mantém diretórios de símbolos retirados da listagem muito depois de a API os ter esquecido. Enumera os diretórios; o primeiro e o último ficheiro mensal de cada símbolo dão-te uma janela de listagem e retirada utilizável, sem precisares de um fornecedor. É uma reconstrução, não um registo, e não recupera os tick sizes nem os intervalos de funding. Mas vai dizer-te que símbolos existiam e quando, o que corresponde a 80% do que precisas para segunda-feira.
O que eu construiria antes de voltares a mexer no sinal
- Um cron que recolhe diariamente exchangeInfo de todas as plataformas que analisas e o guarda em armazenamento de objetos, indexado por data. Com gzip, fica abaixo de meio megabyte. Dez anos custam-te um erro de arredondamento no S3 e dão-te uma capacidade de investigação que não poderás comprar mais tarde.
- Um registo mestre de ativos derivado desses snapshots: uma linha por (venue, symbol, valid_from, valid_to), com o conjunto completo de campos. Compara snapshots consecutivos para o gerar e trata cada alteração de campo como uma nova linha.
- Uma função de universo que exige um argumento de data e hora.
universe(ts), nuncauniverse(). Torna impossível usar outra coisa por omissão, para que ninguém, nem mesmo tu daqui a uns meses à 1 da manhã, recorra acidentalmente à lista de sobreviventes. - Uma verificação de pré-listagem no carregador de dados: nenhuma barra pode existir antes de
onboard_tsmenos um dia. Interrompe a execução; não te limites a emitir um aviso. - Um ID interno de instrumento estável que sobreviva às mudanças de nome, deixando o ticker como atributo secundário. Associa POL e MATIC ao mesmo ID e assinala as redenominações, para que a lógica de continuidade possa recusar uni-las.
Faz isso e volta a executar o backtest. A minha aposta é que vais chegar perto do 0.74 que obtive. A pergunta interessante passa a ser se um 0.74 que inclui os nomes desaparecidos tem algo que valha a pena testar em paper trading. Talvez tenha. O momentum cross-sectional em perpétuos não é irrelevante, e uma parte do que resta depois de corrigires o universo é carry genuíno do lado curto. Também vais ver os resultados do paper trading e do backtest começarem a coincidir, porque o paper trading sempre usou um universo point-in-time. Nunca teve escolha.
P.S. Isto não é uma particularidade das criptomoedas; aqui é apenas mais evidente. Os investigadores de ações lidam com retornos de ativos retirados da listagem e tickers reutilizados desde que a CRSP começou a disponibilizá-los. Nos mercados de previsão, o problema é levado ao extremo: todos os contratos expiram por definição, por isso o universo é feito apenas de listagens e encerramentos. Se algum dia adaptares este filtro à Kalshi, começa pelo registo mestre de ativos. Não há outro tipo de histórico nesse mercado.
← Todos os artigos


