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;
43
44pub use config::ExecutionAlgorithmConfig;
45use nautilus_common::{
46    actor::DataActor,
47    enums::ComponentState,
48    logging::{CMD, EVT, RECV, SEND},
49    messages::execution::{CancelOrder, ModifyOrder, SubmitOrder, TradingCommand},
50    msgbus,
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        let core = self.core_mut();
109        if core.config.log_commands {
110            let id = &core.actor.actor_id;
111            log::info!("{id} {RECV}{CMD} {command:?}");
112        }
113
114        if core.state() != ComponentState::Running {
115            return Ok(());
116        }
117
118        match command {
119            TradingCommand::SubmitOrder(cmd) => {
120                self.subscribe_to_strategy_events(cmd.strategy_id);
121                self.on_order(cmd.order)
122            }
123            TradingCommand::SubmitOrderList(cmd) => {
124                self.subscribe_to_strategy_events(cmd.strategy_id);
125                self.on_order_list(cmd.order_list)
126            }
127            TradingCommand::CancelOrder(cmd) => self.handle_cancel_order(cmd),
128            _ => {
129                log::warn!("Unhandled command type: {command:?}");
130                Ok(())
131            }
132        }
133    }
134
135    /// Called when a primary order is received for execution.
136    ///
137    /// Override this method to implement the algorithm's order slicing logic.
138    ///
139    /// # Errors
140    ///
141    /// Returns an error if order handling fails.
142    fn on_order(&mut self, order: OrderAny) -> anyhow::Result<()>;
143
144    /// Called when an order list is received for execution.
145    ///
146    /// Override this method to handle order lists. The default implementation
147    /// processes each order individually.
148    ///
149    /// # Errors
150    ///
151    /// Returns an error if order list handling fails.
152    fn on_order_list(&mut self, order_list: OrderList) -> anyhow::Result<()> {
153        for order in order_list.orders {
154            self.on_order(order)?;
155        }
156        Ok(())
157    }
158
159    /// Handles a cancel order command for algorithm-managed orders.
160    ///
161    /// This generates an internal cancel event and publishes it. The order
162    /// is canceled locally without sending a command to the execution engine.
163    ///
164    /// # Errors
165    ///
166    /// Returns an error if cancellation fails.
167    fn handle_cancel_order(&mut self, command: CancelOrder) -> anyhow::Result<()> {
168        let (mut order, is_pending_cancel) = {
169            let cache = self.core_mut().cache();
170
171            let Some(order) = cache.order(&command.client_order_id) else {
172                log::warn!(
173                    "Cannot cancel order: {} not found in cache",
174                    command.client_order_id
175                );
176                return Ok(());
177            };
178
179            let is_pending = cache.is_order_pending_cancel_local(&command.client_order_id);
180            (order.clone(), is_pending)
181        };
182
183        if is_pending_cancel {
184            return Ok(());
185        }
186
187        if order.is_closed() {
188            log::warn!("Order already closed for {command:?}");
189            return Ok(());
190        }
191
192        let event = self.generate_order_canceled(&order);
193
194        if let Err(e) = order.apply(OrderEventAny::Canceled(event)) {
195            log::warn!("InvalidStateTrigger: {e}, did not apply cancel event");
196            return Ok(());
197        }
198
199        {
200            let cache_rc = self.core_mut().cache_rc();
201            let mut cache = cache_rc.borrow_mut();
202            cache.update_order(&order)?;
203        }
204
205        let topic = format!("events.order.{}", order.strategy_id());
206        msgbus::publish(topic.into(), &event);
207
208        Ok(())
209    }
210
211    /// Generates an OrderCanceled event for an order.
212    fn generate_order_canceled(&mut self, order: &OrderAny) -> OrderCanceled {
213        let ts_now = self.core_mut().clock().timestamp_ns();
214
215        OrderCanceled::new(
216            order.trader_id(),
217            order.strategy_id(),
218            order.instrument_id(),
219            order.client_order_id(),
220            UUID4::new(),
221            ts_now,
222            ts_now,
223            false, // reconciliation
224            order.venue_order_id(),
225            order.account_id(),
226        )
227    }
228
229    /// Spawns a market order from a primary order.
230    ///
231    /// Creates a new market order with:
232    /// - A unique client order ID: `{primary_id}-E{sequence}`.
233    /// - The primary order's trader ID, strategy ID, and instrument ID.
234    /// - The algorithm's exec_algorithm_id.
235    /// - exec_spawn_id set to the primary order's client order ID.
236    ///
237    /// If `reduce_primary` is true, the primary order's quantity will be reduced
238    /// by the spawned quantity.
239    fn spawn_market(
240        &mut self,
241        primary: &mut OrderAny,
242        quantity: Quantity,
243        time_in_force: TimeInForce,
244        reduce_only: bool,
245        tags: Option<Vec<Ustr>>,
246        reduce_primary: bool,
247    ) -> MarketOrder {
248        if reduce_primary {
249            self.reduce_primary_order(primary, quantity);
250        }
251
252        let core = self.core_mut();
253        let client_order_id = core.spawn_client_order_id(&primary.client_order_id());
254        let ts_init = core.clock().timestamp_ns();
255        let exec_algorithm_id = core.exec_algorithm_id;
256
257        MarketOrder::new(
258            primary.trader_id(),
259            primary.strategy_id(),
260            primary.instrument_id(),
261            client_order_id,
262            primary.order_side(),
263            quantity,
264            time_in_force,
265            UUID4::new(),
266            ts_init,
267            reduce_only,
268            false, // quote_quantity
269            primary.contingency_type(),
270            primary.order_list_id(),
271            primary.linked_order_ids().map(|ids| ids.to_vec()),
272            primary.parent_order_id(),
273            Some(exec_algorithm_id),
274            primary.exec_algorithm_params().cloned(),
275            Some(primary.client_order_id()),
276            tags.or_else(|| primary.tags().map(|t| t.to_vec())),
277        )
278    }
279
280    /// Spawns a limit order from a primary order.
281    ///
282    /// Creates a new limit order with:
283    /// - A unique client order ID: `{primary_id}-E{sequence}`
284    /// - The primary order's trader ID, strategy ID, and instrument ID
285    /// - The algorithm's exec_algorithm_id
286    /// - exec_spawn_id set to the primary order's client order ID
287    ///
288    /// If `reduce_primary` is true, the primary order's quantity will be reduced
289    /// by the spawned quantity.
290    #[allow(clippy::too_many_arguments)]
291    fn spawn_limit(
292        &mut self,
293        primary: &mut OrderAny,
294        quantity: Quantity,
295        price: Price,
296        time_in_force: TimeInForce,
297        expire_time: Option<UnixNanos>,
298        post_only: bool,
299        reduce_only: bool,
300        display_qty: Option<Quantity>,
301        emulation_trigger: Option<TriggerType>,
302        tags: Option<Vec<Ustr>>,
303        reduce_primary: bool,
304    ) -> LimitOrder {
305        if reduce_primary {
306            self.reduce_primary_order(primary, quantity);
307        }
308
309        let core = self.core_mut();
310        let client_order_id = core.spawn_client_order_id(&primary.client_order_id());
311        let ts_init = core.clock().timestamp_ns();
312        let exec_algorithm_id = core.exec_algorithm_id;
313
314        LimitOrder::new(
315            primary.trader_id(),
316            primary.strategy_id(),
317            primary.instrument_id(),
318            client_order_id,
319            primary.order_side(),
320            quantity,
321            price,
322            time_in_force,
323            expire_time,
324            post_only,
325            reduce_only,
326            false, // quote_quantity
327            display_qty,
328            emulation_trigger,
329            None, // trigger_instrument_id
330            primary.contingency_type(),
331            primary.order_list_id(),
332            primary.linked_order_ids().map(|ids| ids.to_vec()),
333            primary.parent_order_id(),
334            Some(exec_algorithm_id),
335            primary.exec_algorithm_params().cloned(),
336            Some(primary.client_order_id()),
337            tags.or_else(|| primary.tags().map(|t| t.to_vec())),
338            UUID4::new(),
339            ts_init,
340        )
341    }
342
343    /// Spawns a market-to-limit order from a primary order.
344    ///
345    /// Creates a new market-to-limit order with:
346    /// - A unique client order ID: `{primary_id}-E{sequence}`
347    /// - The primary order's trader ID, strategy ID, and instrument ID
348    /// - The algorithm's exec_algorithm_id
349    /// - exec_spawn_id set to the primary order's client order ID
350    ///
351    /// If `reduce_primary` is true, the primary order's quantity will be reduced
352    /// by the spawned quantity.
353    #[allow(clippy::too_many_arguments)]
354    fn spawn_market_to_limit(
355        &mut self,
356        primary: &mut OrderAny,
357        quantity: Quantity,
358        time_in_force: TimeInForce,
359        expire_time: Option<UnixNanos>,
360        post_only: bool,
361        reduce_only: bool,
362        display_qty: Option<Quantity>,
363        tags: Option<Vec<Ustr>>,
364        reduce_primary: bool,
365    ) -> MarketToLimitOrder {
366        if reduce_primary {
367            self.reduce_primary_order(primary, quantity);
368        }
369
370        let core = self.core_mut();
371        let client_order_id = core.spawn_client_order_id(&primary.client_order_id());
372        let ts_init = core.clock().timestamp_ns();
373        let exec_algorithm_id = core.exec_algorithm_id;
374
375        MarketToLimitOrder::new(
376            primary.trader_id(),
377            primary.strategy_id(),
378            primary.instrument_id(),
379            client_order_id,
380            primary.order_side(),
381            quantity,
382            time_in_force,
383            expire_time,
384            post_only,
385            reduce_only,
386            false, // quote_quantity
387            display_qty,
388            primary.contingency_type(),
389            primary.order_list_id(),
390            primary.linked_order_ids().map(|ids| ids.to_vec()),
391            primary.parent_order_id(),
392            Some(exec_algorithm_id),
393            primary.exec_algorithm_params().cloned(),
394            Some(primary.client_order_id()),
395            tags.or_else(|| primary.tags().map(|t| t.to_vec())),
396            UUID4::new(),
397            ts_init,
398        )
399    }
400
401    /// Reduces the primary order's quantity by the spawn quantity.
402    ///
403    /// Generates an `OrderUpdated` event and applies it to the primary order,
404    /// then updates the order in the cache.
405    fn reduce_primary_order(&mut self, primary: &mut OrderAny, spawn_qty: Quantity) {
406        let primary_qty = primary.quantity();
407        assert!(
408            primary_qty >= spawn_qty,
409            "Spawn order quantity must be less than or equal to primary order quantity"
410        );
411
412        let new_qty = Quantity::from_raw(primary_qty.raw - spawn_qty.raw, primary_qty.precision);
413
414        let core = self.core_mut();
415        let ts_now = core.clock().timestamp_ns();
416
417        let updated = OrderUpdated::new(
418            primary.trader_id(),
419            primary.strategy_id(),
420            primary.instrument_id(),
421            primary.client_order_id(),
422            new_qty,
423            UUID4::new(),
424            ts_now,
425            ts_now,
426            false, // reconciliation
427            primary.venue_order_id(),
428            primary.account_id(),
429            None, // price
430            None, // trigger_price
431            None, // protection_price
432        );
433
434        primary
435            .apply(OrderEventAny::Updated(updated))
436            .expect("Failed to apply OrderUpdated");
437
438        let cache_rc = core.cache_rc();
439        let mut cache = cache_rc.borrow_mut();
440        cache
441            .update_order(primary)
442            .expect("Failed to update order in cache");
443    }
444
445    /// Submits an order to the execution engine via the risk engine.
446    ///
447    /// # Errors
448    ///
449    /// Returns an error if order submission fails.
450    fn submit_order(
451        &mut self,
452        order: OrderAny,
453        position_id: Option<PositionId>,
454        client_id: Option<ClientId>,
455    ) -> anyhow::Result<()> {
456        let core = self.core_mut();
457
458        let trader_id = core.trader_id().expect("Trader ID not set");
459        let ts_init = core.clock().timestamp_ns();
460
461        // For spawned orders, use the parent's strategy ID
462        let strategy_id = order.strategy_id();
463
464        {
465            let cache_rc = core.cache_rc();
466            let mut cache = cache_rc.borrow_mut();
467            cache.add_order(order.clone(), position_id, client_id, true)?;
468        }
469
470        let command = SubmitOrder::new(
471            trader_id,
472            client_id,
473            strategy_id,
474            order.instrument_id(),
475            order.clone(),
476            order.exec_algorithm_id(),
477            position_id,
478            None, // params
479            UUID4::new(),
480            ts_init,
481        );
482
483        if core.config.log_commands {
484            let id = &core.actor.actor_id;
485            log::info!("{id} {SEND}{CMD} {command:?}");
486        }
487
488        msgbus::send_any(
489            "RiskEngine.execute".into(),
490            &TradingCommand::SubmitOrder(command),
491        );
492
493        Ok(())
494    }
495
496    /// Modifies an order.
497    ///
498    /// # Errors
499    ///
500    /// Returns an error if order modification fails.
501    fn modify_order(
502        &mut self,
503        order: &OrderAny,
504        quantity: Option<Quantity>,
505        price: Option<Price>,
506        trigger_price: Option<Price>,
507        client_id: Option<ClientId>,
508    ) -> anyhow::Result<()> {
509        let core = self.core_mut();
510
511        let trader_id = core.trader_id().expect("Trader ID not set");
512        let strategy_id = order.strategy_id();
513        let ts_init = core.clock().timestamp_ns();
514
515        let command = ModifyOrder::new(
516            trader_id,
517            client_id,
518            strategy_id,
519            order.instrument_id(),
520            order.client_order_id(),
521            order.venue_order_id(),
522            quantity,
523            price,
524            trigger_price,
525            UUID4::new(),
526            ts_init,
527            None, // params
528        );
529
530        if core.config.log_commands {
531            let id = &core.actor.actor_id;
532            log::info!("{id} {SEND}{CMD} {command:?}");
533        }
534
535        // Route based on order state
536        if order.is_emulated() {
537            msgbus::send_any(
538                "OrderEmulator.execute".into(),
539                &TradingCommand::ModifyOrder(command),
540            );
541        } else {
542            msgbus::send_any(
543                "ExecEngine.execute".into(),
544                &TradingCommand::ModifyOrder(command),
545            );
546        }
547
548        Ok(())
549    }
550
551    /// Modifies an INITIALIZED or RELEASED order in place without sending a command.
552    ///
553    /// This is useful for adjusting order parameters before submission. The order
554    /// is updated locally by applying an `OrderUpdated` event and updating the cache.
555    ///
556    /// At least one parameter must differ from the current order values.
557    ///
558    /// # Errors
559    ///
560    /// Returns an error if the order status is not INITIALIZED or RELEASED,
561    /// or if no parameters would change.
562    fn modify_order_in_place(
563        &mut self,
564        order: &mut OrderAny,
565        quantity: Option<Quantity>,
566        price: Option<Price>,
567        trigger_price: Option<Price>,
568    ) -> anyhow::Result<()> {
569        // Validate order status
570        let status = order.status();
571        if status != OrderStatus::Initialized && status != OrderStatus::Released {
572            anyhow::bail!(
573                "Cannot modify order in place: status is {status:?}, expected INITIALIZED or RELEASED"
574            );
575        }
576
577        // Check if any value would actually change
578        let qty_changing = quantity.is_some_and(|q| q != order.quantity());
579        let price_changing = price.is_some() && price != order.price();
580        let trigger_changing = trigger_price.is_some() && trigger_price != order.trigger_price();
581
582        if !qty_changing && !price_changing && !trigger_changing {
583            anyhow::bail!("Cannot modify order in place: no parameters differ from current values");
584        }
585
586        let core = self.core_mut();
587        let ts_now = core.clock().timestamp_ns();
588
589        let updated = OrderUpdated::new(
590            order.trader_id(),
591            order.strategy_id(),
592            order.instrument_id(),
593            order.client_order_id(),
594            quantity.unwrap_or_else(|| order.quantity()),
595            UUID4::new(),
596            ts_now,
597            ts_now,
598            false, // reconciliation
599            order.venue_order_id(),
600            order.account_id(),
601            price,
602            trigger_price,
603            None, // protection_price
604        );
605
606        order
607            .apply(OrderEventAny::Updated(updated))
608            .map_err(|e| anyhow::anyhow!("Failed to apply OrderUpdated: {e}"))?;
609
610        let cache_rc = core.cache_rc();
611        let mut cache = cache_rc.borrow_mut();
612        cache.update_order(order)?;
613
614        Ok(())
615    }
616
617    /// Cancels an order.
618    ///
619    /// # Errors
620    ///
621    /// Returns an error if order cancellation fails.
622    fn cancel_order(
623        &mut self,
624        order: &OrderAny,
625        client_id: Option<ClientId>,
626    ) -> anyhow::Result<()> {
627        let core = self.core_mut();
628
629        let trader_id = core.trader_id().expect("Trader ID not set");
630        let strategy_id = order.strategy_id();
631        let ts_init = core.clock().timestamp_ns();
632
633        let command = CancelOrder::new(
634            trader_id,
635            client_id,
636            strategy_id,
637            order.instrument_id(),
638            order.client_order_id(),
639            order.venue_order_id(),
640            UUID4::new(),
641            ts_init,
642            None, // params
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        // Route based on order state
651        if order.is_emulated()
652            || matches!(order.emulation_trigger(), Some(t) if t != TriggerType::NoTrigger)
653        {
654            msgbus::send_any(
655                "OrderEmulator.execute".into(),
656                &TradingCommand::CancelOrder(command),
657            );
658        } else {
659            msgbus::send_any(
660                "ExecEngine.execute".into(),
661                &TradingCommand::CancelOrder(command),
662            );
663        }
664
665        Ok(())
666    }
667
668    /// Subscribes to events from a strategy.
669    ///
670    /// This is called automatically when the first order is received from a strategy.
671    fn subscribe_to_strategy_events(&mut self, strategy_id: StrategyId) {
672        let core = self.core_mut();
673        if core.is_strategy_subscribed(&strategy_id) {
674            return;
675        }
676
677        // TODO: Subscribe to strategy event topics via message bus
678        // Python implementation subscribes to:
679        // - events.order.{strategy_id} -> _handle_event
680        // - events.position.{strategy_id} -> _handle_event
681        // This requires creating ShareableMessageHandler callbacks that route to
682        // handle_order_event/handle_position_event, similar to DataActor pattern
683
684        core.add_subscribed_strategy(strategy_id);
685        log::debug!("Subscribed to events for strategy {strategy_id}");
686    }
687
688    /// Handles an order event, filtering for algorithm-owned orders.
689    fn handle_order_event(&mut self, event: OrderEventAny) {
690        // Get order from cache to check if it belongs to this algorithm
691        let order = {
692            let cache = self.core_mut().cache();
693            cache.order(&event.client_order_id()).cloned()
694        };
695
696        let Some(order) = order else {
697            return;
698        };
699
700        // Filter: only process events for orders owned by this algorithm
701        let Some(order_algo_id) = order.exec_algorithm_id() else {
702            return;
703        };
704
705        if order_algo_id != self.id() {
706            return;
707        }
708
709        {
710            let core = self.core_mut();
711            if core.config.log_events {
712                let id = &core.actor.actor_id;
713                log::info!("{id} {RECV}{EVT} {event}");
714            }
715        }
716
717        match &event {
718            OrderEventAny::Initialized(e) => self.on_order_initialized(e.clone()),
719            OrderEventAny::Denied(e) => self.on_order_denied(*e),
720            OrderEventAny::Emulated(e) => self.on_order_emulated(*e),
721            OrderEventAny::Released(e) => self.on_order_released(*e),
722            OrderEventAny::Submitted(e) => self.on_order_submitted(*e),
723            OrderEventAny::Rejected(e) => self.on_order_rejected(*e),
724            OrderEventAny::Accepted(e) => self.on_order_accepted(*e),
725            OrderEventAny::Canceled(e) => self.on_algo_order_canceled(*e),
726            OrderEventAny::Expired(e) => self.on_order_expired(*e),
727            OrderEventAny::Triggered(e) => self.on_order_triggered(*e),
728            OrderEventAny::PendingUpdate(e) => self.on_order_pending_update(*e),
729            OrderEventAny::PendingCancel(e) => self.on_order_pending_cancel(*e),
730            OrderEventAny::ModifyRejected(e) => self.on_order_modify_rejected(*e),
731            OrderEventAny::CancelRejected(e) => self.on_order_cancel_rejected(*e),
732            OrderEventAny::Updated(e) => self.on_order_updated(*e),
733            OrderEventAny::Filled(e) => self.on_algo_order_filled(*e),
734        }
735
736        self.on_order_event(event);
737    }
738
739    /// Handles a position event.
740    fn handle_position_event(&mut self, event: PositionEvent) {
741        {
742            let core = self.core_mut();
743            if core.config.log_events {
744                let id = &core.actor.actor_id;
745                log::info!("{id} {RECV}{EVT} {event:?}");
746            }
747        }
748
749        match &event {
750            PositionEvent::PositionOpened(e) => self.on_position_opened(e.clone()),
751            PositionEvent::PositionChanged(e) => self.on_position_changed(e.clone()),
752            PositionEvent::PositionClosed(e) => self.on_position_closed(e.clone()),
753            PositionEvent::PositionAdjusted(_) => {}
754        }
755
756        self.on_position_event(event);
757    }
758
759    /// Called when the algorithm is started.
760    ///
761    /// Override this method to implement custom initialization logic.
762    ///
763    /// # Errors
764    ///
765    /// Returns an error if start fails.
766    fn on_start(&mut self) -> anyhow::Result<()> {
767        let id = self.id();
768        log::info!("Starting {id}");
769        Ok(())
770    }
771
772    /// Called when the algorithm is stopped.
773    ///
774    /// # Errors
775    ///
776    /// Returns an error if stop fails.
777    fn on_stop(&mut self) -> anyhow::Result<()> {
778        Ok(())
779    }
780
781    /// Called when the algorithm is reset.
782    ///
783    /// # Errors
784    ///
785    /// Returns an error if reset fails.
786    fn on_reset(&mut self) -> anyhow::Result<()> {
787        self.core_mut().reset();
788        Ok(())
789    }
790
791    /// Called when a time event is received.
792    ///
793    /// Override this method for timer-based algorithms like TWAP.
794    ///
795    /// # Errors
796    ///
797    /// Returns an error if time event handling fails.
798    fn on_time_event(&mut self, _event: &TimeEvent) -> anyhow::Result<()> {
799        Ok(())
800    }
801
802    /// Called when an order is initialized.
803    #[allow(unused_variables)]
804    fn on_order_initialized(&mut self, event: OrderInitialized) {}
805
806    /// Called when an order is denied.
807    #[allow(unused_variables)]
808    fn on_order_denied(&mut self, event: OrderDenied) {}
809
810    /// Called when an order is emulated.
811    #[allow(unused_variables)]
812    fn on_order_emulated(&mut self, event: OrderEmulated) {}
813
814    /// Called when an order is released from emulation.
815    #[allow(unused_variables)]
816    fn on_order_released(&mut self, event: OrderReleased) {}
817
818    /// Called when an order is submitted.
819    #[allow(unused_variables)]
820    fn on_order_submitted(&mut self, event: OrderSubmitted) {}
821
822    /// Called when an order is rejected.
823    #[allow(unused_variables)]
824    fn on_order_rejected(&mut self, event: OrderRejected) {}
825
826    /// Called when an order is accepted.
827    #[allow(unused_variables)]
828    fn on_order_accepted(&mut self, event: OrderAccepted) {}
829
830    /// Called when an order is canceled.
831    #[allow(unused_variables)]
832    fn on_algo_order_canceled(&mut self, event: OrderCanceled) {}
833
834    /// Called when an order expires.
835    #[allow(unused_variables)]
836    fn on_order_expired(&mut self, event: OrderExpired) {}
837
838    /// Called when an order is triggered.
839    #[allow(unused_variables)]
840    fn on_order_triggered(&mut self, event: OrderTriggered) {}
841
842    /// Called when an order modification is pending.
843    #[allow(unused_variables)]
844    fn on_order_pending_update(&mut self, event: OrderPendingUpdate) {}
845
846    /// Called when an order cancellation is pending.
847    #[allow(unused_variables)]
848    fn on_order_pending_cancel(&mut self, event: OrderPendingCancel) {}
849
850    /// Called when an order modification is rejected.
851    #[allow(unused_variables)]
852    fn on_order_modify_rejected(&mut self, event: OrderModifyRejected) {}
853
854    /// Called when an order cancellation is rejected.
855    #[allow(unused_variables)]
856    fn on_order_cancel_rejected(&mut self, event: OrderCancelRejected) {}
857
858    /// Called when an order is updated.
859    #[allow(unused_variables)]
860    fn on_order_updated(&mut self, event: OrderUpdated) {}
861
862    /// Called when an order is filled.
863    #[allow(unused_variables)]
864    fn on_algo_order_filled(&mut self, event: OrderFilled) {}
865
866    /// Called for any order event (after specific handler).
867    #[allow(unused_variables)]
868    fn on_order_event(&mut self, event: OrderEventAny) {}
869
870    /// Called when a position is opened.
871    #[allow(unused_variables)]
872    fn on_position_opened(&mut self, event: PositionOpened) {}
873
874    /// Called when a position is changed.
875    #[allow(unused_variables)]
876    fn on_position_changed(&mut self, event: PositionChanged) {}
877
878    /// Called when a position is closed.
879    #[allow(unused_variables)]
880    fn on_position_closed(&mut self, event: PositionClosed) {}
881
882    /// Called for any position event (after specific handler).
883    #[allow(unused_variables)]
884    fn on_position_event(&mut self, event: PositionEvent) {}
885}
886
887#[cfg(test)]
888mod tests {
889    use std::{
890        cell::RefCell,
891        ops::{Deref, DerefMut},
892        rc::Rc,
893    };
894
895    use nautilus_common::{
896        actor::{DataActor, DataActorCore},
897        cache::Cache,
898        clock::TestClock,
899    };
900    use nautilus_model::{
901        enums::OrderSide,
902        identifiers::{ClientOrderId, ExecAlgorithmId, InstrumentId, StrategyId, TraderId},
903        orders::{MarketOrder, OrderAny, stubs::TestOrderStubs},
904        types::{Price, Quantity},
905    };
906    use rstest::rstest;
907
908    use super::*;
909
910    #[derive(Debug)]
911    struct TestAlgorithm {
912        core: ExecutionAlgorithmCore,
913        on_order_called: bool,
914        last_order_client_id: Option<ClientOrderId>,
915    }
916
917    impl TestAlgorithm {
918        fn new(config: ExecutionAlgorithmConfig) -> Self {
919            Self {
920                core: ExecutionAlgorithmCore::new(config),
921                on_order_called: false,
922                last_order_client_id: None,
923            }
924        }
925    }
926
927    impl Deref for TestAlgorithm {
928        type Target = DataActorCore;
929        fn deref(&self) -> &Self::Target {
930            &self.core.actor
931        }
932    }
933
934    impl DerefMut for TestAlgorithm {
935        fn deref_mut(&mut self) -> &mut Self::Target {
936            &mut self.core.actor
937        }
938    }
939
940    impl DataActor for TestAlgorithm {}
941
942    impl ExecutionAlgorithm for TestAlgorithm {
943        fn core_mut(&mut self) -> &mut ExecutionAlgorithmCore {
944            &mut self.core
945        }
946
947        fn on_order(&mut self, order: OrderAny) -> anyhow::Result<()> {
948            self.on_order_called = true;
949            self.last_order_client_id = Some(order.client_order_id());
950            Ok(())
951        }
952    }
953
954    fn create_test_algorithm() -> TestAlgorithm {
955        // Use unique ID to avoid thread-local registry/msgbus conflicts in parallel tests
956        let unique_id = format!("TEST-{}", UUID4::new());
957        let config = ExecutionAlgorithmConfig {
958            exec_algorithm_id: Some(ExecAlgorithmId::new(&unique_id)),
959            ..Default::default()
960        };
961        TestAlgorithm::new(config)
962    }
963
964    fn register_algorithm(algo: &mut TestAlgorithm) {
965        let trader_id = TraderId::from("TRADER-001");
966        let clock = Rc::new(RefCell::new(TestClock::new()));
967        let cache = Rc::new(RefCell::new(Cache::default()));
968
969        algo.core.register(trader_id, clock, cache).unwrap();
970    }
971
972    #[rstest]
973    fn test_algorithm_creation() {
974        let algo = create_test_algorithm();
975        assert!(algo.core.exec_algorithm_id.inner().starts_with("TEST-"));
976        assert!(!algo.on_order_called);
977        assert!(algo.last_order_client_id.is_none());
978    }
979
980    #[rstest]
981    fn test_algorithm_registration() {
982        let mut algo = create_test_algorithm();
983        register_algorithm(&mut algo);
984
985        assert!(algo.core.trader_id().is_some());
986        assert_eq!(algo.core.trader_id(), Some(TraderId::from("TRADER-001")));
987    }
988
989    #[rstest]
990    fn test_algorithm_id() {
991        let mut algo = create_test_algorithm();
992        assert!(algo.id().inner().starts_with("TEST-"));
993    }
994
995    #[rstest]
996    fn test_algorithm_spawn_market_creates_valid_order() {
997        let mut algo = create_test_algorithm();
998        register_algorithm(&mut algo);
999
1000        let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
1001        let mut primary = OrderAny::Market(MarketOrder::new(
1002            TraderId::from("TRADER-001"),
1003            StrategyId::from("STRAT-001"),
1004            instrument_id,
1005            ClientOrderId::from("O-001"),
1006            OrderSide::Buy,
1007            Quantity::from("1.0"),
1008            TimeInForce::Gtc,
1009            UUID4::new(),
1010            0.into(),
1011            false, // reduce_only
1012            false, // quote_quantity
1013            None,  // contingency_type
1014            None,  // order_list_id
1015            None,  // linked_order_ids
1016            None,  // parent_order_id
1017            None,  // exec_algorithm_id
1018            None,  // exec_algorithm_params
1019            None,  // exec_spawn_id
1020            None,  // tags
1021        ));
1022
1023        let spawned = algo.spawn_market(
1024            &mut primary,
1025            Quantity::from("0.5"),
1026            TimeInForce::Ioc,
1027            false,
1028            None,  // tags
1029            false, // reduce_primary
1030        );
1031
1032        assert_eq!(spawned.client_order_id.as_str(), "O-001-E1");
1033        assert_eq!(spawned.instrument_id, instrument_id);
1034        assert_eq!(spawned.order_side(), OrderSide::Buy);
1035        assert_eq!(spawned.quantity, Quantity::from("0.5"));
1036        assert_eq!(spawned.time_in_force, TimeInForce::Ioc);
1037        assert_eq!(spawned.exec_algorithm_id, Some(algo.id()));
1038        assert_eq!(spawned.exec_spawn_id, Some(ClientOrderId::from("O-001")));
1039    }
1040
1041    #[rstest]
1042    fn test_algorithm_spawn_increments_sequence() {
1043        let mut algo = create_test_algorithm();
1044        register_algorithm(&mut algo);
1045
1046        let mut primary = OrderAny::Market(MarketOrder::new(
1047            TraderId::from("TRADER-001"),
1048            StrategyId::from("STRAT-001"),
1049            InstrumentId::from("BTC/USDT.BINANCE"),
1050            ClientOrderId::from("O-001"),
1051            OrderSide::Buy,
1052            Quantity::from("1.0"),
1053            TimeInForce::Gtc,
1054            UUID4::new(),
1055            0.into(),
1056            false,
1057            false,
1058            None,
1059            None,
1060            None,
1061            None,
1062            None,
1063            None,
1064            None,
1065            None,
1066        ));
1067
1068        let spawned1 = algo.spawn_market(
1069            &mut primary,
1070            Quantity::from("0.25"),
1071            TimeInForce::Ioc,
1072            false,
1073            None,
1074            false,
1075        );
1076        let spawned2 = algo.spawn_market(
1077            &mut primary,
1078            Quantity::from("0.25"),
1079            TimeInForce::Ioc,
1080            false,
1081            None,
1082            false,
1083        );
1084        let spawned3 = algo.spawn_market(
1085            &mut primary,
1086            Quantity::from("0.25"),
1087            TimeInForce::Ioc,
1088            false,
1089            None,
1090            false,
1091        );
1092
1093        assert_eq!(spawned1.client_order_id.as_str(), "O-001-E1");
1094        assert_eq!(spawned2.client_order_id.as_str(), "O-001-E2");
1095        assert_eq!(spawned3.client_order_id.as_str(), "O-001-E3");
1096    }
1097
1098    #[rstest]
1099    fn test_algorithm_default_handlers_do_not_panic() {
1100        let mut algo = create_test_algorithm();
1101
1102        algo.on_order_initialized(Default::default());
1103        algo.on_order_denied(Default::default());
1104        algo.on_order_emulated(Default::default());
1105        algo.on_order_released(Default::default());
1106        algo.on_order_submitted(Default::default());
1107        algo.on_order_rejected(Default::default());
1108        algo.on_order_accepted(Default::default());
1109        algo.on_algo_order_canceled(Default::default());
1110        algo.on_order_expired(Default::default());
1111        algo.on_order_triggered(Default::default());
1112        algo.on_order_pending_update(Default::default());
1113        algo.on_order_pending_cancel(Default::default());
1114        algo.on_order_modify_rejected(Default::default());
1115        algo.on_order_cancel_rejected(Default::default());
1116        algo.on_order_updated(Default::default());
1117        algo.on_algo_order_filled(Default::default());
1118    }
1119
1120    #[rstest]
1121    fn test_strategy_subscription_tracking() {
1122        let mut algo = create_test_algorithm();
1123        let strategy_id = StrategyId::from("STRAT-001");
1124
1125        assert!(!algo.core.is_strategy_subscribed(&strategy_id));
1126
1127        algo.subscribe_to_strategy_events(strategy_id);
1128        assert!(algo.core.is_strategy_subscribed(&strategy_id));
1129
1130        // Second call should be idempotent
1131        algo.subscribe_to_strategy_events(strategy_id);
1132        assert!(algo.core.is_strategy_subscribed(&strategy_id));
1133    }
1134
1135    #[rstest]
1136    fn test_algorithm_reset() {
1137        let mut algo = create_test_algorithm();
1138        let strategy_id = StrategyId::from("STRAT-001");
1139        let primary_id = ClientOrderId::new("O-001");
1140
1141        let _ = algo.core.spawn_client_order_id(&primary_id);
1142        algo.core.add_subscribed_strategy(strategy_id);
1143
1144        assert!(algo.core.spawn_sequence(&primary_id).is_some());
1145        assert!(algo.core.is_strategy_subscribed(&strategy_id));
1146
1147        ExecutionAlgorithm::on_reset(&mut algo).unwrap();
1148
1149        assert!(algo.core.spawn_sequence(&primary_id).is_none());
1150        assert!(!algo.core.is_strategy_subscribed(&strategy_id));
1151    }
1152
1153    #[rstest]
1154    fn test_algorithm_spawn_limit_creates_valid_order() {
1155        let mut algo = create_test_algorithm();
1156        register_algorithm(&mut algo);
1157
1158        let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
1159        let mut primary = OrderAny::Market(MarketOrder::new(
1160            TraderId::from("TRADER-001"),
1161            StrategyId::from("STRAT-001"),
1162            instrument_id,
1163            ClientOrderId::from("O-001"),
1164            OrderSide::Buy,
1165            Quantity::from("1.0"),
1166            TimeInForce::Gtc,
1167            UUID4::new(),
1168            0.into(),
1169            false,
1170            false,
1171            None,
1172            None,
1173            None,
1174            None,
1175            None,
1176            None,
1177            None,
1178            None,
1179        ));
1180
1181        let price = Price::from("50000.0");
1182        let spawned = algo.spawn_limit(
1183            &mut primary,
1184            Quantity::from("0.5"),
1185            price,
1186            TimeInForce::Gtc,
1187            None,  // expire_time
1188            false, // post_only
1189            false, // reduce_only
1190            None,  // display_qty
1191            None,  // emulation_trigger
1192            None,  // tags
1193            false, // reduce_primary
1194        );
1195
1196        assert_eq!(spawned.client_order_id.as_str(), "O-001-E1");
1197        assert_eq!(spawned.instrument_id, instrument_id);
1198        assert_eq!(spawned.order_side(), OrderSide::Buy);
1199        assert_eq!(spawned.quantity, Quantity::from("0.5"));
1200        assert_eq!(spawned.price, price);
1201        assert_eq!(spawned.time_in_force, TimeInForce::Gtc);
1202        assert_eq!(spawned.exec_algorithm_id, Some(algo.id()));
1203        assert_eq!(spawned.exec_spawn_id, Some(ClientOrderId::from("O-001")));
1204    }
1205
1206    #[rstest]
1207    fn test_algorithm_spawn_market_to_limit_creates_valid_order() {
1208        let mut algo = create_test_algorithm();
1209        register_algorithm(&mut algo);
1210
1211        let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
1212        let mut primary = OrderAny::Market(MarketOrder::new(
1213            TraderId::from("TRADER-001"),
1214            StrategyId::from("STRAT-001"),
1215            instrument_id,
1216            ClientOrderId::from("O-001"),
1217            OrderSide::Buy,
1218            Quantity::from("1.0"),
1219            TimeInForce::Gtc,
1220            UUID4::new(),
1221            0.into(),
1222            false,
1223            false,
1224            None,
1225            None,
1226            None,
1227            None,
1228            None,
1229            None,
1230            None,
1231            None,
1232        ));
1233
1234        let spawned = algo.spawn_market_to_limit(
1235            &mut primary,
1236            Quantity::from("0.5"),
1237            TimeInForce::Gtc,
1238            None,  // expire_time
1239            false, // post_only
1240            false, // reduce_only
1241            None,  // display_qty
1242            None,  // tags
1243            false, // reduce_primary
1244        );
1245
1246        assert_eq!(spawned.client_order_id.as_str(), "O-001-E1");
1247        assert_eq!(spawned.instrument_id, instrument_id);
1248        assert_eq!(spawned.order_side(), OrderSide::Buy);
1249        assert_eq!(spawned.quantity, Quantity::from("0.5"));
1250        assert_eq!(spawned.time_in_force, TimeInForce::Gtc);
1251        assert_eq!(spawned.exec_algorithm_id, Some(algo.id()));
1252        assert_eq!(spawned.exec_spawn_id, Some(ClientOrderId::from("O-001")));
1253    }
1254
1255    #[rstest]
1256    fn test_algorithm_spawn_market_with_tags() {
1257        let mut algo = create_test_algorithm();
1258        register_algorithm(&mut algo);
1259
1260        let mut primary = OrderAny::Market(MarketOrder::new(
1261            TraderId::from("TRADER-001"),
1262            StrategyId::from("STRAT-001"),
1263            InstrumentId::from("BTC/USDT.BINANCE"),
1264            ClientOrderId::from("O-001"),
1265            OrderSide::Buy,
1266            Quantity::from("1.0"),
1267            TimeInForce::Gtc,
1268            UUID4::new(),
1269            0.into(),
1270            false,
1271            false,
1272            None,
1273            None,
1274            None,
1275            None,
1276            None,
1277            None,
1278            None,
1279            None,
1280        ));
1281
1282        let tags = vec![ustr::Ustr::from("TAG1"), ustr::Ustr::from("TAG2")];
1283        let spawned = algo.spawn_market(
1284            &mut primary,
1285            Quantity::from("0.5"),
1286            TimeInForce::Ioc,
1287            false,
1288            Some(tags.clone()),
1289            false,
1290        );
1291
1292        assert_eq!(spawned.tags, Some(tags));
1293    }
1294
1295    #[rstest]
1296    fn test_algorithm_reduce_primary_order() {
1297        let mut algo = create_test_algorithm();
1298        register_algorithm(&mut algo);
1299
1300        let order = OrderAny::Market(MarketOrder::new(
1301            TraderId::from("TRADER-001"),
1302            StrategyId::from("STRAT-001"),
1303            InstrumentId::from("BTC/USDT.BINANCE"),
1304            ClientOrderId::from("O-001"),
1305            OrderSide::Buy,
1306            Quantity::from("1.0"),
1307            TimeInForce::Gtc,
1308            UUID4::new(),
1309            0.into(),
1310            false,
1311            false,
1312            None,
1313            None,
1314            None,
1315            None,
1316            None,
1317            None,
1318            None,
1319            None,
1320        ));
1321
1322        // Make accepted so OrderUpdated can be applied
1323        let mut primary = TestOrderStubs::make_accepted_order(&order);
1324
1325        // Add order to cache first
1326        {
1327            let cache_rc = algo.core.cache_rc();
1328            let mut cache = cache_rc.borrow_mut();
1329            cache.add_order(primary.clone(), None, None, false).unwrap();
1330        }
1331
1332        let spawn_qty = Quantity::from("0.3");
1333        algo.reduce_primary_order(&mut primary, spawn_qty);
1334
1335        assert_eq!(primary.quantity(), Quantity::from("0.7"));
1336    }
1337
1338    #[rstest]
1339    fn test_algorithm_spawn_market_with_reduce_primary() {
1340        let mut algo = create_test_algorithm();
1341        register_algorithm(&mut algo);
1342
1343        let order = OrderAny::Market(MarketOrder::new(
1344            TraderId::from("TRADER-001"),
1345            StrategyId::from("STRAT-001"),
1346            InstrumentId::from("BTC/USDT.BINANCE"),
1347            ClientOrderId::from("O-001"),
1348            OrderSide::Buy,
1349            Quantity::from("1.0"),
1350            TimeInForce::Gtc,
1351            UUID4::new(),
1352            0.into(),
1353            false,
1354            false,
1355            None,
1356            None,
1357            None,
1358            None,
1359            None,
1360            None,
1361            None,
1362            None,
1363        ));
1364
1365        // Make accepted so OrderUpdated can be applied
1366        let mut primary = TestOrderStubs::make_accepted_order(&order);
1367
1368        // Add order to cache first
1369        {
1370            let cache_rc = algo.core.cache_rc();
1371            let mut cache = cache_rc.borrow_mut();
1372            cache.add_order(primary.clone(), None, None, false).unwrap();
1373        }
1374
1375        let spawned = algo.spawn_market(
1376            &mut primary,
1377            Quantity::from("0.4"),
1378            TimeInForce::Ioc,
1379            false,
1380            None,
1381            true, // reduce_primary = true
1382        );
1383
1384        assert_eq!(spawned.quantity, Quantity::from("0.4"));
1385        assert_eq!(primary.quantity(), Quantity::from("0.6"));
1386    }
1387
1388    #[rstest]
1389    fn test_algorithm_generate_order_canceled() {
1390        let mut algo = create_test_algorithm();
1391        register_algorithm(&mut algo);
1392
1393        let order = OrderAny::Market(MarketOrder::new(
1394            TraderId::from("TRADER-001"),
1395            StrategyId::from("STRAT-001"),
1396            InstrumentId::from("BTC/USDT.BINANCE"),
1397            ClientOrderId::from("O-001"),
1398            OrderSide::Buy,
1399            Quantity::from("1.0"),
1400            TimeInForce::Gtc,
1401            UUID4::new(),
1402            0.into(),
1403            false,
1404            false,
1405            None,
1406            None,
1407            None,
1408            None,
1409            None,
1410            None,
1411            None,
1412            None,
1413        ));
1414
1415        let event = algo.generate_order_canceled(&order);
1416
1417        assert_eq!(event.trader_id, TraderId::from("TRADER-001"));
1418        assert_eq!(event.strategy_id, StrategyId::from("STRAT-001"));
1419        assert_eq!(event.instrument_id, InstrumentId::from("BTC/USDT.BINANCE"));
1420        assert_eq!(event.client_order_id, ClientOrderId::from("O-001"));
1421    }
1422
1423    #[rstest]
1424    fn test_algorithm_modify_order_in_place_updates_quantity() {
1425        use nautilus_model::orders::LimitOrder;
1426
1427        let mut algo = create_test_algorithm();
1428        register_algorithm(&mut algo);
1429
1430        let mut order = OrderAny::Limit(LimitOrder::new(
1431            TraderId::from("TRADER-001"),
1432            StrategyId::from("STRAT-001"),
1433            InstrumentId::from("BTC/USDT.BINANCE"),
1434            ClientOrderId::from("O-001"),
1435            OrderSide::Buy,
1436            Quantity::from("1.0"),
1437            Price::from("50000.0"),
1438            TimeInForce::Gtc,
1439            None,  // expire_time
1440            false, // post_only
1441            false, // reduce_only
1442            false, // quote_quantity
1443            None,  // display_qty
1444            None,  // emulation_trigger
1445            None,  // trigger_instrument_id
1446            None,  // contingency_type
1447            None,  // order_list_id
1448            None,  // linked_order_ids
1449            None,  // parent_order_id
1450            None,  // exec_algorithm_id
1451            None,  // exec_algorithm_params
1452            None,  // exec_spawn_id
1453            None,  // tags
1454            UUID4::new(),
1455            0.into(),
1456        ));
1457
1458        // Add order to cache
1459        {
1460            let cache_rc = algo.core.cache_rc();
1461            let mut cache = cache_rc.borrow_mut();
1462            cache.add_order(order.clone(), None, None, false).unwrap();
1463        }
1464
1465        let new_qty = Quantity::from("0.5");
1466        algo.modify_order_in_place(&mut order, Some(new_qty), None, None)
1467            .unwrap();
1468
1469        assert_eq!(order.quantity(), new_qty);
1470    }
1471
1472    #[rstest]
1473    fn test_algorithm_modify_order_in_place_rejects_no_changes() {
1474        use nautilus_model::orders::LimitOrder;
1475
1476        let mut algo = create_test_algorithm();
1477        register_algorithm(&mut algo);
1478
1479        let mut order = OrderAny::Limit(LimitOrder::new(
1480            TraderId::from("TRADER-001"),
1481            StrategyId::from("STRAT-001"),
1482            InstrumentId::from("BTC/USDT.BINANCE"),
1483            ClientOrderId::from("O-001"),
1484            OrderSide::Buy,
1485            Quantity::from("1.0"),
1486            Price::from("50000.0"),
1487            TimeInForce::Gtc,
1488            None,
1489            false,
1490            false,
1491            false,
1492            None,
1493            None,
1494            None,
1495            None,
1496            None,
1497            None,
1498            None,
1499            None,
1500            None,
1501            None,
1502            None,
1503            UUID4::new(),
1504            0.into(),
1505        ));
1506
1507        // Try to modify with same quantity - should fail
1508        let result =
1509            algo.modify_order_in_place(&mut order, Some(Quantity::from("1.0")), None, None);
1510
1511        assert!(result.is_err());
1512        assert!(
1513            result
1514                .unwrap_err()
1515                .to_string()
1516                .contains("no parameters differ")
1517        );
1518    }
1519}