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 7, 2024
1 parent f8d6f4c commit 5ed3483
Show file tree
Hide file tree
Showing 5 changed files with 143 additions and 46 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.avgPriceAndMatchedAmountTaker(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.avgPriceAndMatchedAmountTaker(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.avgPriceAndMatchedAmountTaker(from);
MonetaryAmount vol2(from / pri2, market.base());

PlaceOrderInfo matchedPlacedOrderInfo2(OrderInfo(TradedAmounts(from, vol2), true), OrderId("Order # 1"));
Expand Down
17 changes: 13 additions & 4 deletions src/objects/include/marketorderbook.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include "monetaryamount.hpp"
#include "simpletable.hpp"
#include "timedef.hpp"
#include "tradeside.hpp"
#include "volumeandpricenbdecimals.hpp"

namespace cct {
Expand Down Expand Up @@ -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<MonetaryAmount, MonetaryAmount> 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.
Expand Down Expand Up @@ -209,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
File renamed without changes.
118 changes: 91 additions & 27 deletions src/objects/src/marketorderbook.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -264,55 +264,59 @@ 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};
price = quoteAmount - remainingQuoteAmount;
if (matchedAmount != 0) {
price /= matchedAmount.toNeutral();
}
return {matchedAmount, price};
}

std::optional<MonetaryAmount> MarketOrderBook::computeMinPriceAtWhichAmountWouldBeSoldImmediately(
Expand Down Expand Up @@ -367,12 +371,72 @@ MarketOrderBook::AmountPerPriceVec MarketOrderBook::computePricesAtWhichAmountWo
return ret;
}

std::pair<MonetaryAmount, MonetaryAmount> 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<AmountType> 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<int>(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<MonetaryAmount::AmountType>::max(), _market.quote()));
return {matchedVolume.toNeutral() * price, price};
}

std::optional<MonetaryAmount> MarketOrderBook::computeWorstPriceForTakerAmount(
Expand Down Expand Up @@ -546,7 +610,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] = avgPriceAndMatchedAmountTaker(from);
if (avgMatchedFrom < from) {
log::warn(
"{} is too big to be matched immediately on {}, return limit price instead ({} matched amount among total "
Expand Down
48 changes: 36 additions & 12 deletions src/objects/test/marketorderbook_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include "market.hpp"
#include "monetaryamount.hpp"
#include "timedef.hpp"
#include "tradeside.hpp"

namespace cct {
namespace {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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{};
Expand Down

0 comments on commit 5ed3483

Please sign in to comment.