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.adapters.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
88#[cfg(feature = "python")]
89nautilus_core::impl_pyo3_config_getters!(BybitDataClientConfig {
90 product_types: Vec<BybitProductType>,
91 environment: BybitEnvironment,
92 base_url_http: Option<String>,
93 base_url_ws_public: Option<String>,
94 base_url_ws_private: Option<String>,
95 http_timeout_secs: u64,
96 max_retries: u32,
97 retry_delay_initial_ms: u64,
98 retry_delay_max_ms: u64,
99 heartbeat_interval_secs: u64,
100 recv_window_ms: u64,
101 update_instruments_interval_mins: Option<u64>,
102 transport_backend: TransportBackend,
103});
104
105impl Default for BybitDataClientConfig {
106 fn default() -> Self {
107 Self {
108 update_instruments_interval_mins: Some(60),
109 instrument_poll_interval_secs: Some(60),
110 ..Self::builder().build()
111 }
112 }
113}
114
115impl BybitDataClientConfig {
116 #[must_use]
118 pub fn new() -> Self {
119 Self::default()
120 }
121
122 #[must_use]
124 pub fn has_api_credentials(&self) -> bool {
125 self.api_key.is_some() && self.api_secret.is_some()
126 }
127
128 #[must_use]
130 pub fn http_base_url(&self) -> String {
131 self.base_url_http
132 .clone()
133 .unwrap_or_else(|| bybit_http_base_url(self.environment).to_string())
134 }
135
136 #[must_use]
140 pub fn ws_public_url(&self) -> String {
141 self.base_url_ws_public.clone().unwrap_or_else(|| {
142 let product_type = self
143 .product_types
144 .first()
145 .copied()
146 .unwrap_or(BybitProductType::Linear);
147 bybit_ws_public_url(product_type, self.environment)
148 })
149 }
150
151 #[must_use]
153 pub fn ws_public_url_for(&self, product_type: BybitProductType) -> String {
154 self.base_url_ws_public
155 .clone()
156 .unwrap_or_else(|| bybit_ws_public_url(product_type, self.environment))
157 }
158
159 #[must_use]
161 pub fn ws_private_url(&self) -> String {
162 self.base_url_ws_private
163 .clone()
164 .unwrap_or_else(|| bybit_ws_private_url(self.environment).to_string())
165 }
166
167 #[must_use]
169 pub fn requires_private_ws(&self) -> bool {
170 self.has_api_credentials()
171 }
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
176#[serde(default, deny_unknown_fields)]
177#[cfg_attr(
178 feature = "python",
179 pyo3::pyclass(module = "nautilus_trader.adapters.bybit", from_py_object)
180)]
181#[cfg_attr(
182 feature = "python",
183 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.bybit")
184)]
185pub struct BybitExecutionClientConfig {
186 pub api_key: Option<String>,
188 pub api_secret: Option<String>,
190 #[builder(default = vec![BybitProductType::Linear])]
192 pub product_types: Vec<BybitProductType>,
193 #[builder(default = BybitEnvironment::Mainnet)]
195 pub environment: BybitEnvironment,
196 pub base_url_http: Option<String>,
198 pub base_url_ws_private: Option<String>,
200 pub base_url_ws_trade: Option<String>,
202 pub proxy_url: Option<String>,
204 #[builder(default = 60)]
206 pub http_timeout_secs: u64,
207 #[builder(default = 3)]
209 pub max_retries: u32,
210 #[builder(default = 1_000)]
212 pub retry_delay_initial_ms: u64,
213 #[builder(default = 10_000)]
215 pub retry_delay_max_ms: u64,
216 #[builder(default = 20)]
218 pub heartbeat_interval_secs: u64,
219 pub auth_timeout_secs: Option<u64>,
222 #[builder(default = 5_000)]
224 pub recv_window_ms: u64,
225 pub account_id: Option<AccountId>,
227 #[builder(default)]
232 pub use_spot_position_reports: bool,
233 #[builder(default)]
237 pub auto_repay_spot_borrows: bool,
238 pub futures_leverages: Option<HashMap<String, u32>>,
240 pub position_mode: Option<HashMap<String, BybitPositionMode>>,
242 pub margin_mode: Option<BybitMarginMode>,
244 #[builder(default)]
246 pub transport_backend: TransportBackend,
247}
248
249#[cfg(feature = "python")]
250nautilus_core::impl_pyo3_config_getters!(BybitExecutionClientConfig {
251 product_types: Vec<BybitProductType>,
252 environment: BybitEnvironment,
253 base_url_http: Option<String>,
254 base_url_ws_private: Option<String>,
255 base_url_ws_trade: Option<String>,
256 http_timeout_secs: u64,
257 max_retries: u32,
258 retry_delay_initial_ms: u64,
259 retry_delay_max_ms: u64,
260 heartbeat_interval_secs: u64,
261 auth_timeout_secs: Option<u64>,
262 recv_window_ms: u64,
263 account_id: Option<AccountId>,
264 use_spot_position_reports: bool,
265 auto_repay_spot_borrows: bool,
266 margin_mode: Option<BybitMarginMode>,
267 transport_backend: TransportBackend,
268});
269
270impl Default for BybitExecutionClientConfig {
271 fn default() -> Self {
272 Self::builder().build()
273 }
274}
275
276impl BybitExecutionClientConfig {
277 #[must_use]
279 pub fn new() -> Self {
280 Self::default()
281 }
282
283 #[must_use]
285 pub fn has_api_credentials(&self) -> bool {
286 self.api_key.is_some() && self.api_secret.is_some()
287 }
288
289 #[must_use]
291 pub fn http_base_url(&self) -> String {
292 self.base_url_http
293 .clone()
294 .unwrap_or_else(|| bybit_http_base_url(self.environment).to_string())
295 }
296
297 #[must_use]
299 pub fn ws_private_url(&self) -> String {
300 self.base_url_ws_private
301 .clone()
302 .unwrap_or_else(|| bybit_ws_private_url(self.environment).to_string())
303 }
304
305 #[must_use]
307 pub fn ws_trade_url(&self) -> String {
308 self.base_url_ws_trade
309 .clone()
310 .unwrap_or_else(|| bybit_ws_trade_url(self.environment).to_string())
311 }
312}
313#[cfg(test)]
314mod tests {
315 use rstest::rstest;
316
317 use super::*;
318
319 #[rstest]
320 fn test_data_config_default() {
321 let config = BybitDataClientConfig::default();
322
323 assert!(!config.has_api_credentials());
324 assert_eq!(config.product_types, vec![BybitProductType::Linear]);
325 assert_eq!(config.http_timeout_secs, 60);
326 assert_eq!(config.heartbeat_interval_secs, 20);
327 }
328
329 #[rstest]
330 fn test_data_config_with_credentials() {
331 let config = BybitDataClientConfig {
332 api_key: Some("test_key".to_string()),
333 api_secret: Some("test_secret".to_string()),
334 ..Default::default()
335 };
336
337 assert!(config.has_api_credentials());
338 assert!(config.requires_private_ws());
339 }
340
341 #[rstest]
342 fn test_data_config_http_url_mainnet() {
343 let config = BybitDataClientConfig {
344 environment: BybitEnvironment::Mainnet,
345 ..Default::default()
346 };
347
348 assert_eq!(config.http_base_url(), "https://api.bybit.com");
349 }
350
351 #[rstest]
352 fn test_data_config_http_url_testnet() {
353 let config = BybitDataClientConfig {
354 environment: BybitEnvironment::Testnet,
355 ..Default::default()
356 };
357
358 assert_eq!(config.http_base_url(), "https://api-testnet.bybit.com");
359 }
360
361 #[rstest]
362 fn test_data_config_http_url_demo() {
363 let config = BybitDataClientConfig {
364 environment: BybitEnvironment::Demo,
365 ..Default::default()
366 };
367
368 assert_eq!(config.http_base_url(), "https://api-demo.bybit.com");
369 }
370
371 #[rstest]
372 fn test_data_config_http_url_override() {
373 let custom_url = "https://custom.bybit.com";
374 let config = BybitDataClientConfig {
375 base_url_http: Some(custom_url.to_string()),
376 ..Default::default()
377 };
378
379 assert_eq!(config.http_base_url(), custom_url);
380 }
381
382 #[rstest]
383 fn test_data_config_ws_public_url() {
384 let config = BybitDataClientConfig {
385 environment: BybitEnvironment::Mainnet,
386 ..Default::default()
387 };
388
389 assert_eq!(
390 config.ws_public_url(),
391 "wss://stream.bybit.com/v5/public/linear"
392 );
393 }
394
395 #[rstest]
396 fn test_data_config_ws_public_url_for_spot() {
397 let config = BybitDataClientConfig {
398 environment: BybitEnvironment::Mainnet,
399 ..Default::default()
400 };
401
402 assert_eq!(
403 config.ws_public_url_for(BybitProductType::Spot),
404 "wss://stream.bybit.com/v5/public/spot"
405 );
406 }
407
408 #[rstest]
409 fn test_data_config_ws_private_url() {
410 let config = BybitDataClientConfig {
411 environment: BybitEnvironment::Mainnet,
412 ..Default::default()
413 };
414
415 assert_eq!(config.ws_private_url(), "wss://stream.bybit.com/v5/private");
416 }
417
418 #[rstest]
419 fn test_data_config_ws_private_url_testnet() {
420 let config = BybitDataClientConfig {
421 environment: BybitEnvironment::Testnet,
422 ..Default::default()
423 };
424
425 assert_eq!(
426 config.ws_private_url(),
427 "wss://stream-testnet.bybit.com/v5/private"
428 );
429 }
430
431 #[rstest]
432 fn test_exec_config_default() {
433 let config = BybitExecutionClientConfig::default();
434
435 assert!(!config.has_api_credentials());
436 assert_eq!(config.product_types, vec![BybitProductType::Linear]);
437 assert_eq!(config.http_timeout_secs, 60);
438 assert_eq!(config.heartbeat_interval_secs, 20);
439 }
440
441 #[rstest]
442 fn test_exec_config_with_credentials() {
443 let config = BybitExecutionClientConfig {
444 api_key: Some("test_key".to_string()),
445 api_secret: Some("test_secret".to_string()),
446 ..Default::default()
447 };
448
449 assert!(config.has_api_credentials());
450 }
451
452 #[rstest]
453 fn test_exec_config_urls() {
454 let config = BybitExecutionClientConfig {
455 environment: BybitEnvironment::Mainnet,
456 ..Default::default()
457 };
458
459 assert_eq!(config.http_base_url(), "https://api.bybit.com");
460 assert_eq!(config.ws_private_url(), "wss://stream.bybit.com/v5/private");
461 assert_eq!(config.ws_trade_url(), "wss://stream.bybit.com/v5/trade");
462 }
463
464 #[rstest]
465 fn test_exec_config_urls_testnet() {
466 let config = BybitExecutionClientConfig {
467 environment: BybitEnvironment::Testnet,
468 ..Default::default()
469 };
470
471 assert_eq!(config.http_base_url(), "https://api-testnet.bybit.com");
472 assert_eq!(
473 config.ws_private_url(),
474 "wss://stream-testnet.bybit.com/v5/private"
475 );
476 assert_eq!(
477 config.ws_trade_url(),
478 "wss://stream-testnet.bybit.com/v5/trade"
479 );
480 }
481
482 #[rstest]
483 fn test_exec_config_custom_urls() {
484 let config = BybitExecutionClientConfig {
485 base_url_http: Some("https://custom-http.bybit.com".to_string()),
486 base_url_ws_private: Some("wss://custom-private.bybit.com".to_string()),
487 base_url_ws_trade: Some("wss://custom-trade.bybit.com".to_string()),
488 ..Default::default()
489 };
490
491 assert_eq!(config.http_base_url(), "https://custom-http.bybit.com");
492 assert_eq!(config.ws_private_url(), "wss://custom-private.bybit.com");
493 assert_eq!(config.ws_trade_url(), "wss://custom-trade.bybit.com");
494 }
495
496 #[rstest]
497 fn test_data_config_toml_minimal() {
498 let config: BybitDataClientConfig = toml::from_str(
499 r#"
500environment = "testnet"
501product_types = ["spot", "linear"]
502http_timeout_secs = 45
503"#,
504 )
505 .unwrap();
506
507 assert_eq!(config.environment, BybitEnvironment::Testnet);
508 assert_eq!(
509 config.product_types,
510 vec![BybitProductType::Spot, BybitProductType::Linear]
511 );
512 assert_eq!(config.http_timeout_secs, 45);
513 }
514
515 #[rstest]
516 fn test_exec_config_toml_empty_uses_defaults() {
517 let config: BybitExecutionClientConfig = toml::from_str("").unwrap();
518 let expected = BybitExecutionClientConfig::default();
519
520 assert_eq!(config.environment, expected.environment);
521 assert_eq!(config.product_types, expected.product_types);
522 assert_eq!(config.http_timeout_secs, expected.http_timeout_secs);
523 assert_eq!(
524 config.heartbeat_interval_secs,
525 expected.heartbeat_interval_secs,
526 );
527 assert_eq!(config.recv_window_ms, expected.recv_window_ms);
528 assert_eq!(config.transport_backend, expected.transport_backend);
529 }
530}