1use std::{cell::RefCell, collections::HashMap, fmt::Debug, rc::Rc, time::Duration};
19
20use ahash::{AHashMap, AHashSet};
21use nautilus_common::{
22 cache::{CacheConfig, database::CacheDatabaseFactory},
23 clients::ExecutionClient,
24 clock::Clock,
25 enums::Environment,
26 factories::{
27 ClientConfig, DataClientFactory, ExecutionClientFactory, SimulatedExecutionClientFactory,
28 },
29 logging::logger::LoggerConfig,
30 msgbus::{
31 BusMessage, MessageBusBackingFactory, MessageBusConfig, MessageBusExternalEgress,
32 MessageBusExternalIngress, external_egress_from_backing, external_io_from_backing,
33 },
34};
35use nautilus_core::UUID4;
36use nautilus_data::client::DataClientAdapter;
37use nautilus_execution::engine::ExecutionEngine;
38use nautilus_model::identifiers::{TraderId, Venue};
39use nautilus_portfolio::config::PortfolioConfig;
40#[cfg(feature = "streaming")]
41use nautilus_system::config::StreamingConfig;
42#[cfg(feature = "python")]
43use nautilus_system::trader::Trader;
44use nautilus_system::{
45 clock_factory::ClockFactory,
46 event_store::{EventStoreFactory, KernelEventStore},
47 kernel::{NautilusKernel, NautilusKernelDependencies},
48};
49use nautilus_trading::ImportableControllerConfig;
50
51use super::{
52 LiveNode, LiveNodeHandle,
53 config::{
54 LiveDataEngineConfig, LiveExecutionEngineConfig, LiveNodeConfig, LiveRiskEngineConfig,
55 RoutingConfig, validate_live_environment,
56 },
57};
58use crate::{
59 execution::{
60 client::LiveExecutionClient,
61 manager::{ExecutionManager, ExecutionManagerConfig},
62 },
63 runner::AsyncRunner,
64 socket::SocketReconnectRegistry,
65};
66
67#[derive(Debug)]
68enum ExecutionClientFactoryEntry {
69 Adapter(Box<dyn ExecutionClientFactory>),
70 Simulated(Box<dyn SimulatedExecutionClientFactory>),
71}
72
73pub(crate) struct ExternalMessageBusIngress(Box<dyn MessageBusExternalIngress>);
74
75impl Debug for ExternalMessageBusIngress {
76 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77 f.debug_struct(stringify!(ExternalMessageBusIngress))
78 .finish_non_exhaustive()
79 }
80}
81
82#[cfg_attr(
88 feature = "python",
89 pyo3::pyclass(module = "nautilus_trader.live", unsendable)
90)]
91pub struct LiveNodeBuilder {
92 name: String,
93 config: LiveNodeConfig,
94 data_client_factories: HashMap<String, Box<dyn DataClientFactory>>,
95 exec_client_factories: HashMap<String, ExecutionClientFactoryEntry>,
96 data_client_configs: HashMap<String, Box<dyn ClientConfig>>,
97 exec_client_configs: HashMap<String, Box<dyn ClientConfig>>,
98 data_client_routing: HashMap<String, RoutingConfig>,
99 exec_client_routing: HashMap<String, RoutingConfig>,
100 event_store_factory: Option<EventStoreFactory>,
101 clock_factory: Option<ClockFactory>,
102 cache_database_factory: Option<Box<dyn CacheDatabaseFactory>>,
103 external_msgbus_factory: Option<Box<dyn MessageBusBackingFactory>>,
104 external_msgbus_egress: Option<Box<dyn MessageBusExternalEgress>>,
105 external_msgbus_ingress: Option<ExternalMessageBusIngress>,
106}
107
108impl Debug for LiveNodeBuilder {
109 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110 f.debug_struct(stringify!(LiveNodeBuilder))
111 .field("name", &self.name)
112 .field("config", &self.config)
113 .field("data_client_factories", &self.data_client_factories.keys())
114 .field("exec_client_factories", &self.exec_client_factories.keys())
115 .field("data_client_configs", &self.data_client_configs.keys())
116 .field("exec_client_configs", &self.exec_client_configs.keys())
117 .field("event_store_factory", &self.event_store_factory.is_some())
118 .field("clock_factory", &self.clock_factory.is_some())
119 .field(
120 "cache_database_factory",
121 &self.cache_database_factory.is_some(),
122 )
123 .field(
124 "external_msgbus_factory",
125 &self.external_msgbus_factory.is_some(),
126 )
127 .field(
128 "external_msgbus_egress",
129 &self.external_msgbus_egress.is_some(),
130 )
131 .field(
132 "external_msgbus_ingress",
133 &self.external_msgbus_ingress.is_some(),
134 )
135 .finish_non_exhaustive()
136 }
137}
138
139impl LiveNodeBuilder {
140 pub fn new(trader_id: TraderId, environment: Environment) -> anyhow::Result<Self> {
146 validate_live_environment(environment)?;
147
148 let config = LiveNodeConfig {
149 environment,
150 trader_id,
151 ..Default::default()
152 };
153
154 Ok(Self {
155 name: "LiveNode".to_string(),
156 config,
157 data_client_factories: HashMap::new(),
158 exec_client_factories: HashMap::new(),
159 data_client_configs: HashMap::new(),
160 exec_client_configs: HashMap::new(),
161 data_client_routing: HashMap::new(),
162 exec_client_routing: HashMap::new(),
163 event_store_factory: None,
164 clock_factory: None,
165 cache_database_factory: None,
166 external_msgbus_factory: None,
167 external_msgbus_egress: None,
168 external_msgbus_ingress: None,
169 })
170 }
171
172 pub fn from_config(config: LiveNodeConfig) -> anyhow::Result<Self> {
178 validate_live_environment(config.environment)?;
179
180 Ok(Self {
181 name: "LiveNode".to_string(),
182 config,
183 data_client_factories: HashMap::new(),
184 exec_client_factories: HashMap::new(),
185 data_client_configs: HashMap::new(),
186 exec_client_configs: HashMap::new(),
187 data_client_routing: HashMap::new(),
188 exec_client_routing: HashMap::new(),
189 event_store_factory: None,
190 clock_factory: None,
191 cache_database_factory: None,
192 external_msgbus_factory: None,
193 external_msgbus_egress: None,
194 external_msgbus_ingress: None,
195 })
196 }
197
198 #[must_use]
200 pub fn name(&self) -> &str {
201 &self.name
202 }
203
204 #[must_use]
206 pub fn with_name(mut self, name: impl Into<String>) -> Self {
207 self.name = name.into();
208 self
209 }
210
211 #[must_use]
213 pub const fn with_instance_id(mut self, instance_id: UUID4) -> Self {
214 self.config.instance_id = Some(instance_id);
215 self
216 }
217
218 #[must_use]
220 pub const fn with_load_state(mut self, load_state: bool) -> Self {
221 self.config.load_state = load_state;
222 self
223 }
224
225 #[must_use]
227 pub const fn with_save_state(mut self, save_state: bool) -> Self {
228 self.config.save_state = save_state;
229 self
230 }
231
232 #[must_use]
241 pub fn with_controller(mut self, controller: ImportableControllerConfig) -> Self {
242 self.config.controller = Some(controller);
243 self
244 }
245
246 #[must_use]
248 pub const fn with_timeout_connection(mut self, timeout_secs: u64) -> Self {
249 self.config.timeout_connection = Duration::from_secs(timeout_secs);
250 self
251 }
252
253 #[must_use]
255 pub const fn with_timeout_reconciliation(mut self, timeout_secs: u64) -> Self {
256 self.config.timeout_reconciliation = Duration::from_secs(timeout_secs);
257 self
258 }
259
260 #[must_use]
262 pub fn with_reconciliation(mut self, reconciliation: bool) -> Self {
263 self.config.exec_engine.reconciliation = reconciliation;
264 self
265 }
266
267 #[must_use]
269 pub fn with_reconciliation_lookback_mins(mut self, mins: u32) -> Self {
270 self.config.exec_engine.reconciliation_lookback_mins = Some(mins);
271 self
272 }
273
274 #[must_use]
276 pub const fn with_timeout_portfolio(mut self, timeout_secs: u64) -> Self {
277 self.config.timeout_portfolio = Duration::from_secs(timeout_secs);
278 self
279 }
280
281 #[must_use]
283 pub const fn with_timeout_disconnection_secs(mut self, timeout_secs: u64) -> Self {
284 self.config.timeout_disconnection = Duration::from_secs(timeout_secs);
285 self
286 }
287
288 #[must_use]
290 pub const fn with_delay_post_stop_secs(mut self, delay_secs: u64) -> Self {
291 self.config.delay_post_stop = Duration::from_secs(delay_secs);
292 self
293 }
294
295 #[must_use]
297 pub const fn with_delay_shutdown_secs(mut self, delay_secs: u64) -> Self {
298 self.config.timeout_shutdown = Duration::from_secs(delay_secs);
299 self
300 }
301
302 #[must_use]
304 pub fn with_clock_factory<F>(mut self, factory: F) -> Self
305 where
306 F: Fn() -> Rc<RefCell<dyn Clock>> + 'static,
307 {
308 self.clock_factory = Some(ClockFactory::new(factory));
309 self
310 }
311
312 #[must_use]
314 pub fn with_cache_config(mut self, config: CacheConfig) -> Self {
315 self.config.cache = Some(config);
316 self
317 }
318
319 #[must_use]
324 pub fn with_cache_database_factory(mut self, factory: Box<dyn CacheDatabaseFactory>) -> Self {
325 self.cache_database_factory = Some(factory);
326 self
327 }
328
329 #[must_use]
334 pub fn with_msgbus_config(mut self, config: MessageBusConfig) -> Self {
335 self.config.msgbus = Some(config);
336 self
337 }
338
339 #[must_use]
341 pub fn with_portfolio_config(mut self, config: PortfolioConfig) -> Self {
342 self.config.portfolio = Some(config);
343 self
344 }
345
346 #[cfg(feature = "streaming")]
351 #[must_use]
352 pub fn with_streaming_config(mut self, config: StreamingConfig) -> Self {
353 self.config.streaming = Some(config);
354 self
355 }
356
357 #[must_use]
362 pub fn with_data_engine_config(mut self, config: LiveDataEngineConfig) -> Self {
363 self.config.data_engine = config;
364 self
365 }
366
367 #[must_use]
372 pub fn with_risk_engine_config(mut self, config: LiveRiskEngineConfig) -> Self {
373 self.config.risk_engine = config;
374 self
375 }
376
377 #[must_use]
382 pub fn with_exec_engine_config(mut self, config: LiveExecutionEngineConfig) -> Self {
383 self.config.exec_engine = config;
384 self
385 }
386
387 #[must_use]
396 pub fn with_event_store<F>(mut self, factory: F) -> Self
397 where
398 F: FnOnce(UUID4, Rc<RefCell<dyn Clock>>) -> anyhow::Result<Box<dyn KernelEventStore>>
399 + 'static,
400 {
401 self.event_store_factory = Some(Box::new(factory));
402 self
403 }
404
405 #[must_use]
407 pub fn with_external_msgbus_egress(
408 mut self,
409 external_egress: Box<dyn MessageBusExternalEgress>,
410 ) -> Self {
411 self.external_msgbus_egress = Some(external_egress);
412 self
413 }
414
415 #[must_use]
417 pub fn with_external_msgbus_factory(
418 mut self,
419 factory: Box<dyn MessageBusBackingFactory>,
420 ) -> Self {
421 self.external_msgbus_factory = Some(factory);
422 self
423 }
424
425 #[must_use]
427 pub fn with_external_ingress(
428 mut self,
429 external_ingress: Box<dyn MessageBusExternalIngress>,
430 ) -> Self {
431 self.external_msgbus_ingress = Some(ExternalMessageBusIngress(external_ingress));
432 self
433 }
434
435 #[must_use]
437 pub fn with_logging(mut self, logging: LoggerConfig) -> Self {
438 self.config.logging = logging;
439 self
440 }
441
442 pub fn add_data_client(
448 self,
449 name: Option<String>,
450 factory: Box<dyn DataClientFactory>,
451 config: Box<dyn ClientConfig>,
452 ) -> anyhow::Result<Self> {
453 self.add_data_client_with_routing(name, factory, config, RoutingConfig::default())
454 }
455
456 #[cfg(feature = "python")]
457 pub(crate) fn has_data_client(&self, name: &str) -> bool {
458 self.data_client_factories.contains_key(name)
459 }
460
461 #[cfg(feature = "python")]
462 pub(crate) fn has_exec_client(&self, name: &str) -> bool {
463 self.exec_client_factories.contains_key(name)
464 }
465
466 pub fn add_data_client_with_routing(
472 mut self,
473 name: Option<String>,
474 factory: Box<dyn DataClientFactory>,
475 config: Box<dyn ClientConfig>,
476 routing: RoutingConfig,
477 ) -> anyhow::Result<Self> {
478 let name = name.unwrap_or_else(|| factory.name().to_string());
479
480 if self.data_client_factories.contains_key(&name) {
481 anyhow::bail!("Data client '{name}' is already registered");
482 }
483
484 self.data_client_factories.insert(name.clone(), factory);
485 self.data_client_configs.insert(name.clone(), config);
486 self.data_client_routing.insert(name, routing);
487 Ok(self)
488 }
489
490 pub fn add_exec_client(
499 self,
500 name: Option<String>,
501 factory: Box<dyn ExecutionClientFactory>,
502 config: Box<dyn ClientConfig>,
503 ) -> anyhow::Result<Self> {
504 self.add_exec_client_with_routing(name, factory, config, RoutingConfig::default())
505 }
506
507 pub fn add_exec_client_with_routing(
517 mut self,
518 name: Option<String>,
519 factory: Box<dyn ExecutionClientFactory>,
520 config: Box<dyn ClientConfig>,
521 routing: RoutingConfig,
522 ) -> anyhow::Result<Self> {
523 let name = name.unwrap_or_else(|| factory.name().to_string());
524
525 if self.exec_client_factories.contains_key(&name) {
526 anyhow::bail!("Execution client '{name}' is already registered");
527 }
528
529 self.exec_client_factories
530 .insert(name.clone(), ExecutionClientFactoryEntry::Adapter(factory));
531 self.exec_client_configs.insert(name.clone(), config);
532 self.exec_client_routing.insert(name, routing);
533 Ok(self)
534 }
535
536 pub fn add_simulated_exec_client(
545 mut self,
546 name: Option<String>,
547 factory: Box<dyn SimulatedExecutionClientFactory>,
548 config: Box<dyn ClientConfig>,
549 ) -> anyhow::Result<Self> {
550 let name = name.unwrap_or_else(|| factory.name().to_string());
551
552 if self.exec_client_factories.contains_key(&name) {
553 anyhow::bail!("Execution client '{name}' is already registered");
554 }
555
556 self.exec_client_factories.insert(
557 name.clone(),
558 ExecutionClientFactoryEntry::Simulated(factory),
559 );
560 self.exec_client_configs.insert(name, config);
561 Ok(self)
562 }
563
564 pub fn build(mut self) -> anyhow::Result<LiveNode> {
577 self.build_in_place()
578 }
579
580 pub(crate) fn build_in_place(&mut self) -> anyhow::Result<LiveNode> {
581 let thread_owner = LiveNode::acquire_thread()?;
582
583 log::info!(
584 "Building LiveNode with {} data clients and {} execution clients",
585 self.data_client_factories.len(),
586 self.exec_client_factories.len()
587 );
588
589 self.config.validate_runtime_support()?;
590
591 if self.config.event_store.is_some() && self.event_store_factory.is_none() {
592 anyhow::bail!(
593 "LiveNodeConfig.event_store is set but no factory was registered; \
594 call LiveNodeBuilder::with_event_store(...) to install one"
595 );
596 }
597
598 if self.external_msgbus_factory.is_some()
599 && (self.external_msgbus_egress.is_some() || self.external_msgbus_ingress.is_some())
600 {
601 anyhow::bail!(
602 "external message bus factory cannot be combined with injected egress or ingress"
603 );
604 }
605
606 let handle = LiveNodeHandle::new();
607 let runner = AsyncRunner::new();
608 runner.bind_senders_for_node(handle.clone());
609
610 let socket_registry = SocketReconnectRegistry::default();
611
612 let kernel = NautilusKernel::new_with_dependencies(
613 self.name.clone(),
614 self.config.clone(),
615 NautilusKernelDependencies::default()
616 .with_clock_factory(self.clock_factory.clone())
617 .with_event_store_factory(self.event_store_factory.take()),
618 )?;
619
620 #[cfg(feature = "python")]
621 if let Some(controller) = self.config.controller.as_ref() {
622 Trader::add_controller_from_importable_config(&kernel.trader, controller)?;
623 }
624
625 #[cfg(not(feature = "python"))]
626 if let Some(controller) = self.config.controller.as_ref() {
627 anyhow::bail!(
628 "LiveNodeConfig.controller for importable controller '{}' requires the python feature",
629 controller.controller_path
630 );
631 }
632
633 let (external_egress, external_ingress) = self.create_external_msgbus(&kernel)?;
634
635 if let Some(external_egress) = external_egress {
636 let config = self.config.msgbus.clone().unwrap_or_default();
637 nautilus_common::msgbus::get_message_bus()
638 .borrow_mut()
639 .set_external_egress_config(external_egress, &config)?;
640 }
641
642 for (name, factory) in &self.data_client_factories {
643 if let Some(config) = self.data_client_configs.get(name) {
644 log::debug!("Creating data client {name}");
645
646 let client = socket_registry.scope(|| {
647 factory.create(name, config.as_ref(), kernel.cache().into(), kernel.clock())
648 })?;
649
650 let client_id = client.client_id();
651 let venue = client.venue();
652 socket_registry.register_client(client_id);
653
654 let adapter = DataClientAdapter::new(
655 client_id, venue, true, true, client,
658 );
659
660 let routing = self
661 .data_client_routing
662 .get(name)
663 .cloned()
664 .unwrap_or_default();
665
666 {
667 let mut data_engine = kernel.data_engine.borrow_mut();
668 data_engine.register_client(adapter, venue);
669
670 if routing.default {
671 data_engine.set_default_client(client_id)?;
672 }
673
674 if let Some(venues) = &routing.venues {
675 for venue_str in venues {
676 data_engine.register_venue_routing(
677 client_id,
678 Venue::new(venue_str.as_str()),
679 )?;
680 }
681 }
682 }
683
684 log::info!("Registered DataClient-{client_id}");
685 } else {
686 log::warn!("No config found for data client factory {name}");
687 }
688 }
689
690 let mut exec_clients = Vec::new();
691 let mut venue_candidates = AHashMap::<Venue, Vec<_>>::new();
692 let mut venues_explicit = AHashSet::new();
693 let mut has_default_client = false;
694 let mut instrument_venues = AHashSet::new();
695 let mut self_settling_venues = AHashSet::new();
696
697 for (name, factory) in &self.exec_client_factories {
698 if let Some(config) = self.exec_client_configs.get(name) {
699 log::debug!("Creating execution client {name}");
700
701 let client = socket_registry.scope(|| match factory {
702 ExecutionClientFactoryEntry::Adapter(factory) => factory.create(
703 self.config.trader_id,
704 name,
705 config.as_ref(),
706 kernel.cache().into(),
707 kernel.clock(),
708 ),
709 ExecutionClientFactoryEntry::Simulated(factory) => {
710 factory.create(self.config.trader_id, name, config.as_ref(), kernel.cache())
711 }
712 })?;
713
714 let client = LiveExecutionClient::new(client);
715 let client_id = client.client_id();
716 let venue = client.venue();
717 socket_registry.register_client(client_id);
718
719 let routing = self
720 .exec_client_routing
721 .get(name)
722 .cloned()
723 .unwrap_or_default();
724
725 {
726 let mut exec_engine = kernel.exec_engine.borrow_mut();
727 exec_engine.register_client(Box::new(client.clone()))?;
728
729 if routing.default {
730 exec_engine.set_default_client(client_id)?;
731 has_default_client = true;
732 }
733
734 if let Some(venues) = &routing.venues {
735 for venue_str in venues {
736 let route_venue = Venue::new(venue_str.as_str());
737 exec_engine.register_venue_routing(client_id, route_venue)?;
738 venues_explicit.insert(route_venue);
739 instrument_venues.insert(route_venue);
740
741 if client.settles_contract_expirations() {
742 self_settling_venues.insert(route_venue);
743 }
744 }
745 }
746 }
747
748 venue_candidates.entry(venue).or_default().push(client_id);
749 instrument_venues.insert(venue);
750
751 if client.settles_contract_expirations() {
752 self_settling_venues.insert(venue);
753 }
754
755 exec_clients.push(client);
756
757 log::info!("Registered ExecutionClient-{client_id}");
758 } else {
759 log::warn!("No config found for execution client factory {name}");
760 }
761 }
762
763 {
764 let mut exec_engine = kernel.exec_engine.borrow_mut();
765
766 for (venue, candidates) in venue_candidates {
767 if venues_explicit.contains(&venue) {
768 continue;
769 }
770
771 if let [client_id] = candidates.as_slice() {
772 exec_engine.register_venue_routing(*client_id, venue)?;
773 } else if !has_default_client {
774 anyhow::bail!(
775 "Multiple execution clients for venue {venue}: configure an explicit venue route or default client"
776 );
777 }
778 }
779 }
780
781 for venue in instrument_venues {
782 ExecutionEngine::subscribe_venue_instruments(&kernel.exec_engine, venue);
783
784 if !self_settling_venues.contains(&venue) {
785 ExecutionEngine::subscribe_venue_instrument_closes(&kernel.exec_engine, venue);
786 }
787 }
788
789 let exec_manager_config = ExecutionManagerConfig::from(&self.config.exec_engine)
790 .with_trader_id(self.config.trader_id);
791
792 let mut exec_manager = ExecutionManager::new(
793 kernel.clock.clone(),
794 kernel.cache.clone(),
795 exec_manager_config,
796 )?;
797
798 for client in &exec_clients {
799 exec_manager.set_position_reconciliation_tolerance(
800 client.account_id(),
801 client.position_reconciliation_tolerance(),
802 );
803 }
804
805 let mut node = LiveNode::new_from_builder(
806 kernel,
807 runner,
808 self.config.clone(),
809 handle,
810 exec_manager,
811 exec_clients,
812 socket_registry,
813 None,
814 external_ingress,
815 thread_owner,
816 );
817 node.load_configured_plugins()?;
818 node.cache_database_factory = self.cache_database_factory.take();
819
820 log::info!("Built successfully");
821
822 Ok(node)
823 }
824
825 #[expect(
826 clippy::type_complexity,
827 reason = "external backing returns its paired egress and ingress"
828 )]
829 fn create_external_msgbus(
830 &mut self,
831 kernel: &NautilusKernel,
832 ) -> anyhow::Result<(
833 Option<Box<dyn MessageBusExternalEgress>>,
834 Option<ExternalMessageBusIngress>,
835 )> {
836 let Some(factory) = self.external_msgbus_factory.as_ref() else {
837 return Ok((
838 self.external_msgbus_egress.take(),
839 self.external_msgbus_ingress.take(),
840 ));
841 };
842
843 let config = self.config.msgbus.clone().unwrap_or_default();
844 let has_external_streams = config
845 .external_streams
846 .as_ref()
847 .is_some_and(|streams| !streams.is_empty());
848 config.validate()?;
849 let backing = factory.create(self.config.trader_id, kernel.instance_id, config)?;
850
851 if has_external_streams {
852 let (external_egress, external_ingress) = external_io_from_backing(backing);
853 Ok((
854 Some(external_egress),
855 Some(ExternalMessageBusIngress(external_ingress)),
856 ))
857 } else {
858 Ok((Some(external_egress_from_backing(backing)), None))
859 }
860 }
861}
862
863impl ExternalMessageBusIngress {
864 pub(crate) fn is_closed(&self) -> bool {
865 self.0.is_closed()
866 }
867
868 pub(crate) fn take_receiver(
869 &mut self,
870 ) -> anyhow::Result<tokio::sync::mpsc::Receiver<BusMessage>> {
871 self.0.take_receiver()
872 }
873
874 pub(crate) fn close(&mut self) {
875 self.0.close();
876 }
877}
878
879#[cfg(test)]
880mod tests {
881 use std::{cell::RefCell, collections::HashMap, rc::Rc};
882
883 use nautilus_common::{
884 cache::CacheView,
885 clients::ExecutionClient,
886 clock::Clock,
887 enums::Environment,
888 factories::{ClientConfig, ExecutionClientFactory},
889 messages::execution::{SubmitOrder, TradingCommand},
890 msgbus::{
891 self, MessagingSwitchboard, stubs::get_typed_into_message_saving_handler, switchboard,
892 },
893 };
894 use nautilus_core::{UUID4, UnixNanos};
895 use nautilus_execution::engine::stubs::StubExecutionClient;
896 use nautilus_model::{
897 accounts::{AccountAny, CashAccount},
898 data::InstrumentClose,
899 enums::{AccountType, InstrumentCloseType, OmsType, OrderSide, OrderType},
900 events::{AccountState, OrderDeniedReason, OrderEventAny},
901 identifiers::{AccountId, ClientId, ClientOrderId, TraderId, Venue},
902 instruments::{
903 Instrument, InstrumentAny,
904 stubs::{audusd_sim, binary_option},
905 },
906 orders::{Order, OrderTestBuilder},
907 stubs::TestDefault,
908 types::{AccountBalance, Money, Price, Quantity},
909 };
910 use nautilus_trading::ImportableControllerConfig;
911 use rstest::rstest;
912 use ustr::Ustr;
913
914 use super::LiveNodeBuilder;
915 use crate::node::config::RoutingConfig;
916
917 #[rstest]
918 fn test_with_controller_sets_config_controller() {
919 let controller = ImportableControllerConfig {
920 controller_path: "module:Controller".to_string(),
921 config_path: "module:ControllerConfig".to_string(),
922 config: HashMap::new(),
923 };
924
925 let builder = LiveNodeBuilder::new(TraderId::from("TRADER-001"), Environment::Live)
926 .unwrap()
927 .with_controller(controller);
928
929 assert!(builder.config.controller.is_some());
930 }
931
932 #[rstest]
933 #[case::single(1, false, false, false)]
934 #[case::explicit(2, true, false, false)]
935 #[case::default(2, false, true, false)]
936 #[case::explicit_over_default(2, true, true, false)]
937 #[case::single_empty_venues(1, false, false, true)]
938 #[case::default_empty_venues(2, false, true, true)]
939 fn test_execution_client_routing_and_instruments(
940 #[case] count: usize,
941 #[case] explicit: bool,
942 #[case] default: bool,
943 #[case] empty_venues: bool,
944 #[values(false, true)] reverse: bool,
945 ) {
946 let clients: Vec<_> = (0..count)
947 .map(|i| {
948 StubExecutionClient::new(
949 ClientId::new(format!("CLIENT-{i}")),
950 AccountId::new(format!("ACCOUNT-{i}")),
951 Venue::from("SIM"),
952 OmsType::Netting,
953 None,
954 )
955 })
956 .collect();
957
958 let mut builder =
959 LiveNodeBuilder::new(TraderId::test_default(), Environment::Live).unwrap();
960 let mut indices: Vec<_> = (0..count).collect();
961
962 if reverse {
963 indices.reverse();
964 }
965
966 for i in indices {
967 builder = builder
968 .add_exec_client_with_routing(
969 Some(format!("client-{i}")),
970 Box::new(RoutingClientFactory(clients[i].clone())),
971 Box::new(RoutingClientConfig),
972 RoutingConfig {
973 default: default && i == 0,
974 venues: if empty_venues {
975 Some(vec![])
976 } else {
977 (explicit && i == 1).then(|| vec!["SIM".to_string()])
978 },
979 },
980 )
981 .unwrap();
982 }
983
984 let node = builder.build().unwrap();
985 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
986 msgbus::publish_instrument(
987 switchboard::get_instrument_topic(instrument.id()),
988 &instrument,
989 );
990 let order = OrderTestBuilder::new(OrderType::Market)
991 .instrument_id(instrument.id())
992 .quantity(Quantity::from(1))
993 .build();
994 node.kernel()
995 .cache
996 .borrow_mut()
997 .add_instrument(instrument.clone())
998 .unwrap();
999 node.kernel()
1000 .cache
1001 .borrow_mut()
1002 .add_order(order.clone(), None, None, false)
1003 .unwrap();
1004 let engine = node.kernel().exec_engine.borrow();
1005 engine.execute(TradingCommand::SubmitOrder(SubmitOrder::from_order(
1006 &order,
1007 TraderId::test_default(),
1008 None,
1009 None,
1010 UUID4::new(),
1011 UnixNanos::default(),
1012 )));
1013 let routed = engine.get_clients_for_orders(std::slice::from_ref(&order));
1014 let explicit_index = count - 1 - usize::from(explicit);
1015 let explicit_order = OrderTestBuilder::new(OrderType::Market)
1016 .instrument_id(instrument.id())
1017 .client_order_id(ClientOrderId::from("O-EXPLICIT"))
1018 .quantity(Quantity::from(2))
1019 .build();
1020 node.kernel()
1021 .cache
1022 .borrow_mut()
1023 .add_order(explicit_order.clone(), None, None, false)
1024 .unwrap();
1025 engine.execute(TradingCommand::SubmitOrder(SubmitOrder::from_order(
1026 &explicit_order,
1027 TraderId::test_default(),
1028 Some(clients[explicit_index].client_id()),
1029 None,
1030 UUID4::new(),
1031 UnixNanos::default(),
1032 )));
1033
1034 assert_eq!(engine.client_ids().len(), count);
1035 assert_eq!(routed.len(), 1);
1036 assert_eq!(
1037 routed[0].client_id(),
1038 clients[usize::from(explicit)].client_id()
1039 );
1040
1041 for (i, client) in clients.iter().enumerate() {
1042 let mut expected_orders = if i == usize::from(explicit) {
1043 vec![order.client_order_id()]
1044 } else {
1045 vec![]
1046 };
1047
1048 if i == explicit_index {
1049 expected_orders.push(explicit_order.client_order_id());
1050 }
1051
1052 assert_eq!(*client.submitted_order_ids().borrow(), expected_orders);
1053 assert_eq!(
1054 *client.received_instruments().borrow(),
1055 vec![instrument.clone()]
1056 );
1057 }
1058 }
1059
1060 #[rstest]
1061 fn test_execution_client_routing_selects_risk_check_account() {
1062 let clients: Vec<_> = (0..2)
1063 .map(|i| {
1064 StubExecutionClient::new(
1065 ClientId::new(format!("CLIENT-{i}")),
1066 AccountId::new(format!("ACCOUNT-{i}")),
1067 Venue::from("SIM"),
1068 OmsType::Netting,
1069 None,
1070 )
1071 })
1072 .collect();
1073
1074 let mut builder =
1075 LiveNodeBuilder::new(TraderId::test_default(), Environment::Live).unwrap();
1076
1077 for (i, client) in clients.iter().enumerate() {
1078 builder = builder
1079 .add_exec_client_with_routing(
1080 Some(format!("client-{i}")),
1081 Box::new(RoutingClientFactory(client.clone())),
1082 Box::new(RoutingClientConfig),
1083 RoutingConfig {
1084 default: i == 0,
1085 venues: None,
1086 },
1087 )
1088 .unwrap();
1089 }
1090
1091 let node = builder.build().unwrap();
1092 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
1093 let order = OrderTestBuilder::new(OrderType::Limit)
1094 .instrument_id(instrument.id())
1095 .side(OrderSide::Buy)
1096 .quantity(Quantity::from(100))
1097 .price(Price::from("1.00000"))
1098 .build();
1099 let explicit_order = OrderTestBuilder::new(OrderType::Limit)
1100 .instrument_id(instrument.id())
1101 .client_order_id(ClientOrderId::from("O-EXPLICIT"))
1102 .side(OrderSide::Buy)
1103 .quantity(Quantity::from(200))
1104 .price(Price::from("1.00000"))
1105 .build();
1106 {
1107 let mut cache = node.kernel().cache.borrow_mut();
1108 cache.add_instrument(instrument).unwrap();
1109 cache
1110 .add_account(cash_account(clients[0].account_id(), "1000000 USD"))
1111 .unwrap();
1112 cache
1113 .add_account(cash_account(clients[1].account_id(), "150 USD"))
1114 .unwrap();
1115 cache.add_order(order.clone(), None, None, false).unwrap();
1116 cache
1117 .add_order(explicit_order.clone(), None, None, false)
1118 .unwrap();
1119 }
1120
1121 let (command_handler, commands) =
1122 get_typed_into_message_saving_handler::<TradingCommand>(None);
1123 msgbus::register_trading_command_endpoint(
1124 MessagingSwitchboard::exec_engine_queue_execute(),
1125 command_handler,
1126 );
1127 let (event_handler, events) = get_typed_into_message_saving_handler::<OrderEventAny>(None);
1128 msgbus::register_order_event_endpoint(
1129 MessagingSwitchboard::exec_engine_process(),
1130 event_handler,
1131 );
1132
1133 let mut risk_engine = node.kernel().risk_engine.borrow_mut();
1134 risk_engine.execute(TradingCommand::SubmitOrder(SubmitOrder::from_order(
1135 &order,
1136 TraderId::test_default(),
1137 None,
1138 None,
1139 UUID4::new(),
1140 UnixNanos::default(),
1141 )));
1142 risk_engine.execute(TradingCommand::SubmitOrder(SubmitOrder::from_order(
1143 &explicit_order,
1144 TraderId::test_default(),
1145 Some(clients[1].client_id()),
1146 None,
1147 UUID4::new(),
1148 UnixNanos::default(),
1149 )));
1150
1151 let forwarded: Vec<_> = commands
1152 .get_messages()
1153 .iter()
1154 .map(|command| match command {
1155 TradingCommand::SubmitOrder(cmd) => (cmd.client_order_id, cmd.client_id),
1156 other => panic!("Unexpected command {other:?}"),
1157 })
1158 .collect();
1159
1160 let events = events.get_messages();
1161 assert_eq!(forwarded, vec![(order.client_order_id(), None)]);
1162 assert_eq!(events.len(), 1);
1163
1164 let OrderEventAny::Denied(denied) = &events[0] else {
1165 panic!("Expected OrderDenied")
1166 };
1167
1168 assert_eq!(denied.client_order_id, explicit_order.client_order_id());
1169 assert_eq!(
1170 denied.reason,
1171 Ustr::from(
1172 &OrderDeniedReason::NotionalExceedsFreeBalance {
1173 free_balance: Money::from("150 USD"),
1174 notional: Money::from("200 USD"),
1175 }
1176 .to_string()
1177 )
1178 );
1179 }
1180
1181 #[rstest]
1182 fn test_execution_client_keeps_native_route_with_extra_venue(
1183 #[values(false, true)] other_native_client: bool,
1184 ) {
1185 let client = StubExecutionClient::new(
1186 ClientId::from("CLIENT"),
1187 AccountId::from("CLIENT-001"),
1188 Venue::from("SIM"),
1189 OmsType::Netting,
1190 None,
1191 )
1192 .with_handles_all_order_venues();
1193
1194 let other_client = StubExecutionClient::new(
1195 ClientId::from("OTHER_CLIENT"),
1196 AccountId::from("OTHER_CLIENT-002"),
1197 Venue::from("OTHER"),
1198 OmsType::Netting,
1199 None,
1200 );
1201
1202 let mut builder = LiveNodeBuilder::new(TraderId::test_default(), Environment::Live)
1203 .unwrap()
1204 .add_exec_client_with_routing(
1205 Some("client".to_string()),
1206 Box::new(RoutingClientFactory(client.clone())),
1207 Box::new(RoutingClientConfig),
1208 RoutingConfig {
1209 default: false,
1210 venues: Some(vec!["OTHER".to_string(), "OTHER".to_string()]),
1211 },
1212 )
1213 .unwrap();
1214
1215 if other_native_client {
1216 builder = builder
1217 .add_exec_client_with_routing(
1218 Some("other-client".to_string()),
1219 Box::new(RoutingClientFactory(other_client.clone())),
1220 Box::new(RoutingClientConfig),
1221 RoutingConfig::default(),
1222 )
1223 .unwrap();
1224 }
1225
1226 let node = builder.build().unwrap();
1227
1228 let native = audusd_sim();
1229 let mut other = native.clone();
1230 other.id.venue = Venue::from("OTHER");
1231 let instruments = [
1232 InstrumentAny::CurrencyPair(native),
1233 InstrumentAny::CurrencyPair(other),
1234 ];
1235
1236 for instrument in &instruments {
1237 let order = OrderTestBuilder::new(OrderType::Market)
1238 .instrument_id(instrument.id())
1239 .client_order_id(ClientOrderId::new(format!("O-{}", instrument.id().venue)))
1240 .quantity(Quantity::from(1))
1241 .build();
1242 let engine = node.kernel().exec_engine.borrow();
1243 node.kernel()
1244 .cache
1245 .borrow_mut()
1246 .add_instrument(instrument.clone())
1247 .unwrap();
1248 node.kernel()
1249 .cache
1250 .borrow_mut()
1251 .add_order(order.clone(), None, None, false)
1252 .unwrap();
1253 engine.execute(TradingCommand::SubmitOrder(SubmitOrder::from_order(
1254 &order,
1255 TraderId::test_default(),
1256 None,
1257 None,
1258 UUID4::new(),
1259 UnixNanos::default(),
1260 )));
1261 let routed = engine.get_clients_for_orders(&[order]);
1262 assert_eq!(
1263 routed
1264 .iter()
1265 .map(|client| client.client_id())
1266 .collect::<Vec<_>>(),
1267 vec![client.client_id()]
1268 );
1269 drop(engine);
1270 msgbus::publish_instrument(
1271 switchboard::get_instrument_topic(instrument.id()),
1272 instrument,
1273 );
1274 }
1275
1276 assert_eq!(*client.received_instruments().borrow(), instruments);
1277 assert_eq!(
1278 *client.submitted_order_ids().borrow(),
1279 vec![ClientOrderId::from("O-SIM"), ClientOrderId::from("O-OTHER")]
1280 );
1281 assert_eq!(*other_client.submitted_order_ids().borrow(), vec![]);
1282
1283 let expected_other = if other_native_client {
1284 vec![instruments[1].clone()]
1285 } else {
1286 vec![]
1287 };
1288
1289 assert_eq!(
1290 *other_client.received_instruments().borrow(),
1291 expected_other
1292 );
1293 }
1294
1295 #[rstest]
1296 #[case::ambiguous(
1297 false,
1298 false,
1299 false,
1300 "Multiple execution clients for venue SIM: configure an explicit venue route or default client"
1301 )]
1302 #[case::duplicate_id(true, false, false, "Client already registered with ID CLIENT-0")]
1303 #[case::duplicate_default(false, true, false, "default client already registered")]
1304 #[case::duplicate_route(false, false, true, "cannot re-route")]
1305 fn test_execution_client_routing_rejects_ambiguity(
1306 #[case] duplicate_id: bool,
1307 #[case] default: bool,
1308 #[case] explicit: bool,
1309 #[case] expected: &str,
1310 ) {
1311 let mut builder =
1312 LiveNodeBuilder::new(TraderId::test_default(), Environment::Live).unwrap();
1313
1314 for i in 0..2 {
1315 let id = if duplicate_id { 0 } else { i };
1316
1317 let client = StubExecutionClient::new(
1318 ClientId::new(format!("CLIENT-{id}")),
1319 AccountId::new(format!("ACCOUNT-{i}")),
1320 Venue::from("SIM"),
1321 OmsType::Netting,
1322 None,
1323 );
1324 builder = builder
1325 .add_exec_client_with_routing(
1326 Some(format!("client-{i}")),
1327 Box::new(RoutingClientFactory(client)),
1328 Box::new(RoutingClientConfig),
1329 RoutingConfig {
1330 default,
1331 venues: explicit.then(|| vec!["SIM".to_string()]),
1332 },
1333 )
1334 .unwrap();
1335 }
1336
1337 let error = builder.build().unwrap_err().to_string();
1338 assert!(error.contains(expected), "Unexpected error: {error}");
1339 }
1340
1341 fn cash_account(account_id: AccountId, free: &str) -> AccountAny {
1342 let state = AccountState::new(
1343 account_id,
1344 AccountType::Cash,
1345 vec![AccountBalance::new(
1346 Money::from(free),
1347 Money::from("0 USD"),
1348 Money::from(free),
1349 )],
1350 vec![],
1351 true,
1352 UUID4::new(),
1353 UnixNanos::default(),
1354 UnixNanos::default(),
1355 None,
1356 );
1357 AccountAny::Cash(CashAccount::new(state, false, false))
1358 }
1359
1360 #[rstest]
1361 #[case::venue(false, true)]
1362 #[case::self_settling_venue(true, false)]
1363 fn test_execution_engine_settles_instrument_closes_unless_venue_settles_itself(
1364 #[case] self_settling: bool,
1365 #[case] expected_settled: bool,
1366 ) {
1367 let instrument = InstrumentAny::BinaryOption(binary_option());
1368
1369 let mut client = StubExecutionClient::new(
1370 ClientId::from("CLIENT"),
1371 AccountId::from("CLIENT-001"),
1372 instrument.id().venue,
1373 OmsType::Netting,
1374 None,
1375 );
1376
1377 if self_settling {
1378 client = client.with_settles_contract_expirations();
1379 }
1380
1381 let node = LiveNodeBuilder::new(TraderId::test_default(), Environment::Live)
1382 .unwrap()
1383 .add_exec_client_with_routing(
1384 Some("client".to_string()),
1385 Box::new(RoutingClientFactory(client)),
1386 Box::new(RoutingClientConfig),
1387 RoutingConfig::default(),
1388 )
1389 .unwrap()
1390 .build()
1391 .unwrap();
1392 node.kernel()
1393 .cache
1394 .borrow_mut()
1395 .add_instrument(instrument.clone())
1396 .unwrap();
1397
1398 let close = InstrumentClose::new(
1399 instrument.id(),
1400 Price::from("1.000"),
1401 InstrumentCloseType::ContractExpired,
1402 UnixNanos::from(300),
1403 UnixNanos::from(301),
1404 );
1405
1406 msgbus::publish_any(
1407 switchboard::get_instrument_close_topic(instrument.id()),
1408 &close,
1409 );
1410
1411 assert_eq!(
1412 node.kernel()
1413 .cache
1414 .borrow()
1415 .instrument_close(&instrument.id()),
1416 expected_settled.then_some(&close)
1417 );
1418 }
1419
1420 #[derive(Debug)]
1421 struct RoutingClientFactory(StubExecutionClient);
1422
1423 impl ExecutionClientFactory for RoutingClientFactory {
1424 fn create(
1425 &self,
1426 _trader_id: TraderId,
1427 _name: &str,
1428 _config: &dyn ClientConfig,
1429 _cache: CacheView,
1430 _clock: Rc<RefCell<dyn Clock>>,
1431 ) -> anyhow::Result<Box<dyn ExecutionClient>> {
1432 Ok(Box::new(self.0.clone()))
1433 }
1434
1435 fn name(&self) -> &'static str {
1436 "routing"
1437 }
1438
1439 fn config_type(&self) -> &'static str {
1440 "RoutingClientConfig"
1441 }
1442 }
1443
1444 #[derive(Debug)]
1445 struct RoutingClientConfig;
1446
1447 impl ClientConfig for RoutingClientConfig {
1448 fn as_any(&self) -> &dyn std::any::Any {
1449 self
1450 }
1451 }
1452}