UNPKG

ccxt

Version:

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

446 lines (372 loc) • 16.7 kB
#! /usr/bin/env node import fs from 'fs' import path from 'path' import ansi from 'ansicolor' import asTable from 'as-table' import ololog from 'ololog' import ccxt from '../../js/ccxt.js' import { Agent } from 'https' import { add_static_result } from '../../utils/update-static-tests-data.js' const fsPromises = fs.promises; ansi.nice const log = ololog.configure ({ locate: false }).unlimited const { ExchangeError , NetworkError} = ccxt function jsonStringify (obj, indent = undefined) { return JSON.stringify (obj, function(k, v) { return v === undefined ? null : v; }, indent); } //----------------------------------------------------------------------------- let [processPath, , exchangeId, methodName, ... params] = process.argv.filter (x => !x.startsWith ('--')) , verbose = process.argv.includes ('--verbose') , debug = process.argv.includes ('--debug') , poll = process.argv.includes ('--poll') , no_send = process.argv.includes ('--no-send') , no_load_markets = process.argv.includes ('--no-load-markets') , details = process.argv.includes ('--details') , no_table = process.argv.includes ('--no-table') , table = process.argv.includes ('--table') , iso8601 = process.argv.includes ('--iso8601') , cors = process.argv.includes ('--cors') , cache_markets = process.argv.includes ('--cache-markets') , testnet = process.argv.includes ('--test') || process.argv.includes ('--testnet') || process.argv.includes ('--sandbox') , signIn = process.argv.includes ('--sign-in') || process.argv.includes ('--signIn') , isSpot = process.argv.includes ('--spot') , isSwap = process.argv.includes ('--swap') , isFuture = process.argv.includes ('--future') , isOption = process.argv.includes ('--option') , shouldCreateRequestReport = process.argv.includes ('--report') || process.argv.includes ('--request') , shouldCreateResponseReport = process.argv.includes ('--response') , shouldCreateBoth = process.argv.includes ('--static') , raw = process.argv.includes ('--raw') , noKeys = process.argv.includes ('--no-keys') let foundDescription = undefined; for (let i = 0; i < process.argv.length; i++) { if (process.argv[i] === '--name') { foundDescription = process.argv[i + 1]; // search that string in `params` and remove it for (let j = 0; j < params.length; j++) { if (params[j] === foundDescription) { params.splice(j, 1); break; } } break; } } //----------------------------------------------------------------------------- if (!raw) { log ((new Date ()).toISOString()) log ('Node.js:', process.version) log ('CCXT v' + ccxt.version) } //----------------------------------------------------------------------------- process.on ('uncaughtException', e => { log.bright.red.error (e); log.red.error (e.message); process.exit (1) }) process.on ('unhandledRejection', e => { log.bright.red.error (e); log.red.error (e.message); process.exit (1) }) //----------------------------------------------------------------------------- // set up keys and settings, if any const keysGlobal = path.resolve ('keys.json') const keysLocal = path.resolve ('keys.local.json') const keysFile = fs.existsSync (keysLocal) ? keysLocal : keysGlobal const settingsFile = fs.readFileSync(keysFile); // eslint-disable-next-line import/no-dynamic-require, no-path-concat let settings = JSON.parse(settingsFile) settings = settings[exchangeId] || {} //----------------------------------------------------------------------------- const timeout = 30000 let exchange = undefined const httpsAgent = new Agent ({ ecdhCurve: 'auto', keepAlive: true, }) // check here if we have a arg like this: binance.fetchOrders() const callRegex = /\s*(\w+)\s*\.\s*(\w+)\s*\(([^()]*)\)/ if (callRegex.test (exchangeId)) { const res = callRegex.exec (exchangeId); exchangeId = res[1]; methodName = res[2]; params = res[3].split(",").map(x => x.trim()); } try { if (ccxt.pro.exchanges.includes(exchangeId)) { exchange = new (ccxt.pro)[exchangeId] ({ timeout, httpsAgent, ... settings }) } else { exchange = new (ccxt)[exchangeId] ({ timeout, httpsAgent, ... settings }) } if (exchange === undefined) { process.exit () } if (isSpot) { exchange.options['defaultType'] = 'spot'; } else if (isSwap) { exchange.options['defaultType'] = 'swap'; } else if (isFuture) { exchange.options['defaultType'] = 'future'; } else if (isOption) { exchange.options['defaultType'] = 'option'; } if (!noKeys) { // check auth keys in env var const requiredCredentials = exchange.requiredCredentials; for (const [credential, isRequired] of Object.entries (requiredCredentials)) { if (isRequired && exchange[credential] === undefined) { const credentialEnvName = (exchangeId + '_' + credential).toUpperCase () // example: KRAKEN_APIKEY let credentialValue = process.env[credentialEnvName] if (credentialValue) { if (credentialValue.indexOf('---BEGIN') > -1) { credentialValue = credentialValue.replaceAll('\\n', '\n'); } exchange[credential] = credentialValue } } } } if (testnet) { exchange.setSandboxMode (true) } } catch (e) { log.red (e) printUsage () process.exit () } //----------------------------------------------------------------------------- function createRequestTemplate(exchange, methodName, args, result) { const final = { 'description': 'Fill this with a description of the method call', 'method': methodName, 'url': exchange.last_request_url ?? '', 'input': args, 'output': exchange.last_request_body ?? undefined } log('Report: (paste inside static/request/' + exchange.id + '.json ->' + methodName + ')') log.green('-------------------------------------------') log (JSON.stringify (final, null, 2)) log.green('-------------------------------------------') if (foundDescription !== undefined) { final.description = foundDescription; log.green('auto-saving static result'); add_static_result('request', exchange.id, methodName, final); } } //----------------------------------------------------------------------------- function createResponseTemplate(exchange, methodName, args, result) { const final = { 'description': 'Fill this with a description of the method call', 'method': methodName, 'input': args, 'httpResponse': exchange.last_json_response ?? exchange.last_http_response, 'parsedResponse': result } log('Report: (paste inside static/response/' + exchange.id + '.json ->' + methodName + ')') log.green('-------------------------------------------') log (jsonStringify (final, 2)) log.green('-------------------------------------------') if (foundDescription !== undefined) { final.description = foundDescription; log.green('auto-saving static result'); add_static_result('response', exchange.id, methodName, final); } } //----------------------------------------------------------------------------- function printSupportedExchanges () { log ('Supported exchanges:', ccxt.exchanges.join (', ').green) } //----------------------------------------------------------------------------- function printUsage () { log ('This is an example of a basic command-line interface to all exchanges') log ('Usage: node', process.argv[1], 'id'.green, 'method'.yellow, '"param1" param2 "param3" param4 ...'.blue) log ('Examples:') log ('node', process.argv[1], 'okcoin fetchOHLCV BTC/USD 15m') log ('node', process.argv[1], 'bitfinex fetchBalance') log ('node', process.argv[1], 'kraken fetchOrderBook ETH/BTC') printSupportedExchanges () log ('Supported options:') log ('--verbose Print verbose output') log ('--debug Print debugging output') log ('--poll Repeat continuously in rate-limited mode') log ('--no-send Print the request but do not actually send it to the exchange (sets verbose and load-markets)') log ('--no-load-markets Do not pre-load markets (for debugging)') log ('--details Print detailed fetch responses') log ('--no-table Do not print the fetch response as a table') log ('--table Print the fetch response as a table') log ('--iso8601 Print timestamps as ISO8601 datetimes') log ('--cors use CORS proxy for debugging') log ('--sign-in Call signIn() if any') log ('--sandbox Use the exchange sandbox if available, same as --testnet') log ('--testnet Use the exchange testnet if available, same as --sandbox') log ('--test Use the exchange testnet if available, same as --sandbox') log ('--cache-markets Cache the loaded markets in the .cache folder in the current directory') } //----------------------------------------------------------------------------- const printHumanReadable = (exchange, result) => { if (raw) { return log (jsonStringify (result)) } if (!no_table && Array.isArray (result) || table) { result = Object.values (result) let arrayOfObjects = (typeof result[0] === 'object') if (details) result.forEach (object => { if (arrayOfObjects) log ('-------------------------------------------') log (object) }) if (arrayOfObjects || table && Array.isArray (result)) { const configuredAsTable = asTable.configure ({ delimiter: ' | '.lightGray.dim, right: true, title: x => String (x).lightGray, dash: '-'.lightGray.dim, print: x => { if (typeof x === 'object') { const j = jsonStringify (x).trim () if (j.length < 100) return j } return String (x) } }) log (result.length > 0 ? configuredAsTable (result.map (rawElement => { const element = Object.assign ({}, rawElement) let keys = Object.keys (element) delete element['info'] keys.forEach (key => { if (!iso8601) return element[key] try { const iso8601 = exchange.iso8601 (element[key]) if (iso8601.match (/^20[0-9]{2}[-]?/)) element[key] = iso8601 else throw new Error ('wrong date') } catch (e) { return element[key] } }) return element })) : result) log (result.length, 'objects'); } else { console.dir (result, { depth: null }) log (result.length, 'objects'); } } else { console.dir (result, { depth: null, maxArrayLength: null }) } } //----------------------------------------------------------------------------- async function run () { if (!exchangeId) { printUsage () } else { let args = params .map (s => s.match (/^[0-9]{4}[-][0-9]{2}[-][0-9]{2}[T\s]?[0-9]{2}[:][0-9]{2}[:][0-9]{2}/g) ? exchange.parse8601 (s) : s) .map (s => (() => { if (s.match ( /^\d+$/g)) return s < Number.MAX_SAFE_INTEGER ? Number (s) : s try {return eval ('(() => (' + s + ')) ()') } catch (e) { return s } }) ()) const www = Array.isArray (exchange.urls.www) ? exchange.urls.www[0] : exchange.urls.www if (cors) { exchange.proxy = 'https://cors-anywhere.herokuapp.com/'; exchange.origin = exchange.uuid () } no_load_markets = no_send ? true : no_load_markets if (debug) { exchange.verbose = verbose } const path = '.cache/' + exchangeId + '-markets.json' if (!no_load_markets) { try { await fsPromises.access (path, fs.constants.R_OK) exchange.markets = JSON.parse (await fsPromises.readFile (path)) } catch { await exchange.loadMarkets () if (cache_markets) { await fsPromises.writeFile (path, jsonStringify (exchange.markets)) } } } if (signIn && exchange.has.signIn) { await exchange.signIn () } exchange.verbose = verbose if (no_send) { exchange.verbose = no_send exchange.fetch = function fetch (url, method = 'GET', headers = undefined, body = undefined) { log.dim.noLocate ('-------------------------------------------') log.dim.noLocate (exchange.iso8601 (exchange.milliseconds ())) log.green.unlimited ({ url, method, headers, body, }) } } if (methodName) { if (typeof exchange[methodName] === 'function') { if (!raw) log (exchange.id + '.' + methodName, '(' + args.join (', ') + ')') let start = exchange.milliseconds () let end = exchange.milliseconds () let i = 0; let isWsMethod = false if (methodName.startsWith("watch")) { // handle WS methods isWsMethod = true; } while (true) { try { const result = await exchange[methodName] (... args) end = exchange.milliseconds () if (!isWsMethod && !raw) { log (exchange.iso8601 (end), 'iteration', i++, 'passed in', end - start, 'ms\n') } printHumanReadable (exchange, result) if (!isWsMethod && !raw) { log (exchange.iso8601 (end), 'iteration', i, 'passed in', end - start, 'ms\n') } if (shouldCreateRequestReport || shouldCreateBoth) { createRequestTemplate(exchange, methodName, args, result) } if (shouldCreateResponseReport || shouldCreateBoth) { createResponseTemplate(exchange, methodName, args, result) } start = end } catch (e) { if (e instanceof ExchangeError) { log.red (e.constructor.name, e.message) } else if (e instanceof NetworkError) { log.yellow (e.constructor.name, e.message) } log.dim ('---------------------------------------------------') // rethrow for call-stack // other errors throw e } if (debug) { if (httpsAgent.freeSockets) { const keys = Object.keys (httpsAgent.freeSockets) if (keys.length) { const firstKey = keys[0] let httpAgent = httpsAgent.freeSockets[firstKey]; log (firstKey, httpAgent.length) } } } if (!poll && !isWsMethod){ break } } exchange.close() } else if (exchange[methodName] === undefined) { log.red (exchange.id + '.' + methodName + ': no such property') } else { printHumanReadable (exchange, exchange[methodName]) } } else { log (exchange) } } } //----------------------------------------------------------------------------- run () export { }