1use nautilus_core::string::secret::SecretString;
19use nautilus_live::book::DEFAULT_BOOK_SNAPSHOT_TIMEOUT_SECS;
20use nautilus_model::identifiers::AccountId;
21use nautilus_network::websocket::TransportBackend;
22use serde::{Deserialize, Serialize};
23
24use crate::common::{
25 credential::credential_env_vars,
26 enums::{
27 OKXContractType, OKXEnvironment, OKXInstrumentType, OKXMarginMode, OKXRegion, OKXVipLevel,
28 },
29 urls::{
30 get_http_base_url, get_ws_base_url_business, get_ws_base_url_private,
31 get_ws_base_url_public,
32 },
33};
34
35#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
37#[serde(default, deny_unknown_fields)]
38#[cfg_attr(
39 feature = "python",
40 pyo3::pyclass(module = "nautilus_trader.adapters.okx", from_py_object)
41)]
42#[cfg_attr(
43 feature = "python",
44 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.okx")
45)]
46pub struct OKXDataClientConfig {
47 pub api_key: Option<SecretString>,
49 pub api_secret: Option<SecretString>,
51 pub api_passphrase: Option<SecretString>,
53 #[builder(default = vec![OKXInstrumentType::Spot])]
55 pub instrument_types: Vec<OKXInstrumentType>,
56 pub contract_types: Option<Vec<OKXContractType>>,
58 #[builder(default)]
60 pub load_spreads: bool,
61 pub instrument_families: Option<Vec<String>>,
64 #[builder(default)]
66 pub environment: OKXEnvironment,
67 #[builder(default)]
69 pub region: OKXRegion,
70 pub base_url_http: Option<String>,
72 pub base_url_ws_public: Option<String>,
74 pub base_url_ws_business: Option<String>,
76 pub proxy_url: Option<SecretString>,
78 #[builder(default = 60)]
80 pub http_timeout_secs: u64,
81 #[builder(default = 3)]
83 pub max_retries: u32,
84 #[builder(default = 1_000)]
86 pub retry_delay_initial_ms: u64,
87 #[builder(default = 10_000)]
89 pub retry_delay_max_ms: u64,
90 #[builder(default = 60)]
95 pub update_instruments_interval_mins: u64,
96 #[builder(default = 5)]
98 pub book_stale_check_interval_secs: u64,
99 #[builder(default = 30)]
103 pub book_stale_threshold_secs: u64,
104 #[builder(default = DEFAULT_BOOK_SNAPSHOT_TIMEOUT_SECS)]
107 pub book_snapshot_timeout_secs: u64,
108 pub vip_level: Option<OKXVipLevel>,
110 #[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 OKXDataClientConfig {
144 #[must_use]
146 pub fn new() -> Self {
147 Self::default()
148 }
149
150 #[must_use]
152 pub fn has_api_credentials(&self) -> bool {
153 let (key_var, secret_var, passphrase_var) = credential_env_vars();
154 let has_key = self.api_key.is_some() || std::env::var(key_var).is_ok();
155 let has_secret = self.api_secret.is_some() || std::env::var(secret_var).is_ok();
156 let has_passphrase = self.api_passphrase.is_some() || std::env::var(passphrase_var).is_ok();
157 has_key && has_secret && has_passphrase
158 }
159
160 #[must_use]
162 pub fn http_base_url(&self) -> String {
163 self.base_url_http
164 .clone()
165 .unwrap_or_else(|| get_http_base_url(self.region).to_string())
166 }
167
168 #[must_use]
170 pub fn ws_public_url(&self) -> String {
171 self.base_url_ws_public
172 .clone()
173 .unwrap_or_else(|| get_ws_base_url_public(self.region, self.environment).to_string())
174 }
175
176 #[must_use]
178 pub fn ws_business_url(&self) -> String {
179 self.base_url_ws_business
180 .clone()
181 .unwrap_or_else(|| get_ws_base_url_business(self.region, self.environment).to_string())
182 }
183
184 #[must_use]
189 pub fn requires_business_ws(&self) -> bool {
190 true
191 }
192}
193
194#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
196#[serde(default, deny_unknown_fields)]
197#[cfg_attr(
198 feature = "python",
199 pyo3::pyclass(module = "nautilus_trader.adapters.okx", from_py_object)
200)]
201#[cfg_attr(
202 feature = "python",
203 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.okx")
204)]
205pub struct OKXExecutionClientConfig {
206 #[builder(default = AccountId::from("OKX-001"))]
208 pub account_id: AccountId,
209 pub api_key: Option<SecretString>,
211 pub api_secret: Option<SecretString>,
213 pub api_passphrase: Option<SecretString>,
215 #[builder(default = vec![OKXInstrumentType::Spot])]
217 pub instrument_types: Vec<OKXInstrumentType>,
218 pub contract_types: Option<Vec<OKXContractType>>,
220 pub instrument_families: Option<Vec<String>>,
223 #[builder(default)]
225 pub environment: OKXEnvironment,
226 #[builder(default)]
228 pub region: OKXRegion,
229 pub base_url_http: Option<String>,
231 pub base_url_ws_private: Option<String>,
233 pub base_url_ws_business: Option<String>,
235 pub proxy_url: Option<SecretString>,
237 #[builder(default = 60)]
239 pub http_timeout_secs: u64,
240 #[builder(default)]
242 pub load_spreads: bool,
243 #[builder(default)]
245 pub use_mm_mass_cancel: bool,
246 #[builder(default = 3)]
248 pub max_retries: u32,
249 #[builder(default = 1_000)]
251 pub retry_delay_initial_ms: u64,
252 #[builder(default = 10_000)]
254 pub retry_delay_max_ms: u64,
255 pub margin_mode: Option<OKXMarginMode>,
257 #[builder(default)]
259 pub use_spot_margin: bool,
260 pub spot_trade_quote_ccy: Option<String>,
267 pub auth_timeout_secs: Option<u64>,
270 #[builder(default)]
272 pub transport_backend: TransportBackend,
273}
274
275#[cfg(feature = "python")]
276nautilus_core::impl_pyo3_config_getters!(OKXExecutionClientConfig {
277 account_id: AccountId,
278 instrument_types: Vec<OKXInstrumentType>,
279 environment: OKXEnvironment,
280 region: OKXRegion,
281 base_url_http: Option<String>,
282 base_url_ws_private: Option<String>,
283 base_url_ws_business: Option<String>,
284 http_timeout_secs: u64,
285 max_retries: u32,
286 retry_delay_initial_ms: u64,
287 retry_delay_max_ms: u64,
288 margin_mode: Option<OKXMarginMode>,
289 load_spreads: bool,
290 auth_timeout_secs: Option<u64>,
291 transport_backend: TransportBackend,
292 spot_trade_quote_ccy: Option<String>,
293});
294
295impl Default for OKXExecutionClientConfig {
296 fn default() -> Self {
297 Self::builder().build()
298 }
299}
300
301impl OKXExecutionClientConfig {
302 #[must_use]
304 pub fn new() -> Self {
305 Self::default()
306 }
307
308 #[must_use]
310 pub fn has_api_credentials(&self) -> bool {
311 let (key_var, secret_var, passphrase_var) = credential_env_vars();
312 let has_key = self.api_key.is_some() || std::env::var(key_var).is_ok();
313 let has_secret = self.api_secret.is_some() || std::env::var(secret_var).is_ok();
314 let has_passphrase = self.api_passphrase.is_some() || std::env::var(passphrase_var).is_ok();
315 has_key && has_secret && has_passphrase
316 }
317
318 #[must_use]
320 pub fn http_base_url(&self) -> String {
321 self.base_url_http
322 .clone()
323 .unwrap_or_else(|| get_http_base_url(self.region).to_string())
324 }
325
326 #[must_use]
328 pub fn ws_private_url(&self) -> String {
329 self.base_url_ws_private
330 .clone()
331 .unwrap_or_else(|| get_ws_base_url_private(self.region, self.environment).to_string())
332 }
333
334 #[must_use]
336 pub fn ws_business_url(&self) -> String {
337 self.base_url_ws_business
338 .clone()
339 .unwrap_or_else(|| get_ws_base_url_business(self.region, self.environment).to_string())
340 }
341}
342
343#[cfg(test)]
344mod tests {
345 use nautilus_core::string::secret::REDACTED;
346 use rstest::rstest;
347
348 use super::*;
349
350 const DATA_API_KEY: &str = "okx-data-api-key-sentinel";
351 const DATA_API_SECRET: &str = "okx-data-api-secret-sentinel";
352 const DATA_API_PASSPHRASE: &str = "okx-data-api-passphrase-sentinel";
353 const EXEC_API_KEY: &str = "okx-exec-api-key-sentinel";
354 const EXEC_API_SECRET: &str = "okx-exec-api-secret-sentinel";
355 const EXEC_API_PASSPHRASE: &str = "okx-exec-api-passphrase-sentinel";
356
357 #[rstest]
358 fn test_data_config_debug_redacts_credentials() {
359 let config = OKXDataClientConfig {
360 api_key: Some(DATA_API_KEY.into()),
361 api_secret: Some(DATA_API_SECRET.into()),
362 api_passphrase: Some(DATA_API_PASSPHRASE.into()),
363 environment: OKXEnvironment::Demo,
364 http_timeout_secs: 71,
365 ..Default::default()
366 };
367
368 let debug_output = format!("{config:?}");
369 let redacted = format!("Some({REDACTED})");
370
371 assert!(!debug_output.contains(DATA_API_KEY));
372 assert!(!debug_output.contains(DATA_API_SECRET));
373 assert!(!debug_output.contains(DATA_API_PASSPHRASE));
374 assert!(debug_output.contains(&format!("api_key: {redacted}")));
375 assert!(debug_output.contains(&format!("api_secret: {redacted}")));
376 assert!(debug_output.contains(&format!("api_passphrase: {redacted}")));
377 assert!(debug_output.contains("environment: Demo"));
378 assert!(debug_output.contains("http_timeout_secs: 71"));
379 }
380
381 #[rstest]
382 fn test_exec_config_debug_redacts_credentials() {
383 let config = OKXExecutionClientConfig {
384 account_id: AccountId::from("OKX-042"),
385 api_key: Some(EXEC_API_KEY.into()),
386 api_secret: Some(EXEC_API_SECRET.into()),
387 api_passphrase: Some(EXEC_API_PASSPHRASE.into()),
388 max_retries: 13,
389 ..Default::default()
390 };
391
392 let debug_output = format!("{config:?}");
393 let redacted = format!("Some({REDACTED})");
394
395 assert!(!debug_output.contains(EXEC_API_KEY));
396 assert!(!debug_output.contains(EXEC_API_SECRET));
397 assert!(!debug_output.contains(EXEC_API_PASSPHRASE));
398 assert!(debug_output.contains(&format!("api_key: {redacted}")));
399 assert!(debug_output.contains(&format!("api_secret: {redacted}")));
400 assert!(debug_output.contains(&format!("api_passphrase: {redacted}")));
401 assert!(debug_output.contains("OKX-042"));
402 assert!(debug_output.contains("max_retries: 13"));
403 }
404
405 #[rstest]
406 fn test_config_debug_handles_unset_and_partial_credentials() {
407 let data_debug = format!("{:?}", OKXDataClientConfig::default());
408 let exec_debug = format!(
409 "{:?}",
410 OKXExecutionClientConfig {
411 api_secret: Some(String::new().into()),
412 ..Default::default()
413 }
414 );
415
416 assert!(data_debug.contains("api_key: None"));
417 assert!(data_debug.contains("api_secret: None"));
418 assert!(data_debug.contains("api_passphrase: None"));
419 assert!(exec_debug.contains("api_key: None"));
420 assert!(exec_debug.contains(&format!("api_secret: Some({REDACTED})")));
421 assert!(exec_debug.contains("api_passphrase: None"));
422 }
423
424 #[rstest]
425 fn test_data_config_toml_minimal() {
426 let config: OKXDataClientConfig = toml::from_str(
427 r#"
428environment = "demo"
429instrument_types = ["SPOT", "SWAP"]
430http_timeout_secs = 90
431"#,
432 )
433 .unwrap();
434
435 assert_eq!(config.environment, OKXEnvironment::Demo);
436 assert_eq!(
437 config.instrument_types,
438 vec![OKXInstrumentType::Spot, OKXInstrumentType::Swap]
439 );
440 assert_eq!(config.http_timeout_secs, 90);
441 assert!(!config.load_spreads);
442 assert_eq!(config.book_stale_check_interval_secs, 5);
443 assert_eq!(config.book_stale_threshold_secs, 30);
444 assert_eq!(config.book_snapshot_timeout_secs, 10);
445 }
446
447 #[rstest]
448 fn test_data_config_toml_load_spreads() {
449 let config: OKXDataClientConfig = toml::from_str(
450 "
451load_spreads = true
452",
453 )
454 .unwrap();
455
456 assert!(config.load_spreads);
457 }
458
459 #[rstest]
460 fn test_data_config_toml_book_stale_settings() {
461 let config: OKXDataClientConfig = toml::from_str(
462 "
463book_stale_check_interval_secs = 2
464book_stale_threshold_secs = 7
465book_snapshot_timeout_secs = 4
466",
467 )
468 .unwrap();
469
470 assert_eq!(config.book_stale_check_interval_secs, 2);
471 assert_eq!(config.book_stale_threshold_secs, 7);
472 assert_eq!(config.book_snapshot_timeout_secs, 4);
473 }
474
475 #[rstest]
476 fn test_exec_config_toml_empty_uses_defaults() {
477 let config: OKXExecutionClientConfig = toml::from_str("").unwrap();
478 let expected = OKXExecutionClientConfig::default();
479 assert_eq!(config.account_id, expected.account_id);
480 assert_eq!(config.environment, expected.environment);
481 assert_eq!(config.instrument_types, expected.instrument_types);
482 assert_eq!(config.http_timeout_secs, expected.http_timeout_secs);
483 assert_eq!(config.load_spreads, expected.load_spreads);
484 assert_eq!(config.use_mm_mass_cancel, expected.use_mm_mass_cancel);
485 assert_eq!(config.transport_backend, expected.transport_backend);
486 }
487
488 #[rstest]
489 fn test_exec_config_toml_rejects_removed_fills_channel_key() {
490 let result: Result<OKXExecutionClientConfig, _> =
492 toml::from_str("use_fills_channel = true\n");
493 assert!(result.is_err());
494 }
495
496 #[rstest]
497 fn test_exec_config_toml_load_spreads() {
498 let config: OKXExecutionClientConfig = toml::from_str(
499 "
500load_spreads = true
501",
502 )
503 .unwrap();
504
505 assert!(config.load_spreads);
506 }
507
508 #[rstest]
509 fn test_data_config_default_region_is_global() {
510 let config = OKXDataClientConfig::default();
511
512 assert_eq!(config.region, OKXRegion::Global);
513 assert_eq!(config.http_base_url(), "https://www.okx.com");
514 assert_eq!(config.ws_public_url(), "wss://ws.okx.com:8443/ws/v5/public");
515 }
516
517 #[rstest]
518 fn test_data_config_eea_region_urls() {
519 let config = OKXDataClientConfig::builder()
520 .region(OKXRegion::Eea)
521 .build();
522
523 assert_eq!(config.http_base_url(), "https://eea.okx.com");
524 assert_eq!(
525 config.ws_public_url(),
526 "wss://wseea.okx.com:8443/ws/v5/public"
527 );
528 assert_eq!(
529 config.ws_business_url(),
530 "wss://wseea.okx.com:8443/ws/v5/business"
531 );
532 }
533
534 #[rstest]
535 fn test_exec_config_eea_region_urls() {
536 let config = OKXExecutionClientConfig::builder()
537 .region(OKXRegion::Eea)
538 .build();
539
540 assert_eq!(config.http_base_url(), "https://eea.okx.com");
541 assert_eq!(
542 config.ws_private_url(),
543 "wss://wseea.okx.com:8443/ws/v5/private"
544 );
545 assert_eq!(
546 config.ws_business_url(),
547 "wss://wseea.okx.com:8443/ws/v5/business"
548 );
549 }
550
551 #[rstest]
552 fn test_config_region_override_takes_precedence() {
553 let config = OKXDataClientConfig::builder()
554 .region(OKXRegion::Eea)
555 .base_url_http("https://custom.proxy".to_string())
556 .build();
557
558 assert_eq!(config.http_base_url(), "https://custom.proxy");
559 }
560
561 #[rstest]
562 fn test_data_config_toml_region() {
563 let config: OKXDataClientConfig = toml::from_str(
564 r#"
565region = "eea"
566"#,
567 )
568 .unwrap();
569
570 assert_eq!(config.region, OKXRegion::Eea);
571 }
572
573 #[rstest]
574 fn test_exec_config_auth_timeout_secs() {
575 assert_eq!(OKXExecutionClientConfig::default().auth_timeout_secs, None);
576
577 let exec = OKXExecutionClientConfig::builder()
578 .auth_timeout_secs(4)
579 .build();
580 assert_eq!(exec.auth_timeout_secs, Some(4));
581
582 let exec: OKXExecutionClientConfig = toml::from_str("auth_timeout_secs = 8\n").unwrap();
583 assert_eq!(exec.auth_timeout_secs, Some(8));
584 }
585}