Skip to main content

nautilus_trading/strategy/
core.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
16use std::{
17    cell::RefCell,
18    fmt::Debug,
19    ops::{Deref, DerefMut},
20    rc::Rc,
21};
22
23use ahash::AHashMap;
24use nautilus_common::{
25    actor::{DataActorConfig, DataActorCore},
26    cache::Cache,
27    clock::Clock,
28    factories::OrderFactory,
29};
30use nautilus_execution::order_manager::manager::OrderManager;
31use nautilus_model::identifiers::{ActorId, ClientOrderId, StrategyId, TraderId};
32use nautilus_portfolio::portfolio::Portfolio;
33use ustr::Ustr;
34
35use super::config::StrategyConfig;
36
37/// The core component of a [`Strategy`](super::Strategy), managing data, orders, and state.
38///
39/// This struct is intended to be held as a member within a user's custom strategy struct.
40/// The user's struct should then `Deref` and `DerefMut` to this `StrategyCore` instance
41/// to satisfy the trait bounds of [`Strategy`](super::Strategy) and
42/// [`DataActor`](nautilus_common::actor::data_actor::DataActor).
43pub struct StrategyCore {
44    pub(crate) actor: DataActorCore,
45    /// The strategy configuration.
46    pub config: StrategyConfig,
47    pub(crate) order_manager: Option<OrderManager>,
48    pub(crate) order_factory: Option<OrderFactory>,
49    pub(crate) portfolio: Option<Rc<RefCell<Portfolio>>>,
50    pub(crate) gtd_timers: AHashMap<ClientOrderId, Ustr>,
51    pub(crate) is_exiting: bool,
52    pub(crate) pending_stop: bool,
53    pub(crate) market_exit_attempts: u64,
54    pub(crate) market_exit_timer_name: Ustr,
55    pub(crate) market_exit_tag: Ustr,
56}
57
58impl Debug for StrategyCore {
59    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60        f.debug_struct(stringify!(StrategyCore))
61            .field("actor", &self.actor)
62            .field("config", &self.config)
63            .field("order_manager", &self.order_manager)
64            .field("order_factory", &self.order_factory)
65            .field("is_exiting", &self.is_exiting)
66            .field("pending_stop", &self.pending_stop)
67            .field("market_exit_attempts", &self.market_exit_attempts)
68            .finish()
69    }
70}
71
72impl StrategyCore {
73    /// Creates a new [`StrategyCore`] instance.
74    pub fn new(config: StrategyConfig) -> Self {
75        let actor_config = DataActorConfig {
76            actor_id: config
77                .strategy_id
78                .map(|id| ActorId::from(id.inner().as_str())),
79            log_events: config.log_events,
80            log_commands: config.log_commands,
81        };
82
83        let strategy_id = config
84            .strategy_id
85            .map(|id| id.inner().to_string())
86            .unwrap_or_default();
87        let market_exit_timer_name = Ustr::from(&format!("MARKET_EXIT_CHECK:{strategy_id}"));
88
89        Self {
90            actor: DataActorCore::new(actor_config),
91            config,
92            order_manager: None,
93            order_factory: None,
94            portfolio: None,
95            gtd_timers: AHashMap::new(),
96            is_exiting: false,
97            pending_stop: false,
98            market_exit_attempts: 0,
99            market_exit_timer_name,
100            market_exit_tag: Ustr::from("MARKET_EXIT"),
101        }
102    }
103
104    /// Registers the strategy with the trading engine components.
105    ///
106    /// This is typically called by the framework when the strategy is added to an engine.
107    ///
108    /// # Errors
109    ///
110    /// Returns an error if registration with the actor core fails.
111    pub fn register(
112        &mut self,
113        trader_id: TraderId,
114        clock: Rc<RefCell<dyn Clock>>,
115        cache: Rc<RefCell<Cache>>,
116        portfolio: Rc<RefCell<Portfolio>>,
117    ) -> anyhow::Result<()> {
118        self.actor
119            .register(trader_id, clock.clone(), cache.clone())?;
120
121        let strategy_id = StrategyId::from(self.actor.actor_id.inner().as_str());
122
123        // Update market exit timer name with actual strategy ID
124        self.market_exit_timer_name = Ustr::from(&format!("MARKET_EXIT_CHECK:{strategy_id}"));
125
126        self.order_factory = Some(OrderFactory::new(
127            trader_id,
128            strategy_id,
129            None,
130            None,
131            clock.clone(),
132            self.config.use_uuid_client_order_ids,
133            self.config.use_hyphens_in_client_order_ids,
134        ));
135
136        self.order_manager = Some(OrderManager::new(clock, cache, false, None, None, None));
137
138        self.portfolio = Some(portfolio);
139
140        Ok(())
141    }
142
143    /// Returns a mutable reference to the [`OrderFactory`].
144    ///
145    /// # Panics
146    ///
147    /// Panics if the strategy has not been registered.
148    pub fn order_factory(&mut self) -> &mut OrderFactory {
149        self.order_factory
150            .as_mut()
151            .expect("Strategy not registered: OrderFactory not initialized")
152    }
153
154    /// Returns a mutable reference to the [`OrderManager`].
155    ///
156    /// # Panics
157    ///
158    /// Panics if the strategy has not been registered.
159    pub fn order_manager(&mut self) -> &mut OrderManager {
160        self.order_manager
161            .as_mut()
162            .expect("Strategy not registered: OrderManager not initialized")
163    }
164
165    /// Returns a reference to the [`Portfolio`].
166    ///
167    /// # Panics
168    ///
169    /// Panics if the strategy has not been registered.
170    pub fn portfolio(&self) -> &Rc<RefCell<Portfolio>> {
171        self.portfolio
172            .as_ref()
173            .expect("Strategy not registered: Portfolio not initialized")
174    }
175
176    /// Resets the market exit state.
177    pub fn reset_market_exit_state(&mut self) {
178        self.is_exiting = false;
179        self.pending_stop = false;
180        self.market_exit_attempts = 0;
181    }
182}
183
184impl Deref for StrategyCore {
185    type Target = DataActorCore;
186    fn deref(&self) -> &Self::Target {
187        &self.actor
188    }
189}
190
191impl DerefMut for StrategyCore {
192    fn deref_mut(&mut self) -> &mut Self::Target {
193        &mut self.actor
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use std::{cell::RefCell, rc::Rc};
200
201    use nautilus_common::{cache::Cache, clock::TestClock};
202    use nautilus_model::identifiers::{StrategyId, TraderId};
203    use nautilus_portfolio::portfolio::Portfolio;
204    use rstest::rstest;
205
206    use super::*;
207
208    fn create_test_config() -> StrategyConfig {
209        StrategyConfig {
210            strategy_id: Some(StrategyId::from("TEST-001")),
211            order_id_tag: Some("001".to_string()),
212            ..Default::default()
213        }
214    }
215
216    #[rstest]
217    fn test_strategy_core_new() {
218        let config = create_test_config();
219        let core = StrategyCore::new(config.clone());
220
221        assert_eq!(core.config.strategy_id, config.strategy_id);
222        assert_eq!(core.config.order_id_tag, config.order_id_tag);
223        assert!(core.order_manager.is_none());
224        assert!(core.order_factory.is_none());
225        assert!(core.portfolio.is_none());
226        assert!(!core.is_exiting);
227        assert!(!core.pending_stop);
228        assert_eq!(core.market_exit_attempts, 0);
229    }
230
231    #[rstest]
232    fn test_strategy_core_register() {
233        let config = create_test_config();
234        let mut core = StrategyCore::new(config);
235
236        let trader_id = TraderId::from("TRADER-001");
237        let clock = Rc::new(RefCell::new(TestClock::new()));
238        let cache = Rc::new(RefCell::new(Cache::default()));
239        let portfolio = Rc::new(RefCell::new(Portfolio::new(
240            cache.clone(),
241            clock.clone(),
242            None,
243        )));
244
245        let result = core.register(trader_id, clock, cache, portfolio);
246        assert!(result.is_ok());
247
248        assert!(core.order_manager.is_some());
249        assert!(core.order_factory.is_some());
250        assert!(core.portfolio.is_some());
251        assert_eq!(core.trader_id(), Some(trader_id));
252    }
253
254    #[rstest]
255    fn test_strategy_core_deref() {
256        let config = create_test_config();
257        let core = StrategyCore::new(config);
258
259        assert!(core.trader_id().is_none());
260    }
261
262    #[rstest]
263    fn test_strategy_core_debug() {
264        let config = create_test_config();
265        let core = StrategyCore::new(config);
266
267        let debug_str = format!("{core:?}");
268        assert!(debug_str.contains("StrategyCore"));
269    }
270}