Skip to main content

nautilus_bitmex/
config.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Configuration types for the BitMEX adapter clients.
17
18use nautilus_model::identifiers::AccountId;
19use nautilus_network::websocket::TransportBackend;
20use serde::{Deserialize, Serialize};
21
22use crate::common::{
23    consts::{BITMEX_HTTP_TESTNET_URL, BITMEX_HTTP_URL, BITMEX_WS_TESTNET_URL, BITMEX_WS_URL},
24    credential::credential_env_vars,
25    enums::BitmexEnvironment,
26};
27
28/// Configuration for the BitMEX live data client.
29#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
30#[serde(default, deny_unknown_fields)]
31#[cfg_attr(
32    feature = "python",
33    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.bitmex", from_py_object)
34)]
35#[cfg_attr(
36    feature = "python",
37    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.bitmex")
38)]
39pub struct BitmexDataClientConfig {
40    /// Optional API key used for authenticated REST/WebSocket requests.
41    pub api_key: Option<String>,
42    /// Optional API secret used for authenticated REST/WebSocket requests.
43    pub api_secret: Option<String>,
44    /// Optional override for the REST base URL.
45    pub base_url_http: Option<String>,
46    /// Optional override for the WebSocket URL.
47    pub base_url_ws: Option<String>,
48    /// Optional proxy URL for HTTP and WebSocket transports.
49    pub proxy_url: Option<String>,
50    /// REST timeout in seconds.
51    #[builder(default = 60)]
52    pub http_timeout_secs: u64,
53    /// Maximum retry attempts for REST requests.
54    #[builder(default = 3)]
55    pub max_retries: u32,
56    /// Initial retry backoff in milliseconds.
57    #[builder(default = 1_000)]
58    pub retry_delay_initial_ms: u64,
59    /// Maximum retry backoff in milliseconds.
60    #[builder(default = 10_000)]
61    pub retry_delay_max_ms: u64,
62    /// Optional heartbeat interval (seconds) for the WebSocket client.
63    pub heartbeat_interval_secs: Option<u64>,
64    /// Optional WebSocket authentication timeout (seconds), defaulting to
65    /// `AUTHENTICATION_TIMEOUT_SECS` when unset.
66    pub auth_timeout_secs: Option<u64>,
67    /// Receive window in milliseconds for signed requests.
68    ///
69    /// This value determines how far in the future the `api-expires` timestamp will be set
70    /// for signed REST requests. BitMEX uses seconds-granularity Unix timestamps in the
71    /// `api-expires` header, calculated as: `current_timestamp + (recv_window_ms / 1000)`.
72    ///
73    /// **Note**: This parameter is specified in milliseconds for consistency with other
74    /// adapter configurations (e.g., Bybit's `recv_window_ms`), but BitMEX only supports
75    /// seconds-granularity timestamps. The value is converted via integer division, so
76    /// 10000ms becomes 10 seconds, 15500ms becomes 15 seconds, etc.
77    ///
78    /// A larger window provides more tolerance for clock skew and network latency, but
79    /// increases the replay attack window. The default of 10 seconds should be sufficient
80    /// for most deployments. Consider increasing this value (e.g., to 30_000ms = 30s) if you
81    /// experience request expiration errors due to clock drift or high network latency.
82    #[builder(default = 10_000)]
83    pub recv_window_ms: u64,
84    /// When `true`, only active instruments are requested during bootstrap.
85    #[builder(default = true)]
86    pub active_only: bool,
87    /// Optional interval (minutes) for instrument refresh from REST.
88    pub update_instruments_interval_mins: Option<u64>,
89    /// BitMEX environment (mainnet or testnet).
90    #[builder(default)]
91    pub environment: BitmexEnvironment,
92    /// Maximum number of requests per second (burst limit).
93    #[builder(default = 10)]
94    pub max_requests_per_second: u32,
95    /// Maximum number of requests per minute (rolling window).
96    #[builder(default = 120)]
97    pub max_requests_per_minute: u32,
98    /// WebSocket transport backend (defaults to `Tungstenite`).
99    #[builder(default)]
100    pub transport_backend: TransportBackend,
101}
102
103#[cfg(feature = "python")]
104nautilus_core::impl_pyo3_config_getters!(BitmexDataClientConfig {
105    base_url_http: Option<String>,
106    base_url_ws: Option<String>,
107    http_timeout_secs: u64,
108    max_retries: u32,
109    retry_delay_initial_ms: u64,
110    retry_delay_max_ms: u64,
111    heartbeat_interval_secs: Option<u64>,
112    auth_timeout_secs: Option<u64>,
113    recv_window_ms: u64,
114    active_only: bool,
115    update_instruments_interval_mins: Option<u64>,
116    environment: BitmexEnvironment,
117    max_requests_per_second: u32,
118    max_requests_per_minute: u32,
119    transport_backend: TransportBackend,
120});
121
122impl Default for BitmexDataClientConfig {
123    fn default() -> Self {
124        Self::builder().build()
125    }
126}
127
128impl BitmexDataClientConfig {
129    /// Creates a configuration with default values.
130    #[must_use]
131    pub fn new() -> Self {
132        Self::default()
133    }
134
135    /// Returns `true` if both API key and secret are available
136    /// (either explicitly set or resolvable from environment variables).
137    #[must_use]
138    pub fn has_api_credentials(&self) -> bool {
139        let (key_var, secret_var) = credential_env_vars(self.environment);
140        let has_key = self.api_key.is_some() || std::env::var(key_var).is_ok();
141        let has_secret = self.api_secret.is_some() || std::env::var(secret_var).is_ok();
142        has_key && has_secret
143    }
144
145    /// Returns the REST base URL, considering overrides and the environment.
146    #[must_use]
147    pub fn http_base_url(&self) -> String {
148        self.base_url_http
149            .clone()
150            .unwrap_or_else(|| match self.environment {
151                BitmexEnvironment::Testnet => BITMEX_HTTP_TESTNET_URL.to_string(),
152                BitmexEnvironment::Mainnet => BITMEX_HTTP_URL.to_string(),
153            })
154    }
155
156    /// Returns the WebSocket URL, considering overrides and the environment.
157    #[must_use]
158    pub fn ws_url(&self) -> String {
159        self.base_url_ws
160            .clone()
161            .unwrap_or_else(|| match self.environment {
162                BitmexEnvironment::Testnet => BITMEX_WS_TESTNET_URL.to_string(),
163                BitmexEnvironment::Mainnet => BITMEX_WS_URL.to_string(),
164            })
165    }
166}
167
168/// Configuration for the BitMEX live execution client.
169#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
170#[serde(default, deny_unknown_fields)]
171#[cfg_attr(
172    feature = "python",
173    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.bitmex", from_py_object)
174)]
175#[cfg_attr(
176    feature = "python",
177    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.bitmex")
178)]
179pub struct BitmexExecClientConfig {
180    /// API key used for authenticated requests.
181    pub api_key: Option<String>,
182    /// API secret used for authenticated requests.
183    pub api_secret: Option<String>,
184    /// Optional override for the REST base URL.
185    pub base_url_http: Option<String>,
186    /// Optional override for the WebSocket URL.
187    pub base_url_ws: Option<String>,
188    /// Optional proxy URL for HTTP and WebSocket transports.
189    pub proxy_url: Option<String>,
190    /// REST timeout in seconds.
191    #[builder(default = 60)]
192    pub http_timeout_secs: u64,
193    /// Maximum retry attempts for REST requests.
194    #[builder(default = 3)]
195    pub max_retries: u32,
196    /// Initial retry backoff in milliseconds.
197    #[builder(default = 1_000)]
198    pub retry_delay_initial_ms: u64,
199    /// Maximum retry backoff in milliseconds.
200    #[builder(default = 10_000)]
201    pub retry_delay_max_ms: u64,
202    /// Heartbeat interval (seconds) for the WebSocket client.
203    #[builder(default = 5)]
204    pub heartbeat_interval_secs: u64,
205    /// Optional WebSocket authentication timeout (seconds), defaulting to
206    /// `AUTHENTICATION_TIMEOUT_SECS` when unset.
207    pub auth_timeout_secs: Option<u64>,
208    /// Receive window in milliseconds for signed requests.
209    ///
210    /// This value determines how far in the future the `api-expires` timestamp will be set
211    /// for signed REST requests. BitMEX uses seconds-granularity Unix timestamps in the
212    /// `api-expires` header, calculated as: `current_timestamp + (recv_window_ms / 1000)`.
213    ///
214    /// **Note**: This parameter is specified in milliseconds for consistency with other
215    /// adapter configurations (e.g., Bybit's `recv_window_ms`), but BitMEX only supports
216    /// seconds-granularity timestamps. The value is converted via integer division, so
217    /// 10000ms becomes 10 seconds, 15500ms becomes 15 seconds, etc.
218    ///
219    /// A larger window provides more tolerance for clock skew and network latency, but
220    /// increases the replay attack window. The default of 10 seconds should be sufficient
221    /// for most deployments. Consider increasing this value (e.g., to 30000ms = 30s) if you
222    /// experience request expiration errors due to clock drift or high network latency.
223    #[builder(default = 10_000)]
224    pub recv_window_ms: u64,
225    /// When `true`, only active instruments are requested during bootstrap.
226    #[builder(default = true)]
227    pub active_only: bool,
228    /// BitMEX environment (mainnet or testnet).
229    #[builder(default)]
230    pub environment: BitmexEnvironment,
231    /// Optional account identifier to associate with the execution client.
232    pub account_id: Option<AccountId>,
233    /// Maximum number of requests per second (burst limit).
234    #[builder(default = 10)]
235    pub max_requests_per_second: u32,
236    /// Maximum number of requests per minute (rolling window).
237    #[builder(default = 120)]
238    pub max_requests_per_minute: u32,
239    /// Number of HTTP clients in the submit broadcaster pool (defaults to 1).
240    pub submitter_pool_size: Option<usize>,
241    /// Number of HTTP clients in the cancel broadcaster pool (defaults to 1).
242    pub canceller_pool_size: Option<usize>,
243    /// Optional list of proxy URLs for submit broadcaster pool (path diversity).
244    pub submitter_proxy_urls: Option<Vec<String>>,
245    /// Optional list of proxy URLs for cancel broadcaster pool (path diversity).
246    pub canceller_proxy_urls: Option<Vec<String>>,
247    /// Optional dead man's switch timeout in seconds.
248    ///
249    /// When set, a background task periodically calls the BitMEX `cancelAllAfter` endpoint
250    /// to keep a server-side timer alive. If the client loses connectivity the timer expires
251    /// and BitMEX cancels all open orders. Calling with `timeout=0` disarms the switch.
252    /// The refresh interval is derived as `timeout / 4` (minimum 1 second).
253    pub deadmans_switch_timeout_secs: Option<u64>,
254    /// WebSocket transport backend (defaults to `Tungstenite`).
255    #[builder(default)]
256    pub transport_backend: TransportBackend,
257}
258
259#[cfg(feature = "python")]
260nautilus_core::impl_pyo3_config_getters!(BitmexExecClientConfig {
261    base_url_http: Option<String>,
262    base_url_ws: Option<String>,
263    http_timeout_secs: u64,
264    max_retries: u32,
265    retry_delay_initial_ms: u64,
266    retry_delay_max_ms: u64,
267    heartbeat_interval_secs: u64,
268    auth_timeout_secs: Option<u64>,
269    recv_window_ms: u64,
270    active_only: bool,
271    environment: BitmexEnvironment,
272    account_id: Option<AccountId>,
273    max_requests_per_second: u32,
274    max_requests_per_minute: u32,
275    submitter_pool_size: Option<usize>,
276    canceller_pool_size: Option<usize>,
277    deadmans_switch_timeout_secs: Option<u64>,
278    transport_backend: TransportBackend,
279});
280
281impl Default for BitmexExecClientConfig {
282    fn default() -> Self {
283        Self::builder().build()
284    }
285}
286
287impl BitmexExecClientConfig {
288    /// Creates a configuration with default values.
289    #[must_use]
290    pub fn new() -> Self {
291        Self::default()
292    }
293
294    /// Returns `true` if both API key and secret are available
295    /// (either explicitly set or resolvable from environment variables).
296    #[must_use]
297    pub fn has_api_credentials(&self) -> bool {
298        let (key_var, secret_var) = credential_env_vars(self.environment);
299        let has_key = self.api_key.is_some() || std::env::var(key_var).is_ok();
300        let has_secret = self.api_secret.is_some() || std::env::var(secret_var).is_ok();
301        has_key && has_secret
302    }
303
304    /// Returns the REST base URL, considering overrides and the environment.
305    #[must_use]
306    pub fn http_base_url(&self) -> String {
307        self.base_url_http
308            .clone()
309            .unwrap_or_else(|| match self.environment {
310                BitmexEnvironment::Testnet => BITMEX_HTTP_TESTNET_URL.to_string(),
311                BitmexEnvironment::Mainnet => BITMEX_HTTP_URL.to_string(),
312            })
313    }
314
315    /// Returns the WebSocket URL, considering overrides and the environment.
316    #[must_use]
317    pub fn ws_url(&self) -> String {
318        self.base_url_ws
319            .clone()
320            .unwrap_or_else(|| match self.environment {
321                BitmexEnvironment::Testnet => BITMEX_WS_TESTNET_URL.to_string(),
322                BitmexEnvironment::Mainnet => BITMEX_WS_URL.to_string(),
323            })
324    }
325}
326
327#[cfg(test)]
328mod tests {
329    use rstest::rstest;
330
331    use super::*;
332
333    #[rstest]
334    fn test_data_config_toml_minimal() {
335        let config: BitmexDataClientConfig = toml::from_str(
336            r#"
337environment = "testnet"
338http_timeout_secs = 30
339active_only = false
340max_requests_per_second = 5
341"#,
342        )
343        .unwrap();
344
345        assert_eq!(config.environment, BitmexEnvironment::Testnet);
346        assert_eq!(config.http_timeout_secs, 30);
347        assert!(!config.active_only);
348        assert_eq!(config.max_requests_per_second, 5);
349    }
350
351    #[rstest]
352    fn test_exec_config_toml_empty_uses_defaults() {
353        let config: BitmexExecClientConfig = toml::from_str("").unwrap();
354        let expected = BitmexExecClientConfig::default();
355
356        assert_eq!(config.environment, expected.environment);
357        assert_eq!(config.http_timeout_secs, expected.http_timeout_secs);
358        assert_eq!(
359            config.heartbeat_interval_secs,
360            expected.heartbeat_interval_secs,
361        );
362        assert_eq!(config.recv_window_ms, expected.recv_window_ms);
363        assert_eq!(config.active_only, expected.active_only);
364        assert_eq!(
365            config.max_requests_per_second,
366            expected.max_requests_per_second,
367        );
368        assert_eq!(config.transport_backend, expected.transport_backend);
369    }
370
371    #[rstest]
372    fn test_config_auth_timeout_secs() {
373        assert_eq!(BitmexDataClientConfig::default().auth_timeout_secs, None);
374        assert_eq!(BitmexExecClientConfig::default().auth_timeout_secs, None);
375
376        let data = BitmexDataClientConfig::builder()
377            .auth_timeout_secs(3)
378            .build();
379        assert_eq!(data.auth_timeout_secs, Some(3));
380
381        let exec = BitmexExecClientConfig::builder()
382            .auth_timeout_secs(4)
383            .build();
384        assert_eq!(exec.auth_timeout_secs, Some(4));
385
386        let data: BitmexDataClientConfig = toml::from_str("auth_timeout_secs = 7\n").unwrap();
387        assert_eq!(data.auth_timeout_secs, Some(7));
388
389        let exec: BitmexExecClientConfig = toml::from_str("auth_timeout_secs = 8\n").unwrap();
390        assert_eq!(exec.auth_timeout_secs, Some(8));
391    }
392}