Skip to content

Commit

Permalink
New CLI option - currencies, to get all tradable currencies
Browse files Browse the repository at this point in the history
  • Loading branch information
sjanel committed Nov 20, 2023
1 parent 7f2bcbd commit c61458d
Show file tree
Hide file tree
Showing 25 changed files with 461 additions and 59 deletions.
29 changes: 25 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ Main features:

**Market Data**

- Market
- Currencies
- Markets
- Ticker
- Orderbook
- Traded volume
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions data/static/currencyacronymtranslator.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
"XLTC": "LTC",
"XXLM": "XLM",
"XMLN": "MLN",
"XXDG": "XDG",
"DOGE": "XDG",
"XDG": "DOGE",
"XXDG": "DOGE",
"XZEC": "ZEC",
"ZEUR": "EUR",
"ZCAD": "CAD",
Expand Down
9 changes: 3 additions & 6 deletions src/api/exchanges/test/commonapi_test.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
}
}
}
Expand Down Expand Up @@ -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);
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions src/engine/include/coincenter.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
1 change: 1 addition & 0 deletions src/engine/include/coincenteroptions.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ class CoincenterCmdLineOptions {
std::string_view monitoringUsername;
std::string_view monitoringPassword;

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

std::string_view orderbook;
Expand Down
6 changes: 6 additions & 0 deletions src/engine/include/coincenteroptionsdef.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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",
"<cur1[-cur2][,exch1,...]>",
Expand Down
6 changes: 4 additions & 2 deletions src/engine/include/commandlineoptionsparser.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<decltype(minDistance)>(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);
}
Expand Down
2 changes: 2 additions & 0 deletions src/engine/include/exchangesorchestrator.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions src/engine/include/queryresultprinter.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ class QueryResultPrinter {

void printHealthCheck(const ExchangeHealthCheckStatus &healthCheckPerExchange) const;

void printCurrencies(const CurrenciesPerExchange &currenciesPerExchange) const;

void printMarkets(CurrencyCode cur1, CurrencyCode cur2, const MarketsPerExchange &marketsPerExchange) const;

void printMarketOrderBooks(Market mk, CurrencyCode equiCurrencyCode, std::optional<int> depth,
Expand Down
3 changes: 3 additions & 0 deletions src/engine/include/queryresulttypes.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -43,6 +44,8 @@ using ExchangeHealthCheckStatus = FixedCapacityVector<ExchangeWith<bool>, kNbSup

using ExchangeTickerMaps = FixedCapacityVector<ExchangeWith<MarketOrderBookMap>, kNbSupportedExchanges>;

using CurrenciesPerExchange = FixedCapacityVector<ExchangeWith<CurrencyExchangeFlatSet>, kNbSupportedExchanges>;

using BalancePerExchange = SmallVector<std::pair<Exchange *, BalancePortfolio>, kTypicalNbPrivateAccounts>;

using WalletPerExchange = SmallVector<ExchangeWith<Wallet>, kTypicalNbPrivateAccounts>;
Expand Down
9 changes: 9 additions & 0 deletions src/engine/src/coincenter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions src/engine/src/coincentercommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
5 changes: 5 additions & 0 deletions src/engine/src/coincentercommands.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
13 changes: 13 additions & 0 deletions src/engine/src/exchangesorchestrator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
117 changes: 117 additions & 0 deletions src/engine/src/queryresultprinter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -68,6 +70,34 @@ json HealthCheckJson(const ExchangeHealthCheckStatus &healthCheckPerExchange) {
return ToJson(CoincenterCommandType::kHealthCheck, std::move(in), std::move(out));
}

json CurrenciesJson(const CurrenciesPerExchange &currenciesPerExchange) {
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;
Expand Down Expand Up @@ -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 &currenciesPerExchange) 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<bool> 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);
Expand Down
3 changes: 3 additions & 0 deletions src/engine/test/coincentercommandfactory_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@

#include <gtest/gtest.h>

#include <string_view>

#include "cct_invalid_argument_exception.hpp"
#include "coincentercommand.hpp"
#include "coincentercommandtype.hpp"
#include "coincenteroptions.hpp"
#include "currencycode.hpp"
#include "exchangename.hpp"
#include "monetaryamount.hpp"
Expand Down
Loading

0 comments on commit c61458d

Please sign in to comment.