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