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::HasTsInit;
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 = "posei_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(
68 instrument_id: InstrumentId,
69 price: Price,
70 size: Quantity,
71 aggressor_side: AggressorSide,
72 trade_id: TradeId,
73 ts_event: UnixNanos,
74 ts_init: UnixNanos,
75 ) -> anyhow::Result<Self> {
76 check_positive_quantity(size, stringify!(size))?;
77
78 Ok(Self {
79 instrument_id,
80 price,
81 size,
82 aggressor_side,
83 trade_id,
84 ts_event,
85 ts_init,
86 })
87 }
88
89 #[must_use]
95 pub fn new(
96 instrument_id: InstrumentId,
97 price: Price,
98 size: Quantity,
99 aggressor_side: AggressorSide,
100 trade_id: TradeId,
101 ts_event: UnixNanos,
102 ts_init: UnixNanos,
103 ) -> Self {
104 Self::new_checked(
105 instrument_id,
106 price,
107 size,
108 aggressor_side,
109 trade_id,
110 ts_event,
111 ts_init,
112 )
113 .expect(FAILED)
114 }
115
116 #[must_use]
118 pub fn get_metadata(
119 instrument_id: &InstrumentId,
120 price_precision: u8,
121 size_precision: u8,
122 ) -> HashMap<String, String> {
123 let mut metadata = HashMap::new();
124 metadata.insert("instrument_id".to_string(), instrument_id.to_string());
125 metadata.insert("price_precision".to_string(), price_precision.to_string());
126 metadata.insert("size_precision".to_string(), size_precision.to_string());
127 metadata
128 }
129
130 #[must_use]
132 pub fn get_fields() -> IndexMap<String, String> {
133 let mut metadata = IndexMap::new();
134 metadata.insert("price".to_string(), FIXED_SIZE_BINARY.to_string());
135 metadata.insert("size".to_string(), FIXED_SIZE_BINARY.to_string());
136 metadata.insert("aggressor_side".to_string(), "UInt8".to_string());
137 metadata.insert("trade_id".to_string(), "Utf8".to_string());
138 metadata.insert("ts_event".to_string(), "UInt64".to_string());
139 metadata.insert("ts_init".to_string(), "UInt64".to_string());
140 metadata
141 }
142}
143
144impl Display for TradeTick {
145 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
146 write!(
147 f,
148 "{},{},{},{},{},{}",
149 self.instrument_id,
150 self.price,
151 self.size,
152 self.aggressor_side,
153 self.trade_id,
154 self.ts_event,
155 )
156 }
157}
158
159impl Serializable for TradeTick {}
160
161impl HasTsInit for TradeTick {
162 fn ts_init(&self) -> UnixNanos {
163 self.ts_init
164 }
165}
166
167#[cfg(test)]
171mod tests {
172 use nautilus_core::{UnixNanos, serialization::Serializable};
173 use rstest::rstest;
174
175 use crate::{
176 data::{TradeTick, stubs::stub_trade_ethusdt_buyer},
177 enums::AggressorSide,
178 identifiers::{InstrumentId, TradeId},
179 types::{Price, Quantity},
180 };
181
182 #[cfg(feature = "high-precision")] #[rstest]
184 #[should_panic(expected = "invalid `Quantity` for 'size' not positive, was 0")]
185 fn test_trade_tick_new_with_zero_size_panics() {
186 let instrument_id = InstrumentId::from("ETH-USDT-SWAP.OKX");
187 let price = Price::from("10000.00");
188 let zero_size = Quantity::from(0);
189 let aggressor_side = AggressorSide::Buyer;
190 let trade_id = TradeId::from("123456789");
191 let ts_event = UnixNanos::from(0);
192 let ts_init = UnixNanos::from(1);
193
194 let _ = TradeTick::new(
195 instrument_id,
196 price,
197 zero_size,
198 aggressor_side,
199 trade_id,
200 ts_event,
201 ts_init,
202 );
203 }
204
205 #[rstest]
206 fn test_trade_tick_new_checked_with_zero_size_error() {
207 let instrument_id = InstrumentId::from("ETH-USDT-SWAP.OKX");
208 let price = Price::from("10000.00");
209 let zero_size = Quantity::from(0);
210 let aggressor_side = AggressorSide::Buyer;
211 let trade_id = TradeId::from("123456789");
212 let ts_event = UnixNanos::from(0);
213 let ts_init = UnixNanos::from(1);
214
215 let result = TradeTick::new_checked(
216 instrument_id,
217 price,
218 zero_size,
219 aggressor_side,
220 trade_id,
221 ts_event,
222 ts_init,
223 );
224
225 assert!(result.is_err());
226 }
227
228 #[rstest]
229 fn test_to_string(stub_trade_ethusdt_buyer: TradeTick) {
230 let trade = stub_trade_ethusdt_buyer;
231 assert_eq!(
232 trade.to_string(),
233 "ETHUSDT-PERP.BINANCE,10000.0000,1.00000000,BUYER,123456789,0"
234 );
235 }
236
237 #[rstest]
238 fn test_deserialize_raw_string() {
239 let raw_string = r#"{
240 "type": "TradeTick",
241 "instrument_id": "ETHUSDT-PERP.BINANCE",
242 "price": "10000.0000",
243 "size": "1.00000000",
244 "aggressor_side": "BUYER",
245 "trade_id": "123456789",
246 "ts_event": 0,
247 "ts_init": 1
248 }"#;
249
250 let trade: TradeTick = serde_json::from_str(raw_string).unwrap();
251
252 assert_eq!(trade.aggressor_side, AggressorSide::Buyer);
253 }
254
255 #[rstest]
256 fn test_json_serialization(stub_trade_ethusdt_buyer: TradeTick) {
257 let trade = stub_trade_ethusdt_buyer;
258 let serialized = trade.to_json_bytes().unwrap();
259 let deserialized = TradeTick::from_json_bytes(serialized.as_ref()).unwrap();
260 assert_eq!(deserialized, trade);
261 }
262
263 #[rstest]
264 fn test_msgpack_serialization(stub_trade_ethusdt_buyer: TradeTick) {
265 let trade = stub_trade_ethusdt_buyer;
266 let serialized = trade.to_msgpack_bytes().unwrap();
267 let deserialized = TradeTick::from_msgpack_bytes(serialized.as_ref()).unwrap();
268 assert_eq!(deserialized, trade);
269 }
270
271 #[cfg(feature = "python")]
272 #[rstest]
273 fn test_from_pyobject(stub_trade_ethusdt_buyer: TradeTick) {
274 use pyo3::{IntoPyObjectExt, Python};
275
276 pyo3::prepare_freethreaded_python();
277 let trade = stub_trade_ethusdt_buyer;
278
279 Python::with_gil(|py| {
280 let tick_pyobject = trade.into_py_any(py).unwrap();
281 let parsed_tick = TradeTick::from_pyobject(tick_pyobject.bind(py)).unwrap();
282 assert_eq!(parsed_tick, trade);
283 });
284 }
285}