UNPKG

node-binance-api

Version:

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

1,225 lines (1,163 loc) 68.5 kB
// Work in progress, browser compatible port of the library. Uses built-in websocket which is different from node's implementation. Untested //TODO: Add auto-reconnect let base = 'https://api.binance.com/api/'; let wapi = 'https://api.binance.com/wapi/'; let sapi = 'https://api.binance.com/sapi/'; let fapi = 'https://fapi.binance.com/fapi/'; let fapiTest = 'https://testnet.binancefuture.com/fapi/'; let stream = 'wss://stream.binance.com:9443/ws/'; let fstreamSingle = 'wss://fstream.binance.com/ws/'; let fstream = 'wss://fstream.binance.com/stream?streams='; let combineStream = 'wss://stream.binance.com:9443/stream?streams='; // let base = 'https://api.binance.com/api/'; // let wapi = 'https://api.binance.com/wapi/'; // let sapi = 'https://api.binance.com/sapi/'; // let fapi = 'https://fapi.binance.com/fapi/'; // let fapiTest = 'https://testnet.binancefuture.com/fapi/'; // let fstream = 'wss://fstream.binance.com/stream?streams='; // let fstreamSingle = 'wss://fstream.binance.com/ws/'; // let stream = 'wss://stream.binance.com:9443/ws/'; // let combineStream = 'wss://stream.binance.com:9443/stream?streams='; const contentType = 'x-www-form-urlencoded'; let subscriptions = {}, futuresSubscriptions = {}; let futuresTicks = {}, futuresMeta = {}, futuresRealtime = {}, futuresKlineQueue = {}; let depthCache = {}, depthCacheContext = {}, ohlcLatest = {}, klineQueue = {}, ohlc = {}; const default_options = { recvWindow: 5000, useServerTime: false, reconnect: true, verbose: true, log: console.log }; let options = default_options; let info = { timeOffset: 0 }; let socketHeartbeatInterval = null; if ( options ) setOptions( options ); function setOptions( opt = {}, callback = false ) { if ( typeof opt === 'string' ) { // Pass json config filename options = JSON.parse( file.readFileSync( opt ) ); } else options = opt; if ( typeof options.recvWindow === 'undefined' ) options.recvWindow = default_options.recvWindow; if ( typeof options.useServerTime === 'undefined' ) options.useServerTime = default_options.useServerTime; if ( typeof options.reconnect === 'undefined' ) options.reconnect = default_options.reconnect; if ( typeof options.test === 'undefined' ) options.test = default_options.test; if ( typeof options.log === 'undefined' ) options.log = default_options.log; if ( typeof options.verbose === 'undefined' ) options.verbose = default_options.verbose; if ( typeof options.urls !== 'undefined' ) { const { urls } = options; if ( typeof urls.base === 'string' ) base = urls.base; if ( typeof urls.wapi === 'string' ) wapi = urls.wapi; if ( typeof urls.sapi === 'string' ) sapi = urls.sapi; if ( typeof urls.fapi === 'string' ) fapi = urls.fapi; if ( typeof urls.fapiTest === 'string' ) fapiTest = urls.fapiTest; if ( typeof urls.stream === 'string' ) stream = urls.stream; if ( typeof urls.combineStream === 'string' ) combineStream = urls.combineStream; if ( typeof urls.fstream === 'string' ) fstream = urls.fstream; if ( typeof urls.fstreamSingle === 'string' ) fstreamSingle = urls.fstreamSingle; } if ( options.useServerTime ) { apiRequest( base + 'v3/time', {}, function ( response ) { info.timeOffset = response.serverTime - new Date().getTime(); //options.log("server time set: ", response.serverTime, info.timeOffset); if ( callback ) callback(); } ); } else if ( callback ) callback(); } function request( url, params = {}, callback = false, opt = {} ) { let hasParams = Object.keys( params ).length; if ( !url ) url = opt.url; if ( !hasParams && opt.qs ) { params = opt.qs; hasParams = Object.keys( params ).length; } if ( hasParams ) url = `${ url }?${ new URLSearchParams( params ).toString() }`; if ( !url ) throw `axios error: ${ url }`; if ( options.verbose ) console.info( 'request', url, params, opt ); //opt.url = url; opt.method = !opt.method ? 'get' : opt.method; opt.baseURL = !opt.baseURL ? base : opt.baseURL; axios( url, opt ).then( function ( response ) { if ( callback ) callback( response.data ); } ).catch( function ( error ) { if ( error.response ) console.warn( error.response.data ); throw error.message; } ); } /** * Checks to see of the object is iterable * @param {object} obj - The object check * @return {boolean} true or false is iterable */ const isIterable = obj => { if ( !obj ) return false; return Symbol.iterator in Object( obj ); //return typeof obj[Symbol.iterator] === 'function'; } // if ( Object.keys( params ).length ) url = `${ url }?${ new URLSearchParams( params ).toString() }`; const reqObj = ( url, data = {}, method = 'GET', key ) => ( { url: url, json: data, //qs method, timeout: options.recvWindow, headers: { 'Content-type': contentType, 'X-MBX-APIKEY': key || '' } } ) const reqObjPOST = ( url, data = {}, method = 'POST', key ) => ( { url: url, json: data, //form: data, method, timeout: options.recvWindow, headers: { 'Content-type': contentType, 'X-MBX-APIKEY': key || '' } } ) /** * Create a http request to the public API * @param {string} url - The http endpoint * @param {object} data - The data to send * @param {function} callback - The callback method to call * @param {string} method - the http method * @return {undefined} */ const publicRequest = ( url, data = {}, callback, method = 'GET' ) => { if ( Object.keys( data ).length ) url = `${ url }?${ new URLSearchParams( data ).toString() }`; if ( options.verbose ) console.info( `publicRequest`, url, data, method ); let opt = reqObj( url, data, method ); request( opt, {}, callback ); }; const makeQueryString = q => Object.keys( q ).reduce( ( a, k ) => { if ( q[k] !== undefined ) { a.push( k + '=' + encodeURIComponent( q[k] ) ) } return a }, [] ).join( '&' ); /** * Create a http request to the public API * @param {string} url - The http endpoint * @param {object} data - The data to send * @param {function} callback - The callback method to call * @param {string} method - the http method * @return {undefined} */ const apiRequest = ( url, data = {}, callback, method = 'GET' ) => { if ( Object.keys( data ).length ) url = `${ url }?${ new URLSearchParams( data ).toString() }`; if ( options.verbose ) console.info( `apiRequest`, url, data, method ); if ( !options.APIKEY ) throw Error( 'apiRequest: Invalid API Key' ); let opt = reqObj( url, data, method, options.APIKEY ); request( opt, {}, callback ); }; const promiseRequest = async ( url, data = {}, flags = {} ) => { //if ( Object.keys( params ).length ) url = `${ url }?${ new URLSearchParams( params ).toString() }`; if ( options.verbose ) console.info( `promiseRequest`, url, data, flags ); return new Promise( ( resolve, reject ) => { let query = '', headers = { 'Content-type': 'application/x-www-form-urlencoded' }; if ( typeof flags.method === 'undefined' ) flags.method = 'GET'; // GET POST PUT DELETE if ( typeof flags.type === 'undefined' ) flags.type = false; // TRADE, SIGNED, MARKET_DATA, USER_DATA, USER_STREAM else { if ( typeof data.recvWindow === 'undefined' ) data.recvWindow = options.recvWindow; headers['X-MBX-APIKEY'] = options.APIKEY; if ( !options.APIKEY ) return reject( 'Invalid API Key' ); } let baseURL = typeof flags.base === 'undefined' ? base : flags.base; if ( options.test && baseURL === fapi ) baseURL = fapiTest; let opt = { headers, url: baseURL + url, method: flags.method, timeout: options.recvWindow, followAllRedirects: true }; if ( flags.type === 'SIGNED' || flags.type === 'TRADE' || flags.type === 'USER_DATA' ) { if ( !options.APISECRET ) return reject( 'Invalid API Secret' ); data.timestamp = new Date().getTime() + info.timeOffset; query = makeQueryString( data ); data.signature = crypto.createHmac( 'sha256', options.APISECRET ).update( query ).digest( 'hex' ); // HMAC hash header opt.url = `${ baseURL }${ url }?${ query }&signature=${ data.signature }`; } opt.qs = data; try { request( false, {}, ( data ) => { //response //if ( error ) return reject( error ); try { return resolve ( data ) //return resolve ( response.data ); /*if ( !error && response.statusCode == 200 ) return resolve( response.data ); if ( typeof error.response.status !== 'undefined' ) { return resolve( response.json() ); }*/ //return reject( response.data ); } catch ( err ) { return reject( `promiseRequest error #${ response.statusCode }` ); } }, opt ); } catch ( err ) { return reject( err ); } } ); }; const MD5 = new Hashes.MD5, openState = 1; const socketHeartbeat = () => { return; for ( let endpointId in subscriptions ) { const ws = subscriptions[endpointId]; if ( ws.isAlive ) { ws.isAlive = false; // TODO: Fix heartbeat. Browser client can't send pings // if ( ws.readyState === openState ) ws.send( '{"ping": true}' ); } else { if ( options.verbose ) options.log( 'Terminating inactive/broken WebSocket: ' + ws.endpoint ); if ( ws.readyState === openState ) ws.close(); } } }; /** * Called when socket is opened, subscriptions are registered for later reference * @param {function} opened_callback - a callback function * @return {undefined} */ const handleSocketOpen = function ( opened_callback ) { this.isAlive = true; if ( Object.keys( subscriptions ).length === 0 ) { socketHeartbeatInterval = setInterval( socketHeartbeat, 30000 ); } subscriptions[this.endpoint] = this; if ( typeof opened_callback === 'function' ) opened_callback( this.endpoint ); }; /** * Called when socket is closed, subscriptions are de-registered for later reference * @param {boolean} reconnect - true or false to reconnect the socket * @param {string} code - code associated with the socket * @param {string} reason - string with the response * @return {undefined} */ const handleSocketClose = function ( reconnect, code, reason ) { delete subscriptions[this.endpoint]; if ( subscriptions && Object.keys( subscriptions ).length === 0 ) { clearInterval( socketHeartbeatInterval ); } options.log( 'WebSocket closed: ' + this.endpoint + ( code ? ' (' + code + ')' : '' ) + ( reason ? ' ' + reason : '' ) ); if ( options.reconnect && this.reconnect && reconnect ) { if ( this.endpoint && parseInt( this.endpoint.length, 10 ) === 60 ) options.log( 'Account data WebSocket reconnecting...' ); else options.log( 'WebSocket reconnecting: ' + this.endpoint + '...' ); try { reconnect(); } catch ( error ) { options.log( 'WebSocket reconnect error: ' + error.message ); } } }; /** * Called when socket errors * @param {object} error - error object message * @return {undefined} */ const handleSocketError = function ( error ) { /* Errors ultimately result in a `close` event. see: https://github.com/websockets/ws/blob/828194044bf247af852b31c49e2800d557fedeff/lib/WebSocket.js#L126 */ options.log( 'WebSocket error: ' + this.endpoint + ( error.code ? ' (' + error.code + ')' : '' ) + ( error.message ? ' ' + error.message : '' ) ); }; /** * Called on each socket heartbeat * @return {undefined} */ const handleSocketHeartbeat = function () { this.isAlive = true; }; /** * Used to subscribe to a single websocket endpoint * @param {string} endpoint - endpoint to connect to * @param {function} callback - the function to call when information is received * @param {boolean} reconnect - whether to reconnect on disconnect * @param {object} opened_callback - the function to call when opened * @return {WebSocket} - websocket reference */ const subscribe = function ( endpoint, callback, reconnect = false, opened_callback = false ) { let ws = new WebSocket( stream + endpoint ); if ( options.verbose ) options.log( 'Subscribed to ' + endpoint ); ws.reconnect = options.reconnect; ws.endpoint = endpoint; ws.isAlive = false; ws.onopen = handleSocketOpen.bind( ws, opened_callback ); ws.onping = handleSocketHeartbeat; ws.onerror = handleSocketError; ws.onclose = handleSocketClose.bind( ws, reconnect ); ws.onmessage = event => { //try { callback( JSON.parse( event.data ) ); //} catch ( error ) { // options.log( 'Parse error: ' + error.message ); //} }; return ws; }; /** * Used to subscribe to a combined websocket endpoint * @param {string} streams - streams to connect to * @param {function} callback - the function to call when information is received * @param {boolean} reconnect - whether to reconnect on disconnect * @param {object} opened_callback - the function to call when opened * @return {WebSocket} - websocket reference */ const subscribeCombined = function ( streams, callback, reconnect = false, opened_callback = false ) { let queryParams = streams.join( '/' ), ws = new WebSocket( combineStream + queryParams ); ws.reconnect = options.reconnect; ws.endpoint = MD5.hex( queryParams ); ws.isAlive = false; if ( options.verbose ) options.log( 'CombinedStream: Subscribed to [' + ws.endpoint + '] ' + queryParams ); ws.onopen = handleSocketOpen.bind( ws, opened_callback ); ws.onping = handleSocketHeartbeat; ws.onerror = handleSocketError; ws.onclose = handleSocketClose.bind( ws, reconnect ); ws.onmessage = event => { try { callback( JSON.parse( event.data ).data ); } catch ( error ) { options.log( 'CombinedStream: Parse error: ' + error.message ); } }; return ws; }; /** * Used to terminate a web socket * @param {string} endpoint - endpoint identifier associated with the web socket * @param {boolean} reconnect - auto reconnect after termination * @return {undefined} */ const terminate = function ( endpoint, reconnect = false ) { let ws = subscriptions[endpoint]; if ( !ws ) return; ws.removeAllListeners( 'message' ); ws.reconnect = reconnect; ws.close(); } /** * Futures heartbeat code with a shared single interval tick * @return {undefined} */ const futuresSocketHeartbeat = () => { return; /* Sockets removed from subscriptions during a manual terminate() will no longer be at risk of having functions called on them */ for ( let endpointId in futuresSubscriptions ) { const ws = futuresSubscriptions[endpointId]; if ( ws.isAlive ) { ws.isAlive = false; // TODO: Fix heartbeat. Browser client can't send pings //if ( ws.readyState === openState ) ws.send( '{"ping": true}' ); } else { if ( options.verbose ) options.log( `Terminating zombie futures WebSocket: ${ ws.endpoint }` ); if ( ws.readyState === openState ) ws.close(); } } }; /** * Called when a futures socket is opened, subscriptions are registered for later reference * @param {function} openCallback - a callback function * @return {undefined} */ const handleFuturesSocketOpen = function ( openCallback ) { this.isAlive = true; if ( Object.keys( futuresSubscriptions ).length === 0 ) { socketHeartbeatInterval = setInterval( futuresSocketHeartbeat, 30000 ); } futuresSubscriptions[this.endpoint] = this; if ( typeof openCallback === 'function' ) openCallback( this.endpoint ); }; /** * Called when futures websocket is closed, subscriptions are de-registered for later reference * @param {boolean} reconnect - true or false to reconnect the socket * @param {string} code - code associated with the socket * @param {string} reason - string with the response * @return {undefined} */ const handleFuturesSocketClose = function ( reconnect, code, reason ) { delete futuresSubscriptions[this.endpoint]; if ( futuresSubscriptions && Object.keys( futuresSubscriptions ).length === 0 ) { clearInterval( socketHeartbeatInterval ); } options.log( 'Futures WebSocket closed: ' + this.endpoint + ( code ? ' (' + code + ')' : '' ) + ( reason ? ' ' + reason : '' ) ); if ( options.reconnect && this.reconnect && reconnect ) { if ( this.endpoint && parseInt( this.endpoint.length, 10 ) === 60 ) options.log( 'Futures account data WebSocket reconnecting...' ); else options.log( 'Futures WebSocket reconnecting: ' + this.endpoint + '...' ); try { reconnect(); } catch ( error ) { options.log( 'Futures WebSocket reconnect error: ' + error.message ); } } }; /** * Called when a futures websocket errors * @param {object} error - error object message * @return {undefined} */ const handleFuturesSocketError = function ( error ) { options.log( 'Futures WebSocket error: ' + this.endpoint + ( error.code ? ' (' + error.code + ')' : '' ) + ( error.message ? ' ' + error.message : '' ) ); }; /** * Called on each futures socket heartbeat * @return {undefined} */ const handleFuturesSocketHeartbeat = function () { this.isAlive = true; }; /** * Used to subscribe to a single futures websocket endpoint * @param {string} endpoint - endpoint to connect to * @param {function} callback - the function to call when information is received * @param {object} params - Optional reconnect {boolean} (whether to reconnect on disconnect), openCallback {function}, id {string} * @return {WebSocket} - websocket reference */ const futuresSubscribeSingle = function ( endpoint, callback, params = {} ) { if ( typeof params === 'boolean' ) params = { reconnect: params }; if ( !params.reconnect ) params.reconnect = false; if ( !params.openCallback ) params.openCallback = false; if ( !params.id ) params.id = false; let ws = new WebSocket( fstreamSingle + endpoint ); if ( options.verbose ) options.log( 'futuresSubscribeSingle: Subscribed to ' + endpoint ); ws.reconnect = options.reconnect; ws.endpoint = endpoint; ws.isAlive = false; ws.onopen = handleFuturesSocketOpen.bind( ws, params.openCallback ); ws.onping = handleFuturesSocketHeartbeat; ws.onerror = handleFuturesSocketError; ws.onclose = handleFuturesSocketClose.bind( ws, params.reconnect ); ws.onmessage = event => { callback( JSON.parse( event.data ) ); }; return ws; }; /** * Used to subscribe to a combined futures websocket endpoint * @param {string} streams - streams to connect to * @param {function} callback - the function to call when information is received * @param {object} params - Optional reconnect {boolean} (whether to reconnect on disconnect), openCallback {function}, id {string} * @return {WebSocket} - websocket reference */ const futuresSubscribe = function ( streams, callback, params = {} ) { if ( typeof streams === 'string' ) return futuresSubscribeSingle( streams, callback, params ); if ( typeof params === 'boolean' ) params = { reconnect: params }; if ( !params.reconnect ) params.reconnect = false; if ( !params.openCallback ) params.openCallback = false; if ( !params.id ) params.id = false; const queryParams = streams.join( '/' ); let ws = new WebSocket( fstream + queryParams ); ws.reconnect = options.reconnect; ws.endpoint = MD5.hex( queryParams ); ws.isAlive = false; if ( options.verbose ) options.log( `futuresSubscribe: Subscribed to [${ ws.endpoint }] ${ queryParams }` ); ws.onopen = handleFuturesSocketOpen.bind( ws, params.openCallback ); ws.onping = handleFuturesSocketHeartbeat; ws.onerror = handleFuturesSocketError; ws.onclose = handleFuturesSocketClose.bind( ws, params.reconnect ); ws.onmessage = event => { try { callback( JSON.parse( event.data ).data ); } catch ( error ) { options.log( `futuresSubscribe: Parse error: ${ error.message }` ); } }; return ws; }; /** * Used to terminate a futures websocket * @param {string} endpoint - endpoint identifier associated with the web socket * @param {boolean} reconnect - auto reconnect after termination * @return {undefined} */ const futuresTerminate = function ( endpoint, reconnect = false ) { let ws = futuresSubscriptions[endpoint]; if ( !ws ) return; ws.removeAllListeners( 'message' ); ws.reconnect = reconnect; ws.close(); } /** * Combines all futures OHLC data with the latest update * @param {string} symbol - the symbol * @param {string} interval - time interval * @return {array} interval data for given symbol */ const futuresKlineConcat = ( symbol, interval ) => { let output = futuresTicks[symbol][interval]; if ( typeof futuresRealtime[symbol][interval].time === 'undefined' ) return output; const time = futuresRealtime[symbol][interval].time; const last_updated = Object.keys( futuresTicks[symbol][interval] ).pop(); if ( time >= last_updated ) { output[time] = futuresRealtime[symbol][interval]; output[last_updated].isFinal = true; output[time].isFinal = false; } return output; }; /** * Used for websocket futures @kline * @param {string} symbol - the symbol * @param {object} kline - object with kline info * @param {string} firstTime - time filter * @return {undefined} */ const futuresKlineHandler = ( symbol, kline, firstTime = 0 ) => { // eslint-disable-next-line no-unused-vars let { e: eventType, E: eventTime, k: ticks } = kline; // eslint-disable-next-line no-unused-vars let { o: open, h: high, l: low, c: close, v: volume, i: interval, x: isFinal, q: quoteVolume, V: takerBuyBaseVolume, Q: takerBuyQuoteVolume, n: trades, t: time, T:closeTime } = ticks; if ( time <= firstTime ) return; if ( !isFinal ) return futuresRealtime[symbol][interval] = { time, closeTime, open, high, low, close, volume, quoteVolume, takerBuyBaseVolume, takerBuyQuoteVolume, trades, isFinal }; const first_updated = Object.keys( futuresTicks[symbol][interval] ).shift(); if ( first_updated ) delete futuresTicks[symbol][interval][first_updated]; futuresTicks[symbol][interval][time] = { time, closeTime, open, high, low, close, volume, quoteVolume, takerBuyBaseVolume, takerBuyQuoteVolume, trades, isFinal:false }; }; /** * Used by web sockets depth and populates OHLC and info * @param {string} symbol - symbol to get candlestick info * @param {string} interval - time interval, 1m, 3m, 5m .... * @param {array} ticks - tick array * @return {undefined} */ const klineData = ( symbol, interval, ticks ) => { // Used for /depth let last_time = 0; if ( isIterable( ticks ) ) { for ( let tick of ticks ) { // eslint-disable-next-line no-unused-vars let [ time, open, high, low, close, volume, closeTime, assetVolume, trades, buyBaseVolume, buyAssetVolume, ignored ] = tick; ohlc[symbol][interval][time] = { open: Number( open ), high: Number( high ), low: Number( low ), close: Number( close ), volume: Number( volume ), time: parseInt( time ) }; last_time = time; } info[symbol][interval].timestamp = last_time; } }; /** * Combines all OHLC data with latest update * @param {string} symbol - the symbol * @param {string} interval - time interval, 1m, 3m, 5m .... * @return {array} - interval data for given symbol */ const klineConcat = ( symbol, interval ) => { 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; }; /** * Used for websocket @kline * @param {string} symbol - the symbol * @param {object} kline - object with kline info * @param {string} firstTime - time filter * @return {undefined} */ const klineHandler = ( symbol, kline, firstTime = 0 ) => { // TODO: add Taker buy base asset volume // eslint-disable-next-line no-unused-vars let { e: eventType, E: eventTime, k: ticks } = kline; // eslint-disable-next-line no-unused-vars 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 }; }; /** * Used by futures websockets chart cache * @param {string} symbol - symbol to get candlestick info * @param {string} interval - time interval, 1m, 3m, 5m .... * @param {array} ticks - tick array * @return {undefined} */ const futuresKlineData = ( symbol, interval, ticks ) => { let last_time = 0; if ( options.verbose ) console.info( 'futuresKlineData', symbol, interval, ticks ); if ( isIterable( ticks ) ) { for ( let tick of ticks ) { // eslint-disable-next-line no-unused-vars let [ time, open, high, low, close, volume, closeTime, quoteVolume, trades, takerBuyBaseVolume, takerBuyQuoteVolume, ignored ] = tick; futuresTicks[symbol][interval][time] = { time, closeTime, open, high, low, close, volume, quoteVolume, takerBuyBaseVolume, takerBuyQuoteVolume, trades }; last_time = time; } futuresMeta[symbol][interval].timestamp = last_time; } }; /** * Used for /depth endpoint * @param {object} data - containing the bids and asks * @return {undefined} */ const depthData = data => { if ( !data ) return { bids: [], asks: [] }; let bids = {}, asks = {}, obj; if ( typeof data.bids !== 'undefined' ) { for ( obj of data.bids ) { bids[obj[0]] = parseFloat( obj[1] ); } } if ( typeof data.asks !== 'undefined' ) { for ( obj of data.asks ) { asks[obj[0]] = parseFloat( obj[1] ); } } return { lastUpdateId: data.lastUpdateId, bids: bids, asks: asks }; } /** * Used for /depth endpoint * @param {object} depth - information * @return {undefined} */ const depthHandler = depth => { let symbol = depth.s, obj; let context = depthCacheContext[symbol]; let updateDepthCache = () => { depthCache[symbol].eventTime = depth.E; for ( obj of depth.b ) { //bids if ( obj[1] === '0.00000000' ) { delete depthCache[symbol].bids[obj[0]]; } else { depthCache[symbol].bids[obj[0]] = parseFloat( obj[1] ); } } for ( obj of depth.a ) { //asks if ( obj[1] === '0.00000000' ) { delete depthCache[symbol].asks[obj[0]]; } else { depthCache[symbol].asks[obj[0]] = parseFloat( obj[1] ); } } context.skipCount = 0; context.lastEventUpdateId = depth.u; context.lastEventUpdateTime = depth.E; }; // This now conforms 100% to the Binance docs constraints on managing a local order book if ( context.lastEventUpdateId ) { const expectedUpdateId = context.lastEventUpdateId + 1; if ( depth.U <= expectedUpdateId ) { updateDepthCache(); } else { let msg = 'depthHandler: [' + symbol + '] The depth cache is out of sync.'; msg += ' Symptom: Unexpected Update ID. Expected "' + expectedUpdateId + '", got "' + depth.U + '"'; if ( options.verbose ) options.log( msg ); throw new Error( msg ); } } else if ( depth.U > context.snapshotUpdateId + 1 ) { /* In this case we have a gap between the data of the stream and the snapshot. This is an out of sync error, and the connection must be torn down and reconnected. */ let msg = 'depthHandler: [' + symbol + '] The depth cache is out of sync.'; msg += ' Symptom: Gap between snapshot and first stream data.'; if ( options.verbose ) options.log( msg ); throw new Error( msg ); } else if ( depth.u < context.snapshotUpdateId + 1 ) { /* In this case we've received data that we've already had since the snapshot. This isn't really an issue, and we can just update the cache again, or ignore it entirely. */ // do nothing } else { // This is our first legal update from the stream data updateDepthCache(); } }; /** * Gets depth cache for given symbol * @param {string} symbol - the symbol to fetch * @return {object} - the depth cache object */ const getDepthCache = symbol => { if ( typeof depthCache[symbol] === 'undefined' ) return { bids: {}, asks: {} }; return depthCache[symbol]; }; /** * Calculate Buy/Sell volume from DepthCache * @param {string} symbol - the symbol to fetch * @return {object} - the depth volume cache object */ const depthVolume = symbol => { 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 }; }; /** * Checks whether or not an array contains any duplicate elements * @param {array} array - the array to check * @return {boolean} - true or false */ const isArrayUnique = array => { let s = new Set( array ); return s.size === array.length; }; /** * Converts the futures liquidation stream data into a friendly object * @param {object} data - liquidation data callback data type * @return {object} - user friendly data type */ const fLiquidationConvertData = data => { let eventType = data.e, eventTime = data.E; let { s: symbol, S: side, o: orderType, f: timeInForce, q: origAmount, p: price, ap: avgPrice, X: orderStatus, l: lastFilledQty, z: totalFilledQty, T: tradeTime } = data.o; return { symbol, side, orderType, timeInForce, origAmount, price, avgPrice, orderStatus, lastFilledQty, totalFilledQty, eventType, tradeTime, eventTime }; }; /** * Converts the futures ticker stream data into a friendly object * @param {object} data - user data callback data type * @return {object} - user friendly data type */ const fTickerConvertData = data => { let friendlyData = data => { let { e: eventType, E: eventTime, s: symbol, p: priceChange, P: percentChange, w: averagePrice, c: close, Q: closeQty, o: open, h: high, l: low, v: volume, q: quoteVolume, O: openTime, C: closeTime, F: firstTradeId, L: lastTradeId, n: numTrades } = data; return { eventType, eventTime, symbol, priceChange, percentChange, averagePrice, close, closeQty, open, high, low, volume, quoteVolume, openTime, closeTime, firstTradeId, lastTradeId, numTrades }; } if ( Array.isArray( data ) ) { const result = []; for ( let obj of data ) { result.push( friendlyData( obj ) ); } return result; } return friendlyData( data ); } /** * Converts the futures miniTicker stream data into a friendly object * @param {object} data - user data callback data type * @return {object} - user friendly data type */ const fMiniTickerConvertData = data => { let friendlyData = data => { let { e: eventType, E: eventTime, s: symbol, c: close, o: open, h: high, l: low, v: volume, q: quoteVolume } = data; return { eventType, eventTime, symbol, close, open, high, low, volume, quoteVolume }; } if ( Array.isArray( data ) ) { const result = []; for ( let obj of data ) { result.push( friendlyData( obj ) ); } return result; } return friendlyData( data ); } /** * Converts the futures bookTicker stream data into a friendly object * @param {object} data - user data callback data type * @return {object} - user friendly data type */ const fBookTickerConvertData = data => { let { u: updateId, s: symbol, b: bestBid, B: bestBidQty, a: bestAsk, A: bestAskQty } = data; return { updateId, symbol, bestBid, bestBidQty, bestAsk, bestAskQty }; } /** * Converts the futures markPrice stream data into a friendly object * @param {object} data - user data callback data type * @return {object} - user friendly data type */ const fMarkPriceConvertData = data => { let friendlyData = data => { let { e: eventType, E: eventTime, s: symbol, p: markPrice, r: fundingRate, T: fundingTime } = data; return { eventType, eventTime, symbol, markPrice, fundingRate, fundingTime }; } if ( Array.isArray( data ) ) { const result = []; for ( let obj of data ) { result.push( friendlyData( obj ) ); } return result; } return friendlyData( data ); } /** * Converts the futures aggTrade stream data into a friendly object * @param {object} data - user data callback data type * @return {object} - user friendly data type */ const fAggTradeConvertData = data => { let friendlyData = data => { let { e: eventType, E: eventTime, s: symbol, a: aggTradeId, p: price, q: amount, f: firstTradeId, l: lastTradeId, T: timestamp, m: maker } = data; return { eventType, eventTime, symbol, aggTradeId, price, amount, total: price * amount, firstTradeId, lastTradeId, timestamp, maker }; } if ( Array.isArray( data ) ) { const result = []; for ( let obj of data ) { result.push( friendlyData( obj ) ); } return result; } return friendlyData( data ); } window.binance = { /** * Futures WebSocket aggregated trades * @param {array/string} symbols - an array or string of symbols to query * @param {function} callback - callback function * @return {string} the websocket endpoint */ futuresAggTradeStream: function futuresAggTradeStream( symbols, callback ) { let reconnect = () => { if ( options.reconnect ) futuresAggTradeStream( symbols, callback ) }; let subscription, cleanCallback = data => callback( fAggTradeConvertData( data ) ); if ( Array.isArray( symbols ) ) { if ( !isArrayUnique( symbols ) ) throw Error( 'futuresAggTradeStream: "symbols" cannot contain duplicate elements.' ); let streams = symbols.map( symbol => symbol.toLowerCase() + '@aggTrade' ); subscription = futuresSubscribe( streams, cleanCallback, { reconnect } ); } else { let symbol = symbols; subscription = futuresSubscribeSingle( symbol.toLowerCase() + '@aggTrade', cleanCallback, { reconnect } ); } return subscription.endpoint; }, /** * Futures WebSocket mark price * @param {symbol} symbol name or false. can also be a callback * @param {function} callback - callback function * @param {string} speed - 1 second updates. leave blank for default 3 seconds * @return {string} the websocket endpoint */ futuresMarkPriceStream: function fMarkPriceStream( symbol = false, callback = console.log, speed = '@1s' ) { if ( typeof symbol == 'function' ) { callback = symbol; symbol = false; } let reconnect = () => { if ( options.reconnect ) fMarkPriceStream( symbol, callback ); }; const endpoint = symbol ? `${ symbol.toLowerCase() }@markPrice` : '!markPrice@arr' let subscription = futuresSubscribeSingle( endpoint + speed, data => callback( fMarkPriceConvertData( data ) ), { reconnect } ); return subscription.endpoint; }, /** * Futures WebSocket liquidations stream * @param {symbol} symbol name or false. can also be a callback * @param {function} callback - callback function * @return {string} the websocket endpoint */ futuresLiquidationStream: function fLiquidationStream( symbol = false, callback = console.log ) { if ( typeof symbol == 'function' ) { callback = symbol; symbol = false; } let reconnect = () => { if ( options.reconnect ) fLiquidationStream( symbol, callback ); }; const endpoint = symbol ? `${ symbol.toLowerCase() }@forceOrder` : '!forceOrder@arr' let subscription = futuresSubscribeSingle( endpoint, data => callback( fLiquidationConvertData( data ) ), { reconnect } ); return subscription.endpoint; }, /** * Futures WebSocket prevDay ticker * @param {symbol} symbol name or false. can also be a callback * @param {function} callback - callback function * @return {string} the websocket endpoint */ futuresTickerStream: function fTickerStream( symbol = false, callback = console.log ) { if ( typeof symbol == 'function' ) { callback = symbol; symbol = false; } let reconnect = () => { if ( options.reconnect ) fTickerStream( symbol, callback ); }; const endpoint = symbol ? `${ symbol.toLowerCase() }@ticker` : '!ticker@arr' let subscription = futuresSubscribeSingle( endpoint, data => callback( fTickerConvertData( data ) ), { reconnect } ); return subscription.endpoint; }, /** * Futures WebSocket miniTicker * @param {symbol} symbol name or false. can also be a callback * @param {function} callback - callback function * @return {string} the websocket endpoint */ futuresMiniTickerStream: function fMiniTickerStream( symbol = false, callback = console.log ) { if ( typeof symbol == 'function' ) { callback = symbol; symbol = false; } let reconnect = () => { if ( options.reconnect ) fMiniTickerStream( symbol, callback ); }; const endpoint = symbol ? `${ symbol.toLowerCase() }@miniTicker` : '!miniTicker@arr' let subscription = futuresSubscribeSingle( endpoint, data => callback( fMiniTickerConvertData( data ) ), { reconnect } ); return subscription.endpoint; }, /** * Futures WebSocket bookTicker * @param {symbol} symbol name or false. can also be a callback * @param {function} callback - callback function * @return {string} the websocket endpoint */ futuresBookTickerStream: function fBookTickerStream( symbol = false, callback = console.log ) { if ( typeof symbol == 'function' ) { callback = symbol; symbol = false; } let reconnect = () => { if ( options.reconnect ) fBookTickerStream( symbol, callback ); }; const endpoint = symbol ? `${ symbol.toLowerCase() }@bookTicker` : '!bookTicker' let subscription = futuresSubscribeSingle( endpoint, data => callback( fBookTickerConvertData( data ) ), { reconnect } ); return subscription.endpoint; }, /** * Websocket futures klines * @param {array/string} symbols - an array or string of symbols to query * @param {string} interval - the time interval * @param {function} callback - callback function * @param {int} limit - maximum results, no more than 1000 * @return {string} the websocket endpoint */ futuresChart: async function futuresChart( symbols, interval, callback, limit = 500 ) { let reconnect = () => { if ( options.reconnect ) futuresChart( symbols, interval, callback, limit ); }; let futuresChartInit = symbol => { if ( typeof futuresMeta[symbol] === 'undefined' ) futuresMeta[symbol] = {}; if ( typeof futuresMeta[symbol][interval] === 'undefined' ) futuresMeta[symbol][interval] = {}; if ( typeof futuresTicks[symbol] === 'undefined' ) futuresTicks[symbol] = {}; if ( typeof futuresTicks[symbol][interval] === 'undefined' ) futuresTicks[symbol][interval] = {}; if ( typeof futuresRealtime[symbol] === 'undefined' ) futuresRealtime[symbol] = {}; if ( typeof futuresRealtime[symbol][interval] === 'undefined' ) futuresRealtime[symbol][interval] = {}; if ( typeof futuresKlineQueue[symbol] === 'undefined' ) futuresKlineQueue[symbol] = {}; if ( typeof futuresKlineQueue[symbol][interval] === 'undefined' ) futuresKlineQueue[symbol][interval] = []; futuresMeta[symbol][interval].timestamp = 0; } /* let handleKlineStreamData = kline => { let symbol = kline.s; if ( !info[symbol] || !info[symbol][interval].timestamp ) { console.warn( `${ symbol } no info`, info[symbol], info[symbol][interval] ); if ( kline !== null ) { //typeof klineQueue[symbol] !== 'undefined' && typeof klineQueue[symbol][interval] !== 'undefined' && klineQueue[symbol][interval].push( kline ); } } else { if ( options.verbose ) options.log( 'spot @klines at ' + kline.k.t ); klineHandler( symbol, kline ); if ( callback ) callback( symbol, interval, klineConcat( symbol, interval ) ); } }; */ let handleFuturesKlineStream = kline => { if ( !kline ) return console.error( `handleFuturesKlineStream: kline error`, kline ); let symbol = kline.s, interval = kline.k.i; if ( !futuresMeta[symbol] || !futuresMeta[symbol][interval].timestamp ) { if ( typeof ( futuresKlineQueue[symbol][interval] ) !== 'undefined' && kline !== null ) { futuresKlineQueue[symbol][interval].push( kline ); } } else { //options.log('futures klines at ' + kline.k.t); futuresKlineHandler( symbol, kline ); if ( callback ) callback( symbol, interval, futuresKlineConcat( symbol, interval ) ); } }; /* getSymbolKlineSnapshot publicRequest( base + 'v3/klines', { symbol, interval, limit }, function ( data ) { klineData( symbol, interval, data ); //options.log('/klines at ' + info[symbol][interval].timestamp); if ( typeof klineQueue[symbol][interval] !== 'undefined' ) { 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 ) ); } ); */ let getFuturesKlineSnapshot = async ( symbol, limit = 500 ) => { let data = await promiseRequest( 'v1/klines', { symbol, interval, limit }, { base:fapi } ); if ( options.verbose ) console.info( 'getFuturesKlineSnapshot', symbol, limit, data ); futuresKlineData( symbol, interval, data ); //options.log('/futures klines at ' + futuresMeta[symbol][interval].timestamp); if ( typeof futuresKlineQueue[symbol][interval] !== 'undefined' ) { for ( let kline of futuresKlineQueue[symbol][interval] ) futuresKlineHandler( symbol, kline, futuresMeta[symbol][interval].timestamp ); delete futuresKlineQueue[symbol][interval]; } if ( callback ) callback( symbol, interval, futuresKlineConcat( symbol, interval ) ); }; let subscription; if ( Array.isArray( symbols ) ) { if ( !isArrayUnique( symbols ) ) throw Error( 'futuresChart: "symbols" array cannot contain duplicate elements.' ); symbols.forEach( futuresChartInit ); let streams = symbols.map( symbol => `${ symbol.toLowerCase() }@kline_${ interval }` ); subscription = futuresSubscribe( streams, handleFuturesKlineStream, reconnect ); symbols.forEach( element => getFuturesKlineSnapshot( element, limit ) ); } else { let symbol = symbols; futuresChartInit( symbol ); subscription = futuresSubscribeSingle( symbol.toLowerCase() + '@kline_' + interval, handleFuturesKlineStream, reconnect ); getFuturesKlineSnapshot( symbol, limit ); } return subscription.endpoint; }, /** * Websocket futures candlesticks * @param {array/string} symbols - an array or string of symbols to query * @param {string} interval - the time interval * @param {function} callback - callback function * @return {string} the websocket endpoint */ futuresCandlesticks: function futuresCandlesticks( symbols, interval, callback ) { let reconnect = () => { if ( options.reconnect ) futuresCandlesticks( symbols, interval, callback ); }; let subscription; if ( Array.isArray( symbols ) ) { if ( !isArrayUnique( symbols ) ) throw Error( 'futuresCandlesticks: "symbols" array cannot contain duplicate elements.' ); let streams = symbols.map( symbol => symbol.toLowerCase() + '@kline_' + interval ); subscription = futuresSubscribe( streams, callback, { reconnect } ); } else { let symbol = symbols.toLowerCase(); subscription = futuresSubscribeSingle( symbol + '@kline_' + interval, callback, { reconnect } ); } return subscription.endpoint; }, /** * Subscribe to a generic websocket * @param {string} url - the websocket endpoint * @param {function} callback - optional execution callback * @param {boolean} reconnect - subscription callback * @return {WebSocket} the websocket reference */ subscribe: function ( url, callback, reconnect = false ) { return subscribe( url, call