Skip to main content

bybit/models/
ws_ticker.rs

1use chrono::{DateTime, Utc};
2
3use crate::models::linear_ticker::LinearTickerData;
4use crate::prelude::*;
5
6/// WebSocket ticker message from Bybit.
7///
8/// This struct represents a ticker update received via WebSocket for either linear perpetual futures or spot markets. Bots use this to process real-time market data for trading decisions, including price tracking, volume analysis, and funding rate monitoring.
9///
10/// # Bybit API Reference
11/// The Bybit WebSocket API (https://bybit-exchange.github.io/docs/v5/ws/connect) streams ticker data for subscribed topics. This struct deserializes the JSON payload into a structured format for bot consumption.
12///
13/// # Perpetual Futures Context
14/// For perpetual futures, ticker data includes funding rates, open interest, and mark/index prices, which are critical for bots managing positions and monitoring funding costs. Spot ticker data provides reference prices for arbitrage strategies.
15#[derive(Debug, Serialize, Deserialize, Clone)]
16pub struct WsTicker {
17    /// The topic (channel) of the WebSocket message.
18    ///
19    /// Identifies the type of data stream (e.g., "tickers.BTCUSDT"). Bots use this to route messages to appropriate handlers.
20    pub topic: String,
21
22    /// The event type (e.g., "snapshot", "delta").
23    ///
24    /// Indicates whether the message is a full snapshot or incremental update. Bots use this to determine how to update their internal state.
25    #[serde(rename = "type")]
26    pub event_type: String,
27
28    /// The ticker data payload.
29    ///
30    /// Contains market-specific metrics for linear perpetuals or spot markets. Bots extract fields like last price, volume, and funding rates from this data.
31    pub data: Ticker,
32
33    /// The cross sequence identifier.
34    ///
35    /// Used to ensure message ordering and detect gaps in the data stream. Bots can use this to validate data integrity.
36    #[serde(rename = "cs")]
37    pub cs: i64,
38
39    /// The timestamp in milliseconds.
40    ///
41    /// The server timestamp when the message was generated. Bots use this to calculate latency and age of market data.
42    #[serde(rename = "ts")]
43    pub ts: u64,
44}
45
46impl WsTicker {
47    /// Creates a new `WsTicker` instance.
48    ///
49    /// Bots use this constructor when synthesizing ticker data for testing or simulation purposes.
50    pub fn new(topic: String, event_type: String, data: Ticker, cs: i64, ts: u64) -> Self {
51        Self {
52            topic,
53            event_type,
54            data,
55            cs,
56            ts,
57        }
58    }
59
60    /// Extracts the symbol from the ticker data.
61    ///
62    /// Returns the trading pair symbol (e.g., "BTCUSDT"). Bots use this to identify the market for the ticker update.
63    pub fn symbol(&self) -> &str {
64        match &self.data {
65            Ticker::Linear(linear_data) => match linear_data {
66                LinearTickerData::Snapshot(snapshot) => &snapshot.symbol,
67                LinearTickerData::Delta(delta) => &delta.symbol,
68            },
69            Ticker::Spot(spot_data) => &spot_data.symbol,
70            Ticker::Options(options_ticker) => &options_ticker.symbol,
71            Ticker::Futures(futures_ticker) => &futures_ticker.symbol,
72        }
73    }
74
75    /// Checks if the ticker data is a snapshot.
76    ///
77    /// Returns `true` if the event type is "snapshot". Bots use this to determine whether to replace or update their market data.
78    pub fn is_snapshot(&self) -> bool {
79        self.event_type == "snapshot"
80    }
81
82    /// Checks if the ticker data is a delta (incremental update).
83    ///
84    /// Returns `true` if the event type is "delta". Bots use this to apply incremental updates to their market data.
85    pub fn is_delta(&self) -> bool {
86        self.event_type == "delta"
87    }
88
89    /// Converts the timestamp to a `DateTime<Utc>`.
90    ///
91    /// Returns a UTC datetime representation of the timestamp. Bots use this for time-based analysis and logging.
92    pub fn timestamp_datetime(&self) -> DateTime<Utc> {
93        let secs = (self.ts / 1000) as i64;
94        let nanos = ((self.ts % 1000) * 1_000_000) as u32;
95        DateTime::from_timestamp(secs, nanos).unwrap_or_else(|| Utc::now())
96    }
97
98    /// Calculates the age of the ticker data in milliseconds.
99    ///
100    /// Returns the time elapsed since the ticker was generated. Bots use this to filter stale data and monitor latency.
101    pub fn age_ms(&self) -> u64 {
102        let now = Utc::now().timestamp_millis() as u64;
103        now.saturating_sub(self.ts)
104    }
105
106    /// Checks if the ticker data is stale.
107    ///
108    /// Returns `true` if the data is older than 5000ms (5 seconds). Bots use this to avoid acting on outdated market information.
109    pub fn is_stale(&self) -> bool {
110        self.age_ms() > 5000
111    }
112
113    /// Extracts the last traded price.
114    ///
115    /// Returns the most recent trade price, or `None` if not available. Bots use this for real-time price tracking and order execution.
116    pub fn last_price(&self) -> Option<f64> {
117        match &self.data {
118            Ticker::Linear(linear_data) => match linear_data {
119                LinearTickerData::Snapshot(snapshot) => Some(snapshot.last_price),
120                LinearTickerData::Delta(delta) => delta.last_price,
121            },
122            Ticker::Spot(spot_data) => Some(spot_data.last_price),
123            Ticker::Options(options_ticker) => options_ticker.last_price_f64(),
124            Ticker::Futures(futures_ticker) => Some(futures_ticker.last_price),
125        }
126    }
127
128    /// Extracts the 24-hour price change percentage.
129    ///
130    /// Returns the percentage price change over the last 24 hours, or `None` if not available. Bots use this to assess market trends and volatility.
131    pub fn price_change_24h(&self) -> Option<f64> {
132        match &self.data {
133            Ticker::Linear(linear_data) => match linear_data {
134                LinearTickerData::Snapshot(snapshot) => Some(snapshot.price_24h_pcnt),
135                LinearTickerData::Delta(delta) => delta.price_24h_pcnt,
136            },
137            Ticker::Spot(spot_data) => Some(spot_data.price_24h_pcnt),
138            Ticker::Options(options_ticker) => options_ticker.change_24h_f64(),
139            Ticker::Futures(futures_ticker) => Some(futures_ticker.daily_change_percentage),
140        }
141    }
142
143    /// Extracts the 24-hour high price.
144    ///
145    /// Returns the highest price in the last 24 hours, or `None` if not available. Bots use this to identify resistance levels and assess volatility.
146    pub fn high_24h(&self) -> Option<f64> {
147        match &self.data {
148            Ticker::Linear(linear_data) => match linear_data {
149                LinearTickerData::Snapshot(snapshot) => Some(snapshot.high_price_24h),
150                LinearTickerData::Delta(delta) => delta.high_price_24h,
151            },
152            Ticker::Spot(spot_data) => Some(spot_data.high_price_24h),
153            Ticker::Options(options_ticker) => options_ticker.high_price_24h_f64(),
154            Ticker::Futures(futures_ticker) => Some(futures_ticker.high_24h),
155        }
156    }
157
158    /// Extracts the 24-hour low price.
159    ///
160    /// Returns the lowest price in the last 24 hours, or `None` if not available. Bots use this to identify support levels and assess volatility.
161    pub fn low_24h(&self) -> Option<f64> {
162        match &self.data {
163            Ticker::Linear(linear_data) => match linear_data {
164                LinearTickerData::Snapshot(snapshot) => Some(snapshot.low_price_24h),
165                LinearTickerData::Delta(delta) => delta.low_price_24h,
166            },
167            Ticker::Spot(spot_data) => Some(spot_data.low_price_24h),
168            Ticker::Options(options_ticker) => options_ticker.low_price_24h_f64(),
169            Ticker::Futures(futures_ticker) => Some(futures_ticker.low_24h),
170        }
171    }
172
173    /// Extracts the 24-hour trading volume.
174    ///
175    /// Returns the trading volume in the last 24 hours, or `None` if not available. Bots use this to analyze market activity and liquidity.
176    pub fn volume_24h(&self) -> Option<f64> {
177        match &self.data {
178            Ticker::Linear(linear_data) => match linear_data {
179                LinearTickerData::Snapshot(snapshot) => Some(snapshot.volume_24h),
180                LinearTickerData::Delta(delta) => delta.volume_24h,
181            },
182            Ticker::Spot(spot_data) => Some(spot_data.volume_24h),
183            Ticker::Options(options_ticker) => options_ticker.volume_24h_f64(),
184            Ticker::Futures(futures_ticker) => Some(futures_ticker.volume_24h),
185        }
186    }
187
188    /// Extracts the 24-hour trading turnover.
189    ///
190    /// Returns the trading value in the last 24 hours, or `None` if not available. Bots use this to assess market activity and liquidity in monetary terms.
191    pub fn turnover_24h(&self) -> Option<f64> {
192        match &self.data {
193            Ticker::Linear(linear_data) => match linear_data {
194                LinearTickerData::Snapshot(snapshot) => Some(snapshot.turnover_24h),
195                LinearTickerData::Delta(delta) => delta.turnover_24h,
196            },
197            Ticker::Spot(spot_data) => Some(spot_data.turnover_24h),
198            Ticker::Options(options_ticker) => options_ticker.turnover_24h_f64(),
199            Ticker::Futures(futures_ticker) => Some(futures_ticker.turnover_24h),
200        }
201    }
202
203    /// Extracts the funding rate (linear perpetuals only).
204    ///
205    /// Returns the funding rate for linear perpetuals, or `None` for spot markets. Bots use this to monitor funding costs and arbitrage opportunities.
206    pub fn funding_rate(&self) -> Option<f64> {
207        match &self.data {
208            Ticker::Linear(linear_data) => match linear_data {
209                LinearTickerData::Snapshot(snapshot) => Some(snapshot.funding_rate),
210                LinearTickerData::Delta(delta) => delta.funding_rate,
211            },
212            Ticker::Spot(_) => None,
213            Ticker::Options(_) => None,
214            Ticker::Futures(futures_ticker) => futures_ticker.funding_rate_f64(),
215        }
216    }
217
218    /// Extracts the next funding time (linear perpetuals only).
219    ///
220    /// Returns the timestamp of the next funding settlement, or `None` for spot markets. Bots use this to schedule funding-related operations.
221    pub fn next_funding_time(&self) -> Option<u64> {
222        match &self.data {
223            Ticker::Linear(linear_data) => match linear_data {
224                LinearTickerData::Snapshot(snapshot) => Some(snapshot.next_funding_time),
225                LinearTickerData::Delta(delta) => delta.next_funding_time,
226            },
227            Ticker::Spot(_) => None,
228            Ticker::Options(_) => None,
229            Ticker::Futures(futures_ticker) => Some(futures_ticker.next_funding_time),
230        }
231    }
232
233    /// Extracts the best bid price.
234    ///
235    /// Returns the highest bid price in the order book, or `None` if not available. Bots use this for spread calculations and liquidity assessment.
236    pub fn bid_price(&self) -> Option<f64> {
237        match &self.data {
238            Ticker::Linear(linear_data) => match linear_data {
239                LinearTickerData::Snapshot(snapshot) => Some(snapshot.bid_price),
240                LinearTickerData::Delta(delta) => delta.bid_price,
241            },
242            Ticker::Spot(_) => None, // Spot ticker doesn't have bid/ask prices
243            Ticker::Options(options_ticker) => options_ticker.bid1_price_f64(),
244            Ticker::Futures(futures_ticker) => Some(futures_ticker.bid_price),
245        }
246    }
247
248    /// Extracts the best bid size.
249    ///
250    /// Returns the quantity available at the best bid price, or `None` if not available. Bots use this to evaluate buy-side liquidity.
251    pub fn bid_size(&self) -> Option<f64> {
252        match &self.data {
253            Ticker::Linear(linear_data) => match linear_data {
254                LinearTickerData::Snapshot(snapshot) => Some(snapshot.bid_size),
255                LinearTickerData::Delta(delta) => delta.bid_size,
256            },
257            Ticker::Spot(_) => None, // Spot ticker doesn't have bid/ask sizes
258            Ticker::Options(options_ticker) => options_ticker.bid1_size_f64(),
259            Ticker::Futures(futures_ticker) => Some(futures_ticker.bid_size),
260        }
261    }
262
263    /// Extracts the best ask price.
264    ///
265    /// Returns the lowest ask price in the order book, or `None` if not available. Bots use this for spread calculations and liquidity assessment.
266    pub fn ask_price(&self) -> Option<f64> {
267        match &self.data {
268            Ticker::Linear(linear_data) => match linear_data {
269                LinearTickerData::Snapshot(snapshot) => Some(snapshot.ask_price),
270                LinearTickerData::Delta(delta) => delta.ask_price,
271            },
272            Ticker::Spot(_) => None, // Spot ticker doesn't have bid/ask prices
273            Ticker::Options(options_ticker) => options_ticker.ask1_price_f64(),
274            Ticker::Futures(futures_ticker) => Some(futures_ticker.ask_price),
275        }
276    }
277
278    /// Extracts the best ask size.
279    ///
280    /// Returns the quantity available at the best ask price, or `None` if not available. Bots use this to evaluate sell-side liquidity.
281    pub fn ask_size(&self) -> Option<f64> {
282        match &self.data {
283            Ticker::Linear(linear_data) => match linear_data {
284                LinearTickerData::Snapshot(snapshot) => Some(snapshot.ask_size),
285                LinearTickerData::Delta(delta) => delta.ask_size,
286            },
287            Ticker::Spot(_) => None, // Spot ticker doesn't have bid/ask sizes
288            Ticker::Options(options_ticker) => options_ticker.ask1_size_f64(),
289            Ticker::Futures(futures_ticker) => Some(futures_ticker.ask_size),
290        }
291    }
292
293    /// Calculates the bid-ask spread.
294    ///
295    /// Returns the difference between ask and bid prices, or `None` if either is missing. Bots use this to assess market liquidity and trading costs.
296    pub fn spread(&self) -> Option<f64> {
297        match (self.ask_price(), self.bid_price()) {
298            (Some(ask), Some(bid)) => Some(ask - bid),
299            _ => None,
300        }
301    }
302
303    /// Calculates the mid price.
304    ///
305    /// Returns the average of bid and ask prices, or `None` if either is missing. Bots use this as a reference price for market analysis.
306    pub fn mid_price(&self) -> Option<f64> {
307        match (self.ask_price(), self.bid_price()) {
308            (Some(ask), Some(bid)) => Some((ask + bid) / 2.0),
309            _ => None,
310        }
311    }
312
313    /// Calculates the spread as a percentage of the mid price.
314    ///
315    /// Returns the relative spread (spread / mid price), or `None` if insufficient data. Bots use this to compare liquidity across different markets.
316    pub fn spread_percentage(&self) -> Option<f64> {
317        match (self.spread(), self.mid_price()) {
318            (Some(spread), Some(mid)) if mid > 0.0 => Some(spread / mid * 100.0),
319            _ => None,
320        }
321    }
322
323    /// Extracts the open interest (linear perpetuals only).
324    ///
325    /// Returns the total number of open contracts, or `None` for spot markets. Bots use this to gauge market sentiment and positioning.
326    pub fn open_interest(&self) -> Option<f64> {
327        match &self.data {
328            Ticker::Linear(linear_data) => match linear_data {
329                LinearTickerData::Snapshot(snapshot) => Some(snapshot.open_interest),
330                LinearTickerData::Delta(delta) => delta.open_interest,
331            },
332            Ticker::Spot(_) => None,
333            Ticker::Options(options_ticker) => options_ticker.open_interest_f64(),
334            Ticker::Futures(futures_ticker) => Some(futures_ticker.open_interest),
335        }
336    }
337
338    /// Extracts the open interest value (linear perpetuals only).
339    ///
340    /// Returns the monetary value of open interest, or `None` for spot markets. Bots use this to assess market exposure and leverage levels.
341    pub fn open_interest_value(&self) -> Option<f64> {
342        match &self.data {
343            Ticker::Linear(linear_data) => match linear_data {
344                LinearTickerData::Snapshot(snapshot) => Some(snapshot.open_interest_value),
345                LinearTickerData::Delta(delta) => delta.open_interest_value,
346            },
347            Ticker::Spot(_) => None,
348            Ticker::Options(_) => None, // Options ticker doesn't have open interest value
349            Ticker::Futures(futures_ticker) => Some(futures_ticker.open_interest_value),
350        }
351    }
352
353    /// Extracts the mark price.
354    ///
355    /// Returns the mark price used for liquidation calculations, or `None` if not available. Bots use this to monitor position health and avoid liquidation.
356    pub fn mark_price(&self) -> Option<f64> {
357        match &self.data {
358            Ticker::Linear(linear_data) => match linear_data {
359                LinearTickerData::Snapshot(snapshot) => Some(snapshot.mark_price),
360                LinearTickerData::Delta(delta) => delta.mark_price,
361            },
362            Ticker::Spot(_) => None, // Spot ticker doesn't have mark price
363            Ticker::Options(options_ticker) => options_ticker.mark_price_f64(),
364            Ticker::Futures(futures_ticker) => Some(futures_ticker.mark_price),
365        }
366    }
367
368    /// Extracts the index price.
369    ///
370    /// Returns the underlying index price, or `None` if not available. Bots use this to monitor basis (futures-spot spread) for arbitrage opportunities.
371    pub fn index_price(&self) -> Option<f64> {
372        match &self.data {
373            Ticker::Linear(linear_data) => match linear_data {
374                LinearTickerData::Snapshot(snapshot) => Some(snapshot.index_price),
375                LinearTickerData::Delta(delta) => delta.index_price,
376            },
377            Ticker::Spot(spot_data) => Some(spot_data.usd_index_price),
378            Ticker::Options(options_ticker) => options_ticker.index_price_f64(),
379            Ticker::Futures(futures_ticker) => Some(futures_ticker.index_price),
380        }
381    }
382
383    /// Returns true if the ticker data is valid for trading decisions.
384    ///
385    /// Checks that the data is not stale and has essential fields like symbol and last price. Bots use this to filter out invalid or incomplete market data.
386    pub fn is_valid_for_trading(&self) -> bool {
387        !self.is_stale() && self.last_price().is_some()
388    }
389
390    /// Returns a summary string for this ticker update.
391    ///
392    /// Provides a human-readable summary of key ticker metrics. Bots use this for logging and debugging purposes.
393    pub fn to_summary_string(&self) -> String {
394        let symbol = self.symbol();
395        let last_price = self
396            .last_price()
397            .map(|p| format!("{:.2}", p))
398            .unwrap_or_else(|| "N/A".to_string());
399        let change_24h = self
400            .price_change_24h()
401            .map(|c| format!("{:+.2}%", c * 100.0))
402            .unwrap_or_else(|| "N/A".to_string());
403        let volume = self
404            .volume_24h()
405            .map(|v| format!("{:.2}", v))
406            .unwrap_or_else(|| "N/A".to_string());
407
408        format!(
409            "[{}] {}: Last={}, 24h Change={}, Volume={}",
410            self.timestamp_datetime().format("%H:%M:%S"),
411            symbol,
412            last_price,
413            change_24h,
414            volume
415        )
416    }
417
418    /// Returns the price change amount over 24 hours.
419    ///
420    /// Calculates the absolute price change based on last price and 24-hour percentage change. Bots use this for volatility-based strategies and risk management.
421    pub fn price_change_amount_24h(&self) -> Option<f64> {
422        match (self.last_price(), self.price_change_24h()) {
423            (Some(price), Some(change_pct)) => Some(price * change_pct),
424            _ => None,
425        }
426    }
427
428    /// Returns true if the price has increased over 24 hours.
429    ///
430    /// Checks if the 24-hour price change percentage is positive. Bots use this for trend-following strategies and market sentiment analysis.
431    pub fn is_price_up_24h(&self) -> bool {
432        self.price_change_24h().map(|c| c > 0.0).unwrap_or(false)
433    }
434
435    /// Returns true if the price has decreased over 24 hours.
436    ///
437    /// Checks if the 24-hour price change percentage is negative. Bots use this for trend-following strategies and market sentiment analysis.
438    pub fn is_price_down_24h(&self) -> bool {
439        self.price_change_24h().map(|c| c < 0.0).unwrap_or(false)
440    }
441
442    /// Returns the price range over 24 hours.
443    ///
444    /// Calculates the difference between 24-hour high and low prices. Bots use this to assess volatility and set stop-loss/take-profit levels.
445    pub fn price_range_24h(&self) -> Option<f64> {
446        match (self.high_24h(), self.low_24h()) {
447            (Some(high), Some(low)) => Some(high - low),
448            _ => None,
449        }
450    }
451
452    /// Returns the current price position within the 24-hour range.
453    ///
454    /// Returns a value between 0.0 (at 24-hour low) and 1.0 (at 24-hour high). Bots use this for mean-reversion strategies and overbought/oversold indicators.
455    pub fn price_position_in_range(&self) -> Option<f64> {
456        match (self.last_price(), self.high_24h(), self.low_24h()) {
457            (Some(price), Some(high), Some(low)) if high > low => {
458                Some((price - low) / (high - low))
459            }
460            _ => None,
461        }
462    }
463
464    /// Returns the volume-weighted average price over 24 hours.
465    ///
466    /// Calculates VWAP using 24-hour turnover and volume. Bots use this as a benchmark price for execution quality assessment.
467    pub fn vwap_24h(&self) -> Option<f64> {
468        match (self.turnover_24h(), self.volume_24h()) {
469            (Some(turnover), Some(volume)) if volume > 0.0 => Some(turnover / volume),
470            _ => None,
471        }
472    }
473
474    /// Returns the premium/discount to index price.
475    ///
476    /// Calculates the percentage difference between last price and index price. Bots use this for arbitrage strategies between spot and futures markets.
477    pub fn premium_to_index(&self) -> Option<f64> {
478        match (self.last_price(), self.index_price()) {
479            (Some(last), Some(index)) if index > 0.0 => Some((last - index) / index * 100.0),
480            _ => None,
481        }
482    }
483
484    /// Returns the funding rate annualized.
485    ///
486    /// Converts the funding rate to an annualized percentage. Bots use this to compare funding costs across different timeframes and markets.
487    pub fn funding_rate_annualized(&self) -> Option<f64> {
488        self.funding_rate().map(|rate| rate * 3.0 * 365.0)
489    }
490
491    /// Validates the checksum against the ticker data.
492    ///
493    /// Note: This is a placeholder implementation. Actual checksum validation
494    /// would require the original message bytes. Bots should implement proper
495    /// checksum validation for production use.
496    pub fn validate_checksum(&self) -> bool {
497        // In a real implementation, this would validate the checksum
498        // against the actual data. For now, we assume it's valid.
499        true
500    }
501}