use nautilus_model::identifiers::{AccountId, TraderId};
use nautilus_network::websocket::TransportBackend;
use serde::{Deserialize, Serialize};
use crate::common::{
credential::credential_env_vars,
enums::{
OKXContractType, OKXEnvironment, OKXInstrumentType, OKXMarginMode, OKXRegion, OKXVipLevel,
},
urls::{
get_http_base_url, get_ws_base_url_business, get_ws_base_url_private,
get_ws_base_url_public,
},
};
#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
#[serde(default, deny_unknown_fields)]
#[cfg_attr(
feature = "python",
pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.okx", from_py_object)
)]
#[cfg_attr(
feature = "python",
pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.okx")
)]
pub struct OKXDataClientConfig {
pub api_key: Option<String>,
pub api_secret: Option<String>,
pub api_passphrase: Option<String>,
#[builder(default = vec![OKXInstrumentType::Spot])]
pub instrument_types: Vec<OKXInstrumentType>,
pub contract_types: Option<Vec<OKXContractType>>,
#[builder(default)]
pub load_spreads: bool,
pub instrument_families: Option<Vec<String>>,
pub base_url_http: Option<String>,
pub base_url_ws_public: Option<String>,
pub base_url_ws_business: Option<String>,
pub proxy_url: Option<String>,
#[builder(default)]
pub environment: OKXEnvironment,
#[builder(default)]
pub region: OKXRegion,
#[builder(default = 60)]
pub http_timeout_secs: u64,
#[builder(default = 3)]
pub max_retries: u32,
#[builder(default = 1_000)]
pub retry_delay_initial_ms: u64,
#[builder(default = 10_000)]
pub retry_delay_max_ms: u64,
#[builder(default = 60)]
pub update_instruments_interval_mins: u64,
#[builder(default = 5)]
pub book_stale_check_interval_secs: u64,
#[builder(default = 30)]
pub book_stale_threshold_secs: u64,
#[builder(default = 3)]
pub book_snapshot_timeout_secs: u64,
pub vip_level: Option<OKXVipLevel>,
#[builder(default)]
pub transport_backend: TransportBackend,
}
#[cfg(feature = "python")]
nautilus_core::impl_pyo3_config_getters!(OKXDataClientConfig {
instrument_types: Vec<OKXInstrumentType>,
environment: OKXEnvironment,
region: OKXRegion,
base_url_http: Option<String>,
base_url_ws_public: Option<String>,
base_url_ws_business: Option<String>,
http_timeout_secs: u64,
max_retries: u32,
retry_delay_initial_ms: u64,
retry_delay_max_ms: u64,
update_instruments_interval_mins: u64,
book_stale_check_interval_secs: u64,
book_stale_threshold_secs: u64,
book_snapshot_timeout_secs: u64,
vip_level: Option<OKXVipLevel>,
load_spreads: bool,
transport_backend: TransportBackend,
});
impl Default for OKXDataClientConfig {
fn default() -> Self {
Self::builder().build()
}
}
impl OKXDataClientConfig {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn has_api_credentials(&self) -> bool {
let (key_var, secret_var, passphrase_var) = credential_env_vars();
let has_key = self.api_key.is_some() || std::env::var(key_var).is_ok();
let has_secret = self.api_secret.is_some() || std::env::var(secret_var).is_ok();
let has_passphrase = self.api_passphrase.is_some() || std::env::var(passphrase_var).is_ok();
has_key && has_secret && has_passphrase
}
#[must_use]
pub fn http_base_url(&self) -> String {
self.base_url_http
.clone()
.unwrap_or_else(|| get_http_base_url(self.region).to_string())
}
#[must_use]
pub fn ws_public_url(&self) -> String {
self.base_url_ws_public
.clone()
.unwrap_or_else(|| get_ws_base_url_public(self.region, self.environment).to_string())
}
#[must_use]
pub fn ws_business_url(&self) -> String {
self.base_url_ws_business
.clone()
.unwrap_or_else(|| get_ws_base_url_business(self.region, self.environment).to_string())
}
#[must_use]
pub fn requires_business_ws(&self) -> bool {
true
}
}
#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
#[serde(default, deny_unknown_fields)]
#[cfg_attr(
feature = "python",
pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.okx", from_py_object)
)]
#[cfg_attr(
feature = "python",
pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.okx")
)]
pub struct OKXExecClientConfig {
#[builder(default = TraderId::from("TRADER-001"))]
pub trader_id: TraderId,
#[builder(default = AccountId::from("OKX-001"))]
pub account_id: AccountId,
pub api_key: Option<String>,
pub api_secret: Option<String>,
pub api_passphrase: Option<String>,
#[builder(default = vec![OKXInstrumentType::Spot])]
pub instrument_types: Vec<OKXInstrumentType>,
pub contract_types: Option<Vec<OKXContractType>>,
pub instrument_families: Option<Vec<String>>,
pub base_url_http: Option<String>,
pub base_url_ws_private: Option<String>,
pub base_url_ws_business: Option<String>,
pub proxy_url: Option<String>,
#[builder(default)]
pub environment: OKXEnvironment,
#[builder(default)]
pub region: OKXRegion,
#[builder(default = 60)]
pub http_timeout_secs: u64,
#[builder(default)]
pub use_fills_channel: bool,
#[builder(default)]
pub load_spreads: bool,
#[builder(default)]
pub use_mm_mass_cancel: bool,
#[builder(default = 3)]
pub max_retries: u32,
#[builder(default = 1_000)]
pub retry_delay_initial_ms: u64,
#[builder(default = 10_000)]
pub retry_delay_max_ms: u64,
pub margin_mode: Option<OKXMarginMode>,
#[builder(default)]
pub use_spot_margin: bool,
pub auth_timeout_secs: Option<u64>,
#[builder(default)]
pub transport_backend: TransportBackend,
}
#[cfg(feature = "python")]
nautilus_core::impl_pyo3_config_getters!(OKXExecClientConfig {
trader_id: TraderId,
account_id: AccountId,
instrument_types: Vec<OKXInstrumentType>,
environment: OKXEnvironment,
region: OKXRegion,
base_url_http: Option<String>,
base_url_ws_private: Option<String>,
base_url_ws_business: Option<String>,
http_timeout_secs: u64,
max_retries: u32,
retry_delay_initial_ms: u64,
retry_delay_max_ms: u64,
margin_mode: Option<OKXMarginMode>,
load_spreads: bool,
auth_timeout_secs: Option<u64>,
transport_backend: TransportBackend,
});
impl Default for OKXExecClientConfig {
fn default() -> Self {
Self::builder().build()
}
}
impl OKXExecClientConfig {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn has_api_credentials(&self) -> bool {
let (key_var, secret_var, passphrase_var) = credential_env_vars();
let has_key = self.api_key.is_some() || std::env::var(key_var).is_ok();
let has_secret = self.api_secret.is_some() || std::env::var(secret_var).is_ok();
let has_passphrase = self.api_passphrase.is_some() || std::env::var(passphrase_var).is_ok();
has_key && has_secret && has_passphrase
}
#[must_use]
pub fn http_base_url(&self) -> String {
self.base_url_http
.clone()
.unwrap_or_else(|| get_http_base_url(self.region).to_string())
}
#[must_use]
pub fn ws_private_url(&self) -> String {
self.base_url_ws_private
.clone()
.unwrap_or_else(|| get_ws_base_url_private(self.region, self.environment).to_string())
}
#[must_use]
pub fn ws_business_url(&self) -> String {
self.base_url_ws_business
.clone()
.unwrap_or_else(|| get_ws_base_url_business(self.region, self.environment).to_string())
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[rstest]
fn test_data_config_toml_minimal() {
let config: OKXDataClientConfig = toml::from_str(
r#"
environment = "demo"
instrument_types = ["SPOT", "SWAP"]
http_timeout_secs = 90
"#,
)
.unwrap();
assert_eq!(config.environment, OKXEnvironment::Demo);
assert_eq!(
config.instrument_types,
vec![OKXInstrumentType::Spot, OKXInstrumentType::Swap]
);
assert_eq!(config.http_timeout_secs, 90);
assert!(!config.load_spreads);
assert_eq!(config.book_stale_check_interval_secs, 5);
assert_eq!(config.book_stale_threshold_secs, 30);
assert_eq!(config.book_snapshot_timeout_secs, 3);
}
#[rstest]
fn test_data_config_toml_load_spreads() {
let config: OKXDataClientConfig = toml::from_str(
"
load_spreads = true
",
)
.unwrap();
assert!(config.load_spreads);
}
#[rstest]
fn test_data_config_toml_book_stale_settings() {
let config: OKXDataClientConfig = toml::from_str(
"
book_stale_check_interval_secs = 2
book_stale_threshold_secs = 7
book_snapshot_timeout_secs = 4
",
)
.unwrap();
assert_eq!(config.book_stale_check_interval_secs, 2);
assert_eq!(config.book_stale_threshold_secs, 7);
assert_eq!(config.book_snapshot_timeout_secs, 4);
}
#[rstest]
fn test_exec_config_toml_empty_uses_defaults() {
let config: OKXExecClientConfig = toml::from_str("").unwrap();
let expected = OKXExecClientConfig::default();
assert_eq!(config.trader_id, expected.trader_id);
assert_eq!(config.account_id, expected.account_id);
assert_eq!(config.environment, expected.environment);
assert_eq!(config.instrument_types, expected.instrument_types);
assert_eq!(config.http_timeout_secs, expected.http_timeout_secs);
assert_eq!(config.use_fills_channel, expected.use_fills_channel);
assert_eq!(config.load_spreads, expected.load_spreads);
assert_eq!(config.use_mm_mass_cancel, expected.use_mm_mass_cancel);
assert_eq!(config.transport_backend, expected.transport_backend);
}
#[rstest]
fn test_exec_config_toml_load_spreads() {
let config: OKXExecClientConfig = toml::from_str(
"
load_spreads = true
",
)
.unwrap();
assert!(config.load_spreads);
}
#[rstest]
fn test_data_config_default_region_is_global() {
let config = OKXDataClientConfig::default();
assert_eq!(config.region, OKXRegion::Global);
assert_eq!(config.http_base_url(), "https://www.okx.com");
assert_eq!(config.ws_public_url(), "wss://ws.okx.com:8443/ws/v5/public");
}
#[rstest]
fn test_data_config_eea_region_urls() {
let config = OKXDataClientConfig::builder()
.region(OKXRegion::Eea)
.build();
assert_eq!(config.http_base_url(), "https://eea.okx.com");
assert_eq!(
config.ws_public_url(),
"wss://wseea.okx.com:8443/ws/v5/public"
);
assert_eq!(
config.ws_business_url(),
"wss://wseea.okx.com:8443/ws/v5/business"
);
}
#[rstest]
fn test_exec_config_eea_region_urls() {
let config = OKXExecClientConfig::builder()
.region(OKXRegion::Eea)
.build();
assert_eq!(config.http_base_url(), "https://eea.okx.com");
assert_eq!(
config.ws_private_url(),
"wss://wseea.okx.com:8443/ws/v5/private"
);
assert_eq!(
config.ws_business_url(),
"wss://wseea.okx.com:8443/ws/v5/business"
);
}
#[rstest]
fn test_config_region_override_takes_precedence() {
let config = OKXDataClientConfig::builder()
.region(OKXRegion::Eea)
.base_url_http("https://custom.proxy".to_string())
.build();
assert_eq!(config.http_base_url(), "https://custom.proxy");
}
#[rstest]
fn test_data_config_toml_region() {
let config: OKXDataClientConfig = toml::from_str(
r#"
region = "eea"
"#,
)
.unwrap();
assert_eq!(config.region, OKXRegion::Eea);
}
#[rstest]
fn test_exec_config_auth_timeout_secs() {
assert_eq!(OKXExecClientConfig::default().auth_timeout_secs, None);
let exec = OKXExecClientConfig::builder().auth_timeout_secs(4).build();
assert_eq!(exec.auth_timeout_secs, Some(4));
let exec: OKXExecClientConfig = toml::from_str("auth_timeout_secs = 8\n").unwrap();
assert_eq!(exec.auth_timeout_secs, Some(8));
}
}