1use std::{cell::RefCell, rc::Rc};
19
20use indexmap::IndexMap;
21use nautilus_core::{UUID4, UnixNanos};
22use nautilus_model::{
23 enums::{ContingencyType, OrderSide, OrderType, TimeInForce, TrailingOffsetType, TriggerType},
24 identifiers::{
25 ClientOrderId, ExecAlgorithmId, InstrumentId, OrderListId, StrategyId, TraderId,
26 },
27 orders::{
28 LimitIfTouchedOrder, LimitOrder, MarketIfTouchedOrder, MarketOrder, MarketToLimitOrder,
29 Order, OrderAny, OrderList, StopLimitOrder, StopMarketOrder, TrailingStopLimitOrder,
30 TrailingStopMarketOrder,
31 },
32 types::{Price, Quantity},
33};
34use rust_decimal::Decimal;
35use ustr::Ustr;
36
37use crate::{
38 clock::Clock,
39 generators::{client_order_id::ClientOrderIdGenerator, order_list_id::OrderListIdGenerator},
40};
41
42#[derive(Debug)]
43pub struct OrderFactory {
44 clock: Rc<RefCell<dyn Clock>>,
45 trader_id: TraderId,
46 strategy_id: StrategyId,
47 order_id_generator: ClientOrderIdGenerator,
48 order_list_id_generator: OrderListIdGenerator,
49}
50
51#[bon::bon]
52impl OrderFactory {
53 pub fn new(
55 trader_id: TraderId,
56 strategy_id: StrategyId,
57 init_order_id_count: Option<usize>,
58 init_order_list_id_count: Option<usize>,
59 clock: Rc<RefCell<dyn Clock>>,
60 use_uuids_for_client_order_ids: bool,
61 use_hyphens_in_client_order_ids: bool,
62 ) -> Self {
63 let order_id_generator = ClientOrderIdGenerator::new(
64 trader_id,
65 strategy_id,
66 init_order_id_count.unwrap_or(0),
67 clock.clone(),
68 use_uuids_for_client_order_ids,
69 use_hyphens_in_client_order_ids,
70 );
71
72 let order_list_id_generator = OrderListIdGenerator::new(
73 trader_id,
74 strategy_id,
75 init_order_list_id_count.unwrap_or(0),
76 clock.clone(),
77 );
78
79 Self {
80 clock,
81 trader_id,
82 strategy_id,
83 order_id_generator,
84 order_list_id_generator,
85 }
86 }
87
88 #[must_use]
90 pub const fn trader_id(&self) -> TraderId {
91 self.trader_id
92 }
93
94 #[must_use]
96 pub const fn strategy_id(&self) -> StrategyId {
97 self.strategy_id
98 }
99
100 pub const fn set_client_order_id_count(&mut self, count: usize) {
102 self.order_id_generator.set_count(count);
103 }
104
105 #[must_use]
107 pub const fn client_order_id_count(&self) -> usize {
108 self.order_id_generator.count()
109 }
110
111 pub const fn set_order_list_id_count(&mut self, count: usize) {
113 self.order_list_id_generator.set_count(count);
114 }
115
116 #[must_use]
118 pub const fn order_list_id_count(&self) -> usize {
119 self.order_list_id_generator.count()
120 }
121
122 pub fn generate_client_order_id(&mut self) -> ClientOrderId {
124 self.order_id_generator.generate()
125 }
126
127 pub fn generate_order_list_id(&mut self) -> OrderListId {
129 self.order_list_id_generator.generate()
130 }
131
132 pub const fn reset_factory(&mut self) {
134 self.order_id_generator.reset();
135 self.order_list_id_generator.reset();
136 }
137
138 #[expect(clippy::too_many_arguments)]
144 pub fn market(
145 &mut self,
146 instrument_id: InstrumentId,
147 order_side: OrderSide,
148 quantity: Quantity,
149 time_in_force: Option<TimeInForce>,
150 reduce_only: Option<bool>,
151 quote_quantity: Option<bool>,
152 exec_algorithm_id: Option<ExecAlgorithmId>,
153 exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
154 tags: Option<Vec<Ustr>>,
155 client_order_id: Option<ClientOrderId>,
156 ) -> OrderAny {
157 self.try_market(
158 instrument_id,
159 order_side,
160 quantity,
161 time_in_force,
162 reduce_only,
163 quote_quantity,
164 exec_algorithm_id,
165 exec_algorithm_params,
166 tags,
167 client_order_id,
168 )
169 .unwrap_or_else(|e| panic!("{e}"))
170 }
171
172 #[expect(clippy::too_many_arguments)]
173 pub(crate) fn try_market(
174 &mut self,
175 instrument_id: InstrumentId,
176 order_side: OrderSide,
177 quantity: Quantity,
178 time_in_force: Option<TimeInForce>,
179 reduce_only: Option<bool>,
180 quote_quantity: Option<bool>,
181 exec_algorithm_id: Option<ExecAlgorithmId>,
182 exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
183 tags: Option<Vec<Ustr>>,
184 client_order_id: Option<ClientOrderId>,
185 ) -> anyhow::Result<OrderAny> {
186 let client_order_id = client_order_id.unwrap_or_else(|| self.generate_client_order_id());
187 let exec_spawn_id: Option<ClientOrderId> = if exec_algorithm_id.is_none() {
188 None
189 } else {
190 Some(client_order_id)
191 };
192 let order = MarketOrder::new_checked(
193 self.trader_id,
194 self.strategy_id,
195 instrument_id,
196 client_order_id,
197 order_side,
198 quantity,
199 time_in_force.unwrap_or(TimeInForce::Gtc),
200 UUID4::new(),
201 self.clock.borrow().timestamp_ns(),
202 reduce_only.unwrap_or(false),
203 quote_quantity.unwrap_or(false),
204 None,
205 None,
206 None,
207 None,
208 exec_algorithm_id,
209 exec_algorithm_params,
210 exec_spawn_id,
211 tags,
212 )?;
213 Ok(OrderAny::Market(order))
214 }
215
216 #[expect(clippy::too_many_arguments)]
222 pub fn limit(
223 &mut self,
224 instrument_id: InstrumentId,
225 order_side: OrderSide,
226 quantity: Quantity,
227 price: Price,
228 time_in_force: Option<TimeInForce>,
229 expire_time: Option<nautilus_core::UnixNanos>,
230 post_only: Option<bool>,
231 reduce_only: Option<bool>,
232 quote_quantity: Option<bool>,
233 display_qty: Option<Quantity>,
234 emulation_trigger: Option<TriggerType>,
235 trigger_instrument_id: Option<InstrumentId>,
236 exec_algorithm_id: Option<ExecAlgorithmId>,
237 exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
238 tags: Option<Vec<Ustr>>,
239 client_order_id: Option<ClientOrderId>,
240 ) -> OrderAny {
241 self.try_limit(
242 instrument_id,
243 order_side,
244 quantity,
245 price,
246 time_in_force,
247 expire_time,
248 post_only,
249 reduce_only,
250 quote_quantity,
251 display_qty,
252 emulation_trigger,
253 trigger_instrument_id,
254 exec_algorithm_id,
255 exec_algorithm_params,
256 tags,
257 client_order_id,
258 )
259 .unwrap_or_else(|e| panic!("{e}"))
260 }
261
262 #[expect(clippy::too_many_arguments)]
263 pub(crate) fn try_limit(
264 &mut self,
265 instrument_id: InstrumentId,
266 order_side: OrderSide,
267 quantity: Quantity,
268 price: Price,
269 time_in_force: Option<TimeInForce>,
270 expire_time: Option<nautilus_core::UnixNanos>,
271 post_only: Option<bool>,
272 reduce_only: Option<bool>,
273 quote_quantity: Option<bool>,
274 display_qty: Option<Quantity>,
275 emulation_trigger: Option<TriggerType>,
276 trigger_instrument_id: Option<InstrumentId>,
277 exec_algorithm_id: Option<ExecAlgorithmId>,
278 exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
279 tags: Option<Vec<Ustr>>,
280 client_order_id: Option<ClientOrderId>,
281 ) -> anyhow::Result<OrderAny> {
282 let client_order_id = client_order_id.unwrap_or_else(|| self.generate_client_order_id());
283 let exec_spawn_id: Option<ClientOrderId> = if exec_algorithm_id.is_none() {
284 None
285 } else {
286 Some(client_order_id)
287 };
288 let order = LimitOrder::new_checked(
289 self.trader_id,
290 self.strategy_id,
291 instrument_id,
292 client_order_id,
293 order_side,
294 quantity,
295 price,
296 time_in_force.unwrap_or(TimeInForce::Gtc),
297 expire_time,
298 post_only.unwrap_or(false),
299 reduce_only.unwrap_or(false),
300 quote_quantity.unwrap_or(false),
301 display_qty,
302 emulation_trigger,
303 trigger_instrument_id,
304 None,
305 None,
306 None,
307 None,
308 exec_algorithm_id,
309 exec_algorithm_params,
310 exec_spawn_id,
311 tags,
312 UUID4::new(),
313 self.clock.borrow().timestamp_ns(),
314 )?;
315 Ok(OrderAny::Limit(order))
316 }
317
318 #[expect(clippy::too_many_arguments)]
324 pub fn stop_market(
325 &mut self,
326 instrument_id: InstrumentId,
327 order_side: OrderSide,
328 quantity: Quantity,
329 trigger_price: Price,
330 trigger_type: Option<TriggerType>,
331 time_in_force: Option<TimeInForce>,
332 expire_time: Option<nautilus_core::UnixNanos>,
333 reduce_only: Option<bool>,
334 quote_quantity: Option<bool>,
335 display_qty: Option<Quantity>,
336 emulation_trigger: Option<TriggerType>,
337 trigger_instrument_id: Option<InstrumentId>,
338 exec_algorithm_id: Option<ExecAlgorithmId>,
339 exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
340 tags: Option<Vec<Ustr>>,
341 client_order_id: Option<ClientOrderId>,
342 ) -> OrderAny {
343 self.try_stop_market(
344 instrument_id,
345 order_side,
346 quantity,
347 trigger_price,
348 trigger_type,
349 time_in_force,
350 expire_time,
351 reduce_only,
352 quote_quantity,
353 display_qty,
354 emulation_trigger,
355 trigger_instrument_id,
356 exec_algorithm_id,
357 exec_algorithm_params,
358 tags,
359 client_order_id,
360 )
361 .unwrap_or_else(|e| panic!("{e}"))
362 }
363
364 #[expect(clippy::too_many_arguments)]
365 pub(crate) fn try_stop_market(
366 &mut self,
367 instrument_id: InstrumentId,
368 order_side: OrderSide,
369 quantity: Quantity,
370 trigger_price: Price,
371 trigger_type: Option<TriggerType>,
372 time_in_force: Option<TimeInForce>,
373 expire_time: Option<nautilus_core::UnixNanos>,
374 reduce_only: Option<bool>,
375 quote_quantity: Option<bool>,
376 display_qty: Option<Quantity>,
377 emulation_trigger: Option<TriggerType>,
378 trigger_instrument_id: Option<InstrumentId>,
379 exec_algorithm_id: Option<ExecAlgorithmId>,
380 exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
381 tags: Option<Vec<Ustr>>,
382 client_order_id: Option<ClientOrderId>,
383 ) -> anyhow::Result<OrderAny> {
384 let client_order_id = client_order_id.unwrap_or_else(|| self.generate_client_order_id());
385 let exec_spawn_id: Option<ClientOrderId> = if exec_algorithm_id.is_none() {
386 None
387 } else {
388 Some(client_order_id)
389 };
390 let order = StopMarketOrder::new_checked(
391 self.trader_id,
392 self.strategy_id,
393 instrument_id,
394 client_order_id,
395 order_side,
396 quantity,
397 trigger_price,
398 trigger_type.unwrap_or(TriggerType::Default),
399 time_in_force.unwrap_or(TimeInForce::Gtc),
400 expire_time,
401 reduce_only.unwrap_or(false),
402 quote_quantity.unwrap_or(false),
403 display_qty,
404 emulation_trigger,
405 trigger_instrument_id,
406 None,
407 None,
408 None,
409 None,
410 exec_algorithm_id,
411 exec_algorithm_params,
412 exec_spawn_id,
413 tags,
414 UUID4::new(),
415 self.clock.borrow().timestamp_ns(),
416 )?;
417 Ok(OrderAny::StopMarket(order))
418 }
419
420 #[expect(clippy::too_many_arguments)]
426 pub fn stop_limit(
427 &mut self,
428 instrument_id: InstrumentId,
429 order_side: OrderSide,
430 quantity: Quantity,
431 price: Price,
432 trigger_price: Price,
433 trigger_type: Option<TriggerType>,
434 time_in_force: Option<TimeInForce>,
435 expire_time: Option<nautilus_core::UnixNanos>,
436 post_only: Option<bool>,
437 reduce_only: Option<bool>,
438 quote_quantity: Option<bool>,
439 display_qty: Option<Quantity>,
440 emulation_trigger: Option<TriggerType>,
441 trigger_instrument_id: Option<InstrumentId>,
442 exec_algorithm_id: Option<ExecAlgorithmId>,
443 exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
444 tags: Option<Vec<Ustr>>,
445 client_order_id: Option<ClientOrderId>,
446 ) -> OrderAny {
447 self.try_stop_limit(
448 instrument_id,
449 order_side,
450 quantity,
451 price,
452 trigger_price,
453 trigger_type,
454 time_in_force,
455 expire_time,
456 post_only,
457 reduce_only,
458 quote_quantity,
459 display_qty,
460 emulation_trigger,
461 trigger_instrument_id,
462 exec_algorithm_id,
463 exec_algorithm_params,
464 tags,
465 client_order_id,
466 )
467 .unwrap_or_else(|e| panic!("{e}"))
468 }
469
470 #[expect(clippy::too_many_arguments)]
471 pub(crate) fn try_stop_limit(
472 &mut self,
473 instrument_id: InstrumentId,
474 order_side: OrderSide,
475 quantity: Quantity,
476 price: Price,
477 trigger_price: Price,
478 trigger_type: Option<TriggerType>,
479 time_in_force: Option<TimeInForce>,
480 expire_time: Option<nautilus_core::UnixNanos>,
481 post_only: Option<bool>,
482 reduce_only: Option<bool>,
483 quote_quantity: Option<bool>,
484 display_qty: Option<Quantity>,
485 emulation_trigger: Option<TriggerType>,
486 trigger_instrument_id: Option<InstrumentId>,
487 exec_algorithm_id: Option<ExecAlgorithmId>,
488 exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
489 tags: Option<Vec<Ustr>>,
490 client_order_id: Option<ClientOrderId>,
491 ) -> anyhow::Result<OrderAny> {
492 let client_order_id = client_order_id.unwrap_or_else(|| self.generate_client_order_id());
493 let exec_spawn_id: Option<ClientOrderId> = if exec_algorithm_id.is_none() {
494 None
495 } else {
496 Some(client_order_id)
497 };
498 let order = StopLimitOrder::new_checked(
499 self.trader_id,
500 self.strategy_id,
501 instrument_id,
502 client_order_id,
503 order_side,
504 quantity,
505 price,
506 trigger_price,
507 trigger_type.unwrap_or(TriggerType::Default),
508 time_in_force.unwrap_or(TimeInForce::Gtc),
509 expire_time,
510 post_only.unwrap_or(false),
511 reduce_only.unwrap_or(false),
512 quote_quantity.unwrap_or(false),
513 display_qty,
514 emulation_trigger,
515 trigger_instrument_id,
516 None,
517 None,
518 None,
519 None,
520 exec_algorithm_id,
521 exec_algorithm_params,
522 exec_spawn_id,
523 tags,
524 UUID4::new(),
525 self.clock.borrow().timestamp_ns(),
526 )?;
527 Ok(OrderAny::StopLimit(order))
528 }
529
530 #[expect(clippy::too_many_arguments)]
536 pub fn market_to_limit(
537 &mut self,
538 instrument_id: InstrumentId,
539 order_side: OrderSide,
540 quantity: Quantity,
541 time_in_force: Option<TimeInForce>,
542 expire_time: Option<nautilus_core::UnixNanos>,
543 reduce_only: Option<bool>,
544 quote_quantity: Option<bool>,
545 display_qty: Option<Quantity>,
546 exec_algorithm_id: Option<ExecAlgorithmId>,
547 exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
548 tags: Option<Vec<Ustr>>,
549 client_order_id: Option<ClientOrderId>,
550 ) -> OrderAny {
551 self.try_market_to_limit(
552 instrument_id,
553 order_side,
554 quantity,
555 time_in_force,
556 expire_time,
557 reduce_only,
558 quote_quantity,
559 display_qty,
560 exec_algorithm_id,
561 exec_algorithm_params,
562 tags,
563 client_order_id,
564 )
565 .unwrap_or_else(|e| panic!("{e}"))
566 }
567
568 #[expect(clippy::too_many_arguments)]
569 pub(crate) fn try_market_to_limit(
570 &mut self,
571 instrument_id: InstrumentId,
572 order_side: OrderSide,
573 quantity: Quantity,
574 time_in_force: Option<TimeInForce>,
575 expire_time: Option<nautilus_core::UnixNanos>,
576 reduce_only: Option<bool>,
577 quote_quantity: Option<bool>,
578 display_qty: Option<Quantity>,
579 exec_algorithm_id: Option<ExecAlgorithmId>,
580 exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
581 tags: Option<Vec<Ustr>>,
582 client_order_id: Option<ClientOrderId>,
583 ) -> anyhow::Result<OrderAny> {
584 let client_order_id = client_order_id.unwrap_or_else(|| self.generate_client_order_id());
585 let exec_spawn_id: Option<ClientOrderId> = if exec_algorithm_id.is_none() {
586 None
587 } else {
588 Some(client_order_id)
589 };
590 let order = MarketToLimitOrder::new_checked(
591 self.trader_id,
592 self.strategy_id,
593 instrument_id,
594 client_order_id,
595 order_side,
596 quantity,
597 time_in_force.unwrap_or(TimeInForce::Gtc),
598 expire_time,
599 false, reduce_only.unwrap_or(false),
601 quote_quantity.unwrap_or(false),
602 display_qty,
603 None,
604 None,
605 None,
606 None,
607 exec_algorithm_id,
608 exec_algorithm_params,
609 exec_spawn_id,
610 tags,
611 UUID4::new(),
612 self.clock.borrow().timestamp_ns(),
613 )?;
614 Ok(OrderAny::MarketToLimit(order))
615 }
616
617 #[expect(clippy::too_many_arguments)]
623 pub fn market_if_touched(
624 &mut self,
625 instrument_id: InstrumentId,
626 order_side: OrderSide,
627 quantity: Quantity,
628 trigger_price: Price,
629 trigger_type: Option<TriggerType>,
630 time_in_force: Option<TimeInForce>,
631 expire_time: Option<nautilus_core::UnixNanos>,
632 reduce_only: Option<bool>,
633 quote_quantity: Option<bool>,
634 emulation_trigger: Option<TriggerType>,
635 trigger_instrument_id: Option<InstrumentId>,
636 exec_algorithm_id: Option<ExecAlgorithmId>,
637 exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
638 tags: Option<Vec<Ustr>>,
639 client_order_id: Option<ClientOrderId>,
640 ) -> OrderAny {
641 self.try_market_if_touched(
642 instrument_id,
643 order_side,
644 quantity,
645 trigger_price,
646 trigger_type,
647 time_in_force,
648 expire_time,
649 reduce_only,
650 quote_quantity,
651 emulation_trigger,
652 trigger_instrument_id,
653 exec_algorithm_id,
654 exec_algorithm_params,
655 tags,
656 client_order_id,
657 )
658 .unwrap_or_else(|e| panic!("{e}"))
659 }
660
661 #[expect(clippy::too_many_arguments)]
662 pub(crate) fn try_market_if_touched(
663 &mut self,
664 instrument_id: InstrumentId,
665 order_side: OrderSide,
666 quantity: Quantity,
667 trigger_price: Price,
668 trigger_type: Option<TriggerType>,
669 time_in_force: Option<TimeInForce>,
670 expire_time: Option<nautilus_core::UnixNanos>,
671 reduce_only: Option<bool>,
672 quote_quantity: Option<bool>,
673 emulation_trigger: Option<TriggerType>,
674 trigger_instrument_id: Option<InstrumentId>,
675 exec_algorithm_id: Option<ExecAlgorithmId>,
676 exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
677 tags: Option<Vec<Ustr>>,
678 client_order_id: Option<ClientOrderId>,
679 ) -> anyhow::Result<OrderAny> {
680 let client_order_id = client_order_id.unwrap_or_else(|| self.generate_client_order_id());
681 let exec_spawn_id: Option<ClientOrderId> = if exec_algorithm_id.is_none() {
682 None
683 } else {
684 Some(client_order_id)
685 };
686 let order = MarketIfTouchedOrder::new_checked(
687 self.trader_id,
688 self.strategy_id,
689 instrument_id,
690 client_order_id,
691 order_side,
692 quantity,
693 trigger_price,
694 trigger_type.unwrap_or(TriggerType::Default),
695 time_in_force.unwrap_or(TimeInForce::Gtc),
696 expire_time,
697 reduce_only.unwrap_or(false),
698 quote_quantity.unwrap_or(false),
699 emulation_trigger,
700 trigger_instrument_id,
701 None,
702 None,
703 None,
704 None,
705 exec_algorithm_id,
706 exec_algorithm_params,
707 exec_spawn_id,
708 tags,
709 UUID4::new(),
710 self.clock.borrow().timestamp_ns(),
711 )?;
712 Ok(OrderAny::MarketIfTouched(order))
713 }
714
715 #[expect(clippy::too_many_arguments)]
721 pub fn limit_if_touched(
722 &mut self,
723 instrument_id: InstrumentId,
724 order_side: OrderSide,
725 quantity: Quantity,
726 price: Price,
727 trigger_price: Price,
728 trigger_type: Option<TriggerType>,
729 time_in_force: Option<TimeInForce>,
730 expire_time: Option<nautilus_core::UnixNanos>,
731 post_only: Option<bool>,
732 reduce_only: Option<bool>,
733 quote_quantity: Option<bool>,
734 display_qty: Option<Quantity>,
735 emulation_trigger: Option<TriggerType>,
736 trigger_instrument_id: Option<InstrumentId>,
737 exec_algorithm_id: Option<ExecAlgorithmId>,
738 exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
739 tags: Option<Vec<Ustr>>,
740 client_order_id: Option<ClientOrderId>,
741 ) -> OrderAny {
742 self.try_limit_if_touched(
743 instrument_id,
744 order_side,
745 quantity,
746 price,
747 trigger_price,
748 trigger_type,
749 time_in_force,
750 expire_time,
751 post_only,
752 reduce_only,
753 quote_quantity,
754 display_qty,
755 emulation_trigger,
756 trigger_instrument_id,
757 exec_algorithm_id,
758 exec_algorithm_params,
759 tags,
760 client_order_id,
761 )
762 .unwrap_or_else(|e| panic!("{e}"))
763 }
764
765 #[expect(clippy::too_many_arguments)]
766 pub(crate) fn try_limit_if_touched(
767 &mut self,
768 instrument_id: InstrumentId,
769 order_side: OrderSide,
770 quantity: Quantity,
771 price: Price,
772 trigger_price: Price,
773 trigger_type: Option<TriggerType>,
774 time_in_force: Option<TimeInForce>,
775 expire_time: Option<nautilus_core::UnixNanos>,
776 post_only: Option<bool>,
777 reduce_only: Option<bool>,
778 quote_quantity: Option<bool>,
779 display_qty: Option<Quantity>,
780 emulation_trigger: Option<TriggerType>,
781 trigger_instrument_id: Option<InstrumentId>,
782 exec_algorithm_id: Option<ExecAlgorithmId>,
783 exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
784 tags: Option<Vec<Ustr>>,
785 client_order_id: Option<ClientOrderId>,
786 ) -> anyhow::Result<OrderAny> {
787 let client_order_id = client_order_id.unwrap_or_else(|| self.generate_client_order_id());
788 let exec_spawn_id: Option<ClientOrderId> = if exec_algorithm_id.is_none() {
789 None
790 } else {
791 Some(client_order_id)
792 };
793 let order = LimitIfTouchedOrder::new_checked(
794 self.trader_id,
795 self.strategy_id,
796 instrument_id,
797 client_order_id,
798 order_side,
799 quantity,
800 price,
801 trigger_price,
802 trigger_type.unwrap_or(TriggerType::Default),
803 time_in_force.unwrap_or(TimeInForce::Gtc),
804 expire_time,
805 post_only.unwrap_or(false),
806 reduce_only.unwrap_or(false),
807 quote_quantity.unwrap_or(false),
808 display_qty,
809 emulation_trigger,
810 trigger_instrument_id,
811 None,
812 None,
813 None,
814 None,
815 exec_algorithm_id,
816 exec_algorithm_params,
817 exec_spawn_id,
818 tags,
819 UUID4::new(),
820 self.clock.borrow().timestamp_ns(),
821 )?;
822 Ok(OrderAny::LimitIfTouched(order))
823 }
824
825 #[expect(clippy::too_many_arguments)]
831 pub fn trailing_stop_market(
832 &mut self,
833 instrument_id: InstrumentId,
834 order_side: OrderSide,
835 quantity: Quantity,
836 trailing_offset: Decimal,
837 trailing_offset_type: Option<TrailingOffsetType>,
838 activation_price: Option<Price>,
839 trigger_price: Option<Price>,
840 trigger_type: Option<TriggerType>,
841 time_in_force: Option<TimeInForce>,
842 expire_time: Option<nautilus_core::UnixNanos>,
843 reduce_only: Option<bool>,
844 quote_quantity: Option<bool>,
845 display_qty: Option<Quantity>,
846 emulation_trigger: Option<TriggerType>,
847 trigger_instrument_id: Option<InstrumentId>,
848 exec_algorithm_id: Option<ExecAlgorithmId>,
849 exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
850 tags: Option<Vec<Ustr>>,
851 client_order_id: Option<ClientOrderId>,
852 ) -> OrderAny {
853 self.try_trailing_stop_market(
854 instrument_id,
855 order_side,
856 quantity,
857 trailing_offset,
858 trailing_offset_type,
859 activation_price,
860 trigger_price,
861 trigger_type,
862 time_in_force,
863 expire_time,
864 reduce_only,
865 quote_quantity,
866 display_qty,
867 emulation_trigger,
868 trigger_instrument_id,
869 exec_algorithm_id,
870 exec_algorithm_params,
871 tags,
872 client_order_id,
873 )
874 .unwrap_or_else(|e| panic!("{e}"))
875 }
876
877 #[expect(clippy::too_many_arguments)]
878 pub(crate) fn try_trailing_stop_market(
879 &mut self,
880 instrument_id: InstrumentId,
881 order_side: OrderSide,
882 quantity: Quantity,
883 trailing_offset: Decimal,
884 trailing_offset_type: Option<TrailingOffsetType>,
885 activation_price: Option<Price>,
886 trigger_price: Option<Price>,
887 trigger_type: Option<TriggerType>,
888 time_in_force: Option<TimeInForce>,
889 expire_time: Option<nautilus_core::UnixNanos>,
890 reduce_only: Option<bool>,
891 quote_quantity: Option<bool>,
892 display_qty: Option<Quantity>,
893 emulation_trigger: Option<TriggerType>,
894 trigger_instrument_id: Option<InstrumentId>,
895 exec_algorithm_id: Option<ExecAlgorithmId>,
896 exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
897 tags: Option<Vec<Ustr>>,
898 client_order_id: Option<ClientOrderId>,
899 ) -> anyhow::Result<OrderAny> {
900 let client_order_id = client_order_id.unwrap_or_else(|| self.generate_client_order_id());
901 let exec_spawn_id: Option<ClientOrderId> = if exec_algorithm_id.is_none() {
902 None
903 } else {
904 Some(client_order_id)
905 };
906
907 let order = TrailingStopMarketOrder::new_checked(
911 self.trader_id,
912 self.strategy_id,
913 instrument_id,
914 client_order_id,
915 order_side,
916 quantity,
917 activation_price,
918 trigger_price,
919 trigger_type.unwrap_or(TriggerType::Default),
920 trailing_offset,
921 trailing_offset_type.unwrap_or(TrailingOffsetType::Price),
922 time_in_force.unwrap_or(TimeInForce::Gtc),
923 expire_time,
924 reduce_only.unwrap_or(false),
925 quote_quantity.unwrap_or(false),
926 display_qty,
927 emulation_trigger,
928 trigger_instrument_id,
929 None,
930 None,
931 None,
932 None,
933 exec_algorithm_id,
934 exec_algorithm_params,
935 exec_spawn_id,
936 tags,
937 UUID4::new(),
938 self.clock.borrow().timestamp_ns(),
939 )?;
940
941 Ok(OrderAny::TrailingStopMarket(order))
942 }
943
944 #[expect(clippy::too_many_arguments)]
950 pub fn trailing_stop_limit(
951 &mut self,
952 instrument_id: InstrumentId,
953 order_side: OrderSide,
954 quantity: Quantity,
955 price: Option<Price>,
956 limit_offset: Decimal,
957 trailing_offset: Decimal,
958 trailing_offset_type: Option<TrailingOffsetType>,
959 activation_price: Option<Price>,
960 trigger_price: Option<Price>,
961 trigger_type: Option<TriggerType>,
962 time_in_force: Option<TimeInForce>,
963 expire_time: Option<nautilus_core::UnixNanos>,
964 post_only: Option<bool>,
965 reduce_only: Option<bool>,
966 quote_quantity: Option<bool>,
967 display_qty: Option<Quantity>,
968 emulation_trigger: Option<TriggerType>,
969 trigger_instrument_id: Option<InstrumentId>,
970 exec_algorithm_id: Option<ExecAlgorithmId>,
971 exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
972 tags: Option<Vec<Ustr>>,
973 client_order_id: Option<ClientOrderId>,
974 ) -> OrderAny {
975 self.try_trailing_stop_limit(
976 instrument_id,
977 order_side,
978 quantity,
979 price,
980 limit_offset,
981 trailing_offset,
982 trailing_offset_type,
983 activation_price,
984 trigger_price,
985 trigger_type,
986 time_in_force,
987 expire_time,
988 post_only,
989 reduce_only,
990 quote_quantity,
991 display_qty,
992 emulation_trigger,
993 trigger_instrument_id,
994 exec_algorithm_id,
995 exec_algorithm_params,
996 tags,
997 client_order_id,
998 )
999 .unwrap_or_else(|e| panic!("{e}"))
1000 }
1001
1002 #[expect(clippy::too_many_arguments)]
1003 pub(crate) fn try_trailing_stop_limit(
1004 &mut self,
1005 instrument_id: InstrumentId,
1006 order_side: OrderSide,
1007 quantity: Quantity,
1008 price: Option<Price>,
1009 limit_offset: Decimal,
1010 trailing_offset: Decimal,
1011 trailing_offset_type: Option<TrailingOffsetType>,
1012 activation_price: Option<Price>,
1013 trigger_price: Option<Price>,
1014 trigger_type: Option<TriggerType>,
1015 time_in_force: Option<TimeInForce>,
1016 expire_time: Option<nautilus_core::UnixNanos>,
1017 post_only: Option<bool>,
1018 reduce_only: Option<bool>,
1019 quote_quantity: Option<bool>,
1020 display_qty: Option<Quantity>,
1021 emulation_trigger: Option<TriggerType>,
1022 trigger_instrument_id: Option<InstrumentId>,
1023 exec_algorithm_id: Option<ExecAlgorithmId>,
1024 exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
1025 tags: Option<Vec<Ustr>>,
1026 client_order_id: Option<ClientOrderId>,
1027 ) -> anyhow::Result<OrderAny> {
1028 let client_order_id = client_order_id.unwrap_or_else(|| self.generate_client_order_id());
1029 let exec_spawn_id: Option<ClientOrderId> = if exec_algorithm_id.is_none() {
1030 None
1031 } else {
1032 Some(client_order_id)
1033 };
1034
1035 let order = TrailingStopLimitOrder::new_checked(
1039 self.trader_id,
1040 self.strategy_id,
1041 instrument_id,
1042 client_order_id,
1043 order_side,
1044 quantity,
1045 activation_price,
1046 price,
1047 trigger_price,
1048 trigger_type.unwrap_or(TriggerType::Default),
1049 limit_offset,
1050 trailing_offset,
1051 trailing_offset_type.unwrap_or(TrailingOffsetType::Price),
1052 time_in_force.unwrap_or(TimeInForce::Gtc),
1053 expire_time,
1054 post_only.unwrap_or(false),
1055 reduce_only.unwrap_or(false),
1056 quote_quantity.unwrap_or(false),
1057 display_qty,
1058 emulation_trigger,
1059 trigger_instrument_id,
1060 None,
1061 None,
1062 None,
1063 None,
1064 exec_algorithm_id,
1065 exec_algorithm_params,
1066 exec_spawn_id,
1067 tags,
1068 UUID4::new(),
1069 self.clock.borrow().timestamp_ns(),
1070 )?;
1071
1072 Ok(OrderAny::TrailingStopLimit(order))
1073 }
1074
1075 #[must_use]
1089 pub fn create_list(&mut self, orders: &mut [OrderAny], ts_init: UnixNanos) -> OrderList {
1090 let instrument_id = orders
1091 .first()
1092 .expect("OrderFactory::create_list requires non-empty orders")
1093 .instrument_id();
1094 let venue = instrument_id.venue;
1095
1096 for order in orders.iter() {
1097 assert!(
1098 order.instrument_id().venue == venue,
1099 "OrderFactory::create_list requires all orders to share the same venue; \
1100 expected {venue}, found {} on {}",
1101 order.instrument_id().venue,
1102 order.client_order_id(),
1103 );
1104 }
1105
1106 let order_list_id = self.generate_order_list_id();
1107 let order_ids: Vec<ClientOrderId> = orders.iter().map(OrderAny::client_order_id).collect();
1108
1109 for order in orders.iter_mut() {
1110 order.set_order_list_id(order_list_id);
1111 }
1112
1113 OrderList::new(
1114 order_list_id,
1115 instrument_id,
1116 self.strategy_id,
1117 order_ids,
1118 ts_init,
1119 )
1120 }
1121
1122 #[builder]
1137 pub fn bracket(
1138 &mut self,
1139 instrument_id: InstrumentId,
1140 order_side: OrderSide,
1141 quantity: Quantity,
1142 #[builder(default = false)] quote_quantity: bool,
1143 emulation_trigger: Option<TriggerType>,
1144 trigger_instrument_id: Option<InstrumentId>,
1145 #[builder(default = ContingencyType::Ouo)] contingency_type: ContingencyType,
1146 #[builder(default = OrderType::Market)] entry_order_type: OrderType,
1148 entry_price: Option<Price>,
1149 entry_trigger_price: Option<Price>,
1150 expire_time: Option<nautilus_core::UnixNanos>,
1151 #[builder(default = TimeInForce::Gtc)] time_in_force: TimeInForce,
1152 #[builder(default = false)] entry_post_only: bool,
1153 entry_exec_algorithm_id: Option<ExecAlgorithmId>,
1154 entry_exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
1155 #[builder(default = vec![Ustr::from("ENTRY")])] entry_tags: Vec<Ustr>,
1156 entry_client_order_id: Option<ClientOrderId>,
1157 #[builder(default = OrderType::Limit)] tp_order_type: OrderType,
1159 tp_price: Option<Price>,
1160 tp_trigger_price: Option<Price>,
1161 #[builder(default = TriggerType::Default)] tp_trigger_type: TriggerType,
1162 tp_activation_price: Option<Price>,
1163 tp_trailing_offset: Option<Decimal>,
1164 #[builder(default = TrailingOffsetType::Price)] tp_trailing_offset_type: TrailingOffsetType,
1165 tp_limit_offset: Option<Decimal>,
1166 #[builder(default = TimeInForce::Gtc)] tp_time_in_force: TimeInForce,
1167 #[builder(default = true)] tp_post_only: bool,
1168 tp_exec_algorithm_id: Option<ExecAlgorithmId>,
1169 tp_exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
1170 #[builder(default = vec![Ustr::from("TAKE_PROFIT")])] tp_tags: Vec<Ustr>,
1171 tp_client_order_id: Option<ClientOrderId>,
1172 #[builder(default = OrderType::StopMarket)] sl_order_type: OrderType,
1174 sl_trigger_price: Option<Price>,
1175 #[builder(default = TriggerType::Default)] sl_trigger_type: TriggerType,
1176 sl_activation_price: Option<Price>,
1177 sl_trailing_offset: Option<Decimal>,
1178 #[builder(default = TrailingOffsetType::Price)] sl_trailing_offset_type: TrailingOffsetType,
1179 #[builder(default = TimeInForce::Gtc)] sl_time_in_force: TimeInForce,
1180 sl_exec_algorithm_id: Option<ExecAlgorithmId>,
1181 sl_exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
1182 #[builder(default = vec![Ustr::from("STOP_LOSS")])] sl_tags: Vec<Ustr>,
1183 sl_client_order_id: Option<ClientOrderId>,
1184 ) -> Vec<OrderAny> {
1185 self.try_bracket()
1186 .instrument_id(instrument_id)
1187 .order_side(order_side)
1188 .quantity(quantity)
1189 .quote_quantity(quote_quantity)
1190 .maybe_emulation_trigger(emulation_trigger)
1191 .maybe_trigger_instrument_id(trigger_instrument_id)
1192 .contingency_type(contingency_type)
1193 .entry_order_type(entry_order_type)
1194 .maybe_entry_price(entry_price)
1195 .maybe_entry_trigger_price(entry_trigger_price)
1196 .maybe_expire_time(expire_time)
1197 .time_in_force(time_in_force)
1198 .entry_post_only(entry_post_only)
1199 .maybe_entry_exec_algorithm_id(entry_exec_algorithm_id)
1200 .maybe_entry_exec_algorithm_params(entry_exec_algorithm_params)
1201 .entry_tags(entry_tags)
1202 .maybe_entry_client_order_id(entry_client_order_id)
1203 .tp_order_type(tp_order_type)
1204 .maybe_tp_price(tp_price)
1205 .maybe_tp_trigger_price(tp_trigger_price)
1206 .tp_trigger_type(tp_trigger_type)
1207 .maybe_tp_activation_price(tp_activation_price)
1208 .maybe_tp_trailing_offset(tp_trailing_offset)
1209 .tp_trailing_offset_type(tp_trailing_offset_type)
1210 .maybe_tp_limit_offset(tp_limit_offset)
1211 .tp_time_in_force(tp_time_in_force)
1212 .tp_post_only(tp_post_only)
1213 .maybe_tp_exec_algorithm_id(tp_exec_algorithm_id)
1214 .maybe_tp_exec_algorithm_params(tp_exec_algorithm_params)
1215 .tp_tags(tp_tags)
1216 .maybe_tp_client_order_id(tp_client_order_id)
1217 .sl_order_type(sl_order_type)
1218 .maybe_sl_trigger_price(sl_trigger_price)
1219 .sl_trigger_type(sl_trigger_type)
1220 .maybe_sl_activation_price(sl_activation_price)
1221 .maybe_sl_trailing_offset(sl_trailing_offset)
1222 .sl_trailing_offset_type(sl_trailing_offset_type)
1223 .sl_time_in_force(sl_time_in_force)
1224 .maybe_sl_exec_algorithm_id(sl_exec_algorithm_id)
1225 .maybe_sl_exec_algorithm_params(sl_exec_algorithm_params)
1226 .sl_tags(sl_tags)
1227 .maybe_sl_client_order_id(sl_client_order_id)
1228 .call()
1229 .unwrap_or_else(|e| panic!("{e}"))
1230 }
1231
1232 #[expect(clippy::too_many_lines)]
1233 #[builder]
1234 pub(crate) fn try_bracket(
1235 &mut self,
1236 instrument_id: InstrumentId,
1237 order_side: OrderSide,
1238 quantity: Quantity,
1239 #[builder(default = false)] quote_quantity: bool,
1240 emulation_trigger: Option<TriggerType>,
1241 trigger_instrument_id: Option<InstrumentId>,
1242 #[builder(default = ContingencyType::Ouo)] contingency_type: ContingencyType,
1243 #[builder(default = OrderType::Market)] entry_order_type: OrderType,
1245 entry_price: Option<Price>,
1246 entry_trigger_price: Option<Price>,
1247 expire_time: Option<nautilus_core::UnixNanos>,
1248 #[builder(default = TimeInForce::Gtc)] time_in_force: TimeInForce,
1249 #[builder(default = false)] entry_post_only: bool,
1250 entry_exec_algorithm_id: Option<ExecAlgorithmId>,
1251 entry_exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
1252 #[builder(default = vec![Ustr::from("ENTRY")])] entry_tags: Vec<Ustr>,
1253 entry_client_order_id: Option<ClientOrderId>,
1254 #[builder(default = OrderType::Limit)] tp_order_type: OrderType,
1256 tp_price: Option<Price>,
1257 tp_trigger_price: Option<Price>,
1258 #[builder(default = TriggerType::Default)] tp_trigger_type: TriggerType,
1259 tp_activation_price: Option<Price>,
1260 tp_trailing_offset: Option<Decimal>,
1261 #[builder(default = TrailingOffsetType::Price)] tp_trailing_offset_type: TrailingOffsetType,
1262 tp_limit_offset: Option<Decimal>,
1263 #[builder(default = TimeInForce::Gtc)] tp_time_in_force: TimeInForce,
1264 #[builder(default = true)] tp_post_only: bool,
1265 tp_exec_algorithm_id: Option<ExecAlgorithmId>,
1266 tp_exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
1267 #[builder(default = vec![Ustr::from("TAKE_PROFIT")])] tp_tags: Vec<Ustr>,
1268 tp_client_order_id: Option<ClientOrderId>,
1269 #[builder(default = OrderType::StopMarket)] sl_order_type: OrderType,
1271 sl_trigger_price: Option<Price>,
1272 #[builder(default = TriggerType::Default)] sl_trigger_type: TriggerType,
1273 sl_activation_price: Option<Price>,
1274 sl_trailing_offset: Option<Decimal>,
1275 #[builder(default = TrailingOffsetType::Price)] sl_trailing_offset_type: TrailingOffsetType,
1276 #[builder(default = TimeInForce::Gtc)] sl_time_in_force: TimeInForce,
1277 sl_exec_algorithm_id: Option<ExecAlgorithmId>,
1278 sl_exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
1279 #[builder(default = vec![Ustr::from("STOP_LOSS")])] sl_tags: Vec<Ustr>,
1280 sl_client_order_id: Option<ClientOrderId>,
1281 ) -> anyhow::Result<Vec<OrderAny>> {
1282 let order_list_id = self.generate_order_list_id();
1283 let ts_init = self.clock.borrow().timestamp_ns();
1284
1285 let entry_client_order_id =
1286 entry_client_order_id.unwrap_or_else(|| self.generate_client_order_id());
1287 let sl_client_order_id =
1288 sl_client_order_id.unwrap_or_else(|| self.generate_client_order_id());
1289 let tp_client_order_id =
1290 tp_client_order_id.unwrap_or_else(|| self.generate_client_order_id());
1291
1292 let entry_exec_spawn_id = entry_exec_algorithm_id
1293 .as_ref()
1294 .map(|_| entry_client_order_id);
1295 let tp_exec_spawn_id = tp_exec_algorithm_id.as_ref().map(|_| tp_client_order_id);
1296 let sl_exec_spawn_id = sl_exec_algorithm_id.as_ref().map(|_| sl_client_order_id);
1297
1298 let entry_tags = Some(entry_tags);
1299 let tp_tags = Some(tp_tags);
1300 let sl_tags = Some(sl_tags);
1301
1302 let entry_contingency_type = Some(ContingencyType::Oto);
1303 let entry_order_list_id = Some(order_list_id);
1304 let entry_linked_order_ids = Some(vec![sl_client_order_id, tp_client_order_id]);
1305 let entry_parent_order_id: Option<ClientOrderId> = None;
1306
1307 let entry_order = match entry_order_type {
1308 OrderType::Market => OrderAny::Market(MarketOrder::new_checked(
1309 self.trader_id,
1310 self.strategy_id,
1311 instrument_id,
1312 entry_client_order_id,
1313 order_side,
1314 quantity,
1315 time_in_force,
1316 UUID4::new(),
1317 ts_init,
1318 false, quote_quantity,
1320 entry_contingency_type,
1321 entry_order_list_id,
1322 entry_linked_order_ids,
1323 entry_parent_order_id,
1324 entry_exec_algorithm_id,
1325 entry_exec_algorithm_params,
1326 entry_exec_spawn_id,
1327 entry_tags,
1328 )?),
1329 OrderType::Limit => OrderAny::Limit(LimitOrder::new_checked(
1330 self.trader_id,
1331 self.strategy_id,
1332 instrument_id,
1333 entry_client_order_id,
1334 order_side,
1335 quantity,
1336 required(entry_price, "`entry_price` is required for a LIMIT entry")?,
1337 time_in_force,
1338 expire_time,
1339 entry_post_only,
1340 false, quote_quantity,
1342 None, emulation_trigger,
1344 trigger_instrument_id,
1345 entry_contingency_type,
1346 entry_order_list_id,
1347 entry_linked_order_ids,
1348 entry_parent_order_id,
1349 entry_exec_algorithm_id,
1350 entry_exec_algorithm_params,
1351 entry_exec_spawn_id,
1352 entry_tags,
1353 UUID4::new(),
1354 ts_init,
1355 )?),
1356 OrderType::MarketIfTouched => {
1357 OrderAny::MarketIfTouched(MarketIfTouchedOrder::new_checked(
1358 self.trader_id,
1359 self.strategy_id,
1360 instrument_id,
1361 entry_client_order_id,
1362 order_side,
1363 quantity,
1364 required(
1365 entry_trigger_price,
1366 "`entry_trigger_price` is required for a MARKET_IF_TOUCHED entry",
1367 )?,
1368 TriggerType::Default,
1369 time_in_force,
1370 expire_time,
1371 false, quote_quantity,
1373 emulation_trigger,
1374 trigger_instrument_id,
1375 entry_contingency_type,
1376 entry_order_list_id,
1377 entry_linked_order_ids,
1378 entry_parent_order_id,
1379 entry_exec_algorithm_id,
1380 entry_exec_algorithm_params,
1381 entry_exec_spawn_id,
1382 entry_tags,
1383 UUID4::new(),
1384 ts_init,
1385 )?)
1386 }
1387 OrderType::LimitIfTouched => {
1388 OrderAny::LimitIfTouched(LimitIfTouchedOrder::new_checked(
1389 self.trader_id,
1390 self.strategy_id,
1391 instrument_id,
1392 entry_client_order_id,
1393 order_side,
1394 quantity,
1395 required(
1396 entry_price,
1397 "`entry_price` is required for a LIMIT_IF_TOUCHED entry",
1398 )?,
1399 required(
1400 entry_trigger_price,
1401 "`entry_trigger_price` is required for a LIMIT_IF_TOUCHED entry",
1402 )?,
1403 TriggerType::Default,
1404 time_in_force,
1405 expire_time,
1406 entry_post_only,
1407 false, quote_quantity,
1409 None, emulation_trigger,
1411 trigger_instrument_id,
1412 entry_contingency_type,
1413 entry_order_list_id,
1414 entry_linked_order_ids,
1415 entry_parent_order_id,
1416 entry_exec_algorithm_id,
1417 entry_exec_algorithm_params,
1418 entry_exec_spawn_id,
1419 entry_tags,
1420 UUID4::new(),
1421 ts_init,
1422 )?)
1423 }
1424 OrderType::StopLimit => OrderAny::StopLimit(StopLimitOrder::new_checked(
1425 self.trader_id,
1426 self.strategy_id,
1427 instrument_id,
1428 entry_client_order_id,
1429 order_side,
1430 quantity,
1431 required(
1432 entry_price,
1433 "`entry_price` is required for a STOP_LIMIT entry",
1434 )?,
1435 required(
1436 entry_trigger_price,
1437 "`entry_trigger_price` is required for a STOP_LIMIT entry",
1438 )?,
1439 TriggerType::Default,
1440 time_in_force,
1441 expire_time,
1442 entry_post_only,
1443 false, quote_quantity,
1445 None, emulation_trigger,
1447 trigger_instrument_id,
1448 entry_contingency_type,
1449 entry_order_list_id,
1450 entry_linked_order_ids,
1451 entry_parent_order_id,
1452 entry_exec_algorithm_id,
1453 entry_exec_algorithm_params,
1454 entry_exec_spawn_id,
1455 entry_tags,
1456 UUID4::new(),
1457 ts_init,
1458 )?),
1459 other => anyhow::bail!("invalid `entry_order_type`, was {other}"),
1460 };
1461
1462 let sl_tp_side = match order_side {
1463 OrderSide::Buy => OrderSide::Sell,
1464 OrderSide::Sell => OrderSide::Buy,
1465 };
1466
1467 let tp_contingency_type = Some(contingency_type);
1468 let tp_order_list_id = Some(order_list_id);
1469 let tp_linked_order_ids = Some(vec![sl_client_order_id]);
1470 let tp_parent_order_id = Some(entry_client_order_id);
1471
1472 let tp_order = match tp_order_type {
1473 OrderType::Limit => OrderAny::Limit(LimitOrder::new_checked(
1474 self.trader_id,
1475 self.strategy_id,
1476 instrument_id,
1477 tp_client_order_id,
1478 sl_tp_side,
1479 quantity,
1480 required(tp_price, "`tp_price` is required for a LIMIT take-profit")?,
1481 tp_time_in_force,
1482 None, tp_post_only,
1484 true, quote_quantity,
1486 None, emulation_trigger,
1488 trigger_instrument_id,
1489 tp_contingency_type,
1490 tp_order_list_id,
1491 tp_linked_order_ids,
1492 tp_parent_order_id,
1493 tp_exec_algorithm_id,
1494 tp_exec_algorithm_params,
1495 tp_exec_spawn_id,
1496 tp_tags,
1497 UUID4::new(),
1498 ts_init,
1499 )?),
1500 OrderType::LimitIfTouched => {
1501 OrderAny::LimitIfTouched(LimitIfTouchedOrder::new_checked(
1502 self.trader_id,
1503 self.strategy_id,
1504 instrument_id,
1505 tp_client_order_id,
1506 sl_tp_side,
1507 quantity,
1508 required(
1509 tp_price,
1510 "`tp_price` is required for a LIMIT_IF_TOUCHED take-profit",
1511 )?,
1512 required(
1513 tp_trigger_price,
1514 "`tp_trigger_price` is required for a LIMIT_IF_TOUCHED take-profit",
1515 )?,
1516 tp_trigger_type,
1517 tp_time_in_force,
1518 None, tp_post_only,
1520 true, quote_quantity,
1522 None, emulation_trigger,
1524 trigger_instrument_id,
1525 tp_contingency_type,
1526 tp_order_list_id,
1527 tp_linked_order_ids,
1528 tp_parent_order_id,
1529 tp_exec_algorithm_id,
1530 tp_exec_algorithm_params,
1531 tp_exec_spawn_id,
1532 tp_tags,
1533 UUID4::new(),
1534 ts_init,
1535 )?)
1536 }
1537 OrderType::MarketIfTouched => {
1538 OrderAny::MarketIfTouched(MarketIfTouchedOrder::new_checked(
1539 self.trader_id,
1540 self.strategy_id,
1541 instrument_id,
1542 tp_client_order_id,
1543 sl_tp_side,
1544 quantity,
1545 required(
1546 tp_trigger_price,
1547 "`tp_trigger_price` is required for a MARKET_IF_TOUCHED take-profit",
1548 )?,
1549 tp_trigger_type,
1550 tp_time_in_force,
1551 None, true, quote_quantity,
1554 emulation_trigger,
1555 trigger_instrument_id,
1556 tp_contingency_type,
1557 tp_order_list_id,
1558 tp_linked_order_ids,
1559 tp_parent_order_id,
1560 tp_exec_algorithm_id,
1561 tp_exec_algorithm_params,
1562 tp_exec_spawn_id,
1563 tp_tags,
1564 UUID4::new(),
1565 ts_init,
1566 )?)
1567 }
1568 OrderType::TrailingStopMarket => {
1569 let tp_trailing_offset = required(
1570 tp_trailing_offset,
1571 "`tp_trailing_offset` is required for a TRAILING_STOP_MARKET take-profit",
1572 )?;
1573 let trigger_price = required(
1574 tp_trigger_price.or(tp_activation_price),
1575 "TRAILING_STOP_MARKET take-profit requires `tp_trigger_price` or `tp_activation_price`",
1576 )?;
1577 let order = TrailingStopMarketOrder::new_checked(
1578 self.trader_id,
1579 self.strategy_id,
1580 instrument_id,
1581 tp_client_order_id,
1582 sl_tp_side,
1583 quantity,
1584 tp_activation_price,
1585 Some(trigger_price),
1586 tp_trigger_type,
1587 tp_trailing_offset,
1588 tp_trailing_offset_type,
1589 tp_time_in_force,
1590 None, true, quote_quantity,
1593 None, emulation_trigger,
1595 trigger_instrument_id,
1596 tp_contingency_type,
1597 tp_order_list_id,
1598 tp_linked_order_ids,
1599 tp_parent_order_id,
1600 tp_exec_algorithm_id,
1601 tp_exec_algorithm_params,
1602 tp_exec_spawn_id,
1603 tp_tags,
1604 UUID4::new(),
1605 ts_init,
1606 )?;
1607 OrderAny::TrailingStopMarket(order)
1608 }
1609 OrderType::TrailingStopLimit => {
1610 let tp_trailing_offset = required(
1611 tp_trailing_offset,
1612 "`tp_trailing_offset` is required for a TRAILING_STOP_LIMIT take-profit",
1613 )?;
1614 let tp_limit_offset = required(
1615 tp_limit_offset,
1616 "`tp_limit_offset` is required for a TRAILING_STOP_LIMIT take-profit",
1617 )?;
1618 let trigger_price = required(
1619 tp_trigger_price.or(tp_activation_price),
1620 "TRAILING_STOP_LIMIT take-profit requires `tp_trigger_price` or `tp_activation_price`",
1621 )?;
1622 let price = required(
1623 tp_price,
1624 "`tp_price` is required for a TRAILING_STOP_LIMIT take-profit",
1625 )?;
1626 let order = TrailingStopLimitOrder::new_checked(
1627 self.trader_id,
1628 self.strategy_id,
1629 instrument_id,
1630 tp_client_order_id,
1631 sl_tp_side,
1632 quantity,
1633 tp_activation_price,
1634 Some(price),
1635 Some(trigger_price),
1636 tp_trigger_type,
1637 tp_limit_offset,
1638 tp_trailing_offset,
1639 tp_trailing_offset_type,
1640 tp_time_in_force,
1641 None, false, true, quote_quantity,
1645 None, emulation_trigger,
1647 trigger_instrument_id,
1648 tp_contingency_type,
1649 tp_order_list_id,
1650 tp_linked_order_ids,
1651 tp_parent_order_id,
1652 tp_exec_algorithm_id,
1653 tp_exec_algorithm_params,
1654 tp_exec_spawn_id,
1655 tp_tags,
1656 UUID4::new(),
1657 ts_init,
1658 )?;
1659 OrderAny::TrailingStopLimit(order)
1660 }
1661 other => anyhow::bail!("invalid `tp_order_type`, was {other}"),
1662 };
1663
1664 let sl_contingency_type = Some(contingency_type);
1665 let sl_order_list_id = Some(order_list_id);
1666 let sl_linked_order_ids = Some(vec![tp_client_order_id]);
1667 let sl_parent_order_id = Some(entry_client_order_id);
1668
1669 let sl_order = match sl_order_type {
1670 OrderType::StopMarket => OrderAny::StopMarket(StopMarketOrder::new_checked(
1671 self.trader_id,
1672 self.strategy_id,
1673 instrument_id,
1674 sl_client_order_id,
1675 sl_tp_side,
1676 quantity,
1677 required(
1678 sl_trigger_price,
1679 "`sl_trigger_price` is required for a STOP_MARKET stop-loss",
1680 )?,
1681 sl_trigger_type,
1682 sl_time_in_force,
1683 None, true, quote_quantity,
1686 None, emulation_trigger,
1688 trigger_instrument_id,
1689 sl_contingency_type,
1690 sl_order_list_id,
1691 sl_linked_order_ids,
1692 sl_parent_order_id,
1693 sl_exec_algorithm_id,
1694 sl_exec_algorithm_params,
1695 sl_exec_spawn_id,
1696 sl_tags,
1697 UUID4::new(),
1698 ts_init,
1699 )?),
1700 OrderType::TrailingStopMarket => {
1701 let sl_trailing_offset = required(
1702 sl_trailing_offset,
1703 "`sl_trailing_offset` is required for a TRAILING_STOP_MARKET stop-loss",
1704 )?;
1705 let trigger_price = required(
1706 sl_trigger_price.or(sl_activation_price),
1707 "TRAILING_STOP_MARKET stop-loss requires `sl_trigger_price` or `sl_activation_price`",
1708 )?;
1709 let order = TrailingStopMarketOrder::new_checked(
1710 self.trader_id,
1711 self.strategy_id,
1712 instrument_id,
1713 sl_client_order_id,
1714 sl_tp_side,
1715 quantity,
1716 sl_activation_price,
1717 Some(trigger_price),
1718 sl_trigger_type,
1719 sl_trailing_offset,
1720 sl_trailing_offset_type,
1721 sl_time_in_force,
1722 None, true, quote_quantity,
1725 None, emulation_trigger,
1727 trigger_instrument_id,
1728 sl_contingency_type,
1729 sl_order_list_id,
1730 sl_linked_order_ids,
1731 sl_parent_order_id,
1732 sl_exec_algorithm_id,
1733 sl_exec_algorithm_params,
1734 sl_exec_spawn_id,
1735 sl_tags,
1736 UUID4::new(),
1737 ts_init,
1738 )?;
1739 OrderAny::TrailingStopMarket(order)
1740 }
1741 other => anyhow::bail!("invalid `sl_order_type`, was {other}"),
1742 };
1743
1744 Ok(vec![entry_order, sl_order, tp_order])
1745 }
1746}
1747
1748fn required<T>(value: Option<T>, message: &'static str) -> anyhow::Result<T> {
1749 value.ok_or_else(|| anyhow::anyhow!(message))
1750}
1751
1752#[cfg(test)]
1753pub mod tests {
1754 use std::{cell::RefCell, rc::Rc};
1755
1756 use indexmap::IndexMap;
1757 use nautilus_core::UnixNanos;
1758 use nautilus_model::{
1759 enums::{
1760 ContingencyType, OrderSide, OrderType, TimeInForce, TrailingOffsetType, TriggerType,
1761 },
1762 identifiers::{
1763 ClientOrderId, ExecAlgorithmId, InstrumentId, OrderListId,
1764 stubs::{strategy_id_ema_cross, trader_id},
1765 },
1766 orders::Order,
1767 types::Price,
1768 };
1769 use rstest::{fixture, rstest};
1770 use rust_decimal::Decimal;
1771 use ustr::Ustr;
1772
1773 use crate::{clock::TestClock, factories::OrderFactory};
1774
1775 #[fixture]
1776 pub fn order_factory() -> OrderFactory {
1777 let trader_id = trader_id();
1778 let strategy_id = strategy_id_ema_cross();
1779 let clock = Rc::new(RefCell::new(TestClock::new()));
1780 OrderFactory::new(
1781 trader_id,
1782 strategy_id,
1783 None,
1784 None,
1785 clock,
1786 false, true, )
1789 }
1790
1791 #[rstest]
1792 fn test_order_factory_identity(order_factory: OrderFactory) {
1793 assert_eq!(order_factory.trader_id(), trader_id());
1794 assert_eq!(order_factory.strategy_id(), strategy_id_ema_cross());
1795 }
1796
1797 #[rstest]
1798 fn test_generate_client_order_id(mut order_factory: OrderFactory) {
1799 let client_order_id = order_factory.generate_client_order_id();
1800 assert_eq!(
1801 client_order_id,
1802 ClientOrderId::new("O-19700101-000000-001-001-1")
1803 );
1804 }
1805
1806 #[rstest]
1807 fn test_generate_order_list_id(mut order_factory: OrderFactory) {
1808 let order_list_id = order_factory.generate_order_list_id();
1809 assert_eq!(
1810 order_list_id,
1811 OrderListId::new("OL-19700101-000000-001-001-1")
1812 );
1813 }
1814
1815 #[rstest]
1816 fn test_set_client_order_id_count(mut order_factory: OrderFactory) {
1817 order_factory.set_client_order_id_count(10);
1818 let client_order_id = order_factory.generate_client_order_id();
1819 assert_eq!(
1820 client_order_id,
1821 ClientOrderId::new("O-19700101-000000-001-001-11")
1822 );
1823 }
1824
1825 #[rstest]
1826 fn test_set_order_list_id_count(mut order_factory: OrderFactory) {
1827 order_factory.set_order_list_id_count(10);
1828 let order_list_id = order_factory.generate_order_list_id();
1829 assert_eq!(
1830 order_list_id,
1831 OrderListId::new("OL-19700101-000000-001-001-11")
1832 );
1833 }
1834
1835 #[rstest]
1836 fn test_reset_factory(mut order_factory: OrderFactory) {
1837 order_factory.generate_order_list_id();
1838 order_factory.generate_client_order_id();
1839 order_factory.reset_factory();
1840 let client_order_id = order_factory.generate_client_order_id();
1841 let order_list_id = order_factory.generate_order_list_id();
1842 assert_eq!(
1843 client_order_id,
1844 ClientOrderId::new("O-19700101-000000-001-001-1")
1845 );
1846 assert_eq!(
1847 order_list_id,
1848 OrderListId::new("OL-19700101-000000-001-001-1")
1849 );
1850 }
1851
1852 #[fixture]
1853 pub fn order_factory_with_uuids() -> OrderFactory {
1854 let trader_id = trader_id();
1855 let strategy_id = strategy_id_ema_cross();
1856 let clock = Rc::new(RefCell::new(TestClock::new()));
1857 OrderFactory::new(
1858 trader_id,
1859 strategy_id,
1860 None,
1861 None,
1862 clock,
1863 true, true, )
1866 }
1867
1868 #[fixture]
1869 pub fn order_factory_with_hyphens_removed() -> OrderFactory {
1870 let trader_id = trader_id();
1871 let strategy_id = strategy_id_ema_cross();
1872 let clock = Rc::new(RefCell::new(TestClock::new()));
1873 OrderFactory::new(
1874 trader_id,
1875 strategy_id,
1876 None,
1877 None,
1878 clock,
1879 false, false, )
1882 }
1883
1884 #[fixture]
1885 pub fn order_factory_with_uuids_and_hyphens_removed() -> OrderFactory {
1886 let trader_id = trader_id();
1887 let strategy_id = strategy_id_ema_cross();
1888 let clock = Rc::new(RefCell::new(TestClock::new()));
1889 OrderFactory::new(
1890 trader_id,
1891 strategy_id,
1892 None,
1893 None,
1894 clock,
1895 true, false, )
1898 }
1899
1900 #[rstest]
1901 fn test_generate_client_order_id_with_uuids(mut order_factory_with_uuids: OrderFactory) {
1902 let client_order_id = order_factory_with_uuids.generate_client_order_id();
1903
1904 assert_eq!(client_order_id.as_str().len(), 36);
1906 assert!(client_order_id.as_str().contains('-'));
1907 }
1908
1909 #[rstest]
1910 fn test_generate_client_order_id_with_hyphens_removed(
1911 mut order_factory_with_hyphens_removed: OrderFactory,
1912 ) {
1913 let client_order_id = order_factory_with_hyphens_removed.generate_client_order_id();
1914
1915 assert_eq!(
1916 client_order_id,
1917 ClientOrderId::new("O197001010000000010011")
1918 );
1919 assert!(!client_order_id.as_str().contains('-'));
1920 }
1921
1922 #[rstest]
1923 fn test_generate_client_order_id_with_uuids_and_hyphens_removed(
1924 mut order_factory_with_uuids_and_hyphens_removed: OrderFactory,
1925 ) {
1926 let client_order_id =
1927 order_factory_with_uuids_and_hyphens_removed.generate_client_order_id();
1928
1929 assert_eq!(client_order_id.as_str().len(), 32);
1931 assert!(!client_order_id.as_str().contains('-'));
1932 }
1933
1934 #[rstest]
1935 fn test_market_order(mut order_factory: OrderFactory) {
1936 let market_order = order_factory.market(
1937 InstrumentId::from("BTCUSDT.BINANCE"),
1938 OrderSide::Buy,
1939 100.into(),
1940 Some(TimeInForce::Gtc),
1941 Some(false),
1942 Some(false),
1943 None,
1944 None,
1945 None,
1946 None,
1947 );
1948 assert_eq!(market_order.instrument_id(), "BTCUSDT.BINANCE".into());
1950 assert_eq!(market_order.order_side(), OrderSide::Buy);
1951 assert_eq!(market_order.quantity(), 100.into());
1952 assert_eq!(market_order.exec_algorithm_id(), None);
1956 assert_eq!(
1960 market_order.client_order_id(),
1961 ClientOrderId::new("O-19700101-000000-001-001-1")
1962 );
1963 }
1965
1966 #[rstest]
1967 fn test_market_order_preserves_primary_exec_algorithm_metadata(
1968 mut order_factory: OrderFactory,
1969 ) {
1970 let client_order_id = ClientOrderId::from("O-PRIMARY");
1971 let exec_algorithm_id = ExecAlgorithmId::from("TWAP");
1972 let exec_algorithm_params =
1973 IndexMap::from([(Ustr::from("interval_secs"), Ustr::from("10"))]);
1974 let order = order_factory.market(
1975 InstrumentId::from("BTCUSDT.BINANCE"),
1976 OrderSide::Buy,
1977 100.into(),
1978 Some(TimeInForce::Gtc),
1979 Some(false),
1980 Some(false),
1981 Some(exec_algorithm_id),
1982 Some(exec_algorithm_params.clone()),
1983 None,
1984 Some(client_order_id),
1985 );
1986
1987 assert_eq!(order.client_order_id(), client_order_id);
1988 assert_eq!(order.exec_algorithm_id(), Some(exec_algorithm_id));
1989 assert_eq!(order.exec_algorithm_params(), Some(&exec_algorithm_params));
1990 assert_eq!(order.exec_spawn_id(), Some(client_order_id));
1991 assert!(order.is_primary());
1992 assert!(!order.is_spawned());
1993 }
1994
1995 #[rstest]
1996 fn test_limit_order(mut order_factory: OrderFactory) {
1997 let limit_order = order_factory.limit(
1998 InstrumentId::from("BTCUSDT.BINANCE"),
1999 OrderSide::Buy,
2000 100.into(),
2001 Price::from("50000.00"),
2002 Some(TimeInForce::Gtc),
2003 None,
2004 Some(false),
2005 Some(false),
2006 Some(false),
2007 None,
2008 None,
2009 None,
2010 None,
2011 None,
2012 None,
2013 None,
2014 );
2015
2016 assert_eq!(limit_order.instrument_id(), "BTCUSDT.BINANCE".into());
2017 assert_eq!(limit_order.order_side(), OrderSide::Buy);
2018 assert_eq!(limit_order.quantity(), 100.into());
2019 assert_eq!(limit_order.price(), Some(Price::from("50000.00")));
2020 assert_eq!(
2021 limit_order.client_order_id(),
2022 ClientOrderId::new("O-19700101-000000-001-001-1")
2023 );
2024 }
2025
2026 #[rstest]
2027 fn test_limit_order_with_post_only(mut order_factory: OrderFactory) {
2028 let limit_order = order_factory.limit(
2029 InstrumentId::from("BTCUSDT.BINANCE"),
2030 OrderSide::Buy,
2031 100.into(),
2032 Price::from("50000.00"),
2033 Some(TimeInForce::Gtc),
2034 None,
2035 Some(true), Some(false),
2037 Some(false),
2038 None,
2039 None,
2040 None,
2041 None,
2042 None,
2043 None,
2044 None,
2045 );
2046
2047 assert!(limit_order.is_post_only());
2048 }
2049
2050 #[rstest]
2051 fn test_limit_order_with_display_qty(mut order_factory: OrderFactory) {
2052 let limit_order = order_factory.limit(
2053 InstrumentId::from("BTCUSDT.BINANCE"),
2054 OrderSide::Buy,
2055 100.into(),
2056 Price::from("50000.00"),
2057 Some(TimeInForce::Gtc),
2058 None,
2059 Some(false), Some(false), Some(false), Some(50.into()), None,
2064 None,
2065 None,
2066 None,
2067 None,
2068 None,
2069 );
2070
2071 assert_eq!(limit_order.display_qty(), Some(50.into()));
2072 }
2073
2074 #[rstest]
2075 fn test_stop_market_order(mut order_factory: OrderFactory) {
2076 let stop_order = order_factory.stop_market(
2077 InstrumentId::from("BTCUSDT.BINANCE"),
2078 OrderSide::Sell,
2079 100.into(),
2080 Price::from("45000.00"),
2081 Some(TriggerType::LastPrice),
2082 Some(TimeInForce::Gtc),
2083 None,
2084 Some(false),
2085 Some(false),
2086 None,
2087 None,
2088 None,
2089 None,
2090 None,
2091 None,
2092 None,
2093 );
2094
2095 assert_eq!(stop_order.instrument_id(), "BTCUSDT.BINANCE".into());
2096 assert_eq!(stop_order.order_side(), OrderSide::Sell);
2097 assert_eq!(stop_order.quantity(), 100.into());
2098 assert_eq!(stop_order.trigger_price(), Some(Price::from("45000.00")));
2099 assert_eq!(stop_order.trigger_type(), Some(TriggerType::LastPrice));
2100 }
2101
2102 #[rstest]
2103 fn test_stop_limit_order(mut order_factory: OrderFactory) {
2104 let stop_limit_order = order_factory.stop_limit(
2105 InstrumentId::from("BTCUSDT.BINANCE"),
2106 OrderSide::Sell,
2107 100.into(),
2108 Price::from("45100.00"), Price::from("45000.00"), Some(TriggerType::LastPrice),
2111 Some(TimeInForce::Gtc),
2112 None,
2113 Some(false),
2114 Some(false),
2115 Some(false),
2116 None,
2117 None,
2118 None,
2119 None,
2120 None,
2121 None,
2122 None,
2123 );
2124
2125 assert_eq!(stop_limit_order.instrument_id(), "BTCUSDT.BINANCE".into());
2126 assert_eq!(stop_limit_order.order_side(), OrderSide::Sell);
2127 assert_eq!(stop_limit_order.quantity(), 100.into());
2128 assert_eq!(stop_limit_order.price(), Some(Price::from("45100.00")));
2129 assert_eq!(
2130 stop_limit_order.trigger_price(),
2131 Some(Price::from("45000.00"))
2132 );
2133 assert_eq!(
2134 stop_limit_order.trigger_type(),
2135 Some(TriggerType::LastPrice)
2136 );
2137 }
2138
2139 #[rstest]
2140 fn test_market_if_touched_order(mut order_factory: OrderFactory) {
2141 let mit_order = order_factory.market_if_touched(
2142 InstrumentId::from("BTCUSDT.BINANCE"),
2143 OrderSide::Buy,
2144 100.into(),
2145 Price::from("48000.00"),
2146 Some(TriggerType::LastPrice),
2147 Some(TimeInForce::Gtc),
2148 None,
2149 Some(false),
2150 Some(false),
2151 None,
2152 None,
2153 None,
2154 None,
2155 None,
2156 None,
2157 );
2158
2159 assert_eq!(mit_order.instrument_id(), "BTCUSDT.BINANCE".into());
2160 assert_eq!(mit_order.order_side(), OrderSide::Buy);
2161 assert_eq!(mit_order.quantity(), 100.into());
2162 assert_eq!(mit_order.trigger_price(), Some(Price::from("48000.00")));
2163 assert_eq!(mit_order.trigger_type(), Some(TriggerType::LastPrice));
2164 }
2165
2166 #[rstest]
2167 fn test_limit_if_touched_order(mut order_factory: OrderFactory) {
2168 let lit_order = order_factory.limit_if_touched(
2169 InstrumentId::from("BTCUSDT.BINANCE"),
2170 OrderSide::Buy,
2171 100.into(),
2172 Price::from("48100.00"), Price::from("48000.00"), Some(TriggerType::LastPrice),
2175 Some(TimeInForce::Gtc),
2176 None,
2177 Some(false),
2178 Some(false),
2179 Some(false),
2180 None,
2181 None,
2182 None,
2183 None,
2184 None,
2185 None,
2186 None,
2187 );
2188
2189 assert_eq!(lit_order.instrument_id(), "BTCUSDT.BINANCE".into());
2190 assert_eq!(lit_order.order_side(), OrderSide::Buy);
2191 assert_eq!(lit_order.quantity(), 100.into());
2192 assert_eq!(lit_order.price(), Some(Price::from("48100.00")));
2193 assert_eq!(lit_order.trigger_price(), Some(Price::from("48000.00")));
2194 assert_eq!(lit_order.trigger_type(), Some(TriggerType::LastPrice));
2195 }
2196
2197 #[rstest]
2198 fn test_market_to_limit_order(mut order_factory: OrderFactory) {
2199 let mtl_order = order_factory.market_to_limit(
2200 InstrumentId::from("BTCUSDT.BINANCE"),
2201 OrderSide::Buy,
2202 100.into(),
2203 Some(TimeInForce::Gtc),
2204 None,
2205 Some(false),
2206 Some(false),
2207 None,
2208 None,
2209 None,
2210 None,
2211 None,
2212 );
2213
2214 assert_eq!(mtl_order.instrument_id(), "BTCUSDT.BINANCE".into());
2215 assert_eq!(mtl_order.order_side(), OrderSide::Buy);
2216 assert_eq!(mtl_order.quantity(), 100.into());
2217 assert_eq!(mtl_order.order_type(), OrderType::MarketToLimit);
2218 assert_eq!(
2219 mtl_order.client_order_id(),
2220 ClientOrderId::new("O-19700101-000000-001-001-1")
2221 );
2222 }
2223
2224 #[rstest]
2225 fn test_trailing_stop_limit_order(mut order_factory: OrderFactory) {
2226 let tsl_order = order_factory.trailing_stop_limit(
2227 InstrumentId::from("BTCUSDT.BINANCE"),
2228 OrderSide::Sell,
2229 100.into(),
2230 Some(Price::from("45100.00")), Decimal::new(10, 2), Decimal::new(50, 2), Some(TrailingOffsetType::Price),
2234 Some(Price::from("45000.00")),
2235 Some(Price::from("45000.00")), Some(TriggerType::LastPrice),
2237 Some(TimeInForce::Gtc),
2238 None,
2239 Some(false), Some(true), Some(false), None,
2243 None,
2244 None,
2245 None,
2246 None,
2247 None,
2248 None,
2249 );
2250
2251 assert_eq!(tsl_order.instrument_id(), "BTCUSDT.BINANCE".into());
2252 assert_eq!(tsl_order.order_side(), OrderSide::Sell);
2253 assert_eq!(tsl_order.order_type(), OrderType::TrailingStopLimit);
2254 assert_eq!(tsl_order.price(), Some(Price::from("45100.00")));
2255 assert_eq!(tsl_order.activation_price(), Some(Price::from("45000.00")));
2256 assert_eq!(tsl_order.trigger_price(), Some(Price::from("45000.00")));
2257 assert_eq!(tsl_order.trigger_type(), Some(TriggerType::LastPrice));
2258 assert_eq!(tsl_order.trailing_offset(), Some(Decimal::new(50, 2)));
2259 assert_eq!(tsl_order.limit_offset(), Some(Decimal::new(10, 2)));
2260 }
2261
2262 #[rstest]
2263 fn test_bracket_order_with_market_entry(mut order_factory: OrderFactory) {
2264 let orders = order_factory
2265 .bracket()
2266 .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
2267 .order_side(OrderSide::Buy)
2268 .quantity(100.into())
2269 .tp_price(Price::from("55000.00"))
2270 .sl_trigger_price(Price::from("45000.00"))
2271 .call();
2272
2273 assert_eq!(orders.len(), 3);
2274 assert_eq!(orders[0].instrument_id(), "BTCUSDT.BINANCE".into());
2275
2276 assert_eq!(orders[0].order_side(), OrderSide::Buy);
2278
2279 assert_eq!(orders[1].order_side(), OrderSide::Sell);
2281 assert_eq!(orders[1].trigger_price(), Some(Price::from("45000.00")));
2282
2283 assert_eq!(orders[2].order_side(), OrderSide::Sell);
2285 assert_eq!(orders[2].price(), Some(Price::from("55000.00")));
2286 }
2287
2288 #[rstest]
2289 fn test_bracket_order_with_limit_entry(mut order_factory: OrderFactory) {
2290 let orders = order_factory
2291 .bracket()
2292 .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
2293 .order_side(OrderSide::Buy)
2294 .quantity(100.into())
2295 .entry_order_type(OrderType::Limit)
2296 .entry_price(Price::from("49000.00"))
2297 .tp_price(Price::from("55000.00"))
2298 .sl_trigger_price(Price::from("45000.00"))
2299 .call();
2300
2301 assert_eq!(orders.len(), 3);
2302 assert_eq!(orders[0].price(), Some(Price::from("49000.00")));
2303 }
2304
2305 #[rstest]
2306 fn test_bracket_order_with_stop_limit_entry(mut order_factory: OrderFactory) {
2307 let orders = order_factory
2308 .bracket()
2309 .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
2310 .order_side(OrderSide::Buy)
2311 .quantity(100.into())
2312 .entry_order_type(OrderType::StopLimit)
2313 .entry_price(Price::from("51500.00"))
2314 .entry_trigger_price(Price::from("51000.00"))
2315 .tp_price(Price::from("55000.00"))
2316 .sl_trigger_price(Price::from("45000.00"))
2317 .call();
2318
2319 assert_eq!(orders.len(), 3);
2320 assert_eq!(orders[0].trigger_price(), Some(Price::from("51000.00")));
2321 assert_eq!(orders[0].price(), Some(Price::from("51500.00")));
2322 }
2323
2324 #[rstest]
2325 fn test_bracket_order_sell_side(mut order_factory: OrderFactory) {
2326 let orders = order_factory
2327 .bracket()
2328 .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
2329 .order_side(OrderSide::Sell)
2330 .quantity(100.into())
2331 .entry_order_type(OrderType::Limit)
2332 .entry_price(Price::from("51000.00"))
2333 .tp_price(Price::from("45000.00"))
2334 .sl_trigger_price(Price::from("55000.00"))
2335 .call();
2336
2337 assert_eq!(orders.len(), 3);
2338 assert_eq!(orders[0].order_side(), OrderSide::Sell);
2339 assert_eq!(orders[1].order_side(), OrderSide::Buy);
2340 assert_eq!(orders[2].order_side(), OrderSide::Buy);
2341 }
2342
2343 #[rstest]
2344 fn test_bracket_order_sets_contingencies(mut order_factory: OrderFactory) {
2345 let orders = order_factory
2346 .bracket()
2347 .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
2348 .order_side(OrderSide::Buy)
2349 .quantity(100.into())
2350 .entry_order_type(OrderType::Limit)
2351 .entry_price(Price::from("50000.00"))
2352 .tp_price(Price::from("55000.00"))
2353 .sl_trigger_price(Price::from("45000.00"))
2354 .call();
2355
2356 let entry = &orders[0];
2357 let stop = &orders[1];
2358 let take = &orders[2];
2359
2360 let order_list_id = entry
2361 .order_list_id()
2362 .expect("Entry should have order_list_id");
2363 assert_eq!(entry.contingency_type(), Some(ContingencyType::Oto));
2364 assert_eq!(
2365 entry.linked_order_ids().unwrap(),
2366 &[stop.client_order_id(), take.client_order_id()]
2367 );
2368
2369 assert_eq!(stop.order_list_id(), Some(order_list_id));
2370 assert_eq!(stop.contingency_type(), Some(ContingencyType::Ouo));
2371 assert_eq!(stop.parent_order_id(), Some(entry.client_order_id()));
2372 assert_eq!(stop.linked_order_ids().unwrap(), &[take.client_order_id()]);
2373
2374 assert_eq!(take.order_list_id(), Some(order_list_id));
2375 assert_eq!(take.contingency_type(), Some(ContingencyType::Ouo));
2376 assert_eq!(take.parent_order_id(), Some(entry.client_order_id()));
2377 assert_eq!(take.linked_order_ids().unwrap(), &[stop.client_order_id()]);
2378 }
2379
2380 #[rstest]
2381 fn test_bracket_order_default_tags(mut order_factory: OrderFactory) {
2382 let orders = order_factory
2383 .bracket()
2384 .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
2385 .order_side(OrderSide::Buy)
2386 .quantity(100.into())
2387 .tp_price(Price::from("55000.00"))
2388 .sl_trigger_price(Price::from("45000.00"))
2389 .call();
2390
2391 assert_eq!(orders[0].tags(), Some(&vec![Ustr::from("ENTRY")][..]));
2392 assert_eq!(orders[1].tags(), Some(&vec![Ustr::from("STOP_LOSS")][..]));
2393 assert_eq!(orders[2].tags(), Some(&vec![Ustr::from("TAKE_PROFIT")][..]));
2394 }
2395
2396 #[rstest]
2397 fn test_bracket_order_custom_tags(mut order_factory: OrderFactory) {
2398 let orders = order_factory
2399 .bracket()
2400 .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
2401 .order_side(OrderSide::Buy)
2402 .quantity(100.into())
2403 .tp_price(Price::from("55000.00"))
2404 .sl_trigger_price(Price::from("45000.00"))
2405 .entry_tags(vec![Ustr::from("ALPHA"), Ustr::from("ENTRY-V2")])
2406 .tp_tags(vec![Ustr::from("TP-V2")])
2407 .sl_tags(vec![Ustr::from("SL-V2")])
2408 .call();
2409
2410 assert_eq!(
2411 orders[0].tags(),
2412 Some(&vec![Ustr::from("ALPHA"), Ustr::from("ENTRY-V2")][..])
2413 );
2414 assert_eq!(orders[1].tags(), Some(&vec![Ustr::from("SL-V2")][..]));
2415 assert_eq!(orders[2].tags(), Some(&vec![Ustr::from("TP-V2")][..]));
2416 }
2417
2418 #[rstest]
2419 fn test_bracket_order_custom_contingency_type(mut order_factory: OrderFactory) {
2420 let orders = order_factory
2421 .bracket()
2422 .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
2423 .order_side(OrderSide::Buy)
2424 .quantity(100.into())
2425 .contingency_type(ContingencyType::Oco)
2426 .tp_price(Price::from("55000.00"))
2427 .sl_trigger_price(Price::from("45000.00"))
2428 .call();
2429
2430 assert_eq!(orders[1].contingency_type(), Some(ContingencyType::Oco));
2431 assert_eq!(orders[2].contingency_type(), Some(ContingencyType::Oco));
2432 }
2433
2434 #[rstest]
2435 fn test_bracket_order_custom_client_order_ids(mut order_factory: OrderFactory) {
2436 let entry_id = ClientOrderId::new("CUSTOM-ENTRY");
2437 let tp_id = ClientOrderId::new("CUSTOM-TP");
2438 let sl_id = ClientOrderId::new("CUSTOM-SL");
2439
2440 let orders = order_factory
2441 .bracket()
2442 .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
2443 .order_side(OrderSide::Buy)
2444 .quantity(100.into())
2445 .tp_price(Price::from("55000.00"))
2446 .sl_trigger_price(Price::from("45000.00"))
2447 .entry_client_order_id(entry_id)
2448 .tp_client_order_id(tp_id)
2449 .sl_client_order_id(sl_id)
2450 .call();
2451
2452 assert_eq!(orders[0].client_order_id(), entry_id);
2453 assert_eq!(orders[1].client_order_id(), sl_id);
2454 assert_eq!(orders[2].client_order_id(), tp_id);
2455 }
2456
2457 #[rstest]
2458 fn test_bracket_order_per_leg_order_types(mut order_factory: OrderFactory) {
2459 let orders = order_factory
2460 .bracket()
2461 .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
2462 .order_side(OrderSide::Buy)
2463 .quantity(100.into())
2464 .entry_order_type(OrderType::Limit)
2465 .entry_price(Price::from("50000.00"))
2466 .tp_order_type(OrderType::MarketIfTouched)
2467 .tp_trigger_price(Price::from("55000.00"))
2468 .tp_trigger_type(TriggerType::LastPrice)
2469 .sl_order_type(OrderType::TrailingStopMarket)
2470 .sl_trigger_price(Price::from("45000.00"))
2471 .sl_activation_price(Price::from("44000.00"))
2472 .sl_trailing_offset(Decimal::new(50, 2))
2473 .sl_trailing_offset_type(TrailingOffsetType::BasisPoints)
2474 .call();
2475
2476 assert_eq!(orders[0].order_type(), OrderType::Limit);
2478 assert_eq!(orders[1].order_type(), OrderType::TrailingStopMarket);
2480 assert_eq!(orders[1].trigger_price(), Some(Price::from("45000.00")));
2481 assert_eq!(orders[1].activation_price(), Some(Price::from("44000.00")));
2482 assert_eq!(orders[1].trailing_offset(), Some(Decimal::new(50, 2)));
2483 assert_eq!(
2484 orders[1].trailing_offset_type(),
2485 Some(TrailingOffsetType::BasisPoints)
2486 );
2487 assert_eq!(orders[2].order_type(), OrderType::MarketIfTouched);
2489 assert_eq!(orders[2].trigger_price(), Some(Price::from("55000.00")));
2490 assert_eq!(orders[2].trigger_type(), Some(TriggerType::LastPrice));
2491 }
2492
2493 #[rstest]
2494 fn test_bracket_order_reduce_only_flags(mut order_factory: OrderFactory) {
2495 let orders = order_factory
2496 .bracket()
2497 .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
2498 .order_side(OrderSide::Buy)
2499 .quantity(100.into())
2500 .entry_order_type(OrderType::Limit)
2501 .entry_price(Price::from("50000.00"))
2502 .tp_price(Price::from("55000.00"))
2503 .sl_trigger_price(Price::from("45000.00"))
2504 .call();
2505
2506 assert!(!orders[0].is_reduce_only(), "entry must not be reduce-only");
2507 assert!(orders[1].is_reduce_only(), "SL must be reduce-only");
2508 assert!(orders[2].is_reduce_only(), "TP must be reduce-only");
2509 }
2510
2511 #[rstest]
2512 fn test_bracket_order_default_post_only(mut order_factory: OrderFactory) {
2513 let orders = order_factory
2514 .bracket()
2515 .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
2516 .order_side(OrderSide::Buy)
2517 .quantity(100.into())
2518 .entry_order_type(OrderType::Limit)
2519 .entry_price(Price::from("50000.00"))
2520 .tp_price(Price::from("55000.00"))
2521 .sl_trigger_price(Price::from("45000.00"))
2522 .call();
2523
2524 assert!(!orders[0].is_post_only(), "entry default is not post-only");
2525 assert!(orders[2].is_post_only(), "Limit TP default is post-only");
2526 }
2527
2528 #[rstest]
2529 fn test_bracket_order_trailing_stop_limit_tp_forces_no_post_only(
2530 mut order_factory: OrderFactory,
2531 ) {
2532 let orders = order_factory
2533 .bracket()
2534 .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
2535 .order_side(OrderSide::Buy)
2536 .quantity(100.into())
2537 .tp_order_type(OrderType::TrailingStopLimit)
2538 .tp_price(Price::from("55000.00"))
2539 .tp_trigger_price(Price::from("54000.00"))
2540 .tp_trailing_offset(Decimal::new(50, 2))
2541 .tp_limit_offset(Decimal::new(10, 2))
2542 .tp_post_only(true) .sl_trigger_price(Price::from("45000.00"))
2544 .call();
2545
2546 assert_eq!(orders[2].order_type(), OrderType::TrailingStopLimit);
2547 assert!(
2548 !orders[2].is_post_only(),
2549 "TRAILING_STOP_LIMIT TP must never be post-only"
2550 );
2551 }
2552
2553 #[rstest]
2554 fn test_bracket_order_expire_time_entry_only(mut order_factory: OrderFactory) {
2555 let expire_time = UnixNanos::from(1_700_000_000_000_000_000_u64);
2556 let orders = order_factory
2557 .bracket()
2558 .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
2559 .order_side(OrderSide::Buy)
2560 .quantity(100.into())
2561 .entry_order_type(OrderType::Limit)
2562 .entry_price(Price::from("50000.00"))
2563 .expire_time(expire_time)
2564 .time_in_force(TimeInForce::Gtd)
2565 .tp_price(Price::from("55000.00"))
2566 .sl_trigger_price(Price::from("45000.00"))
2567 .call();
2568
2569 assert_eq!(orders[0].expire_time(), Some(expire_time));
2570 assert_eq!(orders[1].expire_time(), None);
2571 assert_eq!(orders[2].expire_time(), None);
2572 }
2573
2574 #[rstest]
2575 fn test_bracket_order_with_market_if_touched_entry(mut order_factory: OrderFactory) {
2576 let orders = order_factory
2577 .bracket()
2578 .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
2579 .order_side(OrderSide::Buy)
2580 .quantity(100.into())
2581 .entry_order_type(OrderType::MarketIfTouched)
2582 .entry_trigger_price(Price::from("51000.00"))
2583 .tp_price(Price::from("55000.00"))
2584 .sl_trigger_price(Price::from("45000.00"))
2585 .call();
2586
2587 assert_eq!(orders[0].order_type(), OrderType::MarketIfTouched);
2588 assert_eq!(orders[0].trigger_price(), Some(Price::from("51000.00")));
2589 }
2590
2591 #[rstest]
2592 fn test_bracket_order_with_limit_if_touched_entry(mut order_factory: OrderFactory) {
2593 let orders = order_factory
2594 .bracket()
2595 .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
2596 .order_side(OrderSide::Buy)
2597 .quantity(100.into())
2598 .entry_order_type(OrderType::LimitIfTouched)
2599 .entry_price(Price::from("51500.00"))
2600 .entry_trigger_price(Price::from("51000.00"))
2601 .tp_price(Price::from("55000.00"))
2602 .sl_trigger_price(Price::from("45000.00"))
2603 .call();
2604
2605 assert_eq!(orders[0].order_type(), OrderType::LimitIfTouched);
2606 assert_eq!(orders[0].price(), Some(Price::from("51500.00")));
2607 assert_eq!(orders[0].trigger_price(), Some(Price::from("51000.00")));
2608 }
2609
2610 #[rstest]
2611 fn test_bracket_order_with_limit_if_touched_tp(mut order_factory: OrderFactory) {
2612 let orders = order_factory
2614 .bracket()
2615 .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
2616 .order_side(OrderSide::Buy)
2617 .quantity(100.into())
2618 .tp_order_type(OrderType::LimitIfTouched)
2619 .tp_price(Price::from("54500.00"))
2620 .tp_trigger_price(Price::from("55000.00"))
2621 .sl_trigger_price(Price::from("45000.00"))
2622 .call();
2623
2624 assert_eq!(orders[2].order_type(), OrderType::LimitIfTouched);
2625 assert_eq!(orders[2].price(), Some(Price::from("54500.00")));
2626 assert_eq!(orders[2].trigger_price(), Some(Price::from("55000.00")));
2627 assert!(
2628 orders[2].is_post_only(),
2629 "LimitIfTouched TP default is post-only"
2630 );
2631 }
2632
2633 #[rstest]
2634 fn test_bracket_order_with_trailing_stop_market_tp(mut order_factory: OrderFactory) {
2635 let orders = order_factory
2636 .bracket()
2637 .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
2638 .order_side(OrderSide::Buy)
2639 .quantity(100.into())
2640 .tp_order_type(OrderType::TrailingStopMarket)
2641 .tp_trigger_price(Price::from("55000.00"))
2642 .tp_activation_price(Price::from("54500.00"))
2643 .tp_trailing_offset(Decimal::new(75, 2))
2644 .sl_trigger_price(Price::from("45000.00"))
2645 .call();
2646
2647 assert_eq!(orders[2].order_type(), OrderType::TrailingStopMarket);
2648 assert_eq!(orders[2].trigger_price(), Some(Price::from("55000.00")));
2649 assert_eq!(orders[2].activation_price(), Some(Price::from("54500.00")));
2650 assert_eq!(orders[2].trailing_offset(), Some(Decimal::new(75, 2)));
2651 }
2652
2653 #[rstest]
2654 fn test_bracket_order_with_trailing_stop_limit_tp(mut order_factory: OrderFactory) {
2655 let orders = order_factory
2656 .bracket()
2657 .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
2658 .order_side(OrderSide::Buy)
2659 .quantity(100.into())
2660 .tp_order_type(OrderType::TrailingStopLimit)
2661 .tp_price(Price::from("55000.00"))
2662 .tp_trigger_price(Price::from("54000.00"))
2663 .tp_activation_price(Price::from("53500.00"))
2664 .tp_trailing_offset(Decimal::new(50, 2))
2665 .tp_limit_offset(Decimal::new(10, 2))
2666 .sl_trigger_price(Price::from("45000.00"))
2667 .call();
2668
2669 assert_eq!(orders[2].order_type(), OrderType::TrailingStopLimit);
2670 assert_eq!(orders[2].price(), Some(Price::from("55000.00")));
2671 assert_eq!(orders[2].trigger_price(), Some(Price::from("54000.00")));
2672 assert_eq!(orders[2].activation_price(), Some(Price::from("53500.00")));
2673 assert_eq!(orders[2].trailing_offset(), Some(Decimal::new(50, 2)));
2674 assert_eq!(orders[2].limit_offset(), Some(Decimal::new(10, 2)));
2675 }
2676
2677 #[rstest]
2678 #[should_panic(expected = "`tp_price` is required for a LIMIT take-profit")]
2679 fn test_bracket_order_panics_on_missing_tp_price(mut order_factory: OrderFactory) {
2680 let _ = order_factory
2681 .bracket()
2682 .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
2683 .order_side(OrderSide::Buy)
2684 .quantity(100.into())
2685 .sl_trigger_price(Price::from("45000.00"))
2686 .call();
2687 }
2688
2689 #[rstest]
2690 #[should_panic(expected = "`sl_trigger_price` is required for a STOP_MARKET stop-loss")]
2691 fn test_bracket_order_panics_on_missing_sl_trigger_price(mut order_factory: OrderFactory) {
2692 let _ = order_factory
2693 .bracket()
2694 .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
2695 .order_side(OrderSide::Buy)
2696 .quantity(100.into())
2697 .tp_price(Price::from("55000.00"))
2698 .call();
2699 }
2700
2701 #[rstest]
2702 #[should_panic(
2703 expected = "`tp_trailing_offset` is required for a TRAILING_STOP_MARKET take-profit"
2704 )]
2705 fn test_bracket_order_panics_on_missing_tp_trailing_offset(mut order_factory: OrderFactory) {
2706 let _ = order_factory
2707 .bracket()
2708 .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
2709 .order_side(OrderSide::Buy)
2710 .quantity(100.into())
2711 .tp_order_type(OrderType::TrailingStopMarket)
2712 .tp_trigger_price(Price::from("55000.00"))
2713 .sl_trigger_price(Price::from("45000.00"))
2714 .call();
2715 }
2716
2717 #[rstest]
2718 #[should_panic(expected = "invalid `entry_order_type`")]
2719 fn test_bracket_order_panics_on_invalid_entry_order_type(mut order_factory: OrderFactory) {
2720 let _ = order_factory
2721 .bracket()
2722 .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
2723 .order_side(OrderSide::Buy)
2724 .quantity(100.into())
2725 .entry_order_type(OrderType::MarketToLimit)
2726 .tp_price(Price::from("55000.00"))
2727 .sl_trigger_price(Price::from("45000.00"))
2728 .call();
2729 }
2730
2731 #[rstest]
2732 #[should_panic(expected = "invalid `tp_order_type`")]
2733 fn test_bracket_order_panics_on_invalid_tp_order_type(mut order_factory: OrderFactory) {
2734 let _ = order_factory
2735 .bracket()
2736 .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
2737 .order_side(OrderSide::Buy)
2738 .quantity(100.into())
2739 .tp_order_type(OrderType::StopMarket)
2740 .tp_price(Price::from("55000.00"))
2741 .sl_trigger_price(Price::from("45000.00"))
2742 .call();
2743 }
2744
2745 #[rstest]
2746 #[should_panic(expected = "invalid `sl_order_type`")]
2747 fn test_bracket_order_panics_on_invalid_sl_order_type(mut order_factory: OrderFactory) {
2748 let _ = order_factory
2749 .bracket()
2750 .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
2751 .order_side(OrderSide::Buy)
2752 .quantity(100.into())
2753 .sl_order_type(OrderType::Limit)
2754 .tp_price(Price::from("55000.00"))
2755 .sl_trigger_price(Price::from("45000.00"))
2756 .call();
2757 }
2758
2759 #[rstest]
2760 #[should_panic(expected = "share the same venue")]
2761 fn test_create_list_panics_on_mixed_venues(mut order_factory: OrderFactory) {
2762 let binance = order_factory.market(
2763 InstrumentId::from("BTCUSDT.BINANCE"),
2764 OrderSide::Buy,
2765 100.into(),
2766 None,
2767 None,
2768 None,
2769 None,
2770 None,
2771 None,
2772 None,
2773 );
2774 let bybit = order_factory.market(
2775 InstrumentId::from("BTCUSDT.BYBIT"),
2776 OrderSide::Buy,
2777 100.into(),
2778 None,
2779 None,
2780 None,
2781 None,
2782 None,
2783 None,
2784 None,
2785 );
2786
2787 let mut orders = vec![binance, bybit];
2788 let _ = order_factory.create_list(&mut orders, UnixNanos::default());
2789 }
2790
2791 #[rstest]
2792 fn test_create_list_from_plain_orders(mut order_factory: OrderFactory) {
2793 let entry = order_factory.limit(
2794 InstrumentId::from("BTCUSDT.BINANCE"),
2795 OrderSide::Buy,
2796 100.into(),
2797 Price::from("50000.00"),
2798 None,
2799 None,
2800 None,
2801 None,
2802 None,
2803 None,
2804 None,
2805 None,
2806 None,
2807 None,
2808 None,
2809 None,
2810 );
2811 let sl = order_factory.stop_market(
2812 InstrumentId::from("BTCUSDT.BINANCE"),
2813 OrderSide::Sell,
2814 100.into(),
2815 Price::from("45000.00"),
2816 None,
2817 None,
2818 None,
2819 None,
2820 None,
2821 None,
2822 None,
2823 None,
2824 None,
2825 None,
2826 None,
2827 None,
2828 );
2829
2830 let mut orders = vec![entry.clone(), sl.clone()];
2831 let order_list = order_factory.create_list(&mut orders, UnixNanos::default());
2832
2833 assert_eq!(order_list.len(), 2);
2834 assert_eq!(
2835 order_list.instrument_id,
2836 InstrumentId::from("BTCUSDT.BINANCE")
2837 );
2838 assert_eq!(order_list.client_order_ids[0], entry.client_order_id());
2839 assert_eq!(order_list.client_order_ids[1], sl.client_order_id());
2840 assert_eq!(
2841 order_list.id,
2842 OrderListId::new("OL-19700101-000000-001-001-1"),
2843 );
2844 assert_eq!(orders[0].order_list_id(), Some(order_list.id));
2845 assert_eq!(orders[1].order_list_id(), Some(order_list.id));
2846 assert_eq!(orders[0].init_event().order_list_id, Some(order_list.id));
2847 assert_eq!(orders[1].init_event().order_list_id, Some(order_list.id));
2848 }
2849}