1use async_trait::async_trait;
23use ccxt_core::{
24 Result,
25 exchange::{Capability, Exchange, ExchangeCapabilities},
26 traits::PublicExchange,
27 types::{
28 Balance, Market, Ohlcv, Order, OrderBook, OrderSide, OrderType, Ticker, Timeframe, Trade,
29 },
30};
31use rust_decimal::Decimal;
32use rust_decimal::prelude::ToPrimitive;
33use std::collections::HashMap;
34
35use super::Binance;
36
37#[cfg(test)]
39use ccxt_core::exchange::ExchangeExt;
40
41#[async_trait]
42impl Exchange for Binance {
43 fn id(&self) -> &str {
46 "binance"
47 }
48
49 fn name(&self) -> &str {
50 "Binance"
51 }
52
53 fn version(&self) -> &'static str {
54 "v3"
55 }
56
57 fn certified(&self) -> bool {
58 true
59 }
60
61 fn has_websocket(&self) -> bool {
62 true
63 }
64
65 fn capabilities(&self) -> ExchangeCapabilities {
66 ExchangeCapabilities::builder()
70 .all()
71 .without_capability(Capability::EditOrder)
72 .without_capability(Capability::FetchCanceledOrders)
73 .build()
74 }
75
76 fn timeframes(&self) -> Vec<Timeframe> {
77 vec![
78 Timeframe::M1,
79 Timeframe::M3,
80 Timeframe::M5,
81 Timeframe::M15,
82 Timeframe::M30,
83 Timeframe::H1,
84 Timeframe::H2,
85 Timeframe::H4,
86 Timeframe::H6,
87 Timeframe::H8,
88 Timeframe::H12,
89 Timeframe::D1,
90 Timeframe::D3,
91 Timeframe::W1,
92 Timeframe::Mon1,
93 ]
94 }
95
96 fn rate_limit(&self) -> u32 {
97 50
98 }
99
100 async fn fetch_markets(&self) -> Result<Vec<Market>> {
103 let arc_markets = Binance::fetch_markets(self).await?;
104 Ok(arc_markets.into_values().map(|v| (*v).clone()).collect())
105 }
106
107 async fn load_markets(&self, reload: bool) -> Result<HashMap<String, Market>> {
108 let arc_markets = Binance::load_markets(self, reload).await?;
109 Ok(arc_markets
110 .into_iter()
111 .map(|(k, v)| (k, (*v).clone()))
112 .collect())
113 }
114
115 async fn fetch_ticker(&self, symbol: &str) -> Result<Ticker> {
116 Binance::fetch_ticker(self, symbol, ()).await
118 }
119
120 async fn fetch_tickers(&self, symbols: Option<&[String]>) -> Result<Vec<Ticker>> {
121 let symbols_vec = symbols.map(|s| s.to_vec());
123 Binance::fetch_tickers(self, symbols_vec).await
124 }
125
126 async fn fetch_order_book(&self, symbol: &str, limit: Option<u32>) -> Result<OrderBook> {
127 Binance::fetch_order_book(self, symbol, limit).await
129 }
130
131 async fn fetch_trades(&self, symbol: &str, limit: Option<u32>) -> Result<Vec<Trade>> {
132 Binance::fetch_trades(self, symbol, limit).await
134 }
135
136 async fn fetch_ohlcv(
137 &self,
138 symbol: &str,
139 timeframe: Timeframe,
140 since: Option<i64>,
141 limit: Option<u32>,
142 ) -> Result<Vec<Ohlcv>> {
143 use ccxt_core::types::{Amount, Price};
144
145 let timeframe_str = timeframe.to_string();
147 let ohlcv_data =
149 Binance::fetch_ohlcv(self, symbol, &timeframe_str, since, limit, None).await?;
150
151 Ok(ohlcv_data
153 .into_iter()
154 .map(|o| Ohlcv {
155 timestamp: o.timestamp,
156 open: Price::from(Decimal::try_from(o.open).unwrap_or_default()),
157 high: Price::from(Decimal::try_from(o.high).unwrap_or_default()),
158 low: Price::from(Decimal::try_from(o.low).unwrap_or_default()),
159 close: Price::from(Decimal::try_from(o.close).unwrap_or_default()),
160 volume: Amount::from(Decimal::try_from(o.volume).unwrap_or_default()),
161 })
162 .collect())
163 }
164
165 async fn create_order(
168 &self,
169 symbol: &str,
170 order_type: OrderType,
171 side: OrderSide,
172 amount: Decimal,
173 price: Option<Decimal>,
174 ) -> Result<Order> {
175 let amount_f64 = amount
177 .to_f64()
178 .ok_or_else(|| ccxt_core::Error::invalid_request("Failed to convert amount to f64"))?;
179 let price_f64 = match price {
180 Some(p) => Some(p.to_f64().ok_or_else(|| {
181 ccxt_core::Error::invalid_request("Failed to convert price to f64")
182 })?),
183 None => None,
184 };
185
186 Binance::create_order(self, symbol, order_type, side, amount_f64, price_f64, None).await
187 }
188
189 async fn cancel_order(&self, id: &str, symbol: Option<&str>) -> Result<Order> {
190 let symbol_str = symbol.ok_or_else(|| {
193 ccxt_core::Error::invalid_request("Symbol is required for cancel_order on Binance")
194 })?;
195 Binance::cancel_order(self, id, symbol_str).await
196 }
197
198 async fn cancel_all_orders(&self, symbol: Option<&str>) -> Result<Vec<Order>> {
199 let symbol_str = symbol.ok_or_else(|| {
202 ccxt_core::Error::invalid_request("Symbol is required for cancel_all_orders on Binance")
203 })?;
204 Binance::cancel_all_orders(self, symbol_str).await
205 }
206
207 async fn fetch_order(&self, id: &str, symbol: Option<&str>) -> Result<Order> {
208 let symbol_str = symbol.ok_or_else(|| {
211 ccxt_core::Error::invalid_request("Symbol is required for fetch_order on Binance")
212 })?;
213 Binance::fetch_order(self, id, symbol_str).await
214 }
215
216 async fn fetch_open_orders(
217 &self,
218 symbol: Option<&str>,
219 _since: Option<i64>,
220 _limit: Option<u32>,
221 ) -> Result<Vec<Order>> {
222 Binance::fetch_open_orders(self, symbol).await
225 }
226
227 async fn fetch_closed_orders(
228 &self,
229 symbol: Option<&str>,
230 since: Option<i64>,
231 limit: Option<u32>,
232 ) -> Result<Vec<Order>> {
233 Binance::fetch_closed_orders(self, symbol, since, limit).await
236 }
237
238 async fn fetch_balance(&self) -> Result<Balance> {
241 Binance::fetch_balance(self, None).await
243 }
244
245 async fn fetch_my_trades(
246 &self,
247 symbol: Option<&str>,
248 since: Option<i64>,
249 limit: Option<u32>,
250 ) -> Result<Vec<Trade>> {
251 let symbol_str = symbol.ok_or_else(|| {
254 ccxt_core::Error::invalid_request("Symbol is required for fetch_my_trades on Binance")
255 })?;
256 Binance::fetch_my_trades(self, symbol_str, since, limit).await
258 }
259
260 async fn market(&self, symbol: &str) -> Result<Market> {
263 let cache = self.base().market_cache.read().await;
265
266 if !cache.loaded {
267 return Err(ccxt_core::Error::exchange(
268 "-1",
269 "Markets not loaded. Call load_markets() first.",
270 ));
271 }
272
273 cache
274 .markets
275 .get(symbol)
276 .map(|v| (**v).clone())
277 .ok_or_else(|| ccxt_core::Error::bad_symbol(format!("Market {} not found", symbol)))
278 }
279
280 async fn markets(&self) -> HashMap<String, Market> {
281 let cache = self.base().market_cache.read().await;
282 cache
283 .markets
284 .iter()
285 .map(|(k, v)| (k.clone(), (**v).clone()))
286 .collect()
287 }
288}
289
290#[async_trait]
293impl PublicExchange for Binance {
294 fn id(&self) -> &str {
295 "binance"
296 }
297
298 fn name(&self) -> &str {
299 "Binance"
300 }
301
302 fn version(&self) -> &'static str {
303 "v3"
304 }
305
306 fn certified(&self) -> bool {
307 true
308 }
309
310 fn capabilities(&self) -> ExchangeCapabilities {
311 ExchangeCapabilities::builder()
315 .all()
316 .without_capability(Capability::EditOrder)
317 .without_capability(Capability::FetchCanceledOrders)
318 .build()
319 }
320
321 fn timeframes(&self) -> Vec<Timeframe> {
322 vec![
323 Timeframe::M1,
324 Timeframe::M3,
325 Timeframe::M5,
326 Timeframe::M15,
327 Timeframe::M30,
328 Timeframe::H1,
329 Timeframe::H2,
330 Timeframe::H4,
331 Timeframe::H6,
332 Timeframe::H8,
333 Timeframe::H12,
334 Timeframe::D1,
335 Timeframe::D3,
336 Timeframe::W1,
337 Timeframe::Mon1,
338 ]
339 }
340
341 fn rate_limit(&self) -> u32 {
342 50
343 }
344
345 fn has_websocket(&self) -> bool {
346 true
347 }
348}
349
350impl Binance {
352 pub(crate) fn check_required_credentials(&self) -> ccxt_core::Result<()> {
354 if self.base().config.api_key.is_none() || self.base().config.secret.is_none() {
355 return Err(ccxt_core::Error::authentication(
356 "API key and secret are required",
357 ));
358 }
359 Ok(())
360 }
361
362 pub(crate) fn get_auth(&self) -> ccxt_core::Result<super::auth::BinanceAuth> {
364 let api_key = self
365 .base()
366 .config
367 .api_key
368 .as_ref()
369 .ok_or_else(|| ccxt_core::Error::authentication("API key is required"))?
370 .clone();
371
372 let secret = self
373 .base()
374 .config
375 .secret
376 .as_ref()
377 .ok_or_else(|| ccxt_core::Error::authentication("Secret is required"))?
378 .clone();
379
380 Ok(super::auth::BinanceAuth::new(api_key, secret))
381 }
382
383 pub async fn get_signing_timestamp(&self) -> ccxt_core::Result<i64> {
424 if self.time_sync.needs_resync() {
426 if let Err(e) = self.sync_time().await {
428 if !self.time_sync.is_initialized() {
430 return Err(e);
431 }
432 tracing::warn!(
434 error = %e,
435 "Time sync failed, using cached offset"
436 );
437 }
438 }
439
440 if !self.time_sync.is_initialized() {
442 return self.fetch_time_raw().await;
443 }
444
445 Ok(self.time_sync.get_server_timestamp())
447 }
448
449 pub async fn sync_time(&self) -> ccxt_core::Result<()> {
489 let server_time = self.fetch_time_raw().await?;
490 self.time_sync.update_offset(server_time);
491 tracing::debug!(
492 server_time = server_time,
493 offset = self.time_sync.get_offset(),
494 "Time synchronized with Binance server"
495 );
496 Ok(())
497 }
498
499 pub fn is_timestamp_error(&self, error: &ccxt_core::Error) -> bool {
539 let error_str = error.to_string().to_lowercase();
540
541 let has_timestamp_keyword = error_str.contains("timestamp");
543 let has_recv_window = error_str.contains("recvwindow");
544 let has_ahead = error_str.contains("ahead");
545 let has_behind = error_str.contains("behind");
546
547 if has_timestamp_keyword && (has_recv_window || has_ahead || has_behind) {
549 return true;
550 }
551
552 if error_str.contains("-1021") {
556 return true;
557 }
558
559 if let ccxt_core::Error::Exchange(details) = error {
561 if details.code == "-1021" {
562 return true;
563 }
564 }
565
566 false
567 }
568
569 pub async fn execute_signed_request_with_retry<T, F, Fut>(
623 &self,
624 request_fn: F,
625 ) -> ccxt_core::Result<T>
626 where
627 F: Fn(i64) -> Fut,
628 Fut: std::future::Future<Output = ccxt_core::Result<T>>,
629 {
630 let timestamp = self.get_signing_timestamp().await?;
632
633 match request_fn(timestamp).await {
635 Ok(result) => Ok(result),
636 Err(e) if self.is_timestamp_error(&e) => {
637 tracing::warn!(
639 error = %e,
640 "Timestamp error detected, resyncing time and retrying request"
641 );
642
643 if let Err(sync_err) = self.sync_time().await {
645 tracing::error!(
646 error = %sync_err,
647 "Failed to resync time after timestamp error"
648 );
649 return Err(e);
651 }
652
653 let new_timestamp = self.time_sync.get_server_timestamp();
655
656 tracing::debug!(
657 old_timestamp = timestamp,
658 new_timestamp = new_timestamp,
659 offset = self.time_sync.get_offset(),
660 "Retrying request with fresh timestamp"
661 );
662
663 request_fn(new_timestamp).await
665 }
666 Err(e) => Err(e),
667 }
668 }
669
670 pub async fn handle_timestamp_error_and_resync(&self, error: &ccxt_core::Error) -> bool {
708 if self.is_timestamp_error(error) {
709 tracing::warn!(
710 error = %error,
711 "Timestamp error detected, triggering time resync"
712 );
713
714 if let Err(sync_err) = self.sync_time().await {
715 tracing::error!(
716 error = %sync_err,
717 "Failed to resync time after timestamp error"
718 );
719 return false;
720 }
721
722 tracing::debug!(
723 offset = self.time_sync.get_offset(),
724 "Time resync completed after timestamp error"
725 );
726
727 return true;
728 }
729
730 false
731 }
732}
733
734#[cfg(test)]
735mod tests {
736 use super::*;
737 use ccxt_core::ExchangeConfig;
738
739 #[test]
740 fn test_binance_exchange_trait_metadata() {
741 let config = ExchangeConfig::default();
742 let binance = Binance::new(config).unwrap();
743
744 let exchange: &dyn Exchange = &binance;
746
747 assert_eq!(exchange.id(), "binance");
748 assert_eq!(exchange.name(), "Binance");
749 assert_eq!(exchange.version(), "v3");
750 assert!(exchange.certified());
751 assert!(exchange.has_websocket());
752 }
753
754 #[test]
755 fn test_binance_exchange_trait_capabilities() {
756 let config = ExchangeConfig::default();
757 let binance = Binance::new(config).unwrap();
758
759 let exchange: &dyn Exchange = &binance;
760 let caps = exchange.capabilities();
761
762 assert!(caps.fetch_markets());
763 assert!(caps.fetch_ticker());
764 assert!(caps.create_order());
765 assert!(caps.websocket());
766 assert!(!caps.edit_order()); }
768
769 #[test]
770 fn test_binance_exchange_trait_timeframes() {
771 let config = ExchangeConfig::default();
772 let binance = Binance::new(config).unwrap();
773
774 let exchange: &dyn Exchange = &binance;
775 let timeframes = exchange.timeframes();
776
777 assert!(!timeframes.is_empty());
778 assert!(timeframes.contains(&Timeframe::M1));
779 assert!(timeframes.contains(&Timeframe::H1));
780 assert!(timeframes.contains(&Timeframe::D1));
781 }
782
783 #[test]
784 fn test_binance_exchange_trait_object_safety() {
785 let config = ExchangeConfig::default();
786 let binance = Binance::new(config).unwrap();
787
788 let exchange: Box<dyn Exchange> = Box::new(binance);
790
791 assert_eq!(exchange.id(), "binance");
792 assert_eq!(exchange.rate_limit(), 50);
793 }
794
795 #[test]
796 fn test_binance_exchange_ext_trait() {
797 let config = ExchangeConfig::default();
798 let binance = Binance::new(config).unwrap();
799
800 let exchange: &dyn Exchange = &binance;
802
803 assert!(
805 exchange.supports_market_data(),
806 "Binance should support market data"
807 );
808 assert!(
809 exchange.supports_trading(),
810 "Binance should support trading"
811 );
812 assert!(
813 exchange.supports_account(),
814 "Binance should support account operations"
815 );
816 assert!(
817 exchange.supports_margin(),
818 "Binance should support margin operations"
819 );
820 assert!(
821 exchange.supports_funding(),
822 "Binance should support funding operations"
823 );
824 }
825
826 #[test]
827 fn test_binance_implements_both_exchange_and_public_exchange() {
828 let config = ExchangeConfig::default();
829 let binance = Binance::new(config).unwrap();
830
831 let exchange: &dyn Exchange = &binance;
833 let public_exchange: &dyn PublicExchange = &binance;
834
835 assert_eq!(exchange.id(), public_exchange.id());
837 assert_eq!(exchange.name(), public_exchange.name());
838 assert_eq!(exchange.version(), public_exchange.version());
839 assert_eq!(exchange.certified(), public_exchange.certified());
840 assert_eq!(exchange.rate_limit(), public_exchange.rate_limit());
841 assert_eq!(exchange.has_websocket(), public_exchange.has_websocket());
842 assert_eq!(exchange.timeframes(), public_exchange.timeframes());
843 }
844
845 #[test]
848 fn test_is_timestamp_error_with_recv_window_message() {
849 let config = ExchangeConfig::default();
850 let binance = Binance::new(config).unwrap();
851
852 let err = ccxt_core::Error::exchange(
854 "-1021",
855 "Timestamp for this request is outside of the recvWindow",
856 );
857 assert!(
858 binance.is_timestamp_error(&err),
859 "Should detect recvWindow timestamp error"
860 );
861 }
862
863 #[test]
864 fn test_is_timestamp_error_with_ahead_message() {
865 let config = ExchangeConfig::default();
866 let binance = Binance::new(config).unwrap();
867
868 let err = ccxt_core::Error::exchange("-1021", "Timestamp is ahead of server time");
870 assert!(
871 binance.is_timestamp_error(&err),
872 "Should detect 'ahead' timestamp error"
873 );
874 }
875
876 #[test]
877 fn test_is_timestamp_error_with_behind_message() {
878 let config = ExchangeConfig::default();
879 let binance = Binance::new(config).unwrap();
880
881 let err = ccxt_core::Error::exchange("-1021", "Timestamp is behind server time");
883 assert!(
884 binance.is_timestamp_error(&err),
885 "Should detect 'behind' timestamp error"
886 );
887 }
888
889 #[test]
890 fn test_is_timestamp_error_with_error_code_only() {
891 let config = ExchangeConfig::default();
892 let binance = Binance::new(config).unwrap();
893
894 let err = ccxt_core::Error::exchange("-1021", "Some error message");
896 assert!(
897 binance.is_timestamp_error(&err),
898 "Should detect error code -1021"
899 );
900 }
901
902 #[test]
903 fn test_is_timestamp_error_non_timestamp_error() {
904 let config = ExchangeConfig::default();
905 let binance = Binance::new(config).unwrap();
906
907 let err = ccxt_core::Error::exchange("-1100", "Illegal characters found in parameter");
909 assert!(
910 !binance.is_timestamp_error(&err),
911 "Should not detect non-timestamp error"
912 );
913
914 let err = ccxt_core::Error::authentication("Invalid API key");
916 assert!(
917 !binance.is_timestamp_error(&err),
918 "Should not detect authentication error as timestamp error"
919 );
920
921 let err = ccxt_core::Error::network("Connection refused");
923 assert!(
924 !binance.is_timestamp_error(&err),
925 "Should not detect network error as timestamp error"
926 );
927 }
928
929 #[test]
930 fn test_is_timestamp_error_case_insensitive() {
931 let config = ExchangeConfig::default();
932 let binance = Binance::new(config).unwrap();
933
934 let err = ccxt_core::Error::exchange(
936 "-1021",
937 "TIMESTAMP for this request is outside of the RECVWINDOW",
938 );
939 assert!(
940 binance.is_timestamp_error(&err),
941 "Should detect timestamp error case-insensitively"
942 );
943 }
944
945 #[test]
946 fn test_time_sync_manager_accessible() {
947 let config = ExchangeConfig::default();
948 let binance = Binance::new(config).unwrap();
949
950 let time_sync = binance.time_sync();
952 assert!(
953 !time_sync.is_initialized(),
954 "Time sync should not be initialized initially"
955 );
956 assert!(
957 time_sync.needs_resync(),
958 "Time sync should need resync initially"
959 );
960 }
961
962 #[test]
963 fn test_time_sync_manager_update_offset() {
964 let config = ExchangeConfig::default();
965 let binance = Binance::new(config).unwrap();
966
967 let server_time = ccxt_core::time::TimestampUtils::now_ms() + 100;
969 binance.time_sync().update_offset(server_time);
970
971 assert!(
972 binance.time_sync().is_initialized(),
973 "Time sync should be initialized after update"
974 );
975 assert!(
976 !binance.time_sync().needs_resync(),
977 "Time sync should not need resync immediately after update"
978 );
979
980 let offset = binance.time_sync().get_offset();
982 assert!(
983 offset >= 90 && offset <= 110,
984 "Offset should be approximately 100ms, got {}",
985 offset
986 );
987 }
988
989 #[tokio::test]
992 async fn test_execute_signed_request_with_retry_success() {
993 let config = ExchangeConfig::default();
994 let binance = Binance::new(config).unwrap();
995
996 let server_time = ccxt_core::time::TimestampUtils::now_ms();
998 binance.time_sync().update_offset(server_time);
999
1000 let result = binance
1002 .execute_signed_request_with_retry(|timestamp| async move {
1003 assert!(timestamp > 0, "Timestamp should be positive");
1004 Ok::<_, ccxt_core::Error>(42)
1005 })
1006 .await;
1007
1008 assert!(result.is_ok(), "Request should succeed");
1009 assert_eq!(result.unwrap(), 42);
1010 }
1011
1012 #[tokio::test]
1013 async fn test_execute_signed_request_with_retry_non_timestamp_error() {
1014 let config = ExchangeConfig::default();
1015 let binance = Binance::new(config).unwrap();
1016
1017 let server_time = ccxt_core::time::TimestampUtils::now_ms();
1019 binance.time_sync().update_offset(server_time);
1020
1021 let result = binance
1023 .execute_signed_request_with_retry(|_timestamp| async move {
1024 Err::<i32, _>(ccxt_core::Error::exchange("-1100", "Invalid parameter"))
1025 })
1026 .await;
1027
1028 assert!(result.is_err(), "Request should fail");
1029 let err = result.unwrap_err();
1030 assert!(
1031 err.to_string().contains("-1100"),
1032 "Error should contain original error code"
1033 );
1034 }
1035
1036 #[test]
1037 fn test_handle_timestamp_error_detection() {
1038 let config = ExchangeConfig::default();
1039 let binance = Binance::new(config).unwrap();
1040
1041 let timestamp_errors = vec![
1043 ccxt_core::Error::exchange(
1044 "-1021",
1045 "Timestamp for this request is outside of the recvWindow",
1046 ),
1047 ccxt_core::Error::exchange("-1021", "Timestamp is ahead of server time"),
1048 ccxt_core::Error::exchange("-1021", "Timestamp is behind server time"),
1049 ccxt_core::Error::exchange("-1021", "Some error with timestamp and recvwindow"),
1050 ];
1051
1052 for err in timestamp_errors {
1053 assert!(
1054 binance.is_timestamp_error(&err),
1055 "Should detect timestamp error: {}",
1056 err
1057 );
1058 }
1059
1060 let non_timestamp_errors = vec![
1062 ccxt_core::Error::exchange("-1100", "Invalid parameter"),
1063 ccxt_core::Error::exchange("-1000", "Unknown error"),
1064 ccxt_core::Error::authentication("Invalid API key"),
1065 ccxt_core::Error::network("Connection refused"),
1066 ccxt_core::Error::timeout("Request timed out"),
1067 ];
1068
1069 for err in non_timestamp_errors {
1070 assert!(
1071 !binance.is_timestamp_error(&err),
1072 "Should not detect as timestamp error: {}",
1073 err
1074 );
1075 }
1076 }
1077
1078 mod property_tests {
1081 use super::*;
1082 use proptest::prelude::*;
1083
1084 fn arb_exchange_config() -> impl Strategy<Value = ExchangeConfig> {
1086 (
1087 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))), )
1091 .prop_map(|(sandbox, api_key, secret)| ExchangeConfig {
1092 sandbox,
1093 api_key,
1094 secret,
1095 ..Default::default()
1096 })
1097 }
1098
1099 proptest! {
1100 #![proptest_config(ProptestConfig::with_cases(100))]
1101
1102 #[test]
1109 fn prop_timeframes_non_empty(config in arb_exchange_config()) {
1110 let binance = Binance::new(config).expect("Should create Binance instance");
1111 let exchange: &dyn Exchange = &binance;
1112
1113 let timeframes = exchange.timeframes();
1114
1115 prop_assert!(!timeframes.is_empty(), "Timeframes should not be empty");
1117
1118 let mut seen = std::collections::HashSet::new();
1120 for tf in &timeframes {
1121 prop_assert!(
1122 seen.insert(tf.clone()),
1123 "Timeframes should not contain duplicates: {:?}",
1124 tf
1125 );
1126 }
1127
1128 prop_assert!(
1130 timeframes.contains(&Timeframe::M1),
1131 "Should contain 1-minute timeframe"
1132 );
1133 prop_assert!(
1134 timeframes.contains(&Timeframe::H1),
1135 "Should contain 1-hour timeframe"
1136 );
1137 prop_assert!(
1138 timeframes.contains(&Timeframe::D1),
1139 "Should contain 1-day timeframe"
1140 );
1141 }
1142
1143 #[test]
1150 fn prop_backward_compatibility_metadata(config in arb_exchange_config()) {
1151 let binance = Binance::new(config).expect("Should create Binance instance");
1152
1153 let exchange: &dyn Exchange = &binance;
1155
1156 prop_assert_eq!(
1158 exchange.id(),
1159 Binance::id(&binance),
1160 "id() should be consistent between trait and direct call"
1161 );
1162
1163 prop_assert_eq!(
1165 exchange.name(),
1166 Binance::name(&binance),
1167 "name() should be consistent between trait and direct call"
1168 );
1169
1170 prop_assert_eq!(
1172 exchange.version(),
1173 Binance::version(&binance),
1174 "version() should be consistent between trait and direct call"
1175 );
1176
1177 prop_assert_eq!(
1179 exchange.certified(),
1180 Binance::certified(&binance),
1181 "certified() should be consistent between trait and direct call"
1182 );
1183
1184 prop_assert_eq!(
1186 exchange.rate_limit(),
1187 Binance::rate_limit(&binance),
1188 "rate_limit() should be consistent between trait and direct call"
1189 );
1190
1191 let trait_caps = exchange.capabilities();
1193 prop_assert!(trait_caps.fetch_markets(), "Should support fetch_markets");
1194 prop_assert!(trait_caps.fetch_ticker(), "Should support fetch_ticker");
1195 prop_assert!(trait_caps.websocket(), "Should support websocket");
1196 }
1197 }
1198 }
1199}