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