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