polyester/codecs/decode/
orderbook.rs1use 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}