Front End

27 dez, 2017

Como criar um bot de compra e venda de Bitcoin usando Node.js – Parte 02

Publicidade

Desde que escrevi a primeira parte deste artigo sobre criação de um bot para comprar e vender Bitcoins e outras criptomoedas usando a API do Mercado Bitcoin, a valorização da moeda atingiu níveis históricos, ultrapassando a marca de U$15.000 em apenas 1 BTC!!!

Na primeira parte, foquei nos conceitos fundamentais, criação e configuração da conta na exchange, criação do projeto e codificação do algoritmo de monitoramento do mercado. Agora, faremos a codificação do nosso bot para que ele consiga comprar e vender criptomoedas.

Ajustando a API

Abra novamente o seu projeto (sugiro utilizar o Visual Studio Code) e vá até o seu arquivo api.js, que é nosso REST client para a API do Mercado Bitcoin. Caso não tenha feito a primeira parte do artigo, você pode baixá-la no artigo anterior, no formulário ao fim do mesmo.

Logo no início do arquivo api.js, vamos modificar o original para adicionar dois novos módulos que serão necessários, o crypto e o querystring (o unirest é do artigo anterior):

//MERCADO BITCOIN
const unirest = require('unirest')
const crypto  = require('crypto')
const qs      = require('querystring')

O primeiro serve para criptografar mensagens e o segundo para concatenar variáveis no formato de querystring do HTTP. Precisaremos de ambos, pois o endpoint que vamos acessar para efetuar transações exige criptografia e, para algumas operações, devemos enviar dados no body de um POST.

Para que estes módulos funcionem corretamente, precisamos instalá-los no projeto usando o NPM via console:

npm install -S crypto querystring

Ainda no arquivo api.js, logo abaixo dos requires, vamos definir duas novas constantes para o path da Trade API e para o endpoint da mesma (o outro endpoint é o de consulta de informações):

const ENDPOINT_API = 'https://www.mercadobitcoin.com.br/api/'
const ENDPOINT_TRADE_PATH = "/tapi/v3/"
const ENDPOINT_TRADE_API = 'https://www.mercadobitcoin.net' + ENDPOINT_TRADE_PATH

E por fim, logo abaixo do prototype da API de informações, vamos adicionar o construir de um novo objeto que representa a inicialização da API de trading:

var MercadoBitcoinTrade = function (config) {
    this.config = {
        KEY: config.key,
        SECRET: config.secret,
        PIN: config.pin,
        CURRENCY: config.currency
    }
}

As configurações passadas por parâmetro são a chave da API, o segredo da API, o PIN da sua conta e a moeda que vai negociar com esse bot (BTC, BCH e LTC no caso do Mercado Bitcoin). Note que KEY, SECRET e PIN eu lhe ensinei como obter no artigo anterior, dentro da sua área de trader da exchange.

Logo abaixo desta função construtora, vamos adicionar o prototype da API de trading, como segue:

MercadoBitcoinTrade.prototype = {
    
    getAccountInfo: function(success, error) {
        this.call('get_account_info', {}, success, error)
    },
 
    listMyOrders: function (parameters, success, error) {
        this.call('list_orders', parameters, success, error)
    },
 
    placeBuyOrder: function(qty, limit_price, success, error){
        this.call('place_buy_order', {coin_pair: `BRL${config.CURRENCY}`, quantity: (''+qty).substr(0,10), limit_price: ''+limit_price}, success, error)
    },
 
    placeSellOrder: function(qty, limit_price, success, error){
        this.call('place_sell_order', {coin_pair: `BRL${config.CURRENCY}`, quantity: (''+qty).substr(0,10), limit_price: ''+limit_price}, success, error)
    },
 
    cancelOrder: function (orderId, success, error) {
        this.call('cancel_order', {coin_pair: `BRL${config.CURRENCY}`, order_id: orderId}, success, error)
    },
 
    call: function (method, parameters, success, error) {
        //implementar
    }
}

