Skip to main content

nautilus_execution/order_manager/
manager.rs

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