nautilus_binance/common/sbe/stream/
best_bid_ask.rs

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