Aqui estou definindo diversas operações que a API nos permite realizar, apenas configurando as chamadas e passando-as para uma função call, que não está implementada. Vale citar o que cada operação realiza:

  • getAccountInfo: pega informações da sua conta, especialmente o seu saldo atual em cada moeda, incluindo BRL;
  • listMyOrders: traz as suas últimas ordens no mercado, tanto de compra, quanto de venda e incluindo as que estão pendentes e canceladas, sendo que você pode filtrar livremente pelos parâmetros que discutirei mais tarde;
  • placeBuyOrder: criar uma nova ordem de compra no livro de negociações. Caso haja disponibilidade (tem moedas sendo vendidas ao preço que deseja pagar), a ordem será executada imediatamente. Caso contrário, ela vai pro livro e seu saldo fica bloqueado para honrar a compra;
  • placeSellOrder: cria uma nova ordem de venda no livro de negociações. Caso haja disponibilidade (tem ordens de compra ao preço que você deseja vender), a ordem será executada imediatamente. Caso contrário, ela vai pro livro e seu saldo na criptomoeda fica bloqueado para honrar a venda;
  • cancelOrder: cancela uma ordem sua no livro de negociações.

Você deve ter notado que diversas chamadas concatenam ‘BRL’ à sigla da criptomoeda que vamos negociar, gerando strings como BRLBTC, que é o que chamamos de ‘coin pair’ ou ‘par de moedas’, o que indica que você está negociando Reais por Bitcoins.

Também deve ter notado que em algumas ocasiões eu concateno variáveis numéricas com strings vazias, para forçá-las a serem strings, que é o formato que a API pede em diversos casos.

Para lidar com a assincronicidade inerente às chamadas de rede do Node, a maioria das functions espera um callback de success e outro de error.

E a função call? Ela é um pouquinho complexa, mas segue abaixo:

call: function (method, parameters, success, error) {

   var now = Math.round(new Date().getTime() / 1000)
   var queryString = qs.stringify({'tapi_method': method, 'tapi_nonce': now})
   if(parameters) queryString += '&' + qs.stringify(parameters)

   var signature = crypto.createHmac('sha512', this.config.SECRET)
                         .update(ENDPOINT_TRADE_PATH + '?' + queryString)
                         .digest('hex')

   unirest.post(ENDPOINT_TRADE_API)
          .headers({'TAPI-ID': this.config.KEY})
          .headers({'TAPI-MAC': signature})
          .send(queryString)
          .end(function (response) {
              if(response.body){
                 if (response.body.status_code === 100 && success)
                     success(response.body.response_data)
                 else if(error)
                     error(response.body.error_message)
                 else
                     console.log(response.body)
              }
              else console.log(response)
          })
}

A Trade API opera somente com requisições POST, cujos parâmetros da requisição devem estar no formato querystring no body. Para montar este body, conto com a ajuda do módulo querystring, concatenando os parâmetros, o nome da ação que esta request fará e um número único que identifica esta request, que aqui apenas estou pegando o timestamp atual, que sabemos que nunca se repetirá.

A Trade API também exige que cada requisição seja enviada com um cabeçalho TAPI-ID com a chave de API e um header TAPI-MAC, que é um hash HMAC-SHA512 que montamos usando o path completo desta requisição e o API secret como chave de criptografia. De acordo com o response da API, executamos o callback de success ou de error.

Para finalizar a nossa API, vamos apenas modificar o module.exports no final para expor ambas APIs neste módulo api.js:

Comprando Bitcoins em Node.js

Uma vez que seu bot esteja monitorando o mercado e você tenha implementado a APIs de trading, você deverá definir a lógica de compra e venda de criptomoedas. Não há uma regra aqui, exceto que você deve procurar comprar em baixa e vender em alta, para lucrar com a oscilação da moeda.

Para começar, vamos criar uma regra bem simples, baseada em uma observação do gráfico candlestick do Bitcoin no momento que escrevo este artigo:

Gráfico Bitcoin

Podemos observar neste gráfico que o Bitcoin está valorizado em R$51.800 por unidade, o que é um valor mediano considerando a máxima e mínima dos últimos dias. Por exemplo, vamos modificar nosso index.js para ordenar uma compra quando o Bitcoin cair abaixo de R$50.000 a unidade:

//index.js
require("dotenv-safe").load()
const MercadoBitcoin = require("./api").MercadoBitcoin
const MercadoBitcoinTrade = require("./api").MercadoBitcoinTrade
var infoApi = new MercadoBitcoin({ currency: 'BTC' })
var tradeApi = new MercadoBitcoinTrade({ 
    currency: 'BTC', 
    key: process.env.KEY, 
    secret: process.env.SECRET, 
    pin: process.env.PIN 
})
 
