Skip to main content

nautilus_trading/strategy/
mod.rs

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