diff --git a/src/trading/common/include/market-trader-engine-context.hpp b/src/trading/common/include/market-trader-engine-context.hpp index fb3e4576..d0b0dec7 100644 --- a/src/trading/common/include/market-trader-engine-context.hpp +++ b/src/trading/common/include/market-trader-engine-context.hpp @@ -1,23 +1,51 @@ #pragma once +#include + #include "exchangeprivateapitypes.hpp" #include "monetaryamount.hpp" +#include "opened-order.hpp" #include "stringhelpers.hpp" +#include "trader-command.hpp" namespace cct { -struct MarketTraderEngineContext { +class MarketTraderEngineContext { + public: MarketTraderEngineContext(MonetaryAmount startAmountBase, MonetaryAmount startAmountQuote); - void placeNewOrder(TimePoint placedTime, MonetaryAmount volume, MonetaryAmount price, TradeSide side, + MonetaryAmount computeBuyFrom(TraderCommand traderCommand) const; + + MonetaryAmount computeSellVolume(TraderCommand traderCommand) const; + + void placeBuyOrder(TimePoint placedTime, MonetaryAmount remainingVolume, MonetaryAmount price, MonetaryAmount matchedVolume = MonetaryAmount{}); - auto totalNbOrders() const { return openedOrders.size() + closedOrders.size(); } + void placeBuyTakerOrder(TimePoint placedTime, MonetaryAmount avgPrice, MonetaryAmount matchedVolume); + + void placeSellOrder(TimePoint placedTime, MonetaryAmount remainingVolume, MonetaryAmount price, + MonetaryAmount matchedVolume = MonetaryAmount{}); + + void placeSellTakerOrder(TimePoint placedTime, MonetaryAmount avgPrice, MonetaryAmount matchedVolume); + + auto totalNbOrders() const { return _openedOrders.size() + _closedOrders.size(); } auto nextOrderId() const { return ToString(totalNbOrders()); } - MonetaryAmount availableBaseAmount; - MonetaryAmount availableQuoteAmount; - OpenedOrderVector openedOrders; - ClosedOrderVector closedOrders; + MonetaryAmount availableBaseAmount() const { return _availableBaseAmount; } + MonetaryAmount availableQuoteAmount() const { return _availableQuoteAmount; } + + std::span openedOrders() const { return _openedOrders; } + std::span closedOrders() const { return _closedOrders; } + + bool matchOrder(const OpenedOrder &matchedOrder, MonetaryAmount matchedAmount, MonetaryAmount price, + TimePoint matchedTime); + + void eraseNewlyClosedOrders(std::span newlyClosedOrders); + + private: + MonetaryAmount _availableBaseAmount; + MonetaryAmount _availableQuoteAmount; + OpenedOrderVector _openedOrders; + ClosedOrderVector _closedOrders; }; } // namespace cct \ No newline at end of file diff --git a/src/trading/common/include/market-trader-engine.hpp b/src/trading/common/include/market-trader-engine.hpp index bfa81f22..b772e7ec 100644 --- a/src/trading/common/include/market-trader-engine.hpp +++ b/src/trading/common/include/market-trader-engine.hpp @@ -28,7 +28,7 @@ class MarketTraderEngine { void buy(const MarketOrderBook &marketOrderBook, TraderCommand traderCommand); void sell(const MarketOrderBook &marketOrderBook, TraderCommand traderCommand); - void matchOpenedOrdersWith(const MarketOrderBook &marketOrderBook); + void checkOpenedOrdersMatching(const MarketOrderBook &marketOrderBook); AbstractMarketTrader *_pMarketTrader = nullptr; Market _market; diff --git a/src/trading/common/src/market-trader-engine-context.cpp b/src/trading/common/src/market-trader-engine-context.cpp index af4a4583..9203f055 100644 --- a/src/trading/common/src/market-trader-engine-context.cpp +++ b/src/trading/common/src/market-trader-engine-context.cpp @@ -1,14 +1,79 @@ #include "market-trader-engine-context.hpp" +#include "monetaryamount.hpp" +#include "timedef.hpp" +#include "trader-command.hpp" #include "tradeside.hpp" namespace cct { MarketTraderEngineContext::MarketTraderEngineContext(MonetaryAmount startAmountBase, MonetaryAmount startAmountQuote) - : availableBaseAmount(startAmountBase), availableQuoteAmount(startAmountQuote) {} + : _availableBaseAmount(startAmountBase), _availableQuoteAmount(startAmountQuote) {} -void MarketTraderEngineContext::placeNewOrder(TimePoint placedTime, MonetaryAmount volume, MonetaryAmount price, - TradeSide side, MonetaryAmount matchedVolume) { - openedOrders.emplace_back(nextOrderId(), matchedVolume, volume, price, placedTime, side); +MonetaryAmount MarketTraderEngineContext::computeBuyFrom(TraderCommand traderCommand) const { + return (_availableQuoteAmount * traderCommand.amountIntensityPercentage()) / 100; +} + +MonetaryAmount MarketTraderEngineContext::computeSellVolume(TraderCommand traderCommand) const { + return (_availableBaseAmount * traderCommand.amountIntensityPercentage()) / 100; +} + +void MarketTraderEngineContext::placeBuyOrder(TimePoint placedTime, MonetaryAmount remainingVolume, + MonetaryAmount price, MonetaryAmount matchedVolume) { + _availableQuoteAmount -= matchedVolume.toNeutral() * price; + _openedOrders.emplace_back(nextOrderId(), matchedVolume, remainingVolume, price, placedTime, TradeSide::kBuy); + if (remainingVolume == 0) { + _openedOrders.emplace_back(nextOrderId(), matchedVolume, remainingVolume, price, placedTime, TradeSide::kBuy); + } else { + _closedOrders.emplace_back(nextOrderId(), matchedVolume, price, placedTime, placedTime, TradeSide::kBuy); + } +} + +void MarketTraderEngineContext::placeBuyTakerOrder(TimePoint placedTime, MonetaryAmount avgPrice, + MonetaryAmount matchedVolume) { + _availableQuoteAmount -= matchedVolume.toNeutral() * avgPrice; + _closedOrders.emplace_back(nextOrderId(), matchedVolume, avgPrice, placedTime, placedTime, TradeSide::kBuy); +} + +void MarketTraderEngineContext::placeSellOrder(TimePoint placedTime, MonetaryAmount remainingVolume, + MonetaryAmount price, MonetaryAmount matchedVolume) { + _availableBaseAmount -= matchedVolume; + _openedOrders.emplace_back(nextOrderId(), matchedVolume, remainingVolume, price, placedTime, TradeSide::kSell); +} + +void MarketTraderEngineContext::placeSellTakerOrder(TimePoint placedTime, MonetaryAmount avgPrice, + MonetaryAmount matchedVolume) { + _availableBaseAmount -= matchedVolume; + _closedOrders.emplace_back(nextOrderId(), matchedVolume, avgPrice, placedTime, placedTime, TradeSide::kSell); +} + +bool MarketTraderEngineContext::matchOrder(const OpenedOrder &matchedOrder, MonetaryAmount matchedAmount, + MonetaryAmount price, TimePoint matchedTime) { + _closedOrders.emplace_back(matchedOrder.id(), matchedAmount, price, matchedOrder.placedTime(), matchedTime, + matchedOrder.side()); + + if (matchedOrder.remainingVolume() == matchedAmount) { + // no more opened order, we can mark it for remove from the opened orders list + return true; + } + + // Adjust remaining amount of the opened order + auto openedOrderIt = std::ranges::find_if( + _openedOrders, [&matchedOrder](const auto &openedOrder) { return matchedOrder.id() == openedOrder.id(); }); + + *openedOrderIt = OpenedOrder(matchedOrder.id(), matchedOrder.matchedVolume() + matchedAmount, + matchedOrder.remainingVolume() - matchedAmount, matchedOrder.price(), + matchedOrder.placedTime(), matchedOrder.side()); + + return false; +} + +void MarketTraderEngineContext::eraseNewlyClosedOrders(std::span newlyClosedOrders) { + const auto [first, last] = std::ranges::remove_if(_openedOrders, [newlyClosedOrders](const OpenedOrder &openedOrder) { + return std::ranges::any_of(newlyClosedOrders, [&openedOrder](const auto &newlyClosedOrder) { + return openedOrder.id() == newlyClosedOrder.id(); + }); + }); + _openedOrders.erase(first, last); } } // namespace cct \ No newline at end of file diff --git a/src/trading/common/src/market-trader-engine.cpp b/src/trading/common/src/market-trader-engine.cpp index 19692bd6..e7fe6c70 100644 --- a/src/trading/common/src/market-trader-engine.cpp +++ b/src/trading/common/src/market-trader-engine.cpp @@ -35,6 +35,9 @@ void MarketTraderEngine::tradeRange(std::span marketOrder auto tradesBeg = trades.begin(); for (const MarketOrderBook &marketOrderBook : marketOrderBooks) { + // First check opened orders status with new market order book data that may match some + checkOpenedOrdersMatching(marketOrderBook); + // We expect market data (order books and trades) to be sorted by time const auto marketOrderBookTs = marketOrderBook.time(); @@ -76,27 +79,28 @@ void MarketTraderEngine::placeCommand(const MarketOrderBook &marketOrderBook, Tr } void MarketTraderEngine::buy(const MarketOrderBook &marketOrderBook, TraderCommand traderCommand) { - MonetaryAmount from = - (_marketTraderEngineContext.availableQuoteAmount * traderCommand.amountIntensityPercentage()) / 100; + const MonetaryAmount from = _marketTraderEngineContext.computeBuyFrom(traderCommand); switch (traderCommand.priceStrategy()) { case PriceStrategy::kMaker: { - MonetaryAmount price = marketOrderBook.highestBidPrice(); - MonetaryAmount volume(from / price, _market.base()); + const MonetaryAmount price = marketOrderBook.highestBidPrice(); + const MonetaryAmount volume(from / price, _market.base()); - _marketTraderEngineContext.placeNewOrder(marketOrderBook.time(), volume, price, TradeSide::kBuy); + _marketTraderEngineContext.placeBuyOrder(marketOrderBook.time(), volume, price); break; } case PriceStrategy::kNibble: { - MonetaryAmount price = marketOrderBook.lowestAskPrice(); - MonetaryAmount volume(from / price, _market.base()); - MonetaryAmount matchedVolume = std::min(marketOrderBook.amountAtAskPrice(), volume); + const MonetaryAmount price = marketOrderBook.lowestAskPrice(); + const MonetaryAmount volume(from / price, _market.base()); + const MonetaryAmount matchedVolume = std::min(marketOrderBook.amountAtAskPrice(), volume); - _marketTraderEngineContext.placeNewOrder(marketOrderBook.time(), volume - matchedVolume, price, TradeSide::kBuy, - matchedVolume); + _marketTraderEngineContext.placeBuyOrder(marketOrderBook.time(), volume - matchedVolume, price, matchedVolume); break; } case PriceStrategy::kTaker: { + const auto [totalMatchedAmount, avgPrice] = marketOrderBook.avgPriceAndMatchedAmountTaker(from); + + _marketTraderEngineContext.placeBuyTakerOrder(marketOrderBook.time(), avgPrice, totalMatchedAmount); break; } default: @@ -105,55 +109,43 @@ void MarketTraderEngine::buy(const MarketOrderBook &marketOrderBook, TraderComma } void MarketTraderEngine::sell(const MarketOrderBook &marketOrderBook, TraderCommand traderCommand) { - MonetaryAmount volume = - (_marketTraderEngineContext.availableBaseAmount * traderCommand.amountIntensityPercentage()) / 100; + const MonetaryAmount volume = _marketTraderEngineContext.computeSellVolume(traderCommand); switch (traderCommand.priceStrategy()) { case PriceStrategy::kMaker: { - MonetaryAmount price = marketOrderBook.lowestAskPrice(); + const MonetaryAmount price = marketOrderBook.lowestAskPrice(); - _marketTraderEngineContext.placeNewOrder(marketOrderBook.time(), volume, price, TradeSide::kSell); + _marketTraderEngineContext.placeSellOrder(marketOrderBook.time(), volume, price); break; } case PriceStrategy::kNibble: { - MonetaryAmount price = marketOrderBook.highestBidPrice(); - MonetaryAmount matchedVolume = std::min(marketOrderBook.amountAtBidPrice(), volume); + const MonetaryAmount price = marketOrderBook.highestBidPrice(); + const MonetaryAmount matchedVolume = std::min(marketOrderBook.amountAtBidPrice(), volume); - _marketTraderEngineContext.placeNewOrder(marketOrderBook.time(), volume - matchedVolume, price, TradeSide::kSell, - matchedVolume); + _marketTraderEngineContext.placeSellOrder(marketOrderBook.time(), volume - matchedVolume, price, matchedVolume); break; } - case PriceStrategy::kTaker: + case PriceStrategy::kTaker: { + const auto [totalMatchedAmount, avgPrice] = marketOrderBook.avgPriceAndMatchedAmountTaker(volume); + + _marketTraderEngineContext.placeSellTakerOrder(marketOrderBook.time(), avgPrice, totalMatchedAmount); break; + } default: unreachable(); } } -void MarketTraderEngine::matchOpenedOrdersWith(const MarketOrderBook &marketOrderBook) { +void MarketTraderEngine::checkOpenedOrdersMatching(const MarketOrderBook &marketOrderBook) { _newlyClosedOrders.clear(); - for (OpenedOrder &openedOrder : _marketTraderEngineContext.openedOrders) { - const auto matchedParts = - marketOrderBook.computeMatchedParts(openedOrder.side(), openedOrder.remainingVolume(), openedOrder.price()); - - for (const auto &[price, amount] : matchedParts) { - _marketTraderEngineContext.closedOrders.emplace_back(openedOrder.id(), amount, price, openedOrder.placedTime(), - marketOrderBook.time(), openedOrder.side()); - - if (openedOrder.remainingVolume() == amount) { - _newlyClosedOrders.push_back(openedOrder); - } else { - openedOrder = - OpenedOrder(openedOrder.id(), openedOrder.matchedVolume() + amount, openedOrder.remainingVolume() - amount, - openedOrder.price(), openedOrder.placedTime(), openedOrder.side()); - } + for (const OpenedOrder &openedOrder : _marketTraderEngineContext.openedOrders()) { + const auto [matchedVolume, avgPrice] = marketOrderBook.avgPriceAndMatchedVolume( + openedOrder.side(), openedOrder.remainingVolume(), openedOrder.price()); + + if (_marketTraderEngineContext.matchOrder(openedOrder, matchedVolume, avgPrice, marketOrderBook.time())) { + _newlyClosedOrders.push_back(openedOrder); } } - const auto [first, last] = - std::ranges::remove_if(_marketTraderEngineContext.openedOrders, [this](const OpenedOrder &openedOrder) { - return std::ranges::any_of(_newlyClosedOrders, [&openedOrder](const auto &newlyClosedOrder) { - return openedOrder.id() == newlyClosedOrder.id(); - }); - }); - _marketTraderEngineContext.openedOrders.erase(first, last); + + _marketTraderEngineContext.eraseNewlyClosedOrders(_newlyClosedOrders); } } // namespace cct \ No newline at end of file