Skip to main content

nautilus_common/msgbus/
switchboard.rs

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