From 8714b3dd99a0055ea4ade9b1a3358b6c639747e4 Mon Sep 17 00:00:00 2001 From: Stephane Janel Date: Thu, 28 Mar 2024 13:40:55 +0100 Subject: [PATCH] Refactor BalancePortfolio --- src/api/common/include/exchangeprivateapi.hpp | 10 +-- src/api/common/src/exchangeprivateapi.cpp | 52 ++++++------- .../common/test/exchangeprivateapi_test.cpp | 4 +- src/api/exchanges/src/binanceprivateapi.cpp | 19 +++-- src/api/exchanges/src/bithumbprivateapi.cpp | 5 +- src/api/exchanges/src/huobiprivateapi.cpp | 3 +- src/api/exchanges/src/krakenprivateapi.cpp | 5 +- src/api/exchanges/src/kucoinprivateapi.cpp | 7 +- src/api/exchanges/src/upbitprivateapi.cpp | 22 +++--- .../src/balanceperexchangeportfolio.cpp | 4 +- src/engine/src/metricsexporter.cpp | 10 +-- .../test/queryresultprinter_private_test.cpp | 42 +++++++++-- src/objects/include/balanceportfolio.hpp | 43 ++++++----- src/objects/src/balanceportfolio.cpp | 74 +++++++++---------- src/objects/test/balanceportfolio_test.cpp | 37 +++++----- 15 files changed, 185 insertions(+), 152 deletions(-) diff --git a/src/api/common/include/exchangeprivateapi.hpp b/src/api/common/include/exchangeprivateapi.hpp index 9ed3cf3d..038aa420 100644 --- a/src/api/common/include/exchangeprivateapi.hpp +++ b/src/api/common/include/exchangeprivateapi.hpp @@ -132,12 +132,8 @@ class ExchangePrivate : public ExchangeBase { virtual BalancePortfolio queryAccountBalance(const BalanceOptions &balanceOptions = BalanceOptions()) = 0; - /// Adds an amount to given BalancePortfolio. - /// @param equiCurrency Asks conversion of given amount into this currency as well - void addBalance(BalancePortfolio &balancePortfolio, MonetaryAmount amount, CurrencyCode equiCurrency); - - /// Return true if exchange supports simulated order (some exchanges such as Kraken or Binance for instance support - /// this query parameter) + /// Return true if exchange supports simulated order (some exchanges such as Kraken or Binance for instance + /// support this query parameter) virtual bool isSimulatedOrderSupported() const = 0; /// Place an order in mode fire and forget. @@ -190,6 +186,8 @@ class ExchangePrivate : public ExchangeBase { /// Check if withdraw has been confirmed and successful from 'this' exchange SentWithdrawInfo isWithdrawSuccessfullySent(const InitiatedWithdrawInfo &initiatedWithdrawInfo); + + void computeEquiCurrencyAmounts(BalancePortfolio &balancePortfolio, CurrencyCode equiCurrency); }; } // namespace api } // namespace cct diff --git a/src/api/common/src/exchangeprivateapi.cpp b/src/api/common/src/exchangeprivateapi.cpp index 37319b1f..e463acfb 100644 --- a/src/api/common/src/exchangeprivateapi.cpp +++ b/src/api/common/src/exchangeprivateapi.cpp @@ -55,40 +55,31 @@ ExchangePrivate::ExchangePrivate(const CoincenterInfo &coincenterInfo, ExchangeP : _exchangePublic(exchangePublic), _coincenterInfo(coincenterInfo), _apiKey(apiKey) {} BalancePortfolio ExchangePrivate::getAccountBalance(const BalanceOptions &balanceOptions) { - CacheFreezerRAII uniqueQueryHandle; - if (balanceOptions.equiCurrency().isDefined()) { - // In case of balance with equivalent currencies, for each exchange these requests will be made: - // - Markets - // - Ticker - // To avoid making several ticker queries (especially) for each exchange during the whole balance process, - // we "allow" only one query per argument in the cache of results of all exchanges. - // The freeze will be lifted automatically (as the object is RAII) at the end of this function. - uniqueQueryHandle = CacheFreezerRAII(_cachedResultVault); - } BalancePortfolio balancePortfolio = queryAccountBalance(balanceOptions); + + const auto equiCurrency = balanceOptions.equiCurrency(); + if (equiCurrency.isDefined()) { + computeEquiCurrencyAmounts(balancePortfolio, equiCurrency); + } + log::info("Retrieved {} balance for {} assets", exchangeName(), balancePortfolio.size()); + return balancePortfolio; } -void ExchangePrivate::addBalance(BalancePortfolio &balancePortfolio, MonetaryAmount amount, CurrencyCode equiCurrency) { - if (amount != 0) { - ExchangeName exchangeName = this->exchangeName(); - if (equiCurrency.isNeutral()) { - log::trace("{} balance {}", exchangeName, amount); - balancePortfolio.add(amount); - } else { - std::optional optConvertedAmountEquiCurrency = - _exchangePublic.estimatedConvert(amount, equiCurrency); - MonetaryAmount equivalentInMainCurrency; - if (optConvertedAmountEquiCurrency) { - equivalentInMainCurrency = *optConvertedAmountEquiCurrency; - } else { - log::debug("No {} -> {} conversion possible on {}", amount.currencyStr(), equiCurrency, exchangeName); - equivalentInMainCurrency = MonetaryAmount(0, equiCurrency); - } - log::trace("{} Balance {} (eq. {})", exchangeName, amount, equivalentInMainCurrency); - balancePortfolio.add(amount, equivalentInMainCurrency); - } +void ExchangePrivate::computeEquiCurrencyAmounts(BalancePortfolio &balancePortfolio, CurrencyCode equiCurrency) { + MarketOrderBookMap marketOrderBookMap; + auto fiats = _exchangePublic.queryFiats(); + MarketSet markets; + ExchangeName exchangeName = this->exchangeName(); + + for (auto &[amount, equi] : balancePortfolio) { + MarketsPath conversionPath = + _exchangePublic.findMarketsPath(amount.currencyCode(), equiCurrency, markets, fiats, + ExchangePublic::MarketPathMode::kWithPossibleFiatConversionAtExtremity); + equi = _exchangePublic.convert(amount, equiCurrency, conversionPath, fiats, marketOrderBookMap) + .value_or(MonetaryAmount(0, equiCurrency)); + log::trace("{} Balance {} (eq. {})", exchangeName, amount, equi); } } @@ -352,8 +343,7 @@ MarketVector GetPossibleMarketsForDustThresholds(const BalancePortfolio &balance CurrencyCode currencyCode, const MarketSet &markets, const PenaltyPerMarketMap &penaltyPerMarketMap) { MarketVector possibleMarkets; - for (const BalancePortfolio::MonetaryAmountWithEquivalent &avAmountEq : balance) { - MonetaryAmount avAmount = avAmountEq.amount; + for (const auto [avAmount, _] : balance) { CurrencyCode avCur = avAmount.currencyCode(); const auto lbAvAmount = dustThresholds.find(MonetaryAmount(0, avCur)); if (lbAvAmount == dustThresholds.end() || *lbAvAmount < avAmount) { diff --git a/src/api/common/test/exchangeprivateapi_test.cpp b/src/api/common/test/exchangeprivateapi_test.cpp index dc3db158..ee90678c 100644 --- a/src/api/common/test/exchangeprivateapi_test.cpp +++ b/src/api/common/test/exchangeprivateapi_test.cpp @@ -52,8 +52,8 @@ inline bool operator==(const DeliveredWithdrawInfo &lhs, const DeliveredWithdraw inline BalancePortfolio operator+(const BalancePortfolio &balancePortfolio, const TradedAmounts &tradedAmounts) { BalancePortfolio ret = balancePortfolio; - ret.add(tradedAmounts.to); - ret.add(-tradedAmounts.from); + ret += tradedAmounts.to; + ret += -tradedAmounts.from; return ret; } diff --git a/src/api/exchanges/src/binanceprivateapi.cpp b/src/api/exchanges/src/binanceprivateapi.cpp index bf834a9b..5c17988b 100644 --- a/src/api/exchanges/src/binanceprivateapi.cpp +++ b/src/api/exchanges/src/binanceprivateapi.cpp @@ -258,12 +258,21 @@ bool BinancePrivate::validateApiKey() { } BalancePortfolio BinancePrivate::queryAccountBalance(const BalanceOptions& balanceOptions) { - json result = PrivateQuery(_curlHandle, _apiKey, HttpRequestType::kGet, "/api/v3/account", _queryDelay); - bool withBalanceInUse = + const json result = PrivateQuery(_curlHandle, _apiKey, HttpRequestType::kGet, "/api/v3/account", _queryDelay); + const bool withBalanceInUse = balanceOptions.amountIncludePolicy() == BalanceOptions::AmountIncludePolicy::kWithBalanceInUse; - CurrencyCode equiCurrency = balanceOptions.equiCurrency(); + BalancePortfolio balancePortfolio; - for (const json& balance : result["balances"]) { + + auto dataIt = result.find("balances"); + if (dataIt == result.end()) { + log::error("Unexpected get account balance reply from {}", exchangeName()); + return balancePortfolio; + } + + balancePortfolio.reserve(dataIt->size()); + + for (const json& balance : *dataIt) { CurrencyCode currencyCode(balance["asset"].get()); MonetaryAmount amount(balance["free"].get(), currencyCode); @@ -272,7 +281,7 @@ BalancePortfolio BinancePrivate::queryAccountBalance(const BalanceOptions& balan amount += usedAmount; } - addBalance(balancePortfolio, amount, equiCurrency); + balancePortfolio += amount; } return balancePortfolio; } diff --git a/src/api/exchanges/src/bithumbprivateapi.cpp b/src/api/exchanges/src/bithumbprivateapi.cpp index 9b9bb790..0f175430 100644 --- a/src/api/exchanges/src/bithumbprivateapi.cpp +++ b/src/api/exchanges/src/bithumbprivateapi.cpp @@ -364,8 +364,6 @@ BalancePortfolio BithumbPrivate::queryAccountBalance(const BalanceOptions& balan return balancePortfolio; } - CurrencyCode equiCurrency = balanceOptions.equiCurrency(); - static constexpr std::array kKnownPrefixes = {"available_", "in_use_"}; auto endPrefixIt = kKnownPrefixes.end(); if (balanceOptions.amountIncludePolicy() != BalanceOptions::AmountIncludePolicy::kWithBalanceInUse) { @@ -377,7 +375,8 @@ BalancePortfolio BithumbPrivate::queryAccountBalance(const BalanceOptions& balan if (key.starts_with(*prefixIt)) { std::string_view curStr(key.begin() + prefixIt->size(), key.end()); MonetaryAmount amount(value.get(), _coincenterInfo.standardizeCurrencyCode(curStr)); - this->addBalance(balancePortfolio, amount, equiCurrency); + + balancePortfolio += amount; break; } } diff --git a/src/api/exchanges/src/huobiprivateapi.cpp b/src/api/exchanges/src/huobiprivateapi.cpp index f6d98ec4..a9aa4980 100644 --- a/src/api/exchanges/src/huobiprivateapi.cpp +++ b/src/api/exchanges/src/huobiprivateapi.cpp @@ -177,13 +177,12 @@ BalancePortfolio HuobiPrivate::queryAccountBalance(const BalanceOptions& balance bool withBalanceInUse = balanceOptions.amountIncludePolicy() == BalanceOptions::AmountIncludePolicy::kWithBalanceInUse; - CurrencyCode equiCurrency = balanceOptions.equiCurrency(); for (const json& balanceDetail : result["data"]["list"]) { std::string_view typeStr = balanceDetail["type"].get(); CurrencyCode currencyCode(balanceDetail["currency"].get()); MonetaryAmount amount(balanceDetail["balance"].get(), currencyCode); if (typeStr == "trade" || (withBalanceInUse && typeStr == "frozen")) { - this->addBalance(balancePortfolio, amount, equiCurrency); + balancePortfolio += amount; } else { log::trace("Do not consider {} as it is {} on {}", amount, typeStr, _exchangePublic.name()); } diff --git a/src/api/exchanges/src/krakenprivateapi.cpp b/src/api/exchanges/src/krakenprivateapi.cpp index 27f51d8d..dbf355b6 100644 --- a/src/api/exchanges/src/krakenprivateapi.cpp +++ b/src/api/exchanges/src/krakenprivateapi.cpp @@ -171,7 +171,6 @@ BalancePortfolio KrakenPrivate::queryAccountBalance(const BalanceOptions& balanc bool withBalanceInUse = balanceOptions.amountIncludePolicy() == BalanceOptions::AmountIncludePolicy::kWithBalanceInUse; - CurrencyCode equiCurrency = balanceOptions.equiCurrency(); // Kraken returns total balance, including the amounts in use if (!withBalanceInUse) { @@ -203,9 +202,7 @@ BalancePortfolio KrakenPrivate::queryAccountBalance(const BalanceOptions& balanc } } } - for (MonetaryAmount amount : balanceAmounts) { - addBalance(balancePortfolio, amount, equiCurrency); - } + std::ranges::for_each(balanceAmounts, [&balancePortfolio](const auto amt) { balancePortfolio += amt; }); return balancePortfolio; } diff --git a/src/api/exchanges/src/kucoinprivateapi.cpp b/src/api/exchanges/src/kucoinprivateapi.cpp index d3aa705a..5787bc4e 100644 --- a/src/api/exchanges/src/kucoinprivateapi.cpp +++ b/src/api/exchanges/src/kucoinprivateapi.cpp @@ -185,15 +185,18 @@ BalancePortfolio KucoinPrivate::queryAccountBalance(const BalanceOptions& balanc BalancePortfolio balancePortfolio; bool withBalanceInUse = balanceOptions.amountIncludePolicy() == BalanceOptions::AmountIncludePolicy::kWithBalanceInUse; - CurrencyCode equiCurrency = balanceOptions.equiCurrency(); const std::string_view amountKey = withBalanceInUse ? "balance" : "available"; + + balancePortfolio.reserve(result.size()); + for (const json& balanceDetail : result) { std::string_view typeStr = balanceDetail["type"].get(); CurrencyCode currencyCode( _coincenterInfo.standardizeCurrencyCode(balanceDetail["currency"].get())); MonetaryAmount amount(balanceDetail[amountKey].get(), currencyCode); log::debug("{} in account '{}' on {}", amount, typeStr, exchangeName()); - this->addBalance(balancePortfolio, amount, equiCurrency); + + balancePortfolio += amount; } return balancePortfolio; } diff --git a/src/api/exchanges/src/upbitprivateapi.cpp b/src/api/exchanges/src/upbitprivateapi.cpp index 15105b5b..01559722 100644 --- a/src/api/exchanges/src/upbitprivateapi.cpp +++ b/src/api/exchanges/src/upbitprivateapi.cpp @@ -176,22 +176,24 @@ CurrencyExchangeFlatSet UpbitPrivate::TradableCurrenciesFunc::operator()() { BalancePortfolio UpbitPrivate::queryAccountBalance(const BalanceOptions& balanceOptions) { const bool withBalanceInUse = balanceOptions.amountIncludePolicy() == BalanceOptions::AmountIncludePolicy::kWithBalanceInUse; - const CurrencyCode equiCurrency = balanceOptions.equiCurrency(); - BalancePortfolio ret; - for (const json& accountDetail : PrivateQuery(_curlHandle, _apiKey, HttpRequestType::kGet, "/v1/accounts")) { - const CurrencyCode currencyCode(accountDetail["currency"].get()); - const MonetaryAmount availableAmount(accountDetail["balance"].get(), currencyCode); + BalancePortfolio balancePortfolio; - this->addBalance(ret, availableAmount, equiCurrency); + json ret = PrivateQuery(_curlHandle, _apiKey, HttpRequestType::kGet, "/v1/accounts"); - if (withBalanceInUse) { - const MonetaryAmount amountInUse(accountDetail["locked"].get(), currencyCode); + balancePortfolio.reserve(ret.size()); + + for (const json& accountDetail : ret) { + const CurrencyCode currencyCode(accountDetail["currency"].get()); + MonetaryAmount availableAmount(accountDetail["balance"].get(), currencyCode); - this->addBalance(ret, amountInUse, equiCurrency); + if (withBalanceInUse) { + availableAmount += MonetaryAmount(accountDetail["locked"].get(), currencyCode); } + + balancePortfolio += availableAmount; } - return ret; + return balancePortfolio; } Wallet UpbitPrivate::DepositWalletFunc::operator()(CurrencyCode currencyCode) { diff --git a/src/engine/src/balanceperexchangeportfolio.cpp b/src/engine/src/balanceperexchangeportfolio.cpp index 19a71706..4e28c080 100644 --- a/src/engine/src/balanceperexchangeportfolio.cpp +++ b/src/engine/src/balanceperexchangeportfolio.cpp @@ -14,7 +14,7 @@ namespace cct { namespace { MonetaryAmount ComputeTotalSum(const BalancePortfolio &total) { - CurrencyCode balanceCurrencyCode = total.empty() ? CurrencyCode() : total.front().equi.currencyCode(); + CurrencyCode balanceCurrencyCode = total.equiCurrency(); MonetaryAmount totalSum(0, balanceCurrencyCode); for (const auto &[amount, equi] : total) { totalSum += equi; @@ -25,7 +25,7 @@ MonetaryAmount ComputeTotalSum(const BalancePortfolio &total) { SimpleTable BalancePerExchangePortfolio::getTable(bool wide) const { BalancePortfolio total = computeTotal(); - CurrencyCode balanceCurrencyCode = total.empty() ? CurrencyCode() : total.front().equi.currencyCode(); + CurrencyCode balanceCurrencyCode = total.equiCurrency(); const bool countEqui = !balanceCurrencyCode.isNeutral(); table::Row header("Currency", "Total amount on selected"); diff --git a/src/engine/src/metricsexporter.cpp b/src/engine/src/metricsexporter.cpp index a73ce772..3ef8633b 100644 --- a/src/engine/src/metricsexporter.cpp +++ b/src/engine/src/metricsexporter.cpp @@ -45,11 +45,11 @@ void MetricsExporter::exportBalanceMetrics(const BalancePerExchange &balancePerE key.set("account", exchange.keyName()); key.set("total", "no"); MonetaryAmount totalEquiAmount(0, equiCurrency); - for (BalancePortfolio::MonetaryAmountWithEquivalent amountWithEqui : balancePortfolio) { - key.set("currency", amountWithEqui.amount.currencyStr()); - _pMetricsGateway->add(MetricType::kGauge, MetricOperation::kSet, key, amountWithEqui.amount.toDouble()); - if (!equiCurrency.isNeutral()) { - totalEquiAmount += amountWithEqui.equi; + for (auto [amount, equi] : balancePortfolio) { + key.set("currency", amount.currencyStr()); + _pMetricsGateway->add(MetricType::kGauge, MetricOperation::kSet, key, amount.toDouble()); + if (equiCurrency.isDefined()) { + totalEquiAmount += equi; } } if (!equiCurrency.isNeutral()) { diff --git a/src/engine/test/queryresultprinter_private_test.cpp b/src/engine/test/queryresultprinter_private_test.cpp index 7866b256..1ee6770d 100644 --- a/src/engine/test/queryresultprinter_private_test.cpp +++ b/src/engine/test/queryresultprinter_private_test.cpp @@ -5,6 +5,7 @@ #include "accountowner.hpp" #include "apioutputtype.hpp" #include "balanceportfolio.hpp" +#include "cct_exception.hpp" #include "closed-order.hpp" #include "currencycode.hpp" #include "deposit.hpp" @@ -238,14 +239,43 @@ TEST_F(QueryResultPrinterBalanceNoEquiCurTest, NoPrint) { class QueryResultPrinterBalanceEquiCurTest : public QueryResultPrinterTest { protected: + void SetUp() override { + for (auto &[amount, equi] : bp1) { + if (amount == ma1) { + equi = MonetaryAmount{10000, equiCur}; + } else if (amount == ma2) { + equi = MonetaryAmount{90677, equiCur, 1}; + } else { + throw exception("Should not happen"); + } + } + + for (auto &[amount, equi] : bp2) { + if (amount == ma3) { + equi = MonetaryAmount{4508, equiCur, 2}; + } else if (amount == ma4) { + equi = MonetaryAmount{25000, equiCur}; + } else if (amount == ma5) { + equi = MonetaryAmount{675, equiCur, 1}; + } else { + throw exception("Should not happen"); + } + } + + balancePerExchange = BalancePerExchange{{&exchange1, bp1}, {&exchange4, bp2}, {&exchange2, bp3}, {&exchange3, bp3}}; + } + + MonetaryAmount ma1{15000, "ADA"}; + MonetaryAmount ma2{56, "BTC", 2}; + MonetaryAmount ma3{347, "XRP", 1}; + MonetaryAmount ma4{15, "ETH"}; + MonetaryAmount ma5{123, "XLM"}; + 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 bp1{ma1, ma2}; + BalancePortfolio bp2{ma3, ma4, ma5}; BalancePortfolio bp3; - BalancePerExchange balancePerExchange{{&exchange1, bp1}, {&exchange4, bp2}, {&exchange2, bp3}, {&exchange3, bp3}}; + BalancePerExchange balancePerExchange; }; TEST_F(QueryResultPrinterBalanceEquiCurTest, FormattedTable) { diff --git a/src/objects/include/balanceportfolio.hpp b/src/objects/include/balanceportfolio.hpp index add16b63..08f9a092 100644 --- a/src/objects/include/balanceportfolio.hpp +++ b/src/objects/include/balanceportfolio.hpp @@ -10,20 +10,22 @@ namespace cct { +namespace api { +class ExchangePrivate; +} + class BalancePortfolio { - public: + private: struct MonetaryAmountWithEquivalent { MonetaryAmount amount; MonetaryAmount equi; - bool operator==(const MonetaryAmountWithEquivalent &) const = default; + bool operator==(const MonetaryAmountWithEquivalent&) const noexcept = default; }; - private: using MonetaryAmountVec = vector; public: - using const_iterator = MonetaryAmountVec::const_iterator; using size_type = MonetaryAmountVec::size_type; BalancePortfolio() noexcept = default; @@ -33,39 +35,42 @@ class BalancePortfolio { 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()); - MonetaryAmount get(CurrencyCode currencyCode) const; bool hasSome(CurrencyCode cur) const { return get(cur) > 0; } bool hasAtLeast(MonetaryAmount amount) const { return get(amount.currencyCode()) >= amount; } - const_iterator begin() const { return _sortedAmounts.begin(); } - const_iterator end() const { return _sortedAmounts.end(); } + auto begin() { return _sortedAmounts.begin(); } + auto end() { return _sortedAmounts.end(); } + + auto begin() const { return _sortedAmounts.begin(); } + auto end() const { return _sortedAmounts.end(); } - MonetaryAmountWithEquivalent front() const { return _sortedAmounts.front(); } - MonetaryAmountWithEquivalent back() const { return _sortedAmounts.back(); } + auto cbegin() const { return _sortedAmounts.cbegin(); } + auto cend() const { return _sortedAmounts.cend(); } bool empty() const noexcept { return _sortedAmounts.empty(); } size_type size() const noexcept { return _sortedAmounts.size(); } + void reserve(size_type newCapacity) { _sortedAmounts.reserve(newCapacity); } + + CurrencyCode equiCurrency() const; + void sortByDecreasingEquivalentAmount(); - BalancePortfolio &operator+=(const BalancePortfolio &other); + /// Adds an amount in this portfolio without an equivalent amount. + BalancePortfolio& operator+=(MonetaryAmount amount); + + /// Merge the amounts of two different portfolios. + BalancePortfolio& operator+=(const BalancePortfolio& other); - bool operator==(const BalancePortfolio &) const = default; + bool operator==(const BalancePortfolio&) const = default; using trivially_relocatable = is_trivially_relocatable::type; private: MonetaryAmountVec _sortedAmounts; }; + } // namespace cct \ No newline at end of file diff --git a/src/objects/src/balanceportfolio.cpp b/src/objects/src/balanceportfolio.cpp index 5022646b..b55b11cf 100644 --- a/src/objects/src/balanceportfolio.cpp +++ b/src/objects/src/balanceportfolio.cpp @@ -2,52 +2,42 @@ #include #include -#include #include "currencycode.hpp" #include "monetaryamount.hpp" namespace cct { namespace { -using MonetaryAmountWithEquivalent = BalancePortfolio::MonetaryAmountWithEquivalent; - -inline bool CurCompare(const MonetaryAmountWithEquivalent &lhs, const MonetaryAmountWithEquivalent &rhs) { - return lhs.amount.currencyCode() < rhs.amount.currencyCode(); -} - -inline MonetaryAmountWithEquivalent &operator+=(MonetaryAmountWithEquivalent &lhs, - const MonetaryAmountWithEquivalent &rhs) { +inline auto &operator+=(auto &lhs, const auto &rhs) { lhs.amount += rhs.amount; lhs.equi += rhs.equi; + return lhs; } + } // namespace BalancePortfolio::BalancePortfolio(std::span init) { // Simple for loop to avoid complex code eliminating duplicates for same currency - for (MonetaryAmount ma : init) { - add(ma); - } + // TODO (for fun): we could maybe replace this with a more elegant std::accumulate algorithm + std::ranges::for_each(init, [this](const auto &am) { *this += am; }); } -BalancePortfolio::BalancePortfolio(std::span init) { - // Simple for loop to avoid complex code eliminating duplicates for same currency - for (const MonetaryAmountWithEquivalent &monetaryAmountWithEquivalent : init) { - add(monetaryAmountWithEquivalent.amount, monetaryAmountWithEquivalent.equi); - } -} +BalancePortfolio &BalancePortfolio::operator+=(MonetaryAmount amount) { + if (amount != 0) { + auto isCurrencyCodeLt = [amount](const auto &elem) { return elem.amount.currencyCode() < amount.currencyCode(); }; + auto lb = std::ranges::partition_point(_sortedAmounts, isCurrencyCodeLt); -void BalancePortfolio::add(MonetaryAmount amount, MonetaryAmount equivalentInMainCurrency) { - MonetaryAmountWithEquivalent elem{amount, equivalentInMainCurrency}; - auto lb = std::ranges::lower_bound(_sortedAmounts, elem, CurCompare); - if (lb == _sortedAmounts.end()) { - _sortedAmounts.push_back(std::move(elem)); - } else if (CurCompare(elem, *lb)) { - _sortedAmounts.insert(lb, std::move(elem)); - } else { - // equal, sum amounts - *lb += std::move(elem); + if (lb == _sortedAmounts.end()) { + _sortedAmounts.emplace_back(amount, MonetaryAmount()); + } else if (lb->amount.currencyCode() != amount.currencyCode()) { + _sortedAmounts.emplace(lb, amount, MonetaryAmount()); + } else { + // equal currencies, sum amounts + lb->amount += amount; + } } + return *this; } MonetaryAmount BalancePortfolio::get(CurrencyCode currencyCode) const { @@ -66,15 +56,18 @@ BalancePortfolio &BalancePortfolio::operator+=(const BalancePortfolio &other) { auto last1 = _sortedAmounts.end(); auto first2 = other.begin(); auto last2 = other.end(); + auto amountCurrencyCompare = [](const auto &lhs, const auto &rhs) { + return lhs.amount.currencyCode() < rhs.amount.currencyCode(); + }; while (first2 != last2) { if (first1 == last1) { _sortedAmounts.insert(_sortedAmounts.end(), first2, last2); break; } - if (CurCompare(*first1, *first2)) { + if (amountCurrencyCompare(*first1, *first2)) { ++first1; - } else if (CurCompare(*first2, *first1)) { + } else if (amountCurrencyCompare(*first2, *first1)) { first1 = _sortedAmounts.insert(first1, *first2); ++first1; last1 = _sortedAmounts.end(); // as iterators may have been invalidated @@ -90,12 +83,19 @@ BalancePortfolio &BalancePortfolio::operator+=(const BalancePortfolio &other) { } void BalancePortfolio::sortByDecreasingEquivalentAmount() { - std::ranges::sort(_sortedAmounts, - [](const MonetaryAmountWithEquivalent &lhs, const MonetaryAmountWithEquivalent &rhs) { - if (lhs.equi != rhs.equi) { - return lhs.equi > rhs.equi; - } - return lhs.amount.currencyCode() < rhs.amount.currencyCode(); - }); + std::ranges::sort(_sortedAmounts, [](const auto &lhs, const auto &rhs) { + if (lhs.equi != rhs.equi) { + return lhs.equi > rhs.equi; + } + return lhs.amount.currencyCode() < rhs.amount.currencyCode(); + }); } + +CurrencyCode BalancePortfolio::equiCurrency() const { + if (_sortedAmounts.empty()) { + return {}; + } + return _sortedAmounts.front().equi.currencyCode(); +} + } // namespace cct \ No newline at end of file diff --git a/src/objects/test/balanceportfolio_test.cpp b/src/objects/test/balanceportfolio_test.cpp index df3ae051..f2c6788c 100644 --- a/src/objects/test/balanceportfolio_test.cpp +++ b/src/objects/test/balanceportfolio_test.cpp @@ -14,41 +14,42 @@ class BalancePortfolioTest1 : public ::testing::Test { TEST_F(BalancePortfolioTest1, Instantiate) { EXPECT_TRUE(balancePortfolio.empty()); } -TEST_F(BalancePortfolioTest1, NoEquivalentCurrency1) { - balancePortfolio.add(MonetaryAmount("10 EUR")); +TEST_F(BalancePortfolioTest1, NoEquivalentCurrencySimpleNoDuplicates) { + balancePortfolio += MonetaryAmount("10 EUR"); EXPECT_FALSE(balancePortfolio.empty()); + + EXPECT_EQ(balancePortfolio.size(), 1U); EXPECT_EQ(balancePortfolio.get("EUR"), MonetaryAmount("10 EUR")); EXPECT_EQ(balancePortfolio.get("BTC"), MonetaryAmount("0 BTC")); } -TEST_F(BalancePortfolioTest1, NoEquivalentCurrency2) { - balancePortfolio.add(MonetaryAmount("10 EUR")); - balancePortfolio.add(MonetaryAmount("0.45 BTC")); - balancePortfolio.add(MonetaryAmount("11704.5678 XRP")); - balancePortfolio.add(MonetaryAmount("215 XLM")); +TEST_F(BalancePortfolioTest1, NoEquivalentCurrencyWithSameCurrencies) { + balancePortfolio += MonetaryAmount("10 EUR"); + balancePortfolio += MonetaryAmount("0.45 BTC"); + balancePortfolio += MonetaryAmount("11704.5678 XRP"); + balancePortfolio += MonetaryAmount("215 XLM"); + balancePortfolio += MonetaryAmount("0.15 BTC"); + + EXPECT_EQ(balancePortfolio.size(), 4U); EXPECT_EQ(balancePortfolio.get("EUR"), MonetaryAmount("10 EUR")); EXPECT_EQ(balancePortfolio.get("XLM"), MonetaryAmount("215 XLM")); - EXPECT_EQ(balancePortfolio.get("BTC"), MonetaryAmount("0.45 BTC")); + EXPECT_EQ(balancePortfolio.get("BTC"), MonetaryAmount("0.6 BTC")); EXPECT_EQ(balancePortfolio.get("ETH"), MonetaryAmount("0 ETH")); } class BalancePortfolioTest2 : public ::testing::Test { protected: - BalancePortfolioTest2() { - balancePortfolio.add(MonetaryAmount("11704.5678 XRP")); - balancePortfolio.add(MonetaryAmount("10 EUR")); - balancePortfolio.add(MonetaryAmount("0.45 BTC")); - balancePortfolio.add(MonetaryAmount("215 XLM")); - } - - BalancePortfolio balancePortfolio, o; + BalancePortfolio balancePortfolio{MonetaryAmount("10 EUR"), MonetaryAmount("0.45 BTC"), + MonetaryAmount("11704.5678 XRP"), MonetaryAmount("215 XLM")}; + BalancePortfolio o; }; TEST_F(BalancePortfolioTest2, AddBalancePortfolio1) { - o.add(MonetaryAmount("3.5 USD")); - o.add(MonetaryAmount("0.45 XRP")); + o += MonetaryAmount("3.5 USD"); + o += MonetaryAmount("0.45 XRP"); + balancePortfolio += o; EXPECT_EQ(balancePortfolio.size(), 5U); EXPECT_EQ(balancePortfolio.get("XLM"), MonetaryAmount("215 XLM"));