Skip to main content

nautilus_system/python/
registration.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
16//! Registration of Python actors, strategies, controllers, and execution algorithms.
17//!
18//! Every Python component reaches the trader through one of the `add_python_*` methods here, so the
19//! sequence each component needs (component clock, register, global registries, lifecycle tracking)
20//! is expressed once. Registering in the global registries retains the component's Python wrapper,
21//! so a caller of that path cannot forget the wrapper. A [`PyExecutionAlgorithm`] registers through
22//! the native path instead, so [`Trader::add_py_execution_algorithm_instance`] retains its wrapper
23//! directly and is the only place that has to.
24
25use std::{cell::RefCell, collections::HashMap, rc::Rc};
26
27use nautilus_common::{
28    actor::{DataActorNative, 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    /// Adds an importable Python actor to the trader.
56    ///
57    /// # Errors
58    ///
59    /// Returns an error if the actor cannot be imported, configured, registered, or tracked.
60    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    /// Adds a constructed Python actor instance to the trader under `actor_id`.
81    ///
82    /// The actor must already be configured; this runs the registration sequence every Python
83    /// actor needs and rolls back everything the attempt created if any step fails.
84    ///
85    /// # Errors
86    ///
87    /// Returns an error if the trader already tracks a component under the actor's ID, or if the
88    /// actor cannot be registered or tracked.
89    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            // Leave no clock, registry entry, or wrapper behind from a failed attempt
99            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    /// Adds an importable Python controller to the trader.
117    ///
118    /// # Errors
119    ///
120    /// Returns an error if the controller cannot be imported, configured, registered, or tracked.
121    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    /// Adds an importable Python strategy to the trader.
151    ///
152    /// # Errors
153    ///
154    /// Returns an error if the strategy cannot be imported, configured, registered, or tracked.
155    pub fn add_strategy_from_importable_config(
156        &mut self,
157        config: &ImportableStrategyConfig,
158    ) -> anyhow::Result<StrategyId> {
159        // Checked before importing and constructing the Python class so a rejected addition never
160        // runs user constructor code
161        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    /// Adds a constructed Python strategy instance to the trader.
169    ///
170    /// This is the instance-based counterpart to [`Self::add_strategy_from_importable_config`]:
171    /// the strategy is already constructed in Python, avoiding the `dict`-to-JSON round trip of
172    /// the importable-config path. The strategy ID, order ID tag, and logging flags are sourced
173    /// from the instance's retained `.config`.
174    ///
175    /// # Errors
176    ///
177    /// Returns an error if the strategy cannot be configured, registered, or tracked.
178    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    /// Prepares a constructed Python strategy instance for registration without committing it.
187    ///
188    /// # Errors
189    ///
190    /// Returns an error if the strategy cannot be configured, or its ID or order ID tag is
191    /// already registered.
192    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            // Mirrors the native path: a configured ID is kept, otherwise the runtime class name
221            // takes the configured order ID tag, or the next positional tag
222            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        // Rejected here as well as on commit so a caller which acts between the two phases, such
246        // as registering external order claims, does not act on a doomed registration
247        self.ensure_component_id_available(ComponentId::from(strategy_id))?;
248
249        Ok(strategy_id)
250    }
251
252    /// Commits a previously prepared Python strategy instance.
253    ///
254    /// # Errors
255    ///
256    /// Returns an error if the trader already tracks a component under the strategy's ID, or if
257    /// the strategy cannot be registered or its subscriptions cannot be installed.
258    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            // Leave no clock, registry entry, or wrapper behind from a failed attempt
276            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    /// Adds a constructed [`PyExecutionAlgorithm`] instance to the trader.
329    ///
330    /// `wrapper` is the Python object which owns `algorithm`; the trader's registries keep it
331    /// alive for as long as the algorithm stays registered.
332    ///
333    /// # Errors
334    ///
335    /// Returns an error if the trader already tracks a component under the algorithm's ID, or if
336    /// the algorithm cannot be registered or tracked.
337    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        // Checked before the shared guard so a same-kind duplicate keeps its own message
345        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        let message_bus = algorithm.core().message_bus();
353        if let Err(e) = self.add_exec_algorithm(algorithm) {
354            // Without this the guard sees the stranded clock and dead-ends this ID until disposal
355            self.release_component(component_id);
356            return Err(e);
357        }
358
359        Python::attach(|py| {
360            retain_python_wrapper(component_id, wrapper.clone_ref(py), message_bus);
361        });
362
363        Ok(exec_algorithm_id)
364    }
365
366    /// Adds a constructed Python actor instance to the trader as an execution algorithm.
367    ///
368    /// This is the [`PyDataActor`]-backed execution algorithm path, used when the Python class
369    /// derives from `DataActor` rather than `ExecutionAlgorithm`.
370    ///
371    /// # Errors
372    ///
373    /// Returns an error if the trader already tracks a component under the algorithm's ID, or if
374    /// the algorithm cannot be registered or tracked.
375    pub fn add_python_exec_algorithm_instance(
376        &mut self,
377        exec_algorithm: &Py<PyAny>,
378        actor_id: ActorId,
379    ) -> anyhow::Result<ExecAlgorithmId> {
380        let exec_algorithm_id = ExecAlgorithmId::new(actor_id.inner());
381
382        if self.exec_algorithm_ids.contains(&exec_algorithm_id) {
383            anyhow::bail!("Execution algorithm '{exec_algorithm_id}' is already registered");
384        }
385
386        let component_id = ComponentId::from(exec_algorithm_id);
387        self.ensure_component_id_available(component_id)?;
388
389        if let Err(e) =
390            self.register_python_exec_algorithm_components(exec_algorithm, exec_algorithm_id)
391        {
392            // Leave no clock, registry entry, or wrapper behind from a failed attempt
393            self.release_component(component_id);
394            return Err(e);
395        }
396
397        Ok(exec_algorithm_id)
398    }
399
400    fn register_python_exec_algorithm_components(
401        &mut self,
402        exec_algorithm: &Py<PyAny>,
403        exec_algorithm_id: ExecAlgorithmId,
404    ) -> anyhow::Result<()> {
405        self.register_python_data_actor(exec_algorithm, ComponentId::from(exec_algorithm_id))?;
406
407        self.add_exec_algorithm_id_for_lifecycle(exec_algorithm_id)?;
408
409        // Registered once tracking has succeeded, so a rolled back attempt leaves no endpoint
410        register_python_exec_algorithm_endpoint(exec_algorithm_id);
411
412        Ok(())
413    }
414
415    /// Gives `actor` its component clock and registers it in the global component, actor, and
416    /// wrapper registries.
417    fn register_python_data_actor(
418        &mut self,
419        actor: &Py<PyAny>,
420        component_id: ComponentId,
421    ) -> anyhow::Result<()> {
422        let clock = self.create_component_clock(component_id);
423        let trader_id = self.trader_id;
424        let cache = self.cache.clone();
425
426        Python::attach(|py| -> anyhow::Result<()> {
427            let py_actor = actor.bind(py);
428            let mut py_data_actor_ref = py_actor
429                .extract::<PyRefMut<PyDataActor>>()
430                .map_err(Into::<PyErr>::into)
431                .map_err(|e| anyhow::anyhow!("Failed to extract PyDataActor: {e}"))?;
432
433            py_data_actor_ref
434                .register(trader_id, clock, cache)
435                .map_err(|e| anyhow::anyhow!("Failed to register PyDataActor: {e}"))?;
436
437            log::debug!(
438                "Internal PyDataActor registered: {}, state: {:?}",
439                py_data_actor_ref.is_registered(),
440                py_data_actor_ref.state()
441            );
442
443            Ok(())
444        })?;
445
446        Python::attach(|py| -> anyhow::Result<()> {
447            let py_actor = actor.bind(py);
448            let py_data_actor_ref = py_actor
449                .cast::<PyDataActor>()
450                .map_err(|e| anyhow::anyhow!("Failed to downcast to PyDataActor: {e}"))?;
451            py_data_actor_ref.borrow().register_in_global_registries()?;
452            Ok(())
453        })
454    }
455
456    /// Rejects a component ID this trader already tracks, whatever kind registered it.
457    ///
458    /// Duplicate adds are otherwise checked only within a kind, so an actor sharing an ID with a
459    /// live strategy would overwrite that strategy's clock, registry entries, and wrapper, and a
460    /// rollback would then remove state the attempt did not create. The lifecycle collections are
461    /// checked alongside the clocks because a component registered externally and tracked through
462    /// `add_*_id_for_lifecycle` has no trader-owned clock.
463    fn ensure_component_id_available(&self, component_id: ComponentId) -> anyhow::Result<()> {
464        let id = component_id.inner();
465        let tracked = self.clocks.contains_key(&component_id)
466            || self.actor_ids.iter().any(|actor_id| actor_id.inner() == id)
467            || self
468                .strategy_ids
469                .iter()
470                .any(|strategy_id| strategy_id.inner() == id)
471            || self
472                .exec_algorithm_ids
473                .iter()
474                .any(|exec_algorithm_id| exec_algorithm_id.inner() == id);
475
476        if tracked {
477            anyhow::bail!(
478                "Component {component_id} is already registered with trader {}",
479                self.trader_id
480            );
481        }
482
483        Ok(())
484    }
485}
486
487fn create_python_actor(config: &ImportableActorConfig) -> anyhow::Result<(Py<PyAny>, ActorId)> {
488    let (module_name, class_name) = split_import_path(&config.actor_path, "actor_path")?;
489
490    log::info!("Importing actor from module: {module_name} class: {class_name}");
491
492    Python::attach(|py| -> anyhow::Result<(Py<PyAny>, ActorId)> {
493        let actor_class = import_python_class(py, module_name, class_name)?;
494        let config_instance = create_config_instance(py, &config.config_path, &config.config)?;
495
496        let python_actor = if let Some(config_obj) = config_instance.as_ref() {
497            actor_class.call1((config_obj,))?
498        } else {
499            actor_class.call0()?
500        };
501
502        let actor_id = prepare_python_actor(&python_actor, config_instance.as_ref())?;
503
504        Ok((python_actor.unbind(), actor_id))
505    })
506}
507
508fn create_python_strategy(config: &ImportableStrategyConfig) -> anyhow::Result<Py<PyAny>> {
509    let (module_name, class_name) = split_import_path(&config.strategy_path, "strategy_path")?;
510
511    log::info!("Importing strategy from module: {module_name} class: {class_name}");
512
513    Python::attach(|py| -> anyhow::Result<Py<PyAny>> {
514        let strategy_class = import_python_class(py, module_name, class_name)?;
515        let config_instance = create_config_instance(py, &config.config_path, &config.config)?;
516
517        let python_strategy = if let Some(config_obj) = config_instance.as_ref() {
518            strategy_class.call1((config_obj,))?
519        } else {
520            strategy_class.call0()?
521        };
522
523        Ok(python_strategy.unbind())
524    })
525}
526
527fn split_import_path<'a>(path: &'a str, field: &str) -> anyhow::Result<(&'a str, &'a str)> {
528    let Some((module_name, class_name)) = path.split_once(':') else {
529        anyhow::bail!("{field} must be in format 'module.path:ClassName'");
530    };
531
532    if module_name.is_empty() || class_name.is_empty() || class_name.contains(':') {
533        anyhow::bail!("{field} must be in format 'module.path:ClassName'");
534    }
535
536    Ok((module_name, class_name))
537}
538
539fn import_python_class<'py>(
540    py: Python<'py>,
541    module_name: &str,
542    class_name: &str,
543) -> anyhow::Result<Bound<'py, PyAny>> {
544    let module = py
545        .import(module_name)
546        .map_err(|e| anyhow::anyhow!("Failed to import module {module_name}: {e}"))?;
547
548    module
549        .getattr(class_name)
550        .map_err(|e| anyhow::anyhow!("Failed to get class {class_name}: {e}"))
551}
552
553fn create_config_instance<'py>(
554    py: Python<'py>,
555    config_path: &str,
556    config: &HashMap<String, serde_json::Value>,
557) -> anyhow::Result<Option<Bound<'py, PyAny>>> {
558    if config_path.is_empty() && config.is_empty() {
559        log::debug!("No config_path or empty config, using None");
560        return Ok(None);
561    }
562
563    let Some((config_module_name, config_class_name)) = config_path.split_once(':') else {
564        anyhow::bail!("config_path must be in format 'module.path:ClassName', was {config_path}");
565    };
566
567    if config_module_name.is_empty()
568        || config_class_name.is_empty()
569        || config_class_name.contains(':')
570    {
571        anyhow::bail!("config_path must be in format 'module.path:ClassName', was {config_path}");
572    }
573
574    log::debug!(
575        "Importing config class from module: {config_module_name} class: {config_class_name}"
576    );
577
578    let config_module = py
579        .import(config_module_name)
580        .map_err(|e| anyhow::anyhow!("Failed to import config module {config_module_name}: {e}"))?;
581    let config_class = config_module
582        .getattr(config_class_name)
583        .map_err(|e| anyhow::anyhow!("Failed to get config class {config_class_name}: {e}"))?;
584    let py_dict = PyDict::new(py);
585
586    for (key, value) in config {
587        let py_value = config_value_to_py(py, key, value)?;
588        py_dict.set_item(key, py_value)?;
589    }
590
591    let config_instance = match config_class.call((), Some(&py_dict)) {
592        Ok(instance) => instance,
593        Err(kwargs_err) => match config_class.call0() {
594            Ok(instance) => {
595                for (key, value) in config {
596                    let py_value = config_value_to_py(py, key, value)?;
597
598                    if let Err(setattr_err) = instance.setattr(key, py_value) {
599                        anyhow::bail!("Failed to set attribute {key}: {setattr_err}");
600                    }
601                }
602
603                if instance.hasattr("__post_init__")? {
604                    instance.call_method0("__post_init__")?;
605                }
606
607                instance
608            }
609            Err(default_err) => {
610                anyhow::bail!(
611                    "Failed to create config instance. Tried kwargs: {kwargs_err}, default: {default_err}"
612                );
613            }
614        },
615    };
616
617    Ok(Some(config_instance))
618}
619
620fn config_value_to_py<'py>(
621    py: Python<'py>,
622    key: &str,
623    value: &serde_json::Value,
624) -> anyhow::Result<Bound<'py, PyAny>> {
625    if key == "actor_id"
626        && let Some(actor_id) = value.as_str()
627    {
628        return Ok(ActorId::new_checked(actor_id)?
629            .into_pyobject(py)?
630            .into_any());
631    }
632
633    if key == "strategy_id"
634        && let Some(strategy_id) = value.as_str()
635    {
636        return Ok(StrategyId::new_checked(strategy_id)?
637            .into_pyobject(py)?
638            .into_any());
639    }
640
641    let json_str = serde_json::to_string(value)
642        .map_err(|e| anyhow::anyhow!("Failed to serialize config value: {e}"))?;
643
644    Ok(PyModule::import(py, "json")?
645        .call_method("loads", (json_str,), None)?
646        .into_any())
647}
648
649fn configure_py_strategy(
650    strategy: &mut PyRefMut<'_, PyStrategy>,
651    config_obj: &Bound<'_, PyAny>,
652) -> anyhow::Result<()> {
653    if let Some(strategy_id) = config_obj
654        .getattr("strategy_id")
655        .ok()
656        .filter(|value| !value.is_none())
657    {
658        let strategy_id = if let Ok(strategy_id) = strategy_id.extract::<StrategyId>() {
659            strategy_id
660        } else if let Ok(strategy_id_str) = strategy_id.extract::<String>() {
661            StrategyId::new_checked(&strategy_id_str)?
662        } else {
663            anyhow::bail!("Invalid `strategy_id` type");
664        };
665        strategy.set_strategy_id(strategy_id)?;
666    }
667
668    if let Some(order_id_tag) = config_obj
669        .getattr("order_id_tag")
670        .ok()
671        .filter(|value| !value.is_none())
672    {
673        let order_id_tag = order_id_tag
674            .extract::<String>()
675            .map_err(|e| anyhow::anyhow!("Invalid `order_id_tag` type: {e}"))?;
676        strategy.set_order_id_tag(&order_id_tag)?;
677    }
678
679    if let Some(log_events) = extract_bool_config_attr(config_obj, "log_events") {
680        strategy.set_log_events(log_events);
681    }
682
683    if let Some(log_commands) = extract_bool_config_attr(config_obj, "log_commands") {
684        strategy.set_log_commands(log_commands);
685    }
686
687    Ok(())
688}
689
690fn extract_bool_config_attr(config_obj: &Bound<'_, PyAny>, attr: &str) -> Option<bool> {
691    config_obj
692        .getattr(attr)
693        .ok()
694        .and_then(|value| value.extract::<bool>().ok())
695}