feat: add UserID to orders and implement self-trade prevention#25
Draft
Copilot wants to merge 4 commits into
Draft
feat: add UserID to orders and implement self-trade prevention#25Copilot wants to merge 4 commits into
Copilot wants to merge 4 commits into
Conversation
Agent-Logs-Url: https://github.com/geseq/cpp-orderbook/sessions/1375c9ee-a4fa-4c9c-8c02-e7cb3b2eb6a6 Co-authored-by: geseq <5458743+geseq@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR extends the core order model to carry a UserID and threads it through the matching engine to implement self-trade prevention (STP), including new tests that validate STP behavior for limit/market orders.
Changes:
- Added
UserIDtype plus newExecutionReportfields for order/trade user attribution. - Added
user_idtoOrderand updated matching pipeline (OrderBook→PriceLevel→OrderQueue) to skip self-matches whenuser_id != 0. - Updated test suite and added new STP-specific test cases.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| include/types.hpp | Introduces UserID and adds user-id fields to ExecutionReport. |
| src/types.cpp | Adds stringification for the new Error::SelfTrade enum value. |
| include/order.hpp | Adds user_id to Order and updates its constructor. |
| include/orderqueue.hpp | Extends TradeNotification and OrderQueue::process() to include maker/taker user IDs. |
| src/orderqueue.cpp | Implements STP skipping inside the per-price FIFO queue matching loop. |
| include/pricelevel.hpp | Threads taker UserID through price-level processing APIs. |
| src/pricelevel.cpp | Updates limit/market processing loops to avoid infinite loops when STP causes “no progress”. |
| include/orderbook.hpp | Updates addOrder/processOrder/putTradeNotification/eraseOrder to carry and report UserID. |
| main.cpp | Updates benchmark harness to pass user_id=0. |
| test/util.cpp | Imports UserID into the test notification utility compilation unit. |
| test/orderqueue_test.cpp | Updates tests for new process() and TradeNotification signatures. |
| test/pricelevel_test.cpp | Updates order construction to include user_id. |
| test/determinism_test.cpp | Adds user_id to actions and threads it into addOrder. |
| test/orderbook_test.cpp | Extends input parsing with optional user_id column and adds STP test cases. |
Comments suppressed due to low confidence (2)
src/pricelevel.cpp:82
- AoN/FoK pre-check compares requested qty against volume_, but with self-trade prevention enabled (takerUserID != 0) volume_ can include quantity that will be skipped (same-user resting orders). This can allow partial fills on FoK/AoN orders when only self-volume makes the pre-check pass. Compute the available volume excluding maker.user_id == takerUserID (or otherwise make the all-or-nothing checks STP-aware) before allowing any fills.
Decimal PriceLevel<P>::processMarketOrder(const TradeNotification& tn, const PostOrderFill& pf, OrderID takerOrderID, UserID takerUserID, Decimal qty, Flag flag) {
static_assert(Q == PriceType::Bid || Q == PriceType::Ask, "Unsupported PriceType");
if ((flag & (AoN | FoK)) != 0 && qty > volume_) {
// AoN/FoK must match the full requested quantity; return zero processed when aggregate volume is insufficient.
return Decimal{};
}
include/orderbook.hpp:216
- Trade reports now carry maker_user_id/taker_user_id, but the unit tests only assert stringified output that omits these fields, so the new UserID plumbing isn't actually verified. Add at least one test assertion that inspects ExecutionReport.maker_user_id/taker_user_id (and order-event user_id) to prevent regressions.
void OrderBook<Notification>::putTradeNotification(OrderID mOrderID, OrderID tOrderID, UserID mUserID, UserID tUserID, OrderStatus mStatus, OrderStatus tStatus, Decimal qty, Decimal price) {
notification_.onExecutionReport(ExecutionReport{
.exec_type = ExecType::Trade,
.maker_order_id = mOrderID,
.taker_order_id = tOrderID,
.maker_user_id = mUserID,
.taker_user_id = tUserID,
.maker_status = mStatus,
.taker_status = tStatus,
.last_qty = qty,
.last_price = price,
});
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
58
to
65
| ++it; | ||
| // Zero maker quantity before postFill so callbacks/removal observe a fully filled maker order. | ||
| ho->qty = Decimal{}; | ||
| postFill(makerOrderID); | ||
| // qty has already been decremented by matchedQty, so zero means taker is fully filled. | ||
| const auto takerStatus = qty.is_zero() ? OrderStatus::FilledComplete : OrderStatus::FilledPartial; | ||
| tradeNotification(makerOrderID, takerOrderID, OrderStatus::FilledComplete, takerStatus, matchedQty, makerPrice); | ||
| tradeNotification(makerOrderID, takerOrderID, makerUserID, takerUserID, OrderStatus::FilledComplete, takerStatus, matchedQty, makerPrice); | ||
| } |
Comment on lines
+167
to
+176
| Decimal result = orderQueue->process(tn, pf, takerOrderID, takerUserID, qtyLeft); | ||
| if (!result.is_zero()) { | ||
| qtyLeft -= result; | ||
| qtyProcessed += result; | ||
| volume_ -= result; | ||
| orderQueue = getQueue(); | ||
| } else { | ||
| // No progress at this price level (all STP); advance to next queue. | ||
| orderQueue = getNextQueue<Q>(orderQueue->price()); | ||
| } |
Agent-Logs-Url: https://github.com/geseq/cpp-orderbook/sessions/75a9278b-a5dd-4bd9-9aaa-cdb283e81ef8 Co-authored-by: geseq <5458743+geseq@users.noreply.github.com>
…re destruction Agent-Logs-Url: https://github.com/geseq/cpp-orderbook/sessions/a22b1f79-c919-4859-a1fe-6f0c63d80392 Co-authored-by: geseq <5458743+geseq@users.noreply.github.com>
Agent-Logs-Url: https://github.com/geseq/cpp-orderbook/sessions/5e5b3b79-7a18-4d70-a4e1-6b53b05b9084 Co-authored-by: geseq <5458743+geseq@users.noreply.github.com>
Copilot stopped work on behalf of
geseq due to an error
May 12, 2026 04:17
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
UserIDtype andSelfTradeerror totypes.hpp, updateExecutionReportwith user ID fieldsuser_idfield toOrderstruct and update constructorTradeNotificationsignature andOrderQueue::process()to skip self-trade orders (STP applies only whenuser_id != 0)PriceLevelmethod signatures and implementations to threadUserID, handle STP queue advancementUserIDthroughOrderBook(addOrder,processOrder,putTradeNotification,cancelOrder/eraseOrder)Orderconstructors,TradeNotificationlambdas,processLine(optional 7th column for user_id), add 5 STP test cases#include <tuple>toorderbook.hppTestPriceFinding: replace rawnew Order(...)withstd::unique_ptr<Order>in a vectoravailableQty(UserID)toOrderQueue(header + impl) — returns non-self qtyprocessMarketOrderAoN/FoK pre-check to use STP-aware volumeprocessLimitOrderAoN/FoK canFill loop to useavailableQtyinstead oftotalQty