Skip to content

feat: add UserID to orders and implement self-trade prevention#25

Draft
Copilot wants to merge 4 commits into
mainfrom
copilot/add-user-id-and-self-trade-prevention
Draft

feat: add UserID to orders and implement self-trade prevention#25
Copilot wants to merge 4 commits into
mainfrom
copilot/add-user-id-and-self-trade-prevention

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented May 12, 2026

  • Explore codebase and understand structure
  • Add UserID type and SelfTrade error to types.hpp, update ExecutionReport with user ID fields
  • Add user_id field to Order struct and update constructor
  • Update TradeNotification signature and OrderQueue::process() to skip self-trade orders (STP applies only when user_id != 0)
  • Update PriceLevel method signatures and implementations to thread UserID, handle STP queue advancement
  • Thread UserID through OrderBook (addOrder, processOrder, putTradeNotification, cancelOrder/eraseOrder)
  • Update tests: fix Order constructors, TradeNotification lambdas, processLine (optional 7th column for user_id), add 5 STP test cases
  • Add missing #include <tuple> to orderbook.hpp
  • Fix TestPriceFinding: replace raw new Order(...) with std::unique_ptr<Order> in a vector
  • Add failing tests: AoN/FoK with STP where self-liquidity causes pre-check to incorrectly pass
  • Add availableQty(UserID) to OrderQueue (header + impl) — returns non-self qty
  • Fix processMarketOrder AoN/FoK pre-check to use STP-aware volume
  • Fix processLimitOrder AoN/FoK canFill loop to use availableQty instead of totalQty
  • Build succeeds, all test suites pass (100%)

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 UserID type plus new ExecutionReport fields for order/trade user attribution.
  • Added user_id to Order and updated matching pipeline (OrderBookPriceLevelOrderQueue) to skip self-matches when user_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 thread src/orderqueue.cpp
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 thread include/orderbook.hpp
Comment thread test/pricelevel_test.cpp Outdated
Comment thread src/pricelevel.cpp
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/5e5b3b79-7a18-4d70-a4e1-6b53b05b9084

Co-authored-by: geseq <5458743+geseq@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants