nautilus_binance/spot/sbe/stream/
mod.rs1use std::{error::Error, fmt::Display};
28
29pub use nautilus_serialization::sbe::{GroupSize16Encoding, GroupSizeEncoding, decode_var_string8};
31
32use crate::spot::sbe::{cursor::SbeCursor, error::SbeDecodeError};
33
34mod best_bid_ask;
35mod depth_diff;
36mod depth_snapshot;
37mod trades;
38
39pub use best_bid_ask::BestBidAskStreamEvent;
40pub use depth_diff::DepthDiffStreamEvent;
41pub use depth_snapshot::DepthSnapshotStreamEvent;
42pub use trades::{Trade, TradesStreamEvent};
43
44pub const STREAM_SCHEMA_ID: u16 = 1;
46
47pub const STREAM_SCHEMA_VERSION: u16 = 0;
49
50pub const MAX_GROUP_SIZE: usize = 10_000;
53
54pub mod template_id {
56 pub const TRADES_STREAM_EVENT: u16 = 10000;
57 pub const BEST_BID_ASK_STREAM_EVENT: u16 = 10001;
58 pub const DEPTH_SNAPSHOT_STREAM_EVENT: u16 = 10002;
59 pub const DEPTH_DIFF_STREAM_EVENT: u16 = 10003;
60}
61
62#[derive(Debug, Clone, PartialEq, Eq)]
64pub enum StreamDecodeError {
65 BufferTooShort { expected: usize, actual: usize },
67 GroupSizeTooLarge { count: usize, max: usize },
69 InvalidUtf8,
71 InvalidEnumValue { type_name: &'static str, value: u16 },
73 NumericOverflow { type_name: &'static str },
75 InvalidValue { field: &'static str },
77 SchemaMismatch { expected: u16, actual: u16 },
79 UnknownTemplateId(u16),
81 InvalidBlockLength { expected: u16, actual: u16 },
83}
84
85impl Display for StreamDecodeError {
86 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87 match self {
88 Self::BufferTooShort { expected, actual } => {
89 write!(
90 f,
91 "Buffer too short: expected {expected} bytes, was {actual}"
92 )
93 }
94 Self::GroupSizeTooLarge { count, max } => {
95 write!(f, "Group size {count} exceeds maximum {max}")
96 }
97 Self::InvalidUtf8 => write!(f, "Invalid UTF-8 in symbol"),
98 Self::InvalidEnumValue { type_name, value } => {
99 write!(f, "Invalid enum value {value} for {type_name}")
100 }
101 Self::NumericOverflow { type_name } => {
102 write!(f, "Numeric value overflows target type {type_name}")
103 }
104 Self::InvalidValue { field } => write!(f, "Invalid value for {field}"),
105 Self::SchemaMismatch { expected, actual } => {
106 write!(f, "Schema mismatch: expected {expected}, was {actual}")
107 }
108 Self::UnknownTemplateId(id) => write!(f, "Unknown template ID: {id}"),
109 Self::InvalidBlockLength { expected, actual } => {
110 write!(f, "Invalid block length: expected {expected}, was {actual}")
111 }
112 }
113 }
114}
115
116impl Error for StreamDecodeError {}
117
118impl From<SbeDecodeError> for StreamDecodeError {
119 fn from(err: SbeDecodeError) -> Self {
120 match err {
121 SbeDecodeError::BufferTooShort { expected, actual } => {
122 Self::BufferTooShort { expected, actual }
123 }
124 SbeDecodeError::SchemaMismatch { expected, actual } => {
125 Self::SchemaMismatch { expected, actual }
126 }
127 SbeDecodeError::VersionMismatch { .. } => Self::SchemaMismatch {
128 expected: STREAM_SCHEMA_VERSION,
129 actual: 0,
130 },
131 SbeDecodeError::UnknownTemplateId(id) => Self::UnknownTemplateId(id),
132 SbeDecodeError::GroupSizeTooLarge { count, max } => Self::GroupSizeTooLarge {
133 count: count as usize,
134 max: max as usize,
135 },
136 SbeDecodeError::InvalidBlockLength { expected, actual } => {
137 Self::InvalidBlockLength { expected, actual }
138 }
139 SbeDecodeError::InvalidUtf8 => Self::InvalidUtf8,
140 SbeDecodeError::InvalidEnumValue { type_name, value } => {
141 Self::InvalidEnumValue { type_name, value }
142 }
143 SbeDecodeError::NumericOverflow { type_name } => Self::NumericOverflow { type_name },
144 SbeDecodeError::InvalidValue { field } => Self::InvalidValue { field },
145 }
146 }
147}
148
149#[derive(Debug, Clone, Copy)]
151pub struct MessageHeader {
152 pub block_length: u16,
153 pub template_id: u16,
154 pub schema_id: u16,
155 pub version: u16,
156}
157
158impl MessageHeader {
159 pub const ENCODED_LENGTH: usize = 8;
160
161 pub fn decode(buf: &[u8]) -> Result<Self, StreamDecodeError> {
167 if buf.len() < Self::ENCODED_LENGTH {
168 return Err(StreamDecodeError::BufferTooShort {
169 expected: Self::ENCODED_LENGTH,
170 actual: buf.len(),
171 });
172 }
173 Ok(Self {
174 block_length: u16::from_le_bytes([buf[0], buf[1]]),
175 template_id: u16::from_le_bytes([buf[2], buf[3]]),
176 schema_id: u16::from_le_bytes([buf[4], buf[5]]),
177 version: u16::from_le_bytes([buf[6], buf[7]]),
178 })
179 }
180
181 pub fn validate_schema(&self) -> Result<(), StreamDecodeError> {
187 if self.schema_id != STREAM_SCHEMA_ID {
188 return Err(StreamDecodeError::SchemaMismatch {
189 expected: STREAM_SCHEMA_ID,
190 actual: self.schema_id,
191 });
192 }
193 Ok(())
194 }
195}
196
197#[derive(Debug, Clone, Copy)]
199pub struct PriceLevel {
200 pub price_mantissa: i64,
202 pub qty_mantissa: i64,
204}
205
206impl PriceLevel {
207 pub const ENCODED_LENGTH: usize = 16;
208
209 pub(crate) fn decode_group(cursor: &mut SbeCursor<'_>) -> Result<Vec<Self>, SbeDecodeError> {
210 let (block_length, count) = cursor.read_group_header_16()?;
211 if usize::from(block_length) < Self::ENCODED_LENGTH {
212 return Err(SbeDecodeError::InvalidBlockLength {
213 expected: Self::ENCODED_LENGTH as u16,
214 actual: block_length,
215 });
216 }
217
218 cursor.read_group(block_length, u32::from(count), |entry| {
219 let mut entry = SbeCursor::new(entry.read_bytes(Self::ENCODED_LENGTH)?);
220 Self::decode(&mut entry)
221 })
222 }
223
224 pub fn decode(cursor: &mut SbeCursor<'_>) -> Result<Self, SbeDecodeError> {
230 Ok(Self {
231 price_mantissa: cursor.read_i64_le()?,
232 qty_mantissa: cursor.read_i64_le()?,
233 })
234 }
235}
236
237#[inline]
239#[must_use]
240pub fn mantissa_to_f64(mantissa: i64, exponent: i8) -> f64 {
241 mantissa as f64 * 10_f64.powi(exponent as i32)
242}
243
244#[cfg(test)]
245mod tests {
246 use rstest::rstest;
247
248 use super::*;
249 use crate::spot::sbe::error::SbeDecodeError;
250
251 #[rstest]
252 fn test_mantissa_to_f64() {
253 assert!((mantissa_to_f64(12345, -2) - 123.45).abs() < 1e-10);
254 assert!((mantissa_to_f64(100, 0) - 100.0).abs() < 1e-10);
255 assert!((mantissa_to_f64(5, 3) - 5000.0).abs() < 1e-10);
256 }
257
258 #[rstest]
259 fn test_message_header_too_short() {
260 let buf = [0u8; 7];
261 let err = MessageHeader::decode(&buf).unwrap_err();
262 assert_eq!(
263 err,
264 StreamDecodeError::BufferTooShort {
265 expected: 8,
266 actual: 7
267 }
268 );
269 }
270
271 #[rstest]
272 fn test_group_size_too_large() {
273 let mut buf = [0u8; 6];
275 let count = (MAX_GROUP_SIZE + 1) as u32;
276 buf[2..6].copy_from_slice(&count.to_le_bytes());
277
278 let err = GroupSizeEncoding::decode(&buf).unwrap_err();
279 assert!(matches!(err, SbeDecodeError::GroupSizeTooLarge { .. }));
280 }
281
282 #[rstest]
283 fn test_decode_var_string8_empty_buffer() {
284 let err = decode_var_string8(&[]).unwrap_err();
285 assert!(matches!(err, SbeDecodeError::BufferTooShort { .. }));
286 }
287
288 #[rstest]
289 fn test_decode_var_string8_truncated() {
290 let buf = [10u8, b'H', b'E', b'L', b'L'];
292 let err = decode_var_string8(&buf).unwrap_err();
293 assert!(matches!(err, SbeDecodeError::BufferTooShort { .. }));
294 }
295
296 #[rstest]
297 fn test_decode_var_string8_valid() {
298 let buf = [5u8, b'H', b'E', b'L', b'L', b'O'];
299 let (s, consumed) = decode_var_string8(&buf).unwrap();
300 assert_eq!(s, "HELLO");
301 assert_eq!(consumed, 6);
302 }
303
304 #[rstest]
305 fn test_schema_validation() {
306 let header = MessageHeader {
307 block_length: 50,
308 template_id: 10001,
309 schema_id: 99, version: 0,
311 };
312 let err = header.validate_schema().unwrap_err();
313 assert_eq!(
314 err,
315 StreamDecodeError::SchemaMismatch {
316 expected: STREAM_SCHEMA_ID,
317 actual: 99
318 }
319 );
320 }
321
322 #[rstest]
323 fn test_decode_error_conversion_preserves_new_variants() {
324 assert_eq!(
325 StreamDecodeError::from(SbeDecodeError::InvalidEnumValue {
326 type_name: "AggressorSide",
327 value: 99,
328 }),
329 StreamDecodeError::InvalidEnumValue {
330 type_name: "AggressorSide",
331 value: 99,
332 }
333 );
334 assert_eq!(
335 StreamDecodeError::from(SbeDecodeError::NumericOverflow { type_name: "Price" }),
336 StreamDecodeError::NumericOverflow { type_name: "Price" }
337 );
338 assert_eq!(
339 StreamDecodeError::from(SbeDecodeError::InvalidValue {
340 field: "Price.precision",
341 }),
342 StreamDecodeError::InvalidValue {
343 field: "Price.precision",
344 }
345 );
346 }
347}