nautilus_common/
factories.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//! Factories for constructing domain objects such as orders.
17
18use indexmap::IndexMap;
19use nautilus_core::{AtomicTime, UUID4};
20use nautilus_model::{
21    enums::{ContingencyType, OrderSide, TimeInForce, TriggerType},
22    identifiers::{
23        ClientOrderId, ExecAlgorithmId, InstrumentId, OrderListId, StrategyId, TraderId,
24    },
25    orders::{
26        LimitIfTouchedOrder, LimitOrder, MarketIfTouchedOrder, MarketOrder, OrderAny, OrderList,
27        StopLimitOrder, StopMarketOrder,
28    },
29    types::{Price, Quantity},
30};
31use ustr::Ustr;
32
33use crate::generators::{
34    client_order_id::ClientOrderIdGenerator, order_list_id::OrderListIdGenerator,
35};
36
37#[repr(C)]
38#[derive(Debug)]
39pub struct OrderFactory {
40    clock: &'static AtomicTime,
41    trader_id: TraderId,
42    strategy_id: StrategyId,
43    order_id_generator: ClientOrderIdGenerator,
44    order_list_id_generator: OrderListIdGenerator,
45}
46
47impl OrderFactory {
48    /// Creates a new [`OrderFactory`] instance.
49    pub fn new(
50        trader_id: TraderId,
51        strategy_id: StrategyId,
52        init_order_id_count: Option<usize>,
53        init_order_list_id_count: Option<usize>,
54        clock: &'static AtomicTime,
55        use_uuids_for_client_order_ids: bool,
56        use_hyphens_in_client_order_ids: bool,
57    ) -> Self {
58        let order_id_generator = ClientOrderIdGenerator::new(
59            trader_id,
60            strategy_id,
61            init_order_id_count.unwrap_or(0),
62            clock,
63            use_uuids_for_client_order_ids,
64            use_hyphens_in_client_order_ids,
65        );
66
67        let order_list_id_generator = OrderListIdGenerator::new(
68            trader_id,
69            strategy_id,
70            init_order_list_id_count.unwrap_or(0),
71            clock,
72        );
73
74        Self {
75            clock,
76            trader_id,
77            strategy_id,
78            order_id_generator,
79            order_list_id_generator,
80        }
81    }
82
83    /// Sets the client order ID generator count.
84    pub const fn set_client_order_id_count(&mut self, count: usize) {
85        self.order_id_generator.set_count(count);
86    }
87
88    /// Sets the order list ID generator count.
89    pub const fn set_order_list_id_count(&mut self, count: usize) {
90        self.order_list_id_generator.set_count(count);
91    }
92
93    /// Generates a new client order ID.
94    pub fn generate_client_order_id(&mut self) -> ClientOrderId {
95        self.order_id_generator.generate()
96    }
97
98    /// Generates a new order list ID.
99    pub fn generate_order_list_id(&mut self) -> OrderListId {
100        self.order_list_id_generator.generate()
101    }
102
103    /// Resets the factory by resetting all ID generators.
104    pub const fn reset_factory(&mut self) {
105        self.order_id_generator.reset();
106        self.order_list_id_generator.reset();
107    }
108
109    /// Creates a new market order.
110    #[allow(clippy::too_many_arguments)]
111    pub fn market(
112        &mut self,
113        instrument_id: InstrumentId,
114        order_side: OrderSide,
115        quantity: Quantity,
116        time_in_force: Option<TimeInForce>,
117        reduce_only: Option<bool>,
118        quote_quantity: Option<bool>,
119        exec_algorithm_id: Option<ExecAlgorithmId>,
120        exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
121        tags: Option<Vec<Ustr>>,
122        client_order_id: Option<ClientOrderId>,
123    ) -> OrderAny {
124        let client_order_id = client_order_id.unwrap_or_else(|| self.generate_client_order_id());
125        let exec_spawn_id: Option<ClientOrderId> = if exec_algorithm_id.is_none() {
126            None
127        } else {
128            Some(client_order_id)
129        };
130        let order = MarketOrder::new(
131            self.trader_id,
132            self.strategy_id,
133            instrument_id,
134            client_order_id,
135            order_side,
136            quantity,
137            time_in_force.unwrap_or(TimeInForce::Gtc),
138            UUID4::new(),
139            self.clock.get_time_ns(),
140            reduce_only.unwrap_or(false),
141            quote_quantity.unwrap_or(false),
142            Some(ContingencyType::NoContingency),
143            None,
144            None,
145            None,
146            exec_algorithm_id,
147            exec_algorithm_params,
148            exec_spawn_id,
149            tags,
150        );
151        OrderAny::Market(order)
152    }
153
154    /// Creates a new limit order.
155    #[allow(clippy::too_many_arguments)]
156    pub fn limit(
157        &mut self,
158        instrument_id: InstrumentId,
159        order_side: OrderSide,
160        quantity: Quantity,
161        price: Price,
162        time_in_force: Option<TimeInForce>,
163        expire_time: Option<nautilus_core::UnixNanos>,
164        post_only: Option<bool>,
165        reduce_only: Option<bool>,
166        quote_quantity: Option<bool>,
167        display_qty: Option<Quantity>,
168        emulation_trigger: Option<TriggerType>,
169        trigger_instrument_id: Option<InstrumentId>,
170        exec_algorithm_id: Option<ExecAlgorithmId>,
171        exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
172        tags: Option<Vec<Ustr>>,
173        client_order_id: Option<ClientOrderId>,
174    ) -> OrderAny {
175        let client_order_id = client_order_id.unwrap_or_else(|| self.generate_client_order_id());
176        let exec_spawn_id: Option<ClientOrderId> = if exec_algorithm_id.is_none() {
177            None
178        } else {
179            Some(client_order_id)
180        };
181        let order = LimitOrder::new(
182            self.trader_id,
183            self.strategy_id,
184            instrument_id,
185            client_order_id,
186            order_side,
187            quantity,
188            price,
189            time_in_force.unwrap_or(TimeInForce::Gtc),
190            expire_time,
191            post_only.unwrap_or(false),
192            reduce_only.unwrap_or(false),
193            quote_quantity.unwrap_or(false),
194            display_qty,
195            emulation_trigger,
196            trigger_instrument_id,
197            Some(ContingencyType::NoContingency),
198            None,
199            None,
200            None,
201            exec_algorithm_id,
202            exec_algorithm_params,
203            exec_spawn_id,
204            tags,
205            UUID4::new(),
206            self.clock.get_time_ns(),
207        );
208        OrderAny::Limit(order)
209    }
210
211    /// Creates a new stop-market order.
212    #[allow(clippy::too_many_arguments)]
213    pub fn stop_market(
214        &mut self,
215        instrument_id: InstrumentId,
216        order_side: OrderSide,
217        quantity: Quantity,
218        trigger_price: Price,
219        trigger_type: Option<TriggerType>,
220        time_in_force: Option<TimeInForce>,
221        expire_time: Option<nautilus_core::UnixNanos>,
222        reduce_only: Option<bool>,
223        quote_quantity: Option<bool>,
224        display_qty: Option<Quantity>,
225        emulation_trigger: Option<TriggerType>,
226        trigger_instrument_id: Option<InstrumentId>,
227        exec_algorithm_id: Option<ExecAlgorithmId>,
228        exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
229        tags: Option<Vec<Ustr>>,
230        client_order_id: Option<ClientOrderId>,
231    ) -> OrderAny {
232        let client_order_id = client_order_id.unwrap_or_else(|| self.generate_client_order_id());
233        let exec_spawn_id: Option<ClientOrderId> = if exec_algorithm_id.is_none() {
234            None
235        } else {
236            Some(client_order_id)
237        };
238        let order = StopMarketOrder::new(
239            self.trader_id,
240            self.strategy_id,
241            instrument_id,
242            client_order_id,
243            order_side,
244            quantity,
245            trigger_price,
246            trigger_type.unwrap_or(TriggerType::Default),
247            time_in_force.unwrap_or(TimeInForce::Gtc),
248            expire_time,
249            reduce_only.unwrap_or(false),
250            quote_quantity.unwrap_or(false),
251            display_qty,
252            emulation_trigger,
253            trigger_instrument_id,
254            Some(ContingencyType::NoContingency),
255            None,
256            None,
257            None,
258            exec_algorithm_id,
259            exec_algorithm_params,
260            exec_spawn_id,
261            tags,
262            UUID4::new(),
263            self.clock.get_time_ns(),
264        );
265        OrderAny::StopMarket(order)
266    }
267
268    /// Creates a new stop-limit order.
269    #[allow(clippy::too_many_arguments)]
270    pub fn stop_limit(
271        &mut self,
272        instrument_id: InstrumentId,
273        order_side: OrderSide,
274        quantity: Quantity,
275        price: Price,
276        trigger_price: Price,
277        trigger_type: Option<TriggerType>,
278        time_in_force: Option<TimeInForce>,
279        expire_time: Option<nautilus_core::UnixNanos>,
280        post_only: Option<bool>,
281        reduce_only: Option<bool>,
282        quote_quantity: Option<bool>,
283        display_qty: Option<Quantity>,
284        emulation_trigger: Option<TriggerType>,
285        trigger_instrument_id: Option<InstrumentId>,
286        exec_algorithm_id: Option<ExecAlgorithmId>,
287        exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
288        tags: Option<Vec<Ustr>>,
289        client_order_id: Option<ClientOrderId>,
290    ) -> OrderAny {
291        let client_order_id = client_order_id.unwrap_or_else(|| self.generate_client_order_id());
292        let exec_spawn_id: Option<ClientOrderId> = if exec_algorithm_id.is_none() {
293            None
294        } else {
295            Some(client_order_id)
296        };
297        let order = StopLimitOrder::new(
298            self.trader_id,
299            self.strategy_id,
300            instrument_id,
301            client_order_id,
302            order_side,
303            quantity,
304            price,
305            trigger_price,
306            trigger_type.unwrap_or(TriggerType::Default),
307            time_in_force.unwrap_or(TimeInForce::Gtc),
308            expire_time,
309            post_only.unwrap_or(false),
310            reduce_only.unwrap_or(false),
311            quote_quantity.unwrap_or(false),
312            display_qty,
313            emulation_trigger,
314            trigger_instrument_id,
315            Some(ContingencyType::NoContingency),
316            None,
317            None,
318            None,
319            exec_algorithm_id,
320            exec_algorithm_params,
321            exec_spawn_id,
322            tags,
323            UUID4::new(),
324            self.clock.get_time_ns(),
325        );
326        OrderAny::StopLimit(order)
327    }
328
329    /// Creates a new market-if-touched order.
330    #[allow(clippy::too_many_arguments)]
331    pub fn market_if_touched(
332        &mut self,
333        instrument_id: InstrumentId,
334        order_side: OrderSide,
335        quantity: Quantity,
336        trigger_price: Price,
337        trigger_type: Option<TriggerType>,
338        time_in_force: Option<TimeInForce>,
339        expire_time: Option<nautilus_core::UnixNanos>,
340        reduce_only: Option<bool>,
341        quote_quantity: Option<bool>,
342        emulation_trigger: Option<TriggerType>,
343        trigger_instrument_id: Option<InstrumentId>,
344        exec_algorithm_id: Option<ExecAlgorithmId>,
345        exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
346        tags: Option<Vec<Ustr>>,
347        client_order_id: Option<ClientOrderId>,
348    ) -> OrderAny {
349        let client_order_id = client_order_id.unwrap_or_else(|| self.generate_client_order_id());
350        let exec_spawn_id: Option<ClientOrderId> = if exec_algorithm_id.is_none() {
351            None
352        } else {
353            Some(client_order_id)
354        };
355        let order = MarketIfTouchedOrder::new(
356            self.trader_id,
357            self.strategy_id,
358            instrument_id,
359            client_order_id,
360            order_side,
361            quantity,
362            trigger_price,
363            trigger_type.unwrap_or(TriggerType::Default),
364            time_in_force.unwrap_or(TimeInForce::Gtc),
365            expire_time,
366            reduce_only.unwrap_or(false),
367            quote_quantity.unwrap_or(false),
368            emulation_trigger,
369            trigger_instrument_id,
370            Some(ContingencyType::NoContingency),
371            None,
372            None,
373            None,
374            exec_algorithm_id,
375            exec_algorithm_params,
376            exec_spawn_id,
377            tags,
378            UUID4::new(),
379            self.clock.get_time_ns(),
380        );
381        OrderAny::MarketIfTouched(order)
382    }
383
384    /// Creates a new limit-if-touched order.
385    #[allow(clippy::too_many_arguments)]
386    pub fn limit_if_touched(
387        &mut self,
388        instrument_id: InstrumentId,
389        order_side: OrderSide,
390        quantity: Quantity,
391        price: Price,
392        trigger_price: Price,
393        trigger_type: Option<TriggerType>,
394        time_in_force: Option<TimeInForce>,
395        expire_time: Option<nautilus_core::UnixNanos>,
396        post_only: Option<bool>,
397        reduce_only: Option<bool>,
398        quote_quantity: Option<bool>,
399        display_qty: Option<Quantity>,
400        emulation_trigger: Option<TriggerType>,
401        trigger_instrument_id: Option<InstrumentId>,
402        exec_algorithm_id: Option<ExecAlgorithmId>,
403        exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
404        tags: Option<Vec<Ustr>>,
405        client_order_id: Option<ClientOrderId>,
406    ) -> OrderAny {
407        let client_order_id = client_order_id.unwrap_or_else(|| self.generate_client_order_id());
408        let exec_spawn_id: Option<ClientOrderId> = if exec_algorithm_id.is_none() {
409            None
410        } else {
411            Some(client_order_id)
412        };
413        let order = LimitIfTouchedOrder::new(
414            self.trader_id,
415            self.strategy_id,
416            instrument_id,
417            client_order_id,
418            order_side,
419            quantity,
420            price,
421            trigger_price,
422            trigger_type.unwrap_or(TriggerType::Default),
423            time_in_force.unwrap_or(TimeInForce::Gtc),
424            expire_time,
425            post_only.unwrap_or(false),
426            reduce_only.unwrap_or(false),
427            quote_quantity.unwrap_or(false),
428            display_qty,
429            emulation_trigger,
430            trigger_instrument_id,
431            Some(ContingencyType::NoContingency),
432            None,
433            None,
434            None,
435            exec_algorithm_id,
436            exec_algorithm_params,
437            exec_spawn_id,
438            tags,
439            UUID4::new(),
440            self.clock.get_time_ns(),
441        );
442        OrderAny::LimitIfTouched(order)
443    }
444
445    /// Creates a bracket order list with entry order and attached stop-loss and take-profit orders.
446    #[allow(clippy::too_many_arguments)]
447    pub fn bracket(
448        &mut self,
449        instrument_id: InstrumentId,
450        order_side: OrderSide,
451        quantity: Quantity,
452        entry_price: Option<Price>,
453        sl_trigger_price: Price,
454        sl_trigger_type: Option<TriggerType>,
455        tp_price: Price,
456        entry_trigger_price: Option<Price>,
457        time_in_force: Option<TimeInForce>,
458        expire_time: Option<nautilus_core::UnixNanos>,
459        post_only: Option<bool>,
460        reduce_only: Option<bool>,
461        quote_quantity: Option<bool>,
462        emulation_trigger: Option<TriggerType>,
463        trigger_instrument_id: Option<InstrumentId>,
464        exec_algorithm_id: Option<ExecAlgorithmId>,
465        exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
466        tags: Option<Vec<Ustr>>,
467    ) -> OrderList {
468        let order_list_id = self.generate_order_list_id();
469        let ts_init = self.clock.get_time_ns();
470
471        let entry_client_order_id = self.generate_client_order_id();
472        let sl_client_order_id = self.generate_client_order_id();
473        let tp_client_order_id = self.generate_client_order_id();
474
475        // Exec spawn IDs for algorithm orders
476        let entry_exec_spawn_id = exec_algorithm_id.as_ref().map(|_| entry_client_order_id);
477        let sl_exec_spawn_id = exec_algorithm_id.as_ref().map(|_| sl_client_order_id);
478        let tp_exec_spawn_id = exec_algorithm_id.as_ref().map(|_| tp_client_order_id);
479
480        // Entry order linkage
481        let entry_contingency_type = Some(ContingencyType::Oto);
482        let entry_order_list_id = Some(order_list_id);
483        let entry_linked_order_ids = Some(vec![sl_client_order_id, tp_client_order_id]);
484        let entry_parent_order_id = None;
485
486        let entry_order = if let Some(trigger_price) = entry_trigger_price {
487            if let Some(price) = entry_price {
488                OrderAny::StopLimit(StopLimitOrder::new(
489                    self.trader_id,
490                    self.strategy_id,
491                    instrument_id,
492                    entry_client_order_id,
493                    order_side,
494                    quantity,
495                    price,
496                    trigger_price,
497                    TriggerType::Default,
498                    time_in_force.unwrap_or(TimeInForce::Gtc),
499                    expire_time,
500                    post_only.unwrap_or(false),
501                    reduce_only.unwrap_or(false),
502                    quote_quantity.unwrap_or(false),
503                    None, // display_qty
504                    emulation_trigger,
505                    trigger_instrument_id,
506                    entry_contingency_type,
507                    entry_order_list_id,
508                    entry_linked_order_ids,
509                    entry_parent_order_id,
510                    exec_algorithm_id,
511                    exec_algorithm_params.clone(),
512                    entry_exec_spawn_id,
513                    tags.clone(),
514                    UUID4::new(),
515                    ts_init,
516                ))
517            } else {
518                OrderAny::StopMarket(StopMarketOrder::new(
519                    self.trader_id,
520                    self.strategy_id,
521                    instrument_id,
522                    entry_client_order_id,
523                    order_side,
524                    quantity,
525                    trigger_price,
526                    TriggerType::Default,
527                    time_in_force.unwrap_or(TimeInForce::Gtc),
528                    expire_time,
529                    reduce_only.unwrap_or(false),
530                    quote_quantity.unwrap_or(false),
531                    None, // display_qty
532                    emulation_trigger,
533                    trigger_instrument_id,
534                    entry_contingency_type,
535                    entry_order_list_id,
536                    entry_linked_order_ids,
537                    entry_parent_order_id,
538                    exec_algorithm_id,
539                    exec_algorithm_params.clone(),
540                    entry_exec_spawn_id,
541                    tags.clone(),
542                    UUID4::new(),
543                    ts_init,
544                ))
545            }
546        } else if let Some(price) = entry_price {
547            OrderAny::Limit(LimitOrder::new(
548                self.trader_id,
549                self.strategy_id,
550                instrument_id,
551                entry_client_order_id,
552                order_side,
553                quantity,
554                price,
555                time_in_force.unwrap_or(TimeInForce::Gtc),
556                expire_time,
557                post_only.unwrap_or(false),
558                reduce_only.unwrap_or(false),
559                quote_quantity.unwrap_or(false),
560                None, // display_qty
561                emulation_trigger,
562                trigger_instrument_id,
563                entry_contingency_type,
564                entry_order_list_id,
565                entry_linked_order_ids,
566                entry_parent_order_id,
567                exec_algorithm_id,
568                exec_algorithm_params.clone(),
569                entry_exec_spawn_id,
570                tags.clone(),
571                UUID4::new(),
572                ts_init,
573            ))
574        } else {
575            OrderAny::Market(MarketOrder::new(
576                self.trader_id,
577                self.strategy_id,
578                instrument_id,
579                entry_client_order_id,
580                order_side,
581                quantity,
582                time_in_force.unwrap_or(TimeInForce::Gtc),
583                UUID4::new(),
584                ts_init,
585                reduce_only.unwrap_or(false),
586                quote_quantity.unwrap_or(false),
587                entry_contingency_type,
588                entry_order_list_id,
589                entry_linked_order_ids,
590                entry_parent_order_id,
591                exec_algorithm_id,
592                exec_algorithm_params.clone(),
593                entry_exec_spawn_id,
594                tags.clone(),
595            ))
596        };
597
598        let sl_tp_side = match order_side {
599            OrderSide::Buy => OrderSide::Sell,
600            OrderSide::Sell => OrderSide::Buy,
601            OrderSide::NoOrderSide => OrderSide::NoOrderSide,
602        };
603
604        // SL order linkage
605        let sl_contingency_type = Some(ContingencyType::Oco);
606        let sl_order_list_id = Some(order_list_id);
607        let sl_linked_order_ids = Some(vec![tp_client_order_id]);
608        let sl_parent_order_id = Some(entry_client_order_id);
609
610        let sl_order = OrderAny::StopMarket(StopMarketOrder::new(
611            self.trader_id,
612            self.strategy_id,
613            instrument_id,
614            sl_client_order_id,
615            sl_tp_side,
616            quantity,
617            sl_trigger_price,
618            sl_trigger_type.unwrap_or(TriggerType::Default),
619            time_in_force.unwrap_or(TimeInForce::Gtc),
620            expire_time,
621            true, // SL/TP should only reduce positions
622            quote_quantity.unwrap_or(false),
623            None, // display_qty
624            emulation_trigger,
625            trigger_instrument_id,
626            sl_contingency_type,
627            sl_order_list_id,
628            sl_linked_order_ids,
629            sl_parent_order_id,
630            exec_algorithm_id,
631            exec_algorithm_params.clone(),
632            sl_exec_spawn_id,
633            tags.clone(),
634            UUID4::new(),
635            ts_init,
636        ));
637
638        // TP order linkage
639        let tp_contingency_type = Some(ContingencyType::Oco);
640        let tp_order_list_id = Some(order_list_id);
641        let tp_linked_order_ids = Some(vec![sl_client_order_id]);
642        let tp_parent_order_id = Some(entry_client_order_id);
643
644        let tp_order = OrderAny::Limit(LimitOrder::new(
645            self.trader_id,
646            self.strategy_id,
647            instrument_id,
648            tp_client_order_id,
649            sl_tp_side,
650            quantity,
651            tp_price,
652            time_in_force.unwrap_or(TimeInForce::Gtc),
653            expire_time,
654            post_only.unwrap_or(false),
655            true, // SL/TP should only reduce positions
656            quote_quantity.unwrap_or(false),
657            None, // display_qty
658            emulation_trigger,
659            trigger_instrument_id,
660            tp_contingency_type,
661            tp_order_list_id,
662            tp_linked_order_ids,
663            tp_parent_order_id,
664            exec_algorithm_id,
665            exec_algorithm_params,
666            tp_exec_spawn_id,
667            tags,
668            UUID4::new(),
669            ts_init,
670        ));
671
672        OrderList::new(
673            order_list_id,
674            instrument_id,
675            self.strategy_id,
676            vec![entry_order, sl_order, tp_order],
677            ts_init,
678        )
679    }
680}
681
682#[cfg(test)]
683pub mod tests {
684    use nautilus_core::time::get_atomic_clock_static;
685    use nautilus_model::{
686        enums::{ContingencyType, OrderSide, TimeInForce, TriggerType},
687        identifiers::{
688            ClientOrderId, InstrumentId, OrderListId,
689            stubs::{strategy_id_ema_cross, trader_id},
690        },
691        orders::Order,
692        types::Price,
693    };
694    use rstest::{fixture, rstest};
695
696    use crate::factories::OrderFactory;
697
698    #[fixture]
699    pub fn order_factory() -> OrderFactory {
700        let trader_id = trader_id();
701        let strategy_id = strategy_id_ema_cross();
702        OrderFactory::new(
703            trader_id,
704            strategy_id,
705            None,
706            None,
707            get_atomic_clock_static(),
708            false, // use_uuids_for_client_order_ids
709            true,  // use_hyphens_in_client_order_ids
710        )
711    }
712
713    #[rstest]
714    fn test_generate_client_order_id(mut order_factory: OrderFactory) {
715        let client_order_id = order_factory.generate_client_order_id();
716        assert_eq!(
717            client_order_id,
718            ClientOrderId::new("O-19700101-000000-001-001-1")
719        );
720    }
721
722    #[rstest]
723    fn test_generate_order_list_id(mut order_factory: OrderFactory) {
724        let order_list_id = order_factory.generate_order_list_id();
725        assert_eq!(
726            order_list_id,
727            OrderListId::new("OL-19700101-000000-001-001-1")
728        );
729    }
730
731    #[rstest]
732    fn test_set_client_order_id_count(mut order_factory: OrderFactory) {
733        order_factory.set_client_order_id_count(10);
734        let client_order_id = order_factory.generate_client_order_id();
735        assert_eq!(
736            client_order_id,
737            ClientOrderId::new("O-19700101-000000-001-001-11")
738        );
739    }
740
741    #[rstest]
742    fn test_set_order_list_id_count(mut order_factory: OrderFactory) {
743        order_factory.set_order_list_id_count(10);
744        let order_list_id = order_factory.generate_order_list_id();
745        assert_eq!(
746            order_list_id,
747            OrderListId::new("OL-19700101-000000-001-001-11")
748        );
749    }
750
751    #[rstest]
752    fn test_reset_factory(mut order_factory: OrderFactory) {
753        order_factory.generate_order_list_id();
754        order_factory.generate_client_order_id();
755        order_factory.reset_factory();
756        let client_order_id = order_factory.generate_client_order_id();
757        let order_list_id = order_factory.generate_order_list_id();
758        assert_eq!(
759            client_order_id,
760            ClientOrderId::new("O-19700101-000000-001-001-1")
761        );
762        assert_eq!(
763            order_list_id,
764            OrderListId::new("OL-19700101-000000-001-001-1")
765        );
766    }
767
768    #[fixture]
769    pub fn order_factory_with_uuids() -> OrderFactory {
770        let trader_id = trader_id();
771        let strategy_id = strategy_id_ema_cross();
772        OrderFactory::new(
773            trader_id,
774            strategy_id,
775            None,
776            None,
777            get_atomic_clock_static(),
778            true, // use_uuids_for_client_order_ids
779            true, // use_hyphens_in_client_order_ids
780        )
781    }
782
783    #[fixture]
784    pub fn order_factory_with_hyphens_removed() -> OrderFactory {
785        let trader_id = trader_id();
786        let strategy_id = strategy_id_ema_cross();
787        OrderFactory::new(
788            trader_id,
789            strategy_id,
790            None,
791            None,
792            get_atomic_clock_static(),
793            false, // use_uuids_for_client_order_ids
794            false, // use_hyphens_in_client_order_ids
795        )
796    }
797
798    #[fixture]
799    pub fn order_factory_with_uuids_and_hyphens_removed() -> OrderFactory {
800        let trader_id = trader_id();
801        let strategy_id = strategy_id_ema_cross();
802        OrderFactory::new(
803            trader_id,
804            strategy_id,
805            None,
806            None,
807            get_atomic_clock_static(),
808            true,  // use_uuids_for_client_order_ids
809            false, // use_hyphens_in_client_order_ids
810        )
811    }
812
813    #[rstest]
814    fn test_generate_client_order_id_with_uuids(mut order_factory_with_uuids: OrderFactory) {
815        let client_order_id = order_factory_with_uuids.generate_client_order_id();
816
817        // UUID should be 36 characters with hyphens
818        assert_eq!(client_order_id.as_str().len(), 36);
819        assert!(client_order_id.as_str().contains('-'));
820    }
821
822    #[rstest]
823    fn test_generate_client_order_id_with_hyphens_removed(
824        mut order_factory_with_hyphens_removed: OrderFactory,
825    ) {
826        let client_order_id = order_factory_with_hyphens_removed.generate_client_order_id();
827
828        assert_eq!(
829            client_order_id,
830            ClientOrderId::new("O197001010000000010011")
831        );
832        assert!(!client_order_id.as_str().contains('-'));
833    }
834
835    #[rstest]
836    fn test_generate_client_order_id_with_uuids_and_hyphens_removed(
837        mut order_factory_with_uuids_and_hyphens_removed: OrderFactory,
838    ) {
839        let client_order_id =
840            order_factory_with_uuids_and_hyphens_removed.generate_client_order_id();
841
842        // UUID without hyphens should be 32 characters
843        assert_eq!(client_order_id.as_str().len(), 32);
844        assert!(!client_order_id.as_str().contains('-'));
845    }
846
847    #[rstest]
848    fn test_market_order(mut order_factory: OrderFactory) {
849        let market_order = order_factory.market(
850            InstrumentId::from("BTCUSDT.BINANCE"),
851            OrderSide::Buy,
852            100.into(),
853            Some(TimeInForce::Gtc),
854            Some(false),
855            Some(false),
856            None,
857            None,
858            None,
859            None,
860        );
861        // TODO: Add additional polymorphic getters
862        assert_eq!(market_order.instrument_id(), "BTCUSDT.BINANCE".into());
863        assert_eq!(market_order.order_side(), OrderSide::Buy);
864        assert_eq!(market_order.quantity(), 100.into());
865        // assert_eq!(market_order.time_in_force(), TimeInForce::Gtc);
866        // assert!(!market_order.is_reduce_only);
867        // assert!(!market_order.is_quote_quantity);
868        assert_eq!(market_order.exec_algorithm_id(), None);
869        // assert_eq!(market_order.exec_algorithm_params(), None);
870        // assert_eq!(market_order.exec_spawn_id, None);
871        // assert_eq!(market_order.tags, None);
872        assert_eq!(
873            market_order.client_order_id(),
874            ClientOrderId::new("O-19700101-000000-001-001-1")
875        );
876        // assert_eq!(market_order.order_list_id(), None);
877    }
878
879    #[rstest]
880    fn test_limit_order(mut order_factory: OrderFactory) {
881        let limit_order = order_factory.limit(
882            InstrumentId::from("BTCUSDT.BINANCE"),
883            OrderSide::Buy,
884            100.into(),
885            Price::from("50000.00"),
886            Some(TimeInForce::Gtc),
887            None,
888            Some(false),
889            Some(false),
890            Some(false),
891            None,
892            None,
893            None,
894            None,
895            None,
896            None,
897            None,
898        );
899
900        assert_eq!(limit_order.instrument_id(), "BTCUSDT.BINANCE".into());
901        assert_eq!(limit_order.order_side(), OrderSide::Buy);
902        assert_eq!(limit_order.quantity(), 100.into());
903        assert_eq!(limit_order.price(), Some(Price::from("50000.00")));
904        assert_eq!(
905            limit_order.client_order_id(),
906            ClientOrderId::new("O-19700101-000000-001-001-1")
907        );
908    }
909
910    #[rstest]
911    fn test_limit_order_with_post_only(mut order_factory: OrderFactory) {
912        let limit_order = order_factory.limit(
913            InstrumentId::from("BTCUSDT.BINANCE"),
914            OrderSide::Buy,
915            100.into(),
916            Price::from("50000.00"),
917            Some(TimeInForce::Gtc),
918            None,
919            Some(true), // post_only
920            Some(false),
921            Some(false),
922            None,
923            None,
924            None,
925            None,
926            None,
927            None,
928            None,
929        );
930
931        assert!(limit_order.is_post_only());
932    }
933
934    #[rstest]
935    fn test_limit_order_with_display_qty(mut order_factory: OrderFactory) {
936        let limit_order = order_factory.limit(
937            InstrumentId::from("BTCUSDT.BINANCE"),
938            OrderSide::Buy,
939            100.into(),
940            Price::from("50000.00"),
941            Some(TimeInForce::Gtc),
942            None,
943            Some(false),     // post_only
944            Some(false),     // reduce_only
945            Some(false),     // quote_quantity
946            Some(50.into()), // display_qty
947            None,
948            None,
949            None,
950            None,
951            None,
952            None,
953        );
954
955        assert_eq!(limit_order.display_qty(), Some(50.into()));
956    }
957
958    #[rstest]
959    fn test_stop_market_order(mut order_factory: OrderFactory) {
960        let stop_order = order_factory.stop_market(
961            InstrumentId::from("BTCUSDT.BINANCE"),
962            OrderSide::Sell,
963            100.into(),
964            Price::from("45000.00"),
965            Some(TriggerType::LastPrice),
966            Some(TimeInForce::Gtc),
967            None,
968            Some(false),
969            Some(false),
970            None,
971            None,
972            None,
973            None,
974            None,
975            None,
976            None,
977        );
978
979        assert_eq!(stop_order.instrument_id(), "BTCUSDT.BINANCE".into());
980        assert_eq!(stop_order.order_side(), OrderSide::Sell);
981        assert_eq!(stop_order.quantity(), 100.into());
982        assert_eq!(stop_order.trigger_price(), Some(Price::from("45000.00")));
983        assert_eq!(stop_order.trigger_type(), Some(TriggerType::LastPrice));
984    }
985
986    #[rstest]
987    fn test_stop_limit_order(mut order_factory: OrderFactory) {
988        let stop_limit_order = order_factory.stop_limit(
989            InstrumentId::from("BTCUSDT.BINANCE"),
990            OrderSide::Sell,
991            100.into(),
992            Price::from("45100.00"), // limit price
993            Price::from("45000.00"), // trigger price
994            Some(TriggerType::LastPrice),
995            Some(TimeInForce::Gtc),
996            None,
997            Some(false),
998            Some(false),
999            Some(false),
1000            None,
1001            None,
1002            None,
1003            None,
1004            None,
1005            None,
1006            None,
1007        );
1008
1009        assert_eq!(stop_limit_order.instrument_id(), "BTCUSDT.BINANCE".into());
1010        assert_eq!(stop_limit_order.order_side(), OrderSide::Sell);
1011        assert_eq!(stop_limit_order.quantity(), 100.into());
1012        assert_eq!(stop_limit_order.price(), Some(Price::from("45100.00")));
1013        assert_eq!(
1014            stop_limit_order.trigger_price(),
1015            Some(Price::from("45000.00"))
1016        );
1017        assert_eq!(
1018            stop_limit_order.trigger_type(),
1019            Some(TriggerType::LastPrice)
1020        );
1021    }
1022
1023    #[rstest]
1024    fn test_market_if_touched_order(mut order_factory: OrderFactory) {
1025        let mit_order = order_factory.market_if_touched(
1026            InstrumentId::from("BTCUSDT.BINANCE"),
1027            OrderSide::Buy,
1028            100.into(),
1029            Price::from("48000.00"),
1030            Some(TriggerType::LastPrice),
1031            Some(TimeInForce::Gtc),
1032            None,
1033            Some(false),
1034            Some(false),
1035            None,
1036            None,
1037            None,
1038            None,
1039            None,
1040            None,
1041        );
1042
1043        assert_eq!(mit_order.instrument_id(), "BTCUSDT.BINANCE".into());
1044        assert_eq!(mit_order.order_side(), OrderSide::Buy);
1045        assert_eq!(mit_order.quantity(), 100.into());
1046        assert_eq!(mit_order.trigger_price(), Some(Price::from("48000.00")));
1047        assert_eq!(mit_order.trigger_type(), Some(TriggerType::LastPrice));
1048    }
1049
1050    #[rstest]
1051    fn test_limit_if_touched_order(mut order_factory: OrderFactory) {
1052        let lit_order = order_factory.limit_if_touched(
1053            InstrumentId::from("BTCUSDT.BINANCE"),
1054            OrderSide::Buy,
1055            100.into(),
1056            Price::from("48100.00"), // limit price
1057            Price::from("48000.00"), // trigger price
1058            Some(TriggerType::LastPrice),
1059            Some(TimeInForce::Gtc),
1060            None,
1061            Some(false),
1062            Some(false),
1063            Some(false),
1064            None,
1065            None,
1066            None,
1067            None,
1068            None,
1069            None,
1070            None,
1071        );
1072
1073        assert_eq!(lit_order.instrument_id(), "BTCUSDT.BINANCE".into());
1074        assert_eq!(lit_order.order_side(), OrderSide::Buy);
1075        assert_eq!(lit_order.quantity(), 100.into());
1076        assert_eq!(lit_order.price(), Some(Price::from("48100.00")));
1077        assert_eq!(lit_order.trigger_price(), Some(Price::from("48000.00")));
1078        assert_eq!(lit_order.trigger_type(), Some(TriggerType::LastPrice));
1079    }
1080
1081    #[rstest]
1082    fn test_bracket_order_with_market_entry(mut order_factory: OrderFactory) {
1083        let bracket = order_factory.bracket(
1084            InstrumentId::from("BTCUSDT.BINANCE"),
1085            OrderSide::Buy,
1086            100.into(),
1087            None,                    // market entry
1088            Price::from("45000.00"), // SL trigger
1089            None,                    // sl_trigger_type
1090            Price::from("55000.00"), // TP price
1091            None,                    // no entry trigger
1092            Some(TimeInForce::Gtc),
1093            None,
1094            Some(false),
1095            Some(false),
1096            Some(false),
1097            None,
1098            None,
1099            None,
1100            None,
1101            None,
1102        );
1103
1104        assert_eq!(bracket.orders.len(), 3);
1105        assert_eq!(bracket.instrument_id, "BTCUSDT.BINANCE".into());
1106
1107        // Entry should be market order
1108        assert_eq!(bracket.orders[0].order_side(), OrderSide::Buy);
1109
1110        // SL should be opposite side stop-market
1111        assert_eq!(bracket.orders[1].order_side(), OrderSide::Sell);
1112        assert_eq!(
1113            bracket.orders[1].trigger_price(),
1114            Some(Price::from("45000.00"))
1115        );
1116
1117        // TP should be opposite side limit
1118        assert_eq!(bracket.orders[2].order_side(), OrderSide::Sell);
1119        assert_eq!(bracket.orders[2].price(), Some(Price::from("55000.00")));
1120    }
1121
1122    #[rstest]
1123    fn test_bracket_order_with_limit_entry(mut order_factory: OrderFactory) {
1124        let bracket = order_factory.bracket(
1125            InstrumentId::from("BTCUSDT.BINANCE"),
1126            OrderSide::Buy,
1127            100.into(),
1128            Some(Price::from("49000.00")), // limit entry
1129            Price::from("45000.00"),       // SL trigger
1130            None,                          // sl_trigger_type
1131            Price::from("55000.00"),       // TP price
1132            None,                          // no entry trigger
1133            Some(TimeInForce::Gtc),
1134            None,
1135            Some(false),
1136            Some(false),
1137            Some(false),
1138            None,
1139            None,
1140            None,
1141            None,
1142            None,
1143        );
1144
1145        assert_eq!(bracket.orders.len(), 3);
1146
1147        // Entry should be limit order at entry price
1148        assert_eq!(bracket.orders[0].price(), Some(Price::from("49000.00")));
1149    }
1150
1151    #[rstest]
1152    fn test_bracket_order_with_stop_entry(mut order_factory: OrderFactory) {
1153        let bracket = order_factory.bracket(
1154            InstrumentId::from("BTCUSDT.BINANCE"),
1155            OrderSide::Buy,
1156            100.into(),
1157            None,                          // no limit price (stop-market entry)
1158            Price::from("45000.00"),       // SL trigger
1159            None,                          // sl_trigger_type
1160            Price::from("55000.00"),       // TP price
1161            Some(Price::from("51000.00")), // entry trigger (stop entry)
1162            Some(TimeInForce::Gtc),
1163            None,
1164            Some(false),
1165            Some(false),
1166            Some(false),
1167            None,
1168            None,
1169            None,
1170            None,
1171            None,
1172        );
1173
1174        assert_eq!(bracket.orders.len(), 3);
1175
1176        // Entry should be stop-market order
1177        assert_eq!(
1178            bracket.orders[0].trigger_price(),
1179            Some(Price::from("51000.00"))
1180        );
1181    }
1182
1183    #[rstest]
1184    fn test_bracket_order_sell_side(mut order_factory: OrderFactory) {
1185        let bracket = order_factory.bracket(
1186            InstrumentId::from("BTCUSDT.BINANCE"),
1187            OrderSide::Sell,
1188            100.into(),
1189            Some(Price::from("51000.00")), // limit entry
1190            Price::from("55000.00"),       // SL trigger (above entry for sell)
1191            None,                          // sl_trigger_type
1192            Price::from("45000.00"),       // TP price (below entry for sell)
1193            None,
1194            Some(TimeInForce::Gtc),
1195            None,
1196            Some(false),
1197            Some(false),
1198            Some(false),
1199            None,
1200            None,
1201            None,
1202            None,
1203            None,
1204        );
1205
1206        assert_eq!(bracket.orders.len(), 3);
1207
1208        // Entry should be sell
1209        assert_eq!(bracket.orders[0].order_side(), OrderSide::Sell);
1210
1211        // SL should be buy (opposite)
1212        assert_eq!(bracket.orders[1].order_side(), OrderSide::Buy);
1213
1214        // TP should be buy (opposite)
1215        assert_eq!(bracket.orders[2].order_side(), OrderSide::Buy);
1216    }
1217
1218    #[rstest]
1219    fn test_bracket_order_sets_contingencies(mut order_factory: OrderFactory) {
1220        let bracket = order_factory.bracket(
1221            InstrumentId::from("BTCUSDT.BINANCE"),
1222            OrderSide::Buy,
1223            100.into(),
1224            Some(Price::from("50000.00")), // entry_price
1225            Price::from("45000.00"),       // sl_trigger_price
1226            None,                          // sl_trigger_type
1227            Price::from("55000.00"),       // tp_price
1228            None,                          // entry_trigger_price
1229            Some(TimeInForce::Gtc),
1230            None,
1231            Some(false),
1232            Some(false),
1233            Some(false),
1234            None,
1235            None,
1236            None,
1237            None,
1238            None,
1239        );
1240
1241        let entry = &bracket.orders[0];
1242        let stop = &bracket.orders[1];
1243        let take = &bracket.orders[2];
1244
1245        assert_eq!(entry.order_list_id(), Some(bracket.id));
1246        assert_eq!(entry.contingency_type(), Some(ContingencyType::Oto));
1247        assert_eq!(
1248            entry.linked_order_ids().unwrap(),
1249            &[stop.client_order_id(), take.client_order_id()]
1250        );
1251
1252        assert_eq!(stop.order_list_id(), Some(bracket.id));
1253        assert_eq!(stop.contingency_type(), Some(ContingencyType::Oco));
1254        assert_eq!(stop.parent_order_id(), Some(entry.client_order_id()));
1255        assert_eq!(stop.linked_order_ids().unwrap(), &[take.client_order_id()]);
1256
1257        assert_eq!(take.order_list_id(), Some(bracket.id));
1258        assert_eq!(take.contingency_type(), Some(ContingencyType::Oco));
1259        assert_eq!(take.parent_order_id(), Some(entry.client_order_id()));
1260        assert_eq!(take.linked_order_ids().unwrap(), &[stop.client_order_id()]);
1261    }
1262}