Skip to main content

nautilus_system/python/
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::{
17    cell::RefCell,
18    rc::{Rc, Weak},
19};
20
21use nautilus_common::{
22    actor::data_actor::{DataActorConfig, ImportableActorConfig},
23    python::actor::PyDataActor,
24};
25use nautilus_core::python::to_pyruntime_err;
26use nautilus_model::identifiers::{ActorId, StrategyId};
27use nautilus_trading::ImportableStrategyConfig;
28use pyo3::prelude::*;
29use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods};
30
31use crate::{controller::Controller, trader::Trader};
32
33/// Provides a trading controller for managing actors and strategies at runtime.
34///
35/// Subclass this to author a controller in Python. The trader reference is bound when the
36/// controller is registered, so control methods are only available from that point on.
37#[gen_stub_pyclass(module = "nautilus_trader.trading")]
38#[pyclass(
39    module = "nautilus_trader.trading",
40    name = "Controller",
41    extends = PyDataActor,
42    subclass,
43    unsendable
44)]
45#[derive(Debug, Default)]
46pub struct PyController {
47    trader: Option<Weak<RefCell<Trader>>>,
48}
49
50impl PyController {
51    pub(crate) fn bind_trader(&mut self, trader: &Rc<RefCell<Trader>>) {
52        self.trader = Some(Rc::downgrade(trader));
53    }
54}
55
56/// Returns a Rust controller carrying this controller's identity, which
57/// [`Controller::remove_actor`] needs to refuse removing the controller itself.
58fn controller_for(slf: &PyRef<'_, PyController>) -> PyResult<Controller> {
59    let trader = slf
60        .trader
61        .as_ref()
62        .ok_or_else(|| to_pyruntime_err("Controller is not registered with a trader"))?
63        .upgrade()
64        .ok_or_else(|| to_pyruntime_err("Controller trader is no longer available"))?;
65
66    Ok(Controller::new(
67        trader,
68        Some(DataActorConfig {
69            actor_id: Some(slf.as_super().actor_id()),
70            ..Default::default()
71        }),
72    ))
73}
74
75#[gen_stub_pymethods]
76#[pymethods]
77#[allow(
78    clippy::needless_pass_by_value,
79    reason = "PyO3 and the stub generator only recognize an owned `PyRef<'_, Self>` as the receiver"
80)]
81impl PyController {
82    #[new]
83    #[gen_stub(override_return_type(type_repr = "typing.Self", imports = ("typing",)))]
84    #[pyo3(signature = (config=None))]
85    fn py_new(config: Option<Py<PyAny>>) -> PyClassInitializer<Self> {
86        PyClassInitializer::from(PyDataActor::from_py_config(config)).add_subclass(Self::default())
87    }
88
89    #[pyo3(name = "create_actor_from_config", signature = (actor_config, start=true))]
90    fn py_create_actor_from_config(
91        slf: PyRef<'_, Self>,
92        actor_config: ImportableActorConfig,
93        start: bool,
94    ) -> PyResult<ActorId> {
95        controller_for(&slf)?
96            .create_actor_from_config(&actor_config, start)
97            .map_err(to_pyruntime_err)
98    }
99
100    #[pyo3(name = "create_strategy_from_config", signature = (strategy_config, start=true))]
101    fn py_create_strategy_from_config(
102        slf: PyRef<'_, Self>,
103        strategy_config: ImportableStrategyConfig,
104        start: bool,
105    ) -> PyResult<StrategyId> {
106        controller_for(&slf)?
107            .create_strategy_from_config(&strategy_config, start)
108            .map_err(to_pyruntime_err)
109    }
110
111    #[pyo3(name = "start_actor")]
112    fn py_start_actor(slf: PyRef<'_, Self>, actor_id: ActorId) -> PyResult<()> {
113        controller_for(&slf)?
114            .start_actor(&actor_id)
115            .map_err(to_pyruntime_err)
116    }
117
118    #[pyo3(name = "start_actor_from_id")]
119    fn py_start_actor_from_id(slf: PyRef<'_, Self>, actor_id: ActorId) -> PyResult<()> {
120        Self::py_start_actor(slf, actor_id)
121    }
122
123    #[pyo3(name = "stop_actor")]
124    fn py_stop_actor(slf: PyRef<'_, Self>, actor_id: ActorId) -> PyResult<()> {
125        controller_for(&slf)?
126            .stop_actor(&actor_id)
127            .map_err(to_pyruntime_err)
128    }
129
130    #[pyo3(name = "stop_actor_from_id")]
131    fn py_stop_actor_from_id(slf: PyRef<'_, Self>, actor_id: ActorId) -> PyResult<()> {
132        Self::py_stop_actor(slf, actor_id)
133    }
134
135    #[pyo3(name = "remove_actor")]
136    fn py_remove_actor(slf: PyRef<'_, Self>, actor_id: ActorId) -> PyResult<()> {
137        controller_for(&slf)?
138            .remove_actor(&actor_id)
139            .map_err(to_pyruntime_err)
140    }
141
142    #[pyo3(name = "remove_actor_from_id")]
143    fn py_remove_actor_from_id(slf: PyRef<'_, Self>, actor_id: ActorId) -> PyResult<()> {
144        Self::py_remove_actor(slf, actor_id)
145    }
146
147    #[pyo3(name = "start_strategy")]
148    fn py_start_strategy(slf: PyRef<'_, Self>, strategy_id: StrategyId) -> PyResult<()> {
149        controller_for(&slf)?
150            .start_strategy(&strategy_id)
151            .map_err(to_pyruntime_err)
152    }
153
154    #[pyo3(name = "start_strategy_from_id")]
155    fn py_start_strategy_from_id(slf: PyRef<'_, Self>, strategy_id: StrategyId) -> PyResult<()> {
156        Self::py_start_strategy(slf, strategy_id)
157    }
158
159    #[pyo3(name = "stop_strategy")]
160    fn py_stop_strategy(slf: PyRef<'_, Self>, strategy_id: StrategyId) -> PyResult<()> {
161        controller_for(&slf)?
162            .stop_strategy(&strategy_id)
163            .map_err(to_pyruntime_err)
164    }
165
166    #[pyo3(name = "stop_strategy_from_id")]
167    fn py_stop_strategy_from_id(slf: PyRef<'_, Self>, strategy_id: StrategyId) -> PyResult<()> {
168        Self::py_stop_strategy(slf, strategy_id)
169    }
170
171    #[pyo3(name = "market_exit_strategy")]
172    fn py_market_exit_strategy(slf: PyRef<'_, Self>, strategy_id: StrategyId) -> PyResult<()> {
173        controller_for(&slf)?
174            .exit_market(&strategy_id)
175            .map_err(to_pyruntime_err)
176    }
177
178    #[pyo3(name = "market_exit_strategy_from_id")]
179    fn py_market_exit_strategy_from_id(
180        slf: PyRef<'_, Self>,
181        strategy_id: StrategyId,
182    ) -> PyResult<()> {
183        Self::py_market_exit_strategy(slf, strategy_id)
184    }
185
186    #[pyo3(name = "remove_strategy")]
187    fn py_remove_strategy(slf: PyRef<'_, Self>, strategy_id: StrategyId) -> PyResult<()> {
188        controller_for(&slf)?
189            .remove_strategy(&strategy_id)
190            .map_err(to_pyruntime_err)
191    }
192
193    #[pyo3(name = "remove_strategy_from_id")]
194    fn py_remove_strategy_from_id(slf: PyRef<'_, Self>, strategy_id: StrategyId) -> PyResult<()> {
195        Self::py_remove_strategy(slf, strategy_id)
196    }
197}
198
199/// Binds the registered trader to a user-authored Python controller instance.
200pub(crate) fn bind_controller_trader(
201    python_controller: &Py<PyAny>,
202    trader: &Rc<RefCell<Trader>>,
203) -> anyhow::Result<()> {
204    Python::attach(|py| -> anyhow::Result<()> {
205        let mut controller = python_controller
206            .bind(py)
207            .extract::<PyRefMut<PyController>>()
208            .map_err(|e| {
209                anyhow::anyhow!(
210                    "Controller must inherit from `nautilus_trader.trading.Controller`: {e}"
211                )
212            })?;
213
214        controller.bind_trader(trader);
215
216        Ok(())
217    })
218}