UNPKG

crypto-janitor

Version:

The Crypto Janitor provides a simple interface for fetching and cleaning crypto account data.

514 lines 21.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getExchange = void 0; /* eslint-disable new-cap */ /* eslint-disable max-len */ const base_1 = require("./base"); const moment = require("moment"); const ccxt = require("ccxt"); const utils_1 = require("../utils"); const _ = require("lodash"); /** * Create ccxt exchange object * * @param {string} name - Name of exchange * @param {string} creds - User's exchange credentials * @param {number} rateLimit - API request rate limit * @return {ccxt.Exchange} ccxt exchange instance */ function getExchange(name, creds, rateLimit = 1000) { const exchanges = { // public or private access bittrex: new ccxt.bittrex({ ...creds, enableRateLimit: true }), coinbase: new ccxt.coinbase({ ...creds, enableRateLimit: true }), coinbasepro: new ccxt.coinbasepro({ ...creds, enableRateLimit: true }), kucoin: new ccxt.kucoin({ ...creds, enableRateLimit: true }), // only public access gateio: new ccxt.gateio({ ...creds, enableRateLimit: true }), binance: new ccxt.binance({ ...creds, enableRateLimit: true }), bitstamp: new ccxt.bitstamp({ ...creds, enableRateLimit: true }), ftx: new ccxt.ftx({ ...creds, enableRateLimit: true }), hitbtc: new ccxt.hitbtc({ ...creds, enableRateLimit: true }), huobipro: new ccxt.huobipro({ ...creds, enableRateLimit: true }), kraken: new ccxt.kraken({ ...creds, enableRateLimit: true }), okcoin: new ccxt.okcoin({ ...creds, enableRateLimit: true }), poloniex: new ccxt.poloniex({ ...creds, enableRateLimit: true }), yobit: new ccxt.yobit({ ...creds, enableRateLimit: true }), }; const exchange = exchanges[name]; if (!exchange) throw new Error(`"${name}" is not a registered Exchange.`); if (rateLimit) exchange.rateLimit = rateLimit; return exchange; } exports.getExchange = getExchange; /** * A base class to support general ccxt exchange operations */ class ccxtConnection extends base_1.default { /** * Create BaseConnection instance * @param {string} name - Name of exchange (Ex: coinbase) * @param {any} credentials - API creds of exchange * @param {any} params - (Optional) Additional paramaters */ constructor(name, credentials = {}, params = {}) { super(name, "api", params); this.credentials = credentials; this.access = credentials && credentials.apiKey ? "private" : "public"; this.connection = getExchange(name, credentials, params.rateLimit); this.quoteCurrency = "USD"; this.fallbackMarketConnection = params.fallback ? new params.fallback({}) : null; this.markets = []; } /** * HELPER: Format transaction * * @param {any} transaction - Unformatted cctx unified transaction * @param {string} forceType - (Optional) Type to assign to transaction * @return {Transaction} Formatted transaction */ _formatTransaction(transaction, forceType) { const fee = transaction.fee ? transaction.fee.cost : 0; const type = forceType ? forceType : _.kebabCase(transaction.type); const formatted = { id: transaction.id, timestamp: moment.utc(transaction.timestamp).toDate(), type: type, baseCurrency: transaction.currency, baseQuantity: transaction.amount, baseUsdPrice: 0, feeCurrency: transaction.fee ? transaction.fee.currency : "USD", feeQuantity: fee, feeUsdPrice: 0, feeTotal: 0, subTotal: 0, total: 0, }; if (!this.requireUsdValuation) { formatted.feeUsdPrice = formatted.feeCurrency === "USD" ? 1 : formatted.feeUsdPrice; formatted.subTotal = transaction.amount; formatted.total = transaction.amount + fee; } return formatted; } /** * HELPER: Format orders * * @param {any} order - Unformatted cctx unified transaction * @return {Transaction} Formatted transaction */ _formatOrder(order) { const fee = order.fee ? order.fee.cost : 0; const feeCurrency = order.fee ? order.fee.currency : "USD"; const price = order.price ? order.price : order.average; const [baseCurrency, quoteCurrency] = order.symbol.split("/"); // use fill as default value with amount as fallback const baseQuantity = !isNaN(order.filled) ? order.filled : order.amount; const quoteQuantity = order.cost; const formatted = { id: order.id, timestamp: moment.utc(order.timestamp).toDate(), type: order.side, baseCurrency: baseCurrency, baseQuantity: baseQuantity, baseUsdPrice: 0, quoteCurrency: quoteCurrency, quoteQuantity: quoteQuantity, quotePrice: price, quoteUsdPrice: 0, feeCurrency: feeCurrency, feeQuantity: fee, feeUsdPrice: 0, feeTotal: 0, subTotal: 0, total: 0, }; if (!this.requireUsdValuation || quoteCurrency === "USD") { formatted.subTotal = order.cost; formatted.total = order.side === "buy" ? order.cost + fee : order.cost - fee; formatted.quoteUsdPrice = 1; formatted.baseUsdPrice = quoteQuantity / baseQuantity; // formatted.quotePrice = formatted.baseUsdPrice / formatted.quoteUsdPrice; if (formatted.feeCurrency === formatted.quoteCurrency) { formatted.feeUsdPrice = formatted.quoteUsdPrice; formatted.feeTotal = formatted.feeUsdPrice * formatted.feeQuantity; } } return formatted; } /** * HELPER * @description Check if params are satified for call * @param {string} method CCXT exchange method name * @param {string} symbol Currency symbol * @return {void} */ _paramCheck(method, symbol) { if (!this.connection.has[method]) { throw Error(`${this.connection.name} does not support ${method} function`); } if (this.requireSymbols && !symbol) { throw Error(`${this.connection.name}.${method} requires a symbol`); } } /** * @description Initialize exchange by fetching balances and loading markets * @override BaseConnection.initialize * @param {boolean} forceReload - (Optional) Additional paramaters * @return {Promise<void>} */ async initialize(forceReload = false) { if (!this.initialized || forceReload) { await this.connection.loadMarkets(); if (this.fallbackMarketConnection) { await this.fallbackMarketConnection.initialize(); } this.symbols = this.connection.symbols; this.markets = Object.values(this.connection.markets) .filter((market) => market.active) .map((market) => market.symbol); if (this.access === "private") { this.balances = await this.getBalances(); } else { const quotes = _.uniq(this.markets.map((currencyPair) => currencyPair.split("/")[1])); // detect USD (or equivelent) quote currency if (!quotes.includes("USD")) { if (quotes.includes("USDC")) { this.quoteCurrency = "USDC"; } else if (quotes.includes("USDT")) { this.quoteCurrency = "USDT"; } } } this.initialized = true; } } /** * @description Fetch Account Balances * @override BaseConnection.getBalances * @return {Promise<any>} Account balance object */ async getBalances() { if (this.access === "private") { const balances = await this.connection.fetchBalance(); return balances; } return []; } /** * @description Fetch Account Withdrawals * @override BaseConnection.getWithdrawals * @param {string} symbol Currency symbol * @param {string} key (Default=Send) Transaction type key * @param {number} since (Optional) Timestamp to get transactions since * @param {number} limit (Optional) Max number of entries per request * @return {Promise<Array<any>>} Array of withdrawal objects */ async getWithdrawals(symbol, key = "send", since, limit = 100) { if (this.access === "private") { this._paramCheck("fetchWithdrawals", symbol); let withdrawals = await this.connection.fetchWithdrawals(symbol, since, limit); withdrawals = withdrawals.map((deposit) => this._formatTransaction(deposit, key)); if (since) { withdrawals = withdrawals.filter((deposit) => deposit.timestamp.getTime() > since); } if (this.requireUsdValuation) { withdrawals = withdrawals.map((withdrawal) => this.quoteTransaction(withdrawal)); withdrawals = await Promise.all(withdrawals); } return withdrawals; } return []; } /** * @description Fetch Account Deposits * @override BaseConnection.getDeposits * @param {string} symbol Currency symbol * @param {string} key (Default=Receive) Transaction type key * @param {number} since (Optional) Timestamp to get transactions since * @param {number} limit (Optional) Max number of entries per request * @return {Promise<Array<any>>} Array of deposit objects */ async getDeposits(symbol, key = "receive", since, limit = 100) { if (this.access === "private") { this._paramCheck("fetchDeposits", symbol); let deposits = await this.connection.fetchDeposits(symbol, since, limit); deposits = deposits.map((deposit) => this._formatTransaction(deposit, key)); if (since) { deposits = deposits.filter((deposit) => deposit.timestamp.getTime() > since); } if (this.requireUsdValuation) { deposits = deposits.map((deposit) => this.quoteTransaction(deposit)); deposits = await Promise.all(deposits); deposits = deposits.filter((tx) => tx.baseUsdPrice !== 0); } return deposits; } return []; } /** * @description Fetch Account Orders * @override BaseConnection.getOrders * @param {string} symbol Currency symbol * @param {number} since (Optional) Timestamp to get transactions since * @param {number} limit (Optional) Max number of entries per request * @return {Promise<Array<any>>} Array of order objects */ async getOrders(symbol, since, limit = 100) { if (this.access === "private") { this._paramCheck("fetchClosedOrders", symbol); let orders = await this.connection.fetchClosedOrders(symbol, since, limit); orders = orders.filter((order) => order.filled > 0); orders = orders.map((order) => this._formatOrder(order)); if (since) { orders = orders.filter((order) => order.timestamp.getTime() > since); } if (this.requireUsdValuation) { orders = orders.map((order) => this.quoteOrder(order)); orders = await Promise.all(orders); orders = orders.map((order) => this._attemptedSwapConversion(order)); } return orders; } return []; } /** * @description Fetch all account transactions (withdrawals, deposits, and orders) * @override BaseConnection.getLedger * @param {string} symbol Currency symbol * @param {number} since (Optional) Timestamp to get transactions since * @param {number} limit (Optional) Max number of entries per request * @return {Promise<any>} Array of withdrawal objects */ async getLedger(symbol, since, limit = 100) { if (this.access === "private") { this._paramCheck("fetchLedger", symbol); const ledger = await this.connection.fetchLedger(symbol, since, limit); // return ledger.map((transaction: any) => _formatTransaction(transaction)); return ledger; } return []; } /** * @description Fetch account transactions (withdrawals, deposits, and orders) * @override BaseConnection.getTransactions * @param {string} symbol Currency symbol * @param {number} since (Optional) Timestamp to get transactions since * @return {Promise<Array<any>>} Array of withdrawal objects */ async getTransactions(symbol, since) { if (this.access === "private") { const results = await Promise.all([ this.getWithdrawals(symbol, "send", since), this.getDeposits(symbol, "receive", since), this.getOrders(symbol, since), ]); return _.sortBy(_.flatten(results), "timestamp"); } return []; } /** * @description Fetch all transactions (withdrawals, deposits, and orders) for all symbols * @override BaseConnection.getAllTransactions * @param {number} since (Optional) Timestamp to get transactions since * @return {Array<any>} Array of withdrawal objects */ async getAllTransactions(since) { if (this.access === "private") { const catchErrors = [ ccxt.NetworkError, ccxt.ExchangeError, ccxt.InvalidNonce, ]; let allTransactions = []; if (this.requireSymbols) { for (const symbol of this.symbols) { let x = 0; while (x < 3) { try { const transactions = await this.getTransactions(symbol, since); allTransactions = allTransactions.concat(transactions); break; } catch (e) { if (catchErrors.some((error) => e instanceof error)) { if (e.message.includes("does not have currency code")) { break; } await utils_1.sleep(1500); console.log("rate limit exceed...sleeping", e.message); x++; } else { console.log(`getTransactions failed => ${e.message}`); break; } } } } } else { allTransactions = await this.getTransactions(undefined, since); } allTransactions = _.sortBy(allTransactions, "timestamp"); return allTransactions; } return []; } /** * @description Get preferred market(s) to convert asset to USD (or stablecoin) * @param {string} symbol Currency Symbol * @param {Array<string>} exclude Currency Symbol * @return {Promise<Array<string>>} Ex: ["USDC"] or ["BTC", "USD"] */ async getQuoteConversion(symbol, exclude = []) { const currencyPairs = this.markets.filter((currencyPair) => { const [base, quote] = currencyPair.split("/"); return base === symbol && !exclude.includes(quote); }); if (currencyPairs.length > 0) { const quoteOptions = [...this.stableCurrencies, "BTC", "ETH"]; const quoteCurrency = quoteOptions.find((quoteSymbol) => currencyPairs.includes(`${symbol}/${quoteSymbol}`)) || ""; if (!this.stableCurrencies.includes(quoteCurrency)) { return [quoteCurrency, this.quoteCurrency]; } return [quoteCurrency]; } return []; } /** * @description Get price of an asset at a given time (only public access required) * @param {string} symbol Currency symbol * @param {number} timestamp Timestamp to get price at * @return {Promise<number>} Price of asset in USD */ async getQuote(symbol, timestamp) { if (this.stableCurrencies.includes(symbol)) { return 1; } const marketSources = [this]; if (this.fallbackMarketConnection) { marketSources.push(this.fallbackMarketConnection); } for (const source of marketSources) { // Must use this loop to make sure a market is found and that market returns a valid // price for the provided timestamp. Consider making this a shared function let price = 0; let quotes = await source.getQuoteConversion(symbol); let exclude = []; while (quotes.length > 0) { exclude = _.uniq(exclude.concat(quotes)); const candle = await source.connection.fetchOHLCV(`${symbol}/${quotes[0]}`, "1m", timestamp, 2); if (candle.length > 0) { price = candle[0][3]; if (quotes.length > 1) { const conversionCandle = await source.connection.fetchOHLCV(`${quotes[0]}/${quotes[1]}`, "1m", timestamp, 2); price = price * conversionCandle[0][3]; } return price; } else { quotes = await source.getQuoteConversion(symbol, exclude); } } } return 0; // if you are reaching this you should provide fallback market sources for your connection } /** * @description Get price of an asset at a given time (only public access required) * @param {number} timestamp Timestamp to get price at * @param {string} baseCurrency Base currency symbol * @param {string} feeCurrency (Optional) Fee currency symbol - will default to base or quote if not specifed * @param {string} quoteCurrency (Optional) Quote currency symbol * @param {number} quotePrice Price of base currency denominated in quote currency * @return {Promise<any>} Price of asset in USD */ async getCommonPrices(timestamp, baseCurrency, feeCurrency, quoteCurrency, quotePrice) { const symbol = quoteCurrency ? quoteCurrency : baseCurrency; const price = await this.getQuote(symbol, timestamp); const prices = {}; if (symbol === baseCurrency) { prices.baseUsdPrice = price; } else if (symbol === quoteCurrency) { prices.quoteUsdPrice = price; if (quotePrice !== undefined) { prices.baseUsdPrice = prices.quoteUsdPrice * quotePrice; } } // Fetch fee price if neccessary if (feeCurrency) { if (feeCurrency === "USD") { prices.feeUsdPrice = 1; } else if (feeCurrency === baseCurrency) { prices.feeUsdPrice = prices.baseUsdPrice; } else if (feeCurrency === quoteCurrency) { prices.feeUsdPrice = prices.quoteUsdPrice; } else { prices.feeUsdPrice = await this.getQuote(feeCurrency, timestamp); } } return prices; } /** * @description Update USD quote values for a transaction (only public access required) * @param {Transaction} tx Transaction object * @param {any} prices Transaction prices * @return {Promise<Transaction>} Transaction with updated pricing data */ async quoteTransaction(tx, prices) { // Get and update prices if (!prices) { prices = await this.getCommonPrices(tx.timestamp.getTime(), tx.baseCurrency, tx.feeCurrency); } tx.baseUsdPrice = prices.baseUsdPrice; tx.feeUsdPrice = prices.feeUsdPrice; // Update totals tx.feeTotal = tx.feeUsdPrice * tx.feeQuantity; tx.subTotal = tx.baseQuantity * tx.baseUsdPrice; tx.total = tx.subTotal + tx.feeUsdPrice * tx.feeQuantity; return tx; } /** * @description Update USD quote values for an order (only public access required) * @param {Order} order Order object * @param {any} prices Order prices * @return {Promise<Order>} Order with updated pricing data */ async quoteOrder(order, prices) { // Get and update prices if (!prices) { prices = await this.getCommonPrices(order.timestamp.getTime(), order.baseCurrency, order.feeCurrency, order.quoteCurrency, order.quotePrice); } order.baseUsdPrice = prices.baseUsdPrice; order.feeUsdPrice = prices.feeUsdPrice; order.quoteUsdPrice = prices.quoteUsdPrice; // Update totals order.feeTotal = order.feeUsdPrice * order.feeQuantity; order.subTotal = order.baseQuantity * order.baseUsdPrice; order.total = order.type === "buy" ? order.subTotal + order.feeTotal : order.subTotal - order.feeTotal; return order; } } exports.default = ccxtConnection; //# sourceMappingURL=ccxt.js.map