node-binance-api
Version:
Binance API for node https://github.com/ccxt/node-binance-api
1,192 lines (1,190 loc) • 245 kB
JavaScript
'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');
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) ;
}
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 = 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.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.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 connect to
* @param {function} callback - the function to call when information