bybit/models/all_liquidation_update.rs
1use crate::prelude::*;
2
3/// Represents a single liquidation entry in the "all liquidation" WebSocket stream.
4///
5/// Contains details of a liquidation event that occurred on Bybit across all contract types.
6/// Liquidations happen when a trader's position cannot meet margin requirements, leading to
7/// forced closure. This struct provides information about the size, price, and direction
8/// of liquidated positions.
9///
10/// # Bybit API Reference
11/// The Bybit WebSocket API (https://bybit-exchange.github.io/docs/v5/websocket/public/all-liquidation)
12/// provides all liquidation data with a push frequency of 500ms.
13#[derive(Serialize, Deserialize, Clone, Debug)]
14#[serde(rename_all = "camelCase")]
15pub struct AllLiquidationData {
16 /// The timestamp when the liquidation was updated (in milliseconds).
17 ///
18 /// Indicates the exact time when the liquidation event occurred.
19 /// Bots can use this to correlate liquidation events with price movements.
20 #[serde(rename = "T")]
21 #[serde(with = "string_to_u64")]
22 pub updated_time: u64,
23
24 /// The trading pair symbol (e.g., "BTCUSDT").
25 ///
26 /// Specifies the market where the liquidation occurred.
27 /// Bots can filter by symbol to focus on relevant markets.
28 #[serde(rename = "s")]
29 pub symbol: String,
30
31 /// The side of the liquidated position ("Buy" or "Sell").
32 ///
33 /// Indicates whether the liquidated position was long (Buy) or short (Sell).
34 /// When you receive a "Buy" update, this means that a long position has been liquidated.
35 /// A high volume of liquidations on one side can signal a potential price reversal.
36 #[serde(rename = "S")]
37 pub side: String,
38
39 /// The executed size of the liquidated position.
40 ///
41 /// Represents the volume of the position that was forcibly closed.
42 /// Large liquidations can cause significant price movements and increased volatility.
43 #[serde(rename = "v")]
44 #[serde(with = "string_to_float")]
45 pub size: f64,
46
47 /// The price at which the position was liquidated.
48 ///
49 /// This is the bankruptcy price at which the position was forcibly closed.
50 /// Liquidation price levels often act as support or resistance zones.
51 #[serde(rename = "p")]
52 #[serde(with = "string_to_float")]
53 pub price: f64,
54}
55
56impl AllLiquidationData {
57 /// Constructs a new AllLiquidationData with specified parameters.
58 pub fn new(symbol: &str, side: &str, size: f64, price: f64, updated_time: u64) -> Self {
59 Self {
60 symbol: symbol.to_string(),
61 side: side.to_string(),
62 size,
63 price,
64 updated_time,
65 }
66 }
67
68 /// Returns true if the liquidated position was a long position.
69 ///
70 /// Long positions are liquidated when prices fall below the liquidation price.
71 pub fn is_long(&self) -> bool {
72 self.side.eq_ignore_ascii_case("Buy")
73 }
74
75 /// Returns true if the liquidated position was a short position.
76 ///
77 /// Short positions are liquidated when prices rise above the liquidation price.
78 pub fn is_short(&self) -> bool {
79 self.side.eq_ignore_ascii_case("Sell")
80 }
81
82 /// Returns the notional value of the liquidation.
83 ///
84 /// Calculated as `size * price`. This represents the total value of the position
85 /// that was liquidated, useful for assessing the market impact.
86 pub fn notional_value(&self) -> f64 {
87 self.size * self.price
88 }
89
90 /// Returns the updated time as a chrono DateTime.
91 pub fn updated_datetime(&self) -> chrono::DateTime<chrono::Utc> {
92 chrono::DateTime::from_timestamp((self.updated_time / 1000) as i64, 0)
93 .unwrap_or_else(chrono::Utc::now)
94 }
95
96 /// Returns the age of the liquidation in milliseconds.
97 ///
98 /// Calculates how long ago this liquidation occurred relative to current time.
99 pub fn age_ms(&self) -> u64 {
100 let now = chrono::Utc::now().timestamp_millis() as u64;
101 if now > self.updated_time {
102 now - self.updated_time
103 } else {
104 0
105 }
106 }
107
108 /// Returns true if the liquidation is recent (≤ 1 second old).
109 ///
110 /// Recent liquidations are more relevant for real-time trading decisions.
111 pub fn is_recent(&self) -> bool {
112 self.age_ms() <= 1000
113 }
114
115 /// Returns a string representation of the liquidation.
116 pub fn to_display_string(&self) -> String {
117 format!(
118 "{} {} {}: {:.8} @ {:.8} (Value: {:.2})",
119 self.symbol,
120 self.side,
121 if self.is_long() { "LONG" } else { "SHORT" },
122 self.size,
123 self.price,
124 self.notional_value()
125 )
126 }
127
128 /// Returns the liquidation as a tuple for easy pattern matching.
129 pub fn as_tuple(&self) -> (&str, &str, f64, f64, u64) {
130 (
131 &self.symbol,
132 &self.side,
133 self.size,
134 self.price,
135 self.updated_time,
136 )
137 }
138
139 /// Returns true if this liquidation is for a specific symbol.
140 pub fn is_symbol(&self, symbol: &str) -> bool {
141 self.symbol.eq_ignore_ascii_case(symbol)
142 }
143
144 /// Returns the side as an enum-like value.
145 pub fn side_enum(&self) -> LiquidationSide {
146 if self.is_long() {
147 LiquidationSide::Long
148 } else {
149 LiquidationSide::Short
150 }
151 }
152
153 /// Returns the price impact assuming linear market impact model.
154 ///
155 /// This is a simplified model: impact = k * sqrt(notional_value)
156 /// where k is an impact coefficient (default 0.001).
157 pub fn estimated_price_impact(&self, impact_coefficient: f64) -> f64 {
158 impact_coefficient * self.notional_value().sqrt()
159 }
160
161 /// Returns the percentage price impact relative to the liquidation price.
162 pub fn estimated_price_impact_percentage(&self, impact_coefficient: f64) -> f64 {
163 if self.price != 0.0 {
164 self.estimated_price_impact(impact_coefficient) / self.price * 100.0
165 } else {
166 0.0
167 }
168 }
169}
170
171/// Simple enum representation of liquidation side.
172#[derive(Debug, Clone, Copy, PartialEq, Eq)]
173pub enum LiquidationSide {
174 Long,
175 Short,
176}
177
178impl LiquidationSide {
179 /// Returns the string representation as used in Bybit API.
180 pub fn as_str(&self) -> &'static str {
181 match self {
182 LiquidationSide::Long => "Buy",
183 LiquidationSide::Short => "Sell",
184 }
185 }
186
187 /// Returns the opposite side.
188 pub fn opposite(&self) -> Self {
189 match self {
190 LiquidationSide::Long => LiquidationSide::Short,
191 LiquidationSide::Short => LiquidationSide::Long,
192 }
193 }
194}
195
196/// Represents a WebSocket "all liquidation" update event.
197///
198/// Contains real-time liquidation events that occur across all Bybit markets.
199/// This stream pushes all liquidations that occur on Bybit, covering:
200/// - USDT contracts (Perpetual and Delivery)
201/// - USDC contracts (Perpetual and Delivery)
202/// - Inverse contracts
203///
204/// Push frequency: 500ms
205///
206/// # Bybit API Reference
207/// Topic: `allLiquidation.{symbol}` (e.g., `allLiquidation.BTCUSDT`)
208#[derive(Serialize, Deserialize, Debug, Clone)]
209#[serde(rename_all = "camelCase")]
210pub struct AllLiquidationUpdate {
211 /// The WebSocket topic for the event (e.g., "allLiquidation.BTCUSDT").
212 ///
213 /// Specifies the data stream for the liquidation update.
214 /// Bots use this to determine which symbol the update belongs to.
215 #[serde(rename = "topic")]
216 pub topic: String,
217
218 /// The event type (e.g., "snapshot").
219 ///
220 /// All liquidation updates are snapshot type, containing the latest liquidation events.
221 #[serde(rename = "type")]
222 pub event_type: String,
223
224 /// The timestamp of the event in milliseconds.
225 ///
226 /// Indicates when the liquidation update was generated by the system.
227 /// Bots use this to ensure data freshness and time-based analysis.
228 #[serde(rename = "ts")]
229 #[serde(with = "string_to_u64")]
230 pub timestamp: u64,
231
232 /// The liquidation data.
233 ///
234 /// Contains a list of liquidation entries. Each entry represents a single
235 /// liquidation event that occurred on Bybit.
236 #[serde(rename = "data")]
237 pub data: Vec<AllLiquidationData>,
238}
239
240impl AllLiquidationUpdate {
241 /// Extracts the symbol from the topic.
242 ///
243 /// Parses the WebSocket topic to extract the trading symbol.
244 /// Example: "allLiquidation.BTCUSDT" -> "BTCUSDT"
245 pub fn symbol_from_topic(&self) -> Option<&str> {
246 self.topic.split('.').last()
247 }
248
249 /// Returns true if this is a snapshot update.
250 ///
251 /// All liquidation updates are snapshot type.
252 pub fn is_snapshot(&self) -> bool {
253 self.event_type == "snapshot"
254 }
255
256 /// Returns the timestamp as a chrono DateTime.
257 pub fn timestamp_datetime(&self) -> chrono::DateTime<chrono::Utc> {
258 chrono::DateTime::from_timestamp((self.timestamp / 1000) as i64, 0)
259 .unwrap_or_else(chrono::Utc::now)
260 }
261
262 /// Returns the age of the update in milliseconds.
263 ///
264 /// Calculates how old this update is relative to the current time.
265 pub fn age_ms(&self) -> u64 {
266 let now = chrono::Utc::now().timestamp_millis() as u64;
267 if now > self.timestamp {
268 now - self.timestamp
269 } else {
270 0
271 }
272 }
273
274 /// Returns true if the update is stale (older than 1 second).
275 ///
276 /// Since liquidation updates are pushed every 500ms, data older than 1 second
277 /// might be considered stale for real-time trading decisions.
278 pub fn is_stale(&self) -> bool {
279 self.age_ms() > 1000
280 }
281
282 /// Returns the number of liquidation entries in this update.
283 pub fn count(&self) -> usize {
284 self.data.len()
285 }
286
287 /// Returns true if there are no liquidation entries in this update.
288 pub fn is_empty(&self) -> bool {
289 self.data.is_empty()
290 }
291
292 /// Returns the total notional value of all liquidations in this update.
293 ///
294 /// Sums the notional values of all liquidation entries.
295 /// Useful for assessing the overall market impact of liquidations.
296 pub fn total_notional_value(&self) -> f64 {
297 self.data.iter().map(|liq| liq.notional_value()).sum()
298 }
299
300 /// Returns the total size of long liquidations.
301 pub fn total_long_size(&self) -> f64 {
302 self.data
303 .iter()
304 .filter(|liq| liq.is_long())
305 .map(|liq| liq.size)
306 .sum()
307 }
308
309 /// Returns the total size of short liquidations.
310 pub fn total_short_size(&self) -> f64 {
311 self.data
312 .iter()
313 .filter(|liq| liq.is_short())
314 .map(|liq| liq.size)
315 .sum()
316 }
317
318 /// Returns the total notional value of long liquidations.
319 pub fn total_long_notional(&self) -> f64 {
320 self.data
321 .iter()
322 .filter(|liq| liq.is_long())
323 .map(|liq| liq.notional_value())
324 .sum()
325 }
326
327 /// Returns the total notional value of short liquidations.
328 pub fn total_short_notional(&self) -> f64 {
329 self.data
330 .iter()
331 .filter(|liq| liq.is_short())
332 .map(|liq| liq.notional_value())
333 .sum()
334 }
335
336 /// Returns the net liquidation imbalance.
337 ///
338 /// Calculated as (total_long_notional - total_short_notional).
339 /// A positive value indicates more long liquidations (bearish pressure).
340 /// A negative value indicates more short liquidations (bullish pressure).
341 pub fn net_imbalance(&self) -> f64 {
342 self.total_long_notional() - self.total_short_notional()
343 }
344
345 /// Returns the net liquidation imbalance as a percentage of total notional.
346 pub fn net_imbalance_percentage(&self) -> f64 {
347 let total = self.total_notional_value();
348 if total != 0.0 {
349 self.net_imbalance() / total * 100.0
350 } else {
351 0.0
352 }
353 }
354
355 /// Returns the average price of all liquidations.
356 pub fn average_price(&self) -> Option<f64> {
357 if self.data.is_empty() {
358 None
359 } else {
360 let total_notional = self.total_notional_value();
361 let total_size = self.data.iter().map(|liq| liq.size).sum::<f64>();
362 if total_size != 0.0 {
363 Some(total_notional / total_size)
364 } else {
365 None
366 }
367 }
368 }
369
370 /// Returns the weighted average price (by size) of liquidations.
371 pub fn weighted_average_price(&self) -> Option<f64> {
372 self.average_price()
373 }
374
375 /// Returns the maximum liquidation size in this update.
376 pub fn max_size(&self) -> Option<f64> {
377 self.data.iter().map(|liq| liq.size).reduce(f64::max)
378 }
379
380 /// Returns the minimum liquidation size in this update.
381 pub fn min_size(&self) -> Option<f64> {
382 self.data.iter().map(|liq| liq.size).reduce(f64::min)
383 }
384
385 /// Returns the maximum liquidation price in this update.
386 pub fn max_price(&self) -> Option<f64> {
387 self.data.iter().map(|liq| liq.price).reduce(f64::max)
388 }
389
390 /// Returns the minimum liquidation price in this update.
391 pub fn min_price(&self) -> Option<f64> {
392 self.data.iter().map(|liq| liq.price).reduce(f64::min)
393 }
394
395 /// Returns all liquidation entries for a specific side.
396 pub fn filter_by_side(&self, side: &str) -> Vec<&AllLiquidationData> {
397 self.data
398 .iter()
399 .filter(|liq| liq.side.eq_ignore_ascii_case(side))
400 .collect()
401 }
402
403 /// Returns all long liquidation entries.
404 pub fn long_liquidations(&self) -> Vec<&AllLiquidationData> {
405 self.filter_by_side("Buy")
406 }
407
408 /// Returns all short liquidation entries.
409 pub fn short_liquidations(&self) -> Vec<&AllLiquidationData> {
410 self.filter_by_side("Sell")
411 }
412
413 /// Returns the most recent liquidation entry (by updated_time).
414 pub fn most_recent(&self) -> Option<&AllLiquidationData> {
415 self.data.iter().max_by_key(|liq| liq.updated_time)
416 }
417
418 /// Returns the oldest liquidation entry (by updated_time).
419 pub fn oldest(&self) -> Option<&AllLiquidationData> {
420 self.data.iter().min_by_key(|liq| liq.updated_time)
421 }
422
423 /// Returns a summary string for this liquidation update.
424 pub fn to_summary_string(&self) -> String {
425 let symbol = self.symbol_from_topic().unwrap_or("unknown");
426 format!(
427 "[{}] {}: {} liquidations ({} long, {} short), Total=${:.2}, Imbalance={:.2}%",
428 self.timestamp_datetime().format("%H:%M:%S%.3f"),
429 symbol,
430 self.count(),
431 self.long_liquidations().len(),
432 self.short_liquidations().len(),
433 self.total_notional_value(),
434 self.net_imbalance_percentage()
435 )
436 }
437
438 /// Validates the update for trading use.
439 ///
440 /// Returns `true` if:
441 /// 1. The update is not stale (≤ 1 second old)
442 /// 2. The symbol can be extracted from the topic
443 /// 3. The update is a snapshot (all liquidation updates should be snapshots)
444 pub fn is_valid_for_trading(&self) -> bool {
445 !self.is_stale() && self.symbol_from_topic().is_some() && self.is_snapshot()
446 }
447
448 /// Returns the update latency in milliseconds.
449 ///
450 /// For comparing with other market data timestamps.
451 pub fn latency_ms(&self, other_timestamp: u64) -> i64 {
452 if self.timestamp > other_timestamp {
453 (self.timestamp - other_timestamp) as i64
454 } else {
455 (other_timestamp - self.timestamp) as i64
456 }
457 }
458
459 /// Groups liquidations by symbol (useful for multi-symbol topics if supported).
460 pub fn group_by_symbol(&self) -> std::collections::HashMap<String, Vec<&AllLiquidationData>> {
461 let mut groups = std::collections::HashMap::new();
462 for liq in &self.data {
463 groups
464 .entry(liq.symbol.clone())
465 .or_insert_with(Vec::new)
466 .push(liq);
467 }
468 groups
469 }
470
471 /// Returns the estimated total market impact of all liquidations.
472 ///
473 /// Using a simplified model: total_impact = sum(impact_coefficient * sqrt(notional_value))
474 pub fn estimated_total_market_impact(&self, impact_coefficient: f64) -> f64 {
475 self.data
476 .iter()
477 .map(|liq| liq.estimated_price_impact(impact_coefficient))
478 .sum()
479 }
480
481 /// Returns the liquidation with the largest notional value.
482 pub fn largest_liquidation(&self) -> Option<&AllLiquidationData> {
483 self.data.iter().max_by(|a, b| {
484 a.notional_value()
485 .partial_cmp(&b.notional_value())
486 .unwrap_or(std::cmp::Ordering::Equal)
487 })
488 }
489
490 /// Returns the liquidation with the smallest notional value.
491 pub fn smallest_liquidation(&self) -> Option<&AllLiquidationData> {
492 self.data.iter().min_by(|a, b| {
493 a.notional_value()
494 .partial_cmp(&b.notional_value())
495 .unwrap_or(std::cmp::Ordering::Equal)
496 })
497 }
498}