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 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 crate::{
42 data::TradeTick,
43 enums::{AggressorSide, FromU8},
44 identifiers::{InstrumentId, TradeId},
45 python::common::PY_MODULE_MODEL,
46 types::{
47 price::{Price, PriceRaw},
48 quantity::{Quantity, QuantityRaw},
49 },
50};
51
52#[pymethods]
53#[pyo3_stub_gen::derive::gen_stub_pymethods]
54impl TradeTick {
55 #[new]
57 fn py_new(
58 instrument_id: InstrumentId,
59 price: Price,
60 size: Quantity,
61 aggressor_side: AggressorSide,
62 trade_id: TradeId,
63 ts_event: u64,
64 ts_init: u64,
65 ) -> PyResult<Self> {
66 Self::new_checked(
67 instrument_id,
68 price,
69 size,
70 aggressor_side,
71 trade_id,
72 ts_event.into(),
73 ts_init.into(),
74 )
75 .map_err(to_pyvalue_err)
76 }
77
78 fn __setstate__(&mut self, state: &Bound<'_, PyAny>) -> PyResult<()> {
79 let py_tuple: &Bound<'_, PyTuple> = state.cast::<PyTuple>()?;
80 let binding = py_tuple.get_item(0)?;
81 let instrument_id_str = binding.cast::<PyString>()?.extract::<&str>()?;
82 let price_raw = py_tuple
83 .get_item(1)?
84 .cast::<PyInt>()?
85 .extract::<PriceRaw>()?;
86 let price_prec = py_tuple.get_item(2)?.cast::<PyInt>()?.extract::<u8>()?;
87 let size_raw = py_tuple
88 .get_item(3)?
89 .cast::<PyInt>()?
90 .extract::<QuantityRaw>()?;
91 let size_prec = py_tuple.get_item(4)?.cast::<PyInt>()?.extract::<u8>()?;
92
93 let aggressor_side_u8 = py_tuple.get_item(5)?.cast::<PyInt>()?.extract::<u8>()?;
94 let binding = py_tuple.get_item(6)?;
95 let trade_id_str = binding.cast::<PyString>()?.extract::<&str>()?;
96 let ts_event = py_tuple.get_item(7)?.cast::<PyInt>()?.extract::<u64>()?;
97 let ts_init = py_tuple.get_item(8)?.cast::<PyInt>()?.extract::<u64>()?;
98
99 self.instrument_id = InstrumentId::from_str(instrument_id_str).map_err(to_pyvalue_err)?;
100 self.price = Price::from_raw(price_raw, price_prec);
101 self.size = Quantity::from_raw(size_raw, size_prec);
102 self.aggressor_side = AggressorSide::from_u8(aggressor_side_u8).ok_or_else(|| {
103 to_pyvalue_err(format!("Invalid aggressor_side value: {aggressor_side_u8}"))
104 })?;
105 self.trade_id = TradeId::from(trade_id_str);
106 self.ts_event = ts_event.into();
107 self.ts_init = ts_init.into();
108
109 Ok(())
110 }
111
112 fn __getstate__(&self, py: Python) -> PyResult<Py<PyAny>> {
113 (
114 self.instrument_id.to_string(),
115 self.price.raw,
116 self.price.precision,
117 self.size.raw,
118 self.size.precision,
119 self.aggressor_side as u8,
120 self.trade_id.to_string(),
121 self.ts_event.as_u64(),
122 self.ts_init.as_u64(),
123 )
124 .into_py_any(py)
125 }
126
127 fn __reduce__(&self, py: Python) -> PyResult<Py<PyAny>> {
128 let safe_constructor = py.get_type::<Self>().getattr("_safe_constructor")?;
129 let state = self.__getstate__(py)?;
130 (safe_constructor, PyTuple::empty(py), state).into_py_any(py)
131 }
132
133 #[staticmethod]
134 fn _safe_constructor() -> Self {
135 Self::new(
136 InstrumentId::from("NULL.NULL"),
137 Price::zero(0),
138 Quantity::from(1), AggressorSide::NoAggressor,
140 TradeId::from("NULL"),
141 UnixNanos::default(),
142 UnixNanos::default(),
143 )
144 }
145
146 fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py<PyAny> {
147 match op {
148 CompareOp::Eq => self.eq(other).into_py_any_unwrap(py),
149 CompareOp::Ne => self.ne(other).into_py_any_unwrap(py),
150 _ => py.NotImplemented(),
151 }
152 }
153
154 fn __hash__(&self) -> isize {
155 let mut h = DefaultHasher::new();
156 self.hash(&mut h);
157 h.finish() as isize
158 }
159
160 fn __repr__(&self) -> String {
161 format!("{}({})", stringify!(TradeTick), self)
162 }
163
164 fn __str__(&self) -> String {
165 self.to_string()
166 }
167
168 #[getter]
169 #[pyo3(name = "instrument_id")]
170 fn py_instrument_id(&self) -> InstrumentId {
171 self.instrument_id
172 }
173
174 #[getter]
175 #[pyo3(name = "price")]
176 fn py_price(&self) -> Price {
177 self.price
178 }
179
180 #[getter]
181 #[pyo3(name = "size")]
182 fn py_size(&self) -> Quantity {
183 self.size
184 }
185
186 #[getter]
187 #[pyo3(name = "aggressor_side")]
188 fn py_aggressor_side(&self) -> AggressorSide {
189 self.aggressor_side
190 }
191
192 #[getter]
193 #[pyo3(name = "trade_id")]
194 fn py_trade_id(&self) -> TradeId {
195 self.trade_id
196 }
197
198 #[getter]
199 #[pyo3(name = "ts_event")]
200 fn py_ts_event(&self) -> u64 {
201 self.ts_event.as_u64()
202 }
203
204 #[getter]
205 #[pyo3(name = "ts_init")]
206 fn py_ts_init(&self) -> u64 {
207 self.ts_init.as_u64()
208 }
209
210 #[staticmethod]
211 #[pyo3(name = "fully_qualified_name")]
212 fn py_fully_qualified_name() -> String {
213 format!("{}:{}", PY_MODULE_MODEL, stringify!(TradeTick))
214 }
215
216 #[staticmethod]
218 #[pyo3(name = "get_metadata")]
219 fn py_get_metadata(
220 instrument_id: &InstrumentId,
221 price_precision: u8,
222 size_precision: u8,
223 ) -> HashMap<String, String> {
224 Self::get_metadata(instrument_id, price_precision, size_precision)
225 }
226
227 #[staticmethod]
229 #[pyo3(name = "get_fields")]
230 fn py_get_fields(py: Python<'_>) -> PyResult<Bound<'_, PyDict>> {
231 let py_dict = PyDict::new(py);
232 for (k, v) in Self::get_fields() {
233 py_dict.set_item(k, v)?;
234 }
235
236 Ok(py_dict)
237 }
238
239 #[staticmethod]
240 #[pyo3(name = "from_raw")]
241 #[expect(clippy::too_many_arguments)]
242 fn py_from_raw(
243 instrument_id: InstrumentId,
244 price_raw: PriceRaw,
245 price_prec: u8,
246 size_raw: QuantityRaw,
247 size_prec: u8,
248 aggressor_side: AggressorSide,
249 trade_id: TradeId,
250 ts_event: u64,
251 ts_init: u64,
252 ) -> PyResult<Self> {
253 Self::new_checked(
254 instrument_id,
255 Price::from_raw(price_raw, price_prec),
256 Quantity::from_raw(size_raw, size_prec),
257 aggressor_side,
258 trade_id,
259 ts_event.into(),
260 ts_init.into(),
261 )
262 .map_err(to_pyvalue_err)
263 }
264
265 #[staticmethod]
267 #[pyo3(name = "from_dict")]
268 fn py_from_dict(py: Python<'_>, values: Py<PyDict>) -> PyResult<Self> {
269 from_dict_pyo3(py, values)
270 }
271
272 #[pyo3(name = "to_dict")]
274 fn py_to_dict(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
275 to_dict_pyo3(py, self)
276 }
277
278 #[pyo3(name = "to_json_bytes")]
280 fn py_to_json_bytes(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
281 self.to_json_bytes()
282 .map_err(to_pyvalue_err)?
283 .into_py_any(py)
284 }
285
286 #[pyo3(name = "to_msgpack_bytes")]
288 fn py_to_msgpack_bytes(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
289 self.to_msgpack_bytes()
290 .map_err(to_pyvalue_err)?
291 .into_py_any(py)
292 }
293}
294
295#[pymethods]
296impl TradeTick {
297 #[staticmethod]
298 #[pyo3(name = "from_json")]
299 fn py_from_json(data: &[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: &[u8]) -> PyResult<Self> {
306 Self::from_msgpack_bytes(data).map_err(to_pyvalue_err)
307 }
308}
309
310#[cfg(test)]
311mod tests {
312 use pyo3::Python;
313 use rstest::rstest;
314
315 use crate::{
316 data::{TradeTick, stubs::stub_trade_ethusdt_buy},
317 enums::AggressorSide,
318 identifiers::{InstrumentId, TradeId},
319 types::{Price, Quantity},
320 };
321
322 #[rstest]
323 fn test_trade_tick_py_new_with_zero_size() {
324 let instrument_id = InstrumentId::from("ETH-USDT-SWAP.OKX");
325 let price = Price::from("10000.00");
326 let zero_size = Quantity::from(0);
327 let aggressor_side = AggressorSide::Buy;
328 let trade_id = TradeId::from("123456789");
329 let ts_event = 1;
330 let ts_init = 2;
331
332 let result = TradeTick::py_new(
333 instrument_id,
334 price,
335 zero_size,
336 aggressor_side,
337 trade_id,
338 ts_event,
339 ts_init,
340 );
341
342 assert!(result.is_err());
343 }
344
345 #[rstest]
346 fn test_to_dict(stub_trade_ethusdt_buy: TradeTick) {
347 let trade = stub_trade_ethusdt_buy;
348
349 Python::initialize();
350 Python::attach(|py| {
351 let dict_string = trade.py_to_dict(py).unwrap().to_string();
352 let expected_string = "{'type': 'TradeTick', 'instrument_id': 'ETHUSDT-PERP.BINANCE', 'price': '10000.0000', 'size': '1.00000000', 'aggressor_side': 'BUY', 'trade_id': '123456789', 'ts_event': 0, 'ts_init': 1}";
353 assert_eq!(dict_string, expected_string);
354 });
355 }
356
357 #[rstest]
358 fn test_from_dict(stub_trade_ethusdt_buy: TradeTick) {
359 let trade = stub_trade_ethusdt_buy;
360
361 Python::initialize();
362 Python::attach(|py| {
363 let dict = trade.py_to_dict(py).unwrap();
364 let parsed = TradeTick::py_from_dict(py, dict).unwrap();
365 assert_eq!(parsed, trade);
366 });
367 }
368}