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