1use ccxt_core::types::default_type::{DefaultSubType, DefaultType};
7use ccxt_core::{BaseExchange, ExchangeConfig, Result};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11pub mod auth;
12pub mod builder;
13pub mod error;
14pub mod parser;
15pub mod rest;
16pub mod symbol;
17pub mod ws;
18mod ws_exchange_impl;
19
20pub use auth::BybitAuth;
21pub use builder::BybitBuilder;
22pub use error::{BybitErrorCode, is_error_response, parse_error};
23
24#[derive(Debug)]
26pub struct Bybit {
27 base: BaseExchange,
29 options: BybitOptions,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct BybitOptions {
49 pub account_type: String,
53 #[serde(default)]
62 pub default_type: DefaultType,
63 #[serde(default, skip_serializing_if = "Option::is_none")]
71 pub default_sub_type: Option<DefaultSubType>,
72 pub testnet: bool,
74 pub recv_window: u64,
76}
77
78impl Default for BybitOptions {
79 fn default() -> Self {
80 Self {
81 account_type: "UNIFIED".to_string(),
82 default_type: DefaultType::default(), default_sub_type: None,
84 testnet: false,
85 recv_window: 5000,
86 }
87 }
88}
89
90impl Bybit {
91 pub fn builder() -> BybitBuilder {
108 BybitBuilder::new()
109 }
110
111 pub fn new(config: ExchangeConfig) -> Result<Self> {
117 let base = BaseExchange::new(config)?;
118 let options = BybitOptions::default();
119
120 Ok(Self { base, options })
121 }
122
123 pub fn new_with_options(config: ExchangeConfig, options: BybitOptions) -> Result<Self> {
132 let base = BaseExchange::new(config)?;
133 Ok(Self { base, options })
134 }
135
136 pub fn base(&self) -> &BaseExchange {
138 &self.base
139 }
140
141 pub fn base_mut(&mut self) -> &mut BaseExchange {
143 &mut self.base
144 }
145
146 pub fn options(&self) -> &BybitOptions {
148 &self.options
149 }
150
151 pub fn set_options(&mut self, options: BybitOptions) {
153 self.options = options;
154 }
155
156 pub fn id(&self) -> &str {
158 "bybit"
159 }
160
161 pub fn name(&self) -> &str {
163 "Bybit"
164 }
165
166 pub fn version(&self) -> &str {
168 "v5"
169 }
170
171 pub fn certified(&self) -> bool {
173 false
174 }
175
176 pub fn pro(&self) -> bool {
178 true
179 }
180
181 pub fn rate_limit(&self) -> u32 {
183 20
184 }
185
186 pub fn is_sandbox(&self) -> bool {
210 self.base().config.sandbox || self.options.testnet
211 }
212
213 pub fn timeframes(&self) -> HashMap<String, String> {
215 let mut timeframes = HashMap::new();
216 timeframes.insert("1m".to_string(), "1".to_string());
217 timeframes.insert("3m".to_string(), "3".to_string());
218 timeframes.insert("5m".to_string(), "5".to_string());
219 timeframes.insert("15m".to_string(), "15".to_string());
220 timeframes.insert("30m".to_string(), "30".to_string());
221 timeframes.insert("1h".to_string(), "60".to_string());
222 timeframes.insert("2h".to_string(), "120".to_string());
223 timeframes.insert("4h".to_string(), "240".to_string());
224 timeframes.insert("6h".to_string(), "360".to_string());
225 timeframes.insert("12h".to_string(), "720".to_string());
226 timeframes.insert("1d".to_string(), "D".to_string());
227 timeframes.insert("1w".to_string(), "W".to_string());
228 timeframes.insert("1M".to_string(), "M".to_string());
229 timeframes
230 }
231
232 pub fn urls(&self) -> BybitUrls {
234 if self.base().config.sandbox || self.options.testnet {
235 BybitUrls::testnet()
236 } else {
237 BybitUrls::production()
238 }
239 }
240
241 pub fn default_type(&self) -> DefaultType {
243 self.options.default_type
244 }
245
246 pub fn default_sub_type(&self) -> Option<DefaultSubType> {
248 self.options.default_sub_type
249 }
250
251 pub fn is_contract_type(&self) -> bool {
259 self.options.default_type.is_contract()
260 }
261
262 pub fn is_inverse(&self) -> bool {
268 matches!(self.options.default_sub_type, Some(DefaultSubType::Inverse))
269 }
270
271 pub fn is_linear(&self) -> bool {
277 !self.is_inverse()
278 }
279
280 pub fn category(&self) -> &'static str {
295 match self.options.default_type {
296 DefaultType::Spot | DefaultType::Margin => "spot",
297 DefaultType::Swap | DefaultType::Futures => {
298 if self.is_inverse() {
299 "inverse"
300 } else {
301 "linear"
302 }
303 }
304 DefaultType::Option => "option",
305 }
306 }
307
308 pub fn create_ws(&self) -> ws::BybitWs {
333 let urls = self.urls();
334 let category = self.category();
335 let ws_url = urls.ws_public_for_category(category);
336 ws::BybitWs::new(ws_url)
337 }
338}
339
340#[derive(Debug, Clone)]
342pub struct BybitUrls {
343 pub rest: String,
345 pub ws_public_base: String,
347 pub ws_public: String,
349 pub ws_private: String,
351}
352
353impl BybitUrls {
354 pub fn production() -> Self {
356 Self {
357 rest: "https://api.bybit.com".to_string(),
358 ws_public_base: "wss://stream.bybit.com/v5/public".to_string(),
359 ws_public: "wss://stream.bybit.com/v5/public/spot".to_string(),
360 ws_private: "wss://stream.bybit.com/v5/private".to_string(),
361 }
362 }
363
364 pub fn testnet() -> Self {
366 Self {
367 rest: "https://api-testnet.bybit.com".to_string(),
368 ws_public_base: "wss://stream-testnet.bybit.com/v5/public".to_string(),
369 ws_public: "wss://stream-testnet.bybit.com/v5/public/spot".to_string(),
370 ws_private: "wss://stream-testnet.bybit.com/v5/private".to_string(),
371 }
372 }
373
374 pub fn ws_public_for_category(&self, category: &str) -> String {
390 format!("{}/{}", self.ws_public_base, category)
391 }
392}
393
394#[cfg(test)]
395mod tests {
396 use super::*;
397
398 #[test]
399 fn test_bybit_creation() {
400 let config = ExchangeConfig {
401 id: "bybit".to_string(),
402 name: "Bybit".to_string(),
403 ..Default::default()
404 };
405
406 let bybit = Bybit::new(config);
407 assert!(bybit.is_ok());
408
409 let bybit = bybit.unwrap();
410 assert_eq!(bybit.id(), "bybit");
411 assert_eq!(bybit.name(), "Bybit");
412 assert_eq!(bybit.version(), "v5");
413 assert!(!bybit.certified());
414 assert!(bybit.pro());
415 }
416
417 #[test]
418 fn test_timeframes() {
419 let config = ExchangeConfig::default();
420 let bybit = Bybit::new(config).unwrap();
421 let timeframes = bybit.timeframes();
422
423 assert!(timeframes.contains_key("1m"));
424 assert!(timeframes.contains_key("1h"));
425 assert!(timeframes.contains_key("1d"));
426 assert_eq!(timeframes.len(), 13);
427 }
428
429 #[test]
430 fn test_urls() {
431 let config = ExchangeConfig::default();
432 let bybit = Bybit::new(config).unwrap();
433 let urls = bybit.urls();
434
435 assert!(urls.rest.contains("api.bybit.com"));
436 assert!(urls.ws_public.contains("stream.bybit.com"));
437 assert!(urls.ws_public_base.contains("stream.bybit.com"));
438 }
439
440 #[test]
441 fn test_testnet_urls() {
442 let config = ExchangeConfig {
443 sandbox: true,
444 ..Default::default()
445 };
446 let bybit = Bybit::new(config).unwrap();
447 let urls = bybit.urls();
448
449 assert!(urls.rest.contains("api-testnet.bybit.com"));
450 assert!(urls.ws_public.contains("stream-testnet.bybit.com"));
451 assert!(urls.ws_public_base.contains("stream-testnet.bybit.com"));
452 }
453
454 #[test]
455 fn test_is_sandbox_with_config_sandbox() {
456 let config = ExchangeConfig {
457 sandbox: true,
458 ..Default::default()
459 };
460 let bybit = Bybit::new(config).unwrap();
461 assert!(bybit.is_sandbox());
462 }
463
464 #[test]
465 fn test_is_sandbox_with_options_testnet() {
466 let config = ExchangeConfig::default();
467 let options = BybitOptions {
468 testnet: true,
469 ..Default::default()
470 };
471 let bybit = Bybit::new_with_options(config, options).unwrap();
472 assert!(bybit.is_sandbox());
473 }
474
475 #[test]
476 fn test_is_sandbox_false_by_default() {
477 let config = ExchangeConfig::default();
478 let bybit = Bybit::new(config).unwrap();
479 assert!(!bybit.is_sandbox());
480 }
481
482 #[test]
483 fn test_ws_public_for_category() {
484 let urls = BybitUrls::production();
485
486 assert_eq!(
487 urls.ws_public_for_category("spot"),
488 "wss://stream.bybit.com/v5/public/spot"
489 );
490 assert_eq!(
491 urls.ws_public_for_category("linear"),
492 "wss://stream.bybit.com/v5/public/linear"
493 );
494 assert_eq!(
495 urls.ws_public_for_category("inverse"),
496 "wss://stream.bybit.com/v5/public/inverse"
497 );
498 assert_eq!(
499 urls.ws_public_for_category("option"),
500 "wss://stream.bybit.com/v5/public/option"
501 );
502 }
503
504 #[test]
505 fn test_ws_public_for_category_testnet() {
506 let urls = BybitUrls::testnet();
507
508 assert_eq!(
509 urls.ws_public_for_category("spot"),
510 "wss://stream-testnet.bybit.com/v5/public/spot"
511 );
512 assert_eq!(
513 urls.ws_public_for_category("linear"),
514 "wss://stream-testnet.bybit.com/v5/public/linear"
515 );
516 }
517
518 #[test]
519 fn test_default_options() {
520 let options = BybitOptions::default();
521 assert_eq!(options.account_type, "UNIFIED");
522 assert_eq!(options.default_type, DefaultType::Spot);
523 assert_eq!(options.default_sub_type, None);
524 assert!(!options.testnet);
525 assert_eq!(options.recv_window, 5000);
526 }
527
528 #[test]
529 fn test_bybit_options_with_default_type() {
530 let options = BybitOptions {
531 default_type: DefaultType::Swap,
532 default_sub_type: Some(DefaultSubType::Linear),
533 ..Default::default()
534 };
535 assert_eq!(options.default_type, DefaultType::Swap);
536 assert_eq!(options.default_sub_type, Some(DefaultSubType::Linear));
537 }
538
539 #[test]
540 fn test_bybit_options_serialization() {
541 let options = BybitOptions {
542 default_type: DefaultType::Swap,
543 default_sub_type: Some(DefaultSubType::Linear),
544 ..Default::default()
545 };
546 let json = serde_json::to_string(&options).unwrap();
547 assert!(json.contains("\"default_type\":\"swap\""));
548 assert!(json.contains("\"default_sub_type\":\"linear\""));
549 }
550
551 #[test]
552 fn test_bybit_options_deserialization() {
553 let json = r#"{
554 "account_type": "CONTRACT",
555 "default_type": "swap",
556 "default_sub_type": "inverse",
557 "testnet": true,
558 "recv_window": 10000
559 }"#;
560 let options: BybitOptions = serde_json::from_str(json).unwrap();
561 assert_eq!(options.account_type, "CONTRACT");
562 assert_eq!(options.default_type, DefaultType::Swap);
563 assert_eq!(options.default_sub_type, Some(DefaultSubType::Inverse));
564 assert!(options.testnet);
565 assert_eq!(options.recv_window, 10000);
566 }
567
568 #[test]
569 fn test_bybit_options_deserialization_without_default_type() {
570 let json = r#"{
572 "account_type": "UNIFIED",
573 "testnet": false,
574 "recv_window": 5000
575 }"#;
576 let options: BybitOptions = serde_json::from_str(json).unwrap();
577 assert_eq!(options.default_type, DefaultType::Spot);
578 assert_eq!(options.default_sub_type, None);
579 }
580
581 #[test]
582 fn test_bybit_category_spot() {
583 let config = ExchangeConfig::default();
584 let options = BybitOptions {
585 default_type: DefaultType::Spot,
586 ..Default::default()
587 };
588 let bybit = Bybit::new_with_options(config, options).unwrap();
589 assert_eq!(bybit.category(), "spot");
590 }
591
592 #[test]
593 fn test_bybit_category_linear() {
594 let config = ExchangeConfig::default();
595 let options = BybitOptions {
596 default_type: DefaultType::Swap,
597 default_sub_type: Some(DefaultSubType::Linear),
598 ..Default::default()
599 };
600 let bybit = Bybit::new_with_options(config, options).unwrap();
601 assert_eq!(bybit.category(), "linear");
602 }
603
604 #[test]
605 fn test_bybit_category_inverse() {
606 let config = ExchangeConfig::default();
607 let options = BybitOptions {
608 default_type: DefaultType::Swap,
609 default_sub_type: Some(DefaultSubType::Inverse),
610 ..Default::default()
611 };
612 let bybit = Bybit::new_with_options(config, options).unwrap();
613 assert_eq!(bybit.category(), "inverse");
614 }
615
616 #[test]
617 fn test_bybit_category_option() {
618 let config = ExchangeConfig::default();
619 let options = BybitOptions {
620 default_type: DefaultType::Option,
621 ..Default::default()
622 };
623 let bybit = Bybit::new_with_options(config, options).unwrap();
624 assert_eq!(bybit.category(), "option");
625 }
626
627 #[test]
628 fn test_bybit_is_contract_type() {
629 let config = ExchangeConfig::default();
630
631 let options = BybitOptions {
633 default_type: DefaultType::Spot,
634 ..Default::default()
635 };
636 let bybit = Bybit::new_with_options(config.clone(), options).unwrap();
637 assert!(!bybit.is_contract_type());
638
639 let options = BybitOptions {
641 default_type: DefaultType::Swap,
642 ..Default::default()
643 };
644 let bybit = Bybit::new_with_options(config.clone(), options).unwrap();
645 assert!(bybit.is_contract_type());
646
647 let options = BybitOptions {
649 default_type: DefaultType::Futures,
650 ..Default::default()
651 };
652 let bybit = Bybit::new_with_options(config.clone(), options).unwrap();
653 assert!(bybit.is_contract_type());
654
655 let options = BybitOptions {
657 default_type: DefaultType::Option,
658 ..Default::default()
659 };
660 let bybit = Bybit::new_with_options(config, options).unwrap();
661 assert!(bybit.is_contract_type());
662 }
663
664 #[test]
669 fn test_sandbox_market_type_spot() {
670 let config = ExchangeConfig {
672 sandbox: true,
673 ..Default::default()
674 };
675 let options = BybitOptions {
676 default_type: DefaultType::Spot,
677 ..Default::default()
678 };
679 let bybit = Bybit::new_with_options(config, options).unwrap();
680
681 assert!(bybit.is_sandbox());
682 assert_eq!(bybit.category(), "spot");
683
684 let urls = bybit.urls();
685 assert!(
686 urls.rest.contains("api-testnet.bybit.com"),
687 "Spot sandbox REST URL should contain api-testnet.bybit.com, got: {}",
688 urls.rest
689 );
690
691 let ws_url = urls.ws_public_for_category("spot");
692 assert!(
693 ws_url.contains("stream-testnet.bybit.com"),
694 "Spot sandbox WS URL should contain stream-testnet.bybit.com, got: {}",
695 ws_url
696 );
697 assert!(
698 ws_url.contains("/v5/public/spot"),
699 "Spot sandbox WS URL should contain /v5/public/spot, got: {}",
700 ws_url
701 );
702 }
703
704 #[test]
705 fn test_sandbox_market_type_linear() {
706 let config = ExchangeConfig {
708 sandbox: true,
709 ..Default::default()
710 };
711 let options = BybitOptions {
712 default_type: DefaultType::Swap,
713 default_sub_type: Some(DefaultSubType::Linear),
714 ..Default::default()
715 };
716 let bybit = Bybit::new_with_options(config, options).unwrap();
717
718 assert!(bybit.is_sandbox());
719 assert_eq!(bybit.category(), "linear");
720
721 let urls = bybit.urls();
722 assert!(
723 urls.rest.contains("api-testnet.bybit.com"),
724 "Linear sandbox REST URL should contain api-testnet.bybit.com, got: {}",
725 urls.rest
726 );
727
728 let ws_url = urls.ws_public_for_category("linear");
729 assert!(
730 ws_url.contains("stream-testnet.bybit.com"),
731 "Linear sandbox WS URL should contain stream-testnet.bybit.com, got: {}",
732 ws_url
733 );
734 assert!(
735 ws_url.contains("/v5/public/linear"),
736 "Linear sandbox WS URL should contain /v5/public/linear, got: {}",
737 ws_url
738 );
739 }
740
741 #[test]
742 fn test_sandbox_market_type_inverse() {
743 let config = ExchangeConfig {
745 sandbox: true,
746 ..Default::default()
747 };
748 let options = BybitOptions {
749 default_type: DefaultType::Swap,
750 default_sub_type: Some(DefaultSubType::Inverse),
751 ..Default::default()
752 };
753 let bybit = Bybit::new_with_options(config, options).unwrap();
754
755 assert!(bybit.is_sandbox());
756 assert_eq!(bybit.category(), "inverse");
757
758 let urls = bybit.urls();
759 assert!(
760 urls.rest.contains("api-testnet.bybit.com"),
761 "Inverse sandbox REST URL should contain api-testnet.bybit.com, got: {}",
762 urls.rest
763 );
764
765 let ws_url = urls.ws_public_for_category("inverse");
766 assert!(
767 ws_url.contains("stream-testnet.bybit.com"),
768 "Inverse sandbox WS URL should contain stream-testnet.bybit.com, got: {}",
769 ws_url
770 );
771 assert!(
772 ws_url.contains("/v5/public/inverse"),
773 "Inverse sandbox WS URL should contain /v5/public/inverse, got: {}",
774 ws_url
775 );
776 }
777
778 #[test]
779 fn test_sandbox_market_type_option() {
780 let config = ExchangeConfig {
782 sandbox: true,
783 ..Default::default()
784 };
785 let options = BybitOptions {
786 default_type: DefaultType::Option,
787 ..Default::default()
788 };
789 let bybit = Bybit::new_with_options(config, options).unwrap();
790
791 assert!(bybit.is_sandbox());
792 assert_eq!(bybit.category(), "option");
793
794 let urls = bybit.urls();
795 assert!(
796 urls.rest.contains("api-testnet.bybit.com"),
797 "Option sandbox REST URL should contain api-testnet.bybit.com, got: {}",
798 urls.rest
799 );
800
801 let ws_url = urls.ws_public_for_category("option");
802 assert!(
803 ws_url.contains("stream-testnet.bybit.com"),
804 "Option sandbox WS URL should contain stream-testnet.bybit.com, got: {}",
805 ws_url
806 );
807 assert!(
808 ws_url.contains("/v5/public/option"),
809 "Option sandbox WS URL should contain /v5/public/option, got: {}",
810 ws_url
811 );
812 }
813
814 #[test]
815 fn test_sandbox_private_websocket_url() {
816 let config = ExchangeConfig {
818 sandbox: true,
819 ..Default::default()
820 };
821 let bybit = Bybit::new(config).unwrap();
822
823 assert!(bybit.is_sandbox());
824 let urls = bybit.urls();
825 assert!(
826 urls.ws_private.contains("stream-testnet.bybit.com"),
827 "Private WS sandbox URL should contain stream-testnet.bybit.com, got: {}",
828 urls.ws_private
829 );
830 assert!(
831 urls.ws_private.contains("/v5/private"),
832 "Private WS sandbox URL should contain /v5/private, got: {}",
833 urls.ws_private
834 );
835 }
836
837 #[test]
838 fn test_sandbox_futures_linear() {
839 let config = ExchangeConfig {
841 sandbox: true,
842 ..Default::default()
843 };
844 let options = BybitOptions {
845 default_type: DefaultType::Futures,
846 default_sub_type: Some(DefaultSubType::Linear),
847 ..Default::default()
848 };
849 let bybit = Bybit::new_with_options(config, options).unwrap();
850
851 assert!(bybit.is_sandbox());
852 assert_eq!(bybit.category(), "linear");
853
854 let urls = bybit.urls();
855 assert!(
856 urls.rest.contains("api-testnet.bybit.com"),
857 "Futures Linear sandbox REST URL should contain api-testnet.bybit.com, got: {}",
858 urls.rest
859 );
860 }
861
862 #[test]
863 fn test_sandbox_futures_inverse() {
864 let config = ExchangeConfig {
865 sandbox: true,
866 ..Default::default()
867 };
868 let options = BybitOptions {
869 default_type: DefaultType::Futures,
870 default_sub_type: Some(DefaultSubType::Inverse),
871 ..Default::default()
872 };
873 let bybit = Bybit::new_with_options(config, options).unwrap();
874
875 assert!(bybit.is_sandbox());
876 assert_eq!(bybit.category(), "inverse");
877
878 let urls = bybit.urls();
879 assert!(
880 urls.rest.contains("api-testnet.bybit.com"),
881 "Futures Inverse sandbox REST URL should contain api-testnet.bybit.com, got: {}",
882 urls.rest
883 );
884 }
885}