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,
22 Ask,
24}
25
26#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
28pub struct PriceLevel {
29 pub price: Decimal,
31 pub quantity: Decimal,
33}
34
35impl PriceLevel {
36 pub fn new(price: Decimal, quantity: Decimal) -> Self {
38 Self { price, quantity }
39 }
40}
41
42#[derive(Debug, Clone)]
44pub struct BookDelta {
45 pub symbol: String,
47 pub side: BookSide,
49 pub price: Decimal,
51 pub quantity: Decimal,
53 pub sequence: Option<u64>,
55}
56
57impl BookDelta {
58 pub fn new(
63 symbol: impl Into<String>,
64 side: BookSide,
65 price: Decimal,
66 quantity: Decimal,
67 ) -> Self {
68 Self {
69 symbol: symbol.into(),
70 side,
71 price,
72 quantity,
73 sequence: None,
74 }
75 }
76
77 pub fn with_sequence(mut self, seq: u64) -> Self {
79 self.sequence = Some(seq);
80 self
81 }
82}
83
84pub struct OrderBook {
86 symbol: String,
87 bids: BTreeMap<Decimal, Decimal>, asks: BTreeMap<Decimal, Decimal>, last_sequence: Option<u64>,
90}
91
92impl OrderBook {
93 pub fn new(symbol: impl Into<String>) -> Self {
95 Self {
96 symbol: symbol.into(),
97 bids: BTreeMap::new(),
98 asks: BTreeMap::new(),
99 last_sequence: None,
100 }
101 }
102
103 pub fn apply(&mut self, delta: BookDelta) -> Result<(), StreamError> {
105 if delta.symbol != self.symbol {
106 return Err(StreamError::BookReconstructionFailed {
107 symbol: self.symbol.clone(),
108 reason: format!(
109 "delta symbol '{}' does not match book '{}'",
110 delta.symbol, self.symbol
111 ),
112 });
113 }
114 let map = match delta.side {
115 BookSide::Bid => &mut self.bids,
116 BookSide::Ask => &mut self.asks,
117 };
118 if delta.quantity.is_zero() {
119 map.remove(&delta.price);
120 } else {
121 map.insert(delta.price, delta.quantity);
122 }
123 if let Some(seq) = delta.sequence {
124 self.last_sequence = Some(seq);
125 }
126 self.check_crossed()
127 }
128
129 pub fn reset(
131 &mut self,
132 bids: Vec<PriceLevel>,
133 asks: Vec<PriceLevel>,
134 ) -> Result<(), StreamError> {
135 self.bids.clear();
136 self.asks.clear();
137 for lvl in bids {
138 if !lvl.quantity.is_zero() {
139 self.bids.insert(lvl.price, lvl.quantity);
140 }
141 }
142 for lvl in asks {
143 if !lvl.quantity.is_zero() {
144 self.asks.insert(lvl.price, lvl.quantity);
145 }
146 }
147 self.check_crossed()
148 }
149
150 pub fn best_bid(&self) -> Option<PriceLevel> {
152 self.bids
153 .iter()
154 .next_back()
155 .map(|(p, q)| PriceLevel::new(*p, *q))
156 }
157
158 pub fn best_ask(&self) -> Option<PriceLevel> {
160 self.asks
161 .iter()
162 .next()
163 .map(|(p, q)| PriceLevel::new(*p, *q))
164 }
165
166 pub fn mid_price(&self) -> Option<Decimal> {
168 let bid = self.best_bid()?.price;
169 let ask = self.best_ask()?.price;
170 Some((bid + ask) / Decimal::from(2))
171 }
172
173 pub fn spread(&self) -> Option<Decimal> {
175 let bid = self.best_bid()?.price;
176 let ask = self.best_ask()?.price;
177 Some(ask - bid)
178 }
179
180 pub fn bid_depth(&self) -> usize {
182 self.bids.len()
183 }
184
185 pub fn ask_depth(&self) -> usize {
187 self.asks.len()
188 }
189
190 pub fn symbol(&self) -> &str {
192 &self.symbol
193 }
194
195 pub fn last_sequence(&self) -> Option<u64> {
197 self.last_sequence
198 }
199
200 pub fn top_bids(&self, n: usize) -> Vec<PriceLevel> {
202 self.bids
203 .iter()
204 .rev()
205 .take(n)
206 .map(|(p, q)| PriceLevel::new(*p, *q))
207 .collect()
208 }
209
210 pub fn top_asks(&self, n: usize) -> Vec<PriceLevel> {
212 self.asks
213 .iter()
214 .take(n)
215 .map(|(p, q)| PriceLevel::new(*p, *q))
216 .collect()
217 }
218
219 fn check_crossed(&self) -> Result<(), StreamError> {
220 if let (Some(bid), Some(ask)) = (self.best_bid(), self.best_ask()) {
221 if bid.price >= ask.price {
222 return Err(StreamError::BookCrossed {
223 symbol: self.symbol.clone(),
224 bid: bid.price.to_string(),
225 ask: ask.price.to_string(),
226 });
227 }
228 }
229 Ok(())
230 }
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236 use rust_decimal_macros::dec;
237
238 fn book(symbol: &str) -> OrderBook {
239 OrderBook::new(symbol)
240 }
241
242 fn delta(symbol: &str, side: BookSide, price: Decimal, qty: Decimal) -> BookDelta {
243 BookDelta::new(symbol, side, price, qty)
244 }
245
246 #[test]
247 fn test_order_book_apply_bid_level() {
248 let mut b = book("BTC-USD");
249 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(1)))
250 .unwrap();
251 assert_eq!(b.best_bid().unwrap().price, dec!(50000));
252 }
253
254 #[test]
255 fn test_order_book_apply_ask_level() {
256 let mut b = book("BTC-USD");
257 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(2)))
258 .unwrap();
259 assert_eq!(b.best_ask().unwrap().price, dec!(50100));
260 }
261
262 #[test]
263 fn test_order_book_remove_level_with_zero_qty() {
264 let mut b = book("BTC-USD");
265 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(1)))
266 .unwrap();
267 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(0)))
268 .unwrap();
269 assert!(b.best_bid().is_none());
270 }
271
272 #[test]
273 fn test_order_book_best_bid_is_highest() {
274 let mut b = book("BTC-USD");
275 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49900), dec!(1)))
276 .unwrap();
277 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(2)))
278 .unwrap();
279 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49800), dec!(3)))
280 .unwrap();
281 assert_eq!(b.best_bid().unwrap().price, dec!(50000));
282 }
283
284 #[test]
285 fn test_order_book_best_ask_is_lowest() {
286 let mut b = book("BTC-USD");
287 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50200), dec!(1)))
288 .unwrap();
289 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(2)))
290 .unwrap();
291 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50300), dec!(3)))
292 .unwrap();
293 assert_eq!(b.best_ask().unwrap().price, dec!(50100));
294 }
295
296 #[test]
297 fn test_order_book_mid_price() {
298 let mut b = book("BTC-USD");
299 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(1)))
300 .unwrap();
301 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(1)))
302 .unwrap();
303 assert_eq!(b.mid_price().unwrap(), dec!(50050));
304 }
305
306 #[test]
307 fn test_order_book_spread() {
308 let mut b = book("BTC-USD");
309 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(1)))
310 .unwrap();
311 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(1)))
312 .unwrap();
313 assert_eq!(b.spread().unwrap(), dec!(100));
314 }
315
316 #[test]
317 fn test_order_book_crossed_returns_error() {
318 let mut b = book("BTC-USD");
319 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50000), dec!(1)))
320 .unwrap();
321 let result = b.apply(delta("BTC-USD", BookSide::Bid, dec!(50001), dec!(1)));
322 assert!(matches!(result, Err(StreamError::BookCrossed { .. })));
323 }
324
325 #[test]
326 fn test_order_book_wrong_symbol_delta_rejected() {
327 let mut b = book("BTC-USD");
328 let result = b.apply(delta("ETH-USD", BookSide::Bid, dec!(3000), dec!(1)));
329 assert!(matches!(
330 result,
331 Err(StreamError::BookReconstructionFailed { .. })
332 ));
333 }
334
335 #[test]
336 fn test_order_book_reset_clears_and_reloads() {
337 let mut b = book("BTC-USD");
338 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49000), dec!(5)))
339 .unwrap();
340 b.reset(
341 vec![PriceLevel::new(dec!(50000), dec!(1))],
342 vec![PriceLevel::new(dec!(50100), dec!(1))],
343 )
344 .unwrap();
345 assert_eq!(b.bid_depth(), 1);
346 assert_eq!(b.best_bid().unwrap().price, dec!(50000));
347 }
348
349 #[test]
350 fn test_order_book_reset_ignores_zero_qty_levels() {
351 let mut b = book("BTC-USD");
352 b.reset(
353 vec![
354 PriceLevel::new(dec!(50000), dec!(1)),
355 PriceLevel::new(dec!(49900), dec!(0)),
356 ],
357 vec![PriceLevel::new(dec!(50100), dec!(1))],
358 )
359 .unwrap();
360 assert_eq!(b.bid_depth(), 1);
361 }
362
363 #[test]
364 fn test_order_book_depth_counts() {
365 let mut b = book("BTC-USD");
366 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49900), dec!(1)))
367 .unwrap();
368 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49800), dec!(1)))
369 .unwrap();
370 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(1)))
371 .unwrap();
372 assert_eq!(b.bid_depth(), 2);
373 assert_eq!(b.ask_depth(), 1);
374 }
375
376 #[test]
377 fn test_order_book_top_bids_descending() {
378 let mut b = book("BTC-USD");
379 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49800), dec!(3)))
380 .unwrap();
381 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(1)))
382 .unwrap();
383 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49900), dec!(2)))
384 .unwrap();
385 let top = b.top_bids(2);
386 assert_eq!(top[0].price, dec!(50000));
387 assert_eq!(top[1].price, dec!(49900));
388 }
389
390 #[test]
391 fn test_order_book_top_asks_ascending() {
392 let mut b = book("BTC-USD");
393 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50300), dec!(3)))
394 .unwrap();
395 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(1)))
396 .unwrap();
397 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50200), dec!(2)))
398 .unwrap();
399 let top = b.top_asks(2);
400 assert_eq!(top[0].price, dec!(50100));
401 assert_eq!(top[1].price, dec!(50200));
402 }
403
404 #[test]
405 fn test_book_delta_with_sequence() {
406 let d = BookDelta::new("BTC-USD", BookSide::Bid, dec!(50000), dec!(1)).with_sequence(42);
407 assert_eq!(d.sequence, Some(42));
408 }
409
410 #[test]
411 fn test_order_book_sequence_tracking() {
412 let mut b = book("BTC-USD");
413 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(1)).with_sequence(7))
414 .unwrap();
415 assert_eq!(b.last_sequence(), Some(7));
416 }
417
418 #[test]
419 fn test_order_book_mid_price_empty_returns_none() {
420 let b = book("BTC-USD");
421 assert!(b.mid_price().is_none());
422 }
423
424 #[test]
425 fn test_price_level_new() {
426 let lvl = PriceLevel::new(dec!(100), dec!(5));
427 assert_eq!(lvl.price, dec!(100));
428 assert_eq!(lvl.quantity, dec!(5));
429 }
430}