nautilus_model/python/data/
delta.rs1use 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::Serializable,
29};
30use pyo3::{basic::CompareOp, prelude::*, types::PyDict};
31
32use super::data_to_pycapsule;
33use crate::{
34 data::{BookOrder, Data, NULL_ORDER, OrderBookDelta, order::OrderId},
35 enums::{BookAction, FromU8, OrderSide},
36 identifiers::InstrumentId,
37 python::common::PY_MODULE_MODEL,
38 types::{
39 price::{Price, PriceRaw},
40 quantity::{Quantity, QuantityRaw},
41 },
42};
43
44impl OrderBookDelta {
45 pub fn from_pyobject(obj: &Bound<'_, PyAny>) -> PyResult<Self> {
47 let instrument_id_obj: Bound<'_, PyAny> = obj.getattr("instrument_id")?.extract()?;
48 let instrument_id_str: String = instrument_id_obj.getattr("value")?.extract()?;
49 let instrument_id = InstrumentId::from_str(instrument_id_str.as_str())
50 .map_err(to_pyvalue_err)
51 .unwrap();
52
53 let action_obj: Bound<'_, PyAny> = obj.getattr("action")?.extract()?;
54 let action_u8 = action_obj.getattr("value")?.extract()?;
55 let action = BookAction::from_u8(action_u8).unwrap();
56
57 let flags: u8 = obj.getattr("flags")?.extract()?;
58 let sequence: u64 = obj.getattr("sequence")?.extract()?;
59 let ts_event: u64 = obj.getattr("ts_event")?.extract()?;
60 let ts_init: u64 = obj.getattr("ts_init")?.extract()?;
61
62 let order_pyobject = obj.getattr("order")?;
63 let order: BookOrder = if order_pyobject.is_none() {
64 NULL_ORDER
65 } else {
66 let side_obj: Bound<'_, PyAny> = order_pyobject.getattr("side")?.extract()?;
67 let side_u8 = side_obj.getattr("value")?.extract()?;
68 let side = OrderSide::from_u8(side_u8).unwrap();
69
70 let price_py: Bound<'_, PyAny> = order_pyobject.getattr("price")?;
71 let price_raw: PriceRaw = price_py.getattr("raw")?.extract()?;
72 let price_prec: u8 = price_py.getattr("precision")?.extract()?;
73 let price = Price::from_raw(price_raw, price_prec);
74
75 let size_py: Bound<'_, PyAny> = order_pyobject.getattr("size")?;
76 let size_raw: QuantityRaw = size_py.getattr("raw")?.extract()?;
77 let size_prec: u8 = size_py.getattr("precision")?.extract()?;
78 let size = Quantity::from_raw(size_raw, size_prec);
79
80 let order_id: OrderId = order_pyobject.getattr("order_id")?.extract()?;
81 BookOrder {
82 side,
83 price,
84 size,
85 order_id,
86 }
87 };
88
89 Ok(Self::new(
90 instrument_id,
91 action,
92 order,
93 flags,
94 sequence,
95 ts_event.into(),
96 ts_init.into(),
97 ))
98 }
99}
100
101#[pymethods]
102impl OrderBookDelta {
103 #[new]
104 fn py_new(
105 instrument_id: InstrumentId,
106 action: BookAction,
107 order: BookOrder,
108 flags: u8,
109 sequence: u64,
110 ts_event: u64,
111 ts_init: u64,
112 ) -> PyResult<Self> {
113 Self::new_checked(
114 instrument_id,
115 action,
116 order,
117 flags,
118 sequence,
119 ts_event.into(),
120 ts_init.into(),
121 )
122 .map_err(to_pyvalue_err)
123 }
124
125 fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py<PyAny> {
126 match op {
127 CompareOp::Eq => self.eq(other).into_py_any_unwrap(py),
128 CompareOp::Ne => self.ne(other).into_py_any_unwrap(py),
129 _ => py.NotImplemented(),
130 }
131 }
132
133 fn __hash__(&self) -> isize {
134 let mut h = DefaultHasher::new();
135 self.hash(&mut h);
136 h.finish() as isize
137 }
138
139 fn __repr__(&self) -> String {
140 format!("{self:?}")
141 }
142
143 fn __str__(&self) -> String {
144 self.to_string()
145 }
146
147 #[getter]
148 #[pyo3(name = "instrument_id")]
149 fn py_instrument_id(&self) -> InstrumentId {
150 self.instrument_id
151 }
152
153 #[getter]
154 #[pyo3(name = "action")]
155 fn py_action(&self) -> BookAction {
156 self.action
157 }
158
159 #[getter]
160 #[pyo3(name = "order")]
161 fn py_order(&self) -> BookOrder {
162 self.order
163 }
164
165 #[getter]
166 #[pyo3(name = "flags")]
167 fn py_flags(&self) -> u8 {
168 self.flags
169 }
170
171 #[getter]
172 #[pyo3(name = "sequence")]
173 fn py_sequence(&self) -> u64 {
174 self.sequence
175 }
176
177 #[getter]
178 #[pyo3(name = "ts_event")]
179 fn py_ts_event(&self) -> u64 {
180 self.ts_event.as_u64()
181 }
182
183 #[getter]
184 #[pyo3(name = "ts_init")]
185 fn py_ts_init(&self) -> u64 {
186 self.ts_init.as_u64()
187 }
188
189 #[staticmethod]
190 #[pyo3(name = "fully_qualified_name")]
191 fn py_fully_qualified_name() -> String {
192 format!("{}:{}", PY_MODULE_MODEL, stringify!(OrderBookDelta))
193 }
194
195 #[staticmethod]
196 #[pyo3(name = "get_metadata")]
197 fn py_get_metadata(
198 instrument_id: &InstrumentId,
199 price_precision: u8,
200 size_precision: u8,
201 ) -> PyResult<HashMap<String, String>> {
202 Ok(Self::get_metadata(
203 instrument_id,
204 price_precision,
205 size_precision,
206 ))
207 }
208
209 #[staticmethod]
210 #[pyo3(name = "get_fields")]
211 fn py_get_fields(py: Python<'_>) -> PyResult<Bound<'_, PyDict>> {
212 let py_dict = PyDict::new(py);
213 for (k, v) in Self::get_fields() {
214 py_dict.set_item(k, v)?;
215 }
216
217 Ok(py_dict)
218 }
219
220 #[staticmethod]
222 #[pyo3(name = "from_dict")]
223 fn py_from_dict(py: Python<'_>, values: Py<PyDict>) -> PyResult<Self> {
224 from_dict_pyo3(py, values)
225 }
226
227 #[staticmethod]
228 #[pyo3(name = "from_json")]
229 fn py_from_json(data: Vec<u8>) -> PyResult<Self> {
230 Self::from_json_bytes(&data).map_err(to_pyvalue_err)
231 }
232
233 #[staticmethod]
234 #[pyo3(name = "from_msgpack")]
235 fn py_from_msgpack(data: Vec<u8>) -> PyResult<Self> {
236 Self::from_msgpack_bytes(&data).map_err(to_pyvalue_err)
237 }
238
239 #[pyo3(name = "as_pycapsule")]
255 fn py_as_pycapsule(&self, py: Python<'_>) -> PyObject {
256 data_to_pycapsule(py, Data::Delta(*self))
257 }
258
259 #[pyo3(name = "as_dict")]
261 fn py_as_dict(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
262 to_dict_pyo3(py, self)
263 }
264
265 #[pyo3(name = "as_json")]
267 fn py_as_json(&self, py: Python<'_>) -> Py<PyAny> {
268 self.as_json_bytes().unwrap().into_py_any_unwrap(py)
270 }
271
272 #[pyo3(name = "as_msgpack")]
274 fn py_as_msgpack(&self, py: Python<'_>) -> Py<PyAny> {
275 self.as_msgpack_bytes().unwrap().into_py_any_unwrap(py)
277 }
278}
279
280#[cfg(test)]
284mod tests {
285 use rstest::rstest;
286
287 use super::*;
288 use crate::data::stubs::*;
289
290 #[rstest]
291 fn test_order_book_delta_py_new_with_zero_size_returns_error() {
292 pyo3::prepare_freethreaded_python();
293
294 Python::with_gil(|_py| {
295 let instrument_id = InstrumentId::from("AAPL.XNAS");
296 let action = BookAction::Add;
297 let zero_size = Quantity::from(0);
298 let price = Price::from("100.00");
299 let side = OrderSide::Buy;
300 let order_id = 123_456;
301 let flags = 0;
302 let sequence = 1;
303 let ts_event = 1;
304 let ts_init = 2;
305
306 let order = BookOrder::new(side, price, zero_size, order_id);
307
308 let result = OrderBookDelta::py_new(
309 instrument_id,
310 action,
311 order,
312 flags,
313 sequence,
314 ts_event,
315 ts_init,
316 );
317 assert!(result.is_err());
318 });
319 }
320
321 #[rstest]
322 fn test_as_dict(stub_delta: OrderBookDelta) {
323 pyo3::prepare_freethreaded_python();
324 let delta = stub_delta;
325
326 Python::with_gil(|py| {
327 let dict_string = delta.py_as_dict(py).unwrap().to_string();
328 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}";
329 assert_eq!(dict_string, expected_string);
330 });
331 }
332
333 #[rstest]
334 fn test_from_dict(stub_delta: OrderBookDelta) {
335 pyo3::prepare_freethreaded_python();
336 let delta = stub_delta;
337
338 Python::with_gil(|py| {
339 let dict = delta.py_as_dict(py).unwrap();
340 let parsed = OrderBookDelta::py_from_dict(py, dict).unwrap();
341 assert_eq!(parsed, delta);
342 });
343 }
344
345 #[rstest]
346 fn test_from_pyobject(stub_delta: OrderBookDelta) {
347 pyo3::prepare_freethreaded_python();
348 let delta = stub_delta;
349
350 Python::with_gil(|py| {
351 let delta_pyobject = delta.into_py_any_unwrap(py);
352 let parsed_delta = OrderBookDelta::from_pyobject(delta_pyobject.bind(py)).unwrap();
353 assert_eq!(parsed_delta, delta);
354 });
355 }
356}