Skip to main content

nautilus_live/python/
node.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//! Python bindings for live node.
17
18use std::{cell::RefCell, collections::HashMap, rc::Rc, str::FromStr};
19
20use nautilus_common::{
21    actor::data_actor::ImportableActorConfig,
22    cache::CacheConfig,
23    enums::Environment,
24    live::get_runtime,
25    logging::logger::LoggerConfig,
26    python::actor::{PyDataActor, register_python_exec_algorithm_endpoint},
27};
28#[cfg(feature = "examples")]
29use nautilus_core::python::to_pytype_err;
30use nautilus_core::{
31    UUID4,
32    python::{to_pyruntime_err, to_pyvalue_err},
33};
34use nautilus_model::identifiers::{
35    ActorId, ComponentId, ExecAlgorithmId, InstrumentId, StrategyId, TraderId,
36};
37use nautilus_portfolio::config::PortfolioConfig;
38use nautilus_system::get_global_pyo3_registry;
39#[cfg(feature = "examples")]
40use nautilus_testkit::{DataTester, DataTesterConfig, ExecTester, ExecTesterConfig};
41#[cfg(feature = "examples")]
42use nautilus_trading::examples::{
43    actors::{BookImbalanceActor, BookImbalanceActorConfig},
44    strategies::{
45        CompositeMarketMaker, CompositeMarketMakerConfig, DeltaNeutralVol, DeltaNeutralVolConfig,
46        EmaCross, EmaCrossConfig, GridMarketMaker, GridMarketMakerConfig, HurstVpinDirectional,
47        HurstVpinDirectionalConfig,
48    },
49};
50use nautilus_trading::{
51    ImportableExecAlgorithmConfig, ImportableStrategyConfig,
52    python::strategy::{PyStrategy, PyStrategyInner},
53};
54use pyo3::{
55    prelude::*,
56    types::{PyCFunction, PyDict, PyTuple},
57};
58use serde_json;
59
60use crate::{
61    builder::LiveNodeBuilder,
62    config::{
63        LiveDataEngineConfig, LiveExecEngineConfig, LiveNodeConfig, LiveRiskEngineConfig,
64        PluginConfig,
65    },
66    node::LiveNode,
67    python::config::coerce_json_config,
68};
69
70struct SendPtr<T>(*mut T);
71
72// SAFETY: `py_run` has exclusive access to `LiveNode` through `&mut self`.
73#[allow(unsafe_code)]
74unsafe impl<T> Send for SendPtr<T> {}
75
76#[pyo3_stub_gen::derive::gen_stub_pymethods]
77#[pymethods]
78impl LiveNode {
79    #[staticmethod]
80    #[pyo3(name = "build")]
81    #[pyo3(signature = (name, config=None))]
82    fn py_build(name: String, config: Option<LiveNodeConfig>) -> PyResult<Self> {
83        Self::build(name, config).map_err(to_pyruntime_err)
84    }
85
86    #[staticmethod]
87    #[pyo3(name = "builder")]
88    fn py_builder(
89        name: String,
90        trader_id: TraderId,
91        environment: Environment,
92    ) -> PyResult<LiveNodeBuilderPy> {
93        match Self::builder(trader_id, environment) {
94            Ok(builder) => Ok(LiveNodeBuilderPy {
95                inner: Rc::new(RefCell::new(Some(builder.with_name(name)))),
96            }),
97            Err(e) => Err(to_pyruntime_err(e)),
98        }
99    }
100
101    #[getter]
102    #[pyo3(name = "environment")]
103    fn py_environment(&self) -> Environment {
104        self.environment()
105    }
106
107    #[getter]
108    #[pyo3(name = "trader_id")]
109    fn py_trader_id(&self) -> TraderId {
110        self.trader_id()
111    }
112
113    #[getter]
114    #[pyo3(name = "instance_id")]
115    const fn py_instance_id(&self) -> UUID4 {
116        self.instance_id()
117    }
118
119    #[getter]
120    #[pyo3(name = "is_running")]
121    fn py_is_running(&self) -> bool {
122        self.is_running()
123    }
124
125    #[pyo3(name = "start")]
126    fn py_start(&mut self) -> PyResult<()> {
127        if self.is_running() {
128            return Err(to_pyruntime_err("LiveNode is already running"));
129        }
130
131        // Non-blocking start - just start the node in the background
132        get_runtime().block_on(async { self.start().await.map_err(to_pyruntime_err) })
133    }
134
135    #[pyo3(name = "run")]
136    fn py_run(&mut self, py: Python) -> PyResult<()> {
137        if self.is_running() {
138            return Err(to_pyruntime_err("LiveNode is already running"));
139        }
140
141        // Get a handle for coordinating with the signal checker
142        let handle = self.handle();
143
144        // Import signal module
145        let signal_module = py.import("signal")?;
146        let original_handler =
147            signal_module.call_method1("signal", (2, signal_module.getattr("SIG_DFL")?))?; // Save original SIGINT handler (signal 2)
148
149        // Set up a custom signal handler that uses our handle
150        let handle_for_signal = handle;
151        let signal_callback = new_sync_py_callback(
152            py,
153            move |_args: &pyo3::Bound<'_, PyTuple>,
154                  _kwargs: Option<&pyo3::Bound<'_, PyDict>>|
155                  -> PyResult<()> {
156                log::info!("Python signal handler called");
157                handle_for_signal.stop();
158                Ok(())
159            },
160        )?;
161
162        // Install our signal handler
163        signal_module.call_method1("signal", (2, signal_callback))?;
164
165        // Run the node and restore signal handler afterward
166        let result = run_live_node_detached(py, self);
167
168        // Restore original signal handler
169        signal_module.call_method1("signal", (2, original_handler))?;
170
171        result
172    }
173
174    #[pyo3(name = "stop")]
175    fn py_stop(&self) -> PyResult<()> {
176        if !self.is_running() {
177            return Err(to_pyruntime_err("LiveNode is not running"));
178        }
179
180        // Use the handle to signal stop - this is thread-safe and doesn't require async
181        self.handle().stop();
182        Ok(())
183    }
184
185    #[allow(
186        unsafe_code,
187        reason = "Required for Python actor component registration"
188    )]
189    #[pyo3(name = "add_actor_from_config")]
190    #[expect(clippy::needless_pass_by_value)]
191    fn py_add_actor_from_config(
192        &mut self,
193        _py: Python,
194        config: ImportableActorConfig,
195    ) -> PyResult<()> {
196        log::debug!("`add_actor_from_config` with: {config:?}");
197
198        // Extract module and class name from actor_path
199        let parts: Vec<&str> = config.actor_path.split(':').collect();
200        if parts.len() != 2 {
201            return Err(to_pyvalue_err(
202                "actor_path must be in format 'module.path:ClassName'",
203            ));
204        }
205        let (module_name, class_name) = (parts[0], parts[1]);
206
207        log::info!("Importing actor from module: {module_name} class: {class_name}");
208
209        // Phase 1: Create and configure the Python actor, extract its actor_id
210        let (python_actor, actor_id) =
211            Python::attach(|py| -> anyhow::Result<(Py<PyAny>, ActorId)> {
212                let actor_module = py
213                    .import(module_name)
214                    .map_err(|e| anyhow::anyhow!("Failed to import module {module_name}: {e}"))?;
215                let actor_class = actor_module
216                    .getattr(class_name)
217                    .map_err(|e| anyhow::anyhow!("Failed to get class {class_name}: {e}"))?;
218
219                let config_instance =
220                    create_config_instance(py, &config.config_path, &config.config)?;
221
222                let python_actor = if let Some(config_obj) = config_instance.clone() {
223                    actor_class.call1((config_obj,))?
224                } else {
225                    actor_class.call0()?
226                };
227
228                log::debug!("Created Python actor instance: {python_actor:?}");
229
230                let mut py_data_actor_ref = python_actor
231                    .extract::<PyRefMut<PyDataActor>>()
232                    .map_err(Into::<PyErr>::into)
233                    .map_err(|e| anyhow::anyhow!("Failed to extract PyDataActor: {e}"))?;
234
235                // Extract inherited config fields from the Python config
236                if let Some(config_obj) = config_instance.as_ref() {
237                    if let Ok(actor_id) = config_obj.getattr("actor_id")
238                        && !actor_id.is_none()
239                    {
240                        let actor_id_val = if let Ok(aid) = actor_id.extract::<ActorId>() {
241                            aid
242                        } else if let Ok(aid_str) = actor_id.extract::<String>() {
243                            ActorId::new_checked(&aid_str)?
244                        } else {
245                            anyhow::bail!("Invalid `actor_id` type");
246                        };
247                        py_data_actor_ref.set_actor_id(actor_id_val);
248                    }
249
250                    if let Some(val) = extract_bool_config_attr(config_obj, "log_events") {
251                        py_data_actor_ref.set_log_events(val);
252                    }
253
254                    if let Some(val) = extract_bool_config_attr(config_obj, "log_commands") {
255                        py_data_actor_ref.set_log_commands(val);
256                    }
257                }
258
259                py_data_actor_ref.set_python_instance(python_actor.clone().unbind());
260
261                let actor_id = py_data_actor_ref.actor_id();
262
263                Ok((python_actor.unbind(), actor_id))
264            })
265            .map_err(to_pyruntime_err)?;
266
267        // Validate no duplicate before any mutations
268        if self
269            .kernel()
270            .trader
271            .borrow()
272            .actor_ids()
273            .contains(&actor_id)
274        {
275            return Err(to_pyruntime_err(format!(
276                "Actor '{actor_id}' is already registered"
277            )));
278        }
279
280        // Phase 2: Create per-component clock via the trader.
281        // This requires `&mut self` access to the kernel, which cannot be held
282        // inside a `Python::attach` block, hence the separate phases.
283        let trader_id = self.kernel().trader_id();
284        let cache = self.kernel().cache();
285        let component_id = ComponentId::new(actor_id.inner().as_str());
286        let clock = self
287            .kernel_mut()
288            .trader
289            .borrow_mut()
290            .create_component_clock(component_id);
291
292        // Phase 3: Register the actor with its dedicated clock
293        Python::attach(|py| -> anyhow::Result<()> {
294            let py_actor = python_actor.bind(py);
295            let mut py_data_actor_ref = py_actor
296                .extract::<PyRefMut<PyDataActor>>()
297                .map_err(Into::<PyErr>::into)
298                .map_err(|e| anyhow::anyhow!("Failed to extract PyDataActor: {e}"))?;
299
300            py_data_actor_ref
301                .register(trader_id, clock, cache)
302                .map_err(|e| anyhow::anyhow!("Failed to register PyDataActor: {e}"))?;
303
304            log::debug!(
305                "Internal PyDataActor registered: {}, state: {:?}",
306                py_data_actor_ref.is_registered(),
307                py_data_actor_ref.state()
308            );
309
310            Ok(())
311        })
312        .map_err(to_pyruntime_err)?;
313
314        // Phase 4: Register in global registries and track for lifecycle
315        Python::attach(|py| -> anyhow::Result<()> {
316            let py_actor = python_actor.bind(py);
317            let py_data_actor_ref = py_actor
318                .cast::<PyDataActor>()
319                .map_err(|e| anyhow::anyhow!("Failed to downcast to PyDataActor: {e}"))?;
320            py_data_actor_ref.borrow().register_in_global_registries();
321            Ok(())
322        })
323        .map_err(to_pyruntime_err)?;
324
325        self.kernel_mut()
326            .trader
327            .borrow_mut()
328            .add_actor_id_for_lifecycle(actor_id)
329            .map_err(to_pyruntime_err)?;
330
331        log::info!("Registered Python actor {actor_id}");
332        Ok(())
333    }
334
335    #[allow(
336        unsafe_code,
337        reason = "Required for Python strategy component registration"
338    )]
339    #[pyo3(name = "add_strategy_from_config")]
340    #[expect(clippy::needless_pass_by_value)]
341    fn py_add_strategy_from_config(
342        &mut self,
343        _py: Python,
344        config: ImportableStrategyConfig,
345    ) -> PyResult<()> {
346        log::debug!("`add_strategy_from_config` with: {config:?}");
347
348        // Extract module and class name from strategy_path
349        let parts: Vec<&str> = config.strategy_path.split(':').collect();
350        if parts.len() != 2 {
351            return Err(to_pyvalue_err(
352                "strategy_path must be in format 'module.path:ClassName'",
353            ));
354        }
355        let (module_name, class_name) = (parts[0], parts[1]);
356
357        log::info!("Importing strategy from module: {module_name} class: {class_name}");
358
359        // Phase 1: Create and configure the Python strategy, extract its strategy_id
360        let (python_strategy, strategy_id) =
361            Python::attach(|py| -> anyhow::Result<(Py<PyAny>, StrategyId)> {
362                let strategy_module = py
363                    .import(module_name)
364                    .map_err(|e| anyhow::anyhow!("Failed to import module {module_name}: {e}"))?;
365                let strategy_class = strategy_module
366                    .getattr(class_name)
367                    .map_err(|e| anyhow::anyhow!("Failed to get class {class_name}: {e}"))?;
368
369                let config_instance =
370                    create_config_instance(py, &config.config_path, &config.config)?;
371
372                let python_strategy = if let Some(config_obj) = config_instance.clone() {
373                    strategy_class.call1((config_obj,))?
374                } else {
375                    strategy_class.call0()?
376                };
377
378                log::debug!("Created Python strategy instance: {python_strategy:?}");
379
380                let mut py_strategy_ref = python_strategy
381                    .extract::<PyRefMut<PyStrategy>>()
382                    .map_err(Into::<PyErr>::into)
383                    .map_err(|e| anyhow::anyhow!("Failed to extract PyStrategy: {e}"))?;
384
385                // Extract inherited config fields from the Python config
386                if let Some(config_obj) = config_instance.as_ref() {
387                    if let Ok(strategy_id) = config_obj.getattr("strategy_id")
388                        && !strategy_id.is_none()
389                    {
390                        let strategy_id_val = if let Ok(sid) = strategy_id.extract::<StrategyId>() {
391                            sid
392                        } else if let Ok(sid_str) = strategy_id.extract::<String>() {
393                            StrategyId::new_checked(&sid_str)?
394                        } else {
395                            anyhow::bail!("Invalid `strategy_id` type");
396                        };
397                        py_strategy_ref.set_strategy_id(strategy_id_val)?;
398                    }
399
400                    if let Ok(order_id_tag) = config_obj.getattr("order_id_tag")
401                        && !order_id_tag.is_none()
402                    {
403                        let order_id_tag_val = order_id_tag
404                            .extract::<String>()
405                            .map_err(|e| anyhow::anyhow!("Invalid `order_id_tag` type: {e}"))?;
406                        py_strategy_ref.set_order_id_tag(&order_id_tag_val)?;
407                    }
408
409                    if let Some(val) = extract_bool_config_attr(config_obj, "log_events") {
410                        py_strategy_ref.set_log_events(val);
411                    }
412
413                    if let Some(val) = extract_bool_config_attr(config_obj, "log_commands") {
414                        py_strategy_ref.set_log_commands(val);
415                    }
416
417                    if let Some(claims) = extract_external_order_claims_config_attr(config_obj)? {
418                        py_strategy_ref.set_external_order_claims(Some(claims));
419                    }
420                }
421
422                py_strategy_ref.set_python_instance(python_strategy.clone().unbind());
423
424                let strategy_id = py_strategy_ref.strategy_id();
425
426                Ok((python_strategy.unbind(), strategy_id))
427            })
428            .map_err(to_pyruntime_err)?;
429
430        // Validate no duplicate before any mutations
431        if self
432            .kernel()
433            .trader
434            .borrow()
435            .strategy_ids()
436            .contains(&strategy_id)
437        {
438            return Err(to_pyruntime_err(format!(
439                "Strategy '{strategy_id}' is already registered"
440            )));
441        }
442
443        // Phase 2: Create per-component clock via the trader.
444        // This requires `&mut self` access to the kernel, which cannot be held
445        // inside a `Python::attach` block, hence the separate phases.
446        let trader_id = self.kernel().trader_id();
447        let cache = self.kernel().cache();
448        let portfolio = self.kernel().portfolio.clone();
449        let component_id = ComponentId::new(strategy_id.inner().as_str());
450        let clock = self
451            .kernel_mut()
452            .trader
453            .borrow_mut()
454            .create_component_clock(component_id);
455
456        // Phase 3: Register the strategy with its dedicated clock
457        Python::attach(|py| -> anyhow::Result<()> {
458            let py_strategy = python_strategy.bind(py);
459            let mut py_strategy_ref = py_strategy
460                .extract::<PyRefMut<PyStrategy>>()
461                .map_err(Into::<PyErr>::into)
462                .map_err(|e| anyhow::anyhow!("Failed to extract PyStrategy: {e}"))?;
463
464            py_strategy_ref
465                .register(trader_id, clock, cache, portfolio)
466                .map_err(|e| anyhow::anyhow!("Failed to register PyStrategy: {e}"))?;
467
468            log::debug!(
469                "Internal PyStrategy registered: {}",
470                py_strategy_ref.is_registered()
471            );
472
473            Ok(())
474        })
475        .map_err(to_pyruntime_err)?;
476
477        // Phase 4: Register in global registries and install event subscriptions
478        Python::attach(|py| -> anyhow::Result<()> {
479            let py_strategy = python_strategy.bind(py);
480            let py_strategy_ref = py_strategy
481                .cast::<PyStrategy>()
482                .map_err(|e| anyhow::anyhow!("Failed to downcast to PyStrategy: {e}"))?;
483            py_strategy_ref.borrow().register_in_global_registries();
484            Ok(())
485        })
486        .map_err(to_pyruntime_err)?;
487
488        let external_order_claims = Python::attach(|py| -> anyhow::Result<Option<Vec<_>>> {
489            let py_strategy = python_strategy.bind(py);
490            let py_strategy_ref = py_strategy
491                .extract::<PyRef<PyStrategy>>()
492                .map_err(Into::<PyErr>::into)
493                .map_err(|e| anyhow::anyhow!("Failed to extract PyStrategy: {e}"))?;
494
495            Ok(py_strategy_ref.external_order_claims())
496        })
497        .map_err(to_pyruntime_err)?;
498
499        if let Some(claims) = external_order_claims.filter(|claims| !claims.is_empty()) {
500            for instrument_id in &claims {
501                self.exec_manager_mut()
502                    .claim_external_orders(*instrument_id, strategy_id)
503                    .map_err(to_pyruntime_err)?;
504            }
505            log::info!("Registered external order claims for {strategy_id}: {claims:?}");
506        }
507
508        self.kernel_mut()
509            .trader
510            .borrow_mut()
511            .add_strategy_id_with_subscriptions::<PyStrategyInner>(strategy_id)
512            .map_err(to_pyruntime_err)?;
513
514        log::info!("Registered Python strategy {strategy_id}");
515        Ok(())
516    }
517
518    #[allow(
519        unsafe_code,
520        reason = "Required for Python exec algorithm component registration"
521    )]
522    #[pyo3(name = "add_exec_algorithm_from_config")]
523    #[expect(clippy::needless_pass_by_value)]
524    fn py_add_exec_algorithm_from_config(
525        &mut self,
526        _py: Python,
527        config: ImportableExecAlgorithmConfig,
528    ) -> PyResult<()> {
529        if self.is_running() {
530            return Err(to_pyruntime_err(
531                "Cannot add exec algorithm while node is running",
532            ));
533        }
534
535        log::debug!("`add_exec_algorithm_from_config` with: {config:?}");
536
537        let parts: Vec<&str> = config.exec_algorithm_path.split(':').collect();
538        if parts.len() != 2 {
539            return Err(to_pyvalue_err(
540                "exec_algorithm_path must be in format 'module.path:ClassName'",
541            ));
542        }
543        let (module_name, class_name) = (parts[0], parts[1]);
544
545        log::info!("Importing exec algorithm from module: {module_name} class: {class_name}");
546
547        // Phase 1: Create and configure the Python exec algorithm, extract its actor_id
548        let (python_exec_algorithm, actor_id) =
549            Python::attach(|py| -> anyhow::Result<(Py<PyAny>, ActorId)> {
550                let algo_module = py
551                    .import(module_name)
552                    .map_err(|e| anyhow::anyhow!("Failed to import module {module_name}: {e}"))?;
553                let algo_class = algo_module
554                    .getattr(class_name)
555                    .map_err(|e| anyhow::anyhow!("Failed to get class {class_name}: {e}"))?;
556
557                let config_instance =
558                    create_config_instance(py, &config.config_path, &config.config)?;
559
560                let python_exec_algorithm = if let Some(config_obj) = config_instance.clone() {
561                    algo_class.call1((config_obj,))?
562                } else {
563                    algo_class.call0()?
564                };
565
566                log::debug!("Created Python exec algorithm instance: {python_exec_algorithm:?}");
567
568                let mut py_data_actor_ref = python_exec_algorithm
569                    .extract::<PyRefMut<PyDataActor>>()
570                    .map_err(Into::<PyErr>::into)
571                    .map_err(|e| anyhow::anyhow!("Failed to extract PyDataActor: {e}"))?;
572
573                // Extract ID from config: prefer exec_algorithm_id, fall back to actor_id
574                if let Some(config_obj) = config_instance.as_ref() {
575                    let id_attr = config_obj
576                        .getattr("exec_algorithm_id")
577                        .ok()
578                        .filter(|v| !v.is_none())
579                        .or_else(|| config_obj.getattr("actor_id").ok().filter(|v| !v.is_none()));
580
581                    if let Some(id_value) = id_attr {
582                        let actor_id_val = if let Ok(eaid) = id_value.extract::<ExecAlgorithmId>() {
583                            ActorId::new(eaid.inner().as_str())
584                        } else if let Ok(aid) = id_value.extract::<ActorId>() {
585                            aid
586                        } else if let Ok(aid_str) = id_value.extract::<String>() {
587                            ActorId::new_checked(&aid_str)?
588                        } else {
589                            anyhow::bail!("Invalid `exec_algorithm_id`/`actor_id` type");
590                        };
591                        py_data_actor_ref.set_actor_id(actor_id_val);
592                    }
593
594                    if let Some(val) = extract_bool_config_attr(config_obj, "log_events") {
595                        py_data_actor_ref.set_log_events(val);
596                    }
597
598                    if let Some(val) = extract_bool_config_attr(config_obj, "log_commands") {
599                        py_data_actor_ref.set_log_commands(val);
600                    }
601                }
602
603                py_data_actor_ref.set_python_instance(python_exec_algorithm.clone().unbind());
604
605                let actor_id = py_data_actor_ref.actor_id();
606
607                Ok((python_exec_algorithm.unbind(), actor_id))
608            })
609            .map_err(to_pyruntime_err)?;
610
611        let exec_algorithm_id = ExecAlgorithmId::from(actor_id.inner().as_str());
612
613        if self
614            .kernel()
615            .trader
616            .borrow()
617            .exec_algorithm_ids()
618            .contains(&exec_algorithm_id)
619        {
620            return Err(to_pyruntime_err(format!(
621                "Execution algorithm '{exec_algorithm_id}' is already registered"
622            )));
623        }
624
625        // Phase 2: Create per-component clock via the trader.
626        // This requires `&mut self` access to the kernel, which cannot be held
627        // inside a `Python::attach` block, hence the separate phases.
628        let trader_id = self.kernel().trader_id();
629        let cache = self.kernel().cache();
630        let component_id = ComponentId::new(actor_id.inner().as_str());
631        let clock = self
632            .kernel_mut()
633            .trader
634            .borrow_mut()
635            .create_component_clock(component_id);
636
637        // Phase 3: Register the exec algorithm with its dedicated clock
638        Python::attach(|py| -> anyhow::Result<()> {
639            let py_algo = python_exec_algorithm.bind(py);
640            let mut py_data_actor_ref = py_algo
641                .extract::<PyRefMut<PyDataActor>>()
642                .map_err(Into::<PyErr>::into)
643                .map_err(|e| anyhow::anyhow!("Failed to extract PyDataActor: {e}"))?;
644
645            py_data_actor_ref
646                .register(trader_id, clock, cache)
647                .map_err(|e| anyhow::anyhow!("Failed to register PyDataActor: {e}"))?;
648
649            log::debug!(
650                "Internal PyDataActor registered: {}, state: {:?}",
651                py_data_actor_ref.is_registered(),
652                py_data_actor_ref.state()
653            );
654
655            Ok(())
656        })
657        .map_err(to_pyruntime_err)?;
658
659        // Phase 4: Register in global registries and track for lifecycle
660        Python::attach(|py| -> anyhow::Result<()> {
661            let py_algo = python_exec_algorithm.bind(py);
662            let py_data_actor_ref = py_algo
663                .cast::<PyDataActor>()
664                .map_err(|e| anyhow::anyhow!("Failed to downcast to PyDataActor: {e}"))?;
665            py_data_actor_ref.borrow().register_in_global_registries();
666            Ok(())
667        })
668        .map_err(to_pyruntime_err)?;
669
670        register_python_exec_algorithm_endpoint(exec_algorithm_id);
671
672        self.kernel_mut()
673            .trader
674            .borrow_mut()
675            .add_exec_algorithm_id_for_lifecycle(exec_algorithm_id)
676            .map_err(to_pyruntime_err)?;
677
678        log::info!("Registered Python exec algorithm {exec_algorithm_id}");
679        Ok(())
680    }
681
682    /// Adds a Rust-native plug-in component from a cdylib.
683    #[pyo3(name = "add_plugin", signature = (path, type_name, config=None, sha256=None))]
684    fn py_add_plugin(
685        &mut self,
686        path: String,
687        type_name: String,
688        config: Option<HashMap<String, Py<PyAny>>>,
689        sha256: Option<String>,
690    ) -> PyResult<()> {
691        let config = PluginConfig {
692            path,
693            type_name,
694            config: match config {
695                Some(config) => coerce_json_config(config)?,
696                None => HashMap::new(),
697            },
698            sha256,
699        };
700
701        self.add_plugin(config).map_err(to_pyruntime_err)
702    }
703
704    /// Adds a built-in example actor from its type name and config.
705    ///
706    /// This method exists only to single-source bundled example actor code across
707    /// Rust and Python tests/examples. It is not a first-class extension path for
708    /// adding native actors.
709    #[cfg(feature = "examples")]
710    #[pyo3(name = "add_builtin_actor")]
711    fn py_add_builtin_actor(&mut self, type_name: &str, config: &Bound<'_, PyAny>) -> PyResult<()> {
712        let register = builtin_actor_register(type_name).ok_or_else(|| {
713            to_pytype_err(format!("Unsupported built-in actor type: {type_name}"))
714        })?;
715        register(self, config)
716    }
717
718    /// Adds a built-in example strategy from its type name and config.
719    ///
720    /// This method exists only to single-source bundled example strategy code across
721    /// Rust and Python tests/examples. It is not a first-class extension path for
722    /// adding native strategies.
723    #[cfg(feature = "examples")]
724    #[pyo3(name = "add_builtin_strategy")]
725    fn py_add_builtin_strategy(
726        &mut self,
727        type_name: &str,
728        config: &Bound<'_, PyAny>,
729    ) -> PyResult<()> {
730        let register = builtin_strategy_register(type_name).ok_or_else(|| {
731            to_pytype_err(format!("Unsupported built-in strategy type: {type_name}"))
732        })?;
733        register(self, config)
734    }
735
736    fn __repr__(&self) -> String {
737        format!(
738            "LiveNode(trader_id={}, environment={:?}, running={})",
739            self.trader_id(),
740            self.environment(),
741            self.is_running()
742        )
743    }
744}
745
746fn new_sync_py_callback<F>(py: Python<'_>, closure: F) -> PyResult<Bound<'_, PyCFunction>>
747where
748    F: Fn(&Bound<'_, PyTuple>, Option<&Bound<'_, PyDict>>) -> PyResult<()> + Send + Sync + 'static,
749{
750    PyCFunction::new_closure(py, None, None, closure)
751}
752
753#[allow(unsafe_code)]
754fn run_live_node_detached(py: Python<'_>, node: &mut LiveNode) -> PyResult<()> {
755    let node_ptr = SendPtr(std::ptr::from_mut::<LiveNode>(node));
756
757    // SAFETY: `py_run` holds the only mutable reference to `LiveNode` until
758    // `run()` returns, and the detached closure completes before `py_run` can
759    // access `node` again.
760    unsafe {
761        py.detach(move || {
762            let ptr = node_ptr;
763            get_runtime().block_on(async { (*ptr.0).run().await })
764        })
765    }
766    .map_err(to_pyruntime_err)
767}
768
769#[cfg(feature = "examples")]
770type BuiltinActorRegister = for<'py> fn(&mut LiveNode, &Bound<'py, PyAny>) -> PyResult<()>;
771
772#[cfg(feature = "examples")]
773type BuiltinStrategyRegister = for<'py> fn(&mut LiveNode, &Bound<'py, PyAny>) -> PyResult<()>;
774
775#[cfg(feature = "examples")]
776fn builtin_actor_register(type_name: &str) -> Option<BuiltinActorRegister> {
777    match type_name {
778        "BookImbalanceActor" => Some(register_book_imbalance_actor),
779        "DataTester" => Some(register_data_tester),
780        _ => None,
781    }
782}
783
784#[cfg(feature = "examples")]
785fn builtin_strategy_register(type_name: &str) -> Option<BuiltinStrategyRegister> {
786    match type_name {
787        "CompositeMarketMaker" => Some(register_composite_market_maker),
788        "DeltaNeutralVol" => Some(register_delta_neutral_vol),
789        "EmaCross" => Some(register_ema_cross),
790        "ExecTester" => Some(register_exec_tester),
791        "GridMarketMaker" => Some(register_grid_market_maker),
792        "HurstVpinDirectional" => Some(register_hurst_vpin_directional),
793        _ => None,
794    }
795}
796
797#[cfg(feature = "examples")]
798fn register_composite_market_maker(node: &mut LiveNode, config: &Bound<'_, PyAny>) -> PyResult<()> {
799    let config = config.extract::<CompositeMarketMakerConfig>()?;
800    node.add_strategy(CompositeMarketMaker::new(config))
801        .map_err(to_pyruntime_err)
802}
803
804#[cfg(feature = "examples")]
805fn register_delta_neutral_vol(node: &mut LiveNode, config: &Bound<'_, PyAny>) -> PyResult<()> {
806    let config = config.extract::<DeltaNeutralVolConfig>()?;
807    node.add_strategy(DeltaNeutralVol::new(config))
808        .map_err(to_pyruntime_err)
809}
810
811#[cfg(feature = "examples")]
812fn register_ema_cross(node: &mut LiveNode, config: &Bound<'_, PyAny>) -> PyResult<()> {
813    let config = config.extract::<EmaCrossConfig>()?;
814    node.add_strategy(EmaCross::from_config(config))
815        .map_err(to_pyruntime_err)
816}
817
818#[cfg(feature = "examples")]
819fn register_exec_tester(node: &mut LiveNode, config: &Bound<'_, PyAny>) -> PyResult<()> {
820    let config = config.extract::<ExecTesterConfig>()?;
821    node.add_strategy(ExecTester::new(config))
822        .map_err(to_pyruntime_err)
823}
824
825#[cfg(feature = "examples")]
826fn register_grid_market_maker(node: &mut LiveNode, config: &Bound<'_, PyAny>) -> PyResult<()> {
827    let config = config.extract::<GridMarketMakerConfig>()?;
828    node.add_strategy(GridMarketMaker::new(config))
829        .map_err(to_pyruntime_err)
830}
831
832#[cfg(feature = "examples")]
833fn register_hurst_vpin_directional(node: &mut LiveNode, config: &Bound<'_, PyAny>) -> PyResult<()> {
834    let config = config.extract::<HurstVpinDirectionalConfig>()?;
835    node.add_strategy(HurstVpinDirectional::new(config))
836        .map_err(to_pyruntime_err)
837}
838
839#[cfg(feature = "examples")]
840fn register_book_imbalance_actor(node: &mut LiveNode, config: &Bound<'_, PyAny>) -> PyResult<()> {
841    let config = config.extract::<BookImbalanceActorConfig>()?;
842    node.add_actor(BookImbalanceActor::from_config(config))
843        .map_err(to_pyruntime_err)
844}
845
846#[cfg(feature = "examples")]
847fn register_data_tester(node: &mut LiveNode, config: &Bound<'_, PyAny>) -> PyResult<()> {
848    let config = config.extract::<DataTesterConfig>()?;
849    node.add_actor(DataTester::new(config))
850        .map_err(to_pyruntime_err)
851}
852
853/// Python wrapper for `LiveNodeBuilder` that uses interior mutability
854/// to work around PyO3's shared ownership model.
855#[derive(Debug)]
856#[pyclass(name = "LiveNodeBuilder", module = "nautilus_trader.live", unsendable)]
857#[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.live")]
858pub struct LiveNodeBuilderPy {
859    inner: Rc<RefCell<Option<LiveNodeBuilder>>>,
860}
861
862#[pyo3_stub_gen::derive::gen_stub_pymethods]
863#[pymethods]
864impl LiveNodeBuilderPy {
865    #[pyo3(name = "with_instance_id")]
866    fn py_with_instance_id(&self, instance_id: UUID4) -> PyResult<Self> {
867        let mut inner_ref = self.inner.borrow_mut();
868        if let Some(builder) = inner_ref.take() {
869            *inner_ref = Some(builder.with_instance_id(instance_id));
870            Ok(Self {
871                inner: self.inner.clone(),
872            })
873        } else {
874            Err(to_pyruntime_err("Builder already consumed"))
875        }
876    }
877
878    #[pyo3(name = "with_load_state")]
879    fn py_with_load_state(&self, load_state: bool) -> PyResult<Self> {
880        let mut inner_ref = self.inner.borrow_mut();
881        if let Some(builder) = inner_ref.take() {
882            *inner_ref = Some(builder.with_load_state(load_state));
883            Ok(Self {
884                inner: self.inner.clone(),
885            })
886        } else {
887            Err(to_pyruntime_err("Builder already consumed"))
888        }
889    }
890
891    #[pyo3(name = "with_save_state")]
892    fn py_with_save_state(&self, save_state: bool) -> PyResult<Self> {
893        let mut inner_ref = self.inner.borrow_mut();
894        if let Some(builder) = inner_ref.take() {
895            *inner_ref = Some(builder.with_save_state(save_state));
896            Ok(Self {
897                inner: self.inner.clone(),
898            })
899        } else {
900            Err(to_pyruntime_err("Builder already consumed"))
901        }
902    }
903
904    #[pyo3(name = "with_timeout_connection")]
905    fn py_with_timeout_connection(&self, timeout_secs: u64) -> PyResult<Self> {
906        let mut inner_ref = self.inner.borrow_mut();
907        if let Some(builder) = inner_ref.take() {
908            *inner_ref = Some(builder.with_timeout_connection(timeout_secs));
909            Ok(Self {
910                inner: self.inner.clone(),
911            })
912        } else {
913            Err(to_pyruntime_err("Builder already consumed"))
914        }
915    }
916
917    #[pyo3(name = "with_timeout_reconciliation")]
918    fn py_with_timeout_reconciliation(&self, timeout_secs: u64) -> PyResult<Self> {
919        let mut inner_ref = self.inner.borrow_mut();
920        if let Some(builder) = inner_ref.take() {
921            *inner_ref = Some(builder.with_timeout_reconciliation(timeout_secs));
922            Ok(Self {
923                inner: self.inner.clone(),
924            })
925        } else {
926            Err(to_pyruntime_err("Builder already consumed"))
927        }
928    }
929
930    #[pyo3(name = "with_timeout_portfolio")]
931    fn py_with_timeout_portfolio(&self, timeout_secs: u64) -> PyResult<Self> {
932        let mut inner_ref = self.inner.borrow_mut();
933        if let Some(builder) = inner_ref.take() {
934            *inner_ref = Some(builder.with_timeout_portfolio(timeout_secs));
935            Ok(Self {
936                inner: self.inner.clone(),
937            })
938        } else {
939            Err(to_pyruntime_err("Builder already consumed"))
940        }
941    }
942
943    #[pyo3(name = "with_timeout_disconnection_secs")]
944    fn py_with_timeout_disconnection_secs(&self, timeout_secs: u64) -> PyResult<Self> {
945        let mut inner_ref = self.inner.borrow_mut();
946        if let Some(builder) = inner_ref.take() {
947            *inner_ref = Some(builder.with_timeout_disconnection_secs(timeout_secs));
948            Ok(Self {
949                inner: self.inner.clone(),
950            })
951        } else {
952            Err(to_pyruntime_err("Builder already consumed"))
953        }
954    }
955
956    #[pyo3(name = "with_delay_post_stop_secs")]
957    fn py_with_delay_post_stop_secs(&self, delay_secs: u64) -> PyResult<Self> {
958        let mut inner_ref = self.inner.borrow_mut();
959        if let Some(builder) = inner_ref.take() {
960            *inner_ref = Some(builder.with_delay_post_stop_secs(delay_secs));
961            Ok(Self {
962                inner: self.inner.clone(),
963            })
964        } else {
965            Err(to_pyruntime_err("Builder already consumed"))
966        }
967    }
968
969    #[pyo3(name = "with_delay_shutdown_secs")]
970    fn py_with_delay_shutdown_secs(&self, delay_secs: u64) -> PyResult<Self> {
971        let mut inner_ref = self.inner.borrow_mut();
972        if let Some(builder) = inner_ref.take() {
973            *inner_ref = Some(builder.with_delay_shutdown_secs(delay_secs));
974            Ok(Self {
975                inner: self.inner.clone(),
976            })
977        } else {
978            Err(to_pyruntime_err("Builder already consumed"))
979        }
980    }
981
982    #[pyo3(name = "with_reconciliation")]
983    fn py_with_reconciliation(&self, reconciliation: bool) -> PyResult<Self> {
984        let mut inner_ref = self.inner.borrow_mut();
985        if let Some(builder) = inner_ref.take() {
986            *inner_ref = Some(builder.with_reconciliation(reconciliation));
987            Ok(Self {
988                inner: self.inner.clone(),
989            })
990        } else {
991            Err(to_pyruntime_err("Builder already consumed"))
992        }
993    }
994
995    #[pyo3(name = "with_reconciliation_lookback_mins")]
996    fn py_with_reconciliation_lookback_mins(&self, mins: u32) -> PyResult<Self> {
997        let mut inner_ref = self.inner.borrow_mut();
998        if let Some(builder) = inner_ref.take() {
999            *inner_ref = Some(builder.with_reconciliation_lookback_mins(mins));
1000            Ok(Self {
1001                inner: self.inner.clone(),
1002            })
1003        } else {
1004            Err(to_pyruntime_err("Builder already consumed"))
1005        }
1006    }
1007
1008    #[pyo3(name = "with_cache_config")]
1009    fn py_with_cache_config(&self, config: CacheConfig) -> PyResult<Self> {
1010        let mut inner_ref = self.inner.borrow_mut();
1011        if let Some(builder) = inner_ref.take() {
1012            *inner_ref = Some(builder.with_cache_config(config));
1013            Ok(Self {
1014                inner: self.inner.clone(),
1015            })
1016        } else {
1017            Err(to_pyruntime_err("Builder already consumed"))
1018        }
1019    }
1020
1021    #[pyo3(name = "with_portfolio_config")]
1022    fn py_with_portfolio_config(&self, config: PortfolioConfig) -> PyResult<Self> {
1023        let mut inner_ref = self.inner.borrow_mut();
1024        if let Some(builder) = inner_ref.take() {
1025            *inner_ref = Some(builder.with_portfolio_config(config));
1026            Ok(Self {
1027                inner: self.inner.clone(),
1028            })
1029        } else {
1030            Err(to_pyruntime_err("Builder already consumed"))
1031        }
1032    }
1033
1034    #[pyo3(name = "with_data_engine_config")]
1035    fn py_with_data_engine_config(&self, config: LiveDataEngineConfig) -> PyResult<Self> {
1036        let mut inner_ref = self.inner.borrow_mut();
1037        if let Some(builder) = inner_ref.take() {
1038            *inner_ref = Some(builder.with_data_engine_config(config));
1039            Ok(Self {
1040                inner: self.inner.clone(),
1041            })
1042        } else {
1043            Err(to_pyruntime_err("Builder already consumed"))
1044        }
1045    }
1046
1047    #[pyo3(name = "with_risk_engine_config")]
1048    fn py_with_risk_engine_config(&self, config: LiveRiskEngineConfig) -> PyResult<Self> {
1049        let mut inner_ref = self.inner.borrow_mut();
1050        if let Some(builder) = inner_ref.take() {
1051            *inner_ref = Some(builder.with_risk_engine_config(config));
1052            Ok(Self {
1053                inner: self.inner.clone(),
1054            })
1055        } else {
1056            Err(to_pyruntime_err("Builder already consumed"))
1057        }
1058    }
1059
1060    #[pyo3(name = "with_exec_engine_config")]
1061    fn py_with_exec_engine_config(&self, config: LiveExecEngineConfig) -> PyResult<Self> {
1062        let mut inner_ref = self.inner.borrow_mut();
1063        if let Some(builder) = inner_ref.take() {
1064            *inner_ref = Some(builder.with_exec_engine_config(config));
1065            Ok(Self {
1066                inner: self.inner.clone(),
1067            })
1068        } else {
1069            Err(to_pyruntime_err("Builder already consumed"))
1070        }
1071    }
1072
1073    #[pyo3(name = "with_logging")]
1074    fn py_with_logging(&self, logging: LoggerConfig) -> PyResult<Self> {
1075        let mut inner_ref = self.inner.borrow_mut();
1076        if let Some(builder) = inner_ref.take() {
1077            *inner_ref = Some(builder.with_logging(logging));
1078            Ok(Self {
1079                inner: self.inner.clone(),
1080            })
1081        } else {
1082            Err(to_pyruntime_err("Builder already consumed"))
1083        }
1084    }
1085
1086    #[pyo3(name = "add_data_client")]
1087    #[expect(clippy::needless_pass_by_value)]
1088    fn py_add_data_client(
1089        &self,
1090        name: Option<String>,
1091        factory: Py<PyAny>,
1092        config: Py<PyAny>,
1093    ) -> PyResult<Self> {
1094        let mut inner_ref = self.inner.borrow_mut();
1095        if let Some(builder) = inner_ref.take() {
1096            Python::attach(|py| -> PyResult<Self> {
1097                // Use the global registry to extract Py<PyAny>s to trait objects
1098                let registry = get_global_pyo3_registry();
1099
1100                let boxed_factory = registry.extract_factory(py, factory.clone_ref(py))?;
1101                let boxed_config = registry.extract_config(py, config.clone_ref(py))?;
1102
1103                // Use the factory name from the original factory for the client name
1104                let factory_name = factory
1105                    .getattr(py, "name")?
1106                    .call0(py)?
1107                    .extract::<String>(py)?;
1108                let client_name = name.unwrap_or(factory_name);
1109
1110                // Add the data client to the builder using boxed trait objects
1111                match builder.add_data_client(Some(client_name), boxed_factory, boxed_config) {
1112                    Ok(updated_builder) => {
1113                        *inner_ref = Some(updated_builder);
1114                        Ok(Self {
1115                            inner: self.inner.clone(),
1116                        })
1117                    }
1118                    Err(e) => Err(to_pyruntime_err(format!("Failed to add data client: {e}"))),
1119                }
1120            })
1121        } else {
1122            Err(to_pyruntime_err("Builder already consumed"))
1123        }
1124    }
1125
1126    #[pyo3(name = "add_exec_client")]
1127    #[expect(clippy::needless_pass_by_value)]
1128    fn py_add_exec_client(
1129        &self,
1130        name: Option<String>,
1131        factory: Py<PyAny>,
1132        config: Py<PyAny>,
1133    ) -> PyResult<Self> {
1134        let mut inner_ref = self.inner.borrow_mut();
1135        if let Some(builder) = inner_ref.take() {
1136            Python::attach(|py| -> PyResult<Self> {
1137                let registry = get_global_pyo3_registry();
1138
1139                let boxed_factory = registry.extract_exec_factory(py, factory.clone_ref(py))?;
1140                let boxed_config = registry.extract_config(py, config.clone_ref(py))?;
1141
1142                let factory_name = factory
1143                    .getattr(py, "name")?
1144                    .call0(py)?
1145                    .extract::<String>(py)?;
1146                let client_name = name.unwrap_or(factory_name);
1147
1148                match builder.add_exec_client(Some(client_name), boxed_factory, boxed_config) {
1149                    Ok(updated_builder) => {
1150                        *inner_ref = Some(updated_builder);
1151                        Ok(Self {
1152                            inner: self.inner.clone(),
1153                        })
1154                    }
1155                    Err(e) => Err(to_pyruntime_err(format!("Failed to add exec client: {e}"))),
1156                }
1157            })
1158        } else {
1159            Err(to_pyruntime_err("Builder already consumed"))
1160        }
1161    }
1162
1163    #[pyo3(name = "add_simulated_exec_client")]
1164    #[expect(clippy::needless_pass_by_value)]
1165    fn py_add_simulated_exec_client(
1166        &self,
1167        name: Option<String>,
1168        factory: Py<PyAny>,
1169        config: Py<PyAny>,
1170    ) -> PyResult<Self> {
1171        let mut inner_ref = self.inner.borrow_mut();
1172        if let Some(builder) = inner_ref.take() {
1173            Python::attach(|py| -> PyResult<Self> {
1174                let registry = get_global_pyo3_registry();
1175
1176                let boxed_factory = registry.extract_sim_exec_factory(py, factory.clone_ref(py))?;
1177                let boxed_config = registry.extract_config(py, config.clone_ref(py))?;
1178
1179                let factory_name = factory
1180                    .getattr(py, "name")?
1181                    .call0(py)?
1182                    .extract::<String>(py)?;
1183                let client_name = name.unwrap_or(factory_name);
1184
1185                match builder.add_simulated_exec_client(
1186                    Some(client_name),
1187                    boxed_factory,
1188                    boxed_config,
1189                ) {
1190                    Ok(updated_builder) => {
1191                        *inner_ref = Some(updated_builder);
1192                        Ok(Self {
1193                            inner: self.inner.clone(),
1194                        })
1195                    }
1196                    Err(e) => Err(to_pyruntime_err(format!(
1197                        "Failed to add simulated exec client: {e}"
1198                    ))),
1199                }
1200            })
1201        } else {
1202            Err(to_pyruntime_err("Builder already consumed"))
1203        }
1204    }
1205
1206    #[pyo3(name = "build")]
1207    fn py_build(&self) -> PyResult<LiveNode> {
1208        let mut inner_ref = self.inner.borrow_mut();
1209        if let Some(builder) = inner_ref.take() {
1210            match builder.build() {
1211                Ok(node) => Ok(node),
1212                Err(e) => Err(to_pyruntime_err(e)),
1213            }
1214        } else {
1215            Err(to_pyruntime_err("Builder already consumed"))
1216        }
1217    }
1218
1219    fn __repr__(&self) -> String {
1220        format!("{self:?}")
1221    }
1222}
1223
1224/// Creates a Python config instance from a config path and config dictionary.
1225///
1226/// This helper is shared between `add_actor_from_config` and `add_strategy_from_config`.
1227/// It handles:
1228/// 1. Importing the config class from the module path
1229/// 2. Converting the `HashMap<String, serde_json::Value>` to a Python dict
1230/// 3. Trying kwargs-first construction, falling back to default + setattr
1231/// 4. Calling `__post_init__` for dataclasses when using the setattr path
1232fn create_config_instance<'py>(
1233    py: Python<'py>,
1234    config_path: &str,
1235    config: &HashMap<String, serde_json::Value>,
1236) -> anyhow::Result<Option<Bound<'py, PyAny>>> {
1237    if config_path.is_empty() && config.is_empty() {
1238        log::debug!("No config_path or empty config, using None");
1239        return Ok(None);
1240    }
1241
1242    let config_parts: Vec<&str> = config_path.split(':').collect();
1243    if config_parts.len() != 2 {
1244        anyhow::bail!("config_path must be in format 'module.path:ClassName', was {config_path}");
1245    }
1246    let (config_module_name, config_class_name) = (config_parts[0], config_parts[1]);
1247
1248    log::debug!(
1249        "Importing config class from module: {config_module_name} class: {config_class_name}"
1250    );
1251
1252    let config_module = py
1253        .import(config_module_name)
1254        .map_err(|e| anyhow::anyhow!("Failed to import config module {config_module_name}: {e}"))?;
1255    let config_class = config_module
1256        .getattr(config_class_name)
1257        .map_err(|e| anyhow::anyhow!("Failed to get config class {config_class_name}: {e}"))?;
1258
1259    // Convert config dict to Python dict
1260    let py_dict = PyDict::new(py);
1261
1262    for (key, value) in config {
1263        let py_value = config_value_to_py(py, key, value)?;
1264        py_dict.set_item(key, py_value)?;
1265    }
1266
1267    log::debug!("Created config dict: {py_dict:?}");
1268
1269    // Try kwargs first, then default constructor with setattr
1270    let config_instance = match config_class.call((), Some(&py_dict)) {
1271        Ok(instance) => {
1272            log::debug!("Created config instance with kwargs");
1273            instance
1274        }
1275        Err(kwargs_err) => {
1276            log::debug!("Failed to create config with kwargs: {kwargs_err}");
1277
1278            match config_class.call0() {
1279                Ok(instance) => {
1280                    log::debug!("Created default config instance, setting attributes");
1281                    for (key, value) in config {
1282                        let py_value = config_value_to_py(py, key, value)?;
1283
1284                        if let Err(setattr_err) = instance.setattr(key, py_value) {
1285                            log::warn!("Failed to set attribute {key}: {setattr_err}");
1286                        }
1287                    }
1288
1289                    // Only call __post_init__ if it exists (setattr path
1290                    // needs it, kwargs path already triggered it via __init__)
1291                    if instance.hasattr("__post_init__")? {
1292                        instance.call_method0("__post_init__")?;
1293                    }
1294
1295                    instance
1296                }
1297                Err(default_err) => {
1298                    anyhow::bail!(
1299                        "Failed to create config instance. \
1300                         Tried kwargs: {kwargs_err}, default: {default_err}"
1301                    );
1302                }
1303            }
1304        }
1305    };
1306
1307    log::debug!("Created config instance: {config_instance:?}");
1308
1309    Ok(Some(config_instance))
1310}
1311
1312fn config_value_to_py<'py>(
1313    py: Python<'py>,
1314    key: &str,
1315    value: &serde_json::Value,
1316) -> anyhow::Result<Bound<'py, PyAny>> {
1317    if key == "actor_id"
1318        && let Some(actor_id) = value.as_str()
1319    {
1320        return Ok(ActorId::new_checked(actor_id)?
1321            .into_pyobject(py)?
1322            .into_any());
1323    }
1324
1325    let json_str = serde_json::to_string(value)
1326        .map_err(|e| anyhow::anyhow!("Failed to serialize config value: {e}"))?;
1327    Ok(PyModule::import(py, "json")?
1328        .call_method("loads", (json_str,), None)?
1329        .into_any())
1330}
1331
1332/// Extracts an optional boolean attribute from a Python config object.
1333///
1334/// Returns `None` if the attribute doesn't exist or isn't a bool,
1335/// without raising an error (config fields are optional overrides).
1336fn extract_bool_config_attr(config_obj: &Bound<'_, PyAny>, attr: &str) -> Option<bool> {
1337    config_obj
1338        .getattr(attr)
1339        .ok()
1340        .and_then(|val| val.extract::<bool>().ok())
1341}
1342
1343fn extract_external_order_claims_config_attr(
1344    config_obj: &Bound<'_, PyAny>,
1345) -> anyhow::Result<Option<Vec<InstrumentId>>> {
1346    let Ok(claims) = config_obj.getattr("external_order_claims") else {
1347        return Ok(None);
1348    };
1349
1350    if claims.is_none() {
1351        return Ok(None);
1352    }
1353
1354    if let Ok(claims) = claims.extract::<Vec<InstrumentId>>() {
1355        return Ok(Some(claims));
1356    }
1357
1358    let claim_strings = claims
1359        .extract::<Vec<String>>()
1360        .map_err(|e| anyhow::anyhow!("Invalid `external_order_claims` type: {e}"))?;
1361    let claims = claim_strings
1362        .into_iter()
1363        .map(|claim| {
1364            InstrumentId::from_str(&claim).map_err(|e| {
1365                anyhow::anyhow!("Invalid `external_order_claims` instrument ID {claim}: {e}")
1366            })
1367        })
1368        .collect::<anyhow::Result<Vec<_>>>()?;
1369
1370    Ok(Some(claims))
1371}
1372
1373#[cfg(all(test, feature = "python"))]
1374mod tests {
1375    use std::{
1376        any::Any,
1377        cell::RefCell,
1378        collections::HashMap,
1379        ffi::CString,
1380        fmt::Debug,
1381        rc::Rc,
1382        sync::{
1383            Arc,
1384            atomic::{AtomicBool, AtomicUsize, Ordering},
1385            mpsc,
1386        },
1387        thread,
1388        time::{Duration, Instant},
1389    };
1390
1391    use async_trait::async_trait;
1392    use nautilus_common::{
1393        cache::CacheView,
1394        clients::DataClient,
1395        clock::Clock,
1396        enums::Environment,
1397        factories::{ClientConfig, DataClientFactory},
1398        live::runner::get_data_event_sender,
1399        messages::{
1400            DataEvent, DataResponse,
1401            data::{BarsResponse, RequestBars},
1402        },
1403        msgbus::get_message_bus,
1404    };
1405    use nautilus_core::UnixNanos;
1406    use nautilus_model::{
1407        data::{Bar, BarType},
1408        identifiers::{ClientId, InstrumentId, StrategyId, TraderId, Venue},
1409        types::{Price, Quantity},
1410    };
1411    use nautilus_trading::{ImportableStrategyConfig, python::strategy::PyStrategy};
1412    use pyo3::{
1413        Python,
1414        types::{PyAnyMethods, PyDict, PyModule, PyModuleMethods},
1415    };
1416    use rstest::rstest;
1417
1418    use super::LiveNode;
1419    #[derive(Debug, Default)]
1420    struct TestDataClientConfig;
1421
1422    impl ClientConfig for TestDataClientConfig {
1423        fn as_any(&self) -> &dyn Any {
1424            self
1425        }
1426    }
1427
1428    #[derive(Debug)]
1429    #[expect(
1430        clippy::struct_field_names,
1431        reason = "test counters intentionally share the count postfix"
1432    )]
1433    struct TestHistoricalBarsDataClientFactory {
1434        request_count: Arc<AtomicUsize>,
1435        response_sent_count: Arc<AtomicUsize>,
1436        handler_visible_count: Arc<AtomicUsize>,
1437    }
1438
1439    impl TestHistoricalBarsDataClientFactory {
1440        fn new(
1441            request_count: Arc<AtomicUsize>,
1442            response_sent_count: Arc<AtomicUsize>,
1443            handler_visible_count: Arc<AtomicUsize>,
1444        ) -> Self {
1445            Self {
1446                request_count,
1447                response_sent_count,
1448                handler_visible_count,
1449            }
1450        }
1451    }
1452
1453    impl DataClientFactory for TestHistoricalBarsDataClientFactory {
1454        fn create(
1455            &self,
1456            name: &str,
1457            _config: &dyn ClientConfig,
1458            _cache: CacheView,
1459            _clock: Rc<RefCell<dyn Clock>>,
1460        ) -> anyhow::Result<Box<dyn DataClient>> {
1461            Ok(Box::new(TestHistoricalBarsDataClient::new(
1462                ClientId::from(name),
1463                Venue::from("SIM"),
1464                self.request_count.clone(),
1465                self.response_sent_count.clone(),
1466                self.handler_visible_count.clone(),
1467            )))
1468        }
1469
1470        fn name(&self) -> &'static str {
1471            "TEST_DATA"
1472        }
1473
1474        fn config_type(&self) -> &'static str {
1475            "TestDataClientConfig"
1476        }
1477    }
1478
1479    #[derive(Debug)]
1480    struct TestHistoricalBarsDataClient {
1481        client_id: ClientId,
1482        venue: Venue,
1483        connected: Arc<AtomicBool>,
1484        request_count: Arc<AtomicUsize>,
1485        response_sent_count: Arc<AtomicUsize>,
1486        handler_visible_count: Arc<AtomicUsize>,
1487    }
1488
1489    impl TestHistoricalBarsDataClient {
1490        fn new(
1491            client_id: ClientId,
1492            venue: Venue,
1493            request_count: Arc<AtomicUsize>,
1494            response_sent_count: Arc<AtomicUsize>,
1495            handler_visible_count: Arc<AtomicUsize>,
1496        ) -> Self {
1497            Self {
1498                client_id,
1499                venue,
1500                connected: Arc::new(AtomicBool::new(false)),
1501                request_count,
1502                response_sent_count,
1503                handler_visible_count,
1504            }
1505        }
1506
1507        fn make_bar(bar_type: BarType) -> Bar {
1508            Bar::new(
1509                bar_type,
1510                Price::from("1.0000"),
1511                Price::from("1.1000"),
1512                Price::from("0.9000"),
1513                Price::from("1.0500"),
1514                Quantity::from("1000"),
1515                UnixNanos::from(1_700_000_000_000_000_000u64),
1516                UnixNanos::from(1_700_000_000_000_000_001u64),
1517            )
1518        }
1519    }
1520
1521    #[async_trait(?Send)]
1522    impl DataClient for TestHistoricalBarsDataClient {
1523        fn client_id(&self) -> ClientId {
1524            self.client_id
1525        }
1526
1527        fn venue(&self) -> Option<Venue> {
1528            Some(self.venue)
1529        }
1530
1531        fn start(&mut self) -> anyhow::Result<()> {
1532            Ok(())
1533        }
1534
1535        fn stop(&mut self) -> anyhow::Result<()> {
1536            Ok(())
1537        }
1538
1539        fn reset(&mut self) -> anyhow::Result<()> {
1540            Ok(())
1541        }
1542
1543        fn dispose(&mut self) -> anyhow::Result<()> {
1544            Ok(())
1545        }
1546
1547        fn is_connected(&self) -> bool {
1548            self.connected.load(Ordering::Relaxed)
1549        }
1550
1551        fn is_disconnected(&self) -> bool {
1552            !self.is_connected()
1553        }
1554
1555        async fn connect(&mut self) -> anyhow::Result<()> {
1556            self.connected.store(true, Ordering::Relaxed);
1557            Ok(())
1558        }
1559
1560        async fn disconnect(&mut self) -> anyhow::Result<()> {
1561            self.connected.store(false, Ordering::Relaxed);
1562            Ok(())
1563        }
1564
1565        fn request_bars(&self, request: RequestBars) -> anyhow::Result<()> {
1566            self.request_count.fetch_add(1, Ordering::Relaxed);
1567
1568            if get_message_bus()
1569                .borrow()
1570                .get_response_handler(&request.request_id)
1571                .is_some()
1572            {
1573                self.handler_visible_count.fetch_add(1, Ordering::Relaxed);
1574            }
1575
1576            let sender = get_data_event_sender();
1577            let client_id = self.client_id;
1578            let response_sent_count = self.response_sent_count.clone();
1579            let response = BarsResponse::new(
1580                request.request_id,
1581                client_id,
1582                request.bar_type,
1583                vec![Self::make_bar(request.bar_type)],
1584                None,
1585                None,
1586                UnixNanos::from(1_700_000_000_000_000_002u64),
1587                None,
1588            );
1589
1590            tokio::spawn(async move {
1591                tokio::time::sleep(Duration::from_millis(10)).await;
1592                response_sent_count.fetch_add(1, Ordering::Relaxed);
1593                sender
1594                    .send(DataEvent::Response(DataResponse::Bars(response)))
1595                    .expect("test bars response should send");
1596            });
1597
1598            Ok(())
1599        }
1600    }
1601
1602    fn install_tracking_strategy_module(py: Python<'_>, module_name: &str) {
1603        let module = PyModule::new(py, module_name).expect("test module should create");
1604        module
1605            .setattr("Strategy", py.get_type::<PyStrategy>())
1606            .expect("Strategy type should bind");
1607        module
1608            .setattr("BarType", py.get_type::<BarType>())
1609            .expect("BarType type should bind");
1610        module
1611            .setattr("RESULTS", PyDict::new(py))
1612            .expect("RESULTS should bind");
1613
1614        let code = CString::new(
1615            r#"
1616RESULTS["on_start"] = 0
1617RESULTS["on_historical_bars"] = 0
1618RESULTS["historical_bar_count"] = 0
1619RESULTS["last_request_id"] = ""
1620
1621class HistoricalBarsStrategy(Strategy):
1622    def __init__(self):
1623        super().__init__()
1624        self.bar_type = BarType.from_str("AUDUSD.SIM-1-MINUTE-LAST-EXTERNAL")
1625
1626    def on_start(self):
1627        RESULTS["on_start"] += 1
1628        RESULTS["last_request_id"] = self.request_bars(self.bar_type)
1629
1630    def on_stop(self):
1631        pass
1632
1633    def on_historical_bars(self, bars):
1634        RESULTS["on_historical_bars"] += 1
1635        RESULTS["historical_bar_count"] += len(bars)
1636"#,
1637        )
1638        .expect("python test code should be valid CString");
1639
1640        py.run(code.as_c_str(), Some(&module.dict()), None)
1641            .expect("test strategy code should execute");
1642
1643        let sys_modules = py
1644            .import("sys")
1645            .expect("sys should import")
1646            .getattr("modules")
1647            .expect("sys.modules should exist");
1648        sys_modules
1649            .set_item(module_name, module)
1650            .expect("test strategy module should register");
1651    }
1652
1653    fn get_results(py: Python<'_>, module_name: &str) -> (usize, usize, usize) {
1654        let module = py
1655            .import(module_name)
1656            .expect("test strategy module should import");
1657        let results_obj = module.getattr("RESULTS").expect("RESULTS should exist");
1658        let results = results_obj
1659            .cast::<PyDict>()
1660            .expect("RESULTS should be a dict");
1661
1662        let on_start = results
1663            .get_item("on_start")
1664            .expect("on_start key should exist")
1665            .extract::<usize>()
1666            .expect("on_start should extract");
1667        let on_historical_bars = results
1668            .get_item("on_historical_bars")
1669            .expect("on_historical_bars key should exist")
1670            .extract::<usize>()
1671            .expect("on_historical_bars should extract");
1672        let historical_bar_count = results
1673            .get_item("historical_bar_count")
1674            .expect("historical_bar_count key should exist")
1675            .extract::<usize>()
1676            .expect("historical_bar_count should extract");
1677
1678        (on_start, on_historical_bars, historical_bar_count)
1679    }
1680
1681    fn install_timer_strategy_module(py: Python<'_>, module_name: &str) {
1682        let module = PyModule::new(py, module_name).expect("test module should create");
1683        module
1684            .setattr("Strategy", py.get_type::<PyStrategy>())
1685            .expect("Strategy type should bind");
1686        module
1687            .setattr("RESULTS", PyDict::new(py))
1688            .expect("RESULTS should bind");
1689
1690        let code = CString::new(
1691            r#"
1692RESULTS["on_start"] = 0
1693RESULTS["callback_timer_count"] = 0
1694RESULTS["default_timer_count"] = 0
1695RESULTS["callback_event_type"] = ""
1696RESULTS["default_event_type"] = ""
1697RESULTS["callback_event_name"] = ""
1698RESULTS["default_event_name"] = ""
1699
1700class LiveTimerStrategy(Strategy):
1701    def __init__(self):
1702        super().__init__()
1703
1704    def on_start(self):
1705        RESULTS["on_start"] += 1
1706        self.clock.set_timer_ns(
1707            "explicit_timer",
1708            1_000_000,
1709            callback=self._on_timer,
1710            fire_immediately=True,
1711        )
1712        self.clock.set_timer_ns(
1713            "default_timer",
1714            1_000_000,
1715            fire_immediately=True,
1716        )
1717
1718    def on_stop(self):
1719        pass
1720
1721    def _on_timer(self, event):
1722        RESULTS["callback_timer_count"] += 1
1723        RESULTS["callback_event_type"] = type(event).__name__
1724        RESULTS["callback_event_name"] = event.name
1725
1726    def on_time_event(self, event):
1727        RESULTS["default_timer_count"] += 1
1728        RESULTS["default_event_type"] = type(event).__name__
1729        RESULTS["default_event_name"] = event.name
1730"#,
1731        )
1732        .expect("python test code should be valid CString");
1733
1734        py.run(code.as_c_str(), Some(&module.dict()), None)
1735            .expect("test strategy code should execute");
1736
1737        let sys_modules = py
1738            .import("sys")
1739            .expect("sys should import")
1740            .getattr("modules")
1741            .expect("sys.modules should exist");
1742        sys_modules
1743            .set_item(module_name, module)
1744            .expect("test strategy module should register");
1745    }
1746
1747    fn install_claim_strategy_module(py: Python<'_>, module_name: &str) {
1748        let module = PyModule::new(py, module_name).expect("test module should create");
1749        module
1750            .setattr("Strategy", py.get_type::<PyStrategy>())
1751            .expect("Strategy type should bind");
1752
1753        let code = CString::new(
1754            "
1755class ClaimsConfig:
1756    def __init__(self, strategy_id=None, external_order_claims=None):
1757        self.strategy_id = strategy_id
1758        self.external_order_claims = external_order_claims
1759
1760class ClaimsStrategy(Strategy):
1761    def __init__(self, config):
1762        super().__init__(config)
1763",
1764        )
1765        .expect("python test code should be valid CString");
1766
1767        py.run(code.as_c_str(), Some(&module.dict()), None)
1768            .expect("test strategy code should execute");
1769
1770        let sys_modules = py
1771            .import("sys")
1772            .expect("sys should import")
1773            .getattr("modules")
1774            .expect("sys.modules should exist");
1775        sys_modules
1776            .set_item(module_name, module)
1777            .expect("test strategy module should register");
1778    }
1779
1780    #[derive(Debug)]
1781    struct TimerStrategyResults {
1782        on_start: usize,
1783        callback_timer_count: usize,
1784        default_timer_count: usize,
1785        callback_event_type: String,
1786        default_event_type: String,
1787        callback_event_name: String,
1788        default_event_name: String,
1789    }
1790
1791    fn get_timer_results(py: Python<'_>, module_name: &str) -> TimerStrategyResults {
1792        let module = py
1793            .import(module_name)
1794            .expect("test strategy module should import");
1795        let results_obj = module.getattr("RESULTS").expect("RESULTS should exist");
1796        let results = results_obj
1797            .cast::<PyDict>()
1798            .expect("RESULTS should be a dict");
1799
1800        TimerStrategyResults {
1801            on_start: results
1802                .get_item("on_start")
1803                .expect("on_start key should exist")
1804                .extract::<usize>()
1805                .expect("on_start should extract"),
1806            callback_timer_count: results
1807                .get_item("callback_timer_count")
1808                .expect("callback_timer_count key should exist")
1809                .extract::<usize>()
1810                .expect("callback_timer_count should extract"),
1811            default_timer_count: results
1812                .get_item("default_timer_count")
1813                .expect("default_timer_count key should exist")
1814                .extract::<usize>()
1815                .expect("default_timer_count should extract"),
1816            callback_event_type: results
1817                .get_item("callback_event_type")
1818                .expect("callback_event_type key should exist")
1819                .extract::<String>()
1820                .expect("callback_event_type should extract"),
1821            default_event_type: results
1822                .get_item("default_event_type")
1823                .expect("default_event_type key should exist")
1824                .extract::<String>()
1825                .expect("default_event_type should extract"),
1826            callback_event_name: results
1827                .get_item("callback_event_name")
1828                .expect("callback_event_name key should exist")
1829                .extract::<String>()
1830                .expect("callback_event_name should extract"),
1831            default_event_name: results
1832                .get_item("default_event_name")
1833                .expect("default_event_name key should exist")
1834                .extract::<String>()
1835                .expect("default_event_name should extract"),
1836        }
1837    }
1838
1839    #[cfg(feature = "examples")]
1840    #[rstest]
1841    #[case("CompositeMarketMaker")]
1842    #[case("DeltaNeutralVol")]
1843    #[case("EmaCross")]
1844    #[case("ExecTester")]
1845    #[case("GridMarketMaker")]
1846    #[case("HurstVpinDirectional")]
1847    fn test_builtin_strategy_register_accepts_supported_names(#[case] type_name: &str) {
1848        assert!(super::builtin_strategy_register(type_name).is_some());
1849    }
1850
1851    #[cfg(feature = "examples")]
1852    #[rstest]
1853    #[case("BookImbalanceActor")]
1854    #[case("DataTester")]
1855    fn test_builtin_actor_register_accepts_supported_names(#[case] type_name: &str) {
1856        assert!(super::builtin_actor_register(type_name).is_some());
1857    }
1858
1859    #[cfg(feature = "examples")]
1860    #[rstest]
1861    fn test_builtin_register_rejects_unknown_names() {
1862        assert!(super::builtin_strategy_register("UnknownStrategy").is_none());
1863        assert!(super::builtin_actor_register("UnknownActor").is_none());
1864    }
1865
1866    #[cfg(feature = "examples")]
1867    #[rstest]
1868    fn test_builtin_strategy_register_rejects_mismatched_config() {
1869        Python::initialize();
1870
1871        let mut node = LiveNode::builder(TraderId::from("TESTER-001"), Environment::Sandbox)
1872            .unwrap()
1873            .with_reconciliation(false)
1874            .build()
1875            .unwrap();
1876
1877        Python::attach(|py| {
1878            let register = super::builtin_strategy_register("EmaCross").unwrap();
1879            let config = PyDict::new(py);
1880            let error = register(&mut node, config.as_any()).unwrap_err();
1881
1882            assert!(error.is_instance_of::<pyo3::exceptions::PyTypeError>(py));
1883        });
1884    }
1885
1886    #[cfg(feature = "examples")]
1887    #[rstest]
1888    fn test_builtin_actor_register_rejects_mismatched_config() {
1889        Python::initialize();
1890
1891        let mut node = LiveNode::builder(TraderId::from("TESTER-001"), Environment::Sandbox)
1892            .unwrap()
1893            .with_reconciliation(false)
1894            .build()
1895            .unwrap();
1896
1897        Python::attach(|py| {
1898            let register = super::builtin_actor_register("DataTester").unwrap();
1899            let config = PyDict::new(py);
1900            let error = register(&mut node, config.as_any()).unwrap_err();
1901
1902            assert!(error.is_instance_of::<pyo3::exceptions::PyTypeError>(py));
1903        });
1904    }
1905
1906    #[rstest]
1907    fn test_run_live_node_detached_releases_gil() {
1908        Python::initialize();
1909
1910        let mut node = LiveNode::builder(TraderId::from("TESTER-001"), Environment::Sandbox)
1911            .unwrap()
1912            .with_reconciliation(false)
1913            .with_delay_post_stop_secs(0)
1914            .with_timeout_connection(1)
1915            .build()
1916            .unwrap();
1917
1918        let handle = node.handle();
1919        let (gil_tx, gil_rx) = mpsc::channel();
1920        let acquired_before_stop = Arc::new(AtomicBool::new(false));
1921        let acquired_before_stop_for_thread = acquired_before_stop.clone();
1922
1923        let stop_thread = thread::spawn(move || {
1924            if gil_rx.recv_timeout(Duration::from_secs(1)).is_ok() {
1925                acquired_before_stop_for_thread.store(true, Ordering::SeqCst);
1926            }
1927            handle.stop();
1928        });
1929
1930        let gil_thread = thread::spawn(move || {
1931            Python::attach(|_| {});
1932            let _ = gil_tx.send(());
1933        });
1934
1935        Python::attach(|py| {
1936            super::run_live_node_detached(py, &mut node).expect("node should run cleanly");
1937        });
1938
1939        stop_thread.join().expect("stop thread should join");
1940        gil_thread.join().expect("GIL thread should join");
1941
1942        assert!(
1943            acquired_before_stop.load(Ordering::SeqCst),
1944            "worker thread should acquire the GIL while LiveNode::run is blocked"
1945        );
1946    }
1947
1948    #[rstest]
1949    fn test_live_node_pystrategy_timer_callbacks_run_on_event_loop() {
1950        Python::initialize();
1951
1952        let module_name = "test_live_node_timer_strategy";
1953        Python::attach(|py| install_timer_strategy_module(py, module_name));
1954
1955        let mut node = LiveNode::builder(TraderId::from("TESTER-001"), Environment::Sandbox)
1956            .unwrap()
1957            .with_reconciliation(false)
1958            .with_delay_post_stop_secs(0)
1959            .with_timeout_connection(1)
1960            .build()
1961            .unwrap();
1962
1963        let importable = ImportableStrategyConfig {
1964            strategy_path: format!("{module_name}:LiveTimerStrategy"),
1965            config_path: String::new(),
1966            config: HashMap::new(),
1967        };
1968
1969        Python::attach(|py| {
1970            node.py_add_strategy_from_config(py, importable)
1971                .expect("strategy should register");
1972        });
1973
1974        let handle = node.handle();
1975        let stop_handle = handle.clone();
1976        let watchdog_handle = handle;
1977        let (done_tx, done_rx) = mpsc::channel();
1978        let module_name_for_stop = module_name.to_string();
1979
1980        let stop_thread = thread::spawn(move || {
1981            let deadline = Instant::now() + Duration::from_secs(5);
1982
1983            loop {
1984                let fired = Python::attach(|py| {
1985                    let results = get_timer_results(py, &module_name_for_stop);
1986                    results.callback_timer_count > 0 && results.default_timer_count > 0
1987                });
1988
1989                if fired || Instant::now() >= deadline {
1990                    break;
1991                }
1992
1993                thread::sleep(Duration::from_millis(20));
1994            }
1995
1996            stop_handle.stop();
1997        });
1998
1999        let watchdog_thread = thread::spawn(move || {
2000            if done_rx.recv_timeout(Duration::from_secs(5)).is_err() {
2001                watchdog_handle.stop();
2002            }
2003        });
2004
2005        Python::attach(|py| {
2006            super::run_live_node_detached(py, &mut node).expect("node should run cleanly");
2007        });
2008
2009        let _ = done_tx.send(());
2010        stop_thread.join().expect("stop thread should join");
2011        watchdog_thread.join().expect("watchdog thread should join");
2012
2013        let results = Python::attach(|py| get_timer_results(py, module_name));
2014
2015        assert_eq!(results.on_start, 1);
2016        assert!(results.callback_timer_count > 0);
2017        assert!(results.default_timer_count > 0);
2018        assert_eq!(results.callback_event_type, "TimeEvent");
2019        assert_eq!(results.default_event_type, "TimeEvent");
2020        assert_eq!(results.callback_event_name, "explicit_timer");
2021        assert_eq!(results.default_event_name, "default_timer");
2022    }
2023
2024    #[rstest]
2025    fn test_add_strategy_from_config_registers_external_order_claims() {
2026        Python::initialize();
2027
2028        let module_name = "test_live_node_claim_strategy";
2029        Python::attach(|py| install_claim_strategy_module(py, module_name));
2030
2031        let mut node = LiveNode::builder(TraderId::from("TESTER-001"), Environment::Sandbox)
2032            .unwrap()
2033            .with_reconciliation(false)
2034            .with_delay_post_stop_secs(0)
2035            .with_timeout_connection(1)
2036            .build()
2037            .unwrap();
2038
2039        let instrument_id = InstrumentId::from("AUDUSD.SIM");
2040        let strategy_id = StrategyId::from("CLAIMS-001");
2041        let mut config = HashMap::new();
2042        config.insert(
2043            "strategy_id".to_string(),
2044            serde_json::json!(strategy_id.to_string()),
2045        );
2046        config.insert(
2047            "external_order_claims".to_string(),
2048            serde_json::json!([instrument_id.to_string()]),
2049        );
2050        let importable = ImportableStrategyConfig {
2051            strategy_path: format!("{module_name}:ClaimsStrategy"),
2052            config_path: format!("{module_name}:ClaimsConfig"),
2053            config,
2054        };
2055
2056        Python::attach(|py| {
2057            node.py_add_strategy_from_config(py, importable)
2058                .expect("strategy should register");
2059        });
2060
2061        let result = node
2062            .exec_manager_mut()
2063            .claim_external_orders(instrument_id, StrategyId::from("OTHER-001"));
2064
2065        assert!(result.is_err());
2066        assert!(
2067            result
2068                .unwrap_err()
2069                .to_string()
2070                .contains("already exists for CLAIMS-001")
2071        );
2072    }
2073
2074    #[tokio::test(flavor = "current_thread")]
2075    async fn test_live_node_pystrategy_request_bars_dispatches_on_historical_bars() {
2076        Python::initialize();
2077
2078        let module_name = "test_live_node_historical_bars_strategy";
2079        Python::attach(|py| install_tracking_strategy_module(py, module_name));
2080
2081        let request_count = Arc::new(AtomicUsize::new(0));
2082        let response_sent_count = Arc::new(AtomicUsize::new(0));
2083        let handler_visible_count = Arc::new(AtomicUsize::new(0));
2084        let factory = TestHistoricalBarsDataClientFactory::new(
2085            request_count.clone(),
2086            response_sent_count.clone(),
2087            handler_visible_count.clone(),
2088        );
2089        let config = TestDataClientConfig;
2090
2091        let mut node = LiveNode::builder(TraderId::from("TESTER-001"), Environment::Sandbox)
2092            .unwrap()
2093            .with_reconciliation(false)
2094            .with_delay_post_stop_secs(0)
2095            .with_timeout_connection(1)
2096            .add_data_client(
2097                Some("TEST_DATA".to_string()),
2098                Box::new(factory),
2099                Box::new(config),
2100            )
2101            .unwrap()
2102            .build()
2103            .unwrap();
2104
2105        let importable = ImportableStrategyConfig {
2106            strategy_path: format!("{module_name}:HistoricalBarsStrategy"),
2107            config_path: String::new(),
2108            config: HashMap::new(),
2109        };
2110
2111        Python::attach(|py| {
2112            node.py_add_strategy_from_config(py, importable)
2113                .expect("strategy should register");
2114        });
2115
2116        let handle = node.handle();
2117        let stop_handle = handle.clone();
2118        let response_sent_count_for_stop = response_sent_count.clone();
2119
2120        tokio::spawn(async move {
2121            let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
2122
2123            loop {
2124                if response_sent_count_for_stop.load(Ordering::Relaxed) == 1
2125                    || tokio::time::Instant::now() >= deadline
2126                {
2127                    break;
2128                }
2129                tokio::time::sleep(Duration::from_millis(20)).await;
2130            }
2131            tokio::time::sleep(Duration::from_millis(250)).await;
2132            stop_handle.stop();
2133        });
2134
2135        node.run().await.expect("node should run cleanly");
2136
2137        let (on_start, on_historical_bars, historical_bar_count) =
2138            Python::attach(|py| get_results(py, module_name));
2139
2140        assert_eq!(request_count.load(Ordering::Relaxed), 1);
2141        assert_eq!(handler_visible_count.load(Ordering::Relaxed), 1);
2142        assert_eq!(response_sent_count.load(Ordering::Relaxed), 1);
2143        assert_eq!(on_start, 1);
2144        assert_eq!(on_historical_bars, 1);
2145        assert_eq!(historical_bar_count, 1);
2146    }
2147}