1use std::{cell::RefCell, fmt::Debug, rc::Rc};
17
18use nautilus_common::{
19 actor::{
20 DataActor, DataActorCore, data_actor::DataActorConfig, registry::try_get_actor_unchecked,
21 },
22 component::Component,
23 msgbus::{Endpoint, MStr, TypedHandler, get_message_bus},
24 nautilus_actor,
25};
26use nautilus_model::identifiers::{ActorId, StrategyId};
27use nautilus_trading::Strategy;
28
29use crate::{messages::ControllerCommand, trader::Trader};
30
31#[derive(Debug)]
32pub struct Controller {
33 core: DataActorCore,
34 trader: Rc<RefCell<Trader>>,
35}
36
37impl Controller {
38 pub const EXECUTE_ENDPOINT: &str = "Controller.execute";
39
40 #[must_use]
41 pub fn new(trader: Rc<RefCell<Trader>>, config: Option<DataActorConfig>) -> Self {
42 Self {
43 core: DataActorCore::new(config.unwrap_or_default()),
44 trader,
45 }
46 }
47
48 pub fn send(command: &ControllerCommand) -> anyhow::Result<()> {
54 let endpoint = Self::execute_endpoint();
55 let handler = {
56 let msgbus = get_message_bus();
57 msgbus
58 .borrow_mut()
59 .endpoint_map::<ControllerCommand>()
60 .get(endpoint)
61 .cloned()
62 };
63
64 let Some(handler) = handler else {
65 anyhow::bail!(
66 "Controller execute endpoint '{}' not registered",
67 endpoint.as_str()
68 );
69 };
70
71 handler.handle(command);
72 Ok(())
73 }
74
75 pub fn execute(&mut self, command: ControllerCommand) -> anyhow::Result<()> {
81 match command {
82 ControllerCommand::CreateActor(command) => {
83 Self::unsupported_create_actor_command(&command)
84 }
85 ControllerCommand::StartActor(command) => self.start_actor(&command.actor_id),
86 ControllerCommand::StopActor(command) => self.stop_actor(&command.actor_id),
87 ControllerCommand::RemoveActor(command) => self.remove_actor(&command.actor_id),
88 ControllerCommand::CreateStrategy(command) => {
89 Self::unsupported_create_strategy_command(&command)
90 }
91 ControllerCommand::StartStrategy(command) => self.start_strategy(&command.strategy_id),
92 ControllerCommand::StopStrategy(command) => self.stop_strategy(&command.strategy_id),
93 ControllerCommand::ExitMarket(strategy_id) => self.exit_market(&strategy_id),
94 ControllerCommand::RemoveStrategy(command) => {
95 self.remove_strategy(&command.strategy_id)
96 }
97 }
98 }
99
100 pub fn create_actor<T>(&self, actor: T, start: bool) -> anyhow::Result<ActorId>
106 where
107 T: DataActor + Component + Debug + 'static,
108 {
109 let actor_id = actor.actor_id();
110 self.trader.borrow_mut().add_actor(actor)?;
111
112 self.start_created_actor(&actor_id, start)?;
113
114 Ok(actor_id)
115 }
116
117 pub fn create_actor_from_factory<F, T>(
123 &self,
124 factory: F,
125 start: bool,
126 ) -> anyhow::Result<ActorId>
127 where
128 F: FnOnce() -> anyhow::Result<T>,
129 T: DataActor + Component + Debug + 'static,
130 {
131 let actor = factory()?;
132 self.create_actor(actor, start)
133 }
134
135 pub fn create_strategy<T>(&self, mut strategy: T, start: bool) -> anyhow::Result<StrategyId>
141 where
142 T: Strategy + Component + Debug + 'static,
143 {
144 let strategy_id = self
145 .trader
146 .borrow()
147 .prepare_strategy_for_registration(&mut strategy)?;
148 self.trader.borrow_mut().add_strategy(strategy)?;
149
150 self.start_created_strategy(&strategy_id, start)?;
151
152 Ok(strategy_id)
153 }
154
155 pub fn create_strategy_from_factory<F, T>(
161 &self,
162 factory: F,
163 start: bool,
164 ) -> anyhow::Result<StrategyId>
165 where
166 F: FnOnce() -> anyhow::Result<T>,
167 T: Strategy + Component + Debug + 'static,
168 {
169 let strategy = factory()?;
170 self.create_strategy(strategy, start)
171 }
172
173 pub fn start_actor(&self, actor_id: &ActorId) -> anyhow::Result<()> {
179 self.trader.borrow().start_actor(actor_id)
180 }
181
182 pub fn stop_actor(&self, actor_id: &ActorId) -> anyhow::Result<()> {
188 self.trader.borrow().stop_actor(actor_id)
189 }
190
191 pub fn remove_actor(&self, actor_id: &ActorId) -> anyhow::Result<()> {
197 if actor_id.inner() == self.actor_id().inner() {
198 return Ok(());
199 }
200
201 self.trader.borrow_mut().remove_actor(actor_id)
202 }
203
204 pub fn start_strategy(&self, strategy_id: &StrategyId) -> anyhow::Result<()> {
210 self.trader.borrow().start_strategy(strategy_id)
211 }
212
213 pub fn stop_strategy(&self, strategy_id: &StrategyId) -> anyhow::Result<()> {
219 self.trader.borrow_mut().stop_strategy(strategy_id)
220 }
221
222 pub fn exit_market(&self, strategy_id: &StrategyId) -> anyhow::Result<()> {
228 Trader::market_exit_strategy(&self.trader, strategy_id)
229 }
230
231 pub fn remove_strategy(&self, strategy_id: &StrategyId) -> anyhow::Result<()> {
237 self.trader.borrow_mut().remove_strategy(strategy_id)
238 }
239
240 fn start_created_actor(&self, actor_id: &ActorId, start: bool) -> anyhow::Result<()> {
241 if !start {
242 return Ok(());
243 }
244
245 if let Err(start_err) = self.start_actor(actor_id) {
246 return Err(self.rollback_actor_start_failure(actor_id, start_err));
247 }
248
249 Ok(())
250 }
251
252 fn start_created_strategy(&self, strategy_id: &StrategyId, start: bool) -> anyhow::Result<()> {
253 if !start {
254 return Ok(());
255 }
256
257 if let Err(start_err) = self.start_strategy(strategy_id) {
258 return Err(self.rollback_strategy_start_failure(strategy_id, start_err));
259 }
260
261 Ok(())
262 }
263
264 fn rollback_actor_start_failure(
265 &self,
266 actor_id: &ActorId,
267 start_err: anyhow::Error,
268 ) -> anyhow::Error {
269 match self.remove_actor(actor_id) {
270 Ok(()) => start_err,
271 Err(rollback_err) => anyhow::anyhow!(
272 "Failed to start actor {actor_id}: {start_err}; rollback failed: {rollback_err}"
273 ),
274 }
275 }
276
277 fn rollback_strategy_start_failure(
278 &self,
279 strategy_id: &StrategyId,
280 start_err: anyhow::Error,
281 ) -> anyhow::Error {
282 match self.remove_strategy(strategy_id) {
283 Ok(()) => start_err,
284 Err(rollback_err) => anyhow::anyhow!(
285 "Failed to start strategy {strategy_id}: {start_err}; rollback failed: {rollback_err}"
286 ),
287 }
288 }
289
290 fn register_execute_endpoint(&self) {
291 let controller_id = self.actor_id().inner();
292 let handler = TypedHandler::from(move |command: &ControllerCommand| {
293 if let Some(mut controller) = try_get_actor_unchecked::<Self>(&controller_id) {
294 if let Err(e) = controller.execute(command.clone()) {
295 log::error!("Controller command failed for {controller_id}: {e}");
296 }
297 } else {
298 log::error!("Controller {controller_id} not found for command handling");
299 }
300 });
301
302 get_message_bus()
303 .borrow_mut()
304 .endpoint_map::<ControllerCommand>()
305 .register(Self::execute_endpoint(), handler);
306 }
307
308 fn deregister_execute_endpoint(&self) {
309 get_message_bus()
310 .borrow_mut()
311 .endpoint_map::<ControllerCommand>()
312 .deregister(Self::execute_endpoint());
313 }
314
315 fn execute_endpoint() -> MStr<Endpoint> {
316 Self::EXECUTE_ENDPOINT.into()
317 }
318
319 fn unsupported_create_actor_command(
320 command: &crate::messages::CreateActor,
321 ) -> anyhow::Result<()> {
322 anyhow::bail!(
323 "CreateActor command for importable actor '{}' is not supported by the Rust controller",
324 command.actor_config.actor_path
325 );
326 }
327
328 fn unsupported_create_strategy_command(
329 command: &crate::messages::CreateStrategy,
330 ) -> anyhow::Result<()> {
331 anyhow::bail!(
332 "CreateStrategy command for importable strategy '{}' is not supported by the Rust controller",
333 command.strategy_config.strategy_path
334 );
335 }
336}
337
338impl DataActor for Controller {
339 fn on_start(&mut self) -> anyhow::Result<()> {
340 self.register_execute_endpoint();
341 Ok(())
342 }
343
344 fn on_stop(&mut self) -> anyhow::Result<()> {
345 self.deregister_execute_endpoint();
346 Ok(())
347 }
348
349 fn on_resume(&mut self) -> anyhow::Result<()> {
350 self.register_execute_endpoint();
351 Ok(())
352 }
353
354 fn on_dispose(&mut self) -> anyhow::Result<()> {
355 self.deregister_execute_endpoint();
356 Ok(())
357 }
358}
359
360nautilus_actor!(Controller);
361
362#[cfg(test)]
363mod tests {
364 use std::collections::HashMap;
365
366 use nautilus_common::{
367 actor::data_actor::ImportableActorConfig,
368 cache::Cache,
369 clock::{Clock, TestClock},
370 enums::{ComponentState, Environment},
371 msgbus::{MessageBus, set_message_bus},
372 };
373 use nautilus_core::{UUID4, UnixNanos};
374 use nautilus_model::{identifiers::TraderId, stubs::TestDefault};
375 use nautilus_portfolio::portfolio::Portfolio;
376 use nautilus_trading::{
377 ImportableStrategyConfig, nautilus_strategy,
378 strategy::{StrategyConfig, StrategyCore},
379 };
380 use rstest::rstest;
381
382 use super::*;
383 use crate::messages::{
384 CreateActor, CreateStrategy, RemoveActor, RemoveStrategy, StartActor, StartStrategy,
385 StopActor, StopStrategy,
386 };
387
388 fn start_actor_command(actor_id: ActorId) -> ControllerCommand {
389 StartActor::new(actor_id, UUID4::new(), UnixNanos::default()).into()
390 }
391
392 fn stop_actor_command(actor_id: ActorId) -> ControllerCommand {
393 StopActor::new(actor_id, UUID4::new(), UnixNanos::default()).into()
394 }
395
396 fn remove_actor_command(actor_id: ActorId) -> ControllerCommand {
397 RemoveActor::new(actor_id, UUID4::new(), UnixNanos::default()).into()
398 }
399
400 fn start_strategy_command(strategy_id: StrategyId) -> ControllerCommand {
401 StartStrategy::new(strategy_id, UUID4::new(), UnixNanos::default()).into()
402 }
403
404 fn stop_strategy_command(strategy_id: StrategyId) -> ControllerCommand {
405 StopStrategy::new(strategy_id, UUID4::new(), UnixNanos::default()).into()
406 }
407
408 fn remove_strategy_command(strategy_id: StrategyId) -> ControllerCommand {
409 RemoveStrategy::new(strategy_id, UUID4::new(), UnixNanos::default()).into()
410 }
411
412 #[derive(Debug)]
413 struct TestDataActor {
414 core: DataActorCore,
415 }
416
417 impl TestDataActor {
418 fn new(config: DataActorConfig) -> Self {
419 Self {
420 core: DataActorCore::new(config),
421 }
422 }
423 }
424
425 impl DataActor for TestDataActor {}
426
427 nautilus_actor!(TestDataActor);
428
429 #[derive(Debug)]
430 struct TestStrategy {
431 core: StrategyCore,
432 }
433
434 impl TestStrategy {
435 fn new(config: StrategyConfig) -> Self {
436 Self {
437 core: StrategyCore::new(config),
438 }
439 }
440 }
441
442 impl DataActor for TestStrategy {}
443
444 nautilus_strategy!(TestStrategy);
445
446 #[derive(Debug)]
447 struct FailingStartActor {
448 core: DataActorCore,
449 }
450
451 impl FailingStartActor {
452 fn new(config: DataActorConfig) -> Self {
453 Self {
454 core: DataActorCore::new(config),
455 }
456 }
457 }
458
459 impl DataActor for FailingStartActor {
460 fn on_start(&mut self) -> anyhow::Result<()> {
461 anyhow::bail!("Simulated actor start failure")
462 }
463 }
464
465 nautilus_actor!(FailingStartActor);
466
467 #[derive(Debug)]
468 struct FailingStartStrategy {
469 core: StrategyCore,
470 }
471
472 impl FailingStartStrategy {
473 fn new(config: StrategyConfig) -> Self {
474 Self {
475 core: StrategyCore::new(config),
476 }
477 }
478 }
479
480 impl DataActor for FailingStartStrategy {
481 fn on_start(&mut self) -> anyhow::Result<()> {
482 anyhow::bail!("Simulated strategy start failure")
483 }
484 }
485
486 nautilus_strategy!(FailingStartStrategy);
487
488 #[derive(Debug)]
489 struct ReentrantExitStrategy {
490 core: StrategyCore,
491 actor_to_stop: ActorId,
492 }
493
494 impl ReentrantExitStrategy {
495 fn new(config: StrategyConfig, actor_to_stop: ActorId) -> Self {
496 Self {
497 core: StrategyCore::new(config),
498 actor_to_stop,
499 }
500 }
501 }
502
503 impl DataActor for ReentrantExitStrategy {}
504
505 nautilus_strategy!(ReentrantExitStrategy, {
506 fn on_market_exit(&mut self) {
507 Controller::send(&stop_actor_command(self.actor_to_stop)).unwrap();
508 }
509 });
510
511 fn create_running_controller() -> (Rc<RefCell<Trader>>, ActorId) {
512 let trader_id = TraderId::test_default();
513 let instance_id = UUID4::new();
514 let clock = Rc::new(RefCell::new(TestClock::new()));
515 clock.borrow_mut().set_time(1_000_000_000u64.into());
516
517 let msgbus = Rc::new(RefCell::new(MessageBus::new(
518 trader_id,
519 instance_id,
520 Some("test".to_string()),
521 None,
522 )));
523 set_message_bus(msgbus);
524
525 let cache = Rc::new(RefCell::new(Cache::new(None, None)));
526 let portfolio = Rc::new(RefCell::new(Portfolio::new(
527 cache.clone(),
528 clock.clone() as Rc<RefCell<dyn Clock>>,
529 None,
530 )));
531
532 let trader = Rc::new(RefCell::new(Trader::new(
533 trader_id,
534 instance_id,
535 Environment::Backtest,
536 clock as Rc<RefCell<dyn Clock>>,
537 cache,
538 portfolio,
539 )));
540 trader.borrow_mut().initialize().unwrap();
541
542 let controller = Controller::new(
543 trader.clone(),
544 Some(DataActorConfig {
545 actor_id: Some(ActorId::from("Controller-001")),
546 ..Default::default()
547 }),
548 );
549 let controller_id = controller.actor_id();
550
551 trader.borrow_mut().add_actor(controller).unwrap();
552 trader.borrow_mut().start().unwrap();
553
554 (trader, controller_id)
555 }
556
557 #[rstest]
558 fn test_controller_rejects_importable_create_commands() {
559 let (trader, controller_id) = create_running_controller();
560 let controller_actor_id = controller_id.inner();
561
562 let mut controller = try_get_actor_unchecked::<Controller>(&controller_actor_id).unwrap();
563 let actor_config = ImportableActorConfig {
564 actor_path: "tests.actors:Actor".to_string(),
565 config_path: "tests.actors:ActorConfig".to_string(),
566 config: HashMap::new(),
567 };
568 let strategy_config = ImportableStrategyConfig {
569 strategy_path: "tests.strategies:Strategy".to_string(),
570 config_path: "tests.strategies:StrategyConfig".to_string(),
571 config: HashMap::new(),
572 };
573
574 let actor_result = controller.execute(
575 CreateActor::new(actor_config, true, UUID4::new(), UnixNanos::default()).into(),
576 );
577 let strategy_result = controller.execute(
578 CreateStrategy::new(strategy_config, true, UUID4::new(), UnixNanos::default()).into(),
579 );
580
581 assert_eq!(
582 actor_result.unwrap_err().to_string(),
583 "CreateActor command for importable actor 'tests.actors:Actor' is not supported by the Rust controller"
584 );
585 assert_eq!(
586 strategy_result.unwrap_err().to_string(),
587 "CreateStrategy command for importable strategy 'tests.strategies:Strategy' is not supported by the Rust controller"
588 );
589
590 drop(controller);
591 trader.borrow_mut().stop().unwrap();
592 trader.borrow_mut().dispose_components().unwrap();
593 }
594
595 #[rstest]
596 fn test_controller_manages_actor_lifecycle_by_message() {
597 let (trader, controller_id) = create_running_controller();
598 let controller_actor_id = controller_id.inner();
599
600 let actor_id = {
601 let controller = try_get_actor_unchecked::<Controller>(&controller_actor_id).unwrap();
602 controller
603 .create_actor(
604 TestDataActor::new(DataActorConfig {
605 actor_id: Some(ActorId::from("TestActor-001")),
606 ..Default::default()
607 }),
608 false,
609 )
610 .unwrap()
611 };
612
613 assert!(trader.borrow().actor_ids().contains(&actor_id));
614
615 Controller::send(&start_actor_command(actor_id)).unwrap();
616 let actor_registry_id = actor_id.inner();
617 assert_eq!(
618 try_get_actor_unchecked::<TestDataActor>(&actor_registry_id)
619 .unwrap()
620 .state(),
621 ComponentState::Running
622 );
623
624 Controller::send(&stop_actor_command(actor_id)).unwrap();
625 assert_eq!(
626 try_get_actor_unchecked::<TestDataActor>(&actor_registry_id)
627 .unwrap()
628 .state(),
629 ComponentState::Stopped
630 );
631
632 Controller::send(&remove_actor_command(actor_id)).unwrap();
633 assert!(!trader.borrow().actor_ids().contains(&actor_id));
634
635 trader.borrow_mut().stop().unwrap();
636 trader.borrow_mut().dispose_components().unwrap();
637 }
638
639 #[rstest]
640 fn test_controller_manages_strategy_lifecycle_and_exit_market() {
641 let (trader, controller_id) = create_running_controller();
642 let controller_actor_id = controller_id.inner();
643
644 let strategy_id = {
645 let controller = try_get_actor_unchecked::<Controller>(&controller_actor_id).unwrap();
646 controller
647 .create_strategy(
648 TestStrategy::new(StrategyConfig {
649 strategy_id: Some(StrategyId::from("TestStrategy-001")),
650 order_id_tag: Some("001".to_string()),
651 ..Default::default()
652 }),
653 false,
654 )
655 .unwrap()
656 };
657
658 assert!(trader.borrow().strategy_ids().contains(&strategy_id));
659
660 Controller::send(&start_strategy_command(strategy_id)).unwrap();
661 let strategy_registry_id = strategy_id.inner();
662 assert_eq!(
663 try_get_actor_unchecked::<TestStrategy>(&strategy_registry_id)
664 .unwrap()
665 .state(),
666 ComponentState::Running
667 );
668
669 Controller::send(&ControllerCommand::ExitMarket(strategy_id)).unwrap();
670 assert!(
671 try_get_actor_unchecked::<TestStrategy>(&strategy_registry_id)
672 .unwrap()
673 .is_exiting()
674 );
675
676 Controller::send(&stop_strategy_command(strategy_id)).unwrap();
677 let strategy = try_get_actor_unchecked::<TestStrategy>(&strategy_registry_id).unwrap();
678 assert_eq!(strategy.state(), ComponentState::Stopped);
679 assert!(!strategy.is_exiting());
680 drop(strategy);
681
682 Controller::send(&remove_strategy_command(strategy_id)).unwrap();
683 assert!(!trader.borrow().strategy_ids().contains(&strategy_id));
684
685 trader.borrow_mut().stop().unwrap();
686 trader.borrow_mut().dispose_components().unwrap();
687 }
688
689 #[rstest]
690 fn test_controller_create_actor_rolls_back_on_start_failure() {
691 let (trader, controller_id) = create_running_controller();
692 let controller_actor_id = controller_id.inner();
693 let actor_id = ActorId::from("FailingActor-001");
694
695 let result = {
696 let controller = try_get_actor_unchecked::<Controller>(&controller_actor_id).unwrap();
697 controller.create_actor(
698 FailingStartActor::new(DataActorConfig {
699 actor_id: Some(actor_id),
700 ..Default::default()
701 }),
702 true,
703 )
704 };
705
706 assert!(result.is_err());
707 assert!(
708 result
709 .unwrap_err()
710 .to_string()
711 .contains("Simulated actor start failure")
712 );
713 assert!(!trader.borrow().actor_ids().contains(&actor_id));
714 if let Some(actor) = try_get_actor_unchecked::<FailingStartActor>(&actor_id.inner()) {
715 assert_eq!(actor.state(), ComponentState::Disposed);
716 }
717
718 trader.borrow_mut().stop().unwrap();
719 trader.borrow_mut().dispose_components().unwrap();
720 }
721
722 #[rstest]
723 fn test_controller_create_strategy_rolls_back_on_start_failure() {
724 let (trader, controller_id) = create_running_controller();
725 let controller_actor_id = controller_id.inner();
726 let strategy_id = StrategyId::from("FailingStrategy-001");
727
728 let result = {
729 let controller = try_get_actor_unchecked::<Controller>(&controller_actor_id).unwrap();
730 controller.create_strategy(
731 FailingStartStrategy::new(StrategyConfig {
732 strategy_id: Some(strategy_id),
733 order_id_tag: Some("001".to_string()),
734 ..Default::default()
735 }),
736 true,
737 )
738 };
739
740 assert!(result.is_err());
741 assert!(
742 result
743 .unwrap_err()
744 .to_string()
745 .contains("Simulated strategy start failure")
746 );
747 assert!(!trader.borrow().strategy_ids().contains(&strategy_id));
748
749 if let Some(strategy) =
750 try_get_actor_unchecked::<FailingStartStrategy>(&strategy_id.inner())
751 {
752 assert_eq!(strategy.state(), ComponentState::Disposed);
753 }
754
755 trader.borrow_mut().stop().unwrap();
756 trader.borrow_mut().dispose_components().unwrap();
757 }
758
759 #[rstest]
760 fn test_controller_exit_market_allows_reentrant_controller_commands() {
761 let (trader, controller_id) = create_running_controller();
762 let controller_actor_id = controller_id.inner();
763
764 let helper_actor_id = {
765 let controller = try_get_actor_unchecked::<Controller>(&controller_actor_id).unwrap();
766 controller
767 .create_actor(
768 TestDataActor::new(DataActorConfig {
769 actor_id: Some(ActorId::from("HelperActor-001")),
770 ..Default::default()
771 }),
772 true,
773 )
774 .unwrap()
775 };
776
777 let strategy_id = {
778 let controller = try_get_actor_unchecked::<Controller>(&controller_actor_id).unwrap();
779 controller
780 .create_strategy(
781 ReentrantExitStrategy::new(
782 StrategyConfig {
783 strategy_id: Some(StrategyId::from("ReentrantStrategy-001")),
784 order_id_tag: Some("001".to_string()),
785 ..Default::default()
786 },
787 helper_actor_id,
788 ),
789 false,
790 )
791 .unwrap()
792 };
793
794 Controller::send(&start_strategy_command(strategy_id)).unwrap();
795 Controller::send(&ControllerCommand::ExitMarket(strategy_id)).unwrap();
796
797 let helper_actor =
798 try_get_actor_unchecked::<TestDataActor>(&helper_actor_id.inner()).unwrap();
799 assert_eq!(helper_actor.state(), ComponentState::Stopped);
800 drop(helper_actor);
801 assert!(
802 try_get_actor_unchecked::<ReentrantExitStrategy>(&strategy_id.inner())
803 .unwrap()
804 .is_exiting()
805 );
806
807 Controller::send(&stop_strategy_command(strategy_id)).unwrap();
808 Controller::send(&remove_strategy_command(strategy_id)).unwrap();
809 Controller::send(&remove_actor_command(helper_actor_id)).unwrap();
810 trader.borrow_mut().stop().unwrap();
811 trader.borrow_mut().dispose_components().unwrap();
812 }
813
814 #[rstest]
815 fn test_controller_send_fails_after_controller_stop() {
816 let (trader, _) = create_running_controller();
817
818 trader.borrow_mut().stop().unwrap();
819
820 let result = Controller::send(&stop_actor_command(ActorId::from("AnyActor-001")));
821 assert!(result.is_err());
822 assert_eq!(
823 result.unwrap_err().to_string(),
824 "Controller execute endpoint 'Controller.execute' not registered"
825 );
826
827 trader.borrow_mut().dispose_components().unwrap();
828 }
829}