nautilus_model/python/data/
trade.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use 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    /// Create a new [`TradeTick`] extracted from the given [`PyAny`].
50    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), // size cannot be zero
187            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    /// Returns a new object from the given dictionary representation.
290    #[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    /// Creates a `PyCapsule` containing a raw pointer to a `Data::Trade` object.
309    ///
310    /// This function takes the current object (assumed to be of a type that can be represented as
311    /// `Data::Trade`), and encapsulates a raw pointer to it within a `PyCapsule`.
312    ///
313    /// # Safety
314    ///
315    /// This function is safe as long as the following conditions are met:
316    /// - The `Data::Trade` object pointed to by the capsule must remain valid for the lifetime of the capsule.
317    /// - The consumer of the capsule must ensure proper handling to avoid dereferencing a dangling pointer.
318    ///
319    /// # Panics
320    ///
321    /// The function will panic if the `PyCapsule` creation fails, which can occur if the
322    /// `Data::Trade` object cannot be converted into a raw pointer.
323    #[pyo3(name = "as_pycapsule")]
324    fn py_as_pycapsule(&self, py: Python<'_>) -> PyObject {
325        data_to_pycapsule(py, Data::Trade(*self))
326    }
327
328    /// Return a dictionary representation of the object.
329    #[pyo3(name = "as_dict")]
330    fn py_as_dict(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
331        to_dict_pyo3(py, self)
332    }
333
334    /// Return JSON encoded bytes representation of the object.
335    #[pyo3(name = "as_json")]
336    fn py_as_json(&self, py: Python<'_>) -> Py<PyAny> {
337        // SAFETY: Unwrap safe when serializing a valid object
338        self.as_json_bytes().unwrap().into_py(py)
339    }
340
341    /// Return MsgPack encoded bytes representation of the object.
342    #[pyo3(name = "as_msgpack")]
343    fn py_as_msgpack(&self, py: Python<'_>) -> Py<PyAny> {
344        // SAFETY: Unwrap safe when serializing a valid object
345        self.as_msgpack_bytes().unwrap().into_py(py)
346    }
347}
348
349////////////////////////////////////////////////////////////////////////////////
350// Tests
351////////////////////////////////////////////////////////////////////////////////
352#[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}