Skip to main content

polyester/codecs/decode/
orderbook.rs

1//! Orderbook snapshot decoders.
2
3use super::money::{decode_price_ticks, decode_qty_scaled};
4use crate::errors::{Error, Result};
5use crate::models::{OrderbookData, OrderbookLevel};
6use crate::proto::orderbook::v1::{GetOrderBookResponse, PriceLevel};
7
8pub fn depth_enum_for_levels(depth: u32) -> crate::proto::orderbook::v1::Depth {
9    use crate::proto::orderbook::v1::Depth;
10    match depth {
11        0 => Depth::Depth5,
12        1 => Depth::Depth1,
13        2..=5 => Depth::Depth5,
14        6..=10 => Depth::Depth10,
15        11..=20 => Depth::Depth20,
16        21..=50 => Depth::Depth50,
17        51..=100 => Depth::Depth100,
18        101..=200 => Depth::Depth200,
19        201..=500 => Depth::Depth500,
20        _ => Depth::Depth1000,
21    }
22}
23
24pub fn levels_from_proto(
25    levels: &[PriceLevel],
26    symbol: &str,
27    quantity_scale: u32,
28) -> Result<Vec<OrderbookLevel>> {
29    let symbol = Some(symbol.to_owned());
30    levels
31        .iter()
32        .map(|l| {
33            let price = decode_price_ticks(l.price_ticks, symbol.clone())
34                .ok_or_else(|| Error::validation("orderbook level has invalid or missing price"))?;
35            let qty = decode_qty_scaled(l.qty_scaled, Some(quantity_scale), symbol.clone(), None)
36                .ok_or_else(|| {
37                Error::validation("orderbook level has invalid or missing quantity")
38            })?;
39            Ok(OrderbookLevel {
40                price: Some(price),
41                qty: Some(qty),
42            })
43        })
44        .collect()
45}
46
47pub fn orderbook_from_proto(
48    msg: &GetOrderBookResponse,
49    symbol: &str,
50    depth: u32,
51    quantity_scale: u32,
52) -> Result<OrderbookData> {
53    Ok(OrderbookData {
54        symbol: symbol.to_owned(),
55        depth,
56        book_seq: if msg.book_seq == 0 {
57            String::new()
58        } else {
59            msg.book_seq.to_string()
60        },
61        bids: levels_from_proto(&msg.bids, symbol, quantity_scale)?,
62        asks: levels_from_proto(&msg.asks, symbol, quantity_scale)?,
63    })
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69
70    #[test]
71    fn orderbook_maps_levels() {
72        let msg = GetOrderBookResponse {
73            book_seq: 7,
74            bids: vec![PriceLevel {
75                price_ticks: 1_000_000,
76                qty_scaled: 50,
77                ..Default::default()
78            }],
79            asks: vec![PriceLevel {
80                price_ticks: 1_100_000,
81                qty_scaled: 25,
82                ..Default::default()
83            }],
84            ..Default::default()
85        };
86        let book = orderbook_from_proto(&msg, "ETH-USDT", 50, 8).unwrap();
87        assert_eq!(book.symbol, "ETH-USDT");
88        assert_eq!(book.depth, 50);
89        assert_eq!(book.book_seq, "7");
90        assert_eq!(book.bids[0].price.as_ref().unwrap().as_ticks(), 1_000_000);
91        assert_eq!(book.asks[0].qty.as_ref().unwrap().as_scaled(), 25);
92    }
93
94    #[test]
95    fn depth_mapping_preserves_protocol_boundaries() {
96        use crate::proto::orderbook::v1::Depth;
97
98        assert_eq!(depth_enum_for_levels(1), Depth::Depth1);
99        assert_eq!(depth_enum_for_levels(5), Depth::Depth5);
100        assert_eq!(depth_enum_for_levels(500), Depth::Depth500);
101        assert_eq!(depth_enum_for_levels(1000), Depth::Depth1000);
102    }
103
104    #[test]
105    fn orderbook_rejects_levels_with_missing_price_or_quantity() {
106        let missing_price = GetOrderBookResponse {
107            bids: vec![PriceLevel {
108                price_ticks: 0,
109                qty_scaled: 1,
110                ..Default::default()
111            }],
112            ..Default::default()
113        };
114        assert!(orderbook_from_proto(&missing_price, "ETH-USDT", 1, 8).is_err());
115
116        let missing_qty = GetOrderBookResponse {
117            asks: vec![PriceLevel {
118                price_ticks: 1,
119                qty_scaled: 0,
120                ..Default::default()
121            }],
122            ..Default::default()
123        };
124        assert!(orderbook_from_proto(&missing_qty, "ETH-USDT", 1, 8).is_err());
125    }
126}