1use std::{num::NonZeroUsize, sync::OnceLock};
31
32use ahash::AHashMap;
33use nautilus_model::{
34 data::{BarType, DataType},
35 identifiers::{ClientOrderId, InstrumentId, OptionSeriesId, PositionId, StrategyId, Venue},
36};
37
38use super::mstr::{Endpoint, MStr, Pattern, Topic};
39use crate::msgbus::get_message_bus;
40
41pub const CLOSE_TOPIC: &str = "CLOSE";
42pub const TIME_EVENT_TOPIC: &str = "clock.time_event";
43
44static DATA_QUEUE_COMMAND_ENDPOINT: OnceLock<MStr<Endpoint>> = OnceLock::new();
45static DATA_EXECUTE_ENDPOINT: OnceLock<MStr<Endpoint>> = OnceLock::new();
46static DATA_PROCESS_ANY_ENDPOINT: OnceLock<MStr<Endpoint>> = OnceLock::new();
47static DATA_PROCESS_DATA_ENDPOINT: OnceLock<MStr<Endpoint>> = OnceLock::new();
48static DATA_RESPONSE_ENDPOINT: OnceLock<MStr<Endpoint>> = OnceLock::new();
49static DATA_RESPONSE_TOPIC: OnceLock<MStr<Topic>> = OnceLock::new();
50static TIME_EVENT_TOPIC_MSTR: OnceLock<MStr<Topic>> = OnceLock::new();
51static EXEC_QUEUE_COMMAND_ENDPOINT: OnceLock<MStr<Endpoint>> = OnceLock::new();
52static EXEC_EXECUTE_ENDPOINT: OnceLock<MStr<Endpoint>> = OnceLock::new();
53static EXEC_PROCESS_ENDPOINT: OnceLock<MStr<Endpoint>> = OnceLock::new();
54static EXEC_RECONCILE_REPORT_ENDPOINT: OnceLock<MStr<Endpoint>> = OnceLock::new();
55static RISK_EXECUTE_ENDPOINT: OnceLock<MStr<Endpoint>> = OnceLock::new();
56static RISK_QUEUE_EXECUTE_ENDPOINT: OnceLock<MStr<Endpoint>> = OnceLock::new();
57static RISK_PROCESS_ENDPOINT: OnceLock<MStr<Endpoint>> = OnceLock::new();
58static ORDER_EMULATOR_ENDPOINT: OnceLock<MStr<Endpoint>> = OnceLock::new();
59static PORTFOLIO_ACCOUNT_ENDPOINT: OnceLock<MStr<Endpoint>> = OnceLock::new();
60static PORTFOLIO_ORDER_ENDPOINT: OnceLock<MStr<Endpoint>> = OnceLock::new();
61static SHUTDOWN_SYSTEM_TOPIC: OnceLock<MStr<Topic>> = OnceLock::new();
62static RECONCILIATION_RAW_ORDER_REPORT_TOPIC: OnceLock<MStr<Topic>> = OnceLock::new();
63static RECONCILIATION_RAW_FILL_REPORT_TOPIC: OnceLock<MStr<Topic>> = OnceLock::new();
64static RECONCILIATION_RAW_POSITION_REPORT_TOPIC: OnceLock<MStr<Topic>> = OnceLock::new();
65
66#[cfg(feature = "defi")]
67static DATA_PROCESS_DEFI_DATA_ENDPOINT: OnceLock<MStr<Endpoint>> = OnceLock::new();
68
69macro_rules! define_switchboard {
70 ($(
71 $field:ident: $key_ty:ty,
72 $method:ident($($arg_name:ident: $arg_ty:ty),*) -> $key_expr:expr,
73 $val_fmt:expr,
74 $($val_args:expr),*
75 );* $(;)?) => {
76 #[derive(Clone, Debug)]
78 pub struct MessagingSwitchboard {
79 $(
80 $field: AHashMap<$key_ty, MStr<Topic>>,
81 )*
82 pipeline_topics: AHashMap<MStr<Topic>, MStr<Topic>>,
83 instruments_patterns: AHashMap<Venue, MStr<Pattern>>,
84 book_deltas_patterns: AHashMap<InstrumentId, MStr<Pattern>>,
85 book_depth10_patterns: AHashMap<InstrumentId, MStr<Pattern>>,
86 book_snapshots_patterns: AHashMap<(InstrumentId, NonZeroUsize), MStr<Pattern>>,
87 signal_topics: AHashMap<String, MStr<Topic>>,
88 signal_patterns: AHashMap<String, MStr<Pattern>>,
89 #[cfg(feature = "defi")]
90 pub(crate) defi: crate::defi::switchboard::DefiSwitchboard,
91 }
92
93 impl Default for MessagingSwitchboard {
94 fn default() -> Self {
96 Self {
97 $(
98 $field: AHashMap::new(),
99 )*
100 pipeline_topics: AHashMap::new(),
101 instruments_patterns: AHashMap::new(),
102 book_deltas_patterns: AHashMap::new(),
103 book_depth10_patterns: AHashMap::new(),
104 book_snapshots_patterns: AHashMap::new(),
105 signal_topics: AHashMap::new(),
106 signal_patterns: AHashMap::new(),
107 #[cfg(feature = "defi")]
108 defi: crate::defi::switchboard::DefiSwitchboard::default(),
109 }
110 }
111 }
112
113 impl MessagingSwitchboard {
114 #[inline]
118 #[must_use]
119 pub fn data_engine_queue_execute() -> MStr<Endpoint> {
120 *DATA_QUEUE_COMMAND_ENDPOINT.get_or_init(|| "DataEngine.queue_execute".into())
121 }
122
123 #[inline]
125 #[must_use]
126 pub fn data_engine_execute() -> MStr<Endpoint> {
127 *DATA_EXECUTE_ENDPOINT.get_or_init(|| "DataEngine.execute".into())
128 }
129
130 #[inline]
131 #[must_use]
132 pub fn data_engine_process() -> MStr<Endpoint> {
133 *DATA_PROCESS_ANY_ENDPOINT.get_or_init(|| "DataEngine.process".into())
134 }
135
136 #[inline]
137 #[must_use]
138 pub fn data_engine_process_data() -> MStr<Endpoint> {
139 *DATA_PROCESS_DATA_ENDPOINT.get_or_init(|| "DataEngine.process_data".into())
140 }
141
142 #[cfg(feature = "defi")]
143 #[inline]
144 #[must_use]
145 pub fn data_engine_process_defi_data() -> MStr<Endpoint> {
146 *DATA_PROCESS_DEFI_DATA_ENDPOINT
147 .get_or_init(|| "DataEngine.process_defi_data".into())
148 }
149
150 #[inline]
151 #[must_use]
152 pub fn data_engine_response() -> MStr<Endpoint> {
153 *DATA_RESPONSE_ENDPOINT.get_or_init(|| "DataEngine.response".into())
154 }
155
156 #[inline]
157 #[must_use]
158 pub fn data_response_topic() -> MStr<Topic> {
159 *DATA_RESPONSE_TOPIC.get_or_init(|| "data.response".into())
160 }
161
162 #[inline]
164 #[must_use]
165 pub fn time_event_topic() -> MStr<Topic> {
166 *TIME_EVENT_TOPIC_MSTR.get_or_init(|| TIME_EVENT_TOPIC.into())
167 }
168
169 #[inline]
171 #[must_use]
172 pub fn exec_engine_execute() -> MStr<Endpoint> {
173 *EXEC_EXECUTE_ENDPOINT.get_or_init(|| "ExecEngine.execute".into())
174 }
175
176 #[inline]
178 #[must_use]
179 pub fn exec_engine_queue_execute() -> MStr<Endpoint> {
180 *EXEC_QUEUE_COMMAND_ENDPOINT.get_or_init(|| "ExecEngine.queue_execute".into())
181 }
182
183 #[inline]
184 #[must_use]
185 pub fn exec_engine_process() -> MStr<Endpoint> {
186 *EXEC_PROCESS_ENDPOINT.get_or_init(|| "ExecEngine.process".into())
187 }
188
189 #[inline]
190 #[must_use]
191 pub fn exec_engine_reconcile_execution_report() -> MStr<Endpoint> {
192 *EXEC_RECONCILE_REPORT_ENDPOINT.get_or_init(|| "ExecEngine.reconcile_execution_report".into())
193 }
194
195 #[inline]
197 #[must_use]
198 pub fn risk_engine_execute() -> MStr<Endpoint> {
199 *RISK_EXECUTE_ENDPOINT.get_or_init(|| "RiskEngine.execute".into())
200 }
201
202 #[inline]
204 #[must_use]
205 pub fn risk_engine_queue_execute() -> MStr<Endpoint> {
206 *RISK_QUEUE_EXECUTE_ENDPOINT.get_or_init(|| "RiskEngine.queue_execute".into())
207 }
208
209 #[inline]
210 #[must_use]
211 pub fn risk_engine_process() -> MStr<Endpoint> {
212 *RISK_PROCESS_ENDPOINT.get_or_init(|| "RiskEngine.process".into())
213 }
214
215 #[inline]
216 #[must_use]
217 pub fn order_emulator_execute() -> MStr<Endpoint> {
218 *ORDER_EMULATOR_ENDPOINT.get_or_init(|| "OrderEmulator.execute".into())
219 }
220
221 #[inline]
222 #[must_use]
223 pub fn portfolio_update_account() -> MStr<Endpoint> {
224 *PORTFOLIO_ACCOUNT_ENDPOINT.get_or_init(|| "Portfolio.update_account".into())
225 }
226
227 #[inline]
228 #[must_use]
229 pub fn portfolio_update_order() -> MStr<Endpoint> {
230 *PORTFOLIO_ORDER_ENDPOINT.get_or_init(|| "Portfolio.update_order".into())
231 }
232
233 #[inline]
240 #[must_use]
241 pub fn shutdown_system_topic() -> MStr<Topic> {
242 *SHUTDOWN_SYSTEM_TOPIC.get_or_init(|| "commands.system.shutdown".into())
243 }
244
245 #[inline]
254 #[must_use]
255 pub fn reconciliation_raw_order_status_report_topic() -> MStr<Topic> {
256 *RECONCILIATION_RAW_ORDER_REPORT_TOPIC
257 .get_or_init(|| "reconciliation.raw.OrderStatusReport".into())
258 }
259
260 #[inline]
267 #[must_use]
268 pub fn reconciliation_raw_fill_report_topic() -> MStr<Topic> {
269 *RECONCILIATION_RAW_FILL_REPORT_TOPIC
270 .get_or_init(|| "reconciliation.raw.FillReport".into())
271 }
272
273 #[inline]
280 #[must_use]
281 pub fn reconciliation_raw_position_status_report_topic() -> MStr<Topic> {
282 *RECONCILIATION_RAW_POSITION_REPORT_TOPIC
283 .get_or_init(|| "reconciliation.raw.PositionStatusReport".into())
284 }
285
286 #[must_use]
288 pub fn instruments_pattern(&mut self, venue: Venue) -> MStr<Pattern> {
289 *self.instruments_patterns
290 .entry(venue)
291 .or_insert_with(|| format!("data.instrument.{venue}.*").into())
292 }
293
294 #[must_use]
301 pub fn signal_topic(&mut self, name: &str) -> MStr<Topic> {
302 *self
303 .signal_topics
304 .entry(name.to_string())
305 .or_insert_with(|| {
306 format!(
307 "data.Signal{}",
308 nautilus_core::string::conversions::title_case(name)
309 )
310 .into()
311 })
312 }
313
314 #[must_use]
320 pub fn signal_pattern(&mut self, name: &str) -> MStr<Pattern> {
321 *self
322 .signal_patterns
323 .entry(name.to_string())
324 .or_insert_with(|| {
325 format!(
326 "data.Signal{}*",
327 nautilus_core::string::conversions::title_case(name)
328 )
329 .into()
330 })
331 }
332
333 $(
335 #[must_use]
336 pub fn $method(&mut self, $($arg_name: $arg_ty),*) -> MStr<Topic> {
337 let key = $key_expr;
338 *self.$field
339 .entry(key)
340 .or_insert_with(|| format!($val_fmt, $($val_args),*).into())
341 }
342 )*
343 }
344 };
345}
346
347define_switchboard! {
348 custom_topics: DataType,
349 get_custom_topic(data_type: &DataType) -> data_type.clone(),
350 "data.{}", data_type.topic();
351
352 instruments_topics: Venue,
353 get_instruments_topic(venue: Venue) -> venue,
354 "data.instrument.{}", venue;
355
356 instrument_topics: InstrumentId,
357 get_instrument_topic(instrument_id: InstrumentId) -> instrument_id,
358 "data.instrument.{}.{}", instrument_id.venue, instrument_id.symbol;
359
360 book_deltas_topics: InstrumentId,
361 get_book_deltas_topic(instrument_id: InstrumentId) -> instrument_id,
362 "data.book.deltas.{}.{}", instrument_id.venue, instrument_id.symbol;
363
364 book_depth10_topics: InstrumentId,
365 get_book_depth10_topic(instrument_id: InstrumentId) -> instrument_id,
366 "data.book.depth10.{}.{}", instrument_id.venue, instrument_id.symbol;
367
368 book_snapshots_topics: (InstrumentId, NonZeroUsize),
369 get_book_snapshots_topic(instrument_id: InstrumentId, interval_ms: NonZeroUsize) -> (instrument_id, interval_ms),
370 "data.book.snapshots.{}.{}.{}", instrument_id.venue, instrument_id.symbol, interval_ms;
371
372 quote_topics: InstrumentId,
373 get_quotes_topic(instrument_id: InstrumentId) -> instrument_id,
374 "data.quotes.{}.{}", instrument_id.venue, instrument_id.symbol;
375
376 trade_topics: InstrumentId,
377 get_trades_topic(instrument_id: InstrumentId) -> instrument_id,
378 "data.trades.{}.{}", instrument_id.venue, instrument_id.symbol;
379
380 bar_topics: BarType,
381 get_bars_topic(bar_type: BarType) -> bar_type,
382 "data.bars.{}", bar_type;
383
384 mark_price_topics: InstrumentId,
385 get_mark_price_topic(instrument_id: InstrumentId) -> instrument_id,
386 "data.mark_prices.{}.{}", instrument_id.venue, instrument_id.symbol;
387
388 index_price_topics: InstrumentId,
389 get_index_price_topic(instrument_id: InstrumentId) -> instrument_id,
390 "data.index_prices.{}.{}", instrument_id.venue, instrument_id.symbol;
391
392 funding_rate_topics: InstrumentId,
393 get_funding_rate_topic(instrument_id: InstrumentId) -> instrument_id,
394 "data.funding_rates.{}.{}", instrument_id.venue, instrument_id.symbol;
395
396 funding_settlement_topics: InstrumentId,
397 get_funding_settlement_topic(instrument_id: InstrumentId) -> instrument_id,
398 "events.funding_settlements.{}.{}", instrument_id.venue, instrument_id.symbol;
399
400 instrument_status_topics: InstrumentId,
401 get_instrument_status_topic(instrument_id: InstrumentId) -> instrument_id,
402 "data.status.{}.{}", instrument_id.venue, instrument_id.symbol;
403
404 instrument_close_topics: InstrumentId,
405 get_instrument_close_topic(instrument_id: InstrumentId) -> instrument_id,
406 "data.close.{}.{}", instrument_id.venue, instrument_id.symbol;
407
408 option_greeks_topics: InstrumentId,
409 get_option_greeks_topic(instrument_id: InstrumentId) -> instrument_id,
410 "data.option_greeks.{}.{}", instrument_id.venue, instrument_id.symbol;
411
412 option_chain_topics: OptionSeriesId,
413 get_option_chain_topic(series_id: OptionSeriesId) -> series_id,
414 "data.option_chain.{}", series_id;
415
416 order_submitted_topics: InstrumentId,
417 get_order_submitted_topic(instrument_id: InstrumentId) -> instrument_id,
418 "events.order_submitted.{}", instrument_id;
419
420 order_rejected_topics: InstrumentId,
421 get_order_rejected_topic(instrument_id: InstrumentId) -> instrument_id,
422 "events.order_rejected.{}", instrument_id;
423
424 order_pending_update_topics: InstrumentId,
425 get_order_pending_update_topic(instrument_id: InstrumentId) -> instrument_id,
426 "events.order_pending_update.{}", instrument_id;
427
428 order_pending_cancel_topics: InstrumentId,
429 get_order_pending_cancel_topic(instrument_id: InstrumentId) -> instrument_id,
430 "events.order_pending_cancel.{}", instrument_id;
431
432 order_modify_rejected_topics: InstrumentId,
433 get_order_modify_rejected_topic(instrument_id: InstrumentId) -> instrument_id,
434 "events.order_modify_rejected.{}", instrument_id;
435
436 order_cancel_rejected_topics: InstrumentId,
437 get_order_cancel_rejected_topic(instrument_id: InstrumentId) -> instrument_id,
438 "events.order_cancel_rejected.{}", instrument_id;
439
440 order_canceled_topics: InstrumentId,
441 get_order_canceled_topic(instrument_id: InstrumentId) -> instrument_id,
442 "events.order_canceled.{}", instrument_id;
443
444 order_filled_topics: InstrumentId,
445 get_order_filled_topic(instrument_id: InstrumentId) -> instrument_id,
446 "events.order_filled.{}", instrument_id;
447
448 event_order_topics: StrategyId,
449 get_event_order_topic(strategy_id: StrategyId) -> strategy_id,
450 "events.order.{}", strategy_id;
451
452 event_position_topics: StrategyId,
453 get_event_position_topic(strategy_id: StrategyId) -> strategy_id,
454 "events.position.{}", strategy_id;
455
456 snapshot_order_topics: ClientOrderId,
457 get_snapshot_order_topic(client_order_id: ClientOrderId) -> client_order_id,
458 "snapshots.order.{}", client_order_id;
459
460 snapshot_position_topics: PositionId,
461 get_snapshot_position_topic(position_id: PositionId) -> position_id,
462 "snapshots.position.{}", position_id;
463
464}
465
466impl MessagingSwitchboard {
467 #[inline]
468 fn pipeline_topic(&mut self, live: MStr<Topic>) -> MStr<Topic> {
469 *self.pipeline_topics.entry(live).or_insert_with(|| {
470 let live = live.as_ref();
471 let suffix = live
472 .strip_prefix("data.")
473 .expect("live data topic must start with data.");
474 MStr::<Topic>::from(format!("data.pipeline.{suffix}"))
475 })
476 }
477
478 #[must_use]
479 pub fn get_pipeline_custom_topic(&mut self, data_type: &DataType) -> MStr<Topic> {
480 let live = self.get_custom_topic(data_type);
481 self.pipeline_topic(live)
482 }
483
484 #[must_use]
485 pub fn get_pipeline_book_deltas_topic(&mut self, instrument_id: InstrumentId) -> MStr<Topic> {
486 let live = self.get_book_deltas_topic(instrument_id);
487 self.pipeline_topic(live)
488 }
489
490 #[must_use]
491 pub fn get_pipeline_book_depth10_topic(&mut self, instrument_id: InstrumentId) -> MStr<Topic> {
492 let live = self.get_book_depth10_topic(instrument_id);
493 self.pipeline_topic(live)
494 }
495
496 #[must_use]
497 pub fn get_pipeline_quotes_topic(&mut self, instrument_id: InstrumentId) -> MStr<Topic> {
498 let live = self.get_quotes_topic(instrument_id);
499 self.pipeline_topic(live)
500 }
501
502 #[must_use]
503 pub fn get_pipeline_trades_topic(&mut self, instrument_id: InstrumentId) -> MStr<Topic> {
504 let live = self.get_trades_topic(instrument_id);
505 self.pipeline_topic(live)
506 }
507
508 #[must_use]
509 pub fn get_pipeline_bars_topic(&mut self, bar_type: BarType) -> MStr<Topic> {
510 let live = self.get_bars_topic(bar_type);
511 self.pipeline_topic(live)
512 }
513
514 #[must_use]
515 pub fn get_pipeline_mark_price_topic(&mut self, instrument_id: InstrumentId) -> MStr<Topic> {
516 let live = self.get_mark_price_topic(instrument_id);
517 self.pipeline_topic(live)
518 }
519
520 #[must_use]
521 pub fn get_pipeline_index_price_topic(&mut self, instrument_id: InstrumentId) -> MStr<Topic> {
522 let live = self.get_index_price_topic(instrument_id);
523 self.pipeline_topic(live)
524 }
525
526 #[must_use]
527 pub fn get_pipeline_funding_rate_topic(&mut self, instrument_id: InstrumentId) -> MStr<Topic> {
528 let live = self.get_funding_rate_topic(instrument_id);
529 self.pipeline_topic(live)
530 }
531
532 #[must_use]
533 pub fn get_pipeline_instrument_status_topic(
534 &mut self,
535 instrument_id: InstrumentId,
536 ) -> MStr<Topic> {
537 let live = self.get_instrument_status_topic(instrument_id);
538 self.pipeline_topic(live)
539 }
540
541 #[must_use]
542 pub fn get_pipeline_option_greeks_topic(&mut self, instrument_id: InstrumentId) -> MStr<Topic> {
543 let live = self.get_option_greeks_topic(instrument_id);
544 self.pipeline_topic(live)
545 }
546
547 #[must_use]
548 pub fn get_pipeline_instrument_close_topic(
549 &mut self,
550 instrument_id: InstrumentId,
551 ) -> MStr<Topic> {
552 let live = self.get_instrument_close_topic(instrument_id);
553 self.pipeline_topic(live)
554 }
555
556 #[must_use]
558 pub fn get_book_deltas_pattern(&mut self, instrument_id: InstrumentId) -> MStr<Pattern> {
559 *self
560 .book_deltas_patterns
561 .entry(instrument_id)
562 .or_insert_with(|| {
563 format!(
564 "data.book.deltas.{}.{}",
565 instrument_id.venue,
566 instrument_id.symbol.topic(),
567 )
568 .into()
569 })
570 }
571
572 #[must_use]
574 pub fn get_book_depth10_pattern(&mut self, instrument_id: InstrumentId) -> MStr<Pattern> {
575 *self
576 .book_depth10_patterns
577 .entry(instrument_id)
578 .or_insert_with(|| {
579 format!(
580 "data.book.depth10.{}.{}",
581 instrument_id.venue,
582 instrument_id.symbol.topic(),
583 )
584 .into()
585 })
586 }
587
588 #[must_use]
590 pub fn get_book_snapshots_pattern(
591 &mut self,
592 instrument_id: InstrumentId,
593 interval_ms: NonZeroUsize,
594 ) -> MStr<Pattern> {
595 *self
596 .book_snapshots_patterns
597 .entry((instrument_id, interval_ms))
598 .or_insert_with(|| {
599 format!(
600 "data.book.snapshots.{}.{}.{}",
601 instrument_id.venue,
602 instrument_id.symbol.topic(),
603 interval_ms,
604 )
605 .into()
606 })
607 }
608}
609
610macro_rules! define_wrappers {
611 ($($method:ident($($arg_name:ident: $arg_ty:ty),*) -> $ret:ty),* $(,)?) => {
612 $(
613 #[must_use]
614 pub fn $method($($arg_name: $arg_ty),*) -> $ret {
615 get_message_bus()
616 .borrow_mut()
617 .switchboard
618 .$method($($arg_name),*)
619 }
620 )*
621 }
622}
623
624define_wrappers! {
625 get_custom_topic(data_type: &DataType) -> MStr<Topic>,
626 get_instruments_topic(venue: Venue) -> MStr<Topic>,
627 get_instrument_topic(instrument_id: InstrumentId) -> MStr<Topic>,
628 get_book_deltas_topic(instrument_id: InstrumentId) -> MStr<Topic>,
629 get_book_depth10_topic(instrument_id: InstrumentId) -> MStr<Topic>,
630 get_book_snapshots_topic(instrument_id: InstrumentId, interval_ms: NonZeroUsize) -> MStr<Topic>,
631 get_quotes_topic(instrument_id: InstrumentId) -> MStr<Topic>,
632 get_trades_topic(instrument_id: InstrumentId) -> MStr<Topic>,
633 get_bars_topic(bar_type: BarType) -> MStr<Topic>,
634 get_mark_price_topic(instrument_id: InstrumentId) -> MStr<Topic>,
635 get_index_price_topic(instrument_id: InstrumentId) -> MStr<Topic>,
636 get_funding_rate_topic(instrument_id: InstrumentId) -> MStr<Topic>,
637 get_funding_settlement_topic(instrument_id: InstrumentId) -> MStr<Topic>,
638 get_instrument_status_topic(instrument_id: InstrumentId) -> MStr<Topic>,
639 get_instrument_close_topic(instrument_id: InstrumentId) -> MStr<Topic>,
640 get_option_greeks_topic(instrument_id: InstrumentId) -> MStr<Topic>,
641 get_option_chain_topic(series_id: OptionSeriesId) -> MStr<Topic>,
642 get_pipeline_custom_topic(data_type: &DataType) -> MStr<Topic>,
643 get_pipeline_book_deltas_topic(instrument_id: InstrumentId) -> MStr<Topic>,
644 get_pipeline_book_depth10_topic(instrument_id: InstrumentId) -> MStr<Topic>,
645 get_pipeline_quotes_topic(instrument_id: InstrumentId) -> MStr<Topic>,
646 get_pipeline_trades_topic(instrument_id: InstrumentId) -> MStr<Topic>,
647 get_pipeline_bars_topic(bar_type: BarType) -> MStr<Topic>,
648 get_pipeline_mark_price_topic(instrument_id: InstrumentId) -> MStr<Topic>,
649 get_pipeline_index_price_topic(instrument_id: InstrumentId) -> MStr<Topic>,
650 get_pipeline_funding_rate_topic(instrument_id: InstrumentId) -> MStr<Topic>,
651 get_pipeline_instrument_status_topic(instrument_id: InstrumentId) -> MStr<Topic>,
652 get_pipeline_option_greeks_topic(instrument_id: InstrumentId) -> MStr<Topic>,
653 get_pipeline_instrument_close_topic(instrument_id: InstrumentId) -> MStr<Topic>,
654 get_order_submitted_topic(instrument_id: InstrumentId) -> MStr<Topic>,
655 get_order_rejected_topic(instrument_id: InstrumentId) -> MStr<Topic>,
656 get_order_pending_update_topic(instrument_id: InstrumentId) -> MStr<Topic>,
657 get_order_pending_cancel_topic(instrument_id: InstrumentId) -> MStr<Topic>,
658 get_order_modify_rejected_topic(instrument_id: InstrumentId) -> MStr<Topic>,
659 get_order_cancel_rejected_topic(instrument_id: InstrumentId) -> MStr<Topic>,
660 get_order_canceled_topic(instrument_id: InstrumentId) -> MStr<Topic>,
661 get_order_filled_topic(instrument_id: InstrumentId) -> MStr<Topic>,
662 get_snapshot_order_topic(client_order_id: ClientOrderId) -> MStr<Topic>,
663 get_snapshot_position_topic(position_id: PositionId) -> MStr<Topic>,
664 get_event_order_topic(strategy_id: StrategyId) -> MStr<Topic>,
665 get_event_position_topic(strategy_id: StrategyId) -> MStr<Topic>,
666}
667
668#[must_use]
674pub fn get_instruments_pattern(venue: Venue) -> MStr<Pattern> {
675 get_message_bus()
676 .borrow_mut()
677 .switchboard
678 .instruments_pattern(venue)
679}
680
681#[must_use]
683pub fn get_book_deltas_pattern(instrument_id: InstrumentId) -> MStr<Pattern> {
684 get_message_bus()
685 .borrow_mut()
686 .switchboard
687 .get_book_deltas_pattern(instrument_id)
688}
689
690#[must_use]
692pub fn get_book_depth10_pattern(instrument_id: InstrumentId) -> MStr<Pattern> {
693 get_message_bus()
694 .borrow_mut()
695 .switchboard
696 .get_book_depth10_pattern(instrument_id)
697}
698
699#[must_use]
701pub fn get_book_snapshots_pattern(
702 instrument_id: InstrumentId,
703 interval_ms: NonZeroUsize,
704) -> MStr<Pattern> {
705 get_message_bus()
706 .borrow_mut()
707 .switchboard
708 .get_book_snapshots_pattern(instrument_id, interval_ms)
709}
710
711#[must_use]
713pub fn get_signal_topic(name: &str) -> MStr<Topic> {
714 get_message_bus()
715 .borrow_mut()
716 .switchboard
717 .signal_topic(name)
718}
719
720#[must_use]
724pub fn get_signal_pattern(name: &str) -> MStr<Pattern> {
725 get_message_bus()
726 .borrow_mut()
727 .switchboard
728 .signal_pattern(name)
729}
730
731#[cfg(test)]
732mod tests {
733 use nautilus_model::{
734 data::{BarType, DataType},
735 identifiers::{InstrumentId, Venue},
736 };
737 use rstest::*;
738
739 use super::*;
740 use crate::msgbus::matching::is_matching_backtracking;
741
742 #[fixture]
743 fn switchboard() -> MessagingSwitchboard {
744 MessagingSwitchboard::default()
745 }
746
747 #[fixture]
748 fn instrument_id() -> InstrumentId {
749 InstrumentId::from("ESZ24.XCME")
750 }
751
752 #[rstest]
753 fn test_data_response_topic() {
754 let expected_topic = "data.response".into();
755 let result = MessagingSwitchboard::data_response_topic();
756 assert_eq!(result, expected_topic);
757 }
758
759 #[rstest]
760 fn test_time_event_topic() {
761 let expected_topic = "clock.time_event".into();
762 let result = MessagingSwitchboard::time_event_topic();
763 assert_eq!(result, expected_topic);
764 }
765
766 #[rstest]
767 fn test_reconciliation_raw_order_status_report_topic() {
768 let expected_topic = "reconciliation.raw.OrderStatusReport".into();
769 let result = MessagingSwitchboard::reconciliation_raw_order_status_report_topic();
770 assert_eq!(result, expected_topic);
771 }
772
773 #[rstest]
774 fn test_reconciliation_raw_fill_report_topic() {
775 let expected_topic = "reconciliation.raw.FillReport".into();
776 let result = MessagingSwitchboard::reconciliation_raw_fill_report_topic();
777 assert_eq!(result, expected_topic);
778 }
779
780 #[rstest]
781 fn test_reconciliation_raw_position_status_report_topic() {
782 let expected_topic = "reconciliation.raw.PositionStatusReport".into();
783 let result = MessagingSwitchboard::reconciliation_raw_position_status_report_topic();
784 assert_eq!(result, expected_topic);
785 }
786
787 #[rstest]
788 fn test_get_custom_topic(mut switchboard: MessagingSwitchboard) {
789 let data_type = DataType::new("ExampleDataType", None, None);
790 let expected_topic = "data.ExampleDataType".into();
791 let result = switchboard.get_custom_topic(&data_type);
792 assert_eq!(result, expected_topic);
793 assert!(switchboard.custom_topics.contains_key(&data_type));
794 }
795
796 #[rstest]
797 fn test_get_instrument_topic(
798 mut switchboard: MessagingSwitchboard,
799 instrument_id: InstrumentId,
800 ) {
801 let expected_topic = "data.instrument.XCME.ESZ24".into();
802 let result = switchboard.get_instrument_topic(instrument_id);
803 assert_eq!(result, expected_topic);
804 assert!(switchboard.instrument_topics.contains_key(&instrument_id));
805 }
806
807 #[rstest]
808 fn test_get_book_deltas_topic(
809 mut switchboard: MessagingSwitchboard,
810 instrument_id: InstrumentId,
811 ) {
812 let expected_topic = "data.book.deltas.XCME.ESZ24".into();
813 let result = switchboard.get_book_deltas_topic(instrument_id);
814 assert_eq!(result, expected_topic);
815 assert!(switchboard.book_deltas_topics.contains_key(&instrument_id));
816 }
817
818 #[rstest]
819 fn test_get_book_depth10_topic(
820 mut switchboard: MessagingSwitchboard,
821 instrument_id: InstrumentId,
822 ) {
823 let expected_topic = "data.book.depth10.XCME.ESZ24".into();
824 let result = switchboard.get_book_depth10_topic(instrument_id);
825 assert_eq!(result, expected_topic);
826 assert!(switchboard.book_depth10_topics.contains_key(&instrument_id));
827 }
828
829 #[rstest]
830 fn test_get_book_snapshots_topic(
831 mut switchboard: MessagingSwitchboard,
832 instrument_id: InstrumentId,
833 ) {
834 let expected_topic = "data.book.snapshots.XCME.ESZ24.1000".into();
835 let interval_ms = NonZeroUsize::new(1000).unwrap();
836 let result = switchboard.get_book_snapshots_topic(instrument_id, interval_ms);
837 assert_eq!(result, expected_topic);
838
839 assert!(
840 switchboard
841 .book_snapshots_topics
842 .contains_key(&(instrument_id, interval_ms))
843 );
844 }
845
846 #[rstest]
847 fn test_get_quotes_topic(mut switchboard: MessagingSwitchboard, instrument_id: InstrumentId) {
848 let expected_topic = "data.quotes.XCME.ESZ24".into();
849 let result = switchboard.get_quotes_topic(instrument_id);
850 assert_eq!(result, expected_topic);
851 assert!(switchboard.quote_topics.contains_key(&instrument_id));
852 }
853
854 #[rstest]
855 fn test_get_trades_topic(mut switchboard: MessagingSwitchboard, instrument_id: InstrumentId) {
856 let expected_topic = "data.trades.XCME.ESZ24".into();
857 let result = switchboard.get_trades_topic(instrument_id);
858 assert_eq!(result, expected_topic);
859 assert!(switchboard.trade_topics.contains_key(&instrument_id));
860 }
861
862 #[rstest]
863 fn test_get_bars_topic(mut switchboard: MessagingSwitchboard) {
864 let bar_type = BarType::from("ESZ24.XCME-1-MINUTE-LAST-INTERNAL");
865 let expected_topic = format!("data.bars.{bar_type}").into();
866 let result = switchboard.get_bars_topic(bar_type);
867 assert_eq!(result, expected_topic);
868 assert!(switchboard.bar_topics.contains_key(&bar_type));
869 }
870
871 #[rstest]
872 fn test_get_pipeline_custom_topic(mut switchboard: MessagingSwitchboard) {
873 let data_type = DataType::new("ExampleDataType", None, None);
874 let expected_topic = "data.pipeline.ExampleDataType".into();
875 let result = switchboard.get_pipeline_custom_topic(&data_type);
876 assert_eq!(result, expected_topic);
877 assert!(switchboard.custom_topics.contains_key(&data_type));
878 assert_eq!(switchboard.pipeline_topics.len(), 1);
879 }
880
881 type PipelineInstrumentIdTopicFn = fn(&mut MessagingSwitchboard, InstrumentId) -> MStr<Topic>;
882
883 #[rstest]
884 #[case::book_deltas(
885 MessagingSwitchboard::get_pipeline_book_deltas_topic as PipelineInstrumentIdTopicFn,
886 "data.pipeline.book.deltas.XCME.ESZ24",
887 )]
888 #[case::book_depth10(
889 MessagingSwitchboard::get_pipeline_book_depth10_topic as PipelineInstrumentIdTopicFn,
890 "data.pipeline.book.depth10.XCME.ESZ24",
891 )]
892 #[case::quotes(
893 MessagingSwitchboard::get_pipeline_quotes_topic as PipelineInstrumentIdTopicFn,
894 "data.pipeline.quotes.XCME.ESZ24",
895 )]
896 #[case::trades(
897 MessagingSwitchboard::get_pipeline_trades_topic as PipelineInstrumentIdTopicFn,
898 "data.pipeline.trades.XCME.ESZ24",
899 )]
900 #[case::mark_prices(
901 MessagingSwitchboard::get_pipeline_mark_price_topic as PipelineInstrumentIdTopicFn,
902 "data.pipeline.mark_prices.XCME.ESZ24",
903 )]
904 #[case::index_prices(
905 MessagingSwitchboard::get_pipeline_index_price_topic as PipelineInstrumentIdTopicFn,
906 "data.pipeline.index_prices.XCME.ESZ24",
907 )]
908 #[case::funding_rates(
909 MessagingSwitchboard::get_pipeline_funding_rate_topic as PipelineInstrumentIdTopicFn,
910 "data.pipeline.funding_rates.XCME.ESZ24",
911 )]
912 #[case::status(
913 MessagingSwitchboard::get_pipeline_instrument_status_topic as PipelineInstrumentIdTopicFn,
914 "data.pipeline.status.XCME.ESZ24",
915 )]
916 #[case::close(
917 MessagingSwitchboard::get_pipeline_instrument_close_topic as PipelineInstrumentIdTopicFn,
918 "data.pipeline.close.XCME.ESZ24",
919 )]
920 fn test_get_pipeline_instrument_id_topic(
921 mut switchboard: MessagingSwitchboard,
922 instrument_id: InstrumentId,
923 #[case] topic_fn: PipelineInstrumentIdTopicFn,
924 #[case] expected: &str,
925 ) {
926 let result = topic_fn(&mut switchboard, instrument_id);
927 assert_eq!(result.as_ref(), expected);
928 assert_eq!(switchboard.pipeline_topics.len(), 1);
929 }
930
931 #[rstest]
932 fn test_get_pipeline_bars_topic(mut switchboard: MessagingSwitchboard) {
933 let bar_type = BarType::from("ESZ24.XCME-1-MINUTE-LAST-INTERNAL");
934 let expected_topic = format!("data.pipeline.bars.{bar_type}").into();
935 let result = switchboard.get_pipeline_bars_topic(bar_type);
936 assert_eq!(result, expected_topic);
937 assert!(switchboard.bar_topics.contains_key(&bar_type));
938 assert_eq!(switchboard.pipeline_topics.len(), 1);
939 }
940
941 type OrderEventTopicFn = fn(&mut MessagingSwitchboard, InstrumentId) -> MStr<Topic>;
942
943 #[rstest]
944 #[case::submitted(
945 MessagingSwitchboard::get_order_submitted_topic as OrderEventTopicFn,
946 "events.order_submitted.ESZ24.XCME",
947 )]
948 #[case::rejected(
949 MessagingSwitchboard::get_order_rejected_topic as OrderEventTopicFn,
950 "events.order_rejected.ESZ24.XCME",
951 )]
952 #[case::pending_update(
953 MessagingSwitchboard::get_order_pending_update_topic as OrderEventTopicFn,
954 "events.order_pending_update.ESZ24.XCME",
955 )]
956 #[case::pending_cancel(
957 MessagingSwitchboard::get_order_pending_cancel_topic as OrderEventTopicFn,
958 "events.order_pending_cancel.ESZ24.XCME",
959 )]
960 #[case::modify_rejected(
961 MessagingSwitchboard::get_order_modify_rejected_topic as OrderEventTopicFn,
962 "events.order_modify_rejected.ESZ24.XCME",
963 )]
964 #[case::cancel_rejected(
965 MessagingSwitchboard::get_order_cancel_rejected_topic as OrderEventTopicFn,
966 "events.order_cancel_rejected.ESZ24.XCME",
967 )]
968 #[case::canceled(
969 MessagingSwitchboard::get_order_canceled_topic as OrderEventTopicFn,
970 "events.order_canceled.ESZ24.XCME",
971 )]
972 #[case::filled(
973 MessagingSwitchboard::get_order_filled_topic as OrderEventTopicFn,
974 "events.order_filled.ESZ24.XCME",
975 )]
976 fn test_get_order_event_topic(
977 mut switchboard: MessagingSwitchboard,
978 instrument_id: InstrumentId,
979 #[case] topic_fn: OrderEventTopicFn,
980 #[case] expected: &str,
981 ) {
982 let result = topic_fn(&mut switchboard, instrument_id);
983 assert_eq!(result.as_ref(), expected);
984 }
985
986 #[rstest]
987 #[case::submitted(MessagingSwitchboard::get_order_submitted_topic as OrderEventTopicFn)]
988 #[case::rejected(MessagingSwitchboard::get_order_rejected_topic as OrderEventTopicFn)]
989 #[case::pending_update(MessagingSwitchboard::get_order_pending_update_topic as OrderEventTopicFn)]
990 #[case::pending_cancel(MessagingSwitchboard::get_order_pending_cancel_topic as OrderEventTopicFn)]
991 #[case::modify_rejected(MessagingSwitchboard::get_order_modify_rejected_topic as OrderEventTopicFn)]
992 #[case::cancel_rejected(MessagingSwitchboard::get_order_cancel_rejected_topic as OrderEventTopicFn)]
993 #[case::canceled(MessagingSwitchboard::get_order_canceled_topic as OrderEventTopicFn)]
994 #[case::filled(MessagingSwitchboard::get_order_filled_topic as OrderEventTopicFn)]
995 fn test_order_event_topic_does_not_match_strategy_order_pattern(
996 mut switchboard: MessagingSwitchboard,
997 instrument_id: InstrumentId,
998 #[case] topic_fn: OrderEventTopicFn,
999 ) {
1000 let topic = topic_fn(&mut switchboard, instrument_id);
1001 assert!(!is_matching_backtracking(topic, "events.order.*".into()));
1002 }
1003
1004 #[rstest]
1005 fn test_get_snapshot_order_topic(mut switchboard: MessagingSwitchboard) {
1006 let client_order_id = ClientOrderId::from("O-123456789");
1007 let expected_topic = format!("snapshots.order.{client_order_id}").into();
1008 let result = switchboard.get_snapshot_order_topic(client_order_id);
1009 assert_eq!(result, expected_topic);
1010 assert!(
1011 switchboard
1012 .snapshot_order_topics
1013 .contains_key(&client_order_id)
1014 );
1015 }
1016
1017 #[rstest]
1018 fn test_get_snapshot_position_topic(mut switchboard: MessagingSwitchboard) {
1019 let position_id = PositionId::from("P-123456789");
1020 let expected_topic = format!("snapshots.position.{position_id}").into();
1021 let result = switchboard.get_snapshot_position_topic(position_id);
1022 assert_eq!(result, expected_topic);
1023 assert!(
1024 switchboard
1025 .snapshot_position_topics
1026 .contains_key(&position_id)
1027 );
1028 }
1029
1030 #[rstest]
1031 fn test_instruments_pattern_matches_instrument_topic(
1032 mut switchboard: MessagingSwitchboard,
1033 instrument_id: InstrumentId,
1034 ) {
1035 let venue = instrument_id.venue;
1036 let pattern = switchboard.instruments_pattern(venue);
1037 let topic = switchboard.get_instrument_topic(instrument_id);
1038
1039 assert_eq!(pattern.as_ref(), "data.instrument.XCME.*");
1040 assert!(is_matching_backtracking(topic, pattern));
1041 }
1042
1043 #[rstest]
1044 fn test_instruments_pattern_does_not_match_other_venue(mut switchboard: MessagingSwitchboard) {
1045 let pattern = switchboard.instruments_pattern(Venue::from("BINANCE"));
1046 let topic = switchboard.get_instrument_topic(InstrumentId::from("ESZ24.XCME"));
1047
1048 assert!(!is_matching_backtracking(topic, pattern));
1049 }
1050
1051 #[rstest]
1052 fn test_composite_book_deltas_pattern_uses_wildcard(mut switchboard: MessagingSwitchboard) {
1053 let composite_id = InstrumentId::from("ES.FUT.XCME");
1054 let underlying_id = InstrumentId::from("ESZ24.XCME");
1055
1056 let composite_pattern = switchboard.get_book_deltas_pattern(composite_id);
1057 let underlying_topic = switchboard.get_book_deltas_topic(underlying_id);
1058
1059 assert_eq!(composite_pattern.as_ref(), "data.book.deltas.XCME.ES*");
1060 assert_eq!(underlying_topic.as_ref(), "data.book.deltas.XCME.ESZ24");
1061 assert!(is_matching_backtracking(
1062 underlying_topic,
1063 composite_pattern
1064 ));
1065 }
1066
1067 #[rstest]
1068 fn test_book_deltas_pattern_for_non_composite_is_literal(
1069 mut switchboard: MessagingSwitchboard,
1070 instrument_id: InstrumentId,
1071 ) {
1072 let pattern = switchboard.get_book_deltas_pattern(instrument_id);
1073 assert_eq!(pattern.as_ref(), "data.book.deltas.XCME.ESZ24");
1074 }
1075
1076 type PatternFn = fn(&mut MessagingSwitchboard, InstrumentId) -> MStr<Pattern>;
1077
1078 #[rstest]
1079 #[case::book_depth10(
1080 MessagingSwitchboard::get_book_depth10_pattern as PatternFn,
1081 "data.book.depth10.XCME.ESZ24",
1082 )]
1083 fn test_pattern_for_non_composite_is_literal(
1084 mut switchboard: MessagingSwitchboard,
1085 instrument_id: InstrumentId,
1086 #[case] helper: PatternFn,
1087 #[case] expected: &str,
1088 ) {
1089 let pattern = helper(&mut switchboard, instrument_id);
1090 assert_eq!(pattern.as_ref(), expected);
1091 }
1092
1093 #[rstest]
1094 fn test_book_snapshots_pattern_for_non_composite_is_literal(
1095 mut switchboard: MessagingSwitchboard,
1096 instrument_id: InstrumentId,
1097 ) {
1098 let interval_ms = NonZeroUsize::new(1000).unwrap();
1099 let pattern = switchboard.get_book_snapshots_pattern(instrument_id, interval_ms);
1100 assert_eq!(pattern.as_ref(), "data.book.snapshots.XCME.ESZ24.1000");
1101 }
1102
1103 #[rstest]
1104 #[case::book_deltas(MessagingSwitchboard::get_book_deltas_pattern as PatternFn)]
1105 #[case::book_depth10(MessagingSwitchboard::get_book_depth10_pattern as PatternFn)]
1106 fn test_pattern_helper_is_idempotent(
1107 mut switchboard: MessagingSwitchboard,
1108 instrument_id: InstrumentId,
1109 #[case] helper: PatternFn,
1110 ) {
1111 let first = helper(&mut switchboard, instrument_id);
1112 let second = helper(&mut switchboard, instrument_id);
1113 assert_eq!(first, second);
1114 }
1115
1116 #[rstest]
1117 fn test_book_snapshots_pattern_helper_is_idempotent(
1118 mut switchboard: MessagingSwitchboard,
1119 instrument_id: InstrumentId,
1120 ) {
1121 let interval_ms = NonZeroUsize::new(1000).unwrap();
1122 let first = switchboard.get_book_snapshots_pattern(instrument_id, interval_ms);
1123 let second = switchboard.get_book_snapshots_pattern(instrument_id, interval_ms);
1124 assert_eq!(first, second);
1125 }
1126
1127 #[rstest]
1128 fn test_composite_book_depth10_pattern_uses_wildcard(mut switchboard: MessagingSwitchboard) {
1129 let composite_id = InstrumentId::from("ES.FUT.XCME");
1130 let underlying_id = InstrumentId::from("ESZ24.XCME");
1131
1132 let composite_pattern = switchboard.get_book_depth10_pattern(composite_id);
1133 let underlying_topic = switchboard.get_book_depth10_topic(underlying_id);
1134
1135 assert_eq!(composite_pattern.as_ref(), "data.book.depth10.XCME.ES*");
1136 assert!(is_matching_backtracking(
1137 underlying_topic,
1138 composite_pattern
1139 ));
1140 }
1141
1142 #[rstest]
1143 fn test_composite_book_snapshots_pattern_uses_wildcard(mut switchboard: MessagingSwitchboard) {
1144 let composite_id = InstrumentId::from("ES.FUT.XCME");
1145 let underlying_id = InstrumentId::from("ESZ24.XCME");
1146 let interval_ms = NonZeroUsize::new(1000).unwrap();
1147
1148 let composite_pattern = switchboard.get_book_snapshots_pattern(composite_id, interval_ms);
1149 let underlying_topic = switchboard.get_book_snapshots_topic(underlying_id, interval_ms);
1150
1151 assert_eq!(
1152 composite_pattern.as_ref(),
1153 "data.book.snapshots.XCME.ES*.1000"
1154 );
1155 assert_eq!(
1156 underlying_topic.as_ref(),
1157 "data.book.snapshots.XCME.ESZ24.1000"
1158 );
1159 assert!(is_matching_backtracking(
1160 underlying_topic,
1161 composite_pattern
1162 ));
1163 }
1164}