Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve market order book computation of average price for taker buy … #521

Merged
merged 1 commit into from
Mar 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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