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::{
20    cache::Cache,
21    clock::Clock,
22    logging::{CMD, EVT, SEND},
23    messages::execution::{SubmitOrder, TradingCommand},
24    msgbus,
25    msgbus::MessagingSwitchboard,
26};
27use nautilus_core::UUID4;
28use nautilus_model::{
29    enums::{ContingencyType, TriggerType},
30    events::{
31        OrderCanceled, OrderEventAny, OrderExpired, OrderFilled, OrderRejected, OrderUpdated,
32    },
33    identifiers::{ClientId, ClientOrderId, ExecAlgorithmId, PositionId},
34    orders::{Order, OrderAny},
35    types::Quantity,
36};
37
38use super::handlers::{
39    CancelOrderHandler, CancelOrderHandlerAny, ModifyOrderHandler, ModifyOrderHandlerAny,
40    SubmitOrderHandler, SubmitOrderHandlerAny,
41};
42
43/// Manages the lifecycle and state of orders with contingency handling.
44///
45/// The order manager is responsible for managing local order state, handling
46/// contingent orders (OTO, OCO, OUO), and coordinating with emulation and
47/// execution systems. It tracks order commands and manages complex order
48/// relationships for advanced order types.
49pub struct OrderManager {
50    clock: Rc<RefCell<dyn Clock>>,
51    cache: Rc<RefCell<Cache>>,
52    active_local: bool,
53    submit_order_handler: Option<SubmitOrderHandlerAny>,
54    cancel_order_handler: Option<CancelOrderHandlerAny>,
55    modify_order_handler: Option<ModifyOrderHandlerAny>,
56    submit_order_commands: AHashMap<ClientOrderId, SubmitOrder>,
57}
58
59impl Debug for OrderManager {
60    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61        f.debug_struct(stringify!(OrderManager))
62            .field("pending_commands", &self.submit_order_commands.len())
63            .finish()
64    }
65}
66
67impl OrderManager {
68    /// Creates a new [`OrderManager`] instance.
69    pub fn new(
70        clock: Rc<RefCell<dyn Clock>>,
71        cache: Rc<RefCell<Cache>>,
72        active_local: bool,
73        submit_order_handler: Option<SubmitOrderHandlerAny>,
74        cancel_order_handler: Option<CancelOrderHandlerAny>,
75        modify_order_handler: Option<ModifyOrderHandlerAny>,
76    ) -> Self {
77        Self {
78            clock,
79            cache,
80            active_local,
81            submit_order_handler,
82            cancel_order_handler,
83            modify_order_handler,
84            submit_order_commands: AHashMap::new(),
85        }
86    }
87
88    /// Sets the handler for submit order commands to the emulator.
89    pub fn set_submit_order_handler(&mut self, handler: SubmitOrderHandlerAny) {
90        self.submit_order_handler = Some(handler);
91    }
92
93    /// Sets the handler for cancel order commands to the emulator.
94    pub fn set_cancel_order_handler(&mut self, handler: CancelOrderHandlerAny) {
95        self.cancel_order_handler = Some(handler);
96    }
97
98    /// Sets the handler for modify order commands to the emulator.
99    pub fn set_modify_order_handler(&mut self, handler: ModifyOrderHandlerAny) {
100        self.modify_order_handler = Some(handler);
101    }
102
103    #[must_use]
104    /// Returns a copy of all cached submit order commands.
105    pub fn get_submit_order_commands(&self) -> AHashMap<ClientOrderId, SubmitOrder> {
106        self.submit_order_commands.clone()
107    }
108
109    /// Caches a submit order command for later processing.
110    pub fn cache_submit_order_command(&mut self, command: SubmitOrder) {
111        self.submit_order_commands
112            .insert(command.client_order_id, command);
113    }
114
115    /// Removes and returns a cached submit order command.
116    pub fn pop_submit_order_command(
117        &mut self,
118        client_order_id: ClientOrderId,
119    ) -> Option<SubmitOrder> {
120        self.submit_order_commands.remove(&client_order_id)
121    }
122
123    /// Resets the order manager by clearing all cached commands.
124    pub fn reset(&mut self) {
125        self.submit_order_commands.clear();
126    }
127
128    /// Cancels an order if it's not already pending cancellation or closed.
129    pub fn cancel_order(&mut self, order: &OrderAny) {
130        let client_order_id = order.client_order_id();
131        let cache = self.cache.borrow();
132
133        if cache.is_order_pending_cancel_local(&client_order_id) {
134            return;
135        }
136
137        if order.is_closed() || cache.is_order_closed(&client_order_id) {
138            log::warn!("Cannot cancel order: already closed");
139            return;
140        }
141
142        drop(cache);
143        self.submit_order_commands.remove(&client_order_id);
144
145        if let Some(handler) = &self.cancel_order_handler {
146            handler.handle_cancel_order(order);
147        }
148    }
149
150    /// Modifies the quantity of an existing order.
151    pub fn modify_order_quantity(&mut self, order: &OrderAny, new_quantity: Quantity) {
152        if let Some(handler) = &self.modify_order_handler {
153            handler.handle_modify_order(order, new_quantity);
154        }
155    }
156
157    /// # Errors
158    ///
159    /// Returns an error if creating a new submit order fails.
160    pub fn create_new_submit_order(
161        &mut self,
162        order: &OrderAny,
163        position_id: Option<PositionId>,
164        client_id: Option<ClientId>,
165    ) -> anyhow::Result<()> {
166        self.cache
167            .borrow_mut()
168            .add_order(order.clone(), position_id, client_id, true)?;
169
170        let submit = SubmitOrder::new(
171            order.trader_id(),
172            client_id,
173            order.strategy_id(),
174            order.instrument_id(),
175            order.client_order_id(),
176            order.init_event().clone(),
177            order.exec_algorithm_id(),
178            position_id,
179            None, // params
180            UUID4::new(),
181            self.clock.borrow().timestamp_ns(),
182        );
183
184        if order.emulation_trigger() == Some(TriggerType::NoTrigger) {
185            self.cache_submit_order_command(submit.clone());
186
187            match order.exec_algorithm_id() {
188                Some(exec_algorithm_id) => {
189                    self.send_algo_command(submit, exec_algorithm_id);
190                }
191                None => self.send_risk_command(TradingCommand::SubmitOrder(submit)),
192            }
193        } else if let Some(handler) = self.submit_order_handler.clone() {
194            self.cache_submit_order_command(submit.clone());
195            handler.handle_submit_order(submit);
196        }
197
198        Ok(())
199    }
200
201    #[must_use]
202    /// Returns true if the order manager should manage the given order.
203    pub fn should_manage_order(&self, order: &OrderAny) -> bool {
204        self.active_local && order.is_active_local()
205    }
206
207    // Event Handlers
208    /// Handles an order event by routing it to the appropriate handler method.
209    ///
210    /// Note: Only handles specific terminal/actionable events. Other events
211    /// like `OrderSubmitted`, `OrderAccepted`, etc. are no-ops for the order manager.
212    pub fn handle_event(&mut self, event: &OrderEventAny) {
213        match event {
214            OrderEventAny::Rejected(event) => self.handle_order_rejected(*event),
215            OrderEventAny::Canceled(event) => self.handle_order_canceled(*event),
216            OrderEventAny::Expired(event) => self.handle_order_expired(*event),
217            OrderEventAny::Updated(event) => self.handle_order_updated(*event),
218            OrderEventAny::Filled(event) => self.handle_order_filled(*event),
219            _ => {}
220        }
221    }
222
223    /// Handles an order rejected event and manages any contingent orders.
224    pub fn handle_order_rejected(&mut self, rejected: OrderRejected) {
225        let cloned_order = self
226            .cache
227            .borrow()
228            .order(&rejected.client_order_id)
229            .cloned();
230
231        if let Some(order) = cloned_order {
232            if order.contingency_type() != Some(ContingencyType::NoContingency) {
233                self.handle_contingencies(&order);
234            }
235        } else {
236            log::error!(
237                "Cannot handle `OrderRejected`: order for client_order_id: {} not found, {}",
238                rejected.client_order_id,
239                rejected
240            );
241        }
242    }
243
244    pub fn handle_order_canceled(&mut self, canceled: OrderCanceled) {
245        let cloned_order = self
246            .cache
247            .borrow()
248            .order(&canceled.client_order_id)
249            .cloned();
250
251        if let Some(order) = cloned_order {
252            if order.contingency_type() != Some(ContingencyType::NoContingency) {
253                self.handle_contingencies(&order);
254            }
255        } else {
256            log::error!(
257                "Cannot handle `OrderCanceled`: order for client_order_id: {} not found, {}",
258                canceled.client_order_id,
259                canceled
260            );
261        }
262    }
263
264    pub fn handle_order_expired(&mut self, expired: OrderExpired) {
265        let cloned_order = self.cache.borrow().order(&expired.client_order_id).cloned();
266        if let Some(order) = cloned_order {
267            if order.contingency_type() != Some(ContingencyType::NoContingency) {
268                self.handle_contingencies(&order);
269            }
270        } else {
271            log::error!(
272                "Cannot handle `OrderExpired`: order for client_order_id: {} not found, {}",
273                expired.client_order_id,
274                expired
275            );
276        }
277    }
278
279    pub fn handle_order_updated(&mut self, updated: OrderUpdated) {
280        let cloned_order = self.cache.borrow().order(&updated.client_order_id).cloned();
281        if let Some(order) = cloned_order {
282            if order.contingency_type() != Some(ContingencyType::NoContingency) {
283                self.handle_contingencies_update(&order);
284            }
285        } else {
286            log::error!(
287                "Cannot handle `OrderUpdated`: order for client_order_id: {} not found, {}",
288                updated.client_order_id,
289                updated
290            );
291        }
292    }
293
294    /// # Panics
295    ///
296    /// Panics if the OTO child order cannot be found for the given client order ID.
297    pub fn handle_order_filled(&mut self, filled: OrderFilled) {
298        let order = if let Some(order) = self.cache.borrow().order(&filled.client_order_id).cloned()
299        {
300            order
301        } else {
302            log::error!(
303                "Cannot handle `OrderFilled`: order for client_order_id: {} not found, {}",
304                filled.client_order_id,
305                filled
306            );
307            return;
308        };
309
310        match order.contingency_type() {
311            Some(ContingencyType::Oto) => {
312                let position_id = self
313                    .cache
314                    .borrow()
315                    .position_id(&order.client_order_id())
316                    .copied();
317                let client_id = self
318                    .cache
319                    .borrow()
320                    .client_id(&order.client_order_id())
321                    .copied();
322
323                let parent_filled_qty = match order.exec_spawn_id() {
324                    Some(spawn_id) => {
325                        if let Some(qty) = self
326                            .cache
327                            .borrow()
328                            .exec_spawn_total_filled_qty(&spawn_id, true)
329                        {
330                            qty
331                        } else {
332                            log::error!("Failed to get spawn filled quantity for {spawn_id}");
333                            return;
334                        }
335                    }
336                    None => order.filled_qty(),
337                };
338
339                let linked_orders = if let Some(orders) = order.linked_order_ids() {
340                    orders
341                } else {
342                    log::error!("No linked orders found for OTO order");
343                    return;
344                };
345
346                for client_order_id in linked_orders {
347                    let mut child_order =
348                        if let Some(order) = self.cache.borrow().order(client_order_id).cloned() {
349                            order
350                        } else {
351                            panic!(
352                                "Cannot find OTO child order for client_order_id: {client_order_id}"
353                            );
354                        };
355
356                    if !self.should_manage_order(&child_order) {
357                        continue;
358                    }
359
360                    if child_order.position_id().is_none() {
361                        child_order.set_position_id(position_id);
362                    }
363
364                    if parent_filled_qty != child_order.leaves_qty() {
365                        self.modify_order_quantity(&child_order, parent_filled_qty);
366                    }
367
368                    // if self.submit_order_handler.is_none() {
369                    //     return;
370                    // }
371
372                    if !self
373                        .submit_order_commands
374                        .contains_key(&child_order.client_order_id())
375                        && let Err(e) =
376                            self.create_new_submit_order(&child_order, position_id, client_id)
377                    {
378                        log::error!("Failed to create new submit order: {e}");
379                    }
380                }
381            }
382            Some(ContingencyType::Oco) => {
383                let linked_orders = if let Some(orders) = order.linked_order_ids() {
384                    orders
385                } else {
386                    log::error!("No linked orders found for OCO order");
387                    return;
388                };
389
390                for client_order_id in linked_orders {
391                    let contingent_order = match self.cache.borrow().order(client_order_id).cloned()
392                    {
393                        Some(contingent_order) => contingent_order,
394                        None => {
395                            panic!(
396                                "Cannot find OCO contingent order for client_order_id: {client_order_id}"
397                            );
398                        }
399                    };
400
401                    // Not being managed || Already completed
402                    if !self.should_manage_order(&contingent_order) || contingent_order.is_closed()
403                    {
404                        continue;
405                    }
406
407                    if contingent_order.client_order_id() != order.client_order_id() {
408                        self.cancel_order(&contingent_order);
409                    }
410                }
411            }
412            Some(ContingencyType::Ouo) => self.handle_contingencies(&order),
413            _ => {}
414        }
415    }
416
417    /// # Panics
418    ///
419    /// Panics if a contingent order cannot be found for the given client order ID.
420    pub fn handle_contingencies(&mut self, order: &OrderAny) {
421        let (filled_qty, leaves_qty, is_spawn_active) =
422            if let Some(exec_spawn_id) = order.exec_spawn_id() {
423                if let (Some(filled), Some(leaves)) = (
424                    self.cache
425                        .borrow()
426                        .exec_spawn_total_filled_qty(&exec_spawn_id, true),
427                    self.cache
428                        .borrow()
429                        .exec_spawn_total_leaves_qty(&exec_spawn_id, true),
430                ) {
431                    (filled, leaves, leaves.raw > 0)
432                } else {
433                    log::error!("Failed to get spawn quantities for {exec_spawn_id}");
434                    return;
435                }
436            } else {
437                (order.filled_qty(), order.leaves_qty(), false)
438            };
439
440        let linked_orders = if let Some(orders) = order.linked_order_ids() {
441            orders
442        } else {
443            log::error!("No linked orders found");
444            return;
445        };
446
447        for client_order_id in linked_orders {
448            let contingent_order =
449                if let Some(order) = self.cache.borrow().order(client_order_id).cloned() {
450                    order
451                } else {
452                    panic!("Cannot find contingent order for client_order_id: {client_order_id}");
453                };
454
455            if !self.should_manage_order(&contingent_order)
456                || client_order_id == &order.client_order_id()
457            {
458                continue;
459            }
460
461            if contingent_order.is_closed() {
462                self.submit_order_commands.remove(&order.client_order_id());
463                continue;
464            }
465
466            match order.contingency_type() {
467                Some(ContingencyType::Oto) => {
468                    if order.is_closed()
469                        && filled_qty.raw == 0
470                        && (order.exec_spawn_id().is_none() || !is_spawn_active)
471                    {
472                        self.cancel_order(&contingent_order);
473                    } else if filled_qty.raw > 0 && filled_qty != contingent_order.quantity() {
474                        self.modify_order_quantity(&contingent_order, filled_qty);
475                    }
476                }
477                Some(ContingencyType::Oco)
478                    if order.is_closed()
479                        && (order.exec_spawn_id().is_none() || !is_spawn_active) =>
480                {
481                    self.cancel_order(&contingent_order);
482                }
483                Some(ContingencyType::Ouo) => {
484                    if (leaves_qty.raw == 0 && order.exec_spawn_id().is_some())
485                        || (order.is_closed()
486                            && (order.exec_spawn_id().is_none() || !is_spawn_active))
487                    {
488                        self.cancel_order(&contingent_order);
489                    } else if leaves_qty != contingent_order.leaves_qty() {
490                        self.modify_order_quantity(&contingent_order, leaves_qty);
491                    }
492                }
493                _ => {}
494            }
495        }
496    }
497
498    /// # Panics
499    ///
500    /// Panics if an OCO contingent order cannot be found for the given client order ID.
501    pub fn handle_contingencies_update(&mut self, order: &OrderAny) {
502        let quantity = match order.exec_spawn_id() {
503            Some(exec_spawn_id) => {
504                if let Some(qty) = self
505                    .cache
506                    .borrow()
507                    .exec_spawn_total_quantity(&exec_spawn_id, true)
508                {
509                    qty
510                } else {
511                    log::error!("Failed to get spawn total quantity for {exec_spawn_id}");
512                    return;
513                }
514            }
515            None => order.quantity(),
516        };
517
518        if quantity.raw == 0 {
519            return;
520        }
521
522        let linked_orders = if let Some(orders) = order.linked_order_ids() {
523            orders
524        } else {
525            log::error!("No linked orders found for contingent order");
526            return;
527        };
528
529        for client_order_id in linked_orders {
530            let contingent_order = match self.cache.borrow().order(client_order_id).cloned() {
531                Some(contingent_order) => contingent_order,
532                None => panic!(
533                    "Cannot find OCO contingent order for client_order_id: {client_order_id}"
534                ),
535            };
536
537            if !self.should_manage_order(&contingent_order)
538                || client_order_id == &order.client_order_id()
539                || contingent_order.is_closed()
540            {
541                continue;
542            }
543
544            if let Some(contingency_type) = order.contingency_type()
545                && matches!(
546                    contingency_type,
547                    ContingencyType::Oto | ContingencyType::Oco
548                )
549                && quantity != contingent_order.quantity()
550            {
551                self.modify_order_quantity(&contingent_order, quantity);
552            }
553        }
554    }
555
556    // Message sending methods
557    pub fn send_emulator_command(&self, command: TradingCommand) {
558        log_cmd_send(&command);
559        let endpoint = MessagingSwitchboard::order_emulator_execute();
560        msgbus::send_trading_command(endpoint, command);
561    }
562
563    pub fn send_algo_command(&self, command: SubmitOrder, exec_algorithm_id: ExecAlgorithmId) {
564        let id = command.strategy_id;
565        log::info!("{id} {CMD}{SEND} {command}");
566
567        // Dynamic algorithm endpoint - uses Any-based dispatch
568        let endpoint = format!("{exec_algorithm_id}.execute");
569        msgbus::send_any(endpoint.into(), &TradingCommand::SubmitOrder(command));
570    }
571
572    pub fn send_risk_command(&self, command: TradingCommand) {
573        log_cmd_send(&command);
574
575        // Use queued endpoint for re-entrancy safety, commands may be sent from
576        // within event handlers which hold a mutable borrow on the strategy.
577        // This mirrors the pattern used by `send_exec_command()`.
578        let endpoint = MessagingSwitchboard::risk_engine_queue_execute();
579        msgbus::send_trading_command(endpoint, command);
580    }
581
582    pub fn send_exec_command(&self, command: TradingCommand) {
583        log_cmd_send(&command);
584
585        // Use queued endpoint for re-entrancy safety, commands may be sent from
586        // within event handlers which hold a mutable borrow on ExecutionEngine.
587        let endpoint = MessagingSwitchboard::exec_engine_queue_execute();
588        msgbus::send_trading_command(endpoint, command);
589    }
590
591    pub fn send_risk_event(&self, event: OrderEventAny) {
592        log_evt_send(&event);
593        let endpoint = MessagingSwitchboard::risk_engine_process();
594        msgbus::send_order_event(endpoint, event);
595    }
596
597    pub fn send_exec_event(&self, event: OrderEventAny) {
598        log_evt_send(&event);
599        let endpoint = MessagingSwitchboard::exec_engine_process();
600        msgbus::send_order_event(endpoint, event);
601    }
602}
603
604#[inline(always)]
605fn log_cmd_send(command: &TradingCommand) {
606    if let Some(id) = command.strategy_id() {
607        log::info!("{id} {CMD}{SEND} {command}");
608    } else {
609        log::info!("{CMD}{SEND} {command}");
610    }
611}
612
613#[inline(always)]
614fn log_evt_send(event: &OrderEventAny) {
615    let id = event.strategy_id();
616    log::info!("{id} {EVT}{SEND} {event}");
617}
618
619#[cfg(test)]
620mod tests {
621    use std::{cell::RefCell, rc::Rc};
622
623    use nautilus_common::{cache::Cache, clock::TestClock};
624    use nautilus_core::{UUID4, UnixNanos, WeakCell};
625    use nautilus_model::{
626        enums::{OrderSide, OrderType, TriggerType},
627        events::{OrderAccepted, OrderSubmitted},
628        identifiers::{AccountId, ClientOrderId, InstrumentId, StrategyId, TraderId, VenueOrderId},
629        instruments::{Instrument, stubs::audusd_sim},
630        orders::{Order, OrderTestBuilder, stubs::TestOrderEventStubs},
631        types::{Price, Quantity},
632    };
633    use rstest::rstest;
634
635    use super::*;
636    use crate::{
637        order_emulator::emulator::OrderEmulator,
638        order_manager::handlers::{
639            CancelOrderHandlerAny, ModifyOrderHandlerAny, SubmitOrderHandlerAny,
640        },
641    };
642
643    /// Verifies unhandled order events are no-ops and don't panic.
644    /// Previously, unhandled events would hit a todo!() panic.
645    #[rstest]
646    fn test_handle_event_unhandled_events_are_noop() {
647        let submitted = OrderEventAny::Submitted(OrderSubmitted {
648            trader_id: TraderId::from("TRADER-001"),
649            strategy_id: StrategyId::from("STRATEGY-001"),
650            instrument_id: InstrumentId::from("BTC-USDT.OKX"),
651            client_order_id: ClientOrderId::from("O-001"),
652            account_id: AccountId::from("ACCOUNT-001"),
653            event_id: UUID4::new(),
654            ts_event: UnixNanos::default(),
655            ts_init: UnixNanos::default(),
656        });
657        let accepted = OrderEventAny::Accepted(OrderAccepted {
658            trader_id: TraderId::from("TRADER-001"),
659            strategy_id: StrategyId::from("STRATEGY-001"),
660            instrument_id: InstrumentId::from("BTC-USDT.OKX"),
661            client_order_id: ClientOrderId::from("O-001"),
662            venue_order_id: VenueOrderId::from("V-001"),
663            account_id: AccountId::from("ACCOUNT-001"),
664            event_id: UUID4::new(),
665            ts_event: UnixNanos::default(),
666            ts_init: UnixNanos::default(),
667            reconciliation: 0,
668        });
669
670        match submitted {
671            OrderEventAny::Rejected(_) => panic!("Should not match"),
672            OrderEventAny::Canceled(_) => panic!("Should not match"),
673            OrderEventAny::Expired(_) => panic!("Should not match"),
674            OrderEventAny::Updated(_) => panic!("Should not match"),
675            OrderEventAny::Filled(_) => panic!("Should not match"),
676            _ => {}
677        }
678
679        match accepted {
680            OrderEventAny::Rejected(_) => panic!("Should not match"),
681            OrderEventAny::Canceled(_) => panic!("Should not match"),
682            OrderEventAny::Expired(_) => panic!("Should not match"),
683            OrderEventAny::Updated(_) => panic!("Should not match"),
684            OrderEventAny::Filled(_) => panic!("Should not match"),
685            _ => {}
686        }
687    }
688
689    #[expect(clippy::type_complexity)]
690    fn create_test_components() -> (
691        Rc<RefCell<dyn Clock>>,
692        Rc<RefCell<Cache>>,
693        Rc<RefCell<OrderEmulator>>,
694    ) {
695        let clock: Rc<RefCell<dyn Clock>> = Rc::new(RefCell::new(TestClock::new()));
696        let cache = Rc::new(RefCell::new(Cache::new(None, None)));
697        let emulator = Rc::new(RefCell::new(OrderEmulator::new(
698            clock.clone(),
699            cache.clone(),
700        )));
701        (clock, cache, emulator)
702    }
703
704    fn create_test_stop_order() -> OrderAny {
705        let instrument = audusd_sim();
706        OrderTestBuilder::new(OrderType::StopMarket)
707            .instrument_id(instrument.id())
708            .side(OrderSide::Buy)
709            .trigger_price(Price::from("1.00050"))
710            .quantity(Quantity::from(100_000))
711            .emulation_trigger(TriggerType::BidAsk)
712            .build()
713    }
714
715    // Creates a `SubmitOrder` command suitable for seeding `submit_order_commands`
716    // so that whether `cancel_order` removed the entry can be observed.
717    fn make_submit_command(order: &OrderAny) -> SubmitOrder {
718        SubmitOrder::new(
719            order.trader_id(),
720            None,
721            order.strategy_id(),
722            order.instrument_id(),
723            order.client_order_id(),
724            order.init_event().clone(),
725            None,
726            None,
727            None,
728            UUID4::new(),
729            UnixNanos::default(),
730        )
731    }
732
733    #[rstest]
734    fn test_order_manager_with_handlers() {
735        let (clock, cache, emulator) = create_test_components();
736        let submit_handler =
737            SubmitOrderHandlerAny::OrderEmulator(WeakCell::from(Rc::downgrade(&emulator)));
738        let cancel_handler =
739            CancelOrderHandlerAny::OrderEmulator(WeakCell::from(Rc::downgrade(&emulator)));
740        let modify_handler =
741            ModifyOrderHandlerAny::OrderEmulator(WeakCell::from(Rc::downgrade(&emulator)));
742
743        let manager = OrderManager::new(
744            clock,
745            cache,
746            true,
747            Some(submit_handler),
748            Some(cancel_handler),
749            Some(modify_handler),
750        );
751
752        assert!(manager.submit_order_handler.is_some());
753        assert!(manager.cancel_order_handler.is_some());
754        assert!(manager.modify_order_handler.is_some());
755    }
756
757    #[rstest]
758    fn test_order_manager_cancel_order_dispatches_to_handler() {
759        let (clock, cache, emulator) = create_test_components();
760        let cancel_handler =
761            CancelOrderHandlerAny::OrderEmulator(WeakCell::from(Rc::downgrade(&emulator)));
762        let mut manager =
763            OrderManager::new(clock, cache.clone(), true, None, Some(cancel_handler), None);
764        let order = create_test_stop_order();
765        cache
766            .borrow_mut()
767            .add_order(order.clone(), None, None, false)
768            .unwrap();
769        manager
770            .submit_order_commands
771            .insert(order.client_order_id(), make_submit_command(&order));
772
773        manager.cancel_order(&order);
774
775        assert!(
776            !manager
777                .submit_order_commands
778                .contains_key(&order.client_order_id()),
779            "expected dispatch path to remove the submit command",
780        );
781    }
782
783    #[rstest]
784    fn test_order_manager_modify_order_dispatches_to_handler() {
785        let (clock, cache, emulator) = create_test_components();
786        let modify_handler =
787            ModifyOrderHandlerAny::OrderEmulator(WeakCell::from(Rc::downgrade(&emulator)));
788        let mut manager = OrderManager::new(clock, cache, true, None, None, Some(modify_handler));
789        let order = create_test_stop_order();
790        let new_quantity = Quantity::from(50_000);
791
792        manager.modify_order_quantity(&order, new_quantity);
793    }
794
795    #[rstest]
796    fn test_order_manager_without_handlers() {
797        let (clock, cache, _emulator) = create_test_components();
798        let mut manager = OrderManager::new(clock, cache.clone(), true, None, None, None);
799        let order = create_test_stop_order();
800        cache
801            .borrow_mut()
802            .add_order(order.clone(), None, None, false)
803            .unwrap();
804        manager
805            .submit_order_commands
806            .insert(order.client_order_id(), make_submit_command(&order));
807
808        manager.cancel_order(&order);
809        manager.modify_order_quantity(&order, Quantity::from(50_000));
810
811        assert!(
812            !manager
813                .submit_order_commands
814                .contains_key(&order.client_order_id()),
815            "no-handler dispatch path should still remove the submit command",
816        );
817    }
818
819    #[rstest]
820    fn test_cancel_order_skips_when_pending_cancel_local() {
821        let (clock, cache, _emulator) = create_test_components();
822        let mut manager = OrderManager::new(clock, cache.clone(), true, None, None, None);
823        let order = create_test_stop_order();
824        cache
825            .borrow_mut()
826            .add_order(order.clone(), None, None, false)
827            .unwrap();
828        cache.borrow_mut().update_order_pending_cancel_local(&order);
829        manager
830            .submit_order_commands
831            .insert(order.client_order_id(), make_submit_command(&order));
832
833        manager.cancel_order(&order);
834
835        assert!(
836            manager
837                .submit_order_commands
838                .contains_key(&order.client_order_id()),
839            "pending-cancel-local gate should short-circuit before removing the submit command",
840        );
841    }
842
843    #[rstest]
844    fn test_cancel_order_skips_when_passed_order_is_closed() {
845        // The caller has applied a closing event to its local clone but has
846        // not yet called `cache.update_order`, so the cache index still
847        // reports open. The gate must short-circuit on the local state.
848        let (clock, cache, _emulator) = create_test_components();
849        let mut manager = OrderManager::new(clock, cache.clone(), true, None, None, None);
850
851        let mut order = OrderTestBuilder::new(OrderType::StopMarket)
852            .instrument_id(audusd_sim().id())
853            .side(OrderSide::Buy)
854            .trigger_price(Price::from("1.00050"))
855            .quantity(Quantity::from(100_000))
856            .emulation_trigger(TriggerType::BidAsk)
857            .submit(true)
858            .build();
859
860        cache
861            .borrow_mut()
862            .add_order(order.clone(), None, None, false)
863            .unwrap();
864
865        let canceled_event =
866            TestOrderEventStubs::canceled(&order, AccountId::from("ACCOUNT-001"), None);
867        order.apply(canceled_event).unwrap();
868
869        assert!(order.is_closed());
870        assert!(!cache.borrow().is_order_closed(&order.client_order_id()));
871
872        manager
873            .submit_order_commands
874            .insert(order.client_order_id(), make_submit_command(&order));
875
876        manager.cancel_order(&order);
877
878        assert!(
879            manager
880                .submit_order_commands
881                .contains_key(&order.client_order_id()),
882            "closed-order gate should short-circuit on the local state when the cache index is stale",
883        );
884    }
885
886    #[rstest]
887    fn test_cancel_order_skips_when_cache_index_marks_closed() {
888        // The passed `OrderAny` is intentionally a stale (Submitted) clone so
889        // this test would fail if `cancel_order` checked `order.is_closed()`
890        // on the argument instead of `cache.is_order_closed(&id)`.
891        let (clock, cache, _emulator) = create_test_components();
892        let mut manager = OrderManager::new(clock, cache.clone(), true, None, None, None);
893
894        let mut order = OrderTestBuilder::new(OrderType::StopMarket)
895            .instrument_id(audusd_sim().id())
896            .side(OrderSide::Buy)
897            .trigger_price(Price::from("1.00050"))
898            .quantity(Quantity::from(100_000))
899            .emulation_trigger(TriggerType::BidAsk)
900            .submit(true)
901            .build();
902
903        cache
904            .borrow_mut()
905            .add_order(order.clone(), None, None, false)
906            .unwrap();
907
908        let stale_order = order.clone();
909
910        let canceled_event =
911            TestOrderEventStubs::canceled(&order, AccountId::from("ACCOUNT-001"), None);
912        order.apply(canceled_event).unwrap();
913        cache.borrow_mut().update_order(&order).unwrap();
914
915        assert!(cache.borrow().is_order_closed(&order.client_order_id()));
916
917        manager.submit_order_commands.insert(
918            stale_order.client_order_id(),
919            make_submit_command(&stale_order),
920        );
921
922        manager.cancel_order(&stale_order);
923
924        assert!(
925            manager
926                .submit_order_commands
927                .contains_key(&stale_order.client_order_id()),
928            "closed-order gate should short-circuit even when the passed reference is stale",
929        );
930    }
931}