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    orders::{Order, OrderAny},
28    types::Quantity,
29};
30
31use super::OrderManagerAction;
32
33/// Manages the lifecycle and state of orders with contingency handling.
34///
35/// The order manager is responsible for managing local order state, handling
36/// contingent orders (OTO, OCO, OUO), and coordinating with emulation and
37/// execution systems. It tracks order commands and manages complex order
38/// relationships for advanced order types.
39pub struct OrderManager {
40    clock: Rc<RefCell<dyn Clock>>,
41    cache: Rc<RefCell<Cache>>,
42    active_local: bool,
43    submit_order_commands: AHashMap<ClientOrderId, SubmitOrder>,
44    oto_target_quantities: AHashMap<ClientOrderId, Quantity>,
45}
46
47impl Debug for OrderManager {
48    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49        f.debug_struct(stringify!(OrderManager))
50            .field("pending_commands", &self.submit_order_commands.len())
51            .finish()
52    }
53}
54
55impl OrderManager {
56    /// Creates a new [`OrderManager`] instance.
57    pub fn new(
58        clock: Rc<RefCell<dyn Clock>>,
59        cache: Rc<RefCell<Cache>>,
60        active_local: bool,
61    ) -> Self {
62        Self {
63            clock,
64            cache,
65            active_local,
66            submit_order_commands: AHashMap::new(),
67            oto_target_quantities: AHashMap::new(),
68        }
69    }
70
71    #[must_use]
72    /// Returns a copy of all cached submit order commands.
73    pub fn get_submit_order_commands(&self) -> AHashMap<ClientOrderId, SubmitOrder> {
74        self.submit_order_commands.clone()
75    }
76
77    /// Caches a submit order command for later processing.
78    pub fn cache_submit_order_command(&mut self, command: SubmitOrder) {
79        self.submit_order_commands
80            .insert(command.client_order_id, command);
81    }
82
83    /// Removes and returns a cached submit order command.
84    pub fn pop_submit_order_command(
85        &mut self,
86        client_order_id: ClientOrderId,
87    ) -> Option<SubmitOrder> {
88        self.submit_order_commands.remove(&client_order_id)
89    }
90
91    /// Resets the order manager by clearing all stateful values.
92    pub fn reset(&mut self) {
93        self.submit_order_commands.clear();
94        self.oto_target_quantities.clear();
95    }
96
97    /// Cancels an order if it's not already pending cancellation or closed.
98    pub fn cancel_order(&mut self, order: &OrderAny) -> Vec<OrderManagerAction> {
99        let client_order_id = order.client_order_id();
100        let cache = self.cache.borrow();
101
102        if cache.is_order_pending_cancel_local(&client_order_id) {
103            return Vec::new();
104        }
105
106        if order.is_closed() || cache.is_order_closed(&client_order_id) {
107            log::warn!("Cannot cancel order: already closed");
108            return Vec::new();
109        }
110
111        drop(cache);
112        self.submit_order_commands.remove(&client_order_id);
113
114        vec![OrderManagerAction::CancelLocal(order.clone())]
115    }
116
117    /// Modifies the quantity of an existing order.
118    pub fn modify_order_quantity(
119        &mut self,
120        order: &OrderAny,
121        new_quantity: Quantity,
122    ) -> Vec<OrderManagerAction> {
123        vec![OrderManagerAction::ModifyLocalQuantity {
124            order: order.clone(),
125            quantity: new_quantity,
126        }]
127    }
128
129    /// # Errors
130    ///
131    /// Returns an error if creating a new submit order fails.
132    pub fn create_new_submit_order(
133        &mut self,
134        order: &OrderAny,
135        position_id: Option<PositionId>,
136        client_id: Option<ClientId>,
137        correlation_id: Option<UUID4>,
138    ) -> anyhow::Result<Vec<OrderManagerAction>> {
139        let mut actions = Vec::new();
140        let order_exists = self.cache.borrow().order_exists(&order.client_order_id());
141
142        self.cache
143            .borrow_mut()
144            .add_order(order.clone(), position_id, client_id, true)?;
145
146        if !order_exists {
147            actions.push(initialized_action(order));
148        }
149
150        let submit = SubmitOrder::new(
151            order.trader_id(),
152            client_id,
153            order.strategy_id(),
154            order.instrument_id(),
155            order.client_order_id(),
156            order.init_event().clone(),
157            order.exec_algorithm_id(),
158            position_id,
159            None, // params
160            UUID4::new(),
161            self.clock.borrow().timestamp_ns(),
162            correlation_id,
163        );
164
165        if order.emulation_trigger().is_some() {
166            self.cache_submit_order_command(submit.clone());
167            actions.push(OrderManagerAction::SubmitToEmulator(submit));
168        } else {
169            self.cache_submit_order_command(submit.clone());
170
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 position_id = self
317                    .cache
318                    .borrow()
319                    .position_id(&order.client_order_id())
320                    .copied();
321                let client_id = self
322                    .cache
323                    .borrow()
324                    .client_id(&order.client_order_id())
325                    .copied();
326
327                let parent_filled_qty = match order.exec_spawn_id() {
328                    Some(spawn_id) => {
329                        if let Some(qty) = self
330                            .cache
331                            .borrow()
332                            .exec_spawn_total_filled_qty(&spawn_id, false)
333                        {
334                            qty
335                        } else {
336                            log::error!("Failed to get spawn filled quantity for {spawn_id}");
337                            return actions;
338                        }
339                    }
340                    None => order.filled_qty(),
341                };
342
343                let Some(linked_orders) = order.linked_order_ids() else {
344                    log::error!("No linked orders found for OTO order");
345                    return actions;
346                };
347
348                for client_order_id in linked_orders {
349                    if order.parent_order_id().as_ref() == Some(client_order_id) {
350                        continue;
351                    }
352
353                    let Some(mut child_order) = self.cache.borrow().order_owned(client_order_id)
354                    else {
355                        log::error!(
356                            "Cannot find OTO child order for client_order_id: {client_order_id}"
357                        );
358                        continue;
359                    };
360
361                    if !self.should_manage_order(&child_order) || child_order.is_closed() {
362                        self.oto_target_quantities.remove(client_order_id);
363                        continue;
364                    }
365
366                    if self.active_local && child_order.position_id().is_none() {
367                        child_order.set_position_id(position_id);
368                    }
369
370                    actions.extend(self.sync_oto_quantity(&child_order, parent_filled_qty));
371
372                    if self.active_local
373                        && !self
374                            .submit_order_commands
375                            .contains_key(&child_order.client_order_id())
376                    {
377                        match self.create_new_submit_order(
378                            &child_order,
379                            position_id,
380                            client_id,
381                            None,
382                        ) {
383                            Ok(new_actions) => actions.extend(new_actions),
384                            Err(e) => log::error!("Failed to create new submit order: {e}"),
385                        }
386                    }
387                }
388            }
389            Some(ContingencyType::Oco) => {
390                let Some(linked_orders) = order.linked_order_ids() else {
391                    log::error!("No linked orders found for OCO order");
392                    return actions;
393                };
394
395                for client_order_id in linked_orders {
396                    let Some(contingent_order) = self.cache.borrow().order_owned(client_order_id)
397                    else {
398                        log::error!(
399                            "Cannot find OCO contingent order for client_order_id: {client_order_id}"
400                        );
401                        continue;
402                    };
403
404                    // Not being managed || Already completed
405                    if !self.should_manage_order(&contingent_order) || contingent_order.is_closed()
406                    {
407                        continue;
408                    }
409
410                    if contingent_order.client_order_id() != order.client_order_id() {
411                        actions.extend(self.cancel_order(&contingent_order));
412                    }
413                }
414            }
415            Some(ContingencyType::Ouo) => actions.extend(self.handle_contingencies(&order)),
416            _ => {}
417        }
418
419        actions
420    }
421
422    pub fn handle_contingencies(&mut self, order: &OrderAny) -> Vec<OrderManagerAction> {
423        let mut actions = Vec::new();
424        let (filled_qty, leaves_qty, is_spawn_active) =
425            if let Some(exec_spawn_id) = order.exec_spawn_id() {
426                if let (Some(filled), Some(leaves)) = (
427                    self.cache
428                        .borrow()
429                        .exec_spawn_total_filled_qty(&exec_spawn_id, false),
430                    self.cache
431                        .borrow()
432                        .exec_spawn_total_leaves_qty(&exec_spawn_id, true),
433                ) {
434                    (filled, leaves, leaves.raw > 0)
435                } else {
436                    log::error!("Failed to get spawn quantities for {exec_spawn_id}");
437                    return actions;
438                }
439            } else {
440                (order.filled_qty(), order.leaves_qty(), false)
441            };
442
443        let Some(linked_orders) = order.linked_order_ids() else {
444            log::error!("No linked orders found");
445            return actions;
446        };
447
448        for client_order_id in linked_orders {
449            if order.contingency_type() == Some(ContingencyType::Oto)
450                && order.parent_order_id().as_ref() == Some(client_order_id)
451            {
452                continue;
453            }
454
455            let Some(contingent_order) = self.cache.borrow().order_owned(client_order_id) else {
456                log::error!("Cannot find contingent order for client_order_id: {client_order_id}");
457                continue;
458            };
459
460            if !self.should_manage_order(&contingent_order) {
461                self.oto_target_quantities.remove(client_order_id);
462                continue;
463            }
464
465            if client_order_id == &order.client_order_id() {
466                continue;
467            }
468
469            if contingent_order.is_closed() {
470                self.submit_order_commands.remove(client_order_id);
471                self.oto_target_quantities.remove(client_order_id);
472                continue;
473            }
474
475            match order.contingency_type() {
476                Some(ContingencyType::Oto) => {
477                    if order.is_closed()
478                        && filled_qty.raw == 0
479                        && (order.exec_spawn_id().is_none() || !is_spawn_active)
480                    {
481                        actions.extend(self.cancel_order(&contingent_order));
482                    } else if filled_qty.raw > 0 {
483                        actions.extend(self.sync_oto_quantity(&contingent_order, filled_qty));
484                    }
485                }
486                Some(ContingencyType::Oco)
487                    if order.is_closed()
488                        && (order.exec_spawn_id().is_none() || !is_spawn_active) =>
489                {
490                    actions.extend(self.cancel_order(&contingent_order));
491                }
492                Some(ContingencyType::Ouo) => {
493                    if (leaves_qty.raw == 0 && order.exec_spawn_id().is_some())
494                        || (order.is_closed()
495                            && (order.exec_spawn_id().is_none() || !is_spawn_active))
496                        || contingent_order.filled_qty() >= leaves_qty
497                    {
498                        actions.extend(self.cancel_order(&contingent_order));
499                    } else if leaves_qty != contingent_order.leaves_qty() {
500                        actions.extend(self.modify_order_quantity(&contingent_order, leaves_qty));
501                    }
502                }
503                _ => {}
504            }
505        }
506
507        actions
508    }
509
510    pub fn handle_contingencies_update(&mut self, order: &OrderAny) -> Vec<OrderManagerAction> {
511        let mut actions = Vec::new();
512        let contingency_type = order.contingency_type();
513        let quantity = match order.exec_spawn_id() {
514            Some(exec_spawn_id) => {
515                if let Some(qty) = self
516                    .cache
517                    .borrow()
518                    .exec_spawn_total_quantity(&exec_spawn_id, true)
519                {
520                    qty
521                } else {
522                    log::error!("Failed to get spawn total quantity for {exec_spawn_id}");
523                    return actions;
524                }
525            }
526            None => order.quantity(),
527        };
528
529        if quantity.raw == 0 {
530            return actions;
531        }
532
533        let oto_quantity = if contingency_type == Some(ContingencyType::Oto) {
534            let filled_qty = match order.exec_spawn_id() {
535                Some(exec_spawn_id) => {
536                    if let Some(qty) = self
537                        .cache
538                        .borrow()
539                        .exec_spawn_total_filled_qty(&exec_spawn_id, false)
540                    {
541                        qty
542                    } else {
543                        log::error!("Failed to get spawn filled quantity for {exec_spawn_id}");
544                        return actions;
545                    }
546                }
547                None => order.filled_qty(),
548            };
549
550            if filled_qty.raw > 0 {
551                filled_qty
552            } else {
553                quantity
554            }
555        } else {
556            quantity
557        };
558
559        let Some(linked_orders) = order.linked_order_ids() else {
560            log::error!("No linked orders found for contingent order");
561            return actions;
562        };
563
564        for client_order_id in linked_orders {
565            if contingency_type == Some(ContingencyType::Oto)
566                && order.parent_order_id().as_ref() == Some(client_order_id)
567            {
568                continue;
569            }
570
571            let Some(contingent_order) = self.cache.borrow().order_owned(client_order_id) else {
572                log::error!(
573                    "Cannot find OCO contingent order for client_order_id: {client_order_id}"
574                );
575                continue;
576            };
577
578            if !self.should_manage_order(&contingent_order) {
579                self.oto_target_quantities.remove(client_order_id);
580                continue;
581            }
582
583            if client_order_id == &order.client_order_id() || contingent_order.is_closed() {
584                self.oto_target_quantities.remove(client_order_id);
585                continue;
586            }
587
588            match contingency_type {
589                Some(ContingencyType::Oto) => {
590                    actions.extend(self.sync_oto_quantity(&contingent_order, oto_quantity));
591                }
592                Some(ContingencyType::Ouo) if contingent_order.filled_qty() >= quantity => {
593                    actions.extend(self.cancel_order(&contingent_order));
594                }
595                Some(ContingencyType::Ouo) if quantity != contingent_order.quantity() => {
596                    actions.extend(self.modify_order_quantity(&contingent_order, quantity));
597                }
598                _ => {}
599            }
600        }
601
602        actions
603    }
604
605    fn sync_oto_quantity(
606        &mut self,
607        order: &OrderAny,
608        quantity: Quantity,
609    ) -> Vec<OrderManagerAction> {
610        let client_order_id = order.client_order_id();
611
612        if order.filled_qty() >= quantity {
613            self.oto_target_quantities.remove(&client_order_id);
614            return self.cancel_order(order);
615        }
616
617        match self.oto_target_quantities.get(&client_order_id).copied() {
618            Some(target) if target == quantity => return Vec::new(),
619            Some(_) if order.quantity() == quantity && !order.is_pending_update() => {
620                self.oto_target_quantities.insert(client_order_id, quantity);
621                return Vec::new();
622            }
623            Some(_) => {}
624            None if order.quantity() == quantity => return Vec::new(),
625            None => {}
626        }
627
628        self.oto_target_quantities.insert(client_order_id, quantity);
629        self.modify_order_quantity(order, quantity)
630    }
631}
632
633fn initialized_action(order: &OrderAny) -> OrderManagerAction {
634    let event = OrderEventAny::Initialized(order.init_event().clone());
635    OrderManagerAction::PublishInitialized(event)
636}
637
638#[cfg(test)]
639mod tests {
640    use std::{cell::RefCell, rc::Rc};
641
642    use nautilus_common::{cache::Cache, clock::TestClock};
643    use nautilus_core::{UUID4, UnixNanos};
644    use nautilus_model::{
645        enums::{ContingencyType, OrderSide, OrderType, TriggerType},
646        events::order::spec::{
647            OrderAcceptedSpec, OrderCanceledSpec, OrderExpiredSpec, OrderModifyRejectedSpec,
648            OrderPendingUpdateSpec, OrderRejectedSpec, OrderSubmittedSpec, OrderUpdatedSpec,
649        },
650        identifiers::{
651            AccountId, ClientOrderId, ExecAlgorithmId, InstrumentId, StrategyId, TradeId, TraderId,
652            VenueOrderId,
653        },
654        instruments::{Instrument, InstrumentAny, stubs::audusd_sim},
655        orders::{Order, OrderTestBuilder, stubs::TestOrderEventStubs},
656        types::{Price, Quantity},
657    };
658    use rstest::rstest;
659
660    use super::*;
661
662    /// Verifies unhandled order events are no-ops and don't panic.
663    /// Previously, unhandled events would hit a todo!() panic.
664    #[rstest]
665    fn test_handle_event_unhandled_events_are_noop() {
666        let submitted = OrderEventAny::Submitted(
667            OrderSubmittedSpec::builder()
668                .trader_id(TraderId::from("TRADER-001"))
669                .strategy_id(StrategyId::from("STRATEGY-001"))
670                .instrument_id(InstrumentId::from("BTC-USDT.OKX"))
671                .client_order_id(ClientOrderId::from("O-001"))
672                .account_id(AccountId::from("ACCOUNT-001"))
673                .build(),
674        );
675        let accepted = OrderEventAny::Accepted(
676            OrderAcceptedSpec::builder()
677                .trader_id(TraderId::from("TRADER-001"))
678                .strategy_id(StrategyId::from("STRATEGY-001"))
679                .instrument_id(InstrumentId::from("BTC-USDT.OKX"))
680                .client_order_id(ClientOrderId::from("O-001"))
681                .venue_order_id(VenueOrderId::from("V-001"))
682                .account_id(AccountId::from("ACCOUNT-001"))
683                .build(),
684        );
685
686        match submitted {
687            OrderEventAny::Rejected(_) => panic!("Should not match"),
688            OrderEventAny::Canceled(_) => panic!("Should not match"),
689            OrderEventAny::Expired(_) => panic!("Should not match"),
690            OrderEventAny::Updated(_) => panic!("Should not match"),
691            OrderEventAny::Filled(_) => panic!("Should not match"),
692            _ => {}
693        }
694
695        match accepted {
696            OrderEventAny::Rejected(_) => panic!("Should not match"),
697            OrderEventAny::Canceled(_) => panic!("Should not match"),
698            OrderEventAny::Expired(_) => panic!("Should not match"),
699            OrderEventAny::Updated(_) => panic!("Should not match"),
700            OrderEventAny::Filled(_) => panic!("Should not match"),
701            _ => {}
702        }
703    }
704
705    #[rstest]
706    fn test_actionable_events_for_missing_orders_are_noops() {
707        let (clock, cache) = create_test_components();
708        let mut manager = OrderManager::new(clock, cache, false);
709        let trader_id = TraderId::from("TRADER-001");
710        let strategy_id = StrategyId::from("STRATEGY-001");
711        let instrument_id = InstrumentId::from("BTC-USDT.OKX");
712        let client_order_id = ClientOrderId::from("O-MISSING");
713        let account_id = AccountId::from("ACCOUNT-001");
714        let events = [
715            OrderEventAny::Rejected(
716                OrderRejectedSpec::builder()
717                    .trader_id(trader_id)
718                    .strategy_id(strategy_id)
719                    .instrument_id(instrument_id)
720                    .client_order_id(client_order_id)
721                    .account_id(account_id)
722                    .reason("test rejection".into())
723                    .build(),
724            ),
725            OrderEventAny::Canceled(
726                OrderCanceledSpec::builder()
727                    .trader_id(trader_id)
728                    .strategy_id(strategy_id)
729                    .instrument_id(instrument_id)
730                    .client_order_id(client_order_id)
731                    .account_id(account_id)
732                    .build(),
733            ),
734            OrderEventAny::Expired(
735                OrderExpiredSpec::builder()
736                    .trader_id(trader_id)
737                    .strategy_id(strategy_id)
738                    .instrument_id(instrument_id)
739                    .client_order_id(client_order_id)
740                    .account_id(account_id)
741                    .build(),
742            ),
743            OrderEventAny::Updated(
744                OrderUpdatedSpec::builder()
745                    .trader_id(trader_id)
746                    .strategy_id(strategy_id)
747                    .instrument_id(instrument_id)
748                    .client_order_id(client_order_id)
749                    .quantity(Quantity::from(100_000))
750                    .venue_order_id(VenueOrderId::from("V-MISSING"))
751                    .account_id(account_id)
752                    .build(),
753            ),
754        ];
755
756        for event in events {
757            assert!(manager.handle_event(&event).is_empty());
758        }
759    }
760
761    fn create_test_components() -> (Rc<RefCell<dyn Clock>>, Rc<RefCell<Cache>>) {
762        let clock: Rc<RefCell<dyn Clock>> = Rc::new(RefCell::new(TestClock::new()));
763        let cache = Rc::new(RefCell::new(Cache::new(None, None)));
764        (clock, cache)
765    }
766
767    fn create_test_stop_order() -> OrderAny {
768        let instrument = audusd_sim();
769        OrderTestBuilder::new(OrderType::StopMarket)
770            .instrument_id(instrument.id())
771            .side(OrderSide::Buy)
772            .trigger_price(Price::from("1.00050"))
773            .quantity(Quantity::from(100_000))
774            .emulation_trigger(TriggerType::BidAsk)
775            .build()
776    }
777
778    // Creates a `SubmitOrder` command suitable for seeding `submit_order_commands`
779    // so that whether `cancel_order` removed the entry can be observed.
780    fn make_submit_command(order: &OrderAny) -> SubmitOrder {
781        SubmitOrder::new(
782            order.trader_id(),
783            None,
784            order.strategy_id(),
785            order.instrument_id(),
786            order.client_order_id(),
787            order.init_event().clone(),
788            None,
789            None,
790            None,
791            UUID4::new(),
792            UnixNanos::default(),
793            None, // correlation_id
794        )
795    }
796
797    #[rstest]
798    fn test_create_new_submit_order_returns_emulator_submit_action() {
799        let (clock, cache) = create_test_components();
800        let mut manager = OrderManager::new(clock, cache, true);
801        let order = create_test_stop_order();
802
803        let actions = manager
804            .create_new_submit_order(&order, None, None, None)
805            .unwrap();
806
807        assert_eq!(actions.len(), 2);
808        assert!(matches!(
809            &actions[0],
810            OrderManagerAction::PublishInitialized(OrderEventAny::Initialized(event))
811                if event.client_order_id == order.client_order_id()
812        ));
813        assert!(matches!(
814            &actions[1],
815            OrderManagerAction::SubmitToEmulator(command)
816                if command.client_order_id == order.client_order_id()
817        ));
818        assert!(
819            manager
820                .submit_order_commands
821                .contains_key(&order.client_order_id())
822        );
823    }
824
825    #[rstest]
826    fn test_reset_clears_submit_commands_and_oto_targets() {
827        let (clock, cache) = create_test_components();
828        let mut manager = OrderManager::new(clock, cache, true);
829        let order = create_test_stop_order();
830        let client_order_id = order.client_order_id();
831        manager.cache_submit_order_command(make_submit_command(&order));
832        manager
833            .oto_target_quantities
834            .insert(client_order_id, Quantity::from(40_000));
835
836        manager.reset();
837
838        assert!(manager.submit_order_commands.is_empty());
839        assert!(manager.oto_target_quantities.is_empty());
840    }
841
842    #[rstest]
843    fn test_create_new_submit_order_returns_risk_submit_action() {
844        let (clock, cache) = create_test_components();
845        let mut manager = OrderManager::new(clock, cache, true);
846        let order = OrderTestBuilder::new(OrderType::Limit)
847            .instrument_id(audusd_sim().id())
848            .side(OrderSide::Buy)
849            .price(Price::from("1.00000"))
850            .quantity(Quantity::from(100_000))
851            .build();
852
853        let actions = manager
854            .create_new_submit_order(&order, None, None, None)
855            .unwrap();
856
857        assert_eq!(actions.len(), 2);
858        assert!(matches!(
859            &actions[0],
860            OrderManagerAction::PublishInitialized(OrderEventAny::Initialized(event))
861                if event.client_order_id == order.client_order_id()
862        ));
863        assert!(matches!(
864            &actions[1],
865            OrderManagerAction::SubmitToRisk(command)
866                if command.client_order_id == order.client_order_id()
867        ));
868    }
869
870    #[rstest]
871    fn test_create_new_submit_order_returns_risk_action_for_none_trigger() {
872        let (clock, cache) = create_test_components();
873        let mut manager = OrderManager::new(clock, cache, true);
874        let order = OrderTestBuilder::new(OrderType::Market)
875            .instrument_id(audusd_sim().id())
876            .side(OrderSide::Buy)
877            .quantity(Quantity::from(100_000))
878            .build();
879
880        let actions = manager
881            .create_new_submit_order(&order, None, None, None)
882            .unwrap();
883
884        assert_eq!(actions.len(), 2);
885        assert!(order.emulation_trigger().is_none());
886        assert!(matches!(
887            &actions[1],
888            OrderManagerAction::SubmitToRisk(command)
889                if command.client_order_id == order.client_order_id()
890        ));
891    }
892
893    #[rstest]
894    fn test_create_new_submit_order_returns_algorithm_submit_action() {
895        let (clock, cache) = create_test_components();
896        let mut manager = OrderManager::new(clock, cache, true);
897        let exec_algorithm_id = ExecAlgorithmId::from("ALG-001");
898        let client_order_id = ClientOrderId::from("O-001");
899        let order = OrderTestBuilder::new(OrderType::Limit)
900            .client_order_id(client_order_id)
901            .instrument_id(audusd_sim().id())
902            .side(OrderSide::Buy)
903            .price(Price::from("1.00000"))
904            .quantity(Quantity::from(100_000))
905            .exec_algorithm_id(exec_algorithm_id)
906            .exec_spawn_id(client_order_id)
907            .build();
908
909        let actions = manager
910            .create_new_submit_order(&order, None, None, None)
911            .unwrap();
912
913        assert_eq!(actions.len(), 2);
914        assert!(matches!(
915            &actions[1],
916            OrderManagerAction::SubmitToAlgorithm {
917                command,
918                exec_algorithm_id: action_exec_algorithm_id,
919            } if command.client_order_id == order.client_order_id()
920                && *action_exec_algorithm_id == exec_algorithm_id
921        ));
922    }
923
924    #[rstest]
925    fn test_create_new_submit_order_does_not_republish_initialized_for_existing_order() {
926        let (clock, cache) = create_test_components();
927        let mut manager = OrderManager::new(clock, cache.clone(), true);
928        let order = create_test_stop_order();
929        cache
930            .borrow_mut()
931            .add_order(order.clone(), None, None, true)
932            .unwrap();
933
934        let actions = manager
935            .create_new_submit_order(&order, None, None, None)
936            .unwrap();
937
938        assert_eq!(actions.len(), 1);
939        assert!(matches!(
940            &actions[0],
941            OrderManagerAction::SubmitToEmulator(command)
942                if command.client_order_id == order.client_order_id()
943        ));
944    }
945
946    #[rstest]
947    fn test_cancel_order_returns_cancel_local_action() {
948        let (clock, cache) = create_test_components();
949        let mut manager = OrderManager::new(clock, cache.clone(), true);
950        let order = create_test_stop_order();
951        cache
952            .borrow_mut()
953            .add_order(order.clone(), None, None, false)
954            .unwrap();
955        manager
956            .submit_order_commands
957            .insert(order.client_order_id(), make_submit_command(&order));
958
959        let actions = manager.cancel_order(&order);
960
961        assert!(matches!(
962            actions.as_slice(),
963            [OrderManagerAction::CancelLocal(action_order)]
964                if action_order.client_order_id() == order.client_order_id()
965        ));
966        assert!(
967            !manager
968                .submit_order_commands
969                .contains_key(&order.client_order_id()),
970            "expected cancel action path to remove the submit command",
971        );
972    }
973
974    #[rstest]
975    fn test_modify_order_quantity_returns_modify_local_action() {
976        let (clock, cache) = create_test_components();
977        let mut manager = OrderManager::new(clock, cache, true);
978        let order = create_test_stop_order();
979        let new_quantity = Quantity::from(50_000);
980
981        let actions = manager.modify_order_quantity(&order, new_quantity);
982
983        assert!(matches!(
984            actions.as_slice(),
985            [OrderManagerAction::ModifyLocalQuantity { order: action_order, quantity }]
986                if action_order.client_order_id() == order.client_order_id()
987                    && *quantity == new_quantity
988        ));
989    }
990
991    #[rstest]
992    fn test_sync_oto_quantity_reasserts_same_quantity_while_pending_update() {
993        let (clock, cache) = create_test_components();
994        let mut manager = OrderManager::new(clock, cache.clone(), false);
995        let client_order_id = ClientOrderId::from("O-CHILD");
996        let order = OrderTestBuilder::new(OrderType::Limit)
997            .instrument_id(audusd_sim().id())
998            .client_order_id(client_order_id)
999            .side(OrderSide::Sell)
1000            .price(Price::from("1.00100"))
1001            .quantity(Quantity::from(100_000))
1002            .submit(true)
1003            .build();
1004        cache
1005            .borrow_mut()
1006            .add_order(order.clone(), None, None, false)
1007            .unwrap();
1008        apply_accepted(&cache, &order, "V-CHILD");
1009        let pending_update = OrderEventAny::PendingUpdate(
1010            OrderPendingUpdateSpec::builder()
1011                .trader_id(order.trader_id())
1012                .strategy_id(order.strategy_id())
1013                .instrument_id(order.instrument_id())
1014                .client_order_id(client_order_id)
1015                .account_id(AccountId::from("ACCOUNT-001"))
1016                .venue_order_id(VenueOrderId::from("V-CHILD"))
1017                .build(),
1018        );
1019        cache.borrow_mut().update_order(&pending_update).unwrap();
1020        manager
1021            .oto_target_quantities
1022            .insert(client_order_id, Quantity::from(40_000));
1023        let cached_order = cache.borrow().order_owned(&client_order_id).unwrap();
1024
1025        let actions = manager.sync_oto_quantity(&cached_order, Quantity::from(100_000));
1026
1027        assert!(matches!(
1028            actions.as_slice(),
1029            [OrderManagerAction::ModifyLocalQuantity { order, quantity }]
1030                if order.client_order_id() == client_order_id
1031                    && *quantity == Quantity::from(100_000)
1032        ));
1033        assert_eq!(
1034            manager.oto_target_quantities.get(&client_order_id),
1035            Some(&Quantity::from(100_000)),
1036        );
1037    }
1038
1039    #[rstest]
1040    fn test_handle_event_unhandled_events_return_no_actions() {
1041        let (clock, cache) = create_test_components();
1042        let mut manager = OrderManager::new(clock, cache, true);
1043        let order = create_test_stop_order();
1044        let event = OrderEventAny::Submitted(
1045            OrderSubmittedSpec::builder()
1046                .trader_id(order.trader_id())
1047                .strategy_id(order.strategy_id())
1048                .instrument_id(order.instrument_id())
1049                .client_order_id(order.client_order_id())
1050                .account_id(AccountId::from("ACCOUNT-001"))
1051                .build(),
1052        );
1053
1054        let actions = manager.handle_event(&event);
1055
1056        assert!(actions.is_empty());
1057    }
1058
1059    #[rstest]
1060    fn test_handle_order_filled_skips_missing_oco_contingent_order() {
1061        let (clock, cache) = create_test_components();
1062        let mut manager = OrderManager::new(clock, cache.clone(), true);
1063        let instrument = InstrumentAny::CurrencyPair(audusd_sim());
1064        let missing_client_order_id = ClientOrderId::from("O-MISSING");
1065        let valid_client_order_id = ClientOrderId::from("O-CHILD");
1066        let order = OrderTestBuilder::new(OrderType::Limit)
1067            .instrument_id(instrument.id())
1068            .client_order_id(ClientOrderId::from("O-PARENT"))
1069            .side(OrderSide::Buy)
1070            .price(Price::from("1.00000"))
1071            .quantity(Quantity::from(100_000))
1072            .contingency_type(ContingencyType::Oco)
1073            .linked_order_ids(vec![missing_client_order_id, valid_client_order_id])
1074            .build();
1075        let child_order = OrderTestBuilder::new(OrderType::Limit)
1076            .instrument_id(instrument.id())
1077            .client_order_id(valid_client_order_id)
1078            .side(OrderSide::Buy)
1079            .price(Price::from("1.00000"))
1080            .quantity(Quantity::from(100_000))
1081            .build();
1082        cache
1083            .borrow_mut()
1084            .add_order(order.clone(), None, None, false)
1085            .unwrap();
1086        cache
1087            .borrow_mut()
1088            .add_order(child_order.clone(), None, None, false)
1089            .unwrap();
1090        manager
1091            .submit_order_commands
1092            .insert(valid_client_order_id, make_submit_command(&child_order));
1093        let filled = match TestOrderEventStubs::filled(
1094            &order,
1095            &instrument,
1096            None,
1097            None,
1098            None,
1099            None,
1100            None,
1101            None,
1102            None,
1103            Some(AccountId::from("SIM-001")),
1104        ) {
1105            OrderEventAny::Filled(event) => event,
1106            event => panic!("expected OrderFilled, was {event:?}"),
1107        };
1108
1109        let actions = manager.handle_order_filled(&filled);
1110
1111        assert!(matches!(
1112            actions.as_slice(),
1113            [OrderManagerAction::CancelLocal(action_order)]
1114                if action_order.client_order_id() == valid_client_order_id
1115        ));
1116        assert!(
1117            !manager
1118                .submit_order_commands
1119                .contains_key(&valid_client_order_id)
1120        );
1121    }
1122
1123    #[rstest]
1124    fn test_handle_event_non_active_local_manager_returns_oco_cancel_action() {
1125        let (clock, cache) = create_test_components();
1126        let mut manager = OrderManager::new(clock, cache.clone(), false);
1127        let instrument = InstrumentAny::CurrencyPair(audusd_sim());
1128        let parent_id = ClientOrderId::from("O-PARENT");
1129        let child_id = ClientOrderId::from("O-CHILD");
1130        let order = OrderTestBuilder::new(OrderType::Limit)
1131            .instrument_id(instrument.id())
1132            .client_order_id(parent_id)
1133            .side(OrderSide::Buy)
1134            .price(Price::from("1.00000"))
1135            .quantity(Quantity::from(100_000))
1136            .contingency_type(ContingencyType::Oco)
1137            .linked_order_ids(vec![parent_id, child_id])
1138            .submit(true)
1139            .build();
1140        let child_order = OrderTestBuilder::new(OrderType::Limit)
1141            .instrument_id(instrument.id())
1142            .client_order_id(child_id)
1143            .side(OrderSide::Buy)
1144            .price(Price::from("1.00000"))
1145            .quantity(Quantity::from(100_000))
1146            .submit(true)
1147            .build();
1148        cache
1149            .borrow_mut()
1150            .add_order(order.clone(), None, None, false)
1151            .unwrap();
1152        cache
1153            .borrow_mut()
1154            .add_order(child_order.clone(), None, None, false)
1155            .unwrap();
1156        manager
1157            .submit_order_commands
1158            .insert(child_id, make_submit_command(&child_order));
1159        let event = TestOrderEventStubs::filled(
1160            &order,
1161            &instrument,
1162            None,
1163            None,
1164            None,
1165            None,
1166            None,
1167            None,
1168            None,
1169            Some(AccountId::from("SIM-001")),
1170        );
1171
1172        let actions = manager.handle_event(&event);
1173
1174        assert!(matches!(
1175            actions.as_slice(),
1176            [OrderManagerAction::CancelLocal(order)]
1177                if order.client_order_id() == child_id
1178        ));
1179        assert!(
1180            !manager.submit_order_commands.contains_key(&child_id),
1181            "cancel action must remove the cached submit command",
1182        );
1183    }
1184
1185    #[rstest]
1186    fn test_handle_order_filled_skips_missing_oto_child_order() {
1187        let (clock, cache) = create_test_components();
1188        let mut manager = OrderManager::new(clock, cache.clone(), true);
1189        let instrument = InstrumentAny::CurrencyPair(audusd_sim());
1190        let missing_client_order_id = ClientOrderId::from("O-MISSING");
1191        let valid_client_order_id = ClientOrderId::from("O-CHILD");
1192        let order = OrderTestBuilder::new(OrderType::Limit)
1193            .instrument_id(instrument.id())
1194            .client_order_id(ClientOrderId::from("O-PARENT"))
1195            .side(OrderSide::Buy)
1196            .price(Price::from("1.00000"))
1197            .quantity(Quantity::from(100_000))
1198            .contingency_type(ContingencyType::Oto)
1199            .linked_order_ids(vec![missing_client_order_id, valid_client_order_id])
1200            .submit(true)
1201            .build();
1202        let child_order = OrderTestBuilder::new(OrderType::Limit)
1203            .instrument_id(instrument.id())
1204            .client_order_id(valid_client_order_id)
1205            .side(OrderSide::Buy)
1206            .price(Price::from("1.00000"))
1207            .quantity(Quantity::from(100_000))
1208            .build();
1209        cache
1210            .borrow_mut()
1211            .add_order(order.clone(), None, None, false)
1212            .unwrap();
1213        cache
1214            .borrow_mut()
1215            .add_order(child_order, None, None, false)
1216            .unwrap();
1217        let filled = match apply_fill(
1218            &cache,
1219            &order,
1220            &instrument,
1221            "T-PARTIAL",
1222            Quantity::from(40_000),
1223        ) {
1224            OrderEventAny::Filled(event) => event,
1225            event => panic!("expected OrderFilled, was {event:?}"),
1226        };
1227
1228        let actions = manager.handle_order_filled(&filled);
1229
1230        assert_eq!(actions.len(), 2);
1231        assert!(matches!(
1232            &actions[0],
1233            OrderManagerAction::ModifyLocalQuantity { order, quantity }
1234                if order.client_order_id() == valid_client_order_id
1235                    && *quantity == Quantity::from(40_000)
1236        ));
1237        assert!(matches!(
1238            &actions[1],
1239            OrderManagerAction::SubmitToRisk(command)
1240                if command.client_order_id == valid_client_order_id
1241        ));
1242        assert!(
1243            manager
1244                .submit_order_commands
1245                .contains_key(&valid_client_order_id)
1246        );
1247    }
1248
1249    #[rstest]
1250    fn test_handle_contingencies_skips_missing_linked_order() {
1251        let (clock, cache) = create_test_components();
1252        let mut manager = OrderManager::new(clock, cache, true);
1253        let instrument = audusd_sim();
1254        let order = OrderTestBuilder::new(OrderType::Limit)
1255            .instrument_id(instrument.id())
1256            .client_order_id(ClientOrderId::from("O-PARENT"))
1257            .side(OrderSide::Buy)
1258            .price(Price::from("1.00000"))
1259            .quantity(Quantity::from(100_000))
1260            .contingency_type(ContingencyType::Oco)
1261            .linked_order_ids(vec![ClientOrderId::from("O-MISSING")])
1262            .build();
1263
1264        let actions = manager.handle_contingencies(&order);
1265
1266        assert!(actions.is_empty());
1267        assert!(manager.submit_order_commands.is_empty());
1268    }
1269
1270    #[rstest]
1271    fn test_handle_contingencies_update_skips_missing_linked_order() {
1272        let (clock, cache) = create_test_components();
1273        let mut manager = OrderManager::new(clock, cache, true);
1274        let instrument = audusd_sim();
1275        let order = OrderTestBuilder::new(OrderType::Limit)
1276            .instrument_id(instrument.id())
1277            .client_order_id(ClientOrderId::from("O-PARENT"))
1278            .side(OrderSide::Buy)
1279            .price(Price::from("1.00000"))
1280            .quantity(Quantity::from(100_000))
1281            .contingency_type(ContingencyType::Oco)
1282            .linked_order_ids(vec![ClientOrderId::from("O-MISSING")])
1283            .build();
1284
1285        let actions = manager.handle_contingencies_update(&order);
1286
1287        assert!(actions.is_empty());
1288        assert!(manager.submit_order_commands.is_empty());
1289    }
1290
1291    #[rstest]
1292    fn test_oto_child_contingency_handlers_do_not_drive_parent() {
1293        let (clock, cache) = create_test_components();
1294        let mut terminal_manager = OrderManager::new(clock.clone(), cache.clone(), false);
1295        let mut update_manager = OrderManager::new(clock, cache.clone(), false);
1296        let instrument = InstrumentAny::CurrencyPair(audusd_sim());
1297        let parent_id = ClientOrderId::from("O-PARENT");
1298        let child_id = ClientOrderId::from("O-CHILD");
1299        let parent = OrderTestBuilder::new(OrderType::Limit)
1300            .instrument_id(instrument.id())
1301            .client_order_id(parent_id)
1302            .side(OrderSide::Buy)
1303            .price(Price::from("1.00000"))
1304            .quantity(Quantity::from(100_000))
1305            .contingency_type(ContingencyType::Oto)
1306            .linked_order_ids(vec![child_id])
1307            .submit(true)
1308            .build();
1309        let child = OrderTestBuilder::new(OrderType::Limit)
1310            .instrument_id(instrument.id())
1311            .client_order_id(child_id)
1312            .side(OrderSide::Sell)
1313            .price(Price::from("1.00100"))
1314            .quantity(Quantity::from(100_000))
1315            .contingency_type(ContingencyType::Oto)
1316            .linked_order_ids(vec![parent_id])
1317            .parent_order_id(parent_id)
1318            .submit(true)
1319            .build();
1320        cache
1321            .borrow_mut()
1322            .add_order(parent.clone(), None, None, false)
1323            .unwrap();
1324        cache
1325            .borrow_mut()
1326            .add_order(child.clone(), None, None, false)
1327            .unwrap();
1328        apply_accepted(&cache, &parent, "V-PARENT");
1329        apply_accepted(&cache, &child, "V-CHILD");
1330        apply_fill(
1331            &cache,
1332            &child,
1333            &instrument,
1334            "T-CHILD",
1335            Quantity::from(40_000),
1336        );
1337        let cached_child = cache.borrow().order_owned(&child_id).unwrap();
1338
1339        let terminal_actions = terminal_manager.handle_contingencies(&cached_child);
1340        let update_actions = update_manager.handle_contingencies_update(&cached_child);
1341
1342        assert!(terminal_actions.is_empty());
1343        assert!(update_actions.is_empty());
1344        assert!(terminal_manager.oto_target_quantities.is_empty());
1345        assert!(update_manager.oto_target_quantities.is_empty());
1346    }
1347
1348    #[rstest]
1349    fn test_oto_chained_child_drives_its_child_contingencies() {
1350        let (clock, cache) = create_test_components();
1351        let mut terminal_manager = OrderManager::new(clock.clone(), cache.clone(), false);
1352        let mut update_manager = OrderManager::new(clock, cache.clone(), false);
1353        let instrument = InstrumentAny::CurrencyPair(audusd_sim());
1354        let root_id = ClientOrderId::from("O-ROOT");
1355        let parent_id = ClientOrderId::from("O-PARENT");
1356        let child_id = ClientOrderId::from("O-CHILD");
1357        let parent = OrderTestBuilder::new(OrderType::Limit)
1358            .instrument_id(instrument.id())
1359            .client_order_id(parent_id)
1360            .side(OrderSide::Buy)
1361            .price(Price::from("1.00000"))
1362            .quantity(Quantity::from(120_000))
1363            .contingency_type(ContingencyType::Oto)
1364            .linked_order_ids(vec![child_id])
1365            .parent_order_id(root_id)
1366            .submit(true)
1367            .build();
1368        let child = OrderTestBuilder::new(OrderType::Limit)
1369            .instrument_id(instrument.id())
1370            .client_order_id(child_id)
1371            .side(OrderSide::Sell)
1372            .price(Price::from("1.00100"))
1373            .quantity(Quantity::from(100_000))
1374            .parent_order_id(parent_id)
1375            .submit(true)
1376            .build();
1377        cache
1378            .borrow_mut()
1379            .add_order(parent.clone(), None, None, false)
1380            .unwrap();
1381        cache
1382            .borrow_mut()
1383            .add_order(child.clone(), None, None, false)
1384            .unwrap();
1385        apply_accepted(&cache, &parent, "V-PARENT");
1386        apply_accepted(&cache, &child, "V-CHILD");
1387        let cached_parent = cache.borrow().order_owned(&parent_id).unwrap();
1388
1389        let update_actions = update_manager.handle_contingencies_update(&cached_parent);
1390        let terminal = apply_terminal(&cache, &cached_parent, TerminalEvent::Canceled);
1391        let terminal_actions = terminal_manager.handle_event(&terminal);
1392
1393        assert!(matches!(
1394            update_actions.as_slice(),
1395            [OrderManagerAction::ModifyLocalQuantity { order, quantity }]
1396                if order.client_order_id() == child_id
1397                    && *quantity == Quantity::from(120_000)
1398        ));
1399        assert!(matches!(
1400            terminal_actions.as_slice(),
1401            [OrderManagerAction::CancelLocal(order)]
1402                if order.client_order_id() == child_id
1403        ));
1404    }
1405
1406    #[rstest]
1407    fn test_cancel_order_skips_when_pending_cancel_local() {
1408        let (clock, cache) = create_test_components();
1409        let mut manager = OrderManager::new(clock, cache.clone(), true);
1410        let order = create_test_stop_order();
1411        cache
1412            .borrow_mut()
1413            .add_order(order.clone(), None, None, false)
1414            .unwrap();
1415        cache.borrow_mut().update_order_pending_cancel_local(&order);
1416        manager
1417            .submit_order_commands
1418            .insert(order.client_order_id(), make_submit_command(&order));
1419
1420        manager.cancel_order(&order);
1421
1422        assert!(
1423            manager
1424                .submit_order_commands
1425                .contains_key(&order.client_order_id()),
1426            "pending-cancel-local gate should short-circuit before removing the submit command",
1427        );
1428    }
1429
1430    #[rstest]
1431    fn test_cancel_order_skips_when_passed_order_is_closed() {
1432        // The caller has applied a closing event to its local clone but has
1433        // not yet called `cache.update_order`, so the cache index still
1434        // reports open. The gate must short-circuit on the local state.
1435        let (clock, cache) = create_test_components();
1436        let mut manager = OrderManager::new(clock, cache.clone(), true);
1437
1438        let mut order = OrderTestBuilder::new(OrderType::StopMarket)
1439            .instrument_id(audusd_sim().id())
1440            .side(OrderSide::Buy)
1441            .trigger_price(Price::from("1.00050"))
1442            .quantity(Quantity::from(100_000))
1443            .emulation_trigger(TriggerType::BidAsk)
1444            .submit(true)
1445            .build();
1446
1447        cache
1448            .borrow_mut()
1449            .add_order(order.clone(), None, None, false)
1450            .unwrap();
1451
1452        let canceled_event =
1453            TestOrderEventStubs::canceled(&order, AccountId::from("ACCOUNT-001"), None);
1454        order.apply(canceled_event).unwrap();
1455
1456        assert!(order.is_closed());
1457        assert!(!cache.borrow().is_order_closed(&order.client_order_id()));
1458
1459        manager
1460            .submit_order_commands
1461            .insert(order.client_order_id(), make_submit_command(&order));
1462
1463        manager.cancel_order(&order);
1464
1465        assert!(
1466            manager
1467                .submit_order_commands
1468                .contains_key(&order.client_order_id()),
1469            "closed-order gate should short-circuit on the local state when the cache index is stale",
1470        );
1471    }
1472
1473    #[rstest]
1474    fn test_cancel_order_skips_when_cache_index_marks_closed() {
1475        // The passed `OrderAny` is intentionally a stale (Submitted) clone so
1476        // this test would fail if `cancel_order` checked `order.is_closed()`
1477        // on the argument instead of `cache.is_order_closed(&id)`.
1478        let (clock, cache) = create_test_components();
1479        let mut manager = OrderManager::new(clock, cache.clone(), true);
1480
1481        let mut order = OrderTestBuilder::new(OrderType::StopMarket)
1482            .instrument_id(audusd_sim().id())
1483            .side(OrderSide::Buy)
1484            .trigger_price(Price::from("1.00050"))
1485            .quantity(Quantity::from(100_000))
1486            .emulation_trigger(TriggerType::BidAsk)
1487            .submit(true)
1488            .build();
1489
1490        cache
1491            .borrow_mut()
1492            .add_order(order.clone(), None, None, false)
1493            .unwrap();
1494
1495        let stale_order = order.clone();
1496
1497        let canceled_event =
1498            TestOrderEventStubs::canceled(&order, AccountId::from("ACCOUNT-001"), None);
1499        order = cache.borrow_mut().update_order(&canceled_event).unwrap();
1500
1501        assert!(cache.borrow().is_order_closed(&order.client_order_id()));
1502
1503        manager.submit_order_commands.insert(
1504            stale_order.client_order_id(),
1505            make_submit_command(&stale_order),
1506        );
1507
1508        manager.cancel_order(&stale_order);
1509
1510        assert!(
1511            manager
1512                .submit_order_commands
1513                .contains_key(&stale_order.client_order_id()),
1514            "closed-order gate should short-circuit even when the passed reference is stale",
1515        );
1516    }
1517
1518    #[rstest]
1519    fn test_handle_contingencies_update_syncs_quantity_for_ouo_sibling() {
1520        let (clock, cache) = create_test_components();
1521        let mut manager = OrderManager::new(clock, cache.clone(), true);
1522        let instrument = audusd_sim();
1523        let child = OrderTestBuilder::new(OrderType::Limit)
1524            .instrument_id(instrument.id())
1525            .client_order_id(ClientOrderId::from("O-CHILD"))
1526            .side(OrderSide::Sell)
1527            .price(Price::from("1.00100"))
1528            .quantity(Quantity::from(50_000))
1529            .build();
1530        cache
1531            .borrow_mut()
1532            .add_order(child.clone(), None, None, false)
1533            .unwrap();
1534        let parent = OrderTestBuilder::new(OrderType::Limit)
1535            .instrument_id(instrument.id())
1536            .client_order_id(ClientOrderId::from("O-PARENT"))
1537            .side(OrderSide::Buy)
1538            .price(Price::from("1.00000"))
1539            .quantity(Quantity::from(100_000))
1540            .contingency_type(ContingencyType::Ouo)
1541            .linked_order_ids(vec![child.client_order_id()])
1542            .build();
1543
1544        let actions = manager.handle_contingencies_update(&parent);
1545
1546        assert_eq!(actions.len(), 1);
1547        assert!(matches!(
1548            &actions[0],
1549            OrderManagerAction::ModifyLocalQuantity { order, quantity }
1550                if order.client_order_id() == child.client_order_id()
1551                    && *quantity == Quantity::from(100_000)
1552        ));
1553    }
1554
1555    #[rstest]
1556    fn test_handle_contingencies_update_syncs_oto_child_before_parent_fill() {
1557        let (clock, cache) = create_test_components();
1558        let mut manager = OrderManager::new(clock, cache.clone(), false);
1559        let instrument = audusd_sim();
1560        let child = OrderTestBuilder::new(OrderType::Limit)
1561            .instrument_id(instrument.id())
1562            .client_order_id(ClientOrderId::from("O-CHILD"))
1563            .side(OrderSide::Sell)
1564            .price(Price::from("1.00100"))
1565            .quantity(Quantity::from(100_000))
1566            .submit(true)
1567            .build();
1568        cache
1569            .borrow_mut()
1570            .add_order(child.clone(), None, None, false)
1571            .unwrap();
1572        let parent = OrderTestBuilder::new(OrderType::Limit)
1573            .instrument_id(instrument.id())
1574            .client_order_id(ClientOrderId::from("O-PARENT"))
1575            .side(OrderSide::Buy)
1576            .price(Price::from("1.00000"))
1577            .quantity(Quantity::from(120_000))
1578            .contingency_type(ContingencyType::Oto)
1579            .linked_order_ids(vec![child.client_order_id()])
1580            .submit(true)
1581            .build();
1582
1583        let actions = manager.handle_contingencies_update(&parent);
1584
1585        assert!(matches!(
1586            actions.as_slice(),
1587            [OrderManagerAction::ModifyLocalQuantity { order, quantity }]
1588                if order.client_order_id() == child.client_order_id()
1589                    && *quantity == Quantity::from(120_000)
1590        ));
1591        assert_eq!(
1592            manager.oto_target_quantities.get(&child.client_order_id()),
1593            Some(&Quantity::from(120_000)),
1594        );
1595    }
1596
1597    #[rstest]
1598    fn test_handle_contingencies_update_clears_target_for_closed_oto_child() {
1599        let (clock, cache) = create_test_components();
1600        let mut manager = OrderManager::new(clock, cache.clone(), false);
1601        let instrument = audusd_sim();
1602        let child = OrderTestBuilder::new(OrderType::Limit)
1603            .instrument_id(instrument.id())
1604            .client_order_id(ClientOrderId::from("O-CHILD"))
1605            .side(OrderSide::Sell)
1606            .price(Price::from("1.00100"))
1607            .quantity(Quantity::from(100_000))
1608            .submit(true)
1609            .build();
1610        cache
1611            .borrow_mut()
1612            .add_order(child.clone(), None, None, false)
1613            .unwrap();
1614        apply_terminal(&cache, &child, TerminalEvent::Canceled);
1615        manager
1616            .oto_target_quantities
1617            .insert(child.client_order_id(), Quantity::from(40_000));
1618        let parent = OrderTestBuilder::new(OrderType::Limit)
1619            .instrument_id(instrument.id())
1620            .client_order_id(ClientOrderId::from("O-PARENT"))
1621            .side(OrderSide::Buy)
1622            .price(Price::from("1.00000"))
1623            .quantity(Quantity::from(100_000))
1624            .contingency_type(ContingencyType::Oto)
1625            .linked_order_ids(vec![child.client_order_id()])
1626            .submit(true)
1627            .build();
1628
1629        let actions = manager.handle_contingencies_update(&parent);
1630
1631        assert!(actions.is_empty());
1632        assert!(manager.oto_target_quantities.is_empty());
1633    }
1634
1635    #[rstest]
1636    fn test_handle_contingencies_update_cancels_ouo_sibling_filled_past_target() {
1637        let (clock, cache) = create_test_components();
1638        let mut manager = OrderManager::new(clock, cache.clone(), false);
1639        let instrument = InstrumentAny::CurrencyPair(audusd_sim());
1640        let child = OrderTestBuilder::new(OrderType::Limit)
1641            .instrument_id(instrument.id())
1642            .client_order_id(ClientOrderId::from("O-CHILD"))
1643            .side(OrderSide::Sell)
1644            .price(Price::from("1.00100"))
1645            .quantity(Quantity::from(100_000))
1646            .submit(true)
1647            .build();
1648        cache
1649            .borrow_mut()
1650            .add_order(child.clone(), None, None, false)
1651            .unwrap();
1652        apply_fill(
1653            &cache,
1654            &child,
1655            &instrument,
1656            "T-CHILD",
1657            Quantity::from(60_000),
1658        );
1659        let parent = OrderTestBuilder::new(OrderType::Limit)
1660            .instrument_id(instrument.id())
1661            .client_order_id(ClientOrderId::from("O-PARENT"))
1662            .side(OrderSide::Buy)
1663            .price(Price::from("1.00000"))
1664            .quantity(Quantity::from(50_000))
1665            .contingency_type(ContingencyType::Ouo)
1666            .linked_order_ids(vec![child.client_order_id()])
1667            .submit(true)
1668            .build();
1669
1670        let actions = manager.handle_contingencies_update(&parent);
1671
1672        assert!(matches!(
1673            actions.as_slice(),
1674            [OrderManagerAction::CancelLocal(order)]
1675                if order.client_order_id() == child.client_order_id()
1676        ));
1677    }
1678
1679    #[rstest]
1680    fn test_handle_contingencies_update_does_not_sync_quantity_for_oco_sibling() {
1681        let (clock, cache) = create_test_components();
1682        let mut manager = OrderManager::new(clock, cache.clone(), true);
1683        let instrument = audusd_sim();
1684        let child = OrderTestBuilder::new(OrderType::Limit)
1685            .instrument_id(instrument.id())
1686            .client_order_id(ClientOrderId::from("O-CHILD"))
1687            .side(OrderSide::Sell)
1688            .price(Price::from("1.00100"))
1689            .quantity(Quantity::from(50_000))
1690            .build();
1691        cache
1692            .borrow_mut()
1693            .add_order(child.clone(), None, None, false)
1694            .unwrap();
1695        let parent = OrderTestBuilder::new(OrderType::Limit)
1696            .instrument_id(instrument.id())
1697            .client_order_id(ClientOrderId::from("O-PARENT"))
1698            .side(OrderSide::Buy)
1699            .price(Price::from("1.00000"))
1700            .quantity(Quantity::from(100_000))
1701            .contingency_type(ContingencyType::Oco)
1702            .linked_order_ids(vec![child.client_order_id()])
1703            .build();
1704
1705        let actions = manager.handle_contingencies_update(&parent);
1706
1707        assert!(actions.is_empty());
1708    }
1709
1710    #[derive(Debug, Clone, Copy)]
1711    enum TerminalEvent {
1712        Canceled,
1713        Expired,
1714        Rejected,
1715    }
1716
1717    fn apply_fill(
1718        cache: &Rc<RefCell<Cache>>,
1719        order: &OrderAny,
1720        instrument: &InstrumentAny,
1721        trade_id: &str,
1722        last_qty: Quantity,
1723    ) -> OrderEventAny {
1724        let event = TestOrderEventStubs::filled(
1725            order,
1726            instrument,
1727            Some(TradeId::from(trade_id)),
1728            None,
1729            None,
1730            Some(last_qty),
1731            None,
1732            None,
1733            None,
1734            Some(AccountId::from("ACCOUNT-001")),
1735        );
1736        cache.borrow_mut().update_order(&event).unwrap();
1737        event
1738    }
1739
1740    fn apply_accepted(cache: &Rc<RefCell<Cache>>, order: &OrderAny, venue_order_id: &str) {
1741        let event = TestOrderEventStubs::accepted(
1742            order,
1743            AccountId::from("ACCOUNT-001"),
1744            VenueOrderId::from(venue_order_id),
1745        );
1746        cache.borrow_mut().update_order(&event).unwrap();
1747    }
1748
1749    fn apply_update(
1750        cache: &Rc<RefCell<Cache>>,
1751        order: &OrderAny,
1752        quantity: Quantity,
1753    ) -> OrderEventAny {
1754        let event = OrderEventAny::Updated(
1755            OrderUpdatedSpec::builder()
1756                .trader_id(order.trader_id())
1757                .strategy_id(order.strategy_id())
1758                .instrument_id(order.instrument_id())
1759                .client_order_id(order.client_order_id())
1760                .quantity(quantity)
1761                .venue_order_id(VenueOrderId::from("V-CHILD"))
1762                .account_id(AccountId::from("ACCOUNT-001"))
1763                .build(),
1764        );
1765        cache.borrow_mut().update_order(&event).unwrap();
1766        event
1767    }
1768
1769    fn apply_terminal(
1770        cache: &Rc<RefCell<Cache>>,
1771        order: &OrderAny,
1772        terminal: TerminalEvent,
1773    ) -> OrderEventAny {
1774        let event = match terminal {
1775            TerminalEvent::Canceled => OrderEventAny::Canceled(
1776                OrderCanceledSpec::builder()
1777                    .trader_id(order.trader_id())
1778                    .strategy_id(order.strategy_id())
1779                    .instrument_id(order.instrument_id())
1780                    .client_order_id(order.client_order_id())
1781                    .account_id(AccountId::from("ACCOUNT-001"))
1782                    .build(),
1783            ),
1784            TerminalEvent::Expired => OrderEventAny::Expired(
1785                OrderExpiredSpec::builder()
1786                    .trader_id(order.trader_id())
1787                    .strategy_id(order.strategy_id())
1788                    .instrument_id(order.instrument_id())
1789                    .client_order_id(order.client_order_id())
1790                    .account_id(AccountId::from("ACCOUNT-001"))
1791                    .build(),
1792            ),
1793            TerminalEvent::Rejected => OrderEventAny::Rejected(
1794                OrderRejectedSpec::builder()
1795                    .trader_id(order.trader_id())
1796                    .strategy_id(order.strategy_id())
1797                    .instrument_id(order.instrument_id())
1798                    .client_order_id(order.client_order_id())
1799                    .account_id(AccountId::from("ACCOUNT-001"))
1800                    .reason("test rejection".into())
1801                    .build(),
1802            ),
1803        };
1804        cache.borrow_mut().update_order(&event).unwrap();
1805        event
1806    }
1807
1808    #[rstest]
1809    fn test_should_manage_order_selects_symmetric_ownership_classes() {
1810        let (clock, cache) = create_test_components();
1811        let active_local = OrderTestBuilder::new(OrderType::Limit)
1812            .instrument_id(audusd_sim().id())
1813            .side(OrderSide::Buy)
1814            .price(Price::from("1.00000"))
1815            .quantity(Quantity::from(100_000))
1816            .build();
1817        let non_active_local = OrderTestBuilder::new(OrderType::Limit)
1818            .instrument_id(audusd_sim().id())
1819            .side(OrderSide::Buy)
1820            .price(Price::from("1.00000"))
1821            .quantity(Quantity::from(100_000))
1822            .submit(true)
1823            .build();
1824        let emulator_manager = OrderManager::new(clock.clone(), cache.clone(), true);
1825        let strategy_manager = OrderManager::new(clock, cache, false);
1826
1827        assert!(emulator_manager.should_manage_order(&active_local));
1828        assert!(!emulator_manager.should_manage_order(&non_active_local));
1829        assert!(!strategy_manager.should_manage_order(&active_local));
1830        assert!(strategy_manager.should_manage_order(&non_active_local));
1831    }
1832
1833    #[rstest]
1834    #[case::strategy(false, true, false)]
1835    #[case::emulator(true, false, true)]
1836    fn test_handle_oto_partial_fill_resizes_once_and_submits_only_for_active_local(
1837        #[case] active_local: bool,
1838        #[case] child_submitted: bool,
1839        #[case] expects_submit: bool,
1840    ) {
1841        let (clock, cache) = create_test_components();
1842        let mut manager = OrderManager::new(clock, cache.clone(), active_local);
1843        let instrument = InstrumentAny::CurrencyPair(audusd_sim());
1844        let parent_id = ClientOrderId::from("O-PARENT");
1845        let child_id = ClientOrderId::from("O-CHILD");
1846        let parent = OrderTestBuilder::new(OrderType::Limit)
1847            .instrument_id(instrument.id())
1848            .client_order_id(parent_id)
1849            .side(OrderSide::Buy)
1850            .price(Price::from("1.00000"))
1851            .quantity(Quantity::from(100_000))
1852            .contingency_type(ContingencyType::Oto)
1853            .linked_order_ids(vec![child_id])
1854            .submit(true)
1855            .build();
1856        let child = OrderTestBuilder::new(OrderType::Limit)
1857            .instrument_id(instrument.id())
1858            .client_order_id(child_id)
1859            .side(OrderSide::Sell)
1860            .price(Price::from("1.00100"))
1861            .quantity(Quantity::from(100_000))
1862            .submit(child_submitted)
1863            .build();
1864        cache
1865            .borrow_mut()
1866            .add_order(parent.clone(), None, None, false)
1867            .unwrap();
1868        cache
1869            .borrow_mut()
1870            .add_order(child.clone(), None, None, false)
1871            .unwrap();
1872        let event = apply_fill(
1873            &cache,
1874            &parent,
1875            &instrument,
1876            "T-PARTIAL",
1877            Quantity::from(40_000),
1878        );
1879
1880        let actions = manager.handle_event(&event);
1881        let repeated_actions = manager.handle_event(&event);
1882
1883        assert_eq!(actions.len(), if expects_submit { 2 } else { 1 });
1884        assert!(matches!(
1885            &actions[0],
1886            OrderManagerAction::ModifyLocalQuantity { order, quantity }
1887                if order.client_order_id() == child_id
1888                    && *quantity == Quantity::from(40_000)
1889        ));
1890
1891        if expects_submit {
1892            assert!(matches!(
1893                &actions[1],
1894                OrderManagerAction::SubmitToRisk(command)
1895                    if command.client_order_id == child_id
1896            ));
1897        }
1898        assert!(repeated_actions.is_empty());
1899
1900        let modify_rejected = OrderEventAny::ModifyRejected(
1901            OrderModifyRejectedSpec::builder()
1902                .trader_id(child.trader_id())
1903                .strategy_id(child.strategy_id())
1904                .instrument_id(child.instrument_id())
1905                .client_order_id(child_id)
1906                .reason("test rejection".into())
1907                .build(),
1908        );
1909        assert!(manager.handle_event(&modify_rejected).is_empty());
1910        let retry_actions = manager.handle_event(&event);
1911        assert!(matches!(
1912            retry_actions.as_slice(),
1913            [OrderManagerAction::ModifyLocalQuantity { order, quantity }]
1914                if order.client_order_id() == child_id
1915                    && *quantity == Quantity::from(40_000)
1916        ));
1917
1918        let next_event = apply_fill(
1919            &cache,
1920            &parent,
1921            &instrument,
1922            "T-PARTIAL-2",
1923            Quantity::from(60_000),
1924        );
1925        let next_actions = manager.handle_event(&next_event);
1926
1927        assert!(next_actions.is_empty());
1928        assert_eq!(
1929            manager.oto_target_quantities.get(&child_id),
1930            Some(&Quantity::from(100_000)),
1931        );
1932    }
1933
1934    #[rstest]
1935    fn test_handle_oto_chained_child_releases_its_child() {
1936        let (clock, cache) = create_test_components();
1937        let mut manager = OrderManager::new(clock, cache.clone(), true);
1938        let instrument = InstrumentAny::CurrencyPair(audusd_sim());
1939        let root_id = ClientOrderId::from("O-ROOT");
1940        let parent_id = ClientOrderId::from("O-PARENT");
1941        let child_id = ClientOrderId::from("O-CHILD");
1942        let parent = OrderTestBuilder::new(OrderType::Limit)
1943            .instrument_id(instrument.id())
1944            .client_order_id(parent_id)
1945            .side(OrderSide::Buy)
1946            .price(Price::from("1.00000"))
1947            .quantity(Quantity::from(100_000))
1948            .contingency_type(ContingencyType::Oto)
1949            .linked_order_ids(vec![child_id])
1950            .parent_order_id(root_id)
1951            .submit(true)
1952            .build();
1953        let child = OrderTestBuilder::new(OrderType::Limit)
1954            .instrument_id(instrument.id())
1955            .client_order_id(child_id)
1956            .side(OrderSide::Sell)
1957            .price(Price::from("1.00100"))
1958            .quantity(Quantity::from(100_000))
1959            .parent_order_id(parent_id)
1960            .emulation_trigger(TriggerType::BidAsk)
1961            .build();
1962        cache
1963            .borrow_mut()
1964            .add_order(parent.clone(), None, None, false)
1965            .unwrap();
1966        cache
1967            .borrow_mut()
1968            .add_order(child, None, None, false)
1969            .unwrap();
1970        apply_accepted(&cache, &parent, "V-PARENT");
1971        let fill = apply_fill(
1972            &cache,
1973            &parent,
1974            &instrument,
1975            "T-PARENT",
1976            Quantity::from(100_000),
1977        );
1978
1979        let actions = manager.handle_event(&fill);
1980
1981        assert!(matches!(
1982            actions.as_slice(),
1983            [OrderManagerAction::SubmitToEmulator(command)]
1984                if command.client_order_id == child_id
1985        ));
1986    }
1987
1988    #[rstest]
1989    fn test_handle_oto_cancels_child_filled_to_target() {
1990        let (clock, cache) = create_test_components();
1991        let mut manager = OrderManager::new(clock, cache.clone(), false);
1992        let instrument = InstrumentAny::CurrencyPair(audusd_sim());
1993        let child_id = ClientOrderId::from("O-CHILD");
1994        let parent = OrderTestBuilder::new(OrderType::Limit)
1995            .instrument_id(instrument.id())
1996            .client_order_id(ClientOrderId::from("O-PARENT"))
1997            .side(OrderSide::Buy)
1998            .price(Price::from("1.00000"))
1999            .quantity(Quantity::from(100_000))
2000            .contingency_type(ContingencyType::Oto)
2001            .linked_order_ids(vec![child_id])
2002            .submit(true)
2003            .build();
2004        let child = OrderTestBuilder::new(OrderType::Limit)
2005            .instrument_id(instrument.id())
2006            .client_order_id(child_id)
2007            .side(OrderSide::Sell)
2008            .price(Price::from("1.00100"))
2009            .quantity(Quantity::from(100_000))
2010            .submit(true)
2011            .build();
2012        cache
2013            .borrow_mut()
2014            .add_order(parent.clone(), None, None, false)
2015            .unwrap();
2016        cache
2017            .borrow_mut()
2018            .add_order(child.clone(), None, None, false)
2019            .unwrap();
2020        apply_accepted(&cache, &parent, "V-PARENT");
2021        apply_accepted(&cache, &child, "V-CHILD");
2022        apply_fill(
2023            &cache,
2024            &child,
2025            &instrument,
2026            "T-CHILD",
2027            Quantity::from(60_000),
2028        );
2029        let parent_fill = apply_fill(
2030            &cache,
2031            &parent,
2032            &instrument,
2033            "T-PARENT",
2034            Quantity::from(40_000),
2035        );
2036
2037        let actions = manager.handle_event(&parent_fill);
2038
2039        assert!(matches!(
2040            actions.as_slice(),
2041            [OrderManagerAction::CancelLocal(order)]
2042                if order.client_order_id() == child_id
2043        ));
2044        assert!(!manager.oto_target_quantities.contains_key(&child_id));
2045    }
2046
2047    #[rstest]
2048    fn test_handle_oto_clears_target_when_child_leaves_active_local() {
2049        let (clock, cache) = create_test_components();
2050        let mut terminal_manager = OrderManager::new(clock.clone(), cache.clone(), true);
2051        let mut update_manager = OrderManager::new(clock.clone(), cache.clone(), true);
2052        let mut contingency_update_manager = OrderManager::new(clock.clone(), cache.clone(), true);
2053        let mut fill_manager = OrderManager::new(clock, cache.clone(), true);
2054        let instrument = InstrumentAny::CurrencyPair(audusd_sim());
2055        let parent_id = ClientOrderId::from("O-PARENT");
2056        let child_id = ClientOrderId::from("O-CHILD");
2057        let parent = OrderTestBuilder::new(OrderType::Limit)
2058            .instrument_id(instrument.id())
2059            .client_order_id(parent_id)
2060            .side(OrderSide::Buy)
2061            .price(Price::from("1.00000"))
2062            .quantity(Quantity::from(100_000))
2063            .contingency_type(ContingencyType::Oto)
2064            .linked_order_ids(vec![child_id])
2065            .submit(true)
2066            .build();
2067        let child = OrderTestBuilder::new(OrderType::Limit)
2068            .instrument_id(instrument.id())
2069            .client_order_id(child_id)
2070            .side(OrderSide::Sell)
2071            .price(Price::from("1.00100"))
2072            .quantity(Quantity::from(100_000))
2073            .build();
2074        cache
2075            .borrow_mut()
2076            .add_order(parent.clone(), None, None, false)
2077            .unwrap();
2078        cache
2079            .borrow_mut()
2080            .add_order(child.clone(), None, None, false)
2081            .unwrap();
2082        let fill = apply_fill(
2083            &cache,
2084            &parent,
2085            &instrument,
2086            "T-PARTIAL",
2087            Quantity::from(40_000),
2088        );
2089        terminal_manager.handle_event(&fill);
2090        update_manager.handle_event(&fill);
2091        contingency_update_manager.handle_event(&fill);
2092        fill_manager.handle_event(&fill);
2093
2094        let submitted = TestOrderEventStubs::submitted(&child, AccountId::from("ACCOUNT-001"));
2095        cache.borrow_mut().update_order(&submitted).unwrap();
2096        apply_accepted(&cache, &child, "V-CHILD");
2097
2098        let cached_child = cache.borrow().order_owned(&child_id).unwrap();
2099        let child_fill = apply_fill(
2100            &cache,
2101            &cached_child,
2102            &instrument,
2103            "T-CHILD",
2104            Quantity::from(60_000),
2105        );
2106        assert!(fill_manager.handle_event(&child_fill).is_empty());
2107        assert!(!fill_manager.oto_target_quantities.contains_key(&child_id));
2108
2109        let cached_parent = cache.borrow().order_owned(&parent_id).unwrap();
2110        assert!(
2111            contingency_update_manager
2112                .handle_contingencies_update(&cached_parent)
2113                .is_empty()
2114        );
2115        assert!(
2116            !contingency_update_manager
2117                .oto_target_quantities
2118                .contains_key(&child_id)
2119        );
2120
2121        let terminal = apply_terminal(&cache, &parent, TerminalEvent::Canceled);
2122        assert!(terminal_manager.handle_event(&terminal).is_empty());
2123        assert!(
2124            !terminal_manager
2125                .oto_target_quantities
2126                .contains_key(&child_id)
2127        );
2128
2129        let update = apply_update(&cache, &child, Quantity::from(80_000));
2130        assert!(update_manager.handle_event(&update).is_empty());
2131        assert!(!update_manager.oto_target_quantities.contains_key(&child_id));
2132    }
2133
2134    #[rstest]
2135    fn test_handle_oto_out_of_order_updates_reassert_latest_target() {
2136        let (clock, cache) = create_test_components();
2137        let mut manager = OrderManager::new(clock, cache.clone(), false);
2138        let instrument = InstrumentAny::CurrencyPair(audusd_sim());
2139        let child_id = ClientOrderId::from("O-CHILD");
2140        let parent = OrderTestBuilder::new(OrderType::Limit)
2141            .instrument_id(instrument.id())
2142            .client_order_id(ClientOrderId::from("O-PARENT"))
2143            .side(OrderSide::Buy)
2144            .price(Price::from("1.00000"))
2145            .quantity(Quantity::from(100_000))
2146            .contingency_type(ContingencyType::Oto)
2147            .linked_order_ids(vec![child_id])
2148            .submit(true)
2149            .build();
2150        let child = OrderTestBuilder::new(OrderType::Limit)
2151            .instrument_id(instrument.id())
2152            .client_order_id(child_id)
2153            .side(OrderSide::Sell)
2154            .price(Price::from("1.00100"))
2155            .quantity(Quantity::from(100_000))
2156            .submit(true)
2157            .build();
2158        cache
2159            .borrow_mut()
2160            .add_order(parent.clone(), None, None, false)
2161            .unwrap();
2162        cache
2163            .borrow_mut()
2164            .add_order(child.clone(), None, None, false)
2165            .unwrap();
2166        apply_accepted(&cache, &child, "V-CHILD");
2167
2168        let first_fill = apply_fill(
2169            &cache,
2170            &parent,
2171            &instrument,
2172            "T-OUT-OF-ORDER-1",
2173            Quantity::from(40_000),
2174        );
2175        manager.handle_event(&first_fill);
2176        let second_fill = apply_fill(
2177            &cache,
2178            &parent,
2179            &instrument,
2180            "T-OUT-OF-ORDER-2",
2181            Quantity::from(60_000),
2182        );
2183        manager.handle_event(&second_fill);
2184
2185        let latest_update = apply_update(&cache, &child, Quantity::from(100_000));
2186        let latest_actions = manager.handle_event(&latest_update);
2187        let stale_update = apply_update(&cache, &child, Quantity::from(40_000));
2188        let corrective_actions = manager.handle_event(&stale_update);
2189
2190        assert!(latest_actions.is_empty());
2191        assert!(matches!(
2192            corrective_actions.as_slice(),
2193            [OrderManagerAction::ModifyLocalQuantity { order, quantity }]
2194                if order.client_order_id() == child_id
2195                    && *quantity == Quantity::from(100_000)
2196        ));
2197    }
2198
2199    #[rstest]
2200    fn test_handle_oto_child_fill_waits_for_parent_target_refresh() {
2201        let (clock, cache) = create_test_components();
2202        let mut manager = OrderManager::new(clock, cache.clone(), false);
2203        let instrument = InstrumentAny::CurrencyPair(audusd_sim());
2204        let parent_id = ClientOrderId::from("O-PARENT");
2205        let child_id = ClientOrderId::from("O-CHILD");
2206        let parent = OrderTestBuilder::new(OrderType::Limit)
2207            .instrument_id(instrument.id())
2208            .client_order_id(parent_id)
2209            .side(OrderSide::Buy)
2210            .price(Price::from("1.00000"))
2211            .quantity(Quantity::from(100_000))
2212            .contingency_type(ContingencyType::Oto)
2213            .linked_order_ids(vec![child_id])
2214            .submit(true)
2215            .build();
2216        let child = OrderTestBuilder::new(OrderType::Limit)
2217            .instrument_id(instrument.id())
2218            .client_order_id(child_id)
2219            .side(OrderSide::Sell)
2220            .price(Price::from("1.00100"))
2221            .quantity(Quantity::from(100_000))
2222            .contingency_type(ContingencyType::Oto)
2223            .linked_order_ids(vec![parent_id])
2224            .parent_order_id(parent_id)
2225            .submit(true)
2226            .build();
2227        cache
2228            .borrow_mut()
2229            .add_order(parent.clone(), None, None, false)
2230            .unwrap();
2231        cache
2232            .borrow_mut()
2233            .add_order(child.clone(), None, None, false)
2234            .unwrap();
2235        apply_accepted(&cache, &parent, "V-PARENT");
2236        apply_accepted(&cache, &child, "V-CHILD");
2237        let parent_fill = apply_fill(
2238            &cache,
2239            &parent,
2240            &instrument,
2241            "T-PARENT",
2242            Quantity::from(40_000),
2243        );
2244        manager.handle_event(&parent_fill);
2245        let child_fill = apply_fill(
2246            &cache,
2247            &child,
2248            &instrument,
2249            "T-CHILD",
2250            Quantity::from(60_000),
2251        );
2252
2253        let actions = manager.handle_event(&child_fill);
2254
2255        assert!(actions.is_empty());
2256        assert_eq!(
2257            manager.oto_target_quantities.get(&child_id),
2258            Some(&Quantity::from(40_000)),
2259        );
2260
2261        let cached_parent = cache.borrow().order_owned(&parent_id).unwrap();
2262        let final_parent_fill = apply_fill(
2263            &cache,
2264            &cached_parent,
2265            &instrument,
2266            "T-PARENT-FINAL",
2267            Quantity::from(60_000),
2268        );
2269        let final_actions = manager.handle_event(&final_parent_fill);
2270
2271        assert!(final_actions.is_empty());
2272        assert_eq!(
2273            manager.oto_target_quantities.get(&child_id),
2274            Some(&Quantity::from(100_000)),
2275        );
2276    }
2277
2278    #[rstest]
2279    fn test_handle_oto_child_update_waits_when_filled_past_stored_target() {
2280        let (clock, cache) = create_test_components();
2281        let mut manager = OrderManager::new(clock, cache.clone(), false);
2282        let instrument = InstrumentAny::CurrencyPair(audusd_sim());
2283        let child_id = ClientOrderId::from("O-CHILD");
2284        let child = OrderTestBuilder::new(OrderType::Limit)
2285            .instrument_id(instrument.id())
2286            .client_order_id(child_id)
2287            .side(OrderSide::Sell)
2288            .price(Price::from("1.00100"))
2289            .quantity(Quantity::from(100_000))
2290            .submit(true)
2291            .build();
2292        cache
2293            .borrow_mut()
2294            .add_order(child.clone(), None, None, false)
2295            .unwrap();
2296        apply_accepted(&cache, &child, "V-CHILD");
2297        apply_fill(
2298            &cache,
2299            &child,
2300            &instrument,
2301            "T-CHILD",
2302            Quantity::from(60_000),
2303        );
2304        manager
2305            .oto_target_quantities
2306            .insert(child_id, Quantity::from(40_000));
2307        let update = apply_update(&cache, &child, Quantity::from(90_000));
2308
2309        let actions = manager.handle_event(&update);
2310
2311        assert!(actions.is_empty());
2312        assert_eq!(
2313            manager.oto_target_quantities.get(&child_id),
2314            Some(&Quantity::from(40_000)),
2315        );
2316    }
2317
2318    #[rstest]
2319    fn test_handle_oto_parent_update_preserves_partial_fill_target() {
2320        let (clock, cache) = create_test_components();
2321        let mut manager = OrderManager::new(clock, cache.clone(), false);
2322        let instrument = InstrumentAny::CurrencyPair(audusd_sim());
2323        let child_id = ClientOrderId::from("O-CHILD");
2324        let parent = OrderTestBuilder::new(OrderType::Limit)
2325            .instrument_id(instrument.id())
2326            .client_order_id(ClientOrderId::from("O-PARENT"))
2327            .side(OrderSide::Buy)
2328            .price(Price::from("1.00000"))
2329            .quantity(Quantity::from(100_000))
2330            .contingency_type(ContingencyType::Oto)
2331            .linked_order_ids(vec![child_id])
2332            .submit(true)
2333            .build();
2334        let child = OrderTestBuilder::new(OrderType::Limit)
2335            .instrument_id(instrument.id())
2336            .client_order_id(child_id)
2337            .side(OrderSide::Sell)
2338            .price(Price::from("1.00100"))
2339            .quantity(Quantity::from(100_000))
2340            .submit(true)
2341            .build();
2342        cache
2343            .borrow_mut()
2344            .add_order(parent.clone(), None, None, false)
2345            .unwrap();
2346        cache
2347            .borrow_mut()
2348            .add_order(child, None, None, false)
2349            .unwrap();
2350        let fill = apply_fill(
2351            &cache,
2352            &parent,
2353            &instrument,
2354            "T-PARTIAL",
2355            Quantity::from(40_000),
2356        );
2357        manager.handle_event(&fill);
2358        let parent_update = apply_update(&cache, &parent, Quantity::from(120_000));
2359
2360        let actions = manager.handle_event(&parent_update);
2361
2362        assert!(actions.is_empty());
2363        assert_eq!(
2364            manager.oto_target_quantities.get(&child_id),
2365            Some(&Quantity::from(40_000)),
2366        );
2367
2368        let terminal = apply_terminal(&cache, &parent, TerminalEvent::Canceled);
2369        assert!(manager.handle_event(&terminal).is_empty());
2370        assert_eq!(
2371            manager.oto_target_quantities.get(&child_id),
2372            Some(&Quantity::from(40_000)),
2373        );
2374    }
2375
2376    #[rstest]
2377    fn test_handle_oto_fill_uses_completed_and_active_spawn_totals() {
2378        let (clock, cache) = create_test_components();
2379        let mut manager = OrderManager::new(clock, cache.clone(), false);
2380        let instrument = InstrumentAny::CurrencyPair(audusd_sim());
2381        let spawn_id = ClientOrderId::from("O-SPAWN");
2382        let first_id = ClientOrderId::from("O-SPAWN-1");
2383        let second_id = ClientOrderId::from("O-SPAWN-2");
2384        let child_id = ClientOrderId::from("O-CHILD");
2385        let exec_algorithm_id = ExecAlgorithmId::from("TWAP");
2386        let first = OrderTestBuilder::new(OrderType::Limit)
2387            .instrument_id(instrument.id())
2388            .client_order_id(first_id)
2389            .side(OrderSide::Buy)
2390            .price(Price::from("1.00000"))
2391            .quantity(Quantity::from(60_000))
2392            .contingency_type(ContingencyType::Oto)
2393            .linked_order_ids(vec![child_id])
2394            .exec_algorithm_id(exec_algorithm_id)
2395            .exec_spawn_id(spawn_id)
2396            .submit(true)
2397            .build();
2398        let second = OrderTestBuilder::new(OrderType::Limit)
2399            .instrument_id(instrument.id())
2400            .client_order_id(second_id)
2401            .side(OrderSide::Buy)
2402            .price(Price::from("1.00000"))
2403            .quantity(Quantity::from(40_000))
2404            .contingency_type(ContingencyType::Oto)
2405            .linked_order_ids(vec![child_id])
2406            .exec_algorithm_id(exec_algorithm_id)
2407            .exec_spawn_id(spawn_id)
2408            .submit(true)
2409            .build();
2410        let child = OrderTestBuilder::new(OrderType::Limit)
2411            .instrument_id(instrument.id())
2412            .client_order_id(child_id)
2413            .side(OrderSide::Sell)
2414            .price(Price::from("1.00100"))
2415            .quantity(Quantity::from(100_000))
2416            .submit(true)
2417            .build();
2418
2419        for order in [&first, &second, &child] {
2420            cache
2421                .borrow_mut()
2422                .add_order(order.clone(), None, None, false)
2423                .unwrap();
2424        }
2425        apply_accepted(&cache, &first, "V-SPAWN-1");
2426        apply_accepted(&cache, &second, "V-SPAWN-2");
2427        apply_fill(
2428            &cache,
2429            &first,
2430            &instrument,
2431            "T-SPAWN-1",
2432            Quantity::from(60_000),
2433        );
2434        let event = apply_fill(
2435            &cache,
2436            &second,
2437            &instrument,
2438            "T-SPAWN-2",
2439            Quantity::from(10_000),
2440        );
2441
2442        let actions = manager.handle_event(&event);
2443
2444        assert!(matches!(
2445            actions.first(),
2446            Some(OrderManagerAction::ModifyLocalQuantity { order, quantity })
2447                if order.client_order_id() == child_id
2448                    && *quantity == Quantity::from(70_000)
2449        ));
2450
2451        let update = apply_update(&cache, &second, Quantity::from(50_000));
2452        assert!(manager.handle_event(&update).is_empty());
2453        assert_eq!(
2454            manager.oto_target_quantities.get(&child_id),
2455            Some(&Quantity::from(70_000)),
2456        );
2457
2458        let cached_second = cache.borrow().order_owned(&second_id).unwrap();
2459        assert!(manager.handle_contingencies(&cached_second).is_empty());
2460    }
2461
2462    #[rstest]
2463    fn test_handle_ouo_partial_fill_resizes_open_sibling() {
2464        let (clock, cache) = create_test_components();
2465        let mut manager = OrderManager::new(clock, cache.clone(), false);
2466        let instrument = InstrumentAny::CurrencyPair(audusd_sim());
2467        let parent_id = ClientOrderId::from("O-PARENT");
2468        let sibling_id = ClientOrderId::from("O-SIBLING");
2469        let parent = OrderTestBuilder::new(OrderType::Limit)
2470            .instrument_id(instrument.id())
2471            .client_order_id(parent_id)
2472            .side(OrderSide::Buy)
2473            .price(Price::from("1.00000"))
2474            .quantity(Quantity::from(100_000))
2475            .contingency_type(ContingencyType::Ouo)
2476            .linked_order_ids(vec![parent_id, sibling_id])
2477            .submit(true)
2478            .build();
2479        let sibling = OrderTestBuilder::new(OrderType::Limit)
2480            .instrument_id(instrument.id())
2481            .client_order_id(sibling_id)
2482            .side(OrderSide::Sell)
2483            .price(Price::from("1.00100"))
2484            .quantity(Quantity::from(100_000))
2485            .submit(true)
2486            .build();
2487        cache
2488            .borrow_mut()
2489            .add_order(parent.clone(), None, None, false)
2490            .unwrap();
2491        cache
2492            .borrow_mut()
2493            .add_order(sibling, None, None, false)
2494            .unwrap();
2495        let event = apply_fill(
2496            &cache,
2497            &parent,
2498            &instrument,
2499            "T-OUO",
2500            Quantity::from(25_000),
2501        );
2502
2503        let actions = manager.handle_event(&event);
2504
2505        assert!(matches!(
2506            actions.as_slice(),
2507            [OrderManagerAction::ModifyLocalQuantity { order, quantity }]
2508                if order.client_order_id() == sibling_id
2509                    && *quantity == Quantity::from(75_000)
2510        ));
2511    }
2512
2513    #[rstest]
2514    fn test_handle_ouo_partial_fill_cancels_sibling_filled_past_target() {
2515        let (clock, cache) = create_test_components();
2516        let mut manager = OrderManager::new(clock, cache.clone(), false);
2517        let instrument = InstrumentAny::CurrencyPair(audusd_sim());
2518        let parent_id = ClientOrderId::from("O-PARENT");
2519        let sibling_id = ClientOrderId::from("O-SIBLING");
2520        let parent = OrderTestBuilder::new(OrderType::Limit)
2521            .instrument_id(instrument.id())
2522            .client_order_id(parent_id)
2523            .side(OrderSide::Buy)
2524            .price(Price::from("1.00000"))
2525            .quantity(Quantity::from(100_000))
2526            .contingency_type(ContingencyType::Ouo)
2527            .linked_order_ids(vec![parent_id, sibling_id])
2528            .submit(true)
2529            .build();
2530        let sibling = OrderTestBuilder::new(OrderType::Limit)
2531            .instrument_id(instrument.id())
2532            .client_order_id(sibling_id)
2533            .side(OrderSide::Sell)
2534            .price(Price::from("1.00100"))
2535            .quantity(Quantity::from(100_000))
2536            .submit(true)
2537            .build();
2538        cache
2539            .borrow_mut()
2540            .add_order(parent.clone(), None, None, false)
2541            .unwrap();
2542        cache
2543            .borrow_mut()
2544            .add_order(sibling.clone(), None, None, false)
2545            .unwrap();
2546        apply_accepted(&cache, &parent, "V-PARENT");
2547        apply_accepted(&cache, &sibling, "V-SIBLING");
2548        apply_fill(
2549            &cache,
2550            &sibling,
2551            &instrument,
2552            "T-SIBLING",
2553            Quantity::from(30_000),
2554        );
2555        let parent_fill = apply_fill(
2556            &cache,
2557            &parent,
2558            &instrument,
2559            "T-PARENT",
2560            Quantity::from(90_000),
2561        );
2562
2563        let actions = manager.handle_event(&parent_fill);
2564
2565        assert!(matches!(
2566            actions.as_slice(),
2567            [OrderManagerAction::CancelLocal(order)]
2568                if order.client_order_id() == sibling_id
2569        ));
2570    }
2571
2572    #[rstest]
2573    #[case(TerminalEvent::Canceled)]
2574    #[case(TerminalEvent::Expired)]
2575    #[case(TerminalEvent::Rejected)]
2576    fn test_terminal_non_contingent_event_clears_oto_target(#[case] terminal: TerminalEvent) {
2577        let (clock, cache) = create_test_components();
2578        let mut manager = OrderManager::new(clock, cache.clone(), false);
2579        let order = OrderTestBuilder::new(OrderType::Limit)
2580            .instrument_id(audusd_sim().id())
2581            .client_order_id(ClientOrderId::from("O-001"))
2582            .side(OrderSide::Buy)
2583            .price(Price::from("1.00000"))
2584            .quantity(Quantity::from(100_000))
2585            .submit(true)
2586            .build();
2587        cache
2588            .borrow_mut()
2589            .add_order(order.clone(), None, None, false)
2590            .unwrap();
2591
2592        if matches!(terminal, TerminalEvent::Expired) {
2593            apply_accepted(&cache, &order, "V-001");
2594        }
2595        manager
2596            .oto_target_quantities
2597            .insert(order.client_order_id(), Quantity::from(40_000));
2598        let event = apply_terminal(&cache, &order, terminal);
2599
2600        let actions = manager.handle_event(&event);
2601
2602        assert!(actions.is_empty());
2603        assert!(manager.oto_target_quantities.is_empty());
2604    }
2605
2606    #[rstest]
2607    #[case(ContingencyType::Oto, TerminalEvent::Rejected)]
2608    #[case(ContingencyType::Oco, TerminalEvent::Canceled)]
2609    #[case(ContingencyType::Oco, TerminalEvent::Expired)]
2610    #[case(ContingencyType::Oco, TerminalEvent::Rejected)]
2611    #[case(ContingencyType::Ouo, TerminalEvent::Canceled)]
2612    fn test_terminal_contingent_event_cancels_open_sibling(
2613        #[case] contingency_type: ContingencyType,
2614        #[case] terminal: TerminalEvent,
2615    ) {
2616        let (clock, cache) = create_test_components();
2617        let mut manager = OrderManager::new(clock, cache.clone(), false);
2618        let parent_id = ClientOrderId::from("O-PARENT");
2619        let sibling_id = ClientOrderId::from("O-SIBLING");
2620        let linked_order_ids = if contingency_type == ContingencyType::Oto {
2621            vec![sibling_id]
2622        } else {
2623            vec![parent_id, sibling_id]
2624        };
2625        let parent = OrderTestBuilder::new(OrderType::Limit)
2626            .instrument_id(audusd_sim().id())
2627            .client_order_id(parent_id)
2628            .side(OrderSide::Buy)
2629            .price(Price::from("1.00000"))
2630            .quantity(Quantity::from(100_000))
2631            .contingency_type(contingency_type)
2632            .linked_order_ids(linked_order_ids)
2633            .submit(true)
2634            .build();
2635        let sibling = OrderTestBuilder::new(OrderType::Limit)
2636            .instrument_id(audusd_sim().id())
2637            .client_order_id(sibling_id)
2638            .side(OrderSide::Sell)
2639            .price(Price::from("1.00100"))
2640            .quantity(Quantity::from(100_000))
2641            .submit(true)
2642            .build();
2643        cache
2644            .borrow_mut()
2645            .add_order(parent.clone(), None, None, false)
2646            .unwrap();
2647        cache
2648            .borrow_mut()
2649            .add_order(sibling, None, None, false)
2650            .unwrap();
2651
2652        if matches!(terminal, TerminalEvent::Expired) {
2653            apply_accepted(&cache, &parent, "V-PARENT");
2654        }
2655        let event = apply_terminal(&cache, &parent, terminal);
2656
2657        let actions = manager.handle_event(&event);
2658
2659        assert!(matches!(
2660            actions.as_slice(),
2661            [OrderManagerAction::CancelLocal(order)]
2662                if order.client_order_id() == sibling_id
2663        ));
2664    }
2665
2666    #[rstest]
2667    #[case(ContingencyType::Oto)]
2668    #[case(ContingencyType::Ouo)]
2669    fn test_strategy_manager_excludes_active_local_and_closed_siblings(
2670        #[case] contingency_type: ContingencyType,
2671    ) {
2672        let (clock, cache) = create_test_components();
2673        let mut manager = OrderManager::new(clock, cache.clone(), false);
2674        let instrument = InstrumentAny::CurrencyPair(audusd_sim());
2675        let parent_id = ClientOrderId::from("O-PARENT");
2676        let active_local_id = ClientOrderId::from("O-ACTIVE-LOCAL");
2677        let closed_id = ClientOrderId::from("O-CLOSED");
2678        let parent = OrderTestBuilder::new(OrderType::Limit)
2679            .instrument_id(instrument.id())
2680            .client_order_id(parent_id)
2681            .side(OrderSide::Buy)
2682            .price(Price::from("1.00000"))
2683            .quantity(Quantity::from(100_000))
2684            .contingency_type(contingency_type)
2685            .linked_order_ids(vec![active_local_id, closed_id])
2686            .submit(true)
2687            .build();
2688        let active_local = OrderTestBuilder::new(OrderType::Limit)
2689            .instrument_id(instrument.id())
2690            .client_order_id(active_local_id)
2691            .side(OrderSide::Sell)
2692            .price(Price::from("1.00100"))
2693            .quantity(Quantity::from(100_000))
2694            .build();
2695        let closed = OrderTestBuilder::new(OrderType::Limit)
2696            .instrument_id(instrument.id())
2697            .client_order_id(closed_id)
2698            .side(OrderSide::Sell)
2699            .price(Price::from("1.00100"))
2700            .quantity(Quantity::from(100_000))
2701            .submit(true)
2702            .build();
2703        cache
2704            .borrow_mut()
2705            .add_order(parent.clone(), None, None, false)
2706            .unwrap();
2707        cache
2708            .borrow_mut()
2709            .add_order(active_local, None, None, false)
2710            .unwrap();
2711        cache
2712            .borrow_mut()
2713            .add_order(closed.clone(), None, None, false)
2714            .unwrap();
2715        apply_terminal(&cache, &closed, TerminalEvent::Canceled);
2716        let event = apply_fill(
2717            &cache,
2718            &parent,
2719            &instrument,
2720            "T-EXCLUSION",
2721            Quantity::from(100_000),
2722        );
2723
2724        let actions = manager.handle_event(&event);
2725
2726        assert!(actions.is_empty());
2727    }
2728}