Skip to content

Commit

Permalink
Improve market order book computation of average price for taker buy …
Browse files Browse the repository at this point in the history
…/ sell
  • Loading branch information
sjanel committed Mar 5, 2024
1 parent f8c8612 commit 4d12e3e
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 71 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());
MonetaryAmount pri(marketOrderBook1.computeAvgPriceForTakerAmount(from).value_or(MonetaryAmount{-1}));
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());
MonetaryAmount pri(marketOrderBook1.computeAvgPriceForTakerAmount(from).value_or(MonetaryAmount{-1}));
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();

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"));
Expand Down
10 changes: 5 additions & 5 deletions src/objects/include/marketorderbook.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<MonetaryAmount> 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<MonetaryAmount, MonetaryAmount> 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.
Expand Down Expand Up @@ -209,9 +209,9 @@ class MarketOrderBook {
return MonetaryAmount(_orders[pos].price, _market.quote(), _volAndPriNbDecimals.priNbDecimals);
}

std::optional<MonetaryAmount> computeAvgPriceAtWhichAmountWouldBeSoldImmediately(MonetaryAmount ma) const;
std::pair<MonetaryAmount, MonetaryAmount> avgPriceAndMatchedVolumeTakerSell(MonetaryAmount baseAmount) const;

std::optional<MonetaryAmount> computeAvgPriceAtWhichAmountWouldBeBoughtImmediately(MonetaryAmount ma) const;
std::pair<MonetaryAmount, MonetaryAmount> 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.
Expand Down
119 changes: 62 additions & 57 deletions src/objects/src/marketorderbook.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -264,28 +264,55 @@ MarketOrderBook::AmountPerPriceVec MarketOrderBook::computePricesAtWhichAmountWo
return ret;
}

namespace {
inline std::optional<MonetaryAmount> 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<MonetaryAmount, MonetaryAmount> 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<MonetaryAmount> MarketOrderBook::computeAvgPriceAtWhichAmountWouldBeBoughtImmediately(
MonetaryAmount ma) const {
return ComputeAvgPrice(_market, computePricesAtWhichAmountWouldBeBoughtImmediately(ma));
std::pair<MonetaryAmount, MonetaryAmount> 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<MonetaryAmount> MarketOrderBook::computeMinPriceAtWhichAmountWouldBeSoldImmediately(
Expand Down Expand Up @@ -340,34 +367,12 @@ MarketOrderBook::AmountPerPriceVec MarketOrderBook::computePricesAtWhichAmountWo
return ret;
}

std::optional<MonetaryAmount> MarketOrderBook::computeAvgPriceAtWhichAmountWouldBeSoldImmediately(
MonetaryAmount ma) const {
return ComputeAvgPrice(_market, computePricesAtWhichAmountWouldBeSoldImmediately(ma));
}

std::optional<MonetaryAmount> MarketOrderBook::computeAvgPriceForTakerAmount(MonetaryAmount amountInBaseOrQuote) const {
std::pair<MonetaryAmount, MonetaryAmount> 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<MonetaryAmount> MarketOrderBook::computeWorstPriceForTakerAmount(
Expand Down Expand Up @@ -428,14 +433,12 @@ std::optional<MonetaryAmount> 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 {};
Expand Down Expand Up @@ -543,12 +546,14 @@ std::optional<MonetaryAmount> MarketOrderBook::computeAvgPrice(MonetaryAmount fr
CurrencyCode marketCode = _market.base();
switch (priceOptions.priceStrategy()) {
case PriceStrategy::kTaker: {
std::optional<MonetaryAmount> 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();
Expand Down
22 changes: 16 additions & 6 deletions src/objects/test/marketorderbook_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down

0 comments on commit 4d12e3e

Please sign in to comment.