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