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    /// Receive window in milliseconds for signed requests.
65    ///
66    /// This value determines how far in the future the `api-expires` timestamp will be set
67    /// for signed REST requests. BitMEX uses seconds-granularity Unix timestamps in the
68    /// `api-expires` header, calculated as: `current_timestamp + (recv_window_ms / 1000)`.
69    ///
70    /// **Note**: This parameter is specified in milliseconds for consistency with other
71    /// adapter configurations (e.g., Bybit's `recv_window_ms`), but BitMEX only supports
72    /// seconds-granularity timestamps. The value is converted via integer division, so
73    /// 10000ms becomes 10 seconds, 15500ms becomes 15 seconds, etc.
74    ///
75    /// A larger window provides more tolerance for clock skew and network latency, but
76    /// increases the replay attack window. The default of 10 seconds should be sufficient
77    /// for most deployments. Consider increasing this value (e.g., to 30_000ms = 30s) if you
78    /// experience request expiration errors due to clock drift or high network latency.
79    #[builder(default = 10_000)]
80    pub recv_window_ms: u64,
81    /// When `true`, only active instruments are requested during bootstrap.
82    #[builder(default = true)]
83    pub active_only: bool,
84    /// Optional interval (minutes) for instrument refresh from REST.
85    pub update_instruments_interval_mins: Option<u64>,
86    /// BitMEX environment (mainnet or testnet).
87    #[builder(default)]
88    pub environment: BitmexEnvironment,
89    /// Maximum number of requests per second (burst limit).
90    #[builder(default = 10)]
91    pub max_requests_per_second: u32,
92    /// Maximum number of requests per minute (rolling window).
93    #[builder(default = 120)]
94    pub max_requests_per_minute: u32,
95    /// WebSocket transport backend (defaults to `Tungstenite`).
96    #[builder(default)]
97    pub transport_backend: TransportBackend,
98}
99
100impl Default for BitmexDataClientConfig {
101    fn default() -> Self {
102        Self::builder().build()
103    }
104}
105
106impl BitmexDataClientConfig {
107    /// Creates a configuration with default values.
108    #[must_use]
109    pub fn new() -> Self {
110        Self::default()
111    }
112
113    /// Returns `true` if both API key and secret are available
114    /// (either explicitly set or resolvable from environment variables).
115    #[must_use]
116    pub fn has_api_credentials(&self) -> bool {
117        let (key_var, secret_var) = credential_env_vars(self.environment);
118        let has_key = self.api_key.is_some() || std::env::var(key_var).is_ok();
119        let has_secret = self.api_secret.is_some() || std::env::var(secret_var).is_ok();
120        has_key && has_secret
121    }
122
123    /// Returns the REST base URL, considering overrides and the environment.
124    #[must_use]
125    pub fn http_base_url(&self) -> String {
126        self.base_url_http
127            .clone()
128            .unwrap_or_else(|| match self.environment {
129                BitmexEnvironment::Testnet => BITMEX_HTTP_TESTNET_URL.to_string(),
130                BitmexEnvironment::Mainnet => BITMEX_HTTP_URL.to_string(),
131            })
132    }
133
134    /// Returns the WebSocket URL, considering overrides and the environment.
135    #[must_use]
136    pub fn ws_url(&self) -> String {
137        self.base_url_ws
138            .clone()
139            .unwrap_or_else(|| match self.environment {
140                BitmexEnvironment::Testnet => BITMEX_WS_TESTNET_URL.to_string(),
141                BitmexEnvironment::Mainnet => BITMEX_WS_URL.to_string(),
142            })
143    }
144}
145
146/// Configuration for the BitMEX live execution client.
147#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
148#[serde(default, deny_unknown_fields)]
149#[cfg_attr(
150    feature = "python",
151    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.bitmex", from_py_object)
152)]
153#[cfg_attr(
154    feature = "python",
155    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.bitmex")
156)]
157pub struct BitmexExecClientConfig {
158    /// API key used for authenticated requests.
159    pub api_key: Option<String>,
160    /// API secret used for authenticated requests.
161    pub api_secret: Option<String>,
162    /// Optional override for the REST base URL.
163    pub base_url_http: Option<String>,
164    /// Optional override for the WebSocket URL.
165    pub base_url_ws: Option<String>,
166    /// Optional proxy URL for HTTP and WebSocket transports.
167    pub proxy_url: Option<String>,
168    /// REST timeout in seconds.
169    #[builder(default = 60)]
170    pub http_timeout_secs: u64,
171    /// Maximum retry attempts for REST requests.
172    #[builder(default = 3)]
173    pub max_retries: u32,
174    /// Initial retry backoff in milliseconds.
175    #[builder(default = 1_000)]
176    pub retry_delay_initial_ms: u64,
177    /// Maximum retry backoff in milliseconds.
178    #[builder(default = 10_000)]
179    pub retry_delay_max_ms: u64,
180    /// Heartbeat interval (seconds) for the WebSocket client.
181    #[builder(default = 5)]
182    pub heartbeat_interval_secs: u64,
183    /// Receive window in milliseconds for signed requests.
184    ///
185    /// This value determines how far in the future the `api-expires` timestamp will be set
186    /// for signed REST requests. BitMEX uses seconds-granularity Unix timestamps in the
187    /// `api-expires` header, calculated as: `current_timestamp + (recv_window_ms / 1000)`.
188    ///
189    /// **Note**: This parameter is specified in milliseconds for consistency with other
190    /// adapter configurations (e.g., Bybit's `recv_window_ms`), but BitMEX only supports
191    /// seconds-granularity timestamps. The value is converted via integer division, so
192    /// 10000ms becomes 10 seconds, 15500ms becomes 15 seconds, etc.
193    ///
194    /// A larger window provides more tolerance for clock skew and network latency, but
195    /// increases the replay attack window. The default of 10 seconds should be sufficient
196    /// for most deployments. Consider increasing this value (e.g., to 30000ms = 30s) if you
197    /// experience request expiration errors due to clock drift or high network latency.
198    #[builder(default = 10_000)]
199    pub recv_window_ms: u64,
200    /// When `true`, only active instruments are requested during bootstrap.
201    #[builder(default = true)]
202    pub active_only: bool,
203    /// BitMEX environment (mainnet or testnet).
204    #[builder(default)]
205    pub environment: BitmexEnvironment,
206    /// Optional account identifier to associate with the execution client.
207    pub account_id: Option<AccountId>,
208    /// Maximum number of requests per second (burst limit).
209    #[builder(default = 10)]
210    pub max_requests_per_second: u32,
211    /// Maximum number of requests per minute (rolling window).
212    #[builder(default = 120)]
213    pub max_requests_per_minute: u32,
214    /// Number of HTTP clients in the submit broadcaster pool (defaults to 1).
215    pub submitter_pool_size: Option<usize>,
216    /// Number of HTTP clients in the cancel broadcaster pool (defaults to 1).
217    pub canceller_pool_size: Option<usize>,
218    /// Optional list of proxy URLs for submit broadcaster pool (path diversity).
219    pub submitter_proxy_urls: Option<Vec<String>>,
220    /// Optional list of proxy URLs for cancel broadcaster pool (path diversity).
221    pub canceller_proxy_urls: Option<Vec<String>>,
222    /// Optional dead man's switch timeout in seconds.
223    ///
224    /// When set, a background task periodically calls the BitMEX `cancelAllAfter` endpoint
225    /// to keep a server-side timer alive. If the client loses connectivity the timer expires
226    /// and BitMEX cancels all open orders. Calling with `timeout=0` disarms the switch.
227    /// The refresh interval is derived as `timeout / 4` (minimum 1 second).
228    pub deadmans_switch_timeout_secs: Option<u64>,
229    /// WebSocket transport backend (defaults to `Tungstenite`).
230    #[builder(default)]
231    pub transport_backend: TransportBackend,
232}
233
234impl Default for BitmexExecClientConfig {
235    fn default() -> Self {
236        Self::builder().build()
237    }
238}
239
240impl BitmexExecClientConfig {
241    /// Creates a configuration with default values.
242    #[must_use]
243    pub fn new() -> Self {
244        Self::default()
245    }
246
247    /// Returns `true` if both API key and secret are available
248    /// (either explicitly set or resolvable from environment variables).
249    #[must_use]
250    pub fn has_api_credentials(&self) -> bool {
251        let (key_var, secret_var) = credential_env_vars(self.environment);
252        let has_key = self.api_key.is_some() || std::env::var(key_var).is_ok();
253        let has_secret = self.api_secret.is_some() || std::env::var(secret_var).is_ok();
254        has_key && has_secret
255    }
256
257    /// Returns the REST base URL, considering overrides and the environment.
258    #[must_use]
259    pub fn http_base_url(&self) -> String {
260        self.base_url_http
261            .clone()
262            .unwrap_or_else(|| match self.environment {
263                BitmexEnvironment::Testnet => BITMEX_HTTP_TESTNET_URL.to_string(),
264                BitmexEnvironment::Mainnet => BITMEX_HTTP_URL.to_string(),
265            })
266    }
267
268    /// Returns the WebSocket URL, considering overrides and the environment.
269    #[must_use]
270    pub fn ws_url(&self) -> String {
271        self.base_url_ws
272            .clone()
273            .unwrap_or_else(|| match self.environment {
274                BitmexEnvironment::Testnet => BITMEX_WS_TESTNET_URL.to_string(),
275                BitmexEnvironment::Mainnet => BITMEX_WS_URL.to_string(),
276            })
277    }
278}
279
280#[cfg(test)]
281mod tests {
282    use rstest::rstest;
283
284    use super::*;
285
286    #[rstest]
287    fn test_data_config_toml_minimal() {
288        let config: BitmexDataClientConfig = toml::from_str(
289            r#"
290environment = "testnet"
291http_timeout_secs = 30
292active_only = false
293max_requests_per_second = 5
294"#,
295        )
296        .unwrap();
297
298        assert_eq!(config.environment, BitmexEnvironment::Testnet);
299        assert_eq!(config.http_timeout_secs, 30);
300        assert!(!config.active_only);
301        assert_eq!(config.max_requests_per_second, 5);
302    }
303
304    #[rstest]
305    fn test_exec_config_toml_empty_uses_defaults() {
306        let config: BitmexExecClientConfig = toml::from_str("").unwrap();
307        let expected = BitmexExecClientConfig::default();
308
309        assert_eq!(config.environment, expected.environment);
310        assert_eq!(config.http_timeout_secs, expected.http_timeout_secs);
311        assert_eq!(
312            config.heartbeat_interval_secs,
313            expected.heartbeat_interval_secs,
314        );
315        assert_eq!(config.recv_window_ms, expected.recv_window_ms);
316        assert_eq!(config.active_only, expected.active_only);
317        assert_eq!(
318            config.max_requests_per_second,
319            expected.max_requests_per_second,
320        );
321        assert_eq!(config.transport_backend, expected.transport_backend);
322    }
323}