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