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,
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 oto_target_quantities: AHashMap<ClientOrderId, Quantity>,
45}
46
47impl Debug for OrderManager {
48 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49 f.debug_struct(stringify!(OrderManager))
50 .field("pending_commands", &self.submit_order_commands.len())
51 .finish()
52 }
53}
54
55impl OrderManager {
56 pub fn new(
58 clock: Rc<RefCell<dyn Clock>>,
59 cache: Rc<RefCell<Cache>>,
60 active_local: bool,
61 ) -> Self {
62 Self {
63 clock,
64 cache,
65 active_local,
66 submit_order_commands: AHashMap::new(),
67 oto_target_quantities: AHashMap::new(),
68 }
69 }
70
71 #[must_use]
72 pub fn get_submit_order_commands(&self) -> AHashMap<ClientOrderId, SubmitOrder> {
74 self.submit_order_commands.clone()
75 }
76
77 pub fn cache_submit_order_command(&mut self, command: SubmitOrder) {
79 self.submit_order_commands
80 .insert(command.client_order_id, command);
81 }
82
83 pub fn pop_submit_order_command(
85 &mut self,
86 client_order_id: ClientOrderId,
87 ) -> Option<SubmitOrder> {
88 self.submit_order_commands.remove(&client_order_id)
89 }
90
91 pub fn reset(&mut self) {
93 self.submit_order_commands.clear();
94 self.oto_target_quantities.clear();
95 }
96
97 pub fn cancel_order(&mut self, order: &OrderAny) -> Vec<OrderManagerAction> {
99 let client_order_id = order.client_order_id();
100 let cache = self.cache.borrow();
101
102 if cache.is_order_pending_cancel_local(&client_order_id) {
103 return Vec::new();
104 }
105
106 if order.is_closed() || cache.is_order_closed(&client_order_id) {
107 log::warn!("Cannot cancel order: already closed");
108 return Vec::new();
109 }
110
111 drop(cache);
112 self.submit_order_commands.remove(&client_order_id);
113
114 vec![OrderManagerAction::CancelLocal(order.clone())]
115 }
116
117 pub fn modify_order_quantity(
119 &mut self,
120 order: &OrderAny,
121 new_quantity: Quantity,
122 ) -> Vec<OrderManagerAction> {
123 vec![OrderManagerAction::ModifyLocalQuantity {
124 order: order.clone(),
125 quantity: new_quantity,
126 }]
127 }
128
129 pub fn create_new_submit_order(
133 &mut self,
134 order: &OrderAny,
135 position_id: Option<PositionId>,
136 client_id: Option<ClientId>,
137 correlation_id: Option<UUID4>,
138 ) -> anyhow::Result<Vec<OrderManagerAction>> {
139 let mut actions = Vec::new();
140 let order_exists = self.cache.borrow().order_exists(&order.client_order_id());
141
142 self.cache
143 .borrow_mut()
144 .add_order(order.clone(), position_id, client_id, true)?;
145
146 if !order_exists {
147 actions.push(initialized_action(order));
148 }
149
150 let submit = SubmitOrder::new(
151 order.trader_id(),
152 client_id,
153 order.strategy_id(),
154 order.instrument_id(),
155 order.client_order_id(),
156 order.init_event().clone(),
157 order.exec_algorithm_id(),
158 position_id,
159 None, UUID4::new(),
161 self.clock.borrow().timestamp_ns(),
162 correlation_id,
163 );
164
165 if order.emulation_trigger().is_some() {
166 self.cache_submit_order_command(submit.clone());
167 actions.push(OrderManagerAction::SubmitToEmulator(submit));
168 } else {
169 self.cache_submit_order_command(submit.clone());
170
171 match order.exec_algorithm_id() {
172 Some(exec_algorithm_id) => {
173 actions.push(OrderManagerAction::SubmitToAlgorithm {
174 command: submit,
175 exec_algorithm_id,
176 });
177 }
178 None => actions.push(OrderManagerAction::SubmitToRisk(submit)),
179 }
180 }
181
182 Ok(actions)
183 }
184
185 #[must_use]
186 pub fn should_manage_order(&self, order: &OrderAny) -> bool {
188 self.active_local == order.is_active_local()
189 }
190
191 pub fn handle_event(&mut self, event: &OrderEventAny) -> Vec<OrderManagerAction> {
196 match event {
197 OrderEventAny::Rejected(event) => self.handle_order_rejected(*event),
198 OrderEventAny::Canceled(event) => self.handle_order_canceled(*event),
199 OrderEventAny::Expired(event) => self.handle_order_expired(*event),
200 OrderEventAny::Updated(event) => self.handle_order_updated(*event),
201 OrderEventAny::ModifyRejected(event) => {
202 self.oto_target_quantities.remove(&event.client_order_id);
203 Vec::new()
204 }
205 OrderEventAny::Filled(event) => self.handle_order_filled(event),
206 _ => Vec::new(),
207 }
208 }
209
210 pub fn handle_order_rejected(&mut self, rejected: OrderRejected) -> Vec<OrderManagerAction> {
212 self.oto_target_quantities.remove(&rejected.client_order_id);
213 let Some(order) = self.cache.borrow().order_owned(&rejected.client_order_id) else {
214 log::error!(
215 "Cannot handle `OrderRejected`: order for client_order_id: {} not found, {}",
216 rejected.client_order_id,
217 rejected
218 );
219 return Vec::new();
220 };
221
222 if order.contingency_type().is_some() {
223 self.handle_contingencies(&order)
224 } else {
225 Vec::new()
226 }
227 }
228
229 pub fn handle_order_canceled(&mut self, canceled: OrderCanceled) -> Vec<OrderManagerAction> {
230 self.oto_target_quantities.remove(&canceled.client_order_id);
231 let Some(order) = self.cache.borrow().order_owned(&canceled.client_order_id) else {
232 log::error!(
233 "Cannot handle `OrderCanceled`: order for client_order_id: {} not found, {}",
234 canceled.client_order_id,
235 canceled
236 );
237 return Vec::new();
238 };
239
240 if order.contingency_type().is_some() {
241 self.handle_contingencies(&order)
242 } else {
243 Vec::new()
244 }
245 }
246
247 pub fn handle_order_expired(&mut self, expired: OrderExpired) -> Vec<OrderManagerAction> {
248 self.oto_target_quantities.remove(&expired.client_order_id);
249 let Some(order) = self.cache.borrow().order_owned(&expired.client_order_id) else {
250 log::error!(
251 "Cannot handle `OrderExpired`: order for client_order_id: {} not found, {}",
252 expired.client_order_id,
253 expired
254 );
255 return Vec::new();
256 };
257
258 if order.contingency_type().is_some() {
259 self.handle_contingencies(&order)
260 } else {
261 Vec::new()
262 }
263 }
264
265 pub fn handle_order_updated(&mut self, updated: OrderUpdated) -> Vec<OrderManagerAction> {
266 let Some(order) = self.cache.borrow().order_owned(&updated.client_order_id) else {
267 log::error!(
268 "Cannot handle `OrderUpdated`: order for client_order_id: {} not found, {}",
269 updated.client_order_id,
270 updated
271 );
272 return Vec::new();
273 };
274
275 let mut actions = Vec::new();
276
277 if self.should_manage_order(&order) && !order.is_closed() {
278 if let Some(quantity) = self
279 .oto_target_quantities
280 .get(&updated.client_order_id)
281 .copied()
282 .filter(|quantity| *quantity != order.quantity())
283 .filter(|quantity| order.filled_qty() < *quantity)
284 {
285 actions.extend(self.modify_order_quantity(&order, quantity));
286 }
287 } else {
288 self.oto_target_quantities.remove(&updated.client_order_id);
289 }
290
291 if order.contingency_type().is_some() {
292 actions.extend(self.handle_contingencies_update(&order));
293 }
294
295 actions
296 }
297
298 pub fn handle_order_filled(&mut self, filled: &OrderFilled) -> Vec<OrderManagerAction> {
299 let Some(order) = self.cache.borrow().order_owned(&filled.client_order_id) else {
300 log::error!(
301 "Cannot handle `OrderFilled`: order for client_order_id: {} not found, {}",
302 filled.client_order_id,
303 filled
304 );
305 return Vec::new();
306 };
307
308 let mut actions = Vec::new();
309
310 if !self.should_manage_order(&order) || order.is_closed() {
311 self.oto_target_quantities.remove(&filled.client_order_id);
312 }
313
314 match order.contingency_type() {
315 Some(ContingencyType::Oto) => {
316 let position_id = self
317 .cache
318 .borrow()
319 .position_id(&order.client_order_id())
320 .copied();
321 let client_id = self
322 .cache
323 .borrow()
324 .client_id(&order.client_order_id())
325 .copied();
326
327 let parent_filled_qty = match order.exec_spawn_id() {
328 Some(spawn_id) => {
329 if let Some(qty) = self
330 .cache
331 .borrow()
332 .exec_spawn_total_filled_qty(&spawn_id, false)
333 {
334 qty
335 } else {
336 log::error!("Failed to get spawn filled quantity for {spawn_id}");
337 return actions;
338 }
339 }
340 None => order.filled_qty(),
341 };
342
343 let Some(linked_orders) = order.linked_order_ids() else {
344 log::error!("No linked orders found for OTO order");
345 return actions;
346 };
347
348 for client_order_id in linked_orders {
349 if order.parent_order_id().as_ref() == Some(client_order_id) {
350 continue;
351 }
352
353 let Some(mut child_order) = self.cache.borrow().order_owned(client_order_id)
354 else {
355 log::error!(
356 "Cannot find OTO child order for client_order_id: {client_order_id}"
357 );
358 continue;
359 };
360
361 if !self.should_manage_order(&child_order) || child_order.is_closed() {
362 self.oto_target_quantities.remove(client_order_id);
363 continue;
364 }
365
366 if self.active_local && child_order.position_id().is_none() {
367 child_order.set_position_id(position_id);
368 }
369
370 actions.extend(self.sync_oto_quantity(&child_order, parent_filled_qty));
371
372 if self.active_local
373 && !self
374 .submit_order_commands
375 .contains_key(&child_order.client_order_id())
376 {
377 match self.create_new_submit_order(
378 &child_order,
379 position_id,
380 client_id,
381 None,
382 ) {
383 Ok(new_actions) => actions.extend(new_actions),
384 Err(e) => log::error!("Failed to create new submit order: {e}"),
385 }
386 }
387 }
388 }
389 Some(ContingencyType::Oco) => {
390 let Some(linked_orders) = order.linked_order_ids() else {
391 log::error!("No linked orders found for OCO order");
392 return actions;
393 };
394
395 for client_order_id in linked_orders {
396 let Some(contingent_order) = self.cache.borrow().order_owned(client_order_id)
397 else {
398 log::error!(
399 "Cannot find OCO contingent order for client_order_id: {client_order_id}"
400 );
401 continue;
402 };
403
404 if !self.should_manage_order(&contingent_order) || contingent_order.is_closed()
406 {
407 continue;
408 }
409
410 if contingent_order.client_order_id() != order.client_order_id() {
411 actions.extend(self.cancel_order(&contingent_order));
412 }
413 }
414 }
415 Some(ContingencyType::Ouo) => actions.extend(self.handle_contingencies(&order)),
416 _ => {}
417 }
418
419 actions
420 }
421
422 pub fn handle_contingencies(&mut self, order: &OrderAny) -> Vec<OrderManagerAction> {
423 let mut actions = Vec::new();
424 let (filled_qty, leaves_qty, is_spawn_active) =
425 if let Some(exec_spawn_id) = order.exec_spawn_id() {
426 if let (Some(filled), Some(leaves)) = (
427 self.cache
428 .borrow()
429 .exec_spawn_total_filled_qty(&exec_spawn_id, false),
430 self.cache
431 .borrow()
432 .exec_spawn_total_leaves_qty(&exec_spawn_id, true),
433 ) {
434 (filled, leaves, leaves.raw > 0)
435 } else {
436 log::error!("Failed to get spawn quantities for {exec_spawn_id}");
437 return actions;
438 }
439 } else {
440 (order.filled_qty(), order.leaves_qty(), false)
441 };
442
443 let Some(linked_orders) = order.linked_order_ids() else {
444 log::error!("No linked orders found");
445 return actions;
446 };
447
448 for client_order_id in linked_orders {
449 if order.contingency_type() == Some(ContingencyType::Oto)
450 && order.parent_order_id().as_ref() == Some(client_order_id)
451 {
452 continue;
453 }
454
455 let Some(contingent_order) = self.cache.borrow().order_owned(client_order_id) else {
456 log::error!("Cannot find contingent order for client_order_id: {client_order_id}");
457 continue;
458 };
459
460 if !self.should_manage_order(&contingent_order) {
461 self.oto_target_quantities.remove(client_order_id);
462 continue;
463 }
464
465 if client_order_id == &order.client_order_id() {
466 continue;
467 }
468
469 if contingent_order.is_closed() {
470 self.submit_order_commands.remove(client_order_id);
471 self.oto_target_quantities.remove(client_order_id);
472 continue;
473 }
474
475 match order.contingency_type() {
476 Some(ContingencyType::Oto) => {
477 if order.is_closed()
478 && filled_qty.raw == 0
479 && (order.exec_spawn_id().is_none() || !is_spawn_active)
480 {
481 actions.extend(self.cancel_order(&contingent_order));
482 } else if filled_qty.raw > 0 {
483 actions.extend(self.sync_oto_quantity(&contingent_order, filled_qty));
484 }
485 }
486 Some(ContingencyType::Oco)
487 if order.is_closed()
488 && (order.exec_spawn_id().is_none() || !is_spawn_active) =>
489 {
490 actions.extend(self.cancel_order(&contingent_order));
491 }
492 Some(ContingencyType::Ouo) => {
493 if (leaves_qty.raw == 0 && order.exec_spawn_id().is_some())
494 || (order.is_closed()
495 && (order.exec_spawn_id().is_none() || !is_spawn_active))
496 || contingent_order.filled_qty() >= leaves_qty
497 {
498 actions.extend(self.cancel_order(&contingent_order));
499 } else if leaves_qty != contingent_order.leaves_qty() {
500 actions.extend(self.modify_order_quantity(&contingent_order, leaves_qty));
501 }
502 }
503 _ => {}
504 }
505 }
506
507 actions
508 }
509
510 pub fn handle_contingencies_update(&mut self, order: &OrderAny) -> Vec<OrderManagerAction> {
511 let mut actions = Vec::new();
512 let contingency_type = order.contingency_type();
513 let quantity = match order.exec_spawn_id() {
514 Some(exec_spawn_id) => {
515 if let Some(qty) = self
516 .cache
517 .borrow()
518 .exec_spawn_total_quantity(&exec_spawn_id, true)
519 {
520 qty
521 } else {
522 log::error!("Failed to get spawn total quantity for {exec_spawn_id}");
523 return actions;
524 }
525 }
526 None => order.quantity(),
527 };
528
529 if quantity.raw == 0 {
530 return actions;
531 }
532
533 let oto_quantity = if contingency_type == Some(ContingencyType::Oto) {
534 let filled_qty = match order.exec_spawn_id() {
535 Some(exec_spawn_id) => {
536 if let Some(qty) = self
537 .cache
538 .borrow()
539 .exec_spawn_total_filled_qty(&exec_spawn_id, false)
540 {
541 qty
542 } else {
543 log::error!("Failed to get spawn filled quantity for {exec_spawn_id}");
544 return actions;
545 }
546 }
547 None => order.filled_qty(),
548 };
549
550 if filled_qty.raw > 0 {
551 filled_qty
552 } else {
553 quantity
554 }
555 } else {
556 quantity
557 };
558
559 let Some(linked_orders) = order.linked_order_ids() else {
560 log::error!("No linked orders found for contingent order");
561 return actions;
562 };
563
564 for client_order_id in linked_orders {
565 if contingency_type == Some(ContingencyType::Oto)
566 && order.parent_order_id().as_ref() == Some(client_order_id)
567 {
568 continue;
569 }
570
571 let Some(contingent_order) = self.cache.borrow().order_owned(client_order_id) else {
572 log::error!(
573 "Cannot find OCO contingent order for client_order_id: {client_order_id}"
574 );
575 continue;
576 };
577
578 if !self.should_manage_order(&contingent_order) {
579 self.oto_target_quantities.remove(client_order_id);
580 continue;
581 }
582
583 if client_order_id == &order.client_order_id() || contingent_order.is_closed() {
584 self.oto_target_quantities.remove(client_order_id);
585 continue;
586 }
587
588 match contingency_type {
589 Some(ContingencyType::Oto) => {
590 actions.extend(self.sync_oto_quantity(&contingent_order, oto_quantity));
591 }
592 Some(ContingencyType::Ouo) if contingent_order.filled_qty() >= quantity => {
593 actions.extend(self.cancel_order(&contingent_order));
594 }
595 Some(ContingencyType::Ouo) if quantity != contingent_order.quantity() => {
596 actions.extend(self.modify_order_quantity(&contingent_order, quantity));
597 }
598 _ => {}
599 }
600 }
601
602 actions
603 }
604
605 fn sync_oto_quantity(
606 &mut self,
607 order: &OrderAny,
608 quantity: Quantity,
609 ) -> Vec<OrderManagerAction> {
610 let client_order_id = order.client_order_id();
611
612 if order.filled_qty() >= quantity {
613 self.oto_target_quantities.remove(&client_order_id);
614 return self.cancel_order(order);
615 }
616
617 match self.oto_target_quantities.get(&client_order_id).copied() {
618 Some(target) if target == quantity => return Vec::new(),
619 Some(_) if order.quantity() == quantity && !order.is_pending_update() => {
620 self.oto_target_quantities.insert(client_order_id, quantity);
621 return Vec::new();
622 }
623 Some(_) => {}
624 None if order.quantity() == quantity => return Vec::new(),
625 None => {}
626 }
627
628 self.oto_target_quantities.insert(client_order_id, quantity);
629 self.modify_order_quantity(order, quantity)
630 }
631}
632
633fn initialized_action(order: &OrderAny) -> OrderManagerAction {
634 let event = OrderEventAny::Initialized(order.init_event().clone());
635 OrderManagerAction::PublishInitialized(event)
636}
637
638#[cfg(test)]
639mod tests {
640 use std::{cell::RefCell, rc::Rc};
641
642 use nautilus_common::{cache::Cache, clock::TestClock};
643 use nautilus_core::{UUID4, UnixNanos};
644 use nautilus_model::{
645 enums::{ContingencyType, OrderSide, OrderType, TriggerType},
646 events::order::spec::{
647 OrderAcceptedSpec, OrderCanceledSpec, OrderExpiredSpec, OrderModifyRejectedSpec,
648 OrderPendingUpdateSpec, OrderRejectedSpec, OrderSubmittedSpec, OrderUpdatedSpec,
649 },
650 identifiers::{
651 AccountId, ClientOrderId, ExecAlgorithmId, InstrumentId, StrategyId, TradeId, TraderId,
652 VenueOrderId,
653 },
654 instruments::{Instrument, InstrumentAny, stubs::audusd_sim},
655 orders::{Order, OrderTestBuilder, stubs::TestOrderEventStubs},
656 types::{Price, Quantity},
657 };
658 use rstest::rstest;
659
660 use super::*;
661
662 #[rstest]
665 fn test_handle_event_unhandled_events_are_noop() {
666 let submitted = OrderEventAny::Submitted(
667 OrderSubmittedSpec::builder()
668 .trader_id(TraderId::from("TRADER-001"))
669 .strategy_id(StrategyId::from("STRATEGY-001"))
670 .instrument_id(InstrumentId::from("BTC-USDT.OKX"))
671 .client_order_id(ClientOrderId::from("O-001"))
672 .account_id(AccountId::from("ACCOUNT-001"))
673 .build(),
674 );
675 let accepted = OrderEventAny::Accepted(
676 OrderAcceptedSpec::builder()
677 .trader_id(TraderId::from("TRADER-001"))
678 .strategy_id(StrategyId::from("STRATEGY-001"))
679 .instrument_id(InstrumentId::from("BTC-USDT.OKX"))
680 .client_order_id(ClientOrderId::from("O-001"))
681 .venue_order_id(VenueOrderId::from("V-001"))
682 .account_id(AccountId::from("ACCOUNT-001"))
683 .build(),
684 );
685
686 match submitted {
687 OrderEventAny::Rejected(_) => panic!("Should not match"),
688 OrderEventAny::Canceled(_) => panic!("Should not match"),
689 OrderEventAny::Expired(_) => panic!("Should not match"),
690 OrderEventAny::Updated(_) => panic!("Should not match"),
691 OrderEventAny::Filled(_) => panic!("Should not match"),
692 _ => {}
693 }
694
695 match accepted {
696 OrderEventAny::Rejected(_) => panic!("Should not match"),
697 OrderEventAny::Canceled(_) => panic!("Should not match"),
698 OrderEventAny::Expired(_) => panic!("Should not match"),
699 OrderEventAny::Updated(_) => panic!("Should not match"),
700 OrderEventAny::Filled(_) => panic!("Should not match"),
701 _ => {}
702 }
703 }
704
705 #[rstest]
706 fn test_actionable_events_for_missing_orders_are_noops() {
707 let (clock, cache) = create_test_components();
708 let mut manager = OrderManager::new(clock, cache, false);
709 let trader_id = TraderId::from("TRADER-001");
710 let strategy_id = StrategyId::from("STRATEGY-001");
711 let instrument_id = InstrumentId::from("BTC-USDT.OKX");
712 let client_order_id = ClientOrderId::from("O-MISSING");
713 let account_id = AccountId::from("ACCOUNT-001");
714 let events = [
715 OrderEventAny::Rejected(
716 OrderRejectedSpec::builder()
717 .trader_id(trader_id)
718 .strategy_id(strategy_id)
719 .instrument_id(instrument_id)
720 .client_order_id(client_order_id)
721 .account_id(account_id)
722 .reason("test rejection".into())
723 .build(),
724 ),
725 OrderEventAny::Canceled(
726 OrderCanceledSpec::builder()
727 .trader_id(trader_id)
728 .strategy_id(strategy_id)
729 .instrument_id(instrument_id)
730 .client_order_id(client_order_id)
731 .account_id(account_id)
732 .build(),
733 ),
734 OrderEventAny::Expired(
735 OrderExpiredSpec::builder()
736 .trader_id(trader_id)
737 .strategy_id(strategy_id)
738 .instrument_id(instrument_id)
739 .client_order_id(client_order_id)
740 .account_id(account_id)
741 .build(),
742 ),
743 OrderEventAny::Updated(
744 OrderUpdatedSpec::builder()
745 .trader_id(trader_id)
746 .strategy_id(strategy_id)
747 .instrument_id(instrument_id)
748 .client_order_id(client_order_id)
749 .quantity(Quantity::from(100_000))
750 .venue_order_id(VenueOrderId::from("V-MISSING"))
751 .account_id(account_id)
752 .build(),
753 ),
754 ];
755
756 for event in events {
757 assert!(manager.handle_event(&event).is_empty());
758 }
759 }
760
761 fn create_test_components() -> (Rc<RefCell<dyn Clock>>, Rc<RefCell<Cache>>) {
762 let clock: Rc<RefCell<dyn Clock>> = Rc::new(RefCell::new(TestClock::new()));
763 let cache = Rc::new(RefCell::new(Cache::new(None, None)));
764 (clock, cache)
765 }
766
767 fn create_test_stop_order() -> OrderAny {
768 let instrument = audusd_sim();
769 OrderTestBuilder::new(OrderType::StopMarket)
770 .instrument_id(instrument.id())
771 .side(OrderSide::Buy)
772 .trigger_price(Price::from("1.00050"))
773 .quantity(Quantity::from(100_000))
774 .emulation_trigger(TriggerType::BidAsk)
775 .build()
776 }
777
778 fn make_submit_command(order: &OrderAny) -> SubmitOrder {
781 SubmitOrder::new(
782 order.trader_id(),
783 None,
784 order.strategy_id(),
785 order.instrument_id(),
786 order.client_order_id(),
787 order.init_event().clone(),
788 None,
789 None,
790 None,
791 UUID4::new(),
792 UnixNanos::default(),
793 None, )
795 }
796
797 #[rstest]
798 fn test_create_new_submit_order_returns_emulator_submit_action() {
799 let (clock, cache) = create_test_components();
800 let mut manager = OrderManager::new(clock, cache, true);
801 let order = create_test_stop_order();
802
803 let actions = manager
804 .create_new_submit_order(&order, None, None, None)
805 .unwrap();
806
807 assert_eq!(actions.len(), 2);
808 assert!(matches!(
809 &actions[0],
810 OrderManagerAction::PublishInitialized(OrderEventAny::Initialized(event))
811 if event.client_order_id == order.client_order_id()
812 ));
813 assert!(matches!(
814 &actions[1],
815 OrderManagerAction::SubmitToEmulator(command)
816 if command.client_order_id == order.client_order_id()
817 ));
818 assert!(
819 manager
820 .submit_order_commands
821 .contains_key(&order.client_order_id())
822 );
823 }
824
825 #[rstest]
826 fn test_reset_clears_submit_commands_and_oto_targets() {
827 let (clock, cache) = create_test_components();
828 let mut manager = OrderManager::new(clock, cache, true);
829 let order = create_test_stop_order();
830 let client_order_id = order.client_order_id();
831 manager.cache_submit_order_command(make_submit_command(&order));
832 manager
833 .oto_target_quantities
834 .insert(client_order_id, Quantity::from(40_000));
835
836 manager.reset();
837
838 assert!(manager.submit_order_commands.is_empty());
839 assert!(manager.oto_target_quantities.is_empty());
840 }
841
842 #[rstest]
843 fn test_create_new_submit_order_returns_risk_submit_action() {
844 let (clock, cache) = create_test_components();
845 let mut manager = OrderManager::new(clock, cache, true);
846 let order = OrderTestBuilder::new(OrderType::Limit)
847 .instrument_id(audusd_sim().id())
848 .side(OrderSide::Buy)
849 .price(Price::from("1.00000"))
850 .quantity(Quantity::from(100_000))
851 .build();
852
853 let actions = manager
854 .create_new_submit_order(&order, None, None, None)
855 .unwrap();
856
857 assert_eq!(actions.len(), 2);
858 assert!(matches!(
859 &actions[0],
860 OrderManagerAction::PublishInitialized(OrderEventAny::Initialized(event))
861 if event.client_order_id == order.client_order_id()
862 ));
863 assert!(matches!(
864 &actions[1],
865 OrderManagerAction::SubmitToRisk(command)
866 if command.client_order_id == order.client_order_id()
867 ));
868 }
869
870 #[rstest]
871 fn test_create_new_submit_order_returns_risk_action_for_none_trigger() {
872 let (clock, cache) = create_test_components();
873 let mut manager = OrderManager::new(clock, cache, true);
874 let order = OrderTestBuilder::new(OrderType::Market)
875 .instrument_id(audusd_sim().id())
876 .side(OrderSide::Buy)
877 .quantity(Quantity::from(100_000))
878 .build();
879
880 let actions = manager
881 .create_new_submit_order(&order, None, None, None)
882 .unwrap();
883
884 assert_eq!(actions.len(), 2);
885 assert!(order.emulation_trigger().is_none());
886 assert!(matches!(
887 &actions[1],
888 OrderManagerAction::SubmitToRisk(command)
889 if command.client_order_id == order.client_order_id()
890 ));
891 }
892
893 #[rstest]
894 fn test_create_new_submit_order_returns_algorithm_submit_action() {
895 let (clock, cache) = create_test_components();
896 let mut manager = OrderManager::new(clock, cache, true);
897 let exec_algorithm_id = ExecAlgorithmId::from("ALG-001");
898 let client_order_id = ClientOrderId::from("O-001");
899 let order = OrderTestBuilder::new(OrderType::Limit)
900 .client_order_id(client_order_id)
901 .instrument_id(audusd_sim().id())
902 .side(OrderSide::Buy)
903 .price(Price::from("1.00000"))
904 .quantity(Quantity::from(100_000))
905 .exec_algorithm_id(exec_algorithm_id)
906 .exec_spawn_id(client_order_id)
907 .build();
908
909 let actions = manager
910 .create_new_submit_order(&order, None, None, None)
911 .unwrap();
912
913 assert_eq!(actions.len(), 2);
914 assert!(matches!(
915 &actions[1],
916 OrderManagerAction::SubmitToAlgorithm {
917 command,
918 exec_algorithm_id: action_exec_algorithm_id,
919 } if command.client_order_id == order.client_order_id()
920 && *action_exec_algorithm_id == exec_algorithm_id
921 ));
922 }
923
924 #[rstest]
925 fn test_create_new_submit_order_does_not_republish_initialized_for_existing_order() {
926 let (clock, cache) = create_test_components();
927 let mut manager = OrderManager::new(clock, cache.clone(), true);
928 let order = create_test_stop_order();
929 cache
930 .borrow_mut()
931 .add_order(order.clone(), None, None, true)
932 .unwrap();
933
934 let actions = manager
935 .create_new_submit_order(&order, None, None, None)
936 .unwrap();
937
938 assert_eq!(actions.len(), 1);
939 assert!(matches!(
940 &actions[0],
941 OrderManagerAction::SubmitToEmulator(command)
942 if command.client_order_id == order.client_order_id()
943 ));
944 }
945
946 #[rstest]
947 fn test_cancel_order_returns_cancel_local_action() {
948 let (clock, cache) = create_test_components();
949 let mut manager = OrderManager::new(clock, cache.clone(), true);
950 let order = create_test_stop_order();
951 cache
952 .borrow_mut()
953 .add_order(order.clone(), None, None, false)
954 .unwrap();
955 manager
956 .submit_order_commands
957 .insert(order.client_order_id(), make_submit_command(&order));
958
959 let actions = manager.cancel_order(&order);
960
961 assert!(matches!(
962 actions.as_slice(),
963 [OrderManagerAction::CancelLocal(action_order)]
964 if action_order.client_order_id() == order.client_order_id()
965 ));
966 assert!(
967 !manager
968 .submit_order_commands
969 .contains_key(&order.client_order_id()),
970 "expected cancel action path to remove the submit command",
971 );
972 }
973
974 #[rstest]
975 fn test_modify_order_quantity_returns_modify_local_action() {
976 let (clock, cache) = create_test_components();
977 let mut manager = OrderManager::new(clock, cache, true);
978 let order = create_test_stop_order();
979 let new_quantity = Quantity::from(50_000);
980
981 let actions = manager.modify_order_quantity(&order, new_quantity);
982
983 assert!(matches!(
984 actions.as_slice(),
985 [OrderManagerAction::ModifyLocalQuantity { order: action_order, quantity }]
986 if action_order.client_order_id() == order.client_order_id()
987 && *quantity == new_quantity
988 ));
989 }
990
991 #[rstest]
992 fn test_sync_oto_quantity_reasserts_same_quantity_while_pending_update() {
993 let (clock, cache) = create_test_components();
994 let mut manager = OrderManager::new(clock, cache.clone(), false);
995 let client_order_id = ClientOrderId::from("O-CHILD");
996 let order = OrderTestBuilder::new(OrderType::Limit)
997 .instrument_id(audusd_sim().id())
998 .client_order_id(client_order_id)
999 .side(OrderSide::Sell)
1000 .price(Price::from("1.00100"))
1001 .quantity(Quantity::from(100_000))
1002 .submit(true)
1003 .build();
1004 cache
1005 .borrow_mut()
1006 .add_order(order.clone(), None, None, false)
1007 .unwrap();
1008 apply_accepted(&cache, &order, "V-CHILD");
1009 let pending_update = OrderEventAny::PendingUpdate(
1010 OrderPendingUpdateSpec::builder()
1011 .trader_id(order.trader_id())
1012 .strategy_id(order.strategy_id())
1013 .instrument_id(order.instrument_id())
1014 .client_order_id(client_order_id)
1015 .account_id(AccountId::from("ACCOUNT-001"))
1016 .venue_order_id(VenueOrderId::from("V-CHILD"))
1017 .build(),
1018 );
1019 cache.borrow_mut().update_order(&pending_update).unwrap();
1020 manager
1021 .oto_target_quantities
1022 .insert(client_order_id, Quantity::from(40_000));
1023 let cached_order = cache.borrow().order_owned(&client_order_id).unwrap();
1024
1025 let actions = manager.sync_oto_quantity(&cached_order, Quantity::from(100_000));
1026
1027 assert!(matches!(
1028 actions.as_slice(),
1029 [OrderManagerAction::ModifyLocalQuantity { order, quantity }]
1030 if order.client_order_id() == client_order_id
1031 && *quantity == Quantity::from(100_000)
1032 ));
1033 assert_eq!(
1034 manager.oto_target_quantities.get(&client_order_id),
1035 Some(&Quantity::from(100_000)),
1036 );
1037 }
1038
1039 #[rstest]
1040 fn test_handle_event_unhandled_events_return_no_actions() {
1041 let (clock, cache) = create_test_components();
1042 let mut manager = OrderManager::new(clock, cache, true);
1043 let order = create_test_stop_order();
1044 let event = OrderEventAny::Submitted(
1045 OrderSubmittedSpec::builder()
1046 .trader_id(order.trader_id())
1047 .strategy_id(order.strategy_id())
1048 .instrument_id(order.instrument_id())
1049 .client_order_id(order.client_order_id())
1050 .account_id(AccountId::from("ACCOUNT-001"))
1051 .build(),
1052 );
1053
1054 let actions = manager.handle_event(&event);
1055
1056 assert!(actions.is_empty());
1057 }
1058
1059 #[rstest]
1060 fn test_handle_order_filled_skips_missing_oco_contingent_order() {
1061 let (clock, cache) = create_test_components();
1062 let mut manager = OrderManager::new(clock, cache.clone(), true);
1063 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
1064 let missing_client_order_id = ClientOrderId::from("O-MISSING");
1065 let valid_client_order_id = ClientOrderId::from("O-CHILD");
1066 let order = OrderTestBuilder::new(OrderType::Limit)
1067 .instrument_id(instrument.id())
1068 .client_order_id(ClientOrderId::from("O-PARENT"))
1069 .side(OrderSide::Buy)
1070 .price(Price::from("1.00000"))
1071 .quantity(Quantity::from(100_000))
1072 .contingency_type(ContingencyType::Oco)
1073 .linked_order_ids(vec![missing_client_order_id, valid_client_order_id])
1074 .build();
1075 let child_order = OrderTestBuilder::new(OrderType::Limit)
1076 .instrument_id(instrument.id())
1077 .client_order_id(valid_client_order_id)
1078 .side(OrderSide::Buy)
1079 .price(Price::from("1.00000"))
1080 .quantity(Quantity::from(100_000))
1081 .build();
1082 cache
1083 .borrow_mut()
1084 .add_order(order.clone(), None, None, false)
1085 .unwrap();
1086 cache
1087 .borrow_mut()
1088 .add_order(child_order.clone(), None, None, false)
1089 .unwrap();
1090 manager
1091 .submit_order_commands
1092 .insert(valid_client_order_id, make_submit_command(&child_order));
1093 let filled = match TestOrderEventStubs::filled(
1094 &order,
1095 &instrument,
1096 None,
1097 None,
1098 None,
1099 None,
1100 None,
1101 None,
1102 None,
1103 Some(AccountId::from("SIM-001")),
1104 ) {
1105 OrderEventAny::Filled(event) => event,
1106 event => panic!("expected OrderFilled, was {event:?}"),
1107 };
1108
1109 let actions = manager.handle_order_filled(&filled);
1110
1111 assert!(matches!(
1112 actions.as_slice(),
1113 [OrderManagerAction::CancelLocal(action_order)]
1114 if action_order.client_order_id() == valid_client_order_id
1115 ));
1116 assert!(
1117 !manager
1118 .submit_order_commands
1119 .contains_key(&valid_client_order_id)
1120 );
1121 }
1122
1123 #[rstest]
1124 fn test_handle_event_non_active_local_manager_returns_oco_cancel_action() {
1125 let (clock, cache) = create_test_components();
1126 let mut manager = OrderManager::new(clock, cache.clone(), false);
1127 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
1128 let parent_id = ClientOrderId::from("O-PARENT");
1129 let child_id = ClientOrderId::from("O-CHILD");
1130 let order = OrderTestBuilder::new(OrderType::Limit)
1131 .instrument_id(instrument.id())
1132 .client_order_id(parent_id)
1133 .side(OrderSide::Buy)
1134 .price(Price::from("1.00000"))
1135 .quantity(Quantity::from(100_000))
1136 .contingency_type(ContingencyType::Oco)
1137 .linked_order_ids(vec![parent_id, child_id])
1138 .submit(true)
1139 .build();
1140 let child_order = OrderTestBuilder::new(OrderType::Limit)
1141 .instrument_id(instrument.id())
1142 .client_order_id(child_id)
1143 .side(OrderSide::Buy)
1144 .price(Price::from("1.00000"))
1145 .quantity(Quantity::from(100_000))
1146 .submit(true)
1147 .build();
1148 cache
1149 .borrow_mut()
1150 .add_order(order.clone(), None, None, false)
1151 .unwrap();
1152 cache
1153 .borrow_mut()
1154 .add_order(child_order.clone(), None, None, false)
1155 .unwrap();
1156 manager
1157 .submit_order_commands
1158 .insert(child_id, make_submit_command(&child_order));
1159 let event = TestOrderEventStubs::filled(
1160 &order,
1161 &instrument,
1162 None,
1163 None,
1164 None,
1165 None,
1166 None,
1167 None,
1168 None,
1169 Some(AccountId::from("SIM-001")),
1170 );
1171
1172 let actions = manager.handle_event(&event);
1173
1174 assert!(matches!(
1175 actions.as_slice(),
1176 [OrderManagerAction::CancelLocal(order)]
1177 if order.client_order_id() == child_id
1178 ));
1179 assert!(
1180 !manager.submit_order_commands.contains_key(&child_id),
1181 "cancel action must remove the cached submit command",
1182 );
1183 }
1184
1185 #[rstest]
1186 fn test_handle_order_filled_skips_missing_oto_child_order() {
1187 let (clock, cache) = create_test_components();
1188 let mut manager = OrderManager::new(clock, cache.clone(), true);
1189 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
1190 let missing_client_order_id = ClientOrderId::from("O-MISSING");
1191 let valid_client_order_id = ClientOrderId::from("O-CHILD");
1192 let order = OrderTestBuilder::new(OrderType::Limit)
1193 .instrument_id(instrument.id())
1194 .client_order_id(ClientOrderId::from("O-PARENT"))
1195 .side(OrderSide::Buy)
1196 .price(Price::from("1.00000"))
1197 .quantity(Quantity::from(100_000))
1198 .contingency_type(ContingencyType::Oto)
1199 .linked_order_ids(vec![missing_client_order_id, valid_client_order_id])
1200 .submit(true)
1201 .build();
1202 let child_order = OrderTestBuilder::new(OrderType::Limit)
1203 .instrument_id(instrument.id())
1204 .client_order_id(valid_client_order_id)
1205 .side(OrderSide::Buy)
1206 .price(Price::from("1.00000"))
1207 .quantity(Quantity::from(100_000))
1208 .build();
1209 cache
1210 .borrow_mut()
1211 .add_order(order.clone(), None, None, false)
1212 .unwrap();
1213 cache
1214 .borrow_mut()
1215 .add_order(child_order, None, None, false)
1216 .unwrap();
1217 let filled = match apply_fill(
1218 &cache,
1219 &order,
1220 &instrument,
1221 "T-PARTIAL",
1222 Quantity::from(40_000),
1223 ) {
1224 OrderEventAny::Filled(event) => event,
1225 event => panic!("expected OrderFilled, was {event:?}"),
1226 };
1227
1228 let actions = manager.handle_order_filled(&filled);
1229
1230 assert_eq!(actions.len(), 2);
1231 assert!(matches!(
1232 &actions[0],
1233 OrderManagerAction::ModifyLocalQuantity { order, quantity }
1234 if order.client_order_id() == valid_client_order_id
1235 && *quantity == Quantity::from(40_000)
1236 ));
1237 assert!(matches!(
1238 &actions[1],
1239 OrderManagerAction::SubmitToRisk(command)
1240 if command.client_order_id == valid_client_order_id
1241 ));
1242 assert!(
1243 manager
1244 .submit_order_commands
1245 .contains_key(&valid_client_order_id)
1246 );
1247 }
1248
1249 #[rstest]
1250 fn test_handle_contingencies_skips_missing_linked_order() {
1251 let (clock, cache) = create_test_components();
1252 let mut manager = OrderManager::new(clock, cache, true);
1253 let instrument = audusd_sim();
1254 let order = OrderTestBuilder::new(OrderType::Limit)
1255 .instrument_id(instrument.id())
1256 .client_order_id(ClientOrderId::from("O-PARENT"))
1257 .side(OrderSide::Buy)
1258 .price(Price::from("1.00000"))
1259 .quantity(Quantity::from(100_000))
1260 .contingency_type(ContingencyType::Oco)
1261 .linked_order_ids(vec![ClientOrderId::from("O-MISSING")])
1262 .build();
1263
1264 let actions = manager.handle_contingencies(&order);
1265
1266 assert!(actions.is_empty());
1267 assert!(manager.submit_order_commands.is_empty());
1268 }
1269
1270 #[rstest]
1271 fn test_handle_contingencies_update_skips_missing_linked_order() {
1272 let (clock, cache) = create_test_components();
1273 let mut manager = OrderManager::new(clock, cache, true);
1274 let instrument = audusd_sim();
1275 let order = OrderTestBuilder::new(OrderType::Limit)
1276 .instrument_id(instrument.id())
1277 .client_order_id(ClientOrderId::from("O-PARENT"))
1278 .side(OrderSide::Buy)
1279 .price(Price::from("1.00000"))
1280 .quantity(Quantity::from(100_000))
1281 .contingency_type(ContingencyType::Oco)
1282 .linked_order_ids(vec![ClientOrderId::from("O-MISSING")])
1283 .build();
1284
1285 let actions = manager.handle_contingencies_update(&order);
1286
1287 assert!(actions.is_empty());
1288 assert!(manager.submit_order_commands.is_empty());
1289 }
1290
1291 #[rstest]
1292 fn test_oto_child_contingency_handlers_do_not_drive_parent() {
1293 let (clock, cache) = create_test_components();
1294 let mut terminal_manager = OrderManager::new(clock.clone(), cache.clone(), false);
1295 let mut update_manager = OrderManager::new(clock, cache.clone(), false);
1296 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
1297 let parent_id = ClientOrderId::from("O-PARENT");
1298 let child_id = ClientOrderId::from("O-CHILD");
1299 let parent = OrderTestBuilder::new(OrderType::Limit)
1300 .instrument_id(instrument.id())
1301 .client_order_id(parent_id)
1302 .side(OrderSide::Buy)
1303 .price(Price::from("1.00000"))
1304 .quantity(Quantity::from(100_000))
1305 .contingency_type(ContingencyType::Oto)
1306 .linked_order_ids(vec![child_id])
1307 .submit(true)
1308 .build();
1309 let child = OrderTestBuilder::new(OrderType::Limit)
1310 .instrument_id(instrument.id())
1311 .client_order_id(child_id)
1312 .side(OrderSide::Sell)
1313 .price(Price::from("1.00100"))
1314 .quantity(Quantity::from(100_000))
1315 .contingency_type(ContingencyType::Oto)
1316 .linked_order_ids(vec![parent_id])
1317 .parent_order_id(parent_id)
1318 .submit(true)
1319 .build();
1320 cache
1321 .borrow_mut()
1322 .add_order(parent.clone(), None, None, false)
1323 .unwrap();
1324 cache
1325 .borrow_mut()
1326 .add_order(child.clone(), None, None, false)
1327 .unwrap();
1328 apply_accepted(&cache, &parent, "V-PARENT");
1329 apply_accepted(&cache, &child, "V-CHILD");
1330 apply_fill(
1331 &cache,
1332 &child,
1333 &instrument,
1334 "T-CHILD",
1335 Quantity::from(40_000),
1336 );
1337 let cached_child = cache.borrow().order_owned(&child_id).unwrap();
1338
1339 let terminal_actions = terminal_manager.handle_contingencies(&cached_child);
1340 let update_actions = update_manager.handle_contingencies_update(&cached_child);
1341
1342 assert!(terminal_actions.is_empty());
1343 assert!(update_actions.is_empty());
1344 assert!(terminal_manager.oto_target_quantities.is_empty());
1345 assert!(update_manager.oto_target_quantities.is_empty());
1346 }
1347
1348 #[rstest]
1349 fn test_oto_chained_child_drives_its_child_contingencies() {
1350 let (clock, cache) = create_test_components();
1351 let mut terminal_manager = OrderManager::new(clock.clone(), cache.clone(), false);
1352 let mut update_manager = OrderManager::new(clock, cache.clone(), false);
1353 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
1354 let root_id = ClientOrderId::from("O-ROOT");
1355 let parent_id = ClientOrderId::from("O-PARENT");
1356 let child_id = ClientOrderId::from("O-CHILD");
1357 let parent = OrderTestBuilder::new(OrderType::Limit)
1358 .instrument_id(instrument.id())
1359 .client_order_id(parent_id)
1360 .side(OrderSide::Buy)
1361 .price(Price::from("1.00000"))
1362 .quantity(Quantity::from(120_000))
1363 .contingency_type(ContingencyType::Oto)
1364 .linked_order_ids(vec![child_id])
1365 .parent_order_id(root_id)
1366 .submit(true)
1367 .build();
1368 let child = OrderTestBuilder::new(OrderType::Limit)
1369 .instrument_id(instrument.id())
1370 .client_order_id(child_id)
1371 .side(OrderSide::Sell)
1372 .price(Price::from("1.00100"))
1373 .quantity(Quantity::from(100_000))
1374 .parent_order_id(parent_id)
1375 .submit(true)
1376 .build();
1377 cache
1378 .borrow_mut()
1379 .add_order(parent.clone(), None, None, false)
1380 .unwrap();
1381 cache
1382 .borrow_mut()
1383 .add_order(child.clone(), None, None, false)
1384 .unwrap();
1385 apply_accepted(&cache, &parent, "V-PARENT");
1386 apply_accepted(&cache, &child, "V-CHILD");
1387 let cached_parent = cache.borrow().order_owned(&parent_id).unwrap();
1388
1389 let update_actions = update_manager.handle_contingencies_update(&cached_parent);
1390 let terminal = apply_terminal(&cache, &cached_parent, TerminalEvent::Canceled);
1391 let terminal_actions = terminal_manager.handle_event(&terminal);
1392
1393 assert!(matches!(
1394 update_actions.as_slice(),
1395 [OrderManagerAction::ModifyLocalQuantity { order, quantity }]
1396 if order.client_order_id() == child_id
1397 && *quantity == Quantity::from(120_000)
1398 ));
1399 assert!(matches!(
1400 terminal_actions.as_slice(),
1401 [OrderManagerAction::CancelLocal(order)]
1402 if order.client_order_id() == child_id
1403 ));
1404 }
1405
1406 #[rstest]
1407 fn test_cancel_order_skips_when_pending_cancel_local() {
1408 let (clock, cache) = create_test_components();
1409 let mut manager = OrderManager::new(clock, cache.clone(), true);
1410 let order = create_test_stop_order();
1411 cache
1412 .borrow_mut()
1413 .add_order(order.clone(), None, None, false)
1414 .unwrap();
1415 cache.borrow_mut().update_order_pending_cancel_local(&order);
1416 manager
1417 .submit_order_commands
1418 .insert(order.client_order_id(), make_submit_command(&order));
1419
1420 manager.cancel_order(&order);
1421
1422 assert!(
1423 manager
1424 .submit_order_commands
1425 .contains_key(&order.client_order_id()),
1426 "pending-cancel-local gate should short-circuit before removing the submit command",
1427 );
1428 }
1429
1430 #[rstest]
1431 fn test_cancel_order_skips_when_passed_order_is_closed() {
1432 let (clock, cache) = create_test_components();
1436 let mut manager = OrderManager::new(clock, cache.clone(), true);
1437
1438 let mut order = OrderTestBuilder::new(OrderType::StopMarket)
1439 .instrument_id(audusd_sim().id())
1440 .side(OrderSide::Buy)
1441 .trigger_price(Price::from("1.00050"))
1442 .quantity(Quantity::from(100_000))
1443 .emulation_trigger(TriggerType::BidAsk)
1444 .submit(true)
1445 .build();
1446
1447 cache
1448 .borrow_mut()
1449 .add_order(order.clone(), None, None, false)
1450 .unwrap();
1451
1452 let canceled_event =
1453 TestOrderEventStubs::canceled(&order, AccountId::from("ACCOUNT-001"), None);
1454 order.apply(canceled_event).unwrap();
1455
1456 assert!(order.is_closed());
1457 assert!(!cache.borrow().is_order_closed(&order.client_order_id()));
1458
1459 manager
1460 .submit_order_commands
1461 .insert(order.client_order_id(), make_submit_command(&order));
1462
1463 manager.cancel_order(&order);
1464
1465 assert!(
1466 manager
1467 .submit_order_commands
1468 .contains_key(&order.client_order_id()),
1469 "closed-order gate should short-circuit on the local state when the cache index is stale",
1470 );
1471 }
1472
1473 #[rstest]
1474 fn test_cancel_order_skips_when_cache_index_marks_closed() {
1475 let (clock, cache) = create_test_components();
1479 let mut manager = OrderManager::new(clock, cache.clone(), true);
1480
1481 let mut order = OrderTestBuilder::new(OrderType::StopMarket)
1482 .instrument_id(audusd_sim().id())
1483 .side(OrderSide::Buy)
1484 .trigger_price(Price::from("1.00050"))
1485 .quantity(Quantity::from(100_000))
1486 .emulation_trigger(TriggerType::BidAsk)
1487 .submit(true)
1488 .build();
1489
1490 cache
1491 .borrow_mut()
1492 .add_order(order.clone(), None, None, false)
1493 .unwrap();
1494
1495 let stale_order = order.clone();
1496
1497 let canceled_event =
1498 TestOrderEventStubs::canceled(&order, AccountId::from("ACCOUNT-001"), None);
1499 order = cache.borrow_mut().update_order(&canceled_event).unwrap();
1500
1501 assert!(cache.borrow().is_order_closed(&order.client_order_id()));
1502
1503 manager.submit_order_commands.insert(
1504 stale_order.client_order_id(),
1505 make_submit_command(&stale_order),
1506 );
1507
1508 manager.cancel_order(&stale_order);
1509
1510 assert!(
1511 manager
1512 .submit_order_commands
1513 .contains_key(&stale_order.client_order_id()),
1514 "closed-order gate should short-circuit even when the passed reference is stale",
1515 );
1516 }
1517
1518 #[rstest]
1519 fn test_handle_contingencies_update_syncs_quantity_for_ouo_sibling() {
1520 let (clock, cache) = create_test_components();
1521 let mut manager = OrderManager::new(clock, cache.clone(), true);
1522 let instrument = audusd_sim();
1523 let child = OrderTestBuilder::new(OrderType::Limit)
1524 .instrument_id(instrument.id())
1525 .client_order_id(ClientOrderId::from("O-CHILD"))
1526 .side(OrderSide::Sell)
1527 .price(Price::from("1.00100"))
1528 .quantity(Quantity::from(50_000))
1529 .build();
1530 cache
1531 .borrow_mut()
1532 .add_order(child.clone(), None, None, false)
1533 .unwrap();
1534 let parent = OrderTestBuilder::new(OrderType::Limit)
1535 .instrument_id(instrument.id())
1536 .client_order_id(ClientOrderId::from("O-PARENT"))
1537 .side(OrderSide::Buy)
1538 .price(Price::from("1.00000"))
1539 .quantity(Quantity::from(100_000))
1540 .contingency_type(ContingencyType::Ouo)
1541 .linked_order_ids(vec![child.client_order_id()])
1542 .build();
1543
1544 let actions = manager.handle_contingencies_update(&parent);
1545
1546 assert_eq!(actions.len(), 1);
1547 assert!(matches!(
1548 &actions[0],
1549 OrderManagerAction::ModifyLocalQuantity { order, quantity }
1550 if order.client_order_id() == child.client_order_id()
1551 && *quantity == Quantity::from(100_000)
1552 ));
1553 }
1554
1555 #[rstest]
1556 fn test_handle_contingencies_update_syncs_oto_child_before_parent_fill() {
1557 let (clock, cache) = create_test_components();
1558 let mut manager = OrderManager::new(clock, cache.clone(), false);
1559 let instrument = audusd_sim();
1560 let child = OrderTestBuilder::new(OrderType::Limit)
1561 .instrument_id(instrument.id())
1562 .client_order_id(ClientOrderId::from("O-CHILD"))
1563 .side(OrderSide::Sell)
1564 .price(Price::from("1.00100"))
1565 .quantity(Quantity::from(100_000))
1566 .submit(true)
1567 .build();
1568 cache
1569 .borrow_mut()
1570 .add_order(child.clone(), None, None, false)
1571 .unwrap();
1572 let parent = OrderTestBuilder::new(OrderType::Limit)
1573 .instrument_id(instrument.id())
1574 .client_order_id(ClientOrderId::from("O-PARENT"))
1575 .side(OrderSide::Buy)
1576 .price(Price::from("1.00000"))
1577 .quantity(Quantity::from(120_000))
1578 .contingency_type(ContingencyType::Oto)
1579 .linked_order_ids(vec![child.client_order_id()])
1580 .submit(true)
1581 .build();
1582
1583 let actions = manager.handle_contingencies_update(&parent);
1584
1585 assert!(matches!(
1586 actions.as_slice(),
1587 [OrderManagerAction::ModifyLocalQuantity { order, quantity }]
1588 if order.client_order_id() == child.client_order_id()
1589 && *quantity == Quantity::from(120_000)
1590 ));
1591 assert_eq!(
1592 manager.oto_target_quantities.get(&child.client_order_id()),
1593 Some(&Quantity::from(120_000)),
1594 );
1595 }
1596
1597 #[rstest]
1598 fn test_handle_contingencies_update_clears_target_for_closed_oto_child() {
1599 let (clock, cache) = create_test_components();
1600 let mut manager = OrderManager::new(clock, cache.clone(), false);
1601 let instrument = audusd_sim();
1602 let child = OrderTestBuilder::new(OrderType::Limit)
1603 .instrument_id(instrument.id())
1604 .client_order_id(ClientOrderId::from("O-CHILD"))
1605 .side(OrderSide::Sell)
1606 .price(Price::from("1.00100"))
1607 .quantity(Quantity::from(100_000))
1608 .submit(true)
1609 .build();
1610 cache
1611 .borrow_mut()
1612 .add_order(child.clone(), None, None, false)
1613 .unwrap();
1614 apply_terminal(&cache, &child, TerminalEvent::Canceled);
1615 manager
1616 .oto_target_quantities
1617 .insert(child.client_order_id(), Quantity::from(40_000));
1618 let parent = OrderTestBuilder::new(OrderType::Limit)
1619 .instrument_id(instrument.id())
1620 .client_order_id(ClientOrderId::from("O-PARENT"))
1621 .side(OrderSide::Buy)
1622 .price(Price::from("1.00000"))
1623 .quantity(Quantity::from(100_000))
1624 .contingency_type(ContingencyType::Oto)
1625 .linked_order_ids(vec![child.client_order_id()])
1626 .submit(true)
1627 .build();
1628
1629 let actions = manager.handle_contingencies_update(&parent);
1630
1631 assert!(actions.is_empty());
1632 assert!(manager.oto_target_quantities.is_empty());
1633 }
1634
1635 #[rstest]
1636 fn test_handle_contingencies_update_cancels_ouo_sibling_filled_past_target() {
1637 let (clock, cache) = create_test_components();
1638 let mut manager = OrderManager::new(clock, cache.clone(), false);
1639 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
1640 let child = OrderTestBuilder::new(OrderType::Limit)
1641 .instrument_id(instrument.id())
1642 .client_order_id(ClientOrderId::from("O-CHILD"))
1643 .side(OrderSide::Sell)
1644 .price(Price::from("1.00100"))
1645 .quantity(Quantity::from(100_000))
1646 .submit(true)
1647 .build();
1648 cache
1649 .borrow_mut()
1650 .add_order(child.clone(), None, None, false)
1651 .unwrap();
1652 apply_fill(
1653 &cache,
1654 &child,
1655 &instrument,
1656 "T-CHILD",
1657 Quantity::from(60_000),
1658 );
1659 let parent = OrderTestBuilder::new(OrderType::Limit)
1660 .instrument_id(instrument.id())
1661 .client_order_id(ClientOrderId::from("O-PARENT"))
1662 .side(OrderSide::Buy)
1663 .price(Price::from("1.00000"))
1664 .quantity(Quantity::from(50_000))
1665 .contingency_type(ContingencyType::Ouo)
1666 .linked_order_ids(vec![child.client_order_id()])
1667 .submit(true)
1668 .build();
1669
1670 let actions = manager.handle_contingencies_update(&parent);
1671
1672 assert!(matches!(
1673 actions.as_slice(),
1674 [OrderManagerAction::CancelLocal(order)]
1675 if order.client_order_id() == child.client_order_id()
1676 ));
1677 }
1678
1679 #[rstest]
1680 fn test_handle_contingencies_update_does_not_sync_quantity_for_oco_sibling() {
1681 let (clock, cache) = create_test_components();
1682 let mut manager = OrderManager::new(clock, cache.clone(), true);
1683 let instrument = audusd_sim();
1684 let child = OrderTestBuilder::new(OrderType::Limit)
1685 .instrument_id(instrument.id())
1686 .client_order_id(ClientOrderId::from("O-CHILD"))
1687 .side(OrderSide::Sell)
1688 .price(Price::from("1.00100"))
1689 .quantity(Quantity::from(50_000))
1690 .build();
1691 cache
1692 .borrow_mut()
1693 .add_order(child.clone(), None, None, false)
1694 .unwrap();
1695 let parent = OrderTestBuilder::new(OrderType::Limit)
1696 .instrument_id(instrument.id())
1697 .client_order_id(ClientOrderId::from("O-PARENT"))
1698 .side(OrderSide::Buy)
1699 .price(Price::from("1.00000"))
1700 .quantity(Quantity::from(100_000))
1701 .contingency_type(ContingencyType::Oco)
1702 .linked_order_ids(vec![child.client_order_id()])
1703 .build();
1704
1705 let actions = manager.handle_contingencies_update(&parent);
1706
1707 assert!(actions.is_empty());
1708 }
1709
1710 #[derive(Debug, Clone, Copy)]
1711 enum TerminalEvent {
1712 Canceled,
1713 Expired,
1714 Rejected,
1715 }
1716
1717 fn apply_fill(
1718 cache: &Rc<RefCell<Cache>>,
1719 order: &OrderAny,
1720 instrument: &InstrumentAny,
1721 trade_id: &str,
1722 last_qty: Quantity,
1723 ) -> OrderEventAny {
1724 let event = TestOrderEventStubs::filled(
1725 order,
1726 instrument,
1727 Some(TradeId::from(trade_id)),
1728 None,
1729 None,
1730 Some(last_qty),
1731 None,
1732 None,
1733 None,
1734 Some(AccountId::from("ACCOUNT-001")),
1735 );
1736 cache.borrow_mut().update_order(&event).unwrap();
1737 event
1738 }
1739
1740 fn apply_accepted(cache: &Rc<RefCell<Cache>>, order: &OrderAny, venue_order_id: &str) {
1741 let event = TestOrderEventStubs::accepted(
1742 order,
1743 AccountId::from("ACCOUNT-001"),
1744 VenueOrderId::from(venue_order_id),
1745 );
1746 cache.borrow_mut().update_order(&event).unwrap();
1747 }
1748
1749 fn apply_update(
1750 cache: &Rc<RefCell<Cache>>,
1751 order: &OrderAny,
1752 quantity: Quantity,
1753 ) -> OrderEventAny {
1754 let event = OrderEventAny::Updated(
1755 OrderUpdatedSpec::builder()
1756 .trader_id(order.trader_id())
1757 .strategy_id(order.strategy_id())
1758 .instrument_id(order.instrument_id())
1759 .client_order_id(order.client_order_id())
1760 .quantity(quantity)
1761 .venue_order_id(VenueOrderId::from("V-CHILD"))
1762 .account_id(AccountId::from("ACCOUNT-001"))
1763 .build(),
1764 );
1765 cache.borrow_mut().update_order(&event).unwrap();
1766 event
1767 }
1768
1769 fn apply_terminal(
1770 cache: &Rc<RefCell<Cache>>,
1771 order: &OrderAny,
1772 terminal: TerminalEvent,
1773 ) -> OrderEventAny {
1774 let event = match terminal {
1775 TerminalEvent::Canceled => OrderEventAny::Canceled(
1776 OrderCanceledSpec::builder()
1777 .trader_id(order.trader_id())
1778 .strategy_id(order.strategy_id())
1779 .instrument_id(order.instrument_id())
1780 .client_order_id(order.client_order_id())
1781 .account_id(AccountId::from("ACCOUNT-001"))
1782 .build(),
1783 ),
1784 TerminalEvent::Expired => OrderEventAny::Expired(
1785 OrderExpiredSpec::builder()
1786 .trader_id(order.trader_id())
1787 .strategy_id(order.strategy_id())
1788 .instrument_id(order.instrument_id())
1789 .client_order_id(order.client_order_id())
1790 .account_id(AccountId::from("ACCOUNT-001"))
1791 .build(),
1792 ),
1793 TerminalEvent::Rejected => OrderEventAny::Rejected(
1794 OrderRejectedSpec::builder()
1795 .trader_id(order.trader_id())
1796 .strategy_id(order.strategy_id())
1797 .instrument_id(order.instrument_id())
1798 .client_order_id(order.client_order_id())
1799 .account_id(AccountId::from("ACCOUNT-001"))
1800 .reason("test rejection".into())
1801 .build(),
1802 ),
1803 };
1804 cache.borrow_mut().update_order(&event).unwrap();
1805 event
1806 }
1807
1808 #[rstest]
1809 fn test_should_manage_order_selects_symmetric_ownership_classes() {
1810 let (clock, cache) = create_test_components();
1811 let active_local = OrderTestBuilder::new(OrderType::Limit)
1812 .instrument_id(audusd_sim().id())
1813 .side(OrderSide::Buy)
1814 .price(Price::from("1.00000"))
1815 .quantity(Quantity::from(100_000))
1816 .build();
1817 let non_active_local = OrderTestBuilder::new(OrderType::Limit)
1818 .instrument_id(audusd_sim().id())
1819 .side(OrderSide::Buy)
1820 .price(Price::from("1.00000"))
1821 .quantity(Quantity::from(100_000))
1822 .submit(true)
1823 .build();
1824 let emulator_manager = OrderManager::new(clock.clone(), cache.clone(), true);
1825 let strategy_manager = OrderManager::new(clock, cache, false);
1826
1827 assert!(emulator_manager.should_manage_order(&active_local));
1828 assert!(!emulator_manager.should_manage_order(&non_active_local));
1829 assert!(!strategy_manager.should_manage_order(&active_local));
1830 assert!(strategy_manager.should_manage_order(&non_active_local));
1831 }
1832
1833 #[rstest]
1834 #[case::strategy(false, true, false)]
1835 #[case::emulator(true, false, true)]
1836 fn test_handle_oto_partial_fill_resizes_once_and_submits_only_for_active_local(
1837 #[case] active_local: bool,
1838 #[case] child_submitted: bool,
1839 #[case] expects_submit: bool,
1840 ) {
1841 let (clock, cache) = create_test_components();
1842 let mut manager = OrderManager::new(clock, cache.clone(), active_local);
1843 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
1844 let parent_id = ClientOrderId::from("O-PARENT");
1845 let child_id = ClientOrderId::from("O-CHILD");
1846 let parent = OrderTestBuilder::new(OrderType::Limit)
1847 .instrument_id(instrument.id())
1848 .client_order_id(parent_id)
1849 .side(OrderSide::Buy)
1850 .price(Price::from("1.00000"))
1851 .quantity(Quantity::from(100_000))
1852 .contingency_type(ContingencyType::Oto)
1853 .linked_order_ids(vec![child_id])
1854 .submit(true)
1855 .build();
1856 let child = OrderTestBuilder::new(OrderType::Limit)
1857 .instrument_id(instrument.id())
1858 .client_order_id(child_id)
1859 .side(OrderSide::Sell)
1860 .price(Price::from("1.00100"))
1861 .quantity(Quantity::from(100_000))
1862 .submit(child_submitted)
1863 .build();
1864 cache
1865 .borrow_mut()
1866 .add_order(parent.clone(), None, None, false)
1867 .unwrap();
1868 cache
1869 .borrow_mut()
1870 .add_order(child.clone(), None, None, false)
1871 .unwrap();
1872 let event = apply_fill(
1873 &cache,
1874 &parent,
1875 &instrument,
1876 "T-PARTIAL",
1877 Quantity::from(40_000),
1878 );
1879
1880 let actions = manager.handle_event(&event);
1881 let repeated_actions = manager.handle_event(&event);
1882
1883 assert_eq!(actions.len(), if expects_submit { 2 } else { 1 });
1884 assert!(matches!(
1885 &actions[0],
1886 OrderManagerAction::ModifyLocalQuantity { order, quantity }
1887 if order.client_order_id() == child_id
1888 && *quantity == Quantity::from(40_000)
1889 ));
1890
1891 if expects_submit {
1892 assert!(matches!(
1893 &actions[1],
1894 OrderManagerAction::SubmitToRisk(command)
1895 if command.client_order_id == child_id
1896 ));
1897 }
1898 assert!(repeated_actions.is_empty());
1899
1900 let modify_rejected = OrderEventAny::ModifyRejected(
1901 OrderModifyRejectedSpec::builder()
1902 .trader_id(child.trader_id())
1903 .strategy_id(child.strategy_id())
1904 .instrument_id(child.instrument_id())
1905 .client_order_id(child_id)
1906 .reason("test rejection".into())
1907 .build(),
1908 );
1909 assert!(manager.handle_event(&modify_rejected).is_empty());
1910 let retry_actions = manager.handle_event(&event);
1911 assert!(matches!(
1912 retry_actions.as_slice(),
1913 [OrderManagerAction::ModifyLocalQuantity { order, quantity }]
1914 if order.client_order_id() == child_id
1915 && *quantity == Quantity::from(40_000)
1916 ));
1917
1918 let next_event = apply_fill(
1919 &cache,
1920 &parent,
1921 &instrument,
1922 "T-PARTIAL-2",
1923 Quantity::from(60_000),
1924 );
1925 let next_actions = manager.handle_event(&next_event);
1926
1927 assert!(next_actions.is_empty());
1928 assert_eq!(
1929 manager.oto_target_quantities.get(&child_id),
1930 Some(&Quantity::from(100_000)),
1931 );
1932 }
1933
1934 #[rstest]
1935 fn test_handle_oto_chained_child_releases_its_child() {
1936 let (clock, cache) = create_test_components();
1937 let mut manager = OrderManager::new(clock, cache.clone(), true);
1938 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
1939 let root_id = ClientOrderId::from("O-ROOT");
1940 let parent_id = ClientOrderId::from("O-PARENT");
1941 let child_id = ClientOrderId::from("O-CHILD");
1942 let parent = OrderTestBuilder::new(OrderType::Limit)
1943 .instrument_id(instrument.id())
1944 .client_order_id(parent_id)
1945 .side(OrderSide::Buy)
1946 .price(Price::from("1.00000"))
1947 .quantity(Quantity::from(100_000))
1948 .contingency_type(ContingencyType::Oto)
1949 .linked_order_ids(vec![child_id])
1950 .parent_order_id(root_id)
1951 .submit(true)
1952 .build();
1953 let child = OrderTestBuilder::new(OrderType::Limit)
1954 .instrument_id(instrument.id())
1955 .client_order_id(child_id)
1956 .side(OrderSide::Sell)
1957 .price(Price::from("1.00100"))
1958 .quantity(Quantity::from(100_000))
1959 .parent_order_id(parent_id)
1960 .emulation_trigger(TriggerType::BidAsk)
1961 .build();
1962 cache
1963 .borrow_mut()
1964 .add_order(parent.clone(), None, None, false)
1965 .unwrap();
1966 cache
1967 .borrow_mut()
1968 .add_order(child, None, None, false)
1969 .unwrap();
1970 apply_accepted(&cache, &parent, "V-PARENT");
1971 let fill = apply_fill(
1972 &cache,
1973 &parent,
1974 &instrument,
1975 "T-PARENT",
1976 Quantity::from(100_000),
1977 );
1978
1979 let actions = manager.handle_event(&fill);
1980
1981 assert!(matches!(
1982 actions.as_slice(),
1983 [OrderManagerAction::SubmitToEmulator(command)]
1984 if command.client_order_id == child_id
1985 ));
1986 }
1987
1988 #[rstest]
1989 fn test_handle_oto_cancels_child_filled_to_target() {
1990 let (clock, cache) = create_test_components();
1991 let mut manager = OrderManager::new(clock, cache.clone(), false);
1992 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
1993 let child_id = ClientOrderId::from("O-CHILD");
1994 let parent = OrderTestBuilder::new(OrderType::Limit)
1995 .instrument_id(instrument.id())
1996 .client_order_id(ClientOrderId::from("O-PARENT"))
1997 .side(OrderSide::Buy)
1998 .price(Price::from("1.00000"))
1999 .quantity(Quantity::from(100_000))
2000 .contingency_type(ContingencyType::Oto)
2001 .linked_order_ids(vec![child_id])
2002 .submit(true)
2003 .build();
2004 let child = OrderTestBuilder::new(OrderType::Limit)
2005 .instrument_id(instrument.id())
2006 .client_order_id(child_id)
2007 .side(OrderSide::Sell)
2008 .price(Price::from("1.00100"))
2009 .quantity(Quantity::from(100_000))
2010 .submit(true)
2011 .build();
2012 cache
2013 .borrow_mut()
2014 .add_order(parent.clone(), None, None, false)
2015 .unwrap();
2016 cache
2017 .borrow_mut()
2018 .add_order(child.clone(), None, None, false)
2019 .unwrap();
2020 apply_accepted(&cache, &parent, "V-PARENT");
2021 apply_accepted(&cache, &child, "V-CHILD");
2022 apply_fill(
2023 &cache,
2024 &child,
2025 &instrument,
2026 "T-CHILD",
2027 Quantity::from(60_000),
2028 );
2029 let parent_fill = apply_fill(
2030 &cache,
2031 &parent,
2032 &instrument,
2033 "T-PARENT",
2034 Quantity::from(40_000),
2035 );
2036
2037 let actions = manager.handle_event(&parent_fill);
2038
2039 assert!(matches!(
2040 actions.as_slice(),
2041 [OrderManagerAction::CancelLocal(order)]
2042 if order.client_order_id() == child_id
2043 ));
2044 assert!(!manager.oto_target_quantities.contains_key(&child_id));
2045 }
2046
2047 #[rstest]
2048 fn test_handle_oto_clears_target_when_child_leaves_active_local() {
2049 let (clock, cache) = create_test_components();
2050 let mut terminal_manager = OrderManager::new(clock.clone(), cache.clone(), true);
2051 let mut update_manager = OrderManager::new(clock.clone(), cache.clone(), true);
2052 let mut contingency_update_manager = OrderManager::new(clock.clone(), cache.clone(), true);
2053 let mut fill_manager = OrderManager::new(clock, cache.clone(), true);
2054 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
2055 let parent_id = ClientOrderId::from("O-PARENT");
2056 let child_id = ClientOrderId::from("O-CHILD");
2057 let parent = OrderTestBuilder::new(OrderType::Limit)
2058 .instrument_id(instrument.id())
2059 .client_order_id(parent_id)
2060 .side(OrderSide::Buy)
2061 .price(Price::from("1.00000"))
2062 .quantity(Quantity::from(100_000))
2063 .contingency_type(ContingencyType::Oto)
2064 .linked_order_ids(vec![child_id])
2065 .submit(true)
2066 .build();
2067 let child = OrderTestBuilder::new(OrderType::Limit)
2068 .instrument_id(instrument.id())
2069 .client_order_id(child_id)
2070 .side(OrderSide::Sell)
2071 .price(Price::from("1.00100"))
2072 .quantity(Quantity::from(100_000))
2073 .build();
2074 cache
2075 .borrow_mut()
2076 .add_order(parent.clone(), None, None, false)
2077 .unwrap();
2078 cache
2079 .borrow_mut()
2080 .add_order(child.clone(), None, None, false)
2081 .unwrap();
2082 let fill = apply_fill(
2083 &cache,
2084 &parent,
2085 &instrument,
2086 "T-PARTIAL",
2087 Quantity::from(40_000),
2088 );
2089 terminal_manager.handle_event(&fill);
2090 update_manager.handle_event(&fill);
2091 contingency_update_manager.handle_event(&fill);
2092 fill_manager.handle_event(&fill);
2093
2094 let submitted = TestOrderEventStubs::submitted(&child, AccountId::from("ACCOUNT-001"));
2095 cache.borrow_mut().update_order(&submitted).unwrap();
2096 apply_accepted(&cache, &child, "V-CHILD");
2097
2098 let cached_child = cache.borrow().order_owned(&child_id).unwrap();
2099 let child_fill = apply_fill(
2100 &cache,
2101 &cached_child,
2102 &instrument,
2103 "T-CHILD",
2104 Quantity::from(60_000),
2105 );
2106 assert!(fill_manager.handle_event(&child_fill).is_empty());
2107 assert!(!fill_manager.oto_target_quantities.contains_key(&child_id));
2108
2109 let cached_parent = cache.borrow().order_owned(&parent_id).unwrap();
2110 assert!(
2111 contingency_update_manager
2112 .handle_contingencies_update(&cached_parent)
2113 .is_empty()
2114 );
2115 assert!(
2116 !contingency_update_manager
2117 .oto_target_quantities
2118 .contains_key(&child_id)
2119 );
2120
2121 let terminal = apply_terminal(&cache, &parent, TerminalEvent::Canceled);
2122 assert!(terminal_manager.handle_event(&terminal).is_empty());
2123 assert!(
2124 !terminal_manager
2125 .oto_target_quantities
2126 .contains_key(&child_id)
2127 );
2128
2129 let update = apply_update(&cache, &child, Quantity::from(80_000));
2130 assert!(update_manager.handle_event(&update).is_empty());
2131 assert!(!update_manager.oto_target_quantities.contains_key(&child_id));
2132 }
2133
2134 #[rstest]
2135 fn test_handle_oto_out_of_order_updates_reassert_latest_target() {
2136 let (clock, cache) = create_test_components();
2137 let mut manager = OrderManager::new(clock, cache.clone(), false);
2138 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
2139 let child_id = ClientOrderId::from("O-CHILD");
2140 let parent = OrderTestBuilder::new(OrderType::Limit)
2141 .instrument_id(instrument.id())
2142 .client_order_id(ClientOrderId::from("O-PARENT"))
2143 .side(OrderSide::Buy)
2144 .price(Price::from("1.00000"))
2145 .quantity(Quantity::from(100_000))
2146 .contingency_type(ContingencyType::Oto)
2147 .linked_order_ids(vec![child_id])
2148 .submit(true)
2149 .build();
2150 let child = OrderTestBuilder::new(OrderType::Limit)
2151 .instrument_id(instrument.id())
2152 .client_order_id(child_id)
2153 .side(OrderSide::Sell)
2154 .price(Price::from("1.00100"))
2155 .quantity(Quantity::from(100_000))
2156 .submit(true)
2157 .build();
2158 cache
2159 .borrow_mut()
2160 .add_order(parent.clone(), None, None, false)
2161 .unwrap();
2162 cache
2163 .borrow_mut()
2164 .add_order(child.clone(), None, None, false)
2165 .unwrap();
2166 apply_accepted(&cache, &child, "V-CHILD");
2167
2168 let first_fill = apply_fill(
2169 &cache,
2170 &parent,
2171 &instrument,
2172 "T-OUT-OF-ORDER-1",
2173 Quantity::from(40_000),
2174 );
2175 manager.handle_event(&first_fill);
2176 let second_fill = apply_fill(
2177 &cache,
2178 &parent,
2179 &instrument,
2180 "T-OUT-OF-ORDER-2",
2181 Quantity::from(60_000),
2182 );
2183 manager.handle_event(&second_fill);
2184
2185 let latest_update = apply_update(&cache, &child, Quantity::from(100_000));
2186 let latest_actions = manager.handle_event(&latest_update);
2187 let stale_update = apply_update(&cache, &child, Quantity::from(40_000));
2188 let corrective_actions = manager.handle_event(&stale_update);
2189
2190 assert!(latest_actions.is_empty());
2191 assert!(matches!(
2192 corrective_actions.as_slice(),
2193 [OrderManagerAction::ModifyLocalQuantity { order, quantity }]
2194 if order.client_order_id() == child_id
2195 && *quantity == Quantity::from(100_000)
2196 ));
2197 }
2198
2199 #[rstest]
2200 fn test_handle_oto_child_fill_waits_for_parent_target_refresh() {
2201 let (clock, cache) = create_test_components();
2202 let mut manager = OrderManager::new(clock, cache.clone(), false);
2203 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
2204 let parent_id = ClientOrderId::from("O-PARENT");
2205 let child_id = ClientOrderId::from("O-CHILD");
2206 let parent = OrderTestBuilder::new(OrderType::Limit)
2207 .instrument_id(instrument.id())
2208 .client_order_id(parent_id)
2209 .side(OrderSide::Buy)
2210 .price(Price::from("1.00000"))
2211 .quantity(Quantity::from(100_000))
2212 .contingency_type(ContingencyType::Oto)
2213 .linked_order_ids(vec![child_id])
2214 .submit(true)
2215 .build();
2216 let child = OrderTestBuilder::new(OrderType::Limit)
2217 .instrument_id(instrument.id())
2218 .client_order_id(child_id)
2219 .side(OrderSide::Sell)
2220 .price(Price::from("1.00100"))
2221 .quantity(Quantity::from(100_000))
2222 .contingency_type(ContingencyType::Oto)
2223 .linked_order_ids(vec![parent_id])
2224 .parent_order_id(parent_id)
2225 .submit(true)
2226 .build();
2227 cache
2228 .borrow_mut()
2229 .add_order(parent.clone(), None, None, false)
2230 .unwrap();
2231 cache
2232 .borrow_mut()
2233 .add_order(child.clone(), None, None, false)
2234 .unwrap();
2235 apply_accepted(&cache, &parent, "V-PARENT");
2236 apply_accepted(&cache, &child, "V-CHILD");
2237 let parent_fill = apply_fill(
2238 &cache,
2239 &parent,
2240 &instrument,
2241 "T-PARENT",
2242 Quantity::from(40_000),
2243 );
2244 manager.handle_event(&parent_fill);
2245 let child_fill = apply_fill(
2246 &cache,
2247 &child,
2248 &instrument,
2249 "T-CHILD",
2250 Quantity::from(60_000),
2251 );
2252
2253 let actions = manager.handle_event(&child_fill);
2254
2255 assert!(actions.is_empty());
2256 assert_eq!(
2257 manager.oto_target_quantities.get(&child_id),
2258 Some(&Quantity::from(40_000)),
2259 );
2260
2261 let cached_parent = cache.borrow().order_owned(&parent_id).unwrap();
2262 let final_parent_fill = apply_fill(
2263 &cache,
2264 &cached_parent,
2265 &instrument,
2266 "T-PARENT-FINAL",
2267 Quantity::from(60_000),
2268 );
2269 let final_actions = manager.handle_event(&final_parent_fill);
2270
2271 assert!(final_actions.is_empty());
2272 assert_eq!(
2273 manager.oto_target_quantities.get(&child_id),
2274 Some(&Quantity::from(100_000)),
2275 );
2276 }
2277
2278 #[rstest]
2279 fn test_handle_oto_child_update_waits_when_filled_past_stored_target() {
2280 let (clock, cache) = create_test_components();
2281 let mut manager = OrderManager::new(clock, cache.clone(), false);
2282 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
2283 let child_id = ClientOrderId::from("O-CHILD");
2284 let child = OrderTestBuilder::new(OrderType::Limit)
2285 .instrument_id(instrument.id())
2286 .client_order_id(child_id)
2287 .side(OrderSide::Sell)
2288 .price(Price::from("1.00100"))
2289 .quantity(Quantity::from(100_000))
2290 .submit(true)
2291 .build();
2292 cache
2293 .borrow_mut()
2294 .add_order(child.clone(), None, None, false)
2295 .unwrap();
2296 apply_accepted(&cache, &child, "V-CHILD");
2297 apply_fill(
2298 &cache,
2299 &child,
2300 &instrument,
2301 "T-CHILD",
2302 Quantity::from(60_000),
2303 );
2304 manager
2305 .oto_target_quantities
2306 .insert(child_id, Quantity::from(40_000));
2307 let update = apply_update(&cache, &child, Quantity::from(90_000));
2308
2309 let actions = manager.handle_event(&update);
2310
2311 assert!(actions.is_empty());
2312 assert_eq!(
2313 manager.oto_target_quantities.get(&child_id),
2314 Some(&Quantity::from(40_000)),
2315 );
2316 }
2317
2318 #[rstest]
2319 fn test_handle_oto_parent_update_preserves_partial_fill_target() {
2320 let (clock, cache) = create_test_components();
2321 let mut manager = OrderManager::new(clock, cache.clone(), false);
2322 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
2323 let child_id = ClientOrderId::from("O-CHILD");
2324 let parent = OrderTestBuilder::new(OrderType::Limit)
2325 .instrument_id(instrument.id())
2326 .client_order_id(ClientOrderId::from("O-PARENT"))
2327 .side(OrderSide::Buy)
2328 .price(Price::from("1.00000"))
2329 .quantity(Quantity::from(100_000))
2330 .contingency_type(ContingencyType::Oto)
2331 .linked_order_ids(vec![child_id])
2332 .submit(true)
2333 .build();
2334 let child = OrderTestBuilder::new(OrderType::Limit)
2335 .instrument_id(instrument.id())
2336 .client_order_id(child_id)
2337 .side(OrderSide::Sell)
2338 .price(Price::from("1.00100"))
2339 .quantity(Quantity::from(100_000))
2340 .submit(true)
2341 .build();
2342 cache
2343 .borrow_mut()
2344 .add_order(parent.clone(), None, None, false)
2345 .unwrap();
2346 cache
2347 .borrow_mut()
2348 .add_order(child, None, None, false)
2349 .unwrap();
2350 let fill = apply_fill(
2351 &cache,
2352 &parent,
2353 &instrument,
2354 "T-PARTIAL",
2355 Quantity::from(40_000),
2356 );
2357 manager.handle_event(&fill);
2358 let parent_update = apply_update(&cache, &parent, Quantity::from(120_000));
2359
2360 let actions = manager.handle_event(&parent_update);
2361
2362 assert!(actions.is_empty());
2363 assert_eq!(
2364 manager.oto_target_quantities.get(&child_id),
2365 Some(&Quantity::from(40_000)),
2366 );
2367
2368 let terminal = apply_terminal(&cache, &parent, TerminalEvent::Canceled);
2369 assert!(manager.handle_event(&terminal).is_empty());
2370 assert_eq!(
2371 manager.oto_target_quantities.get(&child_id),
2372 Some(&Quantity::from(40_000)),
2373 );
2374 }
2375
2376 #[rstest]
2377 fn test_handle_oto_fill_uses_completed_and_active_spawn_totals() {
2378 let (clock, cache) = create_test_components();
2379 let mut manager = OrderManager::new(clock, cache.clone(), false);
2380 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
2381 let spawn_id = ClientOrderId::from("O-SPAWN");
2382 let first_id = ClientOrderId::from("O-SPAWN-1");
2383 let second_id = ClientOrderId::from("O-SPAWN-2");
2384 let child_id = ClientOrderId::from("O-CHILD");
2385 let exec_algorithm_id = ExecAlgorithmId::from("TWAP");
2386 let first = OrderTestBuilder::new(OrderType::Limit)
2387 .instrument_id(instrument.id())
2388 .client_order_id(first_id)
2389 .side(OrderSide::Buy)
2390 .price(Price::from("1.00000"))
2391 .quantity(Quantity::from(60_000))
2392 .contingency_type(ContingencyType::Oto)
2393 .linked_order_ids(vec![child_id])
2394 .exec_algorithm_id(exec_algorithm_id)
2395 .exec_spawn_id(spawn_id)
2396 .submit(true)
2397 .build();
2398 let second = OrderTestBuilder::new(OrderType::Limit)
2399 .instrument_id(instrument.id())
2400 .client_order_id(second_id)
2401 .side(OrderSide::Buy)
2402 .price(Price::from("1.00000"))
2403 .quantity(Quantity::from(40_000))
2404 .contingency_type(ContingencyType::Oto)
2405 .linked_order_ids(vec![child_id])
2406 .exec_algorithm_id(exec_algorithm_id)
2407 .exec_spawn_id(spawn_id)
2408 .submit(true)
2409 .build();
2410 let child = OrderTestBuilder::new(OrderType::Limit)
2411 .instrument_id(instrument.id())
2412 .client_order_id(child_id)
2413 .side(OrderSide::Sell)
2414 .price(Price::from("1.00100"))
2415 .quantity(Quantity::from(100_000))
2416 .submit(true)
2417 .build();
2418
2419 for order in [&first, &second, &child] {
2420 cache
2421 .borrow_mut()
2422 .add_order(order.clone(), None, None, false)
2423 .unwrap();
2424 }
2425 apply_accepted(&cache, &first, "V-SPAWN-1");
2426 apply_accepted(&cache, &second, "V-SPAWN-2");
2427 apply_fill(
2428 &cache,
2429 &first,
2430 &instrument,
2431 "T-SPAWN-1",
2432 Quantity::from(60_000),
2433 );
2434 let event = apply_fill(
2435 &cache,
2436 &second,
2437 &instrument,
2438 "T-SPAWN-2",
2439 Quantity::from(10_000),
2440 );
2441
2442 let actions = manager.handle_event(&event);
2443
2444 assert!(matches!(
2445 actions.first(),
2446 Some(OrderManagerAction::ModifyLocalQuantity { order, quantity })
2447 if order.client_order_id() == child_id
2448 && *quantity == Quantity::from(70_000)
2449 ));
2450
2451 let update = apply_update(&cache, &second, Quantity::from(50_000));
2452 assert!(manager.handle_event(&update).is_empty());
2453 assert_eq!(
2454 manager.oto_target_quantities.get(&child_id),
2455 Some(&Quantity::from(70_000)),
2456 );
2457
2458 let cached_second = cache.borrow().order_owned(&second_id).unwrap();
2459 assert!(manager.handle_contingencies(&cached_second).is_empty());
2460 }
2461
2462 #[rstest]
2463 fn test_handle_ouo_partial_fill_resizes_open_sibling() {
2464 let (clock, cache) = create_test_components();
2465 let mut manager = OrderManager::new(clock, cache.clone(), false);
2466 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
2467 let parent_id = ClientOrderId::from("O-PARENT");
2468 let sibling_id = ClientOrderId::from("O-SIBLING");
2469 let parent = OrderTestBuilder::new(OrderType::Limit)
2470 .instrument_id(instrument.id())
2471 .client_order_id(parent_id)
2472 .side(OrderSide::Buy)
2473 .price(Price::from("1.00000"))
2474 .quantity(Quantity::from(100_000))
2475 .contingency_type(ContingencyType::Ouo)
2476 .linked_order_ids(vec![parent_id, sibling_id])
2477 .submit(true)
2478 .build();
2479 let sibling = OrderTestBuilder::new(OrderType::Limit)
2480 .instrument_id(instrument.id())
2481 .client_order_id(sibling_id)
2482 .side(OrderSide::Sell)
2483 .price(Price::from("1.00100"))
2484 .quantity(Quantity::from(100_000))
2485 .submit(true)
2486 .build();
2487 cache
2488 .borrow_mut()
2489 .add_order(parent.clone(), None, None, false)
2490 .unwrap();
2491 cache
2492 .borrow_mut()
2493 .add_order(sibling, None, None, false)
2494 .unwrap();
2495 let event = apply_fill(
2496 &cache,
2497 &parent,
2498 &instrument,
2499 "T-OUO",
2500 Quantity::from(25_000),
2501 );
2502
2503 let actions = manager.handle_event(&event);
2504
2505 assert!(matches!(
2506 actions.as_slice(),
2507 [OrderManagerAction::ModifyLocalQuantity { order, quantity }]
2508 if order.client_order_id() == sibling_id
2509 && *quantity == Quantity::from(75_000)
2510 ));
2511 }
2512
2513 #[rstest]
2514 fn test_handle_ouo_partial_fill_cancels_sibling_filled_past_target() {
2515 let (clock, cache) = create_test_components();
2516 let mut manager = OrderManager::new(clock, cache.clone(), false);
2517 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
2518 let parent_id = ClientOrderId::from("O-PARENT");
2519 let sibling_id = ClientOrderId::from("O-SIBLING");
2520 let parent = OrderTestBuilder::new(OrderType::Limit)
2521 .instrument_id(instrument.id())
2522 .client_order_id(parent_id)
2523 .side(OrderSide::Buy)
2524 .price(Price::from("1.00000"))
2525 .quantity(Quantity::from(100_000))
2526 .contingency_type(ContingencyType::Ouo)
2527 .linked_order_ids(vec![parent_id, sibling_id])
2528 .submit(true)
2529 .build();
2530 let sibling = OrderTestBuilder::new(OrderType::Limit)
2531 .instrument_id(instrument.id())
2532 .client_order_id(sibling_id)
2533 .side(OrderSide::Sell)
2534 .price(Price::from("1.00100"))
2535 .quantity(Quantity::from(100_000))
2536 .submit(true)
2537 .build();
2538 cache
2539 .borrow_mut()
2540 .add_order(parent.clone(), None, None, false)
2541 .unwrap();
2542 cache
2543 .borrow_mut()
2544 .add_order(sibling.clone(), None, None, false)
2545 .unwrap();
2546 apply_accepted(&cache, &parent, "V-PARENT");
2547 apply_accepted(&cache, &sibling, "V-SIBLING");
2548 apply_fill(
2549 &cache,
2550 &sibling,
2551 &instrument,
2552 "T-SIBLING",
2553 Quantity::from(30_000),
2554 );
2555 let parent_fill = apply_fill(
2556 &cache,
2557 &parent,
2558 &instrument,
2559 "T-PARENT",
2560 Quantity::from(90_000),
2561 );
2562
2563 let actions = manager.handle_event(&parent_fill);
2564
2565 assert!(matches!(
2566 actions.as_slice(),
2567 [OrderManagerAction::CancelLocal(order)]
2568 if order.client_order_id() == sibling_id
2569 ));
2570 }
2571
2572 #[rstest]
2573 #[case(TerminalEvent::Canceled)]
2574 #[case(TerminalEvent::Expired)]
2575 #[case(TerminalEvent::Rejected)]
2576 fn test_terminal_non_contingent_event_clears_oto_target(#[case] terminal: TerminalEvent) {
2577 let (clock, cache) = create_test_components();
2578 let mut manager = OrderManager::new(clock, cache.clone(), false);
2579 let order = OrderTestBuilder::new(OrderType::Limit)
2580 .instrument_id(audusd_sim().id())
2581 .client_order_id(ClientOrderId::from("O-001"))
2582 .side(OrderSide::Buy)
2583 .price(Price::from("1.00000"))
2584 .quantity(Quantity::from(100_000))
2585 .submit(true)
2586 .build();
2587 cache
2588 .borrow_mut()
2589 .add_order(order.clone(), None, None, false)
2590 .unwrap();
2591
2592 if matches!(terminal, TerminalEvent::Expired) {
2593 apply_accepted(&cache, &order, "V-001");
2594 }
2595 manager
2596 .oto_target_quantities
2597 .insert(order.client_order_id(), Quantity::from(40_000));
2598 let event = apply_terminal(&cache, &order, terminal);
2599
2600 let actions = manager.handle_event(&event);
2601
2602 assert!(actions.is_empty());
2603 assert!(manager.oto_target_quantities.is_empty());
2604 }
2605
2606 #[rstest]
2607 #[case(ContingencyType::Oto, TerminalEvent::Rejected)]
2608 #[case(ContingencyType::Oco, TerminalEvent::Canceled)]
2609 #[case(ContingencyType::Oco, TerminalEvent::Expired)]
2610 #[case(ContingencyType::Oco, TerminalEvent::Rejected)]
2611 #[case(ContingencyType::Ouo, TerminalEvent::Canceled)]
2612 fn test_terminal_contingent_event_cancels_open_sibling(
2613 #[case] contingency_type: ContingencyType,
2614 #[case] terminal: TerminalEvent,
2615 ) {
2616 let (clock, cache) = create_test_components();
2617 let mut manager = OrderManager::new(clock, cache.clone(), false);
2618 let parent_id = ClientOrderId::from("O-PARENT");
2619 let sibling_id = ClientOrderId::from("O-SIBLING");
2620 let linked_order_ids = if contingency_type == ContingencyType::Oto {
2621 vec![sibling_id]
2622 } else {
2623 vec![parent_id, sibling_id]
2624 };
2625 let parent = OrderTestBuilder::new(OrderType::Limit)
2626 .instrument_id(audusd_sim().id())
2627 .client_order_id(parent_id)
2628 .side(OrderSide::Buy)
2629 .price(Price::from("1.00000"))
2630 .quantity(Quantity::from(100_000))
2631 .contingency_type(contingency_type)
2632 .linked_order_ids(linked_order_ids)
2633 .submit(true)
2634 .build();
2635 let sibling = OrderTestBuilder::new(OrderType::Limit)
2636 .instrument_id(audusd_sim().id())
2637 .client_order_id(sibling_id)
2638 .side(OrderSide::Sell)
2639 .price(Price::from("1.00100"))
2640 .quantity(Quantity::from(100_000))
2641 .submit(true)
2642 .build();
2643 cache
2644 .borrow_mut()
2645 .add_order(parent.clone(), None, None, false)
2646 .unwrap();
2647 cache
2648 .borrow_mut()
2649 .add_order(sibling, None, None, false)
2650 .unwrap();
2651
2652 if matches!(terminal, TerminalEvent::Expired) {
2653 apply_accepted(&cache, &parent, "V-PARENT");
2654 }
2655 let event = apply_terminal(&cache, &parent, terminal);
2656
2657 let actions = manager.handle_event(&event);
2658
2659 assert!(matches!(
2660 actions.as_slice(),
2661 [OrderManagerAction::CancelLocal(order)]
2662 if order.client_order_id() == sibling_id
2663 ));
2664 }
2665
2666 #[rstest]
2667 #[case(ContingencyType::Oto)]
2668 #[case(ContingencyType::Ouo)]
2669 fn test_strategy_manager_excludes_active_local_and_closed_siblings(
2670 #[case] contingency_type: ContingencyType,
2671 ) {
2672 let (clock, cache) = create_test_components();
2673 let mut manager = OrderManager::new(clock, cache.clone(), false);
2674 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
2675 let parent_id = ClientOrderId::from("O-PARENT");
2676 let active_local_id = ClientOrderId::from("O-ACTIVE-LOCAL");
2677 let closed_id = ClientOrderId::from("O-CLOSED");
2678 let parent = OrderTestBuilder::new(OrderType::Limit)
2679 .instrument_id(instrument.id())
2680 .client_order_id(parent_id)
2681 .side(OrderSide::Buy)
2682 .price(Price::from("1.00000"))
2683 .quantity(Quantity::from(100_000))
2684 .contingency_type(contingency_type)
2685 .linked_order_ids(vec![active_local_id, closed_id])
2686 .submit(true)
2687 .build();
2688 let active_local = OrderTestBuilder::new(OrderType::Limit)
2689 .instrument_id(instrument.id())
2690 .client_order_id(active_local_id)
2691 .side(OrderSide::Sell)
2692 .price(Price::from("1.00100"))
2693 .quantity(Quantity::from(100_000))
2694 .build();
2695 let closed = OrderTestBuilder::new(OrderType::Limit)
2696 .instrument_id(instrument.id())
2697 .client_order_id(closed_id)
2698 .side(OrderSide::Sell)
2699 .price(Price::from("1.00100"))
2700 .quantity(Quantity::from(100_000))
2701 .submit(true)
2702 .build();
2703 cache
2704 .borrow_mut()
2705 .add_order(parent.clone(), None, None, false)
2706 .unwrap();
2707 cache
2708 .borrow_mut()
2709 .add_order(active_local, None, None, false)
2710 .unwrap();
2711 cache
2712 .borrow_mut()
2713 .add_order(closed.clone(), None, None, false)
2714 .unwrap();
2715 apply_terminal(&cache, &closed, TerminalEvent::Canceled);
2716 let event = apply_fill(
2717 &cache,
2718 &parent,
2719 &instrument,
2720 "T-EXCLUSION",
2721 Quantity::from(100_000),
2722 );
2723
2724 let actions = manager.handle_event(&event);
2725
2726 assert!(actions.is_empty());
2727 }
2728}