Skip to content

Commit

Permalink
[Feature] - Add new services to avg match amounts at price for market…
Browse files Browse the repository at this point in the history
… order book
  • Loading branch information
sjanel committed Mar 6, 2024
1 parent 9440acf commit ed1c20a
Show file tree
Hide file tree
Showing 4 changed files with 60 additions and 40 deletions.
6 changes: 3 additions & 3 deletions src/api/common/test/exchangeprivateapi_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ TEST_F(ExchangePrivateTest, TakerTradeQuoteToBase) {
tradeBaseExpectCalls();

MonetaryAmount from(5000, market.quote());
auto [pri, _] = marketOrderBook1.avgPriceAndMatchedVolumeTaker(from);
auto [_, pri] = marketOrderBook1.avgPriceAndMatchedVolumeTaker(from);

MonetaryAmount vol(from / pri, market.base());
PriceOptions priceOptions(PriceStrategy::kTaker);
Expand All @@ -166,7 +166,7 @@ TEST_F(ExchangePrivateTest, TradeAsyncPolicyTaker) {
tradeBaseExpectCalls();

MonetaryAmount from(5000, market.quote());
auto [pri, _] = marketOrderBook1.avgPriceAndMatchedVolumeTaker(from);
auto [_, pri] = marketOrderBook1.avgPriceAndMatchedVolumeTaker(from);

MonetaryAmount vol(from / pri, market.base());
PriceOptions priceOptions(PriceStrategy::kTaker);
Expand Down Expand Up @@ -387,7 +387,7 @@ TEST_F(ExchangePrivateTest, MakerTradeQuoteToBaseEmergencyTakerTrade) {
// Place taker order
tradeInfo.options.switchToTakerStrategy();

auto [pri2, _] = marketOrderBook1.avgPriceAndMatchedVolumeTaker(from);
auto [_, pri2] = marketOrderBook1.avgPriceAndMatchedVolumeTaker(from);
MonetaryAmount vol2(from / pri2, market.base());

PlaceOrderInfo matchedPlacedOrderInfo2(OrderInfo(TradedAmounts(from, vol2), true), OrderId("Order # 1"));
Expand Down
14 changes: 10 additions & 4 deletions src/objects/include/marketorderbook.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,16 @@ 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<MonetaryAmount, MonetaryAmount> avgPriceAndMatchedVolumeTaker(MonetaryAmount amountInBaseOrQuote) const;
/// @return a pair of {total matched amount in base currency, average matched price}
AmountAtPrice avgPriceAndMatchedVolumeTaker(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
Expand Down Expand Up @@ -212,9 +218,9 @@ class MarketOrderBook {
return MonetaryAmount(_orders[pos].price, _market.quote(), _volAndPriNbDecimals.priNbDecimals);
}

std::pair<MonetaryAmount, MonetaryAmount> avgPriceAndMatchedVolumeTakerSell(MonetaryAmount baseAmount) const;
AmountAtPrice avgPriceAndMatchedVolumeSell(MonetaryAmount baseAmount, MonetaryAmount price) const;

std::pair<MonetaryAmount, MonetaryAmount> 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.
Expand Down
68 changes: 41 additions & 27 deletions src/objects/src/marketorderbook.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -264,55 +264,56 @@ MarketOrderBook::AmountPerPriceVec MarketOrderBook::computePricesAtWhichAmountWo
return ret;
}

std::pair<MonetaryAmount, MonetaryAmount> 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<MonetaryAmount, MonetaryAmount> 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};
MonetaryAmount matchedQuoteAmount = quoteAmount - remainingQuoteAmount;
return {matchedAmount, matchedQuoteAmount / matchedAmount.toNeutral()};
}

std::optional<MonetaryAmount> MarketOrderBook::computeMinPriceAtWhichAmountWouldBeSoldImmediately(
Expand Down Expand Up @@ -413,12 +414,25 @@ MarketOrderBook::AmountPerPriceVec MarketOrderBook::computeMatchedParts(TradeSid
return ret;
}

std::pair<MonetaryAmount, MonetaryAmount> MarketOrderBook::avgPriceAndMatchedVolumeTaker(
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<int>(tradeSide));
}
}

