1use 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
43pub 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 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 pub fn set_submit_order_handler(&mut self, handler: SubmitOrderHandlerAny) {
90 self.submit_order_handler = Some(handler);
91 }
92
93 pub fn set_cancel_order_handler(&mut self, handler: CancelOrderHandlerAny) {
95 self.cancel_order_handler = Some(handler);
96 }
97
98 pub fn set_modify_order_handler(&mut self, handler: ModifyOrderHandlerAny) {
100 self.modify_order_handler = Some(handler);
101 }
102
103 #[must_use]
104 pub fn get_submit_order_commands(&self) -> AHashMap<ClientOrderId, SubmitOrder> {
106 self.submit_order_commands.clone()
107 }
108
109 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 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 pub fn reset(&mut self) {
125 self.submit_order_commands.clear();
126 }
127
128 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 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 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, 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 pub fn should_manage_order(&self, order: &OrderAny) -> bool {
204 self.active_local && order.is_active_local()
205 }
206
207 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 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 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
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 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 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 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 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 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 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 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 #[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 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 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 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}