
import WebSocket from 'ws';
// import request from 'request';
import crypto from 'crypto';
import file from 'fs';
import url from 'url';
import JSONbig from 'json-bigint';
// @ts-ignore
import { HttpsProxyAgent } from 'https-proxy-agent';
// @ts-ignore
import { SocksProxyAgent } from 'socks-proxy-agent';
// @ts-ignore
import nodeFetch from 'node-fetch';

// @ts-ignore
import zip from 'lodash.zipobject';
import stringHash from 'string-hash';
// eslint-disable-next-line
import { Interval, PositionRisk, Order, FuturesOrder, PositionSide, WorkingType, OrderType, OrderStatus, TimeInForce, Callback, IConstructorArgs, OrderSide, FundingRate, CancelOrder, AggregatedTrade, Trade, MyTrade, WithdrawHistoryResponse, DepositHistoryResponse, DepositAddress, WithdrawResponse, Candle, FuturesCancelAllOpenOrder, OrderBook, Ticker, FuturesUserTrade, Account, FuturesAccountInfo, FuturesBalance, QueryOrder, HttpMethod, BookTicker, DailyStats, PremiumIndex, OpenInterest, IWebsocketsMethods, SymbolConfig, OCOOrder, FuturesAlgoOrder, CancelAlgoOrder } from './types.js';
export type * from './types.js';
// export { Interval, PositionRisk, Order, FuturesOrder, PositionSide, WorkingType, OrderType, OrderStatus, TimeInForce, Callback, IConstructorArgs, OrderSide, FundingRate, CancelOrder, AggregatedTrade, Trade, MyTrade, WithdrawHistoryResponse, DepositHistoryResponse, DepositAddress, WithdrawResponse, Candle, FuturesCancelAllOpenOrder, OrderBook, Ticker, FuturesUserTrade, FuturesAccountInfo, FuturesBalance, QueryOrder } from './types';

export interface Dictionary<T> {
    [key: string]: T;
}

export type Dict = Dictionary<any>;

export default class Binance {

    domain = 'com';
    base = `https://api.binance.${this.domain}/api/`;
    baseTest = `https://testnet.binance.vision/api/`;
    baseDemo = `https://demo-api.binance.com/api/`;
    wapi = `https://api.binance.${this.domain}/wapi/`;
    sapi = `https://api.binance.${this.domain}/sapi/`;
    fapi = `https://fapi.binance.${this.domain}/fapi/`;
    dapi = `https://dapi.binance.${this.domain}/dapi/`;
    fapiTest = `https://testnet.binancefuture.com/fapi/`;
    fapiDemo = `https://demo-fapi.binance.com/fapi/`;
    dapiTest = `https://testnet.binancefuture.com/dapi/`;
    dapiDemo = `https://demo-dapi.binance.com/dapi/`;
    fstream = `wss://fstream.binance.${this.domain}/stream?streams=`;
    fstreamSingle = `wss://fstream.binance.${this.domain}/ws/`;
    fstreamSingleTest = `wss://stream.binancefuture.${this.domain}/ws/`;
    fstreamSingleDemo = `wss://fstream.binancefuture.com/ws/`;
    fstreamTest = `wss://stream.binancefuture.${this.domain}/stream?streams=`;
    fstreamDemo = `wss://fstream.binancefuture.com/stream?streams=`;
    dstream = `wss://dstream.binance.${this.domain}/stream?streams=`;
    dstreamSingle = `wss://dstream.binance.${this.domain}/ws/`;
    dstreamSingleTest = `wss://dstream.binancefuture.${this.domain}/ws/`;
    dstreamSingleDemo = `wss://dstream.binancefuture.com/ws/`;
    dstreamTest = `wss://dstream.binancefuture.${this.domain}/stream?streams=`;
    dstreamDemo = `wss://dstream.binancefuture.com/stream?streams=`;
    stream = `wss://stream.binance.${this.domain}:9443/ws/`;
    streamTest = `wss://stream.testnet.binance.vision/ws/`;
    streamDemo = `wss://demo-stream.binance.com/ws/`;
    combineStream = `wss://stream.binance.${this.domain}:9443/stream?streams=`;
    combineStreamTest = `wss://stream.testnet.binance.vision/stream?streams=`;
    combineStreamDemo = `wss://demo-stream.binance.com/stream?streams=`;
    wsApi = `wss://ws-api.binance.${this.domain}:443/ws-api/v3`;
    wsApiTest = `wss://ws-api.testnet.binance.vision/ws-api/v3`;

    verbose = false;

    futuresListenKeyKeepAlive: number = 60 * 30 * 1000; // 30 minutes
    spotListenKeyKeepAlive: number = 60 * 30 * 1000; // 30 minutes
    heartBeatInterval: number = 30000; // 30 seconds

    // proxy variables
    urlProxy: string = undefined;
    httpsProxy: string = undefined;
    socksProxy: string = undefined;
    nodeFetch: any = undefined;

    APIKEY: string = undefined;
    APISECRET: string = undefined;
    PRIVATEKEY: string = undefined;
    PRIVATEKEYPASSWORD: string = undefined;
    test = false; // sandbox mode
    demo = false; // demo mode

    timeOffset: number = 0;

    userAgent = 'Mozilla/4.0 (compatible; Node Binance API)';
    contentType = 'application/x-www-form-urlencoded';
    SPOT_PREFIX = "x-TKT5PX2F";
    CONTRACT_PREFIX = "x-cvBPrNm9";

    // Websockets Options
    isAlive = false;
    socketHeartbeatInterval: any = null;
    // endpoint: string = ""; // endpoint for WS?
    reconnect = true;

    headers: Dict = {};
    subscriptions: Dict = {};
    futuresSubscriptions: Dict = {};
    wsApiConnections: Dict = {}; // WebSocket API connections
    wsApiPendingRequests: Dict = {}; // Pending JSON-RPC requests
    futuresInfo: Dict = {};
    futuresMeta: Dict = {};
    futuresTicks: Dict = {};
    futuresRealtime: Dict = {};
    futuresKlineQueue: Dict = {};
    deliverySubscriptions: Dict = {};
    deliveryInfo: Dict = {};
    deliveryMeta: Dict = {};
    deliveryTicks: Dict = {};
    deliveryRealtime: Dict = {};
    deliveryKlineQueue: Dict = {};
    depthCache: Dict = {};
    depthCacheContext: Dict = {};
    ohlcLatest: Dict = {};
    klineQueue: Dict = {};
    ohlc: Dict = {};
    info: Dict = {};

    websockets: IWebsocketsMethods = { // deprecated structure, keeping it for backwards compatibility
        userData: this.userData.bind(this),
        userMarginData: this.userMarginData.bind(this),
        depthCacheStaggered: this.depthCacheStaggered.bind(this),
        userFutureData: this.userFutureData.bind(this),
        userDeliveryData: this.userDeliveryData.bind(this),
        subscribeCombined: this.subscribeCombined.bind(this),
        subscribe: this.subscribe.bind(this),
        subscriptions: () => this.subscriptions,
        terminate: this.terminate.bind(this),
        depth: this.depthStream.bind(this),
        depthCache: this.depthCacheStream.bind(this),
        clearDepthCache: this.clearDepthCache.bind(this),
        aggTrades: this.aggTradesStream.bind(this),
        trades: this.tradesStream.bind(this),
        chart: this.chart.bind(this),
        candlesticks: this.candlesticksStream.bind(this),
        miniTicker: this.miniTicker.bind(this),
        bookTickers: this.bookTickersStream.bind(this),
        prevDay: this.prevDayStream.bind(this),
        futuresCandlesticks: this.futuresCandlesticksStream.bind(this),
        futuresTicker: this.futuresTickerStream.bind(this),
        futuresMiniTicker: this.futuresMiniTickerStream.bind(this),
        futuresAggTrades: this.futuresAggTradeStream.bind(this),
        futuresMarkPrice: this.futuresMarkPriceStream.bind(this),
        futuresLiquidation: this.futuresLiquidationStream.bind(this),
        futuresBookTicker: this.futuresBookTickerStream.bind(this),
        futuresChart: this.futuresChart.bind(this),
        deliveryAggTrade: this.deliveryAggTradeStream.bind(this),
        deliveryCandlesticks: this.deliveryCandlesticks.bind(this),
        deliveryTicker: this.deliveryTickerStream.bind(this),
        deliveryMiniTicker: this.deliveryMiniTickerStream.bind(this),
        deliveryMarkPrice: this.deliveryMarkPriceStream.bind(this),
        deliveryBookTicker: this.deliveryBookTickerStream.bind(this),
        deliveryChart: this.deliveryChart.bind(this),
        deliveryLiquidation: this.deliveryLiquidationStream.bind(this),
        futuresSubcriptions: () => this.futuresSubscriptions,
        deliverySubcriptions: () => this.deliverySubscriptions,
        futuresTerminate: this.futuresTerminate.bind(this),
        deliveryTerminate: this.deliveryTerminate.bind(this),
    };

    default_options = {
        recvWindow: 5000,
        useServerTime: false,
        reconnect: true,
        keepAlive: true,
        verbose: false,
        test: false,
        demo: false,
        hedgeMode: false,
        localAddress: false,
        family: 4,
        log(...args) {
            console.log(Array.prototype.slice.call(args));
        }
    };

    Options: any = {
    };

    constructor(userOptions: Partial<IConstructorArgs> | string = {}) {

        if (userOptions) {
            this.setOptions(userOptions);
        }

    }

    options(opt = {}): Binance {
        // // return await this.setOptions(opt, callback); // keep this method for backwards compatibility
        // this.assignOptions(opt, callback);
        this.setOptions(opt);
        return this;
    }

    assignOptions(opt = {}) {
        if (typeof opt === 'string') { // Pass json config filename
            this.Options = JSON.parse(file.readFileSync(opt) as any);
        } else this.Options = opt;
        if (!this.Options.recvWindow) this.Options.recvWindow = this.default_options.recvWindow;
        if (!this.Options.useServerTime) this.Options.useServerTime = this.default_options.useServerTime;
        if (!this.Options.reconnect) this.Options.reconnect = this.default_options.reconnect;
        if (!this.Options.test) this.Options.test = this.default_options.test;
        if (!this.Options.hedgeMode) this.Options.hedgeMode = this.default_options.hedgeMode;
        if (!this.Options.log) this.Options.log = this.default_options.log;
        if (!this.Options.verbose) this.Options.verbose = this.default_options.verbose;
        if (!this.Options.keepAlive) this.Options.keepAlive = this.default_options.keepAlive;
        if (!this.Options.localAddress) this.Options.localAddress = this.default_options.localAddress;
        if (!this.Options.family) this.Options.family = this.default_options.family;
        if (this.Options.urls !== undefined) {
            const { urls } = this.Options;
            if (urls.base) this.base = urls.base;
            if (urls.wapi) this.wapi = urls.wapi;
            if (urls.sapi) this.sapi = urls.sapi;
            if (urls.fapi) this.fapi = urls.fapi;
            if (urls.fapiTest) this.fapiTest = urls.fapiTest;
            if (urls.stream) this.stream = urls.stream;
            if (urls.combineStream) this.combineStream = urls.combineStream;
            if (urls.fstream) this.fstream = urls.fstream;
            if (urls.fstreamSingle) this.fstreamSingle = urls.fstreamSingle;
            if (urls.fstreamTest) this.fstreamTest = urls.fstreamTest;
            if (urls.fstreamSingleTest) this.fstreamSingleTest = urls.fstreamSingleTest;
            if (urls.dstream) this.dstream = urls.dstream;
            if (urls.dstreamSingle) this.dstreamSingle = urls.dstreamSingle;
            if (urls.dstreamTest) this.dstreamTest = urls.dstreamTest;
            if (urls.dstreamSingleTest) this.dstreamSingleTest = urls.dstreamSingleTest;
        }

        if (this.Options.APIKEY) this.APIKEY = this.Options.APIKEY;
        if (this.Options.APISECRET) this.APISECRET = this.Options.APISECRET;
        if (this.Options.PRIVATEKEY) this.PRIVATEKEY = this.Options.PRIVATEKEY;
        if (this.Options.PRIVATEKEYPASSWORD) this.PRIVATEKEYPASSWORD = this.Options.PRIVATEKEYPASSWORD;
        if (this.Options.test) this.test = true;
        if (this.Options.demo) this.demo = true;
        if (this.Options.headers) this.headers = this.Options.Headers;
        if (this.Options.domain) this.domain = this.Options.domain;
        if (this.Options.httpsProxy) this.httpsProxy = this.Options.httpsProxy;
    }

    async setOptions(opt = {}): Promise<Binance> {

        this.assignOptions(opt);
        if (this.Options.useServerTime) {
            const res = await this.publicSpotRequest('v3/time');
            this.timeOffset = res.serverTime - new Date().getTime();
        }
        return this;
    }

    // ---- HELPER FUNCTIONS ---- //

    extend = (...args: any[]) => Object.assign({}, ...args);

    getSpotUrl() {
        if (this.Options.demo) return this.baseDemo;
        if (this.Options.test) return this.baseTest;
        return this.base;
    }

    getSapiUrl() {
        return this.sapi;
    }

    getFapiUrl() {
        if (this.Options.demo) return this.fapiDemo;
        if (this.Options.test) return this.fapiTest;
        return this.fapi;
    }

    getDapiUrl() {
        if (this.Options.demo) return this.dapiDemo;
        if (this.Options.test) return this.dapiTest;
        return this.dapi;
    }

    getCombineStreamUrl() {
        if (this.Options.demo) return this.combineStreamDemo;
        if (this.Options.test) return this.combineStreamTest;
        return this.combineStream;
    }

    getStreamUrl() {
        if (this.Options.demo) return this.streamDemo;
        if (this.Options.test) return this.streamTest;
        return this.stream;
    }

    getWsApiUrl() {
        if (this.Options.test) return this.wsApiTest;
        return this.wsApi;
    }

    getDStreamSingleUrl() {
        if (this.Options.demo) return this.dstreamSingleDemo;
        if (this.Options.test) return this.dstreamSingleTest;
        return this.dstreamSingle;
    }

    /**
     * Classify a futures stream endpoint into public, market, or private category
     * per Binance USDⓈ-M Futures WebSocket URL split (2026-03-06)
     */
    classifyFuturesStream(endpoint: string): 'public' | 'market' | 'private' {
        // Public: bookTicker and depth streams (high-frequency)
        if (endpoint.includes('@bookTicker') || endpoint === '!bookTicker'
            || endpoint.includes('@depth')) {
            return 'public';
        }
        // Private: listenKey is a long alphanumeric string (60+ chars, no @ or !)
        if (/^[A-Za-z0-9]{20,}$/.test(endpoint)) {
            return 'private';
        }
        // Market: aggTrade, markPrice, kline, ticker, miniTicker, forceOrder, etc.
        return 'market';
    }

    getFStreamSingleUrl(category?: 'public' | 'market' | 'private') {
        if (category) {
            if (this.Options.demo) return `wss://fstream.binancefuture.com/${category}/ws/`;
            if (this.Options.test) return `wss://stream.binancefuture.${this.domain}/${category}/ws/`;
            return `wss://fstream.binance.${this.domain}/${category}/ws/`;
        }
        if (this.Options.demo) return this.fstreamSingleDemo;
        if (this.Options.test) return this.fstreamSingleTest;
        return this.fstreamSingle;
    }

    getFStreamUrl(category?: 'public' | 'market' | 'private') {
        if (category) {
            if (this.Options.demo) return `wss://fstream.binancefuture.com/${category}/stream?streams=`;
            if (this.Options.test) return `wss://stream.binancefuture.${this.domain}/${category}/stream?streams=`;
            return `wss://fstream.binance.${this.domain}/${category}/stream?streams=`;
        }
        if (this.Options.demo) return this.fstreamDemo;
        if (this.Options.test) return this.fstreamTest;
        return this.fstream;
    }

    getDStreamUrl() {
        if (this.Options.demo) return this.dstreamDemo;
        if (this.Options.test) return this.dstreamTest;
        return this.dstream;
    }

    uuid22(a?: any) {
        return a ? (a ^ Math.random() * 16 >> a / 4).toString(16) : (([1e7] as any) + 1e3 + 4e3 + 8e5).replace(/[018]/g, this.uuid22);
    }

    getUrlProxy() {
        if (this.urlProxy) {
            return this.urlProxy;
        }
        return undefined;
    }

    getHttpsProxy() {
        if (this.httpsProxy) {
            return this.httpsProxy;
        }
        if (process.env.https_proxy) {
            return process.env.https_proxy;
        }
        return undefined;
    }

    getSocksProxy() {
        if (this.socksProxy) {
            return this.socksProxy;
        }
        if (process.env.socks_proxy) {
            return process.env.socks_proxy;
        }
        return undefined;
    }

    // ------ Request Related Functions ------ //

    /**
     * Replaces socks connection uri hostname with IP address
     * @param {string} connString - socks connection string
     * @return {string} modified string with ip address
     */
    proxyReplacewithIp(connString: string) {
        return connString;
    }

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

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

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

    async reqHandler(response) {
        this.info.lastRequest = new Date().getTime();
        if (response) {
            this.info.statusCode = response.status || 0;
            if (response.request) this.info.lastURL = response.request.uri.href;
            if (response.headers) {
                this.info.usedWeight = response.headers['x-mbx-used-weight-1m'] || 0;
                this.info.orderCount1s = response.headers['x-mbx-order-count-1s'] || 0;
                this.info.orderCount1m = response.headers['x-mbx-order-count-1m'] || 0;
                this.info.orderCount1h = response.headers['x-mbx-order-count-1h'] || 0;
                this.info.orderCount1d = response.headers['x-mbx-order-count-1d'] || 0;
            }
        }

        if (response && response.status !== 200) {
            // let parsedResponse = '';
            // try {
            //     parsedResponse = await response.json();
            // } catch (e) {
            //     parsedResponse = await response.text();
            // }
            const error = new Error(await response.text());
            // error.code = response.status;
            // error.url = response.url;
            throw error;
        }
    }

    async proxyRequest(opt: any) {
        const urlBody = new URLSearchParams(opt.form);
        const reqOptions: Dict = {
            method: opt.method,
            headers: opt.headers,
        };
        if (opt.method !== 'GET') {
            reqOptions.body = urlBody;
        }
        if (this.Options.verbose) {
            this.Options.log('HTTP Request:', opt.method, opt.url, reqOptions);
        }

        // https-proxy
        const httpsproxy = this.getHttpsProxy();
        const socksproxy = this.getSocksProxy();
        const urlProxy = this.getUrlProxy();
        if (httpsproxy) {
            if (this.Options.verbose) this.Options.log('using https proxy: ' + httpsproxy);
            reqOptions.agent = new HttpsProxyAgent(httpsproxy);
        } else if (socksproxy) {
            if (this.Options.verbose) this.Options.log('using socks proxy: ' + socksproxy);
            reqOptions.agent = new SocksProxyAgent(socksproxy);
        }

        if (urlProxy) {
            opt.url = urlProxy + opt.url;
        }

        // Apply timeout via AbortController
        const timeout = opt.timeout || this.Options.recvWindow || 30000;
        const controller = new AbortController();
        const timeoutId = setTimeout(() => controller.abort(), timeout);
        reqOptions.signal = controller.signal;

        let fetchImplementation = fetch;
        // require node-fetch
        if (reqOptions.agent) {
            fetchImplementation = nodeFetch;
        }

        try {
            const response = await fetchImplementation(opt.url, reqOptions);
            clearTimeout(timeoutId);

            await this.reqHandler(response);
            const json = await response.json();

            if (this.Options.verbose) {
                this.Options.log('HTTP Response:', json);
            }
            return json;
        } catch (error) {
            clearTimeout(timeoutId);
            if (error.name === 'AbortError') {
                throw new Error(`Request timeout: ${opt.method} ${opt.url} (${timeout}ms)`);
            }
            throw error;
        }
    }

    reqObj(url: string, data: Dict = {}, method: HttpMethod = 'GET', key?: string) {
        return {
            url: url,
            qs: data,
            method: method,
            family: this.Options.family,
            localAddress: this.Options.localAddress,
            timeout: this.Options.recvWindow,
            forever: this.Options.keepAlive,
            headers: {
                'User-Agent': this.userAgent,
                'Content-type': this.contentType,
                'X-MBX-APIKEY': key || ''
            }
        };
    }

    reqObjPOST(url: string, data: Dict = {}, method = 'POST', key: string) {
        return {
            url: url,
            form: data,
            method: method,
            family: this.Options.family,
            localAddress: this.Options.localAddress,
            timeout: this.Options.recvWindow,
            forever: this.Options.keepAlive,
            qsStringifyOptions: {
                arrayFormat: 'repeat'
            },
            headers: {
                'User-Agent': this.userAgent,
                'Content-type': this.contentType,
                'X-MBX-APIKEY': key || ''
            }
        };
    }

    async publicRequest(url: string, data: Dict = {}, method: HttpMethod = 'GET') {
        const query = this.makeQueryString(data);
        const opt = this.reqObj(url + (query ? '?' + query : ''), data, method);
        const res = await this.proxyRequest(opt);
        return res;
    }

    /**
     * Used to make public requests to the futures (FAPI) API
     * @param path
     * @param data
     * @param method
     * @returns
     */
    async publicFuturesRequest(path: string, data: Dict = {}, method: HttpMethod = 'GET') {
        return await this.publicRequest(this.getFapiUrl() + path, data, method);
    }

    /**
     * Used to make public requests to the delivery (DAPI) API
     * @param path
     * @param data
     * @param method
     * @returns
     */
    async publicDeliveryRequest(path: string, data: Dict = {}, method: HttpMethod = 'GET') {
        return await this.publicRequest(this.getDapiUrl() + path, data, method);
    }

    /**
     * Used to make private requests to the futures (FAPI) API
     * @param path
     * @param data
     * @param method
     * @returns
     */
    async privateFuturesRequest(path: string, data: Dict = {}, method: HttpMethod = 'GET'): Promise<any> {
        return await this.futuresRequest(this.getFapiUrl() + path, data, method, true);
    }

    /**
     * Used to make private requests to the delivery (DAPI) API
     * @param path
     * @param data
     * @param method
     * @returns
     */
    async privateDeliveryRequest(path: string, data: Dict = {}, method: HttpMethod = 'GET'): Promise<any> {
        return await this.futuresRequest(this.getDapiUrl() + path, data, method, true);
    }

    /**
     * Used to make a request to the futures API, this is a generic function that can be used to make any request to the futures API
     * @param url
     * @param data
     * @param method
     * @param isPrivate
     * @returns
     */
    async futuresRequest(url: string, data: Dict = {}, method: HttpMethod = 'GET', isPrivate = false) {
        let query = '';
        const headers = {
            'User-Agent': this.userAgent,
            'Content-type': 'application/x-www-form-urlencoded'
        } as Dict;

        if (isPrivate) {
            if (!data.recvWindow) data.recvWindow = this.Options.recvWindow;
            this.requireApiKey('promiseRequest');
            headers['X-MBX-APIKEY'] = this.APIKEY;
        }

        const opt = {
            headers: this.extend(headers, this.headers),
            url: url,
            method: method,
            timeout: this.Options.recvWindow,
            followAllRedirects: true
        };
        query = this.makeQueryString(data);
        if (method === 'GET') {
            opt.url = `${url}?${query}`;
        }
        if (isPrivate) {
            data.timestamp = new Date().getTime();
            if (this.timeOffset) {
                data.timestamp += this.timeOffset;
            }
            query = this.makeQueryString(data);
            data.signature = this.generateSignature(query);
            opt.url = `${url}?${query}&signature=${data.signature}`;
        }
        (opt as any).qs = data;
        const response = await this.proxyRequest(opt);
        return response;

    }

    // ------ Request Related Functions ------ //

    // XXX: This one works with array (e.g. for dust.transfer)
    // XXX: I _guess_ we could use replace this function with the `qs` module
    makeQueryString(q) {

        const res = Object.keys(q)
            .reduce((a, k) => {
                if (Array.isArray(q[k])) {
                    q[k].forEach(v => {
                        a.push(k + "=" + encodeURIComponent(v));
                    });
                } else if (q[k] !== undefined) {
                    a.push(k + "=" + encodeURIComponent(q[k]));
                }
                return a;
            }, [])
            .join("&");
        return res;
    }

    /**
     * Create a http request to the public API
     * @param {string} url - The http endpoint
     * @param {object} data - The data to send
     * @param {function} callback - The callback method to call
     * @param {string} method - the http method
     * @return {undefined}
     */
    async apiRequest(url: string, data: Dict = {}, method: HttpMethod = 'GET') {
        this.requireApiKey('apiRequest');
        const opt = this.reqObj(
            url,
            data,
            method,
            this.APIKEY
        );
        const res = await this.proxyRequest(opt);
        return res;
    }

    requireApiKey(source = 'requireApiKey', fatalError = true) {
        if (!this.APIKEY) {
            if (fatalError) throw Error(`${source}: Invalid API Key!`);
            return false;
        }
        return true;
    }

    // Check if API secret is present
    requireApiSecret(source = 'requireApiSecret', fatalError = true) {
        if (!this.APIKEY) {
            if (fatalError) throw Error(`${source}: Invalid API Key!`);
            return false;
        }
        if (!this.APISECRET && !this.PRIVATEKEY) {
            if (fatalError) throw Error(`${source}: Invalid API Secret or Private Key!`);
            return false;
        }
        return true;
    }

    /**
     * Create a public spot/margin request
     * @param {string} path - url path
     * @param {object} data - The data to send
     * @param {string} method - the http method
     * @param {boolean} noDataInSignature - Prevents data from being added to signature
     * @return {undefined}
     */
    async publicSpotRequest(path: string, data: Dict = {}, method: HttpMethod = 'GET') {
        return await this.publicRequest/**/(this.getSpotUrl() + path, data, method);
    }

    /**
     * Create a signed spot request
     * @param {string} path - url path
     * @param {object} data - The data to send
     * @param {string} method - the http method
     * @param {boolean} noDataInSignature - Prevents data from being added to signature
     * @return {undefined}
     */
    async privateSpotRequest(path: string, data: Dict = {}, method: HttpMethod = 'GET', noDataInSignature = false) {
        return await this.signedRequest/**/(this.getSpotUrl() + path, data, method, noDataInSignature);
    }

    /**
     * Create a signed SAPI request
     */
    async privateSapiRequest(path: string, data: Dict = {}, method: HttpMethod = 'GET', noDataInSignature = false) {
        return await this.signedRequest/**/(this.getSapiUrl() + path, data, method, noDataInSignature);
    }

