Skip to main content

nautilus_execution/order_manager/
manager.rs

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