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 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]
194 #[pyo3(name = "get_metadata")]
195 fn py_get_metadata(
196 instrument_id: &InstrumentId,
197 price_precision: u8,
198 ) -> PyResult<HashMap<String, String>> {
199 Ok(Self::get_metadata(instrument_id, price_precision))
200 }
201
202 #[staticmethod]
203 #[pyo3(name = "get_fields")]
204 fn py_get_fields(py: Python<'_>) -> PyResult<Bound<'_, PyDict>> {
205 let py_dict = PyDict::new(py);
206 for (k, v) in Self::get_fields() {
207 py_dict.set_item(k, v)?;
208 }
209
210 Ok(py_dict)
211 }
212
213 #[staticmethod]
215 #[pyo3(name = "from_dict")]
216 fn py_from_dict(py: Python<'_>, values: Py<PyDict>) -> PyResult<Self> {
217 from_dict_pyo3(py, values)
218 }
219
220 #[staticmethod]
221 #[pyo3(name = "from_json")]
222 fn py_from_json(data: Vec<u8>) -> PyResult<Self> {
223 Self::from_json_bytes(&data).map_err(to_pyvalue_err)
224 }
225
226 #[staticmethod]
227 #[pyo3(name = "from_msgpack")]
228 fn py_from_msgpack(data: Vec<u8>) -> PyResult<Self> {
229 Self::from_msgpack_bytes(&data).map_err(to_pyvalue_err)
230 }
231
232 #[pyo3(name = "as_dict")]
234 fn py_as_dict(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
235 to_dict_pyo3(py, self)
236 }
237
238 #[pyo3(name = "as_json")]
240 fn py_as_json(&self, py: Python<'_>) -> Py<PyAny> {
241 self.as_json_bytes().unwrap().into_py_any_unwrap(py)
243 }
244
245 #[pyo3(name = "as_msgpack")]
247 fn py_as_msgpack(&self, py: Python<'_>) -> Py<PyAny> {
248 self.as_msgpack_bytes().unwrap().into_py_any_unwrap(py)
250 }
251}
252
253impl IndexPriceUpdate {
254 pub fn from_pyobject(obj: &Bound<'_, PyAny>) -> PyResult<Self> {
256 let instrument_id_obj: Bound<'_, PyAny> = obj.getattr("instrument_id")?.extract()?;
257 let instrument_id_str: String = instrument_id_obj.getattr("value")?.extract()?;
258 let instrument_id =
259 InstrumentId::from_str(instrument_id_str.as_str()).map_err(to_pyvalue_err)?;
260
261 let value_py: Bound<'_, PyAny> = obj.getattr("value")?.extract()?;
262 let value_raw: PriceRaw = value_py.getattr("raw")?.extract()?;
263 let value_prec: u8 = value_py.getattr("precision")?.extract()?;
264 let value = Price::from_raw(value_raw, value_prec);
265
266 let ts_event: u64 = obj.getattr("ts_event")?.extract()?;
267 let ts_init: u64 = obj.getattr("ts_init")?.extract()?;
268
269 Ok(Self::new(
270 instrument_id,
271 value,
272 ts_event.into(),
273 ts_init.into(),
274 ))
275 }
276}
277
278#[pymethods]
279impl IndexPriceUpdate {
280 #[new]
281 fn py_new(
282 instrument_id: InstrumentId,
283 value: Price,
284 ts_event: u64,
285 ts_init: u64,
286 ) -> PyResult<Self> {
287 Ok(Self::new(
288 instrument_id,
289 value,
290 ts_event.into(),
291 ts_init.into(),
292 ))
293 }
294
295 fn __setstate__(&mut self, state: &Bound<'_, PyAny>) -> PyResult<()> {
296 let py_tuple: &Bound<'_, PyTuple> = state.downcast::<PyTuple>()?;
297 let binding = py_tuple.get_item(0)?;
298 let instrument_id_str = binding.downcast::<PyString>()?.extract::<&str>()?;
299 let value_raw = py_tuple
300 .get_item(1)?
301 .downcast::<PyInt>()?
302 .extract::<PriceRaw>()?;
303 let value_prec = py_tuple.get_item(2)?.downcast::<PyInt>()?.extract::<u8>()?;
304
305 let ts_event = py_tuple
306 .get_item(7)?
307 .downcast::<PyInt>()?
308 .extract::<u64>()?;
309 let ts_init = py_tuple
310 .get_item(8)?
311 .downcast::<PyInt>()?
312 .extract::<u64>()?;
313
314 self.instrument_id = InstrumentId::from_str(instrument_id_str).map_err(to_pyvalue_err)?;
315 self.value = Price::from_raw(value_raw, value_prec);
316 self.ts_event = ts_event.into();
317 self.ts_init = ts_init.into();
318
319 Ok(())
320 }
321
322 fn __getstate__(&self, py: Python) -> PyResult<PyObject> {
323 (
324 self.instrument_id.to_string(),
325 self.value.raw,
326 self.value.precision,
327 self.ts_event.as_u64(),
328 self.ts_init.as_u64(),
329 )
330 .into_py_any(py)
331 }
332
333 fn __reduce__(&self, py: Python) -> PyResult<PyObject> {
334 let safe_constructor = py.get_type::<Self>().getattr("_safe_constructor")?;
335 let state = self.__getstate__(py)?;
336 (safe_constructor, PyTuple::empty(py), state).into_py_any(py)
337 }
338
339 #[staticmethod]
340 fn _safe_constructor() -> Self {
341 Self::new(
342 InstrumentId::from("NULL.NULL"),
343 Price::zero(0),
344 UnixNanos::default(),
345 UnixNanos::default(),
346 )
347 }
348
349 fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py<PyAny> {
350 match op {
351 CompareOp::Eq => self.eq(other).into_py_any_unwrap(py),
352 CompareOp::Ne => self.ne(other).into_py_any_unwrap(py),
353 _ => py.NotImplemented(),
354 }
355 }
356
357 fn __hash__(&self) -> isize {
358 let mut h = DefaultHasher::new();
359 self.hash(&mut h);
360 h.finish() as isize
361 }
362
363 fn __repr__(&self) -> String {
364 format!("{}({})", stringify!(IndexPriceUpdate), self)
365 }
366
367 fn __str__(&self) -> String {
368 self.to_string()
369 }
370
371 #[getter]
372 #[pyo3(name = "instrument_id")]
373 fn py_instrument_id(&self) -> InstrumentId {
374 self.instrument_id
375 }
376
377 #[getter]
378 #[pyo3(name = "value")]
379 fn py_value(&self) -> Price {
380 self.value
381 }
382
383 #[getter]
384 #[pyo3(name = "ts_event")]
385 fn py_ts_event(&self) -> u64 {
386 self.ts_event.as_u64()
387 }
388
389 #[getter]
390 #[pyo3(name = "ts_init")]
391 fn py_ts_init(&self) -> u64 {
392 self.ts_init.as_u64()
393 }
394
395 #[staticmethod]
396 #[pyo3(name = "fully_qualified_name")]
397 fn py_fully_qualified_name() -> String {
398 format!("{}:{}", PY_MODULE_MODEL, stringify!(IndexPriceUpdate))
399 }
400
401 #[staticmethod]
402 #[pyo3(name = "get_metadata")]
403 fn py_get_metadata(
404 instrument_id: &InstrumentId,
405 price_precision: u8,
406 ) -> PyResult<HashMap<String, String>> {
407 Ok(Self::get_metadata(instrument_id, price_precision))
408 }
409
410 #[staticmethod]
411 #[pyo3(name = "get_fields")]
412 fn py_get_fields(py: Python<'_>) -> PyResult<Bound<'_, PyDict>> {
413 let py_dict = PyDict::new(py);
414 for (k, v) in Self::get_fields() {
415 py_dict.set_item(k, v)?;
416 }
417
418 Ok(py_dict)
419 }
420
421 #[staticmethod]
423 #[pyo3(name = "from_dict")]
424 fn py_from_dict(py: Python<'_>, values: Py<PyDict>) -> PyResult<Self> {
425 from_dict_pyo3(py, values)
426 }
427
428 #[staticmethod]
429 #[pyo3(name = "from_json")]
430 fn py_from_json(data: Vec<u8>) -> PyResult<Self> {
431 Self::from_json_bytes(&data).map_err(to_pyvalue_err)
432 }
433
434 #[staticmethod]
435 #[pyo3(name = "from_msgpack")]
436 fn py_from_msgpack(data: Vec<u8>) -> PyResult<Self> {
437 Self::from_msgpack_bytes(&data).map_err(to_pyvalue_err)
438 }
439
440 #[pyo3(name = "as_dict")]
442 fn py_as_dict(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
443 to_dict_pyo3(py, self)
444 }
445
446 #[pyo3(name = "as_json")]
448 fn py_as_json(&self, py: Python<'_>) -> Py<PyAny> {
449 self.as_json_bytes().unwrap().into_py_any_unwrap(py)
451 }
452
453 #[pyo3(name = "as_msgpack")]
455 fn py_as_msgpack(&self, py: Python<'_>) -> Py<PyAny> {
456 self.as_msgpack_bytes().unwrap().into_py_any_unwrap(py)
458 }
459}
460
461#[cfg(test)]
465mod tests {
466 use nautilus_core::python::IntoPyObjectNautilusExt;
467 use pyo3::Python;
468 use rstest::{fixture, rstest};
469
470 use super::*;
471 use crate::{identifiers::InstrumentId, types::Price};
472
473 #[fixture]
474 fn mark_price() -> MarkPriceUpdate {
475 MarkPriceUpdate::new(
476 InstrumentId::from("BTC-USDT.OKX"),
477 Price::from("100_000.00"),
478 UnixNanos::from(1),
479 UnixNanos::from(2),
480 )
481 }
482
483 #[fixture]
484 fn index_price() -> IndexPriceUpdate {
485 IndexPriceUpdate::new(
486 InstrumentId::from("BTC-USDT.OKX"),
487 Price::from("100_000.00"),
488 UnixNanos::from(1),
489 UnixNanos::from(2),
490 )
491 }
492
493 #[rstest]
494 fn test_mark_price_as_dict(mark_price: MarkPriceUpdate) {
495 pyo3::prepare_freethreaded_python();
496
497 Python::with_gil(|py| {
498 let dict_string = mark_price.py_as_dict(py).unwrap().to_string();
499 let expected_string = r"{'type': 'MarkPriceUpdate', 'instrument_id': 'BTC-USDT.OKX', 'value': '100000.00', 'ts_event': 1, 'ts_init': 2}";
500 assert_eq!(dict_string, expected_string);
501 });
502 }
503
504 #[rstest]
505 fn test_mark_price_from_dict(mark_price: MarkPriceUpdate) {
506 pyo3::prepare_freethreaded_python();
507
508 Python::with_gil(|py| {
509 let dict = mark_price.py_as_dict(py).unwrap();
510 let parsed = MarkPriceUpdate::py_from_dict(py, dict).unwrap();
511 assert_eq!(parsed, mark_price);
512 });
513 }
514
515 #[rstest]
516 fn test_mark_price_from_pyobject(mark_price: MarkPriceUpdate) {
517 pyo3::prepare_freethreaded_python();
518
519 Python::with_gil(|py| {
520 let tick_pyobject = mark_price.into_py_any_unwrap(py);
521 let parsed_tick = MarkPriceUpdate::from_pyobject(tick_pyobject.bind(py)).unwrap();
522 assert_eq!(parsed_tick, mark_price);
523 });
524 }
525
526 #[rstest]
527 fn test_index_price_as_dict(index_price: IndexPriceUpdate) {
528 pyo3::prepare_freethreaded_python();
529
530 Python::with_gil(|py| {
531 let dict_string = index_price.py_as_dict(py).unwrap().to_string();
532 let expected_string = r"{'type': 'IndexPriceUpdate', 'instrument_id': 'BTC-USDT.OKX', 'value': '100000.00', 'ts_event': 1, 'ts_init': 2}";
533 assert_eq!(dict_string, expected_string);
534 });
535 }
536
537 #[rstest]
538 fn test_index_price_from_dict(index_price: IndexPriceUpdate) {
539 pyo3::prepare_freethreaded_python();
540
541 Python::with_gil(|py| {
542 let dict = index_price.py_as_dict(py).unwrap();
543 let parsed = IndexPriceUpdate::py_from_dict(py, dict).unwrap();
544 assert_eq!(parsed, index_price);
545 });
546 }
547
548 #[rstest]
549 fn test_index_price_from_pyobject(index_price: IndexPriceUpdate) {
550 pyo3::prepare_freethreaded_python();
551
552 Python::with_gil(|py| {
553 let tick_pyobject = index_price.into_py_any_unwrap(py);
554 let parsed_tick = IndexPriceUpdate::from_pyobject(tick_pyobject.bind(py)).unwrap();
555 assert_eq!(parsed_tick, index_price);
556 });
557 }
558}