1use anyhow::{Context, Result};
2use hmac::{Hmac, Mac};
3use sha2::Sha256;
4use std::sync::atomic::{AtomicI64, AtomicU64, Ordering};
5use std::time::Instant;
6
7use crate::error::AppError;
8use crate::model::candle::Candle;
9use crate::model::order::OrderSide;
10
11use super::types::{
12 AccountInfo, BinanceAllOrder, BinanceFuturesAccountInfo, BinanceFuturesAllOrder,
13 BinanceFuturesOrderResponse, BinanceFuturesUserTrade, BinanceMyTrade, BinanceOrderResponse,
14 ServerTimeResponse,
15};
16
17#[derive(Debug, Clone, Copy)]
18pub struct SymbolOrderRules {
19 pub min_qty: f64,
20 pub max_qty: f64,
21 pub step_size: f64,
22 pub min_notional: Option<f64>,
23}
24
25pub struct BinanceRestClient {
26 http: reqwest::Client,
27 base_url: String,
28 futures_base_url: String,
29 api_key: String,
30 secret_key: String,
31 futures_api_key: String,
32 futures_secret_key: String,
33 recv_window: u64,
34 time_offset_ms: AtomicI64,
35 request_count: AtomicU64,
37 window_start: std::sync::Mutex<Instant>,
38}
39
40impl BinanceRestClient {
41 pub fn new(
42 base_url: &str,
43 futures_base_url: &str,
44 api_key: &str,
45 secret_key: &str,
46 futures_api_key: &str,
47 futures_secret_key: &str,
48 recv_window: u64,
49 ) -> Self {
50 Self {
51 http: reqwest::Client::new(),
52 base_url: base_url.to_string(),
53 futures_base_url: futures_base_url.to_string(),
54 api_key: api_key.to_string(),
55 secret_key: secret_key.to_string(),
56 futures_api_key: futures_api_key.to_string(),
57 futures_secret_key: futures_secret_key.to_string(),
58 recv_window,
59 time_offset_ms: AtomicI64::new(0),
60 request_count: AtomicU64::new(0),
61 window_start: std::sync::Mutex::new(Instant::now()),
62 }
63 }
64
65 fn sign_with_secret(&self, query: &str, secret_key: &str) -> String {
66 let offset = self.time_offset_ms.load(Ordering::Relaxed);
67 let timestamp = chrono::Utc::now().timestamp_millis() + offset;
68 let full_query = if query.is_empty() {
69 format!("recvWindow={}×tamp={}", self.recv_window, timestamp)
70 } else {
71 format!(
72 "{}&recvWindow={}×tamp={}",
73 query, self.recv_window, timestamp
74 )
75 };
76 let mut mac =
77 Hmac::<Sha256>::new_from_slice(secret_key.as_bytes()).expect("HMAC key error");
78 mac.update(full_query.as_bytes());
79 let signature = hex::encode(mac.finalize().into_bytes());
80 format!("{}&signature={}", full_query, signature)
81 }
82
83 fn sign(&self, query: &str) -> String {
84 self.sign_with_secret(query, &self.secret_key)
85 }
86
87 fn sign_futures(&self, query: &str) -> String {
88 self.sign_with_secret(query, &self.futures_secret_key)
89 }
90
91 async fn sync_time_offset(&self) -> Result<()> {
92 let server_ms = self.server_time().await? as i64;
93 let local_ms = chrono::Utc::now().timestamp_millis();
94 let offset = server_ms - local_ms;
95 self.time_offset_ms.store(offset, Ordering::Relaxed);
96 tracing::warn!(
97 offset_ms = offset,
98 "Synchronized Binance server time offset"
99 );
100 Ok(())
101 }
102
103 fn parse_binance_api_error(body: &str) -> Option<super::types::BinanceApiErrorResponse> {
104 serde_json::from_str::<super::types::BinanceApiErrorResponse>(body).ok()
105 }
106
107 fn check_rate_limit(&self) {
108 let mut start = match self.window_start.lock() {
109 Ok(guard) => guard,
110 Err(poisoned) => {
111 tracing::error!("rate-limit mutex poisoned; continuing with recovered state");
112 poisoned.into_inner()
113 }
114 };
115 if start.elapsed().as_secs() >= 60 {
116 *start = Instant::now();
117 self.request_count.store(0, Ordering::Relaxed);
118 }
119 let count = self.request_count.fetch_add(1, Ordering::Relaxed);
120 if count > 960 {
121 tracing::warn!(count, "Approaching rate limit (80% of 1200/min)");
122 }
123 }
124
125 pub async fn ping(&self) -> Result<()> {
126 let url = format!("{}/api/v3/ping", self.base_url);
127 self.http
128 .get(&url)
129 .send()
130 .await
131 .context("ping failed")?
132 .error_for_status()
133 .context("ping returned error status")?;
134 Ok(())
135 }
136
137 pub async fn server_time(&self) -> Result<u64> {
138 let url = format!("{}/api/v3/time", self.base_url);
139 let resp: ServerTimeResponse = self
140 .http
141 .get(&url)
142 .send()
143 .await
144 .context("server_time failed")?
145 .json()
146 .await?;
147 Ok(resp.server_time)
148 }
149
150 pub async fn get_account(&self) -> Result<AccountInfo> {
151 self.check_rate_limit();
152
153 let signed = self.sign("");
154 let url = format!("{}/api/v3/account?{}", self.base_url, signed);
155
156 let resp = self
157 .http
158 .get(&url)
159 .header("X-MBX-APIKEY", &self.api_key)
160 .send()
161 .await
162 .context("get_account HTTP failed")?;
163
164 if !resp.status().is_success() {
165 let body = resp.text().await.unwrap_or_default();
166 if let Ok(err) = serde_json::from_str::<super::types::BinanceApiErrorResponse>(&body) {
167 return Err(AppError::BinanceApi {
168 code: err.code,
169 msg: err.msg,
170 }
171 .into());
172 }
173 return Err(anyhow::anyhow!("Account request failed: {}", body));
174 }
175
176 Ok(resp.json().await?)
177 }
178
179 pub async fn get_futures_account(&self) -> Result<BinanceFuturesAccountInfo> {
180 self.check_rate_limit();
181
182 let signed = self.sign_futures("");
183 let url = format!("{}/fapi/v2/account?{}", self.futures_base_url, signed);
184
185 let resp = self
186 .http
187 .get(&url)
188 .header("X-MBX-APIKEY", &self.futures_api_key)
189 .send()
190 .await
191 .context("get_futures_account HTTP failed")?;
192
193 if !resp.status().is_success() {
194 let body = resp.text().await.unwrap_or_default();
195 if let Ok(err) = serde_json::from_str::<super::types::BinanceApiErrorResponse>(&body) {
196 return Err(AppError::BinanceApi {
197 code: err.code,
198 msg: err.msg,
199 }
200 .into());
201 }
202 return Err(anyhow::anyhow!("Futures account request failed: {}", body));
203 }
204
205 Ok(resp.json().await?)
206 }
207
208 pub async fn place_market_order(
209 &self,
210 symbol: &str,
211 side: OrderSide,
212 quantity: f64,
213 client_order_id: &str,
214 ) -> Result<BinanceOrderResponse> {
215 self.check_rate_limit();
216
217 let query = format!(
218 "symbol={}&side={}&type=MARKET&quantity={:.5}&newClientOrderId={}&newOrderRespType=FULL",
219 symbol,
220 side.as_binance_str(),
221 quantity,
222 client_order_id,
223 );
224 let signed = self.sign(&query);
225 let url = format!("{}/api/v3/order?{}", self.base_url, signed);
226
227 tracing::info!(
228 symbol,
229 side = %side,
230 quantity,
231 client_order_id,
232 "Placing market order"
233 );
234
235 let resp = self
236 .http
237 .post(&url)
238 .header("X-MBX-APIKEY", &self.api_key)
239 .send()
240 .await
241 .context("place_market_order HTTP failed")?;
242
243 if !resp.status().is_success() {
244 let body = resp.text().await.unwrap_or_default();
245 if let Ok(err) = serde_json::from_str::<super::types::BinanceApiErrorResponse>(&body) {
246 return Err(AppError::BinanceApi {
247 code: err.code,
248 msg: err.msg,
249 }
250 .into());
251 }
252 return Err(anyhow::anyhow!("Order request failed: {}", body));
253 }
254
255 let order: BinanceOrderResponse = resp.json().await?;
256 tracing::info!(
257 order_id = order.order_id,
258 status = %order.status,
259 client_order_id = %order.client_order_id,
260 "Order response received"
261 );
262 Ok(order)
263 }
264
265 pub async fn place_futures_market_order(
266 &self,
267 symbol: &str,
268 side: OrderSide,
269 quantity: f64,
270 client_order_id: &str,
271 ) -> Result<BinanceOrderResponse> {
272 self.check_rate_limit();
273
274 let query = format!(
275 "symbol={}&side={}&type=MARKET&quantity={:.5}&newClientOrderId={}&newOrderRespType=RESULT",
276 symbol,
277 side.as_binance_str(),
278 quantity,
279 client_order_id,
280 );
281 let signed = self.sign_futures(&query);
282 let url = format!("{}/fapi/v1/order?{}", self.futures_base_url, signed);
283
284 tracing::info!(
285 symbol,
286 side = %side,
287 quantity,
288 client_order_id,
289 "Placing futures market order"
290 );
291
292 let resp = self
293 .http
294 .post(&url)
295 .header("X-MBX-APIKEY", &self.futures_api_key)
296 .send()
297 .await
298 .context("place_futures_market_order HTTP failed")?;
299
300 if !resp.status().is_success() {
301 let body = resp.text().await.unwrap_or_default();
302 if let Ok(err) = serde_json::from_str::<super::types::BinanceApiErrorResponse>(&body) {
303 return Err(AppError::BinanceApi {
304 code: err.code,
305 msg: err.msg,
306 }
307 .into());
308 }
309 return Err(anyhow::anyhow!("Futures order request failed: {}", body));
310 }
311
312 let fut: BinanceFuturesOrderResponse = resp.json().await?;
313 let avg = if fut.avg_price > 0.0 {
314 fut.avg_price
315 } else if fut.price > 0.0 {
316 fut.price
317 } else {
318 0.0
319 };
320 let fills = if fut.executed_qty > 0.0 && avg > 0.0 {
321 vec![super::types::BinanceFill {
322 price: avg,
323 qty: fut.executed_qty,
324 commission: 0.0,
325 commission_asset: "USDT".to_string(),
326 }]
327 } else {
328 Vec::new()
329 };
330
331 Ok(BinanceOrderResponse {
332 symbol: fut.symbol,
333 order_id: fut.order_id,
334 client_order_id: fut.client_order_id,
335 price: if fut.price > 0.0 { fut.price } else { avg },
336 orig_qty: fut.orig_qty,
337 executed_qty: fut.executed_qty,
338 status: fut.status,
339 r#type: fut.r#type,
340 side: fut.side,
341 fills,
342 })
343 }
344
345 pub async fn get_spot_symbol_order_rules(&self, symbol: &str) -> Result<SymbolOrderRules> {
346 let url = format!("{}/api/v3/exchangeInfo?symbol={}", self.base_url, symbol);
347 let payload: serde_json::Value = self
348 .http
349 .get(&url)
350 .send()
351 .await
352 .context("get_spot_symbol_order_rules HTTP failed")?
353 .error_for_status()
354 .context("get_spot_symbol_order_rules returned error status")?
355 .json()
356 .await
357 .context("get_spot_symbol_order_rules JSON parse failed")?;
358 parse_symbol_order_rules_from_exchange_info(&payload, symbol, true)
359 }
360
361 pub async fn get_futures_symbol_order_rules(&self, symbol: &str) -> Result<SymbolOrderRules> {
362 let url = format!(
363 "{}/fapi/v1/exchangeInfo?symbol={}",
364 self.futures_base_url, symbol
365 );
366 let payload: serde_json::Value = self
367 .http
368 .get(&url)
369 .send()
370 .await
371 .context("get_futures_symbol_order_rules HTTP failed")?
372 .error_for_status()
373 .context("get_futures_symbol_order_rules returned error status")?
374 .json()
375 .await
376 .context("get_futures_symbol_order_rules JSON parse failed")?;
377 parse_symbol_order_rules_from_exchange_info(&payload, symbol, false)
378 }
379
380 pub async fn get_klines(
383 &self,
384 symbol: &str,
385 interval: &str,
386 limit: usize,
387 ) -> Result<Vec<Candle>> {
388 self.get_klines_for_market(symbol, interval, limit, false)
389 .await
390 }
391
392 pub async fn get_klines_for_market(
393 &self,
394 symbol: &str,
395 interval: &str,
396 limit: usize,
397 is_futures: bool,
398 ) -> Result<Vec<Candle>> {
399 self.check_rate_limit();
400
401 let url = if is_futures {
402 format!(
403 "{}/fapi/v1/klines?symbol={}&interval={}&limit={}",
404 self.futures_base_url, symbol, interval, limit,
405 )
406 } else {
407 format!(
408 "{}/api/v3/klines?symbol={}&interval={}&limit={}",
409 self.base_url, symbol, interval, limit,
410 )
411 };
412
413 let resp: Vec<Vec<serde_json::Value>> = self
414 .http
415 .get(&url)
416 .send()
417 .await
418 .context("get_klines HTTP failed")?
419 .error_for_status()
420 .context("get_klines returned error status")?
421 .json()
422 .await
423 .context("get_klines JSON parse failed")?;
424
425 let candles: Vec<Candle> = resp
426 .iter()
427 .filter_map(|kline| {
428 let open_time = kline.get(0)?.as_u64()?;
429 let open = kline.get(1)?.as_str()?.parse::<f64>().ok()?;
430 let high = kline.get(2)?.as_str()?.parse::<f64>().ok()?;
431 let low = kline.get(3)?.as_str()?.parse::<f64>().ok()?;
432 let close = kline.get(4)?.as_str()?.parse::<f64>().ok()?;
433 let close_time = kline
435 .get(6)?
436 .as_u64()
437 .map(|v| v.saturating_add(1))
438 .unwrap_or(open_time.saturating_add(60_000));
439 Some(Candle {
440 open,
441 high,
442 low,
443 close,
444 open_time,
445 close_time,
446 })
447 })
448 .collect();
449
450 Ok(candles)
451 }
452
453 pub async fn cancel_order(
454 &self,
455 symbol: &str,
456 client_order_id: &str,
457 ) -> Result<BinanceOrderResponse> {
458 self.check_rate_limit();
459
460 let query = format!("symbol={}&origClientOrderId={}", symbol, client_order_id);
461 let signed = self.sign(&query);
462 let url = format!("{}/api/v3/order?{}", self.base_url, signed);
463
464 tracing::info!(symbol, client_order_id, "Cancelling order");
465
466 let resp = self
467 .http
468 .delete(&url)
469 .header("X-MBX-APIKEY", &self.api_key)
470 .send()
471 .await
472 .context("cancel_order HTTP failed")?;
473
474 if !resp.status().is_success() {
475 let body = resp.text().await.unwrap_or_default();
476 if let Ok(err) = serde_json::from_str::<super::types::BinanceApiErrorResponse>(&body) {
477 return Err(AppError::BinanceApi {
478 code: err.code,
479 msg: err.msg,
480 }
481 .into());
482 }
483 return Err(anyhow::anyhow!("Cancel request failed: {}", body));
484 }
485
486 Ok(resp.json().await?)
487 }
488
489 async fn get_all_orders_page(
491 &self,
492 symbol: &str,
493 limit: usize,
494 from_order_id: Option<u64>,
495 ) -> Result<Vec<BinanceAllOrder>> {
496 self.check_rate_limit();
497
498 let limit = limit.clamp(1, 1000);
499 let query = match from_order_id {
500 Some(order_id) => format!("symbol={}&limit={}&orderId={}", symbol, limit, order_id),
501 None => format!("symbol={}&limit={}", symbol, limit),
502 };
503 for attempt in 0..=1 {
504 let signed = self.sign(&query);
505 let url = format!("{}/api/v3/allOrders?{}", self.base_url, signed);
506
507 let resp = self
508 .http
509 .get(&url)
510 .header("X-MBX-APIKEY", &self.api_key)
511 .send()
512 .await
513 .context("get_all_orders HTTP failed")?;
514
515 if resp.status().is_success() {
516 return Ok(resp.json().await?);
517 }
518
519 let body = resp.text().await.unwrap_or_default();
520 if let Some(err) = Self::parse_binance_api_error(&body) {
521 if err.code == -1021 && attempt == 0 {
522 tracing::warn!("allOrders got -1021; syncing server time and retrying once");
523 self.sync_time_offset().await?;
524 continue;
525 }
526 return Err(AppError::BinanceApi {
527 code: err.code,
528 msg: err.msg,
529 }
530 .into());
531 }
532 return Err(anyhow::anyhow!("All orders request failed: {}", body));
533 }
534
535 Err(anyhow::anyhow!("All orders request failed after retry"))
536 }
537
538 pub async fn get_all_orders(&self, symbol: &str, limit: usize) -> Result<Vec<BinanceAllOrder>> {
541 self.get_all_orders_page(symbol, limit, None).await
542 }
543
544 async fn get_futures_all_orders_page(
545 &self,
546 symbol: &str,
547 limit: usize,
548 from_order_id: Option<u64>,
549 ) -> Result<Vec<BinanceAllOrder>> {
550 self.check_rate_limit();
551 let limit = limit.clamp(1, 1000);
552 let query = match from_order_id {
553 Some(order_id) => format!("symbol={}&limit={}&orderId={}", symbol, limit, order_id),
554 None => format!("symbol={}&limit={}", symbol, limit),
555 };
556 let signed = self.sign_futures(&query);
557 let url = format!("{}/fapi/v1/allOrders?{}", self.futures_base_url, signed);
558 let resp = self
559 .http
560 .get(&url)
561 .header("X-MBX-APIKEY", &self.futures_api_key)
562 .send()
563 .await
564 .context("get_futures_all_orders HTTP failed")?;
565 if !resp.status().is_success() {
566 let body = resp.text().await.unwrap_or_default();
567 if let Some(err) = Self::parse_binance_api_error(&body) {
568 return Err(AppError::BinanceApi {
569 code: err.code,
570 msg: err.msg,
571 }
572 .into());
573 }
574 return Err(anyhow::anyhow!(
575 "Futures allOrders request failed: {}",
576 body
577 ));
578 }
579 let rows: Vec<BinanceFuturesAllOrder> = resp.json().await?;
580 Ok(rows
581 .into_iter()
582 .map(|o| {
583 let cumm_quote = if o.cum_quote > 0.0 {
584 o.cum_quote
585 } else {
586 o.avg_price * o.executed_qty
587 };
588 BinanceAllOrder {
589 symbol: o.symbol,
590 order_id: o.order_id,
591 client_order_id: o.client_order_id,
592 price: o.price,
593 orig_qty: o.orig_qty,
594 executed_qty: o.executed_qty,
595 cummulative_quote_qty: cumm_quote,
596 status: o.status,
597 r#type: o.r#type,
598 side: o.side,
599 time: o.time,
600 update_time: o.update_time,
601 }
602 })
603 .collect())
604 }
605
606 pub async fn get_futures_all_orders(
607 &self,
608 symbol: &str,
609 limit: usize,
610 ) -> Result<Vec<BinanceAllOrder>> {
611 self.get_futures_all_orders_page(symbol, limit, None).await
612 }
613
614 async fn get_my_trades_page(
615 &self,
616 symbol: &str,
617 limit: usize,
618 from_id: Option<u64>,
619 ) -> Result<Vec<BinanceMyTrade>> {
620 self.check_rate_limit();
621
622 let limit = limit.clamp(1, 1000);
623 let query = match from_id {
624 Some(v) => format!("symbol={}&limit={}&fromId={}", symbol, limit, v),
625 None => format!("symbol={}&limit={}", symbol, limit),
626 };
627 for attempt in 0..=1 {
628 let signed = self.sign(&query);
629 let url = format!("{}/api/v3/myTrades?{}", self.base_url, signed);
630
631 let resp = self
632 .http
633 .get(&url)
634 .header("X-MBX-APIKEY", &self.api_key)
635 .send()
636 .await
637 .context("get_my_trades HTTP failed")?;
638
639 if resp.status().is_success() {
640 return Ok(resp.json().await?);
641 }
642
643 let body = resp.text().await.unwrap_or_default();
644 if let Some(err) = Self::parse_binance_api_error(&body) {
645 if err.code == -1021 && attempt == 0 {
646 tracing::warn!("myTrades got -1021; syncing server time and retrying once");
647 self.sync_time_offset().await?;
648 continue;
649 }
650 return Err(AppError::BinanceApi {
651 code: err.code,
652 msg: err.msg,
653 }
654 .into());
655 }
656 return Err(anyhow::anyhow!("My trades request failed: {}", body));
657 }
658
659 Err(anyhow::anyhow!("My trades request failed after retry"))
660 }
661
662 pub async fn get_my_trades(&self, symbol: &str, limit: usize) -> Result<Vec<BinanceMyTrade>> {
664 self.get_my_trades_page(symbol, limit, None).await
665 }
666
667 pub async fn get_my_trades_history(
669 &self,
670 symbol: &str,
671 max_total: usize,
672 ) -> Result<Vec<BinanceMyTrade>> {
673 let page_size = 1000usize;
674 let target = max_total.max(1);
675 let mut out = Vec::new();
676 let mut cursor: u64 = 0;
677
678 loop {
679 let page = self
680 .get_my_trades_page(
681 symbol,
682 page_size.min(target.saturating_sub(out.len())),
683 Some(cursor),
684 )
685 .await?;
686 if page.is_empty() {
687 break;
688 }
689 let fetched = page.len();
690 let mut max_trade_id = cursor;
691 for t in page {
692 max_trade_id = max_trade_id.max(t.id);
693 out.push(t);
694 if out.len() >= target {
695 break;
696 }
697 }
698 if out.len() >= target || fetched < page_size {
699 break;
700 }
701 let next = max_trade_id.saturating_add(1);
702 if next <= cursor {
703 break;
704 }
705 cursor = next;
706 }
707
708 Ok(out)
709 }
710
711 pub async fn get_my_trades_since(
713 &self,
714 symbol: &str,
715 from_id: u64,
716 max_pages: usize,
717 ) -> Result<Vec<BinanceMyTrade>> {
718 let page_size = 1000usize;
719 let mut out = Vec::new();
720 let mut cursor = from_id;
721 let mut pages = 0usize;
722
723 while pages < max_pages.max(1) {
724 let page = self
725 .get_my_trades_page(symbol, page_size, Some(cursor))
726 .await?;
727 if page.is_empty() {
728 break;
729 }
730 pages += 1;
731 let fetched = page.len();
732 let mut max_trade_id = cursor;
733 for t in page {
734 max_trade_id = max_trade_id.max(t.id);
735 out.push(t);
736 }
737 if fetched < page_size {
738 break;
739 }
740 let next = max_trade_id.saturating_add(1);
741 if next <= cursor {
742 break;
743 }
744 cursor = next;
745 }
746
747 Ok(out)
748 }
749
750 async fn get_futures_my_trades_page(
751 &self,
752 symbol: &str,
753 limit: usize,
754 from_id: Option<u64>,
755 ) -> Result<Vec<BinanceMyTrade>> {
756 self.check_rate_limit();
757 let limit = limit.clamp(1, 1000);
758 let query = match from_id {
759 Some(v) => format!("symbol={}&limit={}&fromId={}", symbol, limit, v),
760 None => format!("symbol={}&limit={}", symbol, limit),
761 };
762 let signed = self.sign_futures(&query);
763 let url = format!("{}/fapi/v1/userTrades?{}", self.futures_base_url, signed);
764 let resp = self
765 .http
766 .get(&url)
767 .header("X-MBX-APIKEY", &self.futures_api_key)
768 .send()
769 .await
770 .context("get_futures_my_trades HTTP failed")?;
771 if !resp.status().is_success() {
772 let body = resp.text().await.unwrap_or_default();
773 if let Some(err) = Self::parse_binance_api_error(&body) {
774 return Err(AppError::BinanceApi {
775 code: err.code,
776 msg: err.msg,
777 }
778 .into());
779 }
780 return Err(anyhow::anyhow!("Futures myTrades request failed: {}", body));
781 }
782 let rows: Vec<BinanceFuturesUserTrade> = resp.json().await?;
783 Ok(rows
784 .into_iter()
785 .map(|t| BinanceMyTrade {
786 symbol: t.symbol,
787 id: t.id,
788 order_id: t.order_id,
789 price: t.price,
790 qty: t.qty,
791 commission: t.commission,
792 commission_asset: t.commission_asset,
793 time: t.time,
794 is_buyer: t.buyer,
795 is_maker: t.maker,
796 realized_pnl: t.realized_pnl,
797 })
798 .collect())
799 }
800
801 pub async fn get_futures_my_trades_history(
802 &self,
803 symbol: &str,
804 max_total: usize,
805 ) -> Result<Vec<BinanceMyTrade>> {
806 let page_size = 1000usize;
807 let target = max_total.max(1);
808 let mut out = Vec::new();
809 let mut cursor: u64 = 0;
810 loop {
811 let page = self
812 .get_futures_my_trades_page(
813 symbol,
814 page_size.min(target.saturating_sub(out.len())),
815 Some(cursor),
816 )
817 .await?;
818 if page.is_empty() {
819 break;
820 }
821 let fetched = page.len();
822 let mut max_trade_id = cursor;
823 for t in page {
824 max_trade_id = max_trade_id.max(t.id);
825 out.push(t);
826 if out.len() >= target {
827 break;
828 }
829 }
830 if out.len() >= target || fetched < page_size {
831 break;
832 }
833 let next = max_trade_id.saturating_add(1);
834 if next <= cursor {
835 break;
836 }
837 cursor = next;
838 }
839 Ok(out)
840 }
841}
842
843fn parse_symbol_order_rules_from_exchange_info(
844 payload: &serde_json::Value,
845 symbol: &str,
846 prefer_market_lot_size: bool,
847) -> Result<SymbolOrderRules> {
848 let symbols = payload
849 .get("symbols")
850 .and_then(|v| v.as_array())
851 .context("exchangeInfo missing symbols")?;
852 let symbol_row = symbols
853 .iter()
854 .find(|row| row.get("symbol").and_then(|v| v.as_str()) == Some(symbol))
855 .with_context(|| format!("exchangeInfo symbol not found: {}", symbol))?;
856 let filters = symbol_row
857 .get("filters")
858 .and_then(|v| v.as_array())
859 .context("exchangeInfo symbol missing filters")?;
860
861 let primary_type = if prefer_market_lot_size {
862 "MARKET_LOT_SIZE"
863 } else {
864 "LOT_SIZE"
865 };
866 let fallback_type = if prefer_market_lot_size {
867 "LOT_SIZE"
868 } else {
869 "MARKET_LOT_SIZE"
870 };
871 let parsed = find_filter(filters, primary_type)
872 .and_then(parse_lot_filter_values)
873 .or_else(|| find_filter(filters, fallback_type).and_then(parse_lot_filter_values))
874 .context("exchangeInfo missing valid LOT_SIZE/MARKET_LOT_SIZE")?;
875 let (min_qty, max_qty, step_size) = parsed;
876
877 let min_notional = find_filter(filters, "MIN_NOTIONAL")
878 .and_then(|f| f.get("notional").or_else(|| f.get("minNotional")))
879 .and_then(|v| v.as_str())
880 .and_then(|s| s.parse::<f64>().ok());
881
882 Ok(SymbolOrderRules {
883 min_qty,
884 max_qty,
885 step_size,
886 min_notional,
887 })
888}
889
890fn find_filter<'a>(
891 filters: &'a [serde_json::Value],
892 filter_type: &str,
893) -> Option<&'a serde_json::Value> {
894 filters
895 .iter()
896 .find(|f| f.get("filterType").and_then(|v| v.as_str()) == Some(filter_type))
897}
898
899fn parse_lot_filter_values(filter: &serde_json::Value) -> Option<(f64, f64, f64)> {
900 let min_qty = json_str_to_f64(filter, "minQty").ok()?;
901 let max_qty = json_str_to_f64(filter, "maxQty").ok()?;
902 let step_size = json_str_to_f64(filter, "stepSize").ok()?;
903 if step_size <= 0.0 {
904 return None;
905 }
906 Some((min_qty, max_qty, step_size))
907}
908
909fn json_str_to_f64(row: &serde_json::Value, key: &str) -> Result<f64> {
910 let s = row
911 .get(key)
912 .and_then(|v| v.as_str())
913 .with_context(|| format!("missing field {}", key))?;
914 s.parse::<f64>()
915 .with_context(|| format!("invalid {} value {}", key, s))
916}
917
918#[cfg(test)]
919mod tests {
920 use super::*;
921 use serde_json::json;
922
923 #[test]
924 fn hmac_signing_produces_hex_signature() {
925 let client = BinanceRestClient::new(
926 "https://testnet.binance.vision",
927 "https://testnet.binancefuture.com",
928 "test_key",
929 "test_secret",
930 "test_fut_key",
931 "test_fut_secret",
932 5000,
933 );
934 let signed = client.sign("symbol=BTCUSDT&side=BUY");
935 assert!(signed.contains("symbol=BTCUSDT&side=BUY"));
937 assert!(signed.contains("recvWindow=5000"));
938 assert!(signed.contains("timestamp="));
939 assert!(signed.contains("&signature="));
940
941 let sig = signed.split("&signature=").nth(1).unwrap();
943 assert_eq!(sig.len(), 64);
944 assert!(sig.chars().all(|c| c.is_ascii_hexdigit()));
945 }
946
947 #[test]
948 fn hmac_known_vector() {
949 let secret = "NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j";
951 let query = "symbol=LTCBTC&side=BUY&type=LIMIT&timeInForce=GTC&quantity=1&price=0.1&recvWindow=5000×tamp=1499827319559";
952
953 let mut mac = Hmac::<Sha256>::new_from_slice(secret.as_bytes()).unwrap();
954 mac.update(query.as_bytes());
955 let signature = hex::encode(mac.finalize().into_bytes());
956
957 assert_eq!(
958 signature,
959 "c8db56825ae71d6d79447849e617115f4a920fa2acdcab2b053c4b2838bd6b71"
960 );
961 }
962
963 #[test]
964 fn check_rate_limit_does_not_panic_on_poisoned_mutex() {
965 let client = BinanceRestClient::new(
966 "https://testnet.binance.vision",
967 "https://testnet.binancefuture.com",
968 "test_key",
969 "test_secret",
970 "test_fut_key",
971 "test_fut_secret",
972 5000,
973 );
974
975 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
976 let _guard = client.window_start.lock().unwrap();
977 panic!("poison window_start mutex");
978 }));
979
980 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
981 client.check_rate_limit();
982 }));
983 assert!(
984 result.is_ok(),
985 "check_rate_limit should recover from poison"
986 );
987 }
988
989 #[test]
990 fn parse_symbol_rules_prefers_market_lot_size_for_spot() {
991 let payload = json!({
992 "symbols": [{
993 "symbol": "BTCUSDT",
994 "filters": [
995 {"filterType":"LOT_SIZE","minQty":"0.00100000","maxQty":"100.00000000","stepSize":"0.00100000"},
996 {"filterType":"MARKET_LOT_SIZE","minQty":"0.00001000","maxQty":"50.00000000","stepSize":"0.00001000"},
997 {"filterType":"MIN_NOTIONAL","minNotional":"5.00000000"}
998 ]
999 }]
1000 });
1001 let rules = parse_symbol_order_rules_from_exchange_info(&payload, "BTCUSDT", true).unwrap();
1002 assert!((rules.step_size - 0.00001).abs() < 1e-12);
1003 assert!((rules.min_qty - 0.00001).abs() < 1e-12);
1004 assert_eq!(rules.min_notional, Some(5.0));
1005 }
1006
1007 #[test]
1008 fn parse_symbol_rules_uses_lot_size_for_futures() {
1009 let payload = json!({
1010 "symbols": [{
1011 "symbol": "ETHUSDT",
1012 "filters": [
1013 {"filterType":"LOT_SIZE","minQty":"0.001","maxQty":"10000","stepSize":"0.001"},
1014 {"filterType":"MARKET_LOT_SIZE","minQty":"0.01","maxQty":"1000","stepSize":"0.01"}
1015 ]
1016 }]
1017 });
1018 let rules =
1019 parse_symbol_order_rules_from_exchange_info(&payload, "ETHUSDT", false).unwrap();
1020 assert!((rules.step_size - 0.001).abs() < 1e-12);
1021 assert!((rules.min_qty - 0.001).abs() < 1e-12);
1022 }
1023
1024 #[test]
1025 fn parse_symbol_rules_fallback_when_market_lot_size_is_invalid() {
1026 let payload = json!({
1027 "symbols": [{
1028 "symbol": "BTCUSDT",
1029 "filters": [
1030 {"filterType":"LOT_SIZE","minQty":"0.00001000","maxQty":"50.00000000","stepSize":"0.00001000"},
1031 {"filterType":"MARKET_LOT_SIZE","minQty":"0.00001000","maxQty":"50.00000000","stepSize":"0.00000000"}
1032 ]
1033 }]
1034 });
1035 let rules = parse_symbol_order_rules_from_exchange_info(&payload, "BTCUSDT", true).unwrap();
1036 assert!((rules.step_size - 0.00001).abs() < 1e-12);
1037 }
1038}