1use std::{collections::HashMap, num::NonZeroU32, sync::Arc, time::Duration};
19
20use ahash::AHashMap;
21use chrono::{DateTime, Utc};
22use dashmap::DashMap;
23use nautilus_common::cache::InstrumentLookupError;
24use nautilus_core::{
25 consts::NAUTILUS_USER_AGENT, datetime::SECONDS_IN_DAY, nanos::UnixNanos, time::AtomicTime,
26};
27use nautilus_model::{
28 data::{Bar, BarType, FundingRateUpdate, TradeTick},
29 enums::{
30 AggregationSource, AggressorSide, BarAggregation, MarketStatusAction, OrderSide, OrderType,
31 TimeInForce,
32 },
33 events::AccountState,
34 identifiers::{AccountId, ClientOrderId, InstrumentId, TradeId, VenueOrderId},
35 instruments::any::InstrumentAny,
36 reports::{FillReport, OrderStatusReport},
37 types::{Currency, Price, Quantity},
38};
39use nautilus_network::{
40 http::{HttpClient, HttpResponse, Method, USER_AGENT},
41 ratelimiter::quota::Quota,
42};
43use rust_decimal::Decimal;
44use serde::{Deserialize, Serialize, de::DeserializeOwned};
45use ustr::Ustr;
46
47use super::{
48 error::{BinanceFuturesHttpError, BinanceFuturesHttpResult},
49 models::{
50 BatchOrderResult, BinanceBookTicker, BinanceCancelAllOrdersResponse, BinanceFundingRate,
51 BinanceFuturesAccountInfo, BinanceFuturesAlgoOrder, BinanceFuturesAlgoOrderCancelResponse,
52 BinanceFuturesCoinExchangeInfo, BinanceFuturesCoinSymbol, BinanceFuturesKline,
53 BinanceFuturesMarkPrice, BinanceFuturesOrder, BinanceFuturesTicker24hr,
54 BinanceFuturesTrade, BinanceFuturesUsdExchangeInfo, BinanceFuturesUsdSymbol,
55 BinanceHedgeModeResponse, BinanceLeverageResponse, BinanceOpenInterest,
56 BinanceOpenInterestHistRecord, BinanceOrderBook, BinancePositionRisk, BinancePriceTicker,
57 BinanceServerTime, BinanceUserTrade, ListenKeyResponse,
58 },
59 query::{
60 BatchCancelItem, BatchModifyItem, BatchOrderItem, BinanceAlgoOrderQueryParams,
61 BinanceAllAlgoOrdersParams, BinanceAllOrdersParams, BinanceBookTickerParams,
62 BinanceCancelAllAlgoOrdersParams, BinanceCancelAllOrdersParams, BinanceCancelOrderParams,
63 BinanceDepthParams, BinanceFundingRateParams, BinanceKlinesParams, BinanceMarkPriceParams,
64 BinanceModifyOrderParams, BinanceNewAlgoOrderParams, BinanceNewOrderParams,
65 BinanceOpenAlgoOrdersParams, BinanceOpenInterestHistParams, BinanceOpenInterestParams,
66 BinanceOpenOrdersParams, BinanceOrderQueryParams, BinancePositionRiskParams,
67 BinanceSetLeverageParams, BinanceSetMarginTypeParams, BinanceTicker24hrParams,
68 BinanceTradesParams, BinanceUserTradesParams, ListenKeyParams,
69 },
70};
71use crate::{
72 common::{
73 consts::{
74 BINANCE_API_KEY_HEADER, BINANCE_DAPI_PATH, BINANCE_DAPI_RATE_LIMITS, BINANCE_FAPI_PATH,
75 BINANCE_FAPI_RATE_LIMITS, BINANCE_NAUTILUS_FUTURES_BROKER_ID, BinanceRateLimitQuota,
76 },
77 credential::SigningCredential,
78 encoder::encode_broker_id,
79 enums::{
80 BinanceAlgoType, BinanceEnvironment, BinanceFuturesOrderType, BinancePositionSide,
81 BinancePriceMatch, BinanceProductType, BinanceRateLimitInterval, BinanceRateLimitType,
82 BinanceSide, BinanceTimeInForce, BinanceWorkingType,
83 },
84 models::BinanceErrorResponse,
85 parse::{
86 parse_coinm_instrument, parse_required_price_at_precision,
87 parse_required_quantity_at_precision, parse_usdm_instrument,
88 },
89 symbol::{format_binance_symbol, format_instrument_id},
90 urls::get_http_base_url,
91 },
92 futures::conversions::reduce_only_param,
93};
94
95const BINANCE_GLOBAL_RATE_KEY: &str = "binance:global";
96const BINANCE_ORDERS_RATE_KEY: &str = "binance:orders";
97
98#[derive(Debug, Serialize)]
99#[serde(rename_all = "camelCase")]
100struct BatchCancelParams {
101 symbol: String,
102 #[serde(skip_serializing_if = "Option::is_none")]
103 order_id_list: Option<String>,
104 #[serde(skip_serializing_if = "Option::is_none")]
105 orig_client_order_id_list: Option<String>,
106}
107
108#[derive(Debug, Clone)]
110pub struct BinanceRawFuturesHttpClient {
111 client: HttpClient,
112 base_url: String,
113 api_path: &'static str,
114 credential: Option<SigningCredential>,
115 recv_window: Option<u64>,
116 order_rate_keys: Vec<String>,
117}
118
119impl BinanceRawFuturesHttpClient {
120 #[must_use]
122 pub fn http_client(&self) -> &HttpClient {
123 &self.client
124 }
125
126 #[expect(clippy::too_many_arguments)]
132 pub fn new(
133 product_type: BinanceProductType,
134 environment: BinanceEnvironment,
135 api_key: Option<String>,
136 api_secret: Option<String>,
137 base_url_override: Option<String>,
138 recv_window: Option<u64>,
139 timeout_secs: Option<u64>,
140 proxy_url: Option<String>,
141 ) -> BinanceFuturesHttpResult<Self> {
142 let RateLimitConfig {
143 default_quota,
144 keyed_quotas,
145 order_keys,
146 } = Self::rate_limit_config(product_type);
147
148 let credential = match (api_key, api_secret) {
149 (Some(key), Some(secret)) => Some(SigningCredential::new(key, secret)),
150 (None, None) => None,
151 _ => return Err(BinanceFuturesHttpError::MissingCredentials),
152 };
153
154 let base_url = base_url_override
155 .unwrap_or_else(|| get_http_base_url(product_type, environment).to_string());
156
157 let api_path = Self::resolve_api_path(product_type);
158 let headers = Self::default_headers(&credential);
159
160 let client = HttpClient::new(
161 headers,
162 vec![BINANCE_API_KEY_HEADER.to_string()],
163 keyed_quotas,
164 default_quota,
165 timeout_secs,
166 proxy_url,
167 )?;
168
169 Ok(Self {
170 client,
171 base_url,
172 api_path,
173 credential,
174 recv_window,
175 order_rate_keys: order_keys,
176 })
177 }
178
179 pub async fn get<P, T>(
185 &self,
186 path: &str,
187 params: Option<&P>,
188 signed: bool,
189 use_order_quota: bool,
190 ) -> BinanceFuturesHttpResult<T>
191 where
192 P: Serialize + ?Sized,
193 T: DeserializeOwned,
194 {
195 self.request(Method::GET, path, params, signed, use_order_quota, None)
196 .await
197 }
198
199 pub async fn post<P, T>(
205 &self,
206 path: &str,
207 params: Option<&P>,
208 body: Option<Vec<u8>>,
209 signed: bool,
210 use_order_quota: bool,
211 ) -> BinanceFuturesHttpResult<T>
212 where
213 P: Serialize + ?Sized,
214 T: DeserializeOwned,
215 {
216 self.request(Method::POST, path, params, signed, use_order_quota, body)
217 .await
218 }
219
220 pub async fn request_put<P, T>(
226 &self,
227 path: &str,
228 params: Option<&P>,
229 signed: bool,
230 use_order_quota: bool,
231 ) -> BinanceFuturesHttpResult<T>
232 where
233 P: Serialize + ?Sized,
234 T: DeserializeOwned,
235 {
236 self.request(Method::PUT, path, params, signed, use_order_quota, None)
237 .await
238 }
239
240 pub async fn request_delete<P, T>(
246 &self,
247 path: &str,
248 params: Option<&P>,
249 signed: bool,
250 use_order_quota: bool,
251 ) -> BinanceFuturesHttpResult<T>
252 where
253 P: Serialize + ?Sized,
254 T: DeserializeOwned,
255 {
256 self.request(Method::DELETE, path, params, signed, use_order_quota, None)
257 .await
258 }
259
260 pub async fn batch_request<T: Serialize>(
266 &self,
267 path: &str,
268 items: &[T],
269 use_order_quota: bool,
270 ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
271 self.batch_request_method(Method::POST, path, items, use_order_quota)
272 .await
273 }
274
275 pub async fn batch_request_delete<T: Serialize>(
281 &self,
282 path: &str,
283 items: &[T],
284 use_order_quota: bool,
285 ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
286 self.batch_request_method(Method::DELETE, path, items, use_order_quota)
287 .await
288 }
289
290 pub async fn batch_request_put<T: Serialize>(
296 &self,
297 path: &str,
298 items: &[T],
299 use_order_quota: bool,
300 ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
301 self.batch_request_method(Method::PUT, path, items, use_order_quota)
302 .await
303 }
304
305 async fn batch_request_method<T: Serialize>(
306 &self,
307 method: Method,
308 path: &str,
309 items: &[T],
310 use_order_quota: bool,
311 ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
312 let cred = self
313 .credential
314 .as_ref()
315 .ok_or(BinanceFuturesHttpError::MissingCredentials)?;
316
317 let batch_json = serde_json::to_string(items)
318 .map_err(|e| BinanceFuturesHttpError::ValidationError(e.to_string()))?;
319
320 let encoded_batch = Self::percent_encode(&batch_json);
321 let timestamp = Utc::now().timestamp_millis();
322 let mut query = format!("batchOrders={encoded_batch}×tamp={timestamp}");
323
324 if let Some(recv_window) = self.recv_window {
325 query.push_str(&format!("&recvWindow={recv_window}"));
326 }
327
328 let signature = Self::percent_encode(&cred.sign(&query));
329 query.push_str(&format!("&signature={signature}"));
330
331 let url = self.build_url(path, &query);
332
333 let mut headers = HashMap::new();
334 headers.insert(
335 BINANCE_API_KEY_HEADER.to_string(),
336 cred.api_key().to_string(),
337 );
338
339 let keys = self.rate_limit_keys(use_order_quota);
340
341 let response = self
342 .client
343 .request(
344 method,
345 url,
346 None::<&HashMap<String, Vec<String>>>,
347 Some(headers),
348 None,
349 None,
350 Some(keys),
351 )
352 .await?;
353
354 if !response.status.is_success() {
355 return self.parse_error_response(&response);
356 }
357
358 serde_json::from_slice(&response.body)
359 .map_err(|e| BinanceFuturesHttpError::JsonError(e.to_string()))
360 }
361
362 fn percent_encode(input: &str) -> String {
364 let mut result = String::with_capacity(input.len() * 3);
365 for byte in input.bytes() {
366 match byte {
367 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
368 result.push(byte as char);
369 }
370 _ => {
371 result.push('%');
372 result.push_str(&format!("{byte:02X}"));
373 }
374 }
375 }
376 result
377 }
378
379 async fn request<P, T>(
380 &self,
381 method: Method,
382 path: &str,
383 params: Option<&P>,
384 signed: bool,
385 use_order_quota: bool,
386 body: Option<Vec<u8>>,
387 ) -> BinanceFuturesHttpResult<T>
388 where
389 P: Serialize + ?Sized,
390 T: DeserializeOwned,
391 {
392 let mut query = params
393 .map(serde_urlencoded::to_string)
394 .transpose()
395 .map_err(|e| BinanceFuturesHttpError::ValidationError(e.to_string()))?
396 .unwrap_or_default();
397
398 let mut headers = HashMap::new();
399
400 if signed {
401 let cred = self
402 .credential
403 .as_ref()
404 .ok_or(BinanceFuturesHttpError::MissingCredentials)?;
405
406 if !query.is_empty() {
407 query.push('&');
408 }
409
410 let timestamp = Utc::now().timestamp_millis();
411 query.push_str(&format!("timestamp={timestamp}"));
412
413 if let Some(recv_window) = self.recv_window {
414 query.push_str(&format!("&recvWindow={recv_window}"));
415 }
416
417 let signature = Self::percent_encode(&cred.sign(&query));
421 query.push_str(&format!("&signature={signature}"));
422 headers.insert(
423 BINANCE_API_KEY_HEADER.to_string(),
424 cred.api_key().to_string(),
425 );
426 }
427
428 let url = self.build_url(path, &query);
429 let keys = self.rate_limit_keys(use_order_quota);
430
431 let response = self
432 .client
433 .request(
434 method,
435 url,
436 None::<&HashMap<String, Vec<String>>>,
437 Some(headers),
438 body,
439 None,
440 Some(keys),
441 )
442 .await?;
443
444 if !response.status.is_success() {
445 return self.parse_error_response(&response);
446 }
447
448 serde_json::from_slice::<T>(&response.body)
449 .map_err(|e| BinanceFuturesHttpError::JsonError(e.to_string()))
450 }
451
452 fn build_url(&self, path: &str, query: &str) -> String {
453 let url_path = if path.starts_with("/fapi/")
455 || path.starts_with("/dapi/")
456 || path.starts_with("/futures/data/")
457 {
458 path.to_string()
459 } else if path.starts_with('/') {
460 format!("{}{}", self.api_path, path)
461 } else {
462 format!("{}/{}", self.api_path, path)
463 };
464
465 let mut url = format!("{}{}", self.base_url, url_path);
466
467 if !query.is_empty() {
468 url.push('?');
469 url.push_str(query);
470 }
471 url
472 }
473
474 fn rate_limit_keys(&self, use_orders: bool) -> Vec<String> {
475 if use_orders {
476 let mut keys = Vec::with_capacity(1 + self.order_rate_keys.len());
477 keys.push(BINANCE_GLOBAL_RATE_KEY.to_string());
478 keys.extend(self.order_rate_keys.iter().cloned());
479 keys
480 } else {
481 vec![BINANCE_GLOBAL_RATE_KEY.to_string()]
482 }
483 }
484
485 fn parse_error_response<T>(&self, response: &HttpResponse) -> BinanceFuturesHttpResult<T> {
486 let status = response.status.as_u16();
487 let body = String::from_utf8_lossy(&response.body).to_string();
488
489 if let Ok(err) = serde_json::from_str::<BinanceErrorResponse>(&body) {
490 return Err(BinanceFuturesHttpError::BinanceError {
491 code: err.code,
492 message: err.msg,
493 });
494 }
495
496 Err(BinanceFuturesHttpError::UnexpectedStatus { status, body })
497 }
498
499 fn default_headers(credential: &Option<SigningCredential>) -> HashMap<String, String> {
500 let mut headers = HashMap::new();
501 headers.insert(USER_AGENT.to_string(), NAUTILUS_USER_AGENT.to_string());
502
503 if let Some(cred) = credential {
504 headers.insert(
505 BINANCE_API_KEY_HEADER.to_string(),
506 cred.api_key().to_string(),
507 );
508 }
509 headers
510 }
511
512 fn resolve_api_path(product_type: BinanceProductType) -> &'static str {
513 match product_type {
514 BinanceProductType::UsdM => BINANCE_FAPI_PATH,
515 BinanceProductType::CoinM => BINANCE_DAPI_PATH,
516 _ => BINANCE_FAPI_PATH, }
518 }
519
520 fn rate_limit_config(product_type: BinanceProductType) -> RateLimitConfig {
521 let quotas = match product_type {
522 BinanceProductType::UsdM => BINANCE_FAPI_RATE_LIMITS,
523 BinanceProductType::CoinM => BINANCE_DAPI_RATE_LIMITS,
524 _ => BINANCE_FAPI_RATE_LIMITS,
525 };
526
527 let mut keyed = Vec::new();
528 let mut order_keys = Vec::new();
529 let mut default = None;
530
531 for quota in quotas {
532 if let Some(q) = Self::quota_from(quota) {
533 match quota.rate_limit_type {
534 BinanceRateLimitType::RequestWeight if default.is_none() => {
535 default = Some(q);
536 }
537 BinanceRateLimitType::Orders => {
538 let key = format!("{}:{:?}", BINANCE_ORDERS_RATE_KEY, quota.interval);
539 order_keys.push(key.clone());
540 keyed.push((key, q));
541 }
542 _ => {}
543 }
544 }
545 }
546
547 let default_quota = default.unwrap_or_else(|| {
548 Quota::per_second(NonZeroU32::new(10).expect("non-zero")).expect("valid constant")
549 });
550
551 keyed.push((BINANCE_GLOBAL_RATE_KEY.to_string(), default_quota));
552
553 RateLimitConfig {
554 default_quota: Some(default_quota),
555 keyed_quotas: keyed,
556 order_keys,
557 }
558 }
559
560 fn quota_from(quota: &BinanceRateLimitQuota) -> Option<Quota> {
561 let burst = NonZeroU32::new(quota.limit)?;
562 match quota.interval {
563 BinanceRateLimitInterval::Second => Quota::per_second(burst),
564 BinanceRateLimitInterval::Minute => Some(Quota::per_minute(burst)),
565 BinanceRateLimitInterval::Day => {
566 Quota::with_period(Duration::from_secs(SECONDS_IN_DAY))
567 .map(|q| q.allow_burst(burst))
568 }
569 BinanceRateLimitInterval::Unknown => None,
570 }
571 }
572
573 pub async fn ticker_24h(
579 &self,
580 params: &BinanceTicker24hrParams,
581 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesTicker24hr>> {
582 self.get("ticker/24hr", Some(params), false, false).await
583 }
584
585 pub async fn book_ticker(
591 &self,
592 params: &BinanceBookTickerParams,
593 ) -> BinanceFuturesHttpResult<Vec<BinanceBookTicker>> {
594 self.get("ticker/bookTicker", Some(params), false, false)
595 .await
596 }
597
598 pub async fn price_ticker(
604 &self,
605 symbol: Option<&str>,
606 ) -> BinanceFuturesHttpResult<Vec<BinancePriceTicker>> {
607 #[derive(Serialize)]
608 struct Params<'a> {
609 #[serde(skip_serializing_if = "Option::is_none")]
610 symbol: Option<&'a str>,
611 }
612 self.get("ticker/price", Some(&Params { symbol }), false, false)
613 .await
614 }
615
616 pub async fn depth(
622 &self,
623 params: &BinanceDepthParams,
624 ) -> BinanceFuturesHttpResult<BinanceOrderBook> {
625 self.get("depth", Some(params), false, false).await
626 }
627
628 pub async fn mark_price(
634 &self,
635 params: &BinanceMarkPriceParams,
636 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesMarkPrice>> {
637 let response: MarkPriceResponse =
638 self.get("premiumIndex", Some(params), false, false).await?;
639 Ok(response.into())
640 }
641
642 pub async fn funding_rate(
648 &self,
649 params: &BinanceFundingRateParams,
650 ) -> BinanceFuturesHttpResult<Vec<BinanceFundingRate>> {
651 self.get("fundingRate", Some(params), false, false).await
652 }
653
654 pub async fn open_interest(
660 &self,
661 params: &BinanceOpenInterestParams,
662 ) -> BinanceFuturesHttpResult<BinanceOpenInterest> {
663 self.get("openInterest", Some(params), false, false).await
664 }
665
666 pub async fn open_interest_hist(
672 &self,
673 params: &BinanceOpenInterestHistParams,
674 ) -> BinanceFuturesHttpResult<Vec<BinanceOpenInterestHistRecord>> {
675 self.get("/futures/data/openInterestHist", Some(params), false, false)
676 .await
677 }
678
679 pub async fn trades(
685 &self,
686 params: &BinanceTradesParams,
687 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesTrade>> {
688 self.get("trades", Some(params), false, false).await
689 }
690
691 pub async fn klines(
697 &self,
698 params: &BinanceKlinesParams,
699 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesKline>> {
700 self.get("klines", Some(params), false, false).await
701 }
702
703 pub async fn set_leverage(
709 &self,
710 params: &BinanceSetLeverageParams,
711 ) -> BinanceFuturesHttpResult<BinanceLeverageResponse> {
712 self.post("leverage", Some(params), None, true, false).await
713 }
714
715 pub async fn set_margin_type(
721 &self,
722 params: &BinanceSetMarginTypeParams,
723 ) -> BinanceFuturesHttpResult<serde_json::Value> {
724 self.post("marginType", Some(params), None, true, false)
725 .await
726 }
727
728 pub async fn query_hedge_mode(&self) -> BinanceFuturesHttpResult<BinanceHedgeModeResponse> {
734 self.get::<(), _>("positionSide/dual", None, true, false)
735 .await
736 }
737
738 pub async fn create_listen_key(&self) -> BinanceFuturesHttpResult<ListenKeyResponse> {
744 self.post::<(), ListenKeyResponse>("listenKey", None, None, true, false)
745 .await
746 }
747
748 pub async fn keepalive_listen_key(&self, listen_key: &str) -> BinanceFuturesHttpResult<()> {
754 let params = ListenKeyParams {
755 listen_key: listen_key.to_string(),
756 };
757 let _: serde_json::Value = self
758 .request_put("listenKey", Some(¶ms), true, false)
759 .await?;
760 Ok(())
761 }
762
763 pub async fn close_listen_key(&self, listen_key: &str) -> BinanceFuturesHttpResult<()> {
769 let params = ListenKeyParams {
770 listen_key: listen_key.to_string(),
771 };
772 let _: serde_json::Value = self
773 .request_delete("listenKey", Some(¶ms), true, false)
774 .await?;
775 Ok(())
776 }
777
778 pub async fn query_account(&self) -> BinanceFuturesHttpResult<BinanceFuturesAccountInfo> {
784 let path = if self.api_path.starts_with("/fapi") {
786 "/fapi/v2/account"
787 } else {
788 "/dapi/v1/account"
789 };
790 self.get::<(), _>(path, None, true, false).await
791 }
792
793 pub async fn query_positions(
799 &self,
800 params: &BinancePositionRiskParams,
801 ) -> BinanceFuturesHttpResult<Vec<BinancePositionRisk>> {
802 let path = if self.api_path.starts_with("/fapi") {
804 "/fapi/v2/positionRisk"
805 } else {
806 "/dapi/v1/positionRisk"
807 };
808 self.get(path, Some(params), true, false).await
809 }
810
811 pub async fn query_user_trades(
817 &self,
818 params: &BinanceUserTradesParams,
819 ) -> BinanceFuturesHttpResult<Vec<BinanceUserTrade>> {
820 self.get("userTrades", Some(params), true, false).await
821 }
822
823 pub async fn query_order(
829 &self,
830 params: &BinanceOrderQueryParams,
831 ) -> BinanceFuturesHttpResult<BinanceFuturesOrder> {
832 self.get("order", Some(params), true, false).await
833 }
834
835 pub async fn query_open_orders(
841 &self,
842 params: &BinanceOpenOrdersParams,
843 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesOrder>> {
844 self.get("openOrders", Some(params), true, false).await
845 }
846
847 pub async fn query_all_orders(
853 &self,
854 params: &BinanceAllOrdersParams,
855 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesOrder>> {
856 self.get("allOrders", Some(params), true, false).await
857 }
858
859 pub async fn submit_order(
865 &self,
866 params: &BinanceNewOrderParams,
867 ) -> BinanceFuturesHttpResult<BinanceFuturesOrder> {
868 self.post("order", Some(params), None, true, true).await
869 }
870
871 pub async fn submit_order_list(
877 &self,
878 orders: &[BatchOrderItem],
879 ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
880 if orders.is_empty() {
881 return Ok(Vec::new());
882 }
883
884 if orders.len() > 5 {
885 return Err(BinanceFuturesHttpError::ValidationError(
886 "Batch order limit is 5 orders maximum".to_string(),
887 ));
888 }
889
890 self.batch_request("batchOrders", orders, true).await
891 }
892
893 pub async fn modify_order(
899 &self,
900 params: &BinanceModifyOrderParams,
901 ) -> BinanceFuturesHttpResult<BinanceFuturesOrder> {
902 self.request_put("order", Some(params), true, true).await
903 }
904
905 pub async fn batch_modify_orders(
911 &self,
912 modifies: &[BatchModifyItem],
913 ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
914 if modifies.is_empty() {
915 return Ok(Vec::new());
916 }
917
918 if modifies.len() > 5 {
919 return Err(BinanceFuturesHttpError::ValidationError(
920 "Batch modify limit is 5 orders maximum".to_string(),
921 ));
922 }
923
924 self.batch_request_put("batchOrders", modifies, true).await
925 }
926
927 pub async fn cancel_order(
933 &self,
934 params: &BinanceCancelOrderParams,
935 ) -> BinanceFuturesHttpResult<BinanceFuturesOrder> {
936 self.request_delete("order", Some(params), true, true).await
937 }
938
939 pub async fn cancel_all_orders(
945 &self,
946 params: &BinanceCancelAllOrdersParams,
947 ) -> BinanceFuturesHttpResult<BinanceCancelAllOrdersResponse> {
948 self.request_delete("allOpenOrders", Some(params), true, true)
949 .await
950 }
951
952 pub async fn batch_cancel_orders(
958 &self,
959 cancels: &[BatchCancelItem],
960 ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
961 if cancels.is_empty() {
962 return Ok(Vec::new());
963 }
964
965 if cancels.len() > 10 {
966 return Err(BinanceFuturesHttpError::ValidationError(
967 "Batch cancel limit is 10 orders maximum".to_string(),
968 ));
969 }
970
971 let params = Self::batch_cancel_params(cancels)?;
972 self.request_delete("batchOrders", Some(¶ms), true, true)
973 .await
974 }
975
976 fn batch_cancel_params(
977 cancels: &[BatchCancelItem],
978 ) -> BinanceFuturesHttpResult<BatchCancelParams> {
979 let symbol = cancels[0].symbol.clone();
980 let mut order_ids = Vec::new();
981 let mut client_order_ids = Vec::new();
982
983 for cancel in cancels {
984 if cancel.symbol != symbol {
985 return Err(BinanceFuturesHttpError::ValidationError(
986 "Batch cancel orders must use the same symbol".to_string(),
987 ));
988 }
989
990 if let Some(order_id) = cancel.order_id {
991 order_ids.push(order_id);
992 }
993
994 if let Some(client_order_id) = &cancel.orig_client_order_id {
995 client_order_ids.push(client_order_id.clone());
996 }
997 }
998
999 if order_ids.is_empty() && client_order_ids.is_empty() {
1000 return Err(BinanceFuturesHttpError::ValidationError(
1001 "Batch cancel requires at least one order ID or client order ID".to_string(),
1002 ));
1003 }
1004
1005 if !order_ids.is_empty() && !client_order_ids.is_empty() {
1006 return Err(BinanceFuturesHttpError::ValidationError(
1007 "Batch cancel requires either order IDs or client order IDs, not both".to_string(),
1008 ));
1009 }
1010
1011 let order_id_list = if order_ids.is_empty() {
1012 None
1013 } else {
1014 Some(
1015 serde_json::to_string(&order_ids)
1016 .map_err(|e| BinanceFuturesHttpError::ValidationError(e.to_string()))?,
1017 )
1018 };
1019 let orig_client_order_id_list = if client_order_ids.is_empty() {
1020 None
1021 } else {
1022 Some(
1023 serde_json::to_string(&client_order_ids)
1024 .map_err(|e| BinanceFuturesHttpError::ValidationError(e.to_string()))?,
1025 )
1026 };
1027
1028 Ok(BatchCancelParams {
1029 symbol,
1030 order_id_list,
1031 orig_client_order_id_list,
1032 })
1033 }
1034
1035 pub async fn submit_algo_order(
1044 &self,
1045 params: &BinanceNewAlgoOrderParams,
1046 ) -> BinanceFuturesHttpResult<BinanceFuturesAlgoOrder> {
1047 self.post("algoOrder", Some(params), None, true, true).await
1048 }
1049
1050 pub async fn cancel_algo_order(
1058 &self,
1059 params: &BinanceAlgoOrderQueryParams,
1060 ) -> BinanceFuturesHttpResult<BinanceFuturesAlgoOrderCancelResponse> {
1061 self.request_delete("algoOrder", Some(params), true, true)
1062 .await
1063 }
1064
1065 pub async fn query_algo_order(
1073 &self,
1074 params: &BinanceAlgoOrderQueryParams,
1075 ) -> BinanceFuturesHttpResult<BinanceFuturesAlgoOrder> {
1076 self.get("algoOrder", Some(params), true, false).await
1077 }
1078
1079 pub async fn query_open_algo_orders(
1085 &self,
1086 params: &BinanceOpenAlgoOrdersParams,
1087 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesAlgoOrder>> {
1088 self.get("openAlgoOrders", Some(params), true, false).await
1089 }
1090
1091 pub async fn query_all_algo_orders(
1097 &self,
1098 params: &BinanceAllAlgoOrdersParams,
1099 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesAlgoOrder>> {
1100 self.get("allAlgoOrders", Some(params), true, false).await
1101 }
1102
1103 pub async fn cancel_all_algo_orders(
1109 &self,
1110 params: &BinanceCancelAllAlgoOrdersParams,
1111 ) -> BinanceFuturesHttpResult<BinanceCancelAllOrdersResponse> {
1112 self.request_delete("algoOpenOrders", Some(params), true, true)
1113 .await
1114 }
1115}
1116
1117#[derive(Debug, Deserialize)]
1119#[serde(untagged)]
1120enum MarkPriceResponse {
1121 Single(BinanceFuturesMarkPrice),
1122 Multiple(Vec<BinanceFuturesMarkPrice>),
1123}
1124
1125impl From<MarkPriceResponse> for Vec<BinanceFuturesMarkPrice> {
1126 fn from(response: MarkPriceResponse) -> Self {
1127 match response {
1128 MarkPriceResponse::Single(price) => vec![price],
1129 MarkPriceResponse::Multiple(prices) => prices,
1130 }
1131 }
1132}
1133
1134struct RateLimitConfig {
1135 default_quota: Option<Quota>,
1136 keyed_quotas: Vec<(String, Quota)>,
1137 order_keys: Vec<String>,
1138}
1139
1140#[derive(Clone, Debug)]
1142pub enum BinanceFuturesInstrument {
1143 UsdM(BinanceFuturesUsdSymbol),
1145 CoinM(BinanceFuturesCoinSymbol),
1147}
1148
1149impl BinanceFuturesInstrument {
1150 #[must_use]
1152 pub const fn symbol(&self) -> Ustr {
1153 match self {
1154 Self::UsdM(s) => s.symbol,
1155 Self::CoinM(s) => s.symbol,
1156 }
1157 }
1158
1159 #[must_use]
1161 pub const fn price_precision(&self) -> i32 {
1162 match self {
1163 Self::UsdM(s) => s.price_precision,
1164 Self::CoinM(s) => s.price_precision,
1165 }
1166 }
1167
1168 #[must_use]
1170 pub const fn quantity_precision(&self) -> i32 {
1171 match self {
1172 Self::UsdM(s) => s.quantity_precision,
1173 Self::CoinM(s) => s.quantity_precision,
1174 }
1175 }
1176
1177 #[must_use]
1179 pub fn id(&self) -> InstrumentId {
1180 match self {
1181 Self::UsdM(s) => format_instrument_id(&s.symbol, BinanceProductType::UsdM),
1182 Self::CoinM(s) => format_instrument_id(&s.symbol, BinanceProductType::CoinM),
1183 }
1184 }
1185
1186 #[must_use]
1188 pub fn quote_currency(&self) -> Currency {
1189 let quote_asset = match self {
1190 Self::UsdM(s) => &s.quote_asset,
1191 Self::CoinM(s) => &s.quote_asset,
1192 };
1193 Currency::get_or_create_crypto_with_context(quote_asset.as_str(), Some("futures quote"))
1194 }
1195}
1196
1197#[derive(Debug, Clone)]
1199pub struct BinanceFuturesHttpClient {
1200 inner: Arc<BinanceRawFuturesHttpClient>,
1201 product_type: BinanceProductType,
1202 clock: &'static AtomicTime,
1203 instruments: Arc<DashMap<Ustr, BinanceFuturesInstrument>>,
1204 treat_expired_as_canceled: bool,
1205}
1206
1207impl BinanceFuturesHttpClient {
1208 #[expect(clippy::too_many_arguments)]
1214 pub fn new(
1215 product_type: BinanceProductType,
1216 environment: BinanceEnvironment,
1217 clock: &'static AtomicTime,
1218 api_key: Option<String>,
1219 api_secret: Option<String>,
1220 base_url_override: Option<String>,
1221 recv_window: Option<u64>,
1222 timeout_secs: Option<u64>,
1223 proxy_url: Option<String>,
1224 treat_expired_as_canceled: bool,
1225 ) -> BinanceFuturesHttpResult<Self> {
1226 match product_type {
1227 BinanceProductType::UsdM | BinanceProductType::CoinM => {}
1228 _ => {
1229 return Err(BinanceFuturesHttpError::ValidationError(format!(
1230 "BinanceFuturesHttpClient requires UsdM or CoinM product type, was {product_type:?}"
1231 )));
1232 }
1233 }
1234
1235 let raw = BinanceRawFuturesHttpClient::new(
1236 product_type,
1237 environment,
1238 api_key,
1239 api_secret,
1240 base_url_override,
1241 recv_window,
1242 timeout_secs,
1243 proxy_url,
1244 )?;
1245
1246 Ok(Self {
1247 inner: Arc::new(raw),
1248 product_type,
1249 clock,
1250 instruments: Arc::new(DashMap::new()),
1251 treat_expired_as_canceled,
1252 })
1253 }
1254
1255 #[must_use]
1257 pub const fn product_type(&self) -> BinanceProductType {
1258 self.product_type
1259 }
1260
1261 #[must_use]
1263 pub fn inner(&self) -> &BinanceRawFuturesHttpClient {
1264 &self.inner
1265 }
1266
1267 #[must_use]
1269 pub fn instruments_cache(&self) -> Arc<DashMap<Ustr, BinanceFuturesInstrument>> {
1270 Arc::clone(&self.instruments)
1271 }
1272
1273 pub async fn server_time(&self) -> BinanceFuturesHttpResult<BinanceServerTime> {
1279 self.inner
1280 .get::<_, BinanceServerTime>("time", None::<&()>, false, false)
1281 .await
1282 }
1283
1284 pub async fn set_leverage(
1290 &self,
1291 params: &BinanceSetLeverageParams,
1292 ) -> BinanceFuturesHttpResult<BinanceLeverageResponse> {
1293 self.inner.set_leverage(params).await
1294 }
1295
1296 pub async fn set_margin_type(
1302 &self,
1303 params: &BinanceSetMarginTypeParams,
1304 ) -> BinanceFuturesHttpResult<serde_json::Value> {
1305 self.inner.set_margin_type(params).await
1306 }
1307
1308 pub async fn query_hedge_mode(&self) -> BinanceFuturesHttpResult<BinanceHedgeModeResponse> {
1314 self.inner.query_hedge_mode().await
1315 }
1316
1317 pub async fn create_listen_key(&self) -> BinanceFuturesHttpResult<ListenKeyResponse> {
1323 self.inner.create_listen_key().await
1324 }
1325
1326 pub async fn keepalive_listen_key(&self, listen_key: &str) -> BinanceFuturesHttpResult<()> {
1332 self.inner.keepalive_listen_key(listen_key).await
1333 }
1334
1335 pub async fn close_listen_key(&self, listen_key: &str) -> BinanceFuturesHttpResult<()> {
1341 self.inner.close_listen_key(listen_key).await
1342 }
1343
1344 pub async fn exchange_info(&self) -> BinanceFuturesHttpResult<()> {
1350 match self.product_type {
1351 BinanceProductType::UsdM => {
1352 let info: BinanceFuturesUsdExchangeInfo = self
1353 .inner
1354 .get("exchangeInfo", None::<&()>, false, false)
1355 .await?;
1356
1357 for symbol in info.symbols {
1358 self.instruments
1359 .insert(symbol.symbol, BinanceFuturesInstrument::UsdM(symbol));
1360 }
1361 }
1362 BinanceProductType::CoinM => {
1363 let info: BinanceFuturesCoinExchangeInfo = self
1364 .inner
1365 .get("exchangeInfo", None::<&()>, false, false)
1366 .await?;
1367
1368 for symbol in info.symbols {
1369 self.instruments
1370 .insert(symbol.symbol, BinanceFuturesInstrument::CoinM(symbol));
1371 }
1372 }
1373 _ => {
1374 return Err(BinanceFuturesHttpError::ValidationError(
1375 "Invalid product type for futures".to_string(),
1376 ));
1377 }
1378 }
1379
1380 Ok(())
1381 }
1382
1383 pub async fn request_symbol_statuses(
1393 &self,
1394 ) -> BinanceFuturesHttpResult<AHashMap<Ustr, MarketStatusAction>> {
1395 let mut statuses = AHashMap::new();
1396
1397 match self.product_type {
1398 BinanceProductType::UsdM => {
1399 let info: BinanceFuturesUsdExchangeInfo = self
1400 .inner
1401 .get("exchangeInfo", None::<&()>, false, false)
1402 .await?;
1403
1404 for symbol in &info.symbols {
1405 statuses.insert(symbol.symbol, MarketStatusAction::from(symbol.status));
1406 }
1407 }
1408 BinanceProductType::CoinM => {
1409 let info: BinanceFuturesCoinExchangeInfo = self
1410 .inner
1411 .get("exchangeInfo", None::<&()>, false, false)
1412 .await?;
1413
1414 for symbol in &info.symbols {
1415 let action = symbol
1416 .contract_status
1417 .map_or(MarketStatusAction::NotAvailableForTrading, Into::into);
1418 statuses.insert(symbol.symbol, action);
1419 }
1420 }
1421 _ => {
1422 return Err(BinanceFuturesHttpError::ValidationError(
1423 "Invalid product type for futures".to_string(),
1424 ));
1425 }
1426 }
1427
1428 Ok(statuses)
1429 }
1430
1431 pub async fn request_instruments(&self) -> BinanceFuturesHttpResult<Vec<InstrumentAny>> {
1437 let ts_init = UnixNanos::default();
1438
1439 let instruments = match self.product_type {
1440 BinanceProductType::UsdM => {
1441 let info: BinanceFuturesUsdExchangeInfo = self
1442 .inner
1443 .get("exchangeInfo", None::<&()>, false, false)
1444 .await?;
1445
1446 let mut instruments = Vec::with_capacity(info.symbols.len());
1447
1448 for symbol in info.symbols {
1449 self.instruments.insert(
1451 symbol.symbol,
1452 BinanceFuturesInstrument::UsdM(symbol.clone()),
1453 );
1454
1455 match parse_usdm_instrument(&symbol, ts_init, ts_init) {
1456 Ok(instrument) => instruments.push(instrument),
1457 Err(e) => {
1458 log::debug!(
1459 "Skipping symbol during instrument parsing: symbol={}, error={e}",
1460 symbol.symbol
1461 );
1462 }
1463 }
1464 }
1465
1466 log::debug!(
1467 "Loaded USD-M perpetual instruments: count={}",
1468 instruments.len()
1469 );
1470 instruments
1471 }
1472 BinanceProductType::CoinM => {
1473 let info: BinanceFuturesCoinExchangeInfo = self
1474 .inner
1475 .get("exchangeInfo", None::<&()>, false, false)
1476 .await?;
1477
1478 let mut instruments = Vec::with_capacity(info.symbols.len());
1479 for symbol in info.symbols {
1480 self.instruments.insert(
1482 symbol.symbol,
1483 BinanceFuturesInstrument::CoinM(symbol.clone()),
1484 );
1485
1486 match parse_coinm_instrument(&symbol, ts_init, ts_init) {
1487 Ok(instrument) => instruments.push(instrument),
1488 Err(e) => {
1489 log::debug!(
1490 "Skipping symbol during instrument parsing: symbol={}, error={e}",
1491 symbol.symbol
1492 );
1493 }
1494 }
1495 }
1496
1497 log::debug!(
1498 "Loaded COIN-M perpetual instruments: count={}",
1499 instruments.len()
1500 );
1501 instruments
1502 }
1503 _ => {
1504 return Err(BinanceFuturesHttpError::ValidationError(
1505 "Invalid product type for futures".to_string(),
1506 ));
1507 }
1508 };
1509
1510 Ok(instruments)
1511 }
1512
1513 pub async fn ticker_24h(
1519 &self,
1520 params: &BinanceTicker24hrParams,
1521 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesTicker24hr>> {
1522 self.inner.ticker_24h(params).await
1523 }
1524
1525 pub async fn book_ticker(
1531 &self,
1532 params: &BinanceBookTickerParams,
1533 ) -> BinanceFuturesHttpResult<Vec<BinanceBookTicker>> {
1534 self.inner.book_ticker(params).await
1535 }
1536
1537 pub async fn price_ticker(
1543 &self,
1544 symbol: Option<&str>,
1545 ) -> BinanceFuturesHttpResult<Vec<BinancePriceTicker>> {
1546 self.inner.price_ticker(symbol).await
1547 }
1548
1549 pub async fn depth(
1555 &self,
1556 params: &BinanceDepthParams,
1557 ) -> BinanceFuturesHttpResult<BinanceOrderBook> {
1558 self.inner.depth(params).await
1559 }
1560
1561 pub async fn mark_price(
1567 &self,
1568 params: &BinanceMarkPriceParams,
1569 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesMarkPrice>> {
1570 self.inner.mark_price(params).await
1571 }
1572
1573 pub async fn funding_rate(
1579 &self,
1580 params: &BinanceFundingRateParams,
1581 ) -> BinanceFuturesHttpResult<Vec<BinanceFundingRate>> {
1582 self.inner.funding_rate(params).await
1583 }
1584
1585 pub async fn open_interest(
1591 &self,
1592 params: &BinanceOpenInterestParams,
1593 ) -> BinanceFuturesHttpResult<BinanceOpenInterest> {
1594 self.inner.open_interest(params).await
1595 }
1596
1597 pub async fn open_interest_hist(
1603 &self,
1604 params: &BinanceOpenInterestHistParams,
1605 ) -> BinanceFuturesHttpResult<Vec<BinanceOpenInterestHistRecord>> {
1606 self.inner.open_interest_hist(params).await
1607 }
1608
1609 pub async fn query_order(
1615 &self,
1616 params: &BinanceOrderQueryParams,
1617 ) -> BinanceFuturesHttpResult<BinanceFuturesOrder> {
1618 self.inner.query_order(params).await
1619 }
1620
1621 pub async fn query_open_orders(
1627 &self,
1628 params: &BinanceOpenOrdersParams,
1629 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesOrder>> {
1630 self.inner.query_open_orders(params).await
1631 }
1632
1633 pub async fn query_all_orders(
1639 &self,
1640 params: &BinanceAllOrdersParams,
1641 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesOrder>> {
1642 self.inner.query_all_orders(params).await
1643 }
1644
1645 pub async fn query_account(&self) -> BinanceFuturesHttpResult<BinanceFuturesAccountInfo> {
1651 self.inner.query_account().await
1652 }
1653
1654 pub async fn query_positions(
1660 &self,
1661 params: &BinancePositionRiskParams,
1662 ) -> BinanceFuturesHttpResult<Vec<BinancePositionRisk>> {
1663 self.inner.query_positions(params).await
1664 }
1665
1666 pub async fn query_user_trades(
1672 &self,
1673 params: &BinanceUserTradesParams,
1674 ) -> BinanceFuturesHttpResult<Vec<BinanceUserTrade>> {
1675 self.inner.query_user_trades(params).await
1676 }
1677
1678 #[expect(clippy::too_many_arguments)]
1688 pub async fn submit_order(
1689 &self,
1690 account_id: AccountId,
1691 instrument_id: InstrumentId,
1692 client_order_id: ClientOrderId,
1693 order_side: OrderSide,
1694 order_type: OrderType,
1695 quantity: Quantity,
1696 time_in_force: TimeInForce,
1697 price: Option<Price>,
1698 trigger_price: Option<Price>,
1699 reduce_only: bool,
1700 post_only: bool,
1701 position_side: Option<BinancePositionSide>,
1702 price_match: Option<BinancePriceMatch>,
1703 ) -> anyhow::Result<OrderStatusReport> {
1704 let symbol = format_binance_symbol(&instrument_id);
1705 let size_precision = self.get_size_precision(&symbol)?;
1706
1707 let binance_side = BinanceSide::try_from(order_side)?;
1708 let binance_order_type = order_type_to_binance_futures(order_type)?;
1709 let binance_tif = if post_only {
1710 BinanceTimeInForce::Gtx
1711 } else {
1712 BinanceTimeInForce::try_from(time_in_force)?
1713 };
1714
1715 let requires_trigger_price = matches!(
1716 order_type,
1717 OrderType::StopMarket
1718 | OrderType::StopLimit
1719 | OrderType::TrailingStopMarket
1720 | OrderType::MarketIfTouched
1721 | OrderType::LimitIfTouched
1722 );
1723
1724 if requires_trigger_price && trigger_price.is_none() {
1725 anyhow::bail!("Order type {order_type:?} requires a trigger price");
1726 }
1727
1728 let requires_time_in_force = matches!(
1730 order_type,
1731 OrderType::Limit | OrderType::StopLimit | OrderType::LimitIfTouched
1732 );
1733
1734 let qty_str = quantity.to_string();
1735 let price_str = if price_match.is_some() {
1736 None
1737 } else {
1738 price.map(|p| p.to_string())
1739 };
1740 let stop_price_str = trigger_price.map(|p| p.to_string());
1741 let client_id_str = encode_broker_id(&client_order_id, BINANCE_NAUTILUS_FUTURES_BROKER_ID);
1742
1743 let params = BinanceNewOrderParams {
1744 symbol,
1745 side: binance_side,
1746 order_type: binance_order_type,
1747 time_in_force: if requires_time_in_force {
1748 Some(binance_tif)
1749 } else {
1750 None
1751 },
1752 quantity: Some(qty_str),
1753 price: price_str,
1754 new_client_order_id: Some(client_id_str),
1755 stop_price: stop_price_str,
1756 reduce_only: reduce_only_param(reduce_only, position_side),
1757 position_side,
1758 close_position: None,
1759 activation_price: None,
1760 callback_rate: None,
1761 working_type: None,
1762 price_protect: None,
1763 new_order_resp_type: None,
1764 good_till_date: None,
1765 recv_window: None,
1766 price_match,
1767 self_trade_prevention_mode: None,
1768 };
1769
1770 let order = self.inner.submit_order(¶ms).await?;
1771 let ts_init = self.clock.get_time_ns();
1772 order.to_order_status_report(
1773 account_id,
1774 instrument_id,
1775 size_precision,
1776 self.treat_expired_as_canceled,
1777 ts_init,
1778 )
1779 }
1780
1781 #[expect(clippy::too_many_arguments)]
1794 pub async fn submit_algo_order(
1795 &self,
1796 account_id: AccountId,
1797 instrument_id: InstrumentId,
1798 client_order_id: ClientOrderId,
1799 order_side: OrderSide,
1800 order_type: OrderType,
1801 quantity: Quantity,
1802 time_in_force: TimeInForce,
1803 price: Option<Price>,
1804 trigger_price: Option<Price>,
1805 reduce_only: bool,
1806 close_position: bool,
1807 position_side: Option<BinancePositionSide>,
1808 activation_price: Option<Price>,
1809 callback_rate: Option<String>,
1810 working_type: Option<BinanceWorkingType>,
1811 ) -> anyhow::Result<OrderStatusReport> {
1812 let symbol = format_binance_symbol(&instrument_id);
1813 let size_precision = self.get_size_precision(&symbol)?;
1814
1815 let binance_side = BinanceSide::try_from(order_side)?;
1816 let binance_order_type = order_type_to_binance_futures(order_type)?;
1817 let binance_tif = BinanceTimeInForce::try_from(time_in_force)?;
1818
1819 let requires_trigger_price = matches!(
1820 order_type,
1821 OrderType::StopMarket
1822 | OrderType::StopLimit
1823 | OrderType::MarketIfTouched
1824 | OrderType::LimitIfTouched
1825 );
1826 anyhow::ensure!(
1827 !requires_trigger_price || trigger_price.is_some(),
1828 "Algo order type {order_type:?} requires a trigger price"
1829 );
1830
1831 let requires_time_in_force =
1833 matches!(order_type, OrderType::StopLimit | OrderType::LimitIfTouched);
1834
1835 let price_str = price.map(|p| p.to_string());
1836 let trigger_price_str = if matches!(order_type, OrderType::TrailingStopMarket) {
1837 None
1838 } else {
1839 trigger_price.map(|p| p.to_string())
1840 };
1841 let reduce_only = reduce_only_param(reduce_only, position_side);
1842 let client_id_str = encode_broker_id(&client_order_id, BINANCE_NAUTILUS_FUTURES_BROKER_ID);
1843
1844 let params = if close_position {
1846 BinanceNewAlgoOrderParams {
1847 symbol,
1848 side: binance_side,
1849 order_type: binance_order_type,
1850 algo_type: BinanceAlgoType::Conditional,
1851 position_side,
1852 quantity: None,
1853 price: price_str,
1854 trigger_price: trigger_price_str,
1855 time_in_force: if requires_time_in_force {
1856 Some(binance_tif)
1857 } else {
1858 None
1859 },
1860 working_type,
1861 close_position: Some(true),
1862 price_protect: None,
1863 reduce_only: None,
1864 activation_price: activation_price.map(|p| p.to_string()),
1865 callback_rate,
1866 client_algo_id: Some(client_id_str),
1867 good_till_date: None,
1868 recv_window: None,
1869 }
1870 } else {
1871 let qty_str = quantity.to_string();
1872 BinanceNewAlgoOrderParams {
1873 symbol,
1874 side: binance_side,
1875 order_type: binance_order_type,
1876 algo_type: BinanceAlgoType::Conditional,
1877 position_side,
1878 quantity: Some(qty_str),
1879 price: price_str,
1880 trigger_price: trigger_price_str,
1881 time_in_force: if requires_time_in_force {
1882 Some(binance_tif)
1883 } else {
1884 None
1885 },
1886 working_type,
1887 close_position: None,
1888 price_protect: None,
1889 reduce_only,
1890 activation_price: activation_price.map(|p| p.to_string()),
1891 callback_rate,
1892 client_algo_id: Some(client_id_str),
1893 good_till_date: None,
1894 recv_window: None,
1895 }
1896 };
1897
1898 let order = self.inner.submit_algo_order(¶ms).await?;
1899 let ts_init = self.clock.get_time_ns();
1900 order.to_order_status_report(account_id, instrument_id, size_precision, ts_init)
1901 }
1902
1903 pub async fn submit_order_list(
1912 &self,
1913 orders: &[BatchOrderItem],
1914 ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
1915 self.inner.submit_order_list(orders).await
1916 }
1917
1918 #[expect(clippy::too_many_arguments)]
1929 pub async fn modify_order(
1930 &self,
1931 account_id: AccountId,
1932 instrument_id: InstrumentId,
1933 venue_order_id: Option<VenueOrderId>,
1934 client_order_id: Option<ClientOrderId>,
1935 order_side: OrderSide,
1936 quantity: Quantity,
1937 price: Price,
1938 ) -> anyhow::Result<OrderStatusReport> {
1939 anyhow::ensure!(
1940 venue_order_id.is_some() || client_order_id.is_some(),
1941 "Either venue_order_id or client_order_id must be provided"
1942 );
1943
1944 let symbol = format_binance_symbol(&instrument_id);
1945 let size_precision = self.get_size_precision(&symbol)?;
1946
1947 let binance_side = BinanceSide::try_from(order_side)?;
1948
1949 let order_id = venue_order_id
1950 .map(|id| id.inner().parse::<i64>())
1951 .transpose()
1952 .map_err(|_| anyhow::anyhow!("Invalid venue order ID"))?;
1953
1954 let params = BinanceModifyOrderParams {
1955 symbol,
1956 order_id,
1957 orig_client_order_id: client_order_id
1958 .map(|id| encode_broker_id(&id, BINANCE_NAUTILUS_FUTURES_BROKER_ID)),
1959 side: binance_side,
1960 quantity: quantity.to_string(),
1961 price: price.to_string(),
1962 recv_window: None,
1963 };
1964
1965 let order = self.inner.modify_order(¶ms).await?;
1966 let ts_init = self.clock.get_time_ns();
1967 order.to_order_status_report(
1968 account_id,
1969 instrument_id,
1970 size_precision,
1971 self.treat_expired_as_canceled,
1972 ts_init,
1973 )
1974 }
1975
1976 pub async fn batch_modify_orders(
1985 &self,
1986 modifies: &[BatchModifyItem],
1987 ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
1988 self.inner.batch_modify_orders(modifies).await
1989 }
1990
1991 pub async fn cancel_order(
2001 &self,
2002 instrument_id: InstrumentId,
2003 venue_order_id: Option<VenueOrderId>,
2004 client_order_id: Option<ClientOrderId>,
2005 ) -> anyhow::Result<VenueOrderId> {
2006 anyhow::ensure!(
2007 venue_order_id.is_some() || client_order_id.is_some(),
2008 "Either venue_order_id or client_order_id must be provided"
2009 );
2010
2011 let symbol = format_binance_symbol(&instrument_id);
2012
2013 let order_id = match venue_order_id {
2014 Some(venue_order_id) => match venue_order_id.inner().parse::<i64>() {
2015 Ok(order_id) => Some(order_id),
2016 Err(e) if client_order_id.is_some() => {
2017 log::warn!(
2018 "Unable to parse venue_order_id {venue_order_id} for cancel, canceling by client_order_id: {e}"
2019 );
2020 None
2021 }
2022 Err(e) => anyhow::bail!("Invalid venue order ID: {e}"),
2023 },
2024 None => None,
2025 };
2026
2027 let params = BinanceCancelOrderParams {
2028 symbol,
2029 order_id,
2030 orig_client_order_id: client_order_id
2031 .map(|id| encode_broker_id(&id, BINANCE_NAUTILUS_FUTURES_BROKER_ID)),
2032 recv_window: None,
2033 };
2034
2035 let order = self.inner.cancel_order(¶ms).await?;
2036 Ok(VenueOrderId::new(order.order_id.to_string()))
2037 }
2038
2039 pub async fn cancel_algo_order(&self, client_order_id: ClientOrderId) -> anyhow::Result<()> {
2048 let params = BinanceAlgoOrderQueryParams {
2049 algo_id: None,
2050 client_algo_id: Some(encode_broker_id(
2051 &client_order_id,
2052 BINANCE_NAUTILUS_FUTURES_BROKER_ID,
2053 )),
2054 recv_window: None,
2055 };
2056
2057 let response = self.inner.cancel_algo_order(¶ms).await?;
2058 if response.code.parse::<i32>().unwrap_or(0) == 200 {
2059 Ok(())
2060 } else {
2061 anyhow::bail!(
2062 "Cancel algo order failed: code={}, msg={}",
2063 response.code,
2064 response.msg
2065 )
2066 }
2067 }
2068
2069 pub async fn cancel_all_orders(
2075 &self,
2076 instrument_id: InstrumentId,
2077 ) -> anyhow::Result<Vec<VenueOrderId>> {
2078 let symbol = format_binance_symbol(&instrument_id);
2079
2080 let params = BinanceCancelAllOrdersParams {
2081 symbol,
2082 recv_window: None,
2083 };
2084
2085 let response = self.inner.cancel_all_orders(¶ms).await?;
2086 if response.code == 200 {
2087 Ok(vec![])
2088 } else {
2089 anyhow::bail!("Cancel all orders failed: {}", response.msg);
2090 }
2091 }
2092
2093 pub async fn cancel_all_algo_orders(&self, instrument_id: InstrumentId) -> anyhow::Result<()> {
2099 let symbol = format_binance_symbol(&instrument_id);
2100
2101 let params = BinanceCancelAllAlgoOrdersParams {
2102 symbol,
2103 recv_window: None,
2104 };
2105
2106 let response = self.inner.cancel_all_algo_orders(¶ms).await?;
2107 if response.code == 200 {
2108 Ok(())
2109 } else {
2110 anyhow::bail!("Cancel all algo orders failed: {}", response.msg);
2111 }
2112 }
2113
2114 pub async fn batch_cancel_orders(
2123 &self,
2124 cancels: &[BatchCancelItem],
2125 ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
2126 self.inner.batch_cancel_orders(cancels).await
2127 }
2128
2129 pub async fn query_open_algo_orders(
2137 &self,
2138 instrument_id: Option<InstrumentId>,
2139 ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesAlgoOrder>> {
2140 let symbol = instrument_id.map(|id| format_binance_symbol(&id));
2141
2142 let params = BinanceOpenAlgoOrdersParams {
2143 symbol,
2144 recv_window: None,
2145 };
2146
2147 self.inner.query_open_algo_orders(¶ms).await
2148 }
2149
2150 pub async fn query_algo_order(
2156 &self,
2157 client_order_id: ClientOrderId,
2158 ) -> BinanceFuturesHttpResult<BinanceFuturesAlgoOrder> {
2159 let params = BinanceAlgoOrderQueryParams {
2160 algo_id: None,
2161 client_algo_id: Some(encode_broker_id(
2162 &client_order_id,
2163 BINANCE_NAUTILUS_FUTURES_BROKER_ID,
2164 )),
2165 recv_window: None,
2166 };
2167
2168 self.inner.query_algo_order(¶ms).await
2169 }
2170
2171 fn get_size_precision(&self, symbol: &str) -> anyhow::Result<u8> {
2173 let instrument = self
2174 .instruments
2175 .get(&Ustr::from(symbol))
2176 .ok_or_else(|| anyhow::anyhow!("Instrument not found in cache: {symbol}"))?;
2177
2178 let precision = match instrument.value() {
2179 BinanceFuturesInstrument::UsdM(s) => s.quantity_precision,
2180 BinanceFuturesInstrument::CoinM(s) => s.quantity_precision,
2181 };
2182
2183 Ok(precision as u8)
2184 }
2185
2186 fn get_price_precision(&self, symbol: &str) -> anyhow::Result<u8> {
2188 let instrument = self
2189 .instruments
2190 .get(&Ustr::from(symbol))
2191 .ok_or_else(|| anyhow::anyhow!("Instrument not found in cache: {symbol}"))?;
2192
2193 let precision = match instrument.value() {
2194 BinanceFuturesInstrument::UsdM(s) => s.price_precision,
2195 BinanceFuturesInstrument::CoinM(s) => s.price_precision,
2196 };
2197
2198 Ok(precision as u8)
2199 }
2200
2201 pub async fn request_account_state(
2207 &self,
2208 account_id: AccountId,
2209 ) -> anyhow::Result<AccountState> {
2210 let ts_init = UnixNanos::default();
2211 let account_info = self.inner.query_account().await?;
2212 account_info.to_account_state(account_id, ts_init)
2213 }
2214
2215 pub async fn request_order_status_report(
2223 &self,
2224 account_id: AccountId,
2225 instrument_id: InstrumentId,
2226 venue_order_id: Option<VenueOrderId>,
2227 client_order_id: Option<ClientOrderId>,
2228 ) -> anyhow::Result<OrderStatusReport> {
2229 anyhow::ensure!(
2230 venue_order_id.is_some() || client_order_id.is_some(),
2231 "Either venue_order_id or client_order_id must be provided"
2232 );
2233
2234 let symbol = format_binance_symbol(&instrument_id);
2235 let size_precision = self.get_size_precision(&symbol)?;
2236
2237 let order_id = venue_order_id
2238 .map(|id| id.inner().parse::<i64>())
2239 .transpose()
2240 .map_err(|_| anyhow::anyhow!("Invalid venue order ID"))?;
2241
2242 let orig_client_order_id =
2243 client_order_id.map(|id| encode_broker_id(&id, BINANCE_NAUTILUS_FUTURES_BROKER_ID));
2244
2245 let params = BinanceOrderQueryParams {
2246 symbol,
2247 order_id,
2248 orig_client_order_id,
2249 recv_window: None,
2250 };
2251
2252 let order = self.inner.query_order(¶ms).await?;
2253 let ts_init = self.clock.get_time_ns();
2254 order.to_order_status_report(
2255 account_id,
2256 instrument_id,
2257 size_precision,
2258 self.treat_expired_as_canceled,
2259 ts_init,
2260 )
2261 }
2262
2263 pub async fn request_order_status_reports(
2271 &self,
2272 account_id: AccountId,
2273 instrument_id: Option<InstrumentId>,
2274 open_only: bool,
2275 ) -> anyhow::Result<Vec<OrderStatusReport>> {
2276 let symbol = instrument_id.map(|id| format_binance_symbol(&id));
2277
2278 let orders = if open_only {
2279 let params = BinanceOpenOrdersParams {
2280 symbol: symbol.clone(),
2281 recv_window: None,
2282 };
2283 self.inner.query_open_orders(¶ms).await?
2284 } else {
2285 let symbol = symbol.ok_or_else(|| {
2287 anyhow::anyhow!("instrument_id is required for historical orders")
2288 })?;
2289 let params = BinanceAllOrdersParams {
2290 symbol,
2291 order_id: None,
2292 start_time: None,
2293 end_time: None,
2294 limit: None,
2295 recv_window: None,
2296 };
2297 self.inner.query_all_orders(¶ms).await?
2298 };
2299
2300 let ts_init = self.clock.get_time_ns();
2301 let mut reports = Vec::with_capacity(orders.len());
2302
2303 for order in orders {
2304 let order_instrument_id = instrument_id.unwrap_or_else(|| {
2305 let suffix = self.product_type.suffix();
2307 InstrumentId::from(format!("{}{}.BINANCE", order.symbol, suffix))
2308 });
2309
2310 let size_precision = self.get_size_precision(&order.symbol).unwrap_or(8); match order.to_order_status_report(
2313 account_id,
2314 order_instrument_id,
2315 size_precision,
2316 self.treat_expired_as_canceled,
2317 ts_init,
2318 ) {
2319 Ok(report) => reports.push(report),
2320 Err(e) => {
2321 log::warn!("Failed to parse order status report: {e}");
2322 }
2323 }
2324 }
2325
2326 Ok(reports)
2327 }
2328
2329 #[expect(clippy::too_many_arguments)]
2335 pub async fn request_fill_reports(
2336 &self,
2337 account_id: AccountId,
2338 instrument_id: InstrumentId,
2339 venue_order_id: Option<VenueOrderId>,
2340 start: Option<i64>,
2341 end: Option<i64>,
2342 limit: Option<u32>,
2343 bnfcr_currency: Currency,
2344 ) -> anyhow::Result<Vec<FillReport>> {
2345 let symbol = format_binance_symbol(&instrument_id);
2346 let size_precision = self.get_size_precision(&symbol)?;
2347 let price_precision = self.get_price_precision(&symbol)?;
2348
2349 let order_id = venue_order_id
2350 .map(|id| id.inner().parse::<i64>())
2351 .transpose()
2352 .map_err(|_| anyhow::anyhow!("Invalid venue order ID"))?;
2353
2354 let params = BinanceUserTradesParams {
2355 symbol,
2356 order_id,
2357 start_time: start,
2358 end_time: end,
2359 from_id: None,
2360 limit,
2361 recv_window: None,
2362 };
2363
2364 let trades = self.inner.query_user_trades(¶ms).await?;
2365
2366 let ts_init = self.clock.get_time_ns();
2367 let mut reports = Vec::with_capacity(trades.len());
2368
2369 for trade in trades {
2370 match trade.to_fill_report(
2371 account_id,
2372 instrument_id,
2373 price_precision,
2374 size_precision,
2375 bnfcr_currency,
2376 ts_init,
2377 ) {
2378 Ok(report) => reports.push(report),
2379 Err(e) => {
2380 log::warn!("Failed to parse fill report: {e}");
2381 }
2382 }
2383 }
2384
2385 Ok(reports)
2386 }
2387
2388 pub async fn request_trades(
2394 &self,
2395 instrument_id: InstrumentId,
2396 limit: Option<u32>,
2397 ) -> anyhow::Result<Vec<TradeTick>> {
2398 let (symbol, price_precision, size_precision) =
2399 self.cached_precisions_by_id(instrument_id)?;
2400
2401 let params = BinanceTradesParams { symbol, limit };
2402
2403 let trades = self.inner.trades(¶ms).await?;
2404 let ts_init = UnixNanos::default();
2405
2406 let mut result = Vec::with_capacity(trades.len());
2407 for trade in trades {
2408 let tick = parse_futures_trade_tick(
2409 &trade,
2410 instrument_id,
2411 price_precision,
2412 size_precision,
2413 ts_init,
2414 )?;
2415 result.push(tick);
2416 }
2417
2418 Ok(result)
2419 }
2420
2421 pub async fn request_bars(
2428 &self,
2429 bar_type: BarType,
2430 start: Option<DateTime<Utc>>,
2431 end: Option<DateTime<Utc>>,
2432 limit: Option<u32>,
2433 ) -> anyhow::Result<Vec<Bar>> {
2434 anyhow::ensure!(
2435 bar_type.aggregation_source() == AggregationSource::External,
2436 "Only EXTERNAL aggregation is supported"
2437 );
2438
2439 let spec = bar_type.spec();
2440 let step = spec.step.get();
2441 let interval = match spec.aggregation {
2442 BarAggregation::Second => {
2443 anyhow::bail!("Binance Futures does not support second-level kline intervals")
2444 }
2445 BarAggregation::Minute => format!("{step}m"),
2446 BarAggregation::Hour => format!("{step}h"),
2447 BarAggregation::Day => format!("{step}d"),
2448 BarAggregation::Week => format!("{step}w"),
2449 BarAggregation::Month => format!("{step}M"),
2450 a => anyhow::bail!("Binance Futures does not support {a:?} aggregation"),
2451 };
2452
2453 let instrument_id = bar_type.instrument_id();
2454 let (symbol, price_precision, size_precision) =
2455 self.cached_precisions_by_id(instrument_id)?;
2456
2457 let params = BinanceKlinesParams {
2458 symbol,
2459 interval,
2460 start_time: start.map(|dt| dt.timestamp_millis()),
2461 end_time: end.map(|dt| dt.timestamp_millis()),
2462 limit,
2463 };
2464
2465 let klines = self.inner.klines(¶ms).await?;
2466 let ts_init = UnixNanos::default();
2467
2468 let mut result = Vec::with_capacity(klines.len());
2469 for kline in klines {
2470 let bar = parse_futures_kline_bar(
2471 &kline,
2472 bar_type,
2473 price_precision,
2474 size_precision,
2475 ts_init,
2476 )?;
2477 result.push(bar);
2478 }
2479
2480 Ok(result)
2481 }
2482
2483 fn cached_precisions_by_id(
2484 &self,
2485 instrument_id: InstrumentId,
2486 ) -> anyhow::Result<(String, u8, u8)> {
2487 let symbol = format_binance_symbol(&instrument_id);
2488 let instrument = self
2489 .instruments
2490 .get(&Ustr::from(symbol.as_str()))
2491 .ok_or_else(|| InstrumentLookupError::not_found(instrument_id))?;
2492
2493 let (price_precision, size_precision) = match instrument.value() {
2494 BinanceFuturesInstrument::UsdM(s) => (s.price_precision, s.quantity_precision),
2495 BinanceFuturesInstrument::CoinM(s) => (s.price_precision, s.quantity_precision),
2496 };
2497
2498 Ok((symbol, price_precision as u8, size_precision as u8))
2499 }
2500
2501 pub async fn request_funding_rates(
2507 &self,
2508 instrument_id: InstrumentId,
2509 start: Option<DateTime<Utc>>,
2510 end: Option<DateTime<Utc>>,
2511 limit: Option<u32>,
2512 ) -> anyhow::Result<Vec<FundingRateUpdate>> {
2513 let params = BinanceFundingRateParams {
2514 symbol: Some(format_binance_symbol(&instrument_id)),
2515 start_time: start.map(|dt| dt.timestamp_millis()),
2516 end_time: end.map(|dt| dt.timestamp_millis()),
2517 limit,
2518 };
2519
2520 let rates = self.inner.funding_rate(¶ms).await?;
2521 let ts_init = UnixNanos::default();
2522
2523 let mut result = Vec::with_capacity(rates.len());
2524 for rate in rates {
2525 result.push(parse_futures_funding_rate_update(
2526 &rate,
2527 instrument_id,
2528 ts_init,
2529 )?);
2530 }
2531
2532 Ok(result)
2533 }
2534}
2535
2536fn parse_futures_trade_tick(
2537 trade: &BinanceFuturesTrade,
2538 instrument_id: InstrumentId,
2539 price_precision: u8,
2540 size_precision: u8,
2541 ts_init: UnixNanos,
2542) -> anyhow::Result<TradeTick> {
2543 let price = parse_required_price_at_precision(&trade.price, price_precision, "trade.price")
2544 .map_err(|e| anyhow::anyhow!("invalid Futures trade id {}: {e}", trade.id))?;
2545 let size = parse_required_quantity_at_precision(&trade.qty, size_precision, "trade.qty")
2546 .map_err(|e| anyhow::anyhow!("invalid Futures trade id {}: {e}", trade.id))?;
2547 let ts_event = UnixNanos::from_millis(trade.time as u64);
2548
2549 let aggressor_side = if trade.is_buyer_maker {
2550 AggressorSide::Seller
2551 } else {
2552 AggressorSide::Buyer
2553 };
2554
2555 Ok(TradeTick::new(
2556 instrument_id,
2557 price,
2558 size,
2559 aggressor_side,
2560 TradeId::new(trade.id.to_string()),
2561 ts_event,
2562 ts_init,
2563 ))
2564}
2565
2566fn parse_futures_kline_bar(
2567 kline: &BinanceFuturesKline,
2568 bar_type: BarType,
2569 price_precision: u8,
2570 size_precision: u8,
2571 ts_init: UnixNanos,
2572) -> anyhow::Result<Bar> {
2573 let open = parse_required_price_at_precision(&kline.open, price_precision, "kline.open")
2574 .map_err(|e| anyhow::anyhow!("invalid Futures kline {}: {e}", kline.open_time))?;
2575 let high = parse_required_price_at_precision(&kline.high, price_precision, "kline.high")
2576 .map_err(|e| anyhow::anyhow!("invalid Futures kline {}: {e}", kline.open_time))?;
2577 let low = parse_required_price_at_precision(&kline.low, price_precision, "kline.low")
2578 .map_err(|e| anyhow::anyhow!("invalid Futures kline {}: {e}", kline.open_time))?;
2579 let close = parse_required_price_at_precision(&kline.close, price_precision, "kline.close")
2580 .map_err(|e| anyhow::anyhow!("invalid Futures kline {}: {e}", kline.open_time))?;
2581 let volume =
2582 parse_required_quantity_at_precision(&kline.volume, size_precision, "kline.volume")
2583 .map_err(|e| anyhow::anyhow!("invalid Futures kline {}: {e}", kline.open_time))?;
2584 let ts_event = UnixNanos::from_millis(kline.close_time as u64);
2585
2586 Ok(Bar::new(
2587 bar_type, open, high, low, close, volume, ts_event, ts_init,
2588 ))
2589}
2590
2591fn parse_futures_funding_rate_update(
2592 rate: &BinanceFundingRate,
2593 instrument_id: InstrumentId,
2594 ts_init: UnixNanos,
2595) -> anyhow::Result<FundingRateUpdate> {
2596 let funding_rate = rate.funding_rate.parse::<Decimal>().map_err(|e| {
2597 anyhow::anyhow!("invalid Futures funding rate at {}: {e}", rate.funding_time)
2598 })?;
2599 let ts_event = UnixNanos::from_millis(rate.funding_time as u64);
2600
2601 Ok(FundingRateUpdate::new(
2602 instrument_id,
2603 funding_rate,
2604 None, None, ts_event,
2607 ts_init,
2608 ))
2609}
2610
2611#[must_use]
2616pub fn is_algo_order_type(order_type: OrderType) -> bool {
2617 matches!(
2618 order_type,
2619 OrderType::StopMarket
2620 | OrderType::StopLimit
2621 | OrderType::MarketIfTouched
2622 | OrderType::LimitIfTouched
2623 | OrderType::TrailingStopMarket
2624 )
2625}
2626
2627pub(crate) fn order_type_to_binance_futures(
2629 order_type: OrderType,
2630) -> anyhow::Result<BinanceFuturesOrderType> {
2631 match order_type {
2632 OrderType::Market => Ok(BinanceFuturesOrderType::Market),
2633 OrderType::Limit => Ok(BinanceFuturesOrderType::Limit),
2634 OrderType::StopMarket => Ok(BinanceFuturesOrderType::StopMarket),
2635 OrderType::StopLimit => Ok(BinanceFuturesOrderType::Stop),
2636 OrderType::MarketIfTouched => Ok(BinanceFuturesOrderType::TakeProfitMarket),
2637 OrderType::LimitIfTouched => Ok(BinanceFuturesOrderType::TakeProfit),
2638 OrderType::TrailingStopMarket => Ok(BinanceFuturesOrderType::TrailingStopMarket),
2639 _ => anyhow::bail!("Unsupported order type for Binance Futures: {order_type:?}"),
2640 }
2641}
2642
2643#[cfg(test)]
2644mod tests {
2645 use nautilus_core::time::get_atomic_clock_realtime;
2646 use nautilus_network::http::{HttpStatus, StatusCode};
2647 use rstest::rstest;
2648 use tokio_util::bytes::Bytes;
2649
2650 use super::*;
2651 use crate::common::enums::BinanceTradingStatus;
2652
2653 #[rstest]
2654 fn test_rate_limit_config_usdm_has_request_weight_and_orders() {
2655 let config = BinanceRawFuturesHttpClient::rate_limit_config(BinanceProductType::UsdM);
2656
2657 assert!(config.default_quota.is_some());
2658 assert_eq!(config.order_keys.len(), 2);
2659 assert!(config.order_keys.iter().any(|k| k.contains("Second")));
2660 assert!(config.order_keys.iter().any(|k| k.contains("Minute")));
2661 }
2662
2663 #[rstest]
2664 fn test_rate_limit_config_coinm_has_request_weight_and_orders() {
2665 let config = BinanceRawFuturesHttpClient::rate_limit_config(BinanceProductType::CoinM);
2666
2667 assert!(config.default_quota.is_some());
2668 assert_eq!(config.order_keys.len(), 2);
2669 }
2670
2671 #[rstest]
2672 fn test_quota_from_unknown_interval_returns_none() {
2673 let quota = BinanceRateLimitQuota {
2674 rate_limit_type: BinanceRateLimitType::Orders,
2675 interval: BinanceRateLimitInterval::Unknown,
2676 interval_num: 1,
2677 limit: 10,
2678 };
2679
2680 assert!(BinanceRawFuturesHttpClient::quota_from("a).is_none());
2681 }
2682
2683 #[rstest]
2684 fn test_create_client_rejects_spot_product_type() {
2685 let result = BinanceFuturesHttpClient::new(
2686 BinanceProductType::Spot,
2687 BinanceEnvironment::Live,
2688 get_atomic_clock_realtime(),
2689 None,
2690 None,
2691 None,
2692 None,
2693 None,
2694 None,
2695 false,
2696 );
2697
2698 result.unwrap_err();
2699 }
2700
2701 #[rstest]
2702 fn test_parse_futures_trade_tick_rejects_invalid_price() {
2703 let trade = BinanceFuturesTrade {
2704 id: 100,
2705 price: "not-a-number".to_string(),
2706 qty: "0.001".to_string(),
2707 quote_qty: "50.00".to_string(),
2708 time: 1_625_474_304_000,
2709 is_buyer_maker: false,
2710 };
2711
2712 let result = parse_futures_trade_tick(
2713 &trade,
2714 InstrumentId::from("BTCUSDT-PERP.BINANCE"),
2715 2,
2716 3,
2717 UnixNanos::from(1_000_000_000u64),
2718 );
2719
2720 let error = result.unwrap_err().to_string();
2721 assert!(error.contains("trade.price"));
2722 assert!(error.contains("100"));
2723 }
2724
2725 #[rstest]
2726 fn test_parse_futures_kline_bar_rejects_invalid_volume() {
2727 let kline = BinanceFuturesKline {
2728 open_time: 1_625_474_304_000,
2729 open: "50000.00".to_string(),
2730 high: "51000.00".to_string(),
2731 low: "49000.00".to_string(),
2732 close: "50500.00".to_string(),
2733 volume: "not-a-number".to_string(),
2734 close_time: 1_625_474_364_000,
2735 quote_volume: "631250.00".to_string(),
2736 num_trades: 100,
2737 taker_buy_base_volume: "6.2".to_string(),
2738 taker_buy_quote_volume: "313100.00".to_string(),
2739 };
2740
2741 let result = parse_futures_kline_bar(
2742 &kline,
2743 BarType::from("BTCUSDT-PERP.BINANCE-1-MINUTE-LAST-EXTERNAL"),
2744 2,
2745 3,
2746 UnixNanos::from(1_000_000_000u64),
2747 );
2748
2749 let error = result.unwrap_err().to_string();
2750 assert!(error.contains("kline.volume"));
2751 assert!(error.contains("1625474304000"));
2752 }
2753
2754 fn create_test_raw_client() -> BinanceRawFuturesHttpClient {
2755 BinanceRawFuturesHttpClient::new(
2756 BinanceProductType::UsdM,
2757 BinanceEnvironment::Live,
2758 None,
2759 None,
2760 None,
2761 None,
2762 None,
2763 None,
2764 )
2765 .expect("Failed to create test client")
2766 }
2767
2768 fn create_test_client() -> BinanceFuturesHttpClient {
2769 BinanceFuturesHttpClient::new(
2770 BinanceProductType::UsdM,
2771 BinanceEnvironment::Live,
2772 get_atomic_clock_realtime(),
2773 None,
2774 None,
2775 Some("http://127.0.0.1:1".to_string()),
2776 None,
2777 Some(1),
2778 None,
2779 false,
2780 )
2781 .expect("Failed to create test client")
2782 }
2783
2784 fn test_usdm_symbol() -> BinanceFuturesUsdSymbol {
2785 BinanceFuturesUsdSymbol {
2786 symbol: Ustr::from("BTCUSDT"),
2787 pair: Ustr::from("BTCUSDT"),
2788 contract_type: "PERPETUAL".to_string(),
2789 delivery_date: 4_133_404_800_000,
2790 onboard_date: 1_569_398_400_000,
2791 status: BinanceTradingStatus::Trading,
2792 maint_margin_percent: "2.5000".to_string(),
2793 required_margin_percent: "5.0000".to_string(),
2794 base_asset: Ustr::from("BTC"),
2795 quote_asset: Ustr::from("USDT"),
2796 margin_asset: Ustr::from("USDT"),
2797 price_precision: 2,
2798 quantity_precision: 3,
2799 base_asset_precision: 8,
2800 quote_precision: 8,
2801 underlying_type: None,
2802 underlying_sub_type: Vec::new(),
2803 settle_plan: None,
2804 trigger_protect: None,
2805 liquidation_fee: None,
2806 market_take_bound: None,
2807 order_types: Vec::new(),
2808 time_in_force: Vec::new(),
2809 filters: Vec::new(),
2810 }
2811 }
2812
2813 #[rstest]
2814 fn test_cached_precisions_by_id_returns_symbol_and_precisions() {
2815 let client = create_test_client();
2816 client.instruments_cache().insert(
2817 Ustr::from("BTCUSDT"),
2818 BinanceFuturesInstrument::UsdM(test_usdm_symbol()),
2819 );
2820
2821 let (symbol, price_precision, size_precision) = client
2822 .cached_precisions_by_id(InstrumentId::from("BTCUSDT-PERP.BINANCE"))
2823 .unwrap();
2824
2825 assert_eq!(symbol, "BTCUSDT");
2826 assert_eq!(price_precision, 2);
2827 assert_eq!(size_precision, 3);
2828 }
2829
2830 #[rstest]
2831 #[tokio::test]
2832 async fn test_submit_algo_order_stop_market_requires_trigger_price() {
2833 let client = create_test_client();
2834 client.instruments_cache().insert(
2835 Ustr::from("BTCUSDT"),
2836 BinanceFuturesInstrument::UsdM(test_usdm_symbol()),
2837 );
2838
2839 let result = client
2840 .submit_algo_order(
2841 AccountId::from("BINANCE-001"),
2842 InstrumentId::from("BTCUSDT-PERP.BINANCE"),
2843 ClientOrderId::new("missing-trigger-test-001"),
2844 OrderSide::Sell,
2845 OrderType::StopMarket,
2846 Quantity::from("0.001"),
2847 TimeInForce::Gtc,
2848 None,
2849 None,
2850 false,
2851 false,
2852 None,
2853 None,
2854 None,
2855 None,
2856 )
2857 .await;
2858
2859 let error = result.unwrap_err().to_string();
2860 assert_eq!(error, "Algo order type StopMarket requires a trigger price");
2861 }
2862
2863 #[rstest]
2864 fn test_batch_cancel_params_builds_order_id_list() {
2865 let items = vec![
2866 BatchCancelItem::by_order_id("BTCUSDT", 123),
2867 BatchCancelItem::by_order_id("BTCUSDT", 456),
2868 ];
2869
2870 let params = BinanceRawFuturesHttpClient::batch_cancel_params(&items).unwrap();
2871
2872 assert_eq!(params.symbol, "BTCUSDT");
2873 assert_eq!(params.order_id_list.as_deref(), Some("[123,456]"));
2874 assert_eq!(params.orig_client_order_id_list, None);
2875 }
2876
2877 #[rstest]
2878 fn test_batch_cancel_params_builds_client_order_id_list() {
2879 let items = vec![
2880 BatchCancelItem::by_client_order_id("BTCUSDT", "first-order"),
2881 BatchCancelItem::by_client_order_id("BTCUSDT", "second-order"),
2882 ];
2883
2884 let params = BinanceRawFuturesHttpClient::batch_cancel_params(&items).unwrap();
2885
2886 assert_eq!(params.symbol, "BTCUSDT");
2887 assert_eq!(params.order_id_list, None);
2888 assert_eq!(
2889 params.orig_client_order_id_list.as_deref(),
2890 Some("[\"first-order\",\"second-order\"]"),
2891 );
2892 }
2893
2894 #[rstest]
2895 fn test_batch_cancel_params_rejects_mixed_symbols() {
2896 let items = vec![
2897 BatchCancelItem::by_order_id("BTCUSDT", 123),
2898 BatchCancelItem::by_order_id("ETHUSDT", 456),
2899 ];
2900
2901 let result = BinanceRawFuturesHttpClient::batch_cancel_params(&items);
2902
2903 assert_validation_error(result, "same symbol");
2904 }
2905
2906 #[rstest]
2907 fn test_batch_cancel_params_rejects_mixed_id_types() {
2908 let items = vec![
2909 BatchCancelItem::by_order_id("BTCUSDT", 123),
2910 BatchCancelItem::by_client_order_id("BTCUSDT", "client-order"),
2911 ];
2912
2913 let result = BinanceRawFuturesHttpClient::batch_cancel_params(&items);
2914
2915 assert_validation_error(result, "not both");
2916 }
2917
2918 #[rstest]
2919 fn test_batch_cancel_params_rejects_items_without_ids() {
2920 let items = vec![BatchCancelItem {
2921 symbol: "BTCUSDT".to_string(),
2922 order_id: None,
2923 orig_client_order_id: None,
2924 }];
2925
2926 let result = BinanceRawFuturesHttpClient::batch_cancel_params(&items);
2927
2928 assert_validation_error(result, "at least one order ID or client order ID");
2929 }
2930
2931 #[rstest]
2932 #[tokio::test]
2933 async fn test_batch_cancel_orders_rejects_more_than_ten_items() {
2934 let client = create_test_raw_client();
2935 let items = (0..11)
2936 .map(|order_id| BatchCancelItem::by_order_id("BTCUSDT", order_id))
2937 .collect::<Vec<_>>();
2938
2939 let result = client.batch_cancel_orders(&items).await;
2940
2941 match result {
2942 Err(BinanceFuturesHttpError::ValidationError(message)) => {
2943 assert!(message.contains("10 orders maximum"));
2944 }
2945 other => panic!("Expected ValidationError, was {other:?}"),
2946 }
2947 }
2948
2949 fn assert_validation_error(
2950 result: BinanceFuturesHttpResult<BatchCancelParams>,
2951 expected_message: &str,
2952 ) {
2953 match result {
2954 Err(BinanceFuturesHttpError::ValidationError(message)) => {
2955 assert!(message.contains(expected_message));
2956 }
2957 other => panic!("Expected ValidationError, was {other:?}"),
2958 }
2959 }
2960
2961 #[rstest]
2962 fn test_parse_error_response_binance_error() {
2963 let client = create_test_raw_client();
2964 let response = HttpResponse {
2965 status: HttpStatus::new(StatusCode::BAD_REQUEST),
2966 headers: HashMap::new(),
2967 body: Bytes::from(r#"{"code":-1121,"msg":"Invalid symbol."}"#),
2968 };
2969
2970 let result: BinanceFuturesHttpResult<()> = client.parse_error_response(&response);
2971
2972 match result {
2973 Err(BinanceFuturesHttpError::BinanceError { code, message }) => {
2974 assert_eq!(code, -1121);
2975 assert_eq!(message, "Invalid symbol.");
2976 }
2977 other => panic!("Expected BinanceError, was {other:?}"),
2978 }
2979 }
2980}