ccxt_exchanges/bybit/
mod.rs

1//! Bybit exchange implementation.
2//!
3//! Supports spot trading and futures trading (USDT-M and Coin-M) with REST API and WebSocket support.
4//! Bybit uses V5 unified account API with HMAC-SHA256 authentication.
5
6use ccxt_core::{BaseExchange, ExchangeConfig, Result};
7use std::collections::HashMap;
8
9pub mod auth;
10pub mod builder;
11pub mod error;
12pub mod parser;
13pub mod rest;
14pub mod symbol;
15pub mod ws;
16mod ws_exchange_impl;
17
18pub use auth::BybitAuth;
19pub use builder::BybitBuilder;
20pub use error::{BybitErrorCode, is_error_response, parse_error};
21
22/// Bybit exchange structure.
23#[derive(Debug)]
24pub struct Bybit {
25    /// Base exchange instance.
26    base: BaseExchange,
27    /// Bybit-specific options.
28    options: BybitOptions,
29}
30
31/// Bybit-specific options.
32#[derive(Debug, Clone)]
33pub struct BybitOptions {
34    /// Account type: UNIFIED, CONTRACT, SPOT.
35    pub account_type: String,
36    /// Enables testnet environment.
37    pub testnet: bool,
38    /// Receive window in milliseconds.
39    pub recv_window: u64,
40}
41
42impl Default for BybitOptions {
43    fn default() -> Self {
44        Self {
45            account_type: "UNIFIED".to_string(),
46            testnet: false,
47            recv_window: 5000,
48        }
49    }
50}
51
52impl Bybit {
53    /// Creates a new Bybit instance using the builder pattern.
54    ///
55    /// This is the recommended way to create a Bybit instance.
56    ///
57    /// # Example
58    ///
59    /// ```no_run
60    /// use ccxt_exchanges::bybit::Bybit;
61    ///
62    /// let bybit = Bybit::builder()
63    ///     .api_key("your-api-key")
64    ///     .secret("your-secret")
65    ///     .testnet(true)
66    ///     .build()
67    ///     .unwrap();
68    /// ```
69    pub fn builder() -> BybitBuilder {
70        BybitBuilder::new()
71    }
72
73    /// Creates a new Bybit instance.
74    ///
75    /// # Arguments
76    ///
77    /// * `config` - Exchange configuration.
78    pub fn new(config: ExchangeConfig) -> Result<Self> {
79        let base = BaseExchange::new(config)?;
80        let options = BybitOptions::default();
81
82        Ok(Self { base, options })
83    }
84
85    /// Creates a new Bybit instance with custom options.
86    ///
87    /// This is used internally by the builder pattern.
88    ///
89    /// # Arguments
90    ///
91    /// * `config` - Exchange configuration.
92    /// * `options` - Bybit-specific options.
93    pub fn new_with_options(config: ExchangeConfig, options: BybitOptions) -> Result<Self> {
94        let base = BaseExchange::new(config)?;
95        Ok(Self { base, options })
96    }
97
98    /// Returns a reference to the base exchange.
99    pub fn base(&self) -> &BaseExchange {
100        &self.base
101    }
102
103    /// Returns a mutable reference to the base exchange.
104    pub fn base_mut(&mut self) -> &mut BaseExchange {
105        &mut self.base
106    }
107
108    /// Returns the Bybit options.
109    pub fn options(&self) -> &BybitOptions {
110        &self.options
111    }
112
113    /// Sets the Bybit options.
114    pub fn set_options(&mut self, options: BybitOptions) {
115        self.options = options;
116    }
117
118    /// Returns the exchange ID.
119    pub fn id(&self) -> &str {
120        "bybit"
121    }
122
123    /// Returns the exchange name.
124    pub fn name(&self) -> &str {
125        "Bybit"
126    }
127
128    /// Returns the API version.
129    pub fn version(&self) -> &str {
130        "v5"
131    }
132
133    /// Returns `true` if the exchange is CCXT-certified.
134    pub fn certified(&self) -> bool {
135        false
136    }
137
138    /// Returns `true` if Pro version (WebSocket) is supported.
139    pub fn pro(&self) -> bool {
140        true
141    }
142
143    /// Returns the rate limit in requests per second.
144    pub fn rate_limit(&self) -> f64 {
145        20.0
146    }
147
148    /// Returns the supported timeframes.
149    pub fn timeframes(&self) -> HashMap<String, String> {
150        let mut timeframes = HashMap::new();
151        timeframes.insert("1m".to_string(), "1".to_string());
152        timeframes.insert("3m".to_string(), "3".to_string());
153        timeframes.insert("5m".to_string(), "5".to_string());
154        timeframes.insert("15m".to_string(), "15".to_string());
155        timeframes.insert("30m".to_string(), "30".to_string());
156        timeframes.insert("1h".to_string(), "60".to_string());
157        timeframes.insert("2h".to_string(), "120".to_string());
158        timeframes.insert("4h".to_string(), "240".to_string());
159        timeframes.insert("6h".to_string(), "360".to_string());
160        timeframes.insert("12h".to_string(), "720".to_string());
161        timeframes.insert("1d".to_string(), "D".to_string());
162        timeframes.insert("1w".to_string(), "W".to_string());
163        timeframes.insert("1M".to_string(), "M".to_string());
164        timeframes
165    }
166
167    /// Returns the API URLs.
168    pub fn urls(&self) -> BybitUrls {
169        if self.base().config.sandbox || self.options.testnet {
170            BybitUrls::testnet()
171        } else {
172            BybitUrls::production()
173        }
174    }
175
176    /// Creates a public WebSocket client.
177    ///
178    /// # Returns
179    ///
180    /// Returns a `BybitWs` instance for public data streams.
181    ///
182    /// # Example
183    /// ```no_run
184    /// use ccxt_exchanges::bybit::Bybit;
185    /// use ccxt_core::ExchangeConfig;
186    ///
187    /// # async fn example() -> ccxt_core::error::Result<()> {
188    /// let bybit = Bybit::new(ExchangeConfig::default())?;
189    /// let ws = bybit.create_ws();
190    /// ws.connect().await?;
191    /// # Ok(())
192    /// # }
193    /// ```
194    pub fn create_ws(&self) -> ws::BybitWs {
195        let urls = self.urls();
196        ws::BybitWs::new(urls.ws_public)
197    }
198}
199
200/// Bybit API URLs.
201#[derive(Debug, Clone)]
202pub struct BybitUrls {
203    /// REST API base URL.
204    pub rest: String,
205    /// Public WebSocket URL.
206    pub ws_public: String,
207    /// Private WebSocket URL.
208    pub ws_private: String,
209}
210
211impl BybitUrls {
212    /// Returns production environment URLs.
213    pub fn production() -> Self {
214        Self {
215            rest: "https://api.bybit.com".to_string(),
216            ws_public: "wss://stream.bybit.com/v5/public/spot".to_string(),
217            ws_private: "wss://stream.bybit.com/v5/private".to_string(),
218        }
219    }
220
221    /// Returns testnet environment URLs.
222    pub fn testnet() -> Self {
223        Self {
224            rest: "https://api-testnet.bybit.com".to_string(),
225            ws_public: "wss://stream-testnet.bybit.com/v5/public/spot".to_string(),
226            ws_private: "wss://stream-testnet.bybit.com/v5/private".to_string(),
227        }
228    }
229}
230
231#[cfg(test)]
232mod tests {
233    use super::*;
234
235    #[test]
236    fn test_bybit_creation() {
237        let config = ExchangeConfig {
238            id: "bybit".to_string(),
239            name: "Bybit".to_string(),
240            ..Default::default()
241        };
242
243        let bybit = Bybit::new(config);
244        assert!(bybit.is_ok());
245
246        let bybit = bybit.unwrap();
247        assert_eq!(bybit.id(), "bybit");
248        assert_eq!(bybit.name(), "Bybit");
249        assert_eq!(bybit.version(), "v5");
250        assert!(!bybit.certified());
251        assert!(bybit.pro());
252    }
253
254    #[test]
255    fn test_timeframes() {
256        let config = ExchangeConfig::default();
257        let bybit = Bybit::new(config).unwrap();
258        let timeframes = bybit.timeframes();
259
260        assert!(timeframes.contains_key("1m"));
261        assert!(timeframes.contains_key("1h"));
262        assert!(timeframes.contains_key("1d"));
263        assert_eq!(timeframes.len(), 13);
264    }
265
266    #[test]
267    fn test_urls() {
268        let config = ExchangeConfig::default();
269        let bybit = Bybit::new(config).unwrap();
270        let urls = bybit.urls();
271
272        assert!(urls.rest.contains("api.bybit.com"));
273        assert!(urls.ws_public.contains("stream.bybit.com"));
274    }
275
276    #[test]
277    fn test_testnet_urls() {
278        let config = ExchangeConfig {
279            sandbox: true,
280            ..Default::default()
281        };
282        let bybit = Bybit::new(config).unwrap();
283        let urls = bybit.urls();
284
285        assert!(urls.rest.contains("api-testnet.bybit.com"));
286        assert!(urls.ws_public.contains("stream-testnet.bybit.com"));
287    }
288
289    #[test]
290    fn test_default_options() {
291        let options = BybitOptions::default();
292        assert_eq!(options.account_type, "UNIFIED");
293        assert!(!options.testnet);
294        assert_eq!(options.recv_window, 5000);
295    }
296}