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