22
22
from nautilus_trader .backtest .models import MakerTakerFeeModel
23
23
from nautilus_trader .common .component import MessageBus
24
24
from nautilus_trader .common .component import TestClock
25
- from nautilus_trader .model .data import QuoteTick
26
25
from nautilus_trader .model .enums import AccountType
26
+ from nautilus_trader .model .enums import AggressorSide
27
27
from nautilus_trader .model .enums import BookType
28
28
from nautilus_trader .model .enums import InstrumentCloseType
29
29
from nautilus_trader .model .enums import MarketStatusAction
@@ -68,6 +68,7 @@ def setup(self):
68
68
oms_type = OmsType .NETTING ,
69
69
account_type = AccountType .MARGIN ,
70
70
reject_stop_orders = True ,
71
+ trade_execution = True ,
71
72
msgbus = self .msgbus ,
72
73
cache = self .cache ,
73
74
clock = self .clock ,
@@ -114,10 +115,10 @@ def test_instrument_close_expiry_closes_position(self) -> None:
114
115
# Arrange
115
116
exec_messages = []
116
117
self .msgbus .register ("ExecEngine.process" , lambda x : exec_messages .append (x ))
117
- tick : QuoteTick = TestDataStubs .quote_tick (
118
+ quote = TestDataStubs .quote_tick (
118
119
instrument = self .instrument ,
119
120
)
120
- self .matching_engine .process_quote_tick (tick )
121
+ self .matching_engine .process_quote_tick (quote )
121
122
order : MarketOrder = TestExecStubs .limit_order (
122
123
instrument = self .instrument ,
123
124
)
@@ -126,7 +127,7 @@ def test_instrument_close_expiry_closes_position(self) -> None:
126
127
# Act
127
128
instrument_close = TestDataStubs .instrument_close (
128
129
instrument_id = self .instrument_id ,
129
- price = Price ( 2 , 2 ),
130
+ price = Price . from_str ( "2.00" ),
130
131
close_type = InstrumentCloseType .CONTRACT_EXPIRED ,
131
132
ts_event = 2 ,
132
133
)
@@ -176,3 +177,209 @@ def test_process_order_book_depth_10(self) -> None:
176
177
# Assert
177
178
assert self .matching_engine .best_ask_price () == depth .asks [0 ].price
178
179
assert self .matching_engine .best_bid_price () == depth .bids [0 ].price
180
+
181
+ def test_process_trade_buyer_aggressor (self ) -> None :
182
+ # Arrange
183
+ trade = TestDataStubs .trade_tick (
184
+ instrument = self .instrument ,
185
+ price = 1000.0 ,
186
+ aggressor_side = AggressorSide .BUYER ,
187
+ )
188
+
189
+ # Act
190
+ self .matching_engine .process_trade_tick (trade )
191
+
192
+ # Assert - Buyer aggressor should set ask price
193
+ assert self .matching_engine .best_ask_price () == Price .from_str ("1000.0" )
194
+
195
+ def test_process_trade_seller_aggressor (self ) -> None :
196
+ # Arrange
197
+ trade = TestDataStubs .trade_tick (
198
+ instrument = self .instrument ,
199
+ price = 1000.0 ,
200
+ aggressor_side = AggressorSide .SELLER ,
201
+ )
202
+
203
+ # Act
204
+ self .matching_engine .process_trade_tick (trade )
205
+
206
+ # Assert - Seller aggressor should set bid price
207
+ assert self .matching_engine .best_bid_price () == Price .from_str ("1000.0" )
208
+
209
+ def test_process_trade_tick_no_aggressor_above_ask (self ) -> None :
210
+ # Arrange - Set initial bid/ask spread
211
+ quote = TestDataStubs .quote_tick (
212
+ instrument = self .instrument ,
213
+ bid_price = 990.0 ,
214
+ ask_price = 1010.0 ,
215
+ )
216
+ self .matching_engine .process_quote_tick (quote )
217
+
218
+ # Trade above ask with no aggressor
219
+ trade = TestDataStubs .trade_tick (
220
+ instrument = self .instrument ,
221
+ price = 1020.0 ,
222
+ aggressor_side = AggressorSide .NO_AGGRESSOR ,
223
+ )
224
+
225
+ # Act
226
+ self .matching_engine .process_trade_tick (trade )
227
+
228
+ # Assert - L1_MBP book update_trade_tick sets both bid/ask to trade price
229
+ # Then NO_AGGRESSOR logic doesn't modify further since 1020 >= 1020 (ask)
230
+ assert self .matching_engine .best_ask_price () == Price .from_str ("1020.0" )
231
+ assert self .matching_engine .best_bid_price () == Price .from_str ("1020.0" )
232
+
233
+ def test_process_trade_tick_no_aggressor_within_spread (self ) -> None :
234
+ # Arrange - Set initial bid/ask spread
235
+ quote = TestDataStubs .quote_tick (
236
+ instrument = self .instrument ,
237
+ bid_price = 990.0 ,
238
+ ask_price = 1010.0 ,
239
+ )
240
+ self .matching_engine .process_quote_tick (quote )
241
+
242
+ # Trade within the spread with no aggressor
243
+ trade = TestDataStubs .trade_tick (
244
+ instrument = self .instrument ,
245
+ price = 1000.0 ,
246
+ aggressor_side = AggressorSide .NO_AGGRESSOR ,
247
+ )
248
+
249
+ # Act
250
+ self .matching_engine .process_trade_tick (trade )
251
+
252
+ # Assert - L1_MBP book update_trade_tick sets both bid/ask to trade price
253
+ assert self .matching_engine .best_bid_price () == Price .from_str ("1000.0" )
254
+ assert self .matching_engine .best_ask_price () == Price .from_str ("1000.0" )
255
+
256
+ def test_process_trade_tick_no_aggressor_below_bid (self ) -> None :
257
+ # Arrange - Set initial bid/ask spread
258
+ quote = TestDataStubs .quote_tick (
259
+ instrument = self .instrument ,
260
+ bid_price = 1000.0 ,
261
+ ask_price = 1020.0 ,
262
+ )
263
+ self .matching_engine .process_quote_tick (quote )
264
+
265
+ # Trade below current bid with no aggressor
266
+ trade = TestDataStubs .trade_tick (
267
+ instrument = self .instrument ,
268
+ price = 990.0 ,
269
+ aggressor_side = AggressorSide .NO_AGGRESSOR ,
270
+ )
271
+
272
+ # Act
273
+ self .matching_engine .process_trade_tick (trade )
274
+
275
+ # Assert - L1_MBP book update_trade_tick sets both bid/ask to trade price
276
+ assert self .matching_engine .best_bid_price () == Price .from_str ("990.0" )
277
+ assert self .matching_engine .best_ask_price () == Price .from_str ("990.0" )
278
+
279
+ def test_process_trade_tick_no_aggressor_at_bid_and_ask (self ) -> None :
280
+ # Arrange - Set initial bid/ask spread
281
+ quote = TestDataStubs .quote_tick (
282
+ instrument = self .instrument ,
283
+ bid_price = 995.0 ,
284
+ ask_price = 1005.0 ,
285
+ )
286
+ self .matching_engine .process_quote_tick (quote )
287
+
288
+ # Trade exactly at bid level with no aggressor
289
+ trade1 = TestDataStubs .trade_tick (
290
+ instrument = self .instrument ,
291
+ price = 995.0 ,
292
+ aggressor_side = AggressorSide .NO_AGGRESSOR ,
293
+ )
294
+
295
+ # Act
296
+ self .matching_engine .process_trade_tick (trade1 )
297
+
298
+ # Assert - L1_MBP book update_trade_tick sets both bid/ask to trade price
299
+ assert self .matching_engine .best_bid_price () == Price .from_str ("995.0" )
300
+ assert self .matching_engine .best_ask_price () == Price .from_str ("995.0" )
301
+
302
+ # Trade exactly at ask level with no aggressor
303
+ trade2 = TestDataStubs .trade_tick (
304
+ instrument = self .instrument ,
305
+ price = 1005.0 ,
306
+ aggressor_side = AggressorSide .NO_AGGRESSOR ,
307
+ )
308
+
309
+ # Act
310
+ self .matching_engine .process_trade_tick (trade2 )
311
+
312
+ # Assert - L1_MBP book update_trade_tick sets both bid/ask to trade price
313
+ assert self .matching_engine .best_bid_price () == Price .from_str ("1005.0" )
314
+ assert self .matching_engine .best_ask_price () == Price .from_str ("1005.0" )
315
+
316
+ def test_process_trade_tick_with_trade_execution_disabled (self ) -> None :
317
+ # Arrange - Create matching engine with trade_execution=False
318
+ matching_engine = OrderMatchingEngine (
319
+ instrument = self .instrument ,
320
+ raw_id = 0 ,
321
+ fill_model = FillModel (),
322
+ fee_model = MakerTakerFeeModel (),
323
+ book_type = BookType .L1_MBP ,
324
+ oms_type = OmsType .NETTING ,
325
+ account_type = AccountType .MARGIN ,
326
+ reject_stop_orders = True ,
327
+ trade_execution = False , # Disabled
328
+ msgbus = self .msgbus ,
329
+ cache = self .cache ,
330
+ clock = self .clock ,
331
+ )
332
+
333
+ # Process trade tick with BUYER aggressor
334
+ trade = TestDataStubs .trade_tick (
335
+ instrument = self .instrument ,
336
+ price = 1000.0 ,
337
+ aggressor_side = AggressorSide .BUYER ,
338
+ )
339
+
340
+ # Act
341
+ matching_engine .process_trade_tick (trade )
342
+
343
+ # Assert - With trade_execution=False, only book update happens, no aggressor logic
344
+ # L1_MBP book update_trade_tick sets both bid/ask to trade price
345
+ assert matching_engine .best_bid_price () == Price .from_str ("1000.0" )
346
+ assert matching_engine .best_ask_price () == Price .from_str ("1000.0" )
347
+
348
+ def test_trade_execution_difference_buyer_aggressor (self ) -> None :
349
+ # This test demonstrates that trade_execution=True vs False produces the same result
350
+ # for L1_MBP books since update_trade_tick sets both bid/ask to trade price anyway
351
+
352
+ # Test with trade_execution=True (our main matching engine)
353
+ trade_tick_enabled = TestDataStubs .trade_tick (
354
+ instrument = self .instrument ,
355
+ price = 1000.0 ,
356
+ aggressor_side = AggressorSide .BUYER ,
357
+ )
358
+ self .matching_engine .process_trade_tick (trade_tick_enabled )
359
+
360
+ # Test with trade_execution=False
361
+ matching_engine = OrderMatchingEngine (
362
+ instrument = self .instrument ,
363
+ raw_id = 1 ,
364
+ fill_model = FillModel (),
365
+ fee_model = MakerTakerFeeModel (),
366
+ book_type = BookType .L1_MBP ,
367
+ oms_type = OmsType .NETTING ,
368
+ account_type = AccountType .MARGIN ,
369
+ reject_stop_orders = True ,
370
+ trade_execution = False , # Disabled
371
+ msgbus = self .msgbus ,
372
+ cache = self .cache ,
373
+ clock = self .clock ,
374
+ )
375
+
376
+ trade = TestDataStubs .trade_tick (
377
+ instrument = self .instrument ,
378
+ price = 1000.0 ,
379
+ aggressor_side = AggressorSide .BUYER ,
380
+ )
381
+ matching_engine .process_trade_tick (trade )
382
+
383
+ # Assert - Both should have same result for L1_MBP
384
+ assert self .matching_engine .best_bid_price () == matching_engine .best_bid_price ()
385
+ assert self .matching_engine .best_ask_price () == matching_engine .best_ask_price ()
0 commit comments