1use crate::prelude::*;
2
3#[derive(Serialize, Deserialize, Debug, Clone)]
7pub struct WsTrade {
8 #[serde(rename = "T")]
12 pub timestamp: u64,
13
14 #[serde(rename = "s")]
18 pub symbol: String,
19
20 #[serde(rename = "S")]
24 pub side: String,
25
26 #[serde(rename = "v", with = "string_to_float")]
30 pub volume: f64,
31
32 #[serde(rename = "p", with = "string_to_float")]
36 pub price: f64,
37
38 #[serde(rename = "L")]
42 pub tick_direction: TickDirection,
43
44 #[serde(rename = "i")]
48 pub id: String,
49
50 #[serde(rename = "BT")]
54 pub buyer_is_maker: bool,
55}
56
57impl WsTrade {
58 pub fn new(
60 timestamp: u64,
61 symbol: &str,
62 side: &str,
63 volume: f64,
64 price: f64,
65 tick_direction: TickDirection,
66 id: &str,
67 buyer_is_maker: bool,
68 ) -> Self {
69 Self {
70 timestamp,
71 symbol: symbol.to_string(),
72 side: side.to_string(),
73 volume,
74 price,
75 tick_direction,
76 id: id.to_string(),
77 buyer_is_maker,
78 }
79 }
80
81 pub fn is_buy(&self) -> bool {
83 self.side.eq_ignore_ascii_case("buy")
84 }
85
86 pub fn is_sell(&self) -> bool {
88 self.side.eq_ignore_ascii_case("sell")
89 }
90
91 pub fn timestamp_datetime(&self) -> chrono::DateTime<chrono::Utc> {
93 chrono::DateTime::from_timestamp((self.timestamp / 1000) as i64, 0)
94 .unwrap_or_else(chrono::Utc::now)
95 }
96
97 pub fn age_ms(&self) -> u64 {
99 let now = chrono::Utc::now().timestamp_millis() as u64;
100 if now > self.timestamp {
101 now - self.timestamp
102 } else {
103 0
104 }
105 }
106
107 pub fn is_stale(&self) -> bool {
109 self.age_ms() > 5000
110 }
111
112 pub fn value(&self) -> f64 {
114 self.price * self.volume
115 }
116
117 pub fn is_taker(&self) -> bool {
119 !self.buyer_is_maker
120 }
121
122 pub fn is_maker(&self) -> bool {
124 self.buyer_is_maker
125 }
126
127 pub fn trade_type(&self) -> String {
129 if self.is_buy() {
130 if self.is_maker() {
131 "Buy Maker".to_string()
132 } else {
133 "Buy Taker".to_string()
134 }
135 } else {
136 if self.is_maker() {
137 "Sell Maker".to_string()
138 } else {
139 "Sell Taker".to_string()
140 }
141 }
142 }
143
144 pub fn is_uptick(&self) -> bool {
146 matches!(
147 self.tick_direction,
148 TickDirection::PlusTick | TickDirection::ZeroPlusTick
149 )
150 }
151
152 pub fn is_downtick(&self) -> bool {
154 matches!(
155 self.tick_direction,
156 TickDirection::MinusTick | TickDirection::ZeroMinusTick
157 )
158 }
159
160 pub fn is_neutral_tick(&self) -> bool {
162 matches!(
163 self.tick_direction,
164 TickDirection::ZeroPlusTick | TickDirection::ZeroMinusTick
165 )
166 }
167
168 pub fn tick_direction_string(&self) -> &'static str {
170 match self.tick_direction {
171 TickDirection::PlusTick => "PlusTick",
172 TickDirection::ZeroPlusTick => "ZeroPlusTick",
173 TickDirection::MinusTick => "MinusTick",
174 TickDirection::ZeroMinusTick => "ZeroMinusTick",
175 }
176 }
177
178 pub fn is_valid(&self) -> bool {
180 self.timestamp > 0
181 && !self.symbol.is_empty()
182 && (self.is_buy() || self.is_sell())
183 && self.volume > 0.0
184 && self.price > 0.0
185 && !self.id.is_empty()
186 }
187
188 pub fn to_summary_string(&self) -> String {
190 format!(
191 "[{}] {} {} {} @ {} (Value: {:.2}, {})",
192 self.timestamp_datetime().format("%H:%M:%S%.3f"),
193 self.symbol,
194 self.side,
195 self.volume,
196 self.price,
197 self.value(),
198 self.trade_type()
199 )
200 }
201
202 pub fn to_compact_string(&self) -> String {
204 let side_char = if self.is_buy() { 'B' } else { 'S' };
205 let maker_char = if self.is_maker() { 'M' } else { 'T' };
206 let tick_char = match self.tick_direction {
207 TickDirection::PlusTick => '↑',
208 TickDirection::ZeroPlusTick => '↗',
209 TickDirection::MinusTick => '↓',
210 TickDirection::ZeroMinusTick => '↘',
211 };
212
213 format!(
214 "{} {}{}{} {}@{:.2}",
215 self.timestamp_datetime().format("%H:%M:%S"),
216 side_char,
217 maker_char,
218 tick_char,
219 self.volume,
220 self.price
221 )
222 }
223
224 pub fn size_category(&self) -> TradeSizeCategory {
226 let value = self.value();
227 if value >= 1_000_000.0 {
228 TradeSizeCategory::Whale
229 } else if value >= 100_000.0 {
230 TradeSizeCategory::Large
231 } else if value >= 10_000.0 {
232 TradeSizeCategory::Medium
233 } else if value >= 1_000.0 {
234 TradeSizeCategory::Small
235 } else {
236 TradeSizeCategory::Retail
237 }
238 }
239
240 pub fn size_category_string(&self) -> &'static str {
242 match self.size_category() {
243 TradeSizeCategory::Whale => "Whale",
244 TradeSizeCategory::Large => "Large",
245 TradeSizeCategory::Medium => "Medium",
246 TradeSizeCategory::Small => "Small",
247 TradeSizeCategory::Retail => "Retail",
248 }
249 }
250
251 pub fn notional_value(&self) -> f64 {
253 self.value()
254 }
255
256 pub fn impact_ratio(&self) -> f64 {
259 self.volume / self.price
260 }
261
262 pub fn is_recent(&self, max_age_ms: u64) -> bool {
264 self.age_ms() <= max_age_ms
265 }
266
267 pub fn price_diff(&self, other: &WsTrade) -> Option<f64> {
269 if self.symbol == other.symbol {
270 Some(self.price - other.price)
271 } else {
272 None
273 }
274 }
275
276 pub fn price_diff_percentage(&self, other: &WsTrade) -> Option<f64> {
278 if self.symbol == other.symbol && other.price > 0.0 {
279 Some((self.price - other.price) / other.price * 100.0)
280 } else {
281 None
282 }
283 }
284
285 pub fn as_tuple(&self) -> (u64, &str, &str, f64, f64, TickDirection, &str, bool) {
287 (
288 self.timestamp,
289 &self.symbol,
290 &self.side,
291 self.volume,
292 self.price,
293 self.tick_direction.clone(),
294 &self.id,
295 self.buyer_is_maker,
296 )
297 }
298}
299
300#[derive(Debug, Clone, Copy, PartialEq, Eq)]
302pub enum TradeSizeCategory {
303 Retail,
305 Small,
307 Medium,
309 Large,
311 Whale,
313}
314
315impl TradeSizeCategory {
316 pub fn min_value(&self) -> f64 {
318 match self {
319 TradeSizeCategory::Retail => 0.0,
320 TradeSizeCategory::Small => 1_000.0,
321 TradeSizeCategory::Medium => 10_000.0,
322 TradeSizeCategory::Large => 100_000.0,
323 TradeSizeCategory::Whale => 1_000_000.0,
324 }
325 }
326
327 pub fn max_value(&self) -> Option<f64> {
329 match self {
330 TradeSizeCategory::Retail => Some(1_000.0),
331 TradeSizeCategory::Small => Some(10_000.0),
332 TradeSizeCategory::Medium => Some(100_000.0),
333 TradeSizeCategory::Large => Some(1_000_000.0),
334 TradeSizeCategory::Whale => None,
335 }
336 }
337
338 pub fn as_str(&self) -> &'static str {
340 match self {
341 TradeSizeCategory::Retail => "Retail",
342 TradeSizeCategory::Small => "Small",
343 TradeSizeCategory::Medium => "Medium",
344 TradeSizeCategory::Large => "Large",
345 TradeSizeCategory::Whale => "Whale",
346 }
347 }
348}
349
350impl std::fmt::Display for TradeSizeCategory {
351 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
352 write!(f, "{}", self.as_str())
353 }
354}