From 5ed3483c8b8d0938d76932a5eaeb6fbe43ad7587 Mon Sep 17 00:00:00 2001 From: Stephane Janel Date: Thu, 7 Mar 2024 13:13:18 +0100 Subject: [PATCH] [Feature] - Add new services to avg match amounts at price for market order book --- .../common/test/exchangeprivateapi_test.cpp | 6 +- src/objects/include/marketorderbook.hpp | 17 ++- .../include/tradeside.hpp | 0 src/objects/src/marketorderbook.cpp | 118 ++++++++++++++---- src/objects/test/marketorderbook_test.cpp | 48 +++++-- 5 files changed, 143 insertions(+), 46 deletions(-) rename src/{api-objects => objects}/include/tradeside.hpp (100%) diff --git a/src/api/common/test/exchangeprivateapi_test.cpp b/src/api/common/test/exchangeprivateapi_test.cpp index ebfdbe86..1c446d2a 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()); - auto [pri, _] = marketOrderBook1.avgPriceAndMatchedVolumeTaker(from); + auto [_, pri] = marketOrderBook1.avgPriceAndMatchedAmountTaker(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()); - auto [pri, _] = marketOrderBook1.avgPriceAndMatchedVolumeTaker(from); + auto [_, pri] = marketOrderBook1.avgPriceAndMatchedAmountTaker(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(); - auto [pri2, _] = marketOrderBook1.avgPriceAndMatchedVolumeTaker(from); + auto [_, pri2] = marketOrderBook1.avgPriceAndMatchedAmountTaker(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 48e89f4c..b3b32f45 100644 --- a/src/objects/include/marketorderbook.hpp +++ b/src/objects/include/marketorderbook.hpp @@ -11,6 +11,7 @@ #include "monetaryamount.hpp" #include "simpletable.hpp" #include "timedef.hpp" +#include "tradeside.hpp" #include "volumeandpricenbdecimals.hpp" namespace cct { @@ -110,9 +111,17 @@ class MarketOrderBook { /// If operation is not possible, return an empty vector. AmountPerPriceVec computePricesAtWhichAmountWouldBeSoldImmediately(MonetaryAmount ma) const; + /// Given an amount in base currency and the trade side with its price, compute the average matched amount + /// and price + /// @return a pair of {total matched amount in base currency, average matched price} + AmountAtPrice avgPriceAndMatchedVolume(TradeSide tradeSide, MonetaryAmount amount, MonetaryAmount price) 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; + /// @return a pair of {total matched amount in given currency, average matched price} + AmountAtPrice avgPriceAndMatchedAmountTaker(MonetaryAmount amountInBaseOrQuote) const; + + /// Compute the matched amounts that would occur immediately if an order of given amount were placed at given price + AmountPerPriceVec computeMatchedParts(TradeSide tradeSide, MonetaryAmount amount, MonetaryAmount price) 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 +218,9 @@ class MarketOrderBook { return MonetaryAmount(_orders[pos].price, _market.quote(), _volAndPriNbDecimals.priNbDecimals); } - std::pair avgPriceAndMatchedVolumeTakerSell(MonetaryAmount baseAmount) const; + AmountAtPrice avgPriceAndMatchedVolumeSell(MonetaryAmount baseAmount, MonetaryAmount price) const; - std::pair avgPriceAndMatchedVolumeTakerBuy(MonetaryAmount quoteAmount) const; + AmountAtPrice avgPriceAndMatchedVolumeBuy(MonetaryAmount quoteAmount, MonetaryAmount price) 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/api-objects/include/tradeside.hpp b/src/objects/include/tradeside.hpp similarity index 100% rename from src/api-objects/include/tradeside.hpp rename to src/objects/include/tradeside.hpp diff --git a/src/objects/src/marketorderbook.cpp b/src/objects/src/marketorderbook.cpp index 219547b6..a0b956c5 100644 --- a/src/objects/src/marketorderbook.cpp +++ b/src/objects/src/marketorderbook.cpp @@ -264,55 +264,59 @@ MarketOrderBook::AmountPerPriceVec MarketOrderBook::computePricesAtWhichAmountWo return ret; } -std::pair MarketOrderBook::avgPriceAndMatchedVolumeTakerSell( - MonetaryAmount baseAmount) const { +MarketOrderBook::AmountAtPrice MarketOrderBook::avgPriceAndMatchedVolumeSell(MonetaryAmount baseAmount, + MonetaryAmount price) 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); + const MonetaryAmount linePrice = priceAt(pos); + if (linePrice < price) { + break; + } + const MonetaryAmount lineAmount = amountAt(pos); + const MonetaryAmount amountToEat = std::min(lineAmount, remainingBaseAmount); - avgPrice += amountToEat.toNeutral() * price; + avgPrice += amountToEat.toNeutral() * linePrice; remainingBaseAmount -= amountToEat; if (remainingBaseAmount == 0) { break; } } - return {avgPrice / baseAmount.toNeutral(), baseAmount - remainingBaseAmount}; + MonetaryAmount matchedAmount = baseAmount - remainingBaseAmount; + return {matchedAmount, avgPrice / matchedAmount.toNeutral()}; } -std::pair MarketOrderBook::avgPriceAndMatchedVolumeTakerBuy( - MonetaryAmount quoteAmount) const { - MonetaryAmount avgPrice; +MarketOrderBook::AmountAtPrice MarketOrderBook::avgPriceAndMatchedVolumeBuy(MonetaryAmount quoteAmount, + MonetaryAmount price) const { MonetaryAmount remainingQuoteAmount = quoteAmount; - MonetaryAmount totalAmountMatched; + MonetaryAmount matchedAmount; 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; + const MonetaryAmount linePrice = priceAt(pos); + if (linePrice > price) { + break; + } + const MonetaryAmount lineAmount = negAmountAt(pos); + MonetaryAmount quoteAmountToEat = lineAmount.toNeutral() * linePrice; if (quoteAmountToEat < remainingQuoteAmount) { - totalAmountMatched += amount; + matchedAmount += lineAmount; } else { quoteAmountToEat = remainingQuoteAmount; - totalAmountMatched += MonetaryAmount(remainingQuoteAmount / price, _market.base()); + matchedAmount += MonetaryAmount(remainingQuoteAmount / linePrice, _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}; + price = quoteAmount - remainingQuoteAmount; + if (matchedAmount != 0) { + price /= matchedAmount.toNeutral(); + } + return {matchedAmount, price}; } std::optional MarketOrderBook::computeMinPriceAtWhichAmountWouldBeSoldImmediately( @@ -367,12 +371,72 @@ MarketOrderBook::AmountPerPriceVec MarketOrderBook::computePricesAtWhichAmountWo return ret; } -std::pair MarketOrderBook::avgPriceAndMatchedVolumeTaker( +MarketOrderBook::AmountPerPriceVec MarketOrderBook::computeMatchedParts(TradeSide tradeSide, MonetaryAmount amount, + MonetaryAmount price) const { + AmountPerPriceVec ret; + const int nbOrders = _orders.size(); + const auto volumeNbDecimals = _volAndPriNbDecimals.volNbDecimals; + const std::optional integralTotalAmountOpt = amount.amount(volumeNbDecimals); + if (!integralTotalAmountOpt) { + return ret; + } + AmountType integralTotalAmount = *integralTotalAmountOpt; + const auto countAmount = [volumeNbDecimals, &ret, &integralTotalAmount, cur = amount.currencyCode()]( + MonetaryAmount price, const AmountType intAmount) { + if (intAmount < integralTotalAmount) { + ret.emplace_back(MonetaryAmount(intAmount, cur, volumeNbDecimals), price); + integralTotalAmount -= intAmount; + } else { + ret.emplace_back(MonetaryAmount(integralTotalAmount, cur, volumeNbDecimals), price); + integralTotalAmount = 0; + } + }; + switch (tradeSide) { + case TradeSide::kBuy: + for (int pos = _highestBidPricePos + 1; pos < nbOrders && integralTotalAmount > 0; ++pos) { + // amount is < 0 here + const auto linePrice = priceAt(pos); + if (price < linePrice) { + break; + } + countAmount(linePrice, -_orders[pos].amount); + } + break; + case TradeSide::kSell: + for (int pos = _lowestAskPricePos - 1; pos >= 0 && integralTotalAmount > 0; --pos) { + const auto linePrice = priceAt(pos); + if (price > linePrice) { + break; + } + countAmount(linePrice, _orders[pos].amount); + } + break; + default: + unreachable(); + } + return ret; +} + +MarketOrderBook::AmountAtPrice MarketOrderBook::avgPriceAndMatchedVolume(TradeSide tradeSide, MonetaryAmount amount, + MonetaryAmount price) const { + switch (tradeSide) { + case TradeSide::kBuy: + return avgPriceAndMatchedVolumeBuy(amount * price, price); + case TradeSide::kSell: + return avgPriceAndMatchedVolumeSell(amount, price); + default: + throw exception("Unexpected trade side {}", static_cast(tradeSide)); + } +} + +MarketOrderBook::AmountAtPrice MarketOrderBook::avgPriceAndMatchedAmountTaker( MonetaryAmount amountInBaseOrQuote) const { if (amountInBaseOrQuote.currencyCode() == _market.base()) { - return avgPriceAndMatchedVolumeTakerSell(amountInBaseOrQuote); + return avgPriceAndMatchedVolumeSell(amountInBaseOrQuote, MonetaryAmount(0, _market.quote())); } - return avgPriceAndMatchedVolumeTakerBuy(amountInBaseOrQuote); + const auto [matchedVolume, price] = avgPriceAndMatchedVolumeBuy( + amountInBaseOrQuote, MonetaryAmount(std::numeric_limits::max(), _market.quote())); + return {matchedVolume.toNeutral() * price, price}; } std::optional MarketOrderBook::computeWorstPriceForTakerAmount( @@ -546,7 +610,7 @@ std::optional MarketOrderBook::computeAvgPrice(MonetaryAmount fr CurrencyCode marketCode = _market.base(); switch (priceOptions.priceStrategy()) { case PriceStrategy::kTaker: { - auto [avgPri, avgMatchedFrom] = avgPriceAndMatchedVolumeTaker(from); + auto [avgMatchedFrom, avgPri] = avgPriceAndMatchedAmountTaker(from); if (avgMatchedFrom < from) { log::warn( "{} is too big to be matched immediately on {}, return limit price instead ({} matched amount among total " diff --git a/src/objects/test/marketorderbook_test.cpp b/src/objects/test/marketorderbook_test.cpp index fc290a7d..cd39583c 100644 --- a/src/objects/test/marketorderbook_test.cpp +++ b/src/objects/test/marketorderbook_test.cpp @@ -10,6 +10,7 @@ #include "market.hpp" #include "monetaryamount.hpp" #include "timedef.hpp" +#include "tradeside.hpp" namespace cct { namespace { @@ -99,21 +100,21 @@ TEST_F(MarketOrderBookTestCase1, ComputeMaxPriceAtWhichAmountWouldBeBoughtImmedi } 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"))); + EXPECT_EQ(marketOrderBook.avgPriceAndMatchedAmountTaker(MonetaryAmount(1000, "EUR")), + AmountAtPrice(MonetaryAmount("999.99999999998784", "EUR"), MonetaryAmount("1302.00000000000001", "EUR"))); + EXPECT_EQ(marketOrderBook.avgPriceAndMatchedAmountTaker(MonetaryAmount(5000, "EUR")), + AmountAtPrice(MonetaryAmount("4999.9999119826894", "EUR"), MonetaryAmount("1302.31755833325309", "EUR"))); + EXPECT_EQ(marketOrderBook.avgPriceAndMatchedAmountTaker(MonetaryAmount(100000, "EUR")), + AmountAtPrice(MonetaryAmount("79845.737428463776", "EUR"), MonetaryAmount("1302.94629812356546", "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))); + EXPECT_EQ(marketOrderBook.avgPriceAndMatchedAmountTaker(MonetaryAmount(24, "ETH", 2)), + AmountAtPrice(MonetaryAmount(24, "ETH", 2), MonetaryAmount(1301, "EUR"))); + EXPECT_EQ(marketOrderBook.avgPriceAndMatchedAmountTaker(MonetaryAmount(5, "ETH", 1)), + AmountAtPrice(MonetaryAmount(5, "ETH", 1), MonetaryAmount(130074, "EUR", 2))); + EXPECT_EQ(marketOrderBook.avgPriceAndMatchedAmountTaker(MonetaryAmount(4, "ETH")), + AmountAtPrice(MonetaryAmount(89, "ETH", 2), MonetaryAmount("1300.63483146067415", "EUR"))); } TEST_F(MarketOrderBookTestCase1, MoreComplexListOfPricesComputations) { @@ -168,6 +169,29 @@ TEST_F(MarketOrderBookTestCase2, ConvertQuoteAmountToBase) { EXPECT_EQ(marketOrderBook.convert(MonetaryAmount("500000", "KRW")), MonetaryAmount("8649.3845211510554", "APM")); } +TEST_F(MarketOrderBookTestCase2, ComputeMatchedPartsBuy) { + EXPECT_EQ( + marketOrderBook.computeMatchedParts(TradeSide::kBuy, MonetaryAmount(91000, "APM"), + MonetaryAmount("57.81", "KRW")), + AmountAtPriceVec({AmountAtPrice(MonetaryAmount("33.5081914157147", "APM"), MonetaryAmount("57.78", "KRW")), + AmountAtPrice(MonetaryAmount("1991.3922", "APM"), MonetaryAmount("57.8", "KRW")), + AmountAtPrice(MonetaryAmount("88975.0996085842853", "APM"), MonetaryAmount("57.81", "KRW"))})); + EXPECT_EQ(marketOrderBook.computeMatchedParts(TradeSide::kBuy, MonetaryAmount(91000, "APM"), + MonetaryAmount("57.77", "KRW")), + AmountAtPriceVec()); +} + +TEST_F(MarketOrderBookTestCase2, ComputeMatchedPartsSell) { + EXPECT_EQ(marketOrderBook.computeMatchedParts(TradeSide::kSell, MonetaryAmount(5000, "APM"), + MonetaryAmount("57.19", "KRW")), + AmountAtPriceVec({ + AmountAtPrice(MonetaryAmount("3890.879", "APM"), MonetaryAmount("57.19", "KRW")), + })); + EXPECT_EQ(marketOrderBook.computeMatchedParts(TradeSide::kSell, MonetaryAmount(91000, "APM"), + MonetaryAmount("57.23", "KRW")), + AmountAtPriceVec()); +} + class MarketOrderBookTestCase3 : public ::testing::Test { protected: TimePoint time{};