use ustr::Ustr;
use super::{MessageHeader, StreamDecodeError};
use crate::spot::sbe::cursor::SbeCursor;
#[derive(Debug, Clone)]
pub struct BestBidAskStreamEvent {
pub event_time_us: i64,
pub book_update_id: i64,
pub price_exponent: i8,
pub qty_exponent: i8,
pub bid_price_mantissa: i64,
pub bid_qty_mantissa: i64,
pub ask_price_mantissa: i64,
pub ask_qty_mantissa: i64,
pub symbol: Ustr,
}
impl BestBidAskStreamEvent {
pub const BLOCK_LENGTH: usize = 50;
pub const MIN_BUFFER_SIZE: usize = MessageHeader::ENCODED_LENGTH + Self::BLOCK_LENGTH + 1;
pub fn decode(buf: &[u8]) -> Result<Self, StreamDecodeError> {
let header = MessageHeader::decode(buf)?;
header.validate_schema()?;
Self::decode_validated(buf)
}
pub(crate) fn decode_validated(buf: &[u8]) -> Result<Self, StreamDecodeError> {
let mut cursor = SbeCursor::new_at(buf, MessageHeader::ENCODED_LENGTH);
Self::decode_body(&mut cursor)
}
#[inline]
fn decode_body(cursor: &mut SbeCursor<'_>) -> Result<Self, StreamDecodeError> {
let event_time_us = cursor.read_i64_le()?;
let book_update_id = cursor.read_i64_le()?;
let price_exponent = cursor.read_i8()?;
let qty_exponent = cursor.read_i8()?;
let bid_price_mantissa = cursor.read_i64_le()?;
let bid_qty_mantissa = cursor.read_i64_le()?;
let ask_price_mantissa = cursor.read_i64_le()?;
let ask_qty_mantissa = cursor.read_i64_le()?;
let symbol = Ustr::from(cursor.read_var_string8_ref()?);
Ok(Self {
event_time_us,
book_update_id,
price_exponent,
qty_exponent,
bid_price_mantissa,
bid_qty_mantissa,
ask_price_mantissa,
ask_qty_mantissa,
symbol,
})
}
#[inline]
#[must_use]
pub fn bid_price(&self) -> f64 {
super::mantissa_to_f64(self.bid_price_mantissa, self.price_exponent)
}
#[inline]
#[must_use]
pub fn bid_qty(&self) -> f64 {
super::mantissa_to_f64(self.bid_qty_mantissa, self.qty_exponent)
}
#[inline]
#[must_use]
pub fn ask_price(&self) -> f64 {
super::mantissa_to_f64(self.ask_price_mantissa, self.price_exponent)
}
#[inline]
#[must_use]
pub fn ask_qty(&self) -> f64 {
super::mantissa_to_f64(self.ask_qty_mantissa, self.qty_exponent)
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
use crate::spot::sbe::stream::{STREAM_SCHEMA_ID, template_id};
fn make_valid_buffer() -> Vec<u8> {
let mut buf = vec![0u8; 70];
buf[0..2].copy_from_slice(&50u16.to_le_bytes()); buf[2..4].copy_from_slice(&template_id::BEST_BID_ASK_STREAM_EVENT.to_le_bytes());
buf[4..6].copy_from_slice(&STREAM_SCHEMA_ID.to_le_bytes());
buf[6..8].copy_from_slice(&0u16.to_le_bytes());
let body = &mut buf[8..];
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; body[18..26].copy_from_slice(&4200000i64.to_le_bytes()); body[26..34].copy_from_slice(&100000000i64.to_le_bytes()); body[34..42].copy_from_slice(&4200100i64.to_le_bytes()); body[42..50].copy_from_slice(&200000000i64.to_le_bytes());
body[50] = 7;
body[51..58].copy_from_slice(b"BTCUSDT");
buf
}
#[rstest]
fn test_decode_valid() {
let buf = make_valid_buffer();
let event = BestBidAskStreamEvent::decode(&buf).unwrap();
assert_eq!(event.event_time_us, 1000000);
assert_eq!(event.book_update_id, 12345);
assert_eq!(event.price_exponent, -2);
assert_eq!(event.qty_exponent, -8);
assert_eq!(event.symbol, "BTCUSDT");
assert!((event.bid_price() - 42000.0).abs() < 0.01);
}
#[rstest]
fn test_decode_truncated_header() {
let buf = [0u8; 5];
let err = BestBidAskStreamEvent::decode(&buf).unwrap_err();
assert!(matches!(err, StreamDecodeError::BufferTooShort { .. }));
}
#[rstest]
fn test_decode_truncated_body() {
let mut buf = make_valid_buffer();
buf.truncate(40); let err = BestBidAskStreamEvent::decode(&buf).unwrap_err();
assert!(matches!(err, StreamDecodeError::BufferTooShort { .. }));
}
#[rstest]
fn test_decode_wrong_schema() {
let mut buf = make_valid_buffer();
buf[4..6].copy_from_slice(&99u16.to_le_bytes()); let err = BestBidAskStreamEvent::decode(&buf).unwrap_err();
assert!(matches!(err, StreamDecodeError::SchemaMismatch { .. }));
}
#[rstest]
fn test_decode_validated_matches_decode() {
let buf = make_valid_buffer();
let decode_event = BestBidAskStreamEvent::decode(&buf).unwrap();
let validated_event = BestBidAskStreamEvent::decode_validated(&buf).unwrap();
assert_eq!(validated_event.event_time_us, decode_event.event_time_us);
assert_eq!(validated_event.book_update_id, decode_event.book_update_id);
assert_eq!(validated_event.price_exponent, decode_event.price_exponent);
assert_eq!(validated_event.qty_exponent, decode_event.qty_exponent);
assert_eq!(
validated_event.bid_price_mantissa,
decode_event.bid_price_mantissa
);
assert_eq!(validated_event.symbol, decode_event.symbol);
}
}