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