    /**
     * Create a signed http request
     * @param {string} url - The http endpoint
     * @param {object} data - The data to send
     * @param {function} callback - The callback method to call
     * @param {string} method - the http method
     * @param {boolean} noDataInSignature - Prevents data from being added to signature
     * @return {undefined}
     */
    async signedRequest(url: string, data: Dict = {}, method: HttpMethod = 'GET', noDataInSignature = false) {
        this.requireApiSecret('signedRequest');
        const isListenKeyEndpoint = url.includes('v3/userDataStream');

        let query = method === 'POST' && noDataInSignature ? '' : this.makeQueryString(data);

        let signature = undefined;
        if (!noDataInSignature && !isListenKeyEndpoint) {
            data.timestamp = new Date().getTime();

            if (this.timeOffset) data.timestamp += this.timeOffset;

            if (!data.recvWindow) data.recvWindow = this.Options.recvWindow;
            query = this.makeQueryString(data);
            signature = this.generateSignature(query);
        }

        if (method === 'POST') {
            const opt = this.reqObjPOST(
                url,
                data,
                method,
                this.APIKEY
            );
            if (signature) {
                opt.form.signature = signature;
            }
            const reqPost = await this.proxyRequest(opt);
            return reqPost;
        } else {
            let encodedUrl = url;
            if (query) encodedUrl += '?' + query;
            if (signature) encodedUrl += '&signature=' + signature;
            const opt = this.reqObj(
                encodedUrl,
                data,
                method,
                this.APIKEY
            );
            const reqGet = await this.proxyRequest(opt);
            return reqGet;
        }
    }

    generateSignature(query: string, encode = true) {
        const secret = this.APISECRET || this.PRIVATEKEY;
        let signature = '';
        if (secret.includes('PRIVATE KEY')) {
            // if less than the below length, then it can't be RSA key
            let keyObject: crypto.KeyObject;
            try {
                const privateKeyObj: crypto.PrivateKeyInput = { key: secret };

                if (this.PRIVATEKEYPASSWORD) {
                    privateKeyObj.passphrase = this.PRIVATEKEYPASSWORD;
                }

                keyObject = crypto.createPrivateKey(privateKeyObj);

            } catch (e) {
                throw new Error(
                    'Invalid private key. Please provide a valid RSA or ED25519 private key. ' + e.toString()
                );
            }

            if (secret.length > 120) {
                // RSA key
                signature = crypto
                    .sign('RSA-SHA256', Buffer.from(query), keyObject)
                    .toString('base64');
                if (encode) signature = encodeURIComponent(signature);
                return signature;
            } else {
                // Ed25519 key
                signature = crypto.sign(null, Buffer.from(query), keyObject).toString('base64');
            }
        } else {
            signature = crypto.createHmac('sha256', this.Options.APISECRET).update(query).digest('hex'); // set the HMAC hash header
        }
        return signature;
    }

    // --- ENDPOINTS --- //

    /**
     * Create a signed spot order
     * @see https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#new-order-trade
     * @see https://developers.binance.com/docs/binance-spot-api-docs/rest-api/public-api-endpoints#test-new-order-trade
     * @see https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#new-order-list---oco-trade
     * @param {OrderType} type - LIMIT, MARKET, STOP_LOSS, STOP_LOSS_LIMIT, TAKE_PROFIT, TAKE_PROFIT_LIMIT, LIMIT_MAKER, OCO
     * @param {OrderSide} side - BUY or SELL
     * @param {string} symbol - The symbol to buy or sell
     * @param {string} quantity - The quantity to buy or sell
     * @param {string} price - The price per unit to transact each unit at
     * @param {object} params - additional order settings
     * @param {number} [params.quoteOrderQty] - The quote order quantity, used for MARKET orders
     * @param {number} [params.stopPrice] - The stop price, used for STOP_LOSS, STOP_LOSS_LIMIT, TAKE_PROFIT, TAKE_PROFIT_LIMIT orders
     * @param {number} [params.trailingDelta] - Delta price
     * @return {undefined}
     */
    async order(type: OrderType, side: OrderSide, symbol: string, quantity: number, price?: number, params: Dict = {}): Promise<Order> {
        const isOCO = type === 'OCO' || params.type === 'OCO';
        let endpoint = isOCO ? 'v3/orderList/oco' : 'v3/order';
        if (params.test) {
            delete params.test;
            endpoint += '/test';
        }
        const request = {
            symbol: symbol,
            side: side,
            // type: type
        } as Dict;
        if (!isOCO) request.type = type;
        if (params.quoteOrderQty && params.quoteOrderQty > 0)
            request.quoteOrderQty = params.quoteOrderQty;
        else
            request.quantity = quantity;

        if (!isOCO && request.type.includes('LIMIT')) {
            request.price = price;
            if (request.type !== 'LIMIT_MAKER') {
                request.timeInForce = 'GTC';
            }
        }
        if (!isOCO && request.type == 'MARKET' && typeof params.quoteOrderQty !== 'undefined') {
            request.quoteOrderQty = params.quoteOrderQty;
            delete request.quantity;
        }
        // if (typeof params.timeInForce !== 'undefined') opt.timeInForce = params.timeInForce;
        // if (typeof params.newOrderRespType !== 'undefined') opt.newOrderRespType = params.newOrderRespType;
        if (!params.newClientOrderId && !params.listClientOrderId) {
            const id = this.SPOT_PREFIX + this.uuid22();
            if (!isOCO) {
                request.newClientOrderId = id;
            } else {
                request.listClientOrderId = id;
            }
        }

        const allowedTypesForStopAndTrailing = ['STOP_LOSS', 'STOP_LOSS_LIMIT', 'TAKE_PROFIT', 'TAKE_PROFIT_LIMIT', 'OCO'];
        if (params.trailingDelta) {
            request.trailingDelta = params.trailingDelta;

            if (!isOCO && !allowedTypesForStopAndTrailing.includes(request.type)) {
                throw Error('trailingDelta: Must set "type" to one of the following: STOP_LOSS, STOP_LOSS_LIMIT, TAKE_PROFIT, TAKE_PROFIT_LIMIT, OCO');
            }
        }

        /*
         * STOP_LOSS
         * STOP_LOSS_LIMIT
         * TAKE_PROFIT
         * TAKE_PROFIT_LIMIT
         * LIMIT_MAKER
         */
        // if (typeof params.icebergQty !== 'undefined') request.icebergQty = params.icebergQty;
        if (params.stopPrice) {
            request.stopPrice = params.stopPrice;
            if (!isOCO && !allowedTypesForStopAndTrailing.includes(request.type)) {
                throw Error('stopPrice: Must set "type" to one of the following: STOP_LOSS, STOP_LOSS_LIMIT, TAKE_PROFIT, TAKE_PROFIT_LIMIT, OCO');
            }
        }
        const response = await this.privateSpotRequest(endpoint, this.extend(request, params), 'POST');
        // to do error handling
        // if ( !response ) {
        //     if ( callback ) callback( error, response );
        //     else this.options.log( 'Order() error:', error );
        //     return;
        // }
        // if ( typeof response.msg !== 'undefined' && response.msg === 'Filter failure: MIN_NOTIONAL' ) {
        //     this.options.log( 'Order quantity too small. See exchangeInfo() for minimum amounts' );
        // }
        // if ( callback ) callback( error, response );
        // else this.options.log( side + '(' + symbol + ',' + quantity + ',' + price + ') ', response );
        return response;
    }

    /**
     * Create an OCO spot order
     * @see https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#new-order-list---oco-trade
     * @param {OrderSide} side - BUY or SELL
     * @param {string} symbol - The symbol to buy or sell
     * @param {string} quantity - The quantity to buy or sell
     * @param {string} price - The price per unit to transact each unit at
     * @param {object} params - additional order settings
     * @param {string} params.aboveType - The type of the above order
     * @param {string} params.belowType - The type of the below order
     * @param {string} params.abovePrice - The price of the above order
     * @param {string} params.aboveStopPrice - The stop price of the above order
     * @param {string} params.aboveTrailingDelta - The trailing delta of the above order
     * @param {string} params.aboveTimeInForce - The time in force of the above order
     * @param {string} params.belowPrice - The price of the below order
     * @param {string} params.belowStopPrice - The stop price of the below order
     * @param {string} params.belowTrailingDelta - The trailing delta of the below order
     * @param {string} params.belowTimeInForce - The time in force of the below order
     * @return {undefined}
     */
    async ocoOrder(side: OrderSide, symbol: string, quantity: number, params: Dict = {}): Promise<OCOOrder> {
        const request = {
            symbol: symbol,
            side: side,
            quantity: quantity,
        } as Dict;

        if (!params.listClientOrderId) {
            const id = this.SPOT_PREFIX + this.uuid22();
            request.listClientOrderId = id;
        }

        const endpoint = 'v3/orderList/oco';
        const response = await this.privateSpotRequest(endpoint, this.extend(request, params), 'POST');
        return response;
    }

    /**
    * Creates a buy order
    * @param {string} symbol - the symbol to buy
    * @param {numeric} quantity - the quantity required
    * @param {numeric} price - the price to pay for each unit
    * @param {object} flags - additional buy order flags
    * @return {promise or undefined} - omitting the callback returns a promise
    */
    async buy(symbol: string, quantity: number, price: number, flags = {}) {
        return await this.order('LIMIT', 'BUY', symbol, quantity, price, flags);
    }

    /**
* Creates a sell order
* @param {string} symbol - the symbol to sell
* @param {numeric} quantity - the quantity required
* @param {numeric} price - the price to pay for each unit
* @param {object} flags - additional buy order flags
* @param {function} callback - the callback function
* @return {promise or undefined} - omitting the callback returns a promise
*/
    async sell(symbol: string, quantity: number, price: number, flags = {}) {
        return await this.order('LIMIT', 'SELL', symbol, quantity, price, flags);
    }

    /**
* Creates a market buy order
* @see https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#new-order-trade
* @see https://developers.binance.com/docs/binance-spot-api-docs/rest-api/public-api-endpoints#test-new-order-trade
* @see https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#new-order-list---oco-trade
* @param {string} symbol - the symbol to buy
* @param {numeric} quantity - the quantity required
* @param {object} params - additional buy order flags
* @return {promise or undefined} - omitting the callback returns a promise
*/
    async marketBuy(symbol: string, quantity: number, params: Dict = {}) {
        return await this.order('MARKET', 'BUY', symbol, quantity, 0, params);
    }

    /**
* Creates a spot limit order
* @see https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#new-order-trade
* @see https://developers.binance.com/docs/binance-spot-api-docs/rest-api/public-api-endpoints#test-new-order-trade
* @see https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#new-order-list---oco-trade
* @param {string} side - the side of the order (BUY or SELL)
* @param {string} symbol - the symbol to buy
* @param {numeric} quantity - the quantity required
* @param {numeric} price - the price to pay for each unit
* @param {object} params - additional buy order flags
* @return {promise or undefined} - omitting the callback returns a promise
*/
    async limitOrder(side: OrderSide, symbol: string, quantity: number, price: number, params: Dict = {}) {
        return await this.order('LIMIT', side, symbol, quantity, price, params);
    }

    /**
* Creates a market buy order using the cost instead of the quantity (eg: 100usd instead of 0.01btc)
* @see https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#new-order-trade
* @see https://developers.binance.com/docs/binance-spot-api-docs/rest-api/public-api-endpoints#test-new-order-trade
* @see https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#new-order-list---oco-trade
* @param {string} symbol - the symbol to buy
* @param {numeric} quantity - the quantity required
* @param {object} params - additional buy order flags
* @return {promise or undefined} - omitting the callback returns a promise
*/
    async marketBuyWithCost(symbol: string, cost: number, params: Dict = {}) {
        params.quoteOrderQty = cost;
        return await this.order('MARKET', 'BUY', symbol, 0, 0, params);
    }

    /**
    * Creates a market sell order
    * @see https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#new-order-trade
    * @see https://developers.binance.com/docs/binance-spot-api-docs/rest-api/public-api-endpoints#test-new-order-trade
    * @see https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#new-order-list---oco-trade
    * @param {string} symbol - the symbol to sell
    * @param {numeric} quantity - the quantity required
    * @param {object} flags - additional buy order flags
    * @return {promise or undefined} - omitting the callback returns a promise
    */
    async marketSell(symbol: string, quantity: number, params: Dict = {}) {
        return await this.order('MARKET', 'SELL', symbol, quantity, 0, params);
    }

    /**
    * Creates a market sell order using the cost instead of the quantity (eg: 100usd instead of 0.01btc)
    * @see https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#new-order-trade
    * @see https://developers.binance.com/docs/binance-spot-api-docs/rest-api/public-api-endpoints#test-new-order-trade
    * @see https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#new-order-list---oco-trade
    * @param {string} symbol - the symbol to sell
    * @param {numeric} quantity - the quantity required
    * @param {object} flags - additional buy order flags
    * @return {promise or undefined} - omitting the callback returns a promise
    */
    async marketSellWithCost(symbol: string, cost: number, params: Dict = {}) {
        params.quoteOrderQty = cost;
        return await this.order('MARKET', 'SELL', symbol, 0, 0, params);
    }

    /**
    * Cancels an order
    * @see https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#cancel-order-trade
    * @param {string} symbol - the symbol to cancel
    * @param {string} orderid - the orderid to cancel
    * @return {promise or undefined} - omitting the callback returns a promise
    */
    async cancel(symbol: string, orderid: number | string, params: Dict = {}): Promise<CancelOrder> {
        return await this.privateSpotRequest('v3/order', this.extend({ symbol: symbol, orderId: orderid }, params), 'DELETE');
    }

    /**
    * Cancels all orders
    * @see https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#cancel-all-open-orders-on-a-symbol-trade
    * @param {string} symbol - the symbol to cancel
    * @return {promise or undefined} - omitting the callback returns a promise
    */
    async cancelAll(symbol: string, params: Dict = {}): Promise<CancelOrder> {
        return await this.privateSpotRequest('v3/openOrders', this.extend({ symbol: symbol }, params), 'DELETE');
    }

    /**
* Gets the status of an order
* @see https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#query-order-user_data
* @param {string} symbol - the symbol to check
* @param {string} orderid - the orderid to check if !orderid then  use flags to search
* @param {object} flags - any additional flags
* @return {promise or undefined} - omitting the callback returns a promise
*/
    async orderStatus(symbol: string, orderid?: number | string, flags = {}) {
        let parameters = Object.assign({ symbol: symbol }, flags);
        if (orderid) {
            parameters = Object.assign({ orderId: orderid }, parameters);
        }
        return await this.privateSpotRequest('v3/order', parameters);
    }

    /**
* Gets open orders
* @see https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#current-open-orders-user_data
* @param {string} symbol - the symbol to get
* @return {promise or undefined} - omitting the callback returns a promise
*/
    async openOrders(symbol?: string, params: Dict = {}): Promise<QueryOrder[]> {
        const parameters = symbol ? { symbol: symbol } : {};
        return await this.privateSpotRequest('v3/openOrders', this.extend(parameters, params));
    }

    /**
    * Cancels all orders of a given symbol
    * @see https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#cancel-all-open-orders-on-a-symbol-trade
    * @param {string} symbol - the symbol to cancel all orders for
    * @return {promise or undefined} - omitting the callback returns a promise
    */
    async cancelAllOrders(symbol: string, params: Dict = {}) {
        return await this.privateSpotRequest('v3/openOrders', this.extend({ symbol }, params), 'DELETE');
    }

    // /**
    // * Cancels all orders of a given symbol
    // * @param {string} symbol - the symbol to cancel all orders for
    // * @return {promise or undefined} - omitting the callback returns a promise
    // */
    // async cancelOrders(symbol: string, params: Dict = {}) {
    //     const json = await this.privateSpotRequest('v3/openOrders', this.extend({ symbol: symbol }, params), 'DELETE');
    //     // if (json.length === 0) {
    //     //     return callback.call(this, 'No orders present for this symbol', {}, symbol);
    //     // }
    //     // if (Object.keys(json).length === 0) {
    //     //     return callback.call(this, 'No orders present for this symbol', {}, symbol);
    //     // }
    //     // for (let obj of json) {
    //     //     let quantity = obj.origQty - obj.executedQty;
    //     //     this.options.log('cancel order: ' + obj.side + ' ' + symbol + ' ' + quantity + ' @ ' + obj.price + ' #' + obj.orderId);
    //     //     signedRequest(this.getSpotUrl() + 'v3/order', { symbol: symbol, orderId: obj.orderId }, function (error, data) {
    //     //         return callback.call(this, error, data, symbol);
    //     //     }, 'DELETE');
    //     // }
    //     return json; // to do: check this logic of cancelling remaining orders manually

    // }

    /**
    * Gets all order of a given symbol
    * @see https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#all-orders-user_data
    * @param {string} symbol - the symbol
    * @param {object} options - additional options
    * @return {promise or undefined} - omitting the callback returns a promise
    */
    async allOrders(symbol: string, params: Dict = {}): Promise<QueryOrder[]> {
        const parameters = this.extend({ symbol }, params);
        return await this.privateSpotRequest('v3/allOrders', parameters);
    }

    /**
     * Create a signed margin order
     * @see https://developers.binance.com/docs/margin_trading/trade/Margin-Account-New-Order
     * @param {string} side - BUY or SELL
     * @param {string} symbol - The symbol to buy or sell
     * @param {string} quantity - The quantity to buy or sell
     * @param {string} price - The price per unit to transact each unit at
     * @param {object} params - additional order settings
     * @return {undefined}
     */
    async marginOrder(type: OrderType, side: string, symbol: string, quantity: number, price?: number, params: Dict = {}) {
        let endpoint = 'v1/margin/order';
        if (this.Options.test || params.test) endpoint += '/test';
        const request = {
            symbol: symbol,
            side: side,
            type: type,
            quantity: quantity
        } as Dict;
        if (typeof params.type !== 'undefined') request.type = params.type;
        if ('isIsolated' in params) request.isIsolated = params.isIsolated;
        if (request.type.includes('LIMIT')) {
            request.price = price;
            if (request.type !== 'LIMIT_MAKER') {
                request.timeInForce = 'GTC';
            }
        }

        if (typeof params.timeInForce !== 'undefined') request.timeInForce = params.timeInForce;
        if (typeof params.newOrderRespType !== 'undefined') request.newOrderRespType = params.newOrderRespType;
        // if ( typeof flags.newClientOrderId !== 'undefined' ) opt.newClientOrderId = flags.newClientOrderId;
        if (typeof params.newClientOrderId !== 'undefined') {
            request.newClientOrderId = params.newClientOrderId;
        } else {
            request.newClientOrderId = this.SPOT_PREFIX + this.uuid22();
        }
        if (typeof params.sideEffectType !== 'undefined') request.sideEffectType = params.sideEffectType;

        /*
         * STOP_LOSS
         * STOP_LOSS_LIMIT
         * TAKE_PROFIT
         * TAKE_PROFIT_LIMIT
         */
        if (typeof params.icebergQty !== 'undefined') request.icebergQty = params.icebergQty;
        if (typeof params.stopPrice !== 'undefined') {
            request.stopPrice = params.stopPrice;
            if (request.type === 'LIMIT') throw Error('stopPrice: Must set "type" to one of the following: STOP_LOSS, STOP_LOSS_LIMIT, TAKE_PROFIT, TAKE_PROFIT_LIMIT');
        }
        return await this.privateSapiRequest(endpoint, this.extend(request, params), 'POST');
    }

    // Futures internal functions
    /**
     * @see https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/New-Order
     * @see https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/New-Algo-Order
     * @param type
     * @param side
     * @param symbol symbol if the market
     * @param quantity
     * @param price
     * @param params extra parameters to be sent in the request
     * @returns
     */
    async futuresOrder(type: OrderType, side: string, symbol: string, quantity: number, price?: number, params: Dict = {}): Promise<FuturesOrder> {
        params.symbol = symbol;
        params.side = side;
        params.type = type;
        if (quantity) params.quantity = quantity;
        // if in the binance futures setting Hedged mode is active, positionSide parameter is mandatory
        if (!params.positionSide && this.Options.hedgeMode) {
            params.positionSide = side === 'BUY' ? 'LONG' : 'SHORT';
        }
        // LIMIT STOP MARKET STOP_MARKET TAKE_PROFIT TAKE_PROFIT_MARKET
        // reduceOnly stopPrice
        if (price) {
            params.price = price;
        }
        if (!params.timeInForce && (params.type.includes('LIMIT') || params.type === 'STOP' || params.type === 'TAKE_PROFIT')) {
            params.timeInForce = 'GTX'; // Post only by default. Use GTC for limit orders.
        }

        if (!params.newClientOrderId) {
            params.newClientOrderId = this.CONTRACT_PREFIX + this.uuid22();
        }
        // check if it is algoOrder
        const conditionalTypes = [
            'STOP',
            'STOP_MARKET',
            'TAKE_PROFIT',
            'TAKE_PROFIT_MARKET',
            'TRAILING_STOP_MARKET',
        ];
        const typeUpperCase = type.toUpperCase();
        if (typeUpperCase && conditionalTypes.includes(typeUpperCase)) {
            const algoPayload = { ...params };
            if (!algoPayload.clientAlgoId) {
                algoPayload.clientAlgoId = this.CONTRACT_PREFIX + this.uuid22();
            }
            delete algoPayload.newClientOrderId;
            algoPayload.algoType = 'CONDITIONAL';
            if (algoPayload.stopPrice && !algoPayload.triggerPrice) {
                algoPayload.triggerPrice = algoPayload.stopPrice;
                delete algoPayload.stopPrice;
            }
            return await this.privateFuturesRequest('v1/algoOrder', algoPayload, 'POST');
        }
        return await this.privateFuturesRequest('v1/order', params, 'POST');
    }

    // Futures internal functions
    /**
     * @see https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/New-Algo-Order
     * @param type
     * @param side
     * @param symbol symbol if the market
     * @param quantity
     * @param price
     * @param params extra parameters to be sent in the request
     * @returns
     */
    async futuresAlgoOrder(type: OrderType, side: string, symbol: string, quantity: number, price?: number, params: Dict = {}): Promise<FuturesAlgoOrder> {
        params.symbol = symbol;
        params.side = side;
        params.type = type;
        if (quantity) params.quantity = quantity;
        // if in the binance futures setting Hedged mode is active, positionSide parameter is mandatory
        if (!params.positionSide && this.Options.hedgeMode) {
            params.positionSide = side === 'BUY' ? 'LONG' : 'SHORT';
        }
        // LIMIT STOP MARKET STOP_MARKET TAKE_PROFIT TAKE_PROFIT_MARKET
        // reduceOnly stopPrice
        if (price) {
            params.price = price;
        }
        if (!params.timeInForce && (params.type.includes('LIMIT') || params.type === 'STOP' || params.type === 'TAKE_PROFIT')) {
            params.timeInForce = 'GTX'; // Post only by default. Use GTC for limit orders.
        }

        if (!params.clientAlgoId) {
            params.clientAlgoId = this.CONTRACT_PREFIX + this.uuid22();
        }

        if (!params.algoType) {
            params.algoType = 'CONDITIONAL';
        }
        return await this.privateFuturesRequest('v1/algoOrder', params, 'POST');
    }

    /**
     * @see https://developers.binance.com/docs/derivatives/option/trade/New-Order
     * @param type type of order to create
     * @param side side of the order (BUY or SELL)
     * @param symbol symbol of the market to trade
     * @param quantity quantity of the order to create
     * @param price price of the order to create
     * @param params extra parameters to be sent in the request
     * @returns
     */
    async deliveryOrder(type: OrderType, side: string, symbol: string, quantity: number, price?: number, params: Dict = {}): Promise<FuturesOrder> {
        params.symbol = symbol;
        params.side = side;
        params.quantity = quantity;
        params.type = type;
        // if in the binance futures setting Hedged mode is active, positionSide parameter is mandatory
        if (this.Options.hedgeMode) {
            params.positionSide = side === 'BUY' ? 'LONG' : 'SHORT';
        }
        // LIMIT STOP MARKET STOP_MARKET TAKE_PROFIT TAKE_PROFIT_MARKET
        // reduceOnly stopPrice
        if (price) {
            params.price = price;
            if (!params.type) params.type = 'LIMIT';
        } else {
            if (!params.type) params.type = 'MARKET';
        }
        if (!params.timeInForce && (params.type.includes('LIMIT') || params.type === 'STOP' || params.type === 'TAKE_PROFIT')) {
            params.timeInForce = 'GTX'; // Post only by default. Use GTC for limit orders.
        }

        if (!params.newClientOrderId) {
            params.newClientOrderId = this.CONTRACT_PREFIX + this.uuid22();
        }
        return await this.privateDeliveryRequest('v1/order', params, 'POST');
    }

    // ------ WS RELATED FUNCTIONS ------ //

    noop() {
        return;
    }

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

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

    /**
     * Called when socket is closed, subscriptions are de-registered for later reference
     * @param {Function} reconnect - reconnect callback
     * @param {string} code - code associated with the socket
     * @param {string} reason - string with the response
     * @return {undefined}
     */
    handleSocketClose(wsBind, reconnect: Function, code, reason: string) {
        delete this.subscriptions[wsBind.url];
        if (this.subscriptions && Object.keys(this.subscriptions).length === 0) {
            clearInterval(this.socketHeartbeatInterval);
        }
        this.Options.log('WebSocket closed: ' + wsBind.url +
            (code ? ' (' + code + ')' : '') +
            (reason ? ' ' + reason : ''));
        if (this.Options.reconnect && wsBind.reconnect && reconnect) {
            if (wsBind.url && wsBind.url.length === 60) this.Options.log('Account data WebSocket reconnecting...');
            else this.Options.log('WebSocket reconnecting: ' + wsBind.url + '...');
            try {
                reconnect();
            } catch (error) {
                this.Options.log('WebSocket reconnect error: ' + error.message);
            }
        }
    }

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

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

    // ----- WS ENDPOINTS ----- //

    /**
    * Get Binance server time
    * @return {promise or undefined} - omitting the callback returns a promise
    */
    async time() {
        const res = await this.publicSpotRequest('v3/time', {});
        return res;
    }

