UNPKG

preidman-ccxt

Version:

A JavaScript / Python / PHP cryptocurrency trading library with support for 100+ exchanges

1,331 lines (1,100 loc) 60.6 kB
"use strict"; /* ------------------------------------------------------------------------ */ const functions = require ('./functions') , Market = require ('./Market') const { isNode , keys , values , deepExtend , extend , clone , flatten , unique , indexBy , sortBy , groupBy , aggregate , uuid , unCamelCase , precisionFromString , throttle , capitalize , now , sleep , timeout , TimedOut , buildOHLCVC , decimalToPrecision } = functions const { ExchangeError , InvalidAddress , NotSupported , AuthenticationError , DDoSProtection , RequestTimeout , ExchangeNotAvailable } = require ('./errors') const { TRUNCATE, ROUND, DECIMAL_PLACES } = functions.precisionConstants const defaultFetch = typeof (fetch) === "undefined" ? require ('fetch-ponyfill') ().fetch : fetch // ---------------------------------------------------------------------------- // web3 / 0x imports let Web3 = undefined , ethAbi = undefined , ethUtil = undefined , BigNumber = undefined try { const requireFunction = require; Web3 = requireFunction ('web3') // eslint-disable-line global-require ethAbi = requireFunction ('ethereumjs-abi') // eslint-disable-line global-require ethUtil = requireFunction ('ethereumjs-util') // eslint-disable-line global-require BigNumber = requireFunction ('bignumber.js') // eslint-disable-line global-require // we prefer bignumber.js over BN.js // BN = requireFunction ('bn.js') // eslint-disable-line global-require } catch (e) { } const journal = undefined // isNode && require ('./journal') // stub until we get a better solution for Webpack and React /* ------------------------------------------------------------------------ */ module.exports = class Exchange { getMarket (symbol) { if (!this.marketClasses) this.marketClasses = {} let marketClass = this.marketClasses[symbol] if (marketClass) return marketClass marketClass = new Market (this, symbol) this.marketClasses[symbol] = marketClass // only one Market instance per market return marketClass } describe () { return { 'id': undefined, 'name': undefined, 'countries': undefined, 'enableRateLimit': false, 'rateLimit': 2000, // milliseconds = seconds * 1000 'certified': false, 'has': { 'CORS': false, 'publicAPI': true, 'privateAPI': true, 'cancelOrder': true, 'cancelOrders': false, 'createDepositAddress': false, 'createOrder': true, 'createMarketOrder': true, 'createLimitOrder': true, 'deposit': false, 'editOrder': 'emulated', 'fetchBalance': true, 'fetchBidsAsks': false, 'fetchClosedOrders': false, 'fetchCurrencies': false, 'fetchDepositAddress': false, 'fetchDeposits': false, 'fetchFundingFees': false, 'fetchL2OrderBook': true, 'fetchMarkets': true, 'fetchMyTrades': false, 'fetchOHLCV': 'emulated', 'fetchOpenOrders': false, 'fetchOrder': false, 'fetchOrderBook': true, 'fetchOrderBooks': false, 'fetchOrders': false, 'fetchTicker': true, 'fetchTickers': false, 'fetchTrades': true, 'fetchTradingFees': false, 'fetchTradingLimits': false, 'fetchTransactions': false, 'fetchWithdrawals': false, 'withdraw': false, }, 'urls': { 'logo': undefined, 'api': undefined, 'www': undefined, 'doc': undefined, 'fees': undefined, }, 'api': undefined, 'requiredCredentials': { 'apiKey': true, 'secret': true, 'uid': false, 'login': false, 'password': false, 'twofa': false, // 2-factor authentication (one-time password key) 'privateKey': false, // a "0x"-prefixed hexstring private key for a wallet 'walletAddress': false, // the wallet address "0x"-prefixed hexstring }, 'markets': undefined, // to be filled manually or by fetchMarkets 'currencies': {}, // to be filled manually or by fetchMarkets 'timeframes': undefined, // redefine if the exchange has.fetchOHLCV 'fees': { 'trading': { 'tierBased': undefined, 'percentage': undefined, 'taker': undefined, 'maker': undefined, }, 'funding': { 'tierBased': undefined, 'percentage': undefined, 'withdraw': {}, 'deposit': {}, }, }, 'skipJsonOnStatusCodes': [], // array of http status codes which override requirement for JSON response 'exceptions': undefined, 'httpExceptions': { '422': ExchangeError, '418': DDoSProtection, '429': DDoSProtection, '404': ExchangeNotAvailable, '409': ExchangeNotAvailable, '500': ExchangeNotAvailable, '501': ExchangeNotAvailable, '502': ExchangeNotAvailable, '520': ExchangeNotAvailable, '521': ExchangeNotAvailable, '522': ExchangeNotAvailable, '525': ExchangeNotAvailable, '526': ExchangeNotAvailable, '400': ExchangeNotAvailable, '403': ExchangeNotAvailable, '405': ExchangeNotAvailable, '503': ExchangeNotAvailable, '530': ExchangeNotAvailable, '408': RequestTimeout, '504': RequestTimeout, '401': AuthenticationError, '511': AuthenticationError, }, // some exchanges report only 'free' on `fetchBlance` call (i.e. report no 'used' funds) // in this case ccxt will try to infer 'used' funds from open order cache, which might be stale // still, some exchanges report number of open orders together with balance // if you set the following flag to 'true' ccxt will leave 'used' funds undefined in case of discrepancy 'dontGetUsedBalanceFromStaleCache': false, 'commonCurrencies': { // gets extended/overwritten in subclasses 'XBT': 'BTC', 'BCC': 'BCH', 'DRK': 'DASH', 'BCHABC': 'BCH', 'BCHSV': 'BSV', }, 'precisionMode': DECIMAL_PLACES, } // return } // describe () constructor (userConfig = {}) { Object.assign (this, functions, { encode: string => string, decode: string => string }) // if (isNode) { // this.nodeVersion = process.version.match (/\d+\.\d+\.\d+/)[0] // this.userAgent = { // 'User-Agent': 'ccxt/' + Exchange.ccxtVersion + // ' (+https://github.com/ccxt/ccxt)' + // ' Node.js/' + this.nodeVersion + ' (JavaScript)' // } // } this.options = {} // exchange-specific options, if any this.fetchOptions = {} // fetch implementation options (JS only) this.userAgents = { 'chrome': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36', 'chrome39': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.71 Safari/537.36', } this.headers = {} // prepended to URL, like https://proxy.com/https://exchange.com/api... this.proxy = '' this.origin = '*' // CORS origin this.iso8601 = (timestamp) => { const _timestampNumber = parseInt (timestamp, 10); // undefined, null and lots of nasty non-numeric values yield NaN if (isNaN (_timestampNumber) || _timestampNumber < 0) { return undefined; } // last line of defence try { return new Date (_timestampNumber).toISOString (); } catch (e) { return undefined; } } this.parse8601 = (x) => { if (typeof x !== 'string' || !x) { return undefined; } if (x.match (/^[0-9]+$/)) { // a valid number in a string, not a date. return undefined; } if (x.indexOf ('-') < 0 || x.indexOf (':') < 0) { // no date can be without a dash and a colon return undefined; } // last line of defence try { const candidate = Date.parse (((x.indexOf ('+') >= 0) || (x.slice (-1) === 'Z')) ? x : (x + 'Z').replace (/\s(\d\d):/, 'T$1:')); if (isNaN (candidate)) { return undefined; } return candidate; } catch (e) { return undefined; } } this.parseDate = (x) => { if (typeof x !== 'string' || !x) { return undefined; } if (x.indexOf ('GMT') >= 0) { try { return Date.parse (x); } catch (e) { return undefined; } } return this.parse8601 (x); } this.microseconds = () => now () * 1000 // TODO: utilize performance.now for that purpose this.seconds = () => Math.floor (now () / 1000) this.minFundingAddressLength = 1 // used in checkAddress this.substituteCommonCurrencyCodes = true // reserved // do not delete this line, it is needed for users to be able to define their own fetchImplementation this.fetchImplementation = defaultFetch this.timeout = 10000 // milliseconds this.verbose = false this.debug = false this.journal = 'debug.json' this.userAgent = undefined this.twofa = undefined // two-factor authentication (2FA) this.apiKey = undefined this.secret = undefined this.matcherUrl = 'https://matcher.wavesnodes.com' this.uid = undefined this.login = undefined this.password = undefined this.privateKey = undefined // a "0x"-prefixed hexstring private key for a wallet this.walletAddress = undefined // a wallet address "0x"-prefixed hexstring this.balance = {} this.orderbooks = {} this.tickers = {} this.orders = {} this.trades = {} this.transactions = {} this.requiresWeb3 = false this.enableLastJsonResponse = true this.enableLastHttpResponse = true this.enableLastResponseHeaders = true this.last_http_response = undefined this.last_json_response = undefined this.last_response_headers = undefined this.arrayConcat = (a, b) => a.concat (b) const unCamelCaseProperties = (obj = this) => { if (obj !== null) { for (const k of Object.getOwnPropertyNames (obj)) { this[unCamelCase (k)] = this[k] } unCamelCaseProperties (Object.getPrototypeOf (obj)) } } unCamelCaseProperties () // merge configs const config = deepExtend (this.describe (), userConfig) // merge to this for (const [property, value] of Object.entries (config)) this[property] = deepExtend (this[property], value) // generate old metainfo interface for (const k in this.has) { this['has' + capitalize (k)] = !!this.has[k] // converts 'emulated' to true } if (this.api) this.defineRestApi (this.api, 'request') this.initRestRateLimiter () if (this.markets) this.setMarkets (this.markets) if (this.debug && journal) { journal (() => this.journal, this, Object.keys (this.has)) } if (this.requiresWeb3 && !this.web3 && Web3) { const provider = (this.web3ProviderURL) ? new Web3.providers.HttpProvider (this.web3ProviderURL) : new Web3.providers.HttpProvider () this.web3 = new Web3 (Web3.givenProvider || provider) } } defaults () { return { /* override me */ } } nonce () { return this.seconds () } milliseconds () { return now () } encodeURIComponent (...args) { return encodeURIComponent (...args) } checkRequiredCredentials (error = true) { Object.keys (this.requiredCredentials).forEach ((key) => { if (this.requiredCredentials[key] && !this[key]) { if (error) { throw new AuthenticationError (this.id + ' requires `' + key + '`') } else { return error } } }) } checkAddress (address) { if (address === undefined) throw new InvalidAddress (this.id + ' address is undefined') // check the address is not the same letter like 'aaaaa' nor too short nor has a space if ((unique (address).length === 1) || address.length < this.minFundingAddressLength || address.includes (' ')) throw new InvalidAddress (this.id + ' address is invalid or has less than ' + this.minFundingAddressLength.toString () + ' characters: "' + this.json (address) + '"') return address } initRestRateLimiter () { if (this.rateLimit === undefined) throw new Error (this.id + '.rateLimit property is not configured') this.tokenBucket = this.extend ({ refillRate: 1 / this.rateLimit, delay: 1, capacity: 1, defaultCost: 1, maxCapacity: 1000, }, this.tokenBucket) this.throttle = throttle (this.tokenBucket) this.executeRestRequest = (url, method = 'GET', headers = undefined, body = undefined) => { // fetchImplementation cannot be called on this. in browsers: // TypeError Failed to execute 'fetch' on 'Window': Illegal invocation const fetchImplementation = this.fetchImplementation const params = { method, headers, body, timeout: this.timeout } if (url.indexOf ('http://') < 0) { params['agent'] = this.agent || null; } let promise = fetchImplementation (url, this.extend (params, this.fetchOptions)) .catch (e => { if (isNode) throw new ExchangeNotAvailable ([ this.id, method, url, e.type, e.message ].join (' ')) throw e // rethrow all unknown errors }) .then (response => this.handleRestResponse (response, url, method, headers, body)) return timeout (this.timeout, promise).catch (e => { if (e instanceof TimedOut) throw new RequestTimeout (this.id + ' ' + method + ' ' + url + ' request timed out (' + this.timeout + ' ms)') throw e }) } } setSandboxMode (enabled) { if (!!enabled) { if ('test' in this.urls) { this.urls['api_backup'] = clone (this.urls['api']) this.urls['api'] = clone (this.urls['test']) } else { throw new NotSupported (this.id + ' does not have a sandbox URL') } } else if ('api_backup' in this.urls) { this.urls['api'] = clone (this.urls['api_backup']) } } defineRestApi (api, methodName, options = {}) { for (const type of Object.keys (api)) { for (const httpMethod of Object.keys (api[type])) { let paths = api[type][httpMethod] for (let i = 0; i < paths.length; i++) { let path = paths[i].trim () let splitPath = path.split (/[^a-zA-Z0-9]/) let uppercaseMethod = httpMethod.toUpperCase () let lowercaseMethod = httpMethod.toLowerCase () let camelcaseMethod = this.capitalize (lowercaseMethod) let camelcaseSuffix = splitPath.map (this.capitalize).join ('') let underscoreSuffix = splitPath.map (x => x.trim ().toLowerCase ()).filter (x => x.length > 0).join ('_') let camelcase = type + camelcaseMethod + this.capitalize (camelcaseSuffix) let underscore = type + '_' + lowercaseMethod + '_' + underscoreSuffix if ('suffixes' in options) { if ('camelcase' in options['suffixes']) camelcase += options['suffixes']['camelcase'] if ('underscore' in options.suffixes) underscore += options['suffixes']['underscore'] } if ('underscore_suffix' in options) underscore += options.underscoreSuffix; if ('camelcase_suffix' in options) camelcase += options.camelcaseSuffix; let partial = async params => this[methodName] (path, type, uppercaseMethod, params || {}) this[camelcase] = partial this[underscore] = partial } } } } fetch (url, method = 'GET', headers = undefined, body = undefined) { if (isNode && this.userAgent) { if (typeof this.userAgent === 'string') headers = extend ({ 'User-Agent': this.userAgent }, headers) else if ((typeof this.userAgent === 'object') && ('User-Agent' in this.userAgent)) headers = extend (this.userAgent, headers) } if (typeof this.proxy === 'function') { url = this.proxy (url) if (isNode) headers = extend ({ 'Origin': this.origin }, headers) } else if (typeof this.proxy === 'string') { if (this.proxy.length) if (isNode) headers = extend ({ 'Origin': this.origin }, headers) url = this.proxy + url } headers = extend (this.headers, headers) if (this.verbose) console.log ("fetch:\n", this.id, method, url, "\nRequest:\n", headers, "\n", body, "\n") return this.executeRestRequest (url, method, headers, body) } async fetch2 (path, type = 'public', method = 'GET', params = {}, headers = undefined, body = undefined) { if (this.enableRateLimit) await this.throttle () let request = this.sign (path, type, method, params, headers, body) return this.fetch (request.url, request.method, request.headers, request.body) } request (path, type = 'public', method = 'GET', params = {}, headers = undefined, body = undefined) { return this.fetch2 (path, type, method, params, headers, body) } parseJson (jsonString) { return JSON.parse (jsonString) } parseRestResponse (response, responseBody, url, method) { try { return (responseBody.length > 0) ? this.parseJson (responseBody) : {} // empty object for empty body } catch (e) { if (this.verbose) console.log ('parseJson:\n', this.id, method, url, response.status, 'error', e, "response body:\n'" + responseBody + "'\n") let title = undefined let match = responseBody.match (/<title>([^<]+)/i) if (match) title = match[1].trim (); let maintenance = responseBody.match (/offline|busy|retry|wait|unavailable|maintain|maintenance|maintenancing/i) let ddosProtection = responseBody.match (/cloudflare|incapsula|overload|ddos/i) if (e instanceof SyntaxError) { let ExceptionClass = ExchangeNotAvailable let details = 'not accessible from this location at the moment' if (maintenance) details = 'offline, on maintenance or unreachable from this location at the moment' // http error codes proxied by cloudflare are not really DDoSProtection errors (mostly) if ((response.status < 500) && (ddosProtection)) { ExceptionClass = DDoSProtection } throw new ExceptionClass ([ this.id, method, url, response.status, title, details ].join (' ')) } throw e } } // a helper for matching error strings exactly vs broadly findBroadlyMatchedKey (broad, string) { const keys = Object.keys (broad); for (let i = 0; i < keys.length; i++) { const key = keys[i]; if (string.indexOf (key) >= 0) return key; } return undefined; } handleErrors (statusCode, statusText, url, method, responseHeaders, responseBody, response) { // override me } defaultErrorHandler (response, responseBody, url, method) { let { status: code, statusText: reason } = response if ((code >= 200) && (code <= 299)) return let error = undefined let details = responseBody let match = responseBody.match (/<title>([^<]+)/i) if (match) details = match[1].trim (); error = this.httpExceptions[code.toString ()] || ExchangeError if (error === ExchangeNotAvailable) { if (responseBody.match (/cloudflare|incapsula|overload|ddos/i)) { error = DDoSProtection } else { details += ' (possible reasons: ' + [ 'invalid API keys', 'bad or old nonce', 'exchange is down or offline', 'on maintenance', 'DDoS protection', 'rate-limiting', ].join (', ') + ')' } } throw new error ([ this.id, method, url, code, reason, details ].join (' ')) } isJsonEncodedObject (object) { return ((typeof object === 'string') && (object.length >= 2) && ((object[0] === '{') || (object[0] === '['))) } getResponseHeaders (response) { let result = {} response.headers.forEach ((value, key) => { key = key.split ('-').map (word => capitalize (word)).join ('-') result[key] = value }) return result } handleRestResponse (response, url, method = 'GET', requestHeaders = undefined, requestBody = undefined) { return response.text ().then ((responseBody) => { const shouldParseJson = this.isJsonEncodedObject (responseBody) && !this.skipJsonOnStatusCodes.includes (response.status) const json = shouldParseJson ? this.parseRestResponse (response, responseBody, url, method) : undefined let responseHeaders = this.getResponseHeaders (response) if (this.enableLastResponseHeaders) { this.last_response_headers = responseHeaders } if (this.enableLastHttpResponse) { this.last_http_response = responseBody // FIXME: for those classes that haven't switched to handleErrors yet } if (this.enableLastJsonResponse) { this.last_json_response = json // FIXME: for those classes that haven't switched to handleErrors yet } if (this.verbose) console.log ("handleRestResponse:\n", this.id, method, url, response.status, response.statusText, "\nResponse:\n", responseHeaders, "\n", responseBody, "\n") const args = [ response.status, response.statusText, url, method, responseHeaders, responseBody, json ] this.handleErrors (...args) this.defaultErrorHandler (response, responseBody, url, method) return json || responseBody }) } setMarkets (markets, currencies = undefined) { let values = Object.values (markets).map (market => deepExtend ({ 'limits': this.limits, 'precision': this.precision, }, this.fees['trading'], market)) this.markets = deepExtend (this.markets, indexBy (values, 'symbol')) this.marketsById = indexBy (markets, 'id') this.markets_by_id = this.marketsById this.symbols = Object.keys (this.markets).sort () this.ids = Object.keys (this.markets_by_id).sort () if (currencies) { this.currencies = deepExtend (currencies, this.currencies) } else { const baseCurrencies = values.filter (market => 'base' in market) .map (market => ({ id: market.baseId || market.base, numericId: (market.baseNumericId !== undefined) ? market.baseNumericId : undefined, code: market.base, precision: market.precision ? (market.precision.base || market.precision.amount) : 8, })) const quoteCurrencies = values.filter (market => 'quote' in market) .map (market => ({ id: market.quoteId || market.quote, numericId: (market.quoteNumericId !== undefined) ? market.quoteNumericId : undefined, code: market.quote, precision: market.precision ? (market.precision.quote || market.precision.price) : 8, })) const allCurrencies = baseCurrencies.concat (quoteCurrencies) const groupedCurrencies = groupBy (allCurrencies, 'code') const currencies = Object.keys (groupedCurrencies).map (code => groupedCurrencies[code].reduce ((previous, current) => ((previous.precision > current.precision) ? previous : current), groupedCurrencies[code][0])) const sortedCurrencies = sortBy (flatten (currencies), 'code') this.currencies = deepExtend (indexBy (sortedCurrencies, 'code'), this.currencies) } this.currencies_by_id = indexBy (this.currencies, 'id') return this.markets } async loadMarkets (reload = false, params = {}) { if (!reload && this.markets) { if (!this.markets_by_id) { return this.setMarkets (this.markets) } return this.markets } const markets = await this.fetchMarkets (params) let currencies = undefined if (this.has.fetchCurrencies) { currencies = await this.fetchCurrencies () } return this.setMarkets (markets, currencies) } fetchBidsAsks (symbols = undefined, params = {}) { throw new NotSupported (this.id + ' fetchBidsAsks not supported yet') } async fetchOHLCVC (symbol, timeframe = '1m', since = undefined, limits = undefined, params = {}) { if (!this.has['fetchTrades']) throw new NotSupported (this.id + ' fetchOHLCV() not supported yet') await this.loadMarkets () let trades = await this.fetchTrades (symbol, since, limits, params) let ohlcvc = buildOHLCVC (trades, timeframe, since, limits) return ohlcvc } async fetchOHLCV (symbol, timeframe = '1m', since = undefined, limits = undefined, params = {}) { if (!this.has['fetchTrades']) throw new NotSupported (this.id + ' fetchOHLCV() not supported yet') await this.loadMarkets () let trades = await this.fetchTrades (symbol, since, limits, params) let ohlcvc = buildOHLCVC (trades, timeframe, since, limits) return ohlcvc.map (c => c.slice (0, -1)) } parseTradingViewOHLCV (ohlcvs, market = undefined, timeframe = '1m', since = undefined, limit = undefined) { let result = this.convertTradingViewToOHLCV (ohlcvs); return this.parseOHLCVs (result, market, timeframe, since, limit); } convertTradingViewToOHLCV (ohlcvs) { let result = []; for (let i = 0; i < ohlcvs['t'].length; i++) { result.push ([ ohlcvs['t'][i] * 1000, ohlcvs['o'][i], ohlcvs['h'][i], ohlcvs['l'][i], ohlcvs['c'][i], ohlcvs['v'][i], ]); } return result; } convertOHLCVToTradingView (ohlcvs) { let result = { 't': [], 'o': [], 'h': [], 'l': [], 'c': [], 'v': [], }; for (let i = 0; i < ohlcvs.length; i++) { result['t'].push (parseInt (ohlcvs[i][0] / 1000)); result['o'].push (ohlcvs[i][1]); result['h'].push (ohlcvs[i][2]); result['l'].push (ohlcvs[i][3]); result['c'].push (ohlcvs[i][4]); result['v'].push (ohlcvs[i][5]); } return result; } fetchTickers (symbols = undefined, params = {}) { throw new NotSupported (this.id + ' fetchTickers not supported yet') } purgeCachedOrders (before) { const orders = Object .values (this.orders) .filter (order => (order.status === 'open') || (order.timestamp >= before)) this.orders = indexBy (orders, 'id') return this.orders } fetchOrder (id, symbol = undefined, params = {}) { throw new NotSupported (this.id + ' fetchOrder not supported yet'); } fetchOrders (symbol = undefined, since = undefined, limit = undefined, params = {}) { throw new NotSupported (this.id + ' fetchOrders not supported yet'); } fetchOpenOrders (symbol = undefined, since = undefined, limit = undefined, params = {}) { throw new NotSupported (this.id + ' fetchOpenOrders not supported yet'); } fetchClosedOrders (symbol = undefined, since = undefined, limit = undefined, params = {}) { throw new NotSupported (this.id + ' fetchClosedOrders not supported yet'); } fetchMyTrades (symbol = undefined, since = undefined, limit = undefined, params = {}) { throw new NotSupported (this.id + ' fetchMyTrades not supported yet'); } fetchTransactions (symbol = undefined, since = undefined, limit = undefined, params = {}) { throw new NotSupported (this.id + ' fetchTransactions not supported yet'); } fetchDeposits (symbol = undefined, since = undefined, limit = undefined, params = {}) { throw new NotSupported (this.id + ' fetchDeposits not supported yet'); } fetchWithdrawals (symbol = undefined, since = undefined, limit = undefined, params = {}) { throw new NotSupported (this.id + ' fetchWithdrawals not supported yet'); } fetchCurrencies (params = {}) { // markets are returned as a list // currencies are returned as a dict // this is for historical reasons // and may be changed for consistency later return new Promise ((resolve, reject) => resolve (this.currencies)); } fetchMarkets (params = {}) { // markets are returned as a list // currencies are returned as a dict // this is for historical reasons // and may be changed for consistency later return new Promise ((resolve, reject) => resolve (Object.values (this.markets))) } async fetchOrderStatus (id, symbol = undefined, params = {}) { let order = await this.fetchOrder (id, symbol, params); return order['status']; } account () { return { 'free': 0.0, 'used': 0.0, 'total': 0.0, } } commonCurrencyCode (currency) { if (!this.substituteCommonCurrencyCodes) return currency return this.safeString (this.commonCurrencies, currency, currency) } currencyId (commonCode) { if (this.currencies === undefined) { throw new ExchangeError (this.id + ' currencies not loaded') } if (commonCode in this.currencies) { return this.currencies[commonCode]['id']; } let currencyIds = {} let distinct = Object.keys (this.commonCurrencies) for (let i = 0; i < distinct.length; i++) { let k = distinct[i] currencyIds[this.commonCurrencies[k]] = k } return this.safeString (currencyIds, commonCode, commonCode) } currency (code) { if (this.currencies === undefined) throw new ExchangeError (this.id + ' currencies not loaded') if ((typeof code === 'string') && (code in this.currencies)) return this.currencies[code] throw new ExchangeError (this.id + ' does not have currency code ' + code) } findMarket (string) { if (this.markets === undefined) throw new ExchangeError (this.id + ' markets not loaded') if (typeof string === 'string') { if (string in this.markets_by_id) return this.markets_by_id[string] if (string in this.markets) return this.markets[string] } return string } findSymbol (string, market = undefined) { if (market === undefined) market = this.findMarket (string) if (typeof market === 'object') return market['symbol'] return string } market (symbol) { if (this.markets === undefined) throw new ExchangeError (this.id + ' markets not loaded') if ((typeof symbol === 'string') && (symbol in this.markets)) return this.markets[symbol] throw new ExchangeError (this.id + ' does not have market symbol ' + symbol) } marketId (symbol) { let market = this.market (symbol) return (market !== undefined ? market['id'] : symbol) } marketIds (symbols) { return symbols.map (symbol => this.marketId (symbol)); } symbol (symbol) { return this.market (symbol).symbol || symbol } extractParams (string) { let re = /{([\w-]+)}/g let matches = [] let match = re.exec (string) while (match) { matches.push (match[1]) match = re.exec (string) } return matches } implodeParams (string, params) { for (let property in params) string = string.replace ('{' + property + '}', params[property]) return string } url (path, params = {}) { let result = this.implodeParams (path, params); let query = this.omit (params, this.extractParams (path)) if (Object.keys (query).length) result += '?' + this.urlencode (query) return result } parseBidAsk (bidask, priceKey = 0, amountKey = 1) { let price = parseFloat (bidask[priceKey]) let amount = parseFloat (bidask[amountKey]) return [ price, amount ] } parseBidsAsks (bidasks, priceKey = 0, amountKey = 1) { return Object.values (bidasks || []).map (bidask => this.parseBidAsk (bidask, priceKey, amountKey)) } async fetchL2OrderBook (symbol, limit = undefined, params = {}) { let orderbook = await this.fetchOrderBook (symbol, limit, params) return extend (orderbook, { 'bids': sortBy (aggregate (orderbook.bids), 0, true), 'asks': sortBy (aggregate (orderbook.asks), 0), }) } parseOrderBook (orderbook, timestamp = undefined, bidsKey = 'bids', asksKey = 'asks', priceKey = 0, amountKey = 1) { return { 'bids': sortBy ((bidsKey in orderbook) ? this.parseBidsAsks (orderbook[bidsKey], priceKey, amountKey) : [], 0, true), 'asks': sortBy ((asksKey in orderbook) ? this.parseBidsAsks (orderbook[asksKey], priceKey, amountKey) : [], 0), 'timestamp': timestamp, 'datetime': this.iso8601 (timestamp), 'nonce': undefined, } } getCurrencyUsedOnOpenOrders (currency) { return Object.values (this.orders).filter (order => (order['status'] === 'open')).reduce ((total, order) => { let symbol = order['symbol']; let market = this.markets[symbol]; let remaining = order['remaining'] if (currency === market['base'] && order['side'] === 'sell') { return total + remaining } else if (currency === market['quote'] && order['side'] === 'buy') { return total + (order['price'] * remaining) } else { return total } }, 0) } parseBalance (balance) { const currencies = Object.keys (this.omit (balance, 'info')); currencies.forEach ((currency) => { if (balance[currency].free !== undefined && balance[currency].used === undefined) { // exchange reports only 'free' balance -> try to derive 'used' funds from open orders cache if (this.dontGetUsedBalanceFromStaleCache && ('open_orders' in balance['info'])) { // liqui exchange reports number of open orders with balance response // use it to validate the cache const exchangeOrdersCount = balance['info']['open_orders']; const cachedOrdersCount = Object.values (this.orders).filter (order => (order['status'] === 'open')).length; if (cachedOrdersCount === exchangeOrdersCount) { balance[currency].used = this.getCurrencyUsedOnOpenOrders (currency) balance[currency].total = (balance[currency].used || 0) + (balance[currency].free || 0) } } else { balance[currency].used = this.getCurrencyUsedOnOpenOrders (currency) balance[currency].total = (balance[currency].used || 0) + (balance[currency].free || 0) } } [ 'free', 'used', 'total' ].forEach ((account) => { balance[account] = balance[account] || {} balance[account][currency] = balance[currency][account] }) }) return balance } async fetchPartialBalance (part, params = {}) { let balance = await this.fetchBalance (params) return balance[part] } fetchFreeBalance (params = {}) { return this.fetchPartialBalance ('free', params) } fetchUsedBalance (params = {}) { return this.fetchPartialBalance ('used', params) } fetchTotalBalance (params = {}) { return this.fetchPartialBalance ('total', params) } async loadTradingLimits (symbols = undefined, reload = false, params = {}) { if (this.has['fetchTradingLimits']) { if (reload || !('limitsLoaded' in this.options)) { let response = await this.fetchTradingLimits (symbols); for (let i = 0; i < symbols.length; i++) { let symbol = symbols[i]; this.markets[symbol] = this.deepExtend (this.markets[symbol], response[symbol]); } this.options['limitsLoaded'] = this.milliseconds (); } } return this.markets; } filterBySinceLimit (array, since = undefined, limit = undefined) { if (since !== undefined && since !== null) array = array.filter (entry => entry.timestamp >= since) if (limit !== undefined && limit !== null) array = array.slice (0, limit) return array } filterByValueSinceLimit (array, field, value = undefined, since = undefined, limit = undefined) { const valueIsDefined = value !== undefined && value !== null const sinceIsDefined = since !== undefined && since !== null // single-pass filter for both symbol and since if (valueIsDefined || sinceIsDefined) array = Object.values (array).filter (entry => ((valueIsDefined ? (entry[field] === value) : true) && (sinceIsDefined ? (entry.timestamp >= since) : true))) if (limit !== undefined && limit !== null) array = Object.values (array).slice (0, limit) return array } filterBySymbolSinceLimit (array, symbol = undefined, since = undefined, limit = undefined) { return this.filterByValueSinceLimit (array, 'symbol', symbol, since, limit) } filterByCurrencySinceLimit (array, code = undefined, since = undefined, limit = undefined) { return this.filterByValueSinceLimit (array, 'currency', code, since, limit) } filterByArray (objects, key, values = undefined, indexed = true) { objects = Object.values (objects) // return all of them if no values were passed if (values === undefined || values === null) return indexed ? indexBy (objects, key) : objects let result = [] for (let i = 0; i < objects.length; i++) { if (values.includes (objects[i][key])) result.push (objects[i]) } return indexed ? indexBy (result, key) : result } parseTrades (trades, market = undefined, since = undefined, limit = undefined) { // this code is commented out temporarily to catch for exchange-specific errors // if (!this.isArray (trades)) { // throw new ExchangeError (this.id + ' parseTrades expected an array in the trades argument, but got ' + typeof trades); // } let result = Object.values (trades || []).map (trade => this.parseTrade (trade, market)) result = sortBy (result, 'timestamp') let symbol = (market !== undefined) ? market['symbol'] : undefined return this.filterBySymbolSinceLimit (result, symbol, since, limit) } parseTransactions (transactions, currency = undefined, since = undefined, limit = undefined) { // this code is commented out temporarily to catch for exchange-specific errors // if (!this.isArray (transactions)) { // throw new ExchangeError (this.id + ' parseTransactions expected an array in the transactions argument, but got ' + typeof transactions); // } let result = Object.values (transactions || []).map (transaction => this.parseTransaction (transaction, currency)) result = this.sortBy (result, 'timestamp'); let code = (currency !== undefined) ? currency['code'] : undefined; return this.filterByCurrencySinceLimit (result, code, since, limit); } parseOrders (orders, market = undefined, since = undefined, limit = undefined) { // this code is commented out temporarily to catch for exchange-specific errors // if (!this.isArray (orders)) { // throw new ExchangeError (this.id + ' parseOrders expected an array in the orders argument, but got ' + typeof orders); // } let result = Object.values (orders).map (order => this.parseOrder (order, market)) result = sortBy (result, 'timestamp') let symbol = (market !== undefined) ? market['symbol'] : undefined return this.filterBySymbolSinceLimit (result, symbol, since, limit) } filterBySymbol (array, symbol = undefined) { return ((symbol !== undefined) ? array.filter (entry => entry.symbol === symbol) : array) } parseOHLCV (ohlcv, market = undefined, timeframe = '1m', since = undefined, limit = undefined) { return Array.isArray (ohlcv) ? ohlcv.slice (0, 6) : ohlcv } parseOHLCVs (ohlcvs, market = undefined, timeframe = '1m', since = undefined, limit = undefined) { // this code is commented out temporarily to catch for exchange-specific errors // if (!this.isArray (ohlcvs)) { // throw new ExchangeError (this.id + ' parseOHLCVs expected an array in the ohlcvs argument, but got ' + typeof ohlcvs); // } ohlcvs = Object.values (ohlcvs || []) let result = [] for (let i = 0; i < ohlcvs.length; i++) { if (limit && (result.length >= limit)) break; let ohlcv = this.parseOHLCV (ohlcvs[i], market, timeframe, since, limit) if (since && (ohlcv[0] < since)) continue result.push (ohlcv) } return this.sortBy (result, 0) } editLimitBuyOrder (id, symbol, ...args) { return this.editLimitOrder (id, symbol, 'buy', ...args) } editLimitSellOrder (id, symbol, ...args) { return this.editLimitOrder (id, symbol, 'sell', ...args) } editLimitOrder (id, symbol, ...args) { return this.editOrder (id, symbol, 'limit', ...args) } async editOrder (id, symbol, ...args) { if (!this.enableRateLimit) throw new ExchangeError (this.id + ' editOrder() requires enableRateLimit = true') await this.cancelOrder (id, symbol); return this.createOrder (symbol, ...args) } createLimitOrder (symbol, ...args) { return this.createOrder (symbol, 'limit', ...args) } createMarketOrder (symbol, ...args) { return this.createOrder (symbol, 'market', ...args) } createLimitBuyOrder (symbol, ...args) { return this.createOrder (symbol, 'limit', 'buy', ...args) } createLimitSellOrder (symbol, ...args) { return this.createOrder (symbol, 'limit', 'sell', ...args) } createMarketBuyOrder (symbol, amount, params = {}) { return this.createOrder (symbol, 'market', 'buy', amount, undefined, params) } createMarketSellOrder (symbol, amount, params = {}) { return this.createOrder (symbol, 'market', 'sell', amount, undefined, params) } costToPrecision (symbol, cost) { return decimalToPrecision (cost, ROUND, this.markets[symbol].precision.price, this.precisionMode) } priceToPrecision (symbol, price) { return decimalToPrecision (price, ROUND, this.markets[symbol].precision.price, this.precisionMode) } amountToPrecision (symbol, amount) { return decimalToPrecision (amount, TRUNCATE, this.markets[symbol].precision.amount, this.precisionMode) } feeToPrecision (symbol, fee) { return decimalToPrecision (fee, ROUND, this.markets[symbol].precision.price, this.precisionMode) } currencyToPrecision (currency, fee) { return this.decimalToPrecision (fee, ROUND, this.currencies[currency]['precision'], this.precisionMode); } calculateFee (symbol, type, side, amount, price, takerOrMaker = 'taker', params = {}) { let market = this.markets[symbol] let rate = market[takerOrMaker] let cost = parseFloat (this.costToPrecision (symbol, amount * price)) return { 'type': takerOrMaker, 'currency': market['quote'], 'rate': rate, 'cost': parseFloat (this.feeToPrecision (symbol, rate * cost)), } } mdy (timestamp, infix = '-') { infix = infix || '' let date = new Date (timestamp) let Y = date.getUTCFullYear ().toString () let m = date.getUTCMonth () + 1 let d = date.getUTCDate () m = m < 10 ? ('0' + m) : m.toString () d = d < 10 ? ('0' + d) : d.toString () return m + infix + d + infix + Y } ymd (timestamp, infix = '-') { infix = infix || '' let date = new Date (timestamp) let Y = date.getUTCFullYear ().toString () let m = date.getUTCMonth () + 1 let d = date.getUTCDate () m = m < 10 ? ('0' + m) : m.toString () d = d < 10 ? ('0' + d) : d.toString () return Y + infix + m + infix + d } ymdhms (timestamp, infix = ' ') { let date = new Date (timestamp) let Y = date.getUTCFullYear () let m = date.getUTCMonth () + 1 let d = date.getUTCDate () let H = date.getUTCHours () let M = date.getUTCMinutes () let S = date.getUTCSeconds () m = m < 10 ? ('0' + m) : m d = d < 10 ? ('0' + d) : d H = H < 10 ? ('0' + H) : H M = M < 10 ? ('0' + M) : M S = S < 10 ? ('0' + S) : S return Y + '-' + m + '-' + d + infix + H + ':' + M + ':' + S } // ------------------------------------------------------------------------ // web3 / 0x methods static hasWeb3 () { return Web3 && ethUtil && ethAbi && BigNumber } checkRequiredDependencies () { if (!Exchange.hasWeb3 ()) { throw new ExchangeError ('The following npm modules are r