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::{Ref, RefCell, RefMut},
18    fmt::Debug,
19    rc::Rc,
20};
21
22use ahash::AHashMap;
23use nautilus_common::{
24    actor::{DataActorConfig, DataActorCore, DataActorNative},
25    cache::Cache,
26    clock::Clock,
27    factories::OrderFactory,
28};
29use nautilus_execution::order_manager::manager::OrderManager;
30use nautilus_model::identifiers::{
31    ActorId, ClientOrderId, StrategyId, TraderId, normalize_order_id_tag,
32};
33use nautilus_portfolio::portfolio::Portfolio;
34use ustr::Ustr;
35
36use super::{
37    api::{OrderApi, PortfolioApi},
38    config::StrategyConfig,
39};
40
41/// The core component of a [`Strategy`](crate::strategy::Strategy), managing data, orders,
42/// and state.
43///
44/// This struct is intended to be held as a member within a user's custom strategy struct.
45/// Use the `nautilus_strategy!` macro to provide the trait accessors required by
46/// [`Strategy`](crate::strategy::Strategy), [`StrategyNative`], and
47/// [`DataActor`](nautilus_common::actor::DataActor). It does not deref to
48/// [`DataActorCore`]; normal strategy logic should use facade methods on the
49/// strategy value.
50pub struct StrategyCore {
51    pub(crate) actor: DataActorCore,
52    /// The strategy configuration.
53    pub config: StrategyConfig,
54    strategy_id: Option<StrategyId>,
55    order_id_tag: Option<String>,
56    pub(crate) order_manager: Option<OrderManager>,
57    pub(crate) order_factory: Option<Rc<RefCell<OrderFactory>>>,
58    pub(crate) portfolio: Option<Rc<RefCell<Portfolio>>>,
59    pub(crate) gtd_timers: AHashMap<ClientOrderId, Ustr>,
60    pub(crate) is_exiting: bool,
61    pub(crate) pending_stop: bool,
62    pub(crate) market_exit_attempts: u64,
63    pub(crate) market_exit_timer_name: Ustr,
64    pub(crate) market_exit_tag: Ustr,
65}
66
67impl Debug for StrategyCore {
68    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69        f.debug_struct(stringify!(StrategyCore))
70            .field("actor", &self.actor)
71            .field("config", &self.config)
72            .field("strategy_id", &self.strategy_id)
73            .field("order_id_tag", &self.order_id_tag)
74            .field("order_manager", &self.order_manager)
75            .field("order_factory", &self.order_factory)
76            .field("is_exiting", &self.is_exiting)
77            .field("pending_stop", &self.pending_stop)
78            .field("market_exit_attempts", &self.market_exit_attempts)
79            .finish()
80    }
81}
82
83/// Native-only access to internal strategy runtime state.
84///
85/// Use this trait from engine, runtime, testkit, or opt-in native strategy
86/// code when direct access to host runtime objects matters for an explicit
87/// latency-sensitive path, or when host integration code needs access below
88/// the facade API.
89///
90/// Do not import this trait in strategy code intended to run through Python or
91/// the plug-in authoring surface. Those surfaces should use facade methods such
92/// as `order()` and `portfolio()`, because native borrows, `Rc<RefCell<_>>`, and
93/// core references do not cross those boundaries.
94pub trait StrategyNative {
95    /// Returns the strategy core.
96    fn strategy_core(&self) -> &StrategyCore;
97
98    /// Returns the mutable strategy core.
99    fn strategy_core_mut(&mut self) -> &mut StrategyCore;
100
101    /// Returns a mutable borrow of the order factory.
102    ///
103    /// # Panics
104    ///
105    /// Panics if the strategy has not been registered.
106    fn order_factory(&mut self) -> RefMut<'_, OrderFactory> {
107        self.strategy_core_mut()
108            .order_factory
109            .as_ref()
110            .expect("Strategy not registered: OrderFactory not initialized")
111            .borrow_mut()
112    }
113
114    /// Returns a clone of the reference-counted order factory.
115    ///
116    /// # Panics
117    ///
118    /// Panics if the strategy has not been registered.
119    fn order_factory_rc(&self) -> Rc<RefCell<OrderFactory>> {
120        self.strategy_core()
121            .order_factory
122            .as_ref()
123            .expect("Strategy not registered: OrderFactory not initialized")
124            .clone()
125    }
126
127    /// Returns a clone of the reference-counted portfolio.
128    ///
129    /// # Panics
130    ///
131    /// Panics if the strategy has not been registered.
132    fn portfolio_rc(&self) -> Rc<RefCell<Portfolio>> {
133        self.strategy_core()
134            .portfolio
135            .as_ref()
136            .expect("Strategy not registered: Portfolio not initialized")
137            .clone()
138    }
139}
140
141impl StrategyCore {
142    /// Creates a new [`StrategyCore`] instance.
143    #[must_use]
144    pub fn new(config: StrategyConfig) -> Self {
145        let configured_strategy_id = config.strategy_id;
146        let configured_order_id_tag = normalize_order_id_tag(config.order_id_tag.as_deref());
147        let strategy_id = configured_strategy_id
148            .map(|id| strategy_id_with_order_id_tag(id, configured_order_id_tag));
149        let order_id_tag = strategy_id
150            .map(|id| id.get_tag().to_string())
151            .or_else(|| configured_order_id_tag.map(str::to_string));
152
153        let actor_config = DataActorConfig {
154            actor_id: strategy_id.map(|id| ActorId::from(id.inner().as_str())),
155            log_events: config.log_events,
156            log_commands: config.log_commands,
157        };
158
159        let strategy_id_str = strategy_id
160            .map(|id| id.inner().to_string())
161            .unwrap_or_default();
162        let market_exit_timer_name = Ustr::from(&format!("MARKET_EXIT_CHECK:{strategy_id_str}"));
163
164        Self {
165            actor: DataActorCore::new(actor_config),
166            config,
167            strategy_id,
168            order_id_tag,
169            order_manager: None,
170            order_factory: None,
171            portfolio: None,
172            gtd_timers: AHashMap::new(),
173            is_exiting: false,
174            pending_stop: false,
175            market_exit_attempts: 0,
176            market_exit_timer_name,
177            market_exit_tag: Ustr::from("MARKET_EXIT"),
178        }
179    }
180
181    /// Returns the strategy configuration.
182    #[must_use]
183    pub fn config(&self) -> &StrategyConfig {
184        &self.config
185    }
186
187    /// Changes the strategy ID before registration.
188    pub fn change_id(&mut self, strategy_id: StrategyId) {
189        let strategy_id = strategy_id_with_order_id_tag(strategy_id, self.order_id_tag());
190        self.set_runtime_strategy_id(strategy_id);
191    }
192
193    /// Changes the order ID tag before registration.
194    pub fn change_order_id_tag(&mut self, order_id_tag: &str) {
195        self.order_id_tag = normalize_order_id_tag(Some(order_id_tag)).map(str::to_string);
196
197        if let Some(strategy_id) = self.strategy_id
198            && let Some(order_id_tag) = self.order_id_tag()
199        {
200            let strategy_id = strategy_id_with_order_id_tag(strategy_id, Some(order_id_tag));
201            self.set_runtime_strategy_id(strategy_id);
202        }
203    }
204
205    fn set_runtime_strategy_id(&mut self, strategy_id: StrategyId) {
206        let actor_id = ActorId::from(strategy_id.inner().as_str());
207        self.actor.actor_id = actor_id;
208        self.actor.config.actor_id = Some(actor_id);
209        self.strategy_id = Some(strategy_id);
210        self.order_id_tag = Some(strategy_id.get_tag().to_string());
211        self.market_exit_timer_name = Ustr::from(&format!("MARKET_EXIT_CHECK:{strategy_id}"));
212    }
213
214    /// Returns the runtime order ID tag.
215    #[must_use]
216    pub fn order_id_tag(&self) -> Option<&str> {
217        self.order_id_tag.as_deref()
218    }
219
220    /// Returns the runtime strategy ID.
221    #[must_use]
222    pub fn strategy_id(&self) -> Option<StrategyId> {
223        self.strategy_id
224    }
225
226    /// Registers the strategy with the trading engine components.
227    ///
228    /// This is typically called by the framework when the strategy is added to an engine.
229    ///
230    /// # Errors
231    ///
232    /// Returns an error if registration with the actor core fails.
233    pub fn register(
234        &mut self,
235        trader_id: TraderId,
236        clock: Rc<RefCell<dyn Clock>>,
237        cache: Rc<RefCell<Cache>>,
238        portfolio: Rc<RefCell<Portfolio>>,
239    ) -> anyhow::Result<()> {
240        let strategy_id = StrategyId::from(self.actor.actor_id.inner().as_str());
241
242        self.actor
243            .register(trader_id, clock.clone(), cache.clone())?;
244
245        // Update market exit timer name with actual strategy ID
246        self.market_exit_timer_name = Ustr::from(&format!("MARKET_EXIT_CHECK:{strategy_id}"));
247
248        self.strategy_id = Some(strategy_id);
249        self.order_id_tag = Some(strategy_id.get_tag().to_string());
250
251        self.order_factory = Some(Rc::new(RefCell::new(OrderFactory::new(
252            trader_id,
253            strategy_id,
254            None,
255            None,
256            clock.clone(),
257            self.config.use_uuid_client_order_ids,
258            self.config.use_hyphens_in_client_order_ids,
259        ))));
260
261        self.order_manager = Some(OrderManager::new(clock, cache, false));
262
263        self.portfolio = Some(portfolio);
264
265        Ok(())
266    }
267
268    /// Returns the user-facing order creation API.
269    ///
270    /// # Panics
271    ///
272    /// Panics if the strategy has not been registered.
273    #[must_use]
274    pub fn order(&self) -> OrderApi<'_> {
275        let order_factory = self
276            .order_factory
277            .as_ref()
278            .expect("Strategy not registered: OrderFactory not initialized");
279        OrderApi::new(order_factory.as_ref())
280    }
281
282    /// Returns the user-facing portfolio read API.
283    ///
284    /// # Panics
285    ///
286    /// Panics if the strategy has not been registered.
287    #[must_use]
288    pub(crate) fn portfolio_api(&self) -> PortfolioApi<'_> {
289        let portfolio = self
290            .portfolio
291            .as_ref()
292            .expect("Strategy not registered: Portfolio not initialized");
293        PortfolioApi::new(portfolio.as_ref())
294    }
295
296    pub(crate) fn actor_id(&self) -> ActorId {
297        self.actor.actor_id()
298    }
299
300    pub(crate) fn trader_id(&self) -> Option<TraderId> {
301        self.actor.trader_id()
302    }
303
304    pub(crate) fn clock_mut(&mut self) -> RefMut<'_, dyn Clock> {
305        DataActorNative::clock_mut(self)
306    }
307
308    pub(crate) fn cache_ref(&self) -> Ref<'_, Cache> {
309        DataActorNative::cache_ref(self)
310    }
311
312    pub(crate) fn cache_rc(&self) -> Rc<RefCell<Cache>> {
313        DataActorNative::cache_rc(self)
314    }
315
316    /// Resets the market exit state.
317    pub fn reset_market_exit_state(&mut self) {
318        self.is_exiting = false;
319        self.pending_stop = false;
320        self.market_exit_attempts = 0;
321    }
322}
323
324impl DataActorNative for StrategyCore {
325    fn core(&self) -> &DataActorCore {
326        &self.actor
327    }
328
329    fn core_mut(&mut self) -> &mut DataActorCore {
330        &mut self.actor
331    }
332}
333
334impl StrategyNative for StrategyCore {
335    fn strategy_core(&self) -> &StrategyCore {
336        self
337    }
338
339    fn strategy_core_mut(&mut self) -> &mut StrategyCore {
340        self
341    }
342}
343
344fn strategy_id_with_order_id_tag(
345    strategy_id: StrategyId,
346    order_id_tag: Option<&str>,
347) -> StrategyId {
348    let Some(order_id_tag) = normalize_order_id_tag(order_id_tag) else {
349        return strategy_id;
350    };
351
352    if strategy_id.get_tag() == order_id_tag {
353        strategy_id
354    } else {
355        StrategyId::from(format!("{strategy_id}-{order_id_tag}"))
356    }
357}
358
359#[cfg(test)]
360mod tests {
361    use std::{cell::RefCell, rc::Rc};
362
363    use nautilus_common::{cache::Cache, clock::TestClock};
364    use nautilus_core::UnixNanos;
365    use nautilus_model::{
366        enums::{OrderSide, OrderType, TimeInForce, TrailingOffsetType, TriggerType},
367        identifiers::{AccountId, InstrumentId, StrategyId, TraderId},
368        orders::Order,
369        types::{Price, Quantity},
370    };
371    use nautilus_portfolio::portfolio::Portfolio;
372    use rstest::rstest;
373    use rust_decimal::Decimal;
374
375    use super::*;
376
377    fn create_test_config() -> StrategyConfig {
378        StrategyConfig {
379            strategy_id: Some(StrategyId::from("TEST-001")),
380            order_id_tag: Some("001".to_string()),
381            ..Default::default()
382        }
383    }
384
385    #[rstest]
386    fn test_strategy_core_new() {
387        let config = create_test_config();
388        let core = StrategyCore::new(config.clone());
389
390        assert_eq!(core.config.strategy_id, config.strategy_id);
391        assert_eq!(core.config.order_id_tag, config.order_id_tag);
392        assert_eq!(core.strategy_id(), config.strategy_id);
393        assert_eq!(core.order_id_tag(), Some("001"));
394        assert!(core.order_manager.is_none());
395        assert!(core.order_factory.is_none());
396        assert!(core.portfolio.is_none());
397        assert!(!core.is_exiting);
398        assert!(!core.pending_stop);
399        assert_eq!(core.market_exit_attempts, 0);
400    }
401
402    #[rstest]
403    fn test_strategy_core_new_applies_explicit_order_id_tag_to_strategy_id() {
404        let config = StrategyConfig {
405            strategy_id: Some(StrategyId::from("ExampleStrategy-XNAS")),
406            order_id_tag: Some("T01".to_string()),
407            ..Default::default()
408        };
409
410        let core = StrategyCore::new(config.clone());
411
412        assert_eq!(core.actor_id(), ActorId::from("ExampleStrategy-XNAS-T01"));
413        assert_eq!(core.config.strategy_id, config.strategy_id);
414        assert_eq!(core.config.order_id_tag, config.order_id_tag);
415        assert_eq!(
416            core.strategy_id(),
417            Some(StrategyId::from("ExampleStrategy-XNAS-T01"))
418        );
419        assert_eq!(core.order_id_tag(), Some("T01"));
420    }
421
422    #[rstest]
423    fn test_strategy_core_new_uses_strategy_tag_when_order_id_tag_is_omitted() {
424        let config = StrategyConfig {
425            strategy_id: Some(StrategyId::from("ExampleStrategy-XNAS")),
426            ..Default::default()
427        };
428
429        let core = StrategyCore::new(config.clone());
430
431        assert_eq!(core.actor_id(), ActorId::from("ExampleStrategy-XNAS"));
432        assert_eq!(core.config.strategy_id, config.strategy_id);
433        assert_eq!(core.config.order_id_tag, None);
434        assert_eq!(core.strategy_id(), config.strategy_id);
435        assert_eq!(core.order_id_tag(), Some("XNAS"));
436    }
437
438    #[rstest]
439    fn test_strategy_core_change_id_appends_existing_order_id_tag() {
440        let config = StrategyConfig {
441            order_id_tag: Some("T01".to_string()),
442            ..Default::default()
443        };
444        let mut core = StrategyCore::new(config);
445
446        core.change_id(StrategyId::from("ExampleStrategy-XNAS"));
447
448        assert_eq!(core.actor_id(), ActorId::from("ExampleStrategy-XNAS-T01"));
449        assert_eq!(
450            core.strategy_id(),
451            Some(StrategyId::from("ExampleStrategy-XNAS-T01"))
452        );
453        assert_eq!(core.order_id_tag(), Some("T01"));
454    }
455
456    #[rstest]
457    fn test_strategy_core_change_order_id_tag_appends_to_existing_strategy_id() {
458        let config = StrategyConfig {
459            strategy_id: Some(StrategyId::from("ExampleStrategy-XNAS")),
460            ..Default::default()
461        };
462        let mut core = StrategyCore::new(config);
463
464        core.change_order_id_tag("T01");
465
466        assert_eq!(core.actor_id(), ActorId::from("ExampleStrategy-XNAS-T01"));
467        assert_eq!(
468            core.strategy_id(),
469            Some(StrategyId::from("ExampleStrategy-XNAS-T01"))
470        );
471        assert_eq!(core.order_id_tag(), Some("T01"));
472    }
473
474    #[rstest]
475    fn test_strategy_core_change_order_id_tag_does_not_duplicate_matching_tag() {
476        let config = StrategyConfig {
477            strategy_id: Some(StrategyId::from("ExampleStrategy-XNAS-T01")),
478            ..Default::default()
479        };
480        let mut core = StrategyCore::new(config);
481
482        core.change_order_id_tag("T01");
483
484        assert_eq!(core.actor_id(), ActorId::from("ExampleStrategy-XNAS-T01"));
485        assert_eq!(
486            core.strategy_id(),
487            Some(StrategyId::from("ExampleStrategy-XNAS-T01"))
488        );
489        assert_eq!(core.order_id_tag(), Some("T01"));
490    }
491
492    #[rstest]
493    fn test_strategy_core_register() {
494        let config = create_test_config();
495        let mut core = StrategyCore::new(config);
496
497        let trader_id = TraderId::from("TRADER-001");
498        let clock = Rc::new(RefCell::new(TestClock::new()));
499        let cache = Rc::new(RefCell::new(Cache::default()));
500        let portfolio = Rc::new(RefCell::new(Portfolio::new(
501            clock.clone(),
502            cache.clone(),
503            None,
504        )));
505
506        let result = core.register(trader_id, clock, cache, portfolio);
507        assert!(result.is_ok());
508
509        assert!(core.order_manager.is_some());
510        assert!(core.order_factory.is_some());
511        assert!(core.portfolio.is_some());
512        assert_eq!(core.trader_id(), Some(trader_id));
513    }
514
515    #[rstest]
516    fn test_strategy_core_register_uses_order_id_tag_for_order_api_ids() {
517        let config = StrategyConfig {
518            strategy_id: Some(StrategyId::from("ExampleStrategy-XNAS")),
519            order_id_tag: Some("T01".to_string()),
520            ..Default::default()
521        };
522        let mut core = StrategyCore::new(config);
523
524        let trader_id = TraderId::from("TRADER-001");
525        let clock = Rc::new(RefCell::new(TestClock::new()));
526        let cache = Rc::new(RefCell::new(Cache::default()));
527        let portfolio = Rc::new(RefCell::new(Portfolio::new(
528            clock.clone(),
529            cache.clone(),
530            None,
531        )));
532
533        core.register(trader_id, clock, cache, portfolio).unwrap();
534
535        let orders = core.order();
536        let client_order_id = orders.generate_client_order_id();
537        let order_list_id = orders.generate_order_list_id();
538
539        assert_eq!(
540            core.strategy_id(),
541            Some(StrategyId::from("ExampleStrategy-XNAS-T01"))
542        );
543        assert_eq!(client_order_id.as_str(), "O-19700101-000000-001-T01-1");
544        assert_eq!(order_list_id.as_str(), "OL-19700101-000000-001-T01-1");
545    }
546
547    #[rstest]
548    fn test_strategy_core_order_api_creates_orders() {
549        let core = registered_test_core();
550        let orders = core.order();
551
552        let market = orders.market(
553            InstrumentId::from("BTCUSDT.BINANCE"),
554            OrderSide::Buy,
555            Quantity::from("1.0"),
556            None,
557            None,
558            None,
559            None,
560            None,
561            None,
562            None,
563        );
564        let limit = orders.limit(
565            InstrumentId::from("BTCUSDT.BINANCE"),
566            OrderSide::Sell,
567            Quantity::from("2.0"),
568            Price::from("100.00"),
569            None,
570            None,
571            None,
572            None,
573            None,
574            None,
575            None,
576            None,
577            None,
578            None,
579            None,
580            None,
581        );
582
583        assert_eq!(market.order_type(), OrderType::Market);
584        assert_eq!(
585            market.client_order_id().as_str(),
586            "O-19700101-000000-001-001-1"
587        );
588        assert_eq!(limit.order_type(), OrderType::Limit);
589        assert_eq!(
590            limit.client_order_id().as_str(),
591            "O-19700101-000000-001-001-2"
592        );
593    }
594
595    #[rstest]
596    fn test_strategy_core_order_api_creates_remaining_order_types() {
597        let core = registered_test_core();
598        let orders = core.order();
599        let instrument_id = InstrumentId::from("BTCUSDT.BINANCE");
600        let trigger_instrument_id = InstrumentId::from("ETHUSDT.BINANCE");
601        let expire_time = UnixNanos::from(1_000);
602        let display_qty = Quantity::from("0.5");
603
604        let stop_market = orders.stop_market(
605            instrument_id,
606            OrderSide::Buy,
607            Quantity::from("1.0"),
608            Price::from("99.00"),
609            Some(TriggerType::LastPrice),
610            Some(TimeInForce::Gtd),
611            Some(expire_time),
612            Some(true),
613            Some(false),
614            Some(display_qty),
615            Some(TriggerType::BidAsk),
616            Some(trigger_instrument_id),
617            None,
618            None,
619            None,
620            None,
621        );
622        let stop_limit = orders.stop_limit(
623            instrument_id,
624            OrderSide::Sell,
625            Quantity::from("1.1"),
626            Price::from("101.00"),
627            Price::from("100.50"),
628            Some(TriggerType::LastPrice),
629            Some(TimeInForce::Gtd),
630            Some(expire_time),
631            Some(true),
632            Some(false),
633            Some(false),
634            Some(display_qty),
635            Some(TriggerType::BidAsk),
636            Some(trigger_instrument_id),
637            None,
638            None,
639            None,
640            None,
641        );
642        let market_to_limit = orders.market_to_limit(
643            instrument_id,
644            OrderSide::Buy,
645            Quantity::from("1.2"),
646            Some(TimeInForce::Gtd),
647            Some(expire_time),
648            Some(true),
649            Some(false),
650            Some(display_qty),
651            None,
652            None,
653            None,
654            None,
655        );
656        let market_if_touched = orders.market_if_touched(
657            instrument_id,
658            OrderSide::Sell,
659            Quantity::from("1.3"),
660            Price::from("98.50"),
661            Some(TriggerType::LastPrice),
662            Some(TimeInForce::Gtd),
663            Some(expire_time),
664            Some(false),
665            Some(false),
666            Some(TriggerType::BidAsk),
667            Some(trigger_instrument_id),
668            None,
669            None,
670            None,
671            None,
672        );
673        let limit_if_touched = orders.limit_if_touched(
674            instrument_id,
675            OrderSide::Buy,
676            Quantity::from("1.4"),
677            Price::from("97.50"),
678            Price::from("97.00"),
679            Some(TriggerType::LastPrice),
680            Some(TimeInForce::Gtd),
681            Some(expire_time),
682            Some(true),
683            Some(false),
684            Some(false),
685            Some(display_qty),
686            Some(TriggerType::BidAsk),
687            Some(trigger_instrument_id),
688            None,
689            None,
690            None,
691            None,
692        );
693        let trailing_stop_market = orders.trailing_stop_market(
694            instrument_id,
695            OrderSide::Sell,
696            Quantity::from("1.5"),
697            Decimal::new(25, 2),
698            Some(TrailingOffsetType::Price),
699            Some(Price::from("105.00")),
700            Some(Price::from("104.50")),
701            Some(TriggerType::LastPrice),
702            Some(TimeInForce::Gtd),
703            Some(expire_time),
704            Some(false),
705            Some(false),
706            Some(display_qty),
707            Some(TriggerType::BidAsk),
708            Some(trigger_instrument_id),
709            None,
710            None,
711            None,
712            None,
713        );
714        let trailing_stop_limit = orders.trailing_stop_limit(
715            instrument_id,
716            OrderSide::Buy,
717            Quantity::from("1.6"),
718            Price::from("96.00"),
719            Decimal::new(10, 2),
720            Decimal::new(50, 2),
721            Some(TrailingOffsetType::Price),
722            Some(Price::from("97.00")),
723            Some(Price::from("96.50")),
724            Some(TriggerType::LastPrice),
725            Some(TimeInForce::Gtd),
726            Some(expire_time),
727            Some(true),
728            Some(false),
729            Some(false),
730            Some(display_qty),
731            Some(TriggerType::BidAsk),
732            Some(trigger_instrument_id),
733            None,
734            None,
735            None,
736            None,
737        );
738        let mut list_orders = vec![market_to_limit.clone(), stop_limit.clone()];
739        let order_list = orders.create_list(&mut list_orders, expire_time);
740
741        assert_eq!(stop_market.order_type(), OrderType::StopMarket);
742        assert_eq!(stop_market.trigger_price(), Some(Price::from("99.00")));
743        assert_eq!(stop_market.trigger_type(), Some(TriggerType::LastPrice));
744        assert_eq!(stop_market.time_in_force(), TimeInForce::Gtd);
745        assert_eq!(stop_market.expire_time(), Some(expire_time));
746        assert!(stop_market.is_reduce_only());
747        assert_eq!(stop_market.display_qty(), Some(display_qty));
748        assert_eq!(stop_market.emulation_trigger(), Some(TriggerType::BidAsk));
749        assert_eq!(
750            stop_market.trigger_instrument_id(),
751            Some(trigger_instrument_id)
752        );
753
754        assert_eq!(stop_limit.order_type(), OrderType::StopLimit);
755        assert_eq!(stop_limit.price(), Some(Price::from("101.00")));
756        assert_eq!(stop_limit.trigger_price(), Some(Price::from("100.50")));
757        assert!(stop_limit.is_post_only());
758
759        assert_eq!(market_to_limit.order_type(), OrderType::MarketToLimit);
760        assert_eq!(market_to_limit.time_in_force(), TimeInForce::Gtd);
761        assert_eq!(market_to_limit.expire_time(), Some(expire_time));
762        assert!(market_to_limit.is_reduce_only());
763        assert_eq!(market_to_limit.display_qty(), Some(display_qty));
764
765        assert_eq!(market_if_touched.order_type(), OrderType::MarketIfTouched);
766        assert_eq!(
767            market_if_touched.trigger_price(),
768            Some(Price::from("98.50"))
769        );
770        assert_eq!(
771            market_if_touched.trigger_type(),
772            Some(TriggerType::LastPrice)
773        );
774
775        assert_eq!(limit_if_touched.order_type(), OrderType::LimitIfTouched);
776        assert_eq!(limit_if_touched.price(), Some(Price::from("97.50")));
777        assert_eq!(limit_if_touched.trigger_price(), Some(Price::from("97.00")));
778        assert!(limit_if_touched.is_post_only());
779
780        assert_eq!(
781            trailing_stop_market.order_type(),
782            OrderType::TrailingStopMarket
783        );
784        assert_eq!(
785            trailing_stop_market.trailing_offset(),
786            Some(Decimal::new(25, 2))
787        );
788        assert_eq!(
789            trailing_stop_market.trailing_offset_type(),
790            Some(TrailingOffsetType::Price)
791        );
792        assert_eq!(
793            trailing_stop_market.activation_price(),
794            Some(Price::from("105.00"))
795        );
796        assert_eq!(
797            trailing_stop_market.trigger_price(),
798            Some(Price::from("104.50"))
799        );
800
801        assert_eq!(
802            trailing_stop_limit.order_type(),
803            OrderType::TrailingStopLimit
804        );
805        assert_eq!(trailing_stop_limit.price(), Some(Price::from("96.00")));
806        assert_eq!(
807            trailing_stop_limit.limit_offset(),
808            Some(Decimal::new(10, 2))
809        );
810        assert_eq!(
811            trailing_stop_limit.trailing_offset(),
812            Some(Decimal::new(50, 2))
813        );
814        assert_eq!(
815            trailing_stop_limit.activation_price(),
816            Some(Price::from("97.00"))
817        );
818        assert!(trailing_stop_limit.is_post_only());
819
820        assert_eq!(order_list.id, list_orders[0].order_list_id().unwrap());
821        assert_eq!(order_list.id, list_orders[1].order_list_id().unwrap());
822        assert_eq!(order_list.instrument_id, instrument_id);
823        assert_eq!(
824            order_list.client_order_ids,
825            list_orders
826                .iter()
827                .map(Order::client_order_id)
828                .collect::<Vec<_>>()
829        );
830    }
831
832    #[rstest]
833    fn test_strategy_core_order_api_generates_ids() {
834        let core = registered_test_core();
835        let (client_order_id, order_list_id) = {
836            let orders = core.order();
837            (
838                orders.generate_client_order_id(),
839                orders.generate_order_list_id(),
840            )
841        };
842
843        let next_client_order_id = core.order().generate_client_order_id();
844
845        assert_eq!(client_order_id.as_str(), "O-19700101-000000-001-001-1");
846        assert_eq!(order_list_id.as_str(), "OL-19700101-000000-001-001-1");
847        assert_eq!(next_client_order_id.as_str(), "O-19700101-000000-001-001-2");
848    }
849
850    #[rstest]
851    fn test_strategy_core_order_api_creates_bracket_orders() {
852        let core = registered_test_core();
853
854        let orders = core
855            .order()
856            .bracket()
857            .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
858            .order_side(OrderSide::Buy)
859            .quantity(Quantity::from("1.0"))
860            .tp_price(Price::from("110.00"))
861            .sl_trigger_price(Price::from("90.00"))
862            .call();
863        let order_list_id = orders[0].order_list_id();
864
865        assert_eq!(orders.len(), 3);
866        assert_eq!(orders[0].order_type(), OrderType::Market);
867        assert_eq!(orders[1].order_type(), OrderType::StopMarket);
868        assert_eq!(orders[2].order_type(), OrderType::Limit);
869        assert!(order_list_id.is_some());
870        assert!(
871            orders
872                .iter()
873                .all(|order| order.order_list_id() == order_list_id)
874        );
875    }
876
877    #[rstest]
878    fn test_strategy_core_portfolio_api_returns_owned_reads() {
879        let core = registered_test_core();
880        let portfolio = core.portfolio_api();
881        let instrument_id = InstrumentId::from("BTCUSDT.BINANCE");
882        let venue = instrument_id.venue;
883        let account_id = AccountId::from("SIM-001");
884
885        let is_initialized = portfolio.is_initialized();
886        let balances_locked = portfolio.balances_locked(&venue);
887        let margins_init = portfolio.margins_init(&venue);
888        let margins_maint = portfolio.margins_maint(&venue);
889        let unrealized_pnls = portfolio.unrealized_pnls(&venue, None);
890        let realized_pnls = portfolio.realized_pnls(&venue, None);
891        let net_exposures = portfolio.net_exposures(&venue, None);
892        let unrealized_pnl = portfolio.unrealized_pnl(&instrument_id);
893        let realized_pnl = portfolio.realized_pnl(&instrument_id);
894        let total_pnl = portfolio.total_pnl(&instrument_id);
895        let total_pnls = portfolio.total_pnls(&venue, None);
896        let mark_values = portfolio.mark_values(&venue, None);
897        let equity = portfolio.equity(&venue, None);
898        let net_exposure = portfolio.net_exposure(&instrument_id, None);
899        let is_flat = portfolio.is_flat(&instrument_id);
900        let net_position = portfolio.net_position(&instrument_id);
901        let missing_prices = portfolio.missing_price_instruments(&venue);
902        let snapshots = portfolio.snapshots(&account_id);
903        let recorded_realized_pnls = portfolio.recorded_realized_pnls();
904        let built_snapshot = portfolio.build_snapshot(&account_id);
905
906        assert!(!is_initialized);
907        assert!(balances_locked.is_empty());
908        assert!(margins_init.is_empty());
909        assert!(margins_maint.is_empty());
910        assert!(unrealized_pnls.is_empty());
911        assert!(realized_pnls.is_empty());
912        assert_eq!(net_exposures, None);
913        assert_eq!(unrealized_pnl, None);
914        assert_eq!(realized_pnl, None);
915        assert_eq!(total_pnl, None);
916        assert!(total_pnls.is_empty());
917        assert!(mark_values.is_empty());
918        assert!(equity.is_empty());
919        assert_eq!(net_exposure, None);
920        assert!(is_flat);
921        assert_eq!(net_position, Decimal::ZERO);
922        assert!(missing_prices.is_empty());
923        assert!(snapshots.is_empty());
924        assert!(recorded_realized_pnls.is_empty());
925        assert_eq!(built_snapshot, None);
926    }
927
928    #[rstest]
929    fn test_strategy_core_actor_state_starts_unregistered() {
930        let config = create_test_config();
931        let core = StrategyCore::new(config);
932
933        assert!(core.trader_id().is_none());
934    }
935
936    #[rstest]
937    fn test_strategy_core_debug() {
938        let config = create_test_config();
939        let core = StrategyCore::new(config);
940
941        let debug_str = format!("{core:?}");
942        assert!(debug_str.contains("StrategyCore"));
943    }
944
945    fn registered_test_core() -> StrategyCore {
946        let config = create_test_config();
947        let mut core = StrategyCore::new(config);
948
949        let trader_id = TraderId::from("TRADER-001");
950        let clock = Rc::new(RefCell::new(TestClock::new()));
951        let cache = Rc::new(RefCell::new(Cache::default()));
952        let portfolio = Rc::new(RefCell::new(Portfolio::new(
953            clock.clone(),
954            cache.clone(),
955            None,
956        )));
957
958        core.register(trader_id, clock, cache, portfolio).unwrap();
959        core
960    }
961}