Skip to main content

nautilus_bybit/
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 structures for the Bybit adapter.
17
18use std::collections::HashMap;
19
20use nautilus_model::identifiers::AccountId;
21use nautilus_network::websocket::TransportBackend;
22use serde::{Deserialize, Serialize};
23
24use crate::common::{
25    enums::{BybitEnvironment, BybitMarginMode, BybitPositionMode, BybitProductType},
26    urls::{bybit_http_base_url, bybit_ws_private_url, bybit_ws_public_url, bybit_ws_trade_url},
27};
28
29/// Configuration for the Bybit live data client.
30#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
31#[serde(default, deny_unknown_fields)]
32#[cfg_attr(
33    feature = "python",
34    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.bybit", from_py_object)
35)]
36#[cfg_attr(
37    feature = "python",
38    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.bybit")
39)]
40pub struct BybitDataClientConfig {
41    /// Optional API key for authenticated REST/WebSocket requests.
42    pub api_key: Option<String>,
43    /// Optional API secret for authenticated REST/WebSocket requests.
44    pub api_secret: Option<String>,
45    /// Product types to subscribe to (e.g., Linear, Spot, Inverse, Option).
46    #[builder(default = vec![BybitProductType::Linear])]
47    pub product_types: Vec<BybitProductType>,
48    /// Environment selection (Mainnet, Testnet, Demo).
49    #[builder(default = BybitEnvironment::Mainnet)]
50    pub environment: BybitEnvironment,
51    /// Optional override for the REST base URL.
52    pub base_url_http: Option<String>,
53    /// Optional override for the public WebSocket URL.
54    pub base_url_ws_public: Option<String>,
55    /// Optional override for the private WebSocket URL.
56    pub base_url_ws_private: Option<String>,
57    /// Optional proxy URL for HTTP and WebSocket transports.
58    pub proxy_url: Option<String>,
59    /// REST timeout in seconds.
60    #[builder(default = 60)]
61    pub http_timeout_secs: u64,
62    /// Maximum retry attempts for REST requests.
63    #[builder(default = 3)]
64    pub max_retries: u32,
65    /// Initial retry backoff in milliseconds.
66    #[builder(default = 1_000)]
67    pub retry_delay_initial_ms: u64,
68    /// Maximum retry backoff in milliseconds.
69    #[builder(default = 10_000)]
70    pub retry_delay_max_ms: u64,
71    /// Heartbeat interval in seconds for WebSocket clients.
72    #[builder(default = 20)]
73    pub heartbeat_interval_secs: u64,
74    /// Receive window in milliseconds for signed requests.
75    #[builder(default = 5_000)]
76    pub recv_window_ms: u64,
77    /// Interval in minutes for instrument refresh from REST.
78    /// When `None`, instrument refresh is disabled.
79    pub update_instruments_interval_mins: Option<u64>,
80    /// Interval in seconds for polling instrument definitions and status changes from REST.
81    /// When `None`, instrument/status polling is disabled.
82    pub instrument_poll_interval_secs: Option<u64>,
83    /// WebSocket transport backend (defaults to `Tungstenite`).
84    #[builder(default)]
85    pub transport_backend: TransportBackend,
86}
87
88impl Default for BybitDataClientConfig {
89    fn default() -> Self {
90        Self {
91            update_instruments_interval_mins: Some(60),
92            instrument_poll_interval_secs: Some(60),
93            ..Self::builder().build()
94        }
95    }
96}
97
98impl BybitDataClientConfig {
99    /// Creates a configuration with default values.
100    #[must_use]
101    pub fn new() -> Self {
102        Self::default()
103    }
104
105    /// Returns `true` if both API key and secret are available.
106    #[must_use]
107    pub fn has_api_credentials(&self) -> bool {
108        self.api_key.is_some() && self.api_secret.is_some()
109    }
110
111    /// Returns the REST base URL, considering overrides and environment.
112    #[must_use]
113    pub fn http_base_url(&self) -> String {
114        self.base_url_http
115            .clone()
116            .unwrap_or_else(|| bybit_http_base_url(self.environment).to_string())
117    }
118
119    /// Returns the public WebSocket URL for the given product type.
120    ///
121    /// Falls back to the first product type in the config if multiple are configured.
122    #[must_use]
123    pub fn ws_public_url(&self) -> String {
124        self.base_url_ws_public.clone().unwrap_or_else(|| {
125            let product_type = self
126                .product_types
127                .first()
128                .copied()
129                .unwrap_or(BybitProductType::Linear);
130            bybit_ws_public_url(product_type, self.environment)
131        })
132    }
133
134    /// Returns the public WebSocket URL for a specific product type.
135    #[must_use]
136    pub fn ws_public_url_for(&self, product_type: BybitProductType) -> String {
137        self.base_url_ws_public
138            .clone()
139            .unwrap_or_else(|| bybit_ws_public_url(product_type, self.environment))
140    }
141
142    /// Returns the private WebSocket URL, considering overrides and environment.
143    #[must_use]
144    pub fn ws_private_url(&self) -> String {
145        self.base_url_ws_private
146            .clone()
147            .unwrap_or_else(|| bybit_ws_private_url(self.environment).to_string())
148    }
149
150    /// Returns `true` when private WebSocket connection is required.
151    #[must_use]
152    pub fn requires_private_ws(&self) -> bool {
153        self.has_api_credentials()
154    }
155}
156
157/// Configuration for the Bybit live execution client.
158#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
159#[serde(default, deny_unknown_fields)]
160#[cfg_attr(
161    feature = "python",
162    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.bybit", from_py_object)
163)]
164#[cfg_attr(
165    feature = "python",
166    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.bybit")
167)]
168pub struct BybitExecClientConfig {
169    /// API key for authenticated requests.
170    pub api_key: Option<String>,
171    /// API secret for authenticated requests.
172    pub api_secret: Option<String>,
173    /// Product types to support (e.g., Linear, Spot, Inverse, Option).
174    #[builder(default = vec![BybitProductType::Linear])]
175    pub product_types: Vec<BybitProductType>,
176    /// Environment selection (Mainnet, Testnet, Demo).
177    #[builder(default = BybitEnvironment::Mainnet)]
178    pub environment: BybitEnvironment,
179    /// Optional override for the REST base URL.
180    pub base_url_http: Option<String>,
181    /// Optional override for the private WebSocket URL.
182    pub base_url_ws_private: Option<String>,
183    /// Optional override for the trade WebSocket URL.
184    pub base_url_ws_trade: Option<String>,
185    /// Optional proxy URL for HTTP and WebSocket transports.
186    pub proxy_url: Option<String>,
187    /// REST timeout in seconds.
188    #[builder(default = 60)]
189    pub http_timeout_secs: u64,
190    /// Maximum retry attempts for REST requests.
191    #[builder(default = 3)]
192    pub max_retries: u32,
193    /// Initial retry backoff in milliseconds.
194    #[builder(default = 1_000)]
195    pub retry_delay_initial_ms: u64,
196    /// Maximum retry backoff in milliseconds.
197    #[builder(default = 10_000)]
198    pub retry_delay_max_ms: u64,
199    /// Heartbeat interval in seconds for WebSocket clients.
200    #[builder(default = 5)]
201    pub heartbeat_interval_secs: u64,
202    /// Receive window in milliseconds for signed requests.
203    #[builder(default = 5_000)]
204    pub recv_window_ms: u64,
205    /// Optional account identifier to associate with the execution client.
206    pub account_id: Option<AccountId>,
207    /// Whether to generate position reports from wallet balances for SPOT positions.
208    #[builder(default)]
209    pub use_spot_position_reports: bool,
210    /// Leverage configuration for futures (symbol -> leverage).
211    pub futures_leverages: Option<HashMap<String, u32>>,
212    /// Position mode configuration for symbols (symbol -> mode).
213    pub position_mode: Option<HashMap<String, BybitPositionMode>>,
214    /// Unified margin mode setting.
215    pub margin_mode: Option<BybitMarginMode>,
216    /// WebSocket transport backend (defaults to `Tungstenite`).
217    #[builder(default)]
218    pub transport_backend: TransportBackend,
219}
220
221impl Default for BybitExecClientConfig {
222    fn default() -> Self {
223        Self::builder().build()
224    }
225}
226
227impl BybitExecClientConfig {
228    /// Creates a configuration with default values.
229    #[must_use]
230    pub fn new() -> Self {
231        Self::default()
232    }
233
234    /// Returns `true` if both API key and secret are available.
235    #[must_use]
236    pub fn has_api_credentials(&self) -> bool {
237        self.api_key.is_some() && self.api_secret.is_some()
238    }
239
240    /// Returns the REST base URL, considering overrides and environment.
241    #[must_use]
242    pub fn http_base_url(&self) -> String {
243        self.base_url_http
244            .clone()
245            .unwrap_or_else(|| bybit_http_base_url(self.environment).to_string())
246    }
247
248    /// Returns the private WebSocket URL, considering overrides and environment.
249    #[must_use]
250    pub fn ws_private_url(&self) -> String {
251        self.base_url_ws_private
252            .clone()
253            .unwrap_or_else(|| bybit_ws_private_url(self.environment).to_string())
254    }
255
256    /// Returns the trade WebSocket URL, considering overrides and environment.
257    #[must_use]
258    pub fn ws_trade_url(&self) -> String {
259        self.base_url_ws_trade
260            .clone()
261            .unwrap_or_else(|| bybit_ws_trade_url(self.environment).to_string())
262    }
263}
264#[cfg(test)]
265mod tests {
266    use rstest::rstest;
267
268    use super::*;
269
270    #[rstest]
271    fn test_data_config_default() {
272        let config = BybitDataClientConfig::default();
273
274        assert!(!config.has_api_credentials());
275        assert_eq!(config.product_types, vec![BybitProductType::Linear]);
276        assert_eq!(config.http_timeout_secs, 60);
277        assert_eq!(config.heartbeat_interval_secs, 20);
278    }
279
280    #[rstest]
281    fn test_data_config_with_credentials() {
282        let config = BybitDataClientConfig {
283            api_key: Some("test_key".to_string()),
284            api_secret: Some("test_secret".to_string()),
285            ..Default::default()
286        };
287
288        assert!(config.has_api_credentials());
289        assert!(config.requires_private_ws());
290    }
291
292    #[rstest]
293    fn test_data_config_http_url_mainnet() {
294        let config = BybitDataClientConfig {
295            environment: BybitEnvironment::Mainnet,
296            ..Default::default()
297        };
298
299        assert_eq!(config.http_base_url(), "https://api.bybit.com");
300    }
301
302    #[rstest]
303    fn test_data_config_http_url_testnet() {
304        let config = BybitDataClientConfig {
305            environment: BybitEnvironment::Testnet,
306            ..Default::default()
307        };
308
309        assert_eq!(config.http_base_url(), "https://api-testnet.bybit.com");
310    }
311
312    #[rstest]
313    fn test_data_config_http_url_demo() {
314        let config = BybitDataClientConfig {
315            environment: BybitEnvironment::Demo,
316            ..Default::default()
317        };
318
319        assert_eq!(config.http_base_url(), "https://api-demo.bybit.com");
320    }
321
322    #[rstest]
323    fn test_data_config_http_url_override() {
324        let custom_url = "https://custom.bybit.com";
325        let config = BybitDataClientConfig {
326            base_url_http: Some(custom_url.to_string()),
327            ..Default::default()
328        };
329
330        assert_eq!(config.http_base_url(), custom_url);
331    }
332
333    #[rstest]
334    fn test_data_config_ws_public_url() {
335        let config = BybitDataClientConfig {
336            environment: BybitEnvironment::Mainnet,
337            ..Default::default()
338        };
339
340        assert_eq!(
341            config.ws_public_url(),
342            "wss://stream.bybit.com/v5/public/linear"
343        );
344    }
345
346    #[rstest]
347    fn test_data_config_ws_public_url_for_spot() {
348        let config = BybitDataClientConfig {
349            environment: BybitEnvironment::Mainnet,
350            ..Default::default()
351        };
352
353        assert_eq!(
354            config.ws_public_url_for(BybitProductType::Spot),
355            "wss://stream.bybit.com/v5/public/spot"
356        );
357    }
358
359    #[rstest]
360    fn test_data_config_ws_private_url() {
361        let config = BybitDataClientConfig {
362            environment: BybitEnvironment::Mainnet,
363            ..Default::default()
364        };
365
366        assert_eq!(config.ws_private_url(), "wss://stream.bybit.com/v5/private");
367    }
368
369    #[rstest]
370    fn test_data_config_ws_private_url_testnet() {
371        let config = BybitDataClientConfig {
372            environment: BybitEnvironment::Testnet,
373            ..Default::default()
374        };
375
376        assert_eq!(
377            config.ws_private_url(),
378            "wss://stream-testnet.bybit.com/v5/private"
379        );
380    }
381
382    #[rstest]
383    fn test_exec_config_default() {
384        let config = BybitExecClientConfig::default();
385
386        assert!(!config.has_api_credentials());
387        assert_eq!(config.product_types, vec![BybitProductType::Linear]);
388        assert_eq!(config.http_timeout_secs, 60);
389        assert_eq!(config.heartbeat_interval_secs, 5);
390    }
391
392    #[rstest]
393    fn test_exec_config_with_credentials() {
394        let config = BybitExecClientConfig {
395            api_key: Some("test_key".to_string()),
396            api_secret: Some("test_secret".to_string()),
397            ..Default::default()
398        };
399
400        assert!(config.has_api_credentials());
401    }
402
403    #[rstest]
404    fn test_exec_config_urls() {
405        let config = BybitExecClientConfig {
406            environment: BybitEnvironment::Mainnet,
407            ..Default::default()
408        };
409
410        assert_eq!(config.http_base_url(), "https://api.bybit.com");
411        assert_eq!(config.ws_private_url(), "wss://stream.bybit.com/v5/private");
412        assert_eq!(config.ws_trade_url(), "wss://stream.bybit.com/v5/trade");
413    }
414
415    #[rstest]
416    fn test_exec_config_urls_testnet() {
417        let config = BybitExecClientConfig {
418            environment: BybitEnvironment::Testnet,
419            ..Default::default()
420        };
421
422        assert_eq!(config.http_base_url(), "https://api-testnet.bybit.com");
423        assert_eq!(
424            config.ws_private_url(),
425            "wss://stream-testnet.bybit.com/v5/private"
426        );
427        assert_eq!(
428            config.ws_trade_url(),
429            "wss://stream-testnet.bybit.com/v5/trade"
430        );
431    }
432
433    #[rstest]
434    fn test_exec_config_custom_urls() {
435        let config = BybitExecClientConfig {
436            base_url_http: Some("https://custom-http.bybit.com".to_string()),
437            base_url_ws_private: Some("wss://custom-private.bybit.com".to_string()),
438            base_url_ws_trade: Some("wss://custom-trade.bybit.com".to_string()),
439            ..Default::default()
440        };
441
442        assert_eq!(config.http_base_url(), "https://custom-http.bybit.com");
443        assert_eq!(config.ws_private_url(), "wss://custom-private.bybit.com");
444        assert_eq!(config.ws_trade_url(), "wss://custom-trade.bybit.com");
445    }
446
447    #[rstest]
448    fn test_data_config_toml_minimal() {
449        let config: BybitDataClientConfig = toml::from_str(
450            r#"
451environment = "testnet"
452product_types = ["spot", "linear"]
453http_timeout_secs = 45
454"#,
455        )
456        .unwrap();
457
458        assert_eq!(config.environment, BybitEnvironment::Testnet);
459        assert_eq!(
460            config.product_types,
461            vec![BybitProductType::Spot, BybitProductType::Linear]
462        );
463        assert_eq!(config.http_timeout_secs, 45);
464    }
465
466    #[rstest]
467    fn test_exec_config_toml_empty_uses_defaults() {
468        let config: BybitExecClientConfig = toml::from_str("").unwrap();
469        let expected = BybitExecClientConfig::default();
470
471        assert_eq!(config.environment, expected.environment);
472        assert_eq!(config.product_types, expected.product_types);
473        assert_eq!(config.http_timeout_secs, expected.http_timeout_secs);
474        assert_eq!(
475            config.heartbeat_interval_secs,
476            expected.heartbeat_interval_secs,
477        );
478        assert_eq!(config.recv_window_ms, expected.recv_window_ms);
479        assert_eq!(config.transport_backend, expected.transport_backend);
480    }
481}