1use async_trait::async_trait;
6use ccxt_core::{
7 Result,
8 exchange::{Exchange, ExchangeCapabilities},
9 types::{
10 Balance, Market, Ohlcv, Order, OrderBook, OrderSide, OrderType, Ticker, Timeframe, Trade,
11 },
12};
13use rust_decimal::Decimal;
14use std::collections::HashMap;
15
16use super::Binance;
17
18#[async_trait]
19impl Exchange for Binance {
20 fn id(&self) -> &str {
23 "binance"
24 }
25
26 fn name(&self) -> &str {
27 "Binance"
28 }
29
30 fn version(&self) -> &'static str {
31 "v3"
32 }
33
34 fn certified(&self) -> bool {
35 true
36 }
37
38 fn has_websocket(&self) -> bool {
39 true
40 }
41
42 fn capabilities(&self) -> ExchangeCapabilities {
43 ExchangeCapabilities {
44 fetch_markets: true,
46 fetch_currencies: true,
47 fetch_ticker: true,
48 fetch_tickers: true,
49 fetch_order_book: true,
50 fetch_trades: true,
51 fetch_ohlcv: true,
52 fetch_status: true,
53 fetch_time: true,
54
55 create_order: true,
57 create_market_order: true,
58 create_limit_order: true,
59 cancel_order: true,
60 cancel_all_orders: true,
61 edit_order: false, fetch_order: true,
63 fetch_orders: true,
64 fetch_open_orders: true,
65 fetch_closed_orders: true,
66 fetch_canceled_orders: false,
67
68 fetch_balance: true,
70 fetch_my_trades: true,
71 fetch_deposits: true,
72 fetch_withdrawals: true,
73 fetch_transactions: true,
74 fetch_ledger: true,
75
76 fetch_deposit_address: true,
78 create_deposit_address: true,
79 withdraw: true,
80 transfer: true,
81
82 fetch_borrow_rate: true,
84 fetch_borrow_rates: true,
85 fetch_funding_rate: true,
86 fetch_funding_rates: true,
87 fetch_positions: true,
88 set_leverage: true,
89 set_margin_mode: true,
90
91 websocket: true,
93 watch_ticker: true,
94 watch_tickers: true,
95 watch_order_book: true,
96 watch_trades: true,
97 watch_ohlcv: true,
98 watch_balance: true,
99 watch_orders: true,
100 watch_my_trades: true,
101 }
102 }
103
104 fn timeframes(&self) -> Vec<Timeframe> {
105 vec![
106 Timeframe::M1,
107 Timeframe::M3,
108 Timeframe::M5,
109 Timeframe::M15,
110 Timeframe::M30,
111 Timeframe::H1,
112 Timeframe::H2,
113 Timeframe::H4,
114 Timeframe::H6,
115 Timeframe::H8,
116 Timeframe::H12,
117 Timeframe::D1,
118 Timeframe::D3,
119 Timeframe::W1,
120 Timeframe::Mon1,
121 ]
122 }
123
124 fn rate_limit(&self) -> f64 {
125 50.0
126 }
127
128 async fn fetch_markets(&self) -> Result<Vec<Market>> {
131 Binance::fetch_markets(self).await
133 }
134
135 async fn load_markets(&self, reload: bool) -> Result<HashMap<String, Market>> {
136 Binance::load_markets(self, reload).await
138 }
139
140 async fn fetch_ticker(&self, symbol: &str) -> Result<Ticker> {
141 Binance::fetch_ticker(self, symbol, ()).await
143 }
144
145 async fn fetch_tickers(&self, symbols: Option<&[String]>) -> Result<Vec<Ticker>> {
146 let symbols_vec = symbols.map(|s| s.to_vec());
148 Binance::fetch_tickers(self, symbols_vec).await
149 }
150
151 async fn fetch_order_book(&self, symbol: &str, limit: Option<u32>) -> Result<OrderBook> {
152 Binance::fetch_order_book(self, symbol, limit).await
154 }
155
156 async fn fetch_trades(&self, symbol: &str, limit: Option<u32>) -> Result<Vec<Trade>> {
157 Binance::fetch_trades(self, symbol, limit).await
159 }
160
161 async fn fetch_ohlcv(
162 &self,
163 symbol: &str,
164 timeframe: Timeframe,
165 since: Option<i64>,
166 limit: Option<u32>,
167 ) -> Result<Vec<Ohlcv>> {
168 use ccxt_core::types::{Amount, Price};
169
170 let timeframe_str = timeframe.to_string();
172 let ohlcv_data =
173 Binance::fetch_ohlcv(self, symbol, &timeframe_str, since, limit, None).await?;
174
175 Ok(ohlcv_data
177 .into_iter()
178 .map(|o| Ohlcv {
179 timestamp: o.timestamp,
180 open: Price::from(Decimal::try_from(o.open).unwrap_or_default()),
181 high: Price::from(Decimal::try_from(o.high).unwrap_or_default()),
182 low: Price::from(Decimal::try_from(o.low).unwrap_or_default()),
183 close: Price::from(Decimal::try_from(o.close).unwrap_or_default()),
184 volume: Amount::from(Decimal::try_from(o.volume).unwrap_or_default()),
185 })
186 .collect())
187 }
188
189 async fn create_order(
192 &self,
193 symbol: &str,
194 order_type: OrderType,
195 side: OrderSide,
196 amount: Decimal,
197 price: Option<Decimal>,
198 ) -> Result<Order> {
199 let amount_f64 = amount.to_string().parse::<f64>().unwrap_or(0.0);
201 let price_f64 = price.map(|p| p.to_string().parse::<f64>().unwrap_or(0.0));
202
203 Binance::create_order(self, symbol, order_type, side, amount_f64, price_f64, None).await
204 }
205
206 async fn cancel_order(&self, id: &str, symbol: Option<&str>) -> Result<Order> {
207 let symbol_str = symbol.ok_or_else(|| {
210 ccxt_core::Error::invalid_request("Symbol is required for cancel_order on Binance")
211 })?;
212 Binance::cancel_order(self, id, symbol_str).await
213 }
214
215 async fn cancel_all_orders(&self, symbol: Option<&str>) -> Result<Vec<Order>> {
216 let symbol_str = symbol.ok_or_else(|| {
219 ccxt_core::Error::invalid_request("Symbol is required for cancel_all_orders on Binance")
220 })?;
221 Binance::cancel_all_orders(self, symbol_str).await
222 }
223
224 async fn fetch_order(&self, id: &str, symbol: Option<&str>) -> Result<Order> {
225 let symbol_str = symbol.ok_or_else(|| {
228 ccxt_core::Error::invalid_request("Symbol is required for fetch_order on Binance")
229 })?;
230 Binance::fetch_order(self, id, symbol_str).await
231 }
232
233 async fn fetch_open_orders(
234 &self,
235 symbol: Option<&str>,
236 _since: Option<i64>,
237 _limit: Option<u32>,
238 ) -> Result<Vec<Order>> {
239 Binance::fetch_open_orders(self, symbol).await
242 }
243
244 async fn fetch_closed_orders(
245 &self,
246 symbol: Option<&str>,
247 since: Option<i64>,
248 limit: Option<u32>,
249 ) -> Result<Vec<Order>> {
250 let since_u64 = since.map(|s| s as u64);
253 Binance::fetch_closed_orders(self, symbol, since_u64, limit).await
254 }
255
256 async fn fetch_balance(&self) -> Result<Balance> {
259 Binance::fetch_balance(self, None).await
261 }
262
263 async fn fetch_my_trades(
264 &self,
265 symbol: Option<&str>,
266 since: Option<i64>,
267 limit: Option<u32>,
268 ) -> Result<Vec<Trade>> {
269 let symbol_str = symbol.ok_or_else(|| {
272 ccxt_core::Error::invalid_request("Symbol is required for fetch_my_trades on Binance")
273 })?;
274 let since_u64 = since.map(|s| s as u64);
275 Binance::fetch_my_trades(self, symbol_str, since_u64, limit).await
276 }
277
278 async fn market(&self, symbol: &str) -> Result<Market> {
281 let cache = self.base().market_cache.read().await;
283
284 if !cache.loaded {
285 return Err(ccxt_core::Error::exchange(
286 "-1",
287 "Markets not loaded. Call load_markets() first.",
288 ));
289 }
290
291 cache
292 .markets
293 .get(symbol)
294 .cloned()
295 .ok_or_else(|| ccxt_core::Error::bad_symbol(format!("Market {} not found", symbol)))
296 }
297
298 async fn markets(&self) -> HashMap<String, Market> {
299 let cache = self.base().market_cache.read().await;
301 cache.markets.clone()
302 }
303}
304
305#[cfg(test)]
306mod tests {
307 use super::*;
308 use ccxt_core::ExchangeConfig;
309
310 #[test]
311 fn test_binance_exchange_trait_metadata() {
312 let config = ExchangeConfig::default();
313 let binance = Binance::new(config).unwrap();
314
315 let exchange: &dyn Exchange = &binance;
317
318 assert_eq!(exchange.id(), "binance");
319 assert_eq!(exchange.name(), "Binance");
320 assert_eq!(exchange.version(), "v3");
321 assert!(exchange.certified());
322 assert!(exchange.has_websocket());
323 }
324
325 #[test]
326 fn test_binance_exchange_trait_capabilities() {
327 let config = ExchangeConfig::default();
328 let binance = Binance::new(config).unwrap();
329
330 let exchange: &dyn Exchange = &binance;
331 let caps = exchange.capabilities();
332
333 assert!(caps.fetch_markets);
334 assert!(caps.fetch_ticker);
335 assert!(caps.create_order);
336 assert!(caps.websocket);
337 assert!(!caps.edit_order); }
339
340 #[test]
341 fn test_binance_exchange_trait_timeframes() {
342 let config = ExchangeConfig::default();
343 let binance = Binance::new(config).unwrap();
344
345 let exchange: &dyn Exchange = &binance;
346 let timeframes = exchange.timeframes();
347
348 assert!(!timeframes.is_empty());
349 assert!(timeframes.contains(&Timeframe::M1));
350 assert!(timeframes.contains(&Timeframe::H1));
351 assert!(timeframes.contains(&Timeframe::D1));
352 }
353
354 #[test]
355 fn test_binance_exchange_trait_object_safety() {
356 let config = ExchangeConfig::default();
357 let binance = Binance::new(config).unwrap();
358
359 let exchange: Box<dyn Exchange> = Box::new(binance);
361
362 assert_eq!(exchange.id(), "binance");
363 assert_eq!(exchange.rate_limit(), 50.0);
364 }
365
366 mod property_tests {
369 use super::*;
370 use proptest::prelude::*;
371
372 fn arb_exchange_config() -> impl Strategy<Value = ExchangeConfig> {
374 (
375 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))), )
379 .prop_map(|(sandbox, api_key, secret)| ExchangeConfig {
380 sandbox,
381 api_key,
382 secret,
383 ..Default::default()
384 })
385 }
386
387 proptest! {
388 #![proptest_config(ProptestConfig::with_cases(100))]
389
390 #[test]
397 fn prop_timeframes_non_empty(config in arb_exchange_config()) {
398 let binance = Binance::new(config).expect("Should create Binance instance");
399 let exchange: &dyn Exchange = &binance;
400
401 let timeframes = exchange.timeframes();
402
403 prop_assert!(!timeframes.is_empty(), "Timeframes should not be empty");
405
406 let mut seen = std::collections::HashSet::new();
408 for tf in &timeframes {
409 prop_assert!(
410 seen.insert(tf.clone()),
411 "Timeframes should not contain duplicates: {:?}",
412 tf
413 );
414 }
415
416 prop_assert!(
418 timeframes.contains(&Timeframe::M1),
419 "Should contain 1-minute timeframe"
420 );
421 prop_assert!(
422 timeframes.contains(&Timeframe::H1),
423 "Should contain 1-hour timeframe"
424 );
425 prop_assert!(
426 timeframes.contains(&Timeframe::D1),
427 "Should contain 1-day timeframe"
428 );
429 }
430
431 #[test]
438 fn prop_backward_compatibility_metadata(config in arb_exchange_config()) {
439 let binance = Binance::new(config).expect("Should create Binance instance");
440
441 let exchange: &dyn Exchange = &binance;
443
444 prop_assert_eq!(
446 exchange.id(),
447 Binance::id(&binance),
448 "id() should be consistent between trait and direct call"
449 );
450
451 prop_assert_eq!(
453 exchange.name(),
454 Binance::name(&binance),
455 "name() should be consistent between trait and direct call"
456 );
457
458 prop_assert_eq!(
460 exchange.version(),
461 Binance::version(&binance),
462 "version() should be consistent between trait and direct call"
463 );
464
465 prop_assert_eq!(
467 exchange.certified(),
468 Binance::certified(&binance),
469 "certified() should be consistent between trait and direct call"
470 );
471
472 prop_assert!(
474 (exchange.rate_limit() - Binance::rate_limit(&binance)).abs() < f64::EPSILON,
475 "rate_limit() should be consistent between trait and direct call"
476 );
477
478 let trait_caps = exchange.capabilities();
480 prop_assert!(trait_caps.fetch_markets, "Should support fetch_markets");
481 prop_assert!(trait_caps.fetch_ticker, "Should support fetch_ticker");
482 prop_assert!(trait_caps.websocket, "Should support websocket");
483 }
484 }
485 }
486}