Skip to main content

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}