ccxt_exchanges/bybit/
builder.rs

1//! Bybit exchange builder pattern implementation.
2//!
3//! Provides a fluent API for constructing Bybit exchange instances with
4//! type-safe configuration options.
5
6use super::{Bybit, BybitOptions};
7use ccxt_core::types::default_type::{DefaultSubType, DefaultType};
8use ccxt_core::{ExchangeConfig, Result};
9use serde_json::Value;
10use std::collections::HashMap;
11
12/// Builder for creating Bybit exchange instances.
13///
14/// Provides a fluent API for configuring all aspects of the Bybit exchange,
15/// including authentication, connection settings, and Bybit-specific options.
16///
17/// # Example
18///
19/// ```no_run
20/// use ccxt_exchanges::bybit::BybitBuilder;
21///
22/// let bybit = BybitBuilder::new()
23///     .api_key("your-api-key")
24///     .secret("your-secret")
25///     .testnet(true)
26///     .timeout(30)
27///     .build()
28///     .unwrap();
29/// ```
30#[derive(Debug, Clone)]
31pub struct BybitBuilder {
32    /// Exchange configuration
33    config: ExchangeConfig,
34    /// Bybit-specific options
35    options: BybitOptions,
36}
37
38impl Default for BybitBuilder {
39    fn default() -> Self {
40        Self::new()
41    }
42}
43
44impl BybitBuilder {
45    /// Creates a new builder with default configuration.
46    ///
47    /// # Example
48    ///
49    /// ```no_run
50    /// use ccxt_exchanges::bybit::BybitBuilder;
51    ///
52    /// let builder = BybitBuilder::new();
53    /// ```
54    pub fn new() -> Self {
55        Self {
56            config: ExchangeConfig {
57                id: "bybit".to_string(),
58                name: "Bybit".to_string(),
59                ..Default::default()
60            },
61            options: BybitOptions::default(),
62        }
63    }
64
65    /// Sets the API key for authentication.
66    ///
67    /// # Arguments
68    ///
69    /// * `key` - The API key string.
70    pub fn api_key(mut self, key: impl Into<String>) -> Self {
71        self.config.api_key = Some(key.into());
72        self
73    }
74
75    /// Sets the API secret for authentication.
76    ///
77    /// # Arguments
78    ///
79    /// * `secret` - The API secret string.
80    pub fn secret(mut self, secret: impl Into<String>) -> Self {
81        self.config.secret = Some(secret.into());
82        self
83    }
84
85    /// Enables or disables sandbox/testnet mode.
86    ///
87    /// When enabled, the exchange will connect to Bybit's testnet
88    /// environment instead of the production environment.
89    ///
90    /// This method is equivalent to `testnet()` and is provided for
91    /// consistency with other exchanges.
92    ///
93    /// # Arguments
94    ///
95    /// * `enabled` - Whether to enable sandbox mode.
96    pub fn sandbox(mut self, enabled: bool) -> Self {
97        self.config.sandbox = enabled;
98        self.options.testnet = enabled;
99        self
100    }
101
102    /// Enables or disables testnet mode.
103    ///
104    /// When enabled, the exchange will connect to Bybit's testnet
105    /// environment instead of the production environment.
106    ///
107    /// This method is equivalent to `sandbox()` and is provided for
108    /// backward compatibility.
109    ///
110    /// # Arguments
111    ///
112    /// * `enabled` - Whether to enable testnet mode.
113    pub fn testnet(mut self, enabled: bool) -> Self {
114        self.config.sandbox = enabled;
115        self.options.testnet = enabled;
116        self
117    }
118
119    /// Sets the account type for trading.
120    ///
121    /// Valid values: "UNIFIED", "CONTRACT", "SPOT".
122    ///
123    /// # Arguments
124    ///
125    /// * `account_type` - The account type string.
126    pub fn account_type(mut self, account_type: impl Into<String>) -> Self {
127        self.options.account_type = account_type.into();
128        self
129    }
130
131    /// Sets the default market type for trading.
132    ///
133    /// This determines which category to use for API calls.
134    /// Bybit uses a unified V5 API with category-based filtering:
135    /// - `Spot` -> category=spot
136    /// - `Swap` + Linear -> category=linear
137    /// - `Swap` + Inverse -> category=inverse
138    /// - `Option` -> category=option
139    ///
140    /// # Arguments
141    ///
142    /// * `default_type` - The default market type (spot, swap, futures, option).
143    ///
144    /// # Example
145    ///
146    /// ```no_run
147    /// use ccxt_exchanges::bybit::BybitBuilder;
148    /// use ccxt_core::types::default_type::DefaultType;
149    ///
150    /// let bybit = BybitBuilder::new()
151    ///     .default_type(DefaultType::Swap)
152    ///     .build()
153    ///     .unwrap();
154    /// ```
155    pub fn default_type(mut self, default_type: impl Into<DefaultType>) -> Self {
156        self.options.default_type = default_type.into();
157        self
158    }
159
160    /// Sets the default sub-type for contract settlement.
161    ///
162    /// - `Linear`: USDT-margined contracts (category=linear)
163    /// - `Inverse`: Coin-margined contracts (category=inverse)
164    ///
165    /// Only applicable when `default_type` is `Swap` or `Futures`.
166    /// Ignored for `Spot` and `Option` types.
167    ///
168    /// # Arguments
169    ///
170    /// * `sub_type` - The contract settlement type (linear or inverse).
171    ///
172    /// # Example
173    ///
174    /// ```no_run
175    /// use ccxt_exchanges::bybit::BybitBuilder;
176    /// use ccxt_core::types::default_type::{DefaultType, DefaultSubType};
177    ///
178    /// let bybit = BybitBuilder::new()
179    ///     .default_type(DefaultType::Swap)
180    ///     .default_sub_type(DefaultSubType::Linear)
181    ///     .build()
182    ///     .unwrap();
183    /// ```
184    pub fn default_sub_type(mut self, sub_type: DefaultSubType) -> Self {
185        self.options.default_sub_type = Some(sub_type);
186        self
187    }
188
189    /// Sets the receive window for signed requests.
190    ///
191    /// The receive window specifies how long a request is valid after
192    /// the timestamp. Default is 5000 milliseconds.
193    ///
194    /// # Arguments
195    ///
196    /// * `millis` - Receive window in milliseconds.
197    pub fn recv_window(mut self, millis: u64) -> Self {
198        self.options.recv_window = millis;
199        self
200    }
201
202    /// Sets the request timeout in seconds.
203    ///
204    /// # Arguments
205    ///
206    /// * `seconds` - Timeout duration in seconds.
207    pub fn timeout(mut self, seconds: u64) -> Self {
208        self.config.timeout = seconds;
209        self
210    }
211
212    /// Enables or disables rate limiting.
213    ///
214    /// # Arguments
215    ///
216    /// * `enabled` - Whether to enable rate limiting.
217    pub fn enable_rate_limit(mut self, enabled: bool) -> Self {
218        self.config.enable_rate_limit = enabled;
219        self
220    }
221
222    /// Sets the HTTP proxy server URL.
223    ///
224    /// # Arguments
225    ///
226    /// * `proxy` - The proxy server URL.
227    pub fn proxy(mut self, proxy: impl Into<String>) -> Self {
228        self.config.proxy = Some(proxy.into());
229        self
230    }
231
232    /// Enables or disables verbose logging.
233    ///
234    /// # Arguments
235    ///
236    /// * `enabled` - Whether to enable verbose logging.
237    pub fn verbose(mut self, enabled: bool) -> Self {
238        self.config.verbose = enabled;
239        self
240    }
241
242    /// Sets a custom option.
243    ///
244    /// # Arguments
245    ///
246    /// * `key` - Option key.
247    /// * `value` - Option value as JSON.
248    pub fn option(mut self, key: impl Into<String>, value: Value) -> Self {
249        self.config.options.insert(key.into(), value);
250        self
251    }
252
253    /// Sets multiple custom options.
254    ///
255    /// # Arguments
256    ///
257    /// * `options` - HashMap of option key-value pairs.
258    pub fn options(mut self, options: HashMap<String, Value>) -> Self {
259        self.config.options.extend(options);
260        self
261    }
262
263    /// Returns the current configuration (for testing purposes).
264    #[cfg(test)]
265    pub fn get_config(&self) -> &ExchangeConfig {
266        &self.config
267    }
268
269    /// Returns the current options (for testing purposes).
270    #[cfg(test)]
271    pub fn get_options(&self) -> &BybitOptions {
272        &self.options
273    }
274
275    /// Builds the Bybit exchange instance.
276    ///
277    /// # Returns
278    ///
279    /// Returns a `Result` containing the configured `Bybit` instance.
280    ///
281    /// # Errors
282    ///
283    /// Returns an error if the exchange cannot be initialized.
284    pub fn build(self) -> Result<Bybit> {
285        Bybit::new_with_options(self.config, self.options)
286    }
287}
288
289#[cfg(test)]
290mod tests {
291    use super::*;
292
293    #[test]
294    fn test_builder_default() {
295        let builder = BybitBuilder::new();
296        assert_eq!(builder.config.id, "bybit");
297        assert_eq!(builder.config.name, "Bybit");
298        assert!(!builder.config.sandbox);
299        assert_eq!(builder.options.account_type, "UNIFIED");
300        assert_eq!(builder.options.recv_window, 5000);
301    }
302
303    #[test]
304    fn test_builder_api_key() {
305        let builder = BybitBuilder::new().api_key("test-key");
306        assert_eq!(builder.config.api_key, Some("test-key".to_string()));
307    }
308
309    #[test]
310    fn test_builder_secret() {
311        let builder = BybitBuilder::new().secret("test-secret");
312        assert_eq!(builder.config.secret, Some("test-secret".to_string()));
313    }
314
315    #[test]
316    fn test_builder_sandbox() {
317        let builder = BybitBuilder::new().sandbox(true);
318        assert!(builder.config.sandbox);
319        assert!(builder.options.testnet);
320    }
321
322    #[test]
323    fn test_builder_testnet() {
324        let builder = BybitBuilder::new().testnet(true);
325        assert!(builder.config.sandbox);
326        assert!(builder.options.testnet);
327    }
328
329    #[test]
330    fn test_builder_sandbox_testnet_equivalence() {
331        // Verify that sandbox() and testnet() produce equivalent results
332        let sandbox_builder = BybitBuilder::new().sandbox(true);
333        let testnet_builder = BybitBuilder::new().testnet(true);
334
335        assert_eq!(
336            sandbox_builder.config.sandbox,
337            testnet_builder.config.sandbox
338        );
339        assert_eq!(
340            sandbox_builder.options.testnet,
341            testnet_builder.options.testnet
342        );
343    }
344
345    #[test]
346    fn test_builder_account_type() {
347        let builder = BybitBuilder::new().account_type("CONTRACT");
348        assert_eq!(builder.options.account_type, "CONTRACT");
349    }
350
351    #[test]
352    fn test_builder_default_type() {
353        let builder = BybitBuilder::new().default_type(DefaultType::Swap);
354        assert_eq!(builder.options.default_type, DefaultType::Swap);
355    }
356
357    #[test]
358    fn test_builder_default_type_from_string() {
359        let builder = BybitBuilder::new().default_type("futures");
360        assert_eq!(builder.options.default_type, DefaultType::Futures);
361    }
362
363    #[test]
364    fn test_builder_default_sub_type() {
365        let builder = BybitBuilder::new().default_sub_type(DefaultSubType::Inverse);
366        assert_eq!(
367            builder.options.default_sub_type,
368            Some(DefaultSubType::Inverse)
369        );
370    }
371
372    #[test]
373    fn test_builder_default_type_and_sub_type() {
374        let builder = BybitBuilder::new()
375            .default_type(DefaultType::Swap)
376            .default_sub_type(DefaultSubType::Linear);
377        assert_eq!(builder.options.default_type, DefaultType::Swap);
378        assert_eq!(
379            builder.options.default_sub_type,
380            Some(DefaultSubType::Linear)
381        );
382    }
383
384    #[test]
385    fn test_builder_recv_window() {
386        let builder = BybitBuilder::new().recv_window(10000);
387        assert_eq!(builder.options.recv_window, 10000);
388    }
389
390    #[test]
391    fn test_builder_timeout() {
392        let builder = BybitBuilder::new().timeout(60);
393        assert_eq!(builder.config.timeout, 60);
394    }
395
396    #[test]
397    fn test_builder_chaining() {
398        let builder = BybitBuilder::new()
399            .api_key("key")
400            .secret("secret")
401            .testnet(true)
402            .timeout(30)
403            .recv_window(10000)
404            .account_type("SPOT")
405            .default_type(DefaultType::Swap)
406            .default_sub_type(DefaultSubType::Linear);
407
408        assert_eq!(builder.config.api_key, Some("key".to_string()));
409        assert_eq!(builder.config.secret, Some("secret".to_string()));
410        assert!(builder.config.sandbox);
411        assert_eq!(builder.config.timeout, 30);
412        assert_eq!(builder.options.recv_window, 10000);
413        assert_eq!(builder.options.account_type, "SPOT");
414        assert_eq!(builder.options.default_type, DefaultType::Swap);
415        assert_eq!(
416            builder.options.default_sub_type,
417            Some(DefaultSubType::Linear)
418        );
419    }
420
421    #[test]
422    fn test_builder_build() {
423        let result = BybitBuilder::new().build();
424        assert!(result.is_ok());
425
426        let bybit = result.unwrap();
427        assert_eq!(bybit.id(), "bybit");
428        assert_eq!(bybit.name(), "Bybit");
429    }
430
431    #[test]
432    fn test_builder_build_with_credentials() {
433        let result = BybitBuilder::new()
434            .api_key("test-key")
435            .secret("test-secret")
436            .build();
437
438        assert!(result.is_ok());
439    }
440
441    #[test]
442    fn test_builder_enable_rate_limit() {
443        let builder = BybitBuilder::new().enable_rate_limit(false);
444        assert!(!builder.config.enable_rate_limit);
445    }
446
447    #[test]
448    fn test_builder_proxy() {
449        let builder = BybitBuilder::new().proxy("http://proxy.example.com:8080");
450        assert_eq!(
451            builder.config.proxy,
452            Some("http://proxy.example.com:8080".to_string())
453        );
454    }
455
456    #[test]
457    fn test_builder_verbose() {
458        let builder = BybitBuilder::new().verbose(true);
459        assert!(builder.config.verbose);
460    }
461}