crypto-janitor
Version:
The Crypto Janitor provides a simple interface for fetching and cleaning crypto account data.
514 lines • 21.8 kB
JavaScript
"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