Skip to content

Commit

Permalink
Make first currency optional for markets command
Browse files Browse the repository at this point in the history
  • Loading branch information
sjanel committed Nov 23, 2023
1 parent c61458d commit afc1f21
Show file tree
Hide file tree
Showing 12 changed files with 190 additions and 35 deletions.
22 changes: 16 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

Expand All @@ -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
```

Expand Down
3 changes: 2 additions & 1 deletion src/engine/include/coincenter.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion src/engine/include/coincenteroptions.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class CoincenterCmdLineOptions {
std::string_view monitoringPassword;

std::optional<std::string_view> currencies;
std::string_view markets;
std::optional<std::string_view> markets;

std::string_view orderbook;
std::string_view orderbookCur;
Expand Down
4 changes: 2 additions & 2 deletions src/engine/include/coincenteroptionsdef.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -181,10 +181,10 @@ struct CoincenterAllowedOptions : private CoincenterCmdLineOptionsDefinitions {
&OptValueType::currencies},
{{{"Public queries", 2100},
"markets",
"<cur1[-cur2][,exch1,...]>",
"<[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},
Expand Down
2 changes: 1 addition & 1 deletion src/engine/src/coincentercommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/engine/src/coincentercommandfactory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
4 changes: 2 additions & 2 deletions src/engine/src/coincentercommands.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

Expand Down
20 changes: 13 additions & 7 deletions src/engine/src/exchangesorchestrator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
13 changes: 9 additions & 4 deletions src/engine/src/queryresultprinter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,10 @@ json CurrenciesJson(const CurrenciesPerExchange &currenciesPerExchange) {

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());
}
Expand Down Expand Up @@ -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);
Expand Down
23 changes: 21 additions & 2 deletions src/engine/test/coincentercommandfactory_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
22 changes: 22 additions & 0 deletions src/engine/test/exchangesorchestrator_public_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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{};
Expand Down
108 changes: 100 additions & 8 deletions src/engine/test/queryresultprinter_public_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -219,28 +219,65 @@ 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 |
+----------+------------------+
)";

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": {
Expand All @@ -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": {
Expand All @@ -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"
]
Expand All @@ -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();
}

Expand Down

0 comments on commit afc1f21

Please sign in to comment.