ccxt_exchanges/okx/
builder.rs

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