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