Skip to main content

bybit_rust_api/ws/public/
kline.rs

1//! Kline/Candlestick stream — real-time OHLCV updates.
2//!
3//! # Topic format
4//! `kline.{interval}.{symbol}` — e.g. `kline.1.BTCUSDT`
5//!
6//! # Intervals
7//! 1, 3, 5, 15, 30, 60, 120, 240, 360, 720, D, W, M
8//!
9//! Each message contains a single kline (the current candle being updated).
10
11use serde::Deserialize;
12
13/// A single kline/candlestick update.
14#[derive(Debug, Clone, Deserialize)]
15pub struct KlineData {
16    /// Kline start timestamp in ms
17    #[serde(rename = "start")]
18    #[serde(default)]
19    pub start: Option<i64>,
20    /// Kline end timestamp in ms
21    #[serde(rename = "end")]
22    #[serde(default)]
23    pub end: Option<i64>,
24    /// Kline interval (e.g. "1", "5", "D")
25    #[serde(rename = "interval")]
26    #[serde(default)]
27    pub interval: Option<String>,
28    /// Open price
29    #[serde(rename = "open")]
30    #[serde(default)]
31    pub open: Option<String>,
32    /// Close price
33    #[serde(rename = "close")]
34    #[serde(default)]
35    pub close: Option<String>,
36    /// High price
37    #[serde(rename = "high")]
38    #[serde(default)]
39    pub high: Option<String>,
40    /// Low price
41    #[serde(rename = "low")]
42    #[serde(default)]
43    pub low: Option<String>,
44    /// Volume
45    #[serde(rename = "volume")]
46    #[serde(default)]
47    pub volume: Option<String>,
48    /// Turnover (USDT value)
49    #[serde(rename = "turnover")]
50    #[serde(default)]
51    pub turnover: Option<String>,
52    /// Whether this kline is confirmed (final) or still updating
53    #[serde(rename = "confirm")]
54    #[serde(default)]
55    pub confirm: Option<bool>,
56    /// Timestamp of this push
57    #[serde(rename = "timestamp")]
58    #[serde(default)]
59    pub timestamp: Option<i64>,
60}
61
62/// Typed wrapper for kline stream data.
63///
64/// Bybit sends an array with a single kline per message.
65pub struct KlineStream;
66
67impl KlineStream {
68    /// Parse raw WS data into a vector of klines (usually single-element).
69    pub fn parse(data: &serde_json::Value) -> serde_json::Result<Vec<KlineData>> {
70        serde_json::from_value(data.clone())
71    }
72
73    /// Parse and return the first kline (most common case).
74    pub fn parse_single(data: &serde_json::Value) -> serde_json::Result<KlineData> {
75        let mut klines: Vec<KlineData> = serde_json::from_value(data.clone())?;
76        klines
77            .pop()
78            .ok_or_else(|| serde::de::Error::custom("empty kline array"))
79    }
80
81    /// Check if the given topic matches a kline channel.
82    pub fn matches_topic(topic: &str) -> bool {
83        topic.starts_with("kline.")
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    #[test]
92    fn test_parse_kline() {
93        let json = serde_json::json!([{
94            "start": 1672828800000_i64,
95            "end": 1672832400000_i64,
96            "interval": "60",
97            "open": "50000.00",
98            "close": "50100.00",
99            "high": "50200.00",
100            "low": "49900.00",
101            "volume": "150.5",
102            "turnover": "7525000.00",
103            "confirm": false,
104            "timestamp": 1672832100000_i64
105        }]);
106
107        let kline = KlineStream::parse_single(&json).unwrap();
108        assert_eq!(kline.open.as_deref(), Some("50000.00"));
109        assert_eq!(kline.close.as_deref(), Some("50100.00"));
110        assert_eq!(kline.confirm, Some(false));
111    }
112
113    #[test]
114    fn test_matches_topic() {
115        assert!(KlineStream::matches_topic("kline.1.BTCUSDT"));
116        assert!(KlineStream::matches_topic("kline.D.ETHUSDT"));
117        assert!(!KlineStream::matches_topic("tickers.BTCUSDT"));
118    }
119}