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 check_api_key(&self) -> ccxt_core::Result<()> {
347 if self.base().config.api_key.is_none() {
348 return Err(ccxt_core::Error::authentication("API key is required"));
349 }
350 Ok(())
351 }
352
353 pub(crate) fn get_auth(&self) -> ccxt_core::Result<super::auth::BinanceAuth> {
355 let api_key = self
356 .base()
357 .config
358 .api_key
359 .as_ref()
360 .ok_or_else(|| ccxt_core::Error::authentication("API key is required"))?
361 .clone();
362
363 let secret = self
364 .base()
365 .config
366 .secret
367 .as_ref()
368 .ok_or_else(|| ccxt_core::Error::authentication("Secret is required"))?
369 .clone();
370
371 Ok(super::auth::BinanceAuth::new(
372 api_key.expose_secret(),
373 secret.expose_secret(),
374 ))
375 }
376
377 pub async fn get_signing_timestamp(&self) -> ccxt_core::Result<i64> {
418 if self.time_sync.needs_resync() {
420 if let Err(e) = self.sync_time().await {
422 if !self.time_sync.is_initialized() {
424 return Err(e);
425 }
426 tracing::warn!(
428 error = %e,
429 "Time sync failed, using cached offset"
430 );
431 }
432 }
433
434 if !self.time_sync.is_initialized() {
436 return self.fetch_time_raw().await;
437 }
438
439 Ok(self.time_sync.get_server_timestamp())
441 }
442
443 pub async fn sync_time(&self) -> ccxt_core::Result<()> {
483 let server_time = self.fetch_time_raw().await?;
484 self.time_sync.update_offset(server_time);
485 tracing::debug!(
486 server_time = server_time,
487 offset = self.time_sync.get_offset(),
488 "Time synchronized with Binance server"
489 );
490 Ok(())
491 }
492
493 pub fn is_timestamp_error(&self, error: &ccxt_core::Error) -> bool {
533 let error_str = error.to_string().to_lowercase();
534
535 let has_timestamp_keyword = error_str.contains("timestamp");
537 let has_recv_window = error_str.contains("recvwindow");
538 let has_ahead = error_str.contains("ahead");
539 let has_behind = error_str.contains("behind");
540
541 if has_timestamp_keyword && (has_recv_window || has_ahead || has_behind) {
543 return true;
544 }
545
546 if error_str.contains("-1021") {
550 return true;
551 }
552
553 if let ccxt_core::Error::Exchange(details) = error {
555 if details.code == "-1021" {
556 return true;
557 }
558 }
559
560 false
561 }
562
563 pub async fn execute_signed_request_with_retry<T, F, Fut>(
617 &self,
618 request_fn: F,
619 ) -> ccxt_core::Result<T>
620 where
621 F: Fn(i64) -> Fut,
622 Fut: std::future::Future<Output = ccxt_core::Result<T>>,
623 {
624 let timestamp = self.get_signing_timestamp().await?;
626
627 match request_fn(timestamp).await {
629 Ok(result) => Ok(result),
630 Err(e) if self.is_timestamp_error(&e) => {
631 tracing::warn!(
633 error = %e,
634 "Timestamp error detected, resyncing time and retrying request"
635 );
636
637 if let Err(sync_err) = self.sync_time().await {
639 tracing::error!(
640 error = %sync_err,
641 "Failed to resync time after timestamp error"
642 );
643 return Err(e);
645 }
646
647 let new_timestamp = self.time_sync.get_server_timestamp();
649
650 tracing::debug!(
651 old_timestamp = timestamp,
652 new_timestamp = new_timestamp,
653 offset = self.time_sync.get_offset(),
654 "Retrying request with fresh timestamp"
655 );
656
657 request_fn(new_timestamp).await
659 }
660 Err(e) => Err(e),
661 }
662 }
663
664 pub async fn handle_timestamp_error_and_resync(&self, error: &ccxt_core::Error) -> bool {
702 if self.is_timestamp_error(error) {
703 tracing::warn!(
704 error = %error,
705 "Timestamp error detected, triggering time resync"
706 );
707
708 if let Err(sync_err) = self.sync_time().await {
709 tracing::error!(
710 error = %sync_err,
711 "Failed to resync time after timestamp error"
712 );
713 return false;
714 }
715
716 tracing::debug!(
717 offset = self.time_sync.get_offset(),
718 "Time resync completed after timestamp error"
719 );
720
721 return true;
722 }
723
724 false
725 }
726}
727
728impl ccxt_core::signed_request::HasHttpClient for Binance {
730 fn http_client(&self) -> &ccxt_core::http_client::HttpClient {
731 &self.base().http_client
732 }
733
734 fn base_url(&self) -> &'static str {
735 ""
737 }
738}
739
740#[cfg(test)]
741mod tests {
742 #![allow(clippy::disallowed_methods)]
743 use super::*;
744 use ccxt_core::ExchangeConfig;
745
746 #[test]
747 fn test_binance_exchange_trait_metadata() {
748 let config = ExchangeConfig::default();
749 let binance = Binance::new(config).unwrap();
750
751 let exchange: &dyn Exchange = &binance;
753
754 assert_eq!(exchange.id(), "binance");
755 assert_eq!(exchange.name(), "Binance");
756 assert_eq!(exchange.version(), "v3");
757 assert!(exchange.certified());
758 assert!(exchange.has_websocket());
759 }
760
761 #[test]
762 fn test_binance_exchange_trait_capabilities() {
763 let config = ExchangeConfig::default();
764 let binance = Binance::new(config).unwrap();
765
766 let exchange: &dyn Exchange = &binance;
767 let caps = exchange.capabilities();
768
769 assert!(caps.fetch_markets());
770 assert!(caps.fetch_ticker());
771 assert!(caps.create_order());
772 assert!(caps.websocket());
773 assert!(!caps.edit_order()); }
775
776 #[test]
777 fn test_binance_exchange_trait_timeframes() {
778 let config = ExchangeConfig::default();
779 let binance = Binance::new(config).unwrap();
780
781 let exchange: &dyn Exchange = &binance;
782 let timeframes = exchange.timeframes();
783
784 assert!(!timeframes.is_empty());
785 assert!(timeframes.contains(&Timeframe::M1));
786 assert!(timeframes.contains(&Timeframe::H1));
787 assert!(timeframes.contains(&Timeframe::D1));
788 }
789
790 #[test]
791 fn test_binance_exchange_trait_object_safety() {
792 let config = ExchangeConfig::default();
793 let binance = Binance::new(config).unwrap();
794
795 let exchange: Box<dyn Exchange> = Box::new(binance);
797
798 assert_eq!(exchange.id(), "binance");
799 assert_eq!(exchange.rate_limit(), 50);
800 }
801
802 #[test]
803 fn test_binance_exchange_ext_trait() {
804 let config = ExchangeConfig::default();
805 let binance = Binance::new(config).unwrap();
806
807 let exchange: &dyn Exchange = &binance;
809
810 assert!(
812 exchange.supports_market_data(),
813 "Binance should support market data"
814 );
815 assert!(
816 exchange.supports_trading(),
817 "Binance should support trading"
818 );
819 assert!(
820 exchange.supports_account(),
821 "Binance should support account operations"
822 );
823 assert!(
824 exchange.supports_margin(),
825 "Binance should support margin operations"
826 );
827 assert!(
828 exchange.supports_funding(),
829 "Binance should support funding operations"
830 );
831 }
832
833 #[test]
834 fn test_binance_implements_both_exchange_and_public_exchange() {
835 let config = ExchangeConfig::default();
836 let binance = Binance::new(config).unwrap();
837
838 let exchange: &dyn Exchange = &binance;
840 let public_exchange: &dyn PublicExchange = &binance;
841
842 assert_eq!(exchange.id(), public_exchange.id());
844 assert_eq!(exchange.name(), public_exchange.name());
845 assert_eq!(exchange.version(), public_exchange.version());
846 assert_eq!(exchange.certified(), public_exchange.certified());
847 assert_eq!(exchange.rate_limit(), public_exchange.rate_limit());
848 assert_eq!(exchange.has_websocket(), public_exchange.has_websocket());
849 assert_eq!(exchange.timeframes(), public_exchange.timeframes());
850 }
851
852 #[test]
855 fn test_is_timestamp_error_with_recv_window_message() {
856 let config = ExchangeConfig::default();
857 let binance = Binance::new(config).unwrap();
858
859 let err = ccxt_core::Error::exchange(
861 "-1021",
862 "Timestamp for this request is outside of the recvWindow",
863 );
864 assert!(
865 binance.is_timestamp_error(&err),
866 "Should detect recvWindow timestamp error"
867 );
868 }
869
870 #[test]
871 fn test_is_timestamp_error_with_ahead_message() {
872 let config = ExchangeConfig::default();
873 let binance = Binance::new(config).unwrap();
874
875 let err = ccxt_core::Error::exchange("-1021", "Timestamp is ahead of server time");
877 assert!(
878 binance.is_timestamp_error(&err),
879 "Should detect 'ahead' timestamp error"
880 );
881 }
882
883 #[test]
884 fn test_is_timestamp_error_with_behind_message() {
885 let config = ExchangeConfig::default();
886 let binance = Binance::new(config).unwrap();
887
888 let err = ccxt_core::Error::exchange("-1021", "Timestamp is behind server time");
890 assert!(
891 binance.is_timestamp_error(&err),
892 "Should detect 'behind' timestamp error"
893 );
894 }
895
896 #[test]
897 fn test_is_timestamp_error_with_error_code_only() {
898 let config = ExchangeConfig::default();
899 let binance = Binance::new(config).unwrap();
900
901 let err = ccxt_core::Error::exchange("-1021", "Some error message");
903 assert!(
904 binance.is_timestamp_error(&err),
905 "Should detect error code -1021"
906 );
907 }
908
909 #[test]
910 fn test_is_timestamp_error_non_timestamp_error() {
911 let config = ExchangeConfig::default();
912 let binance = Binance::new(config).unwrap();
913
914 let err = ccxt_core::Error::exchange("-1100", "Illegal characters found in parameter");
916 assert!(
917 !binance.is_timestamp_error(&err),
918 "Should not detect non-timestamp error"
919 );
920
921 let err = ccxt_core::Error::authentication("Invalid API key");
923 assert!(
924 !binance.is_timestamp_error(&err),
925 "Should not detect authentication error as timestamp error"
926 );
927
928 let err = ccxt_core::Error::network("Connection refused");
930 assert!(
931 !binance.is_timestamp_error(&err),
932 "Should not detect network error as timestamp error"
933 );
934 }
935
936 #[test]
937 fn test_is_timestamp_error_case_insensitive() {
938 let config = ExchangeConfig::default();
939 let binance = Binance::new(config).unwrap();
940
941 let err = ccxt_core::Error::exchange(
943 "-1021",
944 "TIMESTAMP for this request is outside of the RECVWINDOW",
945 );
946 assert!(
947 binance.is_timestamp_error(&err),
948 "Should detect timestamp error case-insensitively"
949 );
950 }
951
952 #[test]
953 fn test_time_sync_manager_accessible() {
954 let config = ExchangeConfig::default();
955 let binance = Binance::new(config).unwrap();
956
957 let time_sync = binance.time_sync();
959 assert!(
960 !time_sync.is_initialized(),
961 "Time sync should not be initialized initially"
962 );
963 assert!(
964 time_sync.needs_resync(),
965 "Time sync should need resync initially"
966 );
967 }
968
969 #[test]
970 fn test_time_sync_manager_update_offset() {
971 let config = ExchangeConfig::default();
972 let binance = Binance::new(config).unwrap();
973
974 let server_time = ccxt_core::time::TimestampUtils::now_ms() + 100;
976 binance.time_sync().update_offset(server_time);
977
978 assert!(
979 binance.time_sync().is_initialized(),
980 "Time sync should be initialized after update"
981 );
982 assert!(
983 !binance.time_sync().needs_resync(),
984 "Time sync should not need resync immediately after update"
985 );
986
987 let offset = binance.time_sync().get_offset();
989 assert!(
990 offset >= 90 && offset <= 110,
991 "Offset should be approximately 100ms, got {}",
992 offset
993 );
994 }
995
996 #[tokio::test]
999 async fn test_execute_signed_request_with_retry_success() {
1000 let config = ExchangeConfig::default();
1001 let binance = Binance::new(config).unwrap();
1002
1003 let server_time = ccxt_core::time::TimestampUtils::now_ms();
1005 binance.time_sync().update_offset(server_time);
1006
1007 let result = binance
1009 .execute_signed_request_with_retry(|timestamp| async move {
1010 assert!(timestamp > 0, "Timestamp should be positive");
1011 Ok::<_, ccxt_core::Error>(42)
1012 })
1013 .await;
1014
1015 assert!(result.is_ok(), "Request should succeed");
1016 assert_eq!(result.unwrap(), 42);
1017 }
1018
1019 #[tokio::test]
1020 async fn test_execute_signed_request_with_retry_non_timestamp_error() {
1021 let config = ExchangeConfig::default();
1022 let binance = Binance::new(config).unwrap();
1023
1024 let server_time = ccxt_core::time::TimestampUtils::now_ms();
1026 binance.time_sync().update_offset(server_time);
1027
1028 let result = binance
1030 .execute_signed_request_with_retry(|_timestamp| async move {
1031 Err::<i32, _>(ccxt_core::Error::exchange("-1100", "Invalid parameter"))
1032 })
1033 .await;
1034
1035 assert!(result.is_err(), "Request should fail");
1036 let err = result.unwrap_err();
1037 assert!(
1038 err.to_string().contains("-1100"),
1039 "Error should contain original error code"
1040 );
1041 }
1042
1043 #[test]
1044 fn test_handle_timestamp_error_detection() {
1045 let config = ExchangeConfig::default();
1046 let binance = Binance::new(config).unwrap();
1047
1048 let timestamp_errors = vec![
1050 ccxt_core::Error::exchange(
1051 "-1021",
1052 "Timestamp for this request is outside of the recvWindow",
1053 ),
1054 ccxt_core::Error::exchange("-1021", "Timestamp is ahead of server time"),
1055 ccxt_core::Error::exchange("-1021", "Timestamp is behind server time"),
1056 ccxt_core::Error::exchange("-1021", "Some error with timestamp and recvwindow"),
1057 ];
1058
1059 for err in timestamp_errors {
1060 assert!(
1061 binance.is_timestamp_error(&err),
1062 "Should detect timestamp error: {}",
1063 err
1064 );
1065 }
1066
1067 let non_timestamp_errors = vec![
1069 ccxt_core::Error::exchange("-1100", "Invalid parameter"),
1070 ccxt_core::Error::exchange("-1000", "Unknown error"),
1071 ccxt_core::Error::authentication("Invalid API key"),
1072 ccxt_core::Error::network("Connection refused"),
1073 ccxt_core::Error::timeout("Request timed out"),
1074 ];
1075
1076 for err in non_timestamp_errors {
1077 assert!(
1078 !binance.is_timestamp_error(&err),
1079 "Should not detect as timestamp error: {}",
1080 err
1081 );
1082 }
1083 }
1084
1085 mod property_tests {
1088 use super::*;
1089 use proptest::prelude::*;
1090
1091 fn arb_exchange_config() -> impl Strategy<Value = ExchangeConfig> {
1093 (
1094 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))), )
1098 .prop_map(|(sandbox, api_key, secret)| ExchangeConfig {
1099 sandbox,
1100 api_key: api_key.map(ccxt_core::SecretString::new),
1101 secret: secret.map(ccxt_core::SecretString::new),
1102 ..Default::default()
1103 })
1104 }
1105
1106 proptest! {
1107 #![proptest_config(ProptestConfig::with_cases(100))]
1108
1109 #[test]
1116 fn prop_timeframes_non_empty(config in arb_exchange_config()) {
1117 let binance = Binance::new(config).expect("Should create Binance instance");
1118 let exchange: &dyn Exchange = &binance;
1119
1120 let timeframes = exchange.timeframes();
1121
1122 prop_assert!(!timeframes.is_empty(), "Timeframes should not be empty");
1124
1125 let mut seen = std::collections::HashSet::new();
1127 for tf in &timeframes {
1128 prop_assert!(
1129 seen.insert(*tf),
1130 "Timeframes should not contain duplicates: {:?}",
1131 tf
1132 );
1133 }
1134
1135 prop_assert!(
1137 timeframes.contains(&Timeframe::M1),
1138 "Should contain 1-minute timeframe"
1139 );
1140 prop_assert!(
1141 timeframes.contains(&Timeframe::H1),
1142 "Should contain 1-hour timeframe"
1143 );
1144 prop_assert!(
1145 timeframes.contains(&Timeframe::D1),
1146 "Should contain 1-day timeframe"
1147 );
1148 }
1149
1150 #[test]
1157 fn prop_backward_compatibility_metadata(config in arb_exchange_config()) {
1158 let binance = Binance::new(config).expect("Should create Binance instance");
1159
1160 let exchange: &dyn Exchange = &binance;
1162
1163 prop_assert_eq!(
1165 exchange.id(),
1166 Binance::id(&binance),
1167 "id() should be consistent between trait and direct call"
1168 );
1169
1170 prop_assert_eq!(
1172 exchange.name(),
1173 Binance::name(&binance),
1174 "name() should be consistent between trait and direct call"
1175 );
1176
1177 prop_assert_eq!(
1179 exchange.version(),
1180 Binance::version(&binance),
1181 "version() should be consistent between trait and direct call"
1182 );
1183
1184 prop_assert_eq!(
1186 exchange.certified(),
1187 Binance::certified(&binance),
1188 "certified() should be consistent between trait and direct call"
1189 );
1190
1191 prop_assert_eq!(
1193 exchange.rate_limit(),
1194 Binance::rate_limit(&binance),
1195 "rate_limit() should be consistent between trait and direct call"
1196 );
1197
1198 let trait_caps = exchange.capabilities();
1200 prop_assert!(trait_caps.fetch_markets(), "Should support fetch_markets");
1201 prop_assert!(trait_caps.fetch_ticker(), "Should support fetch_ticker");
1202 prop_assert!(trait_caps.websocket(), "Should support websocket");
1203 }
1204 }
1205 }
1206}