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