1use std::{cell::RefCell, fmt::Debug, rc::Rc};
17
18use nautilus_common::{
19 actor::{
20 DataActor, DataActorCore, DataActorNative,
21 data_actor::{DataActorConfig, ImportableActorConfig},
22 registry::try_get_actor_unchecked,
23 },
24 component::Component,
25 msgbus::{Endpoint, MStr, TypedHandler, get_message_bus},
26 nautilus_actor,
27};
28use nautilus_model::identifiers::{ActorId, StrategyId};
29use nautilus_trading::{ImportableStrategyConfig, Strategy, StrategyNative};
30
31use crate::{messages::ControllerCommand, trader::Trader};
32
33#[derive(Debug)]
34pub struct Controller {
35 core: DataActorCore,
36 trader: Rc<RefCell<Trader>>,
37}
38
39impl Controller {
40 pub const EXECUTE_ENDPOINT: &str = "Controller.execute";
41
42 #[must_use]
43 pub fn new(trader: Rc<RefCell<Trader>>, config: Option<DataActorConfig>) -> Self {
44 Self {
45 core: DataActorCore::new(config.unwrap_or_default()),
46 trader,
47 }
48 }
49
50 pub fn send(command: &ControllerCommand) -> anyhow::Result<()> {
56 let endpoint = Self::execute_endpoint();
57 let handler = {
58 let msgbus = get_message_bus();
59 msgbus
60 .borrow_mut()
61 .endpoint_map::<ControllerCommand>()
62 .get(endpoint)
63 .cloned()
64 };
65
66 let Some(handler) = handler else {
67 anyhow::bail!(
68 "Controller execute endpoint '{}' not registered",
69 endpoint.as_str()
70 );
71 };
72
73 handler.handle(command);
74 Ok(())
75 }
76
77 pub fn execute(&mut self, command: ControllerCommand) -> anyhow::Result<()> {
83 match command {
84 ControllerCommand::CreateActor(command) => self
85 .create_actor_from_config(&command.actor_config, command.start)
86 .map(|_| ()),
87 ControllerCommand::StartActor(command) => self.start_actor(&command.actor_id),
88 ControllerCommand::StopActor(command) => self.stop_actor(&command.actor_id),
89 ControllerCommand::RemoveActor(command) => self.remove_actor(&command.actor_id),
90 ControllerCommand::CreateStrategy(command) => self
91 .create_strategy_from_config(&command.strategy_config, command.start)
92 .map(|_| ()),
93 ControllerCommand::StartStrategy(command) => self.start_strategy(&command.strategy_id),
94 ControllerCommand::StopStrategy(command) => self.stop_strategy(&command.strategy_id),
95 ControllerCommand::ExitMarket(strategy_id) => self.exit_market(&strategy_id),
96 ControllerCommand::RemoveStrategy(command) => {
97 self.remove_strategy(&command.strategy_id)
98 }
99 }
100 }
101
102 pub fn create_actor<T>(&self, actor: T, start: bool) -> anyhow::Result<ActorId>
108 where
109 T: DataActor + DataActorNative + Component + Debug + 'static,
110 {
111 let actor_id = actor.actor_id();
112 self.trader.borrow_mut().add_actor(actor)?;
113
114 self.start_created_actor(actor_id, start)?;
115
116 Ok(actor_id)
117 }
118
119 pub fn create_actor_from_factory<F, T>(
125 &self,
126 factory: F,
127 start: bool,
128 ) -> anyhow::Result<ActorId>
129 where
130 F: FnOnce() -> anyhow::Result<T>,
131 T: DataActor + DataActorNative + Component + Debug + 'static,
132 {
133 let actor = factory()?;
134 self.create_actor(actor, start)
135 }
136
137 #[cfg(feature = "python")]
143 pub fn create_actor_from_config(
144 &self,
145 actor_config: &ImportableActorConfig,
146 start: bool,
147 ) -> anyhow::Result<ActorId> {
148 let actor_id = self
149 .trader
150 .borrow_mut()
151 .add_actor_from_importable_config(actor_config)?;
152
153 self.start_created_actor(actor_id, start)?;
154
155 Ok(actor_id)
156 }
157
158 #[cfg(not(feature = "python"))]
164 pub fn create_actor_from_config(
165 &self,
166 actor_config: &ImportableActorConfig,
167 _start: bool,
168 ) -> anyhow::Result<ActorId> {
169 Self::unsupported_create_actor_config(actor_config)
170 }
171
172 pub fn create_strategy<T>(&self, mut strategy: T, start: bool) -> anyhow::Result<StrategyId>
178 where
179 T: Strategy + StrategyNative + DataActorNative + Component + Debug + 'static,
180 {
181 let strategy_id = self
182 .trader
183 .borrow()
184 .prepare_strategy_for_registration(&mut strategy)?;
185 self.trader.borrow_mut().add_strategy(strategy)?;
186
187 self.start_created_strategy(strategy_id, start)?;
188
189 Ok(strategy_id)
190 }
191
192 pub fn create_strategy_from_factory<F, T>(
198 &self,
199 factory: F,
200 start: bool,
201 ) -> anyhow::Result<StrategyId>
202 where
203 F: FnOnce() -> anyhow::Result<T>,
204 T: Strategy + StrategyNative + DataActorNative + Component + Debug + 'static,
205 {
206 let strategy = factory()?;
207 self.create_strategy(strategy, start)
208 }
209
210 #[cfg(feature = "python")]
216 pub fn create_strategy_from_config(
217 &self,
218 strategy_config: &ImportableStrategyConfig,
219 start: bool,
220 ) -> anyhow::Result<StrategyId> {
221 let strategy_id = self
222 .trader
223 .borrow_mut()
224 .add_strategy_from_importable_config(strategy_config)?;
225
226 self.start_created_strategy(strategy_id, start)?;
227
228 Ok(strategy_id)
229 }
230
231 #[cfg(not(feature = "python"))]
237 pub fn create_strategy_from_config(
238 &self,
239 strategy_config: &ImportableStrategyConfig,
240 _start: bool,
241 ) -> anyhow::Result<StrategyId> {
242 Self::unsupported_create_strategy_config(strategy_config)
243 }
244
245 pub fn start_actor(&self, actor_id: &ActorId) -> anyhow::Result<()> {
251 self.trader.borrow().start_actor(actor_id)
252 }
253
254 pub fn stop_actor(&self, actor_id: &ActorId) -> anyhow::Result<()> {
260 self.trader.borrow().stop_actor(actor_id)
261 }
262
263 pub fn remove_actor(&self, actor_id: &ActorId) -> anyhow::Result<()> {
269 if actor_id.inner() == self.core.actor_id().inner() {
270 return Ok(());
271 }
272
273 self.trader.borrow_mut().remove_actor(actor_id)
274 }
275
276 pub fn start_strategy(&self, strategy_id: &StrategyId) -> anyhow::Result<()> {
282 self.trader.borrow().start_strategy(strategy_id)
283 }
284
285 pub fn stop_strategy(&self, strategy_id: &StrategyId) -> anyhow::Result<()> {
291 self.trader.borrow_mut().stop_strategy(strategy_id)
292 }
293
294 pub fn exit_market(&self, strategy_id: &StrategyId) -> anyhow::Result<()> {
300 Trader::market_exit_strategy(&self.trader, strategy_id)
301 }
302
303 pub fn remove_strategy(&self, strategy_id: &StrategyId) -> anyhow::Result<()> {
309 self.trader.borrow_mut().remove_strategy(strategy_id)
310 }
311
312 fn start_created_actor(&self, actor_id: ActorId, start: bool) -> anyhow::Result<()> {
313 if !start {
314 return Ok(());
315 }
316
317 if let Err(start_err) = self.start_actor(&actor_id) {
318 return Err(self.rollback_actor_start_failure(actor_id, start_err));
319 }
320
321 Ok(())
322 }
323
324 fn start_created_strategy(&self, strategy_id: StrategyId, start: bool) -> anyhow::Result<()> {
325 if !start {
326 return Ok(());
327 }
328
329 if let Err(start_err) = self.start_strategy(&strategy_id) {
330 return Err(self.rollback_strategy_start_failure(strategy_id, start_err));
331 }
332
333 Ok(())
334 }
335
336 fn rollback_actor_start_failure(
337 &self,
338 actor_id: ActorId,
339 start_err: anyhow::Error,
340 ) -> anyhow::Error {
341 match self.remove_actor(&actor_id) {
342 Ok(()) => start_err,
343 Err(rollback_err) => anyhow::anyhow!(
344 "Failed to start actor {actor_id}: {start_err}; rollback failed: {rollback_err}"
345 ),
346 }
347 }
348
349 fn rollback_strategy_start_failure(
350 &self,
351 strategy_id: StrategyId,
352 start_err: anyhow::Error,
353 ) -> anyhow::Error {
354 match self.remove_strategy(&strategy_id) {
355 Ok(()) => start_err,
356 Err(rollback_err) => anyhow::anyhow!(
357 "Failed to start strategy {strategy_id}: {start_err}; rollback failed: {rollback_err}"
358 ),
359 }
360 }
361
362 fn register_execute_endpoint(&self) {
363 let controller_id = self.core.actor_id().inner();
364 let handler = TypedHandler::from(move |command: &ControllerCommand| {
365 if let Some(mut controller) = try_get_actor_unchecked::<Self>(&controller_id) {
366 if let Err(e) = controller.execute(command.clone()) {
367 log::error!("Controller command failed for {controller_id}: {e}");
368 }
369 } else {
370 log::error!("Controller {controller_id} not found for command handling");
371 }
372 });
373
374 get_message_bus()
375 .borrow_mut()
376 .endpoint_map::<ControllerCommand>()
377 .register(Self::execute_endpoint(), handler);
378 }
379
380 fn deregister_execute_endpoint() {
381 get_message_bus()
382 .borrow_mut()
383 .endpoint_map::<ControllerCommand>()
384 .deregister(Self::execute_endpoint());
385 }
386
387 fn execute_endpoint() -> MStr<Endpoint> {
388 Self::EXECUTE_ENDPOINT.into()
389 }
390
391 #[cfg(not(feature = "python"))]
392 fn unsupported_create_actor_config(
393 actor_config: &ImportableActorConfig,
394 ) -> anyhow::Result<ActorId> {
395 anyhow::bail!(
396 "CreateActor command for importable actor '{}' is not supported by the Rust controller",
397 actor_config.actor_path
398 );
399 }
400
401 #[cfg(not(feature = "python"))]
402 fn unsupported_create_strategy_config(
403 strategy_config: &ImportableStrategyConfig,
404 ) -> anyhow::Result<StrategyId> {
405 anyhow::bail!(
406 "CreateStrategy command for importable strategy '{}' is not supported by the Rust controller",
407 strategy_config.strategy_path
408 );
409 }
410}
411
412impl DataActor for Controller {
413 fn on_start(&mut self) -> anyhow::Result<()> {
414 self.register_execute_endpoint();
415 Ok(())
416 }
417
418 fn on_stop(&mut self) -> anyhow::Result<()> {
419 Self::deregister_execute_endpoint();
420 Ok(())
421 }
422
423 fn on_resume(&mut self) -> anyhow::Result<()> {
424 self.register_execute_endpoint();
425 Ok(())
426 }
427
428 fn on_dispose(&mut self) -> anyhow::Result<()> {
429 Self::deregister_execute_endpoint();
430 Ok(())
431 }
432}
433
434nautilus_actor!(Controller);
435
436#[cfg(test)]
437mod tests {
438 use std::collections::HashMap;
439 #[cfg(feature = "python")]
440 use std::ffi::CString;
441
442 #[cfg(feature = "python")]
443 use nautilus_common::python::actor::{PyDataActor, PyDataActorInner};
444 use nautilus_common::{
445 actor::data_actor::ImportableActorConfig,
446 cache::Cache,
447 clock::TestClock,
448 enums::{ComponentState, Environment},
449 msgbus::{MessageBus, set_message_bus},
450 };
451 use nautilus_core::{UUID4, UnixNanos};
452 use nautilus_model::{identifiers::TraderId, stubs::TestDefault};
453 use nautilus_portfolio::portfolio::Portfolio;
454 #[cfg(feature = "python")]
455 use nautilus_trading::python::strategy::{PyStrategy, PyStrategyInner};
456 use nautilus_trading::{
457 ImportableStrategyConfig, nautilus_strategy,
458 strategy::{StrategyConfig, StrategyCore},
459 };
460 #[cfg(feature = "python")]
461 use pyo3::{
462 prelude::*,
463 types::{PyDict, PyModule},
464 };
465 use rstest::rstest;
466
467 use super::*;
468 use crate::{
469 clock_factory::ClockFactory,
470 messages::{
471 CreateActor, CreateStrategy, RemoveActor, RemoveStrategy, StartActor, StartStrategy,
472 StopActor, StopStrategy,
473 },
474 };
475
476 fn start_actor_command(actor_id: ActorId) -> ControllerCommand {
477 StartActor::new(actor_id, UUID4::new(), UnixNanos::default()).into()
478 }
479
480 fn stop_actor_command(actor_id: ActorId) -> ControllerCommand {
481 StopActor::new(actor_id, UUID4::new(), UnixNanos::default()).into()
482 }
483
484 fn remove_actor_command(actor_id: ActorId) -> ControllerCommand {
485 RemoveActor::new(actor_id, UUID4::new(), UnixNanos::default()).into()
486 }
487
488 fn start_strategy_command(strategy_id: StrategyId) -> ControllerCommand {
489 StartStrategy::new(strategy_id, UUID4::new(), UnixNanos::default()).into()
490 }
491
492 fn stop_strategy_command(strategy_id: StrategyId) -> ControllerCommand {
493 StopStrategy::new(strategy_id, UUID4::new(), UnixNanos::default()).into()
494 }
495
496 fn remove_strategy_command(strategy_id: StrategyId) -> ControllerCommand {
497 RemoveStrategy::new(strategy_id, UUID4::new(), UnixNanos::default()).into()
498 }
499
500 #[derive(Debug)]
501 struct TestDataActor {
502 core: DataActorCore,
503 }
504
505 impl TestDataActor {
506 fn new(config: DataActorConfig) -> Self {
507 Self {
508 core: DataActorCore::new(config),
509 }
510 }
511 }
512
513 impl DataActor for TestDataActor {}
514
515 nautilus_actor!(TestDataActor);
516
517 #[derive(Debug)]
518 struct TestStrategy {
519 core: StrategyCore,
520 }
521
522 impl TestStrategy {
523 fn new(config: StrategyConfig) -> Self {
524 Self {
525 core: StrategyCore::new(config),
526 }
527 }
528 }
529
530 impl DataActor for TestStrategy {}
531
532 nautilus_strategy!(TestStrategy);
533
534 #[derive(Debug)]
535 struct FailingStartActor {
536 core: DataActorCore,
537 }
538
539 impl FailingStartActor {
540 fn new(config: DataActorConfig) -> Self {
541 Self {
542 core: DataActorCore::new(config),
543 }
544 }
545 }
546
547 impl DataActor for FailingStartActor {
548 fn on_start(&mut self) -> anyhow::Result<()> {
549 anyhow::bail!("Simulated actor start failure")
550 }
551 }
552
553 nautilus_actor!(FailingStartActor);
554
555 #[derive(Debug)]
556 struct FailingStartStrategy {
557 core: StrategyCore,
558 }
559
560 impl FailingStartStrategy {
561 fn new(config: StrategyConfig) -> Self {
562 Self {
563 core: StrategyCore::new(config),
564 }
565 }
566 }
567
568 impl DataActor for FailingStartStrategy {
569 fn on_start(&mut self) -> anyhow::Result<()> {
570 anyhow::bail!("Simulated strategy start failure")
571 }
572 }
573
574 nautilus_strategy!(FailingStartStrategy);
575
576 #[derive(Debug)]
577 struct ReentrantExitStrategy {
578 core: StrategyCore,
579 actor_to_stop: ActorId,
580 }
581
582 impl ReentrantExitStrategy {
583 fn new(config: StrategyConfig, actor_to_stop: ActorId) -> Self {
584 Self {
585 core: StrategyCore::new(config),
586 actor_to_stop,
587 }
588 }
589 }
590
591 impl DataActor for ReentrantExitStrategy {}
592
593 nautilus_strategy!(ReentrantExitStrategy, {
594 fn on_market_exit(&mut self) {
595 Controller::send(&stop_actor_command(self.actor_to_stop)).unwrap();
596 }
597 });
598
599 fn create_running_controller() -> (Rc<RefCell<Trader>>, ActorId) {
600 let trader_id = TraderId::test_default();
601 let instance_id = UUID4::new();
602 let clock_factory = ClockFactory::test_default();
603 let clock = clock_factory.clock();
604 let mut clock_ref = clock.borrow_mut();
605 let test_clock = clock_ref
606 .as_any_mut()
607 .downcast_mut::<TestClock>()
608 .expect("test default clock must be TestClock");
609 test_clock.set_time(1_000_000_000u64.into());
610 drop(clock_ref);
611
612 let msgbus = Rc::new(RefCell::new(MessageBus::new(
613 trader_id,
614 instance_id,
615 Some("test".to_string()),
616 None,
617 )));
618 set_message_bus(msgbus);
619
620 let cache = Rc::new(RefCell::new(Cache::new(None, None)));
621 let portfolio = Rc::new(RefCell::new(Portfolio::new(
622 clock.clone(),
623 cache.clone(),
624 None,
625 )));
626
627 let trader = Rc::new(RefCell::new(Trader::new(
628 trader_id,
629 instance_id,
630 Environment::Backtest,
631 clock_factory,
632 cache,
633 portfolio,
634 )));
635 trader.borrow_mut().initialize().unwrap();
636
637 let controller = Controller::new(
638 trader.clone(),
639 Some(DataActorConfig {
640 actor_id: Some(ActorId::from("Controller-001")),
641 ..Default::default()
642 }),
643 );
644 let controller_id = controller.core.actor_id();
645
646 trader.borrow_mut().add_actor(controller).unwrap();
647 trader.borrow_mut().start().unwrap();
648
649 (trader, controller_id)
650 }
651
652 #[cfg(feature = "python")]
653 fn install_controller_importables_module(py: Python<'_>, module_name: &str) {
654 let module = PyModule::new(py, module_name).expect("test module should create");
655 module
656 .setattr("DataActor", py.get_type::<PyDataActor>())
657 .expect("DataActor type should bind");
658 module
659 .setattr("Strategy", py.get_type::<PyStrategy>())
660 .expect("Strategy type should bind");
661 module
662 .setattr("RESULTS", PyDict::new(py))
663 .expect("RESULTS should bind");
664
665 let code = CString::new(
666 r#"
667RESULTS["actor_start"] = 0
668RESULTS["strategy_start"] = 0
669RESULTS["fallback_post_init"] = 0
670RESULTS["fallback_post_init_seen"] = False
671RESULTS["fallback_actor_id"] = ""
672
673class CommandActorConfig:
674 def __init__(self, actor_id=None, log_events=True, log_commands=True):
675 self.actor_id = actor_id
676 self.log_events = log_events
677 self.log_commands = log_commands
678
679class CommandActor(DataActor):
680 def __init__(self, config):
681 super().__init__(config)
682
683 def on_start(self):
684 RESULTS["actor_start"] += 1
685
686class FailingActor(CommandActor):
687 def on_start(self):
688 raise RuntimeError("simulated actor start failure")
689
690class FallbackActorConfig:
691 def __init__(self):
692 self.actor_id = None
693 self.log_events = True
694 self.log_commands = True
695 self.post_init_called = False
696
697 def __post_init__(self):
698 self.post_init_called = True
699 RESULTS["fallback_post_init"] += 1
700
701class FallbackActor(DataActor):
702 def __init__(self, config):
703 super().__init__(config)
704 RESULTS["fallback_post_init_seen"] = config.post_init_called
705 RESULTS["fallback_actor_id"] = str(config.actor_id)
706
707class CommandStrategyConfig:
708 def __init__(self, strategy_id=None, log_events=True, log_commands=True):
709 self.strategy_id = strategy_id
710 self.log_events = log_events
711 self.log_commands = log_commands
712
713class CommandStrategy(Strategy):
714 def __init__(self, config):
715 super().__init__(config)
716
717 def on_start(self):
718 RESULTS["strategy_start"] += 1
719
720class FailingStrategy(CommandStrategy):
721 def on_start(self):
722 raise RuntimeError("simulated strategy start failure")
723"#,
724 )
725 .expect("python test code should be valid CString");
726
727 py.run(code.as_c_str(), Some(&module.dict()), None)
728 .expect("test importables module should load");
729
730 let sys_modules = py
731 .import("sys")
732 .expect("sys should import")
733 .getattr("modules")
734 .expect("sys.modules should exist");
735 sys_modules
736 .set_item(module_name, module)
737 .expect("test module should register");
738 }
739
740 #[rstest]
741 #[cfg(not(feature = "python"))]
742 fn test_controller_rejects_importable_create_commands() {
743 let (trader, controller_id) = create_running_controller();
744 let controller_actor_id = controller_id.inner();
745
746 let mut controller = try_get_actor_unchecked::<Controller>(&controller_actor_id).unwrap();
747 let actor_config = ImportableActorConfig {
748 actor_path: "tests.actors:Actor".to_string(),
749 config_path: "tests.actors:ActorConfig".to_string(),
750 config: HashMap::new(),
751 };
752 let strategy_config = ImportableStrategyConfig {
753 strategy_path: "tests.strategies:Strategy".to_string(),
754 config_path: "tests.strategies:StrategyConfig".to_string(),
755 config: HashMap::new(),
756 };
757
758 let actor_result = controller.execute(
759 CreateActor::new(actor_config, true, UUID4::new(), UnixNanos::default()).into(),
760 );
761 let strategy_result = controller.execute(
762 CreateStrategy::new(strategy_config, true, UUID4::new(), UnixNanos::default()).into(),
763 );
764
765 assert_eq!(
766 actor_result.unwrap_err().to_string(),
767 "CreateActor command for importable actor 'tests.actors:Actor' is not supported by the Rust controller"
768 );
769 assert_eq!(
770 strategy_result.unwrap_err().to_string(),
771 "CreateStrategy command for importable strategy 'tests.strategies:Strategy' is not supported by the Rust controller"
772 );
773
774 drop(controller);
775 trader.borrow_mut().stop().unwrap();
776 trader.borrow_mut().dispose_components().unwrap();
777 }
778
779 #[rstest]
780 #[cfg(feature = "python")]
781 fn test_controller_creates_importable_actor_and_strategy_commands() {
782 Python::initialize();
783
784 let module_name = "test_system_controller_importables";
785 Python::attach(|py| install_controller_importables_module(py, module_name));
786
787 let (trader, controller_id) = create_running_controller();
788 let controller_actor_id = controller_id.inner();
789 let actor_id = ActorId::from("CommandActor-001");
790 let strategy_id = StrategyId::from("CommandStrategy-001");
791
792 {
793 let mut controller = try_get_actor_unchecked::<Controller>(&controller_actor_id)
794 .expect("controller should be registered");
795 let actor_config = ImportableActorConfig {
796 actor_path: format!("{module_name}:CommandActor"),
797 config_path: format!("{module_name}:CommandActorConfig"),
798 config: HashMap::from([(
799 "actor_id".to_string(),
800 serde_json::Value::String("CommandActor-001".to_string()),
801 )]),
802 };
803 let strategy_config = ImportableStrategyConfig {
804 strategy_path: format!("{module_name}:CommandStrategy"),
805 config_path: format!("{module_name}:CommandStrategyConfig"),
806 config: HashMap::from([(
807 "strategy_id".to_string(),
808 serde_json::Value::String("CommandStrategy-001".to_string()),
809 )]),
810 };
811
812 controller
813 .execute(
814 CreateActor::new(actor_config, false, UUID4::new(), UnixNanos::default())
815 .into(),
816 )
817 .unwrap();
818 controller
819 .execute(
820 CreateStrategy::new(strategy_config, true, UUID4::new(), UnixNanos::default())
821 .into(),
822 )
823 .unwrap();
824 }
825
826 assert!(trader.borrow().actor_ids().contains(&actor_id));
827 assert!(trader.borrow().strategy_ids().contains(&strategy_id));
828
829 assert_eq!(
830 try_get_actor_unchecked::<PyDataActorInner>(&actor_id.inner())
831 .unwrap()
832 .state(),
833 ComponentState::Ready
834 );
835 assert_eq!(
836 try_get_actor_unchecked::<PyStrategyInner>(&strategy_id.inner())
837 .unwrap()
838 .state(),
839 ComponentState::Running
840 );
841
842 Python::attach(|py| {
843 let module = py.import(module_name).expect("test module should import");
844 let results_obj = module.getattr("RESULTS").expect("RESULTS should exist");
845 let results = results_obj
846 .cast::<PyDict>()
847 .expect("RESULTS should be a dict");
848 assert_eq!(
849 results
850 .get_item("actor_start")
851 .expect("actor_start lookup should not error")
852 .expect("actor_start should exist")
853 .extract::<usize>()
854 .expect("actor_start should extract"),
855 0
856 );
857 assert_eq!(
858 results
859 .get_item("strategy_start")
860 .expect("strategy_start lookup should not error")
861 .expect("strategy_start should exist")
862 .extract::<usize>()
863 .expect("strategy_start should extract"),
864 1
865 );
866 });
867
868 trader.borrow_mut().remove_actor(&actor_id).unwrap();
869 trader.borrow_mut().stop().unwrap();
870 trader.borrow_mut().dispose_components().unwrap();
871 }
872
873 #[rstest]
874 #[cfg(feature = "python")]
875 fn test_controller_stop_skips_unstarted_importable_components() {
876 Python::initialize();
877
878 let module_name = "test_system_controller_unstarted_components";
879 Python::attach(|py| install_controller_importables_module(py, module_name));
880
881 let (trader, controller_id) = create_running_controller();
882 let controller_actor_id = controller_id.inner();
883 let actor_id = ActorId::from("CommandActor-001");
884 let strategy_id = StrategyId::from("CommandStrategy-001");
885
886 {
887 let mut controller = try_get_actor_unchecked::<Controller>(&controller_actor_id)
888 .expect("controller should be registered");
889 let actor_config = ImportableActorConfig {
890 actor_path: format!("{module_name}:CommandActor"),
891 config_path: format!("{module_name}:CommandActorConfig"),
892 config: HashMap::from([(
893 "actor_id".to_string(),
894 serde_json::Value::String("CommandActor-001".to_string()),
895 )]),
896 };
897 let strategy_config = ImportableStrategyConfig {
898 strategy_path: format!("{module_name}:CommandStrategy"),
899 config_path: format!("{module_name}:CommandStrategyConfig"),
900 config: HashMap::from([(
901 "strategy_id".to_string(),
902 serde_json::Value::String("CommandStrategy-001".to_string()),
903 )]),
904 };
905
906 controller
907 .execute(
908 CreateActor::new(actor_config, false, UUID4::new(), UnixNanos::default())
909 .into(),
910 )
911 .unwrap();
912 controller
913 .execute(
914 CreateStrategy::new(strategy_config, false, UUID4::new(), UnixNanos::default())
915 .into(),
916 )
917 .unwrap();
918 }
919
920 assert_eq!(
921 try_get_actor_unchecked::<PyDataActorInner>(&actor_id.inner())
922 .unwrap()
923 .state(),
924 ComponentState::Ready
925 );
926 assert_eq!(
927 try_get_actor_unchecked::<PyStrategyInner>(&strategy_id.inner())
928 .unwrap()
929 .state(),
930 ComponentState::Ready
931 );
932
933 trader.borrow_mut().stop().unwrap();
934 trader.borrow_mut().dispose_components().unwrap();
935 }
936
937 #[rstest]
938 #[cfg(feature = "python")]
939 fn test_controller_importable_strategy_tag_collision_does_not_register_orphan() {
940 Python::initialize();
941
942 let module_name = "test_system_controller_tag_collision";
943 Python::attach(|py| install_controller_importables_module(py, module_name));
944
945 let (trader, controller_id) = create_running_controller();
946 let controller_actor_id = controller_id.inner();
947 let existing_strategy_id = StrategyId::from("ExistingStrategy-001");
948 let colliding_strategy_id = StrategyId::from("CommandStrategy-001");
949
950 {
951 let controller = try_get_actor_unchecked::<Controller>(&controller_actor_id)
952 .expect("controller should be registered");
953 controller
954 .create_strategy(
955 TestStrategy::new(StrategyConfig {
956 strategy_id: Some(existing_strategy_id),
957 order_id_tag: Some("001".to_string()),
958 ..Default::default()
959 }),
960 false,
961 )
962 .unwrap();
963 }
964
965 let clock_count_before = trader.borrow().get_component_clocks().len();
966
967 let result = {
968 let mut controller = try_get_actor_unchecked::<Controller>(&controller_actor_id)
969 .expect("controller should be registered");
970 let strategy_config = ImportableStrategyConfig {
971 strategy_path: format!("{module_name}:CommandStrategy"),
972 config_path: format!("{module_name}:CommandStrategyConfig"),
973 config: HashMap::from([(
974 "strategy_id".to_string(),
975 serde_json::Value::String("CommandStrategy-001".to_string()),
976 )]),
977 };
978
979 controller.execute(
980 CreateStrategy::new(strategy_config, false, UUID4::new(), UnixNanos::default())
981 .into(),
982 )
983 };
984
985 assert_eq!(
986 result.unwrap_err().to_string(),
987 "Strategy order_id_tag conflict for '001', explicitly define unique order_id_tag values"
988 );
989 assert!(
990 !trader
991 .borrow()
992 .strategy_ids()
993 .contains(&colliding_strategy_id)
994 );
995 assert!(
996 try_get_actor_unchecked::<PyStrategyInner>(&colliding_strategy_id.inner()).is_none()
997 );
998 assert_eq!(
999 trader.borrow().get_component_clocks().len(),
1000 clock_count_before
1001 );
1002
1003 trader
1004 .borrow_mut()
1005 .remove_strategy(&existing_strategy_id)
1006 .unwrap();
1007 trader.borrow_mut().stop().unwrap();
1008 trader.borrow_mut().dispose_components().unwrap();
1009 }
1010
1011 #[rstest]
1012 #[cfg(feature = "python")]
1013 fn test_controller_importable_start_failure_rolls_back_registration() {
1014 Python::initialize();
1015
1016 let module_name = "test_system_controller_start_failure";
1017 Python::attach(|py| install_controller_importables_module(py, module_name));
1018
1019 let (trader, controller_id) = create_running_controller();
1020 let controller_actor_id = controller_id.inner();
1021 let actor_id = ActorId::from("FailingPyActor-001");
1022 let strategy_id = StrategyId::from("FailingPyStrategy-001");
1023 let clock_count_before = trader.borrow().get_component_clocks().len();
1024
1025 let actor_result = {
1026 let mut controller = try_get_actor_unchecked::<Controller>(&controller_actor_id)
1027 .expect("controller should be registered");
1028 let actor_config = ImportableActorConfig {
1029 actor_path: format!("{module_name}:FailingActor"),
1030 config_path: format!("{module_name}:CommandActorConfig"),
1031 config: HashMap::from([(
1032 "actor_id".to_string(),
1033 serde_json::Value::String("FailingPyActor-001".to_string()),
1034 )]),
1035 };
1036
1037 controller.execute(
1038 CreateActor::new(actor_config, true, UUID4::new(), UnixNanos::default()).into(),
1039 )
1040 };
1041
1042 assert!(
1043 actor_result
1044 .unwrap_err()
1045 .to_string()
1046 .contains("simulated actor start failure")
1047 );
1048 assert!(!trader.borrow().actor_ids().contains(&actor_id));
1049 if let Some(actor) = try_get_actor_unchecked::<PyDataActorInner>(&actor_id.inner()) {
1050 assert_eq!(actor.state(), ComponentState::Disposed);
1051 }
1052 assert_eq!(
1053 trader.borrow().get_component_clocks().len(),
1054 clock_count_before
1055 );
1056
1057 let strategy_result = {
1058 let mut controller = try_get_actor_unchecked::<Controller>(&controller_actor_id)
1059 .expect("controller should be registered");
1060 let strategy_config = ImportableStrategyConfig {
1061 strategy_path: format!("{module_name}:FailingStrategy"),
1062 config_path: format!("{module_name}:CommandStrategyConfig"),
1063 config: HashMap::from([(
1064 "strategy_id".to_string(),
1065 serde_json::Value::String("FailingPyStrategy-001".to_string()),
1066 )]),
1067 };
1068
1069 controller.execute(
1070 CreateStrategy::new(strategy_config, true, UUID4::new(), UnixNanos::default())
1071 .into(),
1072 )
1073 };
1074
1075 assert!(
1076 strategy_result
1077 .unwrap_err()
1078 .to_string()
1079 .contains("simulated strategy start failure")
1080 );
1081 assert!(!trader.borrow().strategy_ids().contains(&strategy_id));
1082 if let Some(strategy) = try_get_actor_unchecked::<PyStrategyInner>(&strategy_id.inner()) {
1083 assert_eq!(strategy.state(), ComponentState::Disposed);
1084 }
1085 assert_eq!(
1086 trader.borrow().get_component_clocks().len(),
1087 clock_count_before
1088 );
1089
1090 trader.borrow_mut().stop().unwrap();
1091 trader.borrow_mut().dispose_components().unwrap();
1092 }
1093
1094 #[rstest]
1095 #[cfg(feature = "python")]
1096 fn test_controller_importable_malformed_paths_fail_without_mutation() {
1097 Python::initialize();
1098
1099 let (trader, controller_id) = create_running_controller();
1100 let controller_actor_id = controller_id.inner();
1101 let clock_count_before = trader.borrow().get_component_clocks().len();
1102
1103 let actor_result = {
1104 let mut controller = try_get_actor_unchecked::<Controller>(&controller_actor_id)
1105 .expect("controller should be registered");
1106 let actor_config = ImportableActorConfig {
1107 actor_path: "no_colon_here".to_string(),
1108 config_path: String::new(),
1109 config: HashMap::new(),
1110 };
1111
1112 controller.execute(
1113 CreateActor::new(actor_config, false, UUID4::new(), UnixNanos::default()).into(),
1114 )
1115 };
1116 let strategy_result = {
1117 let mut controller = try_get_actor_unchecked::<Controller>(&controller_actor_id)
1118 .expect("controller should be registered");
1119 let strategy_config = ImportableStrategyConfig {
1120 strategy_path: "module:Class:Extra".to_string(),
1121 config_path: String::new(),
1122 config: HashMap::new(),
1123 };
1124
1125 controller.execute(
1126 CreateStrategy::new(strategy_config, false, UUID4::new(), UnixNanos::default())
1127 .into(),
1128 )
1129 };
1130
1131 assert_eq!(
1132 actor_result.unwrap_err().to_string(),
1133 "actor_path must be in format 'module.path:ClassName'"
1134 );
1135 assert_eq!(
1136 strategy_result.unwrap_err().to_string(),
1137 "strategy_path must be in format 'module.path:ClassName'"
1138 );
1139 assert_eq!(trader.borrow().actor_ids(), vec![controller_id]);
1140 assert!(trader.borrow().strategy_ids().is_empty());
1141 assert_eq!(
1142 trader.borrow().get_component_clocks().len(),
1143 clock_count_before
1144 );
1145
1146 trader.borrow_mut().stop().unwrap();
1147 trader.borrow_mut().dispose_components().unwrap();
1148 }
1149
1150 #[rstest]
1151 #[cfg(feature = "python")]
1152 fn test_controller_importable_config_fallback_registers_actor() {
1153 Python::initialize();
1154
1155 let module_name = "test_system_controller_config_fallback";
1156 Python::attach(|py| install_controller_importables_module(py, module_name));
1157
1158 let (trader, controller_id) = create_running_controller();
1159 let controller_actor_id = controller_id.inner();
1160 let actor_id = ActorId::from("FallbackActor-001");
1161
1162 {
1163 let mut controller = try_get_actor_unchecked::<Controller>(&controller_actor_id)
1164 .expect("controller should be registered");
1165 let actor_config = ImportableActorConfig {
1166 actor_path: format!("{module_name}:FallbackActor"),
1167 config_path: format!("{module_name}:FallbackActorConfig"),
1168 config: HashMap::from([(
1169 "actor_id".to_string(),
1170 serde_json::Value::String("FallbackActor-001".to_string()),
1171 )]),
1172 };
1173
1174 controller
1175 .execute(
1176 CreateActor::new(actor_config, false, UUID4::new(), UnixNanos::default())
1177 .into(),
1178 )
1179 .unwrap();
1180 }
1181
1182 assert!(trader.borrow().actor_ids().contains(&actor_id));
1183 assert_eq!(
1184 try_get_actor_unchecked::<PyDataActorInner>(&actor_id.inner())
1185 .unwrap()
1186 .state(),
1187 ComponentState::Ready
1188 );
1189
1190 Python::attach(|py| {
1191 let module = py.import(module_name).expect("test module should import");
1192 let results_obj = module.getattr("RESULTS").expect("RESULTS should exist");
1193 let results = results_obj
1194 .cast::<PyDict>()
1195 .expect("RESULTS should be a dict");
1196 assert_eq!(
1197 results
1198 .get_item("fallback_post_init")
1199 .expect("fallback_post_init lookup should not error")
1200 .expect("fallback_post_init should exist")
1201 .extract::<usize>()
1202 .expect("fallback_post_init should extract"),
1203 1
1204 );
1205 assert!(
1206 results
1207 .get_item("fallback_post_init_seen")
1208 .expect("fallback_post_init_seen lookup should not error")
1209 .expect("fallback_post_init_seen should exist")
1210 .extract::<bool>()
1211 .expect("fallback_post_init_seen should extract")
1212 );
1213 assert_eq!(
1214 results
1215 .get_item("fallback_actor_id")
1216 .expect("fallback_actor_id lookup should not error")
1217 .expect("fallback_actor_id should exist")
1218 .extract::<String>()
1219 .expect("fallback_actor_id should extract"),
1220 "FallbackActor-001"
1221 );
1222 });
1223
1224 trader.borrow_mut().remove_actor(&actor_id).unwrap();
1225 trader.borrow_mut().stop().unwrap();
1226 trader.borrow_mut().dispose_components().unwrap();
1227 }
1228
1229 #[rstest]
1230 fn test_controller_manages_actor_lifecycle_by_message() {
1231 let (trader, controller_id) = create_running_controller();
1232 let controller_actor_id = controller_id.inner();
1233
1234 let actor_id = {
1235 let controller = try_get_actor_unchecked::<Controller>(&controller_actor_id).unwrap();
1236 controller
1237 .create_actor(
1238 TestDataActor::new(DataActorConfig {
1239 actor_id: Some(ActorId::from("TestActor-001")),
1240 ..Default::default()
1241 }),
1242 false,
1243 )
1244 .unwrap()
1245 };
1246
1247 assert!(trader.borrow().actor_ids().contains(&actor_id));
1248
1249 Controller::send(&start_actor_command(actor_id)).unwrap();
1250 let actor_registry_id = actor_id.inner();
1251 assert_eq!(
1252 try_get_actor_unchecked::<TestDataActor>(&actor_registry_id)
1253 .unwrap()
1254 .state(),
1255 ComponentState::Running
1256 );
1257
1258 Controller::send(&stop_actor_command(actor_id)).unwrap();
1259 assert_eq!(
1260 try_get_actor_unchecked::<TestDataActor>(&actor_registry_id)
1261 .unwrap()
1262 .state(),
1263 ComponentState::Stopped
1264 );
1265
1266 Controller::send(&remove_actor_command(actor_id)).unwrap();
1267 assert!(!trader.borrow().actor_ids().contains(&actor_id));
1268
1269 trader.borrow_mut().stop().unwrap();
1270 trader.borrow_mut().dispose_components().unwrap();
1271 }
1272
1273 #[rstest]
1274 fn test_controller_manages_strategy_lifecycle_and_exit_market() {
1275 let (trader, controller_id) = create_running_controller();
1276 let controller_actor_id = controller_id.inner();
1277
1278 let strategy_id = {
1279 let controller = try_get_actor_unchecked::<Controller>(&controller_actor_id).unwrap();
1280 controller
1281 .create_strategy(
1282 TestStrategy::new(StrategyConfig {
1283 strategy_id: Some(StrategyId::from("TestStrategy-001")),
1284 order_id_tag: Some("001".to_string()),
1285 ..Default::default()
1286 }),
1287 false,
1288 )
1289 .unwrap()
1290 };
1291
1292 assert!(trader.borrow().strategy_ids().contains(&strategy_id));
1293
1294 Controller::send(&start_strategy_command(strategy_id)).unwrap();
1295 let strategy_registry_id = strategy_id.inner();
1296 assert_eq!(
1297 try_get_actor_unchecked::<TestStrategy>(&strategy_registry_id)
1298 .unwrap()
1299 .state(),
1300 ComponentState::Running
1301 );
1302
1303 Controller::send(&ControllerCommand::ExitMarket(strategy_id)).unwrap();
1304 assert!(
1305 try_get_actor_unchecked::<TestStrategy>(&strategy_registry_id)
1306 .unwrap()
1307 .is_exiting()
1308 );
1309
1310 Controller::send(&stop_strategy_command(strategy_id)).unwrap();
1311 let strategy = try_get_actor_unchecked::<TestStrategy>(&strategy_registry_id).unwrap();
1312 assert_eq!(strategy.state(), ComponentState::Stopped);
1313 assert!(!strategy.is_exiting());
1314 drop(strategy);
1315
1316 Controller::send(&remove_strategy_command(strategy_id)).unwrap();
1317 assert!(!trader.borrow().strategy_ids().contains(&strategy_id));
1318
1319 trader.borrow_mut().stop().unwrap();
1320 trader.borrow_mut().dispose_components().unwrap();
1321 }
1322
1323 #[rstest]
1324 fn test_controller_create_actor_rolls_back_on_start_failure() {
1325 let (trader, controller_id) = create_running_controller();
1326 let controller_actor_id = controller_id.inner();
1327 let actor_id = ActorId::from("FailingActor-001");
1328
1329 let result = {
1330 let controller = try_get_actor_unchecked::<Controller>(&controller_actor_id).unwrap();
1331 controller.create_actor(
1332 FailingStartActor::new(DataActorConfig {
1333 actor_id: Some(actor_id),
1334 ..Default::default()
1335 }),
1336 true,
1337 )
1338 };
1339
1340 assert!(result.is_err());
1341 assert!(
1342 result
1343 .unwrap_err()
1344 .to_string()
1345 .contains("Simulated actor start failure")
1346 );
1347 assert!(!trader.borrow().actor_ids().contains(&actor_id));
1348 if let Some(actor) = try_get_actor_unchecked::<FailingStartActor>(&actor_id.inner()) {
1349 assert_eq!(actor.state(), ComponentState::Disposed);
1350 }
1351
1352 trader.borrow_mut().stop().unwrap();
1353 trader.borrow_mut().dispose_components().unwrap();
1354 }
1355
1356 #[rstest]
1357 fn test_controller_create_strategy_rolls_back_on_start_failure() {
1358 let (trader, controller_id) = create_running_controller();
1359 let controller_actor_id = controller_id.inner();
1360 let strategy_id = StrategyId::from("FailingStrategy-001");
1361
1362 let result = {
1363 let controller = try_get_actor_unchecked::<Controller>(&controller_actor_id).unwrap();
1364 controller.create_strategy(
1365 FailingStartStrategy::new(StrategyConfig {
1366 strategy_id: Some(strategy_id),
1367 order_id_tag: Some("001".to_string()),
1368 ..Default::default()
1369 }),
1370 true,
1371 )
1372 };
1373
1374 assert!(result.is_err());
1375 assert!(
1376 result
1377 .unwrap_err()
1378 .to_string()
1379 .contains("Simulated strategy start failure")
1380 );
1381 assert!(!trader.borrow().strategy_ids().contains(&strategy_id));
1382
1383 if let Some(strategy) =
1384 try_get_actor_unchecked::<FailingStartStrategy>(&strategy_id.inner())
1385 {
1386 assert_eq!(strategy.state(), ComponentState::Disposed);
1387 }
1388
1389 trader.borrow_mut().stop().unwrap();
1390 trader.borrow_mut().dispose_components().unwrap();
1391 }
1392
1393 #[rstest]
1394 fn test_controller_exit_market_allows_reentrant_controller_commands() {
1395 let (trader, controller_id) = create_running_controller();
1396 let controller_actor_id = controller_id.inner();
1397
1398 let helper_actor_id = {
1399 let controller = try_get_actor_unchecked::<Controller>(&controller_actor_id).unwrap();
1400 controller
1401 .create_actor(
1402 TestDataActor::new(DataActorConfig {
1403 actor_id: Some(ActorId::from("HelperActor-001")),
1404 ..Default::default()
1405 }),
1406 true,
1407 )
1408 .unwrap()
1409 };
1410
1411 let strategy_id = {
1412 let controller = try_get_actor_unchecked::<Controller>(&controller_actor_id).unwrap();
1413 controller
1414 .create_strategy(
1415 ReentrantExitStrategy::new(
1416 StrategyConfig {
1417 strategy_id: Some(StrategyId::from("ReentrantStrategy-001")),
1418 order_id_tag: Some("001".to_string()),
1419 ..Default::default()
1420 },
1421 helper_actor_id,
1422 ),
1423 false,
1424 )
1425 .unwrap()
1426 };
1427
1428 Controller::send(&start_strategy_command(strategy_id)).unwrap();
1429 Controller::send(&ControllerCommand::ExitMarket(strategy_id)).unwrap();
1430
1431 let helper_actor =
1432 try_get_actor_unchecked::<TestDataActor>(&helper_actor_id.inner()).unwrap();
1433 assert_eq!(helper_actor.state(), ComponentState::Stopped);
1434 drop(helper_actor);
1435 assert!(
1436 try_get_actor_unchecked::<ReentrantExitStrategy>(&strategy_id.inner())
1437 .unwrap()
1438 .is_exiting()
1439 );
1440
1441 Controller::send(&stop_strategy_command(strategy_id)).unwrap();
1442 Controller::send(&remove_strategy_command(strategy_id)).unwrap();
1443 Controller::send(&remove_actor_command(helper_actor_id)).unwrap();
1444 trader.borrow_mut().stop().unwrap();
1445 trader.borrow_mut().dispose_components().unwrap();
1446 }
1447
1448 #[rstest]
1449 fn test_controller_send_fails_after_controller_stop() {
1450 let (trader, _) = create_running_controller();
1451
1452 trader.borrow_mut().stop().unwrap();
1453
1454 let result = Controller::send(&stop_actor_command(ActorId::from("AnyActor-001")));
1455 assert!(result.is_err());
1456 assert_eq!(
1457 result.unwrap_err().to_string(),
1458 "Controller execute endpoint 'Controller.execute' not registered"
1459 );
1460
1461 trader.borrow_mut().dispose_components().unwrap();
1462 }
1463}