bybit/models/price_limit_update.rs
1use crate::prelude::*;
2
3/// Represents the order price limits for a single trading symbol in a WebSocket update.
4///
5/// Contains the highest bid price (buyLmt) and lowest ask price (sellLmt) for a given symbol.
6/// These limits define the order price boundaries for derivative or spot trading and are
7/// important for risk management and order validation.
8///
9/// # Bybit API Reference
10/// The Bybit WebSocket API (https://bybit-exchange.github.io/docs/v5/websocket/public/order-price-limit)
11/// provides real-time order price limit updates with a push frequency of 300ms.
12#[derive(Serialize, Deserialize, Clone, Debug)]
13#[serde(rename_all = "camelCase")]
14pub struct PriceLimitData {
15 /// The trading pair symbol (e.g., "BTCUSDT").
16 ///
17 /// Identifies the specific instrument for which the price limits apply.
18 /// Trading bots should verify this matches the requested symbol to ensure data integrity.
19 pub symbol: String,
20
21 /// The highest bid price allowed (buy limit) as a string.
22 ///
23 /// Represents the maximum price at which buy orders can be placed.
24 /// Orders with prices above this limit will be rejected.
25 /// Trading bots must ensure buy order prices do not exceed this limit.
26 #[serde(with = "string_to_float")]
27 pub buy_lmt: f64,
28
29 /// The lowest ask price allowed (sell limit) as a string.
30 ///
31 /// Represents the minimum price at which sell orders can be placed.
32 /// Orders with prices below this limit will be rejected.
33 /// Trading bots must ensure sell order prices are not below this limit.
34 #[serde(with = "string_to_float")]
35 pub sell_lmt: f64,
36}
37
38impl PriceLimitData {
39 /// Constructs a new PriceLimitData with specified parameters.
40 pub fn new(symbol: &str, buy_lmt: f64, sell_lmt: f64) -> Self {
41 Self {
42 symbol: symbol.to_string(),
43 buy_lmt,
44 sell_lmt,
45 }
46 }
47
48 /// Returns true if the buy limit is valid (positive value).
49 pub fn is_buy_limit_valid(&self) -> bool {
50 self.buy_lmt > 0.0
51 }
52
53 /// Returns true if the sell limit is valid (positive value).
54 pub fn is_sell_limit_valid(&self) -> bool {
55 self.sell_lmt > 0.0
56 }
57
58 /// Returns true if both price limits are valid.
59 pub fn is_valid(&self) -> bool {
60 self.is_buy_limit_valid() && self.is_sell_limit_valid()
61 }
62
63 /// Returns the price range between buy and sell limits.
64 ///
65 /// Calculated as `sell_lmt - buy_lmt`. A positive value indicates normal market conditions
66 /// where sell limit is higher than buy limit. A negative value would indicate abnormal
67 /// market conditions.
68 pub fn price_range(&self) -> f64 {
69 self.sell_lmt - self.buy_lmt
70 }
71
72 /// Returns the mid price between buy and sell limits.
73 ///
74 /// Calculated as `(buy_lmt + sell_lmt) / 2.0`. This represents the theoretical
75 /// fair value within the allowed trading range.
76 pub fn mid_price(&self) -> f64 {
77 (self.buy_lmt + self.sell_lmt) / 2.0
78 }
79
80 /// Returns the price range as a percentage of the mid price.
81 ///
82 /// Useful for understanding the relative width of the allowed trading range.
83 /// Calculated as `price_range() / mid_price() * 100.0`.
84 pub fn price_range_percentage(&self) -> f64 {
85 let range = self.price_range();
86 let mid = self.mid_price();
87 if mid != 0.0 {
88 (range / mid) * 100.0
89 } else {
90 0.0
91 }
92 }
93
94 /// Checks if a buy price is within the allowed limit.
95 ///
96 /// Returns `true` if `price <= buy_lmt`, meaning the buy order price is acceptable.
97 pub fn is_buy_price_allowed(&self, price: f64) -> bool {
98 price <= self.buy_lmt
99 }
100
101 /// Checks if a sell price is within the allowed limit.
102 ///
103 /// Returns `true` if `price >= sell_lmt`, meaning the sell order price is acceptable.
104 pub fn is_sell_price_allowed(&self, price: f64) -> bool {
105 price >= self.sell_lmt
106 }
107
108 /// Returns the maximum allowable slippage for buy orders.
109 ///
110 /// Calculated as `(buy_lmt - reference_price) / reference_price` where `reference_price`
111 /// is typically the current market price or the price a bot intends to use.
112 pub fn buy_slippage_limit(&self, reference_price: f64) -> f64 {
113 if reference_price != 0.0 {
114 (self.buy_lmt - reference_price) / reference_price
115 } else {
116 0.0
117 }
118 }
119
120 /// Returns the maximum allowable slippage for sell orders.
121 ///
122 /// Calculated as `(reference_price - sell_lmt) / reference_price` where `reference_price`
123 /// is typically the current market price or the price a bot intends to use.
124 pub fn sell_slippage_limit(&self, reference_price: f64) -> f64 {
125 if reference_price != 0.0 {
126 (reference_price - self.sell_lmt) / reference_price
127 } else {
128 0.0
129 }
130 }
131
132 /// Returns the price limits as a tuple (buy_limit, sell_limit).
133 pub fn limits(&self) -> (f64, f64) {
134 (self.buy_lmt, self.sell_lmt)
135 }
136
137 /// Returns a string representation of the price limits.
138 pub fn to_display_string(&self) -> String {
139 format!(
140 "{}: Buy ≤ {:.8}, Sell ≥ {:.8} (Range: {:.2}%)",
141 self.symbol,
142 self.buy_lmt,
143 self.sell_lmt,
144 self.price_range_percentage()
145 )
146 }
147
148 /// Returns the timestamp from the symbol, if it contains expiry information.
149 ///
150 /// For futures contracts with expiry dates in the symbol (e.g., "BTC-26DEC25"),
151 /// this attempts to extract and parse the expiry date.
152 pub fn expiry_timestamp(&self) -> Option<chrono::DateTime<chrono::Utc>> {
153 // Try to parse futures symbol format: BASE-EXPIRY or BASE-EXPIRY-STRIKE-TYPE
154 let parts: Vec<&str> = self.symbol.split('-').collect();
155 if parts.len() >= 2 {
156 let expiry_str = parts[1];
157 // Try to parse as DDMMMYY or DDMMMYYYY
158 if let Ok(dt) = chrono::NaiveDate::parse_from_str(expiry_str, "%d%b%y") {
159 return Some(chrono::DateTime::from_naive_utc_and_offset(
160 dt.and_hms_opt(8, 0, 0).unwrap_or_default(), // Bybit delivery at 08:00 UTC
161 chrono::Utc,
162 ));
163 } else if let Ok(dt) = chrono::NaiveDate::parse_from_str(expiry_str, "%d%b%Y") {
164 return Some(chrono::DateTime::from_naive_utc_and_offset(
165 dt.and_hms_opt(8, 0, 0).unwrap_or_default(),
166 chrono::Utc,
167 ));
168 }
169 }
170 None
171 }
172
173 /// Returns true if this is a perpetual contract (no expiry).
174 pub fn is_perpetual(&self) -> bool {
175 !self.symbol.contains('-') || self.symbol.ends_with("USDT") || self.symbol.ends_with("USDC")
176 }
177
178 /// Returns true if this is a futures contract (has expiry).
179 pub fn is_futures(&self) -> bool {
180 !self.is_perpetual() && self.symbol.contains('-')
181 }
182}
183
184/// Represents a WebSocket price limit update event.
185///
186/// Contains real-time updates to order price limits for trading symbols.
187/// Push frequency: 300ms.
188///
189/// # Bybit API Reference
190/// Topic: `priceLimit.{symbol}` (e.g., `priceLimit.BTCUSDT`)
191#[derive(Serialize, Deserialize, Debug, Clone)]
192#[serde(rename_all = "camelCase")]
193pub struct PriceLimitUpdate {
194 /// The WebSocket topic for the event (e.g., "priceLimit.BTCUSDT").
195 ///
196 /// Specifies the data stream for the price limit update.
197 /// Bots use this to determine which symbol the update belongs to.
198 #[serde(rename = "topic")]
199 pub topic: String,
200
201 /// The timestamp of the event in milliseconds.
202 ///
203 /// Indicates when the price limit update was generated by the system.
204 /// Bots use this to ensure data freshness and time-based analysis.
205 #[serde(rename = "ts")]
206 pub timestamp: u64,
207
208 /// The price limit data.
209 ///
210 /// Contains the current buy and sell price limits for the symbol.
211 #[serde(rename = "data")]
212 pub data: PriceLimitData,
213}
214
215impl PriceLimitUpdate {
216 /// Extracts the symbol from the topic.
217 ///
218 /// Parses the WebSocket topic to extract the trading symbol.
219 /// Example: "priceLimit.BTCUSDT" -> "BTCUSDT"
220 pub fn symbol_from_topic(&self) -> Option<&str> {
221 self.topic.split('.').last()
222 }
223
224 /// Returns true if the symbol in the topic matches the symbol in the data.
225 ///
226 /// Validates data consistency between the topic and the embedded data.
227 pub fn is_consistent(&self) -> bool {
228 if let Some(topic_symbol) = self.symbol_from_topic() {
229 topic_symbol == self.data.symbol
230 } else {
231 false
232 }
233 }
234
235 /// Returns the timestamp as a chrono DateTime.
236 pub fn timestamp_datetime(&self) -> chrono::DateTime<chrono::Utc> {
237 chrono::DateTime::from_timestamp((self.timestamp / 1000) as i64, 0)
238 .unwrap_or_else(chrono::Utc::now)
239 }
240
241 /// Returns the age of the update in milliseconds.
242 ///
243 /// Calculates how old this update is relative to the current time.
244 pub fn age_ms(&self) -> u64 {
245 let now = chrono::Utc::now().timestamp_millis() as u64;
246 if now > self.timestamp {
247 now - self.timestamp
248 } else {
249 0
250 }
251 }
252
253 /// Returns true if the update is stale (older than 1 second).
254 ///
255 /// Since price limit updates are pushed every 300ms, data older than 1 second
256 /// might be considered stale for real-time trading decisions.
257 pub fn is_stale(&self) -> bool {
258 self.age_ms() > 1000
259 }
260
261 /// Returns the buy limit from the embedded data.
262 pub fn buy_limit(&self) -> f64 {
263 self.data.buy_lmt
264 }
265
266 /// Returns the sell limit from the embedded data.
267 pub fn sell_limit(&self) -> f64 {
268 self.data.sell_lmt
269 }
270
271 /// Returns the price range between buy and sell limits.
272 pub fn price_range(&self) -> f64 {
273 self.data.price_range()
274 }
275
276 /// Returns the mid price between buy and sell limits.
277 pub fn mid_price(&self) -> f64 {
278 self.data.mid_price()
279 }
280
281 /// Returns the price range as a percentage of the mid price.
282 pub fn price_range_percentage(&self) -> f64 {
283 self.data.price_range_percentage()
284 }
285
286 /// Checks if a buy price is within the allowed limit.
287 pub fn is_buy_price_allowed(&self, price: f64) -> bool {
288 self.data.is_buy_price_allowed(price)
289 }
290
291 /// Checks if a sell price is within the allowed limit.
292 pub fn is_sell_price_allowed(&self, price: f64) -> bool {
293 self.data.is_sell_price_allowed(price)
294 }
295
296 /// Returns the maximum allowable slippage for buy orders.
297 pub fn buy_slippage_limit(&self, reference_price: f64) -> f64 {
298 self.data.buy_slippage_limit(reference_price)
299 }
300
301 /// Returns the maximum allowable slippage for sell orders.
302 pub fn sell_slippage_limit(&self, reference_price: f64) -> f64 {
303 self.data.sell_slippage_limit(reference_price)
304 }
305
306 /// Returns a string representation of the update.
307 pub fn to_display_string(&self) -> String {
308 format!(
309 "[{}] {} (Age: {}ms)",
310 self.timestamp_datetime().format("%H:%M:%S%.3f"),
311 self.data.to_display_string(),
312 self.age_ms()
313 )
314 }
315
316 /// Validates the update for trading use.
317 ///
318 /// Returns `true` if:
319 /// 1. The topic and data symbols are consistent
320 /// 2. The price limits are valid (positive values)
321 /// 3. The update is not stale (≤ 1 second old)
322 /// 4. The sell limit is greater than the buy limit (normal market)
323 pub fn is_valid_for_trading(&self) -> bool {
324 self.is_consistent()
325 && self.data.is_valid()
326 && !self.is_stale()
327 && self.data.sell_lmt > self.data.buy_lmt
328 }
329
330 /// Returns the update latency in milliseconds.
331 ///
332 /// For comparing with other market data timestamps.
333 pub fn latency_ms(&self, other_timestamp: u64) -> i64 {
334 if self.timestamp > other_timestamp {
335 (self.timestamp - other_timestamp) as i64
336 } else {
337 (other_timestamp - self.timestamp) as i64
338 }
339 }
340}