    /**
     * Used to subscribe to a single websocket endpoint
     * @param {string} endpoint - endpoint to connect to
     * @param {function} callback - the function to call when information is received
     * @param {boolean} reconnect - whether to reconnect on disconnect
     * @param {object} opened_callback - the function to call when opened
     * @return {WebSocket} - websocket reference
     */
    subscribe(endpoint: string, callback: Callback, reconnect?: Callback, opened_callback?: Callback) {
        const httpsproxy = this.getHttpsProxy();
        let socksproxy = this.getSocksProxy();
        let ws: WebSocket = undefined;

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

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

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

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

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

    /**
     * Connect to WebSocket API for bidirectional JSON-RPC communication
     * @param {string} connectionId - unique identifier for this connection
     * @param {function} messageHandler - callback for handling incoming messages/events
     * @param {function} reconnect - reconnect callback
     * @return {WebSocket} - WebSocket connection
     */
    connectWsApi(connectionId: string, messageHandler: Callback, reconnect?: Callback) {
        const httpsproxy = this.getHttpsProxy();
        let socksproxy = this.getSocksProxy();
        let ws: WebSocket = undefined;

        if (socksproxy) {
            socksproxy = this.proxyReplacewithIp(socksproxy);
            if (this.Options.verbose) this.Options.log('WebSocket API: using socks proxy server ' + socksproxy);
            const agent = new SocksProxyAgent({
                protocol: this.parseProxy(socksproxy)[0],
                host: this.parseProxy(socksproxy)[1],
                port: this.parseProxy(socksproxy)[2]
            });
            ws = new WebSocket(this.getWsApiUrl(), { agent: agent });
        } else if (httpsproxy) {
            const config = url.parse(httpsproxy);
            const agent = new HttpsProxyAgent(config);
            if (this.Options.verbose) this.Options.log('WebSocket API: using proxy server ' + agent);
            ws = new WebSocket(this.getWsApiUrl(), { agent: agent });
        } else {
            ws = new WebSocket(this.getWsApiUrl());
        }

        (ws as any).reconnect = this.Options.reconnect;
        (ws as any).connectionId = connectionId;
        (ws as any).isAlive = false;

        ws.on('open', () => {
            if (this.Options.verbose) this.Options.log('WebSocket API: Connected to ' + this.getWsApiUrl());
            (ws as any).isAlive = true;
        });
        ws.on('pong', () => { (ws as any).isAlive = true; });
        ws.on('error', (err) => {
            this.Options.log('WebSocket API error: ' + (ws as any).connectionId + ' error: ' + err.message);
        });
        ws.on('close', (code, reason) => {
            if (this.Options.verbose) this.Options.log('WebSocket API closed: ' + (ws as any).connectionId +
                (code ? ' (' + code + ')' : '') +
                (reason ? ' ' + reason : ''));
            delete this.wsApiConnections[connectionId];
            if ((ws as any).reconnect && typeof reconnect === 'function') {
                if (this.Options.verbose) this.Options.log('WebSocket API reconnecting: ' + connectionId);
                reconnect();
            }
        });
        ws.on('message', data => {
            try {
                if (this.Options.verbose) this.Options.log('WebSocket API data:', data);
                const message = JSONbig.parse(data as any);

                // Handle JSON-RPC responses
                if (message.id && this.wsApiPendingRequests[message.id]) {
                    const pending = this.wsApiPendingRequests[message.id];

                    if (message.status === 200) {
                        pending.resolve(message.result);
                    } else {
                        pending.reject(new Error(`WebSocket API error: ${message.error?.msg || 'Unknown error'}`));
                    }
                }
                // Handle events (messages without 'id' or with 'subscriptionId')
                else if (message.subscriptionId !== undefined || message.event) {
                    messageHandler(message);
                }
            } catch (error) {
                this.Options.log('WebSocket API: Parse error: ' + error.message);
            }
        });

        this.wsApiConnections[connectionId] = ws;
        return ws;
    }

    /**
     * Send a JSON-RPC request on the WebSocket API connection
     * @param {string} connectionId - connection identifier
     * @param {string} method - JSON-RPC method name
     * @param {object} params - method parameters
     * @return {Promise} - resolves with the result or rejects with error
     */
    sendWsApiRequest(connectionId: string, method: string, params: any = {}): Promise<any> {
        return new Promise((resolve, reject) => {
            const ws = this.wsApiConnections[connectionId];
            if (!ws || ws.readyState !== WebSocket.OPEN) {
                reject(new Error('WebSocket API connection not open'));
                return;
            }

            const requestId = this.generateRequestId();
            const request = {
                id: requestId,
                method: method,
                params: params
            };

            const settle = (fn: Function, value: any) => {
                const pending = this.wsApiPendingRequests[requestId];
                if (!pending) return; // already settled
                clearTimeout(pending.timer);
                delete this.wsApiPendingRequests[requestId];
                fn(value);
            };

            const timer = setTimeout(() => {
                settle(reject, new Error('WebSocket API request timeout'));
            }, 30000);

            this.wsApiPendingRequests[requestId] = { resolve: (v: any) => settle(resolve, v), reject: (e: any) => settle(reject, e), timer, connectionId };

            if (this.Options.verbose) {
                this.Options.log('WebSocket API: Sending request:', JSON.stringify(request));
            }

            ws.send(JSON.stringify(request), (error) => {
                if (error) {
                    settle(reject, error);
                }
            });
        });
    }

    /**
     * Generate a unique request ID for JSON-RPC requests
     * @return {string} - unique request ID
     */
    generateRequestId(): string {
        return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
    }

    /**
     * Terminate a WebSocket API connection
     * @param {string} connectionId - connection identifier
     * @param {boolean} reconnect - whether to reconnect
     * @return {undefined}
     */
    terminateWsApi(connectionId: string, reconnect = false) {
        if (this.Options.verbose) this.Options.log('WebSocket API terminating:', connectionId);
        const ws = this.wsApiConnections[connectionId];
        if (!ws) return;
        ws.removeAllListeners('message');
        ws.reconnect = reconnect;
        ws.terminate();
        delete this.wsApiConnections[connectionId];

        // Reject all pending requests for this connection
        const pendingIds = Object.keys(this.wsApiPendingRequests);
        for (const requestId of pendingIds) {
            const pending = this.wsApiPendingRequests[requestId];
            if (pending && pending.connectionId === connectionId) {
                pending.reject(new Error('WebSocket API connection terminated'));
            }
        }
    }

    /**
     * Futures heartbeat code with a shared single interval tick
     * @return {undefined}
     */
    futuresSocketHeartbeat() {
        /* Sockets removed from subscriptions during a manual terminate()
         will no longer be at risk of having functions called on them */
        for (const endpointId in this.futuresSubscriptions) {
            const ws = this.futuresSubscriptions[endpointId];
            if (ws.isAlive) {
                ws.isAlive = false;
                if (ws.readyState === WebSocket.OPEN) ws.ping(this.noop);
            } else {
                if (this.Options.verbose) this.Options.log(`Terminating zombie futures WebSocket: ${ws.endpoint}`);
                if (ws.readyState === WebSocket.OPEN) ws.terminate();
            }
        }
    }

    /**
     * Called when a futures socket is opened, subscriptions are registered for later reference
     * @param {function} openCallback - a callback function
     * @return {undefined}
     */
    handleFuturesSocketOpen(wsBind: any, openCallback: Callback) {
        wsBind.isAlive = true;
        if (Object.keys(this.futuresSubscriptions).length === 0) {
            this.socketHeartbeatInterval = setInterval(this.futuresSocketHeartbeat.bind(this), this.heartBeatInterval);
        }
        this.futuresSubscriptions[wsBind.url] = wsBind;
        if (typeof openCallback === 'function') openCallback(wsBind.url);
    }

    /**
     * Called when futures websocket is closed, subscriptions are de-registered for later reference
     * @param {boolean} reconnect - true or false to reconnect the socket
     * @param {string} code - code associated with the socket
     * @param {string} reason - string with the response
     * @return {undefined}
     */
    handleFuturesSocketClose(wsBind, reconnect, code, reason) {
        delete this.futuresSubscriptions[wsBind.url];
        if (this.futuresSubscriptions && Object.keys(this.futuresSubscriptions).length === 0) {
            clearInterval(this.socketHeartbeatInterval);
        }
        this.Options.log('Futures WebSocket closed: ' + wsBind.url +
            (code ? ' (' + code + ')' : '') +
            (reason ? ' ' + reason : ''));
        if (this.Options.reconnect && wsBind.reconnect && reconnect) {
            if (wsBind.url && wsBind.url.length === 60) this.Options.log('Futures account data WebSocket reconnecting...');
            else this.Options.log('Futures WebSocket reconnecting: ' + wsBind.url + '...');
            try {
                reconnect();
            } catch (error) {
                this.Options.log('Futures WebSocket reconnect error: ' + error.message);
            }
        }
    }

    /**
     * Called when a futures websocket errors
     * @param {object} error - error object message
     * @return {undefined}
     */
    handleFuturesSocketError(wsBind, error) {
        this.Options.log('Futures WebSocket error: ' + wsBind.url +
            (error.code ? ' (' + error.code + ')' : '') +
            (error.message ? ' ' + error.message : ''));
    }

    /**
     * Called on each futures socket heartbeat
     * @return {undefined}
     */
    handleFuturesSocketHeartbeat(wsBind) {
        wsBind.isAlive = true;
    }

    /**
     * Used to subscribe to a single futures websocket endpoint
     * @param {string} endpoint - endpoint to connect to
     * @param {function} callback - the function to call when information is received
     * @param {object} params - Optional reconnect {boolean} (whether to reconnect on disconnect), openCallback {function}, id {string}
     * @return {WebSocket} - websocket reference
     */
    futuresSubscribeSingle(endpoint: string, callback: Callback, params: Dict = {}) {
        if (typeof params === 'boolean') params = { reconnect: params };
        if (!params.reconnect) params.reconnect = false;
        if (!params.openCallback) params.openCallback = false;
        if (!params.id) params.id = false;
        const httpsproxy = this.getHttpsProxy();
        let socksproxy = this.getSocksProxy();
        let ws: any = undefined;
        const category = this.classifyFuturesStream(endpoint);
        const baseUrl = this.getFStreamSingleUrl(category);
        // Private streams use query params: ?listenKey=<key> instead of path: /<key>
        const wsUrl = category === 'private'
            ? baseUrl.replace(/\/$/, '') + '?listenKey=' + endpoint
            : baseUrl + endpoint;

        if (socksproxy) {
            socksproxy = this.proxyReplacewithIp(socksproxy);
            if (this.Options.verbose) this.Options.log(`futuresSubscribeSingle: using socks proxy server: ${socksproxy}`);
            const agent = new SocksProxyAgent({
                protocol: this.parseProxy(socksproxy)[0],
                host: this.parseProxy(socksproxy)[1],
                port: this.parseProxy(socksproxy)[2]
            });
            ws = new WebSocket(wsUrl, { agent });
        } else if (httpsproxy) {
            const config = url.parse(httpsproxy);
            const agent = new HttpsProxyAgent(config);
            if (this.Options.verbose) this.Options.log(`futuresSubscribeSingle: using proxy server: ${agent}`);
            ws = new WebSocket(wsUrl, { agent });
        } else {
            ws = new WebSocket(wsUrl);
        }

        if (this.Options.verbose) this.Options.log('futuresSubscribeSingle: Subscribed to ' + endpoint);
        callback = callback.bind(this);
        ws.reconnect = this.Options.reconnect;
        ws.endpoint = endpoint;
        ws.isAlive = false;
        ws.on('open', this.handleFuturesSocketOpen.bind(this, ws, params.openCallback));
        ws.on('pong', this.handleFuturesSocketHeartbeat.bind(this, ws));
        ws.on('error', this.handleFuturesSocketError.bind(this, ws));
        ws.on('close', this.handleFuturesSocketClose.bind(this, ws, params.reconnect));
        ws.on('message', data => {
            try {
                if (this.Options.verbose) this.Options.log('futuresSubscribeSingle: Received data:', data);
                callback(JSONbig.parse(data));
            } catch (error) {
                this.Options.log('Parse error: ' + error.message);
            }
        });
        return ws;
    }

    /**
     * Used to subscribe to a combined futures websocket endpoint
     * @param {string} streams - streams to connect to
     * @param {function} callback - the function to call when information is received
     * @param {object} params - Optional reconnect {boolean} (whether to reconnect on disconnect), openCallback {function}, id {string}
     * @return {WebSocket} - websocket reference
     */
    futuresSubscribe(streams, callback: Callback, params: Dict = {}) {
        if (typeof streams === 'string') return this.futuresSubscribeSingle(streams, callback, params);
        if (typeof params === 'boolean') params = { reconnect: params };
        if (!params.reconnect) params.reconnect = false;
        if (!params.openCallback) params.openCallback = false;
        if (!params.id) params.id = false;
        const httpsproxy = this.getHttpsProxy();
        let socksproxy = this.getSocksProxy();
        const queryParams = streams.join('/');
        // Binance routes USDⓈ-M futures streams to separate endpoints by category
        // (/public, /market, /private) and will not push cross-category streams on a
        // single connection. Reject mixed-category combos so they fail loudly instead
        // of silently dropping data — subscribe to each category on its own connection.
        const category = this.classifyFuturesStream(streams[0]);
        const mismatch = streams.find(s => this.classifyFuturesStream(s) !== category);
        if (mismatch !== undefined) {
            const mismatchCategory = this.classifyFuturesStream(mismatch);
            throw new Error(`futuresSubscribe: cannot combine '${category}' stream "${streams[0]}" with '${mismatchCategory}' stream "${mismatch}" on one connection. Binance routes futures streams to separate /public, /market and /private endpoints; subscribe to each category separately.`);
        }
        const baseUrl = this.getFStreamUrl(category);
        let ws: any = undefined;
        if (socksproxy) {
            socksproxy = this.proxyReplacewithIp(socksproxy);
            if (this.Options.verbose) this.Options.log(`futuresSubscribe: using socks proxy server ${socksproxy}`);
            const agent = new SocksProxyAgent({
                protocol: this.parseProxy(socksproxy)[0],
                host: this.parseProxy(socksproxy)[1],
                port: this.parseProxy(socksproxy)[2]
            });
            ws = new WebSocket(baseUrl + queryParams, { agent });
        } else if (httpsproxy) {
            if (this.Options.verbose) this.Options.log(`futuresSubscribe: using proxy server ${httpsproxy}`);
            const config = url.parse(httpsproxy);
            const agent = new HttpsProxyAgent(config);
            ws = new WebSocket(baseUrl + queryParams, { agent });
        } else {
            ws = new WebSocket(baseUrl + queryParams);
        }

        ws.reconnect = this.Options.reconnect;
        ws.endpoint = stringHash(queryParams);
        ws.isAlive = false;
        if (this.Options.verbose) {
            this.Options.log(`futuresSubscribe: Subscribed to [${ws.endpoint}] ${queryParams}`);
        }
        ws.on('open', this.handleFuturesSocketOpen.bind(this, ws, params.openCallback));
        ws.on('pong', this.handleFuturesSocketHeartbeat.bind(this, ws));
        ws.on('error', this.handleFuturesSocketError.bind(this, ws));
        ws.on('close', this.handleFuturesSocketClose.bind(this, ws, params.reconnect));
        ws.on('message', data => {
            try {
                if (this.Options.verbose) this.Options.log('futuresSubscribe: Received data:', data);
                callback(JSONbig.parse(data).data);
            } catch (error) {
                this.Options.log(`futuresSubscribe: Parse error: ${error.message}`);
            }
        });
        return ws;
    }

    /**
     * Used to terminate a futures websocket
     * @param {string} endpoint - endpoint identifier associated with the web socket
     * @param {boolean} reconnect - auto reconnect after termination
     * @return {undefined}
     */
    futuresTerminate(endpoint: string, reconnect = false) {
        if (this.Options.verbose) this.Options.log('Futures WebSocket terminating:', endpoint);
        const ws = this.futuresSubscriptions[endpoint];
        if (!ws) return;
        ws.removeAllListeners('message');
        ws.reconnect = reconnect;
        ws.terminate();
    }

    /**
     * Combines all futures OHLC data with the latest update
     * @param {string} symbol - the symbol
     * @param {string} interval - time interval
     * @return {array} - interval data for given symbol
     */
    futuresKlineConcat(symbol: string, interval: Interval) {
        const output = this.futuresTicks[symbol][interval];
        if (!this.futuresRealtime[symbol][interval].time) return output;
        const time = this.futuresRealtime[symbol][interval].time;
        const last_updated = Object.keys(this.futuresTicks[symbol][interval]).pop();
        if (time >= last_updated) {
            output[time] = this.futuresRealtime[symbol][interval];
            //delete output[time].time;
            output[last_updated].isFinal = true;
            output[time].isFinal = false;
        }
        return output;
    }

    /**
     * Used for websocket futures @kline
     * @param {string} symbol - the symbol
     * @param {object} kline - object with kline info
     * @param {string} firstTime - time filter
     * @return {undefined}
     */
    futuresKlineHandler(symbol: string, kline: any, firstTime = 0) {
        // eslint-disable-next-line no-unused-vars
        const { e: eventType, E: eventTime, k: ticks } = kline;
        // eslint-disable-next-line no-unused-vars
        const { o: open, h: high, l: low, c: close, v: volume, i: interval, x: isFinal, q: quoteVolume, V: takerBuyBaseVolume, Q: takerBuyQuoteVolume, n: trades, t: time, T: closeTime } = ticks;
        if (time <= firstTime) return;
        if (!isFinal) {
            // if ( typeof this.futuresRealtime[symbol][interval].time !== 'undefined' ) {
            //     if ( this.futuresRealtime[symbol][interval].time > time ) return;
            // }
            this.futuresRealtime[symbol][interval] = { time, closeTime, open, high, low, close, volume, quoteVolume, takerBuyBaseVolume, takerBuyQuoteVolume, trades, isFinal };
            return;
        }
        const first_updated = Object.keys(this.futuresTicks[symbol][interval]).shift();
        if (first_updated) delete this.futuresTicks[symbol][interval][first_updated];
        this.futuresTicks[symbol][interval][time] = { time, closeTime, open, high, low, close, volume, quoteVolume, takerBuyBaseVolume, takerBuyQuoteVolume, trades, isFinal: false };
    }

    /**
     * Converts the futures liquidation stream data into a friendly object
     * @param {object} data - liquidation data callback data type
     * @return {object} - user friendly data type
     */
    fLiquidationConvertData(data: any) {
        const eventType = data.e, eventTime = data.E;
        const {
            s: symbol,
            S: side,
            o: orderType,
            f: timeInForce,
            q: origAmount,
            p: price,
            ap: avgPrice,
            X: orderStatus,
            l: lastFilledQty,
            z: totalFilledQty,
            T: tradeTime
        } = data.o;
        return { symbol, side, orderType, timeInForce, origAmount, price, avgPrice, orderStatus, lastFilledQty, totalFilledQty, eventType, tradeTime, eventTime };
    }

    /**
     * Converts the futures ticker stream data into a friendly object
     * @param {object} data - user data callback data type
     * @return {object} - user friendly data type
     */
    fTickerConvertData(data: any) {
        const friendlyData = (data: any) => {
            const {
                e: eventType,
                E: eventTime,
                s: symbol,
                p: priceChange,
                P: percentChange,
                w: averagePrice,
                c: close,
                Q: closeQty,
                o: open,
                h: high,
                l: low,
                v: volume,
                q: quoteVolume,
                O: openTime,
                C: closeTime,
                F: firstTradeId,
                L: lastTradeId,
                n: numTrades
            } = data;
            return {
                eventType,
                eventTime,
                symbol,
                priceChange,
                percentChange,
                averagePrice,
                close,
                closeQty,
                open,
                high,
                low,
                volume,
                quoteVolume,
                openTime,
                closeTime,
                firstTradeId,
                lastTradeId,
                numTrades
            };
        };
        if (Array.isArray(data)) {
            const result = [];
            for (const obj of data) {
                result.push(friendlyData(obj));
            }
            return result;
        }
        return friendlyData(data);
    }

    /**
     * Converts the futures miniTicker stream data into a friendly object
     * @param {object} data - user data callback data type
     * @return {object} - user friendly data type
     */
    fMiniTickerConvertData(data: any) {
        const friendlyData = (data: any) => {
            const {
                e: eventType,
                E: eventTime,
                s: symbol,
                c: close,
                o: open,
                h: high,
                l: low,
                v: volume,
                q: quoteVolume
            } = data;
            return {
                eventType,
                eventTime,
                symbol,
                close,
                open,
                high,
                low,
                volume,
                quoteVolume
            };
        };
        if (Array.isArray(data)) {
            const result = [];
            for (const obj of data) {
                result.push(friendlyData(obj));
            }
            return result;
        }
        return friendlyData(data);
    }

    /**
     * Converts the futures bookTicker stream data into a friendly object
     * @param {object} data - user data callback data type
     * @return {object} - user friendly data type
     */
    fBookTickerConvertData(data: any) {
        const {
            u: updateId,
            s: symbol,
            b: bestBid,
            B: bestBidQty,
            a: bestAsk,
            A: bestAskQty
        } = data;
        return {
            updateId,
            symbol,
            bestBid,
            bestBidQty,
            bestAsk,
            bestAskQty
        };
    }

    /**
     * Converts the futures UserData stream MARGIN_CALL data into a friendly object
     * @param {object} data - user data callback data type
     * @return {object} - user friendly data type
     */
    fUserDataMarginConvertData(data: any) {
        const {
            e: eventType,
            E: eventTime,
            cw: crossWalletBalance, // only pushed with crossed position margin call
            // p: positions
        } = data;
        let { positions } = data;
        const positionConverter = position => {
            const {
                s: symbol,
                ps: positionSide,
                pa: positionAmount,
                mt: marginType,
                iw: isolatedWallet, // if isolated position
                mp: markPrice,
                up: unrealizedPnL,
                mm: maintenanceMargin // maintenance margin required
            } = position;
            return {
                symbol,
                positionSide,
                positionAmount,
                marginType,
                isolatedWallet,
                markPrice,
                unrealizedPnL,
                maintenanceMargin
            };
        };
        const convertedPositions = [];
        for (const position of positions) {
            convertedPositions.push(positionConverter(position));
        }
        positions = convertedPositions;
        return {
            eventType,
            eventTime,
            crossWalletBalance,
            positions
        };
    }

    /**
     * Converts the futures UserData stream ACCOUNT_CONFIG_UPDATE into a friendly object
     * @param {object} data - user config callback data type
     * @return {object} - user friendly data type
     */
    fUserConfigDataAccountUpdateConvertData(data: any) {
        return {
            eventType: data.e,
            eventTime: data.E,
            transactionTime: data.T,
            ac: {
                symbol: data.ac.s,
                leverage: data.ac.l
            }
        };
    }

    /**
     * Converts the futures UserData stream ACCOUNT_UPDATE data into a friendly object
     * @param {object} data - user data callback data type
     * @return {object} - user friendly data type
     */
    fUserDataAccountUpdateConvertData(data: any) {
        const {
            e: eventType,
            E: eventTime,
            T: transaction,
        } = data;
        let { a: updateData } = data;
        const updateConverter = updateData => {
            const {
                m: eventReasonType
            } = data;
            let {
                // m: eventReasonType,
                B: balances,
                P: positions
            } = updateData;
            const positionConverter = position => {
                const {
                    s: symbol,
                    pa: positionAmount,
                    ep: entryPrice,
                    cr: accumulatedRealized, // (Pre-fee) Accumulated Realized
                    up: unrealizedPnL,
                    mt: marginType,
                    iw: isolatedWallet, // if isolated position
                    ps: positionSide
                } = position;
                return {
                    symbol,
                    positionAmount,
                    entryPrice,
                    accumulatedRealized,
                    unrealizedPnL,
                    marginType,
                    isolatedWallet,
                    positionSide
                };
            };
            const balanceConverter = (balance: any) => {
                const {
                    a: asset,
                    wb: walletBalance,
                    cw: crossWalletBalance,
                    bc: balanceChange
                } = balance;
                return {
                    asset,
                    walletBalance,
                    crossWalletBalance,
                    balanceChange
                };
            };

            const balanceResult = [];
            const positionResult = [];

            for (const balance of balances) {
                balanceResult.push(balanceConverter(balance));
            }
            for (const position of positions) {
                positionResult.push(positionConverter(position));
            }

            balances = balanceResult;
            positions = positionResult;
            return {
                eventReasonType,
                balances,
                positions
            };
        };
        updateData = updateConverter(updateData);
        return {
            eventType,
            eventTime,
            transaction,
            updateData
        };
    }

    /**
     * Converts the futures UserData stream ORDER_TRADE_UPDATE data into a friendly object
     * @param {object} data - user data callback data type
     * @return {object} - user friendly data type
     */
    fUserDataOrderUpdateConvertData(data: any) {
        const {
            e: eventType,
            E: eventTime,
            T: transaction, // transaction time
        } = data;

        let { o: order } = data;

        const orderConverter = (order: any) => {
            const {
                s: symbol,
                c: clientOrderId,
                // special client order id:
                // starts with "autoclose-": liquidation order
                // "adl_autoclose": ADL auto close order
                S: side,
                o: orderType,
                f: timeInForce,
                q: originalQuantity,
                p: originalPrice,
                ap: averagePrice,
                sp: stopPrice, // please ignore with TRAILING_STOP_MARKET order,
                x: executionType,
                X: orderStatus,
                i: orderId,
                l: orderLastFilledQuantity,
                z: orderFilledAccumulatedQuantity,
                L: lastFilledPrice,
                N: commissionAsset, // will not push if no commission
                n: commission, // will not push if no commission
                T: orderTradeTime,
                t: tradeId,
                b: bidsNotional,
                a: askNotional,
                m: isMakerSide, // is this trade maker side
                R: isReduceOnly, // is this reduce only
                wt: stopPriceWorkingType,
                ot: originalOrderType,
                ps: positionSide,
                cp: closeAll, // if close-all, pushed with conditional order
                AP: activationPrice, // only pushed with TRAILING_STOP_MARKET order
                cr: callbackRate, // only pushed with TRAILING_STOP_MARKET order
                rp: realizedProfit
            } = order;
            return {
                symbol,
                clientOrderId,
                side,
                orderType,
                timeInForce,
                originalQuantity,
                originalPrice,
                averagePrice,
                stopPrice,
                executionType,
                orderStatus,
                orderId,
                orderLastFilledQuantity,
                orderFilledAccumulatedQuantity,
                lastFilledPrice,
                commissionAsset,
                commission,
                orderTradeTime,
                tradeId,
                bidsNotional,
                askNotional,
                isMakerSide,
                isReduceOnly,
                stopPriceWorkingType,
                originalOrderType,
                positionSide,
                closeAll,
                activationPrice,
                callbackRate,
                realizedProfit
            };
        };
        order = orderConverter(order);
        return {
            eventType,
            eventTime,
            transaction,
            order
        };
    }

    /**
     * Converts the futures markPrice stream data into a friendly object
     * @param {object} data - user data callback data type
     * @return {object} - user friendly data type
     */
    fMarkPriceConvertData(data: any) {
        const friendlyData = (data: any) => {
            const {
                e: eventType,
                E: eventTime,
                s: symbol,
                p: markPrice,
                i: indexPrice,
                r: fundingRate,
                T: fundingTime
            } = data;
            return {
                eventType,
                eventTime,
                symbol,
                markPrice,
                indexPrice,
                fundingRate,
                fundingTime
            };
        };
        if (Array.isArray(data)) {
            const result = [];
            for (const obj of data) {
                result.push(friendlyData(obj));
            }
            return result;
        }
        return friendlyData(data);
    }

    /**
     * Converts the futures aggTrade stream data into a friendly object
     * @param {object} data - user data callback data type
     * @return {object} - user friendly data type
     */
    fAggTradeConvertData(data: any) {
        const friendlyData = (data: any) => {
            const {
                e: eventType,
                E: eventTime,
                s: symbol,
                a: aggTradeId,
                p: price,
                q: amount,
                f: firstTradeId,
                l: lastTradeId,
                T: timestamp,
                m: maker
            } = data;
            return {
                eventType,
                eventTime,
                symbol,
                aggTradeId,
                price,
                amount,
                total: price * amount,
                firstTradeId,
                lastTradeId,
                timestamp,
                maker
            };
        };
        if (Array.isArray(data)) {
            const result = [];
            for (const obj of data) {
                result.push(friendlyData(obj));
            }
            return result;
        }
        return friendlyData(data);
    }

    /**
     * Delivery heartbeat code with a shared single interval tick
     * @return {undefined}
     */
    deliverySocketHeartbeat() {
        /* Sockets removed from subscriptions during a manual terminate()
         will no longer be at risk of having functions called on them */
        for (const endpointId in this.deliverySubscriptions) {
            const ws = this.deliverySubscriptions[endpointId];
            if (ws.isAlive) {
                ws.isAlive = false;
                if (ws.readyState === WebSocket.OPEN) ws.ping(this.noop);
            } else {
                if (this.Options.verbose) this.Options.log(`Terminating zombie delivery WebSocket: ${ws.endpoint}`);
                if (ws.readyState === WebSocket.OPEN) ws.terminate();
            }
        }
    }

    /**
     * Called when a delivery socket is opened, subscriptions are registered for later reference
     * @param {function} openCallback - a callback function
     * @return {undefined}
     */
    handleDeliverySocketOpen(wsBind, openCallback: Callback) {
        this.isAlive = true;
        if (Object.keys(this.deliverySubscriptions).length === 0) {
            this.socketHeartbeatInterval = setInterval(this.deliverySocketHeartbeat.bind(this), 30000);
        }
        this.deliverySubscriptions[wsBind.url] = this;
        if (typeof openCallback === 'function') openCallback(wsBind.url);
    }

    /**
     * Called when delivery websocket is closed, subscriptions are de-registered for later reference
     * @param {boolean} reconnect - true or false to reconnect the socket
     * @param {string} code - code associated with the socket
     * @param {string} reason - string with the response
     * @return {undefined}
     */
    handleDeliverySocketClose(wsBind, reconnect, code, reason) {
        delete this.deliverySubscriptions[wsBind.url];
        if (this.deliverySubscriptions && Object.keys(this.deliverySubscriptions).length === 0) {
            clearInterval(this.socketHeartbeatInterval);
        }
        this.Options.log('Delivery WebSocket closed: ' + wsBind.url +
            (code ? ' (' + code + ')' : '') +
            (reason ? ' ' + reason : ''));
        if (this.Options.reconnect && wsBind.reconnect && reconnect) {
            if (wsBind.url && wsBind.url.length === 60) this.Options.log('Delivery account data WebSocket reconnecting...');
            else this.Options.log('Delivery WebSocket reconnecting: ' + wsBind.url + '...');
            try {
                reconnect();
            } catch (error) {
                this.Options.log('Delivery WebSocket reconnect error: ' + error.message);
            }
        }
    }

    /**
     * Called when a delivery websocket errors
     * @param {object} error - error object message
     * @return {undefined}
     */
    handleDeliverySocketError(wsBind, error) {
        this.Options.log('Delivery WebSocket error: ' + wsBind.url +
            (error.code ? ' (' + error.code + ')' : '') +
            (error.message ? ' ' + error.message : ''));
    }

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

    /**
     * Used to subscribe to a single delivery websocket endpoint
     * @param {string} endpoint - endpoint to connect to
     * @param {function} callback - the function to call when information is received
     * @param {object} params - Optional reconnect {boolean} (whether to reconnect on disconnect), openCallback {function}, id {string}
     * @return {WebSocket} - websocket reference
     */
    deliverySubscribeSingle(endpoint: string, callback: Callback, params: Dict = {}) {
        if (typeof params === 'boolean') params = { reconnect: params };
        if (!params.reconnect) params.reconnect = false;
        if (!params.openCallback) params.openCallback = false;
        if (!params.id) params.id = false;
        const httpsproxy = this.getHttpsProxy();
        let socksproxy = this.getSocksProxy();
        let ws: any = undefined;
        if (socksproxy) {
            socksproxy = this.proxyReplacewithIp(socksproxy);
            if (this.Options.verbose) this.Options.log(`deliverySubscribeSingle: using socks proxy server: ${socksproxy}`);
            const agent = new SocksProxyAgent({
                protocol: this.parseProxy(socksproxy)[0],
                host: this.parseProxy(socksproxy)[1],
                port: this.parseProxy(socksproxy)[2]
            });
            ws = new WebSocket((this.getDStreamSingleUrl()) + endpoint, { agent });
        } else if (httpsproxy) {
            const config = url.parse(httpsproxy);
            const agent = new HttpsProxyAgent(config);
            if (this.Options.verbose) this.Options.log(`deliverySubscribeSingle: using proxy server: ${agent}`);
            ws = new WebSocket((this.getDStreamSingleUrl()) + endpoint, { agent });
        } else {
            ws = new WebSocket((this.getDStreamSingleUrl()) + endpoint);
        }

        if (this.Options.verbose) this.Options.log('deliverySubscribeSingle: Subscribed to ' + endpoint);
        ws.reconnect = this.Options.reconnect;
        ws.endpoint = endpoint;
        ws.isAlive = false;
        ws.on('open', this.handleDeliverySocketOpen.bind(this, ws, params.openCallback));
        ws.on('pong', this.handleDeliverySocketHeartbeat.bind(this, ws));
        ws.on('error', this.handleDeliverySocketError.bind(this, ws));
        ws.on('close', this.handleDeliverySocketClose.bind(this, ws, params.reconnect));
        ws.on('message', data => {
            try {
                if (this.Options.verbose) this.Options.log('deliverySubscribeSingle: Received data:', data);
                callback(JSONbig.parse(data));
            } catch (error) {
                this.Options.log('Parse error: ' + error.message);
            }
        });
        return ws;
    }

    /**
     * Used to subscribe to a combined delivery websocket endpoint
     * @param {string} streams - streams to connect to
     * @param {function} callback - the function to call when information is received
     * @param {object} params - Optional reconnect {boolean} (whether to reconnect on disconnect), openCallback {function}, id {string}
     * @return {WebSocket} - websocket reference
     */
    deliverySubscribe(streams, callback: Callback, params: Dict = {}) {
        if (typeof streams === 'string') return this.deliverySubscribeSingle(streams, callback, params);
        if (typeof params === 'boolean') params = { reconnect: params };
        if (!params.reconnect) params.reconnect = false;
        if (!params.openCallback) params.openCallback = false;
        if (!params.id) params.id = false;
        const httpsproxy = this.getHttpsProxy();
        let socksproxy = this.getSocksProxy();
        const queryParams = streams.join('/');
        let ws: any = undefined;
        if (socksproxy) {
            socksproxy = this.proxyReplacewithIp(socksproxy);
            if (this.Options.verbose) this.Options.log(`deliverySubscribe: using socks proxy server ${socksproxy}`);
            const agent = new SocksProxyAgent({
                protocol: this.parseProxy(socksproxy)[0],
                host: this.parseProxy(socksproxy)[1],
                port: this.parseProxy(socksproxy)[2]
            });
            ws = new WebSocket((this.getDStreamUrl()) + queryParams, { agent });
        } else if (httpsproxy) {
            if (this.Options.verbose) this.Options.log(`deliverySubscribe: using proxy server ${httpsproxy}`);
            const config = url.parse(httpsproxy);
            const agent = new HttpsProxyAgent(config);
            ws = new WebSocket((this.getDStreamUrl()) + queryParams, { agent });
        } else {
            ws = new WebSocket((this.getDStreamUrl()) + queryParams);
        }

        ws.reconnect = this.Options.reconnect;
        ws.endpoint = stringHash(queryParams);
        ws.isAlive = false;
        if (this.Options.verbose) {
            this.Options.log(`deliverySubscribe: Subscribed to [${ws.endpoint}] ${queryParams}`);
        }
        ws.on('open', this.handleDeliverySocketOpen.bind(this, ws, params.openCallback));
        ws.on('pong', this.handleDeliverySocketHeartbeat.bind(this, ws));
        ws.on('error', this.handleDeliverySocketError.bind(this, ws));
        ws.on('close', this.handleDeliverySocketClose.bind(this, ws, params.reconnect));
        ws.on('message', data => {
            try {
                if (this.Options.verbose) this.Options.log('deliverySubscribe: Received data:', data);
                callback(JSONbig.parse(data).data);
            } catch (error) {
                this.Options.log(`deliverySubscribe: Parse error: ${error.message}`);
            }
        });
        return ws;
    }

    /**
     * Used to terminate a delivery websocket
     * @param {string} endpoint - endpoint identifier associated with the web socket
     * @param {boolean} reconnect - auto reconnect after termination
     * @return {undefined}
     */
    deliveryTerminate(endpoint: string, reconnect = false) {
        if (this.Options.verbose) this.Options.log('Delivery WebSocket terminating:', endpoint);
        const ws = this.deliverySubscriptions[endpoint];
        if (!ws) return;
        ws.removeAllListeners('message');
        ws.reconnect = reconnect;
        ws.terminate();
    }

    /**
     * Combines all delivery OHLC data with the latest update
     * @param {string} symbol - the symbol
     * @param {string} interval - time interval
     * @return {array} - interval data for given symbol
     */
    deliveryKlineConcat(symbol: string, interval: Interval) {
        const output = this.deliveryTicks[symbol][interval];
        if (typeof this.deliveryRealtime[symbol][interval].time === 'undefined') return output;
        const time = this.deliveryRealtime[symbol][interval].time;
        const last_updated = Object.keys(this.deliveryTicks[symbol][interval]).pop();
        if (time >= last_updated) {
            output[time] = this.deliveryRealtime[symbol][interval];
            //delete output[time].time;
            output[last_updated].isFinal = true;
            output[time].isFinal = false;
        }
        return output;
    }

    /**
     * Used for websocket delivery @kline
     * @param {string} symbol - the symbol
     * @param {object} kline - object with kline info
     * @param {string} firstTime - time filter
     * @return {undefined}
     */
    deliveryKlineHandler(symbol: string, kline: any, firstTime = 0) {
        // eslint-disable-next-line no-unused-vars
        const { e: eventType, E: eventTime, k: ticks } = kline;
        // eslint-disable-next-line no-unused-vars
        const { o: open, h: high, l: low, c: close, v: volume, i: interval, x: isFinal, q: quoteVolume, V: takerBuyBaseVolume, Q: takerBuyQuoteVolume, n: trades, t: time, T: closeTime } = ticks;
        if (time <= firstTime) return;
        if (!isFinal) {
            // if ( typeof this.futuresRealtime[symbol][interval].time !== 'undefined' ) {
            //     if ( this.futuresRealtime[symbol][interval].time > time ) return;
            // }
            this.deliveryRealtime[symbol][interval] = { time, closeTime, open, high, low, close, volume, quoteVolume, takerBuyBaseVolume, takerBuyQuoteVolume, trades, isFinal };
            return;
        }
        const first_updated = Object.keys(this.deliveryTicks[symbol][interval]).shift();
        if (first_updated) delete this.deliveryTicks[symbol][interval][first_updated];
        this.deliveryTicks[symbol][interval][time] = { time, closeTime, open, high, low, close, volume, quoteVolume, takerBuyBaseVolume, takerBuyQuoteVolume, trades, isFinal: false };
    }

    /**
     * Converts the delivery liquidation stream data into a friendly object
     * @param {object} data - liquidation data callback data type
     * @return {object} - user friendly data type
     */
    dLiquidationConvertData(data: any) {
        const eventType = data.e, eventTime = data.E;
        const {
            s: symbol,
            S: side,
            o: orderType,
            f: timeInForce,
            q: origAmount,
            p: price,
            ap: avgPrice,
            X: orderStatus,
            l: lastFilledQty,
            z: totalFilledQty,
            T: tradeTime
        } = data.o;
        return { symbol, side, orderType, timeInForce, origAmount, price, avgPrice, orderStatus, lastFilledQty, totalFilledQty, eventType, tradeTime, eventTime };
    }

    /**
     * Converts the delivery ticker stream data into a friendly object
     * @param {object} data - user data callback data type
     * @return {object} - user friendly data type
     */
    dTickerConvertData(data: any) {
        const friendlyData = (data: any) => {
            const {
                e: eventType,
                E: eventTime,
                s: symbol,
                p: priceChange,
                P: percentChange,
                w: averagePrice,
                c: close,
                Q: closeQty,
                o: open,
                h: high,
                l: low,
                v: volume,
                q: quoteVolume,
                O: openTime,
                C: closeTime,
                F: firstTradeId,
                L: lastTradeId,
                n: numTrades
            } = data;
            return {
                eventType,
                eventTime,
                symbol,
                priceChange,
                percentChange,
                averagePrice,
                close,
                closeQty,
                open,
                high,
                low,
                volume,
                quoteVolume,
                openTime,
                closeTime,
                firstTradeId,
                lastTradeId,
                numTrades
            };
        };
        if (Array.isArray(data)) {
            const result = [];
            for (const obj of data) {
                result.push(friendlyData(obj));
            }
            return result;
        }
        return friendlyData(data);
    }

    /**
     * Converts the delivery miniTicker stream data into a friendly object
     * @param {object} data - user data callback data type
     * @return {object} - user friendly data type
     */
    dMiniTickerConvertData(data: any) {
        const friendlyData = (data: any) => {
            const {
                e: eventType,
                E: eventTime,
                s: symbol,
                c: close,
                o: open,
                h: high,
                l: low,
                v: volume,
                q: quoteVolume
            } = data;
            return {
                eventType,
                eventTime,
                symbol,
                close,
                open,
                high,
                low,
                volume,
                quoteVolume
            };
        };
        if (Array.isArray(data)) {
            const result = [];
            for (const obj of data) {
                result.push(friendlyData(obj));
            }
            return result;
        }
        return friendlyData(data);
    }

    /**
     * Converts the delivery bookTicker stream data into a friendly object
     * @param {object} data - user data callback data type
     * @return {object} - user friendly data type
     */
    dBookTickerConvertData(data: any) {
        const {
            u: updateId,
            s: symbol,
            b: bestBid,
            B: bestBidQty,
            a: bestAsk,
            A: bestAskQty
        } = data;
        return {
            updateId,
            symbol,
            bestBid,
            bestBidQty,
            bestAsk,
            bestAskQty
        };
    }

    /**
     * Converts the delivery markPrice stream data into a friendly object
     * @param {object} data - user data callback data type
     * @return {object} - user friendly data type
     */
    dMarkPriceConvertData(data: any) {
        const friendlyData = (data: any) => {
            const {
                e: eventType,
                E: eventTime,
                s: symbol,
                p: markPrice,
                r: fundingRate,
                T: fundingTime
            } = data;
            return {
                eventType,
                eventTime,
                symbol,
                markPrice,
                fundingRate,
                fundingTime
            };
        };
        if (Array.isArray(data)) {
            const result = [];
            for (const obj of data) {
                result.push(friendlyData(obj));
            }
            return result;
        }
        return friendlyData(data);
    }

    /**
     * Converts the delivery aggTrade stream data into a friendly object
     * @param {object} data - user data callback data type
     * @return {object} - user friendly data type
     */
    dAggTradeConvertData(data: any) {
        const friendlyData = (data: any) => {
            const {
                e: eventType,
                E: eventTime,
                s: symbol,
                a: aggTradeId,
                p: price,
                q: amount,
                f: firstTradeId,
                l: lastTradeId,
                T: timestamp,
                m: maker
            } = data;
            return {
                eventType,
                eventTime,
                symbol,
                aggTradeId,
                price,
                amount,
                total: price * amount,
                firstTradeId,
                lastTradeId,
                timestamp,
                maker
            };
        };
        if (Array.isArray(data)) {
            const result = [];
            for (const obj of data) {
                result.push(friendlyData(obj));
            }
            return result;
        }
        return friendlyData(data);
    }

    /**
   * Converts the delivery UserData stream ORDER_TRADE_UPDATE data into a friendly object
   * @param {object} data - user data callback data type
   * @return {object} - user friendly data type
   */
    dUserDataOrderUpdateConvertData(data: any) {
        const {
            e: eventType,
            E: eventTime,
            T: transaction, // transaction time
        } = data;

        let { o: order } = data;

        const orderConverter = (order) => {
            const {
                s: symbol,
                c: clientOrderId,
                // special client order id:
                // starts with "autoclose-": liquidation order
                // "adl_autoclose": ADL auto close order
                S: side,
                o: orderType,
                f: timeInForce,
                q: originalQuantity,
                p: originalPrice,
                ap: averagePrice,
                sp: stopPrice, // please ignore with TRAILING_STOP_MARKET order,
                x: executionType,
                X: orderStatus,
                i: orderId,
                l: orderLastFilledQuantity,
                z: orderFilledAccumulatedQuantity,
                L: lastFilledPrice,
                ma: marginAsset,
                N: commissionAsset, // will not push if no commission
                n: commission, // will not push if no commission
                T: orderTradeTime,
                t: tradeId,
                rp: realizedProfit,
                b: bidsNotional,
                a: askNotional,
                m: isMakerSide, // is this trade maker side
                R: isReduceOnly, // is this reduce only
                wt: stopPriceWorkingType,
                ot: originalOrderType,
                ps: positionSide,
                cp: closeAll, // if close-all, pushed with conditional order
                AP: activationPrice, // only pushed with TRAILING_STOP_MARKET order
                cr: callbackRate, // only pushed with TRAILING_STOP_MARKET order
                pP: priceProtect, // If conditional order trigger is protected
            } = order;
            return {
                symbol,
                clientOrderId,
                side,
                orderType,
                timeInForce,
                originalQuantity,
                originalPrice,
                averagePrice,
                stopPrice,
                executionType,
                orderStatus,
                orderId,
                orderLastFilledQuantity,
                orderFilledAccumulatedQuantity,
                lastFilledPrice,
                marginAsset,
                commissionAsset,
                commission,
                orderTradeTime,
                tradeId,
                bidsNotional,
                askNotional,
                isMakerSide,
                isReduceOnly,
                stopPriceWorkingType,
                originalOrderType,
                positionSide,
                closeAll,
                activationPrice,
                callbackRate,
                realizedProfit,
                priceProtect,
            };
        };
        order = orderConverter(order);
        return {
            eventType,
            eventTime,
            transaction,
            order,
        };
    }

    /**
     * Used as part of the user data websockets callback
     * @param {object} data - user data callback data type
     * @return {undefined}
     */
    userDataHandler(data: any) {
        let eventData = data;
        if (data.subscriptionId !== undefined && data.event) {
            eventData = data.event;
        }

        const type = eventData.e;

        // Handle event stream termination
        if (type === 'eventStreamTerminated') {
            this.Options.log('User Data Stream terminated at ' + eventData.E);
            if (this.Options.all_updates_callback) this.Options.all_updates_callback(eventData);
            return;
        }

        if (this.Options.all_updates_callback) this.Options.all_updates_callback(eventData);

        if (type === 'outboundAccountInfo') {
            // XXX: Deprecated in 2020-09-08
        } else if (type === 'executionReport') {
            if (this.Options.execution_callback) this.Options.execution_callback(eventData);
        } else if (type === 'listStatus') {
            if (this.Options.list_status_callback) this.Options.list_status_callback(eventData);
        } else if (type === 'outboundAccountPosition' || type === 'balanceUpdate') {
            if (this.Options.balance_callback) this.Options.balance_callback(eventData);
        } else if (type === 'externalLockUpdate') {
            // Handle external lock updates (e.g., when balance is locked for margin collateral)
            if (this.Options.balance_callback) this.Options.balance_callback(eventData);
        } else {
            this.Options.log('Unexpected userData: ' + type);
        }
    }

    /**
     * Used as part of the user data websockets callback
     * @param {object} data - user data callback data type
     * @return {undefined}
     */
    userMarginDataHandler(data: any) {
        let eventData = data;
        if (data.subscriptionId !== undefined && data.event) {
            eventData = data.event;
        }

        const type = eventData.e;

        // Handle event stream termination
        if (type === 'eventStreamTerminated') {
            this.Options.log('Margin Data Stream terminated at ' + eventData.E);
            if (this.Options.margin_all_updates_callback) this.Options.margin_all_updates_callback(eventData);
            return;
        }

        if (this.Options.margin_all_updates_callback) this.Options.margin_all_updates_callback(eventData);

        if (type === 'outboundAccountInfo') {
            // XXX: Deprecated in 2020-09-08
        } else if (type === 'executionReport') {
            if (this.Options.margin_execution_callback) this.Options.margin_execution_callback(eventData);
        } else if (type === 'listStatus') {
            if (this.Options.margin_list_status_callback) this.Options.margin_list_status_callback(eventData);
        } else if (type === 'outboundAccountPosition' || type === 'balanceUpdate') {
            if (this.Options.margin_balance_callback) this.Options.margin_balance_callback(eventData);
        } else if (type === 'externalLockUpdate') {
            // Handle external lock updates (e.g., when balance is locked for margin collateral)
            if (this.Options.margin_balance_callback) this.Options.margin_balance_callback(eventData);
        }
    }

    /**
     * Used as part of the user data websockets callback
     * @param {object} data - user data callback data type
     * @return {undefined}
     */
    userFutureDataHandler(data: any) {
        const type = data.e;

        if (this.Options.futures_all_updates_callback) this.Options.futures_all_updates_callback(data);

        if (type === 'MARGIN_CALL') {
            this.Options.future_margin_call_callback(this.fUserDataMarginConvertData(data));
        } else if (type === 'ACCOUNT_UPDATE') {
            if (this.Options.future_account_update_callback) {
                this.Options.future_account_update_callback(this.fUserDataAccountUpdateConvertData(data));
            }
        } else if (type === 'ORDER_TRADE_UPDATE' || type === 'TRADE_LITE') {
            if (this.Options.future_order_update_callback) {
                this.Options.future_order_update_callback(this.fUserDataOrderUpdateConvertData(data));
            }
        } else if (type === 'ACCOUNT_CONFIG_UPDATE') {
            if (this.Options.future_account_config_update_callback) {
                this.Options.future_account_config_update_callback(this.fUserConfigDataAccountUpdateConvertData(data));
            }
        }
    }

    /**
   * Used as part of the user data websockets callback
   * @param {object} data - user data callback data type
   * @return {undefined}
   */
    userDeliveryDataHandler(data: any) {
        const type = data.e;
        if (type === "MARGIN_CALL") {
            this.Options.delivery_margin_call_callback(
                this.fUserDataMarginConvertData(data)
            );
        } else if (type === "ACCOUNT_UPDATE") {
            if (this.Options.delivery_account_update_callback) {
                this.Options.delivery_account_update_callback(
                    this.fUserDataAccountUpdateConvertData(data)
                );
            }
        } else if (type === "ORDER_TRADE_UPDATE") {
            if (this.Options.delivery_order_update_callback) {
                this.Options.delivery_order_update_callback(
                    this.dUserDataOrderUpdateConvertData(data)
                );
            }
        } else {
            this.Options.log("Unexpected userDeliveryData: " + type);
        }
    }

    /**
    * Universal Transfer requires API permissions enabled
    * @param {string} type - ENUM , example MAIN_UMFUTURE for SPOT to USDT futures, see https://binance-docs.github.io/apidocs/spot/en/#user-universal-transfer
    * @param {string} asset - the asset - example :USDT    *
    * @param {number} amount - the callback function
    * @return {promise}
    */
    async universalTransfer(type: string, asset: string, amount: number) {
        const parameters = Object.assign({
            asset,
            amount,
            type,
        });
        return await this.privateSpotRequest("v1/asset/transfer",
            parameters,
            "POST"
        );
    }

    /**
   * Transfer between main account and futures/delivery accounts
   * @param {string} asset - the asset
   * @param {number} amount - the asset
   * @param {object} options - additional options
   * @return {undefined}
   */
    async transferBetweenMainAndFutures(
        asset: string,
        amount: number,
        type: any,
    ) {
        const parameters = Object.assign({
            asset,
            amount,
            type,
        });
        return await this.privateSpotRequest("v1/futures/transfer",
            parameters,
            "POST"
        );
    }

    /**
     * Converts the previous day stream into friendly object
     * @param {object} data - user data callback data type
     * @return {object} - user friendly data type
     */
    prevDayConvertData(data: any) {
        const convertData = (data: any) => {
            const {
                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 (const obj of data) {
                const 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}
     */
    prevDayStreamHandler(data, callback: Callback) {
        const converted = this.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
     */
    priceData(data: any): { [key: string]: number } {
        const prices = {};
        if (Array.isArray(data)) {
            for (const obj of data) {
                prices[obj.symbol] = obj.price;
            }
        } else { // Single price returned
            prices[data.symbol] = parseFloat(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
     */
    bookPriceData(data: any): { [key: string]: BookTicker } {
        if (!Array.isArray(data)) {
            data = [data];
        }
        const prices = {};
        for (const obj of data) {
            prices[obj.symbol] = obj;
        }
        return prices;
    }

    /**
     * Used by balance to get the balance data
     * @param {array} data - account info object
     * @return {object} - balances hel with available, onorder amounts
     */
    balanceData(data: any) {
        const balances = {};
        if (typeof data === 'undefined') return {};
        if (typeof data.balances === 'undefined') {
            this.Options.log('balanceData error', data);
            return {};
        }
        for (const 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}
     */
    klineData(symbol, interval, ticks) { // Used for /depth
        let last_time = 0;
        if (this.isIterable(ticks)) {
            for (const tick of ticks) {
                // eslint-disable-next-line no-unused-vars
                const [time, open, high, low, close, volume, closeTime, assetVolume, trades, buyBaseVolume, buyAssetVolume, ignored] = tick;
                this.ohlc[symbol][interval][time] = { open: open, high: high, low: low, close: close, volume: volume };
                last_time = time;
            }

            this.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
     */
    klineConcat(symbol: string, interval: Interval) {
        const output = this.ohlc[symbol][interval];
        if (typeof this.ohlcLatest[symbol][interval].time === 'undefined') return output;
        const time = this.ohlcLatest[symbol][interval].time;
        const last_updated = Object.keys(this.ohlc[symbol][interval]).pop();
        if (time >= last_updated) {
            output[time] = this.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}
     */
    klineHandler(symbol, kline, firstTime = 0) {
        // TODO: add Taker buy base asset volume
        // eslint-disable-next-line no-unused-vars
        const { e: eventType, E: eventTime, k: ticks } = kline;
        // eslint-disable-next-line no-unused-vars
        const { 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 this.ohlcLatest[symbol][interval].time !== 'undefined') {
                if (this.ohlcLatest[symbol][interval].time > time) return;
            }
            this.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(this.ohlc[symbol][interval]).shift();
        if (first_updated) delete this.ohlc[symbol][interval][first_updated];
        this.ohlc[symbol][interval][time] = { open: open, high: high, low: low, close: close, volume: volume };
    }

    /**
     * Used by futures websockets chart cache
     * @param {string} symbol - symbol to get candlestick info
     * @param {string} interval - time interval, 1m, 3m, 5m ....
     * @param {array} ticks - tick array
     * @return {undefined}
     */
    futuresKlineData(symbol: string, interval: Interval, ticks: any[]) {
        let last_time = 0;
        if (this.isIterable(ticks)) {
            for (const tick of ticks) {
                // eslint-disable-next-line no-unused-vars
                const [time, open, high, low, close, volume, closeTime, quoteVolume, trades, takerBuyBaseVolume, takerBuyQuoteVolume, ignored] = tick;
                this.futuresTicks[symbol][interval][time] = { time, closeTime, open, high, low, close, volume, quoteVolume, takerBuyBaseVolume, takerBuyQuoteVolume, trades };
                last_time = time;
            }
            this.futuresMeta[symbol][interval].timestamp = last_time;
        }
    }

    /**
     * Used by delivery websockets chart cache
     * @param {string} symbol - symbol to get candlestick info
     * @param {string} interval - time interval, 1m, 3m, 5m ....
     * @param {array} ticks - tick array
     * @return {undefined}
     */
    deliveryKlineData(symbol, interval, ticks) {
        let last_time = 0;
        if (this.isIterable(ticks)) {
            for (const tick of ticks) {
                // eslint-disable-next-line no-unused-vars
                const [time, open, high, low, close, volume, closeTime, quoteVolume, trades, takerBuyBaseVolume, takerBuyQuoteVolume, ignored] = tick;
                this.deliveryTicks[symbol][interval][time] = { time, closeTime, open, high, low, close, volume, quoteVolume, takerBuyBaseVolume, takerBuyQuoteVolume, trades };
                last_time = time;
            }
            this.deliveryMeta[symbol][interval].timestamp = last_time;
        }
    }

    /**
     * Used for /depth endpoint
     * @param {object} data - containing the bids and asks
     * @return {undefined}
     */
    depthData(data: any) {
        if (!data) return { bids: [], asks: [] };
        const bids = {}, asks = {};
        let 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 };
    }

    parseOrderBook(data, symbol: string): OrderBook {
        const { lastUpdateId, bids, asks } = data;
        if (!bids || !asks) {
            return data;
        }
        const orderBook: OrderBook = {
            symbol,
            lastUpdateId,
            bids: bids.map(b => zip(['price', 'quantity'], b)),
            asks: asks.map(a => zip(['price', 'quantity'], a))
        };
        return orderBook;
    }

    /**
     * Used for /depth endpoint
     * @param {object} depth - information
     * @return {undefined}
     */
    depthHandler(depth) {
        const symbol = depth.s;
        let obj;
        const context = this.depthCacheContext[symbol];
        const updateDepthCache = () => {
            this.depthCache[symbol].eventTime = depth.E;
            for (obj of depth.b) { //bids
                if (obj[1] == 0) {
                    delete this.depthCache[symbol].bids[obj[0]];
                } else {
                    this.depthCache[symbol].bids[obj[0]] = parseFloat(obj[1]);
                }
            }
            for (obj of depth.a) { //asks
                if (obj[1] == 0) {
                    delete this.depthCache[symbol].asks[obj[0]];
                } else {
                    this.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 (this.Options.verbose) this.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 (this.Options.verbose) this.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
     */
    getDepthCache(symbol: string) {
        if (typeof this.depthCache[symbol] === 'undefined') return { bids: {}, asks: {}};
        return this.depthCache[symbol];
    }

    /**
     * Calculate Buy/Sell volume from DepthCache
     * @param {string} symbol - the symbol to fetch
     * @return {object} - the depth volume cache object
     */
    depthVolume(symbol: string) {
        const cache = this.getDepthCache(symbol);
        let 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
     */
    isArrayUnique(array: any[]) {
        return new Set(array).size === array.length;
    }

    // --- PUBLIC FUNCTIONS --- //

    /**
        * Count decimal places
        * @param {float} float - get the price precision point
        * @return {int} - number of place
        */
    getPrecision(float: number) {
        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(qty, stepSize) {
        // Integers do not require rounding
        if (Number.isInteger(qty)) return qty;
        const qtyString = parseFloat(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(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(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(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(object) {
        const range = Object.keys(object).reverse(), output = {};
        for (const 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(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(symbol: string, max = Infinity, baseValue?: string) {
        const object = {};
        let count = 0, cache;
        if (typeof symbol === 'object') cache = symbol;
        else cache = this.getDepthCache(symbol).bids;
        const sorted = Object.keys(cache).sort((a, b) => parseFloat(b) - parseFloat(a));
        let cumulative = 0;
        for (const price of sorted) {
            if (!baseValue) object[price] = cache[price];
            else if (baseValue === 'cumulative') {
                cumulative += cache[price];
                object[price] = cumulative;
            } 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(symbol: string, max = Infinity, baseValue?: string) {
        let count = 0, cache;
        const object = {};
        if (typeof symbol === 'object') cache = symbol;
        else cache = this.getDepthCache(symbol).asks;
        const sorted = Object.keys(cache).sort((a, b) => parseFloat(a) - parseFloat(b));
        let cumulative = 0;
        for (const price of sorted) {
            if (!baseValue) object[price] = cache[price];
            else if (baseValue === 'cumulative') {
                cumulative += cache[price];
                object[price] = cumulative;
            } 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(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(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(object, start = 0) {
        return Object.keys(object).slice(start);
    }

    /**
    * Gets the minimum key form object
    * @param {object} object - the object to get the properties form
    * @return {string} - the minimum key
    */
    min(object) {
        // eslint-disable-next-line prefer-spread
        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(object) {
        // eslint-disable-next-line prefer-spread
        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(key, value) {
        this.Options[key] = value;
    }

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

    /**
    * Returns the entire info object
    * @return {object} - the info object
    */
    getInfo() { return this.info; }

    /**
    * Returns the used weight from the last request
    * @return {object} - 1m weight used
    */
    usedWeight() { return this.info.usedWeight; }

    /**
    * Returns the status code from the last http response
    * @return {object} - status code
    */
    statusCode() { return this.info.statusCode; }

    /**
    * Returns the ping time from the last futures request
    * @return {object} - latency/ping (2ms)
    */
    futuresLatency() { return this.info.futuresLatency; }

    /**
    * Returns the complete URL from the last request
    * @return {object} - http address including query string
    */
    lastURL() { return this.info.lastURL; }

    /**
    * Returns the order count from the last request
    * @return {object} - orders allowed per 1m
    */
    orderCount() { return this.info.orderCount1m; }

    /**
    * Returns the entire options object
    * @return {object} - the options object
    */
    getOptions() { return this.Options; }

    // /**
    // * Gets an option given a key
    // * @param {object} opt - the object with the class configuration
    //
    // * @return {undefined}
    // */
    // options() {this.setOptions()}

    // /**
    // * Creates a buy order
    // * @param {string} symbol - the symbol to buy
    // * @param {numeric} quantity - the quantity required
    // * @param {numeric} price - the price to pay for each unit
    // * @param {object} flags - additional buy order flags
    //
    // * @return {promise or undefined} - omitting the callback returns a promise
    // */
    // buy(symbol, quantity, price, flags: Dict = {}, 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
    //
    // * @return {promise or undefined} - omitting the callback returns a promise
    // */
    // sell(symbol, quantity, price, flags: Dict = {}, 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 sell order
    // * @param {string} symbol - the symbol to sell
    // * @param {numeric} quantity - the quantity required
    // * @param {object} flags - additional sell order flags
    //
    // * @return {promise or undefined} - omitting the callback returns a promise
    // */
    // marketSell(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);
    //     }
    // }

    /**
    * Gets the depth information for a given symbol
    * @see https://developers.binance.com/docs/binance-spot-api-docs/rest-api/market-data-endpoints#order-book
    * @param {string} symbol - the symbol
    * @param {int} limit - limit the number of returned orders
    * @return {promise or undefined} - omitting the callback returns a promise
    */
    async depth(symbol: string, limit = 100): Promise<OrderBook> {
        const data = await this.publicSpotRequest('v3/depth', { symbol: symbol, limit: limit });
        return this.parseOrderBook(data, symbol);
    }

    /**
    * Gets the average prices of a given symbol
    * @see https://developers.binance.com/docs/binance-spot-api-docs/rest-api/market-data-endpoints#current-average-price
    * @param {string} symbol - the symbol
    * @return {promise or undefined} - omitting the callback returns a promise
    */
    async avgPrice(symbol: string, params: Dict = {}) {
        params.symbol = symbol;
        return await this.publicSpotRequest('v3/avgPrice', params);
    }

    /**
    * Gets the prices of a given symbol(s)
    * @param {string} symbol - the symbol
    * @see https://developers.binance.com/docs/binance-spot-api-docs/rest-api/market-data-endpoints#symbol-price-ticker
    * @return {promise or undefined} - omitting the callback returns a promise
    */
    async prices(symbol?: string, params: Dict = {}): Promise<{ [key: string]: number }> {
        if (symbol) params.symbol = symbol;
        const data = await this.publicSpotRequest('v3/ticker/price', params);
        return this.priceData(data);
    }

    /**
    * Gets the ticker price via WebSocket API (JSON-RPC)
    * @param {string} symbol - single symbol
    * @param {string[]} symbols - array of symbols
    * @param {object} options - additional options (e.g. symbolStatus)
    * @see https://developers.binance.com/docs/binance-spot-api-docs/web-socket-api/market-data-requests#symbol-price-ticker
    * @return {promise} - resolves with ticker price data
    */
    async tickerPrice(symbol?: string, symbols?: string[], options: Dict = {}): Promise<any> {
        if (symbol && symbols) {
            throw new Error('Cannot specify both symbol and symbols parameters');
        }

        const connectionId = 'marketData';
        await this.ensureWsApiConnection(connectionId);

        const params: Dict = { ...options };
        if (symbol) params.symbol = symbol;
        if (symbols) params.symbols = symbols;

        return this.sendWsApiRequest(connectionId, 'ticker.price', params);
    }

    /**
    * Ensures a WebSocket API connection is open for the given connectionId
    * @param {string} connectionId - connection identifier
    * @return {promise} - resolves when the connection is open
    */
    private ensureWsApiConnection(connectionId: string): Promise<void> {
        return new Promise((resolve, reject) => {
            const existing = this.wsApiConnections[connectionId];
            if (existing) {
                if (existing.readyState === WebSocket.OPEN) {
                    resolve();
                    return;
                }
                if (existing.readyState === WebSocket.CONNECTING) {
                    existing.once('open', () => resolve());
                    existing.once('error', (err: Error) => reject(err));
                    return;
                }
            }
            const ws = this.connectWsApi(connectionId, () => {}, () => {});
            ws.once('open', () => resolve());
            ws.once('error', (err: Error) => reject(err));
        });
    }

    /**
    * Gets the book tickers of given symbol(s)
    * @see https://developers.binance.com/docs/binance-spot-api-docs/testnet/rest-api/market-data-endpoints#symbol-order-book-ticker
    * @param {string} symbol - the symbol
    * @return {promise or undefined} - omitting the callback returns a promise
    */
    async bookTickers(symbol?: string, params: Dict = {}): Promise<{ [key: string]: BookTicker }> {
        if (symbol) params.symbol = symbol;
        const data = await this.publicSpotRequest('v3/ticker/bookTicker', params);
        return this.bookPriceData(data);
    }

    /**
    * Gets the prevday percentage change
    * @see https://developers.binance.com/docs/binance-spot-api-docs/rest-api/market-data-endpoints#24hr-ticker-price-change-statistics
    * @param {string} symbol - the symbol or symbols
    * @return {promise or undefined} - omitting the callback returns a promise
    */
    async prevDay(symbol?: string, params: Dict = {}): Promise<DailyStats[] | DailyStats> {
        if (symbol) params.symbol = symbol;
        return await this.publicSpotRequest('v3/ticker/24hr', params);
    }

    /**
    * Gets the prevday percentage change
    * @see https://developers.binance.com/docs/binance-spot-api-docs/rest-api/market-data-endpoints#24hr-ticker-price-change-statistics
    * @param {string} symbol - the symbol or symbols
    * @return {promise or undefined} - omitting the callback returns a promise
    */
    async ticker24h(symbol?: string, params: Dict = {}): Promise<DailyStats[] | DailyStats> {
        if (symbol) params.symbol = symbol;
        return await this.publicSpotRequest('v3/ticker/24hr', params);
    }

    /**
    * Gets the prevday percentage change
    * @see https://developers.binance.com/docs/binance-spot-api-docs/rest-api/market-data-endpoints#24hr-ticker-price-change-statistics
    * @param {string} symbol - the symbol or symbols
    * @return {promise or undefined} - omitting the callback returns a promise
    */
    async dailyStats(symbol?: string, params: Dict = {}): Promise<DailyStats[] | DailyStats> {
        if (symbol) params.symbol = symbol;
        return await this.publicSpotRequest('v3/ticker/24hr', params);
    }

    /**
    * Gets the the exchange info
    * @see https://developers.binance.com/docs/binance-spot-api-docs/rest-api/general-endpoints#exchange-information
    * @return {promise or undefined} - omitting the callback returns a promise
    */
    async exchangeInfo() {
        return await this.publicSpotRequest('v3/exchangeInfo', {});
    }

    /**
    * Gets the dust log for user
    * @return {promise or undefined} - omitting the callback returns a promise
    */
    async dustLog() {
        return await this.privateSpotRequest('v1/asset/dribblet', {});
    }

    async dustTransfer(assets) {
        return await this.privateSpotRequest('v1/asset/dust', { asset: assets }, 'POST');
    }

    async assetDividendRecord(params: Dict = {}) {
        return await this.privateSpotRequest('v1/asset/assetDividend', params);
    }

    /**
    * Gets the the system status
    * @see https://developers.binance.com/docs/wallet/others/system-status
    * @return {promise or undefined} - omitting the callback returns a promise
    */
    async systemStatus(params: Dict = {}) {
        return await this.publicRequest(this.sapi + 'v1/system/status', params);
    }

    /**
    * Withdraws asset to given wallet id
    * @see https://developers.binance.com/docs/wallet/capital/withdraw
    * @param {string} coin - the coin 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 {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
    */
    async withdraw(coin: string, address: string, amount: number, addressTag?: string, name?: string, params: Dict = {}): Promise<WithdrawResponse> {
        // const params = { asset, address, amount };
        params.coin = coin;
        params.address = address;
        params.amount = amount;
        if (name) params.name = name;
        if (addressTag) params.addressTag = addressTag;

        return await this.privateSapiRequest('v1/capital/withdraw/apply', params, 'POST');
    }

    /**
    * Get the Withdraws history for a given asset
    * @see https://developers.binance.com/docs/wallet/capital/withdraw-history
    * @param {object} params - supports limit and fromId parameters
    * @return {promise or undefined} - omitting the callback returns a promise
    */
    async withdrawHistory(params: Dict = {}): Promise<WithdrawHistoryResponse> {
        if (typeof params === 'string') params = { asset: params };
        return await this.privateSpotRequest('v1/capital/withdraw/history', params);
    }

    /**
    * Get the deposit history
    * @see https://developers.binance.com/docs/wallet/capital/deposite-history#http-request
    * @param {object} params - additional params
    * @return {promise or undefined} - omitting the callback returns a promise
    */
    async depositHistory(asset?: string, params: Dict = {}): Promise<DepositHistoryResponse> {
        if (asset) params = { asset: asset };
        return await this.privateSpotRequest('v1/capital/deposit/hisrec', params);
    }

    /**
    * Get the deposit address for given asset
    * @see https://developers.binance.com/docs/wallet/capital/deposite-address
    * @param {string} coin - the asset
    * @return {promise or undefined} - omitting the callback returns a promise
    */
    async depositAddress(asset: string, params: Dict = {}): Promise<DepositAddress> {
        return await this.privateSpotRequest('v1/capital/deposit/address', this.extend({ coin: asset }, params));
    }

    /**
    * Get the deposit address list for given asset
    * @see https://developers.binance.com/docs/wallet/capital/fetch-deposit-address-list-with-network
    * @param {string} coin - the asset
    * @return {promise or undefined} - omitting the callback returns a promise
    */
    async depositAddressList(asset: string, params: Dict = {}) {
        return await this.privateSpotRequest('v1/capital/deposit/address/list', this.extend({ coin: asset }, params));
    }

    /**
    * Get the account status
    * @see https://developers.binance.com/docs/binance-spot-api-docs/rest-api/account-endpoints#account-information-user_data
    * @return {promise or undefined} - omitting the callback returns a promise
    */
    async accountStatus(params: Dict = {}) {
        return await this.privateSpotRequest('v3/account', params);
    }

    /**
    * @return {promise or undefined} - omitting the callback returns a promise
    */
    async apiPermission(params: Dict = {}) {
        return await this.privateSpotRequest('v1/account/apiRestrictions', params);
    }

    /**
    * Get the trade fee
    * @see https://developers.binance.com/docs/wallet/asset/trade-fee
    * @param {string} symbol (optional)
    * @return {promise or undefined} - omitting the callback returns a promise
    */
    async tradeFee(symbol?: string) {
        const params = symbol ? { symbol: symbol } : {};
        return await this.privateSapiRequest('v1/asset/tradeFee', params);
    }

    /**
    * Fetch asset detail (minWithdrawAmount, depositStatus, withdrawFee, withdrawStatus, depositTip)
    * @see https://developers.binance.com/docs/wallet/asset
    * @return {promise or undefined} - omitting the callback returns a promise
    */
    async assetDetail(params: Dict = {}) {
        return await this.privateSpotRequest('asset/assetDetail', params);
    }

    /**
    * Get the account
    * @see https://developers.binance.com/docs/binance-spot-api-docs/testnet/rest-api/account-endpoints#account-information-user_data
    * @return {promise or undefined} - omitting the callback returns a promise
    */
    async account(params: Dict = {}): Promise<Account> {
        return await this.privateSpotRequest('v3/account', params);
    }

    /**
    * Get the balance data
    * @see https://developers.binance.com/docs/binance-spot-api-docs/testnet/rest-api/account-endpoints#account-information-user_data
    * @return {promise or undefined} - omitting the callback returns a promise
    */
    async balance(params: Dict = {}) {
        const data = await this.privateSpotRequest('v3/account', params);
        return this.balanceData(data);
    }

    /**
    * Get private trades for a given symbol
    * @see https://developers.binance.com/docs/binance-spot-api-docs/testnet/rest-api/account-endpoints#account-trade-list-user_data
    * @param {string} symbol - the symbol
    * @param {object} options - additional options
    * @return {promise or undefined} - omitting the callback returns a promise
    */
    async trades(symbol: string, params: Dict = {}): Promise<MyTrade[]> {
        const parameters = this.extend({ symbol: symbol }, params);
        return await this.privateSpotRequest('v3/myTrades', parameters);
    }

    /**
    * Get private trades for a given symbol
    * @see https://developers.binance.com/docs/binance-spot-api-docs/testnet/rest-api/account-endpoints#account-trade-list-user_data
    * @param {string} symbol - the symbol
    * @param {object} options - additional options
    * @return {promise or undefined} - omitting the callback returns a promise
    */
    async myTrades(symbol: string, params: Dict = {}): Promise<MyTrade[]> {
        const parameters = this.extend({ symbol: symbol }, params);
        return await this.privateSpotRequest('v3/myTrades', parameters);
    }

    /**
    * Tell api to use the server time to offset time indexes
    * @see https://developers.binance.com/docs/binance-spot-api-docs/rest-api/general-endpoints#check-server-time
    * @return {promise or undefined} - omitting the callback returns a promise
    */
    async useServerTime() {
        const response = await this.publicSpotRequest('v3/time', {});
        this.timeOffset = response.serverTime - new Date().getTime();
        return response;
    }

    // /**
    // * Get Binance server time
    // * @return {promise or undefined} - omitting the callback returns a promise
    // */
    // time() {

    //     publicRequest(this.getSpotUrl() + 'v3/time', {}, callback);
    // }

    /**
    * Ping binance
    * @see https://developers.binance.com/docs/binance-spot-api-docs/rest-api/general-endpoints#test-connectivity
    * @return {promise or undefined} - omitting the callback returns a promise
    */
    async ping() {
        return await this.publicSpotRequest('v3/ping', {});
    }

    parseAggTrades(symbol: string, trades: any[]): AggregatedTrade[] {
        const parsedTrades: AggregatedTrade[] = [];
        for (const trade of trades) {
            const aggT: AggregatedTrade = {
                aggId: trade.a,
                symbol: symbol,
                price: trade.p,
                quantity: trade.q,
                firstId: trade.f,
                lastId: trade.l,
                timestamp: trade.T,
                isBuyerMaker: trade.m,
            };
            if (trade.M) aggT.wasBestPrice = trade.M;
            parsedTrades.push(aggT);
        }
        return parsedTrades;
    }

    /**
    * Get agg trades for given symbol
    * @see https://developers.binance.com/docs/binance-spot-api-docs/rest-api/market-data-endpoints#compressedaggregate-trades-list
    * @param {string} symbol - the symbol
    * @param {object} options - additional optoins
    * @return {promise or undefined} - omitting the callback returns a promise
    */
    async aggTrades(symbol: string, params: Dict = {}): Promise<AggregatedTrade[]> { //fromId startTime endTime limit
        const parameters = Object.assign({ symbol }, params);
        const res = await this.publicSpotRequest('v3/aggTrades', parameters);
        return this.parseAggTrades(symbol, res);
    }

    /**
    * Get the recent trades
    * @see https://developers.binance.com/docs/binance-spot-api-docs/rest-api/market-data-endpoints#old-trade-lookup
    * @param {string} symbol - the symbol
    * @param {int} limit - limit the number of items returned
    * @return {promise or undefined} - omitting the callback returns a promise
    */
    async recentTrades(symbol: string, limit = 500, params: Dict = {}): Promise<Trade[]> {
        return await this.publicSpotRequest('v3/trades', this.extend({ symbol: symbol, limit: limit }, params));
    }

    /**
    * Get the historical trade info
    * @see https://developers.binance.com/docs/binance-spot-api-docs/rest-api/market-data-endpoints#old-trade-lookup
    * @param {string} symbol - the symbol
    * @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
    */
    async historicalTrades(symbol: string, limit = 500, fromId?: number, params: Dict = {}): Promise<Trade[]> {
        params.symbol = symbol;
        params.limit = limit;
        if (fromId) params.fromId = fromId;
        return await this.publicSpotRequest('v3/historicalTrades', params);
    }

    /**
    * 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(chart, include_volume = false) {
        const array = [];
        for (const timestamp in chart) {
            const obj = chart[timestamp];
            const 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
    */
    populateOHLC(chart) {
        const open = [], high = [], low = [], close = [], volume = [];
        for (const timestamp in chart) { //this.ohlc[symbol][interval]
            const 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
    * @see https://developers.binance.com/docs/binance-spot-api-docs/rest-api/market-data-endpoints#klinecandlestick-data
    * 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 {object} options - additional options
    * @return {promise or undefined} - omitting the callback returns a promise
    */
    async candlesticks(symbol: string, interval: Interval = '5m', params: Dict = {}): Promise<Candle[]> {
        if (!params.limit) params.limit = 500;
        params = Object.assign({ symbol: symbol, interval: interval }, params);
        const res = await this.publicSpotRequest('v3/klines', params);
        return this.parseCandles(res);
    }

    /**
    * Gets the candles information for a given symbol
    * @see https://developers.binance.com/docs/binance-spot-api-docs/rest-api/market-data-endpoints#klinecandlestick-data
    * 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 {object} options - additional options
    * @return {promise or undefined} - omitting the callback returns a promise
    */
    async candles(symbol: string, interval: Interval = '5m', params: Dict = {}): Promise<Candle[]> {
        return await this.candlesticks(symbol, interval, params); // make name consistent with futures
    }

    parseCandles(candles: any[]): Candle[] {
        const res: Candle[] = [];
        // spot
        // [
        //     [
        //       1499040000000,      // Open time
        //       "0.01634790",       // Open
        //       "0.80000000",       // High
        //       "0.01575800",       // Low
        //       "0.01577100",       // Close
        //       "148976.11427815",  // Volume
        //       1499644799999,      // Close time
        //       "2434.19055334",    // Quote asset volume
        //       308,                // Number of trades
        //       "1756.87402397",    // Taker buy base asset volume
        //       "28.46694368",      // Taker buy quote asset volume
        //       "17928899.62484339" // Ignore.
        //     ]
        // ]
        for (const rawCandle of candles) {
            const candle: Candle = {
                openTime: rawCandle[0],
                open: rawCandle[1],
                high: rawCandle[2],
                low: rawCandle[3],
                close: rawCandle[4],
                volume: rawCandle[5],
                closeTime: rawCandle[6],
                quoteAssetVolume: rawCandle[7],
                trades: rawCandle[8],
            };
            res.push(candle);
        }
        return res;
    }

    async spotGetDataStream(params: Dict = {}) {
        return await this.privateSpotRequest('v3/userDataStream', params, 'POST', true);
    }

    async spotKeepDataStream(listenKey: string | undefined = undefined, params: Dict = {}) {
        listenKey = listenKey || this.Options.listenKey;
        if (!listenKey) throw new Error('A listenKey is required, either as an argument or in this.Options.listenKey');
        return await this.privateSpotRequest('v3/userDataStream', { listenKey, ...params }, 'PUT');
    }

    async spotCloseDataStream(listenKey: string | undefined = undefined, params: Dict = {}) {
        listenKey = listenKey || this.Options.listenKey;
        if (!listenKey) throw new Error('A listenKey is required, either as an argument or in this.Options.listenKey');
        return await this.privateSpotRequest('v3/userDataStream', { listenKey, ...params }, 'DELETE');
    }

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

    // /**
    //  * Queries the futures API by default
    //  * @param {string} url - the signed api endpoint
    //  * @param {object} data - the data to send
    //  * @param {object} flags - type of request, authentication method and endpoint url
    //  */
    // promiseRequest(url, data = {}, flags = {}) {
    //     return await this.promiseRequest(url, data, flags);
    // }

    // /**
    // * Queries the signed api
    // * @param {string} url - the signed api endpoint
    // * @param {object} data - the data to send
    //
    // * @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(url, data, callback, method: HttpMethod = '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(symbol: string) {
        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
    * @return {promise or undefined} - omitting the callback returns a promise
    */
    async lending(params: Dict = {}) {
        return await this.privateSpotRequest('v1/lending/union/account', params);
    }

    //** Futures methods */
    async futuresPing(params: Dict = {}) {
        return await this.publicFuturesRequest('v1/ping', params);
    }

    async futuresTime(params: Dict = {}) {
        return await this.publicFuturesRequest('v1/time', params).then(r => r.serverTime);
    }
    /**
     * @see https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Exchange-Information
     * @returns
     */
    async futuresExchangeInfo(params: Dict = {}) {
        return await this.publicFuturesRequest('v1/exchangeInfo', params);
    }

    /**
     * Get the symbol configuration
     * @See https://developers.binance.com/docs/derivatives/usds-margined-futures/account/rest-api/Symbol-Config
     * @param params
     * @returns
     */
    async futuresSymbolConfig(params: Dict = {}): Promise<SymbolConfig[]> {
        return await this.privateFuturesRequest('v1/symbolConfig', params);
    }

    /**
     * @see https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Symbol-Price-Ticker-v2
     *
     */
    async futuresPrices(symbol?: string, params: Dict = {}): Promise<{ [key: string]: number }> {
        if (symbol) params.symbol = symbol;
        const data = await this.publicFuturesRequest('v2/ticker/price', params);
        return this.priceData(data);
    }

    /**
    * @see https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/24hr-Ticker-Price-Change-Statistics
    */
    async futuresDaily(symbol?: string, params: Dict = {}): Promise<DailyStats | DailyStats[]> {
        if (symbol) params.symbol = symbol;
        return await this.publicFuturesRequest('v1/ticker/24hr', params);
    }

    /**
     * @see https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Open-Interest
     */
    async futuresOpenInterest(symbol: string, params: Dict = {}): Promise<OpenInterest> {
        params.symbol = symbol;
        return await this.publicFuturesRequest('v1/openInterest', params);
    }

    /**
     * @see https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Kline-Candlestick-Data
    */
    async futuresCandles(symbol: string, interval: Interval = "30m", params: Dict = {}): Promise<Candle[]> {
        params.symbol = symbol;
        params.interval = interval;
        const res = await this.publicFuturesRequest('v1/klines', params);
        return this.parseCandles(res);
    }

    /**
     * @see https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Kline-Candlestick-Data
    */
    async futuresCandlesticks(symbol: string, interval: Interval = "30m", params: Dict = {}): Promise<Candle[]> {
        return await this.futuresCandles(symbol, interval, params); // make name consistent with spot
    }

    /**
     * @see https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Mark-Price
     */
    async futuresMarkPrice(symbol?: string, params: Dict = {}): Promise<PremiumIndex | PremiumIndex[]> {
        if (symbol) params.symbol = symbol;
        return await this.publicFuturesRequest('v1/premiumIndex', params);
    }

    /**
     * @see https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Recent-Trades-List
     * @param symbol symbol if the market
     * @param params extra parameters to be sent in the request
     * @returns
     */
    async futuresTrades(symbol: string, params: Dict = {}): Promise<Trade[]> {
        params.symbol = symbol;
        return await this.publicFuturesRequest('v1/trades', params);
    }

    /**
     * @see https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Old-Trades-Lookup
     * @param symbol symbol if the market
     * @param param
     * @returns
     */
    async futuresHistoricalTrades(symbol: string, params: Dict = {}): Promise<Trade[]> {
        params.symbol = symbol;
        return await this.publicFuturesRequest('v1/historicalTrades', params);
    }

    /**
     * @see https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Compressed-Aggregate-Trades-List
    */
    async futuresAggTrades(symbol: string, params: Dict = {}): Promise<AggregatedTrade[]> {
        params.symbol = symbol;
        return await this.publicFuturesRequest('v1/aggTrades', params);
    }

    /**
     * @see https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Users-Force-Orders
     * @param params extra parameters to be sent in the request
     * @returns
     */
    async futuresForceOrders(params: Dict = {}): Promise<Order[]> {
        return await this.privateFuturesRequest('v1/forceOrders', params);
    }
    /**
     * @see https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Position-ADL-Quantile-Estimation
     * @param params extra parameters to be sent in the request
     * @returns
     */
    async futuresDeleverageQuantile(params: Dict = {}) {
        return await this.privateFuturesRequest('v1/adlQuantile', params);
    }

    /**
     * @see https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Account-Trade-List
     * @param params extra parameters to be sent in the request
     * @returns
     */
    async futuresUserTrades(symbol: string, params: Dict = {}): Promise<FuturesUserTrade[]> {
        params.symbol = symbol;
        return await this.privateFuturesRequest('v1/userTrades', params);
    }

    async futuresGetDataStream(params: Dict = {}) {
        //A User Data Stream listenKey is valid for 60 minutes after creation. setInterval
        return await this.privateFuturesRequest('v1/listenKey', params, 'POST');
    }

    async futuresKeepDataStream(params: Dict = {}) {
        return await this.privateFuturesRequest('v1/listenKey', params, 'PUT');
    }

    async futuresCloseDataStream(params: Dict = {}) {
        return await this.privateFuturesRequest('v1/listenKey', params, 'DELETE');
    }

    /**
    * Get the account position risk (v3)
    * @see https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Position-Information-V3
    * @see https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Position-Information-V2
    * @return {promise or undefined} - omitting the callback returns a promise
    */
    async futuresPositionRisk(params: Dict = {}, useV2 = false): Promise<PositionRisk[]> {
        const endpoint = useV2 ? 'v2/positionRisk' : 'v3/positionRisk';
        return await this.privateFuturesRequest(endpoint, params);
    }

    /**
    * Get the account position risk (v2)
    * @see https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Position-Information-V3
    * @see https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Position-Information-V2
    * @return {promise or undefined} - omitting the callback returns a promise
    */
    async futuresPositionRiskV2(params: Dict = {}): Promise<PositionRisk[]> {
        return this.futuresPositionRisk(params, true);
    }

    /**
     * @see https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Get-Funding-Rate-Info
     * @param symbol symbol if the market
     * @param params extra parameters to be sent in the request
     * @returns
     */
    async futuresFundingRate(symbol: string, params: Dict = {}): Promise<FundingRate[]> {
        params.symbol = symbol;
        return await this.publicFuturesRequest('v1/fundingRate', params);
    }

    /**
     * @see https://developers.binance.com/docs/derivatives/usds-margined-futures/account/rest-api/Notional-and-Leverage-Brackets#http-request
     * @param symbol symbol if the market
     * @param params extra parameters to be sent in the request
     * @returns
     */
    async futuresLeverageBracket(symbol?: string, params: Dict = {}) {
        if (symbol) params.symbol = symbol;
        return await this.privateFuturesRequest('v1/leverageBracket', params);
    }

    /**
     * @see https://developers.binance.com/docs/derivatives/usds-margined-futures/account/rest-api/Futures-Trading-Quantitative-Rules-Indicators#http-request
     * @param symbol symbol if the market
     * @param params extra parameters to be sent in the request
     * @returns
     */
    async futuresTradingStatus(symbol?: string, params: Dict = {}) {
        if (symbol) params.symbol = symbol;
        return await this.privateFuturesRequest('v1/apiTradingStatus', params);
    }

    /**
     * @see https://developers.binance.com/docs/derivatives/usds-margined-futures/account/rest-api/User-Commission-Rate#http-request
     * @param symbol symbol if the market
     * @param params extra parameters to be sent in the request
     * @returns
     */
    async futuresCommissionRate(symbol?: string, params: Dict = {}) {
        if (symbol) params.symbol = symbol;
        return await this.privateFuturesRequest('v1/commissionRate', params);
    }

    // leverage 1 to 125
    /**
     * @see https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Change-Initial-Leverage
     */
    async futuresLeverage(symbol: string, leverage: number, params: Dict = {}) {
        params.symbol = symbol;
        params.leverage = leverage;
        return await this.privateFuturesRequest('v1/leverage', params, 'POST');
    }

    // ISOLATED, CROSSED
    /**
     * @see https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Change-Margin-Type
     * @param symbol symbol if the market
     * @param marginType
     * @param params extra parameters to be sent in the request
     * @returns
     */
    async futuresMarginType(symbol: string, marginType: string, params: Dict = {}) {
        params.symbol = symbol;
        params.marginType = marginType;
        return await this.privateFuturesRequest('v1/marginType', params, 'POST');
    }

    // type: 1: Add postion margin，2: Reduce postion margin
    async futuresPositionMargin(symbol: string, amount: number, type = 1, params: Dict = {}) {
        params.symbol = symbol;
        params.amount = amount;
        params.type = type;
        return await this.privateFuturesRequest('v1/positionMargin', params, 'POST');
    }

    async futuresPositionMarginHistory(symbol: string, params: Dict = {}) {
        params.symbol = symbol;
        return await this.privateFuturesRequest('v1/positionMargin/history', params);
    }

    /**
     * @see https://developers.binance.com/docs/derivatives/usds-margined-futures/account/rest-api/Get-Income-History
     */
    async futuresIncome(params: Dict = {}) {
        return await this.privateFuturesRequest('v1/income', params);
    }

    /**
     * @see https://developers.binance.com/docs/derivatives/usds-margined-futures/account/rest-api/Futures-Account-Balance-V2
    */
    async futuresBalance(params: Dict = {}): Promise<FuturesBalance[]> {
        return await this.privateFuturesRequest('v2/balance', params);
    }

    /**
     * @see https://developers.binance.com/docs/derivatives/usds-margined-futures/account/rest-api/Account-Information-V3
     * @param params extra parameters to be sent in the request
     * @returns
     */
    async futuresAccount(params: Dict = {}): Promise<FuturesAccountInfo> {
        return await this.privateFuturesRequest('v3/account', params);
    }

    /**
     * @see https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Order-Book
     * @param symbol symbol if the market
     * @param params extra parameters to be sent in the request
     * @returns
     */
    async futuresDepth(symbol: string, params: Dict = {}): Promise<OrderBook> {
        params.symbol = symbol;
        const res = await this.publicFuturesRequest('v1/depth', params);
        return this.parseOrderBook(res, symbol);
    }

    /**
     * @see https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Symbol-Order-Book-Ticker
     */
    async futuresQuote(symbol?: string, params: Dict = {}) {
        if (symbol) params.symbol = symbol;
        const data = await this.publicFuturesRequest('v1/ticker/bookTicker', params);
        return this.bookPriceData(data);
    }

    /**
     * @see https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Symbol-Order-Book-Ticker
     */
    async futuresBookTicker(symbol?: string, params: Dict = {}) {
        return await this.futuresQuote(symbol, params);
    }

    /**
     * @see https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/New-Order
     * @param symbol symbol if the market
     * @param quantity
     * @param price
     * @param params extra parameters to be sent in the request
     * @returns
     */
    async futuresBuy(symbol: string, quantity: number, price: number, params: Dict = {}) {
        return await this.futuresOrder('LIMIT', 'BUY', symbol, quantity, price, params);
    }

    /**
     * @see https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/New-Order
     * @param symbol symbol if the market
     * @param quantity
     * @param price
     * @param params extra parameters to be sent in the request
     * @returns
     */
    async futuresSell(symbol: string, quantity: number, price: number, params: Dict = {}) {
        return await this.futuresOrder('LIMIT', 'SELL', symbol, quantity, price, params);
    }

    /**
     * @see https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/New-Order
     * @param symbol symbol if the market
     * @param quantity
     * @param price
     * @param params extra parameters to be sent in the request
     * @returns
     */
    async futuresMarketBuy(symbol: string, quantity: number, params: Dict = {}) {
        return await this.futuresOrder('MARKET', 'BUY', symbol, quantity, undefined, params);
    }

    /**
     * @description futures limit order
     * @see https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/New-Order
     * @param side
     * @param symbol
     * @param quantity
     * @param price
     * @param params extra parameters to be sent in the request
     * @returns
     */
    async futuresLimitOrder(side: OrderSide, symbol: string, quantity: number, price: number, params: Dict = {}) {
        return await this.futuresOrder('LIMIT', side, symbol, quantity, price, params);
    }

    /**
     * @see https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/New-Order
     * @param symbol symbol if the market
     * @param quantity
     * @param price
     * @param params extra parameters to be sent in the request
     * @returns
     */
    async futuresMarketSell(symbol: string, quantity: number, params: Dict = {}) {
        return await this.futuresOrder('MARKET', 'SELL', symbol, quantity, undefined, params);
    }

    /**
     * @see https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Place-Multiple-Orders
     */
    async futuresMultipleOrders(orders: Dict[] = []): Promise<FuturesOrder[]> {
        for (let i = 0; i < orders.length; i++) {
            if (!orders[i].newClientOrderId) {
                orders[i].newClientOrderId = this.CONTRACT_PREFIX + this.uuid22();
            }
        }
        const params = { batchOrders: JSON.stringify(orders) };
        return await this.privateFuturesRequest('v1/batchOrders', params, 'POST');
    }

    /**
     * @see https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Cancel-Multiple-Orders
     */
    async futuresCancelMultipleOrders(symbol: string, params: Dict = {}): Promise<(FuturesOrder | Response)[]> {
        return await this.privateFuturesRequest('v1/batchOrders', this.extend({ 'symbol': symbol }, params), 'DELETE');
    }

    // futuresOrder, // side symbol quantity [price] [params]

    /**
     * @see https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Query-Order
     * @see https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Query-Algo-Order
     * @param symbol symbol if the market
     * @param params extra parameters to be sent in the request
     * @param params.conditional set to true to query algo order
     * @returns
     */
    async futuresOrderStatus(symbol: string, params: Dict = {}): Promise<FuturesOrder> { // Either orderId or origClientOrderId must be sent
        params.symbol = symbol;
        if ('conditional' in params) {
            delete params.conditional;
            return await this.privateFuturesRequest('v1/algoOrder', params);
        }
        return await this.privateFuturesRequest('v1/order', params);
    }

    /**
     * @see https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Query-Algo-Order
     * @param symbol symbol if the market
     * @param params extra parameters to be sent in the request
     * @returns
     */
    async futuresAlgoOrderStatus(symbol: string, params: Dict = {}): Promise<FuturesAlgoOrder> { // Either orderId or origClientOrderId must be sent
        params.symbol = symbol;
        return await this.privateFuturesRequest('v1/algoOrder', params);
    }

    /**
     * @see https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Cancel-Order
     * @see https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Cancel-Algo-Order
     * @param symbol symbol if the market
     * @param orderId
     * @param params.conditional set to true to cancel algo order
     * @param params extra parameters to be sent in the request
     * @returns
     */
    async futuresCancel(symbol: string, orderId?: number | string, params: Dict = {}): Promise<CancelOrder> { // Either orderId or origClientOrderId must be sent
        params.symbol = symbol;
        if ('conditional' in params) {
            delete params.conditional;
            if (orderId) params.algoid = orderId;
            return await this.privateFuturesRequest('v1/algoOrder', params, 'DELETE');
        }
        if (orderId) params.orderId = orderId;
        return await this.privateFuturesRequest('v1/order', params, 'DELETE');
    }

    /**
     * @see https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Cancel-Algo-Order
     * @param symbol symbol if the market
     * @param orderId
     * @param params extra parameters to be sent in the request
     * @returns
     */
    async futuresCancelAlgoOrder(symbol: string, orderId?: number | string, params: Dict = {}): Promise<CancelAlgoOrder> { // Either orderId or origClientOrderId must be sent
        params.symbol = symbol;
        if (orderId) params.algoid = orderId;
        return await this.privateFuturesRequest('v1/algoOrder', params, 'DELETE');
    }

    /**
     * @see https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Cancel-All-Open-Orders
     * @param symbol symbol if the market
     * @param params extra parameters to be sent in the request
     * @param params.conditional set to true to cancel algo order
     * @returns
     */
    async futuresCancelAll(symbol: string, params: Dict = {}): Promise<Response> {
        params.symbol = symbol;
        if ('conditional' in params) {
            delete params.conditional;
            return await this.privateFuturesRequest('v1/algoOpenOrders', params, 'DELETE');
        }
        return await this.privateFuturesRequest('v1/allOpenOrders', params, 'DELETE');
    }

    /**
     * @see https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Cancel-All-Algo-Open-Orders
     * @param symbol symbol if the market
     * @param params extra parameters to be sent in the request
     * @returns
     */
    async futuresCancelAllAlgo(symbol: string, params: Dict = {}): Promise<Response> {
        params.symbol = symbol;
        return await this.privateFuturesRequest('v1/algoOpenOrders', params, 'DELETE');
    }

    /**
     *
     * @param symbol symbol if the market
     * @param countdownTime
     * @param params extra parameters to be sent in the request
     * @returns
     */
    async futuresCountdownCancelAll(symbol, countdownTime = 0, params: Dict = {}) {
        params.symbol = symbol;
        params.countdownTime = countdownTime;
        return await this.privateFuturesRequest('v1/countdownCancelAll', params, 'POST');
    }

    /**
     * @see https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Current-All-Open-Orders
     * @param symbol symbol if the market
     * @param params extra parameters to be sent in the request
     * @param params.conditional set to true to query algo open orders
     * @returns
     */
    async futuresOpenOrders(symbol?: string, params: Dict = {}): Promise<FuturesOrder[]> {
        if (symbol) params.symbol = symbol;
        if ('conditional' in params) {
            delete params.conditional;
            return await this.privateFuturesRequest('v1/algoOpenOrders', params);
        }
        return await this.privateFuturesRequest('v1/openOrders', params);
    }

    /**
     * @see https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Current-All-Algo-Open-Orders
     * @param symbol symbol if the market
     * @param params extra parameters to be sent in the request
     * @returns
     */
    async futuresOpenAlgoOrders(symbol?: string, params: Dict = {}): Promise<FuturesOrder[]> {
        if (symbol) params.symbol = symbol;
        return await this.privateFuturesRequest('v1/openAlgoOrders', params);
    }

    /**
     * @see https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/All-Orders
     * @param symbol symbol if the market
     * @param params extra parameters to be sent in the request
     * @returns
     */
    async futuresAllOrders(symbol?: string, params: Dict = {}): Promise<FuturesOrder[]> { // Get all account orders; active, canceled, or filled.
        if (symbol) params.symbol = symbol;
        return await this.privateFuturesRequest('v1/allOrders', params);
    }

    /**
     * @see https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Query-All-Algo-Orders
     * @param symbol symbol if the market
     * @param params extra parameters to be sent in the request
     * @returns
     */
    async futuresAllAlgoOrders(symbol?: string, params: Dict = {}): Promise<FuturesAlgoOrder[]> { // Get all account orders; active, canceled, or filled.
        if (symbol) params.symbol = symbol;
        return await this.privateFuturesRequest('v1/allAlgoOrders', params);
    }

    /**
     *
     * @param params extra parameters to be sent in the request
     * @returns
     */
    async futuresPositionSideDual(params: Dict = {}) {
        return await this.privateFuturesRequest('v1/positionSide/dual', params);
    }

    /**
     *
     * @param dualSidePosition
     * @param params extra parameters to be sent in the request
     * @returns
     */
    async futuresChangePositionSideDual(dualSidePosition, params: Dict = {}) {
        params.dualSidePosition = dualSidePosition;
        return await this.privateFuturesRequest('v1/positionSide/dual', params, 'POST');
    }

    /**
     *
     * @param symbol symbol if the market
     * @param params extra parameters to be sent in the request
     * @returns
     */
    async futuresTransferAsset(asset: string, amount: number, type: string, params: Dict = {}) {
        params = Object.assign({ asset, amount, type });
        return await this.privateSpotRequest('v1/futures/transfer', params, 'POST');
    }

    /**
     * @see https://developers.binance.com/docs/derivatives/futures-data/market-data
     * @param symbol
     * @param params
     * @returns
     */
    async futuresHistDataLink(symbol?: string, params: Dict = {}) {
        if (symbol) params.symbol = symbol;
        return await this.privateSpotRequest('v1/futuresHistDataId', params);
    }

    /**
     * @see https://developers.binance.com/docs/derivatives/usds-margined-futures/account/rest-api/Get-Futures-Trade-Download-Link-by-Id
     * @param downloadId
     * @returns
     */
    async futuresTradeDownloadLink(downloadId) {
        return await this.privateFuturesRequest('v1/trade/asyn/id', { downloadId });
    }

    // futures websockets support: ticker bookTicker miniTicker aggTrade markPrice
    /* TODO: https://binance-docs.github.io/apidocs/futures/en/#change-log
    Cancel multiple orders DELETE /fapi/v1/batchOrders
    New Future Account Transfer POST https://api.binance.com/sapi/v1/futures/transfer
    Get Postion Margin Change History (TRADE)

    wss://fstream.binance.com/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
    Liquidation Order Streams for specific symbol: <symbol>@forceOrder
    Chart data (250ms): <symbol>@kline_<interval>
    SUBSCRIBE, UNSUBSCRIBE, LIST_SUBSCRIPTIONS, SET_PROPERTY, GET_PROPERTY
    Live Subscribing/Unsubscribing to streams: requires sending futures subscription id when connecting
    futuresSubscriptions { "method": "LIST_SUBSCRIPTIONS", "id": 1 }
    futuresUnsubscribe { "method": "UNSUBSCRIBE", "params": [ "btcusdt@depth" ], "id": 1 }
    futures depthCache
    */

    /*
    const futuresOrder = (side, symbol, quantity, price = 0, flags: Dict = {}, 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 this.options.log('futuresOrder error:', error);
            }
            if (callback) return callback(error, response);
            else return this.options.log(`futuresOrder ${side} (${symbol},${quantity},${price})`, response);
        }, 'POST');
    };*/

    //** Delivery methods */

    async deliveryPing(params: Dict = {}) {
        return await this.publicDeliveryRequest('v1/ping', params);
    }

    async deliveryTime(params: Dict = {}) {
        return await this.publicDeliveryRequest('v1/time', params);
    }

    async deliveryExchangeInfo(params: Dict = {}) {
        return await this.publicDeliveryRequest('v1/exchangeInfo', params);
    }

    async deliveryPrices(params: Dict = {}) {
        const data = await this.publicDeliveryRequest('v1/ticker/price', params);
        return data.reduce((out, i) => ((out[i.symbol] = i.price), out), {});
    }

    async deliveryDaily(symbol?: string, params: Dict = {}) {
        if (symbol) params.symbol = symbol;
        const data = await this.publicDeliveryRequest('v1/ticker/24hr', params);
        return symbol ? data : data.reduce((out, i) => ((out[i.symbol] = i), out), {});
    }

    async deliveryOpenInterest(symbol: string, params: Dict = {}) {
        params.symbol = symbol;
        const res = await this.publicDeliveryRequest('v1/openInterest', params);
        return res;
    }

    /**
     * @see https://developers.binance.com/docs/derivatives/coin-margined-futures/market-data/rest-api/Kline-Candlestick-Data
     * @param symbol 
     * @param interval
     * @param params
     * @returns
     */
    async deliveryCandles(symbol: string, interval: Interval = "30m", params: Dict = {}): Promise<Candle[]> {
        params.symbol = symbol;
        params.interval = interval;
        const res = await this.publicDeliveryRequest('v1/klines', params);
        return this.parseCandles(res);
    }

    async deliveryContinuousKlines(pair: string, contractType = "CURRENT_QUARTER", interval: Interval = "30m", params: Dict = {}) {
        params.pair = pair;
        params.interval = interval;
        params.contractType = contractType;
        return await this.publicDeliveryRequest('v1/continuousKlines', params);
    }

    async deliveryIndexKlines(pair: string, interval: Interval = "30m", params: Dict = {}) {
        params.pair = pair;
        params.interval = interval;
        return await this.publicDeliveryRequest('v1/indexPriceKlines', params);
    }

    async deliveryMarkPriceKlines(symbol: string, interval: Interval = "30m", params: Dict = {}) {
        params.symbol = symbol;
        params.interval = interval;
        return await this.publicDeliveryRequest('v1/markPriceKlines', params);
    }

    async deliveryMarkPrice(symbol?: string, params: Dict = {}) {
        if (symbol) params.symbol = symbol;
        return await this.publicDeliveryRequest('v1/premiumIndex', params);
    }

    async deliveryTrades(symbol: string, params: Dict = {}) {
        params.symbol = symbol;
        return await this.publicDeliveryRequest('v1/trades', params);
    }

    async deliveryHistoricalTrades(symbol: string, params: Dict = {}) {
        params.symbol = symbol;
        return await this.publicDeliveryRequest('v1/historicalTrades', params);
    }

    async deliveryAggTrades(symbol: string, params: Dict = {}) {
        params.symbol = symbol;
        return await this.publicDeliveryRequest('v1/aggTrades', params);
    }

    async deliveryUserTrades(symbol: string, params: Dict = {}) {
        params.symbol = symbol;
        return await this.privateDeliveryRequest('v1/userTrades', params);
    }

    async deliveryCommissionRate(symbol: string, params: Dict = {}) {
        if (symbol) params.symbol = symbol;
        return await this.privateDeliveryRequest('v1/commissionRate', params);
    }

    async deliveryGetDataStream(params: Dict = {}) {
        //A User Data Stream listenKey is valid for 60 minutes after creation. setInterval
        return await this.privateDeliveryRequest('v1/listenKey', params, 'POST');
    }

    async deliveryKeepDataStream(params: Dict = {}) {
        return await this.privateDeliveryRequest('v1/listenKey', params, 'PUT');
    }

    async deliveryCloseDataStream(params: Dict = {}) {
        return await this.privateDeliveryRequest('v1/listenKey', params, 'DELETE');
    }

    async deliveryLiquidationOrders(symbol?: string, params: Dict = {}) {
        if (symbol) params.symbol = symbol;
        return await this.publicDeliveryRequest('v1/allForceOrders', params);
    }

    async deliveryPositionRisk(params: Dict = {}) {
        return await this.privateDeliveryRequest('v1/positionRisk', params);
    }

    async deliveryLeverageBracket(symbol?: string, params: Dict = {}) {
        if (symbol) params.symbol = symbol;
        return await this.privateDeliveryRequest('v1/leverageBracket', params);
    }

    async deliveryLeverageBracketSymbols(symbol?: string, params: Dict = {}) {
        if (symbol) params.symbol = symbol;
        return await this.privateDeliveryRequest('v2/leverageBracket', params);
    }

    // leverage 1 to 125
    async deliveryLeverage(symbol: string, leverage: number, params: Dict = {}) {
        params.symbol = symbol;
        params.leverage = leverage;
        return await this.privateDeliveryRequest('v1/leverage', params, 'POST');
    }

    // ISOLATED, CROSSED
    async deliveryMarginType(symbol: string, marginType: string, params: Dict = {}) {
        params.symbol = symbol;
        params.marginType = marginType;
        return await this.privateDeliveryRequest('v1/marginType', params, 'POST');
    }

    // type: 1: Add postion margin，2: Reduce postion margin
    async deliveryPositionMargin(symbol: string, amount: number, type = 1, params: Dict = {}) {
        params.symbol = symbol;
        params.amount = amount;
        params.type = type;
        return await this.privateDeliveryRequest('v1/positionMargin', params, 'POST');
    }

    async deliveryPositionMarginHistory(symbol: string, params: Dict = {}) {
        params.symbol = symbol;
        return await this.privateDeliveryRequest('v1/positionMargin/history', params);
    }

    async deliveryIncome(params: Dict = {}) {
        return await this.privateDeliveryRequest('v1/income', params);
    }

    async deliveryBalance(params: Dict = {}) {
        return await this.privateDeliveryRequest('v1/balance', params);
    }

    async deliveryAccount(params: Dict = {}) {
        return await this.privateDeliveryRequest('v1/account', params);
    }

    async deliveryDepth(symbol: string, params: Dict = {}): Promise<OrderBook> {
        params.symbol = symbol;
        const res = await this.publicDeliveryRequest('v1/depth', params);
        return this.parseOrderBook(res, symbol);
    }

    async deliveryQuote(symbol?: string, params: Dict = {}) {
        if (symbol) params.symbol = symbol;
        //let data = await this.promiseRequest( 'v1/ticker/bookTicker', params, {base:dapi} );
        //return data.reduce((out, i) => ((out[i.symbol] = i), out), {}),
        const data = await this.publicDeliveryRequest('v1/ticker/bookTicker', params);
        return symbol ? data : data.reduce((out, i) => ((out[i.symbol] = i), out), {});
    }

    async deliveryBuy(symbol: string, quantity: number, price: number, params: Dict = {}) {
        return await this.deliveryOrder('LIMIT', 'BUY', symbol, quantity, price, params);
    }

    async deliverySell(symbol: string, quantity: number, price: number, params: Dict = {}) {
        return await this.deliveryOrder('LIMIT', 'SELL', symbol, quantity, price, params);
    }

    async deliveryMarketBuy(symbol: string, quantity: number, params: Dict = {}) {
        return await this.deliveryOrder('MARKET', 'BUY', symbol, quantity, undefined, params);
    }

    async deliveryMarketSell(symbol: string, quantity: number, params: Dict = {}) {
        return await this.deliveryOrder('MARKET', 'SELL', symbol, quantity, undefined, params);
    }

    // deliveryOrder, // side symbol quantity [price] [params]

    async deliveryOrderStatus(symbol: string, params: Dict = {}) { // Either orderId or origClientOrderId must be sent
        params.symbol = symbol;
        return await this.privateDeliveryRequest('v1/order', params);
    }

    async deliveryCancel(symbol: string, params: Dict = {}) { // Either orderId or origClientOrderId must be sent
        params.symbol = symbol;
        return await this.privateDeliveryRequest('v1/order', params, 'DELETE');
    }

    async deliveryCancelAll(symbol: string, params: Dict = {}) {
        params.symbol = symbol;
        return await this.privateDeliveryRequest('v1/allOpenOrders', params, 'DELETE');
    }

    async deliveryCountdownCancelAll(symbol: string, countdownTime = 0, params: Dict = {}) {
        params.symbol = symbol;
        params.countdownTime = countdownTime;
        return await this.privateDeliveryRequest('v1/countdownCancelAll', params, 'POST');
    }

    async deliveryOpenOrders(symbol?: string, params: Dict = {}) {
        if (symbol) params.symbol = symbol;
        return await this.privateDeliveryRequest('v1/openOrders', params);
    }

    async deliveryAllOrders(symbol?: string, params: Dict = {}) { // Get all account orders; active, canceled, or filled.
        if (symbol) params.symbol = symbol;
        return await this.privateDeliveryRequest('v1/allOrders', params);
    }

    async deliveryPositionSideDual(params: Dict = {}) {
        return await this.privateDeliveryRequest('v1/positionSide/dual', params);
    }

    async deliveryChangePositionSideDual(dualSidePosition, params: Dict = {}) {
        params.dualSidePosition = dualSidePosition;
        return await this.privateDeliveryRequest('v1/positionSide/dual', params, 'POST');
    }

    //** Margin methods */

    /**
     * Creates an order
     * @see https://developers.binance.com/docs/margin_trading/trade/Margin-Account-New-Order
     * @param {string} side - BUY or SELL
     * @param {string} symbol - the symbol to buy
     * @param {numeric} quantity - the quantity required
     * @param {numeric} price - the price to pay for each unit
     * @param {object} params - additional buy order flags
     * @param {string} isIsolated - the isolate margin option
     * @return {undefined}
     */
    async mgOrder(type: OrderType, side: string, symbol: string, quantity: number, price: number, params: Dict = {}, isIsolated = 'FALSE') {
        return await this.marginOrder(type, side, symbol, quantity, price, { ...params, isIsolated });
    }

    /**
     * Creates a buy order
     * @see https://developers.binance.com/docs/margin_trading/trade/Margin-Account-New-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} params - additional buy order flags
     * @param {string} isIsolated - the isolate margin option
     * @return {undefined}
     */
    async mgBuy(symbol: string, quantity: number, price: number, params: Dict = {}, isIsolated = 'FALSE') {
        return await this.marginOrder('LIMIT', 'BUY', symbol, quantity, price, { ...params, isIsolated });
    }

    /**
     * Creates a sell order
     * @see https://developers.binance.com/docs/margin_trading/trade/Margin-Account-New-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 {string} isIsolated - the isolate margin option
     * @return {undefined}
     */
    async mgSell(symbol: string, quantity: number, price: number, flags: Dict = {}, isIsolated = 'FALSE') {
        return await this.marginOrder('LIMIT', 'SELL', symbol, quantity, price, { ...flags, isIsolated });
    }

    /**
     * Creates a market buy order
     * @see https://developers.binance.com/docs/margin_trading/trade/Margin-Account-New-Order
     * @param {string} symbol - the symbol to buy
     * @param {numeric} quantity - the quantity required
     * @param {object} flags - additional buy order flags
     * @param {string} isIsolated - the isolate margin option
     * @return {undefined}
     */
    async mgMarketBuy(symbol: string, quantity: number, params: Dict = {}, isIsolated = 'FALSE') {
        return await this.marginOrder('MARKET', 'BUY', symbol, quantity, 0, { ...params, isIsolated });
    }

    /**
     * Creates a market sell order
     * @see https://developers.binance.com/docs/margin_trading/trade/Margin-Account-New-Order
     * @param {string} symbol - the symbol to sell
     * @param {numeric} quantity - the quantity required
     * @param {object} flags - additional sell order flags
     * @param {string} isIsolated - the isolate margin option
     * @return {undefined}
     */
    async mgMarketSell(symbol: string, quantity: number, params: Dict = {}, isIsolated = 'FALSE') {
        return await this.marginOrder('MARKET', 'SELL', symbol, quantity, 0, { ...params, isIsolated });
    }

    /**
     * Cancels an order
     * @param {string} symbol - the symbol to cancel
     * @param {string} orderid - the orderid to cancel
     * @return {undefined}
     */
    async mgCancel(symbol: string, orderid: number | string, isIsolated = 'FALSE'): Promise<CancelOrder> {
        return await this.privateSapiRequest('v1/margin/order', { symbol: symbol, orderId: orderid, isIsolated }, 'DELETE');
    }

    /**
    * Gets all order of a given symbol
    * @param {string} symbol - the symbol
    * @param {object} options - additional options
    * @return {promise or undefined} - omitting the callback returns a promise
    */
    async mgAllOrders(symbol: string, params: Dict = {}): Promise<Order[]> {
        const parameters = Object.assign({ symbol: symbol }, params);
        return await this.privateSapiRequest('v1/margin/allOrders', parameters);
    }

    /**
     * Gets the status of an order
     * @param {string} symbol - the symbol to check
     * @param {string} orderid - the orderid to check
     * @param {object} flags - any additional flags
     * @return {undefined}
     */
    async mgOrderStatus(symbol: string, orderid: number | string, flags = {}): Promise<Order> {
        const parameters = Object.assign({ symbol: symbol, orderId: orderid }, flags);
        return await this.privateSapiRequest('v1/margin/order', parameters);
    }

    /**
     * Gets open orders
     * @param {string} symbol - the symbol to get
     * @return {undefined}
     */
    async mgOpenOrders(symbol?: string, params: Dict = {}): Promise<Order[]> {
        if (symbol) params.symbol = symbol;
        return await this.privateSapiRequest('v1/margin/openOrders', params);
    }

    /**
     * Cancels all order of a given symbol
     * @param {string} symbol - the symbol to cancel all orders for
     * @return {undefined}
     */
    async mgCancelOrders(symbol: string, params: Dict = {}) {
        // signedRequest(this.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;
        //         this.options.log('cancel order: ' + obj.side + ' ' + symbol + ' ' + quantity + ' @ ' + obj.price + ' #' + obj.orderId);
        //         signedRequest(this.sapi + 'v1/margin/order', { symbol: symbol, orderId: obj.orderId }, function (error, data) {
        //             if (callback) return callback.call(this, error, data, symbol);
        //         }, 'DELETE');
        //     }
        // }); // to do check this
        return await this.privateSapiRequest('v1/margin/openOrders', this.extend({ symbol: symbol }, params), 'DELETE');
    }

    /**
     * Transfer from main account to margin account
     * @param {string} asset - the asset
     * @param {number} amount - the asset
     * @param {object} options - additional options
     * @return {undefined}
     */
    async mgTransferMainToMargin(asset: string, amount: number, params: Dict = {}) {
        params = this.extend({ asset: asset, amount: amount, type: 1 }, params);
        return await this.privateSapiRequest('v1/margin/transfer', params, 'POST');
    }

    /**
     * Transfer from margin account to main account
     * @param {string} asset - the asset
     * @param {number} amount - the asset
     * @return {undefined}
     */
    async mgTransferMarginToMain(asset: string, amount: number, params: Dict = {}) {
        const parameters = Object.assign({ asset: asset, amount: amount, type: 2 });
        return await this.privateSapiRequest('v1/margin/transfer', this.extend(parameters, params), 'POST');
    }
    // /**
    // * Universal Transfer requires API permissions enabled
    // * @param {string} type - ENUM , example MAIN_UMFUTURE for SPOT to USDT futures, see https://binance-docs.github.io/apidocs/spot/en/#user-universal-transfer
    // * @param {string} asset - the asset - example :USDT
    // * @param {number} amount - the callback function
    // (optionnal)
    // * @return {promise}
    // */
    // universalTransfer: (type, asset, amount, callback) =>
    //     universalTransfer(type, asset, amount, callback),

    /**
    * Get trades for a given symbol - margin account
    * @param {string} symbol - the symbol
    * @param {object} options - additional options
    * @return {promise or undefined} - omitting the callback returns a promise
    */
    async mgTrades(symbol: string, params: Dict = {}): Promise<MyTrade[]> {
        const parameters = Object.assign({ symbol: symbol }, params);
        return await this.privateSapiRequest('v1/margin/myTrades', parameters);
    }

    /**
    * Transfer from main account to delivery account
    * @param {string} asset - the asset
    * @param {number} amount - the asset
    * @param {object} options - additional options
    * @return {undefined}
    */
    async transferMainToFutures(asset: string, amount: number) {
        return await this.transferBetweenMainAndFutures(asset, amount, 1);
    }

    /**
 * Transfer from delivery account to main account
 * @param {string} asset - the asset
 * @param {number} amount - the asset
 (optionnal)
 * @return {undefined}
 */
    async transferFuturesToMain(asset: string, amount: number) {
        return await this.transferBetweenMainAndFutures(asset, amount, 2);
    }

    /**
     * Transfer from main account to delivery account
     * @param {string} asset - the asset
     * @param {number} amount - the asset
     (optionnal)
     * @param {object} options - additional options
     * @return {undefined}
     */
    async transferMainToDelivery(asset: string, amount: number) {
        return await this.transferBetweenMainAndFutures(asset, amount, 3);
    }

    /**
 * Transfer from delivery account to main account
 * @param {string} asset - the asset
 * @param {number} amount - the asset
 * @return {undefined}
 */
    async transferDeliveryToMain(asset: string, amount: number) {
        return await this.transferBetweenMainAndFutures(asset, amount, 4);
    }

    /**
     * Get maximum transfer-out amount of an asset
     * @param {string} asset - the asset
     * @return {undefined}
     */
    async maxTransferable(asset: string) {
        return await this.privateSpotRequest('v1/margin/maxTransferable', { asset: asset });
    }

    /**
     * Margin account borrow/loan
     * @param {string} asset - the asset
     * @param {number} amount - the asset
     * @param {string} isIsolated - the isolated option
     * @param {string} symbol - symbol for isolated margin
     * @return {undefined}
     */
    async mgBorrow(asset: string, amount: number, isIsolated = 'FALSE', symbol?: string, params: Dict = {}) {
        const parameters = Object.assign({ asset: asset, amount: amount });
        if (isIsolated === 'TRUE' && !symbol) throw new Error('If "isIsolated" = "TRUE", "symbol" must be sent');
        const isolatedObj = isIsolated === 'TRUE' ? {
            isIsolated,
            symbol
        } : {};
        return await this.privateSapiRequest('v1/margin/loan', this.extend({ ...parameters, ...isolatedObj }, params), 'POST');
    }

    /**
     * Margin account borrow/loan
     * @param {string} asset - the asset
     * @param {object} options - additional options
     * @return {undefined}
     */
    async mgQueryLoan(asset: string, options) {
        const parameters = Object.assign({ asset: asset }, options);
        return await this.privateSapiRequest('v1/margin/loan', { ...parameters }, 'GET');
    }

    /**
     * Margin account repay
     * @param {string} asset - the asset
     * @param {object} params - additional options
     * @return {undefined}
     */
    async mgQueryRepay(asset: string, params: Dict = {}) {
        const parameters = Object.assign({ asset: asset }, params);
        return await this.privateSapiRequest('v1/margin/repay', { ...parameters }, 'GET');
    }

    /**
     * Margin account repay
     * @param {string} asset - the asset
     * @param {number} amount - the asset
     * @param {string} isIsolated - the isolated option
     * @param {string} symbol - symbol for isolated margin
     * @return {undefined}
     */
    async mgRepay(asset: string, amount: number, isIsolated = 'FALSE', symbol?: string, params: Dict = {}) {
        const parameters = Object.assign({ asset: asset, amount: amount });
        if (isIsolated === 'TRUE' && !symbol) throw new Error('If "isIsolated" = "TRUE", "symbol" must be sent');
        const isolatedObj = isIsolated === 'TRUE' ? {
            isIsolated,
            symbol
        } : {};
        return await this.privateSapiRequest('v1/margin/repay', this.extend({ ...parameters, ...isolatedObj }, params), 'POST');
    }

    /**
     * Margin account details
     * @param {boolean} isIsolated - the callback function
     * @return {undefined}
     */
    async mgAccount(isIsolated = false, params: Dict = {}) {
        let endpoint = 'v1/margin';
        endpoint += (isIsolated) ? '/isolated' : '' + '/account';
        return await this.privateSapiRequest(endpoint, params);
    }
    /**
     * Get maximum borrow amount of an asset
     * @param {string} asset - the asset
     * @return {undefined}
     */
    async maxBorrowable(asset: string, params: Dict = {}) {
        params.asset = asset;
        return await this.privateSapiRequest('v1/margin/maxBorrowable', params);
    }

    // // Futures WebSocket Functions:
    // /**
    //  * Subscribe to a single futures websocket
    //  * @param {string} url - the futures websocket endpoint
    //  * @param {function} callback - optional execution callback
    //  * @param {object} params - Optional reconnect {boolean} (whether to reconnect on disconnect), openCallback {function}, id {string}
    //  * @return {WebSocket} the websocket reference
    //  */
    // async this.futuresSubscribeSingle(url, callback: Callback, params: Dict = {}) {
    //     return this.futuresSubscribeSingle(url, callback, params);
    // }

    // /**
    //  * Subscribe to a combined futures websocket
    //  * @param {string} streams - the list of websocket endpoints to connect to
    //  * @param {function} callback - optional execution callback
    //  * @param {object} params - Optional reconnect {boolean} (whether to reconnect on disconnect), openCallback {function}, id {string}
    //  * @return {WebSocket} the websocket reference
    //  */
    // futuresSubscribe(streams, callback: Callback, params: Dict = {}) {
    //     return futuresSubscribe(streams, callback, params);
    // }

    /**
     * Returns the known futures websockets subscriptions
     * @return {array} array of futures websocket subscriptions
     */
    getFuturesSubscriptions() {
        return this.futuresSubscriptions;
    }

    // /**
    //  * Terminates a futures websocket
    //  * @param {string} endpoint - the string associated with the endpoint
    //  * @return {undefined}
    //  */
    // // futuresTerminate(endpoint) {
    // //     if (this.options.verbose) this.options.log('Futures WebSocket terminating:', endpoint);
    // //     return futuresTerminate(endpoint);
    // // }

    /**
     * Futures 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
     */
    futuresAggTradeStream(symbols: string[] | string, callback: Callback) {
        const reconnect = () => {
            if (this.Options.reconnect) this.futuresAggTradeStream(symbols, callback);
        };
        let subscription;
        const cleanCallback = data => callback(this.fAggTradeConvertData(data));
        if (Array.isArray(symbols)) {
            if (!this.isArrayUnique(symbols)) throw Error('futuresAggTradeStream: "symbols" cannot contain duplicate elements.');
            const streams = symbols.map(symbol => symbol.toLowerCase() + '@aggTrade');
            subscription = this.futuresSubscribe(streams, cleanCallback, { reconnect });
        } else {
            const symbol = symbols as string;
            subscription = this.futuresSubscribeSingle(symbol.toLowerCase() + '@aggTrade', cleanCallback, { reconnect });
        }
        return (subscription as any).url;
    }

    /**
     * Futures WebSocket mark price
     * @param {symbol} symbol name or false. can also be a callback
     * @param {function} callback - callback function
     * @param {string} speed - 1 second updates. leave blank for default 3 seconds
     * @return {string} the websocket endpoint
     */
    futuresMarkPriceStream(symbol?: string, callback = console.log, speed = '@1s') {
        const reconnect = () => {
            if (this.Options.reconnect) this.futuresMarkPriceStream(symbol, callback, speed);
        };
        const endpoint = symbol ? `${symbol.toLowerCase()}@markPrice` : '!markPrice@arr';
        const subscription = this.futuresSubscribeSingle(endpoint + speed, data => callback(this.fMarkPriceConvertData(data)), { reconnect });
        return (subscription as any).url;
    }

    /**
     * Futures WebSocket liquidations stream
     * @param {symbol} symbol name or false. can also be a callback
     * @param {function} callback - callback function
     * @return {string} the websocket endpoint
     */
    futuresLiquidationStream(symbol?: string, callback = console.log) {
        const reconnect = () => {
            if (this.Options.reconnect) this.futuresLiquidationStream(symbol, callback);
        };
        const endpoint = symbol ? `${symbol.toLowerCase()}@forceOrder` : '!forceOrder@arr';
        const subscription = this.futuresSubscribeSingle(endpoint, data => callback(this.fLiquidationConvertData(data)), { reconnect });
        return (subscription as any).url;
    }

    /**
     * Futures WebSocket prevDay ticker
     * @param {symbol} symbol name or false. can also be a callback
     * @param {function} callback - callback function
     * @return {string} the websocket endpoint
     */
    futuresTickerStream(symbol?: string, callback = console.log) {
        const reconnect = () => {
            if (this.Options.reconnect) this.futuresTickerStream(symbol, callback);
        };
        const endpoint = symbol ? `${symbol.toLowerCase()}@ticker` : '!ticker@arr';
        const subscription = this.futuresSubscribeSingle(endpoint, data => callback(this.fTickerConvertData(data)), { reconnect });
        return (subscription as any).url;
    }

    /**
     * Futures WebSocket miniTicker
     * @param {symbol} symbol name or false. can also be a callback
     * @param {function} callback - callback function
     * @return {string} the websocket endpoint
     */
    futuresMiniTickerStream(symbol?: string, callback: Callback = console.log) {
        const reconnect = () => {
            if (this.Options.reconnect) this.futuresMiniTickerStream(symbol, callback);
        };
        const endpoint = symbol ? `${symbol.toLowerCase()}@miniTicker` : '!miniTicker@arr';
        const subscription = this.futuresSubscribeSingle(endpoint, data => callback(this.fMiniTickerConvertData(data)), { reconnect });
        return (subscription as any).url;
    }

    /**
     * Futures WebSocket bookTicker
     * @param {symbol} symbol name or false. can also be a callback
     * @param {function} callback - callback function
     * @return {string} the websocket endpoint
     */
    futuresBookTickerStream(symbol?: string, callback = console.log) {
        const reconnect = () => {
            if (this.Options.reconnect) this.futuresBookTickerStream(symbol, callback);
        };
        const endpoint = symbol ? `${symbol.toLowerCase()}@bookTicker` : '!bookTicker';
        const subscription = this.futuresSubscribeSingle(endpoint, data => callback(this.fBookTickerConvertData(data)), { reconnect });
        return (subscription as any).url;
    }

    /**
     * Websocket futures 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
     */
    futuresChart(symbols: string[] | string, interval: Interval, callback: Callback, limit = 500) {
        const reconnect = () => {
            if (this.Options.reconnect) this.futuresChart(symbols, interval, callback, limit);
        };

        const futuresChartInit = (symbol: string) => {
            if (typeof this.futuresMeta[symbol] === 'undefined') this.futuresMeta[symbol] = {};
            if (typeof this.futuresMeta[symbol][interval] === 'undefined') this.futuresMeta[symbol][interval] = {};
            if (typeof this.futuresTicks[symbol] === 'undefined') this.futuresTicks[symbol] = {};
            if (typeof this.futuresTicks[symbol][interval] === 'undefined') this.futuresTicks[symbol][interval] = {};
            if (typeof this.futuresRealtime[symbol] === 'undefined') this.futuresRealtime[symbol] = {};
            if (typeof this.futuresRealtime[symbol][interval] === 'undefined') this.futuresRealtime[symbol][interval] = {};
            if (typeof this.futuresKlineQueue[symbol] === 'undefined') this.futuresKlineQueue[symbol] = {};
            if (typeof this.futuresKlineQueue[symbol][interval] === 'undefined') this.futuresKlineQueue[symbol][interval] = [];
            this.futuresMeta[symbol][interval].timestamp = 0;
        };

        const handleFuturesKlineStream = kline => {
            const symbol = kline.s, interval: Interval = kline.k.i;
            if (!this.futuresMeta[symbol][interval].timestamp) {
                if (typeof (this.futuresKlineQueue[symbol][interval]) !== 'undefined' && kline !== null) {
                    this.futuresKlineQueue[symbol][interval].push(kline);
                }
            } else {
                //this.options.log('futures klines at ' + kline.k.t);
                this.futuresKlineHandler(symbol, kline);
                if (callback) callback(symbol, interval, this.futuresKlineConcat(symbol, interval));
            }
        };

        const getFuturesKlineSnapshot = async (symbol: string, limit = 500) => {
            const data = await this.publicFuturesRequest('v1/klines', { symbol, interval, limit });
            this.futuresKlineData(symbol, interval, data);
            //this.options.log('/futures klines at ' + this.futuresMeta[symbol][interval].timestamp);
            if (typeof this.futuresKlineQueue[symbol][interval] !== 'undefined') {
                for (const kline of this.futuresKlineQueue[symbol][interval]) this.futuresKlineHandler(symbol, kline, this.futuresMeta[symbol][interval].timestamp);
                delete this.futuresKlineQueue[symbol][interval];
            }
            if (callback) callback(symbol, interval, this.futuresKlineConcat(symbol, interval));
        };

        let subscription;
        if (Array.isArray(symbols)) {
            if (!this.isArrayUnique(symbols)) throw Error('futuresChart: "symbols" array cannot contain duplicate elements.');
            symbols.forEach(futuresChartInit);
            const streams = symbols.map(symbol => `${symbol.toLowerCase()}@kline_${interval}`);
            subscription = this.futuresSubscribe(streams, handleFuturesKlineStream, reconnect);
            symbols.forEach(element => getFuturesKlineSnapshot(element, limit));
        } else {
            const symbol = symbols;
            futuresChartInit(symbol);
            subscription = this.futuresSubscribeSingle(symbol.toLowerCase() + '@kline_' + interval, handleFuturesKlineStream, { reconnect });
            getFuturesKlineSnapshot(symbol, limit);
        }
        return (subscription as any).url;
    }

    /**
     * Websocket futures candlesticks
     * @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
     */
    futuresCandlesticksStream(symbols: string[] | string, interval: Interval, callback: Callback) {
        const reconnect = () => {
            if (this.Options.reconnect) this.futuresCandlesticksStream(symbols, interval, callback);
        };
        let subscription;
        if (Array.isArray(symbols)) {
            if (!this.isArrayUnique(symbols)) throw Error('futuresCandlesticks: "symbols" array cannot contain duplicate elements.');
            const streams = symbols.map(symbol => symbol.toLowerCase() + '@kline_' + interval);
            subscription = this.futuresSubscribe(streams, callback, { reconnect });
        } else {
            const symbol = symbols.toLowerCase();
            subscription = this.futuresSubscribeSingle(symbol + '@kline_' + interval, callback, { reconnect });
        }
        return (subscription as any).url;
    }

    // Delivery WebSocket Functions:
    /**
     * Subscribe to a single delivery websocket
     * @param {string} url - the delivery websocket endpoint
     * @param {function} callback - optional execution callback
     * @param {object} params - Optional reconnect {boolean} (whether to reconnect on disconnect), openCallback {function}, id {string}
     * @return {WebSocket} the websocket reference
     */
    // deliverySubscribeSingle(url, callback: Callback, params: Dict = {}) {
    //     return deliverySubscribeSingle(url, callback, params);
    // }

    // /**
    //  * Subscribe to a combined delivery websocket
    //  * @param {string} streams - the list of websocket endpoints to connect to
    //  * @param {function} callback - optional execution callback
    //  * @param {object} params - Optional reconnect {boolean} (whether to reconnect on disconnect), openCallback {function}, id {string}
    //  * @return {WebSocket} the websocket reference
    //  */
    // deliverySubscribe(streams, callback: Callback, params: Dict = {}) {
    //     return deliverySubscribe(streams, callback, params);
    // }

    /**
     * Returns the known delivery websockets subscriptions
     * @return {array} array of delivery websocket subscriptions
     */
    getDeliverySubscriptions() {
        return this.deliverySubscriptions;
    }

    // /**
    //  * Terminates a delivery websocket
    //  * @param {string} endpoint - the string associated with the endpoint
    //  * @return {undefined}
    //  */
    // deliveryTerminate(endpoint) {
    //     if (this.options.verbose) this.options.log('Delivery WebSocket terminating:', endpoint);
    //     return deliveryTerminate(endpoint);
    // }

    /**
     * Delivery 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
     */
    deliveryAggTradeStream(symbols: string[] | string, callback: Callback) {
        const reconnect = () => {
            if (this.Options.reconnect) this.deliveryAggTradeStream(symbols, callback);
        };
        let subscription;
        const cleanCallback = data => callback(this.dAggTradeConvertData(data));
        if (Array.isArray(symbols)) {
            if (!this.isArrayUnique(symbols)) throw Error('deliveryAggTradeStream: "symbols" cannot contain duplicate elements.');
            const streams = symbols.map(symbol => symbol.toLowerCase() + '@aggTrade');
            subscription = this.deliverySubscribe(streams, cleanCallback, { reconnect });
        } else {
            const symbol = symbols;
            subscription = this.deliverySubscribeSingle(symbol.toLowerCase() + '@aggTrade', cleanCallback, { reconnect });
        }
        return (subscription as any).url;
    }

    /**
     * Delivery WebSocket mark price
     * @param {symbol} symbol name or false. can also be a callback
     * @param {function} callback - callback function
     * @param {string} speed - 1 second updates. leave blank for default 3 seconds
     * @return {string} the websocket endpoint
     */
    deliveryMarkPriceStream(symbol?: string, callback = console.log, speed = '@1s') {
        const reconnect = () => {
            if (this.Options.reconnect) this.deliveryMarkPriceStream(symbol, callback);
        };
        const endpoint = symbol ? `${symbol.toLowerCase()}@markPrice` : '!markPrice@arr';
        const subscription = this.deliverySubscribeSingle(endpoint + speed, data => callback(this.dMarkPriceConvertData(data)), { reconnect });
        return (subscription as any).url;
    }

    /**
     * Delivery WebSocket liquidations stream
     * @param {symbol} symbol name or false. can also be a callback
     * @param {function} callback - callback function
     * @return {string} the websocket endpoint
     */
    deliveryLiquidationStream(symbol?: string, callback = console.log) {
        const reconnect = () => {
            if (this.Options.reconnect) this.deliveryLiquidationStream(symbol, callback);
        };
        const endpoint = symbol ? `${symbol.toLowerCase()}@forceOrder` : '!forceOrder@arr';
        const subscription = this.deliverySubscribeSingle(endpoint, data => callback(this.dLiquidationConvertData(data)), { reconnect });
        return (subscription as any).url;
    }

    /**
     * Delivery WebSocket prevDay ticker
     * @param {symbol} symbol name or false. can also be a callback
     * @param {function} callback - callback function
     * @return {string} the websocket endpoint
     */
    deliveryTickerStream(symbol?: string, callback = console.log) {
        const reconnect = () => {
            if (this.Options.reconnect) this.deliveryTickerStream(symbol, callback);
        };
        const endpoint = symbol ? `${symbol.toLowerCase()}@ticker` : '!ticker@arr';
        const subscription = this.deliverySubscribeSingle(endpoint, data => callback(this.dTickerConvertData(data)), { reconnect });
        return (subscription as any).url;
    }

    /**
     * Delivery WebSocket miniTicker
     * @param {symbol} symbol name or false. can also be a callback
     * @param {function} callback - callback function
     * @return {string} the websocket endpoint
     */
    deliveryMiniTickerStream(symbol?: string, callback = console.log) {
        const reconnect = () => {
            if (this.Options.reconnect) this.deliveryMiniTickerStream(symbol, callback);
        };
        const endpoint = symbol ? `${symbol.toLowerCase()}@miniTicker` : '!miniTicker@arr';
        const subscription = this.deliverySubscribeSingle(endpoint, data => callback(this.dMiniTickerConvertData(data)), { reconnect });
        return (subscription as any).url;
    }

    /**
     * Delivery WebSocket bookTicker
     * @param {symbol} symbol name or false. can also be a callback
     * @param {function} callback - callback function
     * @return {string} the websocket endpoint
     */
    deliveryBookTickerStream(symbol?: string, callback = console.log) {
        const reconnect = () => {
            if (this.Options.reconnect) this.deliveryBookTickerStream(symbol, callback);
        };
        const endpoint = symbol ? `${symbol.toLowerCase()}@bookTicker` : '!bookTicker';
        const subscription = this.deliverySubscribeSingle(endpoint, data => callback(this.dBookTickerConvertData(data)), { reconnect });
        return (subscription as any).url;
    }

    /**
     * Websocket delivery 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
     */
    deliveryChart(symbols: string[] | string, interval: Interval, callback: Callback, limit = 500) {
        const reconnect = () => {
            if (this.Options.reconnect) this.deliveryChart(symbols, interval, callback, limit);
        };

        const deliveryChartInit = symbol => {
            if (typeof this.deliveryMeta[symbol] === 'undefined') this.deliveryMeta[symbol] = {};
            if (typeof this.deliveryMeta[symbol][interval] === 'undefined') this.deliveryMeta[symbol][interval] = {};
            if (typeof this.deliveryTicks[symbol] === 'undefined') this.deliveryTicks[symbol] = {};
            if (typeof this.deliveryTicks[symbol][interval] === 'undefined') this.deliveryTicks[symbol][interval] = {};
            if (typeof this.deliveryRealtime[symbol] === 'undefined') this.deliveryRealtime[symbol] = {};
            if (typeof this.deliveryRealtime[symbol][interval] === 'undefined') this.deliveryRealtime[symbol][interval] = {};
            if (typeof this.deliveryKlineQueue[symbol] === 'undefined') this.deliveryKlineQueue[symbol] = {};
            if (typeof this.deliveryKlineQueue[symbol][interval] === 'undefined') this.deliveryKlineQueue[symbol][interval] = [];
            this.deliveryMeta[symbol][interval].timestamp = 0;
        };

        const handleDeliveryKlineStream = kline => {
            const symbol = kline.s, interval: Interval = kline.k.i;
            if (!this.deliveryMeta[symbol][interval].timestamp) {
                if (typeof (this.deliveryKlineQueue[symbol][interval]) !== 'undefined' && kline !== null) {
                    this.deliveryKlineQueue[symbol][interval].push(kline);
                }
            } else {
                //this.options.log('futures klines at ' + kline.k.t);
                this.deliveryKlineHandler(symbol, kline);
                if (callback) callback(symbol, interval, this.deliveryKlineConcat(symbol, interval));
            }
        };

        const getDeliveryKlineSnapshot = async (symbol: string, limit = 500) => {
            const data = await this.publicDeliveryRequest('v1/klines', { symbol, interval, limit });
            this.deliveryKlineData(symbol, interval, data);
            //this.options.log('/delivery klines at ' + this.deliveryMeta[symbol][interval].timestamp);
            if (typeof this.deliveryKlineQueue[symbol][interval] !== 'undefined') {
                for (const kline of this.deliveryKlineQueue[symbol][interval]) this.deliveryKlineHandler(symbol, kline, this.deliveryMeta[symbol][interval].timestamp);
                delete this.deliveryKlineQueue[symbol][interval];
            }
            if (callback) callback(symbol, interval, this.deliveryKlineConcat(symbol, interval));
        };

        let subscription;
        if (Array.isArray(symbols)) {
            if (!this.isArrayUnique(symbols)) throw Error('deliveryChart: "symbols" array cannot contain duplicate elements.');
            symbols.forEach(deliveryChartInit);
            const streams = symbols.map(symbol => `${symbol.toLowerCase()}@kline_${interval}`);
            subscription = this.deliverySubscribe(streams, handleDeliveryKlineStream, reconnect);
            symbols.forEach(element => getDeliveryKlineSnapshot(element, limit));
        } else {
            const symbol = symbols;
            deliveryChartInit(symbol);
            subscription = this.deliverySubscribeSingle(symbol.toLowerCase() + '@kline_' + interval, handleDeliveryKlineStream, reconnect);
            getDeliveryKlineSnapshot(symbol, limit);
        }
        return (subscription as any).url;
    }

    /**
     * Websocket delivery candlesticks
     * @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
     */
    deliveryCandlesticks(symbols: string[] | string, interval: Interval, callback: Callback) {
        const reconnect = () => {
            if (this.Options.reconnect) this.deliveryCandlesticks(symbols, interval, callback);
        };
        let subscription;
        if (Array.isArray(symbols)) {
            if (!this.isArrayUnique(symbols)) throw Error('deliveryCandlesticks: "symbols" array cannot contain duplicate elements.');
            const streams = symbols.map(symbol => symbol.toLowerCase() + '@kline_' + interval);
            subscription = this.deliverySubscribe(streams, callback, { reconnect });
        } else {
            const symbol = symbols.toLowerCase();
            subscription = this.deliverySubscribeSingle(symbol + '@kline_' + interval, callback, { reconnect });
        }
        return (subscription as any).url;
    }

    /**
     * Userdata websockets function
     * @param {function} all_updates_callback
     * @param {function} execution_callback - optional execution callback
     * @param {function} subscribed_callback - subscription callback
     * @param {function} list_status_callback - status callback
     * @return {undefined}
     */
    userData(all_updates_callback?: Callback, balance_callback?: Callback, execution_callback?: Callback, subscribed_callback?: Callback, list_status_callback?: Callback) {
        const reconnect = () => {
            if (this.Options.reconnect) this.userData(all_updates_callback, balance_callback, execution_callback, subscribed_callback, list_status_callback);
        };

        // Set up callbacks
        this.Options.all_updates_callback = all_updates_callback;
        this.Options.balance_callback = balance_callback;
        this.Options.execution_callback = execution_callback ? execution_callback : balance_callback;
        this.Options.list_status_callback = list_status_callback;

        // Connect to WebSocket API
        const connectionId = 'userData';
        const ws = this.connectWsApi(connectionId, this.userDataHandler.bind(this), reconnect);

        ws.on('open', async () => {
            try {
                // Subscribe using userDataStream.subscribe.signature method
                const timestamp = Date.now();
                const query = `apiKey=${this.APIKEY}&timestamp=${timestamp}`;
                const signature = this.generateSignature(query);

                const result = await this.sendWsApiRequest(connectionId, 'userDataStream.subscribe.signature', {
                    apiKey: this.APIKEY,
                    timestamp: timestamp,
                    signature: signature
                });

                this.Options.userDataSubscriptionId = result.subscriptionId;
                if (this.Options.verbose) {
                    this.Options.log(`User Data Stream subscribed with subscriptionId: ${result.subscriptionId}`);
                }

                if (subscribed_callback) subscribed_callback(connectionId);
            } catch (error) {
                this.Options.log('User Data Stream subscription error:', error.message);
                if (reconnect) setTimeout(reconnect, 5000);
            }
        });
    }

    /**
     * Margin Userdata websockets function
     * @param {function} all_updates_callback
     * @param {function} execution_callback - optional execution callback
     * @param {function} subscribed_callback - subscription callback
     * @param {function} list_status_callback - status callback
     * @return {undefined}
     */
    userMarginData(all_updates_callback?: Callback, balance_callback?: Callback, execution_callback?: Callback, subscribed_callback?: Callback, list_status_callback?: Callback) {
        const self = this;
        const reconnect = () => {
            if (this.Options.reconnect) this.userMarginData(all_updates_callback, balance_callback, execution_callback, subscribed_callback, list_status_callback);
        };

        // Set up callbacks
        this.Options.margin_all_updates_callback = all_updates_callback;
        this.Options.margin_balance_callback = balance_callback;
        this.Options.margin_execution_callback = execution_callback;
        this.Options.margin_list_status_callback = list_status_callback;

        // Get listenToken from REST API
        this.apiRequest(this.sapi + 'v1/userListenToken', {}, 'POST').then((response: any) => {
            const listenToken = response.token;
            const expirationTime = response.expirationTime;
            this.Options.marginListenToken = listenToken;
            this.Options.marginListenTokenExpiry = expirationTime;

            if (this.Options.verbose) {
                this.Options.log(`Margin listenToken obtained, expires at: ${new Date(expirationTime).toISOString()}`);
            }

            // Connect to WebSocket API
            const connectionId = 'userMarginData';
            const ws = this.connectWsApi(connectionId, this.userMarginDataHandler.bind(this), reconnect);

            ws.on('open', async () => {
                try {
                    // Subscribe using userDataStream.subscribe.listenToken method
                    const result = await this.sendWsApiRequest(connectionId, 'userDataStream.subscribe.listenToken', {
                        listenToken: listenToken
                    });

                    this.Options.marginDataSubscriptionId = result.subscriptionId;
                    const subscriptionExpiry = result.expirationTime;

                    if (this.Options.verbose) {
                        this.Options.log(`Margin Data Stream subscribed with subscriptionId: ${result.subscriptionId}`);
                        this.Options.log(`Subscription expires at: ${new Date(subscriptionExpiry).toISOString()}`);
                    }

                    // Set up renewal before expiration (renew 5 minutes before expiry)
                    const renewalTime = subscriptionExpiry - Date.now() - (5 * 60 * 1000);
                    if (renewalTime > 0) {
                        setTimeout(async function renewSubscription() {
                            try {
                                // Get new listenToken
                                const renewResponse: any = await self.apiRequest(self.sapi + 'v1/userListenToken', {}, 'POST');
                                const newListenToken = renewResponse.token;
                                const newExpirationTime = renewResponse.expirationTime;

                                if (self.Options.verbose) {
                                    self.Options.log(`New margin listenToken obtained, expires at: ${new Date(newExpirationTime).toISOString()}`);
                                }

                                // Re-subscribe with new token
                                const renewResult = await self.sendWsApiRequest(connectionId, 'userDataStream.subscribe.listenToken', {
                                    listenToken: newListenToken
                                });

                                self.Options.marginDataSubscriptionId = renewResult.subscriptionId;
                                const newSubscriptionExpiry = renewResult.expirationTime;

                                if (self.Options.verbose) {
                                    self.Options.log(`Margin Data Stream renewed with subscriptionId: ${renewResult.subscriptionId}`);
                                }

                                // Schedule next renewal
                                const nextRenewalTime = newSubscriptionExpiry - Date.now() - (5 * 60 * 1000);
                                if (nextRenewalTime > 0) {
                                    setTimeout(renewSubscription, nextRenewalTime);
                                }
                            } catch (error) {
                                self.Options.log('Margin Data Stream renewal error:', error.message);
                                // Attempt to reconnect
                                if (reconnect) setTimeout(reconnect, 5000);
                            }
                        }, renewalTime);
                    }

                    if (subscribed_callback) subscribed_callback(connectionId);
                } catch (error) {
                    this.Options.log('Margin Data Stream subscription error:', error.message);
                    if (reconnect) setTimeout(reconnect, 5000);
                }
            });
        }).catch((error: any) => {
            this.Options.log('Failed to obtain margin listenToken:', error.message);
            if (reconnect) setTimeout(reconnect, 5000);
        });
    }

    /**
     * Future Userdata websockets function
     * @param {function} all_updates_callback
     * @param {function} margin_call_callback
     * @param {function} account_update_callback
     * @param {function} order_update_callback
     * @param {Function} subscribed_callback - subscription callback
     */
    userFutureData(all_updates_callback?: Callback, margin_call_callback?: Callback, account_update_callback?: Callback, order_update_callback?: Callback, subscribed_callback?: Callback, account_config_update_callback?: Callback) {
        // const url = (this.Options.test) ? this.fapiTest : this.fapi;
        const url = this.getFapiUrl();

        const reconnect = () => {
            if (this.Options.reconnect) this.userFutureData(all_updates_callback, margin_call_callback, account_update_callback, order_update_callback, subscribed_callback);
        };

        // const response = await this.apiRequest(url + 'v1/listenKey', {}, 'POST');
        this.apiRequest(url + 'v1/listenKey', {}, 'POST').then((response: any) => {
            this.Options.listenFutureKey = response.listenKey;
            const self = this;
            const keepAlive = this.futuresListenKeyKeepAlive;
            setTimeout(async function userDataKeepAlive() { // keepalive
                try {
                    await self.apiRequest(url + 'v1/listenKey?listenKey=' + self.Options.listenFutureKey, {}, 'PUT');
                    setTimeout(userDataKeepAlive, keepAlive); // 30 minute keepalive
                } catch (error) {
                    setTimeout(userDataKeepAlive, 60000); // retry in 1 minute
                }
            }, keepAlive); // 30 minute keepalive
            this.Options.futures_all_updates_callback = all_updates_callback;
            this.Options.future_margin_call_callback = margin_call_callback;
            this.Options.future_account_update_callback = account_update_callback;
            this.Options.future_account_config_update_callback = account_config_update_callback;
            this.Options.future_order_update_callback = order_update_callback;
            const subscription = this.futuresSubscribe(this.Options.listenFutureKey, this.userFutureDataHandler.bind(this), { reconnect });
            if (subscribed_callback) subscribed_callback(subscription.endpoint);

        });
        // const response = await this.apiRequest(url + 'v1/listenKey', {}, 'POST');

    }

    /**
   * Delivery Userdata websockets function
   * @param {function} margin_call_callback
   * @param {function} account_update_callback
   * @param {function} order_update_callback
   * @param {Function} subscribed_callback - subscription callback
   */
    userDeliveryData(
        margin_call_callback: Callback,
        account_update_callback?: Callback,
        order_update_callback?: Callback,
        subscribed_callback?: Callback
    ) {
        const url = this.getDapiUrl();

        const reconnect = async () => {
            if (this.Options.reconnect)
                await this.userDeliveryData(
                    margin_call_callback,
                    account_update_callback,
                    order_update_callback,
                    subscribed_callback
                );
        };

        this.apiRequest(url + "v1/listenKey", {}, "POST").then((response: any) => {
            this.Options.listenDeliveryKey = response.listenKey;
            const getDeliveryKey = () => this.Options.listenDeliveryKey;
            const self = this;
            const keepAlive = this.futuresListenKeyKeepAlive;
            setTimeout(async function userDataKeepAlive() {
                // keepalive
                try {
                    await self.apiRequest(
                        url +
                        "v1/listenKey?listenKey=" +
                        getDeliveryKey(),
                        {},
                        "PUT"
                    );
                    // function (err: any) {
                    //     if (err) setTimeout(userDataKeepAlive, 60000);
                    //     // retry in 1 minute
                    setTimeout(userDataKeepAlive, keepAlive); // 30 minute keepalive
                } catch (error) {
                    setTimeout(userDataKeepAlive, 60000); // retry in 1 minute
                }
            }, keepAlive); // 30 minute keepalive
            this.Options.delivery_margin_call_callback = margin_call_callback;
            this.Options.delivery_account_update_callback = account_update_callback;
            this.Options.delivery_order_update_callback = order_update_callback;
            const subscription = this.deliverySubscribe(
                this.Options.listenDeliveryKey,
                this.userDeliveryDataHandler.bind(this),
                { reconnect }
            );
            if (subscribed_callback) subscribed_callback(subscription.endpoint);

        });

        // }
    }

    // /**
    //  * 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(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(url, callback, reconnect = false) {
    // //     return subscribeCombined(url, callback, reconnect);
    // // }

    // /**
    //  * Returns the known websockets subscriptions
    //  * @return {array} array of web socket subscriptions
    //  */
    getSubscriptions() {
        return this.subscriptions;
    }

    // /**
    //  * Terminates a web socket
    //  * @param {string} endpoint - the string associated with the endpoint
    //  * @return {undefined}
    //  */
    // terminate(endpoint) {
    //     if (this.options.verbose) this.options.log('WebSocket terminating:', endpoint);
    //     return this.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
     */
    depthStream(symbols: string[] | string, callback: Callback) {
        const reconnect = () => {
            if (this.Options.reconnect) this.depthStream(symbols, callback);
        };
        let subscription;
        if (Array.isArray(symbols)) {
            if (!this.isArrayUnique(symbols)) throw Error('depth: "symbols" cannot contain duplicate elements.');
            const streams = symbols.map(function (symbol) {
                return symbol.toLowerCase() + '@depth@100ms';
            });
            subscription = this.subscribeCombined(streams, callback, reconnect);
        } else {
            const symbol = symbols;
            subscription = this.subscribe(symbol.toLowerCase() + '@depth@100ms', callback, reconnect);
        }
        return (subscription as any).url;
    }

    async mapLimit(array, limit, asyncFn) {
        const results = [];
        let i = 0;

        const workers = new Array(limit).fill(0).map(async () => {
            while (i < array.length) {
                const currentIndex = i++;
                const result = await asyncFn(array[currentIndex]);
                results[currentIndex] = result;
            }
        });
        await Promise.all(workers);
        return results;
    }

    /**
     * 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
     */
    depthCacheStream(symbols: string[] | string, callback: Callback, limit = 500) {
        const reconnect = () => {
            if (this.Options.reconnect) this.depthCacheStream(symbols, callback, limit);
        };

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

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

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

        const getSymbolDepthSnapshot = async (symbol: string) => {
            const json = await this.publicSpotRequest('v3/depth', { symbol: symbol, limit: limit });
            json.symbol = symbol;
            // cb(null, json);
            return json;
        };

        const updateSymbolDepthCache = json => {
            // Get previous store symbol
            const symbol = json.symbol;
            // Initialize depth cache from snapshot
            this.depthCache[symbol] = this.depthData(json);
            // Prepare depth cache context
            const context = this.depthCacheContext[symbol];
            context.snapshotUpdateId = json.lastUpdateId;
            context.messageQueue = context.messageQueue.filter(depth => depth.u > context.snapshotUpdateId);
            // Process any pending depth messages
            for (const 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 {
                    this.depthHandler(depth);
                } catch (err) {
                    // Do nothing
                }
            }
            delete context.messageQueue;
            if (callback) callback(symbol, this.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 (!this.isArrayUnique(symbols)) throw Error('depthCache: "symbols" cannot contain duplicate elements.');
            symbols.forEach(symbolDepthInit);
            const streams = symbols.map(function (symbol) {
                return symbol.toLowerCase() + `@depth@100ms`;
            });
            const mapLimit = this.mapLimit.bind(this);
            subscription = this.subscribeCombined(streams, handleDepthStreamData, reconnect, () => {
                mapLimit(symbols, 50, getSymbolDepthSnapshot)
                    .then(results => {
                        results.forEach(updateSymbolDepthCache);
                    })
                    .catch(err => {
                        throw err;
                    });
            });
            symbols.forEach(s => assignEndpointIdToContext(s, subscription.endpoint));
        } else {
            const symbol = symbols;
            symbolDepthInit(symbol);
            const mapLimit = this.mapLimit.bind(this);
            subscription = this.subscribe(symbol.toLowerCase() + `@depth@100ms`, handleDepthStreamData, reconnect, () => {
                mapLimit([symbol], 1, getSymbolDepthSnapshot)
                    .then(results => {
                        results.forEach(updateSymbolDepthCache);
                    })
                    .catch(err => {
                        throw err;
                    });
            });
            assignEndpointIdToContext(symbol, subscription.endpoint);
        }
        return (subscription as any).url;
    }

    /**
     * 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: string[] | string) {
        const symbolsArr = Array.isArray(symbols) ? symbols : [symbols];
        symbolsArr.forEach(thisSymbol => {
            delete this.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(symbols: string[] | string, callback: Callback, limit = 100, stagger = 200) {
        if (!Array.isArray(symbols)) symbols = [symbols];
        let chain = null;

        symbols.forEach(symbol => {
            const promise = () => new Promise(resolve => {
                this.depthCacheStream(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
     */
    aggTradesStream(symbols: string[] | string, callback: Callback) {
        const reconnect = () => {
            if (this.Options.reconnect) this.aggTradesStream(symbols, callback);
        };
        let subscription;
        if (Array.isArray(symbols)) {
            if (!this.isArrayUnique(symbols)) throw Error('trades: "symbols" cannot contain duplicate elements.');
            const streams = symbols.map(function (symbol) {
                return symbol.toLowerCase() + '@aggTrade';
            });
            subscription = this.subscribeCombined(streams, callback, reconnect);
        } else {
            const symbol = symbols;
            subscription = this.subscribe(symbol.toLowerCase() + '@aggTrade', callback, reconnect);
        }
        return (subscription as any).url;
    }

    /**
    * 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
    */
    tradesStream(symbols: string[] | string, callback: Callback) {
        const reconnect = () => {
            if (this.Options.reconnect) this.tradesStream(symbols, callback);
        };

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

    /**
     * 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(symbols: string[] | string, interval: Interval, callback: Callback, limit = 500) {
        const reconnect = () => {
            if (this.Options.reconnect) this.chart(symbols, interval, callback, limit);
        };

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

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

        const getSymbolKlineSnapshot = async (symbol: string, limit = 500) => {
            const data = await this.publicSpotRequest('v3/klines', { symbol: symbol, interval: interval, limit: limit });
            // function (error, data) {
            //     klineData(symbol, interval, data);
            //     //this.options.log('/klines at ' +this.info[symbol][interval].timestamp);
            //     if (typeof this.klineQueue[symbol][interval] !== 'undefined') {
            //         for (let kline of this.klineQueue[symbol][interval]) klineHandler(symbol, kline, this.info[symbol][interval].timestamp);
            //         delete this.klineQueue[symbol][interval];
            //     }
            //     if (callback) callback(symbol, interval, this.klineConcat(symbol, interval));
            // }
            this.klineData(symbol, interval, data);
            if (typeof this.klineQueue[symbol][interval] !== 'undefined') {
                for (const kline of this.klineQueue[symbol][interval]) this.klineHandler(symbol, kline, this.info[symbol][interval].timestamp);
                delete this.klineQueue[symbol][interval];
            }
            if (callback) callback(symbol, interval, this.klineConcat(symbol, interval));
        };

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

    /**
     * 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
     */
    candlesticksStream(symbols: string[] | string, interval: Interval, callback: Callback) {
        const reconnect = () => {
            if (this.Options.reconnect) this.candlesticksStream(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 (!this.isArrayUnique(symbols)) throw Error('candlesticks: "symbols" cannot contain duplicate elements.');
            const streams = symbols.map(function (symbol) {
                return symbol.toLowerCase() + '@kline_' + interval;
            });
            subscription = this.subscribeCombined(streams, callback, reconnect);
        } else {
            const symbol = symbols.toLowerCase();
            subscription = this.subscribe(symbol + '@kline_' + interval, callback, reconnect);
        }
        return (subscription as any).url;
    }

    /**
     * Websocket mini ticker
     * @param {function} callback - callback function
     * @return {string} the websocket endpoint
     */
    miniTicker(callback) {
        const reconnect = () => {
            if (this.Options.reconnect) this.miniTicker(callback);
        };
        const subscription = this.subscribe('!miniTicker@arr', function (data: any) {
            const markets = {};
            for (const 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 as any).url;
    }

    /**
     * Spot WebSocket bookTicker (bid/ask quotes including price & amount)
     * @param {string | string[]} symbol symbol or array of symbols
     * @param {function} callback - callback function
     * @return {string} the websocket endpoint
     */
    bookTickersStream(symbol: string | string[], callback = console.log) {
        const reconnect = () => {
            if (this.Options.reconnect) this.bookTickersStream(symbol, callback);
        };
        let subscription: any;
        if (Array.isArray(symbol)) {
            const streams = symbol.map(function (symbol) {
                return symbol.toLowerCase() + '@bookTicker';
            });
            subscription = this.subscribeCombined(streams, data => callback(this.fBookTickerConvertData(data)), reconnect);
        } else {
            const endpoint = `${(symbol as string).toLowerCase()}@bookTicker`;
            subscription = this.subscribe(endpoint, data => callback(this.fBookTickerConvertData(data)), reconnect);
        }
        return (subscription as any).url;
    }

    /**
     * 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
     */
    prevDayStream(symbols: string[] | string | undefined, callback?: Callback, singleCallback?: Callback) {
        const reconnect = () => {
            if (this.Options.reconnect) this.prevDayStream(symbols, callback, singleCallback);
        };

        let subscription;
        const prevDayStreamHandler = this.prevDayStreamHandler.bind(this);
        // Combine stream for array of symbols
        if (Array.isArray(symbols)) {
            if (!this.isArrayUnique(symbols)) throw Error('prevDay: "symbols" cannot contain duplicate elements.');
            const streams = symbols.map(function (symbol) {
                return symbol.toLowerCase() + '@ticker';
            });
            subscription = this.subscribeCombined(streams, function (data: any) {
                prevDayStreamHandler(data, callback);
            }, reconnect);
            // Raw stream for  a single symbol
        } else if (symbols) {
            const symbol = symbols;
            subscription = this.subscribe(symbol.toLowerCase() + '@ticker', function (data: any) {
                prevDayStreamHandler(data, callback);
            }, reconnect);
            // Raw stream of all listed symbols
        } else {
            subscription = this.subscribe('!miniTicker@arr', function (data: any) {
                if (singleCallback) {
                    prevDayStreamHandler(data, callback);
                } else {
                    for (const line of data) {
                        prevDayStreamHandler(line, callback);
                    }
                }
            }, reconnect);
        }
        return (subscription as any).url;
    }
}
