nautilus_binance/common/sbe/stream/
best_bid_ask.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 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//! BestBidAsk stream event decoder.
17//!
18//! Message layout (after 8-byte header):
19//! - eventTime: i64 (microseconds)
20//! - bookUpdateId: i64
21//! - priceExponent: i8
22//! - qtyExponent: i8
23//! - bidPrice: i64 (mantissa)
24//! - bidQty: i64 (mantissa)
25//! - askPrice: i64 (mantissa)
26//! - askQty: i64 (mantissa)
27//! - symbol: varString8
28
29use super::{MessageHeader, StreamDecodeError, decode_var_string8, read_i8, read_i64_le};
30
31/// Best bid/ask stream event.
32#[derive(Debug, Clone)]
33pub struct BestBidAskStreamEvent {
34    /// Event timestamp in microseconds.
35    pub event_time_us: i64,
36    /// Book update ID for sequencing.
37    pub book_update_id: i64,
38    /// Price exponent (prices = mantissa * 10^exponent).
39    pub price_exponent: i8,
40    /// Quantity exponent (quantities = mantissa * 10^exponent).
41    pub qty_exponent: i8,
42    /// Best bid price mantissa.
43    pub bid_price_mantissa: i64,
44    /// Best bid quantity mantissa.
45    pub bid_qty_mantissa: i64,
46    /// Best ask price mantissa.
47    pub ask_price_mantissa: i64,
48    /// Best ask quantity mantissa.
49    pub ask_qty_mantissa: i64,
50    /// Trading symbol.
51    pub symbol: String,
52}
53
54impl BestBidAskStreamEvent {
55    /// Fixed block length (excluding header and variable-length data).
56    pub const BLOCK_LENGTH: usize = 50;
57
58    /// Minimum buffer size needed (header + block + 1-byte string length).
59    pub const MIN_BUFFER_SIZE: usize = MessageHeader::ENCODED_LENGTH + Self::BLOCK_LENGTH + 1;
60
61    /// Decode from SBE buffer (including 8-byte header).
62    ///
63    /// # Errors
64    ///
65    /// Returns error if buffer is too short or contains invalid data.
66    pub fn decode(buf: &[u8]) -> Result<Self, StreamDecodeError> {
67        let header = MessageHeader::decode(buf)?;
68        header.validate_schema()?;
69
70        let body = &buf[MessageHeader::ENCODED_LENGTH..];
71
72        if body.len() < Self::BLOCK_LENGTH + 1 {
73            return Err(StreamDecodeError::BufferTooShort {
74                expected: Self::MIN_BUFFER_SIZE,
75                actual: buf.len(),
76            });
77        }
78
79        let event_time_us = read_i64_le(body, 0)?;
80        let book_update_id = read_i64_le(body, 8)?;
81        let price_exponent = read_i8(body, 16)?;
82        let qty_exponent = read_i8(body, 17)?;
83        let bid_price_mantissa = read_i64_le(body, 18)?;
84        let bid_qty_mantissa = read_i64_le(body, 26)?;
85        let ask_price_mantissa = read_i64_le(body, 34)?;
86        let ask_qty_mantissa = read_i64_le(body, 42)?;
87
88        let (symbol, _) = decode_var_string8(&body[50..])?;
89
90        Ok(Self {
91            event_time_us,
92            book_update_id,
93            price_exponent,
94            qty_exponent,
95            bid_price_mantissa,
96            bid_qty_mantissa,
97            ask_price_mantissa,
98            ask_qty_mantissa,
99            symbol,
100        })
101    }
102
103    /// Get bid price as f64.
104    #[inline]
105    #[must_use]
106    pub fn bid_price(&self) -> f64 {
107        super::mantissa_to_f64(self.bid_price_mantissa, self.price_exponent)
108    }
109
110    /// Get bid quantity as f64.
111    #[inline]
112    #[must_use]
113    pub fn bid_qty(&self) -> f64 {
114        super::mantissa_to_f64(self.bid_qty_mantissa, self.qty_exponent)
115    }
116
117    /// Get ask price as f64.
118    #[inline]
119    #[must_use]
120    pub fn ask_price(&self) -> f64 {
121        super::mantissa_to_f64(self.ask_price_mantissa, self.price_exponent)
122    }
123
124    /// Get ask quantity as f64.
125    #[inline]
126    #[must_use]
127    pub fn ask_qty(&self) -> f64 {
128        super::mantissa_to_f64(self.ask_qty_mantissa, self.qty_exponent)
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use rstest::rstest;
135
136    use super::*;
137    use crate::common::sbe::stream::{STREAM_SCHEMA_ID, template_id};
138
139    fn make_valid_buffer() -> Vec<u8> {
140        let mut buf = vec![0u8; 70];
141
142        // Header
143        buf[0..2].copy_from_slice(&50u16.to_le_bytes()); // block_length
144        buf[2..4].copy_from_slice(&template_id::BEST_BID_ASK_STREAM_EVENT.to_le_bytes());
145        buf[4..6].copy_from_slice(&STREAM_SCHEMA_ID.to_le_bytes());
146        buf[6..8].copy_from_slice(&0u16.to_le_bytes()); // version
147
148        // Body
149        let body = &mut buf[8..];
150        body[0..8].copy_from_slice(&1000000i64.to_le_bytes()); // event_time_us
151        body[8..16].copy_from_slice(&12345i64.to_le_bytes()); // book_update_id
152        body[16] = (-2i8) as u8; // price_exponent
153        body[17] = (-8i8) as u8; // qty_exponent
154        body[18..26].copy_from_slice(&4200000i64.to_le_bytes()); // bid_price
155        body[26..34].copy_from_slice(&100000000i64.to_le_bytes()); // bid_qty
156        body[34..42].copy_from_slice(&4200100i64.to_le_bytes()); // ask_price
157        body[42..50].copy_from_slice(&200000000i64.to_le_bytes()); // ask_qty
158
159        // Symbol: "BTCUSDT" (7 bytes)
160        body[50] = 7;
161        body[51..58].copy_from_slice(b"BTCUSDT");
162
163        buf
164    }
165
166    #[rstest]
167    fn test_decode_valid() {
168        let buf = make_valid_buffer();
169        let event = BestBidAskStreamEvent::decode(&buf).unwrap();
170
171        assert_eq!(event.event_time_us, 1000000);
172        assert_eq!(event.book_update_id, 12345);
173        assert_eq!(event.price_exponent, -2);
174        assert_eq!(event.qty_exponent, -8);
175        assert_eq!(event.symbol, "BTCUSDT");
176        assert!((event.bid_price() - 42000.0).abs() < 0.01);
177    }
178
179    #[rstest]
180    fn test_decode_truncated_header() {
181        let buf = [0u8; 5];
182        let err = BestBidAskStreamEvent::decode(&buf).unwrap_err();
183        assert!(matches!(err, StreamDecodeError::BufferTooShort { .. }));
184    }
185
186    #[rstest]
187    fn test_decode_truncated_body() {
188        let mut buf = make_valid_buffer();
189        buf.truncate(40); // Truncate in the middle of body
190        let err = BestBidAskStreamEvent::decode(&buf).unwrap_err();
191        assert!(matches!(err, StreamDecodeError::BufferTooShort { .. }));
192    }
193
194    #[rstest]
195    fn test_decode_wrong_schema() {
196        let mut buf = make_valid_buffer();
197        buf[4..6].copy_from_slice(&99u16.to_le_bytes()); // Wrong schema
198        let err = BestBidAskStreamEvent::decode(&buf).unwrap_err();
199        assert!(matches!(err, StreamDecodeError::SchemaMismatch { .. }));
200    }
201}