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
38pub mod config;
39pub mod core;
40pub mod twap;
41
42pub use core::{ExecutionAlgorithmCore, StrategyEventHandlers};
43
44pub use config::ExecutionAlgorithmConfig;
45use nautilus_common::{
46    actor::{DataActor, registry::try_get_actor_unchecked},
47    enums::ComponentState,
48    logging::{CMD, EVT, RECV, SEND},
49    messages::execution::{CancelOrder, ModifyOrder, SubmitOrder, TradingCommand},
50    msgbus::{self, MessagingSwitchboard, TypedHandler},
51    timer::TimeEvent,
52};
53use nautilus_core::{UUID4, UnixNanos};
54use nautilus_model::{
55    enums::{OrderStatus, TimeInForce, TriggerType},
56    events::{
57        OrderAccepted, OrderCancelRejected, OrderCanceled, OrderDenied, OrderEmulated,
58        OrderEventAny, OrderExpired, OrderFilled, OrderInitialized, OrderModifyRejected,
59        OrderPendingCancel, OrderPendingUpdate, OrderRejected, OrderReleased, OrderSubmitted,
60        OrderTriggered, OrderUpdated, PositionChanged, PositionClosed, PositionEvent,
61        PositionOpened,
62    },
63    identifiers::{ClientId, ExecAlgorithmId, PositionId, StrategyId},
64    orders::{LimitOrder, MarketOrder, MarketToLimitOrder, Order, OrderAny, OrderList},
65    types::{Price, Quantity},
66};
67pub use twap::{TwapAlgorithm, TwapAlgorithmConfig};
68use ustr::Ustr;
69
70/// Core trait for implementing execution algorithms in NautilusTrader.
71///
72/// Execution algorithms are specialized [`DataActor`]s that receive orders from strategies
73/// and execute them by spawning child orders. They are used for order slicing algorithms
74/// like TWAP and VWAP.
75///
76/// # Key Capabilities
77///
78/// - All [`DataActor`] capabilities (data subscriptions, event handling, timers)
79/// - Order spawning (market, limit, market-to-limit)
80/// - Order lifecycle management (submit, modify, cancel)
81/// - Event filtering for algorithm-owned orders
82///
83/// # Implementation
84///
85/// User algorithms should implement the required methods and hold an
86/// [`ExecutionAlgorithmCore`] member. The struct should `Deref` and `DerefMut`
87/// to `ExecutionAlgorithmCore` (which itself derefs to `DataActorCore`).
88pub trait ExecutionAlgorithm: DataActor {
89    /// Provides mutable access to the internal `ExecutionAlgorithmCore`.
90    fn core_mut(&mut self) -> &mut ExecutionAlgorithmCore;
91
92    /// Returns the execution algorithm ID.
93    fn id(&mut self) -> ExecAlgorithmId {
94        self.core_mut().exec_algorithm_id
95    }
96
97    /// Executes a trading command.
98    ///
99    /// This is the main entry point for commands routed to the algorithm.
100    /// Dispatches to the appropriate handler based on command type.
101    ///
102    /// Commands are only processed when the algorithm is in `Running` state.
103    ///
104    /// # Errors
105    ///
106    /// Returns an error if command handling fails.
107    fn execute(&mut self, command: TradingCommand) -> anyhow::Result<()>
108    where
109        Self: 'static + std::fmt::Debug + Sized,
110    {
111        let core = self.core_mut();
112        if core.config.log_commands {
113            let id = &core.actor.actor_id;
114            log::info!("{id} {RECV}{CMD} {command:?}");
115        }
116
117        if core.state() != ComponentState::Running {
118            return Ok(());
119        }
120
121        match command {
122            TradingCommand::SubmitOrder(cmd) => {
123                self.subscribe_to_strategy_events(cmd.strategy_id);
124                let order = self.core_mut().get_order(&cmd.client_order_id)?;
125                self.on_order(order)
126            }
127            TradingCommand::SubmitOrderList(cmd) => {
128                self.subscribe_to_strategy_events(cmd.strategy_id);
129                let orders = self.core_mut().get_orders_for_list(&cmd.order_list)?;
130                self.on_order_list(cmd.order_list, orders)
131            }
132            TradingCommand::CancelOrder(cmd) => self.handle_cancel_order(cmd),
133            _ => {
134                log::warn!("Unhandled command type: {command:?}");
135                Ok(())
136            }
137        }
138    }
139
140    /// Called when a primary order is received for execution.
141    ///
142    /// Override this method to implement the algorithm's order slicing logic.
143    ///
144    /// # Errors
145    ///
146    /// Returns an error if order handling fails.
147    fn on_order(&mut self, order: OrderAny) -> anyhow::Result<()>;
148
149    /// Called when an order list is received for execution.
150    ///
151    /// Override this method to handle order lists. The default implementation
152    /// processes each order individually.
153    ///
154    /// # Errors
155    ///
156    /// Returns an error if order list handling fails.
157    fn on_order_list(
158        &mut self,
159        _order_list: OrderList,
160        orders: Vec<OrderAny>,
161    ) -> anyhow::Result<()> {
162        for order in orders {
163            self.on_order(order)?;
164        }
165        Ok(())
166    }
167
168    /// Handles a cancel order command for algorithm-managed orders.
169    ///
170    /// This generates an internal cancel event and publishes it. The order
171    /// is canceled locally without sending a command to the execution engine.
172    ///
173    /// # Errors
174    ///
175    /// Returns an error if cancellation fails.
176    fn handle_cancel_order(&mut self, command: CancelOrder) -> anyhow::Result<()> {
177        let (mut order, is_pending_cancel) = {
178            let cache = self.core_mut().cache();
179
180            let Some(order) = cache.order(&command.client_order_id) else {
181                log::warn!(
182                    "Cannot cancel order: {} not found in cache",
183                    command.client_order_id
184                );
185                return Ok(());
186            };
187
188            let is_pending = cache.is_order_pending_cancel_local(&command.client_order_id);
189            (order.clone(), is_pending)
190        };
191
192        if is_pending_cancel {
193            return Ok(());
194        }
195
196        if order.is_closed() {
197            log::warn!("Order already closed for {command:?}");
198            return Ok(());
199        }
200
201        let event = self.generate_order_canceled(&order);
202
203        if let Err(e) = order.apply(OrderEventAny::Canceled(event)) {
204            log::warn!("InvalidStateTrigger: {e}, did not apply cancel event");
205            return Ok(());
206        }
207
208        {
209            let cache_rc = self.core_mut().cache_rc();
210            let mut cache = cache_rc.borrow_mut();
211            cache.update_order(&order)?;
212        }
213
214        let topic = format!("events.order.{}", order.strategy_id());
215        msgbus::publish_order_event(topic.into(), &OrderEventAny::Canceled(event));
216
217        Ok(())
218    }
219
220    /// Generates an OrderCanceled event for an order.
221    fn generate_order_canceled(&mut self, order: &OrderAny) -> OrderCanceled {
222        let ts_now = self.core_mut().clock().timestamp_ns();
223
224        OrderCanceled::new(
225            order.trader_id(),
226            order.strategy_id(),
227            order.instrument_id(),
228            order.client_order_id(),
229            UUID4::new(),
230            ts_now,
231            ts_now,
232            false, // reconciliation
233            order.venue_order_id(),
234            order.account_id(),
235        )
236    }
237
238    /// Generates an OrderPendingUpdate event for an order.
239    fn generate_order_pending_update(&mut self, order: &OrderAny) -> OrderPendingUpdate {
240        let ts_now = self.core_mut().clock().timestamp_ns();
241
242        OrderPendingUpdate::new(
243            order.trader_id(),
244            order.strategy_id(),
245            order.instrument_id(),
246            order.client_order_id(),
247            order
248                .account_id()
249                .expect("Order must have account_id for pending update"),
250            UUID4::new(),
251            ts_now,
252            ts_now,
253            false, // reconciliation
254            order.venue_order_id(),
255        )
256    }
257
258    /// Generates an OrderPendingCancel event for an order.
259    fn generate_order_pending_cancel(&mut self, order: &OrderAny) -> OrderPendingCancel {
260        let ts_now = self.core_mut().clock().timestamp_ns();
261
262        OrderPendingCancel::new(
263            order.trader_id(),
264            order.strategy_id(),
265            order.instrument_id(),
266            order.client_order_id(),
267            order
268                .account_id()
269                .expect("Order must have account_id for pending cancel"),
270            UUID4::new(),
271            ts_now,
272            ts_now,
273            false, // reconciliation
274            order.venue_order_id(),
275        )
276    }
277
278    /// Spawns a market order from a primary order.
279    ///
280    /// Creates a new market order with:
281    /// - A unique client order ID: `{primary_id}-E{sequence}`.
282    /// - The primary order's trader ID, strategy ID, and instrument ID.
283    /// - The algorithm's exec_algorithm_id.
284    /// - exec_spawn_id set to the primary order's client order ID.
285    ///
286    /// If `reduce_primary` is true, the primary order's quantity will be reduced
287    /// by the spawned quantity. If the spawned order is subsequently denied or
288    /// rejected (before acceptance), the deducted quantity is automatically
289    /// restored to the primary order.
290    fn spawn_market(
291        &mut self,
292        primary: &mut OrderAny,
293        quantity: Quantity,
294        time_in_force: TimeInForce,
295        reduce_only: bool,
296        tags: Option<Vec<Ustr>>,
297        reduce_primary: bool,
298    ) -> MarketOrder {
299        // Generate spawn ID first so we can track the reduction
300        let core = self.core_mut();
301        let client_order_id = core.spawn_client_order_id(&primary.client_order_id());
302        let ts_init = core.clock().timestamp_ns();
303        let exec_algorithm_id = core.exec_algorithm_id;
304
305        if reduce_primary {
306            self.reduce_primary_order(primary, quantity);
307            self.core_mut()
308                .track_pending_spawn_reduction(client_order_id, quantity);
309        }
310
311        MarketOrder::new(
312            primary.trader_id(),
313            primary.strategy_id(),
314            primary.instrument_id(),
315            client_order_id,
316            primary.order_side(),
317            quantity,
318            time_in_force,
319            UUID4::new(),
320            ts_init,
321            reduce_only,
322            false, // quote_quantity
323            primary.contingency_type(),
324            primary.order_list_id(),
325            primary.linked_order_ids().map(|ids| ids.to_vec()),
326            primary.parent_order_id(),
327            Some(exec_algorithm_id),
328            primary.exec_algorithm_params().cloned(),
329            Some(primary.client_order_id()),
330            tags.or_else(|| primary.tags().map(|t| t.to_vec())),
331        )
332    }
333
334    /// Spawns a limit order from a primary order.
335    ///
336    /// Creates a new limit order with:
337    /// - A unique client order ID: `{primary_id}-E{sequence}`
338    /// - The primary order's trader ID, strategy ID, and instrument ID
339    /// - The algorithm's exec_algorithm_id
340    /// - exec_spawn_id set to the primary order's client order ID
341    ///
342    /// If `reduce_primary` is true, the primary order's quantity will be reduced
343    /// by the spawned quantity. If the spawned order is subsequently denied or
344    /// rejected (before acceptance), the deducted quantity is automatically
345    /// restored to the primary order.
346    #[allow(clippy::too_many_arguments)]
347    fn spawn_limit(
348        &mut self,
349        primary: &mut OrderAny,
350        quantity: Quantity,
351        price: Price,
352        time_in_force: TimeInForce,
353        expire_time: Option<UnixNanos>,
354        post_only: bool,
355        reduce_only: bool,
356        display_qty: Option<Quantity>,
357        emulation_trigger: Option<TriggerType>,
358        tags: Option<Vec<Ustr>>,
359        reduce_primary: bool,
360    ) -> LimitOrder {
361        // Generate spawn ID first so we can track the reduction
362        let core = self.core_mut();
363        let client_order_id = core.spawn_client_order_id(&primary.client_order_id());
364        let ts_init = core.clock().timestamp_ns();
365        let exec_algorithm_id = core.exec_algorithm_id;
366
367        if reduce_primary {
368            self.reduce_primary_order(primary, quantity);
369            self.core_mut()
370                .track_pending_spawn_reduction(client_order_id, quantity);
371        }
372
373        LimitOrder::new(
374            primary.trader_id(),
375            primary.strategy_id(),
376            primary.instrument_id(),
377            client_order_id,
378            primary.order_side(),
379            quantity,
380            price,
381            time_in_force,
382            expire_time,
383            post_only,
384            reduce_only,
385            false, // quote_quantity
386            display_qty,
387            emulation_trigger,
388            None, // trigger_instrument_id
389            primary.contingency_type(),
390            primary.order_list_id(),
391            primary.linked_order_ids().map(|ids| ids.to_vec()),
392            primary.parent_order_id(),
393            Some(exec_algorithm_id),
394            primary.exec_algorithm_params().cloned(),
395            Some(primary.client_order_id()),
396            tags.or_else(|| primary.tags().map(|t| t.to_vec())),
397            UUID4::new(),
398            ts_init,
399        )
400    }
401
402    /// Spawns a market-to-limit order from a primary order.
403    ///
404    /// Creates a new market-to-limit order with:
405    /// - A unique client order ID: `{primary_id}-E{sequence}`
406    /// - The primary order's trader ID, strategy ID, and instrument ID
407    /// - The algorithm's exec_algorithm_id
408    /// - exec_spawn_id set to the primary order's client order ID
409    ///
410    /// If `reduce_primary` is true, the primary order's quantity will be reduced
411    /// by the spawned quantity. If the spawned order is subsequently denied or
412    /// rejected (before acceptance), the deducted quantity is automatically
413    /// restored to the primary order.
414    #[allow(clippy::too_many_arguments)]
415    fn spawn_market_to_limit(
416        &mut self,
417        primary: &mut OrderAny,
418        quantity: Quantity,
419        time_in_force: TimeInForce,
420        expire_time: Option<UnixNanos>,
421        reduce_only: bool,
422        display_qty: Option<Quantity>,
423        emulation_trigger: Option<TriggerType>,
424        tags: Option<Vec<Ustr>>,
425        reduce_primary: bool,
426    ) -> MarketToLimitOrder {
427        // Generate spawn ID first so we can track the reduction
428        let core = self.core_mut();
429        let client_order_id = core.spawn_client_order_id(&primary.client_order_id());
430        let ts_init = core.clock().timestamp_ns();
431        let exec_algorithm_id = core.exec_algorithm_id;
432
433        if reduce_primary {
434            self.reduce_primary_order(primary, quantity);
435            self.core_mut()
436                .track_pending_spawn_reduction(client_order_id, quantity);
437        }
438
439        let mut order = MarketToLimitOrder::new(
440            primary.trader_id(),
441            primary.strategy_id(),
442            primary.instrument_id(),
443            client_order_id,
444            primary.order_side(),
445            quantity,
446            time_in_force,
447            expire_time,
448            false, // post_only
449            reduce_only,
450            false, // quote_quantity
451            display_qty,
452            primary.contingency_type(),
453            primary.order_list_id(),
454            primary.linked_order_ids().map(|ids| ids.to_vec()),
455            primary.parent_order_id(),
456            Some(exec_algorithm_id),
457            primary.exec_algorithm_params().cloned(),
458            Some(primary.client_order_id()),
459            tags.or_else(|| primary.tags().map(|t| t.to_vec())),
460            UUID4::new(),
461            ts_init,
462        );
463
464        if emulation_trigger.is_some() {
465            order.set_emulation_trigger(emulation_trigger);
466        }
467
468        order
469    }
470
471    /// Reduces the primary order's quantity by the spawn quantity.
472    ///
473    /// Generates an `OrderUpdated` event and applies it to the primary order,
474    /// then updates the order in the cache.
475    ///
476    /// # Panics
477    ///
478    /// Panics if `spawn_qty` exceeds the primary order's `leaves_qty`.
479    fn reduce_primary_order(&mut self, primary: &mut OrderAny, spawn_qty: Quantity) {
480        let leaves_qty = primary.leaves_qty();
481        assert!(
482            leaves_qty >= spawn_qty,
483            "Spawn quantity {spawn_qty} exceeds primary leaves_qty {leaves_qty}"
484        );
485
486        let primary_qty = primary.quantity();
487        let new_qty = Quantity::from_raw(primary_qty.raw - spawn_qty.raw, primary_qty.precision);
488
489        let core = self.core_mut();
490        let ts_now = core.clock().timestamp_ns();
491
492        let updated = OrderUpdated::new(
493            primary.trader_id(),
494            primary.strategy_id(),
495            primary.instrument_id(),
496            primary.client_order_id(),
497            new_qty,
498            UUID4::new(),
499            ts_now,
500            ts_now,
501            false, // reconciliation
502            primary.venue_order_id(),
503            primary.account_id(),
504            None, // price
505            None, // trigger_price
506            None, // protection_price
507        );
508
509        primary
510            .apply(OrderEventAny::Updated(updated))
511            .expect("Failed to apply OrderUpdated");
512
513        let cache_rc = core.cache_rc();
514        let mut cache = cache_rc.borrow_mut();
515        cache
516            .update_order(primary)
517            .expect("Failed to update order in cache");
518    }
519
520    /// Restores the primary order quantity after a spawned order is denied or rejected.
521    ///
522    /// This is called when a spawned order fails before acceptance. The quantity
523    /// that was deducted from the primary order is restored (up to the spawned
524    /// order's leaves_qty to handle partial fills).
525    fn restore_primary_order_quantity(&mut self, order: &OrderAny) {
526        let Some(exec_spawn_id) = order.exec_spawn_id() else {
527            return;
528        };
529
530        let reduction_qty = {
531            let core = self.core_mut();
532            core.take_pending_spawn_reduction(&order.client_order_id())
533        };
534
535        let Some(reduction_qty) = reduction_qty else {
536            return;
537        };
538
539        let primary = {
540            let cache = self.core_mut().cache();
541            cache.order(&exec_spawn_id).cloned()
542        };
543
544        let Some(mut primary) = primary else {
545            log::warn!(
546                "Cannot restore primary order quantity: primary order {exec_spawn_id} not found",
547            );
548            return;
549        };
550
551        // Cap restore amount by leaves_qty to handle partial fills before rejection
552        let restore_raw = std::cmp::min(reduction_qty.raw, order.leaves_qty().raw);
553        if restore_raw == 0 {
554            return;
555        }
556
557        let restored_qty = Quantity::from_raw(
558            primary.quantity().raw + restore_raw,
559            primary.quantity().precision,
560        );
561
562        let core = self.core_mut();
563        let ts_now = core.clock().timestamp_ns();
564
565        let updated = OrderUpdated::new(
566            primary.trader_id(),
567            primary.strategy_id(),
568            primary.instrument_id(),
569            primary.client_order_id(),
570            restored_qty,
571            UUID4::new(),
572            ts_now,
573            ts_now,
574            false, // reconciliation
575            primary.venue_order_id(),
576            primary.account_id(),
577            None, // price
578            None, // trigger_price
579            None, // protection_price
580        );
581
582        if let Err(e) = primary.apply(OrderEventAny::Updated(updated)) {
583            log::warn!("Failed to apply OrderUpdated for quantity restoration: {e}");
584            return;
585        }
586
587        {
588            let cache_rc = core.cache_rc();
589            let mut cache = cache_rc.borrow_mut();
590            if let Err(e) = cache.update_order(&primary) {
591                log::warn!("Failed to update primary order in cache: {e}");
592                return;
593            }
594        }
595
596        log::info!(
597            "Restored primary order {} quantity to {} after spawned order {} was denied/rejected",
598            primary.client_order_id(),
599            restored_qty,
600            order.client_order_id()
601        );
602    }
603
604    /// Submits an order to the execution engine via the risk engine.
605    ///
606    /// # Errors
607    ///
608    /// Returns an error if order submission fails.
609    fn submit_order(
610        &mut self,
611        order: OrderAny,
612        position_id: Option<PositionId>,
613        client_id: Option<ClientId>,
614    ) -> anyhow::Result<()> {
615        let core = self.core_mut();
616
617        let trader_id = core.trader_id().expect("Trader ID not set");
618        let ts_init = core.clock().timestamp_ns();
619
620        // For spawned orders, use the parent's strategy ID
621        let strategy_id = order.strategy_id();
622
623        {
624            let cache_rc = core.cache_rc();
625            let mut cache = cache_rc.borrow_mut();
626            cache.add_order(order.clone(), position_id, client_id, true)?;
627        }
628
629        let command = SubmitOrder::new(
630            trader_id,
631            client_id,
632            strategy_id,
633            order.instrument_id(),
634            order.client_order_id(),
635            order.init_event().clone(),
636            order.exec_algorithm_id(),
637            position_id,
638            None, // params
639            UUID4::new(),
640            ts_init,
641        );
642
643        if core.config.log_commands {
644            let id = &core.actor.actor_id;
645            log::info!("{id} {SEND}{CMD} {command:?}");
646        }
647
648        msgbus::send_trading_command(
649            MessagingSwitchboard::risk_engine_execute(),
650            TradingCommand::SubmitOrder(command),
651        );
652
653        Ok(())
654    }
655
656    /// Modifies an order.
657    ///
658    /// # Errors
659    ///
660    /// Returns an error if order modification fails.
661    fn modify_order(
662        &mut self,
663        order: &mut OrderAny,
664        quantity: Option<Quantity>,
665        price: Option<Price>,
666        trigger_price: Option<Price>,
667        client_id: Option<ClientId>,
668    ) -> anyhow::Result<()> {
669        let qty_changing = quantity.is_some_and(|q| q != order.quantity());
670        let price_changing = price.is_some() && price != order.price();
671        let trigger_changing = trigger_price.is_some() && trigger_price != order.trigger_price();
672
673        if !qty_changing && !price_changing && !trigger_changing {
674            log::error!(
675                "Cannot create command ModifyOrder: \
676                quantity, price and trigger were either None \
677                or the same as existing values"
678            );
679            return Ok(());
680        }
681
682        if order.is_closed() || order.is_pending_cancel() {
683            log::warn!(
684                "Cannot create command ModifyOrder: state is {:?}, {order:?}",
685                order.status()
686            );
687            return Ok(());
688        }
689
690        let core = self.core_mut();
691        let trader_id = core.trader_id().expect("Trader ID not set");
692        let strategy_id = order.strategy_id();
693
694        if !order.is_active_local() {
695            let event = self.generate_order_pending_update(order);
696            if let Err(e) = order.apply(OrderEventAny::PendingUpdate(event)) {
697                log::warn!("InvalidStateTrigger: {e}, did not apply pending update event");
698                return Ok(());
699            }
700
701            {
702                let cache_rc = self.core_mut().cache_rc();
703                let mut cache = cache_rc.borrow_mut();
704                cache.update_order(order).ok();
705            }
706
707            let topic = format!("events.order.{strategy_id}");
708            msgbus::publish_order_event(topic.into(), &OrderEventAny::PendingUpdate(event));
709        }
710
711        let ts_init = self.core_mut().clock().timestamp_ns();
712        let command = ModifyOrder::new(
713            trader_id,
714            client_id,
715            strategy_id,
716            order.instrument_id(),
717            order.client_order_id(),
718            order.venue_order_id(),
719            quantity,
720            price,
721            trigger_price,
722            UUID4::new(),
723            ts_init,
724            None, // params
725        );
726
727        if self.core_mut().config.log_commands {
728            let id = &self.core_mut().actor.actor_id;
729            log::info!("{id} {SEND}{CMD} {command:?}");
730        }
731
732        let has_emulation_trigger = order
733            .emulation_trigger()
734            .is_some_and(|t| t != TriggerType::NoTrigger);
735
736        if order.is_emulated() || has_emulation_trigger {
737            msgbus::send_trading_command(
738                MessagingSwitchboard::order_emulator_execute(),
739                TradingCommand::ModifyOrder(command),
740            );
741        } else {
742            msgbus::send_trading_command(
743                MessagingSwitchboard::risk_engine_execute(),
744                TradingCommand::ModifyOrder(command),
745            );
746        }
747
748        Ok(())
749    }
750
751    /// Modifies an INITIALIZED or RELEASED order in place without sending a command.
752    ///
753    /// This is useful for adjusting order parameters before submission. The order
754    /// is updated locally by applying an `OrderUpdated` event and updating the cache.
755    ///
756    /// At least one parameter must differ from the current order values.
757    ///
758    /// # Errors
759    ///
760    /// Returns an error if the order status is not INITIALIZED or RELEASED,
761    /// or if no parameters would change.
762    fn modify_order_in_place(
763        &mut self,
764        order: &mut OrderAny,
765        quantity: Option<Quantity>,
766        price: Option<Price>,
767        trigger_price: Option<Price>,
768    ) -> anyhow::Result<()> {
769        // Validate order status
770        let status = order.status();
771        if status != OrderStatus::Initialized && status != OrderStatus::Released {
772            anyhow::bail!(
773                "Cannot modify order in place: status is {status:?}, expected INITIALIZED or RELEASED"
774            );
775        }
776
777        // Validate order type compatibility
778        if price.is_some() && order.price().is_none() {
779            anyhow::bail!(
780                "Cannot modify order in place: {} orders do not have a LIMIT price",
781                order.order_type()
782            );
783        }
784
785        if trigger_price.is_some() && order.trigger_price().is_none() {
786            anyhow::bail!(
787                "Cannot modify order in place: {} orders do not have a STOP trigger price",
788                order.order_type()
789            );
790        }
791
792        // Check if any value would actually change
793        let qty_changing = quantity.is_some_and(|q| q != order.quantity());
794        let price_changing = price.is_some() && price != order.price();
795        let trigger_changing = trigger_price.is_some() && trigger_price != order.trigger_price();
796
797        if !qty_changing && !price_changing && !trigger_changing {
798            anyhow::bail!("Cannot modify order in place: no parameters differ from current values");
799        }
800
801        let core = self.core_mut();
802        let ts_now = core.clock().timestamp_ns();
803
804        let updated = OrderUpdated::new(
805            order.trader_id(),
806            order.strategy_id(),
807            order.instrument_id(),
808            order.client_order_id(),
809            quantity.unwrap_or_else(|| order.quantity()),
810            UUID4::new(),
811            ts_now,
812            ts_now,
813            false, // reconciliation
814            order.venue_order_id(),
815            order.account_id(),
816            price,
817            trigger_price,
818            None, // protection_price
819        );
820
821        order
822            .apply(OrderEventAny::Updated(updated))
823            .map_err(|e| anyhow::anyhow!("Failed to apply OrderUpdated: {e}"))?;
824
825        let cache_rc = core.cache_rc();
826        let mut cache = cache_rc.borrow_mut();
827        cache.update_order(order)?;
828
829        Ok(())
830    }
831
832    /// Cancels an order.
833    ///
834    /// # Errors
835    ///
836    /// Returns an error if order cancellation fails.
837    fn cancel_order(
838        &mut self,
839        order: &mut OrderAny,
840        client_id: Option<ClientId>,
841    ) -> anyhow::Result<()> {
842        if order.is_closed() || order.is_pending_cancel() {
843            log::warn!(
844                "Cannot cancel order: state is {:?}, {order:?}",
845                order.status()
846            );
847            return Ok(());
848        }
849
850        let core = self.core_mut();
851        let trader_id = core.trader_id().expect("Trader ID not set");
852        let strategy_id = order.strategy_id();
853
854        if !order.is_active_local() {
855            let event = self.generate_order_pending_cancel(order);
856            if let Err(e) = order.apply(OrderEventAny::PendingCancel(event)) {
857                log::warn!("InvalidStateTrigger: {e}, did not apply pending cancel event");
858                return Ok(());
859            }
860
861            {
862                let cache_rc = self.core_mut().cache_rc();
863                let mut cache = cache_rc.borrow_mut();
864                cache.update_order(order).ok();
865            }
866
867            let topic = format!("events.order.{strategy_id}");
868            msgbus::publish_order_event(topic.into(), &OrderEventAny::PendingCancel(event));
869        }
870
871        let ts_init = self.core_mut().clock().timestamp_ns();
872        let command = CancelOrder::new(
873            trader_id,
874            client_id,
875            strategy_id,
876            order.instrument_id(),
877            order.client_order_id(),
878            order.venue_order_id(),
879            UUID4::new(),
880            ts_init,
881            None, // params
882        );
883
884        if self.core_mut().config.log_commands {
885            let id = &self.core_mut().actor.actor_id;
886            log::info!("{id} {SEND}{CMD} {command:?}");
887        }
888
889        let has_emulation_trigger = order
890            .emulation_trigger()
891            .is_some_and(|t| t != TriggerType::NoTrigger);
892
893        if order.is_emulated() || order.status() == OrderStatus::Released || has_emulation_trigger {
894            msgbus::send_trading_command(
895                MessagingSwitchboard::order_emulator_execute(),
896                TradingCommand::CancelOrder(command),
897            );
898        } else {
899            msgbus::send_trading_command(
900                MessagingSwitchboard::exec_engine_execute(),
901                TradingCommand::CancelOrder(command),
902            );
903        }
904
905        Ok(())
906    }
907
908    /// Subscribes to events from a strategy.
909    ///
910    /// This is called automatically when the first order is received from a strategy.
911    fn subscribe_to_strategy_events(&mut self, strategy_id: StrategyId)
912    where
913        Self: 'static + std::fmt::Debug + Sized,
914    {
915        let core = self.core_mut();
916        if core.is_strategy_subscribed(&strategy_id) {
917            return;
918        }
919
920        let actor_id = core.actor.actor_id.inner();
921
922        let order_topic = format!("events.order.{strategy_id}");
923        let order_actor_id = actor_id;
924        let order_handler = TypedHandler::from(move |event: &OrderEventAny| {
925            if let Some(mut algo) = try_get_actor_unchecked::<Self>(&order_actor_id) {
926                algo.handle_order_event(event.clone());
927            } else {
928                log::error!(
929                    "ExecutionAlgorithm {order_actor_id} not found for order event handling"
930                );
931            }
932        });
933        msgbus::subscribe_order_events(order_topic.clone().into(), order_handler.clone(), None);
934
935        let position_topic = format!("events.position.{strategy_id}");
936        let position_handler = TypedHandler::from(move |event: &PositionEvent| {
937            if let Some(mut algo) = try_get_actor_unchecked::<Self>(&actor_id) {
938                algo.handle_position_event(event.clone());
939            } else {
940                log::error!("ExecutionAlgorithm {actor_id} not found for position event handling");
941            }
942        });
943        msgbus::subscribe_position_events(
944            position_topic.clone().into(),
945            position_handler.clone(),
946            None,
947        );
948
949        let handlers = StrategyEventHandlers {
950            order_topic,
951            order_handler,
952            position_topic,
953            position_handler,
954        };
955        core.store_strategy_event_handlers(strategy_id, handlers);
956
957        core.add_subscribed_strategy(strategy_id);
958        log::info!("Subscribed to events for strategy {strategy_id}");
959    }
960
961    /// Unsubscribes from all strategy event handlers.
962    ///
963    /// This should be called before reset to properly clean up msgbus subscriptions.
964    fn unsubscribe_all_strategy_events(&mut self) {
965        let handlers = self.core_mut().take_strategy_event_handlers();
966        for (strategy_id, h) in handlers {
967            msgbus::unsubscribe_order_events(h.order_topic.into(), &h.order_handler);
968            msgbus::unsubscribe_position_events(h.position_topic.into(), &h.position_handler);
969            log::info!("Unsubscribed from events for strategy {strategy_id}");
970        }
971        self.core_mut().clear_subscribed_strategies();
972    }
973
974    /// Handles an order event, filtering for algorithm-owned orders.
975    fn handle_order_event(&mut self, event: OrderEventAny) {
976        if self.core_mut().state() != ComponentState::Running {
977            return;
978        }
979
980        let order = {
981            let cache = self.core_mut().cache();
982            cache.order(&event.client_order_id()).cloned()
983        };
984
985        let Some(order) = order else {
986            return;
987        };
988
989        let Some(order_algo_id) = order.exec_algorithm_id() else {
990            return;
991        };
992
993        if order_algo_id != self.id() {
994            return;
995        }
996
997        {
998            let core = self.core_mut();
999            if core.config.log_events {
1000                let id = &core.actor.actor_id;
1001                log::info!("{id} {RECV}{EVT} {event}");
1002            }
1003        }
1004
1005        match &event {
1006            OrderEventAny::Initialized(e) => self.on_order_initialized(e.clone()),
1007            OrderEventAny::Denied(e) => {
1008                self.restore_primary_order_quantity(&order);
1009                self.on_order_denied(*e);
1010            }
1011            OrderEventAny::Emulated(e) => self.on_order_emulated(*e),
1012            OrderEventAny::Released(e) => self.on_order_released(*e),
1013            OrderEventAny::Submitted(e) => self.on_order_submitted(*e),
1014            OrderEventAny::Rejected(e) => {
1015                self.restore_primary_order_quantity(&order);
1016                self.on_order_rejected(*e);
1017            }
1018            OrderEventAny::Accepted(e) => {
1019                // Commit reduction - order accepted by venue
1020                self.core_mut()
1021                    .take_pending_spawn_reduction(&order.client_order_id());
1022                self.on_order_accepted(*e);
1023            }
1024            OrderEventAny::Canceled(e) => {
1025                self.core_mut()
1026                    .take_pending_spawn_reduction(&order.client_order_id());
1027                self.on_algo_order_canceled(*e);
1028            }
1029            OrderEventAny::Expired(e) => {
1030                self.core_mut()
1031                    .take_pending_spawn_reduction(&order.client_order_id());
1032                self.on_order_expired(*e);
1033            }
1034            OrderEventAny::Triggered(e) => self.on_order_triggered(*e),
1035            OrderEventAny::PendingUpdate(e) => self.on_order_pending_update(*e),
1036            OrderEventAny::PendingCancel(e) => self.on_order_pending_cancel(*e),
1037            OrderEventAny::ModifyRejected(e) => self.on_order_modify_rejected(*e),
1038            OrderEventAny::CancelRejected(e) => self.on_order_cancel_rejected(*e),
1039            OrderEventAny::Updated(e) => self.on_order_updated(*e),
1040            OrderEventAny::Filled(e) => self.on_algo_order_filled(*e),
1041        }
1042
1043        self.on_order_event(event);
1044    }
1045
1046    /// Handles a position event.
1047    fn handle_position_event(&mut self, event: PositionEvent) {
1048        if self.core_mut().state() != ComponentState::Running {
1049            return;
1050        }
1051
1052        {
1053            let core = self.core_mut();
1054            if core.config.log_events {
1055                let id = &core.actor.actor_id;
1056                log::info!("{id} {RECV}{EVT} {event:?}");
1057            }
1058        }
1059
1060        match &event {
1061            PositionEvent::PositionOpened(e) => self.on_position_opened(e.clone()),
1062            PositionEvent::PositionChanged(e) => self.on_position_changed(e.clone()),
1063            PositionEvent::PositionClosed(e) => self.on_position_closed(e.clone()),
1064            PositionEvent::PositionAdjusted(_) => {}
1065        }
1066
1067        self.on_position_event(event);
1068    }
1069
1070    /// Called when the algorithm is started.
1071    ///
1072    /// Override this method to implement custom initialization logic.
1073    ///
1074    /// # Errors
1075    ///
1076    /// Returns an error if start fails.
1077    fn on_start(&mut self) -> anyhow::Result<()> {
1078        let id = self.id();
1079        log::info!("Starting {id}");
1080        Ok(())
1081    }
1082
1083    /// Called when the algorithm is stopped.
1084    ///
1085    /// # Errors
1086    ///
1087    /// Returns an error if stop fails.
1088    fn on_stop(&mut self) -> anyhow::Result<()> {
1089        Ok(())
1090    }
1091
1092    /// Called when the algorithm is reset.
1093    ///
1094    /// # Errors
1095    ///
1096    /// Returns an error if reset fails.
1097    fn on_reset(&mut self) -> anyhow::Result<()> {
1098        self.unsubscribe_all_strategy_events();
1099        self.core_mut().reset();
1100        Ok(())
1101    }
1102
1103    /// Called when a time event is received.
1104    ///
1105    /// Override this method for timer-based algorithms like TWAP.
1106    ///
1107    /// # Errors
1108    ///
1109    /// Returns an error if time event handling fails.
1110    fn on_time_event(&mut self, _event: &TimeEvent) -> anyhow::Result<()> {
1111        Ok(())
1112    }
1113
1114    /// Called when an order is initialized.
1115    #[allow(unused_variables)]
1116    fn on_order_initialized(&mut self, event: OrderInitialized) {}
1117
1118    /// Called when an order is denied.
1119    #[allow(unused_variables)]
1120    fn on_order_denied(&mut self, event: OrderDenied) {}
1121
1122    /// Called when an order is emulated.
1123    #[allow(unused_variables)]
1124    fn on_order_emulated(&mut self, event: OrderEmulated) {}
1125
1126    /// Called when an order is released from emulation.
1127    #[allow(unused_variables)]
1128    fn on_order_released(&mut self, event: OrderReleased) {}
1129
1130    /// Called when an order is submitted.
1131    #[allow(unused_variables)]
1132    fn on_order_submitted(&mut self, event: OrderSubmitted) {}
1133
1134    /// Called when an order is rejected.
1135    #[allow(unused_variables)]
1136    fn on_order_rejected(&mut self, event: OrderRejected) {}
1137
1138    /// Called when an order is accepted.
1139    #[allow(unused_variables)]
1140    fn on_order_accepted(&mut self, event: OrderAccepted) {}
1141
1142    /// Called when an order is canceled.
1143    #[allow(unused_variables)]
1144    fn on_algo_order_canceled(&mut self, event: OrderCanceled) {}
1145
1146    /// Called when an order expires.
1147    #[allow(unused_variables)]
1148    fn on_order_expired(&mut self, event: OrderExpired) {}
1149
1150    /// Called when an order is triggered.
1151    #[allow(unused_variables)]
1152    fn on_order_triggered(&mut self, event: OrderTriggered) {}
1153
1154    /// Called when an order modification is pending.
1155    #[allow(unused_variables)]
1156    fn on_order_pending_update(&mut self, event: OrderPendingUpdate) {}
1157
1158    /// Called when an order cancellation is pending.
1159    #[allow(unused_variables)]
1160    fn on_order_pending_cancel(&mut self, event: OrderPendingCancel) {}
1161
1162    /// Called when an order modification is rejected.
1163    #[allow(unused_variables)]
1164    fn on_order_modify_rejected(&mut self, event: OrderModifyRejected) {}
1165
1166    /// Called when an order cancellation is rejected.
1167    #[allow(unused_variables)]
1168    fn on_order_cancel_rejected(&mut self, event: OrderCancelRejected) {}
1169
1170    /// Called when an order is updated.
1171    #[allow(unused_variables)]
1172    fn on_order_updated(&mut self, event: OrderUpdated) {}
1173
1174    /// Called when an order is filled.
1175    #[allow(unused_variables)]
1176    fn on_algo_order_filled(&mut self, event: OrderFilled) {}
1177
1178    /// Called for any order event (after specific handler).
1179    #[allow(unused_variables)]
1180    fn on_order_event(&mut self, event: OrderEventAny) {}
1181
1182    /// Called when a position is opened.
1183    #[allow(unused_variables)]
1184    fn on_position_opened(&mut self, event: PositionOpened) {}
1185
1186    /// Called when a position is changed.
1187    #[allow(unused_variables)]
1188    fn on_position_changed(&mut self, event: PositionChanged) {}
1189
1190    /// Called when a position is closed.
1191    #[allow(unused_variables)]
1192    fn on_position_closed(&mut self, event: PositionClosed) {}
1193
1194    /// Called for any position event (after specific handler).
1195    #[allow(unused_variables)]
1196    fn on_position_event(&mut self, event: PositionEvent) {}
1197}
1198
1199#[cfg(test)]
1200mod tests {
1201    use std::{
1202        cell::RefCell,
1203        ops::{Deref, DerefMut},
1204        rc::Rc,
1205    };
1206
1207    use nautilus_common::{
1208        actor::{DataActor, DataActorCore},
1209        cache::Cache,
1210        clock::TestClock,
1211        component::Component,
1212        enums::ComponentTrigger,
1213    };
1214    use nautilus_model::{
1215        enums::OrderSide,
1216        events::{OrderAccepted, OrderCanceled, OrderDenied, OrderRejected},
1217        identifiers::{
1218            AccountId, ClientOrderId, ExecAlgorithmId, InstrumentId, StrategyId, TraderId,
1219            VenueOrderId,
1220        },
1221        orders::{LimitOrder, MarketOrder, OrderAny, stubs::TestOrderStubs},
1222        types::{Price, Quantity},
1223    };
1224    use rstest::rstest;
1225
1226    use super::*;
1227
1228    #[derive(Debug)]
1229    struct TestAlgorithm {
1230        core: ExecutionAlgorithmCore,
1231        on_order_called: bool,
1232        last_order_client_id: Option<ClientOrderId>,
1233    }
1234
1235    impl TestAlgorithm {
1236        fn new(config: ExecutionAlgorithmConfig) -> Self {
1237            Self {
1238                core: ExecutionAlgorithmCore::new(config),
1239                on_order_called: false,
1240                last_order_client_id: None,
1241            }
1242        }
1243    }
1244
1245    impl Deref for TestAlgorithm {
1246        type Target = DataActorCore;
1247        fn deref(&self) -> &Self::Target {
1248            &self.core.actor
1249        }
1250    }
1251
1252    impl DerefMut for TestAlgorithm {
1253        fn deref_mut(&mut self) -> &mut Self::Target {
1254            &mut self.core.actor
1255        }
1256    }
1257
1258    impl DataActor for TestAlgorithm {}
1259
1260    impl ExecutionAlgorithm for TestAlgorithm {
1261        fn core_mut(&mut self) -> &mut ExecutionAlgorithmCore {
1262            &mut self.core
1263        }
1264
1265        fn on_order(&mut self, order: OrderAny) -> anyhow::Result<()> {
1266            self.on_order_called = true;
1267            self.last_order_client_id = Some(order.client_order_id());
1268            Ok(())
1269        }
1270    }
1271
1272    fn create_test_algorithm() -> TestAlgorithm {
1273        // Use unique ID to avoid thread-local registry/msgbus conflicts in parallel tests
1274        let unique_id = format!("TEST-{}", UUID4::new());
1275        let config = ExecutionAlgorithmConfig {
1276            exec_algorithm_id: Some(ExecAlgorithmId::new(&unique_id)),
1277            ..Default::default()
1278        };
1279        TestAlgorithm::new(config)
1280    }
1281
1282    fn register_algorithm(algo: &mut TestAlgorithm) {
1283        let trader_id = TraderId::from("TRADER-001");
1284        let clock = Rc::new(RefCell::new(TestClock::new()));
1285        let cache = Rc::new(RefCell::new(Cache::default()));
1286
1287        algo.core.register(trader_id, clock, cache).unwrap();
1288
1289        // Transition to Running state for tests
1290        algo.transition_state(ComponentTrigger::Initialize).unwrap();
1291        algo.transition_state(ComponentTrigger::Start).unwrap();
1292        algo.transition_state(ComponentTrigger::StartCompleted)
1293            .unwrap();
1294    }
1295
1296    #[rstest]
1297    fn test_algorithm_creation() {
1298        let algo = create_test_algorithm();
1299        assert!(algo.core.exec_algorithm_id.inner().starts_with("TEST-"));
1300        assert!(!algo.on_order_called);
1301        assert!(algo.last_order_client_id.is_none());
1302    }
1303
1304    #[rstest]
1305    fn test_algorithm_registration() {
1306        let mut algo = create_test_algorithm();
1307        register_algorithm(&mut algo);
1308
1309        assert!(algo.core.trader_id().is_some());
1310        assert_eq!(algo.core.trader_id(), Some(TraderId::from("TRADER-001")));
1311    }
1312
1313    #[rstest]
1314    fn test_algorithm_id() {
1315        let mut algo = create_test_algorithm();
1316        assert!(algo.id().inner().starts_with("TEST-"));
1317    }
1318
1319    #[rstest]
1320    fn test_algorithm_spawn_market_creates_valid_order() {
1321        let mut algo = create_test_algorithm();
1322        register_algorithm(&mut algo);
1323
1324        let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
1325        let mut primary = OrderAny::Market(MarketOrder::new(
1326            TraderId::from("TRADER-001"),
1327            StrategyId::from("STRAT-001"),
1328            instrument_id,
1329            ClientOrderId::from("O-001"),
1330            OrderSide::Buy,
1331            Quantity::from("1.0"),
1332            TimeInForce::Gtc,
1333            UUID4::new(),
1334            0.into(),
1335            false, // reduce_only
1336            false, // quote_quantity
1337            None,  // contingency_type
1338            None,  // order_list_id
1339            None,  // linked_order_ids
1340            None,  // parent_order_id
1341            None,  // exec_algorithm_id
1342            None,  // exec_algorithm_params
1343            None,  // exec_spawn_id
1344            None,  // tags
1345        ));
1346
1347        let spawned = algo.spawn_market(
1348            &mut primary,
1349            Quantity::from("0.5"),
1350            TimeInForce::Ioc,
1351            false,
1352            None,  // tags
1353            false, // reduce_primary
1354        );
1355
1356        assert_eq!(spawned.client_order_id.as_str(), "O-001-E1");
1357        assert_eq!(spawned.instrument_id, instrument_id);
1358        assert_eq!(spawned.order_side(), OrderSide::Buy);
1359        assert_eq!(spawned.quantity, Quantity::from("0.5"));
1360        assert_eq!(spawned.time_in_force, TimeInForce::Ioc);
1361        assert_eq!(spawned.exec_algorithm_id, Some(algo.id()));
1362        assert_eq!(spawned.exec_spawn_id, Some(ClientOrderId::from("O-001")));
1363    }
1364
1365    #[rstest]
1366    fn test_algorithm_spawn_increments_sequence() {
1367        let mut algo = create_test_algorithm();
1368        register_algorithm(&mut algo);
1369
1370        let mut primary = OrderAny::Market(MarketOrder::new(
1371            TraderId::from("TRADER-001"),
1372            StrategyId::from("STRAT-001"),
1373            InstrumentId::from("BTC/USDT.BINANCE"),
1374            ClientOrderId::from("O-001"),
1375            OrderSide::Buy,
1376            Quantity::from("1.0"),
1377            TimeInForce::Gtc,
1378            UUID4::new(),
1379            0.into(),
1380            false,
1381            false,
1382            None,
1383            None,
1384            None,
1385            None,
1386            None,
1387            None,
1388            None,
1389            None,
1390        ));
1391
1392        let spawned1 = algo.spawn_market(
1393            &mut primary,
1394            Quantity::from("0.25"),
1395            TimeInForce::Ioc,
1396            false,
1397            None,
1398            false,
1399        );
1400        let spawned2 = algo.spawn_market(
1401            &mut primary,
1402            Quantity::from("0.25"),
1403            TimeInForce::Ioc,
1404            false,
1405            None,
1406            false,
1407        );
1408        let spawned3 = algo.spawn_market(
1409            &mut primary,
1410            Quantity::from("0.25"),
1411            TimeInForce::Ioc,
1412            false,
1413            None,
1414            false,
1415        );
1416
1417        assert_eq!(spawned1.client_order_id.as_str(), "O-001-E1");
1418        assert_eq!(spawned2.client_order_id.as_str(), "O-001-E2");
1419        assert_eq!(spawned3.client_order_id.as_str(), "O-001-E3");
1420    }
1421
1422    #[rstest]
1423    fn test_algorithm_default_handlers_do_not_panic() {
1424        let mut algo = create_test_algorithm();
1425
1426        algo.on_order_initialized(Default::default());
1427        algo.on_order_denied(Default::default());
1428        algo.on_order_emulated(Default::default());
1429        algo.on_order_released(Default::default());
1430        algo.on_order_submitted(Default::default());
1431        algo.on_order_rejected(Default::default());
1432        algo.on_order_accepted(Default::default());
1433        algo.on_algo_order_canceled(Default::default());
1434        algo.on_order_expired(Default::default());
1435        algo.on_order_triggered(Default::default());
1436        algo.on_order_pending_update(Default::default());
1437        algo.on_order_pending_cancel(Default::default());
1438        algo.on_order_modify_rejected(Default::default());
1439        algo.on_order_cancel_rejected(Default::default());
1440        algo.on_order_updated(Default::default());
1441        algo.on_algo_order_filled(Default::default());
1442    }
1443
1444    #[rstest]
1445    fn test_strategy_subscription_tracking() {
1446        let mut algo = create_test_algorithm();
1447        let strategy_id = StrategyId::from("STRAT-001");
1448
1449        assert!(!algo.core.is_strategy_subscribed(&strategy_id));
1450
1451        algo.subscribe_to_strategy_events(strategy_id);
1452        assert!(algo.core.is_strategy_subscribed(&strategy_id));
1453
1454        // Second call should be idempotent
1455        algo.subscribe_to_strategy_events(strategy_id);
1456        assert!(algo.core.is_strategy_subscribed(&strategy_id));
1457    }
1458
1459    #[rstest]
1460    fn test_algorithm_reset() {
1461        let mut algo = create_test_algorithm();
1462        let strategy_id = StrategyId::from("STRAT-001");
1463        let primary_id = ClientOrderId::new("O-001");
1464
1465        let _ = algo.core.spawn_client_order_id(&primary_id);
1466        algo.core.add_subscribed_strategy(strategy_id);
1467
1468        assert!(algo.core.spawn_sequence(&primary_id).is_some());
1469        assert!(algo.core.is_strategy_subscribed(&strategy_id));
1470
1471        ExecutionAlgorithm::on_reset(&mut algo).unwrap();
1472
1473        assert!(algo.core.spawn_sequence(&primary_id).is_none());
1474        assert!(!algo.core.is_strategy_subscribed(&strategy_id));
1475    }
1476
1477    #[rstest]
1478    fn test_algorithm_spawn_limit_creates_valid_order() {
1479        let mut algo = create_test_algorithm();
1480        register_algorithm(&mut algo);
1481
1482        let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
1483        let mut primary = OrderAny::Market(MarketOrder::new(
1484            TraderId::from("TRADER-001"),
1485            StrategyId::from("STRAT-001"),
1486            instrument_id,
1487            ClientOrderId::from("O-001"),
1488            OrderSide::Buy,
1489            Quantity::from("1.0"),
1490            TimeInForce::Gtc,
1491            UUID4::new(),
1492            0.into(),
1493            false,
1494            false,
1495            None,
1496            None,
1497            None,
1498            None,
1499            None,
1500            None,
1501            None,
1502            None,
1503        ));
1504
1505        let price = Price::from("50000.0");
1506        let spawned = algo.spawn_limit(
1507            &mut primary,
1508            Quantity::from("0.5"),
1509            price,
1510            TimeInForce::Gtc,
1511            None,  // expire_time
1512            false, // post_only
1513            false, // reduce_only
1514            None,  // display_qty
1515            None,  // emulation_trigger
1516            None,  // tags
1517            false, // reduce_primary
1518        );
1519
1520        assert_eq!(spawned.client_order_id.as_str(), "O-001-E1");
1521        assert_eq!(spawned.instrument_id, instrument_id);
1522        assert_eq!(spawned.order_side(), OrderSide::Buy);
1523        assert_eq!(spawned.quantity, Quantity::from("0.5"));
1524        assert_eq!(spawned.price, price);
1525        assert_eq!(spawned.time_in_force, TimeInForce::Gtc);
1526        assert_eq!(spawned.exec_algorithm_id, Some(algo.id()));
1527        assert_eq!(spawned.exec_spawn_id, Some(ClientOrderId::from("O-001")));
1528    }
1529
1530    #[rstest]
1531    fn test_algorithm_spawn_market_to_limit_creates_valid_order() {
1532        let mut algo = create_test_algorithm();
1533        register_algorithm(&mut algo);
1534
1535        let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
1536        let mut primary = OrderAny::Market(MarketOrder::new(
1537            TraderId::from("TRADER-001"),
1538            StrategyId::from("STRAT-001"),
1539            instrument_id,
1540            ClientOrderId::from("O-001"),
1541            OrderSide::Buy,
1542            Quantity::from("1.0"),
1543            TimeInForce::Gtc,
1544            UUID4::new(),
1545            0.into(),
1546            false,
1547            false,
1548            None,
1549            None,
1550            None,
1551            None,
1552            None,
1553            None,
1554            None,
1555            None,
1556        ));
1557
1558        let spawned = algo.spawn_market_to_limit(
1559            &mut primary,
1560            Quantity::from("0.5"),
1561            TimeInForce::Gtc,
1562            None,  // expire_time
1563            false, // reduce_only
1564            None,  // display_qty
1565            None,  // emulation_trigger
1566            None,  // tags
1567            false, // reduce_primary
1568        );
1569
1570        assert_eq!(spawned.client_order_id.as_str(), "O-001-E1");
1571        assert_eq!(spawned.instrument_id, instrument_id);
1572        assert_eq!(spawned.order_side(), OrderSide::Buy);
1573        assert_eq!(spawned.quantity, Quantity::from("0.5"));
1574        assert_eq!(spawned.time_in_force, TimeInForce::Gtc);
1575        assert_eq!(spawned.exec_algorithm_id, Some(algo.id()));
1576        assert_eq!(spawned.exec_spawn_id, Some(ClientOrderId::from("O-001")));
1577    }
1578
1579    #[rstest]
1580    fn test_algorithm_spawn_market_with_tags() {
1581        let mut algo = create_test_algorithm();
1582        register_algorithm(&mut algo);
1583
1584        let mut primary = OrderAny::Market(MarketOrder::new(
1585            TraderId::from("TRADER-001"),
1586            StrategyId::from("STRAT-001"),
1587            InstrumentId::from("BTC/USDT.BINANCE"),
1588            ClientOrderId::from("O-001"),
1589            OrderSide::Buy,
1590            Quantity::from("1.0"),
1591            TimeInForce::Gtc,
1592            UUID4::new(),
1593            0.into(),
1594            false,
1595            false,
1596            None,
1597            None,
1598            None,
1599            None,
1600            None,
1601            None,
1602            None,
1603            None,
1604        ));
1605
1606        let tags = vec![ustr::Ustr::from("TAG1"), ustr::Ustr::from("TAG2")];
1607        let spawned = algo.spawn_market(
1608            &mut primary,
1609            Quantity::from("0.5"),
1610            TimeInForce::Ioc,
1611            false,
1612            Some(tags.clone()),
1613            false,
1614        );
1615
1616        assert_eq!(spawned.tags, Some(tags));
1617    }
1618
1619    #[rstest]
1620    fn test_algorithm_reduce_primary_order() {
1621        let mut algo = create_test_algorithm();
1622        register_algorithm(&mut algo);
1623
1624        let order = OrderAny::Market(MarketOrder::new(
1625            TraderId::from("TRADER-001"),
1626            StrategyId::from("STRAT-001"),
1627            InstrumentId::from("BTC/USDT.BINANCE"),
1628            ClientOrderId::from("O-001"),
1629            OrderSide::Buy,
1630            Quantity::from("1.0"),
1631            TimeInForce::Gtc,
1632            UUID4::new(),
1633            0.into(),
1634            false,
1635            false,
1636            None,
1637            None,
1638            None,
1639            None,
1640            None,
1641            None,
1642            None,
1643            None,
1644        ));
1645
1646        // Make accepted so OrderUpdated can be applied
1647        let mut primary = TestOrderStubs::make_accepted_order(&order);
1648
1649        {
1650            let cache_rc = algo.core.cache_rc();
1651            let mut cache = cache_rc.borrow_mut();
1652            cache.add_order(primary.clone(), None, None, false).unwrap();
1653        }
1654
1655        let spawn_qty = Quantity::from("0.3");
1656        algo.reduce_primary_order(&mut primary, spawn_qty);
1657
1658        assert_eq!(primary.quantity(), Quantity::from("0.7"));
1659    }
1660
1661    #[rstest]
1662    fn test_algorithm_spawn_market_with_reduce_primary() {
1663        let mut algo = create_test_algorithm();
1664        register_algorithm(&mut algo);
1665
1666        let order = OrderAny::Market(MarketOrder::new(
1667            TraderId::from("TRADER-001"),
1668            StrategyId::from("STRAT-001"),
1669            InstrumentId::from("BTC/USDT.BINANCE"),
1670            ClientOrderId::from("O-001"),
1671            OrderSide::Buy,
1672            Quantity::from("1.0"),
1673            TimeInForce::Gtc,
1674            UUID4::new(),
1675            0.into(),
1676            false,
1677            false,
1678            None,
1679            None,
1680            None,
1681            None,
1682            None,
1683            None,
1684            None,
1685            None,
1686        ));
1687
1688        // Make accepted so OrderUpdated can be applied
1689        let mut primary = TestOrderStubs::make_accepted_order(&order);
1690
1691        {
1692            let cache_rc = algo.core.cache_rc();
1693            let mut cache = cache_rc.borrow_mut();
1694            cache.add_order(primary.clone(), None, None, false).unwrap();
1695        }
1696
1697        let spawned = algo.spawn_market(
1698            &mut primary,
1699            Quantity::from("0.4"),
1700            TimeInForce::Ioc,
1701            false,
1702            None,
1703            true, // reduce_primary = true
1704        );
1705
1706        assert_eq!(spawned.quantity, Quantity::from("0.4"));
1707        assert_eq!(primary.quantity(), Quantity::from("0.6"));
1708    }
1709
1710    #[rstest]
1711    fn test_algorithm_generate_order_canceled() {
1712        let mut algo = create_test_algorithm();
1713        register_algorithm(&mut algo);
1714
1715        let order = OrderAny::Market(MarketOrder::new(
1716            TraderId::from("TRADER-001"),
1717            StrategyId::from("STRAT-001"),
1718            InstrumentId::from("BTC/USDT.BINANCE"),
1719            ClientOrderId::from("O-001"),
1720            OrderSide::Buy,
1721            Quantity::from("1.0"),
1722            TimeInForce::Gtc,
1723            UUID4::new(),
1724            0.into(),
1725            false,
1726            false,
1727            None,
1728            None,
1729            None,
1730            None,
1731            None,
1732            None,
1733            None,
1734            None,
1735        ));
1736
1737        let event = algo.generate_order_canceled(&order);
1738
1739        assert_eq!(event.trader_id, TraderId::from("TRADER-001"));
1740        assert_eq!(event.strategy_id, StrategyId::from("STRAT-001"));
1741        assert_eq!(event.instrument_id, InstrumentId::from("BTC/USDT.BINANCE"));
1742        assert_eq!(event.client_order_id, ClientOrderId::from("O-001"));
1743    }
1744
1745    #[rstest]
1746    fn test_algorithm_modify_order_in_place_updates_quantity() {
1747        let mut algo = create_test_algorithm();
1748        register_algorithm(&mut algo);
1749
1750        let mut order = OrderAny::Limit(LimitOrder::new(
1751            TraderId::from("TRADER-001"),
1752            StrategyId::from("STRAT-001"),
1753            InstrumentId::from("BTC/USDT.BINANCE"),
1754            ClientOrderId::from("O-001"),
1755            OrderSide::Buy,
1756            Quantity::from("1.0"),
1757            Price::from("50000.0"),
1758            TimeInForce::Gtc,
1759            None,  // expire_time
1760            false, // post_only
1761            false, // reduce_only
1762            false, // quote_quantity
1763            None,  // display_qty
1764            None,  // emulation_trigger
1765            None,  // trigger_instrument_id
1766            None,  // contingency_type
1767            None,  // order_list_id
1768            None,  // linked_order_ids
1769            None,  // parent_order_id
1770            None,  // exec_algorithm_id
1771            None,  // exec_algorithm_params
1772            None,  // exec_spawn_id
1773            None,  // tags
1774            UUID4::new(),
1775            0.into(),
1776        ));
1777
1778        {
1779            let cache_rc = algo.core.cache_rc();
1780            let mut cache = cache_rc.borrow_mut();
1781            cache.add_order(order.clone(), None, None, false).unwrap();
1782        }
1783
1784        let new_qty = Quantity::from("0.5");
1785        algo.modify_order_in_place(&mut order, Some(new_qty), None, None)
1786            .unwrap();
1787
1788        assert_eq!(order.quantity(), new_qty);
1789    }
1790
1791    #[rstest]
1792    fn test_algorithm_modify_order_in_place_rejects_no_changes() {
1793        let mut algo = create_test_algorithm();
1794        register_algorithm(&mut algo);
1795
1796        let mut order = OrderAny::Limit(LimitOrder::new(
1797            TraderId::from("TRADER-001"),
1798            StrategyId::from("STRAT-001"),
1799            InstrumentId::from("BTC/USDT.BINANCE"),
1800            ClientOrderId::from("O-001"),
1801            OrderSide::Buy,
1802            Quantity::from("1.0"),
1803            Price::from("50000.0"),
1804            TimeInForce::Gtc,
1805            None,
1806            false,
1807            false,
1808            false,
1809            None,
1810            None,
1811            None,
1812            None,
1813            None,
1814            None,
1815            None,
1816            None,
1817            None,
1818            None,
1819            None,
1820            UUID4::new(),
1821            0.into(),
1822        ));
1823
1824        // Try to modify with same quantity - should fail
1825        let result =
1826            algo.modify_order_in_place(&mut order, Some(Quantity::from("1.0")), None, None);
1827
1828        assert!(result.is_err());
1829        assert!(
1830            result
1831                .unwrap_err()
1832                .to_string()
1833                .contains("no parameters differ")
1834        );
1835    }
1836
1837    #[rstest]
1838    fn test_spawned_order_denied_restores_primary_quantity() {
1839        let mut algo = create_test_algorithm();
1840        register_algorithm(&mut algo);
1841
1842        let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
1843        let exec_algorithm_id = algo.id();
1844
1845        let mut primary = OrderAny::Market(MarketOrder::new(
1846            TraderId::from("TRADER-001"),
1847            StrategyId::from("STRAT-001"),
1848            instrument_id,
1849            ClientOrderId::from("O-001"),
1850            OrderSide::Buy,
1851            Quantity::from("1.0"),
1852            TimeInForce::Gtc,
1853            UUID4::new(),
1854            0.into(),
1855            false,
1856            false,
1857            None,
1858            None,
1859            None,
1860            None,
1861            Some(exec_algorithm_id),
1862            None,
1863            None,
1864            None,
1865        ));
1866
1867        {
1868            let cache_rc = algo.core.cache_rc();
1869            let mut cache = cache_rc.borrow_mut();
1870            cache.add_order(primary.clone(), None, None, false).unwrap();
1871        }
1872
1873        let spawned = algo.spawn_market(
1874            &mut primary,
1875            Quantity::from("0.5"),
1876            TimeInForce::Fok,
1877            false,
1878            None,
1879            true,
1880        );
1881
1882        {
1883            let cache_rc = algo.core.cache_rc();
1884            let mut cache = cache_rc.borrow_mut();
1885            cache.update_order(&primary).unwrap();
1886        }
1887
1888        assert_eq!(primary.quantity(), Quantity::from("0.5"));
1889
1890        let mut spawned_order = OrderAny::Market(spawned);
1891        {
1892            let cache_rc = algo.core.cache_rc();
1893            let mut cache = cache_rc.borrow_mut();
1894            cache
1895                .add_order(spawned_order.clone(), None, None, false)
1896                .unwrap();
1897        }
1898
1899        let denied = OrderDenied::new(
1900            spawned_order.trader_id(),
1901            spawned_order.strategy_id(),
1902            spawned_order.instrument_id(),
1903            spawned_order.client_order_id(),
1904            "TEST_DENIAL".into(),
1905            UUID4::new(),
1906            0.into(),
1907            0.into(),
1908        );
1909
1910        spawned_order.apply(OrderEventAny::Denied(denied)).unwrap();
1911        {
1912            let cache_rc = algo.core.cache_rc();
1913            let mut cache = cache_rc.borrow_mut();
1914            cache.update_order(&spawned_order).unwrap();
1915        }
1916
1917        algo.handle_order_event(OrderEventAny::Denied(denied));
1918
1919        let restored_primary = {
1920            let cache = algo.core.cache();
1921            cache.order(&ClientOrderId::from("O-001")).cloned().unwrap()
1922        };
1923        assert_eq!(restored_primary.quantity(), Quantity::from("1.0"));
1924    }
1925
1926    #[rstest]
1927    fn test_spawned_order_rejected_restores_primary_quantity() {
1928        let mut algo = create_test_algorithm();
1929        register_algorithm(&mut algo);
1930
1931        let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
1932        let exec_algorithm_id = algo.id();
1933
1934        let mut primary = OrderAny::Market(MarketOrder::new(
1935            TraderId::from("TRADER-001"),
1936            StrategyId::from("STRAT-001"),
1937            instrument_id,
1938            ClientOrderId::from("O-001"),
1939            OrderSide::Buy,
1940            Quantity::from("1.0"),
1941            TimeInForce::Gtc,
1942            UUID4::new(),
1943            0.into(),
1944            false,
1945            false,
1946            None,
1947            None,
1948            None,
1949            None,
1950            Some(exec_algorithm_id),
1951            None,
1952            None,
1953            None,
1954        ));
1955
1956        {
1957            let cache_rc = algo.core.cache_rc();
1958            let mut cache = cache_rc.borrow_mut();
1959            cache.add_order(primary.clone(), None, None, false).unwrap();
1960        }
1961
1962        let spawned = algo.spawn_market(
1963            &mut primary,
1964            Quantity::from("0.5"),
1965            TimeInForce::Fok,
1966            false,
1967            None,
1968            true,
1969        );
1970
1971        {
1972            let cache_rc = algo.core.cache_rc();
1973            let mut cache = cache_rc.borrow_mut();
1974            cache.update_order(&primary).unwrap();
1975        }
1976
1977        assert_eq!(primary.quantity(), Quantity::from("0.5"));
1978
1979        let mut spawned_order = OrderAny::Market(spawned);
1980        {
1981            let cache_rc = algo.core.cache_rc();
1982            let mut cache = cache_rc.borrow_mut();
1983            cache
1984                .add_order(spawned_order.clone(), None, None, false)
1985                .unwrap();
1986        }
1987
1988        let rejected = OrderRejected::new(
1989            spawned_order.trader_id(),
1990            spawned_order.strategy_id(),
1991            spawned_order.instrument_id(),
1992            spawned_order.client_order_id(),
1993            AccountId::from("BINANCE-001"),
1994            "TEST_REJECTION".into(),
1995            UUID4::new(),
1996            0.into(),
1997            0.into(),
1998            false,
1999            false,
2000        );
2001
2002        spawned_order
2003            .apply(OrderEventAny::Rejected(rejected))
2004            .unwrap();
2005        {
2006            let cache_rc = algo.core.cache_rc();
2007            let mut cache = cache_rc.borrow_mut();
2008            cache.update_order(&spawned_order).unwrap();
2009        }
2010
2011        algo.handle_order_event(OrderEventAny::Rejected(rejected));
2012
2013        let restored_primary = {
2014            let cache = algo.core.cache();
2015            cache.order(&ClientOrderId::from("O-001")).cloned().unwrap()
2016        };
2017        assert_eq!(restored_primary.quantity(), Quantity::from("1.0"));
2018    }
2019
2020    #[rstest]
2021    fn test_spawned_order_with_reduce_primary_false_does_not_restore() {
2022        let mut algo = create_test_algorithm();
2023        register_algorithm(&mut algo);
2024
2025        let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
2026        let exec_algorithm_id = algo.id();
2027
2028        let mut primary = OrderAny::Market(MarketOrder::new(
2029            TraderId::from("TRADER-001"),
2030            StrategyId::from("STRAT-001"),
2031            instrument_id,
2032            ClientOrderId::from("O-001"),
2033            OrderSide::Buy,
2034            Quantity::from("1.0"),
2035            TimeInForce::Gtc,
2036            UUID4::new(),
2037            0.into(),
2038            false,
2039            false,
2040            None,
2041            None,
2042            None,
2043            None,
2044            Some(exec_algorithm_id),
2045            None,
2046            None,
2047            None,
2048        ));
2049
2050        {
2051            let cache_rc = algo.core.cache_rc();
2052            let mut cache = cache_rc.borrow_mut();
2053            cache.add_order(primary.clone(), None, None, false).unwrap();
2054        }
2055
2056        let spawned = algo.spawn_market(
2057            &mut primary,
2058            Quantity::from("0.5"),
2059            TimeInForce::Fok,
2060            false,
2061            None,
2062            false,
2063        );
2064
2065        assert_eq!(primary.quantity(), Quantity::from("1.0"));
2066
2067        let mut spawned_order = OrderAny::Market(spawned);
2068        {
2069            let cache_rc = algo.core.cache_rc();
2070            let mut cache = cache_rc.borrow_mut();
2071            cache
2072                .add_order(spawned_order.clone(), None, None, false)
2073                .unwrap();
2074        }
2075
2076        let denied = OrderDenied::new(
2077            spawned_order.trader_id(),
2078            spawned_order.strategy_id(),
2079            spawned_order.instrument_id(),
2080            spawned_order.client_order_id(),
2081            "TEST_DENIAL".into(),
2082            UUID4::new(),
2083            0.into(),
2084            0.into(),
2085        );
2086
2087        spawned_order.apply(OrderEventAny::Denied(denied)).unwrap();
2088        {
2089            let cache_rc = algo.core.cache_rc();
2090            let mut cache = cache_rc.borrow_mut();
2091            cache.update_order(&spawned_order).unwrap();
2092        }
2093
2094        algo.handle_order_event(OrderEventAny::Denied(denied));
2095
2096        let final_primary = {
2097            let cache = algo.core.cache();
2098            cache.order(&ClientOrderId::from("O-001")).cloned().unwrap()
2099        };
2100        assert_eq!(final_primary.quantity(), Quantity::from("1.0"));
2101    }
2102
2103    #[rstest]
2104    fn test_multiple_spawns_with_one_denied_restores_correctly() {
2105        let mut algo = create_test_algorithm();
2106        register_algorithm(&mut algo);
2107
2108        let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
2109        let exec_algorithm_id = algo.id();
2110
2111        let mut primary = OrderAny::Market(MarketOrder::new(
2112            TraderId::from("TRADER-001"),
2113            StrategyId::from("STRAT-001"),
2114            instrument_id,
2115            ClientOrderId::from("O-001"),
2116            OrderSide::Buy,
2117            Quantity::from("1.0"),
2118            TimeInForce::Gtc,
2119            UUID4::new(),
2120            0.into(),
2121            false,
2122            false,
2123            None,
2124            None,
2125            None,
2126            None,
2127            Some(exec_algorithm_id),
2128            None,
2129            None,
2130            None,
2131        ));
2132
2133        {
2134            let cache_rc = algo.core.cache_rc();
2135            let mut cache = cache_rc.borrow_mut();
2136            cache.add_order(primary.clone(), None, None, false).unwrap();
2137        }
2138
2139        let spawned1 = algo.spawn_market(
2140            &mut primary,
2141            Quantity::from("0.3"),
2142            TimeInForce::Fok,
2143            false,
2144            None,
2145            true,
2146        );
2147        {
2148            let cache_rc = algo.core.cache_rc();
2149            let mut cache = cache_rc.borrow_mut();
2150            cache.update_order(&primary).unwrap();
2151        }
2152
2153        let spawned2 = algo.spawn_market(
2154            &mut primary,
2155            Quantity::from("0.4"),
2156            TimeInForce::Fok,
2157            false,
2158            None,
2159            true,
2160        );
2161        {
2162            let cache_rc = algo.core.cache_rc();
2163            let mut cache = cache_rc.borrow_mut();
2164            cache.update_order(&primary).unwrap();
2165        }
2166
2167        assert_eq!(primary.quantity(), Quantity::from("0.3"));
2168
2169        let spawned_order1 = OrderAny::Market(spawned1);
2170        let mut spawned_order2 = OrderAny::Market(spawned2);
2171        {
2172            let cache_rc = algo.core.cache_rc();
2173            let mut cache = cache_rc.borrow_mut();
2174            cache.add_order(spawned_order1, None, None, false).unwrap();
2175            cache
2176                .add_order(spawned_order2.clone(), None, None, false)
2177                .unwrap();
2178        }
2179
2180        let denied = OrderDenied::new(
2181            spawned_order2.trader_id(),
2182            spawned_order2.strategy_id(),
2183            spawned_order2.instrument_id(),
2184            spawned_order2.client_order_id(),
2185            "TEST_DENIAL".into(),
2186            UUID4::new(),
2187            0.into(),
2188            0.into(),
2189        );
2190
2191        spawned_order2.apply(OrderEventAny::Denied(denied)).unwrap();
2192        {
2193            let cache_rc = algo.core.cache_rc();
2194            let mut cache = cache_rc.borrow_mut();
2195            cache.update_order(&spawned_order2).unwrap();
2196        }
2197
2198        algo.handle_order_event(OrderEventAny::Denied(denied));
2199
2200        let restored_primary = {
2201            let cache = algo.core.cache();
2202            cache.order(&ClientOrderId::from("O-001")).cloned().unwrap()
2203        };
2204        assert_eq!(restored_primary.quantity(), Quantity::from("0.7"));
2205    }
2206
2207    #[rstest]
2208    fn test_spawned_order_accepted_prevents_restoration() {
2209        let mut algo = create_test_algorithm();
2210        register_algorithm(&mut algo);
2211
2212        let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
2213        let exec_algorithm_id = algo.id();
2214
2215        let mut primary = OrderAny::Market(MarketOrder::new(
2216            TraderId::from("TRADER-001"),
2217            StrategyId::from("STRAT-001"),
2218            instrument_id,
2219            ClientOrderId::from("O-001"),
2220            OrderSide::Buy,
2221            Quantity::from("1.0"),
2222            TimeInForce::Gtc,
2223            UUID4::new(),
2224            0.into(),
2225            false,
2226            false,
2227            None,
2228            None,
2229            None,
2230            None,
2231            Some(exec_algorithm_id),
2232            None,
2233            None,
2234            None,
2235        ));
2236
2237        {
2238            let cache_rc = algo.core.cache_rc();
2239            let mut cache = cache_rc.borrow_mut();
2240            cache.add_order(primary.clone(), None, None, false).unwrap();
2241        }
2242
2243        let spawned = algo.spawn_market(
2244            &mut primary,
2245            Quantity::from("0.5"),
2246            TimeInForce::Fok,
2247            false,
2248            None,
2249            true,
2250        );
2251
2252        {
2253            let cache_rc = algo.core.cache_rc();
2254            let mut cache = cache_rc.borrow_mut();
2255            cache.update_order(&primary).unwrap();
2256        }
2257
2258        assert_eq!(primary.quantity(), Quantity::from("0.5"));
2259
2260        let mut spawned_order = OrderAny::Market(spawned);
2261        {
2262            let cache_rc = algo.core.cache_rc();
2263            let mut cache = cache_rc.borrow_mut();
2264            cache
2265                .add_order(spawned_order.clone(), None, None, false)
2266                .unwrap();
2267        }
2268
2269        let accepted = OrderAccepted::new(
2270            spawned_order.trader_id(),
2271            spawned_order.strategy_id(),
2272            spawned_order.instrument_id(),
2273            spawned_order.client_order_id(),
2274            VenueOrderId::from("V-123"),
2275            AccountId::from("BINANCE-001"),
2276            UUID4::new(),
2277            0.into(),
2278            0.into(),
2279            false,
2280        );
2281
2282        spawned_order
2283            .apply(OrderEventAny::Accepted(accepted))
2284            .unwrap();
2285        {
2286            let cache_rc = algo.core.cache_rc();
2287            let mut cache = cache_rc.borrow_mut();
2288            cache.update_order(&spawned_order).unwrap();
2289        }
2290
2291        algo.handle_order_event(OrderEventAny::Accepted(accepted));
2292
2293        let primary_after_accept = {
2294            let cache = algo.core.cache();
2295            cache.order(&ClientOrderId::from("O-001")).cloned().unwrap()
2296        };
2297        assert_eq!(primary_after_accept.quantity(), Quantity::from("0.5"));
2298
2299        // Cancel after acceptance - no restoration should occur
2300        let canceled = OrderCanceled::new(
2301            spawned_order.trader_id(),
2302            spawned_order.strategy_id(),
2303            spawned_order.instrument_id(),
2304            spawned_order.client_order_id(),
2305            UUID4::new(),
2306            0.into(),
2307            0.into(),
2308            false,
2309            Some(VenueOrderId::from("V-123")),
2310            Some(AccountId::from("BINANCE-001")),
2311        );
2312
2313        spawned_order
2314            .apply(OrderEventAny::Canceled(canceled))
2315            .unwrap();
2316        {
2317            let cache_rc = algo.core.cache_rc();
2318            let mut cache = cache_rc.borrow_mut();
2319            cache.update_order(&spawned_order).unwrap();
2320        }
2321
2322        algo.handle_order_event(OrderEventAny::Canceled(canceled));
2323
2324        let final_primary = {
2325            let cache = algo.core.cache();
2326            cache.order(&ClientOrderId::from("O-001")).cloned().unwrap()
2327        };
2328        assert_eq!(final_primary.quantity(), Quantity::from("0.5"));
2329    }
2330
2331    #[rstest]
2332    #[should_panic(expected = "exceeds primary leaves_qty")]
2333    fn test_spawn_quantity_exceeds_leaves_qty_panics() {
2334        let mut algo = create_test_algorithm();
2335        register_algorithm(&mut algo);
2336
2337        let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
2338        let exec_algorithm_id = algo.id();
2339
2340        let mut primary = OrderAny::Market(MarketOrder::new(
2341            TraderId::from("TRADER-001"),
2342            StrategyId::from("STRAT-001"),
2343            instrument_id,
2344            ClientOrderId::from("O-001"),
2345            OrderSide::Buy,
2346            Quantity::from("1.0"),
2347            TimeInForce::Gtc,
2348            UUID4::new(),
2349            0.into(),
2350            false,
2351            false,
2352            None,
2353            None,
2354            None,
2355            None,
2356            Some(exec_algorithm_id),
2357            None,
2358            None,
2359            None,
2360        ));
2361
2362        {
2363            let cache_rc = algo.core.cache_rc();
2364            let mut cache = cache_rc.borrow_mut();
2365            cache.add_order(primary.clone(), None, None, false).unwrap();
2366        }
2367
2368        let _ = algo.spawn_market(
2369            &mut primary,
2370            Quantity::from("0.8"),
2371            TimeInForce::Fok,
2372            false,
2373            None,
2374            true,
2375        );
2376
2377        {
2378            let cache_rc = algo.core.cache_rc();
2379            let mut cache = cache_rc.borrow_mut();
2380            cache.update_order(&primary).unwrap();
2381        }
2382
2383        assert_eq!(primary.quantity(), Quantity::from("0.2"));
2384        assert_eq!(primary.leaves_qty(), Quantity::from("0.2"));
2385
2386        // Should panic - spawning 0.5 when only 0.2 leaves_qty remains
2387        let _ = algo.spawn_market(
2388            &mut primary,
2389            Quantity::from("0.5"),
2390            TimeInForce::Fok,
2391            false,
2392            None,
2393            true,
2394        );
2395    }
2396}