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(symbol: impl Into<String>, side: BookSide, price: Decimal, quantity: Decimal) -> Self {
63 Self { symbol: symbol.into(), side, price, quantity, sequence: None }
64 }
65
66 pub fn with_sequence(mut self, seq: u64) -> Self {
68 self.sequence = Some(seq);
69 self
70 }
71}
72
73pub struct OrderBook {
75 symbol: String,
76 bids: BTreeMap<Decimal, Decimal>, asks: BTreeMap<Decimal, Decimal>, last_sequence: Option<u64>,
79}
80
81impl OrderBook {
82 pub fn new(symbol: impl Into<String>) -> Self {
84 Self {
85 symbol: symbol.into(),
86 bids: BTreeMap::new(),
87 asks: BTreeMap::new(),
88 last_sequence: None,
89 }
90 }
91
92 pub fn apply(&mut self, delta: BookDelta) -> Result<(), StreamError> {
94 if delta.symbol != self.symbol {
95 return Err(StreamError::BookReconstructionFailed {
96 symbol: self.symbol.clone(),
97 reason: format!("delta symbol '{}' does not match book '{}'", delta.symbol, self.symbol),
98 });
99 }
100 let map = match delta.side {
101 BookSide::Bid => &mut self.bids,
102 BookSide::Ask => &mut self.asks,
103 };
104 if delta.quantity.is_zero() {
105 map.remove(&delta.price);
106 } else {
107 map.insert(delta.price, delta.quantity);
108 }
109 if let Some(seq) = delta.sequence {
110 self.last_sequence = Some(seq);
111 }
112 self.check_crossed()
113 }
114
115 pub fn reset(
117 &mut self,
118 bids: Vec<PriceLevel>,
119 asks: Vec<PriceLevel>,
120 ) -> Result<(), StreamError> {
121 self.bids.clear();
122 self.asks.clear();
123 for lvl in bids {
124 if !lvl.quantity.is_zero() {
125 self.bids.insert(lvl.price, lvl.quantity);
126 }
127 }
128 for lvl in asks {
129 if !lvl.quantity.is_zero() {
130 self.asks.insert(lvl.price, lvl.quantity);
131 }
132 }
133 self.check_crossed()
134 }
135
136 pub fn best_bid(&self) -> Option<PriceLevel> {
138 self.bids.iter().next_back().map(|(p, q)| PriceLevel::new(*p, *q))
139 }
140
141 pub fn best_ask(&self) -> Option<PriceLevel> {
143 self.asks.iter().next().map(|(p, q)| PriceLevel::new(*p, *q))
144 }
145
146 pub fn mid_price(&self) -> Option<Decimal> {
148 let bid = self.best_bid()?.price;
149 let ask = self.best_ask()?.price;
150 Some((bid + ask) / Decimal::from(2))
151 }
152
153 pub fn spread(&self) -> Option<Decimal> {
155 let bid = self.best_bid()?.price;
156 let ask = self.best_ask()?.price;
157 Some(ask - bid)
158 }
159
160 pub fn bid_depth(&self) -> usize { self.bids.len() }
162
163 pub fn ask_depth(&self) -> usize { self.asks.len() }
165
166 pub fn symbol(&self) -> &str { &self.symbol }
168
169 pub fn last_sequence(&self) -> Option<u64> { self.last_sequence }
171
172 pub fn top_bids(&self, n: usize) -> Vec<PriceLevel> {
174 self.bids.iter().rev().take(n).map(|(p, q)| PriceLevel::new(*p, *q)).collect()
175 }
176
177 pub fn top_asks(&self, n: usize) -> Vec<PriceLevel> {
179 self.asks.iter().take(n).map(|(p, q)| PriceLevel::new(*p, *q)).collect()
180 }
181
182 fn check_crossed(&self) -> Result<(), StreamError> {
183 if let (Some(bid), Some(ask)) = (self.best_bid(), self.best_ask()) {
184 if bid.price >= ask.price {
185 return Err(StreamError::BookCrossed {
186 symbol: self.symbol.clone(),
187 bid: bid.price.to_string(),
188 ask: ask.price.to_string(),
189 });
190 }
191 }
192 Ok(())
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199 use rust_decimal_macros::dec;
200
201 fn book(symbol: &str) -> OrderBook { OrderBook::new(symbol) }
202
203 fn delta(symbol: &str, side: BookSide, price: Decimal, qty: Decimal) -> BookDelta {
204 BookDelta::new(symbol, side, price, qty)
205 }
206
207 #[test]
208 fn test_order_book_apply_bid_level() {
209 let mut b = book("BTC-USD");
210 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(1))).unwrap();
211 assert_eq!(b.best_bid().unwrap().price, dec!(50000));
212 }
213
214 #[test]
215 fn test_order_book_apply_ask_level() {
216 let mut b = book("BTC-USD");
217 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(2))).unwrap();
218 assert_eq!(b.best_ask().unwrap().price, dec!(50100));
219 }
220
221 #[test]
222 fn test_order_book_remove_level_with_zero_qty() {
223 let mut b = book("BTC-USD");
224 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(1))).unwrap();
225 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(0))).unwrap();
226 assert!(b.best_bid().is_none());
227 }
228
229 #[test]
230 fn test_order_book_best_bid_is_highest() {
231 let mut b = book("BTC-USD");
232 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49900), dec!(1))).unwrap();
233 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(2))).unwrap();
234 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49800), dec!(3))).unwrap();
235 assert_eq!(b.best_bid().unwrap().price, dec!(50000));
236 }
237
238 #[test]
239 fn test_order_book_best_ask_is_lowest() {
240 let mut b = book("BTC-USD");
241 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50200), dec!(1))).unwrap();
242 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(2))).unwrap();
243 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50300), dec!(3))).unwrap();
244 assert_eq!(b.best_ask().unwrap().price, dec!(50100));
245 }
246
247 #[test]
248 fn test_order_book_mid_price() {
249 let mut b = book("BTC-USD");
250 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(1))).unwrap();
251 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(1))).unwrap();
252 assert_eq!(b.mid_price().unwrap(), dec!(50050));
253 }
254
255 #[test]
256 fn test_order_book_spread() {
257 let mut b = book("BTC-USD");
258 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(1))).unwrap();
259 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(1))).unwrap();
260 assert_eq!(b.spread().unwrap(), dec!(100));
261 }
262
263 #[test]
264 fn test_order_book_crossed_returns_error() {
265 let mut b = book("BTC-USD");
266 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50000), dec!(1))).unwrap();
267 let result = b.apply(delta("BTC-USD", BookSide::Bid, dec!(50001), dec!(1)));
268 assert!(matches!(result, Err(StreamError::BookCrossed { .. })));
269 }
270
271 #[test]
272 fn test_order_book_wrong_symbol_delta_rejected() {
273 let mut b = book("BTC-USD");
274 let result = b.apply(delta("ETH-USD", BookSide::Bid, dec!(3000), dec!(1)));
275 assert!(matches!(result, Err(StreamError::BookReconstructionFailed { .. })));
276 }
277
278 #[test]
279 fn test_order_book_reset_clears_and_reloads() {
280 let mut b = book("BTC-USD");
281 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49000), dec!(5))).unwrap();
282 b.reset(
283 vec![PriceLevel::new(dec!(50000), dec!(1))],
284 vec![PriceLevel::new(dec!(50100), dec!(1))],
285 ).unwrap();
286 assert_eq!(b.bid_depth(), 1);
287 assert_eq!(b.best_bid().unwrap().price, dec!(50000));
288 }
289
290 #[test]
291 fn test_order_book_reset_ignores_zero_qty_levels() {
292 let mut b = book("BTC-USD");
293 b.reset(
294 vec![
295 PriceLevel::new(dec!(50000), dec!(1)),
296 PriceLevel::new(dec!(49900), dec!(0)),
297 ],
298 vec![PriceLevel::new(dec!(50100), dec!(1))],
299 ).unwrap();
300 assert_eq!(b.bid_depth(), 1);
301 }
302
303 #[test]
304 fn test_order_book_depth_counts() {
305 let mut b = book("BTC-USD");
306 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49900), dec!(1))).unwrap();
307 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49800), dec!(1))).unwrap();
308 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(1))).unwrap();
309 assert_eq!(b.bid_depth(), 2);
310 assert_eq!(b.ask_depth(), 1);
311 }
312
313 #[test]
314 fn test_order_book_top_bids_descending() {
315 let mut b = book("BTC-USD");
316 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49800), dec!(3))).unwrap();
317 b.apply(delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(1))).unwrap();
318 b.apply(delta("BTC-USD", BookSide::Bid, dec!(49900), dec!(2))).unwrap();
319 let top = b.top_bids(2);
320 assert_eq!(top[0].price, dec!(50000));
321 assert_eq!(top[1].price, dec!(49900));
322 }
323
324 #[test]
325 fn test_order_book_top_asks_ascending() {
326 let mut b = book("BTC-USD");
327 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50300), dec!(3))).unwrap();
328 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50100), dec!(1))).unwrap();
329 b.apply(delta("BTC-USD", BookSide::Ask, dec!(50200), dec!(2))).unwrap();
330 let top = b.top_asks(2);
331 assert_eq!(top[0].price, dec!(50100));
332 assert_eq!(top[1].price, dec!(50200));
333 }
334
335 #[test]
336 fn test_book_delta_with_sequence() {
337 let d = BookDelta::new("BTC-USD", BookSide::Bid, dec!(50000), dec!(1))
338 .with_sequence(42);
339 assert_eq!(d.sequence, Some(42));
340 }
341
342 #[test]
343 fn test_order_book_sequence_tracking() {
344 let mut b = book("BTC-USD");
345 b.apply(
346 delta("BTC-USD", BookSide::Bid, dec!(50000), dec!(1)).with_sequence(7)
347 ).unwrap();
348 assert_eq!(b.last_sequence(), Some(7));
349 }
350
351 #[test]
352 fn test_order_book_mid_price_empty_returns_none() {
353 let b = book("BTC-USD");
354 assert!(b.mid_price().is_none());
355 }
356
357 #[test]
358 fn test_price_level_new() {
359 let lvl = PriceLevel::new(dec!(100), dec!(5));
360 assert_eq!(lvl.price, dec!(100));
361 assert_eq!(lvl.quantity, dec!(5));
362 }
363}