setInterval(() => 
   infoApi.ticker((tick) => {
       console.log(tick)
       if(tick.sell <= 50000){
            tradeApi.placeBuyOrder(1, 50000, 
                (data) => console.log('Ordem de compra inserida no livro. ' + data),
                (data) => console.log('Erro ao inserir ordem de compra no livro. ' + data))
       }
       else
            console.log('Ainda muito alto, vamos esperar pra comprar depois.')
   }),
   process.env.CRAWLER_INTERVAL
)

Neste código, eu inicializo a trade API logo no topo, usando diversas variáveis de ambiente presentes no seu arquivo .env que criamos na parte 01 deste artigo.

Agora, após realizar o ticker para pegar as informações atuais do mercado, eu verifico o preço de venda mais barato do Bitcoin atualmente. Se ele for menor que o número que eu defini como bom para compra (50000), eu mando comprar. Note que você pode criar todo tipo de inteligência para calcular este número, como verificar o spread das últimas 24h para inferir variação, verificar volume de vendas e compras realizadas para inferir tendências de alta ou baixa etc.

Uma vez que o preço esteja dentro do seu patamar considerado aceitável, é hora de criar a ordem de compra no livro de negociações. O primeiro parâmetro é quantos bitcoins você deseja comprar (1, mas podem ser frações com até 5 casas decimais) e o segundo parâmetro é o preço máximo que deseja pagar por uma unidade de bitcoin (50000). Diversas coisas podem acontecer neste momento.

Caso você não tenha saldo suficiente, você terá um erro como retorno. Já no caso de que não exista nenhuma ordem de venda compatível com o preço que deseja pagar, sua compra não será efetuada imediatamente. Ela vai parar no livro, seu saldo será bloqueado, e ela será executada assim que alguém estiver vendendo pelo valor que você deseja comprar (ou mais barato). Nestes casos de execução futura, você está sendo executado, e geralmente paga uma taxa de comissão menor para a exchange.

Caso você tenha saldo e exista uma ordem de venda no mercado compatível com a sua ordem de compra, ela será executada imediatamente. O seu saldo em BRL (R$) será liquidado, você receberá saldo em BTC e pagará a comissão de executador, que geralmente é mais alta do que a taxa de quem foi executado (o vendedor cuja ordem já estava no livro antes da sua).

Vendendo Bitcoins em Node.js

Da mesma forma que realizamos a compra, podemos criar uma condição de venda de criptomoedas em nosso robô. Aqui existem dois cenários: operar em STOP ou buscar a lucratividade máxima. Obviamente, o risco da segunda alternativa é muito maior e não existe um algoritmo 100% eficiente.

Se optar por operar em STOP, você pode definir uma lucratividade padrão que deseja atingir em cada trade, como 3% por exemplo. E assim que realizar uma compra, você já emite uma ordem de venda com o ágio correspondente, como abaixo (substitua o código de compra por este):

  if(tick.sell <= 50000){
 tradeApi.placeBuyOrder(1, 50000, 
 (data) => {
 console.log('Ordem de compra inserida no livro. ' + data)
 //operando em STOP de 3%
 tradeApi.placeSellOrder(1, 51500, 
 (data) => console.log('Ordem de venda inserida no livro. ' + data),
 (data) => console.log('Erro ao inserir ordem de venda no livro. ' + data))
 },
 (data) => console.log('Erro ao inserir ordem de compra no livro. ' + data))
 }

Com isso, você garante que venderá a um valor mínimo que lhe renda o lucro desejado (neste caso, vender a 51500 lhe renderá 2% líquido, por causa das comissões de compra e venda da exchange), tão logo o mercado atinja esse patamar. Eu gosto de operar assim, pois é uma forma de investimento de renda fixa com Bitcoin. Essa porcentagem deve ser relativa ao spread atual (diferença entre mínima e máxima dentro de um período) e deve ser sempre superior a 1% (por causa das comissões).

