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.loaded {
256 return Err(ccxt_core::Error::exchange(
257 "-1",
258 "Markets not loaded. Call load_markets() first.",
259 ));
260 }
261
262 cache
263 .markets
264 .get(symbol)
265 .cloned()
266 .ok_or_else(|| ccxt_core::Error::bad_symbol(format!("Market {} not found", symbol)))
267 }
268
269 async fn markets(&self) -> Arc<HashMap<String, Arc<Market>>> {
270 let cache = self.base().market_cache.read().await;
271 cache.markets.clone()
272 }
273}
274
275#[async_trait]
278impl PublicExchange for Binance {
279 fn id(&self) -> &'static str {
280 "binance"
281 }
282
283 fn name(&self) -> &'static str {
284 "Binance"
285 }
286
287 fn version(&self) -> &'static str {
288 "v3"
289 }
290
291 fn certified(&self) -> bool {
292 true
293 }
294
295 fn capabilities(&self) -> ExchangeCapabilities {
296 ExchangeCapabilities::builder()
300 .all()
301 .without_capability(Capability::EditOrder)
302 .without_capability(Capability::FetchCanceledOrders)
303 .build()
304 }
305
306 fn timeframes(&self) -> Vec<Timeframe> {
307 vec![
308 Timeframe::M1,
309 Timeframe::M3,
310 Timeframe::M5,
311 Timeframe::M15,
312 Timeframe::M30,
313 Timeframe::H1,
314 Timeframe::H2,
315 Timeframe::H4,
316 Timeframe::H6,
317 Timeframe::H8,
318 Timeframe::H12,
319 Timeframe::D1,
320 Timeframe::D3,
321 Timeframe::W1,
322 Timeframe::Mon1,
323 ]
324 }
325
326 fn rate_limit(&self) -> u32 {
327 self.options.rate_limit
328 }
329
330 fn has_websocket(&self) -> bool {
331 true
332 }
333}
334
335impl Binance {
337 pub(crate) fn check_required_credentials(&self) -> ccxt_core::Result<()> {
339 if self.base().config.api_key.is_none() || self.base().config.secret.is_none() {
340 return Err(ccxt_core::Error::authentication(
341 "API key and secret are required",
342 ));
343 }
344 Ok(())
345 }
346
347 pub(crate) fn get_auth(&self) -> ccxt_core::Result<super::auth::BinanceAuth> {
349 let api_key = self
350 .base()
351 .config
352 .api_key
353 .as_ref()
354 .ok_or_else(|| ccxt_core::Error::authentication("API key is required"))?
355 .clone();
356
357 let secret = self
358 .base()
359 .config
360 .secret
361 .as_ref()
362 .ok_or_else(|| ccxt_core::Error::authentication("Secret is required"))?
363 .clone();
364
365 Ok(super::auth::BinanceAuth::new(
366 api_key.expose_secret(),
367 secret.expose_secret(),
368 ))
369 }
370
371 pub async fn get_signing_timestamp(&self) -> ccxt_core::Result<i64> {
412 if self.time_sync.needs_resync() {
414 if let Err(e) = self.sync_time().await {
416 if !self.time_sync.is_initialized() {
418 return Err(e);
419 }
420 tracing::warn!(
422 error = %e,
423 "Time sync failed, using cached offset"
424 );
425 }
426 }
427
428 if !self.time_sync.is_initialized() {
430 return self.fetch_time_raw().await;
431 }
432
433 Ok(self.time_sync.get_server_timestamp())
435 }
436
437 pub async fn sync_time(&self) -> ccxt_core::Result<()> {
477 let server_time = self.fetch_time_raw().await?;
478 self.time_sync.update_offset(server_time);
479 tracing::debug!(
480 server_time = server_time,
481 offset = self.time_sync.get_offset(),
482 "Time synchronized with Binance server"
483 );
484 Ok(())
485 }
486
487 pub fn is_timestamp_error(&self, error: &ccxt_core::Error) -> bool {
527 let error_str = error.to_string().to_lowercase();
528
529 let has_timestamp_keyword = error_str.contains("timestamp");
531 let has_recv_window = error_str.contains("recvwindow");
532 let has_ahead = error_str.contains("ahead");
533 let has_behind = error_str.contains("behind");
534
535 if has_timestamp_keyword && (has_recv_window || has_ahead || has_behind) {
537 return true;
538 }
539
540 if error_str.contains("-1021") {
544 return true;
545 }
546
547 if let ccxt_core::Error::Exchange(details) = error {
549 if details.code == "-1021" {
550 return true;
551 }
552 }
553
554 false
555 }
556
557 pub async fn execute_signed_request_with_retry<T, F, Fut>(
611 &self,
612 request_fn: F,
613 ) -> ccxt_core::Result<T>
614 where
615 F: Fn(i64) -> Fut,
616 Fut: std::future::Future<Output = ccxt_core::Result<T>>,
617 {
618 let timestamp = self.get_signing_timestamp().await?;
620
621 match request_fn(timestamp).await {
623 Ok(result) => Ok(result),
624 Err(e) if self.is_timestamp_error(&e) => {
625 tracing::warn!(
627 error = %e,
628 "Timestamp error detected, resyncing time and retrying request"
629 );
630
631 if let Err(sync_err) = self.sync_time().await {
633 tracing::error!(
634 error = %sync_err,
635 "Failed to resync time after timestamp error"
636 );
637 return Err(e);
639 }
640
641 let new_timestamp = self.time_sync.get_server_timestamp();
643
644 tracing::debug!(
645 old_timestamp = timestamp,
646 new_timestamp = new_timestamp,
647 offset = self.time_sync.get_offset(),
648 "Retrying request with fresh timestamp"
649 );
650
651 request_fn(new_timestamp).await
653 }
654 Err(e) => Err(e),
655 }
656 }
657
658 pub async fn handle_timestamp_error_and_resync(&self, error: &ccxt_core::Error) -> bool {
696 if self.is_timestamp_error(error) {
697 tracing::warn!(
698 error = %error,
699 "Timestamp error detected, triggering time resync"
700 );
701
702 if let Err(sync_err) = self.sync_time().await {
703 tracing::error!(
704 error = %sync_err,
705 "Failed to resync time after timestamp error"
706 );
707 return false;
708 }
709
710 tracing::debug!(
711 offset = self.time_sync.get_offset(),
712 "Time resync completed after timestamp error"
713 );
714
715 return true;
716 }
717
718 false
719 }
720}
721
722#[cfg(test)]
723mod tests {
724 use super::*;
725 use ccxt_core::ExchangeConfig;
726
727 #[test]
728 fn test_binance_exchange_trait_metadata() {
729 let config = ExchangeConfig::default();
730 let binance = Binance::new(config).unwrap();
731
732 let exchange: &dyn Exchange = &binance;
734
735 assert_eq!(exchange.id(), "binance");
736 assert_eq!(exchange.name(), "Binance");
737 assert_eq!(exchange.version(), "v3");
738 assert!(exchange.certified());
739 assert!(exchange.has_websocket());
740 }
741
742 #[test]
743 fn test_binance_exchange_trait_capabilities() {
744 let config = ExchangeConfig::default();
745 let binance = Binance::new(config).unwrap();
746
747 let exchange: &dyn Exchange = &binance;
748 let caps = exchange.capabilities();
749
750 assert!(caps.fetch_markets());
751 assert!(caps.fetch_ticker());
752 assert!(caps.create_order());
753 assert!(caps.websocket());
754 assert!(!caps.edit_order()); }
756
757 #[test]
758 fn test_binance_exchange_trait_timeframes() {
759 let config = ExchangeConfig::default();
760 let binance = Binance::new(config).unwrap();
761
762 let exchange: &dyn Exchange = &binance;
763 let timeframes = exchange.timeframes();
764
765 assert!(!timeframes.is_empty());
766 assert!(timeframes.contains(&Timeframe::M1));
767 assert!(timeframes.contains(&Timeframe::H1));
768 assert!(timeframes.contains(&Timeframe::D1));
769 }
770
771 #[test]
772 fn test_binance_exchange_trait_object_safety() {
773 let config = ExchangeConfig::default();
774 let binance = Binance::new(config).unwrap();
775
776 let exchange: Box<dyn Exchange> = Box::new(binance);
778
779 assert_eq!(exchange.id(), "binance");
780 assert_eq!(exchange.rate_limit(), 50);
781 }
782
783 #[test]
784 fn test_binance_exchange_ext_trait() {
785 let config = ExchangeConfig::default();
786 let binance = Binance::new(config).unwrap();
787
788 let exchange: &dyn Exchange = &binance;
790
791 assert!(
793 exchange.supports_market_data(),
794 "Binance should support market data"
795 );
796 assert!(
797 exchange.supports_trading(),
798 "Binance should support trading"
799 );
800 assert!(
801 exchange.supports_account(),
802 "Binance should support account operations"
803 );
804 assert!(
805 exchange.supports_margin(),
806 "Binance should support margin operations"
807 );
808 assert!(
809 exchange.supports_funding(),
810 "Binance should support funding operations"
811 );
812 }
813
814 #[test]
815 fn test_binance_implements_both_exchange_and_public_exchange() {
816 let config = ExchangeConfig::default();
817 let binance = Binance::new(config).unwrap();
818
819 let exchange: &dyn Exchange = &binance;
821 let public_exchange: &dyn PublicExchange = &binance;
822
823 assert_eq!(exchange.id(), public_exchange.id());
825 assert_eq!(exchange.name(), public_exchange.name());
826 assert_eq!(exchange.version(), public_exchange.version());
827 assert_eq!(exchange.certified(), public_exchange.certified());
828 assert_eq!(exchange.rate_limit(), public_exchange.rate_limit());
829 assert_eq!(exchange.has_websocket(), public_exchange.has_websocket());
830 assert_eq!(exchange.timeframes(), public_exchange.timeframes());
831 }
832
833 #[test]
836 fn test_is_timestamp_error_with_recv_window_message() {
837 let config = ExchangeConfig::default();
838 let binance = Binance::new(config).unwrap();
839
840 let err = ccxt_core::Error::exchange(
842 "-1021",
843 "Timestamp for this request is outside of the recvWindow",
844 );
845 assert!(
846 binance.is_timestamp_error(&err),
847 "Should detect recvWindow timestamp error"
848 );
849 }
850
851 #[test]
852 fn test_is_timestamp_error_with_ahead_message() {
853 let config = ExchangeConfig::default();
854 let binance = Binance::new(config).unwrap();
855
856 let err = ccxt_core::Error::exchange("-1021", "Timestamp is ahead of server time");
858 assert!(
859 binance.is_timestamp_error(&err),
860 "Should detect 'ahead' timestamp error"
861 );
862 }
863
864 #[test]
865 fn test_is_timestamp_error_with_behind_message() {
866 let config = ExchangeConfig::default();
867 let binance = Binance::new(config).unwrap();
868
869 let err = ccxt_core::Error::exchange("-1021", "Timestamp is behind server time");
871 assert!(
872 binance.is_timestamp_error(&err),
873 "Should detect 'behind' timestamp error"
874 );
875 }
876
877 #[test]
878 fn test_is_timestamp_error_with_error_code_only() {
879 let config = ExchangeConfig::default();
880 let binance = Binance::new(config).unwrap();
881
882 let err = ccxt_core::Error::exchange("-1021", "Some error message");
884 assert!(
885 binance.is_timestamp_error(&err),
886 "Should detect error code -1021"
887 );
888 }
889
890 #[test]
891 fn test_is_timestamp_error_non_timestamp_error() {
892 let config = ExchangeConfig::default();
893 let binance = Binance::new(config).unwrap();
894
895 let err = ccxt_core::Error::exchange("-1100", "Illegal characters found in parameter");
897 assert!(
898 !binance.is_timestamp_error(&err),
899 "Should not detect non-timestamp error"
900 );
901
902 let err = ccxt_core::Error::authentication("Invalid API key");
904 assert!(
905 !binance.is_timestamp_error(&err),
906 "Should not detect authentication error as timestamp error"
907 );
908
909 let err = ccxt_core::Error::network("Connection refused");
911 assert!(
912 !binance.is_timestamp_error(&err),
913 "Should not detect network error as timestamp error"
914 );
915 }
916
917 #[test]
918 fn test_is_timestamp_error_case_insensitive() {
919 let config = ExchangeConfig::default();
920 let binance = Binance::new(config).unwrap();
921
922 let err = ccxt_core::Error::exchange(
924 "-1021",
925 "TIMESTAMP for this request is outside of the RECVWINDOW",
926 );
927 assert!(
928 binance.is_timestamp_error(&err),
929 "Should detect timestamp error case-insensitively"
930 );
931 }
932
933 #[test]
934 fn test_time_sync_manager_accessible() {
935 let config = ExchangeConfig::default();
936 let binance = Binance::new(config).unwrap();
937
938 let time_sync = binance.time_sync();
940 assert!(
941 !time_sync.is_initialized(),
942 "Time sync should not be initialized initially"
943 );
944 assert!(
945 time_sync.needs_resync(),
946 "Time sync should need resync initially"
947 );
948 }
949
950 #[test]
951 fn test_time_sync_manager_update_offset() {
952 let config = ExchangeConfig::default();
953 let binance = Binance::new(config).unwrap();
954
955 let server_time = ccxt_core::time::TimestampUtils::now_ms() + 100;
957 binance.time_sync().update_offset(server_time);
958
959 assert!(
960 binance.time_sync().is_initialized(),
961 "Time sync should be initialized after update"
962 );
963 assert!(
964 !binance.time_sync().needs_resync(),
965 "Time sync should not need resync immediately after update"
966 );
967
968 let offset = binance.time_sync().get_offset();
970 assert!(
971 offset >= 90 && offset <= 110,
972 "Offset should be approximately 100ms, got {}",
973 offset
974 );
975 }
976
977 #[tokio::test]
980 async fn test_execute_signed_request_with_retry_success() {
981 let config = ExchangeConfig::default();
982 let binance = Binance::new(config).unwrap();
983
984 let server_time = ccxt_core::time::TimestampUtils::now_ms();
986 binance.time_sync().update_offset(server_time);
987
988 let result = binance
990 .execute_signed_request_with_retry(|timestamp| async move {
991 assert!(timestamp > 0, "Timestamp should be positive");
992 Ok::<_, ccxt_core::Error>(42)
993 })
994 .await;
995
996 assert!(result.is_ok(), "Request should succeed");
997 assert_eq!(result.unwrap(), 42);
998 }
999
1000 #[tokio::test]
1001 async fn test_execute_signed_request_with_retry_non_timestamp_error() {
1002 let config = ExchangeConfig::default();
1003 let binance = Binance::new(config).unwrap();
1004
1005 let server_time = ccxt_core::time::TimestampUtils::now_ms();
1007 binance.time_sync().update_offset(server_time);
1008
1009 let result = binance
1011 .execute_signed_request_with_retry(|_timestamp| async move {
1012 Err::<i32, _>(ccxt_core::Error::exchange("-1100", "Invalid parameter"))
1013 })
1014 .await;
1015
1016 assert!(result.is_err(), "Request should fail");
1017 let err = result.unwrap_err();
1018 assert!(
1019 err.to_string().contains("-1100"),
1020 "Error should contain original error code"
1021 );
1022 }
1023
1024 #[test]
1025 fn test_handle_timestamp_error_detection() {
1026 let config = ExchangeConfig::default();
1027 let binance = Binance::new(config).unwrap();
1028
1029 let timestamp_errors = vec![
1031 ccxt_core::Error::exchange(
1032 "-1021",
1033 "Timestamp for this request is outside of the recvWindow",
1034 ),
1035 ccxt_core::Error::exchange("-1021", "Timestamp is ahead of server time"),
1036 ccxt_core::Error::exchange("-1021", "Timestamp is behind server time"),
1037 ccxt_core::Error::exchange("-1021", "Some error with timestamp and recvwindow"),
1038 ];
1039
1040 for err in timestamp_errors {
1041 assert!(
1042 binance.is_timestamp_error(&err),
1043 "Should detect timestamp error: {}",
1044 err
1045 );
1046 }
1047
1048 let non_timestamp_errors = vec![
1050 ccxt_core::Error::exchange("-1100", "Invalid parameter"),
1051 ccxt_core::Error::exchange("-1000", "Unknown error"),
1052 ccxt_core::Error::authentication("Invalid API key"),
1053 ccxt_core::Error::network("Connection refused"),
1054 ccxt_core::Error::timeout("Request timed out"),
1055 ];
1056
1057 for err in non_timestamp_errors {
1058 assert!(
1059 !binance.is_timestamp_error(&err),
1060 "Should not detect as timestamp error: {}",
1061 err
1062 );
1063 }
1064 }
1065
1066 mod property_tests {
1069 use super::*;
1070 use proptest::prelude::*;
1071
1072 fn arb_exchange_config() -> impl Strategy<Value = ExchangeConfig> {
1074 (
1075 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))), )
1079 .prop_map(|(sandbox, api_key, secret)| ExchangeConfig {
1080 sandbox,
1081 api_key: api_key.map(ccxt_core::SecretString::new),
1082 secret: secret.map(ccxt_core::SecretString::new),
1083 ..Default::default()
1084 })
1085 }
1086
1087 proptest! {
1088 #![proptest_config(ProptestConfig::with_cases(100))]
1089
1090 #[test]
1097 fn prop_timeframes_non_empty(config in arb_exchange_config()) {
1098 let binance = Binance::new(config).expect("Should create Binance instance");
1099 let exchange: &dyn Exchange = &binance;
1100
1101 let timeframes = exchange.timeframes();
1102
1103 prop_assert!(!timeframes.is_empty(), "Timeframes should not be empty");
1105
1106 let mut seen = std::collections::HashSet::new();
1108 for tf in &timeframes {
1109 prop_assert!(
1110 seen.insert(tf.clone()),
1111 "Timeframes should not contain duplicates: {:?}",
1112 tf
1113 );
1114 }
1115
1116 prop_assert!(
1118 timeframes.contains(&Timeframe::M1),
1119 "Should contain 1-minute timeframe"
1120 );
1121 prop_assert!(
1122 timeframes.contains(&Timeframe::H1),
1123 "Should contain 1-hour timeframe"
1124 );
1125 prop_assert!(
1126 timeframes.contains(&Timeframe::D1),
1127 "Should contain 1-day timeframe"
1128 );
1129 }
1130
1131 #[test]
1138 fn prop_backward_compatibility_metadata(config in arb_exchange_config()) {
1139 let binance = Binance::new(config).expect("Should create Binance instance");
1140
1141 let exchange: &dyn Exchange = &binance;
1143
1144 prop_assert_eq!(
1146 exchange.id(),
1147 Binance::id(&binance),
1148 "id() should be consistent between trait and direct call"
1149 );
1150
1151 prop_assert_eq!(
1153 exchange.name(),
1154 Binance::name(&binance),
1155 "name() should be consistent between trait and direct call"
1156 );
1157
1158 prop_assert_eq!(
1160 exchange.version(),
1161 Binance::version(&binance),
1162 "version() should be consistent between trait and direct call"
1163 );
1164
1165 prop_assert_eq!(
1167 exchange.certified(),
1168 Binance::certified(&binance),
1169 "certified() should be consistent between trait and direct call"
1170 );
1171
1172 prop_assert_eq!(
1174 exchange.rate_limit(),
1175 Binance::rate_limit(&binance),
1176 "rate_limit() should be consistent between trait and direct call"
1177 );
1178
1179 let trait_caps = exchange.capabilities();
1181 prop_assert!(trait_caps.fetch_markets(), "Should support fetch_markets");
1182 prop_assert!(trait_caps.fetch_ticker(), "Should support fetch_ticker");
1183 prop_assert!(trait_caps.websocket(), "Should support websocket");
1184 }
1185 }
1186 }
1187}