Aqui está uma barra. Binance USDⓈ-M perpétuo, BTCUSDT, o minuto que começa às 13:46:00 UTC. Ela saiu do endpoint de klines como um array simples de doze valores, e é a unidade de input mais comum em pesquisa quant de varejo e adjacências. Quase toda estratégia que geramos toca em algo com essa cara.
Então vamos desmontá-la, índice por índice, e ver o quanto disso um backtest interpreta errado.
| Índice | Valor | Nome |
|---|---|---|
| 0 | 1773495960000 | horário de abertura (ms) |
| 1 | "84120.50" | abertura |
| 2 | "84177.90" | máxima |
| 3 | "84098.10" | mínima |
| 4 | "84163.40" | fechamento |
| 5 | "38.417" | volume base (BTC) |
| 6 | 1773496019999 | horário de fechamento (ms) |
| 7 | "3232108.94" | volume em quote (USDT) |
| 8 | 1204 | número de negócios |
| 9 | "21.883" | volume base de compra taker |
| 10 | "1841203.55" | volume em quote de compra taker |
| 11 | "0" | ignorar |
[0] e [6]: qual é esse minuto, e o relógio de quem diz isso
Horário de abertura 1773495960000, horário de fechamento 1773496019999. Repare no segundo: termina em 19999, um milissegundo antes da abertura da próxima barra. A janela é semiaberta, e a exchange está te avisando disso explicitamente. Metade dos bugs de dados que já persegui começaram com alguém tratando os dois extremos como inclusivos e contando um negócio na fronteira duas vezes, ou alinhando um resample de forma que cada barra de 5 minutos tomasse emprestado um milissegundo da vizinha.
Os timestamps são do horário do motor de matching da exchange. Não é o seu relógio, não é o relógio de ingestão do seu vendor, e não é o mesmo relógio usado no snapshot de funding. Quando você faz o join de uma série de klines com uma série de funding rate ou de open interest puxada de um endpoint diferente, você está unindo fontes que concordam entre si com margem de aproximadamente um segundo, na maior parte do tempo. Para uma estratégia de 1 minuto isso é um erro de timing de 1,7% em cada linha unida. Para qualquer coisa sub-minuto, é fatal.
E essa barra está carimbada na sua abertura. A informação nela não é conhecível até 13:46:59.999. Toda questão de convenção de indexação — se eu desloco em um, se uso timestamps de fechamento ou de abertura da barra como meu horário de evento — é na verdade a mesma questão de lookahead disfarçada.
[1] "84120.50": a abertura que você não consegue negociar
A abertura é o preço do primeiro negócio impresso dentro da janela. É uma transação já concluída entre duas outras pessoas. No momento em que a barra existe como uma linha no seu dataframe, esse preço já tem sessenta segundos.
A barra anterior fechou em 84109.80, então há $10,70 de movimento de preço escondidos no gap entre duas barras de 1 minuto adjacentes. São 1,3 pontos-base, invisíveis em um gráfico, e cerca de um quarto da taxa taker. Tudo bem. Mas vá olhar a mesma série em um perp de altcoin durante uma divulgação de CPI dos EUA e esses gaps entre barras chegam a 15–40 pontos-base. Um backtest que sinaliza no fechamento da barra e executa na abertura da barra seguinte está assumindo silenciosamente que o gap é zero, e ele é zero exatamente quando isso não importa.
[2] e [3]: "84177.90" máxima, "84098.10" mínima
É aqui que a maioria dos motores de execução vai morrer, então esta seção é a mais longa.
A máxima e a mínima são preços extremos tocados. Não carregam tamanho nem duração. Olhando a fita de negócios crua para esse mesmo minuto, tudo em ou abaixo de 84100,00 somou 0,62 BTC em nove prints dentro de uma janela de 1,4 segundo. Então um stop de backtest em 84100 "executa" — e se sua posição é de 3 BTC, você comeu todo o book visível na queda e o restante do seu tamanho foi executado em algum ponto do recuo entre 84105–84130. A barra diz que a mínima foi 84098,10. A barra não diz que apenas $52.000 de notional foram negociados ali.
A outra direção é pior, porque te favorece. Suponha que você tinha uma ordem limite de venda parada em 84175. A máxima da barra é 84177,90, então um motor ingênuo te executa em 84175 e contabiliza rebate de maker em vez de custo taker. Se esse fill realmente aconteceu depende da posição na fila naquele nível de preço, coisa que a barra não pode saber e que você provavelmente nunca registrou. Tocado não é executado.
A regra em que fechamos no nosso motor de execução: uma ordem limite parada só executa se a barra negociar através do nível, não apenas até ele. Fills no tick extremo exato exigem evidência de volume-por-preço vinda da fita de negócios, caso contrário são rejeitados. Isso removeu cerca de 6% dos trades de uma carteira típica de reversão à média e derrubou o Sharpe de backtest de um candidato de 1,9 para 1,1. Aquele candidato nunca foi real; a regra de execução só foi a primeira coisa honesta o suficiente para dizer isso.
Armadilha relacionada: o caminho intrabarra. Se o range de uma barra abrange tanto o seu stop quanto o seu take-profit, o OHLCV não consegue te dizer qual veio primeiro. Todo motor precisa escolher uma convenção. O nosso assume stop primeiro, sempre, o que é pessimista e ocasionalmente errado e nunca gera um ganho falso. Se o seu motor assume take-profit primeiro, barras de range largo vão fabricar lucro a partir da ambiguidade, e barras de range largo são exatamente as que dominam a sua distribuição de PnL.
[4] "84163.40": o número menos robusto da barra
O fechamento é o último print dentro da janela. Só isso. Pode ser um lote fracionado de 0,002 BTC de um bot arredondando uma posição às 13:46:59,8. Esse tick único e estruturalmente arbitrário é o que a maioria dos pipelines de pesquisa usa para calcular todo sinal, marcar toda posição e avaliar toda saída.
Em perps de BTC isso quase não importa; em um perp de altcoin com pouca liquidez às 04:00 UTC importa enormemente, e a diferença entre os fechamentos de duas exchanges para o mesmo minuto pode superar toda a sua vantagem por trade. Quando o PnL de uma estratégia depende especificamente do fechamento, nós a rodamos de novo marcando pelo mark price da exchange, que é derivado de índice e muito mais difícil de manipular. Se os resultados divergirem, a estratégia estava negociando o artefato.
[5] e [7]: "38.417" e "3232108.94", volume em unidades de quê
O volume base é em BTC; o volume em quote é em USDT. Ambos aqui, o que é uma cortesia que nem toda venue oferece. O motivo para se importar é a agregação entre venues. Contratos margeados em coin são cotados em contratos de $100 de notional. Algumas feeds de ações reportam lotes redondos. Venues de mercados de previsão reportam contagem de shares, onde um share é uma reivindicação binária denominada em dólares. Somar "volume" entre um universo misto sem normalizar para uma única unidade de notional produz um ranking de liquidez que é pura bobagem — e será uma bobagem com aparência estável, que sobrevive à revisão.
Normalize tudo para notional em quote no momento da ingestão. Guarde o campo bruto também, mas nunca deixe uma estratégia vê-lo.
[8] 1204: o campo que deveria definir seu modelo de impacto
Número de negócios dividido pelo volume base dá um print médio de 0,032 BTC, cerca de $2.700. Se sua estratégia candidata quer entrar $250.000 de uma vez, ela está pedindo para ser cerca de 92 vezes o tamanho da transação típica naquele minuto. Esse número, e não alguma constante genérica de slippage de 5 pontos-base, é o que deveria conduzir o seu termo de impacto de raiz quadrada. Nós calculamos a taxa de participação por barra como uma coluna de primeira classe e rejeitamos estratégias cuja entrada mediana excede alguns pontos percentuais do notional da barra, porque tudo que vem depois disso é ficção.
O número de negócios também sinaliza os minutos estranhos a baixo custo. Volume normal, número de negócios despencando para 11? Alguém fez um block. Volume normal, número de negócios em 9.000? Isso é uma cascata de liquidação sendo mastigada em pedacinhos.
[9] e [10]: "21.883", o campo que todo mundo descarta
Volume base de compra taker. 21,883 de 38,417 BTC nessa barra foram iniciados por compradores, então o delta de volume assinado é +5,349 BTC e a divisão de agressores é 57/43 a favor dos compradores. A exchange está te entregando o desequilíbrio de fluxo de ordens, de graça, em um campo que a maioria nunca lê porque o pandas não nomeou a coluna para eles.
Não estou dizendo que isso prevê retornos sozinho; estratégias ingênuas de delta estão entre as formas mais confiáveis de doar dinheiro para o ledger de taxas. Mas é uma medição genuinamente diferente do preço, está disponível na mesma requisição que você já estava fazendo, e permite distinguir um rally que foi comprado de um rally que aconteceu porque os vendedores se afastaram. Essas duas coisas parecem idênticas em OHLC e se comportam de forma diferente dez minutos depois. Nosso agente de pesquisa trata uma hipótese que ignora a divisão taker em uma venue que a publica como deixar evidência sobre a mesa.
[11] "0": ignorar — e tudo mais que não está aqui
O índice 11 é um campo depreciado, permanentemente zero. Mais interessante é a lista do que essa barra não contém: sem bid, sem ask, sem spread, sem profundidade de book, sem funding rate, sem open interest, sem liquidações, sem mark price, sem index price. E, crucialmente, nenhuma forma de saber se a sua ordem teria sido maker ou taker, o que é a diferença entre pagar 0,045% e ganhar 0,01% nessa venue.
Então qualquer modelo de taxas construído só em cima de klines é uma suposição vestida de número. Nós resolvemos isso forçando toda estratégia a declarar seu estilo de execução de antemão, e cobrando taker em tudo que não consegue provar o contrário.
A barra que nunca apareceu
Última peça, e a que mais morde fora dos majors. Um minuto com zero negócios não produz nenhuma kline. Vendors e bibliotecas comumente fazem forward-fill: abertura = máxima = mínima = fechamento = fechamento anterior, volume 0. Seu indicador calcula tranquilamente. Sua estratégia vê uma linha válida e pode gerar um sinal em um minuto em que ninguém no mundo negociou aquele instrumento.
Em um perp mid-cap que ingerimos, 4,1% das barras de 1 minuto em uma janela de doze meses tinham zero negócios. Uma candidata de reversão à média nesse símbolo estava colocando 38% das suas entradas em barras sintéticas, porque preços sintéticos planos são catnip para qualquer coisa que meça desvio de uma média móvel. O backtest ficou lindo. Ela estava negociando os buracos nos dados.
É por isso que a camada de ingestão agora carrega um booleano synthetic por barra, propagado através de todo resample, e o crivo de verificação reprova qualquer estratégia cujos trades se concentrem nele. Coluna barata. Já matou mais candidatas do que qualquer indicador que já escrevemos.
Doze valores. Quatro deles interpretados errado rotineiramente, dois deles descartados rotineiramente, e uma categoria inteira deles ausente da linha e imaginada pelo motor. Antes do seu próximo backtest, vá puxar uma barra bruta do seu próprio armazenamento e leia cada campo em voz alta contra o que sua lógica de execução assume sobre ele. É um exercício de vinte minutos e eu nunca vi ninguém fazer isso e não encontrar nada.
← Todos os artigos