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::{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]
118impl OrderBookDelta {
119    #[new]
120    fn py_new(
121        instrument_id: InstrumentId,
122        action: BookAction,
123        order: BookOrder,
124        flags: u8,
125        sequence: u64,
126        ts_event: u64,
127        ts_init: u64,
128    ) -> PyResult<Self> {
129        Self::new_checked(
130            instrument_id,
131            action,
132            order,
133            flags,
134            sequence,
135            ts_event.into(),
136            ts_init.into(),
137        )
138        .map_err(to_pyvalue_err)
139    }
140
141    fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py<PyAny> {
142        match op {
143            CompareOp::Eq => self.eq(other).into_py_any_unwrap(py),
144            CompareOp::Ne => self.ne(other).into_py_any_unwrap(py),
145            _ => py.NotImplemented(),
146        }
147    }
148
149    fn __hash__(&self) -> isize {
150        let mut h = DefaultHasher::new();
151        self.hash(&mut h);
152        h.finish() as isize
153    }
154
155    fn __repr__(&self) -> String {
156        format!("{self:?}")
157    }
158
159    fn __str__(&self) -> String {
160        self.to_string()
161    }
162
163    #[getter]
164    #[pyo3(name = "instrument_id")]
165    fn py_instrument_id(&self) -> InstrumentId {
166        self.instrument_id
167    }
168
169    #[getter]
170    #[pyo3(name = "action")]
171    fn py_action(&self) -> BookAction {
172        self.action
173    }
174
175    #[getter]
176    #[pyo3(name = "order")]
177    fn py_order(&self) -> BookOrder {
178        self.order
179    }
180
181    #[getter]
182    #[pyo3(name = "flags")]
183    fn py_flags(&self) -> u8 {
184        self.flags
185    }
186
187    #[getter]
188    #[pyo3(name = "sequence")]
189    fn py_sequence(&self) -> u64 {
190        self.sequence
191    }
192
193    #[getter]
194    #[pyo3(name = "ts_event")]
195    fn py_ts_event(&self) -> u64 {
196        self.ts_event.as_u64()
197    }
198
199    #[getter]
200    #[pyo3(name = "ts_init")]
201    fn py_ts_init(&self) -> u64 {
202        self.ts_init.as_u64()
203    }
204
205    #[staticmethod]
206    #[pyo3(name = "fully_qualified_name")]
207    fn py_fully_qualified_name() -> String {
208        format!("{}:{}", PY_MODULE_MODEL, stringify!(OrderBookDelta))
209    }
210
211    #[staticmethod]
212    #[pyo3(name = "get_metadata")]
213    fn py_get_metadata(
214        instrument_id: &InstrumentId,
215        price_precision: u8,
216        size_precision: u8,
217    ) -> PyResult<HashMap<String, String>> {
218        Ok(Self::get_metadata(
219            instrument_id,
220            price_precision,
221            size_precision,
222        ))
223    }
224
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    #[staticmethod]
244    #[pyo3(name = "from_json")]
245    fn py_from_json(data: Vec<u8>) -> PyResult<Self> {
246        Self::from_json_bytes(&data).map_err(to_pyvalue_err)
247    }
248
249    #[staticmethod]
250    #[pyo3(name = "from_msgpack")]
251    fn py_from_msgpack(data: Vec<u8>) -> PyResult<Self> {
252        Self::from_msgpack_bytes(&data).map_err(to_pyvalue_err)
253    }
254
255    /// Creates a `PyCapsule` containing a raw pointer to a `Data::Delta` object.
256    ///
257    /// This function takes the current object (assumed to be of a type that can be represented as
258    /// `Data::Delta`), and encapsulates a raw pointer to it within a `PyCapsule`.
259    ///
260    /// # Safety
261    ///
262    /// This function is safe as long as the following conditions are met:
263    /// - The `Data::Delta` object pointed to by the capsule must remain valid for the lifetime of the capsule.
264    /// - The consumer of the capsule must ensure proper handling to avoid dereferencing a dangling pointer.
265    ///
266    /// # Panics
267    ///
268    /// The function will panic if the `PyCapsule` creation fails, which can occur if the
269    /// `Data::Delta` object cannot be converted into a raw pointer.
270    #[pyo3(name = "as_pycapsule")]
271    fn py_as_pycapsule(&self, py: Python<'_>) -> Py<PyAny> {
272        data_to_pycapsule(py, Data::Delta(*self))
273    }
274
275    /// Return a dictionary representation of the object.
276    #[pyo3(name = "to_dict")]
277    fn py_to_dict(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
278        to_dict_pyo3(py, self)
279    }
280
281    /// Return JSON encoded bytes representation of the object.
282    #[pyo3(name = "to_json_bytes")]
283    fn py_to_json_bytes(&self, py: Python<'_>) -> Py<PyAny> {
284        // SAFETY: Unwrap safe when serializing a valid object
285        self.to_json_bytes().unwrap().into_py_any_unwrap(py)
286    }
287
288    /// Return MsgPack encoded bytes representation of the object.
289    #[pyo3(name = "to_msgpack_bytes")]
290    fn py_to_msgpack_bytes(&self, py: Python<'_>) -> Py<PyAny> {
291        // SAFETY: Unwrap safe when serializing a valid object
292        self.to_msgpack_bytes().unwrap().into_py_any_unwrap(py)
293    }
294}
295
296#[cfg(test)]
297mod tests {
298    use rstest::rstest;
299
300    use super::*;
301    use crate::data::stubs::*;
302
303    #[rstest]
304    fn test_order_book_delta_py_new_with_zero_size_returns_error() {
305        Python::initialize();
306        Python::attach(|_py| {
307            let instrument_id = InstrumentId::from("AAPL.XNAS");
308            let action = BookAction::Add;
309            let zero_size = Quantity::from(0);
310            let price = Price::from("100.00");
311            let side = OrderSide::Buy;
312            let order_id = 123_456;
313            let flags = 0;
314            let sequence = 1;
315            let ts_event = 1;
316            let ts_init = 2;
317
318            let order = BookOrder::new(side, price, zero_size, order_id);
319
320            let result = OrderBookDelta::py_new(
321                instrument_id,
322                action,
323                order,
324                flags,
325                sequence,
326                ts_event,
327                ts_init,
328            );
329            assert!(result.is_err());
330        });
331    }
332
333    #[rstest]
334    fn test_to_dict(stub_delta: OrderBookDelta) {
335        let delta = stub_delta;
336
337        Python::initialize();
338        Python::attach(|py| {
339            let dict_string = delta.py_to_dict(py).unwrap().to_string();
340            let expected_string = r"{'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}";
341            assert_eq!(dict_string, expected_string);
342        });
343    }
344
345    #[rstest]
346    fn test_from_dict(stub_delta: OrderBookDelta) {
347        let delta = stub_delta;
348
349        Python::initialize();
350        Python::attach(|py| {
351            let dict = delta.py_to_dict(py).unwrap();
352            let parsed = OrderBookDelta::py_from_dict(py, dict).unwrap();
353            assert_eq!(parsed, delta);
354        });
355    }
356
357    #[rstest]
358    fn test_from_pyobject(stub_delta: OrderBookDelta) {
359        let delta = stub_delta;
360
361        Python::initialize();
362        Python::attach(|py| {
363            let delta_pyobject = delta.into_py_any_unwrap(py);
364            let parsed_delta = OrderBookDelta::from_pyobject(delta_pyobject.bind(py)).unwrap();
365            assert_eq!(parsed_delta, delta);
366        });
367    }
368}