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 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/// Configuration for the OKX data client.
34#[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    /// Optional API key for authenticated endpoints.
46    pub api_key: Option<String>,
47    /// Optional API secret for authenticated endpoints.
48    pub api_secret: Option<String>,
49    /// Optional API passphrase for authenticated endpoints.
50    pub api_passphrase: Option<String>,
51    /// Instrument types to load and subscribe to.
52    #[builder(default = vec![OKXInstrumentType::Spot])]
53    pub instrument_types: Vec<OKXInstrumentType>,
54    /// Contract type filter applied to loaded instruments.
55    pub contract_types: Option<Vec<OKXContractType>>,
56    /// Whether to load spread trading instruments from the separate spread endpoint.
57    #[builder(default)]
58    pub load_spreads: bool,
59    /// Instrument families to load (e.g., "BTC-USD", "ETH-USD").
60    /// Required for OPTIONS. Optional for FUTURES/SWAP. Not applicable for SPOT/MARGIN.
61    pub instrument_families: Option<Vec<String>>,
62    /// Optional override for the HTTP base URL.
63    pub base_url_http: Option<String>,
64    /// Optional override for the public WebSocket URL.
65    pub base_url_ws_public: Option<String>,
66    /// Optional override for the business WebSocket URL.
67    pub base_url_ws_business: Option<String>,
68    /// Optional proxy URL for HTTP and WebSocket transports.
69    pub proxy_url: Option<String>,
70    /// The API environment (live or demo).
71    #[builder(default)]
72    pub environment: OKXEnvironment,
73    /// The API region (global, EEA, or US).
74    #[builder(default)]
75    pub region: OKXRegion,
76    /// HTTP timeout in seconds.
77    #[builder(default = 60)]
78    pub http_timeout_secs: u64,
79    /// Maximum retry attempts for requests.
80    #[builder(default = 3)]
81    pub max_retries: u32,
82    /// Initial retry delay in milliseconds.
83    #[builder(default = 1_000)]
84    pub retry_delay_initial_ms: u64,
85    /// Maximum retry delay in milliseconds.
86    #[builder(default = 10_000)]
87    pub retry_delay_max_ms: u64,
88    /// Interval for refreshing instruments in minutes.
89    #[builder(default = 60)]
90    pub update_instruments_interval_mins: u64,
91    /// Optional VIP level that unlocks additional subscriptions.
92    pub vip_level: Option<OKXVipLevel>,
93    /// WebSocket transport backend (defaults to `Tungstenite`).
94    #[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    /// Creates a new configuration with default settings.
106    #[must_use]
107    pub fn new() -> Self {
108        Self::default()
109    }
110
111    /// Returns `true` when all API credential fields are available (in config or env vars).
112    #[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    /// Returns the HTTP base URL, falling back to the region default when unset.
122    #[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    /// Returns the public WebSocket URL, respecting the region, environment, and overrides.
130    #[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    /// Returns the business WebSocket URL, respecting the region, environment, and overrides.
138    #[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    /// Returns `true` when the business WebSocket should be instantiated.
146    ///
147    /// The business WebSocket carries public candle data and does not
148    /// require authentication, so it is always needed.
149    #[must_use]
150    pub fn requires_business_ws(&self) -> bool {
151        true
152    }
153}
154
155/// Configuration for the OKX execution client.
156#[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    /// The trader ID for the client.
168    #[builder(default = TraderId::from("TRADER-001"))]
169    pub trader_id: TraderId,
170    /// The account ID for the client.
171    #[builder(default = AccountId::from("OKX-001"))]
172    pub account_id: AccountId,
173    /// Optional API key for authenticated endpoints.
174    pub api_key: Option<String>,
175    /// Optional API secret for authenticated endpoints.
176    pub api_secret: Option<String>,
177    /// Optional API passphrase for authenticated endpoints.
178    pub api_passphrase: Option<String>,
179    /// Instrument types the execution client should support.
180    #[builder(default = vec![OKXInstrumentType::Spot])]
181    pub instrument_types: Vec<OKXInstrumentType>,
182    /// Contract type filter applied to operations.
183    pub contract_types: Option<Vec<OKXContractType>>,
184    /// Instrument families to load (e.g., "BTC-USD", "ETH-USD").
185    /// Required for OPTIONS. Optional for FUTURES/SWAP. Not applicable for SPOT/MARGIN.
186    pub instrument_families: Option<Vec<String>>,
187    /// Optional override for the HTTP base URL.
188    pub base_url_http: Option<String>,
189    /// Optional override for the private WebSocket URL.
190    pub base_url_ws_private: Option<String>,
191    /// Optional override for the business WebSocket URL.
192    pub base_url_ws_business: Option<String>,
193    /// Optional proxy URL for HTTP and WebSocket transports.
194    pub proxy_url: Option<String>,
195    /// The API environment (live or demo).
196    #[builder(default)]
197    pub environment: OKXEnvironment,
198    /// The API region (global, EEA, or US).
199    #[builder(default)]
200    pub region: OKXRegion,
201    /// HTTP timeout in seconds.
202    #[builder(default = 60)]
203    pub http_timeout_secs: u64,
204    /// Enables consumption of the fills WebSocket channel when true.
205    #[builder(default)]
206    pub use_fills_channel: bool,
207    /// Whether to subscribe to spread order updates from the separate spread channel.
208    #[builder(default)]
209    pub load_spreads: bool,
210    /// Enables mass-cancel support when true.
211    #[builder(default)]
212    pub use_mm_mass_cancel: bool,
213    /// Maximum retry attempts for requests.
214    #[builder(default = 3)]
215    pub max_retries: u32,
216    /// Initial retry delay in milliseconds.
217    #[builder(default = 1_000)]
218    pub retry_delay_initial_ms: u64,
219    /// Maximum retry delay in milliseconds.
220    #[builder(default = 10_000)]
221    pub retry_delay_max_ms: u64,
222    /// Optional margin mode (CROSS or ISOLATED) for margin/derivative accounts.
223    pub margin_mode: Option<OKXMarginMode>,
224    /// Enables margin/leverage for SPOT trading when true.
225    #[builder(default)]
226    pub use_spot_margin: bool,
227    /// WebSocket transport backend (defaults to `Tungstenite`).
228    #[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    /// Creates a new configuration with default settings.
240    #[must_use]
241    pub fn new() -> Self {
242        Self::default()
243    }
244
245    /// Returns `true` when all API credential fields are available (in config or env vars).
246    #[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    /// Returns the HTTP base URL, falling back to the region default when unset.
256    #[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    /// Returns the private WebSocket URL, respecting the region, environment, and overrides.
264    #[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    /// Returns the business WebSocket URL, respecting the region, environment, and overrides.
272    #[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}