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