From 9fba71ed9f3b744eee83ae3b5d07de80ede0f233 Mon Sep 17 00:00:00 2001 From: Stanislav Khrapov Date: Wed, 1 Jul 2026 20:57:59 +0200 Subject: [PATCH 1/2] Fix non-removal of filled orders from orders_by_expiration --- src/order_matching/matching_engine.py | 5 ++ tests/test_matching_engine.py | 91 +++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/src/order_matching/matching_engine.py b/src/order_matching/matching_engine.py index 3befa84..32f1d11 100644 --- a/src/order_matching/matching_engine.py +++ b/src/order_matching/matching_engine.py @@ -106,6 +106,11 @@ def _execute_trades_for_one_price(self, incoming_order: Order, price: float) -> if book_order.size == 0: zero_size_orders.append(book_order) opposite_side_orders[price].remove(orders=zero_size_orders) + for book_order in zero_size_orders: + expiration_orders = self.unprocessed_orders.orders_by_expiration[book_order.expiration] + expiration_orders.remove(orders=[book_order]) + if len(expiration_orders) == 0: + self.unprocessed_orders.orders_by_expiration.pop(book_order.expiration) if len(list(filter(lambda order: order.size > 0, opposite_side_orders[price]))) == 0: opposite_side_orders.pop(price) return ExecutedTrades(trades=trades) diff --git a/tests/test_matching_engine.py b/tests/test_matching_engine.py index fa9f669..7a45aba 100644 --- a/tests/test_matching_engine.py +++ b/tests/test_matching_engine.py @@ -744,6 +744,97 @@ def test_cancellation_of_expired_orders(self) -> None: assert matching_engine.unprocessed_orders.bids == dict() assert matching_engine.unprocessed_orders.offers == dict() + def test_removal_of_filled_orders(self) -> None: + matching_engine = MatchingEngine(seed=1) + t0 = datetime(2023, 1, 1) + buy = LimitOrder(side=Side.BUY, price=100.0, size=10, timestamp=t0, order_id="A", trader_id="x") + sell = LimitOrder( + side=Side.SELL, price=100.0, size=10, timestamp=t0 + timedelta(seconds=1), order_id="B", trader_id="y" + ) + matching_engine.match(timestamp=t0, orders=Orders([buy])) + matching_engine.match(timestamp=t0 + timedelta(seconds=1), orders=Orders([sell])) # "A" is fully filled + + order_book = matching_engine.unprocessed_orders + + assert order_book.bids == {} + assert order_book.offers == {} + assert order_book.orders_by_expiration == {} + + def test_live_order_eviction(self) -> None: + matching_engine = MatchingEngine(seed=1) + timestamp = datetime(2023, 1, 1) + expiration1 = timestamp + timedelta(hours=1) + + # 1) rest "X" @100 with a finite expiration, then fully fill it + matching_engine.match( + timestamp=timestamp, + orders=Orders( + [ + LimitOrder( + side=Side.BUY, + price=100.0, + size=10, + timestamp=timestamp, + order_id="X", + trader_id="x", + expiration=expiration1, + ) + ] + ), + ) + matching_engine.match( + timestamp=timestamp + timedelta(seconds=1), + orders=Orders( + [ + LimitOrder( + side=Side.SELL, + price=100.0, + size=10, + timestamp=timestamp + timedelta(seconds=1), + order_id="B", + trader_id="y", + ) + ] + ), + ) + + # 2) a brand-new LIVE order re-uses id "X" at the same price (default far expiration) + matching_engine.match( + timestamp=timestamp + timedelta(seconds=2), + orders=Orders( + [ + LimitOrder( + side=Side.BUY, + price=100.0, + size=5, + timestamp=timestamp + timedelta(seconds=2), + order_id="X", + trader_id="z", + ) + ] + ), + ) + + # 3) advance time past expiration1 with any order -> expiry sweep fires + matching_engine.match( + timestamp=timestamp + timedelta(hours=2), + orders=Orders( + [ + LimitOrder( + side=Side.SELL, + price=999.0, + size=1, + timestamp=timestamp + timedelta(hours=2), + order_id="D", + trader_id="w", + ) + ] + ), + ) + + # the live re-used-id "X" was evicted; expected ['X'] + assert [o.order_id for v in matching_engine.unprocessed_orders.bids.values() for o in v] == ["X"] + def test_matching_with_benchmark(self, random_orders: Orders, benchmark: BenchmarkFixture) -> None: order_book = MatchingEngine() benchmark(order_book.match, orders=random_orders, timestamp=random_orders.orders[-1].timestamp) From 6731eb1d8b2b67a6f5e8253ffbd88d6f64d4f353 Mon Sep 17 00:00:00 2001 From: Stanislav Khrapov Date: Wed, 1 Jul 2026 21:08:50 +0200 Subject: [PATCH 2/2] reformat --- tests/test_matching_engine.py | 76 +++++++++-------------------------- 1 file changed, 18 insertions(+), 58 deletions(-) diff --git a/tests/test_matching_engine.py b/tests/test_matching_engine.py index 7a45aba..c410b36 100644 --- a/tests/test_matching_engine.py +++ b/tests/test_matching_engine.py @@ -763,74 +763,34 @@ def test_removal_of_filled_orders(self) -> None: def test_live_order_eviction(self) -> None: matching_engine = MatchingEngine(seed=1) timestamp = datetime(2023, 1, 1) - expiration1 = timestamp + timedelta(hours=1) + expiration = timestamp + timedelta(hours=1) # 1) rest "X" @100 with a finite expiration, then fully fill it - matching_engine.match( - timestamp=timestamp, - orders=Orders( - [ - LimitOrder( - side=Side.BUY, - price=100.0, - size=10, - timestamp=timestamp, - order_id="X", - trader_id="x", - expiration=expiration1, - ) - ] - ), + order = LimitOrder( + side=Side.BUY, price=100.0, size=10, timestamp=timestamp, order_id="X", trader_id="x", expiration=expiration ) - matching_engine.match( + matching_engine.match(timestamp=timestamp, orders=Orders(orders=[order])) + order = LimitOrder( + side=Side.SELL, + price=100.0, + size=10, timestamp=timestamp + timedelta(seconds=1), - orders=Orders( - [ - LimitOrder( - side=Side.SELL, - price=100.0, - size=10, - timestamp=timestamp + timedelta(seconds=1), - order_id="B", - trader_id="y", - ) - ] - ), + order_id="B", + trader_id="y", ) + matching_engine.match(timestamp=timestamp + timedelta(seconds=1), orders=Orders(orders=[order])) # 2) a brand-new LIVE order re-uses id "X" at the same price (default far expiration) - matching_engine.match( - timestamp=timestamp + timedelta(seconds=2), - orders=Orders( - [ - LimitOrder( - side=Side.BUY, - price=100.0, - size=5, - timestamp=timestamp + timedelta(seconds=2), - order_id="X", - trader_id="z", - ) - ] - ), + order = LimitOrder( + side=Side.BUY, price=100.0, size=5, timestamp=timestamp + timedelta(seconds=2), order_id="X", trader_id="z" ) + matching_engine.match(timestamp=timestamp + timedelta(seconds=2), orders=Orders(orders=[order])) - # 3) advance time past expiration1 with any order -> expiry sweep fires - matching_engine.match( - timestamp=timestamp + timedelta(hours=2), - orders=Orders( - [ - LimitOrder( - side=Side.SELL, - price=999.0, - size=1, - timestamp=timestamp + timedelta(hours=2), - order_id="D", - trader_id="w", - ) - ] - ), + # 3) advance time past expiration with any order -> expiry sweep fires + order = LimitOrder( + side=Side.SELL, price=999.0, size=1, timestamp=timestamp + timedelta(hours=2), order_id="D", trader_id="w" ) + matching_engine.match(timestamp=timestamp + timedelta(hours=2), orders=Orders(orders=[order])) # the live re-used-id "X" was evicted; expected ['X'] assert [o.order_id for v in matching_engine.unprocessed_orders.bids.values() for o in v] == ["X"]