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