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