preidman-ccxt
Version:
A JavaScript / Python / PHP cryptocurrency trading library with support for 100+ exchanges
1,331 lines (1,100 loc) • 60.6 kB
JavaScript
"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