1use std::collections::HashMap;
19
20#[cfg(test)]
21use nautilus_core::string::secret::REDACTED;
22use nautilus_core::string::secret::SecretString;
23use nautilus_live::book::DEFAULT_BOOK_SNAPSHOT_TIMEOUT_SECS;
24use nautilus_model::identifiers::AccountId;
25use nautilus_network::websocket::TransportBackend;
26use serde::{Deserialize, Serialize};
27
28use crate::common::{
29 enums::{
30 BybitEnvironment, BybitMarginMode, BybitOrderSmpType, BybitPositionMode, BybitProductType,
31 },
32 parse::deserialize_optional_smp_type,
33 urls::{bybit_http_base_url, bybit_ws_private_url, bybit_ws_public_url, bybit_ws_trade_url},
34};
35
36#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
38#[serde(default, deny_unknown_fields)]
39#[cfg_attr(
40 feature = "python",
41 pyo3::pyclass(module = "nautilus_trader.adapters.bybit", from_py_object)
42)]
43#[cfg_attr(
44 feature = "python",
45 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.bybit")
46)]
47pub struct BybitDataClientConfig {
48 pub api_key: Option<SecretString>,
50 pub api_secret: Option<SecretString>,
52 #[builder(default = vec![BybitProductType::Linear])]
54 pub product_types: Vec<BybitProductType>,
55 #[builder(default = BybitEnvironment::Mainnet)]
57 pub environment: BybitEnvironment,
58 pub base_url_http: Option<String>,
60 pub base_url_ws_public: Option<String>,
62 pub base_url_ws_private: Option<String>,
64 pub proxy_url: Option<SecretString>,
66 #[builder(default = 60)]
68 pub http_timeout_secs: u64,
69 #[builder(default = 3)]
71 pub max_retries: u32,
72 #[builder(default = 1_000)]
74 pub retry_delay_initial_ms: u64,
75 #[builder(default = 10_000)]
77 pub retry_delay_max_ms: u64,
78 #[builder(default = 20)]
80 pub heartbeat_interval_secs: u64,
81 #[builder(default = 5_000)]
83 pub recv_window_ms: u64,
84 pub update_instruments_interval_mins: Option<u64>,
87 pub instrument_poll_interval_secs: Option<u64>,
90 #[builder(default = DEFAULT_BOOK_SNAPSHOT_TIMEOUT_SECS)]
93 pub book_snapshot_timeout_secs: u64,
94 #[builder(default)]
96 pub transport_backend: TransportBackend,
97 #[builder(default = true)]
99 pub bars_timestamp_on_close: bool,
100}
101
102#[cfg(feature = "python")]
103nautilus_core::impl_pyo3_config_getters!(BybitDataClientConfig {
104 product_types: Vec<BybitProductType>,
105 environment: BybitEnvironment,
106 base_url_http: Option<String>,
107 base_url_ws_public: Option<String>,
108 base_url_ws_private: Option<String>,
109 http_timeout_secs: u64,
110 max_retries: u32,
111 retry_delay_initial_ms: u64,
112 retry_delay_max_ms: u64,
113 heartbeat_interval_secs: u64,
114 recv_window_ms: u64,
115 update_instruments_interval_mins: Option<u64>,
116 book_snapshot_timeout_secs: u64,
117 transport_backend: TransportBackend,
118 bars_timestamp_on_close: bool,
119});
120
121impl Default for BybitDataClientConfig {
122 fn default() -> Self {
123 Self {
124 update_instruments_interval_mins: Some(60),
125 instrument_poll_interval_secs: Some(60),
126 ..Self::builder().build()
127 }
128 }
129}
130
131impl BybitDataClientConfig {
132 #[must_use]
134 pub fn new() -> Self {
135 Self::default()
136 }
137
138 #[must_use]
140 pub fn has_api_credentials(&self) -> bool {
141 self.api_key.is_some() && self.api_secret.is_some()
142 }
143
144 #[must_use]
146 pub fn http_base_url(&self) -> String {
147 self.base_url_http
148 .clone()
149 .unwrap_or_else(|| bybit_http_base_url(self.environment).to_string())
150 }
151
152 #[must_use]
156 pub fn ws_public_url(&self) -> String {
157 self.base_url_ws_public.clone().unwrap_or_else(|| {
158 let product_type = self
159 .product_types
160 .first()
161 .copied()
162 .unwrap_or(BybitProductType::Linear);
163 bybit_ws_public_url(product_type, self.environment)
164 })
165 }
166
167 #[must_use]
169 pub fn ws_public_url_for(&self, product_type: BybitProductType) -> String {
170 self.base_url_ws_public
171 .clone()
172 .unwrap_or_else(|| bybit_ws_public_url(product_type, self.environment))
173 }
174
175 #[must_use]
177 pub fn ws_private_url(&self) -> String {
178 self.base_url_ws_private
179 .clone()
180 .unwrap_or_else(|| bybit_ws_private_url(self.environment).to_string())
181 }
182
183 #[must_use]
185 pub fn requires_private_ws(&self) -> bool {
186 self.has_api_credentials()
187 }
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
192#[serde(default, deny_unknown_fields)]
193#[cfg_attr(
194 feature = "python",
195 pyo3::pyclass(module = "nautilus_trader.adapters.bybit", from_py_object)
196)]
197#[cfg_attr(
198 feature = "python",
199 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.bybit")
200)]
201pub struct BybitExecutionClientConfig {
202 pub api_key: Option<SecretString>,
204 pub api_secret: Option<SecretString>,
206 #[builder(default = vec![BybitProductType::Linear])]
208 pub product_types: Vec<BybitProductType>,
209 #[builder(default = BybitEnvironment::Mainnet)]
211 pub environment: BybitEnvironment,
212 pub base_url_http: Option<String>,
214 pub base_url_ws_private: Option<String>,
216 pub base_url_ws_trade: Option<String>,
218 pub proxy_url: Option<SecretString>,
220 #[builder(default = 60)]
222 pub http_timeout_secs: u64,
223 #[builder(default = 3)]
225 pub max_retries: u32,
226 #[builder(default = 1_000)]
228 pub retry_delay_initial_ms: u64,
229 #[builder(default = 10_000)]
231 pub retry_delay_max_ms: u64,
232 #[builder(default = 20)]
234 pub heartbeat_interval_secs: u64,
235 pub auth_timeout_secs: Option<u64>,
238 #[builder(default = 5_000)]
240 pub recv_window_ms: u64,
241 pub account_id: Option<AccountId>,
243 #[builder(default)]
248 pub use_spot_position_reports: bool,
249 #[builder(default)]
253 pub auto_repay_spot_borrows: bool,
254 pub futures_leverages: Option<HashMap<String, u32>>,
256 pub position_mode: Option<HashMap<String, BybitPositionMode>>,
258 pub margin_mode: Option<BybitMarginMode>,
260 #[serde(deserialize_with = "deserialize_optional_smp_type")]
263 pub smp_type: Option<BybitOrderSmpType>,
264 #[builder(default)]
266 pub transport_backend: TransportBackend,
267}
268
269#[cfg(feature = "python")]
270nautilus_core::impl_pyo3_config_getters!(BybitExecutionClientConfig {
271 product_types: Vec<BybitProductType>,
272 environment: BybitEnvironment,
273 base_url_http: Option<String>,
274 base_url_ws_private: Option<String>,
275 base_url_ws_trade: Option<String>,
276 http_timeout_secs: u64,
277 max_retries: u32,
278 retry_delay_initial_ms: u64,
279 retry_delay_max_ms: u64,
280 heartbeat_interval_secs: u64,
281 auth_timeout_secs: Option<u64>,
282 recv_window_ms: u64,
283 account_id: Option<AccountId>,
284 use_spot_position_reports: bool,
285 auto_repay_spot_borrows: bool,
286 margin_mode: Option<BybitMarginMode>,
287 transport_backend: TransportBackend,
288});
289
290impl Default for BybitExecutionClientConfig {
291 fn default() -> Self {
292 Self::builder().build()
293 }
294}
295
296impl BybitExecutionClientConfig {
297 #[must_use]
299 pub fn new() -> Self {
300 Self::default()
301 }
302
303 #[must_use]
305 pub fn has_api_credentials(&self) -> bool {
306 self.api_key.is_some() && self.api_secret.is_some()
307 }
308
309 #[must_use]
311 pub fn http_base_url(&self) -> String {
312 self.base_url_http
313 .clone()
314 .unwrap_or_else(|| bybit_http_base_url(self.environment).to_string())
315 }
316
317 #[must_use]
319 pub fn ws_private_url(&self) -> String {
320 self.base_url_ws_private
321 .clone()
322 .unwrap_or_else(|| bybit_ws_private_url(self.environment).to_string())
323 }
324
325 #[must_use]
327 pub fn ws_trade_url(&self) -> String {
328 self.base_url_ws_trade
329 .clone()
330 .unwrap_or_else(|| bybit_ws_trade_url(self.environment).to_string())
331 }
332}
333#[cfg(test)]
334mod tests {
335 use rstest::rstest;
336
337 use super::*;
338
339 #[rstest]
340 fn test_config_debug_redacts_credentials() {
341 let data = BybitDataClientConfig {
342 api_key: Some("data-api-key".into()),
343 api_secret: Some("data-api-secret".into()),
344 proxy_url: Some("http://user:data-proxy@localhost".into()),
345 ..Default::default()
346 };
347 let execution = BybitExecutionClientConfig {
348 api_key: Some("exec-api-key".into()),
349 api_secret: Some("exec-api-secret".into()),
350 proxy_url: Some("http://user:exec-proxy@localhost".into()),
351 ..Default::default()
352 };
353
354 let formatted = format!("{data:?} {execution:?}");
355
356 assert_eq!(formatted.matches(REDACTED).count(), 6);
357
358 for secret in [
359 "data-api-key",
360 "data-api-secret",
361 "data-proxy",
362 "exec-api-key",
363 "exec-api-secret",
364 "exec-proxy",
365 ] {
366 assert!(!formatted.contains(secret));
367 }
368 }
369
370 #[rstest]
371 #[case("None", BybitOrderSmpType::None)]
372 #[case("CancelMaker", BybitOrderSmpType::CancelMaker)]
373 #[case("CancelTaker", BybitOrderSmpType::CancelTaker)]
374 #[case("CancelBoth", BybitOrderSmpType::CancelBoth)]
375 fn test_exec_config_deserializes_smp_type(
376 #[case] value: &str,
377 #[case] expected: BybitOrderSmpType,
378 ) {
379 let json = format!(r#"{{"smp_type": "{value}"}}"#);
380 let config: BybitExecutionClientConfig = serde_json::from_str(&json).unwrap();
381
382 assert_eq!(config.smp_type, Some(expected));
383 }
384
385 #[rstest]
386 #[case(r#"{"smp_type": "Other"}"#)]
387 #[case(r#"{"smp_type": "cancel-maker"}"#)]
388 #[case(r#"{"smp_type": ""}"#)]
389 fn test_exec_config_rejects_invalid_smp_type(#[case] json: &str) {
390 let err = serde_json::from_str::<BybitExecutionClientConfig>(json).unwrap_err();
391
392 assert!(
393 err.to_string().contains("invalid Bybit smp_type"),
394 "expected an smp_type rejection, was '{err}'"
395 );
396 }
397
398 #[rstest]
399 fn test_exec_config_smp_type_defaults_to_none() {
400 let config: BybitExecutionClientConfig = serde_json::from_str("{}").unwrap();
401
402 assert_eq!(config.smp_type, None);
403 }
404
405 #[rstest]
406 fn test_data_config_default() {
407 let config = BybitDataClientConfig::default();
408
409 assert!(config.bars_timestamp_on_close);
410 assert!(!config.has_api_credentials());
411 assert_eq!(config.product_types, vec![BybitProductType::Linear]);
412 assert_eq!(config.http_timeout_secs, 60);
413 assert_eq!(config.heartbeat_interval_secs, 20);
414 assert_eq!(config.book_snapshot_timeout_secs, 10);
415 }
416
417 #[rstest]
418 fn test_data_config_bar_timestamp_builder() {
419 assert!(
420 BybitDataClientConfig::builder()
421 .build()
422 .bars_timestamp_on_close
423 );
424 assert!(
425 serde_json::from_str::<BybitDataClientConfig>("{}")
426 .unwrap()
427 .bars_timestamp_on_close
428 );
429 let config = BybitDataClientConfig::builder()
430 .bars_timestamp_on_close(false)
431 .build();
432 assert!(!config.bars_timestamp_on_close);
433 let config: BybitDataClientConfig =
434 serde_json::from_str(r#"{"bars_timestamp_on_close":false}"#).unwrap();
435 assert!(!config.bars_timestamp_on_close);
436 }
437
438 #[rstest]
439 fn test_data_config_with_credentials() {
440 let config = BybitDataClientConfig {
441 api_key: Some("test_key".into()),
442 api_secret: Some("test_secret".into()),
443 ..Default::default()
444 };
445
446 assert!(config.has_api_credentials());
447 assert!(config.requires_private_ws());
448 }
449
450 #[rstest]
451 fn test_data_config_http_url_mainnet() {
452 let config = BybitDataClientConfig {
453 environment: BybitEnvironment::Mainnet,
454 ..Default::default()
455 };
456
457 assert_eq!(config.http_base_url(), "https://api.bybit.com");
458 }
459
460 #[rstest]
461 fn test_data_config_http_url_testnet() {
462 let config = BybitDataClientConfig {
463 environment: BybitEnvironment::Testnet,
464 ..Default::default()
465 };
466
467 assert_eq!(config.http_base_url(), "https://api-testnet.bybit.com");
468 }
469
470 #[rstest]
471 fn test_data_config_http_url_demo() {
472 let config = BybitDataClientConfig {
473 environment: BybitEnvironment::Demo,
474 ..Default::default()
475 };
476
477 assert_eq!(config.http_base_url(), "https://api-demo.bybit.com");
478 }
479
480 #[rstest]
481 fn test_data_config_http_url_override() {
482 let custom_url = "https://custom.bybit.com";
483 let config = BybitDataClientConfig {
484 base_url_http: Some(custom_url.to_string()),
485 ..Default::default()
486 };
487
488 assert_eq!(config.http_base_url(), custom_url);
489 }
490
491 #[rstest]
492 fn test_data_config_ws_public_url() {
493 let config = BybitDataClientConfig {
494 environment: BybitEnvironment::Mainnet,
495 ..Default::default()
496 };
497
498 assert_eq!(
499 config.ws_public_url(),
500 "wss://stream.bybit.com/v5/public/linear"
501 );
502 }
503
504 #[rstest]
505 fn test_data_config_ws_public_url_for_spot() {
506 let config = BybitDataClientConfig {
507 environment: BybitEnvironment::Mainnet,
508 ..Default::default()
509 };
510
511 assert_eq!(
512 config.ws_public_url_for(BybitProductType::Spot),
513 "wss://stream.bybit.com/v5/public/spot"
514 );
515 }
516
517 #[rstest]
518 fn test_data_config_ws_private_url() {
519 let config = BybitDataClientConfig {
520 environment: BybitEnvironment::Mainnet,
521 ..Default::default()
522 };
523
524 assert_eq!(config.ws_private_url(), "wss://stream.bybit.com/v5/private");
525 }
526
527 #[rstest]
528 fn test_data_config_ws_private_url_testnet() {
529 let config = BybitDataClientConfig {
530 environment: BybitEnvironment::Testnet,
531 ..Default::default()
532 };
533
534 assert_eq!(
535 config.ws_private_url(),
536 "wss://stream-testnet.bybit.com/v5/private"
537 );
538 }
539
540 #[rstest]
541 fn test_exec_config_default() {
542 let config = BybitExecutionClientConfig::default();
543
544 assert!(!config.has_api_credentials());
545 assert_eq!(config.product_types, vec![BybitProductType::Linear]);
546 assert_eq!(config.http_timeout_secs, 60);
547 assert_eq!(config.heartbeat_interval_secs, 20);
548 }
549
550 #[rstest]
551 fn test_exec_config_with_credentials() {
552 let config = BybitExecutionClientConfig {
553 api_key: Some("test_key".into()),
554 api_secret: Some("test_secret".into()),
555 ..Default::default()
556 };
557
558 assert!(config.has_api_credentials());
559 }
560
561 #[rstest]
562 fn test_exec_config_urls() {
563 let config = BybitExecutionClientConfig {
564 environment: BybitEnvironment::Mainnet,
565 ..Default::default()
566 };
567
568 assert_eq!(config.http_base_url(), "https://api.bybit.com");
569 assert_eq!(config.ws_private_url(), "wss://stream.bybit.com/v5/private");
570 assert_eq!(config.ws_trade_url(), "wss://stream.bybit.com/v5/trade");
571 }
572
573 #[rstest]
574 fn test_exec_config_urls_testnet() {
575 let config = BybitExecutionClientConfig {
576 environment: BybitEnvironment::Testnet,
577 ..Default::default()
578 };
579
580 assert_eq!(config.http_base_url(), "https://api-testnet.bybit.com");
581 assert_eq!(
582 config.ws_private_url(),
583 "wss://stream-testnet.bybit.com/v5/private"
584 );
585 assert_eq!(
586 config.ws_trade_url(),
587 "wss://stream-testnet.bybit.com/v5/trade"
588 );
589 }
590
591 #[rstest]
592 fn test_exec_config_custom_urls() {
593 let config = BybitExecutionClientConfig {
594 base_url_http: Some("https://custom-http.bybit.com".to_string()),
595 base_url_ws_private: Some("wss://custom-private.bybit.com".to_string()),
596 base_url_ws_trade: Some("wss://custom-trade.bybit.com".to_string()),
597 ..Default::default()
598 };
599
600 assert_eq!(config.http_base_url(), "https://custom-http.bybit.com");
601 assert_eq!(config.ws_private_url(), "wss://custom-private.bybit.com");
602 assert_eq!(config.ws_trade_url(), "wss://custom-trade.bybit.com");
603 }
604
605 #[rstest]
606 fn test_data_config_toml_minimal() {
607 let config: BybitDataClientConfig = toml::from_str(
608 r#"
609environment = "testnet"
610product_types = ["spot", "linear"]
611http_timeout_secs = 45
612"#,
613 )
614 .unwrap();
615
616 assert_eq!(config.environment, BybitEnvironment::Testnet);
617 assert_eq!(
618 config.product_types,
619 vec![BybitProductType::Spot, BybitProductType::Linear]
620 );
621 assert_eq!(config.http_timeout_secs, 45);
622 }
623
624 #[rstest]
625 fn test_exec_config_toml_empty_uses_defaults() {
626 let config: BybitExecutionClientConfig = toml::from_str("").unwrap();
627 let expected = BybitExecutionClientConfig::default();
628
629 assert_eq!(config.environment, expected.environment);
630 assert_eq!(config.product_types, expected.product_types);
631 assert_eq!(config.http_timeout_secs, expected.http_timeout_secs);
632 assert_eq!(
633 config.heartbeat_interval_secs,
634 expected.heartbeat_interval_secs,
635 );
636 assert_eq!(config.recv_window_ms, expected.recv_window_ms);
637 assert_eq!(config.transport_backend, expected.transport_backend);
638 }
639}