nautilus_binance/spot/sbe/stream/
trades.rs1use ustr::Ustr;
31
32use super::{MessageHeader, StreamDecodeError};
33use crate::spot::sbe::{cursor::SbeCursor, error::SbeDecodeError};
34
35#[derive(Debug, Clone, Copy)]
37pub struct Trade {
38 pub id: i64,
40 pub price_mantissa: i64,
42 pub qty_mantissa: i64,
44 pub is_buyer_maker: bool,
46}
47
48impl Trade {
49 pub const ENCODED_LENGTH: usize = 25;
51
52 fn decode(cursor: &mut SbeCursor<'_>) -> Result<Self, SbeDecodeError> {
58 Ok(Self {
59 id: cursor.read_i64_le()?,
60 price_mantissa: cursor.read_i64_le()?,
61 qty_mantissa: cursor.read_i64_le()?,
62 is_buyer_maker: cursor.read_u8()? != 0,
63 })
64 }
65}
66
67#[derive(Debug, Clone)]
69pub struct TradesStreamEvent {
70 pub event_time_us: i64,
72 pub transact_time_us: i64,
74 pub price_exponent: i8,
76 pub qty_exponent: i8,
78 pub trades: Vec<Trade>,
80 pub symbol: Ustr,
82}
83
84impl TradesStreamEvent {
85 pub const BLOCK_LENGTH: usize = 18;
87
88 pub fn decode(buf: &[u8]) -> Result<Self, StreamDecodeError> {
95 let header = MessageHeader::decode(buf)?;
96 header.validate_schema()?;
97 Self::decode_validated(buf)
98 }
99
100 pub(crate) fn decode_validated(buf: &[u8]) -> Result<Self, StreamDecodeError> {
102 let mut cursor = SbeCursor::new_at(buf, MessageHeader::ENCODED_LENGTH);
103 Self::decode_body(&mut cursor)
104 }
105
106 #[inline]
107 fn decode_body(cursor: &mut SbeCursor<'_>) -> Result<Self, StreamDecodeError> {
108 let event_time_us = cursor.read_i64_le()?;
109 let transact_time_us = cursor.read_i64_le()?;
110 let price_exponent = cursor.read_i8()?;
111 let qty_exponent = cursor.read_i8()?;
112
113 let (block_length, num_in_group) = cursor.read_group_header()?;
114 if usize::from(block_length) < Trade::ENCODED_LENGTH {
115 return Err(StreamDecodeError::InvalidBlockLength {
116 expected: Trade::ENCODED_LENGTH as u16,
117 actual: block_length,
118 });
119 }
120
121 let block_length = usize::from(block_length);
122 let count = num_in_group as usize;
123 let entries = cursor.read_bytes(count * block_length)?;
124 let mut trades = Vec::with_capacity(count);
125
126 for entry in entries.chunks_exact(block_length) {
127 let mut entry = SbeCursor::new(entry);
128 trades.push(Trade::decode(&mut entry)?);
129 }
130
131 let symbol = Ustr::from(cursor.read_var_string8_ref()?);
132
133 Ok(Self {
134 event_time_us,
135 transact_time_us,
136 price_exponent,
137 qty_exponent,
138 trades,
139 symbol,
140 })
141 }
142
143 #[inline]
145 #[must_use]
146 pub fn trade_price(&self, trade: &Trade) -> f64 {
147 super::mantissa_to_f64(trade.price_mantissa, self.price_exponent)
148 }
149
150 #[inline]
152 #[must_use]
153 pub fn trade_qty(&self, trade: &Trade) -> f64 {
154 super::mantissa_to_f64(trade.qty_mantissa, self.qty_exponent)
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use rstest::rstest;
161
162 use super::*;
163 use crate::spot::sbe::stream::{STREAM_SCHEMA_ID, template_id};
164
165 fn make_valid_buffer(num_trades: usize) -> Vec<u8> {
166 let trade_block_len = 25u16;
167 let body_size = 18 + 6 + (num_trades * trade_block_len as usize) + 8; let mut buf = vec![0u8; 8 + body_size];
169
170 buf[0..2].copy_from_slice(&18u16.to_le_bytes()); buf[2..4].copy_from_slice(&template_id::TRADES_STREAM_EVENT.to_le_bytes());
173 buf[4..6].copy_from_slice(&STREAM_SCHEMA_ID.to_le_bytes());
174 buf[6..8].copy_from_slice(&0u16.to_le_bytes()); let body = &mut buf[8..];
178 body[0..8].copy_from_slice(&1000000i64.to_le_bytes()); body[8..16].copy_from_slice(&1000001i64.to_le_bytes()); body[16] = (-2i8) as u8; body[17] = (-8i8) as u8; body[18..20].copy_from_slice(&trade_block_len.to_le_bytes());
185 body[20..24].copy_from_slice(&(num_trades as u32).to_le_bytes());
186
187 let mut offset = 24;
189 for i in 0..num_trades {
190 body[offset..offset + 8].copy_from_slice(&(i as i64 + 1).to_le_bytes()); body[offset + 8..offset + 16].copy_from_slice(&4200000i64.to_le_bytes()); body[offset + 16..offset + 24].copy_from_slice(&100000000i64.to_le_bytes()); body[offset + 24] = u8::from(i % 2 == 0); offset += trade_block_len as usize;
195 }
196
197 body[offset] = 7;
199 body[offset + 1..offset + 8].copy_from_slice(b"BTCUSDT");
200
201 buf
202 }
203
204 #[rstest]
205 fn test_decode_valid_single_trade() {
206 let buf = make_valid_buffer(1);
207 let event = TradesStreamEvent::decode(&buf).unwrap();
208
209 assert_eq!(event.event_time_us, 1000000);
210 assert_eq!(event.transact_time_us, 1000001);
211 assert_eq!(event.trades.len(), 1);
212 assert_eq!(event.trades[0].id, 1);
213 assert!(event.trades[0].is_buyer_maker);
214 assert_eq!(event.symbol, "BTCUSDT");
215 }
216
217 #[rstest]
218 fn test_decode_valid_multiple_trades() {
219 let buf = make_valid_buffer(5);
220 let event = TradesStreamEvent::decode(&buf).unwrap();
221
222 assert_eq!(event.trades.len(), 5);
223 for (i, trade) in event.trades.iter().enumerate() {
224 assert_eq!(trade.id, i as i64 + 1);
225 }
226 }
227
228 #[rstest]
229 #[case::current(Trade::ENCODED_LENGTH as u16)]
230 #[case::extended(Trade::ENCODED_LENGTH as u16 + 3)]
231 fn test_decode_valid_empty_trades(#[case] block_length: u16) {
232 let mut buf = make_valid_buffer(0);
233 buf[26..28].copy_from_slice(&block_length.to_le_bytes());
234 let event = TradesStreamEvent::decode(&buf).unwrap();
235
236 assert_eq!(event.event_time_us, 1_000_000);
237 assert_eq!(event.transact_time_us, 1_000_001);
238 assert_eq!(event.price_exponent, -2);
239 assert_eq!(event.qty_exponent, -8);
240 assert!(event.trades.is_empty());
241 assert_eq!(event.symbol, "BTCUSDT");
242 }
243
244 #[rstest]
245 #[case::empty(0)]
246 #[case::single(1)]
247 #[case::maximum(10_000)]
248 fn test_decode_rejects_undersized_trade_blocks(#[case] count: u32) {
249 for block_length in 0..Trade::ENCODED_LENGTH as u16 {
250 let mut buf = make_valid_buffer(1);
251 buf[26..28].copy_from_slice(&block_length.to_le_bytes());
252 buf[28..32].copy_from_slice(&u32::to_le_bytes(count));
253
254 let expected = StreamDecodeError::InvalidBlockLength {
255 expected: Trade::ENCODED_LENGTH as u16,
256 actual: block_length,
257 };
258
259 assert_eq!(TradesStreamEvent::decode(&buf).unwrap_err(), expected);
260 assert_eq!(
261 TradesStreamEvent::decode_validated(&buf).unwrap_err(),
262 expected
263 );
264 }
265 }
266
267 #[rstest]
268 fn test_decode_trade_extensions_and_symbol() {
269 let mut buf = make_valid_buffer(2);
270 let width = Trade::ENCODED_LENGTH;
271 for index in (0..2).rev() {
272 let end = 32 + (index + 1) * width;
273 buf.splice(end..end, [0xFF; 3]);
274 }
275
276 buf[26..28].copy_from_slice(&((width + 3) as u16).to_le_bytes());
277 let event = TradesStreamEvent::decode(&buf).unwrap();
278
279 assert_eq!(event.event_time_us, 1_000_000);
280 assert_eq!(event.transact_time_us, 1_000_001);
281 assert_eq!(event.price_exponent, -2);
282 assert_eq!(event.qty_exponent, -8);
283 assert_eq!(
284 event
285 .trades
286 .iter()
287 .map(|trade| (
288 trade.id,
289 trade.price_mantissa,
290 trade.qty_mantissa,
291 trade.is_buyer_maker
292 ))
293 .collect::<Vec<_>>(),
294 vec![
295 (1, 4_200_000, 100_000_000, true),
296 (2, 4_200_000, 100_000_000, false),
297 ]
298 );
299 assert_eq!(event.symbol, "BTCUSDT");
300 }
301
302 #[rstest]
303 fn test_decode_truncated_trades() {
304 let mut buf = make_valid_buffer(3);
305 buf.truncate(50); let err = TradesStreamEvent::decode(&buf).unwrap_err();
307 assert!(matches!(err, StreamDecodeError::BufferTooShort { .. }));
308 }
309
310 #[rstest]
311 fn test_decode_wrong_schema() {
312 let mut buf = make_valid_buffer(1);
313 buf[4..6].copy_from_slice(&99u16.to_le_bytes());
314 let err = TradesStreamEvent::decode(&buf).unwrap_err();
315 assert!(matches!(err, StreamDecodeError::SchemaMismatch { .. }));
316 }
317
318 #[rstest]
319 fn test_decode_validated_matches_decode() {
320 let buf = make_valid_buffer(3);
321
322 let decode_event = TradesStreamEvent::decode(&buf).unwrap();
323 let validated_event = TradesStreamEvent::decode_validated(&buf).unwrap();
324
325 assert_eq!(validated_event.event_time_us, decode_event.event_time_us);
326 assert_eq!(
327 validated_event.transact_time_us,
328 decode_event.transact_time_us
329 );
330 assert_eq!(validated_event.price_exponent, decode_event.price_exponent);
331 assert_eq!(validated_event.qty_exponent, decode_event.qty_exponent);
332 assert_eq!(validated_event.trades.len(), decode_event.trades.len());
333 assert_eq!(validated_event.symbol, decode_event.symbol);
334 }
335}