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 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 #[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 #[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 #[pyo3(name = "to_dict")]
277 fn py_to_dict(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
278 to_dict_pyo3(py, self)
279 }
280
281 #[pyo3(name = "to_json_bytes")]
283 fn py_to_json_bytes(&self, py: Python<'_>) -> Py<PyAny> {
284 self.to_json_bytes().unwrap().into_py_any_unwrap(py)
286 }
287
288 #[pyo3(name = "to_msgpack_bytes")]
290 fn py_to_msgpack_bytes(&self, py: Python<'_>) -> Py<PyAny> {
291 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}