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::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        if let Err(e) = self.add_exec_algorithm(algorithm) {
353            // Without this the guard sees the stranded clock and dead-ends this ID until disposal
354            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    /// Adds a constructed Python actor instance to the trader as an execution algorithm.
366    ///
367    /// This is the [`PyDataActor`]-backed execution algorithm path, used when the Python class
368    /// derives from `DataActor` rather than `ExecutionAlgorithm`.
369    ///
370    /// # Errors
371    ///
372    /// Returns an error if the trader already tracks a component under the algorithm's ID, or if
373    /// the algorithm cannot be registered or tracked.
374    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            // Leave no clock, registry entry, or wrapper behind from a failed attempt
392            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        // Registered once tracking has succeeded, so a rolled back attempt leaves no endpoint
409        register_python_exec_algorithm_endpoint(exec_algorithm_id);
410
411        Ok(())
412    }
413
414    /// Gives `actor` its component clock and registers it in the global component, actor, and
415    /// wrapper registries.
416    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    /// Rejects a component ID this trader already tracks, whatever kind registered it.
456    ///
457    /// Duplicate adds are otherwise checked only within a kind, so an actor sharing an ID with a
458    /// live strategy would overwrite that strategy's clock, registry entries, and wrapper, and a
459    /// rollback would then remove state the attempt did not create. The lifecycle collections are
460    /// checked alongside the clocks because a component registered externally and tracked through
461    /// `add_*_id_for_lifecycle` has no trader-owned clock.
462    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}