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