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