From 832761922da6e8f830d1881556c7e1e4484e4861 Mon Sep 17 00:00:00 2001 From: Stephane Janel Date: Mon, 20 Nov 2023 00:21:37 +0100 Subject: [PATCH] New CLI option - currencies, to get all tradable currencies --- README.md | 29 +++- data/static/currencyacronymtranslator.json | 4 +- src/api/exchanges/test/commonapi_test.hpp | 9 +- src/engine/include/coincenter.hpp | 3 + src/engine/include/coincenteroptions.hpp | 1 + src/engine/include/coincenteroptionsdef.hpp | 6 + .../include/commandlineoptionsparser.hpp | 6 +- src/engine/include/exchangesorchestrator.hpp | 2 + src/engine/include/queryresultprinter.hpp | 2 + src/engine/include/queryresulttypes.hpp | 3 + src/engine/src/coincenter.cpp | 9 ++ src/engine/src/coincentercommand.cpp | 2 + src/engine/src/coincentercommands.cpp | 5 + src/engine/src/exchangesorchestrator.cpp | 13 ++ src/engine/src/queryresultprinter.cpp | 117 ++++++++++++++ .../test/coincentercommandfactory_test.cpp | 3 + .../test/queryresultprinter_public_test.cpp | 150 ++++++++++++++++++ .../test/transferablecommandresult_test.cpp | 1 + src/main/src/main.cpp | 2 + src/objects/include/coincentercommandtype.hpp | 1 + .../include/currencyexchangeflatset.hpp | 5 +- src/objects/src/coincentercommandtype.cpp | 5 + src/tech/include/simpletable.hpp | 18 ++- src/tech/src/simpletable.cpp | 56 ++++--- src/tech/test/simpletable_test.cpp | 68 ++++++-- 25 files changed, 461 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index 6a2d5d1e..397451bb 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,8 @@ Main features: **Market Data** -- Market +- Currencies +- Markets - Ticker - Orderbook - Traded volume @@ -64,8 +65,10 @@ Main features: - [Parallel requests](#parallel-requests) - [Public requests](#public-requests) - [Health check](#health-check) - - [Markets](#markets) + - [Currencies](#currencies) - [Examples](#examples) + - [Markets](#markets) + - [Examples](#examples-1) - [Ticker information](#ticker-information) - [Order books](#order-books) - [Last 24h traded volume](#last-24h-traded-volume) @@ -98,9 +101,9 @@ Main features: - [Examples with explanation](#examples-with-explanation-1) - [Deposit information](#deposit-information) - [Recent deposits](#recent-deposits) - - [Examples](#examples-1) - - [Recent withdraws](#recent-withdraws) - [Examples](#examples-2) + - [Recent withdraws](#recent-withdraws) + - [Examples](#examples-3) - [Opened orders](#opened-orders) - [Cancel opened orders](#cancel-opened-orders) - [Withdraw coin](#withdraw-coin) @@ -244,6 +247,24 @@ You will have a nice boost of speed when you query the same thing from multiple `health-check` pings the exchanges and checks if there are up and running. It is the first thing that is checked in exchanges unit tests, hence if the health check fails for an exchange, the tests are skipped for the exchange. +### Currencies + +`currencies` command aggregates currency codes for given list of exchanges. It also tells for which exchanges it can be deposited to, withdrawn from, and if it is a fiat money. + +#### Examples + +List all currencies for all supported exchanges + +``` +coincenter currencies +``` + +List all currencies for kucoin and upbit + +``` +coincenter currencies kucoin,upbit +``` + ### Markets Use the `markets` command to list all markets trading a given currencies. This is useful to check how you can trade your coin. diff --git a/data/static/currencyacronymtranslator.json b/data/static/currencyacronymtranslator.json index 9837a50e..e7d67513 100644 --- a/data/static/currencyacronymtranslator.json +++ b/data/static/currencyacronymtranslator.json @@ -9,8 +9,8 @@ "XLTC": "LTC", "XXLM": "XLM", "XMLN": "MLN", - "XXDG": "XDG", - "DOGE": "XDG", + "XDG": "DOGE", + "XXDG": "DOGE", "XZEC": "ZEC", "ZEUR": "EUR", "ZCAD": "CAD", diff --git a/src/api/exchanges/test/commonapi_test.hpp b/src/api/exchanges/test/commonapi_test.hpp index b5f3a386..0ece4b9c 100644 --- a/src/api/exchanges/test/commonapi_test.hpp +++ b/src/api/exchanges/test/commonapi_test.hpp @@ -72,8 +72,7 @@ class TestAPI { currencies = exchangePrivateOpt ? exchangePrivateOpt->queryTradableCurrencies() : exchangePublic.queryTradableCurrencies(); ASSERT_FALSE(currencies.empty()); - EXPECT_TRUE( - std::ranges::none_of(currencies, [](const CurrencyExchange &c) { return c.standardCode().str().empty(); })); + EXPECT_TRUE(std::ranges::none_of(currencies, [](const auto &c) { return c.standardCode().str().empty(); })); // Uncomment below code to print updated Upbit withdrawal fees for static data of withdrawal fees of public API // if (exchangePrivateOpt) { @@ -159,9 +158,8 @@ class TestAPI { ASSERT_NE(withdrawalFeeIt, withdrawalFees.end()); EXPECT_GE(withdrawalFeeIt->second, MonetaryAmount(0, withdrawalFeeIt->second.currencyCode())); break; - } else { - log::warn("{} withdrawal fee is not known (unreliable source), trying another one", cur); } + log::warn("{} withdrawal fee is not known (unreliable source), trying another one", cur); } } } @@ -204,9 +202,8 @@ class TestAPI { } catch (const exception &) { if (exchangePrivateOpt->canGenerateDepositAddress()) { throw; - } else { - log::info("Wallet for {} is not generated, taking next one", cur); } + log::info("Wallet for {} is not generated, taking next one", cur); } } } diff --git a/src/engine/include/coincenter.hpp b/src/engine/include/coincenter.hpp index 73d9f456..682eaf7d 100644 --- a/src/engine/include/coincenter.hpp +++ b/src/engine/include/coincenter.hpp @@ -33,6 +33,9 @@ class Coincenter { ExchangeHealthCheckStatus healthCheck(ExchangeNameSpan exchangeNames); + /// Retrieve all tradable currencies for given selected public exchanges, or all if empty. + CurrenciesPerExchange getCurrenciesPerExchange(ExchangeNameSpan exchangeNames); + /// Retrieve the markets for given selected public exchanges, or all if empty. MarketsPerExchange getMarketsPerExchange(CurrencyCode cur1, CurrencyCode cur2, ExchangeNameSpan exchangeNames); diff --git a/src/engine/include/coincenteroptions.hpp b/src/engine/include/coincenteroptions.hpp index 76019dbc..3bb608c0 100644 --- a/src/engine/include/coincenteroptions.hpp +++ b/src/engine/include/coincenteroptions.hpp @@ -44,6 +44,7 @@ class CoincenterCmdLineOptions { std::string_view monitoringUsername; std::string_view monitoringPassword; + std::optional currencies; std::string_view markets; std::string_view orderbook; diff --git a/src/engine/include/coincenteroptionsdef.hpp b/src/engine/include/coincenteroptionsdef.hpp index dcc90b80..61c9f183 100644 --- a/src/engine/include/coincenteroptionsdef.hpp +++ b/src/engine/include/coincenteroptionsdef.hpp @@ -173,6 +173,12 @@ struct CoincenterAllowedOptions : private CoincenterCmdLineOptionsDefinitions { "<[exch1,...]>", "Simple health check for all exchanges or specified ones"}, &OptValueType::healthCheck}, + {{{"Public queries", 2100}, + "currencies", + "<[exch1,...]>", + "Print tradable currencies for all exchanges, " + "or only the specified ones."}, + &OptValueType::currencies}, {{{"Public queries", 2100}, "markets", "", diff --git a/src/engine/include/commandlineoptionsparser.hpp b/src/engine/include/commandlineoptionsparser.hpp index 2bdee22a..99e722f8 100644 --- a/src/engine/include/commandlineoptionsparser.hpp +++ b/src/engine/include/commandlineoptionsparser.hpp @@ -66,10 +66,12 @@ class CommandLineOptionsParser { std::string_view argStr(groupedArguments[argPos]); if (std::ranges::none_of(_opts, [argStr](const auto& opt) { return opt.first.matches(argStr); })) { const auto [possibleOptionIdx, minDistance] = minLevenshteinDistanceOpt(argStr); + auto existingOptionStr = _opts[possibleOptionIdx].first.fullName(); - if (minDistance <= 2) { + if (minDistance <= 2 || + minDistance < static_cast(std::min(argStr.size(), existingOptionStr.size()) / 2)) { throw invalid_argument("Unrecognized command-line option '{}' - did you mean '{}'?", argStr, - _opts[possibleOptionIdx].first.fullName()); + existingOptionStr); } throw invalid_argument("Unrecognized command-line option '{}'", argStr); } diff --git a/src/engine/include/exchangesorchestrator.hpp b/src/engine/include/exchangesorchestrator.hpp index ddab414e..d49601fb 100644 --- a/src/engine/include/exchangesorchestrator.hpp +++ b/src/engine/include/exchangesorchestrator.hpp @@ -44,6 +44,8 @@ class ExchangesOrchestrator { ConversionPathPerExchange getConversionPaths(Market mk, ExchangeNameSpan exchangeNames); + CurrenciesPerExchange getCurrenciesPerExchange(ExchangeNameSpan exchangeNames); + MarketsPerExchange getMarketsPerExchange(CurrencyCode cur1, CurrencyCode cur2, ExchangeNameSpan exchangeNames); UniquePublicSelectedExchanges getExchangesTradingCurrency(CurrencyCode currencyCode, ExchangeNameSpan exchangeNames, diff --git a/src/engine/include/queryresultprinter.hpp b/src/engine/include/queryresultprinter.hpp index c0a969f8..4e64f5e8 100644 --- a/src/engine/include/queryresultprinter.hpp +++ b/src/engine/include/queryresultprinter.hpp @@ -33,6 +33,8 @@ class QueryResultPrinter { void printHealthCheck(const ExchangeHealthCheckStatus &healthCheckPerExchange) const; + void printCurrencies(const CurrenciesPerExchange ¤ciesPerExchange) const; + void printMarkets(CurrencyCode cur1, CurrencyCode cur2, const MarketsPerExchange &marketsPerExchange) const; void printMarketOrderBooks(Market mk, CurrencyCode equiCurrencyCode, std::optional depth, diff --git a/src/engine/include/queryresulttypes.hpp b/src/engine/include/queryresulttypes.hpp index a25c855e..5cd9263a 100644 --- a/src/engine/include/queryresulttypes.hpp +++ b/src/engine/include/queryresulttypes.hpp @@ -10,6 +10,7 @@ #include "cct_const.hpp" #include "cct_fixedcapacityvector.hpp" #include "cct_smallvector.hpp" +#include "currencyexchangeflatset.hpp" #include "exchangeprivateapitypes.hpp" #include "exchangepublicapitypes.hpp" #include "marketorderbook.hpp" @@ -43,6 +44,8 @@ using ExchangeHealthCheckStatus = FixedCapacityVector, kNbSup using ExchangeTickerMaps = FixedCapacityVector, kNbSupportedExchanges>; +using CurrenciesPerExchange = FixedCapacityVector, kNbSupportedExchanges>; + using BalancePerExchange = SmallVector, kTypicalNbPrivateAccounts>; using WalletPerExchange = SmallVector, kTypicalNbPrivateAccounts>; diff --git a/src/engine/src/coincenter.cpp b/src/engine/src/coincenter.cpp index fd0d7e6e..b12de533 100644 --- a/src/engine/src/coincenter.cpp +++ b/src/engine/src/coincenter.cpp @@ -82,6 +82,11 @@ TransferableCommandResultVector Coincenter::processCommand( _queryResultPrinter.printHealthCheck(healthCheckStatus); break; } + case CoincenterCommandType::kCurrencies: { + const auto currenciesPerExchange = getCurrenciesPerExchange(cmd.exchangeNames()); + _queryResultPrinter.printCurrencies(currenciesPerExchange); + break; + } case CoincenterCommandType::kMarkets: { const auto marketsPerExchange = getMarketsPerExchange(cmd.cur1(), cmd.cur2(), cmd.exchangeNames()); _queryResultPrinter.printMarkets(cmd.cur1(), cmd.cur2(), marketsPerExchange); @@ -292,6 +297,10 @@ ConversionPathPerExchange Coincenter::getConversionPaths(Market mk, ExchangeName return _exchangesOrchestrator.getConversionPaths(mk, exchangeNames); } +CurrenciesPerExchange Coincenter::getCurrenciesPerExchange(ExchangeNameSpan exchangeNames) { + return _exchangesOrchestrator.getCurrenciesPerExchange(exchangeNames); +} + MarketsPerExchange Coincenter::getMarketsPerExchange(CurrencyCode cur1, CurrencyCode cur2, ExchangeNameSpan exchangeNames) { return _exchangesOrchestrator.getMarketsPerExchange(cur1, cur2, exchangeNames); diff --git a/src/engine/src/coincentercommand.cpp b/src/engine/src/coincentercommand.cpp index afed84be..5732e6a9 100644 --- a/src/engine/src/coincentercommand.cpp +++ b/src/engine/src/coincentercommand.cpp @@ -19,6 +19,8 @@ bool CoincenterCommand::isPublic() const { switch (_type) { case CoincenterCommandType::kHealthCheck: // NOLINT(bugprone-branch-clone) [[fallthrough]]; + case CoincenterCommandType::kCurrencies: + [[fallthrough]]; case CoincenterCommandType::kMarkets: [[fallthrough]]; case CoincenterCommandType::kConversionPath: diff --git a/src/engine/src/coincentercommands.cpp b/src/engine/src/coincentercommands.cpp index aa7697fb..2f3327fe 100644 --- a/src/engine/src/coincentercommands.cpp +++ b/src/engine/src/coincentercommands.cpp @@ -51,6 +51,11 @@ void CoincenterCommands::addOption(const CoincenterCmdLineOptions &cmdLineOption _commands.emplace_back(CoincenterCommandType::kHealthCheck).setExchangeNames(optionParser.parseExchanges()); } + if (cmdLineOptions.currencies) { + optionParser = StringOptionParser(*cmdLineOptions.currencies); + _commands.emplace_back(CoincenterCommandType::kCurrencies).setExchangeNames(optionParser.parseExchanges()); + } + if (!cmdLineOptions.markets.empty()) { optionParser = StringOptionParser(cmdLineOptions.markets); _commands.push_back(CoincenterCommandFactory::CreateMarketCommand(optionParser)); diff --git a/src/engine/src/exchangesorchestrator.cpp b/src/engine/src/exchangesorchestrator.cpp index b115a71b..c59dd14a 100644 --- a/src/engine/src/exchangesorchestrator.cpp +++ b/src/engine/src/exchangesorchestrator.cpp @@ -290,6 +290,19 @@ ConversionPathPerExchange ExchangesOrchestrator::getConversionPaths(Market mk, E return conversionPathPerExchange; } +CurrenciesPerExchange ExchangesOrchestrator::getCurrenciesPerExchange(ExchangeNameSpan exchangeNames) { + log::info("Get all tradable currencies for {}", ConstructAccumulatedExchangeNames(exchangeNames)); + + UniquePublicSelectedExchanges selectedExchanges = _exchangeRetriever.selectOneAccount(exchangeNames); + + CurrenciesPerExchange ret(selectedExchanges.size()); + _threadPool.parallelTransform( + selectedExchanges.begin(), selectedExchanges.end(), ret.begin(), + [](Exchange *exchange) { return std::make_pair(exchange, exchange->queryTradableCurrencies()); }); + + return ret; +} + MarketsPerExchange ExchangesOrchestrator::getMarketsPerExchange(CurrencyCode cur1, CurrencyCode cur2, ExchangeNameSpan exchangeNames) { string curStr = cur1.str(); diff --git a/src/engine/src/queryresultprinter.cpp b/src/engine/src/queryresultprinter.cpp index 06f87101..45a81dc8 100644 --- a/src/engine/src/queryresultprinter.cpp +++ b/src/engine/src/queryresultprinter.cpp @@ -16,6 +16,8 @@ #include "cct_string.hpp" #include "coincentercommandtype.hpp" #include "currencycode.hpp" +#include "currencycodevector.hpp" +#include "currencyexchange.hpp" #include "deposit.hpp" #include "depositsconstraints.hpp" #include "durationstring.hpp" @@ -68,6 +70,34 @@ json HealthCheckJson(const ExchangeHealthCheckStatus &healthCheckPerExchange) { return ToJson(CoincenterCommandType::kHealthCheck, std::move(in), std::move(out)); } +json CurrenciesJson(const CurrenciesPerExchange ¤ciesPerExchange) { + json in; + json inOpt; + in.emplace("opt", std::move(inOpt)); + + json out = json::object(); + for (const auto &[e, currencies] : currenciesPerExchange) { + json currenciesForExchange; + for (const CurrencyExchange &cur : currencies) { + json curExchangeJson; + + curExchangeJson.emplace("code", cur.standardCode().str()); + curExchangeJson.emplace("exchangeCode", cur.exchangeCode().str()); + curExchangeJson.emplace("altCode", cur.altCode().str()); + + curExchangeJson.emplace("canDeposit", cur.canDeposit()); + curExchangeJson.emplace("canWithdraw", cur.canWithdraw()); + + curExchangeJson.emplace("isFiat", cur.isFiat()); + + currenciesForExchange.emplace_back(std::move(curExchangeJson)); + } + out.emplace(e->name(), std::move(currenciesForExchange)); + } + + return ToJson(CoincenterCommandType::kCurrencies, std::move(in), std::move(out)); +} + json MarketsJson(CurrencyCode cur1, CurrencyCode cur2, const MarketsPerExchange &marketsPerExchange) { json in; json inOpt; @@ -640,6 +670,93 @@ void QueryResultPrinter::printHealthCheck(const ExchangeHealthCheckStatus &healt logActivity(CoincenterCommandType::kHealthCheck, jsonData); } +namespace { +void AppendWithExchangeName(string &str, std::string_view value, std::string_view exchangeName) { + if (!str.empty()) { + str.push_back(','); + } + str.append(value); + str.push_back('['); + str.append(exchangeName); + str.push_back(']'); +} + +void Append(string &str, std::string_view exchangeName) { + if (!str.empty()) { + str.push_back(','); + } + str.append(exchangeName); +} +} // namespace + +void QueryResultPrinter::printCurrencies(const CurrenciesPerExchange ¤ciesPerExchange) const { + json jsonData = CurrenciesJson(currenciesPerExchange); + switch (_apiOutputType) { + case ApiOutputType::kFormattedTable: { + SimpleTable simpleTable("Currency", "Supported exchanges", "Exchange code(s)", "Alt code(s)", "Can deposit to", + "Can withdraw from", "Is fiat"); + // Compute all currencies for all exchanges + CurrencyCodeVector allCurrencyCodes; + + for (const auto &[_, currencies] : currenciesPerExchange) { + allCurrencyCodes.insert(allCurrencyCodes.end(), currencies.begin(), currencies.end()); + } + std::ranges::sort(allCurrencyCodes); + const auto [eraseIt1, eraseIt2] = std::ranges::unique(allCurrencyCodes); + allCurrencyCodes.erase(eraseIt1, eraseIt2); + + simpleTable.reserve(1U + allCurrencyCodes.size()); + + for (CurrencyCode cur : allCurrencyCodes) { + string supportedExchanges; + string exchangeCodes; + string altCodes; + string canDeposit; + string canWithdraw; + std::optional isFiat; + for (const auto &[exchange, currencies] : currenciesPerExchange) { + auto it = currencies.find(cur); + if (it != currencies.end()) { + // This exchange has this currency + Append(supportedExchanges, exchange->name()); + if (cur != it->exchangeCode()) { + AppendWithExchangeName(exchangeCodes, it->exchangeCode().str(), exchange->name()); + } + if (cur != it->altCode()) { + AppendWithExchangeName(altCodes, it->altCode().str(), exchange->name()); + } + if (it->canDeposit()) { + Append(canDeposit, exchange->name()); + } + if (it->canWithdraw()) { + Append(canWithdraw, exchange->name()); + } + if (!isFiat) { + isFiat = it->isFiat(); + } else if (*isFiat != it->isFiat()) { + log::error("Currency {} is fiat for an exchange and not fiat for another ('{}'), consider not fiat", cur, + exchange->name()); + isFiat = false; + } + } + } + + simpleTable.emplace_back(cur.str(), std::move(supportedExchanges), std::move(exchangeCodes), + std::move(altCodes), std::move(canDeposit), std::move(canWithdraw), isFiat.value()); + } + + printTable(simpleTable); + break; + } + case ApiOutputType::kJson: + printJson(jsonData); + break; + case ApiOutputType::kNoPrint: + break; + } + logActivity(CoincenterCommandType::kCurrencies, jsonData); +} + void QueryResultPrinter::printMarkets(CurrencyCode cur1, CurrencyCode cur2, const MarketsPerExchange &marketsPerExchange) const { json jsonData = MarketsJson(cur1, cur2, marketsPerExchange); diff --git a/src/engine/test/coincentercommandfactory_test.cpp b/src/engine/test/coincentercommandfactory_test.cpp index 5ce1fcd9..bf49925a 100644 --- a/src/engine/test/coincentercommandfactory_test.cpp +++ b/src/engine/test/coincentercommandfactory_test.cpp @@ -2,9 +2,12 @@ #include +#include + #include "cct_invalid_argument_exception.hpp" #include "coincentercommand.hpp" #include "coincentercommandtype.hpp" +#include "coincenteroptions.hpp" #include "currencycode.hpp" #include "exchangename.hpp" #include "monetaryamount.hpp" diff --git a/src/engine/test/queryresultprinter_public_test.cpp b/src/engine/test/queryresultprinter_public_test.cpp index e1464ad0..1203f591 100644 --- a/src/engine/test/queryresultprinter_public_test.cpp +++ b/src/engine/test/queryresultprinter_public_test.cpp @@ -4,6 +4,8 @@ #include "apioutputtype.hpp" #include "currencycode.hpp" +#include "currencyexchange.hpp" +#include "currencyexchangeflatset.hpp" #include "exchangepublicapitypes.hpp" #include "market.hpp" #include "marketorderbook.hpp" @@ -66,6 +68,154 @@ TEST_F(QueryResultPrinterHealthCheckTest, NoPrint) { expectNoStr(); } +class QueryResultPrinterCurrenciesTest : public QueryResultPrinterTest { + protected: + CurrencyExchange cur00{"AAVE", CurrencyExchange::Deposit::kAvailable, CurrencyExchange::Withdraw::kUnavailable, + CurrencyExchange::Type::kCrypto}; + CurrencyExchange cur01{"AAVE", CurrencyExchange::Deposit::kAvailable, CurrencyExchange::Withdraw::kAvailable, + CurrencyExchange::Type::kCrypto}; + CurrencyExchange cur02{"AAVE", CurrencyExchange::Deposit::kUnavailable, CurrencyExchange::Withdraw::kUnavailable, + CurrencyExchange::Type::kCrypto}; + + CurrencyExchange cur10{"BTC", + "XBT", + "BTC", + CurrencyExchange::Deposit::kAvailable, + CurrencyExchange::Withdraw::kAvailable, + CurrencyExchange::Type::kCrypto}; + CurrencyExchange cur11{"BTC", + "XBTC", + CurrencyCode{"BIT"}, + CurrencyExchange::Deposit::kAvailable, + CurrencyExchange::Withdraw::kUnavailable, + CurrencyExchange::Type::kCrypto}; + + CurrencyExchange cur20{"EUR", CurrencyExchange::Deposit::kAvailable, CurrencyExchange::Withdraw::kAvailable, + CurrencyExchange::Type::kFiat}; + CurrencyExchange cur21{"EUR", CurrencyExchange::Deposit::kUnavailable, CurrencyExchange::Withdraw::kUnavailable, + CurrencyExchange::Type::kFiat}; + + CurrenciesPerExchange currenciesPerExchange{ + {&exchange1, CurrencyExchangeFlatSet{{cur00, cur10}}}, + {&exchange2, CurrencyExchangeFlatSet{{cur01, cur10, cur21}}}, + {&exchange3, CurrencyExchangeFlatSet{{cur02, cur11, cur20}}}, + }; +}; + +TEST_F(QueryResultPrinterCurrenciesTest, FormattedTable) { + basicQueryResultPrinter(ApiOutputType::kFormattedTable).printCurrencies(currenciesPerExchange); + static constexpr std::string_view kExpected = R"( ++----------+-----------------------+---------------------------------------+-------------+-----------------------+-------------------+---------+ +| Currency | Supported exchanges | Exchange code(s) | Alt code(s) | Can deposit to | Can withdraw from | Is fiat | ++----------+-----------------------+---------------------------------------+-------------+-----------------------+-------------------+---------+ +| AAVE | binance,bithumb,huobi | | | binance,bithumb | bithumb | no | +| BTC | binance,bithumb,huobi | XBT[binance],XBT[bithumb],XBTC[huobi] | BIT[huobi] | binance,bithumb,huobi | binance,bithumb | no | +| EUR | bithumb,huobi | | | huobi | huobi | yes | ++----------+-----------------------+---------------------------------------+-------------+-----------------------+-------------------+---------+ +)"; + + expectStr(kExpected); +} + +TEST_F(QueryResultPrinterCurrenciesTest, EmptyJson) { + basicQueryResultPrinter(ApiOutputType::kJson).printCurrencies(CurrenciesPerExchange{}); + + static constexpr std::string_view kExpected = R"( +{ + "in": { + "opt": null, + "req": "Currencies" + }, + "out": {} +})"; + expectJson(kExpected); +} + +TEST_F(QueryResultPrinterCurrenciesTest, Json) { + basicQueryResultPrinter(ApiOutputType::kJson).printCurrencies(currenciesPerExchange); + + static constexpr std::string_view kExpected = R"( +{ + "in": { + "opt": null, + "req": "Currencies" + }, + "out": { + "binance": [ + { + "altCode": "AAVE", + "canDeposit": true, + "canWithdraw": false, + "code": "AAVE", + "exchangeCode": "AAVE", + "isFiat": false + }, + { + "altCode": "BTC", + "canDeposit": true, + "canWithdraw": true, + "code": "BTC", + "exchangeCode": "XBT", + "isFiat": false + } + ], + "bithumb": [ + { + "altCode": "AAVE", + "canDeposit": true, + "canWithdraw": true, + "code": "AAVE", + "exchangeCode": "AAVE", + "isFiat": false + }, + { + "altCode": "BTC", + "canDeposit": true, + "canWithdraw": true, + "code": "BTC", + "exchangeCode": "XBT", + "isFiat": false + }, + { + "altCode": "EUR", + "canDeposit": false, + "canWithdraw": false, + "code": "EUR", + "exchangeCode": "EUR", + "isFiat": true + } + ], + "huobi": [ + { + "altCode": "AAVE", + "canDeposit": false, + "canWithdraw": false, + "code": "AAVE", + "exchangeCode": "AAVE", + "isFiat": false + }, + { + "altCode": "BIT", + "canDeposit": true, + "canWithdraw": false, + "code": "BTC", + "exchangeCode": "XBTC", + "isFiat": false + }, + { + "altCode": "EUR", + "canDeposit": true, + "canWithdraw": true, + "code": "EUR", + "exchangeCode": "EUR", + "isFiat": true + } + ] + } +})"; + expectJson(kExpected); +} + class QueryResultPrinterMarketsTest : public QueryResultPrinterTest { protected: CurrencyCode cur1{"XRP"}; diff --git a/src/engine/test/transferablecommandresult_test.cpp b/src/engine/test/transferablecommandresult_test.cpp index 5792b7db..b850b7ed 100644 --- a/src/engine/test/transferablecommandresult_test.cpp +++ b/src/engine/test/transferablecommandresult_test.cpp @@ -6,6 +6,7 @@ #include "cct_exception.hpp" #include "coincentercommand.hpp" +#include "coincentercommandtype.hpp" #include "exchangename.hpp" #include "monetaryamount.hpp" diff --git a/src/main/src/main.cpp b/src/main/src/main.cpp index 21ee8aaa..3a754da3 100644 --- a/src/main/src/main.cpp +++ b/src/main/src/main.cpp @@ -5,6 +5,8 @@ #include "cct_invalid_argument_exception.hpp" #include "coincentercommands.hpp" +#include "coincenteroptions.hpp" +#include "coincenteroptionsdef.hpp" #include "commandlineoptionsparser.hpp" #include "parseoptions.hpp" #include "processcommandsfromcli.hpp" diff --git a/src/objects/include/coincentercommandtype.hpp b/src/objects/include/coincentercommandtype.hpp index 3a1bc996..c0d790ac 100644 --- a/src/objects/include/coincentercommandtype.hpp +++ b/src/objects/include/coincentercommandtype.hpp @@ -6,6 +6,7 @@ namespace cct { enum class CoincenterCommandType : int8_t { kHealthCheck, + kCurrencies, kMarkets, kConversionPath, kLastPrice, diff --git a/src/objects/include/currencyexchangeflatset.hpp b/src/objects/include/currencyexchangeflatset.hpp index 5d9d9b7b..7a4a90da 100644 --- a/src/objects/include/currencyexchangeflatset.hpp +++ b/src/objects/include/currencyexchangeflatset.hpp @@ -53,9 +53,8 @@ class CurrencyExchangeFlatSet { const_iterator find(CurrencyCode standardCode) const { // This is possible as CurrencyExchanges are ordered by standard code - const_iterator lbIt = - std::lower_bound(_set.begin(), _set.end(), standardCode, - [](const CurrencyExchange &lhs, CurrencyCode c) { return lhs.standardCode() < c; }); + const_iterator lbIt = std::ranges::lower_bound( + _set, standardCode, [](const CurrencyExchange &lhs, CurrencyCode c) { return lhs.standardCode() < c; }); return lbIt == end() || standardCode < lbIt->standardCode() ? end() : lbIt; } diff --git a/src/objects/src/coincentercommandtype.cpp b/src/objects/src/coincentercommandtype.cpp index f12c24d4..830c25b7 100644 --- a/src/objects/src/coincentercommandtype.cpp +++ b/src/objects/src/coincentercommandtype.cpp @@ -9,6 +9,8 @@ std::string_view CoincenterCommandTypeToString(CoincenterCommandType type) { switch (type) { case CoincenterCommandType::kHealthCheck: return "HealthCheck"; + case CoincenterCommandType::kCurrencies: + return "Currencies"; case CoincenterCommandType::kMarkets: return "Markets"; case CoincenterCommandType::kConversionPath: @@ -57,6 +59,9 @@ CoincenterCommandType CoincenterCommandTypeFromString(std::string_view str) { if (str == "HealthCheck") { return CoincenterCommandType::kHealthCheck; } + if (str == "Currencies") { + return CoincenterCommandType::kCurrencies; + } if (str == "Markets") { return CoincenterCommandType::kMarkets; } diff --git a/src/tech/include/simpletable.hpp b/src/tech/include/simpletable.hpp index 5bdf1a40..7a4faac1 100644 --- a/src/tech/include/simpletable.hpp +++ b/src/tech/include/simpletable.hpp @@ -1,5 +1,7 @@ #pragma once +#include +#include #include #include #include @@ -21,13 +23,13 @@ namespace cct { /// Simple, lightweight and fast table with dynamic number of columns. /// No checks are made about the number of columns for each Row, it's up to client's responsibility to make sure they /// match. +/// Multi line rows are *not* supported. class SimpleTable { public: class Row; /// Cell in a SimpleTable. - /// Can currently hold only 3 types of values: a string, a string_view or an integral type. - /// TODO: add support of 'double' if needed + /// Can currently hold only 4 types of values: a string, a string_view, a int64_t and a bool. class Cell { public: using IntegralType = int64_t; @@ -39,7 +41,7 @@ class SimpleTable { #else using string_type = string; #endif - using value_type = std::variant; + using value_type = std::variant; using size_type = uint32_t; explicit Cell(std::string_view v) : _data(v) {} @@ -54,7 +56,7 @@ class SimpleTable { explicit Cell(string_type &&v) : _data(std::move(v)) {} #endif - explicit Cell(IntegralType v) : _data(v) {} + explicit Cell(std::integral auto v) : _data(v) {} size_type size() const noexcept; @@ -62,9 +64,9 @@ class SimpleTable { using trivially_relocatable = is_trivially_relocatable::type; - bool operator==(const Cell &) const = default; + bool operator==(const Cell &) const noexcept = default; - auto operator<=>(const Cell &) const = default; + std::strong_ordering operator<=>(const Cell &) const noexcept = default; private: friend class Row; @@ -119,9 +121,9 @@ class SimpleTable { using trivially_relocatable = is_trivially_relocatable>::type; - bool operator==(const Row &) const = default; + bool operator==(const Row &) const noexcept = default; - auto operator<=>(const Row &) const = default; + std::strong_ordering operator<=>(const Row &) const noexcept = default; private: friend class SimpleTable; diff --git a/src/tech/src/simpletable.cpp b/src/tech/src/simpletable.cpp index 1c019041..a2e01030 100644 --- a/src/tech/src/simpletable.cpp +++ b/src/tech/src/simpletable.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include "mathhelpers.hpp" #include "unreachable.hpp" @@ -21,6 +22,9 @@ const SimpleTable::Row SimpleTable::Row::kDivider; namespace { constexpr char kColumnSep = '|'; +constexpr std::string_view kBoolValueTrue = "yes"; +constexpr std::string_view kBoolValueFalse = "no"; + enum class AlignTo : int8_t { kLeft, kRight }; /// Helper function to align to left or right @@ -39,33 +43,39 @@ class Align { } // namespace SimpleTable::size_type SimpleTable::Cell::size() const noexcept { - switch (_data.index()) { - case 0: - return static_cast(std::get(_data).size()); - case 1: - return static_cast(std::get(_data).size()); - case 2: - return static_cast(nchars(std::get(_data))); - default: - unreachable(); - } + return std::visit( + [](auto &&v) -> size_type { + using T = std::decay_t; + if constexpr (std::is_same_v || std::is_same_v) { + return v.size(); + } else if constexpr (std::is_same_v) { + return v ? kBoolValueTrue.size() : kBoolValueFalse.size(); + } else if constexpr (std::is_integral_v) { + return nchars(v); + } else { + unreachable(); + } + }, + _data); } void SimpleTable::Cell::print(std::ostream &os, size_type maxCellWidth) const { os << ' ' << Align(AlignTo::kLeft) << std::setw(maxCellWidth); - switch (_data.index()) { - case 0: - os << std::get(_data); - break; - case 1: - os << std::get(_data); - break; - case 2: - os << std::get(_data); - break; - default: - unreachable(); - } + + std::visit( + [&os](auto &&v) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + os << (v ? kBoolValueTrue : kBoolValueFalse); + } else if constexpr (std::is_same_v || std::is_same_v || + std::is_integral_v) { + os << v; + } else { + unreachable(); + } + }, + _data); + os << ' ' << kColumnSep; } diff --git a/src/tech/test/simpletable_test.cpp b/src/tech/test/simpletable_test.cpp index a5fd898e..fcba5abe 100644 --- a/src/tech/test/simpletable_test.cpp +++ b/src/tech/test/simpletable_test.cpp @@ -2,7 +2,7 @@ #include -#include +#include #include #include "cct_string.hpp" @@ -12,7 +12,9 @@ TEST(SimpleTable, DefaultConstructor) { SimpleTable table; EXPECT_TRUE(table.empty()); - std::cout << table; + std::ostringstream ss; + ss << table; + EXPECT_TRUE(ss.view().empty()); } TEST(SimpleTable, OneLinePrint) { @@ -21,7 +23,15 @@ TEST(SimpleTable, OneLinePrint) { table.emplace_back("Header 1", 42, std::move(str)); EXPECT_EQ(table.size(), 1U); - std::cout << table; + std::ostringstream ss; + + ss << '\n' << table; + static constexpr std::string_view kExpected = R"( ++----------+----+---------------+ +| Header 1 | 42 | I am a string | ++----------+----+---------------+)"; + + EXPECT_EQ(ss.view(), kExpected); } TEST(SimpleTable, SimplePrint) { @@ -29,30 +39,66 @@ TEST(SimpleTable, SimplePrint) { SimpleTable::Row row1; row1.emplace_back("Amount"); row1.emplace_back("Currency"); + row1.emplace_back("Is Fiat"); table.push_back(std::move(row1)); SimpleTable::Row row2; row2.emplace_back("123.45"); row2.emplace_back("EUR"); + row2.emplace_back(true); table.push_back(std::move(row2)); SimpleTable::Row row3; row3.emplace_back(65); row3.emplace_back("BTC"); + row3.emplace_back(false); table.push_back(std::move(row3)); EXPECT_EQ(table.size(), 3U); - std::cout << table; + std::ostringstream ss; + + ss << '\n' << table; + static constexpr std::string_view kExpected = R"( ++--------+----------+---------+ +| Amount | Currency | Is Fiat | ++--------+----------+---------+ +| 123.45 | EUR | yes | +| 65 | BTC | no | ++--------+----------+---------+)"; + + EXPECT_EQ(ss.view(), kExpected); } -TEST(SimpleTable, SettingRowDirectly) { - SimpleTable table("Amount", "Currency", "This header is long and stupid"); +class SimpleTableTest : public ::testing::Test { + protected: + void fill() { + table.emplace_back(1235, "EUR", "Nothing here"); + table.emplace_back("3456.78", "USD", 42); + table.emplace_back("-677234.67", "SUSHI", -12); + table.emplace_back(-677256340000, "KEBAB", "-34.09"); + } + + SimpleTable table{"Amount", "Currency", "This header is longer"}; +}; + +TEST_F(SimpleTableTest, SettingRowDirectly) { EXPECT_EQ(table.size(), 1U); - table.emplace_back(1235, "EUR", "Nothing here"); - table.emplace_back("3456.78", "USD", 42); - table.emplace_back("-677234.67", "SUSHI", -12); - table.emplace_back(-677256340000, "KEBAB", "-34.09"); + fill(); EXPECT_EQ(table[2].front().size(), 7U); EXPECT_EQ(table.back().front().size(), 13U); - std::cout << table; + std::ostringstream ss; + + ss << '\n' << table; + static constexpr std::string_view kExpected = R"( ++---------------+----------+-----------------------+ +| Amount | Currency | This header is longer | ++---------------+----------+-----------------------+ +| 1235 | EUR | Nothing here | +| 3456.78 | USD | 42 | +| -677234.67 | SUSHI | -12 | +| -677256340000 | KEBAB | -34.09 | ++---------------+----------+-----------------------+)"; + + EXPECT_EQ(ss.view(), kExpected); } + } // namespace cct \ No newline at end of file