1use async_trait::async_trait;
23use ccxt_core::{
24 Result,
25 exchange::{Capability, Exchange, ExchangeCapabilities},
26 traits::PublicExchange,
27 types::{
28 Amount, Balance, Market, Ohlcv, Order, OrderBook, OrderSide, OrderType, Price, Ticker,
29 Timeframe, Trade,
30 },
31};
32use rust_decimal::Decimal;
33use std::collections::HashMap;
34use std::sync::Arc;
35
36use super::Binance;
37
38#[cfg(test)]
40use ccxt_core::exchange::ExchangeExt;
41
42#[async_trait]
43impl Exchange for Binance {
44 fn id(&self) -> &'static str {
47 "binance"
48 }
49
50 fn name(&self) -> &'static str {
51 "Binance"
52 }
53
54 fn version(&self) -> &'static str {
55 "v3"
56 }
57
58 fn certified(&self) -> bool {
59 true
60 }
61
62 fn has_websocket(&self) -> bool {
63 true
64 }
65
66 fn capabilities(&self) -> ExchangeCapabilities {
67 ExchangeCapabilities::builder()
71 .all()
72 .without_capability(Capability::EditOrder)
73 .without_capability(Capability::FetchCanceledOrders)
74 .build()
75 }
76
77 fn timeframes(&self) -> Vec<Timeframe> {
78 vec![
79 Timeframe::M1,
80 Timeframe::M3,
81 Timeframe::M5,
82 Timeframe::M15,
83 Timeframe::M30,
84 Timeframe::H1,
85 Timeframe::H2,
86 Timeframe::H4,
87 Timeframe::H6,
88 Timeframe::H8,
89 Timeframe::H12,
90 Timeframe::D1,
91 Timeframe::D3,
92 Timeframe::W1,
93 Timeframe::Mon1,
94 ]
95 }
96
97 fn rate_limit(&self) -> u32 {
98 self.options.rate_limit
99 }
100
101 async fn fetch_markets(&self) -> Result<Vec<Market>> {
104 let arc_markets = Binance::fetch_markets(self).await?;
105 Ok(arc_markets.values().map(|v| (**v).clone()).collect())
106 }
107
108 async fn load_markets(&self, reload: bool) -> Result<Arc<HashMap<String, Arc<Market>>>> {
109 Binance::load_markets(self, reload).await
110 }
111
112 async fn fetch_ticker(&self, symbol: &str) -> Result<Ticker> {
113 Binance::fetch_ticker(self, symbol, ()).await
115 }
116
117 async fn fetch_tickers(&self, symbols: Option<&[String]>) -> Result<Vec<Ticker>> {
118 let symbols_vec = symbols.map(<[String]>::to_vec);
120 Binance::fetch_tickers(self, symbols_vec).await
121 }
122
123 async fn fetch_order_book(&self, symbol: &str, limit: Option<u32>) -> Result<OrderBook> {
124 Binance::fetch_order_book(self, symbol, limit).await
126 }
127
128 async fn fetch_trades(&self, symbol: &str, limit: Option<u32>) -> Result<Vec<Trade>> {
129 Binance::fetch_trades(self, symbol, limit).await
131 }
132
133 async fn fetch_ohlcv(
134 &self,
135 symbol: &str,
136 timeframe: Timeframe,
137 since: Option<i64>,
138 limit: Option<u32>,
139 ) -> Result<Vec<Ohlcv>> {
140 use ccxt_core::types::{Amount, Price};
141
142 let timeframe_str = timeframe.to_string();
144 #[allow(deprecated)]
146 let ohlcv_data =
147 Binance::fetch_ohlcv(self, symbol, &timeframe_str, since, limit, None).await?;
148
149 Ok(ohlcv_data
151 .into_iter()
152 .map(|o| Ohlcv {
153 timestamp: o.timestamp,
154 open: Price::from(Decimal::try_from(o.open).unwrap_or_default()),
155 high: Price::from(Decimal::try_from(o.high).unwrap_or_default()),
156 low: Price::from(Decimal::try_from(o.low).unwrap_or_default()),
157 close: Price::from(Decimal::try_from(o.close).unwrap_or_default()),
158 volume: Amount::from(Decimal::try_from(o.volume).unwrap_or_default()),
159 })
160 .collect())
161 }
162
163 async fn create_order(
166 &self,
167 symbol: &str,
168 order_type: OrderType,
169 side: OrderSide,
170 amount: Amount,
171 price: Option<Price>,
172 ) -> Result<Order> {
173 #[allow(deprecated)]
175 Binance::create_order(self, symbol, order_type, side, amount, price, None).await
176 }
177
178 async fn cancel_order(&self, id: &str, symbol: Option<&str>) -> Result<Order> {
179 let symbol_str = symbol.ok_or_else(|| {
182 ccxt_core::Error::invalid_request("Symbol is required for cancel_order on Binance")
183 })?;
184 Binance::cancel_order(self, id, symbol_str).await
185 }
186
187 async fn cancel_all_orders(&self, symbol: Option<&str>) -> Result<Vec<Order>> {
188 let symbol_str = symbol.ok_or_else(|| {
191 ccxt_core::Error::invalid_request("Symbol is required for cancel_all_orders on Binance")
192 })?;
193 Binance::cancel_all_orders(self, symbol_str).await
194 }
195
196 async fn fetch_order(&self, id: &str, symbol: Option<&str>) -> Result<Order> {
197 let symbol_str = symbol.ok_or_else(|| {
200 ccxt_core::Error::invalid_request("Symbol is required for fetch_order on Binance")
201 })?;
202 Binance::fetch_order(self, id, symbol_str).await
203 }
204
205 async fn fetch_open_orders(
206 &self,
207 symbol: Option<&str>,
208 _since: Option<i64>,
209 _limit: Option<u32>,
210 ) -> Result<Vec<Order>> {
211 Binance::fetch_open_orders(self, symbol).await
214 }
215
216 async fn fetch_closed_orders(
217 &self,
218 symbol: Option<&str>,
219 since: Option<i64>,
220 limit: Option<u32>,
221 ) -> Result<Vec<Order>> {
222 Binance::fetch_closed_orders(self, symbol, since, limit).await
225 }
226
227 async fn fetch_balance(&self) -> Result<Balance> {
230 Binance::fetch_balance(self, None).await
232 }
233
234 async fn fetch_my_trades(
235 &self,
236 symbol: Option<&str>,
237 since: Option<i64>,
238 limit: Option<u32>,
239 ) -> Result<Vec<Trade>> {
240 let symbol_str = symbol.ok_or_else(|| {
243 ccxt_core::Error::invalid_request("Symbol is required for fetch_my_trades on Binance")
244 })?;
245 Binance::fetch_my_trades(self, symbol_str, since, limit).await
247 }
248
249 async fn market(&self, symbol: &str) -> Result<Arc<Market>> {
252 let cache = self.base().market_cache.read().await;
254
255 if !cache.is_loaded() {
256 return Err(ccxt_core::Error::exchange(
257 "-1",
258 "Markets not loaded. Call load_markets() first.",
259 ));
260 }
261
262 cache
263 .get_market(symbol)
264 .ok_or_else(|| ccxt_core::Error::bad_symbol(format!("Market {} not found", symbol)))
265 }
266
267 async fn markets(&self) -> Arc<HashMap<String, Arc<Market>>> {
268 let cache = self.base().market_cache.read().await;
269 cache.markets()
270 }
271}
272
273#[async_trait]
276impl PublicExchange for Binance {
277 fn id(&self) -> &'static str {
278 "binance"
279 }
280
281 fn name(&self) -> &'static str {
282 "Binance"
283 }
284
285 fn version(&self) -> &'static str {
286 "v3"
287 }
288
289 fn certified(&self) -> bool {
290 true
291 }
292
293 fn capabilities(&self) -> ExchangeCapabilities {
294 ExchangeCapabilities::builder()
298 .all()
299 .without_capability(Capability::EditOrder)
300 .without_capability(Capability::FetchCanceledOrders)
301 .build()
302 }
303
304 fn timeframes(&self) -> Vec<Timeframe> {
305 vec![
306 Timeframe::M1,
307 Timeframe::M3,
308 Timeframe::M5,
309 Timeframe::M15,
310 Timeframe::M30,
311 Timeframe::H1,
312 Timeframe::H2,
313 Timeframe::H4,
314 Timeframe::H6,
315 Timeframe::H8,
316 Timeframe::H12,
317 Timeframe::D1,
318 Timeframe::D3,
319 Timeframe::W1,
320 Timeframe::Mon1,
321 ]
322 }
323
324 fn rate_limit(&self) -> u32 {
325 self.options.rate_limit
326 }
327
328 fn has_websocket(&self) -> bool {
329 true
330 }
331}
332
333impl Binance {
335 pub(crate) fn check_required_credentials(&self) -> ccxt_core::Result<()> {
337 if self.base().config.api_key.is_none() || self.base().config.secret.is_none() {
338 return Err(ccxt_core::Error::authentication(
339 "API key and secret are required",
340 ));
341 }
342 Ok(())
343 }
344
345 pub(crate) fn get_auth(&self) -> ccxt_core::Result<super::auth::BinanceAuth> {
347 let api_key = self
348 .base()
349 .config
350 .api_key
351 .as_ref()
352 .ok_or_else(|| ccxt_core::Error::authentication("API key is required"))?
353 .clone();
354
355 let secret = self
356 .base()
357 .config
358 .secret
359 .as_ref()
360 .ok_or_else(|| ccxt_core::Error::authentication("Secret is required"))?
361 .clone();
362
363 Ok(super::auth::BinanceAuth::new(
364 api_key.expose_secret(),
365 secret.expose_secret(),
366 ))
367 }
368
369 pub async fn get_signing_timestamp(&self) -> ccxt_core::Result<i64> {
410 if self.time_sync.needs_resync() {
412 if let Err(e) = self.sync_time().await {
414 if !self.time_sync.is_initialized() {
416 return Err(e);
417 }
418 tracing::warn!(
420 error = %e,
421 "Time sync failed, using cached offset"
422 );
423 }
424 }
425
426 if !self.time_sync.is_initialized() {
428 return self.fetch_time_raw().await;
429 }
430
431 Ok(self.time_sync.get_server_timestamp())
433 }
434
435 pub async fn sync_time(&self) -> ccxt_core::Result<()> {
475 let server_time = self.fetch_time_raw().await?;
476 self.time_sync.update_offset(server_time);
477 tracing::debug!(
478 server_time = server_time,
479 offset = self.time_sync.get_offset(),
480 "Time synchronized with Binance server"
481 );
482 Ok(())
483 }
484
485 pub fn is_timestamp_error(&self, error: &ccxt_core::Error) -> bool {
525 let error_str = error.to_string().to_lowercase();
526
527 let has_timestamp_keyword = error_str.contains("timestamp");
529 let has_recv_window = error_str.contains("recvwindow");
530 let has_ahead = error_str.contains("ahead");
531 let has_behind = error_str.contains("behind");
532
533 if has_timestamp_keyword && (has_recv_window || has_ahead || has_behind) {
535 return true;
536 }
537
538 if error_str.contains("-1021") {
542 return true;
543 }
544
545 if let ccxt_core::Error::Exchange(details) = error {
547 if details.code == "-1021" {
548 return true;
549 }
550 }
551
552 false
553 }
554
555 pub async fn execute_signed_request_with_retry<T, F, Fut>(
609 &self,
610 request_fn: F,
611 ) -> ccxt_core::Result<T>
612 where
613 F: Fn(i64) -> Fut,
614 Fut: std::future::Future<Output = ccxt_core::Result<T>>,
615 {
616 let timestamp = self.get_signing_timestamp().await?;
618
619 match request_fn(timestamp).await {
621 Ok(result) => Ok(result),
622 Err(e) if self.is_timestamp_error(&e) => {
623 tracing::warn!(
625 error = %e,
626 "Timestamp error detected, resyncing time and retrying request"
627 );
628
629 if let Err(sync_err) = self.sync_time().await {
631 tracing::error!(
632 error = %sync_err,
633 "Failed to resync time after timestamp error"
634 );
635 return Err(e);
637 }
638
639 let new_timestamp = self.time_sync.get_server_timestamp();
641
642 tracing::debug!(
643 old_timestamp = timestamp,
644 new_timestamp = new_timestamp,
645 offset = self.time_sync.get_offset(),
646 "Retrying request with fresh timestamp"
647 );
648
649 request_fn(new_timestamp).await
651 }
652 Err(e) => Err(e),
653 }
654 }
655
656 pub async fn handle_timestamp_error_and_resync(&self, error: &ccxt_core::Error) -> bool {
694 if self.is_timestamp_error(error) {
695 tracing::warn!(
696 error = %error,
697 "Timestamp error detected, triggering time resync"
698 );
699
700 if let Err(sync_err) = self.sync_time().await {
701 tracing::error!(
702 error = %sync_err,
703 "Failed to resync time after timestamp error"
704 );
705 return false;
706 }
707
708 tracing::debug!(
709 offset = self.time_sync.get_offset(),
710 "Time resync completed after timestamp error"
711 );
712
713 return true;
714 }
715
716 false
717 }
718}
719
720impl ccxt_core::signed_request::HasHttpClient for Binance {
722 fn http_client(&self) -> &ccxt_core::http_client::HttpClient {
723 &self.base().http_client
724 }
725
726 fn base_url(&self) -> &'static str {
727 ""
729 }
730}
731
732#[cfg(test)]
733mod tests {
734 #![allow(clippy::disallowed_methods)]
735 use super::*;
736 use ccxt_core::ExchangeConfig;
737
738 #[test]
739 fn test_binance_exchange_trait_metadata() {
740 let config = ExchangeConfig::default();
741 let binance = Binance::new(config).unwrap();
742
743 let exchange: &dyn Exchange = &binance;
745
746 assert_eq!(exchange.id(), "binance");
747 assert_eq!(exchange.name(), "Binance");
748 assert_eq!(exchange.version(), "v3");
749 assert!(exchange.certified());
750 assert!(exchange.has_websocket());
751 }
752
753 #[test]
754 fn test_binance_exchange_trait_capabilities() {
755 let config = ExchangeConfig::default();
756 let binance = Binance::new(config).unwrap();
757
758 let exchange: &dyn Exchange = &binance;
759 let caps = exchange.capabilities();
760
761 assert!(caps.fetch_markets());
762 assert!(caps.fetch_ticker());
763 assert!(caps.create_order());
764 assert!(caps.websocket());
765 assert!(!caps.edit_order()); }
767
768 #[test]
769 fn test_binance_exchange_trait_timeframes() {
770 let config = ExchangeConfig::default();
771 let binance = Binance::new(config).unwrap();
772
773 let exchange: &dyn Exchange = &binance;
774 let timeframes = exchange.timeframes();
775
776 assert!(!timeframes.is_empty());
777 assert!(timeframes.contains(&Timeframe::M1));
778 assert!(timeframes.contains(&Timeframe::H1));
779 assert!(timeframes.contains(&Timeframe::D1));
780 }
781
782 #[test]
783 fn test_binance_exchange_trait_object_safety() {
784 let config = ExchangeConfig::default();
785 let binance = Binance::new(config).unwrap();
786
787 let exchange: Box<dyn Exchange> = Box::new(binance);
789
790 assert_eq!(exchange.id(), "binance");
791 assert_eq!(exchange.rate_limit(), 50);
792 }
793
794 #[test]
795 fn test_binance_exchange_ext_trait() {
796 let config = ExchangeConfig::default();
797 let binance = Binance::new(config).unwrap();
798
799 let exchange: &dyn Exchange = &binance;
801
802 assert!(
804 exchange.supports_market_data(),
805 "Binance should support market data"
806 );
807 assert!(
808 exchange.supports_trading(),
809 "Binance should support trading"
810 );
811 assert!(
812 exchange.supports_account(),
813 "Binance should support account operations"
814 );
815 assert!(
816 exchange.supports_margin(),
817 "Binance should support margin operations"
818 );
819 assert!(
820 exchange.supports_funding(),
821 "Binance should support funding operations"
822 );
823 }
824
825 #[test]
826 fn test_binance_implements_both_exchange_and_public_exchange() {
827 let config = ExchangeConfig::default();
828 let binance = Binance::new(config).unwrap();
829
830 let exchange: &dyn Exchange = &binance;
832 let public_exchange: &dyn PublicExchange = &binance;
833
834 assert_eq!(exchange.id(), public_exchange.id());
836 assert_eq!(exchange.name(), public_exchange.name());
837 assert_eq!(exchange.version(), public_exchange.version());
838 assert_eq!(exchange.certified(), public_exchange.certified());
839 assert_eq!(exchange.rate_limit(), public_exchange.rate_limit());
840 assert_eq!(exchange.has_websocket(), public_exchange.has_websocket());
841 assert_eq!(exchange.timeframes(), public_exchange.timeframes());
842 }
843
844 #[test]
847 fn test_is_timestamp_error_with_recv_window_message() {
848 let config = ExchangeConfig::default();
849 let binance = Binance::new(config).unwrap();
850
851 let err = ccxt_core::Error::exchange(
853 "-1021",
854 "Timestamp for this request is outside of the recvWindow",
855 );
856 assert!(
857 binance.is_timestamp_error(&err),
858 "Should detect recvWindow timestamp error"
859 );
860 }
861
862 #[test]
863 fn test_is_timestamp_error_with_ahead_message() {
864 let config = ExchangeConfig::default();
865 let binance = Binance::new(config).unwrap();
866
867 let err = ccxt_core::Error::exchange("-1021", "Timestamp is ahead of server time");
869 assert!(
870 binance.is_timestamp_error(&err),
871 "Should detect 'ahead' timestamp error"
872 );
873 }
874
875 #[test]
876 fn test_is_timestamp_error_with_behind_message() {
877 let config = ExchangeConfig::default();
878 let binance = Binance::new(config).unwrap();
879
880 let err = ccxt_core::Error::exchange("-1021", "Timestamp is behind server time");
882 assert!(
883 binance.is_timestamp_error(&err),
884 "Should detect 'behind' timestamp error"
885 );
886 }
887
888 #[test]
889 fn test_is_timestamp_error_with_error_code_only() {
890 let config = ExchangeConfig::default();
891 let binance = Binance::new(config).unwrap();
892
893 let err = ccxt_core::Error::exchange("-1021", "Some error message");
895 assert!(
896 binance.is_timestamp_error(&err),
897 "Should detect error code -1021"
898 );
899 }
900
901 #[test]
902 fn test_is_timestamp_error_non_timestamp_error() {
903 let config = ExchangeConfig::default();
904 let binance = Binance::new(config).unwrap();
905
906 let err = ccxt_core::Error::exchange("-1100", "Illegal characters found in parameter");
908 assert!(
909 !binance.is_timestamp_error(&err),
910 "Should not detect non-timestamp error"
911 );
912
913 let err = ccxt_core::Error::authentication("Invalid API key");
915 assert!(
916 !binance.is_timestamp_error(&err),
917 "Should not detect authentication error as timestamp error"
918 );
919
920 let err = ccxt_core::Error::network("Connection refused");
922 assert!(
923 !binance.is_timestamp_error(&err),
924 "Should not detect network error as timestamp error"
925 );
926 }
927
928 #[test]
929 fn test_is_timestamp_error_case_insensitive() {
930 let config = ExchangeConfig::default();
931 let binance = Binance::new(config).unwrap();
932
933 let err = ccxt_core::Error::exchange(
935 "-1021",
936 "TIMESTAMP for this request is outside of the RECVWINDOW",
937 );
938 assert!(
939 binance.is_timestamp_error(&err),
940 "Should detect timestamp error case-insensitively"
941 );
942 }
943
944 #[test]
945 fn test_time_sync_manager_accessible() {
946 let config = ExchangeConfig::default();
947 let binance = Binance::new(config).unwrap();
948
949 let time_sync = binance.time_sync();
951 assert!(
952 !time_sync.is_initialized(),
953 "Time sync should not be initialized initially"
954 );
955 assert!(
956 time_sync.needs_resync(),
957 "Time sync should need resync initially"
958 );
959 }
960
961 #[test]
962 fn test_time_sync_manager_update_offset() {
963 let config = ExchangeConfig::default();
964 let binance = Binance::new(config).unwrap();
965
966 let server_time = ccxt_core::time::TimestampUtils::now_ms() + 100;
968 binance.time_sync().update_offset(server_time);
969
970 assert!(
971 binance.time_sync().is_initialized(),
972 "Time sync should be initialized after update"
973 );
974 assert!(
975 !binance.time_sync().needs_resync(),
976 "Time sync should not need resync immediately after update"
977 );
978
979 let offset = binance.time_sync().get_offset();
981 assert!(
982 offset >= 90 && offset <= 110,
983 "Offset should be approximately 100ms, got {}",
984 offset
985 );
986 }
987
988 #[tokio::test]
991 async fn test_execute_signed_request_with_retry_success() {
992 let config = ExchangeConfig::default();
993 let binance = Binance::new(config).unwrap();
994
995 let server_time = ccxt_core::time::TimestampUtils::now_ms();
997 binance.time_sync().update_offset(server_time);
998
999 let result = binance
1001 .execute_signed_request_with_retry(|timestamp| async move {
1002 assert!(timestamp > 0, "Timestamp should be positive");
1003 Ok::<_, ccxt_core::Error>(42)
1004 })
1005 .await;
1006
1007 assert!(result.is_ok(), "Request should succeed");
1008 assert_eq!(result.unwrap(), 42);
1009 }
1010
1011 #[tokio::test]
1012 async fn test_execute_signed_request_with_retry_non_timestamp_error() {
1013 let config = ExchangeConfig::default();
1014 let binance = Binance::new(config).unwrap();
1015
1016 let server_time = ccxt_core::time::TimestampUtils::now_ms();
1018 binance.time_sync().update_offset(server_time);
1019
1020 let result = binance
1022 .execute_signed_request_with_retry(|_timestamp| async move {
1023 Err::<i32, _>(ccxt_core::Error::exchange("-1100", "Invalid parameter"))
1024 })
1025 .await;
1026
1027 assert!(result.is_err(), "Request should fail");
1028 let err = result.unwrap_err();
1029 assert!(
1030 err.to_string().contains("-1100"),
1031 "Error should contain original error code"
1032 );
1033 }
1034
1035 #[test]
1036 fn test_handle_timestamp_error_detection() {
1037 let config = ExchangeConfig::default();
1038 let binance = Binance::new(config).unwrap();
1039
1040 let timestamp_errors = vec![
1042 ccxt_core::Error::exchange(
1043 "-1021",
1044 "Timestamp for this request is outside of the recvWindow",
1045 ),
1046 ccxt_core::Error::exchange("-1021", "Timestamp is ahead of server time"),
1047 ccxt_core::Error::exchange("-1021", "Timestamp is behind server time"),
1048 ccxt_core::Error::exchange("-1021", "Some error with timestamp and recvwindow"),
1049 ];
1050
1051 for err in timestamp_errors {
1052 assert!(
1053 binance.is_timestamp_error(&err),
1054 "Should detect timestamp error: {}",
1055 err
1056 );
1057 }
1058
1059 let non_timestamp_errors = vec![
1061 ccxt_core::Error::exchange("-1100", "Invalid parameter"),
1062 ccxt_core::Error::exchange("-1000", "Unknown error"),
1063 ccxt_core::Error::authentication("Invalid API key"),
1064 ccxt_core::Error::network("Connection refused"),
1065 ccxt_core::Error::timeout("Request timed out"),
1066 ];
1067
1068 for err in non_timestamp_errors {
1069 assert!(
1070 !binance.is_timestamp_error(&err),
1071 "Should not detect as timestamp error: {}",
1072 err
1073 );
1074 }
1075 }
1076
1077 mod property_tests {
1080 use super::*;
1081 use proptest::prelude::*;
1082
1083 fn arb_exchange_config() -> impl Strategy<Value = ExchangeConfig> {
1085 (
1086 prop::bool::ANY, prop::option::of(any::<u64>().prop_map(|n| format!("key_{}", n))), prop::option::of(any::<u64>().prop_map(|n| format!("secret_{}", n))), )
1090 .prop_map(|(sandbox, api_key, secret)| ExchangeConfig {
1091 sandbox,
1092 api_key: api_key.map(ccxt_core::SecretString::new),
1093 secret: secret.map(ccxt_core::SecretString::new),
1094 ..Default::default()
1095 })
1096 }
1097
1098 proptest! {
1099 #![proptest_config(ProptestConfig::with_cases(100))]
1100
1101 #[test]
1108 fn prop_timeframes_non_empty(config in arb_exchange_config()) {
1109 let binance = Binance::new(config).expect("Should create Binance instance");
1110 let exchange: &dyn Exchange = &binance;
1111
1112 let timeframes = exchange.timeframes();
1113
1114 prop_assert!(!timeframes.is_empty(), "Timeframes should not be empty");
1116
1117 let mut seen = std::collections::HashSet::new();
1119 for tf in &timeframes {
1120 prop_assert!(
1121 seen.insert(*tf),
1122 "Timeframes should not contain duplicates: {:?}",
1123 tf
1124 );
1125 }
1126
1127 prop_assert!(
1129 timeframes.contains(&Timeframe::M1),
1130 "Should contain 1-minute timeframe"
1131 );
1132 prop_assert!(
1133 timeframes.contains(&Timeframe::H1),
1134 "Should contain 1-hour timeframe"
1135 );
1136 prop_assert!(
1137 timeframes.contains(&Timeframe::D1),
1138 "Should contain 1-day timeframe"
1139 );
1140 }
1141
1142 #[test]
1149 fn prop_backward_compatibility_metadata(config in arb_exchange_config()) {
1150 let binance = Binance::new(config).expect("Should create Binance instance");
1151
1152 let exchange: &dyn Exchange = &binance;
1154
1155 prop_assert_eq!(
1157 exchange.id(),
1158 Binance::id(&binance),
1159 "id() should be consistent between trait and direct call"
1160 );
1161
1162 prop_assert_eq!(
1164 exchange.name(),
1165 Binance::name(&binance),
1166 "name() should be consistent between trait and direct call"
1167 );
1168
1169 prop_assert_eq!(
1171 exchange.version(),
1172 Binance::version(&binance),
1173 "version() should be consistent between trait and direct call"
1174 );
1175
1176 prop_assert_eq!(
1178 exchange.certified(),
1179 Binance::certified(&binance),
1180 "certified() should be consistent between trait and direct call"
1181 );
1182
1183 prop_assert_eq!(
1185 exchange.rate_limit(),
1186 Binance::rate_limit(&binance),
1187 "rate_limit() should be consistent between trait and direct call"
1188 );
1189
1190 let trait_caps = exchange.capabilities();
1192 prop_assert!(trait_caps.fetch_markets(), "Should support fetch_markets");
1193 prop_assert!(trait_caps.fetch_ticker(), "Should support fetch_ticker");
1194 prop_assert!(trait_caps.websocket(), "Should support websocket");
1195 }
1196 }
1197 }
1198}