1use std::cmp::Ordering;
10use std::collections::BTreeMap;
11use std::str::FromStr;
12
13use rust_decimal::Decimal;
14
15use crate::websocket::error::WebSocketError;
16use crate::websocket::types::{BookUpdateData, PriceLevel};
17
18#[derive(Debug, Clone, Eq, PartialEq)]
23struct PriceKey(String);
24
25impl PriceKey {
26 fn new(price: String) -> Self {
27 Self(price)
28 }
29
30 fn as_str(&self) -> &str {
31 &self.0
32 }
33}
34
35impl Ord for PriceKey {
36 fn cmp(&self, other: &Self) -> Ordering {
37 let self_dec = Decimal::from_str(&self.0).unwrap_or(Decimal::ZERO);
38 let other_dec = Decimal::from_str(&other.0).unwrap_or(Decimal::ZERO);
39 self_dec.cmp(&other_dec)
40 }
41}
42
43impl PartialOrd for PriceKey {
44 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
45 Some(self.cmp(other))
46 }
47}
48
49fn is_zero_size(s: &str) -> bool {
51 Decimal::from_str(s)
52 .map(|v| v.is_zero())
53 .unwrap_or(false)
54}
55
56#[derive(Debug, Clone)]
58pub struct LocalOrderbook {
59 pub orderbook_id: String,
61 bids: BTreeMap<PriceKey, String>,
63 asks: BTreeMap<PriceKey, String>,
65 expected_sequence: u64,
67 has_snapshot: bool,
69 last_timestamp: Option<String>,
71}
72
73impl LocalOrderbook {
74 pub fn new(orderbook_id: String) -> Self {
76 Self {
77 orderbook_id,
78 bids: BTreeMap::new(),
79 asks: BTreeMap::new(),
80 expected_sequence: 0,
81 has_snapshot: false,
82 last_timestamp: None,
83 }
84 }
85
86 pub fn apply_snapshot(&mut self, update: &BookUpdateData) {
88 self.bids.clear();
90 self.asks.clear();
91
92 for level in &update.bids {
94 if !is_zero_size(&level.size) {
95 self.bids
96 .insert(PriceKey::new(level.price.clone()), level.size.clone());
97 }
98 }
99
100 for level in &update.asks {
101 if !is_zero_size(&level.size) {
102 self.asks
103 .insert(PriceKey::new(level.price.clone()), level.size.clone());
104 }
105 }
106
107 self.expected_sequence = update.sequence + 1;
108 self.has_snapshot = true;
109 self.last_timestamp = Some(update.timestamp.clone());
110 }
111
112 pub fn apply_delta(&mut self, update: &BookUpdateData) -> Result<(), WebSocketError> {
116 if update.sequence != self.expected_sequence {
118 return Err(WebSocketError::SequenceGap {
119 expected: self.expected_sequence,
120 received: update.sequence,
121 });
122 }
123
124 for level in &update.bids {
126 let key = PriceKey::new(level.price.clone());
127 if is_zero_size(&level.size) {
128 self.bids.remove(&key);
129 } else {
130 self.bids.insert(key, level.size.clone());
131 }
132 }
133
134 for level in &update.asks {
136 let key = PriceKey::new(level.price.clone());
137 if is_zero_size(&level.size) {
138 self.asks.remove(&key);
139 } else {
140 self.asks.insert(key, level.size.clone());
141 }
142 }
143
144 self.expected_sequence = update.sequence + 1;
145 self.last_timestamp = Some(update.timestamp.clone());
146 Ok(())
147 }
148
149 pub fn apply_update(&mut self, update: &BookUpdateData) -> Result<(), WebSocketError> {
151 if update.is_snapshot {
152 self.apply_snapshot(update);
153 Ok(())
154 } else {
155 self.apply_delta(update)
156 }
157 }
158
159 pub fn get_bids(&self) -> Vec<PriceLevel> {
161 self.bids
162 .iter()
163 .rev()
164 .map(|(price, size)| PriceLevel {
165 side: "bid".to_string(),
166 price: price.as_str().to_string(),
167 size: size.clone(),
168 })
169 .collect()
170 }
171
172 pub fn get_asks(&self) -> Vec<PriceLevel> {
174 self.asks
175 .iter()
176 .map(|(price, size)| PriceLevel {
177 side: "ask".to_string(),
178 price: price.as_str().to_string(),
179 size: size.clone(),
180 })
181 .collect()
182 }
183
184 pub fn get_top_bids(&self, n: usize) -> Vec<PriceLevel> {
186 self.bids
187 .iter()
188 .rev()
189 .take(n)
190 .map(|(price, size)| PriceLevel {
191 side: "bid".to_string(),
192 price: price.as_str().to_string(),
193 size: size.clone(),
194 })
195 .collect()
196 }
197
198 pub fn get_top_asks(&self, n: usize) -> Vec<PriceLevel> {
200 self.asks
201 .iter()
202 .take(n)
203 .map(|(price, size)| PriceLevel {
204 side: "ask".to_string(),
205 price: price.as_str().to_string(),
206 size: size.clone(),
207 })
208 .collect()
209 }
210
211 pub fn best_bid(&self) -> Option<(String, String)> {
213 self.bids
214 .iter()
215 .next_back()
216 .map(|(p, s)| (p.as_str().to_string(), s.clone()))
217 }
218
219 pub fn best_ask(&self) -> Option<(String, String)> {
221 self.asks
222 .iter()
223 .next()
224 .map(|(p, s)| (p.as_str().to_string(), s.clone()))
225 }
226
227 pub fn spread(&self) -> Option<String> {
230 match (self.best_bid(), self.best_ask()) {
231 (Some((bid, _)), Some((ask, _))) => {
232 let bid_dec = Decimal::from_str(&bid).ok()?;
233 let ask_dec = Decimal::from_str(&ask).ok()?;
234 if ask_dec > bid_dec {
235 Some((ask_dec - bid_dec).to_string())
236 } else {
237 Some(Decimal::ZERO.to_string())
238 }
239 }
240 _ => None,
241 }
242 }
243
244 pub fn midpoint(&self) -> Option<String> {
247 match (self.best_bid(), self.best_ask()) {
248 (Some((bid, _)), Some((ask, _))) => {
249 let bid_dec = Decimal::from_str(&bid).ok()?;
250 let ask_dec = Decimal::from_str(&ask).ok()?;
251 let two = Decimal::from(2);
252 Some(((bid_dec + ask_dec) / two).to_string())
253 }
254 _ => None,
255 }
256 }
257
258 pub fn bid_size_at(&self, price: &str) -> Option<String> {
260 self.bids.get(&PriceKey::new(price.to_string())).cloned()
261 }
262
263 pub fn ask_size_at(&self, price: &str) -> Option<String> {
265 self.asks.get(&PriceKey::new(price.to_string())).cloned()
266 }
267
268 pub fn total_bid_depth(&self) -> Decimal {
271 self.bids
272 .values()
273 .filter_map(|s| Decimal::from_str(s).ok())
274 .fold(Decimal::ZERO, |acc, x| acc + x)
275 }
276
277 pub fn total_ask_depth(&self) -> Decimal {
280 self.asks
281 .values()
282 .filter_map(|s| Decimal::from_str(s).ok())
283 .fold(Decimal::ZERO, |acc, x| acc + x)
284 }
285
286 pub fn bid_count(&self) -> usize {
288 self.bids.len()
289 }
290
291 pub fn ask_count(&self) -> usize {
293 self.asks.len()
294 }
295
296 pub fn has_snapshot(&self) -> bool {
298 self.has_snapshot
299 }
300
301 pub fn expected_sequence(&self) -> u64 {
303 self.expected_sequence
304 }
305
306 pub fn last_timestamp(&self) -> Option<&str> {
308 self.last_timestamp.as_deref()
309 }
310
311 pub fn clear(&mut self) {
313 self.bids.clear();
314 self.asks.clear();
315 self.expected_sequence = 0;
316 self.has_snapshot = false;
317 self.last_timestamp = None;
318 }
319}
320
321#[cfg(test)]
322mod tests {
323 use super::*;
324
325 fn create_snapshot() -> BookUpdateData {
326 BookUpdateData {
327 orderbook_id: "test".to_string(),
328 timestamp: "2024-01-01T00:00:00.000Z".to_string(),
329 sequence: 0,
330 bids: vec![
331 PriceLevel {
332 side: "bid".to_string(),
333 price: "0.500000".to_string(),
334 size: "0.001000".to_string(),
335 },
336 PriceLevel {
337 side: "bid".to_string(),
338 price: "0.490000".to_string(),
339 size: "0.002000".to_string(),
340 },
341 ],
342 asks: vec![
343 PriceLevel {
344 side: "ask".to_string(),
345 price: "0.510000".to_string(),
346 size: "0.000500".to_string(),
347 },
348 PriceLevel {
349 side: "ask".to_string(),
350 price: "0.520000".to_string(),
351 size: "0.001500".to_string(),
352 },
353 ],
354 is_snapshot: true,
355 resync: false,
356 message: None,
357 }
358 }
359
360 #[test]
361 fn test_apply_snapshot() {
362 let mut book = LocalOrderbook::new("test".to_string());
363 let snapshot = create_snapshot();
364
365 book.apply_snapshot(&snapshot);
366
367 assert!(book.has_snapshot());
368 assert_eq!(book.expected_sequence(), 1);
369 assert_eq!(book.bid_count(), 2);
370 assert_eq!(book.ask_count(), 2);
371 assert_eq!(book.best_bid(), Some(("0.500000".to_string(), "0.001000".to_string())));
372 assert_eq!(book.best_ask(), Some(("0.510000".to_string(), "0.000500".to_string())));
373 }
374
375 #[test]
376 fn test_apply_delta() {
377 let mut book = LocalOrderbook::new("test".to_string());
378 book.apply_snapshot(&create_snapshot());
379
380 let delta = BookUpdateData {
381 orderbook_id: "test".to_string(),
382 timestamp: "2024-01-01T00:00:00.050Z".to_string(),
383 sequence: 1,
384 bids: vec![PriceLevel {
385 side: "bid".to_string(),
386 price: "0.500000".to_string(),
387 size: "0.001500".to_string(), }],
389 asks: vec![PriceLevel {
390 side: "ask".to_string(),
391 price: "0.510000".to_string(),
392 size: "0".to_string(), }],
394 is_snapshot: false,
395 resync: false,
396 message: None,
397 };
398
399 book.apply_delta(&delta).unwrap();
400
401 assert_eq!(book.best_bid(), Some(("0.500000".to_string(), "0.001500".to_string())));
402 assert_eq!(book.best_ask(), Some(("0.520000".to_string(), "0.001500".to_string())));
403 assert_eq!(book.ask_count(), 1);
404 }
405
406 #[test]
407 fn test_sequence_gap_detection() {
408 let mut book = LocalOrderbook::new("test".to_string());
409 book.apply_snapshot(&create_snapshot());
410
411 let delta = BookUpdateData {
412 orderbook_id: "test".to_string(),
413 timestamp: "2024-01-01T00:00:00.050Z".to_string(),
414 sequence: 5, bids: vec![],
416 asks: vec![],
417 is_snapshot: false,
418 resync: false,
419 message: None,
420 };
421
422 let result = book.apply_delta(&delta);
423 assert!(matches!(result, Err(WebSocketError::SequenceGap { .. })));
424 }
425
426 #[test]
427 fn test_spread_and_midpoint() {
428 let mut book = LocalOrderbook::new("test".to_string());
429 book.apply_snapshot(&create_snapshot());
430
431 assert_eq!(book.spread(), Some("0.010000".to_string()));
433 assert_eq!(book.midpoint(), Some("0.505000".to_string()));
434 }
435
436 #[test]
437 fn test_depth() {
438 let mut book = LocalOrderbook::new("test".to_string());
439 book.apply_snapshot(&create_snapshot());
440
441 assert_eq!(book.total_bid_depth(), Decimal::from_str("0.003").unwrap());
443 assert_eq!(book.total_ask_depth(), Decimal::from_str("0.002").unwrap());
444 }
445
446 #[test]
447 fn test_variable_precision_price_sorting() {
448 let mut book = LocalOrderbook::new("test".to_string());
452
453 let snapshot = BookUpdateData {
454 orderbook_id: "test".to_string(),
455 timestamp: "2024-01-01T00:00:00.000Z".to_string(),
456 sequence: 0,
457 bids: vec![
458 PriceLevel {
459 side: "bid".to_string(),
460 price: "0.10".to_string(), size: "1.0".to_string(),
462 },
463 PriceLevel {
464 side: "bid".to_string(),
465 price: "0.5".to_string(), size: "2.0".to_string(),
467 },
468 PriceLevel {
469 side: "bid".to_string(),
470 price: "0.100000".to_string(), size: "3.0".to_string(),
472 },
473 ],
474 asks: vec![
475 PriceLevel {
476 side: "ask".to_string(),
477 price: "0.9".to_string(), size: "1.0".to_string(),
479 },
480 PriceLevel {
481 side: "ask".to_string(),
482 price: "0.51".to_string(), size: "2.0".to_string(),
484 },
485 ],
486 is_snapshot: true,
487 resync: false,
488 message: None,
489 };
490
491 book.apply_snapshot(&snapshot);
492
493 let best_bid = book.best_bid().unwrap();
495 assert_eq!(best_bid.0, "0.5", "Best bid should be 0.5, not {}", best_bid.0);
496
497 let best_ask = book.best_ask().unwrap();
499 assert_eq!(
500 best_ask.0, "0.51",
501 "Best ask should be 0.51, not {}",
502 best_ask.0
503 );
504
505 let bids = book.get_bids();
508 assert_eq!(bids.len(), 2); assert_eq!(bids[0].price, "0.5"); }
511
512 #[test]
513 fn test_is_zero_size() {
514 assert!(super::is_zero_size("0"));
516 assert!(super::is_zero_size("0.0"));
517 assert!(super::is_zero_size("0.000000"));
518 assert!(super::is_zero_size("0.00000000000"));
519 assert!(!super::is_zero_size("0.001"));
520 assert!(!super::is_zero_size("1"));
521 assert!(!super::is_zero_size("0.0000001"));
522 }
523}