Skip to main content

nautilus_common/factories/
order.rs

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