nautilus_common/
factories.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 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        tp_price: Price,
455        entry_trigger_price: Option<Price>,
456        time_in_force: Option<TimeInForce>,
457        expire_time: Option<nautilus_core::UnixNanos>,
458        post_only: Option<bool>,
459        reduce_only: Option<bool>,
460        quote_quantity: Option<bool>,
461        emulation_trigger: Option<TriggerType>,
462        trigger_instrument_id: Option<InstrumentId>,
463        exec_algorithm_id: Option<ExecAlgorithmId>,
464        exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
465        tags: Option<Vec<Ustr>>,
466    ) -> OrderList {
467        let order_list_id = self.generate_order_list_id();
468        let ts_init = self.clock.get_time_ns();
469
470        let entry_client_order_id = self.generate_client_order_id();
471        let sl_client_order_id = self.generate_client_order_id();
472        let tp_client_order_id = self.generate_client_order_id();
473
474        // Exec spawn IDs for algorithm orders
475        let entry_exec_spawn_id = exec_algorithm_id.as_ref().map(|_| entry_client_order_id);
476        let sl_exec_spawn_id = exec_algorithm_id.as_ref().map(|_| sl_client_order_id);
477        let tp_exec_spawn_id = exec_algorithm_id.as_ref().map(|_| tp_client_order_id);
478
479        // Entry order linkage
480        let entry_contingency_type = Some(ContingencyType::Oto);
481        let entry_order_list_id = Some(order_list_id);
482        let entry_linked_order_ids = Some(vec![sl_client_order_id, tp_client_order_id]);
483        let entry_parent_order_id = None;
484
485        let entry_order = if let Some(trigger_price) = entry_trigger_price {
486            if let Some(price) = entry_price {
487                OrderAny::StopLimit(StopLimitOrder::new(
488                    self.trader_id,
489                    self.strategy_id,
490                    instrument_id,
491                    entry_client_order_id,
492                    order_side,
493                    quantity,
494                    price,
495                    trigger_price,
496                    TriggerType::Default,
497                    time_in_force.unwrap_or(TimeInForce::Gtc),
498                    expire_time,
499                    post_only.unwrap_or(false),
500                    reduce_only.unwrap_or(false),
501                    quote_quantity.unwrap_or(false),
502                    None, // display_qty
503                    emulation_trigger,
504                    trigger_instrument_id,
505                    entry_contingency_type,
506                    entry_order_list_id,
507                    entry_linked_order_ids,
508                    entry_parent_order_id,
509                    exec_algorithm_id,
510                    exec_algorithm_params.clone(),
511                    entry_exec_spawn_id,
512                    tags.clone(),
513                    UUID4::new(),
514                    ts_init,
515                ))
516            } else {
517                OrderAny::StopMarket(StopMarketOrder::new(
518                    self.trader_id,
519                    self.strategy_id,
520                    instrument_id,
521                    entry_client_order_id,
522                    order_side,
523                    quantity,
524                    trigger_price,
525                    TriggerType::Default,
526                    time_in_force.unwrap_or(TimeInForce::Gtc),
527                    expire_time,
528                    reduce_only.unwrap_or(false),
529                    quote_quantity.unwrap_or(false),
530                    None, // display_qty
531                    emulation_trigger,
532                    trigger_instrument_id,
533                    entry_contingency_type,
534                    entry_order_list_id,
535                    entry_linked_order_ids,
536                    entry_parent_order_id,
537                    exec_algorithm_id,
538                    exec_algorithm_params.clone(),
539                    entry_exec_spawn_id,
540                    tags.clone(),
541                    UUID4::new(),
542                    ts_init,
543                ))
544            }
545        } else if let Some(price) = entry_price {
546            OrderAny::Limit(LimitOrder::new(
547                self.trader_id,
548                self.strategy_id,
549                instrument_id,
550                entry_client_order_id,
551                order_side,
552                quantity,
553                price,
554                time_in_force.unwrap_or(TimeInForce::Gtc),
555                expire_time,
556                post_only.unwrap_or(false),
557                reduce_only.unwrap_or(false),
558                quote_quantity.unwrap_or(false),
559                None, // display_qty
560                emulation_trigger,
561                trigger_instrument_id,
562                entry_contingency_type,
563                entry_order_list_id,
564                entry_linked_order_ids,
565                entry_parent_order_id,
566                exec_algorithm_id,
567                exec_algorithm_params.clone(),
568                entry_exec_spawn_id,
569                tags.clone(),
570                UUID4::new(),
571                ts_init,
572            ))
573        } else {
574            OrderAny::Market(MarketOrder::new(
575                self.trader_id,
576                self.strategy_id,
577                instrument_id,
578                entry_client_order_id,
579                order_side,
580                quantity,
581                time_in_force.unwrap_or(TimeInForce::Gtc),
582                UUID4::new(),
583                ts_init,
584                reduce_only.unwrap_or(false),
585                quote_quantity.unwrap_or(false),
586                entry_contingency_type,
587                entry_order_list_id,
588                entry_linked_order_ids,
589                entry_parent_order_id,
590                exec_algorithm_id,
591                exec_algorithm_params.clone(),
592                entry_exec_spawn_id,
593                tags.clone(),
594            ))
595        };
596
597        let sl_tp_side = match order_side {
598            OrderSide::Buy => OrderSide::Sell,
599            OrderSide::Sell => OrderSide::Buy,
600            OrderSide::NoOrderSide => OrderSide::NoOrderSide,
601        };
602
603        // SL order linkage
604        let sl_contingency_type = Some(ContingencyType::Oco);
605        let sl_order_list_id = Some(order_list_id);
606        let sl_linked_order_ids = Some(vec![tp_client_order_id]);
607        let sl_parent_order_id = Some(entry_client_order_id);
608
609        let sl_order = OrderAny::StopMarket(StopMarketOrder::new(
610            self.trader_id,
611            self.strategy_id,
612            instrument_id,
613            sl_client_order_id,
614            sl_tp_side,
615            quantity,
616            sl_trigger_price,
617            TriggerType::Default,
618            time_in_force.unwrap_or(TimeInForce::Gtc),
619            expire_time,
620            true, // SL/TP should only reduce positions
621            quote_quantity.unwrap_or(false),
622            None, // display_qty
623            emulation_trigger,
624            trigger_instrument_id,
625            sl_contingency_type,
626            sl_order_list_id,
627            sl_linked_order_ids,
628            sl_parent_order_id,
629            exec_algorithm_id,
630            exec_algorithm_params.clone(),
631            sl_exec_spawn_id,
632            tags.clone(),
633            UUID4::new(),
634            ts_init,
635        ));
636
637        // TP order linkage
638        let tp_contingency_type = Some(ContingencyType::Oco);
639        let tp_order_list_id = Some(order_list_id);
640        let tp_linked_order_ids = Some(vec![sl_client_order_id]);
641        let tp_parent_order_id = Some(entry_client_order_id);
642
643        let tp_order = OrderAny::Limit(LimitOrder::new(
644            self.trader_id,
645            self.strategy_id,
646            instrument_id,
647            tp_client_order_id,
648            sl_tp_side,
649            quantity,
650            tp_price,
651            time_in_force.unwrap_or(TimeInForce::Gtc),
652            expire_time,
653            post_only.unwrap_or(false),
654            true, // SL/TP should only reduce positions
655            quote_quantity.unwrap_or(false),
656            None, // display_qty
657            emulation_trigger,
658            trigger_instrument_id,
659            tp_contingency_type,
660            tp_order_list_id,
661            tp_linked_order_ids,
662            tp_parent_order_id,
663            exec_algorithm_id,
664            exec_algorithm_params,
665            tp_exec_spawn_id,
666            tags,
667            UUID4::new(),
668            ts_init,
669        ));
670
671        OrderList::new(
672            order_list_id,
673            instrument_id,
674            self.strategy_id,
675            vec![entry_order, sl_order, tp_order],
676            ts_init,
677        )
678    }
679}
680
681#[cfg(test)]
682pub mod tests {
683    use nautilus_core::time::get_atomic_clock_static;
684    use nautilus_model::{
685        enums::{ContingencyType, OrderSide, TimeInForce, TriggerType},
686        identifiers::{
687            ClientOrderId, InstrumentId, OrderListId,
688            stubs::{strategy_id_ema_cross, trader_id},
689        },
690        orders::Order,
691        types::Price,
692    };
693    use rstest::{fixture, rstest};
694
695    use crate::factories::OrderFactory;
696
697    #[fixture]
698    pub fn order_factory() -> OrderFactory {
699        let trader_id = trader_id();
700        let strategy_id = strategy_id_ema_cross();
701        OrderFactory::new(
702            trader_id,
703            strategy_id,
704            None,
705            None,
706            get_atomic_clock_static(),
707            false, // use_uuids_for_client_order_ids
708            true,  // use_hyphens_in_client_order_ids
709        )
710    }
711
712    #[rstest]
713    fn test_generate_client_order_id(mut order_factory: OrderFactory) {
714        let client_order_id = order_factory.generate_client_order_id();
715        assert_eq!(
716            client_order_id,
717            ClientOrderId::new("O-19700101-000000-001-001-1")
718        );
719    }
720
721    #[rstest]
722    fn test_generate_order_list_id(mut order_factory: OrderFactory) {
723        let order_list_id = order_factory.generate_order_list_id();
724        assert_eq!(
725            order_list_id,
726            OrderListId::new("OL-19700101-000000-001-001-1")
727        );
728    }
729
730    #[rstest]
731    fn test_set_client_order_id_count(mut order_factory: OrderFactory) {
732        order_factory.set_client_order_id_count(10);
733        let client_order_id = order_factory.generate_client_order_id();
734        assert_eq!(
735            client_order_id,
736            ClientOrderId::new("O-19700101-000000-001-001-11")
737        );
738    }
739
740    #[rstest]
741    fn test_set_order_list_id_count(mut order_factory: OrderFactory) {
742        order_factory.set_order_list_id_count(10);
743        let order_list_id = order_factory.generate_order_list_id();
744        assert_eq!(
745            order_list_id,
746            OrderListId::new("OL-19700101-000000-001-001-11")
747        );
748    }
749
750    #[rstest]
751    fn test_reset_factory(mut order_factory: OrderFactory) {
752        order_factory.generate_order_list_id();
753        order_factory.generate_client_order_id();
754        order_factory.reset_factory();
755        let client_order_id = order_factory.generate_client_order_id();
756        let order_list_id = order_factory.generate_order_list_id();
757        assert_eq!(
758            client_order_id,
759            ClientOrderId::new("O-19700101-000000-001-001-1")
760        );
761        assert_eq!(
762            order_list_id,
763            OrderListId::new("OL-19700101-000000-001-001-1")
764        );
765    }
766
767    #[fixture]
768    pub fn order_factory_with_uuids() -> OrderFactory {
769        let trader_id = trader_id();
770        let strategy_id = strategy_id_ema_cross();
771        OrderFactory::new(
772            trader_id,
773            strategy_id,
774            None,
775            None,
776            get_atomic_clock_static(),
777            true, // use_uuids_for_client_order_ids
778            true, // use_hyphens_in_client_order_ids
779        )
780    }
781
782    #[fixture]
783    pub fn order_factory_with_hyphens_removed() -> OrderFactory {
784        let trader_id = trader_id();
785        let strategy_id = strategy_id_ema_cross();
786        OrderFactory::new(
787            trader_id,
788            strategy_id,
789            None,
790            None,
791            get_atomic_clock_static(),
792            false, // use_uuids_for_client_order_ids
793            false, // use_hyphens_in_client_order_ids
794        )
795    }
796
797    #[fixture]
798    pub fn order_factory_with_uuids_and_hyphens_removed() -> OrderFactory {
799        let trader_id = trader_id();
800        let strategy_id = strategy_id_ema_cross();
801        OrderFactory::new(
802            trader_id,
803            strategy_id,
804            None,
805            None,
806            get_atomic_clock_static(),
807            true,  // use_uuids_for_client_order_ids
808            false, // use_hyphens_in_client_order_ids
809        )
810    }
811
812    #[rstest]
813    fn test_generate_client_order_id_with_uuids(mut order_factory_with_uuids: OrderFactory) {
814        let client_order_id = order_factory_with_uuids.generate_client_order_id();
815
816        // UUID should be 36 characters with hyphens
817        assert_eq!(client_order_id.as_str().len(), 36);
818        assert!(client_order_id.as_str().contains('-'));
819    }
820
821    #[rstest]
822    fn test_generate_client_order_id_with_hyphens_removed(
823        mut order_factory_with_hyphens_removed: OrderFactory,
824    ) {
825        let client_order_id = order_factory_with_hyphens_removed.generate_client_order_id();
826
827        assert_eq!(
828            client_order_id,
829            ClientOrderId::new("O197001010000000010011")
830        );
831        assert!(!client_order_id.as_str().contains('-'));
832    }
833
834    #[rstest]
835    fn test_generate_client_order_id_with_uuids_and_hyphens_removed(
836        mut order_factory_with_uuids_and_hyphens_removed: OrderFactory,
837    ) {
838        let client_order_id =
839            order_factory_with_uuids_and_hyphens_removed.generate_client_order_id();
840
841        // UUID without hyphens should be 32 characters
842        assert_eq!(client_order_id.as_str().len(), 32);
843        assert!(!client_order_id.as_str().contains('-'));
844    }
845
846    #[rstest]
847    fn test_market_order(mut order_factory: OrderFactory) {
848        let market_order = order_factory.market(
849            InstrumentId::from("BTCUSDT.BINANCE"),
850            OrderSide::Buy,
851            100.into(),
852            Some(TimeInForce::Gtc),
853            Some(false),
854            Some(false),
855            None,
856            None,
857            None,
858            None,
859        );
860        // TODO: Add additional polymorphic getters
861        assert_eq!(market_order.instrument_id(), "BTCUSDT.BINANCE".into());
862        assert_eq!(market_order.order_side(), OrderSide::Buy);
863        assert_eq!(market_order.quantity(), 100.into());
864        // assert_eq!(market_order.time_in_force(), TimeInForce::Gtc);
865        // assert!(!market_order.is_reduce_only);
866        // assert!(!market_order.is_quote_quantity);
867        assert_eq!(market_order.exec_algorithm_id(), None);
868        // assert_eq!(market_order.exec_algorithm_params(), None);
869        // assert_eq!(market_order.exec_spawn_id, None);
870        // assert_eq!(market_order.tags, None);
871        assert_eq!(
872            market_order.client_order_id(),
873            ClientOrderId::new("O-19700101-000000-001-001-1")
874        );
875        // assert_eq!(market_order.order_list_id(), None);
876    }
877
878    #[rstest]
879    fn test_limit_order(mut order_factory: OrderFactory) {
880        let limit_order = order_factory.limit(
881            InstrumentId::from("BTCUSDT.BINANCE"),
882            OrderSide::Buy,
883            100.into(),
884            Price::from("50000.00"),
885            Some(TimeInForce::Gtc),
886            None,
887            Some(false),
888            Some(false),
889            Some(false),
890            None,
891            None,
892            None,
893            None,
894            None,
895            None,
896            None,
897        );
898
899        assert_eq!(limit_order.instrument_id(), "BTCUSDT.BINANCE".into());
900        assert_eq!(limit_order.order_side(), OrderSide::Buy);
901        assert_eq!(limit_order.quantity(), 100.into());
902        assert_eq!(limit_order.price(), Some(Price::from("50000.00")));
903        assert_eq!(
904            limit_order.client_order_id(),
905            ClientOrderId::new("O-19700101-000000-001-001-1")
906        );
907    }
908
909    #[rstest]
910    fn test_limit_order_with_post_only(mut order_factory: OrderFactory) {
911        let limit_order = order_factory.limit(
912            InstrumentId::from("BTCUSDT.BINANCE"),
913            OrderSide::Buy,
914            100.into(),
915            Price::from("50000.00"),
916            Some(TimeInForce::Gtc),
917            None,
918            Some(true), // post_only
919            Some(false),
920            Some(false),
921            None,
922            None,
923            None,
924            None,
925            None,
926            None,
927            None,
928        );
929
930        assert!(limit_order.is_post_only());
931    }
932
933    #[rstest]
934    fn test_limit_order_with_display_qty(mut order_factory: OrderFactory) {
935        let limit_order = order_factory.limit(
936            InstrumentId::from("BTCUSDT.BINANCE"),
937            OrderSide::Buy,
938            100.into(),
939            Price::from("50000.00"),
940            Some(TimeInForce::Gtc),
941            None,
942            Some(false),     // post_only
943            Some(false),     // reduce_only
944            Some(false),     // quote_quantity
945            Some(50.into()), // display_qty
946            None,
947            None,
948            None,
949            None,
950            None,
951            None,
952        );
953
954        assert_eq!(limit_order.display_qty(), Some(50.into()));
955    }
956
957    #[rstest]
958    fn test_stop_market_order(mut order_factory: OrderFactory) {
959        let stop_order = order_factory.stop_market(
960            InstrumentId::from("BTCUSDT.BINANCE"),
961            OrderSide::Sell,
962            100.into(),
963            Price::from("45000.00"),
964            Some(TriggerType::LastPrice),
965            Some(TimeInForce::Gtc),
966            None,
967            Some(false),
968            Some(false),
969            None,
970            None,
971            None,
972            None,
973            None,
974            None,
975            None,
976        );
977
978        assert_eq!(stop_order.instrument_id(), "BTCUSDT.BINANCE".into());
979        assert_eq!(stop_order.order_side(), OrderSide::Sell);
980        assert_eq!(stop_order.quantity(), 100.into());
981        assert_eq!(stop_order.trigger_price(), Some(Price::from("45000.00")));
982        assert_eq!(stop_order.trigger_type(), Some(TriggerType::LastPrice));
983    }
984
985    #[rstest]
986    fn test_stop_limit_order(mut order_factory: OrderFactory) {
987        let stop_limit_order = order_factory.stop_limit(
988            InstrumentId::from("BTCUSDT.BINANCE"),
989            OrderSide::Sell,
990            100.into(),
991            Price::from("45100.00"), // limit price
992            Price::from("45000.00"), // trigger price
993            Some(TriggerType::LastPrice),
994            Some(TimeInForce::Gtc),
995            None,
996            Some(false),
997            Some(false),
998            Some(false),
999            None,
1000            None,
1001            None,
1002            None,
1003            None,
1004            None,
1005            None,
1006        );
1007
1008        assert_eq!(stop_limit_order.instrument_id(), "BTCUSDT.BINANCE".into());
1009        assert_eq!(stop_limit_order.order_side(), OrderSide::Sell);
1010        assert_eq!(stop_limit_order.quantity(), 100.into());
1011        assert_eq!(stop_limit_order.price(), Some(Price::from("45100.00")));
1012        assert_eq!(
1013            stop_limit_order.trigger_price(),
1014            Some(Price::from("45000.00"))
1015        );
1016        assert_eq!(
1017            stop_limit_order.trigger_type(),
1018            Some(TriggerType::LastPrice)
1019        );
1020    }
1021
1022    #[rstest]
1023    fn test_market_if_touched_order(mut order_factory: OrderFactory) {
1024        let mit_order = order_factory.market_if_touched(
1025            InstrumentId::from("BTCUSDT.BINANCE"),
1026            OrderSide::Buy,
1027            100.into(),
1028            Price::from("48000.00"),
1029            Some(TriggerType::LastPrice),
1030            Some(TimeInForce::Gtc),
1031            None,
1032            Some(false),
1033            Some(false),
1034            None,
1035            None,
1036            None,
1037            None,
1038            None,
1039            None,
1040        );
1041
1042        assert_eq!(mit_order.instrument_id(), "BTCUSDT.BINANCE".into());
1043        assert_eq!(mit_order.order_side(), OrderSide::Buy);
1044        assert_eq!(mit_order.quantity(), 100.into());
1045        assert_eq!(mit_order.trigger_price(), Some(Price::from("48000.00")));
1046        assert_eq!(mit_order.trigger_type(), Some(TriggerType::LastPrice));
1047    }
1048
1049    #[rstest]
1050    fn test_limit_if_touched_order(mut order_factory: OrderFactory) {
1051        let lit_order = order_factory.limit_if_touched(
1052            InstrumentId::from("BTCUSDT.BINANCE"),
1053            OrderSide::Buy,
1054            100.into(),
1055            Price::from("48100.00"), // limit price
1056            Price::from("48000.00"), // trigger price
1057            Some(TriggerType::LastPrice),
1058            Some(TimeInForce::Gtc),
1059            None,
1060            Some(false),
1061            Some(false),
1062            Some(false),
1063            None,
1064            None,
1065            None,
1066            None,
1067            None,
1068            None,
1069            None,
1070        );
1071
1072        assert_eq!(lit_order.instrument_id(), "BTCUSDT.BINANCE".into());
1073        assert_eq!(lit_order.order_side(), OrderSide::Buy);
1074        assert_eq!(lit_order.quantity(), 100.into());
1075        assert_eq!(lit_order.price(), Some(Price::from("48100.00")));
1076        assert_eq!(lit_order.trigger_price(), Some(Price::from("48000.00")));
1077        assert_eq!(lit_order.trigger_type(), Some(TriggerType::LastPrice));
1078    }
1079
1080    #[rstest]
1081    fn test_bracket_order_with_market_entry(mut order_factory: OrderFactory) {
1082        let bracket = order_factory.bracket(
1083            InstrumentId::from("BTCUSDT.BINANCE"),
1084            OrderSide::Buy,
1085            100.into(),
1086            None,                    // market entry
1087            Price::from("45000.00"), // SL trigger
1088            Price::from("55000.00"), // TP price
1089            None,                    // no entry trigger
1090            Some(TimeInForce::Gtc),
1091            None,
1092            Some(false),
1093            Some(false),
1094            Some(false),
1095            None,
1096            None,
1097            None,
1098            None,
1099            None,
1100        );
1101
1102        assert_eq!(bracket.orders.len(), 3);
1103        assert_eq!(bracket.instrument_id, "BTCUSDT.BINANCE".into());
1104
1105        // Entry should be market order
1106        assert_eq!(bracket.orders[0].order_side(), OrderSide::Buy);
1107
1108        // SL should be opposite side stop-market
1109        assert_eq!(bracket.orders[1].order_side(), OrderSide::Sell);
1110        assert_eq!(
1111            bracket.orders[1].trigger_price(),
1112            Some(Price::from("45000.00"))
1113        );
1114
1115        // TP should be opposite side limit
1116        assert_eq!(bracket.orders[2].order_side(), OrderSide::Sell);
1117        assert_eq!(bracket.orders[2].price(), Some(Price::from("55000.00")));
1118    }
1119
1120    #[rstest]
1121    fn test_bracket_order_with_limit_entry(mut order_factory: OrderFactory) {
1122        let bracket = order_factory.bracket(
1123            InstrumentId::from("BTCUSDT.BINANCE"),
1124            OrderSide::Buy,
1125            100.into(),
1126            Some(Price::from("49000.00")), // limit entry
1127            Price::from("45000.00"),       // SL trigger
1128            Price::from("55000.00"),       // TP price
1129            None,                          // no entry trigger
1130            Some(TimeInForce::Gtc),
1131            None,
1132            Some(false),
1133            Some(false),
1134            Some(false),
1135            None,
1136            None,
1137            None,
1138            None,
1139            None,
1140        );
1141
1142        assert_eq!(bracket.orders.len(), 3);
1143
1144        // Entry should be limit order at entry price
1145        assert_eq!(bracket.orders[0].price(), Some(Price::from("49000.00")));
1146    }
1147
1148    #[rstest]
1149    fn test_bracket_order_with_stop_entry(mut order_factory: OrderFactory) {
1150        let bracket = order_factory.bracket(
1151            InstrumentId::from("BTCUSDT.BINANCE"),
1152            OrderSide::Buy,
1153            100.into(),
1154            None,                          // no limit price (stop-market entry)
1155            Price::from("45000.00"),       // SL trigger
1156            Price::from("55000.00"),       // TP price
1157            Some(Price::from("51000.00")), // entry trigger (stop entry)
1158            Some(TimeInForce::Gtc),
1159            None,
1160            Some(false),
1161            Some(false),
1162            Some(false),
1163            None,
1164            None,
1165            None,
1166            None,
1167            None,
1168        );
1169
1170        assert_eq!(bracket.orders.len(), 3);
1171
1172        // Entry should be stop-market order
1173        assert_eq!(
1174            bracket.orders[0].trigger_price(),
1175            Some(Price::from("51000.00"))
1176        );
1177    }
1178
1179    #[rstest]
1180    fn test_bracket_order_sell_side(mut order_factory: OrderFactory) {
1181        let bracket = order_factory.bracket(
1182            InstrumentId::from("BTCUSDT.BINANCE"),
1183            OrderSide::Sell,
1184            100.into(),
1185            Some(Price::from("51000.00")), // limit entry
1186            Price::from("55000.00"),       // SL trigger (above entry for sell)
1187            Price::from("45000.00"),       // TP price (below entry for sell)
1188            None,
1189            Some(TimeInForce::Gtc),
1190            None,
1191            Some(false),
1192            Some(false),
1193            Some(false),
1194            None,
1195            None,
1196            None,
1197            None,
1198            None,
1199        );
1200
1201        assert_eq!(bracket.orders.len(), 3);
1202
1203        // Entry should be sell
1204        assert_eq!(bracket.orders[0].order_side(), OrderSide::Sell);
1205
1206        // SL should be buy (opposite)
1207        assert_eq!(bracket.orders[1].order_side(), OrderSide::Buy);
1208
1209        // TP should be buy (opposite)
1210        assert_eq!(bracket.orders[2].order_side(), OrderSide::Buy);
1211    }
1212
1213    #[rstest]
1214    fn test_bracket_order_sets_contingencies(mut order_factory: OrderFactory) {
1215        let bracket = order_factory.bracket(
1216            InstrumentId::from("BTCUSDT.BINANCE"),
1217            OrderSide::Buy,
1218            100.into(),
1219            Some(Price::from("50000.00")),
1220            Price::from("45000.00"),
1221            Price::from("55000.00"),
1222            None,
1223            Some(TimeInForce::Gtc),
1224            None,
1225            Some(false),
1226            Some(false),
1227            Some(false),
1228            None,
1229            None,
1230            None,
1231            None,
1232            None,
1233        );
1234
1235        let entry = &bracket.orders[0];
1236        let stop = &bracket.orders[1];
1237        let take = &bracket.orders[2];
1238
1239        assert_eq!(entry.order_list_id(), Some(bracket.id));
1240        assert_eq!(entry.contingency_type(), Some(ContingencyType::Oto));
1241        assert_eq!(
1242            entry.linked_order_ids().unwrap(),
1243            &[stop.client_order_id(), take.client_order_id()]
1244        );
1245
1246        assert_eq!(stop.order_list_id(), Some(bracket.id));
1247        assert_eq!(stop.contingency_type(), Some(ContingencyType::Oco));
1248        assert_eq!(stop.parent_order_id(), Some(entry.client_order_id()));
1249        assert_eq!(stop.linked_order_ids().unwrap(), &[take.client_order_id()]);
1250
1251        assert_eq!(take.order_list_id(), Some(bracket.id));
1252        assert_eq!(take.contingency_type(), Some(ContingencyType::Oco));
1253        assert_eq!(take.parent_order_id(), Some(entry.client_order_id()));
1254        assert_eq!(take.linked_order_ids().unwrap(), &[stop.client_order_id()]);
1255    }
1256}