use std::collections::HashMap;
use nautilus_model::identifiers::AccountId;
use nautilus_network::websocket::TransportBackend;
use serde::{Deserialize, Serialize};
use crate::common::{
enums::{BybitEnvironment, BybitMarginMode, BybitPositionMode, BybitProductType},
urls::{bybit_http_base_url, bybit_ws_private_url, bybit_ws_public_url, bybit_ws_trade_url},
};
#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
#[serde(default, deny_unknown_fields)]
#[cfg_attr(
feature = "python",
pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.bybit", from_py_object)
)]
#[cfg_attr(
feature = "python",
pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.bybit")
)]
pub struct BybitDataClientConfig {
pub api_key: Option<String>,
pub api_secret: Option<String>,
#[builder(default = vec![BybitProductType::Linear])]
pub product_types: Vec<BybitProductType>,
#[builder(default = BybitEnvironment::Mainnet)]
pub environment: BybitEnvironment,
pub base_url_http: Option<String>,
pub base_url_ws_public: Option<String>,
pub base_url_ws_private: Option<String>,
pub proxy_url: Option<String>,
#[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 = 20)]
pub heartbeat_interval_secs: u64,
#[builder(default = 5_000)]
pub recv_window_ms: u64,
pub update_instruments_interval_mins: Option<u64>,
pub instrument_status_poll_secs: Option<u64>,
#[builder(default)]
pub transport_backend: TransportBackend,
}
impl Default for BybitDataClientConfig {
fn default() -> Self {
Self {
update_instruments_interval_mins: Some(60),
instrument_status_poll_secs: Some(60),
..Self::builder().build()
}
}
}
impl BybitDataClientConfig {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn has_api_credentials(&self) -> bool {
self.api_key.is_some() && self.api_secret.is_some()
}
#[must_use]
pub fn http_base_url(&self) -> String {
self.base_url_http
.clone()
.unwrap_or_else(|| bybit_http_base_url(self.environment).to_string())
}
#[must_use]
pub fn ws_public_url(&self) -> String {
self.base_url_ws_public.clone().unwrap_or_else(|| {
let product_type = self
.product_types
.first()
.copied()
.unwrap_or(BybitProductType::Linear);
bybit_ws_public_url(product_type, self.environment)
})
}
#[must_use]
pub fn ws_public_url_for(&self, product_type: BybitProductType) -> String {
self.base_url_ws_public
.clone()
.unwrap_or_else(|| bybit_ws_public_url(product_type, self.environment))
}
#[must_use]
pub fn ws_private_url(&self) -> String {
self.base_url_ws_private
.clone()
.unwrap_or_else(|| bybit_ws_private_url(self.environment).to_string())
}
#[must_use]
pub fn requires_private_ws(&self) -> bool {
self.has_api_credentials()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
#[serde(default, deny_unknown_fields)]
#[cfg_attr(
feature = "python",
pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.bybit", from_py_object)
)]
#[cfg_attr(
feature = "python",
pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.bybit")
)]
pub struct BybitExecClientConfig {
pub api_key: Option<String>,
pub api_secret: Option<String>,
#[builder(default = vec![BybitProductType::Linear])]
pub product_types: Vec<BybitProductType>,
#[builder(default = BybitEnvironment::Mainnet)]
pub environment: BybitEnvironment,
pub base_url_http: Option<String>,
pub base_url_ws_private: Option<String>,
pub base_url_ws_trade: Option<String>,
pub proxy_url: Option<String>,
#[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 = 5)]
pub heartbeat_interval_secs: u64,
#[builder(default = 5_000)]
pub recv_window_ms: u64,
pub account_id: Option<AccountId>,
#[builder(default)]
pub use_spot_position_reports: bool,
pub futures_leverages: Option<HashMap<String, u32>>,
pub position_mode: Option<HashMap<String, BybitPositionMode>>,
pub margin_mode: Option<BybitMarginMode>,
#[builder(default)]
pub transport_backend: TransportBackend,
}
impl Default for BybitExecClientConfig {
fn default() -> Self {
Self::builder().build()
}
}
impl BybitExecClientConfig {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn has_api_credentials(&self) -> bool {
self.api_key.is_some() && self.api_secret.is_some()
}
#[must_use]
pub fn http_base_url(&self) -> String {
self.base_url_http
.clone()
.unwrap_or_else(|| bybit_http_base_url(self.environment).to_string())
}
#[must_use]
pub fn ws_private_url(&self) -> String {
self.base_url_ws_private
.clone()
.unwrap_or_else(|| bybit_ws_private_url(self.environment).to_string())
}
#[must_use]
pub fn ws_trade_url(&self) -> String {
self.base_url_ws_trade
.clone()
.unwrap_or_else(|| bybit_ws_trade_url(self.environment).to_string())
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[rstest]
fn test_data_config_default() {
let config = BybitDataClientConfig::default();
assert!(!config.has_api_credentials());
assert_eq!(config.product_types, vec![BybitProductType::Linear]);
assert_eq!(config.http_timeout_secs, 60);
assert_eq!(config.heartbeat_interval_secs, 20);
}
#[rstest]
fn test_data_config_with_credentials() {
let config = BybitDataClientConfig {
api_key: Some("test_key".to_string()),
api_secret: Some("test_secret".to_string()),
..Default::default()
};
assert!(config.has_api_credentials());
assert!(config.requires_private_ws());
}
#[rstest]
fn test_data_config_http_url_mainnet() {
let config = BybitDataClientConfig {
environment: BybitEnvironment::Mainnet,
..Default::default()
};
assert_eq!(config.http_base_url(), "https://api.bybit.com");
}
#[rstest]
fn test_data_config_http_url_testnet() {
let config = BybitDataClientConfig {
environment: BybitEnvironment::Testnet,
..Default::default()
};
assert_eq!(config.http_base_url(), "https://api-testnet.bybit.com");
}
#[rstest]
fn test_data_config_http_url_demo() {
let config = BybitDataClientConfig {
environment: BybitEnvironment::Demo,
..Default::default()
};
assert_eq!(config.http_base_url(), "https://api-demo.bybit.com");
}
#[rstest]
fn test_data_config_http_url_override() {
let custom_url = "https://custom.bybit.com";
let config = BybitDataClientConfig {
base_url_http: Some(custom_url.to_string()),
..Default::default()
};
assert_eq!(config.http_base_url(), custom_url);
}
#[rstest]
fn test_data_config_ws_public_url() {
let config = BybitDataClientConfig {
environment: BybitEnvironment::Mainnet,
..Default::default()
};
assert_eq!(
config.ws_public_url(),
"wss://stream.bybit.com/v5/public/linear"
);
}
#[rstest]
fn test_data_config_ws_public_url_for_spot() {
let config = BybitDataClientConfig {
environment: BybitEnvironment::Mainnet,
..Default::default()
};
assert_eq!(
config.ws_public_url_for(BybitProductType::Spot),
"wss://stream.bybit.com/v5/public/spot"
);
}
#[rstest]
fn test_data_config_ws_private_url() {
let config = BybitDataClientConfig {
environment: BybitEnvironment::Mainnet,
..Default::default()
};
assert_eq!(config.ws_private_url(), "wss://stream.bybit.com/v5/private");
}
#[rstest]
fn test_data_config_ws_private_url_testnet() {
let config = BybitDataClientConfig {
environment: BybitEnvironment::Testnet,
..Default::default()
};
assert_eq!(
config.ws_private_url(),
"wss://stream-testnet.bybit.com/v5/private"
);
}
#[rstest]
fn test_exec_config_default() {
let config = BybitExecClientConfig::default();
assert!(!config.has_api_credentials());
assert_eq!(config.product_types, vec![BybitProductType::Linear]);
assert_eq!(config.http_timeout_secs, 60);
assert_eq!(config.heartbeat_interval_secs, 5);
}
#[rstest]
fn test_exec_config_with_credentials() {
let config = BybitExecClientConfig {
api_key: Some("test_key".to_string()),
api_secret: Some("test_secret".to_string()),
..Default::default()
};
assert!(config.has_api_credentials());
}
#[rstest]
fn test_exec_config_urls() {
let config = BybitExecClientConfig {
environment: BybitEnvironment::Mainnet,
..Default::default()
};
assert_eq!(config.http_base_url(), "https://api.bybit.com");
assert_eq!(config.ws_private_url(), "wss://stream.bybit.com/v5/private");
assert_eq!(config.ws_trade_url(), "wss://stream.bybit.com/v5/trade");
}
#[rstest]
fn test_exec_config_urls_testnet() {
let config = BybitExecClientConfig {
environment: BybitEnvironment::Testnet,
..Default::default()
};
assert_eq!(config.http_base_url(), "https://api-testnet.bybit.com");
assert_eq!(
config.ws_private_url(),
"wss://stream-testnet.bybit.com/v5/private"
);
assert_eq!(
config.ws_trade_url(),
"wss://stream-testnet.bybit.com/v5/trade"
);
}
#[rstest]
fn test_exec_config_custom_urls() {
let config = BybitExecClientConfig {
base_url_http: Some("https://custom-http.bybit.com".to_string()),
base_url_ws_private: Some("wss://custom-private.bybit.com".to_string()),
base_url_ws_trade: Some("wss://custom-trade.bybit.com".to_string()),
..Default::default()
};
assert_eq!(config.http_base_url(), "https://custom-http.bybit.com");
assert_eq!(config.ws_private_url(), "wss://custom-private.bybit.com");
assert_eq!(config.ws_trade_url(), "wss://custom-trade.bybit.com");
}
#[rstest]
fn test_data_config_toml_minimal() {
let config: BybitDataClientConfig = toml::from_str(
r#"
environment = "testnet"
product_types = ["spot", "linear"]
http_timeout_secs = 45
"#,
)
.unwrap();
assert_eq!(config.environment, BybitEnvironment::Testnet);
assert_eq!(
config.product_types,
vec![BybitProductType::Spot, BybitProductType::Linear]
);
assert_eq!(config.http_timeout_secs, 45);
}
#[rstest]
fn test_exec_config_toml_empty_uses_defaults() {
let config: BybitExecClientConfig = toml::from_str("").unwrap();
let expected = BybitExecClientConfig::default();
assert_eq!(config.environment, expected.environment);
assert_eq!(config.product_types, expected.product_types);
assert_eq!(config.http_timeout_secs, expected.http_timeout_secs);
assert_eq!(
config.heartbeat_interval_secs,
expected.heartbeat_interval_secs,
);
assert_eq!(config.recv_window_ms, expected.recv_window_ms);
assert_eq!(config.transport_backend, expected.transport_backend);
}
}