diff --git a/README.md b/README.md index 397451bb..cac853f7 100644 --- a/README.md +++ b/README.md @@ -267,24 +267,34 @@ 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. -At least one currency is mandatory, but the list of exchanges is not. If no exchanges are provided, `coincenter` will simply query all supported exchanges and list the markets involving the given currencies if they exist. +Use the `markets` command to list markets. This is useful to check how you can trade your coins. +It takes an optional combination of a maximum of two currencies: -One or two (in this case, querying existence of a market) currencies can be given, separated by a `-`. +- if none is specified, all markets are returned +- if only one is specified, all markets trading the currency will be returned +- if two currencies are specified (should be separated with `-`), only exchanges listing given market will be returned + +Also, result can be narrowed to list of exchanges given after the optional currencies. If no exchanges are provided, `coincenter` will simply query all supported exchanges and list the markets involving the given currencies if they exist. **Note**: Markets are returned with the currency pair presented in original order from the exchange, as it could give additional information for services relying on this option (even though it's not needed for `trade` option of `coincenter`) #### Examples -List all markets involving Ethereum in Huobi +Lists all markets for all exchanges +```bash +coincenter markets ``` + +List all markets involving Ethereum in Huobi + +```bash coincenter markets eth,huobi ``` List exchanges where the pair AVAX-USDT is supported -``` +```bash coincenter markets avax-usdt ``` @@ -295,7 +305,7 @@ List of exchanges should be given as lower case, comma separated. But it is opti Example: Print ticker information for kraken and huobi exchanges -``` +```bash coincenter ticker kraken,huobi ``` diff --git a/src/engine/include/coincenter.hpp b/src/engine/include/coincenter.hpp index 682eaf7d..f4357d81 100644 --- a/src/engine/include/coincenter.hpp +++ b/src/engine/include/coincenter.hpp @@ -36,7 +36,8 @@ class Coincenter { /// 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. + /// Retrieve the markets for given selected public exchanges (or all if empty span) matching given currencies. + /// Currencies are both optional and may be neutral. A market matches any neutral currency. MarketsPerExchange getMarketsPerExchange(CurrencyCode cur1, CurrencyCode cur2, ExchangeNameSpan exchangeNames); /// Retrieve ticker information for given selected public exchanges, or all if empty. diff --git a/src/engine/include/coincenteroptions.hpp b/src/engine/include/coincenteroptions.hpp index 3bb608c0..2c14e54c 100644 --- a/src/engine/include/coincenteroptions.hpp +++ b/src/engine/include/coincenteroptions.hpp @@ -45,7 +45,7 @@ class CoincenterCmdLineOptions { std::string_view monitoringPassword; std::optional currencies; - std::string_view markets; + std::optional markets; std::string_view orderbook; std::string_view orderbookCur; diff --git a/src/engine/include/coincenteroptionsdef.hpp b/src/engine/include/coincenteroptionsdef.hpp index 61c9f183..e619a2e5 100644 --- a/src/engine/include/coincenteroptionsdef.hpp +++ b/src/engine/include/coincenteroptionsdef.hpp @@ -181,10 +181,10 @@ struct CoincenterAllowedOptions : private CoincenterCmdLineOptionsDefinitions { &OptValueType::currencies}, {{{"Public queries", 2100}, "markets", - "", + "<[cur1-cur2][,exch1,...]>", "Print markets involving given currencies for all exchanges, " "or only the specified ones. " - "Either a single currency or a full market can be specified."}, + "Currencies are optional, all markets will be returned if none given."}, &OptValueType::markets}, {{{"Public queries", 2200}, diff --git a/src/engine/src/coincentercommand.cpp b/src/engine/src/coincentercommand.cpp index 5732e6a9..6b3a9ac9 100644 --- a/src/engine/src/coincentercommand.cpp +++ b/src/engine/src/coincentercommand.cpp @@ -174,7 +174,7 @@ CoincenterCommand& CoincenterCommand::setCur1(CurrencyCode cur1) { } CoincenterCommand& CoincenterCommand::setCur2(CurrencyCode cur2) { - if (_cur1.isNeutral()) { + if (_cur1.isNeutral() && !cur2.isNeutral()) { throw exception("First currency should be set before second one"); } _cur2 = cur2; diff --git a/src/engine/src/coincentercommandfactory.cpp b/src/engine/src/coincentercommandfactory.cpp index 7120b364..fbbf9997 100644 --- a/src/engine/src/coincentercommandfactory.cpp +++ b/src/engine/src/coincentercommandfactory.cpp @@ -19,7 +19,7 @@ namespace cct { CoincenterCommand CoincenterCommandFactory::CreateMarketCommand(StringOptionParser &optionParser) { auto market = optionParser.parseMarket(StringOptionParser::FieldIs::kOptional); if (market.isNeutral()) { - market = Market(optionParser.parseCurrency(), CurrencyCode()); + market = Market(optionParser.parseCurrency(StringOptionParser::FieldIs::kOptional), CurrencyCode()); } CoincenterCommand ret(CoincenterCommandType::kMarkets); ret.setCur1(market.base()).setCur2(market.quote()).setExchangeNames(optionParser.parseExchanges()); diff --git a/src/engine/src/coincentercommands.cpp b/src/engine/src/coincentercommands.cpp index 2f3327fe..0f35294f 100644 --- a/src/engine/src/coincentercommands.cpp +++ b/src/engine/src/coincentercommands.cpp @@ -56,8 +56,8 @@ void CoincenterCommands::addOption(const CoincenterCmdLineOptions &cmdLineOption _commands.emplace_back(CoincenterCommandType::kCurrencies).setExchangeNames(optionParser.parseExchanges()); } - if (!cmdLineOptions.markets.empty()) { - optionParser = StringOptionParser(cmdLineOptions.markets); + if (cmdLineOptions.markets) { + 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 c59dd14a..f9db1f79 100644 --- a/src/engine/src/exchangesorchestrator.cpp +++ b/src/engine/src/exchangesorchestrator.cpp @@ -305,19 +305,25 @@ CurrenciesPerExchange ExchangesOrchestrator::getCurrenciesPerExchange(ExchangeNa MarketsPerExchange ExchangesOrchestrator::getMarketsPerExchange(CurrencyCode cur1, CurrencyCode cur2, ExchangeNameSpan exchangeNames) { - string curStr = cur1.str(); - if (!cur2.isNeutral()) { - curStr.push_back('-'); - cur2.appendStrTo(curStr); + string curStr; + if (!cur1.isNeutral()) { + curStr.append(" matching "); + cur1.appendStrTo(curStr); + if (!cur2.isNeutral()) { + curStr.push_back('-'); + cur2.appendStrTo(curStr); + } } - log::info("Query markets with {} from {}", curStr, ConstructAccumulatedExchangeNames(exchangeNames)); + + log::info("Query markets{} from {}", curStr, ConstructAccumulatedExchangeNames(exchangeNames)); UniquePublicSelectedExchanges selectedExchanges = _exchangeRetriever.selectOneAccount(exchangeNames); MarketsPerExchange marketsPerExchange(selectedExchanges.size()); auto marketsWithCur = [cur1, cur2](Exchange *exchange) { MarketSet markets = exchange->queryTradableMarkets(); MarketSet ret; - std::copy_if(markets.begin(), markets.end(), std::inserter(ret, ret.end()), - [cur1, cur2](Market mk) { return mk.canTrade(cur1) && (cur2.isNeutral() || mk.canTrade(cur2)); }); + std::copy_if(markets.begin(), markets.end(), std::inserter(ret, ret.end()), [cur1, cur2](Market mk) { + return (cur1.isNeutral() || mk.canTrade(cur1)) && (cur2.isNeutral() || mk.canTrade(cur2)); + }); return std::make_pair(exchange, std::move(ret)); }; _threadPool.parallelTransform(selectedExchanges.begin(), selectedExchanges.end(), marketsPerExchange.begin(), diff --git a/src/engine/src/queryresultprinter.cpp b/src/engine/src/queryresultprinter.cpp index 45a81dc8..beaaee11 100644 --- a/src/engine/src/queryresultprinter.cpp +++ b/src/engine/src/queryresultprinter.cpp @@ -100,8 +100,10 @@ json CurrenciesJson(const CurrenciesPerExchange ¤ciesPerExchange) { json MarketsJson(CurrencyCode cur1, CurrencyCode cur2, const MarketsPerExchange &marketsPerExchange) { json in; - json inOpt; - inOpt.emplace("cur1", cur1.str()); + json inOpt = json::object(); + if (!cur1.isNeutral()) { + inOpt.emplace("cur1", cur1.str()); + } if (!cur2.isNeutral()) { inOpt.emplace("cur2", cur2.str()); } @@ -762,8 +764,11 @@ void QueryResultPrinter::printMarkets(CurrencyCode cur1, CurrencyCode cur2, json jsonData = MarketsJson(cur1, cur2, marketsPerExchange); switch (_apiOutputType) { case ApiOutputType::kFormattedTable: { - string marketsCol("Markets with "); - cur1.appendStrTo(marketsCol); + string marketsCol("Markets"); + if (!cur1.isNeutral()) { + marketsCol.append(" with "); + cur1.appendStrTo(marketsCol); + } if (!cur2.isNeutral()) { marketsCol.push_back('-'); cur2.appendStrTo(marketsCol); diff --git a/src/engine/test/coincentercommandfactory_test.cpp b/src/engine/test/coincentercommandfactory_test.cpp index bf49925a..79ab238a 100644 --- a/src/engine/test/coincentercommandfactory_test.cpp +++ b/src/engine/test/coincentercommandfactory_test.cpp @@ -29,8 +29,27 @@ class CoincenterCommandFactoryTest : public ::testing::Test { CoincenterCommandFactory commandFactory{cmdLineOptions, pPreviousCommand}; }; -TEST_F(CoincenterCommandFactoryTest, CreateMarketCommandInvalidInputTest) { - EXPECT_THROW(CoincenterCommandFactory::CreateMarketCommand(inputStr("kucoin")), invalid_argument); +TEST_F(CoincenterCommandFactoryTest, CreateMarketCommandEmpty) { + EXPECT_EQ(CoincenterCommandFactory::CreateMarketCommand(inputStr("")), + CoincenterCommand(CoincenterCommandType::kMarkets)); + EXPECT_NO_THROW(optionParser.checkEndParsing()); +} + +TEST_F(CoincenterCommandFactoryTest, CreateMarketCommandInvalidWrongOrder) { + CoincenterCommandFactory::CreateMarketCommand(inputStr("huobi_user2,eth")); + EXPECT_THROW(optionParser.checkEndParsing(), invalid_argument); +} + +TEST_F(CoincenterCommandFactoryTest, CreateMarketCommandInvalidWrongCurrencySeparator) { + CoincenterCommandFactory::CreateMarketCommand(inputStr("eth,btc")); + EXPECT_THROW(optionParser.checkEndParsing(), invalid_argument); +} + +TEST_F(CoincenterCommandFactoryTest, CreateMarketCommandSingleExchange) { + EXPECT_EQ(CoincenterCommandFactory::CreateMarketCommand(inputStr("huobi_user2")), + CoincenterCommand(CoincenterCommandType::kMarkets) + .setExchangeNames(ExchangeNames({ExchangeName("huobi_user2")}))); + EXPECT_NO_THROW(optionParser.checkEndParsing()); } TEST_F(CoincenterCommandFactoryTest, CreateMarketCommandMarketTest) { diff --git a/src/engine/test/exchangesorchestrator_public_test.cpp b/src/engine/test/exchangesorchestrator_public_test.cpp index 89cee325..0bc06b7e 100644 --- a/src/engine/test/exchangesorchestrator_public_test.cpp +++ b/src/engine/test/exchangesorchestrator_public_test.cpp @@ -111,6 +111,28 @@ TEST_F(ExchangeOrchestratorEmptyMarketOrderbookTest, MarketDoesNotExist) { marketOrderBookConversionRates); } +TEST_F(ExchangeOrchestratorTest, GetMarketsPerExchangeNoCurrency) { + CurrencyCode cur1{}; + CurrencyCode cur2{}; + ExchangeNameSpan exchangeNameSpan{}; + + Market m4{"LUNA", "BTC"}; + Market m5{"SHIB", "LUNA"}; + Market m6{"DOGE", "EUR"}; + + MarketSet markets1{m1, m2, m4, m6}; + EXPECT_CALL(exchangePublic1, queryTradableMarkets()).WillOnce(testing::Return(markets1)); + MarketSet markets2{m1, m2, m3, m4, m5, m6}; + EXPECT_CALL(exchangePublic2, queryTradableMarkets()).WillOnce(testing::Return(markets2)); + MarketSet markets3{m1, m2, m6}; + EXPECT_CALL(exchangePublic3, queryTradableMarkets()).WillOnce(testing::Return(markets3)); + + MarketsPerExchange ret{{&exchange1, MarketSet{m1, m2, m4, m6}}, + {&exchange2, MarketSet{m1, m2, m3, m4, m5, m6}}, + {&exchange3, MarketSet{m1, m2, m6}}}; + EXPECT_EQ(exchangesOrchestrator.getMarketsPerExchange(cur1, cur2, exchangeNameSpan), ret); +} + TEST_F(ExchangeOrchestratorTest, GetMarketsPerExchangeOneCurrency) { CurrencyCode cur1{"LUNA"}; CurrencyCode cur2{}; diff --git a/src/engine/test/queryresultprinter_public_test.cpp b/src/engine/test/queryresultprinter_public_test.cpp index 1203f591..c804b733 100644 --- a/src/engine/test/queryresultprinter_public_test.cpp +++ b/src/engine/test/queryresultprinter_public_test.cpp @@ -219,19 +219,40 @@ TEST_F(QueryResultPrinterCurrenciesTest, Json) { class QueryResultPrinterMarketsTest : public QueryResultPrinterTest { protected: CurrencyCode cur1{"XRP"}; - CurrencyCode cur2; - MarketsPerExchange marketsPerExchange{{&exchange1, MarketSet{Market{cur1, "KRW"}, Market{cur1, "BTC"}}}, + CurrencyCode cur2{"BTC"}; + MarketsPerExchange marketsPerExchange{{&exchange1, MarketSet{Market{cur1, "KRW"}, Market{cur1, cur2}}}, + {&exchange2, MarketSet{Market{"SOL", "ETH"}}}, {&exchange3, MarketSet{Market{cur1, "EUR"}}}}; }; -TEST_F(QueryResultPrinterMarketsTest, FormattedTable) { - basicQueryResultPrinter(ApiOutputType::kFormattedTable).printMarkets(cur1, cur2, marketsPerExchange); +TEST_F(QueryResultPrinterMarketsTest, FormattedTableNoCurrency) { + basicQueryResultPrinter(ApiOutputType::kFormattedTable) + .printMarkets(CurrencyCode(), CurrencyCode(), marketsPerExchange); + static constexpr std::string_view kExpected = R"( ++----------+---------+ +| Exchange | Markets | ++----------+---------+ +| binance | XRP-BTC | +| binance | XRP-KRW | +| bithumb | SOL-ETH | +| huobi | XRP-EUR | ++----------+---------+ +)"; + + expectStr(kExpected); +} + +TEST_F(QueryResultPrinterMarketsTest, FormattedTableOneCurrency) { + basicQueryResultPrinter(ApiOutputType::kFormattedTable).printMarkets(cur1, CurrencyCode(), marketsPerExchange); + // We only test the title line here, it's normal that all markets are printed (they come from marketsPerExchange and + // are not filtered again inside the print function) static constexpr std::string_view kExpected = R"( +----------+------------------+ | Exchange | Markets with XRP | +----------+------------------+ | binance | XRP-BTC | | binance | XRP-KRW | +| bithumb | SOL-ETH | | huobi | XRP-EUR | +----------+------------------+ )"; @@ -239,8 +260,24 @@ TEST_F(QueryResultPrinterMarketsTest, FormattedTable) { expectStr(kExpected); } +TEST_F(QueryResultPrinterMarketsTest, FormattedTableTwoCurrencies) { + basicQueryResultPrinter(ApiOutputType::kFormattedTable).printMarkets(cur1, cur2, marketsPerExchange); + static constexpr std::string_view kExpected = R"( ++----------+----------------------+ +| Exchange | Markets with XRP-BTC | ++----------+----------------------+ +| binance | XRP-BTC | +| binance | XRP-KRW | +| bithumb | SOL-ETH | +| huobi | XRP-EUR | ++----------+----------------------+ +)"; + + expectStr(kExpected); +} + TEST_F(QueryResultPrinterMarketsTest, EmptyJson) { - basicQueryResultPrinter(ApiOutputType::kJson).printMarkets(cur1, cur2, MarketsPerExchange{}); + basicQueryResultPrinter(ApiOutputType::kJson).printMarkets(cur1, CurrencyCode(), MarketsPerExchange{}); static constexpr std::string_view kExpected = R"( { "in": { @@ -254,8 +291,33 @@ TEST_F(QueryResultPrinterMarketsTest, EmptyJson) { expectJson(kExpected); } -TEST_F(QueryResultPrinterMarketsTest, Json) { - basicQueryResultPrinter(ApiOutputType::kJson).printMarkets(cur1, cur2, marketsPerExchange); +TEST_F(QueryResultPrinterMarketsTest, JsonNoCurrency) { + basicQueryResultPrinter(ApiOutputType::kJson).printMarkets(CurrencyCode(), CurrencyCode(), marketsPerExchange); + static constexpr std::string_view kExpected = R"( +{ + "in": { + "opt": { + }, + "req": "Markets" + }, + "out": { + "binance": [ + "XRP-BTC", + "XRP-KRW" + ], + "bithumb": [ + "SOL-ETH" + ], + "huobi": [ + "XRP-EUR" + ] + } +})"; + expectJson(kExpected); +} + +TEST_F(QueryResultPrinterMarketsTest, JsonOneCurrency) { + basicQueryResultPrinter(ApiOutputType::kJson).printMarkets(cur1, CurrencyCode(), marketsPerExchange); static constexpr std::string_view kExpected = R"( { "in": { @@ -269,6 +331,36 @@ TEST_F(QueryResultPrinterMarketsTest, Json) { "XRP-BTC", "XRP-KRW" ], + "bithumb": [ + "SOL-ETH" + ], + "huobi": [ + "XRP-EUR" + ] + } +})"; + expectJson(kExpected); +} + +TEST_F(QueryResultPrinterMarketsTest, JsonTwoCurrencies) { + basicQueryResultPrinter(ApiOutputType::kJson).printMarkets(cur1, cur2, marketsPerExchange); + static constexpr std::string_view kExpected = R"( +{ + "in": { + "opt": { + "cur1": "XRP", + "cur2": "BTC" + }, + "req": "Markets" + }, + "out": { + "binance": [ + "XRP-BTC", + "XRP-KRW" + ], + "bithumb": [ + "SOL-ETH" + ], "huobi": [ "XRP-EUR" ] @@ -278,7 +370,7 @@ TEST_F(QueryResultPrinterMarketsTest, Json) { } TEST_F(QueryResultPrinterMarketsTest, NoPrint) { - basicQueryResultPrinter(ApiOutputType::kNoPrint).printMarkets(cur1, cur2, marketsPerExchange); + basicQueryResultPrinter(ApiOutputType::kNoPrint).printMarkets(cur1, CurrencyCode(), marketsPerExchange); expectNoStr(); }