nautilus_model/python/data/
trade.rs1use std::{
17 collections::{hash_map::DefaultHasher, HashMap},
18 hash::{Hash, Hasher},
19 str::FromStr,
20};
21
22use nautilus_core::{
23 python::{
24 serialization::{from_dict_pyo3, to_dict_pyo3},
25 to_pyvalue_err,
26 },
27 serialization::Serializable,
28 UnixNanos,
29};
30use pyo3::{
31 prelude::*,
32 pyclass::CompareOp,
33 types::{PyDict, PyLong, PyString, PyTuple},
34};
35
36use super::data_to_pycapsule;
37use crate::{
38 data::{Data, TradeTick},
39 enums::{AggressorSide, FromU8},
40 identifiers::{InstrumentId, TradeId},
41 python::common::PY_MODULE_MODEL,
42 types::{
43 price::{Price, PriceRaw},
44 quantity::{Quantity, QuantityRaw},
45 },
46};
47
48impl TradeTick {
49 pub fn from_pyobject(obj: &Bound<'_, PyAny>) -> PyResult<Self> {
51 let instrument_id_obj: Bound<'_, PyAny> = obj.getattr("instrument_id")?.extract()?;
52 let instrument_id_str: String = instrument_id_obj.getattr("value")?.extract()?;
53 let instrument_id =
54 InstrumentId::from_str(instrument_id_str.as_str()).map_err(to_pyvalue_err)?;
55
56 let price_py: Bound<'_, PyAny> = obj.getattr("price")?.extract()?;
57 let price_raw: PriceRaw = price_py.getattr("raw")?.extract()?;
58 let price_prec: u8 = price_py.getattr("precision")?.extract()?;
59 let price = Price::from_raw(price_raw, price_prec);
60
61 let size_py: Bound<'_, PyAny> = obj.getattr("size")?.extract()?;
62 let size_raw: QuantityRaw = size_py.getattr("raw")?.extract()?;
63 let size_prec: u8 = size_py.getattr("precision")?.extract()?;
64 let size = Quantity::from_raw(size_raw, size_prec);
65
66 let aggressor_side_obj: Bound<'_, PyAny> = obj.getattr("aggressor_side")?.extract()?;
67 let aggressor_side_u8 = aggressor_side_obj.getattr("value")?.extract()?;
68 let aggressor_side = AggressorSide::from_u8(aggressor_side_u8).unwrap();
69
70 let trade_id_obj: Bound<'_, PyAny> = obj.getattr("trade_id")?.extract()?;
71 let trade_id_str: String = trade_id_obj.getattr("value")?.extract()?;
72 let trade_id = TradeId::from(trade_id_str.as_str());
73
74 let ts_event: u64 = obj.getattr("ts_event")?.extract()?;
75 let ts_init: u64 = obj.getattr("ts_init")?.extract()?;
76
77 Ok(Self::new(
78 instrument_id,
79 price,
80 size,
81 aggressor_side,
82 trade_id,
83 ts_event.into(),
84 ts_init.into(),
85 ))
86 }
87}
88
89#[pymethods]
90impl TradeTick {
91 #[new]
92 fn py_new(
93 instrument_id: InstrumentId,
94 price: Price,
95 size: Quantity,
96 aggressor_side: AggressorSide,
97 trade_id: TradeId,
98 ts_event: u64,
99 ts_init: u64,
100 ) -> PyResult<Self> {
101 Self::new_checked(
102 instrument_id,
103 price,
104 size,
105 aggressor_side,
106 trade_id,
107 ts_event.into(),
108 ts_init.into(),
109 )
110 .map_err(to_pyvalue_err)
111 }
112
113 fn __setstate__(&mut self, state: &Bound<'_, PyAny>) -> PyResult<()> {
114 let py_tuple: &Bound<'_, PyTuple> = state.downcast::<PyTuple>()?;
115 let binding = py_tuple.get_item(0)?;
116 let instrument_id_str = binding.downcast::<PyString>()?.extract::<&str>()?;
117 let price_raw = py_tuple
118 .get_item(1)?
119 .downcast::<PyLong>()?
120 .extract::<PriceRaw>()?;
121 let price_prec = py_tuple
122 .get_item(2)?
123 .downcast::<PyLong>()?
124 .extract::<u8>()?;
125 let size_raw = py_tuple
126 .get_item(3)?
127 .downcast::<PyLong>()?
128 .extract::<QuantityRaw>()?;
129 let size_prec = py_tuple
130 .get_item(4)?
131 .downcast::<PyLong>()?
132 .extract::<u8>()?;
133
134 let aggressor_side_u8 = py_tuple
135 .get_item(5)?
136 .downcast::<PyLong>()?
137 .extract::<u8>()?;
138 let binding = py_tuple.get_item(6)?;
139 let trade_id_str = binding.downcast::<PyString>()?.extract::<&str>()?;
140 let ts_event = py_tuple
141 .get_item(7)?
142 .downcast::<PyLong>()?
143 .extract::<u64>()?;
144 let ts_init = py_tuple
145 .get_item(8)?
146 .downcast::<PyLong>()?
147 .extract::<u64>()?;
148
149 self.instrument_id = InstrumentId::from_str(instrument_id_str).map_err(to_pyvalue_err)?;
150 self.price = Price::from_raw(price_raw, price_prec);
151 self.size = Quantity::from_raw(size_raw, size_prec);
152 self.aggressor_side = AggressorSide::from_u8(aggressor_side_u8).unwrap();
153 self.trade_id = TradeId::from(trade_id_str);
154 self.ts_event = ts_event.into();
155 self.ts_init = ts_init.into();
156
157 Ok(())
158 }
159
160 fn __getstate__(&self, py: Python) -> PyResult<PyObject> {
161 Ok((
162 self.instrument_id.to_string(),
163 self.price.raw,
164 self.price.precision,
165 self.size.raw,
166 self.size.precision,
167 self.aggressor_side as u8,
168 self.trade_id.to_string(),
169 self.ts_event.as_u64(),
170 self.ts_init.as_u64(),
171 )
172 .to_object(py))
173 }
174
175 fn __reduce__(&self, py: Python) -> PyResult<PyObject> {
176 let safe_constructor = py.get_type::<Self>().getattr("_safe_constructor")?;
177 let state = self.__getstate__(py)?;
178 Ok((safe_constructor, PyTuple::empty(py), state).to_object(py))
179 }
180
181 #[staticmethod]
182 fn _safe_constructor() -> Self {
183 Self::new(
184 InstrumentId::from("NULL.NULL"),
185 Price::zero(0),
186 Quantity::from(1), AggressorSide::NoAggressor,
188 TradeId::from("NULL"),
189 UnixNanos::default(),
190 UnixNanos::default(),
191 )
192 }
193
194 fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py<PyAny> {
195 match op {
196 CompareOp::Eq => self.eq(other).into_py(py),
197 CompareOp::Ne => self.ne(other).into_py(py),
198 _ => py.NotImplemented(),
199 }
200 }
201
202 fn __hash__(&self) -> isize {
203 let mut h = DefaultHasher::new();
204 self.hash(&mut h);
205 h.finish() as isize
206 }
207
208 fn __repr__(&self) -> String {
209 format!("{}({})", stringify!(TradeTick), self)
210 }
211
212 fn __str__(&self) -> String {
213 self.to_string()
214 }
215
216 #[getter]
217 #[pyo3(name = "instrument_id")]
218 fn py_instrument_id(&self) -> InstrumentId {
219 self.instrument_id
220 }
221
222 #[getter]
223 #[pyo3(name = "price")]
224 fn py_price(&self) -> Price {
225 self.price
226 }
227
228 #[getter]
229 #[pyo3(name = "size")]
230 fn py_size(&self) -> Quantity {
231 self.size
232 }
233
234 #[getter]
235 #[pyo3(name = "aggressor_side")]
236 fn py_aggressor_side(&self) -> AggressorSide {
237 self.aggressor_side
238 }
239
240 #[getter]
241 #[pyo3(name = "trade_id")]
242 fn py_trade_id(&self) -> TradeId {
243 self.trade_id
244 }
245
246 #[getter]
247 #[pyo3(name = "ts_event")]
248 fn py_ts_event(&self) -> u64 {
249 self.ts_event.as_u64()
250 }
251
252 #[getter]
253 #[pyo3(name = "ts_init")]
254 fn py_ts_init(&self) -> u64 {
255 self.ts_init.as_u64()
256 }
257
258 #[staticmethod]
259 #[pyo3(name = "fully_qualified_name")]
260 fn py_fully_qualified_name() -> String {
261 format!("{}:{}", PY_MODULE_MODEL, stringify!(TradeTick))
262 }
263
264 #[staticmethod]
265 #[pyo3(name = "get_metadata")]
266 fn py_get_metadata(
267 instrument_id: &InstrumentId,
268 price_precision: u8,
269 size_precision: u8,
270 ) -> PyResult<HashMap<String, String>> {
271 Ok(Self::get_metadata(
272 instrument_id,
273 price_precision,
274 size_precision,
275 ))
276 }
277
278 #[staticmethod]
279 #[pyo3(name = "get_fields")]
280 fn py_get_fields(py: Python<'_>) -> PyResult<Bound<'_, PyDict>> {
281 let py_dict = PyDict::new(py);
282 for (k, v) in Self::get_fields() {
283 py_dict.set_item(k, v)?;
284 }
285
286 Ok(py_dict)
287 }
288
289 #[staticmethod]
291 #[pyo3(name = "from_dict")]
292 fn py_from_dict(py: Python<'_>, values: Py<PyDict>) -> PyResult<Self> {
293 from_dict_pyo3(py, values)
294 }
295
296 #[staticmethod]
297 #[pyo3(name = "from_json")]
298 fn py_from_json(data: Vec<u8>) -> PyResult<Self> {
299 Self::from_json_bytes(&data).map_err(to_pyvalue_err)
300 }
301
302 #[staticmethod]
303 #[pyo3(name = "from_msgpack")]
304 fn py_from_msgpack(data: Vec<u8>) -> PyResult<Self> {
305 Self::from_msgpack_bytes(&data).map_err(to_pyvalue_err)
306 }
307
308 #[pyo3(name = "as_pycapsule")]
324 fn py_as_pycapsule(&self, py: Python<'_>) -> PyObject {
325 data_to_pycapsule(py, Data::Trade(*self))
326 }
327
328 #[pyo3(name = "as_dict")]
330 fn py_as_dict(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
331 to_dict_pyo3(py, self)
332 }
333
334 #[pyo3(name = "as_json")]
336 fn py_as_json(&self, py: Python<'_>) -> Py<PyAny> {
337 self.as_json_bytes().unwrap().into_py(py)
339 }
340
341 #[pyo3(name = "as_msgpack")]
343 fn py_as_msgpack(&self, py: Python<'_>) -> Py<PyAny> {
344 self.as_msgpack_bytes().unwrap().into_py(py)
346 }
347}
348
349#[cfg(test)]
353mod tests {
354 use pyo3::{IntoPy, Python};
355 use rstest::rstest;
356
357 use crate::{
358 data::{stubs::stub_trade_ethusdt_buyer, TradeTick},
359 enums::AggressorSide,
360 identifiers::{InstrumentId, TradeId},
361 types::{Price, Quantity},
362 };
363
364 #[rstest]
365 fn test_trade_tick_py_new_with_zero_size() {
366 pyo3::prepare_freethreaded_python();
367
368 let instrument_id = InstrumentId::from("ETH-USDT-SWAP.OKX");
369 let price = Price::from("10000.00");
370 let zero_size = Quantity::from(0);
371 let aggressor_side = AggressorSide::Buyer;
372 let trade_id = TradeId::from("123456789");
373 let ts_event = 1;
374 let ts_init = 2;
375
376 let result = TradeTick::py_new(
377 instrument_id,
378 price,
379 zero_size,
380 aggressor_side,
381 trade_id,
382 ts_event,
383 ts_init,
384 );
385
386 assert!(result.is_err());
387 }
388
389 #[rstest]
390 fn test_as_dict(stub_trade_ethusdt_buyer: TradeTick) {
391 pyo3::prepare_freethreaded_python();
392 let trade = stub_trade_ethusdt_buyer;
393
394 Python::with_gil(|py| {
395 let dict_string = trade.py_as_dict(py).unwrap().to_string();
396 let expected_string = r"{'type': 'TradeTick', 'instrument_id': 'ETHUSDT-PERP.BINANCE', 'price': '10000.0000', 'size': '1.00000000', 'aggressor_side': 'BUYER', 'trade_id': '123456789', 'ts_event': 0, 'ts_init': 1}";
397 assert_eq!(dict_string, expected_string);
398 });
399 }
400
401 #[rstest]
402 fn test_from_dict(stub_trade_ethusdt_buyer: TradeTick) {
403 pyo3::prepare_freethreaded_python();
404 let trade = stub_trade_ethusdt_buyer;
405
406 Python::with_gil(|py| {
407 let dict = trade.py_as_dict(py).unwrap();
408 let parsed = TradeTick::py_from_dict(py, dict).unwrap();
409 assert_eq!(parsed, trade);
410 });
411 }
412
413 #[rstest]
414 fn test_from_pyobject(stub_trade_ethusdt_buyer: TradeTick) {
415 pyo3::prepare_freethreaded_python();
416 let trade = stub_trade_ethusdt_buyer;
417
418 Python::with_gil(|py| {
419 let tick_pyobject = trade.into_py(py);
420 let parsed_tick = TradeTick::from_pyobject(tick_pyobject.bind(py)).unwrap();
421 assert_eq!(parsed_tick, trade);
422 });
423 }
424}