diff --git a/CONFIG.md b/CONFIG.md index 8ecf6b46..6597f3f2 100644 --- a/CONFIG.md +++ b/CONFIG.md @@ -113,11 +113,11 @@ Contains options that are not exchange specific. #### Options description -| Name | Value | Description | -| ---------------------- | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **fiatConversionRate** | Duration string (ex: `8h`) | Minimum time between two consecutive requests of the same fiat conversion | -| **log.file** | Boolean | If `true`, will log in rotating files instead of standard output | -| **log.level** | String | Defines the log level. Can be {'off', 'critical', 'error', 'warning', 'info', 'debug', 'trace'} | -| **log.maxFileSize** | String (ex: `5Mi` for 5 Megabytes) | Defines in bytes the maximum logging file size. A string representation of an integral, possibly with one suffix ending such as k, M, G, T (1k multipliers) or Ki, Mi, Gi, Ti (1024 multipliers) are supported. | -| **log.maxNbFiles** | Integer | Number of maximum rotating files for log in files | -| **printResults** | Boolean | Print query results if `true` | +| Name | Value | Description | +| ---------------------- | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **apiOutputType** | String among {`off`, `table`, `json`} | Configure the output type of coincenter queries | +| **fiatConversionRate** | Duration string (ex: `8h`) | Minimum time between two consecutive requests of the same fiat conversion | +| **log.file** | Boolean | If `true`, will log in rotating files instead of standard output | +| **log.level** | String | Defines the log level. Can be {'off', 'critical', 'error', 'warning', 'info', 'debug', 'trace'} | +| **log.maxFileSize** | String (ex: `5Mi` for 5 Megabytes) | Defines in bytes the maximum logging file size. A string representation of an integral, possibly with one suffix ending such as k, M, G, T (1k multipliers) or Ki, Mi, Gi, Ti (1024 multipliers) are supported. | +| **log.maxNbFiles** | Integer | Number of maximum rotating files for log in files | diff --git a/README.md b/README.md index 71f88d09..cdc8c70b 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,8 @@ Main features: - [Installation](#installation) - [Configuration](#configuration) - [Usage](#usage) - - [General](#general) + - [Input / output](#input--output) + - [Format](#format) - [Logging](#logging) - [Public requests](#public-requests) - [Markets](#markets) @@ -110,7 +111,28 @@ See [CONFIG.md](CONFIG.md) # Usage -## General +## Input / output + +### Format + +`coincenter` is a command line tool with ergonomic and easy to remember option schema. You will usually provide this kind of input: + - Command name, with short hand flag or long name (check with `-h` or `--help`) + - Followed by either: + - Nothing, meaning that command will be applied globally + - Amount with currency or any currency, separated by dash to specify pairs, or source - destination + - Comma separated list of exchanges (all are considered if not provided) + +Example: +``` + Ori curr. exchange1 + | | +coincenter --trade 0.05BTC-USDT,kraken,huobi + | | | + from amt. to curr. exchange2 +``` + +By default, result of command is printed in a formatted table on standard output. +You can also choose a *json* output format with option `-o json`. ### Logging diff --git a/src/api-objects/include/exchangeprivateapitypes.hpp b/src/api-objects/include/exchangeprivateapitypes.hpp index 21b865a5..8d0314d3 100644 --- a/src/api-objects/include/exchangeprivateapitypes.hpp +++ b/src/api-objects/include/exchangeprivateapitypes.hpp @@ -1,8 +1,10 @@ #pragma once +#include "cct_flatset.hpp" #include "cct_vector.hpp" #include "order.hpp" namespace cct { using Orders = vector; +using OrdersSet = FlatSet; } // namespace cct \ No newline at end of file diff --git a/src/api-objects/include/withdrawinfo.hpp b/src/api-objects/include/withdrawinfo.hpp index d99967f6..756b6051 100644 --- a/src/api-objects/include/withdrawinfo.hpp +++ b/src/api-objects/include/withdrawinfo.hpp @@ -14,7 +14,8 @@ using WithdrawIdView = std::string_view; namespace api { class InitiatedWithdrawInfo { public: - InitiatedWithdrawInfo(Wallet receivingWallet, WithdrawIdView withdrawId, MonetaryAmount grossEmittedAmount); + InitiatedWithdrawInfo(Wallet receivingWallet, WithdrawIdView withdrawId, MonetaryAmount grossEmittedAmount, + TimePoint initiatedTime = Clock::now()); TimePoint initiatedTime() const { return _initiatedTime; } @@ -52,10 +53,11 @@ class SentWithdrawInfo { class WithdrawInfo { public: /// Empty withdraw info, when no withdrawal has been done - WithdrawInfo(string &&msg = string()) : _withdrawIdOrMsgIfNotInitiated(std::move(msg)) {} + explicit WithdrawInfo(string &&msg = string()) : _withdrawIdOrMsgIfNotInitiated(std::move(msg)) {} /// Constructs a withdraw info with all information - WithdrawInfo(const api::InitiatedWithdrawInfo &initiatedWithdrawInfo, const api::SentWithdrawInfo &sentWithdrawInfo); + WithdrawInfo(const api::InitiatedWithdrawInfo &initiatedWithdrawInfo, const api::SentWithdrawInfo &sentWithdrawInfo, + TimePoint receivedTime = Clock::now()); bool hasBeenInitiated() const { return _initiatedTime != TimePoint{}; } diff --git a/src/api-objects/src/withdrawinfo.cpp b/src/api-objects/src/withdrawinfo.cpp index e298adb6..e783777f 100644 --- a/src/api-objects/src/withdrawinfo.cpp +++ b/src/api-objects/src/withdrawinfo.cpp @@ -5,19 +5,19 @@ namespace cct { namespace api { InitiatedWithdrawInfo::InitiatedWithdrawInfo(Wallet receivingWallet, WithdrawIdView withdrawId, - MonetaryAmount grossEmittedAmount) + MonetaryAmount grossEmittedAmount, TimePoint initiatedTime) : _receivingWallet(std::move(receivingWallet)), _withdrawIdOrMsgIfNotInitiated(withdrawId), - _initiatedTime(Clock::now()), + _initiatedTime(initiatedTime), _grossEmittedAmount(grossEmittedAmount) {} } // namespace api WithdrawInfo::WithdrawInfo(const api::InitiatedWithdrawInfo &initiatedWithdrawInfo, - const api::SentWithdrawInfo &sentWithdrawInfo) + const api::SentWithdrawInfo &sentWithdrawInfo, TimePoint receivedTime) : _receivingWallet(initiatedWithdrawInfo.receivingWallet()), _withdrawIdOrMsgIfNotInitiated(initiatedWithdrawInfo.withdrawId()), _initiatedTime(initiatedWithdrawInfo.initiatedTime()), - _receivedTime(Clock::now()), + _receivedTime(receivedTime), _netEmittedAmount(sentWithdrawInfo.netEmittedAmount()) {} const WithdrawId &WithdrawInfo::withdrawId() const { diff --git a/src/api/common/src/exchangeprivateapi.cpp b/src/api/common/src/exchangeprivateapi.cpp index 4e7ff571..c5339869 100644 --- a/src/api/common/src/exchangeprivateapi.cpp +++ b/src/api/common/src/exchangeprivateapi.cpp @@ -73,9 +73,6 @@ TradedAmounts ExchangePrivate::trade(MonetaryAmount from, CurrencyCode toCurrenc tradedAmounts.tradedTo = stepTradedAmounts.tradedTo; } } - if (!options.isSimulation() || realOrderPlacedInSimulationMode) { - log::info("**** Traded {} into {} ****", tradedAmounts.tradedFrom.str(), tradedAmounts.tradedTo.str()); - } return tradedAmounts; } diff --git a/src/api/exchanges/src/bithumbprivateapi.cpp b/src/api/exchanges/src/bithumbprivateapi.cpp index 0f8ab086..6d046f37 100644 --- a/src/api/exchanges/src/bithumbprivateapi.cpp +++ b/src/api/exchanges/src/bithumbprivateapi.cpp @@ -3,7 +3,6 @@ #include #include #include -#include #include #include "apikey.hpp" diff --git a/src/engine/CMakeLists.txt b/src/engine/CMakeLists.txt index c19c0ddf..01683be7 100644 --- a/src/engine/CMakeLists.txt +++ b/src/engine/CMakeLists.txt @@ -44,6 +44,17 @@ add_unit_test( coincenter_objects ) +add_unit_test( + queryresultprinter_test + src/balanceperexchangeportfolio.cpp + src/coincentercommandtype.cpp + src/queryresultprinter.cpp + test/queryresultprinter_test.cpp + LIBRARIES + coincenter_exchangeapi + coincenter_objects +) + add_unit_test( stringoptionparser_test src/stringoptionparser.cpp diff --git a/src/engine/include/balanceperexchangeportfolio.hpp b/src/engine/include/balanceperexchangeportfolio.hpp index 61e2a573..8c26bc7f 100644 --- a/src/engine/include/balanceperexchangeportfolio.hpp +++ b/src/engine/include/balanceperexchangeportfolio.hpp @@ -2,26 +2,27 @@ #include -#include "balanceportfolio.hpp" #include "cct_const.hpp" -#include "cct_smallvector.hpp" -#include "exchangename.hpp" +#include "cct_json.hpp" +#include "queryresulttypes.hpp" namespace cct { class BalancePerExchangePortfolio { public: - void add(ExchangeName exchangeName, BalancePortfolio balancePortfolio); + explicit BalancePerExchangePortfolio(const BalancePerExchange &balancePerExchange) + : _balancePerExchange(balancePerExchange) {} /// Pretty print table of balance. /// @param wide if true, all exchange amount will be printed as well - void print(std::ostream &os, bool wide) const; + void printTable(std::ostream &os, bool wide) const; + + /// Print in json format. + json printJson(CurrencyCode equiCurrency) const; private: - // +1 for total in first position - using BalancePortfolioVector = SmallVector; + BalancePortfolio computeTotal() const; - BalancePortfolioVector _balances{1}; - ExchangeNames _exchanges; + const BalancePerExchange &_balancePerExchange; }; } // namespace cct \ No newline at end of file diff --git a/src/engine/include/coincenter.hpp b/src/engine/include/coincenter.hpp index 42593042..3180e5ea 100644 --- a/src/engine/include/coincenter.hpp +++ b/src/engine/include/coincenter.hpp @@ -85,7 +85,7 @@ class Coincenter { ConversionPathPerExchange getConversionPaths(Market m, ExchangeNameSpan exchangeNames); /// Get withdraw fees for all exchanges from given list (or all exchanges if list is empty) - WithdrawFeePerExchange getWithdrawFees(CurrencyCode currencyCode, ExchangeNameSpan exchangeNames); + MonetaryAmountPerExchange getWithdrawFees(CurrencyCode currencyCode, ExchangeNameSpan exchangeNames); /// Trade a specified amount of a given currency into another one, using the market defined in the given exchanges. /// If no exchange name is given, it will attempt to trade given amount on all exchanges with the sufficient balance. diff --git a/src/engine/include/coincentercommand.hpp b/src/engine/include/coincentercommand.hpp index e1baf84a..9e513659 100644 --- a/src/engine/include/coincentercommand.hpp +++ b/src/engine/include/coincentercommand.hpp @@ -1,8 +1,8 @@ #pragma once -#include #include +#include "coincentercommandtype.hpp" #include "currencycode.hpp" #include "exchangename.hpp" #include "market.hpp" @@ -13,27 +13,7 @@ namespace cct { class CoincenterCommand { public: - enum class Type : int8_t { - kMarkets, - kConversionPath, - kLastPrice, - kTicker, - kOrderbook, - kLastTrades, - kLast24hTradedVolume, - kWithdrawFee, - - kBalance, - kDepositInfo, - kOrdersOpened, - kOrdersCancel, - kTrade, - kBuy, - kSell, - kWithdraw, - }; - - explicit CoincenterCommand(Type type) : _type(type) {} + explicit CoincenterCommand(CoincenterCommandType type) : _type(type) {} CoincenterCommand& setExchangeNames(const ExchangeNames& exchangeNames); CoincenterCommand& setExchangeNames(ExchangeNames&& exchangeNames); @@ -78,7 +58,7 @@ class CoincenterCommand { CurrencyCode cur1() const { return _cur1; } CurrencyCode cur2() const { return _cur2; } - Type type() const { return _type; } + CoincenterCommandType type() const { return _type; } bool isPercentageAmount() const { return _isPercentageAmount; } @@ -94,7 +74,7 @@ class CoincenterCommand { int _n = -1; Market _market; CurrencyCode _cur1, _cur2; - Type _type; + CoincenterCommandType _type; bool _isPercentageAmount = false; }; diff --git a/src/engine/include/coincentercommandtype.hpp b/src/engine/include/coincentercommandtype.hpp new file mode 100644 index 00000000..8d6d1a76 --- /dev/null +++ b/src/engine/include/coincentercommandtype.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include +#include + +namespace cct { +enum class CoincenterCommandType : int8_t { + kMarkets, + kConversionPath, + kLastPrice, + kTicker, + kOrderbook, + kLastTrades, + kLast24hTradedVolume, + kWithdrawFee, + + kBalance, + kDepositInfo, + kOrdersOpened, + kOrdersCancel, + kTrade, + kBuy, + kSell, + kWithdraw, +}; + +std::string_view CoincenterCommandTypeToString(CoincenterCommandType type); +} // namespace cct \ No newline at end of file diff --git a/src/engine/include/coincenteroptions.hpp b/src/engine/include/coincenteroptions.hpp index 092f40b2..38134153 100644 --- a/src/engine/include/coincenteroptions.hpp +++ b/src/engine/include/coincenteroptions.hpp @@ -29,11 +29,14 @@ struct CoincenterCmdLineOptions { static constexpr int64_t kDefaultRepeatDurationSeconds = std::chrono::duration_cast(kDefaultRepeatTime).count(); - static constexpr std::string_view kSingleQuote = "'"; - static constexpr std::string_view kClosingParenthesis = ")"; + static constexpr std::string_view kOutput1 = "Output format. One of ("; + static constexpr std::string_view kOutput2 = ") (default configured in general config file)"; + static constexpr std::string_view kOutput = + JoinStringView_v, kApiOutputTypeTableStr, + CharToStringView_v<'|'>, kApiOutputTypeJsonStr, kOutput2>; static constexpr std::string_view kData1 = "Use given 'data' directory instead of the one chosen at build time '"; - static constexpr std::string_view kData = JoinStringView_v; + static constexpr std::string_view kData = JoinStringView_v>; static constexpr std::string_view kRepeat1 = "Set delay between each repeat (default: "; static constexpr std::string_view kRepeat2 = "s)"; @@ -44,7 +47,7 @@ struct CoincenterCmdLineOptions { static constexpr std::string_view kLastTradesN1 = "Change number of last trades to query (default: "; static constexpr std::string_view kLastTradesN = JoinStringView_v, - kClosingParenthesis>; + CharToStringView_v<')'>>; static constexpr std::string_view kSmartBuy1 = "Attempt to buy the specified amount in total, on matching exchange accounts (all are considered if none " @@ -98,23 +101,22 @@ struct CoincenterCmdLineOptions { static constexpr std::string_view kMonitoringPort1 = "Specify port of metric gateway instance (default: "; static constexpr std::string_view kMonitoringPort = JoinStringView_v, - kClosingParenthesis>; + CharToStringView_v<')'>>; static constexpr std::string_view kMonitoringIP1 = "Specify IP (v4) of metric gateway instance (default: "; static constexpr std::string_view kMonitoringIP = - JoinStringView_v; + JoinStringView_v>; static void PrintVersion(std::string_view programName); std::string_view dataDir = kDefaultDataDir; + std::string_view apiOutputType; std::string_view logLevel; bool help = false; bool version = false; bool logFile = false; bool logConsole = false; - bool printResults = false; - bool noPrintResults = false; std::optional nosecrets; CommandLineOptionalInt repeats; Duration repeatTime = kDefaultRepeatTime; @@ -192,16 +194,7 @@ struct CoincenterAllowedOptions { &OptValueType::logConsole}, {{{"General", 4}, "--log-file", "", "Log to rotating files (default configured in general config file)"}, &OptValueType::logFile}, - {{{"General", 5}, - "--print", - "", - "Force print results in standard output (default configured in general config file)"}, - &OptValueType::printResults}, - {{{"General", 6}, - "--no-print", - "", - "Force no print of results in standard output (default configured in general config file)"}, - &OptValueType::noPrintResults}, + {{{"General", 5}, "--output", 'o', "", CoincenterCmdLineOptions::kOutput}, &OptValueType::apiOutputType}, {{{"General", 7}, "--no-secrets", "<[exch1,...]>", @@ -229,7 +222,6 @@ struct CoincenterAllowedOptions { {{{"Public queries", 20}, "--orderbook", - 'o', "", "Print order book of currency pair for all exchanges offering " "this market, or only for specified exchanges."}, diff --git a/src/engine/include/commandlineoptionsparser.hpp b/src/engine/include/commandlineoptionsparser.hpp index 4b4744eb..bfdee271 100644 --- a/src/engine/include/commandlineoptionsparser.hpp +++ b/src/engine/include/commandlineoptionsparser.hpp @@ -2,11 +2,11 @@ #include #include -#include #include #include #include #include +#include #include #include diff --git a/src/engine/include/exchangesorchestrator.hpp b/src/engine/include/exchangesorchestrator.hpp index 5c03cc94..fe474efd 100644 --- a/src/engine/include/exchangesorchestrator.hpp +++ b/src/engine/include/exchangesorchestrator.hpp @@ -54,7 +54,7 @@ class ExchangesOrchestrator { const ExchangeName &fromPrivateExchangeName, const ExchangeName &toPrivateExchangeName, Duration withdrawRefreshTime = api::ExchangePrivate::kWithdrawInfoRefreshTime); - WithdrawFeePerExchange getWithdrawFees(CurrencyCode currencyCode, ExchangeNameSpan exchangeNames); + MonetaryAmountPerExchange getWithdrawFees(CurrencyCode currencyCode, ExchangeNameSpan exchangeNames); MonetaryAmountPerExchange getLast24hTradedVolumePerExchange(Market m, ExchangeNameSpan exchangeNames); diff --git a/src/engine/include/queryresultprinter.hpp b/src/engine/include/queryresultprinter.hpp index a18a11c4..3ff5e509 100644 --- a/src/engine/include/queryresultprinter.hpp +++ b/src/engine/include/queryresultprinter.hpp @@ -1,40 +1,76 @@ #pragma once +#include +#include + +#include "apioutputtype.hpp" +#include "coincentercommandtype.hpp" #include "currencycode.hpp" #include "market.hpp" +#include "ordersconstraints.hpp" #include "queryresulttypes.hpp" namespace cct { +class TradeOptions; +class WithdrawInfo; class QueryResultPrinter { public: - explicit QueryResultPrinter(bool doPrint) : _doPrint(doPrint) {} + QueryResultPrinter(std::ostream &os, ApiOutputType apiOutputType); void printMarkets(CurrencyCode cur1, CurrencyCode cur2, const MarketsPerExchange &marketsPerExchange) const; - void printMarketOrderBooks(const MarketOrderBookConversionRates &marketOrderBooksConversionRates) const; + void printMarketOrderBooks(Market m, CurrencyCode equiCurrencyCode, std::optional depth, + const MarketOrderBookConversionRates &marketOrderBooksConversionRates) const; void printTickerInformation(const ExchangeTickerMaps &exchangeTickerMaps) const; - void printBalance(const BalancePerExchange &balancePerExchange) const; + void printBalance(const BalancePerExchange &balancePerExchange, CurrencyCode equiCurrency) const; void printDepositInfo(CurrencyCode depositCurrencyCode, const WalletPerExchange &walletPerExchange) const; - void printOpenedOrders(const OpenedOrdersPerExchange &openedOrdersPerExchange) const; + void printTrades(const TradedAmountsPerExchange &tradedAmountsPerExchange, MonetaryAmount startAmount, + bool isPercentageTrade, CurrencyCode toCurrency, const TradeOptions &tradeOptions) const { + printTrades(tradedAmountsPerExchange, startAmount, isPercentageTrade, toCurrency, tradeOptions, + CoincenterCommandType::kTrade); + } + + void printBuyTrades(const TradedAmountsPerExchange &tradedAmountsPerExchange, MonetaryAmount endAmount, + const TradeOptions &tradeOptions) const { + printTrades(tradedAmountsPerExchange, endAmount, false, CurrencyCode(), tradeOptions, CoincenterCommandType::kBuy); + } + + void printSellTrades(const TradedAmountsPerExchange &tradedAmountsPerExchange, MonetaryAmount startAmount, + bool isPercentageTrade, const TradeOptions &tradeOptions) const { + printTrades(tradedAmountsPerExchange, startAmount, isPercentageTrade, CurrencyCode(), tradeOptions, + CoincenterCommandType::kSell); + } - void printCancelledOrders(const NbCancelledOrdersPerExchange &nbCancelledOrdersPerExchange) const; + void printOpenedOrders(const OpenedOrdersPerExchange &openedOrdersPerExchange, + const OrdersConstraints &ordersConstraints) const; + + void printCancelledOrders(const NbCancelledOrdersPerExchange &nbCancelledOrdersPerExchange, + const OrdersConstraints &ordersConstraints) const; void printConversionPath(Market m, const ConversionPathPerExchange &conversionPathsPerExchange) const; - void printWithdrawFees(const WithdrawFeePerExchange &withdrawFeePerExchange) const; + void printWithdrawFees(const MonetaryAmountPerExchange &withdrawFeePerExchange, CurrencyCode cur) const; void printLast24hTradedVolume(Market m, const MonetaryAmountPerExchange &tradedVolumePerExchange) const; - void printLastTrades(Market m, const LastTradesPerExchange &lastTradesPerExchange) const; + void printLastTrades(Market m, int nbLastTrades, const LastTradesPerExchange &lastTradesPerExchange) const; void printLastPrice(Market m, const MonetaryAmountPerExchange &pricePerExchange) const; + void printWithdraw(const WithdrawInfo &withdrawInfo, MonetaryAmount grossAmount, bool isPercentageWithdraw, + const ExchangeName &fromPrivateExchangeName, const ExchangeName &toPrivateExchangeName) const; + private: - bool _doPrint; + void printTrades(const TradedAmountsPerExchange &tradedAmountsPerExchange, MonetaryAmount amount, + bool isPercentageTrade, CurrencyCode toCurrency, const TradeOptions &tradeOptions, + CoincenterCommandType commandType) const; + + std::ostream &_os; + ApiOutputType _apiOutputType; }; } // namespace cct diff --git a/src/engine/include/queryresulttypes.hpp b/src/engine/include/queryresulttypes.hpp index 49583a03..2d214579 100644 --- a/src/engine/include/queryresulttypes.hpp +++ b/src/engine/include/queryresulttypes.hpp @@ -38,11 +38,9 @@ using BalancePerExchange = SmallVector, using WalletPerExchange = SmallVector, kTypicalNbPrivateAccounts>; -using OpenedOrdersPerExchange = SmallVector, kTypicalNbPrivateAccounts>; +using OpenedOrdersPerExchange = SmallVector, kTypicalNbPrivateAccounts>; using NbCancelledOrdersPerExchange = SmallVector, kTypicalNbPrivateAccounts>; using ConversionPathPerExchange = FixedCapacityVector, kNbSupportedExchanges>; - -using WithdrawFeePerExchange = FixedCapacityVector, kNbSupportedExchanges>; } // namespace cct \ No newline at end of file diff --git a/src/engine/src/balanceperexchangeportfolio.cpp b/src/engine/src/balanceperexchangeportfolio.cpp index aeb7d1c0..bbb8e6ea 100644 --- a/src/engine/src/balanceperexchangeportfolio.cpp +++ b/src/engine/src/balanceperexchangeportfolio.cpp @@ -1,65 +1,119 @@ #include "balanceperexchangeportfolio.hpp" #include "cct_string.hpp" +#include "exchange.hpp" #include "simpletable.hpp" namespace cct { -void BalancePerExchangePortfolio::add(ExchangeName exchangeName, BalancePortfolio balancePortfolio) { - _balances.front() += balancePortfolio; - _balances.push_back(std::move(balancePortfolio)); - _exchanges.push_back(std::move(exchangeName)); +namespace { +MonetaryAmount ComputeTotalSum(const BalancePortfolio &total) { + CurrencyCode balanceCurrencyCode = total.empty() ? CurrencyCode() : total.front().equi.currencyCode(); + MonetaryAmount totalSum(0, balanceCurrencyCode); + for (const auto &[amount, equi] : total) { + totalSum += equi; + } + return totalSum; } +} // namespace -void BalancePerExchangePortfolio::print(std::ostream &os, bool wide) const { - BalancePortfolio total = _balances.front(); - if (total.empty()) { - os << "No Balance to display" << std::endl; - } else { - CurrencyCode balanceCurrencyCode = total.front().equi.currencyCode(); - const bool countEqui = !balanceCurrencyCode.isNeutral(); - SimpleTable balanceTable; - SimpleTable::Row header("Currency", "Total amount on selected"); +void BalancePerExchangePortfolio::printTable(std::ostream &os, bool wide) const { + BalancePortfolio total = computeTotal(); + CurrencyCode balanceCurrencyCode = total.empty() ? CurrencyCode() : total.front().equi.currencyCode(); + const bool countEqui = !balanceCurrencyCode.isNeutral(); + SimpleTable::Row header("Currency", "Total amount on selected"); - if (countEqui) { - total.sortByDecreasingEquivalentAmount(); + if (countEqui) { + total.sortByDecreasingEquivalentAmount(); + + string balanceEqCur("Total "); + balanceEqCur.append(balanceCurrencyCode.str()).append(" eq"); + header.emplace_back(std::move(balanceEqCur)); + } - string balanceEqCur("Total "); - balanceEqCur.append(balanceCurrencyCode.str()).append(" eq"); - header.emplace_back(std::move(balanceEqCur)); + if (wide) { + for (const auto &[exchangePtr, balancePortfolio] : _balancePerExchange) { + string account(exchangePtr->name()); + account.push_back('_'); + account.append(exchangePtr->keyName()); + header.emplace_back(std::move(account)); } + } + SimpleTable balanceTable{std::move(header)}; + const int nbExchanges = _balancePerExchange.size(); + for (const auto &[amount, equi] : total) { + // Amounts impossible to convert have a zero value + SimpleTable::Row r(amount.currencyStr(), amount.amountStr()); + if (countEqui) { + r.emplace_back(equi.amountStr()); + } if (wide) { - for (const ExchangeName &e : _exchanges) { - string account(e.name()); - account.push_back('_'); - account.append(e.keyName()); - header.emplace_back(std::move(account)); + for (int exchangePos = 0; exchangePos < nbExchanges; ++exchangePos) { + r.emplace_back(_balancePerExchange[exchangePos].second.get(amount.currencyCode()).amountStr()); } } - balanceTable.push_back(std::move(header)); - - MonetaryAmount totalSum(0, balanceCurrencyCode); - const int nbExchanges = _exchanges.size(); - for (const auto &[amount, equi] : total) { - // Amounts impossible to convert have a zero value - SimpleTable::Row r(amount.currencyStr(), amount.amountStr()); - if (countEqui) { - r.emplace_back(equi.amountStr()); - totalSum += equi; - } - if (wide) { - for (int exchangePos = 0; exchangePos < nbExchanges; ++exchangePos) { - r.emplace_back(_balances[1 + exchangePos].get(amount.currencyCode()).amountStr()); - } + balanceTable.push_back(std::move(r)); + } + if (countEqui) { + balanceTable.push_back(SimpleTable::Row::kDivider); + SimpleTable::Row r("Total", "", ComputeTotalSum(total).amountStr()); + if (wide) { + for (int exchangePos = 0; exchangePos < nbExchanges; ++exchangePos) { + r.emplace_back(""); } - balanceTable.push_back(std::move(r)); } - balanceTable.print(os); - if (countEqui) { - os << "* Total: " << totalSum << std::endl; + balanceTable.push_back(std::move(r)); + } + balanceTable.print(os); +} + +namespace { +json JsonForBalancePortfolio(const BalancePortfolio &balancePortfolio, CurrencyCode equiCurrency) { + json ret = json::object(); + for (const auto &[amount, equiAmount] : balancePortfolio) { + json curData; + curData.emplace("a", amount.amountStr()); + if (!equiCurrency.isNeutral()) { + curData.emplace("eq", equiAmount.amountStr()); + } + ret.emplace(amount.currencyStr(), std::move(curData)); + } + return ret; +} +} // namespace + +json BalancePerExchangePortfolio::printJson(CurrencyCode equiCurrency) const { + json exchangePart = json::object(); + for (const auto &[exchangePtr, balancePortfolio] : _balancePerExchange) { + auto it = exchangePart.find(exchangePtr->name()); + if (it == exchangePart.end()) { + json balancePerExchangeData; + balancePerExchangeData.emplace(exchangePtr->keyName(), JsonForBalancePortfolio(balancePortfolio, equiCurrency)); + exchangePart.emplace(exchangePtr->name(), std::move(balancePerExchangeData)); + } else { + it->emplace(exchangePtr->keyName(), JsonForBalancePortfolio(balancePortfolio, equiCurrency)); } } + + BalancePortfolio total = computeTotal(); + json totalPart; + totalPart.emplace("cur", JsonForBalancePortfolio(total, equiCurrency)); + if (!equiCurrency.isNeutral()) { + totalPart.emplace("eq", ComputeTotalSum(total).amountStr()); + } + json out; + out.emplace("exchange", std::move(exchangePart)); + out.emplace("total", std::move(totalPart)); + return out; +} + +BalancePortfolio BalancePerExchangePortfolio::computeTotal() const { + BalancePortfolio total; + for (const auto &[exchangePtr, balancePortfolio] : _balancePerExchange) { + total += balancePortfolio; + } + return total; } } // namespace cct \ No newline at end of file diff --git a/src/engine/src/coincenter.cpp b/src/engine/src/coincenter.cpp index 878201b6..bba3fa1e 100644 --- a/src/engine/src/coincenter.cpp +++ b/src/engine/src/coincenter.cpp @@ -2,10 +2,12 @@ #include #include +#include #include #include "abstractmetricgateway.hpp" #include "coincentercommands.hpp" +#include "coincentercommandtype.hpp" #include "coincenteroptions.hpp" #include "queryresultprinter.hpp" #include "stringoptionparser.hpp" @@ -21,7 +23,7 @@ Coincenter::Coincenter(const CoincenterInfo &coincenterInfo, const ExchangeSecre _metricsExporter(_coincenterInfo.metricGatewayPtr()), _exchangePool(_coincenterInfo, _fiatConverter, _cryptowatchAPI, _apiKeyProvider), _exchangesOrchestrator(_exchangePool.exchanges()), - _queryResultPrinter(_coincenterInfo.printResults()) {} + _queryResultPrinter(std::cout, _coincenterInfo.apiOutputType()) {} void Coincenter::process(const CoincenterCommands &coincenterCommands) { const int nbRepeats = coincenterCommands.repeats(); @@ -44,85 +46,97 @@ void Coincenter::process(const CoincenterCommands &coincenterCommands) { void Coincenter::processCommand(const CoincenterCommand &cmd) { switch (cmd.type()) { - case CoincenterCommand::Type::kMarkets: { + case CoincenterCommandType::kMarkets: { MarketsPerExchange marketsPerExchange = getMarketsPerExchange(cmd.cur1(), cmd.cur2(), cmd.exchangeNames()); _queryResultPrinter.printMarkets(cmd.cur1(), cmd.cur2(), marketsPerExchange); break; } - case CoincenterCommand::Type::kConversionPath: { + case CoincenterCommandType::kConversionPath: { ConversionPathPerExchange conversionPathPerExchange = getConversionPaths(cmd.market(), cmd.exchangeNames()); _queryResultPrinter.printConversionPath(cmd.market(), conversionPathPerExchange); break; } - case CoincenterCommand::Type::kLastPrice: { + case CoincenterCommandType::kLastPrice: { MonetaryAmountPerExchange lastPricePerExchange = getLastPricePerExchange(cmd.market(), cmd.exchangeNames()); _queryResultPrinter.printLastPrice(cmd.market(), lastPricePerExchange); break; } - case CoincenterCommand::Type::kTicker: { + case CoincenterCommandType::kTicker: { ExchangeTickerMaps exchangeTickerMaps = getTickerInformation(cmd.exchangeNames()); _queryResultPrinter.printTickerInformation(exchangeTickerMaps); break; } - case CoincenterCommand::Type::kOrderbook: { + case CoincenterCommandType::kOrderbook: { MarketOrderBookConversionRates marketOrderBooksConversionRates = getMarketOrderBooks(cmd.market(), cmd.exchangeNames(), cmd.cur1(), cmd.optDepth()); - _queryResultPrinter.printMarketOrderBooks(marketOrderBooksConversionRates); + _queryResultPrinter.printMarketOrderBooks(cmd.market(), cmd.cur1(), cmd.optDepth(), + marketOrderBooksConversionRates); break; } - case CoincenterCommand::Type::kLastTrades: { + case CoincenterCommandType::kLastTrades: { LastTradesPerExchange lastTradesPerExchange = getLastTradesPerExchange(cmd.market(), cmd.exchangeNames(), cmd.nbLastTrades()); - _queryResultPrinter.printLastTrades(cmd.market(), lastTradesPerExchange); + _queryResultPrinter.printLastTrades(cmd.market(), cmd.nbLastTrades(), lastTradesPerExchange); break; } - case CoincenterCommand::Type::kLast24hTradedVolume: { + case CoincenterCommandType::kLast24hTradedVolume: { MonetaryAmountPerExchange tradedVolumePerExchange = getLast24hTradedVolumePerExchange(cmd.market(), cmd.exchangeNames()); _queryResultPrinter.printLast24hTradedVolume(cmd.market(), tradedVolumePerExchange); break; } - case CoincenterCommand::Type::kWithdrawFee: { + case CoincenterCommandType::kWithdrawFee: { auto withdrawFeesPerExchange = getWithdrawFees(cmd.cur1(), cmd.exchangeNames()); - _queryResultPrinter.printWithdrawFees(withdrawFeesPerExchange); + _queryResultPrinter.printWithdrawFees(withdrawFeesPerExchange, cmd.cur1()); break; } - case CoincenterCommand::Type::kBalance: { + case CoincenterCommandType::kBalance: { BalancePerExchange balancePerExchange = getBalance(cmd.exchangeNames(), cmd.cur1()); - _queryResultPrinter.printBalance(balancePerExchange); + _queryResultPrinter.printBalance(balancePerExchange, cmd.cur1()); break; } - case CoincenterCommand::Type::kDepositInfo: { + case CoincenterCommandType::kDepositInfo: { WalletPerExchange walletPerExchange = getDepositInfo(cmd.exchangeNames(), cmd.cur1()); _queryResultPrinter.printDepositInfo(cmd.cur1(), walletPerExchange); break; } - case CoincenterCommand::Type::kOrdersOpened: { + case CoincenterCommandType::kOrdersOpened: { OpenedOrdersPerExchange openedOrdersPerExchange = getOpenedOrders(cmd.exchangeNames(), cmd.ordersConstraints()); - _queryResultPrinter.printOpenedOrders(openedOrdersPerExchange); + _queryResultPrinter.printOpenedOrders(openedOrdersPerExchange, cmd.ordersConstraints()); break; } - case CoincenterCommand::Type::kOrdersCancel: { + case CoincenterCommandType::kOrdersCancel: { NbCancelledOrdersPerExchange nbCancelledOrdersPerExchange = cancelOrders(cmd.exchangeNames(), cmd.ordersConstraints()); - _queryResultPrinter.printCancelledOrders(nbCancelledOrdersPerExchange); + _queryResultPrinter.printCancelledOrders(nbCancelledOrdersPerExchange, cmd.ordersConstraints()); break; } - case CoincenterCommand::Type::kTrade: { - trade(cmd.amount(), cmd.isPercentageAmount(), cmd.cur1(), cmd.exchangeNames(), cmd.tradeOptions()); + case CoincenterCommandType::kTrade: { + TradedAmountsPerExchange tradedAmountsPerExchange = + trade(cmd.amount(), cmd.isPercentageAmount(), cmd.cur1(), cmd.exchangeNames(), cmd.tradeOptions()); + _queryResultPrinter.printTrades(tradedAmountsPerExchange, cmd.amount(), cmd.isPercentageAmount(), cmd.cur1(), + cmd.tradeOptions()); break; } - case CoincenterCommand::Type::kBuy: { - smartBuy(cmd.amount(), cmd.exchangeNames(), cmd.tradeOptions()); + case CoincenterCommandType::kBuy: { + TradedAmountsPerExchange tradedAmountsPerExchange = + smartBuy(cmd.amount(), cmd.exchangeNames(), cmd.tradeOptions()); + _queryResultPrinter.printBuyTrades(tradedAmountsPerExchange, cmd.amount(), cmd.tradeOptions()); break; } - case CoincenterCommand::Type::kSell: { - smartSell(cmd.amount(), cmd.isPercentageAmount(), cmd.exchangeNames(), cmd.tradeOptions()); + case CoincenterCommandType::kSell: { + TradedAmountsPerExchange tradedAmountsPerExchange = + smartSell(cmd.amount(), cmd.isPercentageAmount(), cmd.exchangeNames(), cmd.tradeOptions()); + _queryResultPrinter.printSellTrades(tradedAmountsPerExchange, cmd.amount(), cmd.isPercentageAmount(), + cmd.tradeOptions()); break; } - case CoincenterCommand::Type::kWithdraw: { - withdraw(cmd.amount(), cmd.isPercentageAmount(), cmd.exchangeNames().front(), cmd.exchangeNames().back()); + case CoincenterCommandType::kWithdraw: { + WithdrawInfo withdrawInfo = + withdraw(cmd.amount(), cmd.isPercentageAmount(), cmd.exchangeNames().front(), cmd.exchangeNames().back()); + _queryResultPrinter.printWithdraw(withdrawInfo, cmd.amount(), cmd.isPercentageAmount(), + cmd.exchangeNames().front(), cmd.exchangeNames().back()); break; } default: @@ -223,7 +237,7 @@ WithdrawInfo Coincenter::withdraw(MonetaryAmount grossAmount, bool isPercentageW toPrivateExchangeName); } -WithdrawFeePerExchange Coincenter::getWithdrawFees(CurrencyCode currencyCode, ExchangeNameSpan exchangeNames) { +MonetaryAmountPerExchange Coincenter::getWithdrawFees(CurrencyCode currencyCode, ExchangeNameSpan exchangeNames) { return _exchangesOrchestrator.getWithdrawFees(currencyCode, exchangeNames); } diff --git a/src/engine/src/coincentercommand.cpp b/src/engine/src/coincentercommand.cpp index 8433fa77..9123a7ce 100644 --- a/src/engine/src/coincentercommand.cpp +++ b/src/engine/src/coincentercommand.cpp @@ -5,21 +5,21 @@ namespace cct { bool CoincenterCommand::isPublic() const { switch (_type) { - case Type::kMarkets: + case CoincenterCommandType::kMarkets: [[fallthrough]]; - case Type::kConversionPath: + case CoincenterCommandType::kConversionPath: [[fallthrough]]; - case Type::kLastPrice: + case CoincenterCommandType::kLastPrice: [[fallthrough]]; - case Type::kTicker: + case CoincenterCommandType::kTicker: [[fallthrough]]; - case Type::kOrderbook: + case CoincenterCommandType::kOrderbook: [[fallthrough]]; - case Type::kLastTrades: + case CoincenterCommandType::kLastTrades: [[fallthrough]]; - case Type::kLast24hTradedVolume: + case CoincenterCommandType::kLast24hTradedVolume: [[fallthrough]]; - case Type::kWithdrawFee: + case CoincenterCommandType::kWithdrawFee: return true; default: return false; @@ -31,11 +31,11 @@ bool CoincenterCommand::isReadOnly() const { return true; } switch (_type) { - case Type::kBalance: + case CoincenterCommandType::kBalance: [[fallthrough]]; - case Type::kDepositInfo: + case CoincenterCommandType::kDepositInfo: [[fallthrough]]; - case Type::kOrdersOpened: + case CoincenterCommandType::kOrdersOpened: return true; default: return false; @@ -52,7 +52,7 @@ CoincenterCommand& CoincenterCommand::setExchangeNames(ExchangeNames&& exchangeN } CoincenterCommand& CoincenterCommand::setOrdersConstraints(const OrdersConstraints& ordersConstraints) { - if (_type != Type::kOrdersCancel && _type != Type::kOrdersOpened) { + if (_type != CoincenterCommandType::kOrdersCancel && _type != CoincenterCommandType::kOrdersOpened) { throw exception("Order constraints can only be used for orders related commands"); } _tradeOrOrdersOptions = ordersConstraints; @@ -60,7 +60,7 @@ CoincenterCommand& CoincenterCommand::setOrdersConstraints(const OrdersConstrain } CoincenterCommand& CoincenterCommand::setOrdersConstraints(OrdersConstraints&& ordersConstraints) { - if (_type != Type::kOrdersCancel && _type != Type::kOrdersOpened) { + if (_type != CoincenterCommandType::kOrdersCancel && _type != CoincenterCommandType::kOrdersOpened) { throw exception("Order constraints can only be used for orders related commands"); } _tradeOrOrdersOptions = std::move(ordersConstraints); @@ -68,7 +68,8 @@ CoincenterCommand& CoincenterCommand::setOrdersConstraints(OrdersConstraints&& o } CoincenterCommand& CoincenterCommand::setTradeOptions(const TradeOptions& tradeOptions) { - if (_type != Type::kBuy && _type != Type::kSell && _type != Type::kTrade) { + if (_type != CoincenterCommandType::kBuy && _type != CoincenterCommandType::kSell && + _type != CoincenterCommandType::kTrade) { throw exception("Trade options can only be used for trade related commands"); } _tradeOrOrdersOptions = tradeOptions; @@ -76,7 +77,8 @@ CoincenterCommand& CoincenterCommand::setTradeOptions(const TradeOptions& tradeO } CoincenterCommand& CoincenterCommand::setTradeOptions(TradeOptions&& tradeOptions) { - if (_type != Type::kBuy && _type != Type::kSell && _type != Type::kTrade) { + if (_type != CoincenterCommandType::kBuy && _type != CoincenterCommandType::kSell && + _type != CoincenterCommandType::kTrade) { throw exception("Trade options can only be used for trade related commands"); } _tradeOrOrdersOptions = std::move(tradeOptions); @@ -112,7 +114,8 @@ CoincenterCommand& CoincenterCommand::setCur2(CurrencyCode cur2) { } CoincenterCommand& CoincenterCommand::setPercentageAmount(bool value) { - if (_type != Type::kBuy && _type != Type::kSell && _type != Type::kTrade && _type != Type::kWithdraw) { + if (_type != CoincenterCommandType::kBuy && _type != CoincenterCommandType::kSell && + _type != CoincenterCommandType::kTrade && _type != CoincenterCommandType::kWithdraw) { throw exception("Percentage trade can only be set for trade / buy / sell or withdraw command"); } _isPercentageAmount = value; diff --git a/src/engine/src/coincentercommands.cpp b/src/engine/src/coincentercommands.cpp index 96384b5b..ff8df748 100644 --- a/src/engine/src/coincentercommands.cpp +++ b/src/engine/src/coincentercommands.cpp @@ -6,6 +6,7 @@ #include "cct_invalid_argument_exception.hpp" #include "cct_log.hpp" +#include "coincentercommandtype.hpp" #include "coincenteroptions.hpp" #include "commandlineoptionsparser.hpp" #include "stringoptionparser.hpp" @@ -73,7 +74,7 @@ bool CoincenterCommands::setFromOptions(const CoincenterCmdLineOptions &cmdLineO if (!cmdLineOptions.markets.empty()) { StringOptionParser anyParser(cmdLineOptions.markets); auto [cur1, cur2, exchanges] = anyParser.getCurrenciesPublicExchanges(); - _commands.emplace_back(CoincenterCommand::Type::kMarkets) + _commands.emplace_back(CoincenterCommandType::kMarkets) .setCur1(cur1) .setCur2(cur2) .setExchangeNames(std::move(exchanges)); @@ -82,7 +83,7 @@ bool CoincenterCommands::setFromOptions(const CoincenterCmdLineOptions &cmdLineO if (!cmdLineOptions.orderbook.empty()) { StringOptionParser anyParser(cmdLineOptions.orderbook); auto [market, exchanges] = anyParser.getMarketExchanges(); - _commands.emplace_back(CoincenterCommand::Type::kOrderbook) + _commands.emplace_back(CoincenterCommandType::kOrderbook) .setMarket(market) .setExchangeNames(std::move(exchanges)) .setDepth(cmdLineOptions.orderbookDepth) @@ -91,13 +92,13 @@ bool CoincenterCommands::setFromOptions(const CoincenterCmdLineOptions &cmdLineO if (cmdLineOptions.ticker) { StringOptionParser anyParser(*cmdLineOptions.ticker); - _commands.emplace_back(CoincenterCommand::Type::kTicker).setExchangeNames(anyParser.getExchanges()); + _commands.emplace_back(CoincenterCommandType::kTicker).setExchangeNames(anyParser.getExchanges()); } if (!cmdLineOptions.conversionPath.empty()) { StringOptionParser anyParser(cmdLineOptions.conversionPath); auto [market, exchanges] = anyParser.getMarketExchanges(); - _commands.emplace_back(CoincenterCommand::Type::kConversionPath) + _commands.emplace_back(CoincenterCommandType::kConversionPath) .setMarket(market) .setExchangeNames(std::move(exchanges)); } @@ -105,7 +106,7 @@ bool CoincenterCommands::setFromOptions(const CoincenterCmdLineOptions &cmdLineO if (cmdLineOptions.balance) { StringOptionParser anyParser(*cmdLineOptions.balance); auto [balanceCurrencyCode, exchanges] = anyParser.getCurrencyPrivateExchanges(); - _commands.emplace_back(CoincenterCommand::Type::kBalance) + _commands.emplace_back(CoincenterCommandType::kBalance) .setCur1(balanceCurrencyCode) .setExchangeNames(std::move(exchanges)); } @@ -121,19 +122,19 @@ bool CoincenterCommands::setFromOptions(const CoincenterCmdLineOptions &cmdLineO std::string_view tradeArgs; bool isSmartTrade = !cmdLineOptions.buy.empty() || !cmdLineOptions.sell.empty() || !cmdLineOptions.sellAll.empty(); bool isTradeAll = !cmdLineOptions.tradeAll.empty(); - CoincenterCommand::Type commandType; + CoincenterCommandType commandType; if (!cmdLineOptions.buy.empty()) { tradeArgs = cmdLineOptions.buy; - commandType = CoincenterCommand::Type::kBuy; + commandType = CoincenterCommandType::kBuy; } else if (!cmdLineOptions.sellAll.empty()) { tradeArgs = cmdLineOptions.sellAll; - commandType = CoincenterCommand::Type::kSell; + commandType = CoincenterCommandType::kSell; } else if (!cmdLineOptions.sell.empty()) { tradeArgs = cmdLineOptions.sell; - commandType = CoincenterCommand::Type::kSell; + commandType = CoincenterCommandType::kSell; } else { tradeArgs = isTradeAll ? cmdLineOptions.tradeAll : cmdLineOptions.trade; - commandType = CoincenterCommand::Type::kTrade; + commandType = CoincenterCommandType::kTrade; } if (!tradeArgs.empty()) { if (!cmdLineOptions.tradeStrategy.empty() && !cmdLineOptions.tradePrice.empty()) { @@ -219,21 +220,21 @@ bool CoincenterCommands::setFromOptions(const CoincenterCmdLineOptions &cmdLineO if (!cmdLineOptions.depositInfo.empty()) { StringOptionParser anyParser(cmdLineOptions.depositInfo); auto [depositCurrency, exchanges] = anyParser.getCurrencyPrivateExchanges(); - _commands.emplace_back(CoincenterCommand::Type::kDepositInfo) + _commands.emplace_back(CoincenterCommandType::kDepositInfo) .setCur1(depositCurrency) .setExchangeNames(std::move(exchanges)); } if (cmdLineOptions.openedOrdersInfo) { auto [ordersConstraints, exchanges] = ParseOrderRequest(cmdLineOptions, *cmdLineOptions.openedOrdersInfo); - _commands.emplace_back(CoincenterCommand::Type::kOrdersOpened) + _commands.emplace_back(CoincenterCommandType::kOrdersOpened) .setOrdersConstraints(std::move(ordersConstraints)) .setExchangeNames(std::move(exchanges)); } if (cmdLineOptions.cancelOpenedOrders) { auto [ordersConstraints, exchanges] = ParseOrderRequest(cmdLineOptions, *cmdLineOptions.cancelOpenedOrders); - _commands.emplace_back(CoincenterCommand::Type::kOrdersCancel) + _commands.emplace_back(CoincenterCommandType::kOrdersCancel) .setOrdersConstraints(std::move(ordersConstraints)) .setExchangeNames(std::move(exchanges)); } @@ -245,7 +246,7 @@ bool CoincenterCommands::setFromOptions(const CoincenterCmdLineOptions &cmdLineO ExchangeNames exchanges; exchanges.push_back(std::move(fromExchange)); exchanges.push_back(std::move(toExchange)); - _commands.emplace_back(CoincenterCommand::Type::kWithdraw) + _commands.emplace_back(CoincenterCommandType::kWithdraw) .setAmount(amountToWithdraw) .setPercentageAmount(isPercentageWithdraw) .setExchangeNames(std::move(exchanges)); @@ -257,7 +258,7 @@ bool CoincenterCommands::setFromOptions(const CoincenterCmdLineOptions &cmdLineO ExchangeNames exchanges; exchanges.push_back(std::move(fromExchange)); exchanges.push_back(std::move(toExchange)); - _commands.emplace_back(CoincenterCommand::Type::kWithdraw) + _commands.emplace_back(CoincenterCommandType::kWithdraw) .setAmount(MonetaryAmount(100, curToWithdraw)) .setPercentageAmount(true) .setExchangeNames(std::move(exchanges)); @@ -266,7 +267,7 @@ bool CoincenterCommands::setFromOptions(const CoincenterCmdLineOptions &cmdLineO if (!cmdLineOptions.withdrawFee.empty()) { StringOptionParser anyParser(cmdLineOptions.withdrawFee); auto [withdrawFeeCur, exchanges] = anyParser.getCurrencyPublicExchanges(); - _commands.emplace_back(CoincenterCommand::Type::kWithdrawFee) + _commands.emplace_back(CoincenterCommandType::kWithdrawFee) .setCur1(withdrawFeeCur) .setExchangeNames(std::move(exchanges)); } @@ -274,7 +275,7 @@ bool CoincenterCommands::setFromOptions(const CoincenterCmdLineOptions &cmdLineO if (!cmdLineOptions.last24hTradedVolume.empty()) { StringOptionParser anyParser(cmdLineOptions.last24hTradedVolume); auto [tradedVolumeMarket, exchanges] = anyParser.getMarketExchanges(); - _commands.emplace_back(CoincenterCommand::Type::kLast24hTradedVolume) + _commands.emplace_back(CoincenterCommandType::kLast24hTradedVolume) .setMarket(tradedVolumeMarket) .setExchangeNames(std::move(exchanges)); } @@ -282,7 +283,7 @@ bool CoincenterCommands::setFromOptions(const CoincenterCmdLineOptions &cmdLineO if (!cmdLineOptions.lastTrades.empty()) { StringOptionParser anyParser(cmdLineOptions.lastTrades); auto [lastTradesMarket, exchanges] = anyParser.getMarketExchanges(); - _commands.emplace_back(CoincenterCommand::Type::kLastTrades) + _commands.emplace_back(CoincenterCommandType::kLastTrades) .setMarket(lastTradesMarket) .setNbLastTrades(cmdLineOptions.nbLastTrades) .setExchangeNames(std::move(exchanges)); @@ -291,7 +292,7 @@ bool CoincenterCommands::setFromOptions(const CoincenterCmdLineOptions &cmdLineO if (!cmdLineOptions.lastPrice.empty()) { StringOptionParser anyParser(cmdLineOptions.lastPrice); auto [lastPriceMarket, exchanges] = anyParser.getMarketExchanges(); - _commands.emplace_back(CoincenterCommand::Type::kLastPrice) + _commands.emplace_back(CoincenterCommandType::kLastPrice) .setMarket(lastPriceMarket) .setExchangeNames(std::move(exchanges)); } diff --git a/src/engine/src/coincentercommandtype.cpp b/src/engine/src/coincentercommandtype.cpp new file mode 100644 index 00000000..a1675f6a --- /dev/null +++ b/src/engine/src/coincentercommandtype.cpp @@ -0,0 +1,45 @@ +#include "coincentercommandtype.hpp" + +#include "cct_exception.hpp" + +namespace cct { +std::string_view CoincenterCommandTypeToString(CoincenterCommandType type) { + switch (type) { + case CoincenterCommandType::kMarkets: + return "Markets"; + case CoincenterCommandType::kConversionPath: + return "ConversionPath"; + case CoincenterCommandType::kLastPrice: + return "LastPrice"; + case CoincenterCommandType::kTicker: + return "Ticker"; + case CoincenterCommandType::kOrderbook: + return "Orderbook"; + case CoincenterCommandType::kLastTrades: + return "LastTrades"; + case CoincenterCommandType::kLast24hTradedVolume: + return "Last24hTradedVolume"; + case CoincenterCommandType::kWithdrawFee: + return "WithdrawFee"; + + case CoincenterCommandType::kBalance: + return "Balance"; + case CoincenterCommandType::kDepositInfo: + return "DepositInfo"; + case CoincenterCommandType::kOrdersOpened: + return "OrdersOpened"; + case CoincenterCommandType::kOrdersCancel: + return "OrdersCancel"; + case CoincenterCommandType::kTrade: + return "Trade"; + case CoincenterCommandType::kBuy: + return "Buy"; + case CoincenterCommandType::kSell: + return "Sell"; + case CoincenterCommandType::kWithdraw: + return "Withdraw"; + default: + throw exception("Unknown command type"); + } +} +} // namespace cct \ No newline at end of file diff --git a/src/engine/src/exchangesorchestrator.cpp b/src/engine/src/exchangesorchestrator.cpp index ffe6c04f..870bbb40 100644 --- a/src/engine/src/exchangesorchestrator.cpp +++ b/src/engine/src/exchangesorchestrator.cpp @@ -16,12 +16,9 @@ namespace { template void FilterVector(MainVec &main, std::span considerSpan) { - // Erases from last to first to keep index consistent - for (auto pos = main.size(); pos > 0; --pos) { - if (!considerSpan[pos - 1]) { - main.erase(main.begin() + pos - 1); - } - } + const auto begIt = main.begin(); + const auto endIt = main.end(); + main.erase(std::remove_if(begIt, endIt, [=](const auto &v) { return !considerSpan[&v - &*begIt]; }), endIt); } using ExchangeAmountPairVector = SmallVector, kTypicalNbPrivateAccounts>; @@ -165,9 +162,10 @@ OpenedOrdersPerExchange ExchangesOrchestrator::getOpenedOrders(std::spanapiPrivate().queryOpenedOrders(openedOrdersConstraints)); }); + std::transform(std::execution::par, selectedExchanges.begin(), selectedExchanges.end(), ret.begin(), + [&](Exchange *e) { + return std::make_pair(e, OrdersSet(e->apiPrivate().queryOpenedOrders(openedOrdersConstraints))); + }); return ret; } @@ -668,12 +666,12 @@ WithdrawInfo ExchangesOrchestrator::withdraw(MonetaryAmount grossAmount, bool is return fromExchange.apiPrivate().withdraw(grossAmount, toExchange.apiPrivate(), withdrawRefreshTime); } -WithdrawFeePerExchange ExchangesOrchestrator::getWithdrawFees(CurrencyCode currencyCode, - ExchangeNameSpan exchangeNames) { +MonetaryAmountPerExchange ExchangesOrchestrator::getWithdrawFees(CurrencyCode currencyCode, + ExchangeNameSpan exchangeNames) { log::info("{} withdraw fees for {}", currencyCode.str(), ConstructAccumulatedExchangeNames(exchangeNames)); UniquePublicSelectedExchanges selectedExchanges = getExchangesTradingCurrency(currencyCode, exchangeNames, true); - WithdrawFeePerExchange withdrawFeePerExchange(selectedExchanges.size()); + MonetaryAmountPerExchange withdrawFeePerExchange(selectedExchanges.size()); std::transform(std::execution::par, selectedExchanges.begin(), selectedExchanges.end(), withdrawFeePerExchange.begin(), [currencyCode](Exchange *e) { return std::make_pair(e, e->queryWithdrawalFee(currencyCode)); }); diff --git a/src/engine/src/processcommandsfromcli.cpp b/src/engine/src/processcommandsfromcli.cpp index 7cdec40f..a11a8366 100644 --- a/src/engine/src/processcommandsfromcli.cpp +++ b/src/engine/src/processcommandsfromcli.cpp @@ -15,10 +15,8 @@ json LoadGeneralConfigAndOverrideOptionsFromCLI(const CoincenterCmdLineOptions & json generalConfigData = GeneralConfig::LoadFile(cmdLineOptions.dataDir); // Override general config options from CLI - if (cmdLineOptions.printResults) { - generalConfigData["printResults"] = true; - } else if (cmdLineOptions.noPrintResults) { - generalConfigData["printResults"] = false; + if (!cmdLineOptions.apiOutputType.empty()) { + generalConfigData["apiOutputType"] = cmdLineOptions.apiOutputType; } if (!cmdLineOptions.logLevel.empty()) { generalConfigData["log"]["level"] = string(cmdLineOptions.logLevel); @@ -41,7 +39,7 @@ void ProcessCommandsFromCLI(std::string_view programName, const CoincenterComman Duration fiatConversionQueryRate = ParseDuration(generalConfigData["fiatConversion"]["rate"].get()); GeneralConfig generalConfig(LoggingInfo(static_cast(generalConfigData["log"])), fiatConversionQueryRate, - generalConfigData["printResults"].get()); + ApiOutputTypeFromString(generalConfigData["apiOutputType"].get())); LoadConfiguration loadConfiguration(cmdLineOptions.dataDir, LoadConfiguration::ExchangeConfigFileType::kProd); diff --git a/src/engine/src/queryresultprinter.cpp b/src/engine/src/queryresultprinter.cpp index 10f85f3c..b0a15763 100644 --- a/src/engine/src/queryresultprinter.cpp +++ b/src/engine/src/queryresultprinter.cpp @@ -1,199 +1,744 @@ #include "queryresultprinter.hpp" #include "balanceperexchangeportfolio.hpp" +#include "cct_json.hpp" #include "cct_string.hpp" +#include "coincentercommandtype.hpp" +#include "durationstring.hpp" #include "exchange.hpp" #include "simpletable.hpp" #include "stringhelpers.hpp" - -#define RETURN_IF_NO_PRINT \ - if (!_doPrint) return +#include "timestring.hpp" +#include "tradedamounts.hpp" +#include "unreachable.hpp" +#include "withdrawinfo.hpp" namespace cct { + +namespace { + +void PrintOutJson(std::ostream &os, json &&in, json &&out) { + json ret; + ret.emplace("in", std::move(in)); + ret.emplace("out", std::move(out)); + os << ret.dump(); +} +} // namespace + +QueryResultPrinter::QueryResultPrinter(std::ostream &os, ApiOutputType apiOutputType) + : _os(os), _apiOutputType(apiOutputType) {} + void QueryResultPrinter::printMarkets(CurrencyCode cur1, CurrencyCode cur2, const MarketsPerExchange &marketsPerExchange) const { - RETURN_IF_NO_PRINT; - string marketsCol("Markets with "); - marketsCol.append(cur1.str()); - if (!cur2.isNeutral()) { - marketsCol.push_back('-'); - marketsCol.append(cur2.str()); - } - SimpleTable t("Exchange", std::move(marketsCol)); - for (const auto &[e, markets] : marketsPerExchange) { - for (Market m : markets) { - t.emplace_back(e->name(), m.str()); + switch (_apiOutputType) { + case ApiOutputType::kFormattedTable: { + string marketsCol("Markets with "); + marketsCol.append(cur1.str()); + if (!cur2.isNeutral()) { + marketsCol.push_back('-'); + marketsCol.append(cur2.str()); + } + SimpleTable t("Exchange", std::move(marketsCol)); + for (const auto &[e, markets] : marketsPerExchange) { + for (Market m : markets) { + t.emplace_back(e->name(), m.str()); + } + } + t.print(_os); + break; } + case ApiOutputType::kJson: { + json in; + in.emplace("req", CoincenterCommandTypeToString(CoincenterCommandType::kMarkets)); + json inOpt; + inOpt.emplace("cur1", cur1.str()); + if (!cur2.isNeutral()) { + inOpt.emplace("cur2", cur2.str()); + } + in.emplace("opt", std::move(inOpt)); + + json out = json::object(); + for (const auto &[e, markets] : marketsPerExchange) { + json marketsForExchange; + for (const Market &m : markets) { + marketsForExchange.emplace_back(m.str()); + } + out.emplace(e->name(), std::move(marketsForExchange)); + } + PrintOutJson(_os, std::move(in), std::move(out)); + break; + } + case ApiOutputType::kNoPrint: + break; } - t.print(); } void QueryResultPrinter::printTickerInformation(const ExchangeTickerMaps &exchangeTickerMaps) const { - RETURN_IF_NO_PRINT; - SimpleTable t("Exchange", "Market", "Bid price", "Bid volume", "Ask price", "Ask volume"); - for (const auto &[e, marketOrderBookMap] : exchangeTickerMaps) { - for (const auto &[m, marketOrderBook] : marketOrderBookMap) { - t.emplace_back(e->name(), m.str(), marketOrderBook.highestBidPrice().str(), - marketOrderBook.amountAtBidPrice().str(), marketOrderBook.lowestAskPrice().str(), - marketOrderBook.amountAtAskPrice().str()); + switch (_apiOutputType) { + case ApiOutputType::kFormattedTable: { + SimpleTable t("Exchange", "Market", "Bid price", "Bid volume", "Ask price", "Ask volume"); + for (const auto &[e, marketOrderBookMap] : exchangeTickerMaps) { + for (const auto &[m, marketOrderBook] : marketOrderBookMap) { + t.emplace_back(e->name(), m.str(), marketOrderBook.highestBidPrice().str(), + marketOrderBook.amountAtBidPrice().str(), marketOrderBook.lowestAskPrice().str(), + marketOrderBook.amountAtAskPrice().str()); + } + // Sort rows in lexicographical order for consistent output + std::sort(t.begin(), t.end()); + } + t.print(_os); + break; } + case ApiOutputType::kJson: { + json in; + in.emplace("req", CoincenterCommandTypeToString(CoincenterCommandType::kTicker)); + + json out = json::object(); + for (const auto &[e, marketOrderBookMap] : exchangeTickerMaps) { + json allTickerForExchange; + for (const auto &[m, marketOrderBook] : marketOrderBookMap) { + json tickerForExchange; + tickerForExchange.emplace("pair", m.str()); + json ask; + json bid; + ask.emplace("a", marketOrderBook.amountAtAskPrice().amountStr()); + ask.emplace("p", marketOrderBook.lowestAskPrice().amountStr()); + bid.emplace("a", marketOrderBook.amountAtBidPrice().amountStr()); + bid.emplace("p", marketOrderBook.highestBidPrice().amountStr()); + tickerForExchange.emplace("ask", std::move(ask)); + tickerForExchange.emplace("bid", std::move(bid)); + allTickerForExchange.emplace_back(tickerForExchange); + } + // Sort rows by market pair for consistent output + std::sort(allTickerForExchange.begin(), allTickerForExchange.end(), [](const json &lhs, const json &rhs) { + return lhs["pair"].get() < rhs["pair"].get(); + }); + out.emplace(e->name(), std::move(allTickerForExchange)); + } + PrintOutJson(_os, std::move(in), std::move(out)); + break; + } + case ApiOutputType::kNoPrint: + break; } - t.print(); } +namespace { +void AppendOrderbookLine(const MarketOrderBook &marketOrderBook, int pos, + std::optional optConversionRate, json &data) { + auto [p, a] = marketOrderBook[pos]; + json &line = data.emplace_back(); + line.emplace("a", a.amountStr()); + line.emplace("p", p.amountStr()); + if (optConversionRate) { + line.emplace("eq", optConversionRate->amountStr()); + } +} +} // namespace + void QueryResultPrinter::printMarketOrderBooks( + Market m, CurrencyCode equiCurrencyCode, std::optional depth, const MarketOrderBookConversionRates &marketOrderBooksConversionRates) const { - RETURN_IF_NO_PRINT; - for (const auto &[exchangeName, marketOrderBook, optConversionRate] : marketOrderBooksConversionRates) { - marketOrderBook.print(std::cout, exchangeName, optConversionRate); + switch (_apiOutputType) { + case ApiOutputType::kFormattedTable: { + for (const auto &[exchangeName, marketOrderBook, optConversionRate] : marketOrderBooksConversionRates) { + marketOrderBook.print(_os, exchangeName, optConversionRate); + } + break; + } + case ApiOutputType::kJson: { + json in; + in.emplace("req", CoincenterCommandTypeToString(CoincenterCommandType::kOrderbook)); + json inOpt; + inOpt.emplace("pair", m.str()); + if (!equiCurrencyCode.isNeutral()) { + inOpt.emplace("equiCurrency", equiCurrencyCode.str()); + } + if (depth) { + inOpt.emplace("depth", *depth); + } + in.emplace("opt", std::move(inOpt)); + + json out = json::object(); + for (const auto &[exchangeName, marketOrderBook, optConversionRate] : marketOrderBooksConversionRates) { + json marketOrderBookForExchange; + json bidsForExchange; + json asksForExchange; + for (int bidPos = 1; bidPos <= marketOrderBook.nbBidPrices(); ++bidPos) { + AppendOrderbookLine(marketOrderBook, -bidPos, optConversionRate, bidsForExchange); + } + marketOrderBookForExchange.emplace("bid", std::move(bidsForExchange)); + for (int askPos = 1; askPos <= marketOrderBook.nbAskPrices(); ++askPos) { + AppendOrderbookLine(marketOrderBook, askPos, optConversionRate, asksForExchange); + } + marketOrderBookForExchange.emplace("ask", std::move(asksForExchange)); + out.emplace(exchangeName, std::move(marketOrderBookForExchange)); + } + PrintOutJson(_os, std::move(in), std::move(out)); + break; + } + case ApiOutputType::kNoPrint: + break; } } -void QueryResultPrinter::printBalance(const BalancePerExchange &balancePerExchange) const { - RETURN_IF_NO_PRINT; - BalancePerExchangePortfolio totalBalance; - for (const auto &[exchangePtr, balancePortfolio] : balancePerExchange) { - totalBalance.add(exchangePtr->apiPrivate().exchangeName(), balancePortfolio); +void QueryResultPrinter::printBalance(const BalancePerExchange &balancePerExchange, CurrencyCode equiCurrency) const { + BalancePerExchangePortfolio totalBalance(balancePerExchange); + switch (_apiOutputType) { + case ApiOutputType::kFormattedTable: { + totalBalance.printTable(_os, balancePerExchange.size() > 1); + break; + } + case ApiOutputType::kJson: { + json in; + in.emplace("req", CoincenterCommandTypeToString(CoincenterCommandType::kBalance)); + json inOpt = json::object(); + if (!equiCurrency.isNeutral()) { + inOpt.emplace("equiCurrency", equiCurrency.str()); + } + in.emplace("opt", std::move(inOpt)); + + PrintOutJson(_os, std::move(in), totalBalance.printJson(equiCurrency)); + break; + } + case ApiOutputType::kNoPrint: + break; } - totalBalance.print(std::cout, balancePerExchange.size() > 1); } void QueryResultPrinter::printDepositInfo(CurrencyCode depositCurrencyCode, const WalletPerExchange &walletPerExchange) const { - RETURN_IF_NO_PRINT; - string walletStr(depositCurrencyCode.str()); - walletStr.append(" address"); - SimpleTable t("Exchange", "Account", std::move(walletStr), "Destination Tag"); - for (const auto &[exchangePtr, wallet] : walletPerExchange) { - t.emplace_back(exchangePtr->name(), exchangePtr->keyName(), wallet.address(), wallet.tag()); + switch (_apiOutputType) { + case ApiOutputType::kFormattedTable: { + string walletStr(depositCurrencyCode.str()); + walletStr.append(" address"); + SimpleTable t("Exchange", "Account", std::move(walletStr), "Destination Tag"); + for (const auto &[exchangePtr, wallet] : walletPerExchange) { + t.emplace_back(exchangePtr->name(), exchangePtr->keyName(), wallet.address(), wallet.tag()); + } + t.print(_os); + break; + } + case ApiOutputType::kJson: { + json in; + in.emplace("req", CoincenterCommandTypeToString(CoincenterCommandType::kDepositInfo)); + json inOpt; + inOpt.emplace("cur", depositCurrencyCode.str()); + in.emplace("opt", std::move(inOpt)); + + json out = json::object(); + for (const auto &[exchangePtr, wallet] : walletPerExchange) { + json depositPerExchangeData; + + depositPerExchangeData.emplace("address", wallet.address()); + if (wallet.hasTag()) { + depositPerExchangeData.emplace("tag", wallet.tag()); + } + + auto it = out.find(exchangePtr->name()); + if (it == out.end()) { + json depositInfoForExchangeUser; + depositInfoForExchangeUser.emplace(exchangePtr->keyName(), std::move(depositPerExchangeData)); + out.emplace(exchangePtr->name(), std::move(depositInfoForExchangeUser)); + } else { + it->emplace(exchangePtr->keyName(), std::move(depositPerExchangeData)); + } + } + PrintOutJson(_os, std::move(in), std::move(out)); + break; + } + case ApiOutputType::kNoPrint: + break; + } +} + +namespace { +inline const char *TradeModeToStr(TradeMode tradeMode) { return tradeMode == TradeMode::kReal ? "real" : "simulation"; } + +json TradeOptionsToJson(const TradeOptions &tradeOptions) { + json priceOptionsJson; + const PriceOptions &priceOptions = tradeOptions.priceOptions(); + priceOptionsJson.emplace("strategy", priceOptions.priceStrategyStr(false)); + if (priceOptions.isFixedPrice()) { + priceOptionsJson.emplace("fixedPrice", priceOptions.fixedPrice().str()); } - t.print(); + if (priceOptions.isRelativePrice()) { + priceOptionsJson.emplace("relativePrice", priceOptions.relativePrice()); + } + json ret; + ret.emplace("price", std::move(priceOptionsJson)); + ret.emplace("maxTradeTime", DurationToString(tradeOptions.maxTradeTime())); + ret.emplace("minTimeBetweenPriceUpdates", DurationToString(tradeOptions.minTimeBetweenPriceUpdates())); + ret.emplace("mode", TradeModeToStr(tradeOptions.tradeMode())); + ret.emplace("timeoutAction", tradeOptions.timeoutActionStr()); + return ret; } +} // namespace + +void QueryResultPrinter::printTrades(const TradedAmountsPerExchange &tradedAmountsPerExchange, MonetaryAmount amount, + bool isPercentageTrade, CurrencyCode toCurrency, const TradeOptions &tradeOptions, + CoincenterCommandType commandType) const { + switch (_apiOutputType) { + case ApiOutputType::kFormattedTable: { + string tradedFromStr("Traded from amount ("); + tradedFromStr.append(TradeModeToStr(tradeOptions.tradeMode())); + tradedFromStr.push_back(')'); + string tradedToStr("Traded to amount ("); + tradedToStr.append(TradeModeToStr(tradeOptions.tradeMode())); + tradedToStr.push_back(')'); + SimpleTable t("Exchange", "Account", std::move(tradedFromStr), std::move(tradedToStr)); + for (const auto &[exchangePtr, tradedAmount] : tradedAmountsPerExchange) { + t.emplace_back(exchangePtr->name(), exchangePtr->keyName(), tradedAmount.tradedFrom.str(), + tradedAmount.tradedTo.str()); + } + t.print(_os); + break; + } + case ApiOutputType::kJson: { + json in; + in.emplace("req", CoincenterCommandTypeToString(commandType)); + json fromJson; + fromJson.emplace("amount", amount.amountStr()); + fromJson.emplace("currency", amount.currencyStr()); + fromJson.emplace("isPercentage", isPercentageTrade); + + json inOpt; + switch (commandType) { + case CoincenterCommandType::kBuy: + inOpt.emplace("to", std::move(fromJson)); + break; + case CoincenterCommandType::kSell: + inOpt.emplace("from", std::move(fromJson)); + break; + case CoincenterCommandType::kTrade: { + json toJson; + toJson.emplace("currency", toCurrency.str()); -void QueryResultPrinter::printOpenedOrders(const OpenedOrdersPerExchange &openedOrdersPerExchange) const { - RETURN_IF_NO_PRINT; - SimpleTable t("Exchange", "Account", "Exchange Id", "Placed time", "Side", "Price", "Matched Amount", - "Remaining Amount"); - for (const auto &[exchangePtr, openedOrders] : openedOrdersPerExchange) { - for (const Order &openedOrder : openedOrders) { - t.emplace_back(exchangePtr->name(), exchangePtr->keyName(), openedOrder.id(), openedOrder.placedTimeStr(), - openedOrder.sideStr(), openedOrder.price().str(), openedOrder.matchedVolume().str(), - openedOrder.remainingVolume().str()); + inOpt.emplace("from", std::move(fromJson)); + inOpt.emplace("to", std::move(toJson)); + break; + } + default: + unreachable(); + } + + inOpt.emplace("options", TradeOptionsToJson(tradeOptions)); + in.emplace("opt", std::move(inOpt)); + + json out = json::object(); + for (const auto &[exchangePtr, tradedAmount] : tradedAmountsPerExchange) { + json tradedAmountPerExchangeJson; + tradedAmountPerExchangeJson.emplace("from", tradedAmount.tradedFrom.amountStr()); + tradedAmountPerExchangeJson.emplace("to", tradedAmount.tradedTo.amountStr()); + + auto it = out.find(exchangePtr->name()); + if (it == out.end()) { + json tradedAmountPerExchangeUser; + tradedAmountPerExchangeUser.emplace(exchangePtr->keyName(), std::move(tradedAmountPerExchangeJson)); + out.emplace(exchangePtr->name(), std::move(tradedAmountPerExchangeUser)); + } else { + it->emplace(exchangePtr->keyName(), std::move(tradedAmountPerExchangeJson)); + } + } + PrintOutJson(_os, std::move(in), std::move(out)); + break; } + case ApiOutputType::kNoPrint: + break; } - t.print(); } -void QueryResultPrinter::printCancelledOrders(const NbCancelledOrdersPerExchange &nbCancelledOrdersPerExchange) const { - RETURN_IF_NO_PRINT; - SimpleTable t("Exchange", "Account", "Number of cancelled orders"); - for (const auto &[exchangePtr, nbCancelledOrders] : nbCancelledOrdersPerExchange) { - t.emplace_back(exchangePtr->name(), exchangePtr->keyName(), nbCancelledOrders); +namespace { +json OrdersConstraintsToJson(const OrdersConstraints &ordersConstraints) { + json ret; + if (ordersConstraints.isCur1Defined()) { + ret.emplace("cur1", ordersConstraints.curStr1()); + } + if (ordersConstraints.isCur2Defined()) { + ret.emplace("cur2", ordersConstraints.curStr2()); + } + if (ordersConstraints.isPlacedTimeBeforeDefined()) { + ret.emplace("placedBefore", ToString(ordersConstraints.placedBefore())); + } + if (ordersConstraints.isPlacedTimeAfterDefined()) { + ret.emplace("placedAfter", ToString(ordersConstraints.placedAfter())); + } + if (ordersConstraints.isOrderIdDefined()) { + json orderIds = json::array(); + for (const OrderId &orderId : ordersConstraints.orderIdSet()) { + orderIds.emplace_back(orderId); + } + ret.emplace("matchIds", std::move(orderIds)); + } + return ret; +} +} // namespace + +void QueryResultPrinter::printOpenedOrders(const OpenedOrdersPerExchange &openedOrdersPerExchange, + const OrdersConstraints &ordersConstraints) const { + switch (_apiOutputType) { + case ApiOutputType::kFormattedTable: { + SimpleTable t("Exchange", "Account", "Exchange Id", "Placed time", "Side", "Price", "Matched Amount", + "Remaining Amount"); + for (const auto &[exchangePtr, openedOrders] : openedOrdersPerExchange) { + for (const Order &openedOrder : openedOrders) { + t.emplace_back(exchangePtr->name(), exchangePtr->keyName(), openedOrder.id(), openedOrder.placedTimeStr(), + openedOrder.sideStr(), openedOrder.price().str(), openedOrder.matchedVolume().str(), + openedOrder.remainingVolume().str()); + } + } + t.print(_os); + break; + } + case ApiOutputType::kJson: { + json in; + in.emplace("req", CoincenterCommandTypeToString(CoincenterCommandType::kOrdersOpened)); + json inOpt = OrdersConstraintsToJson(ordersConstraints); + + if (!inOpt.empty()) { + in.emplace("opt", std::move(inOpt)); + } + + json out = json::object(); + for (const auto &[exchangePtr, openedOrders] : openedOrdersPerExchange) { + json orders = json::array(); + for (const Order &openedOrder : openedOrders) { + json &order = orders.emplace_back(); + order.emplace("id", openedOrder.id()); + order.emplace("pair", openedOrder.market().str()); + order.emplace("placedTime", openedOrder.placedTimeStr()); + order.emplace("side", openedOrder.sideStr()); + order.emplace("price", openedOrder.price().amountStr()); + order.emplace("matched", openedOrder.matchedVolume().amountStr()); + order.emplace("remaining", openedOrder.remainingVolume().amountStr()); + } + + auto it = out.find(exchangePtr->name()); + if (it == out.end()) { + json ordersPerExchangeUser; + ordersPerExchangeUser.emplace(exchangePtr->keyName(), std::move(orders)); + out.emplace(exchangePtr->name(), std::move(ordersPerExchangeUser)); + } else { + it->emplace(exchangePtr->keyName(), std::move(orders)); + } + } + PrintOutJson(_os, std::move(in), std::move(out)); + break; + } + case ApiOutputType::kNoPrint: + break; + } +} + +void QueryResultPrinter::printCancelledOrders(const NbCancelledOrdersPerExchange &nbCancelledOrdersPerExchange, + const OrdersConstraints &ordersConstraints) const { + switch (_apiOutputType) { + case ApiOutputType::kFormattedTable: { + SimpleTable t("Exchange", "Account", "Number of cancelled orders"); + for (const auto &[exchangePtr, nbCancelledOrders] : nbCancelledOrdersPerExchange) { + t.emplace_back(exchangePtr->name(), exchangePtr->keyName(), nbCancelledOrders); + } + t.print(_os); + break; + } + case ApiOutputType::kJson: { + json in; + in.emplace("req", CoincenterCommandTypeToString(CoincenterCommandType::kOrdersCancel)); + json inOpt = OrdersConstraintsToJson(ordersConstraints); + + if (!inOpt.empty()) { + in.emplace("opt", std::move(inOpt)); + } + + json out = json::object(); + for (const auto &[exchangePtr, nbCancelledOrders] : nbCancelledOrdersPerExchange) { + json cancelledOrdersForAccount; + cancelledOrdersForAccount.emplace("nb", nbCancelledOrders); + + auto it = out.find(exchangePtr->name()); + if (it == out.end()) { + json cancelledOrdersForExchangeUser; + cancelledOrdersForExchangeUser.emplace(exchangePtr->keyName(), std::move(cancelledOrdersForAccount)); + out.emplace(exchangePtr->name(), std::move(cancelledOrdersForExchangeUser)); + } else { + it->emplace(exchangePtr->keyName(), std::move(cancelledOrdersForAccount)); + } + } + PrintOutJson(_os, std::move(in), std::move(out)); + break; + } + case ApiOutputType::kNoPrint: + break; } - t.print(); } void QueryResultPrinter::printConversionPath(Market m, const ConversionPathPerExchange &conversionPathsPerExchange) const { - RETURN_IF_NO_PRINT; - string conversionPathStrHeader("Fastest conversion path for "); - conversionPathStrHeader.append(m.str()); - SimpleTable t("Exchange", std::move(conversionPathStrHeader)); - for (const auto &[e, conversionPath] : conversionPathsPerExchange) { - if (!conversionPath.empty()) { - string conversionPathStr; - for (Market market : conversionPath) { - if (!conversionPathStr.empty()) { - conversionPathStr.push_back(','); + switch (_apiOutputType) { + case ApiOutputType::kFormattedTable: { + string conversionPathStrHeader("Fastest conversion path for "); + conversionPathStrHeader.append(m.str()); + SimpleTable t("Exchange", std::move(conversionPathStrHeader)); + for (const auto &[e, conversionPath] : conversionPathsPerExchange) { + if (!conversionPath.empty()) { + string conversionPathStr; + for (Market market : conversionPath) { + if (!conversionPathStr.empty()) { + conversionPathStr.push_back(','); + } + conversionPathStr.append(market.str()); + } + t.emplace_back(e->name(), std::move(conversionPathStr)); } - conversionPathStr.append(market.str()); } - t.emplace_back(e->name(), std::move(conversionPathStr)); + t.print(_os); + break; } + case ApiOutputType::kJson: { + json in; + in.emplace("req", CoincenterCommandTypeToString(CoincenterCommandType::kConversionPath)); + json inOpt; + inOpt.emplace("market", m.str()); + in.emplace("opt", std::move(inOpt)); + + json out = json::object(); + for (const auto &[e, conversionPath] : conversionPathsPerExchange) { + if (!conversionPath.empty()) { + json conversionPathForExchange; + for (Market market : conversionPath) { + conversionPathForExchange.emplace_back(market.str()); + } + out.emplace(e->name(), std::move(conversionPathForExchange)); + } + } + PrintOutJson(_os, std::move(in), std::move(out)); + break; + } + case ApiOutputType::kNoPrint: + break; } - t.print(); } -void QueryResultPrinter::printWithdrawFees(const WithdrawFeePerExchange &withdrawFeePerExchange) const { - RETURN_IF_NO_PRINT; - SimpleTable t("Exchange", "Withdraw fee"); - for (const auto &[e, withdrawFee] : withdrawFeePerExchange) { - t.emplace_back(e->name(), withdrawFee.str()); +void QueryResultPrinter::printWithdrawFees(const MonetaryAmountPerExchange &withdrawFeePerExchange, + CurrencyCode cur) const { + switch (_apiOutputType) { + case ApiOutputType::kFormattedTable: { + SimpleTable t("Exchange", "Withdraw fee"); + for (const auto &[e, withdrawFee] : withdrawFeePerExchange) { + t.emplace_back(e->name(), withdrawFee.str()); + } + t.print(_os); + break; + } + case ApiOutputType::kJson: { + json in; + in.emplace("req", CoincenterCommandTypeToString(CoincenterCommandType::kWithdrawFee)); + json inOpt; + inOpt.emplace("cur", cur.str()); + in.emplace("opt", std::move(inOpt)); + + json out = json::object(); + for (const auto &[e, withdrawFee] : withdrawFeePerExchange) { + out.emplace(e->name(), withdrawFee.amountStr()); + } + PrintOutJson(_os, std::move(in), std::move(out)); + break; + } + case ApiOutputType::kNoPrint: + break; } - t.print(); } void QueryResultPrinter::printLast24hTradedVolume(Market m, const MonetaryAmountPerExchange &tradedVolumePerExchange) const { - RETURN_IF_NO_PRINT; - string headerTradedVolume("Last 24h "); - headerTradedVolume.append(m.str()); - headerTradedVolume.append(" traded volume"); - SimpleTable t("Exchange", std::move(headerTradedVolume)); - for (const auto &[e, tradedVolume] : tradedVolumePerExchange) { - t.emplace_back(e->name(), tradedVolume.str()); - } - t.print(); -} - -void QueryResultPrinter::printLastTrades(Market m, const LastTradesPerExchange &lastTradesPerExchange) const { - RETURN_IF_NO_PRINT; - for (const auto &[exchangePtr, lastTrades] : lastTradesPerExchange) { - string buyTitle(m.baseStr()); - buyTitle.append(" buys"); - string sellTitle(m.baseStr()); - sellTitle.append(" sells"); - string priceTitle("Price in "); - priceTitle.append(m.quoteStr()); - - string title(exchangePtr->name()); - title.append(" trades - UTC"); - - SimpleTable t(std::move(title), std::move(buyTitle), std::move(priceTitle), std::move(sellTitle)); - std::array totalAmounts{MonetaryAmount(0, m.base()), MonetaryAmount(0, m.base())}; - MonetaryAmount totalPrice(0, m.quote()); - std::array nb{}; - for (const PublicTrade &trade : lastTrades) { - if (trade.side() == TradeSide::kBuy) { - t.emplace_back(trade.timeStr(), trade.amount().amountStr(), trade.price().amountStr(), ""); - totalAmounts[0] += trade.amount(); - ++nb[0]; - } else { - t.emplace_back(trade.timeStr(), "", trade.price().amountStr(), trade.amount().amountStr()); - totalAmounts[1] += trade.amount(); - ++nb[1]; - } - totalPrice += trade.price(); - } - if (nb[0] + nb[1] > 0) { - t.push_back(SimpleTable::Row::kDivider); - std::array summary; - for (int buyOrSell = 0; buyOrSell < 2; ++buyOrSell) { - summary[buyOrSell].append(totalAmounts[buyOrSell].str()); - summary[buyOrSell].append(" ("); - AppendString(summary[buyOrSell], nb[buyOrSell]); - summary[buyOrSell].push_back(' '); - summary[buyOrSell].append(buyOrSell == 0 ? "buys" : "sells"); - summary[buyOrSell].push_back(')'); - } - - MonetaryAmount avgPrice = totalPrice / (nb[0] + nb[1]); - t.emplace_back("Summary", std::move(summary[0]), avgPrice.str(), std::move(summary[1])); - } - - t.print(); + switch (_apiOutputType) { + case ApiOutputType::kFormattedTable: { + string headerTradedVolume("Last 24h "); + headerTradedVolume.append(m.str()); + headerTradedVolume.append(" traded volume"); + SimpleTable t("Exchange", std::move(headerTradedVolume)); + for (const auto &[e, tradedVolume] : tradedVolumePerExchange) { + t.emplace_back(e->name(), tradedVolume.str()); + } + t.print(_os); + break; + } + case ApiOutputType::kJson: { + json in; + in.emplace("req", CoincenterCommandTypeToString(CoincenterCommandType::kLast24hTradedVolume)); + json inOpt; + inOpt.emplace("market", m.str()); + in.emplace("opt", std::move(inOpt)); + + json out = json::object(); + for (const auto &[e, tradedVolume] : tradedVolumePerExchange) { + out.emplace(e->name(), tradedVolume.amountStr()); + } + PrintOutJson(_os, std::move(in), std::move(out)); + break; + } + case ApiOutputType::kNoPrint: + break; + } +} + +void QueryResultPrinter::printLastTrades(Market m, int nbLastTrades, + const LastTradesPerExchange &lastTradesPerExchange) const { + switch (_apiOutputType) { + case ApiOutputType::kFormattedTable: { + for (const auto &[exchangePtr, lastTrades] : lastTradesPerExchange) { + string buyTitle(m.baseStr()); + buyTitle.append(" buys"); + string sellTitle(m.baseStr()); + sellTitle.append(" sells"); + string priceTitle("Price in "); + priceTitle.append(m.quoteStr()); + + string title(exchangePtr->name()); + title.append(" trades - UTC"); + + SimpleTable t(std::move(title), std::move(buyTitle), std::move(priceTitle), std::move(sellTitle)); + std::array totalAmounts{MonetaryAmount(0, m.base()), MonetaryAmount(0, m.base())}; + MonetaryAmount totalPrice(0, m.quote()); + std::array nb{}; + for (const PublicTrade &trade : lastTrades) { + if (trade.side() == TradeSide::kBuy) { + t.emplace_back(trade.timeStr(), trade.amount().amountStr(), trade.price().amountStr(), ""); + totalAmounts[0] += trade.amount(); + ++nb[0]; + } else { + t.emplace_back(trade.timeStr(), "", trade.price().amountStr(), trade.amount().amountStr()); + totalAmounts[1] += trade.amount(); + ++nb[1]; + } + totalPrice += trade.price(); + } + if (nb[0] + nb[1] > 0) { + t.push_back(SimpleTable::Row::kDivider); + std::array summary; + for (int buyOrSell = 0; buyOrSell < 2; ++buyOrSell) { + summary[buyOrSell].append(totalAmounts[buyOrSell].str()); + summary[buyOrSell].append(" ("); + AppendString(summary[buyOrSell], nb[buyOrSell]); + summary[buyOrSell].push_back(' '); + summary[buyOrSell].append(buyOrSell == 0 ? "buys" : "sells"); + summary[buyOrSell].push_back(')'); + } + + MonetaryAmount avgPrice = totalPrice / (nb[0] + nb[1]); + t.emplace_back("Summary", std::move(summary[0]), avgPrice.str(), std::move(summary[1])); + } + + t.print(_os); + } + break; + } + case ApiOutputType::kJson: { + json in; + in.emplace("req", CoincenterCommandTypeToString(CoincenterCommandType::kLastTrades)); + json inOpt; + inOpt.emplace("market", m.str()); + inOpt.emplace("nb", nbLastTrades); + in.emplace("opt", std::move(inOpt)); + + json out = json::object(); + for (const auto &[exchangePtr, lastTrades] : lastTradesPerExchange) { + json lastTradesJson = json::array(); + for (const PublicTrade &trade : lastTrades) { + json &lastTrade = lastTradesJson.emplace_back(); + lastTrade.emplace("a", trade.amount().amountStr()); + lastTrade.emplace("p", trade.price().amountStr()); + lastTrade.emplace("time", trade.timeStr()); + lastTrade.emplace("side", SideStr(trade.side())); + } + out.emplace(exchangePtr->name(), std::move(lastTradesJson)); + } + PrintOutJson(_os, std::move(in), std::move(out)); + break; + } + case ApiOutputType::kNoPrint: + break; } } void QueryResultPrinter::printLastPrice(Market m, const MonetaryAmountPerExchange &pricePerExchange) const { - RETURN_IF_NO_PRINT; - string headerLastPrice(m.str()); - headerLastPrice.append(" last price"); - SimpleTable t("Exchange", std::move(headerLastPrice)); - for (const auto &[e, lastPrice] : pricePerExchange) { - t.emplace_back(e->name(), lastPrice.str()); + switch (_apiOutputType) { + case ApiOutputType::kFormattedTable: { + string headerLastPrice(m.str()); + headerLastPrice.append(" last price"); + SimpleTable t("Exchange", std::move(headerLastPrice)); + for (const auto &[e, lastPrice] : pricePerExchange) { + t.emplace_back(e->name(), lastPrice.str()); + } + t.print(_os); + break; + } + case ApiOutputType::kJson: { + json in; + in.emplace("req", CoincenterCommandTypeToString(CoincenterCommandType::kLastPrice)); + json inOpt; + inOpt.emplace("market", m.str()); + in.emplace("opt", std::move(inOpt)); + + json out = json::object(); + for (const auto &[e, lastPrice] : pricePerExchange) { + out.emplace(e->name(), lastPrice.amountStr()); + } + PrintOutJson(_os, std::move(in), std::move(out)); + break; + } + case ApiOutputType::kNoPrint: + break; + } +} + +void QueryResultPrinter::printWithdraw(const WithdrawInfo &withdrawInfo, MonetaryAmount grossAmount, + bool isPercentageWithdraw, const ExchangeName &fromPrivateExchangeName, + const ExchangeName &toPrivateExchangeName) const { + switch (_apiOutputType) { + case ApiOutputType::kFormattedTable: { + SimpleTable t("From Exchange", "To Exchange", "Gross withdraw amount", "Initiated time", "Received time", + "Net received amount"); + t.emplace_back(fromPrivateExchangeName.name(), toPrivateExchangeName.name(), grossAmount.str(), + ToString(withdrawInfo.initiatedTime()), ToString(withdrawInfo.receivedTime()), + withdrawInfo.netEmittedAmount().str()); + t.print(_os); + break; + } + case ApiOutputType::kJson: { + json in; + in.emplace("req", CoincenterCommandTypeToString(CoincenterCommandType::kWithdraw)); + json inOpt; + inOpt.emplace("cur", grossAmount.currencyStr()); + inOpt.emplace("grossAmount", grossAmount.amountStr()); + inOpt.emplace("isPercentage", isPercentageWithdraw); + in.emplace("opt", std::move(inOpt)); + + json from; + from.emplace("exchange", fromPrivateExchangeName.name()); + from.emplace("account", fromPrivateExchangeName.keyName()); + + json to; + to.emplace("exchange", toPrivateExchangeName.name()); + to.emplace("account", toPrivateExchangeName.keyName()); + to.emplace("address", withdrawInfo.receivingWallet().address()); + if (withdrawInfo.receivingWallet().hasTag()) { + to.emplace("tag", withdrawInfo.receivingWallet().tag()); + } + + json out; + out.emplace("from", std::move(from)); + out.emplace("to", std::move(to)); + out.emplace("initiatedTime", ToString(withdrawInfo.initiatedTime())); + out.emplace("receivedTime", ToString(withdrawInfo.receivedTime())); + out.emplace("netReceivedAmount", withdrawInfo.netEmittedAmount().amountStr()); + + PrintOutJson(_os, std::move(in), std::move(out)); + break; + } + case ApiOutputType::kNoPrint: + break; } - t.print(); } -} // namespace cct +} // namespace cct \ No newline at end of file diff --git a/src/engine/test/exchangedata_test.hpp b/src/engine/test/exchangedata_test.hpp index debc9188..b3c0abb1 100644 --- a/src/engine/test/exchangedata_test.hpp +++ b/src/engine/test/exchangedata_test.hpp @@ -13,22 +13,7 @@ namespace cct { class ExchangesBaseTest : public ::testing::Test { protected: - ExchangesBaseTest() = default; - void SetUp() override { - for (MonetaryAmount a : amounts1) { - balancePortfolio1.add(a); - } - for (MonetaryAmount a : amounts2) { - balancePortfolio2.add(a); - } - for (MonetaryAmount a : amounts3) { - balancePortfolio3.add(a); - } - for (MonetaryAmount a : amounts4) { - balancePortfolio4.add(a); - } - EXPECT_CALL(exchangePrivate5, queryAccountBalance(testing::_)).WillRepeatedly(testing::Return(emptyBalance)); EXPECT_CALL(exchangePrivate6, queryAccountBalance(testing::_)).WillRepeatedly(testing::Return(emptyBalance)); EXPECT_CALL(exchangePrivate7, queryAccountBalance(testing::_)).WillRepeatedly(testing::Return(emptyBalance)); @@ -113,10 +98,10 @@ class ExchangesBaseTest : public ::testing::Test { MonetaryAmount("15004MATIC"), MonetaryAmount("155USD"), MonetaryAmount("107.5USDT"), MonetaryAmount("1200EUR")}; - BalancePortfolio balancePortfolio1; - BalancePortfolio balancePortfolio2; - BalancePortfolio balancePortfolio3; - BalancePortfolio balancePortfolio4; + BalancePortfolio balancePortfolio1{amounts1}; + BalancePortfolio balancePortfolio2{amounts2}; + BalancePortfolio balancePortfolio3{amounts3}; + BalancePortfolio balancePortfolio4{amounts4}; BalancePortfolio emptyBalance; }; } // namespace cct \ No newline at end of file diff --git a/src/engine/test/exchangesorchestrator_test.cpp b/src/engine/test/exchangesorchestrator_test.cpp index 9739fbc3..7383c098 100644 --- a/src/engine/test/exchangesorchestrator_test.cpp +++ b/src/engine/test/exchangesorchestrator_test.cpp @@ -192,7 +192,9 @@ TEST_F(ExchangeOrchestratorTest, GetOpenedOrders) { TradeSide::kBuy)}; EXPECT_CALL(exchangePrivate4, queryOpenedOrders(noConstraints)).WillOnce(testing::Return(orders4)); - OpenedOrdersPerExchange ret{{&exchange2, orders2}, {&exchange3, orders3}, {&exchange4, orders4}}; + OpenedOrdersPerExchange ret{{&exchange2, OrdersSet(orders2.begin(), orders2.end())}, + {&exchange3, OrdersSet(orders3.begin(), orders3.end())}, + {&exchange4, OrdersSet(orders4.begin(), orders4.end())}}; EXPECT_EQ(exchangesOrchestrator.getOpenedOrders(privateExchangeNames, noConstraints), ret); } @@ -334,7 +336,7 @@ TEST_F(ExchangeOrchestratorTest, GetExchangesTradingMarket) { EXPECT_CALL(exchangePrivate, placeOrder(from, vol, pri, testing::_)).Times(0); \ } -#define EXPECT_TWO_STEP_TRADE(exchangePublic, exchangePrivate) \ +#define EXPECT_TWO_STEP_TRADE(exchangePublic, exchangePrivate, m1, m2) \ if (tradableMarketsCall == TradableMarkets::kExpectCall) { \ EXPECT_CALL(exchangePublic, queryTradableMarkets()).WillOnce(testing::Return(markets)); \ } else if (tradableMarketsCall == TradableMarkets::kExpectNoCall) { \ @@ -466,25 +468,25 @@ class ExchangeOrchestratorTradeTest : public ExchangeOrchestratorTest { TradableMarkets tradableMarketsCall, OrderBook orderBookCall, AllOrderBooks allOrderBooksCall, bool makeMarketAvailable) { CurrencyCode interCur("AAA"); - Market m1(from.currencyCode(), interCur); - Market m2(interCur, toCurrency); + Market market1(from.currencyCode(), interCur); + Market market2(interCur, toCurrency); if (side == TradeSide::kBuy) { - m1 = Market(toCurrency, interCur); - m2 = Market(interCur, from.currencyCode()); + market1 = Market(toCurrency, interCur); + market2 = Market(interCur, from.currencyCode()); } else { - m1 = Market(from.currencyCode(), interCur); - m2 = Market(interCur, toCurrency); + market1 = Market(from.currencyCode(), interCur); + market2 = Market(interCur, toCurrency); } // Choose price of 1 such that we do not need to make a division if it's a buy. - MonetaryAmount vol1(from, m1.base()); - MonetaryAmount vol2(from, m2.base()); - MonetaryAmount pri1(1, m1.quote()); - MonetaryAmount pri2(1, m2.quote()); + MonetaryAmount vol1(from, market1.base()); + MonetaryAmount vol2(from, market2.base()); + MonetaryAmount pri1(1, market1.quote()); + MonetaryAmount pri2(1, market2.quote()); - MonetaryAmount maxVol1(std::numeric_limits::max(), m1.base(), + MonetaryAmount maxVol1(std::numeric_limits::max(), market1.base(), volAndPriDec1.volNbDecimals); - MonetaryAmount maxVol2(std::numeric_limits::max(), m2.base(), + MonetaryAmount maxVol2(std::numeric_limits::max(), market2.base(), volAndPriDec1.volNbDecimals); MonetaryAmount tradedTo1(from, interCur); @@ -492,14 +494,12 @@ class ExchangeOrchestratorTradeTest : public ExchangeOrchestratorTest { MonetaryAmount deltaPri1(1, pri1.currencyCode(), volAndPriDec1.priNbDecimals); MonetaryAmount deltaPri2(1, pri2.currencyCode(), volAndPriDec1.priNbDecimals); - MonetaryAmount askPrice1 = side == TradeSide::kBuy ? pri1 : pri1 + deltaPri1; - MonetaryAmount askPrice2 = side == TradeSide::kBuy ? pri2 : pri2 + deltaPri2; - MonetaryAmount bidPrice1 = side == TradeSide::kSell ? pri1 : pri1 - deltaPri1; - MonetaryAmount bidPrice2 = side == TradeSide::kSell ? pri2 : pri2 - deltaPri2; - MarketOrderBook marketOrderbook1{askPrice1, maxVol1, bidPrice1, - maxVol1, volAndPriDec1, MarketOrderBook::kDefaultDepth}; - MarketOrderBook marketOrderbook2{askPrice2, maxVol2, bidPrice2, - maxVol2, volAndPriDec1, MarketOrderBook::kDefaultDepth}; + MonetaryAmount askPri1 = side == TradeSide::kBuy ? pri1 : pri1 + deltaPri1; + MonetaryAmount askPri2 = side == TradeSide::kBuy ? pri2 : pri2 + deltaPri2; + MonetaryAmount bidPri1 = side == TradeSide::kSell ? pri1 : pri1 - deltaPri1; + MonetaryAmount bidPri2 = side == TradeSide::kSell ? pri2 : pri2 - deltaPri2; + MarketOrderBook marketOrderbook1{askPri1, maxVol1, bidPri1, maxVol1, volAndPriDec1, MarketOrderBook::kDefaultDepth}; + MarketOrderBook marketOrderbook2{askPri2, maxVol2, bidPri2, maxVol2, volAndPriDec1, MarketOrderBook::kDefaultDepth}; TradedAmounts tradedAmounts1(from, vol2); TradedAmounts tradedAmounts2(MonetaryAmount(from, interCur), tradedTo2); @@ -512,38 +512,38 @@ class ExchangeOrchestratorTradeTest : public ExchangeOrchestratorTest { PlaceOrderInfo placeOrderInfo2(orderInfo2, orderId2); if (makeMarketAvailable) { - markets.insert(m1); - markets.insert(m2); + markets.insert(market1); + markets.insert(market2); - marketOrderBookMap.insert_or_assign(m1, marketOrderbook1); - marketOrderBookMap.insert_or_assign(m2, marketOrderbook2); + marketOrderBookMap.insert_or_assign(market1, marketOrderbook1); + marketOrderBookMap.insert_or_assign(market2, marketOrderbook2); } // EXPECT_CALL does not allow references. Or I did not found the way to make it work, so we use ugly macros here switch (exchangePrivateNum) { case 1: - EXPECT_TWO_STEP_TRADE(exchangePublic1, exchangePrivate1) + EXPECT_TWO_STEP_TRADE(exchangePublic1, exchangePrivate1, market1, market2) break; case 2: - EXPECT_TWO_STEP_TRADE(exchangePublic2, exchangePrivate2) + EXPECT_TWO_STEP_TRADE(exchangePublic2, exchangePrivate2, market1, market2) break; case 3: - EXPECT_TWO_STEP_TRADE(exchangePublic3, exchangePrivate3) + EXPECT_TWO_STEP_TRADE(exchangePublic3, exchangePrivate3, market1, market2) break; case 4: - EXPECT_TWO_STEP_TRADE(exchangePublic3, exchangePrivate4) + EXPECT_TWO_STEP_TRADE(exchangePublic3, exchangePrivate4, market1, market2) break; case 5: - EXPECT_TWO_STEP_TRADE(exchangePublic3, exchangePrivate5) + EXPECT_TWO_STEP_TRADE(exchangePublic3, exchangePrivate5, market1, market2) break; case 6: - EXPECT_TWO_STEP_TRADE(exchangePublic3, exchangePrivate6) + EXPECT_TWO_STEP_TRADE(exchangePublic3, exchangePrivate6, market1, market2) break; case 7: - EXPECT_TWO_STEP_TRADE(exchangePublic3, exchangePrivate7) + EXPECT_TWO_STEP_TRADE(exchangePublic3, exchangePrivate7, market1, market2) break; case 8: - EXPECT_TWO_STEP_TRADE(exchangePublic1, exchangePrivate8) + EXPECT_TWO_STEP_TRADE(exchangePublic1, exchangePrivate8, market1, market2) break; default: throw exception("Unexpected exchange number "); @@ -743,9 +743,9 @@ TEST_F(ExchangeOrchestratorTradeTest, SingleExchangeBuyAll) { expectSingleTrade(3, MonetaryAmount(1500, fromCurrency), toCurrency, side, TradableMarkets::kExpectCall, OrderBook::kExpectCall, AllOrderBooks::kExpectNoCall, true); - constexpr bool isPercentageTrade = true; + constexpr bool kIsPercentageTrade = true; TradedAmountsPerExchange tradedAmountsPerExchange{std::make_pair(&exchange3, tradedAmounts3)}; - EXPECT_EQ(exchangesOrchestrator.trade(MonetaryAmount(100, fromCurrency), isPercentageTrade, toCurrency, + EXPECT_EQ(exchangesOrchestrator.trade(MonetaryAmount(100, fromCurrency), kIsPercentageTrade, toCurrency, privateExchangeNames, tradeOptions), tradedAmountsPerExchange); } @@ -770,10 +770,10 @@ TEST_F(ExchangeOrchestratorTradeTest, TwoExchangesSellAll) { expectSingleTrade(3, balancePortfolio3.get(fromCurrency), toCurrency, side, TradableMarkets::kExpectCall, OrderBook::kExpectCall, AllOrderBooks::kExpectNoCall, true); - constexpr bool isPercentageTrade = true; + constexpr bool kIsPercentageTrade = true; TradedAmountsPerExchange tradedAmountsPerExchange{std::make_pair(&exchange1, tradedAmounts1), std::make_pair(&exchange3, tradedAmounts3)}; - EXPECT_EQ(exchangesOrchestrator.trade(MonetaryAmount(100, fromCurrency), isPercentageTrade, toCurrency, + EXPECT_EQ(exchangesOrchestrator.trade(MonetaryAmount(100, fromCurrency), kIsPercentageTrade, toCurrency, privateExchangeNames, tradeOptions), tradedAmountsPerExchange); } @@ -805,11 +805,11 @@ TEST_F(ExchangeOrchestratorTradeTest, AllExchangesBuyAllOneMarketUnavailable) { expectSingleTrade(4, balancePortfolio4.get(fromCurrency), toCurrency, side, TradableMarkets::kNoExpectation, OrderBook::kNoExpectation, AllOrderBooks::kNoExpectation, true); - constexpr bool isPercentageTrade = true; + constexpr bool kIsPercentageTrade = true; TradedAmountsPerExchange tradedAmountsPerExchange{std::make_pair(&exchange2, tradedAmounts2), std::make_pair(&exchange3, tradedAmounts3), std::make_pair(&exchange4, tradedAmounts4)}; - EXPECT_EQ(exchangesOrchestrator.trade(MonetaryAmount(100, fromCurrency), isPercentageTrade, toCurrency, + EXPECT_EQ(exchangesOrchestrator.trade(MonetaryAmount(100, fromCurrency), kIsPercentageTrade, toCurrency, privateExchangeNames, tradeOptions), tradedAmountsPerExchange); } diff --git a/src/engine/test/queryresultprinter_test.cpp b/src/engine/test/queryresultprinter_test.cpp new file mode 100644 index 00000000..413e6e58 --- /dev/null +++ b/src/engine/test/queryresultprinter_test.cpp @@ -0,0 +1,1992 @@ +#include "queryresultprinter.hpp" + +#include + +#include +#include + +#include "cct_config.hpp" +#include "exchangedata_test.hpp" + +namespace cct { + +class QueryResultPrinterTest : public ExchangesBaseTest { + protected: + TimePoint tp1{std::chrono::milliseconds{std::numeric_limits::max() / 10000000}}; + TimePoint tp2{std::chrono::milliseconds{std::numeric_limits::max() / 9000000}}; + TimePoint tp3{std::chrono::milliseconds{std::numeric_limits::max() / 8000000}}; + TimePoint tp4{std::chrono::milliseconds{std::numeric_limits::max() / 7000000}}; + + void SetUp() override { ss.clear(); } + +#ifdef CCT_STRINGSTREAM_HAS_VIEW + void expectNoStr() const { EXPECT_TRUE(ss.view().empty()); } +#else + void expectNoStr() const { EXPECT_TRUE(ss.str().empty()); } +#endif + + void expectStr(std::string_view expected) const { + ASSERT_FALSE(expected.empty()); + expected.remove_prefix(1); // skip first newline char of expected string +#ifdef CCT_STRINGSTREAM_HAS_VIEW + EXPECT_EQ(ss.view(), expected); +#else + EXPECT_EQ(ss.str(), expected); +#endif + } + + void expectJson(std::string_view expected) const { + ASSERT_FALSE(expected.empty()); + expected.remove_prefix(1); // skip first newline char of expected string +#ifdef CCT_STRINGSTREAM_HAS_VIEW + EXPECT_EQ(json::parse(ss.view()), json::parse(expected)); +#else + EXPECT_EQ(json::parse(ss.str()), json::parse(expected)); +#endif + } + + std::stringstream ss; +}; + +class QueryResultPrinterMarketsTest : public QueryResultPrinterTest { + protected: + CurrencyCode cur1{"XRP"}; + CurrencyCode cur2; + MarketsPerExchange marketsPerExchange{{&exchange1, MarketSet{Market{cur1, "KRW"}, Market{cur1, "BTC"}}}, + {&exchange3, MarketSet{Market{cur1, "EUR"}}}}; +}; + +TEST_F(QueryResultPrinterMarketsTest, FormattedTable) { + QueryResultPrinter(ss, ApiOutputType::kFormattedTable).printMarkets(cur1, cur2, marketsPerExchange); + static constexpr std::string_view kExpected = R"( +------------------------------- +| Exchange | Markets with XRP | +------------------------------- +| binance | XRP-BTC | +| binance | XRP-KRW | +| huobi | XRP-EUR | +------------------------------- +)"; + + expectStr(kExpected); +} + +TEST_F(QueryResultPrinterMarketsTest, EmptyJson) { + QueryResultPrinter(ss, ApiOutputType::kJson).printMarkets(cur1, cur2, MarketsPerExchange{}); + static constexpr std::string_view kExpected = R"( +{ + "in": { + "opt": { + "cur1": "XRP" + }, + "req": "Markets" + }, + "out": {} +} +)"; + expectJson(kExpected); +} + +TEST_F(QueryResultPrinterMarketsTest, Json) { + QueryResultPrinter(ss, ApiOutputType::kJson).printMarkets(cur1, cur2, marketsPerExchange); + static constexpr std::string_view kExpected = R"( +{ + "in": { + "opt": { + "cur1": "XRP" + }, + "req": "Markets" + }, + "out": { + "binance": [ + "XRP-BTC", + "XRP-KRW" + ], + "huobi": [ + "XRP-EUR" + ] + } +} +)"; + expectJson(kExpected); +} + +TEST_F(QueryResultPrinterMarketsTest, NoPrint) { + QueryResultPrinter(ss, ApiOutputType::kNoPrint).printMarkets(cur1, cur2, marketsPerExchange); + expectNoStr(); +} + +class QueryResultPrinterTickerTest : public QueryResultPrinterTest { + protected: + ExchangeTickerMaps exchangeTickerMaps{ + {&exchange2, MarketOrderBookMap{{Market{"ETH", "EUR"}, this->marketOrderBook11}}}, + {&exchange4, MarketOrderBookMap{{Market{"BTC", "EUR"}, this->marketOrderBook21}, + {Market{"XRP", "BTC"}, this->marketOrderBook3}}}}; +}; + +TEST_F(QueryResultPrinterTickerTest, FormattedTable) { + QueryResultPrinter(ss, ApiOutputType::kFormattedTable).printTickerInformation(exchangeTickerMaps); + static constexpr std::string_view kExpected = R"( +------------------------------------------------------------------------------ +| Exchange | Market | Bid price | Bid volume | Ask price | Ask volume | +------------------------------------------------------------------------------ +| bithumb | ETH-EUR | 2301.05 EUR | 17 ETH | 2301.15 EUR | 0.4 ETH | +| huobi | BTC-EUR | 31051.01 EUR | 1.9087 BTC | 31051.02 EUR | 0.409 BTC | +| huobi | XRP-BTC | 0.36 BTC | 3494 XRP | 0.37 BTC | 916.4 XRP | +------------------------------------------------------------------------------ +)"; + expectStr(kExpected); +} + +TEST_F(QueryResultPrinterTickerTest, EmptyJson) { + QueryResultPrinter(ss, ApiOutputType::kJson).printTickerInformation(ExchangeTickerMaps{}); + static constexpr std::string_view kExpected = R"( +{ + "in": { + "req": "Ticker" + }, + "out": {} +} +)"; + expectJson(kExpected); +} + +TEST_F(QueryResultPrinterTickerTest, Json) { + QueryResultPrinter(ss, ApiOutputType::kJson).printTickerInformation(exchangeTickerMaps); + static constexpr std::string_view kExpected = R"( +{ + "in": { + "req": "Ticker" + }, + "out": { + "bithumb": [ + { + "ask": { + "a": "0.4", + "p": "2301.15" + }, + "bid": { + "a": "17", + "p": "2301.05" + }, + "pair": "ETH-EUR" + } + ], + "huobi": [ + { + "ask": { + "a": "0.409", + "p": "31051.02" + }, + "bid": { + "a": "1.9087", + "p": "31051.01" + }, + "pair": "BTC-EUR" + }, + { + "ask": { + "a": "916.4", + "p": "0.37" + }, + "bid": { + "a": "3494", + "p": "0.36" + }, + "pair": "XRP-BTC" + } + ] + } +} +)"; + expectJson(kExpected); +} + +TEST_F(QueryResultPrinterTickerTest, NoPrint) { + QueryResultPrinter(ss, ApiOutputType::kNoPrint).printTickerInformation(exchangeTickerMaps); + expectNoStr(); +} + +class QueryResultPrinterMarketOrderBookTest : public QueryResultPrinterTest { + protected: + Market m{"BTC", "EUR"}; + int d = 3; + MarketOrderBook mob{askPrice2, MonetaryAmount("0.12BTC"), bidPrice2, MonetaryAmount("0.00234 BTC"), volAndPriDec2, d}; + MarketOrderBookConversionRates marketOrderBookConversionRates{{"exchangeA", mob, {}}, {"exchangeD", mob, {}}}; +}; + +TEST_F(QueryResultPrinterMarketOrderBookTest, FormattedTable) { + QueryResultPrinter(ss, ApiOutputType::kFormattedTable) + .printMarketOrderBooks(m, CurrencyCode{}, d, marketOrderBookConversionRates); + static constexpr std::string_view kExpected = R"( +----------------------------------------------------------------------------- +| Sellers of BTC (asks) | exchangeA BTC price in EUR | Buyers of BTC (bids) | +----------------------------------------------------------------------------- +| 0.18116 | 31056.7 | | +| 0.15058 | 31056.68 | | +| 0.12 | 31056.67 | | +| | 31056.66 | 0.00234 | +| | 31056.65 | 0.03292 | +| | 31056.63 | 0.0635 | +----------------------------------------------------------------------------- +----------------------------------------------------------------------------- +| Sellers of BTC (asks) | exchangeD BTC price in EUR | Buyers of BTC (bids) | +----------------------------------------------------------------------------- +| 0.18116 | 31056.7 | | +| 0.15058 | 31056.68 | | +| 0.12 | 31056.67 | | +| | 31056.66 | 0.00234 | +| | 31056.65 | 0.03292 | +| | 31056.63 | 0.0635 | +----------------------------------------------------------------------------- +)"; + expectStr(kExpected); +} + +TEST_F(QueryResultPrinterMarketOrderBookTest, EmptyJson) { + QueryResultPrinter(ss, ApiOutputType::kJson) + .printMarketOrderBooks(m, CurrencyCode{}, d, MarketOrderBookConversionRates{}); + static constexpr std::string_view kExpected = R"( +{ + "in": { + "opt": { + "depth": 3, + "pair": "BTC-EUR" + }, + "req": "Orderbook" + }, + "out": {} +} +)"; + expectJson(kExpected); +} + +TEST_F(QueryResultPrinterMarketOrderBookTest, Json) { + QueryResultPrinter(ss, ApiOutputType::kJson) + .printMarketOrderBooks(m, CurrencyCode{}, d, marketOrderBookConversionRates); + static constexpr std::string_view kExpected = R"( +{ + "in": { + "opt": { + "depth": 3, + "pair": "BTC-EUR" + }, + "req": "Orderbook" + }, + "out": { + "exchangeA": { + "ask": [ + { + "a": "0.12", + "p": "31056.67" + }, + { + "a": "0.15058", + "p": "31056.68" + }, + { + "a": "0.18116", + "p": "31056.7" + } + ], + "bid": [ + { + "a": "0.00234", + "p": "31056.66" + }, + { + "a": "0.03292", + "p": "31056.65" + }, + { + "a": "0.0635", + "p": "31056.63" + } + ] + }, + "exchangeD": { + "ask": [ + { + "a": "0.12", + "p": "31056.67" + }, + { + "a": "0.15058", + "p": "31056.68" + }, + { + "a": "0.18116", + "p": "31056.7" + } + ], + "bid": [ + { + "a": "0.00234", + "p": "31056.66" + }, + { + "a": "0.03292", + "p": "31056.65" + }, + { + "a": "0.0635", + "p": "31056.63" + } + ] + } + } +} +)"; + expectJson(kExpected); +} + +TEST_F(QueryResultPrinterMarketOrderBookTest, NoPrint) { + QueryResultPrinter(ss, ApiOutputType::kNoPrint) + .printMarketOrderBooks(m, CurrencyCode{}, d, marketOrderBookConversionRates); + expectNoStr(); +} + +class QueryResultPrinterEmptyBalanceNoEquiCurTest : public QueryResultPrinterTest { + protected: + CurrencyCode equiCur; + BalancePortfolio emptyBal; + BalancePerExchange balancePerExchange{{&exchange1, emptyBal}, {&exchange4, emptyBal}}; +}; + +TEST_F(QueryResultPrinterEmptyBalanceNoEquiCurTest, FormattedTable) { + QueryResultPrinter(ss, ApiOutputType::kFormattedTable).printBalance(balancePerExchange, equiCur); + static constexpr std::string_view kExpected = R"( +----------------------------------------------------------------------------- +| Currency | Total amount on selected | binance_testuser1 | huobi_testuser2 | +----------------------------------------------------------------------------- +)"; + expectStr(kExpected); +} + +TEST_F(QueryResultPrinterEmptyBalanceNoEquiCurTest, EmptyJson) { + QueryResultPrinter(ss, ApiOutputType::kJson).printBalance(BalancePerExchange{}, equiCur); + static constexpr std::string_view kExpected = R"( +{ + "in": { + "opt": {}, + "req": "Balance" + }, + "out": { + "exchange": {}, + "total": { + "cur": {} + } + } +} +)"; + expectJson(kExpected); +} + +TEST_F(QueryResultPrinterEmptyBalanceNoEquiCurTest, Json) { + QueryResultPrinter(ss, ApiOutputType::kJson).printBalance(balancePerExchange, equiCur); + static constexpr std::string_view kExpected = R"( +{ + "in": { + "opt": {}, + "req": "Balance" + }, + "out": { + "exchange": { + "binance": { + "testuser1": {} + }, + "huobi": { + "testuser2": {} + } + }, + "total": { + "cur": {} + } + } +} +)"; + expectJson(kExpected); +} + +TEST_F(QueryResultPrinterEmptyBalanceNoEquiCurTest, NoPrint) { + QueryResultPrinter(ss, ApiOutputType::kNoPrint).printBalance(balancePerExchange, equiCur); + expectNoStr(); +} + +class QueryResultPrinterBalanceNoEquiCurTest : public QueryResultPrinterTest { + protected: + CurrencyCode equiCur; + BalancePortfolio bp3; + BalancePerExchange balancePerExchange{ + {&exchange1, balancePortfolio1}, {&exchange4, balancePortfolio4}, {&exchange2, bp3}}; +}; + +TEST_F(QueryResultPrinterBalanceNoEquiCurTest, FormattedTable) { + QueryResultPrinter(ss, ApiOutputType::kFormattedTable).printBalance(balancePerExchange, equiCur); + static constexpr std::string_view kExpected = R"( +------------------------------------------------------------------------------------------------- +| Currency | Total amount on selected | binance_testuser1 | huobi_testuser2 | bithumb_testuser1 | +------------------------------------------------------------------------------------------------- +| ADA | 147 | 0 | 147 | 0 | +| BTC | 15 | 15 | 0 | 0 | +| DOT | 4.76 | 0 | 4.76 | 0 | +| ETH | 1.5 | 1.5 | 0 | 0 | +| EUR | 1200 | 0 | 1200 | 0 | +| MATIC | 15004 | 0 | 15004 | 0 | +| USD | 155 | 0 | 155 | 0 | +| USDT | 5107.5 | 5000 | 107.5 | 0 | +| XRP | 1500 | 1500 | 0 | 0 | +------------------------------------------------------------------------------------------------- +)"; + expectStr(kExpected); +} + +TEST_F(QueryResultPrinterBalanceNoEquiCurTest, EmptyJson) { + QueryResultPrinter(ss, ApiOutputType::kJson).printBalance(BalancePerExchange{}, equiCur); + static constexpr std::string_view kExpected = R"( +{ + "in": { + "opt": {}, + "req": "Balance" + }, + "out": { + "exchange": {}, + "total": { + "cur": {} + } + } +} +)"; + expectJson(kExpected); +} + +TEST_F(QueryResultPrinterBalanceNoEquiCurTest, Json) { + QueryResultPrinter(ss, ApiOutputType::kJson).printBalance(balancePerExchange, equiCur); + static constexpr std::string_view kExpected = R"( +{ + "in": { + "opt": {}, + "req": "Balance" + }, + "out": { + "exchange": { + "binance": { + "testuser1": { + "BTC": { + "a": "15" + }, + "ETH": { + "a": "1.5" + }, + "USDT": { + "a": "5000" + }, + "XRP": { + "a": "1500" + } + } + }, + "bithumb": { + "testuser1": {} + }, + "huobi": { + "testuser2": { + "ADA": { + "a": "147" + }, + "DOT": { + "a": "4.76" + }, + "EUR": { + "a": "1200" + }, + "MATIC": { + "a": "15004" + }, + "USD": { + "a": "155" + }, + "USDT": { + "a": "107.5" + } + } + } + }, + "total": { + "cur": { + "ADA": { + "a": "147" + }, + "BTC": { + "a": "15" + }, + "DOT": { + "a": "4.76" + }, + "ETH": { + "a": "1.5" + }, + "EUR": { + "a": "1200" + }, + "MATIC": { + "a": "15004" + }, + "USD": { + "a": "155" + }, + "USDT": { + "a": "5107.5" + }, + "XRP": { + "a": "1500" + } + } + } + } +} +)"; + expectJson(kExpected); +} + +TEST_F(QueryResultPrinterBalanceNoEquiCurTest, NoPrint) { + QueryResultPrinter(ss, ApiOutputType::kNoPrint).printBalance(balancePerExchange, equiCur); + expectNoStr(); +} + +class QueryResultPrinterBalanceEquiCurTest : public QueryResultPrinterTest { + protected: + CurrencyCode equiCur{"EUR"}; + BalancePortfolio bp1{{MonetaryAmount("15000ADA"), MonetaryAmount("10000EUR")}, + {MonetaryAmount("0.56BTC"), MonetaryAmount("9067.7EUR")}}; + BalancePortfolio bp2{{MonetaryAmount("34.7XRP"), MonetaryAmount("45.08EUR")}, + {MonetaryAmount("15ETH"), MonetaryAmount("25000EUR")}, + {MonetaryAmount("123XLM"), MonetaryAmount("67.5EUR")}}; + BalancePortfolio bp3; + BalancePerExchange balancePerExchange{{&exchange1, bp1}, {&exchange4, bp2}, {&exchange2, bp3}, {&exchange3, bp3}}; +}; + +TEST_F(QueryResultPrinterBalanceEquiCurTest, FormattedTable) { + QueryResultPrinter(ss, ApiOutputType::kFormattedTable).printBalance(balancePerExchange, equiCur); + static constexpr std::string_view kExpected = R"( +---------------------------------------------------------------------------------------------------------------------------------- +| Currency | Total amount on selected | Total EUR eq | binance_testuser1 | huobi_testuser2 | bithumb_testuser1 | huobi_testuser1 | +---------------------------------------------------------------------------------------------------------------------------------- +| ETH | 15 | 25000 | 0 | 15 | 0 | 0 | +| ADA | 15000 | 10000 | 15000 | 0 | 0 | 0 | +| BTC | 0.56 | 9067.7 | 0.56 | 0 | 0 | 0 | +| XLM | 123 | 67.5 | 0 | 123 | 0 | 0 | +| XRP | 34.7 | 45.08 | 0 | 34.7 | 0 | 0 | +---------------------------------------------------------------------------------------------------------------------------------- +| Total | | 44180.28 | | | | | +---------------------------------------------------------------------------------------------------------------------------------- +)"; + expectStr(kExpected); +} + +TEST_F(QueryResultPrinterBalanceEquiCurTest, EmptyJson) { + QueryResultPrinter(ss, ApiOutputType::kJson).printBalance(BalancePerExchange{}, equiCur); + static constexpr std::string_view kExpected = R"( +{ + "in": { + "opt": { + "equiCurrency": "EUR" + }, + "req": "Balance" + }, + "out": { + "exchange": {}, + "total": { + "cur": {}, + "eq": "0" + } + } +} +)"; + expectJson(kExpected); +} + +TEST_F(QueryResultPrinterBalanceEquiCurTest, Json) { + QueryResultPrinter(ss, ApiOutputType::kJson).printBalance(balancePerExchange, equiCur); + static constexpr std::string_view kExpected = R"( +{ + "in": { + "opt": { + "equiCurrency": "EUR" + }, + "req": "Balance" + }, + "out": { + "exchange": { + "binance": { + "testuser1": { + "ADA": { + "a": "15000", + "eq": "10000" + }, + "BTC": { + "a": "0.56", + "eq": "9067.7" + } + } + }, + "bithumb": { + "testuser1": {} + }, + "huobi": { + "testuser1": {}, + "testuser2": { + "ETH": { + "a": "15", + "eq": "25000" + }, + "XLM": { + "a": "123", + "eq": "67.5" + }, + "XRP": { + "a": "34.7", + "eq": "45.08" + } + } + } + }, + "total": { + "cur": { + "ADA": { + "a": "15000", + "eq": "10000" + }, + "BTC": { + "a": "0.56", + "eq": "9067.7" + }, + "ETH": { + "a": "15", + "eq": "25000" + }, + "XLM": { + "a": "123", + "eq": "67.5" + }, + "XRP": { + "a": "34.7", + "eq": "45.08" + } + }, + "eq": "44180.28" + } + } +} +)"; + expectJson(kExpected); +} + +TEST_F(QueryResultPrinterBalanceEquiCurTest, NoPrint) { + QueryResultPrinter(ss, ApiOutputType::kNoPrint).printBalance(balancePerExchange, equiCur); + expectNoStr(); +} + +class QueryResultPrinterDepositInfoWithoutTagTest : public QueryResultPrinterTest { + protected: + CurrencyCode depositCurrencyCode{"ETH"}; + WalletPerExchange walletPerExchange{{&exchange2, Wallet{exchange2.apiPrivate().exchangeName(), depositCurrencyCode, + "ethaddress666", "", WalletCheck{}}}, + {&exchange4, Wallet{exchange4.apiPrivate().exchangeName(), depositCurrencyCode, + "ethaddress667", "", WalletCheck{}}}}; +}; + +TEST_F(QueryResultPrinterDepositInfoWithoutTagTest, FormattedTable) { + QueryResultPrinter(ss, ApiOutputType::kFormattedTable).printDepositInfo(depositCurrencyCode, walletPerExchange); + static constexpr std::string_view kExpected = R"( +---------------------------------------------------------- +| Exchange | Account | ETH address | Destination Tag | +---------------------------------------------------------- +| bithumb | testuser1 | ethaddress666 | | +| huobi | testuser2 | ethaddress667 | | +---------------------------------------------------------- +)"; + + expectStr(kExpected); +} + +TEST_F(QueryResultPrinterDepositInfoWithoutTagTest, EmptyJson) { + QueryResultPrinter(ss, ApiOutputType::kJson).printDepositInfo(depositCurrencyCode, WalletPerExchange{}); + static constexpr std::string_view kExpected = R"( +{ + "in": { + "opt": { + "cur": "ETH" + }, + "req": "DepositInfo" + }, + "out": {} +} +)"; + expectJson(kExpected); +} + +TEST_F(QueryResultPrinterDepositInfoWithoutTagTest, Json) { + QueryResultPrinter(ss, ApiOutputType::kJson).printDepositInfo(depositCurrencyCode, walletPerExchange); + static constexpr std::string_view kExpected = R"( +{ + "in": { + "opt": { + "cur": "ETH" + }, + "req": "DepositInfo" + }, + "out": { + "bithumb": { + "testuser1": { + "address": "ethaddress666" + } + }, + "huobi": { + "testuser2": { + "address": "ethaddress667" + } + } + } +} +)"; + expectJson(kExpected); +} + +TEST_F(QueryResultPrinterDepositInfoWithoutTagTest, NoPrint) { + QueryResultPrinter(ss, ApiOutputType::kNoPrint).printDepositInfo(depositCurrencyCode, walletPerExchange); + expectNoStr(); +} + +class QueryResultPrinterDepositInfoWithTagTest : public QueryResultPrinterTest { + protected: + CurrencyCode depositCurrencyCode{"XRP"}; + WalletPerExchange walletPerExchange{{&exchange3, Wallet{exchange3.apiPrivate().exchangeName(), depositCurrencyCode, + "xrpaddress666", "xrptag1", WalletCheck{}}}, + {&exchange4, Wallet{exchange4.apiPrivate().exchangeName(), depositCurrencyCode, + "xrpaddress666", "xrptag2", WalletCheck{}}}}; +}; + +TEST_F(QueryResultPrinterDepositInfoWithTagTest, FormattedTable) { + QueryResultPrinter(ss, ApiOutputType::kFormattedTable).printDepositInfo(depositCurrencyCode, walletPerExchange); + static constexpr std::string_view kExpected = R"( +---------------------------------------------------------- +| Exchange | Account | XRP address | Destination Tag | +---------------------------------------------------------- +| huobi | testuser1 | xrpaddress666 | xrptag1 | +| huobi | testuser2 | xrpaddress666 | xrptag2 | +---------------------------------------------------------- +)"; + expectStr(kExpected); +} + +TEST_F(QueryResultPrinterDepositInfoWithTagTest, EmptyJson) { + QueryResultPrinter(ss, ApiOutputType::kJson).printDepositInfo(depositCurrencyCode, WalletPerExchange{}); + static constexpr std::string_view kExpected = R"( +{ + "in": { + "opt": { + "cur": "XRP" + }, + "req": "DepositInfo" + }, + "out": {} +} +)"; + expectJson(kExpected); +} + +TEST_F(QueryResultPrinterDepositInfoWithTagTest, Json) { + QueryResultPrinter(ss, ApiOutputType::kJson).printDepositInfo(depositCurrencyCode, walletPerExchange); + static constexpr std::string_view kExpected = R"( +{ + "in": { + "opt": { + "cur": "XRP" + }, + "req": "DepositInfo" + }, + "out": { + "huobi": { + "testuser1": { + "address": "xrpaddress666", + "tag": "xrptag1" + }, + "testuser2": { + "address": "xrpaddress666", + "tag": "xrptag2" + } + } + } +} +)"; + expectJson(kExpected); +} + +TEST_F(QueryResultPrinterDepositInfoWithTagTest, NoPrint) { + QueryResultPrinter(ss, ApiOutputType::kNoPrint).printDepositInfo(depositCurrencyCode, walletPerExchange); + expectNoStr(); +} + +class QueryResultPrinterTradesAmountTest : public QueryResultPrinterTest { + protected: + MonetaryAmount startAmount{"0.5BTC"}; + bool isPercentageTrade{false}; + CurrencyCode toCurrency{"XRP"}; + TradeOptions tradeOptions; + TradedAmountsPerExchange tradedAmountsPerExchange{ + {&exchange1, TradedAmounts{MonetaryAmount("0.1BTC"), MonetaryAmount("1050XRP")}}, + {&exchange3, TradedAmounts{MonetaryAmount("0.3BTC"), MonetaryAmount("3500.6XRP")}}, + {&exchange4, TradedAmounts{MonetaryAmount(0, "BTC"), MonetaryAmount(0, "XRP")}}}; +}; + +TEST_F(QueryResultPrinterTradesAmountTest, FormattedTable) { + QueryResultPrinter(ss, ApiOutputType::kFormattedTable) + .printTrades(tradedAmountsPerExchange, startAmount, isPercentageTrade, toCurrency, tradeOptions); + static constexpr std::string_view kExpected = R"( +------------------------------------------------------------------------------ +| Exchange | Account | Traded from amount (real) | Traded to amount (real) | +------------------------------------------------------------------------------ +| binance | testuser1 | 0.1 BTC | 1050 XRP | +| huobi | testuser1 | 0.3 BTC | 3500.6 XRP | +| huobi | testuser2 | 0 BTC | 0 XRP | +------------------------------------------------------------------------------ +)"; + + expectStr(kExpected); +} + +TEST_F(QueryResultPrinterTradesAmountTest, EmptyJson) { + QueryResultPrinter(ss, ApiOutputType::kJson) + .printTrades(TradedAmountsPerExchange{}, startAmount, isPercentageTrade, toCurrency, tradeOptions); + static constexpr std::string_view kExpected = R"( +{ + "in": { + "opt": { + "from": { + "amount": "0.5", + "currency": "BTC", + "isPercentage": false + }, + "options": { + "maxTradeTime": "30s", + "minTimeBetweenPriceUpdates": "5s", + "mode": "real", + "price": { + "strategy": "maker" + }, + "timeoutAction": "cancel" + }, + "to": { + "currency": "XRP" + } + }, + "req": "Trade" + }, + "out": {} +} +)"; + expectJson(kExpected); +} + +TEST_F(QueryResultPrinterTradesAmountTest, Json) { + QueryResultPrinter(ss, ApiOutputType::kJson) + .printTrades(tradedAmountsPerExchange, startAmount, isPercentageTrade, toCurrency, tradeOptions); + static constexpr std::string_view kExpected = R"( +{ + "in": { + "opt": { + "from": { + "amount": "0.5", + "currency": "BTC", + "isPercentage": false + }, + "options": { + "maxTradeTime": "30s", + "minTimeBetweenPriceUpdates": "5s", + "mode": "real", + "price": { + "strategy": "maker" + }, + "timeoutAction": "cancel" + }, + "to": { + "currency": "XRP" + } + }, + "req": "Trade" + }, + "out": { + "binance": { + "testuser1": { + "from": "0.1", + "to": "1050" + } + }, + "huobi": { + "testuser1": { + "from": "0.3", + "to": "3500.6" + }, + "testuser2": { + "from": "0", + "to": "0" + } + } + } +} +)"; + expectJson(kExpected); +} + +TEST_F(QueryResultPrinterTradesAmountTest, NoPrint) { + QueryResultPrinter(ss, ApiOutputType::kNoPrint) + .printTrades(tradedAmountsPerExchange, startAmount, isPercentageTrade, toCurrency, tradeOptions); + expectNoStr(); +} + +class QueryResultPrinterTradesPercentageTest : public QueryResultPrinterTest { + protected: + MonetaryAmount startAmount{"25.6EUR"}; + bool isPercentageTrade{true}; + CurrencyCode toCurrency{"SHIB"}; + TradeOptions tradeOptions{PriceOptions{PriceStrategy::kTaker}}; + TradedAmountsPerExchange tradedAmountsPerExchange{ + {&exchange2, TradedAmounts{MonetaryAmount("15000.56EUR"), MonetaryAmount("885475102SHIB")}}}; +}; + +TEST_F(QueryResultPrinterTradesPercentageTest, FormattedTable) { + QueryResultPrinter(ss, ApiOutputType::kFormattedTable) + .printTrades(tradedAmountsPerExchange, startAmount, isPercentageTrade, toCurrency, tradeOptions); + static constexpr std::string_view kExpected = R"( +------------------------------------------------------------------------------ +| Exchange | Account | Traded from amount (real) | Traded to amount (real) | +------------------------------------------------------------------------------ +| bithumb | testuser1 | 15000.56 EUR | 885475102 SHIB | +------------------------------------------------------------------------------ +)"; + expectStr(kExpected); +} + +TEST_F(QueryResultPrinterTradesPercentageTest, EmptyJson) { + QueryResultPrinter(ss, ApiOutputType::kJson) + .printTrades(TradedAmountsPerExchange{}, startAmount, isPercentageTrade, toCurrency, tradeOptions); + static constexpr std::string_view kExpected = R"( +{ + "in": { + "opt": { + "from": { + "amount": "25.6", + "currency": "EUR", + "isPercentage": true + }, + "options": { + "maxTradeTime": "30s", + "minTimeBetweenPriceUpdates": "5s", + "mode": "real", + "price": { + "strategy": "taker" + }, + "timeoutAction": "cancel" + }, + "to": { + "currency": "SHIB" + } + }, + "req": "Trade" + }, + "out": {} +} +)"; + expectJson(kExpected); +} + +TEST_F(QueryResultPrinterTradesPercentageTest, Json) { + QueryResultPrinter(ss, ApiOutputType::kJson) + .printTrades(tradedAmountsPerExchange, startAmount, isPercentageTrade, toCurrency, tradeOptions); + static constexpr std::string_view kExpected = R"( +{ + "in": { + "opt": { + "from": { + "amount": "25.6", + "currency": "EUR", + "isPercentage": true + }, + "options": { + "maxTradeTime": "30s", + "minTimeBetweenPriceUpdates": "5s", + "mode": "real", + "price": { + "strategy": "taker" + }, + "timeoutAction": "cancel" + }, + "to": { + "currency": "SHIB" + } + }, + "req": "Trade" + }, + "out": { + "bithumb": { + "testuser1": { + "from": "15000.56", + "to": "885475102" + } + } + } +} +)"; + expectJson(kExpected); +} + +TEST_F(QueryResultPrinterTradesPercentageTest, NoPrint) { + QueryResultPrinter(ss, ApiOutputType::kNoPrint) + .printTrades(tradedAmountsPerExchange, startAmount, isPercentageTrade, toCurrency, tradeOptions); + expectNoStr(); +} + +class QueryResultPrinterSmartBuyTest : public QueryResultPrinterTest { + protected: + MonetaryAmount endAmount{"3ETH"}; + TradeOptions tradeOptions; + TradedAmountsPerExchange tradedAmountsPerExchange{ + {&exchange1, TradedAmounts{MonetaryAmount("4500.67EUR"), MonetaryAmount("3ETH")}}}; +}; + +TEST_F(QueryResultPrinterSmartBuyTest, FormattedTable) { + QueryResultPrinter(ss, ApiOutputType::kFormattedTable) + .printBuyTrades(tradedAmountsPerExchange, endAmount, tradeOptions); + static constexpr std::string_view kExpected = R"( +------------------------------------------------------------------------------ +| Exchange | Account | Traded from amount (real) | Traded to amount (real) | +------------------------------------------------------------------------------ +| binance | testuser1 | 4500.67 EUR | 3 ETH | +------------------------------------------------------------------------------ +)"; + expectStr(kExpected); +} + +TEST_F(QueryResultPrinterSmartBuyTest, EmptyJson) { + QueryResultPrinter(ss, ApiOutputType::kJson).printBuyTrades(TradedAmountsPerExchange{}, endAmount, tradeOptions); + static constexpr std::string_view kExpected = R"( +{ + "in": { + "opt": { + "options": { + "maxTradeTime": "30s", + "minTimeBetweenPriceUpdates": "5s", + "mode": "real", + "price": { + "strategy": "maker" + }, + "timeoutAction": "cancel" + }, + "to": { + "amount": "3", + "currency": "ETH", + "isPercentage": false + } + }, + "req": "Buy" + }, + "out": {} +} +)"; + expectJson(kExpected); +} + +TEST_F(QueryResultPrinterSmartBuyTest, Json) { + QueryResultPrinter(ss, ApiOutputType::kJson).printBuyTrades(tradedAmountsPerExchange, endAmount, tradeOptions); + static constexpr std::string_view kExpected = R"( +{ + "in": { + "opt": { + "options": { + "maxTradeTime": "30s", + "minTimeBetweenPriceUpdates": "5s", + "mode": "real", + "price": { + "strategy": "maker" + }, + "timeoutAction": "cancel" + }, + "to": { + "amount": "3", + "currency": "ETH", + "isPercentage": false + } + }, + "req": "Buy" + }, + "out": { + "binance": { + "testuser1": { + "from": "4500.67", + "to": "3" + } + } + } +} +)"; + expectJson(kExpected); +} + +TEST_F(QueryResultPrinterSmartBuyTest, NoPrint) { + QueryResultPrinter(ss, ApiOutputType::kNoPrint).printBuyTrades(tradedAmountsPerExchange, endAmount, tradeOptions); + expectNoStr(); +} + +class QueryResultPrinterSmartSellTest : public QueryResultPrinterTest { + protected: + MonetaryAmount startAmount{"0.15BTC"}; + TradeOptions tradeOptions; + bool isPercentageTrade{false}; + TradedAmountsPerExchange tradedAmountsPerExchange{ + {&exchange1, TradedAmounts{MonetaryAmount("0.01BTC"), MonetaryAmount("1500USDT")}}, + {&exchange3, TradedAmounts{MonetaryAmount("0.004BTC"), MonetaryAmount("350EUR")}}, + {&exchange4, TradedAmounts{MonetaryAmount("0.1BTC"), MonetaryAmount("17ETH")}}}; +}; + +TEST_F(QueryResultPrinterSmartSellTest, FormattedTable) { + QueryResultPrinter(ss, ApiOutputType::kFormattedTable) + .printSellTrades(tradedAmountsPerExchange, startAmount, isPercentageTrade, tradeOptions); + static constexpr std::string_view kExpected = R"( +------------------------------------------------------------------------------ +| Exchange | Account | Traded from amount (real) | Traded to amount (real) | +------------------------------------------------------------------------------ +| binance | testuser1 | 0.01 BTC | 1500 USDT | +| huobi | testuser1 | 0.004 BTC | 350 EUR | +| huobi | testuser2 | 0.1 BTC | 17 ETH | +------------------------------------------------------------------------------ +)"; + + expectStr(kExpected); +} + +TEST_F(QueryResultPrinterSmartSellTest, EmptyJson) { + QueryResultPrinter(ss, ApiOutputType::kJson) + .printSellTrades(TradedAmountsPerExchange{}, startAmount, isPercentageTrade, tradeOptions); + static constexpr std::string_view kExpected = R"( +{ + "in": { + "opt": { + "from": { + "amount": "0.15", + "currency": "BTC", + "isPercentage": false + }, + "options": { + "maxTradeTime": "30s", + "minTimeBetweenPriceUpdates": "5s", + "mode": "real", + "price": { + "strategy": "maker" + }, + "timeoutAction": "cancel" + } + }, + "req": "Sell" + }, + "out": {} +} +)"; + expectJson(kExpected); +} + +TEST_F(QueryResultPrinterSmartSellTest, Json) { + QueryResultPrinter(ss, ApiOutputType::kJson) + .printSellTrades(tradedAmountsPerExchange, startAmount, isPercentageTrade, tradeOptions); + static constexpr std::string_view kExpected = R"( +{ + "in": { + "opt": { + "from": { + "amount": "0.15", + "currency": "BTC", + "isPercentage": false + }, + "options": { + "maxTradeTime": "30s", + "minTimeBetweenPriceUpdates": "5s", + "mode": "real", + "price": { + "strategy": "maker" + }, + "timeoutAction": "cancel" + } + }, + "req": "Sell" + }, + "out": { + "binance": { + "testuser1": { + "from": "0.01", + "to": "1500" + } + }, + "huobi": { + "testuser1": { + "from": "0.004", + "to": "350" + }, + "testuser2": { + "from": "0.1", + "to": "17" + } + } + } +} +)"; + expectJson(kExpected); +} + +TEST_F(QueryResultPrinterSmartSellTest, NoPrint) { + QueryResultPrinter(ss, ApiOutputType::kNoPrint) + .printSellTrades(tradedAmountsPerExchange, startAmount, isPercentageTrade, tradeOptions); + expectNoStr(); +} + +class QueryResultPrinterOpenedOrdersBaseTest : public QueryResultPrinterTest { + protected: + Order order1{"id1", MonetaryAmount(0, "BTC"), MonetaryAmount(1, "BTC"), MonetaryAmount(50000, "EUR"), + tp1, TradeSide::kBuy}; + Order order2{"id2", MonetaryAmount("0.56ETH"), MonetaryAmount("0.44ETH"), MonetaryAmount("1500.56USDT"), + tp2, TradeSide::kSell}; + Order order3{"id3", MonetaryAmount(13, "XRP"), MonetaryAmount("500.45XRP"), MonetaryAmount("1.31USDT"), tp3, + TradeSide::kBuy}; + Order order4{"id4", MonetaryAmount("34.56LTC"), MonetaryAmount("0.4LTC"), MonetaryAmount("1574564KRW"), tp4, + TradeSide::kSell}; + Order order5{"id5", + MonetaryAmount("11235435435SHIB"), + MonetaryAmount("11235435.59SHIB"), + MonetaryAmount("0.00000045USDT"), + tp2, + TradeSide::kSell}; +}; + +class QueryResultPrinterOpenedOrdersNoConstraintsTest : public QueryResultPrinterOpenedOrdersBaseTest { + protected: + OrdersConstraints ordersConstraints; + OpenedOrdersPerExchange openedOrdersPerExchange{{&exchange1, OrdersSet{}}, + {&exchange2, OrdersSet{order3, order5}}, + {&exchange4, OrdersSet{order2}}, + {&exchange3, OrdersSet{order4, order1}}}; +}; + +TEST_F(QueryResultPrinterOpenedOrdersNoConstraintsTest, FormattedTable) { + QueryResultPrinter(ss, ApiOutputType::kFormattedTable).printOpenedOrders(openedOrdersPerExchange, ordersConstraints); + static constexpr std::string_view kExpected = R"( +--------------------------------------------------------------------------------------------------------------------------- +| Exchange | Account | Exchange Id | Placed time | Side | Price | Matched Amount | Remaining Amount | +--------------------------------------------------------------------------------------------------------------------------- +| bithumb | testuser1 | id5 | 2002-06-23 07:58:35 | Sell | 0.00000045 USDT | 11235435435 SHIB | 11235435.59 SHIB | +| bithumb | testuser1 | id3 | 2006-07-14 23:58:24 | Buy | 1.31 USDT | 13 XRP | 500.45 XRP | +| huobi | testuser2 | id2 | 2002-06-23 07:58:35 | Sell | 1500.56 USDT | 0.56 ETH | 0.44 ETH | +| huobi | testuser1 | id1 | 1999-03-25 04:46:43 | Buy | 50000 EUR | 0 BTC | 1 BTC | +| huobi | testuser1 | id4 | 2011-10-03 06:49:36 | Sell | 1574564 KRW | 34.56 LTC | 0.4 LTC | +--------------------------------------------------------------------------------------------------------------------------- +)"; + expectStr(kExpected); +} + +TEST_F(QueryResultPrinterOpenedOrdersNoConstraintsTest, EmptyJson) { + QueryResultPrinter(ss, ApiOutputType::kJson).printOpenedOrders(OpenedOrdersPerExchange{}, ordersConstraints); + static constexpr std::string_view kExpected = R"( +{ + "in": { + "req": "OrdersOpened" + }, + "out": {} +} +)"; + expectJson(kExpected); +} + +TEST_F(QueryResultPrinterOpenedOrdersNoConstraintsTest, Json) { + QueryResultPrinter(ss, ApiOutputType::kJson).printOpenedOrders(openedOrdersPerExchange, ordersConstraints); + static constexpr std::string_view kExpected = R"( +{ + "in": { + "req": "OrdersOpened" + }, + "out": { + "binance": { + "testuser1": [] + }, + "bithumb": { + "testuser1": [ + { + "id": "id5", + "matched": "11235435435", + "pair": "SHIB-USDT", + "placedTime": "2002-06-23 07:58:35", + "price": "0.00000045", + "remaining": "11235435.59", + "side": "Sell" + }, + { + "id": "id3", + "matched": "13", + "pair": "XRP-USDT", + "placedTime": "2006-07-14 23:58:24", + "price": "1.31", + "remaining": "500.45", + "side": "Buy" + } + ] + }, + "huobi": { + "testuser1": [ + { + "id": "id1", + "matched": "0", + "pair": "BTC-EUR", + "placedTime": "1999-03-25 04:46:43", + "price": "50000", + "remaining": "1", + "side": "Buy" + }, + { + "id": "id4", + "matched": "34.56", + "pair": "LTC-KRW", + "placedTime": "2011-10-03 06:49:36", + "price": "1574564", + "remaining": "0.4", + "side": "Sell" + } + ], + "testuser2": [ + { + "id": "id2", + "matched": "0.56", + "pair": "ETH-USDT", + "placedTime": "2002-06-23 07:58:35", + "price": "1500.56", + "remaining": "0.44", + "side": "Sell" + } + ] + } + } +} +)"; + expectJson(kExpected); +} + +TEST_F(QueryResultPrinterOpenedOrdersNoConstraintsTest, NoPrint) { + QueryResultPrinter(ss, ApiOutputType::kNoPrint).printOpenedOrders(openedOrdersPerExchange, ordersConstraints); + expectNoStr(); +} + +class QueryResultPrinterCancelOrdersTest : public QueryResultPrinterTest { + protected: + OrdersConstraints ordersConstraints; + NbCancelledOrdersPerExchange nbCancelledOrdersPerExchange{ + {&exchange1, 2}, {&exchange2, 3}, {&exchange4, 1}, {&exchange3, 17}}; +}; + +TEST_F(QueryResultPrinterCancelOrdersTest, FormattedTable) { + QueryResultPrinter(ss, ApiOutputType::kFormattedTable) + .printCancelledOrders(nbCancelledOrdersPerExchange, ordersConstraints); + static constexpr std::string_view kExpected = R"( +----------------------------------------------------- +| Exchange | Account | Number of cancelled orders | +----------------------------------------------------- +| binance | testuser1 | 2 | +| bithumb | testuser1 | 3 | +| huobi | testuser2 | 1 | +| huobi | testuser1 | 17 | +----------------------------------------------------- +)"; + expectStr(kExpected); +} + +TEST_F(QueryResultPrinterCancelOrdersTest, EmptyJson) { + QueryResultPrinter(ss, ApiOutputType::kJson).printCancelledOrders(NbCancelledOrdersPerExchange{}, ordersConstraints); + static constexpr std::string_view kExpected = R"( +{ + "in": { + "req": "OrdersCancel" + }, + "out": {} +} +)"; + expectJson(kExpected); +} + +TEST_F(QueryResultPrinterCancelOrdersTest, Json) { + QueryResultPrinter(ss, ApiOutputType::kJson).printCancelledOrders(nbCancelledOrdersPerExchange, ordersConstraints); + static constexpr std::string_view kExpected = R"( +{ + "in": { + "req": "OrdersCancel" + }, + "out": { + "binance": { + "testuser1": { + "nb": 2 + } + }, + "bithumb": { + "testuser1": { + "nb": 3 + } + }, + "huobi": { + "testuser1": { + "nb": 17 + }, + "testuser2": { + "nb": 1 + } + } + } +} +)"; + expectJson(kExpected); +} + +TEST_F(QueryResultPrinterCancelOrdersTest, NoPrint) { + QueryResultPrinter(ss, ApiOutputType::kNoPrint).printCancelledOrders(nbCancelledOrdersPerExchange, ordersConstraints); + expectNoStr(); +} + +class QueryResultPrinterConversionPathTest : public QueryResultPrinterTest { + protected: + Market marketForPath{"XLM", "XRP"}; + ConversionPathPerExchange conversionPathPerExchange{ + {&exchange1, MarketsPath{}}, + {&exchange2, MarketsPath{Market{"XLM", "XRP"}}}, + {&exchange4, MarketsPath{Market{"XLM", "AAA"}, Market{"BBB", "AAA"}, Market{"BBB", "XRP"}}}}; +}; + +TEST_F(QueryResultPrinterConversionPathTest, FormattedTable) { + QueryResultPrinter(ss, ApiOutputType::kFormattedTable).printConversionPath(marketForPath, conversionPathPerExchange); + static constexpr std::string_view kExpected = R"( +-------------------------------------------------- +| Exchange | Fastest conversion path for XLM-XRP | +-------------------------------------------------- +| bithumb | XLM-XRP | +| huobi | XLM-AAA,BBB-AAA,BBB-XRP | +-------------------------------------------------- +)"; + expectStr(kExpected); +} + +TEST_F(QueryResultPrinterConversionPathTest, EmptyJson) { + QueryResultPrinter(ss, ApiOutputType::kJson).printConversionPath(marketForPath, ConversionPathPerExchange{}); + static constexpr std::string_view kExpected = R"( +{ + "in": { + "opt": { + "market": "XLM-XRP" + }, + "req": "ConversionPath" + }, + "out": {} +} +)"; + expectJson(kExpected); +} + +TEST_F(QueryResultPrinterConversionPathTest, Json) { + QueryResultPrinter(ss, ApiOutputType::kJson).printConversionPath(marketForPath, conversionPathPerExchange); + static constexpr std::string_view kExpected = R"( +{ + "in": { + "opt": { + "market": "XLM-XRP" + }, + "req": "ConversionPath" + }, + "out": { + "bithumb": [ + "XLM-XRP" + ], + "huobi": [ + "XLM-AAA", + "BBB-AAA", + "BBB-XRP" + ] + } +} +)"; + expectJson(kExpected); +} + +TEST_F(QueryResultPrinterConversionPathTest, NoPrint) { + QueryResultPrinter(ss, ApiOutputType::kNoPrint).printConversionPath(marketForPath, conversionPathPerExchange); + expectNoStr(); +} + +class QueryResultPrinterWithdrawFeeTest : public QueryResultPrinterTest { + protected: + CurrencyCode curWithdrawFee{"ETH"}; + MonetaryAmountPerExchange withdrawFeePerExchange{{&exchange2, MonetaryAmount{"0.15", "ETH"}}, + {&exchange4, MonetaryAmount{"0.05", "ETH"}}}; +}; + +TEST_F(QueryResultPrinterWithdrawFeeTest, FormattedTable) { + QueryResultPrinter(ss, ApiOutputType::kFormattedTable).printWithdrawFees(withdrawFeePerExchange, curWithdrawFee); + static constexpr std::string_view kExpected = R"( +--------------------------- +| Exchange | Withdraw fee | +--------------------------- +| bithumb | 0.15 ETH | +| huobi | 0.05 ETH | +--------------------------- +)"; + expectStr(kExpected); +} + +TEST_F(QueryResultPrinterWithdrawFeeTest, EmptyJson) { + QueryResultPrinter(ss, ApiOutputType::kJson).printWithdrawFees(MonetaryAmountPerExchange{}, curWithdrawFee); + static constexpr std::string_view kExpected = R"( +{ + "in": { + "opt": { + "cur": "ETH" + }, + "req": "WithdrawFee" + }, + "out": {} +} +)"; + expectJson(kExpected); +} + +TEST_F(QueryResultPrinterWithdrawFeeTest, Json) { + QueryResultPrinter(ss, ApiOutputType::kJson).printWithdrawFees(withdrawFeePerExchange, curWithdrawFee); + static constexpr std::string_view kExpected = R"( +{ + "in": { + "opt": { + "cur": "ETH" + }, + "req": "WithdrawFee" + }, + "out": { + "bithumb": "0.15", + "huobi": "0.05" + } +} +)"; + expectJson(kExpected); +} + +TEST_F(QueryResultPrinterWithdrawFeeTest, NoPrint) { + QueryResultPrinter(ss, ApiOutputType::kNoPrint).printWithdrawFees(withdrawFeePerExchange, curWithdrawFee); + expectNoStr(); +} + +class QueryResultPrinterLast24HoursTradedVolumeTest : public QueryResultPrinterTest { + protected: + Market marketLast24hTradedVolume{"BTC", "EUR"}; + MonetaryAmountPerExchange monetaryAmountPerExchange{{&exchange1, MonetaryAmount{"37.8", "BTC"}}, + {&exchange3, MonetaryAmount{"14", "BTC"}}}; +}; + +TEST_F(QueryResultPrinterLast24HoursTradedVolumeTest, FormattedTable) { + QueryResultPrinter(ss, ApiOutputType::kFormattedTable) + .printLast24hTradedVolume(marketLast24hTradedVolume, monetaryAmountPerExchange); + static constexpr std::string_view kExpected = R"( +--------------------------------------------- +| Exchange | Last 24h BTC-EUR traded volume | +--------------------------------------------- +| binance | 37.8 BTC | +| huobi | 14 BTC | +--------------------------------------------- +)"; + expectStr(kExpected); +} + +TEST_F(QueryResultPrinterLast24HoursTradedVolumeTest, EmptyJson) { + QueryResultPrinter(ss, ApiOutputType::kJson) + .printLast24hTradedVolume(marketLast24hTradedVolume, MonetaryAmountPerExchange{}); + static constexpr std::string_view kExpected = R"( +{ + "in": { + "opt": { + "market": "BTC-EUR" + }, + "req": "Last24hTradedVolume" + }, + "out": {} +} +)"; + expectJson(kExpected); +} + +TEST_F(QueryResultPrinterLast24HoursTradedVolumeTest, Json) { + QueryResultPrinter(ss, ApiOutputType::kJson) + .printLast24hTradedVolume(marketLast24hTradedVolume, monetaryAmountPerExchange); + static constexpr std::string_view kExpected = R"( +{ + "in": { + "opt": { + "market": "BTC-EUR" + }, + "req": "Last24hTradedVolume" + }, + "out": { + "binance": "37.8", + "huobi": "14" + } +} +)"; + expectJson(kExpected); +} + +TEST_F(QueryResultPrinterLast24HoursTradedVolumeTest, NoPrint) { + QueryResultPrinter(ss, ApiOutputType::kNoPrint) + .printLast24hTradedVolume(marketLast24hTradedVolume, monetaryAmountPerExchange); + expectNoStr(); +} + +class QueryResultPrinterLastTradesVolumeTest : public QueryResultPrinterTest { + protected: + Market marketLastTrades{"ETH", "USDT"}; + int nbLastTrades = 3; + LastTradesPerExchange lastTradesPerExchange{ + {&exchange1, + LastTradesVector{ + PublicTrade(TradeSide::kBuy, MonetaryAmount{"0.13", "ETH"}, MonetaryAmount{"1500.5", "USDT"}, tp1), + PublicTrade(TradeSide::kSell, MonetaryAmount{"3.7", "ETH"}, MonetaryAmount{"1500.5", "USDT"}, tp2), + PublicTrade(TradeSide::kBuy, MonetaryAmount{"0.004", "ETH"}, MonetaryAmount{1501, "USDT"}, tp3)}}, + {&exchange3, + LastTradesVector{ + PublicTrade(TradeSide::kSell, MonetaryAmount{"0.13", "ETH"}, MonetaryAmount{"1500.5", "USDT"}, tp4), + PublicTrade(TradeSide::kBuy, MonetaryAmount{"0.004", "ETH"}, MonetaryAmount{1501, "USDT"}, tp2)}}, + {&exchange2, + LastTradesVector{ + PublicTrade(TradeSide::kSell, MonetaryAmount{"0.13", "ETH"}, MonetaryAmount{"1500.5", "USDT"}, tp4), + PublicTrade(TradeSide::kBuy, MonetaryAmount{"0.004", "ETH"}, MonetaryAmount{1501, "USDT"}, tp2), + PublicTrade(TradeSide::kBuy, MonetaryAmount{"47.78", "ETH"}, MonetaryAmount{1498, "USDT"}, tp1)}}}; +}; + +TEST_F(QueryResultPrinterLastTradesVolumeTest, FormattedTable) { + QueryResultPrinter(ss, ApiOutputType::kFormattedTable) + .printLastTrades(marketLastTrades, nbLastTrades, lastTradesPerExchange); + static constexpr std::string_view kExpected = R"( +-------------------------------------------------------------------------------------------- +| binance trades - UTC | ETH buys | Price in USDT | ETH sells | +-------------------------------------------------------------------------------------------- +| 1999-03-25 04:46:43 | 0.13 | 1500.5 | | +| 2002-06-23 07:58:35 | | 1500.5 | 3.7 | +| 2006-07-14 23:58:24 | 0.004 | 1501 | | +-------------------------------------------------------------------------------------------- +| Summary | 0.134 ETH (2 buys) | 1500.66666666666666 USDT | 3.7 ETH (1 sells) | +-------------------------------------------------------------------------------------------- +--------------------------------------------------------------------------------- +| huobi trades - UTC | ETH buys | Price in USDT | ETH sells | +--------------------------------------------------------------------------------- +| 2011-10-03 06:49:36 | | 1500.5 | 0.13 | +| 2002-06-23 07:58:35 | 0.004 | 1501 | | +--------------------------------------------------------------------------------- +| Summary | 0.004 ETH (1 buys) | 1500.75 USDT | 0.13 ETH (1 sells) | +--------------------------------------------------------------------------------- +---------------------------------------------------------------------------------------------- +| bithumb trades - UTC | ETH buys | Price in USDT | ETH sells | +---------------------------------------------------------------------------------------------- +| 2011-10-03 06:49:36 | | 1500.5 | 0.13 | +| 2002-06-23 07:58:35 | 0.004 | 1501 | | +| 1999-03-25 04:46:43 | 47.78 | 1498 | | +---------------------------------------------------------------------------------------------- +| Summary | 47.784 ETH (2 buys) | 1499.83333333333333 USDT | 0.13 ETH (1 sells) | +---------------------------------------------------------------------------------------------- +)"; + expectStr(kExpected); +} + +TEST_F(QueryResultPrinterLastTradesVolumeTest, EmptyJson) { + QueryResultPrinter(ss, ApiOutputType::kJson).printLastTrades(marketLastTrades, nbLastTrades, LastTradesPerExchange{}); + static constexpr std::string_view kExpected = R"( +{ + "in": { + "opt": { + "market": "ETH-USDT", + "nb": 3 + }, + "req": "LastTrades" + }, + "out": {} +} +)"; + expectJson(kExpected); +} + +TEST_F(QueryResultPrinterLastTradesVolumeTest, Json) { + QueryResultPrinter(ss, ApiOutputType::kJson).printLastTrades(marketLastTrades, nbLastTrades, lastTradesPerExchange); + static constexpr std::string_view kExpected = R"( +{ + "in": { + "opt": { + "market": "ETH-USDT", + "nb": 3 + }, + "req": "LastTrades" + }, + "out": { + "binance": [ + { + "a": "0.13", + "p": "1500.5", + "side": "Buy", + "time": "1999-03-25 04:46:43" + }, + { + "a": "3.7", + "p": "1500.5", + "side": "Sell", + "time": "2002-06-23 07:58:35" + }, + { + "a": "0.004", + "p": "1501", + "side": "Buy", + "time": "2006-07-14 23:58:24" + } + ], + "bithumb": [ + { + "a": "0.13", + "p": "1500.5", + "side": "Sell", + "time": "2011-10-03 06:49:36" + }, + { + "a": "0.004", + "p": "1501", + "side": "Buy", + "time": "2002-06-23 07:58:35" + }, + { + "a": "47.78", + "p": "1498", + "side": "Buy", + "time": "1999-03-25 04:46:43" + } + ], + "huobi": [ + { + "a": "0.13", + "p": "1500.5", + "side": "Sell", + "time": "2011-10-03 06:49:36" + }, + { + "a": "0.004", + "p": "1501", + "side": "Buy", + "time": "2002-06-23 07:58:35" + } + ] + } +} +)"; + expectJson(kExpected); +} + +TEST_F(QueryResultPrinterLastTradesVolumeTest, NoPrint) { + QueryResultPrinter(ss, ApiOutputType::kNoPrint) + .printLastTrades(marketLastTrades, nbLastTrades, lastTradesPerExchange); + expectNoStr(); +} + +class QueryResultPrinterLastPriceTest : public QueryResultPrinterTest { + protected: + Market marketLastPrice{"XRP", "KRW"}; + MonetaryAmountPerExchange monetaryAmountPerExchange{{&exchange1, MonetaryAmount{417, "KRW"}}, + {&exchange3, MonetaryAmount{444, "KRW"}}, + {&exchange2, MonetaryAmount{590, "KRW"}}}; +}; + +TEST_F(QueryResultPrinterLastPriceTest, FormattedTable) { + QueryResultPrinter(ss, ApiOutputType::kFormattedTable).printLastPrice(marketLastPrice, monetaryAmountPerExchange); + static constexpr std::string_view kExpected = R"( +--------------------------------- +| Exchange | XRP-KRW last price | +--------------------------------- +| binance | 417 KRW | +| huobi | 444 KRW | +| bithumb | 590 KRW | +--------------------------------- +)"; + expectStr(kExpected); +} + +TEST_F(QueryResultPrinterLastPriceTest, EmptyJson) { + QueryResultPrinter(ss, ApiOutputType::kJson).printLastPrice(marketLastPrice, MonetaryAmountPerExchange{}); + static constexpr std::string_view kExpected = R"( +{ + "in": { + "opt": { + "market": "XRP-KRW" + }, + "req": "LastPrice" + }, + "out": {} +} +)"; + expectJson(kExpected); +} + +TEST_F(QueryResultPrinterLastPriceTest, Json) { + QueryResultPrinter(ss, ApiOutputType::kJson).printLastPrice(marketLastPrice, monetaryAmountPerExchange); + static constexpr std::string_view kExpected = R"( +{ + "in": { + "opt": { + "market": "XRP-KRW" + }, + "req": "LastPrice" + }, + "out": { + "binance": "417", + "bithumb": "590", + "huobi": "444" + } +} +)"; + expectJson(kExpected); +} + +TEST_F(QueryResultPrinterLastPriceTest, NoPrint) { + QueryResultPrinter(ss, ApiOutputType::kNoPrint).printLastPrice(marketLastPrice, monetaryAmountPerExchange); + expectNoStr(); +} + +class QueryResultPrinterWithdrawTest : public QueryResultPrinterTest { + protected: + MonetaryAmount grossAmount{"76.55 XRP"}; + MonetaryAmount netEmittedAmount{"75.55 XRP"}; + bool isWithdrawSent = true; + ExchangeName fromExchange{exchange1.apiPrivate().exchangeName()}; + ExchangeName toExchange{exchange4.apiPrivate().exchangeName()}; + + Wallet receivingWallet{toExchange, grossAmount.currencyCode(), "xrpaddress666", "xrptag2", WalletCheck{}}; + WithdrawIdView withdrawId = "WithdrawTest01"; + MonetaryAmount grossEmittedAmount; + api::InitiatedWithdrawInfo initiatedWithdrawInfo{receivingWallet, withdrawId, grossAmount, tp1}; + api::SentWithdrawInfo sentWithdrawInfo{netEmittedAmount, isWithdrawSent}; + WithdrawInfo withdrawInfo{initiatedWithdrawInfo, sentWithdrawInfo, tp2}; +}; + +class QueryResultPrinterWithdrawAmountTest : public QueryResultPrinterWithdrawTest { + protected: + bool isPercentageWithdraw = false; +}; + +TEST_F(QueryResultPrinterWithdrawAmountTest, FormattedTable) { + QueryResultPrinter(ss, ApiOutputType::kFormattedTable) + .printWithdraw(withdrawInfo, grossAmount, isPercentageWithdraw, fromExchange, toExchange); + static constexpr std::string_view kExpected = R"( +------------------------------------------------------------------------------------------------------------------------- +| From Exchange | To Exchange | Gross withdraw amount | Initiated time | Received time | Net received amount | +------------------------------------------------------------------------------------------------------------------------- +| binance | huobi | 76.55 XRP | 1999-03-25 04:46:43 | 2002-06-23 07:58:35 | 75.55 XRP | +------------------------------------------------------------------------------------------------------------------------- +)"; + expectStr(kExpected); +} + +TEST_F(QueryResultPrinterWithdrawAmountTest, Json) { + QueryResultPrinter(ss, ApiOutputType::kJson) + .printWithdraw(withdrawInfo, grossAmount, isPercentageWithdraw, fromExchange, toExchange); + static constexpr std::string_view kExpected = R"( +{ + "in": { + "opt": { + "cur": "XRP", + "grossAmount": "76.55", + "isPercentage": false + }, + "req": "Withdraw" + }, + "out": { + "from": { + "account": "testuser1", + "exchange": "binance" + }, + "initiatedTime": "1999-03-25 04:46:43", + "netReceivedAmount": "75.55", + "receivedTime": "2002-06-23 07:58:35", + "to": { + "account": "testuser2", + "address": "xrpaddress666", + "exchange": "huobi", + "tag": "xrptag2" + } + } +} +)"; + expectJson(kExpected); +} + +TEST_F(QueryResultPrinterWithdrawAmountTest, NoPrint) { + QueryResultPrinter(ss, ApiOutputType::kNoPrint) + .printWithdraw(withdrawInfo, grossAmount, isPercentageWithdraw, fromExchange, toExchange); + expectNoStr(); +} + +class QueryResultPrinterWithdrawPercentageTest : public QueryResultPrinterWithdrawTest { + protected: + bool isPercentageWithdraw = true; +}; + +TEST_F(QueryResultPrinterWithdrawPercentageTest, FormattedTable) { + QueryResultPrinter(ss, ApiOutputType::kFormattedTable) + .printWithdraw(withdrawInfo, grossAmount, isPercentageWithdraw, fromExchange, toExchange); + static constexpr std::string_view kExpected = R"( +------------------------------------------------------------------------------------------------------------------------- +| From Exchange | To Exchange | Gross withdraw amount | Initiated time | Received time | Net received amount | +------------------------------------------------------------------------------------------------------------------------- +| binance | huobi | 76.55 XRP | 1999-03-25 04:46:43 | 2002-06-23 07:58:35 | 75.55 XRP | +------------------------------------------------------------------------------------------------------------------------- +)"; + expectStr(kExpected); +} + +TEST_F(QueryResultPrinterWithdrawPercentageTest, Json) { + QueryResultPrinter(ss, ApiOutputType::kJson) + .printWithdraw(withdrawInfo, grossAmount, isPercentageWithdraw, fromExchange, toExchange); + static constexpr std::string_view kExpected = R"( +{ + "in": { + "opt": { + "cur": "XRP", + "grossAmount": "76.55", + "isPercentage": true + }, + "req": "Withdraw" + }, + "out": { + "from": { + "account": "testuser1", + "exchange": "binance" + }, + "initiatedTime": "1999-03-25 04:46:43", + "netReceivedAmount": "75.55", + "receivedTime": "2002-06-23 07:58:35", + "to": { + "account": "testuser2", + "address": "xrpaddress666", + "exchange": "huobi", + "tag": "xrptag2" + } + } +} +)"; + expectJson(kExpected); +} + +TEST_F(QueryResultPrinterWithdrawPercentageTest, NoPrint) { + QueryResultPrinter(ss, ApiOutputType::kNoPrint) + .printWithdraw(withdrawInfo, grossAmount, isPercentageWithdraw, fromExchange, toExchange); + expectNoStr(); +} + +} // namespace cct \ No newline at end of file diff --git a/src/objects/include/apioutputtype.hpp b/src/objects/include/apioutputtype.hpp new file mode 100644 index 00000000..55b384f0 --- /dev/null +++ b/src/objects/include/apioutputtype.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include +#include + +namespace cct { +enum class ApiOutputType : int8_t { kNoPrint, kFormattedTable, kJson }; + +static constexpr std::string_view kApiOutputTypeNoPrintStr = "off"; +static constexpr std::string_view kApiOutputTypeTableStr = "table"; +static constexpr std::string_view kApiOutputTypeJsonStr = "json"; + +ApiOutputType ApiOutputTypeFromString(std::string_view str); +} // namespace cct \ No newline at end of file diff --git a/src/objects/include/balanceportfolio.hpp b/src/objects/include/balanceportfolio.hpp index 57760266..e670c24b 100644 --- a/src/objects/include/balanceportfolio.hpp +++ b/src/objects/include/balanceportfolio.hpp @@ -1,5 +1,8 @@ #pragma once +#include +#include + #include "cct_type_traits.hpp" #include "cct_vector.hpp" #include "currencycode.hpp" @@ -23,6 +26,18 @@ class BalancePortfolio { using const_iterator = MonetaryAmountVec::const_iterator; using size_type = MonetaryAmountVec::size_type; + BalancePortfolio() noexcept = default; + + BalancePortfolio(std::initializer_list init) + : BalancePortfolio(std::span(init.begin(), init.end())) {} + + BalancePortfolio(std::span init); + + BalancePortfolio(std::initializer_list init) + : BalancePortfolio(std::span(init.begin(), init.end())) {} + + BalancePortfolio(std::span init); + /// Adds an amount in the `BalancePortfolio`. /// @param equivalentInMainCurrency (optional) also add its corresponding value in another currency void add(MonetaryAmount amount, MonetaryAmount equivalentInMainCurrency = MonetaryAmount()); diff --git a/src/objects/include/coincenterinfo.hpp b/src/objects/include/coincenterinfo.hpp index 43d54951..99b6756e 100644 --- a/src/objects/include/coincenterinfo.hpp +++ b/src/objects/include/coincenterinfo.hpp @@ -6,6 +6,7 @@ #include #include +#include "apioutputtype.hpp" #include "cct_string.hpp" #include "currencycode.hpp" #include "exchangeinfo.hpp" @@ -62,7 +63,7 @@ class CoincenterInfo { AbstractMetricGateway *metricGatewayPtr() const { return _metricGatewayPtr.get(); } - bool printResults() const { return _generalConfig.printResults(); } + ApiOutputType apiOutputType() const { return _generalConfig.apiOutputType(); } Duration fiatConversionQueryRate() const { return _generalConfig.fiatConversionQueryRate(); } diff --git a/src/objects/include/generalconfig.hpp b/src/objects/include/generalconfig.hpp index 97900309..977bbcf9 100644 --- a/src/objects/include/generalconfig.hpp +++ b/src/objects/include/generalconfig.hpp @@ -2,6 +2,7 @@ #include +#include "apioutputtype.hpp" #include "cct_json.hpp" #include "logginginfo.hpp" #include "timedef.hpp" @@ -16,20 +17,20 @@ class GeneralConfig { GeneralConfig() = default; - GeneralConfig(const LoggingInfo &loggingInfo, Duration fiatConversionQueryRate, bool printResults); + GeneralConfig(const LoggingInfo &loggingInfo, Duration fiatConversionQueryRate, ApiOutputType apiOutputType); - GeneralConfig(LoggingInfo &&loggingInfo, Duration fiatConversionQueryRate, bool printResults); + GeneralConfig(LoggingInfo &&loggingInfo, Duration fiatConversionQueryRate, ApiOutputType apiOutputType); const LoggingInfo &loggingInfo() const { return _loggingInfo; } - bool printResults() const { return _printResults; } + ApiOutputType apiOutputType() const { return _apiOutputType; } Duration fiatConversionQueryRate() const { return _fiatConversionQueryRate; } private: LoggingInfo _loggingInfo; Duration _fiatConversionQueryRate = std::chrono::hours(8); - bool _printResults = true; + ApiOutputType _apiOutputType = ApiOutputType::kFormattedTable; }; } // namespace cct \ No newline at end of file diff --git a/src/objects/include/priceoptions.hpp b/src/objects/include/priceoptions.hpp index 6308c354..09b3bbe0 100644 --- a/src/objects/include/priceoptions.hpp +++ b/src/objects/include/priceoptions.hpp @@ -45,15 +45,13 @@ class PriceOptions { constexpr void switchToTakerStrategy() { _priceStrategy = PriceStrategy::kTaker; } - std::string_view timeoutActionStr() const; + std::string_view priceStrategyStr(bool placeRealOrderInSimulationMode) const; string str(bool placeRealOrderInSimulationMode) const; bool operator==(const PriceOptions &) const = default; private: - std::string_view priceStrategyStr(bool placeRealOrderInSimulationMode) const; - MonetaryAmount _fixedPrice; RelativePrice _relativePrice = kNoRelativePrice; PriceStrategy _priceStrategy = PriceStrategy::kMaker; diff --git a/src/objects/include/tradeside.hpp b/src/objects/include/tradeside.hpp index d56e63f0..d9863879 100644 --- a/src/objects/include/tradeside.hpp +++ b/src/objects/include/tradeside.hpp @@ -1,7 +1,21 @@ #pragma once #include +#include + +#include "unreachable.hpp" namespace cct { enum class TradeSide : int8_t { kBuy, kSell }; -} \ No newline at end of file + +inline std::string_view SideStr(TradeSide side) { + switch (side) { + case TradeSide::kBuy: + return "Buy"; + case TradeSide::kSell: + return "Sell"; + default: + unreachable(); + } +} +} // namespace cct \ No newline at end of file diff --git a/src/objects/src/apioutputtype.cpp b/src/objects/src/apioutputtype.cpp new file mode 100644 index 00000000..ad4b9369 --- /dev/null +++ b/src/objects/src/apioutputtype.cpp @@ -0,0 +1,23 @@ +#include "apioutputtype.hpp" + +#include "cct_invalid_argument_exception.hpp" +#include "cct_string.hpp" +#include "toupperlower.hpp" + +namespace cct { +ApiOutputType ApiOutputTypeFromString(std::string_view str) { + string lowerStr = ToLower(str); + if (lowerStr == kApiOutputTypeNoPrintStr) { + return ApiOutputType::kNoPrint; + } + if (lowerStr == kApiOutputTypeTableStr) { + return ApiOutputType::kFormattedTable; + } + if (lowerStr == kApiOutputTypeJsonStr) { + return ApiOutputType::kJson; + } + string err("Unrecognized api output type "); + err.append(str); + throw invalid_argument(std::move(err)); +} +} // namespace cct \ No newline at end of file diff --git a/src/objects/src/balanceportfolio.cpp b/src/objects/src/balanceportfolio.cpp index 3a27e5c3..95aa2685 100644 --- a/src/objects/src/balanceportfolio.cpp +++ b/src/objects/src/balanceportfolio.cpp @@ -6,7 +6,7 @@ namespace cct { namespace { using MonetaryAmountWithEquivalent = BalancePortfolio::MonetaryAmountWithEquivalent; -inline bool Compare(const MonetaryAmountWithEquivalent &lhs, const MonetaryAmountWithEquivalent &rhs) { +inline bool CurCompare(const MonetaryAmountWithEquivalent &lhs, const MonetaryAmountWithEquivalent &rhs) { return lhs.amount.currencyCode() < rhs.amount.currencyCode(); } @@ -18,16 +18,30 @@ inline MonetaryAmountWithEquivalent &operator+=(MonetaryAmountWithEquivalent &lh } } // namespace +BalancePortfolio::BalancePortfolio(std::span init) { + // Simple for loop to avoid complex code eliminating duplicates for same currency + for (MonetaryAmount a : init) { + add(a); + } +} + +BalancePortfolio::BalancePortfolio(std::span init) { + // Simple for loop to avoid complex code eliminating duplicates for same currency + for (const MonetaryAmountWithEquivalent &e : init) { + add(e.amount, e.equi); + } +} + void BalancePortfolio::add(MonetaryAmount amount, MonetaryAmount equivalentInMainCurrency) { MonetaryAmountWithEquivalent elem{amount, equivalentInMainCurrency}; - auto lb = std::ranges::lower_bound(_sortedAmounts, elem, Compare); + auto lb = std::ranges::lower_bound(_sortedAmounts, elem, CurCompare); if (lb == _sortedAmounts.end()) { _sortedAmounts.push_back(std::move(elem)); - } else if (Compare(elem, *lb)) { + } else if (CurCompare(elem, *lb)) { _sortedAmounts.insert(lb, std::move(elem)); } else { // equal, sum amounts - *lb += elem; + *lb += std::move(elem); } } @@ -52,9 +66,9 @@ BalancePortfolio &BalancePortfolio::operator+=(const BalancePortfolio &o) { _sortedAmounts.insert(_sortedAmounts.end(), first2, last2); break; } - if (Compare(*first1, *first2)) { + if (CurCompare(*first1, *first2)) { ++first1; - } else if (Compare(*first2, *first1)) { + } else if (CurCompare(*first2, *first1)) { first1 = _sortedAmounts.insert(first1, *first2); ++first1; last1 = _sortedAmounts.end(); // as iterators may have been invalidated diff --git a/src/objects/src/generalconfig.cpp b/src/objects/src/generalconfig.cpp index 4f2974a3..dc9bbb8f 100644 --- a/src/objects/src/generalconfig.cpp +++ b/src/objects/src/generalconfig.cpp @@ -4,18 +4,20 @@ namespace cct { -GeneralConfig::GeneralConfig(const LoggingInfo &loggingInfo, Duration fiatConversionQueryRate, bool printResults) - : _loggingInfo(loggingInfo), _fiatConversionQueryRate(fiatConversionQueryRate), _printResults(printResults) {} +GeneralConfig::GeneralConfig(const LoggingInfo &loggingInfo, Duration fiatConversionQueryRate, + ApiOutputType apiOutputType) + : _loggingInfo(loggingInfo), _fiatConversionQueryRate(fiatConversionQueryRate), _apiOutputType(apiOutputType) {} -GeneralConfig::GeneralConfig(LoggingInfo &&loggingInfo, Duration fiatConversionQueryRate, bool printResults) +GeneralConfig::GeneralConfig(LoggingInfo &&loggingInfo, Duration fiatConversionQueryRate, ApiOutputType apiOutputType) : _loggingInfo(std::move(loggingInfo)), _fiatConversionQueryRate(fiatConversionQueryRate), - _printResults(printResults) {} + _apiOutputType(apiOutputType) {} json GeneralConfig::LoadFile(std::string_view dataDir) { File generalConfigFile(dataDir, File::Type::kStatic, GeneralConfig::kFilename, File::IfNotFound::kNoThrow); static const json kDefaultGeneralConfig = R"( { + "apiOutputType": "table", "log": { "level": "info", "file": false, @@ -24,8 +26,7 @@ json GeneralConfig::LoadFile(std::string_view dataDir) { }, "fiatConversion": { "rate": "8h" - }, - "printResults": true + } } )"_json; json jsonData = kDefaultGeneralConfig; diff --git a/src/objects/src/monetaryamount.cpp b/src/objects/src/monetaryamount.cpp index debecc4b..be644cd1 100644 --- a/src/objects/src/monetaryamount.cpp +++ b/src/objects/src/monetaryamount.cpp @@ -261,7 +261,7 @@ void MonetaryAmount::round(int8_t nbDecimals, RoundType roundType) { } else { if (roundType != RoundType::kDown) { const AmountType r = epsilon - (_amount % epsilon); - if ( //_amount <= std::numeric_limits::max() - r && // Protection against overflow + if (_amount <= std::numeric_limits::max() - r && // Protection against overflow (roundType == RoundType::kUp || 2 * r <= epsilon)) { _amount += r; } diff --git a/src/objects/src/order.cpp b/src/objects/src/order.cpp index 6409f09f..7cb701a6 100644 --- a/src/objects/src/order.cpp +++ b/src/objects/src/order.cpp @@ -23,16 +23,7 @@ Order::Order(string &&id, MonetaryAmount matchedVolume, MonetaryAmount remaining _price(price), _side(side) {} -std::string_view Order::sideStr() const { - switch (_side) { - case TradeSide::kBuy: - return "Buy"; - case TradeSide::kSell: - return "Sell"; - default: - unreachable(); - } -} +std::string_view Order::sideStr() const { return SideStr(_side); } string Order::placedTimeStr() const { return ToString(_placedTime); } } // namespace cct \ No newline at end of file diff --git a/src/objects/src/wallet.cpp b/src/objects/src/wallet.cpp index a5ac9d34..5b225c77 100644 --- a/src/objects/src/wallet.cpp +++ b/src/objects/src/wallet.cpp @@ -95,7 +95,7 @@ string Wallet::str() const { string ret(_exchangeName.str()); ret.append(" wallet of "); ret.append(_currency.str()); - ret.append(", address: ["); + ret.append(" ["); ret.append(address()); ret.push_back(']'); if (hasTag()) { diff --git a/src/tech/include/cct_config.hpp b/src/tech/include/cct_config.hpp index dbe3f905..112de21e 100644 --- a/src/tech/include/cct_config.hpp +++ b/src/tech/include/cct_config.hpp @@ -63,4 +63,9 @@ // implements it. More information here: // https://stackoverflow.com/questions/70260994/automatic-template-deduction-c20-with-aggregate-type #define CCT_AGGR_INIT_CXX20 +#endif + +#if defined(CCT_MSVC) || (defined(CCT_GCC) && CCT_GCC >= 110000) +// std::stringstream::view is not yet implemented for many compilers. +#define CCT_STRINGSTREAM_HAS_VIEW #endif \ No newline at end of file diff --git a/src/tools/src/curlhandle.cpp b/src/tools/src/curlhandle.cpp index fdbf36c0..48ac4c89 100644 --- a/src/tools/src/curlhandle.cpp +++ b/src/tools/src/curlhandle.cpp @@ -5,7 +5,6 @@ #include #include #include -#include #include #include #include