nautilus_model/python/data/
prices.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Posei Systems Pty Ltd. All rights reserved.
3//  https://poseitrader.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use 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    /// Creates a new [`MarkPriceUpdate`] from a Python object.
47    ///
48    /// # Errors
49    ///
50    /// Returns a `PyErr` if attribute extraction or type conversion fails.
51    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    /// Returns a new object from the given dictionary representation.
218    #[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    /// Return a dictionary representation of the object.
237    #[pyo3(name = "to_dict")]
238    fn py_to_dict(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
239        to_dict_pyo3(py, self)
240    }
241
242    /// Return JSON encoded bytes representation of the object.
243    #[pyo3(name = "to_json_bytes")]
244    fn py_to_json_bytes(&self, py: Python<'_>) -> Py<PyAny> {
245        // SAFETY: Unwrap safe when serializing a valid object
246        self.to_json_bytes().unwrap().into_py_any_unwrap(py)
247    }
248
249    /// Return MsgPack encoded bytes representation of the object.
250    #[pyo3(name = "to_msgpack_bytes")]
251    fn py_to_msgpack_bytes(&self, py: Python<'_>) -> Py<PyAny> {
252        // SAFETY: Unwrap safe when serializing a valid object
253        self.to_msgpack_bytes().unwrap().into_py_any_unwrap(py)
254    }
255}
256
257impl IndexPriceUpdate {
258    /// Creates a new [`IndexPriceUpdate`] from a Python object.
259    ///
260    /// # Errors
261    ///
262    /// Returns a `PyErr` if attribute extraction or type conversion fails.
263    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    /// Returns a new object from the given dictionary representation.
430    #[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    /// Return a dictionary representation of the object.
449    #[pyo3(name = "to_dict")]
450    fn py_to_dict(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
451        to_dict_pyo3(py, self)
452    }
453
454    /// Return JSON encoded bytes representation of the object.
455    #[pyo3(name = "to_json_bytes")]
456    fn py_to_json_bytes(&self, py: Python<'_>) -> Py<PyAny> {
457        // SAFETY: Unwrap safe when serializing a valid object
458        self.to_json_bytes().unwrap().into_py_any_unwrap(py)
459    }
460
461    /// Return MsgPack encoded bytes representation of the object.
462    #[pyo3(name = "to_msgpack_bytes")]
463    fn py_to_msgpack_bytes(&self, py: Python<'_>) -> Py<PyAny> {
464        // SAFETY: Unwrap safe when serializing a valid object
465        self.to_msgpack_bytes().unwrap().into_py_any_unwrap(py)
466    }
467}
468
469////////////////////////////////////////////////////////////////////////////////
470// Tests
471////////////////////////////////////////////////////////////////////////////////
472#[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}