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, ImportableExecAlgorithmConfig};
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            primary.is_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    #[expect(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            primary.is_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    #[expect(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            primary.is_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            primary.is_quote_quantity(),
508        );
509
510        primary
511            .apply(OrderEventAny::Updated(updated))
512            .expect("Failed to apply OrderUpdated");
513
514        let cache_rc = core.cache_rc();
515        let mut cache = cache_rc.borrow_mut();
516        cache
517            .update_order(primary)
518            .expect("Failed to update order in cache");
519    }
520
521    /// Restores the primary order quantity after a spawned order is denied or rejected.
522    ///
523    /// This is called when a spawned order fails before acceptance. The quantity
524    /// that was deducted from the primary order is restored (up to the spawned
525    /// order's leaves_qty to handle partial fills).
526    fn restore_primary_order_quantity(&mut self, order: &OrderAny) {
527        let Some(exec_spawn_id) = order.exec_spawn_id() else {
528            return;
529        };
530
531        let reduction_qty = {
532            let core = self.core_mut();
533            core.take_pending_spawn_reduction(&order.client_order_id())
534        };
535
536        let Some(reduction_qty) = reduction_qty else {
537            return;
538        };
539
540        let primary = {
541            let cache = self.core_mut().cache();
542            cache.order(&exec_spawn_id).cloned()
543        };
544
545        let Some(mut primary) = primary else {
546            log::warn!(
547                "Cannot restore primary order quantity: primary order {exec_spawn_id} not found",
548            );
549            return;
550        };
551
552        // Cap restore amount by leaves_qty to handle partial fills before rejection
553        let restore_raw = std::cmp::min(reduction_qty.raw, order.leaves_qty().raw);
554        if restore_raw == 0 {
555            return;
556        }
557
558        let restored_qty = Quantity::from_raw(
559            primary.quantity().raw + restore_raw,
560            primary.quantity().precision,
561        );
562
563        let core = self.core_mut();
564        let ts_now = core.clock().timestamp_ns();
565
566        let updated = OrderUpdated::new(
567            primary.trader_id(),
568            primary.strategy_id(),
569            primary.instrument_id(),
570            primary.client_order_id(),
571            restored_qty,
572            UUID4::new(),
573            ts_now,
574            ts_now,
575            false, // reconciliation
576            primary.venue_order_id(),
577            primary.account_id(),
578            None, // price
579            None, // trigger_price
580            None, // protection_price
581            primary.is_quote_quantity(),
582        );
583
584        if let Err(e) = primary.apply(OrderEventAny::Updated(updated)) {
585            log::warn!("Failed to apply OrderUpdated for quantity restoration: {e}");
586            return;
587        }
588
589        {
590            let cache_rc = core.cache_rc();
591            let mut cache = cache_rc.borrow_mut();
592            if let Err(e) = cache.update_order(&primary) {
593                log::warn!("Failed to update primary order in cache: {e}");
594                return;
595            }
596        }
597
598        log::info!(
599            "Restored primary order {} quantity to {} after spawned order {} was denied/rejected",
600            primary.client_order_id(),
601            restored_qty,
602            order.client_order_id()
603        );
604    }
605
606    /// Submits an order to the execution engine via the risk engine.
607    ///
608    /// # Errors
609    ///
610    /// Returns an error if order submission fails.
611    fn submit_order(
612        &mut self,
613        order: OrderAny,
614        position_id: Option<PositionId>,
615        client_id: Option<ClientId>,
616    ) -> anyhow::Result<()> {
617        let core = self.core_mut();
618
619        let trader_id = core.trader_id().expect("Trader ID not set");
620        let ts_init = core.clock().timestamp_ns();
621
622        // For spawned orders, use the parent's strategy ID
623        let strategy_id = order.strategy_id();
624
625        {
626            let cache_rc = core.cache_rc();
627            let mut cache = cache_rc.borrow_mut();
628            cache.add_order(order.clone(), position_id, client_id, true)?;
629        }
630
631        let command = SubmitOrder::new(
632            trader_id,
633            client_id,
634            strategy_id,
635            order.instrument_id(),
636            order.client_order_id(),
637            order.init_event().clone(),
638            order.exec_algorithm_id(),
639            position_id,
640            None, // params
641            UUID4::new(),
642            ts_init,
643        );
644
645        if core.config.log_commands {
646            let id = &core.actor.actor_id;
647            log::info!("{id} {SEND}{CMD} {command:?}");
648        }
649
650        msgbus::send_trading_command(
651            MessagingSwitchboard::risk_engine_execute(),
652            TradingCommand::SubmitOrder(command),
653        );
654
655        Ok(())
656    }
657
658    /// Modifies an order.
659    ///
660    /// # Errors
661    ///
662    /// Returns an error if order modification fails.
663    fn modify_order(
664        &mut self,
665        order: &mut OrderAny,
666        quantity: Option<Quantity>,
667        price: Option<Price>,
668        trigger_price: Option<Price>,
669        client_id: Option<ClientId>,
670    ) -> anyhow::Result<()> {
671        let qty_changing = quantity.is_some_and(|q| q != order.quantity());
672        let price_changing = price.is_some() && price != order.price();
673        let trigger_changing = trigger_price.is_some() && trigger_price != order.trigger_price();
674
675        if !qty_changing && !price_changing && !trigger_changing {
676            log::error!(
677                "Cannot create command ModifyOrder: \
678                quantity, price and trigger were either None \
679                or the same as existing values"
680            );
681            return Ok(());
682        }
683
684        if order.is_closed() || order.is_pending_cancel() {
685            log::warn!(
686                "Cannot create command ModifyOrder: state is {:?}, {order:?}",
687                order.status()
688            );
689            return Ok(());
690        }
691
692        let core = self.core_mut();
693        let trader_id = core.trader_id().expect("Trader ID not set");
694        let strategy_id = order.strategy_id();
695
696        if !order.is_active_local() {
697            let event = self.generate_order_pending_update(order);
698            if let Err(e) = order.apply(OrderEventAny::PendingUpdate(event)) {
699                log::warn!("InvalidStateTrigger: {e}, did not apply pending update event");
700                return Ok(());
701            }
702
703            {
704                let cache_rc = self.core_mut().cache_rc();
705                let mut cache = cache_rc.borrow_mut();
706                cache.update_order(order).ok();
707            }
708
709            let topic = format!("events.order.{strategy_id}");
710            msgbus::publish_order_event(topic.into(), &OrderEventAny::PendingUpdate(event));
711        }
712
713        let ts_init = self.core_mut().clock().timestamp_ns();
714        let command = ModifyOrder::new(
715            trader_id,
716            client_id,
717            strategy_id,
718            order.instrument_id(),
719            order.client_order_id(),
720            order.venue_order_id(),
721            quantity,
722            price,
723            trigger_price,
724            UUID4::new(),
725            ts_init,
726            None, // params
727        );
728
729        if self.core_mut().config.log_commands {
730            let id = &self.core_mut().actor.actor_id;
731            log::info!("{id} {SEND}{CMD} {command:?}");
732        }
733
734        let has_emulation_trigger = order
735            .emulation_trigger()
736            .is_some_and(|t| t != TriggerType::NoTrigger);
737
738        if order.is_emulated() || has_emulation_trigger {
739            msgbus::send_trading_command(
740                MessagingSwitchboard::order_emulator_execute(),
741                TradingCommand::ModifyOrder(command),
742            );
743        } else {
744            msgbus::send_trading_command(
745                MessagingSwitchboard::risk_engine_execute(),
746                TradingCommand::ModifyOrder(command),
747            );
748        }
749
750        Ok(())
751    }
752
753    /// Modifies an INITIALIZED or RELEASED order in place without sending a command.
754    ///
755    /// This is useful for adjusting order parameters before submission. The order
756    /// is updated locally by applying an `OrderUpdated` event and updating the cache.
757    ///
758    /// At least one parameter must differ from the current order values.
759    ///
760    /// # Errors
761    ///
762    /// Returns an error if the order status is not INITIALIZED or RELEASED,
763    /// or if no parameters would change.
764    fn modify_order_in_place(
765        &mut self,
766        order: &mut OrderAny,
767        quantity: Option<Quantity>,
768        price: Option<Price>,
769        trigger_price: Option<Price>,
770    ) -> anyhow::Result<()> {
771        // Validate order status
772        let status = order.status();
773        if status != OrderStatus::Initialized && status != OrderStatus::Released {
774            anyhow::bail!(
775                "Cannot modify order in place: status is {status:?}, expected INITIALIZED or RELEASED"
776            );
777        }
778
779        // Validate order type compatibility
780        if price.is_some() && order.price().is_none() {
781            anyhow::bail!(
782                "Cannot modify order in place: {} orders do not have a LIMIT price",
783                order.order_type()
784            );
785        }
786
787        if trigger_price.is_some() && order.trigger_price().is_none() {
788            anyhow::bail!(
789                "Cannot modify order in place: {} orders do not have a STOP trigger price",
790                order.order_type()
791            );
792        }
793
794        // Check if any value would actually change
795        let qty_changing = quantity.is_some_and(|q| q != order.quantity());
796        let price_changing = price.is_some() && price != order.price();
797        let trigger_changing = trigger_price.is_some() && trigger_price != order.trigger_price();
798
799        if !qty_changing && !price_changing && !trigger_changing {
800            anyhow::bail!("Cannot modify order in place: no parameters differ from current values");
801        }
802
803        let core = self.core_mut();
804        let ts_now = core.clock().timestamp_ns();
805
806        let updated = OrderUpdated::new(
807            order.trader_id(),
808            order.strategy_id(),
809            order.instrument_id(),
810            order.client_order_id(),
811            quantity.unwrap_or_else(|| order.quantity()),
812            UUID4::new(),
813            ts_now,
814            ts_now,
815            false, // reconciliation
816            order.venue_order_id(),
817            order.account_id(),
818            price,
819            trigger_price,
820            None, // protection_price
821            order.is_quote_quantity(),
822        );
823
824        order
825            .apply(OrderEventAny::Updated(updated))
826            .map_err(|e| anyhow::anyhow!("Failed to apply OrderUpdated: {e}"))?;
827
828        let cache_rc = core.cache_rc();
829        let mut cache = cache_rc.borrow_mut();
830        cache.update_order(order)?;
831
832        Ok(())
833    }
834
835    /// Cancels an order.
836    ///
837    /// # Errors
838    ///
839    /// Returns an error if order cancellation fails.
840    fn cancel_order(
841        &mut self,
842        order: &mut OrderAny,
843        client_id: Option<ClientId>,
844    ) -> anyhow::Result<()> {
845        if order.is_closed() || order.is_pending_cancel() {
846            log::warn!(
847                "Cannot cancel order: state is {:?}, {order:?}",
848                order.status()
849            );
850            return Ok(());
851        }
852
853        let core = self.core_mut();
854        let trader_id = core.trader_id().expect("Trader ID not set");
855        let strategy_id = order.strategy_id();
856
857        if !order.is_active_local() {
858            let event = self.generate_order_pending_cancel(order);
859            if let Err(e) = order.apply(OrderEventAny::PendingCancel(event)) {
860                log::warn!("InvalidStateTrigger: {e}, did not apply pending cancel event");
861                return Ok(());
862            }
863
864            {
865                let cache_rc = self.core_mut().cache_rc();
866                let mut cache = cache_rc.borrow_mut();
867                cache.update_order(order).ok();
868            }
869
870            let topic = format!("events.order.{strategy_id}");
871            msgbus::publish_order_event(topic.into(), &OrderEventAny::PendingCancel(event));
872        }
873
874        let ts_init = self.core_mut().clock().timestamp_ns();
875        let command = CancelOrder::new(
876            trader_id,
877            client_id,
878            strategy_id,
879            order.instrument_id(),
880            order.client_order_id(),
881            order.venue_order_id(),
882            UUID4::new(),
883            ts_init,
884            None, // params
885        );
886
887        if self.core_mut().config.log_commands {
888            let id = &self.core_mut().actor.actor_id;
889            log::info!("{id} {SEND}{CMD} {command:?}");
890        }
891
892        let has_emulation_trigger = order
893            .emulation_trigger()
894            .is_some_and(|t| t != TriggerType::NoTrigger);
895
896        if order.is_emulated() || order.status() == OrderStatus::Released || has_emulation_trigger {
897            msgbus::send_trading_command(
898                MessagingSwitchboard::order_emulator_execute(),
899                TradingCommand::CancelOrder(command),
900            );
901        } else {
902            msgbus::send_trading_command(
903                MessagingSwitchboard::exec_engine_execute(),
904                TradingCommand::CancelOrder(command),
905            );
906        }
907
908        Ok(())
909    }
910
911    /// Subscribes to events from a strategy.
912    ///
913    /// This is called automatically when the first order is received from a strategy.
914    fn subscribe_to_strategy_events(&mut self, strategy_id: StrategyId)
915    where
916        Self: 'static + std::fmt::Debug + Sized,
917    {
918        let core = self.core_mut();
919        if core.is_strategy_subscribed(&strategy_id) {
920            return;
921        }
922
923        let actor_id = core.actor.actor_id.inner();
924
925        let order_topic = format!("events.order.{strategy_id}");
926        let order_actor_id = actor_id;
927        let order_handler = TypedHandler::from(move |event: &OrderEventAny| {
928            if let Some(mut algo) = try_get_actor_unchecked::<Self>(&order_actor_id) {
929                algo.handle_order_event(event.clone());
930            } else {
931                log::error!(
932                    "ExecutionAlgorithm {order_actor_id} not found for order event handling"
933                );
934            }
935        });
936        msgbus::subscribe_order_events(order_topic.clone().into(), order_handler.clone(), None);
937
938        let position_topic = format!("events.position.{strategy_id}");
939        let position_handler = TypedHandler::from(move |event: &PositionEvent| {
940            if let Some(mut algo) = try_get_actor_unchecked::<Self>(&actor_id) {
941                algo.handle_position_event(event.clone());
942            } else {
943                log::error!("ExecutionAlgorithm {actor_id} not found for position event handling");
944            }
945        });
946        msgbus::subscribe_position_events(
947            position_topic.clone().into(),
948            position_handler.clone(),
949            None,
950        );
951
952        let handlers = StrategyEventHandlers {
953            order_topic,
954            order_handler,
955            position_topic,
956            position_handler,
957        };
958        core.store_strategy_event_handlers(strategy_id, handlers);
959
960        core.add_subscribed_strategy(strategy_id);
961        log::info!("Subscribed to events for strategy {strategy_id}");
962    }
963
964    /// Unsubscribes from all strategy event handlers.
965    ///
966    /// This should be called before reset to properly clean up msgbus subscriptions.
967    fn unsubscribe_all_strategy_events(&mut self) {
968        let handlers = self.core_mut().take_strategy_event_handlers();
969        for (strategy_id, h) in handlers {
970            msgbus::unsubscribe_order_events(h.order_topic.into(), &h.order_handler);
971            msgbus::unsubscribe_position_events(h.position_topic.into(), &h.position_handler);
972            log::info!("Unsubscribed from events for strategy {strategy_id}");
973        }
974        self.core_mut().clear_subscribed_strategies();
975    }
976
977    /// Handles an order event, filtering for algorithm-owned orders.
978    fn handle_order_event(&mut self, event: OrderEventAny) {
979        if self.core_mut().state() != ComponentState::Running {
980            return;
981        }
982
983        let order = {
984            let cache = self.core_mut().cache();
985            cache.order(&event.client_order_id()).cloned()
986        };
987
988        let Some(order) = order else {
989            return;
990        };
991
992        let Some(order_algo_id) = order.exec_algorithm_id() else {
993            return;
994        };
995
996        if order_algo_id != self.id() {
997            return;
998        }
999
1000        {
1001            let core = self.core_mut();
1002            if core.config.log_events {
1003                let id = &core.actor.actor_id;
1004                log::info!("{id} {RECV}{EVT} {event}");
1005            }
1006        }
1007
1008        match &event {
1009            OrderEventAny::Initialized(e) => self.on_order_initialized(e.clone()),
1010            OrderEventAny::Denied(e) => {
1011                self.restore_primary_order_quantity(&order);
1012                self.on_order_denied(*e);
1013            }
1014            OrderEventAny::Emulated(e) => self.on_order_emulated(*e),
1015            OrderEventAny::Released(e) => self.on_order_released(*e),
1016            OrderEventAny::Submitted(e) => self.on_order_submitted(*e),
1017            OrderEventAny::Rejected(e) => {
1018                self.restore_primary_order_quantity(&order);
1019                self.on_order_rejected(*e);
1020            }
1021            OrderEventAny::Accepted(e) => {
1022                // Commit reduction - order accepted by venue
1023                self.core_mut()
1024                    .take_pending_spawn_reduction(&order.client_order_id());
1025                self.on_order_accepted(*e);
1026            }
1027            OrderEventAny::Canceled(e) => {
1028                self.core_mut()
1029                    .take_pending_spawn_reduction(&order.client_order_id());
1030                self.on_algo_order_canceled(*e);
1031            }
1032            OrderEventAny::Expired(e) => {
1033                self.core_mut()
1034                    .take_pending_spawn_reduction(&order.client_order_id());
1035                self.on_order_expired(*e);
1036            }
1037            OrderEventAny::Triggered(e) => self.on_order_triggered(*e),
1038            OrderEventAny::PendingUpdate(e) => self.on_order_pending_update(*e),
1039            OrderEventAny::PendingCancel(e) => self.on_order_pending_cancel(*e),
1040            OrderEventAny::ModifyRejected(e) => self.on_order_modify_rejected(*e),
1041            OrderEventAny::CancelRejected(e) => self.on_order_cancel_rejected(*e),
1042            OrderEventAny::Updated(e) => self.on_order_updated(*e),
1043            OrderEventAny::Filled(e) => self.on_algo_order_filled(*e),
1044        }
1045
1046        self.on_order_event(event);
1047    }
1048
1049    /// Handles a position event.
1050    fn handle_position_event(&mut self, event: PositionEvent) {
1051        if self.core_mut().state() != ComponentState::Running {
1052            return;
1053        }
1054
1055        {
1056            let core = self.core_mut();
1057            if core.config.log_events {
1058                let id = &core.actor.actor_id;
1059                log::info!("{id} {RECV}{EVT} {event:?}");
1060            }
1061        }
1062
1063        match &event {
1064            PositionEvent::PositionOpened(e) => self.on_position_opened(e.clone()),
1065            PositionEvent::PositionChanged(e) => self.on_position_changed(e.clone()),
1066            PositionEvent::PositionClosed(e) => self.on_position_closed(e.clone()),
1067            PositionEvent::PositionAdjusted(_) => {}
1068        }
1069
1070        self.on_position_event(event);
1071    }
1072
1073    /// Called when the algorithm is started.
1074    ///
1075    /// Override this method to implement custom initialization logic.
1076    ///
1077    /// # Errors
1078    ///
1079    /// Returns an error if start fails.
1080    fn on_start(&mut self) -> anyhow::Result<()> {
1081        let id = self.id();
1082        log::info!("Starting {id}");
1083        Ok(())
1084    }
1085
1086    /// Called when the algorithm is stopped.
1087    ///
1088    /// # Errors
1089    ///
1090    /// Returns an error if stop fails.
1091    fn on_stop(&mut self) -> anyhow::Result<()> {
1092        Ok(())
1093    }
1094
1095    /// Called when the algorithm is reset.
1096    ///
1097    /// # Errors
1098    ///
1099    /// Returns an error if reset fails.
1100    fn on_reset(&mut self) -> anyhow::Result<()> {
1101        self.unsubscribe_all_strategy_events();
1102        self.core_mut().reset();
1103        Ok(())
1104    }
1105
1106    /// Called when a time event is received.
1107    ///
1108    /// Override this method for timer-based algorithms like TWAP.
1109    ///
1110    /// # Errors
1111    ///
1112    /// Returns an error if time event handling fails.
1113    fn on_time_event(&mut self, _event: &TimeEvent) -> anyhow::Result<()> {
1114        Ok(())
1115    }
1116
1117    /// Called when an order is initialized.
1118    #[allow(unused_variables)]
1119    fn on_order_initialized(&mut self, event: OrderInitialized) {}
1120
1121    /// Called when an order is denied.
1122    #[allow(unused_variables)]
1123    fn on_order_denied(&mut self, event: OrderDenied) {}
1124
1125    /// Called when an order is emulated.
1126    #[allow(unused_variables)]
1127    fn on_order_emulated(&mut self, event: OrderEmulated) {}
1128
1129    /// Called when an order is released from emulation.
1130    #[allow(unused_variables)]
1131    fn on_order_released(&mut self, event: OrderReleased) {}
1132
1133    /// Called when an order is submitted.
1134    #[allow(unused_variables)]
1135    fn on_order_submitted(&mut self, event: OrderSubmitted) {}
1136
1137    /// Called when an order is rejected.
1138    #[allow(unused_variables)]
1139    fn on_order_rejected(&mut self, event: OrderRejected) {}
1140
1141    /// Called when an order is accepted.
1142    #[allow(unused_variables)]
1143    fn on_order_accepted(&mut self, event: OrderAccepted) {}
1144
1145    /// Called when an order is canceled.
1146    #[allow(unused_variables)]
1147    fn on_algo_order_canceled(&mut self, event: OrderCanceled) {}
1148
1149    /// Called when an order expires.
1150    #[allow(unused_variables)]
1151    fn on_order_expired(&mut self, event: OrderExpired) {}
1152
1153    /// Called when an order is triggered.
1154    #[allow(unused_variables)]
1155    fn on_order_triggered(&mut self, event: OrderTriggered) {}
1156
1157    /// Called when an order modification is pending.
1158    #[allow(unused_variables)]
1159    fn on_order_pending_update(&mut self, event: OrderPendingUpdate) {}
1160
1161    /// Called when an order cancellation is pending.
1162    #[allow(unused_variables)]
1163    fn on_order_pending_cancel(&mut self, event: OrderPendingCancel) {}
1164
1165    /// Called when an order modification is rejected.
1166    #[allow(unused_variables)]
1167    fn on_order_modify_rejected(&mut self, event: OrderModifyRejected) {}
1168
1169    /// Called when an order cancellation is rejected.
1170    #[allow(unused_variables)]
1171    fn on_order_cancel_rejected(&mut self, event: OrderCancelRejected) {}
1172
1173    /// Called when an order is updated.
1174    #[allow(unused_variables)]
1175    fn on_order_updated(&mut self, event: OrderUpdated) {}
1176
1177    /// Called when an order is filled.
1178    #[allow(unused_variables)]
1179    fn on_algo_order_filled(&mut self, event: OrderFilled) {}
1180
1181    /// Called for any order event (after specific handler).
1182    #[allow(unused_variables)]
1183    fn on_order_event(&mut self, event: OrderEventAny) {}
1184
1185    /// Called when a position is opened.
1186    #[allow(unused_variables)]
1187    fn on_position_opened(&mut self, event: PositionOpened) {}
1188
1189    /// Called when a position is changed.
1190    #[allow(unused_variables)]
1191    fn on_position_changed(&mut self, event: PositionChanged) {}
1192
1193    /// Called when a position is closed.
1194    #[allow(unused_variables)]
1195    fn on_position_closed(&mut self, event: PositionClosed) {}
1196
1197    /// Called for any position event (after specific handler).
1198    #[allow(unused_variables)]
1199    fn on_position_event(&mut self, event: PositionEvent) {}
1200}
1201
1202#[cfg(test)]
1203mod tests {
1204    use std::{cell::RefCell, rc::Rc};
1205
1206    use nautilus_common::{
1207        actor::DataActor, cache::Cache, clock::TestClock, component::Component,
1208        enums::ComponentTrigger, nautilus_actor,
1209    };
1210    use nautilus_model::{
1211        enums::OrderSide,
1212        events::{
1213            OrderAccepted, OrderCanceled, OrderDenied, OrderRejected, order::spec::OrderFilledSpec,
1214        },
1215        identifiers::{
1216            AccountId, ClientOrderId, ExecAlgorithmId, InstrumentId, StrategyId, TraderId,
1217            VenueOrderId,
1218        },
1219        orders::{LimitOrder, MarketOrder, OrderAny, stubs::TestOrderStubs},
1220        types::{Price, Quantity},
1221    };
1222    use rstest::rstest;
1223
1224    use super::*;
1225
1226    #[derive(Debug)]
1227    struct TestAlgorithm {
1228        core: ExecutionAlgorithmCore,
1229        on_order_called: bool,
1230        last_order_client_id: Option<ClientOrderId>,
1231    }
1232
1233    impl TestAlgorithm {
1234        fn new(config: ExecutionAlgorithmConfig) -> Self {
1235            Self {
1236                core: ExecutionAlgorithmCore::new(config),
1237                on_order_called: false,
1238                last_order_client_id: None,
1239            }
1240        }
1241    }
1242
1243    impl DataActor for TestAlgorithm {}
1244
1245    nautilus_actor!(TestAlgorithm);
1246
1247    impl ExecutionAlgorithm for TestAlgorithm {
1248        fn core_mut(&mut self) -> &mut ExecutionAlgorithmCore {
1249            &mut self.core
1250        }
1251
1252        fn on_order(&mut self, order: OrderAny) -> anyhow::Result<()> {
1253            self.on_order_called = true;
1254            self.last_order_client_id = Some(order.client_order_id());
1255            Ok(())
1256        }
1257    }
1258
1259    fn create_test_algorithm() -> TestAlgorithm {
1260        // Use unique ID to avoid thread-local registry/msgbus conflicts in parallel tests
1261        let unique_id = format!("TEST-{}", UUID4::new());
1262        let config = ExecutionAlgorithmConfig {
1263            exec_algorithm_id: Some(ExecAlgorithmId::new(&unique_id)),
1264            ..Default::default()
1265        };
1266        TestAlgorithm::new(config)
1267    }
1268
1269    fn register_algorithm(algo: &mut TestAlgorithm) {
1270        let trader_id = TraderId::from("TRADER-001");
1271        let clock = Rc::new(RefCell::new(TestClock::new()));
1272        let cache = Rc::new(RefCell::new(Cache::default()));
1273
1274        algo.core.register(trader_id, clock, cache).unwrap();
1275
1276        // Transition to Running state for tests
1277        algo.transition_state(ComponentTrigger::Initialize).unwrap();
1278        algo.transition_state(ComponentTrigger::Start).unwrap();
1279        algo.transition_state(ComponentTrigger::StartCompleted)
1280            .unwrap();
1281    }
1282
1283    #[rstest]
1284    fn test_algorithm_creation() {
1285        let algo = create_test_algorithm();
1286        assert!(algo.core.exec_algorithm_id.inner().starts_with("TEST-"));
1287        assert!(!algo.on_order_called);
1288        assert!(algo.last_order_client_id.is_none());
1289    }
1290
1291    #[rstest]
1292    fn test_algorithm_registration() {
1293        let mut algo = create_test_algorithm();
1294        register_algorithm(&mut algo);
1295
1296        assert!(algo.core.trader_id().is_some());
1297        assert_eq!(algo.core.trader_id(), Some(TraderId::from("TRADER-001")));
1298    }
1299
1300    #[rstest]
1301    fn test_algorithm_id() {
1302        let mut algo = create_test_algorithm();
1303        assert!(algo.id().inner().starts_with("TEST-"));
1304    }
1305
1306    #[rstest]
1307    fn test_algorithm_spawn_market_creates_valid_order() {
1308        let mut algo = create_test_algorithm();
1309        register_algorithm(&mut algo);
1310
1311        let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
1312        let mut primary = OrderAny::Market(MarketOrder::new(
1313            TraderId::from("TRADER-001"),
1314            StrategyId::from("STRAT-001"),
1315            instrument_id,
1316            ClientOrderId::from("O-001"),
1317            OrderSide::Buy,
1318            Quantity::from("1.0"),
1319            TimeInForce::Gtc,
1320            UUID4::new(),
1321            0.into(),
1322            false, // reduce_only
1323            false, // quote_quantity
1324            None,  // contingency_type
1325            None,  // order_list_id
1326            None,  // linked_order_ids
1327            None,  // parent_order_id
1328            None,  // exec_algorithm_id
1329            None,  // exec_algorithm_params
1330            None,  // exec_spawn_id
1331            None,  // tags
1332        ));
1333
1334        let spawned = algo.spawn_market(
1335            &mut primary,
1336            Quantity::from("0.5"),
1337            TimeInForce::Ioc,
1338            false,
1339            None,  // tags
1340            false, // reduce_primary
1341        );
1342
1343        assert_eq!(spawned.client_order_id.as_str(), "O-001-E1");
1344        assert_eq!(spawned.instrument_id, instrument_id);
1345        assert_eq!(spawned.order_side(), OrderSide::Buy);
1346        assert_eq!(spawned.quantity, Quantity::from("0.5"));
1347        assert_eq!(spawned.time_in_force, TimeInForce::Ioc);
1348        assert_eq!(spawned.exec_algorithm_id, Some(algo.id()));
1349        assert_eq!(spawned.exec_spawn_id, Some(ClientOrderId::from("O-001")));
1350    }
1351
1352    #[rstest]
1353    fn test_algorithm_spawn_increments_sequence() {
1354        let mut algo = create_test_algorithm();
1355        register_algorithm(&mut algo);
1356
1357        let mut primary = OrderAny::Market(MarketOrder::new(
1358            TraderId::from("TRADER-001"),
1359            StrategyId::from("STRAT-001"),
1360            InstrumentId::from("BTC/USDT.BINANCE"),
1361            ClientOrderId::from("O-001"),
1362            OrderSide::Buy,
1363            Quantity::from("1.0"),
1364            TimeInForce::Gtc,
1365            UUID4::new(),
1366            0.into(),
1367            false,
1368            false,
1369            None,
1370            None,
1371            None,
1372            None,
1373            None,
1374            None,
1375            None,
1376            None,
1377        ));
1378
1379        let spawned1 = algo.spawn_market(
1380            &mut primary,
1381            Quantity::from("0.25"),
1382            TimeInForce::Ioc,
1383            false,
1384            None,
1385            false,
1386        );
1387        let spawned2 = algo.spawn_market(
1388            &mut primary,
1389            Quantity::from("0.25"),
1390            TimeInForce::Ioc,
1391            false,
1392            None,
1393            false,
1394        );
1395        let spawned3 = algo.spawn_market(
1396            &mut primary,
1397            Quantity::from("0.25"),
1398            TimeInForce::Ioc,
1399            false,
1400            None,
1401            false,
1402        );
1403
1404        assert_eq!(spawned1.client_order_id.as_str(), "O-001-E1");
1405        assert_eq!(spawned2.client_order_id.as_str(), "O-001-E2");
1406        assert_eq!(spawned3.client_order_id.as_str(), "O-001-E3");
1407    }
1408
1409    #[rstest]
1410    fn test_algorithm_default_handlers_do_not_panic() {
1411        let mut algo = create_test_algorithm();
1412
1413        algo.on_order_initialized(OrderInitialized::default());
1414        algo.on_order_denied(OrderDenied::default());
1415        algo.on_order_emulated(OrderEmulated::default());
1416        algo.on_order_released(OrderReleased::default());
1417        algo.on_order_submitted(OrderSubmitted::default());
1418        algo.on_order_rejected(OrderRejected::default());
1419        algo.on_order_accepted(OrderAccepted::default());
1420        algo.on_algo_order_canceled(OrderCanceled::default());
1421        algo.on_order_expired(OrderExpired::default());
1422        algo.on_order_triggered(OrderTriggered::default());
1423        algo.on_order_pending_update(OrderPendingUpdate::default());
1424        algo.on_order_pending_cancel(OrderPendingCancel::default());
1425        algo.on_order_modify_rejected(OrderModifyRejected::default());
1426        algo.on_order_cancel_rejected(OrderCancelRejected::default());
1427        algo.on_order_updated(OrderUpdated::default());
1428        algo.on_algo_order_filled(OrderFilledSpec::builder().build());
1429    }
1430
1431    #[rstest]
1432    fn test_strategy_subscription_tracking() {
1433        let mut algo = create_test_algorithm();
1434        let strategy_id = StrategyId::from("STRAT-001");
1435
1436        assert!(!algo.core.is_strategy_subscribed(&strategy_id));
1437
1438        algo.subscribe_to_strategy_events(strategy_id);
1439        assert!(algo.core.is_strategy_subscribed(&strategy_id));
1440
1441        // Second call should be idempotent
1442        algo.subscribe_to_strategy_events(strategy_id);
1443        assert!(algo.core.is_strategy_subscribed(&strategy_id));
1444    }
1445
1446    #[rstest]
1447    fn test_algorithm_reset() {
1448        let mut algo = create_test_algorithm();
1449        let strategy_id = StrategyId::from("STRAT-001");
1450        let primary_id = ClientOrderId::new("O-001");
1451
1452        let _ = algo.core.spawn_client_order_id(&primary_id);
1453        algo.core.add_subscribed_strategy(strategy_id);
1454
1455        assert!(algo.core.spawn_sequence(&primary_id).is_some());
1456        assert!(algo.core.is_strategy_subscribed(&strategy_id));
1457
1458        ExecutionAlgorithm::on_reset(&mut algo).unwrap();
1459
1460        assert!(algo.core.spawn_sequence(&primary_id).is_none());
1461        assert!(!algo.core.is_strategy_subscribed(&strategy_id));
1462    }
1463
1464    #[rstest]
1465    fn test_algorithm_spawn_limit_creates_valid_order() {
1466        let mut algo = create_test_algorithm();
1467        register_algorithm(&mut algo);
1468
1469        let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
1470        let mut primary = OrderAny::Market(MarketOrder::new(
1471            TraderId::from("TRADER-001"),
1472            StrategyId::from("STRAT-001"),
1473            instrument_id,
1474            ClientOrderId::from("O-001"),
1475            OrderSide::Buy,
1476            Quantity::from("1.0"),
1477            TimeInForce::Gtc,
1478            UUID4::new(),
1479            0.into(),
1480            false,
1481            false,
1482            None,
1483            None,
1484            None,
1485            None,
1486            None,
1487            None,
1488            None,
1489            None,
1490        ));
1491
1492        let price = Price::from("50000.0");
1493        let spawned = algo.spawn_limit(
1494            &mut primary,
1495            Quantity::from("0.5"),
1496            price,
1497            TimeInForce::Gtc,
1498            None,  // expire_time
1499            false, // post_only
1500            false, // reduce_only
1501            None,  // display_qty
1502            None,  // emulation_trigger
1503            None,  // tags
1504            false, // reduce_primary
1505        );
1506
1507        assert_eq!(spawned.client_order_id.as_str(), "O-001-E1");
1508        assert_eq!(spawned.instrument_id, instrument_id);
1509        assert_eq!(spawned.order_side(), OrderSide::Buy);
1510        assert_eq!(spawned.quantity, Quantity::from("0.5"));
1511        assert_eq!(spawned.price, price);
1512        assert_eq!(spawned.time_in_force, TimeInForce::Gtc);
1513        assert_eq!(spawned.exec_algorithm_id, Some(algo.id()));
1514        assert_eq!(spawned.exec_spawn_id, Some(ClientOrderId::from("O-001")));
1515    }
1516
1517    #[rstest]
1518    fn test_algorithm_spawn_market_to_limit_creates_valid_order() {
1519        let mut algo = create_test_algorithm();
1520        register_algorithm(&mut algo);
1521
1522        let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
1523        let mut primary = OrderAny::Market(MarketOrder::new(
1524            TraderId::from("TRADER-001"),
1525            StrategyId::from("STRAT-001"),
1526            instrument_id,
1527            ClientOrderId::from("O-001"),
1528            OrderSide::Buy,
1529            Quantity::from("1.0"),
1530            TimeInForce::Gtc,
1531            UUID4::new(),
1532            0.into(),
1533            false,
1534            false,
1535            None,
1536            None,
1537            None,
1538            None,
1539            None,
1540            None,
1541            None,
1542            None,
1543        ));
1544
1545        let spawned = algo.spawn_market_to_limit(
1546            &mut primary,
1547            Quantity::from("0.5"),
1548            TimeInForce::Gtc,
1549            None,  // expire_time
1550            false, // reduce_only
1551            None,  // display_qty
1552            None,  // emulation_trigger
1553            None,  // tags
1554            false, // reduce_primary
1555        );
1556
1557        assert_eq!(spawned.client_order_id.as_str(), "O-001-E1");
1558        assert_eq!(spawned.instrument_id, instrument_id);
1559        assert_eq!(spawned.order_side(), OrderSide::Buy);
1560        assert_eq!(spawned.quantity, Quantity::from("0.5"));
1561        assert_eq!(spawned.time_in_force, TimeInForce::Gtc);
1562        assert_eq!(spawned.exec_algorithm_id, Some(algo.id()));
1563        assert_eq!(spawned.exec_spawn_id, Some(ClientOrderId::from("O-001")));
1564    }
1565
1566    #[rstest]
1567    fn test_algorithm_spawn_market_with_tags() {
1568        let mut algo = create_test_algorithm();
1569        register_algorithm(&mut algo);
1570
1571        let mut primary = OrderAny::Market(MarketOrder::new(
1572            TraderId::from("TRADER-001"),
1573            StrategyId::from("STRAT-001"),
1574            InstrumentId::from("BTC/USDT.BINANCE"),
1575            ClientOrderId::from("O-001"),
1576            OrderSide::Buy,
1577            Quantity::from("1.0"),
1578            TimeInForce::Gtc,
1579            UUID4::new(),
1580            0.into(),
1581            false,
1582            false,
1583            None,
1584            None,
1585            None,
1586            None,
1587            None,
1588            None,
1589            None,
1590            None,
1591        ));
1592
1593        let tags = vec![ustr::Ustr::from("TAG1"), ustr::Ustr::from("TAG2")];
1594        let spawned = algo.spawn_market(
1595            &mut primary,
1596            Quantity::from("0.5"),
1597            TimeInForce::Ioc,
1598            false,
1599            Some(tags.clone()),
1600            false,
1601        );
1602
1603        assert_eq!(spawned.tags, Some(tags));
1604    }
1605
1606    #[rstest]
1607    fn test_algorithm_spawn_propagates_primary_fields() {
1608        let mut algo = create_test_algorithm();
1609        register_algorithm(&mut algo);
1610
1611        let mut params = indexmap::IndexMap::new();
1612        params.insert(ustr::Ustr::from("horizon_secs"), ustr::Ustr::from("30"));
1613        params.insert(ustr::Ustr::from("interval_secs"), ustr::Ustr::from("10"));
1614        let primary_tags = vec![ustr::Ustr::from("PRIMARY_TAG")];
1615        let linked_order_ids = vec![ClientOrderId::from("LINK-1")];
1616
1617        let mut primary = OrderAny::Market(MarketOrder::new(
1618            TraderId::from("TRADER-001"),
1619            StrategyId::from("STRAT-001"),
1620            InstrumentId::from("BTC/USDT.BINANCE"),
1621            ClientOrderId::from("O-001"),
1622            OrderSide::Buy,
1623            Quantity::from("1.0"),
1624            TimeInForce::Gtc,
1625            UUID4::new(),
1626            0.into(),
1627            false, // reduce_only
1628            true,  // quote_quantity
1629            None,  // contingency_type
1630            None,  // order_list_id
1631            Some(linked_order_ids.clone()),
1632            None, // parent_order_id
1633            Some(algo.id()),
1634            Some(params.clone()),
1635            None, // exec_spawn_id
1636            Some(primary_tags.clone()),
1637        ));
1638
1639        let spawned_market = algo.spawn_market(
1640            &mut primary,
1641            Quantity::from("0.25"),
1642            TimeInForce::Ioc,
1643            false,
1644            None, // falls back to primary.tags
1645            false,
1646        );
1647        assert!(spawned_market.is_quote_quantity);
1648        assert_eq!(spawned_market.exec_algorithm_params, Some(params.clone()));
1649        assert_eq!(spawned_market.tags, Some(primary_tags.clone()));
1650        assert_eq!(
1651            spawned_market.linked_order_ids,
1652            Some(linked_order_ids.clone())
1653        );
1654
1655        let spawned_limit = algo.spawn_limit(
1656            &mut primary,
1657            Quantity::from("0.25"),
1658            Price::from("50000.0"),
1659            TimeInForce::Gtc,
1660            None,  // expire_time
1661            false, // post_only
1662            false, // reduce_only
1663            None,  // display_qty
1664            None,  // emulation_trigger
1665            None,  // falls back to primary.tags
1666            false,
1667        );
1668        assert!(spawned_limit.is_quote_quantity);
1669        assert_eq!(spawned_limit.exec_algorithm_params, Some(params.clone()));
1670        assert_eq!(spawned_limit.tags, Some(primary_tags.clone()));
1671        assert_eq!(
1672            spawned_limit.linked_order_ids,
1673            Some(linked_order_ids.clone())
1674        );
1675
1676        let spawned_mtl = algo.spawn_market_to_limit(
1677            &mut primary,
1678            Quantity::from("0.25"),
1679            TimeInForce::Gtc,
1680            None,  // expire_time
1681            false, // reduce_only
1682            None,  // display_qty
1683            None,  // emulation_trigger
1684            None,  // falls back to primary.tags
1685            false,
1686        );
1687        assert!(spawned_mtl.is_quote_quantity);
1688        assert_eq!(spawned_mtl.exec_algorithm_params, Some(params));
1689        assert_eq!(spawned_mtl.tags, Some(primary_tags));
1690        assert_eq!(spawned_mtl.linked_order_ids, Some(linked_order_ids));
1691    }
1692
1693    #[rstest]
1694    fn test_algorithm_reduce_primary_order() {
1695        let mut algo = create_test_algorithm();
1696        register_algorithm(&mut algo);
1697
1698        let order = OrderAny::Market(MarketOrder::new(
1699            TraderId::from("TRADER-001"),
1700            StrategyId::from("STRAT-001"),
1701            InstrumentId::from("BTC/USDT.BINANCE"),
1702            ClientOrderId::from("O-001"),
1703            OrderSide::Buy,
1704            Quantity::from("1.0"),
1705            TimeInForce::Gtc,
1706            UUID4::new(),
1707            0.into(),
1708            false,
1709            false,
1710            None,
1711            None,
1712            None,
1713            None,
1714            None,
1715            None,
1716            None,
1717            None,
1718        ));
1719
1720        // Make accepted so OrderUpdated can be applied
1721        let mut primary = TestOrderStubs::make_accepted_order(&order);
1722
1723        {
1724            let cache_rc = algo.core.cache_rc();
1725            let mut cache = cache_rc.borrow_mut();
1726            cache.add_order(primary.clone(), None, None, false).unwrap();
1727        }
1728
1729        let spawn_qty = Quantity::from("0.3");
1730        algo.reduce_primary_order(&mut primary, spawn_qty);
1731
1732        assert_eq!(primary.quantity(), Quantity::from("0.7"));
1733    }
1734
1735    #[rstest]
1736    fn test_algorithm_spawn_market_with_reduce_primary() {
1737        let mut algo = create_test_algorithm();
1738        register_algorithm(&mut algo);
1739
1740        let order = OrderAny::Market(MarketOrder::new(
1741            TraderId::from("TRADER-001"),
1742            StrategyId::from("STRAT-001"),
1743            InstrumentId::from("BTC/USDT.BINANCE"),
1744            ClientOrderId::from("O-001"),
1745            OrderSide::Buy,
1746            Quantity::from("1.0"),
1747            TimeInForce::Gtc,
1748            UUID4::new(),
1749            0.into(),
1750            false,
1751            false,
1752            None,
1753            None,
1754            None,
1755            None,
1756            None,
1757            None,
1758            None,
1759            None,
1760        ));
1761
1762        // Make accepted so OrderUpdated can be applied
1763        let mut primary = TestOrderStubs::make_accepted_order(&order);
1764
1765        {
1766            let cache_rc = algo.core.cache_rc();
1767            let mut cache = cache_rc.borrow_mut();
1768            cache.add_order(primary.clone(), None, None, false).unwrap();
1769        }
1770
1771        let spawned = algo.spawn_market(
1772            &mut primary,
1773            Quantity::from("0.4"),
1774            TimeInForce::Ioc,
1775            false,
1776            None,
1777            true, // reduce_primary = true
1778        );
1779
1780        assert_eq!(spawned.quantity, Quantity::from("0.4"));
1781        assert_eq!(primary.quantity(), Quantity::from("0.6"));
1782    }
1783
1784    #[rstest]
1785    fn test_algorithm_generate_order_canceled() {
1786        let mut algo = create_test_algorithm();
1787        register_algorithm(&mut algo);
1788
1789        let order = OrderAny::Market(MarketOrder::new(
1790            TraderId::from("TRADER-001"),
1791            StrategyId::from("STRAT-001"),
1792            InstrumentId::from("BTC/USDT.BINANCE"),
1793            ClientOrderId::from("O-001"),
1794            OrderSide::Buy,
1795            Quantity::from("1.0"),
1796            TimeInForce::Gtc,
1797            UUID4::new(),
1798            0.into(),
1799            false,
1800            false,
1801            None,
1802            None,
1803            None,
1804            None,
1805            None,
1806            None,
1807            None,
1808            None,
1809        ));
1810
1811        let event = algo.generate_order_canceled(&order);
1812
1813        assert_eq!(event.trader_id, TraderId::from("TRADER-001"));
1814        assert_eq!(event.strategy_id, StrategyId::from("STRAT-001"));
1815        assert_eq!(event.instrument_id, InstrumentId::from("BTC/USDT.BINANCE"));
1816        assert_eq!(event.client_order_id, ClientOrderId::from("O-001"));
1817    }
1818
1819    #[rstest]
1820    fn test_algorithm_modify_order_in_place_updates_quantity() {
1821        let mut algo = create_test_algorithm();
1822        register_algorithm(&mut algo);
1823
1824        let mut order = OrderAny::Limit(LimitOrder::new(
1825            TraderId::from("TRADER-001"),
1826            StrategyId::from("STRAT-001"),
1827            InstrumentId::from("BTC/USDT.BINANCE"),
1828            ClientOrderId::from("O-001"),
1829            OrderSide::Buy,
1830            Quantity::from("1.0"),
1831            Price::from("50000.0"),
1832            TimeInForce::Gtc,
1833            None,  // expire_time
1834            false, // post_only
1835            false, // reduce_only
1836            false, // quote_quantity
1837            None,  // display_qty
1838            None,  // emulation_trigger
1839            None,  // trigger_instrument_id
1840            None,  // contingency_type
1841            None,  // order_list_id
1842            None,  // linked_order_ids
1843            None,  // parent_order_id
1844            None,  // exec_algorithm_id
1845            None,  // exec_algorithm_params
1846            None,  // exec_spawn_id
1847            None,  // tags
1848            UUID4::new(),
1849            0.into(),
1850        ));
1851
1852        {
1853            let cache_rc = algo.core.cache_rc();
1854            let mut cache = cache_rc.borrow_mut();
1855            cache.add_order(order.clone(), None, None, false).unwrap();
1856        }
1857
1858        let new_qty = Quantity::from("0.5");
1859        algo.modify_order_in_place(&mut order, Some(new_qty), None, None)
1860            .unwrap();
1861
1862        assert_eq!(order.quantity(), new_qty);
1863    }
1864
1865    #[rstest]
1866    fn test_algorithm_modify_order_in_place_rejects_no_changes() {
1867        let mut algo = create_test_algorithm();
1868        register_algorithm(&mut algo);
1869
1870        let mut order = OrderAny::Limit(LimitOrder::new(
1871            TraderId::from("TRADER-001"),
1872            StrategyId::from("STRAT-001"),
1873            InstrumentId::from("BTC/USDT.BINANCE"),
1874            ClientOrderId::from("O-001"),
1875            OrderSide::Buy,
1876            Quantity::from("1.0"),
1877            Price::from("50000.0"),
1878            TimeInForce::Gtc,
1879            None,
1880            false,
1881            false,
1882            false,
1883            None,
1884            None,
1885            None,
1886            None,
1887            None,
1888            None,
1889            None,
1890            None,
1891            None,
1892            None,
1893            None,
1894            UUID4::new(),
1895            0.into(),
1896        ));
1897
1898        // Try to modify with same quantity - should fail
1899        let result =
1900            algo.modify_order_in_place(&mut order, Some(Quantity::from("1.0")), None, None);
1901
1902        assert!(result.is_err());
1903        assert!(
1904            result
1905                .unwrap_err()
1906                .to_string()
1907                .contains("no parameters differ")
1908        );
1909    }
1910
1911    #[rstest]
1912    fn test_spawned_order_denied_restores_primary_quantity() {
1913        let mut algo = create_test_algorithm();
1914        register_algorithm(&mut algo);
1915
1916        let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
1917        let exec_algorithm_id = algo.id();
1918
1919        let mut primary = OrderAny::Market(MarketOrder::new(
1920            TraderId::from("TRADER-001"),
1921            StrategyId::from("STRAT-001"),
1922            instrument_id,
1923            ClientOrderId::from("O-001"),
1924            OrderSide::Buy,
1925            Quantity::from("1.0"),
1926            TimeInForce::Gtc,
1927            UUID4::new(),
1928            0.into(),
1929            false,
1930            false,
1931            None,
1932            None,
1933            None,
1934            None,
1935            Some(exec_algorithm_id),
1936            None,
1937            None,
1938            None,
1939        ));
1940
1941        {
1942            let cache_rc = algo.core.cache_rc();
1943            let mut cache = cache_rc.borrow_mut();
1944            cache.add_order(primary.clone(), None, None, false).unwrap();
1945        }
1946
1947        let spawned = algo.spawn_market(
1948            &mut primary,
1949            Quantity::from("0.5"),
1950            TimeInForce::Fok,
1951            false,
1952            None,
1953            true,
1954        );
1955
1956        {
1957            let cache_rc = algo.core.cache_rc();
1958            let mut cache = cache_rc.borrow_mut();
1959            cache.update_order(&primary).unwrap();
1960        }
1961
1962        assert_eq!(primary.quantity(), Quantity::from("0.5"));
1963
1964        let mut spawned_order = OrderAny::Market(spawned);
1965        {
1966            let cache_rc = algo.core.cache_rc();
1967            let mut cache = cache_rc.borrow_mut();
1968            cache
1969                .add_order(spawned_order.clone(), None, None, false)
1970                .unwrap();
1971        }
1972
1973        let denied = OrderDenied::new(
1974            spawned_order.trader_id(),
1975            spawned_order.strategy_id(),
1976            spawned_order.instrument_id(),
1977            spawned_order.client_order_id(),
1978            "TEST_DENIAL".into(),
1979            UUID4::new(),
1980            0.into(),
1981            0.into(),
1982        );
1983
1984        spawned_order.apply(OrderEventAny::Denied(denied)).unwrap();
1985        {
1986            let cache_rc = algo.core.cache_rc();
1987            let mut cache = cache_rc.borrow_mut();
1988            cache.update_order(&spawned_order).unwrap();
1989        }
1990
1991        algo.handle_order_event(OrderEventAny::Denied(denied));
1992
1993        let restored_primary = {
1994            let cache = algo.core.cache();
1995            cache.order(&ClientOrderId::from("O-001")).cloned().unwrap()
1996        };
1997        assert_eq!(restored_primary.quantity(), Quantity::from("1.0"));
1998    }
1999
2000    #[rstest]
2001    fn test_spawned_order_rejected_restores_primary_quantity() {
2002        let mut algo = create_test_algorithm();
2003        register_algorithm(&mut algo);
2004
2005        let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
2006        let exec_algorithm_id = algo.id();
2007
2008        let mut primary = OrderAny::Market(MarketOrder::new(
2009            TraderId::from("TRADER-001"),
2010            StrategyId::from("STRAT-001"),
2011            instrument_id,
2012            ClientOrderId::from("O-001"),
2013            OrderSide::Buy,
2014            Quantity::from("1.0"),
2015            TimeInForce::Gtc,
2016            UUID4::new(),
2017            0.into(),
2018            false,
2019            false,
2020            None,
2021            None,
2022            None,
2023            None,
2024            Some(exec_algorithm_id),
2025            None,
2026            None,
2027            None,
2028        ));
2029
2030        {
2031            let cache_rc = algo.core.cache_rc();
2032            let mut cache = cache_rc.borrow_mut();
2033            cache.add_order(primary.clone(), None, None, false).unwrap();
2034        }
2035
2036        let spawned = algo.spawn_market(
2037            &mut primary,
2038            Quantity::from("0.5"),
2039            TimeInForce::Fok,
2040            false,
2041            None,
2042            true,
2043        );
2044
2045        {
2046            let cache_rc = algo.core.cache_rc();
2047            let mut cache = cache_rc.borrow_mut();
2048            cache.update_order(&primary).unwrap();
2049        }
2050
2051        assert_eq!(primary.quantity(), Quantity::from("0.5"));
2052
2053        let mut spawned_order = OrderAny::Market(spawned);
2054        {
2055            let cache_rc = algo.core.cache_rc();
2056            let mut cache = cache_rc.borrow_mut();
2057            cache
2058                .add_order(spawned_order.clone(), None, None, false)
2059                .unwrap();
2060        }
2061
2062        let rejected = OrderRejected::new(
2063            spawned_order.trader_id(),
2064            spawned_order.strategy_id(),
2065            spawned_order.instrument_id(),
2066            spawned_order.client_order_id(),
2067            AccountId::from("BINANCE-001"),
2068            "TEST_REJECTION".into(),
2069            UUID4::new(),
2070            0.into(),
2071            0.into(),
2072            false,
2073            false,
2074        );
2075
2076        spawned_order
2077            .apply(OrderEventAny::Rejected(rejected))
2078            .unwrap();
2079        {
2080            let cache_rc = algo.core.cache_rc();
2081            let mut cache = cache_rc.borrow_mut();
2082            cache.update_order(&spawned_order).unwrap();
2083        }
2084
2085        algo.handle_order_event(OrderEventAny::Rejected(rejected));
2086
2087        let restored_primary = {
2088            let cache = algo.core.cache();
2089            cache.order(&ClientOrderId::from("O-001")).cloned().unwrap()
2090        };
2091        assert_eq!(restored_primary.quantity(), Quantity::from("1.0"));
2092    }
2093
2094    #[rstest]
2095    fn test_spawned_order_with_reduce_primary_false_does_not_restore() {
2096        let mut algo = create_test_algorithm();
2097        register_algorithm(&mut algo);
2098
2099        let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
2100        let exec_algorithm_id = algo.id();
2101
2102        let mut primary = OrderAny::Market(MarketOrder::new(
2103            TraderId::from("TRADER-001"),
2104            StrategyId::from("STRAT-001"),
2105            instrument_id,
2106            ClientOrderId::from("O-001"),
2107            OrderSide::Buy,
2108            Quantity::from("1.0"),
2109            TimeInForce::Gtc,
2110            UUID4::new(),
2111            0.into(),
2112            false,
2113            false,
2114            None,
2115            None,
2116            None,
2117            None,
2118            Some(exec_algorithm_id),
2119            None,
2120            None,
2121            None,
2122        ));
2123
2124        {
2125            let cache_rc = algo.core.cache_rc();
2126            let mut cache = cache_rc.borrow_mut();
2127            cache.add_order(primary.clone(), None, None, false).unwrap();
2128        }
2129
2130        let spawned = algo.spawn_market(
2131            &mut primary,
2132            Quantity::from("0.5"),
2133            TimeInForce::Fok,
2134            false,
2135            None,
2136            false,
2137        );
2138
2139        assert_eq!(primary.quantity(), Quantity::from("1.0"));
2140
2141        let mut spawned_order = OrderAny::Market(spawned);
2142        {
2143            let cache_rc = algo.core.cache_rc();
2144            let mut cache = cache_rc.borrow_mut();
2145            cache
2146                .add_order(spawned_order.clone(), None, None, false)
2147                .unwrap();
2148        }
2149
2150        let denied = OrderDenied::new(
2151            spawned_order.trader_id(),
2152            spawned_order.strategy_id(),
2153            spawned_order.instrument_id(),
2154            spawned_order.client_order_id(),
2155            "TEST_DENIAL".into(),
2156            UUID4::new(),
2157            0.into(),
2158            0.into(),
2159        );
2160
2161        spawned_order.apply(OrderEventAny::Denied(denied)).unwrap();
2162        {
2163            let cache_rc = algo.core.cache_rc();
2164            let mut cache = cache_rc.borrow_mut();
2165            cache.update_order(&spawned_order).unwrap();
2166        }
2167
2168        algo.handle_order_event(OrderEventAny::Denied(denied));
2169
2170        let final_primary = {
2171            let cache = algo.core.cache();
2172            cache.order(&ClientOrderId::from("O-001")).cloned().unwrap()
2173        };
2174        assert_eq!(final_primary.quantity(), Quantity::from("1.0"));
2175    }
2176
2177    #[rstest]
2178    fn test_multiple_spawns_with_one_denied_restores_correctly() {
2179        let mut algo = create_test_algorithm();
2180        register_algorithm(&mut algo);
2181
2182        let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
2183        let exec_algorithm_id = algo.id();
2184
2185        let mut primary = OrderAny::Market(MarketOrder::new(
2186            TraderId::from("TRADER-001"),
2187            StrategyId::from("STRAT-001"),
2188            instrument_id,
2189            ClientOrderId::from("O-001"),
2190            OrderSide::Buy,
2191            Quantity::from("1.0"),
2192            TimeInForce::Gtc,
2193            UUID4::new(),
2194            0.into(),
2195            false,
2196            false,
2197            None,
2198            None,
2199            None,
2200            None,
2201            Some(exec_algorithm_id),
2202            None,
2203            None,
2204            None,
2205        ));
2206
2207        {
2208            let cache_rc = algo.core.cache_rc();
2209            let mut cache = cache_rc.borrow_mut();
2210            cache.add_order(primary.clone(), None, None, false).unwrap();
2211        }
2212
2213        let spawned1 = algo.spawn_market(
2214            &mut primary,
2215            Quantity::from("0.3"),
2216            TimeInForce::Fok,
2217            false,
2218            None,
2219            true,
2220        );
2221        {
2222            let cache_rc = algo.core.cache_rc();
2223            let mut cache = cache_rc.borrow_mut();
2224            cache.update_order(&primary).unwrap();
2225        }
2226
2227        let spawned2 = algo.spawn_market(
2228            &mut primary,
2229            Quantity::from("0.4"),
2230            TimeInForce::Fok,
2231            false,
2232            None,
2233            true,
2234        );
2235        {
2236            let cache_rc = algo.core.cache_rc();
2237            let mut cache = cache_rc.borrow_mut();
2238            cache.update_order(&primary).unwrap();
2239        }
2240
2241        assert_eq!(primary.quantity(), Quantity::from("0.3"));
2242
2243        let spawned_order1 = OrderAny::Market(spawned1);
2244        let mut spawned_order2 = OrderAny::Market(spawned2);
2245        {
2246            let cache_rc = algo.core.cache_rc();
2247            let mut cache = cache_rc.borrow_mut();
2248            cache.add_order(spawned_order1, None, None, false).unwrap();
2249            cache
2250                .add_order(spawned_order2.clone(), None, None, false)
2251                .unwrap();
2252        }
2253
2254        let denied = OrderDenied::new(
2255            spawned_order2.trader_id(),
2256            spawned_order2.strategy_id(),
2257            spawned_order2.instrument_id(),
2258            spawned_order2.client_order_id(),
2259            "TEST_DENIAL".into(),
2260            UUID4::new(),
2261            0.into(),
2262            0.into(),
2263        );
2264
2265        spawned_order2.apply(OrderEventAny::Denied(denied)).unwrap();
2266        {
2267            let cache_rc = algo.core.cache_rc();
2268            let mut cache = cache_rc.borrow_mut();
2269            cache.update_order(&spawned_order2).unwrap();
2270        }
2271
2272        algo.handle_order_event(OrderEventAny::Denied(denied));
2273
2274        let restored_primary = {
2275            let cache = algo.core.cache();
2276            cache.order(&ClientOrderId::from("O-001")).cloned().unwrap()
2277        };
2278        assert_eq!(restored_primary.quantity(), Quantity::from("0.7"));
2279    }
2280
2281    #[rstest]
2282    fn test_spawned_order_accepted_prevents_restoration() {
2283        let mut algo = create_test_algorithm();
2284        register_algorithm(&mut algo);
2285
2286        let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
2287        let exec_algorithm_id = algo.id();
2288
2289        let mut primary = OrderAny::Market(MarketOrder::new(
2290            TraderId::from("TRADER-001"),
2291            StrategyId::from("STRAT-001"),
2292            instrument_id,
2293            ClientOrderId::from("O-001"),
2294            OrderSide::Buy,
2295            Quantity::from("1.0"),
2296            TimeInForce::Gtc,
2297            UUID4::new(),
2298            0.into(),
2299            false,
2300            false,
2301            None,
2302            None,
2303            None,
2304            None,
2305            Some(exec_algorithm_id),
2306            None,
2307            None,
2308            None,
2309        ));
2310
2311        {
2312            let cache_rc = algo.core.cache_rc();
2313            let mut cache = cache_rc.borrow_mut();
2314            cache.add_order(primary.clone(), None, None, false).unwrap();
2315        }
2316
2317        let spawned = algo.spawn_market(
2318            &mut primary,
2319            Quantity::from("0.5"),
2320            TimeInForce::Fok,
2321            false,
2322            None,
2323            true,
2324        );
2325
2326        {
2327            let cache_rc = algo.core.cache_rc();
2328            let mut cache = cache_rc.borrow_mut();
2329            cache.update_order(&primary).unwrap();
2330        }
2331
2332        assert_eq!(primary.quantity(), Quantity::from("0.5"));
2333
2334        let mut spawned_order = OrderAny::Market(spawned);
2335        {
2336            let cache_rc = algo.core.cache_rc();
2337            let mut cache = cache_rc.borrow_mut();
2338            cache
2339                .add_order(spawned_order.clone(), None, None, false)
2340                .unwrap();
2341        }
2342
2343        let accepted = OrderAccepted::new(
2344            spawned_order.trader_id(),
2345            spawned_order.strategy_id(),
2346            spawned_order.instrument_id(),
2347            spawned_order.client_order_id(),
2348            VenueOrderId::from("V-123"),
2349            AccountId::from("BINANCE-001"),
2350            UUID4::new(),
2351            0.into(),
2352            0.into(),
2353            false,
2354        );
2355
2356        spawned_order
2357            .apply(OrderEventAny::Accepted(accepted))
2358            .unwrap();
2359        {
2360            let cache_rc = algo.core.cache_rc();
2361            let mut cache = cache_rc.borrow_mut();
2362            cache.update_order(&spawned_order).unwrap();
2363        }
2364
2365        algo.handle_order_event(OrderEventAny::Accepted(accepted));
2366
2367        let primary_after_accept = {
2368            let cache = algo.core.cache();
2369            cache.order(&ClientOrderId::from("O-001")).cloned().unwrap()
2370        };
2371        assert_eq!(primary_after_accept.quantity(), Quantity::from("0.5"));
2372
2373        // Cancel after acceptance - no restoration should occur
2374        let canceled = OrderCanceled::new(
2375            spawned_order.trader_id(),
2376            spawned_order.strategy_id(),
2377            spawned_order.instrument_id(),
2378            spawned_order.client_order_id(),
2379            UUID4::new(),
2380            0.into(),
2381            0.into(),
2382            false,
2383            Some(VenueOrderId::from("V-123")),
2384            Some(AccountId::from("BINANCE-001")),
2385        );
2386
2387        spawned_order
2388            .apply(OrderEventAny::Canceled(canceled))
2389            .unwrap();
2390        {
2391            let cache_rc = algo.core.cache_rc();
2392            let mut cache = cache_rc.borrow_mut();
2393            cache.update_order(&spawned_order).unwrap();
2394        }
2395
2396        algo.handle_order_event(OrderEventAny::Canceled(canceled));
2397
2398        let final_primary = {
2399            let cache = algo.core.cache();
2400            cache.order(&ClientOrderId::from("O-001")).cloned().unwrap()
2401        };
2402        assert_eq!(final_primary.quantity(), Quantity::from("0.5"));
2403    }
2404
2405    #[rstest]
2406    #[should_panic(expected = "exceeds primary leaves_qty")]
2407    fn test_spawn_quantity_exceeds_leaves_qty_panics() {
2408        let mut algo = create_test_algorithm();
2409        register_algorithm(&mut algo);
2410
2411        let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
2412        let exec_algorithm_id = algo.id();
2413
2414        let mut primary = OrderAny::Market(MarketOrder::new(
2415            TraderId::from("TRADER-001"),
2416            StrategyId::from("STRAT-001"),
2417            instrument_id,
2418            ClientOrderId::from("O-001"),
2419            OrderSide::Buy,
2420            Quantity::from("1.0"),
2421            TimeInForce::Gtc,
2422            UUID4::new(),
2423            0.into(),
2424            false,
2425            false,
2426            None,
2427            None,
2428            None,
2429            None,
2430            Some(exec_algorithm_id),
2431            None,
2432            None,
2433            None,
2434        ));
2435
2436        {
2437            let cache_rc = algo.core.cache_rc();
2438            let mut cache = cache_rc.borrow_mut();
2439            cache.add_order(primary.clone(), None, None, false).unwrap();
2440        }
2441
2442        let _ = algo.spawn_market(
2443            &mut primary,
2444            Quantity::from("0.8"),
2445            TimeInForce::Fok,
2446            false,
2447            None,
2448            true,
2449        );
2450
2451        {
2452            let cache_rc = algo.core.cache_rc();
2453            let mut cache = cache_rc.borrow_mut();
2454            cache.update_order(&primary).unwrap();
2455        }
2456
2457        assert_eq!(primary.quantity(), Quantity::from("0.2"));
2458        assert_eq!(primary.leaves_qty(), Quantity::from("0.2"));
2459
2460        // Should panic - spawning 0.5 when only 0.2 leaves_qty remains
2461        let _ = algo.spawn_market(
2462            &mut primary,
2463            Quantity::from("0.5"),
2464            TimeInForce::Fok,
2465            false,
2466            None,
2467            true,
2468        );
2469    }
2470}