Skip to main content

nautilus_model/python/data/
prices.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.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        IntoPyObjectNautilusExt, correctness_error_to_pyvalue_err,
26        serialization::{from_dict_pyo3, to_dict_pyo3},
27        to_pyvalue_err,
28    },
29    serialization::{
30        Serializable,
31        msgpack::{FromMsgPack, ToMsgPack},
32    },
33};
34use pyo3::{
35    IntoPyObjectExt,
36    prelude::*,
37    pyclass::CompareOp,
38    types::{PyDict, PyInt, PyString, PyTuple},
39};
40
41use crate::{
42    data::{IndexPriceUpdate, MarkPriceUpdate},
43    identifiers::InstrumentId,
44    python::common::PY_MODULE_MODEL,
45    types::price::{Price, PriceRaw},
46};
47
48#[pymethods]
49#[pyo3_stub_gen::derive::gen_stub_pymethods]
50impl MarkPriceUpdate {
51    /// Represents a mark price update.
52    #[new]
53    fn py_new(instrument_id: InstrumentId, value: Price, ts_event: u64, ts_init: u64) -> Self {
54        Self::new(instrument_id, value, ts_event.into(), ts_init.into())
55    }
56
57    fn __setstate__(&mut self, state: &Bound<'_, PyAny>) -> PyResult<()> {
58        let py_tuple: &Bound<'_, PyTuple> = state.cast::<PyTuple>()?;
59        let binding = py_tuple.get_item(0)?;
60        let instrument_id_str = binding.cast::<PyString>()?.extract::<&str>()?;
61        let value_raw = py_tuple
62            .get_item(1)?
63            .cast::<PyInt>()?
64            .extract::<PriceRaw>()?;
65        let value_prec = py_tuple.get_item(2)?.cast::<PyInt>()?.extract::<u8>()?;
66
67        let ts_event = py_tuple.get_item(3)?.cast::<PyInt>()?.extract::<u64>()?;
68        let ts_init = py_tuple.get_item(4)?.cast::<PyInt>()?.extract::<u64>()?;
69
70        let instrument_id = InstrumentId::from_str(instrument_id_str).map_err(to_pyvalue_err)?;
71        let value = Price::from_raw_checked(value_raw, value_prec)
72            .map_err(correctness_error_to_pyvalue_err)?;
73
74        *self = Self::new(instrument_id, value, ts_event.into(), ts_init.into());
75
76        Ok(())
77    }
78
79    fn __getstate__(&self, py: Python) -> PyResult<Py<PyAny>> {
80        (
81            self.instrument_id.to_string(),
82            self.value.raw(),
83            self.value.precision,
84            self.ts_event.as_u64(),
85            self.ts_init.as_u64(),
86        )
87            .into_py_any(py)
88    }
89
90    fn __reduce__(&self, py: Python) -> PyResult<Py<PyAny>> {
91        let safe_constructor = py.get_type::<Self>().getattr("_safe_constructor")?;
92        let state = self.__getstate__(py)?;
93        (safe_constructor, PyTuple::empty(py), state).into_py_any(py)
94    }
95
96    #[staticmethod]
97    fn _safe_constructor() -> Self {
98        Self::new(
99            InstrumentId::from("NULL.NULL"),
100            Price::zero(0),
101            UnixNanos::default(),
102            UnixNanos::default(),
103        )
104    }
105
106    fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py<PyAny> {
107        match op {
108            CompareOp::Eq => self.eq(other).into_py_any_unwrap(py),
109            CompareOp::Ne => self.ne(other).into_py_any_unwrap(py),
110            _ => py.NotImplemented(),
111        }
112    }
113
114    fn __hash__(&self) -> isize {
115        let mut h = DefaultHasher::new();
116        self.hash(&mut h);
117        h.finish() as isize
118    }
119
120    fn __repr__(&self) -> String {
121        format!("{}({})", stringify!(MarkPriceUpdate), self)
122    }
123
124    fn __str__(&self) -> String {
125        self.to_string()
126    }
127
128    #[getter]
129    #[pyo3(name = "instrument_id")]
130    fn py_instrument_id(&self) -> InstrumentId {
131        self.instrument_id
132    }
133
134    #[getter]
135    #[pyo3(name = "value")]
136    fn py_value(&self) -> Price {
137        self.value
138    }
139
140    #[getter]
141    #[pyo3(name = "ts_event")]
142    fn py_ts_event(&self) -> u64 {
143        self.ts_event.as_u64()
144    }
145
146    #[getter]
147    #[pyo3(name = "ts_init")]
148    fn py_ts_init(&self) -> u64 {
149        self.ts_init.as_u64()
150    }
151
152    #[staticmethod]
153    #[pyo3(name = "fully_qualified_name")]
154    fn py_fully_qualified_name() -> String {
155        format!("{}:{}", PY_MODULE_MODEL, stringify!(MarkPriceUpdate))
156    }
157
158    /// Returns the metadata for the type, for use with serialization formats.
159    #[staticmethod]
160    #[pyo3(name = "get_metadata")]
161    fn py_get_metadata(
162        instrument_id: &InstrumentId,
163        price_precision: u8,
164    ) -> HashMap<String, String> {
165        Self::get_metadata(instrument_id, price_precision)
166    }
167
168    /// Returns the field map for the type, for use with Arrow schemas.
169    #[staticmethod]
170    #[pyo3(name = "get_fields")]
171    fn py_get_fields(py: Python<'_>) -> PyResult<Bound<'_, PyDict>> {
172        let py_dict = PyDict::new(py);
173        for (k, v) in Self::get_fields() {
174            py_dict.set_item(k, v)?;
175        }
176
177        Ok(py_dict)
178    }
179
180    /// Returns a new object from the given dictionary representation.
181    #[staticmethod]
182    #[pyo3(name = "from_dict")]
183    fn py_from_dict(py: Python<'_>, values: Py<PyDict>) -> PyResult<Self> {
184        from_dict_pyo3(py, values)
185    }
186
187    /// Return a dictionary representation of the object.
188    #[pyo3(name = "to_dict")]
189    fn py_to_dict(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
190        to_dict_pyo3(py, self)
191    }
192
193    /// Return JSON encoded bytes representation of the object.
194    #[pyo3(name = "to_json_bytes")]
195    fn py_to_json_bytes(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
196        self.to_json_bytes()
197            .map_err(to_pyvalue_err)?
198            .into_py_any(py)
199    }
200
201    /// Return `MsgPack` encoded bytes representation of the object.
202    #[pyo3(name = "to_msgpack_bytes")]
203    fn py_to_msgpack_bytes(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
204        self.to_msgpack_bytes()
205            .map_err(to_pyvalue_err)?
206            .into_py_any(py)
207    }
208}
209
210#[pymethods]
211impl MarkPriceUpdate {
212    #[staticmethod]
213    #[pyo3(name = "from_json")]
214    fn py_from_json(data: &[u8]) -> PyResult<Self> {
215        Self::from_json_bytes(data).map_err(to_pyvalue_err)
216    }
217
218    #[staticmethod]
219    #[pyo3(name = "from_msgpack")]
220    fn py_from_msgpack(data: &[u8]) -> PyResult<Self> {
221        Self::from_msgpack_bytes(data).map_err(to_pyvalue_err)
222    }
223}
224
225#[pymethods]
226#[pyo3_stub_gen::derive::gen_stub_pymethods]
227impl IndexPriceUpdate {
228    /// Represents an index price update.
229    #[new]
230    fn py_new(instrument_id: InstrumentId, value: Price, ts_event: u64, ts_init: u64) -> Self {
231        Self::new(instrument_id, value, ts_event.into(), ts_init.into())
232    }
233
234    fn __setstate__(&mut self, state: &Bound<'_, PyAny>) -> PyResult<()> {
235        let py_tuple: &Bound<'_, PyTuple> = state.cast::<PyTuple>()?;
236        let binding = py_tuple.get_item(0)?;
237        let instrument_id_str = binding.cast::<PyString>()?.extract::<&str>()?;
238        let value_raw = py_tuple
239            .get_item(1)?
240            .cast::<PyInt>()?
241            .extract::<PriceRaw>()?;
242        let value_prec = py_tuple.get_item(2)?.cast::<PyInt>()?.extract::<u8>()?;
243
244        let ts_event = py_tuple.get_item(3)?.cast::<PyInt>()?.extract::<u64>()?;
245        let ts_init = py_tuple.get_item(4)?.cast::<PyInt>()?.extract::<u64>()?;
246
247        let instrument_id = InstrumentId::from_str(instrument_id_str).map_err(to_pyvalue_err)?;
248        let value = Price::from_raw_checked(value_raw, value_prec)
249            .map_err(correctness_error_to_pyvalue_err)?;
250
251        *self = Self::new(instrument_id, value, ts_event.into(), ts_init.into());
252
253        Ok(())
254    }
255
256    fn __getstate__(&self, py: Python) -> PyResult<Py<PyAny>> {
257        (
258            self.instrument_id.to_string(),
259            self.value.raw(),
260            self.value.precision,
261            self.ts_event.as_u64(),
262            self.ts_init.as_u64(),
263        )
264            .into_py_any(py)
265    }
266
267    fn __reduce__(&self, py: Python) -> PyResult<Py<PyAny>> {
268        let safe_constructor = py.get_type::<Self>().getattr("_safe_constructor")?;
269        let state = self.__getstate__(py)?;
270        (safe_constructor, PyTuple::empty(py), state).into_py_any(py)
271    }
272
273    #[staticmethod]
274    fn _safe_constructor() -> Self {
275        Self::new(
276            InstrumentId::from("NULL.NULL"),
277            Price::zero(0),
278            UnixNanos::default(),
279            UnixNanos::default(),
280        )
281    }
282
283    fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py<PyAny> {
284        match op {
285            CompareOp::Eq => self.eq(other).into_py_any_unwrap(py),
286            CompareOp::Ne => self.ne(other).into_py_any_unwrap(py),
287            _ => py.NotImplemented(),
288        }
289    }
290
291    fn __hash__(&self) -> isize {
292        let mut h = DefaultHasher::new();
293        self.hash(&mut h);
294        h.finish() as isize
295    }
296
297    fn __repr__(&self) -> String {
298        format!("{}({})", stringify!(IndexPriceUpdate), self)
299    }
300
301    fn __str__(&self) -> String {
302        self.to_string()
303    }
304
305    #[getter]
306    #[pyo3(name = "instrument_id")]
307    fn py_instrument_id(&self) -> InstrumentId {
308        self.instrument_id
309    }
310
311    #[getter]
312    #[pyo3(name = "value")]
313    fn py_value(&self) -> Price {
314        self.value
315    }
316
317    #[getter]
318    #[pyo3(name = "ts_event")]
319    fn py_ts_event(&self) -> u64 {
320        self.ts_event.as_u64()
321    }
322
323    #[getter]
324    #[pyo3(name = "ts_init")]
325    fn py_ts_init(&self) -> u64 {
326        self.ts_init.as_u64()
327    }
328
329    #[staticmethod]
330    #[pyo3(name = "fully_qualified_name")]
331    fn py_fully_qualified_name() -> String {
332        format!("{}:{}", PY_MODULE_MODEL, stringify!(IndexPriceUpdate))
333    }
334
335    /// Returns the metadata for the type, for use with serialization formats.
336    #[staticmethod]
337    #[pyo3(name = "get_metadata")]
338    fn py_get_metadata(
339        instrument_id: &InstrumentId,
340        price_precision: u8,
341    ) -> HashMap<String, String> {
342        Self::get_metadata(instrument_id, price_precision)
343    }
344
345    /// Returns the field map for the type, for use with Arrow schemas.
346    #[staticmethod]
347    #[pyo3(name = "get_fields")]
348    fn py_get_fields(py: Python<'_>) -> PyResult<Bound<'_, PyDict>> {
349        let py_dict = PyDict::new(py);
350        for (k, v) in Self::get_fields() {
351            py_dict.set_item(k, v)?;
352        }
353
354        Ok(py_dict)
355    }
356
357    /// Returns a new object from the given dictionary representation.
358    #[staticmethod]
359    #[pyo3(name = "from_dict")]
360    fn py_from_dict(py: Python<'_>, values: Py<PyDict>) -> PyResult<Self> {
361        from_dict_pyo3(py, values)
362    }
363
364    /// Return a dictionary representation of the object.
365    #[pyo3(name = "to_dict")]
366    fn py_to_dict(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
367        to_dict_pyo3(py, self)
368    }
369
370    /// Return JSON encoded bytes representation of the object.
371    #[pyo3(name = "to_json_bytes")]
372    fn py_to_json_bytes(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
373        self.to_json_bytes()
374            .map_err(to_pyvalue_err)?
375            .into_py_any(py)
376    }
377
378    /// Return `MsgPack` encoded bytes representation of the object.
379    #[pyo3(name = "to_msgpack_bytes")]
380    fn py_to_msgpack_bytes(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
381        self.to_msgpack_bytes()
382            .map_err(to_pyvalue_err)?
383            .into_py_any(py)
384    }
385}
386
387#[pymethods]
388impl IndexPriceUpdate {
389    #[staticmethod]
390    #[pyo3(name = "from_json")]
391    fn py_from_json(data: &[u8]) -> PyResult<Self> {
392        Self::from_json_bytes(data).map_err(to_pyvalue_err)
393    }
394
395    #[staticmethod]
396    #[pyo3(name = "from_msgpack")]
397    fn py_from_msgpack(data: &[u8]) -> PyResult<Self> {
398        Self::from_msgpack_bytes(data).map_err(to_pyvalue_err)
399    }
400}
401
402#[cfg(test)]
403mod tests {
404    use pyo3::Python;
405    use rstest::{fixture, rstest};
406
407    use super::*;
408    use crate::{identifiers::InstrumentId, types::Price};
409
410    #[fixture]
411    fn mark_price() -> MarkPriceUpdate {
412        MarkPriceUpdate::new(
413            InstrumentId::from("BTC-USDT.OKX"),
414            Price::from("100_000.00"),
415            UnixNanos::from(1),
416            UnixNanos::from(2),
417        )
418    }
419
420    #[fixture]
421    fn index_price() -> IndexPriceUpdate {
422        IndexPriceUpdate::new(
423            InstrumentId::from("BTC-USDT.OKX"),
424            Price::from("100_000.00"),
425            UnixNanos::from(1),
426            UnixNanos::from(2),
427        )
428    }
429
430    #[rstest]
431    fn test_mark_price_to_dict(mark_price: MarkPriceUpdate) {
432        Python::initialize();
433        Python::attach(|py| {
434            let dict_string = mark_price.py_to_dict(py).unwrap().to_string();
435            let expected_string = "{'type': 'MarkPriceUpdate', 'instrument_id': 'BTC-USDT.OKX', 'value': '100000.00', 'ts_event': 1, 'ts_init': 2}";
436            assert_eq!(dict_string, expected_string);
437        });
438    }
439
440    #[rstest]
441    fn test_mark_price_from_dict(mark_price: MarkPriceUpdate) {
442        Python::initialize();
443        Python::attach(|py| {
444            let dict = mark_price.py_to_dict(py).unwrap();
445            let parsed = MarkPriceUpdate::py_from_dict(py, dict).unwrap();
446            assert_eq!(parsed, mark_price);
447        });
448    }
449
450    #[rstest]
451    fn test_index_price_to_dict(index_price: IndexPriceUpdate) {
452        Python::initialize();
453        Python::attach(|py| {
454            let dict_string = index_price.py_to_dict(py).unwrap().to_string();
455            let expected_string = "{'type': 'IndexPriceUpdate', 'instrument_id': 'BTC-USDT.OKX', 'value': '100000.00', 'ts_event': 1, 'ts_init': 2}";
456            assert_eq!(dict_string, expected_string);
457        });
458    }
459
460    #[rstest]
461    fn test_index_price_from_dict(index_price: IndexPriceUpdate) {
462        Python::initialize();
463        Python::attach(|py| {
464            let dict = index_price.py_to_dict(py).unwrap();
465            let parsed = IndexPriceUpdate::py_from_dict(py, dict).unwrap();
466            assert_eq!(parsed, index_price);
467        });
468    }
469}