1use crate::{
8 books::{Asks, Bids, Level, OrderBook, OrderBookSide, OrderBookTimes},
9 subscription::book::OrderBookEvent,
10};
11use chrono::{DateTime, Utc};
12use ibapi::market_data::realtime::{MarketDepth, MarketDepths};
13use rust_decimal::Decimal;
14
15use super::decimal_from_f64;
16
17const IB_DEPTH_OP_DELETE: i32 = 2;
19
20#[derive(Debug, Default)]
25pub struct DepthAggregator {
26 bids: OrderBookSide<Bids>,
27 asks: OrderBookSide<Asks>,
28 sequence: u64,
29}
30
31impl DepthAggregator {
32 pub fn new() -> Self {
34 Self::default()
35 }
36
37 pub fn update(
52 &mut self,
53 depth: &MarketDepths,
54 time_received: DateTime<Utc>,
55 ) -> Option<OrderBookEvent> {
56 match depth {
57 MarketDepths::MarketDepth(d) => self.process_depth(d, time_received),
58 MarketDepths::MarketDepthL2(_) => {
59 tracing::trace!("Discarding MarketDepthL2 event (market maker data not tracked)");
62 None
63 }
64 MarketDepths::Reset => Some(self.on_venue_reset(time_received)),
65 }
66 }
67
68 fn process_depth(
69 &mut self,
70 depth: &MarketDepth,
71 time_received: DateTime<Utc>,
72 ) -> Option<OrderBookEvent> {
73 let price = decimal_from_f64(depth.price)?;
77
78 let size = if depth.operation == IB_DEPTH_OP_DELETE {
81 Decimal::ZERO
82 } else {
83 decimal_from_f64(depth.size)?
84 };
85
86 match depth.side {
88 0 => self.update_asks(price, size),
89 1 => self.update_bids(price, size),
90 other => {
91 tracing::warn!(
94 side = other,
95 price = %price,
96 "Unknown IB depth side, skipping"
97 );
98 return None;
99 }
100 }
101
102 self.sequence += 1;
103 Some(OrderBookEvent::Snapshot(self.to_order_book(time_received)))
104 }
105
106 fn update_bids(&mut self, price: Decimal, size: Decimal) {
107 let level = Level {
110 price,
111 amount: size,
112 };
113 self.bids
114 .upsert_single(level, |existing| existing.price.cmp(&level.price).reverse());
115 }
116
117 fn update_asks(&mut self, price: Decimal, size: Decimal) {
118 let level = Level {
119 price,
120 amount: size,
121 };
122 self.asks
123 .upsert_single(level, |existing| existing.price.cmp(&level.price));
124 }
125
126 fn to_order_book(&self, time_received: DateTime<Utc>) -> OrderBook {
127 OrderBook::from_sides(
128 self.sequence,
129 OrderBookTimes {
132 time_engine: None,
133 time_exchange: None,
134 time_received,
135 },
136 self.bids.clone(),
137 self.asks.clone(),
138 )
139 }
140
141 pub fn on_venue_reset(&mut self, time_received: DateTime<Utc>) -> OrderBookEvent {
159 self.bids = OrderBookSide::default();
160 self.asks = OrderBookSide::default();
161 self.sequence += 1;
162 OrderBookEvent::Snapshot(self.to_order_book(time_received))
163 }
164
165 pub fn clear(&mut self) {
172 self.bids = OrderBookSide::default();
173 self.asks = OrderBookSide::default();
174 self.sequence = 0;
175 }
176}
177
178#[cfg(test)]
179#[allow(clippy::unwrap_used)]
181mod tests {
182 use super::*;
183 use rust_decimal_macros::dec;
184
185 fn depth(side: i32, operation: i32, price: f64, size: f64) -> MarketDepths {
186 MarketDepths::MarketDepth(MarketDepth {
187 position: 0,
188 operation,
189 side,
190 price,
191 size,
192 })
193 }
194
195 fn tr() -> DateTime<Utc> {
197 DateTime::from_timestamp_millis(1_700_000_000_000).unwrap()
198 }
199
200 #[test]
201 fn order_book_has_only_time_received() {
202 let mut agg = DepthAggregator::new();
206
207 let event = agg.update(&depth(1, 0, 100.0, 10.0), tr()).unwrap();
208 match event {
209 OrderBookEvent::Snapshot(book) => {
210 assert_eq!(book.time_engine(), None);
211 assert_eq!(book.time_exchange(), None);
212 assert_eq!(book.time_received(), tr());
213 }
214 _ => panic!("Expected Snapshot"),
215 }
216 }
217
218 #[test]
219 fn insert_bid() {
220 let mut agg = DepthAggregator::new();
221
222 let event = agg.update(&depth(1, 0, 100.0, 10.0), tr()).unwrap();
223
224 match event {
225 OrderBookEvent::Snapshot(book) => {
226 assert_eq!(book.bids().levels().len(), 1);
227 assert_eq!(book.bids().levels()[0].price, dec!(100));
228 assert_eq!(book.bids().levels()[0].amount, dec!(10));
229 assert!(book.asks().levels().is_empty());
230 }
231 _ => panic!("Expected Snapshot"),
232 }
233 }
234
235 #[test]
236 fn insert_ask() {
237 let mut agg = DepthAggregator::new();
238
239 let event = agg.update(&depth(0, 0, 101.0, 5.0), tr()).unwrap();
240
241 match event {
242 OrderBookEvent::Snapshot(book) => {
243 assert_eq!(book.asks().levels().len(), 1);
244 assert_eq!(book.asks().levels()[0].price, dec!(101));
245 assert_eq!(book.asks().levels()[0].amount, dec!(5));
246 assert!(book.bids().levels().is_empty());
247 }
248 _ => panic!("Expected Snapshot"),
249 }
250 }
251
252 #[test]
253 fn update_level() {
254 let mut agg = DepthAggregator::new();
255
256 agg.update(&depth(1, 0, 100.0, 10.0), tr());
257 let event = agg.update(&depth(1, 1, 100.0, 15.0), tr()).unwrap();
258
259 match event {
260 OrderBookEvent::Snapshot(book) => {
261 assert_eq!(book.bids().levels().len(), 1);
262 assert_eq!(book.bids().levels()[0].amount, dec!(15));
263 }
264 _ => panic!("Expected Snapshot"),
265 }
266 }
267
268 #[test]
269 fn delete_level() {
270 let mut agg = DepthAggregator::new();
271
272 agg.update(&depth(1, 0, 100.0, 10.0), tr());
273 agg.update(&depth(1, 0, 99.0, 5.0), tr());
274 let event = agg.update(&depth(1, 2, 100.0, 0.0), tr()).unwrap();
275
276 match event {
277 OrderBookEvent::Snapshot(book) => {
278 assert_eq!(book.bids().levels().len(), 1);
279 assert_eq!(book.bids().levels()[0].price, dec!(99));
280 }
281 _ => panic!("Expected Snapshot"),
282 }
283 }
284
285 #[test]
286 fn multiple_levels() {
287 let mut agg = DepthAggregator::new();
288
289 agg.update(&depth(1, 0, 100.0, 10.0), tr());
290 agg.update(&depth(1, 0, 99.0, 20.0), tr());
291 agg.update(&depth(0, 0, 101.0, 5.0), tr());
292 let event = agg.update(&depth(0, 0, 102.0, 3.0), tr()).unwrap();
293
294 match event {
295 OrderBookEvent::Snapshot(book) => {
296 assert_eq!(book.bids().levels().len(), 2);
297 assert_eq!(book.asks().levels().len(), 2);
298
299 let bids = book.bids().levels();
301 assert!(
302 bids[0].price > bids[1].price,
303 "bids should be sorted descending: {:?}",
304 bids
305 );
306 let asks = book.asks().levels();
307 assert!(
308 asks[0].price < asks[1].price,
309 "asks should be sorted ascending: {:?}",
310 asks
311 );
312 }
313 _ => panic!("Expected Snapshot"),
314 }
315 }
316
317 #[test]
318 fn invalid_price_skipped() {
319 let mut agg = DepthAggregator::new();
320
321 let result = agg.update(&depth(1, 0, f64::NAN, 10.0), tr());
323 assert!(result.is_none());
324
325 let result = agg.update(&depth(1, 0, f64::INFINITY, 10.0), tr());
327 assert!(result.is_none());
328
329 let result = agg.update(&depth(1, 0, 100.0, 10.0), tr());
331 assert!(result.is_some());
332 }
333
334 #[test]
335 fn delete_with_nan_size_removes_level() {
336 let mut agg = DepthAggregator::new();
337
338 agg.update(&depth(1, 0, 100.0, 10.0), tr());
340
341 let event = agg.update(&depth(1, 2, 100.0, f64::NAN), tr()).unwrap();
343
344 match event {
345 OrderBookEvent::Snapshot(book) => {
346 assert!(
347 book.bids().levels().is_empty(),
348 "Delete with NaN size should remove level"
349 );
350 }
351 _ => panic!("Expected Snapshot"),
352 }
353 }
354
355 #[test]
356 fn venue_reset_empties_the_book_and_returns_the_emptied_snapshot() {
357 let mut agg = DepthAggregator::new();
358 agg.update(&depth(1, 0, 100.0, 10.0), tr());
359 agg.update(&depth(0, 0, 101.0, 8.0), tr());
360 assert!(!agg.bids.levels().is_empty() && !agg.asks.levels().is_empty());
361
362 let event = agg.on_venue_reset(tr());
363
364 assert!(agg.bids.levels().is_empty(), "bids must be dropped");
365 assert!(agg.asks.levels().is_empty(), "asks must be dropped");
366 match event {
367 OrderBookEvent::Snapshot(book) => {
368 assert!(
369 book.bids().levels().is_empty() && book.asks().levels().is_empty(),
370 "the emitted snapshot must carry the emptied book, so a consumer \
371 replacing its state on a snapshot drops its own stale levels too"
372 );
373 }
374 _ => panic!("Expected Snapshot"),
375 }
376 }
377
378 #[test]
383 fn venue_reset_advances_the_sequence_so_the_emptied_book_is_strictly_newer() {
384 let mut agg = DepthAggregator::new();
385 agg.update(&depth(1, 0, 100.0, 10.0), tr());
386 agg.update(&depth(1, 0, 99.0, 5.0), tr());
387 agg.update(&depth(0, 0, 101.0, 8.0), tr());
388 assert_eq!(agg.sequence, 3);
389
390 let event = agg.on_venue_reset(tr());
391
392 assert_eq!(agg.sequence, 4, "sequence must advance, never rewind");
393 match event {
394 OrderBookEvent::Snapshot(book) => assert_eq!(book.sequence(), 4),
395 _ => panic!("Expected Snapshot"),
396 }
397 }
398
399 #[test]
400 fn a_reset_update_empties_the_book_like_a_venue_reset() {
401 let mut agg = DepthAggregator::new();
402 agg.update(&depth(1, 0, 100.0, 10.0), tr());
403 agg.update(&depth(0, 0, 101.0, 8.0), tr());
404
405 let event = agg.update(&MarketDepths::Reset, tr());
406
407 assert!(agg.bids.levels().is_empty() && agg.asks.levels().is_empty());
408 match event {
409 Some(OrderBookEvent::Snapshot(book)) => {
410 assert!(book.bids().levels().is_empty() && book.asks().levels().is_empty());
411 assert_eq!(book.sequence(), 3, "the reset advances the sequence");
412 }
413 other => panic!("Expected the emptied Snapshot, got {other:?}"),
414 }
415 }
416
417 #[test]
418 fn updates_after_a_venue_reset_rebuild_from_empty() {
419 let mut agg = DepthAggregator::new();
420 agg.update(&depth(1, 0, 100.0, 10.0), tr());
421 agg.on_venue_reset(tr());
422
423 let event = agg.update(&depth(1, 0, 50.0, 1.0), tr()).unwrap();
424 match event {
425 OrderBookEvent::Snapshot(book) => {
426 assert_eq!(book.bids().levels().len(), 1, "only the post-reset level");
427 assert_eq!(book.bids().levels()[0].price, dec!(50.0));
428 assert_eq!(book.sequence(), 3);
430 }
431 _ => panic!("Expected Snapshot"),
432 }
433 }
434
435 #[test]
436 fn clear_resets_state() {
437 let mut agg = DepthAggregator::new();
438
439 agg.update(&depth(1, 0, 100.0, 10.0), tr());
441 agg.update(&depth(1, 0, 99.0, 5.0), tr());
442 agg.update(&depth(0, 0, 101.0, 8.0), tr());
443 assert_eq!(agg.sequence, 3);
444
445 agg.clear();
447
448 assert_eq!(agg.sequence, 0, "sequence should reset to 0");
449 assert!(agg.bids.levels().is_empty(), "bids should be empty");
450 assert!(agg.asks.levels().is_empty(), "asks should be empty");
451
452 let event = agg.update(&depth(1, 0, 50.0, 1.0), tr()).unwrap();
454 match event {
455 OrderBookEvent::Snapshot(book) => {
456 assert_eq!(book.sequence(), 1);
457 assert_eq!(book.bids().levels().len(), 1);
458 }
459 _ => panic!("Expected Snapshot"),
460 }
461 }
462}