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, 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 #[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 #[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 #[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 #[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 #[pyo3(name = "to_dict")]
189 fn py_to_dict(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
190 to_dict_pyo3(py, self)
191 }
192
193 #[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 #[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 #[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 #[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 #[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 #[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 #[pyo3(name = "to_dict")]
366 fn py_to_dict(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
367 to_dict_pyo3(py, self)
368 }
369
370 #[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 #[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}