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.adapters.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
88#[cfg(feature = "python")]
89nautilus_core::impl_pyo3_config_getters!(BybitDataClientConfig {
90    product_types: Vec<BybitProductType>,
91    environment: BybitEnvironment,
92    base_url_http: Option<String>,
93    base_url_ws_public: Option<String>,
94    base_url_ws_private: Option<String>,
95    http_timeout_secs: u64,
96    max_retries: u32,
97    retry_delay_initial_ms: u64,
98    retry_delay_max_ms: u64,
99    heartbeat_interval_secs: u64,
100    recv_window_ms: u64,
101    update_instruments_interval_mins: Option<u64>,
102    transport_backend: TransportBackend,
103});
104
105impl Default for BybitDataClientConfig {
106    fn default() -> Self {
107        Self {
108            update_instruments_interval_mins: Some(60),
109            instrument_poll_interval_secs: Some(60),
110            ..Self::builder().build()
111        }
112    }
113}
114
115impl BybitDataClientConfig {
116    /// Creates a configuration with default values.
117    #[must_use]
118    pub fn new() -> Self {
119        Self::default()
120    }
121
122    /// Returns `true` if both API key and secret are available.
123    #[must_use]
124    pub fn has_api_credentials(&self) -> bool {
125        self.api_key.is_some() && self.api_secret.is_some()
126    }
127
128    /// Returns the REST base URL, considering overrides and environment.
129    #[must_use]
130    pub fn http_base_url(&self) -> String {
131        self.base_url_http
132            .clone()
133            .unwrap_or_else(|| bybit_http_base_url(self.environment).to_string())
134    }
135
136    /// Returns the public WebSocket URL for the given product type.
137    ///
138    /// Falls back to the first product type in the config if multiple are configured.
139    #[must_use]
140    pub fn ws_public_url(&self) -> String {
141        self.base_url_ws_public.clone().unwrap_or_else(|| {
142            let product_type = self
143                .product_types
144                .first()
145                .copied()
146                .unwrap_or(BybitProductType::Linear);
147            bybit_ws_public_url(product_type, self.environment)
148        })
149    }
150
151    /// Returns the public WebSocket URL for a specific product type.
152    #[must_use]
153    pub fn ws_public_url_for(&self, product_type: BybitProductType) -> String {
154        self.base_url_ws_public
155            .clone()
156            .unwrap_or_else(|| bybit_ws_public_url(product_type, self.environment))
157    }
158
159    /// Returns the private WebSocket URL, considering overrides and environment.
160    #[must_use]
161    pub fn ws_private_url(&self) -> String {
162        self.base_url_ws_private
163            .clone()
164            .unwrap_or_else(|| bybit_ws_private_url(self.environment).to_string())
165    }
166
167    /// Returns `true` when private WebSocket connection is required.
168    #[must_use]
169    pub fn requires_private_ws(&self) -> bool {
170        self.has_api_credentials()
171    }
172}
173
174/// Configuration for the Bybit live execution client.
175#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
176#[serde(default, deny_unknown_fields)]
177#[cfg_attr(
178    feature = "python",
179    pyo3::pyclass(module = "nautilus_trader.adapters.bybit", from_py_object)
180)]
181#[cfg_attr(
182    feature = "python",
183    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.bybit")
184)]
185pub struct BybitExecutionClientConfig {
186    /// API key for authenticated requests.
187    pub api_key: Option<String>,
188    /// API secret for authenticated requests.
189    pub api_secret: Option<String>,
190    /// Product types to support (e.g., Linear, Spot, Inverse, Option).
191    #[builder(default = vec![BybitProductType::Linear])]
192    pub product_types: Vec<BybitProductType>,
193    /// Environment selection (Mainnet, Testnet, Demo).
194    #[builder(default = BybitEnvironment::Mainnet)]
195    pub environment: BybitEnvironment,
196    /// Optional override for the REST base URL.
197    pub base_url_http: Option<String>,
198    /// Optional override for the private WebSocket URL.
199    pub base_url_ws_private: Option<String>,
200    /// Optional override for the trade WebSocket URL.
201    pub base_url_ws_trade: Option<String>,
202    /// Optional proxy URL for HTTP and WebSocket transports.
203    pub proxy_url: Option<String>,
204    /// REST timeout in seconds.
205    #[builder(default = 60)]
206    pub http_timeout_secs: u64,
207    /// Maximum retry attempts for REST requests.
208    #[builder(default = 3)]
209    pub max_retries: u32,
210    /// Initial retry backoff in milliseconds.
211    #[builder(default = 1_000)]
212    pub retry_delay_initial_ms: u64,
213    /// Maximum retry backoff in milliseconds.
214    #[builder(default = 10_000)]
215    pub retry_delay_max_ms: u64,
216    /// Heartbeat interval in seconds for WebSocket clients.
217    #[builder(default = 20)]
218    pub heartbeat_interval_secs: u64,
219    /// Optional WebSocket authentication wait timeout (seconds), defaulting to
220    /// the client default when unset.
221    pub auth_timeout_secs: Option<u64>,
222    /// Receive window in milliseconds for signed requests.
223    #[builder(default = 5_000)]
224    pub recv_window_ms: u64,
225    /// Optional account identifier to associate with the execution client.
226    pub account_id: Option<AccountId>,
227    /// Whether scoped execution-client SPOT position requests derive positions from wallet
228    /// balances. The HTTP client rejects enabled unscoped SPOT requests because balances cannot be
229    /// attributed to pairs. The execution client omits SPOT from bulk requests and reports its bulk
230    /// coverage as unavailable.
231    #[builder(default)]
232    pub use_spot_position_reports: bool,
233    /// Whether to automatically repay SPOT margin borrows after BUY orders tracked by
234    /// this client and reported on the standard `execution` channel (not `execution.fast`)
235    /// fully fill.
236    #[builder(default)]
237    pub auto_repay_spot_borrows: bool,
238    /// Leverage configuration for futures (symbol -> leverage).
239    pub futures_leverages: Option<HashMap<String, u32>>,
240    /// Position mode configuration for symbols (symbol -> mode).
241    pub position_mode: Option<HashMap<String, BybitPositionMode>>,
242    /// Unified margin mode setting.
243    pub margin_mode: Option<BybitMarginMode>,
244    /// WebSocket transport backend (defaults to `Tungstenite`).
245    #[builder(default)]
246    pub transport_backend: TransportBackend,
247}
248
249#[cfg(feature = "python")]
250nautilus_core::impl_pyo3_config_getters!(BybitExecutionClientConfig {
251    product_types: Vec<BybitProductType>,
252    environment: BybitEnvironment,
253    base_url_http: Option<String>,
254    base_url_ws_private: Option<String>,
255    base_url_ws_trade: Option<String>,
256    http_timeout_secs: u64,
257    max_retries: u32,
258    retry_delay_initial_ms: u64,
259    retry_delay_max_ms: u64,
260    heartbeat_interval_secs: u64,
261    auth_timeout_secs: Option<u64>,
262    recv_window_ms: u64,
263    account_id: Option<AccountId>,
264    use_spot_position_reports: bool,
265    auto_repay_spot_borrows: bool,
266    margin_mode: Option<BybitMarginMode>,
267    transport_backend: TransportBackend,
268});
269
270impl Default for BybitExecutionClientConfig {
271    fn default() -> Self {
272        Self::builder().build()
273    }
274}
275
276impl BybitExecutionClientConfig {
277    /// Creates a configuration with default values.
278    #[must_use]
279    pub fn new() -> Self {
280        Self::default()
281    }
282
283    /// Returns `true` if both API key and secret are available.
284    #[must_use]
285    pub fn has_api_credentials(&self) -> bool {
286        self.api_key.is_some() && self.api_secret.is_some()
287    }
288
289    /// Returns the REST base URL, considering overrides and environment.
290    #[must_use]
291    pub fn http_base_url(&self) -> String {
292        self.base_url_http
293            .clone()
294            .unwrap_or_else(|| bybit_http_base_url(self.environment).to_string())
295    }
296
297    /// Returns the private WebSocket URL, considering overrides and environment.
298    #[must_use]
299    pub fn ws_private_url(&self) -> String {
300        self.base_url_ws_private
301            .clone()
302            .unwrap_or_else(|| bybit_ws_private_url(self.environment).to_string())
303    }
304
305    /// Returns the trade WebSocket URL, considering overrides and environment.
306    #[must_use]
307    pub fn ws_trade_url(&self) -> String {
308        self.base_url_ws_trade
309            .clone()
310            .unwrap_or_else(|| bybit_ws_trade_url(self.environment).to_string())
311    }
312}
313#[cfg(test)]
314mod tests {
315    use rstest::rstest;
316
317    use super::*;
318
319    #[rstest]
320    fn test_data_config_default() {
321        let config = BybitDataClientConfig::default();
322
323        assert!(!config.has_api_credentials());
324        assert_eq!(config.product_types, vec![BybitProductType::Linear]);
325        assert_eq!(config.http_timeout_secs, 60);
326        assert_eq!(config.heartbeat_interval_secs, 20);
327    }
328
329    #[rstest]
330    fn test_data_config_with_credentials() {
331        let config = BybitDataClientConfig {
332            api_key: Some("test_key".to_string()),
333            api_secret: Some("test_secret".to_string()),
334            ..Default::default()
335        };
336
337        assert!(config.has_api_credentials());
338        assert!(config.requires_private_ws());
339    }
340
341    #[rstest]
342    fn test_data_config_http_url_mainnet() {
343        let config = BybitDataClientConfig {
344            environment: BybitEnvironment::Mainnet,
345            ..Default::default()
346        };
347
348        assert_eq!(config.http_base_url(), "https://api.bybit.com");
349    }
350
351    #[rstest]
352    fn test_data_config_http_url_testnet() {
353        let config = BybitDataClientConfig {
354            environment: BybitEnvironment::Testnet,
355            ..Default::default()
356        };
357
358        assert_eq!(config.http_base_url(), "https://api-testnet.bybit.com");
359    }
360
361    #[rstest]
362    fn test_data_config_http_url_demo() {
363        let config = BybitDataClientConfig {
364            environment: BybitEnvironment::Demo,
365            ..Default::default()
366        };
367
368        assert_eq!(config.http_base_url(), "https://api-demo.bybit.com");
369    }
370
371    #[rstest]
372    fn test_data_config_http_url_override() {
373        let custom_url = "https://custom.bybit.com";
374        let config = BybitDataClientConfig {
375            base_url_http: Some(custom_url.to_string()),
376            ..Default::default()
377        };
378
379        assert_eq!(config.http_base_url(), custom_url);
380    }
381
382    #[rstest]
383    fn test_data_config_ws_public_url() {
384        let config = BybitDataClientConfig {
385            environment: BybitEnvironment::Mainnet,
386            ..Default::default()
387        };
388
389        assert_eq!(
390            config.ws_public_url(),
391            "wss://stream.bybit.com/v5/public/linear"
392        );
393    }
394
395    #[rstest]
396    fn test_data_config_ws_public_url_for_spot() {
397        let config = BybitDataClientConfig {
398            environment: BybitEnvironment::Mainnet,
399            ..Default::default()
400        };
401
402        assert_eq!(
403            config.ws_public_url_for(BybitProductType::Spot),
404            "wss://stream.bybit.com/v5/public/spot"
405        );
406    }
407
408    #[rstest]
409    fn test_data_config_ws_private_url() {
410        let config = BybitDataClientConfig {
411            environment: BybitEnvironment::Mainnet,
412            ..Default::default()
413        };
414
415        assert_eq!(config.ws_private_url(), "wss://stream.bybit.com/v5/private");
416    }
417
418    #[rstest]
419    fn test_data_config_ws_private_url_testnet() {
420        let config = BybitDataClientConfig {
421            environment: BybitEnvironment::Testnet,
422            ..Default::default()
423        };
424
425        assert_eq!(
426            config.ws_private_url(),
427            "wss://stream-testnet.bybit.com/v5/private"
428        );
429    }
430
431    #[rstest]
432    fn test_exec_config_default() {
433        let config = BybitExecutionClientConfig::default();
434
435        assert!(!config.has_api_credentials());
436        assert_eq!(config.product_types, vec![BybitProductType::Linear]);
437        assert_eq!(config.http_timeout_secs, 60);
438        assert_eq!(config.heartbeat_interval_secs, 20);
439    }
440
441    #[rstest]
442    fn test_exec_config_with_credentials() {
443        let config = BybitExecutionClientConfig {
444            api_key: Some("test_key".to_string()),
445            api_secret: Some("test_secret".to_string()),
446            ..Default::default()
447        };
448
449        assert!(config.has_api_credentials());
450    }
451
452    #[rstest]
453    fn test_exec_config_urls() {
454        let config = BybitExecutionClientConfig {
455            environment: BybitEnvironment::Mainnet,
456            ..Default::default()
457        };
458
459        assert_eq!(config.http_base_url(), "https://api.bybit.com");
460        assert_eq!(config.ws_private_url(), "wss://stream.bybit.com/v5/private");
461        assert_eq!(config.ws_trade_url(), "wss://stream.bybit.com/v5/trade");
462    }
463
464    #[rstest]
465    fn test_exec_config_urls_testnet() {
466        let config = BybitExecutionClientConfig {
467            environment: BybitEnvironment::Testnet,
468            ..Default::default()
469        };
470
471        assert_eq!(config.http_base_url(), "https://api-testnet.bybit.com");
472        assert_eq!(
473            config.ws_private_url(),
474            "wss://stream-testnet.bybit.com/v5/private"
475        );
476        assert_eq!(
477            config.ws_trade_url(),
478            "wss://stream-testnet.bybit.com/v5/trade"
479        );
480    }
481
482    #[rstest]
483    fn test_exec_config_custom_urls() {
484        let config = BybitExecutionClientConfig {
485            base_url_http: Some("https://custom-http.bybit.com".to_string()),
486            base_url_ws_private: Some("wss://custom-private.bybit.com".to_string()),
487            base_url_ws_trade: Some("wss://custom-trade.bybit.com".to_string()),
488            ..Default::default()
489        };
490
491        assert_eq!(config.http_base_url(), "https://custom-http.bybit.com");
492        assert_eq!(config.ws_private_url(), "wss://custom-private.bybit.com");
493        assert_eq!(config.ws_trade_url(), "wss://custom-trade.bybit.com");
494    }
495
496    #[rstest]
497    fn test_data_config_toml_minimal() {
498        let config: BybitDataClientConfig = toml::from_str(
499            r#"
500environment = "testnet"
501product_types = ["spot", "linear"]
502http_timeout_secs = 45
503"#,
504        )
505        .unwrap();
506
507        assert_eq!(config.environment, BybitEnvironment::Testnet);
508        assert_eq!(
509            config.product_types,
510            vec![BybitProductType::Spot, BybitProductType::Linear]
511        );
512        assert_eq!(config.http_timeout_secs, 45);
513    }
514
515    #[rstest]
516    fn test_exec_config_toml_empty_uses_defaults() {
517        let config: BybitExecutionClientConfig = toml::from_str("").unwrap();
518        let expected = BybitExecutionClientConfig::default();
519
520        assert_eq!(config.environment, expected.environment);
521        assert_eq!(config.product_types, expected.product_types);
522        assert_eq!(config.http_timeout_secs, expected.http_timeout_secs);
523        assert_eq!(
524            config.heartbeat_interval_secs,
525            expected.heartbeat_interval_secs,
526        );
527        assert_eq!(config.recv_window_ms, expected.recv_window_ms);
528        assert_eq!(config.transport_backend, expected.transport_backend);
529    }
530}