Skip to main content

bybit/models/
insurance_pool.rs

1use crate::prelude::*;
2
3/// Represents a single insurance pool entry in the WebSocket stream.
4///
5/// Contains information about the insurance pool balance for a specific symbol or group of symbols.
6/// Insurance pools are used to cover losses when a trader's position is liquidated below the
7/// bankruptcy price, preventing auto-deleveraging of other traders.
8#[derive(Serialize, Deserialize, Clone, Debug)]
9#[serde(rename_all = "camelCase")]
10pub struct InsurancePool {
11    /// The coin/token of the insurance pool (e.g., "USDT", "USDC", "BTC").
12    ///
13    /// Specifies the currency used for the insurance pool.
14    /// For inverse contracts, this would be the base coin (e.g., "BTC" for BTCUSD).
15    pub coin: String,
16
17    /// The symbol(s) associated with this insurance pool.
18    ///
19    /// For isolated insurance pools, this is a single symbol name (e.g., "BTCUSDT").
20    /// For shared insurance pools, this may contain multiple symbols or be a group identifier.
21    /// Note: Shared insurance pool data is not pushed via WebSocket.
22    pub symbols: String,
23
24    /// The current balance of the insurance pool in the specified coin.
25    ///
26    /// A positive balance indicates the fund has surplus to cover losses.
27    /// A negative balance indicates the fund is depleted and may trigger ADL (Auto-Deleveraging).
28    #[serde(with = "string_to_float")]
29    pub balance: f64,
30
31    /// The timestamp when this insurance pool data was last updated (in milliseconds).
32    ///
33    /// Indicates when the balance information was refreshed by Bybit's systems.
34    /// For shared insurance pools, this field follows a T+1 refresh mechanism
35    /// and is updated daily at 00:00 UTC.
36    #[serde(rename = "updateTime")]
37    #[serde(with = "string_to_u64")]
38    pub update_time: u64,
39}
40
41impl InsurancePool {
42    /// Constructs a new InsurancePool with specified parameters.
43    pub fn new(coin: &str, symbols: &str, balance: f64, update_time: u64) -> Self {
44        Self {
45            coin: coin.to_string(),
46            symbols: symbols.to_string(),
47            balance,
48            update_time,
49        }
50    }
51
52    /// Returns true if this insurance pool is for a specific coin.
53    pub fn is_coin(&self, coin: &str) -> bool {
54        self.coin.eq_ignore_ascii_case(coin)
55    }
56
57    /// Returns true if this insurance pool is for a specific symbol.
58    ///
59    /// Note: For shared insurance pools, the symbols field may contain multiple symbols
60    /// or a group identifier, so this check may not be accurate for all cases.
61    pub fn is_symbol(&self, symbol: &str) -> bool {
62        self.symbols.eq_ignore_ascii_case(symbol)
63    }
64
65    /// Returns true if the insurance pool balance is positive.
66    pub fn is_positive(&self) -> bool {
67        self.balance > 0.0
68    }
69
70    /// Returns true if the insurance pool balance is negative or zero.
71    ///
72    /// A non-positive balance may indicate that ADL (Auto-Deleveraging) could be triggered.
73    pub fn is_non_positive(&self) -> bool {
74        self.balance <= 0.0
75    }
76
77    /// Returns the update time as a chrono DateTime.
78    pub fn update_datetime(&self) -> chrono::DateTime<chrono::Utc> {
79        chrono::DateTime::from_timestamp((self.update_time / 1000) as i64, 0)
80            .unwrap_or_else(chrono::Utc::now)
81    }
82
83    /// Returns the time since the last update in seconds.
84    pub fn time_since_update(&self) -> u64 {
85        let now = chrono::Utc::now().timestamp_millis() as u64;
86        if now > self.update_time {
87            (now - self.update_time) / 1000
88        } else {
89            0
90        }
91    }
92
93    /// Checks if the data is stale (older than 5 minutes).
94    ///
95    /// The WebSocket pushes updates every 1 second, so data older than 5 minutes
96    /// might be considered stale for real-time trading decisions.
97    pub fn is_stale(&self) -> bool {
98        self.time_since_update() > 300
99    }
100
101    /// Returns the absolute value of the balance.
102    ///
103    /// Useful for display purposes when the sign indicates deficit/surplus.
104    pub fn absolute_balance(&self) -> f64 {
105        self.balance.abs()
106    }
107
108    /// Returns a string representation of the balance with sign.
109    ///
110    /// Positive balances show "+" prefix, negative balances show "-" prefix.
111    pub fn signed_balance_string(&self) -> String {
112        if self.balance >= 0.0 {
113            format!("+{:.8}", self.balance)
114        } else {
115            format!("{:.8}", self.balance)
116        }
117    }
118}
119
120/// Represents a WebSocket insurance pool update event.
121///
122/// Contains real-time updates to insurance pool balances for various symbols.
123/// Push frequency: 1 second for USDT contracts, USDC contracts, and inverse contracts.
124#[derive(Serialize, Deserialize, Debug, Clone)]
125#[serde(rename_all = "camelCase")]
126pub struct InsurancePoolUpdate {
127    /// The WebSocket topic for the event (e.g., "insurance.USDT", "insurance.USDC", "insurance.inverse").
128    ///
129    /// Specifies the data stream for the insurance pool update.
130    /// Bots use this to determine which contract group the update belongs to.
131    #[serde(rename = "topic")]
132    pub topic: String,
133
134    /// The event type (e.g., "snapshot", "delta").
135    ///
136    /// Snapshot contains the full current state, delta contains incremental changes.
137    /// Bots should use snapshot to initialize state and delta to update it.
138    #[serde(rename = "type")]
139    pub event_type: String,
140
141    /// The timestamp of the event in milliseconds.
142    ///
143    /// Indicates when the insurance pool update was generated by the system.
144    /// Bots use this to ensure data freshness and time-based analysis.
145    #[serde(rename = "ts")]
146    pub timestamp: u64,
147
148    /// The insurance pool data.
149    ///
150    /// Contains a list of insurance pool entries. Each entry represents the balance
151    /// for a specific symbol or group of symbols.
152    /// No event will be published if all insurance pool balances remain unchanged.
153    #[serde(rename = "data")]
154    pub data: Vec<InsurancePool>,
155}
156
157impl InsurancePoolUpdate {
158    /// Returns the contract group from the topic.
159    ///
160    /// Extracts the contract group identifier from the WebSocket topic.
161    /// Examples:
162    /// - "insurance.USDT" -> "USDT"
163    /// - "insurance.USDC" -> "USDC"
164    /// - "insurance.inverse" -> "inverse"
165    pub fn contract_group(&self) -> Option<&str> {
166        self.topic.split('.').last()
167    }
168
169    /// Returns true if this is a snapshot update.
170    ///
171    /// Snapshot updates contain the full insurance pool state and should replace
172    /// the local state for the corresponding contract group.
173    pub fn is_snapshot(&self) -> bool {
174        self.event_type == "snapshot"
175    }
176
177    /// Returns true if this is a delta update.
178    ///
179    /// Delta updates contain incremental changes and should be applied to
180    /// the local insurance pool state.
181    pub fn is_delta(&self) -> bool {
182        self.event_type == "delta"
183    }
184
185    /// Returns the timestamp as a chrono DateTime.
186    pub fn timestamp_datetime(&self) -> chrono::DateTime<chrono::Utc> {
187        chrono::DateTime::from_timestamp((self.timestamp / 1000) as i64, 0)
188            .unwrap_or_else(chrono::Utc::now)
189    }
190
191    /// Returns the number of insurance pool entries in this update.
192    pub fn count(&self) -> usize {
193        self.data.len()
194    }
195
196    /// Returns true if there are no insurance pool entries in this update.
197    pub fn is_empty(&self) -> bool {
198        self.data.is_empty()
199    }
200
201    /// Finds an insurance pool entry for a specific symbol.
202    ///
203    /// Returns the first matching insurance pool entry for the given symbol.
204    /// Note: For shared insurance pools, the symbols field may contain multiple
205    /// symbols or a group identifier, so this may not find all relevant entries.
206    pub fn find_by_symbol(&self, symbol: &str) -> Option<&InsurancePool> {
207        self.data.iter().find(|pool| pool.is_symbol(symbol))
208    }
209
210    /// Finds all insurance pool entries for a specific coin.
211    pub fn filter_by_coin(&self, coin: &str) -> Vec<&InsurancePool> {
212        self.data.iter().filter(|pool| pool.is_coin(coin)).collect()
213    }
214
215    /// Returns the total balance across all insurance pools in this update.
216    ///
217    /// Sums the balances of all insurance pool entries in this update.
218    /// Useful for monitoring the overall health of a contract group's insurance.
219    pub fn total_balance(&self) -> f64 {
220        self.data.iter().map(|pool| pool.balance).sum()
221    }
222
223    /// Returns the number of insurance pools with positive balance.
224    pub fn count_positive(&self) -> usize {
225        self.data.iter().filter(|pool| pool.is_positive()).count()
226    }
227
228    /// Returns the number of insurance pools with non-positive balance.
229    ///
230    /// These are pools that may be at risk of triggering ADL (Auto-Deleveraging).
231    pub fn count_non_positive(&self) -> usize {
232        self.data
233            .iter()
234            .filter(|pool| pool.is_non_positive())
235            .count()
236    }
237
238    /// Returns the minimum balance among all insurance pools.
239    pub fn min_balance(&self) -> Option<f64> {
240        self.data.iter().map(|pool| pool.balance).reduce(f64::min)
241    }
242
243    /// Returns the maximum balance among all insurance pools.
244    pub fn max_balance(&self) -> Option<f64> {
245        self.data.iter().map(|pool| pool.balance).reduce(f64::max)
246    }
247
248    /// Returns the average balance across all insurance pools.
249    pub fn average_balance(&self) -> Option<f64> {
250        if self.data.is_empty() {
251            None
252        } else {
253            Some(self.total_balance() / self.data.len() as f64)
254        }
255    }
256
257    /// Returns all insurance pools that are stale (older than 5 minutes).
258    ///
259    /// Note: The WebSocket pushes updates every 1 second when changes occur,
260    /// so stale entries in a fresh update may indicate issues with specific symbols.
261    pub fn stale_pools(&self) -> Vec<&InsurancePool> {
262        self.data.iter().filter(|pool| pool.is_stale()).collect()
263    }
264
265    /// Returns the time of the most recent update among all insurance pools.
266    pub fn latest_update_time(&self) -> Option<u64> {
267        self.data.iter().map(|pool| pool.update_time).max()
268    }
269
270    /// Returns the time of the oldest update among all insurance pools.
271    pub fn earliest_update_time(&self) -> Option<u64> {
272        self.data.iter().map(|pool| pool.update_time).min()
273    }
274}