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