1use std::collections::HashMap;
17
18use nautilus_core::{UUID4, UnixNanos};
19use rust_decimal_macros::dec;
20
21use super::{
22 any::OrderAny, limit::LimitOrder, limit_if_touched::LimitIfTouchedOrder, market::MarketOrder,
23 market_if_touched::MarketIfTouchedOrder, market_to_limit::MarketToLimitOrder,
24 stop_limit::StopLimitOrder, stop_market::StopMarketOrder,
25 trailing_stop_limit::TrailingStopLimitOrder, trailing_stop_market::TrailingStopMarketOrder,
26};
27use crate::{
28 enums::{LiquiditySide, OrderSide, OrderType, TimeInForce, TrailingOffsetType, TriggerType},
29 events::{
30 OrderEventAny,
31 order::spec::{OrderAcceptedSpec, OrderCanceledSpec, OrderFilledSpec, OrderSubmittedSpec},
32 },
33 identifiers::{
34 AccountId, ClientOrderId, InstrumentId, PositionId, StrategyId, TradeId, TraderId, Venue,
35 VenueOrderId,
36 },
37 instruments::{Instrument, InstrumentAny},
38 orders::{Order, OrderTestBuilder},
39 stubs::TestDefault,
40 types::{Money, Price, Quantity},
41};
42
43impl TestDefault for LimitOrder {
44 fn test_default() -> Self {
46 Self::new(
47 TraderId::test_default(),
48 StrategyId::test_default(),
49 InstrumentId::test_default(),
50 ClientOrderId::test_default(),
51 OrderSide::Buy,
52 Quantity::from(100_000),
53 Price::from("1.00000"),
54 TimeInForce::Gtc,
55 None,
56 false,
57 false,
58 false,
59 None,
60 None,
61 None,
62 None,
63 None,
64 None,
65 None,
66 None,
67 None,
68 None,
69 None,
70 UUID4::default(),
71 UnixNanos::default(),
72 )
73 }
74}
75
76impl TestDefault for LimitIfTouchedOrder {
77 fn test_default() -> Self {
79 Self::new(
80 TraderId::test_default(),
81 StrategyId::test_default(),
82 InstrumentId::test_default(),
83 ClientOrderId::test_default(),
84 OrderSide::Buy,
85 Quantity::from(100_000),
86 Price::from("1.00000"),
87 Price::from("1.00000"),
88 TriggerType::BidAsk,
89 TimeInForce::Gtc,
90 None,
91 false,
92 false,
93 false,
94 None,
95 None,
96 None,
97 None,
98 None,
99 None,
100 None,
101 None,
102 None,
103 None,
104 None,
105 UUID4::default(),
106 UnixNanos::default(),
107 )
108 }
109}
110
111impl TestDefault for MarketOrder {
112 fn test_default() -> Self {
114 Self::new(
115 TraderId::test_default(),
116 StrategyId::test_default(),
117 InstrumentId::test_default(),
118 ClientOrderId::test_default(),
119 OrderSide::Buy,
120 Quantity::from(100_000),
121 TimeInForce::Day,
122 UUID4::default(),
123 UnixNanos::default(),
124 false,
125 false,
126 None,
127 None,
128 None,
129 None,
130 None,
131 None,
132 None,
133 None,
134 )
135 }
136}
137
138impl TestDefault for MarketIfTouchedOrder {
139 fn test_default() -> Self {
141 Self::new(
142 TraderId::test_default(),
143 StrategyId::test_default(),
144 InstrumentId::test_default(),
145 ClientOrderId::test_default(),
146 OrderSide::Buy,
147 Quantity::from(100_000),
148 Price::from("1.00000"),
149 TriggerType::BidAsk,
150 TimeInForce::Gtc,
151 None,
152 false,
153 false,
154 None,
155 None,
156 None,
157 None,
158 None,
159 None,
160 None,
161 None,
162 None,
163 None,
164 UUID4::default(),
165 UnixNanos::default(),
166 )
167 }
168}
169
170impl TestDefault for MarketToLimitOrder {
171 fn test_default() -> Self {
173 Self::new(
174 TraderId::test_default(),
175 StrategyId::test_default(),
176 InstrumentId::test_default(),
177 ClientOrderId::test_default(),
178 OrderSide::Buy,
179 Quantity::from(100_000),
180 TimeInForce::Gtc,
181 None,
182 false,
183 false,
184 false,
185 None,
186 None,
187 None,
188 None,
189 None,
190 None,
191 None,
192 None,
193 None,
194 UUID4::default(),
195 UnixNanos::default(),
196 )
197 }
198}
199
200impl TestDefault for StopLimitOrder {
201 fn test_default() -> Self {
203 Self::new(
204 TraderId::test_default(),
205 StrategyId::test_default(),
206 InstrumentId::test_default(),
207 ClientOrderId::test_default(),
208 OrderSide::Buy,
209 Quantity::from(100_000),
210 Price::from("1.00000"),
211 Price::from("1.00000"),
212 TriggerType::BidAsk,
213 TimeInForce::Gtc,
214 None,
215 false,
216 false,
217 false,
218 None,
219 None,
220 None,
221 None,
222 None,
223 None,
224 None,
225 None,
226 None,
227 None,
228 None,
229 UUID4::default(),
230 UnixNanos::default(),
231 )
232 }
233}
234
235impl TestDefault for StopMarketOrder {
236 fn test_default() -> Self {
238 Self::new(
239 TraderId::test_default(),
240 StrategyId::test_default(),
241 InstrumentId::test_default(),
242 ClientOrderId::test_default(),
243 OrderSide::Buy,
244 Quantity::from(100_000),
245 Price::from("1.00000"),
246 TriggerType::BidAsk,
247 TimeInForce::Gtc,
248 None,
249 false,
250 false,
251 None,
252 None,
253 None,
254 None,
255 None,
256 None,
257 None,
258 None,
259 None,
260 None,
261 None,
262 UUID4::default(),
263 UnixNanos::default(),
264 )
265 }
266}
267
268impl TestDefault for TrailingStopLimitOrder {
269 fn test_default() -> Self {
271 Self::new(
272 TraderId::test_default(),
273 StrategyId::test_default(),
274 InstrumentId::test_default(),
275 ClientOrderId::test_default(),
276 OrderSide::Buy,
277 Quantity::from(100_000),
278 None,
279 Price::from("1.00000"),
280 Price::from("1.00000"),
281 TriggerType::BidAsk,
282 dec!(0.001),
283 dec!(0.001),
284 TrailingOffsetType::Price,
285 TimeInForce::Gtc,
286 None,
287 false,
288 false,
289 false,
290 None,
291 None,
292 None,
293 None,
294 None,
295 None,
296 None,
297 None,
298 None,
299 None,
300 None,
301 UUID4::default(),
302 UnixNanos::default(),
303 )
304 }
305}
306
307impl TestDefault for TrailingStopMarketOrder {
308 fn test_default() -> Self {
310 Self::new(
311 TraderId::test_default(),
312 StrategyId::test_default(),
313 InstrumentId::test_default(),
314 ClientOrderId::test_default(),
315 OrderSide::Buy,
316 Quantity::from(100_000),
317 None,
318 Price::from("1.00000"),
319 TriggerType::BidAsk,
320 dec!(0.001),
321 TrailingOffsetType::Price,
322 TimeInForce::Gtc,
323 None,
324 false,
325 false,
326 None,
327 None,
328 None,
329 None,
330 None,
331 None,
332 None,
333 None,
334 None,
335 None,
336 None,
337 UUID4::default(),
338 UnixNanos::default(),
339 )
340 }
341}
342
343#[derive(Debug)]
344pub struct TestOrderEventStubs;
345
346impl TestOrderEventStubs {
347 #[must_use]
348 pub fn submitted(order: &OrderAny, account_id: AccountId) -> OrderEventAny {
349 let event = OrderSubmittedSpec::builder()
350 .trader_id(order.trader_id())
351 .strategy_id(order.strategy_id())
352 .instrument_id(order.instrument_id())
353 .client_order_id(order.client_order_id())
354 .account_id(account_id)
355 .build();
356 OrderEventAny::Submitted(event)
357 }
358
359 #[must_use]
360 pub fn accepted(
361 order: &OrderAny,
362 account_id: AccountId,
363 venue_order_id: VenueOrderId,
364 ) -> OrderEventAny {
365 let event = OrderAcceptedSpec::builder()
366 .trader_id(order.trader_id())
367 .strategy_id(order.strategy_id())
368 .instrument_id(order.instrument_id())
369 .client_order_id(order.client_order_id())
370 .venue_order_id(venue_order_id)
371 .account_id(account_id)
372 .build();
373 OrderEventAny::Accepted(event)
374 }
375
376 #[must_use]
377 pub fn canceled(
378 order: &OrderAny,
379 account_id: AccountId,
380 venue_order_id: Option<VenueOrderId>,
381 ) -> OrderEventAny {
382 let event = OrderCanceledSpec::builder()
383 .trader_id(order.trader_id())
384 .strategy_id(order.strategy_id())
385 .instrument_id(order.instrument_id())
386 .client_order_id(order.client_order_id())
387 .account_id(account_id)
388 .maybe_venue_order_id(venue_order_id)
389 .build();
390 OrderEventAny::Canceled(event)
391 }
392
393 #[expect(clippy::too_many_arguments)]
397 #[must_use]
398 pub fn filled(
399 order: &OrderAny,
400 instrument: &InstrumentAny,
401 trade_id: Option<TradeId>,
402 position_id: Option<PositionId>,
403 last_px: Option<Price>,
404 last_qty: Option<Quantity>,
405 liquidity_side: Option<LiquiditySide>,
406 commission: Option<Money>,
407 ts_filled_ns: Option<UnixNanos>,
408 account_id: Option<AccountId>,
409 ) -> OrderEventAny {
410 let mut builder = OrderFilledTestBuilder::new(order, instrument);
411
412 if let Some(trade_id) = trade_id {
413 builder.trade_id(trade_id);
414 }
415
416 if let Some(position_id) = position_id {
417 builder.position_id(position_id);
418 }
419
420 if let Some(last_px) = last_px {
421 builder.last_px(last_px);
422 }
423
424 if let Some(last_qty) = last_qty {
425 builder.last_qty(last_qty);
426 }
427
428 if let Some(liquidity_side) = liquidity_side {
429 builder.liquidity_side(liquidity_side);
430 }
431
432 if let Some(commission) = commission {
433 builder.commission(commission);
434 }
435
436 if let Some(ts_event) = ts_filled_ns {
437 builder.ts_event(ts_event);
438 }
439
440 if let Some(account_id) = account_id {
441 builder.account_id(account_id);
442 }
443
444 builder.build()
445 }
446}
447
448#[derive(Debug)]
450pub struct OrderFilledTestBuilder<'a> {
451 order: &'a OrderAny,
452 instrument: &'a InstrumentAny,
453 trade_id: Option<TradeId>,
454 position_id: Option<PositionId>,
455 last_px: Option<Price>,
456 last_qty: Option<Quantity>,
457 liquidity_side: Option<LiquiditySide>,
458 commission: Option<Money>,
459 ts_event: Option<UnixNanos>,
460 account_id: Option<AccountId>,
461 without_position_id: bool,
462 without_commission: bool,
463}
464
465impl<'a> OrderFilledTestBuilder<'a> {
466 #[must_use]
468 pub fn new(order: &'a OrderAny, instrument: &'a InstrumentAny) -> Self {
469 Self {
470 order,
471 instrument,
472 trade_id: None,
473 position_id: None,
474 last_px: None,
475 last_qty: None,
476 liquidity_side: None,
477 commission: None,
478 ts_event: None,
479 account_id: None,
480 without_position_id: false,
481 without_commission: false,
482 }
483 }
484
485 pub fn trade_id(&mut self, trade_id: TradeId) -> &mut Self {
487 self.trade_id = Some(trade_id);
488 self
489 }
490
491 pub fn position_id(&mut self, position_id: PositionId) -> &mut Self {
493 self.position_id = Some(position_id);
494 self.without_position_id = false;
495 self
496 }
497
498 pub fn without_position_id(&mut self) -> &mut Self {
500 self.without_position_id = true;
501 self
502 }
503
504 pub fn last_px(&mut self, last_px: Price) -> &mut Self {
506 self.last_px = Some(last_px);
507 self
508 }
509
510 pub fn last_qty(&mut self, last_qty: Quantity) -> &mut Self {
512 self.last_qty = Some(last_qty);
513 self
514 }
515
516 pub fn liquidity_side(&mut self, liquidity_side: LiquiditySide) -> &mut Self {
518 self.liquidity_side = Some(liquidity_side);
519 self
520 }
521
522 pub fn commission(&mut self, commission: Money) -> &mut Self {
524 self.commission = Some(commission);
525 self.without_commission = false;
526 self
527 }
528
529 pub fn without_commission(&mut self) -> &mut Self {
531 self.without_commission = true;
532 self
533 }
534
535 pub fn ts_event(&mut self, ts_event: UnixNanos) -> &mut Self {
537 self.ts_event = Some(ts_event);
538 self
539 }
540
541 pub fn account_id(&mut self, account_id: AccountId) -> &mut Self {
543 self.account_id = Some(account_id);
544 self
545 }
546
547 #[must_use]
549 pub fn build(&self) -> OrderEventAny {
550 let venue_order_id = self
551 .order
552 .venue_order_id()
553 .unwrap_or_else(VenueOrderId::test_default);
554 let account_id = self
555 .account_id
556 .or(self.order.account_id())
557 .unwrap_or_else(AccountId::test_default);
558 let trade_id = self.trade_id.unwrap_or_else(|| {
559 TradeId::new(
560 self.order
561 .client_order_id()
562 .as_str()
563 .replace('O', "E")
564 .as_str(),
565 )
566 });
567 let position_id = (!self.without_position_id).then(|| {
568 self.position_id
569 .or(self.order.position_id())
570 .unwrap_or(PositionId::new("1"))
571 });
572 let commission =
573 (!self.without_commission).then(|| self.commission.unwrap_or(Money::from("2 USD")));
574 let event = OrderFilledSpec::builder()
575 .trader_id(self.order.trader_id())
576 .strategy_id(self.order.strategy_id())
577 .instrument_id(self.instrument.id())
578 .client_order_id(self.order.client_order_id())
579 .venue_order_id(venue_order_id)
580 .account_id(account_id)
581 .trade_id(trade_id)
582 .order_side(self.order.order_side())
583 .order_type(self.order.order_type())
584 .last_qty(self.last_qty.unwrap_or(self.order.quantity()))
585 .last_px(self.last_px.unwrap_or(Price::from("1.0")))
586 .currency(self.instrument.quote_currency())
587 .liquidity_side(self.liquidity_side.unwrap_or(LiquiditySide::Maker))
588 .ts_event(self.ts_event.unwrap_or_default())
589 .maybe_position_id(position_id)
590 .maybe_commission(commission)
591 .build();
592 OrderEventAny::Filled(event)
593 }
594}
595
596#[derive(Debug)]
597pub struct TestOrderStubs;
598
599impl TestOrderStubs {
600 #[must_use]
604 pub fn make_accepted_order(order: &OrderAny) -> OrderAny {
605 let mut new_order = order.clone();
606 let accepted_event = TestOrderEventStubs::accepted(
607 &new_order,
608 AccountId::from("SIM-001"),
609 VenueOrderId::from("V-001"),
610 );
611 new_order.apply(accepted_event).unwrap();
612 new_order
613 }
614
615 #[must_use]
619 pub fn make_filled_order(
620 order: &OrderAny,
621 instrument: &InstrumentAny,
622 liquidity_side: LiquiditySide,
623 ) -> OrderAny {
624 let mut accepted_order = Self::make_accepted_order(order);
625 let fill = OrderFilledTestBuilder::new(&accepted_order, instrument)
626 .liquidity_side(liquidity_side)
627 .build();
628 accepted_order.apply(fill).unwrap();
629 accepted_order
630 }
631}
632
633#[derive(Debug)]
634pub struct TestOrdersGenerator {
635 order_type: OrderType,
636 venue_instruments: HashMap<Venue, u32>,
637 orders_per_instrument: u32,
638}
639
640impl TestOrdersGenerator {
641 #[must_use]
642 pub fn new(order_type: OrderType) -> Self {
643 Self {
644 order_type,
645 venue_instruments: HashMap::new(),
646 orders_per_instrument: 5,
647 }
648 }
649
650 pub fn set_orders_per_instrument(&mut self, total_orders: u32) {
651 self.orders_per_instrument = total_orders;
652 }
653
654 pub fn add_venue_and_total_instruments(&mut self, venue: Venue, total_instruments: u32) {
655 self.venue_instruments.insert(venue, total_instruments);
656 }
657
658 fn generate_order(&self, instrument_id: InstrumentId, client_order_id_index: u32) -> OrderAny {
659 let client_order_id =
660 ClientOrderId::from(format!("O-{instrument_id}-{client_order_id_index}"));
661 OrderTestBuilder::new(self.order_type)
662 .quantity(Quantity::from("1"))
663 .price(Price::from("1"))
664 .instrument_id(instrument_id)
665 .client_order_id(client_order_id)
666 .build()
667 }
668
669 #[must_use]
670 pub fn build(&self) -> Vec<OrderAny> {
671 let mut orders = Vec::new();
672
673 for (venue, total_instruments) in &self.venue_instruments {
674 for i in 0..*total_instruments {
675 let instrument_id = InstrumentId::from(format!("SYMBOL-{i}.{venue}"));
676 for order_index in 0..self.orders_per_instrument {
677 let order = self.generate_order(instrument_id, order_index);
678 orders.push(order);
679 }
680 }
681 }
682 orders
683 }
684}
685
686#[must_use]
687pub fn create_order_list_sample(
688 total_venues: u8,
689 total_instruments: u32,
690 orders_per_instrument: u32,
691) -> Vec<OrderAny> {
692 let mut order_generator = TestOrdersGenerator::new(OrderType::Limit);
695
696 for i in 0..total_venues {
697 let venue = Venue::from(format!("VENUE-{i}"));
698 order_generator.add_venue_and_total_instruments(venue, total_instruments);
699 }
700 order_generator.set_orders_per_instrument(orders_per_instrument);
701
702 order_generator.build()
703}
704
705#[cfg(test)]
706mod tests {
707 use rstest::rstest;
708
709 use super::*;
710 use crate::instruments::stubs::audusd_sim;
711
712 #[rstest]
713 fn preserves_legacy_fill_defaults() {
714 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
715 let order = OrderTestBuilder::new(OrderType::Market)
716 .instrument_id(instrument.id())
717 .quantity(Quantity::from(1))
718 .build();
719 let OrderEventAny::Filled(fill) = OrderFilledTestBuilder::new(&order, &instrument).build()
720 else {
721 panic!("expected OrderFilled event");
722 };
723
724 assert_eq!(fill.position_id, Some(PositionId::new("1")));
725 assert_eq!(fill.commission, Some(Money::from("2 USD")));
726 assert_eq!(fill.liquidity_side, LiquiditySide::Maker);
727 }
728
729 #[rstest]
730 fn can_omit_position_id_and_commission() {
731 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
732 let order = OrderTestBuilder::new(OrderType::Market)
733 .instrument_id(instrument.id())
734 .quantity(Quantity::from(1))
735 .build();
736 let OrderEventAny::Filled(fill) = OrderFilledTestBuilder::new(&order, &instrument)
737 .without_position_id()
738 .without_commission()
739 .build()
740 else {
741 panic!("expected OrderFilled event");
742 };
743
744 assert_eq!(fill.position_id, None);
745 assert_eq!(fill.commission, None);
746 }
747}