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    #[getter]
109    #[pyo3(name = "order")]
110    fn py_order(&self) -> BookOrder {
111        self.order
112    }
113
114    #[getter]
115    #[pyo3(name = "flags")]
116    fn py_flags(&self) -> u8 {
117        self.flags
118    }
119
120    #[getter]
121    #[pyo3(name = "sequence")]
122    fn py_sequence(&self) -> u64 {
123        self.sequence
124    }
125
126    #[getter]
127    #[pyo3(name = "ts_event")]
128    fn py_ts_event(&self) -> u64 {
129        self.ts_event.as_u64()
130    }
131
132    #[getter]
133    #[pyo3(name = "ts_init")]
134    fn py_ts_init(&self) -> u64 {
135        self.ts_init.as_u64()
136    }
137
138    #[staticmethod]
139    #[pyo3(name = "fully_qualified_name")]
140    fn py_fully_qualified_name() -> String {
141        format!("{}:{}", PY_MODULE_MODEL, stringify!(OrderBookDelta))
142    }
143
144    /// Returns the metadata for the type, for use with serialization formats.
145    #[staticmethod]
146    #[pyo3(name = "get_metadata")]
147    fn py_get_metadata(
148        instrument_id: &InstrumentId,
149        price_precision: u8,
150        size_precision: u8,
151    ) -> HashMap<String, String> {
152        Self::get_metadata(instrument_id, price_precision, size_precision)
153    }
154
155    /// Returns the field map for the type, for use with Arrow schemas.
156    #[staticmethod]
157    #[pyo3(name = "get_fields")]
158    fn py_get_fields(py: Python<'_>) -> PyResult<Bound<'_, PyDict>> {
159        let py_dict = PyDict::new(py);
160        for (k, v) in Self::get_fields() {
161            py_dict.set_item(k, v)?;
162        }
163
164        Ok(py_dict)
165    }
166
167    /// Returns a new object from the given dictionary representation.
168    #[staticmethod]
169    #[pyo3(name = "from_dict")]
170    fn py_from_dict(py: Python<'_>, values: Py<PyDict>) -> PyResult<Self> {
171        from_dict_pyo3(py, values)
172    }
173
174    /// Return a dictionary representation of the object.
175    #[pyo3(name = "to_dict")]
176    fn py_to_dict(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
177        to_dict_pyo3(py, self)
178    }
179
180    /// Return JSON encoded bytes representation of the object.
181    #[pyo3(name = "to_json_bytes")]
182    fn py_to_json_bytes(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
183        self.to_json_bytes()
184            .map_err(to_pyvalue_err)?
185            .into_py_any(py)
186    }
187
188    /// Return `MsgPack` encoded bytes representation of the object.
189    #[pyo3(name = "to_msgpack_bytes")]
190    fn py_to_msgpack_bytes(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
191        self.to_msgpack_bytes()
192            .map_err(to_pyvalue_err)?
193            .into_py_any(py)
194    }
195
196    fn __reduce__(&self, py: Python) -> PyResult<Py<PyAny>> {
197        let from_dict = py.get_type::<Self>().getattr("from_dict")?;
198        let dict = self.py_to_dict(py)?;
199        (from_dict, (dict,)).into_py_any(py)
200    }
201}
202
203#[pymethods]
204impl OrderBookDelta {
205    #[staticmethod]
206    #[pyo3(name = "from_json")]
207    fn py_from_json(data: &[u8]) -> PyResult<Self> {
208        Self::from_json_bytes(data).map_err(to_pyvalue_err)
209    }
210
211    #[staticmethod]
212    #[pyo3(name = "from_msgpack")]
213    fn py_from_msgpack(data: &[u8]) -> PyResult<Self> {
214        Self::from_msgpack_bytes(data).map_err(to_pyvalue_err)
215    }
216}
217
218#[cfg(test)]
219mod tests {
220
221    use rstest::rstest;
222
223    use super::*;
224    use crate::{
225        data::stubs::*,
226        enums::OrderSide,
227        types::{Price, Quantity},
228    };
229
230    #[rstest]
231    fn test_order_book_delta_py_new_with_zero_size_returns_error() {
232        Python::initialize();
233        Python::attach(|_py| {
234            let instrument_id = InstrumentId::from("AAPL.XNAS");
235            let action = BookAction::Add;
236            let zero_size = Quantity::from(0);
237            let price = Price::from("100.00");
238            let side = OrderSide::Buy;
239            let order_id = 123_456;
240            let flags = 0;
241            let sequence = 1;
242            let ts_event = 1;
243            let ts_init = 2;
244
245            let order = BookOrder::new(side, price, zero_size, order_id);
246
247            let result = OrderBookDelta::py_new(
248                instrument_id,
249                action,
250                order,
251                flags,
252                sequence,
253                ts_event,
254                ts_init,
255            );
256            assert!(result.is_err());
257        });
258    }
259
260    #[rstest]
261    fn test_to_dict(stub_delta: OrderBookDelta) {
262        let delta = stub_delta;
263
264        Python::initialize();
265        Python::attach(|py| {
266            let dict_string = delta.py_to_dict(py).unwrap().to_string();
267            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}";
268            assert_eq!(dict_string, expected_string);
269        });
270    }
271
272    #[rstest]
273    fn test_from_dict(stub_delta: OrderBookDelta) {
274        let delta = stub_delta;
275
276        Python::initialize();
277        Python::attach(|py| {
278            let dict = delta.py_to_dict(py).unwrap();
279            let parsed = OrderBookDelta::py_from_dict(py, dict).unwrap();
280            assert_eq!(parsed, delta);
281        });
282    }
283}