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 endpoint_router;
14pub mod error;
15pub mod parser;
16pub mod rest;
17pub mod signed_request;
18pub mod symbol;
19pub mod ws;
20mod ws_exchange_impl;
21
22pub use auth::BybitAuth;
23pub use builder::BybitBuilder;
24pub use endpoint_router::BybitEndpointRouter;
25pub use error::{BybitErrorCode, is_error_response, parse_error};
26
27#[derive(Debug)]
29pub struct Bybit {
30 base: BaseExchange,
32 options: BybitOptions,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct BybitOptions {
52 pub account_type: String,
56 #[serde(default)]
65 pub default_type: DefaultType,
66 #[serde(default, skip_serializing_if = "Option::is_none")]
74 pub default_sub_type: Option<DefaultSubType>,
75 pub testnet: bool,
77 pub recv_window: u64,
79}
80
81impl Default for BybitOptions {
82 fn default() -> Self {
83 Self {
84 account_type: "UNIFIED".to_string(),
85 default_type: DefaultType::default(), default_sub_type: None,
87 testnet: false,
88 recv_window: 5000,
89 }
90 }
91}
92
93impl Bybit {
94 pub fn builder() -> BybitBuilder {
111 BybitBuilder::new()
112 }
113
114 pub fn new(config: ExchangeConfig) -> Result<Self> {
120 let base = BaseExchange::new(config)?;
121 let options = BybitOptions::default();
122
123 Ok(Self { base, options })
124 }
125
126 pub fn new_with_options(config: ExchangeConfig, options: BybitOptions) -> Result<Self> {
135 let base = BaseExchange::new(config)?;
136 Ok(Self { base, options })
137 }
138
139 pub fn base(&self) -> &BaseExchange {
141 &self.base
142 }
143
144 pub fn base_mut(&mut self) -> &mut BaseExchange {
146 &mut self.base
147 }
148
149 pub fn options(&self) -> &BybitOptions {
151 &self.options
152 }
153
154 pub fn set_options(&mut self, options: BybitOptions) {
156 self.options = options;
157 }
158
159 pub fn id(&self) -> &'static str {
161 "bybit"
162 }
163
164 pub fn name(&self) -> &'static str {
166 "Bybit"
167 }
168
169 pub fn version(&self) -> &'static str {
171 "v5"
172 }
173
174 pub fn certified(&self) -> bool {
176 false
177 }
178
179 pub fn pro(&self) -> bool {
181 true
182 }
183
184 pub fn rate_limit(&self) -> u32 {
186 20
187 }
188
189 pub fn is_sandbox(&self) -> bool {
213 self.base().config.sandbox || self.options.testnet
214 }
215
216 pub fn timeframes(&self) -> HashMap<String, String> {
218 let mut timeframes = HashMap::new();
219 timeframes.insert("1m".to_string(), "1".to_string());
220 timeframes.insert("3m".to_string(), "3".to_string());
221 timeframes.insert("5m".to_string(), "5".to_string());
222 timeframes.insert("15m".to_string(), "15".to_string());
223 timeframes.insert("30m".to_string(), "30".to_string());
224 timeframes.insert("1h".to_string(), "60".to_string());
225 timeframes.insert("2h".to_string(), "120".to_string());
226 timeframes.insert("4h".to_string(), "240".to_string());
227 timeframes.insert("6h".to_string(), "360".to_string());
228 timeframes.insert("12h".to_string(), "720".to_string());
229 timeframes.insert("1d".to_string(), "D".to_string());
230 timeframes.insert("1w".to_string(), "W".to_string());
231 timeframes.insert("1M".to_string(), "M".to_string());
232 timeframes
233 }
234
235 pub fn urls(&self) -> BybitUrls {
237 if self.base().config.sandbox || self.options.testnet {
238 BybitUrls::testnet()
239 } else {
240 BybitUrls::production()
241 }
242 }
243
244 pub fn default_type(&self) -> DefaultType {
246 self.options.default_type
247 }
248
249 pub fn default_sub_type(&self) -> Option<DefaultSubType> {
251 self.options.default_sub_type
252 }
253
254 pub fn is_contract_type(&self) -> bool {
262 self.options.default_type.is_contract()
263 }
264
265 pub fn is_inverse(&self) -> bool {
271 matches!(self.options.default_sub_type, Some(DefaultSubType::Inverse))
272 }
273
274 pub fn is_linear(&self) -> bool {
280 !self.is_inverse()
281 }
282
283 pub fn category(&self) -> &'static str {
298 match self.options.default_type {
299 DefaultType::Spot | DefaultType::Margin => "spot",
300 DefaultType::Swap | DefaultType::Futures => {
301 if self.is_inverse() {
302 "inverse"
303 } else {
304 "linear"
305 }
306 }
307 DefaultType::Option => "option",
308 }
309 }
310
311 pub fn create_ws(&self) -> ws::BybitWs {
336 let urls = self.urls();
337 let category = self.category();
338 let ws_url = urls.ws_public_for_category(category);
339 ws::BybitWs::new(ws_url)
340 }
341
342 pub fn signed_request(
393 &self,
394 endpoint: impl Into<String>,
395 ) -> signed_request::BybitSignedRequestBuilder<'_> {
396 signed_request::BybitSignedRequestBuilder::new(self, endpoint)
397 }
398}
399
400#[derive(Debug, Clone)]
402pub struct BybitUrls {
403 pub rest: String,
405 pub ws_public_base: String,
407 pub ws_public: String,
409 pub ws_private: String,
411}
412
413impl BybitUrls {
414 pub fn production() -> Self {
416 Self {
417 rest: "https://api.bybit.com".to_string(),
418 ws_public_base: "wss://stream.bybit.com/v5/public".to_string(),
419 ws_public: "wss://stream.bybit.com/v5/public/spot".to_string(),
420 ws_private: "wss://stream.bybit.com/v5/private".to_string(),
421 }
422 }
423
424 pub fn testnet() -> Self {
426 Self {
427 rest: "https://api-testnet.bybit.com".to_string(),
428 ws_public_base: "wss://stream-testnet.bybit.com/v5/public".to_string(),
429 ws_public: "wss://stream-testnet.bybit.com/v5/public/spot".to_string(),
430 ws_private: "wss://stream-testnet.bybit.com/v5/private".to_string(),
431 }
432 }
433
434 pub fn ws_public_for_category(&self, category: &str) -> String {
450 format!("{}/{}", self.ws_public_base, category)
451 }
452}
453
454#[cfg(test)]
455mod tests {
456 use super::*;
457
458 #[test]
459 fn test_bybit_creation() {
460 let config = ExchangeConfig {
461 id: "bybit".to_string(),
462 name: "Bybit".to_string(),
463 ..Default::default()
464 };
465
466 let bybit = Bybit::new(config);
467 assert!(bybit.is_ok());
468
469 let bybit = bybit.unwrap();
470 assert_eq!(bybit.id(), "bybit");
471 assert_eq!(bybit.name(), "Bybit");
472 assert_eq!(bybit.version(), "v5");
473 assert!(!bybit.certified());
474 assert!(bybit.pro());
475 }
476
477 #[test]
478 fn test_timeframes() {
479 let config = ExchangeConfig::default();
480 let bybit = Bybit::new(config).unwrap();
481 let timeframes = bybit.timeframes();
482
483 assert!(timeframes.contains_key("1m"));
484 assert!(timeframes.contains_key("1h"));
485 assert!(timeframes.contains_key("1d"));
486 assert_eq!(timeframes.len(), 13);
487 }
488
489 #[test]
490 fn test_urls() {
491 let config = ExchangeConfig::default();
492 let bybit = Bybit::new(config).unwrap();
493 let urls = bybit.urls();
494
495 assert!(urls.rest.contains("api.bybit.com"));
496 assert!(urls.ws_public.contains("stream.bybit.com"));
497 assert!(urls.ws_public_base.contains("stream.bybit.com"));
498 }
499
500 #[test]
501 fn test_testnet_urls() {
502 let config = ExchangeConfig {
503 sandbox: true,
504 ..Default::default()
505 };
506 let bybit = Bybit::new(config).unwrap();
507 let urls = bybit.urls();
508
509 assert!(urls.rest.contains("api-testnet.bybit.com"));
510 assert!(urls.ws_public.contains("stream-testnet.bybit.com"));
511 assert!(urls.ws_public_base.contains("stream-testnet.bybit.com"));
512 }
513
514 #[test]
515 fn test_is_sandbox_with_config_sandbox() {
516 let config = ExchangeConfig {
517 sandbox: true,
518 ..Default::default()
519 };
520 let bybit = Bybit::new(config).unwrap();
521 assert!(bybit.is_sandbox());
522 }
523
524 #[test]
525 fn test_is_sandbox_with_options_testnet() {
526 let config = ExchangeConfig::default();
527 let options = BybitOptions {
528 testnet: true,
529 ..Default::default()
530 };
531 let bybit = Bybit::new_with_options(config, options).unwrap();
532 assert!(bybit.is_sandbox());
533 }
534
535 #[test]
536 fn test_is_sandbox_false_by_default() {
537 let config = ExchangeConfig::default();
538 let bybit = Bybit::new(config).unwrap();
539 assert!(!bybit.is_sandbox());
540 }
541
542 #[test]
543 fn test_ws_public_for_category() {
544 let urls = BybitUrls::production();
545
546 assert_eq!(
547 urls.ws_public_for_category("spot"),
548 "wss://stream.bybit.com/v5/public/spot"
549 );
550 assert_eq!(
551 urls.ws_public_for_category("linear"),
552 "wss://stream.bybit.com/v5/public/linear"
553 );
554 assert_eq!(
555 urls.ws_public_for_category("inverse"),
556 "wss://stream.bybit.com/v5/public/inverse"
557 );
558 assert_eq!(
559 urls.ws_public_for_category("option"),
560 "wss://stream.bybit.com/v5/public/option"
561 );
562 }
563
564 #[test]
565 fn test_ws_public_for_category_testnet() {
566 let urls = BybitUrls::testnet();
567
568 assert_eq!(
569 urls.ws_public_for_category("spot"),
570 "wss://stream-testnet.bybit.com/v5/public/spot"
571 );
572 assert_eq!(
573 urls.ws_public_for_category("linear"),
574 "wss://stream-testnet.bybit.com/v5/public/linear"
575 );
576 }
577
578 #[test]
579 fn test_default_options() {
580 let options = BybitOptions::default();
581 assert_eq!(options.account_type, "UNIFIED");
582 assert_eq!(options.default_type, DefaultType::Spot);
583 assert_eq!(options.default_sub_type, None);
584 assert!(!options.testnet);
585 assert_eq!(options.recv_window, 5000);
586 }
587
588 #[test]
589 fn test_bybit_options_with_default_type() {
590 let options = BybitOptions {
591 default_type: DefaultType::Swap,
592 default_sub_type: Some(DefaultSubType::Linear),
593 ..Default::default()
594 };
595 assert_eq!(options.default_type, DefaultType::Swap);
596 assert_eq!(options.default_sub_type, Some(DefaultSubType::Linear));
597 }
598
599 #[test]
600 fn test_bybit_options_serialization() {
601 let options = BybitOptions {
602 default_type: DefaultType::Swap,
603 default_sub_type: Some(DefaultSubType::Linear),
604 ..Default::default()
605 };
606 let json = serde_json::to_string(&options).unwrap();
607 assert!(json.contains("\"default_type\":\"swap\""));
608 assert!(json.contains("\"default_sub_type\":\"linear\""));
609 }
610
611 #[test]
612 fn test_bybit_options_deserialization() {
613 let json = r#"{
614 "account_type": "CONTRACT",
615 "default_type": "swap",
616 "default_sub_type": "inverse",
617 "testnet": true,
618 "recv_window": 10000
619 }"#;
620 let options: BybitOptions = serde_json::from_str(json).unwrap();
621 assert_eq!(options.account_type, "CONTRACT");
622 assert_eq!(options.default_type, DefaultType::Swap);
623 assert_eq!(options.default_sub_type, Some(DefaultSubType::Inverse));
624 assert!(options.testnet);
625 assert_eq!(options.recv_window, 10000);
626 }
627
628 #[test]
629 fn test_bybit_options_deserialization_without_default_type() {
630 let json = r#"{
632 "account_type": "UNIFIED",
633 "testnet": false,
634 "recv_window": 5000
635 }"#;
636 let options: BybitOptions = serde_json::from_str(json).unwrap();
637 assert_eq!(options.default_type, DefaultType::Spot);
638 assert_eq!(options.default_sub_type, None);
639 }
640
641 #[test]
642 fn test_bybit_category_spot() {
643 let config = ExchangeConfig::default();
644 let options = BybitOptions {
645 default_type: DefaultType::Spot,
646 ..Default::default()
647 };
648 let bybit = Bybit::new_with_options(config, options).unwrap();
649 assert_eq!(bybit.category(), "spot");
650 }
651
652 #[test]
653 fn test_bybit_category_linear() {
654 let config = ExchangeConfig::default();
655 let options = BybitOptions {
656 default_type: DefaultType::Swap,
657 default_sub_type: Some(DefaultSubType::Linear),
658 ..Default::default()
659 };
660 let bybit = Bybit::new_with_options(config, options).unwrap();
661 assert_eq!(bybit.category(), "linear");
662 }
663
664 #[test]
665 fn test_bybit_category_inverse() {
666 let config = ExchangeConfig::default();
667 let options = BybitOptions {
668 default_type: DefaultType::Swap,
669 default_sub_type: Some(DefaultSubType::Inverse),
670 ..Default::default()
671 };
672 let bybit = Bybit::new_with_options(config, options).unwrap();
673 assert_eq!(bybit.category(), "inverse");
674 }
675
676 #[test]
677 fn test_bybit_category_option() {
678 let config = ExchangeConfig::default();
679 let options = BybitOptions {
680 default_type: DefaultType::Option,
681 ..Default::default()
682 };
683 let bybit = Bybit::new_with_options(config, options).unwrap();
684 assert_eq!(bybit.category(), "option");
685 }
686
687 #[test]
688 fn test_bybit_is_contract_type() {
689 let config = ExchangeConfig::default();
690
691 let options = BybitOptions {
693 default_type: DefaultType::Spot,
694 ..Default::default()
695 };
696 let bybit = Bybit::new_with_options(config.clone(), options).unwrap();
697 assert!(!bybit.is_contract_type());
698
699 let options = BybitOptions {
701 default_type: DefaultType::Swap,
702 ..Default::default()
703 };
704 let bybit = Bybit::new_with_options(config.clone(), options).unwrap();
705 assert!(bybit.is_contract_type());
706
707 let options = BybitOptions {
709 default_type: DefaultType::Futures,
710 ..Default::default()
711 };
712 let bybit = Bybit::new_with_options(config.clone(), options).unwrap();
713 assert!(bybit.is_contract_type());
714
715 let options = BybitOptions {
717 default_type: DefaultType::Option,
718 ..Default::default()
719 };
720 let bybit = Bybit::new_with_options(config, options).unwrap();
721 assert!(bybit.is_contract_type());
722 }
723
724 #[test]
729 fn test_sandbox_market_type_spot() {
730 let config = ExchangeConfig {
732 sandbox: true,
733 ..Default::default()
734 };
735 let options = BybitOptions {
736 default_type: DefaultType::Spot,
737 ..Default::default()
738 };
739 let bybit = Bybit::new_with_options(config, options).unwrap();
740
741 assert!(bybit.is_sandbox());
742 assert_eq!(bybit.category(), "spot");
743
744 let urls = bybit.urls();
745 assert!(
746 urls.rest.contains("api-testnet.bybit.com"),
747 "Spot sandbox REST URL should contain api-testnet.bybit.com, got: {}",
748 urls.rest
749 );
750
751 let ws_url = urls.ws_public_for_category("spot");
752 assert!(
753 ws_url.contains("stream-testnet.bybit.com"),
754 "Spot sandbox WS URL should contain stream-testnet.bybit.com, got: {}",
755 ws_url
756 );
757 assert!(
758 ws_url.contains("/v5/public/spot"),
759 "Spot sandbox WS URL should contain /v5/public/spot, got: {}",
760 ws_url
761 );
762 }
763
764 #[test]
765 fn test_sandbox_market_type_linear() {
766 let config = ExchangeConfig {
768 sandbox: true,
769 ..Default::default()
770 };
771 let options = BybitOptions {
772 default_type: DefaultType::Swap,
773 default_sub_type: Some(DefaultSubType::Linear),
774 ..Default::default()
775 };
776 let bybit = Bybit::new_with_options(config, options).unwrap();
777
778 assert!(bybit.is_sandbox());
779 assert_eq!(bybit.category(), "linear");
780
781 let urls = bybit.urls();
782 assert!(
783 urls.rest.contains("api-testnet.bybit.com"),
784 "Linear sandbox REST URL should contain api-testnet.bybit.com, got: {}",
785 urls.rest
786 );
787
788 let ws_url = urls.ws_public_for_category("linear");
789 assert!(
790 ws_url.contains("stream-testnet.bybit.com"),
791 "Linear sandbox WS URL should contain stream-testnet.bybit.com, got: {}",
792 ws_url
793 );
794 assert!(
795 ws_url.contains("/v5/public/linear"),
796 "Linear sandbox WS URL should contain /v5/public/linear, got: {}",
797 ws_url
798 );
799 }
800
801 #[test]
802 fn test_sandbox_market_type_inverse() {
803 let config = ExchangeConfig {
805 sandbox: true,
806 ..Default::default()
807 };
808 let options = BybitOptions {
809 default_type: DefaultType::Swap,
810 default_sub_type: Some(DefaultSubType::Inverse),
811 ..Default::default()
812 };
813 let bybit = Bybit::new_with_options(config, options).unwrap();
814
815 assert!(bybit.is_sandbox());
816 assert_eq!(bybit.category(), "inverse");
817
818 let urls = bybit.urls();
819 assert!(
820 urls.rest.contains("api-testnet.bybit.com"),
821 "Inverse sandbox REST URL should contain api-testnet.bybit.com, got: {}",
822 urls.rest
823 );
824
825 let ws_url = urls.ws_public_for_category("inverse");
826 assert!(
827 ws_url.contains("stream-testnet.bybit.com"),
828 "Inverse sandbox WS URL should contain stream-testnet.bybit.com, got: {}",
829 ws_url
830 );
831 assert!(
832 ws_url.contains("/v5/public/inverse"),
833 "Inverse sandbox WS URL should contain /v5/public/inverse, got: {}",
834 ws_url
835 );
836 }
837
838 #[test]
839 fn test_sandbox_market_type_option() {
840 let config = ExchangeConfig {
842 sandbox: true,
843 ..Default::default()
844 };
845 let options = BybitOptions {
846 default_type: DefaultType::Option,
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(), "option");
853
854 let urls = bybit.urls();
855 assert!(
856 urls.rest.contains("api-testnet.bybit.com"),
857 "Option sandbox REST URL should contain api-testnet.bybit.com, got: {}",
858 urls.rest
859 );
860
861 let ws_url = urls.ws_public_for_category("option");
862 assert!(
863 ws_url.contains("stream-testnet.bybit.com"),
864 "Option sandbox WS URL should contain stream-testnet.bybit.com, got: {}",
865 ws_url
866 );
867 assert!(
868 ws_url.contains("/v5/public/option"),
869 "Option sandbox WS URL should contain /v5/public/option, got: {}",
870 ws_url
871 );
872 }
873
874 #[test]
875 fn test_sandbox_private_websocket_url() {
876 let config = ExchangeConfig {
878 sandbox: true,
879 ..Default::default()
880 };
881 let bybit = Bybit::new(config).unwrap();
882
883 assert!(bybit.is_sandbox());
884 let urls = bybit.urls();
885 assert!(
886 urls.ws_private.contains("stream-testnet.bybit.com"),
887 "Private WS sandbox URL should contain stream-testnet.bybit.com, got: {}",
888 urls.ws_private
889 );
890 assert!(
891 urls.ws_private.contains("/v5/private"),
892 "Private WS sandbox URL should contain /v5/private, got: {}",
893 urls.ws_private
894 );
895 }
896
897 #[test]
898 fn test_sandbox_futures_linear() {
899 let config = ExchangeConfig {
901 sandbox: true,
902 ..Default::default()
903 };
904 let options = BybitOptions {
905 default_type: DefaultType::Futures,
906 default_sub_type: Some(DefaultSubType::Linear),
907 ..Default::default()
908 };
909 let bybit = Bybit::new_with_options(config, options).unwrap();
910
911 assert!(bybit.is_sandbox());
912 assert_eq!(bybit.category(), "linear");
913
914 let urls = bybit.urls();
915 assert!(
916 urls.rest.contains("api-testnet.bybit.com"),
917 "Futures Linear sandbox REST URL should contain api-testnet.bybit.com, got: {}",
918 urls.rest
919 );
920 }
921
922 #[test]
923 fn test_sandbox_futures_inverse() {
924 let config = ExchangeConfig {
925 sandbox: true,
926 ..Default::default()
927 };
928 let options = BybitOptions {
929 default_type: DefaultType::Futures,
930 default_sub_type: Some(DefaultSubType::Inverse),
931 ..Default::default()
932 };
933 let bybit = Bybit::new_with_options(config, options).unwrap();
934
935 assert!(bybit.is_sandbox());
936 assert_eq!(bybit.category(), "inverse");
937
938 let urls = bybit.urls();
939 assert!(
940 urls.rest.contains("api-testnet.bybit.com"),
941 "Futures Inverse sandbox REST URL should contain api-testnet.bybit.com, got: {}",
942 urls.rest
943 );
944 }
945}