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