UNPKG

node-binance-api

Version:

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

1,222 lines (1,220 loc) 232 kB
'use strict'; var WebSocket = require('ws'); var crypto = require('crypto'); var file = require('fs'); var url = require('url'); var JSONbig = require('json-bigint'); var httpsProxyAgent = require('https-proxy-agent'); var socksProxyAgent = require('socks-proxy-agent'); var nodeFetch = require('node-fetch'); var zip = require('lodash.zipobject'); var stringHash = require('string-hash'); var async = require('async'); class Binance { base = 'https://api.binance.com/api/'; baseTest = 'https://testnet.binance.vision/api/'; wapi = 'https://api.binance.com/wapi/'; sapi = 'https://api.binance.com/sapi/'; fapi = 'https://fapi.binance.com/fapi/'; dapi = 'https://dapi.binance.com/dapi/'; fapiTest = 'https://testnet.binancefuture.com/fapi/'; dapiTest = 'https://testnet.binancefuture.com/dapi/'; fstream = 'wss://fstream.binance.com/stream?streams='; fstreamSingle = 'wss://fstream.binance.com/ws/'; fstreamSingleTest = 'wss://stream.binancefuture.com/ws/'; fstreamTest = 'wss://stream.binancefuture.com/stream?streams='; dstream = 'wss://dstream.binance.com/stream?streams='; dstreamSingle = 'wss://dstream.binance.com/ws/'; dstreamSingleTest = 'wss://dstream.binancefuture.com/ws/'; dstreamTest = 'wss://dstream.binancefuture.com/stream?streams='; stream = 'wss://stream.binance.com:9443/ws/'; combineStream = 'wss://stream.binance.com:9443/stream?streams='; verbose = false; futuresListenKeyKeepAlive = 60 * 30 * 1000; // 30 minutes spotListenKeyKeepAlive = 60 * 30 * 1000; // 30 minutes heartBeatInterval = 30000; // 30 seconds // proxy variables urlProxy = undefined; httpsProxy = undefined; socksProxy = undefined; nodeFetch = undefined; APIKEY = undefined; APISECRET = undefined; test = false; timeOffset = 0; userAgent = 'Mozilla/4.0 (compatible; Node Binance API)'; contentType = 'application/x-www-form-urlencoded'; SPOT_PREFIX = "x-HNA2TXFJ"; CONTRACT_PREFIX = "x-Cb7ytekJ"; // Websockets Options isAlive = false; socketHeartbeatInterval = null; endpoint = ""; // endpoint for WS? reconnect = true; headers = {}; subscriptions = {}; futuresSubscriptions = {}; futuresInfo = {}; futuresMeta = {}; futuresTicks = {}; futuresRealtime = {}; futuresKlineQueue = {}; deliverySubscriptions = {}; deliveryInfo = {}; deliveryMeta = {}; deliveryTicks = {}; deliveryRealtime = {}; deliveryKlineQueue = {}; depthCache = {}; depthCacheContext = {}; ohlcLatest = {}; klineQueue = {}; ohlc = {}; info = {}; default_options = { recvWindow: 5000, useServerTime: false, reconnect: true, keepAlive: true, verbose: false, test: false, hedgeMode: false, localAddress: false, family: 4, log(...args) { console.log(Array.prototype.slice.call(args)); } }; Options = {}; constructor(userOptions = {}) { if (userOptions) { this.setOptions(userOptions); } } options(opt = {}) { // // return await this.setOptions(opt, callback); // keep this method for backwards compatibility // this.assignOptions(opt, callback); this.setOptions(opt); return this; } assignOptions(opt = {}) { if (typeof opt === 'string') { // Pass json config filename this.Options = JSON.parse(file.readFileSync(opt)); } else this.Options = opt; if (!this.Options.recvWindow) this.Options.recvWindow = this.default_options.recvWindow; if (!this.Options.useServerTime) this.Options.useServerTime = this.default_options.useServerTime; if (!this.Options.reconnect) this.Options.reconnect = this.default_options.reconnect; if (!this.Options.test) this.Options.test = this.default_options.test; if (!this.Options.hedgeMode) this.Options.hedgeMode = this.default_options.hedgeMode; if (!this.Options.log) this.Options.log = this.default_options.log; if (!this.Options.verbose) this.Options.verbose = this.default_options.verbose; if (!this.Options.keepAlive) this.Options.keepAlive = this.default_options.keepAlive; if (!this.Options.localAddress) this.Options.localAddress = this.default_options.localAddress; if (!this.Options.family) this.Options.family = this.default_options.family; if (this.Options.urls !== undefined) { const { urls } = this.Options; if (urls.base) this.base = urls.base; if (urls.wapi) this.wapi = urls.wapi; if (urls.sapi) this.sapi = urls.sapi; if (urls.fapi) this.fapi = urls.fapi; if (urls.fapiTest) this.fapiTest = urls.fapiTest; if (urls.stream) this.stream = urls.stream; if (urls.combineStream) this.combineStream = urls.combineStream; if (urls.fstream) this.fstream = urls.fstream; if (urls.fstreamSingle) this.fstreamSingle = urls.fstreamSingle; if (urls.fstreamTest) this.fstreamTest = urls.fstreamTest; if (urls.fstreamSingleTest) this.fstreamSingleTest = urls.fstreamSingleTest; if (urls.dstream) this.dstream = urls.dstream; if (urls.dstreamSingle) this.dstreamSingle = urls.dstreamSingle; if (urls.dstreamTest) this.dstreamTest = urls.dstreamTest; if (urls.dstreamSingleTest) this.dstreamSingleTest = urls.dstreamSingleTest; } if (this.Options.APIKEY) this.APIKEY = this.Options.APIKEY; if (this.Options.APISECRET) this.APISECRET = this.Options.APISECRET; if (this.Options.test) this.test = true; if (this.Options.headers) this.headers = this.Options.Headers; } async setOptions(opt = {}) { this.assignOptions(opt); if (this.Options.useServerTime) { const res = await this.publicSpotRequest('v3/time'); this.timeOffset = res.serverTime - new Date().getTime(); } return this; } // ---- HELPER FUNCTIONS ---- // extend = (...args) => Object.assign({}, ...args); getSpotUrl() { if (this.Options.test) return this.baseTest; return this.base; } getFapiUrl() { if (this.Options.test) return this.fapiTest; return this.fapi; } getDapiUrl() { if (this.Options.test) return this.dapiTest; return this.dapi; } uuid22(a) { return a ? (a ^ Math.random() * 16 >> a / 4).toString(16) : ([1e7] + 1e3 + 4e3 + 8e5).replace(/[018]/g, this.uuid22); } getUrlProxy() { if (this.urlProxy) { return this.urlProxy; } return undefined; } getHttpsProxy() { if (this.httpsProxy) { return this.httpsProxy; } if (process.env.https_proxy) { return process.env.https_proxy; } return undefined; } getSocksProxy() { if (this.socksProxy) { return this.socksProxy; } if (process.env.socks_proxy) { return process.env.socks_proxy; } return undefined; } // ------ Request Related Functions ------ // /** * Replaces socks connection uri hostname with IP address * @param {string} connString - socks connection string * @return {string} modified string with ip address */ 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 */ parseProxy(connString) { const arr = connString.split('/'); const host = arr[2].split(':')[0]; const 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 */ isIterable(obj) { if (obj === null) return false; return typeof obj[Symbol.iterator] === 'function'; } addProxy(opt) { if (this.Options.proxy) { const proxyauth = this.Options.proxy.auth ? `${this.Options.proxy.auth.username}:${this.Options.proxy.auth.password}@` : ''; opt.proxy = `http://${proxyauth}${this.Options.proxy.host}:${this.Options.proxy.port}`; } return opt; } async reqHandler(response) { this.info.lastRequest = new Date().getTime(); if (response) { this.info.statusCode = response.status || 0; if (response.request) this.info.lastURL = response.request.uri.href; if (response.headers) { this.info.usedWeight = response.headers['x-mbx-used-weight-1m'] || 0; this.info.orderCount1s = response.headers['x-mbx-order-count-1s'] || 0; this.info.orderCount1m = response.headers['x-mbx-order-count-1m'] || 0; this.info.orderCount1h = response.headers['x-mbx-order-count-1h'] || 0; this.info.orderCount1d = response.headers['x-mbx-order-count-1d'] || 0; } } if (response && response.status !== 200) { const error = new Error(await response.text()); // error.code = response.status; // error.url = response.url; throw error; } } async proxyRequest(opt) { // const req = request(this.addProxy(opt), this.reqHandler(cb)).on('error', (err) => { cb(err, {}) }); // family: opt.family, // timeout: opt.timeout, const urlBody = new URLSearchParams(opt.form); const reqOptions = { method: opt.method, headers: opt.headers, // body: urlBody // body: (opt.form) }; if (opt.method !== 'GET') { reqOptions.body = urlBody; } else { if (opt.qs) ; } if (this.Options.verbose) { this.Options.log('HTTP Request:', opt.method, opt.url, reqOptions); } // https-proxy const httpsproxy = this.getHttpsProxy(); const socksproxy = this.getSocksProxy(); const urlProxy = this.getUrlProxy(); if (httpsproxy) { if (this.Options.verbose) this.Options.log('using https proxy: ' + httpsproxy); reqOptions.agent = new httpsProxyAgent.HttpsProxyAgent(httpsproxy); } else if (socksproxy) { if (this.Options.verbose) this.Options.log('using socks proxy: ' + socksproxy); reqOptions.agent = new socksProxyAgent.SocksProxyAgent(socksproxy); } if (urlProxy) { opt.url = urlProxy + opt.url; } let fetchImplementation = fetch; // require node-fetch if (reqOptions.agent) { fetchImplementation = nodeFetch; } const response = await fetchImplementation(opt.url, reqOptions); await this.reqHandler(response); const json = await response.json(); if (this.Options.verbose) { this.Options.log('HTTP Response:', json); } return json; } reqObj(url, data = {}, method = 'GET', key) { return { url: url, qs: data, method: method, family: this.Options.family, localAddress: this.Options.localAddress, timeout: this.Options.recvWindow, forever: this.Options.keepAlive, headers: { 'User-Agent': this.userAgent, 'Content-type': this.contentType, 'X-MBX-APIKEY': key || '' } }; } reqObjPOST(url, data = {}, method = 'POST', key) { return { url: url, form: data, method: method, family: this.Options.family, localAddress: this.Options.localAddress, timeout: this.Options.recvWindow, forever: this.Options.keepAlive, qsStringifyOptions: { arrayFormat: 'repeat' }, headers: { 'User-Agent': this.userAgent, 'Content-type': this.contentType, 'X-MBX-APIKEY': key || '' } }; } async publicRequest(url, data = {}, method = 'GET') { const query = this.makeQueryString(data); const opt = this.reqObj(url + (query ? '?' + query : ''), data, method); const res = await this.proxyRequest(opt); return res; } /** * Used to make public requests to the futures (FAPI) API * @param path * @param data * @param method * @returns */ async publicFuturesRequest(path, data = {}, method = 'GET') { return await this.publicRequest(this.getFapiUrl() + path, data, method); } /** * Used to make public requests to the delivery (DAPI) API * @param path * @param data * @param method * @returns */ async publicDeliveryRequest(path, data = {}, method = 'GET') { return await this.publicRequest(this.getDapiUrl() + path, data, method); } /** * Used to make private requests to the futures (FAPI) API * @param path * @param data * @param method * @returns */ async privateFuturesRequest(path, data = {}, method = 'GET') { return await this.futuresRequest(this.getFapiUrl() + path, data, method, true); } /** * Used to make private requests to the delivery (DAPI) API * @param path * @param data * @param method * @returns */ async privateDeliveryRequest(path, data = {}, method = 'GET') { return await this.futuresRequest(this.getDapiUrl() + path, data, method, true); } /** * Used to make a request to the futures API, this is a generic function that can be used to make any request to the futures API * @param url * @param data * @param method * @param isPrivate * @returns */ async futuresRequest(url, data = {}, method = 'GET', isPrivate = false) { let query = ''; const headers = { 'User-Agent': this.userAgent, 'Content-type': 'application/x-www-form-urlencoded' }; if (isPrivate) { if (!data.recvWindow) data.recvWindow = this.Options.recvWindow; this.requireApiKey('promiseRequest'); headers['X-MBX-APIKEY'] = this.APIKEY; } const opt = { headers: this.extend(headers, this.headers), url: url, method: method, timeout: this.Options.recvWindow, followAllRedirects: true }; query = this.makeQueryString(data); if (method === 'GET') { opt.url = `${url}?${query}`; } if (isPrivate) { data.timestamp = new Date().getTime(); if (this.timeOffset) { data.timestamp += this.timeOffset; } query = this.makeQueryString(data); data.signature = crypto.createHmac('sha256', this.APISECRET).update(query).digest('hex'); // HMAC hash header opt.url = `${url}?${query}&signature=${data.signature}`; } opt.qs = data; const response = await this.proxyRequest(opt); return response; } // ------ Request Related Functions ------ // // XXX: This one works with array (e.g. for dust.transfer) // XXX: I _guess_ we could use replace this function with the `qs` module makeQueryString(q) { const res = 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("&"); return res; } /** * 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} */ async apiRequest(url, data = {}, method = 'GET') { this.requireApiKey('apiRequest'); const opt = this.reqObj(url, data, method, this.APIKEY); const res = await this.proxyRequest(opt); return res; } requireApiKey(source = 'requireApiKey', fatalError = true) { if (!this.APIKEY) { if (fatalError) throw Error(`${source}: Invalid API Key!`); return false; } return true; } // Check if API secret is present requireApiSecret(source = 'requireApiSecret', fatalError = true) { if (!this.APIKEY) { if (fatalError) throw Error(`${source}: Invalid API Key!`); return false; } if (!this.APISECRET) { if (fatalError) throw Error(`${source}: Invalid API Secret!`); return false; } return true; } /** * Create a public spot/margin request * @param {string} path - url path * @param {object} data - The data to send * @param {string} method - the http method * @param {boolean} noDataInSignature - Prevents data from being added to signature * @return {undefined} */ async publicSpotRequest(path, data = {}, method = 'GET') { return await this.publicRequest /**/(this.getSpotUrl() + path, data, method); } /** * Create a signed spot/margin request * @param {string} path - url path * @param {object} data - The data to send * @param {string} method - the http method * @param {boolean} noDataInSignature - Prevents data from being added to signature * @return {undefined} */ async privateSpotRequest(path, data = {}, method = 'GET', noDataInSignature = false) { return await this.signedRequest /**/(this.getSpotUrl() + path, data, method, noDataInSignature); } /** * 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} */ async signedRequest(url, data = {}, method = 'GET', noDataInSignature = false) { this.requireApiSecret('signedRequest'); data.timestamp = new Date().getTime(); if (this.timeOffset) data.timestamp += this.timeOffset; if (!data.recvWindow) data.recvWindow = this.Options.recvWindow; const query = method === 'POST' && noDataInSignature ? '' : this.makeQueryString(data); const signature = crypto.createHmac('sha256', this.Options.APISECRET).update(query).digest('hex'); // set the HMAC hash header if (method === 'POST') { const opt = this.reqObjPOST(url, data, method, this.APIKEY); opt.form.signature = signature; const reqPost = await this.proxyRequest(opt); return reqPost; } else { const opt = this.reqObj(url + '?' + query + '&signature=' + signature, data, method, this.APIKEY); const reqGet = await this.proxyRequest(opt); return reqGet; } } // --- ENDPOINTS --- // /** * Create a signed spot order * @see https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#new-order-trade * @see https://developers.binance.com/docs/binance-spot-api-docs/rest-api/public-api-endpoints#test-new-order-trade * @see https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#new-order-list---oco-trade * @param {OrderType} type - LIMIT, MARKET, STOP_LOSS, STOP_LOSS_LIMIT, TAKE_PROFIT, TAKE_PROFIT_LIMIT, LIMIT_MAKER * @param {OrderSide} 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} params - additional order settings * @return {undefined} */ async order(type, side, symbol, quantity, price, params = {}) { let endpoint = params.type === 'OCO' ? 'v3/orderList/oco' : 'v3/order'; if (params.test) { delete params.test; endpoint += '/test'; } const request = { symbol: symbol, side: side, type: type }; if (params.quoteOrderQty && params.quoteOrderQty > 0) request.quoteOrderQty = params.quoteOrderQty; else request.quantity = quantity; if (request.type.includes('LIMIT')) { request.price = price; if (request.type !== 'LIMIT_MAKER') { request.timeInForce = 'GTC'; } } if (request.type == 'MARKET' && typeof params.quoteOrderQty !== 'undefined') { request.quoteOrderQty = params.quoteOrderQty; delete request.quantity; } if (request.type === 'OCO') { request.price = price; request.stopLimitPrice = params.stopLimitPrice; request.stopLimitTimeInForce = 'GTC'; delete request.type; // if (typeof params.listClientOrderId !== 'undefined') opt.listClientOrderId = params.listClientOrderId; // if (typeof params.limitClientOrderId !== 'undefined') opt.limitClientOrderId = params.limitClientOrderId; // if (typeof params.stopClientOrderId !== 'undefined') opt.stopClientOrderId = params.stopClientOrderId; } // if (typeof params.timeInForce !== 'undefined') opt.timeInForce = params.timeInForce; // if (typeof params.newOrderRespType !== 'undefined') opt.newOrderRespType = params.newOrderRespType; if (!params.newClientOrderId) { request.newClientOrderId = this.SPOT_PREFIX + this.uuid22(); } /* * STOP_LOSS * STOP_LOSS_LIMIT * TAKE_PROFIT * TAKE_PROFIT_LIMIT * LIMIT_MAKER */ // if (typeof params.icebergQty !== 'undefined') request.icebergQty = params.icebergQty; if (params.stopPrice) { request.stopPrice = params.stopPrice; if (request.type === 'LIMIT') throw Error('stopPrice: Must set "type" to one of the following: STOP_LOSS, STOP_LOSS_LIMIT, TAKE_PROFIT, TAKE_PROFIT_LIMIT'); } const response = await this.privateSpotRequest(endpoint, this.extend(request, params), 'POST'); // to do error handling // if ( !response ) { // if ( callback ) callback( error, response ); // else this.options.log( 'Order() error:', error ); // return; // } // if ( typeof response.msg !== 'undefined' && response.msg === 'Filter failure: MIN_NOTIONAL' ) { // this.options.log( 'Order quantity too small. See exchangeInfo() for minimum amounts' ); // } // if ( callback ) callback( error, response ); // else this.options.log( side + '(' + symbol + ',' + quantity + ',' + price + ') ', response ); return response; } /** * Creates a buy order * @param {string} symbol - the symbol to buy * @param {numeric} quantity - the quantity required * @param {numeric} price - the price to pay for each unit * @param {object} flags - additional buy order flags * @return {promise or undefined} - omitting the callback returns a promise */ async buy(symbol, quantity, price, flags = {}) { return await this.order('LIMIT', 'BUY', symbol, quantity, price, flags); } /** * Creates a sell order * @param {string} symbol - the symbol to sell * @param {numeric} quantity - the quantity required * @param {numeric} price - the price to pay for each unit * @param {object} flags - additional buy order flags * @param {function} callback - the callback function * @return {promise or undefined} - omitting the callback returns a promise */ async sell(symbol, quantity, price, flags = {}) { return await this.order('LIMIT', 'SELL', symbol, quantity, price, flags); } /** * Creates a market buy order * @see https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#new-order-trade * @see https://developers.binance.com/docs/binance-spot-api-docs/rest-api/public-api-endpoints#test-new-order-trade * @see https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#new-order-list---oco-trade * @param {string} symbol - the symbol to buy * @param {numeric} quantity - the quantity required * @param {object} params - additional buy order flags * @return {promise or undefined} - omitting the callback returns a promise */ async marketBuy(symbol, quantity, params = {}) { return await this.order('MARKET', 'BUY', symbol, quantity, 0, params); } /** * Creates a market sell order * @see https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#new-order-trade * @see https://developers.binance.com/docs/binance-spot-api-docs/rest-api/public-api-endpoints#test-new-order-trade * @see https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#new-order-list---oco-trade * @param {string} symbol - the symbol to sell * @param {numeric} quantity - the quantity required * @param {object} flags - additional buy order flags * @return {promise or undefined} - omitting the callback returns a promise */ async marketSell(symbol, quantity, params = {}) { return await this.order('MARKET', 'SELL', symbol, quantity, 0, params); } /** * Cancels an order * @see https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#cancel-order-trade * @param {string} symbol - the symbol to cancel * @param {string} orderid - the orderid to cancel * @return {promise or undefined} - omitting the callback returns a promise */ async cancel(symbol, orderid, params = {}) { return await this.privateSpotRequest('v3/order', this.extend({ symbol: symbol, orderId: orderid }, params), 'DELETE'); } /** * Gets the status of an order * @see https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#query-order-user_data * @param {string} symbol - the symbol to check * @param {string} orderid - the orderid to check if !orderid then use flags to search * @param {object} flags - any additional flags * @return {promise or undefined} - omitting the callback returns a promise */ async orderStatus(symbol, orderid, flags = {}) { let parameters = Object.assign({ symbol: symbol }, flags); if (orderid) { parameters = Object.assign({ orderId: orderid }, parameters); } return await this.privateSpotRequest('v3/order', parameters); } /** * Gets open orders * @see https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#current-open-orders-user_data * @param {string} symbol - the symbol to get * @return {promise or undefined} - omitting the callback returns a promise */ async openOrders(symbol, params = {}) { const parameters = symbol ? { symbol: symbol } : {}; return await this.privateSpotRequest('v3/openOrders', this.extend(parameters, params)); } /** * Cancels all orders of a given symbol * @see https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#cancel-all-open-orders-on-a-symbol-trade * @param {string} symbol - the symbol to cancel all orders for * @return {promise or undefined} - omitting the callback returns a promise */ async cancelAll(symbol, params = {}) { return await this.privateSpotRequest('v3/openOrders', this.extend({ symbol }, params), 'DELETE'); } /** * Cancels all orders of a given symbol * @param {string} symbol - the symbol to cancel all orders for * @return {promise or undefined} - omitting the callback returns a promise */ async cancelOrders(symbol, params = {}) { const json = await this.privateSpotRequest('v3/openOrders', this.extend({ symbol: symbol }, params), 'DELETE'); // if (json.length === 0) { // return callback.call(this, 'No orders present for this symbol', {}, symbol); // } // if (Object.keys(json).length === 0) { // return callback.call(this, 'No orders present for this symbol', {}, symbol); // } // for (let obj of json) { // let quantity = obj.origQty - obj.executedQty; // this.options.log('cancel order: ' + obj.side + ' ' + symbol + ' ' + quantity + ' @ ' + obj.price + ' #' + obj.orderId); // signedRequest(this.getSpotUrl() + 'v3/order', { symbol: symbol, orderId: obj.orderId }, function (error, data) { // return callback.call(this, error, data, symbol); // }, 'DELETE'); // } return json; // to do: check this logic of cancelling remaining orders manually } /** * Gets all order of a given symbol * @see https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#all-orders-user_data * @param {string} symbol - the symbol * @param {object} options - additional options * @return {promise or undefined} - omitting the callback returns a promise */ async allOrders(symbol, params = {}) { const parameters = this.extend({ symbol }, params); return await this.privateSpotRequest('v3/allOrders', parameters); } /** * Create a signed margin order * @see https://developers.binance.com/docs/margin_trading/trade/Margin-Account-New-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} params - additional order settings * @return {undefined} */ async marginOrder(type, side, symbol, quantity, price, params = {}) { let endpoint = 'v1/margin/order'; if (this.Options.test || params.test) endpoint += '/test'; const request = { symbol: symbol, side: side, type: type, quantity: quantity }; if (typeof params.type !== 'undefined') request.type = params.type; if ('isIsolated' in params) request.isIsolated = params.isIsolated; if (request.type.includes('LIMIT')) { request.price = price; if (request.type !== 'LIMIT_MAKER') { request.timeInForce = 'GTC'; } } if (typeof params.timeInForce !== 'undefined') request.timeInForce = params.timeInForce; if (typeof params.newOrderRespType !== 'undefined') request.newOrderRespType = params.newOrderRespType; // if ( typeof flags.newClientOrderId !== 'undefined' ) opt.newClientOrderId = flags.newClientOrderId; if (typeof params.newClientOrderId !== 'undefined') { request.newClientOrderId = params.newClientOrderId; } else { request.newClientOrderId = this.SPOT_PREFIX + this.uuid22(); } if (typeof params.sideEffectType !== 'undefined') request.sideEffectType = params.sideEffectType; /* * STOP_LOSS * STOP_LOSS_LIMIT * TAKE_PROFIT * TAKE_PROFIT_LIMIT */ if (typeof params.icebergQty !== 'undefined') request.icebergQty = params.icebergQty; if (typeof params.stopPrice !== 'undefined') { request.stopPrice = params.stopPrice; if (request.type === 'LIMIT') throw Error('stopPrice: Must set "type" to one of the following: STOP_LOSS, STOP_LOSS_LIMIT, TAKE_PROFIT, TAKE_PROFIT_LIMIT'); } return await this.privateSpotRequest(endpoint, this.extend(request, params), 'POST'); } // Futures internal functions /** * @see https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/New-Order * @param type * @param side * @param symbol symbol if the market * @param quantity * @param price * @param params extra parameters to be sent in the request * @returns */ async futuresOrder(type, side, symbol, quantity, price, params = {}) { params.symbol = symbol; params.side = side; params.type = type; if (quantity) params.quantity = quantity; // if in the binance futures setting Hedged mode is active, positionSide parameter is mandatory if (!params.positionSide && this.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 (!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. } if (!params.newClientOrderId) { params.newClientOrderId = this.CONTRACT_PREFIX + this.uuid22(); } return await this.privateFuturesRequest('v1/order', params, 'POST'); } async deliveryOrder(type, side, symbol, quantity, price, params = {}) { params.symbol = symbol; params.side = side; params.quantity = quantity; params.type = type; // if in the binance futures setting Hedged mode is active, positionSide parameter is mandatory if (this.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 (!params.type) params.type = 'LIMIT'; } else { if (!params.type) 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. } if (!params.newClientOrderId) { params.newClientOrderId = this.CONTRACT_PREFIX + this.uuid22(); } return await this.privateDeliveryRequest('v1/order', params, 'POST'); } // ------ WS RELATED FUNCTIONS ------ // noop() { return; } /** * Reworked Tuitio's heartbeat code into a shared single interval tick * @return {undefined} */ socketHeartbeat() { /* Sockets removed from `subscriptions` during a manual terminate() will no longer be at risk of having functions called on them */ for (const endpointId in this.subscriptions) { const ws = this.subscriptions[endpointId]; if (ws.isAlive) { ws.isAlive = false; if (ws.readyState === WebSocket.OPEN) ws.ping(this.noop); } else { if (this.Options.verbose) this.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} */ handleSocketOpen(opened_callback) { this.isAlive = true; if (Object.keys(this.subscriptions).length === 0) { this.socketHeartbeatInterval = setInterval(this.socketHeartbeat, this.heartBeatInterval); } this.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 {Function} reconnect - reconnect callback * @param {string} code - code associated with the socket * @param {string} reason - string with the response * @return {undefined} */ handleSocketClose(reconnect, code, reason) { delete this.subscriptions[this.endpoint]; if (this.subscriptions && Object.keys(this.subscriptions).length === 0) { clearInterval(this.socketHeartbeatInterval); } this.Options.log('WebSocket closed: ' + this.endpoint + (code ? ' (' + code + ')' : '') + (reason ? ' ' + reason : '')); if (this.Options.reconnect && this.reconnect && reconnect) { if (this.endpoint && this.endpoint.length === 60) this.Options.log('Account data WebSocket reconnecting...'); else this.Options.log('WebSocket reconnecting: ' + this.endpoint + '...'); try { reconnect(); } catch (error) { this.Options.log('WebSocket reconnect error: ' + error.message); } } } /** * Called when socket errors * @param {object} error - error object message * @return {undefined} */ handleSocketError(error) { /* Errors ultimately result in a `close` event. see: https://github.com/websockets/ws/blob/828194044bf247af852b31c49e2800d557fedeff/lib/websocket.js#L126 */ this.Options.log('WebSocket error: ' + this.endpoint + (error.code ? ' (' + error.code + ')' : '') + (error.message ? ' ' + error.message : '')); } /** * Called on each socket heartbeat * @return {undefined} */ handleSocketHeartbeat() { this.isAlive = true; } // ----- WS ENDPOINTS ----- // /** * Get Binance server time * @return {promise or undefined} - omitting the callback returns a promise */ async time() { const res = await this.publicSpotRequest('v3/time', {}); return res; } /** * 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 */ subscribe(endpoint, callback, reconnect, opened_callback) { const httpsproxy = this.getHttpsProxy(); let socksproxy = this.getSocksProxy(); let ws = undefined; if (socksproxy) { socksproxy = this.proxyReplacewithIp(socksproxy); if (this.Options.verbose) this.Options.log('using socks proxy server ' + socksproxy); const agent = new socksProxyAgent.SocksProxyAgent({ protocol: this.parseProxy(socksproxy)[0], host: this.parseProxy(socksproxy)[1], port: this.parseProxy(socksproxy)[2] }); ws = new WebSocket(this.stream + endpoint, { agent: agent }); } else if (httpsproxy) { const config = url.parse(httpsproxy); const agent = new httpsProxyAgent.HttpsProxyAgent(config); if (this.Options.verbose) this.Options.log('using proxy server ' + agent); ws = new WebSocket(this.stream + endpoint, { agent: agent }); } else { ws = new WebSocket(this.stream + endpoint); } if (this.Options.verbose) this.Options.log('Subscribed to ' + endpoint); ws.reconnect = this.Options.reconnect; ws.endpoint = endpoint; ws.isAlive = false; ws.on('open', this.handleSocketOpen.bind(this, opened_callback)); ws.on('pong', this.handleSocketHeartbeat.bind(this)); ws.on('error', this.handleSocketError.bind(this)); ws.on('close', this.handleSocketClose.bind(this, reconnect)); ws.on('message', data => { try { if (this.Options.verbose) this.Options.log('WebSocket data:', data); callback(JSON.parse(data)); } catch (error) { this.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 */ subscribeCombined(streams, callback, reconnect, opened_callback) { const httpsproxy = this.getHttpsProxy(); let socksproxy = this.getSocksProxy(); const queryParams = streams.join('/'); let ws = undefined; if (socksproxy) { socksproxy = this.proxyReplacewithIp(socksproxy); if (this.Options.verbose) this.Options.log('using socks proxy server ' + socksproxy); const agent = new socksProxyAgent.SocksProxyAgent({ protocol: this.parseProxy(socksproxy)[0], host: this.parseProxy(socksproxy)[1], port: this.parseProxy(socksproxy)[2] }); ws = new WebSocket(this.combineStream + queryParams, { agent: agent }); } else if (httpsproxy) { if (this.Options.verbose) this.Options.log('using proxy server ' + httpsproxy); const config = url.parse(httpsproxy); const agent = new httpsProxyAgent.HttpsProxyAgent(config); ws = new WebSocket(this.combineStream + queryParams, { agent: agent }); } else { ws = new WebSocket(this.combineStream + queryParams); } ws.reconnect = this.Options.reconnect; ws.endpoint = stringHash(queryParams); ws.isAlive = false; if (this.Options.verbose) { this.Options.log('CombinedStream: Subscribed to [' + ws.endpoint + '] ' + queryParams); } ws.on('open', this.handleSocketOpen.bind(this, opened_callback)); ws.on('pong', this.handleSocketHeartbeat.bind(this)); ws.on('error', this.handleSocketError.bind(this)); ws.on('close', this.handleSocketClose.bind(this, reconnect)); ws.on('message', data => { try { if (this.Options.verbose) this.Options.log('CombinedStream: WebSocket data:', data); callback(JSON.parse(data).data); } catch (error) { this.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} */ terminate(endpoint, reconnect = false) { if (this.Options.verbose) this.Options.log('WebSocket terminating:', endpoint); const ws = this.subscriptions[endpoint]; if (!ws) return; ws.removeAllListeners('message'); ws.reconnect = reconnect; ws.terminate(); } /** * Futures heartbeat code with a shared single interval tick * @return {undefined} */ futuresSocketHeartbeat() { /* Sockets removed from subscriptions during a manual terminate() will no longer be at risk of having functions called on them */ for (const endpointId in this.futuresSubscriptions) { const ws = this.futuresSubscriptions[endpointId]; if (ws.isAlive) { ws.isAlive = false; if (ws.readyState === WebSocket.OPEN) ws.ping(this.noop); } else { if (this.Options.verbose) this.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} */ handleFuturesSocketOpen(openCallback) { this.isAlive = true; if (Object.keys(this.futuresSubscriptions).length === 0) { this.socketHeartbeatInterval = setInterval(this.futuresSocketHeartbeat, this.heartBeatInterval); } this.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} */ handleFuturesSocketClose(reconnect, code, reason) { delete this.futuresSubscriptions[this.endpoint]; if (this.futuresSubscriptions && Object.keys(this.futuresSubscriptions).length === 0) { clearInterval(this.socketHeartbeatInterval); } this.Options.log('Futures WebSocket closed: ' + this.endpoint + (code ? ' (' + code + ')' : '') + (reason ? ' ' + reason : '')); if (this.Options.reconnect && this.reconnect && reconnect) { if (this.endpoint && this.endpoint.length === 60) this.Options.log('Futures account data WebSocket reconnecting...'); else this.Options.log('Futures WebSocket reconnecting: ' + this.endpoint + '...'); try { reconnect(); } catch (error) { this.Options.log('Futures WebSocket reconnect error: ' + error.message); } } } /** * Called when a futures websocket errors * @param {object} error - error object message * @return {undefined} */ handleFuturesSocketError(error) { this.Options.log('Futures WebSocket error: ' + this.endpoint + (error.code ? ' (' + error.code + ')' : '') + (error.message ? ' ' + error.message : '')); } /** * Called on each futures socket heartbeat * @return {undefined} */ handleFuturesSocketHeartbeat() { 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 */ futuresSubscribeSingle(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; const httpsproxy = this.getHttpsProxy(); let socksproxy = this.getSocksProxy(); let ws = undefined; if (socksproxy) { socksproxy = this.proxyReplacewithIp(socksproxy); if (this.Options.verbose) this.Options.log(`futuresSubscribeSingle: using socks proxy server: ${socksproxy}`); const agent = new socksProxyAgent.SocksProxyAgent({ protocol: this.parseProxy(socksproxy)[0], host: this.parseProxy(socksproxy)[1], port: this.parseProxy(socksproxy)[2] }); ws = new WebSocket((this.Options.test ? this.fstreamSingleTest : this.fstreamSingle) + endpoint, { agent }); } else if (httpsproxy) { const config = url.parse(httpspr