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