MarketOrderBook::AmountAtPrice MarketOrderBook::avgPriceAndMatchedVolumeTaker(
MonetaryAmount amountInBaseOrQuote) const {
if (amountInBaseOrQuote.currencyCode() == _market.base()) {
return avgPriceAndMatchedVolumeTakerSell(amountInBaseOrQuote);
return avgPriceAndMatchedVolumeSell(amountInBaseOrQuote, MonetaryAmount(0, _market.quote()));
}
return avgPriceAndMatchedVolumeTakerBuy(amountInBaseOrQuote);
return avgPriceAndMatchedVolumeBuy(
amountInBaseOrQuote, MonetaryAmount(std::numeric_limits<MonetaryAmount::AmountType>::max(), _market.quote()));
}

std::optional<MonetaryAmount> MarketOrderBook::computeWorstPriceForTakerAmount(
Expand Down Expand Up @@ -592,7 +606,7 @@ std::optional<MonetaryAmount> MarketOrderBook::computeAvgPrice(MonetaryAmount fr
CurrencyCode marketCode = _market.base();
switch (priceOptions.priceStrategy()) {
case PriceStrategy::kTaker: {
auto [avgPri, avgMatchedFrom] = avgPriceAndMatchedVolumeTaker(from);
auto [avgMatchedFrom, avgPri] = avgPriceAndMatchedVolumeTaker(from);
if (avgMatchedFrom < from) {
log::warn(
"{} is too big to be matched immediately on {}, return limit price instead ({} matched amount among total "
Expand Down
12 changes: 6 additions & 6 deletions src/objects/test/marketorderbook_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -101,20 +101,20 @@ TEST_F(MarketOrderBookTestCase1, ComputeMaxPriceAtWhichAmountWouldBeBoughtImmedi

TEST_F(MarketOrderBookTestCase1, ComputeAvgPriceForTakerBuy) {
EXPECT_EQ(marketOrderBook.avgPriceAndMatchedVolumeTaker(MonetaryAmount(1000, "EUR")),
std::make_pair(MonetaryAmount(1302, "EUR"), MonetaryAmount(1000, "EUR")));
AmountAtPrice(MonetaryAmount(1000, "EUR"), MonetaryAmount(1302, "EUR")));
EXPECT_EQ(marketOrderBook.avgPriceAndMatchedVolumeTaker(MonetaryAmount(5000, "EUR")),
std::make_pair(MonetaryAmount("1302.31755833325309", "EUR"), MonetaryAmount(5000, "EUR")));
AmountAtPrice(MonetaryAmount(5000, "EUR"), MonetaryAmount("1302.31755833325309", "EUR")));
EXPECT_EQ(marketOrderBook.avgPriceAndMatchedVolumeTaker(MonetaryAmount(100000, "EUR")),
std::make_pair(MonetaryAmount("1302.94629812356546", "EUR"), MonetaryAmount("79845.73830901", "EUR")));
AmountAtPrice(MonetaryAmount("79845.73830901", "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)));
AmountAtPrice(MonetaryAmount(24, "ETH", 2), MonetaryAmount(1301, "EUR")));
EXPECT_EQ(marketOrderBook.avgPriceAndMatchedVolumeTaker(MonetaryAmount(5, "ETH", 1)),
std::make_pair(MonetaryAmount(130074, "EUR", 2), MonetaryAmount(5, "ETH", 1)));
AmountAtPrice(MonetaryAmount(5, "ETH", 1), MonetaryAmount(130074, "EUR", 2)));
EXPECT_EQ(marketOrderBook.avgPriceAndMatchedVolumeTaker(MonetaryAmount(4, "ETH")),
std::make_pair(MonetaryAmount("289.39125", "EUR"), MonetaryAmount(89, "ETH", 2)));
AmountAtPrice(MonetaryAmount(89, "ETH", 2), MonetaryAmount("289.39125", "EUR")));
}

TEST_F(MarketOrderBookTestCase1, MoreComplexListOfPricesComputations) {
Expand Down

0 comments on commit ed1c20a

Please sign in to comment.