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