1use 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_core::correctness::{CorrectnessResult, CorrectnessResultExt, FAILED};
30use nautilus_execution::order_manager::manager::OrderManager;
31use nautilus_model::identifiers::{
32 ActorId, ClientOrderId, StrategyId, TraderId, UNASSIGNED_ORDER_ID_TAG, check_order_id_tag,
33 normalize_order_id_tag,
34};
35use nautilus_portfolio::portfolio::Portfolio;
36use ustr::Ustr;
37
38use super::{
39 api::{OrderApi, PortfolioApi},
40 config::StrategyConfig,
41};
42
43pub struct StrategyCore {
53 pub(crate) actor: DataActorCore,
54 pub config: StrategyConfig,
56 strategy_id: Option<StrategyId>,
57 order_id_tag: Option<String>,
58 pub(crate) order_manager: Option<OrderManager>,
59 pub(crate) order_factory: Option<Rc<RefCell<OrderFactory>>>,
60 pub(crate) portfolio: Option<Rc<RefCell<Portfolio>>>,
61 pub(crate) gtd_timers: AHashMap<ClientOrderId, Ustr>,
62 pub(crate) is_exiting: bool,
63 pub(crate) pending_stop: bool,
64 pub(crate) market_exit_attempts: u64,
65 pub(crate) market_exit_timer_name: Ustr,
66 pub(crate) market_exit_tag: Ustr,
67}
68
69impl Debug for StrategyCore {
70 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71 f.debug_struct(stringify!(StrategyCore))
72 .field("actor", &self.actor)
73 .field("config", &self.config)
74 .field("strategy_id", &self.strategy_id)
75 .field("order_id_tag", &self.order_id_tag)
76 .field("order_manager", &self.order_manager)
77 .field("order_factory", &self.order_factory)
78 .field("is_exiting", &self.is_exiting)
79 .field("pending_stop", &self.pending_stop)
80 .field("market_exit_attempts", &self.market_exit_attempts)
81 .finish()
82 }
83}
84
85pub trait StrategyNative {
97 fn strategy_core(&self) -> &StrategyCore;
99
100 fn strategy_core_mut(&mut self) -> &mut StrategyCore;
102
103 fn order_factory(&mut self) -> RefMut<'_, OrderFactory> {
109 self.strategy_core_mut()
110 .order_factory
111 .as_ref()
112 .expect("Strategy not registered: OrderFactory not initialized")
113 .borrow_mut()
114 }
115
116 fn order_factory_rc(&self) -> Rc<RefCell<OrderFactory>> {
122 self.strategy_core()
123 .order_factory
124 .as_ref()
125 .expect("Strategy not registered: OrderFactory not initialized")
126 .clone()
127 }
128
129 fn portfolio_rc(&self) -> Rc<RefCell<Portfolio>> {
135 self.strategy_core()
136 .portfolio
137 .as_ref()
138 .expect("Strategy not registered: Portfolio not initialized")
139 .clone()
140 }
141}
142
143impl StrategyCore {
144 pub fn new_checked(config: StrategyConfig) -> CorrectnessResult<Self> {
151 if let Some(order_id_tag) = config.order_id_tag.as_deref() {
152 check_order_id_tag(order_id_tag)?;
153 }
154
155 let configured_strategy_id = config.strategy_id;
156 let configured_order_id_tag = normalize_order_id_tag(config.order_id_tag.as_deref());
157 let strategy_id = configured_strategy_id
158 .map(|id| strategy_id_with_order_id_tag(id, configured_order_id_tag))
159 .transpose()?;
160 let order_id_tag = strategy_id
161 .map(|id| id.get_tag().to_string())
162 .or_else(|| configured_order_id_tag.map(str::to_string));
163
164 let actor_config = DataActorConfig {
165 actor_id: Some(strategy_id.map_or_else(unassigned_strategy_actor_id, |id| {
166 ActorId::from(id.inner().as_str())
167 })),
168 log_events: config.log_events,
169 log_commands: config.log_commands,
170 };
171
172 let strategy_id_str = strategy_id
173 .map(|id| id.inner().to_string())
174 .unwrap_or_default();
175 let market_exit_timer_name = Ustr::from(&format!("MARKET_EXIT_CHECK:{strategy_id_str}"));
176
177 Ok(Self {
178 actor: DataActorCore::new(actor_config),
179 config,
180 strategy_id,
181 order_id_tag,
182 order_manager: None,
183 order_factory: None,
184 portfolio: None,
185 gtd_timers: AHashMap::new(),
186 is_exiting: false,
187 pending_stop: false,
188 market_exit_attempts: 0,
189 market_exit_timer_name,
190 market_exit_tag: Ustr::from("MARKET_EXIT"),
191 })
192 }
193
194 #[must_use]
201 pub fn new(config: StrategyConfig) -> Self {
202 Self::new_checked(config).expect_display(FAILED)
203 }
204
205 #[must_use]
207 pub fn config(&self) -> &StrategyConfig {
208 &self.config
209 }
210
211 pub fn change_id(&mut self, strategy_id: StrategyId) -> anyhow::Result<()> {
218 let strategy_id = strategy_id_with_order_id_tag(strategy_id, self.order_id_tag())?;
219 self.set_runtime_strategy_id(strategy_id);
220 Ok(())
221 }
222
223 pub fn change_order_id_tag(&mut self, order_id_tag: &str) -> anyhow::Result<()> {
230 check_order_id_tag(order_id_tag)?;
231
232 let normalized_order_id_tag =
233 normalize_order_id_tag(Some(order_id_tag)).map(str::to_string);
234
235 if let Some(strategy_id) = self.strategy_id
236 && let Some(order_id_tag) = normalized_order_id_tag.as_deref()
237 {
238 let strategy_id = strategy_id_with_order_id_tag(strategy_id, Some(order_id_tag))?;
239 self.set_runtime_strategy_id(strategy_id);
240 } else {
241 self.order_id_tag = normalized_order_id_tag;
242 }
243
244 Ok(())
245 }
246
247 fn set_runtime_strategy_id(&mut self, strategy_id: StrategyId) {
248 let actor_id = ActorId::from(strategy_id.inner().as_str());
249 self.actor.actor_id = actor_id;
250 self.actor.config.actor_id = Some(actor_id);
251 self.strategy_id = Some(strategy_id);
252 self.order_id_tag = Some(strategy_id.get_tag().to_string());
253 self.market_exit_timer_name = Ustr::from(&format!("MARKET_EXIT_CHECK:{strategy_id}"));
254 }
255
256 #[must_use]
258 pub fn order_id_tag(&self) -> Option<&str> {
259 self.order_id_tag.as_deref()
260 }
261
262 #[must_use]
264 pub fn strategy_id(&self) -> Option<StrategyId> {
265 self.strategy_id
266 }
267
268 pub fn register(
277 &mut self,
278 trader_id: TraderId,
279 clock: Rc<RefCell<dyn Clock>>,
280 cache: Rc<RefCell<Cache>>,
281 portfolio: Rc<RefCell<Portfolio>>,
282 ) -> anyhow::Result<()> {
283 if let Some(order_id_tag) = self.config.order_id_tag.as_deref() {
285 check_order_id_tag(order_id_tag)?;
286 }
287
288 let strategy_id = StrategyId::from(self.actor.actor_id.inner().as_str());
289
290 self.actor
291 .register(trader_id, clock.clone(), cache.clone())?;
292
293 self.market_exit_timer_name = Ustr::from(&format!("MARKET_EXIT_CHECK:{strategy_id}"));
295
296 self.strategy_id = Some(strategy_id);
297 self.order_id_tag = Some(strategy_id.get_tag().to_string());
298
299 self.order_factory = Some(Rc::new(RefCell::new(OrderFactory::new(
300 trader_id,
301 strategy_id,
302 None,
303 None,
304 clock.clone(),
305 self.config.use_uuid_client_order_ids,
306 self.config.use_hyphens_in_client_order_ids,
307 ))));
308
309 self.order_manager = Some(OrderManager::new(clock, cache, false));
310
311 self.portfolio = Some(portfolio);
312
313 Ok(())
314 }
315
316 #[must_use]
322 pub fn order(&self) -> OrderApi<'_> {
323 let order_factory = self
324 .order_factory
325 .as_ref()
326 .expect("Strategy not registered: OrderFactory not initialized");
327 OrderApi::new(order_factory.as_ref())
328 }
329
330 #[must_use]
336 pub(crate) fn portfolio_api(&self) -> PortfolioApi<'_> {
337 let portfolio = self
338 .portfolio
339 .as_ref()
340 .expect("Strategy not registered: Portfolio not initialized");
341 PortfolioApi::new(portfolio.as_ref())
342 }
343
344 pub(crate) fn actor_id(&self) -> ActorId {
345 self.actor.actor_id()
346 }
347
348 pub(crate) fn trader_id(&self) -> Option<TraderId> {
349 self.actor.trader_id()
350 }
351
352 pub(crate) fn clock_mut(&mut self) -> RefMut<'_, dyn Clock> {
353 DataActorNative::clock_mut(self)
354 }
355
356 pub(crate) fn cache_ref(&self) -> Ref<'_, Cache> {
357 DataActorNative::cache_ref(self)
358 }
359
360 pub(crate) fn cache_rc(&self) -> Rc<RefCell<Cache>> {
361 DataActorNative::cache_rc(self)
362 }
363
364 pub fn reset_market_exit_state(&mut self) {
366 self.is_exiting = false;
367 self.pending_stop = false;
368 self.market_exit_attempts = 0;
369 }
370}
371
372impl DataActorNative for StrategyCore {
373 fn core(&self) -> &DataActorCore {
374 &self.actor
375 }
376
377 fn core_mut(&mut self) -> &mut DataActorCore {
378 &mut self.actor
379 }
380}
381
382impl StrategyNative for StrategyCore {
383 fn strategy_core(&self) -> &StrategyCore {
384 self
385 }
386
387 fn strategy_core_mut(&mut self) -> &mut StrategyCore {
388 self
389 }
390}
391
392fn unassigned_strategy_actor_id() -> ActorId {
397 ActorId::from(format!(
398 "{}-{UNASSIGNED_ORDER_ID_TAG}",
399 stringify!(Strategy)
400 ))
401}
402
403fn strategy_id_with_order_id_tag(
404 strategy_id: StrategyId,
405 order_id_tag: Option<&str>,
406) -> CorrectnessResult<StrategyId> {
407 let Some(order_id_tag) = normalize_order_id_tag(order_id_tag) else {
408 return Ok(strategy_id);
409 };
410
411 if strategy_id.get_tag() == order_id_tag {
412 Ok(strategy_id)
413 } else {
414 StrategyId::new_checked(format!("{strategy_id}-{order_id_tag}"))
415 }
416}
417
418#[cfg(test)]
419mod tests {
420 use std::{cell::RefCell, rc::Rc};
421
422 use nautilus_common::{cache::Cache, clock::TestClock};
423 use nautilus_core::UnixNanos;
424 use nautilus_model::{
425 enums::{OrderSide, OrderType, TimeInForce, TrailingOffsetType, TriggerType},
426 identifiers::{AccountId, InstrumentId, StrategyId, TraderId},
427 orders::Order,
428 types::{Price, Quantity},
429 };
430 use nautilus_portfolio::portfolio::Portfolio;
431 use rstest::rstest;
432 use rust_decimal::Decimal;
433
434 use super::*;
435
436 fn create_test_config() -> StrategyConfig {
437 StrategyConfig {
438 strategy_id: Some(StrategyId::from("TEST-001")),
439 order_id_tag: Some("001".to_string()),
440 ..Default::default()
441 }
442 }
443
444 #[rstest]
445 fn test_strategy_core_new() {
446 let config = create_test_config();
447 let core = StrategyCore::new(config.clone());
448
449 assert_eq!(core.config.strategy_id, config.strategy_id);
450 assert_eq!(core.config.order_id_tag, config.order_id_tag);
451 assert_eq!(core.strategy_id(), config.strategy_id);
452 assert_eq!(core.order_id_tag(), Some("001"));
453 assert!(core.order_manager.is_none());
454 assert!(core.order_factory.is_none());
455 assert!(core.portfolio.is_none());
456 assert!(!core.is_exiting);
457 assert!(!core.pending_stop);
458 assert_eq!(core.market_exit_attempts, 0);
459 }
460
461 #[rstest]
462 fn test_strategy_core_new_without_configured_id_uses_the_unassigned_actor_id() {
463 let core = StrategyCore::new(StrategyConfig::default());
464
465 assert_eq!(core.actor_id(), ActorId::from("Strategy-None"));
466 assert_eq!(core.strategy_id(), None);
467 assert_eq!(core.order_id_tag(), None);
468 assert_eq!(
469 StrategyId::from(core.actor_id().inner().as_str()),
470 StrategyId::from("Strategy-None")
471 );
472 }
473
474 #[rstest]
475 fn test_strategy_core_new_applies_explicit_order_id_tag_to_strategy_id() {
476 let config = StrategyConfig {
477 strategy_id: Some(StrategyId::from("ExampleStrategy-XNAS")),
478 order_id_tag: Some("T01".to_string()),
479 ..Default::default()
480 };
481
482 let core = StrategyCore::new(config.clone());
483
484 assert_eq!(core.actor_id(), ActorId::from("ExampleStrategy-XNAS-T01"));
485 assert_eq!(core.config.strategy_id, config.strategy_id);
486 assert_eq!(core.config.order_id_tag, config.order_id_tag);
487 assert_eq!(
488 core.strategy_id(),
489 Some(StrategyId::from("ExampleStrategy-XNAS-T01"))
490 );
491 assert_eq!(core.order_id_tag(), Some("T01"));
492 }
493
494 #[rstest]
495 fn test_strategy_core_new_uses_strategy_tag_when_order_id_tag_is_omitted() {
496 let config = StrategyConfig {
497 strategy_id: Some(StrategyId::from("ExampleStrategy-XNAS")),
498 ..Default::default()
499 };
500
501 let core = StrategyCore::new(config.clone());
502
503 assert_eq!(core.actor_id(), ActorId::from("ExampleStrategy-XNAS"));
504 assert_eq!(core.config.strategy_id, config.strategy_id);
505 assert_eq!(core.config.order_id_tag, None);
506 assert_eq!(core.strategy_id(), config.strategy_id);
507 assert_eq!(core.order_id_tag(), Some("XNAS"));
508 }
509
510 #[rstest]
511 fn test_strategy_core_change_id_appends_existing_order_id_tag() {
512 let config = StrategyConfig {
513 order_id_tag: Some("T01".to_string()),
514 ..Default::default()
515 };
516 let mut core = StrategyCore::new(config);
517
518 core.change_id(StrategyId::from("ExampleStrategy-XNAS"))
519 .unwrap();
520
521 assert_eq!(core.actor_id(), ActorId::from("ExampleStrategy-XNAS-T01"));
522 assert_eq!(
523 core.strategy_id(),
524 Some(StrategyId::from("ExampleStrategy-XNAS-T01"))
525 );
526 assert_eq!(core.order_id_tag(), Some("T01"));
527 }
528
529 #[rstest]
530 fn test_strategy_core_change_order_id_tag_appends_to_existing_strategy_id() {
531 let config = StrategyConfig {
532 strategy_id: Some(StrategyId::from("ExampleStrategy-XNAS")),
533 ..Default::default()
534 };
535 let mut core = StrategyCore::new(config);
536
537 core.change_order_id_tag("T01").unwrap();
538
539 assert_eq!(core.actor_id(), ActorId::from("ExampleStrategy-XNAS-T01"));
540 assert_eq!(
541 core.strategy_id(),
542 Some(StrategyId::from("ExampleStrategy-XNAS-T01"))
543 );
544 assert_eq!(core.order_id_tag(), Some("T01"));
545 }
546
547 #[rstest]
548 fn test_strategy_core_change_order_id_tag_does_not_duplicate_matching_tag() {
549 let config = StrategyConfig {
550 strategy_id: Some(StrategyId::from("ExampleStrategy-XNAS-T01")),
551 ..Default::default()
552 };
553 let mut core = StrategyCore::new(config);
554
555 core.change_order_id_tag("T01").unwrap();
556
557 assert_eq!(core.actor_id(), ActorId::from("ExampleStrategy-XNAS-T01"));
558 assert_eq!(
559 core.strategy_id(),
560 Some(StrategyId::from("ExampleStrategy-XNAS-T01"))
561 );
562 assert_eq!(core.order_id_tag(), Some("T01"));
563 }
564
565 #[rstest]
566 fn test_strategy_core_new_checked_rejects_order_id_tag_with_separator() {
567 let config = StrategyConfig {
568 strategy_id: Some(StrategyId::from("HyphenTagStrategy-A-B")),
569 order_id_tag: Some("A-B".to_string()),
570 ..Default::default()
571 };
572
573 let error = StrategyCore::new_checked(config).unwrap_err();
574
575 assert_eq!(
576 error.to_string(),
577 "`order_id_tag` cannot contain the '-' strategy ID separator, was 'A-B'"
578 );
579 }
580
581 #[rstest]
582 #[case(Some("001".to_string()))]
583 #[case(Some("None".to_string()))]
584 #[case(Some(String::new()))]
585 #[case(None)]
586 fn test_strategy_core_new_checked_accepts_usable_order_id_tag(
587 #[case] order_id_tag: Option<String>,
588 ) {
589 let config = StrategyConfig {
590 strategy_id: Some(StrategyId::from("ExampleStrategy-XNAS")),
591 order_id_tag,
592 ..Default::default()
593 };
594
595 assert!(StrategyCore::new_checked(config).is_ok());
596 }
597
598 #[rstest]
599 #[should_panic(expected = "`order_id_tag` cannot contain the '-' strategy ID separator")]
600 fn test_strategy_core_new_panics_on_order_id_tag_with_separator() {
601 let config = StrategyConfig {
602 order_id_tag: Some("A-B".to_string()),
603 ..Default::default()
604 };
605
606 let _ = StrategyCore::new(config);
607 }
608
609 #[rstest]
610 fn test_strategy_core_change_order_id_tag_rejects_separator() {
611 let config = StrategyConfig {
612 strategy_id: Some(StrategyId::from("ExampleStrategy-XNAS")),
613 ..Default::default()
614 };
615 let mut core = StrategyCore::new(config);
616
617 let error = core.change_order_id_tag("A-B").unwrap_err();
618
619 assert_eq!(
620 error.to_string(),
621 "`order_id_tag` cannot contain the '-' strategy ID separator, was 'A-B'"
622 );
623 assert_eq!(core.actor_id(), ActorId::from("ExampleStrategy-XNAS"));
624 assert_eq!(
625 core.strategy_id(),
626 Some(StrategyId::from("ExampleStrategy-XNAS"))
627 );
628 assert_eq!(core.order_id_tag(), Some("XNAS"));
629 }
630
631 #[rstest]
632 fn test_strategy_core_new_checked_rejects_non_ascii_order_id_tag() {
633 let config = StrategyConfig {
634 strategy_id: Some(StrategyId::from("ExampleStrategy-XNAS")),
635 order_id_tag: Some("T01€".to_string()),
636 ..Default::default()
637 };
638
639 let error = StrategyCore::new_checked(config).unwrap_err();
640
641 assert_eq!(
642 error.to_string(),
643 "invalid string for 'value' contained a non-ASCII char, was 'ExampleStrategy-XNAS-T01€'"
644 );
645 }
646
647 #[rstest]
648 #[should_panic(
649 expected = "invalid string for 'value' contained a non-ASCII char, was 'ExampleStrategy-XNAS-T01€'"
650 )]
651 fn test_strategy_core_new_panics_on_non_ascii_order_id_tag() {
652 let config = StrategyConfig {
653 strategy_id: Some(StrategyId::from("ExampleStrategy-XNAS")),
654 order_id_tag: Some("T01€".to_string()),
655 ..Default::default()
656 };
657
658 let _ = StrategyCore::new(config);
659 }
660
661 #[rstest]
662 fn test_strategy_core_change_order_id_tag_rejects_non_ascii() {
663 let config = StrategyConfig {
664 strategy_id: Some(StrategyId::from("ExampleStrategy-XNAS")),
665 ..Default::default()
666 };
667 let mut core = StrategyCore::new(config);
668
669 let error = core.change_order_id_tag("T01€").unwrap_err();
670
671 assert_eq!(
672 error.to_string(),
673 "invalid string for 'value' contained a non-ASCII char, was 'ExampleStrategy-XNAS-T01€'"
674 );
675 assert_eq!(core.actor_id(), ActorId::from("ExampleStrategy-XNAS"));
676 assert_eq!(
677 core.strategy_id(),
678 Some(StrategyId::from("ExampleStrategy-XNAS"))
679 );
680 assert_eq!(core.order_id_tag(), Some("XNAS"));
681 }
682
683 #[rstest]
684 fn test_strategy_core_change_id_rejects_non_ascii_order_id_tag() {
685 let config = StrategyConfig {
686 order_id_tag: Some("T01€".to_string()),
687 ..Default::default()
688 };
689 let mut core = StrategyCore::new(config);
690
691 let error = core
692 .change_id(StrategyId::from("ExampleStrategy-XNAS"))
693 .unwrap_err();
694
695 assert_eq!(
696 error.to_string(),
697 "invalid string for 'value' contained a non-ASCII char, was 'ExampleStrategy-XNAS-T01€'"
698 );
699 assert_eq!(core.actor_id(), ActorId::from("Strategy-None"));
700 assert_eq!(core.strategy_id(), None);
701 assert_eq!(core.order_id_tag(), Some("T01€"));
702 }
703
704 #[rstest]
705 fn test_strategy_core_change_order_id_tag_without_strategy_id_stores_tag() {
706 let mut core = StrategyCore::new(StrategyConfig::default());
707
708 core.change_order_id_tag("T01").unwrap();
709
710 assert_eq!(core.actor_id(), ActorId::from("Strategy-None"));
711 assert_eq!(core.strategy_id(), None);
712 assert_eq!(core.order_id_tag(), Some("T01"));
713 }
714
715 #[rstest]
716 #[case("")]
717 #[case("None")]
718 fn test_strategy_core_change_order_id_tag_clears_unset_sentinel(#[case] order_id_tag: &str) {
719 let config = StrategyConfig {
720 strategy_id: Some(StrategyId::from("ExampleStrategy-XNAS")),
721 ..Default::default()
722 };
723 let mut core = StrategyCore::new(config);
724
725 core.change_order_id_tag(order_id_tag).unwrap();
726
727 assert_eq!(core.actor_id(), ActorId::from("ExampleStrategy-XNAS"));
728 assert_eq!(
729 core.strategy_id(),
730 Some(StrategyId::from("ExampleStrategy-XNAS"))
731 );
732 assert_eq!(core.order_id_tag(), None);
733 }
734
735 #[rstest]
736 fn test_strategy_core_register_rejects_configured_order_id_tag_with_separator() {
737 let config = StrategyConfig {
738 strategy_id: Some(StrategyId::from("HyphenTagStrategy-001")),
739 order_id_tag: Some("001".to_string()),
740 ..Default::default()
741 };
742 let mut core = StrategyCore::new(config);
743 core.config.order_id_tag = Some("A-B".to_string());
744
745 let trader_id = TraderId::from("TRADER-001");
746 let clock = Rc::new(RefCell::new(TestClock::new()));
747 let cache = Rc::new(RefCell::new(Cache::default()));
748 let portfolio = Rc::new(RefCell::new(Portfolio::new(
749 clock.clone(),
750 cache.clone(),
751 None,
752 )));
753
754 let error = core
755 .register(trader_id, clock, cache, portfolio)
756 .unwrap_err();
757
758 assert_eq!(
759 error.to_string(),
760 "`order_id_tag` cannot contain the '-' strategy ID separator, was 'A-B'"
761 );
762 assert!(core.order_factory.is_none());
763 assert!(core.order_manager.is_none());
764 assert!(core.portfolio.is_none());
765 assert_eq!(core.trader_id(), None);
766 }
767
768 #[rstest]
769 fn test_strategy_core_register() {
770 let config = create_test_config();
771 let mut core = StrategyCore::new(config);
772
773 let trader_id = TraderId::from("TRADER-001");
774 let clock = Rc::new(RefCell::new(TestClock::new()));
775 let cache = Rc::new(RefCell::new(Cache::default()));
776 let portfolio = Rc::new(RefCell::new(Portfolio::new(
777 clock.clone(),
778 cache.clone(),
779 None,
780 )));
781
782 let result = core.register(trader_id, clock, cache, portfolio);
783 assert!(result.is_ok());
784
785 assert!(core.order_manager.is_some());
786 assert!(core.order_factory.is_some());
787 assert!(core.portfolio.is_some());
788 assert_eq!(core.trader_id(), Some(trader_id));
789 }
790
791 #[rstest]
792 fn test_strategy_core_register_uses_order_id_tag_for_order_api_ids() {
793 let config = StrategyConfig {
794 strategy_id: Some(StrategyId::from("ExampleStrategy-XNAS")),
795 order_id_tag: Some("T01".to_string()),
796 ..Default::default()
797 };
798 let mut core = StrategyCore::new(config);
799
800 let trader_id = TraderId::from("TRADER-001");
801 let clock = Rc::new(RefCell::new(TestClock::new()));
802 let cache = Rc::new(RefCell::new(Cache::default()));
803 let portfolio = Rc::new(RefCell::new(Portfolio::new(
804 clock.clone(),
805 cache.clone(),
806 None,
807 )));
808
809 core.register(trader_id, clock, cache, portfolio).unwrap();
810
811 let orders = core.order();
812 let client_order_id = orders.generate_client_order_id();
813 let order_list_id = orders.generate_order_list_id();
814
815 assert_eq!(
816 core.strategy_id(),
817 Some(StrategyId::from("ExampleStrategy-XNAS-T01"))
818 );
819 assert_eq!(client_order_id.as_str(), "O-19700101-000000-001-T01-1");
820 assert_eq!(order_list_id.as_str(), "OL-19700101-000000-001-T01-1");
821 }
822
823 #[rstest]
824 fn test_strategy_core_order_api_creates_orders() {
825 let core = registered_test_core();
826 let orders = core.order();
827
828 let market = orders.market(
829 InstrumentId::from("BTCUSDT.BINANCE"),
830 OrderSide::Buy,
831 Quantity::from("1.0"),
832 None,
833 None,
834 None,
835 None,
836 None,
837 None,
838 None,
839 );
840 let limit = orders.limit(
841 InstrumentId::from("BTCUSDT.BINANCE"),
842 OrderSide::Sell,
843 Quantity::from("2.0"),
844 Price::from("100.00"),
845 None,
846 None,
847 None,
848 None,
849 None,
850 None,
851 None,
852 None,
853 None,
854 None,
855 None,
856 None,
857 );
858
859 assert_eq!(market.order_type(), OrderType::Market);
860 assert_eq!(
861 market.client_order_id().as_str(),
862 "O-19700101-000000-001-001-1"
863 );
864 assert_eq!(limit.order_type(), OrderType::Limit);
865 assert_eq!(
866 limit.client_order_id().as_str(),
867 "O-19700101-000000-001-001-2"
868 );
869 }
870
871 #[rstest]
872 fn test_strategy_core_order_api_creates_remaining_order_types() {
873 let core = registered_test_core();
874 let orders = core.order();
875 let instrument_id = InstrumentId::from("BTCUSDT.BINANCE");
876 let trigger_instrument_id = InstrumentId::from("ETHUSDT.BINANCE");
877 let expire_time = UnixNanos::from(1_000);
878 let display_qty = Quantity::from("0.5");
879
880 let stop_market = orders.stop_market(
881 instrument_id,
882 OrderSide::Buy,
883 Quantity::from("1.0"),
884 Price::from("99.00"),
885 Some(TriggerType::LastPrice),
886 Some(TimeInForce::Gtd),
887 Some(expire_time),
888 Some(true),
889 Some(false),
890 Some(display_qty),
891 Some(TriggerType::BidAsk),
892 Some(trigger_instrument_id),
893 None,
894 None,
895 None,
896 None,
897 );
898 let stop_limit = orders.stop_limit(
899 instrument_id,
900 OrderSide::Sell,
901 Quantity::from("1.1"),
902 Price::from("101.00"),
903 Price::from("100.50"),
904 Some(TriggerType::LastPrice),
905 Some(TimeInForce::Gtd),
906 Some(expire_time),
907 Some(true),
908 Some(false),
909 Some(false),
910 Some(display_qty),
911 Some(TriggerType::BidAsk),
912 Some(trigger_instrument_id),
913 None,
914 None,
915 None,
916 None,
917 );
918 let market_to_limit = orders.market_to_limit(
919 instrument_id,
920 OrderSide::Buy,
921 Quantity::from("1.2"),
922 Some(TimeInForce::Gtd),
923 Some(expire_time),
924 Some(true),
925 Some(false),
926 Some(display_qty),
927 None,
928 None,
929 None,
930 None,
931 );
932 let market_if_touched = orders.market_if_touched(
933 instrument_id,
934 OrderSide::Sell,
935 Quantity::from("1.3"),
936 Price::from("98.50"),
937 Some(TriggerType::LastPrice),
938 Some(TimeInForce::Gtd),
939 Some(expire_time),
940 Some(false),
941 Some(false),
942 Some(TriggerType::BidAsk),
943 Some(trigger_instrument_id),
944 None,
945 None,
946 None,
947 None,
948 );
949 let limit_if_touched = orders.limit_if_touched(
950 instrument_id,
951 OrderSide::Buy,
952 Quantity::from("1.4"),
953 Price::from("97.50"),
954 Price::from("97.00"),
955 Some(TriggerType::LastPrice),
956 Some(TimeInForce::Gtd),
957 Some(expire_time),
958 Some(true),
959 Some(false),
960 Some(false),
961 Some(display_qty),
962 Some(TriggerType::BidAsk),
963 Some(trigger_instrument_id),
964 None,
965 None,
966 None,
967 None,
968 );
969 let trailing_stop_market = orders.trailing_stop_market(
970 instrument_id,
971 OrderSide::Sell,
972 Quantity::from("1.5"),
973 Decimal::new(25, 2),
974 Some(TrailingOffsetType::Price),
975 Some(Price::from("105.00")),
976 Some(Price::from("104.50")),
977 Some(TriggerType::LastPrice),
978 Some(TimeInForce::Gtd),
979 Some(expire_time),
980 Some(false),
981 Some(false),
982 Some(display_qty),
983 Some(TriggerType::BidAsk),
984 Some(trigger_instrument_id),
985 None,
986 None,
987 None,
988 None,
989 );
990 let trailing_stop_limit = orders.trailing_stop_limit(
991 instrument_id,
992 OrderSide::Buy,
993 Quantity::from("1.6"),
994 Price::from("96.00"),
995 Decimal::new(10, 2),
996 Decimal::new(50, 2),
997 Some(TrailingOffsetType::Price),
998 Some(Price::from("97.00")),
999 Some(Price::from("96.50")),
1000 Some(TriggerType::LastPrice),
1001 Some(TimeInForce::Gtd),
1002 Some(expire_time),
1003 Some(true),
1004 Some(false),
1005 Some(false),
1006 Some(display_qty),
1007 Some(TriggerType::BidAsk),
1008 Some(trigger_instrument_id),
1009 None,
1010 None,
1011 None,
1012 None,
1013 );
1014 let mut list_orders = vec![market_to_limit.clone(), stop_limit.clone()];
1015 let order_list = orders.create_list(&mut list_orders, expire_time);
1016
1017 assert_eq!(stop_market.order_type(), OrderType::StopMarket);
1018 assert_eq!(stop_market.trigger_price(), Some(Price::from("99.00")));
1019 assert_eq!(stop_market.trigger_type(), Some(TriggerType::LastPrice));
1020 assert_eq!(stop_market.time_in_force(), TimeInForce::Gtd);
1021 assert_eq!(stop_market.expire_time(), Some(expire_time));
1022 assert!(stop_market.is_reduce_only());
1023 assert_eq!(stop_market.display_qty(), Some(display_qty));
1024 assert_eq!(stop_market.emulation_trigger(), Some(TriggerType::BidAsk));
1025 assert_eq!(
1026 stop_market.trigger_instrument_id(),
1027 Some(trigger_instrument_id)
1028 );
1029
1030 assert_eq!(stop_limit.order_type(), OrderType::StopLimit);
1031 assert_eq!(stop_limit.price(), Some(Price::from("101.00")));
1032 assert_eq!(stop_limit.trigger_price(), Some(Price::from("100.50")));
1033 assert!(stop_limit.is_post_only());
1034
1035 assert_eq!(market_to_limit.order_type(), OrderType::MarketToLimit);
1036 assert_eq!(market_to_limit.time_in_force(), TimeInForce::Gtd);
1037 assert_eq!(market_to_limit.expire_time(), Some(expire_time));
1038 assert!(market_to_limit.is_reduce_only());
1039 assert_eq!(market_to_limit.display_qty(), Some(display_qty));
1040
1041 assert_eq!(market_if_touched.order_type(), OrderType::MarketIfTouched);
1042 assert_eq!(
1043 market_if_touched.trigger_price(),
1044 Some(Price::from("98.50"))
1045 );
1046 assert_eq!(
1047 market_if_touched.trigger_type(),
1048 Some(TriggerType::LastPrice)
1049 );
1050
1051 assert_eq!(limit_if_touched.order_type(), OrderType::LimitIfTouched);
1052 assert_eq!(limit_if_touched.price(), Some(Price::from("97.50")));
1053 assert_eq!(limit_if_touched.trigger_price(), Some(Price::from("97.00")));
1054 assert!(limit_if_touched.is_post_only());
1055
1056 assert_eq!(
1057 trailing_stop_market.order_type(),
1058 OrderType::TrailingStopMarket
1059 );
1060 assert_eq!(
1061 trailing_stop_market.trailing_offset(),
1062 Some(Decimal::new(25, 2))
1063 );
1064 assert_eq!(
1065 trailing_stop_market.trailing_offset_type(),
1066 Some(TrailingOffsetType::Price)
1067 );
1068 assert_eq!(
1069 trailing_stop_market.activation_price(),
1070 Some(Price::from("105.00"))
1071 );
1072 assert_eq!(
1073 trailing_stop_market.trigger_price(),
1074 Some(Price::from("104.50"))
1075 );
1076
1077 assert_eq!(
1078 trailing_stop_limit.order_type(),
1079 OrderType::TrailingStopLimit
1080 );
1081 assert_eq!(trailing_stop_limit.price(), Some(Price::from("96.00")));
1082 assert_eq!(
1083 trailing_stop_limit.limit_offset(),
1084 Some(Decimal::new(10, 2))
1085 );
1086 assert_eq!(
1087 trailing_stop_limit.trailing_offset(),
1088 Some(Decimal::new(50, 2))
1089 );
1090 assert_eq!(
1091 trailing_stop_limit.activation_price(),
1092 Some(Price::from("97.00"))
1093 );
1094 assert!(trailing_stop_limit.is_post_only());
1095
1096 assert_eq!(order_list.id, list_orders[0].order_list_id().unwrap());
1097 assert_eq!(order_list.id, list_orders[1].order_list_id().unwrap());
1098 assert_eq!(order_list.instrument_id, instrument_id);
1099 assert_eq!(
1100 order_list.client_order_ids,
1101 list_orders
1102 .iter()
1103 .map(Order::client_order_id)
1104 .collect::<Vec<_>>()
1105 );
1106 }
1107
1108 #[rstest]
1109 fn test_strategy_core_order_api_generates_ids() {
1110 let core = registered_test_core();
1111 let (client_order_id, order_list_id) = {
1112 let orders = core.order();
1113 (
1114 orders.generate_client_order_id(),
1115 orders.generate_order_list_id(),
1116 )
1117 };
1118
1119 let next_client_order_id = core.order().generate_client_order_id();
1120
1121 assert_eq!(client_order_id.as_str(), "O-19700101-000000-001-001-1");
1122 assert_eq!(order_list_id.as_str(), "OL-19700101-000000-001-001-1");
1123 assert_eq!(next_client_order_id.as_str(), "O-19700101-000000-001-001-2");
1124 }
1125
1126 #[rstest]
1127 fn test_strategy_core_order_api_creates_bracket_orders() {
1128 let core = registered_test_core();
1129
1130 let orders = core
1131 .order()
1132 .bracket()
1133 .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
1134 .order_side(OrderSide::Buy)
1135 .quantity(Quantity::from("1.0"))
1136 .tp_price(Price::from("110.00"))
1137 .sl_trigger_price(Price::from("90.00"))
1138 .call();
1139 let order_list_id = orders[0].order_list_id();
1140
1141 assert_eq!(orders.len(), 3);
1142 assert_eq!(orders[0].order_type(), OrderType::Market);
1143 assert_eq!(orders[1].order_type(), OrderType::StopMarket);
1144 assert_eq!(orders[2].order_type(), OrderType::Limit);
1145 assert!(order_list_id.is_some());
1146 assert!(
1147 orders
1148 .iter()
1149 .all(|order| order.order_list_id() == order_list_id)
1150 );
1151 }
1152
1153 #[rstest]
1154 fn test_strategy_core_portfolio_api_returns_owned_reads() {
1155 let core = registered_test_core();
1156 let portfolio = core.portfolio_api();
1157 let instrument_id = InstrumentId::from("BTCUSDT.BINANCE");
1158 let venue = instrument_id.venue;
1159 let account_id = AccountId::from("SIM-001");
1160
1161 let is_initialized = portfolio.is_initialized();
1162 let balances_locked = portfolio.balances_locked(&venue);
1163 let initial_margins = portfolio.instrument_initial_margins(&venue);
1164 let maintenance_margins = portfolio.instrument_maintenance_margins(&venue);
1165 let unrealized_pnls = portfolio.unrealized_pnls(&venue, None);
1166 let realized_pnls = portfolio.realized_pnls(&venue, None);
1167 let net_exposures = portfolio.net_exposures(&venue, None);
1168 let unrealized_pnl = portfolio.unrealized_pnl(&instrument_id);
1169 let realized_pnl = portfolio.realized_pnl(&instrument_id);
1170 let total_pnl = portfolio.total_pnl(&instrument_id);
1171 let total_pnls = portfolio.total_pnls(&venue, None);
1172 let mark_values = portfolio.mark_values(&venue, None);
1173 let equity = portfolio.equity(&venue, None);
1174 let net_exposure = portfolio.net_exposure(&instrument_id, None);
1175 let is_flat = portfolio.is_net_flat(&instrument_id);
1176 let net_position = portfolio.net_position(&instrument_id);
1177 let missing_prices = portfolio.missing_price_instruments(&venue);
1178 let snapshots = portfolio.snapshots(&account_id);
1179 let recorded_realized_pnls = portfolio.recorded_realized_pnls();
1180 let built_snapshot = portfolio.build_snapshot(&account_id);
1181
1182 assert!(!is_initialized);
1183 assert!(balances_locked.is_empty());
1184 assert!(initial_margins.is_empty());
1185 assert!(maintenance_margins.is_empty());
1186 assert!(unrealized_pnls.is_some_and(|values| values.is_empty()));
1187 assert!(realized_pnls.is_some_and(|values| values.is_empty()));
1188 assert_eq!(net_exposures, None);
1189 assert_eq!(unrealized_pnl, None);
1190 assert_eq!(realized_pnl, None);
1191 assert_eq!(total_pnl, None);
1192 assert!(total_pnls.is_some_and(|values| values.is_empty()));
1193 assert!(mark_values.is_empty());
1194 assert!(equity.is_empty());
1195 assert_eq!(net_exposure, None);
1196 assert!(is_flat);
1197 assert_eq!(net_position, Decimal::ZERO);
1198 assert!(missing_prices.is_empty());
1199 assert!(snapshots.is_empty());
1200 assert!(recorded_realized_pnls.is_empty());
1201 assert_eq!(built_snapshot, None);
1202 }
1203
1204 #[rstest]
1205 fn test_strategy_core_actor_state_starts_unregistered() {
1206 let config = create_test_config();
1207 let core = StrategyCore::new(config);
1208
1209 assert!(core.trader_id().is_none());
1210 }
1211
1212 #[rstest]
1213 fn test_strategy_core_debug() {
1214 let config = create_test_config();
1215 let core = StrategyCore::new(config);
1216
1217 let debug_str = format!("{core:?}");
1218 assert!(debug_str.contains("StrategyCore"));
1219 }
1220
1221 fn registered_test_core() -> StrategyCore {
1222 let config = create_test_config();
1223 let mut core = StrategyCore::new(config);
1224
1225 let trader_id = TraderId::from("TRADER-001");
1226 let clock = Rc::new(RefCell::new(TestClock::new()));
1227 let cache = Rc::new(RefCell::new(Cache::default()));
1228 let portfolio = Rc::new(RefCell::new(Portfolio::new(
1229 clock.clone(),
1230 cache.clone(),
1231 None,
1232 )));
1233
1234 core.register(trader_id, clock, cache, portfolio).unwrap();
1235 core
1236 }
1237}