1use 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#[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 pub api_key: Option<String>,
41 pub api_secret: Option<String>,
43 #[builder(default = vec![BybitProductType::Linear])]
45 pub product_types: Vec<BybitProductType>,
46 #[builder(default = BybitEnvironment::Mainnet)]
48 pub environment: BybitEnvironment,
49 pub base_url_http: Option<String>,
51 pub base_url_ws_public: Option<String>,
53 pub base_url_ws_private: Option<String>,
55 pub proxy_url: Option<String>,
57 #[builder(default = 60)]
59 pub http_timeout_secs: u64,
60 #[builder(default = 3)]
62 pub max_retries: u32,
63 #[builder(default = 1_000)]
65 pub retry_delay_initial_ms: u64,
66 #[builder(default = 10_000)]
68 pub retry_delay_max_ms: u64,
69 #[builder(default = 20)]
71 pub heartbeat_interval_secs: u64,
72 #[builder(default = 5_000)]
74 pub recv_window_ms: u64,
75 pub update_instruments_interval_mins: Option<u64>,
78 pub instrument_status_poll_secs: Option<u64>,
81 #[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 #[must_use]
99 pub fn new() -> Self {
100 Self::default()
101 }
102
103 #[must_use]
105 pub fn has_api_credentials(&self) -> bool {
106 self.api_key.is_some() && self.api_secret.is_some()
107 }
108
109 #[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 #[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 #[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 #[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 #[must_use]
150 pub fn requires_private_ws(&self) -> bool {
151 self.has_api_credentials()
152 }
153}
154
155#[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 pub api_key: Option<String>,
168 pub api_secret: Option<String>,
170 #[builder(default = vec![BybitProductType::Linear])]
172 pub product_types: Vec<BybitProductType>,
173 #[builder(default = BybitEnvironment::Mainnet)]
175 pub environment: BybitEnvironment,
176 pub base_url_http: Option<String>,
178 pub base_url_ws_private: Option<String>,
180 pub base_url_ws_trade: Option<String>,
182 pub proxy_url: Option<String>,
184 #[builder(default = 60)]
186 pub http_timeout_secs: u64,
187 #[builder(default = 3)]
189 pub max_retries: u32,
190 #[builder(default = 1_000)]
192 pub retry_delay_initial_ms: u64,
193 #[builder(default = 10_000)]
195 pub retry_delay_max_ms: u64,
196 #[builder(default = 5)]
198 pub heartbeat_interval_secs: u64,
199 #[builder(default = 5_000)]
201 pub recv_window_ms: u64,
202 pub account_id: Option<AccountId>,
204 #[builder(default)]
206 pub use_spot_position_reports: bool,
207 pub futures_leverages: Option<HashMap<String, u32>>,
209 pub position_mode: Option<HashMap<String, BybitPositionMode>>,
211 pub margin_mode: Option<BybitMarginMode>,
213 #[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 #[must_use]
227 pub fn new() -> Self {
228 Self::default()
229 }
230
231 #[must_use]
233 pub fn has_api_credentials(&self) -> bool {
234 self.api_key.is_some() && self.api_secret.is_some()
235 }
236
237 #[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 #[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 #[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}