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