UNPKG

node-binance-us-api

Version:

Binance.US API for node https://github.com/tdeineka/node-binance-us-api

3,261 lines 146 kB
/* ============================================================
 * node-binance-us-api
 * https://github.com/tdeineka/node-binance-us-api
 * ============================================================
 * Copyright 2017-, Jon Eyrick
 * Released under the MIT License
 * ============================================================
 * @module tdeineka/node-binance-us-api
 * @return {object} instance to class object */
let api = function Binance( options = {} ) {
    //'use strict'; // eslint-disable-line no-unused-expressions
    let Binance = this; // eslint-disable-line consistent-this
    
    const WebSocket = require( 'ws' );
    const request = require( 'request' );
    const crypto = require( 'crypto' );
    const file = require( 'fs' );
    const url = require( 'url' );
    const HttpsProxyAgent = require( 'https-proxy-agent' );
    const SocksProxyAgent = require( 'socks-proxy-agent' );
    const stringHash = require( 'string-hash' );
    const async = require( 'async' );
    let base = 'https://api.binance.us/api/';
    let wapi = 'https://api.binance.us/wapi/';
    let sapi = 'https://api.binance.us/sapi/';
    let fapi = 'https://fapi.binance.us/fapi/';
    let stream = 'wss://stream.binance.us:9443/ws/';
    let combineStream = 'wss://stream.binance.us:9443/stream?streams=';
    const userAgent = 'Mozilla/4.0 (compatible; Node Binance.US API)';
    const contentType = 'application/x-www-form-urlencoded';
    Binance.subscriptions = {};
    Binance.depthCache = {};
    Binance.depthCacheContext = {};
    Binance.ohlcLatest = {};
    Binance.klineQueue = {};
    Binance.ohlc = {};
    const default_options = {
        recvWindow: 5000,
        useServerTime: false,
        reconnect: true,
        verbose: false,
        test: false,
        log: function ( ...args ) {
            console.log( Array.prototype.slice.call( args ) );
        }
    };
    Binance.options = default_options;
    Binance.info = { timeOffset: 0 };
    Binance.socketHeartbeatInterval = null;
    if ( options ) setOptions( options );

    function setOptions( opt = {}, callback = false ) {
        if ( typeof opt === 'string' ) { // Pass json config filename
            Binance.options = JSON.parse( file.readFileSync( opt ) );
        } else Binance.options = opt;
        if ( typeof Binance.options.recvWindow === 'undefined' ) Binance.options.recvWindow = default_options.recvWindow;
        if ( typeof Binance.options.useServerTime === 'undefined' ) Binance.options.useServerTime = default_options.useServerTime;
        if ( typeof Binance.options.reconnect === 'undefined' ) Binance.options.reconnect = default_options.reconnect;
        if ( typeof Binance.options.test === 'undefined' ) Binance.options.test = default_options.test;
        if ( typeof Binance.options.log === 'undefined' ) Binance.options.log = default_options.log;
        if ( typeof Binance.options.verbose === 'undefined' ) Binance.options.verbose = default_options.verbose;
        if ( typeof Binance.options.urls !== 'undefined' ) {
            const { urls } = Binance.options;
            if( typeof urls.base === 'string' ) base = urls.base;
            if( typeof urls.wapi === 'string' ) wapi = urls.wapi;
            if( typeof urls.sapi === 'string' ) sapi = urls.sapi;
            if( typeof urls.fapi === 'string' ) fapi = urls.fapi;
            if( typeof urls.stream === 'string' ) stream = urls.stream;
            if( typeof urls.combineStream === 'string' ) combineStream = urls.combineStream;
        }
        if ( Binance.options.useServerTime ) {
            apiRequest( base + 'v1/time', {}, function ( error, response ) {
                Binance.info.timeOffset = response.serverTime - new Date().getTime();
                //Binance.options.log("server time set: ", response.serverTime, Binance.info.timeOffset);
                if ( callback ) callback();
            } );
        } else if ( callback ) callback();
        return this;
    }

    /**
     * Replaces socks connection uri hostname with IP address
     * @param {string} connString - socks connection string
     * @return {string} modified string with ip address
     */
    const proxyReplacewithIp = connString => {
        return connString;
    }

    /**
     * Returns an array in the form of [host, port]
     * @param {string} connString - connection string
     * @return {array} array of host and port
     */
    const parseProxy = connString => {
        let arr = connString.split( '/' );
        let host = arr[2].split( ':' )[0];
        let port = arr[2].split( ':' )[1];
        return [arr[0], host, port];
    }

    /**
     * Checks to see of the object is iterable
     * @param {object} obj - The object check
     * @return {boolean} true or false is iterable
     */
    const isIterable = obj => {
        if ( obj === null ) return false;
        return typeof obj[Symbol.iterator] === 'function';
    }

    const addProxy = opt => {
        if ( Binance.options.proxy ) {
            const proxyauth = Binance.options.proxy.auth ? `${Binance.options.proxy.auth.username}:${Binance.options.proxy.auth.password}@` : '';
            opt.proxy = `http://${proxyauth}${Binance.options.proxy.host}:${Binance.options.proxy.port}`;
        }
        return opt;
    }

    const reqHandler = cb => ( error, response, body ) => {
        if ( !cb ) return;
        if ( error ) return cb( error, {} );
        if ( response && response.statusCode !== 200 ) return cb( response, {} );
        return cb( null, JSON.parse( body ) );
    }

    const proxyRequest = ( opt, cb ) => request( addProxy( opt ), reqHandler( cb ) );

    const reqObj = ( url, data = {}, method = 'GET', key ) => ( {
        url: url,
        qs: data,
        method: method,
        timeout: Binance.options.recvWindow,
        headers: {
            'User-Agent': userAgent,
            'Content-type': contentType,
            'X-MBX-APIKEY': key || ''
        }
    } )
    const reqObjPOST = ( url, data = {}, method = 'POST', key ) => ( {
        url: url,
        form: data,
        method: method,
        timeout: Binance.options.recvWindow,
        headers: {
            'User-Agent': userAgent,
            'Content-type': contentType,
            'X-MBX-APIKEY': key || ''
        }
    } )
    /**
     * Create a http request to the public API
     * @param {string} url - The http endpoint
     * @param {object} data - The data to send
     * @param {function} callback - The callback method to call
     * @param {string} method - the http method
     * @return {undefined}
     */
    const publicRequest = ( url, data = {}, callback, method = 'GET' ) => {
        let opt = reqObj( url, data, method );
        proxyRequest( opt, callback );
    };

    const makeQueryString = q => Object.keys( q ).reduce( ( a,k )=>{ if ( q[k] !== undefined ) { a.push( k+'='+encodeURIComponent( q[k] ) ) } return a }, [] ).join( '&' );

    /**
     * Create a http request to the public API
     * @param {string} url - The http endpoint
     * @param {object} data - The data to send
     * @param {function} callback - The callback method to call
     * @param {string} method - the http method
     * @return {undefined}
     */
    const apiRequest = ( url, data = {}, callback, method = 'GET' ) => {
        if ( !Binance.options.APIKEY ) throw Error( 'apiRequest: Invalid API Key' );
        let opt = reqObj(
            url,
            data,
            method,
            Binance.options.APIKEY
        );
        proxyRequest( opt, callback );
    };

    /**
     * Make market request
     * @param {string} url - The http endpoint
     * @param {object} data - The data to send
     * @param {function} callback - The callback method to call
     * @param {string} method - the http method
     * @return {undefined}
     */
    const marketRequest = ( url, data = {}, callback, method = 'GET' ) => {
        if ( !Binance.options.APIKEY ) throw Error( 'apiRequest: Invalid API Key' );
        let query = makeQueryString( data );
        let opt = reqObj(
            url + ( query ? '?' + query : '' ),
            data,
            method,
            Binance.options.APIKEY
        );
        proxyRequest( opt, callback );
    };

    /**
     * Create a signed http request
     * @param {string} url - The http endpoint
     * @param {object} data - The data to send
     * @param {function} callback - The callback method to call
     * @param {string} method - the http method
     * @param {boolean} noDataInSignature - Prevents data from being added to signature
     * @return {undefined}
     */
    const signedRequest = ( url, data = {}, callback, method = 'GET', noDataInSignature = false ) => {
        if ( !Binance.options.APIKEY ) throw Error( 'apiRequest: Invalid API Key' );
        if ( !Binance.options.APISECRET ) throw Error( 'signedRequest: Invalid API Secret' );
        data.timestamp = new Date().getTime() + Binance.info.timeOffset;
        if ( typeof data.recvWindow === 'undefined' ) data.recvWindow = Binance.options.recvWindow;
        let query = method === 'POST' && noDataInSignature ? '' : makeQueryString( data );
        let signature = crypto.createHmac( 'sha256', Binance.options.APISECRET ).update( query ).digest( 'hex' ); // set the HMAC hash header
        if ( method === 'POST' ) {
            let opt = reqObjPOST(
                url + '?signature=' + signature,
                data,
                method,
                Binance.options.APIKEY
            );
            proxyRequest( opt, callback );
        } else {
            let opt = reqObj(
                url + '?' + query + '&signature=' + signature,
                data,
                method,
                Binance.options.APIKEY
            );
            proxyRequest( opt, callback );
        }
    };

    /**
     * Create a signed http request
     * @param {string} side - BUY or SELL
     * @param {string} symbol - The symbol to buy or sell
     * @param {string} quantity - The quantity to buy or sell
     * @param {string} price - The price per unit to transact each unit at
     * @param {object} flags - additional order settings
     * @param {function} callback - the callback function
     * @return {undefined}
     */
    const order = ( side, symbol, quantity, price, flags = {}, callback = false ) => {
        let endpoint = flags.type === 'OCO' ? 'v3/order/oco' : 'v3/order';
        if ( Binance.options.test ) endpoint += '/test';
        let opt = {
            symbol: symbol,
            side: side,
            type: 'LIMIT',
            quantity: quantity
        };
        if ( typeof flags.type !== 'undefined' ) opt.type = flags.type;
        if ( opt.type.includes( 'LIMIT' ) ) {
            opt.price = price;
            if ( opt.type !== 'LIMIT_MAKER' ) {
                opt.timeInForce = 'GTC';
            }
        }
        if ( opt.type === 'OCO' ) {
            opt.price = price;
            opt.stopLimitPrice = flags.stopLimitPrice;
            opt.stopLimitTimeInForce = 'GTC';
            delete opt.type;
        }
        if ( typeof flags.timeInForce !== 'undefined' ) opt.timeInForce = flags.timeInForce;
        if ( typeof flags.newOrderRespType !== 'undefined' ) opt.newOrderRespType = flags.newOrderRespType;
        if ( typeof flags.newClientOrderId !== 'undefined' ) opt.newClientOrderId = flags.newClientOrderId;

        /*
         * STOP_LOSS
         * STOP_LOSS_LIMIT
         * TAKE_PROFIT
         * TAKE_PROFIT_LIMIT
         * LIMIT_MAKER
         */
        if ( typeof flags.icebergQty !== 'undefined' ) opt.icebergQty = flags.icebergQty;
        if ( typeof flags.stopPrice !== 'undefined' ) {
            opt.stopPrice = flags.stopPrice;
            if ( opt.type === 'LIMIT' ) throw Error( 'stopPrice: Must set "type" to one of the following: STOP_LOSS, STOP_LOSS_LIMIT, TAKE_PROFIT, TAKE_PROFIT_LIMIT' );
        }
        signedRequest( base + endpoint, opt, ( error, response ) => {
            if ( !response ) {
                if ( callback ) callback( error, response );
                else Binance.options.log( 'Order() error:', error );
                return;
            }
            if ( typeof response.msg !== 'undefined' && response.msg === 'Filter failure: MIN_NOTIONAL' ) {
                Binance.options.log( 'Order quantity too small. See exchangeInfo() for minimum amounts' );
            }
            if ( callback ) callback( error, response );
            else Binance.options.log( side + '(' + symbol + ',' + quantity + ',' + price + ') ', response );
        }, 'POST' );
    };

    /**
     * Create a signed http request
     * @param {string} side - BUY or SELL
     * @param {string} symbol - The symbol to buy or sell
     * @param {string} quantity - The quantity to buy or sell
     * @param {string} price - The price per unit to transact each unit at
     * @param {object} flags - additional order settings
     * @param {function} callback - the callback function
     * @return {undefined}
     */
    const marginOrder = ( side, symbol, quantity, price, flags = {}, callback = false ) => {
        let endpoint = 'v1/margin/order';
        if ( Binance.options.test ) endpoint += '/test';
        let opt = {
            symbol: symbol,
            side: side,
            type: 'LIMIT',
            quantity: quantity
        };
        if ( typeof flags.type !== 'undefined' ) opt.type = flags.type;
        if ( opt.type.includes( 'LIMIT' ) ) {
            opt.price = price;
            if ( opt.type !== 'LIMIT_MAKER' ) {
                opt.timeInForce = 'GTC';
            }
        }

        if ( typeof flags.timeInForce !== 'undefined' ) opt.timeInForce = flags.timeInForce;
        if ( typeof flags.newOrderRespType !== 'undefined' ) opt.newOrderRespType = flags.newOrderRespType;
        if ( typeof flags.newClientOrderId !== 'undefined' ) opt.newClientOrderId = flags.newClientOrderId;

        /*
         * STOP_LOSS
         * STOP_LOSS_LIMIT
         * TAKE_PROFIT
         * TAKE_PROFIT_LIMIT
         */
        if ( typeof flags.icebergQty !== 'undefined' ) opt.icebergQty = flags.icebergQty;
        if ( typeof flags.stopPrice !== 'undefined' ) {
            opt.stopPrice = flags.stopPrice;
            if ( opt.type === 'LIMIT' ) throw Error( 'stopPrice: Must set "type" to one of the following: STOP_LOSS, STOP_LOSS_LIMIT, TAKE_PROFIT, TAKE_PROFIT_LIMIT' );
        }
        signedRequest( sapi + endpoint, opt, function ( error, response ) {
            if ( !response ) {
                if ( callback ) callback( error, response );
                else Binance.options.log( 'Order() error:', error );
                return;
            }
            if ( typeof response.msg !== 'undefined' && response.msg === 'Filter failure: MIN_NOTIONAL' ) {
                Binance.options.log( 'Order quantity too small. See exchangeInfo() for minimum amounts' );
            }
            if ( callback ) callback( error, response );
            else Binance.options.log( side + '(' + symbol + ',' + quantity + ',' + price + ') ', response );
        }, 'POST' );
    };

    // Futures internal functions
    const futuresOrder = async ( side, symbol, quantity, price = false, params = {} ) => {
        params.symbol = symbol;
        params.side = side;
        params.quantity = quantity;
        // LIMIT STOP MARKET STOP_MARKET TAKE_PROFIT TAKE_PROFIT_MARKET
        // reduceOnly stopPrice
        if ( price ) {
            params.price = price;
            if ( typeof params.type === 'undefined' ) params.type = 'LIMIT';
        } else {
            if ( typeof params.type === 'undefined' ) params.type = 'MARKET';
        }
        if ( params.type.includes( 'LIMIT' ) || params.type === 'STOP' || params.type === 'TAKE_PROFIT' ) params.timeInForce = 'GTC';
        return promiseRequest( 'v1/order', params, {base:fapi, type:'TRADE', method:'POST'} );
    };
    const promiseRequest = async ( url, data = {}, flags = {} ) => {
        return new Promise( ( resolve, reject ) => {
            let query = '', headers = {
                'User-Agent': userAgent,
                'Content-type': 'application/x-www-form-urlencoded'
            };
            if ( typeof flags.method === 'undefined' ) flags.method = 'GET'; // GET POST PUT DELETE
            if ( typeof flags.type === 'undefined' ) flags.type = false; // TRADE, SIGNED, MARKET_DATA, USER_DATA, USER_STREAM
            else {
                if ( typeof data.recvWindow === 'undefined' ) data.recvWindow = Binance.options.recvWindow;
                headers['X-MBX-APIKEY'] = Binance.options.APIKEY;
                if ( !Binance.options.APIKEY ) return reject( 'Invalid API Key' );
            }
            let baseURL = typeof flags.base === 'undefined' ? base : flags.base;
            let opt = {
                headers,
                url: baseURL + url,
                method: flags.method,
                timeout: Binance.options.recvWindow,
                followAllRedirects: true
            };
            if ( flags.type === 'SIGNED' || flags.type === 'TRADE' || flags.type === 'USER_DATA' ) {
                if ( !Binance.options.APISECRET ) return reject( 'Invalid API Secret' );
                data.timestamp = new Date().getTime() + Binance.info.timeOffset;
                query = makeQueryString( data );
                data.signature = crypto.createHmac( 'sha256', Binance.options.APISECRET ).update( query ).digest( 'hex' ); // HMAC hash header
                opt.url = `${baseURL}${url}?${query}&signature=${data.signature}`;
            }
            opt.qs = data;
            /*if ( flags.method === 'POST' ) {
                opt.form = data;
            } else {
                opt.qs = data;
            }*/
            try {
                request( addProxy( opt ), ( error, response, body ) => {
                    if ( error ) return reject( error );
                    try {
                        if ( !error && response.statusCode == 200 ) return resolve( JSON.parse( body ) );
                        if ( typeof response.body !== 'undefined' ) {
                            return resolve( JSON.parse( response.body ) );
                        }
                        return reject( response );
                    } catch ( err ) {
                        return reject( `promiseRequest error #${response.statusCode}` );
                    }
                } );
            } catch ( err ) {
                return reject( err );
            }
        } );
    };

    /**
     * No-operation function
     * @return {undefined}
     */
    const noop = () => {
        // Do nothing
    };

    /**
     * Reworked Tuitio's heartbeat code into a shared single interval tick
     * @return {undefined}
     */
    const socketHeartbeat = () => {
        /* Sockets removed from `subscriptions` during a manual terminate()
         will no longer be at risk of having functions called on them */
        for ( let endpointId in Binance.subscriptions ) {
            const ws = Binance.subscriptions[endpointId];
            if ( ws.isAlive ) {
                ws.isAlive = false;
                if ( ws.readyState === WebSocket.OPEN ) ws.ping( noop );
            } else {
                if ( Binance.options.verbose ) Binance.options.log( 'Terminating inactive/broken WebSocket: ' + ws.endpoint );
                if ( ws.readyState === WebSocket.OPEN ) ws.terminate();
            }
        }
    };

    /**
     * Called when socket is opened, subscriptions are registered for later reference
     * @param {function} opened_callback - a callback function
     * @return {undefined}
     */
    const handleSocketOpen = function ( opened_callback ) {
        this.isAlive = true;
        if ( Object.keys( Binance.subscriptions ).length === 0 ) {
            Binance.socketHeartbeatInterval = setInterval( socketHeartbeat, 30000 );
        }
        Binance.subscriptions[this.endpoint] = this;
        if ( typeof opened_callback === 'function' ) opened_callback( this.endpoint );
    };

    /**
     * Called when socket is closed, subscriptions are de-registered for later reference
     * @param {boolean} reconnect - true or false to reconnect the socket
     * @param {string} code - code associated with the socket
     * @param {string} reason - string with the response
     * @return {undefined}
     */
    const handleSocketClose = function ( reconnect, code, reason ) {
        delete Binance.subscriptions[this.endpoint];
        if ( Binance.subscriptions && Object.keys( Binance.subscriptions ).length === 0 ) {
            clearInterval( Binance.socketHeartbeatInterval );
        }
        Binance.options.log( 'WebSocket closed: ' + this.endpoint +
          ( code ? ' (' + code + ')' : '' ) +
          ( reason ? ' ' + reason : '' ) );
        if ( Binance.options.reconnect && this.reconnect && reconnect ) {
            if ( this.endpoint && parseInt( this.endpoint.length, 10 ) === 60 ) Binance.options.log( 'Account data WebSocket reconnecting...' );
            else Binance.options.log( 'WebSocket reconnecting: ' + this.endpoint + '...' );
            try {
                reconnect();
            } catch ( error ) {
                Binance.options.log( 'WebSocket reconnect error: ' + error.message );
            }
        }
    };

    /**
     * Called when socket errors
     * @param {object} error - error object message
     * @return {undefined}
     */
    const handleSocketError = function ( error ) {
        /* Errors ultimately result in a `close` event.
         see: https://github.com/websockets/ws/blob/828194044bf247af852b31c49e2800d557fedeff/lib/websocket.js#L126 */
        Binance.options.log( 'WebSocket error: ' + this.endpoint +
          ( error.code ? ' (' + error.code + ')' : '' ) +
          ( error.message ? ' ' + error.message : '' ) );
    };

    /**
     * Called on each socket heartbeat
     * @return {undefined}
     */
    const handleSocketHeartbeat = function () {
        this.isAlive = true;
    };

    /**
     * Used to subscribe to a single websocket endpoint
     * @param {string} endpoint - endpoint to connect to
     * @param {function} callback - the function to call when information is received
     * @param {boolean} reconnect - whether to reconnect on disconnect
     * @param {object} opened_callback - the function to call when opened
     * @return {WebSocket} - websocket reference
     */
    const subscribe = function ( endpoint, callback, reconnect = false, opened_callback = false ) {
        let httpsproxy = process.env.https_proxy || false;
        let socksproxy = process.env.socks_proxy || false;
        let ws = false;

        if ( socksproxy !== false ) {
            socksproxy = proxyReplacewithIp( socksproxy );
            if ( Binance.options.verbose ) Binance.options.log( 'using socks proxy server ' + socksproxy );
            let agent = new SocksProxyAgent( {
                protocol: parseProxy( socksproxy )[0],
                host: parseProxy( socksproxy )[1],
                port: parseProxy( socksproxy )[2]
            } );
            ws = new WebSocket( stream + endpoint, { agent: agent } );
        } else if ( httpsproxy !== false ) {
            if ( Binance.options.verbose ) Binance.options.log( 'using proxy server ' + agent );
            let config = url.parse( httpsproxy );
            let agent = new HttpsProxyAgent( config );
            ws = new WebSocket( stream + endpoint, { agent: agent } );
        } else {
            ws = new WebSocket( stream + endpoint );
        }

        if ( Binance.options.verbose ) Binance.options.log( 'Subscribed to ' + endpoint );
        ws.reconnect = Binance.options.reconnect;
        ws.endpoint = endpoint;
        ws.isAlive = false;
        ws.on( 'open', handleSocketOpen.bind( ws, opened_callback ) );
        ws.on( 'pong', handleSocketHeartbeat );
        ws.on( 'error', handleSocketError );
        ws.on( 'close', handleSocketClose.bind( ws, reconnect ) );
        ws.on( 'message', data => {
            try {
                callback( JSON.parse( data ) );
            } catch ( error ) {
                Binance.options.log( 'Parse error: ' + error.message );
            }
        } );
        return ws;
    };

    /**
     * Used to subscribe to a combined websocket endpoint
     * @param {string} streams - streams to connect to
     * @param {function} callback - the function to call when information is received
     * @param {boolean} reconnect - whether to reconnect on disconnect
     * @param {object} opened_callback - the function to call when opened
     * @return {WebSocket} - websocket reference
     */
    const subscribeCombined = function ( streams, callback, reconnect = false, opened_callback = false ) {
        let httpsproxy = process.env.https_proxy || false;
        let socksproxy = process.env.socks_proxy || false;
        const queryParams = streams.join( '/' );
        let ws = false;
        if ( socksproxy !== false ) {
            socksproxy = proxyReplacewithIp( socksproxy );
            if ( Binance.options.verbose ) Binance.options.log( 'using socks proxy server ' + socksproxy );
            let agent = new SocksProxyAgent( {
                protocol: parseProxy( socksproxy )[0],
                host: parseProxy( socksproxy )[1],
                port: parseProxy( socksproxy )[2]
            } );
            ws = new WebSocket( combineStream + queryParams, { agent: agent } );
        } else if ( httpsproxy !== false ) {
            if ( Binance.options.verbose ) Binance.options.log( 'using proxy server ' + httpsproxy );
            let config = url.parse( httpsproxy );
            let agent = new HttpsProxyAgent( config );
            ws = new WebSocket( combineStream + queryParams, { agent: agent } );
        } else {
            ws = new WebSocket( combineStream + queryParams );
        }

        ws.reconnect = Binance.options.reconnect;
        ws.endpoint = stringHash( queryParams );
        ws.isAlive = false;
        if ( Binance.options.verbose ) {
            Binance.options.log( 'CombinedStream: Subscribed to [' + ws.endpoint + '] ' + queryParams );
        }
        ws.on( 'open', handleSocketOpen.bind( ws, opened_callback ) );
        ws.on( 'pong', handleSocketHeartbeat );
        ws.on( 'error', handleSocketError );
        ws.on( 'close', handleSocketClose.bind( ws, reconnect ) );
        ws.on( 'message', data => {
            try {
                callback( JSON.parse( data ).data );
            } catch ( error ) {
                Binance.options.log( 'CombinedStream: Parse error: ' + error.message );
            }
        } );
        return ws;
    };

    /**
     * Used to terminate a web socket
     * @param {string} endpoint - endpoint identifier associated with the web socket
     * @param {boolean} reconnect - auto reconnect after termination
     * @return {undefined}
     */
    const terminate = function ( endpoint, reconnect = false ) {
        let ws = Binance.subscriptions[endpoint];
        if ( !ws ) return;
        ws.removeAllListeners( 'message' );
        ws.reconnect = reconnect;
        ws.terminate();
    }

    /**
     * Used as part of the user data websockets callback
     * @param {object} data - user data callback data type
     * @return {undefined}
     */
    const userDataHandler = data => {
        let type = data.e;
        if ( type === 'outboundAccountInfo' ) {
            Binance.options.balance_callback( data );
        } else if ( type === 'executionReport' ) {
            if ( Binance.options.execution_callback ) Binance.options.execution_callback( data );
        } else if ( type === 'listStatus' ) {
            if ( Binance.options.list_status_callback ) Binance.options.list_status_callback( data );
        } else if ( type === 'outboundAccountPosition' ) {
            // TODO: Does this mean something?
        } else {
            Binance.options.log( 'Unexpected userData: ' + type );
        }
    };

    /**
     * Used as part of the user data websockets callback
     * @param {object} data - user data callback data type
     * @return {undefined}
     */
    const userMarginDataHandler = data => {
        let type = data.e;
        if ( type === 'outboundAccountInfo' ) {
            Binance.options.margin_balance_callback( data );
        } else if ( type === 'executionReport' ) {
            if ( Binance.options.margin_execution_callback ) Binance.options.margin_execution_callback( data );
        } else if ( type === 'listStatus' ) {
            if ( Binance.options.margin_list_status_callback ) Binance.options.margin_list_status_callback( data );
        } else if ( type === 'outboundAccountPosition' ) {
            // TODO: Does this mean something?
        } else {
            Binance.options.log( 'Unexpected userMarginData: ' + type );
        }
    };

    /**
     * Converts the previous day stream into friendly object
     * @param {object} data - user data callback data type
     * @return {object} - user friendly data type
     */
    const prevDayConvertData = data => {
        let convertData = data => {
            let {
                e: eventType,
                E: eventTime,
                s: symbol,
                p: priceChange,
                P: percentChange,
                w: averagePrice,
                x: prevClose,
                c: close,
                Q: closeQty,
                b: bestBid,
                B: bestBidQty,
                a: bestAsk,
                A: bestAskQty,
                o: open,
                h: high,
                l: low,
                v: volume,
                q: quoteVolume,
                O: openTime,
                C: closeTime,
                F: firstTradeId,
                L: lastTradeId,
                n: numTrades
            } = data;
            return {
                eventType,
                eventTime,
                symbol,
                priceChange,
                percentChange,
                averagePrice,
                prevClose,
                close,
                closeQty,
                bestBid,
                bestBidQty,
                bestAsk,
                bestAskQty,
                open,
                high,
                low,
                volume,
                quoteVolume,
                openTime,
                closeTime,
                firstTradeId,
                lastTradeId,
                numTrades
            };
        }
        if ( Array.isArray( data ) ) {
            const result = [];
            for ( let obj of data ) {
                let converted = convertData( obj );
                result.push( converted );
            }
            return result;
            // eslint-disable-next-line no-else-return
        } else {
            return convertData( data );
        }
    }

    /**
     * Parses the previous day stream and calls the user callback with friendly object
     * @param {object} data - user data callback data type
     * @param {function} callback - user data callback data type
     * @return {undefined}
     */
    const prevDayStreamHandler = ( data, callback ) => {
        const converted = prevDayConvertData( data );
        callback( null, converted );
    };

    /**
     * Gets the price of a given symbol or symbols
     * @param {array} data - array of symbols
     * @return {array} - symbols with their current prices
     */
    const priceData = ( data ) => {
        const prices = {};
        if ( Array.isArray( data ) ) {
            for ( let obj of data ) {
                prices[obj.symbol] = obj.price;
            }
        } else { // Single price returned
            prices[data.symbol] = data.price;
        }
        return prices;
    };

    /**
     * Used by bookTickers to format the bids and asks given given symbols
     * @param {array} data - array of symbols
     * @return {object} - symbols with their bids and asks data
     */
    const bookPriceData = data => {
        let prices = {};
        for ( let obj of data ) {
            prices[obj.symbol] = {
                bid: obj.bidPrice,
                bids: obj.bidQty,
                ask: obj.askPrice,
                asks: obj.askQty
            };
        }
        return prices;
    };

    /**
     * Used by balance to get the balance data
     * @param {array} data - account info object
     * @return {object} - balances hel with available, onorder amounts
     */
    const balanceData = data => {
        let balances = {};
        if ( typeof data === 'undefined' ) return {};
        if ( typeof data.balances === 'undefined' ) {
            Binance.options.log( 'balanceData error', data );
            return {};
        }
        for ( let obj of data.balances ) {
            balances[obj.asset] = { available: obj.free, onOrder: obj.locked };
        }
        return balances;
    };

    /**
     * Used by web sockets depth and populates OHLC and info
     * @param {string} symbol - symbol to get candlestick info
     * @param {string} interval - time interval, 1m, 3m, 5m ....
     * @param {array} ticks - tick array
     * @return {undefined}
     */
    const klineData = ( symbol, interval, ticks ) => { // Used for /depth
        let last_time = 0;
        if ( isIterable( ticks ) ) {
            for ( let tick of ticks ) {
                // eslint-disable-next-line no-unused-vars
                let [time, open, high, low, close, volume, closeTime, assetVolume, trades, buyBaseVolume, buyAssetVolume, ignored] = tick;
                Binance.ohlc[symbol][interval][time] = { open: open, high: high, low: low, close: close, volume: volume };
                last_time = time;
            }

            Binance.info[symbol][interval].timestamp = last_time;
        }
    };

    /**
     * Combines all OHLC data with latest update
     * @param {string} symbol - the symbol
     * @param {string} interval - time interval, 1m, 3m, 5m ....
     * @return {array} - interval data for given symbol
     */
    const klineConcat = ( symbol, interval ) => {
        let output = Binance.ohlc[symbol][interval];
        if ( typeof Binance.ohlcLatest[symbol][interval].time === 'undefined' ) return output;
        const time = Binance.ohlcLatest[symbol][interval].time;
        const last_updated = Object.keys( Binance.ohlc[symbol][interval] ).pop();
        if ( time >= last_updated ) {
            output[time] = Binance.ohlcLatest[symbol][interval];
            delete output[time].time;
            output[time].isFinal = false;
        }
        return output;
    };

    /**
     * Used for websocket @kline
     * @param {string} symbol - the symbol
     * @param {object} kline - object with kline info
     * @param {string} firstTime - time filter
     * @return {undefined}
     */
    const klineHandler = ( symbol, kline, firstTime = 0 ) => {
        // TODO: add Taker buy base asset volume
        // eslint-disable-next-line no-unused-vars
        let { e: eventType, E: eventTime, k: ticks } = kline;
        // eslint-disable-next-line no-unused-vars
        let { o: open, h: high, l: low, c: close, v: volume, i: interval, x: isFinal, q: quoteVolume, t: time } = ticks; //n:trades, V:buyVolume, Q:quoteBuyVolume
        if ( time <= firstTime ) return;
        if ( !isFinal ) {
            if ( typeof Binance.ohlcLatest[symbol][interval].time !== 'undefined' ) {
                if ( Binance.ohlcLatest[symbol][interval].time > time ) return;
            }
            Binance.ohlcLatest[symbol][interval] = { open: open, high: high, low: low, close: close, volume: volume, time: time };
            return;
        }
        // Delete an element from the beginning so we don't run out of memory
        const first_updated = Object.keys( Binance.ohlc[symbol][interval] ).shift();
        if ( first_updated ) delete Binance.ohlc[symbol][interval][first_updated];
        Binance.ohlc[symbol][interval][time] = { open: open, high: high, low: low, close: close, volume: volume };
    };

    /**
     * Used for /depth endpoint
     * @param {object} data - containing the bids and asks
     * @return {undefined}
     */
    const depthData = data => {
        if ( !data ) return { bids: [], asks: [] };
        let bids = {}, asks = {}, obj;
        if ( typeof data.bids !== 'undefined' ) {
            for ( obj of data.bids ) {
                bids[obj[0]] = parseFloat( obj[1] );
            }
        }
        if ( typeof data.asks !== 'undefined' ) {
            for ( obj of data.asks ) {
                asks[obj[0]] = parseFloat( obj[1] );
            }
        }
        return { lastUpdateId: data.lastUpdateId, bids: bids, asks: asks };
    }

    /**
     * Used for /depth endpoint
     * @param {object} depth - information
     * @return {undefined}
     */
    const depthHandler = depth => {
        let symbol = depth.s, obj;
        let context = Binance.depthCacheContext[symbol];
        let updateDepthCache = () => {
            Binance.depthCache[symbol].eventTime = depth.E;
            for ( obj of depth.b ) { //bids
                if ( obj[1] === '0.00000000' ) {
                    delete Binance.depthCache[symbol].bids[obj[0]];
                } else {
                    Binance.depthCache[symbol].bids[obj[0]] = parseFloat( obj[1] );
                }
            }
            for ( obj of depth.a ) { //asks
                if ( obj[1] === '0.00000000' ) {
                    delete Binance.depthCache[symbol].asks[obj[0]];
                } else {
                    Binance.depthCache[symbol].asks[obj[0]] = parseFloat( obj[1] );
                }
            }
            context.skipCount = 0;
            context.lastEventUpdateId = depth.u;
            context.lastEventUpdateTime = depth.E;
        };

        // This now conforms 100% to the Binance docs constraints on managing a local order book
        if ( context.lastEventUpdateId ) {
            const expectedUpdateId = context.lastEventUpdateId + 1;
            if ( depth.U <= expectedUpdateId ) {
                updateDepthCache();
            } else {
                let msg = 'depthHandler: [' + symbol + '] The depth cache is out of sync.';
                msg += ' Symptom: Unexpected Update ID. Expected "' + expectedUpdateId + '", got "' + depth.U + '"';
                if ( Binance.options.verbose ) Binance.options.log( msg );
                throw new Error( msg );
            }
        } else if ( depth.U > context.snapshotUpdateId + 1 ) {
            /* In this case we have a gap between the data of the stream and the snapshot.
             This is an out of sync error, and the connection must be torn down and reconnected. */
            let msg = 'depthHandler: [' + symbol + '] The depth cache is out of sync.';
            msg += ' Symptom: Gap between snapshot and first stream data.';
            if ( Binance.options.verbose ) Binance.options.log( msg );
            throw new Error( msg );
        } else if ( depth.u < context.snapshotUpdateId + 1 ) {
            /* In this case we've received data that we've already had since the snapshot.
             This isn't really an issue, and we can just update the cache again, or ignore it entirely. */

            // do nothing
        } else {
            // This is our first legal update from the stream data
            updateDepthCache();
        }
    };

    /**
     * Gets depth cache for given symbol
     * @param {string} symbol - the symbol to fetch
     * @return {object} - the depth cache object
     */
    const getDepthCache = symbol => {
        if ( typeof Binance.depthCache[symbol] === 'undefined' ) return { bids: {}, asks: {} };
        return Binance.depthCache[symbol];
    };

    /**
     * Calculate Buy/Sell volume from DepthCache
     * @param {string} symbol - the symbol to fetch
     * @return {object} - the depth volume cache object
     */
    const depthVolume = symbol => {
        let cache = getDepthCache( symbol ), quantity, price;
        let bidbase = 0, askbase = 0, bidqty = 0, askqty = 0;
        for ( price in cache.bids ) {
            quantity = cache.bids[price];
            bidbase += parseFloat( ( quantity * parseFloat( price ) ).toFixed( 8 ) );
            bidqty += quantity;
        }
        for ( price in cache.asks ) {
            quantity = cache.asks[price];
            askbase += parseFloat( ( quantity * parseFloat( price ) ).toFixed( 8 ) );
            askqty += quantity;
        }
        return { bids: bidbase, asks: askbase, bidQty: bidqty, askQty: askqty };
    };

    /**
     * Checks whether or not an array contains any duplicate elements
     * @param {array} array - the array to check
     * @return {boolean} - true or false
     */
    const isArrayUnique = array => {
        let s = new Set( array );
        return s.size === array.length;
    };
    return {
        /**
        * Gets depth cache for given symbol
        * @param {symbol} symbol - get depch cache for this symbol
        * @return {object} - object
        */
        depthCache: symbol => {
            return getDepthCache( symbol );
        },

        /**
        * Gets depth volume for given symbol
        * @param {symbol} symbol - get depch volume for this symbol
        * @return {object} - object
        */
        depthVolume: symbol => {
            return depthVolume( symbol );
        },

        /**
        * Count decimal places
        * @param {float} float - get the price precision point
        * @return {int} - number of place
        */
        getPrecision: function ( float ) {
            if ( !float || Number.isInteger( float ) ) return 0;
            return float.toString().split( '.' )[1].length || 0;
        },

        /**
        * rounds number with given step
        * @param {float} qty - quantity to round
        * @param {float} stepSize - stepSize as specified by exchangeInfo
        * @return {float} - number
        */
        roundStep: function ( qty, stepSize ) {
            // Integers do not require rounding
            if ( Number.isInteger( qty ) ) return qty;
            const qtyString = qty.toFixed( 16 );
            const desiredDecimals = Math.max( stepSize.indexOf( '1' ) - 1, 0 );
            const decimalIndex = qtyString.indexOf( '.' );
            return parseFloat( qtyString.slice( 0, decimalIndex + desiredDecimals + 1 ) );
        },

        /**
        * rounds price to required precision
        * @param {float} price - price to round
        * @param {float} tickSize - tickSize as specified by exchangeInfo
        * @return {float} - number
        */
        roundTicks: function ( price, tickSize ) {
            const formatter = new Intl.NumberFormat( 'en-US', { style: 'decimal', minimumFractionDigits: 0, maximumFractionDigits: 8 } );
            const precision = formatter.format( tickSize ).split( '.' )[1].length || 0;
            if ( typeof price === 'string' ) price = parseFloat( price );
            return price.toFixed( precision );
        },

        /**
        * Gets percentage of given numbers
        * @param {float} min - the smaller number
        * @param {float} max - the bigger number
        * @param {int} width - percentage width
        * @return {float} - percentage
        */
        percent: function ( min, max, width = 100 ) {
            return ( min * 0.01 ) / ( max * 0.01 ) * width;
        },

        /**
        * Gets the sum of an array of numbers
        * @param {array} array - the number to add
        * @return {float} - sum
        */
        sum: function ( array ) {
            return array.reduce( ( a, b ) => a + b, 0 );
        },

        /**
        * Reverses the keys of an object
        * @param {object} object - the object
        * @return {object} - the object
        */
        reverse: function ( object ) {
            let range = Object.keys( object ).reverse(), output = {};
            for ( let price of range ) {
                output[price] = object[price];
            }
            return output;
        },

        /**
        * Converts an object to an array
        * @param {object} obj - the object
        * @return {array} - the array
        */
        array: function( obj ) {
            return Object.keys( obj ).map( function ( key ) {
                return [Number( key ), obj[key]];
            } );
        },

        /**
        * Sorts bids
        * @param {string} symbol - the object
        * @param {int} max - the max number of bids
        * @param {string} baseValue - the object
        * @return {object} - the object
        */
        sortBids: function ( symbol, max = Infinity, baseValue = false ) {
            let object = {}, count = 0, cache;
            if ( typeof symbol === 'object' ) cache = symbol;
            else cache = getDepthCache( symbol ).bids;
            let sorted = Object.keys( cache ).sort( function ( a, b ) {
                return parseFloat( b ) - parseFloat( a )
            } );
            let cumulative = 0;
            for ( let price of sorted ) {
                if ( baseValue === 'cumulative' ) {
                    cumulative += parseFloat( cache[price] );
                    object[price] = cumulative;
                } else if ( !baseValue ) object[price] = parseFloat( cache[price] );
                else object[price] = parseFloat( ( cache[price] * parseFloat( price ) ).toFixed( 8 ) );
                if ( ++count >= max ) break;
            }
            return object;
        },

        /**
        * Sorts asks
        * @param {string} symbol - the object
        * @param {int} max - the max number of bids
        * @param {string} baseValue - the object
        * @return {object} - the object
        */
        sortAsks: function ( symbol, max = Infinity, baseValue = false ) {
            let object = {}, count = 0, cache;
            if ( typeof symbol === 'object' ) cache = symbol;
            else cache = getDepthCache( symbol ).asks;
            let sorted = Object.keys( cache ).sort( function ( a, b ) {
                return parseFloat( a ) - parseFloat( b );
            } );
            let cumulative = 0;
            for ( let price of sorted ) {
                if ( baseValue === 'cumulative' ) {
                    cumulative += parseFloat( cache[price] );
                    object[price] = cumulative;
                } else if ( !baseValue ) object[price] = parseFloat( cache[price] );
                else object[price] = parseFloat( ( cache[price] * parseFloat( price ) ).toFixed( 8 ) );
                if ( ++count >= max ) break;
            }
            return object;
        },

        /**
        * Returns the first property of an object
        * @param {object} object - the object to get the first member
        * @return {string} - the object key
        */
        first: function ( object ) {
            return Object.keys( object ).shift();
        },

        /**
        * Returns the last property of an object
        * @param {object} object - the object to get the first member
        * @return {string} - the object key
        */
        last: function ( object ) {
            return Object.keys( object ).pop();
        },

        /**
        * Returns an array of properties starting at start
        * @param {object} object - the object to get the properties form
        * @param {int} start - the starting index
        * @return {array} - the array of entires
        */
        slice: function ( object, start = 0 ) {
            return Object.entries( object ).slice( start ).map( entry => entry[0] );
        },

        /**
        * Gets the minimum key form object
        * @param {object} object - the object to get the properties form
        * @return {string} - the minimum key
        */
        min: function ( object ) {
            return Math.min.apply( Math, Object.keys( object ) );
        },

        /**
        * Gets the maximum key form object
        * @param {object} object - the object to get the properties form
        * @return {string} - the minimum key
        */
        max: function ( object ) {
            return Math.max.apply( Math, Object.keys( object ) );
        },

        /**
        * Sets an option given a key and value
        * @param {string} key - the key to set
        * @param {object} value - the value of the key
        * @return {undefined}
        */
        setOption: function ( key, value ) {
            Binance.options[key] = value;
        },

        /**
        * Gets an option given a key
        * @param {string} key - the key to set
        * @return {undefined}
        */
        getOption: function ( key ) {
            return Binance.options[key];
        },

        /**
        * Returns the entire info object
        * @return {object} - the info object
        */
        getInfo: function() {
            return Binance.info;
        },

        /**
        * Returns the entire options object
        * @return {object} - the options object
        */
        getOptions: function() {
            return Binance.options;
        },

        /**
        * Gets an option given a key
        * @param {object} opt - the object with the class configuration
        * @param {function} callback - the callback function
        * @return {undefined}
        */
        options: setOptions,

        /**
        * Creates an order
        * @param {string} side - 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} flags - aadditionalbuy order flags
        * @param {function} callback - the callback function
        * @return {promise or undefined} - omitting the callback returns a promise
        */
        order: function ( side, symbol, quantity, price, flags = {}, callback = false ) {
            if ( !callback ) {
                return new Promise( ( resolve, reject ) => {
                    callback = ( error, response ) => {
                        if ( error ) {
                            reject( error );
                        } else {
                            resolve( response );
                        }
                    }
                    order( side, symbol, quantity, price, flags, callback );
                } )
            } else {
                order( side, symbol, quantity, price, flags, callback );
            }
        },

        /**
        * 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
        * @param {function} callback - the callback function
        * @return {promise or undefined} - omitting the callback returns a promise
        */
        buy: function( symbol, quantity, price, flags = {}, callback = false ) {
            if ( !callback ) {
                return new Promise( ( resolve, reject ) => {
                    callback = ( error, response ) => {
                        if ( error ) {
                            reject( error );
                        } else {
                            resolve( response );
                        }
                    }
                    order( 'BUY', symbol, quantity, price, flags, callback );
                } )
            } else {
                order( 'BUY', symbol, quantity, price, flags, callback );
            }
        },

        /**
        * Creates a sell order
        * @param {string} symbol - the symbol to sell
        * @param {numeric} quantity - the quantity required
        * @param {numeric} price - the price to sell each unit for
        * @param {object} flags - additional order flags
        * @param {function} callback - the callback function
        * @return {promise or undefined} - omitting the callback returns a promise
        */
        sell: function ( symbol, quantity, price, flags = {}, callback = false ) {
            if ( !callback ) {
                return new Promise( ( resolve, reject ) => {
                    callback = ( error, response ) => {
                        if ( error ) {
                            reject( error );
                        } else {
                            resolve( response );
                        }
                    }
                    order( 'SELL', symbol, quantity, price, flags, callback );
                } )
            } else {
                order( 'SELL', symbol, quantity, price, flags, callback );
            }

        },

        /**
        * Creates a market buy order
        * @param {string} symbol - the symbol to buy
        * @param {numeric} quantity - the quantity required
        * @param {object} flags - additional buy order flags
        * @param {function} callback - the callback function
        * @return {promise or undefined} - omitting the callback returns a promise
        */
        marketBuy: function ( symbol, quantity, flags = { type: 'MARKET' }, callback = false ) {
            if ( typeof flags === 'function' ) { // Accept callback as third parameter
                callback = flags;
                flags = { type: 'MARKET' };
            }
            if ( typeof flags.type === 'undefined' ) flags.type = 'MARKET';
            if ( !callback ) {
                return new Promise( ( resolve, reject ) => {
                    callback = ( error, response ) => {
                        if ( error ) {
                            reject( error );
                        } else {
                            resolve( response );
                        }
                    }
                    order( 'BUY', symbol, quantity, 0, flags, callback );
                } )
            } else {
                order( 'BUY', symbol, quantity, 0, flags, callback );
            }
        },

        /**
        * Creates a market sell order
        * @param {string} symbol - the symbol to sell
        * @param {numeric} quantity - the quantity required
        * @param {object} flags - additional sell order flags
        * @param {function} callback - the callback function
        * @return {promise or undefined} - omitting the callback returns a promise
        */
        marketSell: function ( symbol, quantity, flags = { type: 'MARKET' }, callback = false ) {
            if ( typeof flags === 'function' ) { // Accept callback as third parameter
                callback = flags;
                flags = { type: 'MARKET' };
            }
            if ( typeof flags.type === 'undefined' ) flags.type = 'MARKET';
            if ( !callback ) {
                return new Promise( ( resolve, reject ) => {
                    callback = ( error, response ) => {
                        if ( error ) {
                            reject( error );
                        } else {
                            resolve( response );
                        }
                    }
                    order( 'SELL', symbol, quantity, 0, flags, callback );
                } )
            } else {
                order( 'SELL', symbol, quantity, 0, flags, callback );
            }
        },

        /**
        * Cancels an order
        * @param {string} symbol - the symbol to cancel
        * @param {string} orderid - the orderid to cancel
        * @param {function} callback - the callback function
        * @return {promise or undefined} - omitting the callback returns a promise
        */
        cancel: function ( symbol, orderid, callback = false ) {
            if ( !callback ) {
                return new Promise( ( resolve, reject ) => {
                    callback = ( error, response ) => {
                        if ( error ) {
                            reject( error );
                        } else {
                            resolve( response );
                        }
                    }
                    signedRequest( base + 'v3/order', { symbol: symbol, orderId: orderid }, function ( error, data ) {
                        return callback.call( this, error, data, symbol );
                    }, 'DELETE' );
                } )
            } else {
                signedRequest( base + 'v3/order', { symbol: symbol, orderId: orderid }, function ( error, data ) {
                    return callback.call( this, error, data, symbol );
                }, 'DELETE' );
            }
        },

        /**
        * Gets the status of an order
        * @param {string} symbol - the symbol to check
        * @param {string} orderid - the orderid to check
        * @param {function} callback - the callback function
        * @param {object} flags - any additional flags
        * @return {promise or undefined} - omitting the callback returns a promise
        */
        orderStatus: function ( symbol, orderid, callback, flags = {} ) {
            let parameters = Object.assign( { symbol: symbol, orderId: orderid }, flags );
            if ( !callback ) {
                return new Promise( ( resolve, reject ) => {
                    callback = ( error, response ) => {
                        if ( error ) {
                            reject( error );
                        } else {
                            resolve( response );
                        }
                    }
                    signedRequest( base + 'v3/order', parameters, function ( error, data ) {
                        return callback.call( this, error, data, symbol );
                    } );
                } )
            } else {
                signedRequest( base + 'v3/order', parameters, function ( error, data ) {
                    return callback.call( this, error, data, symbol );
                } );
            }
        },

        /**
        * Gets open orders
        * @param {string} symbol - the symbol to get
        * @param {function} callback - the callback function
        * @return {promise or undefined} - omitting the callback returns a promise
        */
        openOrders: function ( symbol, callback ) {
            let parameters = symbol ? { symbol: symbol } : {};
            if ( !callback ) {
                return new Promise( ( resolve, reject ) => {
                    callback = ( error, response ) => {
                        if ( error ) {
                            reject( error );
                        } else {
                            resolve( response );
                        }
                    }
                    signedRequest( base + 'v3/openOrders', parameters, function ( error, data ) {
                        return callback.call( this, error, data, symbol );
                    } );
                } )
            } else {
                signedRequest( base + 'v3/openOrders', parameters, function ( error, data ) {
                    return callback.call( this, error, data, symbol );
                } );
            }
        },

        /**
        * Cancels all order of a given symbol
        * @param {string} symbol - the symbol to cancel all orders for
        * @param {function} callback - the callback function
        * @return {promise or undefined} - omitting the callback returns a promise
        */
        cancelOrders: function ( symbol, callback = false ) {
            if ( !callback ) {
                return new Promise( ( resolve, reject ) => {
                    callback = ( error, response ) => {
                        if ( error ) {
                            reject( error );
                        } else {
                            resolve( response );
                        }
                    }
                    signedRequest( base + 'v3/openOrders', { symbol: symbol }, function ( error, json ) {
                        if ( 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;
                            Binance.options.log( 'cancel order: ' + obj.side + ' ' + symbol + ' ' + quantity + ' @ ' + obj.price + ' #' + obj.orderId );
                            signedRequest( base + 'v3/order', { symbol: symbol, orderId: obj.orderId }, function ( error, data ) {
                                return callback.call( this, error, data, symbol );
                            }, 'DELETE' );
                        }
                    } );
                } )
            } else {
                signedRequest( base + 'v3/openOrders', { symbol: symbol }, function ( error, json ) {
                    if ( 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;
                        Binance.options.log( 'cancel order: ' + obj.side + ' ' + symbol + ' ' + quantity + ' @ ' + obj.price + ' #' + obj.orderId );
                        signedRequest( base + 'v3/order', { symbol: symbol, orderId: obj.orderId }, function ( error, data ) {
                            return callback.call( this, error, data, symbol );
                        }, 'DELETE' );
                    }
                } );
            }
        },

        /**
        * Gets all order of a given symbol
        * @param {string} symbol - the symbol
        * @param {function} callback - the callback function
        * @param {object} options - additional options
        * @return {promise or undefined} - omitting the callback returns a promise
        */
        allOrders: function ( symbol, callback, options = {} ) {
            let parameters = Object.assign( { symbol: symbol }, options );
            if ( !callback ) {
                return new Promise( ( resolve, reject ) => {
                    callback = ( error, response ) => {
                        if ( error ) {
                            reject( error );
                        } else {
                            resolve( response );
                        }
                    }
                    signedRequest( base + 'v3/allOrders', parameters, function ( error, data ) {
                        return callback.call( this, error, data, symbol );
                    } );
                } )
            } else {
                signedRequest( base + 'v3/allOrders', parameters, function ( error, data ) {
                    return callback.call( this, error, data, symbol );
                } );
            }
        },

        /**
        * Gets the depth information for a given symbol
        * @param {string} symbol - the symbol
        * @param {function} callback - the callback function
        * @param {int} limit - limit the number of returned orders
        * @return {promise or undefined} - omitting the callback returns a promise
        */
        depth: function ( symbol, callback, limit = 100 ) {
            if ( !callback ) {
                return new Promise( ( resolve, reject ) => {
                    callback = ( error, response ) => {
                        if ( error ) {
                            reject( error );
                        } else {
                            resolve( response );
                        }
                    }
                    publicRequest( base + 'v1/depth', { symbol: symbol, limit: limit }, function ( error, data ) {
                        return callback.call( this, error, depthData( data ), symbol );
                    } );
                } )
            } else {
                publicRequest( base + 'v1/depth', { symbol: symbol, limit: limit }, function ( error, data ) {
                    return callback.call( this, error, depthData( data ), symbol );
                } );
            }
        },

        /**
        * Gets the average prices of a given symbol
        * @param {string} symbol - the symbol
        * @param {function} callback - the callback function
        * @return {promise or undefined} - omitting the callback returns a promise
        */
        avgPrice: function ( symbol, callback = false ) {
            let opt = {
                url: base + 'v3/avgPrice?symbol=' + symbol,
                timeout: Binance.options.recvWindow
            };
            if ( !callback ) {
                return new Promise( ( resolve, reject ) => {
                    request( addProxy( opt ), ( error, response, body ) => {
                        if ( error ) return reject( error );
                        if ( response.statusCode !== 200 ) return reject( response );
                        let result = {};
                        result[symbol] = JSON.parse( response.body ).price;
                        return resolve( result );
                    } );
                } );
            }
            request( addProxy( opt ), ( error, response, body ) => {
                if ( error ) return callback( error );
                if ( response.statusCode !== 200 ) return callback( response );
                let result = {};
                result[symbol] = JSON.parse( response.body ).price;
                return callback( null, result );
            } );
        },

        /**
        * Gets the prices of a given symbol(s)
        * @param {string} symbol - the symbol
        * @param {function} callback - the callback function
        * @return {promise or undefined} - omitting the callback returns a promise
        */
        prices: function ( symbol, callback = false ) {
            const params = typeof symbol === 'string' ? '?symbol=' + symbol : '';
            if ( typeof symbol === 'function' ) callback = symbol; // backwards compatibility

            let opt = {
                url: base + 'v3/ticker/price' + params,
                timeout: Binance.options.recvWindow
            };
            if ( !callback ) {
                return new Promise( ( resolve, reject ) => {
                    request( addProxy( opt ), ( error, response, body ) => {
                        if ( error ) return reject( error );
                        if ( response.statusCode !== 200 ) return reject( response );
                        return resolve( priceData( JSON.parse( body ) ) );
                    } );
                } );
            }
            request( addProxy( opt ), ( error, response, body ) => {
                if ( error ) return callback( error );
                if ( response.statusCode !== 200 ) return callback( response );
                return callback( null, priceData( JSON.parse( body ) ) );
            } );
        },

        /**
        * Gets the book tickers of given symbol(s)
        * @param {string} symbol - the symbol
        * @param {function} callback - the callback function
        * @return {promise or undefined} - omitting the callback returns a promise
        */
        bookTickers: function ( symbol, callback ) {
            const params = typeof symbol === 'string' ? '?symbol=' + symbol : '';
            if ( typeof symbol === 'function' ) callback = symbol; // backwards compatibility
            let opt = {
                url: base + 'v3/ticker/bookTicker' + params,
                timeout: Binance.options.recvWindow
            };
            if ( !callback ) {
                return new Promise( ( resolve, reject ) => {
                    request( addProxy( opt ), function ( error, response, body ) {
                        if ( error ) return reject( error );
                        if ( response.statusCode !== 200 ) return reject( response );
                        const result = symbol ? JSON.parse( body ) : bookPriceData( JSON.parse( body ) );
                        return resolve( result );
                    } );
                } );
            }
            request( addProxy( opt ), ( error, response, body ) => {
                if ( error ) return callback( error );
                if ( response.statusCode !== 200 ) return callback( response );
                const result = symbol ? JSON.parse( body ) : bookPriceData( JSON.parse( body ) );
                return callback( null, result );
            } );
        },

        /**
        * Gets the prevday percentage change
        * @param {string} symbol - the symbol or symbols
        * @param {function} callback - the callback function
        * @return {promise or undefined} - omitting the callback returns a promise
        */
        prevDay: function ( symbol, callback ) {
            let input = symbol ? { symbol: symbol } : {};
            if ( !callback ) {
                return new Promise( ( resolve, reject ) => {
                    callback = ( error, response ) => {
                        if ( error ) {
                            reject( error );
                        } else {
                            resolve( response );
                        }
                    }
                    publicRequest( base + 'v1/ticker/24hr', input, ( error, data ) => {
                        return callback.call( this, error, data, symbol );
                    } );
                } )
            } else {
                publicRequest( base + 'v1/ticker/24hr', input, ( error, data ) => {
                    return callback.call( this, error, data, symbol );
                } );
            }
        },

        /**
        * Gets the the exchange info
        * @param {function} callback - the callback function
        * @return {promise or undefined} - omitting the callback returns a promise
        */
        exchangeInfo: function ( callback ) {
            if ( !callback ) {
                return new Promise( ( resolve, reject ) => {
                    callback = ( error, response ) => {
                        if ( error ) {
                            reject( error );
                        } else {
                            resolve( response );
                        }
                    }
                    publicRequest( base + 'v1/exchangeInfo', {}, callback );
                } )
            } else {
                publicRequest( base + 'v1/exchangeInfo', {}, callback );
            }
        },

        /**
        * Gets the dust log for user
        * @param {function} callback - the callback function
        * @return {promise or undefined} - omitting the callback returns a promise
        */
        dustLog: function ( callback ) {
            if ( !callback ) {
                return new Promise( ( resolve, reject ) => {
                    callback = ( error, response ) => {
                        if ( error ) {
                            reject( error );
                        } else {
                            resolve( response );
                        }
                    }
                    signedRequest( wapi + '/v3/userAssetDribbletLog.html', {}, callback );
                } )
            } else {
                signedRequest( wapi + '/v3/userAssetDribbletLog.html', {}, callback );
            }
        },

        /**
        * Gets the the system status
        * @param {function} callback - the callback function
        * @return {promise or undefined} - omitting the callback returns a promise
        */
        systemStatus: function ( callback ) {
            if ( !callback ) {
                return new Promise( ( resolve, reject ) => {
                    callback = ( error, response ) => {
                        if ( error ) {
                            reject( error );
                        } else {
                            resolve( response );
                        }
                    }
                    publicRequest( wapi + 'v3/systemStatus.html', {}, callback );
                } )
            } else {
                publicRequest( wapi + 'v3/systemStatus.html', {}, callback );
            }
        },

        /**
        * Withdraws asset to given wallet id
        * @param {string} asset - the asset symbol
        * @param {string} address - the wallet to transfer it to
        * @param {number} amount - the amount to transfer
        * @param {string} addressTag - and addtional address tag
        * @param {function} callback - the callback function
        * @param {string} name - the name to save the address as. Set falsy to prevent Binance saving to address book
        * @return {promise or undefined} - omitting the callback returns a promise
        */
        withdraw: function ( asset, address, amount, addressTag = false, callback = false, name = 'API Withdraw' ) {
            let params = { asset, address, amount };
            if ( addressTag ) params.addressTag = addressTag;
            if ( !callback ) {
                return new Promise( ( resolve, reject ) => {
                    callback = ( error, response ) => {
                        if ( error ) {
                            reject( error );
                        } else {
                            resolve( response );
                        }
                    }
                    signedRequest( wapi + 'v3/withdraw.html', params, callback, 'POST' );
                } )
            } else {
                signedRequest( wapi + 'v3/withdraw.html', params, callback, 'POST' );
            }
        },

        /**
        * Get the Withdraws history for a given asset
        * @param {function} callback - the callback function
        * @param {object} params - supports limit and fromId parameters
        * @return {promise or undefined} - omitting the callback returns a promise
        */
        withdrawHistory: function ( callback, params = {} ) {
            if ( typeof params === 'string' ) params = { asset: params };
            if ( !callback ) {
                return new Promise( ( resolve, reject ) => {
                    callback = ( error, response ) => {
                        if ( error ) {
                            reject( error );
                        } else {
                            resolve( response );
                        }
                    }
                    signedRequest( wapi + 'v3/withdrawHistory.html', params, callback );
                } )
            } else {
                signedRequest( wapi + 'v3/withdrawHistory.html', params, callback );
            }
        },

        fiatWithdrawHistory: async ( params = {} ) => {
          return promiseRequest('v1/fiatpayment/query/withdraw/history', params, { base:sapi, type:'SIGNED'});
        },

        /**
        * Get the deposit history
        * @param {function} callback - the callback function
        * @param {object} params - additional params
        * @return {promise or undefined} - omitting the callback returns a promise
        */
        depositHistory: function ( callback, params = {} ) {
            if ( typeof params === 'string' ) params = { asset: params }; // Support 'asset' (string) or optional parameters (object)
            if ( !callback ) {
                return new Promise( ( resolve, reject ) => {
                    callback = ( error, response ) => {
                        if ( error ) {
                            reject( error );
                        } else {
                            resolve( response );
                        }
                    }
                    signedRequest( wapi + 'v3/depositHistory.html', params, callback );
                } )
            } else {
                signedRequest( wapi + 'v3/depositHistory.html', params, callback );
            }
        },

        fiatDepositHistory: async ( params = {} ) => {
          return promiseRequest('v1/fiatpayment/query/deposit/history', params, { base:sapi, type:'SIGNED'});
        },

        /**
        * Get the deposit history for given asset
        * @param {string} asset - the asset
        * @param {function} callback - the callback function
        * @return {promise or undefined} - omitting the callback returns a promise
        */
        depositAddress: function ( asset, callback ) {
            if ( !callback ) {
                return new Promise( ( resolve, reject ) => {
                    callback = ( error, response ) => {
                        if ( error ) {
                            reject( error );
                        } else {
                            resolve( response );
                        }
                    }
                    signedRequest( wapi + 'v3/depositAddress.html', { asset: asset }, callback );
                } )
            } else {
                signedRequest( wapi + 'v3/depositAddress.html', { asset: asset }, callback );
            }
        },

        /**
        * Get the account status
        * @param {function} callback - the callback function
        * @return {promise or undefined} - omitting the callback returns a promise
        */
        accountStatus: function ( callback ) {
            if ( !callback ) {
                return new Promise( ( resolve, reject ) => {
                    callback = ( error, response ) => {
                        if ( error ) {
                            reject( error );
                        } else {
                            resolve( response );
                        }
                    }
                    signedRequest( wapi + 'v3/accountStatus.html', {}, callback );
                } )
            } else {
                signedRequest( wapi + 'v3/accountStatus.html', {}, callback );
            }
        },

        /**
        * Get the trade fee
        * @param {function} callback - the callback function
        * @param {string} symbol (optional)
        * @return {promise or undefined} - omitting the callback returns a promise
        */
        tradeFee: function ( callback, symbol = false ) {
            let params = symbol ? { symbol: symbol } : {};
            if ( !callback ) {
                return new Promise( ( resolve, reject ) => {
                    callback = ( error, response ) => {
                        if ( error ) {
                            reject( error );
                        } else {
                            resolve( response );
                        }
                    }
                    signedRequest( wapi + 'v3/tradeFee.html', params, callback );
                } )
            } else {
                signedRequest( wapi + 'v3/tradeFee.html', params, callback );
            }
        },

        /**
        * Fetch asset detail (minWithdrawAmount, depositStatus, withdrawFee, withdrawStatus, depositTip)
        * @param {function} callback - the callback function
        * @return {promise or undefined} - omitting the callback returns a promise
        */
        assetDetail: function ( callback ) {
            if ( !callback ) {
                return new Promise( ( resolve, reject ) => {
                    callback = ( error, response ) => {
                        if ( error ) {
                            reject( error );
                        } else {
                            resolve( response );
                        }
                    }
                    signedRequest( wapi + 'v3/assetDetail.html', {}, callback );
                } )
            } else {
                signedRequest( wapi + 'v3/assetDetail.html', {}, callback );
            }
        },

        /**
        * Get the account
        * @param {function} callback - the callback function
        * @return {promise or undefined} - omitting the callback returns a promise
        */
        account: function ( callback ) {
            if ( !callback ) {
                return new Promise( ( resolve, reject ) => {
                    callback = ( error, response ) => {
                        if ( error ) {
                            reject( error );
                        } else {
                            resolve( response );
                        }
                    }
                    signedRequest( base + 'v3/account', {}, callback );
                } )
            } else {
                signedRequest( base + 'v3/account', {}, callback );
            }
        },

        /**
        * Get the balance data
        * @param {function} callback - the callback function
        * @return {promise or undefined} - omitting the callback returns a promise
        */
        balance: function ( callback ) {
            if ( !callback ) {
                return new Promise( ( resolve, reject ) => {
                    callback = ( error, response ) => {
                        if ( error ) {
                            reject( error );
                        } else {
                            resolve( response );
                        }
                    }
                    signedRequest( base + 'v3/account', {}, function ( error, data ) {
                        callback( error, balanceData( data ) );
                    } );
                } )
            } else {
                signedRequest( base + 'v3/account', {}, function ( error, data ) {
                    callback( error, balanceData( data ) );
                } );
            }
        },

        /**
        * Get trades for a given symbol
        * @param {string} symbol - the symbol
        * @param {function} callback - the callback function
        * @param {object} options - additional options
        * @return {promise or undefined} - omitting the callback returns a promise
        */
        trades: ( symbol, callback, options = {} ) => {
            let parameters = Object.assign( { symbol: symbol }, options );
            if ( !callback ) {
                return new Promise( ( resolve, reject ) => {
                    callback = ( error, response ) => {
                        if ( error ) {
                            reject( error );
                        } else {
                            resolve( response );
                        }
                    }
                    signedRequest( base + 'v3/myTrades', parameters, function ( error, data ) {
                        return callback.call( this, error, data, symbol );
                    } );
                } )
            } else {
                signedRequest( base + 'v3/myTrades', parameters, function ( error, data ) {
                    return callback.call( this, error, data, symbol );
                } );
            }
        },

        /**
        * Tell api to use the server time to offset time indexes
        * @param {function} callback - the callback function
        * @return {promise or undefined} - omitting the callback returns a promise
        */
        useServerTime: ( callback = false ) => {
            if ( !callback ) {
                return new Promise( ( resolve, reject ) => {
                    callback = ( error, response ) => {
                        if ( error ) {
                            reject( error );
                        } else {
                            resolve( response );
                        }
                    }
                    apiRequest( base + 'v1/time', {}, function ( error, response ) {
                        Binance.info.timeOffset = response.serverTime - new Date().getTime();
                        //Binance.options.log("server time set: ", response.serverTime, Binance.info.timeOffset);
                        callback( error, response );
                    } );
                } )
            } else {
                apiRequest( base + 'v1/time', {}, function ( error, response ) {
                    Binance.info.timeOffset = response.serverTime - new Date().getTime();
                    //Binance.options.log("server time set: ", response.serverTime, Binance.info.timeOffset);
                    callback( error, response );
                } );
            }
        },

        /**
        * Gets the time
        * @param {function} callback - the callback function
        * @return {promise or undefined} - omitting the callback returns a promise
        */
        time: function ( callback ) {
            if ( !callback ) {
                return new Promise( ( resolve, reject ) => {
                    callback = ( error, response ) => {
                        if ( error ) {
                            reject( error );
                        } else {
                            resolve( response );
                        }
                    }
                    apiRequest( base + 'v1/time', {}, callback );
                } )
            } else {
                apiRequest( base + 'v1/time', {}, callback );
            }
        },

        /**
        * Get agg trades for given symbol
        * @param {string} symbol - the symbol
        * @param {object} options - addtional optoins
        * @param {function} callback - the callback function
        * @return {promise or undefined} - omitting the callback returns a promise
        */
        aggTrades: function ( symbol, options = {}, callback = false ) { //fromId startTime endTime limit
            let parameters = Object.assign( { symbol }, options );
            if ( !callback ) {
                return new Promise( ( resolve, reject ) => {
                    callback = ( error, response ) => {
                        if ( error ) {
                            reject( error );
                        } else {
                            resolve( response );
                        }
                    }
                    marketRequest( base + 'v1/aggTrades', parameters, callback );
                } )
            } else {
                marketRequest( base + 'v1/aggTrades', parameters, callback );
            }
        },

        /**
        * Get the recent trades
        * @param {string} symbol - the symbol
        * @param {function} callback - the callback function
        * @param {int} limit - limit the number of items returned
        * @return {promise or undefined} - omitting the callback returns a promise
        */
        recentTrades: function ( symbol, callback, limit = 500 ) {
            if ( !callback ) {
                return new Promise( ( resolve, reject ) => {
                    callback = ( error, response ) => {
                        if ( error ) {
                            reject( error );
                        } else {
                            resolve( response );
                        }
                    }
                    marketRequest( base + 'v1/trades', { symbol: symbol, limit: limit }, callback );
                } )
            } else {
                marketRequest( base + 'v1/trades', { symbol: symbol, limit: limit }, callback );
            }
        },

        /**
        * Get the historical trade info
        * @param {string} symbol - the symbol
        * @param {function} callback - the callback function
        * @param {int} limit - limit the number of items returned
        * @param {int} fromId - from this id
        * @return {promise or undefined} - omitting the callback returns a promise
        */
        historicalTrades: function ( symbol, callback, limit = 500, fromId = false ) {
            let parameters = { symbol: symbol, limit: limit };
            if ( fromId ) parameters.fromId = fromId;
            if ( !callback ) {
                return new Promise( ( resolve, reject ) => {
                    callback = ( error, response ) => {
                        if ( error ) {
                            reject( error );
                        } else {
                            resolve( response );
                        }
                    }
                    marketRequest( base + 'v1/historicalTrades', parameters, callback );
                } )
            } else {
                marketRequest( base + 'v1/historicalTrades', parameters, callback );
            }
        },

        /**
        * Convert chart data to highstock array [timestamp,open,high,low,close]
        * @param {object} chart - the chart
        * @param {boolean} include_volume - to include the volume or not
        * @return {array} - an array
        */
        highstock: function ( chart, include_volume = false ) {
            let array = [];
            for ( let timestamp in chart ) {
                let obj = chart[timestamp];
                let line = [
                    Number( timestamp ),
                    parseFloat( obj.open ),
                    parseFloat( obj.high ),
                    parseFloat( obj.low ),
                    parseFloat( obj.close )
                ];
                if ( include_volume ) line.push( parseFloat( obj.volume ) );
                array.push( line );
            }
            return array;
        },

        /**
        * Populates OHLC information
        * @param {object} chart - the chart
        * @return {object} - object with candle information
        */
        ohlc: function ( chart ) {
            let open = [], high = [], low = [], close = [], volume = [];
            for ( let timestamp in chart ) { //Binance.ohlc[symbol][interval]
                let obj = chart[timestamp];
                open.push( parseFloat( obj.open ) );
                high.push( parseFloat( obj.high ) );
                low.push( parseFloat( obj.low ) );
                close.push( parseFloat( obj.close ) );
                volume.push( parseFloat( obj.volume ) );
            }
            return { open: open, high: high, low: low, close: close, volume: volume };
        },

        /**
        * Gets the candles information for a given symbol
        * intervals: 1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,1M
        * @param {string} symbol - the symbol
        * @param {function} interval - the callback function
        * @param {function} callback - the callback function
        * @param {object} options - additional options
        * @return {promise or undefined} - omitting the callback returns a promise
        */
        candlesticks: function ( symbol, interval = '5m', callback = false, options = { limit: 500 } ) {
            let params = Object.assign( { symbol: symbol, interval: interval }, options );
            if ( !callback ) {
                return new Promise( ( resolve, reject ) => {
                    callback = ( error, response ) => {
                        if ( error ) {
                            reject( error );
                        } else {
                            resolve( response );
                        }
                    }
                    publicRequest( base + 'v1/klines', params, function ( error, data ) {
                        return callback.call( this, error, data, symbol );
                    } );
                } )
            } else {
                publicRequest( base + 'v1/klines', params, function ( error, data ) {
                    return callback.call( this, error, data, symbol );
                } );
            }
        },

        /**
        * Queries the public api
        * @param {string} url - the public api endpoint
        * @param {object} data - the data to send
        * @param {function} callback - the callback function
        * @param {string} method - the http method
        * @return {promise or undefined} - omitting the callback returns a promise
        */
        publicRequest: function ( url, data, callback, method = 'GET' ) {
            if ( !callback ) {
                return new Promise( ( resolve, reject ) => {
                    callback = ( error, response ) => {
                        if ( error ) {
                            reject( error );
                        } else {
                            resolve( response );
                        }
                    }
                    publicRequest( url, data, callback, method );
                } )
            } else {
                publicRequest( url, data, callback, method );
            }
        },

        /**
        * Queries the signed api
        * @param {string} url - the signed api endpoint
        * @param {object} data - the data to send
        * @param {function} callback - the callback function
        * @param {string} method - the http method
        * @param {boolean} noDataInSignature - Prevents data from being added to signature
        * @return {promise or undefined} - omitting the callback returns a promise
        */
        signedRequest: function ( url, data, callback, method = 'GET', noDataInSignature = false ) {
            if ( !callback ) {
                return new Promise( ( resolve, reject ) => {
                    callback = ( error, response ) => {
                        if ( error ) {
                            reject( error );
                        } else {
                            resolve( response );
                        }
                    }
                    signedRequest( url, data, callback, method, noDataInSignature );
                } )
            } else {
                signedRequest( url, data, callback, method, noDataInSignature );
            }
        },

        /**
        * Gets the market asset of given symbol
        * @param {string} symbol - the public api endpoint
        * @return {undefined}
        */
        getMarket: function ( symbol ) {
            if ( symbol.endsWith( 'BTC' ) ) return 'BTC';
            else if ( symbol.endsWith( 'ETH' ) ) return 'ETH';
            else if ( symbol.endsWith( 'BNB' ) ) return 'BNB';
            else if ( symbol.endsWith( 'XRP' ) ) return 'XRP';
            else if ( symbol.endsWith( 'PAX' ) ) return 'PAX';
            else if ( symbol.endsWith( 'USDT' ) ) return 'USDT';
            else if ( symbol.endsWith( 'USDC' ) ) return 'USDC';
            else if ( symbol.endsWith( 'USDS' ) ) return 'USDS';
            else if ( symbol.endsWith( 'TUSD' ) ) return 'TUSD';
        },

        /**
        * Get the account binance lending information
        * @param {function} callback - the callback function
        * @return {promise or undefined} - omitting the callback returns a promise
        */
       lending: async ( params = {} ) => {
        return promiseRequest( 'v1/lending/union/account', params, {base:sapi, type:'SIGNED'});
       },

        //** Futures methods */
        futuresPing: async ( params = {} ) => {
            return promiseRequest( 'v1/ping', params, {base:fapi} );
        },

        futuresTime: async ( params = {} ) => {
            return promiseRequest( 'v1/time', params, {base:fapi} ).then( r => r.serverTime );
        },

        futuresExchangeInfo: async () => {
            return promiseRequest( 'v1/exchangeInfo', {}, {base:fapi} );
        },

        futuresPrices: async ( params = {} ) => {
            let data = await promiseRequest( 'v1/ticker/price', params, {base:fapi} );
            return data.reduce( ( out, i ) => ( ( out[i.symbol] =  i.price ), out ) );
        },

        futuresDaily: async ( symbol = false, params = {} ) => {
            if ( symbol ) params.symbol = symbol;
            let data = await promiseRequest( 'v1/ticker/24hr', params, {base:fapi} );
            return symbol ? data : data.reduce( ( out, i ) => ( ( out[i.symbol] = i ), out ), {} );
        },

        futuresOpenInterest: async ( symbol ) => {
            return promiseRequest( 'v1/openInterest', {symbol}, {base:fapi} ).then( r => r.openInterest );
        },

        futuresCandles: async ( symbol, interval = "30m", params = {} ) => {
            params.symbol = symbol;
            params.interval = interval;
            return promiseRequest( 'v1/klines', params, {base:fapi} );
        },

        futuresMarkPrice: async ( symbol = false ) => {
            return promiseRequest( 'v1/premiumIndex', symbol ? {symbol} : {}, {base:fapi} );
        },

        futuresTrades: async ( symbol, params = {} ) => {
            params.symbol = symbol;
            return promiseRequest( 'v1/trades', params, {base:fapi} );
        },

        futuresHistoricalTrades: async ( symbol, params = {} ) => {
            params.symbol = symbol;
            return promiseRequest( 'v1/historicalTrades', params, {base:fapi, type:'MARKET_DATA'} );
        },

        futuresAggTrades: async ( symbol, params = {} ) => {
            params.symbol = symbol;
            return promiseRequest( 'v1/aggTrades', params, {base:fapi} );
        },
        
        futuresUserTrades: async ( symbol, params = {} ) => {
            params.symbol = symbol;
            return promiseRequest( 'v1/userTrades', params, {base:fapi, type:'SIGNED'} );
        },
        
        futuresGetDataStream: async ( params = {} ) => {
            //A User Data Stream listenKey is valid for 60 minutes after creation. setInterval
            return promiseRequest( 'v1/listenKey', params, {base:fapi, type:'SIGNED', method:'POST'} );
        },

        futuresKeepDataStream: async ( params = {} ) => {
            return promiseRequest( 'v1/listenKey', params, {base:fapi, type:'SIGNED', method:'PUT'} );
        },

        futuresCloseDataStream: async ( params = {} ) => {
            return promiseRequest( 'v1/listenKey', params, {base:fapi, type:'SIGNED', method:'DELETE'} );
        },

        futuresLiquidationOrders: async ( symbol = false, params = {} ) => {
            if ( symbol ) params.symbol = symbol;
            return promiseRequest( 'v1/allForceOrders', params, {base:fapi} );
        },
        
        futuresPositionRisk: async ( params = {} ) => {
            return promiseRequest( 'v1/positionRisk', params, {base:fapi, type:'SIGNED'} ).then( r=>r.reduce( ( out, i ) => ( ( out[i.symbol] = i ), out ), {} ) );
        },

        futuresFundingRate: async ( symbol, params = {} ) => {
            params.symbol = symbol;
            return promiseRequest( 'v1/fundingRate', params, {base:fapi} );
        },

        futuresLeverageBracket: async ( symbol = false, params = {} ) => {
            if ( symbol ) params.symbol = symbol;
            return promiseRequest( 'v1/leverageBracket', params, {base:fapi, type:'MARKET_DATA'} );
        },

        // leverage 1 to 125
        futuresLeverage: async ( symbol, leverage, params = {} ) => {
            params.symbol = symbol;
            params.leverage = leverage;
            return promiseRequest( 'v1/leverage', params, {base:fapi, method:'POST', type:'SIGNED'} );
        },

        // ISOLATED, CROSSED
        futuresMarginType: async ( symbol, marginType, params = {} ) => {
            params.symbol = symbol;
            params.marginType = marginType;
            return promiseRequest( 'v1/marginType', params, {base:fapi, method:'POST', type:'SIGNED'} );
        },

        // type: 1: Add postion margin,2: Reduce postion margin
        futuresPositionMargin: async ( symbol, amount, type = 1, params = {} ) => {
            params.symbol = symbol;
            params.marginType = marginType;
            return promiseRequest( 'v1/positionMargin', params, {base:fapi, method:'POST', type:'SIGNED'} );
        },

        futuresPositionMarginHistory: async ( symbol, params = {} ) => {
            params.symbol = symbol;
            return promiseRequest( 'v1/positionMargin/history', params, {base:fapi, type:'SIGNED'} );
        },
        
        futuresIncome: async ( params = {} ) => {
            return promiseRequest( 'v1/income', params, {base:fapi, type:'SIGNED'} );
        },

        futuresBalance: async ( params = {} ) => {
            return promiseRequest( 'v1/balance', params, {base:fapi, type:'SIGNED'} );
        },

        futuresAccount: async ( params = {} ) => {
            return promiseRequest( 'v1/account', params, {base:fapi, type:'SIGNED'} );
        },

        futuresDepth: async ( symbol, params = {} ) => {
            params.symbol = symbol;
            return promiseRequest( 'v1/depth', params, {base:fapi} );
        },

        futuresQuote: async ( symbol = false, params = {} ) => {
            if ( symbol ) params.symbol = symbol;
            //let data = await promiseRequest( 'v1/ticker/bookTicker', params, {base:fapi} );
            //return data.reduce((out, i) => ((out[i.symbol] = i), out), {}),
            let data = await promiseRequest( 'v1/ticker/bookTicker', params, {base:fapi} );
            return symbol ? data : data.reduce( ( out, i ) => ( ( out[i.symbol] = i ), out ), {} );
        },

        futuresBuy: async ( symbol, quantity, price, params = {} ) => {
            return futuresOrder( 'BUY', symbol, quantity, price, params );
        },

        futuresSell: async ( symbol, quantity, price, params = {} ) => {
            return futuresOrder( 'SELL', symbol, quantity, price, params );
        },

        futuresMarketBuy: async ( symbol, quantity, params = {} ) => {
            return futuresOrder( 'BUY', symbol, quantity, false, params );
        },

        futuresMarketSell: async ( symbol, quantity, params = {} ) => {
            return futuresOrder( 'SELL', symbol, quantity, false, params );
        },
        
        futuresOrder, // side symbol quantity [price] [params]
        
        futuresOrderStatus: async ( symbol, params = {} ) => { // Either orderId or origClientOrderId must be sent
            params.symbol = symbol;
            return promiseRequest( 'v1/order', params, {base:fapi, type:'SIGNED'} );
        },

        futuresCancel: async ( symbol, params = {} ) => { // Either orderId or origClientOrderId must be sent
            params.symbol = symbol;
            return promiseRequest( 'v1/order', params, {base:fapi, type:'SIGNED', method:'DELETE'} );
        },

        futuresCancelAll: async ( symbol, params = {} ) => {
            params.symbol = symbol;
            return promiseRequest( 'v1/allOpenOrders', params, {base:fapi, type:'SIGNED', method:'DELETE'} );
        },

        futuresOpenOrders: async ( symbol = false, params = {} ) => {
            if ( symbol ) params.symbol = symbol;
            return promiseRequest( 'v1/openOrders', params, {base:fapi, type:'SIGNED'} );
        },

        futuresAllOrders: async ( symbol = false, params = {} ) => { // Get all account orders; active, canceled, or filled.
            if ( symbol ) params.symbol = symbol;
            return promiseRequest( 'v1/allOrders', params, {base:fapi, type:'SIGNED'} );
        },

        /* Coming soon:
        futuresSubscribe
        Cancel multiple orders DELETE /fapi/v1/batchOrders
        New Future Account Transfer POST https://api.binance.us/sapi/v1/futures/transfer (HMAC SHA
        Get Postion Margin Change History (TRADE)

        wss://fstream.binance.us/ws/<listenKey>
        Diff. Book Depth Streams (250ms, 100ms, or realtime): <symbol>@depth OR <symbol>@depth@100ms OR <symbol>@depth@0ms
        Partial Book Depth Streams (5, 10, 20): <symbol>@depth<levels> OR <symbol>@depth<levels>@100ms
        All Market Liquidation Order Streams: !forceOrder@arr
        All Book Tickers Stream: !bookTicker
        Individual Symbol Book Ticker Streams: <symbol>@bookTicker
        All Market Tickers Streams (24h, updates 3 seconds): <symbol>!ticker@arr
        Individual Symbol Ticker Streams (24h, 3 sec): <symbol>@ticker
        Liquidation Order Streams for specific symbol: <symbol>@forceOrder
        All Market Mini Tickers Stream (24h, 3 sec): <symbol>!miniTicker@arr
        Individual Symbol Mini Ticker Stream (24h, 3 sec): <symbol>@miniTicker
        Chart data (250ms): <symbol>@kline_<interval>
        Mark price (3 sec): <symbol>@markPrice
        Aggregate Trade Streams (100ms): <symbol>@aggTrade
        Raw streams are accessed at /ws/<streamName>
        Combined streams are accessed at /stream?streams=<streamName1>/<streamName2>/<streamName3>
        SUBSCRIBE, UNSUBSCRIBE, LIST_SUBSCRIPTIONS, SET_PROPERTY, GET_PROPERTY
        */

        /*
        const futuresOrder = (side, symbol, quantity, price = 0, flags = {}, callback = false) => {
            let opt = {
                symbol: symbol,
                side: side,
                type: 'LIMIT',
                quantity: quantity
            };
            if (typeof flags.type !== 'undefined') opt.type = flags.type;
            if (opt.type.includes('LIMIT')) {
                opt.price = price;
                opt.timeInForce = 'GTC';
            }
            if (typeof flags.timeInForce !== 'undefined') opt.timeInForce = flags.timeInForce;
            signedRequest(`${fapi}v1/order`, opt, function (error, response) {
                if (!response) {
                    if (callback) return callback(error, response);
                    else return Binance.options.log('futuresOrder error:', error);
                }
                if (callback) return callback(error, response);
                else return Binance.options.log(`futuresOrder ${side} (${symbol},${quantity},${price})`, response);
            }, 'POST');
        };*/
          
        //** Margin methods */
        /**
         * Creates an order
         * @param {string} side - 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} flags - additional buy order flags
         * @param {function} callback - the callback function
         * @return {undefined}
         */
        mgOrder: function ( side, symbol, quantity, price, flags = {}, callback = false ) {
            marginOrder( side, symbol, quantity, price, flags, callback );
        },

        /**
         * 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
         * @param {function} callback - the callback function
         * @return {undefined}
         */
        mgBuy: function ( symbol, quantity, price, flags = {}, callback = false ) {
            marginOrder( 'BUY', symbol, quantity, price, flags, callback );
        },

        /**
         * Creates a sell order
         * @param {string} symbol - the symbol to sell
         * @param {numeric} quantity - the quantity required
         * @param {numeric} price - the price to sell each unit for
         * @param {object} flags - additional order flags
         * @param {function} callback - the callback function
         * @return {undefined}
         */
        mgSell: function ( symbol, quantity, price, flags = {}, callback = false ) {
            marginOrder( 'SELL', symbol, quantity, price, flags, callback );
        },

        /**
         * Creates a market buy order
         * @param {string} symbol - the symbol to buy
         * @param {numeric} quantity - the quantity required
         * @param {object} flags - additional buy order flags
         * @param {function} callback - the callback function
         * @return {undefined}
         */
        mgMarketBuy: function ( symbol, quantity, flags = { type: 'MARKET' }, callback = false ) {
            if ( typeof flags === 'function' ) { // Accept callback as third parameter
                callback = flags;
                flags = { type: 'MARKET' };
            }
            if ( typeof flags.type === 'undefined' ) flags.type = 'MARKET';
            marginOrder( 'BUY', symbol, quantity, 0, flags, callback );
        },

        /**
         * Creates a market sell order
         * @param {string} symbol - the symbol to sell
         * @param {numeric} quantity - the quantity required
         * @param {object} flags - additional sell order flags
         * @param {function} callback - the callback function
         * @return {undefined}
         */
        mgMarketSell: function ( symbol, quantity, flags = { type: 'MARKET' }, callback = false ) {
            if ( typeof flags === 'function' ) { // Accept callback as third parameter
                callback = flags;
                flags = { type: 'MARKET' };
            }
            if ( typeof flags.type === 'undefined' ) flags.type = 'MARKET';
            marginOrder( 'SELL', symbol, quantity, 0, flags, callback );
        },

        /**
         * Cancels an order
         * @param {string} symbol - the symbol to cancel
         * @param {string} orderid - the orderid to cancel
         * @param {function} callback - the callback function
         * @return {undefined}
         */
        mgCancel: function ( symbol, orderid, callback = false ) {
            signedRequest( sapi + 'v1/margin/order', { symbol: symbol, orderId: orderid }, function ( error, data ) {
                if ( callback ) return callback.call( this, error, data, symbol );
            }, 'DELETE' );
        },

        /**
        * Gets all order of a given symbol
        * @param {string} symbol - the symbol
        * @param {function} callback - the callback function
        * @param {object} options - additional options
        * @return {promise or undefined} - omitting the callback returns a promise
        */
        mgAllOrders: function ( symbol, callback, options = {} ) {
            let parameters = Object.assign( { symbol: symbol }, options );
            if ( !callback ) {
                return new Promise( ( resolve, reject ) => {
                    callback = ( error, response ) => {
                        if ( error ) {
                            reject( error );
                        } else {
                            resolve( response );
                        }
                    }
                    signedRequest( sapi + 'v1/margin/allOrders', parameters, function ( error, data ) {
                        return callback.call( this, error, data, symbol );
                    } );
                } )
            } else {
                signedRequest( sapi + 'v1/margin/allOrders', parameters, function ( error, data ) {
                    return callback.call( this, error, data, symbol );
                } );
            }
        },      

        /**
         * Gets the status of an order
         * @param {string} symbol - the symbol to check
         * @param {string} orderid - the orderid to check
         * @param {function} callback - the callback function
         * @param {object} flags - any additional flags
         * @return {undefined}
         */
        mgOrderStatus: function ( symbol, orderid, callback, flags = {} ) {
            let parameters = Object.assign( { symbol: symbol, orderId: orderid }, flags );
            signedRequest( sapi + 'v1/margin/order', parameters, function ( error, data ) {
                if ( callback ) return callback.call( this, error, data, symbol );
            } );
        },

        /**
         * Gets open orders
         * @param {string} symbol - the symbol to get
         * @param {function} callback - the callback function
         * @return {undefined}
         */
        mgOpenOrders: function ( symbol, callback ) {
            let parameters = symbol ? { symbol: symbol } : {};
            signedRequest( sapi + 'v1/margin/openOrders', parameters, function ( error, data ) {
                return callback.call( this, error, data, symbol );
            } );
        },

        /**
         * Cancels all order of a given symbol
         * @param {string} symbol - the symbol to cancel all orders for
         * @param {function} callback - the callback function
         * @return {undefined}
         */
        mgCancelOrders: function ( symbol, callback = false ) {
            signedRequest( sapi + 'v1/margin/openOrders', { symbol: symbol }, function ( error, json ) {
                if ( json.length === 0 ) {
                    if ( callback ) return callback.call( this, 'No orders present for this symbol', {}, symbol );
                }
                for ( let obj of json ) {
                    let quantity = obj.origQty - obj.executedQty;
                    Binance.options.log( 'cancel order: ' + obj.side + ' ' + symbol + ' ' + quantity + ' @ ' + obj.price + ' #' + obj.orderId );
                    signedRequest( sapi + 'v1/margin/order', { symbol: symbol, orderId: obj.orderId }, function ( error, data ) {
                        if ( callback ) return callback.call( this, error, data, symbol );
                    }, 'DELETE' );
                }
            } );
        },

        /**
         * Transfer from main account to margin account
         * @param {string} asset - the asset
         * @param {number} amount - the asset
         * @param {function} callback - the callback function
         * @param {object} options - additional options
         * @return {undefined}
         */
        mgTransferMainToMargin: function ( asset, amount, callback ) {
            let parameters = Object.assign( { asset: asset, amount: amount, type: 1 } );
            signedRequest( sapi + 'v1/margin/transfer', parameters, function ( error, data ) {
                if ( callback ) return callback( error, data );
            }, 'POST' );
        },

        /**
         * Transfer from margin account to main account
         * @param {string} asset - the asset
         * @param {number} amount - the asset
         * @param {function} callback - the callback function
         * @return {undefined}
         */
        mgTransferMarginToMain: function ( asset, amount, callback ) {
            let parameters = Object.assign( { asset: asset, amount: amount, type: 2 } );
            signedRequest( sapi + 'v1/margin/transfer', parameters, function ( error, data ) {
                if ( callback ) return callback( error, data );
            }, 'POST' );
        },

        /**
         * Get maximum transfer-out amount of an asset
         * @param {string} asset - the asset
         * @param {function} callback - the callback function
         * @return {undefined}
         */
        maxTransferable: function ( asset, callback ) {
            signedRequest( sapi + 'v1/margin/maxTransferable', { asset: asset }, function( error, data ) {
                if( callback ) return callback( error, data );
            });
        },

        /**
         * Margin account borrow/loan
         * @param {string} asset - the asset
         * @param {number} amount - the asset
         * @param {function} callback - the callback function
         * @return {undefined}
         */
        mgBorrow: function ( asset, amount, callback ) {
            let parameters = Object.assign( { asset: asset, amount: amount } );
            signedRequest( sapi + 'v1/margin/loan', parameters, function ( error, data ) {
                if ( callback ) return callback( error, data );
            }, 'POST' );
        },

        /**
         * Margin account repay
         * @param {string} asset - the asset
         * @param {number} amount - the asset
         * @param {function} callback - the callback function
         * @return {undefined}
         */
        mgRepay: function ( asset, amount, callback ) {
            let parameters = Object.assign( { asset: asset, amount: amount } );
            signedRequest( sapi + 'v1/margin/repay', parameters, function ( error, data ) {
                if ( callback ) return callback( error, data );
            }, 'POST' );
        },
        /**
         * Margin account details
         * @param {function} callback - the callback function
         * @return {undefined}
         */
        mgAccount: function( callback ) {
            signedRequest( sapi + 'v1/margin/account', {}, function( error, data ) {
                if( callback ) return callback( error, data );
            });
        },
        /**
         * Get maximum borrow amount of an asset
         * @param {string} asset - the asset
         * @param {function} callback - the callback function
         * @return {undefined}
         */
        maxBorrowable: function ( asset, callback ) {
            signedRequest( sapi + 'v1/margin/maxBorrowable', { asset: asset }, function( error, data ) {
                if( callback ) return callback( error, data );
            });
        },

        websockets: {
            /**
             * Userdata websockets function
             * @param {function} callback - the callback function
             * @param {function} execution_callback - optional execution callback
             * @param {function} subscribed_callback - subscription callback
             * @param {function} list_status_callback - status callback
             * @return {undefined}
             */
            userData: function userData( callback, execution_callback = false, subscribed_callback = false, list_status_callback = false ) {
                let reconnect = () => {
                    if ( Binance.options.reconnect ) userData( callback, execution_callback, subscribed_callback );
                };
                apiRequest( base + 'v1/userDataStream', {}, function ( error, response ) {
                    Binance.options.listenKey = response.listenKey;
                    setTimeout( function userDataKeepAlive() { // keepalive
                        try {
                            apiRequest( base + 'v1/userDataStream?listenKey=' + Binance.options.listenKey, {}, function ( err ) {
                                if ( err ) setTimeout( userDataKeepAlive, 60000 ); // retry in 1 minute
                                else setTimeout( userDataKeepAlive, 60 * 30 * 1000 ); // 30 minute keepalive
                            }, 'PUT' );
                        } catch ( error ) {
                            setTimeout( userDataKeepAlive, 60000 ); // retry in 1 minute
                        }
                    }, 60 * 30 * 1000 ); // 30 minute keepalive
                    Binance.options.balance_callback = callback;
                    Binance.options.execution_callback = execution_callback;
                    Binance.options.list_status_callback = list_status_callback;
                    const subscription = subscribe( Binance.options.listenKey, userDataHandler, reconnect );
                    if ( subscribed_callback ) subscribed_callback( subscription.endpoint );
                }, 'POST' );
            },

            /**
             * Margin Userdata websockets function
             * @param {function} callback - the callback function
             * @param {function} execution_callback - optional execution callback
             * @param {function} subscribed_callback - subscription callback
             * @param {function} list_status_callback - status callback
             * @return {undefined}
             */
            userMarginData: function userMarginData( callback, execution_callback = false, subscribed_callback = false, list_status_callback = false ) {
                let reconnect = () => {
                    if ( Binance.options.reconnect ) userMarginData( callback, execution_callback, subscribed_callback );
                };
                apiRequest( sapi + 'v1/userDataStream', {}, function ( error, response ) {
                    Binance.options.listenMarginKey = response.listenKey;
                    setTimeout( function userDataKeepAlive() { // keepalive
                        try {
                            apiRequest( sapi + 'v1/userDataStream?listenKey=' + Binance.options.listenMarginKey, {}, function ( err ) {
                                if ( err ) setTimeout( userDataKeepAlive, 60000 ); // retry in 1 minute
                                else setTimeout( userDataKeepAlive, 60 * 30 * 1000 ); // 30 minute keepalive
                            }, 'PUT' );
                        } catch ( error ) {
                            setTimeout( userDataKeepAlive, 60000 ); // retry in 1 minute
                        }
                    }, 60 * 30 * 1000 ); // 30 minute keepalive
                    Binance.options.margin_balance_callback = callback;
                    Binance.options.margin_execution_callback = execution_callback;
                    Binance.options.margin_list_status_callback = list_status_callback;
                    const subscription = subscribe( Binance.options.listenMarginKey, userMarginDataHandler, reconnect );
                    if ( subscribed_callback ) subscribed_callback( subscription.endpoint );
                }, 'POST' );
            },

            /**
             * Subscribe to a generic websocket
             * @param {string} url - the websocket endpoint
             * @param {function} callback - optional execution callback
             * @param {boolean} reconnect - subscription callback
             * @return {WebSocket} the websocket reference
             */
            subscribe: function ( url, callback, reconnect = false ) {
                return subscribe( url, callback, reconnect );
            },

            /**
             * Subscribe to a generic combined websocket
             * @param {string} url - the websocket endpoint
             * @param {function} callback - optional execution callback
             * @param {boolean} reconnect - subscription callback
             * @return {WebSocket} the websocket reference
             */
            subscribeCombined: function ( url, callback, reconnect = false ) {
                return subscribeCombined( url, callback, reconnect );
            },

            /**
             * Returns the known websockets subscriptions
             * @return {array} array of web socket subscriptions
             */
            subscriptions: function() {
                return Binance.subscriptions;
            },

            /**
             * Terminates a web socket
             * @param {string} endpoint - the string associated with the endpoint
             * @return {undefined}
             */
            terminate: function ( endpoint ) {
                if ( Binance.options.verbose ) Binance.options.log( 'WebSocket terminating:', endpoint );
                return terminate( endpoint );
            },

            /**
             * Websocket depth chart
             * @param {array/string} symbols - an array or string of symbols to query
             * @param {function} callback - callback function
             * @return {string} the websocket endpoint
             */
            depth: function depth ( symbols, callback ) {
                let reconnect = () => {
                    if ( Binance.options.reconnect ) depth( symbols, callback );
                };
                let subscription;
                if ( Array.isArray( symbols ) ) {
                    if ( !isArrayUnique( symbols ) ) throw Error( 'depth: "symbols" cannot contain duplicate elements.' );
                    let streams = symbols.map( function ( symbol ) {
                        return symbol.toLowerCase() + '@depth';
                    } );
                    subscription = subscribeCombined( streams, callback, reconnect );
                } else {
                    let symbol = symbols;
                    subscription = subscribe( symbol.toLowerCase() + '@depth', callback, reconnect );
                }
                return subscription.endpoint;
            },

            /**
             * Websocket depth cache
             * @param {array/string} symbols - an array or string of symbols to query
             * @param {function} callback - callback function
             * @param {int} limit - the number of entries
             * @return {string} the websocket endpoint
             */
            depthCache: function depthCacheFunction( symbols, callback, limit = 500 ) {
                let reconnect = () => {
                    if ( Binance.options.reconnect ) depthCacheFunction( symbols, callback, limit );
                };

                let symbolDepthInit = symbol => {
                    if ( typeof Binance.depthCacheContext[symbol] === 'undefined' ) Binance.depthCacheContext[symbol] = {};
                    let context = Binance.depthCacheContext[symbol];
                    context.snapshotUpdateId = null;
                    context.lastEventUpdateId = null;
                    context.messageQueue = [];
                    Binance.depthCache[symbol] = { bids: {}, asks: {} };
                };

                let assignEndpointIdToContext = ( symbol, endpointId ) => {
                    if ( Binance.depthCacheContext[symbol] ) {
                        let context = Binance.depthCacheContext[symbol];
                        context.endpointId = endpointId;
                    }
                };

                let handleDepthStreamData = depth => {
                    let symbol = depth.s;
                    let context = Binance.depthCacheContext[symbol];
                    if ( context.messageQueue && !context.snapshotUpdateId ) {
                        context.messageQueue.push( depth );
                    } else {
                        try {
                            depthHandler( depth );
                        } catch ( err ) {
                            return terminate( context.endpointId, true );
                        }
                        if ( callback ) callback( symbol, Binance.depthCache[symbol], context );
                    }
                };

                let getSymbolDepthSnapshot = ( symbol, cb ) => {
                    publicRequest( base + 'v1/depth', { symbol: symbol, limit: limit }, function ( error, json ) {
                        if ( error ) {
                            return cb( error, null );
                        }
                        // Store symbol next use
                        json.symb = symbol;
                        cb( null, json )
                    } );
                };

                let updateSymbolDepthCache = json => {
                    // Get previous store symbol
                    let symbol = json.symb;
                    // Initialize depth cache from snapshot
                    Binance.depthCache[symbol] = depthData( json );
                    // Prepare depth cache context
                    let context = Binance.depthCacheContext[symbol];
                    context.snapshotUpdateId = json.lastUpdateId;
                    context.messageQueue = context.messageQueue.filter( depth => depth.u > context.snapshotUpdateId );
                    // Process any pending depth messages
                    for ( let depth of context.messageQueue ) {
                        /* Although sync errors shouldn't ever happen here, we catch and swallow them anyway
                         just in case. The stream handler function above will deal with broken caches. */
                        try {
                            depthHandler( depth );
                        } catch ( err ) {
                            // Do nothing
                        }
                    }
                    delete context.messageQueue;
                    if ( callback ) callback( symbol, Binance.depthCache[symbol] );
                };

                /* If an array of symbols are sent we use a combined stream connection rather.
                 This is transparent to the developer, and results in a single socket connection.
                 This essentially eliminates "unexpected response" errors when subscribing to a lot of data. */
                let subscription;
                if ( Array.isArray( symbols ) ) {
                    if ( !isArrayUnique( symbols ) ) throw Error( 'depthCache: "symbols" cannot contain duplicate elements.' );
                    symbols.forEach( symbolDepthInit );
                    let streams = symbols.map( function ( symbol ) {
                        return symbol.toLowerCase() + '@depth';
                    } );
                    subscription = subscribeCombined( streams, handleDepthStreamData, reconnect, function () {
                        async.mapLimit( symbols, 50, getSymbolDepthSnapshot, ( err, results ) => {
                            if ( err ) throw err;
                            results.forEach( updateSymbolDepthCache );
                        } );
                    } );
                    symbols.forEach( s => assignEndpointIdToContext( s, subscription.endpoint ) );
                } else {
                    let symbol = symbols;
                    symbolDepthInit( symbol );
                    subscription = subscribe( symbol.toLowerCase() + '@depth', handleDepthStreamData, reconnect, function () {
                        async.mapLimit( [symbol], 1, getSymbolDepthSnapshot, ( err, results ) => {
                            if ( err ) throw err;
                            results.forEach( updateSymbolDepthCache );
                        } );
                    } );
                    assignEndpointIdToContext( symbol, subscription.endpoint );
                }
                return subscription.endpoint;
            },

            /**
             * Clear Websocket depth cache
             * @param {String|Array} symbols   - a single symbol, or an array of symbols, to clear the cache of
             * @returns {void}
             */
            clearDepthCache( symbols ) {
                const symbolsArr = Array.isArray( symbols ) ? symbols : [symbols];
                symbolsArr.forEach( thisSymbol => {
                    delete Binance.depthCache[thisSymbol];
                } );
            },

            /**
             * Websocket staggered depth cache
             * @param {array/string} symbols - an array of symbols to query
             * @param {function} callback - callback function
             * @param {int} limit - the number of entries
             * @param {int} stagger - ms between each depth cache
             * @return {Promise} the websocket endpoint
             */
            depthCacheStaggered: function ( symbols, callback, limit = 100, stagger = 200 ) {
                if ( !Array.isArray( symbols ) ) symbols = [symbols];
                let chain = null;

                symbols.forEach( symbol => {
                    let promise = () => new Promise( resolve => {
                        this.depthCache( symbol, callback, limit );
                        setTimeout( resolve, stagger );
                    } );
                    chain = chain ? chain.then( promise ) : promise();
                } );

                return chain;
            },

            /**
             * Websocket aggregated trades
             * @param {array/string} symbols - an array or string of symbols to query
             * @param {function} callback - callback function
             * @return {string} the websocket endpoint
             */
            aggTrades: function trades( symbols, callback ) {
                let reconnect = () => {
                    if ( Binance.options.reconnect ) trades( symbols, callback );
                };
                let subscription;
                if ( Array.isArray( symbols ) ) {
                    if ( !isArrayUnique( symbols ) ) throw Error( 'trades: "symbols" cannot contain duplicate elements.' );
                    let streams = symbols.map( function ( symbol ) {
                        return symbol.toLowerCase() + '@aggTrade';
                    } );
                    subscription = subscribeCombined( streams, callback, reconnect );
                } else {
                    let symbol = symbols;
                    subscription = subscribe( symbol.toLowerCase() + '@aggTrade', callback, reconnect );
                }
                return subscription.endpoint;
            },

            /**
            * Websocket raw trades
            * @param {array/string} symbols - an array or string of symbols to query
            * @param {function} callback - callback function
            * @return {string} the websocket endpoint
            */
            trades: function trades( symbols, callback ) {
                let reconnect = () => {
                    if ( Binance.options.reconnect ) trades( symbols, callback );
                };

                let subscription;
                if ( Array.isArray( symbols ) ) {
                    if ( !isArrayUnique( symbols ) ) throw Error( 'trades: "symbols" cannot contain duplicate elements.' );
                    let streams = symbols.map( function ( symbol ) {
                        return symbol.toLowerCase() + '@trade';
                    } );
                    subscription = subscribeCombined( streams, callback, reconnect );
                } else {
                    let symbol = symbols;
                    subscription = subscribe( symbol.toLowerCase() + '@trade', callback, reconnect );
                }
                return subscription.endpoint;
            },

            /**
             * Websocket klines
             * @param {array/string} symbols - an array or string of symbols to query
             * @param {string} interval - the time interval
             * @param {function} callback - callback function
             * @param {int} limit - maximum results, no more than 1000
             * @return {string} the websocket endpoint
             */
            chart: function chart( symbols, interval, callback, limit = 500 ) {
                let reconnect = () => {
                    if ( Binance.options.reconnect ) chart( symbols, interval, callback, limit );
                };

                let symbolChartInit = symbol => {
                    if ( typeof Binance.info[symbol] === 'undefined' ) Binance.info[symbol] = {};
                    if ( typeof Binance.info[symbol][interval] === 'undefined' ) Binance.info[symbol][interval] = {};
                    if ( typeof Binance.ohlc[symbol] === 'undefined' ) Binance.ohlc[symbol] = {};
                    if ( typeof Binance.ohlc[symbol][interval] === 'undefined' ) Binance.ohlc[symbol][interval] = {};
                    if ( typeof Binance.ohlcLatest[symbol] === 'undefined' ) Binance.ohlcLatest[symbol] = {};
                    if ( typeof Binance.ohlcLatest[symbol][interval] === 'undefined' ) Binance.ohlcLatest[symbol][interval] = {};
                    if ( typeof Binance.klineQueue[symbol] === 'undefined' ) Binance.klineQueue[symbol] = {};
                    if ( typeof Binance.klineQueue[symbol][interval] === 'undefined' ) Binance.klineQueue[symbol][interval] = [];
                    Binance.info[symbol][interval].timestamp = 0;
                }

                let handleKlineStreamData = kline => {
                    let symbol = kline.s;
                    if ( !Binance.info[symbol][interval].timestamp ) {
                        if ( typeof ( Binance.klineQueue[symbol][interval] ) !== 'undefined' && kline !== null ) {
                            Binance.klineQueue[symbol][interval].push( kline );
                        }
                    } else {
                        //Binance.options.log('@klines at ' + kline.k.t);
                        klineHandler( symbol, kline );
                        if ( callback ) callback( symbol, interval, klineConcat( symbol, interval ) );
                    }
                };

                let getSymbolKlineSnapshot = ( symbol, limit = 500 ) => {
                    publicRequest( base + 'v1/klines', { symbol: symbol, interval: interval, limit: limit }, function ( error, data ) {
                        klineData( symbol, interval, data );
                        //Binance.options.log('/klines at ' + Binance.info[symbol][interval].timestamp);
                        if ( typeof Binance.klineQueue[symbol][interval] !== 'undefined' ) {
                            for ( let kline of Binance.klineQueue[symbol][interval] ) klineHandler( symbol, kline, Binance.info[symbol][interval].timestamp );
                            delete Binance.klineQueue[symbol][interval];
                        }
                        if ( callback ) callback( symbol, interval, klineConcat( symbol, interval ) );
                    } );
                };

                let subscription;
                if ( Array.isArray( symbols ) ) {
                    if ( !isArrayUnique( symbols ) ) throw Error( 'chart: "symbols" cannot contain duplicate elements.' );
                    symbols.forEach( symbolChartInit );
                    let streams = symbols.map( function ( symbol ) {
                        return symbol.toLowerCase() + '@kline_' + interval;
                    } );
                    subscription = subscribeCombined( streams, handleKlineStreamData, reconnect );
                    symbols.forEach( element => getSymbolKlineSnapshot( element, limit ) );
                } else {
                    let symbol = symbols;
                    symbolChartInit( symbol );
                    subscription = subscribe( symbol.toLowerCase() + '@kline_' + interval, handleKlineStreamData, reconnect );
                    getSymbolKlineSnapshot( symbol, limit );
                }
                return subscription.endpoint;
            },

            /**
             * Websocket candle sticks
             * @param {array/string} symbols - an array or string of symbols to query
             * @param {string} interval - the time interval
             * @param {function} callback - callback function
             * @return {string} the websocket endpoint
             */
            candlesticks: function candlesticks( symbols, interval, callback ) {
                let reconnect = () => {
                    if ( Binance.options.reconnect ) candlesticks( symbols, interval, callback );
                };

                /* If an array of symbols are sent we use a combined stream connection rather.
                 This is transparent to the developer, and results in a single socket connection.
                 This essentially eliminates "unexpected response" errors when subscribing to a lot of data. */
                let subscription;
                if ( Array.isArray( symbols ) ) {
                    if ( !isArrayUnique( symbols ) ) throw Error( 'candlesticks: "symbols" cannot contain duplicate elements.' );
                    let streams = symbols.map( function ( symbol ) {
                        return symbol.toLowerCase() + '@kline_' + interval;
                    } );
                    subscription = subscribeCombined( streams, callback, reconnect );
                } else {
                    let symbol = symbols.toLowerCase();
                    subscription = subscribe( symbol + '@kline_' + interval, callback, reconnect );
                }
                return subscription.endpoint;
            },

            /**
             * Websocket mini ticker
             * @param {function} callback - callback function
             * @return {string} the websocket endpoint
             */
            miniTicker: function miniTicker( callback ) {
                let reconnect = () => {
                    if ( Binance.options.reconnect ) miniTicker( callback );
                };
                let subscription = subscribe( '!miniTicker@arr', function ( data ) {
                    let markets = {};
                    for ( let obj of data ) {
                        markets[obj.s] = {
                            close: obj.c,
                            open: obj.o,
                            high: obj.h,
                            low: obj.l,
                            volume: obj.v,
                            quoteVolume: obj.q,
                            eventTime: obj.E
                        };
                    }
                    callback( markets );
                }, reconnect );
                return subscription.endpoint;
            },

            /**
             * Websocket prevday percentage
             * @param {array/string} symbols - an array or string of symbols to query
             * @param {function} callback - callback function
             * @param {boolean} singleCallback - avoid call one callback for each symbol in data array
             * @return {string} the websocket endpoint
             */
            prevDay: function prevDay( symbols, callback, singleCallback ) {
                let reconnect = () => {
                    if ( Binance.options.reconnect ) prevDay( symbols, callback );
                };

                let subscription;
                // Combine stream for array of symbols
                if ( Array.isArray( symbols ) ) {
                    if ( !isArrayUnique( symbols ) ) throw Error( 'prevDay: "symbols" cannot contain duplicate elements.' );
                    let streams = symbols.map( function ( symbol ) {
                        return symbol.toLowerCase() + '@ticker';
                    } );
                    subscription = subscribeCombined( streams, function ( data ) {
                        prevDayStreamHandler( data, callback );
                    }, reconnect );
                    // Raw stream for  a single symbol
                } else if ( symbols ) {
                    let symbol = symbols;
                    subscription = subscribe( symbol.toLowerCase() + '@ticker', function ( data ) {
                        prevDayStreamHandler( data, callback );
                    }, reconnect );
                    // Raw stream of all listed symbols
                } else {
                    subscription = subscribe( '!ticker@arr', function ( data ) {
                        if ( singleCallback ) {
                            prevDayStreamHandler( data, callback );
                        } else {
                            for ( let line of data ) {
                                prevDayStreamHandler( line, callback );
                            }
                        }
                    }, reconnect );
                }
                return subscription.endpoint;
            }
        }
    };
}
function instanceWrapper( options = {} ) {
    if ( new.target ) return api( options );
    return new api( options );
}
module.exports = instanceWrapper;
//https://github.com/binance-exchange/binance-official-api-docs