1#![allow(dead_code)]
18
19use indexmap::IndexMap;
20use nautilus_core::{UUID4, UnixNanos, correctness::FAILED};
21use rust_decimal::Decimal;
22use ustr::Ustr;
23
24use crate::{
25 enums::{ContingencyType, OrderSide, OrderType, TimeInForce, TrailingOffsetType, TriggerType},
26 events::{OrderEventAny, order::spec::OrderSubmittedSpec},
27 identifiers::{
28 AccountId, ClientOrderId, ExecAlgorithmId, InstrumentId, OrderListId, StrategyId, TraderId,
29 },
30 orders::{
31 Order, OrderAny, limit::LimitOrder, limit_if_touched::LimitIfTouchedOrder,
32 market::MarketOrder, market_if_touched::MarketIfTouchedOrder,
33 market_to_limit::MarketToLimitOrder, stop_limit::StopLimitOrder,
34 stop_market::StopMarketOrder, trailing_stop_limit::TrailingStopLimitOrder,
35 trailing_stop_market::TrailingStopMarketOrder,
36 },
37 stubs::TestDefault,
38 types::{Price, Quantity},
39};
40
41#[derive(Debug)]
42pub struct OrderTestBuilder {
43 kind: OrderType,
44 trader_id: Option<TraderId>,
45 strategy_id: Option<StrategyId>,
46 instrument_id: Option<InstrumentId>,
47 client_order_id: Option<ClientOrderId>,
48 side: Option<OrderSide>,
49 quantity: Option<Quantity>,
50 price: Option<Price>,
51 activation_price: Option<Price>,
52 trigger_price: Option<Price>,
53 trigger_type: Option<TriggerType>,
54 limit_offset: Option<Decimal>,
55 trailing_offset: Option<Decimal>,
56 trailing_offset_type: Option<TrailingOffsetType>,
57 time_in_force: Option<TimeInForce>,
58 expire_time: Option<UnixNanos>,
59 reduce_only: Option<bool>,
60 post_only: Option<bool>,
61 quote_quantity: Option<bool>,
62 display_qty: Option<Quantity>,
63 emulation_trigger: Option<TriggerType>,
64 trigger_instrument_id: Option<InstrumentId>,
65 order_list_id: Option<OrderListId>,
66 linked_order_ids: Option<Vec<ClientOrderId>>,
67 parent_order_id: Option<ClientOrderId>,
68 exec_algorithm_id: Option<ExecAlgorithmId>,
69 exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
70 exec_spawn_id: Option<ClientOrderId>,
71 tags: Option<Vec<Ustr>>,
72 init_id: Option<UUID4>,
73 ts_init: Option<UnixNanos>,
74 contingency_type: Option<ContingencyType>,
75 submitted: bool,
76}
77
78impl OrderTestBuilder {
79 #[must_use]
81 pub fn new(kind: OrderType) -> Self {
82 Self {
83 kind,
84 trader_id: None,
85 strategy_id: None,
86 instrument_id: None,
87 client_order_id: None,
88 side: None,
89 quantity: None,
90 price: None,
91 activation_price: None,
92 trigger_price: None,
93 trigger_type: None,
94 limit_offset: None,
95 trailing_offset: None,
96 trailing_offset_type: None,
97 time_in_force: None,
98 contingency_type: None,
99 expire_time: None,
100 reduce_only: None,
101 post_only: None,
102 quote_quantity: None,
103 display_qty: None,
104 emulation_trigger: None,
105 trigger_instrument_id: None,
106 linked_order_ids: None,
107 order_list_id: None,
108 parent_order_id: None,
109 exec_algorithm_id: None,
110 exec_algorithm_params: None,
111 exec_spawn_id: None,
112 init_id: None,
113 ts_init: None,
114 tags: None,
115 submitted: false,
116 }
117 }
118
119 pub fn submit(&mut self, submit: bool) -> &mut Self {
120 self.submitted = submit;
121 self
122 }
123
124 pub fn kind(&mut self, kind: OrderType) -> &mut Self {
125 self.kind = kind;
126 self
127 }
128
129 pub fn trader_id(&mut self, trader_id: TraderId) -> &mut Self {
130 self.trader_id = Some(trader_id);
131 self
132 }
133
134 fn get_trader_id(&self) -> TraderId {
135 self.trader_id.unwrap_or_else(TraderId::test_default)
136 }
137
138 pub fn strategy_id(&mut self, strategy_id: StrategyId) -> &mut Self {
140 self.strategy_id = Some(strategy_id);
141 self
142 }
143
144 fn get_strategy_id(&self) -> StrategyId {
145 self.strategy_id.unwrap_or_else(StrategyId::test_default)
146 }
147
148 pub fn instrument_id(&mut self, instrument_id: InstrumentId) -> &mut Self {
150 self.instrument_id = Some(instrument_id);
151 self
152 }
153
154 fn get_instrument_id(&self) -> InstrumentId {
155 self.instrument_id.expect("Instrument ID not set")
156 }
157
158 pub fn client_order_id(&mut self, client_order_id: ClientOrderId) -> &mut Self {
160 self.client_order_id = Some(client_order_id);
161 self
162 }
163
164 fn get_client_order_id(&self) -> ClientOrderId {
165 self.client_order_id
166 .unwrap_or_else(ClientOrderId::test_default)
167 }
168
169 pub fn side(&mut self, side: OrderSide) -> &mut Self {
171 self.side = Some(side);
172 self
173 }
174
175 fn get_side(&self) -> OrderSide {
176 self.side.unwrap_or(OrderSide::Buy)
177 }
178
179 pub fn quantity(&mut self, quantity: Quantity) -> &mut Self {
181 self.quantity = Some(quantity);
182 self
183 }
184
185 fn get_quantity(&self) -> Quantity {
186 self.quantity.expect("Order quantity not set")
187 }
188
189 pub fn price(&mut self, price: Price) -> &mut Self {
191 self.price = Some(price);
192 self
193 }
194
195 fn get_price(&self) -> Price {
196 self.price.expect("Price not set")
197 }
198
199 pub fn trigger_price(&mut self, trigger_price: Price) -> &mut Self {
201 self.trigger_price = Some(trigger_price);
202 self
203 }
204
205 fn get_trigger_price(&self) -> Price {
206 self.trigger_price.expect("Trigger price not set")
207 }
208
209 pub fn activation_price(&mut self, activation_price: Price) -> &mut Self {
211 self.activation_price = Some(activation_price);
212 self
213 }
214
215 fn get_activation_price(&self) -> Option<Price> {
216 self.activation_price
217 }
218
219 pub fn trigger_type(&mut self, trigger_type: TriggerType) -> &mut Self {
221 self.trigger_type = Some(trigger_type);
222 self
223 }
224
225 fn get_trigger_type(&self) -> TriggerType {
226 self.trigger_type.unwrap_or(TriggerType::Default)
227 }
228
229 pub fn limit_offset(&mut self, limit_offset: Decimal) -> &mut Self {
231 self.limit_offset = Some(limit_offset);
232 self
233 }
234
235 fn get_limit_offset(&self) -> Decimal {
236 self.limit_offset.expect("Limit offset not set")
237 }
238
239 pub fn trailing_offset(&mut self, trailing_offset: Decimal) -> &mut Self {
241 self.trailing_offset = Some(trailing_offset);
242 self
243 }
244
245 fn get_trailing_offset(&self) -> Decimal {
246 self.trailing_offset.expect("Trailing offset not set")
247 }
248
249 pub fn trailing_offset_type(&mut self, trailing_offset_type: TrailingOffsetType) -> &mut Self {
251 self.trailing_offset_type = Some(trailing_offset_type);
252 self
253 }
254
255 fn get_trailing_offset_type(&self) -> TrailingOffsetType {
256 self.trailing_offset_type
257 .expect("Trailing offset type not set")
258 }
259
260 pub fn time_in_force(&mut self, time_in_force: TimeInForce) -> &mut Self {
262 self.time_in_force = Some(time_in_force);
263 self
264 }
265
266 fn get_time_in_force(&self) -> TimeInForce {
267 self.time_in_force.unwrap_or(TimeInForce::Gtc)
268 }
269
270 pub fn expire_time(&mut self, expire_time: UnixNanos) -> &mut Self {
272 self.expire_time = Some(expire_time);
273 self
274 }
275
276 fn get_expire_time(&self) -> Option<UnixNanos> {
277 self.expire_time
278 }
279
280 pub fn display_qty(&mut self, display_qty: Quantity) -> &mut Self {
282 self.display_qty = Some(display_qty);
283 self
284 }
285
286 fn get_display_qty(&self) -> Option<Quantity> {
287 self.display_qty
288 }
289
290 pub fn emulation_trigger(&mut self, emulation_trigger: TriggerType) -> &mut Self {
292 self.emulation_trigger = Some(emulation_trigger);
293 self
294 }
295
296 fn get_emulation_trigger(&self) -> Option<TriggerType> {
297 self.emulation_trigger
298 }
299
300 pub fn trigger_instrument_id(&mut self, trigger_instrument_id: InstrumentId) -> &mut Self {
302 self.trigger_instrument_id = Some(trigger_instrument_id);
303 self
304 }
305
306 fn get_trigger_instrument_id(&self) -> Option<InstrumentId> {
307 self.trigger_instrument_id
308 }
309
310 pub fn order_list_id(&mut self, order_list_id: OrderListId) -> &mut Self {
312 self.order_list_id = Some(order_list_id);
313 self
314 }
315
316 fn get_order_list_id(&self) -> Option<OrderListId> {
317 self.order_list_id
318 }
319
320 pub fn linked_order_ids(&mut self, linked_order_ids: Vec<ClientOrderId>) -> &mut Self {
322 self.linked_order_ids = Some(linked_order_ids);
323 self
324 }
325
326 fn get_linked_order_ids(&self) -> Option<Vec<ClientOrderId>> {
327 self.linked_order_ids.clone()
328 }
329
330 pub fn parent_order_id(&mut self, parent_order_id: ClientOrderId) -> &mut Self {
332 self.parent_order_id = Some(parent_order_id);
333 self
334 }
335
336 fn get_parent_order_id(&self) -> Option<ClientOrderId> {
337 self.parent_order_id
338 }
339
340 pub fn exec_algorithm_id(&mut self, exec_algorithm_id: ExecAlgorithmId) -> &mut Self {
342 self.exec_algorithm_id = Some(exec_algorithm_id);
343 self
344 }
345
346 fn get_exec_algorithm_id(&self) -> Option<ExecAlgorithmId> {
347 self.exec_algorithm_id
348 }
349
350 pub fn exec_algorithm_params(
352 &mut self,
353 exec_algorithm_params: IndexMap<Ustr, Ustr>,
354 ) -> &mut Self {
355 self.exec_algorithm_params = Some(exec_algorithm_params);
356 self
357 }
358
359 fn get_exec_algorithm_params(&self) -> Option<IndexMap<Ustr, Ustr>> {
360 self.exec_algorithm_params.clone()
361 }
362
363 pub fn exec_spawn_id(&mut self, exec_spawn_id: ClientOrderId) -> &mut Self {
365 self.exec_spawn_id = Some(exec_spawn_id);
366 self
367 }
368
369 fn get_exec_spawn_id(&self) -> Option<ClientOrderId> {
370 self.exec_spawn_id
371 }
372
373 pub fn tags(&mut self, tags: Vec<Ustr>) -> &mut Self {
375 self.tags = Some(tags);
376 self
377 }
378
379 fn get_tags(&self) -> Option<Vec<Ustr>> {
380 self.tags.clone()
381 }
382
383 pub fn init_id(&mut self, init_id: UUID4) -> &mut Self {
385 self.init_id = Some(init_id);
386 self
387 }
388
389 fn get_init_id(&self) -> UUID4 {
390 self.init_id.unwrap_or_default()
391 }
392
393 pub fn ts_init(&mut self, ts_init: UnixNanos) -> &mut Self {
395 self.ts_init = Some(ts_init);
396 self
397 }
398
399 fn get_ts_init(&self) -> UnixNanos {
400 self.ts_init.unwrap_or_default()
401 }
402
403 pub fn reduce_only(&mut self, reduce_only: bool) -> &mut Self {
405 self.reduce_only = Some(reduce_only);
406 self
407 }
408
409 fn get_reduce_only(&self) -> bool {
410 self.reduce_only.unwrap_or(false)
411 }
412
413 pub fn post_only(&mut self, post_only: bool) -> &mut Self {
415 self.post_only = Some(post_only);
416 self
417 }
418
419 fn get_post_only(&self) -> bool {
420 self.post_only.unwrap_or(false)
421 }
422
423 pub fn quote_quantity(&mut self, quote_quantity: bool) -> &mut Self {
425 self.quote_quantity = Some(quote_quantity);
426 self
427 }
428
429 fn get_quote_quantity(&self) -> bool {
430 self.quote_quantity.unwrap_or(false)
431 }
432
433 pub fn contingency_type(&mut self, contingency_type: ContingencyType) -> &mut Self {
435 self.contingency_type = Some(contingency_type);
436 self
437 }
438
439 fn get_contingency_type(&self) -> Option<ContingencyType> {
440 self.contingency_type
441 }
442
443 #[must_use]
450 pub fn build(&self) -> OrderAny {
451 let mut order = match self.kind {
452 OrderType::Market => OrderAny::Market(MarketOrder::new(
453 self.get_trader_id(),
454 self.get_strategy_id(),
455 self.get_instrument_id(),
456 self.get_client_order_id(),
457 self.get_side(),
458 self.get_quantity(),
459 self.get_time_in_force(),
460 self.get_init_id(),
461 self.get_ts_init(),
462 self.get_reduce_only(),
463 self.get_quote_quantity(),
464 self.get_contingency_type(),
465 self.get_order_list_id(),
466 self.get_linked_order_ids(),
467 self.get_parent_order_id(),
468 self.get_exec_algorithm_id(),
469 self.get_exec_algorithm_params(),
470 self.get_exec_spawn_id(),
471 self.get_tags(),
472 )),
473 OrderType::Limit => OrderAny::Limit(LimitOrder::new(
474 self.get_trader_id(),
475 self.get_strategy_id(),
476 self.get_instrument_id(),
477 self.get_client_order_id(),
478 self.get_side(),
479 self.get_quantity(),
480 self.get_price(),
481 self.get_time_in_force(),
482 self.get_expire_time(),
483 self.get_post_only(),
484 self.get_reduce_only(),
485 self.get_quote_quantity(),
486 self.get_display_qty(),
487 self.get_emulation_trigger(),
488 self.get_trigger_instrument_id(),
489 self.get_contingency_type(),
490 self.get_order_list_id(),
491 self.get_linked_order_ids(),
492 self.get_parent_order_id(),
493 self.get_exec_algorithm_id(),
494 self.get_exec_algorithm_params(),
495 self.get_exec_spawn_id(),
496 self.get_tags(),
497 self.get_init_id(),
498 self.get_ts_init(),
499 )),
500 OrderType::StopMarket => OrderAny::StopMarket(StopMarketOrder::new(
501 self.get_trader_id(),
502 self.get_strategy_id(),
503 self.get_instrument_id(),
504 self.get_client_order_id(),
505 self.get_side(),
506 self.get_quantity(),
507 self.get_trigger_price(),
508 self.get_trigger_type(),
509 self.get_time_in_force(),
510 self.get_expire_time(),
511 self.get_reduce_only(),
512 self.get_quote_quantity(),
513 self.get_display_qty(),
514 self.get_emulation_trigger(),
515 self.get_trigger_instrument_id(),
516 self.get_contingency_type(),
517 self.get_order_list_id(),
518 self.get_linked_order_ids(),
519 self.get_parent_order_id(),
520 self.get_exec_algorithm_id(),
521 self.get_exec_algorithm_params(),
522 self.get_exec_spawn_id(),
523 self.get_tags(),
524 self.get_init_id(),
525 self.get_ts_init(),
526 )),
527 OrderType::StopLimit => OrderAny::StopLimit(StopLimitOrder::new(
528 self.get_trader_id(),
529 self.get_strategy_id(),
530 self.get_instrument_id(),
531 self.get_client_order_id(),
532 self.get_side(),
533 self.get_quantity(),
534 self.get_price(),
535 self.get_trigger_price(),
536 self.get_trigger_type(),
537 self.get_time_in_force(),
538 self.get_expire_time(),
539 self.get_post_only(),
540 self.get_reduce_only(),
541 self.get_quote_quantity(),
542 self.get_display_qty(),
543 self.get_emulation_trigger(),
544 self.get_trigger_instrument_id(),
545 self.get_contingency_type(),
546 self.get_order_list_id(),
547 self.get_linked_order_ids(),
548 self.get_parent_order_id(),
549 self.get_exec_algorithm_id(),
550 self.get_exec_algorithm_params(),
551 self.get_exec_spawn_id(),
552 self.get_tags(),
553 self.get_init_id(),
554 self.get_ts_init(),
555 )),
556 OrderType::MarketToLimit => OrderAny::MarketToLimit(MarketToLimitOrder::new(
557 self.get_trader_id(),
558 self.get_strategy_id(),
559 self.get_instrument_id(),
560 self.get_client_order_id(),
561 self.get_side(),
562 self.get_quantity(),
563 self.get_time_in_force(),
564 self.get_expire_time(),
565 self.get_post_only(),
566 self.get_reduce_only(),
567 self.get_quote_quantity(),
568 self.get_display_qty(),
569 self.get_contingency_type(),
570 self.get_order_list_id(),
571 self.get_linked_order_ids(),
572 self.get_parent_order_id(),
573 self.get_exec_algorithm_id(),
574 self.get_exec_algorithm_params(),
575 self.get_exec_spawn_id(),
576 self.get_tags(),
577 self.get_init_id(),
578 self.get_ts_init(),
579 )),
580 OrderType::MarketIfTouched => OrderAny::MarketIfTouched(MarketIfTouchedOrder::new(
581 self.get_trader_id(),
582 self.get_strategy_id(),
583 self.get_instrument_id(),
584 self.get_client_order_id(),
585 self.get_side(),
586 self.get_quantity(),
587 self.get_trigger_price(),
588 self.get_trigger_type(),
589 self.get_time_in_force(),
590 self.get_expire_time(),
591 self.get_reduce_only(),
592 self.get_quote_quantity(),
593 self.get_emulation_trigger(),
594 self.get_trigger_instrument_id(),
595 self.get_contingency_type(),
596 self.get_order_list_id(),
597 self.get_linked_order_ids(),
598 self.get_parent_order_id(),
599 self.get_exec_algorithm_id(),
600 self.get_exec_algorithm_params(),
601 self.get_exec_spawn_id(),
602 self.get_tags(),
603 self.get_init_id(),
604 self.get_ts_init(),
605 )),
606 OrderType::LimitIfTouched => OrderAny::LimitIfTouched(LimitIfTouchedOrder::new(
607 self.get_trader_id(),
608 self.get_strategy_id(),
609 self.get_instrument_id(),
610 self.get_client_order_id(),
611 self.get_side(),
612 self.get_quantity(),
613 self.get_price(),
614 self.get_trigger_price(),
615 self.get_trigger_type(),
616 self.get_time_in_force(),
617 self.get_expire_time(),
618 self.get_post_only(),
619 self.get_reduce_only(),
620 self.get_quote_quantity(),
621 self.get_display_qty(),
622 self.get_emulation_trigger(),
623 self.get_trigger_instrument_id(),
624 self.get_contingency_type(),
625 self.get_order_list_id(),
626 self.get_linked_order_ids(),
627 self.get_parent_order_id(),
628 self.get_exec_algorithm_id(),
629 self.get_exec_algorithm_params(),
630 self.get_exec_spawn_id(),
631 self.get_tags(),
632 self.get_init_id(),
633 self.get_ts_init(),
634 )),
635 OrderType::TrailingStopMarket => OrderAny::TrailingStopMarket(
636 TrailingStopMarketOrder::new_checked(
639 self.get_trader_id(),
640 self.get_strategy_id(),
641 self.get_instrument_id(),
642 self.get_client_order_id(),
643 self.get_side(),
644 self.get_quantity(),
645 self.get_activation_price(),
646 self.trigger_price,
647 self.get_trigger_type(),
648 self.get_trailing_offset(),
649 self.get_trailing_offset_type(),
650 self.get_time_in_force(),
651 self.get_expire_time(),
652 self.get_reduce_only(),
653 self.get_quote_quantity(),
654 self.get_display_qty(),
655 self.get_emulation_trigger(),
656 self.get_trigger_instrument_id(),
657 self.get_contingency_type(),
658 self.get_order_list_id(),
659 self.get_linked_order_ids(),
660 self.get_parent_order_id(),
661 self.get_exec_algorithm_id(),
662 self.get_exec_algorithm_params(),
663 self.get_exec_spawn_id(),
664 self.get_tags(),
665 self.get_init_id(),
666 self.get_ts_init(),
667 )
668 .unwrap_or_else(|e| panic!("{FAILED}: {e}")),
669 ),
670 OrderType::TrailingStopLimit => OrderAny::TrailingStopLimit(
671 TrailingStopLimitOrder::new_checked(
672 self.get_trader_id(),
673 self.get_strategy_id(),
674 self.get_instrument_id(),
675 self.get_client_order_id(),
676 self.get_side(),
677 self.get_quantity(),
678 self.get_activation_price(),
679 self.price,
680 self.trigger_price,
681 self.get_trigger_type(),
682 self.get_limit_offset(),
683 self.get_trailing_offset(),
684 self.get_trailing_offset_type(),
685 self.get_time_in_force(),
686 self.get_expire_time(),
687 self.get_post_only(),
688 self.get_reduce_only(),
689 self.get_quote_quantity(),
690 self.get_display_qty(),
691 self.get_emulation_trigger(),
692 self.get_trigger_instrument_id(),
693 self.get_contingency_type(),
694 self.get_order_list_id(),
695 self.get_linked_order_ids(),
696 self.get_parent_order_id(),
697 self.get_exec_algorithm_id(),
698 self.get_exec_algorithm_params(),
699 self.get_exec_spawn_id(),
700 self.get_tags(),
701 self.get_init_id(),
702 self.get_ts_init(),
703 )
704 .unwrap_or_else(|e| panic!("{FAILED}: {e}")),
705 ),
706 };
707
708 if self.submitted {
709 let submit_event = OrderSubmittedSpec::builder()
710 .trader_id(order.trader_id())
711 .strategy_id(order.strategy_id())
712 .instrument_id(order.instrument_id())
713 .client_order_id(order.client_order_id())
714 .account_id(AccountId::from("ACCOUNT-001"))
715 .build();
716 order.apply(OrderEventAny::Submitted(submit_event)).unwrap();
717 }
718
719 order
720 }
721}
722
723#[cfg(test)]
724mod tests {
725 use rstest::rstest;
726
727 use super::*;
728 use crate::orders::Order;
729
730 #[rstest]
731 fn preserves_an_absent_contingency_type() {
732 let order = OrderTestBuilder::new(OrderType::Limit)
733 .instrument_id(InstrumentId::test_default())
734 .quantity(Quantity::from(1))
735 .price(Price::from("1"))
736 .build();
737
738 assert_eq!(order.contingency_type(), None);
739 assert!(!order.is_contingency());
740 }
741
742 #[rstest]
743 fn preserves_a_configured_contingency_type() {
744 let order = OrderTestBuilder::new(OrderType::Limit)
745 .instrument_id(InstrumentId::test_default())
746 .quantity(Quantity::from(1))
747 .price(Price::from("1"))
748 .contingency_type(ContingencyType::Oto)
749 .linked_order_ids(vec![ClientOrderId::from("O-LINKED")])
750 .build();
751
752 assert_eq!(order.contingency_type(), Some(ContingencyType::Oto));
753 assert!(order.is_contingency());
754 }
755
756 #[rstest]
757 fn submits_to_the_account_issuer() {
758 let order = OrderTestBuilder::new(OrderType::Market)
759 .instrument_id(InstrumentId::test_default())
760 .quantity(Quantity::from(1))
761 .submit(true)
762 .build();
763
764 assert_eq!(order.account_id(), Some(AccountId::from("ACCOUNT-001")));
765 }
766}