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