UNPKG

node-binance-api

Version:

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

1,195 lines 245 kB
import WebSocket from 'ws'; // import request from 'request'; import crypto from 'crypto'; import file from 'fs'; import url from 'url'; import JSONbig from 'json-bigint'; // @ts-ignore import { HttpsProxyAgent } from 'https-proxy-agent'; // @ts-ignore import { SocksProxyAgent } from 'socks-proxy-agent'; // @ts-ignore import nodeFetch from 'node-fetch'; // @ts-ignore import zip from 'lodash.zipobject'; import stringHash from 'string-hash'; export default class Binance { domain = 'com'; base = `https://api.binance.${this.domain}/api/`; baseTest = `https://testnet.binance.vision/api/`; wapi = `https://api.binance.${this.domain}/wapi/`; sapi = `https://api.binance.${this.domain}/sapi/`; fapi = `https://fapi.binance.${this.domain}/fapi/`; dapi = `https://dapi.binance.${this.domain}/dapi/`; fapiTest = `https://testnet.binancefuture.com/fapi/`; dapiTest = `https://testnet.binancefuture.com/dapi/`; fstream = `wss://fstream.binance.${this.domain}/stream?streams=`; fstreamSingle = `wss://fstream.binance.${this.domain}/ws/`; fstreamSingleTest = `wss://stream.binancefuture.${this.domain}/ws/`; fstreamTest = `wss://stream.binancefuture.${this.domain}/stream?streams=`; dstream = `wss://dstream.binance.${this.domain}/stream?streams=`; dstreamSingle = `wss://dstream.binance.${this.domain}/ws/`; dstreamSingleTest = `wss://dstream.binancefuture.${this.domain}/ws/`; dstreamTest = `wss://dstream.binancefuture.${this.domain}/stream?streams=`; stream = `wss://stream.binance.${this.domain}:9443/ws/`; streamTest = `wss://stream.testnet.binance.vision/ws/`; combineStream = `wss://stream.binance.${this.domain}:9443/stream?streams=`; combineStreamTest = `wss://testnet.binance.vision/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; PRIVATEKEY = undefined; PRIVATEKEYPASSWORD = 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: string = ""; // 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 = {}; websockets = { userData: this.userData.bind(this), userMarginData: this.userMarginData.bind(this), depthCacheStaggered: this.depthCacheStaggered.bind(this), userFutureData: this.userFutureData.bind(this), userDeliveryData: this.userDeliveryData.bind(this), subscribeCombined: this.subscribeCombined.bind(this), subscribe: this.subscribe.bind(this), subscriptions: () => this.getSubscriptions.bind(this), terminate: this.terminate.bind(this), depth: this.depthStream.bind(this), depthCache: this.depthCacheStream.bind(this), clearDepthCache: this.clearDepthCache.bind(this), aggTrades: this.aggTradesStream.bind(this), trades: this.tradesStream.bind(this), chart: this.chart.bind(this), candlesticks: this.candlesticksStream.bind(this), miniTicker: this.miniTicker.bind(this), bookTickers: this.bookTickersStream.bind(this), prevDay: this.prevDayStream.bind(this), futuresCandlesticks: this.futuresCandlesticksStream.bind(this), futuresTicker: this.futuresTickerStream.bind(this), futuresMiniTicker: this.futuresMiniTickerStream.bind(this), futuresAggTrades: this.futuresAggTradeStream.bind(this), futuresMarkPrice: this.futuresMarkPriceStream.bind(this), futuresLiquidation: this.futuresLiquidationStream.bind(this), futuresBookTicker: this.futuresBookTickerStream.bind(this), futuresChart: this.futuresChart.bind(this), deliveryAggTrade: this.deliveryAggTradeStream.bind(this), deliveryCandlesticks: this.deliveryCandlesticks.bind(this), deliveryTicker: this.deliveryTickerStream.bind(this), deliveryMiniTicker: this.deliveryMiniTickerStream.bind(this), deliveryMarkPrice: this.deliveryMarkPriceStream.bind(this), deliveryBookTicker: this.deliveryBookTickerStream.bind(this), deliveryChart: this.deliveryChart.bind(this), deliveryLiquidation: this.deliveryLiquidationStream.bind(this), futuresSubcriptions: () => this.getFuturesSubscriptions.bind(this), deliverySubcriptions: () => this.getDeliverySubscriptions.bind(this), futuresTerminate: this.futuresTerminate.bind(this), deliveryTerminate: this.deliveryTerminate.bind(this), }; 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.PRIVATEKEY) this.PRIVATEKEY = this.Options.PRIVATEKEY; if (this.Options.PRIVATEKEYPASSWORD) this.PRIVATEKEYPASSWORD = this.Options.PRIVATEKEYPASSWORD; if (this.Options.test) this.test = true; if (this.Options.headers) this.headers = this.Options.Headers; if (this.Options.domain) this.domain = this.Options.domain; } 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; } getSapiUrl() { return this.sapi; } getFapiUrl() { if (this.Options.test) return this.fapiTest; return this.fapi; } getDapiUrl() { if (this.Options.test) return this.dapiTest; return this.dapi; } getCombineStreamUrl() { if (this.Options.test) return this.combineStreamTest; return this.combineStream; } getStreamUrl() { if (this.Options.test) return this.streamTest; return this.stream; } 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) { // let parsedResponse = ''; // try { // parsedResponse = await response.json(); // } catch (e) { // parsedResponse = await response.text(); // } 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) { // opt.url += '?' + this.makeQueryString(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(httpsproxy); } else if (socksproxy) { if (this.Options.verbose) this.Options.log('using socks proxy: ' + socksproxy); reqOptions.agent = new 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 = this.generateSignature(query); 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 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); } async privateSapiRequest(path, data = {}, method = 'GET', noDataInSignature = false) { return await this.signedRequest /**/(this.getSapiUrl() + 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 = this.generateSignature(query); 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; } } generateSignature(query, encode = true) { const secret = this.APISECRET || this.PRIVATEKEY; let signature = ''; if (secret.includes('PRIVATE KEY')) { // if less than the below length, then it can't be RSA key let keyObject; try { const privateKeyObj = { key: secret }; if (this.PRIVATEKEYPASSWORD) { privateKeyObj.passphrase = this.PRIVATEKEYPASSWORD; } keyObject = crypto.createPrivateKey(privateKeyObj); } catch (e) { throw new Error('Invalid private key. Please provide a valid RSA or ED25519 private key. ' + e.toString()); } if (secret.length > 120) { // RSA key signature = crypto .sign('RSA-SHA256', Buffer.from(query), keyObject) .toString('base64'); if (encode) signature = encodeURIComponent(signature); return signature; } else { // Ed25519 key signature = crypto.sign(null, Buffer.from(query), keyObject).toString('base64'); } } else { signature = crypto.createHmac('sha256', this.Options.APISECRET).update(query).digest('hex'); // set the HMAC hash header } return signature; } // --- 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 spot limit 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} side - the side of the order (BUY or SELL) * @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} params - additional buy order flags * @return {promise or undefined} - omitting the callback returns a promise */ async limitOrder(side, symbol, quantity, price, params = {}) { return await this.order('LIMIT', side, symbol, quantity, price, params); } /** * Creates a market buy order using the cost instead of the quantity (eg: 100usd instead of 0.01btc) * @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 marketBuyWithCost(symbol, cost, params = {}) { params.quoteOrderQty = cost; return await this.order('MARKET', 'BUY', symbol, 0, 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); } /** * Creates a market sell order using the cost instead of the quantity (eg: 100usd instead of 0.01btc) * @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 marketSellWithCost(symbol, cost, params = {}) { params.quoteOrderQty = cost; return await this.order('MARKET', 'SELL', symbol, 0, 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 cancelAllOrders(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: string, params: Dict = {}) { // 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.privateSapiRequest(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(wsBind, opened_callback) { wsBind.isAlive = true; if (Object.keys(this.subscriptions).length === 0) { this.socketHeartbeatInterval = setInterval(this.socketHeartbeat, this.heartBeatInterval); } this.subscriptions[wsBind.url] = wsBind; if (typeof opened_callback === 'function') opened_callback(wsBind.url); } /** * 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(wsBind, reconnect, code, reason) { delete this.subscriptions[wsBind.url]; if (this.subscriptions && Object.keys(this.subscriptions).length === 0) { clearInterval(this.socketHeartbeatInterval); } this.Options.log('WebSocket closed: ' + wsBind.url + (code ? ' (' + code + ')' : '') + (reason ? ' ' + reason : '')); if (this.Options.reconnect && wsBind.reconnect && reconnect) { if (wsBind.url && wsBind.url.length === 60) this.Options.log('Account data WebSocket reconnecting...'); else this.Options.log('WebSocket reconnecting: ' + wsBind.url + '...'); 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(wsBind, 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: ' + wsBind.url + (error.code ? ' (' + error.code + ')' : '') + (error.message ? ' ' + error.message : '')); } /** * Called on each socket heartbeat * @return {undefined} */ handleSocketHeartbeat(wsBind) { wsBind.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({ protocol: this.parseProxy(socksproxy)[0], host: this.parseProxy(socksproxy)[1], port: this.parseProxy(socksproxy)[2] }); ws = new WebSocket(this.getStreamUrl() + endpoint, { agent: agent }); } else if (httpsproxy) { const config = url.parse(httpsproxy); const agent = new HttpsProxyAgent(config); if (this.Options.verbose) this.Options.log('using proxy server ' + agent); ws = new WebSocket(this.getStreamUrl() + endpoint, { agent: agent }); } else { ws = new WebSocket(this.getStreamUrl() + 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, ws, opened_callback)); ws.on('pong', this.handleSocketHeartbeat.bind(this, ws)); ws.on('error', this.handleSocketError.bind(this, ws)); ws.on('close', this.handleSocketClose.bind(this, ws, reconnect)); ws.on('message', data => { try { if (this.Options.verbose) this.Options.log('WebSocket data:', data); callback(JSONbig.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 co