1use std::{
19 cell::{Cell, Ref, RefCell, RefMut},
20 collections::HashMap,
21 fmt::Debug,
22 future::Future,
23 pin::Pin,
24 rc::Rc,
25 str::FromStr,
26 sync::{
27 Arc,
28 atomic::{AtomicBool, Ordering},
29 mpsc,
30 },
31 task::{Context, Poll, Waker},
32 thread,
33 time::{Duration, Instant},
34};
35
36#[cfg(feature = "examples")]
37use nautilus_common::python::config_error_to_pyvalue_err;
38use nautilus_common::{
39 actor::data_actor::ImportableActorConfig,
40 cache::CacheConfig,
41 enums::Environment,
42 live::get_runtime,
43 logging::logger::LoggerConfig,
44 msgbus::MessageBusConfig,
45 python::{
46 actor::{PyDataActor, prepare_python_actor},
47 cache::{PyCache, get_global_cache_database_factory_registry},
48 msgbus::get_global_msgbus_factory_registry,
49 },
50};
51#[cfg(feature = "examples")]
52use nautilus_core::python::to_pytype_err;
53use nautilus_core::{
54 UUID4,
55 python::{to_pyruntime_err, to_pyvalue_err},
56};
57use nautilus_model::{
58 enums::OmsType,
59 identifiers::{ActorId, ExecAlgorithmId, InstrumentId, StrategyId, TraderId},
60};
61use nautilus_portfolio::{config::PortfolioConfig, python::PyPortfolio};
62use nautilus_system::get_global_pyo3_registry;
63#[cfg(feature = "examples")]
64use nautilus_testkit::{DataTester, DataTesterConfig, ExecTester, ExecTesterConfig};
65#[cfg(feature = "examples")]
66use nautilus_trading::examples::{
67 actors::{BookImbalanceActor, BookImbalanceActorConfig},
68 strategies::{
69 CompositeMarketMaker, CompositeMarketMakerConfig, DeltaNeutralVol, DeltaNeutralVolConfig,
70 EmaCross, EmaCrossConfig, GridMarketMaker, GridMarketMakerConfig, HurstVpinDirectional,
71 HurstVpinDirectionalConfig,
72 },
73};
74use nautilus_trading::{
75 ImportableControllerConfig, ImportableExecutionAlgorithmConfig, ImportableStrategyConfig,
76 python::{algorithm::PyExecutionAlgorithm, strategy::PyStrategy},
77};
78use parking_lot::{Condvar, Mutex};
79use pyo3::{
80 exceptions::PyBaseExceptionGroup,
81 ffi::c_str,
82 intern,
83 prelude::*,
84 sync::PyOnceLock,
85 types::{PyCFunction, PyDict, PyTuple},
86};
87use serde_json;
88
89use super::client::{PythonClientConfig, PythonClients, PythonDataFactory, PythonExecutionFactory};
90pub use crate::node::NodeState;
92use crate::{
93 builder::LiveNodeBuilder,
94 config::{
95 LiveDataEngineConfig, LiveExecutionEngineConfig, LiveNodeConfig, LiveRiskEngineConfig,
96 PluginConfig,
97 },
98 node::{LiveNode, LiveNodeHandle, NodeRunMode, config::RoutingConfig},
99 python::config::{coerce_json_config, json_value_to_py},
100};
101
102#[pyo3::pyclass(module = "nautilus_trader.live", name = "LiveNode", unsendable)]
109#[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.live")]
110#[derive(Debug)]
111pub struct PyLiveNode {
112 inner: Rc<RefCell<Option<LiveNode>>>,
113 handle: LiveNodeHandle,
114 clients: PythonClients,
115}
116
117impl PyLiveNode {
118 #[must_use]
120 pub fn new(node: LiveNode) -> Self {
121 let handle = node.handle();
122
123 Self {
124 inner: Rc::new(RefCell::new(Some(node))),
125 handle,
126 clients: PythonClients::default(),
127 }
128 }
129
130 fn node(&self) -> PyResult<Ref<'_, LiveNode>> {
131 let borrow = self.inner.try_borrow().map_err(|_| node_busy_err())?;
132 if borrow.is_none() {
133 return Err(node_consumed_err());
134 }
135
136 Ok(Ref::map(borrow, |node| {
137 node.as_ref().expect("node presence checked above")
138 }))
139 }
140
141 fn node_mut(&self) -> PyResult<RefMut<'_, LiveNode>> {
142 let borrow = self.inner.try_borrow_mut().map_err(|_| node_busy_err())?;
143 if borrow.is_none() {
144 return Err(node_consumed_err());
145 }
146
147 Ok(RefMut::map(borrow, |node| {
148 node.as_mut().expect("node presence checked above")
149 }))
150 }
151
152 fn is_consumed(&self) -> bool {
153 self.inner.try_borrow().is_ok_and(|node| node.is_none())
154 }
155}
156
157#[pyo3::pyclass(
162 module = "nautilus_trader.live",
163 name = "LiveNodeHandle",
164 frozen,
165 skip_from_py_object
166)]
167#[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.live")]
168#[derive(Clone, Debug)]
169pub struct PyLiveNodeHandle {
170 inner: LiveNodeHandle,
171}
172
173#[pyo3_stub_gen::derive::gen_stub_pymethods]
174#[pymethods]
175impl PyLiveNodeHandle {
176 #[pyo3(name = "stop")]
181 fn py_stop(&self) {
182 self.inner.stop();
183 }
184
185 #[getter]
187 #[pyo3(name = "is_stopping")]
188 fn py_is_stopping(&self) -> bool {
189 self.inner.should_stop()
190 }
191
192 #[getter]
194 #[pyo3(name = "is_running")]
195 fn py_is_running(&self) -> bool {
196 self.inner.is_running()
197 }
198
199 #[getter]
201 #[pyo3(name = "state")]
202 fn py_state(&self) -> NodeState {
203 self.inner.state()
204 }
205
206 fn __repr__(&self) -> String {
207 format!("LiveNodeHandle(state={:?})", self.inner.state())
208 }
209}
210
211const CLOSE_DRIVE_TIMEOUT: Duration = Duration::from_secs(30);
216
217thread_local! {
218 static HOSTED_RUN_ACTIVE: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
223}
224
225#[derive(Debug, Default)]
230struct BlockingWake {
231 woken: Mutex<bool>,
232 signal: Condvar,
233}
234
235impl BlockingWake {
236 fn wait_until(&self, deadline: Instant) -> bool {
238 let mut woken = self.woken.lock();
239 while !*woken {
240 let Some(remaining) = deadline.checked_duration_since(Instant::now()) else {
241 return false;
242 };
243
244 let timeout = self.signal.wait_for(&mut woken, remaining);
245
246 if timeout.timed_out() && !*woken {
247 return false;
248 }
249 }
250
251 *woken = false;
252 true
253 }
254}
255
256impl std::task::Wake for BlockingWake {
257 fn wake(self: Arc<Self>) {
258 self.wake_by_ref();
259 }
260
261 fn wake_by_ref(self: &Arc<Self>) {
262 *self.woken.lock() = true;
263 self.signal.notify_all();
264 }
265}
266
267#[derive(Debug, Default)]
269struct RunWakeState {
270 pending: Mutex<Option<RunSuspension>>,
271 closed: AtomicBool,
272}
273
274#[derive(Debug)]
275struct RunSuspension {
276 generation: u64,
277 future: Py<PyAny>,
278}
279
280impl RunWakeState {
281 fn suspend(&self, generation: u64, future: Py<PyAny>) {
282 *self.pending.lock() = Some(RunSuspension { generation, future });
283 }
284
285 fn resume(&self, py: Python<'_>, generation: u64) -> PyResult<()> {
286 let mut pending = self.pending.lock();
287
288 let Some(suspension) = pending.as_ref() else {
289 return Ok(());
290 };
291
292 if self.closed.load(Ordering::Acquire) || suspension.generation != generation {
293 return Ok(());
294 }
295
296 let future = pending.take().expect("suspension presence checked").future;
297 drop(pending);
298
299 let future = future.bind(py);
300 if !future
301 .call_method0(intern!(py, "done"))?
302 .extract::<bool>()?
303 {
304 future.call_method1(intern!(py, "set_result"), (py.None(),))?;
305 }
306
307 Ok(())
308 }
309
310 fn close(&self) {
311 self.closed.store(true, Ordering::Release);
312 self.pending.lock().take();
313 }
314}
315
316#[pyo3::pyclass(name = "NodeRunWake", frozen)]
318struct PyNodeRunWake {
319 state: Arc<RunWakeState>,
320}
321
322#[pymethods]
323impl PyNodeRunWake {
324 fn __call__(&self, py: Python<'_>, generation: u64) -> PyResult<()> {
325 self.state.resume(py, generation)
326 }
327}
328
329enum HostWakeSignal {
330 Resume(u64),
331 Shutdown,
332}
333
334struct HostWakeControl {
335 sender: mpsc::Sender<HostWakeSignal>,
336 active: AtomicBool,
337 handle: LiveNodeHandle,
338}
339
340impl HostWakeControl {
341 fn resume(&self, generation: u64) {
342 if !self.active.load(Ordering::Acquire) {
343 return;
344 }
345
346 if self
347 .sender
348 .send(HostWakeSignal::Resume(generation))
349 .is_err()
350 && self.active.swap(false, Ordering::AcqRel)
351 {
352 log::error!("Hosted run wake pump stopped unexpectedly, stopping node");
355 self.handle.stop();
356 }
357 }
358
359 fn close(&self) {
360 if self.active.swap(false, Ordering::AcqRel) {
361 let _ = self.sender.send(HostWakeSignal::Shutdown);
362 }
363 }
364}
365
366struct HostLoopWaker {
368 generation: u64,
369 scheduled: AtomicBool,
370 control: Arc<HostWakeControl>,
371}
372
373impl std::task::Wake for HostLoopWaker {
374 fn wake(self: Arc<Self>) {
375 self.wake_by_ref();
376 }
377
378 fn wake_by_ref(self: &Arc<Self>) {
379 if !self.scheduled.swap(true, Ordering::AcqRel) {
380 self.control.resume(self.generation);
381 }
382 }
383}
384
385struct HostWakePump {
387 control: Arc<HostWakeControl>,
388 thread: Option<thread::JoinHandle<()>>,
389}
390
391impl HostWakePump {
392 fn start(
393 event_loop: Py<PyAny>,
394 wake_callback: Py<PyAny>,
395 handle: LiveNodeHandle,
396 ) -> PyResult<Self> {
397 let (sender, receiver) = mpsc::channel();
398
399 let control = Arc::new(HostWakeControl {
400 sender,
401 active: AtomicBool::new(true),
402 handle: handle.clone(),
403 });
404
405 let control_for_thread = control.clone();
406
407 let thread = thread::Builder::new()
408 .name("nautilus-host-wake".to_string())
409 .spawn(move || {
410 while let Ok(signal) = receiver.recv() {
411 match signal {
412 HostWakeSignal::Resume(generation) => {
413 if !control_for_thread.active.load(Ordering::Acquire) {
414 continue;
415 }
416
417 let Some(result) = Python::try_attach(|py| {
418 event_loop.bind(py).call_method1(
419 intern!(py, "call_soon_threadsafe"),
420 (wake_callback.bind(py), generation),
421 )?;
422 Ok::<(), PyErr>(())
423 }) else {
424 log::error!(
425 "Python unavailable while scheduling hosted run wake-up, stopping node"
426 );
427 handle.stop();
428 break;
429 };
430
431 if let Err(e) = result {
432 log::error!(
433 "Failed to schedule hosted run wake-up, stopping node: {e}"
434 );
435 handle.stop();
436 break;
437 }
438 }
439 HostWakeSignal::Shutdown => break,
440 }
441 }
442
443 control_for_thread.active.store(false, Ordering::Release);
444 })
445 .map_err(|e| to_pyruntime_err(format!("failed to start hosted run wake pump: {e}")))?;
446
447 Ok(Self {
448 control,
449 thread: Some(thread),
450 })
451 }
452
453 fn waker(&self, generation: u64) -> Waker {
454 Waker::from(Arc::new(HostLoopWaker {
455 generation,
456 scheduled: AtomicBool::new(false),
457 control: self.control.clone(),
458 }))
459 }
460
461 fn close(&self) {
462 self.control.close();
463 }
464
465 fn join(&mut self, py: Option<Python<'_>>) {
466 let Some(thread) = self.thread.take() else {
467 return;
468 };
469
470 let mut thread = Some(thread);
471
472 let result = match py {
473 Some(py) => py.detach(|| thread.take().expect("thread presence checked").join()),
474 None => Python::try_attach(|py| {
475 py.detach(|| thread.take().expect("thread presence checked").join())
476 })
477 .unwrap_or_else(|| thread.take().expect("thread presence checked").join()),
478 };
479
480 if result.is_err() {
481 log::error!("Hosted run wake pump panicked while stopping");
482 }
483 }
484}
485
486impl Drop for HostWakePump {
487 fn drop(&mut self) {
488 self.close();
489 self.join(None);
490 }
491}
492
493struct SendPtr<T>(*mut T);
494
495#[allow(unsafe_code)]
498unsafe impl<T> Send for SendPtr<T> {}
499
500#[pyo3::pyclass(name = "NodeRun", unsendable)]
505pub struct PyNodeRun {
506 clients: PythonClients,
507 future: Option<Pin<Box<dyn Future<Output = anyhow::Result<()>>>>>,
509 node: Option<Box<LiveNode>>,
510 owner: Rc<RefCell<Option<LiveNode>>>,
511 handle: LiveNodeHandle,
512 event_loop: Py<PyAny>,
513 wake_pump: HostWakePump,
514 state: Arc<RunWakeState>,
515 generation: u64,
516 pending_throw: Option<PyErr>,
517}
518
519#[allow(
520 clippy::missing_fields_in_debug,
521 reason = "the future and node fields have no useful debug representation"
522)]
523impl Debug for PyNodeRun {
524 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
525 f.debug_struct(stringify!(PyNodeRun))
526 .field("state", &self.handle.state())
527 .field("completed", &self.future.is_none())
528 .finish()
529 }
530}
531
532impl Drop for PyNodeRun {
533 fn drop(&mut self) {
534 self.restore_node(None);
535 }
536}
537
538impl PyNodeRun {
539 #[allow(
540 unsafe_code,
541 reason = "the run future borrows the boxed node this type owns"
542 )]
543 fn new(
544 node: LiveNode,
545 owner: Rc<RefCell<Option<LiveNode>>>,
546 event_loop: Bound<'_, PyAny>,
547 state: Arc<RunWakeState>,
548 wake_pump: HostWakePump,
549 clients: PythonClients,
550 ) -> Self {
551 let handle = node.handle();
552 let mut node = Box::new(node);
553 let node_ptr = SendPtr(std::ptr::from_mut::<LiveNode>(node.as_mut()));
554
555 let future: Pin<Box<dyn Future<Output = anyhow::Result<()>>>> = Box::pin(async move {
556 let ptr = node_ptr;
557 unsafe { (*ptr.0).run_with_mode(NodeRunMode::Hosted).await }
561 });
562
563 Self {
564 clients,
565 future: Some(future),
566 node: Some(node),
567 owner,
568 handle,
569 event_loop: event_loop.unbind(),
570 wake_pump,
571 state,
572 generation: 0,
573 pending_throw: None,
574 }
575 }
576
577 fn restore_node(&mut self, py: Option<Python<'_>>) {
582 Python::attach(|py| {
583 if let Err(e) = self.clients.finish(py) {
584 log::error!("{e}");
585 }
586 });
587
588 self.state.close();
589 self.wake_pump.close();
590 self.future = None;
591 self.wake_pump.join(py);
592
593 if let Some(node) = self.node.take() {
594 *self.owner.borrow_mut() = Some(*node);
595
596 HOSTED_RUN_ACTIVE.set(false);
599 }
600 }
601
602 fn host_loop_is_running(&self, py: Python<'_>) -> bool {
606 self.event_loop
607 .bind(py)
608 .call_method0(intern!(py, "is_running"))
609 .and_then(|running| running.extract::<bool>())
610 .unwrap_or(true)
611 }
612
613 fn drive_to_completion(&mut self, py: Python<'_>) {
615 let signal = Arc::new(BlockingWake::default());
616 let waker = Waker::from(signal.clone());
617 let deadline = Instant::now() + CLOSE_DRIVE_TIMEOUT;
618
619 loop {
620 let Some(future) = self.future.as_mut() else {
621 return;
622 };
623
624 let poll = {
625 let _guard = get_runtime().enter();
626 future.as_mut().poll(&mut Context::from_waker(&waker))
627 };
628
629 if let Poll::Ready(result) = poll {
630 if let Err(e) = result {
631 log::error!("Hosted run failed during inline shutdown: {e}");
632 }
633
634 self.restore_node(Some(py));
635 return;
636 }
637
638 if !py.detach(|| signal.wait_until(deadline)) {
640 log::error!(
641 "Hosted run did not stop within {}s of the host loop closing; abandoning it \
642 with resources still held",
643 CLOSE_DRIVE_TIMEOUT.as_secs()
644 );
645 return;
646 }
647 }
648 }
649
650 fn step(&mut self, py: Python<'_>) -> PyResult<Option<Py<PyAny>>> {
652 let Some(future) = self.future.as_mut() else {
653 return Err(to_pyruntime_err("Hosted run has already completed"));
654 };
655
656 let generation = self
657 .generation
658 .checked_add(1)
659 .ok_or_else(|| to_pyruntime_err("Hosted run wake generation exhausted"))?;
660 self.generation = generation;
661
662 let waker = self.wake_pump.waker(generation);
664
665 let poll = {
667 let _guard = get_runtime().enter();
668 future.as_mut().poll(&mut Context::from_waker(&waker))
669 };
670
671 match poll {
672 Poll::Ready(result) => {
673 let cleanup = self.clients.finish(py);
674 self.restore_node(Some(py));
675
676 if let Err(e) = &cleanup {
677 log::error!("{e}");
678 }
679
680 if let Err(e) = &result {
681 log::error!("Hosted run failed: {e}");
682 }
683
684 if let Some(raised) = self.pending_throw.take() {
685 return Err(raised);
688 }
689
690 result.map_err(to_pyruntime_err)?;
691 cleanup?;
692 Ok(None)
693 }
694 Poll::Pending => {
695 let suspended = self
696 .event_loop
697 .bind(py)
698 .call_method0(intern!(py, "create_future"))?;
699 suspended.setattr(intern!(py, "_asyncio_future_blocking"), true)?;
700
701 self.state.suspend(generation, suspended.clone().unbind());
703
704 Ok(Some(suspended.unbind()))
705 }
706 }
707 }
708}
709
710#[pymethods]
711impl PyNodeRun {
712 fn __await__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> {
713 slf
714 }
715
716 fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> {
717 slf
718 }
719
720 fn __next__(&mut self, py: Python<'_>) -> PyResult<Option<Py<PyAny>>> {
721 self.step(py)
722 }
723
724 #[pyo3(signature = (value=None))]
725 fn send(
726 &mut self,
727 py: Python<'_>,
728 value: Option<&Bound<'_, PyAny>>,
729 ) -> PyResult<Option<Py<PyAny>>> {
730 let _ = value;
731 self.step(py)
732 }
733
734 #[pyo3(signature = (*args))]
739 fn throw(&mut self, py: Python<'_>, args: &Bound<'_, PyTuple>) -> PyResult<Option<Py<PyAny>>> {
740 let raised = args.get_item(0)?;
741
742 if self.pending_throw.is_none() {
743 let cancelled_type = cancelled_error_type(py)?;
744 let is_cancelled = raised.is_instance(&cancelled_type)? || raised.is(&cancelled_type);
745 if is_cancelled {
746 log::info!("Hosted run cancelled, requesting graceful shutdown");
747 } else {
748 log::warn!("Exception thrown into hosted run, requesting graceful shutdown");
749 }
750
751 self.pending_throw = Some(PyErr::from_value(raised));
752 self.handle.stop();
753 }
754
755 self.step(py)
756 }
757
758 fn close(&mut self, py: Python<'_>) {
766 if self.future.is_none() {
767 return;
768 }
769
770 self.handle.stop();
771
772 if self.host_loop_is_running(py) {
775 log::warn!(
776 "Hosted run discarded while its event loop is running; shutdown was requested but \
777 not completed, await the run instead of discarding it"
778 );
779 return;
780 }
781
782 log::warn!("Hosted run closed with no running event loop, draining shutdown inline");
783 self.drive_to_completion(py);
784 }
785
786 fn __repr__(&self) -> String {
787 format!("{self:?}")
788 }
789}
790
791fn node_run_driver(py: Python<'_>) -> PyResult<&Py<PyAny>> {
797 static DRIVER: PyOnceLock<Py<PyAny>> = PyOnceLock::new();
798
799 DRIVER.get_or_try_init(py, || {
800 let module = PyModule::from_code(
801 py,
802 c_str!("async def drive(run):\n return await run\n"),
803 c_str!("nautilus_trader/live/_hosted_run.py"),
804 c_str!("nautilus_trader._hosted_run"),
805 )?;
806 Ok(module.getattr("drive")?.unbind())
807 })
808}
809
810fn cancelled_error_type(py: Python<'_>) -> PyResult<Bound<'_, PyAny>> {
812 py.import("asyncio")?.getattr(intern!(py, "CancelledError"))
813}
814
815fn node_busy_err() -> PyErr {
817 to_pyruntime_err(
818 "LiveNode is busy servicing another call; do not re-enter the node from a component \
819 callback, use the cache, portfolio, and handle captured beforehand",
820 )
821}
822
823fn node_consumed_err() -> PyErr {
824 to_pyruntime_err(
825 "LiveNode is being run by `run_async`; use the handle returned by `handle()` to stop it, \
826 and the `cache` and `portfolio` captured before the run to read state",
827 )
828}
829
830#[pyo3_stub_gen::derive::gen_stub_pymethods]
831#[pymethods]
832impl PyLiveNode {
833 #[staticmethod]
843 #[pyo3(name = "build")]
844 #[pyo3(signature = (name, config=None, *, data_factories=None, exec_factories=None))]
845 fn py_build(
846 py: Python<'_>,
847 name: String,
848 config: Option<Py<LiveNodeConfig>>,
849 data_factories: Option<Bound<'_, PyDict>>,
850 exec_factories: Option<Bound<'_, PyDict>>,
851 ) -> PyResult<Self> {
852 PyLiveNodeBuilder::py_from_config(py, name, config, data_factories, exec_factories)?
853 .py_build()
854 }
855
856 #[staticmethod]
862 #[pyo3(name = "builder")]
863 fn py_builder(
864 name: String,
865 trader_id: TraderId,
866 environment: Environment,
867 ) -> PyResult<PyLiveNodeBuilder> {
868 match LiveNode::builder(trader_id, environment) {
869 Ok(builder) => Ok(PyLiveNodeBuilder {
870 clients: PythonClients::default(),
871 state: Rc::new(Cell::new(PyLiveNodeBuilderState::Ready(Box::new(
872 builder.with_name(name),
873 )))),
874 }),
875 Err(e) => Err(to_pyruntime_err(e)),
876 }
877 }
878
879 #[getter]
881 #[pyo3(name = "environment")]
882 fn py_environment(&self) -> PyResult<Environment> {
883 Ok(self.node()?.environment())
884 }
885
886 #[getter]
888 #[pyo3(name = "trader_id")]
889 fn py_trader_id(&self) -> PyResult<TraderId> {
890 Ok(self.node()?.trader_id())
891 }
892
893 #[getter]
895 #[pyo3(name = "instance_id")]
896 fn py_instance_id(&self) -> PyResult<UUID4> {
897 Ok(self.node()?.instance_id())
898 }
899
900 #[getter]
904 #[pyo3(name = "is_running")]
905 fn py_is_running(&self) -> bool {
906 self.handle.is_running()
907 }
908
909 #[getter]
911 #[pyo3(name = "cache")]
912 fn py_cache(&self) -> PyResult<PyCache> {
913 Ok(PyCache::from_rc(self.node()?.kernel().cache()))
914 }
915
916 #[getter]
918 #[pyo3(name = "portfolio")]
919 fn py_portfolio(&self) -> PyResult<PyPortfolio> {
920 Ok(PyPortfolio::from_rc(
921 self.node()?.kernel().portfolio.clone(),
922 ))
923 }
924
925 #[pyo3(name = "handle")]
930 fn py_handle(&self) -> PyLiveNodeHandle {
931 PyLiveNodeHandle {
932 inner: self.handle.clone(),
933 }
934 }
935
936 #[pyo3(name = "add_stream_processor")]
950 fn py_add_stream_processor(&self, callback: Py<PyAny>) -> PyResult<()> {
951 self.node_mut()?
952 .add_stream_processor_with_mapping(move |_, mapping| {
953 Python::attach(|py| {
954 let payload = json_value_to_py(py, mapping)?;
955 callback.call1(py, (payload,))?;
956 Ok(())
957 })
958 .map_err(|e: PyErr| anyhow::anyhow!("Python stream processor failed: {e}"))
959 });
960
961 Ok(())
962 }
963
964 #[gen_stub(override_return_type(
986 type_repr = "collections.abc.Coroutine[typing.Any, typing.Any, None]",
987 imports = ("collections.abc", "typing")
988 ))]
989 #[pyo3(name = "run_async")]
990 fn py_run_async(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
991 let event_loop = py
992 .import("asyncio")?
993 .call_method0("get_running_loop")
994 .map_err(|_| {
995 to_pyruntime_err(
996 "`run_async` requires a running asyncio event loop; use `run` to have the node \
997 own the thread instead",
998 )
999 })?;
1000
1001 let state = self.node()?.state();
1004 if state != NodeState::Idle {
1005 return Err(to_pyruntime_err(format!(
1006 "LiveNode cannot be run from state {state:?}; build a new node to run again"
1007 )));
1008 }
1009
1010 if self.node()?.has_pending_cache_database() {
1014 return Err(to_pyruntime_err(
1015 "a cache database backing is not supported on a host event loop, because its \
1016 blocking calls would stall the loop; native-only nodes can use `run()`, \
1017 but custom Python clients require a node without a cache database",
1018 ));
1019 }
1020
1021 if HOSTED_RUN_ACTIVE.get() {
1022 return Err(to_pyruntime_err(
1023 "another LiveNode is already running on this event loop; run one concurrent \
1024 LiveNode per process, or run additional nodes in separate processes",
1025 ));
1026 }
1027
1028 let driver = node_run_driver(py)?.clone_ref(py);
1031 let state = Arc::new(RunWakeState::default());
1032
1033 let wake_callback = Py::new(
1034 py,
1035 PyNodeRunWake {
1036 state: state.clone(),
1037 },
1038 )?
1039 .into_any();
1040
1041 let wake_pump = HostWakePump::start(
1042 event_loop.clone().unbind(),
1043 wake_callback,
1044 self.handle.clone(),
1045 )?;
1046
1047 self.clients.bind(py, &event_loop)?;
1048
1049 let node = self
1050 .inner
1051 .borrow_mut()
1052 .take()
1053 .ok_or_else(node_consumed_err)?;
1054
1055 let run = PyNodeRun::new(
1056 node,
1057 self.inner.clone(),
1058 event_loop,
1059 state,
1060 wake_pump,
1061 self.clients.clone(),
1062 );
1063 HOSTED_RUN_ACTIVE.set(true);
1064
1065 driver.call1(py, (Py::new(py, run)?,))
1066 }
1067
1068 #[pyo3(name = "run", signature = ())]
1090 fn py_run(slf: &Bound<'_, Self>, py: Python) -> PyResult<()> {
1091 let this = slf.borrow();
1092 if !this.clients.is_empty() {
1093 let asyncio = py.import("asyncio")?;
1094 if asyncio.call_method0("get_running_loop").is_ok() {
1095 return Err(to_pyruntime_err(
1096 "run() cannot be called from a running event loop; await run_async()",
1097 ));
1098 }
1099
1100 let handle = this.handle.clone();
1101 drop(this);
1102 let driver = PyModule::from_code(
1103 py,
1104 c_str!("async def run(node):\n return await node.run_async()\n"),
1105 c_str!("client_run.py"),
1106 c_str!("client_run"),
1107 )?;
1108 let event_loop = asyncio.call_method0("new_event_loop")?;
1109 let signal = py.import("signal")?;
1110 let threading = py.import("threading")?;
1111 let is_main = threading
1112 .call_method0("current_thread")?
1113 .is(&threading.call_method0("main_thread")?);
1114
1115 let mut handlers = Vec::new();
1116
1117 let result = (|| -> PyResult<()> {
1118 if is_main {
1119 let callback = new_sync_py_callback(py, move |_args, _kwargs| {
1120 handle.stop();
1121 Ok(())
1122 })?;
1123
1124 for name in ["SIGINT", "SIGTERM"] {
1125 if let Ok(signum) = signal.getattr(name) {
1126 let original = signal.call_method1("signal", (&signum, &callback))?;
1127 handlers.push((signum, original));
1128 }
1129 }
1130 }
1131
1132 let coroutine = driver.getattr("run")?.call1((slf,))?;
1133 event_loop.call_method1("run_until_complete", (coroutine,))?;
1134 Ok(())
1135 })();
1136
1137 return finish_owned_run(&event_loop, &signal, handlers, result);
1138 }
1139
1140 if this.node()?.is_running() {
1141 return Err(to_pyruntime_err("LiveNode is already running"));
1142 }
1143
1144 let handle = this.node()?.handle();
1146
1147 let signal_module = py.import("signal")?;
1149 let original_handler =
1150 signal_module.call_method1("signal", (2, signal_module.getattr("SIG_DFL")?))?; let handle_for_signal = handle;
1154
1155 let signal_callback = new_sync_py_callback(
1156 py,
1157 move |_args: &pyo3::Bound<'_, PyTuple>,
1158 _kwargs: Option<&pyo3::Bound<'_, PyDict>>|
1159 -> PyResult<()> {
1160 log::info!("Python signal handler called");
1161 handle_for_signal.stop();
1162 Ok(())
1163 },
1164 )?;
1165
1166 signal_module.call_method1("signal", (2, signal_callback))?;
1168
1169 let mut node = this.node_mut()?;
1171 let result = run_live_node_detached(py, &mut node);
1172
1173 signal_module.call_method1("signal", (2, original_handler))?;
1175
1176 result
1177 }
1178
1179 #[pyo3(name = "stop")]
1188 fn py_stop(&self, py: Python<'_>) -> PyResult<()> {
1189 let mut node = self.node_mut()?;
1190 if !node.is_running() {
1191 return Err(to_pyruntime_err("LiveNode is not running"));
1192 }
1193
1194 stop_live_node_detached(py, &mut node)
1195 }
1196
1197 #[pyo3(name = "dispose")]
1202 fn py_dispose(&self, py: Python<'_>) -> PyResult<()> {
1203 if self.is_consumed() {
1204 return Ok(());
1205 }
1206
1207 let mut node = self.node_mut()?;
1208
1209 let stop_result = if node.is_running() {
1210 stop_live_node_detached(py, &mut node)
1211 } else {
1212 Ok(())
1213 };
1214
1215 if let Err(ref err) = stop_result {
1216 log::error!("Failed to stop LiveNode during dispose: {err}");
1217 }
1218
1219 node.dispose();
1220 drop(node);
1221 let cleanup_result = self.clients.finish(py);
1222 stop_result.and(cleanup_result)
1223 }
1224
1225 #[pyo3(name = "add_actor")]
1231 fn py_add_actor(&self, actor: &Bound<'_, PyAny>) -> PyResult<()> {
1232 if self.node()?.state() != NodeState::Idle {
1233 return Err(to_pyruntime_err(
1234 "Cannot add actor while node is running, add actors before running the node",
1235 ));
1236 }
1237
1238 log::debug!("`add_actor` with a constructed instance");
1239
1240 let actor = actor.clone().unbind();
1241
1242 let actor_id = Python::attach(|py| {
1243 let actor = actor.bind(py);
1244 let config = actor
1245 .getattr("config")
1246 .ok()
1247 .filter(|config| !config.is_none());
1248 prepare_python_actor(actor, config.as_ref())
1249 })
1250 .map_err(to_pyruntime_err)?;
1251
1252 self.register_python_actor(&actor, actor_id)
1253 }
1254
1255 #[pyo3(name = "add_actor_from_config")]
1256 #[expect(clippy::needless_pass_by_value)]
1257 fn py_add_actor_from_config(&self, _py: Python, config: ImportableActorConfig) -> PyResult<()> {
1258 log::debug!("`add_actor_from_config` with: {config:?}");
1259
1260 let parts: Vec<&str> = config.actor_path.split(':').collect();
1262 if parts.len() != 2 {
1263 return Err(to_pyvalue_err(
1264 "actor_path must be in format 'module.path:ClassName'",
1265 ));
1266 }
1267
1268 let (module_name, class_name) = (parts[0], parts[1]);
1269
1270 log::info!("Importing actor from module: {module_name} class: {class_name}");
1271
1272 let (python_actor, actor_id) =
1273 Python::attach(|py| -> anyhow::Result<(Py<PyAny>, ActorId)> {
1274 let actor_module = py
1275 .import(module_name)
1276 .map_err(|e| anyhow::anyhow!("Failed to import module {module_name}: {e}"))?;
1277 let actor_class = actor_module
1278 .getattr(class_name)
1279 .map_err(|e| anyhow::anyhow!("Failed to get class {class_name}: {e}"))?;
1280
1281 let config_instance =
1282 create_config_instance(py, &config.config_path, &config.config)?;
1283
1284 let python_actor = if let Some(config_obj) = config_instance.as_ref() {
1285 actor_class.call1((config_obj,))?
1286 } else {
1287 actor_class.call0()?
1288 };
1289
1290 log::debug!("Created Python actor instance: {python_actor:?}");
1291
1292 let actor_id = prepare_python_actor(&python_actor, config_instance.as_ref())?;
1293
1294 Ok((python_actor.unbind(), actor_id))
1295 })
1296 .map_err(to_pyruntime_err)?;
1297
1298 self.register_python_actor(&python_actor, actor_id)
1299 }
1300
1301 #[allow(
1317 unsafe_code,
1318 reason = "Required for Python strategy component registration"
1319 )]
1320 #[pyo3(name = "add_strategy")]
1321 fn py_add_strategy(&self, strategy: &Bound<'_, PyAny>) -> PyResult<()> {
1322 if self.node()?.state() != NodeState::Idle {
1323 return Err(to_pyruntime_err(
1324 "Cannot add strategy while node is running, add strategies before running the node",
1325 ));
1326 }
1327
1328 log::debug!("`add_strategy` with a constructed instance");
1329
1330 let strategy = strategy.clone().unbind();
1331
1332 let strategy_id = self
1333 .node_mut()?
1334 .kernel_mut()
1335 .trader
1336 .borrow_mut()
1337 .prepare_python_strategy_instance(&strategy)
1338 .map_err(to_pyruntime_err)?;
1339
1340 let (external_order_instrument_ids, oms_type) = Python::attach(
1341 |py| -> anyhow::Result<(Option<Vec<InstrumentId>>, Option<OmsType>)> {
1342 let bound = strategy.bind(py);
1343 let config_obj = bound
1344 .getattr("config")
1345 .ok()
1346 .filter(|config| !config.is_none());
1347
1348 let mut py_strategy_ref = bound
1349 .extract::<PyRefMut<PyStrategy>>()
1350 .map_err(Into::<PyErr>::into)
1351 .map_err(|e| anyhow::anyhow!("Failed to extract PyStrategy: {e}"))?;
1352
1353 if let Some(config_obj) = config_obj.as_ref()
1354 && let Some(claims) =
1355 extract_external_order_instrument_ids_config_attr(config_obj)?
1356 {
1357 py_strategy_ref.set_external_order_instrument_ids(Some(claims));
1358 }
1359
1360 let claims = py_strategy_ref.external_order_instrument_ids();
1361 let oms_type = config_obj
1362 .as_ref()
1363 .and_then(|cfg| cfg.getattr("oms_type").ok())
1364 .filter(|value| !value.is_none())
1365 .and_then(|value| value.extract::<OmsType>().ok());
1366
1367 Ok((claims, oms_type))
1368 },
1369 )
1370 .map_err(to_pyruntime_err)?;
1371
1372 let external_order_instrument_ids =
1373 external_order_instrument_ids.filter(|claims| !claims.is_empty());
1374 if let Some(claims) = &external_order_instrument_ids {
1375 self.node_mut()?
1376 .register_external_order_claims(strategy_id, claims)
1377 .map_err(to_pyruntime_err)?;
1378 }
1379
1380 let commit_result = self
1381 .node_mut()?
1382 .kernel_mut()
1383 .trader
1384 .borrow_mut()
1385 .commit_python_strategy_instance(&strategy);
1386
1387 if let Err(commit_error) = commit_result {
1388 if let Some(instrument_ids) = external_order_instrument_ids.as_deref()
1389 && let Err(rollback_error) = self
1390 .node_mut()?
1391 .rollback_external_order_claims(strategy_id, instrument_ids)
1392 {
1393 return Err(to_pyruntime_err(format!(
1394 "Failed to add strategy {strategy_id}: {commit_error}; failed to roll back external order claims: {rollback_error}"
1395 )));
1396 }
1397
1398 return Err(to_pyruntime_err(commit_error));
1399 }
1400
1401 if let Some(oms_type) = oms_type {
1402 self.node_mut()?
1403 .kernel()
1404 .exec_engine
1405 .borrow_mut()
1406 .register_oms_type(strategy_id, oms_type);
1407 }
1408
1409 log::info!("Registered Python strategy {strategy_id}");
1410 Ok(())
1411 }
1412
1413 #[pyo3(name = "add_strategy_from_config")]
1414 #[expect(clippy::needless_pass_by_value)]
1415 fn py_add_strategy_from_config(
1416 &self,
1417 _py: Python,
1418 config: ImportableStrategyConfig,
1419 ) -> PyResult<()> {
1420 log::debug!("`add_strategy_from_config` with: {config:?}");
1421
1422 let parts: Vec<&str> = config.strategy_path.split(':').collect();
1424 if parts.len() != 2 {
1425 return Err(to_pyvalue_err(
1426 "strategy_path must be in format 'module.path:ClassName'",
1427 ));
1428 }
1429
1430 let (module_name, class_name) = (parts[0], parts[1]);
1431
1432 log::info!("Importing strategy from module: {module_name} class: {class_name}");
1433
1434 let python_strategy = Python::attach(|py| -> anyhow::Result<Py<PyAny>> {
1437 let strategy_module = py
1438 .import(module_name)
1439 .map_err(|e| anyhow::anyhow!("Failed to import module {module_name}: {e}"))?;
1440 let strategy_class = strategy_module
1441 .getattr(class_name)
1442 .map_err(|e| anyhow::anyhow!("Failed to get class {class_name}: {e}"))?;
1443
1444 let config_instance = create_config_instance(py, &config.config_path, &config.config)?;
1445
1446 let python_strategy = if let Some(config_obj) = config_instance {
1447 strategy_class.call1((config_obj,))?
1448 } else {
1449 strategy_class.call0()?
1450 };
1451
1452 log::debug!("Created Python strategy instance: {python_strategy:?}");
1453
1454 Ok(python_strategy.unbind())
1455 })
1456 .map_err(to_pyruntime_err)?;
1457
1458 let strategy_id = self
1459 .node_mut()?
1460 .kernel_mut()
1461 .trader
1462 .borrow_mut()
1463 .prepare_python_strategy_instance(&python_strategy)
1464 .map_err(to_pyruntime_err)?;
1465
1466 Python::attach(|py| -> anyhow::Result<()> {
1467 let bound = python_strategy.bind(py);
1468 let config_obj = bound
1469 .getattr("config")
1470 .ok()
1471 .filter(|config| !config.is_none());
1472
1473 let mut py_strategy_ref = bound
1474 .extract::<PyRefMut<PyStrategy>>()
1475 .map_err(Into::<PyErr>::into)
1476 .map_err(|e| anyhow::anyhow!("Failed to extract PyStrategy: {e}"))?;
1477
1478 if let Some(config_obj) = config_obj.as_ref()
1479 && let Some(claims) = extract_external_order_instrument_ids_config_attr(config_obj)?
1480 {
1481 py_strategy_ref.set_external_order_instrument_ids(Some(claims));
1482 }
1483
1484 Ok(())
1485 })
1486 .map_err(to_pyruntime_err)?;
1487
1488 let external_order_instrument_ids =
1490 Python::attach(|py| -> anyhow::Result<Option<Vec<_>>> {
1491 let py_strategy = python_strategy.bind(py);
1492 let py_strategy_ref = py_strategy
1493 .extract::<PyRef<PyStrategy>>()
1494 .map_err(Into::<PyErr>::into)
1495 .map_err(|e| anyhow::anyhow!("Failed to extract PyStrategy: {e}"))?;
1496
1497 Ok(py_strategy_ref.external_order_instrument_ids())
1498 })
1499 .map_err(to_pyruntime_err)?;
1500
1501 let external_order_instrument_ids =
1502 external_order_instrument_ids.filter(|claims| !claims.is_empty());
1503 if let Some(claims) = &external_order_instrument_ids {
1504 self.node_mut()?
1505 .register_external_order_claims(strategy_id, claims)
1506 .map_err(to_pyruntime_err)?;
1507 }
1508
1509 let commit_result = self
1511 .node_mut()?
1512 .kernel_mut()
1513 .trader
1514 .borrow_mut()
1515 .commit_python_strategy_instance(&python_strategy);
1516
1517 if let Err(commit_error) = commit_result {
1518 if let Some(instrument_ids) = external_order_instrument_ids.as_deref()
1519 && let Err(rollback_error) = self
1520 .node_mut()?
1521 .rollback_external_order_claims(strategy_id, instrument_ids)
1522 {
1523 return Err(to_pyruntime_err(format!(
1524 "Failed to add strategy {strategy_id}: {commit_error}; failed to roll back external order claims: {rollback_error}"
1525 )));
1526 }
1527
1528 return Err(to_pyruntime_err(commit_error));
1529 }
1530
1531 log::info!("Registered Python strategy {strategy_id}");
1532 Ok(())
1533 }
1534
1535 #[pyo3(name = "add_exec_algorithm")]
1546 fn py_add_exec_algorithm(&self, exec_algorithm: &Bound<'_, PyAny>) -> PyResult<()> {
1547 if self.node()?.state() != NodeState::Idle {
1548 return Err(to_pyruntime_err(
1549 "Cannot add exec algorithm while node is running, add exec algorithms before running the node",
1550 ));
1551 }
1552
1553 log::debug!("`add_exec_algorithm` with a constructed instance");
1554
1555 let exec_algorithm = exec_algorithm.clone().unbind();
1556
1557 let py_exec_algorithm = Python::attach(|py| -> anyhow::Result<PyExecutionAlgorithm> {
1558 let bound = exec_algorithm.bind(py);
1559 let config = bound
1560 .getattr("config")
1561 .ok()
1562 .filter(|config| !config.is_none());
1563
1564 let mut py_exec_algorithm_ref = bound
1565 .extract::<PyRefMut<PyExecutionAlgorithm>>()
1566 .map_err(Into::<PyErr>::into)
1567 .map_err(|e| {
1568 anyhow::anyhow!(
1569 "LiveNode.add_exec_algorithm requires a Python v2 ExecutionAlgorithm instance; use add_exec_algorithm_from_config for DataActor-based algorithms: {e}"
1570 )
1571 })?;
1572
1573 if let Some(config) = config.as_ref() {
1574 py_exec_algorithm_ref.configure_from_py_config(config)?;
1575 }
1576
1577 py_exec_algorithm_ref.set_python_instance(bound)?;
1578 Ok(py_exec_algorithm_ref.clone())
1579 })
1580 .map_err(to_pyruntime_err)?;
1581
1582 let exec_algorithm_id = self
1583 .node_mut()?
1584 .kernel_mut()
1585 .trader
1586 .borrow_mut()
1587 .add_py_execution_algorithm_instance(py_exec_algorithm, &exec_algorithm)
1588 .map_err(to_pyruntime_err)?;
1589
1590 log::info!("Registered Python exec algorithm {exec_algorithm_id}");
1591 Ok(())
1592 }
1593
1594 #[pyo3(name = "add_exec_algorithm_from_config")]
1595 #[expect(clippy::needless_pass_by_value)]
1596 fn py_add_exec_algorithm_from_config(
1597 &self,
1598 _py: Python,
1599 config: ImportableExecutionAlgorithmConfig,
1600 ) -> PyResult<()> {
1601 if self.node()?.is_running() {
1602 return Err(to_pyruntime_err(
1603 "Cannot add exec algorithm while node is running",
1604 ));
1605 }
1606
1607 log::debug!("`add_exec_algorithm_from_config` with: {config:?}");
1608
1609 let parts: Vec<&str> = config.exec_algorithm_path.split(':').collect();
1610 if parts.len() != 2 {
1611 return Err(to_pyvalue_err(
1612 "exec_algorithm_path must be in format 'module.path:ClassName'",
1613 ));
1614 }
1615
1616 let (module_name, class_name) = (parts[0], parts[1]);
1617
1618 log::info!("Importing exec algorithm from module: {module_name} class: {class_name}");
1619
1620 let (python_exec_algorithm, py_execution_algorithm, actor_id) = Python::attach(
1622 |py| -> anyhow::Result<(Py<PyAny>, Option<PyExecutionAlgorithm>, ActorId)> {
1623 let algo_module = py
1624 .import(module_name)
1625 .map_err(|e| anyhow::anyhow!("Failed to import module {module_name}: {e}"))?;
1626 let algo_class = algo_module
1627 .getattr(class_name)
1628 .map_err(|e| anyhow::anyhow!("Failed to get class {class_name}: {e}"))?;
1629
1630 let config_instance =
1631 create_config_instance(py, &config.config_path, &config.config)?;
1632
1633 let python_exec_algorithm = if let Some(config_obj) = config_instance.clone() {
1634 algo_class.call1((config_obj,))?
1635 } else {
1636 algo_class.call0()?
1637 };
1638
1639 log::debug!("Created Python exec algorithm instance: {python_exec_algorithm:?}");
1640
1641 if let Ok(mut py_exec_algorithm_ref) =
1642 python_exec_algorithm.extract::<PyRefMut<PyExecutionAlgorithm>>()
1643 {
1644 if let Some(config_obj) = config_instance.as_ref() {
1645 py_exec_algorithm_ref.configure_from_py_config(config_obj)?;
1646 }
1647
1648 py_exec_algorithm_ref.set_python_instance(&python_exec_algorithm)?;
1649 let actor_id = ActorId::new(py_exec_algorithm_ref.exec_algorithm_id().inner());
1650
1651 return Ok((
1652 python_exec_algorithm.unbind(),
1653 Some(py_exec_algorithm_ref.clone()),
1654 actor_id,
1655 ));
1656 }
1657
1658 let mut py_data_actor_ref = python_exec_algorithm
1659 .extract::<PyRefMut<PyDataActor>>()
1660 .map_err(Into::<PyErr>::into)
1661 .map_err(|e| anyhow::anyhow!("Failed to extract PyDataActor: {e}"))?;
1662
1663 if let Some(config_obj) = config_instance.as_ref() {
1665 let id_attr = config_obj
1666 .getattr("exec_algorithm_id")
1667 .ok()
1668 .filter(|v| !v.is_none())
1669 .or_else(|| config_obj.getattr("actor_id").ok().filter(|v| !v.is_none()));
1670
1671 if let Some(id_value) = id_attr {
1672 let actor_id_val = if let Ok(eaid) = id_value.extract::<ExecAlgorithmId>() {
1673 ActorId::new(eaid.inner())
1674 } else if let Ok(aid) = id_value.extract::<ActorId>() {
1675 aid
1676 } else if let Ok(aid_str) = id_value.extract::<String>() {
1677 ActorId::new_checked(&aid_str)?
1678 } else {
1679 anyhow::bail!("Invalid `exec_algorithm_id`/`actor_id` type");
1680 };
1681
1682 py_data_actor_ref.set_actor_id(actor_id_val);
1683 }
1684
1685 if let Some(val) = extract_bool_config_attr(config_obj, "log_events") {
1686 py_data_actor_ref.set_log_events(val);
1687 }
1688
1689 if let Some(val) = extract_bool_config_attr(config_obj, "log_commands") {
1690 py_data_actor_ref.set_log_commands(val);
1691 }
1692 }
1693
1694 py_data_actor_ref.set_python_instance(&python_exec_algorithm)?;
1695
1696 let actor_id = py_data_actor_ref.actor_id();
1697
1698 Ok((python_exec_algorithm.unbind(), None, actor_id))
1699 },
1700 )
1701 .map_err(to_pyruntime_err)?;
1702
1703 let exec_algorithm_id = if let Some(py_execution_algorithm) = py_execution_algorithm {
1704 if self.node()?.state() != NodeState::Idle {
1707 return Err(to_pyruntime_err(
1708 "Cannot add exec algorithm while node is running, add exec algorithms before running the node",
1709 ));
1710 }
1711
1712 self.node_mut()?
1713 .kernel_mut()
1714 .trader
1715 .borrow_mut()
1716 .add_py_execution_algorithm_instance(py_execution_algorithm, &python_exec_algorithm)
1717 .map_err(to_pyruntime_err)?
1718 } else {
1719 self.node_mut()?
1722 .kernel_mut()
1723 .trader
1724 .borrow_mut()
1725 .add_python_exec_algorithm_instance(&python_exec_algorithm, actor_id)
1726 .map_err(to_pyruntime_err)?
1727 };
1728
1729 log::info!("Registered Python exec algorithm {exec_algorithm_id}");
1730 Ok(())
1731 }
1732
1733 #[pyo3(name = "add_plugin", signature = (path, type_name, config=None, sha256=None))]
1739 fn py_add_plugin(
1740 &self,
1741 path: String,
1742 type_name: String,
1743 config: Option<HashMap<String, Py<PyAny>>>,
1744 sha256: Option<String>,
1745 ) -> PyResult<()> {
1746 let config = PluginConfig {
1747 path,
1748 type_name,
1749 config: match config {
1750 Some(config) => coerce_json_config(config)?,
1751 None => HashMap::new(),
1752 },
1753 sha256,
1754 };
1755
1756 self.node_mut()?
1757 .add_plugin(config)
1758 .map_err(to_pyruntime_err)
1759 }
1760
1761 #[cfg(feature = "examples")]
1767 #[pyo3(name = "add_builtin_actor")]
1768 fn py_add_builtin_actor(&self, type_name: &str, config: &Bound<'_, PyAny>) -> PyResult<()> {
1769 let register = builtin_actor_register(type_name).ok_or_else(|| {
1770 to_pytype_err(format!("Unsupported built-in actor type: {type_name}"))
1771 })?;
1772
1773 let mut node = self.node_mut()?;
1774 register(&mut node, config)
1775 }
1776
1777 #[cfg(feature = "examples")]
1783 #[pyo3(name = "add_builtin_strategy")]
1784 fn py_add_builtin_strategy(&self, type_name: &str, config: &Bound<'_, PyAny>) -> PyResult<()> {
1785 let register = builtin_strategy_register(type_name).ok_or_else(|| {
1786 to_pytype_err(format!("Unsupported built-in strategy type: {type_name}"))
1787 })?;
1788
1789 let mut node = self.node_mut()?;
1790 register(&mut node, config)
1791 }
1792
1793 fn __repr__(&self) -> String {
1794 format!(
1795 "LiveNode(trader_id={}, environment={}, running={})",
1796 self.node()
1797 .map_or_else(|_| "<running>".to_string(), |n| n.trader_id().to_string()),
1798 self.node().map_or_else(
1799 |_| "<running>".to_string(),
1800 |n| format!("{:?}", n.environment())
1801 ),
1802 self.py_is_running()
1803 )
1804 }
1805}
1806
1807impl PyLiveNode {
1808 fn register_python_actor(&self, actor: &Py<PyAny>, actor_id: ActorId) -> PyResult<()> {
1809 if self
1810 .node()?
1811 .kernel()
1812 .trader
1813 .borrow()
1814 .actor_ids()
1815 .contains(&actor_id)
1816 {
1817 return Err(to_pyruntime_err(format!(
1818 "Actor '{actor_id}' is already registered"
1819 )));
1820 }
1821
1822 self.node_mut()?
1823 .kernel_mut()
1824 .trader
1825 .borrow_mut()
1826 .add_python_actor_instance(actor, actor_id)
1827 .map_err(to_pyruntime_err)?;
1828
1829 log::info!("Registered Python actor {actor_id}");
1830 Ok(())
1831 }
1832}
1833
1834fn finish_owned_run<'py>(
1835 event_loop: &Bound<'py, PyAny>,
1836 signal: &Bound<'py, PyModule>,
1837 handlers: Vec<(Bound<'py, PyAny>, Bound<'py, PyAny>)>,
1838 result: PyResult<()>,
1839) -> PyResult<()> {
1840 let mut errors: Vec<PyErr> = result.err().into_iter().collect();
1841
1842 if let Err(e) = event_loop.call_method0("close") {
1843 errors.push(e);
1844 }
1845
1846 for (signum, original) in handlers {
1847 if let Err(e) = signal.call_method1("signal", (signum, original)) {
1848 errors.push(e);
1849 }
1850 }
1851
1852 match errors.len() {
1853 0 => Ok(()),
1854 1 => Err(errors.pop().unwrap()),
1855 _ => Err(PyBaseExceptionGroup::new_err((
1856 "LiveNode run and cleanup failed",
1857 errors,
1858 ))),
1859 }
1860}
1861
1862fn new_sync_py_callback<F>(py: Python<'_>, closure: F) -> PyResult<Bound<'_, PyCFunction>>
1863where
1864 F: Fn(&Bound<'_, PyTuple>, Option<&Bound<'_, PyDict>>) -> PyResult<()> + Send + Sync + 'static,
1865{
1866 PyCFunction::new_closure(py, None, None, closure)
1867}
1868
1869#[allow(unsafe_code)]
1870fn run_live_node_detached(py: Python<'_>, node: &mut LiveNode) -> PyResult<()> {
1871 let node_ptr = SendPtr(std::ptr::from_mut::<LiveNode>(node));
1872
1873 unsafe {
1877 py.detach(move || {
1878 let ptr = node_ptr;
1879 get_runtime().block_on(async { (*ptr.0).run().await })
1880 })
1881 }
1882 .map_err(to_pyruntime_err)
1883}
1884
1885#[allow(unsafe_code)]
1886fn stop_live_node_detached(py: Python<'_>, node: &mut LiveNode) -> PyResult<()> {
1887 let node_ptr = SendPtr(std::ptr::from_mut::<LiveNode>(node));
1888
1889 unsafe {
1893 py.detach(move || {
1894 let ptr = node_ptr;
1895 get_runtime().block_on(async { (*ptr.0).stop().await })
1896 })
1897 }
1898 .map_err(to_pyruntime_err)
1899}
1900
1901fn create_config_instance<'py>(
1910 py: Python<'py>,
1911 config_path: &str,
1912 config: &HashMap<String, serde_json::Value>,
1913) -> anyhow::Result<Option<Bound<'py, PyAny>>> {
1914 if config_path.is_empty() && config.is_empty() {
1915 log::debug!("No config_path or empty config, using None");
1916 return Ok(None);
1917 }
1918
1919 let config_parts: Vec<&str> = config_path.split(':').collect();
1920 if config_parts.len() != 2 {
1921 anyhow::bail!("config_path must be in format 'module.path:ClassName', was {config_path}");
1922 }
1923
1924 let (config_module_name, config_class_name) = (config_parts[0], config_parts[1]);
1925
1926 log::debug!(
1927 "Importing config class from module: {config_module_name} class: {config_class_name}"
1928 );
1929
1930 let config_module = py
1931 .import(config_module_name)
1932 .map_err(|e| anyhow::anyhow!("Failed to import config module {config_module_name}: {e}"))?;
1933 let config_class = config_module
1934 .getattr(config_class_name)
1935 .map_err(|e| anyhow::anyhow!("Failed to get config class {config_class_name}: {e}"))?;
1936
1937 let py_dict = PyDict::new(py);
1939
1940 for (key, value) in config {
1941 let py_value = config_value_to_py(py, key, value)?;
1942 py_dict.set_item(key, py_value)?;
1943 }
1944
1945 log::debug!("Created config dict: {py_dict:?}");
1946
1947 let config_instance = match config_class.call((), Some(&py_dict)) {
1949 Ok(instance) => {
1950 log::debug!("Created config instance with kwargs");
1951 instance
1952 }
1953 Err(kwargs_err) => {
1954 log::debug!("Failed to create config with kwargs: {kwargs_err}");
1955
1956 match config_class.call0() {
1957 Ok(instance) => {
1958 log::debug!("Created default config instance, setting attributes");
1959
1960 for (key, value) in config {
1961 let py_value = config_value_to_py(py, key, value)?;
1962
1963 if let Err(setattr_err) = instance.setattr(key, py_value) {
1964 anyhow::bail!("Failed to set attribute {key}: {setattr_err}");
1965 }
1966 }
1967
1968 if instance.hasattr("__post_init__")? {
1971 instance.call_method0("__post_init__")?;
1972 }
1973
1974 instance
1975 }
1976 Err(default_err) => {
1977 anyhow::bail!(
1978 "Failed to create config instance. \
1979 Tried kwargs: {kwargs_err}, default: {default_err}"
1980 );
1981 }
1982 }
1983 }
1984 };
1985
1986 log::debug!("Created config instance: {config_instance:?}");
1987
1988 Ok(Some(config_instance))
1989}
1990
1991fn config_value_to_py<'py>(
1992 py: Python<'py>,
1993 key: &str,
1994 value: &serde_json::Value,
1995) -> anyhow::Result<Bound<'py, PyAny>> {
1996 if key == "actor_id"
1997 && let Some(actor_id) = value.as_str()
1998 {
1999 return Ok(ActorId::new_checked(actor_id)?
2000 .into_pyobject(py)?
2001 .into_any());
2002 }
2003
2004 if key == "strategy_id"
2005 && let Some(strategy_id) = value.as_str()
2006 {
2007 return Ok(StrategyId::new_checked(strategy_id)?
2008 .into_pyobject(py)?
2009 .into_any());
2010 }
2011
2012 let json_str = serde_json::to_string(value)
2013 .map_err(|e| anyhow::anyhow!("Failed to serialize config value: {e}"))?;
2014 Ok(PyModule::import(py, "json")?
2015 .call_method("loads", (json_str,), None)?
2016 .into_any())
2017}
2018
2019fn extract_bool_config_attr(config_obj: &Bound<'_, PyAny>, attr: &str) -> Option<bool> {
2024 config_obj
2025 .getattr(attr)
2026 .ok()
2027 .and_then(|val| val.extract::<bool>().ok())
2028}
2029
2030fn extract_external_order_instrument_ids_config_attr(
2031 config_obj: &Bound<'_, PyAny>,
2032) -> anyhow::Result<Option<Vec<InstrumentId>>> {
2033 let Ok(claims) = config_obj.getattr("external_order_instrument_ids") else {
2034 return Ok(None);
2035 };
2036
2037 if claims.is_none() {
2038 return Ok(None);
2039 }
2040
2041 if let Ok(claims) = claims.extract::<Vec<InstrumentId>>() {
2042 return Ok(Some(claims));
2043 }
2044
2045 let claim_strings = claims
2046 .extract::<Vec<String>>()
2047 .map_err(|e| anyhow::anyhow!("Invalid `external_order_instrument_ids` type: {e}"))?;
2048
2049 let claims = claim_strings
2050 .into_iter()
2051 .map(|claim| {
2052 InstrumentId::from_str(&claim).map_err(|e| {
2053 anyhow::anyhow!(
2054 "Invalid `external_order_instrument_ids` instrument ID {claim}: {e}"
2055 )
2056 })
2057 })
2058 .collect::<anyhow::Result<Vec<_>>>()?;
2059
2060 Ok(Some(claims))
2061}
2062
2063#[cfg(feature = "examples")]
2064type BuiltinActorRegister = for<'py> fn(&mut LiveNode, &Bound<'py, PyAny>) -> PyResult<()>;
2065
2066#[cfg(feature = "examples")]
2067type BuiltinStrategyRegister = for<'py> fn(&mut LiveNode, &Bound<'py, PyAny>) -> PyResult<()>;
2068
2069#[cfg(feature = "examples")]
2070fn builtin_actor_register(type_name: &str) -> Option<BuiltinActorRegister> {
2071 match type_name {
2072 "BookImbalanceActor" => Some(register_book_imbalance_actor),
2073 "DataTester" => Some(register_data_tester),
2074 _ => None,
2075 }
2076}
2077
2078#[cfg(feature = "examples")]
2079fn builtin_strategy_register(type_name: &str) -> Option<BuiltinStrategyRegister> {
2080 match type_name {
2081 "CompositeMarketMaker" => Some(register_composite_market_maker),
2082 "DeltaNeutralVol" => Some(register_delta_neutral_vol),
2083 "EmaCross" => Some(register_ema_cross),
2084 "ExecTester" => Some(register_exec_tester),
2085 "GridMarketMaker" => Some(register_grid_market_maker),
2086 "HurstVpinDirectional" => Some(register_hurst_vpin_directional),
2087 _ => None,
2088 }
2089}
2090
2091#[cfg(feature = "examples")]
2092fn register_composite_market_maker(node: &mut LiveNode, config: &Bound<'_, PyAny>) -> PyResult<()> {
2093 let config = config.extract::<CompositeMarketMakerConfig>()?;
2094 node.add_strategy(CompositeMarketMaker::new(config))
2095 .map_err(to_pyruntime_err)
2096}
2097
2098#[cfg(feature = "examples")]
2099fn register_delta_neutral_vol(node: &mut LiveNode, config: &Bound<'_, PyAny>) -> PyResult<()> {
2100 let config = config.extract::<DeltaNeutralVolConfig>()?;
2101 node.add_strategy(DeltaNeutralVol::new(config))
2102 .map_err(to_pyruntime_err)
2103}
2104
2105#[cfg(feature = "examples")]
2106fn register_ema_cross(node: &mut LiveNode, config: &Bound<'_, PyAny>) -> PyResult<()> {
2107 let config = config.extract::<EmaCrossConfig>()?;
2108 node.add_strategy(EmaCross::from_config(config))
2109 .map_err(to_pyruntime_err)
2110}
2111
2112#[cfg(feature = "examples")]
2113fn register_exec_tester(node: &mut LiveNode, config: &Bound<'_, PyAny>) -> PyResult<()> {
2114 let config = config.extract::<ExecTesterConfig>()?;
2115 node.add_strategy(ExecTester::new(config))
2116 .map_err(to_pyruntime_err)
2117}
2118
2119#[cfg(feature = "examples")]
2120fn register_grid_market_maker(node: &mut LiveNode, config: &Bound<'_, PyAny>) -> PyResult<()> {
2121 let config = config.extract::<GridMarketMakerConfig>()?;
2122 node.add_strategy(GridMarketMaker::new(config))
2123 .map_err(to_pyruntime_err)
2124}
2125
2126#[cfg(feature = "examples")]
2127fn register_hurst_vpin_directional(node: &mut LiveNode, config: &Bound<'_, PyAny>) -> PyResult<()> {
2128 let config = config.extract::<HurstVpinDirectionalConfig>()?;
2129 let strategy =
2130 HurstVpinDirectional::new_checked(config).map_err(config_error_to_pyvalue_err)?;
2131 node.add_strategy(strategy).map_err(to_pyruntime_err)
2132}
2133
2134#[cfg(feature = "examples")]
2135fn register_book_imbalance_actor(node: &mut LiveNode, config: &Bound<'_, PyAny>) -> PyResult<()> {
2136 let config = config.extract::<BookImbalanceActorConfig>()?;
2137 node.add_actor(BookImbalanceActor::from_config(config))
2138 .map_err(to_pyruntime_err)
2139}
2140
2141#[cfg(feature = "examples")]
2142fn register_data_tester(node: &mut LiveNode, config: &Bound<'_, PyAny>) -> PyResult<()> {
2143 let config = config.extract::<DataTesterConfig>()?;
2144 node.add_actor(DataTester::new(config))
2145 .map_err(to_pyruntime_err)
2146}
2147
2148#[pyclass(name = "LiveNodeBuilder", module = "nautilus_trader.live", unsendable)]
2151#[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.live")]
2152pub struct PyLiveNodeBuilder {
2153 clients: PythonClients,
2154 state: Rc<Cell<PyLiveNodeBuilderState>>,
2155}
2156
2157#[pyo3_stub_gen::derive::gen_stub_pymethods]
2158#[pymethods]
2159impl PyLiveNodeBuilder {
2160 #[staticmethod]
2161 #[pyo3(name = "from_config", signature = (name, config=None, *, data_factories=None, exec_factories=None))]
2162 fn py_from_config(
2163 py: Python<'_>,
2164 name: String,
2165 config: Option<Py<LiveNodeConfig>>,
2166 data_factories: Option<Bound<'_, PyDict>>,
2167 exec_factories: Option<Bound<'_, PyDict>>,
2168 ) -> PyResult<Self> {
2169 let native_config = config
2170 .as_ref()
2171 .map(|config| config.bind(py).extract::<LiveNodeConfig>())
2172 .transpose()?
2173 .unwrap_or_default();
2174
2175 let builder = Self {
2176 clients: PythonClients::default(),
2177 state: Rc::new(Cell::new(PyLiveNodeBuilderState::Ready(Box::new(
2178 LiveNodeBuilder::from_config(native_config)
2179 .map_err(to_pyruntime_err)?
2180 .with_name(name),
2181 )))),
2182 };
2183
2184 if let Some(config) = config {
2185 let configured_clients = py
2186 .import("nautilus_trader.live.config")?
2187 .getattr("configured_clients")?;
2188 let data: Vec<(String, Py<PyAny>, Py<PyAny>)> = configured_clients
2189 .call1((config.getattr(py, "data_clients")?, data_factories))?
2190 .extract()?;
2191 let execution: Vec<(String, Py<PyAny>, Py<PyAny>)> = configured_clients
2192 .call1((config.getattr(py, "exec_clients")?, exec_factories))?
2193 .extract()?;
2194
2195 for (name, factory, config) in data {
2196 builder.py_add_data_client(Some(name), factory, config, None)?;
2197 }
2198
2199 for (name, factory, config) in execution {
2200 builder.py_add_exec_client(Some(name), factory, config, None)?;
2201 }
2202 }
2203
2204 Ok(builder)
2205 }
2206
2207 #[pyo3(name = "with_instance_id")]
2208 fn py_with_instance_id(&self, instance_id: UUID4) -> PyResult<Self> {
2209 self.update_builder(|builder| builder.with_instance_id(instance_id))
2210 }
2211
2212 #[pyo3(name = "with_load_state")]
2213 fn py_with_load_state(&self, load_state: bool) -> PyResult<Self> {
2214 self.update_builder(|builder| builder.with_load_state(load_state))
2215 }
2216
2217 #[pyo3(name = "with_save_state")]
2218 fn py_with_save_state(&self, save_state: bool) -> PyResult<Self> {
2219 self.update_builder(|builder| builder.with_save_state(save_state))
2220 }
2221
2222 #[pyo3(name = "with_timeout_connection")]
2223 fn py_with_timeout_connection(&self, timeout_secs: u64) -> PyResult<Self> {
2224 self.update_builder(|builder| builder.with_timeout_connection(timeout_secs))
2225 }
2226
2227 #[pyo3(name = "with_timeout_reconciliation")]
2228 fn py_with_timeout_reconciliation(&self, timeout_secs: u64) -> PyResult<Self> {
2229 self.update_builder(|builder| builder.with_timeout_reconciliation(timeout_secs))
2230 }
2231
2232 #[pyo3(name = "with_timeout_portfolio")]
2233 fn py_with_timeout_portfolio(&self, timeout_secs: u64) -> PyResult<Self> {
2234 self.update_builder(|builder| builder.with_timeout_portfolio(timeout_secs))
2235 }
2236
2237 #[pyo3(name = "with_timeout_disconnection_secs")]
2238 fn py_with_timeout_disconnection_secs(&self, timeout_secs: u64) -> PyResult<Self> {
2239 self.update_builder(|builder| builder.with_timeout_disconnection_secs(timeout_secs))
2240 }
2241
2242 #[pyo3(name = "with_delay_post_stop_secs")]
2243 fn py_with_delay_post_stop_secs(&self, delay_secs: u64) -> PyResult<Self> {
2244 self.update_builder(|builder| builder.with_delay_post_stop_secs(delay_secs))
2245 }
2246
2247 #[pyo3(name = "with_delay_shutdown_secs")]
2248 fn py_with_delay_shutdown_secs(&self, delay_secs: u64) -> PyResult<Self> {
2249 self.update_builder(|builder| builder.with_delay_shutdown_secs(delay_secs))
2250 }
2251
2252 #[pyo3(name = "with_reconciliation")]
2253 fn py_with_reconciliation(&self, reconciliation: bool) -> PyResult<Self> {
2254 self.update_builder(|builder| builder.with_reconciliation(reconciliation))
2255 }
2256
2257 #[pyo3(name = "with_controller")]
2258 fn py_with_controller(&self, controller: ImportableControllerConfig) -> PyResult<Self> {
2259 self.update_builder(|builder| builder.with_controller(controller))
2260 }
2261
2262 #[pyo3(name = "with_reconciliation_lookback_mins")]
2263 fn py_with_reconciliation_lookback_mins(&self, mins: u32) -> PyResult<Self> {
2264 self.update_builder(|builder| builder.with_reconciliation_lookback_mins(mins))
2265 }
2266
2267 #[pyo3(name = "with_cache_config")]
2268 fn py_with_cache_config(&self, config: CacheConfig) -> PyResult<Self> {
2269 self.update_builder(|builder| builder.with_cache_config(config))
2270 }
2271
2272 #[pyo3(name = "with_cache_database_factory")]
2273 fn py_with_cache_database_factory(&self, factory: Py<PyAny>) -> PyResult<Self> {
2274 let mut operation = self.begin_operation()?;
2275 let factory =
2276 Python::attach(|py| get_global_cache_database_factory_registry().extract(py, factory))?;
2277 let builder = operation.take_builder()?;
2278 operation.complete(builder.with_cache_database_factory(factory));
2279 Ok(self.shared())
2280 }
2281
2282 #[pyo3(name = "with_msgbus_config")]
2283 fn py_with_msgbus_config(&self, config: MessageBusConfig) -> PyResult<Self> {
2284 self.update_builder(|builder| builder.with_msgbus_config(config))
2285 }
2286
2287 #[pyo3(name = "with_external_msgbus_factory")]
2288 fn py_with_external_msgbus_factory(&self, factory: Py<PyAny>) -> PyResult<Self> {
2289 let mut operation = self.begin_operation()?;
2290 let factory =
2291 Python::attach(|py| get_global_msgbus_factory_registry().extract(py, factory))?;
2292 let builder = operation.take_builder()?;
2293 operation.complete(builder.with_external_msgbus_factory(factory));
2294 Ok(self.shared())
2295 }
2296
2297 #[pyo3(name = "with_portfolio_config")]
2298 fn py_with_portfolio_config(&self, config: PortfolioConfig) -> PyResult<Self> {
2299 self.update_builder(|builder| builder.with_portfolio_config(config))
2300 }
2301
2302 #[pyo3(name = "with_data_engine_config")]
2303 fn py_with_data_engine_config(&self, config: LiveDataEngineConfig) -> PyResult<Self> {
2304 self.update_builder(|builder| builder.with_data_engine_config(config))
2305 }
2306
2307 #[pyo3(name = "with_risk_engine_config")]
2308 fn py_with_risk_engine_config(&self, config: LiveRiskEngineConfig) -> PyResult<Self> {
2309 self.update_builder(|builder| builder.with_risk_engine_config(config))
2310 }
2311
2312 #[pyo3(name = "with_exec_engine_config")]
2313 fn py_with_exec_engine_config(&self, config: LiveExecutionEngineConfig) -> PyResult<Self> {
2314 self.update_builder(|builder| builder.with_exec_engine_config(config))
2315 }
2316
2317 #[pyo3(name = "with_logging")]
2318 fn py_with_logging(&self, logging: LoggerConfig) -> PyResult<Self> {
2319 self.update_builder(|builder| builder.with_logging(logging))
2320 }
2321
2322 #[pyo3(name = "add_data_client", signature = (name, factory, config, routing=None))]
2323 fn py_add_data_client(
2324 &self,
2325 name: Option<String>,
2326 factory: Py<PyAny>,
2327 config: Py<PyAny>,
2328 routing: Option<RoutingConfig>,
2329 ) -> PyResult<Self> {
2330 let mut operation = self.begin_operation()?;
2331 Python::attach(|py| -> PyResult<Self> {
2332 let (factory, config) = resolve_registered_client_pair(py, factory, config)?;
2333 let registry = get_global_pyo3_registry();
2334 let is_custom =
2335 python_client_factory_is_custom(py, factory.bind(py), "DataClientFactory")?;
2336
2337 if is_custom {
2338 config.extract::<crate::config::DataClientConfig>(py)?;
2339 let client_name =
2340 name.ok_or_else(|| to_pyvalue_err("Python clients require an explicit name"))?;
2341
2342 let routing = match routing {
2343 Some(routing) => routing,
2344 None => config
2345 .getattr(py, "routing")?
2346 .extract::<RoutingConfig>(py)?,
2347 };
2348
2349 if operation
2350 .builder
2351 .as_ref()
2352 .is_some_and(|builder| builder.has_data_client(&client_name))
2353 {
2354 return Err(to_pyvalue_err(format!(
2355 "Data client '{client_name}' is already registered"
2356 )));
2357 }
2358
2359 let builder = operation.take_builder()?;
2360
2361 let updated = builder
2362 .add_data_client_with_routing(
2363 Some(client_name),
2364 Box::new(PythonDataFactory {
2365 factory,
2366 clients: self.clients.clone(),
2367 }),
2368 Box::new(PythonClientConfig(config)),
2369 routing,
2370 )
2371 .map_err(to_pyruntime_err)?;
2372
2373 operation.complete(updated);
2374 return Ok(self.shared());
2375 }
2376
2377 let boxed_factory = registry.extract_factory(py, factory.clone_ref(py))?;
2378 let boxed_config = registry.extract_config(py, config.clone_ref(py))?;
2379 let factory_name = factory
2380 .getattr(py, "name")?
2381 .call0(py)?
2382 .extract::<String>(py)?;
2383 let client_name = name.unwrap_or(factory_name);
2384
2385 if operation
2386 .builder
2387 .as_ref()
2388 .is_some_and(|builder| builder.has_data_client(&client_name))
2389 {
2390 return Err(to_pyvalue_err(format!(
2391 "Client '{client_name}' is already registered"
2392 )));
2393 }
2394
2395 let builder = operation.take_builder()?;
2396
2397 let updated_builder = match routing {
2398 Some(routing) => builder.add_data_client_with_routing(
2399 Some(client_name),
2400 boxed_factory,
2401 boxed_config,
2402 routing,
2403 ),
2404 None => builder.add_data_client(Some(client_name), boxed_factory, boxed_config),
2405 }
2406 .map_err(|e| to_pyruntime_err(format!("Failed to add data client: {e}")))?;
2407
2408 operation.complete(updated_builder);
2409 Ok(self.shared())
2410 })
2411 }
2412
2413 #[pyo3(name = "add_exec_client", signature = (name, factory, config, routing=None))]
2414 fn py_add_exec_client(
2415 &self,
2416 name: Option<String>,
2417 factory: Py<PyAny>,
2418 config: Py<PyAny>,
2419 routing: Option<RoutingConfig>,
2420 ) -> PyResult<Self> {
2421 let mut operation = self.begin_operation()?;
2422 Python::attach(|py| -> PyResult<Self> {
2423 let (factory, config) = resolve_registered_client_pair(py, factory, config)?;
2424 let registry = get_global_pyo3_registry();
2425 let is_custom =
2426 python_client_factory_is_custom(py, factory.bind(py), "ExecutionClientFactory")?;
2427
2428 if is_custom {
2429 config.extract::<crate::config::ExecutionClientConfig>(py)?;
2430 let client_name =
2431 name.ok_or_else(|| to_pyvalue_err("Python clients require an explicit name"))?;
2432
2433 let routing = match routing {
2434 Some(routing) => routing,
2435 None => config
2436 .getattr(py, "routing")?
2437 .extract::<RoutingConfig>(py)?,
2438 };
2439
2440 if operation
2441 .builder
2442 .as_ref()
2443 .is_some_and(|builder| builder.has_exec_client(&client_name))
2444 {
2445 return Err(to_pyvalue_err(format!(
2446 "Execution client '{client_name}' is already registered"
2447 )));
2448 }
2449
2450 let builder = operation.take_builder()?;
2451
2452 let updated = builder
2453 .add_exec_client_with_routing(
2454 Some(client_name),
2455 Box::new(PythonExecutionFactory {
2456 factory,
2457 clients: self.clients.clone(),
2458 }),
2459 Box::new(PythonClientConfig(config)),
2460 routing,
2461 )
2462 .map_err(to_pyruntime_err)?;
2463
2464 operation.complete(updated);
2465 return Ok(self.shared());
2466 }
2467
2468 let boxed_factory = registry.extract_exec_factory(py, factory.clone_ref(py))?;
2469 let boxed_config = registry.extract_config(py, config.clone_ref(py))?;
2470 let factory_name = factory
2471 .getattr(py, "name")?
2472 .call0(py)?
2473 .extract::<String>(py)?;
2474 let client_name = name.unwrap_or(factory_name);
2475
2476 if operation
2477 .builder
2478 .as_ref()
2479 .is_some_and(|builder| builder.has_exec_client(&client_name))
2480 {
2481 return Err(to_pyvalue_err(format!(
2482 "Client '{client_name}' is already registered"
2483 )));
2484 }
2485
2486 let builder = operation.take_builder()?;
2487
2488 let updated_builder = match routing {
2489 Some(routing) => builder.add_exec_client_with_routing(
2490 Some(client_name),
2491 boxed_factory,
2492 boxed_config,
2493 routing,
2494 ),
2495 None => builder.add_exec_client(Some(client_name), boxed_factory, boxed_config),
2496 }
2497 .map_err(|e| to_pyruntime_err(format!("Failed to add exec client: {e}")))?;
2498
2499 operation.complete(updated_builder);
2500 Ok(self.shared())
2501 })
2502 }
2503
2504 #[pyo3(name = "add_simulated_exec_client")]
2505 #[expect(clippy::needless_pass_by_value)]
2506 fn py_add_simulated_exec_client(
2507 &self,
2508 name: Option<String>,
2509 factory: Py<PyAny>,
2510 config: Py<PyAny>,
2511 ) -> PyResult<Self> {
2512 let mut operation = self.begin_operation()?;
2513 Python::attach(|py| -> PyResult<Self> {
2514 let registry = get_global_pyo3_registry();
2515 let boxed_factory = registry.extract_sim_exec_factory(py, factory.clone_ref(py))?;
2516 let boxed_config = registry.extract_config(py, config.clone_ref(py))?;
2517 let factory_name = factory
2518 .getattr(py, "name")?
2519 .call0(py)?
2520 .extract::<String>(py)?;
2521 let client_name = name.unwrap_or(factory_name);
2522 let builder = operation.take_builder()?;
2523
2524 let updated_builder = builder
2525 .add_simulated_exec_client(Some(client_name), boxed_factory, boxed_config)
2526 .map_err(|e| {
2527 to_pyruntime_err(format!("Failed to add simulated exec client: {e}"))
2528 })?;
2529
2530 operation.complete(updated_builder);
2531 Ok(self.shared())
2532 })
2533 }
2534
2535 #[pyo3(name = "build")]
2536 fn py_build(&self) -> PyResult<PyLiveNode> {
2537 let mut operation = self.begin_operation()?;
2538 let mut builder = operation.take_builder()?;
2539
2540 let node = match builder.build_in_place() {
2541 Ok(node) => node,
2542 Err(e) => {
2543 Python::attach(|py| {
2544 if let Err(cleanup_error) = self.clients.finish(py) {
2545 log::error!(
2546 "Failed to clean up Python clients after build failure: {cleanup_error}"
2547 );
2548 }
2549 });
2550
2551 operation.complete(builder);
2552 return Err(to_pyruntime_err(e));
2553 }
2554 };
2555
2556 let mut node = PyLiveNode::new(node);
2557 node.clients = self.clients.take();
2558 Ok(node)
2559 }
2560
2561 fn __repr__(&self) -> String {
2562 format!("{self:?}")
2563 }
2564}
2565
2566const BUILDER_OPERATION_IN_PROGRESS: &str = "Builder operation already in progress";
2567const BUILDER_OPERATION_VALUE_TAKEN: &str = "Builder operation value already taken";
2568
2569enum PyLiveNodeBuilderState {
2570 Ready(Box<LiveNodeBuilder>),
2571 InProgress,
2572 Consumed,
2573}
2574
2575struct PyLiveNodeBuilderOperation<'a> {
2576 state: &'a Cell<PyLiveNodeBuilderState>,
2577 builder: Option<LiveNodeBuilder>,
2578}
2579
2580impl PyLiveNodeBuilder {
2581 fn begin_operation(&self) -> PyResult<PyLiveNodeBuilderOperation<'_>> {
2582 match self.state.replace(PyLiveNodeBuilderState::InProgress) {
2583 PyLiveNodeBuilderState::Ready(builder) => Ok(PyLiveNodeBuilderOperation {
2584 state: &self.state,
2585 builder: Some(*builder),
2586 }),
2587 PyLiveNodeBuilderState::InProgress => {
2588 self.state.set(PyLiveNodeBuilderState::InProgress);
2589 Err(to_pyruntime_err(BUILDER_OPERATION_IN_PROGRESS))
2590 }
2591 PyLiveNodeBuilderState::Consumed => {
2592 self.state.set(PyLiveNodeBuilderState::Consumed);
2593 Err(to_pyruntime_err("Builder already consumed"))
2594 }
2595 }
2596 }
2597
2598 fn update_builder<F>(&self, update: F) -> PyResult<Self>
2599 where
2600 F: FnOnce(LiveNodeBuilder) -> LiveNodeBuilder,
2601 {
2602 let mut operation = self.begin_operation()?;
2603 let builder = operation.take_builder()?;
2604 operation.complete(update(builder));
2605 Ok(self.shared())
2606 }
2607
2608 fn shared(&self) -> Self {
2609 Self {
2610 state: self.state.clone(),
2611 clients: self.clients.clone(),
2612 }
2613 }
2614}
2615
2616impl Debug for PyLiveNodeBuilder {
2617 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2618 let state = self.state.replace(PyLiveNodeBuilderState::InProgress);
2620
2621 let result = match &state {
2622 PyLiveNodeBuilderState::Ready(builder) => write!(
2623 f,
2624 "PyLiveNodeBuilder {{ inner: RefCell {{ value: Some({builder:?}) }} }}"
2625 ),
2626 PyLiveNodeBuilderState::InProgress => {
2627 f.write_str("PyLiveNodeBuilder { inner: <operation active> }")
2628 }
2629 PyLiveNodeBuilderState::Consumed => {
2630 f.write_str("PyLiveNodeBuilder { inner: RefCell { value: None } }")
2631 }
2632 };
2633
2634 self.state.set(state);
2635 result
2636 }
2637}
2638
2639impl PyLiveNodeBuilderOperation<'_> {
2640 fn take_builder(&mut self) -> PyResult<LiveNodeBuilder> {
2641 self.builder
2642 .take()
2643 .ok_or_else(|| to_pyruntime_err(BUILDER_OPERATION_VALUE_TAKEN))
2644 }
2645
2646 fn complete(&mut self, builder: LiveNodeBuilder) {
2647 self.builder = Some(builder);
2648 }
2649}
2650
2651impl Drop for PyLiveNodeBuilderOperation<'_> {
2652 fn drop(&mut self) {
2653 self.state.set(match self.builder.take() {
2654 Some(builder) => PyLiveNodeBuilderState::Ready(Box::new(builder)),
2655 None => PyLiveNodeBuilderState::Consumed,
2656 });
2657 }
2658}
2659
2660fn resolve_registered_client_pair(
2661 py: Python<'_>,
2662 factory: Py<PyAny>,
2663 config: Py<PyAny>,
2664) -> PyResult<(Py<PyAny>, Py<PyAny>)> {
2665 match py.import("nautilus_trader.live.config") {
2666 Ok(module) => module
2667 .getattr("resolve_client_registration")?
2668 .call1((factory, config))?
2669 .extract(),
2670 Err(e) if e.is_instance_of::<pyo3::exceptions::PyModuleNotFoundError>(py) => {
2671 Ok((factory, config))
2672 }
2673 Err(e) => Err(e),
2674 }
2675}
2676
2677fn python_client_factory_is_custom(
2678 py: Python<'_>,
2679 factory: &Bound<'_, PyAny>,
2680 base_name: &str,
2681) -> PyResult<bool> {
2682 let base = match py.import("nautilus_trader.live.clients") {
2683 Ok(module) => module.getattr(base_name)?,
2684 Err(e) if e.is_instance_of::<pyo3::exceptions::PyModuleNotFoundError>(py) => {
2685 return Ok(false);
2686 }
2687 Err(e) => return Err(e),
2688 };
2689
2690 if let Ok(factory_type) = factory.cast::<pyo3::types::PyType>() {
2691 factory_type.is_subclass(&base)
2692 } else {
2693 factory.is_instance(&base)
2694 }
2695}
2696
2697#[cfg(all(test, feature = "python"))]
2698#[allow(
2699 clippy::await_holding_refcell_ref,
2700 reason = "each test owns its node exclusively, so the wrapper borrow cannot contend"
2701)]
2702mod tests {
2703 use std::{
2704 any::Any,
2705 cell::RefCell,
2706 collections::HashMap,
2707 ffi::CString,
2708 fmt::Debug,
2709 rc::Rc,
2710 sync::{
2711 Arc,
2712 atomic::{AtomicBool, AtomicUsize, Ordering},
2713 mpsc,
2714 },
2715 thread,
2716 time::{Duration, Instant},
2717 };
2718
2719 use async_trait::async_trait;
2720 use indexmap::IndexMap;
2721 use nautilus_common::{
2722 actor::DataActor,
2723 cache::{
2724 CacheConfig, CacheView,
2725 database::{CacheDatabaseAdapter, CacheDatabaseFactory},
2726 },
2727 clients::DataClient,
2728 clock::Clock,
2729 enums::Environment,
2730 factories::{ClientConfig, DataClientFactory},
2731 live::{runner::get_data_event_sender, runtime::get_runtime},
2732 messages::{
2733 DataEvent, DataResponse,
2734 data::{BarsResponse, RequestBars},
2735 execution::{CancelAllOrders, SubmitOrder, TradingCommand},
2736 },
2737 msgbus::{
2738 BusMessage, MessageBusBacking, MessageBusBackingFactory, MessageBusConfig,
2739 MessagingSwitchboard, get_message_bus,
2740 },
2741 python::{
2742 actor::PyDataActor, cache::get_global_cache_database_factory_registry,
2743 msgbus::get_global_msgbus_factory_registry,
2744 },
2745 runner::{TradingCommandMessage, get_trading_cmd_sender},
2746 };
2747 use nautilus_core::{UUID4, UnixNanos};
2748 use nautilus_execution::engine::stubs::StubExecutionClient;
2749 use nautilus_model::{
2750 data::{Bar, BarType},
2751 enums::{OmsType, OrderStatus, OrderType},
2752 identifiers::{
2753 AccountId, ActorId, ClientId, InstrumentId, PositionId, StrategyId, TraderId, Venue,
2754 },
2755 instruments::{Instrument, InstrumentAny, stubs::crypto_perpetual_ethusdt},
2756 orders::{Order, OrderTestBuilder},
2757 types::{Price, Quantity},
2758 };
2759 use nautilus_testkit::cache::{TestCacheDatabase, TestCacheDatabaseControl};
2760 use nautilus_trading::{
2761 ImportableStrategyConfig, nautilus_strategy,
2762 python::strategy::PyStrategy,
2763 strategy::{StrategyConfig, StrategyCore},
2764 };
2765 use parking_lot::Mutex;
2766 use pyo3::{
2767 IntoPyObject, Py, PyRef, Python,
2768 ffi::c_str,
2769 types::{PyAnyMethods, PyDict, PyModule, PyModuleMethods},
2770 };
2771 use rstest::rstest;
2772
2773 use super::{
2774 BUILDER_OPERATION_IN_PROGRESS, LiveNode, PyLiveNode, PyLiveNodeBuilder,
2775 PyLiveNodeBuilderState, finish_owned_run, get_global_pyo3_registry,
2776 };
2777 use crate::node::config::RoutingConfig;
2778
2779 #[rstest]
2780 fn test_owned_run_cleanup_preserves_all_errors(
2781 #[values(false, true)] run_error: bool,
2782 #[values(false, true)] close_error: bool,
2783 #[values(false, true)] restore_error: bool,
2784 ) {
2785 Python::initialize();
2786 Python::attach(|py| {
2787 let module = PyModule::from_code(
2788 py,
2789 c_str!(
2790 r#"
2791calls = []
2792
2793class Loop:
2794 def close(self):
2795 calls.append("close")
2796 if close_error:
2797 raise RuntimeError("close failed")
2798
2799def signal(signum, original):
2800 calls.append(f"signal:{signum}")
2801 if restore_error and signum == 1:
2802 raise OSError("restore failed")
2803
2804loop = Loop()
2805primary = KeyboardInterrupt("run failed")
2806"#
2807 ),
2808 c_str!("owned_cleanup_test.py"),
2809 c_str!("owned_cleanup_test"),
2810 )
2811 .unwrap();
2812 module.setattr("close_error", close_error).unwrap();
2813 module.setattr("restore_error", restore_error).unwrap();
2814 let primary = module.getattr("primary").unwrap();
2815
2816 let result = if run_error {
2817 Err(pyo3::PyErr::from_value(primary.clone()))
2818 } else {
2819 Ok(())
2820 };
2821
2822 let handlers = [1_i32, 2]
2823 .into_iter()
2824 .map(|signum| {
2825 (
2826 signum.into_pyobject(py).unwrap().into_any(),
2827 py.None().into_bound(py),
2828 )
2829 })
2830 .collect();
2831
2832 let result =
2833 finish_owned_run(&module.getattr("loop").unwrap(), &module, handlers, result);
2834 let expected: Vec<&str> = [
2835 (run_error, "run failed"),
2836 (close_error, "close failed"),
2837 (restore_error, "restore failed"),
2838 ]
2839 .into_iter()
2840 .filter_map(|(failed, message)| failed.then_some(message))
2841 .collect();
2842
2843 let actual = match result {
2844 Ok(()) => Vec::new(),
2845 Err(e) => {
2846 let error = e.value(py);
2847
2848 let errors = if error.is_instance_of::<pyo3::exceptions::PyBaseExceptionGroup>()
2849 {
2850 error
2851 .getattr("exceptions")
2852 .unwrap()
2853 .extract::<Vec<pyo3::Bound<'_, pyo3::PyAny>>>()
2854 .unwrap()
2855 } else {
2856 vec![error.clone().into_any()]
2857 };
2858
2859 if run_error {
2860 assert!(errors[0].is(&primary));
2861 }
2862
2863 errors
2864 .into_iter()
2865 .map(|e| e.str().unwrap().to_string())
2866 .collect()
2867 }
2868 };
2869
2870 assert_eq!(actual, expected);
2871 assert_eq!(
2872 module
2873 .getattr("calls")
2874 .unwrap()
2875 .extract::<Vec<String>>()
2876 .unwrap(),
2877 ["close", "signal:1", "signal:2"],
2878 );
2879 });
2880 }
2881
2882 #[derive(Clone, Copy, Debug)]
2883 enum ShutdownRunPath {
2884 Native,
2885 PyO3,
2886 }
2887
2888 static TEST_MSGBUS_FACTORY_CALLS: AtomicUsize = AtomicUsize::new(0);
2889
2890 #[derive(Debug, Clone)]
2891 #[pyo3::pyclass(name = "TestMessageBusFactory", from_py_object)]
2892 struct TestMessageBusFactory;
2893
2894 impl MessageBusBackingFactory for TestMessageBusFactory {
2895 fn create(
2896 &self,
2897 trader_id: TraderId,
2898 _instance_id: UUID4,
2899 config: MessageBusConfig,
2900 ) -> anyhow::Result<Box<dyn MessageBusBacking>> {
2901 TEST_MSGBUS_FACTORY_CALLS.fetch_add(1, Ordering::SeqCst);
2902
2903 anyhow::ensure!(
2904 trader_id == TraderId::from("TESTER-001"),
2905 "unexpected trader ID: {trader_id}"
2906 );
2907 anyhow::ensure!(
2908 config.external_streams == Some(vec!["external-stream".to_string()]),
2909 "unexpected external streams: {:?}",
2910 config.external_streams
2911 );
2912
2913 let (_tx, rx) = tokio::sync::mpsc::channel(1);
2914 Ok(Box::new(TestMessageBusBacking {
2915 rx: Some(rx),
2916 closed: false,
2917 }))
2918 }
2919 }
2920
2921 #[derive(Debug)]
2922 struct TestMessageBusBacking {
2923 rx: Option<tokio::sync::mpsc::Receiver<BusMessage>>,
2924 closed: bool,
2925 }
2926
2927 impl MessageBusBacking for TestMessageBusBacking {
2928 fn is_closed(&self) -> bool {
2929 self.closed
2930 }
2931
2932 fn publish(&self, _message: BusMessage) {}
2933
2934 fn take_receiver(&mut self) -> anyhow::Result<tokio::sync::mpsc::Receiver<BusMessage>> {
2935 self.rx
2936 .take()
2937 .ok_or_else(|| anyhow::anyhow!("Stream receiver already taken"))
2938 }
2939
2940 fn close(&mut self) {
2941 self.closed = true;
2942 }
2943 }
2944
2945 #[expect(clippy::needless_pass_by_value)]
2946 fn extract_test_msgbus_factory(
2947 py: Python<'_>,
2948 factory: Py<pyo3::PyAny>,
2949 ) -> pyo3::PyResult<Box<dyn MessageBusBackingFactory>> {
2950 Ok(Box::new(factory.extract::<TestMessageBusFactory>(py)?))
2951 }
2952
2953 #[rstest]
2954 fn test_python_builder_installs_external_msgbus_factory() {
2955 TEST_MSGBUS_FACTORY_CALLS.store(0, Ordering::SeqCst);
2956 get_global_msgbus_factory_registry()
2957 .register(
2958 "TestMessageBusFactory".to_string(),
2959 extract_test_msgbus_factory,
2960 )
2961 .unwrap();
2962 Python::initialize();
2963
2964 Python::attach(|py| {
2965 let factory = Py::new(py, TestMessageBusFactory).unwrap().into_any();
2966
2967 let builder = PyLiveNode::py_builder(
2968 "TEST".to_string(),
2969 TraderId::from("TESTER-001"),
2970 Environment::Sandbox,
2971 )
2972 .unwrap()
2973 .py_with_msgbus_config(MessageBusConfig {
2974 external_streams: Some(vec!["external-stream".to_string()]),
2975 ..Default::default()
2976 })
2977 .unwrap()
2978 .py_with_external_msgbus_factory(factory)
2979 .unwrap();
2980
2981 let node = builder.py_build().unwrap();
2982 let consumed_error = builder
2983 .py_with_msgbus_config(MessageBusConfig::default())
2984 .unwrap_err();
2985
2986 assert!(!node.node_mut().unwrap().is_running());
2987 assert_eq!(TEST_MSGBUS_FACTORY_CALLS.load(Ordering::SeqCst), 1);
2988 assert_eq!(
2989 consumed_error.to_string(),
2990 "RuntimeError: Builder already consumed"
2991 );
2992 assert_eq!(
2993 builder.__repr__(),
2994 "PyLiveNodeBuilder { inner: RefCell { value: None } }"
2995 );
2996 get_message_bus().borrow_mut().dispose();
2997 });
2998 }
2999
3000 static TEST_CACHE_DATABASE_FACTORY_CALLS: AtomicUsize = AtomicUsize::new(0);
3001
3002 #[derive(Debug, Clone)]
3003 #[pyo3::pyclass(name = "TestCacheDatabaseFactory", from_py_object)]
3004 struct TestCacheDatabaseFactory {
3005 database: Arc<parking_lot::Mutex<Option<TestCacheDatabase>>>,
3006 instance_id: Arc<parking_lot::Mutex<Option<UUID4>>>,
3007 }
3008
3009 impl TestCacheDatabaseFactory {
3010 fn new(database: Option<TestCacheDatabase>) -> Self {
3011 Self {
3012 database: Arc::new(parking_lot::Mutex::new(database)),
3013 instance_id: Arc::new(parking_lot::Mutex::new(None)),
3014 }
3015 }
3016
3017 fn received_instance_id(&self) -> Option<UUID4> {
3018 *self.instance_id.lock()
3019 }
3020 }
3021
3022 #[async_trait]
3023 impl CacheDatabaseFactory for TestCacheDatabaseFactory {
3024 async fn create(
3025 &self,
3026 trader_id: TraderId,
3027 instance_id: UUID4,
3028 config: CacheConfig,
3029 ) -> anyhow::Result<Box<dyn CacheDatabaseAdapter>> {
3030 TEST_CACHE_DATABASE_FACTORY_CALLS.fetch_add(1, Ordering::SeqCst);
3031 *self.instance_id.lock() = Some(instance_id);
3032
3033 anyhow::ensure!(
3034 trader_id == TraderId::from("TESTER-001"),
3035 "unexpected trader ID: {trader_id}"
3036 );
3037 anyhow::ensure!(
3038 config.buffer_interval_ms == Some(25),
3039 "unexpected buffer interval: {:?}",
3040 config.buffer_interval_ms
3041 );
3042
3043 let database = self
3044 .database
3045 .lock()
3046 .take()
3047 .ok_or_else(|| anyhow::anyhow!("Test cache database unavailable"))?;
3048 Ok(Box::new(database))
3049 }
3050 }
3051
3052 #[expect(clippy::needless_pass_by_value)]
3053 fn extract_test_cache_database_factory(
3054 py: Python<'_>,
3055 factory: Py<pyo3::PyAny>,
3056 ) -> pyo3::PyResult<Box<dyn CacheDatabaseFactory>> {
3057 Ok(Box::new(factory.extract::<TestCacheDatabaseFactory>(py)?))
3058 }
3059
3060 fn state_factory_builder(factory: &TestCacheDatabaseFactory) -> PyLiveNodeBuilder {
3061 Python::attach(|py| {
3062 PyLiveNode::py_builder(
3063 "TEST".to_string(),
3064 TraderId::from("TESTER-001"),
3065 Environment::Sandbox,
3066 )
3067 .unwrap()
3068 .py_with_cache_config(CacheConfig {
3069 buffer_interval_ms: Some(25),
3070 ..Default::default()
3071 })
3072 .unwrap()
3073 .py_with_cache_database_factory(Py::new(py, factory.clone()).unwrap().into_any())
3074 .unwrap()
3075 .py_with_reconciliation(false)
3076 .unwrap()
3077 .py_with_timeout_connection(0)
3078 .unwrap()
3079 .py_with_timeout_reconciliation(0)
3080 .unwrap()
3081 .py_with_timeout_portfolio(0)
3082 .unwrap()
3083 .py_with_timeout_disconnection_secs(0)
3084 .unwrap()
3085 .py_with_delay_post_stop_secs(0)
3086 .unwrap()
3087 .py_with_delay_shutdown_secs(0)
3088 .unwrap()
3089 })
3090 }
3091
3092 #[tokio::test]
3093 async fn test_python_builder_installs_cache_database_factory_on_start() {
3094 TEST_CACHE_DATABASE_FACTORY_CALLS.store(0, Ordering::SeqCst);
3095 get_global_cache_database_factory_registry()
3096 .register(
3097 "TestCacheDatabaseFactory".to_string(),
3098 extract_test_cache_database_factory,
3099 )
3100 .unwrap();
3101 Python::initialize();
3102
3103 let (database, control) = TestCacheDatabaseControl::create();
3104 let actor_id = ActorId::from("PY-CACHE-FACTORY-ACTOR");
3105 let actor_state = IndexMap::from([("loaded".to_string(), b"value".to_vec())]);
3106 control.set_actor_state(actor_id, &actor_state);
3107 let factory = TestCacheDatabaseFactory::new(Some(database));
3108
3109 let node = state_factory_builder(&factory).py_build().unwrap();
3110
3111 assert_eq!(TEST_CACHE_DATABASE_FACTORY_CALLS.load(Ordering::SeqCst), 0);
3112 assert!(
3113 !node
3114 .node_mut()
3115 .unwrap()
3116 .kernel()
3117 .cache()
3118 .borrow()
3119 .has_backing()
3120 );
3121
3122 node.node_mut().unwrap().start().await.unwrap();
3123
3124 assert_eq!(TEST_CACHE_DATABASE_FACTORY_CALLS.load(Ordering::SeqCst), 1);
3125 assert_eq!(
3126 factory.received_instance_id(),
3127 Some(node.node_mut().unwrap().instance_id())
3128 );
3129 assert!(
3130 node.node_mut()
3131 .unwrap()
3132 .kernel()
3133 .cache()
3134 .borrow()
3135 .has_backing()
3136 );
3137 assert_eq!(
3138 node.node_mut()
3139 .unwrap()
3140 .kernel()
3141 .cache()
3142 .borrow()
3143 .load_actor_state(&actor_id)
3144 .unwrap(),
3145 Some(actor_state)
3146 );
3147
3148 node.node_mut().unwrap().stop().await.unwrap();
3149 node.node_mut().unwrap().dispose();
3150 get_message_bus().borrow_mut().dispose();
3151 }
3152
3153 #[tokio::test]
3154 async fn test_python_builder_propagates_cache_database_factory_error_on_start() {
3155 get_global_cache_database_factory_registry()
3156 .register(
3157 "TestCacheDatabaseFactory".to_string(),
3158 extract_test_cache_database_factory,
3159 )
3160 .unwrap();
3161 Python::initialize();
3162
3163 let factory = TestCacheDatabaseFactory::new(None);
3164 let node = state_factory_builder(&factory).py_build().unwrap();
3165
3166 let error = node.node_mut().unwrap().start().await.unwrap_err();
3167
3168 assert_eq!(
3169 format!("{error:#}"),
3170 "failed to create cache database backing: Test cache database unavailable"
3171 );
3172 assert!(
3173 !node
3174 .node_mut()
3175 .unwrap()
3176 .kernel()
3177 .cache()
3178 .borrow()
3179 .has_backing()
3180 );
3181
3182 let retry_error = node.node_mut().unwrap().start().await.unwrap_err();
3183
3184 assert_eq!(
3185 format!("{retry_error:#}"),
3186 "failed to create cache database backing: Test cache database unavailable"
3187 );
3188 assert!(
3189 !node
3190 .node_mut()
3191 .unwrap()
3192 .kernel()
3193 .cache()
3194 .borrow()
3195 .has_backing()
3196 );
3197
3198 node.node_mut().unwrap().dispose();
3199 get_message_bus().borrow_mut().dispose();
3200 }
3201
3202 #[rstest]
3203 fn test_python_builder_rejects_unregistered_cache_database_factory() {
3204 Python::initialize();
3205
3206 Python::attach(|py| {
3207 let factory = PyDict::new(py).unbind().into_any();
3208 let builder = PyLiveNode::py_builder(
3209 "TEST".to_string(),
3210 TraderId::from("TESTER-001"),
3211 Environment::Sandbox,
3212 )
3213 .unwrap();
3214
3215 let error = builder.py_with_cache_database_factory(factory).unwrap_err();
3216
3217 assert_eq!(
3218 error.to_string(),
3219 "NotImplementedError: No cache database factory extractor registered for 'dict'"
3220 );
3221 builder
3222 .py_with_cache_config(CacheConfig::default())
3223 .unwrap();
3224 });
3225 }
3226
3227 #[rstest]
3228 fn test_python_builder_restores_state_after_factory_type_reentry() {
3229 Python::initialize();
3230
3231 Python::attach(|py| {
3232 let builder = Py::new(
3233 py,
3234 PyLiveNode::py_builder(
3235 "TEST".to_string(),
3236 TraderId::from("TESTER-001"),
3237 Environment::Sandbox,
3238 )
3239 .unwrap(),
3240 )
3241 .unwrap();
3242 let locals = PyDict::new(py);
3243 locals.set_item("builder", &builder).unwrap();
3244 py.run(
3245 pyo3::ffi::c_str!(
3246 "class ReentrantFactory:\n def __getattribute__(self, name):\n if name == '__class__':\n builder.with_load_state(True)\n return object.__getattribute__(self, name)\n\nfactory = ReentrantFactory()"
3247 ),
3248 Some(&locals),
3249 None,
3250 )
3251 .unwrap();
3252 let factory = locals.get_item("factory").unwrap();
3253
3254 let error = builder
3255 .call_method1(py, "with_cache_database_factory", (factory,))
3256 .unwrap_err();
3257
3258 assert_eq!(
3259 error.to_string(),
3260 format!("RuntimeError: {BUILDER_OPERATION_IN_PROGRESS}")
3261 );
3262 let builder_ref = builder.borrow(py);
3263 let state = builder_ref
3264 .state
3265 .replace(PyLiveNodeBuilderState::InProgress);
3266 let is_ready = matches!(&state, PyLiveNodeBuilderState::Ready(_));
3267 builder_ref.state.set(state);
3268 assert!(is_ready);
3269 locals.call_method0("clear").unwrap();
3270 });
3271 }
3272
3273 #[rstest]
3274 fn test_python_builder_rejects_unregistered_external_msgbus_factory() {
3275 Python::initialize();
3276
3277 Python::attach(|py| {
3278 let factory = PyDict::new(py).unbind().into_any();
3279 let builder = PyLiveNode::py_builder(
3280 "TEST".to_string(),
3281 TraderId::from("TESTER-001"),
3282 Environment::Sandbox,
3283 )
3284 .unwrap();
3285
3286 let error = builder
3287 .py_with_external_msgbus_factory(factory)
3288 .unwrap_err();
3289
3290 assert_eq!(
3291 error.to_string(),
3292 "NotImplementedError: No message bus factory extractor registered for 'dict'"
3293 );
3294 builder
3295 .py_with_msgbus_config(MessageBusConfig::default())
3296 .unwrap();
3297 });
3298 }
3299
3300 #[derive(Debug)]
3301 struct ShutdownCancelStrategy {
3302 core: StrategyCore,
3303 instrument_id: InstrumentId,
3304 }
3305
3306 impl ShutdownCancelStrategy {
3307 fn new(instrument_id: InstrumentId) -> Self {
3308 Self {
3309 core: StrategyCore::new(StrategyConfig {
3310 strategy_id: Some(StrategyId::from("SHUTDOWN-CANCEL-001")),
3311 ..Default::default()
3312 }),
3313 instrument_id,
3314 }
3315 }
3316 }
3317
3318 nautilus_strategy!(ShutdownCancelStrategy);
3319
3320 impl DataActor for ShutdownCancelStrategy {
3321 fn on_stop(&mut self) -> anyhow::Result<()> {
3322 get_trading_cmd_sender().execute(TradingCommandMessage::new(
3323 MessagingSwitchboard::exec_engine_execute(),
3324 TradingCommand::CancelAllOrders(CancelAllOrders::new(
3325 TraderId::from("TESTER-001"),
3326 None,
3327 StrategyId::from("SHUTDOWN-CANCEL-001"),
3328 self.instrument_id,
3329 None,
3330 UUID4::new(),
3331 UnixNanos::default(),
3332 None,
3333 None,
3334 )),
3335 ));
3336 Ok(())
3337 }
3338 }
3339 #[derive(Debug, Default)]
3340 struct TestDataClientConfig;
3341
3342 impl ClientConfig for TestDataClientConfig {
3343 fn as_any(&self) -> &dyn Any {
3344 self
3345 }
3346 }
3347
3348 #[derive(Debug)]
3349 #[expect(
3350 clippy::struct_field_names,
3351 reason = "test counters intentionally share the count postfix"
3352 )]
3353 struct TestHistoricalBarsDataClientFactory {
3354 request_count: Arc<AtomicUsize>,
3355 response_sent_count: Arc<AtomicUsize>,
3356 handler_visible_count: Arc<AtomicUsize>,
3357 }
3358
3359 impl TestHistoricalBarsDataClientFactory {
3360 fn new(
3361 request_count: Arc<AtomicUsize>,
3362 response_sent_count: Arc<AtomicUsize>,
3363 handler_visible_count: Arc<AtomicUsize>,
3364 ) -> Self {
3365 Self {
3366 request_count,
3367 response_sent_count,
3368 handler_visible_count,
3369 }
3370 }
3371 }
3372
3373 impl DataClientFactory for TestHistoricalBarsDataClientFactory {
3374 fn create(
3375 &self,
3376 name: &str,
3377 _config: &dyn ClientConfig,
3378 _cache: CacheView,
3379 _clock: Rc<RefCell<dyn Clock>>,
3380 ) -> anyhow::Result<Box<dyn DataClient>> {
3381 Ok(Box::new(TestHistoricalBarsDataClient::new(
3382 ClientId::from(name),
3383 Venue::from("SIM"),
3384 self.request_count.clone(),
3385 self.response_sent_count.clone(),
3386 self.handler_visible_count.clone(),
3387 )))
3388 }
3389
3390 fn name(&self) -> &'static str {
3391 "TEST_DATA"
3392 }
3393
3394 fn config_type(&self) -> &'static str {
3395 "TestDataClientConfig"
3396 }
3397 }
3398
3399 #[derive(Debug)]
3400 struct TestDisconnectFailureDataClientFactory {
3401 dispose_count: Arc<AtomicUsize>,
3402 }
3403
3404 impl TestDisconnectFailureDataClientFactory {
3405 fn new(dispose_count: Arc<AtomicUsize>) -> Self {
3406 Self { dispose_count }
3407 }
3408 }
3409
3410 impl DataClientFactory for TestDisconnectFailureDataClientFactory {
3411 fn create(
3412 &self,
3413 name: &str,
3414 _config: &dyn ClientConfig,
3415 _cache: CacheView,
3416 _clock: Rc<RefCell<dyn Clock>>,
3417 ) -> anyhow::Result<Box<dyn DataClient>> {
3418 Ok(Box::new(TestDisconnectFailureDataClient::new(
3419 ClientId::from(name),
3420 Venue::from("SIM"),
3421 self.dispose_count.clone(),
3422 )))
3423 }
3424
3425 fn name(&self) -> &'static str {
3426 "TEST_DISCONNECT_FAILURE"
3427 }
3428
3429 fn config_type(&self) -> &'static str {
3430 "TestDataClientConfig"
3431 }
3432 }
3433
3434 #[derive(Debug)]
3435 struct TestDisconnectFailureDataClient {
3436 client_id: ClientId,
3437 venue: Venue,
3438 connected: Arc<AtomicBool>,
3439 dispose_count: Arc<AtomicUsize>,
3440 }
3441
3442 impl TestDisconnectFailureDataClient {
3443 fn new(client_id: ClientId, venue: Venue, dispose_count: Arc<AtomicUsize>) -> Self {
3444 Self {
3445 client_id,
3446 venue,
3447 connected: Arc::new(AtomicBool::new(false)),
3448 dispose_count,
3449 }
3450 }
3451 }
3452
3453 #[async_trait(?Send)]
3454 impl DataClient for TestDisconnectFailureDataClient {
3455 fn client_id(&self) -> ClientId {
3456 self.client_id
3457 }
3458
3459 fn venue(&self) -> Option<Venue> {
3460 Some(self.venue)
3461 }
3462
3463 fn start(&mut self) -> anyhow::Result<()> {
3464 Ok(())
3465 }
3466
3467 fn stop(&mut self) -> anyhow::Result<()> {
3468 Ok(())
3469 }
3470
3471 fn reset(&mut self) -> anyhow::Result<()> {
3472 Ok(())
3473 }
3474
3475 fn dispose(&mut self) -> anyhow::Result<()> {
3476 self.dispose_count.fetch_add(1, Ordering::Relaxed);
3477 Ok(())
3478 }
3479
3480 fn is_connected(&self) -> bool {
3481 self.connected.load(Ordering::Relaxed)
3482 }
3483
3484 fn is_disconnected(&self) -> bool {
3485 !self.is_connected()
3486 }
3487
3488 async fn connect(&mut self) -> anyhow::Result<()> {
3489 self.connected.store(true, Ordering::Relaxed);
3490 Ok(())
3491 }
3492
3493 async fn disconnect(&mut self) -> anyhow::Result<()> {
3494 self.connected.store(false, Ordering::Relaxed);
3495 anyhow::bail!("test disconnect failed")
3496 }
3497 }
3498
3499 struct VenueLessDataClient {
3500 client_id: ClientId,
3501 }
3502
3503 impl VenueLessDataClient {
3504 fn new(client_id: ClientId) -> Self {
3505 Self { client_id }
3506 }
3507 }
3508
3509 #[async_trait(?Send)]
3510 impl DataClient for VenueLessDataClient {
3511 fn client_id(&self) -> ClientId {
3512 self.client_id
3513 }
3514
3515 fn venue(&self) -> Option<Venue> {
3516 None
3517 }
3518
3519 fn start(&mut self) -> anyhow::Result<()> {
3520 Ok(())
3521 }
3522
3523 fn stop(&mut self) -> anyhow::Result<()> {
3524 Ok(())
3525 }
3526
3527 fn reset(&mut self) -> anyhow::Result<()> {
3528 Ok(())
3529 }
3530
3531 fn dispose(&mut self) -> anyhow::Result<()> {
3532 Ok(())
3533 }
3534
3535 fn is_connected(&self) -> bool {
3536 true
3537 }
3538
3539 fn is_disconnected(&self) -> bool {
3540 false
3541 }
3542
3543 async fn connect(&mut self) -> anyhow::Result<()> {
3544 Ok(())
3545 }
3546
3547 async fn disconnect(&mut self) -> anyhow::Result<()> {
3548 Ok(())
3549 }
3550 }
3551
3552 #[derive(Debug)]
3553 struct VenueLessDataClientFactory;
3554
3555 impl DataClientFactory for VenueLessDataClientFactory {
3556 fn create(
3557 &self,
3558 name: &str,
3559 _config: &dyn ClientConfig,
3560 _cache: CacheView,
3561 _clock: Rc<RefCell<dyn Clock>>,
3562 ) -> anyhow::Result<Box<dyn DataClient>> {
3563 Ok(Box::new(VenueLessDataClient::new(ClientId::from(name))))
3564 }
3565
3566 fn name(&self) -> &'static str {
3567 "VENUE_LESS"
3568 }
3569
3570 fn config_type(&self) -> &'static str {
3571 "TestDataClientConfig"
3572 }
3573 }
3574
3575 #[expect(
3576 clippy::unnecessary_wraps,
3577 reason = "signature must match the factory extractor function pointer"
3578 )]
3579 fn extract_reentrant_data_client_factory(
3580 _py: Python<'_>,
3581 _factory: Py<pyo3::PyAny>,
3582 ) -> pyo3::PyResult<Box<dyn DataClientFactory>> {
3583 Ok(Box::new(VenueLessDataClientFactory))
3584 }
3585
3586 #[expect(
3587 clippy::unnecessary_wraps,
3588 reason = "signature must match the config extractor function pointer"
3589 )]
3590 fn extract_reentrant_data_client_config(
3591 _py: Python<'_>,
3592 _config: Py<pyo3::PyAny>,
3593 ) -> pyo3::PyResult<Box<dyn ClientConfig>> {
3594 Ok(Box::new(TestDataClientConfig))
3595 }
3596
3597 #[rstest]
3598 fn test_python_builder_blocks_factory_name_reentry() {
3599 get_global_pyo3_registry()
3600 .register_factory_extractor(
3601 "REENTRANT_DATA".to_string(),
3602 extract_reentrant_data_client_factory,
3603 )
3604 .unwrap();
3605 get_global_pyo3_registry()
3606 .register_config_extractor(
3607 "ReentrantDataClientConfig".to_string(),
3608 extract_reentrant_data_client_config,
3609 )
3610 .unwrap();
3611 Python::initialize();
3612
3613 Python::attach(|py| {
3614 let builder = Py::new(
3615 py,
3616 PyLiveNode::py_builder(
3617 "TEST".to_string(),
3618 TraderId::from("TESTER-001"),
3619 Environment::Sandbox,
3620 )
3621 .unwrap(),
3622 )
3623 .unwrap();
3624 let locals = PyDict::new(py);
3625 locals.set_item("builder", &builder).unwrap();
3626 py.run(
3627 pyo3::ffi::c_str!(
3628 r#"
3629import sys
3630from types import ModuleType
3631from unittest.mock import patch
3632
3633class ReentrantDataClientFactory:
3634 def __init__(self):
3635 self.reprs = []
3636 self.results = []
3637
3638 def name(self):
3639 self.reprs.append(repr(builder))
3640 try:
3641 builder.with_save_state(True)
3642 except RuntimeError as e:
3643 self.results.append((type(e).__name__, str(e)))
3644 return "REENTRANT_DATA"
3645
3646class ReentrantDataClientConfig:
3647 pass
3648
3649factory = ReentrantDataClientFactory()
3650config = ReentrantDataClientConfig()
3651package_module = ModuleType("nautilus_trader")
3652config_module = ModuleType("nautilus_trader.live.config")
3653config_module.resolve_client_registration = lambda factory, config: (factory, config)
3654clients_module = ModuleType("nautilus_trader.live.clients")
3655clients_module.DataClientFactory = type("DataClientFactory", (), {})
3656
3657def add_data_client():
3658 # Exercise re-entry without importing an independently built extension
3659 with patch.dict(sys.modules, {
3660 package_module.__name__: package_module,
3661 config_module.__name__: config_module,
3662 clients_module.__name__: clients_module,
3663 }):
3664 return builder.add_data_client(None, factory, config)
3665"#
3666 ),
3667 Some(&locals),
3668 None,
3669 )
3670 .unwrap();
3671 let factory = locals.get_item("factory").unwrap();
3672 let add_data_client = locals.get_item("add_data_client").unwrap();
3673
3674 add_data_client.call0().unwrap();
3675
3676 let results = factory
3677 .getattr("results")
3678 .unwrap()
3679 .extract::<Vec<(String, String)>>()
3680 .unwrap();
3681 assert_eq!(
3682 results,
3683 vec![
3684 (
3685 "RuntimeError".to_string(),
3686 BUILDER_OPERATION_IN_PROGRESS.to_string(),
3687 ),
3688 (
3689 "RuntimeError".to_string(),
3690 BUILDER_OPERATION_IN_PROGRESS.to_string(),
3691 ),
3692 ]
3693 );
3694 assert_eq!(
3695 factory
3696 .getattr("reprs")
3697 .unwrap()
3698 .extract::<Vec<String>>()
3699 .unwrap(),
3700 vec!["PyLiveNodeBuilder { inner: <operation active> }"; 2]
3701 );
3702 let builder_ref = builder.borrow(py);
3703 let state = builder_ref
3704 .state
3705 .replace(PyLiveNodeBuilderState::InProgress);
3706 let is_ready = matches!(&state, PyLiveNodeBuilderState::Ready(_));
3707 builder_ref.state.set(state);
3708 assert!(is_ready);
3709 drop(builder_ref);
3710
3711 let duplicate_error = add_data_client.call0().unwrap_err();
3712
3713 assert_eq!(
3714 duplicate_error.to_string(),
3715 "ValueError: Client 'REENTRANT_DATA' is already registered"
3716 );
3717 let builder_ref = builder.borrow(py);
3718 let state = builder_ref
3719 .state
3720 .replace(PyLiveNodeBuilderState::InProgress);
3721 let retains_registration = matches!(
3722 &state,
3723 PyLiveNodeBuilderState::Ready(inner) if inner.has_data_client("REENTRANT_DATA")
3724 );
3725 builder_ref.state.set(state);
3726 assert!(retains_registration);
3727 drop(builder_ref);
3728 builder
3729 .call_method1(py, "with_save_state", (true,))
3730 .unwrap();
3731 locals.call_method0("clear").unwrap();
3732 });
3733 }
3734
3735 #[derive(Debug)]
3736 struct TestHistoricalBarsDataClient {
3737 client_id: ClientId,
3738 venue: Venue,
3739 connected: Arc<AtomicBool>,
3740 request_count: Arc<AtomicUsize>,
3741 response_sent_count: Arc<AtomicUsize>,
3742 handler_visible_count: Arc<AtomicUsize>,
3743 }
3744
3745 impl TestHistoricalBarsDataClient {
3746 fn new(
3747 client_id: ClientId,
3748 venue: Venue,
3749 request_count: Arc<AtomicUsize>,
3750 response_sent_count: Arc<AtomicUsize>,
3751 handler_visible_count: Arc<AtomicUsize>,
3752 ) -> Self {
3753 Self {
3754 client_id,
3755 venue,
3756 connected: Arc::new(AtomicBool::new(false)),
3757 request_count,
3758 response_sent_count,
3759 handler_visible_count,
3760 }
3761 }
3762
3763 fn make_bar(bar_type: BarType) -> Bar {
3764 Bar::new(
3765 bar_type,
3766 Price::from("1.0000"),
3767 Price::from("1.1000"),
3768 Price::from("0.9000"),
3769 Price::from("1.0500"),
3770 Quantity::from("1000"),
3771 UnixNanos::from(1_700_000_000_000_000_000u64),
3772 UnixNanos::from(1_700_000_000_000_000_001u64),
3773 )
3774 }
3775 }
3776
3777 #[async_trait(?Send)]
3778 impl DataClient for TestHistoricalBarsDataClient {
3779 fn client_id(&self) -> ClientId {
3780 self.client_id
3781 }
3782
3783 fn venue(&self) -> Option<Venue> {
3784 Some(self.venue)
3785 }
3786
3787 fn start(&mut self) -> anyhow::Result<()> {
3788 Ok(())
3789 }
3790
3791 fn stop(&mut self) -> anyhow::Result<()> {
3792 Ok(())
3793 }
3794
3795 fn reset(&mut self) -> anyhow::Result<()> {
3796 Ok(())
3797 }
3798
3799 fn dispose(&mut self) -> anyhow::Result<()> {
3800 Ok(())
3801 }
3802
3803 fn is_connected(&self) -> bool {
3804 self.connected.load(Ordering::Relaxed)
3805 }
3806
3807 fn is_disconnected(&self) -> bool {
3808 !self.is_connected()
3809 }
3810
3811 async fn connect(&mut self) -> anyhow::Result<()> {
3812 self.connected.store(true, Ordering::Relaxed);
3813 Ok(())
3814 }
3815
3816 async fn disconnect(&mut self) -> anyhow::Result<()> {
3817 self.connected.store(false, Ordering::Relaxed);
3818 Ok(())
3819 }
3820
3821 fn request_bars(&self, request: RequestBars) -> anyhow::Result<()> {
3822 self.request_count.fetch_add(1, Ordering::Relaxed);
3823
3824 if get_message_bus()
3825 .borrow()
3826 .get_response_handler(&request.request_id)
3827 .is_some()
3828 {
3829 self.handler_visible_count.fetch_add(1, Ordering::Relaxed);
3830 }
3831
3832 let sender = get_data_event_sender();
3833 let client_id = self.client_id;
3834 let response_sent_count = self.response_sent_count.clone();
3835
3836 let response = BarsResponse::new(
3837 request.request_id,
3838 client_id,
3839 request.bar_type,
3840 vec![Self::make_bar(request.bar_type)],
3841 None,
3842 None,
3843 UnixNanos::from(1_700_000_000_000_000_002u64),
3844 None,
3845 );
3846
3847 tokio::spawn(async move {
3848 tokio::time::sleep(Duration::from_millis(10)).await;
3849 response_sent_count.fetch_add(1, Ordering::Relaxed);
3850 sender
3851 .send(DataEvent::Response(DataResponse::Bars(response)))
3852 .expect("test bars response should send");
3853 });
3854
3855 Ok(())
3856 }
3857 }
3858
3859 fn install_tracking_strategy_module(py: Python<'_>, module_name: &str) {
3860 let module = PyModule::new(py, module_name).expect("test module should create");
3861 module
3862 .setattr("Strategy", py.get_type::<PyStrategy>())
3863 .expect("Strategy type should bind");
3864 module
3865 .setattr("BarType", py.get_type::<BarType>())
3866 .expect("BarType type should bind");
3867 module
3868 .setattr("RESULTS", PyDict::new(py))
3869 .expect("RESULTS should bind");
3870
3871 let code = CString::new(
3872 r#"
3873RESULTS["on_start"] = 0
3874RESULTS["on_historical_bars"] = 0
3875RESULTS["historical_bar_count"] = 0
3876RESULTS["last_request_id"] = ""
3877
3878class HistoricalBarsStrategy(Strategy):
3879 def __init__(self):
3880 super().__init__()
3881 self.bar_type = BarType.from_str("AUDUSD.SIM-1-MINUTE-LAST-EXTERNAL")
3882
3883 def on_start(self):
3884 RESULTS["on_start"] += 1
3885 RESULTS["last_request_id"] = self.request_bars(self.bar_type)
3886
3887 def on_stop(self):
3888 pass
3889
3890 def on_historical_bars(self, bars):
3891 RESULTS["on_historical_bars"] += 1
3892 RESULTS["historical_bar_count"] += len(bars)
3893"#,
3894 )
3895 .expect("python test code should be valid CString");
3896
3897 py.run(code.as_c_str(), Some(&module.dict()), None)
3898 .expect("test strategy code should execute");
3899
3900 let sys_modules = py
3901 .import("sys")
3902 .expect("sys should import")
3903 .getattr("modules")
3904 .expect("sys.modules should exist");
3905 sys_modules
3906 .set_item(module_name, module)
3907 .expect("test strategy module should register");
3908 }
3909
3910 fn get_results(py: Python<'_>, module_name: &str) -> (usize, usize, usize) {
3911 let module = py
3912 .import(module_name)
3913 .expect("test strategy module should import");
3914 let results_obj = module.getattr("RESULTS").expect("RESULTS should exist");
3915 let results = results_obj
3916 .cast::<PyDict>()
3917 .expect("RESULTS should be a dict");
3918
3919 let on_start = results
3920 .get_item("on_start")
3921 .expect("on_start key should exist")
3922 .extract::<usize>()
3923 .expect("on_start should extract");
3924 let on_historical_bars = results
3925 .get_item("on_historical_bars")
3926 .expect("on_historical_bars key should exist")
3927 .extract::<usize>()
3928 .expect("on_historical_bars should extract");
3929 let historical_bar_count = results
3930 .get_item("historical_bar_count")
3931 .expect("historical_bar_count key should exist")
3932 .extract::<usize>()
3933 .expect("historical_bar_count should extract");
3934
3935 (on_start, on_historical_bars, historical_bar_count)
3936 }
3937
3938 fn install_timer_strategy_module(py: Python<'_>, module_name: &str) {
3939 let module = PyModule::new(py, module_name).expect("test module should create");
3940 module
3941 .setattr("Strategy", py.get_type::<PyStrategy>())
3942 .expect("Strategy type should bind");
3943 module
3944 .setattr("RESULTS", PyDict::new(py))
3945 .expect("RESULTS should bind");
3946
3947 let code = CString::new(
3948 r#"
3949RESULTS["on_start"] = 0
3950RESULTS["callback_timer_count"] = 0
3951RESULTS["default_timer_count"] = 0
3952RESULTS["callback_event_type"] = ""
3953RESULTS["default_event_type"] = ""
3954RESULTS["callback_event_name"] = ""
3955RESULTS["default_event_name"] = ""
3956
3957class LiveTimerStrategy(Strategy):
3958 def __init__(self):
3959 super().__init__()
3960
3961 def on_start(self):
3962 RESULTS["on_start"] += 1
3963 self.clock.set_timer_ns(
3964 "explicit_timer",
3965 1_000_000,
3966 callback=self._on_timer,
3967 fire_immediately=True,
3968 )
3969 self.clock.set_timer_ns(
3970 "default_timer",
3971 1_000_000,
3972 fire_immediately=True,
3973 )
3974
3975 def on_stop(self):
3976 pass
3977
3978 def _on_timer(self, event):
3979 RESULTS["callback_timer_count"] += 1
3980 RESULTS["callback_event_type"] = type(event).__name__
3981 RESULTS["callback_event_name"] = event.name
3982
3983 def on_time_event(self, event):
3984 RESULTS["default_timer_count"] += 1
3985 RESULTS["default_event_type"] = type(event).__name__
3986 RESULTS["default_event_name"] = event.name
3987"#,
3988 )
3989 .expect("python test code should be valid CString");
3990
3991 py.run(code.as_c_str(), Some(&module.dict()), None)
3992 .expect("test strategy code should execute");
3993
3994 let sys_modules = py
3995 .import("sys")
3996 .expect("sys should import")
3997 .getattr("modules")
3998 .expect("sys.modules should exist");
3999 sys_modules
4000 .set_item(module_name, module)
4001 .expect("test strategy module should register");
4002 }
4003
4004 fn install_claim_strategy_module(py: Python<'_>, module_name: &str) {
4005 let module = PyModule::new(py, module_name).expect("test module should create");
4006 module
4007 .setattr("Strategy", py.get_type::<PyStrategy>())
4008 .expect("Strategy type should bind");
4009
4010 let code = CString::new(
4011 "
4012class ClaimsConfig:
4013 def __init__(
4014 self,
4015 strategy_id=None,
4016 order_id_tag=None,
4017 external_order_instrument_ids=None,
4018 oms_type=None,
4019 ):
4020 self.strategy_id = strategy_id
4021 self.order_id_tag = order_id_tag
4022 self.external_order_instrument_ids = external_order_instrument_ids
4023 self.oms_type = oms_type
4024
4025class ClaimsStrategy(Strategy):
4026 def __init__(self, config):
4027 super().__init__(config)
4028",
4029 )
4030 .expect("python test code should be valid CString");
4031
4032 py.run(code.as_c_str(), Some(&module.dict()), None)
4033 .expect("test strategy code should execute");
4034
4035 let sys_modules = py
4036 .import("sys")
4037 .expect("sys should import")
4038 .getattr("modules")
4039 .expect("sys.modules should exist");
4040 sys_modules
4041 .set_item(module_name, module)
4042 .expect("test strategy module should register");
4043 }
4044
4045 #[derive(Debug)]
4046 struct TimerStrategyResults {
4047 on_start: usize,
4048 callback_timer_count: usize,
4049 default_timer_count: usize,
4050 callback_event_type: String,
4051 default_event_type: String,
4052 callback_event_name: String,
4053 default_event_name: String,
4054 }
4055
4056 fn get_timer_results(py: Python<'_>, module_name: &str) -> TimerStrategyResults {
4057 let module = py
4058 .import(module_name)
4059 .expect("test strategy module should import");
4060 let results_obj = module.getattr("RESULTS").expect("RESULTS should exist");
4061 let results = results_obj
4062 .cast::<PyDict>()
4063 .expect("RESULTS should be a dict");
4064
4065 TimerStrategyResults {
4066 on_start: results
4067 .get_item("on_start")
4068 .expect("on_start key should exist")
4069 .extract::<usize>()
4070 .expect("on_start should extract"),
4071 callback_timer_count: results
4072 .get_item("callback_timer_count")
4073 .expect("callback_timer_count key should exist")
4074 .extract::<usize>()
4075 .expect("callback_timer_count should extract"),
4076 default_timer_count: results
4077 .get_item("default_timer_count")
4078 .expect("default_timer_count key should exist")
4079 .extract::<usize>()
4080 .expect("default_timer_count should extract"),
4081 callback_event_type: results
4082 .get_item("callback_event_type")
4083 .expect("callback_event_type key should exist")
4084 .extract::<String>()
4085 .expect("callback_event_type should extract"),
4086 default_event_type: results
4087 .get_item("default_event_type")
4088 .expect("default_event_type key should exist")
4089 .extract::<String>()
4090 .expect("default_event_type should extract"),
4091 callback_event_name: results
4092 .get_item("callback_event_name")
4093 .expect("callback_event_name key should exist")
4094 .extract::<String>()
4095 .expect("callback_event_name should extract"),
4096 default_event_name: results
4097 .get_item("default_event_name")
4098 .expect("default_event_name key should exist")
4099 .extract::<String>()
4100 .expect("default_event_name should extract"),
4101 }
4102 }
4103
4104 #[cfg(feature = "examples")]
4105 #[rstest]
4106 #[case("CompositeMarketMaker")]
4107 #[case("DeltaNeutralVol")]
4108 #[case("EmaCross")]
4109 #[case("ExecTester")]
4110 #[case("GridMarketMaker")]
4111 #[case("HurstVpinDirectional")]
4112 fn test_builtin_strategy_register_accepts_supported_names(#[case] type_name: &str) {
4113 assert!(super::builtin_strategy_register(type_name).is_some());
4114 }
4115
4116 #[cfg(feature = "examples")]
4117 #[rstest]
4118 #[case("BookImbalanceActor")]
4119 #[case("DataTester")]
4120 fn test_builtin_actor_register_accepts_supported_names(#[case] type_name: &str) {
4121 assert!(super::builtin_actor_register(type_name).is_some());
4122 }
4123
4124 #[cfg(feature = "examples")]
4125 #[rstest]
4126 fn test_builtin_register_rejects_unknown_names() {
4127 assert!(super::builtin_strategy_register("UnknownStrategy").is_none());
4128 assert!(super::builtin_actor_register("UnknownActor").is_none());
4129 }
4130
4131 #[rstest]
4132 fn test_inspection_wrappers_share_kernel_state() {
4133 let node = LiveNode::builder(TraderId::from("TESTER-001"), Environment::Sandbox)
4134 .unwrap()
4135 .with_reconciliation(false)
4136 .build()
4137 .map(PyLiveNode::new)
4138 .unwrap();
4139
4140 let cache = node.py_cache().unwrap();
4141 let portfolio = node.py_portfolio().unwrap();
4142
4143 assert!(Rc::ptr_eq(
4144 &cache.cache_rc(),
4145 &node.node_mut().unwrap().kernel().cache
4146 ));
4147 assert!(Rc::ptr_eq(
4148 &portfolio.portfolio_rc(),
4149 &node.node_mut().unwrap().kernel().portfolio
4150 ));
4151 }
4152
4153 #[cfg(feature = "examples")]
4154 #[rstest]
4155 fn test_builtin_strategy_register_rejects_mismatched_config() {
4156 Python::initialize();
4157
4158 let node = LiveNode::builder(TraderId::from("TESTER-001"), Environment::Sandbox)
4159 .unwrap()
4160 .with_reconciliation(false)
4161 .build()
4162 .map(PyLiveNode::new)
4163 .unwrap();
4164
4165 Python::attach(|py| {
4166 let register = super::builtin_strategy_register("EmaCross").unwrap();
4167 let config = PyDict::new(py);
4168 let error = register(&mut node.node_mut().unwrap(), config.as_any()).unwrap_err();
4169
4170 assert!(error.is_instance_of::<pyo3::exceptions::PyTypeError>(py));
4171 });
4172 }
4173
4174 #[cfg(feature = "examples")]
4175 #[rstest]
4176 fn test_builtin_actor_register_rejects_mismatched_config() {
4177 Python::initialize();
4178
4179 let node = LiveNode::builder(TraderId::from("TESTER-001"), Environment::Sandbox)
4180 .unwrap()
4181 .with_reconciliation(false)
4182 .build()
4183 .map(PyLiveNode::new)
4184 .unwrap();
4185
4186 Python::attach(|py| {
4187 let register = super::builtin_actor_register("DataTester").unwrap();
4188 let config = PyDict::new(py);
4189 let error = register(&mut node.node_mut().unwrap(), config.as_any()).unwrap_err();
4190
4191 assert!(error.is_instance_of::<pyo3::exceptions::PyTypeError>(py));
4192 });
4193 }
4194
4195 #[rstest]
4196 #[case(ShutdownRunPath::Native)]
4197 #[case(ShutdownRunPath::PyO3)]
4198 fn test_native_and_python_shutdown_paths_drain_cancel_command(
4199 #[case] run_path: ShutdownRunPath,
4200 ) {
4201 Python::initialize();
4202
4203 let node = LiveNode::builder(TraderId::from("TESTER-001"), Environment::Sandbox)
4204 .unwrap()
4205 .with_reconciliation(false)
4206 .with_delay_post_stop_secs(1)
4207 .with_timeout_connection(1)
4208 .build()
4209 .map(PyLiveNode::new)
4210 .unwrap();
4211 node.node_mut()
4212 .unwrap()
4213 .add_strategy(ShutdownCancelStrategy::new(InstrumentId::from(
4214 "TEST.POLYMARKET",
4215 )))
4216 .unwrap();
4217
4218 let handle = node.node_mut().unwrap().handle();
4219 let stop_handle = handle.clone();
4220
4221 let stop_thread = thread::spawn(move || {
4222 let deadline = Instant::now() + Duration::from_secs(5);
4223 while !stop_handle.is_running() && Instant::now() < deadline {
4224 thread::sleep(Duration::from_millis(10));
4225 }
4226
4227 stop_handle.stop();
4228 });
4229
4230 match run_path {
4231 ShutdownRunPath::Native => get_runtime()
4232 .block_on(node.node_mut().unwrap().run())
4233 .expect("native LiveNode run should stop cleanly"),
4234 ShutdownRunPath::PyO3 => Python::attach(|py| {
4235 super::run_live_node_detached(py, &mut node.node_mut().unwrap())
4236 .expect("Python LiveNode run should stop cleanly");
4237 }),
4238 }
4239
4240 stop_thread.join().expect("stop thread should join");
4241 let metrics = handle.metrics_snapshot();
4242
4243 assert_eq!(metrics.exec_commands.dispatched, 1);
4244 assert_eq!(metrics.exec_commands.queue_depth, 0);
4245 assert!(!handle.is_running());
4246 }
4247
4248 #[rstest]
4249 fn test_run_live_node_detached_releases_gil() {
4250 Python::initialize();
4251
4252 let node = LiveNode::builder(TraderId::from("TESTER-001"), Environment::Sandbox)
4253 .unwrap()
4254 .with_reconciliation(false)
4255 .with_delay_post_stop_secs(0)
4256 .with_timeout_connection(1)
4257 .build()
4258 .map(PyLiveNode::new)
4259 .unwrap();
4260
4261 let handle = node.node_mut().unwrap().handle();
4262 let (gil_tx, gil_rx) = mpsc::channel();
4263 let acquired_before_stop = Arc::new(AtomicBool::new(false));
4264 let acquired_before_stop_for_thread = acquired_before_stop.clone();
4265
4266 let stop_thread = thread::spawn(move || {
4267 if gil_rx.recv_timeout(Duration::from_secs(1)).is_ok() {
4268 acquired_before_stop_for_thread.store(true, Ordering::SeqCst);
4269 }
4270
4271 handle.stop();
4272 });
4273
4274 let gil_thread = thread::spawn(move || {
4275 Python::attach(|_| {});
4276 let _ = gil_tx.send(());
4277 });
4278
4279 Python::attach(|py| {
4280 super::run_live_node_detached(py, &mut node.node_mut().unwrap())
4281 .expect("node should run cleanly");
4282 });
4283
4284 stop_thread.join().expect("stop thread should join");
4285 gil_thread.join().expect("GIL thread should join");
4286
4287 assert!(
4288 acquired_before_stop.load(Ordering::SeqCst),
4289 "worker thread should acquire the GIL while LiveNode::run is blocked"
4290 );
4291 }
4292
4293 #[rstest]
4294 fn test_host_loop_waker_coalesces_redundant_wakes() {
4295 let (waker, receiver, _) = host_loop_waker_probe();
4296
4297 std::task::Wake::wake(waker.clone());
4298 let consuming_wake = receiver.recv_timeout(Duration::from_secs(1));
4299 std::task::Wake::wake_by_ref(&waker);
4300 let redundant_wake = receiver.try_recv();
4301
4302 assert!(matches!(
4303 consuming_wake,
4304 Ok(super::HostWakeSignal::Resume(7))
4305 ));
4306 assert!(matches!(redundant_wake, Err(mpsc::TryRecvError::Empty)));
4307 }
4308
4309 #[rstest]
4310 fn test_host_loop_waker_ignores_wake_after_close() {
4311 let (waker, receiver, handle) = host_loop_waker_probe();
4312 waker.control.close();
4313
4314 std::task::Wake::wake_by_ref(&waker);
4315
4316 assert!(matches!(
4317 receiver.recv().unwrap(),
4318 super::HostWakeSignal::Shutdown
4319 ));
4320 assert!(matches!(
4321 receiver.try_recv(),
4322 Err(mpsc::TryRecvError::Empty)
4323 ));
4324 assert!(!handle.should_stop());
4325 }
4326
4327 #[rstest]
4328 fn test_host_loop_waker_stops_node_when_pump_is_gone() {
4329 let (waker, receiver, handle) = host_loop_waker_probe();
4330 drop(receiver);
4331
4332 std::task::Wake::wake_by_ref(&waker);
4333
4334 assert!(!waker.control.active.load(Ordering::Acquire));
4335 assert!(handle.should_stop());
4336 }
4337
4338 fn host_loop_waker_probe() -> (
4339 Arc<super::HostLoopWaker>,
4340 mpsc::Receiver<super::HostWakeSignal>,
4341 crate::node::LiveNodeHandle,
4342 ) {
4343 let (sender, receiver) = mpsc::channel();
4344 let handle = crate::node::LiveNodeHandle::new();
4345
4346 let control = Arc::new(super::HostWakeControl {
4347 sender,
4348 active: AtomicBool::new(true),
4349 handle: handle.clone(),
4350 });
4351
4352 let waker = Arc::new(super::HostLoopWaker {
4353 generation: 7,
4354 scheduled: AtomicBool::new(false),
4355 control,
4356 });
4357
4358 (waker, receiver, handle)
4359 }
4360
4361 #[rstest]
4362 fn test_host_loop_waker_breaks_gil_dependency_lock_cycle() {
4363 Python::initialize();
4364
4365 let (waker, mut wake_pump, event_loop) = Python::attach(|py| {
4366 let event_loop = py
4367 .import("asyncio")
4368 .unwrap()
4369 .call_method0("new_event_loop")
4370 .unwrap();
4371 let state = Arc::new(super::RunWakeState::default());
4372 let wake_callback = Py::new(py, super::PyNodeRunWake { state })
4373 .unwrap()
4374 .into_any();
4375 let event_loop = event_loop.unbind();
4376 let wake_pump = super::HostWakePump::start(
4377 event_loop.clone_ref(py),
4378 wake_callback,
4379 crate::node::LiveNodeHandle::new(),
4380 )
4381 .unwrap();
4382 let waker = wake_pump.waker(1);
4383
4384 (waker, wake_pump, event_loop)
4385 });
4386
4387 let (start_tx, start_rx) = mpsc::channel();
4388 let (locked_tx, locked_rx) = mpsc::channel();
4389 let dependency_lock = Arc::new(Mutex::new(()));
4390 let dependency_lock_for_thread = dependency_lock.clone();
4391
4392 let wake_thread = thread::spawn(move || {
4393 let _guard = dependency_lock_for_thread.lock();
4394 locked_tx.send(()).unwrap();
4395 start_rx.recv().unwrap();
4396 waker.wake_by_ref();
4397 });
4398
4399 locked_rx
4400 .recv_timeout(Duration::from_secs(1))
4401 .expect("wake thread should hold the dependency lock");
4402
4403 let dependency_released_while_gil_held = Python::attach(|_| {
4404 start_tx.send(()).unwrap();
4405 let deadline = Instant::now() + Duration::from_secs(1);
4406
4407 loop {
4408 match dependency_lock.try_lock() {
4409 Some(_) => break true,
4410 None if Instant::now() < deadline => {
4411 thread::yield_now();
4412 }
4413 None => break false,
4414 }
4415 }
4416 });
4417
4418 wake_thread.join().unwrap();
4419 Python::attach(|py| {
4420 wake_pump.close();
4421 wake_pump.join(Some(py));
4422 event_loop.bind(py).call_method0("close").unwrap();
4423 });
4424
4425 assert!(dependency_released_while_gil_held);
4426 }
4427
4428 #[rstest]
4429 fn test_run_wake_state_ignores_stale_generation() {
4430 Python::initialize();
4431
4432 Python::attach(|py| {
4433 let event_loop = py
4434 .import("asyncio")
4435 .unwrap()
4436 .call_method0("new_event_loop")
4437 .unwrap();
4438 let future = event_loop.call_method0("create_future").unwrap();
4439 let state = super::RunWakeState::default();
4440 state.suspend(2, future.clone().unbind());
4441
4442 state.resume(py, 1).unwrap();
4443 let done_after_stale: bool = future.call_method0("done").unwrap().extract().unwrap();
4444 state.resume(py, 2).unwrap();
4445 let done_after_current: bool = future.call_method0("done").unwrap().extract().unwrap();
4446 event_loop.call_method0("close").unwrap();
4447
4448 assert!(!done_after_stale);
4449 assert!(done_after_current);
4450 });
4451 }
4452
4453 #[rstest]
4454 fn test_host_wake_pump_resumes_current_suspension() {
4455 Python::initialize();
4456
4457 Python::attach(|py| {
4458 let asyncio = py.import("asyncio").unwrap();
4459 let event_loop = asyncio.call_method0("new_event_loop").unwrap();
4460 let future = event_loop.call_method0("create_future").unwrap();
4461 let state = Arc::new(super::RunWakeState::default());
4462 state.suspend(7, future.clone().unbind());
4463 let wake_callback = Py::new(py, super::PyNodeRunWake { state })
4464 .unwrap()
4465 .into_any();
4466 let mut wake_pump = super::HostWakePump::start(
4467 event_loop.clone().unbind(),
4468 wake_callback,
4469 crate::node::LiveNodeHandle::new(),
4470 )
4471 .unwrap();
4472
4473 wake_pump.waker(7).wake_by_ref();
4474 let wait_for = asyncio.call_method1("wait_for", (&future, 1.0)).unwrap();
4475 let result = event_loop
4476 .call_method1("run_until_complete", (wait_for,))
4477 .unwrap();
4478 wake_pump.close();
4479 wake_pump.join(Some(py));
4480 event_loop.call_method0("close").unwrap();
4481
4482 assert!(result.is_none());
4483 });
4484 }
4485
4486 #[rstest]
4487 fn test_host_wake_pump_stops_node_when_loop_is_closed() {
4488 Python::initialize();
4489
4490 Python::attach(|py| {
4491 let event_loop = py
4492 .import("asyncio")
4493 .unwrap()
4494 .call_method0("new_event_loop")
4495 .unwrap();
4496 event_loop.call_method0("close").unwrap();
4497 let state = Arc::new(super::RunWakeState::default());
4498 let wake_callback = Py::new(py, super::PyNodeRunWake { state })
4499 .unwrap()
4500 .into_any();
4501 let handle = crate::node::LiveNodeHandle::new();
4502 let mut wake_pump =
4503 super::HostWakePump::start(event_loop.unbind(), wake_callback, handle.clone())
4504 .unwrap();
4505
4506 wake_pump.waker(1).wake_by_ref();
4507 wake_pump.join(Some(py));
4508
4509 assert!(handle.should_stop());
4510 });
4511 }
4512
4513 #[rstest]
4514 fn test_hosted_run_completes_after_stop() {
4515 Python::initialize();
4516
4517 let node = LiveNode::builder(TraderId::from("TESTER-001"), Environment::Sandbox)
4518 .unwrap()
4519 .with_reconciliation(false)
4520 .with_delay_post_stop_secs(0)
4521 .with_delay_shutdown_secs(0)
4522 .with_timeout_connection(0)
4523 .with_timeout_disconnection_secs(0)
4524 .build()
4525 .map(PyLiveNode::new)
4526 .unwrap();
4527
4528 Python::attach(|py| {
4529 let locals = PyDict::new(py);
4530 let py_node = Py::new(py, node).unwrap();
4531 locals.set_item("node", &py_node).unwrap();
4532 py.run(
4533 pyo3::ffi::c_str!(
4534 "import asyncio\n\nasync def run_node():\n handle = node.handle()\n asyncio.get_running_loop().call_soon(handle.stop)\n await asyncio.wait_for(node.run_async(), timeout=5)\n\nasyncio.run(run_node())"
4535 ),
4536 Some(&locals),
4537 None,
4538 )
4539 .unwrap();
4540
4541 let node = py_node.borrow(py);
4542 assert!(!node.is_consumed());
4543 assert_eq!(
4544 node.node().unwrap().state(),
4545 crate::node::NodeState::Stopped
4546 );
4547 });
4548
4549 get_message_bus().borrow_mut().dispose();
4550 }
4551
4552 #[rstest]
4553 fn test_build_routes_venue_less_data_client_with_venue_routing() {
4554 Python::initialize();
4555
4556 let routing = RoutingConfig::builder()
4557 .venues(vec!["IBIS".to_string()])
4558 .build();
4559 let node = LiveNode::builder(TraderId::from("TEST-001"), Environment::Sandbox)
4560 .unwrap()
4561 .with_reconciliation(false)
4562 .with_timeout_connection(1)
4563 .add_data_client_with_routing(
4564 Some("IB".to_string()),
4565 Box::new(VenueLessDataClientFactory),
4566 Box::new(TestDataClientConfig),
4567 routing,
4568 )
4569 .unwrap()
4570 .build();
4571
4572 assert!(node.is_ok(), "build should succeed: {:?}", node.err());
4573 }
4574
4575 #[rstest]
4576 fn test_build_routes_venue_less_data_client_with_default_and_venues() {
4577 Python::initialize();
4578
4579 let routing = RoutingConfig::builder()
4580 .default(true)
4581 .venues(vec!["IBIS".to_string()])
4582 .build();
4583 let node = LiveNode::builder(TraderId::from("TEST-001"), Environment::Sandbox)
4584 .unwrap()
4585 .with_reconciliation(false)
4586 .with_timeout_connection(1)
4587 .add_data_client_with_routing(
4588 Some("IB".to_string()),
4589 Box::new(VenueLessDataClientFactory),
4590 Box::new(TestDataClientConfig),
4591 routing,
4592 )
4593 .unwrap()
4594 .build();
4595
4596 assert!(node.is_ok(), "build should succeed: {:?}", node.err());
4597 }
4598
4599 #[rstest]
4600 fn test_stop_live_node_detached_releases_gil() {
4601 Python::initialize();
4602
4603 let node = LiveNode::builder(TraderId::from("TESTER-002"), Environment::Sandbox)
4604 .unwrap()
4605 .with_reconciliation(false)
4606 .with_delay_post_stop_secs(1)
4607 .with_timeout_connection(1)
4608 .build()
4609 .map(PyLiveNode::new)
4610 .unwrap();
4611
4612 get_runtime()
4613 .block_on(async { node.node_mut().unwrap().start().await })
4614 .expect("node should start");
4615
4616 let (attempt_tx, attempt_rx) = mpsc::channel();
4617 let acquired_before_stop_return = Arc::new(AtomicBool::new(false));
4618 let acquired_before_stop_return_for_thread = acquired_before_stop_return.clone();
4619 let stop_returned = Arc::new(AtomicBool::new(false));
4620 let stop_returned_for_thread = stop_returned.clone();
4621 let mut gil_thread = None;
4622
4623 Python::attach(|py| {
4624 gil_thread = Some(thread::spawn(move || {
4625 attempt_tx
4626 .send(())
4627 .expect("GIL acquisition attempt should send");
4628 Python::attach(|_| {});
4629
4630 if !stop_returned_for_thread.load(Ordering::SeqCst) {
4631 acquired_before_stop_return_for_thread.store(true, Ordering::SeqCst);
4632 }
4633 }));
4634
4635 attempt_rx
4636 .recv_timeout(Duration::from_secs(1))
4637 .expect("worker thread should attempt to acquire the GIL");
4638
4639 super::stop_live_node_detached(py, &mut node.node_mut().unwrap())
4640 .expect("node should stop cleanly");
4641 stop_returned.store(true, Ordering::SeqCst);
4642 });
4643
4644 gil_thread
4645 .expect("GIL worker thread should be spawned")
4646 .join()
4647 .expect("GIL worker thread should join");
4648
4649 assert!(
4650 acquired_before_stop_return.load(Ordering::SeqCst),
4651 "worker thread should acquire the GIL while LiveNode::stop is blocked"
4652 );
4653 assert!(!node.node_mut().unwrap().is_running());
4654 }
4655
4656 #[rstest]
4657 fn test_py_dispose_disposes_kernel_after_stop_error() {
4658 Python::initialize();
4659
4660 let dispose_count = Arc::new(AtomicUsize::new(0));
4661 let factory = TestDisconnectFailureDataClientFactory::new(dispose_count.clone());
4662 let config = TestDataClientConfig;
4663 let node = LiveNode::builder(TraderId::from("TESTER-003"), Environment::Sandbox)
4664 .unwrap()
4665 .with_reconciliation(false)
4666 .with_delay_post_stop_secs(0)
4667 .with_timeout_connection(1)
4668 .with_timeout_disconnection_secs(0)
4669 .add_data_client(
4670 Some("TEST_DISCONNECT_FAILURE".to_string()),
4671 Box::new(factory),
4672 Box::new(config),
4673 )
4674 .unwrap()
4675 .build()
4676 .map(PyLiveNode::new)
4677 .unwrap();
4678
4679 let dispose_result = Python::attach(|py| {
4680 get_runtime()
4681 .block_on(async { node.node_mut().unwrap().start().await })
4682 .expect("node should start");
4683 assert!(node.node_mut().unwrap().is_running());
4684
4685 node.py_dispose(py)
4686 });
4687
4688 let error = dispose_result.expect_err("dispose should return the stop error");
4689
4690 assert!(error.to_string().contains("test disconnect failed"));
4691 assert_eq!(dispose_count.load(Ordering::Relaxed), 1);
4692 assert!(!node.node_mut().unwrap().is_running());
4693 }
4694
4695 #[rstest]
4696 fn test_live_node_pystrategy_timer_callbacks_run_on_event_loop() {
4697 Python::initialize();
4698
4699 let module_name = "test_live_node_timer_strategy";
4700 Python::attach(|py| install_timer_strategy_module(py, module_name));
4701
4702 let node = LiveNode::builder(TraderId::from("TESTER-001"), Environment::Sandbox)
4703 .unwrap()
4704 .with_reconciliation(false)
4705 .with_delay_post_stop_secs(0)
4706 .with_timeout_connection(1)
4707 .build()
4708 .map(PyLiveNode::new)
4709 .unwrap();
4710
4711 let importable = ImportableStrategyConfig {
4712 strategy_path: format!("{module_name}:LiveTimerStrategy"),
4713 config_path: String::new(),
4714 config: HashMap::new(),
4715 };
4716
4717 Python::attach(|py| {
4718 node.py_add_strategy_from_config(py, importable)
4719 .expect("strategy should register");
4720 });
4721
4722 let handle = node.node_mut().unwrap().handle();
4723 let stop_handle = handle.clone();
4724 let watchdog_handle = handle;
4725 let (done_tx, done_rx) = mpsc::channel();
4726 let module_name_for_stop = module_name.to_string();
4727
4728 let stop_thread = thread::spawn(move || {
4729 let deadline = Instant::now() + Duration::from_secs(5);
4730
4731 loop {
4732 let fired = Python::attach(|py| {
4733 let results = get_timer_results(py, &module_name_for_stop);
4734 results.callback_timer_count > 0 && results.default_timer_count > 0
4735 });
4736
4737 if fired || Instant::now() >= deadline {
4738 break;
4739 }
4740
4741 thread::sleep(Duration::from_millis(20));
4742 }
4743
4744 stop_handle.stop();
4745 });
4746
4747 let watchdog_thread = thread::spawn(move || {
4748 if done_rx.recv_timeout(Duration::from_secs(5)).is_err() {
4749 watchdog_handle.stop();
4750 }
4751 });
4752
4753 Python::attach(|py| {
4754 super::run_live_node_detached(py, &mut node.node_mut().unwrap())
4755 .expect("node should run cleanly");
4756 });
4757
4758 let _ = done_tx.send(());
4759 stop_thread.join().expect("stop thread should join");
4760 watchdog_thread.join().expect("watchdog thread should join");
4761
4762 let results = Python::attach(|py| get_timer_results(py, module_name));
4763
4764 assert_eq!(results.on_start, 1);
4765 assert!(results.callback_timer_count > 0);
4766 assert!(results.default_timer_count > 0);
4767 assert_eq!(results.callback_event_type, "TimeEvent");
4768 assert_eq!(results.default_event_type, "TimeEvent");
4769 assert_eq!(results.callback_event_name, "explicit_timer");
4770 assert_eq!(results.default_event_name, "default_timer");
4771 }
4772
4773 #[rstest]
4774 fn test_add_actor_registers_constructed_python_instance() {
4775 Python::initialize();
4776
4777 let node = LiveNode::builder(TraderId::from("TESTER-001"), Environment::Sandbox)
4778 .unwrap()
4779 .with_reconciliation(false)
4780 .with_delay_post_stop_secs(0)
4781 .with_timeout_connection(1)
4782 .build()
4783 .map(PyLiveNode::new)
4784 .unwrap();
4785 let actor_id = ActorId::from("ACTOR-INSTANCE-001");
4786
4787 Python::attach(|py| {
4788 let config = py
4789 .eval(c_str!("type('_Cfg', (), {})()"), None, None)
4790 .unwrap();
4791 config.setattr("actor_id", actor_id.to_string()).unwrap();
4792 let actor = py
4793 .get_type::<PyDataActor>()
4794 .as_any()
4795 .call1((config,))
4796 .unwrap();
4797
4798 node.py_add_actor(&actor).expect("actor should register");
4799
4800 let actor = actor.extract::<PyRef<PyDataActor>>().unwrap();
4801 assert_eq!(actor.actor_id(), actor_id);
4802 assert!(actor.is_registered());
4803 });
4804
4805 assert_eq!(
4806 node.node_mut()
4807 .unwrap()
4808 .kernel()
4809 .trader
4810 .borrow()
4811 .actor_ids(),
4812 vec![actor_id]
4813 );
4814 }
4815
4816 #[rstest]
4817 fn test_add_actor_rejects_non_idle_node() {
4818 Python::initialize();
4819
4820 let node = LiveNode::builder(TraderId::from("TESTER-001"), Environment::Sandbox)
4821 .unwrap()
4822 .with_reconciliation(false)
4823 .with_delay_post_stop_secs(0)
4824 .with_timeout_connection(1)
4825 .build()
4826 .map(PyLiveNode::new)
4827 .unwrap();
4828
4829 Python::attach(|py| {
4830 let actor = py.get_type::<PyDataActor>().call0().unwrap();
4831 node.handle.set_starting();
4832
4833 let error = node
4834 .py_add_actor(&actor)
4835 .expect_err("a non-idle node should reject actor registration");
4836 let actor = actor.extract::<PyRef<PyDataActor>>().unwrap();
4837
4838 assert_eq!(
4839 error.to_string(),
4840 "RuntimeError: Cannot add actor while node is running, add actors before running the node"
4841 );
4842 assert!(!actor.is_registered());
4843 });
4844
4845 assert!(
4846 node.node()
4847 .unwrap()
4848 .kernel()
4849 .trader
4850 .borrow()
4851 .actor_ids()
4852 .is_empty()
4853 );
4854 }
4855
4856 #[rstest]
4857 fn test_add_strategy_from_config_registers_external_order_instrument_ids() {
4858 Python::initialize();
4859
4860 let module_name = "test_live_node_claim_strategy";
4861 Python::attach(|py| install_claim_strategy_module(py, module_name));
4862
4863 let node = LiveNode::builder(TraderId::from("TESTER-001"), Environment::Sandbox)
4864 .unwrap()
4865 .with_reconciliation(false)
4866 .with_delay_post_stop_secs(0)
4867 .with_timeout_connection(1)
4868 .build()
4869 .map(PyLiveNode::new)
4870 .unwrap();
4871
4872 let instrument_id = InstrumentId::from("AUDUSD.SIM");
4873 let strategy_id = StrategyId::from("CLAIMS-001");
4874 let mut config = HashMap::new();
4875 config.insert(
4876 "strategy_id".to_string(),
4877 serde_json::json!(strategy_id.to_string()),
4878 );
4879 config.insert(
4880 "external_order_instrument_ids".to_string(),
4881 serde_json::json!([instrument_id.to_string()]),
4882 );
4883
4884 let importable = ImportableStrategyConfig {
4885 strategy_path: format!("{module_name}:ClaimsStrategy"),
4886 config_path: format!("{module_name}:ClaimsConfig"),
4887 config,
4888 };
4889
4890 Python::attach(|py| {
4891 node.py_add_strategy_from_config(py, importable)
4892 .expect("strategy should register");
4893 });
4894
4895 {
4896 let guard = node.node_mut().unwrap();
4897 let exec_engine = guard.kernel().exec_engine.borrow();
4898 assert_eq!(
4899 exec_engine.get_external_order_claim(&instrument_id),
4900 Some(strategy_id)
4901 );
4902 }
4903
4904 let result = node
4905 .node_mut()
4906 .unwrap()
4907 .exec_manager_mut()
4908 .claim_external_orders(instrument_id, StrategyId::from("OTHER-001"));
4909
4910 assert!(result.is_err());
4911 assert!(
4912 result
4913 .unwrap_err()
4914 .to_string()
4915 .contains("already exists for CLAIMS-001")
4916 );
4917 }
4918
4919 #[rstest]
4920 fn test_add_strategy_registers_constructed_python_instance() {
4921 Python::initialize();
4922
4923 let module_name = "test_live_node_add_strategy_instance";
4924 Python::attach(|py| install_claim_strategy_module(py, module_name));
4925
4926 let node = LiveNode::builder(TraderId::from("TESTER-001"), Environment::Sandbox)
4927 .unwrap()
4928 .with_reconciliation(false)
4929 .with_delay_post_stop_secs(0)
4930 .with_timeout_connection(1)
4931 .build()
4932 .map(PyLiveNode::new)
4933 .unwrap();
4934
4935 let instrument_id = InstrumentId::from("AUDUSD.SIM");
4936 let strategy_id = StrategyId::from("CLAIMS-002");
4937
4938 Python::attach(|py| {
4939 let module = py.import(module_name).expect("test module should import");
4940 let kwargs = PyDict::new(py);
4941 kwargs
4942 .set_item("strategy_id", strategy_id.to_string())
4943 .unwrap();
4944 kwargs
4945 .set_item(
4946 "external_order_instrument_ids",
4947 vec![instrument_id.to_string()],
4948 )
4949 .unwrap();
4950 let config = module
4951 .getattr("ClaimsConfig")
4952 .unwrap()
4953 .call((), Some(&kwargs))
4954 .unwrap();
4955 let strategy = module
4956 .getattr("ClaimsStrategy")
4957 .unwrap()
4958 .call1((config,))
4959 .unwrap();
4960
4961 node.py_add_strategy(&strategy)
4962 .expect("strategy should register");
4963 });
4964
4965 {
4966 let guard = node.node_mut().unwrap();
4967 let exec_engine = guard.kernel().exec_engine.borrow();
4968 assert_eq!(
4969 exec_engine.get_external_order_claim(&instrument_id),
4970 Some(strategy_id)
4971 );
4972 }
4973 }
4974
4975 #[rstest]
4976 fn test_add_strategy_constructed_python_instance_registers_oms_type() {
4977 Python::initialize();
4978
4979 let module_name = "test_live_node_add_strategy_instance_oms_type";
4980 Python::attach(|py| install_claim_strategy_module(py, module_name));
4981
4982 let node = LiveNode::builder(TraderId::from("TESTER-001"), Environment::Sandbox)
4983 .unwrap()
4984 .with_reconciliation(false)
4985 .with_delay_post_stop_secs(0)
4986 .with_timeout_connection(1)
4987 .build()
4988 .map(PyLiveNode::new)
4989 .unwrap();
4990 let strategy_id = StrategyId::from("FUNDING_ARBITRAGE-003");
4991
4992 Python::attach(|py| {
4993 let module = py.import(module_name).expect("test module should import");
4994 let kwargs = PyDict::new(py);
4995 kwargs
4996 .set_item("strategy_id", strategy_id.to_string())
4997 .unwrap();
4998 kwargs.set_item("oms_type", OmsType::Hedging).unwrap();
4999 let config = module
5000 .getattr("ClaimsConfig")
5001 .unwrap()
5002 .call((), Some(&kwargs))
5003 .unwrap();
5004 let strategy = module
5005 .getattr("ClaimsStrategy")
5006 .unwrap()
5007 .call1((config,))
5008 .unwrap();
5009
5010 node.py_add_strategy(&strategy)
5011 .expect("strategy should register");
5012 });
5013
5014 let instrument = crypto_perpetual_ethusdt();
5015 let instrument_id = instrument.id();
5016 let client_id = ClientId::from("STUB");
5017
5018 node.node_mut()
5019 .unwrap()
5020 .kernel()
5021 .cache
5022 .borrow_mut()
5023 .add_instrument(InstrumentAny::CryptoPerpetual(instrument))
5024 .unwrap();
5025 node.node_mut()
5026 .unwrap()
5027 .kernel()
5028 .exec_engine
5029 .borrow_mut()
5030 .register_client(Box::new(StubExecutionClient::new(
5031 client_id,
5032 AccountId::from("TEST-ACCOUNT"),
5033 instrument_id.venue,
5034 OmsType::Netting,
5035 None,
5036 )))
5037 .unwrap();
5038
5039 let order = OrderTestBuilder::new(OrderType::Market)
5040 .trader_id(node.node_mut().unwrap().trader_id())
5041 .strategy_id(strategy_id)
5042 .instrument_id(instrument_id)
5043 .quantity(Quantity::from("1.000"))
5044 .build();
5045 let position_id = PositionId::new("CUSTOM-POSITION-003");
5046
5047 node.node_mut()
5048 .unwrap()
5049 .kernel()
5050 .cache
5051 .borrow_mut()
5052 .add_order(order.clone(), Some(position_id), Some(client_id), true)
5053 .unwrap();
5054
5055 let submit_order = SubmitOrder::new(
5056 order.trader_id(),
5057 Some(client_id),
5058 strategy_id,
5059 instrument_id,
5060 order.client_order_id(),
5061 order.init_event().clone(),
5062 order.exec_algorithm_id(),
5063 Some(position_id),
5064 None,
5065 UUID4::new(),
5066 UnixNanos::default(),
5067 None,
5068 );
5069
5070 node.node_mut()
5071 .unwrap()
5072 .kernel()
5073 .exec_engine
5074 .borrow()
5075 .execute(TradingCommand::SubmitOrder(submit_order));
5076
5077 let guard = node.node_mut().unwrap();
5078 let exec_engine = guard.kernel().exec_engine.borrow();
5079 let cache = exec_engine.cache().borrow();
5080 let cached_order = cache
5081 .order(&order.client_order_id())
5082 .expect("Order should be cached");
5083
5084 assert_eq!(cached_order.status(), OrderStatus::Initialized);
5085 }
5086
5087 #[rstest]
5088 fn test_add_strategy_constructed_python_instance_claim_conflict_does_not_register() {
5089 Python::initialize();
5090
5091 let module_name = "test_live_node_add_strategy_instance_claim_conflict";
5092 Python::attach(|py| install_claim_strategy_module(py, module_name));
5093
5094 let node = LiveNode::builder(TraderId::from("TESTER-001"), Environment::Sandbox)
5095 .unwrap()
5096 .with_reconciliation(false)
5097 .with_delay_post_stop_secs(0)
5098 .with_timeout_connection(1)
5099 .build()
5100 .map(PyLiveNode::new)
5101 .unwrap();
5102 let instrument_id = InstrumentId::from("AUDUSD.SIM");
5103 let first_strategy_id = StrategyId::from("CLAIMS-PRIMARY-001");
5104 let conflicting_strategy_id = StrategyId::from("CLAIMS-CONFLICT-002");
5105
5106 let (error, conflicting_strategy_registered) = Python::attach(|py| {
5107 let module = py.import(module_name).expect("test module should import");
5108 let first_kwargs = PyDict::new(py);
5109 first_kwargs
5110 .set_item("strategy_id", first_strategy_id.to_string())
5111 .unwrap();
5112 first_kwargs
5113 .set_item(
5114 "external_order_instrument_ids",
5115 vec![instrument_id.to_string()],
5116 )
5117 .unwrap();
5118 let first_config = module
5119 .getattr("ClaimsConfig")
5120 .unwrap()
5121 .call((), Some(&first_kwargs))
5122 .unwrap();
5123 let first_strategy = module
5124 .getattr("ClaimsStrategy")
5125 .unwrap()
5126 .call1((first_config,))
5127 .unwrap();
5128 node.py_add_strategy(&first_strategy)
5129 .expect("first strategy should register");
5130
5131 let conflicting_kwargs = PyDict::new(py);
5132 conflicting_kwargs
5133 .set_item("strategy_id", conflicting_strategy_id.to_string())
5134 .unwrap();
5135 conflicting_kwargs
5136 .set_item(
5137 "external_order_instrument_ids",
5138 vec![instrument_id.to_string()],
5139 )
5140 .unwrap();
5141 let conflicting_config = module
5142 .getattr("ClaimsConfig")
5143 .unwrap()
5144 .call((), Some(&conflicting_kwargs))
5145 .unwrap();
5146 let conflicting_strategy = module
5147 .getattr("ClaimsStrategy")
5148 .unwrap()
5149 .call1((conflicting_config,))
5150 .unwrap();
5151 let error = node
5152 .py_add_strategy(&conflicting_strategy)
5153 .expect_err("conflicting claim should fail");
5154 let is_registered = conflicting_strategy
5155 .extract::<PyRef<PyStrategy>>()
5156 .unwrap()
5157 .is_registered();
5158
5159 (error, is_registered)
5160 });
5161
5162 let strategy_ids = node
5163 .node_mut()
5164 .unwrap()
5165 .kernel()
5166 .trader
5167 .borrow()
5168 .strategy_ids();
5169 let manager_claim = node
5170 .node_mut()
5171 .unwrap()
5172 .exec_manager()
5173 .get_external_order_claim(&instrument_id);
5174 let engine_claim = node
5175 .node_mut()
5176 .unwrap()
5177 .kernel()
5178 .exec_engine
5179 .borrow()
5180 .get_external_order_claim(&instrument_id);
5181
5182 assert!(
5183 error
5184 .to_string()
5185 .contains("already exists for CLAIMS-PRIMARY-001")
5186 );
5187 assert!(!conflicting_strategy_registered);
5188 assert_eq!(strategy_ids, vec![first_strategy_id]);
5189 assert_eq!(manager_claim, Some(first_strategy_id));
5190 assert_eq!(engine_claim, Some(first_strategy_id));
5191 }
5192
5193 #[rstest]
5194 fn test_add_strategy_constructed_python_instance_duplicate_tag_does_not_register() {
5195 Python::initialize();
5196
5197 let module_name = "test_live_node_add_strategy_instance_duplicate_tag";
5198 Python::attach(|py| install_claim_strategy_module(py, module_name));
5199
5200 let node = LiveNode::builder(TraderId::from("TESTER-001"), Environment::Sandbox)
5201 .unwrap()
5202 .with_reconciliation(false)
5203 .with_delay_post_stop_secs(0)
5204 .with_timeout_connection(1)
5205 .build()
5206 .map(PyLiveNode::new)
5207 .unwrap();
5208 let first_strategy_id = StrategyId::from("TAGGED-FIRST-777");
5209 let instrument_id = InstrumentId::from("AUDUSD.SIM");
5210
5211 let (error, duplicate_strategy_registered) = Python::attach(|py| {
5212 let module = py.import(module_name).expect("test module should import");
5213 let first_kwargs = PyDict::new(py);
5214 first_kwargs
5215 .set_item("strategy_id", "TAGGED-FIRST")
5216 .unwrap();
5217 first_kwargs.set_item("order_id_tag", "777").unwrap();
5218 let first_config = module
5219 .getattr("ClaimsConfig")
5220 .unwrap()
5221 .call((), Some(&first_kwargs))
5222 .unwrap();
5223 let first_strategy = module
5224 .getattr("ClaimsStrategy")
5225 .unwrap()
5226 .call1((first_config,))
5227 .unwrap();
5228 node.py_add_strategy(&first_strategy)
5229 .expect("first strategy should register");
5230
5231 let duplicate_kwargs = PyDict::new(py);
5232 duplicate_kwargs
5233 .set_item("strategy_id", "TAGGED-SECOND")
5234 .unwrap();
5235 duplicate_kwargs.set_item("order_id_tag", "777").unwrap();
5236 duplicate_kwargs
5237 .set_item(
5238 "external_order_instrument_ids",
5239 vec![instrument_id.to_string()],
5240 )
5241 .unwrap();
5242 let duplicate_config = module
5243 .getattr("ClaimsConfig")
5244 .unwrap()
5245 .call((), Some(&duplicate_kwargs))
5246 .unwrap();
5247 let duplicate_strategy = module
5248 .getattr("ClaimsStrategy")
5249 .unwrap()
5250 .call1((duplicate_config,))
5251 .unwrap();
5252 let error = node
5253 .py_add_strategy(&duplicate_strategy)
5254 .expect_err("duplicate order ID tag should fail");
5255 let is_registered = duplicate_strategy
5256 .extract::<PyRef<PyStrategy>>()
5257 .unwrap()
5258 .is_registered();
5259
5260 (error, is_registered)
5261 });
5262
5263 let strategy_ids = node
5264 .node_mut()
5265 .unwrap()
5266 .kernel()
5267 .trader
5268 .borrow()
5269 .strategy_ids();
5270
5271 assert!(error.to_string().contains("order_id_tag conflict"));
5272 assert!(!duplicate_strategy_registered);
5273 assert_eq!(strategy_ids, vec![first_strategy_id]);
5274 assert_eq!(
5275 node.node_mut()
5276 .unwrap()
5277 .kernel()
5278 .cache
5279 .borrow()
5280 .external_order_claim(&instrument_id),
5281 None
5282 );
5283 }
5284
5285 #[tokio::test(flavor = "current_thread")]
5286 async fn test_live_node_pystrategy_request_bars_dispatches_on_historical_bars() {
5287 Python::initialize();
5288
5289 let module_name = "test_live_node_historical_bars_strategy";
5290 Python::attach(|py| install_tracking_strategy_module(py, module_name));
5291
5292 let request_count = Arc::new(AtomicUsize::new(0));
5293 let response_sent_count = Arc::new(AtomicUsize::new(0));
5294 let handler_visible_count = Arc::new(AtomicUsize::new(0));
5295
5296 let factory = TestHistoricalBarsDataClientFactory::new(
5297 request_count.clone(),
5298 response_sent_count.clone(),
5299 handler_visible_count.clone(),
5300 );
5301 let config = TestDataClientConfig;
5302
5303 let node = LiveNode::builder(TraderId::from("TESTER-001"), Environment::Sandbox)
5304 .unwrap()
5305 .with_reconciliation(false)
5306 .with_delay_post_stop_secs(0)
5307 .with_timeout_connection(1)
5308 .add_data_client(
5309 Some("TEST_DATA".to_string()),
5310 Box::new(factory),
5311 Box::new(config),
5312 )
5313 .unwrap()
5314 .build()
5315 .map(PyLiveNode::new)
5316 .unwrap();
5317
5318 let importable = ImportableStrategyConfig {
5319 strategy_path: format!("{module_name}:HistoricalBarsStrategy"),
5320 config_path: String::new(),
5321 config: HashMap::new(),
5322 };
5323
5324 Python::attach(|py| {
5325 node.py_add_strategy_from_config(py, importable)
5326 .expect("strategy should register");
5327 });
5328
5329 let handle = node.node_mut().unwrap().handle();
5330 let stop_handle = handle.clone();
5331 let response_sent_count_for_stop = response_sent_count.clone();
5332
5333 tokio::spawn(async move {
5334 let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
5335
5336 loop {
5337 if response_sent_count_for_stop.load(Ordering::Relaxed) == 1
5338 || tokio::time::Instant::now() >= deadline
5339 {
5340 break;
5341 }
5342
5343 tokio::time::sleep(Duration::from_millis(20)).await;
5344 }
5345
5346 tokio::time::sleep(Duration::from_millis(250)).await;
5347 stop_handle.stop();
5348 });
5349
5350 node.node_mut()
5351 .unwrap()
5352 .run()
5353 .await
5354 .expect("node should run cleanly");
5355
5356 let (on_start, on_historical_bars, historical_bar_count) =
5357 Python::attach(|py| get_results(py, module_name));
5358
5359 assert_eq!(request_count.load(Ordering::Relaxed), 1);
5360 assert_eq!(handler_visible_count.load(Ordering::Relaxed), 1);
5361 assert_eq!(response_sent_count.load(Ordering::Relaxed), 1);
5362 assert_eq!(on_start, 1);
5363 assert_eq!(on_historical_bars, 1);
5364 assert_eq!(historical_bar_count, 1);
5365 }
5366}