1use std::{fmt::Write, num::NonZeroUsize, sync::OnceLock};
31
32use ahash::AHashMap;
33use nautilus_model::{
34 data::{BarType, DataType, data_type::IDENTIFIER_TOPIC_SUFFIX},
35 identifiers::{
36 ClientId, ClientOrderId, InstrumentId, OptionSeriesId, PositionId, StrategyId, Venue,
37 },
38};
39
40use super::mstr::{Endpoint, MStr, Pattern, Topic};
41use crate::{msgbus::get_message_bus, runner::SystemChannel};
42
43pub const CLOSE_TOPIC: &str = "CLOSE";
44pub const TIME_EVENT_TOPIC: &str = "clock.time_event";
45
46static DATA_QUEUE_COMMAND_ENDPOINT: OnceLock<MStr<Endpoint>> = OnceLock::new();
47static DATA_EXECUTE_ENDPOINT: OnceLock<MStr<Endpoint>> = OnceLock::new();
48static DATA_PROCESS_ANY_ENDPOINT: OnceLock<MStr<Endpoint>> = OnceLock::new();
49static DATA_PROCESS_DATA_ENDPOINT: OnceLock<MStr<Endpoint>> = OnceLock::new();
50static DATA_RESPONSE_ENDPOINT: OnceLock<MStr<Endpoint>> = OnceLock::new();
51static DATA_RESPONSE_TOPIC: OnceLock<MStr<Topic>> = OnceLock::new();
52static TIME_EVENT_TOPIC_MSTR: OnceLock<MStr<Topic>> = OnceLock::new();
53static EXEC_QUEUE_COMMAND_ENDPOINT: OnceLock<MStr<Endpoint>> = OnceLock::new();
54static EXEC_EXECUTE_ENDPOINT: OnceLock<MStr<Endpoint>> = OnceLock::new();
55static EXEC_PROCESS_ENDPOINT: OnceLock<MStr<Endpoint>> = OnceLock::new();
56static EXEC_RECONCILE_REPORT_ENDPOINT: OnceLock<MStr<Endpoint>> = OnceLock::new();
57static RISK_EXECUTE_ENDPOINT: OnceLock<MStr<Endpoint>> = OnceLock::new();
58static RISK_QUEUE_EXECUTE_ENDPOINT: OnceLock<MStr<Endpoint>> = OnceLock::new();
59static RISK_PROCESS_ENDPOINT: OnceLock<MStr<Endpoint>> = OnceLock::new();
60static RISK_EVENTS_TOPIC: OnceLock<MStr<Topic>> = OnceLock::new();
61static ORDER_EMULATOR_ENDPOINT: OnceLock<MStr<Endpoint>> = OnceLock::new();
62static PORTFOLIO_ACCOUNT_ENDPOINT: OnceLock<MStr<Endpoint>> = OnceLock::new();
63static PORTFOLIO_ORDER_ENDPOINT: OnceLock<MStr<Endpoint>> = OnceLock::new();
64static SYSTEM_SHUTDOWN_TOPIC: OnceLock<MStr<Topic>> = OnceLock::new();
65static SUBMISSION_RECOVERY_EXHAUSTED_TOPIC: OnceLock<MStr<Topic>> = OnceLock::new();
66static RECONCILIATION_RAW_ORDER_REPORT_TOPIC: OnceLock<MStr<Topic>> = OnceLock::new();
67static RECONCILIATION_RAW_FILL_REPORT_TOPIC: OnceLock<MStr<Topic>> = OnceLock::new();
68static RECONCILIATION_RAW_POSITION_REPORT_TOPIC: OnceLock<MStr<Topic>> = OnceLock::new();
69
70#[cfg(feature = "defi")]
71static DATA_PROCESS_DEFI_DATA_ENDPOINT: OnceLock<MStr<Endpoint>> = OnceLock::new();
72
73macro_rules! define_switchboard {
74 ($(
75 $field:ident: $key_ty:ty,
76 $method:ident($($arg_name:ident: $arg_ty:ty),*) -> $key_expr:expr,
77 $val_fmt:expr,
78 $($val_args:expr),*
79 );* $(;)?) => {
80 #[derive(Clone, Debug)]
82 pub struct MessagingSwitchboard {
83 $(
84 $field: AHashMap<$key_ty, MStr<Topic>>,
85 )*
86 pipeline_topics: AHashMap<MStr<Topic>, MStr<Topic>>,
87 instruments_patterns: AHashMap<Venue, MStr<Pattern>>,
88 book_deltas_patterns: AHashMap<InstrumentId, MStr<Pattern>>,
89 book_depth_patterns: AHashMap<InstrumentId, MStr<Pattern>>,
90 book_snapshots_patterns: AHashMap<(InstrumentId, NonZeroUsize), MStr<Pattern>>,
91 signal_topics: AHashMap<String, MStr<Topic>>,
92 signal_patterns: AHashMap<String, MStr<Pattern>>,
93 #[cfg(feature = "defi")]
94 pub(crate) defi: crate::defi::switchboard::DefiSwitchboard,
95 }
96
97 impl Default for MessagingSwitchboard {
98 fn default() -> Self {
100 Self {
101 $(
102 $field: AHashMap::new(),
103 )*
104 pipeline_topics: AHashMap::new(),
105 instruments_patterns: AHashMap::new(),
106 book_deltas_patterns: AHashMap::new(),
107 book_depth_patterns: AHashMap::new(),
108 book_snapshots_patterns: AHashMap::new(),
109 signal_topics: AHashMap::new(),
110 signal_patterns: AHashMap::new(),
111 #[cfg(feature = "defi")]
112 defi: crate::defi::switchboard::DefiSwitchboard::default(),
113 }
114 }
115 }
116
117 impl MessagingSwitchboard {
118 #[inline]
122 #[must_use]
123 pub fn data_engine_queue_execute() -> MStr<Endpoint> {
124 *DATA_QUEUE_COMMAND_ENDPOINT.get_or_init(|| "DataEngine.queue_execute".into())
125 }
126
127 #[inline]
129 #[must_use]
130 pub fn data_engine_execute() -> MStr<Endpoint> {
131 *DATA_EXECUTE_ENDPOINT.get_or_init(|| "DataEngine.execute".into())
132 }
133
134 #[inline]
135 #[must_use]
136 pub fn data_engine_process() -> MStr<Endpoint> {
137 *DATA_PROCESS_ANY_ENDPOINT.get_or_init(|| "DataEngine.process".into())
138 }
139
140 #[inline]
141 #[must_use]
142 pub fn data_engine_process_data() -> MStr<Endpoint> {
143 *DATA_PROCESS_DATA_ENDPOINT.get_or_init(|| "DataEngine.process_data".into())
144 }
145
146 #[cfg(feature = "defi")]
147 #[inline]
148 #[must_use]
149 pub fn data_engine_process_defi_data() -> MStr<Endpoint> {
150 *DATA_PROCESS_DEFI_DATA_ENDPOINT
151 .get_or_init(|| "DataEngine.process_defi_data".into())
152 }
153
154 #[inline]
155 #[must_use]
156 pub fn data_engine_response() -> MStr<Endpoint> {
157 *DATA_RESPONSE_ENDPOINT.get_or_init(|| "DataEngine.response".into())
158 }
159
160 #[inline]
161 #[must_use]
162 pub fn data_response_topic() -> MStr<Topic> {
163 *DATA_RESPONSE_TOPIC.get_or_init(|| "data.response".into())
164 }
165
166 #[inline]
168 #[must_use]
169 pub fn time_event_topic() -> MStr<Topic> {
170 *TIME_EVENT_TOPIC_MSTR.get_or_init(|| TIME_EVENT_TOPIC.into())
171 }
172
173 #[inline]
175 #[must_use]
176 pub fn exec_engine_execute() -> MStr<Endpoint> {
177 *EXEC_EXECUTE_ENDPOINT.get_or_init(|| "ExecEngine.execute".into())
178 }
179
180 #[inline]
182 #[must_use]
183 pub fn exec_engine_queue_execute() -> MStr<Endpoint> {
184 *EXEC_QUEUE_COMMAND_ENDPOINT.get_or_init(|| "ExecEngine.queue_execute".into())
185 }
186
187 #[inline]
188 #[must_use]
189 pub fn exec_engine_process() -> MStr<Endpoint> {
190 *EXEC_PROCESS_ENDPOINT.get_or_init(|| "ExecEngine.process".into())
191 }
192
193 #[inline]
194 #[must_use]
195 pub fn exec_engine_reconcile_execution_report() -> MStr<Endpoint> {
196 *EXEC_RECONCILE_REPORT_ENDPOINT.get_or_init(|| "ExecEngine.reconcile_execution_report".into())
197 }
198
199 #[inline]
201 #[must_use]
202 pub fn risk_engine_execute() -> MStr<Endpoint> {
203 *RISK_EXECUTE_ENDPOINT.get_or_init(|| "RiskEngine.execute".into())
204 }
205
206 #[inline]
208 #[must_use]
209 pub fn risk_engine_queue_execute() -> MStr<Endpoint> {
210 *RISK_QUEUE_EXECUTE_ENDPOINT.get_or_init(|| "RiskEngine.queue_execute".into())
211 }
212
213 #[inline]
214 #[must_use]
215 pub fn risk_engine_process() -> MStr<Endpoint> {
216 *RISK_PROCESS_ENDPOINT.get_or_init(|| "RiskEngine.process".into())
217 }
218
219 #[inline]
221 #[must_use]
222 pub fn risk_events_topic() -> MStr<Topic> {
223 *RISK_EVENTS_TOPIC.get_or_init(|| "events.risk".into())
224 }
225
226 #[inline]
227 #[must_use]
228 pub fn order_emulator_execute() -> MStr<Endpoint> {
229 *ORDER_EMULATOR_ENDPOINT.get_or_init(|| "OrderEmulator.execute".into())
230 }
231
232 #[inline]
233 #[must_use]
234 pub fn portfolio_update_account() -> MStr<Endpoint> {
235 *PORTFOLIO_ACCOUNT_ENDPOINT.get_or_init(|| "Portfolio.update_account".into())
236 }
237
238 #[inline]
239 #[must_use]
240 pub fn portfolio_update_order() -> MStr<Endpoint> {
241 *PORTFOLIO_ORDER_ENDPOINT.get_or_init(|| "Portfolio.update_order".into())
242 }
243
244 #[must_use]
246 pub fn queue_state_changed_topic(channel: SystemChannel) -> MStr<Topic> {
247 Self::queue_state_changed_pattern(Some(channel)).as_ref().into()
248 }
249
250 #[must_use]
252 pub fn queue_state_changed_pattern(channel: Option<SystemChannel>) -> MStr<Pattern> {
253 let channel = channel.map_or_else(|| "*".to_string(), |value| format!("{value:?}"));
254 format!("events.system.QueueStateChanged.{channel}").into()
255 }
256
257 #[must_use]
259 pub fn socket_state_changed_topic(client_id: ClientId, endpoint: &str) -> MStr<Topic> {
260 Self::socket_state_changed_pattern(Some(client_id), Some(endpoint)).as_ref().into()
261 }
262
263 #[must_use]
268 pub fn socket_state_changed_pattern(
269 client_id: Option<ClientId>,
270 endpoint: Option<&str>,
271 ) -> MStr<Pattern> {
272 let client_id = client_id.map_or_else(
273 || "*".to_string(),
274 |value| state_topic_component(value.as_str()),
275 );
276 let endpoint = endpoint.map_or_else(|| "*".to_string(), state_topic_component);
277 format!("events.system.SocketStateChanged.{client_id}.{endpoint}").into()
278 }
279
280 #[inline]
287 #[must_use]
288 pub fn shutdown_system_topic() -> MStr<Topic> {
289 *SYSTEM_SHUTDOWN_TOPIC.get_or_init(|| "commands.system.shutdown".into())
290 }
291
292 #[inline]
294 #[must_use]
295 pub fn submission_recovery_exhausted_topic() -> MStr<Topic> {
296 *SUBMISSION_RECOVERY_EXHAUSTED_TOPIC
297 .get_or_init(|| "reconciliation.SubmissionRecoveryExhausted".into())
298 }
299
300 #[inline]
309 #[must_use]
310 pub fn reconciliation_raw_order_status_report_topic() -> MStr<Topic> {
311 *RECONCILIATION_RAW_ORDER_REPORT_TOPIC
312 .get_or_init(|| "reconciliation.raw.OrderStatusReport".into())
313 }
314
315 #[inline]
322 #[must_use]
323 pub fn reconciliation_raw_fill_report_topic() -> MStr<Topic> {
324 *RECONCILIATION_RAW_FILL_REPORT_TOPIC
325 .get_or_init(|| "reconciliation.raw.FillReport".into())
326 }
327
328 #[inline]
335 #[must_use]
336 pub fn reconciliation_raw_position_status_report_topic() -> MStr<Topic> {
337 *RECONCILIATION_RAW_POSITION_REPORT_TOPIC
338 .get_or_init(|| "reconciliation.raw.PositionStatusReport".into())
339 }
340
341 #[must_use]
343 pub fn instruments_pattern(&mut self, venue: Venue) -> MStr<Pattern> {
344 *self.instruments_patterns
345 .entry(venue)
346 .or_insert_with(|| format!("data.instrument.{venue}.*").into())
347 }
348
349 #[must_use]
356 pub fn signal_topic(&mut self, name: &str) -> MStr<Topic> {
357 *self
358 .signal_topics
359 .entry(name.to_string())
360 .or_insert_with(|| {
361 format!(
362 "data.Signal{}",
363 nautilus_core::string::conversions::title_case(name)
364 )
365 .into()
366 })
367 }
368
369 #[must_use]
375 pub fn signal_pattern(&mut self, name: &str) -> MStr<Pattern> {
376 *self
377 .signal_patterns
378 .entry(name.to_string())
379 .or_insert_with(|| {
380 format!(
381 "data.Signal{}*",
382 nautilus_core::string::conversions::title_case(name)
383 )
384 .into()
385 })
386 }
387
388 $(
390 #[must_use]
391 pub fn $method(&mut self, $($arg_name: $arg_ty),*) -> MStr<Topic> {
392 let key = $key_expr;
393 *self.$field
394 .entry(key)
395 .or_insert_with(|| format!($val_fmt, $($val_args),*).into())
396 }
397 )*
398 }
399 };
400}
401
402define_switchboard! {
403 custom_topics: DataType,
404 get_custom_topic(data_type: &DataType) -> data_type.clone(),
405 "data.{}", data_type.topic();
406
407 instruments_topics: Venue,
408 get_instruments_topic(venue: Venue) -> venue,
409 "data.instrument.{}", venue;
410
411 instrument_topics: InstrumentId,
412 get_instrument_topic(instrument_id: InstrumentId) -> instrument_id,
413 "data.instrument.{}.{}", instrument_id.venue, instrument_id.symbol;
414
415 book_deltas_topics: InstrumentId,
416 get_book_deltas_topic(instrument_id: InstrumentId) -> instrument_id,
417 "data.book.deltas.{}.{}", instrument_id.venue, instrument_id.symbol;
418
419 book_depth_topics: InstrumentId,
420 get_book_depth_topic(instrument_id: InstrumentId) -> instrument_id,
421 "data.book.depth.{}.{}", instrument_id.venue, instrument_id.symbol;
422
423 book_snapshots_topics: (InstrumentId, NonZeroUsize),
424 get_book_snapshots_topic(instrument_id: InstrumentId, interval_ms: NonZeroUsize) -> (instrument_id, interval_ms),
425 "data.book.snapshots.{}.{}.{}", instrument_id.venue, instrument_id.symbol, interval_ms;
426
427 quote_topics: InstrumentId,
428 get_quotes_topic(instrument_id: InstrumentId) -> instrument_id,
429 "data.quotes.{}.{}", instrument_id.venue, instrument_id.symbol;
430
431 trade_topics: InstrumentId,
432 get_trades_topic(instrument_id: InstrumentId) -> instrument_id,
433 "data.trades.{}.{}", instrument_id.venue, instrument_id.symbol;
434
435 bar_topics: BarType,
436 get_bars_topic(bar_type: BarType) -> bar_type,
437 "data.bars.{}", bar_type;
438
439 mark_price_topics: InstrumentId,
440 get_mark_price_topic(instrument_id: InstrumentId) -> instrument_id,
441 "data.mark_prices.{}.{}", instrument_id.venue, instrument_id.symbol;
442
443 index_price_topics: InstrumentId,
444 get_index_price_topic(instrument_id: InstrumentId) -> instrument_id,
445 "data.index_prices.{}.{}", instrument_id.venue, instrument_id.symbol;
446
447 funding_rate_topics: InstrumentId,
448 get_funding_rate_topic(instrument_id: InstrumentId) -> instrument_id,
449 "data.funding_rates.{}.{}", instrument_id.venue, instrument_id.symbol;
450
451 funding_settlement_topics: InstrumentId,
452 get_funding_settlement_topic(instrument_id: InstrumentId) -> instrument_id,
453 "events.funding_settlements.{}.{}", instrument_id.venue, instrument_id.symbol;
454
455 instrument_status_topics: InstrumentId,
456 get_instrument_status_topic(instrument_id: InstrumentId) -> instrument_id,
457 "data.status.{}.{}", instrument_id.venue, instrument_id.symbol;
458
459 instrument_close_topics: InstrumentId,
460 get_instrument_close_topic(instrument_id: InstrumentId) -> instrument_id,
461 "data.close.{}.{}", instrument_id.venue, instrument_id.symbol;
462
463 option_greeks_topics: InstrumentId,
464 get_option_greeks_topic(instrument_id: InstrumentId) -> instrument_id,
465 "data.option_greeks.{}.{}", instrument_id.venue, instrument_id.symbol;
466
467 option_chain_topics: OptionSeriesId,
468 get_option_chain_topic(series_id: OptionSeriesId) -> series_id,
469 "data.option_chain.{}", series_id;
470
471 order_submitted_topics: InstrumentId,
472 get_order_submitted_topic(instrument_id: InstrumentId) -> instrument_id,
473 "events.order_submitted.{}", instrument_id;
474
475 order_rejected_topics: InstrumentId,
476 get_order_rejected_topic(instrument_id: InstrumentId) -> instrument_id,
477 "events.order_rejected.{}", instrument_id;
478
479 order_pending_update_topics: InstrumentId,
480 get_order_pending_update_topic(instrument_id: InstrumentId) -> instrument_id,
481 "events.order_pending_update.{}", instrument_id;
482
483 order_pending_cancel_topics: InstrumentId,
484 get_order_pending_cancel_topic(instrument_id: InstrumentId) -> instrument_id,
485 "events.order_pending_cancel.{}", instrument_id;
486
487 order_modify_rejected_topics: InstrumentId,
488 get_order_modify_rejected_topic(instrument_id: InstrumentId) -> instrument_id,
489 "events.order_modify_rejected.{}", instrument_id;
490
491 order_cancel_rejected_topics: InstrumentId,
492 get_order_cancel_rejected_topic(instrument_id: InstrumentId) -> instrument_id,
493 "events.order_cancel_rejected.{}", instrument_id;
494
495 order_canceled_topics: InstrumentId,
496 get_order_canceled_topic(instrument_id: InstrumentId) -> instrument_id,
497 "events.order_canceled.{}", instrument_id;
498
499 order_filled_topics: InstrumentId,
500 get_order_filled_topic(instrument_id: InstrumentId) -> instrument_id,
501 "events.order_filled.{}", instrument_id;
502
503 order_fill_voided_topics: InstrumentId,
504 get_order_fill_voided_topic(instrument_id: InstrumentId) -> instrument_id,
505 "events.order_fill_voided.{}", instrument_id;
506
507 order_fill_declined_topics: InstrumentId,
508 get_order_fill_declined_topic(instrument_id: InstrumentId) -> instrument_id,
509 "events.order_fill_declined.{}", instrument_id;
510
511 event_order_topics: StrategyId,
512 get_event_order_topic(strategy_id: StrategyId) -> strategy_id,
513 "events.order.{}", strategy_id;
514
515 event_position_topics: StrategyId,
516 get_event_position_topic(strategy_id: StrategyId) -> strategy_id,
517 "events.position.{}", strategy_id;
518
519 snapshot_order_topics: ClientOrderId,
520 get_snapshot_order_topic(client_order_id: ClientOrderId) -> client_order_id,
521 "snapshots.order.{}", client_order_id;
522
523 snapshot_position_topics: PositionId,
524 get_snapshot_position_topic(position_id: PositionId) -> position_id,
525 "snapshots.position.{}", position_id;
526
527}
528
529impl MessagingSwitchboard {
530 #[inline]
531 fn pipeline_topic(&mut self, live: MStr<Topic>) -> MStr<Topic> {
532 *self.pipeline_topics.entry(live).or_insert_with(|| {
533 let live = live.as_ref();
534 let suffix = live
535 .strip_prefix("data.")
536 .expect("live data topic must start with data.");
537 MStr::<Topic>::from(format!("data.pipeline.{suffix}"))
538 })
539 }
540
541 #[must_use]
542 pub fn get_pipeline_custom_topic(&mut self, data_type: &DataType) -> MStr<Topic> {
543 let live = self.get_custom_topic(data_type);
544 self.pipeline_topic(live)
545 }
546
547 #[must_use]
548 pub fn get_pipeline_book_deltas_topic(&mut self, instrument_id: InstrumentId) -> MStr<Topic> {
549 let live = self.get_book_deltas_topic(instrument_id);
550 self.pipeline_topic(live)
551 }
552
553 #[must_use]
554 pub fn get_pipeline_book_depth_topic(&mut self, instrument_id: InstrumentId) -> MStr<Topic> {
555 let live = self.get_book_depth_topic(instrument_id);
556 self.pipeline_topic(live)
557 }
558
559 #[must_use]
560 pub fn get_pipeline_quotes_topic(&mut self, instrument_id: InstrumentId) -> MStr<Topic> {
561 let live = self.get_quotes_topic(instrument_id);
562 self.pipeline_topic(live)
563 }
564
565 #[must_use]
566 pub fn get_pipeline_trades_topic(&mut self, instrument_id: InstrumentId) -> MStr<Topic> {
567 let live = self.get_trades_topic(instrument_id);
568 self.pipeline_topic(live)
569 }
570
571 #[must_use]
572 pub fn get_pipeline_bars_topic(&mut self, bar_type: BarType) -> MStr<Topic> {
573 let live = self.get_bars_topic(bar_type);
574 self.pipeline_topic(live)
575 }
576
577 #[must_use]
578 pub fn get_pipeline_mark_price_topic(&mut self, instrument_id: InstrumentId) -> MStr<Topic> {
579 let live = self.get_mark_price_topic(instrument_id);
580 self.pipeline_topic(live)
581 }
582
583 #[must_use]
584 pub fn get_pipeline_index_price_topic(&mut self, instrument_id: InstrumentId) -> MStr<Topic> {
585 let live = self.get_index_price_topic(instrument_id);
586 self.pipeline_topic(live)
587 }
588
589 #[must_use]
590 pub fn get_pipeline_funding_rate_topic(&mut self, instrument_id: InstrumentId) -> MStr<Topic> {
591 let live = self.get_funding_rate_topic(instrument_id);
592 self.pipeline_topic(live)
593 }
594
595 #[must_use]
596 pub fn get_pipeline_instrument_status_topic(
597 &mut self,
598 instrument_id: InstrumentId,
599 ) -> MStr<Topic> {
600 let live = self.get_instrument_status_topic(instrument_id);
601 self.pipeline_topic(live)
602 }
603
604 #[must_use]
605 pub fn get_pipeline_option_greeks_topic(&mut self, instrument_id: InstrumentId) -> MStr<Topic> {
606 let live = self.get_option_greeks_topic(instrument_id);
607 self.pipeline_topic(live)
608 }
609
610 #[must_use]
611 pub fn get_pipeline_instrument_close_topic(
612 &mut self,
613 instrument_id: InstrumentId,
614 ) -> MStr<Topic> {
615 let live = self.get_instrument_close_topic(instrument_id);
616 self.pipeline_topic(live)
617 }
618
619 #[must_use]
621 pub fn get_book_deltas_pattern(&mut self, instrument_id: InstrumentId) -> MStr<Pattern> {
622 *self
623 .book_deltas_patterns
624 .entry(instrument_id)
625 .or_insert_with(|| {
626 format!(
627 "data.book.deltas.{}.{}",
628 instrument_id.venue,
629 instrument_id.symbol.topic(),
630 )
631 .into()
632 })
633 }
634
635 #[must_use]
637 pub fn get_book_depth_pattern(&mut self, instrument_id: InstrumentId) -> MStr<Pattern> {
638 *self
639 .book_depth_patterns
640 .entry(instrument_id)
641 .or_insert_with(|| {
642 format!(
643 "data.book.depth.{}.{}",
644 instrument_id.venue,
645 instrument_id.symbol.topic(),
646 )
647 .into()
648 })
649 }
650
651 #[must_use]
653 pub fn get_book_snapshots_pattern(
654 &mut self,
655 instrument_id: InstrumentId,
656 interval_ms: NonZeroUsize,
657 ) -> MStr<Pattern> {
658 *self
659 .book_snapshots_patterns
660 .entry((instrument_id, interval_ms))
661 .or_insert_with(|| {
662 format!(
663 "data.book.snapshots.{}.{}.{}",
664 instrument_id.venue,
665 instrument_id.symbol.topic(),
666 interval_ms,
667 )
668 .into()
669 })
670 }
671}
672
673macro_rules! define_wrappers {
674 ($($method:ident($($arg_name:ident: $arg_ty:ty),*) -> $ret:ty),* $(,)?) => {
675 $(
676 #[must_use]
677 pub fn $method($($arg_name: $arg_ty),*) -> $ret {
678 get_message_bus()
679 .borrow_mut()
680 .switchboard
681 .$method($($arg_name),*)
682 }
683 )*
684 }
685}
686
687define_wrappers! {
688 get_custom_topic(data_type: &DataType) -> MStr<Topic>,
689 get_instruments_topic(venue: Venue) -> MStr<Topic>,
690 get_instrument_topic(instrument_id: InstrumentId) -> MStr<Topic>,
691 get_book_deltas_topic(instrument_id: InstrumentId) -> MStr<Topic>,
692 get_book_depth_topic(instrument_id: InstrumentId) -> MStr<Topic>,
693 get_book_snapshots_topic(instrument_id: InstrumentId, interval_ms: NonZeroUsize) -> MStr<Topic>,
694 get_quotes_topic(instrument_id: InstrumentId) -> MStr<Topic>,
695 get_trades_topic(instrument_id: InstrumentId) -> MStr<Topic>,
696 get_bars_topic(bar_type: BarType) -> MStr<Topic>,
697 get_mark_price_topic(instrument_id: InstrumentId) -> MStr<Topic>,
698 get_index_price_topic(instrument_id: InstrumentId) -> MStr<Topic>,
699 get_funding_rate_topic(instrument_id: InstrumentId) -> MStr<Topic>,
700 get_funding_settlement_topic(instrument_id: InstrumentId) -> MStr<Topic>,
701 get_instrument_status_topic(instrument_id: InstrumentId) -> MStr<Topic>,
702 get_instrument_close_topic(instrument_id: InstrumentId) -> MStr<Topic>,
703 get_option_greeks_topic(instrument_id: InstrumentId) -> MStr<Topic>,
704 get_option_chain_topic(series_id: OptionSeriesId) -> MStr<Topic>,
705 get_pipeline_custom_topic(data_type: &DataType) -> MStr<Topic>,
706 get_pipeline_book_deltas_topic(instrument_id: InstrumentId) -> MStr<Topic>,
707 get_pipeline_book_depth_topic(instrument_id: InstrumentId) -> MStr<Topic>,
708 get_pipeline_quotes_topic(instrument_id: InstrumentId) -> MStr<Topic>,
709 get_pipeline_trades_topic(instrument_id: InstrumentId) -> MStr<Topic>,
710 get_pipeline_bars_topic(bar_type: BarType) -> MStr<Topic>,
711 get_pipeline_mark_price_topic(instrument_id: InstrumentId) -> MStr<Topic>,
712 get_pipeline_index_price_topic(instrument_id: InstrumentId) -> MStr<Topic>,
713 get_pipeline_funding_rate_topic(instrument_id: InstrumentId) -> MStr<Topic>,
714 get_pipeline_instrument_status_topic(instrument_id: InstrumentId) -> MStr<Topic>,
715 get_pipeline_option_greeks_topic(instrument_id: InstrumentId) -> MStr<Topic>,
716 get_pipeline_instrument_close_topic(instrument_id: InstrumentId) -> MStr<Topic>,
717 get_order_submitted_topic(instrument_id: InstrumentId) -> MStr<Topic>,
718 get_order_rejected_topic(instrument_id: InstrumentId) -> MStr<Topic>,
719 get_order_pending_update_topic(instrument_id: InstrumentId) -> MStr<Topic>,
720 get_order_pending_cancel_topic(instrument_id: InstrumentId) -> MStr<Topic>,
721 get_order_modify_rejected_topic(instrument_id: InstrumentId) -> MStr<Topic>,
722 get_order_cancel_rejected_topic(instrument_id: InstrumentId) -> MStr<Topic>,
723 get_order_canceled_topic(instrument_id: InstrumentId) -> MStr<Topic>,
724 get_order_filled_topic(instrument_id: InstrumentId) -> MStr<Topic>,
725 get_order_fill_voided_topic(instrument_id: InstrumentId) -> MStr<Topic>,
726 get_order_fill_declined_topic(instrument_id: InstrumentId) -> MStr<Topic>,
727 get_snapshot_order_topic(client_order_id: ClientOrderId) -> MStr<Topic>,
728 get_snapshot_position_topic(position_id: PositionId) -> MStr<Topic>,
729 get_event_order_topic(strategy_id: StrategyId) -> MStr<Topic>,
730 get_event_position_topic(strategy_id: StrategyId) -> MStr<Topic>,
731}
732
733#[must_use]
739pub fn get_instruments_pattern(venue: Venue) -> MStr<Pattern> {
740 get_message_bus()
741 .borrow_mut()
742 .switchboard
743 .instruments_pattern(venue)
744}
745
746#[must_use]
748pub fn get_book_deltas_pattern(instrument_id: InstrumentId) -> MStr<Pattern> {
749 get_message_bus()
750 .borrow_mut()
751 .switchboard
752 .get_book_deltas_pattern(instrument_id)
753}
754
755#[must_use]
757pub fn get_book_depth_pattern(instrument_id: InstrumentId) -> MStr<Pattern> {
758 get_message_bus()
759 .borrow_mut()
760 .switchboard
761 .get_book_depth_pattern(instrument_id)
762}
763
764#[must_use]
766pub fn get_book_snapshots_pattern(
767 instrument_id: InstrumentId,
768 interval_ms: NonZeroUsize,
769) -> MStr<Pattern> {
770 get_message_bus()
771 .borrow_mut()
772 .switchboard
773 .get_book_snapshots_pattern(instrument_id, interval_ms)
774}
775
776#[must_use]
778pub fn get_signal_topic(name: &str) -> MStr<Topic> {
779 get_message_bus()
780 .borrow_mut()
781 .switchboard
782 .signal_topic(name)
783}
784
785#[must_use]
789pub fn get_signal_pattern(name: &str) -> MStr<Pattern> {
790 get_message_bus()
791 .borrow_mut()
792 .switchboard
793 .signal_pattern(name)
794}
795
796#[must_use]
798pub fn get_custom_subscription_topics(data_type: &DataType) -> Vec<MStr<Pattern>> {
799 let topic = get_custom_topic(data_type);
800 let mut topics = vec![topic.into()];
801 if data_type.identifier().is_none() {
802 topics.push(MStr::pattern(format!("{topic}{IDENTIFIER_TOPIC_SUFFIX}*")));
803 }
804 topics
805}
806
807fn state_topic_component(value: &str) -> String {
808 let mut encoded = String::with_capacity(value.len());
809 for byte in value.bytes() {
810 if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_') {
811 encoded.push(char::from(byte));
812 } else {
813 write!(encoded, "%{byte:02X}").expect("String formatting cannot fail");
814 }
815 }
816
817 encoded
818}
819
820#[cfg(test)]
821mod tests {
822 use nautilus_model::{
823 data::{BarType, DataType},
824 identifiers::{InstrumentId, Venue},
825 };
826 use rstest::*;
827
828 use super::*;
829 use crate::msgbus::matching::is_matching_backtracking;
830
831 #[fixture]
832 fn switchboard() -> MessagingSwitchboard {
833 MessagingSwitchboard::default()
834 }
835
836 #[fixture]
837 fn instrument_id() -> InstrumentId {
838 InstrumentId::from("ESZ24.XCME")
839 }
840
841 #[rstest]
842 fn test_data_response_topic() {
843 let expected_topic = "data.response".into();
844 let result = MessagingSwitchboard::data_response_topic();
845 assert_eq!(result, expected_topic);
846 }
847
848 #[rstest]
849 fn test_time_event_topic() {
850 let expected_topic = "clock.time_event".into();
851 let result = MessagingSwitchboard::time_event_topic();
852 assert_eq!(result, expected_topic);
853 }
854
855 #[rstest]
856 fn test_reconciliation_raw_order_status_report_topic() {
857 let expected_topic = "reconciliation.raw.OrderStatusReport".into();
858 let result = MessagingSwitchboard::reconciliation_raw_order_status_report_topic();
859 assert_eq!(result, expected_topic);
860 }
861
862 #[rstest]
863 fn test_reconciliation_raw_fill_report_topic() {
864 let expected_topic = "reconciliation.raw.FillReport".into();
865 let result = MessagingSwitchboard::reconciliation_raw_fill_report_topic();
866 assert_eq!(result, expected_topic);
867 }
868
869 #[rstest]
870 fn test_reconciliation_raw_position_status_report_topic() {
871 let expected_topic = "reconciliation.raw.PositionStatusReport".into();
872 let result = MessagingSwitchboard::reconciliation_raw_position_status_report_topic();
873 assert_eq!(result, expected_topic);
874 }
875
876 #[rstest]
877 fn test_get_custom_topic(mut switchboard: MessagingSwitchboard) {
878 let data_type = DataType::new("ExampleDataType", None, None);
879 let expected_topic = "data.ExampleDataType".into();
880 let result = switchboard.get_custom_topic(&data_type);
881 assert_eq!(result, expected_topic);
882 assert!(switchboard.custom_topics.contains_key(&data_type));
883 }
884
885 #[rstest]
886 fn test_get_instrument_topic(
887 mut switchboard: MessagingSwitchboard,
888 instrument_id: InstrumentId,
889 ) {
890 let expected_topic = "data.instrument.XCME.ESZ24".into();
891 let result = switchboard.get_instrument_topic(instrument_id);
892 assert_eq!(result, expected_topic);
893 assert!(switchboard.instrument_topics.contains_key(&instrument_id));
894 }
895
896 #[rstest]
897 fn test_get_book_deltas_topic(
898 mut switchboard: MessagingSwitchboard,
899 instrument_id: InstrumentId,
900 ) {
901 let expected_topic = "data.book.deltas.XCME.ESZ24".into();
902 let result = switchboard.get_book_deltas_topic(instrument_id);
903 assert_eq!(result, expected_topic);
904 assert!(switchboard.book_deltas_topics.contains_key(&instrument_id));
905 }
906
907 #[rstest]
908 fn test_get_book_depth_topic(
909 mut switchboard: MessagingSwitchboard,
910 instrument_id: InstrumentId,
911 ) {
912 let expected_topic = "data.book.depth.XCME.ESZ24".into();
913 let result = switchboard.get_book_depth_topic(instrument_id);
914 assert_eq!(result, expected_topic);
915 assert!(switchboard.book_depth_topics.contains_key(&instrument_id));
916 }
917
918 #[rstest]
919 fn test_get_book_snapshots_topic(
920 mut switchboard: MessagingSwitchboard,
921 instrument_id: InstrumentId,
922 ) {
923 let expected_topic = "data.book.snapshots.XCME.ESZ24.1000".into();
924 let interval_ms = NonZeroUsize::new(1000).unwrap();
925 let result = switchboard.get_book_snapshots_topic(instrument_id, interval_ms);
926 assert_eq!(result, expected_topic);
927
928 assert!(
929 switchboard
930 .book_snapshots_topics
931 .contains_key(&(instrument_id, interval_ms))
932 );
933 }
934
935 #[rstest]
936 fn test_get_quotes_topic(mut switchboard: MessagingSwitchboard, instrument_id: InstrumentId) {
937 let expected_topic = "data.quotes.XCME.ESZ24".into();
938 let result = switchboard.get_quotes_topic(instrument_id);
939 assert_eq!(result, expected_topic);
940 assert!(switchboard.quote_topics.contains_key(&instrument_id));
941 }
942
943 #[rstest]
944 fn test_get_trades_topic(mut switchboard: MessagingSwitchboard, instrument_id: InstrumentId) {
945 let expected_topic = "data.trades.XCME.ESZ24".into();
946 let result = switchboard.get_trades_topic(instrument_id);
947 assert_eq!(result, expected_topic);
948 assert!(switchboard.trade_topics.contains_key(&instrument_id));
949 }
950
951 #[rstest]
952 fn test_get_bars_topic(mut switchboard: MessagingSwitchboard) {
953 let bar_type = BarType::from("ESZ24.XCME-1-MINUTE-LAST-INTERNAL");
954 let expected_topic = format!("data.bars.{bar_type}").into();
955 let result = switchboard.get_bars_topic(bar_type);
956 assert_eq!(result, expected_topic);
957 assert!(switchboard.bar_topics.contains_key(&bar_type));
958 }
959
960 #[rstest]
961 fn test_get_pipeline_custom_topic(mut switchboard: MessagingSwitchboard) {
962 let data_type = DataType::new("ExampleDataType", None, None);
963 let expected_topic = "data.pipeline.ExampleDataType".into();
964 let result = switchboard.get_pipeline_custom_topic(&data_type);
965 assert_eq!(result, expected_topic);
966 assert!(switchboard.custom_topics.contains_key(&data_type));
967 assert_eq!(switchboard.pipeline_topics.len(), 1);
968 }
969
970 type PipelineInstrumentIdTopicFn = fn(&mut MessagingSwitchboard, InstrumentId) -> MStr<Topic>;
971
972 #[rstest]
973 #[case::book_deltas(
974 MessagingSwitchboard::get_pipeline_book_deltas_topic as PipelineInstrumentIdTopicFn,
975 "data.pipeline.book.deltas.XCME.ESZ24",
976 )]
977 #[case::book_depth(
978 MessagingSwitchboard::get_pipeline_book_depth_topic as PipelineInstrumentIdTopicFn,
979 "data.pipeline.book.depth.XCME.ESZ24",
980 )]
981 #[case::quotes(
982 MessagingSwitchboard::get_pipeline_quotes_topic as PipelineInstrumentIdTopicFn,
983 "data.pipeline.quotes.XCME.ESZ24",
984 )]
985 #[case::trades(
986 MessagingSwitchboard::get_pipeline_trades_topic as PipelineInstrumentIdTopicFn,
987 "data.pipeline.trades.XCME.ESZ24",
988 )]
989 #[case::mark_prices(
990 MessagingSwitchboard::get_pipeline_mark_price_topic as PipelineInstrumentIdTopicFn,
991 "data.pipeline.mark_prices.XCME.ESZ24",
992 )]
993 #[case::index_prices(
994 MessagingSwitchboard::get_pipeline_index_price_topic as PipelineInstrumentIdTopicFn,
995 "data.pipeline.index_prices.XCME.ESZ24",
996 )]
997 #[case::funding_rates(
998 MessagingSwitchboard::get_pipeline_funding_rate_topic as PipelineInstrumentIdTopicFn,
999 "data.pipeline.funding_rates.XCME.ESZ24",
1000 )]
1001 #[case::status(
1002 MessagingSwitchboard::get_pipeline_instrument_status_topic as PipelineInstrumentIdTopicFn,
1003 "data.pipeline.status.XCME.ESZ24",
1004 )]
1005 #[case::close(
1006 MessagingSwitchboard::get_pipeline_instrument_close_topic as PipelineInstrumentIdTopicFn,
1007 "data.pipeline.close.XCME.ESZ24",
1008 )]
1009 fn test_get_pipeline_instrument_id_topic(
1010 mut switchboard: MessagingSwitchboard,
1011 instrument_id: InstrumentId,
1012 #[case] topic_fn: PipelineInstrumentIdTopicFn,
1013 #[case] expected: &str,
1014 ) {
1015 let result = topic_fn(&mut switchboard, instrument_id);
1016 assert_eq!(result.as_ref(), expected);
1017 assert_eq!(switchboard.pipeline_topics.len(), 1);
1018 }
1019
1020 #[rstest]
1021 fn test_get_pipeline_bars_topic(mut switchboard: MessagingSwitchboard) {
1022 let bar_type = BarType::from("ESZ24.XCME-1-MINUTE-LAST-INTERNAL");
1023 let expected_topic = format!("data.pipeline.bars.{bar_type}").into();
1024 let result = switchboard.get_pipeline_bars_topic(bar_type);
1025 assert_eq!(result, expected_topic);
1026 assert!(switchboard.bar_topics.contains_key(&bar_type));
1027 assert_eq!(switchboard.pipeline_topics.len(), 1);
1028 }
1029
1030 type OrderEventTopicFn = fn(&mut MessagingSwitchboard, InstrumentId) -> MStr<Topic>;
1031
1032 #[rstest]
1033 #[case::submitted(
1034 MessagingSwitchboard::get_order_submitted_topic as OrderEventTopicFn,
1035 "events.order_submitted.ESZ24.XCME",
1036 )]
1037 #[case::rejected(
1038 MessagingSwitchboard::get_order_rejected_topic as OrderEventTopicFn,
1039 "events.order_rejected.ESZ24.XCME",
1040 )]
1041 #[case::pending_update(
1042 MessagingSwitchboard::get_order_pending_update_topic as OrderEventTopicFn,
1043 "events.order_pending_update.ESZ24.XCME",
1044 )]
1045 #[case::pending_cancel(
1046 MessagingSwitchboard::get_order_pending_cancel_topic as OrderEventTopicFn,
1047 "events.order_pending_cancel.ESZ24.XCME",
1048 )]
1049 #[case::modify_rejected(
1050 MessagingSwitchboard::get_order_modify_rejected_topic as OrderEventTopicFn,
1051 "events.order_modify_rejected.ESZ24.XCME",
1052 )]
1053 #[case::cancel_rejected(
1054 MessagingSwitchboard::get_order_cancel_rejected_topic as OrderEventTopicFn,
1055 "events.order_cancel_rejected.ESZ24.XCME",
1056 )]
1057 #[case::canceled(
1058 MessagingSwitchboard::get_order_canceled_topic as OrderEventTopicFn,
1059 "events.order_canceled.ESZ24.XCME",
1060 )]
1061 #[case::filled(
1062 MessagingSwitchboard::get_order_filled_topic as OrderEventTopicFn,
1063 "events.order_filled.ESZ24.XCME",
1064 )]
1065 #[case::fill_voided(
1066 MessagingSwitchboard::get_order_fill_voided_topic as OrderEventTopicFn,
1067 "events.order_fill_voided.ESZ24.XCME",
1068 )]
1069 fn test_get_order_event_topic(
1070 mut switchboard: MessagingSwitchboard,
1071 instrument_id: InstrumentId,
1072 #[case] topic_fn: OrderEventTopicFn,
1073 #[case] expected: &str,
1074 ) {
1075 let result = topic_fn(&mut switchboard, instrument_id);
1076 assert_eq!(result.as_ref(), expected);
1077 }
1078
1079 #[rstest]
1080 #[case::submitted(MessagingSwitchboard::get_order_submitted_topic as OrderEventTopicFn)]
1081 #[case::rejected(MessagingSwitchboard::get_order_rejected_topic as OrderEventTopicFn)]
1082 #[case::pending_update(MessagingSwitchboard::get_order_pending_update_topic as OrderEventTopicFn)]
1083 #[case::pending_cancel(MessagingSwitchboard::get_order_pending_cancel_topic as OrderEventTopicFn)]
1084 #[case::modify_rejected(MessagingSwitchboard::get_order_modify_rejected_topic as OrderEventTopicFn)]
1085 #[case::cancel_rejected(MessagingSwitchboard::get_order_cancel_rejected_topic as OrderEventTopicFn)]
1086 #[case::canceled(MessagingSwitchboard::get_order_canceled_topic as OrderEventTopicFn)]
1087 #[case::filled(MessagingSwitchboard::get_order_filled_topic as OrderEventTopicFn)]
1088 #[case::fill_voided(MessagingSwitchboard::get_order_fill_voided_topic as OrderEventTopicFn)]
1089 fn test_order_event_topic_does_not_match_strategy_order_pattern(
1090 mut switchboard: MessagingSwitchboard,
1091 instrument_id: InstrumentId,
1092 #[case] topic_fn: OrderEventTopicFn,
1093 ) {
1094 let topic = topic_fn(&mut switchboard, instrument_id);
1095 assert!(!is_matching_backtracking(topic, "events.order.*".into()));
1096 }
1097
1098 #[rstest]
1099 fn test_get_snapshot_order_topic(mut switchboard: MessagingSwitchboard) {
1100 let client_order_id = ClientOrderId::from("O-123456789");
1101 let expected_topic = format!("snapshots.order.{client_order_id}").into();
1102 let result = switchboard.get_snapshot_order_topic(client_order_id);
1103 assert_eq!(result, expected_topic);
1104 assert!(
1105 switchboard
1106 .snapshot_order_topics
1107 .contains_key(&client_order_id)
1108 );
1109 }
1110
1111 #[rstest]
1112 fn test_get_snapshot_position_topic(mut switchboard: MessagingSwitchboard) {
1113 let position_id = PositionId::from("P-123456789");
1114 let expected_topic = format!("snapshots.position.{position_id}").into();
1115 let result = switchboard.get_snapshot_position_topic(position_id);
1116 assert_eq!(result, expected_topic);
1117 assert!(
1118 switchboard
1119 .snapshot_position_topics
1120 .contains_key(&position_id)
1121 );
1122 }
1123
1124 #[rstest]
1125 #[case(
1126 "CLIENT.A",
1127 "orders",
1128 "events.system.SocketStateChanged.CLIENT%2EA.orders"
1129 )]
1130 #[case(
1131 "CLIENT*?",
1132 "public.market",
1133 "events.system.SocketStateChanged.CLIENT%2A%3F.public%2Emarket"
1134 )]
1135 #[case(
1136 "CLIENT%2E",
1137 "market",
1138 "events.system.SocketStateChanged.CLIENT%252E.market"
1139 )]
1140 fn test_socket_state_topic_encodes_literal_components(
1141 #[case] client_id: &str,
1142 #[case] endpoint: &str,
1143 #[case] expected: &str,
1144 ) {
1145 let topic =
1146 MessagingSwitchboard::socket_state_changed_topic(ClientId::from(client_id), endpoint);
1147 assert_eq!(topic.as_ref(), expected);
1148 }
1149
1150 #[rstest]
1151 #[case(Some("CLIENT"), None, "CLIENT.A", "market", false)]
1152 #[case(None, Some("market"), "CLIENT", "public.market", false)]
1153 #[case(Some("CLIENT*"), None, "CLIENT1", "market", false)]
1154 #[case(Some("CLIENT?"), None, "CLIENT1", "market", false)]
1155 #[case(
1156 Some("CLIENT.A"),
1157 Some("public.market"),
1158 "CLIENT.A",
1159 "public.market",
1160 true
1161 )]
1162 #[case(None, Some("public.market"), "CLIENT.A", "public.market", true)]
1163 fn test_socket_state_pattern_matches_literal_fields(
1164 #[case] client_filter: Option<&str>,
1165 #[case] endpoint_filter: Option<&str>,
1166 #[case] client_id: &str,
1167 #[case] endpoint: &str,
1168 #[case] expected: bool,
1169 ) {
1170 let topic =
1171 MessagingSwitchboard::socket_state_changed_topic(ClientId::from(client_id), endpoint);
1172 let pattern = MessagingSwitchboard::socket_state_changed_pattern(
1173 client_filter.map(ClientId::from),
1174 endpoint_filter,
1175 );
1176 assert_eq!(is_matching_backtracking(topic, pattern), expected);
1177 }
1178
1179 #[rstest]
1180 fn test_queue_state_changed_topic_identity() {
1181 assert_eq!(
1182 MessagingSwitchboard::queue_state_changed_topic(SystemChannel::ExecCommands).as_ref(),
1183 "events.system.QueueStateChanged.ExecCommands"
1184 );
1185 }
1186
1187 #[rstest]
1188 fn test_socket_state_changed_topic_identity() {
1189 assert_eq!(
1190 MessagingSwitchboard::socket_state_changed_topic(ClientId::from("BINANCE"), "market")
1191 .as_ref(),
1192 "events.system.SocketStateChanged.BINANCE.market"
1193 );
1194 }
1195
1196 #[rstest]
1197 fn test_instruments_pattern_matches_instrument_topic(
1198 mut switchboard: MessagingSwitchboard,
1199 instrument_id: InstrumentId,
1200 ) {
1201 let venue = instrument_id.venue;
1202 let pattern = switchboard.instruments_pattern(venue);
1203 let topic = switchboard.get_instrument_topic(instrument_id);
1204
1205 assert_eq!(pattern.as_ref(), "data.instrument.XCME.*");
1206 assert!(is_matching_backtracking(topic, pattern));
1207 }
1208
1209 #[rstest]
1210 fn test_instruments_pattern_does_not_match_other_venue(mut switchboard: MessagingSwitchboard) {
1211 let pattern = switchboard.instruments_pattern(Venue::from("BINANCE"));
1212 let topic = switchboard.get_instrument_topic(InstrumentId::from("ESZ24.XCME"));
1213
1214 assert!(!is_matching_backtracking(topic, pattern));
1215 }
1216
1217 #[rstest]
1218 fn test_composite_book_deltas_pattern_uses_wildcard(mut switchboard: MessagingSwitchboard) {
1219 let composite_id = InstrumentId::from("ES.FUT.XCME");
1220 let underlying_id = InstrumentId::from("ESZ24.XCME");
1221
1222 let composite_pattern = switchboard.get_book_deltas_pattern(composite_id);
1223 let underlying_topic = switchboard.get_book_deltas_topic(underlying_id);
1224
1225 assert_eq!(composite_pattern.as_ref(), "data.book.deltas.XCME.ES*");
1226 assert_eq!(underlying_topic.as_ref(), "data.book.deltas.XCME.ESZ24");
1227 assert!(is_matching_backtracking(
1228 underlying_topic,
1229 composite_pattern
1230 ));
1231 }
1232
1233 #[rstest]
1234 fn test_book_deltas_pattern_for_non_composite_is_literal(
1235 mut switchboard: MessagingSwitchboard,
1236 instrument_id: InstrumentId,
1237 ) {
1238 let pattern = switchboard.get_book_deltas_pattern(instrument_id);
1239 assert_eq!(pattern.as_ref(), "data.book.deltas.XCME.ESZ24");
1240 }
1241
1242 type PatternFn = fn(&mut MessagingSwitchboard, InstrumentId) -> MStr<Pattern>;
1243
1244 #[rstest]
1245 #[case::book_depth(
1246 MessagingSwitchboard::get_book_depth_pattern as PatternFn,
1247 "data.book.depth.XCME.ESZ24",
1248 )]
1249 fn test_pattern_for_non_composite_is_literal(
1250 mut switchboard: MessagingSwitchboard,
1251 instrument_id: InstrumentId,
1252 #[case] pattern_fn: PatternFn,
1253 #[case] expected: &str,
1254 ) {
1255 let pattern = pattern_fn(&mut switchboard, instrument_id);
1256 assert_eq!(pattern.as_ref(), expected);
1257 }
1258
1259 #[rstest]
1260 fn test_book_snapshots_pattern_for_non_composite_is_literal(
1261 mut switchboard: MessagingSwitchboard,
1262 instrument_id: InstrumentId,
1263 ) {
1264 let interval_ms = NonZeroUsize::new(1000).unwrap();
1265 let pattern = switchboard.get_book_snapshots_pattern(instrument_id, interval_ms);
1266 assert_eq!(pattern.as_ref(), "data.book.snapshots.XCME.ESZ24.1000");
1267 }
1268
1269 #[rstest]
1270 #[case::book_deltas(MessagingSwitchboard::get_book_deltas_pattern as PatternFn)]
1271 #[case::book_depth(MessagingSwitchboard::get_book_depth_pattern as PatternFn)]
1272 fn test_pattern_function_is_idempotent(
1273 mut switchboard: MessagingSwitchboard,
1274 instrument_id: InstrumentId,
1275 #[case] pattern_fn: PatternFn,
1276 ) {
1277 let first = pattern_fn(&mut switchboard, instrument_id);
1278 let second = pattern_fn(&mut switchboard, instrument_id);
1279 assert_eq!(first, second);
1280 }
1281
1282 #[rstest]
1283 fn test_book_snapshots_pattern_is_idempotent(
1284 mut switchboard: MessagingSwitchboard,
1285 instrument_id: InstrumentId,
1286 ) {
1287 let interval_ms = NonZeroUsize::new(1000).unwrap();
1288 let first = switchboard.get_book_snapshots_pattern(instrument_id, interval_ms);
1289 let second = switchboard.get_book_snapshots_pattern(instrument_id, interval_ms);
1290 assert_eq!(first, second);
1291 }
1292
1293 #[rstest]
1294 fn test_composite_book_depth_pattern_uses_wildcard(mut switchboard: MessagingSwitchboard) {
1295 let composite_id = InstrumentId::from("ES.FUT.XCME");
1296 let underlying_id = InstrumentId::from("ESZ24.XCME");
1297
1298 let composite_pattern = switchboard.get_book_depth_pattern(composite_id);
1299 let underlying_topic = switchboard.get_book_depth_topic(underlying_id);
1300
1301 assert_eq!(composite_pattern.as_ref(), "data.book.depth.XCME.ES*");
1302 assert!(is_matching_backtracking(
1303 underlying_topic,
1304 composite_pattern
1305 ));
1306 }
1307
1308 #[rstest]
1309 fn test_composite_book_snapshots_pattern_uses_wildcard(mut switchboard: MessagingSwitchboard) {
1310 let composite_id = InstrumentId::from("ES.FUT.XCME");
1311 let underlying_id = InstrumentId::from("ESZ24.XCME");
1312 let interval_ms = NonZeroUsize::new(1000).unwrap();
1313
1314 let composite_pattern = switchboard.get_book_snapshots_pattern(composite_id, interval_ms);
1315 let underlying_topic = switchboard.get_book_snapshots_topic(underlying_id, interval_ms);
1316
1317 assert_eq!(
1318 composite_pattern.as_ref(),
1319 "data.book.snapshots.XCME.ES*.1000"
1320 );
1321 assert_eq!(
1322 underlying_topic.as_ref(),
1323 "data.book.snapshots.XCME.ESZ24.1000"
1324 );
1325 assert!(is_matching_backtracking(
1326 underlying_topic,
1327 composite_pattern
1328 ));
1329 }
1330}