1use nautilus_core::{correctness::check_in_range_inclusive_usize, string::secret::SecretString};
19use nautilus_model::identifiers::AccountId;
20use nautilus_network::websocket::TransportBackend;
21use serde::{Deserialize, Serialize};
22
23use crate::common::{
24 consts::{BITMEX_HTTP_TESTNET_URL, BITMEX_HTTP_URL, BITMEX_WS_TESTNET_URL, BITMEX_WS_URL},
25 credential::credential_env_vars,
26 enums::BitmexEnvironment,
27};
28
29pub(crate) const MAX_BROADCASTER_POOL_SIZE: usize = 16;
30
31pub(crate) fn validate_broadcaster_pool_size(
37 pool_size: usize,
38 parameter: &str,
39) -> anyhow::Result<()> {
40 check_in_range_inclusive_usize(pool_size, 1, MAX_BROADCASTER_POOL_SIZE, parameter)?;
41 Ok(())
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
46#[serde(default, deny_unknown_fields)]
47#[cfg_attr(
48 feature = "python",
49 pyo3::pyclass(module = "nautilus_trader.adapters.bitmex", from_py_object)
50)]
51#[cfg_attr(
52 feature = "python",
53 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.bitmex")
54)]
55pub struct BitmexDataClientConfig {
56 pub api_key: Option<SecretString>,
58 pub api_secret: Option<SecretString>,
60 pub base_url_http: Option<String>,
62 pub base_url_ws: 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 pub heartbeat_interval_secs: Option<u64>,
80 pub auth_timeout_secs: Option<u64>,
83 #[builder(default = 10_000)]
99 pub recv_window_ms: u64,
100 #[builder(default = true)]
102 pub active_only: bool,
103 pub update_instruments_interval_mins: Option<u64>,
105 #[builder(default)]
107 pub environment: BitmexEnvironment,
108 #[builder(default = 10)]
110 pub max_requests_per_second: u32,
111 #[builder(default = 120)]
113 pub max_requests_per_minute: u32,
114 #[builder(default)]
116 pub transport_backend: TransportBackend,
117}
118
119#[cfg(feature = "python")]
120nautilus_core::impl_pyo3_config_getters!(BitmexDataClientConfig {
121 base_url_http: Option<String>,
122 base_url_ws: Option<String>,
123 http_timeout_secs: u64,
124 max_retries: u32,
125 retry_delay_initial_ms: u64,
126 retry_delay_max_ms: u64,
127 heartbeat_interval_secs: Option<u64>,
128 auth_timeout_secs: Option<u64>,
129 recv_window_ms: u64,
130 active_only: bool,
131 update_instruments_interval_mins: Option<u64>,
132 environment: BitmexEnvironment,
133 max_requests_per_second: u32,
134 max_requests_per_minute: u32,
135 transport_backend: TransportBackend,
136});
137
138impl Default for BitmexDataClientConfig {
139 fn default() -> Self {
140 Self::builder().build()
141 }
142}
143
144impl BitmexDataClientConfig {
145 #[must_use]
147 pub fn new() -> Self {
148 Self::default()
149 }
150
151 #[must_use]
154 pub fn has_api_credentials(&self) -> bool {
155 let (key_var, secret_var) = credential_env_vars(self.environment);
156 let has_key = self.api_key.is_some() || std::env::var(key_var).is_ok();
157 let has_secret = self.api_secret.is_some() || std::env::var(secret_var).is_ok();
158 has_key && has_secret
159 }
160
161 #[must_use]
163 pub fn http_base_url(&self) -> String {
164 self.base_url_http
165 .clone()
166 .unwrap_or_else(|| match self.environment {
167 BitmexEnvironment::Testnet => BITMEX_HTTP_TESTNET_URL.to_string(),
168 BitmexEnvironment::Mainnet => BITMEX_HTTP_URL.to_string(),
169 })
170 }
171
172 #[must_use]
174 pub fn ws_url(&self) -> String {
175 self.base_url_ws
176 .clone()
177 .unwrap_or_else(|| match self.environment {
178 BitmexEnvironment::Testnet => BITMEX_WS_TESTNET_URL.to_string(),
179 BitmexEnvironment::Mainnet => BITMEX_WS_URL.to_string(),
180 })
181 }
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
189#[serde(default, deny_unknown_fields)]
190#[cfg_attr(
191 feature = "python",
192 pyo3::pyclass(module = "nautilus_trader.adapters.bitmex", from_py_object)
193)]
194#[cfg_attr(
195 feature = "python",
196 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.bitmex")
197)]
198pub struct BitmexExecutionClientConfig {
199 pub api_key: Option<SecretString>,
201 pub api_secret: Option<SecretString>,
203 pub base_url_http: Option<String>,
205 pub base_url_ws: Option<String>,
207 pub proxy_url: Option<SecretString>,
209 #[builder(default = 60)]
211 pub http_timeout_secs: u64,
212 #[builder(default = 3)]
214 pub max_retries: u32,
215 #[builder(default = 1_000)]
217 pub retry_delay_initial_ms: u64,
218 #[builder(default = 10_000)]
220 pub retry_delay_max_ms: u64,
221 #[builder(default = 5)]
223 pub heartbeat_interval_secs: u64,
224 pub auth_timeout_secs: Option<u64>,
227 #[builder(default = 10_000)]
243 pub recv_window_ms: u64,
244 #[builder(default = true)]
246 pub active_only: bool,
247 #[builder(default)]
249 pub environment: BitmexEnvironment,
250 pub account_id: Option<AccountId>,
252 #[builder(default = 10)]
254 pub max_requests_per_second: u32,
255 #[builder(default = 120)]
257 pub max_requests_per_minute: u32,
258 pub submitter_pool_size: Option<usize>,
261 pub canceller_pool_size: Option<usize>,
264 pub submitter_proxy_urls: Option<Vec<SecretString>>,
266 pub canceller_proxy_urls: Option<Vec<SecretString>>,
268 pub deadmans_switch_timeout_secs: Option<u64>,
275 #[builder(default)]
277 pub transport_backend: TransportBackend,
278}
279
280#[cfg(feature = "python")]
281nautilus_core::impl_pyo3_config_getters!(BitmexExecutionClientConfig {
282 base_url_http: Option<String>,
283 base_url_ws: Option<String>,
284 http_timeout_secs: u64,
285 max_retries: u32,
286 retry_delay_initial_ms: u64,
287 retry_delay_max_ms: u64,
288 heartbeat_interval_secs: u64,
289 auth_timeout_secs: Option<u64>,
290 recv_window_ms: u64,
291 active_only: bool,
292 environment: BitmexEnvironment,
293 account_id: Option<AccountId>,
294 max_requests_per_second: u32,
295 max_requests_per_minute: u32,
296 submitter_pool_size: Option<usize>,
297 canceller_pool_size: Option<usize>,
298 deadmans_switch_timeout_secs: Option<u64>,
299 transport_backend: TransportBackend,
300});
301
302impl Default for BitmexExecutionClientConfig {
303 fn default() -> Self {
304 Self::builder().build()
305 }
306}
307
308impl BitmexExecutionClientConfig {
309 #[must_use]
311 pub fn new() -> Self {
312 Self::default()
313 }
314
315 pub(crate) fn validate_broadcaster_pool_sizes(&self) -> anyhow::Result<()> {
322 let submitter_pool_size = self.submitter_pool_size.unwrap_or(1);
323 let canceller_pool_size = self.canceller_pool_size.unwrap_or(1);
324 validate_broadcaster_pool_size(submitter_pool_size, "submitter_pool_size")?;
325 validate_broadcaster_pool_size(canceller_pool_size, "canceller_pool_size")?;
326 let combined_pool_size = submitter_pool_size
327 .checked_add(canceller_pool_size)
328 .ok_or_else(|| anyhow::anyhow!("combined BitMEX broadcaster pool size overflow"))?;
329 check_in_range_inclusive_usize(
330 combined_pool_size,
331 2,
332 MAX_BROADCASTER_POOL_SIZE,
333 "combined_pool_size",
334 )?;
335 Ok(())
336 }
337
338 #[must_use]
341 pub fn has_api_credentials(&self) -> bool {
342 let (key_var, secret_var) = credential_env_vars(self.environment);
343 let has_key = self.api_key.is_some() || std::env::var(key_var).is_ok();
344 let has_secret = self.api_secret.is_some() || std::env::var(secret_var).is_ok();
345 has_key && has_secret
346 }
347
348 #[must_use]
350 pub fn http_base_url(&self) -> String {
351 self.base_url_http
352 .clone()
353 .unwrap_or_else(|| match self.environment {
354 BitmexEnvironment::Testnet => BITMEX_HTTP_TESTNET_URL.to_string(),
355 BitmexEnvironment::Mainnet => BITMEX_HTTP_URL.to_string(),
356 })
357 }
358
359 #[must_use]
361 pub fn ws_url(&self) -> String {
362 self.base_url_ws
363 .clone()
364 .unwrap_or_else(|| match self.environment {
365 BitmexEnvironment::Testnet => BITMEX_WS_TESTNET_URL.to_string(),
366 BitmexEnvironment::Mainnet => BITMEX_WS_URL.to_string(),
367 })
368 }
369}
370
371#[cfg(test)]
372mod tests {
373 use rstest::rstest;
374
375 use super::*;
376
377 #[rstest]
378 #[case(1)]
379 #[case(3)]
380 #[case(MAX_BROADCASTER_POOL_SIZE)]
381 fn test_validate_broadcaster_pool_size_accepts_supported_values(#[case] pool_size: usize) {
382 assert!(validate_broadcaster_pool_size(pool_size, "pool_size").is_ok());
383 }
384
385 #[rstest]
386 #[case(0)]
387 #[case(MAX_BROADCASTER_POOL_SIZE + 1)]
388 #[case(usize::MAX)]
389 fn test_validate_broadcaster_pool_size_rejects_invalid_values(#[case] pool_size: usize) {
390 assert!(validate_broadcaster_pool_size(pool_size, "pool_size").is_err());
391 }
392
393 #[rstest]
394 #[case(Some(1), Some(1))]
395 #[case(Some(MAX_BROADCASTER_POOL_SIZE - 1), Some(1))]
396 #[case(Some(1), Some(MAX_BROADCASTER_POOL_SIZE - 1))]
397 fn test_execution_config_accepts_supported_combined_pool_size(
398 #[case] submitter_pool_size: Option<usize>,
399 #[case] canceller_pool_size: Option<usize>,
400 ) {
401 let config = BitmexExecutionClientConfig {
402 submitter_pool_size,
403 canceller_pool_size,
404 ..Default::default()
405 };
406
407 assert!(config.validate_broadcaster_pool_sizes().is_ok());
408 }
409
410 #[rstest]
411 #[case(Some(0), Some(1))]
412 #[case(Some(1), Some(0))]
413 #[case(Some(MAX_BROADCASTER_POOL_SIZE), Some(1))]
414 #[case(Some(usize::MAX), Some(1))]
415 fn test_execution_config_rejects_invalid_pool_sizes(
416 #[case] submitter_pool_size: Option<usize>,
417 #[case] canceller_pool_size: Option<usize>,
418 ) {
419 let config = BitmexExecutionClientConfig {
420 submitter_pool_size,
421 canceller_pool_size,
422 ..Default::default()
423 };
424
425 assert!(config.validate_broadcaster_pool_sizes().is_err());
426 }
427
428 #[rstest]
429 fn test_data_config_toml_minimal() {
430 let config: BitmexDataClientConfig = toml::from_str(
431 r#"
432environment = "testnet"
433http_timeout_secs = 30
434active_only = false
435max_requests_per_second = 5
436"#,
437 )
438 .unwrap();
439
440 assert_eq!(config.environment, BitmexEnvironment::Testnet);
441 assert_eq!(config.http_timeout_secs, 30);
442 assert!(!config.active_only);
443 assert_eq!(config.max_requests_per_second, 5);
444 }
445
446 #[rstest]
447 fn test_exec_config_toml_empty_uses_defaults() {
448 let config: BitmexExecutionClientConfig = toml::from_str("").unwrap();
449 let expected = BitmexExecutionClientConfig::default();
450
451 assert_eq!(config.environment, expected.environment);
452 assert_eq!(config.http_timeout_secs, expected.http_timeout_secs);
453 assert_eq!(
454 config.heartbeat_interval_secs,
455 expected.heartbeat_interval_secs,
456 );
457 assert_eq!(config.recv_window_ms, expected.recv_window_ms);
458 assert_eq!(config.active_only, expected.active_only);
459 assert_eq!(
460 config.max_requests_per_second,
461 expected.max_requests_per_second,
462 );
463 assert_eq!(config.transport_backend, expected.transport_backend);
464 }
465
466 #[rstest]
467 fn test_config_auth_timeout_secs() {
468 assert_eq!(BitmexDataClientConfig::default().auth_timeout_secs, None);
469 assert_eq!(
470 BitmexExecutionClientConfig::default().auth_timeout_secs,
471 None
472 );
473
474 let data = BitmexDataClientConfig::builder()
475 .auth_timeout_secs(3)
476 .build();
477 assert_eq!(data.auth_timeout_secs, Some(3));
478
479 let exec = BitmexExecutionClientConfig::builder()
480 .auth_timeout_secs(4)
481 .build();
482 assert_eq!(exec.auth_timeout_secs, Some(4));
483
484 let data: BitmexDataClientConfig = toml::from_str("auth_timeout_secs = 7\n").unwrap();
485 assert_eq!(data.auth_timeout_secs, Some(7));
486
487 let exec: BitmexExecutionClientConfig = toml::from_str("auth_timeout_secs = 8\n").unwrap();
488 assert_eq!(exec.auth_timeout_secs, Some(8));
489 }
490
491 #[rstest]
492 fn test_config_debug_redacts_credentials() {
493 let data = BitmexDataClientConfig {
494 api_key: Some("data-api-key".into()),
495 api_secret: Some("data-api-secret".into()),
496 proxy_url: Some("http://data-user:data-password@localhost".into()),
497 ..Default::default()
498 };
499 let execution = BitmexExecutionClientConfig {
500 api_key: Some("execution-api-key".into()),
501 api_secret: Some("execution-api-secret".into()),
502 proxy_url: Some("http://execution-user:execution-password@localhost".into()),
503 submitter_proxy_urls: Some(vec!["http://submit-user:submit-password@localhost".into()]),
504 canceller_proxy_urls: Some(vec!["http://cancel-user:cancel-password@localhost".into()]),
505 ..Default::default()
506 };
507
508 let debug = format!("{data:?} {execution:?}");
509
510 assert!(!debug.contains("data-api-key"));
511 assert!(!debug.contains("data-api-secret"));
512 assert!(!debug.contains("data-password"));
513 assert!(!debug.contains("execution-api-key"));
514 assert!(!debug.contains("execution-api-secret"));
515 assert!(!debug.contains("execution-password"));
516 assert!(!debug.contains("submit-password"));
517 assert!(!debug.contains("cancel-password"));
518 }
519}