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
720#[cfg(test)]
721mod tests {
722 use super::*;
723 use ccxt_core::ExchangeConfig;
724
725 #[test]
726 fn test_binance_exchange_trait_metadata() {
727 let config = ExchangeConfig::default();
728 let binance = Binance::new(config).unwrap();
729
730 let exchange: &dyn Exchange = &binance;
732
733 assert_eq!(exchange.id(), "binance");
734 assert_eq!(exchange.name(), "Binance");
735 assert_eq!(exchange.version(), "v3");
736 assert!(exchange.certified());
737 assert!(exchange.has_websocket());
738 }
739
740 #[test]
741 fn test_binance_exchange_trait_capabilities() {
742 let config = ExchangeConfig::default();
743 let binance = Binance::new(config).unwrap();
744
745 let exchange: &dyn Exchange = &binance;
746 let caps = exchange.capabilities();
747
748 assert!(caps.fetch_markets());
749 assert!(caps.fetch_ticker());
750 assert!(caps.create_order());
751 assert!(caps.websocket());
752 assert!(!caps.edit_order()); }
754
755 #[test]
756 fn test_binance_exchange_trait_timeframes() {
757 let config = ExchangeConfig::default();
758 let binance = Binance::new(config).unwrap();
759
760 let exchange: &dyn Exchange = &binance;
761 let timeframes = exchange.timeframes();
762
763 assert!(!timeframes.is_empty());
764 assert!(timeframes.contains(&Timeframe::M1));
765 assert!(timeframes.contains(&Timeframe::H1));
766 assert!(timeframes.contains(&Timeframe::D1));
767 }
768
769 #[test]
770 fn test_binance_exchange_trait_object_safety() {
771 let config = ExchangeConfig::default();
772 let binance = Binance::new(config).unwrap();
773
774 let exchange: Box<dyn Exchange> = Box::new(binance);
776
777 assert_eq!(exchange.id(), "binance");
778 assert_eq!(exchange.rate_limit(), 50);
779 }
780
781 #[test]
782 fn test_binance_exchange_ext_trait() {
783 let config = ExchangeConfig::default();
784 let binance = Binance::new(config).unwrap();
785
786 let exchange: &dyn Exchange = &binance;
788
789 assert!(
791 exchange.supports_market_data(),
792 "Binance should support market data"
793 );
794 assert!(
795 exchange.supports_trading(),
796 "Binance should support trading"
797 );
798 assert!(
799 exchange.supports_account(),
800 "Binance should support account operations"
801 );
802 assert!(
803 exchange.supports_margin(),
804 "Binance should support margin operations"
805 );
806 assert!(
807 exchange.supports_funding(),
808 "Binance should support funding operations"
809 );
810 }
811
812 #[test]
813 fn test_binance_implements_both_exchange_and_public_exchange() {
814 let config = ExchangeConfig::default();
815 let binance = Binance::new(config).unwrap();
816
817 let exchange: &dyn Exchange = &binance;
819 let public_exchange: &dyn PublicExchange = &binance;
820
821 assert_eq!(exchange.id(), public_exchange.id());
823 assert_eq!(exchange.name(), public_exchange.name());
824 assert_eq!(exchange.version(), public_exchange.version());
825 assert_eq!(exchange.certified(), public_exchange.certified());
826 assert_eq!(exchange.rate_limit(), public_exchange.rate_limit());
827 assert_eq!(exchange.has_websocket(), public_exchange.has_websocket());
828 assert_eq!(exchange.timeframes(), public_exchange.timeframes());
829 }
830
831 #[test]
834 fn test_is_timestamp_error_with_recv_window_message() {
835 let config = ExchangeConfig::default();
836 let binance = Binance::new(config).unwrap();
837
838 let err = ccxt_core::Error::exchange(
840 "-1021",
841 "Timestamp for this request is outside of the recvWindow",
842 );
843 assert!(
844 binance.is_timestamp_error(&err),
845 "Should detect recvWindow timestamp error"
846 );
847 }
848
849 #[test]
850 fn test_is_timestamp_error_with_ahead_message() {
851 let config = ExchangeConfig::default();
852 let binance = Binance::new(config).unwrap();
853
854 let err = ccxt_core::Error::exchange("-1021", "Timestamp is ahead of server time");
856 assert!(
857 binance.is_timestamp_error(&err),
858 "Should detect 'ahead' timestamp error"
859 );
860 }
861
862 #[test]
863 fn test_is_timestamp_error_with_behind_message() {
864 let config = ExchangeConfig::default();
865 let binance = Binance::new(config).unwrap();
866
867 let err = ccxt_core::Error::exchange("-1021", "Timestamp is behind server time");
869 assert!(
870 binance.is_timestamp_error(&err),
871 "Should detect 'behind' timestamp error"
872 );
873 }
874
875 #[test]
876 fn test_is_timestamp_error_with_error_code_only() {
877 let config = ExchangeConfig::default();
878 let binance = Binance::new(config).unwrap();
879
880 let err = ccxt_core::Error::exchange("-1021", "Some error message");
882 assert!(
883 binance.is_timestamp_error(&err),
884 "Should detect error code -1021"
885 );
886 }
887
888 #[test]
889 fn test_is_timestamp_error_non_timestamp_error() {
890 let config = ExchangeConfig::default();
891 let binance = Binance::new(config).unwrap();
892
893 let err = ccxt_core::Error::exchange("-1100", "Illegal characters found in parameter");
895 assert!(
896 !binance.is_timestamp_error(&err),
897 "Should not detect non-timestamp error"
898 );
899
900 let err = ccxt_core::Error::authentication("Invalid API key");
902 assert!(
903 !binance.is_timestamp_error(&err),
904 "Should not detect authentication error as timestamp error"
905 );
906
907 let err = ccxt_core::Error::network("Connection refused");
909 assert!(
910 !binance.is_timestamp_error(&err),
911 "Should not detect network error as timestamp error"
912 );
913 }
914
915 #[test]
916 fn test_is_timestamp_error_case_insensitive() {
917 let config = ExchangeConfig::default();
918 let binance = Binance::new(config).unwrap();
919
920 let err = ccxt_core::Error::exchange(
922 "-1021",
923 "TIMESTAMP for this request is outside of the RECVWINDOW",
924 );
925 assert!(
926 binance.is_timestamp_error(&err),
927 "Should detect timestamp error case-insensitively"
928 );
929 }
930
931 #[test]
932 fn test_time_sync_manager_accessible() {
933 let config = ExchangeConfig::default();
934 let binance = Binance::new(config).unwrap();
935
936 let time_sync = binance.time_sync();
938 assert!(
939 !time_sync.is_initialized(),
940 "Time sync should not be initialized initially"
941 );
942 assert!(
943 time_sync.needs_resync(),
944 "Time sync should need resync initially"
945 );
946 }
947
948 #[test]
949 fn test_time_sync_manager_update_offset() {
950 let config = ExchangeConfig::default();
951 let binance = Binance::new(config).unwrap();
952
953 let server_time = ccxt_core::time::TimestampUtils::now_ms() + 100;
955 binance.time_sync().update_offset(server_time);
956
957 assert!(
958 binance.time_sync().is_initialized(),
959 "Time sync should be initialized after update"
960 );
961 assert!(
962 !binance.time_sync().needs_resync(),
963 "Time sync should not need resync immediately after update"
964 );
965
966 let offset = binance.time_sync().get_offset();
968 assert!(
969 offset >= 90 && offset <= 110,
970 "Offset should be approximately 100ms, got {}",
971 offset
972 );
973 }
974
975 #[tokio::test]
978 async fn test_execute_signed_request_with_retry_success() {
979 let config = ExchangeConfig::default();
980 let binance = Binance::new(config).unwrap();
981
982 let server_time = ccxt_core::time::TimestampUtils::now_ms();
984 binance.time_sync().update_offset(server_time);
985
986 let result = binance
988 .execute_signed_request_with_retry(|timestamp| async move {
989 assert!(timestamp > 0, "Timestamp should be positive");
990 Ok::<_, ccxt_core::Error>(42)
991 })
992 .await;
993
994 assert!(result.is_ok(), "Request should succeed");
995 assert_eq!(result.unwrap(), 42);
996 }
997
998 #[tokio::test]
999 async fn test_execute_signed_request_with_retry_non_timestamp_error() {
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 Err::<i32, _>(ccxt_core::Error::exchange("-1100", "Invalid parameter"))
1011 })
1012 .await;
1013
1014 assert!(result.is_err(), "Request should fail");
1015 let err = result.unwrap_err();
1016 assert!(
1017 err.to_string().contains("-1100"),
1018 "Error should contain original error code"
1019 );
1020 }
1021
1022 #[test]
1023 fn test_handle_timestamp_error_detection() {
1024 let config = ExchangeConfig::default();
1025 let binance = Binance::new(config).unwrap();
1026
1027 let timestamp_errors = vec![
1029 ccxt_core::Error::exchange(
1030 "-1021",
1031 "Timestamp for this request is outside of the recvWindow",
1032 ),
1033 ccxt_core::Error::exchange("-1021", "Timestamp is ahead of server time"),
1034 ccxt_core::Error::exchange("-1021", "Timestamp is behind server time"),
1035 ccxt_core::Error::exchange("-1021", "Some error with timestamp and recvwindow"),
1036 ];
1037
1038 for err in timestamp_errors {
1039 assert!(
1040 binance.is_timestamp_error(&err),
1041 "Should detect timestamp error: {}",
1042 err
1043 );
1044 }
1045
1046 let non_timestamp_errors = vec![
1048 ccxt_core::Error::exchange("-1100", "Invalid parameter"),
1049 ccxt_core::Error::exchange("-1000", "Unknown error"),
1050 ccxt_core::Error::authentication("Invalid API key"),
1051 ccxt_core::Error::network("Connection refused"),
1052 ccxt_core::Error::timeout("Request timed out"),
1053 ];
1054
1055 for err in non_timestamp_errors {
1056 assert!(
1057 !binance.is_timestamp_error(&err),
1058 "Should not detect as timestamp error: {}",
1059 err
1060 );
1061 }
1062 }
1063
1064 mod property_tests {
1067 use super::*;
1068 use proptest::prelude::*;
1069
1070 fn arb_exchange_config() -> impl Strategy<Value = ExchangeConfig> {
1072 (
1073 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))), )
1077 .prop_map(|(sandbox, api_key, secret)| ExchangeConfig {
1078 sandbox,
1079 api_key: api_key.map(ccxt_core::SecretString::new),
1080 secret: secret.map(ccxt_core::SecretString::new),
1081 ..Default::default()
1082 })
1083 }
1084
1085 proptest! {
1086 #![proptest_config(ProptestConfig::with_cases(100))]
1087
1088 #[test]
1095 fn prop_timeframes_non_empty(config in arb_exchange_config()) {
1096 let binance = Binance::new(config).expect("Should create Binance instance");
1097 let exchange: &dyn Exchange = &binance;
1098
1099 let timeframes = exchange.timeframes();
1100
1101 prop_assert!(!timeframes.is_empty(), "Timeframes should not be empty");
1103
1104 let mut seen = std::collections::HashSet::new();
1106 for tf in &timeframes {
1107 prop_assert!(
1108 seen.insert(tf.clone()),
1109 "Timeframes should not contain duplicates: {:?}",
1110 tf
1111 );
1112 }
1113
1114 prop_assert!(
1116 timeframes.contains(&Timeframe::M1),
1117 "Should contain 1-minute timeframe"
1118 );
1119 prop_assert!(
1120 timeframes.contains(&Timeframe::H1),
1121 "Should contain 1-hour timeframe"
1122 );
1123 prop_assert!(
1124 timeframes.contains(&Timeframe::D1),
1125 "Should contain 1-day timeframe"
1126 );
1127 }
1128
1129 #[test]
1136 fn prop_backward_compatibility_metadata(config in arb_exchange_config()) {
1137 let binance = Binance::new(config).expect("Should create Binance instance");
1138
1139 let exchange: &dyn Exchange = &binance;
1141
1142 prop_assert_eq!(
1144 exchange.id(),
1145 Binance::id(&binance),
1146 "id() should be consistent between trait and direct call"
1147 );
1148
1149 prop_assert_eq!(
1151 exchange.name(),
1152 Binance::name(&binance),
1153 "name() should be consistent between trait and direct call"
1154 );
1155
1156 prop_assert_eq!(
1158 exchange.version(),
1159 Binance::version(&binance),
1160 "version() should be consistent between trait and direct call"
1161 );
1162
1163 prop_assert_eq!(
1165 exchange.certified(),
1166 Binance::certified(&binance),
1167 "certified() should be consistent between trait and direct call"
1168 );
1169
1170 prop_assert_eq!(
1172 exchange.rate_limit(),
1173 Binance::rate_limit(&binance),
1174 "rate_limit() should be consistent between trait and direct call"
1175 );
1176
1177 let trait_caps = exchange.capabilities();
1179 prop_assert!(trait_caps.fetch_markets(), "Should support fetch_markets");
1180 prop_assert!(trait_caps.fetch_ticker(), "Should support fetch_ticker");
1181 prop_assert!(trait_caps.websocket(), "Should support websocket");
1182 }
1183 }
1184 }
1185}
1186
1187impl ccxt_core::signed_request::HasHttpClient for Binance {
1189 fn http_client(&self) -> &ccxt_core::http_client::HttpClient {
1190 &self.base().http_client
1191 }
1192
1193 fn base_url(&self) -> &'static str {
1194 ""
1196 }
1197}