nautilus_model/data/
delta.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Posei Systems Pty Ltd. All rights reserved.
3//  https://poseitrader.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    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/// 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 = "posei_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 instance was created.
56    pub ts_init: UnixNanos,
57}
58
59impl OrderBookDelta {
60    /// Creates a new [`OrderBookDelta`] instance with correctness checking.
61    ///
62    /// # Errors
63    ///
64    /// Returns an error if `action` is [`BookAction::Add`] or [`BookAction::Update`] and `size` is not positive (> 0).
65    ///
66    /// # Notes
67    ///
68    /// PyO3 requires a `Result` type for proper error handling and stacktrace printing in Python.
69    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    /// Creates a new [`OrderBookDelta`] instance.
94    ///
95    /// # Panics
96    ///
97    /// Panics if `action` is [`BookAction::Add`] or [`BookAction::Update`] and `size` is not positive (> 0).
98    #[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    /// Creates a new [`OrderBookDelta`] instance with a `Clear` action and NULL order.
121    #[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    /// Returns the metadata for the type, for use with serialization formats.
140    #[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    /// Returns the field map for the type, for use with Arrow schemas.
154    #[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////////////////////////////////////////////////////////////////////////////////
195// Tests
196////////////////////////////////////////////////////////////////////////////////
197#[cfg(test)]
198mod tests {
199    use nautilus_core::{UnixNanos, serialization::Serializable};
200    use rstest::rstest;
201
202    use crate::{
203        data::{BookOrder, OrderBookDelta, stubs::*},
204        enums::{BookAction, OrderSide},
205        identifiers::InstrumentId,
206        types::{Price, Quantity},
207    };
208
209    #[rstest]
210    fn test_order_book_delta_new_with_zero_size_panics() {
211        let instrument_id = InstrumentId::from("AAPL.XNAS");
212        let action = BookAction::Add;
213        let price = Price::from("100.00");
214        let zero_size = Quantity::from(0);
215        let side = OrderSide::Buy;
216        let order_id = 123_456;
217        let flags = 0;
218        let sequence = 1;
219        let ts_event = UnixNanos::from(0);
220        let ts_init = UnixNanos::from(1);
221
222        let order = BookOrder::new(side, price, zero_size, order_id);
223
224        let result = std::panic::catch_unwind(|| {
225            let _ = OrderBookDelta::new(
226                instrument_id,
227                action,
228                order,
229                flags,
230                sequence,
231                ts_event,
232                ts_init,
233            );
234        });
235        assert!(result.is_err());
236    }
237
238    #[rstest]
239    fn test_order_book_delta_new_checked_with_zero_size_error() {
240        let instrument_id = InstrumentId::from("AAPL.XNAS");
241        let action = BookAction::Add;
242        let price = Price::from("100.00");
243        let zero_size = Quantity::from(0);
244        let side = OrderSide::Buy;
245        let order_id = 123_456;
246        let flags = 0;
247        let sequence = 1;
248        let ts_event = UnixNanos::from(0);
249        let ts_init = UnixNanos::from(1);
250
251        let order = BookOrder::new(side, price, zero_size, order_id);
252
253        let result = OrderBookDelta::new_checked(
254            instrument_id,
255            action,
256            order,
257            flags,
258            sequence,
259            ts_event,
260            ts_init,
261        );
262
263        assert!(result.is_err());
264    }
265
266    #[rstest]
267    fn test_new() {
268        let instrument_id = InstrumentId::from("AAPL.XNAS");
269        let action = BookAction::Add;
270        let price = Price::from("100.00");
271        let size = Quantity::from("10");
272        let side = OrderSide::Buy;
273        let order_id = 123_456;
274        let flags = 0;
275        let sequence = 1;
276        let ts_event = 1;
277        let ts_init = 2;
278
279        let order = BookOrder::new(side, price, size, order_id);
280
281        let delta = OrderBookDelta::new(
282            instrument_id,
283            action,
284            order,
285            flags,
286            sequence,
287            ts_event.into(),
288            ts_init.into(),
289        );
290
291        assert_eq!(delta.instrument_id, instrument_id);
292        assert_eq!(delta.action, action);
293        assert_eq!(delta.order.price, price);
294        assert_eq!(delta.order.size, size);
295        assert_eq!(delta.order.side, side);
296        assert_eq!(delta.order.order_id, order_id);
297        assert_eq!(delta.flags, flags);
298        assert_eq!(delta.sequence, sequence);
299        assert_eq!(delta.ts_event, ts_event);
300        assert_eq!(delta.ts_init, ts_init);
301    }
302
303    #[rstest]
304    fn test_clear() {
305        let instrument_id = InstrumentId::from("AAPL.XNAS");
306        let sequence = 1;
307        let ts_event = 2;
308        let ts_init = 3;
309
310        let delta = OrderBookDelta::clear(instrument_id, sequence, ts_event.into(), ts_init.into());
311
312        assert_eq!(delta.instrument_id, instrument_id);
313        assert_eq!(delta.action, BookAction::Clear);
314        assert!(delta.order.price.is_zero());
315        assert!(delta.order.size.is_zero());
316        assert_eq!(delta.order.side, OrderSide::NoOrderSide);
317        assert_eq!(delta.order.order_id, 0);
318        assert_eq!(delta.flags, 32);
319        assert_eq!(delta.sequence, sequence);
320        assert_eq!(delta.ts_event, ts_event);
321        assert_eq!(delta.ts_init, ts_init);
322    }
323
324    #[rstest]
325    fn test_display(stub_delta: OrderBookDelta) {
326        let delta = stub_delta;
327        assert_eq!(
328            format!("{delta}"),
329            "AAPL.XNAS,ADD,BUY,100.00,10,123456,0,1,1,2".to_string()
330        );
331    }
332
333    #[rstest]
334    fn test_json_serialization(stub_delta: OrderBookDelta) {
335        let delta = stub_delta;
336        let serialized = delta.to_json_bytes().unwrap();
337        let deserialized = OrderBookDelta::from_json_bytes(serialized.as_ref()).unwrap();
338        assert_eq!(deserialized, delta);
339    }
340
341    #[rstest]
342    fn test_msgpack_serialization(stub_delta: OrderBookDelta) {
343        let delta = stub_delta;
344        let serialized = delta.to_msgpack_bytes().unwrap();
345        let deserialized = OrderBookDelta::from_msgpack_bytes(serialized.as_ref()).unwrap();
346        assert_eq!(deserialized, delta);
347    }
348}