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