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            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            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::{OrderAccepted, OrderCanceled, OrderDenied, OrderRejected},
1213        identifiers::{
1214            AccountId, ClientOrderId, ExecAlgorithmId, InstrumentId, StrategyId, TraderId,
1215            VenueOrderId,
1216        },
1217        orders::{LimitOrder, MarketOrder, OrderAny, stubs::TestOrderStubs},
1218        types::{Price, Quantity},
1219    };
1220    use rstest::rstest;
1221
1222    use super::*;
1223
1224    #[derive(Debug)]
1225    struct TestAlgorithm {
1226        core: ExecutionAlgorithmCore,
1227        on_order_called: bool,
1228        last_order_client_id: Option<ClientOrderId>,
1229    }
1230
1231    impl TestAlgorithm {
1232        fn new(config: ExecutionAlgorithmConfig) -> Self {
1233            Self {
1234                core: ExecutionAlgorithmCore::new(config),
1235                on_order_called: false,
1236                last_order_client_id: None,
1237            }
1238        }
1239    }
1240
1241    impl DataActor for TestAlgorithm {}
1242
1243    nautilus_actor!(TestAlgorithm);
1244
1245    impl ExecutionAlgorithm for TestAlgorithm {
1246        fn core_mut(&mut self) -> &mut ExecutionAlgorithmCore {
1247            &mut self.core
1248        }
1249
1250        fn on_order(&mut self, order: OrderAny) -> anyhow::Result<()> {
1251            self.on_order_called = true;
1252            self.last_order_client_id = Some(order.client_order_id());
1253            Ok(())
1254        }
1255    }
1256
1257    fn create_test_algorithm() -> TestAlgorithm {
1258        // Use unique ID to avoid thread-local registry/msgbus conflicts in parallel tests
1259        let unique_id = format!("TEST-{}", UUID4::new());
1260        let config = ExecutionAlgorithmConfig {
1261            exec_algorithm_id: Some(ExecAlgorithmId::new(&unique_id)),
1262            ..Default::default()
1263        };
1264        TestAlgorithm::new(config)
1265    }
1266
1267    fn register_algorithm(algo: &mut TestAlgorithm) {
1268        let trader_id = TraderId::from("TRADER-001");
1269        let clock = Rc::new(RefCell::new(TestClock::new()));
1270        let cache = Rc::new(RefCell::new(Cache::default()));
1271
1272        algo.core.register(trader_id, clock, cache).unwrap();
1273
1274        // Transition to Running state for tests
1275        algo.transition_state(ComponentTrigger::Initialize).unwrap();
1276        algo.transition_state(ComponentTrigger::Start).unwrap();
1277        algo.transition_state(ComponentTrigger::StartCompleted)
1278            .unwrap();
1279    }
1280
1281    #[rstest]
1282    fn test_algorithm_creation() {
1283        let algo = create_test_algorithm();
1284        assert!(algo.core.exec_algorithm_id.inner().starts_with("TEST-"));
1285        assert!(!algo.on_order_called);
1286        assert!(algo.last_order_client_id.is_none());
1287    }
1288
1289    #[rstest]
1290    fn test_algorithm_registration() {
1291        let mut algo = create_test_algorithm();
1292        register_algorithm(&mut algo);
1293
1294        assert!(algo.core.trader_id().is_some());
1295        assert_eq!(algo.core.trader_id(), Some(TraderId::from("TRADER-001")));
1296    }
1297
1298    #[rstest]
1299    fn test_algorithm_id() {
1300        let mut algo = create_test_algorithm();
1301        assert!(algo.id().inner().starts_with("TEST-"));
1302    }
1303
1304    #[rstest]
1305    fn test_algorithm_spawn_market_creates_valid_order() {
1306        let mut algo = create_test_algorithm();
1307        register_algorithm(&mut algo);
1308
1309        let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
1310        let mut primary = OrderAny::Market(MarketOrder::new(
1311            TraderId::from("TRADER-001"),
1312            StrategyId::from("STRAT-001"),
1313            instrument_id,
1314            ClientOrderId::from("O-001"),
1315            OrderSide::Buy,
1316            Quantity::from("1.0"),
1317            TimeInForce::Gtc,
1318            UUID4::new(),
1319            0.into(),
1320            false, // reduce_only
1321            false, // quote_quantity
1322            None,  // contingency_type
1323            None,  // order_list_id
1324            None,  // linked_order_ids
1325            None,  // parent_order_id
1326            None,  // exec_algorithm_id
1327            None,  // exec_algorithm_params
1328            None,  // exec_spawn_id
1329            None,  // tags
1330        ));
1331
1332        let spawned = algo.spawn_market(
1333            &mut primary,
1334            Quantity::from("0.5"),
1335            TimeInForce::Ioc,
1336            false,
1337            None,  // tags
1338            false, // reduce_primary
1339        );
1340
1341        assert_eq!(spawned.client_order_id.as_str(), "O-001-E1");
1342        assert_eq!(spawned.instrument_id, instrument_id);
1343        assert_eq!(spawned.order_side(), OrderSide::Buy);
1344        assert_eq!(spawned.quantity, Quantity::from("0.5"));
1345        assert_eq!(spawned.time_in_force, TimeInForce::Ioc);
1346        assert_eq!(spawned.exec_algorithm_id, Some(algo.id()));
1347        assert_eq!(spawned.exec_spawn_id, Some(ClientOrderId::from("O-001")));
1348    }
1349
1350    #[rstest]
1351    fn test_algorithm_spawn_increments_sequence() {
1352        let mut algo = create_test_algorithm();
1353        register_algorithm(&mut algo);
1354
1355        let mut primary = OrderAny::Market(MarketOrder::new(
1356            TraderId::from("TRADER-001"),
1357            StrategyId::from("STRAT-001"),
1358            InstrumentId::from("BTC/USDT.BINANCE"),
1359            ClientOrderId::from("O-001"),
1360            OrderSide::Buy,
1361            Quantity::from("1.0"),
1362            TimeInForce::Gtc,
1363            UUID4::new(),
1364            0.into(),
1365            false,
1366            false,
1367            None,
1368            None,
1369            None,
1370            None,
1371            None,
1372            None,
1373            None,
1374            None,
1375        ));
1376
1377        let spawned1 = algo.spawn_market(
1378            &mut primary,
1379            Quantity::from("0.25"),
1380            TimeInForce::Ioc,
1381            false,
1382            None,
1383            false,
1384        );
1385        let spawned2 = algo.spawn_market(
1386            &mut primary,
1387            Quantity::from("0.25"),
1388            TimeInForce::Ioc,
1389            false,
1390            None,
1391            false,
1392        );
1393        let spawned3 = algo.spawn_market(
1394            &mut primary,
1395            Quantity::from("0.25"),
1396            TimeInForce::Ioc,
1397            false,
1398            None,
1399            false,
1400        );
1401
1402        assert_eq!(spawned1.client_order_id.as_str(), "O-001-E1");
1403        assert_eq!(spawned2.client_order_id.as_str(), "O-001-E2");
1404        assert_eq!(spawned3.client_order_id.as_str(), "O-001-E3");
1405    }
1406
1407    #[rstest]
1408    fn test_algorithm_default_handlers_do_not_panic() {
1409        let mut algo = create_test_algorithm();
1410
1411        algo.on_order_initialized(OrderInitialized::default());
1412        algo.on_order_denied(OrderDenied::default());
1413        algo.on_order_emulated(OrderEmulated::default());
1414        algo.on_order_released(OrderReleased::default());
1415        algo.on_order_submitted(OrderSubmitted::default());
1416        algo.on_order_rejected(OrderRejected::default());
1417        algo.on_order_accepted(OrderAccepted::default());
1418        algo.on_algo_order_canceled(OrderCanceled::default());
1419        algo.on_order_expired(OrderExpired::default());
1420        algo.on_order_triggered(OrderTriggered::default());
1421        algo.on_order_pending_update(OrderPendingUpdate::default());
1422        algo.on_order_pending_cancel(OrderPendingCancel::default());
1423        algo.on_order_modify_rejected(OrderModifyRejected::default());
1424        algo.on_order_cancel_rejected(OrderCancelRejected::default());
1425        algo.on_order_updated(OrderUpdated::default());
1426        algo.on_algo_order_filled(OrderFilled::default());
1427    }
1428
1429    #[rstest]
1430    fn test_strategy_subscription_tracking() {
1431        let mut algo = create_test_algorithm();
1432        let strategy_id = StrategyId::from("STRAT-001");
1433
1434        assert!(!algo.core.is_strategy_subscribed(&strategy_id));
1435
1436        algo.subscribe_to_strategy_events(strategy_id);
1437        assert!(algo.core.is_strategy_subscribed(&strategy_id));
1438
1439        // Second call should be idempotent
1440        algo.subscribe_to_strategy_events(strategy_id);
1441        assert!(algo.core.is_strategy_subscribed(&strategy_id));
1442    }
1443
1444    #[rstest]
1445    fn test_algorithm_reset() {
1446        let mut algo = create_test_algorithm();
1447        let strategy_id = StrategyId::from("STRAT-001");
1448        let primary_id = ClientOrderId::new("O-001");
1449
1450        let _ = algo.core.spawn_client_order_id(&primary_id);
1451        algo.core.add_subscribed_strategy(strategy_id);
1452
1453        assert!(algo.core.spawn_sequence(&primary_id).is_some());
1454        assert!(algo.core.is_strategy_subscribed(&strategy_id));
1455
1456        ExecutionAlgorithm::on_reset(&mut algo).unwrap();
1457
1458        assert!(algo.core.spawn_sequence(&primary_id).is_none());
1459        assert!(!algo.core.is_strategy_subscribed(&strategy_id));
1460    }
1461
1462    #[rstest]
1463    fn test_algorithm_spawn_limit_creates_valid_order() {
1464        let mut algo = create_test_algorithm();
1465        register_algorithm(&mut algo);
1466
1467        let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
1468        let mut primary = OrderAny::Market(MarketOrder::new(
1469            TraderId::from("TRADER-001"),
1470            StrategyId::from("STRAT-001"),
1471            instrument_id,
1472            ClientOrderId::from("O-001"),
1473            OrderSide::Buy,
1474            Quantity::from("1.0"),
1475            TimeInForce::Gtc,
1476            UUID4::new(),
1477            0.into(),
1478            false,
1479            false,
1480            None,
1481            None,
1482            None,
1483            None,
1484            None,
1485            None,
1486            None,
1487            None,
1488        ));
1489
1490        let price = Price::from("50000.0");
1491        let spawned = algo.spawn_limit(
1492            &mut primary,
1493            Quantity::from("0.5"),
1494            price,
1495            TimeInForce::Gtc,
1496            None,  // expire_time
1497            false, // post_only
1498            false, // reduce_only
1499            None,  // display_qty
1500            None,  // emulation_trigger
1501            None,  // tags
1502            false, // reduce_primary
1503        );
1504
1505        assert_eq!(spawned.client_order_id.as_str(), "O-001-E1");
1506        assert_eq!(spawned.instrument_id, instrument_id);
1507        assert_eq!(spawned.order_side(), OrderSide::Buy);
1508        assert_eq!(spawned.quantity, Quantity::from("0.5"));
1509        assert_eq!(spawned.price, price);
1510        assert_eq!(spawned.time_in_force, TimeInForce::Gtc);
1511        assert_eq!(spawned.exec_algorithm_id, Some(algo.id()));
1512        assert_eq!(spawned.exec_spawn_id, Some(ClientOrderId::from("O-001")));
1513    }
1514
1515    #[rstest]
1516    fn test_algorithm_spawn_market_to_limit_creates_valid_order() {
1517        let mut algo = create_test_algorithm();
1518        register_algorithm(&mut algo);
1519
1520        let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
1521        let mut primary = OrderAny::Market(MarketOrder::new(
1522            TraderId::from("TRADER-001"),
1523            StrategyId::from("STRAT-001"),
1524            instrument_id,
1525            ClientOrderId::from("O-001"),
1526            OrderSide::Buy,
1527            Quantity::from("1.0"),
1528            TimeInForce::Gtc,
1529            UUID4::new(),
1530            0.into(),
1531            false,
1532            false,
1533            None,
1534            None,
1535            None,
1536            None,
1537            None,
1538            None,
1539            None,
1540            None,
1541        ));
1542
1543        let spawned = algo.spawn_market_to_limit(
1544            &mut primary,
1545            Quantity::from("0.5"),
1546            TimeInForce::Gtc,
1547            None,  // expire_time
1548            false, // reduce_only
1549            None,  // display_qty
1550            None,  // emulation_trigger
1551            None,  // tags
1552            false, // reduce_primary
1553        );
1554
1555        assert_eq!(spawned.client_order_id.as_str(), "O-001-E1");
1556        assert_eq!(spawned.instrument_id, instrument_id);
1557        assert_eq!(spawned.order_side(), OrderSide::Buy);
1558        assert_eq!(spawned.quantity, Quantity::from("0.5"));
1559        assert_eq!(spawned.time_in_force, TimeInForce::Gtc);
1560        assert_eq!(spawned.exec_algorithm_id, Some(algo.id()));
1561        assert_eq!(spawned.exec_spawn_id, Some(ClientOrderId::from("O-001")));
1562    }
1563
1564    #[rstest]
1565    fn test_algorithm_spawn_market_with_tags() {
1566        let mut algo = create_test_algorithm();
1567        register_algorithm(&mut algo);
1568
1569        let mut primary = OrderAny::Market(MarketOrder::new(
1570            TraderId::from("TRADER-001"),
1571            StrategyId::from("STRAT-001"),
1572            InstrumentId::from("BTC/USDT.BINANCE"),
1573            ClientOrderId::from("O-001"),
1574            OrderSide::Buy,
1575            Quantity::from("1.0"),
1576            TimeInForce::Gtc,
1577            UUID4::new(),
1578            0.into(),
1579            false,
1580            false,
1581            None,
1582            None,
1583            None,
1584            None,
1585            None,
1586            None,
1587            None,
1588            None,
1589        ));
1590
1591        let tags = vec![ustr::Ustr::from("TAG1"), ustr::Ustr::from("TAG2")];
1592        let spawned = algo.spawn_market(
1593            &mut primary,
1594            Quantity::from("0.5"),
1595            TimeInForce::Ioc,
1596            false,
1597            Some(tags.clone()),
1598            false,
1599        );
1600
1601        assert_eq!(spawned.tags, Some(tags));
1602    }
1603
1604    #[rstest]
1605    fn test_algorithm_reduce_primary_order() {
1606        let mut algo = create_test_algorithm();
1607        register_algorithm(&mut algo);
1608
1609        let order = OrderAny::Market(MarketOrder::new(
1610            TraderId::from("TRADER-001"),
1611            StrategyId::from("STRAT-001"),
1612            InstrumentId::from("BTC/USDT.BINANCE"),
1613            ClientOrderId::from("O-001"),
1614            OrderSide::Buy,
1615            Quantity::from("1.0"),
1616            TimeInForce::Gtc,
1617            UUID4::new(),
1618            0.into(),
1619            false,
1620            false,
1621            None,
1622            None,
1623            None,
1624            None,
1625            None,
1626            None,
1627            None,
1628            None,
1629        ));
1630
1631        // Make accepted so OrderUpdated can be applied
1632        let mut primary = TestOrderStubs::make_accepted_order(&order);
1633
1634        {
1635            let cache_rc = algo.core.cache_rc();
1636            let mut cache = cache_rc.borrow_mut();
1637            cache.add_order(primary.clone(), None, None, false).unwrap();
1638        }
1639
1640        let spawn_qty = Quantity::from("0.3");
1641        algo.reduce_primary_order(&mut primary, spawn_qty);
1642
1643        assert_eq!(primary.quantity(), Quantity::from("0.7"));
1644    }
1645
1646    #[rstest]
1647    fn test_algorithm_spawn_market_with_reduce_primary() {
1648        let mut algo = create_test_algorithm();
1649        register_algorithm(&mut algo);
1650
1651        let order = OrderAny::Market(MarketOrder::new(
1652            TraderId::from("TRADER-001"),
1653            StrategyId::from("STRAT-001"),
1654            InstrumentId::from("BTC/USDT.BINANCE"),
1655            ClientOrderId::from("O-001"),
1656            OrderSide::Buy,
1657            Quantity::from("1.0"),
1658            TimeInForce::Gtc,
1659            UUID4::new(),
1660            0.into(),
1661            false,
1662            false,
1663            None,
1664            None,
1665            None,
1666            None,
1667            None,
1668            None,
1669            None,
1670            None,
1671        ));
1672
1673        // Make accepted so OrderUpdated can be applied
1674        let mut primary = TestOrderStubs::make_accepted_order(&order);
1675
1676        {
1677            let cache_rc = algo.core.cache_rc();
1678            let mut cache = cache_rc.borrow_mut();
1679            cache.add_order(primary.clone(), None, None, false).unwrap();
1680        }
1681
1682        let spawned = algo.spawn_market(
1683            &mut primary,
1684            Quantity::from("0.4"),
1685            TimeInForce::Ioc,
1686            false,
1687            None,
1688            true, // reduce_primary = true
1689        );
1690
1691        assert_eq!(spawned.quantity, Quantity::from("0.4"));
1692        assert_eq!(primary.quantity(), Quantity::from("0.6"));
1693    }
1694
1695    #[rstest]
1696    fn test_algorithm_generate_order_canceled() {
1697        let mut algo = create_test_algorithm();
1698        register_algorithm(&mut algo);
1699
1700        let order = OrderAny::Market(MarketOrder::new(
1701            TraderId::from("TRADER-001"),
1702            StrategyId::from("STRAT-001"),
1703            InstrumentId::from("BTC/USDT.BINANCE"),
1704            ClientOrderId::from("O-001"),
1705            OrderSide::Buy,
1706            Quantity::from("1.0"),
1707            TimeInForce::Gtc,
1708            UUID4::new(),
1709            0.into(),
1710            false,
1711            false,
1712            None,
1713            None,
1714            None,
1715            None,
1716            None,
1717            None,
1718            None,
1719            None,
1720        ));
1721
1722        let event = algo.generate_order_canceled(&order);
1723
1724        assert_eq!(event.trader_id, TraderId::from("TRADER-001"));
1725        assert_eq!(event.strategy_id, StrategyId::from("STRAT-001"));
1726        assert_eq!(event.instrument_id, InstrumentId::from("BTC/USDT.BINANCE"));
1727        assert_eq!(event.client_order_id, ClientOrderId::from("O-001"));
1728    }
1729
1730    #[rstest]
1731    fn test_algorithm_modify_order_in_place_updates_quantity() {
1732        let mut algo = create_test_algorithm();
1733        register_algorithm(&mut algo);
1734
1735        let mut order = OrderAny::Limit(LimitOrder::new(
1736            TraderId::from("TRADER-001"),
1737            StrategyId::from("STRAT-001"),
1738            InstrumentId::from("BTC/USDT.BINANCE"),
1739            ClientOrderId::from("O-001"),
1740            OrderSide::Buy,
1741            Quantity::from("1.0"),
1742            Price::from("50000.0"),
1743            TimeInForce::Gtc,
1744            None,  // expire_time
1745            false, // post_only
1746            false, // reduce_only
1747            false, // quote_quantity
1748            None,  // display_qty
1749            None,  // emulation_trigger
1750            None,  // trigger_instrument_id
1751            None,  // contingency_type
1752            None,  // order_list_id
1753            None,  // linked_order_ids
1754            None,  // parent_order_id
1755            None,  // exec_algorithm_id
1756            None,  // exec_algorithm_params
1757            None,  // exec_spawn_id
1758            None,  // tags
1759            UUID4::new(),
1760            0.into(),
1761        ));
1762
1763        {
1764            let cache_rc = algo.core.cache_rc();
1765            let mut cache = cache_rc.borrow_mut();
1766            cache.add_order(order.clone(), None, None, false).unwrap();
1767        }
1768
1769        let new_qty = Quantity::from("0.5");
1770        algo.modify_order_in_place(&mut order, Some(new_qty), None, None)
1771            .unwrap();
1772
1773        assert_eq!(order.quantity(), new_qty);
1774    }
1775
1776    #[rstest]
1777    fn test_algorithm_modify_order_in_place_rejects_no_changes() {
1778        let mut algo = create_test_algorithm();
1779        register_algorithm(&mut algo);
1780
1781        let mut order = OrderAny::Limit(LimitOrder::new(
1782            TraderId::from("TRADER-001"),
1783            StrategyId::from("STRAT-001"),
1784            InstrumentId::from("BTC/USDT.BINANCE"),
1785            ClientOrderId::from("O-001"),
1786            OrderSide::Buy,
1787            Quantity::from("1.0"),
1788            Price::from("50000.0"),
1789            TimeInForce::Gtc,
1790            None,
1791            false,
1792            false,
1793            false,
1794            None,
1795            None,
1796            None,
1797            None,
1798            None,
1799            None,
1800            None,
1801            None,
1802            None,
1803            None,
1804            None,
1805            UUID4::new(),
1806            0.into(),
1807        ));
1808
1809        // Try to modify with same quantity - should fail
1810        let result =
1811            algo.modify_order_in_place(&mut order, Some(Quantity::from("1.0")), None, None);
1812
1813        assert!(result.is_err());
1814        assert!(
1815            result
1816                .unwrap_err()
1817                .to_string()
1818                .contains("no parameters differ")
1819        );
1820    }
1821
1822    #[rstest]
1823    fn test_spawned_order_denied_restores_primary_quantity() {
1824        let mut algo = create_test_algorithm();
1825        register_algorithm(&mut algo);
1826
1827        let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
1828        let exec_algorithm_id = algo.id();
1829
1830        let mut primary = OrderAny::Market(MarketOrder::new(
1831            TraderId::from("TRADER-001"),
1832            StrategyId::from("STRAT-001"),
1833            instrument_id,
1834            ClientOrderId::from("O-001"),
1835            OrderSide::Buy,
1836            Quantity::from("1.0"),
1837            TimeInForce::Gtc,
1838            UUID4::new(),
1839            0.into(),
1840            false,
1841            false,
1842            None,
1843            None,
1844            None,
1845            None,
1846            Some(exec_algorithm_id),
1847            None,
1848            None,
1849            None,
1850        ));
1851
1852        {
1853            let cache_rc = algo.core.cache_rc();
1854            let mut cache = cache_rc.borrow_mut();
1855            cache.add_order(primary.clone(), None, None, false).unwrap();
1856        }
1857
1858        let spawned = algo.spawn_market(
1859            &mut primary,
1860            Quantity::from("0.5"),
1861            TimeInForce::Fok,
1862            false,
1863            None,
1864            true,
1865        );
1866
1867        {
1868            let cache_rc = algo.core.cache_rc();
1869            let mut cache = cache_rc.borrow_mut();
1870            cache.update_order(&primary).unwrap();
1871        }
1872
1873        assert_eq!(primary.quantity(), Quantity::from("0.5"));
1874
1875        let mut spawned_order = OrderAny::Market(spawned);
1876        {
1877            let cache_rc = algo.core.cache_rc();
1878            let mut cache = cache_rc.borrow_mut();
1879            cache
1880                .add_order(spawned_order.clone(), None, None, false)
1881                .unwrap();
1882        }
1883
1884        let denied = OrderDenied::new(
1885            spawned_order.trader_id(),
1886            spawned_order.strategy_id(),
1887            spawned_order.instrument_id(),
1888            spawned_order.client_order_id(),
1889            "TEST_DENIAL".into(),
1890            UUID4::new(),
1891            0.into(),
1892            0.into(),
1893        );
1894
1895        spawned_order.apply(OrderEventAny::Denied(denied)).unwrap();
1896        {
1897            let cache_rc = algo.core.cache_rc();
1898            let mut cache = cache_rc.borrow_mut();
1899            cache.update_order(&spawned_order).unwrap();
1900        }
1901
1902        algo.handle_order_event(OrderEventAny::Denied(denied));
1903
1904        let restored_primary = {
1905            let cache = algo.core.cache();
1906            cache.order(&ClientOrderId::from("O-001")).cloned().unwrap()
1907        };
1908        assert_eq!(restored_primary.quantity(), Quantity::from("1.0"));
1909    }
1910
1911    #[rstest]
1912    fn test_spawned_order_rejected_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 rejected = OrderRejected::new(
1974            spawned_order.trader_id(),
1975            spawned_order.strategy_id(),
1976            spawned_order.instrument_id(),
1977            spawned_order.client_order_id(),
1978            AccountId::from("BINANCE-001"),
1979            "TEST_REJECTION".into(),
1980            UUID4::new(),
1981            0.into(),
1982            0.into(),
1983            false,
1984            false,
1985        );
1986
1987        spawned_order
1988            .apply(OrderEventAny::Rejected(rejected))
1989            .unwrap();
1990        {
1991            let cache_rc = algo.core.cache_rc();
1992            let mut cache = cache_rc.borrow_mut();
1993            cache.update_order(&spawned_order).unwrap();
1994        }
1995
1996        algo.handle_order_event(OrderEventAny::Rejected(rejected));
1997
1998        let restored_primary = {
1999            let cache = algo.core.cache();
2000            cache.order(&ClientOrderId::from("O-001")).cloned().unwrap()
2001        };
2002        assert_eq!(restored_primary.quantity(), Quantity::from("1.0"));
2003    }
2004
2005    #[rstest]
2006    fn test_spawned_order_with_reduce_primary_false_does_not_restore() {
2007        let mut algo = create_test_algorithm();
2008        register_algorithm(&mut algo);
2009
2010        let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
2011        let exec_algorithm_id = algo.id();
2012
2013        let mut primary = OrderAny::Market(MarketOrder::new(
2014            TraderId::from("TRADER-001"),
2015            StrategyId::from("STRAT-001"),
2016            instrument_id,
2017            ClientOrderId::from("O-001"),
2018            OrderSide::Buy,
2019            Quantity::from("1.0"),
2020            TimeInForce::Gtc,
2021            UUID4::new(),
2022            0.into(),
2023            false,
2024            false,
2025            None,
2026            None,
2027            None,
2028            None,
2029            Some(exec_algorithm_id),
2030            None,
2031            None,
2032            None,
2033        ));
2034
2035        {
2036            let cache_rc = algo.core.cache_rc();
2037            let mut cache = cache_rc.borrow_mut();
2038            cache.add_order(primary.clone(), None, None, false).unwrap();
2039        }
2040
2041        let spawned = algo.spawn_market(
2042            &mut primary,
2043            Quantity::from("0.5"),
2044            TimeInForce::Fok,
2045            false,
2046            None,
2047            false,
2048        );
2049
2050        assert_eq!(primary.quantity(), Quantity::from("1.0"));
2051
2052        let mut spawned_order = OrderAny::Market(spawned);
2053        {
2054            let cache_rc = algo.core.cache_rc();
2055            let mut cache = cache_rc.borrow_mut();
2056            cache
2057                .add_order(spawned_order.clone(), None, None, false)
2058                .unwrap();
2059        }
2060
2061        let denied = OrderDenied::new(
2062            spawned_order.trader_id(),
2063            spawned_order.strategy_id(),
2064            spawned_order.instrument_id(),
2065            spawned_order.client_order_id(),
2066            "TEST_DENIAL".into(),
2067            UUID4::new(),
2068            0.into(),
2069            0.into(),
2070        );
2071
2072        spawned_order.apply(OrderEventAny::Denied(denied)).unwrap();
2073        {
2074            let cache_rc = algo.core.cache_rc();
2075            let mut cache = cache_rc.borrow_mut();
2076            cache.update_order(&spawned_order).unwrap();
2077        }
2078
2079        algo.handle_order_event(OrderEventAny::Denied(denied));
2080
2081        let final_primary = {
2082            let cache = algo.core.cache();
2083            cache.order(&ClientOrderId::from("O-001")).cloned().unwrap()
2084        };
2085        assert_eq!(final_primary.quantity(), Quantity::from("1.0"));
2086    }
2087
2088    #[rstest]
2089    fn test_multiple_spawns_with_one_denied_restores_correctly() {
2090        let mut algo = create_test_algorithm();
2091        register_algorithm(&mut algo);
2092
2093        let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
2094        let exec_algorithm_id = algo.id();
2095
2096        let mut primary = OrderAny::Market(MarketOrder::new(
2097            TraderId::from("TRADER-001"),
2098            StrategyId::from("STRAT-001"),
2099            instrument_id,
2100            ClientOrderId::from("O-001"),
2101            OrderSide::Buy,
2102            Quantity::from("1.0"),
2103            TimeInForce::Gtc,
2104            UUID4::new(),
2105            0.into(),
2106            false,
2107            false,
2108            None,
2109            None,
2110            None,
2111            None,
2112            Some(exec_algorithm_id),
2113            None,
2114            None,
2115            None,
2116        ));
2117
2118        {
2119            let cache_rc = algo.core.cache_rc();
2120            let mut cache = cache_rc.borrow_mut();
2121            cache.add_order(primary.clone(), None, None, false).unwrap();
2122        }
2123
2124        let spawned1 = algo.spawn_market(
2125            &mut primary,
2126            Quantity::from("0.3"),
2127            TimeInForce::Fok,
2128            false,
2129            None,
2130            true,
2131        );
2132        {
2133            let cache_rc = algo.core.cache_rc();
2134            let mut cache = cache_rc.borrow_mut();
2135            cache.update_order(&primary).unwrap();
2136        }
2137
2138        let spawned2 = algo.spawn_market(
2139            &mut primary,
2140            Quantity::from("0.4"),
2141            TimeInForce::Fok,
2142            false,
2143            None,
2144            true,
2145        );
2146        {
2147            let cache_rc = algo.core.cache_rc();
2148            let mut cache = cache_rc.borrow_mut();
2149            cache.update_order(&primary).unwrap();
2150        }
2151
2152        assert_eq!(primary.quantity(), Quantity::from("0.3"));
2153
2154        let spawned_order1 = OrderAny::Market(spawned1);
2155        let mut spawned_order2 = OrderAny::Market(spawned2);
2156        {
2157            let cache_rc = algo.core.cache_rc();
2158            let mut cache = cache_rc.borrow_mut();
2159            cache.add_order(spawned_order1, None, None, false).unwrap();
2160            cache
2161                .add_order(spawned_order2.clone(), None, None, false)
2162                .unwrap();
2163        }
2164
2165        let denied = OrderDenied::new(
2166            spawned_order2.trader_id(),
2167            spawned_order2.strategy_id(),
2168            spawned_order2.instrument_id(),
2169            spawned_order2.client_order_id(),
2170            "TEST_DENIAL".into(),
2171            UUID4::new(),
2172            0.into(),
2173            0.into(),
2174        );
2175
2176        spawned_order2.apply(OrderEventAny::Denied(denied)).unwrap();
2177        {
2178            let cache_rc = algo.core.cache_rc();
2179            let mut cache = cache_rc.borrow_mut();
2180            cache.update_order(&spawned_order2).unwrap();
2181        }
2182
2183        algo.handle_order_event(OrderEventAny::Denied(denied));
2184
2185        let restored_primary = {
2186            let cache = algo.core.cache();
2187            cache.order(&ClientOrderId::from("O-001")).cloned().unwrap()
2188        };
2189        assert_eq!(restored_primary.quantity(), Quantity::from("0.7"));
2190    }
2191
2192    #[rstest]
2193    fn test_spawned_order_accepted_prevents_restoration() {
2194        let mut algo = create_test_algorithm();
2195        register_algorithm(&mut algo);
2196
2197        let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
2198        let exec_algorithm_id = algo.id();
2199
2200        let mut primary = OrderAny::Market(MarketOrder::new(
2201            TraderId::from("TRADER-001"),
2202            StrategyId::from("STRAT-001"),
2203            instrument_id,
2204            ClientOrderId::from("O-001"),
2205            OrderSide::Buy,
2206            Quantity::from("1.0"),
2207            TimeInForce::Gtc,
2208            UUID4::new(),
2209            0.into(),
2210            false,
2211            false,
2212            None,
2213            None,
2214            None,
2215            None,
2216            Some(exec_algorithm_id),
2217            None,
2218            None,
2219            None,
2220        ));
2221
2222        {
2223            let cache_rc = algo.core.cache_rc();
2224            let mut cache = cache_rc.borrow_mut();
2225            cache.add_order(primary.clone(), None, None, false).unwrap();
2226        }
2227
2228        let spawned = algo.spawn_market(
2229            &mut primary,
2230            Quantity::from("0.5"),
2231            TimeInForce::Fok,
2232            false,
2233            None,
2234            true,
2235        );
2236
2237        {
2238            let cache_rc = algo.core.cache_rc();
2239            let mut cache = cache_rc.borrow_mut();
2240            cache.update_order(&primary).unwrap();
2241        }
2242
2243        assert_eq!(primary.quantity(), Quantity::from("0.5"));
2244
2245        let mut spawned_order = OrderAny::Market(spawned);
2246        {
2247            let cache_rc = algo.core.cache_rc();
2248            let mut cache = cache_rc.borrow_mut();
2249            cache
2250                .add_order(spawned_order.clone(), None, None, false)
2251                .unwrap();
2252        }
2253
2254        let accepted = OrderAccepted::new(
2255            spawned_order.trader_id(),
2256            spawned_order.strategy_id(),
2257            spawned_order.instrument_id(),
2258            spawned_order.client_order_id(),
2259            VenueOrderId::from("V-123"),
2260            AccountId::from("BINANCE-001"),
2261            UUID4::new(),
2262            0.into(),
2263            0.into(),
2264            false,
2265        );
2266
2267        spawned_order
2268            .apply(OrderEventAny::Accepted(accepted))
2269            .unwrap();
2270        {
2271            let cache_rc = algo.core.cache_rc();
2272            let mut cache = cache_rc.borrow_mut();
2273            cache.update_order(&spawned_order).unwrap();
2274        }
2275
2276        algo.handle_order_event(OrderEventAny::Accepted(accepted));
2277
2278        let primary_after_accept = {
2279            let cache = algo.core.cache();
2280            cache.order(&ClientOrderId::from("O-001")).cloned().unwrap()
2281        };
2282        assert_eq!(primary_after_accept.quantity(), Quantity::from("0.5"));
2283
2284        // Cancel after acceptance - no restoration should occur
2285        let canceled = OrderCanceled::new(
2286            spawned_order.trader_id(),
2287            spawned_order.strategy_id(),
2288            spawned_order.instrument_id(),
2289            spawned_order.client_order_id(),
2290            UUID4::new(),
2291            0.into(),
2292            0.into(),
2293            false,
2294            Some(VenueOrderId::from("V-123")),
2295            Some(AccountId::from("BINANCE-001")),
2296        );
2297
2298        spawned_order
2299            .apply(OrderEventAny::Canceled(canceled))
2300            .unwrap();
2301        {
2302            let cache_rc = algo.core.cache_rc();
2303            let mut cache = cache_rc.borrow_mut();
2304            cache.update_order(&spawned_order).unwrap();
2305        }
2306
2307        algo.handle_order_event(OrderEventAny::Canceled(canceled));
2308
2309        let final_primary = {
2310            let cache = algo.core.cache();
2311            cache.order(&ClientOrderId::from("O-001")).cloned().unwrap()
2312        };
2313        assert_eq!(final_primary.quantity(), Quantity::from("0.5"));
2314    }
2315
2316    #[rstest]
2317    #[should_panic(expected = "exceeds primary leaves_qty")]
2318    fn test_spawn_quantity_exceeds_leaves_qty_panics() {
2319        let mut algo = create_test_algorithm();
2320        register_algorithm(&mut algo);
2321
2322        let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
2323        let exec_algorithm_id = algo.id();
2324
2325        let mut primary = OrderAny::Market(MarketOrder::new(
2326            TraderId::from("TRADER-001"),
2327            StrategyId::from("STRAT-001"),
2328            instrument_id,
2329            ClientOrderId::from("O-001"),
2330            OrderSide::Buy,
2331            Quantity::from("1.0"),
2332            TimeInForce::Gtc,
2333            UUID4::new(),
2334            0.into(),
2335            false,
2336            false,
2337            None,
2338            None,
2339            None,
2340            None,
2341            Some(exec_algorithm_id),
2342            None,
2343            None,
2344            None,
2345        ));
2346
2347        {
2348            let cache_rc = algo.core.cache_rc();
2349            let mut cache = cache_rc.borrow_mut();
2350            cache.add_order(primary.clone(), None, None, false).unwrap();
2351        }
2352
2353        let _ = algo.spawn_market(
2354            &mut primary,
2355            Quantity::from("0.8"),
2356            TimeInForce::Fok,
2357            false,
2358            None,
2359            true,
2360        );
2361
2362        {
2363            let cache_rc = algo.core.cache_rc();
2364            let mut cache = cache_rc.borrow_mut();
2365            cache.update_order(&primary).unwrap();
2366        }
2367
2368        assert_eq!(primary.quantity(), Quantity::from("0.2"));
2369        assert_eq!(primary.leaves_qty(), Quantity::from("0.2"));
2370
2371        // Should panic - spawning 0.5 when only 0.2 leaves_qty remains
2372        let _ = algo.spawn_market(
2373            &mut primary,
2374            Quantity::from("0.5"),
2375            TimeInForce::Fok,
2376            false,
2377            None,
2378            true,
2379        );
2380    }
2381}