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