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}