1use std::{collections::HashMap, fmt::Display, hash::Hash};
19
20use indexmap::IndexMap;
21use nautilus_core::{UnixNanos, correctness::FAILED, serialization::Serializable};
22use serde::{Deserialize, Serialize};
23
24use super::{
25 HasTsInit,
26 order::{BookOrder, NULL_ORDER},
27};
28use crate::{
29 enums::{BookAction, RecordFlag},
30 identifiers::InstrumentId,
31 types::{fixed::FIXED_SIZE_BINARY, quantity::check_positive_quantity},
32};
33
34#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
36#[serde(tag = "type")]
37#[cfg_attr(
38 feature = "python",
39 pyo3::pyclass(module = "nautilus_trader.model", from_py_object)
40)]
41#[cfg_attr(
42 feature = "python",
43 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
44)]
45pub struct OrderBookDelta {
46 pub instrument_id: InstrumentId,
48 pub action: BookAction,
50 pub order: BookOrder,
52 pub flags: u8,
54 pub sequence: u64,
56 pub ts_event: UnixNanos,
58 pub ts_init: UnixNanos,
60}
61
62impl OrderBookDelta {
63 pub fn new_checked(
73 instrument_id: InstrumentId,
74 action: BookAction,
75 order: BookOrder,
76 flags: u8,
77 sequence: u64,
78 ts_event: UnixNanos,
79 ts_init: UnixNanos,
80 ) -> anyhow::Result<Self> {
81 if matches!(action, BookAction::Add | BookAction::Update) {
82 check_positive_quantity(order.size, stringify!(order.size))?;
83 }
84
85 Ok(Self {
86 instrument_id,
87 action,
88 order,
89 flags,
90 sequence,
91 ts_event,
92 ts_init,
93 })
94 }
95
96 #[must_use]
102 pub fn new(
103 instrument_id: InstrumentId,
104 action: BookAction,
105 order: BookOrder,
106 flags: u8,
107 sequence: u64,
108 ts_event: UnixNanos,
109 ts_init: UnixNanos,
110 ) -> Self {
111 Self::new_checked(
112 instrument_id,
113 action,
114 order,
115 flags,
116 sequence,
117 ts_event,
118 ts_init,
119 )
120 .expect(FAILED)
121 }
122
123 #[must_use]
125 pub fn clear(
126 instrument_id: InstrumentId,
127 sequence: u64,
128 ts_event: UnixNanos,
129 ts_init: UnixNanos,
130 ) -> Self {
131 Self {
132 instrument_id,
133 action: BookAction::Clear,
134 order: NULL_ORDER,
135 flags: RecordFlag::F_SNAPSHOT as u8,
136 sequence,
137 ts_event,
138 ts_init,
139 }
140 }
141
142 #[must_use]
144 pub fn get_metadata(
145 instrument_id: &InstrumentId,
146 price_precision: u8,
147 size_precision: u8,
148 ) -> HashMap<String, String> {
149 let mut metadata = HashMap::new();
150 metadata.insert("instrument_id".to_string(), instrument_id.to_string());
151 metadata.insert("price_precision".to_string(), price_precision.to_string());
152 metadata.insert("size_precision".to_string(), size_precision.to_string());
153 metadata
154 }
155
156 #[must_use]
158 pub fn get_fields() -> IndexMap<String, String> {
159 let mut metadata = IndexMap::new();
160 metadata.insert("action".to_string(), "UInt8".to_string());
161 metadata.insert("side".to_string(), "UInt8".to_string());
162 metadata.insert("price".to_string(), FIXED_SIZE_BINARY.to_string());
163 metadata.insert("size".to_string(), FIXED_SIZE_BINARY.to_string());
164 metadata.insert("order_id".to_string(), "UInt64".to_string());
165 metadata.insert("flags".to_string(), "UInt8".to_string());
166 metadata.insert("sequence".to_string(), "UInt64".to_string());
167 metadata.insert("ts_event".to_string(), "UInt64".to_string());
168 metadata.insert("ts_init".to_string(), "UInt64".to_string());
169 metadata
170 }
171}
172
173impl Display for OrderBookDelta {
174 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
175 write!(
176 f,
177 "{},{},{},{},{},{},{}",
178 self.instrument_id,
179 self.action,
180 self.order,
181 self.flags,
182 self.sequence,
183 self.ts_event,
184 self.ts_init
185 )
186 }
187}
188
189impl Serializable for OrderBookDelta {}
190
191impl HasTsInit for OrderBookDelta {
192 fn ts_init(&self) -> UnixNanos {
193 self.ts_init
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use std::{
200 collections::hash_map::DefaultHasher,
201 hash::{Hash, Hasher},
202 };
203
204 use nautilus_core::{
205 UnixNanos,
206 serialization::{
207 Serializable,
208 msgpack::{FromMsgPack, ToMsgPack},
209 },
210 };
211 use rstest::rstest;
212
213 use crate::{
214 data::{BookOrder, HasTsInit, OrderBookDelta, stubs::*},
215 enums::{BookAction, OrderSide, RecordFlag},
216 identifiers::InstrumentId,
217 types::{Price, Quantity},
218 };
219
220 fn create_test_delta() -> OrderBookDelta {
221 let order = BookOrder::new(
222 OrderSide::Buy,
223 Price::from("1.0500"),
224 Quantity::from("100000"),
225 12345,
226 );
227 OrderBookDelta::new(
228 InstrumentId::from("EURUSD.SIM"),
229 BookAction::Add,
230 order,
231 0,
232 123,
233 UnixNanos::from(1_000_000_000),
234 UnixNanos::from(2_000_000_000),
235 )
236 }
237
238 #[rstest]
239 fn test_order_book_delta_new() {
240 let delta = create_test_delta();
241
242 assert_eq!(delta.instrument_id, InstrumentId::from("EURUSD.SIM"));
243 assert_eq!(delta.action, BookAction::Add);
244 assert_eq!(delta.order.side, OrderSide::Buy.into());
245 assert_eq!(delta.order.price, Price::from("1.0500"));
246 assert_eq!(delta.order.size, Quantity::from("100000"));
247 assert_eq!(delta.order.order_id, 12345);
248 assert_eq!(delta.flags, 0);
249 assert_eq!(delta.sequence, 123);
250 assert_eq!(delta.ts_event, UnixNanos::from(1_000_000_000));
251 assert_eq!(delta.ts_init, UnixNanos::from(2_000_000_000));
252 }
253
254 #[rstest]
255 fn test_order_book_delta_new_checked_valid() {
256 let order = BookOrder::new(
257 OrderSide::Sell,
258 Price::from("1.0505"),
259 Quantity::from("50000"),
260 67890,
261 );
262 let result = OrderBookDelta::new_checked(
263 InstrumentId::from("GBPUSD.SIM"),
264 BookAction::Update,
265 order,
266 16,
267 456,
268 UnixNanos::from(500_000_000),
269 UnixNanos::from(1_500_000_000),
270 );
271
272 assert!(result.is_ok());
273 let delta = result.unwrap();
274 assert_eq!(delta.instrument_id, InstrumentId::from("GBPUSD.SIM"));
275 assert_eq!(delta.action, BookAction::Update);
276 assert_eq!(delta.order.side, OrderSide::Sell.into());
277 assert_eq!(delta.flags, 16);
278 }
279
280 #[rstest]
281 #[should_panic(expected = "invalid `Quantity` for 'order.size' not positive, was 0")]
282 fn test_order_book_delta_new_with_zero_size_panics() {
283 let instrument_id = InstrumentId::from("AAPL.XNAS");
284 let action = BookAction::Add;
285 let price = Price::from("100.00");
286 let zero_size = Quantity::from(0);
287 let side = OrderSide::Buy;
288 let order_id = 123_456;
289 let flags = 0;
290 let sequence = 1;
291 let ts_event = UnixNanos::from(0);
292 let ts_init = UnixNanos::from(1);
293
294 let order = BookOrder::new(side, price, zero_size, order_id);
295
296 let _ = OrderBookDelta::new(
297 instrument_id,
298 action,
299 order,
300 flags,
301 sequence,
302 ts_event,
303 ts_init,
304 );
305 }
306
307 #[rstest]
308 fn test_order_book_delta_new_checked_with_zero_size_error() {
309 let instrument_id = InstrumentId::from("AAPL.XNAS");
310 let action = BookAction::Add;
311 let price = Price::from("100.00");
312 let zero_size = Quantity::from(0);
313 let side = OrderSide::Buy;
314 let order_id = 123_456;
315 let flags = 0;
316 let sequence = 1;
317 let ts_event = UnixNanos::from(0);
318 let ts_init = UnixNanos::from(1);
319
320 let order = BookOrder::new(side, price, zero_size, order_id);
321
322 let result = OrderBookDelta::new_checked(
323 instrument_id,
324 action,
325 order,
326 flags,
327 sequence,
328 ts_event,
329 ts_init,
330 );
331
332 assert!(result.is_err());
333 assert!(
334 result
335 .unwrap_err()
336 .to_string()
337 .contains("invalid `Quantity` for 'order.size' not positive")
338 );
339 }
340
341 #[rstest]
342 fn test_order_book_delta_new_checked_delete_with_zero_size_ok() {
343 let order = BookOrder::new(
344 OrderSide::Buy,
345 Price::from("100.00"),
346 Quantity::from(0),
347 123_456,
348 );
349 let result = OrderBookDelta::new_checked(
350 InstrumentId::from("TEST.SIM"),
351 BookAction::Delete,
352 order,
353 0,
354 1,
355 UnixNanos::from(0),
356 UnixNanos::from(1),
357 );
358
359 assert!(result.is_ok());
360 }
361
362 #[rstest]
363 fn test_order_book_delta_clear() {
364 let instrument_id = InstrumentId::from("BTCUSD.CRYPTO");
365 let sequence = 999;
366 let ts_event = UnixNanos::from(3_000_000_000);
367 let ts_init = UnixNanos::from(4_000_000_000);
368
369 let delta = OrderBookDelta::clear(instrument_id, sequence, ts_event, ts_init);
370
371 assert_eq!(delta.instrument_id, instrument_id);
372 assert_eq!(delta.action, BookAction::Clear);
373 assert!(delta.order.price.is_zero());
374 assert!(delta.order.size.is_zero());
375 assert_eq!(delta.order.side, None);
376 assert_eq!(delta.order.order_id, 0);
377 assert_eq!(delta.flags, RecordFlag::F_SNAPSHOT as u8);
378 assert_eq!(delta.sequence, sequence);
379 assert_eq!(delta.ts_event, ts_event);
380 assert_eq!(delta.ts_init, ts_init);
381 }
382
383 #[rstest]
384 fn test_get_metadata() {
385 let instrument_id = InstrumentId::from("EURUSD.SIM");
386 let metadata = OrderBookDelta::get_metadata(&instrument_id, 5, 8);
387
388 assert_eq!(metadata.len(), 3);
389 assert_eq!(
390 metadata.get("instrument_id"),
391 Some(&"EURUSD.SIM".to_string())
392 );
393 assert_eq!(metadata.get("price_precision"), Some(&"5".to_string()));
394 assert_eq!(metadata.get("size_precision"), Some(&"8".to_string()));
395 }
396
397 #[rstest]
398 fn test_get_fields() {
399 let fields = OrderBookDelta::get_fields();
400
401 assert_eq!(fields.len(), 9);
402 assert_eq!(fields.get("action"), Some(&"UInt8".to_string()));
403 assert_eq!(fields.get("side"), Some(&"UInt8".to_string()));
404
405 #[cfg(feature = "high-precision")]
406 {
407 assert_eq!(
408 fields.get("price"),
409 Some(&"FixedSizeBinary(16)".to_string())
410 );
411 assert_eq!(fields.get("size"), Some(&"FixedSizeBinary(16)".to_string()));
412 }
413 #[cfg(not(feature = "high-precision"))]
414 {
415 assert_eq!(fields.get("price"), Some(&"FixedSizeBinary(8)".to_string()));
416 assert_eq!(fields.get("size"), Some(&"FixedSizeBinary(8)".to_string()));
417 }
418
419 assert_eq!(fields.get("order_id"), Some(&"UInt64".to_string()));
420 assert_eq!(fields.get("flags"), Some(&"UInt8".to_string()));
421 assert_eq!(fields.get("sequence"), Some(&"UInt64".to_string()));
422 assert_eq!(fields.get("ts_event"), Some(&"UInt64".to_string()));
423 assert_eq!(fields.get("ts_init"), Some(&"UInt64".to_string()));
424 }
425
426 #[rstest]
427 #[case(BookAction::Add)]
428 #[case(BookAction::Update)]
429 #[case(BookAction::Delete)]
430 #[case(BookAction::Clear)]
431 fn test_order_book_delta_with_different_actions(#[case] action: BookAction) {
432 let order = BookOrder::new(
433 OrderSide::Buy,
434 Price::from("100.00"),
435 if matches!(action, BookAction::Delete | BookAction::Clear) {
436 Quantity::from(0)
437 } else {
438 Quantity::from("1000")
439 },
440 123_456,
441 );
442
443 let result = if matches!(action, BookAction::Clear) {
444 Ok(OrderBookDelta::clear(
445 InstrumentId::from("TEST.SIM"),
446 1,
447 UnixNanos::from(1_000_000_000),
448 UnixNanos::from(2_000_000_000),
449 ))
450 } else {
451 OrderBookDelta::new_checked(
452 InstrumentId::from("TEST.SIM"),
453 action,
454 order,
455 0,
456 1,
457 UnixNanos::from(1_000_000_000),
458 UnixNanos::from(2_000_000_000),
459 )
460 };
461
462 assert!(result.is_ok());
463 let delta = result.unwrap();
464 assert_eq!(delta.action, action);
465 }
466
467 #[rstest]
468 #[case(OrderSide::Buy)]
469 #[case(OrderSide::Sell)]
470 fn test_order_book_delta_with_different_sides(#[case] side: OrderSide) {
471 let order = BookOrder::new(side, Price::from("100.00"), Quantity::from("1000"), 123_456);
472
473 let delta = OrderBookDelta::new(
474 InstrumentId::from("TEST.SIM"),
475 BookAction::Add,
476 order,
477 0,
478 1,
479 UnixNanos::from(1_000_000_000),
480 UnixNanos::from(2_000_000_000),
481 );
482
483 assert_eq!(delta.order.side, side.into());
484 }
485
486 #[rstest]
487 fn test_order_book_delta_has_ts_init() {
488 let delta = create_test_delta();
489 assert_eq!(delta.ts_init(), UnixNanos::from(2_000_000_000));
490 }
491
492 #[rstest]
493 fn test_order_book_delta_display() {
494 let delta = create_test_delta();
495 let display_str = format!("{delta}");
496
497 assert!(display_str.contains("EURUSD.SIM"));
498 assert!(display_str.contains("ADD"));
499 assert!(display_str.contains("BUY"));
500 assert!(display_str.contains("1.0500"));
501 assert!(display_str.contains("100000"));
502 assert!(display_str.contains("12345"));
503 assert!(display_str.contains("123"));
504 }
505
506 #[rstest]
507 fn test_order_book_delta_with_zero_timestamps() {
508 let order = BookOrder::new(
509 OrderSide::Buy,
510 Price::from("100.00"),
511 Quantity::from("1000"),
512 123_456,
513 );
514 let delta = OrderBookDelta::new(
515 InstrumentId::from("TEST.SIM"),
516 BookAction::Add,
517 order,
518 0,
519 0,
520 UnixNanos::from(0),
521 UnixNanos::from(0),
522 );
523
524 assert_eq!(delta.sequence, 0);
525 assert_eq!(delta.ts_event, UnixNanos::from(0));
526 assert_eq!(delta.ts_init, UnixNanos::from(0));
527 }
528
529 #[rstest]
530 fn test_order_book_delta_with_max_values() {
531 let order = BookOrder::new(
532 OrderSide::Sell,
533 Price::from("999999.9999"),
534 Quantity::from("999999999.9999"),
535 u64::MAX,
536 );
537 let delta = OrderBookDelta::new(
538 InstrumentId::from("TEST.SIM"),
539 BookAction::Update,
540 order,
541 u8::MAX,
542 u64::MAX,
543 UnixNanos::from(u64::MAX),
544 UnixNanos::from(u64::MAX),
545 );
546
547 assert_eq!(delta.flags, u8::MAX);
548 assert_eq!(delta.sequence, u64::MAX);
549 assert_eq!(delta.order.order_id, u64::MAX);
550 assert_eq!(delta.ts_event, UnixNanos::from(u64::MAX));
551 assert_eq!(delta.ts_init, UnixNanos::from(u64::MAX));
552 }
553
554 #[rstest]
555 fn test_new() {
556 let instrument_id = InstrumentId::from("AAPL.XNAS");
557 let action = BookAction::Add;
558 let price = Price::from("100.00");
559 let size = Quantity::from("10");
560 let side = OrderSide::Buy;
561 let order_id = 123_456;
562 let flags = 0;
563 let sequence = 1;
564 let ts_event = 1;
565 let ts_init = 2;
566
567 let order = BookOrder::new(side, price, size, order_id);
568
569 let delta = OrderBookDelta::new(
570 instrument_id,
571 action,
572 order,
573 flags,
574 sequence,
575 ts_event.into(),
576 ts_init.into(),
577 );
578
579 assert_eq!(delta.instrument_id, instrument_id);
580 assert_eq!(delta.action, action);
581 assert_eq!(delta.order.price, price);
582 assert_eq!(delta.order.size, size);
583 assert_eq!(delta.order.side, side.into());
584 assert_eq!(delta.order.order_id, order_id);
585 assert_eq!(delta.flags, flags);
586 assert_eq!(delta.sequence, sequence);
587 assert_eq!(delta.ts_event, ts_event);
588 assert_eq!(delta.ts_init, ts_init);
589 }
590
591 #[rstest]
592 fn test_clear() {
593 let instrument_id = InstrumentId::from("AAPL.XNAS");
594 let sequence = 1;
595 let ts_event = 2;
596 let ts_init = 3;
597
598 let delta = OrderBookDelta::clear(instrument_id, sequence, ts_event.into(), ts_init.into());
599
600 assert_eq!(delta.instrument_id, instrument_id);
601 assert_eq!(delta.action, BookAction::Clear);
602 assert!(delta.order.price.is_zero());
603 assert!(delta.order.size.is_zero());
604 assert_eq!(delta.order.side, None);
605 assert_eq!(delta.order.order_id, 0);
606 assert_eq!(delta.flags, 32);
607 assert_eq!(delta.sequence, sequence);
608 assert_eq!(delta.ts_event, ts_event);
609 assert_eq!(delta.ts_init, ts_init);
610 }
611
612 #[rstest]
613 fn test_order_book_delta_hash() {
614 let delta1 = create_test_delta();
615 let delta2 = create_test_delta();
616
617 let mut hasher1 = DefaultHasher::new();
618 let mut hasher2 = DefaultHasher::new();
619
620 delta1.hash(&mut hasher1);
621 delta2.hash(&mut hasher2);
622
623 assert_eq!(hasher1.finish(), hasher2.finish());
624 }
625
626 #[rstest]
627 fn test_order_book_delta_hash_different_deltas() {
628 let delta1 = create_test_delta();
629 let order2 = BookOrder::new(
630 OrderSide::Sell,
631 Price::from("1.0505"),
632 Quantity::from("50000"),
633 67890,
634 );
635 let delta2 = OrderBookDelta::new(
636 InstrumentId::from("EURUSD.SIM"),
637 BookAction::Add,
638 order2,
639 0,
640 123,
641 UnixNanos::from(1_000_000_000),
642 UnixNanos::from(2_000_000_000),
643 );
644
645 let mut hasher1 = DefaultHasher::new();
646 let mut hasher2 = DefaultHasher::new();
647
648 delta1.hash(&mut hasher1);
649 delta2.hash(&mut hasher2);
650
651 assert_ne!(hasher1.finish(), hasher2.finish());
652 }
653
654 #[rstest]
655 fn test_order_book_delta_partial_eq() {
656 let delta1 = create_test_delta();
657 let delta2 = create_test_delta();
658
659 assert_eq!(delta1, delta2);
661
662 let order3 = BookOrder::new(
664 OrderSide::Buy,
665 Price::from("1.0500"),
666 Quantity::from("100000"),
667 12345,
668 );
669 let delta3 = OrderBookDelta::new(
670 InstrumentId::from("GBPUSD.SIM"),
671 BookAction::Add,
672 order3,
673 0,
674 123,
675 UnixNanos::from(1_000_000_000),
676 UnixNanos::from(2_000_000_000),
677 );
678
679 assert_ne!(delta1, delta3);
680 }
681
682 #[rstest]
683 fn test_order_book_delta_clone() {
684 let delta1 = create_test_delta();
685 let delta2 = delta1;
686
687 assert_eq!(delta1, delta2);
688 assert_eq!(delta1.instrument_id, delta2.instrument_id);
689 assert_eq!(delta1.action, delta2.action);
690 assert_eq!(delta1.order, delta2.order);
691 assert_eq!(delta1.flags, delta2.flags);
692 assert_eq!(delta1.sequence, delta2.sequence);
693 assert_eq!(delta1.ts_event, delta2.ts_event);
694 assert_eq!(delta1.ts_init, delta2.ts_init);
695 }
696
697 #[rstest]
698 fn test_order_book_delta_debug() {
699 let delta = create_test_delta();
700 let debug_str = format!("{delta:?}");
701
702 assert!(debug_str.contains("OrderBookDelta"));
703 assert!(debug_str.contains("EURUSD.SIM"));
704 assert!(debug_str.contains("Add"));
705 assert!(debug_str.contains("BUY"));
706 assert!(debug_str.contains("1.0500"));
707 }
708
709 #[rstest]
710 fn test_order_book_delta_serialization() {
711 let delta = create_test_delta();
712
713 let json = serde_json::to_string(&delta).unwrap();
714 let deserialized: OrderBookDelta = serde_json::from_str(&json).unwrap();
715
716 assert_eq!(delta, deserialized);
717 }
718
719 #[rstest]
720 fn test_json_serialization(stub_delta: OrderBookDelta) {
721 let delta = stub_delta;
722 let serialized = delta.to_json_bytes().unwrap();
723 let deserialized = OrderBookDelta::from_json_bytes(serialized.as_ref()).unwrap();
724 assert_eq!(deserialized, delta);
725 }
726
727 #[rstest]
728 fn test_msgpack_serialization(stub_delta: OrderBookDelta) {
729 let delta = stub_delta;
730 let serialized = delta.to_msgpack_bytes().unwrap();
731 let deserialized = OrderBookDelta::from_msgpack_bytes(serialized.as_ref()).unwrap();
732 assert_eq!(deserialized, delta);
733 }
734}