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