1use std::collections::HashMap;
19
20use nautilus_model::identifiers::AccountId;
21use nautilus_network::websocket::TransportBackend;
22use serde::{Deserialize, Serialize};
23
24use crate::common::{
25 enums::{BybitEnvironment, BybitMarginMode, BybitPositionMode, BybitProductType},
26 urls::{bybit_http_base_url, bybit_ws_private_url, bybit_ws_public_url, bybit_ws_trade_url},
27};
28
29#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
31#[serde(default, deny_unknown_fields)]
32#[cfg_attr(
33 feature = "python",
34 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.bybit", from_py_object)
35)]
36#[cfg_attr(
37 feature = "python",
38 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.bybit")
39)]
40pub struct BybitDataClientConfig {
41 pub api_key: Option<String>,
43 pub api_secret: Option<String>,
45 #[builder(default = vec![BybitProductType::Linear])]
47 pub product_types: Vec<BybitProductType>,
48 #[builder(default = BybitEnvironment::Mainnet)]
50 pub environment: BybitEnvironment,
51 pub base_url_http: Option<String>,
53 pub base_url_ws_public: Option<String>,
55 pub base_url_ws_private: Option<String>,
57 pub proxy_url: Option<String>,
59 #[builder(default = 60)]
61 pub http_timeout_secs: u64,
62 #[builder(default = 3)]
64 pub max_retries: u32,
65 #[builder(default = 1_000)]
67 pub retry_delay_initial_ms: u64,
68 #[builder(default = 10_000)]
70 pub retry_delay_max_ms: u64,
71 #[builder(default = 20)]
73 pub heartbeat_interval_secs: u64,
74 #[builder(default = 5_000)]
76 pub recv_window_ms: u64,
77 pub update_instruments_interval_mins: Option<u64>,
80 pub instrument_poll_interval_secs: Option<u64>,
83 #[builder(default)]
85 pub transport_backend: TransportBackend,
86}
87
88impl Default for BybitDataClientConfig {
89 fn default() -> Self {
90 Self {
91 update_instruments_interval_mins: Some(60),
92 instrument_poll_interval_secs: Some(60),
93 ..Self::builder().build()
94 }
95 }
96}
97
98impl BybitDataClientConfig {
99 #[must_use]
101 pub fn new() -> Self {
102 Self::default()
103 }
104
105 #[must_use]
107 pub fn has_api_credentials(&self) -> bool {
108 self.api_key.is_some() && self.api_secret.is_some()
109 }
110
111 #[must_use]
113 pub fn http_base_url(&self) -> String {
114 self.base_url_http
115 .clone()
116 .unwrap_or_else(|| bybit_http_base_url(self.environment).to_string())
117 }
118
119 #[must_use]
123 pub fn ws_public_url(&self) -> String {
124 self.base_url_ws_public.clone().unwrap_or_else(|| {
125 let product_type = self
126 .product_types
127 .first()
128 .copied()
129 .unwrap_or(BybitProductType::Linear);
130 bybit_ws_public_url(product_type, self.environment)
131 })
132 }
133
134 #[must_use]
136 pub fn ws_public_url_for(&self, product_type: BybitProductType) -> String {
137 self.base_url_ws_public
138 .clone()
139 .unwrap_or_else(|| bybit_ws_public_url(product_type, self.environment))
140 }
141
142 #[must_use]
144 pub fn ws_private_url(&self) -> String {
145 self.base_url_ws_private
146 .clone()
147 .unwrap_or_else(|| bybit_ws_private_url(self.environment).to_string())
148 }
149
150 #[must_use]
152 pub fn requires_private_ws(&self) -> bool {
153 self.has_api_credentials()
154 }
155}
156
157#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
159#[serde(default, deny_unknown_fields)]
160#[cfg_attr(
161 feature = "python",
162 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.bybit", from_py_object)
163)]
164#[cfg_attr(
165 feature = "python",
166 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.bybit")
167)]
168pub struct BybitExecClientConfig {
169 pub api_key: Option<String>,
171 pub api_secret: Option<String>,
173 #[builder(default = vec![BybitProductType::Linear])]
175 pub product_types: Vec<BybitProductType>,
176 #[builder(default = BybitEnvironment::Mainnet)]
178 pub environment: BybitEnvironment,
179 pub base_url_http: Option<String>,
181 pub base_url_ws_private: Option<String>,
183 pub base_url_ws_trade: Option<String>,
185 pub proxy_url: Option<String>,
187 #[builder(default = 60)]
189 pub http_timeout_secs: u64,
190 #[builder(default = 3)]
192 pub max_retries: u32,
193 #[builder(default = 1_000)]
195 pub retry_delay_initial_ms: u64,
196 #[builder(default = 10_000)]
198 pub retry_delay_max_ms: u64,
199 #[builder(default = 5)]
201 pub heartbeat_interval_secs: u64,
202 #[builder(default = 5_000)]
204 pub recv_window_ms: u64,
205 pub account_id: Option<AccountId>,
207 #[builder(default)]
209 pub use_spot_position_reports: bool,
210 pub futures_leverages: Option<HashMap<String, u32>>,
212 pub position_mode: Option<HashMap<String, BybitPositionMode>>,
214 pub margin_mode: Option<BybitMarginMode>,
216 #[builder(default)]
218 pub transport_backend: TransportBackend,
219}
220
221impl Default for BybitExecClientConfig {
222 fn default() -> Self {
223 Self::builder().build()
224 }
225}
226
227impl BybitExecClientConfig {
228 #[must_use]
230 pub fn new() -> Self {
231 Self::default()
232 }
233
234 #[must_use]
236 pub fn has_api_credentials(&self) -> bool {
237 self.api_key.is_some() && self.api_secret.is_some()
238 }
239
240 #[must_use]
242 pub fn http_base_url(&self) -> String {
243 self.base_url_http
244 .clone()
245 .unwrap_or_else(|| bybit_http_base_url(self.environment).to_string())
246 }
247
248 #[must_use]
250 pub fn ws_private_url(&self) -> String {
251 self.base_url_ws_private
252 .clone()
253 .unwrap_or_else(|| bybit_ws_private_url(self.environment).to_string())
254 }
255
256 #[must_use]
258 pub fn ws_trade_url(&self) -> String {
259 self.base_url_ws_trade
260 .clone()
261 .unwrap_or_else(|| bybit_ws_trade_url(self.environment).to_string())
262 }
263}
264#[cfg(test)]
265mod tests {
266 use rstest::rstest;
267
268 use super::*;
269
270 #[rstest]
271 fn test_data_config_default() {
272 let config = BybitDataClientConfig::default();
273
274 assert!(!config.has_api_credentials());
275 assert_eq!(config.product_types, vec![BybitProductType::Linear]);
276 assert_eq!(config.http_timeout_secs, 60);
277 assert_eq!(config.heartbeat_interval_secs, 20);
278 }
279
280 #[rstest]
281 fn test_data_config_with_credentials() {
282 let config = BybitDataClientConfig {
283 api_key: Some("test_key".to_string()),
284 api_secret: Some("test_secret".to_string()),
285 ..Default::default()
286 };
287
288 assert!(config.has_api_credentials());
289 assert!(config.requires_private_ws());
290 }
291
292 #[rstest]
293 fn test_data_config_http_url_mainnet() {
294 let config = BybitDataClientConfig {
295 environment: BybitEnvironment::Mainnet,
296 ..Default::default()
297 };
298
299 assert_eq!(config.http_base_url(), "https://api.bybit.com");
300 }
301
302 #[rstest]
303 fn test_data_config_http_url_testnet() {
304 let config = BybitDataClientConfig {
305 environment: BybitEnvironment::Testnet,
306 ..Default::default()
307 };
308
309 assert_eq!(config.http_base_url(), "https://api-testnet.bybit.com");
310 }
311
312 #[rstest]
313 fn test_data_config_http_url_demo() {
314 let config = BybitDataClientConfig {
315 environment: BybitEnvironment::Demo,
316 ..Default::default()
317 };
318
319 assert_eq!(config.http_base_url(), "https://api-demo.bybit.com");
320 }
321
322 #[rstest]
323 fn test_data_config_http_url_override() {
324 let custom_url = "https://custom.bybit.com";
325 let config = BybitDataClientConfig {
326 base_url_http: Some(custom_url.to_string()),
327 ..Default::default()
328 };
329
330 assert_eq!(config.http_base_url(), custom_url);
331 }
332
333 #[rstest]
334 fn test_data_config_ws_public_url() {
335 let config = BybitDataClientConfig {
336 environment: BybitEnvironment::Mainnet,
337 ..Default::default()
338 };
339
340 assert_eq!(
341 config.ws_public_url(),
342 "wss://stream.bybit.com/v5/public/linear"
343 );
344 }
345
346 #[rstest]
347 fn test_data_config_ws_public_url_for_spot() {
348 let config = BybitDataClientConfig {
349 environment: BybitEnvironment::Mainnet,
350 ..Default::default()
351 };
352
353 assert_eq!(
354 config.ws_public_url_for(BybitProductType::Spot),
355 "wss://stream.bybit.com/v5/public/spot"
356 );
357 }
358
359 #[rstest]
360 fn test_data_config_ws_private_url() {
361 let config = BybitDataClientConfig {
362 environment: BybitEnvironment::Mainnet,
363 ..Default::default()
364 };
365
366 assert_eq!(config.ws_private_url(), "wss://stream.bybit.com/v5/private");
367 }
368
369 #[rstest]
370 fn test_data_config_ws_private_url_testnet() {
371 let config = BybitDataClientConfig {
372 environment: BybitEnvironment::Testnet,
373 ..Default::default()
374 };
375
376 assert_eq!(
377 config.ws_private_url(),
378 "wss://stream-testnet.bybit.com/v5/private"
379 );
380 }
381
382 #[rstest]
383 fn test_exec_config_default() {
384 let config = BybitExecClientConfig::default();
385
386 assert!(!config.has_api_credentials());
387 assert_eq!(config.product_types, vec![BybitProductType::Linear]);
388 assert_eq!(config.http_timeout_secs, 60);
389 assert_eq!(config.heartbeat_interval_secs, 5);
390 }
391
392 #[rstest]
393 fn test_exec_config_with_credentials() {
394 let config = BybitExecClientConfig {
395 api_key: Some("test_key".to_string()),
396 api_secret: Some("test_secret".to_string()),
397 ..Default::default()
398 };
399
400 assert!(config.has_api_credentials());
401 }
402
403 #[rstest]
404 fn test_exec_config_urls() {
405 let config = BybitExecClientConfig {
406 environment: BybitEnvironment::Mainnet,
407 ..Default::default()
408 };
409
410 assert_eq!(config.http_base_url(), "https://api.bybit.com");
411 assert_eq!(config.ws_private_url(), "wss://stream.bybit.com/v5/private");
412 assert_eq!(config.ws_trade_url(), "wss://stream.bybit.com/v5/trade");
413 }
414
415 #[rstest]
416 fn test_exec_config_urls_testnet() {
417 let config = BybitExecClientConfig {
418 environment: BybitEnvironment::Testnet,
419 ..Default::default()
420 };
421
422 assert_eq!(config.http_base_url(), "https://api-testnet.bybit.com");
423 assert_eq!(
424 config.ws_private_url(),
425 "wss://stream-testnet.bybit.com/v5/private"
426 );
427 assert_eq!(
428 config.ws_trade_url(),
429 "wss://stream-testnet.bybit.com/v5/trade"
430 );
431 }
432
433 #[rstest]
434 fn test_exec_config_custom_urls() {
435 let config = BybitExecClientConfig {
436 base_url_http: Some("https://custom-http.bybit.com".to_string()),
437 base_url_ws_private: Some("wss://custom-private.bybit.com".to_string()),
438 base_url_ws_trade: Some("wss://custom-trade.bybit.com".to_string()),
439 ..Default::default()
440 };
441
442 assert_eq!(config.http_base_url(), "https://custom-http.bybit.com");
443 assert_eq!(config.ws_private_url(), "wss://custom-private.bybit.com");
444 assert_eq!(config.ws_trade_url(), "wss://custom-trade.bybit.com");
445 }
446
447 #[rstest]
448 fn test_data_config_toml_minimal() {
449 let config: BybitDataClientConfig = toml::from_str(
450 r#"
451environment = "testnet"
452product_types = ["spot", "linear"]
453http_timeout_secs = 45
454"#,
455 )
456 .unwrap();
457
458 assert_eq!(config.environment, BybitEnvironment::Testnet);
459 assert_eq!(
460 config.product_types,
461 vec![BybitProductType::Spot, BybitProductType::Linear]
462 );
463 assert_eq!(config.http_timeout_secs, 45);
464 }
465
466 #[rstest]
467 fn test_exec_config_toml_empty_uses_defaults() {
468 let config: BybitExecClientConfig = toml::from_str("").unwrap();
469 let expected = BybitExecClientConfig::default();
470
471 assert_eq!(config.environment, expected.environment);
472 assert_eq!(config.product_types, expected.product_types);
473 assert_eq!(config.http_timeout_secs, expected.http_timeout_secs);
474 assert_eq!(
475 config.heartbeat_interval_secs,
476 expected.heartbeat_interval_secs,
477 );
478 assert_eq!(config.recv_window_ms, expected.recv_window_ms);
479 assert_eq!(config.transport_backend, expected.transport_backend);
480 }
481}