nautilus_model/python/data/
prices.rs1use std::{
17 collections::{HashMap, hash_map::DefaultHasher},
18 hash::{Hash, Hasher},
19 str::FromStr,
20};
21
22use nautilus_core::{
23 UnixNanos,
24 python::{
25 IntoPyObjectPoseiExt,
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> {
52 let instrument_id_obj: Bound<'_, PyAny> = obj.getattr("instrument_id")?.extract()?;
53 let instrument_id_str: String = instrument_id_obj.getattr("value")?.extract()?;
54 let instrument_id =
55 InstrumentId::from_str(instrument_id_str.as_str()).map_err(to_pyvalue_err)?;
56
57 let value_py: Bound<'_, PyAny> = obj.getattr("value")?.extract()?;
58 let value_raw: PriceRaw = value_py.getattr("raw")?.extract()?;
59 let value_prec: u8 = value_py.getattr("precision")?.extract()?;
60 let value = Price::from_raw(value_raw, value_prec);
61
62 let ts_event: u64 = obj.getattr("ts_event")?.extract()?;
63 let ts_init: u64 = obj.getattr("ts_init")?.extract()?;
64
65 Ok(Self::new(
66 instrument_id,
67 value,
68 ts_event.into(),
69 ts_init.into(),
70 ))
71 }
72}
73
74#[pymethods]
75impl MarkPriceUpdate {
76 #[new]
77 fn py_new(
78 instrument_id: InstrumentId,
79 value: Price,
80 ts_event: u64,
81 ts_init: u64,
82 ) -> PyResult<Self> {
83 Ok(Self::new(
84 instrument_id,
85 value,
86 ts_event.into(),
87 ts_init.into(),
88 ))
89 }
90
91 fn __setstate__(&mut self, state: &Bound<'_, PyAny>) -> PyResult<()> {
92 let py_tuple: &Bound<'_, PyTuple> = state.downcast::<PyTuple>()?;
93 let binding = py_tuple.get_item(0)?;
94 let instrument_id_str = binding.downcast::<PyString>()?.extract::<&str>()?;
95 let value_raw = py_tuple
96 .get_item(1)?
97 .downcast::<PyInt>()?
98 .extract::<PriceRaw>()?;
99 let value_prec = py_tuple.get_item(2)?.downcast::<PyInt>()?.extract::<u8>()?;
100
101 let ts_event = py_tuple
102 .get_item(7)?
103 .downcast::<PyInt>()?
104 .extract::<u64>()?;
105 let ts_init = py_tuple
106 .get_item(8)?
107 .downcast::<PyInt>()?
108 .extract::<u64>()?;
109
110 self.instrument_id = InstrumentId::from_str(instrument_id_str).map_err(to_pyvalue_err)?;
111 self.value = Price::from_raw(value_raw, value_prec);
112 self.ts_event = ts_event.into();
113 self.ts_init = ts_init.into();
114
115 Ok(())
116 }
117
118 fn __getstate__(&self, py: Python) -> PyResult<PyObject> {
119 (
120 self.instrument_id.to_string(),
121 self.value.raw,
122 self.value.precision,
123 self.ts_event.as_u64(),
124 self.ts_init.as_u64(),
125 )
126 .into_py_any(py)
127 }
128
129 fn __reduce__(&self, py: Python) -> PyResult<PyObject> {
130 let safe_constructor = py.get_type::<Self>().getattr("_safe_constructor")?;
131 let state = self.__getstate__(py)?;
132 (safe_constructor, PyTuple::empty(py), state).into_py_any(py)
133 }
134
135 #[staticmethod]
136 fn _safe_constructor() -> Self {
137 Self::new(
138 InstrumentId::from("NULL.NULL"),
139 Price::zero(0),
140 UnixNanos::default(),
141 UnixNanos::default(),
142 )
143 }
144
145 fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py<PyAny> {
146 match op {
147 CompareOp::Eq => self.eq(other).into_py_any_unwrap(py),
148 CompareOp::Ne => self.ne(other).into_py_any_unwrap(py),
149 _ => py.NotImplemented(),
150 }
151 }
152
153 fn __hash__(&self) -> isize {
154 let mut h = DefaultHasher::new();
155 self.hash(&mut h);
156 h.finish() as isize
157 }
158
159 fn __repr__(&self) -> String {
160 format!("{}({})", stringify!(MarkPriceUpdate), self)
161 }
162
163 fn __str__(&self) -> String {
164 self.to_string()
165 }
166
167 #[getter]
168 #[pyo3(name = "instrument_id")]
169 fn py_instrument_id(&self) -> InstrumentId {
170 self.instrument_id
171 }
172
173 #[getter]
174 #[pyo3(name = "value")]
175 fn py_value(&self) -> Price {
176 self.value
177 }
178
179 #[getter]
180 #[pyo3(name = "ts_event")]
181 fn py_ts_event(&self) -> u64 {
182 self.ts_event.as_u64()
183 }
184
185 #[getter]
186 #[pyo3(name = "ts_init")]
187 fn py_ts_init(&self) -> u64 {
188 self.ts_init.as_u64()
189 }
190
191 #[staticmethod]
192 #[pyo3(name = "fully_qualified_name")]
193 fn py_fully_qualified_name() -> String {
194 format!("{}:{}", PY_MODULE_MODEL, stringify!(MarkPriceUpdate))
195 }
196
197 #[staticmethod]
198 #[pyo3(name = "get_metadata")]
199 fn py_get_metadata(
200 instrument_id: &InstrumentId,
201 price_precision: u8,
202 ) -> PyResult<HashMap<String, String>> {
203 Ok(Self::get_metadata(instrument_id, price_precision))
204 }
205
206 #[staticmethod]
207 #[pyo3(name = "get_fields")]
208 fn py_get_fields(py: Python<'_>) -> PyResult<Bound<'_, PyDict>> {
209 let py_dict = PyDict::new(py);
210 for (k, v) in Self::get_fields() {
211 py_dict.set_item(k, v)?;
212 }
213
214 Ok(py_dict)
215 }
216
217 #[staticmethod]
219 #[pyo3(name = "from_dict")]
220 fn py_from_dict(py: Python<'_>, values: Py<PyDict>) -> PyResult<Self> {
221 from_dict_pyo3(py, values)
222 }
223
224 #[staticmethod]
225 #[pyo3(name = "from_json")]
226 fn py_from_json(data: Vec<u8>) -> PyResult<Self> {
227 Self::from_json_bytes(&data).map_err(to_pyvalue_err)
228 }
229
230 #[staticmethod]
231 #[pyo3(name = "from_msgpack")]
232 fn py_from_msgpack(data: Vec<u8>) -> PyResult<Self> {
233 Self::from_msgpack_bytes(&data).map_err(to_pyvalue_err)
234 }
235
236 #[pyo3(name = "to_dict")]
238 fn py_to_dict(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
239 to_dict_pyo3(py, self)
240 }
241
242 #[pyo3(name = "to_json_bytes")]
244 fn py_to_json_bytes(&self, py: Python<'_>) -> Py<PyAny> {
245 self.to_json_bytes().unwrap().into_py_any_unwrap(py)
247 }
248
249 #[pyo3(name = "to_msgpack_bytes")]
251 fn py_to_msgpack_bytes(&self, py: Python<'_>) -> Py<PyAny> {
252 self.to_msgpack_bytes().unwrap().into_py_any_unwrap(py)
254 }
255}
256
257impl IndexPriceUpdate {
258 pub fn from_pyobject(obj: &Bound<'_, PyAny>) -> PyResult<Self> {
264 let instrument_id_obj: Bound<'_, PyAny> = obj.getattr("instrument_id")?.extract()?;
265 let instrument_id_str: String = instrument_id_obj.getattr("value")?.extract()?;
266 let instrument_id =
267 InstrumentId::from_str(instrument_id_str.as_str()).map_err(to_pyvalue_err)?;
268
269 let value_py: Bound<'_, PyAny> = obj.getattr("value")?.extract()?;
270 let value_raw: PriceRaw = value_py.getattr("raw")?.extract()?;
271 let value_prec: u8 = value_py.getattr("precision")?.extract()?;
272 let value = Price::from_raw(value_raw, value_prec);
273
274 let ts_event: u64 = obj.getattr("ts_event")?.extract()?;
275 let ts_init: u64 = obj.getattr("ts_init")?.extract()?;
276
277 Ok(Self::new(
278 instrument_id,
279 value,
280 ts_event.into(),
281 ts_init.into(),
282 ))
283 }
284}
285
286#[pymethods]
287impl IndexPriceUpdate {
288 #[new]
289 fn py_new(
290 instrument_id: InstrumentId,
291 value: Price,
292 ts_event: u64,
293 ts_init: u64,
294 ) -> PyResult<Self> {
295 Ok(Self::new(
296 instrument_id,
297 value,
298 ts_event.into(),
299 ts_init.into(),
300 ))
301 }
302
303 fn __setstate__(&mut self, state: &Bound<'_, PyAny>) -> PyResult<()> {
304 let py_tuple: &Bound<'_, PyTuple> = state.downcast::<PyTuple>()?;
305 let binding = py_tuple.get_item(0)?;
306 let instrument_id_str = binding.downcast::<PyString>()?.extract::<&str>()?;
307 let value_raw = py_tuple
308 .get_item(1)?
309 .downcast::<PyInt>()?
310 .extract::<PriceRaw>()?;
311 let value_prec = py_tuple.get_item(2)?.downcast::<PyInt>()?.extract::<u8>()?;
312
313 let ts_event = py_tuple
314 .get_item(7)?
315 .downcast::<PyInt>()?
316 .extract::<u64>()?;
317 let ts_init = py_tuple
318 .get_item(8)?
319 .downcast::<PyInt>()?
320 .extract::<u64>()?;
321
322 self.instrument_id = InstrumentId::from_str(instrument_id_str).map_err(to_pyvalue_err)?;
323 self.value = Price::from_raw(value_raw, value_prec);
324 self.ts_event = ts_event.into();
325 self.ts_init = ts_init.into();
326
327 Ok(())
328 }
329
330 fn __getstate__(&self, py: Python) -> PyResult<PyObject> {
331 (
332 self.instrument_id.to_string(),
333 self.value.raw,
334 self.value.precision,
335 self.ts_event.as_u64(),
336 self.ts_init.as_u64(),
337 )
338 .into_py_any(py)
339 }
340
341 fn __reduce__(&self, py: Python) -> PyResult<PyObject> {
342 let safe_constructor = py.get_type::<Self>().getattr("_safe_constructor")?;
343 let state = self.__getstate__(py)?;
344 (safe_constructor, PyTuple::empty(py), state).into_py_any(py)
345 }
346
347 #[staticmethod]
348 fn _safe_constructor() -> Self {
349 Self::new(
350 InstrumentId::from("NULL.NULL"),
351 Price::zero(0),
352 UnixNanos::default(),
353 UnixNanos::default(),
354 )
355 }
356
357 fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py<PyAny> {
358 match op {
359 CompareOp::Eq => self.eq(other).into_py_any_unwrap(py),
360 CompareOp::Ne => self.ne(other).into_py_any_unwrap(py),
361 _ => py.NotImplemented(),
362 }
363 }
364
365 fn __hash__(&self) -> isize {
366 let mut h = DefaultHasher::new();
367 self.hash(&mut h);
368 h.finish() as isize
369 }
370
371 fn __repr__(&self) -> String {
372 format!("{}({})", stringify!(IndexPriceUpdate), self)
373 }
374
375 fn __str__(&self) -> String {
376 self.to_string()
377 }
378
379 #[getter]
380 #[pyo3(name = "instrument_id")]
381 fn py_instrument_id(&self) -> InstrumentId {
382 self.instrument_id
383 }
384
385 #[getter]
386 #[pyo3(name = "value")]
387 fn py_value(&self) -> Price {
388 self.value
389 }
390
391 #[getter]
392 #[pyo3(name = "ts_event")]
393 fn py_ts_event(&self) -> u64 {
394 self.ts_event.as_u64()
395 }
396
397 #[getter]
398 #[pyo3(name = "ts_init")]
399 fn py_ts_init(&self) -> u64 {
400 self.ts_init.as_u64()
401 }
402
403 #[staticmethod]
404 #[pyo3(name = "fully_qualified_name")]
405 fn py_fully_qualified_name() -> String {
406 format!("{}:{}", PY_MODULE_MODEL, stringify!(IndexPriceUpdate))
407 }
408
409 #[staticmethod]
410 #[pyo3(name = "get_metadata")]
411 fn py_get_metadata(
412 instrument_id: &InstrumentId,
413 price_precision: u8,
414 ) -> PyResult<HashMap<String, String>> {
415 Ok(Self::get_metadata(instrument_id, price_precision))
416 }
417
418 #[staticmethod]
419 #[pyo3(name = "get_fields")]
420 fn py_get_fields(py: Python<'_>) -> PyResult<Bound<'_, PyDict>> {
421 let py_dict = PyDict::new(py);
422 for (k, v) in Self::get_fields() {
423 py_dict.set_item(k, v)?;
424 }
425
426 Ok(py_dict)
427 }
428
429 #[staticmethod]
431 #[pyo3(name = "from_dict")]
432 fn py_from_dict(py: Python<'_>, values: Py<PyDict>) -> PyResult<Self> {
433 from_dict_pyo3(py, values)
434 }
435
436 #[staticmethod]
437 #[pyo3(name = "from_json")]
438 fn py_from_json(data: Vec<u8>) -> PyResult<Self> {
439 Self::from_json_bytes(&data).map_err(to_pyvalue_err)
440 }
441
442 #[staticmethod]
443 #[pyo3(name = "from_msgpack")]
444 fn py_from_msgpack(data: Vec<u8>) -> PyResult<Self> {
445 Self::from_msgpack_bytes(&data).map_err(to_pyvalue_err)
446 }
447
448 #[pyo3(name = "to_dict")]
450 fn py_to_dict(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
451 to_dict_pyo3(py, self)
452 }
453
454 #[pyo3(name = "to_json_bytes")]
456 fn py_to_json_bytes(&self, py: Python<'_>) -> Py<PyAny> {
457 self.to_json_bytes().unwrap().into_py_any_unwrap(py)
459 }
460
461 #[pyo3(name = "to_msgpack_bytes")]
463 fn py_to_msgpack_bytes(&self, py: Python<'_>) -> Py<PyAny> {
464 self.to_msgpack_bytes().unwrap().into_py_any_unwrap(py)
466 }
467}
468
469#[cfg(test)]
473mod tests {
474 use nautilus_core::python::IntoPyObjectPoseiExt;
475 use pyo3::Python;
476 use rstest::{fixture, rstest};
477
478 use super::*;
479 use crate::{identifiers::InstrumentId, types::Price};
480
481 #[fixture]
482 fn mark_price() -> MarkPriceUpdate {
483 MarkPriceUpdate::new(
484 InstrumentId::from("BTC-USDT.OKX"),
485 Price::from("100_000.00"),
486 UnixNanos::from(1),
487 UnixNanos::from(2),
488 )
489 }
490
491 #[fixture]
492 fn index_price() -> IndexPriceUpdate {
493 IndexPriceUpdate::new(
494 InstrumentId::from("BTC-USDT.OKX"),
495 Price::from("100_000.00"),
496 UnixNanos::from(1),
497 UnixNanos::from(2),
498 )
499 }
500
501 #[rstest]
502 fn test_mark_price_to_dict(mark_price: MarkPriceUpdate) {
503 pyo3::prepare_freethreaded_python();
504
505 Python::with_gil(|py| {
506 let dict_string = mark_price.py_to_dict(py).unwrap().to_string();
507 let expected_string = r"{'type': 'MarkPriceUpdate', 'instrument_id': 'BTC-USDT.OKX', 'value': '100000.00', 'ts_event': 1, 'ts_init': 2}";
508 assert_eq!(dict_string, expected_string);
509 });
510 }
511
512 #[rstest]
513 fn test_mark_price_from_dict(mark_price: MarkPriceUpdate) {
514 pyo3::prepare_freethreaded_python();
515
516 Python::with_gil(|py| {
517 let dict = mark_price.py_to_dict(py).unwrap();
518 let parsed = MarkPriceUpdate::py_from_dict(py, dict).unwrap();
519 assert_eq!(parsed, mark_price);
520 });
521 }
522
523 #[rstest]
524 fn test_mark_price_from_pyobject(mark_price: MarkPriceUpdate) {
525 pyo3::prepare_freethreaded_python();
526
527 Python::with_gil(|py| {
528 let tick_pyobject = mark_price.into_py_any_unwrap(py);
529 let parsed_tick = MarkPriceUpdate::from_pyobject(tick_pyobject.bind(py)).unwrap();
530 assert_eq!(parsed_tick, mark_price);
531 });
532 }
533
534 #[rstest]
535 fn test_index_price_to_dict(index_price: IndexPriceUpdate) {
536 pyo3::prepare_freethreaded_python();
537
538 Python::with_gil(|py| {
539 let dict_string = index_price.py_to_dict(py).unwrap().to_string();
540 let expected_string = r"{'type': 'IndexPriceUpdate', 'instrument_id': 'BTC-USDT.OKX', 'value': '100000.00', 'ts_event': 1, 'ts_init': 2}";
541 assert_eq!(dict_string, expected_string);
542 });
543 }
544
545 #[rstest]
546 fn test_index_price_from_dict(index_price: IndexPriceUpdate) {
547 pyo3::prepare_freethreaded_python();
548
549 Python::with_gil(|py| {
550 let dict = index_price.py_to_dict(py).unwrap();
551 let parsed = IndexPriceUpdate::py_from_dict(py, dict).unwrap();
552 assert_eq!(parsed, index_price);
553 });
554 }
555
556 #[rstest]
557 fn test_index_price_from_pyobject(index_price: IndexPriceUpdate) {
558 pyo3::prepare_freethreaded_python();
559
560 Python::with_gil(|py| {
561 let tick_pyobject = index_price.into_py_any_unwrap(py);
562 let parsed_tick = IndexPriceUpdate::from_pyobject(tick_pyobject.bind(py)).unwrap();
563 assert_eq!(parsed_tick, index_price);
564 });
565 }
566}