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