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}