UNPKG

node-binance-api

Version:

Binance API for node https://github.com/jaggedsoft/node-binance-api

560 lines (554 loc) 20.6 kB
/* ============================================================ * node-binance-api * https://github.com/jaggedsoft/node-binance-api * ============================================================ * Copyright 2017-, Jon Eyrick * Released under the MIT License * ============================================================ */ module.exports = function() { 'use strict'; const WebSocket = require('ws'); const request = require('request'); const crypto = require('crypto'); const base = 'https://api.binance.com/api/'; const wapi = 'https://api.binance.com/wapi/'; const websocket_base = 'wss://stream.binance.com:9443/ws/'; let messageQueue = {}; let depthCache = {}; let ohlcLatest = {}; let klineQueue = {}; let options = {}; let info = {}; let ohlc = {}; let btcValue = 0.00; const publicRequest = function(url, data, callback, method = "GET") { if ( !data ) data = {}; let opt = { url: url, qs: data, method: method, agent: false, headers: { 'User-Agent': 'Mozilla/4.0 (compatible; Node Binance API)', 'Content-type': 'application/x-www-form-urlencoded' } }; request(opt, function(error, response, body) { if ( !response || !body ) throw "publicRequest error: "+error; if ( callback ) callback(JSON.parse(body)); }); }; const apiRequest = function(url, callback, method = "GET") { let opt = { url: url, method: method, agent: false, headers: { 'User-Agent': 'Mozilla/4.0 (compatible; Node Binance API)', 'Content-type': 'application/x-www-form-urlencoded', 'X-MBX-APIKEY': options.APIKEY } }; request(opt, function(error, response, body) { if ( !response || !body ) throw "apiRequest error: "+error; if ( callback ) callback(JSON.parse(body)); }); }; const signedRequest = function(url, data, callback, method = "GET") { if ( !data ) data = {}; data.timestamp = new Date().getTime(); if ( typeof data.symbol !== "undefined" ) data.symbol = data.symbol.replace('_',''); if ( typeof data.recvWindow == "undefined" ) data.recvWindow = options.recvWindow; let query = Object.keys(data).reduce(function(a,k){a.push(k+'='+encodeURIComponent(data[k]));return a},[]).join('&'); let signature = crypto.createHmac("sha256", options.APISECRET).update(query).digest("hex"); // set the HMAC hash header let opt = { url: url+'?'+query+'&signature='+signature, method: method, agent: false, headers: { 'User-Agent': 'Mozilla/4.0 (compatible; Node Binance API)', 'Content-type': 'application/x-www-form-urlencoded', 'X-MBX-APIKEY': options.APIKEY } }; request(opt, function(error, response, body) { if ( !response || !body ) throw "signedRequest error: "+error; if ( callback ) callback(JSON.parse(body)); }); }; const order = function(side, symbol, quantity, price, flags = {}, callback = false) { let opt = { symbol: symbol, side: side, type: "LIMIT", quantity: quantity }; if ( typeof flags.type !== "undefined" ) opt.type = flags.type; if ( opt.type == "LIMIT" ) { opt.price = price; opt.timeInForce = "GTC"; } if ( typeof flags.icebergQty !== "undefined" ) opt.icebergQty = flags.icebergQty; if ( typeof flags.stopPrice !== "undefined" ) opt.stopPrice = flags.stopPrice; signedRequest(base+"v3/order", opt, function(response) { if ( typeof response.msg !== "undefined" && response.msg == "Filter failure: MIN_NOTIONAL" ) { console.log("Order quantity too small. Must be > 0.01"); } if ( callback ) callback(response); else console.log(side+"("+symbol+","+quantity+","+price+") ",response); }, "POST"); }; //////////////////////////// const subscribe = function(endpoint, callback, reconnect = false) { const ws = new WebSocket(websocket_base+endpoint); ws.on('open', function() { //console.log("subscribe("+endpoint+")"); }); ws.on('close', function() { if ( reconnect ) { console.log("WebSocket reconnecting: "+endpoint); reconnect(); } else console.log("WebSocket connection closed! "+endpoint); }); ws.on('message', function(data) { //console.log(data); callback(JSON.parse(data)); }); }; const userDataHandler = function(data) { let type = data.e; if ( type == "outboundAccountInfo" ) { options.balance_callback(data); } else if ( type == "executionReport" ) { options.execution_callback(data); } else { console.log("Unexpected data: "+type); } }; //////////////////////////// const priceData = function(data) { let prices = {}; for ( let obj of data ) { prices[obj.symbol] = obj.price; } return prices; }; const bookPriceData = function(data) { let prices = {}; for ( let obj of data ) { prices[obj.symbol] = { bid:obj.bidPrice, bids:obj.bidQty, ask:obj.askPrice, asks:obj.askQty }; } return prices; }; const balanceData = function(data) { let balances = {}; for ( let obj of data.balances ) { balances[obj.asset] = {available:obj.free, onOrder:obj.locked}; } return balances; }; const klineData = function(symbol, interval, ticks) { // Used for /depth let last_time = 0; for ( let tick of ticks ) { let [time, open, high, low, close, volume, closeTime, assetVolume, trades, buyBaseVolume, buyAssetVolume, ignored] = tick; ohlc[symbol][interval][time] = {open:open, high:high, low:low, close:close, volume:volume}; last_time = time; } info[symbol][interval].timestamp = last_time; }; const klineConcat = function(symbol, interval) { // Combine all OHLC data with latest update let output = ohlc[symbol][interval]; if ( typeof ohlcLatest[symbol][interval].time == "undefined" ) return output; const time = ohlcLatest[symbol][interval].time; const last_updated = Object.keys(ohlc[symbol][interval]).pop(); if ( time >= last_updated ) { output[time] = ohlcLatest[symbol][interval]; delete output[time].time; output[time].isFinal = false; } return output; }; const klineHandler = function(symbol, kline, firstTime = 0) { // Used for websocket @kline //TODO: add Taker buy base asset volume let { e:eventType, E:eventTime, k:ticks } = kline; let { o:open, h:high, l:low, c:close, v:volume, i:interval, x:isFinal, q:quoteVolume, t:time } = ticks; //n:trades, V:buyVolume, Q:quoteBuyVolume if ( time <= firstTime ) return; if ( !isFinal ) { if ( typeof ohlcLatest[symbol][interval].time !== "undefined" ) { if ( ohlcLatest[symbol][interval].time > time ) return; } ohlcLatest[symbol][interval] = {open:open, high:high, low:low, close:close, volume:volume, time:time}; return; } // Delete an element from the beginning so we don't run out of memory const first_updated = Object.keys(ohlc[symbol][interval]).shift(); if ( first_updated ) delete ohlc[symbol][interval][first_updated]; ohlc[symbol][interval][time] = {open:open, high:high, low:low, close:close, volume:volume}; }; const depthData = function(data) { // Used for /depth endpoint let bids = {}, asks = {}, obj; for ( obj of data.bids ) { bids[obj[0]] = parseFloat(obj[1]); } for ( obj of data.asks ) { asks[obj[0]] = parseFloat(obj[1]); } return {bids:bids, asks:asks}; } const depthHandler = function(depth, firstUpdateId = 0) { // Used for websocket @depth let symbol = depth.s, obj; if ( depth.u <= firstUpdateId ) return; for ( obj of depth.b ) { //bids depthCache[symbol].bids[obj[0]] = parseFloat(obj[1]); if ( obj[1] == '0.00000000' ) { delete depthCache[symbol].bids[obj[0]]; } } for ( obj of depth.a ) { //asks depthCache[symbol].asks[obj[0]] = parseFloat(obj[1]); if ( obj[1] == '0.00000000' ) { delete depthCache[symbol].asks[obj[0]]; } } }; const depthVolume = function(symbol) { // Calculate Buy/Sell volume from DepthCache let cache = getDepthCache(symbol), quantity, price; let bidbase = 0, askbase = 0, bidqty = 0, askqty = 0; for ( price in cache.bids ) { quantity = cache.bids[price]; bidbase+= parseFloat((quantity * parseFloat(price)).toFixed(8)); bidqty+= quantity; } for ( price in cache.asks ) { quantity = cache.asks[price]; askbase+= parseFloat((quantity * parseFloat(price)).toFixed(8)); askqty+= quantity; } return {bids: bidbase, asks: askbase, bidQty: bidqty, askQty: askqty}; }; const getDepthCache = function(symbol) { if ( typeof depthCache[symbol] == "undefined" ) return {bids: {}, asks: {}}; return depthCache[symbol]; }; //////////////////////////// return { depthCache: function(symbol) { return getDepthCache(symbol); }, depthVolume: function(symbol) { return depthVolume(symbol); }, percent: function(min, max, width = 100) { return ( min * 0.01 ) / ( max * 0.01 ) * width; }, sum: function(array) { return array.reduce((a, b) => a + b, 0); }, reverse: function(object) { let range = Object.keys(object).reverse(), output = {}; for ( let price of range ) { output[price] = object[price]; } return output; }, array: function(obj) { return Object.keys(obj).map(function(key) { return [Number(key), obj[key]]; }); }, sortBids: function(symbol, max = Infinity, baseValue = false) { let object = {}, count = 0, cache; if ( typeof symbol == "object" ) cache = symbol; else cache = getDepthCache(symbol).bids; let sorted = Object.keys(cache).sort(function(a, b){return parseFloat(b)-parseFloat(a)}); let cumulative = 0; for ( let price of sorted ) { if ( baseValue == "cumulative" ) { cumulative+= parseFloat(cache[price]); object[price] = cumulative; } else if ( !baseValue ) object[price] = parseFloat(cache[price]); else object[price] = parseFloat((cache[price] * parseFloat(price)).toFixed(8)); if ( ++count > max ) break; } return object; }, sortAsks: function(symbol, max = Infinity, baseValue = false) { let object = {}, count = 0, cache; if ( typeof symbol == "object" ) cache = symbol; else cache = getDepthCache(symbol).asks; let sorted = Object.keys(cache).sort(function(a, b){return parseFloat(a)-parseFloat(b)}); let cumulative = 0; for ( let price of sorted ) { if ( baseValue == "cumulative" ) { cumulative+= parseFloat(cache[price]); object[price] = cumulative; } else if ( !baseValue ) object[price] = parseFloat(cache[price]); else object[price] = parseFloat((cache[price] * parseFloat(price)).toFixed(8)); if ( ++count > max ) break; } return object; }, first: function(object) { return Object.keys(object).shift(); }, last: function(object) { return Object.keys(object).pop(); }, slice: function(object, start = 0) { return Object.entries(object).slice(start).map(entry => entry[0]); }, min: function(object) { return Math.min.apply(Math, Object.keys(object)); }, max: function(object) { return Math.max.apply(Math, Object.keys(object)); }, options: function(opt) { options = opt; if ( typeof options.recvWindow == "undefined" ) options.recvWindow = 60000; }, buy: function(symbol, quantity, price, flags = {}, callback = false) { order("BUY", symbol, quantity, price, flags, callback); }, sell: function(symbol, quantity, price, flags = {}, callback = false) { order("SELL", symbol, quantity, price, flags, callback); }, marketBuy: function(symbol, quantity, callback = false) { order("BUY", symbol, quantity, 0, {type:"MARKET"}, callback); }, marketSell: function(symbol, quantity, callback = false) { order("SELL", symbol, quantity, 0, {type:"MARKET"}, callback); }, cancel: function(symbol, orderid, callback = false) { signedRequest(base+"v3/order", {symbol:symbol, orderId:orderid}, function(data) { if ( callback ) return callback.call(this, data, symbol); }, "DELETE"); }, orderStatus: function(symbol, orderid, callback) { signedRequest(base+"v3/order", {symbol:symbol, orderId:orderid}, function(data) { if ( callback ) return callback.call(this, data, symbol); }); }, openOrders: function(symbol, callback) { signedRequest(base+"v3/openOrders", {symbol:symbol}, function(data) { return callback.call(this, data, symbol); }); }, cancelOrders: function(symbol, callback = false) { signedRequest(base+"v3/openOrders", {symbol:symbol}, function(json) { for ( let obj of json ) { let quantity = obj.origQty - obj.executedQty; console.log("cancel order: "+obj.side+" "+symbol+" "+quantity+" @ "+obj.price+" #"+obj.orderId); signedRequest(base+"v3/order", {symbol:symbol, orderId:obj.orderId}, function(data) { if ( callback ) return callback.call(this, data, symbol); }, "DELETE"); } }); }, allOrders: function(symbol, callback) { signedRequest(base+"v3/allOrders", {symbol:symbol, limit:500}, function(data) { if ( callback ) return callback.call(this, data, symbol); }); }, depth: function(symbol, callback) { publicRequest(base+"v1/depth", {symbol:symbol}, function(data) { return callback.call(this, depthData(data), symbol); }); }, prices: function(callback) { request(base+"v1/ticker/allPrices", function(error, response, body) { if ( !response || !body ) throw "allPrices error: "+error; if ( callback ) callback(priceData(JSON.parse(body))); }); }, bookTickers: function(callback) { request(base+"v1/ticker/allBookTickers", function(error, response, body) { if ( !response || !body ) throw "allBookTickers error: "+error; if ( callback ) callback(bookPriceData(JSON.parse(body))); }); }, prevDay: function(symbol, callback) { publicRequest(base+"v1/ticker/24hr", {symbol:symbol}, function(data) { if ( callback ) return callback.call(this, data, symbol); }); }, withdraw: function(asset, address, amount, addressTag = false, callback = false) { let params = {asset, address, amount}; if ( addressTag ) params.addressTag = addressTag; signedRequest(wapi+"v3/withdraw.html", params, callback, "POST"); }, withdrawHistory: function(callback, asset = false) { let params = asset ? {asset:asset} : {}; signedRequest(wapi+"v3/withdrawHistory.html", params, callback); }, depositHistory: function(callback, asset = false) { let params = asset ? {asset:asset} : {}; signedRequest(wapi+"v3/depositHistory.html", params, callback); }, depositAddress: function(asset, callback) { signedRequest(wapi+"v3/depositAddress.html", {asset:asset}, callback); }, account: function(callback) { signedRequest(base+"v3/account", {}, callback); }, balance: function(callback) { signedRequest(base+"v3/account", {}, function(data) { if ( callback ) callback(balanceData(data)); }); }, trades: function(symbol, callback) { signedRequest(base+"v3/myTrades", {symbol:symbol}, function(data) { if ( callback ) return callback.call(this, data, symbol); }); }, // convert chart data to highstock array [timestamp,open,high,low,close] highstock: function(chart, include_volume = false) { let array = []; for ( let timestamp in chart ) { let obj = chart[timestamp]; let line = [ Number(timestamp), parseFloat(obj.open), parseFloat(obj.high), parseFloat(obj.low), parseFloat(obj.close) ]; if ( include_volume ) line.push(parseFloat(obj.volume)); array.push(line); } return array; }, ohlc: function(chart) { let open = [], high = [], low = [], close = [], volume = []; for ( let timestamp in chart ) { //ohlc[symbol][interval] let obj = chart[timestamp]; open.push(parseFloat(obj.open)); high.push(parseFloat(obj.high)); low.push(parseFloat(obj.low)); close.push(parseFloat(obj.close)); volume.push(parseFloat(obj.volume)); } return {open:open, high:high, low:low, close:close, volume:volume}; }, candlesticks: function(symbol, interval = "5m", callback) { //1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,1M publicRequest(base+"v1/klines", {symbol:symbol, interval:interval}, function(data) { return callback.call(this, data, symbol); }); }, publicRequest: function(url, data, callback, method = "GET") { publicRequest(url, data, callback, method) }, signedRequest: function(url, data, callback, method = "GET") { signedRequest(url, data, callback, method); }, websockets: { userData: function userData(callback, execution_callback = null) { let reconnect = function() { userData(callback, execution_callback); }; apiRequest(base+"v1/userDataStream", function(response) { options.listenKey = response.listenKey; setInterval(function() { // keepalive apiRequest(base+"v1/userDataStream", false, "PUT"); },60000); if ( typeof execution_callback == "function" ) { options.balance_callback = callback; options.execution_callback = execution_callback; subscribe(options.listenKey, userDataHandler, reconnect); return; } subscribe(options.listenKey, callback, reconnect); },"POST"); }, subscribe: function(url, callback, reconnect = false) { subscribe(url, callback, reconnect); }, depth: function depth(symbols, callback) { for ( let symbol of symbols ) { subscribe(symbol.toLowerCase()+"@depth", callback); } }, depthCache: function depthCacheFunction(symbols, callback) { for ( let symbol of symbols ) { if ( typeof info[symbol] == "undefined" ) info[symbol] = {}; info[symbol].firstUpdateId = 0; depthCache[symbol] = {bids: {}, asks: {}}; messageQueue[symbol] = []; let reconnect = function() { depthCacheFunction(symbols, callback); }; subscribe(symbol.toLowerCase()+"@depth", function(depth) { if ( !info[symbol].firstUpdateId ) { messageQueue[symbol].push(depth); return; } depthHandler(depth); if ( callback ) callback(symbol, depthCache[symbol]); }, reconnect); publicRequest(base+"v1/depth", {symbol:symbol}, function(json) { info[symbol].firstUpdateId = json.lastUpdateId; depthCache[symbol] = depthData(json); for ( let depth of messageQueue[symbol] ) { depthHandler(depth, json.lastUpdateId); } delete messageQueue[symbol]; if ( callback ) callback(symbol, depthCache[symbol]); }); } }, trades: function(symbols, callback) { for ( let symbol of symbols ) { subscribe(symbol.toLowerCase()+"@aggTrade", callback); } }, chart: function chart(symbols, interval, callback) { if ( typeof symbols == "string" ) symbols = [symbols]; // accept both strings and arrays for ( let symbol of symbols ) { if ( typeof info[symbol] == "undefined" ) info[symbol] = {}; if ( typeof info[symbol][interval] == "undefined" ) info[symbol][interval] = {}; if ( typeof ohlc[symbol] == "undefined" ) ohlc[symbol] = {}; if ( typeof ohlc[symbol][interval] == "undefined" ) ohlc[symbol][interval] = {}; if ( typeof ohlcLatest[symbol] == "undefined" ) ohlcLatest[symbol] = {}; if ( typeof ohlcLatest[symbol][interval] == "undefined" ) ohlcLatest[symbol][interval] = {}; if ( typeof klineQueue[symbol] == "undefined" ) klineQueue[symbol] = {}; if ( typeof klineQueue[symbol][interval] == "undefined" ) klineQueue[symbol][interval] = []; info[symbol][interval].timestamp = 0; let reconnect = function() { chart(symbols, interval, callback); }; subscribe(symbol.toLowerCase()+"@kline_"+interval, function(kline) { if ( !info[symbol][interval].timestamp ) { klineQueue[symbol][interval].push(kline); return; } //console.log("@klines at " + kline.k.t); klineHandler(symbol, kline); if ( callback ) callback(symbol, interval, klineConcat(symbol, interval)); }, reconnect); publicRequest(base+"v1/klines", {symbol:symbol, interval:interval}, function(data) { klineData(symbol, interval, data); //console.log("/klines at " + info[symbol][interval].timestamp); for ( let kline of klineQueue[symbol][interval] ) { klineHandler(symbol, kline, info[symbol][interval].timestamp); } delete klineQueue[symbol][interval]; if ( callback ) callback(symbol, interval, klineConcat(symbol, interval)); }); } }, candlesticks: function candlesticks(symbols, interval, callback) { let reconnect = function() { candlesticks(symbols, interval, callback); }; for ( let symbol of symbols ) { subscribe(symbol.toLowerCase()+"@kline_"+interval, callback, reconnect); } } } }; }();