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