nautilus_model/data/
delta.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! An `OrderBookDelta` data type intended to carry book state information.
17
18use 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    GetTsInit,
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/// Represents a single change/delta in an order book.
35#[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    /// The instrument ID for the book.
44    pub instrument_id: InstrumentId,
45    /// The order book delta action.
46    pub action: BookAction,
47    /// The order to apply.
48    pub order: BookOrder,
49    /// The record flags bit field indicating event end and data information.
50    pub flags: u8,
51    /// The message sequence number assigned at the venue.
52    pub sequence: u64,
53    /// UNIX timestamp (nanoseconds) when the book event occurred.
54    pub ts_event: UnixNanos,
55    /// UNIX timestamp (nanoseconds) when the struct was initialized.
56    pub ts_init: UnixNanos,
57}
58
59impl OrderBookDelta {
60    /// Creates a new [`OrderBookDelta`] instance with correctness checking.
61    ///
62    /// # Errors
63    ///
64    /// This function returns an error:
65    /// - If `action` is [`BookAction::Add`] or [`BookAction::Update`] and `size` is not positive (> 0).
66    ///
67    /// # Notes
68    ///
69    /// PyO3 requires a `Result` type for proper error handling and stacktrace printing in Python.
70    pub fn new_checked(
71        instrument_id: InstrumentId,
72        action: BookAction,
73        order: BookOrder,
74        flags: u8,
75        sequence: u64,
76        ts_event: UnixNanos,
77        ts_init: UnixNanos,
78    ) -> anyhow::Result<Self> {
79        if matches!(action, BookAction::Add | BookAction::Update) {
80            check_positive_quantity(order.size, stringify!(order.size))?;
81        }
82
83        Ok(Self {
84            instrument_id,
85            action,
86            order,
87            flags,
88            sequence,
89            ts_event,
90            ts_init,
91        })
92    }
93
94    /// Creates a new [`OrderBookDelta`] instance.
95    ///
96    /// # Panics
97    ///
98    /// This function panics:
99    /// - If `action` is [`BookAction::Add`] or [`BookAction::Update`] and `size` is not positive (> 0).
100    #[must_use]
101    pub fn new(
102        instrument_id: InstrumentId,
103        action: BookAction,
104        order: BookOrder,
105        flags: u8,
106        sequence: u64,
107        ts_event: UnixNanos,
108        ts_init: UnixNanos,
109    ) -> Self {
110        Self::new_checked(
111            instrument_id,
112            action,
113            order,
114            flags,
115            sequence,
116            ts_event,
117            ts_init,
118        )
119        .expect(FAILED)
120    }
121
122    /// Creates a new [`OrderBookDelta`] instance with a `Clear` action and NULL order.
123    #[must_use]
124    pub fn clear(
125        instrument_id: InstrumentId,
126        sequence: u64,
127        ts_event: UnixNanos,
128        ts_init: UnixNanos,
129    ) -> Self {
130        Self {
131            instrument_id,
132            action: BookAction::Clear,
133            order: NULL_ORDER,
134            flags: RecordFlag::F_SNAPSHOT as u8,
135            sequence,
136            ts_event,
137            ts_init,
138        }
139    }
140
141    /// Returns the metadata for the type, for use with serialization formats.
142    #[must_use]
143    pub fn get_metadata(
144        instrument_id: &InstrumentId,
145        price_precision: u8,
146        size_precision: u8,
147    ) -> HashMap<String, String> {
148        let mut metadata = HashMap::new();
149        metadata.insert("instrument_id".to_string(), instrument_id.to_string());
150        metadata.insert("price_precision".to_string(), price_precision.to_string());
151        metadata.insert("size_precision".to_string(), size_precision.to_string());
152        metadata
153    }
154
155    /// Returns the field map for the type, for use with Arrow schemas.
156    #[must_use]
157    pub fn get_fields() -> IndexMap<String, String> {
158        let mut metadata = IndexMap::new();
159        metadata.insert("action".to_string(), "UInt8".to_string());
160        metadata.insert("side".to_string(), "UInt8".to_string());
161        metadata.insert("price".to_string(), FIXED_SIZE_BINARY.to_string());
162        metadata.insert("size".to_string(), FIXED_SIZE_BINARY.to_string());
163        metadata.insert("order_id".to_string(), "UInt64".to_string());
164        metadata.insert("flags".to_string(), "UInt8".to_string());
165        metadata.insert("sequence".to_string(), "UInt64".to_string());
166        metadata.insert("ts_event".to_string(), "UInt64".to_string());
167        metadata.insert("ts_init".to_string(), "UInt64".to_string());
168        metadata
169    }
170}
171
172impl Display for OrderBookDelta {
173    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
174        write!(
175            f,
176            "{},{},{},{},{},{},{}",
177            self.instrument_id,
178            self.action,
179            self.order,
180            self.flags,
181            self.sequence,
182            self.ts_event,
183            self.ts_init
184        )
185    }
186}
187
188impl Serializable for OrderBookDelta {}
189
190impl GetTsInit for OrderBookDelta {
191    fn ts_init(&self) -> UnixNanos {
192        self.ts_init
193    }
194}
195
196////////////////////////////////////////////////////////////////////////////////
197// Tests
198////////////////////////////////////////////////////////////////////////////////
199#[cfg(test)]
200mod tests {
201    use nautilus_core::{UnixNanos, serialization::Serializable};
202    use rstest::rstest;
203
204    use crate::{
205        data::{BookOrder, OrderBookDelta, stubs::*},
206        enums::{BookAction, OrderSide},
207        identifiers::InstrumentId,
208        types::{Price, Quantity},
209    };
210
211    #[rstest]
212    fn test_order_book_delta_new_with_zero_size_panics() {
213        let instrument_id = InstrumentId::from("AAPL.XNAS");
214        let action = BookAction::Add;
215        let price = Price::from("100.00");
216        let zero_size = Quantity::from(0);
217        let side = OrderSide::Buy;
218        let order_id = 123_456;
219        let flags = 0;
220        let sequence = 1;
221        let ts_event = UnixNanos::from(0);
222        let ts_init = UnixNanos::from(1);
223
224        let order = BookOrder::new(side, price, zero_size, order_id);
225
226        let result = std::panic::catch_unwind(|| {
227            let _ = OrderBookDelta::new(
228                instrument_id,
229                action,
230                order,
231                flags,
232                sequence,
233                ts_event,
234                ts_init,
235            );
236        });
237        assert!(result.is_err());
238    }
239
240    #[rstest]
241    fn test_order_book_delta_new_checked_with_zero_size_error() {
242        let instrument_id = InstrumentId::from("AAPL.XNAS");
243        let action = BookAction::Add;
244        let price = Price::from("100.00");
245        let zero_size = Quantity::from(0);
246        let side = OrderSide::Buy;
247        let order_id = 123_456;
248        let flags = 0;
249        let sequence = 1;
250        let ts_event = UnixNanos::from(0);
251        let ts_init = UnixNanos::from(1);
252
253        let order = BookOrder::new(side, price, zero_size, order_id);
254
255        let result = OrderBookDelta::new_checked(
256            instrument_id,
257            action,
258            order,
259            flags,
260            sequence,
261            ts_event,
262            ts_init,
263        );
264
265        assert!(result.is_err());
266    }
267
268    #[rstest]
269    fn test_new() {
270        let instrument_id = InstrumentId::from("AAPL.XNAS");
271        let action = BookAction::Add;
272        let price = Price::from("100.00");
273        let size = Quantity::from("10");
274        let side = OrderSide::Buy;
275        let order_id = 123_456;
276        let flags = 0;
277        let sequence = 1;
278        let ts_event = 1;
279        let ts_init = 2;
280
281        let order = BookOrder::new(side, price, size, order_id);
282
283        let delta = OrderBookDelta::new(
284            instrument_id,
285            action,
286            order,
287            flags,
288            sequence,
289            ts_event.into(),
290            ts_init.into(),
291        );
292
293        assert_eq!(delta.instrument_id, instrument_id);
294        assert_eq!(delta.action, action);
295        assert_eq!(delta.order.price, price);
296        assert_eq!(delta.order.size, size);
297        assert_eq!(delta.order.side, side);
298        assert_eq!(delta.order.order_id, order_id);
299        assert_eq!(delta.flags, flags);
300        assert_eq!(delta.sequence, sequence);
301        assert_eq!(delta.ts_event, ts_event);
302        assert_eq!(delta.ts_init, ts_init);
303    }
304
305    #[rstest]
306    fn test_clear() {
307        let instrument_id = InstrumentId::from("AAPL.XNAS");
308        let sequence = 1;
309        let ts_event = 2;
310        let ts_init = 3;
311
312        let delta = OrderBookDelta::clear(instrument_id, sequence, ts_event.into(), ts_init.into());
313
314        assert_eq!(delta.instrument_id, instrument_id);
315        assert_eq!(delta.action, BookAction::Clear);
316        assert!(delta.order.price.is_zero());
317        assert!(delta.order.size.is_zero());
318        assert_eq!(delta.order.side, OrderSide::NoOrderSide);
319        assert_eq!(delta.order.order_id, 0);
320        assert_eq!(delta.flags, 32);
321        assert_eq!(delta.sequence, sequence);
322        assert_eq!(delta.ts_event, ts_event);
323        assert_eq!(delta.ts_init, ts_init);
324    }
325
326    #[rstest]
327    fn test_display(stub_delta: OrderBookDelta) {
328        let delta = stub_delta;
329        assert_eq!(
330            format!("{delta}"),
331            "AAPL.XNAS,ADD,BUY,100.00,10,123456,0,1,1,2".to_string()
332        );
333    }
334
335    #[rstest]
336    fn test_json_serialization(stub_delta: OrderBookDelta) {
337        let delta = stub_delta;
338        let serialized = delta.as_json_bytes().unwrap();
339        let deserialized = OrderBookDelta::from_json_bytes(serialized.as_ref()).unwrap();
340        assert_eq!(deserialized, delta);
341    }
342
343    #[rstest]
344    fn test_msgpack_serialization(stub_delta: OrderBookDelta) {
345        let delta = stub_delta;
346        let serialized = delta.as_msgpack_bytes().unwrap();
347        let deserialized = OrderBookDelta::from_msgpack_bytes(serialized.as_ref()).unwrap();
348        assert_eq!(deserialized, delta);
349    }
350}