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}