1use std::fmt::Display;
17
18use bytes::Bytes;
19use nautilus_core::UUID4;
20use nautilus_model::identifiers::TraderId;
21use serde::{Deserialize, Serialize, de::Error as _};
22use ustr::Ustr;
23
24use super::switchboard::CLOSE_TOPIC;
25use crate::enums::SerializationEncoding;
26
27#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
29pub enum BusPayloadCategory {
30 MarketData,
31 BuiltIn,
32 Other,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
37pub struct BusMessageSource {
38 pub publisher_id: TraderId,
40 pub instance_id: UUID4,
42 pub stream: Ustr,
44 pub sequence: u64,
46}
47
48#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
57pub enum BusPayloadType {
58 Custom(Ustr),
60 Instrument,
61 OrderBookDeltas,
62 OrderBookDepth,
63 QuoteTick,
64 TradeTick,
65 Bar,
66 MarkPriceUpdate,
67 IndexPriceUpdate,
68 FundingRateUpdate,
69 OptionGreeks,
70 AccountState,
71 OrderEvent,
72 PositionEvent,
73 PortfolioSnapshot,
74 SubscribeCommand,
75 UnsubscribeCommand,
76 RequestCommand,
77 DataResponse,
78 DataResponseEnvelope,
79 TradingCommand,
80 GenerateExecutionMassStatus,
81 OrderStatusReport,
82 FillReport,
83 PositionStatusReport,
84 ExecutionMassStatus,
85 #[cfg(feature = "defi")]
86 Block,
87 #[cfg(feature = "defi")]
88 Pool,
89 #[cfg(feature = "defi")]
90 PoolLiquidityUpdate,
91 #[cfg(feature = "defi")]
92 PoolFeeCollect,
93 #[cfg(feature = "defi")]
94 PoolFlash,
95}
96
97impl BusPayloadType {
98 pub(crate) const PUBLISHED_TYPES: &'static [Self] = &[
99 Self::Instrument,
100 Self::OrderBookDeltas,
101 Self::OrderBookDepth,
102 Self::QuoteTick,
103 Self::TradeTick,
104 Self::Bar,
105 Self::MarkPriceUpdate,
106 Self::IndexPriceUpdate,
107 Self::FundingRateUpdate,
108 Self::OptionGreeks,
109 Self::AccountState,
110 Self::OrderEvent,
111 Self::PositionEvent,
112 Self::PortfolioSnapshot,
113 Self::SubscribeCommand,
114 Self::UnsubscribeCommand,
115 Self::RequestCommand,
116 Self::DataResponse,
117 Self::DataResponseEnvelope,
118 Self::TradingCommand,
119 Self::GenerateExecutionMassStatus,
120 Self::OrderStatusReport,
121 Self::FillReport,
122 Self::PositionStatusReport,
123 Self::ExecutionMassStatus,
124 #[cfg(feature = "defi")]
125 Self::Block,
126 #[cfg(feature = "defi")]
127 Self::Pool,
128 #[cfg(feature = "defi")]
129 Self::PoolLiquidityUpdate,
130 #[cfg(feature = "defi")]
131 Self::PoolFeeCollect,
132 #[cfg(feature = "defi")]
133 Self::PoolFlash,
134 ];
135
136 #[must_use]
141 pub fn as_str(&self) -> &str {
142 match self {
143 Self::Custom(type_name) => type_name.as_str(),
144 Self::Instrument => "InstrumentAny",
145 Self::OrderBookDeltas => "OrderBookDeltas",
146 Self::OrderBookDepth => "OrderBookDepth",
147 Self::QuoteTick => "QuoteTick",
148 Self::TradeTick => "TradeTick",
149 Self::Bar => "Bar",
150 Self::MarkPriceUpdate => "MarkPriceUpdate",
151 Self::IndexPriceUpdate => "IndexPriceUpdate",
152 Self::FundingRateUpdate => "FundingRateUpdate",
153 Self::OptionGreeks => "OptionGreeks",
154 Self::AccountState => "AccountState",
155 Self::OrderEvent => "OrderEventAny",
156 Self::PositionEvent => "PositionEvent",
157 Self::PortfolioSnapshot => "PortfolioSnapshot",
158 Self::SubscribeCommand => "SubscribeCommand",
159 Self::UnsubscribeCommand => "UnsubscribeCommand",
160 Self::RequestCommand => "RequestCommand",
161 Self::DataResponse => "DataResponse",
162 Self::DataResponseEnvelope => "DataResponseEnvelope",
163 Self::TradingCommand => "TradingCommand",
164 Self::GenerateExecutionMassStatus => "GenerateExecutionMassStatus",
165 Self::OrderStatusReport => "OrderStatusReport",
166 Self::FillReport => "FillReport",
167 Self::PositionStatusReport => "PositionStatusReport",
168 Self::ExecutionMassStatus => "ExecutionMassStatus",
169 #[cfg(feature = "defi")]
170 Self::Block => "Block",
171 #[cfg(feature = "defi")]
172 Self::Pool => "Pool",
173 #[cfg(feature = "defi")]
174 Self::PoolLiquidityUpdate => "PoolLiquidityUpdate",
175 #[cfg(feature = "defi")]
176 Self::PoolFeeCollect => "PoolFeeCollect",
177 #[cfg(feature = "defi")]
178 Self::PoolFlash => "PoolFlash",
179 }
180 }
181
182 #[must_use]
188 pub fn from_name(name: &str) -> Self {
189 match name {
190 "InstrumentAny" => Self::Instrument,
191 "OrderBookDeltas" => Self::OrderBookDeltas,
192 "OrderBookDepth" => Self::OrderBookDepth,
193 "QuoteTick" => Self::QuoteTick,
194 "TradeTick" => Self::TradeTick,
195 "Bar" => Self::Bar,
196 "MarkPriceUpdate" => Self::MarkPriceUpdate,
197 "IndexPriceUpdate" => Self::IndexPriceUpdate,
198 "FundingRateUpdate" => Self::FundingRateUpdate,
199 "OptionGreeks" => Self::OptionGreeks,
200 "AccountState" => Self::AccountState,
201 "OrderEventAny" => Self::OrderEvent,
202 "PositionEvent" => Self::PositionEvent,
203 "PortfolioSnapshot" => Self::PortfolioSnapshot,
204 #[cfg(feature = "defi")]
205 "Block" => Self::Block,
206 #[cfg(feature = "defi")]
207 "Pool" => Self::Pool,
208 #[cfg(feature = "defi")]
209 "PoolLiquidityUpdate" => Self::PoolLiquidityUpdate,
210 #[cfg(feature = "defi")]
211 "PoolFeeCollect" => Self::PoolFeeCollect,
212 #[cfg(feature = "defi")]
213 "PoolFlash" => Self::PoolFlash,
214 other => Self::Custom(Ustr::from(other)),
215 }
216 }
217
218 #[must_use]
222 pub fn from_typed_name(name: &str) -> Option<Self> {
223 match name {
224 "SubscribeCommand" => Some(Self::SubscribeCommand),
225 "UnsubscribeCommand" => Some(Self::UnsubscribeCommand),
226 "RequestCommand" => Some(Self::RequestCommand),
227 "DataResponse" => Some(Self::DataResponse),
228 "DataResponseEnvelope" => Some(Self::DataResponseEnvelope),
229 "TradingCommand" => Some(Self::TradingCommand),
230 "GenerateExecutionMassStatus" => Some(Self::GenerateExecutionMassStatus),
231 "OrderStatusReport" => Some(Self::OrderStatusReport),
232 "FillReport" => Some(Self::FillReport),
233 "PositionStatusReport" => Some(Self::PositionStatusReport),
234 "ExecutionMassStatus" => Some(Self::ExecutionMassStatus),
235 _ => None,
236 }
237 }
238
239 #[must_use]
241 pub const fn is_typed_message(self) -> bool {
242 matches!(
243 self,
244 Self::SubscribeCommand
245 | Self::UnsubscribeCommand
246 | Self::RequestCommand
247 | Self::DataResponse
248 | Self::DataResponseEnvelope
249 | Self::TradingCommand
250 | Self::GenerateExecutionMassStatus
251 | Self::OrderStatusReport
252 | Self::FillReport
253 | Self::PositionStatusReport
254 | Self::ExecutionMassStatus
255 )
256 }
257
258 #[must_use]
260 pub fn category(&self) -> BusPayloadCategory {
261 if self.has_bus_binary_schema() {
262 BusPayloadCategory::MarketData
263 } else {
264 match self {
265 Self::AccountState
266 | Self::OrderEvent
267 | Self::PositionEvent
268 | Self::PortfolioSnapshot => BusPayloadCategory::BuiltIn,
269 _ => BusPayloadCategory::Other,
270 }
271 }
272 }
273
274 #[must_use]
276 pub fn supports(&self, encoding: SerializationEncoding) -> bool {
277 match encoding {
278 SerializationEncoding::Json | SerializationEncoding::MsgPack => true,
279 SerializationEncoding::Sbe => cfg!(feature = "sbe") && self.has_bus_binary_schema(),
280 }
281 }
282
283 fn has_bus_binary_schema(&self) -> bool {
284 matches!(
285 self,
286 Self::OrderBookDeltas
287 | Self::OrderBookDepth
288 | Self::QuoteTick
289 | Self::TradeTick
290 | Self::Bar
291 | Self::MarkPriceUpdate
292 | Self::IndexPriceUpdate
293 | Self::FundingRateUpdate
294 | Self::OptionGreeks
295 )
296 }
297}
298
299impl Display for BusPayloadType {
300 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
301 f.write_str(self.as_str())
302 }
303}
304
305impl Serialize for BusPayloadType {
306 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
307 if self.is_typed_message() {
308 TypedPayloadTypeRef {
309 typed: self.as_str(),
310 }
311 .serialize(serializer)
312 } else {
313 serializer.serialize_str(self.as_str())
314 }
315 }
316}
317
318impl<'de> Deserialize<'de> for BusPayloadType {
319 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
320 match BusPayloadTypeRepr::deserialize(deserializer)? {
321 BusPayloadTypeRepr::Name(name) => Ok(Self::from_name(&name)),
322 BusPayloadTypeRepr::Typed(value) => Self::from_typed_name(&value.typed)
323 .ok_or_else(|| D::Error::custom(format!("unknown typed payload: {}", value.typed))),
324 }
325 }
326}
327
328#[derive(Serialize)]
329struct TypedPayloadTypeRef<'a> {
330 typed: &'a str,
331}
332
333#[derive(Deserialize)]
334#[serde(deny_unknown_fields)]
335struct TypedPayloadType {
336 typed: String,
337}
338
339#[derive(Deserialize)]
340#[serde(untagged)]
341enum BusPayloadTypeRepr {
342 Name(String),
343 Typed(TypedPayloadType),
344}
345
346#[derive(Clone, Debug, Serialize, Deserialize)]
351#[cfg_attr(
352 feature = "python",
353 pyo3::pyclass(module = "nautilus_trader.common", from_py_object)
354)]
355#[cfg_attr(
356 feature = "python",
357 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.common")
358)]
359pub struct BusMessage {
360 pub topic: Ustr,
362 pub payload_type: BusPayloadType,
365 pub payload: Bytes,
367 pub encoding: SerializationEncoding,
370 #[serde(default, skip_serializing_if = "Option::is_none")]
372 pub source: Option<BusMessageSource>,
373}
374
375impl BusMessage {
376 pub fn new(
378 topic: Ustr,
379 payload_type: BusPayloadType,
380 payload: Bytes,
381 encoding: SerializationEncoding,
382 ) -> Self {
383 debug_assert!(!topic.is_empty());
384
385 Self {
386 topic,
387 payload_type,
388 payload,
389 encoding,
390 source: None,
391 }
392 }
393
394 pub fn with_str_topic<T: AsRef<str>>(
399 topic: T,
400 payload_type: BusPayloadType,
401 payload: Bytes,
402 encoding: SerializationEncoding,
403 ) -> Self {
404 Self::new(Ustr::from(topic.as_ref()), payload_type, payload, encoding)
405 }
406
407 pub fn new_close() -> Self {
409 Self::with_str_topic(
410 CLOSE_TOPIC,
411 BusPayloadType::Custom(Ustr::default()),
412 Bytes::new(),
413 SerializationEncoding::default(),
414 )
415 }
416}
417
418impl Display for BusMessage {
419 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
420 write!(
421 f,
422 "[{}] {} {} {}",
423 self.topic,
424 self.payload_type.as_str(),
425 String::from_utf8_lossy(&self.payload),
426 self.encoding
427 )
428 }
429}
430
431#[cfg(test)]
432mod tests {
433 use bytes::Bytes;
434 use rstest::rstest;
435
436 use super::*;
437
438 #[rstest]
439 #[case(false)]
440 #[case(true)]
441 fn test_source_round_trip_and_legacy_message_compatibility(#[case] with_source: bool) {
442 let mut message = BusMessage::with_str_topic(
443 "external.responses",
444 BusPayloadType::DataResponse,
445 Bytes::from_static(b"reply"),
446 SerializationEncoding::MsgPack,
447 );
448
449 if with_source {
450 message.source = Some(BusMessageSource {
451 publisher_id: "PUBLISHER-017".into(),
452 instance_id: UUID4::new(),
453 stream: "snapshot".into(),
454 sequence: 37,
455 });
456 }
457
458 let encoded = serde_json::to_value(&message).unwrap();
459 let decoded: BusMessage = serde_json::from_value(encoded.clone()).unwrap();
460
461 assert_eq!(encoded.get("source").is_some(), with_source);
462 assert_eq!(decoded.topic, message.topic);
463 assert_eq!(decoded.payload_type, message.payload_type);
464 assert_eq!(decoded.payload, message.payload);
465 assert_eq!(decoded.encoding, message.encoding);
466 assert_eq!(decoded.source, message.source);
467 }
468
469 #[rstest]
470 #[case("test/topic", "payload data")]
471 #[case("events/trading", "Another payload")]
472 fn test_with_str_topic_str(#[case] topic: &str, #[case] payload_str: &str) {
473 let payload = Bytes::from(payload_str.to_string());
474
475 let message = BusMessage::with_str_topic(
476 topic,
477 BusPayloadType::QuoteTick,
478 payload.clone(),
479 SerializationEncoding::Json,
480 );
481
482 assert_eq!(message.topic, topic);
483 assert_eq!(message.payload_type, BusPayloadType::QuoteTick);
484 assert_eq!(message.encoding, SerializationEncoding::Json);
485 assert_eq!(message.payload, payload);
486 }
487
488 #[rstest]
489 fn test_with_str_topic_string() {
490 let topic_string = String::from("orders/new");
491 let payload = Bytes::from("order payload data");
492
493 let message = BusMessage::with_str_topic(
494 topic_string.clone(),
495 BusPayloadType::OrderEvent,
496 payload.clone(),
497 SerializationEncoding::MsgPack,
498 );
499
500 assert_eq!(message.topic, topic_string);
501 assert_eq!(message.payload_type, BusPayloadType::OrderEvent);
502 assert_eq!(message.encoding, SerializationEncoding::MsgPack);
503 assert_eq!(message.payload, payload);
504 }
505
506 #[rstest]
507 fn test_new_close() {
508 let message = BusMessage::new_close();
509
510 assert_eq!(message.topic, "CLOSE");
511 assert!(message.payload.is_empty());
512 }
513
514 #[rstest]
515 #[case(BusPayloadType::QuoteTick, BusPayloadCategory::MarketData)]
516 #[case(BusPayloadType::OrderBookDeltas, BusPayloadCategory::MarketData)]
517 #[case(BusPayloadType::AccountState, BusPayloadCategory::BuiltIn)]
518 #[case(BusPayloadType::OrderEvent, BusPayloadCategory::BuiltIn)]
519 #[case(BusPayloadType::Instrument, BusPayloadCategory::Other)]
520 #[case(BusPayloadType::OptionGreeks, BusPayloadCategory::MarketData)]
521 #[case(
522 BusPayloadType::Custom(Ustr::from("CustomPayload")),
523 BusPayloadCategory::Other
524 )]
525 fn bus_payload_type_category(
526 #[case] payload_type: BusPayloadType,
527 #[case] expected: BusPayloadCategory,
528 ) {
529 assert_eq!(payload_type.category(), expected);
530 }
531
532 #[rstest]
533 #[case(BusPayloadType::QuoteTick, SerializationEncoding::Json, true)]
534 #[case(BusPayloadType::QuoteTick, SerializationEncoding::MsgPack, true)]
535 #[cfg_attr(
536 feature = "sbe",
537 case(BusPayloadType::QuoteTick, SerializationEncoding::Sbe, true)
538 )]
539 #[cfg_attr(
540 not(feature = "sbe"),
541 case(BusPayloadType::QuoteTick, SerializationEncoding::Sbe, false)
542 )]
543 #[case(BusPayloadType::AccountState, SerializationEncoding::Json, true)]
544 #[case(BusPayloadType::Instrument, SerializationEncoding::Sbe, false)]
545 #[cfg_attr(
546 feature = "sbe",
547 case(BusPayloadType::OptionGreeks, SerializationEncoding::Sbe, true)
548 )]
549 #[cfg_attr(
550 not(feature = "sbe"),
551 case(BusPayloadType::OptionGreeks, SerializationEncoding::Sbe, false)
552 )]
553 #[case(
554 BusPayloadType::Custom(Ustr::from("CustomPayload")),
555 SerializationEncoding::MsgPack,
556 true
557 )]
558 #[case(
559 BusPayloadType::Custom(Ustr::from("CustomPayload")),
560 SerializationEncoding::Sbe,
561 false
562 )]
563 fn bus_payload_type_supports_encoding(
564 #[case] payload_type: BusPayloadType,
565 #[case] encoding: SerializationEncoding,
566 #[case] expected: bool,
567 ) {
568 assert_eq!(payload_type.supports(encoding), expected);
569 }
570
571 #[rstest]
572 fn bus_payload_type_accepts_canonical_depth_name() {
573 assert_eq!(
574 BusPayloadType::from_name("OrderBookDepth"),
575 BusPayloadType::OrderBookDepth
576 );
577 assert_eq!(
578 BusPayloadType::from_name("OrderBookDepth10"),
579 BusPayloadType::Custom(Ustr::from("OrderBookDepth10")),
580 );
581 }
582
583 #[rstest]
584 fn typed_payload_types_use_json_msgpack_policy() {
585 let payload_types = [
586 (BusPayloadType::SubscribeCommand, "SubscribeCommand"),
587 (BusPayloadType::UnsubscribeCommand, "UnsubscribeCommand"),
588 (BusPayloadType::RequestCommand, "RequestCommand"),
589 (BusPayloadType::DataResponse, "DataResponse"),
590 (BusPayloadType::TradingCommand, "TradingCommand"),
591 (
592 BusPayloadType::GenerateExecutionMassStatus,
593 "GenerateExecutionMassStatus",
594 ),
595 (BusPayloadType::OrderStatusReport, "OrderStatusReport"),
596 (BusPayloadType::FillReport, "FillReport"),
597 (BusPayloadType::PositionStatusReport, "PositionStatusReport"),
598 (BusPayloadType::ExecutionMassStatus, "ExecutionMassStatus"),
599 ];
600
601 for (payload_type, name) in payload_types {
602 let custom_payload_type = BusPayloadType::Custom(Ustr::from(name));
603 let typed_json =
604 serde_json::to_value(payload_type).expect("typed payload type must serialize");
605 let custom_json = serde_json::to_value(custom_payload_type)
606 .expect("custom payload type must serialize");
607
608 assert_eq!(payload_type.as_str(), name);
609 assert_eq!(BusPayloadType::from_name(name), custom_payload_type);
610 assert_eq!(BusPayloadType::from_typed_name(name), Some(payload_type));
611 assert_eq!(typed_json, serde_json::json!({ "typed": name }));
612 assert_eq!(custom_json, serde_json::json!(name));
613 assert_eq!(
614 serde_json::from_value::<BusPayloadType>(typed_json)
615 .expect("typed payload type must deserialize"),
616 payload_type
617 );
618 assert_eq!(
619 serde_json::from_value::<BusPayloadType>(custom_json)
620 .expect("custom payload type must deserialize"),
621 custom_payload_type
622 );
623 assert!(payload_type.is_typed_message());
624 assert!(!custom_payload_type.is_typed_message());
625 assert_eq!(payload_type.category(), BusPayloadCategory::Other);
626 assert!(payload_type.supports(SerializationEncoding::Json));
627 assert!(payload_type.supports(SerializationEncoding::MsgPack));
628 assert!(!payload_type.supports(SerializationEncoding::Sbe));
629 }
630 }
631}