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 order modification fails.
1093    fn modify_order(
1094        &mut self,
1095        order: &mut OrderAny,
1096        quantity: Option<Quantity>,
1097        price: Option<Price>,
1098        trigger_price: Option<Price>,
1099        client_id: Option<ClientId>,
1100    ) -> anyhow::Result<()>
1101    where
1102        Self: ExecutionAlgorithmNative,
1103    {
1104        let qty_changing = quantity.is_some_and(|q| q != order.quantity());
1105        let price_changing = price.is_some() && price != order.price();
1106        let trigger_changing = trigger_price.is_some() && trigger_price != order.trigger_price();
1107
1108        if !qty_changing && !price_changing && !trigger_changing {
1109            log::error!(
1110                "Cannot create command ModifyOrder: \
1111                quantity, price, and trigger were either None \
1112                or the same as existing values"
1113            );
1114            return Ok(());
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::TestClock,
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(TestClock::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 mut params = nautilus_core::Params::new();
2464        params.insert(
2465            "route".to_string(),
2466            serde_json::Value::String("A".to_string()),
2467        );
2468        algo.core
2469            .remember_submit_params(order.client_order_id(), Some(params));
2470
2471        let error = algo
2472            .deny_order(
2473                &order,
2474                Ustr::from("VALIDATION_FAILED: invalid execution schedule"),
2475            )
2476            .unwrap_err();
2477
2478        msgbus::unsubscribe_order_events(format!("events.order.{strategy_id}").into(), &handler);
2479        let cached_order = algo.cache().order(&order.client_order_id()).unwrap();
2480
2481        assert!(matches!(
2482            error.downcast_ref::<OrderError>(),
2483            Some(OrderError::InvalidStateTransition)
2484        ));
2485        assert_eq!(cached_order.status(), OrderStatus::Accepted);
2486        assert!(events.borrow().is_empty());
2487        // A failed denial is not terminal, so the submit params must be retained
2488        assert!(algo.core.submit_params(&order.client_order_id()).is_some());
2489    }
2490
2491    #[rstest]
2492    fn test_algorithm_deny_order_removes_submit_params() {
2493        let mut algo = create_test_algorithm();
2494        register_algorithm(&mut algo);
2495
2496        let strategy_id = StrategyId::from("STRAT-ALGO-DENY-PARAMS");
2497        let order = OrderTestBuilder::new(OrderType::Market)
2498            .strategy_id(strategy_id)
2499            .instrument_id(InstrumentId::from("BTC/USDT.BINANCE"))
2500            .client_order_id(ClientOrderId::from("O-ALGO-DENY-PARAMS"))
2501            .quantity(Quantity::from("1.0"))
2502            .build();
2503        {
2504            let cache_rc = algo.core.cache_rc();
2505            cache_rc
2506                .borrow_mut()
2507                .add_order(order.clone(), None, None, false)
2508                .unwrap();
2509        }
2510
2511        let mut params = nautilus_core::Params::new();
2512        params.insert(
2513            "route".to_string(),
2514            serde_json::Value::String("A".to_string()),
2515        );
2516        algo.core
2517            .remember_submit_params(order.client_order_id(), Some(params));
2518        assert!(algo.core.submit_params(&order.client_order_id()).is_some());
2519
2520        algo.deny_order(&order, Ustr::from("VALIDATION_FAILED: test"))
2521            .unwrap();
2522
2523        assert!(algo.core.submit_params(&order.client_order_id()).is_none());
2524    }
2525
2526    #[rstest]
2527    fn test_submit_order_errors_when_algorithm_not_registered() {
2528        let mut algo = create_test_algorithm();
2529        let order = OrderAny::Market(MarketOrder::new(
2530            TraderId::from("TRADER-001"),
2531            StrategyId::from("STRAT-001"),
2532            InstrumentId::from("BTC/USDT.BINANCE"),
2533            ClientOrderId::from("O-UNREGISTERED-001"),
2534            OrderSide::Buy,
2535            Quantity::from("1.0"),
2536            TimeInForce::Gtc,
2537            UUID4::new(),
2538            0.into(),
2539            false,
2540            false,
2541            None,
2542            None,
2543            None,
2544            None,
2545            None,
2546            None,
2547            None,
2548            None,
2549        ));
2550
2551        let err = algo
2552            .submit_order(order, None, None)
2553            .unwrap_err()
2554            .to_string();
2555
2556        assert_eq!(
2557            err,
2558            "ExecutionAlgorithm not registered: trader_id is not set"
2559        );
2560    }
2561
2562    #[rstest]
2563    fn test_required_account_id_errors_when_missing_for_algorithm_event() {
2564        let order = OrderAny::Market(MarketOrder::new(
2565            TraderId::from("TRADER-001"),
2566            StrategyId::from("STRAT-001"),
2567            InstrumentId::from("BTC/USDT.BINANCE"),
2568            ClientOrderId::from("O-NO-ACCOUNT-001"),
2569            OrderSide::Buy,
2570            Quantity::from("1.0"),
2571            TimeInForce::Gtc,
2572            UUID4::new(),
2573            0.into(),
2574            false,
2575            false,
2576            None,
2577            None,
2578            None,
2579            None,
2580            None,
2581            None,
2582            None,
2583            None,
2584        ));
2585
2586        let err = required_account_id(&order, "pending update")
2587            .unwrap_err()
2588            .to_string();
2589
2590        assert_eq!(
2591            err,
2592            "Cannot generate pending update event for O-NO-ACCOUNT-001: account_id is not set"
2593        );
2594    }
2595
2596    #[rstest]
2597    fn test_algorithm_id() {
2598        let algo = create_test_algorithm();
2599        assert!(algo.id().inner().starts_with("TEST-"));
2600    }
2601
2602    #[rstest]
2603    fn test_execution_algorithm_behavior_does_not_require_native_core_access() {
2604        fn assert_execution_algorithm<T: ExecutionAlgorithm + DataActor>() {}
2605
2606        assert_execution_algorithm::<CoreFreeExecutionAlgorithm>();
2607
2608        let mut algorithm = CoreFreeExecutionAlgorithm { orders_seen: 0 };
2609        let order = OrderTestBuilder::new(OrderType::Market)
2610            .instrument_id(InstrumentId::from("BTC/USDT.BINANCE"))
2611            .quantity(Quantity::from("1.0"))
2612            .build();
2613
2614        algorithm.on_order(order).unwrap();
2615
2616        assert_eq!(algorithm.orders_seen, 1);
2617    }
2618
2619    #[rstest]
2620    fn test_nautilus_execution_algorithm_macro_custom_field() {
2621        let exec_algorithm_id = ExecAlgorithmId::from("MACRO-001");
2622        let algorithm = MacroTestCustomField {
2623            inner: ExecutionAlgorithmCore::new(ExecutionAlgorithmConfig {
2624                exec_algorithm_id: Some(exec_algorithm_id),
2625                ..Default::default()
2626            }),
2627        };
2628
2629        assert_eq!(algorithm.id(), exec_algorithm_id);
2630        assert_eq!(algorithm.actor_id(), ActorId::from("MACRO-001"));
2631    }
2632
2633    #[rstest]
2634    fn test_algorithm_spawn_market_creates_valid_order() {
2635        let mut algo = create_test_algorithm();
2636        register_algorithm(&mut algo);
2637
2638        let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
2639        let mut primary = OrderAny::Market(MarketOrder::new(
2640            TraderId::from("TRADER-001"),
2641            StrategyId::from("STRAT-001"),
2642            instrument_id,
2643            ClientOrderId::from("O-001"),
2644            OrderSide::Buy,
2645            Quantity::from("1.0"),
2646            TimeInForce::Gtc,
2647            UUID4::new(),
2648            0.into(),
2649            false, // reduce_only
2650            false, // quote_quantity
2651            None,  // contingency_type
2652            None,  // order_list_id
2653            None,  // linked_order_ids
2654            None,  // parent_order_id
2655            None,  // exec_algorithm_id
2656            None,  // exec_algorithm_params
2657            None,  // exec_spawn_id
2658            None,  // tags
2659        ));
2660
2661        let spawned = algo.spawn_market(
2662            &mut primary,
2663            Quantity::from("0.5"),
2664            TimeInForce::Ioc,
2665            false,
2666            None,  // tags
2667            false, // reduce_primary
2668        );
2669
2670        assert_eq!(spawned.client_order_id.as_str(), "O-001-E1");
2671        assert_eq!(spawned.instrument_id, instrument_id);
2672        assert_eq!(spawned.order_side(), OrderSide::Buy);
2673        assert_eq!(spawned.quantity, Quantity::from("0.5"));
2674        assert_eq!(spawned.time_in_force, TimeInForce::Ioc);
2675        assert_eq!(spawned.exec_algorithm_id, Some(algo.id()));
2676        assert_eq!(spawned.exec_spawn_id, Some(ClientOrderId::from("O-001")));
2677    }
2678
2679    #[rstest]
2680    fn test_algorithm_spawn_increments_sequence() {
2681        let mut algo = create_test_algorithm();
2682        register_algorithm(&mut algo);
2683
2684        let mut primary = OrderAny::Market(MarketOrder::new(
2685            TraderId::from("TRADER-001"),
2686            StrategyId::from("STRAT-001"),
2687            InstrumentId::from("BTC/USDT.BINANCE"),
2688            ClientOrderId::from("O-001"),
2689            OrderSide::Buy,
2690            Quantity::from("1.0"),
2691            TimeInForce::Gtc,
2692            UUID4::new(),
2693            0.into(),
2694            false,
2695            false,
2696            None,
2697            None,
2698            None,
2699            None,
2700            None,
2701            None,
2702            None,
2703            None,
2704        ));
2705
2706        let spawned1 = algo.spawn_market(
2707            &mut primary,
2708            Quantity::from("0.25"),
2709            TimeInForce::Ioc,
2710            false,
2711            None,
2712            false,
2713        );
2714        let spawned2 = algo.spawn_market(
2715            &mut primary,
2716            Quantity::from("0.25"),
2717            TimeInForce::Ioc,
2718            false,
2719            None,
2720            false,
2721        );
2722        let spawned3 = algo.spawn_market(
2723            &mut primary,
2724            Quantity::from("0.25"),
2725            TimeInForce::Ioc,
2726            false,
2727            None,
2728            false,
2729        );
2730
2731        assert_eq!(spawned1.client_order_id.as_str(), "O-001-E1");
2732        assert_eq!(spawned2.client_order_id.as_str(), "O-001-E2");
2733        assert_eq!(spawned3.client_order_id.as_str(), "O-001-E3");
2734    }
2735
2736    #[rstest]
2737    fn test_algorithm_default_handlers_do_not_panic() {
2738        let mut algo = create_test_algorithm();
2739
2740        algo.on_order_initialized(OrderInitialized::default());
2741        algo.on_order_denied(OrderDenied::default());
2742        algo.on_order_emulated(OrderEmulated::default());
2743        algo.on_order_released(OrderReleased::default());
2744        algo.on_order_submitted(OrderSubmitted::default());
2745        algo.on_order_rejected(OrderRejected::default());
2746        algo.on_order_accepted(OrderAccepted::default());
2747        algo.on_algo_order_canceled(OrderCanceled::default());
2748        algo.on_order_expired(OrderExpired::default());
2749        algo.on_order_triggered(OrderTriggered::default());
2750        algo.on_order_pending_update(OrderPendingUpdate::default());
2751        algo.on_order_pending_cancel(OrderPendingCancel::default());
2752        algo.on_order_modify_rejected(OrderModifyRejected::default());
2753        algo.on_order_cancel_rejected(OrderCancelRejected::default());
2754        algo.on_order_updated(OrderUpdated::default());
2755        algo.on_algo_order_filled(OrderFilledSpec::builder().build());
2756        algo.on_order_fill_voided(&OrderFillVoidedSpec::builder().build());
2757    }
2758
2759    #[rstest]
2760    fn test_strategy_subscription_tracking() {
2761        let mut algo = create_test_algorithm();
2762        let strategy_id = StrategyId::from("STRAT-001");
2763
2764        assert!(!algo.core.is_strategy_subscribed(&strategy_id));
2765
2766        algo.subscribe_to_strategy_events(strategy_id);
2767        assert!(algo.core.is_strategy_subscribed(&strategy_id));
2768
2769        // Second call should be idempotent
2770        algo.subscribe_to_strategy_events(strategy_id);
2771        assert!(algo.core.is_strategy_subscribed(&strategy_id));
2772    }
2773
2774    #[rstest]
2775    fn test_algorithm_reset() {
2776        let mut algo = create_test_algorithm();
2777        let strategy_id = StrategyId::from("STRAT-001");
2778        let primary_id = ClientOrderId::new("O-001");
2779
2780        let _ = algo.core.spawn_client_order_id(&primary_id);
2781        algo.core.add_subscribed_strategy(strategy_id);
2782
2783        assert!(algo.core.spawn_sequence(&primary_id).is_some());
2784        assert!(algo.core.is_strategy_subscribed(&strategy_id));
2785
2786        ExecutionAlgorithm::on_reset(&mut algo).unwrap();
2787
2788        assert!(algo.core.spawn_sequence(&primary_id).is_none());
2789        assert!(!algo.core.is_strategy_subscribed(&strategy_id));
2790    }
2791
2792    #[rstest]
2793    fn test_algorithm_spawn_limit_creates_valid_order() {
2794        let mut algo = create_test_algorithm();
2795        register_algorithm(&mut algo);
2796
2797        let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
2798        let mut primary = OrderAny::Market(MarketOrder::new(
2799            TraderId::from("TRADER-001"),
2800            StrategyId::from("STRAT-001"),
2801            instrument_id,
2802            ClientOrderId::from("O-001"),
2803            OrderSide::Buy,
2804            Quantity::from("1.0"),
2805            TimeInForce::Gtc,
2806            UUID4::new(),
2807            0.into(),
2808            false,
2809            false,
2810            None,
2811            None,
2812            None,
2813            None,
2814            None,
2815            None,
2816            None,
2817            None,
2818        ));
2819
2820        let price = Price::from("50000.0");
2821        let spawned = algo.spawn_limit(
2822            &mut primary,
2823            Quantity::from("0.5"),
2824            price,
2825            TimeInForce::Gtc,
2826            None,  // expire_time
2827            false, // post_only
2828            false, // reduce_only
2829            None,  // display_qty
2830            None,  // emulation_trigger
2831            None,  // tags
2832            false, // reduce_primary
2833        );
2834
2835        assert_eq!(spawned.client_order_id.as_str(), "O-001-E1");
2836        assert_eq!(spawned.instrument_id, instrument_id);
2837        assert_eq!(spawned.order_side(), OrderSide::Buy);
2838        assert_eq!(spawned.quantity, Quantity::from("0.5"));
2839        assert_eq!(spawned.price, price);
2840        assert_eq!(spawned.time_in_force, TimeInForce::Gtc);
2841        assert_eq!(spawned.exec_algorithm_id, Some(algo.id()));
2842        assert_eq!(spawned.exec_spawn_id, Some(ClientOrderId::from("O-001")));
2843    }
2844
2845    #[rstest]
2846    fn test_algorithm_spawn_market_to_limit_creates_valid_order() {
2847        let mut algo = create_test_algorithm();
2848        register_algorithm(&mut algo);
2849
2850        let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
2851        let mut primary = OrderAny::Market(MarketOrder::new(
2852            TraderId::from("TRADER-001"),
2853            StrategyId::from("STRAT-001"),
2854            instrument_id,
2855            ClientOrderId::from("O-001"),
2856            OrderSide::Buy,
2857            Quantity::from("1.0"),
2858            TimeInForce::Gtc,
2859            UUID4::new(),
2860            0.into(),
2861            false,
2862            false,
2863            None,
2864            None,
2865            None,
2866            None,
2867            None,
2868            None,
2869            None,
2870            None,
2871        ));
2872
2873        let spawned = algo.spawn_market_to_limit(
2874            &mut primary,
2875            Quantity::from("0.5"),
2876            TimeInForce::Gtc,
2877            None,  // expire_time
2878            false, // reduce_only
2879            None,  // display_qty
2880            None,  // emulation_trigger
2881            None,  // tags
2882            false, // reduce_primary
2883        );
2884
2885        assert_eq!(spawned.client_order_id.as_str(), "O-001-E1");
2886        assert_eq!(spawned.instrument_id, instrument_id);
2887        assert_eq!(spawned.order_side(), OrderSide::Buy);
2888        assert_eq!(spawned.quantity, Quantity::from("0.5"));
2889        assert_eq!(spawned.time_in_force, TimeInForce::Gtc);
2890        assert_eq!(spawned.exec_algorithm_id, Some(algo.id()));
2891        assert_eq!(spawned.exec_spawn_id, Some(ClientOrderId::from("O-001")));
2892    }
2893
2894    #[rstest]
2895    fn test_algorithm_spawn_market_with_tags() {
2896        let mut algo = create_test_algorithm();
2897        register_algorithm(&mut algo);
2898
2899        let mut primary = OrderAny::Market(MarketOrder::new(
2900            TraderId::from("TRADER-001"),
2901            StrategyId::from("STRAT-001"),
2902            InstrumentId::from("BTC/USDT.BINANCE"),
2903            ClientOrderId::from("O-001"),
2904            OrderSide::Buy,
2905            Quantity::from("1.0"),
2906            TimeInForce::Gtc,
2907            UUID4::new(),
2908            0.into(),
2909            false,
2910            false,
2911            None,
2912            None,
2913            None,
2914            None,
2915            None,
2916            None,
2917            None,
2918            None,
2919        ));
2920
2921        let tags = vec![ustr::Ustr::from("TAG1"), ustr::Ustr::from("TAG2")];
2922        let spawned = algo.spawn_market(
2923            &mut primary,
2924            Quantity::from("0.5"),
2925            TimeInForce::Ioc,
2926            false,
2927            Some(tags.clone()),
2928            false,
2929        );
2930
2931        assert_eq!(spawned.tags, Some(tags));
2932    }
2933
2934    #[rstest]
2935    fn test_algorithm_spawn_propagates_primary_fields() {
2936        let mut algo = create_test_algorithm();
2937        register_algorithm(&mut algo);
2938
2939        let mut params = indexmap::IndexMap::new();
2940        params.insert(ustr::Ustr::from("horizon_secs"), ustr::Ustr::from("30"));
2941        params.insert(ustr::Ustr::from("interval_secs"), ustr::Ustr::from("10"));
2942        let primary_tags = vec![ustr::Ustr::from("PRIMARY_TAG")];
2943        let linked_order_ids = vec![ClientOrderId::from("LINK-1")];
2944        let client_order_id = ClientOrderId::from("O-001");
2945
2946        let mut primary = OrderAny::Market(MarketOrder::new(
2947            TraderId::from("TRADER-001"),
2948            StrategyId::from("STRAT-001"),
2949            InstrumentId::from("BTC/USDT.BINANCE"),
2950            client_order_id,
2951            OrderSide::Buy,
2952            Quantity::from("1.0"),
2953            TimeInForce::Gtc,
2954            UUID4::new(),
2955            0.into(),
2956            false, // reduce_only
2957            true,  // quote_quantity
2958            None,  // contingency_type
2959            None,  // order_list_id
2960            Some(linked_order_ids.clone()),
2961            None, // parent_order_id
2962            Some(algo.id()),
2963            Some(params.clone()),
2964            Some(client_order_id),
2965            Some(primary_tags.clone()),
2966        ));
2967
2968        let spawned_market = algo.spawn_market(
2969            &mut primary,
2970            Quantity::from("0.25"),
2971            TimeInForce::Ioc,
2972            false,
2973            None, // falls back to primary.tags
2974            false,
2975        );
2976        assert!(spawned_market.is_quote_quantity);
2977        assert_eq!(spawned_market.exec_algorithm_params, Some(params.clone()));
2978        assert_eq!(spawned_market.tags, Some(primary_tags.clone()));
2979        assert_eq!(
2980            spawned_market.linked_order_ids,
2981            Some(linked_order_ids.clone())
2982        );
2983
2984        let spawned_limit = algo.spawn_limit(
2985            &mut primary,
2986            Quantity::from("0.25"),
2987            Price::from("50000.0"),
2988            TimeInForce::Gtc,
2989            None,  // expire_time
2990            false, // post_only
2991            false, // reduce_only
2992            None,  // display_qty
2993            None,  // emulation_trigger
2994            None,  // falls back to primary.tags
2995            false,
2996        );
2997        assert!(spawned_limit.is_quote_quantity);
2998        assert_eq!(spawned_limit.exec_algorithm_params, Some(params.clone()));
2999        assert_eq!(spawned_limit.tags, Some(primary_tags.clone()));
3000        assert_eq!(
3001            spawned_limit.linked_order_ids,
3002            Some(linked_order_ids.clone())
3003        );
3004
3005        let spawned_mtl = algo.spawn_market_to_limit(
3006            &mut primary,
3007            Quantity::from("0.25"),
3008            TimeInForce::Gtc,
3009            None,  // expire_time
3010            false, // reduce_only
3011            None,  // display_qty
3012            None,  // emulation_trigger
3013            None,  // falls back to primary.tags
3014            false,
3015        );
3016        assert!(spawned_mtl.is_quote_quantity);
3017        assert_eq!(spawned_mtl.exec_algorithm_params, Some(params));
3018        assert_eq!(spawned_mtl.tags, Some(primary_tags));
3019        assert_eq!(spawned_mtl.linked_order_ids, Some(linked_order_ids));
3020    }
3021
3022    #[rstest]
3023    fn test_muldiv_floor_u128_narrow_path_truncates() {
3024        assert_eq!(muldiv_floor_u128(50, 3, 5), 30);
3025        assert_eq!(muldiv_floor_u128(10, 2, 3), 6);
3026        assert_eq!(muldiv_floor_u128(u128::MAX, 7, 7), u128::MAX);
3027    }
3028
3029    #[rstest]
3030    fn test_muldiv_floor_u128_wide_path_floors_exactly() {
3031        // a * b overflows u128; the exact quotient is a - a/c, fractionally
3032        // under a, so an implementation that rounds instead of flooring
3033        // returns a. This is the shape of the Decimal-fallback defect.
3034        let a = 10u128.pow(30);
3035        let b = 7 * 10u128.pow(30);
3036        let c = b + 1;
3037        assert!(a.checked_mul(b).is_none());
3038        assert_eq!(muldiv_floor_u128(a, b, c), a - 1);
3039    }
3040
3041    #[rstest]
3042    fn test_algorithm_reduce_primary_order() {
3043        let mut algo = create_test_algorithm();
3044        register_algorithm(&mut algo);
3045
3046        let order = OrderAny::Market(MarketOrder::new(
3047            TraderId::from("TRADER-001"),
3048            StrategyId::from("STRAT-001"),
3049            InstrumentId::from("BTC/USDT.BINANCE"),
3050            ClientOrderId::from("O-001"),
3051            OrderSide::Buy,
3052            Quantity::from("1.0"),
3053            TimeInForce::Gtc,
3054            UUID4::new(),
3055            0.into(),
3056            false,
3057            false,
3058            None,
3059            None,
3060            None,
3061            None,
3062            None,
3063            None,
3064            None,
3065            None,
3066        ));
3067
3068        // Make accepted so OrderUpdated can be applied
3069        let mut primary = TestOrderStubs::make_accepted_order(&order);
3070
3071        {
3072            let cache_rc = algo.core.cache_rc();
3073            let mut cache = cache_rc.borrow_mut();
3074            cache.add_order(primary.clone(), None, None, false).unwrap();
3075        }
3076
3077        let spawn_qty = Quantity::from("0.3");
3078        algo.reduce_primary_order(&mut primary, spawn_qty);
3079
3080        assert_eq!(primary.quantity(), Quantity::from("0.7"));
3081    }
3082
3083    #[rstest]
3084    fn test_algorithm_reduce_primary_order_publishes_updated_event() {
3085        let mut algo = create_test_algorithm();
3086        register_algorithm(&mut algo);
3087
3088        let strategy_id = StrategyId::from("STRAT-ALGO-REDUCE-PUBLISH");
3089        let order = OrderAny::Market(MarketOrder::new(
3090            TraderId::from("TRADER-001"),
3091            strategy_id,
3092            InstrumentId::from("BTC/USDT.BINANCE"),
3093            ClientOrderId::from("O-ALGO-REDUCE"),
3094            OrderSide::Buy,
3095            Quantity::from("1.0"),
3096            TimeInForce::Gtc,
3097            UUID4::new(),
3098            0.into(),
3099            false,
3100            false,
3101            None,
3102            None,
3103            None,
3104            None,
3105            None,
3106            None,
3107            None,
3108            None,
3109        ));
3110        let mut primary = TestOrderStubs::make_accepted_order(&order);
3111
3112        {
3113            let cache_rc = algo.core.cache_rc();
3114            let mut cache = cache_rc.borrow_mut();
3115            cache.add_order(primary.clone(), None, None, false).unwrap();
3116        }
3117
3118        let (handler, events) = subscribe_order_topic(strategy_id);
3119
3120        algo.reduce_primary_order(&mut primary, Quantity::from("0.3"));
3121
3122        msgbus::unsubscribe_order_events(format!("events.order.{strategy_id}").into(), &handler);
3123        let events = events.borrow();
3124
3125        assert_eq!(events.len(), 1);
3126        assert!(matches!(
3127            &events[0],
3128            OrderEventAny::Updated(event) if event.quantity == Quantity::from("0.7")
3129        ));
3130    }
3131
3132    #[rstest]
3133    fn test_algorithm_submit_order_publishes_initialized_for_new_order() {
3134        let mut algo = create_test_algorithm();
3135        register_algorithm(&mut algo);
3136
3137        let strategy_id = StrategyId::from("STRAT-ALGO-INIT-PUBLISH");
3138        let order = OrderAny::Market(MarketOrder::new(
3139            TraderId::from("TRADER-001"),
3140            strategy_id,
3141            InstrumentId::from("BTC/USDT.BINANCE"),
3142            ClientOrderId::from("O-ALGO-INIT"),
3143            OrderSide::Buy,
3144            Quantity::from("1.0"),
3145            TimeInForce::Gtc,
3146            UUID4::new(),
3147            0.into(),
3148            false,
3149            false,
3150            None,
3151            None,
3152            None,
3153            None,
3154            None,
3155            None,
3156            None,
3157            None,
3158        ));
3159        let (handler, events) = subscribe_order_topic(strategy_id);
3160
3161        algo.submit_order(order.clone(), None, None).unwrap();
3162
3163        msgbus::unsubscribe_order_events(format!("events.order.{strategy_id}").into(), &handler);
3164        let events = events.borrow();
3165
3166        assert_eq!(events.len(), 1);
3167        assert!(matches!(
3168            &events[0],
3169            OrderEventAny::Initialized(event) if event.client_order_id == order.client_order_id()
3170        ));
3171    }
3172
3173    #[rstest]
3174    fn test_algorithm_submit_order_does_not_republish_initialized_for_existing_order() {
3175        let mut algo = create_test_algorithm();
3176        register_algorithm(&mut algo);
3177
3178        let strategy_id = StrategyId::from("STRAT-ALGO-INIT-EXISTING");
3179        let order = OrderAny::Market(MarketOrder::new(
3180            TraderId::from("TRADER-001"),
3181            strategy_id,
3182            InstrumentId::from("BTC/USDT.BINANCE"),
3183            ClientOrderId::from("O-ALGO-INIT-EXISTING"),
3184            OrderSide::Buy,
3185            Quantity::from("1.0"),
3186            TimeInForce::Gtc,
3187            UUID4::new(),
3188            0.into(),
3189            false,
3190            false,
3191            None,
3192            None,
3193            None,
3194            None,
3195            None,
3196            None,
3197            None,
3198            None,
3199        ));
3200        {
3201            let cache_rc = algo.core.cache_rc();
3202            let mut cache = cache_rc.borrow_mut();
3203            cache.add_order(order.clone(), None, None, true).unwrap();
3204        }
3205        let (handler, events) = subscribe_order_topic(strategy_id);
3206
3207        algo.submit_order(order, None, None).unwrap();
3208
3209        msgbus::unsubscribe_order_events(format!("events.order.{strategy_id}").into(), &handler);
3210        assert!(events.borrow().is_empty());
3211    }
3212
3213    #[rstest]
3214    fn test_algorithm_submit_order_refuses_emulated_limit_spawn() {
3215        let mut algo = create_test_algorithm();
3216        register_algorithm(&mut algo);
3217
3218        let strategy_id = StrategyId::from("STRAT-ALGO-EMULATED-LIMIT");
3219        let order = OrderTestBuilder::new(OrderType::Market)
3220            .strategy_id(strategy_id)
3221            .instrument_id(InstrumentId::from("BTC/USDT.BINANCE"))
3222            .client_order_id(ClientOrderId::from("O-ALGO-EMULATED-LIMIT"))
3223            .quantity(Quantity::from("1.0"))
3224            .build();
3225        let mut primary = TestOrderStubs::make_accepted_order(&order);
3226        {
3227            let cache_rc = algo.core.cache_rc();
3228            let mut cache = cache_rc.borrow_mut();
3229            cache.add_order(primary.clone(), None, None, false).unwrap();
3230        }
3231        let (event_handler, events) = subscribe_order_topic(strategy_id);
3232        let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
3233            get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
3234        msgbus::register_trading_command_endpoint(
3235            MessagingSwitchboard::risk_engine_queue_execute(),
3236            risk_handler,
3237        );
3238        let (emulator_handler, emulator_messages): (
3239            _,
3240            TypedIntoMessageSavingHandler<TradingCommand>,
3241        ) = get_typed_into_message_saving_handler(Some(Ustr::from("OrderEmulator.execute")));
3242        msgbus::register_trading_command_endpoint(
3243            MessagingSwitchboard::order_emulator_execute(),
3244            emulator_handler,
3245        );
3246
3247        let spawned = algo.spawn_limit(
3248            &mut primary,
3249            Quantity::from("0.4"),
3250            Price::from("50000.0"),
3251            TimeInForce::Gtc,
3252            None,
3253            false,
3254            false,
3255            None,
3256            Some(TriggerType::BidAsk),
3257            None,
3258            true,
3259        );
3260        let spawned = OrderAny::Limit(spawned);
3261        let client_order_id = spawned.client_order_id();
3262        let result = algo.submit_order(spawned, None, None);
3263
3264        msgbus::unsubscribe_order_events(
3265            format!("events.order.{strategy_id}").into(),
3266            &event_handler,
3267        );
3268        let cache = algo.core.cache_ref();
3269        let error = result.unwrap_err();
3270        assert!(
3271            error
3272                .downcast_ref::<EmulatedOrderSubmissionError>()
3273                .is_some()
3274        );
3275        let error = error.to_string();
3276        assert!(error.contains("live emulation trigger"), "{error}");
3277        assert!(error.contains(client_order_id.as_str()), "{error}");
3278        assert!(risk_messages.get_messages().is_empty());
3279        assert!(emulator_messages.get_messages().is_empty());
3280        assert!(!cache.order_exists(&client_order_id));
3281        assert!(!events.borrow().iter().any(|event| matches!(
3282            event,
3283            OrderEventAny::Initialized(initialized)
3284                if initialized.client_order_id == client_order_id
3285        )));
3286        // The spawn already reduced the accepted primary locally (the venue
3287        // still works 1.0); restoration declines to mutate a non-local
3288        // primary, so the local deduction stands.
3289        assert_eq!(
3290            cache.order(&primary.client_order_id()).unwrap().quantity(),
3291            Quantity::from("0.6"),
3292        );
3293        drop(cache);
3294        assert!(
3295            algo.core
3296                .take_pending_spawn_reduction(client_order_id)
3297                .is_none()
3298        );
3299    }
3300
3301    #[rstest]
3302    fn test_algorithm_submit_order_routes_unemulated_spawn_to_risk() {
3303        let mut algo = create_test_algorithm();
3304        register_algorithm(&mut algo);
3305
3306        let mut primary = OrderTestBuilder::new(OrderType::Market)
3307            .instrument_id(InstrumentId::from("BTC/USDT.BINANCE"))
3308            .client_order_id(ClientOrderId::from("O-ALGO-UNEMULATED"))
3309            .quantity(Quantity::from("1.0"))
3310            .build();
3311        let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
3312            get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
3313        msgbus::register_trading_command_endpoint(
3314            MessagingSwitchboard::risk_engine_queue_execute(),
3315            risk_handler,
3316        );
3317
3318        let spawned = algo.spawn_limit(
3319            &mut primary,
3320            Quantity::from("0.4"),
3321            Price::from("50000.0"),
3322            TimeInForce::Gtc,
3323            None,
3324            false,
3325            false,
3326            None,
3327            None,
3328            None,
3329            false,
3330        );
3331        let client_order_id = spawned.client_order_id;
3332        algo.submit_order(OrderAny::Limit(spawned), None, None)
3333            .unwrap();
3334
3335        let risk_messages = risk_messages.get_messages();
3336        assert_eq!(risk_messages.len(), 1);
3337        assert!(matches!(
3338            risk_messages.first(),
3339            Some(TradingCommand::SubmitOrder(command))
3340                if command.client_order_id == client_order_id
3341        ));
3342    }
3343
3344    #[rstest]
3345    fn test_algorithm_spawn_market_with_reduce_primary() {
3346        let mut algo = create_test_algorithm();
3347        register_algorithm(&mut algo);
3348
3349        let order = OrderAny::Market(MarketOrder::new(
3350            TraderId::from("TRADER-001"),
3351            StrategyId::from("STRAT-001"),
3352            InstrumentId::from("BTC/USDT.BINANCE"),
3353            ClientOrderId::from("O-001"),
3354            OrderSide::Buy,
3355            Quantity::from("1.0"),
3356            TimeInForce::Gtc,
3357            UUID4::new(),
3358            0.into(),
3359            false,
3360            false,
3361            None,
3362            None,
3363            None,
3364            None,
3365            None,
3366            None,
3367            None,
3368            None,
3369        ));
3370
3371        // Make accepted so OrderUpdated can be applied
3372        let mut primary = TestOrderStubs::make_accepted_order(&order);
3373
3374        {
3375            let cache_rc = algo.core.cache_rc();
3376            let mut cache = cache_rc.borrow_mut();
3377            cache.add_order(primary.clone(), None, None, false).unwrap();
3378        }
3379
3380        let spawned = algo.spawn_market(
3381            &mut primary,
3382            Quantity::from("0.4"),
3383            TimeInForce::Ioc,
3384            false,
3385            None,
3386            true, // reduce_primary = true
3387        );
3388
3389        assert_eq!(spawned.quantity, Quantity::from("0.4"));
3390        assert_eq!(primary.quantity(), Quantity::from("0.6"));
3391    }
3392    #[rstest]
3393    fn test_algorithm_forwards_captured_params_to_spawned_order() {
3394        let mut algo = create_test_algorithm();
3395        register_algorithm(&mut algo);
3396
3397        let strategy_id = StrategyId::from("STRAT-FWD-001");
3398        let mut primary = OrderAny::Market(MarketOrder::new(
3399            TraderId::from("TRADER-001"),
3400            strategy_id,
3401            InstrumentId::from("BTC/USDT.BINANCE"),
3402            ClientOrderId::from("O-FWD-001"),
3403            OrderSide::Buy,
3404            Quantity::from("1.0"),
3405            TimeInForce::Gtc,
3406            UUID4::new(),
3407            0.into(),
3408            false,
3409            false,
3410            None,
3411            None,
3412            None,
3413            None,
3414            None,
3415            None,
3416            None,
3417            None,
3418        ));
3419        {
3420            let cache_rc = algo.core.cache_rc();
3421            let mut cache = cache_rc.borrow_mut();
3422            cache.add_order(primary.clone(), None, None, true).unwrap();
3423        }
3424
3425        let mut params = nautilus_core::Params::new();
3426        params.insert("is_leverage".to_string(), serde_json::Value::Bool(true));
3427        let command = SubmitOrder::new(
3428            TraderId::from("TRADER-001"),
3429            None,
3430            strategy_id,
3431            primary.instrument_id(),
3432            primary.client_order_id(),
3433            primary.init_event().clone(),
3434            primary.exec_algorithm_id(),
3435            None,
3436            Some(params),
3437            UUID4::new(),
3438            0.into(),
3439            None,
3440        );
3441        algo.execute(TradingCommand::SubmitOrder(command)).unwrap();
3442
3443        let received = Rc::new(RefCell::new(None::<SubmitOrder>));
3444        let handler = msgbus::TypedIntoHandler::from({
3445            let captured = received.clone();
3446            move |cmd: TradingCommand| {
3447                if let TradingCommand::SubmitOrder(cmd) = cmd {
3448                    *captured.borrow_mut() = Some(cmd);
3449                }
3450            }
3451        });
3452        msgbus::register_trading_command_endpoint(
3453            MessagingSwitchboard::risk_engine_queue_execute(),
3454            handler,
3455        );
3456
3457        let spawned = algo.spawn_market(
3458            &mut primary,
3459            Quantity::from("0.4"),
3460            TimeInForce::Ioc,
3461            false,
3462            None,
3463            false, // reduce_primary
3464        );
3465        algo.submit_order(OrderAny::Market(spawned), None, None)
3466            .unwrap();
3467
3468        let captured = received.borrow();
3469        let cmd = captured.as_ref().expect("expected a forwarded SubmitOrder");
3470        assert_eq!(cmd.client_order_id, ClientOrderId::from("O-FWD-001-E1"));
3471        assert_eq!(
3472            cmd.params.as_ref().and_then(|p| p.get_bool("is_leverage")),
3473            Some(true),
3474        );
3475    }
3476
3477    #[rstest]
3478    fn test_algorithm_routes_modify_and_cancel_commands_through_engine_queues() {
3479        let mut modify_algo = create_test_algorithm();
3480        let mut cancel_algo = create_test_algorithm();
3481        register_algorithm(&mut modify_algo);
3482        register_algorithm(&mut cancel_algo);
3483
3484        let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
3485            get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
3486        msgbus::register_trading_command_endpoint(
3487            MessagingSwitchboard::risk_engine_queue_execute(),
3488            risk_handler,
3489        );
3490        let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
3491            get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
3492        msgbus::register_trading_command_endpoint(
3493            MessagingSwitchboard::exec_engine_queue_execute(),
3494            exec_handler,
3495        );
3496
3497        let mut modify_order = TestOrderStubs::make_accepted_order(
3498            &OrderTestBuilder::new(OrderType::Limit)
3499                .strategy_id(StrategyId::from("STRAT-ALGO-ROUTING"))
3500                .instrument_id(InstrumentId::from("BTC/USDT.BINANCE"))
3501                .client_order_id(ClientOrderId::from("O-ALGO-MODIFY"))
3502                .quantity(Quantity::from("1.0"))
3503                .price(Price::from("50000.0"))
3504                .build(),
3505        );
3506        let mut cancel_order = TestOrderStubs::make_accepted_order(
3507            &OrderTestBuilder::new(OrderType::Market)
3508                .strategy_id(StrategyId::from("STRAT-ALGO-ROUTING"))
3509                .instrument_id(InstrumentId::from("BTC/USDT.BINANCE"))
3510                .client_order_id(ClientOrderId::from("O-ALGO-CANCEL"))
3511                .quantity(Quantity::from("1.0"))
3512                .build(),
3513        );
3514        {
3515            let cache_rc = modify_algo.core.cache_rc();
3516            let mut cache = cache_rc.borrow_mut();
3517            cache
3518                .add_order(modify_order.clone(), None, None, false)
3519                .unwrap();
3520        }
3521        {
3522            let cache_rc = cancel_algo.core.cache_rc();
3523            let mut cache = cache_rc.borrow_mut();
3524            cache
3525                .add_order(cancel_order.clone(), None, None, false)
3526                .unwrap();
3527        }
3528
3529        modify_algo
3530            .modify_order(
3531                &mut modify_order,
3532                None,
3533                Some(Price::from("51000.0")),
3534                None,
3535                None,
3536            )
3537            .unwrap();
3538        cancel_algo.cancel_order(&mut cancel_order, None).unwrap();
3539
3540        let risk_messages = risk_messages.get_messages();
3541        let exec_messages = exec_messages.get_messages();
3542        assert_eq!(risk_messages.len(), 1);
3543        assert!(matches!(
3544            risk_messages.first(),
3545            Some(TradingCommand::ModifyOrder(command))
3546                if command.client_order_id == modify_order.client_order_id()
3547        ));
3548        assert_eq!(exec_messages.len(), 1);
3549        assert!(matches!(
3550            exec_messages.first(),
3551            Some(TradingCommand::CancelOrder(command))
3552                if command.client_order_id == cancel_order.client_order_id()
3553        ));
3554    }
3555
3556    #[rstest]
3557    fn test_algorithm_submit_order_list_captures_params_per_order() {
3558        use nautilus_common::messages::execution::SubmitOrderList;
3559        use nautilus_model::identifiers::OrderListId;
3560
3561        let mut algo = create_test_algorithm();
3562        register_algorithm(&mut algo);
3563
3564        let strategy_id = StrategyId::from("STRAT-LIST-001");
3565        let order1 = OrderAny::Market(MarketOrder::new(
3566            TraderId::from("TRADER-001"),
3567            strategy_id,
3568            InstrumentId::from("BTC/USDT.BINANCE"),
3569            ClientOrderId::from("O-LIST-001"),
3570            OrderSide::Buy,
3571            Quantity::from("1.0"),
3572            TimeInForce::Gtc,
3573            UUID4::new(),
3574            0.into(),
3575            false,
3576            false,
3577            None,
3578            None,
3579            None,
3580            None,
3581            None,
3582            None,
3583            None,
3584            None,
3585        ));
3586        let order2 = OrderAny::Market(MarketOrder::new(
3587            TraderId::from("TRADER-001"),
3588            strategy_id,
3589            InstrumentId::from("BTC/USDT.BINANCE"),
3590            ClientOrderId::from("O-LIST-002"),
3591            OrderSide::Buy,
3592            Quantity::from("1.0"),
3593            TimeInForce::Gtc,
3594            UUID4::new(),
3595            0.into(),
3596            false,
3597            false,
3598            None,
3599            None,
3600            None,
3601            None,
3602            None,
3603            None,
3604            None,
3605            None,
3606        ));
3607        {
3608            let cache_rc = algo.core.cache_rc();
3609            let mut cache = cache_rc.borrow_mut();
3610            cache.add_order(order1.clone(), None, None, true).unwrap();
3611            cache.add_order(order2.clone(), None, None, true).unwrap();
3612        }
3613
3614        let order_list = OrderList::new(
3615            OrderListId::from("OL-001"),
3616            order1.instrument_id(),
3617            strategy_id,
3618            vec![order1.client_order_id(), order2.client_order_id()],
3619            0.into(),
3620        );
3621
3622        let mut params = nautilus_core::Params::new();
3623        params.insert("is_leverage".to_string(), serde_json::Value::Bool(true));
3624        let command = SubmitOrderList::new(
3625            TraderId::from("TRADER-001"),
3626            None,
3627            strategy_id,
3628            order_list,
3629            vec![order1.init_event().clone(), order2.init_event().clone()],
3630            order1.exec_algorithm_id(),
3631            None,
3632            Some(params),
3633            UUID4::new(),
3634            0.into(),
3635            None,
3636        );
3637        algo.execute(TradingCommand::SubmitOrderList(command))
3638            .unwrap();
3639
3640        assert_eq!(
3641            algo.order_client_ids,
3642            [
3643                ClientOrderId::from("O-LIST-001"),
3644                ClientOrderId::from("O-LIST-002"),
3645            ],
3646        );
3647
3648        for id in ["O-LIST-001", "O-LIST-002"] {
3649            assert_eq!(
3650                algo.core
3651                    .submit_params(&ClientOrderId::from(id))
3652                    .and_then(|p| p.get_bool("is_leverage")),
3653                Some(true),
3654                "expected forwarded params for {id}",
3655            );
3656        }
3657    }
3658
3659    #[rstest]
3660    fn test_algorithm_generate_order_canceled() {
3661        let mut algo = create_test_algorithm();
3662        register_algorithm(&mut algo);
3663
3664        let order = OrderAny::Market(MarketOrder::new(
3665            TraderId::from("TRADER-001"),
3666            StrategyId::from("STRAT-001"),
3667            InstrumentId::from("BTC/USDT.BINANCE"),
3668            ClientOrderId::from("O-001"),
3669            OrderSide::Buy,
3670            Quantity::from("1.0"),
3671            TimeInForce::Gtc,
3672            UUID4::new(),
3673            0.into(),
3674            false,
3675            false,
3676            None,
3677            None,
3678            None,
3679            None,
3680            None,
3681            None,
3682            None,
3683            None,
3684        ));
3685
3686        let event = algo.generate_order_canceled(&order);
3687
3688        assert_eq!(event.trader_id, TraderId::from("TRADER-001"));
3689        assert_eq!(event.strategy_id, StrategyId::from("STRAT-001"));
3690        assert_eq!(event.instrument_id, InstrumentId::from("BTC/USDT.BINANCE"));
3691        assert_eq!(event.client_order_id, ClientOrderId::from("O-001"));
3692    }
3693
3694    #[rstest]
3695    fn test_algorithm_handle_cancel_order_publishes_instrument_canceled_topic() {
3696        let mut algo = create_test_algorithm();
3697        register_algorithm(&mut algo);
3698
3699        let strategy_id = StrategyId::from("STRAT-ALGO-CANCEL-PUBLISH");
3700        let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
3701        let order = OrderAny::Market(MarketOrder::new(
3702            TraderId::from("TRADER-001"),
3703            strategy_id,
3704            instrument_id,
3705            ClientOrderId::from("O-ALGO-CANCEL"),
3706            OrderSide::Buy,
3707            Quantity::from("1.0"),
3708            TimeInForce::Gtc,
3709            UUID4::new(),
3710            0.into(),
3711            false,
3712            false,
3713            None,
3714            None,
3715            None,
3716            None,
3717            None,
3718            None,
3719            None,
3720            None,
3721        ));
3722        let order = TestOrderStubs::make_accepted_order(&order);
3723
3724        {
3725            let cache_rc = algo.core.cache_rc();
3726            let mut cache = cache_rc.borrow_mut();
3727            cache.add_order(order.clone(), None, None, false).unwrap();
3728        }
3729
3730        let received = Rc::new(RefCell::new(Vec::<OrderEventAny>::new()));
3731        let handler = TypedHandler::from({
3732            let received = received.clone();
3733            move |event: &OrderEventAny| {
3734                received.borrow_mut().push(event.clone());
3735            }
3736        });
3737        let topic = msgbus::switchboard::get_order_canceled_topic(instrument_id);
3738        msgbus::subscribe_order_events(topic.into(), handler.clone(), None);
3739
3740        let command = CancelOrder::new(
3741            order.trader_id(),
3742            None,
3743            strategy_id,
3744            instrument_id,
3745            order.client_order_id(),
3746            order.venue_order_id(),
3747            UUID4::new(),
3748            0.into(),
3749            None,
3750            None,
3751        );
3752        algo.handle_cancel_order(command).unwrap();
3753
3754        msgbus::unsubscribe_order_events(topic.into(), &handler);
3755        let received = received.borrow();
3756        assert_eq!(received.len(), 1);
3757        assert!(matches!(received[0], OrderEventAny::Canceled(_)));
3758        assert_eq!(received[0].client_order_id(), order.client_order_id());
3759        assert_eq!(received[0].instrument_id(), instrument_id);
3760    }
3761
3762    #[rstest]
3763    fn test_algorithm_execute_dispatches_modify_order_to_handler() {
3764        let unique_id = format!("TEST-{}", UUID4::new());
3765        let config = ExecutionAlgorithmConfig {
3766            exec_algorithm_id: Some(ExecAlgorithmId::new(&unique_id)),
3767            ..Default::default()
3768        };
3769        let mut algo = ModifyDispatchAlgorithm::new(config);
3770        algo.core
3771            .register(
3772                TraderId::from("TRADER-001"),
3773                Rc::new(RefCell::new(TestClock::new())),
3774                Rc::new(RefCell::new(Cache::default())),
3775            )
3776            .unwrap();
3777        algo.transition_state(ComponentTrigger::Initialize).unwrap();
3778        algo.transition_state(ComponentTrigger::Start).unwrap();
3779        algo.transition_state(ComponentTrigger::StartCompleted)
3780            .unwrap();
3781
3782        let client_order_id = ClientOrderId::from("O-ALGO-DISPATCH");
3783        let command = ModifyOrder::new(
3784            TraderId::from("TRADER-001"),
3785            None,
3786            StrategyId::from("STRAT-ALGO-DISPATCH"),
3787            InstrumentId::from("BTC/USDT.BINANCE"),
3788            client_order_id,
3789            None,
3790            Some(Quantity::from("0.5")),
3791            None,
3792            None,
3793            UUID4::new(),
3794            0.into(),
3795            None,
3796            None,
3797        );
3798
3799        algo.execute(TradingCommand::ModifyOrder(command)).unwrap();
3800
3801        assert_eq!(algo.modify_client_order_ids, vec![client_order_id]);
3802    }
3803
3804    #[rstest]
3805    fn test_algorithm_handle_modify_order_refuses_active_local_order_without_events() {
3806        let mut algo = create_test_algorithm();
3807        register_algorithm(&mut algo);
3808
3809        let strategy_id = StrategyId::from("STRAT-ALGO-MODIFY");
3810        let order = OrderTestBuilder::new(OrderType::Market)
3811            .trader_id(TraderId::from("TRADER-001"))
3812            .strategy_id(strategy_id)
3813            .instrument_id(InstrumentId::from("BTC/USDT.BINANCE"))
3814            .client_order_id(ClientOrderId::from("O-ALGO-MODIFY"))
3815            .quantity(Quantity::from("1.0"))
3816            .exec_algorithm_id(algo.id())
3817            .exec_spawn_id(ClientOrderId::from("O-ALGO-MODIFY"))
3818            .build();
3819        {
3820            let cache_rc = algo.core.cache_rc();
3821            cache_rc
3822                .borrow_mut()
3823                .add_order(order.clone(), None, None, false)
3824                .unwrap();
3825        }
3826        let (handler, events) = subscribe_order_topic(strategy_id);
3827        let command = ModifyOrder::new(
3828            order.trader_id(),
3829            None,
3830            strategy_id,
3831            order.instrument_id(),
3832            order.client_order_id(),
3833            None,
3834            Some(Quantity::from("0.5")),
3835            None,
3836            None,
3837            UUID4::new(),
3838            0.into(),
3839            None,
3840            None,
3841        );
3842
3843        algo.execute(TradingCommand::ModifyOrder(command)).unwrap();
3844
3845        msgbus::unsubscribe_order_events(format!("events.order.{strategy_id}").into(), &handler);
3846        let cached_order = algo.cache().order(&order.client_order_id()).unwrap();
3847        assert_eq!(cached_order.status(), OrderStatus::Initialized);
3848        assert_eq!(cached_order.quantity(), Quantity::from("1.0"));
3849        assert!(events.borrow().is_empty());
3850    }
3851
3852    #[rstest]
3853    fn test_algorithm_modify_order_in_place_updates_quantity() {
3854        let mut algo = create_test_algorithm();
3855        register_algorithm(&mut algo);
3856
3857        let strategy_id = StrategyId::from("STRAT-ALGO-MODIFY-IN-PLACE");
3858        let mut order = OrderAny::Limit(LimitOrder::new(
3859            TraderId::from("TRADER-001"),
3860            strategy_id,
3861            InstrumentId::from("BTC/USDT.BINANCE"),
3862            ClientOrderId::from("O-001"),
3863            OrderSide::Buy,
3864            Quantity::from("1.0"),
3865            Price::from("50000.0"),
3866            TimeInForce::Gtc,
3867            None,  // expire_time
3868            false, // post_only
3869            false, // reduce_only
3870            false, // quote_quantity
3871            None,  // display_qty
3872            None,  // emulation_trigger
3873            None,  // trigger_instrument_id
3874            None,  // contingency_type
3875            None,  // order_list_id
3876            None,  // linked_order_ids
3877            None,  // parent_order_id
3878            None,  // exec_algorithm_id
3879            None,  // exec_algorithm_params
3880            None,  // exec_spawn_id
3881            None,  // tags
3882            UUID4::new(),
3883            0.into(),
3884        ));
3885
3886        {
3887            let cache_rc = algo.core.cache_rc();
3888            let mut cache = cache_rc.borrow_mut();
3889            cache.add_order(order.clone(), None, None, false).unwrap();
3890        }
3891
3892        let new_qty = Quantity::from("0.5");
3893        let (handler, events) = subscribe_order_topic(strategy_id);
3894
3895        algo.modify_order_in_place(&mut order, Some(new_qty), None, None)
3896            .unwrap();
3897
3898        msgbus::unsubscribe_order_events(format!("events.order.{strategy_id}").into(), &handler);
3899        let events = events.borrow();
3900
3901        assert_eq!(order.quantity(), new_qty);
3902        assert_eq!(events.len(), 1);
3903        assert!(matches!(
3904            &events[0],
3905            OrderEventAny::Updated(event) if event.quantity == new_qty
3906        ));
3907    }
3908
3909    #[rstest]
3910    fn test_algorithm_modify_order_in_place_rejects_no_changes() {
3911        let mut algo = create_test_algorithm();
3912        register_algorithm(&mut algo);
3913
3914        let mut order = OrderAny::Limit(LimitOrder::new(
3915            TraderId::from("TRADER-001"),
3916            StrategyId::from("STRAT-001"),
3917            InstrumentId::from("BTC/USDT.BINANCE"),
3918            ClientOrderId::from("O-001"),
3919            OrderSide::Buy,
3920            Quantity::from("1.0"),
3921            Price::from("50000.0"),
3922            TimeInForce::Gtc,
3923            None,
3924            false,
3925            false,
3926            false,
3927            None,
3928            None,
3929            None,
3930            None,
3931            None,
3932            None,
3933            None,
3934            None,
3935            None,
3936            None,
3937            None,
3938            UUID4::new(),
3939            0.into(),
3940        ));
3941
3942        // Try to modify with same quantity - should fail
3943        let result =
3944            algo.modify_order_in_place(&mut order, Some(Quantity::from("1.0")), None, None);
3945
3946        assert!(result.is_err());
3947        assert!(
3948            result
3949                .unwrap_err()
3950                .to_string()
3951                .contains("no parameters differ")
3952        );
3953    }
3954
3955    #[rstest]
3956    fn test_spawned_order_denied_restores_primary_quantity() {
3957        let mut algo = create_test_algorithm();
3958        register_algorithm(&mut algo);
3959
3960        let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
3961        let exec_algorithm_id = algo.id();
3962        let client_order_id = ClientOrderId::from("O-001");
3963
3964        let mut primary = OrderAny::Market(MarketOrder::new(
3965            TraderId::from("TRADER-001"),
3966            StrategyId::from("STRAT-001"),
3967            instrument_id,
3968            client_order_id,
3969            OrderSide::Buy,
3970            Quantity::from("1.0"),
3971            TimeInForce::Gtc,
3972            UUID4::new(),
3973            0.into(),
3974            false,
3975            false,
3976            None,
3977            None,
3978            None,
3979            None,
3980            Some(exec_algorithm_id),
3981            None,
3982            Some(client_order_id),
3983            None,
3984        ));
3985
3986        {
3987            let cache_rc = algo.core.cache_rc();
3988            let mut cache = cache_rc.borrow_mut();
3989            cache.add_order(primary.clone(), None, None, false).unwrap();
3990        }
3991
3992        let spawned = algo.spawn_market(
3993            &mut primary,
3994            Quantity::from("0.5"),
3995            TimeInForce::Fok,
3996            false,
3997            None,
3998            true,
3999        );
4000
4001        assert_eq!(primary.quantity(), Quantity::from("0.5"));
4002
4003        let spawned_order = OrderAny::Market(spawned);
4004        {
4005            let cache_rc = algo.core.cache_rc();
4006            let mut cache = cache_rc.borrow_mut();
4007            cache
4008                .add_order(spawned_order.clone(), None, None, false)
4009                .unwrap();
4010        }
4011
4012        let denied = OrderDeniedSpec::builder()
4013            .trader_id(spawned_order.trader_id())
4014            .strategy_id(spawned_order.strategy_id())
4015            .instrument_id(spawned_order.instrument_id())
4016            .client_order_id(spawned_order.client_order_id())
4017            .reason("TEST_DENIAL".into())
4018            .build();
4019
4020        {
4021            let cache_rc = algo.core.cache_rc();
4022            let mut cache = cache_rc.borrow_mut();
4023            cache.update_order(&OrderEventAny::Denied(denied)).unwrap();
4024        }
4025
4026        algo.handle_order_event(OrderEventAny::Denied(denied));
4027
4028        let restored_primary = algo.cache().order(&client_order_id).unwrap();
4029        assert_eq!(restored_primary.quantity(), Quantity::from("1.0"));
4030    }
4031
4032    #[rstest]
4033    fn test_spawned_order_rejected_restores_primary_quantity() {
4034        let mut algo = create_test_algorithm();
4035        register_algorithm(&mut algo);
4036
4037        let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
4038        let exec_algorithm_id = algo.id();
4039        let client_order_id = ClientOrderId::from("O-001");
4040
4041        let mut primary = OrderAny::Market(MarketOrder::new(
4042            TraderId::from("TRADER-001"),
4043            StrategyId::from("STRAT-001"),
4044            instrument_id,
4045            client_order_id,
4046            OrderSide::Buy,
4047            Quantity::from("1.0"),
4048            TimeInForce::Gtc,
4049            UUID4::new(),
4050            0.into(),
4051            false,
4052            false,
4053            None,
4054            None,
4055            None,
4056            None,
4057            Some(exec_algorithm_id),
4058            None,
4059            Some(client_order_id),
4060            None,
4061        ));
4062
4063        {
4064            let cache_rc = algo.core.cache_rc();
4065            let mut cache = cache_rc.borrow_mut();
4066            cache.add_order(primary.clone(), None, None, false).unwrap();
4067        }
4068
4069        let spawned = algo.spawn_market(
4070            &mut primary,
4071            Quantity::from("0.5"),
4072            TimeInForce::Fok,
4073            false,
4074            None,
4075            true,
4076        );
4077
4078        assert_eq!(primary.quantity(), Quantity::from("0.5"));
4079
4080        let spawned_order = OrderAny::Market(spawned);
4081        {
4082            let cache_rc = algo.core.cache_rc();
4083            let mut cache = cache_rc.borrow_mut();
4084            cache
4085                .add_order(spawned_order.clone(), None, None, false)
4086                .unwrap();
4087        }
4088
4089        let rejected = OrderRejectedSpec::builder()
4090            .trader_id(spawned_order.trader_id())
4091            .strategy_id(spawned_order.strategy_id())
4092            .instrument_id(spawned_order.instrument_id())
4093            .client_order_id(spawned_order.client_order_id())
4094            .account_id(AccountId::from("BINANCE-001"))
4095            .reason("TEST_REJECTION".into())
4096            .build();
4097
4098        {
4099            let cache_rc = algo.core.cache_rc();
4100            let mut cache = cache_rc.borrow_mut();
4101            cache
4102                .update_order(&OrderEventAny::Rejected(rejected))
4103                .unwrap();
4104        }
4105
4106        algo.handle_order_event(OrderEventAny::Rejected(rejected));
4107
4108        let restored_primary = algo.cache().order(&client_order_id).unwrap();
4109        assert_eq!(restored_primary.quantity(), Quantity::from("1.0"));
4110    }
4111
4112    #[rstest]
4113    fn test_spawned_order_with_reduce_primary_false_does_not_restore() {
4114        let mut algo = create_test_algorithm();
4115        register_algorithm(&mut algo);
4116
4117        let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
4118        let exec_algorithm_id = algo.id();
4119        let client_order_id = ClientOrderId::from("O-001");
4120
4121        let mut primary = OrderAny::Market(MarketOrder::new(
4122            TraderId::from("TRADER-001"),
4123            StrategyId::from("STRAT-001"),
4124            instrument_id,
4125            client_order_id,
4126            OrderSide::Buy,
4127            Quantity::from("1.0"),
4128            TimeInForce::Gtc,
4129            UUID4::new(),
4130            0.into(),
4131            false,
4132            false,
4133            None,
4134            None,
4135            None,
4136            None,
4137            Some(exec_algorithm_id),
4138            None,
4139            Some(client_order_id),
4140            None,
4141        ));
4142
4143        {
4144            let cache_rc = algo.core.cache_rc();
4145            let mut cache = cache_rc.borrow_mut();
4146            cache.add_order(primary.clone(), None, None, false).unwrap();
4147        }
4148
4149        let spawned = algo.spawn_market(
4150            &mut primary,
4151            Quantity::from("0.5"),
4152            TimeInForce::Fok,
4153            false,
4154            None,
4155            false,
4156        );
4157
4158        assert_eq!(primary.quantity(), Quantity::from("1.0"));
4159
4160        let spawned_order = OrderAny::Market(spawned);
4161        {
4162            let cache_rc = algo.core.cache_rc();
4163            let mut cache = cache_rc.borrow_mut();
4164            cache
4165                .add_order(spawned_order.clone(), None, None, false)
4166                .unwrap();
4167        }
4168
4169        let denied = OrderDeniedSpec::builder()
4170            .trader_id(spawned_order.trader_id())
4171            .strategy_id(spawned_order.strategy_id())
4172            .instrument_id(spawned_order.instrument_id())
4173            .client_order_id(spawned_order.client_order_id())
4174            .reason("TEST_DENIAL".into())
4175            .build();
4176
4177        {
4178            let cache_rc = algo.core.cache_rc();
4179            let mut cache = cache_rc.borrow_mut();
4180            cache.update_order(&OrderEventAny::Denied(denied)).unwrap();
4181        }
4182
4183        algo.handle_order_event(OrderEventAny::Denied(denied));
4184
4185        let final_primary = algo.cache().order(&client_order_id).unwrap();
4186        assert_eq!(final_primary.quantity(), Quantity::from("1.0"));
4187    }
4188
4189    #[rstest]
4190    fn test_multiple_spawns_with_one_denied_restores_correctly() {
4191        let mut algo = create_test_algorithm();
4192        register_algorithm(&mut algo);
4193
4194        let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
4195        let exec_algorithm_id = algo.id();
4196        let client_order_id = ClientOrderId::from("O-001");
4197
4198        let mut primary = OrderAny::Market(MarketOrder::new(
4199            TraderId::from("TRADER-001"),
4200            StrategyId::from("STRAT-001"),
4201            instrument_id,
4202            client_order_id,
4203            OrderSide::Buy,
4204            Quantity::from("1.0"),
4205            TimeInForce::Gtc,
4206            UUID4::new(),
4207            0.into(),
4208            false,
4209            false,
4210            None,
4211            None,
4212            None,
4213            None,
4214            Some(exec_algorithm_id),
4215            None,
4216            Some(client_order_id),
4217            None,
4218        ));
4219
4220        {
4221            let cache_rc = algo.core.cache_rc();
4222            let mut cache = cache_rc.borrow_mut();
4223            cache.add_order(primary.clone(), None, None, false).unwrap();
4224        }
4225
4226        let spawned1 = algo.spawn_market(
4227            &mut primary,
4228            Quantity::from("0.3"),
4229            TimeInForce::Fok,
4230            false,
4231            None,
4232            true,
4233        );
4234        let spawned2 = algo.spawn_market(
4235            &mut primary,
4236            Quantity::from("0.4"),
4237            TimeInForce::Fok,
4238            false,
4239            None,
4240            true,
4241        );
4242        assert_eq!(primary.quantity(), Quantity::from("0.3"));
4243
4244        let spawned_order1 = OrderAny::Market(spawned1);
4245        let spawned_order2 = OrderAny::Market(spawned2);
4246        {
4247            let cache_rc = algo.core.cache_rc();
4248            let mut cache = cache_rc.borrow_mut();
4249            cache.add_order(spawned_order1, None, None, false).unwrap();
4250            cache
4251                .add_order(spawned_order2.clone(), None, None, false)
4252                .unwrap();
4253        }
4254
4255        let denied = OrderDeniedSpec::builder()
4256            .trader_id(spawned_order2.trader_id())
4257            .strategy_id(spawned_order2.strategy_id())
4258            .instrument_id(spawned_order2.instrument_id())
4259            .client_order_id(spawned_order2.client_order_id())
4260            .reason("TEST_DENIAL".into())
4261            .build();
4262
4263        {
4264            let cache_rc = algo.core.cache_rc();
4265            let mut cache = cache_rc.borrow_mut();
4266            cache.update_order(&OrderEventAny::Denied(denied)).unwrap();
4267        }
4268
4269        let (handler, events) = subscribe_order_topic(spawned_order2.strategy_id());
4270
4271        algo.handle_order_event(OrderEventAny::Denied(denied));
4272
4273        msgbus::unsubscribe_order_events(
4274            format!("events.order.{}", spawned_order2.strategy_id()).into(),
4275            &handler,
4276        );
4277        let events = events.borrow();
4278
4279        let restored_primary = algo.cache().order(&client_order_id).unwrap();
4280        assert_eq!(restored_primary.quantity(), Quantity::from("0.7"));
4281        assert_eq!(events.len(), 1);
4282        assert!(matches!(
4283            &events[0],
4284            OrderEventAny::Updated(event) if event.quantity == Quantity::from("0.7")
4285        ));
4286    }
4287
4288    #[rstest]
4289    fn test_spawned_order_accepted_then_canceled_restores_reduction() {
4290        let (mut algo, client_order_id, mut spawned_order) = setup_accepted_spawn();
4291
4292        let primary_after_accept = algo.cache().order(&client_order_id).unwrap();
4293        assert_eq!(primary_after_accept.quantity(), Quantity::from("0.5"));
4294
4295        // Per the maintainer's ruling, acceptance preserves the reduction until terminal outcome
4296        cancel_spawned_order(&mut algo, &mut spawned_order);
4297
4298        let final_primary = algo.cache().order(&client_order_id).unwrap();
4299        assert_eq!(final_primary.quantity(), Quantity::from("1.0"));
4300    }
4301
4302    #[rstest]
4303    fn test_spawned_order_canceled_after_primary_submission_does_not_restore_reduction() {
4304        let (mut algo, client_order_id, mut spawned_order) = setup_accepted_spawn();
4305        let mut primary = algo.cache().order(&client_order_id).unwrap();
4306        submit_order_in_cache(&mut algo, &mut primary);
4307
4308        cancel_spawned_order(&mut algo, &mut spawned_order);
4309
4310        let mut submitted_primary = algo.cache().order(&client_order_id).unwrap();
4311        assert_eq!(submitted_primary.quantity(), Quantity::from("0.5"));
4312        assert!(
4313            algo.core
4314                .take_pending_spawn_reduction(spawned_order.client_order_id())
4315                .is_none()
4316        );
4317        fill_spawned_order(&mut algo, &mut submitted_primary, Quantity::from("0.5"));
4318        assert!(submitted_primary.is_closed());
4319        assert_eq!(submitted_primary.status(), OrderStatus::Filled);
4320    }
4321
4322    #[rstest]
4323    fn test_spawned_order_canceled_during_primary_submission_handoff_does_not_restore() {
4324        let (mut algo, client_order_id, mut spawned_order) = setup_accepted_spawn();
4325        let primary = algo.cache().order(&client_order_id).unwrap();
4326        algo.core
4327            .add_spawn_fill_debt(client_order_id, Quantity::from("0.1"));
4328        let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
4329            get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
4330        msgbus::register_trading_command_endpoint(
4331            MessagingSwitchboard::risk_engine_queue_execute(),
4332            risk_handler,
4333        );
4334        let strategy_id = primary.strategy_id();
4335        let (event_handler, events) = subscribe_order_topic(strategy_id);
4336
4337        algo.submit_order(primary, None, None).unwrap();
4338        assert_eq!(
4339            algo.cache().order(&client_order_id).unwrap().status(),
4340            OrderStatus::Initialized,
4341        );
4342        cancel_spawned_order(&mut algo, &mut spawned_order);
4343
4344        msgbus::unsubscribe_order_events(
4345            format!("events.order.{strategy_id}").into(),
4346            &event_handler,
4347        );
4348        let risk_messages = risk_messages.get_messages();
4349        let [TradingCommand::SubmitOrder(command)] = risk_messages.as_slice() else {
4350            panic!("Expected exactly one SubmitOrder command");
4351        };
4352        assert_eq!(command.client_order_id, client_order_id);
4353        // The command embeds the immutable OrderInitialized event, so its
4354        // quantity is the pre-reduction 1.0; consumers resolve the cached
4355        // order, which carries the reduced quantity asserted below.
4356        assert_eq!(command.order_init.quantity, Quantity::from("1.0"));
4357        assert_eq!(
4358            algo.cache().order(&client_order_id).unwrap().quantity(),
4359            Quantity::from("0.5"),
4360        );
4361        assert!(!events.borrow().iter().any(|event| matches!(
4362            event,
4363            OrderEventAny::Updated(updated) if updated.client_order_id == client_order_id
4364        )));
4365        assert!(
4366            algo.core
4367                .take_pending_spawn_reduction(spawned_order.client_order_id())
4368                .is_none()
4369        );
4370        assert!(algo.core.spawn_fill_debt(client_order_id).is_none());
4371    }
4372
4373    #[rstest]
4374    fn test_late_spawn_fill_rededucts_restored_primary_before_final_submission() {
4375        let (mut algo, client_order_id, mut spawned_order) = setup_accepted_spawn();
4376        cancel_spawned_order(&mut algo, &mut spawned_order);
4377        assert_eq!(
4378            algo.cache().order(&client_order_id).unwrap().quantity(),
4379            Quantity::from("1.0"),
4380        );
4381
4382        fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("0.5"));
4383
4384        let mut primary = algo.cache().order(&client_order_id).unwrap();
4385        assert_eq!(primary.quantity(), Quantity::from("0.5"));
4386        submit_order_in_cache(&mut algo, &mut primary);
4387        assert_eq!(
4388            spawned_order.filled_qty() + primary.quantity(),
4389            Quantity::from("1.0"),
4390        );
4391    }
4392
4393    #[rstest]
4394    fn test_late_spawn_fill_during_primary_submission_handoff_does_not_rededuct() {
4395        let (mut algo, client_order_id, mut spawned_order) = setup_accepted_spawn();
4396        cancel_spawned_order(&mut algo, &mut spawned_order);
4397        let primary = algo.cache().order(&client_order_id).unwrap();
4398        algo.core
4399            .add_spawn_fill_debt(client_order_id, Quantity::from("0.1"));
4400        let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
4401            get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
4402        msgbus::register_trading_command_endpoint(
4403            MessagingSwitchboard::risk_engine_queue_execute(),
4404            risk_handler,
4405        );
4406        let strategy_id = primary.strategy_id();
4407        let (event_handler, events) = subscribe_order_topic(strategy_id);
4408
4409        algo.submit_order(primary, None, None).unwrap();
4410        assert_eq!(
4411            algo.cache().order(&client_order_id).unwrap().status(),
4412            OrderStatus::Initialized,
4413        );
4414        fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("0.5"));
4415
4416        msgbus::unsubscribe_order_events(
4417            format!("events.order.{strategy_id}").into(),
4418            &event_handler,
4419        );
4420        let risk_messages = risk_messages.get_messages();
4421        let [TradingCommand::SubmitOrder(command)] = risk_messages.as_slice() else {
4422            panic!("Expected exactly one SubmitOrder command");
4423        };
4424        assert_eq!(command.client_order_id, client_order_id);
4425        // The command embeds the immutable OrderInitialized event; here the
4426        // cancellation already restored the cache to 1.0, so the two agree.
4427        assert_eq!(command.order_init.quantity, Quantity::from("1.0"));
4428        assert_eq!(
4429            algo.cache().order(&client_order_id).unwrap().quantity(),
4430            Quantity::from("1.0"),
4431        );
4432        assert!(!events.borrow().iter().any(|event| matches!(
4433            event,
4434            OrderEventAny::Updated(updated) if updated.client_order_id == client_order_id
4435        )));
4436        assert!(
4437            algo.core
4438                .take_pending_spawn_reduction(spawned_order.client_order_id())
4439                .is_none()
4440        );
4441        assert!(algo.core.spawn_fill_debt(client_order_id).is_none());
4442    }
4443
4444    #[rstest]
4445    fn test_converted_quote_spawn_canceled_unfilled_restores_full_quote_quantity() {
4446        let (mut algo, client_order_id, mut spawned_order) =
4447            setup_accepted_quote_spawn(Quantity::from("100"), Quantity::from("50"));
4448        convert_spawn_to_base(&mut algo, &mut spawned_order, Quantity::from("5"));
4449
4450        cancel_spawned_order(&mut algo, &mut spawned_order);
4451
4452        assert_eq!(
4453            algo.cache().order(&client_order_id).unwrap().quantity(),
4454            Quantity::from("100"),
4455        );
4456        assert!(
4457            algo.cache()
4458                .order(&client_order_id)
4459                .unwrap()
4460                .is_quote_quantity()
4461        );
4462        assert_eq!(
4463            algo.core
4464                .spawn_reduction(spawned_order.client_order_id())
4465                .unwrap()
4466                .restored_qty
4467                .unwrap(),
4468            Quantity::from("50"),
4469        );
4470    }
4471
4472    #[rstest]
4473    fn test_converted_quote_spawn_partial_fill_restores_proportional_quote_quantity() {
4474        let (mut algo, client_order_id, mut spawned_order) =
4475            setup_accepted_quote_spawn(Quantity::from("100"), Quantity::from("50"));
4476        convert_spawn_to_base(&mut algo, &mut spawned_order, Quantity::from("5"));
4477        fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("2"));
4478
4479        cancel_spawned_order(&mut algo, &mut spawned_order);
4480
4481        assert_eq!(
4482            algo.cache().order(&client_order_id).unwrap().quantity(),
4483            Quantity::from("80"),
4484        );
4485        assert!(
4486            algo.cache()
4487                .order(&client_order_id)
4488                .unwrap()
4489                .is_quote_quantity()
4490        );
4491        assert_eq!(
4492            algo.core
4493                .spawn_reduction(spawned_order.client_order_id())
4494                .unwrap()
4495                .restored_qty
4496                .unwrap(),
4497            Quantity::from("30"),
4498        );
4499    }
4500
4501    #[rstest]
4502    fn test_converted_quote_spawn_late_fill_charges_proportional_quote_quantity() {
4503        let (mut algo, client_order_id, mut spawned_order) =
4504            setup_accepted_quote_spawn(Quantity::from("100"), Quantity::from("50"));
4505        convert_spawn_to_base(&mut algo, &mut spawned_order, Quantity::from("5"));
4506        cancel_spawned_order(&mut algo, &mut spawned_order);
4507
4508        fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("1"));
4509
4510        assert_eq!(
4511            algo.cache().order(&client_order_id).unwrap().quantity(),
4512            Quantity::from("90"),
4513        );
4514        assert!(
4515            algo.cache()
4516                .order(&client_order_id)
4517                .unwrap()
4518                .is_quote_quantity()
4519        );
4520        assert_eq!(
4521            algo.core
4522                .spawn_reduction(spawned_order.client_order_id())
4523                .unwrap()
4524                .restored_qty
4525                .unwrap(),
4526            Quantity::from("40"),
4527        );
4528    }
4529
4530    #[rstest]
4531    fn test_converted_quote_spawn_restoration_rounds_down_to_primary_precision() {
4532        let (mut algo, client_order_id, mut spawned_order) =
4533            setup_accepted_quote_spawn(Quantity::from("20.00"), Quantity::from("10.00"));
4534        convert_spawn_to_base(&mut algo, &mut spawned_order, Quantity::from("3.000"));
4535        fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("1.000"));
4536
4537        cancel_spawned_order(&mut algo, &mut spawned_order);
4538
4539        assert_eq!(
4540            algo.cache().order(&client_order_id).unwrap().quantity(),
4541            Quantity::from("16.66"),
4542        );
4543        assert!(
4544            algo.cache()
4545                .order(&client_order_id)
4546                .unwrap()
4547                .is_quote_quantity()
4548        );
4549        assert_eq!(
4550            algo.core
4551                .spawn_reduction(spawned_order.client_order_id())
4552                .unwrap()
4553                .restored_qty
4554                .unwrap(),
4555            Quantity::from("6.66"),
4556        );
4557    }
4558
4559    #[rstest]
4560    fn test_converted_quote_spawn_repeated_fractional_late_fills_conserve_budget() {
4561        let (mut algo, client_order_id, mut spawned_order) =
4562            setup_accepted_quote_spawn(Quantity::from("20.00"), Quantity::from("10.00"));
4563        convert_spawn_to_base(&mut algo, &mut spawned_order, Quantity::from("3.000"));
4564        cancel_spawned_order(&mut algo, &mut spawned_order);
4565
4566        for (primary_qty, restored_qty) in [
4567            (Quantity::from("16.66"), Quantity::from("6.66")),
4568            (Quantity::from("13.33"), Quantity::from("3.33")),
4569            (Quantity::from("10.00"), Quantity::from("0.00")),
4570        ] {
4571            fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("1.000"));
4572            assert_eq!(
4573                algo.cache().order(&client_order_id).unwrap().quantity(),
4574                primary_qty,
4575            );
4576            assert_eq!(
4577                algo.core
4578                    .spawn_reduction(spawned_order.client_order_id())
4579                    .unwrap()
4580                    .restored_qty
4581                    .unwrap(),
4582                restored_qty,
4583            );
4584        }
4585    }
4586
4587    #[rstest]
4588    #[case::single_fill(Quantity::from("3.0"), 1)]
4589    #[case::split_fills(Quantity::from("0.1"), 30)]
4590    fn test_converted_quote_spawn_fill_partition_preserves_total(
4591        #[case] fill_qty: Quantity,
4592        #[case] fills: usize,
4593    ) {
4594        let (mut algo, primary_id, mut child) =
4595            setup_accepted_quote_spawn(Quantity::from("20"), Quantity::from("10"));
4596        convert_spawn_to_base(&mut algo, &mut child, Quantity::from("3.0"));
4597        cancel_spawned_order(&mut algo, &mut child);
4598
4599        for _ in 0..fills {
4600            fill_spawned_order(&mut algo, &mut child, fill_qty);
4601        }
4602
4603        assert_eq!(child.filled_qty(), Quantity::from("3.0"));
4604        assert_eq!(
4605            algo.cache().order(&primary_id).unwrap().quantity(),
4606            Quantity::from("10")
4607        );
4608        assert_eq!(
4609            algo.core
4610                .spawn_reduction(child.client_order_id())
4611                .unwrap()
4612                .restored_qty,
4613            Some(Quantity::from("0"))
4614        );
4615        assert!(algo.core.spawn_fill_debt(primary_id).is_none());
4616    }
4617
4618    #[rstest]
4619    fn test_increased_spawn_late_fill_debits_original_restoration() {
4620        let (mut algo, primary_id, mut child) = setup_pending_spawn();
4621        algo.modify_order_in_place(&mut child, Some(Quantity::from("0.8")), None, None)
4622            .unwrap();
4623        accept_spawned_order(&mut algo, &mut child);
4624        cancel_spawned_order(&mut algo, &mut child);
4625
4626        fill_spawned_order(&mut algo, &mut child, Quantity::from("0.1"));
4627
4628        assert_eq!(child.quantity(), Quantity::from("0.8"));
4629        assert_eq!(child.filled_qty(), Quantity::from("0.1"));
4630        assert_eq!(
4631            algo.cache().order(&primary_id).unwrap().quantity(),
4632            Quantity::from("0.9")
4633        );
4634        assert_eq!(
4635            algo.core
4636                .spawn_reduction(child.client_order_id())
4637                .unwrap()
4638                .restored_qty,
4639            Some(Quantity::from("0.4"))
4640        );
4641        assert!(algo.core.spawn_fill_debt(primary_id).is_none());
4642
4643        void_last_spawn_fill(&mut algo, &mut child, Quantity::from("0.1"));
4644
4645        assert_eq!(
4646            algo.cache().order(&primary_id).unwrap().quantity(),
4647            Quantity::from("1.0")
4648        );
4649    }
4650
4651    #[rstest]
4652    #[case::partial_fill(Quantity::from("0.1"))]
4653    #[case::full_fill(Quantity::from("0.5"))]
4654    fn test_voided_late_spawn_fill_restores_primary_quantity(#[case] fill_qty: Quantity) {
4655        let (mut algo, primary_id, mut child) = setup_accepted_spawn();
4656        cancel_spawned_order(&mut algo, &mut child);
4657        fill_spawned_order(&mut algo, &mut child, fill_qty);
4658
4659        void_last_spawn_fill(&mut algo, &mut child, fill_qty);
4660
4661        assert_eq!(child.filled_qty(), Quantity::from("0.0"));
4662        assert_eq!(
4663            algo.cache().order(&primary_id).unwrap().quantity(),
4664            Quantity::from("1.0")
4665        );
4666        assert!(algo.core.spawn_fill_debt(primary_id).is_none());
4667        assert_eq!(
4668            algo.core
4669                .spawn_reduction(child.client_order_id())
4670                .unwrap()
4671                .restored_qty,
4672            Some(Quantity::from("0.5"))
4673        );
4674    }
4675
4676    #[rstest]
4677    #[case::debt_only(
4678        Quantity::from("0.1"),
4679        Quantity::from("0.0"),
4680        Some(Quantity::from("0.1"))
4681    )]
4682    #[case::debt_and_quantity(Quantity::from("0.3"), Quantity::from("0.1"), None)]
4683    fn test_voided_late_spawn_fill_discharges_debt_before_restoring_quantity(
4684        #[case] voided_qty: Quantity,
4685        #[case] primary_qty: Quantity,
4686        #[case] debt_qty: Option<Quantity>,
4687    ) {
4688        let (mut algo, primary_id, mut child) = setup_accepted_spawn();
4689        cancel_spawned_order(&mut algo, &mut child);
4690        let _second = spawn_reduced_child(&mut algo, primary_id, Quantity::from("0.8"));
4691        fill_spawned_order(&mut algo, &mut child, Quantity::from("0.4"));
4692        assert_eq!(
4693            algo.core.spawn_fill_debt(primary_id),
4694            Some(Quantity::from("0.2"))
4695        );
4696
4697        void_last_spawn_fill(&mut algo, &mut child, voided_qty);
4698
4699        assert_eq!(
4700            algo.cache().order(&primary_id).unwrap().quantity(),
4701            primary_qty
4702        );
4703        assert_eq!(algo.core.spawn_fill_debt(primary_id), debt_qty);
4704        assert_eq!(child.filled_qty(), Quantity::from("0.4") - voided_qty);
4705        assert_eq!(
4706            algo.core
4707                .spawn_reduction(child.client_order_id())
4708                .unwrap()
4709                .restored_qty,
4710            Some(Quantity::from("0.1") + voided_qty)
4711        );
4712    }
4713
4714    #[rstest]
4715    fn test_spawn_fill_void_before_restoration_preserves_reserved_quantity() {
4716        let (mut algo, primary_id, mut child) = setup_accepted_spawn();
4717        fill_spawned_order(&mut algo, &mut child, Quantity::from("0.1"));
4718
4719        void_last_spawn_fill(&mut algo, &mut child, Quantity::from("0.1"));
4720
4721        assert_eq!(
4722            algo.cache().order(&primary_id).unwrap().quantity(),
4723            Quantity::from("0.5")
4724        );
4725        assert_eq!(
4726            algo.core
4727                .spawn_reduction(child.client_order_id())
4728                .unwrap()
4729                .restored_qty,
4730            None
4731        );
4732        cancel_spawned_order(&mut algo, &mut child);
4733        assert_eq!(
4734            algo.cache().order(&primary_id).unwrap().quantity(),
4735            Quantity::from("1.0")
4736        );
4737    }
4738
4739    #[rstest]
4740    fn test_spawn_fill_void_after_primary_handoff_preserves_submitted_quantity() {
4741        let (mut algo, primary_id, mut child) = setup_accepted_spawn();
4742        cancel_spawned_order(&mut algo, &mut child);
4743        fill_spawned_order(&mut algo, &mut child, Quantity::from("0.1"));
4744        let primary = algo.cache().order(&primary_id).unwrap();
4745        let (handler, _messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
4746            get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
4747        msgbus::register_trading_command_endpoint(
4748            MessagingSwitchboard::risk_engine_queue_execute(),
4749            handler,
4750        );
4751        algo.submit_order(primary, None, None).unwrap();
4752
4753        void_last_spawn_fill(&mut algo, &mut child, Quantity::from("0.1"));
4754
4755        assert_eq!(
4756            algo.cache().order(&primary_id).unwrap().quantity(),
4757            Quantity::from("0.9")
4758        );
4759        assert_eq!(
4760            algo.cache().order(&primary_id).unwrap().status(),
4761            OrderStatus::Initialized
4762        );
4763        assert!(algo.core.primary_was_handed_off(primary_id));
4764        assert!(algo.core.spawn_reduction(child.client_order_id()).is_none());
4765        assert!(algo.core.spawn_fill_debt(primary_id).is_none());
4766    }
4767
4768    #[rstest]
4769    fn test_converted_quote_spawn_fill_void_restores_cumulative_budget_once() {
4770        let (mut algo, primary_id, mut child) =
4771            setup_accepted_quote_spawn(Quantity::from("20.00"), Quantity::from("10.00"));
4772        convert_spawn_to_base(&mut algo, &mut child, Quantity::from("3.000"));
4773        cancel_spawned_order(&mut algo, &mut child);
4774        fill_spawned_order(&mut algo, &mut child, Quantity::from("1.000"));
4775        fill_spawned_order(&mut algo, &mut child, Quantity::from("1.000"));
4776
4777        void_last_spawn_fill(&mut algo, &mut child, Quantity::from("0.500"));
4778        assert_eq!(
4779            algo.cache().order(&primary_id).unwrap().quantity(),
4780            Quantity::from("15.00")
4781        );
4782        void_last_spawn_fill(&mut algo, &mut child, Quantity::from("1.000"));
4783        let duplicate = child.events().into_iter().last().unwrap().clone();
4784        algo.handle_order_event(duplicate);
4785
4786        assert_eq!(child.filled_qty(), Quantity::from("1.000"));
4787        assert_eq!(
4788            algo.cache().order(&primary_id).unwrap().quantity(),
4789            Quantity::from("16.66")
4790        );
4791        assert_eq!(
4792            algo.core
4793                .spawn_reduction(child.client_order_id())
4794                .unwrap()
4795                .restored_qty,
4796            Some(Quantity::from("6.66"))
4797        );
4798        assert!(algo.core.spawn_fill_debt(primary_id).is_none());
4799    }
4800
4801    fn void_last_spawn_fill(algo: &mut TestAlgorithm, order: &mut OrderAny, quantity: Quantity) {
4802        let fill = order
4803            .events()
4804            .into_iter()
4805            .rev()
4806            .find_map(|event| match event {
4807                OrderEventAny::Filled(fill) => Some(fill.clone()),
4808                _ => None,
4809            })
4810            .unwrap();
4811        let voided = OrderFillVoidedSpec::builder()
4812            .trader_id(fill.trader_id)
4813            .strategy_id(fill.strategy_id)
4814            .instrument_id(fill.instrument_id)
4815            .client_order_id(fill.client_order_id)
4816            .venue_order_id(fill.venue_order_id)
4817            .account_id(fill.account_id)
4818            .trade_id(fill.trade_id)
4819            .voided_qty(quantity)
4820            .order_side(fill.order_side)
4821            .order_type(fill.order_type)
4822            .last_px(fill.last_px)
4823            .currency(fill.currency)
4824            .liquidity_side(fill.liquidity_side)
4825            .build();
4826        *order = algo
4827            .core
4828            .cache_rc()
4829            .borrow_mut()
4830            .update_order(&OrderEventAny::FillVoided(voided.clone()))
4831            .unwrap();
4832        algo.handle_order_event(OrderEventAny::FillVoided(voided));
4833    }
4834
4835    #[rstest]
4836    fn test_primary_handoff_clears_spawn_accounting_without_child_events() {
4837        let (mut algo, primary_id, mut child) = setup_accepted_spawn();
4838        cancel_spawned_order(&mut algo, &mut child);
4839        let primary = algo.cache().order(&primary_id).unwrap();
4840        let (handler, _messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
4841            get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
4842        msgbus::register_trading_command_endpoint(
4843            MessagingSwitchboard::risk_engine_queue_execute(),
4844            handler,
4845        );
4846
4847        algo.submit_order(primary, None, None).unwrap();
4848
4849        assert!(algo.core.spawn_reduction(child.client_order_id()).is_none());
4850        assert!(algo.core.spawn_fill_debt(primary_id).is_none());
4851        assert!(algo.core.primary_was_handed_off(primary_id));
4852
4853        let mut primary = algo.cache().order(&primary_id).unwrap();
4854        submit_order_in_cache(&mut algo, &mut primary);
4855        assert!(!algo.core.primary_was_handed_off(primary_id));
4856    }
4857
4858    #[rstest]
4859    fn test_primary_cancellation_clears_spawn_accounting() {
4860        let (mut algo, primary_id, mut child) = setup_accepted_spawn();
4861        cancel_spawned_order(&mut algo, &mut child);
4862        let second = spawn_reduced_child(&mut algo, primary_id, Quantity::from("0.8"));
4863        fill_spawned_order(&mut algo, &mut child, Quantity::from("0.4"));
4864        let mut primary = algo.cache().order(&primary_id).unwrap();
4865
4866        cancel_spawned_order(&mut algo, &mut primary);
4867
4868        assert_eq!(primary.status(), OrderStatus::Canceled);
4869        assert!(algo.core.spawn_reduction(child.client_order_id()).is_none());
4870        assert!(
4871            algo.core
4872                .spawn_reduction(second.client_order_id())
4873                .is_none()
4874        );
4875        assert!(algo.core.spawn_fill_debt(primary_id).is_none());
4876        assert!(!algo.core.primary_was_handed_off(primary_id));
4877    }
4878
4879    #[rstest]
4880    fn test_converted_quote_spawn_partial_cancel_then_late_fill_charges_restored_budget() {
4881        let (mut algo, client_order_id, mut spawned_order) =
4882            setup_accepted_quote_spawn(Quantity::from("100"), Quantity::from("50"));
4883        convert_spawn_to_base(&mut algo, &mut spawned_order, Quantity::from("5"));
4884        fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("2"));
4885        cancel_spawned_order(&mut algo, &mut spawned_order);
4886
4887        assert_eq!(
4888            algo.cache().order(&client_order_id).unwrap().quantity(),
4889            Quantity::from("80"),
4890        );
4891        assert_eq!(
4892            algo.core
4893                .spawn_reduction(spawned_order.client_order_id())
4894                .unwrap()
4895                .restored_qty
4896                .unwrap(),
4897            Quantity::from("30"),
4898        );
4899
4900        fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("1"));
4901
4902        assert_eq!(
4903            algo.cache().order(&client_order_id).unwrap().quantity(),
4904            Quantity::from("70"),
4905        );
4906        assert_eq!(
4907            algo.core
4908                .spawn_reduction(spawned_order.client_order_id())
4909                .unwrap()
4910                .restored_qty
4911                .unwrap(),
4912            Quantity::from("20"),
4913        );
4914    }
4915
4916    #[rstest]
4917    fn test_unmarked_submitted_primary_cancellation_discards_reduction_and_debt() {
4918        let (mut algo, client_order_id, mut spawned_order) = setup_accepted_spawn();
4919        let mut primary = algo.cache().order(&client_order_id).unwrap();
4920        algo.core
4921            .add_spawn_fill_debt(client_order_id, Quantity::from("0.1"));
4922
4923        submit_order_in_cache(&mut algo, &mut primary);
4924        assert!(algo.core.spawn_fill_debt(client_order_id).is_none());
4925        cancel_spawned_order(&mut algo, &mut spawned_order);
4926
4927        assert_eq!(
4928            algo.cache().order(&client_order_id).unwrap().quantity(),
4929            Quantity::from("0.5"),
4930        );
4931        assert!(
4932            algo.core
4933                .spawn_reduction(spawned_order.client_order_id())
4934                .is_none()
4935        );
4936        assert!(algo.core.spawn_fill_debt(client_order_id).is_none());
4937    }
4938
4939    #[rstest]
4940    #[case::denied(true)]
4941    #[case::rejected(false)]
4942    fn test_spawn_refusal_does_not_restore_submitted_primary(#[case] denied: bool) {
4943        let (mut algo, client_order_id, spawned_order) = setup_pending_spawn();
4944        let spawned_id = spawned_order.client_order_id();
4945        let mut primary = algo.cache().order(&client_order_id).unwrap();
4946        submit_order_in_cache(&mut algo, &mut primary);
4947
4948        if denied {
4949            let event = OrderDeniedSpec::builder()
4950                .trader_id(spawned_order.trader_id())
4951                .strategy_id(spawned_order.strategy_id())
4952                .instrument_id(spawned_order.instrument_id())
4953                .client_order_id(spawned_id)
4954                .reason("TEST_DENIAL".into())
4955                .build();
4956            algo.handle_order_event(OrderEventAny::Denied(event));
4957        } else {
4958            let event = OrderRejectedSpec::builder()
4959                .trader_id(spawned_order.trader_id())
4960                .strategy_id(spawned_order.strategy_id())
4961                .instrument_id(spawned_order.instrument_id())
4962                .client_order_id(spawned_id)
4963                .account_id(AccountId::from("BINANCE-001"))
4964                .reason("TEST_REJECTION".into())
4965                .build();
4966            algo.handle_order_event(OrderEventAny::Rejected(event));
4967        }
4968
4969        assert_eq!(
4970            algo.cache().order(&client_order_id).unwrap().quantity(),
4971            Quantity::from("0.5"),
4972        );
4973        assert!(algo.core.take_pending_spawn_reduction(spawned_id).is_none());
4974    }
4975
4976    #[rstest]
4977    fn test_multiple_late_partial_fills_net_only_the_restored_quantity() {
4978        let (mut algo, client_order_id, mut spawned_order) = setup_accepted_spawn();
4979        fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("0.2"));
4980        cancel_spawned_order(&mut algo, &mut spawned_order);
4981
4982        fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("0.1"));
4983        assert_eq!(
4984            algo.cache().order(&client_order_id).unwrap().quantity(),
4985            Quantity::from("0.7"),
4986        );
4987
4988        fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("0.2"));
4989        assert_eq!(
4990            algo.cache().order(&client_order_id).unwrap().quantity(),
4991            Quantity::from("0.5"),
4992        );
4993        assert_eq!(
4994            algo.core
4995                .spawn_reduction(spawned_order.client_order_id())
4996                .unwrap()
4997                .restored_qty,
4998            Some(Quantity::from("0.0")),
4999        );
5000    }
5001
5002    #[rstest]
5003    fn test_late_spawn_fill_after_restored_quantity_reused_caps_at_primary_quantity() {
5004        let (mut algo, client_order_id, mut spawned_order) = setup_accepted_spawn();
5005        cancel_spawned_order(&mut algo, &mut spawned_order);
5006        assert_eq!(
5007            algo.cache().order(&client_order_id).unwrap().quantity(),
5008            Quantity::from("1.0"),
5009        );
5010
5011        // Reuse most of the restored quantity through a second spawn
5012        let mut primary = algo.cache().order(&client_order_id).unwrap();
5013        let second_order = OrderAny::Market(algo.spawn_market(
5014            &mut primary,
5015            Quantity::from("0.8"),
5016            TimeInForce::Fok,
5017            false,
5018            None,
5019            true,
5020        ));
5021        algo.core
5022            .cache_rc()
5023            .borrow_mut()
5024            .add_order(second_order.clone(), None, None, false)
5025            .unwrap();
5026        assert_eq!(
5027            algo.cache().order(&client_order_id).unwrap().quantity(),
5028            Quantity::from("0.2"),
5029        );
5030
5031        // The late fill exceeds the primary's remaining quantity: the
5032        // re-deduction caps at zero and the shortfall becomes debt
5033        fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("0.5"));
5034        assert_eq!(
5035            algo.cache().order(&client_order_id).unwrap().quantity(),
5036            Quantity::from("0.0"),
5037        );
5038        assert_eq!(
5039            algo.core
5040                .spawn_reduction(spawned_order.client_order_id())
5041                .unwrap()
5042                .restored_qty,
5043            Some(Quantity::from("0.0")),
5044        );
5045        assert_eq!(
5046            algo.core.spawn_fill_debt(client_order_id),
5047            Some(Quantity::from("0.3")),
5048        );
5049
5050        // The second spawn terminating unfilled discharges the debt before
5051        // returning quantity: 0.8 restores only 0.5
5052        let rejected = OrderRejectedSpec::builder()
5053            .trader_id(second_order.trader_id())
5054            .strategy_id(second_order.strategy_id())
5055            .instrument_id(second_order.instrument_id())
5056            .client_order_id(second_order.client_order_id())
5057            .account_id(AccountId::from("BINANCE-001"))
5058            .reason("TEST_REJECTION".into())
5059            .build();
5060        algo.handle_order_event(OrderEventAny::Rejected(rejected));
5061
5062        let final_primary = algo.cache().order(&client_order_id).unwrap();
5063        assert_eq!(final_primary.quantity(), Quantity::from("0.5"));
5064        assert!(algo.core.spawn_fill_debt(client_order_id).is_none());
5065        assert_eq!(
5066            spawned_order.filled_qty() + final_primary.quantity(),
5067            Quantity::from("1.0"),
5068        );
5069    }
5070
5071    #[rstest]
5072    fn test_late_fill_on_child_that_fully_discharged_debt_recreates_debt() {
5073        let (mut algo, client_order_id, mut spawned_order) = setup_accepted_spawn();
5074        cancel_spawned_order(&mut algo, &mut spawned_order);
5075
5076        let mut child_b = spawn_reduced_child(&mut algo, client_order_id, Quantity::from("0.3"));
5077        let mut child_c = spawn_reduced_child(&mut algo, client_order_id, Quantity::from("0.5"));
5078        accept_spawned_order(&mut algo, &mut child_b);
5079        accept_spawned_order(&mut algo, &mut child_c);
5080        assert_eq!(
5081            algo.cache().order(&client_order_id).unwrap().quantity(),
5082            Quantity::from("0.2"),
5083        );
5084
5085        fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("0.5"));
5086        assert_eq!(
5087            algo.cache().order(&client_order_id).unwrap().quantity(),
5088            Quantity::from("0.0"),
5089        );
5090        assert_eq!(
5091            algo.core.spawn_fill_debt(client_order_id),
5092            Some(Quantity::from("0.3")),
5093        );
5094
5095        // B's cancellation fully discharges the debt; its record keeps the
5096        // gross released amount for late-fill tracking
5097        cancel_spawned_order(&mut algo, &mut child_b);
5098        assert_eq!(
5099            algo.cache().order(&client_order_id).unwrap().quantity(),
5100            Quantity::from("0.0"),
5101        );
5102        assert!(algo.core.spawn_fill_debt(client_order_id).is_none());
5103        assert_eq!(
5104            algo.core
5105                .spawn_reduction(child_b.client_order_id())
5106                .unwrap()
5107                .restored_qty
5108                .unwrap(),
5109            Quantity::from("0.3"),
5110        );
5111
5112        // B's late fill reverses the settlement for the filled amount
5113        fill_spawned_order(&mut algo, &mut child_b, Quantity::from("0.2"));
5114        assert_eq!(
5115            algo.cache().order(&client_order_id).unwrap().quantity(),
5116            Quantity::from("0.0"),
5117        );
5118        assert_eq!(
5119            algo.core.spawn_fill_debt(client_order_id),
5120            Some(Quantity::from("0.2")),
5121        );
5122
5123        cancel_spawned_order(&mut algo, &mut child_c);
5124        let final_primary = algo.cache().order(&client_order_id).unwrap();
5125        assert_eq!(final_primary.quantity(), Quantity::from("0.3"));
5126        assert!(algo.core.spawn_fill_debt(client_order_id).is_none());
5127        assert_eq!(
5128            spawned_order.filled_qty() + child_b.filled_qty() + final_primary.quantity(),
5129            Quantity::from("1.0"),
5130        );
5131    }
5132
5133    #[rstest]
5134    fn test_late_fill_on_child_after_partial_debt_discharge_nets_from_primary() {
5135        let (mut algo, client_order_id, mut spawned_order) = setup_accepted_spawn();
5136        cancel_spawned_order(&mut algo, &mut spawned_order);
5137
5138        let mut child_b = spawn_reduced_child(&mut algo, client_order_id, Quantity::from("0.8"));
5139        accept_spawned_order(&mut algo, &mut child_b);
5140        fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("0.5"));
5141        assert_eq!(
5142            algo.core.spawn_fill_debt(client_order_id),
5143            Some(Quantity::from("0.3")),
5144        );
5145
5146        // B's cancellation discharges 0.3 of debt and restores the net 0.5;
5147        // its record keeps the gross 0.8
5148        cancel_spawned_order(&mut algo, &mut child_b);
5149        assert_eq!(
5150            algo.cache().order(&client_order_id).unwrap().quantity(),
5151            Quantity::from("0.5"),
5152        );
5153        assert!(algo.core.spawn_fill_debt(client_order_id).is_none());
5154        assert_eq!(
5155            algo.core
5156                .spawn_reduction(child_b.client_order_id())
5157                .unwrap()
5158                .restored_qty
5159                .unwrap(),
5160            Quantity::from("0.8"),
5161        );
5162
5163        fill_spawned_order(&mut algo, &mut child_b, Quantity::from("0.3"));
5164        let final_primary = algo.cache().order(&client_order_id).unwrap();
5165        assert_eq!(final_primary.quantity(), Quantity::from("0.2"));
5166        assert_eq!(
5167            spawned_order.filled_qty() + child_b.filled_qty() + final_primary.quantity(),
5168            Quantity::from("1.0"),
5169        );
5170    }
5171
5172    #[rstest]
5173    fn test_emulated_spawn_refusal_restores_initialized_primary() {
5174        let (mut algo, client_order_id, _spawned_order) = setup_pending_spawn();
5175        let mut primary = algo.cache().order(&client_order_id).unwrap();
5176        let spawned = algo.spawn_limit(
5177            &mut primary,
5178            Quantity::from("0.3"),
5179            Price::from("50000.0"),
5180            TimeInForce::Gtc,
5181            None,
5182            false,
5183            false,
5184            None,
5185            Some(TriggerType::BidAsk),
5186            None,
5187            true,
5188        );
5189        assert_eq!(
5190            algo.cache().order(&client_order_id).unwrap().quantity(),
5191            Quantity::from("0.2"),
5192        );
5193
5194        let result = algo.submit_order(OrderAny::Limit(spawned), None, None);
5195
5196        assert!(result.is_err());
5197        assert_eq!(
5198            algo.cache().order(&client_order_id).unwrap().quantity(),
5199            Quantity::from("0.5"),
5200        );
5201    }
5202
5203    #[rstest]
5204    fn test_spawned_order_accepted_then_expired_restores_reduction() {
5205        let (mut algo, client_order_id, mut spawned_order) = setup_accepted_spawn();
5206
5207        expire_spawned_order(&mut algo, &mut spawned_order);
5208
5209        let final_primary = algo.cache().order(&client_order_id).unwrap();
5210        assert_eq!(final_primary.quantity(), Quantity::from("1.0"));
5211    }
5212
5213    #[rstest]
5214    fn test_partially_filled_spawned_order_canceled_restores_leaves_quantity() {
5215        let (mut algo, client_order_id, mut spawned_order) = setup_accepted_spawn();
5216        fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("0.2"));
5217
5218        cancel_spawned_order(&mut algo, &mut spawned_order);
5219
5220        let final_primary = algo.cache().order(&client_order_id).unwrap();
5221        assert_eq!(final_primary.quantity(), Quantity::from("0.8"));
5222    }
5223
5224    #[rstest]
5225    fn test_partially_filled_spawned_order_expired_restores_leaves_quantity() {
5226        let (mut algo, client_order_id, mut spawned_order) = setup_accepted_spawn();
5227        fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("0.2"));
5228
5229        expire_spawned_order(&mut algo, &mut spawned_order);
5230
5231        let final_primary = algo.cache().order(&client_order_id).unwrap();
5232        assert_eq!(final_primary.quantity(), Quantity::from("0.8"));
5233    }
5234
5235    #[rstest]
5236    fn test_fully_filled_spawned_order_consumes_reduction_without_restoration() {
5237        let (mut algo, client_order_id, mut spawned_order) = setup_accepted_spawn();
5238        let spawned_id = spawned_order.client_order_id();
5239
5240        fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("0.5"));
5241
5242        let final_primary = algo.cache().order(&client_order_id).unwrap();
5243        assert_eq!(final_primary.quantity(), Quantity::from("0.5"));
5244        assert!(algo.core.take_pending_spawn_reduction(spawned_id).is_none());
5245    }
5246
5247    #[rstest]
5248    #[case::denied(true)]
5249    #[case::rejected(false)]
5250    fn test_spawned_order_refusal_then_terminal_event_restores_only_once(#[case] denied: bool) {
5251        // A canceled event after a denial/rejection is not an applicable state
5252        // transition and the engine drops such races before publication; the
5253        // second event is dispatched directly to exercise the handler's own
5254        // idempotence (the accounted unfilled quantity is unchanged).
5255        let (mut algo, client_order_id, spawned_order) = setup_pending_spawn();
5256
5257        if denied {
5258            let event = OrderDeniedSpec::builder()
5259                .trader_id(spawned_order.trader_id())
5260                .strategy_id(spawned_order.strategy_id())
5261                .instrument_id(spawned_order.instrument_id())
5262                .client_order_id(spawned_order.client_order_id())
5263                .reason("TEST_DENIAL".into())
5264                .build();
5265            algo.core
5266                .cache_rc()
5267                .borrow_mut()
5268                .update_order(&OrderEventAny::Denied(event))
5269                .unwrap();
5270            algo.handle_order_event(OrderEventAny::Denied(event));
5271        } else {
5272            let event = OrderRejectedSpec::builder()
5273                .trader_id(spawned_order.trader_id())
5274                .strategy_id(spawned_order.strategy_id())
5275                .instrument_id(spawned_order.instrument_id())
5276                .client_order_id(spawned_order.client_order_id())
5277                .account_id(AccountId::from("BINANCE-001"))
5278                .reason("TEST_REJECTION".into())
5279                .build();
5280            algo.core
5281                .cache_rc()
5282                .borrow_mut()
5283                .update_order(&OrderEventAny::Rejected(event))
5284                .unwrap();
5285            algo.handle_order_event(OrderEventAny::Rejected(event));
5286        }
5287
5288        assert_eq!(
5289            algo.cache().order(&client_order_id).unwrap().quantity(),
5290            Quantity::from("1.0"),
5291        );
5292
5293        let canceled = OrderCanceledSpec::builder()
5294            .trader_id(spawned_order.trader_id())
5295            .strategy_id(spawned_order.strategy_id())
5296            .instrument_id(spawned_order.instrument_id())
5297            .client_order_id(spawned_order.client_order_id())
5298            .build();
5299        algo.handle_order_event(OrderEventAny::Canceled(canceled));
5300
5301        let final_primary = algo.cache().order(&client_order_id).unwrap();
5302        assert_eq!(final_primary.quantity(), Quantity::from("1.0"));
5303    }
5304
5305    #[rstest]
5306    #[should_panic(expected = "exceeds primary leaves_qty")]
5307    fn test_spawn_quantity_exceeds_leaves_qty_panics() {
5308        let mut algo = create_test_algorithm();
5309        register_algorithm(&mut algo);
5310
5311        let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
5312        let exec_algorithm_id = algo.id();
5313        let client_order_id = ClientOrderId::from("O-001");
5314
5315        let mut primary = OrderAny::Market(MarketOrder::new(
5316            TraderId::from("TRADER-001"),
5317            StrategyId::from("STRAT-001"),
5318            instrument_id,
5319            client_order_id,
5320            OrderSide::Buy,
5321            Quantity::from("1.0"),
5322            TimeInForce::Gtc,
5323            UUID4::new(),
5324            0.into(),
5325            false,
5326            false,
5327            None,
5328            None,
5329            None,
5330            None,
5331            Some(exec_algorithm_id),
5332            None,
5333            Some(client_order_id),
5334            None,
5335        ));
5336
5337        {
5338            let cache_rc = algo.core.cache_rc();
5339            let mut cache = cache_rc.borrow_mut();
5340            cache.add_order(primary.clone(), None, None, false).unwrap();
5341        }
5342
5343        let _ = algo.spawn_market(
5344            &mut primary,
5345            Quantity::from("0.8"),
5346            TimeInForce::Fok,
5347            false,
5348            None,
5349            true,
5350        );
5351
5352        assert_eq!(primary.quantity(), Quantity::from("0.2"));
5353        assert_eq!(primary.leaves_qty(), Quantity::from("0.2"));
5354
5355        // Should panic - spawning 0.5 when only 0.2 leaves_qty remains
5356        let _ = algo.spawn_market(
5357            &mut primary,
5358            Quantity::from("0.5"),
5359            TimeInForce::Fok,
5360            false,
5361            None,
5362            true,
5363        );
5364    }
5365}