1use 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#[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 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 let handle = self.handle();
143
144 let signal_module = py.import("signal")?;
146 let original_handler =
147 signal_module.call_method1("signal", (2, signal_module.getattr("SIG_DFL")?))?; 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 signal_module.call_method1("signal", (2, signal_callback))?;
164
165 let result = run_live_node_detached(py, self);
167
168 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 #[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 #[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 #[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 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#[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 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 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 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
1224fn 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 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 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 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
1332fn 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}