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