1use nautilus_model::identifiers::{AccountId, TraderId};
19use nautilus_network::websocket::TransportBackend;
20use serde::{Deserialize, Serialize};
21
22use crate::common::{
23 credential::credential_env_vars,
24 enums::{
25 OKXContractType, OKXEnvironment, OKXInstrumentType, OKXMarginMode, OKXRegion, OKXVipLevel,
26 },
27 urls::{
28 get_http_base_url, get_ws_base_url_business, get_ws_base_url_private,
29 get_ws_base_url_public,
30 },
31};
32
33#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
35#[serde(default, deny_unknown_fields)]
36#[cfg_attr(
37 feature = "python",
38 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.okx", from_py_object)
39)]
40#[cfg_attr(
41 feature = "python",
42 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.okx")
43)]
44pub struct OKXDataClientConfig {
45 pub api_key: Option<String>,
47 pub api_secret: Option<String>,
49 pub api_passphrase: Option<String>,
51 #[builder(default = vec![OKXInstrumentType::Spot])]
53 pub instrument_types: Vec<OKXInstrumentType>,
54 pub contract_types: Option<Vec<OKXContractType>>,
56 #[builder(default)]
58 pub load_spreads: bool,
59 pub instrument_families: Option<Vec<String>>,
62 pub base_url_http: Option<String>,
64 pub base_url_ws_public: Option<String>,
66 pub base_url_ws_business: Option<String>,
68 pub proxy_url: Option<String>,
70 #[builder(default)]
72 pub environment: OKXEnvironment,
73 #[builder(default)]
75 pub region: OKXRegion,
76 #[builder(default = 60)]
78 pub http_timeout_secs: u64,
79 #[builder(default = 3)]
81 pub max_retries: u32,
82 #[builder(default = 1_000)]
84 pub retry_delay_initial_ms: u64,
85 #[builder(default = 10_000)]
87 pub retry_delay_max_ms: u64,
88 #[builder(default = 60)]
90 pub update_instruments_interval_mins: u64,
91 pub vip_level: Option<OKXVipLevel>,
93 #[builder(default)]
95 pub transport_backend: TransportBackend,
96}
97
98impl Default for OKXDataClientConfig {
99 fn default() -> Self {
100 Self::builder().build()
101 }
102}
103
104impl OKXDataClientConfig {
105 #[must_use]
107 pub fn new() -> Self {
108 Self::default()
109 }
110
111 #[must_use]
113 pub fn has_api_credentials(&self) -> bool {
114 let (key_var, secret_var, passphrase_var) = credential_env_vars();
115 let has_key = self.api_key.is_some() || std::env::var(key_var).is_ok();
116 let has_secret = self.api_secret.is_some() || std::env::var(secret_var).is_ok();
117 let has_passphrase = self.api_passphrase.is_some() || std::env::var(passphrase_var).is_ok();
118 has_key && has_secret && has_passphrase
119 }
120
121 #[must_use]
123 pub fn http_base_url(&self) -> String {
124 self.base_url_http
125 .clone()
126 .unwrap_or_else(|| get_http_base_url(self.region).to_string())
127 }
128
129 #[must_use]
131 pub fn ws_public_url(&self) -> String {
132 self.base_url_ws_public
133 .clone()
134 .unwrap_or_else(|| get_ws_base_url_public(self.region, self.environment).to_string())
135 }
136
137 #[must_use]
139 pub fn ws_business_url(&self) -> String {
140 self.base_url_ws_business
141 .clone()
142 .unwrap_or_else(|| get_ws_base_url_business(self.region, self.environment).to_string())
143 }
144
145 #[must_use]
150 pub fn requires_business_ws(&self) -> bool {
151 true
152 }
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
157#[serde(default, deny_unknown_fields)]
158#[cfg_attr(
159 feature = "python",
160 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.okx", from_py_object)
161)]
162#[cfg_attr(
163 feature = "python",
164 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.okx")
165)]
166pub struct OKXExecClientConfig {
167 #[builder(default = TraderId::from("TRADER-001"))]
169 pub trader_id: TraderId,
170 #[builder(default = AccountId::from("OKX-001"))]
172 pub account_id: AccountId,
173 pub api_key: Option<String>,
175 pub api_secret: Option<String>,
177 pub api_passphrase: Option<String>,
179 #[builder(default = vec![OKXInstrumentType::Spot])]
181 pub instrument_types: Vec<OKXInstrumentType>,
182 pub contract_types: Option<Vec<OKXContractType>>,
184 pub instrument_families: Option<Vec<String>>,
187 pub base_url_http: Option<String>,
189 pub base_url_ws_private: Option<String>,
191 pub base_url_ws_business: Option<String>,
193 pub proxy_url: Option<String>,
195 #[builder(default)]
197 pub environment: OKXEnvironment,
198 #[builder(default)]
200 pub region: OKXRegion,
201 #[builder(default = 60)]
203 pub http_timeout_secs: u64,
204 #[builder(default)]
206 pub use_fills_channel: bool,
207 #[builder(default)]
209 pub load_spreads: bool,
210 #[builder(default)]
212 pub use_mm_mass_cancel: bool,
213 #[builder(default = 3)]
215 pub max_retries: u32,
216 #[builder(default = 1_000)]
218 pub retry_delay_initial_ms: u64,
219 #[builder(default = 10_000)]
221 pub retry_delay_max_ms: u64,
222 pub margin_mode: Option<OKXMarginMode>,
224 #[builder(default)]
226 pub use_spot_margin: bool,
227 #[builder(default)]
229 pub transport_backend: TransportBackend,
230}
231
232impl Default for OKXExecClientConfig {
233 fn default() -> Self {
234 Self::builder().build()
235 }
236}
237
238impl OKXExecClientConfig {
239 #[must_use]
241 pub fn new() -> Self {
242 Self::default()
243 }
244
245 #[must_use]
247 pub fn has_api_credentials(&self) -> bool {
248 let (key_var, secret_var, passphrase_var) = credential_env_vars();
249 let has_key = self.api_key.is_some() || std::env::var(key_var).is_ok();
250 let has_secret = self.api_secret.is_some() || std::env::var(secret_var).is_ok();
251 let has_passphrase = self.api_passphrase.is_some() || std::env::var(passphrase_var).is_ok();
252 has_key && has_secret && has_passphrase
253 }
254
255 #[must_use]
257 pub fn http_base_url(&self) -> String {
258 self.base_url_http
259 .clone()
260 .unwrap_or_else(|| get_http_base_url(self.region).to_string())
261 }
262
263 #[must_use]
265 pub fn ws_private_url(&self) -> String {
266 self.base_url_ws_private
267 .clone()
268 .unwrap_or_else(|| get_ws_base_url_private(self.region, self.environment).to_string())
269 }
270
271 #[must_use]
273 pub fn ws_business_url(&self) -> String {
274 self.base_url_ws_business
275 .clone()
276 .unwrap_or_else(|| get_ws_base_url_business(self.region, self.environment).to_string())
277 }
278}
279
280#[cfg(test)]
281mod tests {
282 use rstest::rstest;
283
284 use super::*;
285
286 #[rstest]
287 fn test_data_config_toml_minimal() {
288 let config: OKXDataClientConfig = toml::from_str(
289 r#"
290environment = "demo"
291instrument_types = ["SPOT", "SWAP"]
292http_timeout_secs = 90
293"#,
294 )
295 .unwrap();
296
297 assert_eq!(config.environment, OKXEnvironment::Demo);
298 assert_eq!(
299 config.instrument_types,
300 vec![OKXInstrumentType::Spot, OKXInstrumentType::Swap]
301 );
302 assert_eq!(config.http_timeout_secs, 90);
303 assert!(!config.load_spreads);
304 }
305
306 #[rstest]
307 fn test_data_config_toml_load_spreads() {
308 let config: OKXDataClientConfig = toml::from_str(
309 "
310load_spreads = true
311",
312 )
313 .unwrap();
314
315 assert!(config.load_spreads);
316 }
317
318 #[rstest]
319 fn test_exec_config_toml_empty_uses_defaults() {
320 let config: OKXExecClientConfig = toml::from_str("").unwrap();
321 let expected = OKXExecClientConfig::default();
322
323 assert_eq!(config.trader_id, expected.trader_id);
324 assert_eq!(config.account_id, expected.account_id);
325 assert_eq!(config.environment, expected.environment);
326 assert_eq!(config.instrument_types, expected.instrument_types);
327 assert_eq!(config.http_timeout_secs, expected.http_timeout_secs);
328 assert_eq!(config.use_fills_channel, expected.use_fills_channel);
329 assert_eq!(config.load_spreads, expected.load_spreads);
330 assert_eq!(config.use_mm_mass_cancel, expected.use_mm_mass_cancel);
331 assert_eq!(config.transport_backend, expected.transport_backend);
332 }
333
334 #[rstest]
335 fn test_exec_config_toml_load_spreads() {
336 let config: OKXExecClientConfig = toml::from_str(
337 "
338load_spreads = true
339",
340 )
341 .unwrap();
342
343 assert!(config.load_spreads);
344 }
345
346 #[rstest]
347 fn test_data_config_default_region_is_global() {
348 let config = OKXDataClientConfig::default();
349
350 assert_eq!(config.region, OKXRegion::Global);
351 assert_eq!(config.http_base_url(), "https://www.okx.com");
352 assert_eq!(config.ws_public_url(), "wss://ws.okx.com:8443/ws/v5/public");
353 }
354
355 #[rstest]
356 fn test_data_config_eea_region_urls() {
357 let config = OKXDataClientConfig::builder()
358 .region(OKXRegion::Eea)
359 .build();
360
361 assert_eq!(config.http_base_url(), "https://eea.okx.com");
362 assert_eq!(
363 config.ws_public_url(),
364 "wss://wseea.okx.com:8443/ws/v5/public"
365 );
366 assert_eq!(
367 config.ws_business_url(),
368 "wss://wseea.okx.com:8443/ws/v5/business"
369 );
370 }
371
372 #[rstest]
373 fn test_exec_config_eea_region_urls() {
374 let config = OKXExecClientConfig::builder()
375 .region(OKXRegion::Eea)
376 .build();
377
378 assert_eq!(config.http_base_url(), "https://eea.okx.com");
379 assert_eq!(
380 config.ws_private_url(),
381 "wss://wseea.okx.com:8443/ws/v5/private"
382 );
383 assert_eq!(
384 config.ws_business_url(),
385 "wss://wseea.okx.com:8443/ws/v5/business"
386 );
387 }
388
389 #[rstest]
390 fn test_config_region_override_takes_precedence() {
391 let config = OKXDataClientConfig::builder()
392 .region(OKXRegion::Eea)
393 .base_url_http("https://custom.proxy".to_string())
394 .build();
395
396 assert_eq!(config.http_base_url(), "https://custom.proxy");
397 }
398
399 #[rstest]
400 fn test_data_config_toml_region() {
401 let config: OKXDataClientConfig = toml::from_str(
402 r#"
403region = "eea"
404"#,
405 )
406 .unwrap();
407
408 assert_eq!(config.region, OKXRegion::Eea);
409 }
410}