27 de agosto de 2026 · engenharia de dados

O universo de símbolos é uma máquina do tempo: viés de sobrevivência nos backtests de perpétuos cripto

O universo de símbolos é uma máquina do tempo: viés de sobrevivência nos backtests de perpétuos cripto

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.

2.31 → 0.74Sharpe, lista de sobreviventes vs universo point-in-time
~1 em 5do teu universo de 2021 já não existe
380 KBum snapshot diário de exchangeInfo, comprimido com gzip

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.

CampoComo mudaO que falha
statusTRADING → SETTLING → removidoViés de sobrevivência; saídas fantasma a um preço de fecho que nunca foi negociado
onboardDateNovas listagens todas as semanas; ausente para símbolos descontinuadosNegociar contratos antes de existirem
tickSize / stepSizeAjustados à medida que os níveis de preço mudamArredondamento das ordens e preços limite que teriam sido rejeitados
minNotionalAumentado ao longo do tempo em livros pouco líquidosPosições pequenas que o teu encaminhador de ordens em produção recusaria
fundingIntervalHours8h durante anos, depois 4h ou 1h para muitos símbolosCarry errado por um fator de 2–3× precisamente nas altcoins que o teu filtro mantém
leverage bracketsEscalões e margem de manutenção revistosModelaçã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

  1. 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.
  2. 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.
  3. Uma função de universo que exige um argumento de data e hora. universe(ts), nunca universe(). 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.
  4. Uma verificação de pré-listagem no carregador de dados: nenhuma barra pode existir antes de onboard_ts menos um dia. Interrompe a execução; não te limites a emitir um aviso.
  5. 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.

viés de sobrevivênciadados point-in-timefuturos criptobacktestingengenharia de dados
← Todos os artigos