From 4d12e3efe9fff2ecbc35ee837494f33d26f41f3d Mon Sep 17 00:00:00 2001 From: Stephane Janel Date: Tue, 5 Mar 2024 17:58:49 +0100 Subject: [PATCH] Improve market order book computation of average price for taker buy / sell --- .../common/test/exchangeprivateapi_test.cpp | 6 +- src/objects/include/marketorderbook.hpp | 10 +- src/objects/src/marketorderbook.cpp | 119 +++++++++--------- src/objects/test/marketorderbook_test.cpp | 22 +++- 4 files changed, 86 insertions(+), 71 deletions(-) diff --git a/src/api/common/test/exchangeprivateapi_test.cpp b/src/api/common/test/exchangeprivateapi_test.cpp index 81bb6a30..ebfdbe86 100644 --- a/src/api/common/test/exchangeprivateapi_test.cpp +++ b/src/api/common/test/exchangeprivateapi_test.cpp @@ -143,7 +143,7 @@ TEST_F(ExchangePrivateTest, TakerTradeQuoteToBase) { tradeBaseExpectCalls(); MonetaryAmount from(5000, market.quote()); - MonetaryAmount pri(marketOrderBook1.computeAvgPriceForTakerAmount(from).value_or(MonetaryAmount{-1})); + auto [pri, _] = marketOrderBook1.avgPriceAndMatchedVolumeTaker(from); MonetaryAmount vol(from / pri, market.base()); PriceOptions priceOptions(PriceStrategy::kTaker); @@ -166,7 +166,7 @@ TEST_F(ExchangePrivateTest, TradeAsyncPolicyTaker) { tradeBaseExpectCalls(); MonetaryAmount from(5000, market.quote()); - MonetaryAmount pri(marketOrderBook1.computeAvgPriceForTakerAmount(from).value_or(MonetaryAmount{-1})); + auto [pri, _] = marketOrderBook1.avgPriceAndMatchedVolumeTaker(from); MonetaryAmount vol(from / pri, market.base()); PriceOptions priceOptions(PriceStrategy::kTaker); @@ -387,7 +387,7 @@ TEST_F(ExchangePrivateTest, MakerTradeQuoteToBaseEmergencyTakerTrade) { // Place taker order tradeInfo.options.switchToTakerStrategy(); - MonetaryAmount pri2 = marketOrderBook1.computeAvgPriceForTakerAmount(from).value_or(MonetaryAmount{-1}); + auto [pri2, _] = marketOrderBook1.avgPriceAndMatchedVolumeTaker(from); MonetaryAmount vol2(from / pri2, market.base()); PlaceOrderInfo matchedPlacedOrderInfo2(OrderInfo(TradedAmounts(from, vol2), true), OrderId("Order # 1")); diff --git a/src/objects/include/marketorderbook.hpp b/src/objects/include/marketorderbook.hpp index 9a065be2..48e89f4c 100644 --- a/src/objects/include/marketorderbook.hpp +++ b/src/objects/include/marketorderbook.hpp @@ -110,9 +110,9 @@ class MarketOrderBook { /// If operation is not possible, return an empty vector. AmountPerPriceVec computePricesAtWhichAmountWouldBeSoldImmediately(MonetaryAmount ma) const; - /// Given an amount in either base or quote currency, attempt to convert it at market price immediately and return - /// the average price matched. - std::optional computeAvgPriceForTakerAmount(MonetaryAmount amountInBaseOrQuote) const; + /// Given an amount in either base or quote currency, attempt to convert it at market price immediately. + /// @return a pair of {average matched price, total matched amount given in input} + std::pair avgPriceAndMatchedVolumeTaker(MonetaryAmount amountInBaseOrQuote) const; /// Given an amount in either base or quote currency, attempt to convert it at market price immediately and return /// the worst price matched. @@ -209,9 +209,9 @@ class MarketOrderBook { return MonetaryAmount(_orders[pos].price, _market.quote(), _volAndPriNbDecimals.priNbDecimals); } - std::optional computeAvgPriceAtWhichAmountWouldBeSoldImmediately(MonetaryAmount ma) const; + std::pair avgPriceAndMatchedVolumeTakerSell(MonetaryAmount baseAmount) const; - std::optional computeAvgPriceAtWhichAmountWouldBeBoughtImmediately(MonetaryAmount ma) const; + std::pair avgPriceAndMatchedVolumeTakerBuy(MonetaryAmount quoteAmount) const; /// Attempt to convert given amount expressed in base currency to quote currency. /// It may not be possible, in which case an empty optional will be returned. diff --git a/src/objects/src/marketorderbook.cpp b/src/objects/src/marketorderbook.cpp index 1b3d64dd..219547b6 100644 --- a/src/objects/src/marketorderbook.cpp +++ b/src/objects/src/marketorderbook.cpp @@ -264,28 +264,55 @@ MarketOrderBook::AmountPerPriceVec MarketOrderBook::computePricesAtWhichAmountWo return ret; } -namespace { -inline std::optional ComputeAvgPrice(Market mk, - const MarketOrderBook::AmountPerPriceVec& amountsPerPrice) { - if (amountsPerPrice.empty()) { - return {}; - } - if (amountsPerPrice.size() == 1) { - return amountsPerPrice.front().price; - } - MonetaryAmount ret(0, mk.quote()); - MonetaryAmount totalAmount(0, mk.base()); - for (const MarketOrderBook::AmountAtPrice& amountAtPrice : amountsPerPrice) { - ret += amountAtPrice.amount.toNeutral() * amountAtPrice.price; - totalAmount += amountAtPrice.amount; +std::pair MarketOrderBook::avgPriceAndMatchedVolumeTakerSell( + MonetaryAmount baseAmount) const { + MonetaryAmount avgPrice(0, _market.quote()); + MonetaryAmount remainingBaseAmount = baseAmount; + for (int pos = _lowestAskPricePos - 1; pos >= 0; --pos) { + const MonetaryAmount amount = amountAt(pos); + const MonetaryAmount price = priceAt(pos); + const MonetaryAmount amountToEat = std::min(amount, remainingBaseAmount); + + avgPrice += amountToEat.toNeutral() * price; + remainingBaseAmount -= amountToEat; + if (remainingBaseAmount == 0) { + break; + } } - return ret / totalAmount.toNeutral(); + return {avgPrice / baseAmount.toNeutral(), baseAmount - remainingBaseAmount}; } -} // namespace -std::optional MarketOrderBook::computeAvgPriceAtWhichAmountWouldBeBoughtImmediately( - MonetaryAmount ma) const { - return ComputeAvgPrice(_market, computePricesAtWhichAmountWouldBeBoughtImmediately(ma)); +std::pair MarketOrderBook::avgPriceAndMatchedVolumeTakerBuy( + MonetaryAmount quoteAmount) const { + MonetaryAmount avgPrice; + MonetaryAmount remainingQuoteAmount = quoteAmount; + MonetaryAmount totalAmountMatched; + const int nbOrders = _orders.size(); + for (int pos = _highestBidPricePos + 1; pos < nbOrders; ++pos) { + const MonetaryAmount amount = negAmountAt(pos); + const MonetaryAmount price = priceAt(pos); + MonetaryAmount quoteAmountToEat = amount.toNeutral() * price; + + if (quoteAmountToEat < remainingQuoteAmount) { + totalAmountMatched += amount; + } else { + quoteAmountToEat = remainingQuoteAmount; + totalAmountMatched += MonetaryAmount(remainingQuoteAmount / price, _market.base()); + } + + remainingQuoteAmount -= quoteAmountToEat; + + if (remainingQuoteAmount == 0 || pos + 1 == nbOrders) { + if (pos == _highestBidPricePos + 1) { + // to avoid rounding issues + avgPrice = price; + } else { + avgPrice = (quoteAmount - remainingQuoteAmount) / totalAmountMatched.toNeutral(); + } + break; + } + } + return {avgPrice, quoteAmount - remainingQuoteAmount}; } std::optional MarketOrderBook::computeMinPriceAtWhichAmountWouldBeSoldImmediately( @@ -340,34 +367,12 @@ MarketOrderBook::AmountPerPriceVec MarketOrderBook::computePricesAtWhichAmountWo return ret; } -std::optional MarketOrderBook::computeAvgPriceAtWhichAmountWouldBeSoldImmediately( - MonetaryAmount ma) const { - return ComputeAvgPrice(_market, computePricesAtWhichAmountWouldBeSoldImmediately(ma)); -} - -std::optional MarketOrderBook::computeAvgPriceForTakerAmount(MonetaryAmount amountInBaseOrQuote) const { +std::pair MarketOrderBook::avgPriceAndMatchedVolumeTaker( + MonetaryAmount amountInBaseOrQuote) const { if (amountInBaseOrQuote.currencyCode() == _market.base()) { - return computeAvgPriceAtWhichAmountWouldBeSoldImmediately(amountInBaseOrQuote); + return avgPriceAndMatchedVolumeTakerSell(amountInBaseOrQuote); } - MonetaryAmount avgPrice(0, _market.quote()); - MonetaryAmount remQuoteAmount = amountInBaseOrQuote; - const int nbOrders = _orders.size(); - for (int pos = _highestBidPricePos + 1; pos < nbOrders; ++pos) { - const MonetaryAmount amount = negAmountAt(pos); - const MonetaryAmount price = priceAt(pos); - const MonetaryAmount maxAmountToTakeFromThisLine = amount.toNeutral() * price; - - if (maxAmountToTakeFromThisLine < remQuoteAmount) { - // We can eat all from this line, take the max and continue - avgPrice += maxAmountToTakeFromThisLine.toNeutral() * price; - remQuoteAmount -= maxAmountToTakeFromThisLine; - } else { - // We can finish here - avgPrice += remQuoteAmount.toNeutral() * price; - return avgPrice / amountInBaseOrQuote.toNeutral(); - } - } - return {}; + return avgPriceAndMatchedVolumeTakerBuy(amountInBaseOrQuote); } std::optional MarketOrderBook::computeWorstPriceForTakerAmount( @@ -428,14 +433,12 @@ std::optional MarketOrderBook::convertBaseAmountToQuote(Monetary for (int pos = _lowestAskPricePos - 1; pos >= 0; --pos) { const MonetaryAmount amount = amountAt(pos); const MonetaryAmount price = priceAt(pos); + const MonetaryAmount amountToEat = std::min(amount, amountInBaseCurrency); - if (amount < amountInBaseCurrency) { - // We can eat all from this line, take the max and continue - quoteAmount += amount.toNeutral() * price; - amountInBaseCurrency -= amount; - } else { - // We can finish here - return quoteAmount + amountInBaseCurrency.toNeutral() * price; + quoteAmount += amountToEat.toNeutral() * price; + amountInBaseCurrency -= amountToEat; + if (amountInBaseCurrency == 0) { + return quoteAmount; } } return {}; @@ -543,12 +546,14 @@ std::optional MarketOrderBook::computeAvgPrice(MonetaryAmount fr CurrencyCode marketCode = _market.base(); switch (priceOptions.priceStrategy()) { case PriceStrategy::kTaker: { - std::optional optRet = computeAvgPriceForTakerAmount(from); - if (optRet) { - return optRet; + auto [avgPri, avgMatchedFrom] = avgPriceAndMatchedVolumeTaker(from); + if (avgMatchedFrom < from) { + log::warn( + "{} is too big to be matched immediately on {}, return limit price instead ({} matched amount among total " + "of {})", + from, _market, avgMatchedFrom, from); } - log::warn("{} is too big to be matched immediately on {}, return limit price instead", from, _market); - [[fallthrough]]; + return avgPri; } case PriceStrategy::kNibble: marketCode = _market.quote(); diff --git a/src/objects/test/marketorderbook_test.cpp b/src/objects/test/marketorderbook_test.cpp index 6f092adf..fc290a7d 100644 --- a/src/objects/test/marketorderbook_test.cpp +++ b/src/objects/test/marketorderbook_test.cpp @@ -98,12 +98,22 @@ TEST_F(MarketOrderBookTestCase1, ComputeMaxPriceAtWhichAmountWouldBeBoughtImmedi std::nullopt); } -TEST_F(MarketOrderBookTestCase1, ComputeAvgPriceForTakerAmount) { - EXPECT_EQ(marketOrderBook.computeAvgPriceForTakerAmount(MonetaryAmount(4, "ETH")), std::nullopt); - EXPECT_EQ(marketOrderBook.computeAvgPriceForTakerAmount(MonetaryAmount("0.24", "ETH")), MonetaryAmount("1301 EUR")); - EXPECT_EQ(marketOrderBook.computeAvgPriceForTakerAmount(MonetaryAmount("1000 EUR")), MonetaryAmount("1302 EUR")); - EXPECT_EQ(marketOrderBook.computeAvgPriceForTakerAmount(MonetaryAmount("5000 EUR")), - MonetaryAmount("1302.31760282 EUR")); +TEST_F(MarketOrderBookTestCase1, ComputeAvgPriceForTakerBuy) { + EXPECT_EQ(marketOrderBook.avgPriceAndMatchedVolumeTaker(MonetaryAmount(1000, "EUR")), + std::make_pair(MonetaryAmount(1302, "EUR"), MonetaryAmount(1000, "EUR"))); + EXPECT_EQ(marketOrderBook.avgPriceAndMatchedVolumeTaker(MonetaryAmount(5000, "EUR")), + std::make_pair(MonetaryAmount("1302.31755833325309", "EUR"), MonetaryAmount(5000, "EUR"))); + EXPECT_EQ(marketOrderBook.avgPriceAndMatchedVolumeTaker(MonetaryAmount(100000, "EUR")), + std::make_pair(MonetaryAmount("1302.94629812356546", "EUR"), MonetaryAmount("79845.73830901", "EUR"))); +} + +TEST_F(MarketOrderBookTestCase1, ComputeAvgPriceForTakerSell) { + EXPECT_EQ(marketOrderBook.avgPriceAndMatchedVolumeTaker(MonetaryAmount(24, "ETH", 2)), + std::make_pair(MonetaryAmount(1301, "EUR"), MonetaryAmount(24, "ETH", 2))); + EXPECT_EQ(marketOrderBook.avgPriceAndMatchedVolumeTaker(MonetaryAmount(5, "ETH", 1)), + std::make_pair(MonetaryAmount(130074, "EUR", 2), MonetaryAmount(5, "ETH", 1))); + EXPECT_EQ(marketOrderBook.avgPriceAndMatchedVolumeTaker(MonetaryAmount(4, "ETH")), + std::make_pair(MonetaryAmount("289.39125", "EUR"), MonetaryAmount(89, "ETH", 2))); } TEST_F(MarketOrderBookTestCase1, MoreComplexListOfPricesComputations) {