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