1use std::{
19 collections::HashMap,
20 num::NonZeroU32,
21 sync::{Arc, LazyLock, Weak},
22 time::Duration,
23};
24
25use ahash::{AHashMap, AHashSet};
26use aws_lc_rs::digest;
27use dashmap::DashMap;
28use jiff::Timestamp;
29use nautilus_common::cache::InstrumentLookupError;
30use nautilus_core::{
31 AtomicMap, consts::NAUTILUS_USER_AGENT, datetime::SECONDS_IN_DAY, nanos::UnixNanos,
32 time::AtomicTime,
33};
34use nautilus_model::{
35 data::{Bar, BarType, BookOrder, FundingRateUpdate, TradeTick},
36 enums::{
37 AggregationSource, AggressorSide, BarAggregation, BookType, MarketStatusAction, OrderSide,
38 OrderType, TimeInForce,
39 },
40 events::AccountState,
41 identifiers::{AccountId, ClientOrderId, InstrumentId, TradeId, VenueOrderId},
42 instruments::{Instrument, any::InstrumentAny},
43 orderbook::OrderBook,
44 reports::{FillReport, OrderStatusReport},
45 types::{Currency, Price, Quantity, fixed::FIXED_PRECISION},
46};
47use nautilus_network::{
48 http::{HttpClient, HttpResponse, Method, USER_AGENT},
49 ratelimiter::{RateLimiter, clock::MonotonicClock, quota::Quota},
50};
51use parking_lot::Mutex;
52use rust_decimal::Decimal;
53use serde::{Deserialize, Serialize, de::DeserializeOwned};
54use ustr::Ustr;
55
56use super::{
57 error::{BinanceFuturesHttpError, BinanceFuturesHttpResult},
58 models::{
59 BatchOrderResult, BinanceBookTicker, BinanceCancelAllOrdersResponse, BinanceFundingRate,
60 BinanceFuturesAccountInfo, BinanceFuturesAggTrade, BinanceFuturesAlgoOrder,
61 BinanceFuturesAlgoOrderCancelResponse, BinanceFuturesCoinExchangeInfo,
62 BinanceFuturesCoinSymbol, BinanceFuturesCommissionRate, BinanceFuturesKline,
63 BinanceFuturesMarkPrice, BinanceFuturesOrder, BinanceFuturesTicker24hr,
64 BinanceFuturesTrade, BinanceFuturesUsdExchangeInfo, BinanceFuturesUsdSymbol,
65 BinanceHedgeModeResponse, BinanceLeverageResponse, BinanceOpenInterest,
66 BinanceOpenInterestHistRecord, BinanceOrderBook, BinancePositionRisk, BinancePriceTicker,
67 BinanceServerTime, BinanceUserTrade, ListenKeyResponse,
68 },
69 query::{
70 BatchCancelItem, BatchModifyItem, BatchOrderItem, BinanceAggTradesParams,
71 BinanceAlgoOrderQueryParams, BinanceAllAlgoOrdersParams, BinanceAllOrdersParams,
72 BinanceBookTickerParams, BinanceCancelAllAlgoOrdersParams, BinanceCancelAllOrdersParams,
73 BinanceCancelOrderParams, BinanceCommissionRateParams, BinanceDepthParams,
74 BinanceFundingRateParams, BinanceKlinesParams, BinanceMarkPriceParams,
75 BinanceModifyOrderParams, BinanceNewAlgoOrderParams, BinanceNewOrderParams,
76 BinanceOpenAlgoOrdersParams, BinanceOpenInterestHistParams, BinanceOpenInterestParams,
77 BinanceOpenOrdersParams, BinanceOrderQueryParams, BinancePositionRiskParams,
78 BinanceSetLeverageParams, BinanceSetMarginTypeParams, BinanceTicker24hrParams,
79 BinanceTradesParams, BinanceUserTradesParams, ListenKeyParams,
80 },
81};
82use crate::{
83 common::{
84 bar::BinanceBar,
85 consts::{
86 BINANCE_API_KEY_HEADER, BINANCE_DAPI_PATH, BINANCE_DAPI_RATE_LIMITS, BINANCE_FAPI_PATH,
87 BINANCE_FAPI_RATE_LIMITS, BINANCE_NAUTILUS_FUTURES_BROKER_ID, BinanceRateLimitQuota,
88 },
89 credential::SigningCredential,
90 encoder::encode_broker_id,
91 enums::{
92 BinanceAlgoType, BinanceEnvironment, BinanceFuturesOrderType, BinancePositionSide,
93 BinancePriceMatch, BinanceProductType, BinanceRateLimitInterval, BinanceRateLimitType,
94 BinanceSide, BinanceTimeInForce, BinanceWorkingType,
95 },
96 fees::futures_fee_tier_rates,
97 instruments::BinanceInstrumentSelector,
98 models::BinanceErrorResponse,
99 parse::{
100 parse_coinm_instrument_with_fees, parse_millis, parse_required_price_at_precision,
101 parse_required_quantity_at_precision, parse_usdm_instrument_with_fees,
102 },
103 symbol::{format_binance_symbol, format_instrument_id},
104 urls::get_http_base_url,
105 },
106 config::BinanceInstrumentProviderConfig,
107 futures::conversions::reduce_only_param,
108};
109
110const BINANCE_GLOBAL_RATE_KEY: &str = "binance:global";
111const BINANCE_ORDERS_RATE_KEY: &str = "binance:orders";
112
113type BinanceFuturesLimiter = RateLimiter<Ustr, MonotonicClock>;
114type BinanceFuturesRateLimiter = Arc<BinanceFuturesLimiter>;
115type BinanceFuturesRateLimiterRegistry<S> = Mutex<AHashMap<S, Weak<BinanceFuturesLimiter>>>;
116
117#[derive(Clone, PartialEq, Eq, Hash)]
118enum BinanceFuturesEndpointScope {
119 Environment(BinanceEnvironment),
120 Custom {
121 environment: BinanceEnvironment,
122 base_url: String,
123 },
124}
125
126#[derive(Clone, PartialEq, Eq, Hash)]
127struct BinanceFuturesRequestScope {
128 endpoint: BinanceFuturesEndpointScope,
129 proxy_url: Option<String>,
130}
131
132#[derive(Clone, Copy, PartialEq, Eq, Hash)]
133struct BinanceFuturesAccountScope([u8; 32]);
134
135#[derive(Clone, PartialEq, Eq, Hash)]
136struct BinanceFuturesOrderScope {
137 endpoint: BinanceFuturesEndpointScope,
138 account: BinanceFuturesAccountScope,
139}
140
141static BINANCE_FUTURES_REQUEST_LIMITERS: LazyLock<
142 BinanceFuturesRateLimiterRegistry<BinanceFuturesRequestScope>,
143> = LazyLock::new(|| Mutex::new(AHashMap::new()));
144
145static BINANCE_FUTURES_ORDER_LIMITERS: LazyLock<
146 BinanceFuturesRateLimiterRegistry<BinanceFuturesOrderScope>,
147> = LazyLock::new(|| Mutex::new(AHashMap::new()));
148
149#[derive(Debug)]
151pub struct BinanceFuturesAlgoOrderQueryResult {
152 pub algo: BinanceFuturesAlgoOrder,
154 pub actual: Option<BinanceFuturesOrder>,
156}
157
158#[derive(Debug, Serialize)]
159#[serde(rename_all = "camelCase")]
160struct BatchCancelParams {
161 symbol: String,
162 #[serde(skip_serializing_if = "Option::is_none")]
163 order_id_list: Option<String>,
164 #[serde(skip_serializing_if = "Option::is_none")]
165 orig_client_order_id_list: Option<String>,
166}
167
168#[derive(Debug, Clone)]
170pub struct BinanceRawFuturesHttpClient {
171 client: HttpClient,
172 base_url: String,
173 api_path: &'static str,
174 credential: Option<SigningCredential>,
175 recv_window: Option<u64>,
176 order_rate_keys: Vec<String>,
177}
178
179impl BinanceRawFuturesHttpClient {
180 #[must_use]
182 pub fn http_client(&self) -> &HttpClient {
183 &self.client
184 }
185
186 #[must_use]
188 pub const fn has_credentials(&self) -> bool {
189 self.credential.is_some()
190 }
191
192 #[expect(clippy::too_many_arguments)]
198 pub fn new(
199 product_type: BinanceProductType,
200 environment: BinanceEnvironment,
201 api_key: Option<String>,
202 api_secret: Option<String>,
203 base_url_override: Option<String>,
204 recv_window: Option<u64>,
205 timeout_secs: Option<u64>,
206 proxy_url: Option<String>,
207 ) -> BinanceFuturesHttpResult<Self> {
208 let RateLimitConfig {
209 request_quota,
210 order_quotas,
211 order_keys,
212 } = Self::rate_limit_config(product_type);
213
214 let credential = match (api_key, api_secret) {
215 (Some(key), Some(secret)) => Some(SigningCredential::new(key, secret)),
216 (None, None) => None,
217 _ => return Err(BinanceFuturesHttpError::MissingCredentials),
218 };
219
220 let account_scope = credential.as_ref().map(Self::account_scope);
221 let rate_limiters = Self::shared_rate_limiters(
222 environment,
223 base_url_override.as_deref(),
224 proxy_url.as_deref(),
225 account_scope,
226 request_quota,
227 order_quotas,
228 );
229 let base_url = base_url_override
230 .unwrap_or_else(|| get_http_base_url(product_type, environment).to_string());
231 let api_path = Self::resolve_api_path(product_type);
232 let headers = Self::default_headers(&credential);
233
234 let client = HttpClient::builder()
235 .headers(headers)
236 .header_keys(vec![BINANCE_API_KEY_HEADER.to_string()])
237 .maybe_timeout_secs(timeout_secs)
238 .maybe_proxy_url(proxy_url)
239 .rate_limiters(rate_limiters)
240 .build()?;
241
242 Ok(Self {
243 client,
244 base_url,
245 api_path,
246 credential,
247 recv_window,
248 order_rate_keys: order_keys,
249 })
250 }
251
252 fn shared_rate_limiters(
253 environment: BinanceEnvironment,
254 base_url_override: Option<&str>,
255 proxy_url: Option<&str>,
256 account_scope: Option<BinanceFuturesAccountScope>,
257 request_quota: Quota,
258 order_quotas: Vec<(String, Quota)>,
259 ) -> Vec<BinanceFuturesRateLimiter> {
260 let endpoint = Self::endpoint_scope(environment, base_url_override);
261 let request_scope = BinanceFuturesRequestScope {
262 endpoint: endpoint.clone(),
263 proxy_url: proxy_url.map(ToOwned::to_owned),
264 };
265 let request_limiter = Self::request_rate_limiter(request_scope, request_quota);
266 let mut limiters = vec![request_limiter];
267
268 if let Some(account) = account_scope {
269 let order_scope = BinanceFuturesOrderScope { endpoint, account };
270 limiters.push(Self::order_rate_limiter(order_scope, order_quotas));
271 }
272
273 limiters
274 }
275
276 fn endpoint_scope(
277 environment: BinanceEnvironment,
278 base_url_override: Option<&str>,
279 ) -> BinanceFuturesEndpointScope {
280 let Some(base_url) = base_url_override else {
281 return BinanceFuturesEndpointScope::Environment(environment);
282 };
283
284 let normalized = base_url.trim_end_matches('/');
285 let official_urls = [
286 get_http_base_url(BinanceProductType::UsdM, environment),
287 get_http_base_url(BinanceProductType::CoinM, environment),
288 ];
289
290 if official_urls.contains(&normalized) {
291 BinanceFuturesEndpointScope::Environment(environment)
292 } else {
293 BinanceFuturesEndpointScope::Custom {
294 environment,
295 base_url: normalized.to_string(),
296 }
297 }
298 }
299
300 fn account_scope(credential: &SigningCredential) -> BinanceFuturesAccountScope {
301 let digest = digest::digest(&digest::SHA256, credential.api_key().as_bytes());
302 let bytes = digest
303 .as_ref()
304 .try_into()
305 .expect("SHA-256 digest must contain 32 bytes");
306 BinanceFuturesAccountScope(bytes)
307 }
308
309 fn request_rate_limiter(
310 scope: BinanceFuturesRequestScope,
311 quota: Quota,
312 ) -> BinanceFuturesRateLimiter {
313 let mut registry = BINANCE_FUTURES_REQUEST_LIMITERS.lock();
314
315 if let Some(limiter) = registry.get(&scope).and_then(Weak::upgrade) {
316 return limiter;
317 }
318
319 let limiter = Arc::new(RateLimiter::new_with_quota(
320 None,
321 vec![(Ustr::from(BINANCE_GLOBAL_RATE_KEY), quota)],
322 ));
323 registry.insert(scope, Arc::downgrade(&limiter));
324 limiter
325 }
326
327 fn order_rate_limiter(
328 scope: BinanceFuturesOrderScope,
329 quotas: Vec<(String, Quota)>,
330 ) -> BinanceFuturesRateLimiter {
331 let mut registry = BINANCE_FUTURES_ORDER_LIMITERS.lock();
332
333 if let Some(limiter) = registry.get(&scope).and_then(Weak::upgrade) {
334 return limiter;
335 }
336
337 let quotas = quotas
338 .into_iter()
339 .map(|(key, quota)| (Ustr::from(&key), quota))
340 .collect();
341 let limiter = Arc::new(RateLimiter::new_with_quota(None, quotas));
342 registry.insert(scope, Arc::downgrade(&limiter));
343 limiter
344 }
345
346 pub async fn get<P, T>(
352 &self,
353 path: &str,
354 params: Option<&P>,
355 signed: bool,
356 use_order_quota: bool,
357 ) -> BinanceFuturesHttpResult<T>
358 where
359 P: Serialize + ?Sized,
360 T: DeserializeOwned,
361 {
362 self.request(Method::GET, path, params, signed, use_order_quota, None)
363 .await
364 }
365
366 pub async fn post<P, T>(
372 &self,
373 path: &str,
374 params: Option<&P>,
375 body: Option<Vec<u8>>,
376 signed: bool,
377 use_order_quota: bool,
378 ) -> BinanceFuturesHttpResult<T>
379 where
380 P: Serialize + ?Sized,
381 T: DeserializeOwned,
382 {
383 self.request(Method::POST, path, params, signed, use_order_quota, body)
384 .await
385 }
386
387 pub async fn request_put<P, T>(
393 &self,
394 path: &str,
395 params: Option<&P>,
396 signed: bool,
397 use_order_quota: bool,
398 ) -> BinanceFuturesHttpResult<T>
399 where
400 P: Serialize + ?Sized,
401 T: DeserializeOwned,
402 {
403 self.request(Method::PUT, path, params, signed, use_order_quota, None)
404 .await
405 }
406
407 pub async fn request_delete<P, T>(
413 &self,
414 path: &str,
415 params: Option<&P>,
416 signed: bool,
417 use_order_quota: bool,
418 ) -> BinanceFuturesHttpResult<T>
419 where
420 P: Serialize + ?Sized,
421 T: DeserializeOwned,
422 {
423 self.request(Method::DELETE, path, params, signed, use_order_quota, None)
424 .await
425 }
426
427 pub async fn batch_request<T: Serialize>(
433 &self,
434 path: &str,
435 items: &[T],
436 use_order_quota: bool,
437 ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
438 self.batch_request_method(Method::POST, path, items, use_order_quota)
439 .await
440 }
441
442 pub async fn batch_request_delete<T: Serialize>(
448 &self,
449 path: &str,
450 items: &[T],
451 use_order_quota: bool,
452 ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
453 self.batch_request_method(Method::DELETE, path, items, use_order_quota)
454 .await
455 }
456
457 pub async fn batch_request_put<T: Serialize>(
463 &self,
464 path: &str,
465 items: &[T],
466 use_order_quota: bool,
467 ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
468 self.batch_request_method(Method::PUT, path, items, use_order_quota)
469 .await
470 }
471
472 async fn batch_request_method<T: Serialize>(
473 &self,
474 method: Method,
475 path: &str,
476 items: &[T],
477 use_order_quota: bool,
478 ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
479 let cred = self
480 .credential
481 .as_ref()
482 .ok_or(BinanceFuturesHttpError::MissingCredentials)?;
483
484 let batch_json = serde_json::to_string(items)
485 .map_err(|e| BinanceFuturesHttpError::ValidationError(e.to_string()))?;
486
487 let encoded_batch = Self::percent_encode(&batch_json);
488 let timestamp = Timestamp::now().as_millisecond();
489 let mut query = format!("batchOrders={encoded_batch}×tamp={timestamp}");
490
491 if let Some(recv_window) = self.recv_window {
492 query.push_str(&format!("&recvWindow={recv_window}"));
493 }
494
495 let signature = Self::percent_encode(&cred.sign(&query));
496 query.push_str(&format!("&signature={signature}"));
497
498 let url = self.build_url(path, &query);
499
500 let mut headers = HashMap::new();
501 headers.insert(
502 BINANCE_API_KEY_HEADER.to_string(),
503 cred.api_key().to_string(),
504 );
505
506 let keys = self.rate_limit_keys(use_order_quota);
507
508 let response = self
509 .client
510 .request(
511 method,
512 url,
513 None::<&HashMap<String, Vec<String>>>,
514 Some(headers),
515 None,
516 None,
517 Some(keys),
518 )
519 .await?;
520
521 if !response.status.is_success() {
522 return self.parse_error_response(&response);
523 }
524
525 serde_json::from_slice(&response.body)
526 .map_err(|e| BinanceFuturesHttpError::JsonError(e.to_string()))
527 }
528
529 fn percent_encode(input: &str) -> String {
531 let mut result = String::with_capacity(input.len() * 3);
532 for byte in input.bytes() {
533 match byte {
534 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
535 result.push(byte as char);
536 }
537 _ => {
538 result.push('%');
539 result.push_str(&format!("{byte:02X}"));
540 }
541 }
542 }
543 result
544 }
545
546 async fn request<P, T>(
547 &self,
548 method: Method,
549 path: &str,
550 params: Option<&P>,
551 signed: bool,
552 use_order_quota: bool,
553 body: Option<Vec<u8>>,
554 ) -> BinanceFuturesHttpResult<T>
555 where
556 P: Serialize + ?Sized,
557 T: DeserializeOwned,
558 {
559 let mut query = params
560 .map(serde_urlencoded::to_string)
561 .transpose()
562 .map_err(|e| BinanceFuturesHttpError::ValidationError(e.to_string()))?
563 .unwrap_or_default();
564
565 let mut headers = HashMap::new();
566
567 if signed {
568 let cred = self
569 .credential
570 .as_ref()
571 .ok_or(BinanceFuturesHttpError::MissingCredentials)?;
572
573 if !query.is_empty() {
574 query.push('&');
575 }
576
577 let timestamp = Timestamp::now().as_millisecond();
578 query.push_str(&format!("timestamp={timestamp}"));
579
580 if let Some(recv_window) = self.recv_window {
581 query.push_str(&format!("&recvWindow={recv_window}"));
582 }
583
584 let signature = Self::percent_encode(&cred.sign(&query));
588 query.push_str(&format!("&signature={signature}"));
589 headers.insert(
590 BINANCE_API_KEY_HEADER.to_string(),
591 cred.api_key().to_string(),
592 );
593 }
594
595 let url = self.build_url(path, &query);
596 let keys = self.rate_limit_keys(use_order_quota);
597
598 let response = self
599 .client
600 .request(
601 method,
602 url,
603 None::<&HashMap<String, Vec<String>>>,
604 Some(headers),
605 body,
606 None,
607 Some(keys),
608 )
609 .await?;
610
611 if !response.status.is_success() {
612 return self.parse_error_response(&response);
613 }
614
615 serde_json::from_slice::<T>(&response.body)
616 .map_err(|e| BinanceFuturesHttpError::JsonError(e.to_string()))
617 }
618
619 fn build_url(&self, path: &str, query: &str) -> String {
620 let url_path = if path.starts_with("/fapi/")
622 || path.starts_with("/dapi/")
623 || path.starts_with("/futures/data/")
624 {
625 path.to_string()
626 } else if path.starts_with('/') {
627 format!("{}{}", self.api_path, path)
628 } else {
629 format!("{}/{}", self.api_path, path)
630 };
631
632 let mut url = format!("{}{}", self.base_url, url_path);
633
634 if !query.is_empty() {
635 url.push('?');
636 url.push_str(query);
637 }
638 url
639 }
640
641 fn rate_limit_keys(&self, use_orders: bool) -> Vec<String> {
642 if use_orders {
643 let mut keys = Vec::with_capacity(1 + self.order_rate_keys.len());
644 keys.push(BINANCE_GLOBAL_RATE_KEY.to_string());
645 keys.extend(self.order_rate_keys.iter().cloned());
646 keys
647 } else {
648 vec![BINANCE_GLOBAL_RATE_KEY.to_string()]
649 }
650 }
651
652 fn parse_error_response<T>(&self, response: &HttpResponse) -> BinanceFuturesHttpResult<T> {
653 let status = response.status.as_u16();
654 let body = String::from_utf8_lossy(&response.body).to_string();
655
656 if let Ok(err) = serde_json::from_str::<BinanceErrorResponse>(&body) {
657 return Err(BinanceFuturesHttpError::BinanceError {
658 code: err.code,
659 message: err.msg,
660 });
661 }
662
663 Err(BinanceFuturesHttpError::UnexpectedStatus { status, body })
664 }
665
666 fn default_headers(credential: &Option<SigningCredential>) -> HashMap<String, String> {
667 let mut headers = HashMap::new();
668 headers.insert(USER_AGENT.to_string(), NAUTILUS_USER_AGENT.to_string());
669
670 if let Some(cred) = credential {
671 headers.insert(
672 BINANCE_API_KEY_HEADER.to_string(),
673 cred.api_key().to_string(),
674 );
675 }
676 headers
677 }
678
679 fn resolve_api_path(product_type: BinanceProductType) -> &'static str {
680 match product_type {
681 BinanceProductType::UsdM => BINANCE_FAPI_PATH,
682 BinanceProductType::CoinM => BINANCE_DAPI_PATH,
683 _ => BINANCE_FAPI_PATH, }
685 }
686
687 fn rate_limit_config(product_type: BinanceProductType) -> RateLimitConfig {
688 let quotas = match product_type {
689 BinanceProductType::UsdM => BINANCE_FAPI_RATE_LIMITS,
690 BinanceProductType::CoinM => BINANCE_DAPI_RATE_LIMITS,
691 _ => BINANCE_FAPI_RATE_LIMITS,
692 };
693
694 let mut order_quotas = Vec::new();
695 let mut order_keys = Vec::new();
696 let mut request_quota = None;
697
698 for quota in quotas {
699 if let Some(q) = Self::quota_from(quota) {
700 match quota.rate_limit_type {
701 BinanceRateLimitType::RequestWeight if request_quota.is_none() => {
702 request_quota = Some(q);
703 }
704 BinanceRateLimitType::Orders => {
705 let key = format!(
706 "{}:{}:{:?}",
707 BINANCE_ORDERS_RATE_KEY, quota.interval_num, quota.interval
708 );
709 order_keys.push(key.clone());
710 order_quotas.push((key, q));
711 }
712 _ => {}
713 }
714 }
715 }
716
717 let request_quota = request_quota.unwrap_or_else(|| {
718 Quota::per_second(NonZeroU32::new(10).expect("non-zero")).expect("valid constant")
719 });
720
721 RateLimitConfig {
722 request_quota,
723 order_quotas,
724 order_keys,
725 }
726 }
727
728 fn quota_from(quota: &BinanceRateLimitQuota) -> Option<Quota> {
729 let burst = NonZeroU32::new(quota.limit)?;
730 let period = Self::quota_period(quota)?;
731 let replenish_interval_ns = period.as_nanos() / u128::from(quota.limit);
732 let replenish_interval_ns = u64::try_from(replenish_interval_ns).ok()?;
733
734 Quota::with_period(Duration::from_nanos(replenish_interval_ns))
735 .map(|q| q.allow_burst(burst))
736 }
737
738 fn quota_period(quota: &BinanceRateLimitQuota) -> Option<Duration> {
739 match quota.interval {
740 BinanceRateLimitInterval::Second => {
741 Some(Duration::from_secs(u64::from(quota.interval_num)))
742 }
743 BinanceRateLimitInterval::Minute => {
744 Some(Duration::from_secs(60 * u64::from(quota.interval_num)))
745 }
746 BinanceRateLimitInterval::Day => Some(Duration::from_secs(
747 SECONDS_IN_DAY * u64::from(quota.interval_num),
748 )),
749 BinanceRateLimitInterval::Unknown => None,
750 }
751 }
752
753 pub async fn ticker_24h(
759 &self,
760 params: &BinanceTicker24hrParams,
761 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesTicker24hr>> {
762 self.get("ticker/24hr", Some(params), false, false).await
763 }
764
765 pub async fn book_ticker(
771 &self,
772 params: &BinanceBookTickerParams,
773 ) -> BinanceFuturesHttpResult<Vec<BinanceBookTicker>> {
774 self.get("ticker/bookTicker", Some(params), false, false)
775 .await
776 }
777
778 pub async fn price_ticker(
784 &self,
785 symbol: Option<&str>,
786 ) -> BinanceFuturesHttpResult<Vec<BinancePriceTicker>> {
787 #[derive(Serialize)]
788 struct Params<'a> {
789 #[serde(skip_serializing_if = "Option::is_none")]
790 symbol: Option<&'a str>,
791 }
792 self.get("ticker/price", Some(&Params { symbol }), false, false)
793 .await
794 }
795
796 pub async fn depth(
802 &self,
803 params: &BinanceDepthParams,
804 ) -> BinanceFuturesHttpResult<BinanceOrderBook> {
805 self.get("depth", Some(params), false, false).await
806 }
807
808 pub async fn mark_price(
814 &self,
815 params: &BinanceMarkPriceParams,
816 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesMarkPrice>> {
817 let response: MarkPriceResponse =
818 self.get("premiumIndex", Some(params), false, false).await?;
819 Ok(response.into())
820 }
821
822 pub async fn funding_rate(
828 &self,
829 params: &BinanceFundingRateParams,
830 ) -> BinanceFuturesHttpResult<Vec<BinanceFundingRate>> {
831 self.get("fundingRate", Some(params), false, false).await
832 }
833
834 pub async fn open_interest(
840 &self,
841 params: &BinanceOpenInterestParams,
842 ) -> BinanceFuturesHttpResult<BinanceOpenInterest> {
843 self.get("openInterest", Some(params), false, false).await
844 }
845
846 pub async fn open_interest_hist(
852 &self,
853 params: &BinanceOpenInterestHistParams,
854 ) -> BinanceFuturesHttpResult<Vec<BinanceOpenInterestHistRecord>> {
855 self.get("/futures/data/openInterestHist", Some(params), false, false)
856 .await
857 }
858
859 pub async fn trades(
865 &self,
866 params: &BinanceTradesParams,
867 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesTrade>> {
868 self.get("trades", Some(params), false, false).await
869 }
870
871 pub async fn agg_trades(
877 &self,
878 params: &BinanceAggTradesParams,
879 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesAggTrade>> {
880 if params.limit.is_some_and(|limit| limit > 1000) {
881 return Err(BinanceFuturesHttpError::ValidationError(
882 "aggregate trade limit must not exceed 1000".to_string(),
883 ));
884 }
885
886 if let (Some(start), Some(end)) = (params.start_time, params.end_time) {
887 if start > end {
888 return Err(BinanceFuturesHttpError::ValidationError(
889 "aggregate trade startTime must not exceed endTime".to_string(),
890 ));
891 }
892 let Some(range) = end.checked_sub(start) else {
893 return Err(BinanceFuturesHttpError::ValidationError(
894 "aggregate trade time range must be less than one hour".to_string(),
895 ));
896 };
897
898 if range >= 3_600_000 {
899 return Err(BinanceFuturesHttpError::ValidationError(
900 "aggregate trade time range must be less than one hour".to_string(),
901 ));
902 }
903 }
904
905 self.get("aggTrades", Some(params), false, false).await
906 }
907
908 pub async fn klines(
914 &self,
915 params: &BinanceKlinesParams,
916 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesKline>> {
917 self.get("klines", Some(params), false, false).await
918 }
919
920 pub async fn set_leverage(
926 &self,
927 params: &BinanceSetLeverageParams,
928 ) -> BinanceFuturesHttpResult<BinanceLeverageResponse> {
929 self.post("leverage", Some(params), None, true, false).await
930 }
931
932 pub async fn set_margin_type(
938 &self,
939 params: &BinanceSetMarginTypeParams,
940 ) -> BinanceFuturesHttpResult<serde_json::Value> {
941 self.post("marginType", Some(params), None, true, false)
942 .await
943 }
944
945 pub async fn query_hedge_mode(&self) -> BinanceFuturesHttpResult<BinanceHedgeModeResponse> {
951 self.get::<(), _>("positionSide/dual", None, true, false)
952 .await
953 }
954
955 pub async fn create_listen_key(&self) -> BinanceFuturesHttpResult<ListenKeyResponse> {
961 self.post::<(), ListenKeyResponse>("listenKey", None, None, true, false)
962 .await
963 }
964
965 pub async fn keepalive_listen_key(&self, listen_key: &str) -> BinanceFuturesHttpResult<()> {
971 let params = ListenKeyParams {
972 listen_key: listen_key.to_string(),
973 };
974 let _: serde_json::Value = self
975 .request_put("listenKey", Some(¶ms), true, false)
976 .await?;
977 Ok(())
978 }
979
980 pub async fn close_listen_key(&self, listen_key: &str) -> BinanceFuturesHttpResult<()> {
986 let params = ListenKeyParams {
987 listen_key: listen_key.to_string(),
988 };
989 let _: serde_json::Value = self
990 .request_delete("listenKey", Some(¶ms), true, false)
991 .await?;
992 Ok(())
993 }
994
995 pub async fn query_account(&self) -> BinanceFuturesHttpResult<BinanceFuturesAccountInfo> {
1001 let path = if self.api_path.starts_with("/fapi") {
1003 "/fapi/v2/account"
1004 } else {
1005 "/dapi/v1/account"
1006 };
1007 self.get::<(), _>(path, None, true, false).await
1008 }
1009
1010 pub async fn commission_rate(
1016 &self,
1017 params: &BinanceCommissionRateParams,
1018 ) -> BinanceFuturesHttpResult<BinanceFuturesCommissionRate> {
1019 self.get("commissionRate", Some(params), true, false).await
1020 }
1021
1022 pub async fn query_positions(
1028 &self,
1029 params: &BinancePositionRiskParams,
1030 ) -> BinanceFuturesHttpResult<Vec<BinancePositionRisk>> {
1031 let path = if self.api_path.starts_with("/fapi") {
1033 "/fapi/v2/positionRisk"
1034 } else {
1035 "/dapi/v1/positionRisk"
1036 };
1037 self.get(path, Some(params), true, false).await
1038 }
1039
1040 pub async fn query_user_trades(
1046 &self,
1047 params: &BinanceUserTradesParams,
1048 ) -> BinanceFuturesHttpResult<Vec<BinanceUserTrade>> {
1049 self.get("userTrades", Some(params), true, false).await
1050 }
1051
1052 pub async fn query_order(
1058 &self,
1059 params: &BinanceOrderQueryParams,
1060 ) -> BinanceFuturesHttpResult<BinanceFuturesOrder> {
1061 self.get("order", Some(params), true, false).await
1062 }
1063
1064 pub async fn query_open_orders(
1070 &self,
1071 params: &BinanceOpenOrdersParams,
1072 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesOrder>> {
1073 self.get("openOrders", Some(params), true, false).await
1074 }
1075
1076 pub async fn query_all_orders(
1082 &self,
1083 params: &BinanceAllOrdersParams,
1084 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesOrder>> {
1085 self.get("allOrders", Some(params), true, false).await
1086 }
1087
1088 pub async fn submit_order(
1094 &self,
1095 params: &BinanceNewOrderParams,
1096 ) -> BinanceFuturesHttpResult<BinanceFuturesOrder> {
1097 self.post("order", Some(params), None, true, true).await
1098 }
1099
1100 pub async fn submit_order_list(
1106 &self,
1107 orders: &[BatchOrderItem],
1108 ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
1109 if orders.is_empty() {
1110 return Ok(Vec::new());
1111 }
1112
1113 if orders.len() > 5 {
1114 return Err(BinanceFuturesHttpError::ValidationError(
1115 "Batch order limit is 5 orders maximum".to_string(),
1116 ));
1117 }
1118
1119 self.batch_request("batchOrders", orders, true).await
1120 }
1121
1122 pub async fn modify_order(
1128 &self,
1129 params: &BinanceModifyOrderParams,
1130 ) -> BinanceFuturesHttpResult<BinanceFuturesOrder> {
1131 self.request_put("order", Some(params), true, true).await
1132 }
1133
1134 pub async fn batch_modify_orders(
1140 &self,
1141 modifies: &[BatchModifyItem],
1142 ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
1143 if modifies.is_empty() {
1144 return Ok(Vec::new());
1145 }
1146
1147 if modifies.len() > 5 {
1148 return Err(BinanceFuturesHttpError::ValidationError(
1149 "Batch modify limit is 5 orders maximum".to_string(),
1150 ));
1151 }
1152
1153 self.batch_request_put("batchOrders", modifies, true).await
1154 }
1155
1156 pub async fn cancel_order(
1162 &self,
1163 params: &BinanceCancelOrderParams,
1164 ) -> BinanceFuturesHttpResult<BinanceFuturesOrder> {
1165 self.request_delete("order", Some(params), true, true).await
1166 }
1167
1168 pub async fn cancel_all_orders(
1174 &self,
1175 params: &BinanceCancelAllOrdersParams,
1176 ) -> BinanceFuturesHttpResult<BinanceCancelAllOrdersResponse> {
1177 self.request_delete("allOpenOrders", Some(params), true, true)
1178 .await
1179 }
1180
1181 pub async fn batch_cancel_orders(
1187 &self,
1188 cancels: &[BatchCancelItem],
1189 ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
1190 if cancels.is_empty() {
1191 return Ok(Vec::new());
1192 }
1193
1194 if cancels.len() > 10 {
1195 return Err(BinanceFuturesHttpError::ValidationError(
1196 "Batch cancel limit is 10 orders maximum".to_string(),
1197 ));
1198 }
1199
1200 let params = Self::batch_cancel_params(cancels)?;
1201 self.request_delete("batchOrders", Some(¶ms), true, true)
1202 .await
1203 }
1204
1205 fn batch_cancel_params(
1206 cancels: &[BatchCancelItem],
1207 ) -> BinanceFuturesHttpResult<BatchCancelParams> {
1208 let symbol = cancels[0].symbol.clone();
1209 let mut order_ids = Vec::new();
1210 let mut client_order_ids = Vec::new();
1211
1212 for cancel in cancels {
1213 if cancel.symbol != symbol {
1214 return Err(BinanceFuturesHttpError::ValidationError(
1215 "Batch cancel orders must use the same symbol".to_string(),
1216 ));
1217 }
1218
1219 if let Some(order_id) = cancel.order_id {
1220 order_ids.push(order_id);
1221 }
1222
1223 if let Some(client_order_id) = &cancel.orig_client_order_id {
1224 client_order_ids.push(client_order_id.clone());
1225 }
1226 }
1227
1228 if order_ids.is_empty() && client_order_ids.is_empty() {
1229 return Err(BinanceFuturesHttpError::ValidationError(
1230 "Batch cancel requires at least one order ID or client order ID".to_string(),
1231 ));
1232 }
1233
1234 if !order_ids.is_empty() && !client_order_ids.is_empty() {
1235 return Err(BinanceFuturesHttpError::ValidationError(
1236 "Batch cancel requires either order IDs or client order IDs, not both".to_string(),
1237 ));
1238 }
1239
1240 let order_id_list = if order_ids.is_empty() {
1241 None
1242 } else {
1243 Some(
1244 serde_json::to_string(&order_ids)
1245 .map_err(|e| BinanceFuturesHttpError::ValidationError(e.to_string()))?,
1246 )
1247 };
1248 let orig_client_order_id_list = if client_order_ids.is_empty() {
1249 None
1250 } else {
1251 Some(
1252 serde_json::to_string(&client_order_ids)
1253 .map_err(|e| BinanceFuturesHttpError::ValidationError(e.to_string()))?,
1254 )
1255 };
1256
1257 Ok(BatchCancelParams {
1258 symbol,
1259 order_id_list,
1260 orig_client_order_id_list,
1261 })
1262 }
1263
1264 pub async fn submit_algo_order(
1273 &self,
1274 params: &BinanceNewAlgoOrderParams,
1275 ) -> BinanceFuturesHttpResult<BinanceFuturesAlgoOrder> {
1276 self.post("algoOrder", Some(params), None, true, true).await
1277 }
1278
1279 pub async fn cancel_algo_order(
1287 &self,
1288 params: &BinanceAlgoOrderQueryParams,
1289 ) -> BinanceFuturesHttpResult<BinanceFuturesAlgoOrderCancelResponse> {
1290 self.request_delete("algoOrder", Some(params), true, true)
1291 .await
1292 }
1293
1294 pub async fn query_algo_order(
1302 &self,
1303 params: &BinanceAlgoOrderQueryParams,
1304 ) -> BinanceFuturesHttpResult<BinanceFuturesAlgoOrder> {
1305 self.get("algoOrder", Some(params), true, false).await
1306 }
1307
1308 pub async fn query_open_algo_orders(
1314 &self,
1315 params: &BinanceOpenAlgoOrdersParams,
1316 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesAlgoOrder>> {
1317 self.get("openAlgoOrders", Some(params), true, false).await
1318 }
1319
1320 pub async fn query_all_algo_orders(
1326 &self,
1327 params: &BinanceAllAlgoOrdersParams,
1328 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesAlgoOrder>> {
1329 self.get("allAlgoOrders", Some(params), true, false).await
1330 }
1331
1332 pub async fn cancel_all_algo_orders(
1338 &self,
1339 params: &BinanceCancelAllAlgoOrdersParams,
1340 ) -> BinanceFuturesHttpResult<BinanceCancelAllOrdersResponse> {
1341 self.request_delete("algoOpenOrders", Some(params), true, true)
1342 .await
1343 }
1344}
1345
1346#[derive(Debug, Deserialize)]
1348#[serde(untagged)]
1349enum MarkPriceResponse {
1350 Single(BinanceFuturesMarkPrice),
1351 Multiple(Vec<BinanceFuturesMarkPrice>),
1352}
1353
1354impl From<MarkPriceResponse> for Vec<BinanceFuturesMarkPrice> {
1355 fn from(response: MarkPriceResponse) -> Self {
1356 match response {
1357 MarkPriceResponse::Single(price) => vec![price],
1358 MarkPriceResponse::Multiple(prices) => prices,
1359 }
1360 }
1361}
1362
1363struct RateLimitConfig {
1364 request_quota: Quota,
1365 order_quotas: Vec<(String, Quota)>,
1366 order_keys: Vec<String>,
1367}
1368
1369#[derive(Clone, Debug)]
1371pub enum BinanceFuturesInstrument {
1372 UsdM(BinanceFuturesUsdSymbol),
1374 CoinM(BinanceFuturesCoinSymbol),
1376}
1377
1378impl BinanceFuturesInstrument {
1379 #[must_use]
1381 pub const fn symbol(&self) -> Ustr {
1382 match self {
1383 Self::UsdM(s) => s.symbol,
1384 Self::CoinM(s) => s.symbol,
1385 }
1386 }
1387
1388 #[must_use]
1390 pub const fn price_precision(&self) -> i32 {
1391 match self {
1392 Self::UsdM(s) => s.price_precision,
1393 Self::CoinM(s) => s.price_precision,
1394 }
1395 }
1396
1397 #[must_use]
1399 pub const fn quantity_precision(&self) -> i32 {
1400 match self {
1401 Self::UsdM(s) => s.quantity_precision,
1402 Self::CoinM(s) => s.quantity_precision,
1403 }
1404 }
1405
1406 pub fn precisions(&self) -> BinanceFuturesHttpResult<(u8, u8)> {
1412 let price_precision = u8::try_from(self.price_precision()).map_err(|_| {
1413 BinanceFuturesHttpError::ValidationError(format!(
1414 "Invalid Binance Futures price precision {} for {}",
1415 self.price_precision(),
1416 self.symbol()
1417 ))
1418 })?;
1419 let quantity_precision = u8::try_from(self.quantity_precision()).map_err(|_| {
1420 BinanceFuturesHttpError::ValidationError(format!(
1421 "Invalid Binance Futures quantity precision {} for {}",
1422 self.quantity_precision(),
1423 self.symbol()
1424 ))
1425 })?;
1426
1427 if price_precision > FIXED_PRECISION || quantity_precision > FIXED_PRECISION {
1428 return Err(BinanceFuturesHttpError::ValidationError(format!(
1429 "Binance Futures precision exceeds maximum {FIXED_PRECISION} for {}: price={price_precision}, quantity={quantity_precision}",
1430 self.symbol()
1431 )));
1432 }
1433
1434 Ok((price_precision, quantity_precision))
1435 }
1436
1437 #[must_use]
1439 pub fn id(&self) -> InstrumentId {
1440 match self {
1441 Self::UsdM(s) => format_instrument_id(&s.symbol, BinanceProductType::UsdM),
1442 Self::CoinM(s) => format_instrument_id(&s.symbol, BinanceProductType::CoinM),
1443 }
1444 }
1445
1446 #[must_use]
1448 pub fn quote_currency(&self) -> Currency {
1449 let quote_asset = match self {
1450 Self::UsdM(s) => &s.quote_asset,
1451 Self::CoinM(s) => &s.quote_asset,
1452 };
1453 Currency::get_or_create_crypto_with_context(quote_asset.as_str(), Some("futures quote"))
1454 }
1455}
1456
1457#[derive(Debug, Clone)]
1459pub struct BinanceFuturesHttpClient {
1460 inner: Arc<BinanceRawFuturesHttpClient>,
1461 product_type: BinanceProductType,
1462 clock: &'static AtomicTime,
1463 instruments: Arc<DashMap<Ustr, BinanceFuturesInstrument>>,
1464 instruments_reconciliation: Arc<AtomicMap<InstrumentId, InstrumentAny>>,
1465 instruments_load_lock: Arc<tokio::sync::Mutex<()>>,
1466 treat_expired_as_canceled: bool,
1467}
1468
1469impl BinanceFuturesHttpClient {
1470 #[expect(clippy::too_many_arguments)]
1476 pub fn new(
1477 product_type: BinanceProductType,
1478 environment: BinanceEnvironment,
1479 clock: &'static AtomicTime,
1480 api_key: Option<String>,
1481 api_secret: Option<String>,
1482 base_url_override: Option<String>,
1483 recv_window: Option<u64>,
1484 timeout_secs: Option<u64>,
1485 proxy_url: Option<String>,
1486 treat_expired_as_canceled: bool,
1487 ) -> BinanceFuturesHttpResult<Self> {
1488 match product_type {
1489 BinanceProductType::UsdM | BinanceProductType::CoinM => {}
1490 _ => {
1491 return Err(BinanceFuturesHttpError::ValidationError(format!(
1492 "BinanceFuturesHttpClient requires UsdM or CoinM product type, was {product_type:?}"
1493 )));
1494 }
1495 }
1496
1497 let raw = BinanceRawFuturesHttpClient::new(
1498 product_type,
1499 environment,
1500 api_key,
1501 api_secret,
1502 base_url_override,
1503 recv_window,
1504 timeout_secs,
1505 proxy_url,
1506 )?;
1507
1508 Ok(Self {
1509 inner: Arc::new(raw),
1510 product_type,
1511 clock,
1512 instruments: Arc::new(DashMap::new()),
1513 instruments_reconciliation: Arc::new(AtomicMap::new()),
1514 instruments_load_lock: Arc::new(tokio::sync::Mutex::new(())),
1515 treat_expired_as_canceled,
1516 })
1517 }
1518
1519 #[must_use]
1521 pub const fn product_type(&self) -> BinanceProductType {
1522 self.product_type
1523 }
1524
1525 #[must_use]
1527 pub fn inner(&self) -> &BinanceRawFuturesHttpClient {
1528 &self.inner
1529 }
1530
1531 #[must_use]
1533 pub fn instruments_cache(&self) -> Arc<DashMap<Ustr, BinanceFuturesInstrument>> {
1534 Arc::clone(&self.instruments)
1535 }
1536
1537 pub(crate) fn instrument_reconciliation(
1539 &self,
1540 instrument_id: &InstrumentId,
1541 ) -> Option<InstrumentAny> {
1542 self.instruments_reconciliation.get_cloned(instrument_id)
1543 }
1544
1545 #[must_use]
1547 pub fn has_credentials(&self) -> bool {
1548 self.inner.has_credentials()
1549 }
1550
1551 fn replace_instruments(
1553 &self,
1554 instruments: Vec<(Ustr, BinanceFuturesInstrument)>,
1555 ) -> BinanceFuturesHttpResult<()> {
1556 let mut snapshot = AHashMap::with_capacity(instruments.len());
1557 for (symbol, instrument) in instruments {
1558 instrument.precisions()?;
1559 if instrument.symbol() != symbol {
1560 return Err(BinanceFuturesHttpError::ValidationError(format!(
1561 "Binance Futures catalogue key {symbol} does not match instrument symbol {}",
1562 instrument.symbol()
1563 )));
1564 }
1565 let expected_id = format_instrument_id(&symbol, self.product_type);
1566 if instrument.id() != expected_id {
1567 return Err(BinanceFuturesHttpError::ValidationError(format!(
1568 "Binance Futures catalogue instrument ID {} does not match expected ID {expected_id}",
1569 instrument.id()
1570 )));
1571 }
1572
1573 if snapshot.insert(symbol, instrument).is_some() {
1574 return Err(BinanceFuturesHttpError::ValidationError(format!(
1575 "Duplicate Binance Futures catalogue symbol {symbol}"
1576 )));
1577 }
1578 }
1579
1580 let symbols: AHashSet<_> = snapshot.keys().copied().collect();
1581 for (symbol, instrument) in snapshot {
1582 self.instruments.insert(symbol, instrument);
1583 }
1584 self.instruments
1585 .retain(|symbol, _| symbols.contains(symbol));
1586 Ok(())
1587 }
1588
1589 pub async fn server_time(&self) -> BinanceFuturesHttpResult<BinanceServerTime> {
1595 self.inner
1596 .get::<_, BinanceServerTime>("time", None::<&()>, false, false)
1597 .await
1598 }
1599
1600 pub async fn set_leverage(
1606 &self,
1607 params: &BinanceSetLeverageParams,
1608 ) -> BinanceFuturesHttpResult<BinanceLeverageResponse> {
1609 self.inner.set_leverage(params).await
1610 }
1611
1612 pub async fn set_margin_type(
1618 &self,
1619 params: &BinanceSetMarginTypeParams,
1620 ) -> BinanceFuturesHttpResult<serde_json::Value> {
1621 self.inner.set_margin_type(params).await
1622 }
1623
1624 pub async fn query_hedge_mode(&self) -> BinanceFuturesHttpResult<BinanceHedgeModeResponse> {
1630 self.inner.query_hedge_mode().await
1631 }
1632
1633 pub async fn create_listen_key(&self) -> BinanceFuturesHttpResult<ListenKeyResponse> {
1639 self.inner.create_listen_key().await
1640 }
1641
1642 pub async fn keepalive_listen_key(&self, listen_key: &str) -> BinanceFuturesHttpResult<()> {
1648 self.inner.keepalive_listen_key(listen_key).await
1649 }
1650
1651 pub async fn close_listen_key(&self, listen_key: &str) -> BinanceFuturesHttpResult<()> {
1657 self.inner.close_listen_key(listen_key).await
1658 }
1659
1660 pub async fn exchange_info(&self) -> BinanceFuturesHttpResult<()> {
1666 let _guard = self.instruments_load_lock.lock().await;
1667 let instruments = match self.product_type {
1668 BinanceProductType::UsdM => {
1669 let info: BinanceFuturesUsdExchangeInfo = self
1670 .inner
1671 .get("exchangeInfo", None::<&()>, false, false)
1672 .await?;
1673
1674 info.symbols
1675 .into_iter()
1676 .map(|symbol| (symbol.symbol, BinanceFuturesInstrument::UsdM(symbol)))
1677 .collect()
1678 }
1679 BinanceProductType::CoinM => {
1680 let info: BinanceFuturesCoinExchangeInfo = self
1681 .inner
1682 .get("exchangeInfo", None::<&()>, false, false)
1683 .await?;
1684
1685 info.symbols
1686 .into_iter()
1687 .map(|symbol| (symbol.symbol, BinanceFuturesInstrument::CoinM(symbol)))
1688 .collect()
1689 }
1690 _ => {
1691 return Err(BinanceFuturesHttpError::ValidationError(
1692 "Invalid product type for futures".to_string(),
1693 ));
1694 }
1695 };
1696
1697 self.replace_instruments(instruments)
1698 }
1699
1700 pub async fn request_symbol_statuses(
1710 &self,
1711 ) -> BinanceFuturesHttpResult<AHashMap<Ustr, MarketStatusAction>> {
1712 let mut statuses = AHashMap::new();
1713
1714 match self.product_type {
1715 BinanceProductType::UsdM => {
1716 let info: BinanceFuturesUsdExchangeInfo = self
1717 .inner
1718 .get("exchangeInfo", None::<&()>, false, false)
1719 .await?;
1720
1721 for symbol in &info.symbols {
1722 statuses.insert(symbol.symbol, MarketStatusAction::from(symbol.status));
1723 }
1724 }
1725 BinanceProductType::CoinM => {
1726 let info: BinanceFuturesCoinExchangeInfo = self
1727 .inner
1728 .get("exchangeInfo", None::<&()>, false, false)
1729 .await?;
1730
1731 for symbol in &info.symbols {
1732 let action = symbol
1733 .contract_status
1734 .map_or(MarketStatusAction::NotAvailableForTrading, Into::into);
1735 statuses.insert(symbol.symbol, action);
1736 }
1737 }
1738 _ => {
1739 return Err(BinanceFuturesHttpError::ValidationError(
1740 "Invalid product type for futures".to_string(),
1741 ));
1742 }
1743 }
1744
1745 Ok(statuses)
1746 }
1747
1748 pub async fn request_instruments(&self) -> BinanceFuturesHttpResult<Vec<InstrumentAny>> {
1754 self.request_instruments_with_config(&BinanceInstrumentProviderConfig::default())
1755 .await
1756 }
1757
1758 pub async fn request_instruments_with_config(
1767 &self,
1768 config: &BinanceInstrumentProviderConfig,
1769 ) -> BinanceFuturesHttpResult<Vec<InstrumentAny>> {
1770 let _guard = self.instruments_load_lock.lock().await;
1771 config
1772 .validate(self.product_type)
1773 .map_err(|e| BinanceFuturesHttpError::ValidationError(e.to_string()))?;
1774 let selector = BinanceInstrumentSelector::new(config)
1775 .map_err(|e| BinanceFuturesHttpError::ValidationError(e.to_string()))?;
1776 let ts_init = UnixNanos::default();
1777 let fallback_fees = self.futures_fallback_fees(config).await;
1778 let mut cache = Vec::new();
1779 let mut reconciliation = AHashMap::new();
1780
1781 let instruments = match self.product_type {
1782 BinanceProductType::UsdM => {
1783 let info: BinanceFuturesUsdExchangeInfo = self
1784 .inner
1785 .get("exchangeInfo", None::<&()>, false, false)
1786 .await?;
1787
1788 let mut instruments = Vec::with_capacity(info.symbols.len());
1789
1790 for symbol in info.symbols {
1791 let instrument_id =
1792 format_instrument_id(&symbol.symbol, BinanceProductType::UsdM);
1793 cache.push((
1794 symbol.symbol,
1795 BinanceFuturesInstrument::UsdM(symbol.clone()),
1796 ));
1797
1798 if !selector.includes(
1799 instrument_id,
1800 &symbol.symbol,
1801 &symbol.base_asset,
1802 &symbol.quote_asset,
1803 Some(&symbol.contract_type),
1804 ) {
1805 continue;
1806 }
1807
1808 let fees = self
1809 .futures_symbol_fees(config, &symbol.symbol, fallback_fees)
1810 .await;
1811
1812 match parse_usdm_instrument_with_fees(
1813 &symbol,
1814 Some(fees.0),
1815 Some(fees.1),
1816 ts_init,
1817 ts_init,
1818 ) {
1819 Ok(instrument) => {
1820 validate_reconciliation_instrument(
1821 &mut reconciliation,
1822 instrument_id,
1823 &instrument,
1824 )?;
1825 instruments.push(instrument);
1826 }
1827 Err(e) => {
1828 log_futures_instrument_parse_error(config, &symbol.symbol, &e);
1829 }
1830 }
1831 }
1832
1833 log::debug!(
1834 "Loaded USD-M Futures instruments: count={}",
1835 instruments.len()
1836 );
1837 instruments
1838 }
1839 BinanceProductType::CoinM => {
1840 let info: BinanceFuturesCoinExchangeInfo = self
1841 .inner
1842 .get("exchangeInfo", None::<&()>, false, false)
1843 .await?;
1844
1845 let mut instruments = Vec::with_capacity(info.symbols.len());
1846 for symbol in info.symbols {
1847 let instrument_id =
1848 format_instrument_id(&symbol.symbol, BinanceProductType::CoinM);
1849 cache.push((
1850 symbol.symbol,
1851 BinanceFuturesInstrument::CoinM(symbol.clone()),
1852 ));
1853
1854 if !selector.includes(
1855 instrument_id,
1856 &symbol.symbol,
1857 &symbol.base_asset,
1858 &symbol.quote_asset,
1859 Some(&symbol.contract_type),
1860 ) {
1861 continue;
1862 }
1863
1864 let fees = self
1865 .futures_symbol_fees(config, &symbol.symbol, fallback_fees)
1866 .await;
1867
1868 match parse_coinm_instrument_with_fees(
1869 &symbol,
1870 Some(fees.0),
1871 Some(fees.1),
1872 ts_init,
1873 ts_init,
1874 ) {
1875 Ok(instrument) => {
1876 validate_reconciliation_instrument(
1877 &mut reconciliation,
1878 instrument_id,
1879 &instrument,
1880 )?;
1881 instruments.push(instrument);
1882 }
1883 Err(e) => {
1884 log_futures_instrument_parse_error(config, &symbol.symbol, &e);
1885 }
1886 }
1887 }
1888
1889 log::debug!(
1890 "Loaded COIN-M Futures instruments: count={}",
1891 instruments.len()
1892 );
1893 instruments
1894 }
1895 _ => {
1896 return Err(BinanceFuturesHttpError::ValidationError(
1897 "Invalid product type for futures".to_string(),
1898 ));
1899 }
1900 };
1901
1902 self.replace_instruments(cache)?;
1903 self.instruments_reconciliation.store(reconciliation);
1904 Ok(instruments)
1905 }
1906
1907 async fn futures_fallback_fees(
1908 &self,
1909 config: &BinanceInstrumentProviderConfig,
1910 ) -> (Decimal, Decimal) {
1911 if !self.has_credentials() {
1912 return futures_fee_tier_rates(0);
1913 }
1914
1915 match self.query_account().await {
1916 Ok(account) => futures_fee_tier_rates(account.fee_tier),
1917 Err(e) => {
1918 if config.log_warnings {
1919 log::warn!("Unable to query Binance Futures fee tier; using VIP 0 rates: {e}");
1920 } else {
1921 log::debug!("Unable to query Binance Futures fee tier; using VIP 0 rates: {e}");
1922 }
1923 futures_fee_tier_rates(0)
1924 }
1925 }
1926 }
1927
1928 async fn futures_symbol_fees(
1929 &self,
1930 config: &BinanceInstrumentProviderConfig,
1931 symbol: &str,
1932 fallback: (Decimal, Decimal),
1933 ) -> (Decimal, Decimal) {
1934 if !config.query_commission_rates || !self.has_credentials() {
1935 return fallback;
1936 }
1937
1938 let params = BinanceCommissionRateParams {
1939 symbol: symbol.to_string(),
1940 };
1941
1942 match self.inner.commission_rate(¶ms).await {
1943 Ok(response) => parse_futures_commission_rates(&response).unwrap_or_else(|e| {
1944 log_futures_commission_fallback(config, symbol, &e, fallback);
1945 fallback
1946 }),
1947 Err(e) => {
1948 log_futures_commission_fallback(config, symbol, &e, fallback);
1949 fallback
1950 }
1951 }
1952 }
1953
1954 pub async fn ticker_24h(
1960 &self,
1961 params: &BinanceTicker24hrParams,
1962 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesTicker24hr>> {
1963 self.inner.ticker_24h(params).await
1964 }
1965
1966 pub async fn book_ticker(
1972 &self,
1973 params: &BinanceBookTickerParams,
1974 ) -> BinanceFuturesHttpResult<Vec<BinanceBookTicker>> {
1975 self.inner.book_ticker(params).await
1976 }
1977
1978 pub async fn price_ticker(
1984 &self,
1985 symbol: Option<&str>,
1986 ) -> BinanceFuturesHttpResult<Vec<BinancePriceTicker>> {
1987 self.inner.price_ticker(symbol).await
1988 }
1989
1990 pub async fn depth(
1996 &self,
1997 params: &BinanceDepthParams,
1998 ) -> BinanceFuturesHttpResult<BinanceOrderBook> {
1999 self.inner.depth(params).await
2000 }
2001
2002 pub async fn mark_price(
2008 &self,
2009 params: &BinanceMarkPriceParams,
2010 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesMarkPrice>> {
2011 self.inner.mark_price(params).await
2012 }
2013
2014 pub async fn funding_rate(
2020 &self,
2021 params: &BinanceFundingRateParams,
2022 ) -> BinanceFuturesHttpResult<Vec<BinanceFundingRate>> {
2023 self.inner.funding_rate(params).await
2024 }
2025
2026 pub async fn open_interest(
2032 &self,
2033 params: &BinanceOpenInterestParams,
2034 ) -> BinanceFuturesHttpResult<BinanceOpenInterest> {
2035 self.inner.open_interest(params).await
2036 }
2037
2038 pub async fn open_interest_hist(
2044 &self,
2045 params: &BinanceOpenInterestHistParams,
2046 ) -> BinanceFuturesHttpResult<Vec<BinanceOpenInterestHistRecord>> {
2047 self.inner.open_interest_hist(params).await
2048 }
2049
2050 pub async fn query_order(
2056 &self,
2057 params: &BinanceOrderQueryParams,
2058 ) -> BinanceFuturesHttpResult<BinanceFuturesOrder> {
2059 self.inner.query_order(params).await
2060 }
2061
2062 pub async fn query_open_orders(
2068 &self,
2069 params: &BinanceOpenOrdersParams,
2070 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesOrder>> {
2071 self.inner.query_open_orders(params).await
2072 }
2073
2074 pub async fn query_all_orders(
2080 &self,
2081 params: &BinanceAllOrdersParams,
2082 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesOrder>> {
2083 self.inner.query_all_orders(params).await
2084 }
2085
2086 pub async fn query_account(&self) -> BinanceFuturesHttpResult<BinanceFuturesAccountInfo> {
2092 self.inner.query_account().await
2093 }
2094
2095 pub async fn query_positions(
2101 &self,
2102 params: &BinancePositionRiskParams,
2103 ) -> BinanceFuturesHttpResult<Vec<BinancePositionRisk>> {
2104 self.inner.query_positions(params).await
2105 }
2106
2107 pub async fn query_user_trades(
2113 &self,
2114 params: &BinanceUserTradesParams,
2115 ) -> BinanceFuturesHttpResult<Vec<BinanceUserTrade>> {
2116 self.inner.query_user_trades(params).await
2117 }
2118
2119 #[expect(clippy::too_many_arguments)]
2129 pub async fn submit_order(
2130 &self,
2131 account_id: AccountId,
2132 instrument_id: InstrumentId,
2133 client_order_id: ClientOrderId,
2134 order_side: OrderSide,
2135 order_type: OrderType,
2136 quantity: Quantity,
2137 time_in_force: TimeInForce,
2138 price: Option<Price>,
2139 trigger_price: Option<Price>,
2140 reduce_only: bool,
2141 post_only: bool,
2142 position_side: Option<BinancePositionSide>,
2143 price_match: Option<BinancePriceMatch>,
2144 good_till_date: Option<i64>,
2145 ) -> anyhow::Result<OrderStatusReport> {
2146 let (symbol, price_precision, size_precision) =
2147 self.cached_precisions_by_id(instrument_id)?;
2148
2149 let binance_side = BinanceSide::try_from(order_side)?;
2150 let binance_order_type = order_type_to_binance_futures(order_type)?;
2151 let binance_tif = if post_only {
2152 BinanceTimeInForce::Gtx
2153 } else {
2154 BinanceTimeInForce::try_from(time_in_force)?
2155 };
2156
2157 let requires_trigger_price = matches!(
2158 order_type,
2159 OrderType::StopMarket
2160 | OrderType::StopLimit
2161 | OrderType::TrailingStopMarket
2162 | OrderType::MarketIfTouched
2163 | OrderType::LimitIfTouched
2164 );
2165
2166 if requires_trigger_price && trigger_price.is_none() {
2167 anyhow::bail!("Order type {order_type:?} requires a trigger price");
2168 }
2169
2170 let requires_time_in_force = matches!(
2172 order_type,
2173 OrderType::Limit | OrderType::StopLimit | OrderType::LimitIfTouched
2174 );
2175
2176 let qty_str = quantity.to_string();
2177 let price_str = if price_match.is_some() {
2178 None
2179 } else {
2180 price.map(|p| p.to_string())
2181 };
2182 let stop_price_str = trigger_price.map(|p| p.to_string());
2183 let client_id_str = encode_broker_id(&client_order_id, BINANCE_NAUTILUS_FUTURES_BROKER_ID);
2184
2185 let params = BinanceNewOrderParams {
2186 symbol,
2187 side: binance_side,
2188 order_type: binance_order_type,
2189 time_in_force: if requires_time_in_force {
2190 Some(binance_tif)
2191 } else {
2192 None
2193 },
2194 quantity: Some(qty_str),
2195 price: price_str,
2196 new_client_order_id: Some(client_id_str),
2197 stop_price: stop_price_str,
2198 reduce_only: reduce_only_param(reduce_only, position_side),
2199 position_side,
2200 close_position: None,
2201 activation_price: None,
2202 callback_rate: None,
2203 working_type: None,
2204 price_protect: None,
2205 new_order_resp_type: None,
2206 good_till_date,
2207 recv_window: None,
2208 price_match,
2209 self_trade_prevention_mode: None,
2210 };
2211
2212 let order = self.inner.submit_order(¶ms).await?;
2213 let ts_init = self.clock.get_time_ns();
2214 order.to_order_status_report(
2215 account_id,
2216 instrument_id,
2217 price_precision,
2218 size_precision,
2219 self.treat_expired_as_canceled,
2220 ts_init,
2221 )
2222 }
2223
2224 #[expect(clippy::too_many_arguments)]
2237 pub async fn submit_algo_order(
2238 &self,
2239 account_id: AccountId,
2240 instrument_id: InstrumentId,
2241 client_order_id: ClientOrderId,
2242 order_side: OrderSide,
2243 order_type: OrderType,
2244 quantity: Quantity,
2245 time_in_force: TimeInForce,
2246 price: Option<Price>,
2247 trigger_price: Option<Price>,
2248 reduce_only: bool,
2249 close_position: bool,
2250 position_side: Option<BinancePositionSide>,
2251 activation_price: Option<Price>,
2252 callback_rate: Option<String>,
2253 working_type: Option<BinanceWorkingType>,
2254 good_till_date: Option<i64>,
2255 ) -> anyhow::Result<OrderStatusReport> {
2256 let (symbol, price_precision, size_precision) =
2257 self.cached_precisions_by_id(instrument_id)?;
2258
2259 let binance_side = BinanceSide::try_from(order_side)?;
2260 let binance_order_type = order_type_to_binance_futures(order_type)?;
2261 let binance_tif = BinanceTimeInForce::try_from(time_in_force)?;
2262
2263 let requires_trigger_price = matches!(
2264 order_type,
2265 OrderType::StopMarket
2266 | OrderType::StopLimit
2267 | OrderType::MarketIfTouched
2268 | OrderType::LimitIfTouched
2269 );
2270 anyhow::ensure!(
2271 !requires_trigger_price || trigger_price.is_some(),
2272 "Algo order type {order_type:?} requires a trigger price"
2273 );
2274
2275 let requires_time_in_force =
2277 matches!(order_type, OrderType::StopLimit | OrderType::LimitIfTouched);
2278
2279 let price_str = price.map(|p| p.to_string());
2280 let trigger_price_str = if matches!(order_type, OrderType::TrailingStopMarket) {
2281 None
2282 } else {
2283 trigger_price.map(|p| p.to_string())
2284 };
2285 let reduce_only = reduce_only_param(reduce_only, position_side);
2286 let client_id_str = encode_broker_id(&client_order_id, BINANCE_NAUTILUS_FUTURES_BROKER_ID);
2287
2288 let params = if close_position {
2290 BinanceNewAlgoOrderParams {
2291 symbol,
2292 side: binance_side,
2293 order_type: binance_order_type,
2294 algo_type: BinanceAlgoType::Conditional,
2295 position_side,
2296 quantity: None,
2297 price: price_str,
2298 trigger_price: trigger_price_str,
2299 time_in_force: if requires_time_in_force {
2300 Some(binance_tif)
2301 } else {
2302 None
2303 },
2304 working_type,
2305 close_position: Some(true),
2306 price_protect: None,
2307 reduce_only: None,
2308 activation_price: activation_price.map(|p| p.to_string()),
2309 callback_rate,
2310 client_algo_id: Some(client_id_str),
2311 good_till_date,
2312 recv_window: None,
2313 }
2314 } else {
2315 let qty_str = quantity.to_string();
2316 BinanceNewAlgoOrderParams {
2317 symbol,
2318 side: binance_side,
2319 order_type: binance_order_type,
2320 algo_type: BinanceAlgoType::Conditional,
2321 position_side,
2322 quantity: Some(qty_str),
2323 price: price_str,
2324 trigger_price: trigger_price_str,
2325 time_in_force: if requires_time_in_force {
2326 Some(binance_tif)
2327 } else {
2328 None
2329 },
2330 working_type,
2331 close_position: None,
2332 price_protect: None,
2333 reduce_only,
2334 activation_price: activation_price.map(|p| p.to_string()),
2335 callback_rate,
2336 client_algo_id: Some(client_id_str),
2337 good_till_date,
2338 recv_window: None,
2339 }
2340 };
2341
2342 let order = self.inner.submit_algo_order(¶ms).await?;
2343 let ts_init = self.clock.get_time_ns();
2344 order.to_order_status_report(
2345 account_id,
2346 instrument_id,
2347 price_precision,
2348 size_precision,
2349 ts_init,
2350 )
2351 }
2352
2353 pub async fn submit_order_list(
2362 &self,
2363 orders: &[BatchOrderItem],
2364 ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
2365 self.inner.submit_order_list(orders).await
2366 }
2367
2368 #[expect(clippy::too_many_arguments)]
2379 pub async fn modify_order(
2380 &self,
2381 account_id: AccountId,
2382 instrument_id: InstrumentId,
2383 venue_order_id: Option<VenueOrderId>,
2384 client_order_id: Option<ClientOrderId>,
2385 order_side: OrderSide,
2386 quantity: Quantity,
2387 price: Price,
2388 ) -> anyhow::Result<OrderStatusReport> {
2389 anyhow::ensure!(
2390 venue_order_id.is_some() || client_order_id.is_some(),
2391 "Either venue_order_id or client_order_id must be provided"
2392 );
2393
2394 let (symbol, price_precision, size_precision) =
2395 self.cached_precisions_by_id(instrument_id)?;
2396
2397 let binance_side = BinanceSide::try_from(order_side)?;
2398
2399 let order_id = venue_order_id
2400 .map(|id| id.inner().parse::<i64>())
2401 .transpose()
2402 .map_err(|_| anyhow::anyhow!("Invalid venue order ID"))?;
2403
2404 let params = BinanceModifyOrderParams {
2405 symbol,
2406 order_id,
2407 orig_client_order_id: client_order_id
2408 .map(|id| encode_broker_id(&id, BINANCE_NAUTILUS_FUTURES_BROKER_ID)),
2409 side: binance_side,
2410 quantity: quantity.to_string(),
2411 price: price.to_string(),
2412 recv_window: None,
2413 };
2414
2415 let order = self.inner.modify_order(¶ms).await?;
2416 let ts_init = self.clock.get_time_ns();
2417 order.to_order_status_report(
2418 account_id,
2419 instrument_id,
2420 price_precision,
2421 size_precision,
2422 self.treat_expired_as_canceled,
2423 ts_init,
2424 )
2425 }
2426
2427 pub async fn batch_modify_orders(
2436 &self,
2437 modifies: &[BatchModifyItem],
2438 ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
2439 self.inner.batch_modify_orders(modifies).await
2440 }
2441
2442 pub async fn cancel_order(
2452 &self,
2453 instrument_id: InstrumentId,
2454 venue_order_id: Option<VenueOrderId>,
2455 client_order_id: Option<ClientOrderId>,
2456 ) -> anyhow::Result<VenueOrderId> {
2457 anyhow::ensure!(
2458 venue_order_id.is_some() || client_order_id.is_some(),
2459 "Either venue_order_id or client_order_id must be provided"
2460 );
2461
2462 let symbol = format_binance_symbol(&instrument_id);
2463
2464 let order_id = match venue_order_id {
2465 Some(venue_order_id) => match venue_order_id.inner().parse::<i64>() {
2466 Ok(order_id) => Some(order_id),
2467 Err(e) if client_order_id.is_some() => {
2468 log::warn!(
2469 "Unable to parse venue_order_id {venue_order_id} for cancel, canceling by client_order_id: {e}"
2470 );
2471 None
2472 }
2473 Err(e) => anyhow::bail!("Invalid venue order ID: {e}"),
2474 },
2475 None => None,
2476 };
2477
2478 let params = BinanceCancelOrderParams {
2479 symbol,
2480 order_id,
2481 orig_client_order_id: client_order_id
2482 .map(|id| encode_broker_id(&id, BINANCE_NAUTILUS_FUTURES_BROKER_ID)),
2483 recv_window: None,
2484 };
2485
2486 let order = self.inner.cancel_order(¶ms).await?;
2487 Ok(VenueOrderId::new(order.order_id.to_string()))
2488 }
2489
2490 pub async fn cancel_algo_order(&self, client_order_id: ClientOrderId) -> anyhow::Result<()> {
2499 let params = BinanceAlgoOrderQueryParams {
2500 algo_id: None,
2501 client_algo_id: Some(encode_broker_id(
2502 &client_order_id,
2503 BINANCE_NAUTILUS_FUTURES_BROKER_ID,
2504 )),
2505 recv_window: None,
2506 };
2507
2508 let response = self.inner.cancel_algo_order(¶ms).await?;
2509 if response.code.parse::<i32>().unwrap_or(0) == 200 {
2510 Ok(())
2511 } else {
2512 anyhow::bail!(
2513 "Cancel algo order failed: code={}, msg={}",
2514 response.code,
2515 response.msg
2516 )
2517 }
2518 }
2519
2520 pub async fn cancel_all_orders(
2526 &self,
2527 instrument_id: InstrumentId,
2528 ) -> anyhow::Result<Vec<VenueOrderId>> {
2529 let symbol = format_binance_symbol(&instrument_id);
2530
2531 let params = BinanceCancelAllOrdersParams {
2532 symbol,
2533 recv_window: None,
2534 };
2535
2536 let response = self.inner.cancel_all_orders(¶ms).await?;
2537 if response.code == 200 {
2538 Ok(vec![])
2539 } else {
2540 anyhow::bail!("Cancel all orders failed: {}", response.msg);
2541 }
2542 }
2543
2544 pub async fn cancel_all_algo_orders(&self, instrument_id: InstrumentId) -> anyhow::Result<()> {
2550 let symbol = format_binance_symbol(&instrument_id);
2551
2552 let params = BinanceCancelAllAlgoOrdersParams {
2553 symbol,
2554 recv_window: None,
2555 };
2556
2557 let response = self.inner.cancel_all_algo_orders(¶ms).await?;
2558 if response.code == 200 {
2559 Ok(())
2560 } else {
2561 anyhow::bail!("Cancel all algo orders failed: {}", response.msg);
2562 }
2563 }
2564
2565 pub async fn batch_cancel_orders(
2574 &self,
2575 cancels: &[BatchCancelItem],
2576 ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
2577 self.inner.batch_cancel_orders(cancels).await
2578 }
2579
2580 pub async fn query_open_algo_orders(
2588 &self,
2589 instrument_id: Option<InstrumentId>,
2590 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesAlgoOrder>> {
2591 let symbol = instrument_id.map(|id| format_binance_symbol(&id));
2592
2593 let params = BinanceOpenAlgoOrdersParams {
2594 symbol,
2595 recv_window: None,
2596 };
2597
2598 self.inner.query_open_algo_orders(¶ms).await
2599 }
2600
2601 pub async fn query_algo_order(
2607 &self,
2608 client_order_id: ClientOrderId,
2609 ) -> BinanceFuturesHttpResult<BinanceFuturesAlgoOrder> {
2610 let params = BinanceAlgoOrderQueryParams {
2611 algo_id: None,
2612 client_algo_id: Some(encode_broker_id(
2613 &client_order_id,
2614 BINANCE_NAUTILUS_FUTURES_BROKER_ID,
2615 )),
2616 recv_window: None,
2617 };
2618
2619 self.inner.query_algo_order(¶ms).await
2620 }
2621
2622 pub async fn query_algo_order_by_venue_order_id(
2628 &self,
2629 venue_order_id: VenueOrderId,
2630 ) -> BinanceFuturesHttpResult<BinanceFuturesAlgoOrder> {
2631 let algo_id = venue_order_id
2632 .inner()
2633 .parse::<i64>()
2634 .map_err(|e| BinanceFuturesHttpError::ValidationError(e.to_string()))?;
2635 let params = BinanceAlgoOrderQueryParams {
2636 algo_id: Some(algo_id),
2637 client_algo_id: None,
2638 recv_window: None,
2639 };
2640
2641 self.inner.query_algo_order(¶ms).await
2642 }
2643
2644 async fn query_historical_algo_order_by_venue_order_id(
2645 &self,
2646 instrument_id: InstrumentId,
2647 venue_order_id: VenueOrderId,
2648 ) -> BinanceFuturesHttpResult<Option<BinanceFuturesAlgoOrder>> {
2649 let algo_id = venue_order_id
2650 .inner()
2651 .parse::<i64>()
2652 .map_err(|e| BinanceFuturesHttpError::ValidationError(e.to_string()))?;
2653 let symbol = format_binance_symbol(&instrument_id);
2654 let params = BinanceAllAlgoOrdersParams {
2655 symbol,
2656 algo_id: Some(algo_id),
2657 start_time: None,
2658 end_time: None,
2659 page: None,
2660 limit: Some(1),
2661 recv_window: None,
2662 };
2663 let order = self
2664 .inner
2665 .query_all_algo_orders(¶ms)
2666 .await?
2667 .into_iter()
2668 .next()
2669 .filter(|order| order.algo_id == algo_id);
2670
2671 Ok(order)
2672 }
2673
2674 pub async fn query_algo_order_with_history(
2685 &self,
2686 instrument_id: InstrumentId,
2687 client_order_id: Option<ClientOrderId>,
2688 algo_venue_order_id: Option<VenueOrderId>,
2689 ) -> BinanceFuturesHttpResult<Option<BinanceFuturesAlgoOrderQueryResult>> {
2690 let order = if let Some(venue_order_id) = algo_venue_order_id {
2691 match self
2692 .query_algo_order_by_venue_order_id(venue_order_id)
2693 .await
2694 {
2695 Ok(order) => Some(order),
2696 Err(BinanceFuturesHttpError::BinanceError { code: -2013, .. }) => {
2697 self.query_historical_algo_order_by_venue_order_id(
2698 instrument_id,
2699 venue_order_id,
2700 )
2701 .await?
2702 }
2703 Err(e) => return Err(e),
2704 }
2705 } else {
2706 let Some(client_order_id) = client_order_id else {
2707 return Ok(None);
2708 };
2709
2710 match self.query_algo_order(client_order_id).await {
2711 Ok(order) => Some(order),
2712 Err(BinanceFuturesHttpError::BinanceError { code: -2013, .. }) => None,
2713 Err(e) => return Err(e),
2714 }
2715 };
2716
2717 let Some(order) = order else {
2718 return Ok(None);
2719 };
2720 let actual = if let Some(actual_order_id) = order
2721 .actual_order_id
2722 .as_deref()
2723 .filter(|id| !id.is_empty())
2724 .map(str::parse::<i64>)
2725 .transpose()
2726 .map_err(|e| BinanceFuturesHttpError::ValidationError(e.to_string()))?
2727 {
2728 let params = BinanceOrderQueryParams {
2729 symbol: format_binance_symbol(&instrument_id),
2730 order_id: Some(actual_order_id),
2731 orig_client_order_id: None,
2732 recv_window: None,
2733 };
2734
2735 match self.inner.query_order(¶ms).await {
2736 Ok(actual) => Some(actual),
2737 Err(
2738 e @ (BinanceFuturesHttpError::MissingCredentials
2739 | BinanceFuturesHttpError::ValidationError(_)),
2740 ) => return Err(e),
2741 Err(e) => {
2742 log::warn!(
2743 "Failed to enrich algo order with matching-engine order \
2744 {actual_order_id}: {e}; falling back to Algo Service execution fields"
2745 );
2746 None
2747 }
2748 }
2749 } else {
2750 None
2751 };
2752
2753 Ok(Some(BinanceFuturesAlgoOrderQueryResult {
2754 algo: order,
2755 actual,
2756 }))
2757 }
2758
2759 pub async fn request_account_state(
2765 &self,
2766 account_id: AccountId,
2767 ) -> anyhow::Result<AccountState> {
2768 let ts_init = UnixNanos::default();
2769 let account_info = self.inner.query_account().await?;
2770 account_info.to_account_state(account_id, ts_init)
2771 }
2772
2773 pub async fn request_order_status_report(
2781 &self,
2782 account_id: AccountId,
2783 instrument_id: InstrumentId,
2784 venue_order_id: Option<VenueOrderId>,
2785 client_order_id: Option<ClientOrderId>,
2786 ) -> anyhow::Result<OrderStatusReport> {
2787 anyhow::ensure!(
2788 venue_order_id.is_some() || client_order_id.is_some(),
2789 "Either venue_order_id or client_order_id must be provided"
2790 );
2791
2792 let (symbol, price_precision, size_precision) =
2793 self.cached_precisions_by_id(instrument_id)?;
2794
2795 let order_id = venue_order_id
2796 .map(|id| id.inner().parse::<i64>())
2797 .transpose()
2798 .map_err(|_| anyhow::anyhow!("Invalid venue order ID"))?;
2799
2800 let orig_client_order_id =
2801 client_order_id.map(|id| encode_broker_id(&id, BINANCE_NAUTILUS_FUTURES_BROKER_ID));
2802
2803 let params = BinanceOrderQueryParams {
2804 symbol,
2805 order_id,
2806 orig_client_order_id,
2807 recv_window: None,
2808 };
2809
2810 let order = self.inner.query_order(¶ms).await?;
2811 let ts_init = self.clock.get_time_ns();
2812 order.to_order_status_report(
2813 account_id,
2814 instrument_id,
2815 price_precision,
2816 size_precision,
2817 self.treat_expired_as_canceled,
2818 ts_init,
2819 )
2820 }
2821
2822 pub async fn request_order_status_reports(
2830 &self,
2831 account_id: AccountId,
2832 instrument_id: Option<InstrumentId>,
2833 open_only: bool,
2834 ) -> anyhow::Result<Vec<OrderStatusReport>> {
2835 let symbol = instrument_id.map(|id| format_binance_symbol(&id));
2836
2837 let orders = if open_only {
2838 let params = BinanceOpenOrdersParams {
2839 symbol: symbol.clone(),
2840 recv_window: None,
2841 };
2842 self.inner.query_open_orders(¶ms).await?
2843 } else {
2844 let symbol = symbol.ok_or_else(|| {
2846 anyhow::anyhow!("instrument_id is required for historical orders")
2847 })?;
2848 let params = BinanceAllOrdersParams {
2849 symbol,
2850 order_id: None,
2851 start_time: None,
2852 end_time: None,
2853 limit: None,
2854 recv_window: None,
2855 };
2856 self.inner.query_all_orders(¶ms).await?
2857 };
2858
2859 let ts_init = self.clock.get_time_ns();
2860 let mut reports = Vec::with_capacity(orders.len());
2861
2862 for order in orders {
2863 let order_instrument_id = instrument_id
2864 .unwrap_or_else(|| format_instrument_id(&order.symbol, self.product_type));
2865 let (_, price_precision, size_precision) =
2866 self.cached_precisions_by_id(order_instrument_id)?;
2867
2868 match order.to_order_status_report(
2869 account_id,
2870 order_instrument_id,
2871 price_precision,
2872 size_precision,
2873 self.treat_expired_as_canceled,
2874 ts_init,
2875 ) {
2876 Ok(report) => reports.push(report),
2877 Err(e) => {
2878 log::warn!("Failed to parse order status report: {e}");
2879 }
2880 }
2881 }
2882
2883 Ok(reports)
2884 }
2885
2886 #[expect(clippy::too_many_arguments)]
2892 pub async fn request_fill_reports(
2893 &self,
2894 account_id: AccountId,
2895 instrument_id: InstrumentId,
2896 venue_order_id: Option<VenueOrderId>,
2897 start: Option<i64>,
2898 end: Option<i64>,
2899 limit: Option<u32>,
2900 bnfcr_currency: Currency,
2901 ) -> anyhow::Result<Vec<FillReport>> {
2902 let (symbol, price_precision, size_precision) =
2903 self.cached_precisions_by_id(instrument_id)?;
2904
2905 let order_id = venue_order_id
2906 .map(|id| id.inner().parse::<i64>())
2907 .transpose()
2908 .map_err(|_| anyhow::anyhow!("Invalid venue order ID"))?;
2909
2910 let params = BinanceUserTradesParams {
2911 symbol,
2912 order_id,
2913 start_time: start,
2914 end_time: end,
2915 from_id: None,
2916 limit,
2917 recv_window: None,
2918 };
2919
2920 let trades = self.inner.query_user_trades(¶ms).await?;
2921
2922 let ts_init = self.clock.get_time_ns();
2923 let mut reports = Vec::with_capacity(trades.len());
2924
2925 for trade in trades {
2926 match trade.to_fill_report(
2927 account_id,
2928 instrument_id,
2929 price_precision,
2930 size_precision,
2931 bnfcr_currency,
2932 ts_init,
2933 ) {
2934 Ok(report) => reports.push(report),
2935 Err(e) => {
2936 log::warn!("Failed to parse fill report: {e}");
2937 }
2938 }
2939 }
2940
2941 Ok(reports)
2942 }
2943
2944 pub async fn request_trades(
2950 &self,
2951 instrument_id: InstrumentId,
2952 limit: Option<u32>,
2953 ) -> anyhow::Result<Vec<TradeTick>> {
2954 let (symbol, price_precision, size_precision) =
2955 self.cached_precisions_by_id(instrument_id)?;
2956
2957 let params = BinanceTradesParams { symbol, limit };
2958
2959 let trades = self.inner.trades(¶ms).await?;
2960 let ts_init = UnixNanos::default();
2961
2962 let mut result = Vec::with_capacity(trades.len());
2963 for trade in trades {
2964 let tick = parse_futures_trade_tick(
2965 &trade,
2966 instrument_id,
2967 price_precision,
2968 size_precision,
2969 ts_init,
2970 )?;
2971 result.push(tick);
2972 }
2973
2974 Ok(result)
2975 }
2976
2977 pub async fn request_agg_trades(
2983 &self,
2984 instrument_id: InstrumentId,
2985 start: Option<Timestamp>,
2986 end: Option<Timestamp>,
2987 limit: Option<u32>,
2988 ) -> anyhow::Result<Vec<TradeTick>> {
2989 let cutoff =
2990 self.clock.get_time_ns().to_datetime_utc() - jiff::SignedDuration::from_hours(24);
2991 anyhow::ensure!(
2992 start.as_ref().is_none_or(|value| value >= &cutoff)
2993 && end.as_ref().is_none_or(|value| value >= &cutoff),
2994 "Binance Futures aggregate trade history is limited to the past 24 hours"
2995 );
2996 let (symbol, price_precision, size_precision) =
2997 self.cached_precisions_by_id(instrument_id)?;
2998 let params = BinanceAggTradesParams {
2999 symbol,
3000 from_id: None,
3001 start_time: start.map(|value| value.as_millisecond()),
3002 end_time: end.map(|value| value.as_millisecond()),
3003 limit,
3004 };
3005 let trades = self.inner.agg_trades(¶ms).await?;
3006 trades
3007 .iter()
3008 .map(|trade| {
3009 let ts_init = parse_millis(trade.time, "Futures aggregate trade time")?;
3010 parse_futures_agg_trade_tick(
3011 trade,
3012 instrument_id,
3013 price_precision,
3014 size_precision,
3015 ts_init,
3016 )
3017 })
3018 .collect()
3019 }
3020
3021 pub async fn request_binance_bars(
3028 &self,
3029 bar_type: BarType,
3030 start: Option<Timestamp>,
3031 end: Option<Timestamp>,
3032 limit: Option<u32>,
3033 ) -> anyhow::Result<Vec<BinanceBar>> {
3034 anyhow::ensure!(
3035 bar_type.aggregation_source() == AggregationSource::External,
3036 "Only EXTERNAL aggregation is supported"
3037 );
3038
3039 let spec = bar_type.spec();
3040 let step = spec.step.get();
3041 let interval = match spec.aggregation {
3042 BarAggregation::Second => {
3043 anyhow::bail!("Binance Futures does not support second-level kline intervals")
3044 }
3045 BarAggregation::Minute => format!("{step}m"),
3046 BarAggregation::Hour => format!("{step}h"),
3047 BarAggregation::Day => format!("{step}d"),
3048 BarAggregation::Week => format!("{step}w"),
3049 BarAggregation::Month => format!("{step}M"),
3050 a => anyhow::bail!("Binance Futures does not support {a:?} aggregation"),
3051 };
3052
3053 let instrument_id = bar_type.instrument_id();
3054 let (symbol, price_precision, size_precision) =
3055 self.cached_precisions_by_id(instrument_id)?;
3056
3057 let params = BinanceKlinesParams {
3058 symbol,
3059 interval,
3060 start_time: start.map(|dt| dt.as_millisecond()),
3061 end_time: end.map(|dt| dt.as_millisecond()),
3062 limit,
3063 };
3064
3065 let klines = self.inner.klines(¶ms).await?;
3066 let now = self.clock.get_time_ns();
3067
3068 let mut result = Vec::with_capacity(klines.len());
3069 for kline in klines {
3070 let ts_init = parse_millis(kline.close_time, "Futures kline close time")?;
3071 let bar = parse_futures_kline_binance_bar(
3072 &kline,
3073 bar_type,
3074 price_precision,
3075 size_precision,
3076 ts_init,
3077 )?;
3078
3079 if bar.ts_event < now {
3080 result.push(bar);
3081 }
3082 }
3083
3084 Ok(result)
3085 }
3086
3087 pub async fn request_bars(
3093 &self,
3094 bar_type: BarType,
3095 start: Option<Timestamp>,
3096 end: Option<Timestamp>,
3097 limit: Option<u32>,
3098 ) -> anyhow::Result<Vec<Bar>> {
3099 Ok(self
3100 .request_binance_bars(bar_type, start, end, limit)
3101 .await?
3102 .into_iter()
3103 .map(|bar| bar.bar())
3104 .collect())
3105 }
3106
3107 pub async fn request_book_snapshot(
3113 &self,
3114 instrument_id: InstrumentId,
3115 depth: Option<u32>,
3116 ) -> anyhow::Result<OrderBook> {
3117 if depth.is_some_and(|value| !crate::common::consts::BINANCE_BOOK_DEPTHS.contains(&value)) {
3118 anyhow::bail!(
3119 "invalid Binance Futures order-book depth; valid values are {:?}",
3120 crate::common::consts::BINANCE_BOOK_DEPTHS
3121 );
3122 }
3123 let (symbol, price_precision, size_precision) =
3124 self.cached_precisions_by_id(instrument_id)?;
3125 let params = BinanceDepthParams {
3126 symbol,
3127 limit: depth,
3128 };
3129 let snapshot = self.inner.depth(¶ms).await?;
3130 let ts_event = self.clock.get_time_ns();
3131 let sequence = u64::try_from(snapshot.last_update_id)
3132 .map_err(|_| anyhow::anyhow!("invalid negative order-book update ID"))?;
3133 let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
3134 for (index, level) in snapshot.bids.iter().enumerate() {
3135 let order = BookOrder::new(
3136 OrderSide::Buy,
3137 parse_required_price_at_precision(&level.0, price_precision, "bid price")?,
3138 parse_required_quantity_at_precision(&level.1, size_precision, "bid quantity")?,
3139 index as u64,
3140 );
3141 book.add(order, 0, sequence, ts_event);
3142 }
3143 let bid_count = snapshot.bids.len();
3144 for (index, level) in snapshot.asks.iter().enumerate() {
3145 let order = BookOrder::new(
3146 OrderSide::Sell,
3147 parse_required_price_at_precision(&level.0, price_precision, "ask price")?,
3148 parse_required_quantity_at_precision(&level.1, size_precision, "ask quantity")?,
3149 (bid_count + index) as u64,
3150 );
3151 book.add(order, 0, sequence, ts_event);
3152 }
3153 Ok(book)
3154 }
3155
3156 fn cached_precisions_by_id(
3157 &self,
3158 instrument_id: InstrumentId,
3159 ) -> anyhow::Result<(String, u8, u8)> {
3160 let symbol = format_binance_symbol(&instrument_id);
3161 let instrument = self.instrument_metadata(instrument_id)?;
3162 let (price_precision, size_precision) = instrument.precisions()?;
3163
3164 Ok((symbol, price_precision, size_precision))
3165 }
3166
3167 pub(crate) fn instrument_metadata(
3168 &self,
3169 instrument_id: InstrumentId,
3170 ) -> anyhow::Result<BinanceFuturesInstrument> {
3171 let symbol = format_binance_symbol(&instrument_id);
3172 let instrument = self
3173 .instruments
3174 .get(&Ustr::from(symbol.as_str()))
3175 .map(|instrument| instrument.value().clone())
3176 .ok_or_else(|| InstrumentLookupError::not_found(instrument_id))?;
3177 if instrument.id() != instrument_id {
3178 return Err(InstrumentLookupError::not_found(instrument_id).into());
3179 }
3180
3181 Ok(instrument)
3182 }
3183
3184 pub async fn request_funding_rates(
3190 &self,
3191 instrument_id: InstrumentId,
3192 start: Option<Timestamp>,
3193 end: Option<Timestamp>,
3194 limit: Option<u32>,
3195 ) -> anyhow::Result<Vec<FundingRateUpdate>> {
3196 let params = BinanceFundingRateParams {
3197 symbol: Some(format_binance_symbol(&instrument_id)),
3198 start_time: start.map(|dt| dt.as_millisecond()),
3199 end_time: end.map(|dt| dt.as_millisecond()),
3200 limit,
3201 };
3202
3203 let rates = self.inner.funding_rate(¶ms).await?;
3204 let ts_init = UnixNanos::default();
3205
3206 let mut result = Vec::with_capacity(rates.len());
3207 for rate in rates {
3208 result.push(parse_futures_funding_rate_update(
3209 &rate,
3210 instrument_id,
3211 ts_init,
3212 )?);
3213 }
3214
3215 Ok(result)
3216 }
3217}
3218
3219fn parse_futures_trade_tick(
3220 trade: &BinanceFuturesTrade,
3221 instrument_id: InstrumentId,
3222 price_precision: u8,
3223 size_precision: u8,
3224 ts_init: UnixNanos,
3225) -> anyhow::Result<TradeTick> {
3226 let price = parse_required_price_at_precision(&trade.price, price_precision, "trade.price")
3227 .map_err(|e| anyhow::anyhow!("invalid Futures trade id {}: {e}", trade.id))?;
3228 let size = parse_required_quantity_at_precision(&trade.qty, size_precision, "trade.qty")
3229 .map_err(|e| anyhow::anyhow!("invalid Futures trade id {}: {e}", trade.id))?;
3230 let ts_event = parse_millis(trade.time, "Futures trade time")?;
3231
3232 let aggressor_side = if trade.is_buyer_maker {
3233 AggressorSide::Sell
3234 } else {
3235 AggressorSide::Buy
3236 };
3237
3238 Ok(TradeTick::new(
3239 instrument_id,
3240 price,
3241 size,
3242 aggressor_side,
3243 TradeId::new(trade.id.to_string()),
3244 ts_event,
3245 ts_init,
3246 ))
3247}
3248
3249fn parse_futures_agg_trade_tick(
3250 trade: &BinanceFuturesAggTrade,
3251 instrument_id: InstrumentId,
3252 price_precision: u8,
3253 size_precision: u8,
3254 ts_init: UnixNanos,
3255) -> anyhow::Result<TradeTick> {
3256 let trade = BinanceFuturesTrade {
3257 id: trade.id,
3258 price: trade.price.clone(),
3259 qty: trade.qty.clone(),
3260 quote_qty: String::new(),
3261 time: trade.time,
3262 is_buyer_maker: trade.is_buyer_maker,
3263 };
3264 parse_futures_trade_tick(
3265 &trade,
3266 instrument_id,
3267 price_precision,
3268 size_precision,
3269 ts_init,
3270 )
3271}
3272
3273fn parse_futures_kline_binance_bar(
3274 kline: &BinanceFuturesKline,
3275 bar_type: BarType,
3276 price_precision: u8,
3277 size_precision: u8,
3278 ts_init: UnixNanos,
3279) -> anyhow::Result<BinanceBar> {
3280 let open = parse_required_price_at_precision(&kline.open, price_precision, "kline.open")
3281 .map_err(|e| anyhow::anyhow!("invalid Futures kline {}: {e}", kline.open_time))?;
3282 let high = parse_required_price_at_precision(&kline.high, price_precision, "kline.high")
3283 .map_err(|e| anyhow::anyhow!("invalid Futures kline {}: {e}", kline.open_time))?;
3284 let low = parse_required_price_at_precision(&kline.low, price_precision, "kline.low")
3285 .map_err(|e| anyhow::anyhow!("invalid Futures kline {}: {e}", kline.open_time))?;
3286 let close = parse_required_price_at_precision(&kline.close, price_precision, "kline.close")
3287 .map_err(|e| anyhow::anyhow!("invalid Futures kline {}: {e}", kline.open_time))?;
3288 let volume =
3289 parse_required_quantity_at_precision(&kline.volume, size_precision, "kline.volume")
3290 .map_err(|e| anyhow::anyhow!("invalid Futures kline {}: {e}", kline.open_time))?;
3291 let ts_event = parse_millis(kline.close_time, "Futures kline close time")?;
3292
3293 let quote_volume = kline.quote_volume.parse::<Decimal>().map_err(|e| {
3294 anyhow::anyhow!(
3295 "invalid Futures kline {} quote volume: {e}",
3296 kline.open_time
3297 )
3298 })?;
3299 let taker_buy_base_volume = kline
3300 .taker_buy_base_volume
3301 .parse::<Decimal>()
3302 .map_err(|e| {
3303 anyhow::anyhow!(
3304 "invalid Futures kline {} taker buy base volume: {e}",
3305 kline.open_time
3306 )
3307 })?;
3308 let taker_buy_quote_volume = kline
3309 .taker_buy_quote_volume
3310 .parse::<Decimal>()
3311 .map_err(|e| {
3312 anyhow::anyhow!(
3313 "invalid Futures kline {} taker buy quote volume: {e}",
3314 kline.open_time
3315 )
3316 })?;
3317 let count = u64::try_from(kline.num_trades).map_err(|_| {
3318 anyhow::anyhow!(
3319 "invalid Futures kline {} negative trade count",
3320 kline.open_time
3321 )
3322 })?;
3323
3324 Ok(BinanceBar::new(
3325 bar_type,
3326 open,
3327 high,
3328 low,
3329 close,
3330 volume,
3331 quote_volume,
3332 count,
3333 taker_buy_base_volume,
3334 taker_buy_quote_volume,
3335 ts_event,
3336 ts_init,
3337 ))
3338}
3339
3340fn parse_futures_funding_rate_update(
3341 rate: &BinanceFundingRate,
3342 instrument_id: InstrumentId,
3343 ts_init: UnixNanos,
3344) -> anyhow::Result<FundingRateUpdate> {
3345 let funding_rate = rate.funding_rate.parse::<Decimal>().map_err(|e| {
3346 anyhow::anyhow!("invalid Futures funding rate at {}: {e}", rate.funding_time)
3347 })?;
3348 let ts_event = parse_millis(rate.funding_time, "Futures funding time")?;
3349
3350 Ok(FundingRateUpdate::new(
3351 instrument_id,
3352 funding_rate,
3353 None, None, ts_event,
3356 ts_init,
3357 ))
3358}
3359
3360fn parse_futures_commission_rates(
3361 response: &BinanceFuturesCommissionRate,
3362) -> anyhow::Result<(Decimal, Decimal)> {
3363 Ok((
3364 response.maker_commission_rate.parse()?,
3365 response.taker_commission_rate.parse()?,
3366 ))
3367}
3368
3369fn validate_reconciliation_instrument(
3370 instruments: &mut AHashMap<InstrumentId, InstrumentAny>,
3371 expected_id: InstrumentId,
3372 instrument: &InstrumentAny,
3373) -> BinanceFuturesHttpResult<()> {
3374 let parsed_id = instrument.id();
3375 if parsed_id != expected_id {
3376 return Err(BinanceFuturesHttpError::ValidationError(format!(
3377 "Parsed Binance Futures instrument ID {parsed_id} does not match expected ID {expected_id}"
3378 )));
3379 }
3380
3381 if instruments.insert(parsed_id, instrument.clone()).is_some() {
3382 return Err(BinanceFuturesHttpError::ValidationError(format!(
3383 "Duplicate parsed Binance Futures instrument ID {parsed_id}"
3384 )));
3385 }
3386
3387 Ok(())
3388}
3389
3390fn log_futures_instrument_parse_error(
3391 config: &BinanceInstrumentProviderConfig,
3392 symbol: &str,
3393 error: &anyhow::Error,
3394) {
3395 if config.log_warnings {
3396 log::warn!("Skipping Binance Futures instrument {symbol}: {error}");
3397 } else {
3398 log::debug!("Skipping Binance Futures instrument {symbol}: {error}");
3399 }
3400}
3401
3402fn log_futures_commission_fallback(
3403 config: &BinanceInstrumentProviderConfig,
3404 symbol: &str,
3405 error: &dyn std::fmt::Display,
3406 fallback: (Decimal, Decimal),
3407) {
3408 if config.log_warnings {
3409 log::warn!(
3410 "Unable to query Binance Futures commission for {symbol}; using maker={} taker={}: {error}",
3411 fallback.0,
3412 fallback.1,
3413 );
3414 } else {
3415 log::debug!(
3416 "Unable to query Binance Futures commission for {symbol}; using maker={} taker={}: {error}",
3417 fallback.0,
3418 fallback.1,
3419 );
3420 }
3421}
3422
3423#[must_use]
3428pub fn is_algo_order_type(order_type: OrderType) -> bool {
3429 matches!(
3430 order_type,
3431 OrderType::StopMarket
3432 | OrderType::StopLimit
3433 | OrderType::MarketIfTouched
3434 | OrderType::LimitIfTouched
3435 | OrderType::TrailingStopMarket
3436 )
3437}
3438
3439pub(crate) fn order_type_to_binance_futures(
3441 order_type: OrderType,
3442) -> anyhow::Result<BinanceFuturesOrderType> {
3443 match order_type {
3444 OrderType::Market => Ok(BinanceFuturesOrderType::Market),
3445 OrderType::Limit => Ok(BinanceFuturesOrderType::Limit),
3446 OrderType::StopMarket => Ok(BinanceFuturesOrderType::StopMarket),
3447 OrderType::StopLimit => Ok(BinanceFuturesOrderType::Stop),
3448 OrderType::MarketIfTouched => Ok(BinanceFuturesOrderType::TakeProfitMarket),
3449 OrderType::LimitIfTouched => Ok(BinanceFuturesOrderType::TakeProfit),
3450 OrderType::TrailingStopMarket => Ok(BinanceFuturesOrderType::TrailingStopMarket),
3451 _ => anyhow::bail!("Unsupported order type for Binance Futures: {order_type:?}"),
3452 }
3453}
3454
3455#[cfg(test)]
3456mod tests {
3457 use nautilus_core::time::get_atomic_clock_realtime;
3458 use nautilus_network::http::{HttpStatus, StatusCode};
3459 use rstest::rstest;
3460 use tokio_util::bytes::Bytes;
3461
3462 use super::*;
3463 use crate::common::enums::BinanceTradingStatus;
3464
3465 #[rstest]
3466 fn test_rate_limit_config_usdm_has_request_weight_and_orders() {
3467 let config = BinanceRawFuturesHttpClient::rate_limit_config(BinanceProductType::UsdM);
3468
3469 assert_eq!(config.request_quota.burst_size().get(), 2_400);
3470 assert_eq!(config.order_keys.len(), 2);
3471 assert!(
3472 config
3473 .order_keys
3474 .iter()
3475 .any(|key| key == "binance:orders:10:Second")
3476 );
3477 assert!(
3478 config
3479 .order_keys
3480 .iter()
3481 .any(|key| key == "binance:orders:1:Minute")
3482 );
3483
3484 let ten_second_quota = config
3485 .order_quotas
3486 .iter()
3487 .find(|(key, _)| key == "binance:orders:10:Second")
3488 .map(|(_, quota)| quota)
3489 .expect("USD-M 10-second order quota");
3490 assert_eq!(ten_second_quota.burst_size().get(), 300);
3491 assert_eq!(
3492 ten_second_quota.replenish_interval(),
3493 Duration::from_nanos(33_333_333)
3494 );
3495 }
3496
3497 #[rstest]
3498 fn test_rate_limit_config_coinm_has_request_weight_and_orders() {
3499 let config = BinanceRawFuturesHttpClient::rate_limit_config(BinanceProductType::CoinM);
3500
3501 assert_eq!(config.request_quota.burst_size().get(), 2_400);
3502 assert_eq!(config.order_keys.len(), 2);
3503 assert!(
3504 config
3505 .order_keys
3506 .iter()
3507 .any(|key| key == "binance:orders:10:Second")
3508 );
3509 assert!(
3510 config
3511 .order_keys
3512 .iter()
3513 .any(|key| key == "binance:orders:1:Minute")
3514 );
3515
3516 let ten_second_quota = config
3517 .order_quotas
3518 .iter()
3519 .find(|(key, _)| key == "binance:orders:10:Second")
3520 .map(|(_, quota)| quota)
3521 .expect("COIN-M 10-second order quota");
3522 let minute_quota = config
3523 .order_quotas
3524 .iter()
3525 .find(|(key, _)| key == "binance:orders:1:Minute")
3526 .map(|(_, quota)| quota)
3527 .expect("COIN-M one-minute order quota");
3528
3529 assert_eq!(ten_second_quota.burst_size().get(), 300);
3530 assert_eq!(minute_quota.burst_size().get(), 1_200);
3531 }
3532
3533 #[rstest]
3534 fn test_rate_limiters_share_usdm_and_coinm_scopes() {
3535 let account = Some(BinanceFuturesAccountScope([1; 32]));
3536 let usdm_config = BinanceRawFuturesHttpClient::rate_limit_config(BinanceProductType::UsdM);
3537 let coinm_config =
3538 BinanceRawFuturesHttpClient::rate_limit_config(BinanceProductType::CoinM);
3539
3540 let usdm = BinanceRawFuturesHttpClient::shared_rate_limiters(
3541 BinanceEnvironment::Live,
3542 None,
3543 None,
3544 account,
3545 usdm_config.request_quota,
3546 usdm_config.order_quotas,
3547 );
3548 let coinm = BinanceRawFuturesHttpClient::shared_rate_limiters(
3549 BinanceEnvironment::Live,
3550 Some(get_http_base_url(
3551 BinanceProductType::CoinM,
3552 BinanceEnvironment::Live,
3553 )),
3554 None,
3555 account,
3556 coinm_config.request_quota,
3557 coinm_config.order_quotas,
3558 );
3559 let public = create_test_rate_limiters(BinanceEnvironment::Live, None, None, None);
3560
3561 assert_eq!(usdm.len(), 2);
3562 assert_eq!(coinm.len(), 2);
3563 assert_eq!(public.len(), 1);
3564 assert!(Arc::ptr_eq(&usdm[0], &coinm[0]));
3565 assert!(Arc::ptr_eq(&usdm[0], &public[0]));
3566 assert!(Arc::ptr_eq(&usdm[1], &coinm[1]));
3567 }
3568
3569 #[rstest]
3570 fn test_rate_limiters_isolate_unrelated_scopes() {
3571 let account_a = Some(BinanceFuturesAccountScope([2; 32]));
3572 let account_b = Some(BinanceFuturesAccountScope([3; 32]));
3573
3574 let live_direct =
3575 create_test_rate_limiters(BinanceEnvironment::Live, None, None, account_a);
3576 let testnet_direct =
3577 create_test_rate_limiters(BinanceEnvironment::Testnet, None, None, account_a);
3578 let demo_direct =
3579 create_test_rate_limiters(BinanceEnvironment::Demo, None, None, account_a);
3580 let custom_a = create_test_rate_limiters(
3581 BinanceEnvironment::Live,
3582 Some("http://127.0.0.1:41001"),
3583 None,
3584 account_a,
3585 );
3586 let custom_b = create_test_rate_limiters(
3587 BinanceEnvironment::Live,
3588 Some("http://127.0.0.1:41002"),
3589 None,
3590 account_a,
3591 );
3592 let proxy_a = create_test_rate_limiters(
3593 BinanceEnvironment::Live,
3594 None,
3595 Some("http://127.0.0.1:42001"),
3596 account_a,
3597 );
3598 let proxy_b = create_test_rate_limiters(
3599 BinanceEnvironment::Live,
3600 None,
3601 Some("http://127.0.0.1:42002"),
3602 account_a,
3603 );
3604 let other_account =
3605 create_test_rate_limiters(BinanceEnvironment::Live, None, None, account_b);
3606
3607 assert!(!Arc::ptr_eq(&live_direct[0], &testnet_direct[0]));
3608 assert!(!Arc::ptr_eq(&live_direct[0], &demo_direct[0]));
3609 assert!(!Arc::ptr_eq(&live_direct[0], &custom_a[0]));
3610 assert!(!Arc::ptr_eq(&custom_a[0], &custom_b[0]));
3611 assert!(!Arc::ptr_eq(&proxy_a[0], &proxy_b[0]));
3612 assert!(Arc::ptr_eq(&proxy_a[1], &proxy_b[1]));
3613 assert!(Arc::ptr_eq(&live_direct[0], &other_account[0]));
3614 assert!(!Arc::ptr_eq(&live_direct[1], &other_account[1]));
3615 }
3616
3617 fn create_test_rate_limiters(
3618 environment: BinanceEnvironment,
3619 base_url_override: Option<&str>,
3620 proxy_url: Option<&str>,
3621 account_scope: Option<BinanceFuturesAccountScope>,
3622 ) -> Vec<BinanceFuturesRateLimiter> {
3623 let config = BinanceRawFuturesHttpClient::rate_limit_config(BinanceProductType::UsdM);
3624 BinanceRawFuturesHttpClient::shared_rate_limiters(
3625 environment,
3626 base_url_override,
3627 proxy_url,
3628 account_scope,
3629 config.request_quota,
3630 config.order_quotas,
3631 )
3632 }
3633
3634 #[rstest]
3635 fn test_rate_limit_keys_usdm_include_order_buckets() {
3636 let client = BinanceRawFuturesHttpClient::new(
3637 BinanceProductType::UsdM,
3638 BinanceEnvironment::Live,
3639 None,
3640 None,
3641 None,
3642 None,
3643 None,
3644 None,
3645 )
3646 .unwrap();
3647
3648 assert_eq!(
3649 client.rate_limit_keys(false),
3650 vec![BINANCE_GLOBAL_RATE_KEY.to_string()]
3651 );
3652 assert_eq!(
3653 client.rate_limit_keys(true),
3654 vec![
3655 BINANCE_GLOBAL_RATE_KEY.to_string(),
3656 "binance:orders:10:Second".to_string(),
3657 "binance:orders:1:Minute".to_string(),
3658 ]
3659 );
3660 }
3661
3662 #[rstest]
3663 fn test_quota_from_unknown_interval_returns_none() {
3664 let quota = BinanceRateLimitQuota {
3665 rate_limit_type: BinanceRateLimitType::Orders,
3666 interval: BinanceRateLimitInterval::Unknown,
3667 interval_num: 1,
3668 limit: 10,
3669 };
3670
3671 assert!(BinanceRawFuturesHttpClient::quota_from("a).is_none());
3672 }
3673
3674 #[rstest]
3675 fn test_create_client_rejects_spot_product_type() {
3676 let result = BinanceFuturesHttpClient::new(
3677 BinanceProductType::Spot,
3678 BinanceEnvironment::Live,
3679 get_atomic_clock_realtime(),
3680 None,
3681 None,
3682 None,
3683 None,
3684 None,
3685 None,
3686 false,
3687 );
3688
3689 result.unwrap_err();
3690 }
3691
3692 #[rstest]
3693 fn test_parse_futures_trade_tick_rejects_invalid_price() {
3694 let trade = BinanceFuturesTrade {
3695 id: 100,
3696 price: "not-a-number".to_string(),
3697 qty: "0.001".to_string(),
3698 quote_qty: "50.00".to_string(),
3699 time: 1_625_474_304_000,
3700 is_buyer_maker: false,
3701 };
3702
3703 let result = parse_futures_trade_tick(
3704 &trade,
3705 InstrumentId::from("BTCUSDT-PERP.BINANCE"),
3706 2,
3707 3,
3708 UnixNanos::from(1_000_000_000u64),
3709 );
3710
3711 let error = result.unwrap_err().to_string();
3712 assert!(error.contains("trade.price"));
3713 assert!(error.contains("100"));
3714 }
3715
3716 #[rstest]
3717 fn test_parse_futures_kline_bar_rejects_invalid_volume() {
3718 let kline = BinanceFuturesKline {
3719 open_time: 1_625_474_304_000,
3720 open: "50000.00".to_string(),
3721 high: "51000.00".to_string(),
3722 low: "49000.00".to_string(),
3723 close: "50500.00".to_string(),
3724 volume: "not-a-number".to_string(),
3725 close_time: 1_625_474_364_000,
3726 quote_volume: "631250.00".to_string(),
3727 num_trades: 100,
3728 taker_buy_base_volume: "6.2".to_string(),
3729 taker_buy_quote_volume: "313100.00".to_string(),
3730 };
3731
3732 let result = parse_futures_kline_binance_bar(
3733 &kline,
3734 BarType::from("BTCUSDT-PERP.BINANCE-1-MINUTE-LAST-EXTERNAL"),
3735 2,
3736 3,
3737 UnixNanos::from(1_000_000_000u64),
3738 );
3739
3740 let error = result.unwrap_err().to_string();
3741 assert!(error.contains("kline.volume"));
3742 assert!(error.contains("1625474304000"));
3743 }
3744
3745 #[rstest]
3746 #[case::limit(
3747 BinanceAggTradesParams {
3748 symbol: "BTCUSDT".to_string(),
3749 from_id: None,
3750 start_time: None,
3751 end_time: None,
3752 limit: Some(1001),
3753 },
3754 "Validation error: aggregate trade limit must not exceed 1000"
3755 )]
3756 #[case::order(
3757 BinanceAggTradesParams {
3758 symbol: "BTCUSDT".to_string(),
3759 from_id: None,
3760 start_time: Some(2000),
3761 end_time: Some(1000),
3762 limit: Some(1000),
3763 },
3764 "Validation error: aggregate trade startTime must not exceed endTime"
3765 )]
3766 #[case::range(
3767 BinanceAggTradesParams {
3768 symbol: "BTCUSDT".to_string(),
3769 from_id: None,
3770 start_time: Some(1000),
3771 end_time: Some(3_601_000),
3772 limit: Some(1000),
3773 },
3774 "Validation error: aggregate trade time range must be less than one hour"
3775 )]
3776 #[case::overflow(
3777 BinanceAggTradesParams {
3778 symbol: "BTCUSDT".to_string(),
3779 from_id: None,
3780 start_time: Some(i64::MIN),
3781 end_time: Some(i64::MAX),
3782 limit: Some(1000),
3783 },
3784 "Validation error: aggregate trade time range must be less than one hour"
3785 )]
3786 #[tokio::test]
3787 async fn test_agg_trades_rejects_invalid_bounds(
3788 #[case] params: BinanceAggTradesParams,
3789 #[case] expected: &str,
3790 ) {
3791 let error = create_test_raw_client()
3792 .agg_trades(¶ms)
3793 .await
3794 .unwrap_err();
3795
3796 assert_eq!(error.to_string(), expected);
3797 }
3798
3799 #[rstest]
3800 #[case::start(true, false)]
3801 #[case::end(false, true)]
3802 #[case::both(true, true)]
3803 #[tokio::test]
3804 async fn test_request_agg_trades_rejects_history_older_than_24_hours(
3805 #[case] include_start: bool,
3806 #[case] include_end: bool,
3807 ) {
3808 let client = create_test_client();
3809 let start = Timestamp::now() - jiff::SignedDuration::from_hours(25);
3810 let end = start + jiff::SignedDuration::from_mins(30);
3811
3812 let error = client
3813 .request_agg_trades(
3814 InstrumentId::from("BTCUSDT-PERP.BINANCE"),
3815 include_start.then_some(start),
3816 include_end.then_some(end),
3817 Some(1000),
3818 )
3819 .await
3820 .unwrap_err();
3821
3822 assert_eq!(
3823 error.to_string(),
3824 "Binance Futures aggregate trade history is limited to the past 24 hours"
3825 );
3826 }
3827
3828 fn create_test_raw_client() -> BinanceRawFuturesHttpClient {
3829 BinanceRawFuturesHttpClient::new(
3830 BinanceProductType::UsdM,
3831 BinanceEnvironment::Live,
3832 None,
3833 None,
3834 None,
3835 None,
3836 None,
3837 None,
3838 )
3839 .expect("Failed to create test client")
3840 }
3841
3842 fn create_test_client() -> BinanceFuturesHttpClient {
3843 BinanceFuturesHttpClient::new(
3844 BinanceProductType::UsdM,
3845 BinanceEnvironment::Live,
3846 get_atomic_clock_realtime(),
3847 None,
3848 None,
3849 Some("http://127.0.0.1:1".to_string()),
3850 None,
3851 Some(1),
3852 None,
3853 false,
3854 )
3855 .expect("Failed to create test client")
3856 }
3857
3858 fn test_usdm_symbol() -> BinanceFuturesUsdSymbol {
3859 BinanceFuturesUsdSymbol {
3860 symbol: Ustr::from("BTCUSDT"),
3861 pair: Ustr::from("BTCUSDT"),
3862 contract_type: "PERPETUAL".to_string(),
3863 delivery_date: 4_133_404_800_000,
3864 onboard_date: 1_569_398_400_000,
3865 status: BinanceTradingStatus::Trading,
3866 maint_margin_percent: "2.5000".to_string(),
3867 required_margin_percent: "5.0000".to_string(),
3868 base_asset: Ustr::from("BTC"),
3869 quote_asset: Ustr::from("USDT"),
3870 margin_asset: Ustr::from("USDT"),
3871 price_precision: 2,
3872 quantity_precision: 3,
3873 base_asset_precision: 8,
3874 quote_precision: 8,
3875 underlying_type: None,
3876 underlying_sub_type: Vec::new(),
3877 settle_plan: None,
3878 trigger_protect: None,
3879 liquidation_fee: None,
3880 market_take_bound: None,
3881 order_types: Vec::new(),
3882 time_in_force: Vec::new(),
3883 filters: Vec::new(),
3884 }
3885 }
3886
3887 #[rstest]
3888 fn test_cached_precisions_by_id_returns_symbol_and_precisions() {
3889 let client = create_test_client();
3890 client.instruments_cache().insert(
3891 Ustr::from("BTCUSDT"),
3892 BinanceFuturesInstrument::UsdM(test_usdm_symbol()),
3893 );
3894
3895 let (symbol, price_precision, size_precision) = client
3896 .cached_precisions_by_id(InstrumentId::from("BTCUSDT-PERP.BINANCE"))
3897 .unwrap();
3898
3899 assert_eq!(symbol, "BTCUSDT");
3900 assert_eq!(price_precision, 2);
3901 assert_eq!(size_precision, 3);
3902 }
3903
3904 #[rstest]
3905 fn test_cached_precisions_by_id_rejects_spot_alias() {
3906 let client = create_test_client();
3907 client.instruments_cache().insert(
3908 Ustr::from("BTCUSDT"),
3909 BinanceFuturesInstrument::UsdM(test_usdm_symbol()),
3910 );
3911
3912 let error = client
3913 .cached_precisions_by_id(InstrumentId::from("BTCUSDT.BINANCE"))
3914 .unwrap_err();
3915
3916 assert_eq!(
3917 error.to_string(),
3918 InstrumentLookupError::not_found(InstrumentId::from("BTCUSDT.BINANCE")).to_string()
3919 );
3920 }
3921
3922 #[rstest]
3923 fn test_invalid_precision_preserves_previous_raw_snapshot() {
3924 let client = create_test_client();
3925 let valid = test_usdm_symbol();
3926 client
3927 .replace_instruments(vec![(
3928 valid.symbol,
3929 BinanceFuturesInstrument::UsdM(valid.clone()),
3930 )])
3931 .unwrap();
3932 let mut invalid = valid;
3933 invalid.price_precision = i32::from(FIXED_PRECISION) + 1;
3934
3935 let error = client
3936 .replace_instruments(vec![(
3937 invalid.symbol,
3938 BinanceFuturesInstrument::UsdM(invalid),
3939 )])
3940 .unwrap_err();
3941 let retained = client
3942 .instruments_cache()
3943 .get(&Ustr::from("BTCUSDT"))
3944 .map(|instrument| instrument.value().clone())
3945 .unwrap();
3946
3947 assert!(error.to_string().contains("precision exceeds maximum"));
3948 assert_eq!(retained.precisions().unwrap(), (2, 3));
3949 }
3950
3951 #[rstest]
3952 #[tokio::test]
3953 async fn test_submit_algo_order_stop_market_requires_trigger_price() {
3954 let client = create_test_client();
3955 client.instruments_cache().insert(
3956 Ustr::from("BTCUSDT"),
3957 BinanceFuturesInstrument::UsdM(test_usdm_symbol()),
3958 );
3959
3960 let result = client
3961 .submit_algo_order(
3962 AccountId::from("BINANCE-001"),
3963 InstrumentId::from("BTCUSDT-PERP.BINANCE"),
3964 ClientOrderId::new("missing-trigger-test-001"),
3965 OrderSide::Sell,
3966 OrderType::StopMarket,
3967 Quantity::from("0.001"),
3968 TimeInForce::Gtc,
3969 None,
3970 None,
3971 false,
3972 false,
3973 None,
3974 None,
3975 None,
3976 None,
3977 None,
3978 )
3979 .await;
3980
3981 let error = result.unwrap_err().to_string();
3982 assert_eq!(error, "Algo order type StopMarket requires a trigger price");
3983 }
3984
3985 #[rstest]
3986 fn test_batch_cancel_params_builds_order_id_list() {
3987 let items = vec![
3988 BatchCancelItem::by_order_id("BTCUSDT", 123),
3989 BatchCancelItem::by_order_id("BTCUSDT", 456),
3990 ];
3991
3992 let params = BinanceRawFuturesHttpClient::batch_cancel_params(&items).unwrap();
3993
3994 assert_eq!(params.symbol, "BTCUSDT");
3995 assert_eq!(params.order_id_list.as_deref(), Some("[123,456]"));
3996 assert_eq!(params.orig_client_order_id_list, None);
3997 }
3998
3999 #[rstest]
4000 fn test_batch_cancel_params_builds_client_order_id_list() {
4001 let items = vec![
4002 BatchCancelItem::by_client_order_id("BTCUSDT", "first-order"),
4003 BatchCancelItem::by_client_order_id("BTCUSDT", "second-order"),
4004 ];
4005
4006 let params = BinanceRawFuturesHttpClient::batch_cancel_params(&items).unwrap();
4007
4008 assert_eq!(params.symbol, "BTCUSDT");
4009 assert_eq!(params.order_id_list, None);
4010 assert_eq!(
4011 params.orig_client_order_id_list.as_deref(),
4012 Some("[\"first-order\",\"second-order\"]"),
4013 );
4014 }
4015
4016 #[rstest]
4017 fn test_batch_cancel_params_rejects_mixed_symbols() {
4018 let items = vec![
4019 BatchCancelItem::by_order_id("BTCUSDT", 123),
4020 BatchCancelItem::by_order_id("ETHUSDT", 456),
4021 ];
4022
4023 let result = BinanceRawFuturesHttpClient::batch_cancel_params(&items);
4024
4025 assert_validation_error(result, "same symbol");
4026 }
4027
4028 #[rstest]
4029 fn test_batch_cancel_params_rejects_mixed_id_types() {
4030 let items = vec![
4031 BatchCancelItem::by_order_id("BTCUSDT", 123),
4032 BatchCancelItem::by_client_order_id("BTCUSDT", "client-order"),
4033 ];
4034
4035 let result = BinanceRawFuturesHttpClient::batch_cancel_params(&items);
4036
4037 assert_validation_error(result, "not both");
4038 }
4039
4040 #[rstest]
4041 fn test_batch_cancel_params_rejects_items_without_ids() {
4042 let items = vec![BatchCancelItem {
4043 symbol: "BTCUSDT".to_string(),
4044 order_id: None,
4045 orig_client_order_id: None,
4046 }];
4047
4048 let result = BinanceRawFuturesHttpClient::batch_cancel_params(&items);
4049
4050 assert_validation_error(result, "at least one order ID or client order ID");
4051 }
4052
4053 #[rstest]
4054 #[tokio::test]
4055 async fn test_batch_cancel_orders_rejects_more_than_ten_items() {
4056 let client = create_test_raw_client();
4057 let items = (0..11)
4058 .map(|order_id| BatchCancelItem::by_order_id("BTCUSDT", order_id))
4059 .collect::<Vec<_>>();
4060
4061 let result = client.batch_cancel_orders(&items).await;
4062
4063 match result {
4064 Err(BinanceFuturesHttpError::ValidationError(message)) => {
4065 assert!(message.contains("10 orders maximum"));
4066 }
4067 other => panic!("Expected ValidationError, was {other:?}"),
4068 }
4069 }
4070
4071 fn assert_validation_error(
4072 result: BinanceFuturesHttpResult<BatchCancelParams>,
4073 expected_message: &str,
4074 ) {
4075 match result {
4076 Err(BinanceFuturesHttpError::ValidationError(message)) => {
4077 assert!(message.contains(expected_message));
4078 }
4079 other => panic!("Expected ValidationError, was {other:?}"),
4080 }
4081 }
4082
4083 #[rstest]
4084 fn test_parse_error_response_binance_error() {
4085 let client = create_test_raw_client();
4086 let response = HttpResponse {
4087 status: HttpStatus::new(StatusCode::BAD_REQUEST),
4088 headers: HashMap::new(),
4089 body: Bytes::from(r#"{"code":-1121,"msg":"Invalid symbol."}"#),
4090 };
4091
4092 let result: BinanceFuturesHttpResult<()> = client.parse_error_response(&response);
4093
4094 match result {
4095 Err(BinanceFuturesHttpError::BinanceError { code, message }) => {
4096 assert_eq!(code, -1121);
4097 assert_eq!(message, "Invalid symbol.");
4098 }
4099 other => panic!("Expected BinanceError, was {other:?}"),
4100 }
4101 }
4102}