Skip to main content

nautilus_common/factories/
order.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Factory for constructing order objects.
17
18use 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    /// Creates a new [`OrderFactory`] instance.
54    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    /// Sets the client order ID generator count.
89    pub const fn set_client_order_id_count(&mut self, count: usize) {
90        self.order_id_generator.set_count(count);
91    }
92
93    /// Returns the client order ID generator count.
94    #[must_use]
95    pub const fn client_order_id_count(&self) -> usize {
96        self.order_id_generator.count()
97    }
98
99    /// Sets the order list ID generator count.
100    pub const fn set_order_list_id_count(&mut self, count: usize) {
101        self.order_list_id_generator.set_count(count);
102    }
103
104    /// Returns the order list ID generator count.
105    #[must_use]
106    pub const fn order_list_id_count(&self) -> usize {
107        self.order_list_id_generator.count()
108    }
109
110    /// Generates a new client order ID.
111    pub fn generate_client_order_id(&mut self) -> ClientOrderId {
112        self.order_id_generator.generate()
113    }
114
115    /// Generates a new order list ID.
116    pub fn generate_order_list_id(&mut self) -> OrderListId {
117        self.order_list_id_generator.generate()
118    }
119
120    /// Resets the factory by resetting all ID generators.
121    pub const fn reset_factory(&mut self) {
122        self.order_id_generator.reset();
123        self.order_list_id_generator.reset();
124    }
125
126    /// Creates a new market order.
127    ///
128    /// # Panics
129    ///
130    /// Panics if the order parameters fail validation.
131    #[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    /// Creates a new limit order.
205    ///
206    /// # Panics
207    ///
208    /// Panics if the order parameters fail validation.
209    #[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    /// Creates a new stop-market order.
307    ///
308    /// # Panics
309    ///
310    /// Panics if the order parameters fail validation.
311    #[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    /// Creates a new stop-limit order.
409    ///
410    /// # Panics
411    ///
412    /// Panics if the order parameters fail validation.
413    #[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    /// Creates a new market-to-limit order.
519    ///
520    /// # Panics
521    ///
522    /// Panics if the order parameters fail validation.
523    #[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, // post_only
588            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    /// Creates a new market-if-touched order.
606    ///
607    /// # Panics
608    ///
609    /// Panics if the order parameters fail validation.
610    #[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    /// Creates a new limit-if-touched order.
704    ///
705    /// # Panics
706    ///
707    /// Panics if the order parameters fail validation.
708    #[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    /// Creates a new trailing-stop-market order.
814    ///
815    /// # Panics
816    ///
817    /// If neither `trigger_price` nor `activation_price` is provided.
818    #[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        // Trailing stops need an initial trigger level: prefer explicit trigger_price,
896        // fall back to activation_price which serves as the initial trigger on OKX
897        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    /// Creates a new trailing-stop-limit order.
943    ///
944    /// # Panics
945    ///
946    /// If neither `trigger_price` nor `activation_price` is provided.
947    #[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        // Trailing stops need an initial trigger level: prefer explicit trigger_price,
1034        // fall back to activation_price which serves as the initial trigger on OKX
1035        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    /// Creates a new [`OrderList`] from the given orders, generating a fresh
1084    /// order list ID and propagating it back to each order.
1085    ///
1086    /// All orders must share the same venue; the caller is responsible for
1087    /// passing orders with the factory's `strategy_id`. The returned list's
1088    /// invariants are checked by [`OrderList::validate`] at submission time.
1089    ///
1090    /// # Panics
1091    ///
1092    /// Panics if `orders` is empty or if orders span more than one venue.
1093    /// Callers are expected to guard non-empty input; `Strategy::submit_order_list`
1094    /// filters out the empty case and bails on mixed venues before reaching
1095    /// this constructor.
1096    #[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    /// Creates a bracket order with an entry order and attached take-profit and stop-loss legs.
1131    ///
1132    /// Defaults:
1133    /// - `contingency_type`: `Ouo` for the TP/SL legs.
1134    /// - `entry_order_type`: `Market`; `tp_order_type`: `Limit`; `sl_order_type`: `StopMarket`.
1135    /// - `entry_tags`: `["ENTRY"]`; `tp_tags`: `["TAKE_PROFIT"]`; `sl_tags`: `["STOP_LOSS"]`.
1136    /// - `tp_post_only`: `true` for `Limit` and `LimitIfTouched`; `entry_post_only`: `false`.
1137    /// - TP and SL legs are always `reduce_only = true`; the entry is `reduce_only = false`.
1138    /// - TP and SL legs do not inherit `expire_time` from the entry.
1139    ///
1140    /// # Panics
1141    ///
1142    /// Panics if `entry_order_type`, `tp_order_type`, or `sl_order_type` is not one of the
1143    /// supported variants, or if a required price/trigger field is missing for the chosen type.
1144    #[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        // Entry order
1155        #[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        // Take-profit order
1166        #[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        // Stop-loss order
1181        #[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        // Entry order
1252        #[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        // Take-profit order
1263        #[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        // Stop-loss order
1278        #[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, // reduce_only
1327                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, // reduce_only
1349                quote_quantity,
1350                None, // display_qty
1351                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, // reduce_only
1380                    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, // reduce_only
1416                    quote_quantity,
1417                    None, // display_qty
1418                    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, // reduce_only
1452                quote_quantity,
1453                None, // display_qty
1454                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, // expire_time
1492                tp_post_only,
1493                true, // reduce_only
1494                quote_quantity,
1495                None, // display_qty
1496                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, // expire_time
1528                    tp_post_only,
1529                    true, // reduce_only
1530                    quote_quantity,
1531                    None, // display_qty
1532                    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, // expire_time
1561                    true, // reduce_only
1562                    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, // expire_time
1599                    true, // reduce_only
1600                    quote_quantity,
1601                    None, // display_qty
1602                    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,  // expire_time
1650                    false, // post_only (TRAILING_STOP_LIMIT TP must not be post-only)
1651                    true,  // reduce_only
1652                    quote_quantity,
1653                    None, // display_qty
1654                    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, // expire_time
1693                true, // reduce_only
1694                quote_quantity,
1695                None, // display_qty
1696                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, // expire_time
1731                    true, // reduce_only
1732                    quote_quantity,
1733                    None, // display_qty
1734                    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, // use_uuids_for_client_order_ids
1795            true,  // use_hyphens_in_client_order_ids
1796        )
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, // use_uuids_for_client_order_ids
1866            true, // use_hyphens_in_client_order_ids
1867        )
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, // use_uuids_for_client_order_ids
1882            false, // use_hyphens_in_client_order_ids
1883        )
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,  // use_uuids_for_client_order_ids
1898            false, // use_hyphens_in_client_order_ids
1899        )
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        // UUID should be 36 characters with hyphens
1907        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        // UUID without hyphens should be 32 characters
1932        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        // TODO: Add additional polymorphic getters
1951        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.time_in_force(), TimeInForce::Gtc);
1955        // assert!(!market_order.is_reduce_only);
1956        // assert!(!market_order.is_quote_quantity);
1957        assert_eq!(market_order.exec_algorithm_id(), None);
1958        // assert_eq!(market_order.exec_algorithm_params(), None);
1959        // assert_eq!(market_order.exec_spawn_id, None);
1960        // assert_eq!(market_order.tags, None);
1961        assert_eq!(
1962            market_order.client_order_id(),
1963            ClientOrderId::new("O-19700101-000000-001-001-1")
1964        );
1965        // assert_eq!(market_order.order_list_id(), None);
1966    }
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), // post_only
2009            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),     // post_only
2033            Some(false),     // reduce_only
2034            Some(false),     // quote_quantity
2035            Some(50.into()), // display_qty
2036            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"), // limit price
2082            Price::from("45000.00"), // trigger price
2083            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"), // limit price
2146            Price::from("48000.00"), // trigger price
2147            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"), // limit price
2204            Decimal::new(10, 2),     // limit_offset
2205            Decimal::new(50, 2),     // trailing_offset
2206            Some(TrailingOffsetType::Price),
2207            Some(Price::from("45000.00")), // activation_price
2208            Some(Price::from("45000.00")), // trigger_price
2209            Some(TriggerType::LastPrice),
2210            Some(TimeInForce::Gtc),
2211            None,
2212            Some(false), // post_only
2213            Some(true),  // reduce_only
2214            Some(false), // quote_quantity
2215            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        // Entry should be market order
2250        assert_eq!(orders[0].order_side(), OrderSide::Buy);
2251
2252        // SL should be opposite side stop-market
2253        assert_eq!(orders[1].order_side(), OrderSide::Sell);
2254        assert_eq!(orders[1].trigger_price(), Some(Price::from("45000.00")));
2255
2256        // TP should be opposite side limit
2257        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        // Entry: limit
2450        assert_eq!(orders[0].order_type(), OrderType::Limit);
2451        // SL: trailing stop market with non-default offset type
2452        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        // TP: market-if-touched with non-default trigger type
2461        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) // explicitly true; constructor must override to false
2516            .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        // BUY entry => SELL TP; SELL LimitIfTouched requires trigger_price >= price.
2586        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}