Em outras oportunidades aqui no blog e também no meu canal do Youtube eu trouxe tutoriais de como criar robôs de criptomoedas, ou seja, automações via software para comprar e vender criptoativos em corretoras como Binance, Mercado Bitcoin e BityPreço. Em todas essas ocasiões eu trouxe o racional de como monitorar, comprar e vender, mas muitas vezes temos uma ideia de estratégia ou até mesmo encontramos uma em algum vídeo de trading e queremos apenas testá-la, não queremos sair comprando e vendendo logo de cara pois afinal estamos falando de um mercado de risco.
Independente se você é um trader experiente ou iniciante você sempre deve testar as suas estratégias antes de colocá-la em produção e no tutorial de hoje eu quero te mostrar como escrever scripts para fazer justamente isso, utilizando dados históricos da Binance para ver como a estratégia se sairia. Claro, sempre vale a máxima de que resultados passados não são garatia de resultados futuros, mas uma estratégia que tenha performado bem em um backtest tem muito mais chance de performar bem no futuro do que uma que performou mal, certo?
Se preferir, pode assistir ao vídeo abaixo ao invés de ler.
Vamos lá!
#1 – Setup do Projeto
Dentre os players mundiais de criptomoedas, uma das mais agressivas em termos de taxas e portfólio de moedas é a Binance, considerada a maior exchange de criptomoedas do mundo em volume de negociações. Usaremos ela como fonte de dados aqui pois é um bom termômetro do mercado de criptomoedas, mas o que vou mostrar pode ser usado com qualquer exchange (de cripto ou não) que disponibilize publicamente as suas velas. Nem mesmo você precisa ter conta na Binance para fazer este tutorial, pois ele não efetuará as compras e vendas de fato, ok?
Dito isso, antes de sairmos programando precisamos configurar nosso projeto. Usarei aqui a linguagem JavaScript, então precisamos do Node.js instalado na máquina. Se ainda não tem o ambiente de desenvolvimento para Node.js configurado, você pode ver como fazer no vídeo abaixo.
Agora crie uma pasta no seu computador com o nome de binance-backtest e dentro dela rode o comando abaixo pelo seu terminal de linha de comando para inicializar um projeto Node.js na pasta.
1 2 3 |
npm init -y |
Agora coloque um arquivo index.js vazio dentro da pasta e vamos instalar um pacote via NPM pra deixar nosso projeto preparado. Segue o comando de instalação:
1 2 3 |
npm i axios |
O pacote Axios serve para fazer chamadas HTTP nas APIs da Binance. Caso nunca tenha usado Axios antes e queira uma introdução mais completa, o vídeo abaixo pode ajudar.
Crie um arquivo index.js na raiz do seu projeto, uma pasta data (para guardar nosso cache) e opcionalmente ajuste seu package.json para que o script de start inicie o programa pelo index.js, como abaixo:
1 2 3 4 5 |
"scripts": { "start": "node index" }, |
Faremos nosso script de backtest em duas etapas. Na primeira, faremos chamadas HTTP para a API de velas históricos da Binance e armazenaremos os dados localmente, para termos o montante suficiente para os testes. Depois, escreveremos um script de exemplo para os testes, com uma estratégia didática (não é uma recomendação), para você entender como pode fazer o processamento dos dados das velas.
#2 – Obtendo os dados históricos das velas
Para consumir a API de velas históricas da Binance eu usarei o pacote Axios, que instalamos anteriormente. Vamos começar importando o Axios e também o pacote File System (fs) em seu arquivo index.js (abaixo) e algumas constantes definindo as informações que queremos baixar. Neste exemplo vou fazer um script de backtest em cima de ETHUSDT com velas de 15 minutos que serão armazenadas em uma pasta data localmente (essa pasta já deve estar criada), com um objetivo de 2% de lucratividade por trade (ajuste os parâmetros conforme o interesse do seu teste).
1 2 3 4 5 6 7 8 9 |
const axios = require("axios"); const fs = require("fs"); const SYMBOL = "ETHUSDT"; const INTERVAL = "15m"; const FILENAME = `data/${SYMBOL}_${INTERVAL}.txt`; const PROFITABILITY = 2;//% |
Com essas informações agora posso escrever a função que acessa a API de velas históricas, pega somente os valores de fechamento das velas (closes) e salva os dados em um arquivo, como abaixo.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
async function downloadCandles(startTime) { if (startTime >= Date.now()) return; const response = await axios.get(`https://api.binance.com/api/v3/klines?symbol=${SYMBOL}&interval=${INTERVAL}&limit=1000&startTime=${startTime}`); const closes = response.data.map(k => k[4]).reduce((a, b) => a + "\n" + b); console.log(closes); if (fs.existsSync(FILENAME)) fs.appendFileSync(FILENAME, "\n" + closes); else fs.writeFileSync(FILENAME, closes); await downloadCandles(response.data[response.data.length - 1][6] + 1); } downloadCandles(Date.now() - (365 * 24 * 60 * 60 * 1000)); |
Aqui eu criei uma função downloadCandles que espera o timestamp inicial do histórico que vamos baixar. Se olhar mais ao final do script eu faço uma chamada à função downloadCandles passando como startTime 1 ano no passado (em timestamp). Acho que um teste com 1 ano de dados seja uma período ok para ter uma ideia se a estratégia vale ou não à pena, mas você pode ajustar à sua realidade, apenas tome cuidado com os limites de chamadas da Binance. Para este endpoint você não pode fazer mais de 3000 requests por minuto. Mais informações no vídeo abaixo.
Mas voltando ao código acima, a primeira coisa que eu testo é se o startTime ainda está no passado, porque quero que a função seja recursiva, baixando tantos dados quanto necessários para pegar do startTime até hoje. Então eu pego somente os close prices (posição 4 do array OHLCV que vem da API) e reduzo tudo a uma string separada por quebra de linha (“\n”). Essa string eu mando escrever em um arquivo local. Caso o arquivo já exista (afinal é uma função recursiva), eu anexo os dados ao final dele.
Por fim, faço downloadCandles chamar a si própria novamente, mas usando o close time da última vela (posição 6 do OHLCV) +1 milisegundo como startTime da próxima recursão, garantindo que a cada ida na API a gente pegue 1000 velas (limit) que ainda não temos os dados.
Se tudo deu certo você pode rodar nosso programa com npm start e ver no console os dados dos close prices sendo impressos e, após a conclusão do script, ver o arquivo gerado na pasta data. Caso tenha algum erro, deixe nos comentários que tentarei ajudar, mas não avance antes de ter o arquivo com os dados.
#3 – Script de Backtest
Agora que temos os dados que precisamos vamos fazer o script de backtest em si. Esse script vai variar enormemente pois é nele que você vai escrever a estratégia que quer testar. Claro, algumas etapas são globais e a ideia aqui é eu me focar justamente nelas. A estratégia que vou testar é super tosca e não é uma recomendação: vou começar comprando e quando a moeda subir x % vou vender. Depois, quando ela cair x % vou comprar de novo e assim sucessivamente. Será que funciona?
Comente a chamada de downloadCandles (afinal já baixamos os dados que precisamos) e vamos criar a função doBacktest como abaixo.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
//downloadCandles(Date.now() - (365 * 24 * 60 * 60 * 1000)); async function doBacktest() { let closes = fs.readFileSync(FILENAME, { encoding: "utf-8" }); closes = closes.split("\n").map(c => parseFloat(c)); const firstCandle = closes[0]; let orderPrice = firstCandle; let isOpened = true; let qtdSells = 0; let accPnl = 0; console.log(`Abriu e comprou no preço ${firstCandle}`); //script de backtest vai aqui const lastCandle = closes[closes.length - 1]; let holdPnl = ((lastCandle * 100) / firstCandle) - 100; console.log("Fechou no preço: " + lastCandle); console.log("Operações: ", qtdSells); console.log("PnL Trade %: ", accPnl); console.log("PnL Hold %: ", holdPnl); } doBacktest(); |
O primeiro passo é ler o arquivo e transformar a enorme string de dentro dele em um array de números decimais que chamei de closes. Depois eu vou declarar algumas variáveis e constantes, a saber:
- firstCandle: valor de fechamento do primeiro candle do período;
- orderPrice: valor que estou monitorando para negociar;
- isOpened: booleano indicando se estou com a posição aberta (comprado) ou fechada (vendido). Como vou considerar que já vou sair comprando, vou deixar true;
- qtdSells: quantidade de vendas realizadas no período, o que indica ciclos completos de compra e venda;
- accPnl: PnL acumulado no período em % (Profits and Losses);
- lastCandle: valor de fechamento do último candle do período;
- holdPnl: PnL caso eu tivesse feito hold, ou seja, comprei no início e vendi no final;
Outro ponto que vou assumir aqui é que obrigatoriamente vamos vender no último candle se ainda estivermos com a posição aberta. Isso para que tenhamos um percentual de resultado final. Note também que para saber se nossos trades realmente valeram a pena temos de comparar no mínimo com alguém que fez o hold da moeda durante o mesmo período, caso contrário é fácil ter um resultado falso, acreditando que performou bem, por exemplo.
Realize um teste do script executando-o com npm start e verá nesse primeiro momento quanto que um hold no período teria dado de resultado.
Agora vamos seguir em frente no espaço que deixei comentado anteriormente.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
for (let i = 1; i < closes.length; i++) { const currentCandle = closes[i]; const targetPrice = isOpened ? (orderPrice * (1 + (PROFITABILITY / 100))) : (orderPrice * (1 - (PROFITABILITY / 100))); const isLastCandle = i === closes.length - 1; if (isOpened) { if ((currentCandle >= targetPrice) || isLastCandle) { const pnl = ((currentCandle * 100) / orderPrice) - 100; accPnl += pnl; isOpened = false; orderPrice = currentCandle; qtdSells++; console.log(`Vendeu com ${pnl.toFixed(2)}% de lucro no preço: ${currentCandle}`); } } else if (!isOpened) { if (currentCandle <= targetPrice) { isOpened = true; console.log(`Comprou no preço ${currentCandle}`); } orderPrice = currentCandle; } } |
Aqui eu fiz um laço que vai percorrer do segundo candle (1) até o último (closes.length). Começaremos do segundo porque assumo imediatamente que compramos no primeiro. Dentro do for, a primeira coisa que faço é pegar o preço do candle que estamos iterando atualmente (currentCandle). Todo o script será executado em cima desse valor.
Depois, calculo o preço-alvo (targetprice), entendendo que se estou com a posição aberta vou querer vender, então o preço-alvo será x % acima do monitorado atualmente (orderPrice). Agora se estou com a posição fechada vou querer comprar, então o preço-alvo será x % abaixo do monitorado. Um operador ternário nos facilita essa lógica para que na sequência eu verifique se a iteração atual é a última ou não (isLastCandle), lembrando que na última iteração eu vou vender/fechar se ainda estiver comprado/aberto.
Com essas informações eu tenho um grande if/else da lógica central, baseado se estou aberto (isOpened true) ou fechado, que é o mesmo que dizer se estou comprado ou vendido. Se eu estiver com a posição aberta, ou seja, já comprei, eu tenho duas condições possíveis para venda: se o preço do candle atual é maior ou igual ao preço-alvo ou se é o último candle. Em ambos casos, eu calculo o pnl com uma “regra de três”, somo no pnl acumulado, inverto a isOpened, atualizo o preço monitorado, incremento a quantidade de vendas realizadas e mando uma mensagem no console.
Agora no else, se estamos com a posição fechada, eu verifico se o preço atual é menor que o preço-alvo, porque se for, eu vou comprar e atualizar a flag isOpened. Independente se comprei ou não eu devo atualizar o preço monitorado porque se tratando de compra não posso ficar preso em um preço muito antigo que talvez nunca mais volte.
Agora você pode rodar novamente o script com npm start e terá um resultado parecido com esse (claro que vai mudar conforme o tempo passa).
Repare na imagem que aparece a última compra, a última venda e para cada venda, o % de lucro obtido com a mesma (antes das taxas de corretagem, importante frisar). Depois, o preço em que a moeda fechou o período, quantas operações realizamos (ciclos de compra-venda completos), o PnL dos trades (acumulado) e o PnL de hold (comprando na primeira vela e vendendo na última).
Como pode ver, ficaram bem próximos no período de 1 ano neste exemplo de estratégia tosca. Estratégias mais elaboradas como as que usam Médias Móveis, RSI e outros indicadores costumam performar muito melhor do que apenas com preço, por exemplo. Você também pode pensar em coisas como trailing stop, para vender no topo, ou ainda em stop loss para evitar ficar “preso” na moeda por muito tempo quando ela muda de direção. Mas esses são os seus deveres de casa, pois este tutorial termina aqui.
Um abraço e até a próxima!
Olá, tudo bem?
O que você achou deste conteúdo? Conte nos comentários.