Skip to main content

nautilus_trading/strategy/
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
16pub mod config;
17pub mod core;
18
19pub use core::StrategyCore;
20use std::panic::{AssertUnwindSafe, catch_unwind};
21
22use ahash::AHashSet;
23pub use config::{ImportableStrategyConfig, StrategyConfig};
24use nautilus_common::{
25    actor::DataActor,
26    component::Component,
27    enums::ComponentState,
28    logging::{EVT, RECV},
29    messages::execution::{
30        BatchCancelOrders, CancelAllOrders, CancelOrder, ModifyOrder, QueryAccount, QueryOrder,
31        SubmitOrder, SubmitOrderList, TradingCommand,
32    },
33    msgbus,
34    timer::TimeEvent,
35};
36use nautilus_core::{Params, UUID4};
37use nautilus_model::{
38    enums::{OrderSide, OrderStatus, PositionSide, TimeInForce, TriggerType},
39    events::{
40        OrderAccepted, OrderCancelRejected, OrderDenied, OrderEmulated, OrderEventAny,
41        OrderExpired, OrderInitialized, OrderModifyRejected, OrderPendingCancel,
42        OrderPendingUpdate, OrderRejected, OrderReleased, OrderSubmitted, OrderTriggered,
43        OrderUpdated, PositionChanged, PositionClosed, PositionEvent, PositionOpened,
44    },
45    identifiers::{AccountId, ClientId, ClientOrderId, InstrumentId, PositionId, StrategyId},
46    orders::{Order, OrderAny, OrderCore, OrderList},
47    position::Position,
48    types::{Price, Quantity},
49};
50use ustr::Ustr;
51
52/// Core trait for implementing trading strategies in NautilusTrader.
53///
54/// Strategies are specialized [`DataActor`]s that combine data ingestion capabilities with
55/// order and position management functionality. By implementing this trait,
56/// custom strategies gain access to the full trading execution stack including order
57/// submission, modification, cancellation, and position management.
58///
59/// # Key Capabilities
60///
61/// - All [`DataActor`] capabilities (data subscriptions, event handling, timers).
62/// - Order lifecycle management (submit, modify, cancel).
63/// - Position management (open, close, monitor).
64/// - Access to the trading cache and portfolio.
65/// - Event routing to order manager and emulator.
66///
67/// # Implementation
68///
69/// Use the `nautilus_strategy!` macro to generate `Deref`, `DerefMut`, and
70/// `Strategy` implementations. For strategies that override additional trait
71/// methods, pass them in a block:
72///
73/// ```ignore
74/// nautilus_strategy!(MyStrategy, {
75///     fn on_order_rejected(&mut self, event: OrderRejected) {
76///         // custom handling
77///     }
78/// });
79/// ```
80///
81/// All order and position management methods are provided as default
82/// implementations.
83pub trait Strategy: DataActor {
84    /// Provides access to the internal `StrategyCore`.
85    ///
86    /// Generated automatically by the `nautilus_strategy!` macro.
87    fn core(&self) -> &StrategyCore;
88
89    /// Provides mutable access to the internal `StrategyCore`.
90    ///
91    /// Generated automatically by the `nautilus_strategy!` macro.
92    fn core_mut(&mut self) -> &mut StrategyCore;
93
94    /// Returns the external order claims for this strategy.
95    ///
96    /// These are instrument IDs whose external orders should be claimed by this strategy
97    /// during reconciliation.
98    fn external_order_claims(&self) -> Option<Vec<InstrumentId>> {
99        None
100    }
101
102    /// Submits an order.
103    ///
104    /// # Errors
105    ///
106    /// Returns an error if the strategy is not registered or order submission fails.
107    fn submit_order(
108        &mut self,
109        order: OrderAny,
110        position_id: Option<PositionId>,
111        client_id: Option<ClientId>,
112    ) -> anyhow::Result<()> {
113        self.submit_order_with_params(order, position_id, client_id, Params::new())
114    }
115
116    /// Submits an order with adapter-specific parameters.
117    ///
118    /// # Errors
119    ///
120    /// Returns an error if the strategy is not registered or order submission fails.
121    fn submit_order_with_params(
122        &mut self,
123        order: OrderAny,
124        position_id: Option<PositionId>,
125        client_id: Option<ClientId>,
126        params: Params,
127    ) -> anyhow::Result<()> {
128        let core = self.core_mut();
129
130        let trader_id = core.trader_id().expect("Trader ID not set");
131        let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
132        let ts_init = core.clock().timestamp_ns();
133
134        let market_exit_tag = core.market_exit_tag;
135        let is_market_exit_order = order
136            .tags()
137            .is_some_and(|tags| tags.contains(&market_exit_tag));
138
139        if core.is_exiting && !order.is_reduce_only() && !is_market_exit_order {
140            self.deny_order(&order, Ustr::from("MARKET_EXIT_IN_PROGRESS"));
141            return Ok(());
142        }
143
144        let core = self.core_mut();
145        let params = if params.is_empty() {
146            None
147        } else {
148            Some(params)
149        };
150
151        {
152            let cache_rc = core.cache_rc();
153            let mut cache = cache_rc.borrow_mut();
154            cache.add_order(order.clone(), position_id, client_id, true)?;
155        }
156
157        let command = SubmitOrder::new(
158            trader_id,
159            client_id,
160            strategy_id,
161            order.instrument_id(),
162            order.client_order_id(),
163            order.init_event().clone(),
164            order.exec_algorithm_id(),
165            position_id,
166            params,
167            UUID4::new(),
168            ts_init,
169        );
170
171        let manager = core.order_manager();
172
173        if matches!(order.emulation_trigger(), Some(trigger) if trigger != TriggerType::NoTrigger) {
174            manager.send_emulator_command(TradingCommand::SubmitOrder(command));
175        } else if order.exec_algorithm_id().is_some() {
176            manager.send_algo_command(command, order.exec_algorithm_id().unwrap());
177        } else {
178            manager.send_risk_command(TradingCommand::SubmitOrder(command));
179        }
180
181        self.set_gtd_expiry(&order)?;
182        Ok(())
183    }
184
185    /// Submits an order list.
186    ///
187    /// # Errors
188    ///
189    /// Returns an error if the strategy is not registered, the order list is invalid,
190    /// or order list submission fails.
191    fn submit_order_list(
192        &mut self,
193        mut orders: Vec<OrderAny>,
194        position_id: Option<PositionId>,
195        client_id: Option<ClientId>,
196    ) -> anyhow::Result<()> {
197        let should_deny = {
198            let core = self.core_mut();
199            let tag = core.market_exit_tag;
200            core.is_exiting
201                && orders.iter().any(|o| {
202                    !o.is_reduce_only() && !o.tags().is_some_and(|tags| tags.contains(&tag))
203                })
204        };
205
206        if should_deny {
207            self.deny_order_list(&orders, Ustr::from("MARKET_EXIT_IN_PROGRESS"));
208            return Ok(());
209        }
210
211        let core = self.core_mut();
212        let trader_id = core.trader_id().expect("Trader ID not set");
213        let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
214        let ts_init = core.clock().timestamp_ns();
215
216        // TODO: Replace with fluent builder API for order list construction
217        let order_list = if orders.first().is_some_and(|o| o.order_list_id().is_some()) {
218            OrderList::from_orders(&orders, ts_init)
219        } else {
220            core.order_factory().create_list(&mut orders, ts_init)
221        };
222
223        {
224            let cache_rc = core.cache_rc();
225            let cache = cache_rc.borrow();
226            if cache.order_list_exists(&order_list.id) {
227                anyhow::bail!("OrderList denied: duplicate {}", order_list.id);
228            }
229
230            for order in &orders {
231                if order.status() != OrderStatus::Initialized {
232                    anyhow::bail!(
233                        "Order in list denied: invalid status for {}, expected INITIALIZED",
234                        order.client_order_id()
235                    );
236                }
237
238                if cache.order_exists(&order.client_order_id()) {
239                    anyhow::bail!(
240                        "Order in list denied: duplicate {}",
241                        order.client_order_id()
242                    );
243                }
244            }
245        }
246
247        {
248            let cache_rc = core.cache_rc();
249            let mut cache = cache_rc.borrow_mut();
250            cache.add_order_list(order_list.clone())?;
251            for order in &orders {
252                cache.add_order(order.clone(), position_id, client_id, true)?;
253            }
254        }
255
256        let first_order = orders.first();
257        let order_inits: Vec<_> = orders.iter().map(|o| o.init_event().clone()).collect();
258        let exec_algorithm_id = first_order.and_then(|o| o.exec_algorithm_id());
259
260        let command = SubmitOrderList::new(
261            trader_id,
262            client_id,
263            strategy_id,
264            order_list,
265            order_inits,
266            exec_algorithm_id,
267            position_id,
268            None, // params
269            UUID4::new(),
270            ts_init,
271        );
272
273        let has_emulated_order = orders.iter().any(|o| {
274            matches!(o.emulation_trigger(), Some(trigger) if trigger != TriggerType::NoTrigger)
275                || o.is_emulated()
276        });
277
278        let manager = core.order_manager();
279
280        if has_emulated_order {
281            manager.send_emulator_command(TradingCommand::SubmitOrderList(command));
282        } else if let Some(algo_id) = exec_algorithm_id {
283            let endpoint = format!("{algo_id}.execute");
284            msgbus::send_any(endpoint.into(), &TradingCommand::SubmitOrderList(command));
285        } else {
286            manager.send_risk_command(TradingCommand::SubmitOrderList(command));
287        }
288
289        for order in &orders {
290            self.set_gtd_expiry(order)?;
291        }
292
293        Ok(())
294    }
295
296    /// Submits an order list with adapter-specific parameters.
297    ///
298    /// # Errors
299    ///
300    /// Returns an error if the strategy is not registered, the order list is invalid,
301    /// or order list submission fails.
302    fn submit_order_list_with_params(
303        &mut self,
304        mut orders: Vec<OrderAny>,
305        position_id: Option<PositionId>,
306        client_id: Option<ClientId>,
307        params: Params,
308    ) -> anyhow::Result<()> {
309        let should_deny = {
310            let core = self.core_mut();
311            let tag = core.market_exit_tag;
312            core.is_exiting
313                && orders.iter().any(|o| {
314                    !o.is_reduce_only() && !o.tags().is_some_and(|tags| tags.contains(&tag))
315                })
316        };
317
318        if should_deny {
319            self.deny_order_list(&orders, Ustr::from("MARKET_EXIT_IN_PROGRESS"));
320            return Ok(());
321        }
322
323        let core = self.core_mut();
324
325        let trader_id = core.trader_id().expect("Trader ID not set");
326        let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
327        let ts_init = core.clock().timestamp_ns();
328
329        // TODO: Replace with fluent builder API for order list construction
330        let order_list = if orders.first().is_some_and(|o| o.order_list_id().is_some()) {
331            OrderList::from_orders(&orders, ts_init)
332        } else {
333            core.order_factory().create_list(&mut orders, ts_init)
334        };
335
336        {
337            let cache_rc = core.cache_rc();
338            let cache = cache_rc.borrow();
339            if cache.order_list_exists(&order_list.id) {
340                anyhow::bail!("OrderList denied: duplicate {}", order_list.id);
341            }
342
343            for order in &orders {
344                if order.status() != OrderStatus::Initialized {
345                    anyhow::bail!(
346                        "Order in list denied: invalid status for {}, expected INITIALIZED",
347                        order.client_order_id()
348                    );
349                }
350
351                if cache.order_exists(&order.client_order_id()) {
352                    anyhow::bail!(
353                        "Order in list denied: duplicate {}",
354                        order.client_order_id()
355                    );
356                }
357            }
358        }
359
360        {
361            let cache_rc = core.cache_rc();
362            let mut cache = cache_rc.borrow_mut();
363            cache.add_order_list(order_list.clone())?;
364            for order in &orders {
365                cache.add_order(order.clone(), position_id, client_id, true)?;
366            }
367        }
368
369        let params_opt = if params.is_empty() {
370            None
371        } else {
372            Some(params)
373        };
374
375        let first_order = orders.first();
376        let order_inits: Vec<_> = orders.iter().map(|o| o.init_event().clone()).collect();
377        let exec_algorithm_id = first_order.and_then(|o| o.exec_algorithm_id());
378
379        let command = SubmitOrderList::new(
380            trader_id,
381            client_id,
382            strategy_id,
383            order_list,
384            order_inits,
385            exec_algorithm_id,
386            position_id,
387            params_opt,
388            UUID4::new(),
389            ts_init,
390        );
391
392        let has_emulated_order = orders.iter().any(|o| {
393            matches!(o.emulation_trigger(), Some(trigger) if trigger != TriggerType::NoTrigger)
394                || o.is_emulated()
395        });
396
397        let manager = core.order_manager();
398
399        if has_emulated_order {
400            manager.send_emulator_command(TradingCommand::SubmitOrderList(command));
401        } else if let Some(algo_id) = exec_algorithm_id {
402            let endpoint = format!("{algo_id}.execute");
403            msgbus::send_any(endpoint.into(), &TradingCommand::SubmitOrderList(command));
404        } else {
405            manager.send_risk_command(TradingCommand::SubmitOrderList(command));
406        }
407
408        for order in &orders {
409            self.set_gtd_expiry(order)?;
410        }
411
412        Ok(())
413    }
414
415    /// Modifies an order.
416    ///
417    /// # Errors
418    ///
419    /// Returns an error if the strategy is not registered or order modification fails.
420    fn modify_order(
421        &mut self,
422        order: OrderAny,
423        quantity: Option<Quantity>,
424        price: Option<Price>,
425        trigger_price: Option<Price>,
426        client_id: Option<ClientId>,
427    ) -> anyhow::Result<()> {
428        self.modify_order_with_params(
429            order,
430            quantity,
431            price,
432            trigger_price,
433            client_id,
434            Params::new(),
435        )
436    }
437
438    /// Modifies an order with adapter-specific parameters.
439    ///
440    /// # Errors
441    ///
442    /// Returns an error if the strategy is not registered or order modification fails.
443    fn modify_order_with_params(
444        &mut self,
445        order: OrderAny,
446        quantity: Option<Quantity>,
447        price: Option<Price>,
448        trigger_price: Option<Price>,
449        client_id: Option<ClientId>,
450        params: Params,
451    ) -> anyhow::Result<()> {
452        let core = self.core_mut();
453
454        let trader_id = core.trader_id().expect("Trader ID not set");
455        let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
456        let ts_init = core.clock().timestamp_ns();
457
458        let params = if params.is_empty() {
459            None
460        } else {
461            Some(params)
462        };
463
464        let command = ModifyOrder::new(
465            trader_id,
466            client_id,
467            strategy_id,
468            order.instrument_id(),
469            order.client_order_id(),
470            order.venue_order_id(),
471            quantity,
472            price,
473            trigger_price,
474            UUID4::new(),
475            ts_init,
476            params,
477        );
478
479        let manager = core.order_manager();
480
481        if matches!(order.emulation_trigger(), Some(trigger) if trigger != TriggerType::NoTrigger) {
482            manager.send_emulator_command(TradingCommand::ModifyOrder(command));
483        } else {
484            manager.send_risk_command(TradingCommand::ModifyOrder(command));
485        }
486        Ok(())
487    }
488
489    /// Cancels an order.
490    ///
491    /// # Errors
492    ///
493    /// Returns an error if the strategy is not registered or order cancellation fails.
494    fn cancel_order(&mut self, order: OrderAny, client_id: Option<ClientId>) -> anyhow::Result<()> {
495        self.cancel_order_with_params(order, client_id, Params::new())
496    }
497
498    /// Cancels an order with adapter-specific parameters.
499    ///
500    /// # Errors
501    ///
502    /// Returns an error if the strategy is not registered or order cancellation fails.
503    fn cancel_order_with_params(
504        &mut self,
505        order: OrderAny,
506        client_id: Option<ClientId>,
507        params: Params,
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 = StrategyId::from(core.actor_id().inner().as_str());
513        let ts_init = core.clock().timestamp_ns();
514
515        let params = if params.is_empty() {
516            None
517        } else {
518            Some(params)
519        };
520
521        let command = CancelOrder::new(
522            trader_id,
523            client_id,
524            strategy_id,
525            order.instrument_id(),
526            order.client_order_id(),
527            order.venue_order_id(),
528            UUID4::new(),
529            ts_init,
530            params,
531        );
532
533        let manager = core.order_manager();
534
535        if matches!(order.emulation_trigger(), Some(trigger) if trigger != TriggerType::NoTrigger)
536            || order.is_emulated()
537        {
538            manager.send_emulator_command(TradingCommand::CancelOrder(command));
539        } else if let Some(algo_id) = order.exec_algorithm_id() {
540            let endpoint = format!("{algo_id}.execute");
541            msgbus::send_any(endpoint.into(), &TradingCommand::CancelOrder(command));
542        } else {
543            manager.send_exec_command(TradingCommand::CancelOrder(command));
544        }
545        Ok(())
546    }
547
548    /// Batch cancels multiple orders for the same instrument.
549    ///
550    /// # Errors
551    ///
552    /// Returns an error if the strategy is not registered, the orders span multiple instruments,
553    /// or contain emulated/local orders.
554    fn cancel_orders(
555        &mut self,
556        mut orders: Vec<OrderAny>,
557        client_id: Option<ClientId>,
558        params: Option<Params>,
559    ) -> anyhow::Result<()> {
560        if orders.is_empty() {
561            anyhow::bail!("Cannot batch cancel empty order list");
562        }
563
564        let core = self.core_mut();
565        let trader_id = core.trader_id().expect("Trader ID not set");
566        let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
567        let ts_init = core.clock().timestamp_ns();
568
569        let manager = core.order_manager();
570
571        let first = orders.remove(0);
572        let instrument_id = first.instrument_id();
573
574        if first.is_emulated() || first.is_active_local() {
575            anyhow::bail!("Cannot include emulated or local orders in batch cancel");
576        }
577
578        let mut cancels = Vec::with_capacity(orders.len() + 1);
579        cancels.push(CancelOrder::new(
580            trader_id,
581            client_id,
582            strategy_id,
583            instrument_id,
584            first.client_order_id(),
585            first.venue_order_id(),
586            UUID4::new(),
587            ts_init,
588            params.clone(),
589        ));
590
591        for order in orders {
592            if order.instrument_id() != instrument_id {
593                anyhow::bail!(
594                    "Cannot batch cancel orders for different instruments: {} vs {}",
595                    instrument_id,
596                    order.instrument_id()
597                );
598            }
599
600            if order.is_emulated() || order.is_active_local() {
601                anyhow::bail!("Cannot include emulated or local orders in batch cancel");
602            }
603
604            cancels.push(CancelOrder::new(
605                trader_id,
606                client_id,
607                strategy_id,
608                instrument_id,
609                order.client_order_id(),
610                order.venue_order_id(),
611                UUID4::new(),
612                ts_init,
613                params.clone(),
614            ));
615        }
616
617        let command = BatchCancelOrders::new(
618            trader_id,
619            client_id,
620            strategy_id,
621            instrument_id,
622            cancels,
623            UUID4::new(),
624            ts_init,
625            params,
626        );
627
628        manager.send_exec_command(TradingCommand::BatchCancelOrders(command));
629        Ok(())
630    }
631
632    /// Cancels all open orders for the given instrument.
633    ///
634    /// # Errors
635    ///
636    /// Returns an error if the strategy is not registered or order cancellation fails.
637    fn cancel_all_orders(
638        &mut self,
639        instrument_id: InstrumentId,
640        order_side: Option<OrderSide>,
641        client_id: Option<ClientId>,
642    ) -> anyhow::Result<()> {
643        self.cancel_all_orders_with_params(instrument_id, order_side, client_id, Params::new())
644    }
645
646    /// Cancels all open orders for the given instrument with adapter-specific parameters.
647    ///
648    /// # Errors
649    ///
650    /// Returns an error if the strategy is not registered or order cancellation fails.
651    fn cancel_all_orders_with_params(
652        &mut self,
653        instrument_id: InstrumentId,
654        order_side: Option<OrderSide>,
655        client_id: Option<ClientId>,
656        params: Params,
657    ) -> anyhow::Result<()> {
658        let params = if params.is_empty() {
659            None
660        } else {
661            Some(params)
662        };
663        let core = self.core_mut();
664
665        let trader_id = core.trader_id().expect("Trader ID not set");
666        let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
667        let ts_init = core.clock().timestamp_ns();
668        let cache = core.cache();
669
670        let open_orders = cache.orders_open(
671            None,
672            Some(&instrument_id),
673            Some(&strategy_id),
674            None,
675            order_side,
676        );
677
678        let emulated_orders = cache.orders_emulated(
679            None,
680            Some(&instrument_id),
681            Some(&strategy_id),
682            None,
683            order_side,
684        );
685
686        let inflight_orders = cache.orders_inflight(
687            None,
688            Some(&instrument_id),
689            Some(&strategy_id),
690            None,
691            order_side,
692        );
693
694        // Sort the algorithm IDs so the per-algo cancel cascade fires msgbus
695        // events in a deterministic order across runs; the cache returns an
696        // unordered AHashSet.
697        let mut exec_algorithm_ids: Vec<_> = cache.exec_algorithm_ids().into_iter().collect();
698        exec_algorithm_ids.sort();
699        let mut algo_orders = Vec::new();
700
701        for algo_id in &exec_algorithm_ids {
702            let orders = cache.orders_for_exec_algorithm(
703                algo_id,
704                None,
705                Some(&instrument_id),
706                Some(&strategy_id),
707                None,
708                order_side,
709            );
710            algo_orders.extend(orders.iter().map(|o| (*o).clone()));
711        }
712
713        let open_count = open_orders.len();
714        let emulated_count = emulated_orders.len();
715        let inflight_count = inflight_orders.len();
716        let algo_count = algo_orders.len();
717
718        drop(cache);
719
720        if open_count == 0 && emulated_count == 0 && inflight_count == 0 && algo_count == 0 {
721            let side_str = order_side.map(|s| format!(" {s}")).unwrap_or_default();
722            log::info!("No {instrument_id} open, emulated, or inflight{side_str} orders to cancel");
723            return Ok(());
724        }
725
726        let manager = core.order_manager();
727
728        let side_str = order_side.map(|s| format!(" {s}")).unwrap_or_default();
729
730        if open_count > 0 {
731            log::info!(
732                "Canceling {open_count} open{side_str} {instrument_id} order{}",
733                if open_count == 1 { "" } else { "s" }
734            );
735        }
736
737        if emulated_count > 0 {
738            log::info!(
739                "Canceling {emulated_count} emulated{side_str} {instrument_id} order{}",
740                if emulated_count == 1 { "" } else { "s" }
741            );
742        }
743
744        if inflight_count > 0 {
745            log::info!(
746                "Canceling {inflight_count} inflight{side_str} {instrument_id} order{}",
747                if inflight_count == 1 { "" } else { "s" }
748            );
749        }
750
751        if open_count > 0 || inflight_count > 0 {
752            let command = CancelAllOrders::new(
753                trader_id,
754                client_id,
755                strategy_id,
756                instrument_id,
757                order_side.unwrap_or(OrderSide::NoOrderSide),
758                UUID4::new(),
759                ts_init,
760                params.clone(),
761            );
762
763            manager.send_exec_command(TradingCommand::CancelAllOrders(command));
764        }
765
766        if emulated_count > 0 {
767            let command = CancelAllOrders::new(
768                trader_id,
769                client_id,
770                strategy_id,
771                instrument_id,
772                order_side.unwrap_or(OrderSide::NoOrderSide),
773                UUID4::new(),
774                ts_init,
775                params,
776            );
777
778            manager.send_emulator_command(TradingCommand::CancelAllOrders(command));
779        }
780
781        for order in algo_orders {
782            self.cancel_order(order, client_id)?;
783        }
784
785        Ok(())
786    }
787
788    /// Closes a position by submitting a market order for the opposite side.
789    ///
790    /// # Errors
791    ///
792    /// Returns an error if the strategy is not registered or position closing fails.
793    fn close_position(
794        &mut self,
795        position: &Position,
796        client_id: Option<ClientId>,
797        tags: Option<Vec<Ustr>>,
798        time_in_force: Option<TimeInForce>,
799        reduce_only: Option<bool>,
800        quote_quantity: Option<bool>,
801    ) -> anyhow::Result<()> {
802        let core = self.core_mut();
803
804        if position.is_closed() {
805            log::warn!("Cannot close position (already closed): {}", position.id);
806            return Ok(());
807        }
808
809        let closing_side = OrderCore::closing_side(position.side);
810
811        let order = core.order_factory().market(
812            position.instrument_id,
813            closing_side,
814            position.quantity,
815            time_in_force,
816            reduce_only.or(Some(true)),
817            quote_quantity,
818            None,
819            None,
820            tags,
821            None,
822        );
823
824        self.submit_order(order, Some(position.id), client_id)
825    }
826
827    /// Closes all open positions for the given instrument.
828    ///
829    /// # Errors
830    ///
831    /// Returns an error if the strategy is not registered or position closing fails.
832    #[expect(clippy::too_many_arguments)]
833    fn close_all_positions(
834        &mut self,
835        instrument_id: InstrumentId,
836        position_side: Option<PositionSide>,
837        client_id: Option<ClientId>,
838        tags: Option<Vec<Ustr>>,
839        time_in_force: Option<TimeInForce>,
840        reduce_only: Option<bool>,
841        quote_quantity: Option<bool>,
842    ) -> anyhow::Result<()> {
843        let core = self.core_mut();
844        let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
845        let cache = core.cache();
846
847        let positions_open = cache.positions_open(
848            None,
849            Some(&instrument_id),
850            Some(&strategy_id),
851            None,
852            position_side,
853        );
854
855        let side_str = position_side.map(|s| format!(" {s}")).unwrap_or_default();
856
857        if positions_open.is_empty() {
858            log::info!("No {instrument_id} open{side_str} positions to close");
859            return Ok(());
860        }
861
862        let count = positions_open.len();
863        log::info!(
864            "Closing {count} open{side_str} position{}",
865            if count == 1 { "" } else { "s" }
866        );
867
868        let positions_data: Vec<_> = positions_open
869            .iter()
870            .map(|p| (p.id, p.instrument_id, p.side, p.quantity, p.is_closed()))
871            .collect();
872
873        drop(cache);
874
875        for (pos_id, pos_instrument_id, pos_side, pos_quantity, is_closed) in positions_data {
876            if is_closed {
877                continue;
878            }
879
880            let core = self.core_mut();
881            let closing_side = OrderCore::closing_side(pos_side);
882            let order = core.order_factory().market(
883                pos_instrument_id,
884                closing_side,
885                pos_quantity,
886                time_in_force,
887                reduce_only.or(Some(true)),
888                quote_quantity,
889                None,
890                None,
891                tags.clone(),
892                None,
893            );
894
895            self.submit_order(order, Some(pos_id), client_id)?;
896        }
897
898        Ok(())
899    }
900
901    /// Queries account state from the execution client.
902    ///
903    /// Creates a [`QueryAccount`] command and sends it to the execution engine,
904    /// which will request the current account state from the execution client.
905    ///
906    /// # Errors
907    ///
908    /// Returns an error if the strategy is not registered.
909    fn query_account(
910        &mut self,
911        account_id: AccountId,
912        client_id: Option<ClientId>,
913        params: Option<Params>,
914    ) -> anyhow::Result<()> {
915        let core = self.core_mut();
916
917        let trader_id = core.trader_id().expect("Trader ID not set");
918        let ts_init = core.clock().timestamp_ns();
919
920        let command = QueryAccount::new(
921            trader_id,
922            client_id,
923            account_id,
924            UUID4::new(),
925            ts_init,
926            params,
927        );
928
929        core.order_manager()
930            .send_exec_command(TradingCommand::QueryAccount(command));
931        Ok(())
932    }
933
934    /// Queries order state from the execution client.
935    ///
936    /// Creates a [`QueryOrder`] command and sends it to the execution engine,
937    /// which will request the current order state from the execution client.
938    ///
939    /// # Errors
940    ///
941    /// Returns an error if the strategy is not registered.
942    fn query_order(
943        &mut self,
944        order: &OrderAny,
945        client_id: Option<ClientId>,
946        params: Option<Params>,
947    ) -> anyhow::Result<()> {
948        let core = self.core_mut();
949
950        let trader_id = core.trader_id().expect("Trader ID not set");
951        let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
952        let ts_init = core.clock().timestamp_ns();
953
954        let command = QueryOrder::new(
955            trader_id,
956            client_id,
957            strategy_id,
958            order.instrument_id(),
959            order.client_order_id(),
960            order.venue_order_id(),
961            UUID4::new(),
962            ts_init,
963            params,
964        );
965
966        core.order_manager()
967            .send_exec_command(TradingCommand::QueryOrder(command));
968        Ok(())
969    }
970
971    /// Handles an order event, dispatching to the appropriate handler and routing to the order manager.
972    fn handle_order_event(&mut self, event: OrderEventAny) {
973        let state = {
974            let core = self.core_mut();
975            let id = &core.actor.actor_id;
976            let is_warning = matches!(
977                &event,
978                OrderEventAny::Denied(_)
979                    | OrderEventAny::Rejected(_)
980                    | OrderEventAny::CancelRejected(_)
981                    | OrderEventAny::ModifyRejected(_)
982            );
983
984            if is_warning {
985                log::warn!("{id} {RECV}{EVT} {event}");
986            } else if core.config.log_events {
987                log::info!("{id} {RECV}{EVT} {event}");
988            }
989
990            core.actor.state()
991        };
992
993        let client_order_id = event.client_order_id();
994        let is_terminal = matches!(
995            &event,
996            OrderEventAny::Filled(_)
997                | OrderEventAny::Canceled(_)
998                | OrderEventAny::Rejected(_)
999                | OrderEventAny::Expired(_)
1000                | OrderEventAny::Denied(_)
1001        );
1002
1003        // GTD timer cleanup runs regardless of state so timers do not leak when
1004        // terminal events arrive during the post-stop delay.
1005        if is_terminal {
1006            self.cancel_gtd_expiry(&client_order_id);
1007        }
1008
1009        // Events are logged unconditionally so residual events received after stop
1010        // remain observable, but dispatch is gated on the running state.
1011        if state != ComponentState::Running {
1012            return;
1013        }
1014
1015        // Contingent order manager observes events before user handlers so OCO
1016        // bookkeeping is consistent with what the strategy then sees.
1017        {
1018            let core = self.core_mut();
1019            if let Some(manager) = &mut core.order_manager {
1020                manager.handle_event(&event);
1021            }
1022        }
1023
1024        match &event {
1025            OrderEventAny::Initialized(e) => self.on_order_initialized(e.clone()),
1026            OrderEventAny::Denied(e) => self.on_order_denied(*e),
1027            OrderEventAny::Emulated(e) => self.on_order_emulated(*e),
1028            OrderEventAny::Released(e) => self.on_order_released(*e),
1029            OrderEventAny::Submitted(e) => self.on_order_submitted(*e),
1030            OrderEventAny::Rejected(e) => self.on_order_rejected(*e),
1031            OrderEventAny::Accepted(e) => self.on_order_accepted(*e),
1032            OrderEventAny::Canceled(e) => {
1033                let _ = DataActor::on_order_canceled(self, e);
1034            }
1035            OrderEventAny::Expired(e) => self.on_order_expired(*e),
1036            OrderEventAny::Triggered(e) => self.on_order_triggered(*e),
1037            OrderEventAny::PendingUpdate(e) => self.on_order_pending_update(*e),
1038            OrderEventAny::PendingCancel(e) => self.on_order_pending_cancel(*e),
1039            OrderEventAny::ModifyRejected(e) => self.on_order_modify_rejected(*e),
1040            OrderEventAny::CancelRejected(e) => self.on_order_cancel_rejected(*e),
1041            OrderEventAny::Updated(e) => self.on_order_updated(*e),
1042            OrderEventAny::Filled(e) => {
1043                let _ = DataActor::on_order_filled(self, e);
1044            }
1045        }
1046    }
1047
1048    /// Handles a position event, dispatching to the appropriate handler.
1049    fn handle_position_event(&mut self, event: PositionEvent) {
1050        let state = {
1051            let core = self.core_mut();
1052
1053            if core.config.log_events {
1054                let id = &core.actor.actor_id;
1055                log::info!("{id} {RECV}{EVT} {event:?}");
1056            }
1057
1058            core.actor.state()
1059        };
1060
1061        if state != ComponentState::Running {
1062            return;
1063        }
1064
1065        match event {
1066            PositionEvent::PositionOpened(e) => self.on_position_opened(e),
1067            PositionEvent::PositionChanged(e) => self.on_position_changed(e),
1068            PositionEvent::PositionClosed(e) => self.on_position_closed(e),
1069            PositionEvent::PositionAdjusted(_) => {
1070                // No handler for adjusted events yet
1071            }
1072        }
1073    }
1074
1075    // -- LIFECYCLE METHODS -----------------------------------------------------------------------
1076
1077    /// Called when the strategy is started.
1078    ///
1079    /// Override this method to implement custom initialization logic.
1080    /// The default implementation reactivates GTD timers if `manage_gtd_expiry` is enabled.
1081    ///
1082    /// # Errors
1083    ///
1084    /// Returns an error if strategy initialization fails.
1085    fn on_start(&mut self) -> anyhow::Result<()> {
1086        let core = self.core_mut();
1087        let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
1088        log::info!("Starting {strategy_id}");
1089
1090        if core.config.manage_gtd_expiry {
1091            self.reactivate_gtd_timers();
1092        }
1093
1094        Ok(())
1095    }
1096
1097    /// Called when a time event is received.
1098    ///
1099    /// Routes GTD expiry timer events to the expiry handler and market exit timer events
1100    /// to the market exit checker.
1101    ///
1102    /// # Errors
1103    ///
1104    /// Returns an error if time event handling fails.
1105    fn on_time_event(&mut self, event: &TimeEvent) -> anyhow::Result<()> {
1106        if event.name.starts_with("GTD-EXPIRY:") {
1107            self.expire_gtd_order(event.clone());
1108        } else if event.name.starts_with("MARKET_EXIT_CHECK:") {
1109            self.check_market_exit(event.clone());
1110        }
1111        Ok(())
1112    }
1113
1114    // -- EVENT HANDLERS --------------------------------------------------------------------------
1115
1116    /// Called when an order is initialized.
1117    ///
1118    /// Override this method to implement custom logic when an order is first created.
1119    #[allow(unused_variables)]
1120    fn on_order_initialized(&mut self, event: OrderInitialized) {}
1121
1122    /// Called when an order is denied by the system.
1123    ///
1124    /// Override this method to implement custom logic when an order is denied before submission.
1125    #[allow(unused_variables)]
1126    fn on_order_denied(&mut self, event: OrderDenied) {}
1127
1128    /// Called when an order is emulated.
1129    ///
1130    /// Override this method to implement custom logic when an order is taken over by the emulator.
1131    #[allow(unused_variables)]
1132    fn on_order_emulated(&mut self, event: OrderEmulated) {}
1133
1134    /// Called when an order is released from emulation.
1135    ///
1136    /// Override this method to implement custom logic when an emulated order is released.
1137    #[allow(unused_variables)]
1138    fn on_order_released(&mut self, event: OrderReleased) {}
1139
1140    /// Called when an order is submitted to the venue.
1141    ///
1142    /// Override this method to implement custom logic when an order is submitted.
1143    #[allow(unused_variables)]
1144    fn on_order_submitted(&mut self, event: OrderSubmitted) {}
1145
1146    /// Called when an order is rejected by the venue.
1147    ///
1148    /// Override this method to implement custom logic when an order is rejected.
1149    #[allow(unused_variables)]
1150    fn on_order_rejected(&mut self, event: OrderRejected) {}
1151
1152    /// Called when an order is accepted by the venue.
1153    ///
1154    /// Override this method to implement custom logic when an order is accepted.
1155    #[allow(unused_variables)]
1156    fn on_order_accepted(&mut self, event: OrderAccepted) {}
1157
1158    /// Called when an order expires.
1159    ///
1160    /// Override this method to implement custom logic when an order expires.
1161    #[allow(unused_variables)]
1162    fn on_order_expired(&mut self, event: OrderExpired) {}
1163
1164    /// Called when an order is triggered.
1165    ///
1166    /// Override this method to implement custom logic when a stop or conditional order is triggered.
1167    #[allow(unused_variables)]
1168    fn on_order_triggered(&mut self, event: OrderTriggered) {}
1169
1170    /// Called when an order modification is pending.
1171    ///
1172    /// Override this method to implement custom logic when an order is pending modification.
1173    #[allow(unused_variables)]
1174    fn on_order_pending_update(&mut self, event: OrderPendingUpdate) {}
1175
1176    /// Called when an order cancellation is pending.
1177    ///
1178    /// Override this method to implement custom logic when an order is pending cancellation.
1179    #[allow(unused_variables)]
1180    fn on_order_pending_cancel(&mut self, event: OrderPendingCancel) {}
1181
1182    /// Called when an order modification is rejected.
1183    ///
1184    /// Override this method to implement custom logic when an order modification is rejected.
1185    #[allow(unused_variables)]
1186    fn on_order_modify_rejected(&mut self, event: OrderModifyRejected) {}
1187
1188    /// Called when an order cancellation is rejected.
1189    ///
1190    /// Override this method to implement custom logic when an order cancellation is rejected.
1191    #[allow(unused_variables)]
1192    fn on_order_cancel_rejected(&mut self, event: OrderCancelRejected) {}
1193
1194    /// Called when an order is updated.
1195    ///
1196    /// Override this method to implement custom logic when an order is modified.
1197    #[allow(unused_variables)]
1198    fn on_order_updated(&mut self, event: OrderUpdated) {}
1199
1200    // Note: on_order_filled is inherited from DataActor trait
1201
1202    /// Called when a position is opened.
1203    ///
1204    /// Override this method to implement custom logic when a position is opened.
1205    #[allow(unused_variables)]
1206    fn on_position_opened(&mut self, event: PositionOpened) {}
1207
1208    /// Called when a position is changed (quantity or price updated).
1209    ///
1210    /// Override this method to implement custom logic when a position changes.
1211    #[allow(unused_variables)]
1212    fn on_position_changed(&mut self, event: PositionChanged) {}
1213
1214    /// Called when a position is closed.
1215    ///
1216    /// Override this method to implement custom logic when a position is closed.
1217    #[allow(unused_variables)]
1218    fn on_position_closed(&mut self, event: PositionClosed) {}
1219
1220    /// Called when a market exit has been initiated.
1221    ///
1222    /// Override this method to implement custom logic when a market exit begins.
1223    fn on_market_exit(&mut self) {}
1224
1225    /// Called after a market exit has completed.
1226    ///
1227    /// Override this method to implement custom logic after a market exit completes.
1228    fn post_market_exit(&mut self) {}
1229
1230    /// Returns whether the strategy is currently executing a market exit.
1231    ///
1232    /// Strategies can check this to avoid submitting new orders during exit.
1233    fn is_exiting(&self) -> bool {
1234        self.core().is_exiting
1235    }
1236
1237    /// Initiates an iterative market exit for the strategy.
1238    ///
1239    /// Will cancel all open orders and close all open positions, and wait for
1240    /// all in-flight orders to resolve and positions to close. The strategy
1241    /// remains running after the exit completes.
1242    ///
1243    /// The `on_market_exit` hook is called when the exit process begins.
1244    /// The `post_market_exit` hook is called when the exit process completes.
1245    ///
1246    /// Uses `market_exit_time_in_force` and `market_exit_reduce_only` from
1247    /// the strategy config for closing market orders.
1248    ///
1249    /// # Errors
1250    ///
1251    /// Returns an error if the market exit cannot be initiated.
1252    fn market_exit(&mut self) -> anyhow::Result<()> {
1253        let core = self.core_mut();
1254        let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
1255
1256        if core.actor.state() != ComponentState::Running {
1257            log::warn!("{strategy_id} Cannot market exit: strategy is not running");
1258            return Ok(());
1259        }
1260
1261        if core.is_exiting {
1262            log::warn!("{strategy_id} Market exit called when already in progress");
1263            return Ok(());
1264        }
1265
1266        core.is_exiting = true;
1267        core.market_exit_attempts = 0;
1268        let time_in_force = core.config.market_exit_time_in_force;
1269        let reduce_only = core.config.market_exit_reduce_only;
1270
1271        log::info!("{strategy_id} Initiating market exit...");
1272
1273        self.on_market_exit();
1274
1275        let core = self.core_mut();
1276        let cache = core.cache();
1277
1278        let open_orders = cache.orders_open(None, None, Some(&strategy_id), None, None);
1279        let inflight_orders = cache.orders_inflight(None, None, Some(&strategy_id), None, None);
1280        let open_positions = cache.positions_open(None, None, Some(&strategy_id), None, None);
1281
1282        let mut instruments: AHashSet<InstrumentId> = AHashSet::new();
1283
1284        for order in &open_orders {
1285            instruments.insert(order.instrument_id());
1286        }
1287
1288        for order in &inflight_orders {
1289            instruments.insert(order.instrument_id());
1290        }
1291
1292        for position in &open_positions {
1293            instruments.insert(position.instrument_id);
1294        }
1295
1296        let market_exit_tag = core.market_exit_tag;
1297        // Sort so the per-instrument cancel_all_orders/close_all_positions
1298        // cascade fires msgbus commands in a deterministic sequence; the
1299        // upstream dedup is AHash-backed.
1300        let mut instruments: Vec<_> = instruments.into_iter().collect();
1301        instruments.sort();
1302        drop(cache);
1303
1304        for instrument_id in instruments {
1305            if let Err(e) = self.cancel_all_orders(instrument_id, None, None) {
1306                log::error!("Error canceling orders for {instrument_id}: {e}");
1307            }
1308
1309            if let Err(e) = self.close_all_positions(
1310                instrument_id,
1311                None,
1312                None,
1313                Some(vec![market_exit_tag]),
1314                Some(time_in_force),
1315                Some(reduce_only),
1316                None,
1317            ) {
1318                log::error!("Error closing positions for {instrument_id}: {e}");
1319            }
1320        }
1321
1322        let core = self.core_mut();
1323        let interval_ms = core.config.market_exit_interval_ms;
1324        let timer_name = core.market_exit_timer_name;
1325
1326        log::info!("{strategy_id} Setting market exit timer at {interval_ms}ms intervals");
1327
1328        let interval_ns = interval_ms * 1_000_000;
1329        let result = core.clock().set_timer_ns(
1330            timer_name.as_str(),
1331            interval_ns,
1332            None,
1333            None,
1334            None,
1335            None,
1336            None,
1337        );
1338
1339        if let Err(e) = result {
1340            // Reset exit state on timer failure (caller handles pending_stop)
1341            core.is_exiting = false;
1342            core.market_exit_attempts = 0;
1343            return Err(e);
1344        }
1345
1346        Ok(())
1347    }
1348
1349    /// Checks if the market exit is complete and finalizes if so.
1350    ///
1351    /// This method is called by the market exit timer.
1352    fn check_market_exit(&mut self, _event: TimeEvent) {
1353        // Guard against stale timer events after cancel_market_exit
1354        if !self.is_exiting() {
1355            return;
1356        }
1357
1358        let core = self.core_mut();
1359        let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
1360
1361        core.market_exit_attempts += 1;
1362        let attempts = core.market_exit_attempts;
1363        let max_attempts = core.config.market_exit_max_attempts;
1364
1365        log::debug!(
1366            "{strategy_id} Market exit check triggered (attempt {attempts}/{max_attempts})"
1367        );
1368
1369        if attempts >= max_attempts {
1370            let cache = core.cache();
1371            let open_orders_count = cache
1372                .orders_open(None, None, Some(&strategy_id), None, None)
1373                .len();
1374            let inflight_orders_count = cache
1375                .orders_inflight(None, None, Some(&strategy_id), None, None)
1376                .len();
1377            let open_positions_count = cache
1378                .positions_open(None, None, Some(&strategy_id), None, None)
1379                .len();
1380            drop(cache);
1381
1382            log::warn!(
1383                "{strategy_id} Market exit max attempts ({max_attempts}) reached, \
1384                completing with open orders: {open_orders_count}, \
1385                inflight orders: {inflight_orders_count}, \
1386                open positions: {open_positions_count}"
1387            );
1388
1389            self.finalize_market_exit();
1390            return;
1391        }
1392
1393        let cache = core.cache();
1394        let open_orders = cache.orders_open(None, None, Some(&strategy_id), None, None);
1395        let inflight_orders = cache.orders_inflight(None, None, Some(&strategy_id), None, None);
1396
1397        if !open_orders.is_empty() || !inflight_orders.is_empty() {
1398            return;
1399        }
1400
1401        let open_positions = cache.positions_open(None, None, Some(&strategy_id), None, None);
1402
1403        if !open_positions.is_empty() {
1404            // If there are open positions but no orders, re-send close orders
1405            let positions_data: Vec<_> = open_positions
1406                .iter()
1407                .map(|p| (p.id, p.instrument_id, p.side, p.quantity, p.is_closed()))
1408                .collect();
1409
1410            drop(cache);
1411
1412            for (pos_id, instrument_id, side, quantity, is_closed) in positions_data {
1413                if is_closed {
1414                    continue;
1415                }
1416
1417                let core = self.core_mut();
1418                let time_in_force = core.config.market_exit_time_in_force;
1419                let reduce_only = core.config.market_exit_reduce_only;
1420                let market_exit_tag = core.market_exit_tag;
1421                let closing_side = OrderCore::closing_side(side);
1422                let order = core.order_factory().market(
1423                    instrument_id,
1424                    closing_side,
1425                    quantity,
1426                    Some(time_in_force),
1427                    Some(reduce_only),
1428                    None,
1429                    None,
1430                    None,
1431                    Some(vec![market_exit_tag]),
1432                    None,
1433                );
1434
1435                if let Err(e) = self.submit_order(order, Some(pos_id), None) {
1436                    log::error!("Error re-submitting close order for position {pos_id}: {e}");
1437                }
1438            }
1439            return;
1440        }
1441
1442        drop(cache);
1443        self.finalize_market_exit();
1444    }
1445
1446    /// Finalizes the market exit process.
1447    ///
1448    /// Cancels the market exit timer, resets state, calls the post_market_exit hook,
1449    /// and stops the strategy if a stop was pending.
1450    fn finalize_market_exit(&mut self) {
1451        let (strategy_id, should_stop) = {
1452            let core = self.core_mut();
1453            let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
1454            let should_stop = core.pending_stop;
1455            (strategy_id, should_stop)
1456        };
1457
1458        self.cancel_market_exit();
1459
1460        let hook_result = catch_unwind(AssertUnwindSafe(|| {
1461            self.post_market_exit();
1462        }));
1463
1464        if let Err(e) = hook_result {
1465            log::error!("{strategy_id} Error in post_market_exit: {e:?}");
1466        }
1467
1468        if should_stop {
1469            log::info!("{strategy_id} Market exit complete, stopping strategy");
1470
1471            if let Err(e) = Component::stop(self) {
1472                log::error!("{strategy_id} Failed to stop: {e}");
1473            }
1474        }
1475
1476        let core = self.core_mut();
1477        debug_assert!(
1478            !(core.pending_stop
1479                && !core.is_exiting
1480                && core.actor.state() == ComponentState::Running),
1481            "INVARIANT: stuck state after finalize_market_exit"
1482        );
1483    }
1484
1485    /// Cancels an active market exit without calling hooks.
1486    ///
1487    /// Used when stop() is called during an active market exit to avoid state leaks.
1488    fn cancel_market_exit(&mut self) {
1489        let core = self.core_mut();
1490        let timer_name = core.market_exit_timer_name;
1491
1492        if core.clock().timer_names().contains(&timer_name.as_str()) {
1493            core.clock().cancel_timer(timer_name.as_str());
1494        }
1495
1496        core.is_exiting = false;
1497        core.pending_stop = false;
1498        core.market_exit_attempts = 0;
1499    }
1500
1501    /// Stops the strategy with optional managed stop behavior.
1502    ///
1503    /// If `manage_stop` is enabled in the config, the strategy will first complete
1504    /// any active market exit (or initiate one) before stopping. If `manage_stop`
1505    /// is disabled, the strategy stops immediately, cleaning up any active market
1506    /// exit state.
1507    ///
1508    /// # Returns
1509    ///
1510    /// Returns `true` if the strategy should proceed with stopping, `false` if
1511    /// the stop is being deferred until market exit completes.
1512    fn stop(&mut self) -> bool {
1513        let (manage_stop, is_exiting, should_initiate_exit) = {
1514            let core = self.core_mut();
1515            let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
1516            let manage_stop = core.config.manage_stop;
1517            let state = core.actor.state();
1518            let pending_stop = core.pending_stop;
1519            let is_exiting = core.is_exiting;
1520
1521            if manage_stop {
1522                if state != ComponentState::Running {
1523                    return true; // Proceed with stop
1524                }
1525
1526                if pending_stop {
1527                    return false; // Already waiting for market exit
1528                }
1529
1530                core.pending_stop = true;
1531                let should_initiate_exit = !is_exiting;
1532
1533                if should_initiate_exit {
1534                    log::info!("{strategy_id} Initiating market exit before stop");
1535                }
1536
1537                (manage_stop, is_exiting, should_initiate_exit)
1538            } else {
1539                (manage_stop, is_exiting, false)
1540            }
1541        };
1542
1543        if manage_stop {
1544            if should_initiate_exit && let Err(e) = self.market_exit() {
1545                log::warn!("Market exit failed during stop: {e}, proceeding with stop");
1546                self.core_mut().pending_stop = false;
1547                return true;
1548            }
1549            debug_assert!(
1550                self.is_exiting(),
1551                "INVARIANT: deferring stop but not exiting"
1552            );
1553            return false; // Defer stop until market exit completes
1554        }
1555
1556        // manage_stop is false - clean up any active market exit
1557        if is_exiting {
1558            self.cancel_market_exit();
1559        }
1560
1561        true // Proceed with stop
1562    }
1563
1564    /// Denies an order by generating an OrderDenied event.
1565    ///
1566    /// This method creates an OrderDenied event, applies it to the order,
1567    /// and updates the cache.
1568    fn deny_order(&mut self, order: &OrderAny, reason: Ustr) {
1569        let core = self.core_mut();
1570        let trader_id = core.trader_id().expect("Trader ID not set");
1571        let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
1572        let ts_now = core.clock().timestamp_ns();
1573
1574        let event = OrderDenied::new(
1575            trader_id,
1576            strategy_id,
1577            order.instrument_id(),
1578            order.client_order_id(),
1579            reason,
1580            UUID4::new(),
1581            ts_now,
1582            ts_now,
1583        );
1584
1585        log::warn!(
1586            "{strategy_id} Order {} denied: {reason}",
1587            order.client_order_id()
1588        );
1589
1590        // Add order to cache if not exists, then update with denied event
1591        {
1592            let cache_rc = core.cache_rc();
1593            let mut cache = cache_rc.borrow_mut();
1594            if !cache.order_exists(&order.client_order_id()) {
1595                let _ = cache.add_order(order.clone(), None, None, true);
1596            }
1597        }
1598
1599        // Apply event and update cache
1600        let mut order_clone = order.clone();
1601        if let Err(e) = order_clone.apply(OrderEventAny::Denied(event)) {
1602            log::warn!("Failed to apply OrderDenied event: {e}");
1603            return;
1604        }
1605
1606        {
1607            let cache_rc = core.cache_rc();
1608            let mut cache = cache_rc.borrow_mut();
1609            let _ = cache.update_order(&order_clone);
1610        }
1611    }
1612
1613    /// Denies all orders in an order list.
1614    ///
1615    /// This method denies each non-closed order in the list.
1616    fn deny_order_list(&mut self, orders: &[OrderAny], reason: Ustr) {
1617        for order in orders {
1618            if !order.is_closed() {
1619                self.deny_order(order, reason);
1620            }
1621        }
1622    }
1623
1624    // -- GTD EXPIRY MANAGEMENT -------------------------------------------------------------------
1625
1626    /// Sets a GTD expiry timer for an order.
1627    ///
1628    /// Creates a timer that will automatically cancel the order when it expires.
1629    ///
1630    /// # Errors
1631    ///
1632    /// Returns an error if timer creation fails.
1633    fn set_gtd_expiry(&mut self, order: &OrderAny) -> anyhow::Result<()> {
1634        let core = self.core_mut();
1635
1636        if !core.config.manage_gtd_expiry || order.time_in_force() != TimeInForce::Gtd {
1637            return Ok(());
1638        }
1639
1640        let Some(expire_time) = order.expire_time() else {
1641            return Ok(());
1642        };
1643
1644        let client_order_id = order.client_order_id();
1645        let timer_name = format!("GTD-EXPIRY:{client_order_id}");
1646
1647        let current_time_ns = {
1648            let clock = core.clock();
1649            clock.timestamp_ns()
1650        };
1651
1652        if current_time_ns >= expire_time.as_u64() {
1653            log::info!("GTD order {client_order_id} already expired, canceling immediately");
1654            return self.cancel_order(order.clone(), None);
1655        }
1656
1657        {
1658            let mut clock = core.clock();
1659            clock.set_time_alert_ns(&timer_name, expire_time, None, None)?;
1660        }
1661
1662        core.gtd_timers
1663            .insert(client_order_id, Ustr::from(&timer_name));
1664
1665        log::debug!("Set GTD expiry timer for {client_order_id} at {expire_time}");
1666        Ok(())
1667    }
1668
1669    /// Cancels a GTD expiry timer for an order.
1670    fn cancel_gtd_expiry(&mut self, client_order_id: &ClientOrderId) {
1671        let core = self.core_mut();
1672
1673        if let Some(timer_name) = core.gtd_timers.remove(client_order_id) {
1674            core.clock().cancel_timer(timer_name.as_str());
1675            log::debug!("Canceled GTD expiry timer for {client_order_id}");
1676        }
1677    }
1678
1679    /// Checks if a GTD expiry timer exists for an order.
1680    fn has_gtd_expiry_timer(&mut self, client_order_id: &ClientOrderId) -> bool {
1681        let core = self.core_mut();
1682        core.gtd_timers.contains_key(client_order_id)
1683    }
1684
1685    /// Handles GTD order expiry by canceling the order.
1686    ///
1687    /// This method is called when a GTD expiry timer fires.
1688    fn expire_gtd_order(&mut self, event: TimeEvent) {
1689        let timer_name = event.name.to_string();
1690        let Some(client_order_id_str) = timer_name.strip_prefix("GTD-EXPIRY:") else {
1691            log::error!("Invalid GTD timer name format: {timer_name}");
1692            return;
1693        };
1694
1695        let client_order_id = ClientOrderId::from(client_order_id_str);
1696
1697        let core = self.core_mut();
1698        core.gtd_timers.remove(&client_order_id);
1699
1700        let cache = core.cache();
1701        let Some(order) = cache.order(&client_order_id) else {
1702            log::warn!("GTD order {client_order_id} not found in cache");
1703            return;
1704        };
1705
1706        let order = order.clone();
1707        drop(cache);
1708
1709        log::info!("GTD order {client_order_id} expired");
1710
1711        if let Err(e) = self.cancel_order(order, None) {
1712            log::error!("Failed to cancel expired GTD order {client_order_id}: {e}");
1713        }
1714    }
1715
1716    /// Reactivates GTD timers for open orders on strategy start.
1717    ///
1718    /// Queries the cache for all open GTD orders and creates timers for those
1719    /// that haven't expired yet. Orders that have already expired are canceled immediately.
1720    fn reactivate_gtd_timers(&mut self) {
1721        let core = self.core_mut();
1722        let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
1723        let current_time_ns = core.clock().timestamp_ns();
1724        let cache = core.cache();
1725
1726        let open_orders = cache.orders_open(None, None, Some(&strategy_id), None, None);
1727
1728        let gtd_orders: Vec<_> = open_orders
1729            .iter()
1730            .filter(|o| o.time_in_force() == TimeInForce::Gtd)
1731            .map(|o| (*o).clone())
1732            .collect();
1733
1734        drop(cache);
1735
1736        for order in gtd_orders {
1737            let Some(expire_time) = order.expire_time() else {
1738                continue;
1739            };
1740
1741            let expire_time_ns = expire_time.as_u64();
1742            let client_order_id = order.client_order_id();
1743
1744            if current_time_ns >= expire_time_ns {
1745                log::info!("GTD order {client_order_id} already expired, canceling immediately");
1746                if let Err(e) = self.cancel_order(order, None) {
1747                    log::error!("Failed to cancel expired GTD order {client_order_id}: {e}");
1748                }
1749            } else if let Err(e) = self.set_gtd_expiry(&order) {
1750                log::error!("Failed to set GTD expiry timer for {client_order_id}: {e}");
1751            }
1752        }
1753    }
1754}
1755
1756#[cfg(test)]
1757mod tests {
1758    use std::{cell::RefCell, rc::Rc};
1759
1760    use nautilus_common::{
1761        actor::DataActor,
1762        cache::Cache,
1763        clock::{Clock, TestClock},
1764        component::Component,
1765        msgbus::{
1766            self, MessagingSwitchboard,
1767            stubs::{TypedIntoMessageSavingHandler, get_typed_into_message_saving_handler},
1768        },
1769        timer::{TimeEvent, TimeEventCallback},
1770    };
1771    use nautilus_core::UnixNanos;
1772    use nautilus_model::{
1773        enums::{LiquiditySide, OrderSide, OrderType, PositionSide},
1774        events::{OrderAccepted, OrderCanceled, OrderFilled, OrderRejected},
1775        identifiers::{
1776            AccountId, ClientOrderId, InstrumentId, PositionId, StrategyId, TradeId, TraderId,
1777            VenueOrderId,
1778        },
1779        orders::MarketOrder,
1780        stubs::TestDefault,
1781        types::{Currency, Money},
1782    };
1783    use nautilus_portfolio::portfolio::Portfolio;
1784    use rstest::rstest;
1785
1786    use super::*;
1787    use crate::nautilus_strategy;
1788
1789    #[derive(Debug)]
1790    struct TestStrategy {
1791        core: StrategyCore,
1792        on_order_rejected_called: bool,
1793        on_order_accepted_called: bool,
1794        on_order_canceled_called: bool,
1795        on_order_filled_called: bool,
1796        on_order_expired_called: bool,
1797        on_position_opened_called: bool,
1798        on_position_changed_called: bool,
1799        on_position_closed_called: bool,
1800    }
1801
1802    impl TestStrategy {
1803        fn new(config: StrategyConfig) -> Self {
1804            Self {
1805                core: StrategyCore::new(config),
1806                on_order_rejected_called: false,
1807                on_order_accepted_called: false,
1808                on_order_canceled_called: false,
1809                on_order_filled_called: false,
1810                on_order_expired_called: false,
1811                on_position_opened_called: false,
1812                on_position_changed_called: false,
1813                on_position_closed_called: false,
1814            }
1815        }
1816    }
1817
1818    impl DataActor for TestStrategy {
1819        fn on_order_canceled(&mut self, _event: &OrderCanceled) -> anyhow::Result<()> {
1820            self.on_order_canceled_called = true;
1821            Ok(())
1822        }
1823
1824        fn on_order_filled(&mut self, _event: &OrderFilled) -> anyhow::Result<()> {
1825            self.on_order_filled_called = true;
1826            Ok(())
1827        }
1828    }
1829
1830    nautilus_strategy!(TestStrategy, {
1831        fn on_order_rejected(&mut self, _event: OrderRejected) {
1832            self.on_order_rejected_called = true;
1833        }
1834
1835        fn on_order_accepted(&mut self, _event: OrderAccepted) {
1836            self.on_order_accepted_called = true;
1837        }
1838
1839        fn on_order_expired(&mut self, _event: OrderExpired) {
1840            self.on_order_expired_called = true;
1841        }
1842
1843        fn on_position_opened(&mut self, _event: PositionOpened) {
1844            self.on_position_opened_called = true;
1845        }
1846
1847        fn on_position_changed(&mut self, _event: PositionChanged) {
1848            self.on_position_changed_called = true;
1849        }
1850
1851        fn on_position_closed(&mut self, _event: PositionClosed) {
1852            self.on_position_closed_called = true;
1853        }
1854    });
1855
1856    fn create_test_strategy() -> TestStrategy {
1857        let config = StrategyConfig {
1858            strategy_id: Some(StrategyId::from("TEST-001")),
1859            order_id_tag: Some("001".to_string()),
1860            ..Default::default()
1861        };
1862        TestStrategy::new(config)
1863    }
1864
1865    fn register_strategy(strategy: &mut TestStrategy) {
1866        let trader_id = TraderId::from("TRADER-001");
1867        let clock = Rc::new(RefCell::new(TestClock::new()));
1868        let cache = Rc::new(RefCell::new(Cache::default()));
1869        let portfolio = Rc::new(RefCell::new(Portfolio::new(
1870            cache.clone(),
1871            clock.clone(),
1872            None,
1873        )));
1874
1875        strategy
1876            .core
1877            .register(trader_id, clock, cache, portfolio)
1878            .unwrap();
1879        strategy.initialize().unwrap();
1880    }
1881
1882    fn start_strategy(strategy: &mut TestStrategy) {
1883        strategy.start().unwrap();
1884    }
1885
1886    fn stop_strategy(strategy: &mut TestStrategy) {
1887        Component::stop(strategy).unwrap();
1888    }
1889
1890    fn make_filled(client_order_id: ClientOrderId) -> OrderEventAny {
1891        OrderEventAny::Filled(OrderFilled {
1892            trader_id: TraderId::from("TRADER-001"),
1893            strategy_id: StrategyId::from("TEST-001"),
1894            instrument_id: InstrumentId::from("BTCUSDT.BINANCE"),
1895            client_order_id,
1896            venue_order_id: VenueOrderId::test_default(),
1897            account_id: AccountId::from("ACC-001"),
1898            trade_id: TradeId::test_default(),
1899            position_id: None,
1900            order_side: OrderSide::Buy,
1901            order_type: OrderType::Market,
1902            last_qty: Quantity::default(),
1903            last_px: Price::default(),
1904            currency: Currency::from("USD"),
1905            liquidity_side: LiquiditySide::Taker,
1906            event_id: UUID4::default(),
1907            ts_event: UnixNanos::default(),
1908            ts_init: UnixNanos::default(),
1909            reconciliation: false,
1910            commission: None,
1911        })
1912    }
1913
1914    fn make_canceled(client_order_id: ClientOrderId) -> OrderEventAny {
1915        OrderEventAny::Canceled(OrderCanceled {
1916            trader_id: TraderId::from("TRADER-001"),
1917            strategy_id: StrategyId::from("TEST-001"),
1918            instrument_id: InstrumentId::from("BTCUSDT.BINANCE"),
1919            client_order_id,
1920            venue_order_id: None,
1921            account_id: Some(AccountId::from("ACC-001")),
1922            event_id: UUID4::default(),
1923            ts_event: UnixNanos::default(),
1924            ts_init: UnixNanos::default(),
1925            reconciliation: 0,
1926        })
1927    }
1928
1929    fn make_rejected(client_order_id: ClientOrderId) -> OrderEventAny {
1930        OrderEventAny::Rejected(OrderRejected {
1931            trader_id: TraderId::from("TRADER-001"),
1932            strategy_id: StrategyId::from("TEST-001"),
1933            instrument_id: InstrumentId::from("BTCUSDT.BINANCE"),
1934            client_order_id,
1935            account_id: AccountId::from("ACC-001"),
1936            reason: "Test rejection".into(),
1937            event_id: UUID4::default(),
1938            ts_event: UnixNanos::default(),
1939            ts_init: UnixNanos::default(),
1940            reconciliation: 0,
1941            due_post_only: 0,
1942        })
1943    }
1944
1945    fn make_expired(client_order_id: ClientOrderId) -> OrderEventAny {
1946        OrderEventAny::Expired(OrderExpired {
1947            trader_id: TraderId::from("TRADER-001"),
1948            strategy_id: StrategyId::from("TEST-001"),
1949            instrument_id: InstrumentId::from("BTCUSDT.BINANCE"),
1950            client_order_id,
1951            venue_order_id: None,
1952            account_id: Some(AccountId::from("ACC-001")),
1953            event_id: UUID4::default(),
1954            ts_event: UnixNanos::default(),
1955            ts_init: UnixNanos::default(),
1956            reconciliation: 0,
1957        })
1958    }
1959
1960    fn make_accepted(client_order_id: ClientOrderId) -> OrderEventAny {
1961        OrderEventAny::Accepted(OrderAccepted {
1962            trader_id: TraderId::from("TRADER-001"),
1963            strategy_id: StrategyId::from("TEST-001"),
1964            instrument_id: InstrumentId::from("BTCUSDT.BINANCE"),
1965            client_order_id,
1966            venue_order_id: VenueOrderId::test_default(),
1967            account_id: AccountId::from("ACC-001"),
1968            event_id: UUID4::default(),
1969            ts_event: UnixNanos::default(),
1970            ts_init: UnixNanos::default(),
1971            reconciliation: 0,
1972        })
1973    }
1974
1975    fn make_position_opened() -> PositionEvent {
1976        PositionEvent::PositionOpened(PositionOpened {
1977            trader_id: TraderId::from("TRADER-001"),
1978            strategy_id: StrategyId::from("TEST-001"),
1979            instrument_id: InstrumentId::from("BTCUSDT.BINANCE"),
1980            position_id: PositionId::test_default(),
1981            account_id: AccountId::from("ACC-001"),
1982            opening_order_id: ClientOrderId::from("O-001"),
1983            entry: OrderSide::Buy,
1984            side: PositionSide::Long,
1985            signed_qty: 1.0,
1986            quantity: Quantity::default(),
1987            last_qty: Quantity::default(),
1988            last_px: Price::default(),
1989            currency: Currency::from("USD"),
1990            avg_px_open: 0.0,
1991            event_id: UUID4::default(),
1992            ts_event: UnixNanos::default(),
1993            ts_init: UnixNanos::default(),
1994        })
1995    }
1996
1997    fn make_position_changed() -> PositionEvent {
1998        let currency = Currency::from("USD");
1999        PositionEvent::PositionChanged(PositionChanged {
2000            trader_id: TraderId::from("TRADER-001"),
2001            strategy_id: StrategyId::from("TEST-001"),
2002            instrument_id: InstrumentId::from("BTCUSDT.BINANCE"),
2003            position_id: PositionId::test_default(),
2004            account_id: AccountId::from("ACC-001"),
2005            opening_order_id: ClientOrderId::from("O-001"),
2006            entry: OrderSide::Buy,
2007            side: PositionSide::Long,
2008            signed_qty: 2.0,
2009            quantity: Quantity::default(),
2010            peak_quantity: Quantity::default(),
2011            last_qty: Quantity::default(),
2012            last_px: Price::default(),
2013            currency,
2014            avg_px_open: 0.0,
2015            avg_px_close: None,
2016            realized_return: 0.0,
2017            realized_pnl: None,
2018            unrealized_pnl: Money::new(0.0, currency),
2019            event_id: UUID4::default(),
2020            ts_opened: UnixNanos::default(),
2021            ts_event: UnixNanos::default(),
2022            ts_init: UnixNanos::default(),
2023        })
2024    }
2025
2026    fn make_position_closed() -> PositionEvent {
2027        let currency = Currency::from("USD");
2028        PositionEvent::PositionClosed(PositionClosed {
2029            trader_id: TraderId::from("TRADER-001"),
2030            strategy_id: StrategyId::from("TEST-001"),
2031            instrument_id: InstrumentId::from("BTCUSDT.BINANCE"),
2032            position_id: PositionId::test_default(),
2033            account_id: AccountId::from("ACC-001"),
2034            opening_order_id: ClientOrderId::from("O-001"),
2035            closing_order_id: Some(ClientOrderId::from("O-002")),
2036            entry: OrderSide::Buy,
2037            side: PositionSide::Flat,
2038            signed_qty: 0.0,
2039            quantity: Quantity::default(),
2040            peak_quantity: Quantity::default(),
2041            last_qty: Quantity::default(),
2042            last_px: Price::default(),
2043            currency,
2044            avg_px_open: 0.0,
2045            avg_px_close: None,
2046            realized_return: 0.0,
2047            realized_pnl: None,
2048            unrealized_pnl: Money::new(0.0, currency),
2049            duration: 0,
2050            event_id: UUID4::default(),
2051            ts_opened: UnixNanos::default(),
2052            ts_closed: None,
2053            ts_event: UnixNanos::default(),
2054            ts_init: UnixNanos::default(),
2055        })
2056    }
2057
2058    #[rstest]
2059    fn test_strategy_creation() {
2060        let strategy = create_test_strategy();
2061        assert_eq!(
2062            strategy.core.config.strategy_id,
2063            Some(StrategyId::from("TEST-001"))
2064        );
2065        assert!(!strategy.on_order_rejected_called);
2066        assert!(!strategy.on_position_opened_called);
2067    }
2068
2069    #[rstest]
2070    fn test_strategy_registration() {
2071        let mut strategy = create_test_strategy();
2072        register_strategy(&mut strategy);
2073
2074        assert!(strategy.core.order_manager.is_some());
2075        assert!(strategy.core.order_factory.is_some());
2076        assert!(strategy.core.portfolio.is_some());
2077    }
2078
2079    #[rstest]
2080    fn test_handle_order_event_dispatches_to_handler() {
2081        let mut strategy = create_test_strategy();
2082        register_strategy(&mut strategy);
2083        start_strategy(&mut strategy);
2084
2085        let event = OrderEventAny::Rejected(OrderRejected {
2086            trader_id: TraderId::from("TRADER-001"),
2087            strategy_id: StrategyId::from("TEST-001"),
2088            instrument_id: InstrumentId::from("BTCUSDT.BINANCE"),
2089            client_order_id: ClientOrderId::from("O-001"),
2090            account_id: AccountId::from("ACC-001"),
2091            reason: "Test rejection".into(),
2092            event_id: UUID4::default(),
2093            ts_event: UnixNanos::default(),
2094            ts_init: UnixNanos::default(),
2095            reconciliation: 0,
2096            due_post_only: 0,
2097        });
2098
2099        strategy.handle_order_event(event);
2100
2101        assert!(strategy.on_order_rejected_called);
2102    }
2103
2104    #[rstest]
2105    #[case::opened(make_position_opened())]
2106    #[case::changed(make_position_changed())]
2107    #[case::closed(make_position_closed())]
2108    fn test_handle_position_event_dispatches_to_handler(#[case] event: PositionEvent) {
2109        let mut strategy = create_test_strategy();
2110        register_strategy(&mut strategy);
2111        start_strategy(&mut strategy);
2112
2113        let expected_opened = matches!(event, PositionEvent::PositionOpened(_));
2114        let expected_changed = matches!(event, PositionEvent::PositionChanged(_));
2115        let expected_closed = matches!(event, PositionEvent::PositionClosed(_));
2116
2117        strategy.handle_position_event(event);
2118
2119        assert_eq!(strategy.on_position_opened_called, expected_opened);
2120        assert_eq!(strategy.on_position_changed_called, expected_changed);
2121        assert_eq!(strategy.on_position_closed_called, expected_closed);
2122    }
2123
2124    #[rstest]
2125    fn test_handle_position_event_skips_dispatch_when_stopped() {
2126        let mut strategy = create_test_strategy();
2127        register_strategy(&mut strategy);
2128        start_strategy(&mut strategy);
2129        stop_strategy(&mut strategy);
2130        assert_eq!(strategy.core.actor.state(), ComponentState::Stopped);
2131
2132        strategy.handle_position_event(make_position_opened());
2133
2134        assert!(!strategy.on_position_opened_called);
2135    }
2136
2137    #[rstest]
2138    fn test_strategy_default_handlers_do_not_panic() {
2139        let mut strategy = create_test_strategy();
2140
2141        strategy.on_order_initialized(OrderInitialized::default());
2142        strategy.on_order_denied(OrderDenied::default());
2143        strategy.on_order_emulated(OrderEmulated::default());
2144        strategy.on_order_released(OrderReleased::default());
2145        strategy.on_order_submitted(OrderSubmitted::default());
2146        strategy.on_order_rejected(OrderRejected::default());
2147        let _ = DataActor::on_order_canceled(&mut strategy, &OrderCanceled::default());
2148        strategy.on_order_expired(OrderExpired::default());
2149        strategy.on_order_triggered(OrderTriggered::default());
2150        strategy.on_order_pending_update(OrderPendingUpdate::default());
2151        strategy.on_order_pending_cancel(OrderPendingCancel::default());
2152        strategy.on_order_modify_rejected(OrderModifyRejected::default());
2153        strategy.on_order_cancel_rejected(OrderCancelRejected::default());
2154        strategy.on_order_updated(OrderUpdated::default());
2155    }
2156
2157    #[rstest]
2158    fn test_modify_order_routes_non_emulated_orders_to_risk() {
2159        let mut strategy = create_test_strategy();
2160        register_strategy(&mut strategy);
2161
2162        let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
2163            get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
2164        msgbus::register_trading_command_endpoint(
2165            MessagingSwitchboard::risk_engine_queue_execute(),
2166            risk_handler,
2167        );
2168
2169        let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
2170            get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
2171        msgbus::register_trading_command_endpoint(
2172            MessagingSwitchboard::exec_engine_queue_execute(),
2173            exec_handler,
2174        );
2175
2176        let order = OrderAny::Market(MarketOrder::new(
2177            TraderId::from("TRADER-001"),
2178            StrategyId::from("TEST-001"),
2179            InstrumentId::from("BTCUSDT.BINANCE"),
2180            ClientOrderId::from("O-20250208-0003"),
2181            OrderSide::Buy,
2182            Quantity::from(100_000),
2183            TimeInForce::Gtc,
2184            UUID4::new(),
2185            UnixNanos::default(),
2186            false,
2187            false,
2188            None,
2189            None,
2190            None,
2191            None,
2192            None,
2193            None,
2194            None,
2195            None,
2196        ));
2197
2198        strategy
2199            .modify_order(order, Some(Quantity::from(200_000)), None, None, None)
2200            .unwrap();
2201
2202        let risk_messages = risk_messages.get_messages();
2203        let exec_messages = exec_messages.get_messages();
2204
2205        assert_eq!(risk_messages.len(), 1);
2206        assert!(matches!(
2207            risk_messages.first(),
2208            Some(TradingCommand::ModifyOrder(_))
2209        ));
2210        assert!(exec_messages.is_empty());
2211    }
2212
2213    // -- GTD EXPIRY TESTS ----------------------------------------------------------------------------
2214
2215    #[rstest]
2216    fn test_has_gtd_expiry_timer_when_timer_not_set() {
2217        let mut strategy = create_test_strategy();
2218        let client_order_id = ClientOrderId::from("O-001");
2219
2220        assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
2221    }
2222
2223    #[rstest]
2224    fn test_has_gtd_expiry_timer_when_timer_set() {
2225        let mut strategy = create_test_strategy();
2226        let client_order_id = ClientOrderId::from("O-001");
2227
2228        strategy
2229            .core
2230            .gtd_timers
2231            .insert(client_order_id, Ustr::from("GTD-EXPIRY:O-001"));
2232
2233        assert!(strategy.has_gtd_expiry_timer(&client_order_id));
2234    }
2235
2236    #[rstest]
2237    fn test_cancel_gtd_expiry_removes_timer() {
2238        let mut strategy = create_test_strategy();
2239        register_strategy(&mut strategy);
2240
2241        let client_order_id = ClientOrderId::from("O-001");
2242        strategy
2243            .core
2244            .gtd_timers
2245            .insert(client_order_id, Ustr::from("GTD-EXPIRY:O-001"));
2246
2247        strategy.cancel_gtd_expiry(&client_order_id);
2248
2249        assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
2250    }
2251
2252    #[rstest]
2253    fn test_cancel_gtd_expiry_when_timer_not_set() {
2254        let mut strategy = create_test_strategy();
2255        register_strategy(&mut strategy);
2256
2257        let client_order_id = ClientOrderId::from("O-001");
2258
2259        strategy.cancel_gtd_expiry(&client_order_id);
2260
2261        assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
2262    }
2263
2264    #[rstest]
2265    #[case::filled(make_filled)]
2266    #[case::canceled(make_canceled)]
2267    #[case::rejected(make_rejected)]
2268    #[case::expired(make_expired)]
2269    fn test_handle_order_event_cancels_gtd_timer_for_terminal_event(
2270        #[case] make_event: fn(ClientOrderId) -> OrderEventAny,
2271    ) {
2272        let mut strategy = create_test_strategy();
2273        register_strategy(&mut strategy);
2274        start_strategy(&mut strategy);
2275
2276        let client_order_id = ClientOrderId::from("O-001");
2277        strategy
2278            .core
2279            .gtd_timers
2280            .insert(client_order_id, Ustr::from("GTD-EXPIRY:O-001"));
2281
2282        strategy.handle_order_event(make_event(client_order_id));
2283
2284        assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
2285    }
2286
2287    #[rstest]
2288    #[case::filled(make_filled)]
2289    #[case::canceled(make_canceled)]
2290    #[case::rejected(make_rejected)]
2291    #[case::expired(make_expired)]
2292    fn test_handle_order_event_cancels_gtd_timer_when_stopped(
2293        #[case] make_event: fn(ClientOrderId) -> OrderEventAny,
2294    ) {
2295        let mut strategy = create_test_strategy();
2296        register_strategy(&mut strategy);
2297        start_strategy(&mut strategy);
2298
2299        let client_order_id = ClientOrderId::from("O-001");
2300        strategy
2301            .core
2302            .gtd_timers
2303            .insert(client_order_id, Ustr::from("GTD-EXPIRY:O-001"));
2304
2305        stop_strategy(&mut strategy);
2306        assert_eq!(strategy.core.actor.state(), ComponentState::Stopped);
2307
2308        strategy.handle_order_event(make_event(client_order_id));
2309
2310        assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
2311    }
2312
2313    #[rstest]
2314    fn test_handle_order_event_skips_gtd_cancel_for_non_terminal() {
2315        let mut strategy = create_test_strategy();
2316        register_strategy(&mut strategy);
2317        start_strategy(&mut strategy);
2318
2319        let client_order_id = ClientOrderId::from("O-001");
2320        strategy
2321            .core
2322            .gtd_timers
2323            .insert(client_order_id, Ustr::from("GTD-EXPIRY:O-001"));
2324
2325        strategy.handle_order_event(make_accepted(client_order_id));
2326
2327        assert!(strategy.has_gtd_expiry_timer(&client_order_id));
2328    }
2329
2330    #[rstest]
2331    fn test_handle_order_event_skips_dispatch_when_stopped() {
2332        let mut strategy = create_test_strategy();
2333        register_strategy(&mut strategy);
2334        start_strategy(&mut strategy);
2335        stop_strategy(&mut strategy);
2336        assert_eq!(strategy.core.actor.state(), ComponentState::Stopped);
2337
2338        strategy.handle_order_event(make_rejected(ClientOrderId::from("O-001")));
2339
2340        assert!(!strategy.on_order_rejected_called);
2341    }
2342
2343    #[rstest]
2344    fn test_on_start_calls_reactivate_gtd_timers_when_enabled() {
2345        let config = StrategyConfig {
2346            strategy_id: Some(StrategyId::from("TEST-001")),
2347            order_id_tag: Some("001".to_string()),
2348            manage_gtd_expiry: true,
2349            ..Default::default()
2350        };
2351        let mut strategy = TestStrategy::new(config);
2352        register_strategy(&mut strategy);
2353
2354        let result = Strategy::on_start(&mut strategy);
2355        assert!(result.is_ok());
2356    }
2357
2358    #[rstest]
2359    fn test_on_start_does_not_panic_when_gtd_disabled() {
2360        let config = StrategyConfig {
2361            strategy_id: Some(StrategyId::from("TEST-001")),
2362            order_id_tag: Some("001".to_string()),
2363            manage_gtd_expiry: false,
2364            ..Default::default()
2365        };
2366        let mut strategy = TestStrategy::new(config);
2367        register_strategy(&mut strategy);
2368
2369        let result = Strategy::on_start(&mut strategy);
2370        assert!(result.is_ok());
2371    }
2372
2373    // -- QUERY TESTS ---------------------------------------------------------------------------------
2374
2375    #[rstest]
2376    fn test_query_account_when_registered() {
2377        let mut strategy = create_test_strategy();
2378        register_strategy(&mut strategy);
2379
2380        let account_id = AccountId::from("ACC-001");
2381
2382        let result = strategy.query_account(account_id, None, None);
2383
2384        assert!(result.is_ok());
2385    }
2386
2387    #[rstest]
2388    fn test_query_account_with_client_id() {
2389        let mut strategy = create_test_strategy();
2390        register_strategy(&mut strategy);
2391
2392        let account_id = AccountId::from("ACC-001");
2393        let client_id = ClientId::from("BINANCE");
2394
2395        let result = strategy.query_account(account_id, Some(client_id), None);
2396
2397        assert!(result.is_ok());
2398    }
2399
2400    #[rstest]
2401    fn test_query_order_when_registered() {
2402        let mut strategy = create_test_strategy();
2403        register_strategy(&mut strategy);
2404
2405        let order = OrderAny::Market(MarketOrder::test_default());
2406
2407        let result = strategy.query_order(&order, None, None);
2408
2409        assert!(result.is_ok());
2410    }
2411
2412    #[rstest]
2413    fn test_query_order_with_client_id() {
2414        let mut strategy = create_test_strategy();
2415        register_strategy(&mut strategy);
2416
2417        let order = OrderAny::Market(MarketOrder::test_default());
2418        let client_id = ClientId::from("BINANCE");
2419
2420        let result = strategy.query_order(&order, Some(client_id), None);
2421
2422        assert!(result.is_ok());
2423    }
2424
2425    #[rstest]
2426    fn test_is_exiting_returns_false_by_default() {
2427        let strategy = create_test_strategy();
2428        assert!(!strategy.is_exiting());
2429    }
2430
2431    #[rstest]
2432    fn test_is_exiting_returns_true_when_set_manually() {
2433        let mut strategy = create_test_strategy();
2434        register_strategy(&mut strategy);
2435
2436        // Manually set the exiting state (as market_exit would do)
2437        strategy.core.is_exiting = true;
2438
2439        assert!(strategy.is_exiting());
2440    }
2441
2442    #[rstest]
2443    fn test_market_exit_sets_is_exiting_flag() {
2444        // Test the state changes that market_exit would make
2445        let mut strategy = create_test_strategy();
2446        register_strategy(&mut strategy);
2447
2448        assert!(!strategy.core.is_exiting);
2449
2450        // Simulate what market_exit does to the state
2451        strategy.core.is_exiting = true;
2452        strategy.core.market_exit_attempts = 0;
2453
2454        assert!(strategy.core.is_exiting);
2455        assert_eq!(strategy.core.market_exit_attempts, 0);
2456    }
2457
2458    #[rstest]
2459    fn test_market_exit_uses_config_time_in_force_and_reduce_only() {
2460        let config = StrategyConfig {
2461            strategy_id: Some(StrategyId::from("TEST-001")),
2462            order_id_tag: Some("001".to_string()),
2463            market_exit_time_in_force: TimeInForce::Ioc,
2464            market_exit_reduce_only: false,
2465            ..Default::default()
2466        };
2467        let strategy = TestStrategy::new(config);
2468
2469        assert_eq!(
2470            strategy.core.config.market_exit_time_in_force,
2471            TimeInForce::Ioc
2472        );
2473        assert!(!strategy.core.config.market_exit_reduce_only);
2474    }
2475
2476    #[rstest]
2477    fn test_market_exit_resets_attempt_counter() {
2478        let mut strategy = create_test_strategy();
2479        register_strategy(&mut strategy);
2480
2481        // Manually set attempts to simulate prior exit
2482        strategy.core.market_exit_attempts = 50;
2483
2484        // Reset via the reset method
2485        strategy.core.reset_market_exit_state();
2486
2487        assert_eq!(strategy.core.market_exit_attempts, 0);
2488    }
2489
2490    #[rstest]
2491    fn test_market_exit_second_call_returns_early_when_exiting() {
2492        let mut strategy = create_test_strategy();
2493        register_strategy(&mut strategy);
2494
2495        // First set exiting to true to simulate an in-progress exit
2496        strategy.core.is_exiting = true;
2497
2498        // Second call should return Ok and not change state
2499        let result = strategy.market_exit();
2500        assert!(result.is_ok());
2501        assert!(strategy.core.is_exiting);
2502    }
2503
2504    #[rstest]
2505    fn test_finalize_market_exit_resets_state() {
2506        let mut strategy = create_test_strategy();
2507        register_strategy(&mut strategy);
2508
2509        // Set up exiting state
2510        strategy.core.is_exiting = true;
2511        strategy.core.pending_stop = true;
2512        strategy.core.market_exit_attempts = 50;
2513
2514        strategy.finalize_market_exit();
2515
2516        assert!(!strategy.core.is_exiting);
2517        assert!(!strategy.core.pending_stop);
2518        assert_eq!(strategy.core.market_exit_attempts, 0);
2519    }
2520
2521    #[rstest]
2522    fn test_market_exit_config_defaults() {
2523        let config = StrategyConfig::default();
2524
2525        assert!(!config.manage_stop);
2526        assert_eq!(config.market_exit_interval_ms, 100);
2527        assert_eq!(config.market_exit_max_attempts, 100);
2528    }
2529
2530    #[rstest]
2531    fn test_market_exit_with_custom_config() {
2532        let config = StrategyConfig {
2533            strategy_id: Some(StrategyId::from("TEST-001")),
2534            manage_stop: true,
2535            market_exit_interval_ms: 50,
2536            market_exit_max_attempts: 200,
2537            ..Default::default()
2538        };
2539        let strategy = TestStrategy::new(config);
2540
2541        assert!(strategy.core.config.manage_stop);
2542        assert_eq!(strategy.core.config.market_exit_interval_ms, 50);
2543        assert_eq!(strategy.core.config.market_exit_max_attempts, 200);
2544    }
2545
2546    #[derive(Debug)]
2547    struct MarketExitHookTrackingStrategy {
2548        core: StrategyCore,
2549        on_market_exit_called: bool,
2550        post_market_exit_called: bool,
2551    }
2552
2553    impl MarketExitHookTrackingStrategy {
2554        fn new(config: StrategyConfig) -> Self {
2555            Self {
2556                core: StrategyCore::new(config),
2557                on_market_exit_called: false,
2558                post_market_exit_called: false,
2559            }
2560        }
2561    }
2562
2563    impl DataActor for MarketExitHookTrackingStrategy {}
2564
2565    nautilus_strategy!(MarketExitHookTrackingStrategy, {
2566        fn on_market_exit(&mut self) {
2567            self.on_market_exit_called = true;
2568        }
2569
2570        fn post_market_exit(&mut self) {
2571            self.post_market_exit_called = true;
2572        }
2573    });
2574
2575    #[rstest]
2576    fn test_market_exit_calls_on_market_exit_hook() {
2577        let config = StrategyConfig {
2578            strategy_id: Some(StrategyId::from("TEST-001")),
2579            order_id_tag: Some("001".to_string()),
2580            ..Default::default()
2581        };
2582        let mut strategy = MarketExitHookTrackingStrategy::new(config);
2583
2584        let trader_id = TraderId::from("TRADER-001");
2585        let clock = Rc::new(RefCell::new(TestClock::new()));
2586        let cache = Rc::new(RefCell::new(Cache::default()));
2587        let portfolio = Rc::new(RefCell::new(Portfolio::new(
2588            cache.clone(),
2589            clock.clone(),
2590            None,
2591        )));
2592        strategy
2593            .core
2594            .register(trader_id, clock, cache, portfolio)
2595            .unwrap();
2596        strategy.initialize().unwrap();
2597        strategy.start().unwrap();
2598
2599        let _ = strategy.market_exit();
2600
2601        assert!(strategy.on_market_exit_called);
2602    }
2603
2604    #[rstest]
2605    fn test_finalize_market_exit_calls_post_market_exit_hook() {
2606        let config = StrategyConfig {
2607            strategy_id: Some(StrategyId::from("TEST-001")),
2608            order_id_tag: Some("001".to_string()),
2609            ..Default::default()
2610        };
2611        let mut strategy = MarketExitHookTrackingStrategy::new(config);
2612
2613        let trader_id = TraderId::from("TRADER-001");
2614        let clock = Rc::new(RefCell::new(TestClock::new()));
2615        let cache = Rc::new(RefCell::new(Cache::default()));
2616        let portfolio = Rc::new(RefCell::new(Portfolio::new(
2617            cache.clone(),
2618            clock.clone(),
2619            None,
2620        )));
2621        strategy
2622            .core
2623            .register(trader_id, clock, cache, portfolio)
2624            .unwrap();
2625
2626        strategy.core.is_exiting = true;
2627        strategy.finalize_market_exit();
2628
2629        assert!(strategy.post_market_exit_called);
2630    }
2631
2632    #[derive(Debug)]
2633    struct FailingPostExitStrategy {
2634        core: StrategyCore,
2635    }
2636
2637    impl FailingPostExitStrategy {
2638        fn new(config: StrategyConfig) -> Self {
2639            Self {
2640                core: StrategyCore::new(config),
2641            }
2642        }
2643    }
2644
2645    impl DataActor for FailingPostExitStrategy {}
2646
2647    nautilus_strategy!(FailingPostExitStrategy, {
2648        fn post_market_exit(&mut self) {
2649            panic!("Simulated error in post_market_exit");
2650        }
2651    });
2652
2653    #[rstest]
2654    fn test_finalize_market_exit_handles_hook_panic() {
2655        let config = StrategyConfig {
2656            strategy_id: Some(StrategyId::from("TEST-001")),
2657            order_id_tag: Some("001".to_string()),
2658            ..Default::default()
2659        };
2660        let mut strategy = FailingPostExitStrategy::new(config);
2661
2662        let trader_id = TraderId::from("TRADER-001");
2663        let clock = Rc::new(RefCell::new(TestClock::new()));
2664        let cache = Rc::new(RefCell::new(Cache::default()));
2665        let portfolio = Rc::new(RefCell::new(Portfolio::new(
2666            cache.clone(),
2667            clock.clone(),
2668            None,
2669        )));
2670        strategy
2671            .core
2672            .register(trader_id, clock, cache, portfolio)
2673            .unwrap();
2674
2675        strategy.core.is_exiting = true;
2676        strategy.core.pending_stop = true;
2677
2678        // This should not panic - it should catch the panic in post_market_exit
2679        strategy.finalize_market_exit();
2680
2681        // State should still be reset
2682        assert!(!strategy.core.is_exiting);
2683        assert!(!strategy.core.pending_stop);
2684    }
2685
2686    #[rstest]
2687    fn test_check_market_exit_increments_attempts_before_finalizing() {
2688        let mut strategy = create_test_strategy();
2689        register_strategy(&mut strategy);
2690
2691        strategy.core.is_exiting = true;
2692        assert_eq!(strategy.core.market_exit_attempts, 0);
2693
2694        let event = TimeEvent::new(
2695            Ustr::from("MARKET_EXIT_CHECK:TEST-001"),
2696            UUID4::new(),
2697            UnixNanos::default(),
2698            UnixNanos::default(),
2699        );
2700        strategy.check_market_exit(event);
2701
2702        // With no orders/positions, check_market_exit will finalize immediately
2703        // which resets attempts to 0. This is correct behavior.
2704        // The attempt WAS incremented to 1 during the check, then reset on finalize.
2705        assert!(!strategy.core.is_exiting);
2706        assert_eq!(strategy.core.market_exit_attempts, 0);
2707    }
2708
2709    #[rstest]
2710    fn test_check_market_exit_finalizes_when_max_attempts_reached() {
2711        let config = StrategyConfig {
2712            strategy_id: Some(StrategyId::from("TEST-001")),
2713            order_id_tag: Some("001".to_string()),
2714            market_exit_max_attempts: 3,
2715            ..Default::default()
2716        };
2717        let mut strategy = TestStrategy::new(config);
2718        register_strategy(&mut strategy);
2719
2720        strategy.core.is_exiting = true;
2721        strategy.core.market_exit_attempts = 2; // One below max
2722
2723        let event = TimeEvent::new(
2724            Ustr::from("MARKET_EXIT_CHECK:TEST-001"),
2725            UUID4::new(),
2726            UnixNanos::default(),
2727            UnixNanos::default(),
2728        );
2729        strategy.check_market_exit(event);
2730
2731        // Should have finalized since attempts >= max_attempts
2732        assert!(!strategy.core.is_exiting);
2733        assert_eq!(strategy.core.market_exit_attempts, 0);
2734    }
2735
2736    #[rstest]
2737    fn test_check_market_exit_finalizes_when_no_orders_or_positions() {
2738        let mut strategy = create_test_strategy();
2739        register_strategy(&mut strategy);
2740
2741        strategy.core.is_exiting = true;
2742
2743        let event = TimeEvent::new(
2744            Ustr::from("MARKET_EXIT_CHECK:TEST-001"),
2745            UUID4::new(),
2746            UnixNanos::default(),
2747            UnixNanos::default(),
2748        );
2749        strategy.check_market_exit(event);
2750
2751        // Should have finalized since there are no orders or positions
2752        assert!(!strategy.core.is_exiting);
2753    }
2754
2755    #[rstest]
2756    fn test_market_exit_timer_name_format() {
2757        let config = StrategyConfig {
2758            strategy_id: Some(StrategyId::from("MY-STRATEGY-001")),
2759            ..Default::default()
2760        };
2761        let strategy = TestStrategy::new(config);
2762
2763        assert_eq!(
2764            strategy.core.market_exit_timer_name.as_str(),
2765            "MARKET_EXIT_CHECK:MY-STRATEGY-001"
2766        );
2767    }
2768
2769    #[rstest]
2770    fn test_reset_market_exit_state() {
2771        let mut strategy = create_test_strategy();
2772
2773        strategy.core.is_exiting = true;
2774        strategy.core.pending_stop = true;
2775        strategy.core.market_exit_attempts = 50;
2776
2777        strategy.core.reset_market_exit_state();
2778
2779        assert!(!strategy.core.is_exiting);
2780        assert!(!strategy.core.pending_stop);
2781        assert_eq!(strategy.core.market_exit_attempts, 0);
2782    }
2783
2784    #[rstest]
2785    fn test_cancel_market_exit_resets_state_without_hooks() {
2786        let config = StrategyConfig {
2787            strategy_id: Some(StrategyId::from("TEST-001")),
2788            order_id_tag: Some("001".to_string()),
2789            ..Default::default()
2790        };
2791        let mut strategy = MarketExitHookTrackingStrategy::new(config);
2792
2793        let trader_id = TraderId::from("TRADER-001");
2794        let clock = Rc::new(RefCell::new(TestClock::new()));
2795        let cache = Rc::new(RefCell::new(Cache::default()));
2796        let portfolio = Rc::new(RefCell::new(Portfolio::new(
2797            cache.clone(),
2798            clock.clone(),
2799            None,
2800        )));
2801        strategy
2802            .core
2803            .register(trader_id, clock, cache, portfolio)
2804            .unwrap();
2805
2806        // Set up exiting state
2807        strategy.core.is_exiting = true;
2808        strategy.core.pending_stop = true;
2809        strategy.core.market_exit_attempts = 50;
2810
2811        // Call cancel_market_exit
2812        strategy.cancel_market_exit();
2813
2814        // State should be reset
2815        assert!(!strategy.core.is_exiting);
2816        assert!(!strategy.core.pending_stop);
2817        assert_eq!(strategy.core.market_exit_attempts, 0);
2818
2819        // Hooks should NOT have been called
2820        assert!(!strategy.on_market_exit_called);
2821        assert!(!strategy.post_market_exit_called);
2822    }
2823
2824    #[rstest]
2825    fn test_market_exit_returns_early_when_not_running() {
2826        let mut strategy = create_test_strategy();
2827        register_strategy(&mut strategy);
2828
2829        // State is not Running (default is PreInitialized)
2830        assert_ne!(strategy.core.actor.state(), ComponentState::Running);
2831
2832        let result = strategy.market_exit();
2833
2834        // Should return Ok but not set is_exiting
2835        assert!(result.is_ok());
2836        assert!(!strategy.core.is_exiting);
2837    }
2838
2839    #[rstest]
2840    fn test_stop_with_manage_stop_false_cleans_up_active_exit() {
2841        let config = StrategyConfig {
2842            strategy_id: Some(StrategyId::from("TEST-001")),
2843            order_id_tag: Some("001".to_string()),
2844            manage_stop: false,
2845            ..Default::default()
2846        };
2847        let mut strategy = TestStrategy::new(config);
2848        register_strategy(&mut strategy);
2849
2850        // Simulate an active market exit
2851        strategy.core.is_exiting = true;
2852        strategy.core.market_exit_attempts = 5;
2853
2854        // Call stop
2855        let should_proceed = Strategy::stop(&mut strategy);
2856
2857        // Should clean up state and allow stop to proceed
2858        assert!(should_proceed);
2859        assert!(!strategy.core.is_exiting);
2860        assert_eq!(strategy.core.market_exit_attempts, 0);
2861    }
2862
2863    #[rstest]
2864    fn test_stop_with_manage_stop_true_defers_when_running() {
2865        let config = StrategyConfig {
2866            strategy_id: Some(StrategyId::from("TEST-001")),
2867            order_id_tag: Some("001".to_string()),
2868            manage_stop: true,
2869            ..Default::default()
2870        };
2871        let mut strategy = TestStrategy::new(config);
2872
2873        // Custom setup with a default callback so timer scheduling succeeds
2874        let trader_id = TraderId::from("TRADER-001");
2875        let clock = Rc::new(RefCell::new(TestClock::new()));
2876        clock
2877            .borrow_mut()
2878            .register_default_handler(TimeEventCallback::from(|_event: TimeEvent| {}));
2879        let cache = Rc::new(RefCell::new(Cache::default()));
2880        let portfolio = Rc::new(RefCell::new(Portfolio::new(
2881            cache.clone(),
2882            clock.clone(),
2883            None,
2884        )));
2885        strategy
2886            .core
2887            .register(trader_id, clock, cache, portfolio)
2888            .unwrap();
2889        strategy.initialize().unwrap();
2890        strategy.start().unwrap();
2891
2892        let should_proceed = Strategy::stop(&mut strategy);
2893
2894        // Should set pending_stop and defer
2895        assert!(!should_proceed);
2896        assert!(strategy.core.pending_stop);
2897    }
2898
2899    #[rstest]
2900    fn test_stop_with_manage_stop_true_returns_early_if_pending() {
2901        let config = StrategyConfig {
2902            strategy_id: Some(StrategyId::from("TEST-001")),
2903            order_id_tag: Some("001".to_string()),
2904            manage_stop: true,
2905            ..Default::default()
2906        };
2907        let mut strategy = TestStrategy::new(config);
2908        register_strategy(&mut strategy);
2909        start_strategy(&mut strategy);
2910        strategy.core.pending_stop = true;
2911
2912        // Call stop again
2913        let should_proceed = Strategy::stop(&mut strategy);
2914
2915        // Should return early without changing state
2916        assert!(!should_proceed);
2917        assert!(strategy.core.pending_stop);
2918    }
2919
2920    #[rstest]
2921    fn test_stop_with_manage_stop_true_proceeds_when_not_running() {
2922        let config = StrategyConfig {
2923            strategy_id: Some(StrategyId::from("TEST-001")),
2924            order_id_tag: Some("001".to_string()),
2925            manage_stop: true,
2926            ..Default::default()
2927        };
2928        let mut strategy = TestStrategy::new(config);
2929        register_strategy(&mut strategy);
2930
2931        // State is not Running (default)
2932        assert_ne!(strategy.core.actor.state(), ComponentState::Running);
2933
2934        let should_proceed = Strategy::stop(&mut strategy);
2935
2936        // Should proceed with stop
2937        assert!(should_proceed);
2938    }
2939
2940    #[rstest]
2941    fn test_finalize_market_exit_stops_strategy_when_pending() {
2942        let config = StrategyConfig {
2943            strategy_id: Some(StrategyId::from("TEST-001")),
2944            order_id_tag: Some("001".to_string()),
2945            ..Default::default()
2946        };
2947        let mut strategy = TestStrategy::new(config);
2948        register_strategy(&mut strategy);
2949        start_strategy(&mut strategy);
2950
2951        // Simulate a market exit with pending stop
2952        strategy.core.is_exiting = true;
2953        strategy.core.pending_stop = true;
2954
2955        strategy.finalize_market_exit();
2956
2957        // Should have transitioned to Stopped
2958        assert_eq!(strategy.core.actor.state(), ComponentState::Stopped);
2959        assert!(!strategy.core.is_exiting);
2960        assert!(!strategy.core.pending_stop);
2961    }
2962
2963    #[rstest]
2964    fn test_finalize_market_exit_stays_running_when_not_pending() {
2965        let config = StrategyConfig {
2966            strategy_id: Some(StrategyId::from("TEST-001")),
2967            order_id_tag: Some("001".to_string()),
2968            ..Default::default()
2969        };
2970        let mut strategy = TestStrategy::new(config);
2971        register_strategy(&mut strategy);
2972        start_strategy(&mut strategy);
2973
2974        // Simulate a market exit without pending stop
2975        strategy.core.is_exiting = true;
2976        strategy.core.pending_stop = false;
2977
2978        strategy.finalize_market_exit();
2979
2980        // Should stay Running
2981        assert_eq!(strategy.core.actor.state(), ComponentState::Running);
2982        assert!(!strategy.core.is_exiting);
2983    }
2984
2985    #[rstest]
2986    fn test_submit_order_denied_during_market_exit_when_not_reduce_only() {
2987        let mut strategy = create_test_strategy();
2988        register_strategy(&mut strategy);
2989        start_strategy(&mut strategy);
2990        strategy.core.is_exiting = true;
2991
2992        let order = OrderAny::Market(MarketOrder::new(
2993            TraderId::from("TRADER-001"),
2994            StrategyId::from("TEST-001"),
2995            InstrumentId::from("BTCUSDT.BINANCE"),
2996            ClientOrderId::from("O-20250208-0001"),
2997            OrderSide::Buy,
2998            Quantity::from(100_000),
2999            TimeInForce::Gtc,
3000            UUID4::new(),
3001            UnixNanos::default(),
3002            false, // not reduce_only
3003            false,
3004            None,
3005            None,
3006            None,
3007            None,
3008            None,
3009            None,
3010            None,
3011            None,
3012        ));
3013        let client_order_id = order.client_order_id();
3014        let result = strategy.submit_order(order, None, None);
3015
3016        assert!(result.is_ok());
3017        let cache = strategy.core.cache();
3018        let cached_order = cache.order(&client_order_id).unwrap();
3019        assert_eq!(cached_order.status(), OrderStatus::Denied);
3020    }
3021
3022    #[rstest]
3023    fn test_submit_order_allowed_during_market_exit_when_reduce_only() {
3024        let mut strategy = create_test_strategy();
3025        register_strategy(&mut strategy);
3026        start_strategy(&mut strategy);
3027        strategy.core.is_exiting = true;
3028
3029        let order = OrderAny::Market(MarketOrder::new(
3030            TraderId::from("TRADER-001"),
3031            StrategyId::from("TEST-001"),
3032            InstrumentId::from("BTCUSDT.BINANCE"),
3033            ClientOrderId::from("O-20250208-0001"),
3034            OrderSide::Buy,
3035            Quantity::from(100_000),
3036            TimeInForce::Gtc,
3037            UUID4::new(),
3038            UnixNanos::default(),
3039            true, // reduce_only
3040            false,
3041            None,
3042            None,
3043            None,
3044            None,
3045            None,
3046            None,
3047            None,
3048            None,
3049        ));
3050        let client_order_id = order.client_order_id();
3051        let result = strategy.submit_order(order, None, None);
3052
3053        assert!(result.is_ok());
3054        let cache = strategy.core.cache();
3055        let cached_order = cache.order(&client_order_id).unwrap();
3056        assert_ne!(cached_order.status(), OrderStatus::Denied);
3057    }
3058
3059    #[rstest]
3060    fn test_submit_order_allowed_during_market_exit_when_tagged() {
3061        let mut strategy = create_test_strategy();
3062        register_strategy(&mut strategy);
3063        start_strategy(&mut strategy);
3064        strategy.core.is_exiting = true;
3065
3066        let order = OrderAny::Market(MarketOrder::new(
3067            TraderId::from("TRADER-001"),
3068            StrategyId::from("TEST-001"),
3069            InstrumentId::from("BTCUSDT.BINANCE"),
3070            ClientOrderId::from("O-20250208-0002"),
3071            OrderSide::Buy,
3072            Quantity::from(100_000),
3073            TimeInForce::Gtc,
3074            UUID4::new(),
3075            UnixNanos::default(),
3076            false, // not reduce_only
3077            false,
3078            None,
3079            None,
3080            None,
3081            None,
3082            None,
3083            None,
3084            None,
3085            Some(vec![Ustr::from("MARKET_EXIT")]),
3086        ));
3087        let client_order_id = order.client_order_id();
3088        let result = strategy.submit_order(order, None, None);
3089
3090        assert!(result.is_ok());
3091        let cache = strategy.core.cache();
3092        let cached_order = cache.order(&client_order_id).unwrap();
3093        assert_ne!(cached_order.status(), OrderStatus::Denied);
3094    }
3095
3096    #[derive(Debug)]
3097    struct MacroTestSimple {
3098        core: StrategyCore,
3099    }
3100
3101    nautilus_strategy!(MacroTestSimple);
3102
3103    impl DataActor for MacroTestSimple {}
3104
3105    #[derive(Debug)]
3106    struct MacroTestWithHooks {
3107        core: StrategyCore,
3108    }
3109
3110    nautilus_strategy!(MacroTestWithHooks, {
3111        fn on_order_rejected(&mut self, _event: OrderRejected) {}
3112    });
3113
3114    impl DataActor for MacroTestWithHooks {}
3115
3116    #[derive(Debug)]
3117    struct MacroTestCustomField {
3118        inner: StrategyCore,
3119    }
3120
3121    nautilus_strategy!(MacroTestCustomField, inner, {
3122        fn external_order_claims(&self) -> Option<Vec<InstrumentId>> {
3123            None
3124        }
3125    });
3126
3127    impl DataActor for MacroTestCustomField {}
3128
3129    #[rstest]
3130    fn test_nautilus_strategy_macro_forms() {
3131        let config = StrategyConfig {
3132            strategy_id: Some(StrategyId::from("MACRO-001")),
3133            order_id_tag: Some("001".to_string()),
3134            ..Default::default()
3135        };
3136
3137        let simple = MacroTestSimple {
3138            core: StrategyCore::new(config.clone()),
3139        };
3140        assert_eq!(simple.core().config.strategy_id, config.strategy_id);
3141
3142        let hooks = MacroTestWithHooks {
3143            core: StrategyCore::new(config.clone()),
3144        };
3145        assert_eq!(hooks.core().config.strategy_id, config.strategy_id);
3146
3147        let custom = MacroTestCustomField {
3148            inner: StrategyCore::new(config.clone()),
3149        };
3150        assert_eq!(custom.core().config.strategy_id, config.strategy_id);
3151        assert!(custom.external_order_claims().is_none());
3152    }
3153}