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