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