Skip to main content

nautilus_okx/
config.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Configuration structures for the OKX adapter.
17
18use std::fmt::Debug;
19
20use nautilus_core::string::secret::REDACTED;
21use nautilus_model::identifiers::AccountId;
22use nautilus_network::websocket::TransportBackend;
23use serde::{Deserialize, Serialize};
24
25use crate::common::{
26    credential::credential_env_vars,
27    enums::{
28        OKXContractType, OKXEnvironment, OKXInstrumentType, OKXMarginMode, OKXRegion, OKXVipLevel,
29    },
30    urls::{
31        get_http_base_url, get_ws_base_url_business, get_ws_base_url_private,
32        get_ws_base_url_public,
33    },
34};
35
36/// Configuration for the OKX data client.
37#[derive(Clone, Serialize, Deserialize, bon::Builder)]
38#[serde(default, deny_unknown_fields)]
39#[cfg_attr(
40    feature = "python",
41    pyo3::pyclass(module = "nautilus_trader.adapters.okx", from_py_object)
42)]
43#[cfg_attr(
44    feature = "python",
45    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.okx")
46)]
47pub struct OKXDataClientConfig {
48    /// Optional API key for authenticated endpoints.
49    pub api_key: Option<String>,
50    /// Optional API secret for authenticated endpoints.
51    pub api_secret: Option<String>,
52    /// Optional API passphrase for authenticated endpoints.
53    pub api_passphrase: Option<String>,
54    /// Instrument types to load and subscribe to.
55    #[builder(default = vec![OKXInstrumentType::Spot])]
56    pub instrument_types: Vec<OKXInstrumentType>,
57    /// Contract type filter applied to loaded instruments.
58    pub contract_types: Option<Vec<OKXContractType>>,
59    /// Whether to load spread trading instruments from the separate spread endpoint.
60    #[builder(default)]
61    pub load_spreads: bool,
62    /// Instrument families to load (e.g., "BTC-USD", "ETH-USD").
63    /// Required for OPTIONS. Optional for FUTURES/SWAP. Not applicable for SPOT/MARGIN.
64    pub instrument_families: Option<Vec<String>>,
65    /// Optional override for the HTTP base URL.
66    pub base_url_http: Option<String>,
67    /// Optional override for the public WebSocket URL.
68    pub base_url_ws_public: Option<String>,
69    /// Optional override for the business WebSocket URL.
70    pub base_url_ws_business: Option<String>,
71    /// Optional proxy URL for HTTP and WebSocket transports.
72    pub proxy_url: Option<String>,
73    /// The API environment (live or demo).
74    #[builder(default)]
75    pub environment: OKXEnvironment,
76    /// The API region (global, EEA, or US).
77    #[builder(default)]
78    pub region: OKXRegion,
79    /// HTTP timeout in seconds.
80    #[builder(default = 60)]
81    pub http_timeout_secs: u64,
82    /// Maximum retry attempts for requests.
83    #[builder(default = 3)]
84    pub max_retries: u32,
85    /// Initial retry delay in milliseconds.
86    #[builder(default = 1_000)]
87    pub retry_delay_initial_ms: u64,
88    /// Maximum retry delay in milliseconds.
89    #[builder(default = 10_000)]
90    pub retry_delay_max_ms: u64,
91    /// Interval for reconciling instruments from the REST API in minutes.
92    ///
93    /// Set to 0 to disable periodic reconciliation. WebSocket instrument
94    /// updates are always applied regardless of this interval.
95    #[builder(default = 60)]
96    pub update_instruments_interval_mins: u64,
97    /// Interval for checking order book feed staleness in seconds.
98    #[builder(default = 5)]
99    pub book_stale_check_interval_secs: u64,
100    /// Maximum time without order book updates before emitting a stale signal in seconds.
101    ///
102    /// Set to 0 to disable. Quiet markets can idle without book changes.
103    #[builder(default = 30)]
104    pub book_stale_threshold_secs: u64,
105    /// Maximum time to wait for a post-reconnect order book snapshot in seconds.
106    #[builder(default = 3)]
107    pub book_snapshot_timeout_secs: u64,
108    /// Optional VIP level that unlocks additional subscriptions.
109    pub vip_level: Option<OKXVipLevel>,
110    /// WebSocket transport backend (defaults to `Tungstenite`).
111    #[builder(default)]
112    pub transport_backend: TransportBackend,
113}
114
115#[cfg(feature = "python")]
116nautilus_core::impl_pyo3_config_getters!(OKXDataClientConfig {
117    instrument_types: Vec<OKXInstrumentType>,
118    instrument_families: Option<Vec<String>>,
119    environment: OKXEnvironment,
120    region: OKXRegion,
121    base_url_http: Option<String>,
122    base_url_ws_public: Option<String>,
123    base_url_ws_business: Option<String>,
124    http_timeout_secs: u64,
125    max_retries: u32,
126    retry_delay_initial_ms: u64,
127    retry_delay_max_ms: u64,
128    update_instruments_interval_mins: u64,
129    book_stale_check_interval_secs: u64,
130    book_stale_threshold_secs: u64,
131    book_snapshot_timeout_secs: u64,
132    vip_level: Option<OKXVipLevel>,
133    load_spreads: bool,
134    transport_backend: TransportBackend,
135});
136
137impl Default for OKXDataClientConfig {
138    fn default() -> Self {
139        Self::builder().build()
140    }
141}
142
143impl Debug for OKXDataClientConfig {
144    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
145        f.debug_struct(stringify!(OKXDataClientConfig))
146            .field("api_key", &self.api_key.as_ref().map(|_| REDACTED))
147            .field("api_secret", &self.api_secret.as_ref().map(|_| REDACTED))
148            .field(
149                "api_passphrase",
150                &self.api_passphrase.as_ref().map(|_| REDACTED),
151            )
152            .field("instrument_types", &self.instrument_types)
153            .field("contract_types", &self.contract_types)
154            .field("load_spreads", &self.load_spreads)
155            .field("instrument_families", &self.instrument_families)
156            .field("base_url_http", &self.base_url_http)
157            .field("base_url_ws_public", &self.base_url_ws_public)
158            .field("base_url_ws_business", &self.base_url_ws_business)
159            .field("proxy_url", &self.proxy_url)
160            .field("environment", &self.environment)
161            .field("region", &self.region)
162            .field("http_timeout_secs", &self.http_timeout_secs)
163            .field("max_retries", &self.max_retries)
164            .field("retry_delay_initial_ms", &self.retry_delay_initial_ms)
165            .field("retry_delay_max_ms", &self.retry_delay_max_ms)
166            .field(
167                "update_instruments_interval_mins",
168                &self.update_instruments_interval_mins,
169            )
170            .field(
171                "book_stale_check_interval_secs",
172                &self.book_stale_check_interval_secs,
173            )
174            .field("book_stale_threshold_secs", &self.book_stale_threshold_secs)
175            .field(
176                "book_snapshot_timeout_secs",
177                &self.book_snapshot_timeout_secs,
178            )
179            .field("vip_level", &self.vip_level)
180            .field("transport_backend", &self.transport_backend)
181            .finish()
182    }
183}
184
185impl OKXDataClientConfig {
186    /// Creates a new configuration with default settings.
187    #[must_use]
188    pub fn new() -> Self {
189        Self::default()
190    }
191
192    /// Returns `true` when all API credential fields are available (in config or env vars).
193    #[must_use]
194    pub fn has_api_credentials(&self) -> bool {
195        let (key_var, secret_var, passphrase_var) = credential_env_vars();
196        let has_key = self.api_key.is_some() || std::env::var(key_var).is_ok();
197        let has_secret = self.api_secret.is_some() || std::env::var(secret_var).is_ok();
198        let has_passphrase = self.api_passphrase.is_some() || std::env::var(passphrase_var).is_ok();
199        has_key && has_secret && has_passphrase
200    }
201
202    /// Returns the HTTP base URL, falling back to the region default when unset.
203    #[must_use]
204    pub fn http_base_url(&self) -> String {
205        self.base_url_http
206            .clone()
207            .unwrap_or_else(|| get_http_base_url(self.region).to_string())
208    }
209
210    /// Returns the public WebSocket URL, respecting the region, environment, and overrides.
211    #[must_use]
212    pub fn ws_public_url(&self) -> String {
213        self.base_url_ws_public
214            .clone()
215            .unwrap_or_else(|| get_ws_base_url_public(self.region, self.environment).to_string())
216    }
217
218    /// Returns the business WebSocket URL, respecting the region, environment, and overrides.
219    #[must_use]
220    pub fn ws_business_url(&self) -> String {
221        self.base_url_ws_business
222            .clone()
223            .unwrap_or_else(|| get_ws_base_url_business(self.region, self.environment).to_string())
224    }
225
226    /// Returns `true` when the business WebSocket should be instantiated.
227    ///
228    /// The business WebSocket carries public candle data and does not
229    /// require authentication, so it is always needed.
230    #[must_use]
231    pub fn requires_business_ws(&self) -> bool {
232        true
233    }
234}
235
236/// Configuration for the OKX execution client.
237#[derive(Clone, Serialize, Deserialize, bon::Builder)]
238#[serde(default, deny_unknown_fields)]
239#[cfg_attr(
240    feature = "python",
241    pyo3::pyclass(module = "nautilus_trader.adapters.okx", from_py_object)
242)]
243#[cfg_attr(
244    feature = "python",
245    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.okx")
246)]
247pub struct OKXExecutionClientConfig {
248    /// The account ID for the client.
249    #[builder(default = AccountId::from("OKX-001"))]
250    pub account_id: AccountId,
251    /// Optional API key for authenticated endpoints.
252    pub api_key: Option<String>,
253    /// Optional API secret for authenticated endpoints.
254    pub api_secret: Option<String>,
255    /// Optional API passphrase for authenticated endpoints.
256    pub api_passphrase: Option<String>,
257    /// Instrument types the execution client should support.
258    #[builder(default = vec![OKXInstrumentType::Spot])]
259    pub instrument_types: Vec<OKXInstrumentType>,
260    /// Contract type filter applied to operations.
261    pub contract_types: Option<Vec<OKXContractType>>,
262    /// Instrument families to load (e.g., "BTC-USD", "ETH-USD").
263    /// Required for OPTIONS. Optional for FUTURES/SWAP. Not applicable for SPOT/MARGIN.
264    pub instrument_families: Option<Vec<String>>,
265    /// Optional override for the HTTP base URL.
266    pub base_url_http: Option<String>,
267    /// Optional override for the private WebSocket URL.
268    pub base_url_ws_private: Option<String>,
269    /// Optional override for the business WebSocket URL.
270    pub base_url_ws_business: Option<String>,
271    /// Optional proxy URL for HTTP and WebSocket transports.
272    pub proxy_url: Option<String>,
273    /// The API environment (live or demo).
274    #[builder(default)]
275    pub environment: OKXEnvironment,
276    /// The API region (global, EEA, or US).
277    #[builder(default)]
278    pub region: OKXRegion,
279    /// HTTP timeout in seconds.
280    #[builder(default = 60)]
281    pub http_timeout_secs: u64,
282    /// Whether to subscribe to spread order updates from the separate spread channel.
283    #[builder(default)]
284    pub load_spreads: bool,
285    /// Enables mass-cancel support when true.
286    #[builder(default)]
287    pub use_mm_mass_cancel: bool,
288    /// Maximum retry attempts for requests.
289    #[builder(default = 3)]
290    pub max_retries: u32,
291    /// Initial retry delay in milliseconds.
292    #[builder(default = 1_000)]
293    pub retry_delay_initial_ms: u64,
294    /// Maximum retry delay in milliseconds.
295    #[builder(default = 10_000)]
296    pub retry_delay_max_ms: u64,
297    /// Optional margin mode (CROSS or ISOLATED) for margin/derivative accounts.
298    pub margin_mode: Option<OKXMarginMode>,
299    /// Enables margin/leverage for SPOT trading when true.
300    #[builder(default)]
301    pub use_spot_margin: bool,
302    /// Optional WebSocket authentication timeout (seconds), defaulting to
303    /// `AUTHENTICATION_TIMEOUT_SECS` when unset.
304    pub auth_timeout_secs: Option<u64>,
305    /// WebSocket transport backend (defaults to `Tungstenite`).
306    #[builder(default)]
307    pub transport_backend: TransportBackend,
308}
309
310#[cfg(feature = "python")]
311nautilus_core::impl_pyo3_config_getters!(OKXExecutionClientConfig {
312    account_id: AccountId,
313    instrument_types: Vec<OKXInstrumentType>,
314    environment: OKXEnvironment,
315    region: OKXRegion,
316    base_url_http: Option<String>,
317    base_url_ws_private: Option<String>,
318    base_url_ws_business: Option<String>,
319    http_timeout_secs: u64,
320    max_retries: u32,
321    retry_delay_initial_ms: u64,
322    retry_delay_max_ms: u64,
323    margin_mode: Option<OKXMarginMode>,
324    load_spreads: bool,
325    auth_timeout_secs: Option<u64>,
326    transport_backend: TransportBackend,
327});
328
329impl Default for OKXExecutionClientConfig {
330    fn default() -> Self {
331        Self::builder().build()
332    }
333}
334
335impl Debug for OKXExecutionClientConfig {
336    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
337        f.debug_struct(stringify!(OKXExecutionClientConfig))
338            .field("account_id", &self.account_id)
339            .field("api_key", &self.api_key.as_ref().map(|_| REDACTED))
340            .field("api_secret", &self.api_secret.as_ref().map(|_| REDACTED))
341            .field(
342                "api_passphrase",
343                &self.api_passphrase.as_ref().map(|_| REDACTED),
344            )
345            .field("instrument_types", &self.instrument_types)
346            .field("contract_types", &self.contract_types)
347            .field("instrument_families", &self.instrument_families)
348            .field("base_url_http", &self.base_url_http)
349            .field("base_url_ws_private", &self.base_url_ws_private)
350            .field("base_url_ws_business", &self.base_url_ws_business)
351            .field("proxy_url", &self.proxy_url)
352            .field("environment", &self.environment)
353            .field("region", &self.region)
354            .field("http_timeout_secs", &self.http_timeout_secs)
355            .field("load_spreads", &self.load_spreads)
356            .field("use_mm_mass_cancel", &self.use_mm_mass_cancel)
357            .field("max_retries", &self.max_retries)
358            .field("retry_delay_initial_ms", &self.retry_delay_initial_ms)
359            .field("retry_delay_max_ms", &self.retry_delay_max_ms)
360            .field("margin_mode", &self.margin_mode)
361            .field("use_spot_margin", &self.use_spot_margin)
362            .field("auth_timeout_secs", &self.auth_timeout_secs)
363            .field("transport_backend", &self.transport_backend)
364            .finish()
365    }
366}
367
368impl OKXExecutionClientConfig {
369    /// Creates a new configuration with default settings.
370    #[must_use]
371    pub fn new() -> Self {
372        Self::default()
373    }
374
375    /// Returns `true` when all API credential fields are available (in config or env vars).
376    #[must_use]
377    pub fn has_api_credentials(&self) -> bool {
378        let (key_var, secret_var, passphrase_var) = credential_env_vars();
379        let has_key = self.api_key.is_some() || std::env::var(key_var).is_ok();
380        let has_secret = self.api_secret.is_some() || std::env::var(secret_var).is_ok();
381        let has_passphrase = self.api_passphrase.is_some() || std::env::var(passphrase_var).is_ok();
382        has_key && has_secret && has_passphrase
383    }
384
385    /// Returns the HTTP base URL, falling back to the region default when unset.
386    #[must_use]
387    pub fn http_base_url(&self) -> String {
388        self.base_url_http
389            .clone()
390            .unwrap_or_else(|| get_http_base_url(self.region).to_string())
391    }
392
393    /// Returns the private WebSocket URL, respecting the region, environment, and overrides.
394    #[must_use]
395    pub fn ws_private_url(&self) -> String {
396        self.base_url_ws_private
397            .clone()
398            .unwrap_or_else(|| get_ws_base_url_private(self.region, self.environment).to_string())
399    }
400
401    /// Returns the business WebSocket URL, respecting the region, environment, and overrides.
402    #[must_use]
403    pub fn ws_business_url(&self) -> String {
404        self.base_url_ws_business
405            .clone()
406            .unwrap_or_else(|| get_ws_base_url_business(self.region, self.environment).to_string())
407    }
408}
409
410#[cfg(test)]
411mod tests {
412    use rstest::rstest;
413
414    use super::*;
415
416    const DATA_API_KEY: &str = "okx-data-api-key-sentinel";
417    const DATA_API_SECRET: &str = "okx-data-api-secret-sentinel";
418    const DATA_API_PASSPHRASE: &str = "okx-data-api-passphrase-sentinel";
419    const EXEC_API_KEY: &str = "okx-exec-api-key-sentinel";
420    const EXEC_API_SECRET: &str = "okx-exec-api-secret-sentinel";
421    const EXEC_API_PASSPHRASE: &str = "okx-exec-api-passphrase-sentinel";
422
423    #[rstest]
424    fn test_data_config_debug_redacts_credentials() {
425        let config = OKXDataClientConfig {
426            api_key: Some(DATA_API_KEY.to_string()),
427            api_secret: Some(DATA_API_SECRET.to_string()),
428            api_passphrase: Some(DATA_API_PASSPHRASE.to_string()),
429            environment: OKXEnvironment::Demo,
430            http_timeout_secs: 71,
431            ..Default::default()
432        };
433
434        let debug_output = format!("{config:?}");
435        let redacted = format!("Some({REDACTED:?})");
436
437        assert!(!debug_output.contains(DATA_API_KEY));
438        assert!(!debug_output.contains(DATA_API_SECRET));
439        assert!(!debug_output.contains(DATA_API_PASSPHRASE));
440        assert!(debug_output.contains(&format!("api_key: {redacted}")));
441        assert!(debug_output.contains(&format!("api_secret: {redacted}")));
442        assert!(debug_output.contains(&format!("api_passphrase: {redacted}")));
443        assert!(debug_output.contains("environment: Demo"));
444        assert!(debug_output.contains("http_timeout_secs: 71"));
445    }
446
447    #[rstest]
448    fn test_exec_config_debug_redacts_credentials() {
449        let config = OKXExecutionClientConfig {
450            account_id: AccountId::from("OKX-042"),
451            api_key: Some(EXEC_API_KEY.to_string()),
452            api_secret: Some(EXEC_API_SECRET.to_string()),
453            api_passphrase: Some(EXEC_API_PASSPHRASE.to_string()),
454            max_retries: 13,
455            ..Default::default()
456        };
457
458        let debug_output = format!("{config:?}");
459        let redacted = format!("Some({REDACTED:?})");
460
461        assert!(!debug_output.contains(EXEC_API_KEY));
462        assert!(!debug_output.contains(EXEC_API_SECRET));
463        assert!(!debug_output.contains(EXEC_API_PASSPHRASE));
464        assert!(debug_output.contains(&format!("api_key: {redacted}")));
465        assert!(debug_output.contains(&format!("api_secret: {redacted}")));
466        assert!(debug_output.contains(&format!("api_passphrase: {redacted}")));
467        assert!(debug_output.contains("OKX-042"));
468        assert!(debug_output.contains("max_retries: 13"));
469    }
470
471    #[rstest]
472    fn test_config_debug_handles_unset_and_partial_credentials() {
473        let data_debug = format!("{:?}", OKXDataClientConfig::default());
474        let exec_debug = format!(
475            "{:?}",
476            OKXExecutionClientConfig {
477                api_secret: Some(String::new()),
478                ..Default::default()
479            }
480        );
481
482        assert!(data_debug.contains("api_key: None"));
483        assert!(data_debug.contains("api_secret: None"));
484        assert!(data_debug.contains("api_passphrase: None"));
485        assert!(exec_debug.contains("api_key: None"));
486        assert!(exec_debug.contains(&format!("api_secret: Some({REDACTED:?})")));
487        assert!(exec_debug.contains("api_passphrase: None"));
488    }
489
490    #[rstest]
491    fn test_data_config_toml_minimal() {
492        let config: OKXDataClientConfig = toml::from_str(
493            r#"
494environment = "demo"
495instrument_types = ["SPOT", "SWAP"]
496http_timeout_secs = 90
497"#,
498        )
499        .unwrap();
500
501        assert_eq!(config.environment, OKXEnvironment::Demo);
502        assert_eq!(
503            config.instrument_types,
504            vec![OKXInstrumentType::Spot, OKXInstrumentType::Swap]
505        );
506        assert_eq!(config.http_timeout_secs, 90);
507        assert!(!config.load_spreads);
508        assert_eq!(config.book_stale_check_interval_secs, 5);
509        assert_eq!(config.book_stale_threshold_secs, 30);
510        assert_eq!(config.book_snapshot_timeout_secs, 3);
511    }
512
513    #[rstest]
514    fn test_data_config_toml_load_spreads() {
515        let config: OKXDataClientConfig = toml::from_str(
516            "
517load_spreads = true
518",
519        )
520        .unwrap();
521
522        assert!(config.load_spreads);
523    }
524
525    #[rstest]
526    fn test_data_config_toml_book_stale_settings() {
527        let config: OKXDataClientConfig = toml::from_str(
528            "
529book_stale_check_interval_secs = 2
530book_stale_threshold_secs = 7
531book_snapshot_timeout_secs = 4
532",
533        )
534        .unwrap();
535
536        assert_eq!(config.book_stale_check_interval_secs, 2);
537        assert_eq!(config.book_stale_threshold_secs, 7);
538        assert_eq!(config.book_snapshot_timeout_secs, 4);
539    }
540
541    #[rstest]
542    fn test_exec_config_toml_empty_uses_defaults() {
543        let config: OKXExecutionClientConfig = toml::from_str("").unwrap();
544        let expected = OKXExecutionClientConfig::default();
545        assert_eq!(config.account_id, expected.account_id);
546        assert_eq!(config.environment, expected.environment);
547        assert_eq!(config.instrument_types, expected.instrument_types);
548        assert_eq!(config.http_timeout_secs, expected.http_timeout_secs);
549        assert_eq!(config.load_spreads, expected.load_spreads);
550        assert_eq!(config.use_mm_mass_cancel, expected.use_mm_mass_cancel);
551        assert_eq!(config.transport_backend, expected.transport_backend);
552    }
553
554    #[rstest]
555    fn test_exec_config_toml_rejects_removed_fills_channel_key() {
556        // use_fills_channel was removed: strict decoding must reject stale configs
557        let result: Result<OKXExecutionClientConfig, _> =
558            toml::from_str("use_fills_channel = true\n");
559        assert!(result.is_err());
560    }
561
562    #[rstest]
563    fn test_exec_config_toml_load_spreads() {
564        let config: OKXExecutionClientConfig = toml::from_str(
565            "
566load_spreads = true
567",
568        )
569        .unwrap();
570
571        assert!(config.load_spreads);
572    }
573
574    #[rstest]
575    fn test_data_config_default_region_is_global() {
576        let config = OKXDataClientConfig::default();
577
578        assert_eq!(config.region, OKXRegion::Global);
579        assert_eq!(config.http_base_url(), "https://www.okx.com");
580        assert_eq!(config.ws_public_url(), "wss://ws.okx.com:8443/ws/v5/public");
581    }
582
583    #[rstest]
584    fn test_data_config_eea_region_urls() {
585        let config = OKXDataClientConfig::builder()
586            .region(OKXRegion::Eea)
587            .build();
588
589        assert_eq!(config.http_base_url(), "https://eea.okx.com");
590        assert_eq!(
591            config.ws_public_url(),
592            "wss://wseea.okx.com:8443/ws/v5/public"
593        );
594        assert_eq!(
595            config.ws_business_url(),
596            "wss://wseea.okx.com:8443/ws/v5/business"
597        );
598    }
599
600    #[rstest]
601    fn test_exec_config_eea_region_urls() {
602        let config = OKXExecutionClientConfig::builder()
603            .region(OKXRegion::Eea)
604            .build();
605
606        assert_eq!(config.http_base_url(), "https://eea.okx.com");
607        assert_eq!(
608            config.ws_private_url(),
609            "wss://wseea.okx.com:8443/ws/v5/private"
610        );
611        assert_eq!(
612            config.ws_business_url(),
613            "wss://wseea.okx.com:8443/ws/v5/business"
614        );
615    }
616
617    #[rstest]
618    fn test_config_region_override_takes_precedence() {
619        let config = OKXDataClientConfig::builder()
620            .region(OKXRegion::Eea)
621            .base_url_http("https://custom.proxy".to_string())
622            .build();
623
624        assert_eq!(config.http_base_url(), "https://custom.proxy");
625    }
626
627    #[rstest]
628    fn test_data_config_toml_region() {
629        let config: OKXDataClientConfig = toml::from_str(
630            r#"
631region = "eea"
632"#,
633        )
634        .unwrap();
635
636        assert_eq!(config.region, OKXRegion::Eea);
637    }
638
639    #[rstest]
640    fn test_exec_config_auth_timeout_secs() {
641        assert_eq!(OKXExecutionClientConfig::default().auth_timeout_secs, None);
642
643        let exec = OKXExecutionClientConfig::builder()
644            .auth_timeout_secs(4)
645            .build();
646        assert_eq!(exec.auth_timeout_secs, Some(4));
647
648        let exec: OKXExecutionClientConfig = toml::from_str("auth_timeout_secs = 8\n").unwrap();
649        assert_eq!(exec.auth_timeout_secs, Some(8));
650    }
651}