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::{AtomicMap, datetime::SECONDS_IN_DAY, nanos::UnixNanos, time::AtomicTime};
31use nautilus_model::{
32 data::{Bar, BarType, BookOrder, FundingRateUpdate, TradeTick},
33 enums::{
34 AggregationSource, AggressorSide, BarAggregation, BookType, MarketStatusAction, OrderSide,
35 OrderType, TimeInForce,
36 },
37 events::AccountState,
38 identifiers::{AccountId, ClientOrderId, InstrumentId, TradeId, VenueOrderId},
39 instruments::{Instrument, any::InstrumentAny},
40 orderbook::OrderBook,
41 reports::{FillReport, OrderStatusReport},
42 types::{Currency, Price, Quantity, fixed::FIXED_PRECISION},
43};
44use nautilus_network::{
45 http::{
46 HttpClient, HttpRedirectPolicy, HttpResponse, Method, create_standard_nautilus_headers,
47 },
48 ratelimiter::{RateLimiter, clock::MonotonicClock, quota::Quota},
49 retry::{RetryConfig, RetryError, RetryManager},
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,
88 BINANCE_RETRY_AFTER_HEADER, BinanceRateLimitQuota,
89 },
90 credential::SigningCredential,
91 encoder::encode_broker_id,
92 enums::{
93 BinanceAlgoType, BinanceEnvironment, BinanceFuturesOrderType, BinancePositionSide,
94 BinancePriceMatch, BinanceProductType, BinanceRateLimitInterval, BinanceRateLimitType,
95 BinanceSide, BinanceTimeInForce, BinanceWorkingType,
96 },
97 fees::futures_fee_tier_rates,
98 instruments::BinanceInstrumentSelector,
99 models::BinanceErrorResponse,
100 parse::{
101 parse_coinm_instrument_with_fees, parse_millis, parse_required_price_at_precision,
102 parse_required_quantity_at_precision, parse_usdm_instrument_with_fees,
103 should_warn_on_instrument_parse_error,
104 },
105 symbol::{format_binance_symbol, format_instrument_id},
106 urls::get_http_base_url,
107 },
108 config::BinanceInstrumentProviderConfig,
109 futures::conversions::reduce_only_param,
110};
111
112const BINANCE_GLOBAL_RATE_KEY: &str = "binance:global";
113const BINANCE_ORDERS_RATE_KEY: &str = "binance:orders";
114
115type BinanceFuturesLimiter = RateLimiter<Ustr, MonotonicClock>;
116type BinanceFuturesRateLimiter = Arc<BinanceFuturesLimiter>;
117type BinanceFuturesRateLimiterRegistry<S> = Mutex<AHashMap<S, Weak<BinanceFuturesLimiter>>>;
118
119#[derive(Clone, PartialEq, Eq, Hash)]
120enum BinanceFuturesEndpointScope {
121 Environment(BinanceEnvironment),
122 Custom {
123 environment: BinanceEnvironment,
124 base_url: String,
125 },
126}
127
128#[derive(Clone, PartialEq, Eq, Hash)]
129struct BinanceFuturesRequestScope {
130 endpoint: BinanceFuturesEndpointScope,
131 proxy_url_digest: Option<[u8; 32]>,
132}
133
134#[derive(Clone, Copy, PartialEq, Eq, Hash)]
135struct BinanceFuturesAccountScope([u8; 32]);
136
137#[derive(Clone, PartialEq, Eq, Hash)]
138struct BinanceFuturesOrderScope {
139 endpoint: BinanceFuturesEndpointScope,
140 account: BinanceFuturesAccountScope,
141}
142
143static BINANCE_FUTURES_REQUEST_LIMITERS: LazyLock<
144 BinanceFuturesRateLimiterRegistry<BinanceFuturesRequestScope>,
145> = LazyLock::new(|| Mutex::new(AHashMap::new()));
146
147static BINANCE_FUTURES_ORDER_LIMITERS: LazyLock<
148 BinanceFuturesRateLimiterRegistry<BinanceFuturesOrderScope>,
149> = LazyLock::new(|| Mutex::new(AHashMap::new()));
150
151#[derive(Debug)]
153pub struct BinanceFuturesAlgoOrderQueryResult {
154 pub algo: BinanceFuturesAlgoOrder,
156 pub actual: Option<BinanceFuturesOrder>,
158}
159
160#[derive(Debug, Serialize)]
161#[serde(rename_all = "camelCase")]
162struct BatchCancelParams {
163 symbol: String,
164 #[serde(skip_serializing_if = "Option::is_none")]
165 order_id_list: Option<String>,
166 #[serde(skip_serializing_if = "Option::is_none")]
167 orig_client_order_id_list: Option<String>,
168}
169
170#[derive(Debug, Clone)]
172pub struct BinanceRawFuturesHttpClient {
173 retry_manager: Arc<RetryManager<BinanceFuturesHttpError>>,
174 client: HttpClient,
175 base_url: String,
176 api_path: &'static str,
177 credential: Option<SigningCredential>,
178 recv_window: Option<u64>,
179 order_rate_keys: Vec<String>,
180}
181
182impl BinanceRawFuturesHttpClient {
183 #[must_use]
185 pub fn http_client(&self) -> &HttpClient {
186 &self.client
187 }
188
189 #[must_use]
191 pub const fn has_credentials(&self) -> bool {
192 self.credential.is_some()
193 }
194
195 #[expect(clippy::too_many_arguments)]
201 pub fn new(
202 product_type: BinanceProductType,
203 environment: BinanceEnvironment,
204 api_key: Option<String>,
205 api_secret: Option<String>,
206 base_url_override: Option<String>,
207 recv_window: Option<u64>,
208 timeout_secs: Option<u64>,
209 proxy_url: Option<String>,
210 ) -> BinanceFuturesHttpResult<Self> {
211 let RateLimitConfig {
212 request_quota,
213 order_quotas,
214 order_keys,
215 } = Self::rate_limit_config(product_type);
216
217 let credential = match (api_key, api_secret) {
218 (Some(key), Some(secret)) => Some(SigningCredential::new(key, secret)),
219 (None, None) => None,
220 _ => return Err(BinanceFuturesHttpError::MissingCredentials),
221 };
222
223 let account_scope = credential.as_ref().map(Self::account_scope);
224 let rate_limiters = Self::shared_rate_limiters(
225 environment,
226 base_url_override.as_deref(),
227 proxy_url.as_deref(),
228 account_scope,
229 request_quota,
230 order_quotas,
231 );
232 let base_url = base_url_override
233 .unwrap_or_else(|| get_http_base_url(product_type, environment).to_string());
234 let api_path = Self::resolve_api_path(product_type);
235 let headers = Self::default_headers(&credential);
236
237 let client = HttpClient::builder()
238 .redirect_policy(HttpRedirectPolicy::Reject)
239 .headers(headers)
240 .header_keys(vec![
241 BINANCE_API_KEY_HEADER.to_string(),
242 BINANCE_RETRY_AFTER_HEADER.to_string(),
243 ])
244 .maybe_timeout_secs(timeout_secs)
245 .maybe_proxy_url(proxy_url)
246 .rate_limiters(rate_limiters)
247 .build()?;
248
249 Ok(Self {
250 retry_manager: Arc::new(RetryManager::new(crate::common::http::retry_config())),
251 client,
252 base_url,
253 api_path,
254 credential,
255 recv_window,
256 order_rate_keys: order_keys,
257 })
258 }
259
260 fn shared_rate_limiters(
261 environment: BinanceEnvironment,
262 base_url_override: Option<&str>,
263 proxy_url: Option<&str>,
264 account_scope: Option<BinanceFuturesAccountScope>,
265 request_quota: Quota,
266 order_quotas: Vec<(String, Quota)>,
267 ) -> Vec<BinanceFuturesRateLimiter> {
268 let endpoint = Self::endpoint_scope(environment, base_url_override);
269 let request_scope = BinanceFuturesRequestScope {
270 endpoint: endpoint.clone(),
271 proxy_url_digest: proxy_url.map(Self::sha256),
272 };
273 let request_limiter = Self::request_rate_limiter(request_scope, request_quota);
274 let mut limiters = vec![request_limiter];
275
276 if let Some(account) = account_scope {
277 let order_scope = BinanceFuturesOrderScope { endpoint, account };
278 limiters.push(Self::order_rate_limiter(order_scope, order_quotas));
279 }
280
281 limiters
282 }
283
284 fn endpoint_scope(
285 environment: BinanceEnvironment,
286 base_url_override: Option<&str>,
287 ) -> BinanceFuturesEndpointScope {
288 let Some(base_url) = base_url_override else {
289 return BinanceFuturesEndpointScope::Environment(environment);
290 };
291
292 let normalized = base_url.trim_end_matches('/');
293 let official_urls = [
294 get_http_base_url(BinanceProductType::UsdM, environment),
295 get_http_base_url(BinanceProductType::CoinM, environment),
296 ];
297
298 if official_urls.contains(&normalized) {
299 BinanceFuturesEndpointScope::Environment(environment)
300 } else {
301 BinanceFuturesEndpointScope::Custom {
302 environment,
303 base_url: normalized.to_string(),
304 }
305 }
306 }
307
308 fn account_scope(credential: &SigningCredential) -> BinanceFuturesAccountScope {
309 BinanceFuturesAccountScope(Self::sha256(credential.api_key()))
310 }
311
312 fn sha256(value: &str) -> [u8; 32] {
313 digest::digest(&digest::SHA256, value.as_bytes())
314 .as_ref()
315 .try_into()
316 .expect("SHA-256 digest must contain 32 bytes")
317 }
318
319 fn request_rate_limiter(
320 scope: BinanceFuturesRequestScope,
321 quota: Quota,
322 ) -> BinanceFuturesRateLimiter {
323 let mut registry = BINANCE_FUTURES_REQUEST_LIMITERS.lock();
324
325 if let Some(limiter) = registry.get(&scope).and_then(Weak::upgrade) {
326 return limiter;
327 }
328
329 let limiter = Arc::new(RateLimiter::new_with_quota(
330 None,
331 vec![(Ustr::from(BINANCE_GLOBAL_RATE_KEY), quota)],
332 ));
333 registry.insert(scope, Arc::downgrade(&limiter));
334 limiter
335 }
336
337 fn order_rate_limiter(
338 scope: BinanceFuturesOrderScope,
339 quotas: Vec<(String, Quota)>,
340 ) -> BinanceFuturesRateLimiter {
341 let mut registry = BINANCE_FUTURES_ORDER_LIMITERS.lock();
342
343 if let Some(limiter) = registry.get(&scope).and_then(Weak::upgrade) {
344 return limiter;
345 }
346
347 let quotas = quotas
348 .into_iter()
349 .map(|(key, quota)| (Ustr::from(&key), quota))
350 .collect();
351 let limiter = Arc::new(RateLimiter::new_with_quota(None, quotas));
352 registry.insert(scope, Arc::downgrade(&limiter));
353 limiter
354 }
355
356 pub async fn get<P, T>(
362 &self,
363 path: &str,
364 params: Option<&P>,
365 signed: bool,
366 use_order_quota: bool,
367 ) -> BinanceFuturesHttpResult<T>
368 where
369 P: Serialize + ?Sized,
370 T: DeserializeOwned,
371 {
372 self.request(Method::GET, path, params, signed, use_order_quota, None)
373 .await
374 }
375
376 pub async fn post<P, T>(
382 &self,
383 path: &str,
384 params: Option<&P>,
385 body: Option<Vec<u8>>,
386 signed: bool,
387 use_order_quota: bool,
388 ) -> BinanceFuturesHttpResult<T>
389 where
390 P: Serialize + ?Sized,
391 T: DeserializeOwned,
392 {
393 self.request(Method::POST, path, params, signed, use_order_quota, body)
394 .await
395 }
396
397 pub async fn request_put<P, T>(
403 &self,
404 path: &str,
405 params: Option<&P>,
406 signed: bool,
407 use_order_quota: bool,
408 ) -> BinanceFuturesHttpResult<T>
409 where
410 P: Serialize + ?Sized,
411 T: DeserializeOwned,
412 {
413 self.request(Method::PUT, path, params, signed, use_order_quota, None)
414 .await
415 }
416
417 pub async fn request_delete<P, T>(
423 &self,
424 path: &str,
425 params: Option<&P>,
426 signed: bool,
427 use_order_quota: bool,
428 ) -> BinanceFuturesHttpResult<T>
429 where
430 P: Serialize + ?Sized,
431 T: DeserializeOwned,
432 {
433 self.request(Method::DELETE, path, params, signed, use_order_quota, None)
434 .await
435 }
436
437 pub async fn batch_request<T: Serialize>(
443 &self,
444 path: &str,
445 items: &[T],
446 use_order_quota: bool,
447 ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
448 self.batch_request_method(Method::POST, path, items, use_order_quota)
449 .await
450 }
451
452 pub async fn batch_request_delete<T: Serialize>(
458 &self,
459 path: &str,
460 items: &[T],
461 use_order_quota: bool,
462 ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
463 self.batch_request_method(Method::DELETE, path, items, use_order_quota)
464 .await
465 }
466
467 pub async fn batch_request_put<T: Serialize>(
473 &self,
474 path: &str,
475 items: &[T],
476 use_order_quota: bool,
477 ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
478 self.batch_request_method(Method::PUT, path, items, use_order_quota)
479 .await
480 }
481
482 async fn batch_request_method<T: Serialize>(
483 &self,
484 method: Method,
485 path: &str,
486 items: &[T],
487 use_order_quota: bool,
488 ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
489 let cred = self
490 .credential
491 .as_ref()
492 .ok_or(BinanceFuturesHttpError::MissingCredentials)?;
493
494 let batch_json = serde_json::to_string(items)
495 .map_err(|e| BinanceFuturesHttpError::ValidationError(e.to_string()))?;
496
497 let encoded_batch = Self::percent_encode(&batch_json);
498 let timestamp = Timestamp::now().as_millisecond();
499 let mut query = format!("batchOrders={encoded_batch}×tamp={timestamp}");
500
501 if let Some(recv_window) = self.recv_window {
502 query.push_str(&format!("&recvWindow={recv_window}"));
503 }
504
505 let signature = Self::percent_encode(&cred.sign(&query));
506 query.push_str(&format!("&signature={signature}"));
507
508 let url = self.build_url(path, &query);
509
510 let mut headers = HashMap::new();
511 headers.insert(
512 BINANCE_API_KEY_HEADER.to_string(),
513 cred.api_key().to_string(),
514 );
515
516 let keys = self.rate_limit_keys(use_order_quota);
517
518 let response = self
519 .client
520 .request_with_url_redacted(
521 method,
522 url,
523 None::<&HashMap<String, Vec<String>>>,
524 Some(headers),
525 None,
526 None,
527 Some(keys),
528 )
529 .await?;
530
531 if !response.status.is_success() {
532 return self.parse_error_response(&response);
533 }
534
535 serde_json::from_slice(&response.body)
536 .map_err(|e| BinanceFuturesHttpError::JsonError(e.to_string()))
537 }
538
539 fn percent_encode(input: &str) -> String {
541 let mut result = String::with_capacity(input.len() * 3);
542 for byte in input.bytes() {
543 match byte {
544 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
545 result.push(byte as char);
546 }
547 _ => {
548 result.push('%');
549 result.push_str(&format!("{byte:02X}"));
550 }
551 }
552 }
553 result
554 }
555
556 async fn request<P, T>(
557 &self,
558 method: Method,
559 path: &str,
560 params: Option<&P>,
561 signed: bool,
562 use_order_quota: bool,
563 body: Option<Vec<u8>>,
564 ) -> BinanceFuturesHttpResult<T>
565 where
566 P: Serialize + ?Sized,
567 T: DeserializeOwned,
568 {
569 let operation = || {
570 self.request_once(
571 method.clone(),
572 path,
573 params,
574 signed,
575 use_order_quota,
576 body.clone(),
577 )
578 };
579
580 if method != Method::GET {
581 return operation().await;
582 }
583 self.retry_manager
584 .invocation(
585 path,
586 operation,
587 BinanceFuturesHttpError::is_retryable,
588 |e| match e {
589 RetryError::Canceled => {
590 BinanceFuturesHttpError::Canceled("HTTP requests canceled".to_string())
591 }
592 RetryError::OperationTimeout { timeout_ms } => {
593 BinanceFuturesHttpError::Timeout(format!("Request exceeded {timeout_ms}ms"))
594 }
595 RetryError::InvalidConfiguration { message } => {
596 BinanceFuturesHttpError::ValidationError(message)
597 }
598 e @ RetryError::ElapsedBudgetExceeded { .. } => {
599 BinanceFuturesHttpError::RetryBudgetExceeded(e.to_string())
600 }
601 },
602 )
603 .retry_delay(&BinanceFuturesHttpError::retry_after)
604 .execute()
605 .await
606 }
607
608 async fn request_once<P, T>(
609 &self,
610 method: Method,
611 path: &str,
612 params: Option<&P>,
613 signed: bool,
614 use_order_quota: bool,
615 body: Option<Vec<u8>>,
616 ) -> BinanceFuturesHttpResult<T>
617 where
618 P: Serialize + ?Sized,
619 T: DeserializeOwned,
620 {
621 let mut query = params
622 .map(serde_urlencoded::to_string)
623 .transpose()
624 .map_err(|e| BinanceFuturesHttpError::ValidationError(e.to_string()))?
625 .unwrap_or_default();
626
627 let mut headers = HashMap::new();
628
629 if signed {
630 let cred = self
631 .credential
632 .as_ref()
633 .ok_or(BinanceFuturesHttpError::MissingCredentials)?;
634
635 if !query.is_empty() {
636 query.push('&');
637 }
638
639 let timestamp = Timestamp::now().as_millisecond();
640 query.push_str(&format!("timestamp={timestamp}"));
641
642 if let Some(recv_window) = self.recv_window {
643 query.push_str(&format!("&recvWindow={recv_window}"));
644 }
645
646 let signature = Self::percent_encode(&cred.sign(&query));
650 query.push_str(&format!("&signature={signature}"));
651 headers.insert(
652 BINANCE_API_KEY_HEADER.to_string(),
653 cred.api_key().to_string(),
654 );
655 }
656
657 let url = self.build_url(path, &query);
658 let keys = self.rate_limit_keys(use_order_quota);
659
660 let response = self
661 .client
662 .request_with_url_redacted(
663 method,
664 url,
665 None::<&HashMap<String, Vec<String>>>,
666 Some(headers),
667 body,
668 None,
669 Some(keys),
670 )
671 .await?;
672
673 if !response.status.is_success() {
674 return self.parse_error_response(&response);
675 }
676
677 serde_json::from_slice::<T>(&response.body)
678 .map_err(|e| BinanceFuturesHttpError::JsonError(e.to_string()))
679 }
680
681 fn build_url(&self, path: &str, query: &str) -> String {
682 let url_path = if path.starts_with("/fapi/")
684 || path.starts_with("/dapi/")
685 || path.starts_with("/futures/data/")
686 {
687 path.to_string()
688 } else if path.starts_with('/') {
689 format!("{}{}", self.api_path, path)
690 } else {
691 format!("{}/{}", self.api_path, path)
692 };
693
694 let mut url = format!("{}{}", self.base_url, url_path);
695
696 if !query.is_empty() {
697 url.push('?');
698 url.push_str(query);
699 }
700 url
701 }
702
703 fn rate_limit_keys(&self, use_orders: bool) -> Vec<String> {
704 if use_orders {
705 let mut keys = Vec::with_capacity(1 + self.order_rate_keys.len());
706 keys.push(BINANCE_GLOBAL_RATE_KEY.to_string());
707 keys.extend(self.order_rate_keys.iter().cloned());
708 keys
709 } else {
710 vec![BINANCE_GLOBAL_RATE_KEY.to_string()]
711 }
712 }
713
714 fn parse_error_response<T>(&self, response: &HttpResponse) -> BinanceFuturesHttpResult<T> {
715 let status = response.status.as_u16();
716 let body = String::from_utf8_lossy(&response.body).to_string();
717 let retry_after = crate::common::http::retry_after(&response.headers, Timestamp::now());
718
719 if let Ok(err) = serde_json::from_str::<BinanceErrorResponse>(&body) {
720 return Err(BinanceFuturesHttpError::BinanceError {
721 code: err.code,
722 message: err.msg,
723 status,
724 retry_after,
725 });
726 }
727
728 Err(BinanceFuturesHttpError::UnexpectedStatus {
729 status,
730 body,
731 retry_after,
732 })
733 }
734
735 fn default_headers(credential: &Option<SigningCredential>) -> HashMap<String, String> {
736 let mut headers: HashMap<String, String> =
737 create_standard_nautilus_headers().into_iter().collect();
738
739 if let Some(cred) = credential {
740 headers.insert(
741 BINANCE_API_KEY_HEADER.to_string(),
742 cred.api_key().to_string(),
743 );
744 }
745 headers
746 }
747
748 fn resolve_api_path(product_type: BinanceProductType) -> &'static str {
749 match product_type {
750 BinanceProductType::UsdM => BINANCE_FAPI_PATH,
751 BinanceProductType::CoinM => BINANCE_DAPI_PATH,
752 _ => BINANCE_FAPI_PATH, }
754 }
755
756 fn rate_limit_config(product_type: BinanceProductType) -> RateLimitConfig {
757 let quotas = match product_type {
758 BinanceProductType::UsdM => BINANCE_FAPI_RATE_LIMITS,
759 BinanceProductType::CoinM => BINANCE_DAPI_RATE_LIMITS,
760 _ => BINANCE_FAPI_RATE_LIMITS,
761 };
762
763 let mut order_quotas = Vec::new();
764 let mut order_keys = Vec::new();
765 let mut request_quota = None;
766
767 for quota in quotas {
768 if let Some(q) = Self::quota_from(quota) {
769 match quota.rate_limit_type {
770 BinanceRateLimitType::RequestWeight if request_quota.is_none() => {
771 request_quota = Some(q);
772 }
773 BinanceRateLimitType::Orders => {
774 let key = format!(
775 "{}:{}:{:?}",
776 BINANCE_ORDERS_RATE_KEY, quota.interval_num, quota.interval
777 );
778 order_keys.push(key.clone());
779 order_quotas.push((key, q));
780 }
781 _ => {}
782 }
783 }
784 }
785
786 let request_quota = request_quota.unwrap_or_else(|| {
787 Quota::per_second(NonZeroU32::new(10).expect("non-zero")).expect("valid constant")
788 });
789
790 RateLimitConfig {
791 request_quota,
792 order_quotas,
793 order_keys,
794 }
795 }
796
797 fn quota_from(quota: &BinanceRateLimitQuota) -> Option<Quota> {
798 let burst = NonZeroU32::new(quota.limit)?;
799 let period = Self::quota_period(quota)?;
800 let replenish_interval_ns = period.as_nanos() / u128::from(quota.limit);
801 let replenish_interval_ns = u64::try_from(replenish_interval_ns).ok()?;
802
803 Quota::with_period(Duration::from_nanos(replenish_interval_ns))
804 .map(|q| q.allow_burst(burst))
805 }
806
807 fn quota_period(quota: &BinanceRateLimitQuota) -> Option<Duration> {
808 match quota.interval {
809 BinanceRateLimitInterval::Second => {
810 Some(Duration::from_secs(u64::from(quota.interval_num)))
811 }
812 BinanceRateLimitInterval::Minute => {
813 Some(Duration::from_secs(60 * u64::from(quota.interval_num)))
814 }
815 BinanceRateLimitInterval::Day => Some(Duration::from_secs(
816 SECONDS_IN_DAY * u64::from(quota.interval_num),
817 )),
818 BinanceRateLimitInterval::Unknown => None,
819 }
820 }
821
822 pub async fn ticker_24h(
828 &self,
829 params: &BinanceTicker24hrParams,
830 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesTicker24hr>> {
831 self.get("ticker/24hr", Some(params), false, false).await
832 }
833
834 pub async fn book_ticker(
840 &self,
841 params: &BinanceBookTickerParams,
842 ) -> BinanceFuturesHttpResult<Vec<BinanceBookTicker>> {
843 self.get("ticker/bookTicker", Some(params), false, false)
844 .await
845 }
846
847 pub async fn price_ticker(
853 &self,
854 symbol: Option<&str>,
855 ) -> BinanceFuturesHttpResult<Vec<BinancePriceTicker>> {
856 #[derive(Serialize)]
857 struct Params<'a> {
858 #[serde(skip_serializing_if = "Option::is_none")]
859 symbol: Option<&'a str>,
860 }
861 self.get("ticker/price", Some(&Params { symbol }), false, false)
862 .await
863 }
864
865 pub async fn depth(
871 &self,
872 params: &BinanceDepthParams,
873 ) -> BinanceFuturesHttpResult<BinanceOrderBook> {
874 self.get("depth", Some(params), false, false).await
875 }
876
877 pub async fn mark_price(
883 &self,
884 params: &BinanceMarkPriceParams,
885 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesMarkPrice>> {
886 let response: MarkPriceResponse =
887 self.get("premiumIndex", Some(params), false, false).await?;
888 Ok(response.into())
889 }
890
891 pub async fn funding_rate(
897 &self,
898 params: &BinanceFundingRateParams,
899 ) -> BinanceFuturesHttpResult<Vec<BinanceFundingRate>> {
900 self.get("fundingRate", Some(params), false, false).await
901 }
902
903 pub async fn open_interest(
909 &self,
910 params: &BinanceOpenInterestParams,
911 ) -> BinanceFuturesHttpResult<BinanceOpenInterest> {
912 self.get("openInterest", Some(params), false, false).await
913 }
914
915 pub async fn open_interest_hist(
921 &self,
922 params: &BinanceOpenInterestHistParams,
923 ) -> BinanceFuturesHttpResult<Vec<BinanceOpenInterestHistRecord>> {
924 self.get("/futures/data/openInterestHist", Some(params), false, false)
925 .await
926 }
927
928 pub async fn trades(
934 &self,
935 params: &BinanceTradesParams,
936 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesTrade>> {
937 self.get("trades", Some(params), false, false).await
938 }
939
940 pub async fn agg_trades(
946 &self,
947 params: &BinanceAggTradesParams,
948 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesAggTrade>> {
949 if params.limit.is_some_and(|limit| limit > 1000) {
950 return Err(BinanceFuturesHttpError::ValidationError(
951 "aggregate trade limit must not exceed 1000".to_string(),
952 ));
953 }
954
955 if let (Some(start), Some(end)) = (params.start_time, params.end_time) {
956 if start > end {
957 return Err(BinanceFuturesHttpError::ValidationError(
958 "aggregate trade startTime must not exceed endTime".to_string(),
959 ));
960 }
961 let Some(range) = end.checked_sub(start) else {
962 return Err(BinanceFuturesHttpError::ValidationError(
963 "aggregate trade time range must be less than one hour".to_string(),
964 ));
965 };
966
967 if range >= 3_600_000 {
968 return Err(BinanceFuturesHttpError::ValidationError(
969 "aggregate trade time range must be less than one hour".to_string(),
970 ));
971 }
972 }
973
974 self.get("aggTrades", Some(params), false, false).await
975 }
976
977 pub async fn klines(
983 &self,
984 params: &BinanceKlinesParams,
985 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesKline>> {
986 self.get("klines", Some(params), false, false).await
987 }
988
989 pub async fn set_leverage(
995 &self,
996 params: &BinanceSetLeverageParams,
997 ) -> BinanceFuturesHttpResult<BinanceLeverageResponse> {
998 self.post("leverage", Some(params), None, true, false).await
999 }
1000
1001 pub async fn set_margin_type(
1007 &self,
1008 params: &BinanceSetMarginTypeParams,
1009 ) -> BinanceFuturesHttpResult<serde_json::Value> {
1010 self.post("marginType", Some(params), None, true, false)
1011 .await
1012 }
1013
1014 pub async fn query_hedge_mode(&self) -> BinanceFuturesHttpResult<BinanceHedgeModeResponse> {
1020 self.get::<(), _>("positionSide/dual", None, true, false)
1021 .await
1022 }
1023
1024 pub async fn create_listen_key(&self) -> BinanceFuturesHttpResult<ListenKeyResponse> {
1030 self.post::<(), ListenKeyResponse>("listenKey", None, None, true, false)
1031 .await
1032 }
1033
1034 pub async fn keepalive_listen_key(&self, listen_key: &str) -> BinanceFuturesHttpResult<()> {
1040 let params = ListenKeyParams {
1041 listen_key: listen_key.into(),
1042 };
1043 let _: serde_json::Value = self
1044 .request_put("listenKey", Some(¶ms), true, false)
1045 .await?;
1046 Ok(())
1047 }
1048
1049 pub async fn close_listen_key(&self, listen_key: &str) -> BinanceFuturesHttpResult<()> {
1055 let params = ListenKeyParams {
1056 listen_key: listen_key.into(),
1057 };
1058 let _: serde_json::Value = self
1059 .request_delete("listenKey", Some(¶ms), true, false)
1060 .await?;
1061 Ok(())
1062 }
1063
1064 pub async fn query_account(&self) -> BinanceFuturesHttpResult<BinanceFuturesAccountInfo> {
1070 let path = if self.api_path.starts_with("/fapi") {
1072 "/fapi/v2/account"
1073 } else {
1074 "/dapi/v1/account"
1075 };
1076 self.get::<(), _>(path, None, true, false).await
1077 }
1078
1079 pub async fn commission_rate(
1085 &self,
1086 params: &BinanceCommissionRateParams,
1087 ) -> BinanceFuturesHttpResult<BinanceFuturesCommissionRate> {
1088 self.get("commissionRate", Some(params), true, false).await
1089 }
1090
1091 pub async fn query_positions(
1097 &self,
1098 params: &BinancePositionRiskParams,
1099 ) -> BinanceFuturesHttpResult<Vec<BinancePositionRisk>> {
1100 let path = if self.api_path.starts_with("/fapi") {
1102 "/fapi/v2/positionRisk"
1103 } else {
1104 "/dapi/v1/positionRisk"
1105 };
1106 self.get(path, Some(params), true, false).await
1107 }
1108
1109 pub async fn query_user_trades(
1115 &self,
1116 params: &BinanceUserTradesParams,
1117 ) -> BinanceFuturesHttpResult<Vec<BinanceUserTrade>> {
1118 self.get("userTrades", Some(params), true, false).await
1119 }
1120
1121 pub async fn query_order(
1127 &self,
1128 params: &BinanceOrderQueryParams,
1129 ) -> BinanceFuturesHttpResult<BinanceFuturesOrder> {
1130 self.get("order", Some(params), true, false).await
1131 }
1132
1133 pub async fn query_open_orders(
1139 &self,
1140 params: &BinanceOpenOrdersParams,
1141 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesOrder>> {
1142 self.get("openOrders", Some(params), true, false).await
1143 }
1144
1145 pub async fn query_all_orders(
1151 &self,
1152 params: &BinanceAllOrdersParams,
1153 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesOrder>> {
1154 self.get("allOrders", Some(params), true, false).await
1155 }
1156
1157 pub async fn submit_order(
1163 &self,
1164 params: &BinanceNewOrderParams,
1165 ) -> BinanceFuturesHttpResult<BinanceFuturesOrder> {
1166 self.post("order", Some(params), None, true, true).await
1167 }
1168
1169 pub async fn submit_order_list(
1175 &self,
1176 orders: &[BatchOrderItem],
1177 ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
1178 if orders.is_empty() {
1179 return Ok(Vec::new());
1180 }
1181
1182 if orders.len() > 5 {
1183 return Err(BinanceFuturesHttpError::ValidationError(
1184 "Batch order limit is 5 orders maximum".to_string(),
1185 ));
1186 }
1187
1188 self.batch_request("batchOrders", orders, true).await
1189 }
1190
1191 pub async fn modify_order(
1197 &self,
1198 params: &BinanceModifyOrderParams,
1199 ) -> BinanceFuturesHttpResult<BinanceFuturesOrder> {
1200 self.request_put("order", Some(params), true, true).await
1201 }
1202
1203 pub async fn batch_modify_orders(
1209 &self,
1210 modifies: &[BatchModifyItem],
1211 ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
1212 if modifies.is_empty() {
1213 return Ok(Vec::new());
1214 }
1215
1216 if modifies.len() > 5 {
1217 return Err(BinanceFuturesHttpError::ValidationError(
1218 "Batch modify limit is 5 orders maximum".to_string(),
1219 ));
1220 }
1221
1222 self.batch_request_put("batchOrders", modifies, true).await
1223 }
1224
1225 pub async fn cancel_order(
1231 &self,
1232 params: &BinanceCancelOrderParams,
1233 ) -> BinanceFuturesHttpResult<BinanceFuturesOrder> {
1234 self.request_delete("order", Some(params), true, true).await
1235 }
1236
1237 pub async fn cancel_all_orders(
1243 &self,
1244 params: &BinanceCancelAllOrdersParams,
1245 ) -> BinanceFuturesHttpResult<BinanceCancelAllOrdersResponse> {
1246 self.request_delete("allOpenOrders", Some(params), true, true)
1247 .await
1248 }
1249
1250 pub async fn batch_cancel_orders(
1256 &self,
1257 cancels: &[BatchCancelItem],
1258 ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
1259 if cancels.is_empty() {
1260 return Ok(Vec::new());
1261 }
1262
1263 if cancels.len() > 10 {
1264 return Err(BinanceFuturesHttpError::ValidationError(
1265 "Batch cancel limit is 10 orders maximum".to_string(),
1266 ));
1267 }
1268
1269 let params = Self::batch_cancel_params(cancels)?;
1270 self.request_delete("batchOrders", Some(¶ms), true, true)
1271 .await
1272 }
1273
1274 fn batch_cancel_params(
1275 cancels: &[BatchCancelItem],
1276 ) -> BinanceFuturesHttpResult<BatchCancelParams> {
1277 let symbol = cancels[0].symbol.clone();
1278 let mut order_ids = Vec::new();
1279 let mut client_order_ids = Vec::new();
1280
1281 for cancel in cancels {
1282 if cancel.symbol != symbol {
1283 return Err(BinanceFuturesHttpError::ValidationError(
1284 "Batch cancel orders must use the same symbol".to_string(),
1285 ));
1286 }
1287
1288 if let Some(order_id) = cancel.order_id {
1289 order_ids.push(order_id);
1290 }
1291
1292 if let Some(client_order_id) = &cancel.orig_client_order_id {
1293 client_order_ids.push(client_order_id.clone());
1294 }
1295 }
1296
1297 if order_ids.is_empty() && client_order_ids.is_empty() {
1298 return Err(BinanceFuturesHttpError::ValidationError(
1299 "Batch cancel requires at least one order ID or client order ID".to_string(),
1300 ));
1301 }
1302
1303 if !order_ids.is_empty() && !client_order_ids.is_empty() {
1304 return Err(BinanceFuturesHttpError::ValidationError(
1305 "Batch cancel requires either order IDs or client order IDs, not both".to_string(),
1306 ));
1307 }
1308
1309 let order_id_list = if order_ids.is_empty() {
1310 None
1311 } else {
1312 Some(
1313 serde_json::to_string(&order_ids)
1314 .map_err(|e| BinanceFuturesHttpError::ValidationError(e.to_string()))?,
1315 )
1316 };
1317 let orig_client_order_id_list = if client_order_ids.is_empty() {
1318 None
1319 } else {
1320 Some(
1321 serde_json::to_string(&client_order_ids)
1322 .map_err(|e| BinanceFuturesHttpError::ValidationError(e.to_string()))?,
1323 )
1324 };
1325
1326 Ok(BatchCancelParams {
1327 symbol,
1328 order_id_list,
1329 orig_client_order_id_list,
1330 })
1331 }
1332
1333 pub async fn submit_algo_order(
1342 &self,
1343 params: &BinanceNewAlgoOrderParams,
1344 ) -> BinanceFuturesHttpResult<BinanceFuturesAlgoOrder> {
1345 self.post("algoOrder", Some(params), None, true, true).await
1346 }
1347
1348 pub async fn cancel_algo_order(
1356 &self,
1357 params: &BinanceAlgoOrderQueryParams,
1358 ) -> BinanceFuturesHttpResult<BinanceFuturesAlgoOrderCancelResponse> {
1359 self.request_delete("algoOrder", Some(params), true, true)
1360 .await
1361 }
1362
1363 pub async fn query_algo_order(
1371 &self,
1372 params: &BinanceAlgoOrderQueryParams,
1373 ) -> BinanceFuturesHttpResult<BinanceFuturesAlgoOrder> {
1374 self.get("algoOrder", Some(params), true, false).await
1375 }
1376
1377 pub async fn query_open_algo_orders(
1383 &self,
1384 params: &BinanceOpenAlgoOrdersParams,
1385 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesAlgoOrder>> {
1386 self.get("openAlgoOrders", Some(params), true, false).await
1387 }
1388
1389 pub async fn query_all_algo_orders(
1395 &self,
1396 params: &BinanceAllAlgoOrdersParams,
1397 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesAlgoOrder>> {
1398 self.get("allAlgoOrders", Some(params), true, false).await
1399 }
1400
1401 pub async fn cancel_all_algo_orders(
1407 &self,
1408 params: &BinanceCancelAllAlgoOrdersParams,
1409 ) -> BinanceFuturesHttpResult<BinanceCancelAllOrdersResponse> {
1410 self.request_delete("algoOpenOrders", Some(params), true, true)
1411 .await
1412 }
1413}
1414
1415#[derive(Debug, Deserialize)]
1417#[serde(untagged)]
1418enum MarkPriceResponse {
1419 Single(BinanceFuturesMarkPrice),
1420 Multiple(Vec<BinanceFuturesMarkPrice>),
1421}
1422
1423impl From<MarkPriceResponse> for Vec<BinanceFuturesMarkPrice> {
1424 fn from(response: MarkPriceResponse) -> Self {
1425 match response {
1426 MarkPriceResponse::Single(price) => vec![price],
1427 MarkPriceResponse::Multiple(prices) => prices,
1428 }
1429 }
1430}
1431
1432struct RateLimitConfig {
1433 request_quota: Quota,
1434 order_quotas: Vec<(String, Quota)>,
1435 order_keys: Vec<String>,
1436}
1437
1438#[derive(Clone, Debug)]
1440pub enum BinanceFuturesInstrument {
1441 UsdM(BinanceFuturesUsdSymbol),
1443 CoinM(BinanceFuturesCoinSymbol),
1445}
1446
1447impl BinanceFuturesInstrument {
1448 #[must_use]
1450 pub const fn symbol(&self) -> Ustr {
1451 match self {
1452 Self::UsdM(s) => s.symbol,
1453 Self::CoinM(s) => s.symbol,
1454 }
1455 }
1456
1457 #[must_use]
1459 pub const fn price_precision(&self) -> i32 {
1460 match self {
1461 Self::UsdM(s) => s.price_precision,
1462 Self::CoinM(s) => s.price_precision,
1463 }
1464 }
1465
1466 #[must_use]
1468 pub const fn quantity_precision(&self) -> i32 {
1469 match self {
1470 Self::UsdM(s) => s.quantity_precision,
1471 Self::CoinM(s) => s.quantity_precision,
1472 }
1473 }
1474
1475 pub fn precisions(&self) -> BinanceFuturesHttpResult<(u8, u8)> {
1481 let price_precision = u8::try_from(self.price_precision()).map_err(|_| {
1482 BinanceFuturesHttpError::ValidationError(format!(
1483 "Invalid Binance Futures price precision {} for {}",
1484 self.price_precision(),
1485 self.symbol()
1486 ))
1487 })?;
1488 let quantity_precision = u8::try_from(self.quantity_precision()).map_err(|_| {
1489 BinanceFuturesHttpError::ValidationError(format!(
1490 "Invalid Binance Futures quantity precision {} for {}",
1491 self.quantity_precision(),
1492 self.symbol()
1493 ))
1494 })?;
1495
1496 if price_precision > FIXED_PRECISION || quantity_precision > FIXED_PRECISION {
1497 return Err(BinanceFuturesHttpError::ValidationError(format!(
1498 "Binance Futures precision exceeds maximum {FIXED_PRECISION} for {}: price={price_precision}, quantity={quantity_precision}",
1499 self.symbol()
1500 )));
1501 }
1502
1503 Ok((price_precision, quantity_precision))
1504 }
1505
1506 #[must_use]
1508 pub fn id(&self) -> InstrumentId {
1509 match self {
1510 Self::UsdM(s) => format_instrument_id(&s.symbol, BinanceProductType::UsdM),
1511 Self::CoinM(s) => format_instrument_id(&s.symbol, BinanceProductType::CoinM),
1512 }
1513 }
1514
1515 #[must_use]
1517 pub fn quote_currency(&self) -> Currency {
1518 let quote_asset = match self {
1519 Self::UsdM(s) => &s.quote_asset,
1520 Self::CoinM(s) => &s.quote_asset,
1521 };
1522 Currency::get_or_create_crypto_with_context(quote_asset.as_str(), Some("futures quote"))
1523 }
1524}
1525
1526#[derive(Debug, Clone)]
1528pub struct BinanceFuturesHttpClient {
1529 inner: Arc<BinanceRawFuturesHttpClient>,
1530 product_type: BinanceProductType,
1531 clock: &'static AtomicTime,
1532 instruments: Arc<DashMap<Ustr, BinanceFuturesInstrument>>,
1533 instruments_reconciliation: Arc<AtomicMap<InstrumentId, InstrumentAny>>,
1534 instruments_load_lock: Arc<tokio::sync::Mutex<()>>,
1535 treat_expired_as_canceled: bool,
1536}
1537
1538impl BinanceFuturesHttpClient {
1539 #[expect(clippy::too_many_arguments)]
1545 pub fn new(
1546 product_type: BinanceProductType,
1547 environment: BinanceEnvironment,
1548 clock: &'static AtomicTime,
1549 api_key: Option<String>,
1550 api_secret: Option<String>,
1551 base_url_override: Option<String>,
1552 recv_window: Option<u64>,
1553 timeout_secs: Option<u64>,
1554 proxy_url: Option<String>,
1555 treat_expired_as_canceled: bool,
1556 ) -> BinanceFuturesHttpResult<Self> {
1557 match product_type {
1558 BinanceProductType::UsdM | BinanceProductType::CoinM => {}
1559 _ => {
1560 return Err(BinanceFuturesHttpError::ValidationError(format!(
1561 "BinanceFuturesHttpClient requires UsdM or CoinM product type, was {product_type:?}"
1562 )));
1563 }
1564 }
1565
1566 let raw = BinanceRawFuturesHttpClient::new(
1567 product_type,
1568 environment,
1569 api_key,
1570 api_secret,
1571 base_url_override,
1572 recv_window,
1573 timeout_secs,
1574 proxy_url,
1575 )?;
1576
1577 Ok(Self {
1578 inner: Arc::new(raw),
1579 product_type,
1580 clock,
1581 instruments: Arc::new(DashMap::new()),
1582 instruments_reconciliation: Arc::new(AtomicMap::new()),
1583 instruments_load_lock: Arc::new(tokio::sync::Mutex::new(())),
1584 treat_expired_as_canceled,
1585 })
1586 }
1587
1588 pub(crate) fn with_retry_config(mut self, config: RetryConfig) -> Self {
1589 Arc::make_mut(&mut self.inner).retry_manager = Arc::new(RetryManager::new(config));
1590 self
1591 }
1592
1593 #[must_use]
1595 pub const fn product_type(&self) -> BinanceProductType {
1596 self.product_type
1597 }
1598
1599 #[must_use]
1601 pub fn inner(&self) -> &BinanceRawFuturesHttpClient {
1602 &self.inner
1603 }
1604
1605 #[must_use]
1607 pub fn instruments_cache(&self) -> Arc<DashMap<Ustr, BinanceFuturesInstrument>> {
1608 Arc::clone(&self.instruments)
1609 }
1610
1611 pub(crate) fn instrument_reconciliation(
1613 &self,
1614 instrument_id: &InstrumentId,
1615 ) -> Option<InstrumentAny> {
1616 self.instruments_reconciliation.get_cloned(instrument_id)
1617 }
1618
1619 #[must_use]
1621 pub fn has_credentials(&self) -> bool {
1622 self.inner.has_credentials()
1623 }
1624
1625 fn replace_instruments(
1627 &self,
1628 instruments: Vec<(Ustr, BinanceFuturesInstrument)>,
1629 ) -> BinanceFuturesHttpResult<()> {
1630 let mut snapshot = AHashMap::with_capacity(instruments.len());
1631 for (symbol, instrument) in instruments {
1632 instrument.precisions()?;
1633 if instrument.symbol() != symbol {
1634 return Err(BinanceFuturesHttpError::ValidationError(format!(
1635 "Binance Futures catalog key {symbol} does not match instrument symbol {}",
1636 instrument.symbol()
1637 )));
1638 }
1639 let expected_id = format_instrument_id(&symbol, self.product_type);
1640 if instrument.id() != expected_id {
1641 return Err(BinanceFuturesHttpError::ValidationError(format!(
1642 "Binance Futures catalog instrument ID {} does not match expected ID {expected_id}",
1643 instrument.id()
1644 )));
1645 }
1646
1647 if snapshot.insert(symbol, instrument).is_some() {
1648 return Err(BinanceFuturesHttpError::ValidationError(format!(
1649 "Duplicate Binance Futures catalog symbol {symbol}"
1650 )));
1651 }
1652 }
1653
1654 let symbols: AHashSet<_> = snapshot.keys().copied().collect();
1655 for (symbol, instrument) in snapshot {
1656 self.instruments.insert(symbol, instrument);
1657 }
1658 self.instruments
1659 .retain(|symbol, _| symbols.contains(symbol));
1660 Ok(())
1661 }
1662
1663 pub async fn server_time(&self) -> BinanceFuturesHttpResult<BinanceServerTime> {
1669 self.inner
1670 .get::<_, BinanceServerTime>("time", None::<&()>, false, false)
1671 .await
1672 }
1673
1674 pub async fn set_leverage(
1680 &self,
1681 params: &BinanceSetLeverageParams,
1682 ) -> BinanceFuturesHttpResult<BinanceLeverageResponse> {
1683 self.inner.set_leverage(params).await
1684 }
1685
1686 pub async fn set_margin_type(
1692 &self,
1693 params: &BinanceSetMarginTypeParams,
1694 ) -> BinanceFuturesHttpResult<serde_json::Value> {
1695 self.inner.set_margin_type(params).await
1696 }
1697
1698 pub async fn query_hedge_mode(&self) -> BinanceFuturesHttpResult<BinanceHedgeModeResponse> {
1704 self.inner.query_hedge_mode().await
1705 }
1706
1707 pub async fn create_listen_key(&self) -> BinanceFuturesHttpResult<ListenKeyResponse> {
1713 self.inner.create_listen_key().await
1714 }
1715
1716 pub async fn keepalive_listen_key(&self, listen_key: &str) -> BinanceFuturesHttpResult<()> {
1722 self.inner.keepalive_listen_key(listen_key).await
1723 }
1724
1725 pub async fn close_listen_key(&self, listen_key: &str) -> BinanceFuturesHttpResult<()> {
1731 self.inner.close_listen_key(listen_key).await
1732 }
1733
1734 pub async fn exchange_info(&self) -> BinanceFuturesHttpResult<()> {
1740 let _guard = self.instruments_load_lock.lock().await;
1741 let instruments = match self.product_type {
1742 BinanceProductType::UsdM => {
1743 let info: BinanceFuturesUsdExchangeInfo = self
1744 .inner
1745 .get("exchangeInfo", None::<&()>, false, false)
1746 .await?;
1747
1748 info.symbols
1749 .into_iter()
1750 .map(|symbol| (symbol.symbol, BinanceFuturesInstrument::UsdM(symbol)))
1751 .collect()
1752 }
1753 BinanceProductType::CoinM => {
1754 let info: BinanceFuturesCoinExchangeInfo = self
1755 .inner
1756 .get("exchangeInfo", None::<&()>, false, false)
1757 .await?;
1758
1759 info.symbols
1760 .into_iter()
1761 .map(|symbol| (symbol.symbol, BinanceFuturesInstrument::CoinM(symbol)))
1762 .collect()
1763 }
1764 _ => {
1765 return Err(BinanceFuturesHttpError::ValidationError(
1766 "Invalid product type for futures".to_string(),
1767 ));
1768 }
1769 };
1770
1771 self.replace_instruments(instruments)
1772 }
1773
1774 pub async fn request_symbol_statuses(
1784 &self,
1785 ) -> BinanceFuturesHttpResult<AHashMap<Ustr, MarketStatusAction>> {
1786 let mut statuses = AHashMap::new();
1787
1788 match self.product_type {
1789 BinanceProductType::UsdM => {
1790 let info: BinanceFuturesUsdExchangeInfo = self
1791 .inner
1792 .get("exchangeInfo", None::<&()>, false, false)
1793 .await?;
1794
1795 for symbol in &info.symbols {
1796 statuses.insert(symbol.symbol, MarketStatusAction::from(symbol.status));
1797 }
1798 }
1799 BinanceProductType::CoinM => {
1800 let info: BinanceFuturesCoinExchangeInfo = self
1801 .inner
1802 .get("exchangeInfo", None::<&()>, false, false)
1803 .await?;
1804
1805 for symbol in &info.symbols {
1806 let action = symbol
1807 .contract_status
1808 .map_or(MarketStatusAction::NotAvailableForTrading, Into::into);
1809 statuses.insert(symbol.symbol, action);
1810 }
1811 }
1812 _ => {
1813 return Err(BinanceFuturesHttpError::ValidationError(
1814 "Invalid product type for futures".to_string(),
1815 ));
1816 }
1817 }
1818
1819 Ok(statuses)
1820 }
1821
1822 pub async fn request_instruments(&self) -> BinanceFuturesHttpResult<Vec<InstrumentAny>> {
1828 self.request_instruments_with_config(&BinanceInstrumentProviderConfig::default())
1829 .await
1830 }
1831
1832 pub async fn request_instruments_with_config(
1844 &self,
1845 config: &BinanceInstrumentProviderConfig,
1846 ) -> BinanceFuturesHttpResult<Vec<InstrumentAny>> {
1847 let _guard = self.instruments_load_lock.lock().await;
1848 config
1849 .validate(self.product_type)
1850 .map_err(|e| BinanceFuturesHttpError::ValidationError(e.to_string()))?;
1851 let selector = BinanceInstrumentSelector::new(config)
1852 .map_err(|e| BinanceFuturesHttpError::ValidationError(e.to_string()))?;
1853 let ts_init = UnixNanos::default();
1854 let fallback_fees = self.futures_fallback_fees(config).await;
1855 let mut cache = Vec::new();
1856 let mut reconciliation = AHashMap::new();
1857
1858 let instruments = match self.product_type {
1859 BinanceProductType::UsdM => {
1860 let info: BinanceFuturesUsdExchangeInfo = self
1861 .inner
1862 .get("exchangeInfo", None::<&()>, false, false)
1863 .await?;
1864
1865 let mut instruments = Vec::with_capacity(info.symbols.len());
1866
1867 for symbol in info.symbols {
1868 let instrument_id =
1869 format_instrument_id(&symbol.symbol, BinanceProductType::UsdM);
1870 cache.push((
1871 symbol.symbol,
1872 BinanceFuturesInstrument::UsdM(symbol.clone()),
1873 ));
1874
1875 if !selector.includes(
1876 instrument_id,
1877 &symbol.symbol,
1878 &symbol.base_asset,
1879 &symbol.quote_asset,
1880 Some(&symbol.contract_type),
1881 ) {
1882 continue;
1883 }
1884
1885 let fees = self
1886 .futures_symbol_fees(config, &symbol.symbol, fallback_fees)
1887 .await;
1888
1889 match parse_usdm_instrument_with_fees(
1890 &symbol,
1891 Some(fees.0),
1892 Some(fees.1),
1893 ts_init,
1894 ts_init,
1895 ) {
1896 Ok(instrument) => {
1897 validate_reconciliation_instrument(
1898 &mut reconciliation,
1899 instrument_id,
1900 &instrument,
1901 )?;
1902 instruments.push(instrument);
1903 }
1904 Err(e) => {
1905 log_futures_instrument_parse_error(
1906 config,
1907 &selector,
1908 instrument_id,
1909 &symbol.symbol,
1910 &e,
1911 );
1912 }
1913 }
1914 }
1915
1916 log::debug!(
1917 "Loaded USD-M Futures instruments: count={}",
1918 instruments.len()
1919 );
1920 instruments
1921 }
1922 BinanceProductType::CoinM => {
1923 let info: BinanceFuturesCoinExchangeInfo = self
1924 .inner
1925 .get("exchangeInfo", None::<&()>, false, false)
1926 .await?;
1927
1928 let mut instruments = Vec::with_capacity(info.symbols.len());
1929 for symbol in info.symbols {
1930 let instrument_id =
1931 format_instrument_id(&symbol.symbol, BinanceProductType::CoinM);
1932 cache.push((
1933 symbol.symbol,
1934 BinanceFuturesInstrument::CoinM(symbol.clone()),
1935 ));
1936
1937 if !selector.includes(
1938 instrument_id,
1939 &symbol.symbol,
1940 &symbol.base_asset,
1941 &symbol.quote_asset,
1942 Some(&symbol.contract_type),
1943 ) {
1944 continue;
1945 }
1946
1947 let fees = self
1948 .futures_symbol_fees(config, &symbol.symbol, fallback_fees)
1949 .await;
1950
1951 match parse_coinm_instrument_with_fees(
1952 &symbol,
1953 Some(fees.0),
1954 Some(fees.1),
1955 ts_init,
1956 ts_init,
1957 ) {
1958 Ok(instrument) => {
1959 validate_reconciliation_instrument(
1960 &mut reconciliation,
1961 instrument_id,
1962 &instrument,
1963 )?;
1964 instruments.push(instrument);
1965 }
1966 Err(e) => {
1967 log_futures_instrument_parse_error(
1968 config,
1969 &selector,
1970 instrument_id,
1971 &symbol.symbol,
1972 &e,
1973 );
1974 }
1975 }
1976 }
1977
1978 log::debug!(
1979 "Loaded COIN-M Futures instruments: count={}",
1980 instruments.len()
1981 );
1982 instruments
1983 }
1984 _ => {
1985 return Err(BinanceFuturesHttpError::ValidationError(
1986 "Invalid product type for futures".to_string(),
1987 ));
1988 }
1989 };
1990
1991 self.replace_instruments(cache)?;
1992 self.instruments_reconciliation.store(reconciliation);
1993 Ok(instruments)
1994 }
1995
1996 async fn futures_fallback_fees(
1997 &self,
1998 config: &BinanceInstrumentProviderConfig,
1999 ) -> (Decimal, Decimal) {
2000 if !self.has_credentials() {
2001 return futures_fee_tier_rates(0);
2002 }
2003
2004 match self.query_account().await {
2005 Ok(account) => futures_fee_tier_rates(account.fee_tier),
2006 Err(e) => {
2007 if config.log_warnings {
2008 log::warn!("Unable to query Binance Futures fee tier; using VIP 0 rates: {e}");
2009 } else {
2010 log::debug!("Unable to query Binance Futures fee tier; using VIP 0 rates: {e}");
2011 }
2012 futures_fee_tier_rates(0)
2013 }
2014 }
2015 }
2016
2017 async fn futures_symbol_fees(
2018 &self,
2019 config: &BinanceInstrumentProviderConfig,
2020 symbol: &str,
2021 fallback: (Decimal, Decimal),
2022 ) -> (Decimal, Decimal) {
2023 if !config.query_commission_rates || !self.has_credentials() {
2024 return fallback;
2025 }
2026
2027 let params = BinanceCommissionRateParams {
2028 symbol: symbol.to_string(),
2029 };
2030
2031 match self.inner.commission_rate(¶ms).await {
2032 Ok(response) => parse_futures_commission_rates(&response).unwrap_or_else(|e| {
2033 log_futures_commission_fallback(config, symbol, &e, fallback);
2034 fallback
2035 }),
2036 Err(e) => {
2037 log_futures_commission_fallback(config, symbol, &e, fallback);
2038 fallback
2039 }
2040 }
2041 }
2042
2043 pub async fn ticker_24h(
2049 &self,
2050 params: &BinanceTicker24hrParams,
2051 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesTicker24hr>> {
2052 self.inner.ticker_24h(params).await
2053 }
2054
2055 pub async fn book_ticker(
2061 &self,
2062 params: &BinanceBookTickerParams,
2063 ) -> BinanceFuturesHttpResult<Vec<BinanceBookTicker>> {
2064 self.inner.book_ticker(params).await
2065 }
2066
2067 pub async fn price_ticker(
2073 &self,
2074 symbol: Option<&str>,
2075 ) -> BinanceFuturesHttpResult<Vec<BinancePriceTicker>> {
2076 self.inner.price_ticker(symbol).await
2077 }
2078
2079 pub async fn depth(
2085 &self,
2086 params: &BinanceDepthParams,
2087 ) -> BinanceFuturesHttpResult<BinanceOrderBook> {
2088 self.inner.depth(params).await
2089 }
2090
2091 pub async fn mark_price(
2097 &self,
2098 params: &BinanceMarkPriceParams,
2099 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesMarkPrice>> {
2100 self.inner.mark_price(params).await
2101 }
2102
2103 pub async fn funding_rate(
2109 &self,
2110 params: &BinanceFundingRateParams,
2111 ) -> BinanceFuturesHttpResult<Vec<BinanceFundingRate>> {
2112 self.inner.funding_rate(params).await
2113 }
2114
2115 pub async fn open_interest(
2121 &self,
2122 params: &BinanceOpenInterestParams,
2123 ) -> BinanceFuturesHttpResult<BinanceOpenInterest> {
2124 self.inner.open_interest(params).await
2125 }
2126
2127 pub async fn open_interest_hist(
2133 &self,
2134 params: &BinanceOpenInterestHistParams,
2135 ) -> BinanceFuturesHttpResult<Vec<BinanceOpenInterestHistRecord>> {
2136 self.inner.open_interest_hist(params).await
2137 }
2138
2139 pub async fn query_order(
2145 &self,
2146 params: &BinanceOrderQueryParams,
2147 ) -> BinanceFuturesHttpResult<BinanceFuturesOrder> {
2148 self.inner.query_order(params).await
2149 }
2150
2151 pub async fn query_open_orders(
2157 &self,
2158 params: &BinanceOpenOrdersParams,
2159 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesOrder>> {
2160 self.inner.query_open_orders(params).await
2161 }
2162
2163 pub async fn query_all_orders(
2169 &self,
2170 params: &BinanceAllOrdersParams,
2171 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesOrder>> {
2172 self.inner.query_all_orders(params).await
2173 }
2174
2175 pub async fn query_account(&self) -> BinanceFuturesHttpResult<BinanceFuturesAccountInfo> {
2181 self.inner.query_account().await
2182 }
2183
2184 pub async fn query_positions(
2190 &self,
2191 params: &BinancePositionRiskParams,
2192 ) -> BinanceFuturesHttpResult<Vec<BinancePositionRisk>> {
2193 self.inner.query_positions(params).await
2194 }
2195
2196 pub async fn query_user_trades(
2202 &self,
2203 params: &BinanceUserTradesParams,
2204 ) -> BinanceFuturesHttpResult<Vec<BinanceUserTrade>> {
2205 self.inner.query_user_trades(params).await
2206 }
2207
2208 #[expect(clippy::too_many_arguments)]
2218 pub async fn submit_order(
2219 &self,
2220 account_id: AccountId,
2221 instrument_id: InstrumentId,
2222 client_order_id: ClientOrderId,
2223 order_side: OrderSide,
2224 order_type: OrderType,
2225 quantity: Quantity,
2226 time_in_force: TimeInForce,
2227 price: Option<Price>,
2228 trigger_price: Option<Price>,
2229 reduce_only: bool,
2230 post_only: bool,
2231 rpi: bool,
2232 position_side: Option<BinancePositionSide>,
2233 price_match: Option<BinancePriceMatch>,
2234 good_till_date: Option<i64>,
2235 ) -> anyhow::Result<OrderStatusReport> {
2236 let (symbol, price_precision, size_precision) =
2237 self.cached_precisions_by_id(instrument_id)?;
2238
2239 let binance_side = BinanceSide::try_from(order_side)?;
2240 let binance_order_type = order_type_to_binance_futures(order_type)?;
2241 let binance_tif = if rpi {
2242 BinanceTimeInForce::Rpi
2243 } else if post_only {
2244 BinanceTimeInForce::Gtx
2245 } else {
2246 BinanceTimeInForce::try_from(time_in_force)?
2247 };
2248
2249 let requires_trigger_price = matches!(
2250 order_type,
2251 OrderType::StopMarket
2252 | OrderType::StopLimit
2253 | OrderType::TrailingStopMarket
2254 | OrderType::MarketIfTouched
2255 | OrderType::LimitIfTouched
2256 );
2257
2258 if requires_trigger_price && trigger_price.is_none() {
2259 anyhow::bail!("Order type {order_type:?} requires a trigger price");
2260 }
2261
2262 let requires_time_in_force = matches!(
2264 order_type,
2265 OrderType::Limit | OrderType::StopLimit | OrderType::LimitIfTouched
2266 );
2267
2268 let qty_str = quantity.to_string();
2269 let price_str = if price_match.is_some() {
2270 None
2271 } else {
2272 price.map(|p| p.to_string())
2273 };
2274 let stop_price_str = trigger_price.map(|p| p.to_string());
2275 let client_id_str = encode_broker_id(&client_order_id, BINANCE_NAUTILUS_FUTURES_BROKER_ID);
2276
2277 let params = BinanceNewOrderParams {
2278 symbol,
2279 side: binance_side,
2280 order_type: binance_order_type,
2281 time_in_force: if requires_time_in_force {
2282 Some(binance_tif)
2283 } else {
2284 None
2285 },
2286 quantity: Some(qty_str),
2287 price: price_str,
2288 new_client_order_id: Some(client_id_str),
2289 stop_price: stop_price_str,
2290 reduce_only: reduce_only_param(reduce_only, position_side),
2291 position_side,
2292 close_position: None,
2293 activation_price: None,
2294 callback_rate: None,
2295 working_type: None,
2296 price_protect: None,
2297 new_order_resp_type: None,
2298 good_till_date,
2299 recv_window: None,
2300 price_match,
2301 self_trade_prevention_mode: None,
2302 };
2303
2304 let order = self.inner.submit_order(¶ms).await?;
2305 let ts_init = self.clock.get_time_ns();
2306 order.to_order_status_report(
2307 account_id,
2308 instrument_id,
2309 price_precision,
2310 size_precision,
2311 self.treat_expired_as_canceled,
2312 ts_init,
2313 )
2314 }
2315
2316 #[expect(clippy::too_many_arguments)]
2329 pub async fn submit_algo_order(
2330 &self,
2331 account_id: AccountId,
2332 instrument_id: InstrumentId,
2333 client_order_id: ClientOrderId,
2334 order_side: OrderSide,
2335 order_type: OrderType,
2336 quantity: Quantity,
2337 time_in_force: TimeInForce,
2338 price: Option<Price>,
2339 trigger_price: Option<Price>,
2340 reduce_only: bool,
2341 close_position: bool,
2342 position_side: Option<BinancePositionSide>,
2343 activation_price: Option<Price>,
2344 callback_rate: Option<String>,
2345 working_type: Option<BinanceWorkingType>,
2346 good_till_date: Option<i64>,
2347 ) -> anyhow::Result<OrderStatusReport> {
2348 let (symbol, price_precision, size_precision) =
2349 self.cached_precisions_by_id(instrument_id)?;
2350
2351 let binance_side = BinanceSide::try_from(order_side)?;
2352 let binance_order_type = order_type_to_binance_futures(order_type)?;
2353 let binance_tif = BinanceTimeInForce::try_from(time_in_force)?;
2354
2355 let requires_trigger_price = matches!(
2356 order_type,
2357 OrderType::StopMarket
2358 | OrderType::StopLimit
2359 | OrderType::MarketIfTouched
2360 | OrderType::LimitIfTouched
2361 );
2362 anyhow::ensure!(
2363 !requires_trigger_price || trigger_price.is_some(),
2364 "Algo order type {order_type:?} requires a trigger price"
2365 );
2366
2367 let requires_time_in_force =
2369 matches!(order_type, OrderType::StopLimit | OrderType::LimitIfTouched);
2370
2371 let price_str = price.map(|p| p.to_string());
2372 let trigger_price_str = if matches!(order_type, OrderType::TrailingStopMarket) {
2373 None
2374 } else {
2375 trigger_price.map(|p| p.to_string())
2376 };
2377 let reduce_only = reduce_only_param(reduce_only, position_side);
2378 let client_id_str = encode_broker_id(&client_order_id, BINANCE_NAUTILUS_FUTURES_BROKER_ID);
2379
2380 let params = if close_position {
2382 BinanceNewAlgoOrderParams {
2383 symbol,
2384 side: binance_side,
2385 order_type: binance_order_type,
2386 algo_type: BinanceAlgoType::Conditional,
2387 position_side,
2388 quantity: None,
2389 price: price_str,
2390 trigger_price: trigger_price_str,
2391 time_in_force: if requires_time_in_force {
2392 Some(binance_tif)
2393 } else {
2394 None
2395 },
2396 working_type,
2397 close_position: Some(true),
2398 price_protect: None,
2399 reduce_only: None,
2400 activation_price: activation_price.map(|p| p.to_string()),
2401 callback_rate,
2402 client_algo_id: Some(client_id_str),
2403 good_till_date,
2404 recv_window: None,
2405 }
2406 } else {
2407 let qty_str = quantity.to_string();
2408 BinanceNewAlgoOrderParams {
2409 symbol,
2410 side: binance_side,
2411 order_type: binance_order_type,
2412 algo_type: BinanceAlgoType::Conditional,
2413 position_side,
2414 quantity: Some(qty_str),
2415 price: price_str,
2416 trigger_price: trigger_price_str,
2417 time_in_force: if requires_time_in_force {
2418 Some(binance_tif)
2419 } else {
2420 None
2421 },
2422 working_type,
2423 close_position: None,
2424 price_protect: None,
2425 reduce_only,
2426 activation_price: activation_price.map(|p| p.to_string()),
2427 callback_rate,
2428 client_algo_id: Some(client_id_str),
2429 good_till_date,
2430 recv_window: None,
2431 }
2432 };
2433
2434 let order = self.inner.submit_algo_order(¶ms).await?;
2435 let ts_init = self.clock.get_time_ns();
2436 order.to_order_status_report(
2437 account_id,
2438 instrument_id,
2439 price_precision,
2440 size_precision,
2441 ts_init,
2442 )
2443 }
2444
2445 pub async fn submit_order_list(
2454 &self,
2455 orders: &[BatchOrderItem],
2456 ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
2457 self.inner.submit_order_list(orders).await
2458 }
2459
2460 #[expect(clippy::too_many_arguments)]
2471 pub async fn modify_order(
2472 &self,
2473 account_id: AccountId,
2474 instrument_id: InstrumentId,
2475 venue_order_id: Option<VenueOrderId>,
2476 client_order_id: Option<ClientOrderId>,
2477 order_side: OrderSide,
2478 quantity: Quantity,
2479 price: Price,
2480 ) -> anyhow::Result<OrderStatusReport> {
2481 anyhow::ensure!(
2482 venue_order_id.is_some() || client_order_id.is_some(),
2483 "Either venue_order_id or client_order_id must be provided"
2484 );
2485
2486 let (symbol, price_precision, size_precision) =
2487 self.cached_precisions_by_id(instrument_id)?;
2488
2489 let binance_side = BinanceSide::try_from(order_side)?;
2490
2491 let order_id = venue_order_id
2492 .map(|id| id.inner().parse::<i64>())
2493 .transpose()
2494 .map_err(|_| anyhow::anyhow!("Invalid venue order ID"))?;
2495
2496 let params = BinanceModifyOrderParams {
2497 symbol,
2498 order_id,
2499 orig_client_order_id: client_order_id
2500 .map(|id| encode_broker_id(&id, BINANCE_NAUTILUS_FUTURES_BROKER_ID)),
2501 side: binance_side,
2502 quantity: quantity.to_string(),
2503 price: price.to_string(),
2504 recv_window: None,
2505 };
2506
2507 let order = self.inner.modify_order(¶ms).await?;
2508 let ts_init = self.clock.get_time_ns();
2509 order.to_order_status_report(
2510 account_id,
2511 instrument_id,
2512 price_precision,
2513 size_precision,
2514 self.treat_expired_as_canceled,
2515 ts_init,
2516 )
2517 }
2518
2519 pub async fn batch_modify_orders(
2528 &self,
2529 modifies: &[BatchModifyItem],
2530 ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
2531 self.inner.batch_modify_orders(modifies).await
2532 }
2533
2534 pub async fn cancel_order(
2544 &self,
2545 instrument_id: InstrumentId,
2546 venue_order_id: Option<VenueOrderId>,
2547 client_order_id: Option<ClientOrderId>,
2548 ) -> anyhow::Result<VenueOrderId> {
2549 anyhow::ensure!(
2550 venue_order_id.is_some() || client_order_id.is_some(),
2551 "Either venue_order_id or client_order_id must be provided"
2552 );
2553
2554 let symbol = format_binance_symbol(&instrument_id);
2555
2556 let order_id = match venue_order_id {
2557 Some(venue_order_id) => match venue_order_id.inner().parse::<i64>() {
2558 Ok(order_id) => Some(order_id),
2559 Err(e) if client_order_id.is_some() => {
2560 log::warn!(
2561 "Unable to parse venue_order_id {venue_order_id} for cancel, canceling by client_order_id: {e}"
2562 );
2563 None
2564 }
2565 Err(e) => anyhow::bail!("Invalid venue order ID: {e}"),
2566 },
2567 None => None,
2568 };
2569
2570 let params = BinanceCancelOrderParams {
2571 symbol,
2572 order_id,
2573 orig_client_order_id: client_order_id
2574 .map(|id| encode_broker_id(&id, BINANCE_NAUTILUS_FUTURES_BROKER_ID)),
2575 recv_window: None,
2576 };
2577
2578 let order = self.inner.cancel_order(¶ms).await?;
2579 Ok(VenueOrderId::new(order.order_id.to_string()))
2580 }
2581
2582 pub async fn cancel_algo_order(&self, client_order_id: ClientOrderId) -> anyhow::Result<()> {
2591 let params = BinanceAlgoOrderQueryParams {
2592 algo_id: None,
2593 client_algo_id: Some(encode_broker_id(
2594 &client_order_id,
2595 BINANCE_NAUTILUS_FUTURES_BROKER_ID,
2596 )),
2597 recv_window: None,
2598 };
2599
2600 let response = self.inner.cancel_algo_order(¶ms).await?;
2601 if response.code.parse::<i32>().unwrap_or(0) == 200 {
2602 Ok(())
2603 } else {
2604 anyhow::bail!(
2605 "Cancel algo order failed: code={}, msg={}",
2606 response.code,
2607 response.msg
2608 )
2609 }
2610 }
2611
2612 pub async fn cancel_all_orders(
2618 &self,
2619 instrument_id: InstrumentId,
2620 ) -> anyhow::Result<Vec<VenueOrderId>> {
2621 let symbol = format_binance_symbol(&instrument_id);
2622
2623 let params = BinanceCancelAllOrdersParams {
2624 symbol,
2625 recv_window: None,
2626 };
2627
2628 let response = self.inner.cancel_all_orders(¶ms).await?;
2629 if response.code == 200 {
2630 Ok(vec![])
2631 } else {
2632 anyhow::bail!("Cancel all orders failed: {}", response.msg);
2633 }
2634 }
2635
2636 pub async fn cancel_all_algo_orders(&self, instrument_id: InstrumentId) -> anyhow::Result<()> {
2642 let symbol = format_binance_symbol(&instrument_id);
2643
2644 let params = BinanceCancelAllAlgoOrdersParams {
2645 symbol,
2646 recv_window: None,
2647 };
2648
2649 let response = self.inner.cancel_all_algo_orders(¶ms).await?;
2650 if response.code == 200 {
2651 Ok(())
2652 } else {
2653 anyhow::bail!("Cancel all algo orders failed: {}", response.msg);
2654 }
2655 }
2656
2657 pub async fn batch_cancel_orders(
2666 &self,
2667 cancels: &[BatchCancelItem],
2668 ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
2669 self.inner.batch_cancel_orders(cancels).await
2670 }
2671
2672 pub async fn query_open_algo_orders(
2680 &self,
2681 instrument_id: Option<InstrumentId>,
2682 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesAlgoOrder>> {
2683 let symbol = instrument_id.map(|id| format_binance_symbol(&id));
2684
2685 let params = BinanceOpenAlgoOrdersParams {
2686 symbol,
2687 recv_window: None,
2688 };
2689
2690 self.inner.query_open_algo_orders(¶ms).await
2691 }
2692
2693 pub async fn query_algo_order(
2699 &self,
2700 client_order_id: ClientOrderId,
2701 ) -> BinanceFuturesHttpResult<BinanceFuturesAlgoOrder> {
2702 let params = BinanceAlgoOrderQueryParams {
2703 algo_id: None,
2704 client_algo_id: Some(encode_broker_id(
2705 &client_order_id,
2706 BINANCE_NAUTILUS_FUTURES_BROKER_ID,
2707 )),
2708 recv_window: None,
2709 };
2710
2711 self.inner.query_algo_order(¶ms).await
2712 }
2713
2714 pub async fn query_algo_order_by_venue_order_id(
2720 &self,
2721 venue_order_id: VenueOrderId,
2722 ) -> BinanceFuturesHttpResult<BinanceFuturesAlgoOrder> {
2723 let algo_id = venue_order_id
2724 .inner()
2725 .parse::<i64>()
2726 .map_err(|e| BinanceFuturesHttpError::ValidationError(e.to_string()))?;
2727 let params = BinanceAlgoOrderQueryParams {
2728 algo_id: Some(algo_id),
2729 client_algo_id: None,
2730 recv_window: None,
2731 };
2732
2733 self.inner.query_algo_order(¶ms).await
2734 }
2735
2736 async fn query_historical_algo_order_by_venue_order_id(
2737 &self,
2738 instrument_id: InstrumentId,
2739 venue_order_id: VenueOrderId,
2740 ) -> BinanceFuturesHttpResult<Option<BinanceFuturesAlgoOrder>> {
2741 let algo_id = venue_order_id
2742 .inner()
2743 .parse::<i64>()
2744 .map_err(|e| BinanceFuturesHttpError::ValidationError(e.to_string()))?;
2745 let symbol = format_binance_symbol(&instrument_id);
2746 let params = BinanceAllAlgoOrdersParams {
2747 symbol,
2748 algo_id: Some(algo_id),
2749 start_time: None,
2750 end_time: None,
2751 page: None,
2752 limit: Some(1),
2753 recv_window: None,
2754 };
2755 let order = self
2756 .inner
2757 .query_all_algo_orders(¶ms)
2758 .await?
2759 .into_iter()
2760 .next()
2761 .filter(|order| order.algo_id == algo_id);
2762
2763 Ok(order)
2764 }
2765
2766 pub async fn query_algo_order_with_history(
2777 &self,
2778 instrument_id: InstrumentId,
2779 client_order_id: Option<ClientOrderId>,
2780 algo_venue_order_id: Option<VenueOrderId>,
2781 ) -> BinanceFuturesHttpResult<Option<BinanceFuturesAlgoOrderQueryResult>> {
2782 let order = if let Some(venue_order_id) = algo_venue_order_id {
2783 match self
2784 .query_algo_order_by_venue_order_id(venue_order_id)
2785 .await
2786 {
2787 Ok(order) => Some(order),
2788 Err(BinanceFuturesHttpError::BinanceError { code: -2013, .. }) => {
2789 self.query_historical_algo_order_by_venue_order_id(
2790 instrument_id,
2791 venue_order_id,
2792 )
2793 .await?
2794 }
2795 Err(e) => return Err(e),
2796 }
2797 } else {
2798 let Some(client_order_id) = client_order_id else {
2799 return Ok(None);
2800 };
2801
2802 match self.query_algo_order(client_order_id).await {
2803 Ok(order) => Some(order),
2804 Err(BinanceFuturesHttpError::BinanceError { code: -2013, .. }) => None,
2805 Err(e) => return Err(e),
2806 }
2807 };
2808
2809 let Some(order) = order else {
2810 return Ok(None);
2811 };
2812 let actual = if let Some(actual_order_id) = order
2813 .actual_order_id
2814 .as_deref()
2815 .filter(|id| !id.is_empty())
2816 .map(str::parse::<i64>)
2817 .transpose()
2818 .map_err(|e| BinanceFuturesHttpError::ValidationError(e.to_string()))?
2819 {
2820 let params = BinanceOrderQueryParams {
2821 symbol: format_binance_symbol(&instrument_id),
2822 order_id: Some(actual_order_id),
2823 orig_client_order_id: None,
2824 recv_window: None,
2825 };
2826
2827 match self.inner.query_order(¶ms).await {
2828 Ok(actual) => Some(actual),
2829 Err(
2830 e @ (BinanceFuturesHttpError::MissingCredentials
2831 | BinanceFuturesHttpError::ValidationError(_)),
2832 ) => return Err(e),
2833 Err(e) => {
2834 log::warn!(
2835 "Failed to enrich algo order with matching-engine order \
2836 {actual_order_id}: {e}; falling back to Algo Service execution fields"
2837 );
2838 None
2839 }
2840 }
2841 } else {
2842 None
2843 };
2844
2845 Ok(Some(BinanceFuturesAlgoOrderQueryResult {
2846 algo: order,
2847 actual,
2848 }))
2849 }
2850
2851 pub async fn request_account_state(
2857 &self,
2858 account_id: AccountId,
2859 ) -> anyhow::Result<AccountState> {
2860 let ts_init = UnixNanos::default();
2861 let account_info = self.inner.query_account().await?;
2862 account_info.to_account_state(account_id, ts_init)
2863 }
2864
2865 pub async fn request_order_status_report(
2873 &self,
2874 account_id: AccountId,
2875 instrument_id: InstrumentId,
2876 venue_order_id: Option<VenueOrderId>,
2877 client_order_id: Option<ClientOrderId>,
2878 ) -> anyhow::Result<OrderStatusReport> {
2879 anyhow::ensure!(
2880 venue_order_id.is_some() || client_order_id.is_some(),
2881 "Either venue_order_id or client_order_id must be provided"
2882 );
2883
2884 let (symbol, price_precision, size_precision) =
2885 self.cached_precisions_by_id(instrument_id)?;
2886
2887 let order_id = venue_order_id
2888 .map(|id| id.inner().parse::<i64>())
2889 .transpose()
2890 .map_err(|_| anyhow::anyhow!("Invalid venue order ID"))?;
2891
2892 let orig_client_order_id =
2893 client_order_id.map(|id| encode_broker_id(&id, BINANCE_NAUTILUS_FUTURES_BROKER_ID));
2894
2895 let params = BinanceOrderQueryParams {
2896 symbol,
2897 order_id,
2898 orig_client_order_id,
2899 recv_window: None,
2900 };
2901
2902 let order = self.inner.query_order(¶ms).await?;
2903 let ts_init = self.clock.get_time_ns();
2904 order.to_order_status_report(
2905 account_id,
2906 instrument_id,
2907 price_precision,
2908 size_precision,
2909 self.treat_expired_as_canceled,
2910 ts_init,
2911 )
2912 }
2913
2914 pub async fn request_order_status_reports(
2922 &self,
2923 account_id: AccountId,
2924 instrument_id: Option<InstrumentId>,
2925 open_only: bool,
2926 ) -> anyhow::Result<Vec<OrderStatusReport>> {
2927 let symbol = instrument_id.map(|id| format_binance_symbol(&id));
2928
2929 let orders = if open_only {
2930 let params = BinanceOpenOrdersParams {
2931 symbol: symbol.clone(),
2932 recv_window: None,
2933 };
2934 self.inner.query_open_orders(¶ms).await?
2935 } else {
2936 let symbol = symbol.ok_or_else(|| {
2938 anyhow::anyhow!("instrument_id is required for historical orders")
2939 })?;
2940 let params = BinanceAllOrdersParams {
2941 symbol,
2942 order_id: None,
2943 start_time: None,
2944 end_time: None,
2945 limit: None,
2946 recv_window: None,
2947 };
2948 self.inner.query_all_orders(¶ms).await?
2949 };
2950
2951 let ts_init = self.clock.get_time_ns();
2952 let mut reports = Vec::with_capacity(orders.len());
2953
2954 for order in orders {
2955 let order_instrument_id = instrument_id
2956 .unwrap_or_else(|| format_instrument_id(&order.symbol, self.product_type));
2957 let (_, price_precision, size_precision) =
2958 self.cached_precisions_by_id(order_instrument_id)?;
2959
2960 match order.to_order_status_report(
2961 account_id,
2962 order_instrument_id,
2963 price_precision,
2964 size_precision,
2965 self.treat_expired_as_canceled,
2966 ts_init,
2967 ) {
2968 Ok(report) => reports.push(report),
2969 Err(e) => {
2970 log::warn!("Failed to parse order status report: {e}");
2971 }
2972 }
2973 }
2974
2975 Ok(reports)
2976 }
2977
2978 #[expect(clippy::too_many_arguments)]
2984 pub async fn request_fill_reports(
2985 &self,
2986 account_id: AccountId,
2987 instrument_id: InstrumentId,
2988 venue_order_id: Option<VenueOrderId>,
2989 start: Option<i64>,
2990 end: Option<i64>,
2991 limit: Option<u32>,
2992 bnfcr_currency: Currency,
2993 ) -> anyhow::Result<Vec<FillReport>> {
2994 let (symbol, price_precision, size_precision) =
2995 self.cached_precisions_by_id(instrument_id)?;
2996
2997 let order_id = venue_order_id
2998 .map(|id| id.inner().parse::<i64>())
2999 .transpose()
3000 .map_err(|_| anyhow::anyhow!("Invalid venue order ID"))?;
3001
3002 let params = BinanceUserTradesParams {
3003 symbol,
3004 order_id,
3005 start_time: start,
3006 end_time: end,
3007 from_id: None,
3008 limit,
3009 recv_window: None,
3010 };
3011
3012 let trades = self.inner.query_user_trades(¶ms).await?;
3013
3014 let ts_init = self.clock.get_time_ns();
3015 let mut reports = Vec::with_capacity(trades.len());
3016
3017 for trade in trades {
3018 match trade.to_fill_report(
3019 account_id,
3020 instrument_id,
3021 price_precision,
3022 size_precision,
3023 bnfcr_currency,
3024 ts_init,
3025 ) {
3026 Ok(report) => reports.push(report),
3027 Err(e) => {
3028 log::warn!("Failed to parse fill report: {e}");
3029 }
3030 }
3031 }
3032
3033 Ok(reports)
3034 }
3035
3036 pub async fn request_trades(
3042 &self,
3043 instrument_id: InstrumentId,
3044 limit: Option<u32>,
3045 ) -> anyhow::Result<Vec<TradeTick>> {
3046 let (symbol, price_precision, size_precision) =
3047 self.cached_precisions_by_id(instrument_id)?;
3048
3049 let params = BinanceTradesParams { symbol, limit };
3050
3051 let trades = self.inner.trades(¶ms).await?;
3052 let ts_init = UnixNanos::default();
3053
3054 let mut result = Vec::with_capacity(trades.len());
3055 for trade in trades {
3056 let tick = parse_futures_trade_tick(
3057 &trade,
3058 instrument_id,
3059 price_precision,
3060 size_precision,
3061 ts_init,
3062 )?;
3063 result.push(tick);
3064 }
3065
3066 Ok(result)
3067 }
3068
3069 pub async fn request_agg_trades(
3075 &self,
3076 instrument_id: InstrumentId,
3077 start: Option<Timestamp>,
3078 end: Option<Timestamp>,
3079 limit: Option<u32>,
3080 ) -> anyhow::Result<Vec<TradeTick>> {
3081 let cutoff =
3082 self.clock.get_time_ns().to_datetime_utc() - jiff::SignedDuration::from_hours(24);
3083 anyhow::ensure!(
3084 start.as_ref().is_none_or(|value| value >= &cutoff)
3085 && end.as_ref().is_none_or(|value| value >= &cutoff),
3086 "Binance Futures aggregate trade history is limited to the past 24 hours"
3087 );
3088 let (symbol, price_precision, size_precision) =
3089 self.cached_precisions_by_id(instrument_id)?;
3090 let params = BinanceAggTradesParams {
3091 symbol,
3092 from_id: None,
3093 start_time: start.map(|value| value.as_millisecond()),
3094 end_time: end.map(|value| value.as_millisecond()),
3095 limit,
3096 };
3097 let trades = self.inner.agg_trades(¶ms).await?;
3098 trades
3099 .iter()
3100 .map(|trade| {
3101 let ts_init = parse_millis(trade.time, "Futures aggregate trade time")?;
3102 parse_futures_agg_trade_tick(
3103 trade,
3104 instrument_id,
3105 price_precision,
3106 size_precision,
3107 ts_init,
3108 )
3109 })
3110 .collect()
3111 }
3112
3113 pub async fn request_binance_bars(
3120 &self,
3121 bar_type: BarType,
3122 start: Option<Timestamp>,
3123 end: Option<Timestamp>,
3124 limit: Option<u32>,
3125 ) -> anyhow::Result<Vec<BinanceBar>> {
3126 anyhow::ensure!(
3127 bar_type.aggregation_source() == AggregationSource::External,
3128 "Only EXTERNAL aggregation is supported"
3129 );
3130
3131 let spec = bar_type.spec();
3132 let step = spec.step.get();
3133 let interval = match spec.aggregation {
3134 BarAggregation::Second => {
3135 anyhow::bail!("Binance Futures does not support second-level kline intervals")
3136 }
3137 BarAggregation::Minute => format!("{step}m"),
3138 BarAggregation::Hour => format!("{step}h"),
3139 BarAggregation::Day => format!("{step}d"),
3140 BarAggregation::Week => format!("{step}w"),
3141 BarAggregation::Month => format!("{step}M"),
3142 a => anyhow::bail!("Binance Futures does not support {a:?} aggregation"),
3143 };
3144
3145 let instrument_id = bar_type.instrument_id();
3146 let (symbol, price_precision, size_precision) =
3147 self.cached_precisions_by_id(instrument_id)?;
3148
3149 let params = BinanceKlinesParams {
3150 symbol,
3151 interval,
3152 start_time: start.map(|dt| dt.as_millisecond()),
3153 end_time: end.map(|dt| dt.as_millisecond()),
3154 limit,
3155 };
3156
3157 let klines = self.inner.klines(¶ms).await?;
3158 let now = self.clock.get_time_ns();
3159
3160 let mut result = Vec::with_capacity(klines.len());
3161 for kline in klines {
3162 let ts_init = parse_millis(kline.close_time, "Futures kline close time")?;
3163 let bar = parse_futures_kline_binance_bar(
3164 &kline,
3165 bar_type,
3166 price_precision,
3167 size_precision,
3168 ts_init,
3169 )?;
3170
3171 if bar.ts_event < now {
3172 result.push(bar);
3173 }
3174 }
3175
3176 Ok(result)
3177 }
3178
3179 pub async fn request_bars(
3185 &self,
3186 bar_type: BarType,
3187 start: Option<Timestamp>,
3188 end: Option<Timestamp>,
3189 limit: Option<u32>,
3190 ) -> anyhow::Result<Vec<Bar>> {
3191 Ok(self
3192 .request_binance_bars(bar_type, start, end, limit)
3193 .await?
3194 .into_iter()
3195 .map(|bar| bar.bar())
3196 .collect())
3197 }
3198
3199 pub async fn request_book_snapshot(
3205 &self,
3206 instrument_id: InstrumentId,
3207 depth: Option<u32>,
3208 ) -> anyhow::Result<OrderBook> {
3209 if depth.is_some_and(|value| !crate::common::consts::BINANCE_BOOK_DEPTHS.contains(&value)) {
3210 anyhow::bail!(
3211 "invalid Binance Futures order-book depth; valid values are {:?}",
3212 crate::common::consts::BINANCE_BOOK_DEPTHS
3213 );
3214 }
3215 let (symbol, price_precision, size_precision) =
3216 self.cached_precisions_by_id(instrument_id)?;
3217 let params = BinanceDepthParams {
3218 symbol,
3219 limit: depth,
3220 };
3221 let snapshot = self.inner.depth(¶ms).await?;
3222 let ts_event = self.clock.get_time_ns();
3223 let sequence = u64::try_from(snapshot.last_update_id)
3224 .map_err(|_| anyhow::anyhow!("invalid negative order-book update ID"))?;
3225 let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
3226 for (index, level) in snapshot.bids.iter().enumerate() {
3227 let order = BookOrder::new(
3228 OrderSide::Buy,
3229 parse_required_price_at_precision(&level.0, price_precision, "bid price")?,
3230 parse_required_quantity_at_precision(&level.1, size_precision, "bid quantity")?,
3231 index as u64,
3232 );
3233 book.add(order, 0, sequence, ts_event);
3234 }
3235 let bid_count = snapshot.bids.len();
3236 for (index, level) in snapshot.asks.iter().enumerate() {
3237 let order = BookOrder::new(
3238 OrderSide::Sell,
3239 parse_required_price_at_precision(&level.0, price_precision, "ask price")?,
3240 parse_required_quantity_at_precision(&level.1, size_precision, "ask quantity")?,
3241 (bid_count + index) as u64,
3242 );
3243 book.add(order, 0, sequence, ts_event);
3244 }
3245 Ok(book)
3246 }
3247
3248 fn cached_precisions_by_id(
3249 &self,
3250 instrument_id: InstrumentId,
3251 ) -> anyhow::Result<(String, u8, u8)> {
3252 let symbol = format_binance_symbol(&instrument_id);
3253 let instrument = self.instrument_metadata(instrument_id)?;
3254 let (price_precision, size_precision) = instrument.precisions()?;
3255
3256 Ok((symbol, price_precision, size_precision))
3257 }
3258
3259 pub(crate) fn instrument_metadata(
3260 &self,
3261 instrument_id: InstrumentId,
3262 ) -> anyhow::Result<BinanceFuturesInstrument> {
3263 let symbol = format_binance_symbol(&instrument_id);
3264 let instrument = self
3265 .instruments
3266 .get(&Ustr::from(symbol.as_str()))
3267 .map(|instrument| instrument.value().clone())
3268 .ok_or_else(|| InstrumentLookupError::not_found(instrument_id))?;
3269 if instrument.id() != instrument_id {
3270 return Err(InstrumentLookupError::not_found(instrument_id).into());
3271 }
3272
3273 Ok(instrument)
3274 }
3275
3276 pub async fn request_funding_rates(
3282 &self,
3283 instrument_id: InstrumentId,
3284 start: Option<Timestamp>,
3285 end: Option<Timestamp>,
3286 limit: Option<u32>,
3287 ) -> anyhow::Result<Vec<FundingRateUpdate>> {
3288 let params = BinanceFundingRateParams {
3289 symbol: Some(format_binance_symbol(&instrument_id)),
3290 start_time: start.map(|dt| dt.as_millisecond()),
3291 end_time: end.map(|dt| dt.as_millisecond()),
3292 limit,
3293 };
3294
3295 let rates = self.inner.funding_rate(¶ms).await?;
3296 let ts_init = UnixNanos::default();
3297
3298 let mut result = Vec::with_capacity(rates.len());
3299 for rate in rates {
3300 result.push(parse_futures_funding_rate_update(
3301 &rate,
3302 instrument_id,
3303 ts_init,
3304 )?);
3305 }
3306
3307 Ok(result)
3308 }
3309}
3310
3311fn parse_futures_trade_tick(
3312 trade: &BinanceFuturesTrade,
3313 instrument_id: InstrumentId,
3314 price_precision: u8,
3315 size_precision: u8,
3316 ts_init: UnixNanos,
3317) -> anyhow::Result<TradeTick> {
3318 let price = parse_required_price_at_precision(&trade.price, price_precision, "trade.price")
3319 .map_err(|e| anyhow::anyhow!("invalid Futures trade id {}: {e}", trade.id))?;
3320 let size = parse_required_quantity_at_precision(&trade.qty, size_precision, "trade.qty")
3321 .map_err(|e| anyhow::anyhow!("invalid Futures trade id {}: {e}", trade.id))?;
3322 let ts_event = parse_millis(trade.time, "Futures trade time")?;
3323
3324 let aggressor_side = if trade.is_buyer_maker {
3325 AggressorSide::Sell
3326 } else {
3327 AggressorSide::Buy
3328 };
3329
3330 Ok(TradeTick::new(
3331 instrument_id,
3332 price,
3333 size,
3334 aggressor_side,
3335 TradeId::new(trade.id.to_string()),
3336 ts_event,
3337 ts_init,
3338 ))
3339}
3340
3341fn parse_futures_agg_trade_tick(
3342 trade: &BinanceFuturesAggTrade,
3343 instrument_id: InstrumentId,
3344 price_precision: u8,
3345 size_precision: u8,
3346 ts_init: UnixNanos,
3347) -> anyhow::Result<TradeTick> {
3348 let trade = BinanceFuturesTrade {
3349 id: trade.id,
3350 price: trade.price.clone(),
3351 qty: trade.qty.clone(),
3352 quote_qty: String::new(),
3353 time: trade.time,
3354 is_buyer_maker: trade.is_buyer_maker,
3355 };
3356 parse_futures_trade_tick(
3357 &trade,
3358 instrument_id,
3359 price_precision,
3360 size_precision,
3361 ts_init,
3362 )
3363}
3364
3365fn parse_futures_kline_binance_bar(
3366 kline: &BinanceFuturesKline,
3367 bar_type: BarType,
3368 price_precision: u8,
3369 size_precision: u8,
3370 ts_init: UnixNanos,
3371) -> anyhow::Result<BinanceBar> {
3372 let open = parse_required_price_at_precision(&kline.open, price_precision, "kline.open")
3373 .map_err(|e| anyhow::anyhow!("invalid Futures kline {}: {e}", kline.open_time))?;
3374 let high = parse_required_price_at_precision(&kline.high, price_precision, "kline.high")
3375 .map_err(|e| anyhow::anyhow!("invalid Futures kline {}: {e}", kline.open_time))?;
3376 let low = parse_required_price_at_precision(&kline.low, price_precision, "kline.low")
3377 .map_err(|e| anyhow::anyhow!("invalid Futures kline {}: {e}", kline.open_time))?;
3378 let close = parse_required_price_at_precision(&kline.close, price_precision, "kline.close")
3379 .map_err(|e| anyhow::anyhow!("invalid Futures kline {}: {e}", kline.open_time))?;
3380 let volume =
3381 parse_required_quantity_at_precision(&kline.volume, size_precision, "kline.volume")
3382 .map_err(|e| anyhow::anyhow!("invalid Futures kline {}: {e}", kline.open_time))?;
3383 let ts_event = parse_millis(kline.close_time, "Futures kline close time")?;
3384
3385 let quote_volume = kline.quote_volume.parse::<Decimal>().map_err(|e| {
3386 anyhow::anyhow!(
3387 "invalid Futures kline {} quote volume: {e}",
3388 kline.open_time
3389 )
3390 })?;
3391 let taker_buy_base_volume = kline
3392 .taker_buy_base_volume
3393 .parse::<Decimal>()
3394 .map_err(|e| {
3395 anyhow::anyhow!(
3396 "invalid Futures kline {} taker buy base volume: {e}",
3397 kline.open_time
3398 )
3399 })?;
3400 let taker_buy_quote_volume = kline
3401 .taker_buy_quote_volume
3402 .parse::<Decimal>()
3403 .map_err(|e| {
3404 anyhow::anyhow!(
3405 "invalid Futures kline {} taker buy quote volume: {e}",
3406 kline.open_time
3407 )
3408 })?;
3409 let count = u64::try_from(kline.num_trades).map_err(|_| {
3410 anyhow::anyhow!(
3411 "invalid Futures kline {} negative trade count",
3412 kline.open_time
3413 )
3414 })?;
3415
3416 Ok(BinanceBar::new(
3417 bar_type,
3418 open,
3419 high,
3420 low,
3421 close,
3422 volume,
3423 quote_volume,
3424 count,
3425 taker_buy_base_volume,
3426 taker_buy_quote_volume,
3427 ts_event,
3428 ts_init,
3429 ))
3430}
3431
3432fn parse_futures_funding_rate_update(
3433 rate: &BinanceFundingRate,
3434 instrument_id: InstrumentId,
3435 ts_init: UnixNanos,
3436) -> anyhow::Result<FundingRateUpdate> {
3437 let funding_rate = rate.funding_rate.parse::<Decimal>().map_err(|e| {
3438 anyhow::anyhow!("invalid Futures funding rate at {}: {e}", rate.funding_time)
3439 })?;
3440 let ts_event = parse_millis(rate.funding_time, "Futures funding time")?;
3441
3442 Ok(FundingRateUpdate::new(
3443 instrument_id,
3444 funding_rate,
3445 None, None, ts_event,
3448 ts_init,
3449 ))
3450}
3451
3452fn parse_futures_commission_rates(
3453 response: &BinanceFuturesCommissionRate,
3454) -> anyhow::Result<(Decimal, Decimal)> {
3455 Ok((
3456 response.maker_commission_rate.parse()?,
3457 response.taker_commission_rate.parse()?,
3458 ))
3459}
3460
3461fn validate_reconciliation_instrument(
3462 instruments: &mut AHashMap<InstrumentId, InstrumentAny>,
3463 expected_id: InstrumentId,
3464 instrument: &InstrumentAny,
3465) -> BinanceFuturesHttpResult<()> {
3466 let parsed_id = instrument.id();
3467 if parsed_id != expected_id {
3468 return Err(BinanceFuturesHttpError::ValidationError(format!(
3469 "Parsed Binance Futures instrument ID {parsed_id} does not match expected ID {expected_id}"
3470 )));
3471 }
3472
3473 if instruments.insert(parsed_id, instrument.clone()).is_some() {
3474 return Err(BinanceFuturesHttpError::ValidationError(format!(
3475 "Duplicate parsed Binance Futures instrument ID {parsed_id}"
3476 )));
3477 }
3478
3479 Ok(())
3480}
3481
3482fn log_futures_instrument_parse_error(
3483 config: &BinanceInstrumentProviderConfig,
3484 selector: &BinanceInstrumentSelector,
3485 instrument_id: InstrumentId,
3486 symbol: &str,
3487 error: &anyhow::Error,
3488) {
3489 let explicit = selector.is_explicit(instrument_id, symbol);
3490 if should_warn_on_instrument_parse_error(config.log_warnings, explicit, error) {
3491 log::warn!("Skipping Binance Futures instrument {symbol}: {error}");
3492 } else {
3493 log::debug!("Skipping Binance Futures instrument {symbol}: {error}");
3494 }
3495}
3496
3497fn log_futures_commission_fallback(
3498 config: &BinanceInstrumentProviderConfig,
3499 symbol: &str,
3500 error: &dyn std::fmt::Display,
3501 fallback: (Decimal, Decimal),
3502) {
3503 if config.log_warnings {
3504 log::warn!(
3505 "Unable to query Binance Futures commission for {symbol}; using maker={} taker={}: {error}",
3506 fallback.0,
3507 fallback.1,
3508 );
3509 } else {
3510 log::debug!(
3511 "Unable to query Binance Futures commission for {symbol}; using maker={} taker={}: {error}",
3512 fallback.0,
3513 fallback.1,
3514 );
3515 }
3516}
3517
3518#[must_use]
3523pub fn is_algo_order_type(order_type: OrderType) -> bool {
3524 matches!(
3525 order_type,
3526 OrderType::StopMarket
3527 | OrderType::StopLimit
3528 | OrderType::MarketIfTouched
3529 | OrderType::LimitIfTouched
3530 | OrderType::TrailingStopMarket
3531 )
3532}
3533
3534pub(crate) fn order_type_to_binance_futures(
3536 order_type: OrderType,
3537) -> anyhow::Result<BinanceFuturesOrderType> {
3538 match order_type {
3539 OrderType::Market => Ok(BinanceFuturesOrderType::Market),
3540 OrderType::Limit => Ok(BinanceFuturesOrderType::Limit),
3541 OrderType::StopMarket => Ok(BinanceFuturesOrderType::StopMarket),
3542 OrderType::StopLimit => Ok(BinanceFuturesOrderType::Stop),
3543 OrderType::MarketIfTouched => Ok(BinanceFuturesOrderType::TakeProfitMarket),
3544 OrderType::LimitIfTouched => Ok(BinanceFuturesOrderType::TakeProfit),
3545 OrderType::TrailingStopMarket => Ok(BinanceFuturesOrderType::TrailingStopMarket),
3546 _ => anyhow::bail!("Unsupported order type for Binance Futures: {order_type:?}"),
3547 }
3548}
3549
3550#[cfg(test)]
3551mod tests {
3552 use nautilus_core::time::get_atomic_clock_realtime;
3553 use nautilus_network::http::{HttpStatus, StatusCode};
3554 use nautilus_testkit::http::assert_http_redirect_rejected;
3555 use rstest::rstest;
3556 use tokio_util::bytes::Bytes;
3557
3558 use super::*;
3559 use crate::common::enums::BinanceTradingStatus;
3560
3561 #[tokio::test]
3562 async fn test_authenticated_client_rejects_redirects() {
3563 let client = BinanceRawFuturesHttpClient::new(
3564 BinanceProductType::UsdM,
3565 BinanceEnvironment::Testnet,
3566 Some("key".into()),
3567 Some("secret".into()),
3568 None,
3569 None,
3570 Some(3),
3571 None,
3572 )
3573 .unwrap()
3574 .client;
3575 assert_http_redirect_rejected(|url| async move {
3576 client
3577 .get(url, None, None, Some(3), None)
3578 .await
3579 .unwrap()
3580 .status
3581 .as_u16()
3582 })
3583 .await;
3584 }
3585
3586 #[rstest]
3587 fn test_rate_limit_config_usdm_has_request_weight_and_orders() {
3588 let config = BinanceRawFuturesHttpClient::rate_limit_config(BinanceProductType::UsdM);
3589
3590 assert_eq!(config.request_quota.burst_size().get(), 2_400);
3591 assert_eq!(config.order_keys.len(), 2);
3592 assert!(
3593 config
3594 .order_keys
3595 .iter()
3596 .any(|key| key == "binance:orders:10:Second")
3597 );
3598 assert!(
3599 config
3600 .order_keys
3601 .iter()
3602 .any(|key| key == "binance:orders:1:Minute")
3603 );
3604
3605 let ten_second_quota = config
3606 .order_quotas
3607 .iter()
3608 .find(|(key, _)| key == "binance:orders:10:Second")
3609 .map(|(_, quota)| quota)
3610 .expect("USD-M 10-second order quota");
3611 assert_eq!(ten_second_quota.burst_size().get(), 300);
3612 assert_eq!(
3613 ten_second_quota.replenish_interval(),
3614 Duration::from_nanos(33_333_333)
3615 );
3616 }
3617
3618 #[rstest]
3619 fn test_rate_limit_config_coinm_has_request_weight_and_orders() {
3620 let config = BinanceRawFuturesHttpClient::rate_limit_config(BinanceProductType::CoinM);
3621
3622 assert_eq!(config.request_quota.burst_size().get(), 2_400);
3623 assert_eq!(config.order_keys.len(), 2);
3624 assert!(
3625 config
3626 .order_keys
3627 .iter()
3628 .any(|key| key == "binance:orders:10:Second")
3629 );
3630 assert!(
3631 config
3632 .order_keys
3633 .iter()
3634 .any(|key| key == "binance:orders:1:Minute")
3635 );
3636
3637 let ten_second_quota = config
3638 .order_quotas
3639 .iter()
3640 .find(|(key, _)| key == "binance:orders:10:Second")
3641 .map(|(_, quota)| quota)
3642 .expect("COIN-M 10-second order quota");
3643 let minute_quota = config
3644 .order_quotas
3645 .iter()
3646 .find(|(key, _)| key == "binance:orders:1:Minute")
3647 .map(|(_, quota)| quota)
3648 .expect("COIN-M one-minute order quota");
3649
3650 assert_eq!(ten_second_quota.burst_size().get(), 300);
3651 assert_eq!(minute_quota.burst_size().get(), 1_200);
3652 }
3653
3654 #[rstest]
3655 fn test_rate_limiters_share_usdm_and_coinm_scopes() {
3656 let account = Some(BinanceFuturesAccountScope([1; 32]));
3657 let usdm_config = BinanceRawFuturesHttpClient::rate_limit_config(BinanceProductType::UsdM);
3658 let coinm_config =
3659 BinanceRawFuturesHttpClient::rate_limit_config(BinanceProductType::CoinM);
3660
3661 let usdm = BinanceRawFuturesHttpClient::shared_rate_limiters(
3662 BinanceEnvironment::Live,
3663 None,
3664 None,
3665 account,
3666 usdm_config.request_quota,
3667 usdm_config.order_quotas,
3668 );
3669 let coinm = BinanceRawFuturesHttpClient::shared_rate_limiters(
3670 BinanceEnvironment::Live,
3671 Some(get_http_base_url(
3672 BinanceProductType::CoinM,
3673 BinanceEnvironment::Live,
3674 )),
3675 None,
3676 account,
3677 coinm_config.request_quota,
3678 coinm_config.order_quotas,
3679 );
3680 let public = create_test_rate_limiters(BinanceEnvironment::Live, None, None, None);
3681
3682 assert_eq!(usdm.len(), 2);
3683 assert_eq!(coinm.len(), 2);
3684 assert_eq!(public.len(), 1);
3685 assert!(Arc::ptr_eq(&usdm[0], &coinm[0]));
3686 assert!(Arc::ptr_eq(&usdm[0], &public[0]));
3687 assert!(Arc::ptr_eq(&usdm[1], &coinm[1]));
3688 }
3689
3690 #[rstest]
3691 fn test_rate_limiters_isolate_unrelated_scopes() {
3692 let account_a = Some(BinanceFuturesAccountScope([2; 32]));
3693 let account_b = Some(BinanceFuturesAccountScope([3; 32]));
3694
3695 let live_direct =
3696 create_test_rate_limiters(BinanceEnvironment::Live, None, None, account_a);
3697 let testnet_direct =
3698 create_test_rate_limiters(BinanceEnvironment::Testnet, None, None, account_a);
3699 let demo_direct =
3700 create_test_rate_limiters(BinanceEnvironment::Demo, None, None, account_a);
3701 let custom_a = create_test_rate_limiters(
3702 BinanceEnvironment::Live,
3703 Some("http://127.0.0.1:41001"),
3704 None,
3705 account_a,
3706 );
3707 let custom_b = create_test_rate_limiters(
3708 BinanceEnvironment::Live,
3709 Some("http://127.0.0.1:41002"),
3710 None,
3711 account_a,
3712 );
3713 let proxy_a = create_test_rate_limiters(
3714 BinanceEnvironment::Live,
3715 None,
3716 Some("http://127.0.0.1:42001"),
3717 account_a,
3718 );
3719 let proxy_b = create_test_rate_limiters(
3720 BinanceEnvironment::Live,
3721 None,
3722 Some("http://127.0.0.1:42002"),
3723 account_a,
3724 );
3725 let other_account =
3726 create_test_rate_limiters(BinanceEnvironment::Live, None, None, account_b);
3727
3728 assert!(!Arc::ptr_eq(&live_direct[0], &testnet_direct[0]));
3729 assert!(!Arc::ptr_eq(&live_direct[0], &demo_direct[0]));
3730 assert!(!Arc::ptr_eq(&live_direct[0], &custom_a[0]));
3731 assert!(!Arc::ptr_eq(&custom_a[0], &custom_b[0]));
3732 assert!(!Arc::ptr_eq(&proxy_a[0], &proxy_b[0]));
3733 assert!(Arc::ptr_eq(&proxy_a[1], &proxy_b[1]));
3734 assert!(Arc::ptr_eq(&live_direct[0], &other_account[0]));
3735 assert!(!Arc::ptr_eq(&live_direct[1], &other_account[1]));
3736 }
3737
3738 fn create_test_rate_limiters(
3739 environment: BinanceEnvironment,
3740 base_url_override: Option<&str>,
3741 proxy_url: Option<&str>,
3742 account_scope: Option<BinanceFuturesAccountScope>,
3743 ) -> Vec<BinanceFuturesRateLimiter> {
3744 let config = BinanceRawFuturesHttpClient::rate_limit_config(BinanceProductType::UsdM);
3745 BinanceRawFuturesHttpClient::shared_rate_limiters(
3746 environment,
3747 base_url_override,
3748 proxy_url,
3749 account_scope,
3750 config.request_quota,
3751 config.order_quotas,
3752 )
3753 }
3754
3755 #[rstest]
3756 fn test_rate_limit_keys_usdm_include_order_buckets() {
3757 let client = BinanceRawFuturesHttpClient::new(
3758 BinanceProductType::UsdM,
3759 BinanceEnvironment::Live,
3760 None,
3761 None,
3762 None,
3763 None,
3764 None,
3765 None,
3766 )
3767 .unwrap();
3768
3769 assert_eq!(
3770 client.rate_limit_keys(false),
3771 vec![BINANCE_GLOBAL_RATE_KEY.to_string()]
3772 );
3773 assert_eq!(
3774 client.rate_limit_keys(true),
3775 vec![
3776 BINANCE_GLOBAL_RATE_KEY.to_string(),
3777 "binance:orders:10:Second".to_string(),
3778 "binance:orders:1:Minute".to_string(),
3779 ]
3780 );
3781 }
3782
3783 #[rstest]
3784 fn test_quota_from_unknown_interval_returns_none() {
3785 let quota = BinanceRateLimitQuota {
3786 rate_limit_type: BinanceRateLimitType::Orders,
3787 interval: BinanceRateLimitInterval::Unknown,
3788 interval_num: 1,
3789 limit: 10,
3790 };
3791
3792 assert!(BinanceRawFuturesHttpClient::quota_from("a).is_none());
3793 }
3794
3795 #[rstest]
3796 fn test_create_client_rejects_spot_product_type() {
3797 let result = BinanceFuturesHttpClient::new(
3798 BinanceProductType::Spot,
3799 BinanceEnvironment::Live,
3800 get_atomic_clock_realtime(),
3801 None,
3802 None,
3803 None,
3804 None,
3805 None,
3806 None,
3807 false,
3808 );
3809
3810 result.unwrap_err();
3811 }
3812
3813 #[rstest]
3814 fn test_parse_futures_trade_tick_rejects_invalid_price() {
3815 let trade = BinanceFuturesTrade {
3816 id: 100,
3817 price: "not-a-number".to_string(),
3818 qty: "0.001".to_string(),
3819 quote_qty: "50.00".to_string(),
3820 time: 1_625_474_304_000,
3821 is_buyer_maker: false,
3822 };
3823
3824 let result = parse_futures_trade_tick(
3825 &trade,
3826 InstrumentId::from("BTCUSDT-PERP.BINANCE"),
3827 2,
3828 3,
3829 UnixNanos::from(1_000_000_000u64),
3830 );
3831
3832 let error = result.unwrap_err().to_string();
3833 assert!(error.contains("trade.price"));
3834 assert!(error.contains("100"));
3835 }
3836
3837 #[rstest]
3838 fn test_parse_futures_kline_bar_rejects_invalid_volume() {
3839 let kline = BinanceFuturesKline {
3840 open_time: 1_625_474_304_000,
3841 open: "50000.00".to_string(),
3842 high: "51000.00".to_string(),
3843 low: "49000.00".to_string(),
3844 close: "50500.00".to_string(),
3845 volume: "not-a-number".to_string(),
3846 close_time: 1_625_474_364_000,
3847 quote_volume: "631250.00".to_string(),
3848 num_trades: 100,
3849 taker_buy_base_volume: "6.2".to_string(),
3850 taker_buy_quote_volume: "313100.00".to_string(),
3851 };
3852
3853 let result = parse_futures_kline_binance_bar(
3854 &kline,
3855 BarType::from("BTCUSDT-PERP.BINANCE-1-MINUTE-LAST-EXTERNAL"),
3856 2,
3857 3,
3858 UnixNanos::from(1_000_000_000u64),
3859 );
3860
3861 let error = result.unwrap_err().to_string();
3862 assert!(error.contains("kline.volume"));
3863 assert!(error.contains("1625474304000"));
3864 }
3865
3866 #[rstest]
3867 #[case::limit(
3868 BinanceAggTradesParams {
3869 symbol: "BTCUSDT".to_string(),
3870 from_id: None,
3871 start_time: None,
3872 end_time: None,
3873 limit: Some(1001),
3874 },
3875 "Validation error: aggregate trade limit must not exceed 1000"
3876 )]
3877 #[case::order(
3878 BinanceAggTradesParams {
3879 symbol: "BTCUSDT".to_string(),
3880 from_id: None,
3881 start_time: Some(2000),
3882 end_time: Some(1000),
3883 limit: Some(1000),
3884 },
3885 "Validation error: aggregate trade startTime must not exceed endTime"
3886 )]
3887 #[case::range(
3888 BinanceAggTradesParams {
3889 symbol: "BTCUSDT".to_string(),
3890 from_id: None,
3891 start_time: Some(1000),
3892 end_time: Some(3_601_000),
3893 limit: Some(1000),
3894 },
3895 "Validation error: aggregate trade time range must be less than one hour"
3896 )]
3897 #[case::overflow(
3898 BinanceAggTradesParams {
3899 symbol: "BTCUSDT".to_string(),
3900 from_id: None,
3901 start_time: Some(i64::MIN),
3902 end_time: Some(i64::MAX),
3903 limit: Some(1000),
3904 },
3905 "Validation error: aggregate trade time range must be less than one hour"
3906 )]
3907 #[tokio::test]
3908 async fn test_agg_trades_rejects_invalid_bounds(
3909 #[case] params: BinanceAggTradesParams,
3910 #[case] expected: &str,
3911 ) {
3912 let error = create_test_raw_client()
3913 .agg_trades(¶ms)
3914 .await
3915 .unwrap_err();
3916
3917 assert_eq!(error.to_string(), expected);
3918 }
3919
3920 #[rstest]
3921 #[case::start(true, false)]
3922 #[case::end(false, true)]
3923 #[case::both(true, true)]
3924 #[tokio::test]
3925 async fn test_request_agg_trades_rejects_history_older_than_24_hours(
3926 #[case] include_start: bool,
3927 #[case] include_end: bool,
3928 ) {
3929 let client = create_test_client();
3930 let start = Timestamp::now() - jiff::SignedDuration::from_hours(25);
3931 let end = start + jiff::SignedDuration::from_mins(30);
3932
3933 let error = client
3934 .request_agg_trades(
3935 InstrumentId::from("BTCUSDT-PERP.BINANCE"),
3936 include_start.then_some(start),
3937 include_end.then_some(end),
3938 Some(1000),
3939 )
3940 .await
3941 .unwrap_err();
3942
3943 assert_eq!(
3944 error.to_string(),
3945 "Binance Futures aggregate trade history is limited to the past 24 hours"
3946 );
3947 }
3948
3949 fn create_test_raw_client() -> BinanceRawFuturesHttpClient {
3950 BinanceRawFuturesHttpClient::new(
3951 BinanceProductType::UsdM,
3952 BinanceEnvironment::Live,
3953 None,
3954 None,
3955 None,
3956 None,
3957 None,
3958 None,
3959 )
3960 .expect("Failed to create test client")
3961 }
3962
3963 fn create_test_client() -> BinanceFuturesHttpClient {
3964 BinanceFuturesHttpClient::new(
3965 BinanceProductType::UsdM,
3966 BinanceEnvironment::Live,
3967 get_atomic_clock_realtime(),
3968 None,
3969 None,
3970 Some("http://127.0.0.1:1".to_string()),
3971 None,
3972 Some(1),
3973 None,
3974 false,
3975 )
3976 .expect("Failed to create test client")
3977 }
3978
3979 fn test_usdm_symbol() -> BinanceFuturesUsdSymbol {
3980 BinanceFuturesUsdSymbol {
3981 symbol: Ustr::from("BTCUSDT"),
3982 pair: Ustr::from("BTCUSDT"),
3983 contract_type: "PERPETUAL".to_string(),
3984 delivery_date: 4_133_404_800_000,
3985 onboard_date: 1_569_398_400_000,
3986 status: BinanceTradingStatus::Trading,
3987 maint_margin_percent: "2.5000".to_string(),
3988 required_margin_percent: "5.0000".to_string(),
3989 base_asset: Ustr::from("BTC"),
3990 quote_asset: Ustr::from("USDT"),
3991 margin_asset: Ustr::from("USDT"),
3992 price_precision: 2,
3993 quantity_precision: 3,
3994 base_asset_precision: 8,
3995 quote_precision: 8,
3996 underlying_type: None,
3997 underlying_sub_type: Vec::new(),
3998 settle_plan: None,
3999 trigger_protect: None,
4000 liquidation_fee: None,
4001 market_take_bound: None,
4002 order_types: Vec::new(),
4003 time_in_force: Vec::new(),
4004 filters: Vec::new(),
4005 }
4006 }
4007
4008 #[rstest]
4009 fn test_cached_precisions_by_id_returns_symbol_and_precisions() {
4010 let client = create_test_client();
4011 client.instruments_cache().insert(
4012 Ustr::from("BTCUSDT"),
4013 BinanceFuturesInstrument::UsdM(test_usdm_symbol()),
4014 );
4015
4016 let (symbol, price_precision, size_precision) = client
4017 .cached_precisions_by_id(InstrumentId::from("BTCUSDT-PERP.BINANCE"))
4018 .unwrap();
4019
4020 assert_eq!(symbol, "BTCUSDT");
4021 assert_eq!(price_precision, 2);
4022 assert_eq!(size_precision, 3);
4023 }
4024
4025 #[rstest]
4026 fn test_cached_precisions_by_id_rejects_spot_alias() {
4027 let client = create_test_client();
4028 client.instruments_cache().insert(
4029 Ustr::from("BTCUSDT"),
4030 BinanceFuturesInstrument::UsdM(test_usdm_symbol()),
4031 );
4032
4033 let error = client
4034 .cached_precisions_by_id(InstrumentId::from("BTCUSDT.BINANCE"))
4035 .unwrap_err();
4036
4037 assert_eq!(
4038 error.to_string(),
4039 InstrumentLookupError::not_found(InstrumentId::from("BTCUSDT.BINANCE")).to_string()
4040 );
4041 }
4042
4043 #[rstest]
4044 fn test_invalid_precision_preserves_previous_raw_snapshot() {
4045 let client = create_test_client();
4046 let valid = test_usdm_symbol();
4047 client
4048 .replace_instruments(vec![(
4049 valid.symbol,
4050 BinanceFuturesInstrument::UsdM(valid.clone()),
4051 )])
4052 .unwrap();
4053 let mut invalid = valid;
4054 invalid.price_precision = i32::from(FIXED_PRECISION) + 1;
4055
4056 let error = client
4057 .replace_instruments(vec![(
4058 invalid.symbol,
4059 BinanceFuturesInstrument::UsdM(invalid),
4060 )])
4061 .unwrap_err();
4062 let retained = client
4063 .instruments_cache()
4064 .get(&Ustr::from("BTCUSDT"))
4065 .map(|instrument| instrument.value().clone())
4066 .unwrap();
4067
4068 assert!(error.to_string().contains("precision exceeds maximum"));
4069 assert_eq!(retained.precisions().unwrap(), (2, 3));
4070 }
4071
4072 #[rstest]
4073 #[tokio::test]
4074 async fn test_submit_algo_order_stop_market_requires_trigger_price() {
4075 let client = create_test_client();
4076 client.instruments_cache().insert(
4077 Ustr::from("BTCUSDT"),
4078 BinanceFuturesInstrument::UsdM(test_usdm_symbol()),
4079 );
4080
4081 let result = client
4082 .submit_algo_order(
4083 AccountId::from("BINANCE-001"),
4084 InstrumentId::from("BTCUSDT-PERP.BINANCE"),
4085 ClientOrderId::new("missing-trigger-test-001"),
4086 OrderSide::Sell,
4087 OrderType::StopMarket,
4088 Quantity::from("0.001"),
4089 TimeInForce::Gtc,
4090 None,
4091 None,
4092 false,
4093 false,
4094 None,
4095 None,
4096 None,
4097 None,
4098 None,
4099 )
4100 .await;
4101
4102 let error = result.unwrap_err().to_string();
4103 assert_eq!(error, "Algo order type StopMarket requires a trigger price");
4104 }
4105
4106 #[rstest]
4107 fn test_batch_cancel_params_builds_order_id_list() {
4108 let items = vec![
4109 BatchCancelItem::by_order_id("BTCUSDT", 123),
4110 BatchCancelItem::by_order_id("BTCUSDT", 456),
4111 ];
4112
4113 let params = BinanceRawFuturesHttpClient::batch_cancel_params(&items).unwrap();
4114
4115 assert_eq!(params.symbol, "BTCUSDT");
4116 assert_eq!(params.order_id_list.as_deref(), Some("[123,456]"));
4117 assert_eq!(params.orig_client_order_id_list, None);
4118 }
4119
4120 #[rstest]
4121 fn test_batch_cancel_params_builds_client_order_id_list() {
4122 let items = vec![
4123 BatchCancelItem::by_client_order_id("BTCUSDT", "first-order"),
4124 BatchCancelItem::by_client_order_id("BTCUSDT", "second-order"),
4125 ];
4126
4127 let params = BinanceRawFuturesHttpClient::batch_cancel_params(&items).unwrap();
4128
4129 assert_eq!(params.symbol, "BTCUSDT");
4130 assert_eq!(params.order_id_list, None);
4131 assert_eq!(
4132 params.orig_client_order_id_list.as_deref(),
4133 Some("[\"first-order\",\"second-order\"]"),
4134 );
4135 }
4136
4137 #[rstest]
4138 fn test_batch_cancel_params_rejects_mixed_symbols() {
4139 let items = vec![
4140 BatchCancelItem::by_order_id("BTCUSDT", 123),
4141 BatchCancelItem::by_order_id("ETHUSDT", 456),
4142 ];
4143
4144 let result = BinanceRawFuturesHttpClient::batch_cancel_params(&items);
4145
4146 assert_validation_error(result, "same symbol");
4147 }
4148
4149 #[rstest]
4150 fn test_batch_cancel_params_rejects_mixed_id_types() {
4151 let items = vec![
4152 BatchCancelItem::by_order_id("BTCUSDT", 123),
4153 BatchCancelItem::by_client_order_id("BTCUSDT", "client-order"),
4154 ];
4155
4156 let result = BinanceRawFuturesHttpClient::batch_cancel_params(&items);
4157
4158 assert_validation_error(result, "not both");
4159 }
4160
4161 #[rstest]
4162 fn test_batch_cancel_params_rejects_items_without_ids() {
4163 let items = vec![BatchCancelItem {
4164 symbol: "BTCUSDT".to_string(),
4165 order_id: None,
4166 orig_client_order_id: None,
4167 }];
4168
4169 let result = BinanceRawFuturesHttpClient::batch_cancel_params(&items);
4170
4171 assert_validation_error(result, "at least one order ID or client order ID");
4172 }
4173
4174 #[rstest]
4175 #[tokio::test]
4176 async fn test_batch_cancel_orders_rejects_more_than_ten_items() {
4177 let client = create_test_raw_client();
4178 let items = (0..11)
4179 .map(|order_id| BatchCancelItem::by_order_id("BTCUSDT", order_id))
4180 .collect::<Vec<_>>();
4181
4182 let result = client.batch_cancel_orders(&items).await;
4183
4184 match result {
4185 Err(BinanceFuturesHttpError::ValidationError(message)) => {
4186 assert!(message.contains("10 orders maximum"));
4187 }
4188 other => panic!("Expected ValidationError, was {other:?}"),
4189 }
4190 }
4191
4192 fn assert_validation_error(
4193 result: BinanceFuturesHttpResult<BatchCancelParams>,
4194 expected_message: &str,
4195 ) {
4196 match result {
4197 Err(BinanceFuturesHttpError::ValidationError(message)) => {
4198 assert!(message.contains(expected_message));
4199 }
4200 other => panic!("Expected ValidationError, was {other:?}"),
4201 }
4202 }
4203
4204 #[rstest]
4205 fn test_parse_error_response_binance_error() {
4206 let client = create_test_raw_client();
4207 let response = HttpResponse {
4208 status: HttpStatus::new(StatusCode::BAD_REQUEST),
4209 headers: HashMap::new(),
4210 body: Bytes::from(r#"{"code":-1121,"msg":"Invalid symbol."}"#),
4211 };
4212
4213 let result: BinanceFuturesHttpResult<()> = client.parse_error_response(&response);
4214
4215 match result {
4216 Err(BinanceFuturesHttpError::BinanceError {
4217 code,
4218 message,
4219 status,
4220 retry_after,
4221 }) => {
4222 assert_eq!(code, -1121);
4223 assert_eq!(message, "Invalid symbol.");
4224 assert_eq!(status, 400);
4225 assert_eq!(retry_after, None);
4226 }
4227 other => panic!("Expected BinanceError, was {other:?}"),
4228 }
4229 }
4230}