1use 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
33pub 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 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 pub fn get_submit_order_commands(&self) -> AHashMap<ClientOrderId, SubmitOrder> {
72 self.submit_order_commands.clone()
73 }
74
75 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 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 pub fn reset(&mut self) {
91 self.submit_order_commands.clear();
92 }
93
94 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 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 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, 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 pub fn should_manage_order(&self, order: &OrderAny) -> bool {
185 self.active_local && order.is_active_local()
186 }
187
188 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 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 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 #[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 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, )
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 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 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}