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 instruments::Instrument,
28 orders::{Order, OrderAny},
29 types::Quantity,
30};
31
32use super::OrderManagerAction;
33
34pub struct OrderManager {
41 clock: Rc<RefCell<dyn Clock>>,
42 cache: Rc<RefCell<Cache>>,
43 active_local: bool,
44 submit_order_commands: AHashMap<ClientOrderId, SubmitOrder>,
45 oto_target_quantities: AHashMap<ClientOrderId, Quantity>,
46}
47
48impl Debug for OrderManager {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 f.debug_struct(stringify!(OrderManager))
51 .field("pending_commands", &self.submit_order_commands.len())
52 .finish()
53 }
54}
55
56impl OrderManager {
57 pub fn new(
59 clock: Rc<RefCell<dyn Clock>>,
60 cache: Rc<RefCell<Cache>>,
61 active_local: bool,
62 ) -> Self {
63 Self {
64 clock,
65 cache,
66 active_local,
67 submit_order_commands: AHashMap::new(),
68 oto_target_quantities: AHashMap::new(),
69 }
70 }
71
72 #[must_use]
73 pub fn get_submit_order_commands(&self) -> AHashMap<ClientOrderId, SubmitOrder> {
75 self.submit_order_commands.clone()
76 }
77
78 pub fn cache_submit_order_command(&mut self, command: SubmitOrder) {
80 self.submit_order_commands
81 .insert(command.client_order_id, command);
82 }
83
84 pub fn pop_submit_order_command(
86 &mut self,
87 client_order_id: ClientOrderId,
88 ) -> Option<SubmitOrder> {
89 self.submit_order_commands.remove(&client_order_id)
90 }
91
92 pub fn reset(&mut self) {
94 self.submit_order_commands.clear();
95 self.oto_target_quantities.clear();
96 }
97
98 pub fn cancel_order(&mut self, order: &OrderAny) -> Vec<OrderManagerAction> {
100 let client_order_id = order.client_order_id();
101 let cache = self.cache.borrow();
102
103 if cache.is_order_pending_cancel_local(&client_order_id) {
104 return Vec::new();
105 }
106
107 if order.is_closed() || cache.is_order_closed(&client_order_id) {
108 log::warn!("Cannot cancel order: already closed");
109 return Vec::new();
110 }
111
112 drop(cache);
113 self.submit_order_commands.remove(&client_order_id);
114
115 vec![OrderManagerAction::CancelLocal(order.clone())]
116 }
117
118 pub fn modify_order_quantity(
120 &mut self,
121 order: &OrderAny,
122 new_quantity: Quantity,
123 ) -> Vec<OrderManagerAction> {
124 vec![OrderManagerAction::ModifyLocalQuantity {
125 order: order.clone(),
126 quantity: new_quantity,
127 }]
128 }
129
130 pub fn create_new_submit_order(
134 &mut self,
135 order: &OrderAny,
136 position_id: Option<PositionId>,
137 client_id: Option<ClientId>,
138 correlation_id: Option<UUID4>,
139 ) -> anyhow::Result<Vec<OrderManagerAction>> {
140 let mut actions = Vec::new();
141 let order_exists = self.cache.borrow().order_exists(&order.client_order_id());
142
143 self.cache
144 .borrow_mut()
145 .add_order(order.clone(), position_id, client_id, true)?;
146
147 if !order_exists {
148 actions.push(initialized_action(order));
149 }
150
151 let submit = SubmitOrder::new(
152 order.trader_id(),
153 client_id,
154 order.strategy_id(),
155 order.instrument_id(),
156 order.client_order_id(),
157 order.init_event().clone(),
158 order.exec_algorithm_id(),
159 position_id,
160 None, UUID4::new(),
162 self.clock.borrow().timestamp_ns(),
163 correlation_id,
164 );
165
166 self.cache_submit_order_command(submit.clone());
167
168 if order.emulation_trigger().is_some() {
169 actions.push(OrderManagerAction::SubmitToEmulator(submit));
170 } else {
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 cached_position_id = self
317 .cache
318 .borrow()
319 .position_id(&order.client_order_id())
320 .copied();
321
322 if let (Some(fill_position_id), Some(cached_position_id)) =
323 (filled.position_id, cached_position_id)
324 && fill_position_id != cached_position_id
325 {
326 log::error!(
327 "Cannot handle OTO fill: event position {fill_position_id} does not match cached position {cached_position_id}"
328 );
329 return actions;
330 }
331
332 let position_id = filled.position_id.or(cached_position_id);
333 let client_id = self
334 .cache
335 .borrow()
336 .client_id(&order.client_order_id())
337 .copied();
338
339 let (parent_filled_qty, is_spawn_active) = match order.exec_spawn_id() {
340 Some(spawn_id) => {
341 let cache = self.cache.borrow();
342 let Some(filled_qty) = cache.exec_spawn_total_filled_qty(&spawn_id, false)
343 else {
344 log::error!("Failed to get spawn filled quantity for {spawn_id}");
345 return actions;
346 };
347 let is_spawn_active = cache
348 .exec_spawn_total_leaves_qty(&spawn_id, true)
349 .is_some_and(|leaves_qty| leaves_qty.is_positive());
350 (filled_qty, is_spawn_active)
351 }
352 None => (order.filled_qty(), false),
353 };
354
355 let Some(linked_orders) = order.linked_order_ids() else {
356 log::error!("No linked orders found for OTO order");
357 return actions;
358 };
359
360 for client_order_id in linked_orders {
361 if order.parent_order_id().as_ref() == Some(client_order_id) {
362 continue;
363 }
364
365 let Some(mut child_order) = self.cache.borrow().order_owned(client_order_id)
366 else {
367 log::error!(
368 "Cannot find OTO child order for client_order_id: {client_order_id}"
369 );
370 continue;
371 };
372
373 if !self.should_manage_order(&child_order) || child_order.is_closed() {
374 self.oto_target_quantities.remove(client_order_id);
375 continue;
376 }
377
378 if self.active_local && child_order.position_id().is_none() {
379 child_order.set_position_id(position_id);
380 }
381
382 let Some(child_quantity) = self.position_adjusted_oto_quantity(
383 &order,
384 &child_order,
385 parent_filled_qty,
386 child_order.filled_qty(),
387 position_id,
388 ) else {
389 continue;
390 };
391
392 if child_quantity.is_zero() {
393 self.oto_target_quantities.remove(client_order_id);
394
395 if order.is_closed() && !is_spawn_active {
396 actions.extend(self.cancel_order(&child_order));
397 }
398 continue;
399 }
400
401 actions.extend(self.sync_oto_quantity(&child_order, child_quantity));
402
403 if child_quantity.is_positive()
404 && self.active_local
405 && !self
406 .submit_order_commands
407 .contains_key(&child_order.client_order_id())
408 {
409 match self.create_new_submit_order(
410 &child_order,
411 position_id,
412 client_id,
413 None,
414 ) {
415 Ok(new_actions) => actions.extend(new_actions),
416 Err(e) => log::error!("Failed to create new submit order: {e}"),
417 }
418 }
419 }
420 }
421 Some(ContingencyType::Oco) => {
422 let Some(linked_orders) = order.linked_order_ids() else {
423 log::error!("No linked orders found for OCO order");
424 return actions;
425 };
426
427 for client_order_id in linked_orders {
428 let Some(contingent_order) = self.cache.borrow().order_owned(client_order_id)
429 else {
430 log::error!(
431 "Cannot find OCO contingent order for client_order_id: {client_order_id}"
432 );
433 continue;
434 };
435
436 if !self.should_manage_order(&contingent_order) || contingent_order.is_closed()
438 {
439 continue;
440 }
441
442 if contingent_order.client_order_id() != order.client_order_id() {
443 actions.extend(self.cancel_order(&contingent_order));
444 }
445 }
446 }
447 Some(ContingencyType::Ouo) => actions.extend(self.handle_contingencies(&order)),
448 _ => {}
449 }
450
451 actions
452 }
453
454 pub fn handle_contingencies(&mut self, order: &OrderAny) -> Vec<OrderManagerAction> {
455 let mut actions = Vec::new();
456
457 let (filled_qty, leaves_qty, is_spawn_active) =
458 if let Some(exec_spawn_id) = order.exec_spawn_id() {
459 let cache = self.cache.borrow();
460 let Some(filled) = cache.exec_spawn_total_filled_qty(&exec_spawn_id, false) else {
461 log::error!("Failed to get spawn filled quantity for {exec_spawn_id}");
462 return actions;
463 };
464 let leaves = cache
465 .exec_spawn_total_leaves_qty(&exec_spawn_id, true)
466 .unwrap_or_else(|| Quantity::zero(filled.precision));
467
468 (filled, leaves, leaves.is_positive())
469 } else {
470 (order.filled_qty(), order.leaves_qty(), false)
471 };
472
473 let Some(linked_orders) = order.linked_order_ids() else {
474 log::error!("No linked orders found");
475 return actions;
476 };
477
478 for client_order_id in linked_orders {
479 if order.contingency_type() == Some(ContingencyType::Oto)
480 && order.parent_order_id().as_ref() == Some(client_order_id)
481 {
482 continue;
483 }
484
485 let Some(contingent_order) = self.cache.borrow().order_owned(client_order_id) else {
486 log::error!("Cannot find contingent order for client_order_id: {client_order_id}");
487 continue;
488 };
489
490 if !self.should_manage_order(&contingent_order) {
491 self.oto_target_quantities.remove(client_order_id);
492 continue;
493 }
494
495 if client_order_id == &order.client_order_id() {
496 continue;
497 }
498
499 if contingent_order.is_closed() {
500 self.submit_order_commands.remove(client_order_id);
501 self.oto_target_quantities.remove(client_order_id);
502 continue;
503 }
504
505 match order.contingency_type() {
506 Some(ContingencyType::Oto) => {
507 if filled_qty.is_zero()
508 && order.is_closed()
509 && (order.exec_spawn_id().is_none() || !is_spawn_active)
510 {
511 self.oto_target_quantities.remove(client_order_id);
512 actions.extend(self.cancel_order(&contingent_order));
513 continue;
514 }
515
516 let position_id = self.oto_parent_position_id(order);
517
518 let Some(child_quantity) = self.position_adjusted_oto_quantity(
519 order,
520 &contingent_order,
521 filled_qty,
522 contingent_order.filled_qty(),
523 position_id,
524 ) else {
525 continue;
526 };
527
528 if child_quantity.is_zero() {
529 self.oto_target_quantities.remove(client_order_id);
530
531 if order.is_closed()
532 && (order.exec_spawn_id().is_none() || !is_spawn_active)
533 {
534 actions.extend(self.cancel_order(&contingent_order));
535 }
536 } else {
537 actions.extend(self.sync_oto_quantity(&contingent_order, child_quantity));
538 }
539 }
540 Some(ContingencyType::Oco)
541 if order.is_closed()
542 && (order.exec_spawn_id().is_none() || !is_spawn_active) =>
543 {
544 actions.extend(self.cancel_order(&contingent_order));
545 }
546 Some(ContingencyType::Ouo) => {
547 if (leaves_qty.is_zero() && order.exec_spawn_id().is_some())
548 || (order.is_closed()
549 && (order.exec_spawn_id().is_none() || !is_spawn_active))
550 || contingent_order.filled_qty() >= leaves_qty
551 {
552 actions.extend(self.cancel_order(&contingent_order));
553 } else if leaves_qty != contingent_order.leaves_qty() {
554 actions.extend(self.modify_order_quantity(&contingent_order, leaves_qty));
555 }
556 }
557 _ => {}
558 }
559 }
560
561 actions
562 }
563
564 pub fn handle_contingencies_update(&mut self, order: &OrderAny) -> Vec<OrderManagerAction> {
565 let mut actions = Vec::new();
566 let contingency_type = order.contingency_type();
567
568 let quantity = match order.exec_spawn_id() {
569 Some(exec_spawn_id) => {
570 if let Some(qty) = self
571 .cache
572 .borrow()
573 .exec_spawn_total_quantity(&exec_spawn_id, true)
574 {
575 qty
576 } else {
577 log::error!("Failed to get spawn total quantity for {exec_spawn_id}");
578 return actions;
579 }
580 }
581 None => order.quantity(),
582 };
583
584 if quantity.is_zero() {
585 return actions;
586 }
587
588 let oto_filled_qty = if contingency_type == Some(ContingencyType::Oto) {
589 Some(match order.exec_spawn_id() {
590 Some(exec_spawn_id) => {
591 if let Some(qty) = self
592 .cache
593 .borrow()
594 .exec_spawn_total_filled_qty(&exec_spawn_id, false)
595 {
596 qty
597 } else {
598 log::error!("Failed to get spawn filled quantity for {exec_spawn_id}");
599 return actions;
600 }
601 }
602 None => order.filled_qty(),
603 })
604 } else {
605 None
606 };
607
608 let Some(linked_orders) = order.linked_order_ids() else {
609 log::error!("No linked orders found for contingent order");
610 return actions;
611 };
612
613 for client_order_id in linked_orders {
614 if contingency_type == Some(ContingencyType::Oto)
615 && order.parent_order_id().as_ref() == Some(client_order_id)
616 {
617 continue;
618 }
619
620 let Some(contingent_order) = self.cache.borrow().order_owned(client_order_id) else {
621 log::error!(
622 "Cannot find OCO contingent order for client_order_id: {client_order_id}"
623 );
624 continue;
625 };
626
627 if !self.should_manage_order(&contingent_order) {
628 self.oto_target_quantities.remove(client_order_id);
629 continue;
630 }
631
632 if client_order_id == &order.client_order_id() || contingent_order.is_closed() {
633 self.oto_target_quantities.remove(client_order_id);
634 continue;
635 }
636
637 match contingency_type {
638 Some(ContingencyType::Oto) => {
639 let child_quantity =
640 if let Some(filled_qty) = oto_filled_qty.filter(Quantity::is_positive) {
641 let position_id = self.oto_parent_position_id(order);
642
643 let Some(quantity) = self.position_adjusted_oto_quantity(
644 order,
645 &contingent_order,
646 filled_qty,
647 contingent_order.filled_qty(),
648 position_id,
649 ) else {
650 continue;
651 };
652 quantity
653 } else {
654 quantity
655 };
656
657 actions.extend(self.sync_oto_quantity(&contingent_order, child_quantity));
658 }
659 Some(ContingencyType::Ouo) if contingent_order.filled_qty() >= quantity => {
660 actions.extend(self.cancel_order(&contingent_order));
661 }
662 Some(ContingencyType::Ouo) if quantity != contingent_order.quantity() => {
663 actions.extend(self.modify_order_quantity(&contingent_order, quantity));
664 }
665 _ => {}
666 }
667 }
668
669 actions
670 }
671
672 fn oto_parent_position_id(&self, order: &OrderAny) -> Option<PositionId> {
673 if let Some(position_id) = order.position_id() {
674 return Some(position_id);
675 }
676
677 let cache = self.cache.borrow();
678
679 cache
680 .position_id(&order.client_order_id())
681 .copied()
682 .or_else(|| {
683 order.exec_spawn_id().and_then(|spawn_id| {
684 cache
685 .orders_for_exec_spawn(&spawn_id)
686 .into_iter()
687 .find_map(|spawn_order| spawn_order.position_id())
688 })
689 })
690 }
691
692 fn position_adjusted_oto_quantity(
693 &self,
694 parent_order: &OrderAny,
695 child_order: &OrderAny,
696 filled_qty: Quantity,
697 child_filled_qty: Quantity,
698 position_id: Option<PositionId>,
699 ) -> Option<Quantity> {
700 let Some(position_id) = position_id else {
701 return Some(filled_qty);
702 };
703
704 let cache = self.cache.borrow();
705 let Some(parent_instrument) = cache.instrument(&parent_order.instrument_id()) else {
706 log::error!(
707 "Cannot size OTO orders: instrument {} not found in cache",
708 parent_order.instrument_id()
709 );
710 return None;
711 };
712
713 let Some(child_instrument) = cache.instrument(&child_order.instrument_id()) else {
714 log::error!(
715 "Cannot size OTO order {}: instrument {} not found in cache",
716 child_order.client_order_id(),
717 child_order.instrument_id()
718 );
719 return None;
720 };
721
722 let increment = child_instrument.size_increment();
723 if increment.is_zero() {
724 log::error!(
725 "Cannot size OTO orders: size increment is zero for {}",
726 child_order.instrument_id()
727 );
728 return None;
729 }
730
731 let capped = if parent_instrument.is_spread() || !child_order.is_reduce_only() {
732 filled_qty
733 } else {
734 let Some(position) = cache.position(&position_id) else {
735 log::error!("Cannot size OTO orders: position {position_id} not found in cache");
736 return None;
737 };
738
739 let position_cap = child_filled_qty.saturating_add(position.quantity);
740 filled_qty.min(position_cap)
741 };
742
743 let target_raw = capped.raw() - capped.raw() % increment.raw();
744
745 let target_raw = if child_instrument
746 .min_quantity()
747 .is_some_and(|min_quantity| target_raw < min_quantity.raw())
748 {
749 0
750 } else {
751 target_raw
752 };
753
754 match Quantity::from_raw_checked(target_raw, child_instrument.size_precision()) {
755 Ok(quantity) => Some(quantity),
756 Err(e) => {
757 log::error!(
758 "Cannot size OTO orders for {} from raw quantity {target_raw}: {e}",
759 child_order.instrument_id()
760 );
761 None
762 }
763 }
764 }
765
766 fn sync_oto_quantity(
767 &mut self,
768 order: &OrderAny,
769 quantity: Quantity,
770 ) -> Vec<OrderManagerAction> {
771 let client_order_id = order.client_order_id();
772
773 if quantity.is_zero() {
774 self.oto_target_quantities.remove(&client_order_id);
775 return Vec::new();
776 }
777
778 if order.filled_qty() >= quantity {
779 self.oto_target_quantities.remove(&client_order_id);
780 return self.cancel_order(order);
781 }
782
783 match self.oto_target_quantities.get(&client_order_id).copied() {
784 Some(target) if target == quantity => return Vec::new(),
785 Some(_) if order.quantity() == quantity && !order.is_pending_update() => {
786 self.oto_target_quantities.insert(client_order_id, quantity);
787 return Vec::new();
788 }
789 Some(_) => {}
790 None if order.quantity() == quantity => return Vec::new(),
791 None => {}
792 }
793
794 self.oto_target_quantities.insert(client_order_id, quantity);
795 self.modify_order_quantity(order, quantity)
796 }
797}
798
799fn initialized_action(order: &OrderAny) -> OrderManagerAction {
800 let event = OrderEventAny::Initialized(order.init_event().clone());
801 OrderManagerAction::PublishInitialized(event)
802}
803
804#[cfg(test)]
805mod tests {
806 use std::{cell::RefCell, rc::Rc};
807
808 use nautilus_common::{cache::Cache, clock::TestClock};
809 use nautilus_core::{UUID4, UnixNanos};
810 use nautilus_model::{
811 enums::{ContingencyType, OmsType, OrderSide, OrderStatus, OrderType, TriggerType},
812 events::order::spec::{
813 OrderAcceptedSpec, OrderCanceledSpec, OrderEmulatedSpec, OrderExpiredSpec,
814 OrderModifyRejectedSpec, OrderPendingUpdateSpec, OrderRejectedSpec, OrderSubmittedSpec,
815 OrderUpdatedSpec,
816 },
817 identifiers::{
818 AccountId, ClientOrderId, ExecAlgorithmId, InstrumentId, StrategyId, TradeId, TraderId,
819 VenueOrderId,
820 },
821 instruments::{
822 Instrument, InstrumentAny,
823 stubs::{audusd_sim, currency_pair_btcusdt, futures_spread_es},
824 },
825 orders::{
826 Order, OrderTestBuilder,
827 stubs::{OrderFilledTestBuilder, TestOrderEventStubs},
828 },
829 position::Position,
830 types::{Money, Price, Quantity},
831 };
832 use rstest::rstest;
833
834 use super::*;
835
836 #[rstest]
839 fn test_handle_event_unhandled_events_are_noop() {
840 let submitted = OrderEventAny::Submitted(
841 OrderSubmittedSpec::builder()
842 .trader_id(TraderId::from("TRADER-001"))
843 .strategy_id(StrategyId::from("STRATEGY-001"))
844 .instrument_id(InstrumentId::from("BTC-USDT.OKX"))
845 .client_order_id(ClientOrderId::from("O-001"))
846 .account_id(AccountId::from("ACCOUNT-001"))
847 .build(),
848 );
849 let accepted = OrderEventAny::Accepted(
850 OrderAcceptedSpec::builder()
851 .trader_id(TraderId::from("TRADER-001"))
852 .strategy_id(StrategyId::from("STRATEGY-001"))
853 .instrument_id(InstrumentId::from("BTC-USDT.OKX"))
854 .client_order_id(ClientOrderId::from("O-001"))
855 .venue_order_id(VenueOrderId::from("V-001"))
856 .account_id(AccountId::from("ACCOUNT-001"))
857 .build(),
858 );
859
860 match submitted {
861 OrderEventAny::Rejected(_) => panic!("Should not match"),
862 OrderEventAny::Canceled(_) => panic!("Should not match"),
863 OrderEventAny::Expired(_) => panic!("Should not match"),
864 OrderEventAny::Updated(_) => panic!("Should not match"),
865 OrderEventAny::Filled(_) => panic!("Should not match"),
866 _ => {}
867 }
868
869 match accepted {
870 OrderEventAny::Rejected(_) => panic!("Should not match"),
871 OrderEventAny::Canceled(_) => panic!("Should not match"),
872 OrderEventAny::Expired(_) => panic!("Should not match"),
873 OrderEventAny::Updated(_) => panic!("Should not match"),
874 OrderEventAny::Filled(_) => panic!("Should not match"),
875 _ => {}
876 }
877 }
878
879 #[rstest]
880 fn test_actionable_events_for_missing_orders_are_noops() {
881 let (clock, cache) = create_test_components();
882 let mut manager = OrderManager::new(clock, cache, false);
883 let trader_id = TraderId::from("TRADER-001");
884 let strategy_id = StrategyId::from("STRATEGY-001");
885 let instrument_id = InstrumentId::from("BTC-USDT.OKX");
886 let client_order_id = ClientOrderId::from("O-MISSING");
887 let account_id = AccountId::from("ACCOUNT-001");
888 let events = [
889 OrderEventAny::Rejected(
890 OrderRejectedSpec::builder()
891 .trader_id(trader_id)
892 .strategy_id(strategy_id)
893 .instrument_id(instrument_id)
894 .client_order_id(client_order_id)
895 .account_id(account_id)
896 .reason("test rejection".into())
897 .build(),
898 ),
899 OrderEventAny::Canceled(
900 OrderCanceledSpec::builder()
901 .trader_id(trader_id)
902 .strategy_id(strategy_id)
903 .instrument_id(instrument_id)
904 .client_order_id(client_order_id)
905 .account_id(account_id)
906 .build(),
907 ),
908 OrderEventAny::Expired(
909 OrderExpiredSpec::builder()
910 .trader_id(trader_id)
911 .strategy_id(strategy_id)
912 .instrument_id(instrument_id)
913 .client_order_id(client_order_id)
914 .account_id(account_id)
915 .build(),
916 ),
917 OrderEventAny::Updated(
918 OrderUpdatedSpec::builder()
919 .trader_id(trader_id)
920 .strategy_id(strategy_id)
921 .instrument_id(instrument_id)
922 .client_order_id(client_order_id)
923 .quantity(Quantity::from(100_000))
924 .venue_order_id(VenueOrderId::from("V-MISSING"))
925 .account_id(account_id)
926 .build(),
927 ),
928 ];
929
930 for event in events {
931 assert!(manager.handle_event(&event).is_empty());
932 }
933 }
934
935 fn create_test_components() -> (Rc<RefCell<dyn Clock>>, Rc<RefCell<Cache>>) {
936 let clock: Rc<RefCell<dyn Clock>> = Rc::new(RefCell::new(TestClock::new()));
937 let cache = Rc::new(RefCell::new(Cache::new(None, None)));
938 (clock, cache)
939 }
940
941 fn create_test_stop_order() -> OrderAny {
942 let instrument = audusd_sim();
943 OrderTestBuilder::new(OrderType::StopMarket)
944 .instrument_id(instrument.id())
945 .side(OrderSide::Buy)
946 .trigger_price(Price::from("1.00050"))
947 .quantity(Quantity::from(100_000))
948 .emulation_trigger(TriggerType::BidAsk)
949 .build()
950 }
951
952 fn make_submit_command(order: &OrderAny) -> SubmitOrder {
955 SubmitOrder::new(
956 order.trader_id(),
957 None,
958 order.strategy_id(),
959 order.instrument_id(),
960 order.client_order_id(),
961 order.init_event().clone(),
962 None,
963 None,
964 None,
965 UUID4::new(),
966 UnixNanos::default(),
967 None, )
969 }
970
971 #[rstest]
972 fn test_create_new_submit_order_returns_emulator_submit_action() {
973 let (clock, cache) = create_test_components();
974 let mut manager = OrderManager::new(clock, cache, true);
975 let order = create_test_stop_order();
976
977 let actions = manager
978 .create_new_submit_order(&order, None, None, None)
979 .unwrap();
980
981 assert_eq!(actions.len(), 2);
982 assert!(matches!(
983 &actions[0],
984 OrderManagerAction::PublishInitialized(OrderEventAny::Initialized(event))
985 if event.client_order_id == order.client_order_id()
986 ));
987 assert!(matches!(
988 &actions[1],
989 OrderManagerAction::SubmitToEmulator(command)
990 if command.client_order_id == order.client_order_id()
991 ));
992 assert!(
993 manager
994 .submit_order_commands
995 .contains_key(&order.client_order_id())
996 );
997 }
998
999 #[rstest]
1000 fn test_reset_clears_submit_commands_and_oto_targets() {
1001 let (clock, cache) = create_test_components();
1002 let mut manager = OrderManager::new(clock, cache, true);
1003 let order = create_test_stop_order();
1004 let client_order_id = order.client_order_id();
1005 manager.cache_submit_order_command(make_submit_command(&order));
1006 manager
1007 .oto_target_quantities
1008 .insert(client_order_id, Quantity::from(40_000));
1009
1010 manager.reset();
1011
1012 assert!(manager.submit_order_commands.is_empty());
1013 assert!(manager.oto_target_quantities.is_empty());
1014 }
1015
1016 #[rstest]
1017 fn test_create_new_submit_order_returns_risk_submit_action() {
1018 let (clock, cache) = create_test_components();
1019 let mut manager = OrderManager::new(clock, cache, true);
1020 let order = OrderTestBuilder::new(OrderType::Limit)
1021 .instrument_id(audusd_sim().id())
1022 .side(OrderSide::Buy)
1023 .price(Price::from("1.00000"))
1024 .quantity(Quantity::from(100_000))
1025 .build();
1026
1027 let actions = manager
1028 .create_new_submit_order(&order, None, None, None)
1029 .unwrap();
1030
1031 assert_eq!(actions.len(), 2);
1032 assert!(matches!(
1033 &actions[0],
1034 OrderManagerAction::PublishInitialized(OrderEventAny::Initialized(event))
1035 if event.client_order_id == order.client_order_id()
1036 ));
1037 assert!(matches!(
1038 &actions[1],
1039 OrderManagerAction::SubmitToRisk(command)
1040 if command.client_order_id == order.client_order_id()
1041 ));
1042 }
1043
1044 #[rstest]
1045 fn test_create_new_submit_order_returns_risk_action_for_none_trigger() {
1046 let (clock, cache) = create_test_components();
1047 let mut manager = OrderManager::new(clock, cache, true);
1048 let order = OrderTestBuilder::new(OrderType::Market)
1049 .instrument_id(audusd_sim().id())
1050 .side(OrderSide::Buy)
1051 .quantity(Quantity::from(100_000))
1052 .build();
1053
1054 let actions = manager
1055 .create_new_submit_order(&order, None, None, None)
1056 .unwrap();
1057
1058 assert_eq!(actions.len(), 2);
1059 assert!(order.emulation_trigger().is_none());
1060 assert!(matches!(
1061 &actions[1],
1062 OrderManagerAction::SubmitToRisk(command)
1063 if command.client_order_id == order.client_order_id()
1064 ));
1065 }
1066
1067 #[rstest]
1068 fn test_create_new_submit_order_returns_algorithm_submit_action() {
1069 let (clock, cache) = create_test_components();
1070 let mut manager = OrderManager::new(clock, cache, true);
1071 let exec_algorithm_id = ExecAlgorithmId::from("ALG-001");
1072 let client_order_id = ClientOrderId::from("O-001");
1073 let order = OrderTestBuilder::new(OrderType::Limit)
1074 .client_order_id(client_order_id)
1075 .instrument_id(audusd_sim().id())
1076 .side(OrderSide::Buy)
1077 .price(Price::from("1.00000"))
1078 .quantity(Quantity::from(100_000))
1079 .exec_algorithm_id(exec_algorithm_id)
1080 .exec_spawn_id(client_order_id)
1081 .build();
1082
1083 let actions = manager
1084 .create_new_submit_order(&order, None, None, None)
1085 .unwrap();
1086
1087 assert_eq!(actions.len(), 2);
1088 assert!(matches!(
1089 &actions[1],
1090 OrderManagerAction::SubmitToAlgorithm {
1091 command,
1092 exec_algorithm_id: action_exec_algorithm_id,
1093 } if command.client_order_id == order.client_order_id()
1094 && *action_exec_algorithm_id == exec_algorithm_id
1095 ));
1096 }
1097
1098 #[rstest]
1099 fn test_create_new_submit_order_does_not_republish_initialized_for_existing_order() {
1100 let (clock, cache) = create_test_components();
1101 let mut manager = OrderManager::new(clock, cache.clone(), true);
1102 let order = create_test_stop_order();
1103 cache
1104 .borrow_mut()
1105 .add_order(order.clone(), None, None, true)
1106 .unwrap();
1107
1108 let actions = manager
1109 .create_new_submit_order(&order, None, None, None)
1110 .unwrap();
1111
1112 assert_eq!(actions.len(), 1);
1113 assert!(matches!(
1114 &actions[0],
1115 OrderManagerAction::SubmitToEmulator(command)
1116 if command.client_order_id == order.client_order_id()
1117 ));
1118 }
1119
1120 #[rstest]
1121 fn test_cancel_order_returns_cancel_local_action() {
1122 let (clock, cache) = create_test_components();
1123 let mut manager = OrderManager::new(clock, cache.clone(), true);
1124 let order = create_test_stop_order();
1125 cache
1126 .borrow_mut()
1127 .add_order(order.clone(), None, None, false)
1128 .unwrap();
1129 manager
1130 .submit_order_commands
1131 .insert(order.client_order_id(), make_submit_command(&order));
1132
1133 let actions = manager.cancel_order(&order);
1134
1135 assert!(matches!(
1136 actions.as_slice(),
1137 [OrderManagerAction::CancelLocal(action_order)]
1138 if action_order.client_order_id() == order.client_order_id()
1139 ));
1140 assert!(
1141 !manager
1142 .submit_order_commands
1143 .contains_key(&order.client_order_id()),
1144 "expected cancel action path to remove the submit command",
1145 );
1146 }
1147
1148 #[rstest]
1149 fn test_modify_order_quantity_returns_modify_local_action() {
1150 let (clock, cache) = create_test_components();
1151 let mut manager = OrderManager::new(clock, cache, true);
1152 let order = create_test_stop_order();
1153 let new_quantity = Quantity::from(50_000);
1154
1155 let actions = manager.modify_order_quantity(&order, new_quantity);
1156
1157 assert!(matches!(
1158 actions.as_slice(),
1159 [OrderManagerAction::ModifyLocalQuantity { order: action_order, quantity }]
1160 if action_order.client_order_id() == order.client_order_id()
1161 && *quantity == new_quantity
1162 ));
1163 }
1164
1165 #[rstest]
1166 fn test_sync_oto_quantity_reasserts_same_quantity_while_pending_update() {
1167 let (clock, cache) = create_test_components();
1168 let mut manager = OrderManager::new(clock, cache.clone(), false);
1169 let client_order_id = ClientOrderId::from("O-CHILD");
1170 let order = OrderTestBuilder::new(OrderType::Limit)
1171 .instrument_id(audusd_sim().id())
1172 .client_order_id(client_order_id)
1173 .side(OrderSide::Sell)
1174 .price(Price::from("1.00100"))
1175 .quantity(Quantity::from(100_000))
1176 .submit(true)
1177 .build();
1178 cache
1179 .borrow_mut()
1180 .add_order(order.clone(), None, None, false)
1181 .unwrap();
1182 apply_accepted(&cache, &order, "V-CHILD");
1183 let pending_update = OrderEventAny::PendingUpdate(
1184 OrderPendingUpdateSpec::builder()
1185 .trader_id(order.trader_id())
1186 .strategy_id(order.strategy_id())
1187 .instrument_id(order.instrument_id())
1188 .client_order_id(client_order_id)
1189 .account_id(AccountId::from("ACCOUNT-001"))
1190 .venue_order_id(VenueOrderId::from("V-CHILD"))
1191 .build(),
1192 );
1193 cache.borrow_mut().update_order(&pending_update).unwrap();
1194 manager
1195 .oto_target_quantities
1196 .insert(client_order_id, Quantity::from(40_000));
1197 let cached_order = cache.borrow().order_owned(&client_order_id).unwrap();
1198
1199 let actions = manager.sync_oto_quantity(&cached_order, Quantity::from(100_000));
1200
1201 assert!(matches!(
1202 actions.as_slice(),
1203 [OrderManagerAction::ModifyLocalQuantity { order, quantity }]
1204 if order.client_order_id() == client_order_id
1205 && *quantity == Quantity::from(100_000)
1206 ));
1207 assert_eq!(
1208 manager.oto_target_quantities.get(&client_order_id),
1209 Some(&Quantity::from(100_000)),
1210 );
1211 }
1212
1213 #[rstest]
1214 fn test_handle_event_unhandled_events_return_no_actions() {
1215 let (clock, cache) = create_test_components();
1216 let mut manager = OrderManager::new(clock, cache, true);
1217 let order = create_test_stop_order();
1218 let event = OrderEventAny::Submitted(
1219 OrderSubmittedSpec::builder()
1220 .trader_id(order.trader_id())
1221 .strategy_id(order.strategy_id())
1222 .instrument_id(order.instrument_id())
1223 .client_order_id(order.client_order_id())
1224 .account_id(AccountId::from("ACCOUNT-001"))
1225 .build(),
1226 );
1227
1228 let actions = manager.handle_event(&event);
1229
1230 assert!(actions.is_empty());
1231 }
1232
1233 #[rstest]
1234 fn test_handle_order_filled_skips_missing_oco_contingent_order() {
1235 let (clock, cache) = create_test_components();
1236 let mut manager = OrderManager::new(clock, cache.clone(), true);
1237 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
1238 let missing_client_order_id = ClientOrderId::from("O-MISSING");
1239 let valid_client_order_id = ClientOrderId::from("O-CHILD");
1240 let order = OrderTestBuilder::new(OrderType::Limit)
1241 .instrument_id(instrument.id())
1242 .client_order_id(ClientOrderId::from("O-PARENT"))
1243 .side(OrderSide::Buy)
1244 .price(Price::from("1.00000"))
1245 .quantity(Quantity::from(100_000))
1246 .contingency_type(ContingencyType::Oco)
1247 .linked_order_ids(vec![missing_client_order_id, valid_client_order_id])
1248 .build();
1249 let child_order = OrderTestBuilder::new(OrderType::Limit)
1250 .instrument_id(instrument.id())
1251 .client_order_id(valid_client_order_id)
1252 .side(OrderSide::Buy)
1253 .price(Price::from("1.00000"))
1254 .quantity(Quantity::from(100_000))
1255 .build();
1256 cache
1257 .borrow_mut()
1258 .add_order(order.clone(), None, None, false)
1259 .unwrap();
1260 cache
1261 .borrow_mut()
1262 .add_order(child_order.clone(), None, None, false)
1263 .unwrap();
1264 manager
1265 .submit_order_commands
1266 .insert(valid_client_order_id, make_submit_command(&child_order));
1267 let filled = match TestOrderEventStubs::filled(
1268 &order,
1269 &instrument,
1270 None,
1271 None,
1272 None,
1273 None,
1274 None,
1275 None,
1276 None,
1277 Some(AccountId::from("SIM-001")),
1278 ) {
1279 OrderEventAny::Filled(event) => event,
1280 event => panic!("expected OrderFilled, was {event:?}"),
1281 };
1282
1283 let actions = manager.handle_order_filled(&filled);
1284
1285 assert!(matches!(
1286 actions.as_slice(),
1287 [OrderManagerAction::CancelLocal(action_order)]
1288 if action_order.client_order_id() == valid_client_order_id
1289 ));
1290 assert!(
1291 !manager
1292 .submit_order_commands
1293 .contains_key(&valid_client_order_id)
1294 );
1295 }
1296
1297 #[rstest]
1298 fn test_handle_event_non_active_local_manager_returns_oco_cancel_action() {
1299 let (clock, cache) = create_test_components();
1300 let mut manager = OrderManager::new(clock, cache.clone(), false);
1301 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
1302 let parent_id = ClientOrderId::from("O-PARENT");
1303 let child_id = ClientOrderId::from("O-CHILD");
1304 let order = OrderTestBuilder::new(OrderType::Limit)
1305 .instrument_id(instrument.id())
1306 .client_order_id(parent_id)
1307 .side(OrderSide::Buy)
1308 .price(Price::from("1.00000"))
1309 .quantity(Quantity::from(100_000))
1310 .contingency_type(ContingencyType::Oco)
1311 .linked_order_ids(vec![parent_id, child_id])
1312 .submit(true)
1313 .build();
1314 let child_order = OrderTestBuilder::new(OrderType::Limit)
1315 .instrument_id(instrument.id())
1316 .client_order_id(child_id)
1317 .side(OrderSide::Buy)
1318 .price(Price::from("1.00000"))
1319 .quantity(Quantity::from(100_000))
1320 .submit(true)
1321 .build();
1322 cache
1323 .borrow_mut()
1324 .add_order(order.clone(), None, None, false)
1325 .unwrap();
1326 cache
1327 .borrow_mut()
1328 .add_order(child_order.clone(), None, None, false)
1329 .unwrap();
1330 manager
1331 .submit_order_commands
1332 .insert(child_id, make_submit_command(&child_order));
1333 let event = TestOrderEventStubs::filled(
1334 &order,
1335 &instrument,
1336 None,
1337 None,
1338 None,
1339 None,
1340 None,
1341 None,
1342 None,
1343 Some(AccountId::from("SIM-001")),
1344 );
1345
1346 let actions = manager.handle_event(&event);
1347
1348 assert!(matches!(
1349 actions.as_slice(),
1350 [OrderManagerAction::CancelLocal(order)]
1351 if order.client_order_id() == child_id
1352 ));
1353 assert!(
1354 !manager.submit_order_commands.contains_key(&child_id),
1355 "cancel action must remove the cached submit command",
1356 );
1357 }
1358
1359 #[rstest]
1360 fn test_handle_order_filled_skips_missing_oto_child_order() {
1361 let (clock, cache) = create_test_components();
1362 let mut manager = OrderManager::new(clock, cache.clone(), true);
1363 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
1364 let missing_client_order_id = ClientOrderId::from("O-MISSING");
1365 let valid_client_order_id = ClientOrderId::from("O-CHILD");
1366 let order = OrderTestBuilder::new(OrderType::Limit)
1367 .instrument_id(instrument.id())
1368 .client_order_id(ClientOrderId::from("O-PARENT"))
1369 .side(OrderSide::Buy)
1370 .price(Price::from("1.00000"))
1371 .quantity(Quantity::from(100_000))
1372 .contingency_type(ContingencyType::Oto)
1373 .linked_order_ids(vec![missing_client_order_id, valid_client_order_id])
1374 .submit(true)
1375 .build();
1376 let child_order = OrderTestBuilder::new(OrderType::Limit)
1377 .instrument_id(instrument.id())
1378 .client_order_id(valid_client_order_id)
1379 .side(OrderSide::Buy)
1380 .price(Price::from("1.00000"))
1381 .quantity(Quantity::from(100_000))
1382 .build();
1383 cache
1384 .borrow_mut()
1385 .add_order(order.clone(), None, None, false)
1386 .unwrap();
1387 cache
1388 .borrow_mut()
1389 .add_order(child_order, None, None, false)
1390 .unwrap();
1391 let filled = match apply_fill(
1392 &cache,
1393 &order,
1394 &instrument,
1395 "T-PARTIAL",
1396 Quantity::from(40_000),
1397 ) {
1398 OrderEventAny::Filled(event) => event,
1399 event => panic!("expected OrderFilled, was {event:?}"),
1400 };
1401
1402 let actions = manager.handle_order_filled(&filled);
1403
1404 assert_eq!(actions.len(), 2);
1405 assert!(matches!(
1406 &actions[0],
1407 OrderManagerAction::ModifyLocalQuantity { order, quantity }
1408 if order.client_order_id() == valid_client_order_id
1409 && *quantity == Quantity::from(40_000)
1410 ));
1411 assert!(matches!(
1412 &actions[1],
1413 OrderManagerAction::SubmitToRisk(command)
1414 if command.client_order_id == valid_client_order_id
1415 ));
1416 assert!(
1417 manager
1418 .submit_order_commands
1419 .contains_key(&valid_client_order_id)
1420 );
1421 }
1422
1423 #[rstest]
1424 fn test_handle_contingencies_skips_missing_linked_order() {
1425 let (clock, cache) = create_test_components();
1426 let mut manager = OrderManager::new(clock, cache, true);
1427 let instrument = audusd_sim();
1428 let order = OrderTestBuilder::new(OrderType::Limit)
1429 .instrument_id(instrument.id())
1430 .client_order_id(ClientOrderId::from("O-PARENT"))
1431 .side(OrderSide::Buy)
1432 .price(Price::from("1.00000"))
1433 .quantity(Quantity::from(100_000))
1434 .contingency_type(ContingencyType::Oco)
1435 .linked_order_ids(vec![ClientOrderId::from("O-MISSING")])
1436 .build();
1437
1438 let actions = manager.handle_contingencies(&order);
1439
1440 assert!(actions.is_empty());
1441 assert!(manager.submit_order_commands.is_empty());
1442 }
1443
1444 #[rstest]
1445 fn test_handle_contingencies_update_skips_missing_linked_order() {
1446 let (clock, cache) = create_test_components();
1447 let mut manager = OrderManager::new(clock, cache, true);
1448 let instrument = audusd_sim();
1449 let order = OrderTestBuilder::new(OrderType::Limit)
1450 .instrument_id(instrument.id())
1451 .client_order_id(ClientOrderId::from("O-PARENT"))
1452 .side(OrderSide::Buy)
1453 .price(Price::from("1.00000"))
1454 .quantity(Quantity::from(100_000))
1455 .contingency_type(ContingencyType::Oco)
1456 .linked_order_ids(vec![ClientOrderId::from("O-MISSING")])
1457 .build();
1458
1459 let actions = manager.handle_contingencies_update(&order);
1460
1461 assert!(actions.is_empty());
1462 assert!(manager.submit_order_commands.is_empty());
1463 }
1464
1465 #[rstest]
1466 fn test_oto_child_contingency_handlers_do_not_drive_parent() {
1467 let (clock, cache) = create_test_components();
1468 let mut terminal_manager = OrderManager::new(clock.clone(), cache.clone(), false);
1469 let mut update_manager = OrderManager::new(clock, cache.clone(), false);
1470 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
1471 let parent_id = ClientOrderId::from("O-PARENT");
1472 let child_id = ClientOrderId::from("O-CHILD");
1473 let parent = OrderTestBuilder::new(OrderType::Limit)
1474 .instrument_id(instrument.id())
1475 .client_order_id(parent_id)
1476 .side(OrderSide::Buy)
1477 .price(Price::from("1.00000"))
1478 .quantity(Quantity::from(100_000))
1479 .contingency_type(ContingencyType::Oto)
1480 .linked_order_ids(vec![child_id])
1481 .submit(true)
1482 .build();
1483 let child = OrderTestBuilder::new(OrderType::Limit)
1484 .instrument_id(instrument.id())
1485 .client_order_id(child_id)
1486 .side(OrderSide::Sell)
1487 .price(Price::from("1.00100"))
1488 .quantity(Quantity::from(100_000))
1489 .contingency_type(ContingencyType::Oto)
1490 .linked_order_ids(vec![parent_id])
1491 .parent_order_id(parent_id)
1492 .submit(true)
1493 .build();
1494 cache
1495 .borrow_mut()
1496 .add_order(parent.clone(), None, None, false)
1497 .unwrap();
1498 cache
1499 .borrow_mut()
1500 .add_order(child.clone(), None, None, false)
1501 .unwrap();
1502 apply_accepted(&cache, &parent, "V-PARENT");
1503 apply_accepted(&cache, &child, "V-CHILD");
1504 apply_fill(
1505 &cache,
1506 &child,
1507 &instrument,
1508 "T-CHILD",
1509 Quantity::from(40_000),
1510 );
1511 let cached_child = cache.borrow().order_owned(&child_id).unwrap();
1512
1513 let terminal_actions = terminal_manager.handle_contingencies(&cached_child);
1514 let update_actions = update_manager.handle_contingencies_update(&cached_child);
1515
1516 assert!(terminal_actions.is_empty());
1517 assert!(update_actions.is_empty());
1518 assert!(terminal_manager.oto_target_quantities.is_empty());
1519 assert!(update_manager.oto_target_quantities.is_empty());
1520 }
1521
1522 #[rstest]
1523 fn test_oto_chained_child_drives_its_child_contingencies() {
1524 let (clock, cache) = create_test_components();
1525 let mut terminal_manager = OrderManager::new(clock.clone(), cache.clone(), false);
1526 let mut update_manager = OrderManager::new(clock, cache.clone(), false);
1527 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
1528 let root_id = ClientOrderId::from("O-ROOT");
1529 let parent_id = ClientOrderId::from("O-PARENT");
1530 let child_id = ClientOrderId::from("O-CHILD");
1531 let parent = OrderTestBuilder::new(OrderType::Limit)
1532 .instrument_id(instrument.id())
1533 .client_order_id(parent_id)
1534 .side(OrderSide::Buy)
1535 .price(Price::from("1.00000"))
1536 .quantity(Quantity::from(120_000))
1537 .contingency_type(ContingencyType::Oto)
1538 .linked_order_ids(vec![child_id])
1539 .parent_order_id(root_id)
1540 .submit(true)
1541 .build();
1542 let child = OrderTestBuilder::new(OrderType::Limit)
1543 .instrument_id(instrument.id())
1544 .client_order_id(child_id)
1545 .side(OrderSide::Sell)
1546 .price(Price::from("1.00100"))
1547 .quantity(Quantity::from(100_000))
1548 .parent_order_id(parent_id)
1549 .submit(true)
1550 .build();
1551 cache
1552 .borrow_mut()
1553 .add_order(parent.clone(), None, None, false)
1554 .unwrap();
1555 cache
1556 .borrow_mut()
1557 .add_order(child.clone(), None, None, false)
1558 .unwrap();
1559 apply_accepted(&cache, &parent, "V-PARENT");
1560 apply_accepted(&cache, &child, "V-CHILD");
1561 let cached_parent = cache.borrow().order_owned(&parent_id).unwrap();
1562
1563 let update_actions = update_manager.handle_contingencies_update(&cached_parent);
1564 let terminal = apply_terminal(&cache, &cached_parent, TerminalEvent::Canceled);
1565 let terminal_actions = terminal_manager.handle_event(&terminal);
1566
1567 assert!(matches!(
1568 update_actions.as_slice(),
1569 [OrderManagerAction::ModifyLocalQuantity { order, quantity }]
1570 if order.client_order_id() == child_id
1571 && *quantity == Quantity::from(120_000)
1572 ));
1573 assert!(matches!(
1574 terminal_actions.as_slice(),
1575 [OrderManagerAction::CancelLocal(order)]
1576 if order.client_order_id() == child_id
1577 ));
1578 }
1579
1580 #[rstest]
1581 fn test_cancel_order_skips_when_pending_cancel_local() {
1582 let (clock, cache) = create_test_components();
1583 let mut manager = OrderManager::new(clock, cache.clone(), true);
1584 let order = create_test_stop_order();
1585 cache
1586 .borrow_mut()
1587 .add_order(order.clone(), None, None, false)
1588 .unwrap();
1589 cache.borrow_mut().update_order_pending_cancel_local(&order);
1590 manager
1591 .submit_order_commands
1592 .insert(order.client_order_id(), make_submit_command(&order));
1593
1594 manager.cancel_order(&order);
1595
1596 assert!(
1597 manager
1598 .submit_order_commands
1599 .contains_key(&order.client_order_id()),
1600 "pending-cancel-local gate should short-circuit before removing the submit command",
1601 );
1602 }
1603
1604 #[rstest]
1605 fn test_cancel_order_skips_when_passed_order_is_closed() {
1606 let (clock, cache) = create_test_components();
1610 let mut manager = OrderManager::new(clock, cache.clone(), true);
1611
1612 let mut order = OrderTestBuilder::new(OrderType::StopMarket)
1613 .instrument_id(audusd_sim().id())
1614 .side(OrderSide::Buy)
1615 .trigger_price(Price::from("1.00050"))
1616 .quantity(Quantity::from(100_000))
1617 .emulation_trigger(TriggerType::BidAsk)
1618 .submit(true)
1619 .build();
1620
1621 cache
1622 .borrow_mut()
1623 .add_order(order.clone(), None, None, false)
1624 .unwrap();
1625
1626 let canceled_event =
1627 TestOrderEventStubs::canceled(&order, AccountId::from("ACCOUNT-001"), None);
1628 order.apply(canceled_event).unwrap();
1629
1630 assert!(order.is_closed());
1631 assert!(!cache.borrow().is_order_closed(&order.client_order_id()));
1632
1633 manager
1634 .submit_order_commands
1635 .insert(order.client_order_id(), make_submit_command(&order));
1636
1637 manager.cancel_order(&order);
1638
1639 assert!(
1640 manager
1641 .submit_order_commands
1642 .contains_key(&order.client_order_id()),
1643 "closed-order gate should short-circuit on the local state when the cache index is stale",
1644 );
1645 }
1646
1647 #[rstest]
1648 fn test_cancel_order_skips_when_cache_index_marks_closed() {
1649 let (clock, cache) = create_test_components();
1653 let mut manager = OrderManager::new(clock, cache.clone(), true);
1654
1655 let mut order = OrderTestBuilder::new(OrderType::StopMarket)
1656 .instrument_id(audusd_sim().id())
1657 .side(OrderSide::Buy)
1658 .trigger_price(Price::from("1.00050"))
1659 .quantity(Quantity::from(100_000))
1660 .emulation_trigger(TriggerType::BidAsk)
1661 .submit(true)
1662 .build();
1663
1664 cache
1665 .borrow_mut()
1666 .add_order(order.clone(), None, None, false)
1667 .unwrap();
1668
1669 let stale_order = order.clone();
1670
1671 let canceled_event =
1672 TestOrderEventStubs::canceled(&order, AccountId::from("ACCOUNT-001"), None);
1673 order = cache.borrow_mut().update_order(&canceled_event).unwrap();
1674
1675 assert!(cache.borrow().is_order_closed(&order.client_order_id()));
1676
1677 manager.submit_order_commands.insert(
1678 stale_order.client_order_id(),
1679 make_submit_command(&stale_order),
1680 );
1681
1682 manager.cancel_order(&stale_order);
1683
1684 assert!(
1685 manager
1686 .submit_order_commands
1687 .contains_key(&stale_order.client_order_id()),
1688 "closed-order gate should short-circuit even when the passed reference is stale",
1689 );
1690 }
1691
1692 #[rstest]
1693 fn test_handle_contingencies_update_syncs_quantity_for_ouo_sibling() {
1694 let (clock, cache) = create_test_components();
1695 let mut manager = OrderManager::new(clock, cache.clone(), true);
1696 let instrument = audusd_sim();
1697 let child = OrderTestBuilder::new(OrderType::Limit)
1698 .instrument_id(instrument.id())
1699 .client_order_id(ClientOrderId::from("O-CHILD"))
1700 .side(OrderSide::Sell)
1701 .price(Price::from("1.00100"))
1702 .quantity(Quantity::from(50_000))
1703 .build();
1704 cache
1705 .borrow_mut()
1706 .add_order(child.clone(), None, None, false)
1707 .unwrap();
1708 let parent = OrderTestBuilder::new(OrderType::Limit)
1709 .instrument_id(instrument.id())
1710 .client_order_id(ClientOrderId::from("O-PARENT"))
1711 .side(OrderSide::Buy)
1712 .price(Price::from("1.00000"))
1713 .quantity(Quantity::from(100_000))
1714 .contingency_type(ContingencyType::Ouo)
1715 .linked_order_ids(vec![child.client_order_id()])
1716 .build();
1717
1718 let actions = manager.handle_contingencies_update(&parent);
1719
1720 assert_eq!(actions.len(), 1);
1721 assert!(matches!(
1722 &actions[0],
1723 OrderManagerAction::ModifyLocalQuantity { order, quantity }
1724 if order.client_order_id() == child.client_order_id()
1725 && *quantity == Quantity::from(100_000)
1726 ));
1727 }
1728
1729 #[rstest]
1730 fn test_handle_contingencies_update_syncs_oto_child_before_parent_fill() {
1731 let (clock, cache) = create_test_components();
1732 let mut manager = OrderManager::new(clock, cache.clone(), false);
1733 let instrument = audusd_sim();
1734 let child = OrderTestBuilder::new(OrderType::Limit)
1735 .instrument_id(instrument.id())
1736 .client_order_id(ClientOrderId::from("O-CHILD"))
1737 .side(OrderSide::Sell)
1738 .price(Price::from("1.00100"))
1739 .quantity(Quantity::from(100_000))
1740 .submit(true)
1741 .build();
1742 cache
1743 .borrow_mut()
1744 .add_order(child.clone(), None, None, false)
1745 .unwrap();
1746 let parent = OrderTestBuilder::new(OrderType::Limit)
1747 .instrument_id(instrument.id())
1748 .client_order_id(ClientOrderId::from("O-PARENT"))
1749 .side(OrderSide::Buy)
1750 .price(Price::from("1.00000"))
1751 .quantity(Quantity::from(120_000))
1752 .contingency_type(ContingencyType::Oto)
1753 .linked_order_ids(vec![child.client_order_id()])
1754 .submit(true)
1755 .build();
1756
1757 let actions = manager.handle_contingencies_update(&parent);
1758
1759 assert!(matches!(
1760 actions.as_slice(),
1761 [OrderManagerAction::ModifyLocalQuantity { order, quantity }]
1762 if order.client_order_id() == child.client_order_id()
1763 && *quantity == Quantity::from(120_000)
1764 ));
1765 assert_eq!(
1766 manager.oto_target_quantities.get(&child.client_order_id()),
1767 Some(&Quantity::from(120_000)),
1768 );
1769 }
1770
1771 #[rstest]
1772 fn test_handle_contingencies_update_clears_target_for_closed_oto_child() {
1773 let (clock, cache) = create_test_components();
1774 let mut manager = OrderManager::new(clock, cache.clone(), false);
1775 let instrument = audusd_sim();
1776 let child = OrderTestBuilder::new(OrderType::Limit)
1777 .instrument_id(instrument.id())
1778 .client_order_id(ClientOrderId::from("O-CHILD"))
1779 .side(OrderSide::Sell)
1780 .price(Price::from("1.00100"))
1781 .quantity(Quantity::from(100_000))
1782 .submit(true)
1783 .build();
1784 cache
1785 .borrow_mut()
1786 .add_order(child.clone(), None, None, false)
1787 .unwrap();
1788 apply_terminal(&cache, &child, TerminalEvent::Canceled);
1789 manager
1790 .oto_target_quantities
1791 .insert(child.client_order_id(), Quantity::from(40_000));
1792 let parent = OrderTestBuilder::new(OrderType::Limit)
1793 .instrument_id(instrument.id())
1794 .client_order_id(ClientOrderId::from("O-PARENT"))
1795 .side(OrderSide::Buy)
1796 .price(Price::from("1.00000"))
1797 .quantity(Quantity::from(100_000))
1798 .contingency_type(ContingencyType::Oto)
1799 .linked_order_ids(vec![child.client_order_id()])
1800 .submit(true)
1801 .build();
1802
1803 let actions = manager.handle_contingencies_update(&parent);
1804
1805 assert!(actions.is_empty());
1806 assert!(manager.oto_target_quantities.is_empty());
1807 }
1808
1809 #[rstest]
1810 fn test_handle_contingencies_update_cancels_ouo_sibling_filled_past_target() {
1811 let (clock, cache) = create_test_components();
1812 let mut manager = OrderManager::new(clock, cache.clone(), false);
1813 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
1814 let child = OrderTestBuilder::new(OrderType::Limit)
1815 .instrument_id(instrument.id())
1816 .client_order_id(ClientOrderId::from("O-CHILD"))
1817 .side(OrderSide::Sell)
1818 .price(Price::from("1.00100"))
1819 .quantity(Quantity::from(100_000))
1820 .submit(true)
1821 .build();
1822 cache
1823 .borrow_mut()
1824 .add_order(child.clone(), None, None, false)
1825 .unwrap();
1826 apply_fill(
1827 &cache,
1828 &child,
1829 &instrument,
1830 "T-CHILD",
1831 Quantity::from(60_000),
1832 );
1833 let parent = OrderTestBuilder::new(OrderType::Limit)
1834 .instrument_id(instrument.id())
1835 .client_order_id(ClientOrderId::from("O-PARENT"))
1836 .side(OrderSide::Buy)
1837 .price(Price::from("1.00000"))
1838 .quantity(Quantity::from(50_000))
1839 .contingency_type(ContingencyType::Ouo)
1840 .linked_order_ids(vec![child.client_order_id()])
1841 .submit(true)
1842 .build();
1843
1844 let actions = manager.handle_contingencies_update(&parent);
1845
1846 assert!(matches!(
1847 actions.as_slice(),
1848 [OrderManagerAction::CancelLocal(order)]
1849 if order.client_order_id() == child.client_order_id()
1850 ));
1851 }
1852
1853 #[rstest]
1854 fn test_handle_contingencies_update_does_not_sync_quantity_for_oco_sibling() {
1855 let (clock, cache) = create_test_components();
1856 let mut manager = OrderManager::new(clock, cache.clone(), true);
1857 let instrument = audusd_sim();
1858 let child = OrderTestBuilder::new(OrderType::Limit)
1859 .instrument_id(instrument.id())
1860 .client_order_id(ClientOrderId::from("O-CHILD"))
1861 .side(OrderSide::Sell)
1862 .price(Price::from("1.00100"))
1863 .quantity(Quantity::from(50_000))
1864 .build();
1865 cache
1866 .borrow_mut()
1867 .add_order(child.clone(), None, None, false)
1868 .unwrap();
1869 let parent = OrderTestBuilder::new(OrderType::Limit)
1870 .instrument_id(instrument.id())
1871 .client_order_id(ClientOrderId::from("O-PARENT"))
1872 .side(OrderSide::Buy)
1873 .price(Price::from("1.00000"))
1874 .quantity(Quantity::from(100_000))
1875 .contingency_type(ContingencyType::Oco)
1876 .linked_order_ids(vec![child.client_order_id()])
1877 .build();
1878
1879 let actions = manager.handle_contingencies_update(&parent);
1880
1881 assert!(actions.is_empty());
1882 }
1883
1884 #[derive(Debug, Clone, Copy)]
1885 enum TerminalEvent {
1886 Canceled,
1887 Expired,
1888 Rejected,
1889 }
1890
1891 fn apply_fill(
1892 cache: &Rc<RefCell<Cache>>,
1893 order: &OrderAny,
1894 instrument: &InstrumentAny,
1895 trade_id: &str,
1896 last_qty: Quantity,
1897 ) -> OrderEventAny {
1898 let event = OrderFilledTestBuilder::new(order, instrument)
1899 .trade_id(TradeId::from(trade_id))
1900 .last_qty(last_qty)
1901 .account_id(AccountId::from("ACCOUNT-001"))
1902 .without_position_id()
1903 .build();
1904 cache.borrow_mut().update_order(&event).unwrap();
1905 event
1906 }
1907
1908 fn apply_position_fill(
1909 cache: &Rc<RefCell<Cache>>,
1910 order: &OrderAny,
1911 instrument: &InstrumentAny,
1912 position_id: PositionId,
1913 trade_id: &str,
1914 last_qty: Quantity,
1915 commission: Option<Money>,
1916 ) -> OrderEventAny {
1917 let mut builder = OrderFilledTestBuilder::new(order, instrument);
1918 builder
1919 .trade_id(TradeId::from(trade_id))
1920 .position_id(position_id)
1921 .last_qty(last_qty)
1922 .account_id(AccountId::from("ACCOUNT-001"));
1923
1924 if let Some(commission) = commission {
1925 builder.commission(commission);
1926 } else {
1927 builder.without_commission();
1928 }
1929 let event = builder.build();
1930 cache.borrow_mut().update_order(&event).unwrap();
1931 let OrderEventAny::Filled(fill) = &event else {
1932 unreachable!();
1933 };
1934
1935 if cache.borrow().position_exists(&position_id) {
1936 cache
1937 .borrow_mut()
1938 .update_position_from_fill(position_id, fill)
1939 .unwrap();
1940 } else {
1941 let position = Position::new(instrument, fill.clone());
1942 cache
1943 .borrow_mut()
1944 .add_position(&position, OmsType::Netting)
1945 .unwrap();
1946 }
1947
1948 event
1949 }
1950
1951 fn apply_accepted(cache: &Rc<RefCell<Cache>>, order: &OrderAny, venue_order_id: &str) {
1952 let event = TestOrderEventStubs::accepted(
1953 order,
1954 AccountId::from("ACCOUNT-001"),
1955 VenueOrderId::from(venue_order_id),
1956 );
1957 cache.borrow_mut().update_order(&event).unwrap();
1958 }
1959
1960 fn apply_update(
1961 cache: &Rc<RefCell<Cache>>,
1962 order: &OrderAny,
1963 quantity: Quantity,
1964 ) -> OrderEventAny {
1965 let event = OrderEventAny::Updated(
1966 OrderUpdatedSpec::builder()
1967 .trader_id(order.trader_id())
1968 .strategy_id(order.strategy_id())
1969 .instrument_id(order.instrument_id())
1970 .client_order_id(order.client_order_id())
1971 .quantity(quantity)
1972 .venue_order_id(VenueOrderId::from("V-CHILD"))
1973 .account_id(AccountId::from("ACCOUNT-001"))
1974 .build(),
1975 );
1976 cache.borrow_mut().update_order(&event).unwrap();
1977 event
1978 }
1979
1980 fn apply_terminal(
1981 cache: &Rc<RefCell<Cache>>,
1982 order: &OrderAny,
1983 terminal: TerminalEvent,
1984 ) -> OrderEventAny {
1985 let event = match terminal {
1986 TerminalEvent::Canceled => OrderEventAny::Canceled(
1987 OrderCanceledSpec::builder()
1988 .trader_id(order.trader_id())
1989 .strategy_id(order.strategy_id())
1990 .instrument_id(order.instrument_id())
1991 .client_order_id(order.client_order_id())
1992 .account_id(AccountId::from("ACCOUNT-001"))
1993 .build(),
1994 ),
1995 TerminalEvent::Expired => OrderEventAny::Expired(
1996 OrderExpiredSpec::builder()
1997 .trader_id(order.trader_id())
1998 .strategy_id(order.strategy_id())
1999 .instrument_id(order.instrument_id())
2000 .client_order_id(order.client_order_id())
2001 .account_id(AccountId::from("ACCOUNT-001"))
2002 .build(),
2003 ),
2004 TerminalEvent::Rejected => OrderEventAny::Rejected(
2005 OrderRejectedSpec::builder()
2006 .trader_id(order.trader_id())
2007 .strategy_id(order.strategy_id())
2008 .instrument_id(order.instrument_id())
2009 .client_order_id(order.client_order_id())
2010 .account_id(AccountId::from("ACCOUNT-001"))
2011 .reason("test rejection".into())
2012 .build(),
2013 ),
2014 };
2015 cache.borrow_mut().update_order(&event).unwrap();
2016 event
2017 }
2018
2019 #[rstest]
2020 fn test_should_manage_order_selects_symmetric_ownership_classes() {
2021 let (clock, cache) = create_test_components();
2022 let active_local = OrderTestBuilder::new(OrderType::Limit)
2023 .instrument_id(audusd_sim().id())
2024 .side(OrderSide::Buy)
2025 .price(Price::from("1.00000"))
2026 .quantity(Quantity::from(100_000))
2027 .build();
2028 let non_active_local = OrderTestBuilder::new(OrderType::Limit)
2029 .instrument_id(audusd_sim().id())
2030 .side(OrderSide::Buy)
2031 .price(Price::from("1.00000"))
2032 .quantity(Quantity::from(100_000))
2033 .submit(true)
2034 .build();
2035 let emulator_manager = OrderManager::new(clock.clone(), cache.clone(), true);
2036 let strategy_manager = OrderManager::new(clock, cache, false);
2037
2038 assert!(emulator_manager.should_manage_order(&active_local));
2039 assert!(!emulator_manager.should_manage_order(&non_active_local));
2040 assert!(!strategy_manager.should_manage_order(&active_local));
2041 assert!(strategy_manager.should_manage_order(&non_active_local));
2042 }
2043
2044 #[rstest]
2045 #[case::strategy(false, true, false)]
2046 #[case::emulator(true, false, true)]
2047 fn test_handle_oto_partial_fill_resizes_once_and_submits_only_for_active_local(
2048 #[case] active_local: bool,
2049 #[case] child_submitted: bool,
2050 #[case] expects_submit: bool,
2051 ) {
2052 let (clock, cache) = create_test_components();
2053 let mut manager = OrderManager::new(clock, cache.clone(), active_local);
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 .submit(child_submitted)
2074 .build();
2075 cache
2076 .borrow_mut()
2077 .add_order(parent.clone(), None, None, false)
2078 .unwrap();
2079 cache
2080 .borrow_mut()
2081 .add_order(child.clone(), None, None, false)
2082 .unwrap();
2083 let event = apply_fill(
2084 &cache,
2085 &parent,
2086 &instrument,
2087 "T-PARTIAL",
2088 Quantity::from(40_000),
2089 );
2090
2091 let actions = manager.handle_event(&event);
2092 let repeated_actions = manager.handle_event(&event);
2093
2094 assert_eq!(actions.len(), if expects_submit { 2 } else { 1 });
2095 assert!(matches!(
2096 &actions[0],
2097 OrderManagerAction::ModifyLocalQuantity { order, quantity }
2098 if order.client_order_id() == child_id
2099 && *quantity == Quantity::from(40_000)
2100 ));
2101
2102 if expects_submit {
2103 assert!(matches!(
2104 &actions[1],
2105 OrderManagerAction::SubmitToRisk(command)
2106 if command.client_order_id == child_id
2107 ));
2108 }
2109 assert!(repeated_actions.is_empty());
2110
2111 let modify_rejected = OrderEventAny::ModifyRejected(
2112 OrderModifyRejectedSpec::builder()
2113 .trader_id(child.trader_id())
2114 .strategy_id(child.strategy_id())
2115 .instrument_id(child.instrument_id())
2116 .client_order_id(child_id)
2117 .reason("test rejection".into())
2118 .build(),
2119 );
2120 assert!(manager.handle_event(&modify_rejected).is_empty());
2121 let retry_actions = manager.handle_event(&event);
2122 assert!(matches!(
2123 retry_actions.as_slice(),
2124 [OrderManagerAction::ModifyLocalQuantity { order, quantity }]
2125 if order.client_order_id() == child_id
2126 && *quantity == Quantity::from(40_000)
2127 ));
2128
2129 let next_event = apply_fill(
2130 &cache,
2131 &parent,
2132 &instrument,
2133 "T-PARTIAL-2",
2134 Quantity::from(60_000),
2135 );
2136 let next_actions = manager.handle_event(&next_event);
2137
2138 assert!(next_actions.is_empty());
2139 assert_eq!(
2140 manager.oto_target_quantities.get(&child_id),
2141 Some(&Quantity::from(100_000)),
2142 );
2143 }
2144
2145 #[rstest]
2146 fn test_handle_oto_spread_fill_preserves_positionless_sizing() {
2147 let (clock, cache) = create_test_components();
2148 let mut manager = OrderManager::new(clock, cache.clone(), false);
2149 let instrument = InstrumentAny::FuturesSpread(futures_spread_es());
2150 let position_id = PositionId::from("P-SPREAD");
2151 let parent_id = ClientOrderId::from("O-PARENT");
2152 let child_id = ClientOrderId::from("O-CHILD");
2153 let parent = OrderTestBuilder::new(OrderType::Limit)
2154 .instrument_id(instrument.id())
2155 .client_order_id(parent_id)
2156 .side(OrderSide::Buy)
2157 .price(Price::from("100.00"))
2158 .quantity(Quantity::from(10))
2159 .contingency_type(ContingencyType::Oto)
2160 .linked_order_ids(vec![child_id])
2161 .submit(true)
2162 .build();
2163 let child = OrderTestBuilder::new(OrderType::Limit)
2164 .instrument_id(instrument.id())
2165 .client_order_id(child_id)
2166 .side(OrderSide::Sell)
2167 .price(Price::from("101.00"))
2168 .quantity(Quantity::from(10))
2169 .submit(true)
2170 .build();
2171
2172 cache
2173 .borrow_mut()
2174 .add_instrument(instrument.clone())
2175 .unwrap();
2176
2177 for order in [&parent, &child] {
2178 cache
2179 .borrow_mut()
2180 .add_order(order.clone(), None, None, false)
2181 .unwrap();
2182 }
2183 let mut builder = OrderFilledTestBuilder::new(&parent, &instrument);
2184 let event = builder
2185 .trade_id(TradeId::from("T-SPREAD"))
2186 .position_id(position_id)
2187 .last_qty(Quantity::from(4))
2188 .account_id(AccountId::from("ACCOUNT-001"))
2189 .without_commission()
2190 .build();
2191 cache.borrow_mut().update_order(&event).unwrap();
2192
2193 let actions = manager.handle_event(&event);
2194 let repeated_actions = manager.handle_event(&event);
2195 let cached_parent = cache.borrow().order_owned(&parent_id).unwrap();
2196
2197 assert!(matches!(
2198 actions.as_slice(),
2199 [OrderManagerAction::ModifyLocalQuantity { order, quantity }]
2200 if order.client_order_id() == child_id
2201 && *quantity == Quantity::from(4)
2202 ));
2203 assert!(repeated_actions.is_empty());
2204 assert_eq!(cached_parent.position_id(), Some(position_id));
2205 assert!(!cache.borrow().position_exists(&position_id));
2206 assert!(manager.handle_contingencies(&cached_parent).is_empty());
2207 assert!(
2208 manager
2209 .handle_contingencies_update(&cached_parent)
2210 .is_empty()
2211 );
2212 }
2213
2214 #[rstest]
2215 fn test_handle_oto_sizes_mixed_instrument_child_to_its_increment() {
2216 let (clock, cache) = create_test_components();
2217 let mut manager = OrderManager::new(clock, cache.clone(), false);
2218 let mut parent_instrument = currency_pair_btcusdt();
2219 parent_instrument.size_increment = Quantity::from("0.005000");
2220 let mut child_instrument = parent_instrument.clone();
2221 child_instrument.id = InstrumentId::from("BTC-USDT-EXIT.OKX");
2222 child_instrument.size_increment = Quantity::from("0.010000");
2223 let parent_instrument = InstrumentAny::CurrencyPair(parent_instrument);
2224 let child_instrument = InstrumentAny::CurrencyPair(child_instrument);
2225 let position_id = PositionId::from("P-MIXED");
2226 let parent_id = ClientOrderId::from("O-PARENT");
2227 let child_id = ClientOrderId::from("O-CHILD");
2228 let parent = OrderTestBuilder::new(OrderType::Limit)
2229 .instrument_id(parent_instrument.id())
2230 .client_order_id(parent_id)
2231 .side(OrderSide::Buy)
2232 .price(Price::from("50000.00"))
2233 .quantity(Quantity::from("1.000000"))
2234 .contingency_type(ContingencyType::Oto)
2235 .linked_order_ids(vec![child_id])
2236 .submit(true)
2237 .build();
2238 let child = OrderTestBuilder::new(OrderType::StopMarket)
2239 .instrument_id(child_instrument.id())
2240 .client_order_id(child_id)
2241 .side(OrderSide::Sell)
2242 .trigger_price(Price::from("49000.00"))
2243 .quantity(Quantity::from("1.000000"))
2244 .reduce_only(true)
2245 .submit(true)
2246 .build();
2247
2248 for instrument in [&parent_instrument, &child_instrument] {
2249 cache
2250 .borrow_mut()
2251 .add_instrument(instrument.clone())
2252 .unwrap();
2253 }
2254
2255 for order in [&parent, &child] {
2256 cache
2257 .borrow_mut()
2258 .add_order(order.clone(), None, None, false)
2259 .unwrap();
2260 }
2261 let event = apply_position_fill(
2262 &cache,
2263 &parent,
2264 &parent_instrument,
2265 position_id,
2266 "T-PARENT",
2267 Quantity::from("1.000000"),
2268 Some(Money::from("0.00040000 BTC")),
2269 );
2270
2271 let actions = manager.handle_event(&event);
2272 let position = cache.borrow().position_owned(&position_id).unwrap();
2273
2274 assert!(matches!(
2275 actions.as_slice(),
2276 [OrderManagerAction::ModifyLocalQuantity { order, quantity }]
2277 if order.client_order_id() == child_id
2278 && *quantity == Quantity::from("0.990000")
2279 && quantity.raw().is_multiple_of(child_instrument.size_increment().raw())
2280 && order.instrument_id() == child_instrument.id()
2281 ));
2282 let OrderManagerAction::ModifyLocalQuantity { order, quantity } = &actions[0] else {
2283 unreachable!();
2284 };
2285 let mut resized = order.clone();
2286 resized.set_quantity(*quantity);
2287 resized.set_leaves_qty(*quantity);
2288
2289 assert_eq!(position.quantity, Quantity::from("0.999600"));
2290 assert!(resized.would_reduce_only(position.side, position.quantity));
2291 }
2292
2293 #[rstest]
2294 fn test_handle_oto_base_commission_sizes_both_exits_to_net_position() {
2295 let (clock, cache) = create_test_components();
2296 let mut manager = OrderManager::new(clock, cache.clone(), true);
2297 let mut instrument = currency_pair_btcusdt();
2298 instrument.size_increment = Quantity::from("0.005000");
2299 let instrument = InstrumentAny::CurrencyPair(instrument);
2300 let position_id = PositionId::from("P-COMMISSION");
2301 let parent_id = ClientOrderId::from("O-PARENT");
2302 let stop_id = ClientOrderId::from("O-STOP");
2303 let take_id = ClientOrderId::from("O-TAKE");
2304 let parent = OrderTestBuilder::new(OrderType::Limit)
2305 .instrument_id(instrument.id())
2306 .client_order_id(parent_id)
2307 .side(OrderSide::Buy)
2308 .price(Price::from("50000.00"))
2309 .quantity(Quantity::from("1.000000"))
2310 .contingency_type(ContingencyType::Oto)
2311 .linked_order_ids(vec![stop_id, take_id])
2312 .submit(true)
2313 .build();
2314 let stop = OrderTestBuilder::new(OrderType::StopMarket)
2315 .instrument_id(instrument.id())
2316 .client_order_id(stop_id)
2317 .side(OrderSide::Sell)
2318 .trigger_price(Price::from("49000.00"))
2319 .quantity(Quantity::from("1.000000"))
2320 .reduce_only(true)
2321 .contingency_type(ContingencyType::Oco)
2322 .linked_order_ids(vec![stop_id, take_id])
2323 .parent_order_id(parent_id)
2324 .build();
2325 let take = OrderTestBuilder::new(OrderType::Limit)
2326 .instrument_id(instrument.id())
2327 .client_order_id(take_id)
2328 .side(OrderSide::Sell)
2329 .price(Price::from("51000.00"))
2330 .quantity(Quantity::from("1.000000"))
2331 .reduce_only(true)
2332 .contingency_type(ContingencyType::Oco)
2333 .linked_order_ids(vec![stop_id, take_id])
2334 .parent_order_id(parent_id)
2335 .build();
2336
2337 cache
2338 .borrow_mut()
2339 .add_instrument(instrument.clone())
2340 .unwrap();
2341
2342 for order in [&parent, &stop, &take] {
2343 cache
2344 .borrow_mut()
2345 .add_order(order.clone(), None, None, false)
2346 .unwrap();
2347 }
2348 let first = apply_position_fill(
2349 &cache,
2350 &parent,
2351 &instrument,
2352 position_id,
2353 "T-PARENT-1",
2354 Quantity::from("0.600000"),
2355 Some(Money::from("0.00040000 BTC")),
2356 );
2357
2358 let first_actions = manager.handle_event(&first);
2359 let repeated_first_actions = manager.handle_event(&first);
2360 let first_position = cache.borrow().position_owned(&position_id).unwrap();
2361
2362 assert_eq!(first_position.quantity, Quantity::from("0.599600"));
2363 assert_eq!(first_actions.len(), 4);
2364 for (actions, child_id) in first_actions
2365 .as_chunks::<2>()
2366 .0
2367 .iter()
2368 .zip([stop_id, take_id])
2369 {
2370 let OrderManagerAction::ModifyLocalQuantity { order, quantity } = &actions[0] else {
2371 panic!("expected child quantity update");
2372 };
2373 let mut resized = order.clone();
2374 resized.set_quantity(*quantity);
2375 resized.set_leaves_qty(*quantity);
2376
2377 assert_eq!(order.client_order_id(), child_id);
2378 assert_eq!(*quantity, Quantity::from("0.595000"));
2379 assert!(resized.would_reduce_only(first_position.side, first_position.quantity));
2380 assert!(matches!(
2381 &actions[1],
2382 OrderManagerAction::SubmitToRisk(command)
2383 if command.client_order_id == child_id
2384 && command.position_id == Some(position_id)
2385 ));
2386 }
2387 assert!(repeated_first_actions.is_empty());
2388
2389 let second = apply_position_fill(
2390 &cache,
2391 &parent,
2392 &instrument,
2393 position_id,
2394 "T-PARENT-2",
2395 Quantity::from("0.400000"),
2396 Some(Money::from("0.00040000 BTC")),
2397 );
2398
2399 let second_actions = manager.handle_event(&second);
2400 let repeated_second_actions = manager.handle_event(&second);
2401 let second_position = cache.borrow().position_owned(&position_id).unwrap();
2402
2403 assert_eq!(second_position.quantity, Quantity::from("0.999200"));
2404 assert_eq!(second_actions.len(), 2);
2405 for (action, child_id) in second_actions.iter().zip([stop_id, take_id]) {
2406 let OrderManagerAction::ModifyLocalQuantity { order, quantity } = action else {
2407 panic!("expected child quantity update");
2408 };
2409 let mut resized = order.clone();
2410 resized.set_quantity(*quantity);
2411 resized.set_leaves_qty(*quantity);
2412
2413 assert_eq!(order.client_order_id(), child_id);
2414 assert_eq!(*quantity, Quantity::from("0.995000"));
2415 assert!(resized.would_reduce_only(second_position.side, second_position.quantity));
2416 }
2417 assert!(repeated_second_actions.is_empty());
2418 assert_eq!(manager.submit_order_commands.len(), 2);
2419
2420 let cached_parent = cache.borrow().order_owned(&parent_id).unwrap();
2421 assert!(manager.handle_contingencies(&cached_parent).is_empty());
2422 assert!(
2423 manager
2424 .handle_contingencies_update(&cached_parent)
2425 .is_empty()
2426 );
2427 }
2428
2429 #[rstest]
2430 fn test_handle_oto_sizes_partially_filled_child_by_leaves_quantity() {
2431 let (clock, cache) = create_test_components();
2432 let mut manager = OrderManager::new(clock, cache.clone(), false);
2433 let mut instrument = currency_pair_btcusdt();
2434 instrument.size_increment = Quantity::from("0.005000");
2435 let instrument = InstrumentAny::CurrencyPair(instrument);
2436 let position_id = PositionId::from("P-COMMISSION");
2437 let parent_id = ClientOrderId::from("O-PARENT");
2438 let child_id = ClientOrderId::from("O-CHILD");
2439 let parent = OrderTestBuilder::new(OrderType::Limit)
2440 .instrument_id(instrument.id())
2441 .client_order_id(parent_id)
2442 .side(OrderSide::Buy)
2443 .price(Price::from("50000.00"))
2444 .quantity(Quantity::from("1.000000"))
2445 .contingency_type(ContingencyType::Oto)
2446 .linked_order_ids(vec![child_id])
2447 .submit(true)
2448 .build();
2449 let child = OrderTestBuilder::new(OrderType::StopMarket)
2450 .instrument_id(instrument.id())
2451 .client_order_id(child_id)
2452 .side(OrderSide::Sell)
2453 .trigger_price(Price::from("49000.00"))
2454 .quantity(Quantity::from("1.000000"))
2455 .reduce_only(true)
2456 .submit(true)
2457 .build();
2458
2459 cache
2460 .borrow_mut()
2461 .add_instrument(instrument.clone())
2462 .unwrap();
2463
2464 for order in [&parent, &child] {
2465 cache
2466 .borrow_mut()
2467 .add_order(order.clone(), None, None, false)
2468 .unwrap();
2469 }
2470 let first_parent_fill = apply_position_fill(
2471 &cache,
2472 &parent,
2473 &instrument,
2474 position_id,
2475 "T-PARENT-1",
2476 Quantity::from("0.600000"),
2477 Some(Money::from("0.00040000 BTC")),
2478 );
2479 let first_actions = manager.handle_event(&first_parent_fill);
2480 assert!(matches!(
2481 first_actions.as_slice(),
2482 [OrderManagerAction::ModifyLocalQuantity { quantity, .. }]
2483 if *quantity == Quantity::from("0.595000")
2484 ));
2485
2486 apply_accepted(&cache, &child, "V-CHILD");
2487 apply_update(&cache, &child, Quantity::from("0.595000"));
2488 let child = cache.borrow().order_owned(&child_id).unwrap();
2489 let child_fill = apply_position_fill(
2490 &cache,
2491 &child,
2492 &instrument,
2493 position_id,
2494 "T-CHILD",
2495 Quantity::from("0.400000"),
2496 None,
2497 );
2498 manager.handle_event(&child_fill);
2499 let position = cache.borrow().position_owned(&position_id).unwrap();
2500 assert_eq!(position.quantity, Quantity::from("0.199600"));
2501
2502 let parent = cache.borrow().order_owned(&parent_id).unwrap();
2503 let second_parent_fill = apply_position_fill(
2504 &cache,
2505 &parent,
2506 &instrument,
2507 position_id,
2508 "T-PARENT-2",
2509 Quantity::from("0.400000"),
2510 Some(Money::from("0.00040000 BTC")),
2511 );
2512 let actions = manager.handle_event(&second_parent_fill);
2513 let position = cache.borrow().position_owned(&position_id).unwrap();
2514
2515 let [OrderManagerAction::ModifyLocalQuantity { order, quantity }] = actions.as_slice()
2516 else {
2517 panic!("expected child quantity update");
2518 };
2519 let leaves_qty = *quantity - order.filled_qty();
2520 let mut resized = order.clone();
2521 resized.set_quantity(*quantity);
2522 resized.set_leaves_qty(leaves_qty);
2523
2524 assert_eq!(position.quantity, Quantity::from("0.599200"));
2525 assert_eq!(order.filled_qty(), Quantity::from("0.400000"));
2526 assert_eq!(*quantity, Quantity::from("0.995000"));
2527 assert_eq!(leaves_qty, Quantity::from("0.595000"));
2528 assert!(resized.would_reduce_only(position.side, position.quantity));
2529 }
2530
2531 #[rstest]
2532 fn test_handle_oto_child_base_commission_reduces_total_target() {
2533 let (clock, cache) = create_test_components();
2534 let mut manager = OrderManager::new(clock, cache.clone(), false);
2535 let mut instrument = currency_pair_btcusdt();
2536 instrument.size_increment = Quantity::from("0.005000");
2537 let instrument = InstrumentAny::CurrencyPair(instrument);
2538 let position_id = PositionId::from("P-CHILD-COMMISSION");
2539 let parent_id = ClientOrderId::from("O-PARENT");
2540 let child_id = ClientOrderId::from("O-CHILD");
2541 let parent = OrderTestBuilder::new(OrderType::Limit)
2542 .instrument_id(instrument.id())
2543 .client_order_id(parent_id)
2544 .side(OrderSide::Buy)
2545 .price(Price::from("50000.00"))
2546 .quantity(Quantity::from("1.000000"))
2547 .contingency_type(ContingencyType::Oto)
2548 .linked_order_ids(vec![child_id])
2549 .submit(true)
2550 .build();
2551 let child = OrderTestBuilder::new(OrderType::StopMarket)
2552 .instrument_id(instrument.id())
2553 .client_order_id(child_id)
2554 .side(OrderSide::Sell)
2555 .trigger_price(Price::from("49000.00"))
2556 .quantity(Quantity::from("1.000000"))
2557 .reduce_only(true)
2558 .submit(true)
2559 .build();
2560
2561 cache
2562 .borrow_mut()
2563 .add_instrument(instrument.clone())
2564 .unwrap();
2565
2566 for order in [&parent, &child] {
2567 cache
2568 .borrow_mut()
2569 .add_order(order.clone(), None, None, false)
2570 .unwrap();
2571 }
2572 apply_accepted(&cache, &child, "V-CHILD");
2573 let parent_fill = apply_position_fill(
2574 &cache,
2575 &parent,
2576 &instrument,
2577 position_id,
2578 "T-PARENT",
2579 Quantity::from("1.000000"),
2580 None,
2581 );
2582 assert!(manager.handle_event(&parent_fill).is_empty());
2583
2584 let child = cache.borrow().order_owned(&child_id).unwrap();
2585 let child_fill = apply_position_fill(
2586 &cache,
2587 &child,
2588 &instrument,
2589 position_id,
2590 "T-CHILD",
2591 Quantity::from("0.400000"),
2592 Some(Money::from("0.00100000 BTC")),
2593 );
2594 assert!(manager.handle_event(&child_fill).is_empty());
2595
2596 let position = cache.borrow().position_owned(&position_id).unwrap();
2597 let parent = cache.borrow().order_owned(&parent_id).unwrap();
2598 let actions = manager.handle_contingencies(&parent);
2599
2600 assert_eq!(position.quantity, Quantity::from("0.599000"));
2601 assert!(matches!(
2602 actions.as_slice(),
2603 [OrderManagerAction::ModifyLocalQuantity { order, quantity }]
2604 if order.client_order_id() == child_id
2605 && order.filled_qty() == Quantity::from("0.400000")
2606 && *quantity == Quantity::from("0.995000")
2607 ));
2608 }
2609
2610 #[rstest]
2611 fn test_handle_oto_non_reduce_only_child_uses_parent_fill_quantity() {
2612 let (clock, cache) = create_test_components();
2613 let mut manager = OrderManager::new(clock, cache.clone(), false);
2614 let instrument = InstrumentAny::CurrencyPair(currency_pair_btcusdt());
2615 let position_id = PositionId::from("P-REENTRY");
2616 let parent_id = ClientOrderId::from("O-PARENT");
2617 let child_id = ClientOrderId::from("O-CHILD");
2618 let entry = OrderTestBuilder::new(OrderType::Market)
2619 .instrument_id(instrument.id())
2620 .client_order_id(ClientOrderId::from("O-ENTRY"))
2621 .side(OrderSide::Buy)
2622 .quantity(Quantity::from("1.000000"))
2623 .submit(true)
2624 .build();
2625 let parent = OrderTestBuilder::new(OrderType::Market)
2626 .instrument_id(instrument.id())
2627 .client_order_id(parent_id)
2628 .side(OrderSide::Sell)
2629 .quantity(Quantity::from("1.000000"))
2630 .contingency_type(ContingencyType::Oto)
2631 .linked_order_ids(vec![child_id])
2632 .submit(true)
2633 .build();
2634 let child = OrderTestBuilder::new(OrderType::Market)
2635 .instrument_id(instrument.id())
2636 .client_order_id(child_id)
2637 .side(OrderSide::Buy)
2638 .quantity(Quantity::from("0.500000"))
2639 .submit(true)
2640 .build();
2641
2642 cache
2643 .borrow_mut()
2644 .add_instrument(instrument.clone())
2645 .unwrap();
2646 cache
2647 .borrow_mut()
2648 .add_order(entry.clone(), Some(position_id), None, false)
2649 .unwrap();
2650 apply_accepted(&cache, &entry, "V-ENTRY");
2651 let entry = cache
2652 .borrow()
2653 .order_owned(&entry.client_order_id())
2654 .unwrap();
2655 apply_position_fill(
2656 &cache,
2657 &entry,
2658 &instrument,
2659 position_id,
2660 "T-ENTRY",
2661 Quantity::from("1.000000"),
2662 None,
2663 );
2664 cache
2665 .borrow_mut()
2666 .add_order(parent.clone(), Some(position_id), None, false)
2667 .unwrap();
2668 cache
2669 .borrow_mut()
2670 .add_order(child, Some(position_id), None, false)
2671 .unwrap();
2672 apply_accepted(&cache, &parent, "V-PARENT");
2673 let parent = cache.borrow().order_owned(&parent_id).unwrap();
2674 let event = apply_position_fill(
2675 &cache,
2676 &parent,
2677 &instrument,
2678 position_id,
2679 "T-PARENT",
2680 Quantity::from("1.000000"),
2681 None,
2682 );
2683
2684 let actions = manager.handle_event(&event);
2685
2686 assert!(cache.borrow().position(&position_id).unwrap().is_closed());
2687 assert!(matches!(
2688 actions.as_slice(),
2689 [OrderManagerAction::ModifyLocalQuantity { order, quantity }]
2690 if order.client_order_id() == child_id
2691 && *quantity == Quantity::from("1.000000")
2692 ));
2693 }
2694
2695 #[rstest]
2696 fn test_handle_oto_zero_target_clears_stored_positive_target() {
2697 let (clock, cache) = create_test_components();
2698 let mut manager = OrderManager::new(clock, cache.clone(), false);
2699 let mut instrument = currency_pair_btcusdt();
2700 instrument.size_increment = Quantity::from("0.005000");
2701 let instrument = InstrumentAny::CurrencyPair(instrument);
2702 let position_id = PositionId::from("P-CLEAR-TARGET");
2703 let parent_id = ClientOrderId::from("O-PARENT");
2704 let child_id = ClientOrderId::from("O-CHILD");
2705 let parent = OrderTestBuilder::new(OrderType::Limit)
2706 .instrument_id(instrument.id())
2707 .client_order_id(parent_id)
2708 .side(OrderSide::Buy)
2709 .price(Price::from("50000.00"))
2710 .quantity(Quantity::from("1.000000"))
2711 .contingency_type(ContingencyType::Oto)
2712 .linked_order_ids(vec![child_id])
2713 .submit(true)
2714 .build();
2715 let child = OrderTestBuilder::new(OrderType::StopMarket)
2716 .instrument_id(instrument.id())
2717 .client_order_id(child_id)
2718 .side(OrderSide::Sell)
2719 .trigger_price(Price::from("49000.00"))
2720 .quantity(Quantity::from("1.000000"))
2721 .reduce_only(true)
2722 .submit(true)
2723 .build();
2724 let external_exit = OrderTestBuilder::new(OrderType::Market)
2725 .instrument_id(instrument.id())
2726 .client_order_id(ClientOrderId::from("O-EXTERNAL-EXIT"))
2727 .side(OrderSide::Sell)
2728 .quantity(Quantity::from("0.598000"))
2729 .submit(true)
2730 .build();
2731
2732 cache
2733 .borrow_mut()
2734 .add_instrument(instrument.clone())
2735 .unwrap();
2736
2737 for order in [&parent, &child, &external_exit] {
2738 cache
2739 .borrow_mut()
2740 .add_order(order.clone(), Some(position_id), None, false)
2741 .unwrap();
2742 }
2743 apply_accepted(&cache, &parent, "V-PARENT");
2744 apply_accepted(&cache, &external_exit, "V-EXTERNAL-EXIT");
2745 let parent = cache.borrow().order_owned(&parent_id).unwrap();
2746 let external_exit = cache
2747 .borrow()
2748 .order_owned(&external_exit.client_order_id())
2749 .unwrap();
2750 let first = apply_position_fill(
2751 &cache,
2752 &parent,
2753 &instrument,
2754 position_id,
2755 "T-PARENT-1",
2756 Quantity::from("0.600000"),
2757 Some(Money::from("0.00040000 BTC")),
2758 );
2759 let first_actions = manager.handle_event(&first);
2760 assert!(matches!(
2761 first_actions.as_slice(),
2762 [OrderManagerAction::ModifyLocalQuantity { quantity, .. }]
2763 if *quantity == Quantity::from("0.595000")
2764 ));
2765 assert_eq!(
2766 manager.oto_target_quantities.get(&child_id),
2767 Some(&Quantity::from("0.595000"))
2768 );
2769
2770 apply_position_fill(
2771 &cache,
2772 &external_exit,
2773 &instrument,
2774 position_id,
2775 "T-EXTERNAL-EXIT",
2776 Quantity::from("0.598000"),
2777 None,
2778 );
2779 let parent = cache.borrow().order_owned(&parent_id).unwrap();
2780 let second = apply_position_fill(
2781 &cache,
2782 &parent,
2783 &instrument,
2784 position_id,
2785 "T-PARENT-2",
2786 Quantity::from("0.001000"),
2787 None,
2788 );
2789 let second_actions = manager.handle_event(&second);
2790
2791 assert_eq!(
2792 cache.borrow().position(&position_id).unwrap().quantity,
2793 Quantity::from("0.002600")
2794 );
2795 assert!(second_actions.is_empty());
2796 assert!(!manager.oto_target_quantities.contains_key(&child_id));
2797 }
2798
2799 #[rstest]
2800 #[case::none(None)]
2801 #[case::zero_base(Some(Money::from("0 BTC")))]
2802 #[case::quote(Some(Money::from("10 USDT")))]
2803 fn test_oto_quantity_preserves_gross_without_base_commission(
2804 #[case] commission: Option<Money>,
2805 ) {
2806 let (clock, cache) = create_test_components();
2807 let manager = OrderManager::new(clock, cache.clone(), true);
2808 let mut instrument = currency_pair_btcusdt();
2809 instrument.size_increment = Quantity::from("0.005000");
2810 let instrument = InstrumentAny::CurrencyPair(instrument);
2811 let position_id = PositionId::from("P-COMMISSION");
2812 let parent = OrderTestBuilder::new(OrderType::Limit)
2813 .instrument_id(instrument.id())
2814 .client_order_id(ClientOrderId::from("O-PARENT"))
2815 .side(OrderSide::Buy)
2816 .price(Price::from("50000.00"))
2817 .quantity(Quantity::from("0.600000"))
2818 .reduce_only(true)
2819 .contingency_type(ContingencyType::Oto)
2820 .linked_order_ids(vec![ClientOrderId::from("O-CHILD")])
2821 .submit(true)
2822 .build();
2823
2824 cache
2825 .borrow_mut()
2826 .add_instrument(instrument.clone())
2827 .unwrap();
2828 cache
2829 .borrow_mut()
2830 .add_order(parent.clone(), None, None, false)
2831 .unwrap();
2832 apply_position_fill(
2833 &cache,
2834 &parent,
2835 &instrument,
2836 position_id,
2837 "T-PARENT",
2838 Quantity::from("0.600000"),
2839 commission,
2840 );
2841 let position = cache.borrow().position_owned(&position_id).unwrap();
2842
2843 let quantity = manager
2844 .position_adjusted_oto_quantity(
2845 &parent,
2846 &parent,
2847 Quantity::from("0.600000"),
2848 Quantity::from("0.000000"),
2849 Some(position_id),
2850 )
2851 .unwrap();
2852
2853 assert_eq!(position.quantity, Quantity::from("0.600000"));
2854 assert_eq!(quantity, Quantity::from("0.600000"));
2855 }
2856
2857 #[rstest]
2858 #[case::position(true, false)]
2859 #[case::instrument(false, true)]
2860 fn test_oto_handlers_require_complete_position_state(
2861 #[case] add_instrument: bool,
2862 #[case] add_position: bool,
2863 ) {
2864 let (clock, cache) = create_test_components();
2865 let mut manager = OrderManager::new(clock, cache.clone(), true);
2866 let instrument = InstrumentAny::CurrencyPair(currency_pair_btcusdt());
2867 let position_id = PositionId::from("P-MISSING");
2868 let parent_id = ClientOrderId::from("O-PARENT");
2869 let child_id = ClientOrderId::from("O-CHILD");
2870 let parent = OrderTestBuilder::new(OrderType::Limit)
2871 .instrument_id(instrument.id())
2872 .client_order_id(parent_id)
2873 .side(OrderSide::Buy)
2874 .price(Price::from("50000.00"))
2875 .quantity(Quantity::from("1.000000"))
2876 .contingency_type(ContingencyType::Oto)
2877 .linked_order_ids(vec![child_id])
2878 .submit(true)
2879 .build();
2880 let child = OrderTestBuilder::new(OrderType::StopMarket)
2881 .instrument_id(instrument.id())
2882 .client_order_id(child_id)
2883 .side(OrderSide::Sell)
2884 .trigger_price(Price::from("49000.00"))
2885 .quantity(Quantity::from("1.000000"))
2886 .reduce_only(true)
2887 .build();
2888 let event = TestOrderEventStubs::filled(
2889 &parent,
2890 &instrument,
2891 Some(TradeId::from("T-PARENT")),
2892 Some(position_id),
2893 None,
2894 None,
2895 None,
2896 None,
2897 None,
2898 Some(AccountId::from("ACCOUNT-001")),
2899 );
2900 let OrderEventAny::Filled(fill) = &event else {
2901 unreachable!();
2902 };
2903
2904 cache
2905 .borrow_mut()
2906 .add_order(parent, Some(position_id), None, false)
2907 .unwrap();
2908 cache
2909 .borrow_mut()
2910 .add_order(child, None, None, false)
2911 .unwrap();
2912 cache.borrow_mut().update_order(&event).unwrap();
2913
2914 if add_instrument {
2915 cache
2916 .borrow_mut()
2917 .add_instrument(instrument.clone())
2918 .unwrap();
2919 }
2920
2921 if add_position {
2922 let position = Position::new(&instrument, fill.clone());
2923 cache
2924 .borrow_mut()
2925 .add_position(&position, OmsType::Netting)
2926 .unwrap();
2927 }
2928
2929 let fill_actions = manager.handle_event(&event);
2930 let cached_parent = cache.borrow().order_owned(&parent_id).unwrap();
2931 let contingency_actions = manager.handle_contingencies(&cached_parent);
2932 let update_actions = manager.handle_contingencies_update(&cached_parent);
2933
2934 assert!(fill_actions.is_empty());
2935 assert!(contingency_actions.is_empty());
2936 assert!(update_actions.is_empty());
2937 assert!(manager.submit_order_commands.is_empty());
2938 }
2939
2940 #[rstest]
2941 fn test_oto_quantity_rejects_zero_size_increment() {
2942 let (clock, cache) = create_test_components();
2943 let manager = OrderManager::new(clock, cache.clone(), true);
2944 let mut instrument = currency_pair_btcusdt();
2945 instrument.size_increment = Quantity::zero(instrument.size_precision);
2946 let instrument = InstrumentAny::CurrencyPair(instrument);
2947 let position_id = PositionId::from("P-ZERO-INCREMENT");
2948 let parent = OrderTestBuilder::new(OrderType::Limit)
2949 .instrument_id(instrument.id())
2950 .side(OrderSide::Buy)
2951 .price(Price::from("50000.00"))
2952 .quantity(Quantity::from("1.000000"))
2953 .build();
2954 let event = TestOrderEventStubs::filled(
2955 &parent,
2956 &instrument,
2957 Some(TradeId::from("T-PARENT")),
2958 Some(position_id),
2959 None,
2960 None,
2961 None,
2962 None,
2963 None,
2964 Some(AccountId::from("ACCOUNT-001")),
2965 );
2966 let OrderEventAny::Filled(fill) = event else {
2967 unreachable!();
2968 };
2969 let position = Position::new(&instrument, fill);
2970
2971 cache.borrow_mut().add_instrument(instrument).unwrap();
2972 cache
2973 .borrow_mut()
2974 .add_position(&position, OmsType::Netting)
2975 .unwrap();
2976
2977 assert_eq!(
2978 manager.position_adjusted_oto_quantity(
2979 &parent,
2980 &parent,
2981 Quantity::from("1.000000"),
2982 Quantity::from("0.000000"),
2983 Some(position_id),
2984 ),
2985 None,
2986 );
2987 }
2988
2989 #[rstest]
2990 fn test_handle_oto_waits_for_instrument_minimum_quantity() {
2991 let (clock, cache) = create_test_components();
2992 let mut manager = OrderManager::new(clock, cache.clone(), true);
2993 let mut instrument = currency_pair_btcusdt();
2994 instrument.size_increment = Quantity::from("0.005000");
2995 instrument.min_quantity = Some(Quantity::from("0.010000"));
2996 let instrument = InstrumentAny::CurrencyPair(instrument);
2997 let position_id = PositionId::from("P-MINIMUM");
2998 let parent_id = ClientOrderId::from("O-PARENT");
2999 let child_id = ClientOrderId::from("O-CHILD");
3000 let parent = OrderTestBuilder::new(OrderType::Limit)
3001 .instrument_id(instrument.id())
3002 .client_order_id(parent_id)
3003 .side(OrderSide::Buy)
3004 .price(Price::from("50000.00"))
3005 .quantity(Quantity::from("0.012000"))
3006 .contingency_type(ContingencyType::Oto)
3007 .linked_order_ids(vec![child_id])
3008 .submit(true)
3009 .build();
3010 let child = OrderTestBuilder::new(OrderType::StopMarket)
3011 .instrument_id(instrument.id())
3012 .client_order_id(child_id)
3013 .side(OrderSide::Sell)
3014 .trigger_price(Price::from("49000.00"))
3015 .quantity(Quantity::from("0.012000"))
3016 .reduce_only(true)
3017 .build();
3018
3019 cache
3020 .borrow_mut()
3021 .add_instrument(instrument.clone())
3022 .unwrap();
3023
3024 for order in [&parent, &child] {
3025 cache
3026 .borrow_mut()
3027 .add_order(order.clone(), None, None, false)
3028 .unwrap();
3029 }
3030 let first = apply_position_fill(
3031 &cache,
3032 &parent,
3033 &instrument,
3034 position_id,
3035 "T-PARENT-1",
3036 Quantity::from("0.006000"),
3037 None,
3038 );
3039
3040 let first_actions = manager.handle_event(&first);
3041
3042 assert!(first_actions.is_empty());
3043 assert!(manager.submit_order_commands.is_empty());
3044
3045 let parent = cache.borrow().order_owned(&parent_id).unwrap();
3046 let second = apply_position_fill(
3047 &cache,
3048 &parent,
3049 &instrument,
3050 position_id,
3051 "T-PARENT-2",
3052 Quantity::from("0.006000"),
3053 None,
3054 );
3055 let second_actions = manager.handle_event(&second);
3056
3057 assert!(matches!(
3058 second_actions.as_slice(),
3059 [
3060 OrderManagerAction::ModifyLocalQuantity { order, quantity },
3061 OrderManagerAction::SubmitToRisk(command),
3062 ] if order.client_order_id() == child_id
3063 && *quantity == Quantity::from("0.010000")
3064 && command.client_order_id == child_id
3065 ));
3066 }
3067
3068 #[rstest]
3069 fn test_handle_oto_waits_for_executable_quantity_and_preserves_residual() {
3070 let (clock, cache) = create_test_components();
3071 let mut manager = OrderManager::new(clock, cache.clone(), true);
3072 let mut instrument = currency_pair_btcusdt();
3073 instrument.size_increment = Quantity::from("0.005000");
3074 let instrument = InstrumentAny::CurrencyPair(instrument);
3075 let position_id = PositionId::from("P-RESIDUAL");
3076 let parent_id = ClientOrderId::from("O-PARENT");
3077 let child_id = ClientOrderId::from("O-CHILD");
3078 let parent = OrderTestBuilder::new(OrderType::Limit)
3079 .instrument_id(instrument.id())
3080 .client_order_id(parent_id)
3081 .side(OrderSide::Buy)
3082 .price(Price::from("50000.00"))
3083 .quantity(Quantity::from("0.010000"))
3084 .contingency_type(ContingencyType::Oto)
3085 .linked_order_ids(vec![child_id])
3086 .submit(true)
3087 .build();
3088 let child = OrderTestBuilder::new(OrderType::StopMarket)
3089 .instrument_id(instrument.id())
3090 .client_order_id(child_id)
3091 .side(OrderSide::Sell)
3092 .trigger_price(Price::from("49000.00"))
3093 .quantity(Quantity::from("0.010000"))
3094 .reduce_only(true)
3095 .build();
3096
3097 cache
3098 .borrow_mut()
3099 .add_instrument(instrument.clone())
3100 .unwrap();
3101
3102 for order in [&parent, &child] {
3103 cache
3104 .borrow_mut()
3105 .add_order(order.clone(), None, None, false)
3106 .unwrap();
3107 }
3108 let first = apply_position_fill(
3109 &cache,
3110 &parent,
3111 &instrument,
3112 position_id,
3113 "T-PARENT-1",
3114 Quantity::from("0.003000"),
3115 Some(Money::from("0.00040000 BTC")),
3116 );
3117
3118 let first_actions = manager.handle_event(&first);
3119 let first_position = cache.borrow().position_owned(&position_id).unwrap();
3120
3121 assert!(first_actions.is_empty());
3122 assert_eq!(first_position.quantity, Quantity::from("0.002600"));
3123 assert!(manager.submit_order_commands.is_empty());
3124
3125 let second = apply_position_fill(
3126 &cache,
3127 &parent,
3128 &instrument,
3129 position_id,
3130 "T-PARENT-2",
3131 Quantity::from("0.007000"),
3132 Some(Money::from("0.00040000 BTC")),
3133 );
3134
3135 let second_actions = manager.handle_event(&second);
3136 let position = cache.borrow().position_owned(&position_id).unwrap();
3137
3138 assert_eq!(position.quantity, Quantity::from("0.009200"));
3139 assert!(matches!(
3140 second_actions.as_slice(),
3141 [
3142 OrderManagerAction::ModifyLocalQuantity { order, quantity },
3143 OrderManagerAction::SubmitToRisk(command),
3144 ] if order.client_order_id() == child_id
3145 && *quantity == Quantity::from("0.005000")
3146 && command.client_order_id == child_id
3147 ));
3148 assert_eq!(
3149 position.quantity - Quantity::from("0.005000"),
3150 Quantity::from("0.004200"),
3151 );
3152 }
3153
3154 #[rstest]
3155 #[case::accepted(false)]
3156 #[case::emulated(true)]
3157 fn test_handle_oto_zero_target_waits_for_more_executable_quantity(#[case] active_local: bool) {
3158 let (clock, cache) = create_test_components();
3159 let mut manager = OrderManager::new(clock, cache.clone(), active_local);
3160 let mut instrument = currency_pair_btcusdt();
3161 instrument.size_increment = Quantity::from("0.005000");
3162 let instrument = InstrumentAny::CurrencyPair(instrument);
3163 let position_id = PositionId::from("P-WAIT");
3164 let parent_id = ClientOrderId::from("O-PARENT");
3165 let child_id = ClientOrderId::from("O-CHILD");
3166 let parent = OrderTestBuilder::new(OrderType::Limit)
3167 .instrument_id(instrument.id())
3168 .client_order_id(parent_id)
3169 .side(OrderSide::Buy)
3170 .price(Price::from("50000.00"))
3171 .quantity(Quantity::from("0.010000"))
3172 .contingency_type(ContingencyType::Oto)
3173 .linked_order_ids(vec![child_id])
3174 .submit(true)
3175 .build();
3176 let child = OrderTestBuilder::new(OrderType::StopMarket)
3177 .instrument_id(instrument.id())
3178 .client_order_id(child_id)
3179 .side(OrderSide::Sell)
3180 .trigger_price(Price::from("49000.00"))
3181 .quantity(Quantity::from("0.010000"))
3182 .reduce_only(true)
3183 .submit(!active_local)
3184 .build();
3185
3186 cache
3187 .borrow_mut()
3188 .add_instrument(instrument.clone())
3189 .unwrap();
3190
3191 for order in [&parent, &child] {
3192 cache
3193 .borrow_mut()
3194 .add_order(order.clone(), None, None, false)
3195 .unwrap();
3196 }
3197
3198 if active_local {
3199 let event = OrderEventAny::Emulated(
3200 OrderEmulatedSpec::builder()
3201 .trader_id(child.trader_id())
3202 .strategy_id(child.strategy_id())
3203 .instrument_id(child.instrument_id())
3204 .client_order_id(child_id)
3205 .build(),
3206 );
3207 cache.borrow_mut().update_order(&event).unwrap();
3208 } else {
3209 apply_accepted(&cache, &child, "V-CHILD");
3210 }
3211 let first = apply_position_fill(
3212 &cache,
3213 &parent,
3214 &instrument,
3215 position_id,
3216 "T-PARENT-1",
3217 Quantity::from("0.003000"),
3218 Some(Money::from("0.00040000 BTC")),
3219 );
3220
3221 let first_actions = manager.handle_event(&first);
3222 let cached_child = cache.borrow().order_owned(&child_id).unwrap();
3223
3224 assert!(first_actions.is_empty());
3225 assert_eq!(
3226 cached_child.status(),
3227 if active_local {
3228 OrderStatus::Emulated
3229 } else {
3230 OrderStatus::Accepted
3231 }
3232 );
3233
3234 let second = apply_position_fill(
3235 &cache,
3236 &parent,
3237 &instrument,
3238 position_id,
3239 "T-PARENT-2",
3240 Quantity::from("0.007000"),
3241 Some(Money::from("0.00040000 BTC")),
3242 );
3243 let second_actions = manager.handle_event(&second);
3244
3245 assert_eq!(second_actions.len(), if active_local { 2 } else { 1 });
3246 assert!(matches!(
3247 second_actions.first(),
3248 Some(OrderManagerAction::ModifyLocalQuantity { order, quantity })
3249 if order.client_order_id() == child_id
3250 && *quantity == Quantity::from("0.005000")
3251 ));
3252 }
3253
3254 #[rstest]
3255 fn test_handle_oto_closed_spawn_slice_waits_while_spawn_remains_active() {
3256 let (clock, cache) = create_test_components();
3257 let mut manager = OrderManager::new(clock, cache.clone(), true);
3258 let mut instrument = currency_pair_btcusdt();
3259 instrument.size_increment = Quantity::from("0.005000");
3260 let instrument = InstrumentAny::CurrencyPair(instrument);
3261 let position_id = PositionId::from("P-SPAWN-WAIT");
3262 let spawn_id = ClientOrderId::from("O-SPAWN");
3263 let first_id = ClientOrderId::from("O-SPAWN-1");
3264 let second_id = ClientOrderId::from("O-SPAWN-2");
3265 let child_id = ClientOrderId::from("O-CHILD");
3266 let exec_algorithm_id = ExecAlgorithmId::from("TWAP");
3267 let first = OrderTestBuilder::new(OrderType::Limit)
3268 .instrument_id(instrument.id())
3269 .client_order_id(first_id)
3270 .side(OrderSide::Buy)
3271 .price(Price::from("50000.00"))
3272 .quantity(Quantity::from("0.003000"))
3273 .contingency_type(ContingencyType::Oto)
3274 .linked_order_ids(vec![child_id])
3275 .exec_algorithm_id(exec_algorithm_id)
3276 .exec_spawn_id(spawn_id)
3277 .submit(true)
3278 .build();
3279 let second = OrderTestBuilder::new(OrderType::Limit)
3280 .instrument_id(instrument.id())
3281 .client_order_id(second_id)
3282 .side(OrderSide::Buy)
3283 .price(Price::from("50000.00"))
3284 .quantity(Quantity::from("0.007000"))
3285 .contingency_type(ContingencyType::Oto)
3286 .linked_order_ids(vec![child_id])
3287 .exec_algorithm_id(exec_algorithm_id)
3288 .exec_spawn_id(spawn_id)
3289 .submit(true)
3290 .build();
3291 let child = OrderTestBuilder::new(OrderType::StopMarket)
3292 .instrument_id(instrument.id())
3293 .client_order_id(child_id)
3294 .side(OrderSide::Sell)
3295 .trigger_price(Price::from("49000.00"))
3296 .quantity(Quantity::from("0.010000"))
3297 .reduce_only(true)
3298 .build();
3299
3300 cache
3301 .borrow_mut()
3302 .add_instrument(instrument.clone())
3303 .unwrap();
3304
3305 for order in [&first, &second, &child] {
3306 cache
3307 .borrow_mut()
3308 .add_order(order.clone(), None, None, false)
3309 .unwrap();
3310 }
3311 apply_accepted(&cache, &first, "V-SPAWN-1");
3312 apply_accepted(&cache, &second, "V-SPAWN-2");
3313 let first = cache.borrow().order_owned(&first_id).unwrap();
3314 let event = apply_position_fill(
3315 &cache,
3316 &first,
3317 &instrument,
3318 position_id,
3319 "T-SPAWN-1",
3320 Quantity::from("0.003000"),
3321 Some(Money::from("0.00040000 BTC")),
3322 );
3323
3324 let first_actions = manager.handle_event(&event);
3325
3326 assert!(first_actions.is_empty());
3327
3328 let second = cache.borrow().order_owned(&second_id).unwrap();
3329 let event = apply_position_fill(
3330 &cache,
3331 &second,
3332 &instrument,
3333 position_id,
3334 "T-SPAWN-2",
3335 Quantity::from("0.007000"),
3336 Some(Money::from("0.00040000 BTC")),
3337 );
3338 let second_actions = manager.handle_event(&event);
3339
3340 assert!(matches!(
3341 second_actions.as_slice(),
3342 [
3343 OrderManagerAction::ModifyLocalQuantity { order, quantity },
3344 OrderManagerAction::SubmitToRisk(command),
3345 ] if order.client_order_id() == child_id
3346 && *quantity == Quantity::from("0.005000")
3347 && command.client_order_id == child_id
3348 ));
3349 }
3350
3351 #[rstest]
3352 #[case::missing_primary(None)]
3353 #[case::active_primary(Some(false))]
3354 #[case::closed_primary(Some(true))]
3355 fn test_handle_oto_last_spawn_cancellation_respects_primary_state(
3356 #[case] primary_closed: Option<bool>,
3357 ) {
3358 let (clock, cache) = create_test_components();
3359 let mut manager = OrderManager::new(clock, cache.clone(), true);
3360 let mut instrument = currency_pair_btcusdt();
3361 instrument.size_increment = Quantity::from("0.005000");
3362 let instrument = InstrumentAny::CurrencyPair(instrument);
3363 let position_id = PositionId::from("P-SPAWN-CANCEL");
3364 let spawn_id = ClientOrderId::from("O-SPAWN");
3365 let first_id = ClientOrderId::from("O-SPAWN-1");
3366 let second_id = ClientOrderId::from("O-SPAWN-2");
3367 let child_id = ClientOrderId::from("O-CHILD");
3368 let exec_algorithm_id = ExecAlgorithmId::from("TWAP");
3369
3370 if let Some(closed) = primary_closed {
3371 let primary = OrderTestBuilder::new(OrderType::Limit)
3372 .instrument_id(instrument.id())
3373 .client_order_id(spawn_id)
3374 .side(OrderSide::Buy)
3375 .price(Price::from("50000.00"))
3376 .quantity(Quantity::from("0.010000"))
3377 .contingency_type(ContingencyType::Oto)
3378 .linked_order_ids(vec![child_id])
3379 .exec_algorithm_id(exec_algorithm_id)
3380 .exec_spawn_id(spawn_id)
3381 .build();
3382 cache
3383 .borrow_mut()
3384 .add_order(primary.clone(), None, None, false)
3385 .unwrap();
3386
3387 if closed {
3388 apply_terminal(&cache, &primary, TerminalEvent::Canceled);
3389 }
3390 }
3391 let first = OrderTestBuilder::new(OrderType::Limit)
3392 .instrument_id(instrument.id())
3393 .client_order_id(first_id)
3394 .side(OrderSide::Buy)
3395 .price(Price::from("50000.00"))
3396 .quantity(Quantity::from("0.003000"))
3397 .contingency_type(ContingencyType::Oto)
3398 .linked_order_ids(vec![child_id])
3399 .exec_algorithm_id(exec_algorithm_id)
3400 .exec_spawn_id(spawn_id)
3401 .submit(true)
3402 .build();
3403 let second = OrderTestBuilder::new(OrderType::Limit)
3404 .instrument_id(instrument.id())
3405 .client_order_id(second_id)
3406 .side(OrderSide::Buy)
3407 .price(Price::from("50000.00"))
3408 .quantity(Quantity::from("0.007000"))
3409 .contingency_type(ContingencyType::Oto)
3410 .linked_order_ids(vec![child_id])
3411 .exec_algorithm_id(exec_algorithm_id)
3412 .exec_spawn_id(spawn_id)
3413 .submit(true)
3414 .build();
3415 let child = OrderTestBuilder::new(OrderType::StopMarket)
3416 .instrument_id(instrument.id())
3417 .client_order_id(child_id)
3418 .side(OrderSide::Sell)
3419 .trigger_price(Price::from("49000.00"))
3420 .quantity(Quantity::from("0.010000"))
3421 .reduce_only(true)
3422 .build();
3423
3424 cache
3425 .borrow_mut()
3426 .add_instrument(instrument.clone())
3427 .unwrap();
3428
3429 for order in [&first, &second, &child] {
3430 cache
3431 .borrow_mut()
3432 .add_order(order.clone(), None, None, false)
3433 .unwrap();
3434 }
3435 apply_accepted(&cache, &first, "V-SPAWN-1");
3436 apply_accepted(&cache, &second, "V-SPAWN-2");
3437 let first = cache.borrow().order_owned(&first_id).unwrap();
3438 let first_fill = apply_position_fill(
3439 &cache,
3440 &first,
3441 &instrument,
3442 position_id,
3443 "T-SPAWN-1",
3444 Quantity::from("0.003000"),
3445 Some(Money::from("0.00040000 BTC")),
3446 );
3447
3448 assert!(manager.handle_event(&first_fill).is_empty());
3449
3450 let second = cache.borrow().order_owned(&second_id).unwrap();
3451 let canceled = apply_terminal(&cache, &second, TerminalEvent::Canceled);
3452 let actions = manager.handle_event(&canceled);
3453
3454 if primary_closed == Some(false) {
3455 assert!(actions.is_empty());
3456 } else {
3457 assert!(matches!(
3458 actions.as_slice(),
3459 [OrderManagerAction::CancelLocal(order)]
3460 if order.client_order_id() == child_id
3461 ));
3462 }
3463 }
3464
3465 #[rstest]
3466 fn test_handle_oto_missing_child_instrument_does_not_block_siblings() {
3467 let (clock, cache) = create_test_components();
3468 let mut manager = OrderManager::new(clock, cache.clone(), false);
3469 let mut instrument = currency_pair_btcusdt();
3470 instrument.size_increment = Quantity::from("0.005000");
3471 let instrument = InstrumentAny::CurrencyPair(instrument);
3472 let position_id = PositionId::from("P-SIBLINGS");
3473 let missing_id = ClientOrderId::from("O-MISSING");
3474 let valid_id = ClientOrderId::from("O-VALID");
3475 let parent = OrderTestBuilder::new(OrderType::Limit)
3476 .instrument_id(instrument.id())
3477 .client_order_id(ClientOrderId::from("O-PARENT"))
3478 .side(OrderSide::Buy)
3479 .price(Price::from("50000.00"))
3480 .quantity(Quantity::from("0.010000"))
3481 .contingency_type(ContingencyType::Oto)
3482 .linked_order_ids(vec![missing_id, valid_id])
3483 .submit(true)
3484 .build();
3485 let missing = OrderTestBuilder::new(OrderType::StopMarket)
3486 .instrument_id(InstrumentId::from("BTC-USDT-MISSING.OKX"))
3487 .client_order_id(missing_id)
3488 .side(OrderSide::Sell)
3489 .trigger_price(Price::from("49000.00"))
3490 .quantity(Quantity::from("0.010000"))
3491 .reduce_only(true)
3492 .submit(true)
3493 .build();
3494 let valid = OrderTestBuilder::new(OrderType::StopMarket)
3495 .instrument_id(instrument.id())
3496 .client_order_id(valid_id)
3497 .side(OrderSide::Sell)
3498 .trigger_price(Price::from("49000.00"))
3499 .quantity(Quantity::from("0.010000"))
3500 .reduce_only(true)
3501 .submit(true)
3502 .build();
3503
3504 cache
3505 .borrow_mut()
3506 .add_instrument(instrument.clone())
3507 .unwrap();
3508
3509 for order in [&parent, &missing, &valid] {
3510 cache
3511 .borrow_mut()
3512 .add_order(order.clone(), None, None, false)
3513 .unwrap();
3514 }
3515 let event = apply_position_fill(
3516 &cache,
3517 &parent,
3518 &instrument,
3519 position_id,
3520 "T-PARENT",
3521 Quantity::from("0.010000"),
3522 Some(Money::from("0.00040000 BTC")),
3523 );
3524
3525 let actions = manager.handle_event(&event);
3526
3527 assert!(matches!(
3528 actions.as_slice(),
3529 [OrderManagerAction::ModifyLocalQuantity { order, quantity }]
3530 if order.client_order_id() == valid_id
3531 && *quantity == Quantity::from("0.005000")
3532 ));
3533 }
3534
3535 #[rstest]
3536 fn test_handle_oto_parent_cancel_cancels_child_with_zero_target() {
3537 let (clock, cache) = create_test_components();
3538 let mut manager = OrderManager::new(clock, cache.clone(), true);
3539 let mut instrument = currency_pair_btcusdt();
3540 instrument.size_increment = Quantity::from("0.005000");
3541 let instrument = InstrumentAny::CurrencyPair(instrument);
3542 let position_id = PositionId::from("P-CANCEL");
3543 let parent_id = ClientOrderId::from("O-PARENT");
3544 let child_id = ClientOrderId::from("O-CHILD");
3545 let parent = OrderTestBuilder::new(OrderType::Limit)
3546 .instrument_id(instrument.id())
3547 .client_order_id(parent_id)
3548 .side(OrderSide::Buy)
3549 .price(Price::from("50000.00"))
3550 .quantity(Quantity::from("0.010000"))
3551 .contingency_type(ContingencyType::Oto)
3552 .linked_order_ids(vec![child_id])
3553 .submit(true)
3554 .build();
3555 let child = OrderTestBuilder::new(OrderType::StopMarket)
3556 .instrument_id(instrument.id())
3557 .client_order_id(child_id)
3558 .side(OrderSide::Sell)
3559 .trigger_price(Price::from("49000.00"))
3560 .quantity(Quantity::from("0.010000"))
3561 .reduce_only(true)
3562 .build();
3563
3564 cache
3565 .borrow_mut()
3566 .add_instrument(instrument.clone())
3567 .unwrap();
3568
3569 for order in [&parent, &child] {
3570 cache
3571 .borrow_mut()
3572 .add_order(order.clone(), None, None, false)
3573 .unwrap();
3574 }
3575 let fill = apply_position_fill(
3576 &cache,
3577 &parent,
3578 &instrument,
3579 position_id,
3580 "T-PARENT",
3581 Quantity::from("0.003000"),
3582 Some(Money::from("0.00040000 BTC")),
3583 );
3584 assert!(manager.handle_event(&fill).is_empty());
3585
3586 let parent = cache.borrow().order_owned(&parent_id).unwrap();
3587 let canceled = apply_terminal(&cache, &parent, TerminalEvent::Canceled);
3588 let actions = manager.handle_event(&canceled);
3589
3590 assert_eq!(
3591 cache.borrow().position(&position_id).unwrap().quantity,
3592 Quantity::from("0.002600")
3593 );
3594 assert!(matches!(
3595 actions.as_slice(),
3596 [OrderManagerAction::CancelLocal(order)]
3597 if order.client_order_id() == child_id
3598 ));
3599 }
3600
3601 #[rstest]
3602 fn test_handle_oto_unfilled_terminal_parent_cancels_child_without_cached_position() {
3603 let (clock, cache) = create_test_components();
3604 let mut manager = OrderManager::new(clock, cache.clone(), true);
3605 let instrument = InstrumentAny::CurrencyPair(currency_pair_btcusdt());
3606 let position_id = PositionId::from("P-NOT-OPENED");
3607 let parent_id = ClientOrderId::from("O-PARENT");
3608 let child_id = ClientOrderId::from("O-CHILD");
3609 let parent = OrderTestBuilder::new(OrderType::Limit)
3610 .instrument_id(instrument.id())
3611 .client_order_id(parent_id)
3612 .side(OrderSide::Buy)
3613 .price(Price::from("50000.00"))
3614 .quantity(Quantity::from("1.000000"))
3615 .contingency_type(ContingencyType::Oto)
3616 .linked_order_ids(vec![child_id])
3617 .submit(true)
3618 .build();
3619 let child = OrderTestBuilder::new(OrderType::StopMarket)
3620 .instrument_id(instrument.id())
3621 .client_order_id(child_id)
3622 .side(OrderSide::Sell)
3623 .trigger_price(Price::from("49000.00"))
3624 .quantity(Quantity::from("1.000000"))
3625 .reduce_only(true)
3626 .build();
3627
3628 cache.borrow_mut().add_instrument(instrument).unwrap();
3629 cache
3630 .borrow_mut()
3631 .add_order(parent.clone(), Some(position_id), None, false)
3632 .unwrap();
3633 cache
3634 .borrow_mut()
3635 .add_order(child, Some(position_id), None, false)
3636 .unwrap();
3637 let canceled = apply_terminal(&cache, &parent, TerminalEvent::Canceled);
3638
3639 let actions = manager.handle_event(&canceled);
3640
3641 assert!(!cache.borrow().position_exists(&position_id));
3642 assert!(matches!(
3643 actions.as_slice(),
3644 [OrderManagerAction::CancelLocal(order)]
3645 if order.client_order_id() == child_id
3646 ));
3647 }
3648
3649 #[rstest]
3650 fn test_handle_oto_terminal_fill_cancels_child_when_position_is_below_increment() {
3651 let (clock, cache) = create_test_components();
3652 let mut manager = OrderManager::new(clock, cache.clone(), true);
3653 let mut instrument = currency_pair_btcusdt();
3654 instrument.size_increment = Quantity::from("0.005000");
3655 let instrument = InstrumentAny::CurrencyPair(instrument);
3656 let position_id = PositionId::from("P-ZERO");
3657 let parent_id = ClientOrderId::from("O-PARENT");
3658 let child_id = ClientOrderId::from("O-CHILD");
3659 let parent = OrderTestBuilder::new(OrderType::Limit)
3660 .instrument_id(instrument.id())
3661 .client_order_id(parent_id)
3662 .side(OrderSide::Buy)
3663 .price(Price::from("50000.00"))
3664 .quantity(Quantity::from("0.004000"))
3665 .contingency_type(ContingencyType::Oto)
3666 .linked_order_ids(vec![child_id])
3667 .submit(true)
3668 .build();
3669 let child = OrderTestBuilder::new(OrderType::StopMarket)
3670 .instrument_id(instrument.id())
3671 .client_order_id(child_id)
3672 .side(OrderSide::Sell)
3673 .trigger_price(Price::from("49000.00"))
3674 .quantity(Quantity::from("0.005000"))
3675 .reduce_only(true)
3676 .build();
3677
3678 cache
3679 .borrow_mut()
3680 .add_instrument(instrument.clone())
3681 .unwrap();
3682
3683 for order in [&parent, &child] {
3684 cache
3685 .borrow_mut()
3686 .add_order(order.clone(), None, None, false)
3687 .unwrap();
3688 }
3689 let event = apply_position_fill(
3690 &cache,
3691 &parent,
3692 &instrument,
3693 position_id,
3694 "T-PARENT",
3695 Quantity::from("0.004000"),
3696 Some(Money::from("0.00400000 BTC")),
3697 );
3698
3699 let actions = manager.handle_event(&event);
3700
3701 assert!(cache.borrow().position(&position_id).unwrap().is_closed());
3702 assert!(matches!(
3703 actions.as_slice(),
3704 [OrderManagerAction::CancelLocal(order)]
3705 if order.client_order_id() == child_id
3706 ));
3707 assert!(manager.submit_order_commands.is_empty());
3708 }
3709
3710 #[rstest]
3711 fn test_handle_oto_fill_rejects_conflicting_position_ids() {
3712 let (clock, cache) = create_test_components();
3713 let mut manager = OrderManager::new(clock, cache.clone(), true);
3714 let instrument = InstrumentAny::CurrencyPair(currency_pair_btcusdt());
3715 let cached_position_id = PositionId::from("P-CACHED");
3716 let event_position_id = PositionId::from("P-EVENT");
3717 let parent_id = ClientOrderId::from("O-PARENT");
3718 let child_id = ClientOrderId::from("O-CHILD");
3719 let parent = OrderTestBuilder::new(OrderType::Limit)
3720 .instrument_id(instrument.id())
3721 .client_order_id(parent_id)
3722 .side(OrderSide::Buy)
3723 .price(Price::from("50000.00"))
3724 .quantity(Quantity::from("1.000000"))
3725 .contingency_type(ContingencyType::Oto)
3726 .linked_order_ids(vec![child_id])
3727 .submit(true)
3728 .build();
3729 let event = TestOrderEventStubs::filled(
3730 &parent,
3731 &instrument,
3732 Some(TradeId::from("T-PARENT")),
3733 Some(event_position_id),
3734 None,
3735 None,
3736 None,
3737 None,
3738 None,
3739 Some(AccountId::from("ACCOUNT-001")),
3740 );
3741 cache
3742 .borrow_mut()
3743 .add_order(parent, Some(cached_position_id), None, false)
3744 .unwrap();
3745
3746 let actions = manager.handle_event(&event);
3747
3748 assert!(actions.is_empty());
3749 assert!(manager.submit_order_commands.is_empty());
3750 }
3751
3752 #[rstest]
3753 fn test_handle_oto_chained_child_releases_its_child() {
3754 let (clock, cache) = create_test_components();
3755 let mut manager = OrderManager::new(clock, cache.clone(), true);
3756 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
3757 let root_id = ClientOrderId::from("O-ROOT");
3758 let parent_id = ClientOrderId::from("O-PARENT");
3759 let child_id = ClientOrderId::from("O-CHILD");
3760 let parent = OrderTestBuilder::new(OrderType::Limit)
3761 .instrument_id(instrument.id())
3762 .client_order_id(parent_id)
3763 .side(OrderSide::Buy)
3764 .price(Price::from("1.00000"))
3765 .quantity(Quantity::from(100_000))
3766 .contingency_type(ContingencyType::Oto)
3767 .linked_order_ids(vec![child_id])
3768 .parent_order_id(root_id)
3769 .submit(true)
3770 .build();
3771 let child = OrderTestBuilder::new(OrderType::Limit)
3772 .instrument_id(instrument.id())
3773 .client_order_id(child_id)
3774 .side(OrderSide::Sell)
3775 .price(Price::from("1.00100"))
3776 .quantity(Quantity::from(100_000))
3777 .parent_order_id(parent_id)
3778 .emulation_trigger(TriggerType::BidAsk)
3779 .build();
3780 cache
3781 .borrow_mut()
3782 .add_order(parent.clone(), None, None, false)
3783 .unwrap();
3784 cache
3785 .borrow_mut()
3786 .add_order(child, None, None, false)
3787 .unwrap();
3788 apply_accepted(&cache, &parent, "V-PARENT");
3789 let fill = apply_fill(
3790 &cache,
3791 &parent,
3792 &instrument,
3793 "T-PARENT",
3794 Quantity::from(100_000),
3795 );
3796
3797 let actions = manager.handle_event(&fill);
3798
3799 assert!(matches!(
3800 actions.as_slice(),
3801 [OrderManagerAction::SubmitToEmulator(command)]
3802 if command.client_order_id == child_id
3803 ));
3804 }
3805
3806 #[rstest]
3807 fn test_handle_oto_cancels_child_filled_to_target() {
3808 let (clock, cache) = create_test_components();
3809 let mut manager = OrderManager::new(clock, cache.clone(), false);
3810 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
3811 let child_id = ClientOrderId::from("O-CHILD");
3812 let parent = OrderTestBuilder::new(OrderType::Limit)
3813 .instrument_id(instrument.id())
3814 .client_order_id(ClientOrderId::from("O-PARENT"))
3815 .side(OrderSide::Buy)
3816 .price(Price::from("1.00000"))
3817 .quantity(Quantity::from(100_000))
3818 .contingency_type(ContingencyType::Oto)
3819 .linked_order_ids(vec![child_id])
3820 .submit(true)
3821 .build();
3822 let child = OrderTestBuilder::new(OrderType::Limit)
3823 .instrument_id(instrument.id())
3824 .client_order_id(child_id)
3825 .side(OrderSide::Sell)
3826 .price(Price::from("1.00100"))
3827 .quantity(Quantity::from(100_000))
3828 .submit(true)
3829 .build();
3830 cache
3831 .borrow_mut()
3832 .add_order(parent.clone(), None, None, false)
3833 .unwrap();
3834 cache
3835 .borrow_mut()
3836 .add_order(child.clone(), None, None, false)
3837 .unwrap();
3838 apply_accepted(&cache, &parent, "V-PARENT");
3839 apply_accepted(&cache, &child, "V-CHILD");
3840 apply_fill(
3841 &cache,
3842 &child,
3843 &instrument,
3844 "T-CHILD",
3845 Quantity::from(60_000),
3846 );
3847 let parent_fill = apply_fill(
3848 &cache,
3849 &parent,
3850 &instrument,
3851 "T-PARENT",
3852 Quantity::from(40_000),
3853 );
3854
3855 let actions = manager.handle_event(&parent_fill);
3856
3857 assert!(matches!(
3858 actions.as_slice(),
3859 [OrderManagerAction::CancelLocal(order)]
3860 if order.client_order_id() == child_id
3861 ));
3862 assert!(!manager.oto_target_quantities.contains_key(&child_id));
3863 }
3864
3865 #[rstest]
3866 fn test_handle_oto_clears_target_when_child_leaves_active_local() {
3867 let (clock, cache) = create_test_components();
3868 let mut terminal_manager = OrderManager::new(clock.clone(), cache.clone(), true);
3869 let mut update_manager = OrderManager::new(clock.clone(), cache.clone(), true);
3870 let mut contingency_update_manager = OrderManager::new(clock.clone(), cache.clone(), true);
3871 let mut fill_manager = OrderManager::new(clock, cache.clone(), true);
3872 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
3873 let parent_id = ClientOrderId::from("O-PARENT");
3874 let child_id = ClientOrderId::from("O-CHILD");
3875 let parent = OrderTestBuilder::new(OrderType::Limit)
3876 .instrument_id(instrument.id())
3877 .client_order_id(parent_id)
3878 .side(OrderSide::Buy)
3879 .price(Price::from("1.00000"))
3880 .quantity(Quantity::from(100_000))
3881 .contingency_type(ContingencyType::Oto)
3882 .linked_order_ids(vec![child_id])
3883 .submit(true)
3884 .build();
3885 let child = OrderTestBuilder::new(OrderType::Limit)
3886 .instrument_id(instrument.id())
3887 .client_order_id(child_id)
3888 .side(OrderSide::Sell)
3889 .price(Price::from("1.00100"))
3890 .quantity(Quantity::from(100_000))
3891 .build();
3892 cache
3893 .borrow_mut()
3894 .add_order(parent.clone(), None, None, false)
3895 .unwrap();
3896 cache
3897 .borrow_mut()
3898 .add_order(child.clone(), None, None, false)
3899 .unwrap();
3900 let fill = apply_fill(
3901 &cache,
3902 &parent,
3903 &instrument,
3904 "T-PARTIAL",
3905 Quantity::from(40_000),
3906 );
3907 terminal_manager.handle_event(&fill);
3908 update_manager.handle_event(&fill);
3909 contingency_update_manager.handle_event(&fill);
3910 fill_manager.handle_event(&fill);
3911
3912 let submitted = TestOrderEventStubs::submitted(&child, AccountId::from("ACCOUNT-001"));
3913 cache.borrow_mut().update_order(&submitted).unwrap();
3914 apply_accepted(&cache, &child, "V-CHILD");
3915
3916 let cached_child = cache.borrow().order_owned(&child_id).unwrap();
3917 let child_fill = apply_fill(
3918 &cache,
3919 &cached_child,
3920 &instrument,
3921 "T-CHILD",
3922 Quantity::from(60_000),
3923 );
3924 assert!(fill_manager.handle_event(&child_fill).is_empty());
3925 assert!(!fill_manager.oto_target_quantities.contains_key(&child_id));
3926
3927 let cached_parent = cache.borrow().order_owned(&parent_id).unwrap();
3928 assert!(
3929 contingency_update_manager
3930 .handle_contingencies_update(&cached_parent)
3931 .is_empty()
3932 );
3933 assert!(
3934 !contingency_update_manager
3935 .oto_target_quantities
3936 .contains_key(&child_id)
3937 );
3938
3939 let terminal = apply_terminal(&cache, &parent, TerminalEvent::Canceled);
3940 assert!(terminal_manager.handle_event(&terminal).is_empty());
3941 assert!(
3942 !terminal_manager
3943 .oto_target_quantities
3944 .contains_key(&child_id)
3945 );
3946
3947 let update = apply_update(&cache, &child, Quantity::from(80_000));
3948 assert!(update_manager.handle_event(&update).is_empty());
3949 assert!(!update_manager.oto_target_quantities.contains_key(&child_id));
3950 }
3951
3952 #[rstest]
3953 fn test_handle_oto_out_of_order_updates_reassert_latest_target() {
3954 let (clock, cache) = create_test_components();
3955 let mut manager = OrderManager::new(clock, cache.clone(), false);
3956 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
3957 let child_id = ClientOrderId::from("O-CHILD");
3958 let parent = OrderTestBuilder::new(OrderType::Limit)
3959 .instrument_id(instrument.id())
3960 .client_order_id(ClientOrderId::from("O-PARENT"))
3961 .side(OrderSide::Buy)
3962 .price(Price::from("1.00000"))
3963 .quantity(Quantity::from(100_000))
3964 .contingency_type(ContingencyType::Oto)
3965 .linked_order_ids(vec![child_id])
3966 .submit(true)
3967 .build();
3968 let child = OrderTestBuilder::new(OrderType::Limit)
3969 .instrument_id(instrument.id())
3970 .client_order_id(child_id)
3971 .side(OrderSide::Sell)
3972 .price(Price::from("1.00100"))
3973 .quantity(Quantity::from(100_000))
3974 .submit(true)
3975 .build();
3976 cache
3977 .borrow_mut()
3978 .add_order(parent.clone(), None, None, false)
3979 .unwrap();
3980 cache
3981 .borrow_mut()
3982 .add_order(child.clone(), None, None, false)
3983 .unwrap();
3984 apply_accepted(&cache, &child, "V-CHILD");
3985
3986 let first_fill = apply_fill(
3987 &cache,
3988 &parent,
3989 &instrument,
3990 "T-OUT-OF-ORDER-1",
3991 Quantity::from(40_000),
3992 );
3993 manager.handle_event(&first_fill);
3994 let second_fill = apply_fill(
3995 &cache,
3996 &parent,
3997 &instrument,
3998 "T-OUT-OF-ORDER-2",
3999 Quantity::from(60_000),
4000 );
4001 manager.handle_event(&second_fill);
4002
4003 let latest_update = apply_update(&cache, &child, Quantity::from(100_000));
4004 let latest_actions = manager.handle_event(&latest_update);
4005 let stale_update = apply_update(&cache, &child, Quantity::from(40_000));
4006 let corrective_actions = manager.handle_event(&stale_update);
4007
4008 assert!(latest_actions.is_empty());
4009 assert!(matches!(
4010 corrective_actions.as_slice(),
4011 [OrderManagerAction::ModifyLocalQuantity { order, quantity }]
4012 if order.client_order_id() == child_id
4013 && *quantity == Quantity::from(100_000)
4014 ));
4015 }
4016
4017 #[rstest]
4018 fn test_handle_oto_child_fill_waits_for_parent_target_refresh() {
4019 let (clock, cache) = create_test_components();
4020 let mut manager = OrderManager::new(clock, cache.clone(), false);
4021 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
4022 let parent_id = ClientOrderId::from("O-PARENT");
4023 let child_id = ClientOrderId::from("O-CHILD");
4024 let parent = OrderTestBuilder::new(OrderType::Limit)
4025 .instrument_id(instrument.id())
4026 .client_order_id(parent_id)
4027 .side(OrderSide::Buy)
4028 .price(Price::from("1.00000"))
4029 .quantity(Quantity::from(100_000))
4030 .contingency_type(ContingencyType::Oto)
4031 .linked_order_ids(vec![child_id])
4032 .submit(true)
4033 .build();
4034 let child = OrderTestBuilder::new(OrderType::Limit)
4035 .instrument_id(instrument.id())
4036 .client_order_id(child_id)
4037 .side(OrderSide::Sell)
4038 .price(Price::from("1.00100"))
4039 .quantity(Quantity::from(100_000))
4040 .contingency_type(ContingencyType::Oto)
4041 .linked_order_ids(vec![parent_id])
4042 .parent_order_id(parent_id)
4043 .submit(true)
4044 .build();
4045 cache
4046 .borrow_mut()
4047 .add_order(parent.clone(), None, None, false)
4048 .unwrap();
4049 cache
4050 .borrow_mut()
4051 .add_order(child.clone(), None, None, false)
4052 .unwrap();
4053 apply_accepted(&cache, &parent, "V-PARENT");
4054 apply_accepted(&cache, &child, "V-CHILD");
4055 let parent_fill = apply_fill(
4056 &cache,
4057 &parent,
4058 &instrument,
4059 "T-PARENT",
4060 Quantity::from(40_000),
4061 );
4062 manager.handle_event(&parent_fill);
4063 let child_fill = apply_fill(
4064 &cache,
4065 &child,
4066 &instrument,
4067 "T-CHILD",
4068 Quantity::from(60_000),
4069 );
4070
4071 let actions = manager.handle_event(&child_fill);
4072
4073 assert!(actions.is_empty());
4074 assert_eq!(
4075 manager.oto_target_quantities.get(&child_id),
4076 Some(&Quantity::from(40_000)),
4077 );
4078
4079 let cached_parent = cache.borrow().order_owned(&parent_id).unwrap();
4080 let final_parent_fill = apply_fill(
4081 &cache,
4082 &cached_parent,
4083 &instrument,
4084 "T-PARENT-FINAL",
4085 Quantity::from(60_000),
4086 );
4087 let final_actions = manager.handle_event(&final_parent_fill);
4088
4089 assert!(final_actions.is_empty());
4090 assert_eq!(
4091 manager.oto_target_quantities.get(&child_id),
4092 Some(&Quantity::from(100_000)),
4093 );
4094 }
4095
4096 #[rstest]
4097 fn test_handle_oto_child_update_waits_when_filled_past_stored_target() {
4098 let (clock, cache) = create_test_components();
4099 let mut manager = OrderManager::new(clock, cache.clone(), false);
4100 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
4101 let child_id = ClientOrderId::from("O-CHILD");
4102 let child = OrderTestBuilder::new(OrderType::Limit)
4103 .instrument_id(instrument.id())
4104 .client_order_id(child_id)
4105 .side(OrderSide::Sell)
4106 .price(Price::from("1.00100"))
4107 .quantity(Quantity::from(100_000))
4108 .submit(true)
4109 .build();
4110 cache
4111 .borrow_mut()
4112 .add_order(child.clone(), None, None, false)
4113 .unwrap();
4114 apply_accepted(&cache, &child, "V-CHILD");
4115 apply_fill(
4116 &cache,
4117 &child,
4118 &instrument,
4119 "T-CHILD",
4120 Quantity::from(60_000),
4121 );
4122 manager
4123 .oto_target_quantities
4124 .insert(child_id, Quantity::from(40_000));
4125 let update = apply_update(&cache, &child, Quantity::from(90_000));
4126
4127 let actions = manager.handle_event(&update);
4128
4129 assert!(actions.is_empty());
4130 assert_eq!(
4131 manager.oto_target_quantities.get(&child_id),
4132 Some(&Quantity::from(40_000)),
4133 );
4134 }
4135
4136 #[rstest]
4137 fn test_handle_oto_parent_update_preserves_partial_fill_target() {
4138 let (clock, cache) = create_test_components();
4139 let mut manager = OrderManager::new(clock, cache.clone(), false);
4140 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
4141 let child_id = ClientOrderId::from("O-CHILD");
4142 let parent = OrderTestBuilder::new(OrderType::Limit)
4143 .instrument_id(instrument.id())
4144 .client_order_id(ClientOrderId::from("O-PARENT"))
4145 .side(OrderSide::Buy)
4146 .price(Price::from("1.00000"))
4147 .quantity(Quantity::from(100_000))
4148 .contingency_type(ContingencyType::Oto)
4149 .linked_order_ids(vec![child_id])
4150 .submit(true)
4151 .build();
4152 let child = OrderTestBuilder::new(OrderType::Limit)
4153 .instrument_id(instrument.id())
4154 .client_order_id(child_id)
4155 .side(OrderSide::Sell)
4156 .price(Price::from("1.00100"))
4157 .quantity(Quantity::from(100_000))
4158 .submit(true)
4159 .build();
4160 cache
4161 .borrow_mut()
4162 .add_order(parent.clone(), None, None, false)
4163 .unwrap();
4164 cache
4165 .borrow_mut()
4166 .add_order(child, None, None, false)
4167 .unwrap();
4168 let fill = apply_fill(
4169 &cache,
4170 &parent,
4171 &instrument,
4172 "T-PARTIAL",
4173 Quantity::from(40_000),
4174 );
4175 manager.handle_event(&fill);
4176 let parent_update = apply_update(&cache, &parent, Quantity::from(120_000));
4177
4178 let actions = manager.handle_event(&parent_update);
4179
4180 assert!(actions.is_empty());
4181 assert_eq!(
4182 manager.oto_target_quantities.get(&child_id),
4183 Some(&Quantity::from(40_000)),
4184 );
4185
4186 let terminal = apply_terminal(&cache, &parent, TerminalEvent::Canceled);
4187 assert!(manager.handle_event(&terminal).is_empty());
4188 assert_eq!(
4189 manager.oto_target_quantities.get(&child_id),
4190 Some(&Quantity::from(40_000)),
4191 );
4192 }
4193
4194 #[rstest]
4195 fn test_handle_oto_exec_spawn_fill_uses_commission_adjusted_position() {
4196 let (clock, cache) = create_test_components();
4197 let mut manager = OrderManager::new(clock, cache.clone(), false);
4198 let mut instrument = currency_pair_btcusdt();
4199 instrument.size_increment = Quantity::from("0.005000");
4200 let instrument = InstrumentAny::CurrencyPair(instrument);
4201 let position_id = PositionId::from("P-SPAWN");
4202 let spawn_id = ClientOrderId::from("O-SPAWN");
4203 let first_id = ClientOrderId::from("O-SPAWN-1");
4204 let second_id = ClientOrderId::from("O-SPAWN-2");
4205 let child_id = ClientOrderId::from("O-CHILD");
4206 let exec_algorithm_id = ExecAlgorithmId::from("TWAP");
4207 let first = OrderTestBuilder::new(OrderType::Limit)
4208 .instrument_id(instrument.id())
4209 .client_order_id(first_id)
4210 .side(OrderSide::Buy)
4211 .price(Price::from("50000.00"))
4212 .quantity(Quantity::from("0.600000"))
4213 .contingency_type(ContingencyType::Oto)
4214 .linked_order_ids(vec![child_id])
4215 .exec_algorithm_id(exec_algorithm_id)
4216 .exec_spawn_id(spawn_id)
4217 .submit(true)
4218 .build();
4219 let second = OrderTestBuilder::new(OrderType::Limit)
4220 .instrument_id(instrument.id())
4221 .client_order_id(second_id)
4222 .side(OrderSide::Buy)
4223 .price(Price::from("50000.00"))
4224 .quantity(Quantity::from("0.400000"))
4225 .contingency_type(ContingencyType::Oto)
4226 .linked_order_ids(vec![child_id])
4227 .exec_algorithm_id(exec_algorithm_id)
4228 .exec_spawn_id(spawn_id)
4229 .submit(true)
4230 .build();
4231 let child = OrderTestBuilder::new(OrderType::StopMarket)
4232 .instrument_id(instrument.id())
4233 .client_order_id(child_id)
4234 .side(OrderSide::Sell)
4235 .trigger_price(Price::from("49000.00"))
4236 .quantity(Quantity::from("1.000000"))
4237 .reduce_only(true)
4238 .submit(true)
4239 .build();
4240
4241 cache
4242 .borrow_mut()
4243 .add_instrument(instrument.clone())
4244 .unwrap();
4245
4246 for order in [&first, &second, &child] {
4247 cache
4248 .borrow_mut()
4249 .add_order(order.clone(), None, None, false)
4250 .unwrap();
4251 }
4252 apply_accepted(&cache, &first, "V-SPAWN-1");
4253 apply_accepted(&cache, &second, "V-SPAWN-2");
4254 let first = cache.borrow().order_owned(&first_id).unwrap();
4255 let second = cache.borrow().order_owned(&second_id).unwrap();
4256 apply_position_fill(
4257 &cache,
4258 &first,
4259 &instrument,
4260 position_id,
4261 "T-SPAWN-1",
4262 Quantity::from("0.600000"),
4263 Some(Money::from("0.00040000 BTC")),
4264 );
4265 let event = apply_position_fill(
4266 &cache,
4267 &second,
4268 &instrument,
4269 position_id,
4270 "T-SPAWN-2",
4271 Quantity::from("0.100000"),
4272 Some(Money::from("0.00040000 BTC")),
4273 );
4274
4275 let actions = manager.handle_event(&event);
4276 let repeated_actions = manager.handle_event(&event);
4277 let position = cache.borrow().position_owned(&position_id).unwrap();
4278
4279 assert_eq!(position.quantity, Quantity::from("0.699200"));
4280 assert!(matches!(
4281 actions.as_slice(),
4282 [OrderManagerAction::ModifyLocalQuantity { order, quantity }]
4283 if order.client_order_id() == child_id
4284 && *quantity == Quantity::from("0.695000")
4285 ));
4286 let OrderManagerAction::ModifyLocalQuantity { order, quantity } = &actions[0] else {
4287 unreachable!();
4288 };
4289 let mut resized = order.clone();
4290 resized.set_quantity(*quantity);
4291 resized.set_leaves_qty(*quantity);
4292
4293 assert!(resized.would_reduce_only(position.side, position.quantity));
4294 assert!(repeated_actions.is_empty());
4295
4296 let second = cache.borrow().order_owned(&second_id).unwrap();
4297 assert_eq!(second.position_id(), Some(position_id));
4298 assert!(manager.handle_contingencies(&second).is_empty());
4299 assert!(manager.handle_contingencies_update(&second).is_empty());
4300 }
4301
4302 #[rstest]
4303 fn test_handle_oto_exec_spawn_update_uses_filled_sibling_position() {
4304 let (clock, cache) = create_test_components();
4305 let mut manager = OrderManager::new(clock, cache.clone(), false);
4306 let mut instrument = currency_pair_btcusdt();
4307 instrument.size_increment = Quantity::from("0.005000");
4308 let instrument = InstrumentAny::CurrencyPair(instrument);
4309 let position_id = PositionId::from("P-SPAWN");
4310 let spawn_id = ClientOrderId::from("O-SPAWN");
4311 let first_id = ClientOrderId::from("O-SPAWN-1");
4312 let second_id = ClientOrderId::from("O-SPAWN-2");
4313 let child_id = ClientOrderId::from("O-CHILD");
4314 let exec_algorithm_id = ExecAlgorithmId::from("TWAP");
4315 let first = OrderTestBuilder::new(OrderType::Limit)
4316 .instrument_id(instrument.id())
4317 .client_order_id(first_id)
4318 .side(OrderSide::Buy)
4319 .price(Price::from("50000.00"))
4320 .quantity(Quantity::from("0.600000"))
4321 .contingency_type(ContingencyType::Oto)
4322 .linked_order_ids(vec![child_id])
4323 .exec_algorithm_id(exec_algorithm_id)
4324 .exec_spawn_id(spawn_id)
4325 .submit(true)
4326 .build();
4327 let second = OrderTestBuilder::new(OrderType::Limit)
4328 .instrument_id(instrument.id())
4329 .client_order_id(second_id)
4330 .side(OrderSide::Buy)
4331 .price(Price::from("50000.00"))
4332 .quantity(Quantity::from("0.400000"))
4333 .contingency_type(ContingencyType::Oto)
4334 .linked_order_ids(vec![child_id])
4335 .exec_algorithm_id(exec_algorithm_id)
4336 .exec_spawn_id(spawn_id)
4337 .submit(true)
4338 .build();
4339 let child = OrderTestBuilder::new(OrderType::StopMarket)
4340 .instrument_id(instrument.id())
4341 .client_order_id(child_id)
4342 .side(OrderSide::Sell)
4343 .trigger_price(Price::from("49000.00"))
4344 .quantity(Quantity::from("1.000000"))
4345 .reduce_only(true)
4346 .submit(true)
4347 .build();
4348
4349 cache
4350 .borrow_mut()
4351 .add_instrument(instrument.clone())
4352 .unwrap();
4353
4354 for order in [&first, &second, &child] {
4355 cache
4356 .borrow_mut()
4357 .add_order(order.clone(), None, None, false)
4358 .unwrap();
4359 }
4360 apply_accepted(&cache, &first, "V-SPAWN-1");
4361 apply_accepted(&cache, &second, "V-SPAWN-2");
4362 let first = cache.borrow().order_owned(&first_id).unwrap();
4363 let first_fill = apply_position_fill(
4364 &cache,
4365 &first,
4366 &instrument,
4367 position_id,
4368 "T-SPAWN-1",
4369 Quantity::from("0.600000"),
4370 Some(Money::from("0.00040000 BTC")),
4371 );
4372 let first_actions = manager.handle_event(&first_fill);
4373 assert!(matches!(
4374 first_actions.as_slice(),
4375 [OrderManagerAction::ModifyLocalQuantity { quantity, .. }]
4376 if *quantity == Quantity::from("0.595000")
4377 ));
4378 apply_update(&cache, &child, Quantity::from("0.595000"));
4379
4380 let second = cache.borrow().order_owned(&second_id).unwrap();
4381 assert_eq!(second.position_id(), None);
4382 let update = OrderEventAny::Updated(
4383 OrderUpdatedSpec::builder()
4384 .trader_id(second.trader_id())
4385 .strategy_id(second.strategy_id())
4386 .instrument_id(second.instrument_id())
4387 .client_order_id(second.client_order_id())
4388 .quantity(Quantity::from("0.500000"))
4389 .venue_order_id(VenueOrderId::from("V-SPAWN-2"))
4390 .account_id(AccountId::from("ACCOUNT-001"))
4391 .build(),
4392 );
4393 cache.borrow_mut().update_order(&update).unwrap();
4394 let actions = manager.handle_event(&update);
4395
4396 assert!(actions.is_empty());
4397 assert_eq!(
4398 manager.oto_target_quantities.get(&child_id),
4399 Some(&Quantity::from("0.595000")),
4400 );
4401 }
4402
4403 #[rstest]
4404 fn test_handle_oto_fill_uses_completed_and_active_spawn_totals() {
4405 let (clock, cache) = create_test_components();
4406 let mut manager = OrderManager::new(clock, cache.clone(), false);
4407 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
4408 let spawn_id = ClientOrderId::from("O-SPAWN");
4409 let first_id = ClientOrderId::from("O-SPAWN-1");
4410 let second_id = ClientOrderId::from("O-SPAWN-2");
4411 let child_id = ClientOrderId::from("O-CHILD");
4412 let exec_algorithm_id = ExecAlgorithmId::from("TWAP");
4413 let first = OrderTestBuilder::new(OrderType::Limit)
4414 .instrument_id(instrument.id())
4415 .client_order_id(first_id)
4416 .side(OrderSide::Buy)
4417 .price(Price::from("1.00000"))
4418 .quantity(Quantity::from(60_000))
4419 .contingency_type(ContingencyType::Oto)
4420 .linked_order_ids(vec![child_id])
4421 .exec_algorithm_id(exec_algorithm_id)
4422 .exec_spawn_id(spawn_id)
4423 .submit(true)
4424 .build();
4425 let second = OrderTestBuilder::new(OrderType::Limit)
4426 .instrument_id(instrument.id())
4427 .client_order_id(second_id)
4428 .side(OrderSide::Buy)
4429 .price(Price::from("1.00000"))
4430 .quantity(Quantity::from(40_000))
4431 .contingency_type(ContingencyType::Oto)
4432 .linked_order_ids(vec![child_id])
4433 .exec_algorithm_id(exec_algorithm_id)
4434 .exec_spawn_id(spawn_id)
4435 .submit(true)
4436 .build();
4437 let child = OrderTestBuilder::new(OrderType::Limit)
4438 .instrument_id(instrument.id())
4439 .client_order_id(child_id)
4440 .side(OrderSide::Sell)
4441 .price(Price::from("1.00100"))
4442 .quantity(Quantity::from(100_000))
4443 .submit(true)
4444 .build();
4445
4446 for order in [&first, &second, &child] {
4447 cache
4448 .borrow_mut()
4449 .add_order(order.clone(), None, None, false)
4450 .unwrap();
4451 }
4452 apply_accepted(&cache, &first, "V-SPAWN-1");
4453 apply_accepted(&cache, &second, "V-SPAWN-2");
4454 apply_fill(
4455 &cache,
4456 &first,
4457 &instrument,
4458 "T-SPAWN-1",
4459 Quantity::from(60_000),
4460 );
4461 let event = apply_fill(
4462 &cache,
4463 &second,
4464 &instrument,
4465 "T-SPAWN-2",
4466 Quantity::from(10_000),
4467 );
4468
4469 let actions = manager.handle_event(&event);
4470
4471 assert!(matches!(
4472 actions.first(),
4473 Some(OrderManagerAction::ModifyLocalQuantity { order, quantity })
4474 if order.client_order_id() == child_id
4475 && *quantity == Quantity::from(70_000)
4476 ));
4477
4478 let update = apply_update(&cache, &second, Quantity::from(50_000));
4479 assert!(manager.handle_event(&update).is_empty());
4480 assert_eq!(
4481 manager.oto_target_quantities.get(&child_id),
4482 Some(&Quantity::from(70_000)),
4483 );
4484
4485 let cached_second = cache.borrow().order_owned(&second_id).unwrap();
4486 assert!(manager.handle_contingencies(&cached_second).is_empty());
4487 }
4488
4489 #[rstest]
4490 fn test_handle_ouo_partial_fill_resizes_open_sibling() {
4491 let (clock, cache) = create_test_components();
4492 let mut manager = OrderManager::new(clock, cache.clone(), false);
4493 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
4494 let parent_id = ClientOrderId::from("O-PARENT");
4495 let sibling_id = ClientOrderId::from("O-SIBLING");
4496 let parent = OrderTestBuilder::new(OrderType::Limit)
4497 .instrument_id(instrument.id())
4498 .client_order_id(parent_id)
4499 .side(OrderSide::Buy)
4500 .price(Price::from("1.00000"))
4501 .quantity(Quantity::from(100_000))
4502 .contingency_type(ContingencyType::Ouo)
4503 .linked_order_ids(vec![parent_id, sibling_id])
4504 .submit(true)
4505 .build();
4506 let sibling = OrderTestBuilder::new(OrderType::Limit)
4507 .instrument_id(instrument.id())
4508 .client_order_id(sibling_id)
4509 .side(OrderSide::Sell)
4510 .price(Price::from("1.00100"))
4511 .quantity(Quantity::from(100_000))
4512 .submit(true)
4513 .build();
4514 cache
4515 .borrow_mut()
4516 .add_order(parent.clone(), None, None, false)
4517 .unwrap();
4518 cache
4519 .borrow_mut()
4520 .add_order(sibling, None, None, false)
4521 .unwrap();
4522 let event = apply_fill(
4523 &cache,
4524 &parent,
4525 &instrument,
4526 "T-OUO",
4527 Quantity::from(25_000),
4528 );
4529
4530 let actions = manager.handle_event(&event);
4531
4532 assert!(matches!(
4533 actions.as_slice(),
4534 [OrderManagerAction::ModifyLocalQuantity { order, quantity }]
4535 if order.client_order_id() == sibling_id
4536 && *quantity == Quantity::from(75_000)
4537 ));
4538 }
4539
4540 #[rstest]
4541 fn test_handle_ouo_partial_fill_cancels_sibling_filled_past_target() {
4542 let (clock, cache) = create_test_components();
4543 let mut manager = OrderManager::new(clock, cache.clone(), false);
4544 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
4545 let parent_id = ClientOrderId::from("O-PARENT");
4546 let sibling_id = ClientOrderId::from("O-SIBLING");
4547 let parent = OrderTestBuilder::new(OrderType::Limit)
4548 .instrument_id(instrument.id())
4549 .client_order_id(parent_id)
4550 .side(OrderSide::Buy)
4551 .price(Price::from("1.00000"))
4552 .quantity(Quantity::from(100_000))
4553 .contingency_type(ContingencyType::Ouo)
4554 .linked_order_ids(vec![parent_id, sibling_id])
4555 .submit(true)
4556 .build();
4557 let sibling = OrderTestBuilder::new(OrderType::Limit)
4558 .instrument_id(instrument.id())
4559 .client_order_id(sibling_id)
4560 .side(OrderSide::Sell)
4561 .price(Price::from("1.00100"))
4562 .quantity(Quantity::from(100_000))
4563 .submit(true)
4564 .build();
4565 cache
4566 .borrow_mut()
4567 .add_order(parent.clone(), None, None, false)
4568 .unwrap();
4569 cache
4570 .borrow_mut()
4571 .add_order(sibling.clone(), None, None, false)
4572 .unwrap();
4573 apply_accepted(&cache, &parent, "V-PARENT");
4574 apply_accepted(&cache, &sibling, "V-SIBLING");
4575 apply_fill(
4576 &cache,
4577 &sibling,
4578 &instrument,
4579 "T-SIBLING",
4580 Quantity::from(30_000),
4581 );
4582 let parent_fill = apply_fill(
4583 &cache,
4584 &parent,
4585 &instrument,
4586 "T-PARENT",
4587 Quantity::from(90_000),
4588 );
4589
4590 let actions = manager.handle_event(&parent_fill);
4591
4592 assert!(matches!(
4593 actions.as_slice(),
4594 [OrderManagerAction::CancelLocal(order)]
4595 if order.client_order_id() == sibling_id
4596 ));
4597 }
4598
4599 #[rstest]
4600 #[case(TerminalEvent::Canceled)]
4601 #[case(TerminalEvent::Expired)]
4602 #[case(TerminalEvent::Rejected)]
4603 fn test_terminal_non_contingent_event_clears_oto_target(#[case] terminal: TerminalEvent) {
4604 let (clock, cache) = create_test_components();
4605 let mut manager = OrderManager::new(clock, cache.clone(), false);
4606 let order = OrderTestBuilder::new(OrderType::Limit)
4607 .instrument_id(audusd_sim().id())
4608 .client_order_id(ClientOrderId::from("O-001"))
4609 .side(OrderSide::Buy)
4610 .price(Price::from("1.00000"))
4611 .quantity(Quantity::from(100_000))
4612 .submit(true)
4613 .build();
4614 cache
4615 .borrow_mut()
4616 .add_order(order.clone(), None, None, false)
4617 .unwrap();
4618
4619 if matches!(terminal, TerminalEvent::Expired) {
4620 apply_accepted(&cache, &order, "V-001");
4621 }
4622 manager
4623 .oto_target_quantities
4624 .insert(order.client_order_id(), Quantity::from(40_000));
4625 let event = apply_terminal(&cache, &order, terminal);
4626
4627 let actions = manager.handle_event(&event);
4628
4629 assert!(actions.is_empty());
4630 assert!(manager.oto_target_quantities.is_empty());
4631 }
4632
4633 #[rstest]
4634 #[case(ContingencyType::Oto, TerminalEvent::Rejected)]
4635 #[case(ContingencyType::Oco, TerminalEvent::Canceled)]
4636 #[case(ContingencyType::Oco, TerminalEvent::Expired)]
4637 #[case(ContingencyType::Oco, TerminalEvent::Rejected)]
4638 #[case(ContingencyType::Ouo, TerminalEvent::Canceled)]
4639 fn test_terminal_contingent_event_cancels_open_sibling(
4640 #[case] contingency_type: ContingencyType,
4641 #[case] terminal: TerminalEvent,
4642 ) {
4643 let (clock, cache) = create_test_components();
4644 let mut manager = OrderManager::new(clock, cache.clone(), false);
4645 let parent_id = ClientOrderId::from("O-PARENT");
4646 let sibling_id = ClientOrderId::from("O-SIBLING");
4647 let linked_order_ids = if contingency_type == ContingencyType::Oto {
4648 vec![sibling_id]
4649 } else {
4650 vec![parent_id, sibling_id]
4651 };
4652 let parent = OrderTestBuilder::new(OrderType::Limit)
4653 .instrument_id(audusd_sim().id())
4654 .client_order_id(parent_id)
4655 .side(OrderSide::Buy)
4656 .price(Price::from("1.00000"))
4657 .quantity(Quantity::from(100_000))
4658 .contingency_type(contingency_type)
4659 .linked_order_ids(linked_order_ids)
4660 .submit(true)
4661 .build();
4662 let sibling = OrderTestBuilder::new(OrderType::Limit)
4663 .instrument_id(audusd_sim().id())
4664 .client_order_id(sibling_id)
4665 .side(OrderSide::Sell)
4666 .price(Price::from("1.00100"))
4667 .quantity(Quantity::from(100_000))
4668 .submit(true)
4669 .build();
4670 cache
4671 .borrow_mut()
4672 .add_order(parent.clone(), None, None, false)
4673 .unwrap();
4674 cache
4675 .borrow_mut()
4676 .add_order(sibling, None, None, false)
4677 .unwrap();
4678
4679 if matches!(terminal, TerminalEvent::Expired) {
4680 apply_accepted(&cache, &parent, "V-PARENT");
4681 }
4682 let event = apply_terminal(&cache, &parent, terminal);
4683
4684 let actions = manager.handle_event(&event);
4685
4686 assert!(matches!(
4687 actions.as_slice(),
4688 [OrderManagerAction::CancelLocal(order)]
4689 if order.client_order_id() == sibling_id
4690 ));
4691 }
4692
4693 #[rstest]
4694 #[case(ContingencyType::Oto)]
4695 #[case(ContingencyType::Ouo)]
4696 fn test_strategy_manager_excludes_active_local_and_closed_siblings(
4697 #[case] contingency_type: ContingencyType,
4698 ) {
4699 let (clock, cache) = create_test_components();
4700 let mut manager = OrderManager::new(clock, cache.clone(), false);
4701 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
4702 let parent_id = ClientOrderId::from("O-PARENT");
4703 let active_local_id = ClientOrderId::from("O-ACTIVE-LOCAL");
4704 let closed_id = ClientOrderId::from("O-CLOSED");
4705 let parent = OrderTestBuilder::new(OrderType::Limit)
4706 .instrument_id(instrument.id())
4707 .client_order_id(parent_id)
4708 .side(OrderSide::Buy)
4709 .price(Price::from("1.00000"))
4710 .quantity(Quantity::from(100_000))
4711 .contingency_type(contingency_type)
4712 .linked_order_ids(vec![active_local_id, closed_id])
4713 .submit(true)
4714 .build();
4715 let active_local = OrderTestBuilder::new(OrderType::Limit)
4716 .instrument_id(instrument.id())
4717 .client_order_id(active_local_id)
4718 .side(OrderSide::Sell)
4719 .price(Price::from("1.00100"))
4720 .quantity(Quantity::from(100_000))
4721 .build();
4722 let closed = OrderTestBuilder::new(OrderType::Limit)
4723 .instrument_id(instrument.id())
4724 .client_order_id(closed_id)
4725 .side(OrderSide::Sell)
4726 .price(Price::from("1.00100"))
4727 .quantity(Quantity::from(100_000))
4728 .submit(true)
4729 .build();
4730 cache
4731 .borrow_mut()
4732 .add_order(parent.clone(), None, None, false)
4733 .unwrap();
4734 cache
4735 .borrow_mut()
4736 .add_order(active_local, None, None, false)
4737 .unwrap();
4738 cache
4739 .borrow_mut()
4740 .add_order(closed.clone(), None, None, false)
4741 .unwrap();
4742 apply_terminal(&cache, &closed, TerminalEvent::Canceled);
4743 let event = apply_fill(
4744 &cache,
4745 &parent,
4746 &instrument,
4747 "T-EXCLUSION",
4748 Quantity::from(100_000),
4749 );
4750
4751 let actions = manager.handle_event(&event);
4752
4753 assert!(actions.is_empty());
4754 }
4755}