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::{
19    collections::HashMap,
20    fmt::{Display, Formatter},
21    hash::Hash,
22};
23
24use indexmap::IndexMap;
25use nautilus_core::{correctness::FAILED, serialization::Serializable, UnixNanos};
26use serde::{Deserialize, Serialize};
27
28use super::{
29    order::{BookOrder, NULL_ORDER},
30    GetTsInit,
31};
32use crate::{
33    enums::{BookAction, RecordFlag},
34    identifiers::InstrumentId,
35    types::quantity::check_positive_quantity,
36};
37
38/// Represents a single change/delta in an order book.
39#[repr(C)]
40#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
41#[serde(tag = "type")]
42#[cfg_attr(
43    feature = "python",
44    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
45)]
46pub struct OrderBookDelta {
47    /// The instrument ID for the book.
48    pub instrument_id: InstrumentId,
49    /// The order book delta action.
50    pub action: BookAction,
51    /// The order to apply.
52    pub order: BookOrder,
53    /// The record flags bit field indicating event end and data information.
54    pub flags: u8,
55    /// The message sequence number assigned at the venue.
56    pub sequence: u64,
57    /// UNIX timestamp (nanoseconds) when the book event occurred.
58    pub ts_event: UnixNanos,
59    /// UNIX timestamp (nanoseconds) when the struct was initialized.
60    pub ts_init: UnixNanos,
61}
62
63impl OrderBookDelta {
64    /// Creates a new [`OrderBookDelta`] instance with correctness checking.
65    ///
66    /// # Errors
67    ///
68    /// This function returns an error:
69    /// - If `action` is [`BookAction::Add`] or [`BookAction::Update`] and `size` is not positive (> 0).
70    ///
71    /// # Notes
72    ///
73    /// PyO3 requires a `Result` type for proper error handling and stacktrace printing in Python.
74    pub fn new_checked(
75        instrument_id: InstrumentId,
76        action: BookAction,
77        order: BookOrder,
78        flags: u8,
79        sequence: u64,
80        ts_event: UnixNanos,
81        ts_init: UnixNanos,
82    ) -> anyhow::Result<Self> {
83        if matches!(action, BookAction::Add | BookAction::Update) {
84            check_positive_quantity(order.size.raw, "order.size.raw")?;
85        }
86
87        Ok(Self {
88            instrument_id,
89            action,
90            order,
91            flags,
92            sequence,
93            ts_event,
94            ts_init,
95        })
96    }
97
98    /// Creates a new [`OrderBookDelta`] instance.
99    ///
100    /// # Panics
101    ///
102    /// This function panics:
103    /// - If `action` is [`BookAction::Add`] or [`BookAction::Update`] and `size` is not positive (> 0).
104    #[must_use]
105    pub fn new(
106        instrument_id: InstrumentId,
107        action: BookAction,
108        order: BookOrder,
109        flags: u8,
110        sequence: u64,
111        ts_event: UnixNanos,
112        ts_init: UnixNanos,
113    ) -> Self {
114        Self::new_checked(
115            instrument_id,
116            action,
117            order,
118            flags,
119            sequence,
120            ts_event,
121            ts_init,
122        )
123        .expect(FAILED)
124    }
125
126    /// Creates a new [`OrderBookDelta`] instance with a `Clear` action and NULL order.
127    #[must_use]
128    pub fn clear(
129        instrument_id: InstrumentId,
130        sequence: u64,
131        ts_event: UnixNanos,
132        ts_init: UnixNanos,
133    ) -> Self {
134        Self {
135            instrument_id,
136            action: BookAction::Clear,
137            order: NULL_ORDER,
138            flags: RecordFlag::F_SNAPSHOT as u8,
139            sequence,
140            ts_event,
141            ts_init,
142        }
143    }
144
145    /// Returns the metadata for the type, for use with serialization formats.
146    #[must_use]
147    pub fn get_metadata(
148        instrument_id: &InstrumentId,
149        price_precision: u8,
150        size_precision: u8,
151    ) -> HashMap<String, String> {
152        let mut metadata = HashMap::new();
153        metadata.insert("instrument_id".to_string(), instrument_id.to_string());
154        metadata.insert("price_precision".to_string(), price_precision.to_string());
155        metadata.insert("size_precision".to_string(), size_precision.to_string());
156        metadata
157    }
158
159    /// Returns the field map for the type, for use with Arrow schemas.
160    #[must_use]
161    pub fn get_fields() -> IndexMap<String, String> {
162        let mut metadata = IndexMap::new();
163        metadata.insert("action".to_string(), "UInt8".to_string());
164        metadata.insert("side".to_string(), "UInt8".to_string());
165        metadata.insert("price".to_string(), "Int64".to_string());
166        metadata.insert("size".to_string(), "UInt64".to_string());
167        metadata.insert("order_id".to_string(), "UInt64".to_string());
168        metadata.insert("flags".to_string(), "UInt8".to_string());
169        metadata.insert("sequence".to_string(), "UInt64".to_string());
170        metadata.insert("ts_event".to_string(), "UInt64".to_string());
171        metadata.insert("ts_init".to_string(), "UInt64".to_string());
172        metadata
173    }
174}
175
176impl Display for OrderBookDelta {
177    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
178        write!(
179            f,
180            "{},{},{},{},{},{},{}",
181            self.instrument_id,
182            self.action,
183            self.order,
184            self.flags,
185            self.sequence,
186            self.ts_event,
187            self.ts_init
188        )
189    }
190}
191
192impl Serializable for OrderBookDelta {}
193
194impl GetTsInit for OrderBookDelta {
195    fn ts_init(&self) -> UnixNanos {
196        self.ts_init
197    }
198}
199
200////////////////////////////////////////////////////////////////////////////////
201// Tests
202////////////////////////////////////////////////////////////////////////////////
203#[cfg(test)]
204mod tests {
205    use nautilus_core::{serialization::Serializable, UnixNanos};
206    use rstest::rstest;
207
208    use crate::{
209        data::{stubs::*, BookOrder, OrderBookDelta},
210        enums::{BookAction, OrderSide},
211        identifiers::InstrumentId,
212        types::{Price, Quantity},
213    };
214
215    #[rstest]
216    fn test_order_book_delta_new_with_zero_size_panics() {
217        let instrument_id = InstrumentId::from("AAPL.XNAS");
218        let action = BookAction::Add;
219        let price = Price::from("100.00");
220        let zero_size = Quantity::from(0);
221        let side = OrderSide::Buy;
222        let order_id = 123_456;
223        let flags = 0;
224        let sequence = 1;
225        let ts_event = UnixNanos::from(0);
226        let ts_init = UnixNanos::from(1);
227
228        let order = BookOrder::new(side, price, zero_size, order_id);
229
230        let result = std::panic::catch_unwind(|| {
231            let _ = OrderBookDelta::new(
232                instrument_id,
233                action,
234                order,
235                flags,
236                sequence,
237                ts_event,
238                ts_init,
239            );
240        });
241        assert!(result.is_err());
242    }
243
244    #[rstest]
245    fn test_order_book_delta_new_checked_with_zero_size_error() {
246        let instrument_id = InstrumentId::from("AAPL.XNAS");
247        let action = BookAction::Add;
248        let price = Price::from("100.00");
249        let zero_size = Quantity::from(0);
250        let side = OrderSide::Buy;
251        let order_id = 123_456;
252        let flags = 0;
253        let sequence = 1;
254        let ts_event = UnixNanos::from(0);
255        let ts_init = UnixNanos::from(1);
256
257        let order = BookOrder::new(side, price, zero_size, order_id);
258
259        let result = OrderBookDelta::new_checked(
260            instrument_id,
261            action,
262            order,
263            flags,
264            sequence,
265            ts_event,
266            ts_init,
267        );
268
269        assert!(result.is_err());
270    }
271
272    #[rstest]
273    fn test_new() {
274        let instrument_id = InstrumentId::from("AAPL.XNAS");
275        let action = BookAction::Add;
276        let price = Price::from("100.00");
277        let size = Quantity::from("10");
278        let side = OrderSide::Buy;
279        let order_id = 123_456;
280        let flags = 0;
281        let sequence = 1;
282        let ts_event = 1;
283        let ts_init = 2;
284
285        let order = BookOrder::new(side, price, size, order_id);
286
287        let delta = OrderBookDelta::new(
288            instrument_id,
289            action,
290            order,
291            flags,
292            sequence,
293            ts_event.into(),
294            ts_init.into(),
295        );
296
297        assert_eq!(delta.instrument_id, instrument_id);
298        assert_eq!(delta.action, action);
299        assert_eq!(delta.order.price, price);
300        assert_eq!(delta.order.size, size);
301        assert_eq!(delta.order.side, side);
302        assert_eq!(delta.order.order_id, order_id);
303        assert_eq!(delta.flags, flags);
304        assert_eq!(delta.sequence, sequence);
305        assert_eq!(delta.ts_event, ts_event);
306        assert_eq!(delta.ts_init, ts_init);
307    }
308
309    #[rstest]
310    fn test_clear() {
311        let instrument_id = InstrumentId::from("AAPL.XNAS");
312        let sequence = 1;
313        let ts_event = 2;
314        let ts_init = 3;
315
316        let delta = OrderBookDelta::clear(instrument_id, sequence, ts_event.into(), ts_init.into());
317
318        assert_eq!(delta.instrument_id, instrument_id);
319        assert_eq!(delta.action, BookAction::Clear);
320        assert!(delta.order.price.is_zero());
321        assert!(delta.order.size.is_zero());
322        assert_eq!(delta.order.side, OrderSide::NoOrderSide);
323        assert_eq!(delta.order.order_id, 0);
324        assert_eq!(delta.flags, 32);
325        assert_eq!(delta.sequence, sequence);
326        assert_eq!(delta.ts_event, ts_event);
327        assert_eq!(delta.ts_init, ts_init);
328    }
329
330    #[rstest]
331    fn test_display(stub_delta: OrderBookDelta) {
332        let delta = stub_delta;
333        assert_eq!(
334            format!("{delta}"),
335            "AAPL.XNAS,ADD,BUY,100.00,10,123456,0,1,1,2".to_string()
336        );
337    }
338
339    #[rstest]
340    fn test_json_serialization(stub_delta: OrderBookDelta) {
341        let delta = stub_delta;
342        let serialized = delta.as_json_bytes().unwrap();
343        let deserialized = OrderBookDelta::from_json_bytes(serialized.as_ref()).unwrap();
344        assert_eq!(deserialized, delta);
345    }
346
347    #[rstest]
348    fn test_msgpack_serialization(stub_delta: OrderBookDelta) {
349        let delta = stub_delta;
350        let serialized = delta.as_msgpack_bytes().unwrap();
351        let deserialized = OrderBookDelta::from_msgpack_bytes(serialized.as_ref()).unwrap();
352        assert_eq!(deserialized, delta);
353    }
354}