1use std::collections::BTreeMap;
20
21use rust_decimal::Decimal;
22
23use crate::{
24 http::{Orderbook, OrderbookLevel},
25 ws::{OrderbookDataMsg, OrderbookMsg},
26};
27
28type Side = BTreeMap<Decimal, Decimal>;
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum ApplyOutcome {
35 Buffered,
38 Synced,
42 Applied,
44 Ignored,
47 ResyncRequired,
52}
53
54#[derive(Debug)]
64pub struct OrderBookState {
65 inner: Inner,
66 max_buffered: usize,
68}
69
70pub const DEFAULT_MAX_BUFFERED_DIFFS: usize = 1000;
72
73impl Default for OrderBookState {
74 fn default() -> Self {
75 Self {
76 inner: Inner::default(),
77 max_buffered: DEFAULT_MAX_BUFFERED_DIFFS,
78 }
79 }
80}
81
82#[derive(Debug)]
83enum Inner {
84 Empty {
88 buffered: BTreeMap<i64, OrderbookDataMsg>,
89 },
90 Synced {
92 last_id: i64,
93 bids: Side,
94 asks: Side,
95 },
96}
97
98impl Default for Inner {
99 fn default() -> Self {
100 Self::Empty {
101 buffered: BTreeMap::new(),
102 }
103 }
104}
105
106impl OrderBookState {
107 pub fn new() -> Self {
109 Self::default()
110 }
111
112 pub fn with_max_buffered(max_buffered: usize) -> Self {
115 Self {
116 inner: Inner::default(),
117 max_buffered,
118 }
119 }
120
121 pub fn apply_snapshot(&mut self, snapshot: Orderbook) -> ApplyOutcome {
123 self.set_snapshot(snapshot.update_id, snapshot.bids, snapshot.asks)
124 }
125
126 pub fn apply_diff(&mut self, diff: OrderbookDataMsg) -> ApplyOutcome {
133 match &mut self.inner {
134 Inner::Empty { buffered } => {
135 buffered.insert(diff.update_id, diff);
136 while buffered.len() > self.max_buffered {
137 buffered.pop_first();
138 }
139 ApplyOutcome::Buffered
140 }
141 Inner::Synced {
142 last_id,
143 bids,
144 asks,
145 } => {
146 if diff.update_id == 1 {
150 self.inner = Inner::default();
151 return ApplyOutcome::ResyncRequired;
152 }
153 if diff.update_id <= *last_id {
154 return ApplyOutcome::Ignored;
155 }
156 if diff.update_id != *last_id + 1 {
157 self.inner = Inner::Empty {
161 buffered: BTreeMap::from([(diff.update_id, diff)]),
162 };
163 return ApplyOutcome::ResyncRequired;
164 }
165 apply_levels(bids, &diff.bids);
166 apply_levels(asks, &diff.asks);
167 *last_id = diff.update_id;
168 ApplyOutcome::Applied
169 }
170 }
171 }
172
173 pub fn apply(&mut self, msg: OrderbookMsg) -> ApplyOutcome {
177 match msg {
178 OrderbookMsg::Snapshot { data, .. } => {
179 self.set_snapshot(data.update_id, data.bids, data.asks)
180 }
181 OrderbookMsg::Delta { data, .. } => self.apply_diff(data),
182 }
183 }
184
185 pub fn is_synced(&self) -> bool {
187 matches!(self.inner, Inner::Synced { .. })
188 }
189
190 pub fn last_update_id(&self) -> Option<i64> {
192 match &self.inner {
193 Inner::Synced { last_id, .. } => Some(*last_id),
194 Inner::Empty { .. } => None,
195 }
196 }
197
198 pub fn bids(&self) -> Option<&Side> {
200 match &self.inner {
201 Inner::Synced { bids, .. } => Some(bids),
202 Inner::Empty { .. } => None,
203 }
204 }
205
206 pub fn asks(&self) -> Option<&Side> {
208 match &self.inner {
209 Inner::Synced { asks, .. } => Some(asks),
210 Inner::Empty { .. } => None,
211 }
212 }
213
214 pub fn best_bid(&self) -> Option<(Decimal, Decimal)> {
216 self.bids()
217 .and_then(|bids| bids.iter().next_back())
218 .map(|(&price, &size)| (price, size))
219 }
220
221 pub fn best_ask(&self) -> Option<(Decimal, Decimal)> {
223 self.asks()
224 .and_then(|asks| asks.iter().next())
225 .map(|(&price, &size)| (price, size))
226 }
227
228 fn set_snapshot(
232 &mut self,
233 new_id: i64,
234 bids: Vec<OrderbookLevel>,
235 asks: Vec<OrderbookLevel>,
236 ) -> ApplyOutcome {
237 if let Inner::Synced { last_id, .. } = &self.inner {
238 if new_id != 1 && new_id <= *last_id {
242 return ApplyOutcome::Ignored;
243 }
244 }
245
246 let mut bids: Side = bids.into_iter().map(|l| (l.price, l.size)).collect();
247 let mut asks: Side = asks.into_iter().map(|l| (l.price, l.size)).collect();
248
249 let buffered = match std::mem::take(&mut self.inner) {
250 Inner::Empty { buffered } => buffered,
251 Inner::Synced { .. } => BTreeMap::new(),
252 };
253
254 let mut last_id = new_id;
259 for (&id, diff) in buffered.range((new_id + 1)..) {
260 if id != last_id + 1 {
261 break;
262 }
263 apply_levels(&mut bids, &diff.bids);
264 apply_levels(&mut asks, &diff.asks);
265 last_id = id;
266 }
267
268 self.inner = Inner::Synced {
269 last_id,
270 bids,
271 asks,
272 };
273 ApplyOutcome::Synced
274 }
275}
276
277fn apply_levels(side: &mut Side, levels: &[OrderbookLevel]) {
280 for level in levels {
281 if level.size.is_zero() {
282 side.remove(&level.price);
283 } else {
284 side.insert(level.price, level.size);
285 }
286 }
287}
288
289#[cfg(test)]
290mod tests {
291 use rust_decimal::dec;
292
293 use crate::{DepthLevel, Topic};
294
295 use super::*;
296
297 fn level(price: Decimal, size: Decimal) -> OrderbookLevel {
298 OrderbookLevel { price, size }
299 }
300
301 fn snapshot(update_id: i64, bids: Vec<OrderbookLevel>, asks: Vec<OrderbookLevel>) -> Orderbook {
302 Orderbook {
303 symbol: "BTCUSDT".into(),
304 bids,
305 asks,
306 ts: 0,
307 update_id,
308 seq: update_id,
309 cts: None,
310 }
311 }
312
313 fn diff(
314 update_id: i64,
315 bids: Vec<OrderbookLevel>,
316 asks: Vec<OrderbookLevel>,
317 ) -> OrderbookDataMsg {
318 OrderbookDataMsg {
319 symbol: "BTCUSDT".into(),
320 bids,
321 asks,
322 update_id,
323 seq: update_id,
324 }
325 }
326
327 #[test]
328 fn snapshot_then_contiguous_diff_is_applied() {
329 let mut book = OrderBookState::new();
330 assert_eq!(
331 book.apply_snapshot(snapshot(
332 1,
333 vec![level(dec!(100), dec!(1))],
334 vec![level(dec!(101), dec!(2))]
335 )),
336 ApplyOutcome::Synced
337 );
338 assert!(book.is_synced());
339
340 assert_eq!(
341 book.apply_diff(diff(2, vec![level(dec!(100), dec!(1.5))], vec![])),
342 ApplyOutcome::Applied
343 );
344 assert_eq!(book.last_update_id(), Some(2));
345 assert_eq!(book.best_bid(), Some((dec!(100), dec!(1.5))));
346 assert_eq!(book.best_ask(), Some((dec!(101), dec!(2))));
347 }
348
349 #[test]
350 fn diff_buffered_before_snapshot_bridges_on_arrival() {
351 let mut book = OrderBookState::new();
352 assert_eq!(
353 book.apply_diff(diff(2, vec![level(dec!(100), dec!(1.5))], vec![])),
354 ApplyOutcome::Buffered
355 );
356 assert!(!book.is_synced());
357
358 assert_eq!(
359 book.apply_snapshot(snapshot(
360 1,
361 vec![level(dec!(100), dec!(1))],
362 vec![level(dec!(101), dec!(2))]
363 )),
364 ApplyOutcome::Synced
365 );
366 assert_eq!(book.last_update_id(), Some(2));
367 assert_eq!(book.best_bid(), Some((dec!(100), dec!(1.5))));
368 }
369
370 #[test]
371 fn stale_snapshot_is_ignored() {
372 let mut book = OrderBookState::new();
373 book.apply_snapshot(snapshot(5, vec![], vec![]));
374 assert_eq!(
375 book.apply_snapshot(snapshot(3, vec![], vec![])),
376 ApplyOutcome::Ignored
377 );
378 assert_eq!(book.last_update_id(), Some(5));
379 }
380
381 #[test]
382 fn stale_diff_is_ignored() {
383 let mut book = OrderBookState::new();
384 book.apply_snapshot(snapshot(5, vec![], vec![]));
385 assert_eq!(
386 book.apply_diff(diff(4, vec![], vec![])),
387 ApplyOutcome::Ignored
388 );
389 assert_eq!(book.last_update_id(), Some(5));
390 }
391
392 #[test]
393 fn gap_in_diff_stream_requires_resync_and_a_fresh_snapshot_bridges_it() {
394 let mut book = OrderBookState::new();
395 book.apply_snapshot(snapshot(1, vec![], vec![]));
396
397 assert_eq!(
398 book.apply_diff(diff(5, vec![level(dec!(100), dec!(1))], vec![])),
399 ApplyOutcome::ResyncRequired
400 );
401 assert!(!book.is_synced());
402
403 assert_eq!(
405 book.apply_snapshot(snapshot(4, vec![], vec![])),
406 ApplyOutcome::Synced
407 );
408 assert_eq!(book.last_update_id(), Some(5));
409 assert_eq!(book.best_bid(), Some((dec!(100), dec!(1))));
410 }
411
412 #[test]
413 fn restart_signal_resets_the_book_even_if_id_is_smaller() {
414 let mut book = OrderBookState::new();
415 book.apply_snapshot(snapshot(50, vec![level(dec!(100), dec!(1))], vec![]));
416 assert!(book.is_synced());
417
418 assert_eq!(
419 book.apply_snapshot(snapshot(1, vec![level(dec!(200), dec!(9))], vec![])),
420 ApplyOutcome::Synced
421 );
422 assert_eq!(book.last_update_id(), Some(1));
423 assert_eq!(book.best_bid(), Some((dec!(200), dec!(9))));
424 }
425
426 #[test]
427 fn zero_size_level_removes_the_price_from_the_book() {
428 let mut book = OrderBookState::new();
429 book.apply_snapshot(snapshot(
430 1,
431 vec![level(dec!(100), dec!(1)), level(dec!(99), dec!(2))],
432 vec![],
433 ));
434
435 assert_eq!(
436 book.apply_diff(diff(2, vec![level(dec!(100), dec!(0))], vec![])),
437 ApplyOutcome::Applied
438 );
439 assert_eq!(book.best_bid(), Some((dec!(99), dec!(2))));
440 }
441
442 #[test]
443 fn apply_dispatches_snapshot_and_delta_messages() {
444 let mut book = OrderBookState::new();
445 let topic = Topic::Orderbook {
446 symbol: "BTCUSDT".into(),
447 depth: DepthLevel::Level50,
448 };
449
450 let snapshot_msg = OrderbookMsg::Snapshot {
451 topic: topic.clone(),
452 ts: 0,
453 data: diff(
454 1,
455 vec![level(dec!(100), dec!(1))],
456 vec![level(dec!(101), dec!(2))],
457 ),
458 cts: 0,
459 };
460 assert_eq!(book.apply(snapshot_msg), ApplyOutcome::Synced);
461
462 let delta_msg = OrderbookMsg::Delta {
463 topic,
464 ts: 0,
465 data: diff(2, vec![level(dec!(100), dec!(1.5))], vec![]),
466 cts: 0,
467 };
468 assert_eq!(book.apply(delta_msg), ApplyOutcome::Applied);
469 assert_eq!(book.best_bid(), Some((dec!(100), dec!(1.5))));
470 }
471
472 #[test]
473 fn buffer_keeps_only_the_newest_diffs() {
474 let mut book = OrderBookState::with_max_buffered(3);
475 for id in 1..=10 {
476 let outcome =
477 book.apply_diff(diff(id, vec![level(dec!(100), Decimal::from(id))], vec![]));
478 assert_eq!(outcome, ApplyOutcome::Buffered);
479 }
480
481 let outcome = book.apply_snapshot(snapshot(7, vec![level(dec!(100), dec!(7))], vec![]));
483
484 assert_eq!(outcome, ApplyOutcome::Synced);
485 assert_eq!(book.last_update_id(), Some(10));
486 assert_eq!(book.best_bid(), Some((dec!(100), dec!(10))));
487 }
488
489 #[test]
490 fn evicted_diffs_do_not_bridge_an_older_snapshot() {
491 let mut book = OrderBookState::with_max_buffered(3);
492 for id in 3..=10 {
493 book.apply_diff(diff(id, vec![level(dec!(100), Decimal::from(id))], vec![]));
494 }
495
496 let outcome = book.apply_snapshot(snapshot(2, vec![level(dec!(100), dec!(2))], vec![]));
498
499 assert_eq!(outcome, ApplyOutcome::Synced);
500 assert_eq!(book.last_update_id(), Some(2));
501 }
502}