Note que esta venda está sendo posicionada no livro de negociações no mesmo instante que você está comprando a moeda. Ela não vai ser executada instantaneamente, mas assim que o mercado atingir aquele patamar, o que pode levar alguns minutos em dias de alta volatilidade, ou até mesmo semanas, em períodos de baixa volatilidade (ou caso tenha escolhido um valor muito alto).

Ordens de compra e de venda não executadas podem ser canceladas a qualquer momento usando a function apropriada e seu dinheiro é devolvido (seja ele BTC ou BRL).

Se optar por operar com lucratividade máxima, você não deverá posicionar sua ordem de venda imediatamente após a ordem de compra e, sim, deverá continuar monitorando o mercado para entender qual o melhor momento de vender. Esse desafio eu deixo para você bolar.

Otimizando compra e venda de Bitcoin

Usei números fixos nos exemplos anteriores, mas é óbvio que essa estratégia não é lá muito boa, pois cria um bot muito burro. O que sugiro é que você sempre realize uma consulta à sua conta para saber quanto possui de saldo da moeda que vai utilizar, para poder calcular quanto pode comprar/vender, evitando erros na API.

Para fazer isso, vamos criar uma function na index.js chamada getQuantity:

function getQuantity(coin, price, isBuy, callback){
    price = parseFloat(price)
    coin = isBuy ? 'brl' : coin.toLowerCase()
 
    tradeApi.getAccountInfo((response_data) => {
        var balance = parseFloat(response_data.balance[coin].available).toFixed(5)
		balance = parseFloat(balance)
        if(isBuy && balance < 50) return console.log('Sem saldo disponível para comprar!')
        console.log(`Saldo disponível de ${coin}: ${balance}`)
        
        if(isBuy) balance = parseFloat((balance / price).toFixed(5))
        callback(parseFloat(balance) - 0.00001)//tira a diferença que se ganha no arredondamento
    }, 
    (data) => console.log(data))
}

Esta function espera a moeda que estamos negociando, o preço de uma unidade dela, se é uma compra ou venda (true/false) e um callback.

Após algumas conversões, usamos a Trade API para pegar informações da conta do indivíduo e descobrir o saldo dele da moeda que vamos criar a ordem. Se for uma compra (isBuy === true), estaremos operando em BRL e temos de ter no mínimo R$50 na conta para poder gastar, comprando uma quantidade igual a saldo / preço. Se for uma venda de criptomoeda, o próprio saldo (balance) da criptomoeda é a quantidade que queremos negociar.

Aqui considero que você sempre deseja usar todo o seu saldo na transação o que, em volumes altos, pode ser muito arriscado.

Agora, vamos usar esta getQuantity nas nossas chamadas de compra, para deixá-las dinâmicas, adaptadas ao saldo atual na sua conta do Mercado Bitcoin:

if(tick.sell <= 50000){
    getQuantity('BRL', tick.sell, true, (qty) => {
         tradeApi.placeBuyOrder(qty, tick.sell, 
             (data) => {
                 console.log('Ordem de compra inserida no livro. ' + data)
                 //operando em STOP
                 tradeApi.placeSellOrder(data.quantity, tick.sell * parseFloat(process.env.PROFITABILITY), 
                     (data) => console.log('Ordem de venda inserida no livro. ' + data),
                     (data) => console.log('Erro ao inserir ordem de venda no livro. ' + data))
             },
             (data) => console.log('Erro ao inserir ordem de compra no livro. ' + data))
    })
}

Também usei a quantidade retornada pelo response da compra para passar ao request de venda, evitando chamadas desnecessárias à API. E por fim, usei o valor de venda do mercado (tick.sell) e multipliquei pela rentabilidade desejada que eu deixei configurada no arquivo .env na primeira parte do artigo, lembra? Assim, fica fácil de mudar isso depois, conforme a sua análise do mercado, até que tenha capacidade para montar uma fórmula em cima do spread corrente.

A única otimização que eu não tenho como lhe ajudar é a de determinar quando é o melhor momento para compra, deixei o 50000 chumbado ali no código e depois você deve mudar conforme o mercado. Parece um problema simples, mas não é. É tão complicado descobrir a mínima de compra quanto a máxima de venda e se eu tivesse essa fórmula já estaria rico.

Espero que tenham gostado e tendo qualquer dúvida, deixe nos comentários!