nautilus_model/python/data/
prices.rs
1use std::{
17 collections::hash_map::DefaultHasher,
18 hash::{Hash, Hasher},
19 str::FromStr,
20};
21
22use nautilus_core::{
23 UnixNanos,
24 python::{
25 IntoPyObjectNautilusExt,
26 serialization::{from_dict_pyo3, to_dict_pyo3},
27 to_pyvalue_err,
28 },
29 serialization::Serializable,
30};
31use pyo3::{
32 IntoPyObjectExt,
33 prelude::*,
34 pyclass::CompareOp,
35 types::{PyDict, PyInt, PyString, PyTuple},
36};
37
38use crate::{
39 data::{IndexPriceUpdate, MarkPriceUpdate},
40 identifiers::InstrumentId,
41 python::common::PY_MODULE_MODEL,
42 types::price::{Price, PriceRaw},
43};
44
45impl MarkPriceUpdate {
46 pub fn from_pyobject(obj: &Bound<'_, PyAny>) -> PyResult<Self> {
48 let instrument_id_obj: Bound<'_, PyAny> = obj.getattr("instrument_id")?.extract()?;
49 let instrument_id_str: String = instrument_id_obj.getattr("value")?.extract()?;
50 let instrument_id =
51 InstrumentId::from_str(instrument_id_str.as_str()).map_err(to_pyvalue_err)?;
52
53 let value_py: Bound<'_, PyAny> = obj.getattr("value")?.extract()?;
54 let value_raw: PriceRaw = value_py.getattr("raw")?.extract()?;
55 let value_prec: u8 = value_py.getattr("precision")?.extract()?;
56 let value = Price::from_raw(value_raw, value_prec);
57
58 let ts_event: u64 = obj.getattr("ts_event")?.extract()?;
59 let ts_init: u64 = obj.getattr("ts_init")?.extract()?;
60
61 Ok(Self::new(
62 instrument_id,
63 value,
64 ts_event.into(),
65 ts_init.into(),
66 ))
67 }
68}
69
70#[pymethods]
71impl MarkPriceUpdate {
72 #[new]
73 fn py_new(
74 instrument_id: InstrumentId,
75 value: Price,
76 ts_event: u64,
77 ts_init: u64,
78 ) -> PyResult<Self> {
79 Ok(Self::new(
80 instrument_id,
81 value,
82 ts_event.into(),
83 ts_init.into(),
84 ))
85 }
86
87 fn __setstate__(&mut self, state: &Bound<'_, PyAny>) -> PyResult<()> {
88 let py_tuple: &Bound<'_, PyTuple> = state.downcast::<PyTuple>()?;
89 let binding = py_tuple.get_item(0)?;
90 let instrument_id_str = binding.downcast::<PyString>()?.extract::<&str>()?;
91 let value_raw = py_tuple
92 .get_item(1)?
93 .downcast::<PyInt>()?
94 .extract::<PriceRaw>()?;
95 let value_prec = py_tuple.get_item(2)?.downcast::<PyInt>()?.extract::<u8>()?;
96
97 let ts_event = py_tuple
98 .get_item(7)?
99 .downcast::<PyInt>()?
100 .extract::<u64>()?;
101 let ts_init = py_tuple
102 .get_item(8)?
103 .downcast::<PyInt>()?
104 .extract::<u64>()?;
105
106 self.instrument_id = InstrumentId::from_str(instrument_id_str).map_err(to_pyvalue_err)?;
107 self.value = Price::from_raw(value_raw, value_prec);
108 self.ts_event = ts_event.into();
109 self.ts_init = ts_init.into();
110
111 Ok(())
112 }
113
114 fn __getstate__(&self, py: Python) -> PyResult<PyObject> {
115 (
116 self.instrument_id.to_string(),
117 self.value.raw,
118 self.value.precision,
119 self.ts_event.as_u64(),
120 self.ts_init.as_u64(),
121 )
122 .into_py_any(py)
123 }
124
125 fn __reduce__(&self, py: Python) -> PyResult<PyObject> {
126 let safe_constructor = py.get_type::<Self>().getattr("_safe_constructor")?;
127 let state = self.__getstate__(py)?;
128 (safe_constructor, PyTuple::empty(py), state).into_py_any(py)
129 }
130
131 #[staticmethod]
132 fn _safe_constructor() -> Self {
133 Self::new(
134 InstrumentId::from("NULL.NULL"),
135 Price::zero(0),
136 UnixNanos::default(),
137 UnixNanos::default(),
138 )
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!("{}({})", stringify!(MarkPriceUpdate), 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 = "value")]
171 fn py_value(&self) -> Price {
172 self.value
173 }
174
175 #[getter]
176 #[pyo3(name = "ts_event")]
177 fn py_ts_event(&self) -> u64 {
178 self.ts_event.as_u64()
179 }
180
181 #[getter]
182 #[pyo3(name = "ts_init")]
183 fn py_ts_init(&self) -> u64 {
184 self.ts_init.as_u64()
185 }
186
187 #[staticmethod]
188 #[pyo3(name = "fully_qualified_name")]
189 fn py_fully_qualified_name() -> String {
190 format!("{}:{}", PY_MODULE_MODEL, stringify!(MarkPriceUpdate))
191 }
192
193 #[staticmethod]
195 #[pyo3(name = "from_dict")]
196 fn py_from_dict(py: Python<'_>, values: Py<PyDict>) -> PyResult<Self> {
197 from_dict_pyo3(py, values)
198 }
199
200 #[staticmethod]
201 #[pyo3(name = "from_json")]
202 fn py_from_json(data: Vec<u8>) -> PyResult<Self> {
203 Self::from_json_bytes(&data).map_err(to_pyvalue_err)
204 }
205
206 #[staticmethod]
207 #[pyo3(name = "from_msgpack")]
208 fn py_from_msgpack(data: Vec<u8>) -> PyResult<Self> {
209 Self::from_msgpack_bytes(&data).map_err(to_pyvalue_err)
210 }
211
212 #[pyo3(name = "as_dict")]
214 fn py_as_dict(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
215 to_dict_pyo3(py, self)
216 }
217
218 #[pyo3(name = "as_json")]
220 fn py_as_json(&self, py: Python<'_>) -> Py<PyAny> {
221 self.as_json_bytes().unwrap().into_py_any_unwrap(py)
223 }
224
225 #[pyo3(name = "as_msgpack")]
227 fn py_as_msgpack(&self, py: Python<'_>) -> Py<PyAny> {
228 self.as_msgpack_bytes().unwrap().into_py_any_unwrap(py)
230 }
231}
232
233impl IndexPriceUpdate {
234 pub fn from_pyobject(obj: &Bound<'_, PyAny>) -> PyResult<Self> {
236 let instrument_id_obj: Bound<'_, PyAny> = obj.getattr("instrument_id")?.extract()?;
237 let instrument_id_str: String = instrument_id_obj.getattr("value")?.extract()?;
238 let instrument_id =
239 InstrumentId::from_str(instrument_id_str.as_str()).map_err(to_pyvalue_err)?;
240
241 let value_py: Bound<'_, PyAny> = obj.getattr("value")?.extract()?;
242 let value_raw: PriceRaw = value_py.getattr("raw")?.extract()?;
243 let value_prec: u8 = value_py.getattr("precision")?.extract()?;
244 let value = Price::from_raw(value_raw, value_prec);
245
246 let ts_event: u64 = obj.getattr("ts_event")?.extract()?;
247 let ts_init: u64 = obj.getattr("ts_init")?.extract()?;
248
249 Ok(Self::new(
250 instrument_id,
251 value,
252 ts_event.into(),
253 ts_init.into(),
254 ))
255 }
256}
257
258#[pymethods]
259impl IndexPriceUpdate {
260 #[new]
261 fn py_new(
262 instrument_id: InstrumentId,
263 value: Price,
264 ts_event: u64,
265 ts_init: u64,
266 ) -> PyResult<Self> {
267 Ok(Self::new(
268 instrument_id,
269 value,
270 ts_event.into(),
271 ts_init.into(),
272 ))
273 }
274
275 fn __setstate__(&mut self, state: &Bound<'_, PyAny>) -> PyResult<()> {
276 let py_tuple: &Bound<'_, PyTuple> = state.downcast::<PyTuple>()?;
277 let binding = py_tuple.get_item(0)?;
278 let instrument_id_str = binding.downcast::<PyString>()?.extract::<&str>()?;
279 let value_raw = py_tuple
280 .get_item(1)?
281 .downcast::<PyInt>()?
282 .extract::<PriceRaw>()?;
283 let value_prec = py_tuple.get_item(2)?.downcast::<PyInt>()?.extract::<u8>()?;
284
285 let ts_event = py_tuple
286 .get_item(7)?
287 .downcast::<PyInt>()?
288 .extract::<u64>()?;
289 let ts_init = py_tuple
290 .get_item(8)?
291 .downcast::<PyInt>()?
292 .extract::<u64>()?;
293
294 self.instrument_id = InstrumentId::from_str(instrument_id_str).map_err(to_pyvalue_err)?;
295 self.value = Price::from_raw(value_raw, value_prec);
296 self.ts_event = ts_event.into();
297 self.ts_init = ts_init.into();
298
299 Ok(())
300 }
301
302 fn __getstate__(&self, py: Python) -> PyResult<PyObject> {
303 (
304 self.instrument_id.to_string(),
305 self.value.raw,
306 self.value.precision,
307 self.ts_event.as_u64(),
308 self.ts_init.as_u64(),
309 )
310 .into_py_any(py)
311 }
312
313 fn __reduce__(&self, py: Python) -> PyResult<PyObject> {
314 let safe_constructor = py.get_type::<Self>().getattr("_safe_constructor")?;
315 let state = self.__getstate__(py)?;
316 (safe_constructor, PyTuple::empty(py), state).into_py_any(py)
317 }
318
319 #[staticmethod]
320 fn _safe_constructor() -> Self {
321 Self::new(
322 InstrumentId::from("NULL.NULL"),
323 Price::zero(0),
324 UnixNanos::default(),
325 UnixNanos::default(),
326 )
327 }
328
329 fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py<PyAny> {
330 match op {
331 CompareOp::Eq => self.eq(other).into_py_any_unwrap(py),
332 CompareOp::Ne => self.ne(other).into_py_any_unwrap(py),
333 _ => py.NotImplemented(),
334 }
335 }
336
337 fn __hash__(&self) -> isize {
338 let mut h = DefaultHasher::new();
339 self.hash(&mut h);
340 h.finish() as isize
341 }
342
343 fn __repr__(&self) -> String {
344 format!("{}({})", stringify!(IndexPriceUpdate), self)
345 }
346
347 fn __str__(&self) -> String {
348 self.to_string()
349 }
350
351 #[getter]
352 #[pyo3(name = "instrument_id")]
353 fn py_instrument_id(&self) -> InstrumentId {
354 self.instrument_id
355 }
356
357 #[getter]
358 #[pyo3(name = "value")]
359 fn py_value(&self) -> Price {
360 self.value
361 }
362
363 #[getter]
364 #[pyo3(name = "ts_event")]
365 fn py_ts_event(&self) -> u64 {
366 self.ts_event.as_u64()
367 }
368
369 #[getter]
370 #[pyo3(name = "ts_init")]
371 fn py_ts_init(&self) -> u64 {
372 self.ts_init.as_u64()
373 }
374
375 #[staticmethod]
376 #[pyo3(name = "fully_qualified_name")]
377 fn py_fully_qualified_name() -> String {
378 format!("{}:{}", PY_MODULE_MODEL, stringify!(IndexPriceUpdate))
379 }
380
381 #[staticmethod]
383 #[pyo3(name = "from_dict")]
384 fn py_from_dict(py: Python<'_>, values: Py<PyDict>) -> PyResult<Self> {
385 from_dict_pyo3(py, values)
386 }
387
388 #[staticmethod]
389 #[pyo3(name = "from_json")]
390 fn py_from_json(data: Vec<u8>) -> PyResult<Self> {
391 Self::from_json_bytes(&data).map_err(to_pyvalue_err)
392 }
393
394 #[staticmethod]
395 #[pyo3(name = "from_msgpack")]
396 fn py_from_msgpack(data: Vec<u8>) -> PyResult<Self> {
397 Self::from_msgpack_bytes(&data).map_err(to_pyvalue_err)
398 }
399
400 #[pyo3(name = "as_dict")]
402 fn py_as_dict(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
403 to_dict_pyo3(py, self)
404 }
405
406 #[pyo3(name = "as_json")]
408 fn py_as_json(&self, py: Python<'_>) -> Py<PyAny> {
409 self.as_json_bytes().unwrap().into_py_any_unwrap(py)
411 }
412
413 #[pyo3(name = "as_msgpack")]
415 fn py_as_msgpack(&self, py: Python<'_>) -> Py<PyAny> {
416 self.as_msgpack_bytes().unwrap().into_py_any_unwrap(py)
418 }
419}
420
421#[cfg(test)]
425mod tests {
426 use nautilus_core::python::IntoPyObjectNautilusExt;
427 use pyo3::Python;
428 use rstest::{fixture, rstest};
429
430 use super::*;
431 use crate::{identifiers::InstrumentId, types::Price};
432
433 #[fixture]
434 fn mark_price() -> MarkPriceUpdate {
435 MarkPriceUpdate::new(
436 InstrumentId::from("BTC-USDT.OKX"),
437 Price::from("100_000.00"),
438 UnixNanos::from(1),
439 UnixNanos::from(2),
440 )
441 }
442
443 #[fixture]
444 fn index_price() -> IndexPriceUpdate {
445 IndexPriceUpdate::new(
446 InstrumentId::from("BTC-USDT.OKX"),
447 Price::from("100_000.00"),
448 UnixNanos::from(1),
449 UnixNanos::from(2),
450 )
451 }
452
453 #[rstest]
454 fn test_mark_price_as_dict(mark_price: MarkPriceUpdate) {
455 pyo3::prepare_freethreaded_python();
456
457 Python::with_gil(|py| {
458 let dict_string = mark_price.py_as_dict(py).unwrap().to_string();
459 let expected_string = r"{'type': 'MarkPriceUpdate', 'instrument_id': 'BTC-USDT.OKX', 'value': '100000.00', 'ts_event': 1, 'ts_init': 2}";
460 assert_eq!(dict_string, expected_string);
461 });
462 }
463
464 #[rstest]
465 fn test_mark_price_from_dict(mark_price: MarkPriceUpdate) {
466 pyo3::prepare_freethreaded_python();
467
468 Python::with_gil(|py| {
469 let dict = mark_price.py_as_dict(py).unwrap();
470 let parsed = MarkPriceUpdate::py_from_dict(py, dict).unwrap();
471 assert_eq!(parsed, mark_price);
472 });
473 }
474
475 #[rstest]
476 fn test_mark_price_from_pyobject(mark_price: MarkPriceUpdate) {
477 pyo3::prepare_freethreaded_python();
478
479 Python::with_gil(|py| {
480 let tick_pyobject = mark_price.into_py_any_unwrap(py);
481 let parsed_tick = MarkPriceUpdate::from_pyobject(tick_pyobject.bind(py)).unwrap();
482 assert_eq!(parsed_tick, mark_price);
483 });
484 }
485
486 #[rstest]
487 fn test_index_price_as_dict(index_price: IndexPriceUpdate) {
488 pyo3::prepare_freethreaded_python();
489
490 Python::with_gil(|py| {
491 let dict_string = index_price.py_as_dict(py).unwrap().to_string();
492 let expected_string = r"{'type': 'IndexPriceUpdate', 'instrument_id': 'BTC-USDT.OKX', 'value': '100000.00', 'ts_event': 1, 'ts_init': 2}";
493 assert_eq!(dict_string, expected_string);
494 });
495 }
496
497 #[rstest]
498 fn test_index_price_from_dict(index_price: IndexPriceUpdate) {
499 pyo3::prepare_freethreaded_python();
500
501 Python::with_gil(|py| {
502 let dict = index_price.py_as_dict(py).unwrap();
503 let parsed = IndexPriceUpdate::py_from_dict(py, dict).unwrap();
504 assert_eq!(parsed, index_price);
505 });
506 }
507
508 #[rstest]
509 fn test_index_price_from_pyobject(index_price: IndexPriceUpdate) {
510 pyo3::prepare_freethreaded_python();
511
512 Python::with_gil(|py| {
513 let tick_pyobject = index_price.into_py_any_unwrap(py);
514 let parsed_tick = IndexPriceUpdate::from_pyobject(tick_pyobject.bind(py)).unwrap();
515 assert_eq!(parsed_tick, index_price);
516 });
517 }
518}