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.contingency_type() != Some(ContingencyType::NoContingency) {
213                return self.handle_contingencies(&order);
214            }
215        } else {
216            log::error!(
217                "Cannot handle `OrderRejected`: order for client_order_id: {} not found, {}",
218                rejected.client_order_id,
219                rejected
220            );
221        }
222
223        Vec::new()
224    }
225
226    pub fn handle_order_canceled(&mut self, canceled: OrderCanceled) -> Vec<OrderManagerAction> {
227        let cloned_order = self
228            .cache
229            .borrow()
230            .order(&canceled.client_order_id)
231            .map(|o| o.clone());
232
233        if let Some(order) = cloned_order {
234            if order.contingency_type() != Some(ContingencyType::NoContingency) {
235                return self.handle_contingencies(&order);
236            }
237        } else {
238            log::error!(
239                "Cannot handle `OrderCanceled`: order for client_order_id: {} not found, {}",
240                canceled.client_order_id,
241                canceled
242            );
243        }
244
245        Vec::new()
246    }
247
248    pub fn handle_order_expired(&mut self, expired: OrderExpired) -> Vec<OrderManagerAction> {
249        let cloned_order = self
250            .cache
251            .borrow()
252            .order(&expired.client_order_id)
253            .map(|o| o.clone());
254        if let Some(order) = cloned_order {
255            if order.contingency_type() != Some(ContingencyType::NoContingency) {
256                return self.handle_contingencies(&order);
257            }
258        } else {
259            log::error!(
260                "Cannot handle `OrderExpired`: order for client_order_id: {} not found, {}",
261                expired.client_order_id,
262                expired
263            );
264        }
265
266        Vec::new()
267    }
268
269    pub fn handle_order_updated(&mut self, updated: OrderUpdated) -> Vec<OrderManagerAction> {
270        let cloned_order = self
271            .cache
272            .borrow()
273            .order(&updated.client_order_id)
274            .map(|o| o.clone());
275        if let Some(order) = cloned_order {
276            if order.contingency_type() != Some(ContingencyType::NoContingency) {
277                return self.handle_contingencies_update(&order);
278            }
279        } else {
280            log::error!(
281                "Cannot handle `OrderUpdated`: order for client_order_id: {} not found, {}",
282                updated.client_order_id,
283                updated
284            );
285        }
286
287        Vec::new()
288    }
289
290    pub fn handle_order_filled(&mut self, filled: OrderFilled) -> Vec<OrderManagerAction> {
291        let order = if let Some(order) = self
292            .cache
293            .borrow()
294            .order(&filled.client_order_id)
295            .map(|o| o.clone())
296        {
297            order
298        } else {
299            log::error!(
300                "Cannot handle `OrderFilled`: order for client_order_id: {} not found, {}",
301                filled.client_order_id,
302                filled
303            );
304            return Vec::new();
305        };
306
307        let mut actions = Vec::new();
308
309        match order.contingency_type() {
310            Some(ContingencyType::Oto) => {
311                let position_id = self
312                    .cache
313                    .borrow()
314                    .position_id(&order.client_order_id())
315                    .copied();
316                let client_id = self
317                    .cache
318                    .borrow()
319                    .client_id(&order.client_order_id())
320                    .copied();
321
322                let parent_filled_qty = match order.exec_spawn_id() {
323                    Some(spawn_id) => {
324                        if let Some(qty) = self
325                            .cache
326                            .borrow()
327                            .exec_spawn_total_filled_qty(&spawn_id, true)
328                        {
329                            qty
330                        } else {
331                            log::error!("Failed to get spawn filled quantity for {spawn_id}");
332                            return actions;
333                        }
334                    }
335                    None => order.filled_qty(),
336                };
337
338                let linked_orders = if let Some(orders) = order.linked_order_ids() {
339                    orders
340                } else {
341                    log::error!("No linked orders found for OTO order");
342                    return actions;
343                };
344
345                for client_order_id in linked_orders {
346                    let mut child_order = if let Some(order) = self
347                        .cache
348                        .borrow()
349                        .order(client_order_id)
350                        .map(|o| o.clone())
351                    {
352                        order
353                    } else {
354                        log::error!(
355                            "Cannot find OTO child order for client_order_id: {client_order_id}"
356                        );
357                        continue;
358                    };
359
360                    if !self.should_manage_order(&child_order) {
361                        continue;
362                    }
363
364                    if child_order.position_id().is_none() {
365                        child_order.set_position_id(position_id);
366                    }
367
368                    if parent_filled_qty != child_order.leaves_qty() {
369                        actions.extend(self.modify_order_quantity(&child_order, parent_filled_qty));
370                    }
371
372                    if !self
373                        .submit_order_commands
374                        .contains_key(&child_order.client_order_id())
375                    {
376                        match self.create_new_submit_order(
377                            &child_order,
378                            position_id,
379                            client_id,
380                            None,
381                        ) {
382                            Ok(new_actions) => actions.extend(new_actions),
383                            Err(e) => log::error!("Failed to create new submit order: {e}"),
384                        }
385                    }
386                }
387            }
388            Some(ContingencyType::Oco) => {
389                let linked_orders = if let Some(orders) = order.linked_order_ids() {
390                    orders
391                } else {
392                    log::error!("No linked orders found for OCO order");
393                    return actions;
394                };
395
396                for client_order_id in linked_orders {
397                    let contingent_order = match self
398                        .cache
399                        .borrow()
400                        .order(client_order_id)
401                        .map(|o| o.clone())
402                    {
403                        Some(contingent_order) => contingent_order,
404                        None => {
405                            log::error!(
406                                "Cannot find OCO contingent order for client_order_id: {client_order_id}"
407                            );
408                            continue;
409                        }
410                    };
411
412                    // Not being managed || Already completed
413                    if !self.should_manage_order(&contingent_order) || contingent_order.is_closed()
414                    {
415                        continue;
416                    }
417
418                    if contingent_order.client_order_id() != order.client_order_id() {
419                        actions.extend(self.cancel_order(&contingent_order));
420                    }
421                }
422            }
423            Some(ContingencyType::Ouo) => actions.extend(self.handle_contingencies(&order)),
424            _ => {}
425        }
426
427        actions
428    }
429
430    pub fn handle_contingencies(&mut self, order: &OrderAny) -> Vec<OrderManagerAction> {
431        let mut actions = Vec::new();
432        let (filled_qty, leaves_qty, is_spawn_active) =
433            if let Some(exec_spawn_id) = order.exec_spawn_id() {
434                if let (Some(filled), Some(leaves)) = (
435                    self.cache
436                        .borrow()
437                        .exec_spawn_total_filled_qty(&exec_spawn_id, true),
438                    self.cache
439                        .borrow()
440                        .exec_spawn_total_leaves_qty(&exec_spawn_id, true),
441                ) {
442                    (filled, leaves, leaves.raw > 0)
443                } else {
444                    log::error!("Failed to get spawn quantities for {exec_spawn_id}");
445                    return actions;
446                }
447            } else {
448                (order.filled_qty(), order.leaves_qty(), false)
449            };
450
451        let linked_orders = if let Some(orders) = order.linked_order_ids() {
452            orders
453        } else {
454            log::error!("No linked orders found");
455            return actions;
456        };
457
458        for client_order_id in linked_orders {
459            let contingent_order = if let Some(order) = self
460                .cache
461                .borrow()
462                .order(client_order_id)
463                .map(|o| o.clone())
464            {
465                order
466            } else {
467                log::error!("Cannot find contingent order for client_order_id: {client_order_id}");
468                continue;
469            };
470
471            if !self.should_manage_order(&contingent_order)
472                || client_order_id == &order.client_order_id()
473            {
474                continue;
475            }
476
477            if contingent_order.is_closed() {
478                self.submit_order_commands.remove(&order.client_order_id());
479                continue;
480            }
481
482            match order.contingency_type() {
483                Some(ContingencyType::Oto) => {
484                    if order.is_closed()
485                        && filled_qty.raw == 0
486                        && (order.exec_spawn_id().is_none() || !is_spawn_active)
487                    {
488                        actions.extend(self.cancel_order(&contingent_order));
489                    } else if filled_qty.raw > 0 && filled_qty != contingent_order.quantity() {
490                        actions.extend(self.modify_order_quantity(&contingent_order, filled_qty));
491                    }
492                }
493                Some(ContingencyType::Oco)
494                    if order.is_closed()
495                        && (order.exec_spawn_id().is_none() || !is_spawn_active) =>
496                {
497                    actions.extend(self.cancel_order(&contingent_order));
498                }
499                Some(ContingencyType::Ouo) => {
500                    if (leaves_qty.raw == 0 && order.exec_spawn_id().is_some())
501                        || (order.is_closed()
502                            && (order.exec_spawn_id().is_none() || !is_spawn_active))
503                    {
504                        actions.extend(self.cancel_order(&contingent_order));
505                    } else if leaves_qty != contingent_order.leaves_qty() {
506                        actions.extend(self.modify_order_quantity(&contingent_order, leaves_qty));
507                    }
508                }
509                _ => {}
510            }
511        }
512
513        actions
514    }
515
516    pub fn handle_contingencies_update(&mut self, order: &OrderAny) -> Vec<OrderManagerAction> {
517        let mut actions = Vec::new();
518        let quantity = match order.exec_spawn_id() {
519            Some(exec_spawn_id) => {
520                if let Some(qty) = self
521                    .cache
522                    .borrow()
523                    .exec_spawn_total_quantity(&exec_spawn_id, true)
524                {
525                    qty
526                } else {
527                    log::error!("Failed to get spawn total quantity for {exec_spawn_id}");
528                    return actions;
529                }
530            }
531            None => order.quantity(),
532        };
533
534        if quantity.raw == 0 {
535            return actions;
536        }
537
538        let linked_orders = if let Some(orders) = order.linked_order_ids() {
539            orders
540        } else {
541            log::error!("No linked orders found for contingent order");
542            return actions;
543        };
544
545        for client_order_id in linked_orders {
546            let contingent_order = match self
547                .cache
548                .borrow()
549                .order(client_order_id)
550                .map(|o| o.clone())
551            {
552                Some(contingent_order) => contingent_order,
553                None => {
554                    log::error!(
555                        "Cannot find OCO contingent order for client_order_id: {client_order_id}"
556                    );
557                    continue;
558                }
559            };
560
561            if !self.should_manage_order(&contingent_order)
562                || client_order_id == &order.client_order_id()
563                || contingent_order.is_closed()
564            {
565                continue;
566            }
567
568            if let Some(contingency_type) = order.contingency_type()
569                && matches!(
570                    contingency_type,
571                    ContingencyType::Oto | ContingencyType::Oco
572                )
573                && quantity != contingent_order.quantity()
574            {
575                actions.extend(self.modify_order_quantity(&contingent_order, quantity));
576            }
577        }
578
579        actions
580    }
581}
582
583fn initialized_action(order: &OrderAny) -> OrderManagerAction {
584    let event = OrderEventAny::Initialized(order.init_event().clone());
585    OrderManagerAction::PublishInitialized(event)
586}
587
588#[cfg(test)]
589mod tests {
590    use std::{cell::RefCell, rc::Rc};
591
592    use nautilus_common::{cache::Cache, clock::TestClock};
593    use nautilus_core::{UUID4, UnixNanos};
594    use nautilus_model::{
595        enums::{ContingencyType, OrderSide, OrderType, TriggerType},
596        events::{OrderAccepted, OrderSubmitted},
597        identifiers::{
598            AccountId, ClientOrderId, ExecAlgorithmId, InstrumentId, StrategyId, TraderId,
599            VenueOrderId,
600        },
601        instruments::{Instrument, InstrumentAny, stubs::audusd_sim},
602        orders::{Order, OrderTestBuilder, stubs::TestOrderEventStubs},
603        types::{Price, Quantity},
604    };
605    use rstest::rstest;
606
607    use super::*;
608
609    /// Verifies unhandled order events are no-ops and don't panic.
610    /// Previously, unhandled events would hit a todo!() panic.
611    #[rstest]
612    fn test_handle_event_unhandled_events_are_noop() {
613        let submitted = OrderEventAny::Submitted(OrderSubmitted {
614            trader_id: TraderId::from("TRADER-001"),
615            strategy_id: StrategyId::from("STRATEGY-001"),
616            instrument_id: InstrumentId::from("BTC-USDT.OKX"),
617            client_order_id: ClientOrderId::from("O-001"),
618            account_id: AccountId::from("ACCOUNT-001"),
619            event_id: UUID4::new(),
620            ts_event: UnixNanos::default(),
621            ts_init: UnixNanos::default(),
622            causation_id: None,
623        });
624        let accepted = OrderEventAny::Accepted(OrderAccepted {
625            trader_id: TraderId::from("TRADER-001"),
626            strategy_id: StrategyId::from("STRATEGY-001"),
627            instrument_id: InstrumentId::from("BTC-USDT.OKX"),
628            client_order_id: ClientOrderId::from("O-001"),
629            venue_order_id: VenueOrderId::from("V-001"),
630            account_id: AccountId::from("ACCOUNT-001"),
631            event_id: UUID4::new(),
632            ts_event: UnixNanos::default(),
633            ts_init: UnixNanos::default(),
634            reconciliation: false,
635            causation_id: None,
636        });
637
638        match submitted {
639            OrderEventAny::Rejected(_) => panic!("Should not match"),
640            OrderEventAny::Canceled(_) => panic!("Should not match"),
641            OrderEventAny::Expired(_) => panic!("Should not match"),
642            OrderEventAny::Updated(_) => panic!("Should not match"),
643            OrderEventAny::Filled(_) => panic!("Should not match"),
644            _ => {}
645        }
646
647        match accepted {
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
657    fn create_test_components() -> (Rc<RefCell<dyn Clock>>, Rc<RefCell<Cache>>) {
658        let clock: Rc<RefCell<dyn Clock>> = Rc::new(RefCell::new(TestClock::new()));
659        let cache = Rc::new(RefCell::new(Cache::new(None, None)));
660        (clock, cache)
661    }
662
663    fn create_test_stop_order() -> OrderAny {
664        let instrument = audusd_sim();
665        OrderTestBuilder::new(OrderType::StopMarket)
666            .instrument_id(instrument.id())
667            .side(OrderSide::Buy)
668            .trigger_price(Price::from("1.00050"))
669            .quantity(Quantity::from(100_000))
670            .emulation_trigger(TriggerType::BidAsk)
671            .build()
672    }
673
674    // Creates a `SubmitOrder` command suitable for seeding `submit_order_commands`
675    // so that whether `cancel_order` removed the entry can be observed.
676    fn make_submit_command(order: &OrderAny) -> SubmitOrder {
677        SubmitOrder::new(
678            order.trader_id(),
679            None,
680            order.strategy_id(),
681            order.instrument_id(),
682            order.client_order_id(),
683            order.init_event().clone(),
684            None,
685            None,
686            None,
687            UUID4::new(),
688            UnixNanos::default(),
689            None, // correlation_id
690        )
691    }
692
693    #[rstest]
694    fn test_create_new_submit_order_returns_emulator_submit_action() {
695        let (clock, cache) = create_test_components();
696        let mut manager = OrderManager::new(clock, cache, true);
697        let order = create_test_stop_order();
698
699        let actions = manager
700            .create_new_submit_order(&order, None, None, None)
701            .unwrap();
702
703        assert_eq!(actions.len(), 2);
704        assert!(matches!(
705            &actions[0],
706            OrderManagerAction::PublishInitialized(OrderEventAny::Initialized(event))
707                if event.client_order_id == order.client_order_id()
708        ));
709        assert!(matches!(
710            &actions[1],
711            OrderManagerAction::SubmitToEmulator(command)
712                if command.client_order_id == order.client_order_id()
713        ));
714        assert!(
715            manager
716                .submit_order_commands
717                .contains_key(&order.client_order_id())
718        );
719    }
720
721    #[rstest]
722    fn test_create_new_submit_order_returns_risk_submit_action() {
723        let (clock, cache) = create_test_components();
724        let mut manager = OrderManager::new(clock, cache, true);
725        let order = OrderTestBuilder::new(OrderType::Limit)
726            .instrument_id(audusd_sim().id())
727            .side(OrderSide::Buy)
728            .price(Price::from("1.00000"))
729            .quantity(Quantity::from(100_000))
730            .emulation_trigger(TriggerType::NoTrigger)
731            .build();
732
733        let actions = manager
734            .create_new_submit_order(&order, None, None, None)
735            .unwrap();
736
737        assert_eq!(actions.len(), 2);
738        assert!(matches!(
739            &actions[0],
740            OrderManagerAction::PublishInitialized(OrderEventAny::Initialized(event))
741                if event.client_order_id == order.client_order_id()
742        ));
743        assert!(matches!(
744            &actions[1],
745            OrderManagerAction::SubmitToRisk(command)
746                if command.client_order_id == order.client_order_id()
747        ));
748    }
749
750    #[rstest]
751    fn test_create_new_submit_order_returns_risk_action_for_none_trigger() {
752        let (clock, cache) = create_test_components();
753        let mut manager = OrderManager::new(clock, cache, true);
754        let order = OrderTestBuilder::new(OrderType::Market)
755            .instrument_id(audusd_sim().id())
756            .side(OrderSide::Buy)
757            .quantity(Quantity::from(100_000))
758            .build();
759
760        let actions = manager
761            .create_new_submit_order(&order, None, None, None)
762            .unwrap();
763
764        assert_eq!(actions.len(), 2);
765        assert!(order.emulation_trigger().is_none());
766        assert!(matches!(
767            &actions[1],
768            OrderManagerAction::SubmitToRisk(command)
769                if command.client_order_id == order.client_order_id()
770        ));
771    }
772
773    #[rstest]
774    fn test_create_new_submit_order_returns_algorithm_submit_action() {
775        let (clock, cache) = create_test_components();
776        let mut manager = OrderManager::new(clock, cache, true);
777        let exec_algorithm_id = ExecAlgorithmId::from("ALG-001");
778        let order = OrderTestBuilder::new(OrderType::Limit)
779            .instrument_id(audusd_sim().id())
780            .side(OrderSide::Buy)
781            .price(Price::from("1.00000"))
782            .quantity(Quantity::from(100_000))
783            .emulation_trigger(TriggerType::NoTrigger)
784            .exec_algorithm_id(exec_algorithm_id)
785            .build();
786
787        let actions = manager
788            .create_new_submit_order(&order, None, None, None)
789            .unwrap();
790
791        assert_eq!(actions.len(), 2);
792        assert!(matches!(
793            &actions[1],
794            OrderManagerAction::SubmitToAlgorithm {
795                command,
796                exec_algorithm_id: action_exec_algorithm_id,
797            } if command.client_order_id == order.client_order_id()
798                && *action_exec_algorithm_id == exec_algorithm_id
799        ));
800    }
801
802    #[rstest]
803    fn test_create_new_submit_order_does_not_republish_initialized_for_existing_order() {
804        let (clock, cache) = create_test_components();
805        let mut manager = OrderManager::new(clock, cache.clone(), true);
806        let order = create_test_stop_order();
807        cache
808            .borrow_mut()
809            .add_order(order.clone(), None, None, true)
810            .unwrap();
811
812        let actions = manager
813            .create_new_submit_order(&order, None, None, None)
814            .unwrap();
815
816        assert_eq!(actions.len(), 1);
817        assert!(matches!(
818            &actions[0],
819            OrderManagerAction::SubmitToEmulator(command)
820                if command.client_order_id == order.client_order_id()
821        ));
822    }
823
824    #[rstest]
825    fn test_cancel_order_returns_cancel_local_action() {
826        let (clock, cache) = create_test_components();
827        let mut manager = OrderManager::new(clock, cache.clone(), true);
828        let order = create_test_stop_order();
829        cache
830            .borrow_mut()
831            .add_order(order.clone(), None, None, false)
832            .unwrap();
833        manager
834            .submit_order_commands
835            .insert(order.client_order_id(), make_submit_command(&order));
836
837        let actions = manager.cancel_order(&order);
838
839        assert!(matches!(
840            actions.as_slice(),
841            [OrderManagerAction::CancelLocal(action_order)]
842                if action_order.client_order_id() == order.client_order_id()
843        ));
844        assert!(
845            !manager
846                .submit_order_commands
847                .contains_key(&order.client_order_id()),
848            "expected cancel action path to remove the submit command",
849        );
850    }
851
852    #[rstest]
853    fn test_modify_order_quantity_returns_modify_local_action() {
854        let (clock, cache) = create_test_components();
855        let mut manager = OrderManager::new(clock, cache, true);
856        let order = create_test_stop_order();
857        let new_quantity = Quantity::from(50_000);
858
859        let actions = manager.modify_order_quantity(&order, new_quantity);
860
861        assert!(matches!(
862            actions.as_slice(),
863            [OrderManagerAction::ModifyLocalQuantity { order: action_order, quantity }]
864                if action_order.client_order_id() == order.client_order_id()
865                    && *quantity == new_quantity
866        ));
867    }
868
869    #[rstest]
870    fn test_handle_event_unhandled_events_return_no_actions() {
871        let (clock, cache) = create_test_components();
872        let mut manager = OrderManager::new(clock, cache, true);
873        let order = create_test_stop_order();
874        let event = OrderEventAny::Submitted(OrderSubmitted {
875            trader_id: order.trader_id(),
876            strategy_id: order.strategy_id(),
877            instrument_id: order.instrument_id(),
878            client_order_id: order.client_order_id(),
879            account_id: AccountId::from("ACCOUNT-001"),
880            event_id: UUID4::new(),
881            ts_event: UnixNanos::default(),
882            ts_init: UnixNanos::default(),
883            causation_id: None,
884        });
885
886        let actions = manager.handle_event(&event);
887
888        assert!(actions.is_empty());
889    }
890
891    #[rstest]
892    fn test_handle_order_filled_skips_missing_oco_contingent_order() {
893        let (clock, cache) = create_test_components();
894        let mut manager = OrderManager::new(clock, cache.clone(), true);
895        let instrument = InstrumentAny::CurrencyPair(audusd_sim());
896        let missing_client_order_id = ClientOrderId::from("O-MISSING");
897        let valid_client_order_id = ClientOrderId::from("O-CHILD");
898        let order = OrderTestBuilder::new(OrderType::Limit)
899            .instrument_id(instrument.id())
900            .client_order_id(ClientOrderId::from("O-PARENT"))
901            .side(OrderSide::Buy)
902            .price(Price::from("1.00000"))
903            .quantity(Quantity::from(100_000))
904            .contingency_type(ContingencyType::Oco)
905            .linked_order_ids(vec![missing_client_order_id, valid_client_order_id])
906            .build();
907        let child_order = OrderTestBuilder::new(OrderType::Limit)
908            .instrument_id(instrument.id())
909            .client_order_id(valid_client_order_id)
910            .side(OrderSide::Buy)
911            .price(Price::from("1.00000"))
912            .quantity(Quantity::from(100_000))
913            .build();
914        cache
915            .borrow_mut()
916            .add_order(order.clone(), None, None, false)
917            .unwrap();
918        cache
919            .borrow_mut()
920            .add_order(child_order.clone(), None, None, false)
921            .unwrap();
922        manager
923            .submit_order_commands
924            .insert(valid_client_order_id, make_submit_command(&child_order));
925        let filled = match TestOrderEventStubs::filled(
926            &order,
927            &instrument,
928            None,
929            None,
930            None,
931            None,
932            None,
933            None,
934            None,
935            Some(AccountId::from("SIM-001")),
936        ) {
937            OrderEventAny::Filled(event) => event,
938            event => panic!("expected OrderFilled, was {event:?}"),
939        };
940
941        let actions = manager.handle_order_filled(filled);
942
943        assert!(matches!(
944            actions.as_slice(),
945            [OrderManagerAction::CancelLocal(action_order)]
946                if action_order.client_order_id() == valid_client_order_id
947        ));
948        assert!(
949            !manager
950                .submit_order_commands
951                .contains_key(&valid_client_order_id)
952        );
953    }
954
955    #[rstest]
956    fn test_handle_event_inactive_manager_returns_no_local_actions() {
957        let (clock, cache) = create_test_components();
958        let mut manager = OrderManager::new(clock, cache.clone(), false);
959        let instrument = InstrumentAny::CurrencyPair(audusd_sim());
960        let parent_id = ClientOrderId::from("O-PARENT");
961        let child_id = ClientOrderId::from("O-CHILD");
962        let order = OrderTestBuilder::new(OrderType::Limit)
963            .instrument_id(instrument.id())
964            .client_order_id(parent_id)
965            .side(OrderSide::Buy)
966            .price(Price::from("1.00000"))
967            .quantity(Quantity::from(100_000))
968            .contingency_type(ContingencyType::Oco)
969            .linked_order_ids(vec![parent_id, child_id])
970            .build();
971        let child_order = OrderTestBuilder::new(OrderType::Limit)
972            .instrument_id(instrument.id())
973            .client_order_id(child_id)
974            .side(OrderSide::Buy)
975            .price(Price::from("1.00000"))
976            .quantity(Quantity::from(100_000))
977            .build();
978        cache
979            .borrow_mut()
980            .add_order(order.clone(), None, None, false)
981            .unwrap();
982        cache
983            .borrow_mut()
984            .add_order(child_order.clone(), None, None, false)
985            .unwrap();
986        manager
987            .submit_order_commands
988            .insert(child_id, make_submit_command(&child_order));
989        let event = TestOrderEventStubs::filled(
990            &order,
991            &instrument,
992            None,
993            None,
994            None,
995            None,
996            None,
997            None,
998            None,
999            Some(AccountId::from("SIM-001")),
1000        );
1001
1002        let actions = manager.handle_event(&event);
1003
1004        assert!(actions.is_empty());
1005        assert!(
1006            manager.submit_order_commands.contains_key(&child_id),
1007            "inactive manager must not start local contingency actions",
1008        );
1009    }
1010
1011    #[rstest]
1012    fn test_handle_order_filled_skips_missing_oto_child_order() {
1013        let (clock, cache) = create_test_components();
1014        let mut manager = OrderManager::new(clock, cache.clone(), true);
1015        let instrument = InstrumentAny::CurrencyPair(audusd_sim());
1016        let missing_client_order_id = ClientOrderId::from("O-MISSING");
1017        let valid_client_order_id = ClientOrderId::from("O-CHILD");
1018        let order = OrderTestBuilder::new(OrderType::Limit)
1019            .instrument_id(instrument.id())
1020            .client_order_id(ClientOrderId::from("O-PARENT"))
1021            .side(OrderSide::Buy)
1022            .price(Price::from("1.00000"))
1023            .quantity(Quantity::from(100_000))
1024            .contingency_type(ContingencyType::Oto)
1025            .linked_order_ids(vec![missing_client_order_id, valid_client_order_id])
1026            .build();
1027        let child_order = OrderTestBuilder::new(OrderType::Limit)
1028            .instrument_id(instrument.id())
1029            .client_order_id(valid_client_order_id)
1030            .side(OrderSide::Buy)
1031            .price(Price::from("1.00000"))
1032            .quantity(Quantity::from(100_000))
1033            .emulation_trigger(TriggerType::NoTrigger)
1034            .build();
1035        cache
1036            .borrow_mut()
1037            .add_order(order.clone(), None, None, false)
1038            .unwrap();
1039        cache
1040            .borrow_mut()
1041            .add_order(child_order, None, None, false)
1042            .unwrap();
1043        let filled = match TestOrderEventStubs::filled(
1044            &order,
1045            &instrument,
1046            None,
1047            None,
1048            None,
1049            None,
1050            None,
1051            None,
1052            None,
1053            Some(AccountId::from("SIM-001")),
1054        ) {
1055            OrderEventAny::Filled(event) => event,
1056            event => panic!("expected OrderFilled, was {event:?}"),
1057        };
1058
1059        let actions = manager.handle_order_filled(filled);
1060
1061        assert_eq!(actions.len(), 2);
1062        assert!(matches!(
1063            &actions[0],
1064            OrderManagerAction::ModifyLocalQuantity { order, quantity }
1065                if order.client_order_id() == valid_client_order_id
1066                    && *quantity == Quantity::zero(0)
1067        ));
1068        assert!(matches!(
1069            &actions[1],
1070            OrderManagerAction::SubmitToRisk(command)
1071                if command.client_order_id == valid_client_order_id
1072        ));
1073        assert!(
1074            manager
1075                .submit_order_commands
1076                .contains_key(&valid_client_order_id)
1077        );
1078    }
1079
1080    #[rstest]
1081    fn test_handle_contingencies_skips_missing_linked_order() {
1082        let (clock, cache) = create_test_components();
1083        let mut manager = OrderManager::new(clock, cache, true);
1084        let instrument = audusd_sim();
1085        let order = OrderTestBuilder::new(OrderType::Limit)
1086            .instrument_id(instrument.id())
1087            .client_order_id(ClientOrderId::from("O-PARENT"))
1088            .side(OrderSide::Buy)
1089            .price(Price::from("1.00000"))
1090            .quantity(Quantity::from(100_000))
1091            .contingency_type(ContingencyType::Oco)
1092            .linked_order_ids(vec![ClientOrderId::from("O-MISSING")])
1093            .build();
1094
1095        let actions = manager.handle_contingencies(&order);
1096
1097        assert!(actions.is_empty());
1098        assert!(manager.submit_order_commands.is_empty());
1099    }
1100
1101    #[rstest]
1102    fn test_handle_contingencies_update_skips_missing_linked_order() {
1103        let (clock, cache) = create_test_components();
1104        let mut manager = OrderManager::new(clock, cache, true);
1105        let instrument = audusd_sim();
1106        let order = OrderTestBuilder::new(OrderType::Limit)
1107            .instrument_id(instrument.id())
1108            .client_order_id(ClientOrderId::from("O-PARENT"))
1109            .side(OrderSide::Buy)
1110            .price(Price::from("1.00000"))
1111            .quantity(Quantity::from(100_000))
1112            .contingency_type(ContingencyType::Oco)
1113            .linked_order_ids(vec![ClientOrderId::from("O-MISSING")])
1114            .build();
1115
1116        let actions = manager.handle_contingencies_update(&order);
1117
1118        assert!(actions.is_empty());
1119        assert!(manager.submit_order_commands.is_empty());
1120    }
1121
1122    #[rstest]
1123    fn test_cancel_order_skips_when_pending_cancel_local() {
1124        let (clock, cache) = create_test_components();
1125        let mut manager = OrderManager::new(clock, cache.clone(), true);
1126        let order = create_test_stop_order();
1127        cache
1128            .borrow_mut()
1129            .add_order(order.clone(), None, None, false)
1130            .unwrap();
1131        cache.borrow_mut().update_order_pending_cancel_local(&order);
1132        manager
1133            .submit_order_commands
1134            .insert(order.client_order_id(), make_submit_command(&order));
1135
1136        manager.cancel_order(&order);
1137
1138        assert!(
1139            manager
1140                .submit_order_commands
1141                .contains_key(&order.client_order_id()),
1142            "pending-cancel-local gate should short-circuit before removing the submit command",
1143        );
1144    }
1145
1146    #[rstest]
1147    fn test_cancel_order_skips_when_passed_order_is_closed() {
1148        // The caller has applied a closing event to its local clone but has
1149        // not yet called `cache.update_order`, so the cache index still
1150        // reports open. The gate must short-circuit on the local state.
1151        let (clock, cache) = create_test_components();
1152        let mut manager = OrderManager::new(clock, cache.clone(), true);
1153
1154        let mut order = OrderTestBuilder::new(OrderType::StopMarket)
1155            .instrument_id(audusd_sim().id())
1156            .side(OrderSide::Buy)
1157            .trigger_price(Price::from("1.00050"))
1158            .quantity(Quantity::from(100_000))
1159            .emulation_trigger(TriggerType::BidAsk)
1160            .submit(true)
1161            .build();
1162
1163        cache
1164            .borrow_mut()
1165            .add_order(order.clone(), None, None, false)
1166            .unwrap();
1167
1168        let canceled_event =
1169            TestOrderEventStubs::canceled(&order, AccountId::from("ACCOUNT-001"), None);
1170        order.apply(canceled_event).unwrap();
1171
1172        assert!(order.is_closed());
1173        assert!(!cache.borrow().is_order_closed(&order.client_order_id()));
1174
1175        manager
1176            .submit_order_commands
1177            .insert(order.client_order_id(), make_submit_command(&order));
1178
1179        manager.cancel_order(&order);
1180
1181        assert!(
1182            manager
1183                .submit_order_commands
1184                .contains_key(&order.client_order_id()),
1185            "closed-order gate should short-circuit on the local state when the cache index is stale",
1186        );
1187    }
1188
1189    #[rstest]
1190    fn test_cancel_order_skips_when_cache_index_marks_closed() {
1191        // The passed `OrderAny` is intentionally a stale (Submitted) clone so
1192        // this test would fail if `cancel_order` checked `order.is_closed()`
1193        // on the argument instead of `cache.is_order_closed(&id)`.
1194        let (clock, cache) = create_test_components();
1195        let mut manager = OrderManager::new(clock, cache.clone(), true);
1196
1197        let mut order = OrderTestBuilder::new(OrderType::StopMarket)
1198            .instrument_id(audusd_sim().id())
1199            .side(OrderSide::Buy)
1200            .trigger_price(Price::from("1.00050"))
1201            .quantity(Quantity::from(100_000))
1202            .emulation_trigger(TriggerType::BidAsk)
1203            .submit(true)
1204            .build();
1205
1206        cache
1207            .borrow_mut()
1208            .add_order(order.clone(), None, None, false)
1209            .unwrap();
1210
1211        let stale_order = order.clone();
1212
1213        let canceled_event =
1214            TestOrderEventStubs::canceled(&order, AccountId::from("ACCOUNT-001"), None);
1215        order = cache.borrow_mut().update_order(&canceled_event).unwrap();
1216
1217        assert!(cache.borrow().is_order_closed(&order.client_order_id()));
1218
1219        manager.submit_order_commands.insert(
1220            stale_order.client_order_id(),
1221            make_submit_command(&stale_order),
1222        );
1223
1224        manager.cancel_order(&stale_order);
1225
1226        assert!(
1227            manager
1228                .submit_order_commands
1229                .contains_key(&stale_order.client_order_id()),
1230            "closed-order gate should short-circuit even when the passed reference is stale",
1231        );
1232    }
1233}