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