Skip to main content

nautilus_trading/algorithm/
mod.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
16//! Execution algorithm infrastructure for order slicing and execution optimization.
17//!
18//! This module provides the [`ExecutionAlgorithm`] trait and supporting infrastructure
19//! for implementing algorithms like TWAP (Time-Weighted Average Price) and VWAP
20//! (Volume-Weighted Average Price) that slice large orders into smaller child orders.
21//!
22//! # Architecture
23//!
24//! Execution algorithms extend [`DataActor`] (not [`Strategy`](super::Strategy)) because:
25//! - They don't own positions (the parent Strategy does).
26//! - Spawned orders carry the parent Strategy's ID, not the algorithm's ID.
27//! - They act as order processors/transformers, not position managers.
28//!
29//! # Order Flow
30//!
31//! 1. A Strategy submits an order with `exec_algorithm_id` set.
32//! 2. The order is routed to the algorithm's `{id}.execute` endpoint.
33//! 3. The algorithm receives the order via `on_order()`.
34//! 4. The algorithm spawns child orders using `spawn_market()`, `spawn_limit()`, etc.
35//! 5. Spawned orders are submitted through the `RiskEngine`.
36//! 6. The algorithm receives fill events and manages remaining quantity.
37
38use std::fmt::Display;
39
40pub mod config;
41pub mod core;
42pub mod twap;
43
44pub use core::{ExecutionAlgorithmCore, ExecutionAlgorithmNative, StrategyEventHandlers};
45
46pub use config::{ExecutionAlgorithmConfig, ImportableExecutionAlgorithmConfig};
47use nautilus_common::{
48    actor::{DataActor, DataActorNative, registry::try_get_actor_unchecked},
49    enums::ComponentState,
50    logging::{CMD, EVT, RECV, SEND},
51    messages::execution::{CancelOrder, ModifyOrder, SubmitOrder, TradingCommand},
52    msgbus::{self, MessagingSwitchboard, TypedHandler},
53    timer::TimeEvent,
54};
55use nautilus_core::{UUID4, UnixNanos};
56use nautilus_model::{
57    enums::{OrderStatus, TimeInForce, TriggerType},
58    events::{
59        OrderAccepted, OrderCancelRejected, OrderCanceled, OrderDenied, OrderEmulated,
60        OrderEventAny, OrderExpired, OrderFillVoided, OrderFilled, OrderInitialized,
61        OrderModifyRejected, OrderPendingCancel, OrderPendingUpdate, OrderRejected, OrderReleased,
62        OrderSubmitted, OrderTriggered, OrderUpdated, PositionChanged, PositionClosed,
63        PositionEvent, PositionOpened,
64    },
65    identifiers::{
66        AccountId, ClientId, ClientOrderId, ExecAlgorithmId, PositionId, StrategyId, TraderId,
67    },
68    orders::{LimitOrder, MarketOrder, MarketToLimitOrder, Order, OrderAny, OrderError, OrderList},
69    types::{Price, Quantity, quantity::QuantityRaw},
70};
71pub use twap::{TwapAlgorithm, TwapAlgorithmConfig};
72use ustr::Ustr;
73
74use crate::algorithm::core::SpawnReduction;
75
76/// Core trait for implementing execution algorithms in NautilusTrader.
77///
78/// Execution algorithms are specialized [`DataActor`]s that receive orders from strategies
79/// and execute them by spawning child orders. They are used for order slicing algorithms
80/// like TWAP and VWAP.
81///
82/// # Key Capabilities
83///
84/// - All [`DataActor`] capabilities (data subscriptions, event handling, timers)
85/// - Order spawning (market, limit, market-to-limit)
86/// - Order lifecycle management (submit, modify, cancel)
87/// - Event filtering for algorithm-owned orders
88///
89/// When a reduced spawned order terminates with unfilled quantity, its reduction
90/// is restored in primary quantity units while the primary remains locally
91/// mutable. Submission handoff permanently ends restoration and late-fill
92/// re-deduction. A caller-held primary value remains reduced and must be
93/// discarded or refreshed from the cache before reuse.
94///
95/// # Implementation
96///
97/// Use the `nautilus_execution_algorithm!` macro to generate the native runtime
98/// wiring and `ExecutionAlgorithm` implementation, including the required
99/// `on_order()` method. Normal execution algorithm logic should call facade
100/// methods such as `submit_order()`, `spawn_market()`, and
101/// `unsubscribe_all_strategy_events()`. Native runtime code that needs the
102/// internal core should use [`ExecutionAlgorithmNative`].
103pub trait ExecutionAlgorithm: DataActor {
104    /// Returns the execution algorithm ID.
105    fn id(&self) -> ExecAlgorithmId
106    where
107        Self: ExecutionAlgorithmNative,
108    {
109        ExecutionAlgorithmNative::exec_algorithm_core(self).exec_algorithm_id
110    }
111
112    /// Executes a trading command.
113    ///
114    /// This is the main entry point for commands routed to the algorithm.
115    /// Dispatches to the appropriate handler based on command type.
116    ///
117    /// Commands are only processed when the algorithm is in `Running` state.
118    ///
119    /// # Errors
120    ///
121    /// Returns an error if command handling fails.
122    fn execute(&mut self, command: TradingCommand) -> anyhow::Result<()>
123    where
124        Self: ExecutionAlgorithmNative,
125        Self: 'static + std::fmt::Debug + Sized,
126    {
127        let core = ExecutionAlgorithmNative::exec_algorithm_core_mut(self);
128        if core.config.log_commands {
129            let id = &core.actor.actor_id;
130            log::info!("{id} {RECV}{CMD} {command}");
131        }
132
133        if DataActorNative::core(core).state() != ComponentState::Running {
134            return Ok(());
135        }
136
137        match command {
138            TradingCommand::SubmitOrder(cmd) => {
139                self.subscribe_to_strategy_events(cmd.strategy_id);
140                let core = ExecutionAlgorithmNative::exec_algorithm_core_mut(self);
141                core.remember_submit_params(cmd.client_order_id, cmd.params.clone());
142                let order = core.get_order(&cmd.client_order_id)?;
143                self.on_order(order)
144            }
145            TradingCommand::SubmitOrderList(cmd) => {
146                self.subscribe_to_strategy_events(cmd.strategy_id);
147                let core = ExecutionAlgorithmNative::exec_algorithm_core_mut(self);
148                for client_order_id in &cmd.order_list.client_order_ids {
149                    core.remember_submit_params(*client_order_id, cmd.params.clone());
150                }
151                let orders = core.get_orders_for_list(&cmd.order_list)?;
152                self.on_order_list(cmd.order_list, orders)
153            }
154            TradingCommand::ModifyOrder(cmd) => self.handle_modify_order(cmd),
155            TradingCommand::CancelOrder(cmd) => self.handle_cancel_order(cmd),
156            _ => {
157                log::warn!("Unhandled command type: {command}");
158                Ok(())
159            }
160        }
161    }
162
163    /// Called when a primary order is received for execution.
164    ///
165    /// Override this method to implement the algorithm's order slicing logic.
166    ///
167    /// # Errors
168    ///
169    /// Returns an error if order handling fails.
170    fn on_order(&mut self, order: OrderAny) -> anyhow::Result<()>;
171
172    /// Called when an order list is received for execution.
173    ///
174    /// Override this method to handle order lists. The default implementation
175    /// processes each order individually.
176    ///
177    /// # Errors
178    ///
179    /// Returns an error if order list handling fails.
180    fn on_order_list(
181        &mut self,
182        _order_list: OrderList,
183        orders: Vec<OrderAny>,
184    ) -> anyhow::Result<()> {
185        for order in orders {
186            self.on_order(order)?;
187        }
188        Ok(())
189    }
190
191    /// Denies an order by applying and publishing an `OrderDenied` event.
192    ///
193    /// An order absent from the cache is added first, with its `OrderInitialized` event published
194    /// before the denial. A closed cached order is left unchanged. Use an `OrderDeniedReason`
195    /// string for the standardized reason.
196    ///
197    /// # Errors
198    ///
199    /// Returns an error if:
200    /// - The algorithm is not registered with a trader.
201    /// - The order cannot be added to the cache.
202    /// - The denial cannot be applied, including an invalid order state transition.
203    ///
204    /// No event is published when the denial cannot be applied.
205    fn deny_order(&mut self, order: &OrderAny, reason: Ustr) -> anyhow::Result<()>
206    where
207        Self: ExecutionAlgorithmNative,
208    {
209        let core = ExecutionAlgorithmNative::exec_algorithm_core_mut(self);
210        registered_trader_id(core)?;
211        let ts_now = core.clock_mut().timestamp_ns();
212        let event = OrderEventAny::Denied(OrderDenied::new(
213            order.trader_id(),
214            order.strategy_id(),
215            order.instrument_id(),
216            order.client_order_id(),
217            reason,
218            UUID4::new(),
219            ts_now,
220            ts_now,
221        ));
222
223        let publish_initialized = {
224            let cache_rc = core.cache_rc();
225            let mut cache = cache_rc.borrow_mut();
226
227            if cache
228                .order(&order.client_order_id())
229                .is_some_and(|cached_order| cached_order.is_closed())
230            {
231                return Ok(());
232            }
233
234            let publish_initialized = if cache.order_exists(&order.client_order_id()) {
235                false
236            } else {
237                cache.add_order(order.clone(), None, None, false)?;
238                true
239            };
240
241            cache.update_order(&event)?;
242            publish_initialized
243        };
244
245        if publish_initialized {
246            publish_order_initialized(order);
247        }
248        publish_order_event(&event);
249
250        // A denied order never executes, so its stored submit params are dropped here
251        // rather than waiting for an execution completion that will never arrive.
252        ExecutionAlgorithmNative::exec_algorithm_core_mut(self)
253            .remove_submit_params(&order.client_order_id());
254
255        Ok(())
256    }
257
258    /// Handles a cancel order command for algorithm-managed orders.
259    ///
260    /// This generates an internal cancel event and publishes it. The order
261    /// is canceled locally without sending a command to the execution engine.
262    ///
263    /// # Errors
264    ///
265    /// Returns an error if cancellation fails.
266    fn handle_cancel_order(&mut self, command: CancelOrder) -> anyhow::Result<()>
267    where
268        Self: ExecutionAlgorithmNative,
269    {
270        let (order, is_pending_cancel) = {
271            let cache = ExecutionAlgorithmNative::exec_algorithm_core_mut(self).cache_ref();
272
273            let Some(order) = cache.order(&command.client_order_id) else {
274                log::warn!(
275                    "Cannot cancel order: {} not found in cache",
276                    command.client_order_id
277                );
278                return Ok(());
279            };
280
281            let is_pending = cache.is_order_pending_cancel_local(&command.client_order_id);
282            (order.clone(), is_pending)
283        };
284
285        if is_pending_cancel {
286            return Ok(());
287        }
288
289        if order.is_closed() {
290            log::warn!("Order already closed for {command}");
291            return Ok(());
292        }
293
294        let event = OrderEventAny::Canceled(self.generate_order_canceled(&order));
295
296        let order = {
297            let cache_rc = ExecutionAlgorithmNative::exec_algorithm_core_mut(self).cache_rc();
298            let mut cache = cache_rc.borrow_mut();
299            match cache.update_order(&event) {
300                Ok(order) => order,
301                Err(e)
302                    if matches!(
303                        e.downcast_ref::<OrderError>(),
304                        Some(OrderError::InvalidStateTransition)
305                    ) =>
306                {
307                    log::warn!("InvalidStateTrigger: {e}, did not apply cancel event");
308                    return Ok(());
309                }
310                Err(e) => return Err(e),
311            }
312        };
313
314        let topic = format!("events.order.{}", order.strategy_id());
315        msgbus::publish_order_event(topic.into(), &event);
316        msgbus::publish_order_event(
317            msgbus::switchboard::get_order_canceled_topic(order.instrument_id()),
318            &event,
319        );
320
321        Ok(())
322    }
323
324    /// Handles a modify order command for algorithm-managed orders.
325    ///
326    /// Active-local orders are left unchanged because the algorithm owns their execution state.
327    ///
328    /// # Errors
329    ///
330    /// Returns an error if command handling fails.
331    fn handle_modify_order(&mut self, command: ModifyOrder) -> anyhow::Result<()>
332    where
333        Self: ExecutionAlgorithmNative,
334    {
335        let (is_closed, is_active_local) = {
336            let cache = ExecutionAlgorithmNative::exec_algorithm_core_mut(self).cache_ref();
337
338            let Some(order) = cache.order(&command.client_order_id) else {
339                log::warn!(
340                    "Cannot modify order: {} not found in cache",
341                    command.client_order_id
342                );
343                return Ok(());
344            };
345
346            (order.is_closed(), order.is_active_local())
347        };
348
349        if is_closed {
350            log::warn!("Order already closed for {command}");
351            return Ok(());
352        }
353
354        if is_active_local {
355            log::warn!(
356                "Cannot modify {}: order is being executed by this algorithm",
357                command.client_order_id
358            );
359            return Ok(());
360        }
361
362        // A venue-active order is routed to the execution path, not here
363        log::warn!(
364            "Cannot modify {}: order is not active-local",
365            command.client_order_id
366        );
367        Ok(())
368    }
369
370    /// Generates an `OrderCanceled` event for an order.
371    fn generate_order_canceled(&mut self, order: &OrderAny) -> OrderCanceled
372    where
373        Self: ExecutionAlgorithmNative,
374    {
375        let ts_now = ExecutionAlgorithmNative::exec_algorithm_core_mut(self)
376            .clock_mut()
377            .timestamp_ns();
378
379        OrderCanceled::new(
380            order.trader_id(),
381            order.strategy_id(),
382            order.instrument_id(),
383            order.client_order_id(),
384            UUID4::new(),
385            ts_now,
386            ts_now,
387            false, // reconciliation
388            order.venue_order_id(),
389            order.account_id(),
390            None,
391        )
392    }
393
394    /// Generates an `OrderPendingUpdate` event for an order.
395    fn generate_order_pending_update(&mut self, order: &OrderAny) -> OrderPendingUpdate
396    where
397        Self: ExecutionAlgorithmNative,
398    {
399        let ts_now = ExecutionAlgorithmNative::exec_algorithm_core_mut(self)
400            .clock_mut()
401            .timestamp_ns();
402
403        OrderPendingUpdate::new(
404            order.trader_id(),
405            order.strategy_id(),
406            order.instrument_id(),
407            order.client_order_id(),
408            order.account_id(),
409            UUID4::new(),
410            ts_now,
411            ts_now,
412            false, // reconciliation
413            order.venue_order_id(),
414        )
415    }
416
417    /// Generates an `OrderPendingCancel` event for an order.
418    fn generate_order_pending_cancel(&mut self, order: &OrderAny) -> OrderPendingCancel
419    where
420        Self: ExecutionAlgorithmNative,
421    {
422        let ts_now = ExecutionAlgorithmNative::exec_algorithm_core_mut(self)
423            .clock_mut()
424            .timestamp_ns();
425
426        OrderPendingCancel::new(
427            order.trader_id(),
428            order.strategy_id(),
429            order.instrument_id(),
430            order.client_order_id(),
431            order.account_id(),
432            UUID4::new(),
433            ts_now,
434            ts_now,
435            false, // reconciliation
436            order.venue_order_id(),
437        )
438    }
439
440    /// Spawns a market order from a primary order.
441    ///
442    /// Creates a new market order with:
443    /// - A unique client order ID: `{primary_id}-E{sequence}`.
444    /// - The primary order's trader ID, strategy ID, and instrument ID.
445    /// - The algorithm's `exec_algorithm_id`.
446    /// - `exec_spawn_id` set to the primary order's client order ID.
447    ///
448    /// If `reduce_primary` is true, the primary order's quantity is reduced by
449    /// the spawned quantity. Unfilled quantity is restored when the spawn is
450    /// denied, rejected, canceled, expired, or refused before submission while
451    /// the primary remains locally mutable. Converted quote-quantity spawns are
452    /// restored proportionally in primary units. Late fills re-deduct restored
453    /// quantity until primary submission is handed off.
454    fn spawn_market(
455        &mut self,
456        primary: &mut OrderAny,
457        quantity: Quantity,
458        time_in_force: TimeInForce,
459        reduce_only: bool,
460        tags: Option<Vec<Ustr>>,
461        reduce_primary: bool,
462    ) -> MarketOrder
463    where
464        Self: ExecutionAlgorithmNative,
465    {
466        // Generate spawn ID first so we can track the reduction
467        let core = ExecutionAlgorithmNative::exec_algorithm_core_mut(self);
468        let client_order_id = core.spawn_client_order_id(&primary.client_order_id());
469        let ts_init = core.clock_mut().timestamp_ns();
470        let exec_algorithm_id = core.exec_algorithm_id;
471
472        if reduce_primary {
473            self.reduce_primary_order(primary, quantity);
474            ExecutionAlgorithmNative::exec_algorithm_core_mut(self).track_pending_spawn_reduction(
475                client_order_id,
476                primary.client_order_id(),
477                quantity,
478                primary.is_quote_quantity(),
479            );
480        }
481
482        MarketOrder::new(
483            primary.trader_id(),
484            primary.strategy_id(),
485            primary.instrument_id(),
486            client_order_id,
487            primary.order_side(),
488            quantity,
489            time_in_force,
490            UUID4::new(),
491            ts_init,
492            reduce_only,
493            primary.is_quote_quantity(),
494            primary.contingency_type(),
495            primary.order_list_id(),
496            primary.linked_order_ids().map(<[ClientOrderId]>::to_vec),
497            primary.parent_order_id(),
498            Some(exec_algorithm_id),
499            primary.exec_algorithm_params().cloned(),
500            Some(primary.client_order_id()),
501            tags.or_else(|| primary.tags().map(<[Ustr]>::to_vec)),
502        )
503    }
504
505    /// Spawns a limit order from a primary order.
506    ///
507    /// Creates a new limit order with:
508    /// - A unique client order ID: `{primary_id}-E{sequence}`
509    /// - The primary order's trader ID, strategy ID, and instrument ID
510    /// - The algorithm's `exec_algorithm_id`
511    /// - `exec_spawn_id` set to the primary order's client order ID
512    ///
513    /// `submit_order` refuses the returned order when `emulation_trigger` is
514    /// `Some`; use `None` for an order that the execution algorithm will submit.
515    ///
516    /// If `reduce_primary` is true, the primary order's quantity is reduced by
517    /// the spawned quantity. Unfilled quantity is restored when the spawn is
518    /// denied, rejected, canceled, expired, or refused before submission while
519    /// the primary remains locally mutable. Converted quote-quantity spawns are
520    /// restored proportionally in primary units. Late fills re-deduct restored
521    /// quantity until primary submission is handed off.
522    #[expect(clippy::too_many_arguments)]
523    fn spawn_limit(
524        &mut self,
525        primary: &mut OrderAny,
526        quantity: Quantity,
527        price: Price,
528        time_in_force: TimeInForce,
529        expire_time: Option<UnixNanos>,
530        post_only: bool,
531        reduce_only: bool,
532        display_qty: Option<Quantity>,
533        emulation_trigger: Option<TriggerType>,
534        tags: Option<Vec<Ustr>>,
535        reduce_primary: bool,
536    ) -> LimitOrder
537    where
538        Self: ExecutionAlgorithmNative,
539    {
540        // Generate spawn ID first so we can track the reduction
541        let core = ExecutionAlgorithmNative::exec_algorithm_core_mut(self);
542        let client_order_id = core.spawn_client_order_id(&primary.client_order_id());
543        let ts_init = core.clock_mut().timestamp_ns();
544        let exec_algorithm_id = core.exec_algorithm_id;
545
546        if reduce_primary {
547            self.reduce_primary_order(primary, quantity);
548            ExecutionAlgorithmNative::exec_algorithm_core_mut(self).track_pending_spawn_reduction(
549                client_order_id,
550                primary.client_order_id(),
551                quantity,
552                primary.is_quote_quantity(),
553            );
554        }
555
556        LimitOrder::new(
557            primary.trader_id(),
558            primary.strategy_id(),
559            primary.instrument_id(),
560            client_order_id,
561            primary.order_side(),
562            quantity,
563            price,
564            time_in_force,
565            expire_time,
566            post_only,
567            reduce_only,
568            primary.is_quote_quantity(),
569            display_qty,
570            emulation_trigger,
571            None, // trigger_instrument_id
572            primary.contingency_type(),
573            primary.order_list_id(),
574            primary.linked_order_ids().map(<[ClientOrderId]>::to_vec),
575            primary.parent_order_id(),
576            Some(exec_algorithm_id),
577            primary.exec_algorithm_params().cloned(),
578            Some(primary.client_order_id()),
579            tags.or_else(|| primary.tags().map(<[Ustr]>::to_vec)),
580            UUID4::new(),
581            ts_init,
582        )
583    }
584
585    /// Spawns a market-to-limit order from a primary order.
586    ///
587    /// Creates a new market-to-limit order with:
588    /// - A unique client order ID: `{primary_id}-E{sequence}`
589    /// - The primary order's trader ID, strategy ID, and instrument ID
590    /// - The algorithm's `exec_algorithm_id`
591    /// - `exec_spawn_id` set to the primary order's client order ID
592    ///
593    /// If `reduce_primary` is true, the primary order's quantity is reduced by
594    /// the spawned quantity. Unfilled quantity is restored when the spawn is
595    /// denied, rejected, canceled, expired, or refused before submission while
596    /// the primary remains locally mutable. Converted quote-quantity spawns are
597    /// restored proportionally in primary units. Late fills re-deduct restored
598    /// quantity until primary submission is handed off.
599    ///
600    /// `_emulation_trigger` is accepted for signature parity and is not applied:
601    /// a `MARKET_TO_LIMIT` order is always initialized with no emulation trigger
602    /// and cannot be emulated.
603    #[expect(clippy::too_many_arguments)]
604    fn spawn_market_to_limit(
605        &mut self,
606        primary: &mut OrderAny,
607        quantity: Quantity,
608        time_in_force: TimeInForce,
609        expire_time: Option<UnixNanos>,
610        reduce_only: bool,
611        display_qty: Option<Quantity>,
612        _emulation_trigger: Option<TriggerType>,
613        tags: Option<Vec<Ustr>>,
614        reduce_primary: bool,
615    ) -> MarketToLimitOrder
616    where
617        Self: ExecutionAlgorithmNative,
618    {
619        // Generate spawn ID first so we can track the reduction
620        let core = ExecutionAlgorithmNative::exec_algorithm_core_mut(self);
621        let client_order_id = core.spawn_client_order_id(&primary.client_order_id());
622        let ts_init = core.clock_mut().timestamp_ns();
623        let exec_algorithm_id = core.exec_algorithm_id;
624
625        if reduce_primary {
626            self.reduce_primary_order(primary, quantity);
627            ExecutionAlgorithmNative::exec_algorithm_core_mut(self).track_pending_spawn_reduction(
628                client_order_id,
629                primary.client_order_id(),
630                quantity,
631                primary.is_quote_quantity(),
632            );
633        }
634
635        MarketToLimitOrder::new(
636            primary.trader_id(),
637            primary.strategy_id(),
638            primary.instrument_id(),
639            client_order_id,
640            primary.order_side(),
641            quantity,
642            time_in_force,
643            expire_time,
644            false, // post_only
645            reduce_only,
646            primary.is_quote_quantity(),
647            display_qty,
648            primary.contingency_type(),
649            primary.order_list_id(),
650            primary.linked_order_ids().map(<[ClientOrderId]>::to_vec),
651            primary.parent_order_id(),
652            Some(exec_algorithm_id),
653            primary.exec_algorithm_params().cloned(),
654            Some(primary.client_order_id()),
655            tags.or_else(|| primary.tags().map(<[Ustr]>::to_vec)),
656            UUID4::new(),
657            ts_init,
658        )
659    }
660
661    /// Reduces the primary order's quantity by the spawn quantity.
662    ///
663    /// Generates an `OrderUpdated` event and applies it to the primary order,
664    /// then updates the order in the cache.
665    ///
666    /// # Panics
667    ///
668    /// Panics if `spawn_qty` exceeds the primary order's `leaves_qty`.
669    fn reduce_primary_order(&mut self, primary: &mut OrderAny, spawn_qty: Quantity)
670    where
671        Self: ExecutionAlgorithmNative,
672    {
673        let leaves_qty = primary.leaves_qty();
674        assert!(
675            leaves_qty >= spawn_qty,
676            "Spawn quantity {spawn_qty} exceeds primary leaves_qty {leaves_qty}"
677        );
678
679        let primary_qty = primary.quantity();
680        let mut new_qty = primary_qty - spawn_qty;
681        new_qty.precision = primary_qty.precision;
682
683        let core = ExecutionAlgorithmNative::exec_algorithm_core_mut(self);
684        let ts_now = core.clock_mut().timestamp_ns();
685
686        let updated = OrderUpdated::new(
687            primary.trader_id(),
688            primary.strategy_id(),
689            primary.instrument_id(),
690            primary.client_order_id(),
691            new_qty,
692            UUID4::new(),
693            ts_now,
694            ts_now,
695            false, // reconciliation
696            primary.venue_order_id(),
697            primary.account_id(),
698            None, // price
699            None, // trigger_price
700            None, // protection_price
701            primary.is_quote_quantity(),
702        );
703
704        let event = OrderEventAny::Updated(updated);
705
706        {
707            let cache_rc = core.cache_rc();
708            let mut cache = cache_rc.borrow_mut();
709            *primary = cache
710                .update_order(&event)
711                .expect("Failed to update order in cache");
712        }
713
714        publish_order_event(&event);
715    }
716
717    /// Restores a spawn reduction while the cached primary order remains local.
718    ///
719    /// The quantity deducted from the cached primary order is restored up to the
720    /// spawned order's unfilled proportion in primary units. Tracked fill voids
721    /// return only the additional budget released by the correction. Primaries handed
722    /// off for submission retain their committed quantity. Uncompensated
723    /// late-fill debt on the primary is discharged before quantity is returned.
724    ///
725    /// `refused_before_submission` selects whether the restoration log records a
726    /// refusal or an order update.
727    fn restore_primary_order_quantity(&mut self, order: &OrderAny, refused_before_submission: bool)
728    where
729        Self: ExecutionAlgorithmNative,
730    {
731        let Some(exec_spawn_id) = order.exec_spawn_id() else {
732            return;
733        };
734
735        let reduction = {
736            let core = ExecutionAlgorithmNative::exec_algorithm_core_mut(self);
737            core.spawn_reduction(order.client_order_id())
738        };
739
740        let Some(mut reduction) = reduction else {
741            return;
742        };
743
744        let primary = {
745            let cache = ExecutionAlgorithmNative::exec_algorithm_core_mut(self).cache_ref();
746            cache
747                .order(&exec_spawn_id)
748                .map(|o| <OrderAny as Clone>::clone(&o))
749        };
750
751        let Some(primary) = primary else {
752            ExecutionAlgorithmNative::exec_algorithm_core_mut(self)
753                .take_pending_spawn_reduction(order.client_order_id());
754            log::warn!(
755                "Cannot restore primary order quantity: primary order {exec_spawn_id} not found",
756            );
757            return;
758        };
759
760        let handed_off = ExecutionAlgorithmNative::exec_algorithm_core_mut(self)
761            .primary_was_handed_off(exec_spawn_id);
762
763        if !primary.is_active_local() || handed_off {
764            let core = ExecutionAlgorithmNative::exec_algorithm_core_mut(self);
765            core.take_pending_spawn_reduction(order.client_order_id());
766            core.discard_spawn_fill_debt(exec_spawn_id);
767            log::info!(
768                "Skipped restoring primary order {exec_spawn_id} after spawned order {}: primary is no longer locally mutable",
769                order.client_order_id(),
770            );
771            return;
772        }
773
774        let Some(unfilled_qty) =
775            spawn_unfilled_quantity(order, reduction, primary.quantity().precision)
776        else {
777            ExecutionAlgorithmNative::exec_algorithm_core_mut(self)
778                .take_pending_spawn_reduction(order.client_order_id());
779            return;
780        };
781        let restored_qty = reduction
782            .restored_qty
783            .unwrap_or_else(|| Quantity::zero(unfilled_qty.precision));
784        let restore_qty = unfilled_qty.saturating_sub(restored_qty);
785        reduction.restored_qty = Some(unfilled_qty);
786
787        if restore_qty.is_zero() {
788            ExecutionAlgorithmNative::exec_algorithm_core_mut(self)
789                .set_spawn_reduction(order.client_order_id(), reduction);
790            return;
791        }
792
793        // Discharge uncompensated late-fill debt on this primary before
794        // returning quantity to it
795        let debt_qty = ExecutionAlgorithmNative::exec_algorithm_core_mut(self)
796            .spawn_fill_debt(exec_spawn_id)
797            .unwrap_or_else(|| Quantity::zero(restore_qty.precision));
798        let discharge_qty = restore_qty.min(debt_qty);
799        let net_restore_qty = restore_qty - discharge_qty;
800
801        if net_restore_qty.is_zero() {
802            // The whole restoration discharged debt: keep the record with the
803            // gross released amount so this child's own late fills stay tracked
804            let core = ExecutionAlgorithmNative::exec_algorithm_core_mut(self);
805            core.set_spawn_reduction(order.client_order_id(), reduction);
806            core.set_spawn_fill_debt(exec_spawn_id, debt_qty - discharge_qty);
807            log::info!(
808                "Restoration from spawned order {} fully discharged late-fill debt on primary order {exec_spawn_id}",
809                order.client_order_id(),
810            );
811            return;
812        }
813
814        let mut restored_qty = primary.quantity() + net_restore_qty;
815        restored_qty.precision = primary.quantity().precision;
816
817        let core = ExecutionAlgorithmNative::exec_algorithm_core_mut(self);
818        let ts_now = core.clock_mut().timestamp_ns();
819
820        let updated = OrderUpdated::new(
821            primary.trader_id(),
822            primary.strategy_id(),
823            primary.instrument_id(),
824            primary.client_order_id(),
825            restored_qty,
826            UUID4::new(),
827            ts_now,
828            ts_now,
829            false, // reconciliation
830            primary.venue_order_id(),
831            primary.account_id(),
832            None, // price
833            None, // trigger_price
834            None, // protection_price
835            primary.is_quote_quantity(),
836        );
837
838        let event = OrderEventAny::Updated(updated);
839
840        let primary = {
841            let cache_rc = core.cache_rc();
842            let mut cache = cache_rc.borrow_mut();
843            match cache.update_order(&event) {
844                Ok(primary) => primary,
845                Err(e) => {
846                    log::warn!("Failed to update primary order in cache: {e}");
847                    return;
848                }
849            }
850        };
851
852        // Commit the lifecycle record and debt before publishing: subscribers
853        // run synchronously and may re-enter order handling. The record keeps
854        // the gross released amount (including the debt-discharged portion) as
855        // this child's late-fill accounting budget; only the net reaches the
856        // primary.
857        let core = ExecutionAlgorithmNative::exec_algorithm_core_mut(self);
858        core.set_spawn_reduction(order.client_order_id(), reduction);
859        if !discharge_qty.is_zero() {
860            core.set_spawn_fill_debt(exec_spawn_id, debt_qty - discharge_qty);
861        }
862
863        publish_order_event(&event);
864
865        let outcome = if refused_before_submission {
866            "refused before submission"
867        } else {
868            "updated with unfilled quantity"
869        };
870        log::info!(
871            "Restored primary order {} quantity to {} after spawned order {} was {outcome}",
872            primary.client_order_id(),
873            restored_qty,
874            order.client_order_id()
875        );
876    }
877
878    /// Re-deducts a late spawn fill from a previously restored local primary order.
879    fn rededuct_late_spawn_fill(&mut self, order: &OrderAny)
880    where
881        Self: ExecutionAlgorithmNative,
882    {
883        let spawn_id = order.client_order_id();
884        let Some(exec_spawn_id) = order.exec_spawn_id() else {
885            return;
886        };
887        let Some(reduction) =
888            ExecutionAlgorithmNative::exec_algorithm_core_mut(self).spawn_reduction(spawn_id)
889        else {
890            return;
891        };
892
893        let Some(restored_qty) = reduction.restored_qty else {
894            return;
895        };
896
897        let primary = {
898            let cache = ExecutionAlgorithmNative::exec_algorithm_core_mut(self).cache_ref();
899            cache
900                .order(&exec_spawn_id)
901                .map(|o| <OrderAny as Clone>::clone(&o))
902        };
903        let Some(primary) = primary else {
904            ExecutionAlgorithmNative::exec_algorithm_core_mut(self)
905                .take_pending_spawn_reduction(spawn_id);
906            log::warn!(
907                "Cannot re-deduct late fill from primary order {exec_spawn_id}: order not found",
908            );
909            return;
910        };
911
912        let handed_off = ExecutionAlgorithmNative::exec_algorithm_core_mut(self)
913            .primary_was_handed_off(exec_spawn_id);
914
915        if !primary.is_active_local() || handed_off {
916            let core = ExecutionAlgorithmNative::exec_algorithm_core_mut(self);
917            core.take_pending_spawn_reduction(spawn_id);
918            core.discard_spawn_fill_debt(exec_spawn_id);
919            log::info!(
920                "Skipped re-deducting late fill from primary order {exec_spawn_id}: primary is no longer locally mutable",
921            );
922            return;
923        }
924
925        let Some(unfilled_qty) =
926            spawn_unfilled_quantity(order, reduction, primary.quantity().precision)
927        else {
928            ExecutionAlgorithmNative::exec_algorithm_core_mut(self)
929                .take_pending_spawn_reduction(spawn_id);
930            return;
931        };
932        let uncapped_qty = restored_qty.saturating_sub(unfilled_qty);
933        let primary_qty = primary.quantity();
934        // Restored quantity may already have been reused by a later spawn, so
935        // cap at the primary's remaining quantity; the shortfall becomes debt
936        // discharged against later spawn restorations for this primary.
937        let rededuct_qty = uncapped_qty.min(primary_qty);
938        let shortfall_qty = uncapped_qty - rededuct_qty;
939        if rededuct_qty.is_zero() {
940            if !uncapped_qty.is_zero() {
941                charge_spawn_reduction(
942                    ExecutionAlgorithmNative::exec_algorithm_core_mut(self),
943                    spawn_id,
944                    exec_spawn_id,
945                    reduction,
946                    unfilled_qty,
947                    shortfall_qty,
948                );
949                log::warn!(
950                    "Cannot re-deduct late fill on spawned order {spawn_id} from primary order {exec_spawn_id}: primary quantity exhausted, shortfall recorded as debt",
951                );
952            }
953            return;
954        }
955        let mut new_qty = primary_qty - rededuct_qty;
956        new_qty.precision = primary_qty.precision;
957        let core = ExecutionAlgorithmNative::exec_algorithm_core_mut(self);
958        let ts_now = core.clock_mut().timestamp_ns();
959        let event = OrderEventAny::Updated(OrderUpdated::new(
960            primary.trader_id(),
961            primary.strategy_id(),
962            primary.instrument_id(),
963            primary.client_order_id(),
964            new_qty,
965            UUID4::new(),
966            ts_now,
967            ts_now,
968            false,
969            primary.venue_order_id(),
970            primary.account_id(),
971            None,
972            None,
973            None,
974            primary.is_quote_quantity(),
975        ));
976
977        if let Err(e) = core.cache_rc().borrow_mut().update_order(&event) {
978            log::warn!("Failed to update primary order in cache: {e}");
979            return;
980        }
981
982        // Commit the lifecycle record before publishing: subscribers run
983        // synchronously and may re-enter order handling.
984        charge_spawn_reduction(
985            ExecutionAlgorithmNative::exec_algorithm_core_mut(self),
986            spawn_id,
987            exec_spawn_id,
988            reduction,
989            unfilled_qty,
990            shortfall_qty,
991        );
992
993        if !shortfall_qty.is_zero() {
994            log::warn!(
995                "Late fill on spawned order {spawn_id} partially re-deducted from primary order {exec_spawn_id}: shortfall recorded as debt",
996            );
997        }
998
999        publish_order_event(&event);
1000    }
1001
1002    /// Submits an order to the execution engine via the risk engine.
1003    ///
1004    /// Orders carrying a live emulation trigger are refused before submission.
1005    /// For spawned orders with a pending primary reduction, refusal restores the
1006    /// cached primary order quantity while it remains local and publishes
1007    /// `OrderUpdated`.
1008    ///
1009    /// # Errors
1010    ///
1011    /// Returns an error if the order carries a live emulation trigger or submission fails.
1012    fn submit_order(
1013        &mut self,
1014        order: OrderAny,
1015        position_id: Option<PositionId>,
1016        client_id: Option<ClientId>,
1017    ) -> anyhow::Result<()>
1018    where
1019        Self: ExecutionAlgorithmNative,
1020    {
1021        let trader_id =
1022            registered_trader_id(ExecutionAlgorithmNative::exec_algorithm_core_mut(self))?;
1023
1024        if order.emulation_trigger().is_some() {
1025            let client_order_id = order.client_order_id();
1026            self.restore_primary_order_quantity(&order, true);
1027            return Err(EmulatedOrderSubmissionError { client_order_id }.into());
1028        }
1029
1030        let core = ExecutionAlgorithmNative::exec_algorithm_core_mut(self);
1031        let ts_init = core.clock_mut().timestamp_ns();
1032
1033        // For spawned orders, use the parent's strategy ID
1034        let strategy_id = order.strategy_id();
1035
1036        let primary_id = order
1037            .exec_spawn_id()
1038            .unwrap_or_else(|| order.client_order_id());
1039        let params = core.submit_params(&primary_id);
1040
1041        let order_exists = {
1042            let cache = core.cache_ref();
1043            cache.order_exists(&order.client_order_id())
1044        };
1045
1046        {
1047            let cache_rc = core.cache_rc();
1048            let mut cache = cache_rc.borrow_mut();
1049            cache.add_order(order.clone(), position_id, client_id, true)?;
1050        }
1051
1052        if !order_exists {
1053            publish_order_initialized(&order);
1054        }
1055
1056        let command = SubmitOrder::new(
1057            trader_id,
1058            client_id,
1059            strategy_id,
1060            order.instrument_id(),
1061            order.client_order_id(),
1062            order.init_event().clone(),
1063            order.exec_algorithm_id(),
1064            position_id,
1065            params,
1066            UUID4::new(),
1067            ts_init,
1068            None, // correlation_id
1069        );
1070
1071        if core.config.log_commands {
1072            let id = &core.actor.actor_id;
1073            log::info!("{id} {SEND}{CMD} {command}");
1074        }
1075
1076        if order.is_primary() {
1077            core.mark_primary_handed_off(order.client_order_id());
1078        }
1079
1080        msgbus::send_trading_command(
1081            MessagingSwitchboard::risk_engine_queue_execute(),
1082            TradingCommand::SubmitOrder(command),
1083        );
1084
1085        Ok(())
1086    }
1087
1088    /// Modifies an order.
1089    ///
1090    /// # Errors
1091    ///
1092    /// Returns an error if all supplied values are absent or unchanged, or if
1093    /// order modification fails.
1094    fn modify_order(
1095        &mut self,
1096        order: &mut OrderAny,
1097        quantity: Option<Quantity>,
1098        price: Option<Price>,
1099        trigger_price: Option<Price>,
1100        client_id: Option<ClientId>,
1101    ) -> anyhow::Result<()>
1102    where
1103        Self: ExecutionAlgorithmNative,
1104    {
1105        let qty_changing = quantity.is_some_and(|q| q != order.quantity());
1106        let price_changing = price.is_some() && price != order.price();
1107        let trigger_changing = trigger_price.is_some() && trigger_price != order.trigger_price();
1108
1109        if !qty_changing && !price_changing && !trigger_changing {
1110            anyhow::bail!(
1111                "Cannot create command ModifyOrder: \
1112                quantity, price, and trigger were either None \
1113                or the same as existing values"
1114            );
1115        }
1116
1117        if order.is_closed() || order.is_pending_cancel() {
1118            log::warn!(
1119                "Cannot create command ModifyOrder: state is {:?}, {order:?}",
1120                order.status()
1121            );
1122            return Ok(());
1123        }
1124
1125        let core = ExecutionAlgorithmNative::exec_algorithm_core_mut(self);
1126        let trader_id = registered_trader_id(core)?;
1127        let strategy_id = order.strategy_id();
1128
1129        if !order.is_active_local() {
1130            required_account_id(order, "pending update")?;
1131            let event = self.generate_order_pending_update(order);
1132            let event = OrderEventAny::PendingUpdate(event);
1133
1134            {
1135                let cache_rc = ExecutionAlgorithmNative::exec_algorithm_core_mut(self).cache_rc();
1136                let mut cache = cache_rc.borrow_mut();
1137                match cache.update_order(&event) {
1138                    Ok(updated) => *order = updated,
1139                    Err(e)
1140                        if matches!(
1141                            e.downcast_ref::<OrderError>(),
1142                            Some(OrderError::InvalidStateTransition)
1143                        ) =>
1144                    {
1145                        log::warn!("InvalidStateTrigger: {e}, did not apply pending update event");
1146                        return Ok(());
1147                    }
1148                    Err(e) => return Err(e),
1149                }
1150            }
1151
1152            let topic = format!("events.order.{strategy_id}");
1153            msgbus::publish_order_event(topic.into(), &event);
1154            msgbus::publish_order_event(
1155                msgbus::switchboard::get_order_pending_update_topic(order.instrument_id()),
1156                &event,
1157            );
1158        }
1159
1160        let ts_init = ExecutionAlgorithmNative::exec_algorithm_core_mut(self)
1161            .clock_mut()
1162            .timestamp_ns();
1163        let command = ModifyOrder::new(
1164            trader_id,
1165            client_id,
1166            strategy_id,
1167            order.instrument_id(),
1168            order.client_order_id(),
1169            order.venue_order_id(),
1170            quantity,
1171            price,
1172            trigger_price,
1173            UUID4::new(),
1174            ts_init,
1175            None, // params,
1176            None, // correlation_id
1177        );
1178
1179        if ExecutionAlgorithmNative::exec_algorithm_core_mut(self)
1180            .config
1181            .log_commands
1182        {
1183            let id = &ExecutionAlgorithmNative::exec_algorithm_core_mut(self)
1184                .actor
1185                .actor_id;
1186            log::info!("{id} {SEND}{CMD} {command}");
1187        }
1188
1189        let has_emulation_trigger = order.emulation_trigger().is_some();
1190
1191        if order.is_emulated() || has_emulation_trigger {
1192            msgbus::send_trading_command(
1193                MessagingSwitchboard::order_emulator_execute(),
1194                TradingCommand::ModifyOrder(command),
1195            );
1196        } else {
1197            msgbus::send_trading_command(
1198                MessagingSwitchboard::risk_engine_queue_execute(),
1199                TradingCommand::ModifyOrder(command),
1200            );
1201        }
1202
1203        Ok(())
1204    }
1205
1206    /// Modifies an INITIALIZED or RELEASED order in place without sending a command.
1207    ///
1208    /// This is useful for adjusting order parameters before submission. The order
1209    /// is updated locally by applying an `OrderUpdated` event and updating the cache.
1210    ///
1211    /// At least one parameter must differ from the current order values.
1212    ///
1213    /// # Errors
1214    ///
1215    /// Returns an error if the order status is not INITIALIZED or RELEASED,
1216    /// or if no parameters would change.
1217    fn modify_order_in_place(
1218        &mut self,
1219        order: &mut OrderAny,
1220        quantity: Option<Quantity>,
1221        price: Option<Price>,
1222        trigger_price: Option<Price>,
1223    ) -> anyhow::Result<()>
1224    where
1225        Self: ExecutionAlgorithmNative,
1226    {
1227        // Validate order status
1228        let status = order.status();
1229        if status != OrderStatus::Initialized && status != OrderStatus::Released {
1230            anyhow::bail!(
1231                "Cannot modify order in place: status is {status:?}, expected INITIALIZED or RELEASED"
1232            );
1233        }
1234
1235        // Validate order type compatibility
1236        if price.is_some() && order.price().is_none() {
1237            anyhow::bail!(
1238                "Cannot modify order in place: {} orders do not have a LIMIT price",
1239                order.order_type()
1240            );
1241        }
1242
1243        if trigger_price.is_some() && order.trigger_price().is_none() {
1244            anyhow::bail!(
1245                "Cannot modify order in place: {} orders do not have a STOP trigger price",
1246                order.order_type()
1247            );
1248        }
1249
1250        // Check if any value would actually change
1251        let qty_changing = quantity.is_some_and(|q| q != order.quantity());
1252        let price_changing = price.is_some() && price != order.price();
1253        let trigger_changing = trigger_price.is_some() && trigger_price != order.trigger_price();
1254
1255        if !qty_changing && !price_changing && !trigger_changing {
1256            anyhow::bail!("Cannot modify order in place: no parameters differ from current values");
1257        }
1258
1259        let core = ExecutionAlgorithmNative::exec_algorithm_core_mut(self);
1260        let ts_now = core.clock_mut().timestamp_ns();
1261
1262        let updated = OrderUpdated::new(
1263            order.trader_id(),
1264            order.strategy_id(),
1265            order.instrument_id(),
1266            order.client_order_id(),
1267            quantity.unwrap_or_else(|| order.quantity()),
1268            UUID4::new(),
1269            ts_now,
1270            ts_now,
1271            false, // reconciliation
1272            order.venue_order_id(),
1273            order.account_id(),
1274            price,
1275            trigger_price,
1276            None, // protection_price
1277            order.is_quote_quantity(),
1278        );
1279
1280        let event = OrderEventAny::Updated(updated);
1281
1282        {
1283            let cache_rc = core.cache_rc();
1284            let mut cache = cache_rc.borrow_mut();
1285            *order = cache.update_order(&event)?;
1286        }
1287
1288        publish_order_event(&event);
1289
1290        Ok(())
1291    }
1292
1293    /// Cancels an order.
1294    ///
1295    /// # Errors
1296    ///
1297    /// Returns an error if order cancellation fails.
1298    fn cancel_order(
1299        &mut self,
1300        order: &mut OrderAny,
1301        client_id: Option<ClientId>,
1302    ) -> anyhow::Result<()>
1303    where
1304        Self: ExecutionAlgorithmNative,
1305    {
1306        if order.is_closed() || order.is_pending_cancel() {
1307            log::warn!(
1308                "Cannot cancel order: state is {:?}, {order:?}",
1309                order.status()
1310            );
1311            return Ok(());
1312        }
1313
1314        let core = ExecutionAlgorithmNative::exec_algorithm_core_mut(self);
1315        let trader_id = registered_trader_id(core)?;
1316        let strategy_id = order.strategy_id();
1317
1318        if !order.is_active_local() {
1319            required_account_id(order, "pending cancel")?;
1320            let event = self.generate_order_pending_cancel(order);
1321            let event = OrderEventAny::PendingCancel(event);
1322
1323            {
1324                let cache_rc = ExecutionAlgorithmNative::exec_algorithm_core_mut(self).cache_rc();
1325                let mut cache = cache_rc.borrow_mut();
1326                match cache.update_order(&event) {
1327                    Ok(updated) => *order = updated,
1328                    Err(e)
1329                        if matches!(
1330                            e.downcast_ref::<OrderError>(),
1331                            Some(OrderError::InvalidStateTransition)
1332                        ) =>
1333                    {
1334                        log::warn!("InvalidStateTrigger: {e}, did not apply pending cancel event");
1335                        return Ok(());
1336                    }
1337                    Err(e) => return Err(e),
1338                }
1339            }
1340
1341            let topic = format!("events.order.{strategy_id}");
1342            msgbus::publish_order_event(topic.into(), &event);
1343            msgbus::publish_order_event(
1344                msgbus::switchboard::get_order_pending_cancel_topic(order.instrument_id()),
1345                &event,
1346            );
1347        }
1348
1349        let ts_init = ExecutionAlgorithmNative::exec_algorithm_core_mut(self)
1350            .clock_mut()
1351            .timestamp_ns();
1352        let command = CancelOrder::new(
1353            trader_id,
1354            client_id,
1355            strategy_id,
1356            order.instrument_id(),
1357            order.client_order_id(),
1358            order.venue_order_id(),
1359            UUID4::new(),
1360            ts_init,
1361            None, // params,
1362            None, // correlation_id
1363        );
1364
1365        if ExecutionAlgorithmNative::exec_algorithm_core_mut(self)
1366            .config
1367            .log_commands
1368        {
1369            let id = &ExecutionAlgorithmNative::exec_algorithm_core_mut(self)
1370                .actor
1371                .actor_id;
1372            log::info!("{id} {SEND}{CMD} {command}");
1373        }
1374
1375        let has_emulation_trigger = order.emulation_trigger().is_some();
1376
1377        if order.is_emulated() || order.status() == OrderStatus::Released || has_emulation_trigger {
1378            msgbus::send_trading_command(
1379                MessagingSwitchboard::order_emulator_execute(),
1380                TradingCommand::CancelOrder(command),
1381            );
1382        } else {
1383            msgbus::send_trading_command(
1384                MessagingSwitchboard::exec_engine_queue_execute(),
1385                TradingCommand::CancelOrder(command),
1386            );
1387        }
1388
1389        Ok(())
1390    }
1391
1392    /// Subscribes to events from a strategy.
1393    ///
1394    /// This is called automatically when the first order is received from a strategy.
1395    fn subscribe_to_strategy_events(&mut self, strategy_id: StrategyId)
1396    where
1397        Self: ExecutionAlgorithmNative,
1398        Self: 'static + std::fmt::Debug + Sized,
1399    {
1400        let core = ExecutionAlgorithmNative::exec_algorithm_core_mut(self);
1401        if core.is_strategy_subscribed(&strategy_id) {
1402            return;
1403        }
1404
1405        let actor_id = core.actor.actor_id.inner();
1406
1407        let order_topic = format!("events.order.{strategy_id}");
1408        let order_actor_id = actor_id;
1409        let order_handler = TypedHandler::from(move |event: &OrderEventAny| {
1410            if let Some(mut algo) = try_get_actor_unchecked::<Self>(&order_actor_id) {
1411                algo.handle_order_event(event.clone());
1412            } else {
1413                log::error!(
1414                    "ExecutionAlgorithm {order_actor_id} not found for order event handling"
1415                );
1416            }
1417        });
1418        msgbus::subscribe_order_events(order_topic.clone().into(), order_handler.clone(), None);
1419
1420        let position_topic = format!("events.position.{strategy_id}");
1421        let position_handler = TypedHandler::from(move |event: &PositionEvent| {
1422            if let Some(mut algo) = try_get_actor_unchecked::<Self>(&actor_id) {
1423                algo.handle_position_event(event.clone());
1424            } else {
1425                log::error!("ExecutionAlgorithm {actor_id} not found for position event handling");
1426            }
1427        });
1428        msgbus::subscribe_position_events(
1429            position_topic.clone().into(),
1430            position_handler.clone(),
1431            None,
1432        );
1433
1434        let handlers = StrategyEventHandlers {
1435            order_topic,
1436            order_handler,
1437            position_topic,
1438            position_handler,
1439        };
1440        core.store_strategy_event_handlers(strategy_id, handlers);
1441
1442        core.add_subscribed_strategy(strategy_id);
1443        log::info!("Subscribed to events for strategy {strategy_id}");
1444    }
1445
1446    /// Unsubscribes from all strategy event handlers.
1447    ///
1448    /// This should be called before reset to properly clean up msgbus subscriptions.
1449    fn unsubscribe_all_strategy_events(&mut self)
1450    where
1451        Self: ExecutionAlgorithmNative,
1452    {
1453        let handlers =
1454            ExecutionAlgorithmNative::exec_algorithm_core_mut(self).take_strategy_event_handlers();
1455
1456        for (strategy_id, h) in handlers {
1457            msgbus::unsubscribe_order_events(h.order_topic.into(), &h.order_handler);
1458            msgbus::unsubscribe_position_events(h.position_topic.into(), &h.position_handler);
1459            log::info!("Unsubscribed from events for strategy {strategy_id}");
1460        }
1461        ExecutionAlgorithmNative::exec_algorithm_core_mut(self).clear_subscribed_strategies();
1462    }
1463
1464    /// Handles an order event, filtering for algorithm-owned orders.
1465    fn handle_order_event(&mut self, event: OrderEventAny)
1466    where
1467        Self: ExecutionAlgorithmNative,
1468    {
1469        if DataActorNative::core(ExecutionAlgorithmNative::exec_algorithm_core_mut(self)).state()
1470            != ComponentState::Running
1471        {
1472            return;
1473        }
1474
1475        let order = {
1476            let cache = ExecutionAlgorithmNative::exec_algorithm_core_mut(self).cache_ref();
1477            cache.order(&event.client_order_id()).map(|o| o.clone())
1478        };
1479
1480        let Some(order) = order else {
1481            return;
1482        };
1483
1484        let Some(order_algo_id) = order.exec_algorithm_id() else {
1485            return;
1486        };
1487
1488        if order_algo_id != self.id() {
1489            return;
1490        }
1491
1492        {
1493            let core = ExecutionAlgorithmNative::exec_algorithm_core_mut(self);
1494            if core.config.log_events {
1495                let id = &core.actor.actor_id;
1496                log::info!("{id} {RECV}{EVT} {event}");
1497            }
1498        }
1499
1500        if order.is_primary() && !order.is_active_local() {
1501            ExecutionAlgorithmNative::exec_algorithm_core_mut(self)
1502                .clear_primary_spawn_state(order.client_order_id());
1503        }
1504
1505        match &event {
1506            OrderEventAny::Initialized(e) => self.on_order_initialized(e.clone()),
1507            OrderEventAny::Denied(e) => {
1508                self.restore_primary_order_quantity(&order, false);
1509                self.on_order_denied(*e);
1510            }
1511            OrderEventAny::Emulated(e) => self.on_order_emulated(*e),
1512            OrderEventAny::Released(e) => self.on_order_released(*e),
1513            OrderEventAny::Submitted(e) => self.on_order_submitted(*e),
1514            OrderEventAny::Rejected(e) => {
1515                self.restore_primary_order_quantity(&order, false);
1516                self.on_order_rejected(*e);
1517            }
1518            OrderEventAny::Accepted(e) => self.on_order_accepted(*e),
1519            OrderEventAny::Canceled(e) => {
1520                self.restore_primary_order_quantity(&order, false);
1521                self.on_algo_order_canceled(*e);
1522            }
1523            OrderEventAny::Expired(e) => {
1524                self.restore_primary_order_quantity(&order, false);
1525                self.on_order_expired(*e);
1526            }
1527            OrderEventAny::Triggered(e) => self.on_order_triggered(*e),
1528            OrderEventAny::PendingUpdate(e) => self.on_order_pending_update(*e),
1529            OrderEventAny::PendingCancel(e) => self.on_order_pending_cancel(*e),
1530            OrderEventAny::ModifyRejected(e) => self.on_order_modify_rejected(*e),
1531            OrderEventAny::CancelRejected(e) => self.on_order_cancel_rejected(*e),
1532            OrderEventAny::Updated(e) => self.on_order_updated(*e),
1533            OrderEventAny::Filled(e) => {
1534                self.rededuct_late_spawn_fill(&order);
1535                if order.leaves_qty().is_zero() {
1536                    let core = ExecutionAlgorithmNative::exec_algorithm_core_mut(self);
1537                    if core
1538                        .spawn_reduction(order.client_order_id())
1539                        .is_some_and(|reduction| reduction.restored_qty.is_none())
1540                    {
1541                        core.take_pending_spawn_reduction(order.client_order_id());
1542                    }
1543                }
1544                self.on_algo_order_filled(e.clone());
1545            }
1546            OrderEventAny::FillVoided(e) => {
1547                if ExecutionAlgorithmNative::exec_algorithm_core_mut(self)
1548                    .spawn_reduction(order.client_order_id())
1549                    .is_some_and(|reduction| reduction.restored_qty.is_some())
1550                {
1551                    self.restore_primary_order_quantity(&order, false);
1552                }
1553                self.on_order_fill_voided(e);
1554            }
1555        }
1556
1557        self.on_order_event(event);
1558    }
1559
1560    /// Handles a position event.
1561    fn handle_position_event(&mut self, event: PositionEvent)
1562    where
1563        Self: ExecutionAlgorithmNative,
1564    {
1565        if DataActorNative::core(ExecutionAlgorithmNative::exec_algorithm_core_mut(self)).state()
1566            != ComponentState::Running
1567        {
1568            return;
1569        }
1570
1571        {
1572            let core = ExecutionAlgorithmNative::exec_algorithm_core_mut(self);
1573            if core.config.log_events {
1574                let id = &core.actor.actor_id;
1575                log::info!("{id} {RECV}{EVT} {event:?}");
1576            }
1577        }
1578
1579        match &event {
1580            PositionEvent::PositionOpened(e) => self.on_position_opened(e.clone()),
1581            PositionEvent::PositionChanged(e) => self.on_position_changed(e.clone()),
1582            PositionEvent::PositionClosed(e) => self.on_position_closed(e.clone()),
1583            PositionEvent::PositionAdjusted(_) => {}
1584        }
1585
1586        self.on_position_event(event);
1587    }
1588
1589    /// Called when the algorithm is started.
1590    ///
1591    /// Override this method to implement custom initialization logic.
1592    ///
1593    /// # Errors
1594    ///
1595    /// Returns an error if start fails.
1596    fn on_start(&mut self) -> anyhow::Result<()>
1597    where
1598        Self: ExecutionAlgorithmNative,
1599    {
1600        let id = self.id();
1601        log::info!("Starting {id}");
1602        Ok(())
1603    }
1604
1605    /// Called when the algorithm is stopped.
1606    ///
1607    /// # Errors
1608    ///
1609    /// Returns an error if stop fails.
1610    fn on_stop(&mut self) -> anyhow::Result<()> {
1611        Ok(())
1612    }
1613
1614    /// Called when the algorithm is resumed.
1615    ///
1616    /// # Errors
1617    ///
1618    /// Returns an error if resume fails.
1619    fn on_resume(&mut self) -> anyhow::Result<()> {
1620        Ok(())
1621    }
1622
1623    /// Called when the algorithm is reset.
1624    ///
1625    /// # Errors
1626    ///
1627    /// Returns an error if reset fails.
1628    fn on_reset(&mut self) -> anyhow::Result<()>
1629    where
1630        Self: ExecutionAlgorithmNative,
1631    {
1632        self.unsubscribe_all_strategy_events();
1633        ExecutionAlgorithmNative::exec_algorithm_core_mut(self).reset();
1634        Ok(())
1635    }
1636
1637    /// Called when a time event is received.
1638    ///
1639    /// Override this method for timer-based algorithms like TWAP.
1640    ///
1641    /// # Errors
1642    ///
1643    /// Returns an error if time event handling fails.
1644    fn on_time_event(&mut self, _event: &TimeEvent) -> anyhow::Result<()> {
1645        Ok(())
1646    }
1647
1648    /// Called when an order is initialized.
1649    #[allow(unused_variables)]
1650    fn on_order_initialized(&mut self, event: OrderInitialized) {}
1651
1652    /// Called when an order is denied.
1653    #[allow(unused_variables)]
1654    fn on_order_denied(&mut self, event: OrderDenied) {}
1655
1656    /// Called when an order is emulated.
1657    #[allow(unused_variables)]
1658    fn on_order_emulated(&mut self, event: OrderEmulated) {}
1659
1660    /// Called when an order is released from emulation.
1661    #[allow(unused_variables)]
1662    fn on_order_released(&mut self, event: OrderReleased) {}
1663
1664    /// Called when an order is submitted.
1665    #[allow(unused_variables)]
1666    fn on_order_submitted(&mut self, event: OrderSubmitted) {}
1667
1668    /// Called when an order is rejected.
1669    #[allow(unused_variables)]
1670    fn on_order_rejected(&mut self, event: OrderRejected) {}
1671
1672    /// Called when an order is accepted.
1673    #[allow(unused_variables)]
1674    fn on_order_accepted(&mut self, event: OrderAccepted) {}
1675
1676    /// Called when an order is canceled.
1677    #[allow(unused_variables)]
1678    fn on_algo_order_canceled(&mut self, event: OrderCanceled) {}
1679
1680    /// Called when an order expires.
1681    #[allow(unused_variables)]
1682    fn on_order_expired(&mut self, event: OrderExpired) {}
1683
1684    /// Called when an order is triggered.
1685    #[allow(unused_variables)]
1686    fn on_order_triggered(&mut self, event: OrderTriggered) {}
1687
1688    /// Called when an order modification is pending.
1689    #[allow(unused_variables)]
1690    fn on_order_pending_update(&mut self, event: OrderPendingUpdate) {}
1691
1692    /// Called when an order cancellation is pending.
1693    #[allow(unused_variables)]
1694    fn on_order_pending_cancel(&mut self, event: OrderPendingCancel) {}
1695
1696    /// Called when an order modification is rejected.
1697    #[allow(unused_variables)]
1698    fn on_order_modify_rejected(&mut self, event: OrderModifyRejected) {}
1699
1700    /// Called when an order cancellation is rejected.
1701    #[allow(unused_variables)]
1702    fn on_order_cancel_rejected(&mut self, event: OrderCancelRejected) {}
1703
1704    /// Called when an order is updated.
1705    #[allow(unused_variables)]
1706    fn on_order_updated(&mut self, event: OrderUpdated) {}
1707
1708    /// Called when an order is filled.
1709    #[allow(unused_variables)]
1710    fn on_algo_order_filled(&mut self, event: OrderFilled) {}
1711
1712    /// Called when an applied order fill is partly or fully voided.
1713    #[allow(unused_variables)]
1714    fn on_order_fill_voided(&mut self, event: &OrderFillVoided) {}
1715
1716    /// Called for any order event (after specific handler).
1717    #[allow(unused_variables)]
1718    fn on_order_event(&mut self, event: OrderEventAny) {}
1719
1720    /// Called when a position is opened.
1721    #[allow(unused_variables)]
1722    fn on_position_opened(&mut self, event: PositionOpened) {}
1723
1724    /// Called when a position is changed.
1725    #[allow(unused_variables)]
1726    fn on_position_changed(&mut self, event: PositionChanged) {}
1727
1728    /// Called when a position is closed.
1729    #[allow(unused_variables)]
1730    fn on_position_closed(&mut self, event: PositionClosed) {}
1731
1732    /// Called for any position event (after specific handler).
1733    #[allow(unused_variables)]
1734    fn on_position_event(&mut self, event: PositionEvent) {}
1735}
1736
1737#[derive(Debug)]
1738pub(crate) struct EmulatedOrderSubmissionError {
1739    client_order_id: ClientOrderId,
1740}
1741
1742impl Display for EmulatedOrderSubmissionError {
1743    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1744        write!(
1745            f,
1746            "Execution algorithm cannot submit order {} with a live emulation trigger",
1747            self.client_order_id
1748        )
1749    }
1750}
1751
1752impl std::error::Error for EmulatedOrderSubmissionError {}
1753
1754fn spawn_unfilled_quantity(
1755    order: &OrderAny,
1756    reduction: SpawnReduction,
1757    primary_precision: u8,
1758) -> Option<Quantity> {
1759    if reduction.spawn_was_quote_quantity == order.is_quote_quantity() {
1760        return Some(
1761            order
1762                .quantity()
1763                .min(reduction.deducted_qty)
1764                .saturating_sub(order.filled_qty()),
1765        );
1766    }
1767
1768    if !reduction.spawn_was_quote_quantity {
1769        log::warn!(
1770            "Cannot account for spawned order {} quantity: denomination changed from base to quote",
1771            order.client_order_id(),
1772        );
1773        return None;
1774    }
1775
1776    let converted_total = order.quantity();
1777    if converted_total.is_zero() {
1778        log::warn!(
1779            "Cannot account for converted spawned order {} quantity: converted total is zero",
1780            order.client_order_id(),
1781        );
1782        return None;
1783    }
1784
1785    // Voided fills release budget even when the venue does not reopen their leaves
1786    let child_qty = converted_total.saturating_sub(order.filled_qty());
1787
1788    // Fills and leaves never exceed the order total; the clamp makes that a
1789    // structural bound so the quotient below always fits the raw width.
1790    let child_qty = child_qty.min(converted_total);
1791    // QuantityRaw is u64 or u128 depending on the high-precision feature, so
1792    // the widening is real in one build shape and an identity in the other.
1793    #[allow(clippy::useless_conversion)]
1794    let proportional_raw = QuantityRaw::try_from(muldiv_floor_u128(
1795        u128::from(reduction.deducted_qty.raw()),
1796        u128::from(child_qty.raw()),
1797        u128::from(converted_total.raw()),
1798    ))
1799    .expect("Quotient bounded by the deducted quantity");
1800
1801    let precision_increment = Quantity::from_decimal_dp(
1802        rust_decimal::Decimal::new(1, u32::from(primary_precision)),
1803        primary_precision,
1804    )
1805    .expect("Primary quantity precision must be valid")
1806    .raw();
1807    let floored_raw = proportional_raw - proportional_raw % precision_increment;
1808    Some(Quantity::from_raw(floored_raw, primary_precision))
1809}
1810
1811/// Returns `floor(a * b / c)` without intermediate overflow.
1812///
1813/// # Panics
1814///
1815/// Panics if `c` is zero, or if `b > c` and the quotient exceeds `u128`
1816/// (callers bound `b` by `c`, which bounds the quotient by `a`).
1817fn muldiv_floor_u128(a: u128, b: u128, c: u128) -> u128 {
1818    if let Some(product) = a.checked_mul(b) {
1819        return product / c;
1820    }
1821
1822    let (hi, lo) = mul_wide_u128(a, b);
1823    assert!(hi < c, "muldiv_floor_u128 quotient exceeds u128");
1824    div_wide_u128(hi, lo, c)
1825}
1826
1827/// Returns the 256-bit product of two `u128` values as `(high, low)` halves.
1828fn mul_wide_u128(a: u128, b: u128) -> (u128, u128) {
1829    const MASK: u128 = (1u128 << 64) - 1;
1830    let (a_hi, a_lo) = (a >> 64, a & MASK);
1831    let (b_hi, b_lo) = (b >> 64, b & MASK);
1832
1833    let ll = a_lo * b_lo;
1834    let lh = a_lo * b_hi;
1835    let hl = a_hi * b_lo;
1836    let hh = a_hi * b_hi;
1837
1838    let mid = (ll >> 64) + (lh & MASK) + (hl & MASK);
1839    let lo = (mid << 64) | (ll & MASK);
1840    let hi = hh + (lh >> 64) + (hl >> 64) + (mid >> 64);
1841    (hi, lo)
1842}
1843
1844/// Divides the 256-bit value `(hi, lo)` by `c` via restoring long division,
1845/// truncating toward zero. Requires `hi < c` so the quotient fits `u128`.
1846fn div_wide_u128(hi: u128, lo: u128, c: u128) -> u128 {
1847    let mut rem = hi;
1848    let mut quotient = 0u128;
1849
1850    for i in (0..u128::BITS).rev() {
1851        // A carry out of the shift means the true remainder is rem + 2^128,
1852        // which always exceeds c; wrapping_sub then yields the exact value.
1853        let carry = rem >> 127;
1854        rem = (rem << 1) | ((lo >> i) & 1);
1855
1856        if carry == 1 || rem >= c {
1857            rem = rem.wrapping_sub(c);
1858            quotient |= 1 << i;
1859        }
1860    }
1861    quotient
1862}
1863
1864/// Charges a late fill against a spawn reduction record, booking any shortfall
1865/// as debt against the primary order.
1866fn charge_spawn_reduction(
1867    core: &mut ExecutionAlgorithmCore,
1868    spawn_id: ClientOrderId,
1869    primary_id: ClientOrderId,
1870    mut reduction: SpawnReduction,
1871    unfilled_qty: Quantity,
1872    shortfall_qty: Quantity,
1873) {
1874    reduction.restored_qty = Some(unfilled_qty);
1875    core.set_spawn_reduction(spawn_id, reduction);
1876
1877    if !shortfall_qty.is_zero() {
1878        core.add_spawn_fill_debt(primary_id, shortfall_qty);
1879    }
1880}
1881
1882fn publish_order_initialized(order: &OrderAny) {
1883    let event = OrderEventAny::Initialized(order.init_event().clone());
1884    publish_order_event(&event);
1885}
1886
1887fn publish_order_event(event: &OrderEventAny) {
1888    let topic = format!("events.order.{}", event.strategy_id());
1889    msgbus::publish_order_event(topic.into(), event);
1890}
1891
1892fn registered_trader_id(core: &ExecutionAlgorithmCore) -> anyhow::Result<TraderId> {
1893    DataActorNative::core(core)
1894        .trader_id()
1895        .ok_or_else(|| anyhow::anyhow!("ExecutionAlgorithm not registered: trader_id is not set"))
1896}
1897
1898fn required_account_id(order: &OrderAny, operation: &str) -> anyhow::Result<AccountId> {
1899    order.account_id().ok_or_else(|| {
1900        anyhow::anyhow!(
1901            "Cannot generate {operation} event for {}: account_id is not set",
1902            order.client_order_id()
1903        )
1904    })
1905}
1906
1907#[cfg(test)]
1908mod tests {
1909    use std::{cell::RefCell, rc::Rc};
1910
1911    use nautilus_common::{
1912        actor::DataActor,
1913        cache::Cache,
1914        clock::VirtualClock,
1915        component::Component,
1916        enums::ComponentTrigger,
1917        msgbus::{
1918            self, TypedHandler,
1919            stubs::{TypedIntoMessageSavingHandler, get_typed_into_message_saving_handler},
1920        },
1921    };
1922    use nautilus_model::{
1923        enums::{LiquiditySide, OrderSide, OrderStatus, OrderType},
1924        events::{
1925            OrderAccepted, OrderCanceled, OrderDenied, OrderDeniedReason, OrderRejected,
1926            order::spec::{
1927                OrderAcceptedSpec, OrderCanceledSpec, OrderDeniedSpec, OrderExpiredSpec,
1928                OrderFillVoidedSpec, OrderFilledSpec, OrderRejectedSpec, OrderSubmittedSpec,
1929                OrderUpdatedSpec,
1930            },
1931        },
1932        identifiers::{
1933            AccountId, ActorId, ClientOrderId, ExecAlgorithmId, InstrumentId, StrategyId, TradeId,
1934            TraderId, VenueOrderId,
1935        },
1936        orders::{LimitOrder, MarketOrder, OrderAny, OrderTestBuilder, stubs::TestOrderStubs},
1937        types::{Currency, Price, Quantity},
1938    };
1939    use rstest::rstest;
1940
1941    use super::*;
1942    use crate::nautilus_execution_algorithm;
1943
1944    #[derive(Debug)]
1945    struct TestAlgorithm {
1946        core: ExecutionAlgorithmCore,
1947        order_client_ids: Vec<ClientOrderId>,
1948    }
1949
1950    #[derive(Debug)]
1951    struct ModifyDispatchAlgorithm {
1952        core: ExecutionAlgorithmCore,
1953        modify_client_order_ids: Vec<ClientOrderId>,
1954    }
1955
1956    #[derive(Debug)]
1957    struct CoreFreeExecutionAlgorithm {
1958        orders_seen: usize,
1959    }
1960
1961    #[derive(Debug)]
1962    struct MacroTestCustomField {
1963        inner: ExecutionAlgorithmCore,
1964    }
1965
1966    impl DataActor for CoreFreeExecutionAlgorithm {}
1967
1968    impl ExecutionAlgorithm for CoreFreeExecutionAlgorithm {
1969        fn on_order(&mut self, _order: OrderAny) -> anyhow::Result<()> {
1970            self.orders_seen += 1;
1971            Ok(())
1972        }
1973    }
1974
1975    impl DataActor for MacroTestCustomField {}
1976
1977    nautilus_execution_algorithm!(MacroTestCustomField, inner, {
1978        fn on_order(&mut self, _order: OrderAny) -> anyhow::Result<()> {
1979            Ok(())
1980        }
1981    });
1982
1983    impl TestAlgorithm {
1984        fn new(config: ExecutionAlgorithmConfig) -> Self {
1985            Self {
1986                core: ExecutionAlgorithmCore::new(config),
1987                order_client_ids: Vec::new(),
1988            }
1989        }
1990    }
1991
1992    impl DataActor for TestAlgorithm {}
1993
1994    nautilus_execution_algorithm!(TestAlgorithm, {
1995        fn on_order(&mut self, order: OrderAny) -> anyhow::Result<()> {
1996            self.order_client_ids.push(order.client_order_id());
1997            Ok(())
1998        }
1999    });
2000
2001    impl ModifyDispatchAlgorithm {
2002        fn new(config: ExecutionAlgorithmConfig) -> Self {
2003            Self {
2004                core: ExecutionAlgorithmCore::new(config),
2005                modify_client_order_ids: Vec::new(),
2006            }
2007        }
2008    }
2009
2010    impl DataActor for ModifyDispatchAlgorithm {}
2011
2012    nautilus_execution_algorithm!(ModifyDispatchAlgorithm, {
2013        fn on_order(&mut self, _order: OrderAny) -> anyhow::Result<()> {
2014            Ok(())
2015        }
2016
2017        fn handle_modify_order(&mut self, command: ModifyOrder) -> anyhow::Result<()> {
2018            self.modify_client_order_ids.push(command.client_order_id);
2019            Ok(())
2020        }
2021    });
2022
2023    fn create_test_algorithm() -> TestAlgorithm {
2024        // Use unique ID to avoid thread-local registry/msgbus conflicts in parallel tests
2025        let unique_id = format!("TEST-{}", UUID4::new());
2026        let config = ExecutionAlgorithmConfig {
2027            exec_algorithm_id: Some(ExecAlgorithmId::new(&unique_id)),
2028            ..Default::default()
2029        };
2030        TestAlgorithm::new(config)
2031    }
2032
2033    fn register_algorithm(algo: &mut TestAlgorithm) {
2034        let trader_id = TraderId::from("TRADER-001");
2035        let clock = Rc::new(RefCell::new(VirtualClock::new()));
2036        let cache = Rc::new(RefCell::new(Cache::default()));
2037
2038        algo.core.register(trader_id, clock, cache).unwrap();
2039
2040        // Transition to Running state for tests
2041        algo.transition_state(ComponentTrigger::Initialize).unwrap();
2042        algo.transition_state(ComponentTrigger::Start).unwrap();
2043        algo.transition_state(ComponentTrigger::StartCompleted)
2044            .unwrap();
2045    }
2046
2047    fn subscribe_order_topic(
2048        strategy_id: StrategyId,
2049    ) -> (TypedHandler<OrderEventAny>, Rc<RefCell<Vec<OrderEventAny>>>) {
2050        let events = Rc::new(RefCell::new(Vec::new()));
2051        let handler = TypedHandler::from({
2052            let events = events.clone();
2053            move |event: &OrderEventAny| {
2054                events.borrow_mut().push(event.clone());
2055            }
2056        });
2057        msgbus::subscribe_order_events(
2058            format!("events.order.{strategy_id}").into(),
2059            handler.clone(),
2060            None,
2061        );
2062        (handler, events)
2063    }
2064
2065    fn setup_pending_spawn() -> (TestAlgorithm, ClientOrderId, OrderAny) {
2066        let mut algo = create_test_algorithm();
2067        register_algorithm(&mut algo);
2068        let client_order_id = ClientOrderId::from("O-001");
2069        let mut primary = OrderAny::Market(MarketOrder::new(
2070            TraderId::from("TRADER-001"),
2071            StrategyId::from("STRAT-001"),
2072            InstrumentId::from("BTC/USDT.BINANCE"),
2073            client_order_id,
2074            OrderSide::Buy,
2075            Quantity::from("1.0"),
2076            TimeInForce::Gtc,
2077            UUID4::new(),
2078            0.into(),
2079            false,
2080            false,
2081            None,
2082            None,
2083            None,
2084            None,
2085            Some(algo.id()),
2086            None,
2087            Some(client_order_id),
2088            None,
2089        ));
2090        algo.core
2091            .cache_rc()
2092            .borrow_mut()
2093            .add_order(primary.clone(), None, None, false)
2094            .unwrap();
2095        let spawned = algo.spawn_market(
2096            &mut primary,
2097            Quantity::from("0.5"),
2098            TimeInForce::Fok,
2099            false,
2100            None,
2101            true,
2102        );
2103        let spawned_order = OrderAny::Market(spawned);
2104        algo.core
2105            .cache_rc()
2106            .borrow_mut()
2107            .add_order(spawned_order.clone(), None, None, false)
2108            .unwrap();
2109        (algo, client_order_id, spawned_order)
2110    }
2111
2112    fn setup_accepted_spawn() -> (TestAlgorithm, ClientOrderId, OrderAny) {
2113        let (mut algo, client_order_id, mut spawned_order) = setup_pending_spawn();
2114        let accepted = OrderAcceptedSpec::builder()
2115            .trader_id(spawned_order.trader_id())
2116            .strategy_id(spawned_order.strategy_id())
2117            .instrument_id(spawned_order.instrument_id())
2118            .client_order_id(spawned_order.client_order_id())
2119            .venue_order_id(VenueOrderId::from("V-123"))
2120            .account_id(AccountId::from("BINANCE-001"))
2121            .build();
2122        spawned_order = algo
2123            .core
2124            .cache_rc()
2125            .borrow_mut()
2126            .update_order(&OrderEventAny::Accepted(accepted))
2127            .unwrap();
2128        algo.handle_order_event(OrderEventAny::Accepted(accepted));
2129        (algo, client_order_id, spawned_order)
2130    }
2131
2132    fn setup_accepted_quote_spawn(
2133        primary_qty: Quantity,
2134        spawn_qty: Quantity,
2135    ) -> (TestAlgorithm, ClientOrderId, OrderAny) {
2136        let mut algo = create_test_algorithm();
2137        register_algorithm(&mut algo);
2138        let client_order_id = ClientOrderId::from("O-QUOTE");
2139        let mut primary = OrderAny::Market(MarketOrder::new(
2140            TraderId::from("TRADER-001"),
2141            StrategyId::from("STRAT-001"),
2142            InstrumentId::from("BTC/USDT.BINANCE"),
2143            client_order_id,
2144            OrderSide::Buy,
2145            primary_qty,
2146            TimeInForce::Gtc,
2147            UUID4::new(),
2148            0.into(),
2149            false,
2150            true,
2151            None,
2152            None,
2153            None,
2154            None,
2155            Some(algo.id()),
2156            None,
2157            Some(client_order_id),
2158            None,
2159        ));
2160        algo.core
2161            .cache_rc()
2162            .borrow_mut()
2163            .add_order(primary.clone(), None, None, false)
2164            .unwrap();
2165        let mut spawned_order = OrderAny::Market(algo.spawn_market(
2166            &mut primary,
2167            spawn_qty,
2168            TimeInForce::Fok,
2169            false,
2170            None,
2171            true,
2172        ));
2173        algo.core
2174            .cache_rc()
2175            .borrow_mut()
2176            .add_order(spawned_order.clone(), None, None, false)
2177            .unwrap();
2178        accept_spawned_order(&mut algo, &mut spawned_order);
2179        (algo, client_order_id, spawned_order)
2180    }
2181
2182    fn fill_spawned_order(algo: &mut TestAlgorithm, order: &mut OrderAny, quantity: Quantity) {
2183        let venue_order_id = order
2184            .venue_order_id()
2185            .unwrap_or_else(|| VenueOrderId::from("V-PRIMARY"));
2186        let filled = OrderFilledSpec::builder()
2187            .trader_id(order.trader_id())
2188            .strategy_id(order.strategy_id())
2189            .instrument_id(order.instrument_id())
2190            .client_order_id(order.client_order_id())
2191            .venue_order_id(venue_order_id)
2192            .account_id(AccountId::from("BINANCE-001"))
2193            .trade_id(TradeId::new(UUID4::new().to_string()))
2194            .order_side(order.order_side())
2195            .order_type(order.order_type())
2196            .last_qty(quantity)
2197            .last_px(Price::from("50000.0"))
2198            .currency(Currency::USD())
2199            .liquidity_side(LiquiditySide::Taker)
2200            .build();
2201        *order = algo
2202            .core
2203            .cache_rc()
2204            .borrow_mut()
2205            .update_order(&OrderEventAny::Filled(filled.clone()))
2206            .unwrap();
2207        algo.handle_order_event(OrderEventAny::Filled(filled));
2208    }
2209
2210    fn submit_order_in_cache(algo: &mut TestAlgorithm, order: &mut OrderAny) {
2211        let submitted = OrderSubmittedSpec::builder()
2212            .trader_id(order.trader_id())
2213            .strategy_id(order.strategy_id())
2214            .instrument_id(order.instrument_id())
2215            .client_order_id(order.client_order_id())
2216            .account_id(AccountId::from("BINANCE-001"))
2217            .build();
2218        *order = algo
2219            .core
2220            .cache_rc()
2221            .borrow_mut()
2222            .update_order(&OrderEventAny::Submitted(submitted))
2223            .unwrap();
2224        algo.handle_order_event(OrderEventAny::Submitted(submitted));
2225    }
2226
2227    fn cancel_spawned_order(algo: &mut TestAlgorithm, order: &mut OrderAny) {
2228        let venue_order_id = order
2229            .venue_order_id()
2230            .unwrap_or_else(|| VenueOrderId::from("V-123"));
2231        let canceled = OrderCanceledSpec::builder()
2232            .trader_id(order.trader_id())
2233            .strategy_id(order.strategy_id())
2234            .instrument_id(order.instrument_id())
2235            .client_order_id(order.client_order_id())
2236            .venue_order_id(venue_order_id)
2237            .account_id(AccountId::from("BINANCE-001"))
2238            .build();
2239        *order = algo
2240            .core
2241            .cache_rc()
2242            .borrow_mut()
2243            .update_order(&OrderEventAny::Canceled(canceled))
2244            .unwrap();
2245        algo.handle_order_event(OrderEventAny::Canceled(canceled));
2246    }
2247
2248    fn convert_spawn_to_base(algo: &mut TestAlgorithm, order: &mut OrderAny, quantity: Quantity) {
2249        let updated = OrderUpdatedSpec::builder()
2250            .trader_id(order.trader_id())
2251            .strategy_id(order.strategy_id())
2252            .instrument_id(order.instrument_id())
2253            .client_order_id(order.client_order_id())
2254            .quantity(quantity)
2255            .maybe_venue_order_id(order.venue_order_id())
2256            .maybe_account_id(order.account_id())
2257            .is_quote_quantity(false)
2258            .build();
2259        *order = algo
2260            .core
2261            .cache_rc()
2262            .borrow_mut()
2263            .update_order(&OrderEventAny::Updated(updated))
2264            .unwrap();
2265        algo.handle_order_event(OrderEventAny::Updated(updated));
2266    }
2267
2268    fn expire_spawned_order(algo: &mut TestAlgorithm, order: &mut OrderAny) {
2269        let expired = OrderExpiredSpec::builder()
2270            .trader_id(order.trader_id())
2271            .strategy_id(order.strategy_id())
2272            .instrument_id(order.instrument_id())
2273            .client_order_id(order.client_order_id())
2274            .venue_order_id(VenueOrderId::from("V-123"))
2275            .account_id(AccountId::from("BINANCE-001"))
2276            .build();
2277        *order = algo
2278            .core
2279            .cache_rc()
2280            .borrow_mut()
2281            .update_order(&OrderEventAny::Expired(expired))
2282            .unwrap();
2283        algo.handle_order_event(OrderEventAny::Expired(expired));
2284    }
2285
2286    fn accept_spawned_order(algo: &mut TestAlgorithm, order: &mut OrderAny) {
2287        let venue_order_id = VenueOrderId::from(format!("V-{}", order.client_order_id()).as_str());
2288        let accepted = OrderAcceptedSpec::builder()
2289            .trader_id(order.trader_id())
2290            .strategy_id(order.strategy_id())
2291            .instrument_id(order.instrument_id())
2292            .client_order_id(order.client_order_id())
2293            .venue_order_id(venue_order_id)
2294            .account_id(AccountId::from("BINANCE-001"))
2295            .build();
2296        *order = algo
2297            .core
2298            .cache_rc()
2299            .borrow_mut()
2300            .update_order(&OrderEventAny::Accepted(accepted))
2301            .unwrap();
2302        algo.handle_order_event(OrderEventAny::Accepted(accepted));
2303    }
2304
2305    fn spawn_reduced_child(
2306        algo: &mut TestAlgorithm,
2307        primary_id: ClientOrderId,
2308        quantity: Quantity,
2309    ) -> OrderAny {
2310        let mut primary = algo.cache().order(&primary_id).unwrap();
2311        let child = OrderAny::Market(algo.spawn_market(
2312            &mut primary,
2313            quantity,
2314            TimeInForce::Fok,
2315            false,
2316            None,
2317            true,
2318        ));
2319        algo.core
2320            .cache_rc()
2321            .borrow_mut()
2322            .add_order(child.clone(), None, None, false)
2323            .unwrap();
2324        child
2325    }
2326
2327    #[rstest]
2328    fn test_algorithm_creation() {
2329        let algo = create_test_algorithm();
2330        assert!(algo.id().inner().starts_with("TEST-"));
2331        assert!(algo.order_client_ids.is_empty());
2332    }
2333
2334    #[rstest]
2335    fn test_algorithm_registration() {
2336        let mut algo = create_test_algorithm();
2337        register_algorithm(&mut algo);
2338
2339        assert_eq!(algo.trader_id(), Some(TraderId::from("TRADER-001")));
2340    }
2341
2342    #[rstest]
2343    fn test_algorithm_deny_order_updates_cache_and_publishes_once() {
2344        let mut algo = create_test_algorithm();
2345        register_algorithm(&mut algo);
2346
2347        let strategy_id = StrategyId::from("STRAT-ALGO-DENY");
2348        let order = OrderAny::Market(MarketOrder::new(
2349            TraderId::from("TRADER-001"),
2350            strategy_id,
2351            InstrumentId::from("BTC/USDT.BINANCE"),
2352            ClientOrderId::from("O-ALGO-DENY"),
2353            OrderSide::Buy,
2354            Quantity::from("1.0"),
2355            TimeInForce::Gtc,
2356            UUID4::new(),
2357            0.into(),
2358            false,
2359            false,
2360            None,
2361            None,
2362            None,
2363            None,
2364            None,
2365            None,
2366            None,
2367            None,
2368        ));
2369        {
2370            let cache_rc = algo.core.cache_rc();
2371            cache_rc
2372                .borrow_mut()
2373                .add_order(order.clone(), None, None, false)
2374                .unwrap();
2375        }
2376        let reason = OrderDeniedReason::ValidationFailed {
2377            detail: "invalid execution schedule".to_string(),
2378        }
2379        .to_string();
2380        let reason = Ustr::from(&reason);
2381        let (handler, events) = subscribe_order_topic(strategy_id);
2382
2383        algo.deny_order(&order, reason).unwrap();
2384        algo.deny_order(&order, reason).unwrap();
2385
2386        msgbus::unsubscribe_order_events(format!("events.order.{strategy_id}").into(), &handler);
2387        let cached_order = algo.cache().order(&order.client_order_id()).unwrap();
2388        let events = events.borrow();
2389
2390        assert_eq!(cached_order.status(), OrderStatus::Denied);
2391        assert_eq!(events.len(), 1);
2392        assert!(matches!(
2393            &events[0],
2394            OrderEventAny::Denied(event)
2395                if event.reason == reason
2396                    && event.strategy_id == strategy_id
2397                    && event.client_order_id == order.client_order_id()
2398        ));
2399    }
2400
2401    #[rstest]
2402    fn test_algorithm_deny_order_initializes_missing_order_once() {
2403        let mut algo = create_test_algorithm();
2404        register_algorithm(&mut algo);
2405
2406        let strategy_id = StrategyId::from("STRAT-ALGO-DENY-MISSING");
2407        let order = OrderTestBuilder::new(OrderType::Market)
2408            .strategy_id(strategy_id)
2409            .instrument_id(InstrumentId::from("BTC/USDT.BINANCE"))
2410            .client_order_id(ClientOrderId::from("O-ALGO-DENY-MISSING"))
2411            .quantity(Quantity::from("1.0"))
2412            .build();
2413        let reason = Ustr::from("VALIDATION_FAILED: invalid execution schedule");
2414        let (handler, events) = subscribe_order_topic(strategy_id);
2415
2416        algo.deny_order(&order, reason).unwrap();
2417        algo.deny_order(&order, reason).unwrap();
2418
2419        msgbus::unsubscribe_order_events(format!("events.order.{strategy_id}").into(), &handler);
2420        let cached_order = algo.cache().order(&order.client_order_id()).unwrap();
2421        let events = events.borrow();
2422
2423        assert_eq!(cached_order.status(), OrderStatus::Denied);
2424        assert_eq!(cached_order.event_count(), 2);
2425        assert_eq!(events.len(), 2);
2426        assert!(matches!(
2427            &events[0],
2428            OrderEventAny::Initialized(event)
2429                if event.strategy_id == strategy_id
2430                    && event.client_order_id == order.client_order_id()
2431        ));
2432        assert!(matches!(
2433            &events[1],
2434            OrderEventAny::Denied(event)
2435                if event.reason == reason
2436                    && event.strategy_id == strategy_id
2437                    && event.client_order_id == order.client_order_id()
2438        ));
2439    }
2440
2441    #[rstest]
2442    fn test_algorithm_deny_order_does_not_publish_when_apply_fails() {
2443        let mut algo = create_test_algorithm();
2444        register_algorithm(&mut algo);
2445
2446        let strategy_id = StrategyId::from("STRAT-ALGO-DENY-APPLY");
2447        let order = OrderTestBuilder::new(OrderType::Market)
2448            .strategy_id(strategy_id)
2449            .instrument_id(InstrumentId::from("BTC/USDT.BINANCE"))
2450            .client_order_id(ClientOrderId::from("O-ALGO-DENY-APPLY"))
2451            .quantity(Quantity::from("1.0"))
2452            .build();
2453        let order = TestOrderStubs::make_accepted_order(&order);
2454        {
2455            let cache_rc = algo.core.cache_rc();
2456            cache_rc
2457                .borrow_mut()
2458                .add_order(order.clone(), None, None, false)
2459                .unwrap();
2460        }
2461        let (handler, events) = subscribe_order_topic(strategy_id);
2462
2463        let params = nautilus_core::Params::new(
2464            [(
2465                "route".to_string(),
2466                serde_json::Value::String("A".to_string()),
2467            )]
2468            .into(),
2469        );
2470        algo.core
2471            .remember_submit_params(order.client_order_id(), Some(params));
2472
2473        let error = algo
2474            .deny_order(
2475                &order,
2476                Ustr::from("VALIDATION_FAILED: invalid execution schedule"),
2477            )
2478            .unwrap_err();
2479
2480        msgbus::unsubscribe_order_events(format!("events.order.{strategy_id}").into(), &handler);
2481        let cached_order = algo.cache().order(&order.client_order_id()).unwrap();
2482
2483        assert!(matches!(
2484            error.downcast_ref::<OrderError>(),
2485            Some(OrderError::InvalidStateTransition)
2486        ));
2487        assert_eq!(cached_order.status(), OrderStatus::Accepted);
2488        assert!(events.borrow().is_empty());
2489        // A failed denial is not terminal, so the submit params must be retained
2490        assert!(algo.core.submit_params(&order.client_order_id()).is_some());
2491    }
2492
2493    #[rstest]
2494    fn test_algorithm_deny_order_removes_submit_params() {
2495        let mut algo = create_test_algorithm();
2496        register_algorithm(&mut algo);
2497
2498        let strategy_id = StrategyId::from("STRAT-ALGO-DENY-PARAMS");
2499        let order = OrderTestBuilder::new(OrderType::Market)
2500            .strategy_id(strategy_id)
2501            .instrument_id(InstrumentId::from("BTC/USDT.BINANCE"))
2502            .client_order_id(ClientOrderId::from("O-ALGO-DENY-PARAMS"))
2503            .quantity(Quantity::from("1.0"))
2504            .build();
2505        {
2506            let cache_rc = algo.core.cache_rc();
2507            cache_rc
2508                .borrow_mut()
2509                .add_order(order.clone(), None, None, false)
2510                .unwrap();
2511        }
2512
2513        let params = nautilus_core::Params::new(
2514            [(
2515                "route".to_string(),
2516                serde_json::Value::String("A".to_string()),
2517            )]
2518            .into(),
2519        );
2520        algo.core
2521            .remember_submit_params(order.client_order_id(), Some(params));
2522        assert!(algo.core.submit_params(&order.client_order_id()).is_some());
2523
2524        algo.deny_order(&order, Ustr::from("VALIDATION_FAILED: test"))
2525            .unwrap();
2526
2527        assert!(algo.core.submit_params(&order.client_order_id()).is_none());
2528    }
2529
2530    #[rstest]
2531    fn test_submit_order_errors_when_algorithm_not_registered() {
2532        let mut algo = create_test_algorithm();
2533        let order = OrderAny::Market(MarketOrder::new(
2534            TraderId::from("TRADER-001"),
2535            StrategyId::from("STRAT-001"),
2536            InstrumentId::from("BTC/USDT.BINANCE"),
2537            ClientOrderId::from("O-UNREGISTERED-001"),
2538            OrderSide::Buy,
2539            Quantity::from("1.0"),
2540            TimeInForce::Gtc,
2541            UUID4::new(),
2542            0.into(),
2543            false,
2544            false,
2545            None,
2546            None,
2547            None,
2548            None,
2549            None,
2550            None,
2551            None,
2552            None,
2553        ));
2554
2555        let err = algo
2556            .submit_order(order, None, None)
2557            .unwrap_err()
2558            .to_string();
2559
2560        assert_eq!(
2561            err,
2562            "ExecutionAlgorithm not registered: trader_id is not set"
2563        );
2564    }
2565
2566    #[rstest]
2567    fn test_required_account_id_errors_when_missing_for_algorithm_event() {
2568        let order = OrderAny::Market(MarketOrder::new(
2569            TraderId::from("TRADER-001"),
2570            StrategyId::from("STRAT-001"),
2571            InstrumentId::from("BTC/USDT.BINANCE"),
2572            ClientOrderId::from("O-NO-ACCOUNT-001"),
2573            OrderSide::Buy,
2574            Quantity::from("1.0"),
2575            TimeInForce::Gtc,
2576            UUID4::new(),
2577            0.into(),
2578            false,
2579            false,
2580            None,
2581            None,
2582            None,
2583            None,
2584            None,
2585            None,
2586            None,
2587            None,
2588        ));
2589
2590        let err = required_account_id(&order, "pending update")
2591            .unwrap_err()
2592            .to_string();
2593
2594        assert_eq!(
2595            err,
2596            "Cannot generate pending update event for O-NO-ACCOUNT-001: account_id is not set"
2597        );
2598    }
2599
2600    #[rstest]
2601    fn test_algorithm_id() {
2602        let algo = create_test_algorithm();
2603        assert!(algo.id().inner().starts_with("TEST-"));
2604    }
2605
2606    #[rstest]
2607    fn test_execution_algorithm_behavior_does_not_require_native_core_access() {
2608        fn assert_execution_algorithm<T: ExecutionAlgorithm + DataActor>() {}
2609
2610        assert_execution_algorithm::<CoreFreeExecutionAlgorithm>();
2611
2612        let mut algorithm = CoreFreeExecutionAlgorithm { orders_seen: 0 };
2613        let order = OrderTestBuilder::new(OrderType::Market)
2614            .instrument_id(InstrumentId::from("BTC/USDT.BINANCE"))
2615            .quantity(Quantity::from("1.0"))
2616            .build();
2617
2618        algorithm.on_order(order).unwrap();
2619
2620        assert_eq!(algorithm.orders_seen, 1);
2621    }
2622
2623    #[rstest]
2624    fn test_nautilus_execution_algorithm_macro_custom_field() {
2625        let exec_algorithm_id = ExecAlgorithmId::from("MACRO-001");
2626        let algorithm = MacroTestCustomField {
2627            inner: ExecutionAlgorithmCore::new(ExecutionAlgorithmConfig {
2628                exec_algorithm_id: Some(exec_algorithm_id),
2629                ..Default::default()
2630            }),
2631        };
2632
2633        assert_eq!(algorithm.id(), exec_algorithm_id);
2634        assert_eq!(algorithm.actor_id(), ActorId::from("MACRO-001"));
2635    }
2636
2637    #[rstest]
2638    fn test_algorithm_spawn_market_creates_valid_order() {
2639        let mut algo = create_test_algorithm();
2640        register_algorithm(&mut algo);
2641
2642        let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
2643        let mut primary = OrderAny::Market(MarketOrder::new(
2644            TraderId::from("TRADER-001"),
2645            StrategyId::from("STRAT-001"),
2646            instrument_id,
2647            ClientOrderId::from("O-001"),
2648            OrderSide::Buy,
2649            Quantity::from("1.0"),
2650            TimeInForce::Gtc,
2651            UUID4::new(),
2652            0.into(),
2653            false, // reduce_only
2654            false, // quote_quantity
2655            None,  // contingency_type
2656            None,  // order_list_id
2657            None,  // linked_order_ids
2658            None,  // parent_order_id
2659            None,  // exec_algorithm_id
2660            None,  // exec_algorithm_params
2661            None,  // exec_spawn_id
2662            None,  // tags
2663        ));
2664
2665        let spawned = algo.spawn_market(
2666            &mut primary,
2667            Quantity::from("0.5"),
2668            TimeInForce::Ioc,
2669            false,
2670            None,  // tags
2671            false, // reduce_primary
2672        );
2673
2674        assert_eq!(spawned.client_order_id.as_str(), "O-001-E1");
2675        assert_eq!(spawned.instrument_id, instrument_id);
2676        assert_eq!(spawned.order_side(), OrderSide::Buy);
2677        assert_eq!(spawned.quantity, Quantity::from("0.5"));
2678        assert_eq!(spawned.time_in_force, TimeInForce::Ioc);
2679        assert_eq!(spawned.exec_algorithm_id, Some(algo.id()));
2680        assert_eq!(spawned.exec_spawn_id, Some(ClientOrderId::from("O-001")));
2681    }
2682
2683    #[rstest]
2684    fn test_algorithm_spawn_increments_sequence() {
2685        let mut algo = create_test_algorithm();
2686        register_algorithm(&mut algo);
2687
2688        let mut primary = OrderAny::Market(MarketOrder::new(
2689            TraderId::from("TRADER-001"),
2690            StrategyId::from("STRAT-001"),
2691            InstrumentId::from("BTC/USDT.BINANCE"),
2692            ClientOrderId::from("O-001"),
2693            OrderSide::Buy,
2694            Quantity::from("1.0"),
2695            TimeInForce::Gtc,
2696            UUID4::new(),
2697            0.into(),
2698            false,
2699            false,
2700            None,
2701            None,
2702            None,
2703            None,
2704            None,
2705            None,
2706            None,
2707            None,
2708        ));
2709
2710        let spawned1 = algo.spawn_market(
2711            &mut primary,
2712            Quantity::from("0.25"),
2713            TimeInForce::Ioc,
2714            false,
2715            None,
2716            false,
2717        );
2718        let spawned2 = algo.spawn_market(
2719            &mut primary,
2720            Quantity::from("0.25"),
2721            TimeInForce::Ioc,
2722            false,
2723            None,
2724            false,
2725        );
2726        let spawned3 = algo.spawn_market(
2727            &mut primary,
2728            Quantity::from("0.25"),
2729            TimeInForce::Ioc,
2730            false,
2731            None,
2732            false,
2733        );
2734
2735        assert_eq!(spawned1.client_order_id.as_str(), "O-001-E1");
2736        assert_eq!(spawned2.client_order_id.as_str(), "O-001-E2");
2737        assert_eq!(spawned3.client_order_id.as_str(), "O-001-E3");
2738    }
2739
2740    #[rstest]
2741    fn test_algorithm_default_handlers_do_not_panic() {
2742        let mut algo = create_test_algorithm();
2743
2744        algo.on_order_initialized(OrderInitialized::default());
2745        algo.on_order_denied(OrderDenied::default());
2746        algo.on_order_emulated(OrderEmulated::default());
2747        algo.on_order_released(OrderReleased::default());
2748        algo.on_order_submitted(OrderSubmitted::default());
2749        algo.on_order_rejected(OrderRejected::default());
2750        algo.on_order_accepted(OrderAccepted::default());
2751        algo.on_algo_order_canceled(OrderCanceled::default());
2752        algo.on_order_expired(OrderExpired::default());
2753        algo.on_order_triggered(OrderTriggered::default());
2754        algo.on_order_pending_update(OrderPendingUpdate::default());
2755        algo.on_order_pending_cancel(OrderPendingCancel::default());
2756        algo.on_order_modify_rejected(OrderModifyRejected::default());
2757        algo.on_order_cancel_rejected(OrderCancelRejected::default());
2758        algo.on_order_updated(OrderUpdated::default());
2759        algo.on_algo_order_filled(OrderFilledSpec::builder().build());
2760        algo.on_order_fill_voided(&OrderFillVoidedSpec::builder().build());
2761    }
2762
2763    #[rstest]
2764    fn test_strategy_subscription_tracking() {
2765        let mut algo = create_test_algorithm();
2766        let strategy_id = StrategyId::from("STRAT-001");
2767
2768        assert!(!algo.core.is_strategy_subscribed(&strategy_id));
2769
2770        algo.subscribe_to_strategy_events(strategy_id);
2771        assert!(algo.core.is_strategy_subscribed(&strategy_id));
2772
2773        // Second call should be idempotent
2774        algo.subscribe_to_strategy_events(strategy_id);
2775        assert!(algo.core.is_strategy_subscribed(&strategy_id));
2776    }
2777
2778    #[rstest]
2779    fn test_algorithm_reset() {
2780        let mut algo = create_test_algorithm();
2781        let strategy_id = StrategyId::from("STRAT-001");
2782        let primary_id = ClientOrderId::new("O-001");
2783
2784        let _ = algo.core.spawn_client_order_id(&primary_id);
2785        algo.core.add_subscribed_strategy(strategy_id);
2786
2787        assert!(algo.core.spawn_sequence(&primary_id).is_some());
2788        assert!(algo.core.is_strategy_subscribed(&strategy_id));
2789
2790        ExecutionAlgorithm::on_reset(&mut algo).unwrap();
2791
2792        assert!(algo.core.spawn_sequence(&primary_id).is_none());
2793        assert!(!algo.core.is_strategy_subscribed(&strategy_id));
2794    }
2795
2796    #[rstest]
2797    fn test_algorithm_spawn_limit_creates_valid_order() {
2798        let mut algo = create_test_algorithm();
2799        register_algorithm(&mut algo);
2800
2801        let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
2802        let mut primary = OrderAny::Market(MarketOrder::new(
2803            TraderId::from("TRADER-001"),
2804            StrategyId::from("STRAT-001"),
2805            instrument_id,
2806            ClientOrderId::from("O-001"),
2807            OrderSide::Buy,
2808            Quantity::from("1.0"),
2809            TimeInForce::Gtc,
2810            UUID4::new(),
2811            0.into(),
2812            false,
2813            false,
2814            None,
2815            None,
2816            None,
2817            None,
2818            None,
2819            None,
2820            None,
2821            None,
2822        ));
2823
2824        let price = Price::from("50000.0");
2825        let spawned = algo.spawn_limit(
2826            &mut primary,
2827            Quantity::from("0.5"),
2828            price,
2829            TimeInForce::Gtc,
2830            None,  // expire_time
2831            false, // post_only
2832            false, // reduce_only
2833            None,  // display_qty
2834            None,  // emulation_trigger
2835            None,  // tags
2836            false, // reduce_primary
2837        );
2838
2839        assert_eq!(spawned.client_order_id.as_str(), "O-001-E1");
2840        assert_eq!(spawned.instrument_id, instrument_id);
2841        assert_eq!(spawned.order_side(), OrderSide::Buy);
2842        assert_eq!(spawned.quantity, Quantity::from("0.5"));
2843        assert_eq!(spawned.price, price);
2844        assert_eq!(spawned.time_in_force, TimeInForce::Gtc);
2845        assert_eq!(spawned.exec_algorithm_id, Some(algo.id()));
2846        assert_eq!(spawned.exec_spawn_id, Some(ClientOrderId::from("O-001")));
2847    }
2848
2849    #[rstest]
2850    fn test_algorithm_spawn_market_to_limit_creates_valid_order() {
2851        let mut algo = create_test_algorithm();
2852        register_algorithm(&mut algo);
2853
2854        let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
2855        let mut primary = OrderAny::Market(MarketOrder::new(
2856            TraderId::from("TRADER-001"),
2857            StrategyId::from("STRAT-001"),
2858            instrument_id,
2859            ClientOrderId::from("O-001"),
2860            OrderSide::Buy,
2861            Quantity::from("1.0"),
2862            TimeInForce::Gtc,
2863            UUID4::new(),
2864            0.into(),
2865            false,
2866            false,
2867            None,
2868            None,
2869            None,
2870            None,
2871            None,
2872            None,
2873            None,
2874            None,
2875        ));
2876
2877        let spawned = algo.spawn_market_to_limit(
2878            &mut primary,
2879            Quantity::from("0.5"),
2880            TimeInForce::Gtc,
2881            None,  // expire_time
2882            false, // reduce_only
2883            None,  // display_qty
2884            None,  // emulation_trigger
2885            None,  // tags
2886            false, // reduce_primary
2887        );
2888
2889        assert_eq!(spawned.client_order_id.as_str(), "O-001-E1");
2890        assert_eq!(spawned.instrument_id, instrument_id);
2891        assert_eq!(spawned.order_side(), OrderSide::Buy);
2892        assert_eq!(spawned.quantity, Quantity::from("0.5"));
2893        assert_eq!(spawned.time_in_force, TimeInForce::Gtc);
2894        assert_eq!(spawned.exec_algorithm_id, Some(algo.id()));
2895        assert_eq!(spawned.exec_spawn_id, Some(ClientOrderId::from("O-001")));
2896    }
2897
2898    #[rstest]
2899    fn test_algorithm_spawn_market_with_tags() {
2900        let mut algo = create_test_algorithm();
2901        register_algorithm(&mut algo);
2902
2903        let mut primary = OrderAny::Market(MarketOrder::new(
2904            TraderId::from("TRADER-001"),
2905            StrategyId::from("STRAT-001"),
2906            InstrumentId::from("BTC/USDT.BINANCE"),
2907            ClientOrderId::from("O-001"),
2908            OrderSide::Buy,
2909            Quantity::from("1.0"),
2910            TimeInForce::Gtc,
2911            UUID4::new(),
2912            0.into(),
2913            false,
2914            false,
2915            None,
2916            None,
2917            None,
2918            None,
2919            None,
2920            None,
2921            None,
2922            None,
2923        ));
2924
2925        let tags = vec![ustr::Ustr::from("TAG1"), ustr::Ustr::from("TAG2")];
2926        let spawned = algo.spawn_market(
2927            &mut primary,
2928            Quantity::from("0.5"),
2929            TimeInForce::Ioc,
2930            false,
2931            Some(tags.clone()),
2932            false,
2933        );
2934
2935        assert_eq!(spawned.tags, Some(tags));
2936    }
2937
2938    #[rstest]
2939    fn test_algorithm_spawn_propagates_primary_fields() {
2940        let mut algo = create_test_algorithm();
2941        register_algorithm(&mut algo);
2942
2943        let mut params = indexmap::IndexMap::new();
2944        params.insert(ustr::Ustr::from("horizon_secs"), ustr::Ustr::from("30"));
2945        params.insert(ustr::Ustr::from("interval_secs"), ustr::Ustr::from("10"));
2946        let primary_tags = vec![ustr::Ustr::from("PRIMARY_TAG")];
2947        let linked_order_ids = vec![ClientOrderId::from("LINK-1")];
2948        let client_order_id = ClientOrderId::from("O-001");
2949
2950        let mut primary = OrderAny::Market(MarketOrder::new(
2951            TraderId::from("TRADER-001"),
2952            StrategyId::from("STRAT-001"),
2953            InstrumentId::from("BTC/USDT.BINANCE"),
2954            client_order_id,
2955            OrderSide::Buy,
2956            Quantity::from("1.0"),
2957            TimeInForce::Gtc,
2958            UUID4::new(),
2959            0.into(),
2960            false, // reduce_only
2961            true,  // quote_quantity
2962            None,  // contingency_type
2963            None,  // order_list_id
2964            Some(linked_order_ids.clone()),
2965            None, // parent_order_id
2966            Some(algo.id()),
2967            Some(params.clone()),
2968            Some(client_order_id),
2969            Some(primary_tags.clone()),
2970        ));
2971
2972        let spawned_market = algo.spawn_market(
2973            &mut primary,
2974            Quantity::from("0.25"),
2975            TimeInForce::Ioc,
2976            false,
2977            None, // falls back to primary.tags
2978            false,
2979        );
2980        assert!(spawned_market.is_quote_quantity);
2981        assert_eq!(spawned_market.exec_algorithm_params, Some(params.clone()));
2982        assert_eq!(spawned_market.tags, Some(primary_tags.clone()));
2983        assert_eq!(
2984            spawned_market.linked_order_ids,
2985            Some(linked_order_ids.clone())
2986        );
2987
2988        let spawned_limit = algo.spawn_limit(
2989            &mut primary,
2990            Quantity::from("0.25"),
2991            Price::from("50000.0"),
2992            TimeInForce::Gtc,
2993            None,  // expire_time
2994            false, // post_only
2995            false, // reduce_only
2996            None,  // display_qty
2997            None,  // emulation_trigger
2998            None,  // falls back to primary.tags
2999            false,
3000        );
3001        assert!(spawned_limit.is_quote_quantity);
3002        assert_eq!(spawned_limit.exec_algorithm_params, Some(params.clone()));
3003        assert_eq!(spawned_limit.tags, Some(primary_tags.clone()));
3004        assert_eq!(
3005            spawned_limit.linked_order_ids,
3006            Some(linked_order_ids.clone())
3007        );
3008
3009        let spawned_mtl = algo.spawn_market_to_limit(
3010            &mut primary,
3011            Quantity::from("0.25"),
3012            TimeInForce::Gtc,
3013            None,  // expire_time
3014            false, // reduce_only
3015            None,  // display_qty
3016            None,  // emulation_trigger
3017            None,  // falls back to primary.tags
3018            false,
3019        );
3020        assert!(spawned_mtl.is_quote_quantity);
3021        assert_eq!(spawned_mtl.exec_algorithm_params, Some(params));
3022        assert_eq!(spawned_mtl.tags, Some(primary_tags));
3023        assert_eq!(spawned_mtl.linked_order_ids, Some(linked_order_ids));
3024    }
3025
3026    #[rstest]
3027    fn test_muldiv_floor_u128_narrow_path_truncates() {
3028        assert_eq!(muldiv_floor_u128(50, 3, 5), 30);
3029        assert_eq!(muldiv_floor_u128(10, 2, 3), 6);
3030        assert_eq!(muldiv_floor_u128(u128::MAX, 7, 7), u128::MAX);
3031    }
3032
3033    #[rstest]
3034    fn test_muldiv_floor_u128_wide_path_floors_exactly() {
3035        // a * b overflows u128; the exact quotient is a - a/c, fractionally
3036        // under a, so an implementation that rounds instead of flooring
3037        // returns a. This is the shape of the Decimal-fallback defect.
3038        let a = 10u128.pow(30);
3039        let b = 7 * 10u128.pow(30);
3040        let c = b + 1;
3041        assert!(a.checked_mul(b).is_none());
3042        assert_eq!(muldiv_floor_u128(a, b, c), a - 1);
3043    }
3044
3045    #[rstest]
3046    fn test_algorithm_reduce_primary_order() {
3047        let mut algo = create_test_algorithm();
3048        register_algorithm(&mut algo);
3049
3050        let order = OrderAny::Market(MarketOrder::new(
3051            TraderId::from("TRADER-001"),
3052            StrategyId::from("STRAT-001"),
3053            InstrumentId::from("BTC/USDT.BINANCE"),
3054            ClientOrderId::from("O-001"),
3055            OrderSide::Buy,
3056            Quantity::from("1.0"),
3057            TimeInForce::Gtc,
3058            UUID4::new(),
3059            0.into(),
3060            false,
3061            false,
3062            None,
3063            None,
3064            None,
3065            None,
3066            None,
3067            None,
3068            None,
3069            None,
3070        ));
3071
3072        // Make accepted so OrderUpdated can be applied
3073        let mut primary = TestOrderStubs::make_accepted_order(&order);
3074
3075        {
3076            let cache_rc = algo.core.cache_rc();
3077            let mut cache = cache_rc.borrow_mut();
3078            cache.add_order(primary.clone(), None, None, false).unwrap();
3079        }
3080
3081        let spawn_qty = Quantity::from("0.3");
3082        algo.reduce_primary_order(&mut primary, spawn_qty);
3083
3084        assert_eq!(primary.quantity(), Quantity::from("0.7"));
3085    }
3086
3087    #[rstest]
3088    fn test_algorithm_reduce_primary_order_publishes_updated_event() {
3089        let mut algo = create_test_algorithm();
3090        register_algorithm(&mut algo);
3091
3092        let strategy_id = StrategyId::from("STRAT-ALGO-REDUCE-PUBLISH");
3093        let order = OrderAny::Market(MarketOrder::new(
3094            TraderId::from("TRADER-001"),
3095            strategy_id,
3096            InstrumentId::from("BTC/USDT.BINANCE"),
3097            ClientOrderId::from("O-ALGO-REDUCE"),
3098            OrderSide::Buy,
3099            Quantity::from("1.0"),
3100            TimeInForce::Gtc,
3101            UUID4::new(),
3102            0.into(),
3103            false,
3104            false,
3105            None,
3106            None,
3107            None,
3108            None,
3109            None,
3110            None,
3111            None,
3112            None,
3113        ));
3114        let mut primary = TestOrderStubs::make_accepted_order(&order);
3115
3116        {
3117            let cache_rc = algo.core.cache_rc();
3118            let mut cache = cache_rc.borrow_mut();
3119            cache.add_order(primary.clone(), None, None, false).unwrap();
3120        }
3121
3122        let (handler, events) = subscribe_order_topic(strategy_id);
3123
3124        algo.reduce_primary_order(&mut primary, Quantity::from("0.3"));
3125
3126        msgbus::unsubscribe_order_events(format!("events.order.{strategy_id}").into(), &handler);
3127        let events = events.borrow();
3128
3129        assert_eq!(events.len(), 1);
3130        assert!(matches!(
3131            &events[0],
3132            OrderEventAny::Updated(event) if event.quantity == Quantity::from("0.7")
3133        ));
3134    }
3135
3136    #[rstest]
3137    fn test_algorithm_submit_order_publishes_initialized_for_new_order() {
3138        let mut algo = create_test_algorithm();
3139        register_algorithm(&mut algo);
3140
3141        let strategy_id = StrategyId::from("STRAT-ALGO-INIT-PUBLISH");
3142        let order = OrderAny::Market(MarketOrder::new(
3143            TraderId::from("TRADER-001"),
3144            strategy_id,
3145            InstrumentId::from("BTC/USDT.BINANCE"),
3146            ClientOrderId::from("O-ALGO-INIT"),
3147            OrderSide::Buy,
3148            Quantity::from("1.0"),
3149            TimeInForce::Gtc,
3150            UUID4::new(),
3151            0.into(),
3152            false,
3153            false,
3154            None,
3155            None,
3156            None,
3157            None,
3158            None,
3159            None,
3160            None,
3161            None,
3162        ));
3163        let (handler, events) = subscribe_order_topic(strategy_id);
3164
3165        algo.submit_order(order.clone(), None, None).unwrap();
3166
3167        msgbus::unsubscribe_order_events(format!("events.order.{strategy_id}").into(), &handler);
3168        let events = events.borrow();
3169
3170        assert_eq!(events.len(), 1);
3171        assert!(matches!(
3172            &events[0],
3173            OrderEventAny::Initialized(event) if event.client_order_id == order.client_order_id()
3174        ));
3175    }
3176
3177    #[rstest]
3178    fn test_algorithm_submit_order_does_not_republish_initialized_for_existing_order() {
3179        let mut algo = create_test_algorithm();
3180        register_algorithm(&mut algo);
3181
3182        let strategy_id = StrategyId::from("STRAT-ALGO-INIT-EXISTING");
3183        let order = OrderAny::Market(MarketOrder::new(
3184            TraderId::from("TRADER-001"),
3185            strategy_id,
3186            InstrumentId::from("BTC/USDT.BINANCE"),
3187            ClientOrderId::from("O-ALGO-INIT-EXISTING"),
3188            OrderSide::Buy,
3189            Quantity::from("1.0"),
3190            TimeInForce::Gtc,
3191            UUID4::new(),
3192            0.into(),
3193            false,
3194            false,
3195            None,
3196            None,
3197            None,
3198            None,
3199            None,
3200            None,
3201            None,
3202            None,
3203        ));
3204        {
3205            let cache_rc = algo.core.cache_rc();
3206            let mut cache = cache_rc.borrow_mut();
3207            cache.add_order(order.clone(), None, None, true).unwrap();
3208        }
3209        let (handler, events) = subscribe_order_topic(strategy_id);
3210
3211        algo.submit_order(order, None, None).unwrap();
3212
3213        msgbus::unsubscribe_order_events(format!("events.order.{strategy_id}").into(), &handler);
3214        assert!(events.borrow().is_empty());
3215    }
3216
3217    #[rstest]
3218    fn test_algorithm_submit_order_refuses_emulated_limit_spawn() {
3219        let mut algo = create_test_algorithm();
3220        register_algorithm(&mut algo);
3221
3222        let strategy_id = StrategyId::from("STRAT-ALGO-EMULATED-LIMIT");
3223        let order = OrderTestBuilder::new(OrderType::Market)
3224            .strategy_id(strategy_id)
3225            .instrument_id(InstrumentId::from("BTC/USDT.BINANCE"))
3226            .client_order_id(ClientOrderId::from("O-ALGO-EMULATED-LIMIT"))
3227            .quantity(Quantity::from("1.0"))
3228            .build();
3229        let mut primary = TestOrderStubs::make_accepted_order(&order);
3230        {
3231            let cache_rc = algo.core.cache_rc();
3232            let mut cache = cache_rc.borrow_mut();
3233            cache.add_order(primary.clone(), None, None, false).unwrap();
3234        }
3235        let (event_handler, events) = subscribe_order_topic(strategy_id);
3236        let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
3237            get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
3238        msgbus::register_trading_command_endpoint(
3239            MessagingSwitchboard::risk_engine_queue_execute(),
3240            risk_handler,
3241        );
3242        let (emulator_handler, emulator_messages): (
3243            _,
3244            TypedIntoMessageSavingHandler<TradingCommand>,
3245        ) = get_typed_into_message_saving_handler(Some(Ustr::from("OrderEmulator.execute")));
3246        msgbus::register_trading_command_endpoint(
3247            MessagingSwitchboard::order_emulator_execute(),
3248            emulator_handler,
3249        );
3250
3251        let spawned = algo.spawn_limit(
3252            &mut primary,
3253            Quantity::from("0.4"),
3254            Price::from("50000.0"),
3255            TimeInForce::Gtc,
3256            None,
3257            false,
3258            false,
3259            None,
3260            Some(TriggerType::BidAsk),
3261            None,
3262            true,
3263        );
3264        let spawned = OrderAny::Limit(spawned);
3265        let client_order_id = spawned.client_order_id();
3266        let result = algo.submit_order(spawned, None, None);
3267
3268        msgbus::unsubscribe_order_events(
3269            format!("events.order.{strategy_id}").into(),
3270            &event_handler,
3271        );
3272        let cache = algo.core.cache_ref();
3273        let error = result.unwrap_err();
3274        assert!(
3275            error
3276                .downcast_ref::<EmulatedOrderSubmissionError>()
3277                .is_some()
3278        );
3279        let error = error.to_string();
3280        assert!(error.contains("live emulation trigger"), "{error}");
3281        assert!(error.contains(client_order_id.as_str()), "{error}");
3282        assert!(risk_messages.get_messages().is_empty());
3283        assert!(emulator_messages.get_messages().is_empty());
3284        assert!(!cache.order_exists(&client_order_id));
3285        assert!(!events.borrow().iter().any(|event| matches!(
3286            event,
3287            OrderEventAny::Initialized(initialized)
3288                if initialized.client_order_id == client_order_id
3289        )));
3290        // The spawn already reduced the accepted primary locally (the venue
3291        // still works 1.0); restoration declines to mutate a non-local
3292        // primary, so the local deduction stands.
3293        assert_eq!(
3294            cache.order(&primary.client_order_id()).unwrap().quantity(),
3295            Quantity::from("0.6"),
3296        );
3297        drop(cache);
3298        assert!(
3299            algo.core
3300                .take_pending_spawn_reduction(client_order_id)
3301                .is_none()
3302        );
3303    }
3304
3305    #[rstest]
3306    fn test_algorithm_submit_order_routes_unemulated_spawn_to_risk() {
3307        let mut algo = create_test_algorithm();
3308        register_algorithm(&mut algo);
3309
3310        let mut primary = OrderTestBuilder::new(OrderType::Market)
3311            .instrument_id(InstrumentId::from("BTC/USDT.BINANCE"))
3312            .client_order_id(ClientOrderId::from("O-ALGO-UNEMULATED"))
3313            .quantity(Quantity::from("1.0"))
3314            .build();
3315        let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
3316            get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
3317        msgbus::register_trading_command_endpoint(
3318            MessagingSwitchboard::risk_engine_queue_execute(),
3319            risk_handler,
3320        );
3321
3322        let spawned = algo.spawn_limit(
3323            &mut primary,
3324            Quantity::from("0.4"),
3325            Price::from("50000.0"),
3326            TimeInForce::Gtc,
3327            None,
3328            false,
3329            false,
3330            None,
3331            None,
3332            None,
3333            false,
3334        );
3335        let client_order_id = spawned.client_order_id;
3336        algo.submit_order(OrderAny::Limit(spawned), None, None)
3337            .unwrap();
3338
3339        let risk_messages = risk_messages.get_messages();
3340        assert_eq!(risk_messages.len(), 1);
3341        assert!(matches!(
3342            risk_messages.first(),
3343            Some(TradingCommand::SubmitOrder(command))
3344                if command.client_order_id == client_order_id
3345        ));
3346    }
3347
3348    #[rstest]
3349    fn test_algorithm_spawn_market_with_reduce_primary() {
3350        let mut algo = create_test_algorithm();
3351        register_algorithm(&mut algo);
3352
3353        let order = OrderAny::Market(MarketOrder::new(
3354            TraderId::from("TRADER-001"),
3355            StrategyId::from("STRAT-001"),
3356            InstrumentId::from("BTC/USDT.BINANCE"),
3357            ClientOrderId::from("O-001"),
3358            OrderSide::Buy,
3359            Quantity::from("1.0"),
3360            TimeInForce::Gtc,
3361            UUID4::new(),
3362            0.into(),
3363            false,
3364            false,
3365            None,
3366            None,
3367            None,
3368            None,
3369            None,
3370            None,
3371            None,
3372            None,
3373        ));
3374
3375        // Make accepted so OrderUpdated can be applied
3376        let mut primary = TestOrderStubs::make_accepted_order(&order);
3377
3378        {
3379            let cache_rc = algo.core.cache_rc();
3380            let mut cache = cache_rc.borrow_mut();
3381            cache.add_order(primary.clone(), None, None, false).unwrap();
3382        }
3383
3384        let spawned = algo.spawn_market(
3385            &mut primary,
3386            Quantity::from("0.4"),
3387            TimeInForce::Ioc,
3388            false,
3389            None,
3390            true, // reduce_primary = true
3391        );
3392
3393        assert_eq!(spawned.quantity, Quantity::from("0.4"));
3394        assert_eq!(primary.quantity(), Quantity::from("0.6"));
3395    }
3396    #[rstest]
3397    fn test_algorithm_forwards_captured_params_to_spawned_order() {
3398        let mut algo = create_test_algorithm();
3399        register_algorithm(&mut algo);
3400
3401        let strategy_id = StrategyId::from("STRAT-FWD-001");
3402        let mut primary = OrderAny::Market(MarketOrder::new(
3403            TraderId::from("TRADER-001"),
3404            strategy_id,
3405            InstrumentId::from("BTC/USDT.BINANCE"),
3406            ClientOrderId::from("O-FWD-001"),
3407            OrderSide::Buy,
3408            Quantity::from("1.0"),
3409            TimeInForce::Gtc,
3410            UUID4::new(),
3411            0.into(),
3412            false,
3413            false,
3414            None,
3415            None,
3416            None,
3417            None,
3418            None,
3419            None,
3420            None,
3421            None,
3422        ));
3423        {
3424            let cache_rc = algo.core.cache_rc();
3425            let mut cache = cache_rc.borrow_mut();
3426            cache.add_order(primary.clone(), None, None, true).unwrap();
3427        }
3428
3429        let params = nautilus_core::Params::new(
3430            [("is_leverage".to_string(), serde_json::Value::Bool(true))].into(),
3431        );
3432
3433        let command = SubmitOrder::new(
3434            TraderId::from("TRADER-001"),
3435            None,
3436            strategy_id,
3437            primary.instrument_id(),
3438            primary.client_order_id(),
3439            primary.init_event().clone(),
3440            primary.exec_algorithm_id(),
3441            None,
3442            Some(params),
3443            UUID4::new(),
3444            0.into(),
3445            None,
3446        );
3447        algo.execute(TradingCommand::SubmitOrder(command)).unwrap();
3448
3449        let received = Rc::new(RefCell::new(None::<SubmitOrder>));
3450        let handler = msgbus::TypedIntoHandler::from({
3451            let captured = received.clone();
3452            move |cmd: TradingCommand| {
3453                if let TradingCommand::SubmitOrder(cmd) = cmd {
3454                    *captured.borrow_mut() = Some(cmd);
3455                }
3456            }
3457        });
3458        msgbus::register_trading_command_endpoint(
3459            MessagingSwitchboard::risk_engine_queue_execute(),
3460            handler,
3461        );
3462
3463        let spawned = algo.spawn_market(
3464            &mut primary,
3465            Quantity::from("0.4"),
3466            TimeInForce::Ioc,
3467            false,
3468            None,
3469            false, // reduce_primary
3470        );
3471        algo.submit_order(OrderAny::Market(spawned), None, None)
3472            .unwrap();
3473
3474        let captured = received.borrow();
3475        let cmd = captured.as_ref().expect("expected a forwarded SubmitOrder");
3476        assert_eq!(cmd.client_order_id, ClientOrderId::from("O-FWD-001-E1"));
3477        assert_eq!(
3478            cmd.params.as_ref().and_then(|p| p.get_bool("is_leverage")),
3479            Some(true),
3480        );
3481    }
3482
3483    #[rstest]
3484    fn test_algorithm_routes_modify_and_cancel_commands_through_engine_queues() {
3485        let mut modify_algo = create_test_algorithm();
3486        let mut cancel_algo = create_test_algorithm();
3487        register_algorithm(&mut modify_algo);
3488        register_algorithm(&mut cancel_algo);
3489
3490        let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
3491            get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
3492        msgbus::register_trading_command_endpoint(
3493            MessagingSwitchboard::risk_engine_queue_execute(),
3494            risk_handler,
3495        );
3496        let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
3497            get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
3498        msgbus::register_trading_command_endpoint(
3499            MessagingSwitchboard::exec_engine_queue_execute(),
3500            exec_handler,
3501        );
3502
3503        let mut modify_order = TestOrderStubs::make_accepted_order(
3504            &OrderTestBuilder::new(OrderType::Limit)
3505                .strategy_id(StrategyId::from("STRAT-ALGO-ROUTING"))
3506                .instrument_id(InstrumentId::from("BTC/USDT.BINANCE"))
3507                .client_order_id(ClientOrderId::from("O-ALGO-MODIFY"))
3508                .quantity(Quantity::from("1.0"))
3509                .price(Price::from("50000.0"))
3510                .build(),
3511        );
3512        let mut cancel_order = TestOrderStubs::make_accepted_order(
3513            &OrderTestBuilder::new(OrderType::Market)
3514                .strategy_id(StrategyId::from("STRAT-ALGO-ROUTING"))
3515                .instrument_id(InstrumentId::from("BTC/USDT.BINANCE"))
3516                .client_order_id(ClientOrderId::from("O-ALGO-CANCEL"))
3517                .quantity(Quantity::from("1.0"))
3518                .build(),
3519        );
3520        {
3521            let cache_rc = modify_algo.core.cache_rc();
3522            let mut cache = cache_rc.borrow_mut();
3523            cache
3524                .add_order(modify_order.clone(), None, None, false)
3525                .unwrap();
3526        }
3527        {
3528            let cache_rc = cancel_algo.core.cache_rc();
3529            let mut cache = cache_rc.borrow_mut();
3530            cache
3531                .add_order(cancel_order.clone(), None, None, false)
3532                .unwrap();
3533        }
3534
3535        modify_algo
3536            .modify_order(
3537                &mut modify_order,
3538                None,
3539                Some(Price::from("51000.0")),
3540                None,
3541                None,
3542            )
3543            .unwrap();
3544        cancel_algo.cancel_order(&mut cancel_order, None).unwrap();
3545
3546        let risk_messages = risk_messages.get_messages();
3547        let exec_messages = exec_messages.get_messages();
3548        assert_eq!(risk_messages.len(), 1);
3549        assert!(matches!(
3550            risk_messages.first(),
3551            Some(TradingCommand::ModifyOrder(command))
3552                if command.client_order_id == modify_order.client_order_id()
3553        ));
3554        assert_eq!(exec_messages.len(), 1);
3555        assert!(matches!(
3556            exec_messages.first(),
3557            Some(TradingCommand::CancelOrder(command))
3558                if command.client_order_id == cancel_order.client_order_id()
3559        ));
3560    }
3561
3562    #[rstest]
3563    fn test_algorithm_submit_order_list_captures_params_per_order() {
3564        use nautilus_common::messages::execution::SubmitOrderList;
3565        use nautilus_model::identifiers::OrderListId;
3566
3567        let mut algo = create_test_algorithm();
3568        register_algorithm(&mut algo);
3569
3570        let strategy_id = StrategyId::from("STRAT-LIST-001");
3571        let order1 = OrderAny::Market(MarketOrder::new(
3572            TraderId::from("TRADER-001"),
3573            strategy_id,
3574            InstrumentId::from("BTC/USDT.BINANCE"),
3575            ClientOrderId::from("O-LIST-001"),
3576            OrderSide::Buy,
3577            Quantity::from("1.0"),
3578            TimeInForce::Gtc,
3579            UUID4::new(),
3580            0.into(),
3581            false,
3582            false,
3583            None,
3584            None,
3585            None,
3586            None,
3587            None,
3588            None,
3589            None,
3590            None,
3591        ));
3592        let order2 = OrderAny::Market(MarketOrder::new(
3593            TraderId::from("TRADER-001"),
3594            strategy_id,
3595            InstrumentId::from("BTC/USDT.BINANCE"),
3596            ClientOrderId::from("O-LIST-002"),
3597            OrderSide::Buy,
3598            Quantity::from("1.0"),
3599            TimeInForce::Gtc,
3600            UUID4::new(),
3601            0.into(),
3602            false,
3603            false,
3604            None,
3605            None,
3606            None,
3607            None,
3608            None,
3609            None,
3610            None,
3611            None,
3612        ));
3613        {
3614            let cache_rc = algo.core.cache_rc();
3615            let mut cache = cache_rc.borrow_mut();
3616            cache.add_order(order1.clone(), None, None, true).unwrap();
3617            cache.add_order(order2.clone(), None, None, true).unwrap();
3618        }
3619
3620        let order_list = OrderList::new(
3621            OrderListId::from("OL-001"),
3622            order1.instrument_id(),
3623            strategy_id,
3624            vec![order1.client_order_id(), order2.client_order_id()],
3625            0.into(),
3626        );
3627
3628        let params = nautilus_core::Params::new(
3629            [("is_leverage".to_string(), serde_json::Value::Bool(true))].into(),
3630        );
3631
3632        let command = SubmitOrderList::new(
3633            TraderId::from("TRADER-001"),
3634            None,
3635            strategy_id,
3636            order_list,
3637            vec![order1.init_event().clone(), order2.init_event().clone()],
3638            order1.exec_algorithm_id(),
3639            None,
3640            Some(params),
3641            UUID4::new(),
3642            0.into(),
3643            None,
3644        );
3645        algo.execute(TradingCommand::SubmitOrderList(command))
3646            .unwrap();
3647
3648        assert_eq!(
3649            algo.order_client_ids,
3650            [
3651                ClientOrderId::from("O-LIST-001"),
3652                ClientOrderId::from("O-LIST-002"),
3653            ],
3654        );
3655
3656        for id in ["O-LIST-001", "O-LIST-002"] {
3657            assert_eq!(
3658                algo.core
3659                    .submit_params(&ClientOrderId::from(id))
3660                    .and_then(|p| p.get_bool("is_leverage")),
3661                Some(true),
3662                "expected forwarded params for {id}",
3663            );
3664        }
3665    }
3666
3667    #[rstest]
3668    fn test_algorithm_generate_order_canceled() {
3669        let mut algo = create_test_algorithm();
3670        register_algorithm(&mut algo);
3671
3672        let order = OrderAny::Market(MarketOrder::new(
3673            TraderId::from("TRADER-001"),
3674            StrategyId::from("STRAT-001"),
3675            InstrumentId::from("BTC/USDT.BINANCE"),
3676            ClientOrderId::from("O-001"),
3677            OrderSide::Buy,
3678            Quantity::from("1.0"),
3679            TimeInForce::Gtc,
3680            UUID4::new(),
3681            0.into(),
3682            false,
3683            false,
3684            None,
3685            None,
3686            None,
3687            None,
3688            None,
3689            None,
3690            None,
3691            None,
3692        ));
3693
3694        let event = algo.generate_order_canceled(&order);
3695
3696        assert_eq!(event.trader_id, TraderId::from("TRADER-001"));
3697        assert_eq!(event.strategy_id, StrategyId::from("STRAT-001"));
3698        assert_eq!(event.instrument_id, InstrumentId::from("BTC/USDT.BINANCE"));
3699        assert_eq!(event.client_order_id, ClientOrderId::from("O-001"));
3700    }
3701
3702    #[rstest]
3703    fn test_algorithm_handle_cancel_order_publishes_instrument_canceled_topic() {
3704        let mut algo = create_test_algorithm();
3705        register_algorithm(&mut algo);
3706
3707        let strategy_id = StrategyId::from("STRAT-ALGO-CANCEL-PUBLISH");
3708        let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
3709        let order = OrderAny::Market(MarketOrder::new(
3710            TraderId::from("TRADER-001"),
3711            strategy_id,
3712            instrument_id,
3713            ClientOrderId::from("O-ALGO-CANCEL"),
3714            OrderSide::Buy,
3715            Quantity::from("1.0"),
3716            TimeInForce::Gtc,
3717            UUID4::new(),
3718            0.into(),
3719            false,
3720            false,
3721            None,
3722            None,
3723            None,
3724            None,
3725            None,
3726            None,
3727            None,
3728            None,
3729        ));
3730        let order = TestOrderStubs::make_accepted_order(&order);
3731
3732        {
3733            let cache_rc = algo.core.cache_rc();
3734            let mut cache = cache_rc.borrow_mut();
3735            cache.add_order(order.clone(), None, None, false).unwrap();
3736        }
3737
3738        let received = Rc::new(RefCell::new(Vec::<OrderEventAny>::new()));
3739        let handler = TypedHandler::from({
3740            let received = received.clone();
3741            move |event: &OrderEventAny| {
3742                received.borrow_mut().push(event.clone());
3743            }
3744        });
3745        let topic = msgbus::switchboard::get_order_canceled_topic(instrument_id);
3746        msgbus::subscribe_order_events(topic.into(), handler.clone(), None);
3747
3748        let command = CancelOrder::new(
3749            order.trader_id(),
3750            None,
3751            strategy_id,
3752            instrument_id,
3753            order.client_order_id(),
3754            order.venue_order_id(),
3755            UUID4::new(),
3756            0.into(),
3757            None,
3758            None,
3759        );
3760        algo.handle_cancel_order(command).unwrap();
3761
3762        msgbus::unsubscribe_order_events(topic.into(), &handler);
3763        let received = received.borrow();
3764        assert_eq!(received.len(), 1);
3765        assert!(matches!(received[0], OrderEventAny::Canceled(_)));
3766        assert_eq!(received[0].client_order_id(), order.client_order_id());
3767        assert_eq!(received[0].instrument_id(), instrument_id);
3768    }
3769
3770    #[rstest]
3771    fn test_algorithm_execute_dispatches_modify_order_to_handler() {
3772        let unique_id = format!("TEST-{}", UUID4::new());
3773        let config = ExecutionAlgorithmConfig {
3774            exec_algorithm_id: Some(ExecAlgorithmId::new(&unique_id)),
3775            ..Default::default()
3776        };
3777        let mut algo = ModifyDispatchAlgorithm::new(config);
3778        algo.core
3779            .register(
3780                TraderId::from("TRADER-001"),
3781                Rc::new(RefCell::new(VirtualClock::new())),
3782                Rc::new(RefCell::new(Cache::default())),
3783            )
3784            .unwrap();
3785        algo.transition_state(ComponentTrigger::Initialize).unwrap();
3786        algo.transition_state(ComponentTrigger::Start).unwrap();
3787        algo.transition_state(ComponentTrigger::StartCompleted)
3788            .unwrap();
3789
3790        let client_order_id = ClientOrderId::from("O-ALGO-DISPATCH");
3791        let command = ModifyOrder::new(
3792            TraderId::from("TRADER-001"),
3793            None,
3794            StrategyId::from("STRAT-ALGO-DISPATCH"),
3795            InstrumentId::from("BTC/USDT.BINANCE"),
3796            client_order_id,
3797            None,
3798            Some(Quantity::from("0.5")),
3799            None,
3800            None,
3801            UUID4::new(),
3802            0.into(),
3803            None,
3804            None,
3805        );
3806
3807        algo.execute(TradingCommand::ModifyOrder(command)).unwrap();
3808
3809        assert_eq!(algo.modify_client_order_ids, vec![client_order_id]);
3810    }
3811
3812    #[rstest]
3813    fn test_algorithm_handle_modify_order_refuses_active_local_order_without_events() {
3814        let mut algo = create_test_algorithm();
3815        register_algorithm(&mut algo);
3816
3817        let strategy_id = StrategyId::from("STRAT-ALGO-MODIFY");
3818        let order = OrderTestBuilder::new(OrderType::Market)
3819            .trader_id(TraderId::from("TRADER-001"))
3820            .strategy_id(strategy_id)
3821            .instrument_id(InstrumentId::from("BTC/USDT.BINANCE"))
3822            .client_order_id(ClientOrderId::from("O-ALGO-MODIFY"))
3823            .quantity(Quantity::from("1.0"))
3824            .exec_algorithm_id(algo.id())
3825            .exec_spawn_id(ClientOrderId::from("O-ALGO-MODIFY"))
3826            .build();
3827        {
3828            let cache_rc = algo.core.cache_rc();
3829            cache_rc
3830                .borrow_mut()
3831                .add_order(order.clone(), None, None, false)
3832                .unwrap();
3833        }
3834        let (handler, events) = subscribe_order_topic(strategy_id);
3835        let command = ModifyOrder::new(
3836            order.trader_id(),
3837            None,
3838            strategy_id,
3839            order.instrument_id(),
3840            order.client_order_id(),
3841            None,
3842            Some(Quantity::from("0.5")),
3843            None,
3844            None,
3845            UUID4::new(),
3846            0.into(),
3847            None,
3848            None,
3849        );
3850
3851        algo.execute(TradingCommand::ModifyOrder(command)).unwrap();
3852
3853        msgbus::unsubscribe_order_events(format!("events.order.{strategy_id}").into(), &handler);
3854        let cached_order = algo.cache().order(&order.client_order_id()).unwrap();
3855        assert_eq!(cached_order.status(), OrderStatus::Initialized);
3856        assert_eq!(cached_order.quantity(), Quantity::from("1.0"));
3857        assert!(events.borrow().is_empty());
3858    }
3859
3860    #[rstest]
3861    fn test_algorithm_modify_order_in_place_updates_quantity() {
3862        let mut algo = create_test_algorithm();
3863        register_algorithm(&mut algo);
3864
3865        let strategy_id = StrategyId::from("STRAT-ALGO-MODIFY-IN-PLACE");
3866        let mut order = OrderAny::Limit(LimitOrder::new(
3867            TraderId::from("TRADER-001"),
3868            strategy_id,
3869            InstrumentId::from("BTC/USDT.BINANCE"),
3870            ClientOrderId::from("O-001"),
3871            OrderSide::Buy,
3872            Quantity::from("1.0"),
3873            Price::from("50000.0"),
3874            TimeInForce::Gtc,
3875            None,  // expire_time
3876            false, // post_only
3877            false, // reduce_only
3878            false, // quote_quantity
3879            None,  // display_qty
3880            None,  // emulation_trigger
3881            None,  // trigger_instrument_id
3882            None,  // contingency_type
3883            None,  // order_list_id
3884            None,  // linked_order_ids
3885            None,  // parent_order_id
3886            None,  // exec_algorithm_id
3887            None,  // exec_algorithm_params
3888            None,  // exec_spawn_id
3889            None,  // tags
3890            UUID4::new(),
3891            0.into(),
3892        ));
3893
3894        {
3895            let cache_rc = algo.core.cache_rc();
3896            let mut cache = cache_rc.borrow_mut();
3897            cache.add_order(order.clone(), None, None, false).unwrap();
3898        }
3899
3900        let new_qty = Quantity::from("0.5");
3901        let (handler, events) = subscribe_order_topic(strategy_id);
3902
3903        algo.modify_order_in_place(&mut order, Some(new_qty), None, None)
3904            .unwrap();
3905
3906        msgbus::unsubscribe_order_events(format!("events.order.{strategy_id}").into(), &handler);
3907        let events = events.borrow();
3908
3909        assert_eq!(order.quantity(), new_qty);
3910        assert_eq!(events.len(), 1);
3911        assert!(matches!(
3912            &events[0],
3913            OrderEventAny::Updated(event) if event.quantity == new_qty
3914        ));
3915    }
3916
3917    #[rstest]
3918    fn test_algorithm_modify_order_in_place_rejects_no_changes() {
3919        let mut algo = create_test_algorithm();
3920        register_algorithm(&mut algo);
3921
3922        let mut order = OrderAny::Limit(LimitOrder::new(
3923            TraderId::from("TRADER-001"),
3924            StrategyId::from("STRAT-001"),
3925            InstrumentId::from("BTC/USDT.BINANCE"),
3926            ClientOrderId::from("O-001"),
3927            OrderSide::Buy,
3928            Quantity::from("1.0"),
3929            Price::from("50000.0"),
3930            TimeInForce::Gtc,
3931            None,
3932            false,
3933            false,
3934            false,
3935            None,
3936            None,
3937            None,
3938            None,
3939            None,
3940            None,
3941            None,
3942            None,
3943            None,
3944            None,
3945            None,
3946            UUID4::new(),
3947            0.into(),
3948        ));
3949
3950        // Try to modify with same quantity - should fail
3951        let result =
3952            algo.modify_order_in_place(&mut order, Some(Quantity::from("1.0")), None, None);
3953
3954        assert!(result.is_err());
3955        assert!(
3956            result
3957                .unwrap_err()
3958                .to_string()
3959                .contains("no parameters differ")
3960        );
3961    }
3962
3963    #[rstest]
3964    fn test_spawned_order_denied_restores_primary_quantity() {
3965        let mut algo = create_test_algorithm();
3966        register_algorithm(&mut algo);
3967
3968        let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
3969        let exec_algorithm_id = algo.id();
3970        let client_order_id = ClientOrderId::from("O-001");
3971
3972        let mut primary = OrderAny::Market(MarketOrder::new(
3973            TraderId::from("TRADER-001"),
3974            StrategyId::from("STRAT-001"),
3975            instrument_id,
3976            client_order_id,
3977            OrderSide::Buy,
3978            Quantity::from("1.0"),
3979            TimeInForce::Gtc,
3980            UUID4::new(),
3981            0.into(),
3982            false,
3983            false,
3984            None,
3985            None,
3986            None,
3987            None,
3988            Some(exec_algorithm_id),
3989            None,
3990            Some(client_order_id),
3991            None,
3992        ));
3993
3994        {
3995            let cache_rc = algo.core.cache_rc();
3996            let mut cache = cache_rc.borrow_mut();
3997            cache.add_order(primary.clone(), None, None, false).unwrap();
3998        }
3999
4000        let spawned = algo.spawn_market(
4001            &mut primary,
4002            Quantity::from("0.5"),
4003            TimeInForce::Fok,
4004            false,
4005            None,
4006            true,
4007        );
4008
4009        assert_eq!(primary.quantity(), Quantity::from("0.5"));
4010
4011        let spawned_order = OrderAny::Market(spawned);
4012        {
4013            let cache_rc = algo.core.cache_rc();
4014            let mut cache = cache_rc.borrow_mut();
4015            cache
4016                .add_order(spawned_order.clone(), None, None, false)
4017                .unwrap();
4018        }
4019
4020        let denied = OrderDeniedSpec::builder()
4021            .trader_id(spawned_order.trader_id())
4022            .strategy_id(spawned_order.strategy_id())
4023            .instrument_id(spawned_order.instrument_id())
4024            .client_order_id(spawned_order.client_order_id())
4025            .reason("TEST_DENIAL".into())
4026            .build();
4027
4028        {
4029            let cache_rc = algo.core.cache_rc();
4030            let mut cache = cache_rc.borrow_mut();
4031            cache.update_order(&OrderEventAny::Denied(denied)).unwrap();
4032        }
4033
4034        algo.handle_order_event(OrderEventAny::Denied(denied));
4035
4036        let restored_primary = algo.cache().order(&client_order_id).unwrap();
4037        assert_eq!(restored_primary.quantity(), Quantity::from("1.0"));
4038    }
4039
4040    #[rstest]
4041    fn test_spawned_order_rejected_restores_primary_quantity() {
4042        let mut algo = create_test_algorithm();
4043        register_algorithm(&mut algo);
4044
4045        let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
4046        let exec_algorithm_id = algo.id();
4047        let client_order_id = ClientOrderId::from("O-001");
4048
4049        let mut primary = OrderAny::Market(MarketOrder::new(
4050            TraderId::from("TRADER-001"),
4051            StrategyId::from("STRAT-001"),
4052            instrument_id,
4053            client_order_id,
4054            OrderSide::Buy,
4055            Quantity::from("1.0"),
4056            TimeInForce::Gtc,
4057            UUID4::new(),
4058            0.into(),
4059            false,
4060            false,
4061            None,
4062            None,
4063            None,
4064            None,
4065            Some(exec_algorithm_id),
4066            None,
4067            Some(client_order_id),
4068            None,
4069        ));
4070
4071        {
4072            let cache_rc = algo.core.cache_rc();
4073            let mut cache = cache_rc.borrow_mut();
4074            cache.add_order(primary.clone(), None, None, false).unwrap();
4075        }
4076
4077        let spawned = algo.spawn_market(
4078            &mut primary,
4079            Quantity::from("0.5"),
4080            TimeInForce::Fok,
4081            false,
4082            None,
4083            true,
4084        );
4085
4086        assert_eq!(primary.quantity(), Quantity::from("0.5"));
4087
4088        let spawned_order = OrderAny::Market(spawned);
4089        {
4090            let cache_rc = algo.core.cache_rc();
4091            let mut cache = cache_rc.borrow_mut();
4092            cache
4093                .add_order(spawned_order.clone(), None, None, false)
4094                .unwrap();
4095        }
4096
4097        let rejected = OrderRejectedSpec::builder()
4098            .trader_id(spawned_order.trader_id())
4099            .strategy_id(spawned_order.strategy_id())
4100            .instrument_id(spawned_order.instrument_id())
4101            .client_order_id(spawned_order.client_order_id())
4102            .account_id(AccountId::from("BINANCE-001"))
4103            .reason("TEST_REJECTION".into())
4104            .build();
4105
4106        {
4107            let cache_rc = algo.core.cache_rc();
4108            let mut cache = cache_rc.borrow_mut();
4109            cache
4110                .update_order(&OrderEventAny::Rejected(rejected))
4111                .unwrap();
4112        }
4113
4114        algo.handle_order_event(OrderEventAny::Rejected(rejected));
4115
4116        let restored_primary = algo.cache().order(&client_order_id).unwrap();
4117        assert_eq!(restored_primary.quantity(), Quantity::from("1.0"));
4118    }
4119
4120    #[rstest]
4121    fn test_spawned_order_with_reduce_primary_false_does_not_restore() {
4122        let mut algo = create_test_algorithm();
4123        register_algorithm(&mut algo);
4124
4125        let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
4126        let exec_algorithm_id = algo.id();
4127        let client_order_id = ClientOrderId::from("O-001");
4128
4129        let mut primary = OrderAny::Market(MarketOrder::new(
4130            TraderId::from("TRADER-001"),
4131            StrategyId::from("STRAT-001"),
4132            instrument_id,
4133            client_order_id,
4134            OrderSide::Buy,
4135            Quantity::from("1.0"),
4136            TimeInForce::Gtc,
4137            UUID4::new(),
4138            0.into(),
4139            false,
4140            false,
4141            None,
4142            None,
4143            None,
4144            None,
4145            Some(exec_algorithm_id),
4146            None,
4147            Some(client_order_id),
4148            None,
4149        ));
4150
4151        {
4152            let cache_rc = algo.core.cache_rc();
4153            let mut cache = cache_rc.borrow_mut();
4154            cache.add_order(primary.clone(), None, None, false).unwrap();
4155        }
4156
4157        let spawned = algo.spawn_market(
4158            &mut primary,
4159            Quantity::from("0.5"),
4160            TimeInForce::Fok,
4161            false,
4162            None,
4163            false,
4164        );
4165
4166        assert_eq!(primary.quantity(), Quantity::from("1.0"));
4167
4168        let spawned_order = OrderAny::Market(spawned);
4169        {
4170            let cache_rc = algo.core.cache_rc();
4171            let mut cache = cache_rc.borrow_mut();
4172            cache
4173                .add_order(spawned_order.clone(), None, None, false)
4174                .unwrap();
4175        }
4176
4177        let denied = OrderDeniedSpec::builder()
4178            .trader_id(spawned_order.trader_id())
4179            .strategy_id(spawned_order.strategy_id())
4180            .instrument_id(spawned_order.instrument_id())
4181            .client_order_id(spawned_order.client_order_id())
4182            .reason("TEST_DENIAL".into())
4183            .build();
4184
4185        {
4186            let cache_rc = algo.core.cache_rc();
4187            let mut cache = cache_rc.borrow_mut();
4188            cache.update_order(&OrderEventAny::Denied(denied)).unwrap();
4189        }
4190
4191        algo.handle_order_event(OrderEventAny::Denied(denied));
4192
4193        let final_primary = algo.cache().order(&client_order_id).unwrap();
4194        assert_eq!(final_primary.quantity(), Quantity::from("1.0"));
4195    }
4196
4197    #[rstest]
4198    fn test_multiple_spawns_with_one_denied_restores_correctly() {
4199        let mut algo = create_test_algorithm();
4200        register_algorithm(&mut algo);
4201
4202        let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
4203        let exec_algorithm_id = algo.id();
4204        let client_order_id = ClientOrderId::from("O-001");
4205
4206        let mut primary = OrderAny::Market(MarketOrder::new(
4207            TraderId::from("TRADER-001"),
4208            StrategyId::from("STRAT-001"),
4209            instrument_id,
4210            client_order_id,
4211            OrderSide::Buy,
4212            Quantity::from("1.0"),
4213            TimeInForce::Gtc,
4214            UUID4::new(),
4215            0.into(),
4216            false,
4217            false,
4218            None,
4219            None,
4220            None,
4221            None,
4222            Some(exec_algorithm_id),
4223            None,
4224            Some(client_order_id),
4225            None,
4226        ));
4227
4228        {
4229            let cache_rc = algo.core.cache_rc();
4230            let mut cache = cache_rc.borrow_mut();
4231            cache.add_order(primary.clone(), None, None, false).unwrap();
4232        }
4233
4234        let spawned1 = algo.spawn_market(
4235            &mut primary,
4236            Quantity::from("0.3"),
4237            TimeInForce::Fok,
4238            false,
4239            None,
4240            true,
4241        );
4242        let spawned2 = algo.spawn_market(
4243            &mut primary,
4244            Quantity::from("0.4"),
4245            TimeInForce::Fok,
4246            false,
4247            None,
4248            true,
4249        );
4250        assert_eq!(primary.quantity(), Quantity::from("0.3"));
4251
4252        let spawned_order1 = OrderAny::Market(spawned1);
4253        let spawned_order2 = OrderAny::Market(spawned2);
4254        {
4255            let cache_rc = algo.core.cache_rc();
4256            let mut cache = cache_rc.borrow_mut();
4257            cache.add_order(spawned_order1, None, None, false).unwrap();
4258            cache
4259                .add_order(spawned_order2.clone(), None, None, false)
4260                .unwrap();
4261        }
4262
4263        let denied = OrderDeniedSpec::builder()
4264            .trader_id(spawned_order2.trader_id())
4265            .strategy_id(spawned_order2.strategy_id())
4266            .instrument_id(spawned_order2.instrument_id())
4267            .client_order_id(spawned_order2.client_order_id())
4268            .reason("TEST_DENIAL".into())
4269            .build();
4270
4271        {
4272            let cache_rc = algo.core.cache_rc();
4273            let mut cache = cache_rc.borrow_mut();
4274            cache.update_order(&OrderEventAny::Denied(denied)).unwrap();
4275        }
4276
4277        let (handler, events) = subscribe_order_topic(spawned_order2.strategy_id());
4278
4279        algo.handle_order_event(OrderEventAny::Denied(denied));
4280
4281        msgbus::unsubscribe_order_events(
4282            format!("events.order.{}", spawned_order2.strategy_id()).into(),
4283            &handler,
4284        );
4285        let events = events.borrow();
4286
4287        let restored_primary = algo.cache().order(&client_order_id).unwrap();
4288        assert_eq!(restored_primary.quantity(), Quantity::from("0.7"));
4289        assert_eq!(events.len(), 1);
4290        assert!(matches!(
4291            &events[0],
4292            OrderEventAny::Updated(event) if event.quantity == Quantity::from("0.7")
4293        ));
4294    }
4295
4296    #[rstest]
4297    fn test_spawned_order_accepted_then_canceled_restores_reduction() {
4298        let (mut algo, client_order_id, mut spawned_order) = setup_accepted_spawn();
4299
4300        let primary_after_accept = algo.cache().order(&client_order_id).unwrap();
4301        assert_eq!(primary_after_accept.quantity(), Quantity::from("0.5"));
4302
4303        // Per the maintainer's ruling, acceptance preserves the reduction until terminal outcome
4304        cancel_spawned_order(&mut algo, &mut spawned_order);
4305
4306        let final_primary = algo.cache().order(&client_order_id).unwrap();
4307        assert_eq!(final_primary.quantity(), Quantity::from("1.0"));
4308    }
4309
4310    #[rstest]
4311    fn test_spawned_order_canceled_after_primary_submission_does_not_restore_reduction() {
4312        let (mut algo, client_order_id, mut spawned_order) = setup_accepted_spawn();
4313        let mut primary = algo.cache().order(&client_order_id).unwrap();
4314        submit_order_in_cache(&mut algo, &mut primary);
4315
4316        cancel_spawned_order(&mut algo, &mut spawned_order);
4317
4318        let mut submitted_primary = algo.cache().order(&client_order_id).unwrap();
4319        assert_eq!(submitted_primary.quantity(), Quantity::from("0.5"));
4320        assert!(
4321            algo.core
4322                .take_pending_spawn_reduction(spawned_order.client_order_id())
4323                .is_none()
4324        );
4325        fill_spawned_order(&mut algo, &mut submitted_primary, Quantity::from("0.5"));
4326        assert!(submitted_primary.is_closed());
4327        assert_eq!(submitted_primary.status(), OrderStatus::Filled);
4328    }
4329
4330    #[rstest]
4331    fn test_spawned_order_canceled_during_primary_submission_handoff_does_not_restore() {
4332        let (mut algo, client_order_id, mut spawned_order) = setup_accepted_spawn();
4333        let primary = algo.cache().order(&client_order_id).unwrap();
4334        algo.core
4335            .add_spawn_fill_debt(client_order_id, Quantity::from("0.1"));
4336        let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
4337            get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
4338        msgbus::register_trading_command_endpoint(
4339            MessagingSwitchboard::risk_engine_queue_execute(),
4340            risk_handler,
4341        );
4342        let strategy_id = primary.strategy_id();
4343        let (event_handler, events) = subscribe_order_topic(strategy_id);
4344
4345        algo.submit_order(primary, None, None).unwrap();
4346        assert_eq!(
4347            algo.cache().order(&client_order_id).unwrap().status(),
4348            OrderStatus::Initialized,
4349        );
4350        cancel_spawned_order(&mut algo, &mut spawned_order);
4351
4352        msgbus::unsubscribe_order_events(
4353            format!("events.order.{strategy_id}").into(),
4354            &event_handler,
4355        );
4356        let risk_messages = risk_messages.get_messages();
4357        let [TradingCommand::SubmitOrder(command)] = risk_messages.as_slice() else {
4358            panic!("Expected exactly one SubmitOrder command");
4359        };
4360        assert_eq!(command.client_order_id, client_order_id);
4361        // The command embeds the immutable OrderInitialized event, so its
4362        // quantity is the pre-reduction 1.0; consumers resolve the cached
4363        // order, which carries the reduced quantity asserted below.
4364        assert_eq!(command.order_init.quantity, Quantity::from("1.0"));
4365        assert_eq!(
4366            algo.cache().order(&client_order_id).unwrap().quantity(),
4367            Quantity::from("0.5"),
4368        );
4369        assert!(!events.borrow().iter().any(|event| matches!(
4370            event,
4371            OrderEventAny::Updated(updated) if updated.client_order_id == client_order_id
4372        )));
4373        assert!(
4374            algo.core
4375                .take_pending_spawn_reduction(spawned_order.client_order_id())
4376                .is_none()
4377        );
4378        assert!(algo.core.spawn_fill_debt(client_order_id).is_none());
4379    }
4380
4381    #[rstest]
4382    fn test_late_spawn_fill_rededucts_restored_primary_before_final_submission() {
4383        let (mut algo, client_order_id, mut spawned_order) = setup_accepted_spawn();
4384        cancel_spawned_order(&mut algo, &mut spawned_order);
4385        assert_eq!(
4386            algo.cache().order(&client_order_id).unwrap().quantity(),
4387            Quantity::from("1.0"),
4388        );
4389
4390        fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("0.5"));
4391
4392        let mut primary = algo.cache().order(&client_order_id).unwrap();
4393        assert_eq!(primary.quantity(), Quantity::from("0.5"));
4394        submit_order_in_cache(&mut algo, &mut primary);
4395        assert_eq!(
4396            spawned_order.filled_qty() + primary.quantity(),
4397            Quantity::from("1.0"),
4398        );
4399    }
4400
4401    #[rstest]
4402    fn test_late_spawn_fill_during_primary_submission_handoff_does_not_rededuct() {
4403        let (mut algo, client_order_id, mut spawned_order) = setup_accepted_spawn();
4404        cancel_spawned_order(&mut algo, &mut spawned_order);
4405        let primary = algo.cache().order(&client_order_id).unwrap();
4406        algo.core
4407            .add_spawn_fill_debt(client_order_id, Quantity::from("0.1"));
4408        let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
4409            get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
4410        msgbus::register_trading_command_endpoint(
4411            MessagingSwitchboard::risk_engine_queue_execute(),
4412            risk_handler,
4413        );
4414        let strategy_id = primary.strategy_id();
4415        let (event_handler, events) = subscribe_order_topic(strategy_id);
4416
4417        algo.submit_order(primary, None, None).unwrap();
4418        assert_eq!(
4419            algo.cache().order(&client_order_id).unwrap().status(),
4420            OrderStatus::Initialized,
4421        );
4422        fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("0.5"));
4423
4424        msgbus::unsubscribe_order_events(
4425            format!("events.order.{strategy_id}").into(),
4426            &event_handler,
4427        );
4428        let risk_messages = risk_messages.get_messages();
4429        let [TradingCommand::SubmitOrder(command)] = risk_messages.as_slice() else {
4430            panic!("Expected exactly one SubmitOrder command");
4431        };
4432        assert_eq!(command.client_order_id, client_order_id);
4433        // The command embeds the immutable OrderInitialized event; here the
4434        // cancellation already restored the cache to 1.0, so the two agree.
4435        assert_eq!(command.order_init.quantity, Quantity::from("1.0"));
4436        assert_eq!(
4437            algo.cache().order(&client_order_id).unwrap().quantity(),
4438            Quantity::from("1.0"),
4439        );
4440        assert!(!events.borrow().iter().any(|event| matches!(
4441            event,
4442            OrderEventAny::Updated(updated) if updated.client_order_id == client_order_id
4443        )));
4444        assert!(
4445            algo.core
4446                .take_pending_spawn_reduction(spawned_order.client_order_id())
4447                .is_none()
4448        );
4449        assert!(algo.core.spawn_fill_debt(client_order_id).is_none());
4450    }
4451
4452    #[rstest]
4453    fn test_converted_quote_spawn_canceled_unfilled_restores_full_quote_quantity() {
4454        let (mut algo, client_order_id, mut spawned_order) =
4455            setup_accepted_quote_spawn(Quantity::from("100"), Quantity::from("50"));
4456        convert_spawn_to_base(&mut algo, &mut spawned_order, Quantity::from("5"));
4457
4458        cancel_spawned_order(&mut algo, &mut spawned_order);
4459
4460        assert_eq!(
4461            algo.cache().order(&client_order_id).unwrap().quantity(),
4462            Quantity::from("100"),
4463        );
4464        assert!(
4465            algo.cache()
4466                .order(&client_order_id)
4467                .unwrap()
4468                .is_quote_quantity()
4469        );
4470        assert_eq!(
4471            algo.core
4472                .spawn_reduction(spawned_order.client_order_id())
4473                .unwrap()
4474                .restored_qty
4475                .unwrap(),
4476            Quantity::from("50"),
4477        );
4478    }
4479
4480    #[rstest]
4481    fn test_converted_quote_spawn_partial_fill_restores_proportional_quote_quantity() {
4482        let (mut algo, client_order_id, mut spawned_order) =
4483            setup_accepted_quote_spawn(Quantity::from("100"), Quantity::from("50"));
4484        convert_spawn_to_base(&mut algo, &mut spawned_order, Quantity::from("5"));
4485        fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("2"));
4486
4487        cancel_spawned_order(&mut algo, &mut spawned_order);
4488
4489        assert_eq!(
4490            algo.cache().order(&client_order_id).unwrap().quantity(),
4491            Quantity::from("80"),
4492        );
4493        assert!(
4494            algo.cache()
4495                .order(&client_order_id)
4496                .unwrap()
4497                .is_quote_quantity()
4498        );
4499        assert_eq!(
4500            algo.core
4501                .spawn_reduction(spawned_order.client_order_id())
4502                .unwrap()
4503                .restored_qty
4504                .unwrap(),
4505            Quantity::from("30"),
4506        );
4507    }
4508
4509    #[rstest]
4510    fn test_converted_quote_spawn_late_fill_charges_proportional_quote_quantity() {
4511        let (mut algo, client_order_id, mut spawned_order) =
4512            setup_accepted_quote_spawn(Quantity::from("100"), Quantity::from("50"));
4513        convert_spawn_to_base(&mut algo, &mut spawned_order, Quantity::from("5"));
4514        cancel_spawned_order(&mut algo, &mut spawned_order);
4515
4516        fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("1"));
4517
4518        assert_eq!(
4519            algo.cache().order(&client_order_id).unwrap().quantity(),
4520            Quantity::from("90"),
4521        );
4522        assert!(
4523            algo.cache()
4524                .order(&client_order_id)
4525                .unwrap()
4526                .is_quote_quantity()
4527        );
4528        assert_eq!(
4529            algo.core
4530                .spawn_reduction(spawned_order.client_order_id())
4531                .unwrap()
4532                .restored_qty
4533                .unwrap(),
4534            Quantity::from("40"),
4535        );
4536    }
4537
4538    #[rstest]
4539    fn test_converted_quote_spawn_restoration_rounds_down_to_primary_precision() {
4540        let (mut algo, client_order_id, mut spawned_order) =
4541            setup_accepted_quote_spawn(Quantity::from("20.00"), Quantity::from("10.00"));
4542        convert_spawn_to_base(&mut algo, &mut spawned_order, Quantity::from("3.000"));
4543        fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("1.000"));
4544
4545        cancel_spawned_order(&mut algo, &mut spawned_order);
4546
4547        assert_eq!(
4548            algo.cache().order(&client_order_id).unwrap().quantity(),
4549            Quantity::from("16.66"),
4550        );
4551        assert!(
4552            algo.cache()
4553                .order(&client_order_id)
4554                .unwrap()
4555                .is_quote_quantity()
4556        );
4557        assert_eq!(
4558            algo.core
4559                .spawn_reduction(spawned_order.client_order_id())
4560                .unwrap()
4561                .restored_qty
4562                .unwrap(),
4563            Quantity::from("6.66"),
4564        );
4565    }
4566
4567    #[rstest]
4568    fn test_converted_quote_spawn_repeated_fractional_late_fills_conserve_budget() {
4569        let (mut algo, client_order_id, mut spawned_order) =
4570            setup_accepted_quote_spawn(Quantity::from("20.00"), Quantity::from("10.00"));
4571        convert_spawn_to_base(&mut algo, &mut spawned_order, Quantity::from("3.000"));
4572        cancel_spawned_order(&mut algo, &mut spawned_order);
4573
4574        for (primary_qty, restored_qty) in [
4575            (Quantity::from("16.66"), Quantity::from("6.66")),
4576            (Quantity::from("13.33"), Quantity::from("3.33")),
4577            (Quantity::from("10.00"), Quantity::from("0.00")),
4578        ] {
4579            fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("1.000"));
4580            assert_eq!(
4581                algo.cache().order(&client_order_id).unwrap().quantity(),
4582                primary_qty,
4583            );
4584            assert_eq!(
4585                algo.core
4586                    .spawn_reduction(spawned_order.client_order_id())
4587                    .unwrap()
4588                    .restored_qty
4589                    .unwrap(),
4590                restored_qty,
4591            );
4592        }
4593    }
4594
4595    #[rstest]
4596    #[case::single_fill(Quantity::from("3.0"), 1)]
4597    #[case::split_fills(Quantity::from("0.1"), 30)]
4598    fn test_converted_quote_spawn_fill_partition_preserves_total(
4599        #[case] fill_qty: Quantity,
4600        #[case] fills: usize,
4601    ) {
4602        let (mut algo, primary_id, mut child) =
4603            setup_accepted_quote_spawn(Quantity::from("20"), Quantity::from("10"));
4604        convert_spawn_to_base(&mut algo, &mut child, Quantity::from("3.0"));
4605        cancel_spawned_order(&mut algo, &mut child);
4606
4607        for _ in 0..fills {
4608            fill_spawned_order(&mut algo, &mut child, fill_qty);
4609        }
4610
4611        assert_eq!(child.filled_qty(), Quantity::from("3.0"));
4612        assert_eq!(
4613            algo.cache().order(&primary_id).unwrap().quantity(),
4614            Quantity::from("10")
4615        );
4616        assert_eq!(
4617            algo.core
4618                .spawn_reduction(child.client_order_id())
4619                .unwrap()
4620                .restored_qty,
4621            Some(Quantity::from("0"))
4622        );
4623        assert!(algo.core.spawn_fill_debt(primary_id).is_none());
4624    }
4625
4626    #[rstest]
4627    fn test_increased_spawn_late_fill_debits_original_restoration() {
4628        let (mut algo, primary_id, mut child) = setup_pending_spawn();
4629        algo.modify_order_in_place(&mut child, Some(Quantity::from("0.8")), None, None)
4630            .unwrap();
4631        accept_spawned_order(&mut algo, &mut child);
4632        cancel_spawned_order(&mut algo, &mut child);
4633
4634        fill_spawned_order(&mut algo, &mut child, Quantity::from("0.1"));
4635
4636        assert_eq!(child.quantity(), Quantity::from("0.8"));
4637        assert_eq!(child.filled_qty(), Quantity::from("0.1"));
4638        assert_eq!(
4639            algo.cache().order(&primary_id).unwrap().quantity(),
4640            Quantity::from("0.9")
4641        );
4642        assert_eq!(
4643            algo.core
4644                .spawn_reduction(child.client_order_id())
4645                .unwrap()
4646                .restored_qty,
4647            Some(Quantity::from("0.4"))
4648        );
4649        assert!(algo.core.spawn_fill_debt(primary_id).is_none());
4650
4651        void_last_spawn_fill(&mut algo, &mut child, Quantity::from("0.1"));
4652
4653        assert_eq!(
4654            algo.cache().order(&primary_id).unwrap().quantity(),
4655            Quantity::from("1.0")
4656        );
4657    }
4658
4659    #[rstest]
4660    #[case::partial_fill(Quantity::from("0.1"))]
4661    #[case::full_fill(Quantity::from("0.5"))]
4662    fn test_voided_late_spawn_fill_restores_primary_quantity(#[case] fill_qty: Quantity) {
4663        let (mut algo, primary_id, mut child) = setup_accepted_spawn();
4664        cancel_spawned_order(&mut algo, &mut child);
4665        fill_spawned_order(&mut algo, &mut child, fill_qty);
4666
4667        void_last_spawn_fill(&mut algo, &mut child, fill_qty);
4668
4669        assert_eq!(child.filled_qty(), Quantity::from("0.0"));
4670        assert_eq!(
4671            algo.cache().order(&primary_id).unwrap().quantity(),
4672            Quantity::from("1.0")
4673        );
4674        assert!(algo.core.spawn_fill_debt(primary_id).is_none());
4675        assert_eq!(
4676            algo.core
4677                .spawn_reduction(child.client_order_id())
4678                .unwrap()
4679                .restored_qty,
4680            Some(Quantity::from("0.5"))
4681        );
4682    }
4683
4684    #[rstest]
4685    #[case::debt_only(
4686        Quantity::from("0.1"),
4687        Quantity::from("0.0"),
4688        Some(Quantity::from("0.1"))
4689    )]
4690    #[case::debt_and_quantity(Quantity::from("0.3"), Quantity::from("0.1"), None)]
4691    fn test_voided_late_spawn_fill_discharges_debt_before_restoring_quantity(
4692        #[case] voided_qty: Quantity,
4693        #[case] primary_qty: Quantity,
4694        #[case] debt_qty: Option<Quantity>,
4695    ) {
4696        let (mut algo, primary_id, mut child) = setup_accepted_spawn();
4697        cancel_spawned_order(&mut algo, &mut child);
4698        let _second = spawn_reduced_child(&mut algo, primary_id, Quantity::from("0.8"));
4699        fill_spawned_order(&mut algo, &mut child, Quantity::from("0.4"));
4700        assert_eq!(
4701            algo.core.spawn_fill_debt(primary_id),
4702            Some(Quantity::from("0.2"))
4703        );
4704
4705        void_last_spawn_fill(&mut algo, &mut child, voided_qty);
4706
4707        assert_eq!(
4708            algo.cache().order(&primary_id).unwrap().quantity(),
4709            primary_qty
4710        );
4711        assert_eq!(algo.core.spawn_fill_debt(primary_id), debt_qty);
4712        assert_eq!(child.filled_qty(), Quantity::from("0.4") - voided_qty);
4713        assert_eq!(
4714            algo.core
4715                .spawn_reduction(child.client_order_id())
4716                .unwrap()
4717                .restored_qty,
4718            Some(Quantity::from("0.1") + voided_qty)
4719        );
4720    }
4721
4722    #[rstest]
4723    fn test_spawn_fill_void_before_restoration_preserves_reserved_quantity() {
4724        let (mut algo, primary_id, mut child) = setup_accepted_spawn();
4725        fill_spawned_order(&mut algo, &mut child, Quantity::from("0.1"));
4726
4727        void_last_spawn_fill(&mut algo, &mut child, Quantity::from("0.1"));
4728
4729        assert_eq!(
4730            algo.cache().order(&primary_id).unwrap().quantity(),
4731            Quantity::from("0.5")
4732        );
4733        assert_eq!(
4734            algo.core
4735                .spawn_reduction(child.client_order_id())
4736                .unwrap()
4737                .restored_qty,
4738            None
4739        );
4740        cancel_spawned_order(&mut algo, &mut child);
4741        assert_eq!(
4742            algo.cache().order(&primary_id).unwrap().quantity(),
4743            Quantity::from("1.0")
4744        );
4745    }
4746
4747    #[rstest]
4748    fn test_spawn_fill_void_after_primary_handoff_preserves_submitted_quantity() {
4749        let (mut algo, primary_id, mut child) = setup_accepted_spawn();
4750        cancel_spawned_order(&mut algo, &mut child);
4751        fill_spawned_order(&mut algo, &mut child, Quantity::from("0.1"));
4752        let primary = algo.cache().order(&primary_id).unwrap();
4753        let (handler, _messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
4754            get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
4755        msgbus::register_trading_command_endpoint(
4756            MessagingSwitchboard::risk_engine_queue_execute(),
4757            handler,
4758        );
4759        algo.submit_order(primary, None, None).unwrap();
4760
4761        void_last_spawn_fill(&mut algo, &mut child, Quantity::from("0.1"));
4762
4763        assert_eq!(
4764            algo.cache().order(&primary_id).unwrap().quantity(),
4765            Quantity::from("0.9")
4766        );
4767        assert_eq!(
4768            algo.cache().order(&primary_id).unwrap().status(),
4769            OrderStatus::Initialized
4770        );
4771        assert!(algo.core.primary_was_handed_off(primary_id));
4772        assert!(algo.core.spawn_reduction(child.client_order_id()).is_none());
4773        assert!(algo.core.spawn_fill_debt(primary_id).is_none());
4774    }
4775
4776    #[rstest]
4777    fn test_converted_quote_spawn_fill_void_restores_cumulative_budget_once() {
4778        let (mut algo, primary_id, mut child) =
4779            setup_accepted_quote_spawn(Quantity::from("20.00"), Quantity::from("10.00"));
4780        convert_spawn_to_base(&mut algo, &mut child, Quantity::from("3.000"));
4781        cancel_spawned_order(&mut algo, &mut child);
4782        fill_spawned_order(&mut algo, &mut child, Quantity::from("1.000"));
4783        fill_spawned_order(&mut algo, &mut child, Quantity::from("1.000"));
4784
4785        void_last_spawn_fill(&mut algo, &mut child, Quantity::from("0.500"));
4786        assert_eq!(
4787            algo.cache().order(&primary_id).unwrap().quantity(),
4788            Quantity::from("15.00")
4789        );
4790        void_last_spawn_fill(&mut algo, &mut child, Quantity::from("1.000"));
4791        let duplicate = child.events().into_iter().last().unwrap().clone();
4792        algo.handle_order_event(duplicate);
4793
4794        assert_eq!(child.filled_qty(), Quantity::from("1.000"));
4795        assert_eq!(
4796            algo.cache().order(&primary_id).unwrap().quantity(),
4797            Quantity::from("16.66")
4798        );
4799        assert_eq!(
4800            algo.core
4801                .spawn_reduction(child.client_order_id())
4802                .unwrap()
4803                .restored_qty,
4804            Some(Quantity::from("6.66"))
4805        );
4806        assert!(algo.core.spawn_fill_debt(primary_id).is_none());
4807    }
4808
4809    fn void_last_spawn_fill(algo: &mut TestAlgorithm, order: &mut OrderAny, quantity: Quantity) {
4810        let fill = order
4811            .events()
4812            .into_iter()
4813            .rev()
4814            .find_map(|event| match event {
4815                OrderEventAny::Filled(fill) => Some(fill.clone()),
4816                _ => None,
4817            })
4818            .unwrap();
4819        let voided = OrderFillVoidedSpec::builder()
4820            .trader_id(fill.trader_id)
4821            .strategy_id(fill.strategy_id)
4822            .instrument_id(fill.instrument_id)
4823            .client_order_id(fill.client_order_id)
4824            .venue_order_id(fill.venue_order_id)
4825            .account_id(fill.account_id)
4826            .trade_id(fill.trade_id)
4827            .voided_qty(quantity)
4828            .order_side(fill.order_side)
4829            .order_type(fill.order_type)
4830            .last_px(fill.last_px)
4831            .currency(fill.currency)
4832            .liquidity_side(fill.liquidity_side)
4833            .build();
4834        *order = algo
4835            .core
4836            .cache_rc()
4837            .borrow_mut()
4838            .update_order(&OrderEventAny::FillVoided(voided.clone()))
4839            .unwrap();
4840        algo.handle_order_event(OrderEventAny::FillVoided(voided));
4841    }
4842
4843    #[rstest]
4844    fn test_primary_handoff_clears_spawn_accounting_without_child_events() {
4845        let (mut algo, primary_id, mut child) = setup_accepted_spawn();
4846        cancel_spawned_order(&mut algo, &mut child);
4847        let primary = algo.cache().order(&primary_id).unwrap();
4848        let (handler, _messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
4849            get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
4850        msgbus::register_trading_command_endpoint(
4851            MessagingSwitchboard::risk_engine_queue_execute(),
4852            handler,
4853        );
4854
4855        algo.submit_order(primary, None, None).unwrap();
4856
4857        assert!(algo.core.spawn_reduction(child.client_order_id()).is_none());
4858        assert!(algo.core.spawn_fill_debt(primary_id).is_none());
4859        assert!(algo.core.primary_was_handed_off(primary_id));
4860
4861        let mut primary = algo.cache().order(&primary_id).unwrap();
4862        submit_order_in_cache(&mut algo, &mut primary);
4863        assert!(!algo.core.primary_was_handed_off(primary_id));
4864    }
4865
4866    #[rstest]
4867    fn test_primary_cancellation_clears_spawn_accounting() {
4868        let (mut algo, primary_id, mut child) = setup_accepted_spawn();
4869        cancel_spawned_order(&mut algo, &mut child);
4870        let second = spawn_reduced_child(&mut algo, primary_id, Quantity::from("0.8"));
4871        fill_spawned_order(&mut algo, &mut child, Quantity::from("0.4"));
4872        let mut primary = algo.cache().order(&primary_id).unwrap();
4873
4874        cancel_spawned_order(&mut algo, &mut primary);
4875
4876        assert_eq!(primary.status(), OrderStatus::Canceled);
4877        assert!(algo.core.spawn_reduction(child.client_order_id()).is_none());
4878        assert!(
4879            algo.core
4880                .spawn_reduction(second.client_order_id())
4881                .is_none()
4882        );
4883        assert!(algo.core.spawn_fill_debt(primary_id).is_none());
4884        assert!(!algo.core.primary_was_handed_off(primary_id));
4885    }
4886
4887    #[rstest]
4888    fn test_converted_quote_spawn_partial_cancel_then_late_fill_charges_restored_budget() {
4889        let (mut algo, client_order_id, mut spawned_order) =
4890            setup_accepted_quote_spawn(Quantity::from("100"), Quantity::from("50"));
4891        convert_spawn_to_base(&mut algo, &mut spawned_order, Quantity::from("5"));
4892        fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("2"));
4893        cancel_spawned_order(&mut algo, &mut spawned_order);
4894
4895        assert_eq!(
4896            algo.cache().order(&client_order_id).unwrap().quantity(),
4897            Quantity::from("80"),
4898        );
4899        assert_eq!(
4900            algo.core
4901                .spawn_reduction(spawned_order.client_order_id())
4902                .unwrap()
4903                .restored_qty
4904                .unwrap(),
4905            Quantity::from("30"),
4906        );
4907
4908        fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("1"));
4909
4910        assert_eq!(
4911            algo.cache().order(&client_order_id).unwrap().quantity(),
4912            Quantity::from("70"),
4913        );
4914        assert_eq!(
4915            algo.core
4916                .spawn_reduction(spawned_order.client_order_id())
4917                .unwrap()
4918                .restored_qty
4919                .unwrap(),
4920            Quantity::from("20"),
4921        );
4922    }
4923
4924    #[rstest]
4925    fn test_unmarked_submitted_primary_cancellation_discards_reduction_and_debt() {
4926        let (mut algo, client_order_id, mut spawned_order) = setup_accepted_spawn();
4927        let mut primary = algo.cache().order(&client_order_id).unwrap();
4928        algo.core
4929            .add_spawn_fill_debt(client_order_id, Quantity::from("0.1"));
4930
4931        submit_order_in_cache(&mut algo, &mut primary);
4932        assert!(algo.core.spawn_fill_debt(client_order_id).is_none());
4933        cancel_spawned_order(&mut algo, &mut spawned_order);
4934
4935        assert_eq!(
4936            algo.cache().order(&client_order_id).unwrap().quantity(),
4937            Quantity::from("0.5"),
4938        );
4939        assert!(
4940            algo.core
4941                .spawn_reduction(spawned_order.client_order_id())
4942                .is_none()
4943        );
4944        assert!(algo.core.spawn_fill_debt(client_order_id).is_none());
4945    }
4946
4947    #[rstest]
4948    #[case::denied(true)]
4949    #[case::rejected(false)]
4950    fn test_spawn_refusal_does_not_restore_submitted_primary(#[case] denied: bool) {
4951        let (mut algo, client_order_id, spawned_order) = setup_pending_spawn();
4952        let spawned_id = spawned_order.client_order_id();
4953        let mut primary = algo.cache().order(&client_order_id).unwrap();
4954        submit_order_in_cache(&mut algo, &mut primary);
4955
4956        if denied {
4957            let event = OrderDeniedSpec::builder()
4958                .trader_id(spawned_order.trader_id())
4959                .strategy_id(spawned_order.strategy_id())
4960                .instrument_id(spawned_order.instrument_id())
4961                .client_order_id(spawned_id)
4962                .reason("TEST_DENIAL".into())
4963                .build();
4964            algo.handle_order_event(OrderEventAny::Denied(event));
4965        } else {
4966            let event = OrderRejectedSpec::builder()
4967                .trader_id(spawned_order.trader_id())
4968                .strategy_id(spawned_order.strategy_id())
4969                .instrument_id(spawned_order.instrument_id())
4970                .client_order_id(spawned_id)
4971                .account_id(AccountId::from("BINANCE-001"))
4972                .reason("TEST_REJECTION".into())
4973                .build();
4974            algo.handle_order_event(OrderEventAny::Rejected(event));
4975        }
4976
4977        assert_eq!(
4978            algo.cache().order(&client_order_id).unwrap().quantity(),
4979            Quantity::from("0.5"),
4980        );
4981        assert!(algo.core.take_pending_spawn_reduction(spawned_id).is_none());
4982    }
4983
4984    #[rstest]
4985    fn test_multiple_late_partial_fills_net_only_the_restored_quantity() {
4986        let (mut algo, client_order_id, mut spawned_order) = setup_accepted_spawn();
4987        fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("0.2"));
4988        cancel_spawned_order(&mut algo, &mut spawned_order);
4989
4990        fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("0.1"));
4991        assert_eq!(
4992            algo.cache().order(&client_order_id).unwrap().quantity(),
4993            Quantity::from("0.7"),
4994        );
4995
4996        fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("0.2"));
4997        assert_eq!(
4998            algo.cache().order(&client_order_id).unwrap().quantity(),
4999            Quantity::from("0.5"),
5000        );
5001        assert_eq!(
5002            algo.core
5003                .spawn_reduction(spawned_order.client_order_id())
5004                .unwrap()
5005                .restored_qty,
5006            Some(Quantity::from("0.0")),
5007        );
5008    }
5009
5010    #[rstest]
5011    fn test_late_spawn_fill_after_restored_quantity_reused_caps_at_primary_quantity() {
5012        let (mut algo, client_order_id, mut spawned_order) = setup_accepted_spawn();
5013        cancel_spawned_order(&mut algo, &mut spawned_order);
5014        assert_eq!(
5015            algo.cache().order(&client_order_id).unwrap().quantity(),
5016            Quantity::from("1.0"),
5017        );
5018
5019        // Reuse most of the restored quantity through a second spawn
5020        let mut primary = algo.cache().order(&client_order_id).unwrap();
5021        let second_order = OrderAny::Market(algo.spawn_market(
5022            &mut primary,
5023            Quantity::from("0.8"),
5024            TimeInForce::Fok,
5025            false,
5026            None,
5027            true,
5028        ));
5029        algo.core
5030            .cache_rc()
5031            .borrow_mut()
5032            .add_order(second_order.clone(), None, None, false)
5033            .unwrap();
5034        assert_eq!(
5035            algo.cache().order(&client_order_id).unwrap().quantity(),
5036            Quantity::from("0.2"),
5037        );
5038
5039        // The late fill exceeds the primary's remaining quantity: the
5040        // re-deduction caps at zero and the shortfall becomes debt
5041        fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("0.5"));
5042        assert_eq!(
5043            algo.cache().order(&client_order_id).unwrap().quantity(),
5044            Quantity::from("0.0"),
5045        );
5046        assert_eq!(
5047            algo.core
5048                .spawn_reduction(spawned_order.client_order_id())
5049                .unwrap()
5050                .restored_qty,
5051            Some(Quantity::from("0.0")),
5052        );
5053        assert_eq!(
5054            algo.core.spawn_fill_debt(client_order_id),
5055            Some(Quantity::from("0.3")),
5056        );
5057
5058        // The second spawn terminating unfilled discharges the debt before
5059        // returning quantity: 0.8 restores only 0.5
5060        let rejected = OrderRejectedSpec::builder()
5061            .trader_id(second_order.trader_id())
5062            .strategy_id(second_order.strategy_id())
5063            .instrument_id(second_order.instrument_id())
5064            .client_order_id(second_order.client_order_id())
5065            .account_id(AccountId::from("BINANCE-001"))
5066            .reason("TEST_REJECTION".into())
5067            .build();
5068        algo.handle_order_event(OrderEventAny::Rejected(rejected));
5069
5070        let final_primary = algo.cache().order(&client_order_id).unwrap();
5071        assert_eq!(final_primary.quantity(), Quantity::from("0.5"));
5072        assert!(algo.core.spawn_fill_debt(client_order_id).is_none());
5073        assert_eq!(
5074            spawned_order.filled_qty() + final_primary.quantity(),
5075            Quantity::from("1.0"),
5076        );
5077    }
5078
5079    #[rstest]
5080    fn test_late_fill_on_child_that_fully_discharged_debt_recreates_debt() {
5081        let (mut algo, client_order_id, mut spawned_order) = setup_accepted_spawn();
5082        cancel_spawned_order(&mut algo, &mut spawned_order);
5083
5084        let mut child_b = spawn_reduced_child(&mut algo, client_order_id, Quantity::from("0.3"));
5085        let mut child_c = spawn_reduced_child(&mut algo, client_order_id, Quantity::from("0.5"));
5086        accept_spawned_order(&mut algo, &mut child_b);
5087        accept_spawned_order(&mut algo, &mut child_c);
5088        assert_eq!(
5089            algo.cache().order(&client_order_id).unwrap().quantity(),
5090            Quantity::from("0.2"),
5091        );
5092
5093        fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("0.5"));
5094        assert_eq!(
5095            algo.cache().order(&client_order_id).unwrap().quantity(),
5096            Quantity::from("0.0"),
5097        );
5098        assert_eq!(
5099            algo.core.spawn_fill_debt(client_order_id),
5100            Some(Quantity::from("0.3")),
5101        );
5102
5103        // B's cancellation fully discharges the debt; its record keeps the
5104        // gross released amount for late-fill tracking
5105        cancel_spawned_order(&mut algo, &mut child_b);
5106        assert_eq!(
5107            algo.cache().order(&client_order_id).unwrap().quantity(),
5108            Quantity::from("0.0"),
5109        );
5110        assert!(algo.core.spawn_fill_debt(client_order_id).is_none());
5111        assert_eq!(
5112            algo.core
5113                .spawn_reduction(child_b.client_order_id())
5114                .unwrap()
5115                .restored_qty
5116                .unwrap(),
5117            Quantity::from("0.3"),
5118        );
5119
5120        // B's late fill reverses the settlement for the filled amount
5121        fill_spawned_order(&mut algo, &mut child_b, Quantity::from("0.2"));
5122        assert_eq!(
5123            algo.cache().order(&client_order_id).unwrap().quantity(),
5124            Quantity::from("0.0"),
5125        );
5126        assert_eq!(
5127            algo.core.spawn_fill_debt(client_order_id),
5128            Some(Quantity::from("0.2")),
5129        );
5130
5131        cancel_spawned_order(&mut algo, &mut child_c);
5132        let final_primary = algo.cache().order(&client_order_id).unwrap();
5133        assert_eq!(final_primary.quantity(), Quantity::from("0.3"));
5134        assert!(algo.core.spawn_fill_debt(client_order_id).is_none());
5135        assert_eq!(
5136            spawned_order.filled_qty() + child_b.filled_qty() + final_primary.quantity(),
5137            Quantity::from("1.0"),
5138        );
5139    }
5140
5141    #[rstest]
5142    fn test_late_fill_on_child_after_partial_debt_discharge_nets_from_primary() {
5143        let (mut algo, client_order_id, mut spawned_order) = setup_accepted_spawn();
5144        cancel_spawned_order(&mut algo, &mut spawned_order);
5145
5146        let mut child_b = spawn_reduced_child(&mut algo, client_order_id, Quantity::from("0.8"));
5147        accept_spawned_order(&mut algo, &mut child_b);
5148        fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("0.5"));
5149        assert_eq!(
5150            algo.core.spawn_fill_debt(client_order_id),
5151            Some(Quantity::from("0.3")),
5152        );
5153
5154        // B's cancellation discharges 0.3 of debt and restores the net 0.5;
5155        // its record keeps the gross 0.8
5156        cancel_spawned_order(&mut algo, &mut child_b);
5157        assert_eq!(
5158            algo.cache().order(&client_order_id).unwrap().quantity(),
5159            Quantity::from("0.5"),
5160        );
5161        assert!(algo.core.spawn_fill_debt(client_order_id).is_none());
5162        assert_eq!(
5163            algo.core
5164                .spawn_reduction(child_b.client_order_id())
5165                .unwrap()
5166                .restored_qty
5167                .unwrap(),
5168            Quantity::from("0.8"),
5169        );
5170
5171        fill_spawned_order(&mut algo, &mut child_b, Quantity::from("0.3"));
5172        let final_primary = algo.cache().order(&client_order_id).unwrap();
5173        assert_eq!(final_primary.quantity(), Quantity::from("0.2"));
5174        assert_eq!(
5175            spawned_order.filled_qty() + child_b.filled_qty() + final_primary.quantity(),
5176            Quantity::from("1.0"),
5177        );
5178    }
5179
5180    #[rstest]
5181    fn test_emulated_spawn_refusal_restores_initialized_primary() {
5182        let (mut algo, client_order_id, _spawned_order) = setup_pending_spawn();
5183        let mut primary = algo.cache().order(&client_order_id).unwrap();
5184        let spawned = algo.spawn_limit(
5185            &mut primary,
5186            Quantity::from("0.3"),
5187            Price::from("50000.0"),
5188            TimeInForce::Gtc,
5189            None,
5190            false,
5191            false,
5192            None,
5193            Some(TriggerType::BidAsk),
5194            None,
5195            true,
5196        );
5197        assert_eq!(
5198            algo.cache().order(&client_order_id).unwrap().quantity(),
5199            Quantity::from("0.2"),
5200        );
5201
5202        let result = algo.submit_order(OrderAny::Limit(spawned), None, None);
5203
5204        assert!(result.is_err());
5205        assert_eq!(
5206            algo.cache().order(&client_order_id).unwrap().quantity(),
5207            Quantity::from("0.5"),
5208        );
5209    }
5210
5211    #[rstest]
5212    fn test_spawned_order_accepted_then_expired_restores_reduction() {
5213        let (mut algo, client_order_id, mut spawned_order) = setup_accepted_spawn();
5214
5215        expire_spawned_order(&mut algo, &mut spawned_order);
5216
5217        let final_primary = algo.cache().order(&client_order_id).unwrap();
5218        assert_eq!(final_primary.quantity(), Quantity::from("1.0"));
5219    }
5220
5221    #[rstest]
5222    fn test_partially_filled_spawned_order_canceled_restores_leaves_quantity() {
5223        let (mut algo, client_order_id, mut spawned_order) = setup_accepted_spawn();
5224        fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("0.2"));
5225
5226        cancel_spawned_order(&mut algo, &mut spawned_order);
5227
5228        let final_primary = algo.cache().order(&client_order_id).unwrap();
5229        assert_eq!(final_primary.quantity(), Quantity::from("0.8"));
5230    }
5231
5232    #[rstest]
5233    fn test_partially_filled_spawned_order_expired_restores_leaves_quantity() {
5234        let (mut algo, client_order_id, mut spawned_order) = setup_accepted_spawn();
5235        fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("0.2"));
5236
5237        expire_spawned_order(&mut algo, &mut spawned_order);
5238
5239        let final_primary = algo.cache().order(&client_order_id).unwrap();
5240        assert_eq!(final_primary.quantity(), Quantity::from("0.8"));
5241    }
5242
5243    #[rstest]
5244    fn test_fully_filled_spawned_order_consumes_reduction_without_restoration() {
5245        let (mut algo, client_order_id, mut spawned_order) = setup_accepted_spawn();
5246        let spawned_id = spawned_order.client_order_id();
5247
5248        fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("0.5"));
5249
5250        let final_primary = algo.cache().order(&client_order_id).unwrap();
5251        assert_eq!(final_primary.quantity(), Quantity::from("0.5"));
5252        assert!(algo.core.take_pending_spawn_reduction(spawned_id).is_none());
5253    }
5254
5255    #[rstest]
5256    #[case::denied(true)]
5257    #[case::rejected(false)]
5258    fn test_spawned_order_refusal_then_terminal_event_restores_only_once(#[case] denied: bool) {
5259        // A canceled event after a denial/rejection is not an applicable state
5260        // transition and the engine drops such races before publication; the
5261        // second event is dispatched directly to exercise the handler's own
5262        // idempotence (the accounted unfilled quantity is unchanged).
5263        let (mut algo, client_order_id, spawned_order) = setup_pending_spawn();
5264
5265        if denied {
5266            let event = OrderDeniedSpec::builder()
5267                .trader_id(spawned_order.trader_id())
5268                .strategy_id(spawned_order.strategy_id())
5269                .instrument_id(spawned_order.instrument_id())
5270                .client_order_id(spawned_order.client_order_id())
5271                .reason("TEST_DENIAL".into())
5272                .build();
5273            algo.core
5274                .cache_rc()
5275                .borrow_mut()
5276                .update_order(&OrderEventAny::Denied(event))
5277                .unwrap();
5278            algo.handle_order_event(OrderEventAny::Denied(event));
5279        } else {
5280            let event = OrderRejectedSpec::builder()
5281                .trader_id(spawned_order.trader_id())
5282                .strategy_id(spawned_order.strategy_id())
5283                .instrument_id(spawned_order.instrument_id())
5284                .client_order_id(spawned_order.client_order_id())
5285                .account_id(AccountId::from("BINANCE-001"))
5286                .reason("TEST_REJECTION".into())
5287                .build();
5288            algo.core
5289                .cache_rc()
5290                .borrow_mut()
5291                .update_order(&OrderEventAny::Rejected(event))
5292                .unwrap();
5293            algo.handle_order_event(OrderEventAny::Rejected(event));
5294        }
5295
5296        assert_eq!(
5297            algo.cache().order(&client_order_id).unwrap().quantity(),
5298            Quantity::from("1.0"),
5299        );
5300
5301        let canceled = OrderCanceledSpec::builder()
5302            .trader_id(spawned_order.trader_id())
5303            .strategy_id(spawned_order.strategy_id())
5304            .instrument_id(spawned_order.instrument_id())
5305            .client_order_id(spawned_order.client_order_id())
5306            .build();
5307        algo.handle_order_event(OrderEventAny::Canceled(canceled));
5308
5309        let final_primary = algo.cache().order(&client_order_id).unwrap();
5310        assert_eq!(final_primary.quantity(), Quantity::from("1.0"));
5311    }
5312
5313    #[rstest]
5314    #[should_panic(expected = "exceeds primary leaves_qty")]
5315    fn test_spawn_quantity_exceeds_leaves_qty_panics() {
5316        let mut algo = create_test_algorithm();
5317        register_algorithm(&mut algo);
5318
5319        let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
5320        let exec_algorithm_id = algo.id();
5321        let client_order_id = ClientOrderId::from("O-001");
5322
5323        let mut primary = OrderAny::Market(MarketOrder::new(
5324            TraderId::from("TRADER-001"),
5325            StrategyId::from("STRAT-001"),
5326            instrument_id,
5327            client_order_id,
5328            OrderSide::Buy,
5329            Quantity::from("1.0"),
5330            TimeInForce::Gtc,
5331            UUID4::new(),
5332            0.into(),
5333            false,
5334            false,
5335            None,
5336            None,
5337            None,
5338            None,
5339            Some(exec_algorithm_id),
5340            None,
5341            Some(client_order_id),
5342            None,
5343        ));
5344
5345        {
5346            let cache_rc = algo.core.cache_rc();
5347            let mut cache = cache_rc.borrow_mut();
5348            cache.add_order(primary.clone(), None, None, false).unwrap();
5349        }
5350
5351        let _ = algo.spawn_market(
5352            &mut primary,
5353            Quantity::from("0.8"),
5354            TimeInForce::Fok,
5355            false,
5356            None,
5357            true,
5358        );
5359
5360        assert_eq!(primary.quantity(), Quantity::from("0.2"));
5361        assert_eq!(primary.leaves_qty(), Quantity::from("0.2"));
5362
5363        // Should panic - spawning 0.5 when only 0.2 leaves_qty remains
5364        let _ = algo.spawn_market(
5365            &mut primary,
5366            Quantity::from("0.5"),
5367            TimeInForce::Fok,
5368            false,
5369            None,
5370            true,
5371        );
5372    }
5373}