node-binance-api
Version:
Binance API for node https://github.com/jaggedsoft/node-binance-api
1,086 lines (1,036 loc) • 259 kB
JavaScript
/* ============================================================
* node-binance-api
* https://github.com/jaggedsoft/node-binance-api
* ============================================================
* Copyright 2017-, Jon Eyrick
* Released under the MIT License
* ============================================================
* @module jaggedsoft/node-binance-api
* @return {object} instance to class object */
let api = function Binance( options = {} ) {
if ( !new.target ) return new api( options ); // Legacy support for calling the constructor without 'new'
let Binance = this; // eslint-disable-line consistent-this
const WebSocket = require( 'ws' );
const request = require( 'request' );
const crypto = require( 'crypto' );
const file = require( 'fs' );
const url = require( 'url' );
const JSONbig = require( 'json-bigint' );
const HttpsProxyAgent = require( 'https-proxy-agent' );
const SocksProxyAgent = require( 'socks-proxy-agent' );
const stringHash = require( 'string-hash' );
const async = require( 'async' );
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 dapi = 'https://dapi.binance.com/dapi/';
let fapiTest = 'https://testnet.binancefuture.com/fapi/';
let dapiTest = 'https://testnet.binancefuture.com/dapi/';
let fstream = 'wss://fstream.binance.com/stream?streams=';
let fstreamSingle = 'wss://fstream.binance.com/ws/';
let fstreamSingleTest = 'wss://stream.binancefuture.com/ws/';
let fstreamTest = 'wss://stream.binancefuture.com/stream?streams=';
let dstream = 'wss://dstream.binance.com/stream?streams=';
let dstreamSingle = 'wss://dstream.binance.com/ws/';
let dstreamSingleTest = 'wss://dstream.binancefuture.com/ws/';
let dstreamTest = 'wss://dstream.binancefuture.com/stream?streams=';
let stream = 'wss://stream.binance.com:9443/ws/';
let combineStream = 'wss://stream.binance.com:9443/stream?streams=';
const userAgent = 'Mozilla/4.0 (compatible; Node Binance API)';
const contentType = 'application/x-www-form-urlencoded';
Binance.subscriptions = {};
Binance.futuresSubscriptions = {};
Binance.futuresInfo = {};
Binance.futuresMeta = {};
Binance.futuresTicks = {};
Binance.futuresRealtime = {};
Binance.futuresKlineQueue = {};
Binance.deliverySubscriptions = {};
Binance.deliveryInfo = {};
Binance.deliveryMeta = {};
Binance.deliveryTicks = {};
Binance.deliveryRealtime = {};
Binance.deliveryKlineQueue = {};
Binance.depthCache = {};
Binance.depthCacheContext = {};
Binance.ohlcLatest = {};
Binance.klineQueue = {};
Binance.ohlc = {};
const default_options = {
recvWindow: 5000,
useServerTime: false,
reconnect: true,
keepAlive: true,
verbose: false,
test: false,
hedgeMode: false,
localAddress: false,
family: false,
log: function ( ...args ) {
console.log( Array.prototype.slice.call( args ) );
}
};
Binance.options = default_options;
Binance.info = {
usedWeight: 0,
futuresLatency: false,
lastRequest: false,
lastURL: false,
statusCode: 0,
orderCount1s: 0,
orderCount1m: 0,
orderCount1h: 0,
orderCount1d: 0,
timeOffset: 0
};
Binance.socketHeartbeatInterval = null;
if ( options ) setOptions( options );
function setOptions( opt = {}, callback = false ) {
if ( typeof opt === 'string' ) { // Pass json config filename
Binance.options = JSON.parse( file.readFileSync( opt ) );
} else Binance.options = opt;
if ( typeof Binance.options.recvWindow === 'undefined' ) Binance.options.recvWindow = default_options.recvWindow;
if ( typeof Binance.options.useServerTime === 'undefined' ) Binance.options.useServerTime = default_options.useServerTime;
if ( typeof Binance.options.reconnect === 'undefined' ) Binance.options.reconnect = default_options.reconnect;
if ( typeof Binance.options.test === 'undefined' ) Binance.options.test = default_options.test;
if ( typeof Binance.options.hedgeMode === 'undefined' ) Binance.options.hedgeMode = default_options.hedgeMode;
if ( typeof Binance.options.log === 'undefined' ) Binance.options.log = default_options.log;
if ( typeof Binance.options.verbose === 'undefined' ) Binance.options.verbose = default_options.verbose;
if ( typeof Binance.options.keepAlive === 'undefined' ) Binance.options.keepAlive = default_options.keepAlive;
if ( typeof Binance.options.localAddress === 'undefined' ) Binance.options.localAddress = default_options.localAddress;
if ( typeof Binance.options.family === 'undefined' ) Binance.options.family = default_options.family;
if ( typeof Binance.options.urls !== 'undefined' ) {
const { urls } = Binance.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 ( typeof urls.fstreamTest === 'string' ) fstreamTest = urls.fstreamTest;
if ( typeof urls.fstreamSingleTest === 'string' ) fstreamSingleTest = urls.fstreamSingleTest;
if ( typeof urls.dstream === 'string' ) dstream = urls.dstream;
if ( typeof urls.dstreamSingle === 'string' ) dstreamSingle = urls.dstreamSingle;
if ( typeof urls.dstreamTest === 'string' ) dstreamTest = urls.dstreamTest;
if ( typeof urls.dstreamSingleTest === 'string' ) dstreamSingleTest = urls.dstreamSingleTest;
}
if ( Binance.options.useServerTime ) {
publicRequest( base + 'v3/time', {}, function ( error, response ) {
Binance.info.timeOffset = response.serverTime - new Date().getTime();
//Binance.options.log("server time set: ", response.serverTime, Binance.info.timeOffset);
if ( callback ) callback();
} );
} else if ( callback ) callback();
return this;
}
/**
* Replaces socks connection uri hostname with IP address
* @param {string} connString - socks connection string
* @return {string} modified string with ip address
*/
const proxyReplacewithIp = connString => {
return connString;
}
/**
* Returns an array in the form of [host, port]
* @param {string} connString - connection string
* @return {array} array of host and port
*/
const parseProxy = connString => {
let arr = connString.split( '/' );
let host = arr[2].split( ':' )[0];
let port = arr[2].split( ':' )[1];
return [ arr[0], host, port ];
}
/**
* 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 === null ) return false;
return typeof obj[Symbol.iterator] === 'function';
}
const addProxy = opt => {
if ( Binance.options.proxy ) {
const proxyauth = Binance.options.proxy.auth ? `${ Binance.options.proxy.auth.username }:${ Binance.options.proxy.auth.password }@` : '';
opt.proxy = `http://${ proxyauth }${ Binance.options.proxy.host }:${ Binance.options.proxy.port }`;
}
return opt;
}
const reqHandler = cb => ( error, response, body ) => {
Binance.info.lastRequest = new Date().getTime();
if ( response ) {
Binance.info.statusCode = response.statusCode || 0;
if ( response.request ) Binance.info.lastURL = response.request.uri.href;
if ( response.headers ) {
Binance.info.usedWeight = response.headers['x-mbx-used-weight-1m'] || 0;
Binance.info.orderCount1s = response.headers['x-mbx-order-count-1s'] || 0;
Binance.info.orderCount1m = response.headers['x-mbx-order-count-1m'] || 0;
Binance.info.orderCount1h = response.headers['x-mbx-order-count-1h'] || 0;
Binance.info.orderCount1d = response.headers['x-mbx-order-count-1d'] || 0;
}
}
if ( !cb ) return;
if ( error ) return cb( error, {} );
if ( response && response.statusCode !== 200 ) return cb( response, {} );
return cb( null, JSONbig.parse( body ) );
}
const proxyRequest = ( opt, cb ) => {
const req = request( addProxy( opt ), reqHandler( cb ) ).on('error', (err) => { cb( err, {} ) });
return req;
}
const reqObj = ( url, data = {}, method = 'GET', key ) => ( {
url: url,
qs: data,
method: method,
family: Binance.options.family,
localAddress: Binance.options.localAddress,
timeout: Binance.options.recvWindow,
forever: Binance.options.keepAlive,
headers: {
'User-Agent': userAgent,
'Content-type': contentType,
'X-MBX-APIKEY': key || ''
}
} )
const reqObjPOST = ( url, data = {}, method = 'POST', key ) => ( {
url: url,
form: data,
method: method,
family: Binance.options.family,
localAddress: Binance.options.localAddress,
timeout: Binance.options.recvWindow,
forever: Binance.options.keepAlive,
qsStringifyOptions: {
arrayFormat: 'repeat'
},
headers: {
'User-Agent': userAgent,
'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' ) => {
let opt = reqObj( url, data, method );
proxyRequest( opt, callback );
};
// XXX: This one works with array (e.g. for dust.transfer)
// XXX: I _guess_ we could use replace this function with the `qs` module
const makeQueryString = q =>
Object.keys( q )
.reduce( ( a, k ) => {
if ( Array.isArray( q[k] ) ) {
q[k].forEach( v => {
a.push( k + "=" + encodeURIComponent( v ) )
} )
} else 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' ) => {
requireApiKey( 'apiRequest' );
let opt = reqObj(
url,
data,
method,
Binance.options.APIKEY
);
proxyRequest( opt, callback );
};
// Check if API key is empty or invalid
const requireApiKey = function( source = 'requireApiKey', fatalError = true ) {
if ( !Binance.options.APIKEY ) {
if ( fatalError ) throw Error( `${ source }: Invalid API Key!` );
return false;
}
return true;
}
// Check if API secret is present
const requireApiSecret = function( source = 'requireApiSecret', fatalError = true ) {
if ( !Binance.options.APIKEY ) {
if ( fatalError ) throw Error( `${ source }: Invalid API Key!` );
return false;
}
if ( !Binance.options.APISECRET ) {
if ( fatalError ) throw Error( `${ source }: Invalid API Secret!` );
return false;
}
return true;
}
/**
* Make market request
* @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 marketRequest = ( url, data = {}, callback, method = 'GET' ) => {
requireApiKey( 'marketRequest' );
let query = makeQueryString( data );
let opt = reqObj(
url + ( query ? '?' + query : '' ),
data,
method,
Binance.options.APIKEY
);
proxyRequest( opt, callback );
};
/**
* Create a signed http request
* @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
* @param {boolean} noDataInSignature - Prevents data from being added to signature
* @return {undefined}
*/
const signedRequest = ( url, data = {}, callback, method = 'GET', noDataInSignature = false ) => {
requireApiSecret( 'signedRequest' );
data.timestamp = new Date().getTime() + Binance.info.timeOffset;
if ( typeof data.recvWindow === 'undefined' ) data.recvWindow = Binance.options.recvWindow;
let query = method === 'POST' && noDataInSignature ? '' : makeQueryString( data );
let signature = crypto.createHmac( 'sha256', Binance.options.APISECRET ).update( query ).digest( 'hex' ); // set the HMAC hash header
if ( method === 'POST' ) {
let opt = reqObjPOST(
url,
data,
method,
Binance.options.APIKEY
);
opt.form.signature = signature;
proxyRequest( opt, callback );
} else {
let opt = reqObj(
url + '?' + query + '&signature=' + signature,
data,
method,
Binance.options.APIKEY
);
proxyRequest( opt, callback );
}
};
/**
* Create a signed spot order
* @param {string} side - BUY or SELL
* @param {string} symbol - The symbol to buy or sell
* @param {string} quantity - The quantity to buy or sell
* @param {string} price - The price per unit to transact each unit at
* @param {object} flags - additional order settings
* @param {function} callback - the callback function
* @return {undefined}
*/
const order = ( side, symbol, quantity, price, flags = {}, callback = false ) => {
let endpoint = flags.type === 'OCO' ? 'v3/order/oco' : 'v3/order';
if ( Binance.options.test ) endpoint += '/test';
let opt = {
symbol: symbol,
side: side,
type: 'LIMIT',
quantity: quantity
};
if ( typeof flags.type !== 'undefined' ) opt.type = flags.type;
if ( opt.type.includes( 'LIMIT' ) ) {
opt.price = price;
if ( opt.type !== 'LIMIT_MAKER' ) {
opt.timeInForce = 'GTC';
}
}
if ( opt.type === 'OCO' ) {
opt.price = price;
opt.stopLimitPrice = flags.stopLimitPrice;
opt.stopLimitTimeInForce = 'GTC';
delete opt.type;
if ( typeof flags.listClientOrderId !== 'undefined' ) opt.listClientOrderId = flags.listClientOrderId;
if ( typeof flags.limitClientOrderId !== 'undefined' ) opt.limitClientOrderId = flags.limitClientOrderId;
if ( typeof flags.stopClientOrderId !== 'undefined' ) opt.stopClientOrderId = flags.stopClientOrderId;
}
if ( typeof flags.timeInForce !== 'undefined' ) opt.timeInForce = flags.timeInForce;
if ( typeof flags.newOrderRespType !== 'undefined' ) opt.newOrderRespType = flags.newOrderRespType;
if ( typeof flags.newClientOrderId !== 'undefined' ) opt.newClientOrderId = flags.newClientOrderId;
/*
* STOP_LOSS
* STOP_LOSS_LIMIT
* TAKE_PROFIT
* TAKE_PROFIT_LIMIT
* LIMIT_MAKER
*/
if ( typeof flags.icebergQty !== 'undefined' ) opt.icebergQty = flags.icebergQty;
if ( typeof flags.stopPrice !== 'undefined' ) {
opt.stopPrice = flags.stopPrice;
if ( opt.type === 'LIMIT' ) throw Error( 'stopPrice: Must set "type" to one of the following: STOP_LOSS, STOP_LOSS_LIMIT, TAKE_PROFIT, TAKE_PROFIT_LIMIT' );
}
signedRequest( base + endpoint, opt, ( error, response ) => {
if ( !response ) {
if ( callback ) callback( error, response );
else Binance.options.log( 'Order() error:', error );
return;
}
if ( typeof response.msg !== 'undefined' && response.msg === 'Filter failure: MIN_NOTIONAL' ) {
Binance.options.log( 'Order quantity too small. See exchangeInfo() for minimum amounts' );
}
if ( callback ) callback( error, response );
else Binance.options.log( side + '(' + symbol + ',' + quantity + ',' + price + ') ', response );
}, 'POST' );
};
/**
* Create a signed margin order
* @param {string} side - BUY or SELL
* @param {string} symbol - The symbol to buy or sell
* @param {string} quantity - The quantity to buy or sell
* @param {string} price - The price per unit to transact each unit at
* @param {object} flags - additional order settings
* @param {function} callback - the callback function
* @return {undefined}
*/
const marginOrder = ( side, symbol, quantity, price, flags = {}, callback = false ) => {
let endpoint = 'v1/margin/order';
if ( Binance.options.test ) endpoint += '/test';
let opt = {
symbol: symbol,
side: side,
type: 'LIMIT',
quantity: quantity
};
if ( typeof flags.type !== 'undefined' ) opt.type = flags.type;
if (typeof flags.isIsolated !== 'undefined') opt.isIsolated = flags.isIsolated;
if ( opt.type.includes( 'LIMIT' ) ) {
opt.price = price;
if ( opt.type !== 'LIMIT_MAKER' ) {
opt.timeInForce = 'GTC';
}
}
if ( typeof flags.timeInForce !== 'undefined' ) opt.timeInForce = flags.timeInForce;
if ( typeof flags.newOrderRespType !== 'undefined' ) opt.newOrderRespType = flags.newOrderRespType;
if ( typeof flags.newClientOrderId !== 'undefined' ) opt.newClientOrderId = flags.newClientOrderId;
if ( typeof flags.sideEffectType !== 'undefined' ) opt.sideEffectType = flags.sideEffectType;
/*
* STOP_LOSS
* STOP_LOSS_LIMIT
* TAKE_PROFIT
* TAKE_PROFIT_LIMIT
*/
if ( typeof flags.icebergQty !== 'undefined' ) opt.icebergQty = flags.icebergQty;
if ( typeof flags.stopPrice !== 'undefined' ) {
opt.stopPrice = flags.stopPrice;
if ( opt.type === 'LIMIT' ) throw Error( 'stopPrice: Must set "type" to one of the following: STOP_LOSS, STOP_LOSS_LIMIT, TAKE_PROFIT, TAKE_PROFIT_LIMIT' );
}
signedRequest( sapi + endpoint, opt, function ( error, response ) {
if ( !response ) {
if ( callback ) callback( error, response );
else Binance.options.log( 'Order() error:', error );
return;
}
if ( typeof response.msg !== 'undefined' && response.msg === 'Filter failure: MIN_NOTIONAL' ) {
Binance.options.log( 'Order quantity too small. See exchangeInfo() for minimum amounts' );
}
if ( callback ) callback( error, response );
else Binance.options.log( side + '(' + symbol + ',' + quantity + ',' + price + ') ', response );
}, 'POST' );
};
// Futures internal functions
const futuresOrder = async ( side, symbol, quantity, price = false, params = {} ) => {
params.symbol = symbol;
params.side = side;
if ( quantity ) params.quantity = quantity;
// if in the binance futures setting Hedged mode is active, positionSide parameter is mandatory
if( typeof params.positionSide === 'undefined' && Binance.options.hedgeMode ){
params.positionSide = side === 'BUY' ? 'LONG' : 'SHORT';
}
// LIMIT STOP MARKET STOP_MARKET TAKE_PROFIT TAKE_PROFIT_MARKET
// reduceOnly stopPrice
if ( price ) {
params.price = price;
if ( typeof params.type === 'undefined' ) params.type = 'LIMIT';
} else {
if ( typeof params.type === 'undefined' ) params.type = 'MARKET';
}
if ( !params.timeInForce && ( params.type.includes( 'LIMIT' ) || params.type === 'STOP' || params.type === 'TAKE_PROFIT' ) ) {
params.timeInForce = 'GTX'; // Post only by default. Use GTC for limit orders.
}
return promiseRequest( 'v1/order', params, { base:fapi, type:'TRADE', method:'POST' } );
};
const deliveryOrder = async ( side, symbol, quantity, price = false, params = {} ) => {
params.symbol = symbol;
params.side = side;
params.quantity = quantity;
// if in the binance futures setting Hedged mode is active, positionSide parameter is mandatory
if( Binance.options.hedgeMode ){
params.positionSide = side === 'BUY' ? 'LONG' : 'SHORT';
}
// LIMIT STOP MARKET STOP_MARKET TAKE_PROFIT TAKE_PROFIT_MARKET
// reduceOnly stopPrice
if ( price ) {
params.price = price;
if ( typeof params.type === 'undefined' ) params.type = 'LIMIT';
} else {
if ( typeof params.type === 'undefined' ) params.type = 'MARKET';
}
if ( !params.timeInForce && ( params.type.includes( 'LIMIT' ) || params.type === 'STOP' || params.type === 'TAKE_PROFIT' ) ) {
params.timeInForce = 'GTX'; // Post only by default. Use GTC for limit orders.
}
return promiseRequest( 'v1/order', params, { base:dapi, type:'TRADE', method:'POST' } );
};
const promiseRequest = async ( url, data = {}, flags = {} ) => {
return new Promise( ( resolve, reject ) => {
let query = '', headers = {
'User-Agent': userAgent,
'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 = Binance.options.recvWindow;
requireApiKey( 'promiseRequest' );
headers['X-MBX-APIKEY'] = Binance.options.APIKEY;
}
let baseURL = typeof flags.base === 'undefined' ? base : flags.base;
if ( Binance.options.test && baseURL === fapi ) baseURL = fapiTest;
if ( Binance.options.test && baseURL === dapi ) baseURL = dapiTest;
let opt = {
headers,
url: baseURL + url,
method: flags.method,
timeout: Binance.options.recvWindow,
followAllRedirects: true
};
if ( flags.type === 'SIGNED' || flags.type === 'TRADE' || flags.type === 'USER_DATA' ) {
if ( !requireApiSecret( 'promiseRequest' ) ) return reject( 'promiseRequest: Invalid API Secret!' );
data.timestamp = new Date().getTime() + Binance.info.timeOffset;
query = makeQueryString( data );
data.signature = crypto.createHmac( 'sha256', Binance.options.APISECRET ).update( query ).digest( 'hex' ); // HMAC hash header
opt.url = `${ baseURL }${ url }?${ query }&signature=${ data.signature }`;
}
opt.qs = data;
/*if ( flags.method === 'POST' ) {
opt.form = data;
} else {
opt.qs = data;
}*/
try {
request( addProxy( opt ), ( error, response, body ) => {
if ( error ) return reject( error );
try {
Binance.info.lastRequest = new Date().getTime();
if ( response ) {
Binance.info.statusCode = response.statusCode || 0;
if ( response.request ) Binance.info.lastURL = response.request.uri.href;
if ( response.headers ) {
Binance.info.usedWeight = response.headers['x-mbx-used-weight-1m'] || 0;
Binance.info.futuresLatency = response.headers['x-response-time'] || 0;
}
}
if ( !error && response.statusCode == 200 ) return resolve( JSONbig.parse( body ) );
if ( typeof response.body !== 'undefined' ) {
return resolve( JSONbig.parse( response.body ) );
}
return reject( response );
} catch ( err ) {
return reject( `promiseRequest error #${ response.statusCode }` );
}
} ).on( 'error', reject );
} catch ( err ) {
return reject( err );
}
} );
};
/**
* No-operation function
* @return {undefined}
*/
const noop = () => { }; // Do nothing.
/**
* Reworked Tuitio's heartbeat code into a shared single interval tick
* @return {undefined}
*/
const socketHeartbeat = () => {
/* Sockets removed from `subscriptions` during a manual terminate()
will no longer be at risk of having functions called on them */
for ( let endpointId in Binance.subscriptions ) {
const ws = Binance.subscriptions[endpointId];
if ( ws.isAlive ) {
ws.isAlive = false;
if ( ws.readyState === WebSocket.OPEN ) ws.ping( noop );
} else {
if ( Binance.options.verbose ) Binance.options.log( 'Terminating inactive/broken WebSocket: ' + ws.endpoint );
if ( ws.readyState === WebSocket.OPEN ) ws.terminate();
}
}
};
/**
* 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( Binance.subscriptions ).length === 0 ) {
Binance.socketHeartbeatInterval = setInterval( socketHeartbeat, 30000 );
}
Binance.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 Binance.subscriptions[this.endpoint];
if ( Binance.subscriptions && Object.keys( Binance.subscriptions ).length === 0 ) {
clearInterval( Binance.socketHeartbeatInterval );
}
Binance.options.log( 'WebSocket closed: ' + this.endpoint +
( code ? ' (' + code + ')' : '' ) +
( reason ? ' ' + reason : '' ) );
if ( Binance.options.reconnect && this.reconnect && reconnect ) {
if ( this.endpoint && parseInt( this.endpoint.length, 10 ) === 60 ) Binance.options.log( 'Account data WebSocket reconnecting...' );
else Binance.options.log( 'WebSocket reconnecting: ' + this.endpoint + '...' );
try {
reconnect();
} catch ( error ) {
Binance.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 */
Binance.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 httpsproxy = process.env.https_proxy || false;
let socksproxy = process.env.socks_proxy || false;
let ws = false;
if ( socksproxy !== false ) {
socksproxy = proxyReplacewithIp( socksproxy );
if ( Binance.options.verbose ) Binance.options.log( 'using socks proxy server ' + socksproxy );
let agent = new SocksProxyAgent( {
protocol: parseProxy( socksproxy )[0],
host: parseProxy( socksproxy )[1],
port: parseProxy( socksproxy )[2]
} );
ws = new WebSocket( stream + endpoint, { agent: agent } );
} else if ( httpsproxy !== false ) {
let config = url.parse( httpsproxy );
let agent = new HttpsProxyAgent( config );
if ( Binance.options.verbose ) Binance.options.log( 'using proxy server ' + agent );
ws = new WebSocket( stream + endpoint, { agent: agent } );
} else {
ws = new WebSocket( stream + endpoint );
}
if ( Binance.options.verbose ) Binance.options.log( 'Subscribed to ' + endpoint );
ws.reconnect = Binance.options.reconnect;
ws.endpoint = endpoint;
ws.isAlive = false;
ws.on( 'open', handleSocketOpen.bind( ws, opened_callback ) );
ws.on( 'pong', handleSocketHeartbeat );
ws.on( 'error', handleSocketError );
ws.on( 'close', handleSocketClose.bind( ws, reconnect ) );
ws.on( 'message', data => {
try {
callback( JSON.parse( data ) );
} catch ( error ) {
Binance.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 httpsproxy = process.env.https_proxy || false;
let socksproxy = process.env.socks_proxy || false;
const queryParams = streams.join( '/' );
let ws = false;
if ( socksproxy !== false ) {
socksproxy = proxyReplacewithIp( socksproxy );
if ( Binance.options.verbose ) Binance.options.log( 'using socks proxy server ' + socksproxy );
let agent = new SocksProxyAgent( {
protocol: parseProxy( socksproxy )[0],
host: parseProxy( socksproxy )[1],
port: parseProxy( socksproxy )[2]
} );
ws = new WebSocket( combineStream + queryParams, { agent: agent } );
} else if ( httpsproxy !== false ) {
if ( Binance.options.verbose ) Binance.options.log( 'using proxy server ' + httpsproxy );
let config = url.parse( httpsproxy );
let agent = new HttpsProxyAgent( config );
ws = new WebSocket( combineStream + queryParams, { agent: agent } );
} else {
ws = new WebSocket( combineStream + queryParams );
}
ws.reconnect = Binance.options.reconnect;
ws.endpoint = stringHash( queryParams );
ws.isAlive = false;
if ( Binance.options.verbose ) {
Binance.options.log( 'CombinedStream: Subscribed to [' + ws.endpoint + '] ' + queryParams );
}
ws.on( 'open', handleSocketOpen.bind( ws, opened_callback ) );
ws.on( 'pong', handleSocketHeartbeat );
ws.on( 'error', handleSocketError );
ws.on( 'close', handleSocketClose.bind( ws, reconnect ) );
ws.on( 'message', data => {
try {
callback( JSON.parse( data ).data );
} catch ( error ) {
Binance.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 = Binance.subscriptions[endpoint];
if ( !ws ) return;
ws.removeAllListeners( 'message' );
ws.reconnect = reconnect;
ws.terminate();
}
/**
* Futures heartbeat code with a shared single interval tick
* @return {undefined}
*/
const futuresSocketHeartbeat = () => {
/* Sockets removed from subscriptions during a manual terminate()
will no longer be at risk of having functions called on them */
for ( let endpointId in Binance.futuresSubscriptions ) {
const ws = Binance.futuresSubscriptions[endpointId];
if ( ws.isAlive ) {
ws.isAlive = false;
if ( ws.readyState === WebSocket.OPEN ) ws.ping( noop );
} else {
if ( Binance.options.verbose ) Binance.options.log( `Terminating zombie futures WebSocket: ${ ws.endpoint }` );
if ( ws.readyState === WebSocket.OPEN ) ws.terminate();
}
}
};
/**
* 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( Binance.futuresSubscriptions ).length === 0 ) {
Binance.socketHeartbeatInterval = setInterval( futuresSocketHeartbeat, 30000 );
}
Binance.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 Binance.futuresSubscriptions[this.endpoint];
if ( Binance.futuresSubscriptions && Object.keys( Binance.futuresSubscriptions ).length === 0 ) {
clearInterval( Binance.socketHeartbeatInterval );
}
Binance.options.log( 'Futures WebSocket closed: ' + this.endpoint +
( code ? ' (' + code + ')' : '' ) +
( reason ? ' ' + reason : '' ) );
if ( Binance.options.reconnect && this.reconnect && reconnect ) {
if ( this.endpoint && parseInt( this.endpoint.length, 10 ) === 60 ) Binance.options.log( 'Futures account data WebSocket reconnecting...' );
else Binance.options.log( 'Futures WebSocket reconnecting: ' + this.endpoint + '...' );
try {
reconnect();
} catch ( error ) {
Binance.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 ) {
Binance.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 httpsproxy = process.env.https_proxy || false;
let socksproxy = process.env.socks_proxy || false;
let ws = false;
if ( socksproxy !== false ) {
socksproxy = proxyReplacewithIp( socksproxy );
if ( Binance.options.verbose ) Binance.options.log( `futuresSubscribeSingle: using socks proxy server: ${ socksproxy }` );
let agent = new SocksProxyAgent( {
protocol: parseProxy( socksproxy )[0],
host: parseProxy( socksproxy )[1],
port: parseProxy( socksproxy )[2]
} );
ws = new WebSocket( ( Binance.options.test ? fstreamSingleTest : fstreamSingle ) + endpoint, { agent } );
} else if ( httpsproxy !== false ) {
if ( Binance.options.verbose ) Binance.options.log( `futuresSubscribeSingle: using proxy server: ${ agent }` );
let config = url.parse( httpsproxy );
let agent = new HttpsProxyAgent( config );
ws = new WebSocket( ( Binance.options.test ? fstreamSingleTest : fstreamSingle ) + endpoint, { agent } );
} else {
ws = new WebSocket( ( Binance.options.test ? fstreamSingleTest : fstreamSingle ) + endpoint );
}
if ( Binance.options.verbose ) Binance.options.log( 'futuresSubscribeSingle: Subscribed to ' + endpoint );
ws.reconnect = Binance.options.reconnect;
ws.endpoint = endpoint;
ws.isAlive = false;
ws.on( 'open', handleFuturesSocketOpen.bind( ws, params.openCallback ) );
ws.on( 'pong', handleFuturesSocketHeartbeat );
ws.on( 'error', handleFuturesSocketError );
ws.on( 'close', handleFuturesSocketClose.bind( ws, params.reconnect ) );
ws.on( 'message', data => {
try {
callback( JSON.parse( data ) );
} catch ( error ) {
Binance.options.log( 'Parse error: ' + error.message );
}
} );
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;
let httpsproxy = process.env.https_proxy || false;
let socksproxy = process.env.socks_proxy || false;
const queryParams = streams.join( '/' );
let ws = false;
if ( socksproxy !== false ) {
socksproxy = proxyReplacewithIp( socksproxy );
if ( Binance.options.verbose ) Binance.options.log( `futuresSubscribe: using socks proxy server ${ socksproxy }` );
let agent = new SocksProxyAgent( {
protocol: parseProxy( socksproxy )[0],
host: parseProxy( socksproxy )[1],
port: parseProxy( socksproxy )[2]
} );
ws = new WebSocket( ( Binance.options.test ? fstreamTest : fstream ) + queryParams, { agent } );
} else if ( httpsproxy !== false ) {
if ( Binance.options.verbose ) Binance.options.log( `futuresSubscribe: using proxy server ${ httpsproxy }` );
let config = url.parse( httpsproxy );
let agent = new HttpsProxyAgent( config );
ws = new WebSocket( ( Binance.options.test ? fstreamTest : fstream ) + queryParams, { agent } );
} else {
ws = new WebSocket( ( Binance.options.test ? fstreamTest : fstream ) + queryParams );
}
ws.reconnect = Binance.options.reconnect;
ws.endpoint = stringHash( queryParams );
ws.isAlive = false;
if ( Binance.options.verbose ) {
Binance.options.log( `futuresSubscribe: Subscribed to [${ ws.endpoint }] ${ queryParams }` );
}
ws.on( 'open', handleFuturesSocketOpen.bind( ws, params.openCallback ) );
ws.on( 'pong', handleFuturesSocketHeartbeat );
ws.on( 'error', handleFuturesSocketError );
ws.on( 'close', handleFuturesSocketClose.bind( ws, params.reconnect ) );
ws.on( 'message', data => {
try {
callback( JSON.parse( data ).data );
} catch ( error ) {
Binance.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 = Binance.futuresSubscriptions[endpoint];
if ( !ws ) return;
ws.removeAllListeners( 'message' );
ws.reconnect = reconnect;
ws.terminate();
}
/**
* 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 = Binance.futuresTicks[symbol][interval];
if ( typeof Binance.futuresRealtime[symbol][interval].time === 'undefined' ) return output;
const time = Binance.futuresRealtime[symbol][interval].time;
const last_updated = Object.keys( Binance.futuresTicks[symbol][interval] ).pop();
if ( time >= last_updated ) {
output[time] = Binance.futuresRealtime[symbol][interval];
//delete output[time].time;
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 ) {
// if ( typeof Binance.futuresRealtime[symbol][interval].time !== 'undefined' ) {
// if ( Binance.futuresRealtime[symbol][interval].time > time ) return;
// }
Binance.futuresRealtime[symbol][interval] = { time, closeTime, open, high, low, close, volume, quoteVolume, takerBuyBaseVolume, takerBuyQuoteVolume, trades, isFinal };
return;
}
const first_updated = Object.keys( Binance.futuresTicks[symbol][interval] ).shift();
if ( first_updated ) delete Binance.futuresTicks[symbol][interval][first_updated];
Binance.futuresTicks[symbol][interval][time] = { time, closeTime, open, high, low, close, volume, quoteVolume, takerBuyBaseVolume, takerBuyQuoteVolume, trades, isFinal:false };
};
/**
* 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,