Skip to main content

nautilus_model/python/data/
delta.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 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
16use std::{
17    collections::{HashMap, hash_map::DefaultHasher},
18    hash::{Hash, Hasher},
19};
20
21use nautilus_core::{
22    python::{
23        IntoPyObjectNautilusExt,
24        serialization::{from_dict_pyo3, to_dict_pyo3},
25        to_pyvalue_err,
26    },
27    serialization::{
28        Serializable,
29        msgpack::{FromMsgPack, ToMsgPack},
30    },
31};
32use pyo3::{IntoPyObjectExt, basic::CompareOp, prelude::*, types::PyDict};
33
34use crate::{
35    data::{BookOrder, OrderBookDelta},
36    enums::BookAction,
37    identifiers::InstrumentId,
38    python::common::PY_MODULE_MODEL,
39};
40
41#[pymethods]
42#[pyo3_stub_gen::derive::gen_stub_pymethods]
43impl OrderBookDelta {
44    /// Represents a single change/delta in an order book.
45    #[new]
46    fn py_new(
47        instrument_id: InstrumentId,
48        action: BookAction,
49        order: BookOrder,
50        flags: u8,
51        sequence: u64,
52        ts_event: u64,
53        ts_init: u64,
54    ) -> PyResult<Self> {
55        Self::new_checked(
56            instrument_id,
57            action,
58            order,
59            flags,
60            sequence,
61            ts_event.into(),
62            ts_init.into(),
63        )
64        .map_err(to_pyvalue_err)
65    }
66
67    /// Creates a new `OrderBookDelta` instance with a `Clear` action and NULL order.
68    #[staticmethod]
69    #[pyo3(name = "clear")]
70    fn py_clear(instrument_id: InstrumentId, sequence: u64, ts_event: u64, ts_init: u64) -> Self {
71        Self::clear(instrument_id, sequence, ts_event.into(), ts_init.into())
72    }
73
74    fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py<PyAny> {
75        match op {
76            CompareOp::Eq => self.eq(other).into_py_any_unwrap(py),
77            CompareOp::Ne => self.ne(other).into_py_any_unwrap(py),
78            _ => py.NotImplemented(),
79        }
80    }
81
82    fn __hash__(&self) -> isize {
83        let mut h = DefaultHasher::new();
84        self.hash(&mut h);
85        h.finish() as isize
86    }
87
88    fn __repr__(&self) -> String {
89        format!("{self:?}")
90    }
91
92    fn __str__(&self) -> String {
93        self.to_string()
94    }
95
96    #[getter]
97    #[pyo3(name = "instrument_id")]
98    fn py_instrument_id(&self) -> InstrumentId {
99        self.instrument_id
100    }
101
102    #[getter]
103    #[pyo3(name = "action")]
104    fn py_action(&self) -> BookAction {
105        self.action
106    }
107
108    /// Returns whether the delta adds an order.
109    #[getter]
110    #[pyo3(name = "is_add")]
111    fn py_is_add(&self) -> bool {
112        self.is_add()
113    }
114
115    /// Returns whether the delta updates an order.
116    #[getter]
117    #[pyo3(name = "is_update")]
118    fn py_is_update(&self) -> bool {
119        self.is_update()
120    }
121
122    /// Returns whether the delta deletes an order.
123    #[getter]
124    #[pyo3(name = "is_delete")]
125    fn py_is_delete(&self) -> bool {
126        self.is_delete()
127    }
128
129    /// Returns whether the delta clears the order book.
130    #[getter]
131    #[pyo3(name = "is_clear")]
132    fn py_is_clear(&self) -> bool {
133        self.is_clear()
134    }
135
136    #[getter]
137    #[pyo3(name = "order")]
138    fn py_order(&self) -> BookOrder {
139        self.order
140    }
141
142    #[getter]
143    #[pyo3(name = "flags")]
144    fn py_flags(&self) -> u8 {
145        self.flags
146    }
147
148    #[getter]
149    #[pyo3(name = "sequence")]
150    fn py_sequence(&self) -> u64 {
151        self.sequence
152    }
153
154    #[getter]
155    #[pyo3(name = "ts_event")]
156    fn py_ts_event(&self) -> u64 {
157        self.ts_event.as_u64()
158    }
159
160    #[getter]
161    #[pyo3(name = "ts_init")]
162    fn py_ts_init(&self) -> u64 {
163        self.ts_init.as_u64()
164    }
165
166    #[staticmethod]
167    #[pyo3(name = "fully_qualified_name")]
168    fn py_fully_qualified_name() -> String {
169        format!("{}:{}", PY_MODULE_MODEL, stringify!(OrderBookDelta))
170    }
171
172    /// Returns the metadata for the type, for use with serialization formats.
173    #[staticmethod]
174    #[pyo3(name = "get_metadata")]
175    fn py_get_metadata(
176        instrument_id: &InstrumentId,
177        price_precision: u8,
178        size_precision: u8,
179    ) -> HashMap<String, String> {
180        Self::get_metadata(instrument_id, price_precision, size_precision)
181    }
182
183    /// Returns the field map for the type, for use with Arrow schemas.
184    #[staticmethod]
185    #[pyo3(name = "get_fields")]
186    fn py_get_fields(py: Python<'_>) -> PyResult<Bound<'_, PyDict>> {
187        let py_dict = PyDict::new(py);
188        for (k, v) in Self::get_fields() {
189            py_dict.set_item(k, v)?;
190        }
191
192        Ok(py_dict)
193    }
194
195    /// Returns a new object from the given dictionary representation.
196    #[staticmethod]
197    #[pyo3(name = "from_dict")]
198    fn py_from_dict(py: Python<'_>, values: Py<PyDict>) -> PyResult<Self> {
199        from_dict_pyo3(py, values)
200    }
201
202    /// Return a dictionary representation of the object.
203    #[pyo3(name = "to_dict")]
204    fn py_to_dict(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
205        to_dict_pyo3(py, self)
206    }
207
208    /// Return JSON encoded bytes representation of the object.
209    #[pyo3(name = "to_json_bytes")]
210    fn py_to_json_bytes(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
211        self.to_json_bytes()
212            .map_err(to_pyvalue_err)?
213            .into_py_any(py)
214    }
215
216    /// Return `MsgPack` encoded bytes representation of the object.
217    #[pyo3(name = "to_msgpack_bytes")]
218    fn py_to_msgpack_bytes(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
219        self.to_msgpack_bytes()
220            .map_err(to_pyvalue_err)?
221            .into_py_any(py)
222    }
223
224    fn __reduce__(&self, py: Python) -> PyResult<Py<PyAny>> {
225        let from_dict = py.get_type::<Self>().getattr("from_dict")?;
226        let dict = self.py_to_dict(py)?;
227        (from_dict, (dict,)).into_py_any(py)
228    }
229}
230
231#[pymethods]
232impl OrderBookDelta {
233    #[staticmethod]
234    #[pyo3(name = "from_json")]
235    fn py_from_json(data: &[u8]) -> PyResult<Self> {
236        Self::from_json_bytes(data).map_err(to_pyvalue_err)
237    }
238
239    #[staticmethod]
240    #[pyo3(name = "from_msgpack")]
241    fn py_from_msgpack(data: &[u8]) -> PyResult<Self> {
242        Self::from_msgpack_bytes(data).map_err(to_pyvalue_err)
243    }
244}
245
246#[cfg(test)]
247mod tests {
248
249    use rstest::rstest;
250
251    use super::*;
252    use crate::{
253        data::stubs::*,
254        enums::OrderSide,
255        types::{Price, Quantity},
256    };
257
258    #[rstest]
259    fn test_order_book_delta_py_new_with_zero_size_returns_error() {
260        Python::initialize();
261        Python::attach(|_py| {
262            let instrument_id = InstrumentId::from("AAPL.XNAS");
263            let action = BookAction::Add;
264            let zero_size = Quantity::from(0);
265            let price = Price::from("100.00");
266            let side = OrderSide::Buy;
267            let order_id = 123_456;
268            let flags = 0;
269            let sequence = 1;
270            let ts_event = 1;
271            let ts_init = 2;
272
273            let order = BookOrder::new(side, price, zero_size, order_id);
274
275            let result = OrderBookDelta::py_new(
276                instrument_id,
277                action,
278                order,
279                flags,
280                sequence,
281                ts_event,
282                ts_init,
283            );
284            assert!(result.is_err());
285        });
286    }
287
288    #[rstest]
289    fn test_to_dict(stub_delta: OrderBookDelta) {
290        let delta = stub_delta;
291
292        Python::initialize();
293        Python::attach(|py| {
294            let dict_string = delta.py_to_dict(py).unwrap().to_string();
295            let expected_string = "{'type': 'OrderBookDelta', 'instrument_id': 'AAPL.XNAS', 'action': 'ADD', 'order': {'side': 'BUY', 'price': '100.00', 'size': '10', 'order_id': 123456}, 'flags': 0, 'sequence': 1, 'ts_event': 1, 'ts_init': 2}";
296            assert_eq!(dict_string, expected_string);
297        });
298    }
299
300    #[rstest]
301    fn test_from_dict(stub_delta: OrderBookDelta) {
302        let delta = stub_delta;
303
304        Python::initialize();
305        Python::attach(|py| {
306            let dict = delta.py_to_dict(py).unwrap();
307            let parsed = OrderBookDelta::py_from_dict(py, dict).unwrap();
308            assert_eq!(parsed, delta);
309        });
310    }
311}