nautilus_model/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
16//! A `TradeTick` data type representing a single trade in a market.
17
18use std::{collections::HashMap, fmt::Display, hash::Hash};
19
20use derive_builder::Builder;
21use indexmap::IndexMap;
22use nautilus_core::{UnixNanos, correctness::FAILED, serialization::Serializable};
23use serde::{Deserialize, Serialize};
24
25use super::GetTsInit;
26use crate::{
27    enums::AggressorSide,
28    identifiers::{InstrumentId, TradeId},
29    types::{Price, Quantity, fixed::FIXED_SIZE_BINARY, quantity::check_positive_quantity},
30};
31
32/// Represents a trade tick in a market.
33#[repr(C)]
34#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Builder)]
35#[serde(tag = "type")]
36#[cfg_attr(
37    feature = "python",
38    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
39)]
40pub struct TradeTick {
41    /// The trade instrument ID.
42    pub instrument_id: InstrumentId,
43    /// The traded price.
44    pub price: Price,
45    /// The traded size.
46    pub size: Quantity,
47    /// The trade aggressor side.
48    pub aggressor_side: AggressorSide,
49    /// The trade match ID (assigned by the venue).
50    pub trade_id: TradeId,
51    /// UNIX timestamp (nanoseconds) when the trade event occurred.
52    pub ts_event: UnixNanos,
53    /// UNIX timestamp (nanoseconds) when the struct was initialized.
54    pub ts_init: UnixNanos,
55}
56
57impl TradeTick {
58    /// Creates a new [`TradeTick`] instance with correctness checking.
59    ///
60    /// # Errors
61    ///
62    /// This function returns an error:
63    /// - If `size` is not positive (> 0).
64    ///
65    /// # Notes
66    ///
67    /// PyO3 requires a `Result` type for proper error handling and stacktrace printing in Python.
68    pub fn new_checked(
69        instrument_id: InstrumentId,
70        price: Price,
71        size: Quantity,
72        aggressor_side: AggressorSide,
73        trade_id: TradeId,
74        ts_event: UnixNanos,
75        ts_init: UnixNanos,
76    ) -> anyhow::Result<Self> {
77        check_positive_quantity(size, stringify!(size))?;
78
79        Ok(Self {
80            instrument_id,
81            price,
82            size,
83            aggressor_side,
84            trade_id,
85            ts_event,
86            ts_init,
87        })
88    }
89
90    /// Creates a new [`TradeTick`] instance.
91    ///
92    /// # Panics
93    ///
94    /// This function panics:
95    /// - If `size` is not positive (> 0).
96    #[must_use]
97    pub fn new(
98        instrument_id: InstrumentId,
99        price: Price,
100        size: Quantity,
101        aggressor_side: AggressorSide,
102        trade_id: TradeId,
103        ts_event: UnixNanos,
104        ts_init: UnixNanos,
105    ) -> Self {
106        Self::new_checked(
107            instrument_id,
108            price,
109            size,
110            aggressor_side,
111            trade_id,
112            ts_event,
113            ts_init,
114        )
115        .expect(FAILED)
116    }
117
118    /// Returns the metadata for the type, for use with serialization formats.
119    #[must_use]
120    pub fn get_metadata(
121        instrument_id: &InstrumentId,
122        price_precision: u8,
123        size_precision: u8,
124    ) -> HashMap<String, String> {
125        let mut metadata = HashMap::new();
126        metadata.insert("instrument_id".to_string(), instrument_id.to_string());
127        metadata.insert("price_precision".to_string(), price_precision.to_string());
128        metadata.insert("size_precision".to_string(), size_precision.to_string());
129        metadata
130    }
131
132    /// Returns the field map for the type, for use with Arrow schemas.
133    #[must_use]
134    pub fn get_fields() -> IndexMap<String, String> {
135        let mut metadata = IndexMap::new();
136        metadata.insert("price".to_string(), FIXED_SIZE_BINARY.to_string());
137        metadata.insert("size".to_string(), FIXED_SIZE_BINARY.to_string());
138        metadata.insert("aggressor_side".to_string(), "UInt8".to_string());
139        metadata.insert("trade_id".to_string(), "Utf8".to_string());
140        metadata.insert("ts_event".to_string(), "UInt64".to_string());
141        metadata.insert("ts_init".to_string(), "UInt64".to_string());
142        metadata
143    }
144}
145
146impl Display for TradeTick {
147    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148        write!(
149            f,
150            "{},{},{},{},{},{}",
151            self.instrument_id,
152            self.price,
153            self.size,
154            self.aggressor_side,
155            self.trade_id,
156            self.ts_event,
157        )
158    }
159}
160
161impl Serializable for TradeTick {}
162
163impl GetTsInit for TradeTick {
164    fn ts_init(&self) -> UnixNanos {
165        self.ts_init
166    }
167}
168
169////////////////////////////////////////////////////////////////////////////////
170// Tests
171////////////////////////////////////////////////////////////////////////////////
172#[cfg(test)]
173mod tests {
174    use nautilus_core::{UnixNanos, serialization::Serializable};
175    use pyo3::{IntoPyObjectExt, Python};
176    use rstest::rstest;
177
178    use crate::{
179        data::{TradeTick, stubs::stub_trade_ethusdt_buyer},
180        enums::AggressorSide,
181        identifiers::{InstrumentId, TradeId},
182        types::{Price, Quantity},
183    };
184
185    #[cfg(feature = "high-precision")] // TODO: Add 64-bit precision version of test
186    #[rstest]
187    #[should_panic(expected = "invalid `Quantity` for 'size' not positive, was 0")]
188    fn test_trade_tick_new_with_zero_size_panics() {
189        let instrument_id = InstrumentId::from("ETH-USDT-SWAP.OKX");
190        let price = Price::from("10000.00");
191        let zero_size = Quantity::from(0);
192        let aggressor_side = AggressorSide::Buyer;
193        let trade_id = TradeId::from("123456789");
194        let ts_event = UnixNanos::from(0);
195        let ts_init = UnixNanos::from(1);
196
197        let _ = TradeTick::new(
198            instrument_id,
199            price,
200            zero_size,
201            aggressor_side,
202            trade_id,
203            ts_event,
204            ts_init,
205        );
206    }
207
208    #[rstest]
209    fn test_trade_tick_new_checked_with_zero_size_error() {
210        let instrument_id = InstrumentId::from("ETH-USDT-SWAP.OKX");
211        let price = Price::from("10000.00");
212        let zero_size = Quantity::from(0);
213        let aggressor_side = AggressorSide::Buyer;
214        let trade_id = TradeId::from("123456789");
215        let ts_event = UnixNanos::from(0);
216        let ts_init = UnixNanos::from(1);
217
218        let result = TradeTick::new_checked(
219            instrument_id,
220            price,
221            zero_size,
222            aggressor_side,
223            trade_id,
224            ts_event,
225            ts_init,
226        );
227
228        assert!(result.is_err());
229    }
230
231    #[rstest]
232    fn test_to_string(stub_trade_ethusdt_buyer: TradeTick) {
233        let trade = stub_trade_ethusdt_buyer;
234        assert_eq!(
235            trade.to_string(),
236            "ETHUSDT-PERP.BINANCE,10000.0000,1.00000000,BUYER,123456789,0"
237        );
238    }
239
240    #[rstest]
241    fn test_deserialize_raw_string() {
242        let raw_string = r#"{
243            "type": "TradeTick",
244            "instrument_id": "ETHUSDT-PERP.BINANCE",
245            "price": "10000.0000",
246            "size": "1.00000000",
247            "aggressor_side": "BUYER",
248            "trade_id": "123456789",
249            "ts_event": 0,
250            "ts_init": 1
251        }"#;
252
253        let trade: TradeTick = serde_json::from_str(raw_string).unwrap();
254
255        assert_eq!(trade.aggressor_side, AggressorSide::Buyer);
256    }
257
258    #[rstest]
259    fn test_from_pyobject(stub_trade_ethusdt_buyer: TradeTick) {
260        pyo3::prepare_freethreaded_python();
261        let trade = stub_trade_ethusdt_buyer;
262
263        Python::with_gil(|py| {
264            let tick_pyobject = trade.into_py_any(py).unwrap();
265            let parsed_tick = TradeTick::from_pyobject(tick_pyobject.bind(py)).unwrap();
266            assert_eq!(parsed_tick, trade);
267        });
268    }
269
270    #[rstest]
271    fn test_json_serialization(stub_trade_ethusdt_buyer: TradeTick) {
272        let trade = stub_trade_ethusdt_buyer;
273        let serialized = trade.as_json_bytes().unwrap();
274        let deserialized = TradeTick::from_json_bytes(serialized.as_ref()).unwrap();
275        assert_eq!(deserialized, trade);
276    }
277
278    #[rstest]
279    fn test_msgpack_serialization(stub_trade_ethusdt_buyer: TradeTick) {
280        let trade = stub_trade_ethusdt_buyer;
281        let serialized = trade.as_msgpack_bytes().unwrap();
282        let deserialized = TradeTick::from_msgpack_bytes(serialized.as_ref()).unwrap();
283        assert_eq!(deserialized, trade);
284    }
285}