Skip to main content

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}