1use std::{cell::RefCell, collections::HashMap, rc::Rc};
26
27use nautilus_common::{
28 actor::data_actor::ImportableActorConfig,
29 python::{
30 actor::{
31 PyDataActor, PyDataActorInner, prepare_python_actor,
32 register_python_exec_algorithm_endpoint,
33 },
34 wrappers::retain_python_wrapper,
35 },
36};
37use nautilus_model::identifiers::{
38 ActorId, ComponentId, ExecAlgorithmId, StrategyId, normalize_order_id_tag,
39};
40use nautilus_trading::{
41 ImportableControllerConfig, ImportableStrategyConfig,
42 python::{
43 algorithm::PyExecutionAlgorithm,
44 strategy::{PyStrategy, PyStrategyInner},
45 },
46};
47use pyo3::{
48 prelude::*,
49 types::{PyDict, PyModule},
50};
51
52use crate::{registration::ensure_unique_order_id_tag, trader::Trader};
53
54impl Trader {
55 pub fn add_actor_from_importable_config(
61 &mut self,
62 config: &ImportableActorConfig,
63 ) -> anyhow::Result<ActorId> {
64 self.validate_actor_or_strategy_registration()?;
65
66 let (python_actor, actor_id) = create_python_actor(config)?;
67 if self.actor_ids.contains(&actor_id) {
68 anyhow::bail!("Actor {actor_id} is already registered");
69 }
70
71 self.add_python_actor_instance(&python_actor, actor_id)?;
72
73 log::info!(
74 "Registered Python actor {actor_id} with trader {}",
75 self.trader_id
76 );
77 Ok(actor_id)
78 }
79
80 pub fn add_python_actor_instance(
90 &mut self,
91 actor: &Py<PyAny>,
92 actor_id: ActorId,
93 ) -> anyhow::Result<()> {
94 let component_id = ComponentId::from(actor_id);
95 self.ensure_component_id_available(component_id)?;
96
97 if let Err(e) = self.register_python_actor_components(actor, actor_id) {
98 self.release_component(component_id);
100 return Err(e);
101 }
102
103 Ok(())
104 }
105
106 fn register_python_actor_components(
107 &mut self,
108 actor: &Py<PyAny>,
109 actor_id: ActorId,
110 ) -> anyhow::Result<()> {
111 self.register_python_data_actor(actor, ComponentId::from(actor_id))?;
112
113 self.add_actor_id_for_lifecycle::<PyDataActorInner>(actor_id)
114 }
115
116 pub fn add_controller_from_importable_config(
122 trader: &Rc<RefCell<Self>>,
123 config: &ImportableControllerConfig,
124 ) -> anyhow::Result<ActorId> {
125 trader.borrow().validate_actor_or_strategy_registration()?;
126
127 let actor_config = ImportableActorConfig {
128 actor_path: config.controller_path.clone(),
129 config_path: config.config_path.clone(),
130 config: config.config.clone(),
131 };
132 let (python_controller, actor_id) = create_python_actor(&actor_config)?;
133 if trader.borrow().actor_ids.contains(&actor_id) {
134 anyhow::bail!("Actor {actor_id} is already registered");
135 }
136
137 crate::python::controller::bind_controller_trader(&python_controller, trader)?;
138
139 trader
140 .borrow_mut()
141 .add_python_actor_instance(&python_controller, actor_id)?;
142
143 log::info!(
144 "Registered Python controller {actor_id} with trader {}",
145 trader.borrow().trader_id
146 );
147 Ok(actor_id)
148 }
149
150 pub fn add_strategy_from_importable_config(
156 &mut self,
157 config: &ImportableStrategyConfig,
158 ) -> anyhow::Result<StrategyId> {
159 self.validate_actor_or_strategy_registration()?;
162
163 let python_strategy = create_python_strategy(config)?;
164
165 self.add_python_strategy_instance(&python_strategy)
166 }
167
168 pub fn add_python_strategy_instance(
179 &mut self,
180 strategy: &Py<PyAny>,
181 ) -> anyhow::Result<StrategyId> {
182 self.prepare_python_strategy_instance(strategy)?;
183 self.commit_python_strategy_instance(strategy)
184 }
185
186 pub fn prepare_python_strategy_instance(
193 &mut self,
194 strategy: &Py<PyAny>,
195 ) -> anyhow::Result<StrategyId> {
196 self.validate_actor_or_strategy_registration()?;
197
198 let existing_order_id_tags: Vec<&str> =
199 self.strategy_ids.iter().map(StrategyId::get_tag).collect();
200
201 let strategy_id = Python::attach(|py| -> anyhow::Result<StrategyId> {
202 let bound = strategy.bind(py);
203
204 let config_instance = bound
205 .getattr("config")
206 .ok()
207 .filter(|config| !config.is_none());
208
209 let class_name = bound.get_type().name()?.to_string();
210
211 let mut py_strategy_ref = bound
212 .extract::<PyRefMut<PyStrategy>>()
213 .map_err(Into::<PyErr>::into)
214 .map_err(|e| anyhow::anyhow!("Failed to extract PyStrategy: {e}"))?;
215
216 if let Some(config_obj) = config_instance.as_ref() {
217 configure_py_strategy(&mut py_strategy_ref, config_obj)?;
218 }
219
220 let runtime_order_id_tag = py_strategy_ref.order_id_tag();
223 let strategy_id = if let Some(strategy_id) = py_strategy_ref.configured_strategy_id() {
224 strategy_id
225 } else {
226 let order_id_tag = normalize_order_id_tag(runtime_order_id_tag.as_deref())
227 .map_or_else(
228 || format!("{:03}", existing_order_id_tags.len()),
229 str::to_string,
230 );
231 StrategyId::new_checked(format!("{class_name}-{order_id_tag}"))?
232 };
233
234 if self.strategy_ids.contains(&strategy_id) {
235 anyhow::bail!("Strategy {strategy_id} is already registered");
236 }
237 ensure_unique_order_id_tag(&existing_order_id_tags, strategy_id.get_tag())?;
238
239 py_strategy_ref.set_strategy_id(strategy_id)?;
240 py_strategy_ref.set_python_instance(bound)?;
241
242 Ok(py_strategy_ref.strategy_id())
243 })?;
244
245 self.ensure_component_id_available(ComponentId::from(strategy_id))?;
248
249 Ok(strategy_id)
250 }
251
252 pub fn commit_python_strategy_instance(
259 &mut self,
260 strategy: &Py<PyAny>,
261 ) -> anyhow::Result<StrategyId> {
262 let strategy_id = Python::attach(|py| -> anyhow::Result<StrategyId> {
263 Ok(strategy
264 .bind(py)
265 .extract::<PyRef<PyStrategy>>()
266 .map_err(Into::<PyErr>::into)
267 .map_err(|e| anyhow::anyhow!("Failed to extract PyStrategy: {e}"))?
268 .strategy_id())
269 })?;
270
271 let component_id = ComponentId::from(strategy_id);
272 self.ensure_component_id_available(component_id)?;
273
274 if let Err(e) = self.register_python_strategy_components(strategy, strategy_id) {
275 self.release_component(component_id);
277 return Err(e);
278 }
279
280 log::info!(
281 "Registered Python strategy {strategy_id} with trader {}",
282 self.trader_id
283 );
284 Ok(strategy_id)
285 }
286
287 fn register_python_strategy_components(
288 &mut self,
289 strategy: &Py<PyAny>,
290 strategy_id: StrategyId,
291 ) -> anyhow::Result<()> {
292 let clock = self.create_component_clock(ComponentId::from(strategy_id));
293 let trader_id = self.trader_id;
294 let cache = self.cache.clone();
295 let portfolio = self.portfolio.clone();
296
297 Python::attach(|py| -> anyhow::Result<()> {
298 let py_strategy = strategy.bind(py);
299 let mut py_strategy_ref = py_strategy
300 .extract::<PyRefMut<PyStrategy>>()
301 .map_err(Into::<PyErr>::into)
302 .map_err(|e| anyhow::anyhow!("Failed to extract PyStrategy: {e}"))?;
303
304 py_strategy_ref
305 .register(trader_id, clock, cache, portfolio)
306 .map_err(|e| anyhow::anyhow!("Failed to register PyStrategy: {e}"))?;
307
308 log::debug!(
309 "Internal PyStrategy registered: {}",
310 py_strategy_ref.is_registered()
311 );
312
313 Ok(())
314 })?;
315
316 Python::attach(|py| -> anyhow::Result<()> {
317 let py_strategy = strategy.bind(py);
318 let py_strategy_ref = py_strategy
319 .cast::<PyStrategy>()
320 .map_err(|e| anyhow::anyhow!("Failed to downcast to PyStrategy: {e}"))?;
321 py_strategy_ref.borrow().register_in_global_registries()?;
322 Ok(())
323 })?;
324
325 self.add_strategy_id_with_subscriptions::<PyStrategyInner>(strategy_id)
326 }
327
328 pub fn add_py_execution_algorithm_instance(
338 &mut self,
339 algorithm: PyExecutionAlgorithm,
340 wrapper: &Py<PyAny>,
341 ) -> anyhow::Result<ExecAlgorithmId> {
342 let exec_algorithm_id = algorithm.exec_algorithm_id();
343
344 if self.exec_algorithm_ids.contains(&exec_algorithm_id) {
346 anyhow::bail!("Execution algorithm '{exec_algorithm_id}' is already registered");
347 }
348
349 let component_id = ComponentId::from(exec_algorithm_id);
350 self.ensure_component_id_available(component_id)?;
351
352 if let Err(e) = self.add_exec_algorithm(algorithm) {
353 self.release_component(component_id);
355 return Err(e);
356 }
357
358 Python::attach(|py| {
359 retain_python_wrapper(component_id, wrapper.clone_ref(py));
360 });
361
362 Ok(exec_algorithm_id)
363 }
364
365 pub fn add_python_exec_algorithm_instance(
375 &mut self,
376 exec_algorithm: &Py<PyAny>,
377 actor_id: ActorId,
378 ) -> anyhow::Result<ExecAlgorithmId> {
379 let exec_algorithm_id = ExecAlgorithmId::from(actor_id.inner().as_str());
380
381 if self.exec_algorithm_ids.contains(&exec_algorithm_id) {
382 anyhow::bail!("Execution algorithm '{exec_algorithm_id}' is already registered");
383 }
384
385 let component_id = ComponentId::from(exec_algorithm_id);
386 self.ensure_component_id_available(component_id)?;
387
388 if let Err(e) =
389 self.register_python_exec_algorithm_components(exec_algorithm, exec_algorithm_id)
390 {
391 self.release_component(component_id);
393 return Err(e);
394 }
395
396 Ok(exec_algorithm_id)
397 }
398
399 fn register_python_exec_algorithm_components(
400 &mut self,
401 exec_algorithm: &Py<PyAny>,
402 exec_algorithm_id: ExecAlgorithmId,
403 ) -> anyhow::Result<()> {
404 self.register_python_data_actor(exec_algorithm, ComponentId::from(exec_algorithm_id))?;
405
406 self.add_exec_algorithm_id_for_lifecycle(exec_algorithm_id)?;
407
408 register_python_exec_algorithm_endpoint(exec_algorithm_id);
410
411 Ok(())
412 }
413
414 fn register_python_data_actor(
417 &mut self,
418 actor: &Py<PyAny>,
419 component_id: ComponentId,
420 ) -> anyhow::Result<()> {
421 let clock = self.create_component_clock(component_id);
422 let trader_id = self.trader_id;
423 let cache = self.cache.clone();
424
425 Python::attach(|py| -> anyhow::Result<()> {
426 let py_actor = actor.bind(py);
427 let mut py_data_actor_ref = py_actor
428 .extract::<PyRefMut<PyDataActor>>()
429 .map_err(Into::<PyErr>::into)
430 .map_err(|e| anyhow::anyhow!("Failed to extract PyDataActor: {e}"))?;
431
432 py_data_actor_ref
433 .register(trader_id, clock, cache)
434 .map_err(|e| anyhow::anyhow!("Failed to register PyDataActor: {e}"))?;
435
436 log::debug!(
437 "Internal PyDataActor registered: {}, state: {:?}",
438 py_data_actor_ref.is_registered(),
439 py_data_actor_ref.state()
440 );
441
442 Ok(())
443 })?;
444
445 Python::attach(|py| -> anyhow::Result<()> {
446 let py_actor = actor.bind(py);
447 let py_data_actor_ref = py_actor
448 .cast::<PyDataActor>()
449 .map_err(|e| anyhow::anyhow!("Failed to downcast to PyDataActor: {e}"))?;
450 py_data_actor_ref.borrow().register_in_global_registries()?;
451 Ok(())
452 })
453 }
454
455 fn ensure_component_id_available(&self, component_id: ComponentId) -> anyhow::Result<()> {
463 let id = component_id.inner();
464 let tracked = self.clocks.contains_key(&component_id)
465 || self.actor_ids.iter().any(|actor_id| actor_id.inner() == id)
466 || self
467 .strategy_ids
468 .iter()
469 .any(|strategy_id| strategy_id.inner() == id)
470 || self
471 .exec_algorithm_ids
472 .iter()
473 .any(|exec_algorithm_id| exec_algorithm_id.inner() == id);
474
475 if tracked {
476 anyhow::bail!(
477 "Component {component_id} is already registered with trader {}",
478 self.trader_id
479 );
480 }
481
482 Ok(())
483 }
484}
485
486fn create_python_actor(config: &ImportableActorConfig) -> anyhow::Result<(Py<PyAny>, ActorId)> {
487 let (module_name, class_name) = split_import_path(&config.actor_path, "actor_path")?;
488
489 log::info!("Importing actor from module: {module_name} class: {class_name}");
490
491 Python::attach(|py| -> anyhow::Result<(Py<PyAny>, ActorId)> {
492 let actor_class = import_python_class(py, module_name, class_name)?;
493 let config_instance = create_config_instance(py, &config.config_path, &config.config)?;
494
495 let python_actor = if let Some(config_obj) = config_instance.as_ref() {
496 actor_class.call1((config_obj,))?
497 } else {
498 actor_class.call0()?
499 };
500
501 let actor_id = prepare_python_actor(&python_actor, config_instance.as_ref())?;
502
503 Ok((python_actor.unbind(), actor_id))
504 })
505}
506
507fn create_python_strategy(config: &ImportableStrategyConfig) -> anyhow::Result<Py<PyAny>> {
508 let (module_name, class_name) = split_import_path(&config.strategy_path, "strategy_path")?;
509
510 log::info!("Importing strategy from module: {module_name} class: {class_name}");
511
512 Python::attach(|py| -> anyhow::Result<Py<PyAny>> {
513 let strategy_class = import_python_class(py, module_name, class_name)?;
514 let config_instance = create_config_instance(py, &config.config_path, &config.config)?;
515
516 let python_strategy = if let Some(config_obj) = config_instance.as_ref() {
517 strategy_class.call1((config_obj,))?
518 } else {
519 strategy_class.call0()?
520 };
521
522 Ok(python_strategy.unbind())
523 })
524}
525
526fn split_import_path<'a>(path: &'a str, field: &str) -> anyhow::Result<(&'a str, &'a str)> {
527 let Some((module_name, class_name)) = path.split_once(':') else {
528 anyhow::bail!("{field} must be in format 'module.path:ClassName'");
529 };
530
531 if module_name.is_empty() || class_name.is_empty() || class_name.contains(':') {
532 anyhow::bail!("{field} must be in format 'module.path:ClassName'");
533 }
534
535 Ok((module_name, class_name))
536}
537
538fn import_python_class<'py>(
539 py: Python<'py>,
540 module_name: &str,
541 class_name: &str,
542) -> anyhow::Result<Bound<'py, PyAny>> {
543 let module = py
544 .import(module_name)
545 .map_err(|e| anyhow::anyhow!("Failed to import module {module_name}: {e}"))?;
546
547 module
548 .getattr(class_name)
549 .map_err(|e| anyhow::anyhow!("Failed to get class {class_name}: {e}"))
550}
551
552fn create_config_instance<'py>(
553 py: Python<'py>,
554 config_path: &str,
555 config: &HashMap<String, serde_json::Value>,
556) -> anyhow::Result<Option<Bound<'py, PyAny>>> {
557 if config_path.is_empty() && config.is_empty() {
558 log::debug!("No config_path or empty config, using None");
559 return Ok(None);
560 }
561
562 let Some((config_module_name, config_class_name)) = config_path.split_once(':') else {
563 anyhow::bail!("config_path must be in format 'module.path:ClassName', was {config_path}");
564 };
565
566 if config_module_name.is_empty()
567 || config_class_name.is_empty()
568 || config_class_name.contains(':')
569 {
570 anyhow::bail!("config_path must be in format 'module.path:ClassName', was {config_path}");
571 }
572
573 log::debug!(
574 "Importing config class from module: {config_module_name} class: {config_class_name}"
575 );
576
577 let config_module = py
578 .import(config_module_name)
579 .map_err(|e| anyhow::anyhow!("Failed to import config module {config_module_name}: {e}"))?;
580 let config_class = config_module
581 .getattr(config_class_name)
582 .map_err(|e| anyhow::anyhow!("Failed to get config class {config_class_name}: {e}"))?;
583 let py_dict = PyDict::new(py);
584
585 for (key, value) in config {
586 let py_value = config_value_to_py(py, key, value)?;
587 py_dict.set_item(key, py_value)?;
588 }
589
590 let config_instance = match config_class.call((), Some(&py_dict)) {
591 Ok(instance) => instance,
592 Err(kwargs_err) => match config_class.call0() {
593 Ok(instance) => {
594 for (key, value) in config {
595 let py_value = config_value_to_py(py, key, value)?;
596
597 if let Err(setattr_err) = instance.setattr(key, py_value) {
598 log::warn!("Failed to set attribute {key}: {setattr_err}");
599 }
600 }
601
602 if instance.hasattr("__post_init__")? {
603 instance.call_method0("__post_init__")?;
604 }
605
606 instance
607 }
608 Err(default_err) => {
609 anyhow::bail!(
610 "Failed to create config instance. Tried kwargs: {kwargs_err}, default: {default_err}"
611 );
612 }
613 },
614 };
615
616 Ok(Some(config_instance))
617}
618
619fn config_value_to_py<'py>(
620 py: Python<'py>,
621 key: &str,
622 value: &serde_json::Value,
623) -> anyhow::Result<Bound<'py, PyAny>> {
624 if key == "actor_id"
625 && let Some(actor_id) = value.as_str()
626 {
627 return Ok(ActorId::new_checked(actor_id)?
628 .into_pyobject(py)?
629 .into_any());
630 }
631
632 let json_str = serde_json::to_string(value)
633 .map_err(|e| anyhow::anyhow!("Failed to serialize config value: {e}"))?;
634
635 Ok(PyModule::import(py, "json")?
636 .call_method("loads", (json_str,), None)?
637 .into_any())
638}
639
640fn configure_py_strategy(
641 strategy: &mut PyRefMut<'_, PyStrategy>,
642 config_obj: &Bound<'_, PyAny>,
643) -> anyhow::Result<()> {
644 if let Some(strategy_id) = config_obj
645 .getattr("strategy_id")
646 .ok()
647 .filter(|value| !value.is_none())
648 {
649 let strategy_id = if let Ok(strategy_id) = strategy_id.extract::<StrategyId>() {
650 strategy_id
651 } else if let Ok(strategy_id_str) = strategy_id.extract::<String>() {
652 StrategyId::new_checked(&strategy_id_str)?
653 } else {
654 anyhow::bail!("Invalid `strategy_id` type");
655 };
656 strategy.set_strategy_id(strategy_id)?;
657 }
658
659 if let Some(order_id_tag) = config_obj
660 .getattr("order_id_tag")
661 .ok()
662 .filter(|value| !value.is_none())
663 {
664 let order_id_tag = order_id_tag
665 .extract::<String>()
666 .map_err(|e| anyhow::anyhow!("Invalid `order_id_tag` type: {e}"))?;
667 strategy.set_order_id_tag(&order_id_tag)?;
668 }
669
670 if let Some(log_events) = extract_bool_config_attr(config_obj, "log_events") {
671 strategy.set_log_events(log_events);
672 }
673
674 if let Some(log_commands) = extract_bool_config_attr(config_obj, "log_commands") {
675 strategy.set_log_commands(log_commands);
676 }
677
678 Ok(())
679}
680
681fn extract_bool_config_attr(config_obj: &Bound<'_, PyAny>, attr: &str) -> Option<bool> {
682 config_obj
683 .getattr(attr)
684 .ok()
685 .and_then(|value| value.extract::<bool>().ok())
686}