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    str::FromStr,
20};
21
22use nautilus_core::{
23    python::{
24        IntoPyObjectNautilusExt,
25        serialization::{from_dict_pyo3, to_dict_pyo3},
26        to_pyvalue_err,
27    },
28    serialization::{
29        Serializable,
30        msgpack::{FromMsgPack, ToMsgPack},
31    },
32};
33use pyo3::{IntoPyObjectExt, basic::CompareOp, prelude::*, types::PyDict};
34
35use super::data_to_pycapsule;
36use crate::{
37    data::{BookOrder, Data, NULL_ORDER, OrderBookDelta, order::OrderId},
38    enums::{BookAction, FromU8, OrderSide},
39    identifiers::InstrumentId,
40    python::common::PY_MODULE_MODEL,
41    types::{
42        price::{Price, PriceRaw},
43        quantity::{Quantity, QuantityRaw},
44    },
45};
46
47impl OrderBookDelta {
48    /// Creates a new [`OrderBookDelta`] from a Python object.
49    ///
50    /// # Panics
51    ///
52    /// Panics if converting `instrument_id` from string or `action` from u8 fails.
53    ///
54    /// # Errors
55    ///
56    /// Returns a `PyErr` if extracting any attribute or converting types fails.
57    pub fn from_pyobject(obj: &Bound<'_, PyAny>) -> PyResult<Self> {
58        // Fast path: avoid property getters that trigger enum type deadlocks
59        if let Ok(delta) = obj.cast::<Self>() {
60            return Ok(*delta.borrow());
61        }
62
63        let instrument_id_obj: Bound<'_, PyAny> = obj.getattr("instrument_id")?.extract()?;
64        let instrument_id_str: String = instrument_id_obj.getattr("value")?.extract()?;
65        let instrument_id = InstrumentId::from_str(instrument_id_str.as_str())
66            .map_err(to_pyvalue_err)
67            .unwrap();
68
69        let action_obj: Bound<'_, PyAny> = obj.getattr("action")?.extract()?;
70        let action_u8 = action_obj.getattr("value")?.extract()?;
71        let action = BookAction::from_u8(action_u8).unwrap();
72
73        let flags: u8 = obj.getattr("flags")?.extract()?;
74        let sequence: u64 = obj.getattr("sequence")?.extract()?;
75        let ts_event: u64 = obj.getattr("ts_event")?.extract()?;
76        let ts_init: u64 = obj.getattr("ts_init")?.extract()?;
77
78        let order_pyobject = obj.getattr("order")?;
79        let order: BookOrder = if order_pyobject.is_none() {
80            NULL_ORDER
81        } else {
82            let side_obj: Bound<'_, PyAny> = order_pyobject.getattr("side")?.extract()?;
83            let side_u8 = side_obj.getattr("value")?.extract()?;
84            let side = OrderSide::from_u8(side_u8).unwrap();
85
86            let price_py: Bound<'_, PyAny> = order_pyobject.getattr("price")?;
87            let price_raw: PriceRaw = price_py.getattr("raw")?.extract()?;
88            let price_prec: u8 = price_py.getattr("precision")?.extract()?;
89            let price = Price::from_raw(price_raw, price_prec);
90
91            let size_py: Bound<'_, PyAny> = order_pyobject.getattr("size")?;
92            let size_raw: QuantityRaw = size_py.getattr("raw")?.extract()?;
93            let size_prec: u8 = size_py.getattr("precision")?.extract()?;
94            let size = Quantity::from_raw(size_raw, size_prec);
95
96            let order_id: OrderId = order_pyobject.getattr("order_id")?.extract()?;
97            BookOrder {
98                side,
99                price,
100                size,
101                order_id,
102            }
103        };
104
105        Ok(Self::new(
106            instrument_id,
107            action,
108            order,
109            flags,
110            sequence,
111            ts_event.into(),
112            ts_init.into(),
113        ))
114    }
115}
116
117#[pymethods]
118#[pyo3_stub_gen::derive::gen_stub_pymethods]
119impl OrderBookDelta {
120    /// Represents a single change/delta in an order book.
121    #[new]
122    fn py_new(
123        instrument_id: InstrumentId,
124        action: BookAction,
125        order: BookOrder,
126        flags: u8,
127        sequence: u64,
128        ts_event: u64,
129        ts_init: u64,
130    ) -> PyResult<Self> {
131        Self::new_checked(
132            instrument_id,
133            action,
134            order,
135            flags,
136            sequence,
137            ts_event.into(),
138            ts_init.into(),
139        )
140        .map_err(to_pyvalue_err)
141    }
142
143    fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py<PyAny> {
144        match op {
145            CompareOp::Eq => self.eq(other).into_py_any_unwrap(py),
146            CompareOp::Ne => self.ne(other).into_py_any_unwrap(py),
147            _ => py.NotImplemented(),
148        }
149    }
150
151    fn __hash__(&self) -> isize {
152        let mut h = DefaultHasher::new();
153        self.hash(&mut h);
154        h.finish() as isize
155    }
156
157    fn __repr__(&self) -> String {
158        format!("{self:?}")
159    }
160
161    fn __str__(&self) -> String {
162        self.to_string()
163    }
164
165    #[getter]
166    #[pyo3(name = "instrument_id")]
167    fn py_instrument_id(&self) -> InstrumentId {
168        self.instrument_id
169    }
170
171    #[getter]
172    #[pyo3(name = "action")]
173    fn py_action(&self) -> BookAction {
174        self.action
175    }
176
177    #[getter]
178    #[pyo3(name = "order")]
179    fn py_order(&self) -> BookOrder {
180        self.order
181    }
182
183    #[getter]
184    #[pyo3(name = "flags")]
185    fn py_flags(&self) -> u8 {
186        self.flags
187    }
188
189    #[getter]
190    #[pyo3(name = "sequence")]
191    fn py_sequence(&self) -> u64 {
192        self.sequence
193    }
194
195    #[getter]
196    #[pyo3(name = "ts_event")]
197    fn py_ts_event(&self) -> u64 {
198        self.ts_event.as_u64()
199    }
200
201    #[getter]
202    #[pyo3(name = "ts_init")]
203    fn py_ts_init(&self) -> u64 {
204        self.ts_init.as_u64()
205    }
206
207    #[staticmethod]
208    #[pyo3(name = "fully_qualified_name")]
209    fn py_fully_qualified_name() -> String {
210        format!("{}:{}", PY_MODULE_MODEL, stringify!(OrderBookDelta))
211    }
212
213    /// Returns the metadata for the type, for use with serialization formats.
214    #[staticmethod]
215    #[pyo3(name = "get_metadata")]
216    fn py_get_metadata(
217        instrument_id: &InstrumentId,
218        price_precision: u8,
219        size_precision: u8,
220    ) -> HashMap<String, String> {
221        Self::get_metadata(instrument_id, price_precision, size_precision)
222    }
223
224    /// Returns the field map for the type, for use with Arrow schemas.
225    #[staticmethod]
226    #[pyo3(name = "get_fields")]
227    fn py_get_fields(py: Python<'_>) -> PyResult<Bound<'_, PyDict>> {
228        let py_dict = PyDict::new(py);
229        for (k, v) in Self::get_fields() {
230            py_dict.set_item(k, v)?;
231        }
232
233        Ok(py_dict)
234    }
235
236    /// Returns a new object from the given dictionary representation.
237    #[staticmethod]
238    #[pyo3(name = "from_dict")]
239    fn py_from_dict(py: Python<'_>, values: Py<PyDict>) -> PyResult<Self> {
240        from_dict_pyo3(py, values)
241    }
242
243    /// Creates a `PyCapsule` containing a raw pointer to a `Data::Delta` object.
244    ///
245    /// This function takes the current object (assumed to be of a type that can be represented as
246    /// `Data::Delta`), and encapsulates a raw pointer to it within a `PyCapsule`.
247    ///
248    /// # Safety
249    ///
250    /// This function is safe as long as the following conditions are met:
251    /// - The `Data::Delta` object pointed to by the capsule must remain valid for the lifetime of the capsule.
252    /// - The consumer of the capsule must ensure proper handling to avoid dereferencing a dangling pointer.
253    ///
254    /// # Panics
255    ///
256    /// The function will panic if the `PyCapsule` creation fails, which can occur if the
257    /// `Data::Delta` object cannot be converted into a raw pointer.
258    #[pyo3(name = "as_pycapsule")]
259    fn py_as_pycapsule(&self, py: Python<'_>) -> Py<PyAny> {
260        data_to_pycapsule(py, Data::Delta(*self))
261    }
262
263    /// Return a dictionary representation of the object.
264    #[pyo3(name = "to_dict")]
265    fn py_to_dict(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
266        to_dict_pyo3(py, self)
267    }
268
269    /// Return JSON encoded bytes representation of the object.
270    #[pyo3(name = "to_json_bytes")]
271    fn py_to_json_bytes(&self, py: Python<'_>) -> Py<PyAny> {
272        self.to_json_bytes().unwrap().into_py_any_unwrap(py)
273    }
274
275    /// Return `MsgPack` encoded bytes representation of the object.
276    #[pyo3(name = "to_msgpack_bytes")]
277    fn py_to_msgpack_bytes(&self, py: Python<'_>) -> Py<PyAny> {
278        self.to_msgpack_bytes().unwrap().into_py_any_unwrap(py)
279    }
280
281    fn __reduce__(&self, py: Python) -> PyResult<Py<PyAny>> {
282        let from_dict = py.get_type::<Self>().getattr("from_dict")?;
283        let dict = self.py_to_dict(py)?;
284        (from_dict, (dict,)).into_py_any(py)
285    }
286}
287
288#[pymethods]
289impl OrderBookDelta {
290    #[staticmethod]
291    #[pyo3(name = "from_json")]
292    fn py_from_json(data: &[u8]) -> PyResult<Self> {
293        Self::from_json_bytes(data).map_err(to_pyvalue_err)
294    }
295
296    #[staticmethod]
297    #[pyo3(name = "from_msgpack")]
298    fn py_from_msgpack(data: &[u8]) -> PyResult<Self> {
299        Self::from_msgpack_bytes(data).map_err(to_pyvalue_err)
300    }
301}
302
303#[cfg(test)]
304mod tests {
305    use rstest::rstest;
306
307    use super::*;
308    use crate::data::stubs::*;
309
310    #[rstest]
311    fn test_order_book_delta_py_new_with_zero_size_returns_error() {
312        Python::initialize();
313        Python::attach(|_py| {
314            let instrument_id = InstrumentId::from("AAPL.XNAS");
315            let action = BookAction::Add;
316            let zero_size = Quantity::from(0);
317            let price = Price::from("100.00");
318            let side = OrderSide::Buy;
319            let order_id = 123_456;
320            let flags = 0;
321            let sequence = 1;
322            let ts_event = 1;
323            let ts_init = 2;
324
325            let order = BookOrder::new(side, price, zero_size, order_id);
326
327            let result = OrderBookDelta::py_new(
328                instrument_id,
329                action,
330                order,
331                flags,
332                sequence,
333                ts_event,
334                ts_init,
335            );
336            assert!(result.is_err());
337        });
338    }
339
340    #[rstest]
341    fn test_to_dict(stub_delta: OrderBookDelta) {
342        let delta = stub_delta;
343
344        Python::initialize();
345        Python::attach(|py| {
346            let dict_string = delta.py_to_dict(py).unwrap().to_string();
347            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}";
348            assert_eq!(dict_string, expected_string);
349        });
350    }
351
352    #[rstest]
353    fn test_from_dict(stub_delta: OrderBookDelta) {
354        let delta = stub_delta;
355
356        Python::initialize();
357        Python::attach(|py| {
358            let dict = delta.py_to_dict(py).unwrap();
359            let parsed = OrderBookDelta::py_from_dict(py, dict).unwrap();
360            assert_eq!(parsed, delta);
361        });
362    }
363
364    #[rstest]
365    fn test_from_pyobject(stub_delta: OrderBookDelta) {
366        let delta = stub_delta;
367
368        Python::initialize();
369        Python::attach(|py| {
370            let delta_pyobject = delta.into_py_any_unwrap(py);
371            let parsed_delta = OrderBookDelta::from_pyobject(delta_pyobject.bind(py)).unwrap();
372            assert_eq!(parsed_delta, delta);
373        });
374    }
375}