nautilus_binance/spot/sbe/stream/
depth_snapshot.rs1use ustr::Ustr;
32
33use super::{MessageHeader, PriceLevel, StreamDecodeError};
34use crate::spot::sbe::cursor::SbeCursor;
35
36#[derive(Debug, Clone)]
38pub struct DepthSnapshotStreamEvent {
39 pub event_time_us: i64,
41 pub book_update_id: i64,
43 pub price_exponent: i8,
45 pub qty_exponent: i8,
47 pub bids: Vec<PriceLevel>,
49 pub asks: Vec<PriceLevel>,
51 pub symbol: Ustr,
53}
54
55impl DepthSnapshotStreamEvent {
56 pub const BLOCK_LENGTH: usize = 18;
58
59 pub fn decode(buf: &[u8]) -> Result<Self, StreamDecodeError> {
66 let header = MessageHeader::decode(buf)?;
67 header.validate_schema()?;
68 Self::decode_validated(buf)
69 }
70
71 pub(crate) fn decode_validated(buf: &[u8]) -> Result<Self, StreamDecodeError> {
73 let mut cursor = SbeCursor::new_at(buf, MessageHeader::ENCODED_LENGTH);
74 Self::decode_body(&mut cursor)
75 }
76
77 #[inline]
78 fn decode_body(cursor: &mut SbeCursor<'_>) -> Result<Self, StreamDecodeError> {
79 let event_time_us = cursor.read_i64_le()?;
80 let book_update_id = cursor.read_i64_le()?;
81 let price_exponent = cursor.read_i8()?;
82 let qty_exponent = cursor.read_i8()?;
83
84 let bids = PriceLevel::decode_group(cursor)?;
85 let asks = PriceLevel::decode_group(cursor)?;
86
87 let symbol = Ustr::from(cursor.read_var_string8_ref()?);
88
89 Ok(Self {
90 event_time_us,
91 book_update_id,
92 price_exponent,
93 qty_exponent,
94 bids,
95 asks,
96 symbol,
97 })
98 }
99
100 #[inline]
102 #[must_use]
103 pub fn level_price(&self, level: &PriceLevel) -> f64 {
104 super::mantissa_to_f64(level.price_mantissa, self.price_exponent)
105 }
106
107 #[inline]
109 #[must_use]
110 pub fn level_qty(&self, level: &PriceLevel) -> f64 {
111 super::mantissa_to_f64(level.qty_mantissa, self.qty_exponent)
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use rstest::rstest;
118
119 use super::*;
120 use crate::spot::sbe::stream::{STREAM_SCHEMA_ID, template_id};
121
122 fn make_valid_buffer(num_bids: usize, num_asks: usize) -> Vec<u8> {
123 let level_block_len = 16u16;
124 let body_size = 18
125 + 4
126 + (num_bids * level_block_len as usize)
127 + 4
128 + (num_asks * level_block_len as usize)
129 + 8;
130 let mut buf = vec![0u8; 8 + body_size];
131
132 buf[0..2].copy_from_slice(&18u16.to_le_bytes()); buf[2..4].copy_from_slice(&template_id::DEPTH_SNAPSHOT_STREAM_EVENT.to_le_bytes());
135 buf[4..6].copy_from_slice(&STREAM_SCHEMA_ID.to_le_bytes());
136 buf[6..8].copy_from_slice(&0u16.to_le_bytes()); let body = &mut buf[8..];
140 body[0..8].copy_from_slice(&1000000i64.to_le_bytes()); body[8..16].copy_from_slice(&12345i64.to_le_bytes()); body[16] = (-2i8) as u8; body[17] = (-8i8) as u8; let mut offset = 18;
146
147 body[offset..offset + 2].copy_from_slice(&level_block_len.to_le_bytes());
149 body[offset + 2..offset + 4].copy_from_slice(&(num_bids as u16).to_le_bytes());
150 offset += 4;
151
152 for i in 0..num_bids {
154 body[offset..offset + 8].copy_from_slice(&(4200000i64 - i as i64 * 100).to_le_bytes());
155 body[offset + 8..offset + 16].copy_from_slice(&100000000i64.to_le_bytes());
156 offset += level_block_len as usize;
157 }
158
159 body[offset..offset + 2].copy_from_slice(&level_block_len.to_le_bytes());
161 body[offset + 2..offset + 4].copy_from_slice(&(num_asks as u16).to_le_bytes());
162 offset += 4;
163
164 for i in 0..num_asks {
166 body[offset..offset + 8].copy_from_slice(&(4200100i64 + i as i64 * 100).to_le_bytes());
167 body[offset + 8..offset + 16].copy_from_slice(&200000000i64.to_le_bytes());
168 offset += level_block_len as usize;
169 }
170
171 body[offset] = 7;
173 body[offset + 1..offset + 8].copy_from_slice(b"BTCUSDT");
174
175 buf
176 }
177
178 #[rstest]
179 fn test_decode_valid() {
180 let buf = make_valid_buffer(5, 5);
181 let event = DepthSnapshotStreamEvent::decode(&buf).unwrap();
182
183 assert_eq!(event.event_time_us, 1000000);
184 assert_eq!(event.book_update_id, 12345);
185 assert_eq!(event.bids.len(), 5);
186 assert_eq!(event.asks.len(), 5);
187 assert_eq!(event.symbol, "BTCUSDT");
188 }
189
190 #[rstest]
191 fn test_decode_empty_books() {
192 let buf = make_valid_buffer(0, 0);
193 let event = DepthSnapshotStreamEvent::decode(&buf).unwrap();
194
195 assert!(event.bids.is_empty());
196 assert!(event.asks.is_empty());
197 }
198
199 #[rstest]
200 #[case::bids(false)]
201 #[case::asks(true)]
202 fn test_decode_rejects_undersized_depth_blocks(
203 #[case] asks: bool,
204 #[values(0, 1, 10_000)] count: u16,
205 ) {
206 for block_length in 0..PriceLevel::ENCODED_LENGTH as u16 {
207 let mut buf = make_valid_buffer(1, 1);
208
209 let offset = MessageHeader::ENCODED_LENGTH
210 + DepthSnapshotStreamEvent::BLOCK_LENGTH
211 + if asks {
212 4 + PriceLevel::ENCODED_LENGTH
213 } else {
214 0
215 };
216
217 buf[offset..offset + 2].copy_from_slice(&block_length.to_le_bytes());
218 buf[offset + 2..offset + 4].copy_from_slice(&u16::to_le_bytes(count));
219
220 let expected = StreamDecodeError::InvalidBlockLength {
221 expected: PriceLevel::ENCODED_LENGTH as u16,
222 actual: block_length,
223 };
224
225 assert_eq!(
226 DepthSnapshotStreamEvent::decode(&buf).unwrap_err(),
227 expected
228 );
229 assert_eq!(
230 DepthSnapshotStreamEvent::decode_validated(&buf).unwrap_err(),
231 expected
232 );
233 }
234 }
235
236 #[rstest]
237 fn test_decode_depth_extensions_and_symbol() {
238 let mut buf = make_valid_buffer(2, 2);
239 let width = PriceLevel::ENCODED_LENGTH;
240 let bids_header = MessageHeader::ENCODED_LENGTH + DepthSnapshotStreamEvent::BLOCK_LENGTH;
241 let asks_header = bids_header + 4 + 2 * width;
242 for header in [asks_header, bids_header] {
243 for index in (0..2).rev() {
244 let end = header + 4 + (index + 1) * width;
245 buf.splice(end..end, [0xFF; 3]);
246 }
247
248 buf[header..header + 2].copy_from_slice(&((width + 3) as u16).to_le_bytes());
249 }
250
251 let event = DepthSnapshotStreamEvent::decode(&buf).unwrap();
252
253 assert_eq!(event.event_time_us, 1_000_000);
254 assert_eq!(event.book_update_id, 12_345);
255 assert_eq!(event.price_exponent, -2);
256 assert_eq!(event.qty_exponent, -8);
257 assert_eq!(
258 event
259 .bids
260 .iter()
261 .map(|level| (level.price_mantissa, level.qty_mantissa))
262 .collect::<Vec<_>>(),
263 vec![(4_200_000, 100_000_000), (4_199_900, 100_000_000)]
264 );
265 assert_eq!(
266 event
267 .asks
268 .iter()
269 .map(|level| (level.price_mantissa, level.qty_mantissa))
270 .collect::<Vec<_>>(),
271 vec![(4_200_100, 200_000_000), (4_200_200, 200_000_000)]
272 );
273 assert_eq!(event.symbol, "BTCUSDT");
274 }
275
276 #[rstest]
277 fn test_decode_truncated() {
278 let mut buf = make_valid_buffer(10, 10);
279 buf.truncate(100); let err = DepthSnapshotStreamEvent::decode(&buf).unwrap_err();
281 assert!(matches!(err, StreamDecodeError::BufferTooShort { .. }));
282 }
283
284 #[rstest]
285 fn test_decode_wrong_schema() {
286 let mut buf = make_valid_buffer(5, 5);
287 buf[4..6].copy_from_slice(&99u16.to_le_bytes());
288 let err = DepthSnapshotStreamEvent::decode(&buf).unwrap_err();
289 assert!(matches!(err, StreamDecodeError::SchemaMismatch { .. }));
290 }
291
292 #[rstest]
293 fn test_decode_validated_matches_decode() {
294 let buf = make_valid_buffer(2, 3);
295
296 let decode_event = DepthSnapshotStreamEvent::decode(&buf).unwrap();
297 let validated_event = DepthSnapshotStreamEvent::decode_validated(&buf).unwrap();
298
299 assert_eq!(validated_event.event_time_us, decode_event.event_time_us);
300 assert_eq!(validated_event.book_update_id, decode_event.book_update_id);
301 assert_eq!(validated_event.price_exponent, decode_event.price_exponent);
302 assert_eq!(validated_event.qty_exponent, decode_event.qty_exponent);
303 assert_eq!(validated_event.bids.len(), decode_event.bids.len());
304 assert_eq!(validated_event.asks.len(), decode_event.asks.len());
305 assert_eq!(validated_event.symbol, decode_event.symbol);
306 }
307}