1use crate::error::StreamError;
14use rust_decimal::Decimal;
15use std::collections::BTreeMap;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
19pub enum BookSide {
20 Bid,
21 Ask,
22}
23
24#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
26pub struct PriceLevel {
27 pub price: Decimal,
28 pub quantity: Decimal,
29}
30
31impl PriceLevel {
32 pub fn new(price: Decimal, quantity: Decimal) -> Self {
33 Self { price, quantity }
34 }
35}
36
37#[derive(Debug, Clone)]
39pub struct BookDelta {
40 pub symbol: String,
41 pub side: BookSide,
42 pub price: Decimal,
44 pub quantity: Decimal,
45 pub sequence: Option<u64>,
46}
47
48impl BookDelta {
49 pub fn new(symbol: impl Into<String>, side: BookSide, price: Decimal, quantity: Decimal) -> Self {
50 Self { symbol: symbol.into(), side, price, quantity, sequence: None }
51 }
52
53 pub fn with_sequence(mut self, seq: u64) -> Self {
54 self.sequence = Some(seq);
55 self
56 }
57}
58
59pub struct OrderBook {
61 symbol: String,
62 bids: BTreeMap<Decimal, Decimal>, asks: BTreeMap<Decimal, Decimal>, last_sequence: Option<u64>,
65}
66
67impl OrderBook {
68 pub fn new(symbol: impl Into<String>) -> Self {
69 Self {
70 symbol: symbol.into(),
71 bids: BTreeMap::new(),
72 asks: BTreeMap::new(),
73 last_sequence: None,
74 }
75 }
76
77 pub fn apply(&mut self, delta: BookDelta) -> Result<(), StreamError> {
79 if delta.symbol != self.symbol {
80 return Err(StreamError::BookReconstructionFailed {
81 symbol: self.symbol.clone(),
82 reason: format!("delta symbol '{}' does not match book '{}'", delta.symbol, self.symbol),
83 });
84 }
85 let map = match delta.side {
86 BookSide::Bid => &mut self.bids,
87 BookSide::Ask => &mut self.asks,
88 };
89 if delta.quantity.is_zero() {
90 map.remove(&delta.price);
91 } else {
92 map.insert(delta.price, delta.quantity);
93 }
94 if let Some(seq) = delta.sequence {
95 self.last_sequence = Some(seq);
96 }
97 self.check_crossed()
98 }
99
100 pub fn reset(
102 &mut self,
103 bids: Vec<PriceLevel>,
104 asks: Vec<PriceLevel>,
105 ) -> Result<(), StreamError> {
106 self.bids.clear();
107 self.asks.clear();
108 for lvl in bids {
109 if !lvl.quantity.is_zero() {
110 self.bids.insert(lvl.price, lvl.quantity);
111 }
112 }
113 for lvl in asks {
114 if !lvl.quantity.is_zero() {
115 self.asks.insert(lvl.price, lvl.quantity);
116 }
117 }
118 self.check_crossed()
119 }
120
121 pub fn best_bid(&self) -> Option<PriceLevel> {
123 self.bids.iter().next_back().map(|(p, q)| PriceLevel::new(*p, *q))
124 }
125
126 pub fn best_ask(&self) -> Option<PriceLevel> {
128 self.asks.iter().next().map(|(p, q)| PriceLevel::new(*p, *q))
129 }
130
131 pub fn mid_price(&self) -> Option<Decimal> {
133 let bid = self.best_bid()?.price;
134 let ask = self.best_ask()?.price;
135 Some((bid + ask) / Decimal::from(2))
136 }
137
138 pub fn spread(&self) -> Option<Decimal> {
140 let bid = self.best_bid()?.price;
141 let ask = self.best_ask()?.price;
142 Some(ask - bid)
143 }
144
145 pub fn bid_depth(&self) -> usize { self.bids.len() }
147
148 pub fn ask_depth(&self) -> usize { self.asks.len() }
150
151 pub fn symbol(&self) -> &str { &self.symbol }
152 pub fn last_sequence(&self) -> Option<u64> { self.last_sequence }
153
154 pub fn top_bids(&self, n: usize) -> Vec<PriceLevel> {
156 self.bids.iter().rev().take(n).map(|(p, q)| PriceLevel::new(*p, *q)).collect()
157 }
158
159 pub fn top_asks(&self, n: usize) -> Vec<PriceLevel> {
161 self.asks.iter().take(n).map(|(p, q)| PriceLevel::new(*p, *q)).collect()
162 }
163
164 fn check_crossed(&self) -> Result<(), StreamError> {
165 if let (Some(bid), Some(ask)) = (self.best_bid(), self.best_ask()) {
166 if bid.price >= ask.price {
167 return Err(StreamError::BookCrossed {
168 symbol: self.symbol.clone(),
169 bid: bid.price.to_string(),
170 ask: ask.price.to_string(),
171 });
172 }
173 }
174 Ok(())
175 }
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181 use rust_decimal_macros::dec;
182
183 fn book(symbol: &str) -> OrderBook { OrderBook::new(symbol) }
184
185 fn delta(symbol: &str, side: BookSide, price: Decimal, qty: Decimal) -> BookDelta {
186 BookDelta::new(symbol, side, price, qty)
187 }
188
189 #[test]
190 fn test_order_book_apply_bid_level() {
191 let mut b = book("BTC-USD");
192 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(1))).unwrap();
193 assert_eq!(b.best_bid().unwrap().price, dec!(50000));
194 }
195
196 #[test]
197 fn test_order_book_apply_ask_level() {
198 let mut b = book("BTC-USD");
199 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(2))).unwrap();
200 assert_eq!(b.best_ask().unwrap().price, dec!(50100));
201 }
202
203 #[test]
204 fn test_order_book_remove_level_with_zero_qty() {
205 let mut b = book("BTC-USD");
206 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(1))).unwrap();
207 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(0))).unwrap();
208 assert!(b.best_bid().is_none());
209 }
210
211 #[test]
212 fn test_order_book_best_bid_is_highest() {
213 let mut b = book("BTC-USD");
214 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49900), dec!(1))).unwrap();
215 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(2))).unwrap();
216 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49800), dec!(3))).unwrap();
217 assert_eq!(b.best_bid().unwrap().price, dec!(50000));
218 }
219
220 #[test]
221 fn test_order_book_best_ask_is_lowest() {
222 let mut b = book("BTC-USD");
223 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50200), dec!(1))).unwrap();
224 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(2))).unwrap();
225 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50300), dec!(3))).unwrap();
226 assert_eq!(b.best_ask().unwrap().price, dec!(50100));
227 }
228
229 #[test]
230 fn test_order_book_mid_price() {
231 let mut b = book("BTC-USD");
232 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(1))).unwrap();
233 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(1))).unwrap();
234 assert_eq!(b.mid_price().unwrap(), dec!(50050));
235 }
236
237 #[test]
238 fn test_order_book_spread() {
239 let mut b = book("BTC-USD");
240 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(1))).unwrap();
241 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(1))).unwrap();
242 assert_eq!(b.spread().unwrap(), dec!(100));
243 }
244
245 #[test]
246 fn test_order_book_crossed_returns_error() {
247 let mut b = book("BTC-USD");
248 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50000), dec!(1))).unwrap();
249 let result = b.apply(delta("BTC-USD", BookSide::Bid, dec!(50001), dec!(1)));
250 assert!(matches!(result, Err(StreamError::BookCrossed { .. })));
251 }
252
253 #[test]
254 fn test_order_book_wrong_symbol_delta_rejected() {
255 let mut b = book("BTC-USD");
256 let result = b.apply(delta("ETH-USD", BookSide::Bid, dec!(3000), dec!(1)));
257 assert!(matches!(result, Err(StreamError::BookReconstructionFailed { .. })));
258 }
259
260 #[test]
261 fn test_order_book_reset_clears_and_reloads() {
262 let mut b = book("BTC-USD");
263 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49000), dec!(5))).unwrap();
264 b.reset(
265 vec![PriceLevel::new(dec!(50000), dec!(1))],
266 vec![PriceLevel::new(dec!(50100), dec!(1))],
267 ).unwrap();
268 assert_eq!(b.bid_depth(), 1);
269 assert_eq!(b.best_bid().unwrap().price, dec!(50000));
270 }
271
272 #[test]
273 fn test_order_book_reset_ignores_zero_qty_levels() {
274 let mut b = book("BTC-USD");
275 b.reset(
276 vec![
277 PriceLevel::new(dec!(50000), dec!(1)),
278 PriceLevel::new(dec!(49900), dec!(0)),
279 ],
280 vec![PriceLevel::new(dec!(50100), dec!(1))],
281 ).unwrap();
282 assert_eq!(b.bid_depth(), 1);
283 }
284
285 #[test]
286 fn test_order_book_depth_counts() {
287 let mut b = book("BTC-USD");
288 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49900), dec!(1))).unwrap();
289 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49800), dec!(1))).unwrap();
290 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(1))).unwrap();
291 assert_eq!(b.bid_depth(), 2);
292 assert_eq!(b.ask_depth(), 1);
293 }
294
295 #[test]
296 fn test_order_book_top_bids_descending() {
297 let mut b = book("BTC-USD");
298 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49800), dec!(3))).unwrap();
299 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(1))).unwrap();
300 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49900), dec!(2))).unwrap();
301 let top = b.top_bids(2);
302 assert_eq!(top[0].price, dec!(50000));
303 assert_eq!(top[1].price, dec!(49900));
304 }
305
306 #[test]
307 fn test_order_book_top_asks_ascending() {
308 let mut b = book("BTC-USD");
309 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50300), dec!(3))).unwrap();
310 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(1))).unwrap();
311 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50200), dec!(2))).unwrap();
312 let top = b.top_asks(2);
313 assert_eq!(top[0].price, dec!(50100));
314 assert_eq!(top[1].price, dec!(50200));
315 }
316
317 #[test]
318 fn test_book_delta_with_sequence() {
319 let d = BookDelta::new("BTC-USD", BookSide::Bid, dec!(50000), dec!(1))
320 .with_sequence(42);
321 assert_eq!(d.sequence, Some(42));
322 }
323
324 #[test]
325 fn test_order_book_sequence_tracking() {
326 let mut b = book("BTC-USD");
327 b.apply(
328 delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(1)).with_sequence(7)
329 ).unwrap();
330 assert_eq!(b.last_sequence(), Some(7));
331 }
332
333 #[test]
334 fn test_order_book_mid_price_empty_returns_none() {
335 let b = book("BTC-USD");
336 assert!(b.mid_price().is_none());
337 }
338
339 #[test]
340 fn test_price_level_new() {
341 let lvl = PriceLevel::new(dec!(100), dec!(5));
342 assert_eq!(lvl.price, dec!(100));
343 assert_eq!(lvl.quantity, dec!(5));
344 }
345}