Skip to main content

nautilus_system/
controller.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use 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    /// Sends a controller command to the registered controller endpoint.
51    ///
52    /// # Errors
53    ///
54    /// Returns an error if the controller execute endpoint is not registered.
55    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    /// Executes a controller command against the underlying trader.
78    ///
79    /// # Errors
80    ///
81    /// Returns an error if the requested lifecycle operation fails.
82    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    /// Creates a new actor and optionally starts it.
103    ///
104    /// # Errors
105    ///
106    /// Returns an error if actor registration or startup fails.
107    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    /// Creates a new actor from a factory and optionally starts it.
120    ///
121    /// # Errors
122    ///
123    /// Returns an error if the factory, actor registration, or startup fails.
124    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    /// Creates a new actor from an importable config and optionally starts it.
138    ///
139    /// # Errors
140    ///
141    /// Returns an error if the import, actor registration, or startup fails.
142    #[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    /// Rejects importable actor creation when Python support is not compiled in.
159    ///
160    /// # Errors
161    ///
162    /// Always returns an unsupported command error.
163    #[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    /// Creates a new strategy and optionally starts it.
173    ///
174    /// # Errors
175    ///
176    /// Returns an error if strategy registration or startup fails.
177    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    /// Creates a new strategy from a factory and optionally starts it.
193    ///
194    /// # Errors
195    ///
196    /// Returns an error if the factory, strategy registration, or startup fails.
197    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    /// Creates a new strategy from an importable config and optionally starts it.
211    ///
212    /// # Errors
213    ///
214    /// Returns an error if the import, strategy registration, or startup fails.
215    #[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    /// Rejects importable strategy creation when Python support is not compiled in.
232    ///
233    /// # Errors
234    ///
235    /// Always returns an unsupported command error.
236    #[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    /// Starts the registered actor with the given identifier.
246    ///
247    /// # Errors
248    ///
249    /// Returns an error if the actor is not registered or cannot be started.
250    pub fn start_actor(&self, actor_id: &ActorId) -> anyhow::Result<()> {
251        self.trader.borrow().start_actor(actor_id)
252    }
253
254    /// Stops the registered actor with the given identifier.
255    ///
256    /// # Errors
257    ///
258    /// Returns an error if the actor is not registered or cannot be stopped.
259    pub fn stop_actor(&self, actor_id: &ActorId) -> anyhow::Result<()> {
260        self.trader.borrow().stop_actor(actor_id)
261    }
262
263    /// Removes the registered actor with the given identifier.
264    ///
265    /// # Errors
266    ///
267    /// Returns an error if the actor cannot be removed.
268    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    /// Starts the registered strategy with the given identifier.
277    ///
278    /// # Errors
279    ///
280    /// Returns an error if the strategy is not registered or cannot be started.
281    pub fn start_strategy(&self, strategy_id: &StrategyId) -> anyhow::Result<()> {
282        self.trader.borrow().start_strategy(strategy_id)
283    }
284
285    /// Stops the registered strategy with the given identifier.
286    ///
287    /// # Errors
288    ///
289    /// Returns an error if the strategy is not registered or cannot be stopped.
290    pub fn stop_strategy(&self, strategy_id: &StrategyId) -> anyhow::Result<()> {
291        self.trader.borrow_mut().stop_strategy(strategy_id)
292    }
293
294    /// Sends an exit-market command to the registered strategy.
295    ///
296    /// # Errors
297    ///
298    /// Returns an error if the strategy is not registered or its control endpoint is missing.
299    pub fn exit_market(&self, strategy_id: &StrategyId) -> anyhow::Result<()> {
300        Trader::market_exit_strategy(&self.trader, strategy_id)
301    }
302
303    /// Removes the registered strategy with the given identifier.
304    ///
305    /// # Errors
306    ///
307    /// Returns an error if the strategy cannot be removed.
308    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}