Skip to main content

nautilus_execution/order_manager/
manager.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
16use std::{cell::RefCell, fmt::Debug, rc::Rc};
17
18use ahash::AHashMap;
19use nautilus_common::{cache::Cache, clock::Clock, messages::execution::SubmitOrder};
20use nautilus_core::UUID4;
21use nautilus_model::{
22    enums::{ContingencyType, TriggerType},
23    events::{
24        OrderCanceled, OrderEventAny, OrderExpired, OrderFilled, OrderRejected, OrderUpdated,
25    },
26    identifiers::{ClientId, ClientOrderId, PositionId},
27    orders::{Order, OrderAny},
28    types::Quantity,
29};
30
31use super::OrderManagerAction;
32
33/// Manages the lifecycle and state of orders with contingency handling.
34///
35/// The order manager is responsible for managing local order state, handling
36/// contingent orders (OTO, OCO, OUO), and coordinating with emulation and
37/// execution systems. It tracks order commands and manages complex order
38/// relationships for advanced order types.
39pub struct OrderManager {
40    clock: Rc<RefCell<dyn Clock>>,
41    cache: Rc<RefCell<Cache>>,
42    active_local: bool,
43    submit_order_commands: AHashMap<ClientOrderId, SubmitOrder>,
44}
45
46impl Debug for OrderManager {
47    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        f.debug_struct(stringify!(OrderManager))
49            .field("pending_commands", &self.submit_order_commands.len())
50            .finish()
51    }
52}
53
54impl OrderManager {
55    /// Creates a new [`OrderManager`] instance.
56    pub fn new(
57        clock: Rc<RefCell<dyn Clock>>,
58        cache: Rc<RefCell<Cache>>,
59        active_local: bool,
60    ) -> Self {
61        Self {
62            clock,
63            cache,
64            active_local,
65            submit_order_commands: AHashMap::new(),
66        }
67    }
68
69    #[must_use]
70    /// Returns a copy of all cached submit order commands.
71    pub fn get_submit_order_commands(&self) -> AHashMap<ClientOrderId, SubmitOrder> {
72        self.submit_order_commands.clone()
73    }
74
75    /// Caches a submit order command for later processing.
76    pub fn cache_submit_order_command(&mut self, command: SubmitOrder) {
77        self.submit_order_commands
78            .insert(command.client_order_id, command);
79    }
80
81    /// Removes and returns a cached submit order command.
82    pub fn pop_submit_order_command(
83        &mut self,
84        client_order_id: ClientOrderId,
85    ) -> Option<SubmitOrder> {
86        self.submit_order_commands.remove(&client_order_id)
87    }
88
89    /// Resets the order manager by clearing all cached commands.
90    pub fn reset(&mut self) {
91        self.submit_order_commands.clear();
92    }
93
94    /// Cancels an order if it's not already pending cancellation or closed.
95    pub fn cancel_order(&mut self, order: &OrderAny) -> Vec<OrderManagerAction> {
96        let client_order_id = order.client_order_id();
97        let cache = self.cache.borrow();
98
99        if cache.is_order_pending_cancel_local(&client_order_id) {
100            return Vec::new();
101        }
102
103        if order.is_closed() || cache.is_order_closed(&client_order_id) {
104            log::warn!("Cannot cancel order: already closed");
105            return Vec::new();
106        }
107
108        drop(cache);
109        self.submit_order_commands.remove(&client_order_id);
110
111        vec![OrderManagerAction::CancelLocal(order.clone())]
112    }
113
114    /// Modifies the quantity of an existing order.
115    pub fn modify_order_quantity(
116        &mut self,
117        order: &OrderAny,
118        new_quantity: Quantity,
119    ) -> Vec<OrderManagerAction> {
120        vec![OrderManagerAction::ModifyLocalQuantity {
121            order: order.clone(),
122            quantity: new_quantity,
123        }]
124    }
125
126    /// # Errors
127    ///
128    /// Returns an error if creating a new submit order fails.
129    pub fn create_new_submit_order(
130        &mut self,
131        order: &OrderAny,
132        position_id: Option<PositionId>,
133        client_id: Option<ClientId>,
134        correlation_id: Option<UUID4>,
135    ) -> anyhow::Result<Vec<OrderManagerAction>> {
136        let mut actions = Vec::new();
137        let order_exists = self.cache.borrow().order_exists(&order.client_order_id());
138
139        self.cache
140            .borrow_mut()
141            .add_order(order.clone(), position_id, client_id, true)?;
142
143        if !order_exists {
144            actions.push(initialized_action(order));
145        }
146
147        let submit = SubmitOrder::new(
148            order.trader_id(),
149            client_id,
150            order.strategy_id(),
151            order.instrument_id(),
152            order.client_order_id(),
153            order.init_event().clone(),
154            order.exec_algorithm_id(),
155            position_id,
156            None, // params
157            UUID4::new(),
158            self.clock.borrow().timestamp_ns(),
159            correlation_id,
160        );
161
162        if matches!(order.emulation_trigger(), Some(trigger) if trigger != TriggerType::NoTrigger) {
163            self.cache_submit_order_command(submit.clone());
164            actions.push(OrderManagerAction::SubmitToEmulator(submit));
165        } else {
166            self.cache_submit_order_command(submit.clone());
167
168            match order.exec_algorithm_id() {
169                Some(exec_algorithm_id) => {
170                    actions.push(OrderManagerAction::SubmitToAlgorithm {
171                        command: submit,
172                        exec_algorithm_id,
173                    });
174                }
175                None => actions.push(OrderManagerAction::SubmitToRisk(submit)),
176            }
177        }
178
179        Ok(actions)
180    }
181
182    #[must_use]
183    /// Returns true if the order manager should manage the given order.
184    pub fn should_manage_order(&self, order: &OrderAny) -> bool {
185        self.active_local && order.is_active_local()
186    }
187
188    /// Handles an order event by routing it to the appropriate handler method.
189    ///
190    /// Note: Only handles specific terminal/actionable events. Other events
191    /// like `OrderSubmitted`, `OrderAccepted`, etc. are no-ops for the order manager.
192    pub fn handle_event(&mut self, event: &OrderEventAny) -> Vec<OrderManagerAction> {
193        match event {
194            OrderEventAny::Rejected(event) => self.handle_order_rejected(*event),
195            OrderEventAny::Canceled(event) => self.handle_order_canceled(*event),
196            OrderEventAny::Expired(event) => self.handle_order_expired(*event),
197            OrderEventAny::Updated(event) => self.handle_order_updated(*event),
198            OrderEventAny::Filled(event) => self.handle_order_filled(event),
199            _ => Vec::new(),
200        }
201    }
202
203    /// Handles an order rejected event and manages any contingent orders.
204    pub fn handle_order_rejected(&mut self, rejected: OrderRejected) -> Vec<OrderManagerAction> {
205        let cloned_order = self
206            .cache
207            .borrow()
208            .order(&rejected.client_order_id)
209            .map(|o| o.clone());
210
211        if let Some(order) = cloned_order {
212            if order
213                .contingency_type()
214                .is_some_and(|c| c != ContingencyType::NoContingency)
215            {
216                return self.handle_contingencies(&order);
217            }
218        } else {
219            log::error!(
220                "Cannot handle `OrderRejected`: order for client_order_id: {} not found, {}",
221                rejected.client_order_id,
222                rejected
223            );
224        }
225
226        Vec::new()
227    }
228
229    pub fn handle_order_canceled(&mut self, canceled: OrderCanceled) -> Vec<OrderManagerAction> {
230        let cloned_order = self
231            .cache
232            .borrow()
233            .order(&canceled.client_order_id)
234            .map(|o| o.clone());
235
236        if let Some(order) = cloned_order {
237            if order
238                .contingency_type()
239                .is_some_and(|c| c != ContingencyType::NoContingency)
240            {
241                return self.handle_contingencies(&order);
242            }
243        } else {
244            log::error!(
245                "Cannot handle `OrderCanceled`: order for client_order_id: {} not found, {}",
246                canceled.client_order_id,
247                canceled
248            );
249        }
250
251        Vec::new()
252    }
253
254    pub fn handle_order_expired(&mut self, expired: OrderExpired) -> Vec<OrderManagerAction> {
255        let cloned_order = self
256            .cache
257            .borrow()
258            .order(&expired.client_order_id)
259            .map(|o| o.clone());
260
261        if let Some(order) = cloned_order {
262            if order
263                .contingency_type()
264                .is_some_and(|c| c != ContingencyType::NoContingency)
265            {
266                return self.handle_contingencies(&order);
267            }
268        } else {
269            log::error!(
270                "Cannot handle `OrderExpired`: order for client_order_id: {} not found, {}",
271                expired.client_order_id,
272                expired
273            );
274        }
275
276        Vec::new()
277    }
278
279    pub fn handle_order_updated(&mut self, updated: OrderUpdated) -> Vec<OrderManagerAction> {
280        let cloned_order = self
281            .cache
282            .borrow()
283            .order(&updated.client_order_id)
284            .map(|o| o.clone());
285
286        if let Some(order) = cloned_order {
287            if order
288                .contingency_type()
289                .is_some_and(|c| c != ContingencyType::NoContingency)
290            {
291                return self.handle_contingencies_update(&order);
292            }
293        } else {
294            log::error!(
295                "Cannot handle `OrderUpdated`: order for client_order_id: {} not found, {}",
296                updated.client_order_id,
297                updated
298            );
299        }
300
301        Vec::new()
302    }
303
304    pub fn handle_order_filled(&mut self, filled: &OrderFilled) -> Vec<OrderManagerAction> {
305        let order = if let Some(order) = self
306            .cache
307            .borrow()
308            .order(&filled.client_order_id)
309            .map(|o| o.clone())
310        {
311            order
312        } else {
313            log::error!(
314                "Cannot handle `OrderFilled`: order for client_order_id: {} not found, {}",
315                filled.client_order_id,
316                filled
317            );
318            return Vec::new();
319        };
320
321        let mut actions = Vec::new();
322
323        match order.contingency_type() {
324            Some(ContingencyType::Oto) => {
325                let position_id = self
326                    .cache
327                    .borrow()
328                    .position_id(&order.client_order_id())
329                    .copied();
330                let client_id = self
331                    .cache
332                    .borrow()
333                    .client_id(&order.client_order_id())
334                    .copied();
335
336                let parent_filled_qty = match order.exec_spawn_id() {
337                    Some(spawn_id) => {
338                        if let Some(qty) = self
339                            .cache
340                            .borrow()
341                            .exec_spawn_total_filled_qty(&spawn_id, true)
342                        {
343                            qty
344                        } else {
345                            log::error!("Failed to get spawn filled quantity for {spawn_id}");
346                            return actions;
347                        }
348                    }
349                    None => order.filled_qty(),
350                };
351
352                let linked_orders = if let Some(orders) = order.linked_order_ids() {
353                    orders
354                } else {
355                    log::error!("No linked orders found for OTO order");
356                    return actions;
357                };
358
359                for client_order_id in linked_orders {
360                    let mut child_order = if let Some(order) = self
361                        .cache
362                        .borrow()
363                        .order(client_order_id)
364                        .map(|o| o.clone())
365                    {
366                        order
367                    } else {
368                        log::error!(
369                            "Cannot find OTO child order for client_order_id: {client_order_id}"
370                        );
371                        continue;
372                    };
373
374                    if !self.should_manage_order(&child_order) {
375                        continue;
376                    }
377
378                    if child_order.position_id().is_none() {
379                        child_order.set_position_id(position_id);
380                    }
381
382                    if parent_filled_qty != child_order.leaves_qty() {
383                        actions.extend(self.modify_order_quantity(&child_order, parent_filled_qty));
384                    }
385
386                    if !self
387                        .submit_order_commands
388                        .contains_key(&child_order.client_order_id())
389                    {
390                        match self.create_new_submit_order(
391                            &child_order,
392                            position_id,
393                            client_id,
394                            None,
395                        ) {
396                            Ok(new_actions) => actions.extend(new_actions),
397                            Err(e) => log::error!("Failed to create new submit order: {e}"),
398                        }
399                    }
400                }
401            }
402            Some(ContingencyType::Oco) => {
403                let linked_orders = if let Some(orders) = order.linked_order_ids() {
404                    orders
405                } else {
406                    log::error!("No linked orders found for OCO order");
407                    return actions;
408                };
409
410                for client_order_id in linked_orders {
411                    let contingent_order = match self
412                        .cache
413                        .borrow()
414                        .order(client_order_id)
415                        .map(|o| o.clone())
416                    {
417                        Some(contingent_order) => contingent_order,
418                        None => {
419                            log::error!(
420                                "Cannot find OCO contingent order for client_order_id: {client_order_id}"
421                            );
422                            continue;
423                        }
424                    };
425
426                    // Not being managed || Already completed
427                    if !self.should_manage_order(&contingent_order) || contingent_order.is_closed()
428                    {
429                        continue;
430                    }
431
432                    if contingent_order.client_order_id() != order.client_order_id() {
433                        actions.extend(self.cancel_order(&contingent_order));
434                    }
435                }
436            }
437            Some(ContingencyType::Ouo) => actions.extend(self.handle_contingencies(&order)),
438            _ => {}
439        }
440
441        actions
442    }
443
444    pub fn handle_contingencies(&mut self, order: &OrderAny) -> Vec<OrderManagerAction> {
445        let mut actions = Vec::new();
446        let (filled_qty, leaves_qty, is_spawn_active) =
447            if let Some(exec_spawn_id) = order.exec_spawn_id() {
448                if let (Some(filled), Some(leaves)) = (
449                    self.cache
450                        .borrow()
451                        .exec_spawn_total_filled_qty(&exec_spawn_id, true),
452                    self.cache
453                        .borrow()
454                        .exec_spawn_total_leaves_qty(&exec_spawn_id, true),
455                ) {
456                    (filled, leaves, leaves.raw > 0)
457                } else {
458                    log::error!("Failed to get spawn quantities for {exec_spawn_id}");
459                    return actions;
460                }
461            } else {
462                (order.filled_qty(), order.leaves_qty(), false)
463            };
464
465        let linked_orders = if let Some(orders) = order.linked_order_ids() {
466            orders
467        } else {
468            log::error!("No linked orders found");
469            return actions;
470        };
471
472        for client_order_id in linked_orders {
473            let contingent_order = if let Some(order) = self
474                .cache
475                .borrow()
476                .order(client_order_id)
477                .map(|o| o.clone())
478            {
479                order
480            } else {
481                log::error!("Cannot find contingent order for client_order_id: {client_order_id}");
482                continue;
483            };
484
485            if !self.should_manage_order(&contingent_order)
486                || client_order_id == &order.client_order_id()
487            {
488                continue;
489            }
490
491            if contingent_order.is_closed() {
492                self.submit_order_commands.remove(&order.client_order_id());
493                continue;
494            }
495
496            match order.contingency_type() {
497                Some(ContingencyType::Oto) => {
498                    if order.is_closed()
499                        && filled_qty.raw == 0
500                        && (order.exec_spawn_id().is_none() || !is_spawn_active)
501                    {
502                        actions.extend(self.cancel_order(&contingent_order));
503                    } else if filled_qty.raw > 0 && filled_qty != contingent_order.quantity() {
504                        actions.extend(self.modify_order_quantity(&contingent_order, filled_qty));
505                    }
506                }
507                Some(ContingencyType::Oco)
508                    if order.is_closed()
509                        && (order.exec_spawn_id().is_none() || !is_spawn_active) =>
510                {
511                    actions.extend(self.cancel_order(&contingent_order));
512                }
513                Some(ContingencyType::Ouo) => {
514                    if (leaves_qty.raw == 0 && order.exec_spawn_id().is_some())
515                        || (order.is_closed()
516                            && (order.exec_spawn_id().is_none() || !is_spawn_active))
517                    {
518                        actions.extend(self.cancel_order(&contingent_order));
519                    } else if leaves_qty != contingent_order.leaves_qty() {
520                        actions.extend(self.modify_order_quantity(&contingent_order, leaves_qty));
521                    }
522                }
523                _ => {}
524            }
525        }
526
527        actions
528    }
529
530    pub fn handle_contingencies_update(&mut self, order: &OrderAny) -> Vec<OrderManagerAction> {
531        let mut actions = Vec::new();
532        let quantity = match order.exec_spawn_id() {
533            Some(exec_spawn_id) => {
534                if let Some(qty) = self
535                    .cache
536                    .borrow()
537                    .exec_spawn_total_quantity(&exec_spawn_id, true)
538                {
539                    qty
540                } else {
541                    log::error!("Failed to get spawn total quantity for {exec_spawn_id}");
542                    return actions;
543                }
544            }
545            None => order.quantity(),
546        };
547
548        if quantity.raw == 0 {
549            return actions;
550        }
551
552        let linked_orders = if let Some(orders) = order.linked_order_ids() {
553            orders
554        } else {
555            log::error!("No linked orders found for contingent order");
556            return actions;
557        };
558
559        for client_order_id in linked_orders {
560            let contingent_order = match self
561                .cache
562                .borrow()
563                .order(client_order_id)
564                .map(|o| o.clone())
565            {
566                Some(contingent_order) => contingent_order,
567                None => {
568                    log::error!(
569                        "Cannot find OCO contingent order for client_order_id: {client_order_id}"
570                    );
571                    continue;
572                }
573            };
574
575            if !self.should_manage_order(&contingent_order)
576                || client_order_id == &order.client_order_id()
577                || contingent_order.is_closed()
578            {
579                continue;
580            }
581
582            if let Some(contingency_type) = order.contingency_type()
583                && matches!(
584                    contingency_type,
585                    ContingencyType::Oto | ContingencyType::Ouo
586                )
587                && quantity != contingent_order.quantity()
588            {
589                actions.extend(self.modify_order_quantity(&contingent_order, quantity));
590            }
591        }
592
593        actions
594    }
595}
596
597fn initialized_action(order: &OrderAny) -> OrderManagerAction {
598    let event = OrderEventAny::Initialized(order.init_event().clone());
599    OrderManagerAction::PublishInitialized(event)
600}
601
602#[cfg(test)]
603mod tests {
604    use std::{cell::RefCell, rc::Rc};
605
606    use nautilus_common::{cache::Cache, clock::TestClock};
607    use nautilus_core::{UUID4, UnixNanos};
608    use nautilus_model::{
609        enums::{ContingencyType, OrderSide, OrderType, TriggerType},
610        events::order::spec::{OrderAcceptedSpec, OrderSubmittedSpec},
611        identifiers::{
612            AccountId, ClientOrderId, ExecAlgorithmId, InstrumentId, StrategyId, TraderId,
613            VenueOrderId,
614        },
615        instruments::{Instrument, InstrumentAny, stubs::audusd_sim},
616        orders::{Order, OrderTestBuilder, stubs::TestOrderEventStubs},
617        types::{Price, Quantity},
618    };
619    use rstest::rstest;
620
621    use super::*;
622
623    /// Verifies unhandled order events are no-ops and don't panic.
624    /// Previously, unhandled events would hit a todo!() panic.
625    #[rstest]
626    fn test_handle_event_unhandled_events_are_noop() {
627        let submitted = OrderEventAny::Submitted(
628            OrderSubmittedSpec::builder()
629                .trader_id(TraderId::from("TRADER-001"))
630                .strategy_id(StrategyId::from("STRATEGY-001"))
631                .instrument_id(InstrumentId::from("BTC-USDT.OKX"))
632                .client_order_id(ClientOrderId::from("O-001"))
633                .account_id(AccountId::from("ACCOUNT-001"))
634                .build(),
635        );
636        let accepted = OrderEventAny::Accepted(
637            OrderAcceptedSpec::builder()
638                .trader_id(TraderId::from("TRADER-001"))
639                .strategy_id(StrategyId::from("STRATEGY-001"))
640                .instrument_id(InstrumentId::from("BTC-USDT.OKX"))
641                .client_order_id(ClientOrderId::from("O-001"))
642                .venue_order_id(VenueOrderId::from("V-001"))
643                .account_id(AccountId::from("ACCOUNT-001"))
644                .build(),
645        );
646
647        match submitted {
648            OrderEventAny::Rejected(_) => panic!("Should not match"),
649            OrderEventAny::Canceled(_) => panic!("Should not match"),
650            OrderEventAny::Expired(_) => panic!("Should not match"),
651            OrderEventAny::Updated(_) => panic!("Should not match"),
652            OrderEventAny::Filled(_) => panic!("Should not match"),
653            _ => {}
654        }
655
656        match accepted {
657            OrderEventAny::Rejected(_) => panic!("Should not match"),
658            OrderEventAny::Canceled(_) => panic!("Should not match"),
659            OrderEventAny::Expired(_) => panic!("Should not match"),
660            OrderEventAny::Updated(_) => panic!("Should not match"),
661            OrderEventAny::Filled(_) => panic!("Should not match"),
662            _ => {}
663        }
664    }
665
666    fn create_test_components() -> (Rc<RefCell<dyn Clock>>, Rc<RefCell<Cache>>) {
667        let clock: Rc<RefCell<dyn Clock>> = Rc::new(RefCell::new(TestClock::new()));
668        let cache = Rc::new(RefCell::new(Cache::new(None, None)));
669        (clock, cache)
670    }
671
672    fn create_test_stop_order() -> OrderAny {
673        let instrument = audusd_sim();
674        OrderTestBuilder::new(OrderType::StopMarket)
675            .instrument_id(instrument.id())
676            .side(OrderSide::Buy)
677            .trigger_price(Price::from("1.00050"))
678            .quantity(Quantity::from(100_000))
679            .emulation_trigger(TriggerType::BidAsk)
680            .build()
681    }
682
683    // Creates a `SubmitOrder` command suitable for seeding `submit_order_commands`
684    // so that whether `cancel_order` removed the entry can be observed.
685    fn make_submit_command(order: &OrderAny) -> SubmitOrder {
686        SubmitOrder::new(
687            order.trader_id(),
688            None,
689            order.strategy_id(),
690            order.instrument_id(),
691            order.client_order_id(),
692            order.init_event().clone(),
693            None,
694            None,
695            None,
696            UUID4::new(),
697            UnixNanos::default(),
698            None, // correlation_id
699        )
700    }
701
702    #[rstest]
703    fn test_create_new_submit_order_returns_emulator_submit_action() {
704        let (clock, cache) = create_test_components();
705        let mut manager = OrderManager::new(clock, cache, true);
706        let order = create_test_stop_order();
707
708        let actions = manager
709            .create_new_submit_order(&order, None, None, None)
710            .unwrap();
711
712        assert_eq!(actions.len(), 2);
713        assert!(matches!(
714            &actions[0],
715            OrderManagerAction::PublishInitialized(OrderEventAny::Initialized(event))
716                if event.client_order_id == order.client_order_id()
717        ));
718        assert!(matches!(
719            &actions[1],
720            OrderManagerAction::SubmitToEmulator(command)
721                if command.client_order_id == order.client_order_id()
722        ));
723        assert!(
724            manager
725                .submit_order_commands
726                .contains_key(&order.client_order_id())
727        );
728    }
729
730    #[rstest]
731    fn test_create_new_submit_order_returns_risk_submit_action() {
732        let (clock, cache) = create_test_components();
733        let mut manager = OrderManager::new(clock, cache, true);
734        let order = OrderTestBuilder::new(OrderType::Limit)
735            .instrument_id(audusd_sim().id())
736            .side(OrderSide::Buy)
737            .price(Price::from("1.00000"))
738            .quantity(Quantity::from(100_000))
739            .emulation_trigger(TriggerType::NoTrigger)
740            .build();
741
742        let actions = manager
743            .create_new_submit_order(&order, None, None, None)
744            .unwrap();
745
746        assert_eq!(actions.len(), 2);
747        assert!(matches!(
748            &actions[0],
749            OrderManagerAction::PublishInitialized(OrderEventAny::Initialized(event))
750                if event.client_order_id == order.client_order_id()
751        ));
752        assert!(matches!(
753            &actions[1],
754            OrderManagerAction::SubmitToRisk(command)
755                if command.client_order_id == order.client_order_id()
756        ));
757    }
758
759    #[rstest]
760    fn test_create_new_submit_order_returns_risk_action_for_none_trigger() {
761        let (clock, cache) = create_test_components();
762        let mut manager = OrderManager::new(clock, cache, true);
763        let order = OrderTestBuilder::new(OrderType::Market)
764            .instrument_id(audusd_sim().id())
765            .side(OrderSide::Buy)
766            .quantity(Quantity::from(100_000))
767            .build();
768
769        let actions = manager
770            .create_new_submit_order(&order, None, None, None)
771            .unwrap();
772
773        assert_eq!(actions.len(), 2);
774        assert!(order.emulation_trigger().is_none());
775        assert!(matches!(
776            &actions[1],
777            OrderManagerAction::SubmitToRisk(command)
778                if command.client_order_id == order.client_order_id()
779        ));
780    }
781
782    #[rstest]
783    fn test_create_new_submit_order_returns_algorithm_submit_action() {
784        let (clock, cache) = create_test_components();
785        let mut manager = OrderManager::new(clock, cache, true);
786        let exec_algorithm_id = ExecAlgorithmId::from("ALG-001");
787        let client_order_id = ClientOrderId::from("O-001");
788        let order = OrderTestBuilder::new(OrderType::Limit)
789            .client_order_id(client_order_id)
790            .instrument_id(audusd_sim().id())
791            .side(OrderSide::Buy)
792            .price(Price::from("1.00000"))
793            .quantity(Quantity::from(100_000))
794            .emulation_trigger(TriggerType::NoTrigger)
795            .exec_algorithm_id(exec_algorithm_id)
796            .exec_spawn_id(client_order_id)
797            .build();
798
799        let actions = manager
800            .create_new_submit_order(&order, None, None, None)
801            .unwrap();
802
803        assert_eq!(actions.len(), 2);
804        assert!(matches!(
805            &actions[1],
806            OrderManagerAction::SubmitToAlgorithm {
807                command,
808                exec_algorithm_id: action_exec_algorithm_id,
809            } if command.client_order_id == order.client_order_id()
810                && *action_exec_algorithm_id == exec_algorithm_id
811        ));
812    }
813
814    #[rstest]
815    fn test_create_new_submit_order_does_not_republish_initialized_for_existing_order() {
816        let (clock, cache) = create_test_components();
817        let mut manager = OrderManager::new(clock, cache.clone(), true);
818        let order = create_test_stop_order();
819        cache
820            .borrow_mut()
821            .add_order(order.clone(), None, None, true)
822            .unwrap();
823
824        let actions = manager
825            .create_new_submit_order(&order, None, None, None)
826            .unwrap();
827
828        assert_eq!(actions.len(), 1);
829        assert!(matches!(
830            &actions[0],
831            OrderManagerAction::SubmitToEmulator(command)
832                if command.client_order_id == order.client_order_id()
833        ));
834    }
835
836    #[rstest]
837    fn test_cancel_order_returns_cancel_local_action() {
838        let (clock, cache) = create_test_components();
839        let mut manager = OrderManager::new(clock, cache.clone(), true);
840        let order = create_test_stop_order();
841        cache
842            .borrow_mut()
843            .add_order(order.clone(), None, None, false)
844            .unwrap();
845        manager
846            .submit_order_commands
847            .insert(order.client_order_id(), make_submit_command(&order));
848
849        let actions = manager.cancel_order(&order);
850
851        assert!(matches!(
852            actions.as_slice(),
853            [OrderManagerAction::CancelLocal(action_order)]
854                if action_order.client_order_id() == order.client_order_id()
855        ));
856        assert!(
857            !manager
858                .submit_order_commands
859                .contains_key(&order.client_order_id()),
860            "expected cancel action path to remove the submit command",
861        );
862    }
863
864    #[rstest]
865    fn test_modify_order_quantity_returns_modify_local_action() {
866        let (clock, cache) = create_test_components();
867        let mut manager = OrderManager::new(clock, cache, true);
868        let order = create_test_stop_order();
869        let new_quantity = Quantity::from(50_000);
870
871        let actions = manager.modify_order_quantity(&order, new_quantity);
872
873        assert!(matches!(
874            actions.as_slice(),
875            [OrderManagerAction::ModifyLocalQuantity { order: action_order, quantity }]
876                if action_order.client_order_id() == order.client_order_id()
877                    && *quantity == new_quantity
878        ));
879    }
880
881    #[rstest]
882    fn test_handle_event_unhandled_events_return_no_actions() {
883        let (clock, cache) = create_test_components();
884        let mut manager = OrderManager::new(clock, cache, true);
885        let order = create_test_stop_order();
886        let event = OrderEventAny::Submitted(
887            OrderSubmittedSpec::builder()
888                .trader_id(order.trader_id())
889                .strategy_id(order.strategy_id())
890                .instrument_id(order.instrument_id())
891                .client_order_id(order.client_order_id())
892                .account_id(AccountId::from("ACCOUNT-001"))
893                .build(),
894        );
895
896        let actions = manager.handle_event(&event);
897
898        assert!(actions.is_empty());
899    }
900
901    #[rstest]
902    fn test_handle_order_filled_skips_missing_oco_contingent_order() {
903        let (clock, cache) = create_test_components();
904        let mut manager = OrderManager::new(clock, cache.clone(), true);
905        let instrument = InstrumentAny::CurrencyPair(audusd_sim());
906        let missing_client_order_id = ClientOrderId::from("O-MISSING");
907        let valid_client_order_id = ClientOrderId::from("O-CHILD");
908        let order = OrderTestBuilder::new(OrderType::Limit)
909            .instrument_id(instrument.id())
910            .client_order_id(ClientOrderId::from("O-PARENT"))
911            .side(OrderSide::Buy)
912            .price(Price::from("1.00000"))
913            .quantity(Quantity::from(100_000))
914            .contingency_type(ContingencyType::Oco)
915            .linked_order_ids(vec![missing_client_order_id, valid_client_order_id])
916            .build();
917        let child_order = OrderTestBuilder::new(OrderType::Limit)
918            .instrument_id(instrument.id())
919            .client_order_id(valid_client_order_id)
920            .side(OrderSide::Buy)
921            .price(Price::from("1.00000"))
922            .quantity(Quantity::from(100_000))
923            .build();
924        cache
925            .borrow_mut()
926            .add_order(order.clone(), None, None, false)
927            .unwrap();
928        cache
929            .borrow_mut()
930            .add_order(child_order.clone(), None, None, false)
931            .unwrap();
932        manager
933            .submit_order_commands
934            .insert(valid_client_order_id, make_submit_command(&child_order));
935        let filled = match TestOrderEventStubs::filled(
936            &order,
937            &instrument,
938            None,
939            None,
940            None,
941            None,
942            None,
943            None,
944            None,
945            Some(AccountId::from("SIM-001")),
946        ) {
947            OrderEventAny::Filled(event) => event,
948            event => panic!("expected OrderFilled, was {event:?}"),
949        };
950
951        let actions = manager.handle_order_filled(&filled);
952
953        assert!(matches!(
954            actions.as_slice(),
955            [OrderManagerAction::CancelLocal(action_order)]
956                if action_order.client_order_id() == valid_client_order_id
957        ));
958        assert!(
959            !manager
960                .submit_order_commands
961                .contains_key(&valid_client_order_id)
962        );
963    }
964
965    #[rstest]
966    fn test_handle_event_inactive_manager_returns_no_local_actions() {
967        let (clock, cache) = create_test_components();
968        let mut manager = OrderManager::new(clock, cache.clone(), false);
969        let instrument = InstrumentAny::CurrencyPair(audusd_sim());
970        let parent_id = ClientOrderId::from("O-PARENT");
971        let child_id = ClientOrderId::from("O-CHILD");
972        let order = OrderTestBuilder::new(OrderType::Limit)
973            .instrument_id(instrument.id())
974            .client_order_id(parent_id)
975            .side(OrderSide::Buy)
976            .price(Price::from("1.00000"))
977            .quantity(Quantity::from(100_000))
978            .contingency_type(ContingencyType::Oco)
979            .linked_order_ids(vec![parent_id, child_id])
980            .build();
981        let child_order = OrderTestBuilder::new(OrderType::Limit)
982            .instrument_id(instrument.id())
983            .client_order_id(child_id)
984            .side(OrderSide::Buy)
985            .price(Price::from("1.00000"))
986            .quantity(Quantity::from(100_000))
987            .build();
988        cache
989            .borrow_mut()
990            .add_order(order.clone(), None, None, false)
991            .unwrap();
992        cache
993            .borrow_mut()
994            .add_order(child_order.clone(), None, None, false)
995            .unwrap();
996        manager
997            .submit_order_commands
998            .insert(child_id, make_submit_command(&child_order));
999        let event = TestOrderEventStubs::filled(
1000            &order,
1001            &instrument,
1002            None,
1003            None,
1004            None,
1005            None,
1006            None,
1007            None,
1008            None,
1009            Some(AccountId::from("SIM-001")),
1010        );
1011
1012        let actions = manager.handle_event(&event);
1013
1014        assert!(actions.is_empty());
1015        assert!(
1016            manager.submit_order_commands.contains_key(&child_id),
1017            "inactive manager must not start local contingency actions",
1018        );
1019    }
1020
1021    #[rstest]
1022    fn test_handle_order_filled_skips_missing_oto_child_order() {
1023        let (clock, cache) = create_test_components();
1024        let mut manager = OrderManager::new(clock, cache.clone(), true);
1025        let instrument = InstrumentAny::CurrencyPair(audusd_sim());
1026        let missing_client_order_id = ClientOrderId::from("O-MISSING");
1027        let valid_client_order_id = ClientOrderId::from("O-CHILD");
1028        let order = OrderTestBuilder::new(OrderType::Limit)
1029            .instrument_id(instrument.id())
1030            .client_order_id(ClientOrderId::from("O-PARENT"))
1031            .side(OrderSide::Buy)
1032            .price(Price::from("1.00000"))
1033            .quantity(Quantity::from(100_000))
1034            .contingency_type(ContingencyType::Oto)
1035            .linked_order_ids(vec![missing_client_order_id, valid_client_order_id])
1036            .build();
1037        let child_order = OrderTestBuilder::new(OrderType::Limit)
1038            .instrument_id(instrument.id())
1039            .client_order_id(valid_client_order_id)
1040            .side(OrderSide::Buy)
1041            .price(Price::from("1.00000"))
1042            .quantity(Quantity::from(100_000))
1043            .emulation_trigger(TriggerType::NoTrigger)
1044            .build();
1045        cache
1046            .borrow_mut()
1047            .add_order(order.clone(), None, None, false)
1048            .unwrap();
1049        cache
1050            .borrow_mut()
1051            .add_order(child_order, None, None, false)
1052            .unwrap();
1053        let filled = match TestOrderEventStubs::filled(
1054            &order,
1055            &instrument,
1056            None,
1057            None,
1058            None,
1059            None,
1060            None,
1061            None,
1062            None,
1063            Some(AccountId::from("SIM-001")),
1064        ) {
1065            OrderEventAny::Filled(event) => event,
1066            event => panic!("expected OrderFilled, was {event:?}"),
1067        };
1068
1069        let actions = manager.handle_order_filled(&filled);
1070
1071        assert_eq!(actions.len(), 2);
1072        assert!(matches!(
1073            &actions[0],
1074            OrderManagerAction::ModifyLocalQuantity { order, quantity }
1075                if order.client_order_id() == valid_client_order_id
1076                    && *quantity == Quantity::zero(0)
1077        ));
1078        assert!(matches!(
1079            &actions[1],
1080            OrderManagerAction::SubmitToRisk(command)
1081                if command.client_order_id == valid_client_order_id
1082        ));
1083        assert!(
1084            manager
1085                .submit_order_commands
1086                .contains_key(&valid_client_order_id)
1087        );
1088    }
1089
1090    #[rstest]
1091    fn test_handle_contingencies_skips_missing_linked_order() {
1092        let (clock, cache) = create_test_components();
1093        let mut manager = OrderManager::new(clock, cache, true);
1094        let instrument = audusd_sim();
1095        let order = OrderTestBuilder::new(OrderType::Limit)
1096            .instrument_id(instrument.id())
1097            .client_order_id(ClientOrderId::from("O-PARENT"))
1098            .side(OrderSide::Buy)
1099            .price(Price::from("1.00000"))
1100            .quantity(Quantity::from(100_000))
1101            .contingency_type(ContingencyType::Oco)
1102            .linked_order_ids(vec![ClientOrderId::from("O-MISSING")])
1103            .build();
1104
1105        let actions = manager.handle_contingencies(&order);
1106
1107        assert!(actions.is_empty());
1108        assert!(manager.submit_order_commands.is_empty());
1109    }
1110
1111    #[rstest]
1112    fn test_handle_contingencies_update_skips_missing_linked_order() {
1113        let (clock, cache) = create_test_components();
1114        let mut manager = OrderManager::new(clock, cache, true);
1115        let instrument = audusd_sim();
1116        let order = OrderTestBuilder::new(OrderType::Limit)
1117            .instrument_id(instrument.id())
1118            .client_order_id(ClientOrderId::from("O-PARENT"))
1119            .side(OrderSide::Buy)
1120            .price(Price::from("1.00000"))
1121            .quantity(Quantity::from(100_000))
1122            .contingency_type(ContingencyType::Oco)
1123            .linked_order_ids(vec![ClientOrderId::from("O-MISSING")])
1124            .build();
1125
1126        let actions = manager.handle_contingencies_update(&order);
1127
1128        assert!(actions.is_empty());
1129        assert!(manager.submit_order_commands.is_empty());
1130    }
1131
1132    #[rstest]
1133    fn test_cancel_order_skips_when_pending_cancel_local() {
1134        let (clock, cache) = create_test_components();
1135        let mut manager = OrderManager::new(clock, cache.clone(), true);
1136        let order = create_test_stop_order();
1137        cache
1138            .borrow_mut()
1139            .add_order(order.clone(), None, None, false)
1140            .unwrap();
1141        cache.borrow_mut().update_order_pending_cancel_local(&order);
1142        manager
1143            .submit_order_commands
1144            .insert(order.client_order_id(), make_submit_command(&order));
1145
1146        manager.cancel_order(&order);
1147
1148        assert!(
1149            manager
1150                .submit_order_commands
1151                .contains_key(&order.client_order_id()),
1152            "pending-cancel-local gate should short-circuit before removing the submit command",
1153        );
1154    }
1155
1156    #[rstest]
1157    fn test_cancel_order_skips_when_passed_order_is_closed() {
1158        // The caller has applied a closing event to its local clone but has
1159        // not yet called `cache.update_order`, so the cache index still
1160        // reports open. The gate must short-circuit on the local state.
1161        let (clock, cache) = create_test_components();
1162        let mut manager = OrderManager::new(clock, cache.clone(), true);
1163
1164        let mut order = OrderTestBuilder::new(OrderType::StopMarket)
1165            .instrument_id(audusd_sim().id())
1166            .side(OrderSide::Buy)
1167            .trigger_price(Price::from("1.00050"))
1168            .quantity(Quantity::from(100_000))
1169            .emulation_trigger(TriggerType::BidAsk)
1170            .submit(true)
1171            .build();
1172
1173        cache
1174            .borrow_mut()
1175            .add_order(order.clone(), None, None, false)
1176            .unwrap();
1177
1178        let canceled_event =
1179            TestOrderEventStubs::canceled(&order, AccountId::from("ACCOUNT-001"), None);
1180        order.apply(canceled_event).unwrap();
1181
1182        assert!(order.is_closed());
1183        assert!(!cache.borrow().is_order_closed(&order.client_order_id()));
1184
1185        manager
1186            .submit_order_commands
1187            .insert(order.client_order_id(), make_submit_command(&order));
1188
1189        manager.cancel_order(&order);
1190
1191        assert!(
1192            manager
1193                .submit_order_commands
1194                .contains_key(&order.client_order_id()),
1195            "closed-order gate should short-circuit on the local state when the cache index is stale",
1196        );
1197    }
1198
1199    #[rstest]
1200    fn test_cancel_order_skips_when_cache_index_marks_closed() {
1201        // The passed `OrderAny` is intentionally a stale (Submitted) clone so
1202        // this test would fail if `cancel_order` checked `order.is_closed()`
1203        // on the argument instead of `cache.is_order_closed(&id)`.
1204        let (clock, cache) = create_test_components();
1205        let mut manager = OrderManager::new(clock, cache.clone(), true);
1206
1207        let mut order = OrderTestBuilder::new(OrderType::StopMarket)
1208            .instrument_id(audusd_sim().id())
1209            .side(OrderSide::Buy)
1210            .trigger_price(Price::from("1.00050"))
1211            .quantity(Quantity::from(100_000))
1212            .emulation_trigger(TriggerType::BidAsk)
1213            .submit(true)
1214            .build();
1215
1216        cache
1217            .borrow_mut()
1218            .add_order(order.clone(), None, None, false)
1219            .unwrap();
1220
1221        let stale_order = order.clone();
1222
1223        let canceled_event =
1224            TestOrderEventStubs::canceled(&order, AccountId::from("ACCOUNT-001"), None);
1225        order = cache.borrow_mut().update_order(&canceled_event).unwrap();
1226
1227        assert!(cache.borrow().is_order_closed(&order.client_order_id()));
1228
1229        manager.submit_order_commands.insert(
1230            stale_order.client_order_id(),
1231            make_submit_command(&stale_order),
1232        );
1233
1234        manager.cancel_order(&stale_order);
1235
1236        assert!(
1237            manager
1238                .submit_order_commands
1239                .contains_key(&stale_order.client_order_id()),
1240            "closed-order gate should short-circuit even when the passed reference is stale",
1241        );
1242    }
1243
1244    #[rstest]
1245    fn test_handle_contingencies_update_syncs_quantity_for_ouo_sibling() {
1246        let (clock, cache) = create_test_components();
1247        let mut manager = OrderManager::new(clock, cache.clone(), true);
1248        let instrument = audusd_sim();
1249        let child = OrderTestBuilder::new(OrderType::Limit)
1250            .instrument_id(instrument.id())
1251            .client_order_id(ClientOrderId::from("O-CHILD"))
1252            .side(OrderSide::Sell)
1253            .price(Price::from("1.00100"))
1254            .quantity(Quantity::from(50_000))
1255            .build();
1256        cache
1257            .borrow_mut()
1258            .add_order(child.clone(), None, None, false)
1259            .unwrap();
1260        let parent = OrderTestBuilder::new(OrderType::Limit)
1261            .instrument_id(instrument.id())
1262            .client_order_id(ClientOrderId::from("O-PARENT"))
1263            .side(OrderSide::Buy)
1264            .price(Price::from("1.00000"))
1265            .quantity(Quantity::from(100_000))
1266            .contingency_type(ContingencyType::Ouo)
1267            .linked_order_ids(vec![child.client_order_id()])
1268            .build();
1269
1270        let actions = manager.handle_contingencies_update(&parent);
1271
1272        assert_eq!(actions.len(), 1);
1273        assert!(matches!(
1274            &actions[0],
1275            OrderManagerAction::ModifyLocalQuantity { order, quantity }
1276                if order.client_order_id() == child.client_order_id()
1277                    && *quantity == Quantity::from(100_000)
1278        ));
1279    }
1280
1281    #[rstest]
1282    fn test_handle_contingencies_update_does_not_sync_quantity_for_oco_sibling() {
1283        let (clock, cache) = create_test_components();
1284        let mut manager = OrderManager::new(clock, cache.clone(), true);
1285        let instrument = audusd_sim();
1286        let child = OrderTestBuilder::new(OrderType::Limit)
1287            .instrument_id(instrument.id())
1288            .client_order_id(ClientOrderId::from("O-CHILD"))
1289            .side(OrderSide::Sell)
1290            .price(Price::from("1.00100"))
1291            .quantity(Quantity::from(50_000))
1292            .build();
1293        cache
1294            .borrow_mut()
1295            .add_order(child.clone(), None, None, false)
1296            .unwrap();
1297        let parent = OrderTestBuilder::new(OrderType::Limit)
1298            .instrument_id(instrument.id())
1299            .client_order_id(ClientOrderId::from("O-PARENT"))
1300            .side(OrderSide::Buy)
1301            .price(Price::from("1.00000"))
1302            .quantity(Quantity::from(100_000))
1303            .contingency_type(ContingencyType::Oco)
1304            .linked_order_ids(vec![child.client_order_id()])
1305            .build();
1306
1307        let actions = manager.handle_contingencies_update(&parent);
1308
1309        assert!(actions.is_empty());
1310    }
1311}