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]
102#[pyo3_stub_gen::derive::gen_stub_pymethods]
103impl QuoteTick {
104 #[new]
106 fn py_new(
107 instrument_id: InstrumentId,
108 bid_price: Price,
109 ask_price: Price,
110 bid_size: Quantity,
111 ask_size: Quantity,
112 ts_event: u64,
113 ts_init: u64,
114 ) -> PyResult<Self> {
115 Self::new_checked(
116 instrument_id,
117 bid_price,
118 ask_price,
119 bid_size,
120 ask_size,
121 ts_event.into(),
122 ts_init.into(),
123 )
124 .map_err(to_pyvalue_err)
125 }
126
127 fn __setstate__(&mut self, state: &Bound<'_, PyAny>) -> PyResult<()> {
128 let py_tuple: &Bound<'_, PyTuple> = state.cast::<PyTuple>()?;
129 let binding = py_tuple.get_item(0)?;
130 let instrument_id_str: &str = binding.cast::<PyString>()?.extract()?;
131 let bid_price_raw: PriceRaw = py_tuple.get_item(1)?.cast::<PyInt>()?.extract()?;
132 let ask_price_raw: PriceRaw = py_tuple.get_item(2)?.cast::<PyInt>()?.extract()?;
133 let bid_price_prec: u8 = py_tuple.get_item(3)?.cast::<PyInt>()?.extract()?;
134 let ask_price_prec: u8 = py_tuple.get_item(4)?.cast::<PyInt>()?.extract()?;
135
136 let bid_size_raw: QuantityRaw = py_tuple.get_item(5)?.cast::<PyInt>()?.extract()?;
137 let ask_size_raw: QuantityRaw = py_tuple.get_item(6)?.cast::<PyInt>()?.extract()?;
138 let bid_size_prec: u8 = py_tuple.get_item(7)?.cast::<PyInt>()?.extract()?;
139 let ask_size_prec: u8 = py_tuple.get_item(8)?.cast::<PyInt>()?.extract()?;
140 let ts_event: u64 = py_tuple.get_item(9)?.cast::<PyInt>()?.extract()?;
141 let ts_init: u64 = py_tuple.get_item(10)?.cast::<PyInt>()?.extract()?;
142
143 self.instrument_id = InstrumentId::from_str(instrument_id_str).map_err(to_pyvalue_err)?;
144 self.bid_price = Price::from_raw(bid_price_raw, bid_price_prec);
145 self.ask_price = Price::from_raw(ask_price_raw, ask_price_prec);
146 self.bid_size = Quantity::from_raw(bid_size_raw, bid_size_prec);
147 self.ask_size = Quantity::from_raw(ask_size_raw, ask_size_prec);
148 self.ts_event = ts_event.into();
149 self.ts_init = ts_init.into();
150
151 Ok(())
152 }
153
154 fn __getstate__(&self, py: Python) -> PyResult<Py<PyAny>> {
155 (
156 self.instrument_id.to_string(),
157 self.bid_price.raw,
158 self.ask_price.raw,
159 self.bid_price.precision,
160 self.ask_price.precision,
161 self.bid_size.raw,
162 self.ask_size.raw,
163 self.bid_size.precision,
164 self.ask_size.precision,
165 self.ts_event.as_u64(),
166 self.ts_init.as_u64(),
167 )
168 .into_py_any(py)
169 }
170
171 fn __reduce__(&self, py: Python) -> PyResult<Py<PyAny>> {
172 let safe_constructor = py.get_type::<Self>().getattr("_safe_constructor")?;
173 let state = self.__getstate__(py)?;
174 (safe_constructor, PyTuple::empty(py), state).into_py_any(py)
175 }
176
177 #[staticmethod]
178 fn _safe_constructor() -> PyResult<Self> {
179 Self::new_checked(
180 InstrumentId::from("NULL.NULL"),
181 Price::zero(0),
182 Price::zero(0),
183 Quantity::zero(0),
184 Quantity::zero(0),
185 UnixNanos::default(),
186 UnixNanos::default(),
187 )
188 .map_err(to_pyvalue_err)
189 }
190
191 fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py<PyAny> {
192 match op {
193 CompareOp::Eq => self.eq(other).into_py_any_unwrap(py),
194 CompareOp::Ne => self.ne(other).into_py_any_unwrap(py),
195 _ => py.NotImplemented(),
196 }
197 }
198
199 fn __hash__(&self) -> isize {
200 let mut h = DefaultHasher::new();
201 self.hash(&mut h);
202 h.finish() as isize
203 }
204
205 fn __repr__(&self) -> String {
206 format!("{}({})", stringify!(QuoteTick), self)
207 }
208
209 fn __str__(&self) -> String {
210 self.to_string()
211 }
212
213 #[getter]
214 #[pyo3(name = "instrument_id")]
215 fn py_instrument_id(&self) -> InstrumentId {
216 self.instrument_id
217 }
218
219 #[getter]
220 #[pyo3(name = "bid_price")]
221 fn py_bid_price(&self) -> Price {
222 self.bid_price
223 }
224
225 #[getter]
226 #[pyo3(name = "ask_price")]
227 fn py_ask_price(&self) -> Price {
228 self.ask_price
229 }
230
231 #[getter]
232 #[pyo3(name = "bid_size")]
233 fn py_bid_size(&self) -> Quantity {
234 self.bid_size
235 }
236
237 #[getter]
238 #[pyo3(name = "ask_size")]
239 fn py_ask_size(&self) -> Quantity {
240 self.ask_size
241 }
242
243 #[getter]
244 #[pyo3(name = "ts_event")]
245 fn py_ts_event(&self) -> u64 {
246 self.ts_event.as_u64()
247 }
248
249 #[getter]
250 #[pyo3(name = "ts_init")]
251 fn py_ts_init(&self) -> u64 {
252 self.ts_init.as_u64()
253 }
254
255 #[staticmethod]
256 #[pyo3(name = "fully_qualified_name")]
257 fn py_fully_qualified_name() -> String {
258 format!("{}:{}", PY_MODULE_MODEL, stringify!(QuoteTick))
259 }
260
261 #[staticmethod]
263 #[pyo3(name = "get_metadata")]
264 fn py_get_metadata(
265 instrument_id: &InstrumentId,
266 price_precision: u8,
267 size_precision: u8,
268 ) -> HashMap<String, String> {
269 Self::get_metadata(instrument_id, price_precision, size_precision)
270 }
271
272 #[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 #[expect(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 #[pyo3(name = "extract_price")]
321 fn py_extract_price(&self, price_type: PriceType) -> Price {
322 self.extract_price(price_type)
323 }
324
325 #[pyo3(name = "extract_size")]
327 fn py_extract_size(&self, price_type: PriceType) -> Quantity {
328 self.extract_size(price_type)
329 }
330
331 #[pyo3(name = "as_pycapsule")]
347 fn py_as_pycapsule(&self, py: Python<'_>) -> Py<PyAny> {
348 data_to_pycapsule(py, Data::Quote(*self))
349 }
350
351 #[pyo3(name = "to_dict")]
353 fn py_to_dict(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
354 to_dict_pyo3(py, self)
355 }
356
357 #[pyo3(name = "to_json_bytes")]
359 fn py_to_json_bytes(&self, py: Python<'_>) -> Py<PyAny> {
360 self.to_json_bytes().unwrap().into_py_any_unwrap(py)
361 }
362
363 #[pyo3(name = "to_msgpack_bytes")]
365 fn py_to_msgpack_bytes(&self, py: Python<'_>) -> Py<PyAny> {
366 self.to_msgpack_bytes().unwrap().into_py_any_unwrap(py)
367 }
368}
369
370#[pymethods]
371impl QuoteTick {
372 #[staticmethod]
373 #[pyo3(name = "from_json")]
374 fn py_from_json(data: &[u8]) -> PyResult<Self> {
375 Self::from_json_bytes(data).map_err(to_pyvalue_err)
376 }
377
378 #[staticmethod]
379 #[pyo3(name = "from_msgpack")]
380 fn py_from_msgpack(data: &[u8]) -> PyResult<Self> {
381 Self::from_msgpack_bytes(data).map_err(to_pyvalue_err)
382 }
383}
384
385#[cfg(test)]
386mod tests {
387 use nautilus_core::python::IntoPyObjectNautilusExt;
388 use pyo3::Python;
389 use rstest::rstest;
390
391 use crate::{
392 data::{QuoteTick, stubs::quote_ethusdt_binance},
393 identifiers::InstrumentId,
394 types::{Price, Quantity},
395 };
396
397 #[rstest]
398 #[case(
399 Price::new(0.010_000, 6),
400 Price::new(0.010_001_0, 7), Quantity::new(0.001_000, 6),
402 Quantity::new(0.001_000, 6),
403)]
404 #[case(
405 Price::new(0.010_000, 6),
406 Price::new(0.010_001, 6),
407 Quantity::new(0.001_000, 6),
408 Quantity::new(0.001_000_0, 7), )]
410 fn test_quote_tick_py_new_invalid_precisions(
411 #[case] bid_price: Price,
412 #[case] ask_price: Price,
413 #[case] bid_size: Quantity,
414 #[case] ask_size: Quantity,
415 ) {
416 let instrument_id = InstrumentId::from("ETH-USDT-SWAP.OKX");
417 let ts_event = 0;
418 let ts_init = 1;
419
420 let result = QuoteTick::py_new(
421 instrument_id,
422 bid_price,
423 ask_price,
424 bid_size,
425 ask_size,
426 ts_event,
427 ts_init,
428 );
429
430 assert!(result.is_err());
431 }
432
433 #[rstest]
434 fn test_to_dict(quote_ethusdt_binance: QuoteTick) {
435 let quote = quote_ethusdt_binance;
436
437 Python::initialize();
438 Python::attach(|py| {
439 let dict_string = quote.py_to_dict(py).unwrap().to_string();
440 let expected_string = "{'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}";
441 assert_eq!(dict_string, expected_string);
442 });
443 }
444
445 #[rstest]
446 fn test_from_dict(quote_ethusdt_binance: QuoteTick) {
447 let quote = quote_ethusdt_binance;
448
449 Python::initialize();
450 Python::attach(|py| {
451 let dict = quote.py_to_dict(py).unwrap();
452 let parsed = QuoteTick::py_from_dict(py, dict).unwrap();
453 assert_eq!(parsed, quote);
454 });
455 }
456
457 #[rstest]
458 fn test_from_pyobject(quote_ethusdt_binance: QuoteTick) {
459 let quote = quote_ethusdt_binance;
460
461 Python::initialize();
462 Python::attach(|py| {
463 let tick_pyobject = quote.into_py_any_unwrap(py);
464 let parsed_tick = QuoteTick::from_pyobject(tick_pyobject.bind(py)).unwrap();
465 assert_eq!(parsed_tick, quote);
466 });
467 }
468}