1use 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 super::data_to_pycapsule;
39use crate::{
40 data::{Data, QuoteTick},
41 enums::PriceType,
42 identifiers::InstrumentId,
43 python::common::PY_MODULE_MODEL,
44 types::{
45 price::{Price, PriceRaw},
46 quantity::{Quantity, QuantityRaw},
47 },
48};
49
50impl QuoteTick {
51 pub fn from_pyobject(obj: &Bound<'_, PyAny>) -> PyResult<Self> {
57 let instrument_id_obj: Bound<'_, PyAny> = obj.getattr("instrument_id")?.extract()?;
58 let instrument_id_str: String = instrument_id_obj.getattr("value")?.extract()?;
59 let instrument_id =
60 InstrumentId::from_str(instrument_id_str.as_str()).map_err(to_pyvalue_err)?;
61
62 let bid_price_py: Bound<'_, PyAny> = obj.getattr("bid_price")?.extract()?;
63 let bid_price_raw: PriceRaw = bid_price_py.getattr("raw")?.extract()?;
64 let bid_price_prec: u8 = bid_price_py.getattr("precision")?.extract()?;
65 let bid_price = Price::from_raw(bid_price_raw, bid_price_prec);
66
67 let ask_price_py: Bound<'_, PyAny> = obj.getattr("ask_price")?.extract()?;
68 let ask_price_raw: PriceRaw = ask_price_py.getattr("raw")?.extract()?;
69 let ask_price_prec: u8 = ask_price_py.getattr("precision")?.extract()?;
70 let ask_price = Price::from_raw(ask_price_raw, ask_price_prec);
71
72 let bid_size_py: Bound<'_, PyAny> = obj.getattr("bid_size")?.extract()?;
73 let bid_size_raw: QuantityRaw = bid_size_py.getattr("raw")?.extract()?;
74 let bid_size_prec: u8 = bid_size_py.getattr("precision")?.extract()?;
75 let bid_size = Quantity::from_raw(bid_size_raw, bid_size_prec);
76
77 let ask_size_py: Bound<'_, PyAny> = obj.getattr("ask_size")?.extract()?;
78 let ask_size_raw: QuantityRaw = ask_size_py.getattr("raw")?.extract()?;
79 let ask_size_prec: u8 = ask_size_py.getattr("precision")?.extract()?;
80 let ask_size = Quantity::from_raw(ask_size_raw, ask_size_prec);
81
82 let ts_event: u64 = obj.getattr("ts_event")?.extract()?;
83 let ts_init: u64 = obj.getattr("ts_init")?.extract()?;
84
85 Self::new_checked(
86 instrument_id,
87 bid_price,
88 ask_price,
89 bid_size,
90 ask_size,
91 ts_event.into(),
92 ts_init.into(),
93 )
94 .map_err(to_pyvalue_err)
95 }
96}
97
98#[pymethods]
99impl QuoteTick {
100 #[new]
101 fn py_new(
102 instrument_id: InstrumentId,
103 bid_price: Price,
104 ask_price: Price,
105 bid_size: Quantity,
106 ask_size: Quantity,
107 ts_event: u64,
108 ts_init: u64,
109 ) -> PyResult<Self> {
110 Self::new_checked(
111 instrument_id,
112 bid_price,
113 ask_price,
114 bid_size,
115 ask_size,
116 ts_event.into(),
117 ts_init.into(),
118 )
119 .map_err(to_pyvalue_err)
120 }
121
122 fn __setstate__(&mut self, state: &Bound<'_, PyAny>) -> PyResult<()> {
123 let py_tuple: &Bound<'_, PyTuple> = state.downcast::<PyTuple>()?;
124 let binding = py_tuple.get_item(0)?;
125 let instrument_id_str: &str = binding.downcast::<PyString>()?.extract()?;
126 let bid_price_raw: PriceRaw = py_tuple.get_item(1)?.downcast::<PyInt>()?.extract()?;
127 let ask_price_raw: PriceRaw = py_tuple.get_item(2)?.downcast::<PyInt>()?.extract()?;
128 let bid_price_prec: u8 = py_tuple.get_item(3)?.downcast::<PyInt>()?.extract()?;
129 let ask_price_prec: u8 = py_tuple.get_item(4)?.downcast::<PyInt>()?.extract()?;
130
131 let bid_size_raw: QuantityRaw = py_tuple.get_item(5)?.downcast::<PyInt>()?.extract()?;
132 let ask_size_raw: QuantityRaw = py_tuple.get_item(6)?.downcast::<PyInt>()?.extract()?;
133 let bid_size_prec: u8 = py_tuple.get_item(7)?.downcast::<PyInt>()?.extract()?;
134 let ask_size_prec: u8 = py_tuple.get_item(8)?.downcast::<PyInt>()?.extract()?;
135 let ts_event: u64 = py_tuple.get_item(9)?.downcast::<PyInt>()?.extract()?;
136 let ts_init: u64 = py_tuple.get_item(10)?.downcast::<PyInt>()?.extract()?;
137
138 self.instrument_id = InstrumentId::from_str(instrument_id_str).map_err(to_pyvalue_err)?;
139 self.bid_price = Price::from_raw(bid_price_raw, bid_price_prec);
140 self.ask_price = Price::from_raw(ask_price_raw, ask_price_prec);
141 self.bid_size = Quantity::from_raw(bid_size_raw, bid_size_prec);
142 self.ask_size = Quantity::from_raw(ask_size_raw, ask_size_prec);
143 self.ts_event = ts_event.into();
144 self.ts_init = ts_init.into();
145
146 Ok(())
147 }
148
149 fn __getstate__(&self, py: Python) -> PyResult<PyObject> {
150 (
151 self.instrument_id.to_string(),
152 self.bid_price.raw,
153 self.ask_price.raw,
154 self.bid_price.precision,
155 self.ask_price.precision,
156 self.bid_size.raw,
157 self.ask_size.raw,
158 self.bid_size.precision,
159 self.ask_size.precision,
160 self.ts_event.as_u64(),
161 self.ts_init.as_u64(),
162 )
163 .into_py_any(py)
164 }
165
166 fn __reduce__(&self, py: Python) -> PyResult<PyObject> {
167 let safe_constructor = py.get_type::<Self>().getattr("_safe_constructor")?;
168 let state = self.__getstate__(py)?;
169 (safe_constructor, PyTuple::empty(py), state).into_py_any(py)
170 }
171
172 #[staticmethod]
173 fn _safe_constructor() -> PyResult<Self> {
174 Self::new_checked(
175 InstrumentId::from("NULL.NULL"),
176 Price::zero(0),
177 Price::zero(0),
178 Quantity::zero(0),
179 Quantity::zero(0),
180 UnixNanos::default(),
181 UnixNanos::default(),
182 )
183 .map_err(to_pyvalue_err)
184 }
185
186 fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py<PyAny> {
187 match op {
188 CompareOp::Eq => self.eq(other).into_py_any_unwrap(py),
189 CompareOp::Ne => self.ne(other).into_py_any_unwrap(py),
190 _ => py.NotImplemented(),
191 }
192 }
193
194 fn __hash__(&self) -> isize {
195 let mut h = DefaultHasher::new();
196 self.hash(&mut h);
197 h.finish() as isize
198 }
199
200 fn __repr__(&self) -> String {
201 format!("{}({})", stringify!(QuoteTick), self)
202 }
203
204 fn __str__(&self) -> String {
205 self.to_string()
206 }
207
208 #[getter]
209 #[pyo3(name = "instrument_id")]
210 fn py_instrument_id(&self) -> InstrumentId {
211 self.instrument_id
212 }
213
214 #[getter]
215 #[pyo3(name = "bid_price")]
216 fn py_bid_price(&self) -> Price {
217 self.bid_price
218 }
219
220 #[getter]
221 #[pyo3(name = "ask_price")]
222 fn py_ask_price(&self) -> Price {
223 self.ask_price
224 }
225
226 #[getter]
227 #[pyo3(name = "bid_size")]
228 fn py_bid_size(&self) -> Quantity {
229 self.bid_size
230 }
231
232 #[getter]
233 #[pyo3(name = "ask_size")]
234 fn py_ask_size(&self) -> Quantity {
235 self.ask_size
236 }
237
238 #[getter]
239 #[pyo3(name = "ts_event")]
240 fn py_ts_event(&self) -> u64 {
241 self.ts_event.as_u64()
242 }
243
244 #[getter]
245 #[pyo3(name = "ts_init")]
246 fn py_ts_init(&self) -> u64 {
247 self.ts_init.as_u64()
248 }
249
250 #[staticmethod]
251 #[pyo3(name = "fully_qualified_name")]
252 fn py_fully_qualified_name() -> String {
253 format!("{}:{}", PY_MODULE_MODEL, stringify!(QuoteTick))
254 }
255
256 #[staticmethod]
257 #[pyo3(name = "get_metadata")]
258 fn py_get_metadata(
259 instrument_id: &InstrumentId,
260 price_precision: u8,
261 size_precision: u8,
262 ) -> PyResult<HashMap<String, String>> {
263 Ok(Self::get_metadata(
264 instrument_id,
265 price_precision,
266 size_precision,
267 ))
268 }
269
270 #[staticmethod]
271 #[pyo3(name = "get_fields")]
272 fn py_get_fields(py: Python<'_>) -> PyResult<Bound<'_, PyDict>> {
273 let py_dict = PyDict::new(py);
274 for (k, v) in Self::get_fields() {
275 py_dict.set_item(k, v)?;
276 }
277
278 Ok(py_dict)
279 }
280
281 #[staticmethod]
282 #[pyo3(name = "from_raw")]
283 #[allow(clippy::too_many_arguments)]
284 fn py_from_raw(
285 instrument_id: InstrumentId,
286 bid_price_raw: PriceRaw,
287 ask_price_raw: PriceRaw,
288 bid_price_prec: u8,
289 ask_price_prec: u8,
290 bid_size_raw: QuantityRaw,
291 ask_size_raw: QuantityRaw,
292 bid_size_prec: u8,
293 ask_size_prec: u8,
294 ts_event: u64,
295 ts_init: u64,
296 ) -> PyResult<Self> {
297 Self::new_checked(
298 instrument_id,
299 Price::from_raw(bid_price_raw, bid_price_prec),
300 Price::from_raw(ask_price_raw, ask_price_prec),
301 Quantity::from_raw(bid_size_raw, bid_size_prec),
302 Quantity::from_raw(ask_size_raw, ask_size_prec),
303 ts_event.into(),
304 ts_init.into(),
305 )
306 .map_err(to_pyvalue_err)
307 }
308
309 #[staticmethod]
311 #[pyo3(name = "from_dict")]
312 fn py_from_dict(py: Python<'_>, values: Py<PyDict>) -> PyResult<Self> {
313 from_dict_pyo3(py, values)
314 }
315
316 #[staticmethod]
317 #[pyo3(name = "from_json")]
318 fn py_from_json(data: Vec<u8>) -> PyResult<Self> {
319 Self::from_json_bytes(&data).map_err(to_pyvalue_err)
320 }
321
322 #[staticmethod]
323 #[pyo3(name = "from_msgpack")]
324 fn py_from_msgpack(data: Vec<u8>) -> PyResult<Self> {
325 Self::from_msgpack_bytes(&data).map_err(to_pyvalue_err)
326 }
327
328 #[pyo3(name = "extract_price")]
329 fn py_extract_price(&self, price_type: PriceType) -> PyResult<Price> {
330 Ok(self.extract_price(price_type))
331 }
332
333 #[pyo3(name = "extract_size")]
334 fn py_extract_size(&self, price_type: PriceType) -> PyResult<Quantity> {
335 Ok(self.extract_size(price_type))
336 }
337
338 #[pyo3(name = "as_pycapsule")]
354 fn py_as_pycapsule(&self, py: Python<'_>) -> PyObject {
355 data_to_pycapsule(py, Data::Quote(*self))
356 }
357
358 #[pyo3(name = "to_dict")]
360 fn py_to_dict(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
361 to_dict_pyo3(py, self)
362 }
363
364 #[pyo3(name = "to_json_bytes")]
366 fn py_to_json_bytes(&self, py: Python<'_>) -> Py<PyAny> {
367 self.to_json_bytes().unwrap().into_py_any_unwrap(py)
369 }
370
371 #[pyo3(name = "to_msgpack_bytes")]
373 fn py_to_msgpack_bytes(&self, py: Python<'_>) -> Py<PyAny> {
374 self.to_msgpack_bytes().unwrap().into_py_any_unwrap(py)
376 }
377}
378
379#[cfg(test)]
383mod tests {
384 use nautilus_core::python::IntoPyObjectPoseiExt;
385 use pyo3::Python;
386 use rstest::rstest;
387
388 use crate::{
389 data::{QuoteTick, stubs::quote_ethusdt_binance},
390 identifiers::InstrumentId,
391 types::{Price, Quantity},
392 };
393
394 #[rstest]
395 #[case(
396 Price::from_raw(10_000_000, 6),
397 Price::from_raw(10_001_000, 7), Quantity::from_raw(1_000_000, 6),
399 Quantity::from_raw(1_000_000, 6),
400)]
401 #[case(
402 Price::from_raw(10_000_000, 6),
403 Price::from_raw(10_001_000, 6),
404 Quantity::from_raw(1_000_000, 6),
405 Quantity::from_raw(1_000_000, 7), )]
407 fn test_quote_tick_py_new_invalid_precisions(
408 #[case] bid_price: Price,
409 #[case] ask_price: Price,
410 #[case] bid_size: Quantity,
411 #[case] ask_size: Quantity,
412 ) {
413 pyo3::prepare_freethreaded_python();
414
415 let instrument_id = InstrumentId::from("ETH-USDT-SWAP.OKX");
416 let ts_event = 0;
417 let ts_init = 1;
418
419 let result = QuoteTick::py_new(
420 instrument_id,
421 bid_price,
422 ask_price,
423 bid_size,
424 ask_size,
425 ts_event,
426 ts_init,
427 );
428
429 assert!(result.is_err());
430 }
431
432 #[rstest]
433 fn test_to_dict(quote_ethusdt_binance: QuoteTick) {
434 pyo3::prepare_freethreaded_python();
435 let quote = quote_ethusdt_binance;
436
437 Python::with_gil(|py| {
438 let dict_string = quote.py_to_dict(py).unwrap().to_string();
439 let expected_string = r"{'type': 'QuoteTick', 'instrument_id': 'ETHUSDT-PERP.BINANCE', 'bid_price': '10000.0000', 'ask_price': '10001.0000', 'bid_size': '1.00000000', 'ask_size': '1.00000000', 'ts_event': 0, 'ts_init': 1}";
440 assert_eq!(dict_string, expected_string);
441 });
442 }
443
444 #[rstest]
445 fn test_from_dict(quote_ethusdt_binance: QuoteTick) {
446 pyo3::prepare_freethreaded_python();
447 let quote = quote_ethusdt_binance;
448
449 Python::with_gil(|py| {
450 let dict = quote.py_to_dict(py).unwrap();
451 let parsed = QuoteTick::py_from_dict(py, dict).unwrap();
452 assert_eq!(parsed, quote);
453 });
454 }
455
456 #[rstest]
457 fn test_from_pyobject(quote_ethusdt_binance: QuoteTick) {
458 pyo3::prepare_freethreaded_python();
459 let quote = quote_ethusdt_binance;
460
461 Python::with_gil(|py| {
462 let tick_pyobject = quote.into_py_any_unwrap(py);
463 let parsed_tick = QuoteTick::from_pyobject(tick_pyobject.bind(py)).unwrap();
464 assert_eq!(parsed_tick, quote);
465 });
466 }
467}