1use 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#[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 pub instrument_id: InstrumentId,
43 pub price: Price,
45 pub size: Quantity,
47 pub aggressor_side: AggressorSide,
49 pub trade_id: TradeId,
51 pub ts_event: UnixNanos,
53 pub ts_init: UnixNanos,
55}
56
57impl TradeTick {
58 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 #[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 #[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 #[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#[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")] #[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}