Skip to main content

nautilus_binance/futures/http/
client.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Binance Futures HTTP client for USD-M and COIN-M markets.
17
18use std::{collections::HashMap, num::NonZeroU32, sync::Arc, time::Duration};
19
20use ahash::AHashMap;
21use chrono::{DateTime, Utc};
22use dashmap::DashMap;
23use nautilus_core::{
24    consts::NAUTILUS_USER_AGENT, datetime::SECONDS_IN_DAY, nanos::UnixNanos, time::AtomicTime,
25};
26use nautilus_model::{
27    data::{Bar, BarType, TradeTick},
28    enums::{
29        AggregationSource, AggressorSide, BarAggregation, MarketStatusAction, OrderSide, OrderType,
30        TimeInForce,
31    },
32    events::AccountState,
33    identifiers::{AccountId, ClientOrderId, InstrumentId, TradeId, VenueOrderId},
34    instruments::any::InstrumentAny,
35    reports::{FillReport, OrderStatusReport},
36    types::{Currency, Price, Quantity},
37};
38use nautilus_network::{
39    http::{HttpClient, HttpResponse, Method},
40    ratelimiter::quota::Quota,
41};
42use serde::{Deserialize, Serialize, de::DeserializeOwned};
43use ustr::Ustr;
44
45use super::{
46    error::{BinanceFuturesHttpError, BinanceFuturesHttpResult},
47    models::{
48        BatchOrderResult, BinanceBookTicker, BinanceCancelAllOrdersResponse, BinanceFundingRate,
49        BinanceFuturesAccountInfo, BinanceFuturesAlgoOrder, BinanceFuturesAlgoOrderCancelResponse,
50        BinanceFuturesCoinExchangeInfo, BinanceFuturesCoinSymbol, BinanceFuturesKline,
51        BinanceFuturesMarkPrice, BinanceFuturesOrder, BinanceFuturesTicker24hr,
52        BinanceFuturesTrade, BinanceFuturesUsdExchangeInfo, BinanceFuturesUsdSymbol,
53        BinanceHedgeModeResponse, BinanceLeverageResponse, BinanceOpenInterest, BinanceOrderBook,
54        BinancePositionRisk, BinancePriceTicker, BinanceServerTime, BinanceUserTrade,
55        ListenKeyResponse,
56    },
57    query::{
58        BatchCancelItem, BatchModifyItem, BatchOrderItem, BinanceAlgoOrderQueryParams,
59        BinanceAllAlgoOrdersParams, BinanceAllOrdersParams, BinanceBookTickerParams,
60        BinanceCancelAllAlgoOrdersParams, BinanceCancelAllOrdersParams, BinanceCancelOrderParams,
61        BinanceDepthParams, BinanceFundingRateParams, BinanceKlinesParams, BinanceMarkPriceParams,
62        BinanceModifyOrderParams, BinanceNewAlgoOrderParams, BinanceNewOrderParams,
63        BinanceOpenAlgoOrdersParams, BinanceOpenInterestParams, BinanceOpenOrdersParams,
64        BinanceOrderQueryParams, BinancePositionRiskParams, BinanceSetLeverageParams,
65        BinanceSetMarginTypeParams, BinanceTicker24hrParams, BinanceTradesParams,
66        BinanceUserTradesParams, ListenKeyParams,
67    },
68};
69use crate::common::{
70    consts::{
71        BINANCE_API_KEY_HEADER, BINANCE_DAPI_PATH, BINANCE_DAPI_RATE_LIMITS, BINANCE_FAPI_PATH,
72        BINANCE_FAPI_RATE_LIMITS, BINANCE_NAUTILUS_FUTURES_BROKER_ID, BinanceRateLimitQuota,
73    },
74    credential::SigningCredential,
75    encoder::encode_broker_id,
76    enums::{
77        BinanceAlgoType, BinanceEnvironment, BinanceFuturesOrderType, BinancePositionSide,
78        BinancePriceMatch, BinanceProductType, BinanceRateLimitInterval, BinanceRateLimitType,
79        BinanceSide, BinanceTimeInForce, BinanceWorkingType,
80    },
81    models::BinanceErrorResponse,
82    parse::{parse_coinm_instrument, parse_usdm_instrument},
83    symbol::{format_binance_symbol, format_instrument_id},
84    urls::get_http_base_url,
85};
86
87const BINANCE_GLOBAL_RATE_KEY: &str = "binance:global";
88const BINANCE_ORDERS_RATE_KEY: &str = "binance:orders";
89
90/// Raw HTTP client for Binance Futures REST API.
91#[derive(Debug, Clone)]
92pub struct BinanceRawFuturesHttpClient {
93    client: HttpClient,
94    base_url: String,
95    api_path: &'static str,
96    credential: Option<SigningCredential>,
97    recv_window: Option<u64>,
98    order_rate_keys: Vec<String>,
99}
100
101impl BinanceRawFuturesHttpClient {
102    /// Returns a reference to the underlying HTTP client.
103    #[must_use]
104    pub fn http_client(&self) -> &HttpClient {
105        &self.client
106    }
107
108    /// Creates a new Binance raw futures HTTP client.
109    ///
110    /// # Errors
111    ///
112    /// Returns an error if credentials are incomplete or the HTTP client fails to build.
113    #[expect(clippy::too_many_arguments)]
114    pub fn new(
115        product_type: BinanceProductType,
116        environment: BinanceEnvironment,
117        api_key: Option<String>,
118        api_secret: Option<String>,
119        base_url_override: Option<String>,
120        recv_window: Option<u64>,
121        timeout_secs: Option<u64>,
122        proxy_url: Option<String>,
123    ) -> BinanceFuturesHttpResult<Self> {
124        let RateLimitConfig {
125            default_quota,
126            keyed_quotas,
127            order_keys,
128        } = Self::rate_limit_config(product_type);
129
130        let credential = match (api_key, api_secret) {
131            (Some(key), Some(secret)) => Some(SigningCredential::new(key, secret)),
132            (None, None) => None,
133            _ => return Err(BinanceFuturesHttpError::MissingCredentials),
134        };
135
136        let base_url = base_url_override
137            .unwrap_or_else(|| get_http_base_url(product_type, environment).to_string());
138
139        let api_path = Self::resolve_api_path(product_type);
140        let headers = Self::default_headers(&credential);
141
142        let client = HttpClient::new(
143            headers,
144            vec![BINANCE_API_KEY_HEADER.to_string()],
145            keyed_quotas,
146            default_quota,
147            timeout_secs,
148            proxy_url,
149        )?;
150
151        Ok(Self {
152            client,
153            base_url,
154            api_path,
155            credential,
156            recv_window,
157            order_rate_keys: order_keys,
158        })
159    }
160
161    /// Performs a GET request and deserializes the response body.
162    ///
163    /// # Errors
164    ///
165    /// Returns an error if the request fails or response deserialization fails.
166    pub async fn get<P, T>(
167        &self,
168        path: &str,
169        params: Option<&P>,
170        signed: bool,
171        use_order_quota: bool,
172    ) -> BinanceFuturesHttpResult<T>
173    where
174        P: Serialize + ?Sized,
175        T: DeserializeOwned,
176    {
177        self.request(Method::GET, path, params, signed, use_order_quota, None)
178            .await
179    }
180
181    /// Performs a POST request with optional body and signed query.
182    ///
183    /// # Errors
184    ///
185    /// Returns an error if the request fails or response deserialization fails.
186    pub async fn post<P, T>(
187        &self,
188        path: &str,
189        params: Option<&P>,
190        body: Option<Vec<u8>>,
191        signed: bool,
192        use_order_quota: bool,
193    ) -> BinanceFuturesHttpResult<T>
194    where
195        P: Serialize + ?Sized,
196        T: DeserializeOwned,
197    {
198        self.request(Method::POST, path, params, signed, use_order_quota, body)
199            .await
200    }
201
202    /// Performs a PUT request with signed query.
203    ///
204    /// # Errors
205    ///
206    /// Returns an error if the request fails or response deserialization fails.
207    pub async fn request_put<P, T>(
208        &self,
209        path: &str,
210        params: Option<&P>,
211        signed: bool,
212        use_order_quota: bool,
213    ) -> BinanceFuturesHttpResult<T>
214    where
215        P: Serialize + ?Sized,
216        T: DeserializeOwned,
217    {
218        self.request(Method::PUT, path, params, signed, use_order_quota, None)
219            .await
220    }
221
222    /// Performs a DELETE request with signed query.
223    ///
224    /// # Errors
225    ///
226    /// Returns an error if the request fails or response deserialization fails.
227    pub async fn request_delete<P, T>(
228        &self,
229        path: &str,
230        params: Option<&P>,
231        signed: bool,
232        use_order_quota: bool,
233    ) -> BinanceFuturesHttpResult<T>
234    where
235        P: Serialize + ?Sized,
236        T: DeserializeOwned,
237    {
238        self.request(Method::DELETE, path, params, signed, use_order_quota, None)
239            .await
240    }
241
242    /// Performs a batch POST request with batchOrders parameter.
243    ///
244    /// # Errors
245    ///
246    /// Returns an error if credentials are missing, the request fails, or JSON parsing fails.
247    pub async fn batch_request<T: Serialize>(
248        &self,
249        path: &str,
250        items: &[T],
251        use_order_quota: bool,
252    ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
253        self.batch_request_method(Method::POST, path, items, use_order_quota)
254            .await
255    }
256
257    /// Performs a batch DELETE request with batchOrders parameter.
258    ///
259    /// # Errors
260    ///
261    /// Returns an error if credentials are missing, the request fails, or JSON parsing fails.
262    pub async fn batch_request_delete<T: Serialize>(
263        &self,
264        path: &str,
265        items: &[T],
266        use_order_quota: bool,
267    ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
268        self.batch_request_method(Method::DELETE, path, items, use_order_quota)
269            .await
270    }
271
272    /// Performs a batch PUT request with batchOrders parameter.
273    ///
274    /// # Errors
275    ///
276    /// Returns an error if credentials are missing, the request fails, or JSON parsing fails.
277    pub async fn batch_request_put<T: Serialize>(
278        &self,
279        path: &str,
280        items: &[T],
281        use_order_quota: bool,
282    ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
283        self.batch_request_method(Method::PUT, path, items, use_order_quota)
284            .await
285    }
286
287    async fn batch_request_method<T: Serialize>(
288        &self,
289        method: Method,
290        path: &str,
291        items: &[T],
292        use_order_quota: bool,
293    ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
294        let cred = self
295            .credential
296            .as_ref()
297            .ok_or(BinanceFuturesHttpError::MissingCredentials)?;
298
299        let batch_json = serde_json::to_string(items)
300            .map_err(|e| BinanceFuturesHttpError::ValidationError(e.to_string()))?;
301
302        let encoded_batch = Self::percent_encode(&batch_json);
303        let timestamp = Utc::now().timestamp_millis();
304        let mut query = format!("batchOrders={encoded_batch}&timestamp={timestamp}");
305
306        if let Some(recv_window) = self.recv_window {
307            query.push_str(&format!("&recvWindow={recv_window}"));
308        }
309
310        let signature = Self::percent_encode(&cred.sign(&query));
311        query.push_str(&format!("&signature={signature}"));
312
313        let url = self.build_url(path, &query);
314
315        let mut headers = HashMap::new();
316        headers.insert(
317            BINANCE_API_KEY_HEADER.to_string(),
318            cred.api_key().to_string(),
319        );
320
321        let keys = self.rate_limit_keys(use_order_quota);
322
323        let response = self
324            .client
325            .request(
326                method,
327                url,
328                None::<&HashMap<String, Vec<String>>>,
329                Some(headers),
330                None,
331                None,
332                Some(keys),
333            )
334            .await?;
335
336        if !response.status.is_success() {
337            return self.parse_error_response(&response);
338        }
339
340        serde_json::from_slice(&response.body)
341            .map_err(|e| BinanceFuturesHttpError::JsonError(e.to_string()))
342    }
343
344    /// Percent-encodes a string for use in URL query parameters.
345    fn percent_encode(input: &str) -> String {
346        let mut result = String::with_capacity(input.len() * 3);
347        for byte in input.bytes() {
348            match byte {
349                b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
350                    result.push(byte as char);
351                }
352                _ => {
353                    result.push('%');
354                    result.push_str(&format!("{byte:02X}"));
355                }
356            }
357        }
358        result
359    }
360
361    async fn request<P, T>(
362        &self,
363        method: Method,
364        path: &str,
365        params: Option<&P>,
366        signed: bool,
367        use_order_quota: bool,
368        body: Option<Vec<u8>>,
369    ) -> BinanceFuturesHttpResult<T>
370    where
371        P: Serialize + ?Sized,
372        T: DeserializeOwned,
373    {
374        let mut query = params
375            .map(serde_urlencoded::to_string)
376            .transpose()
377            .map_err(|e| BinanceFuturesHttpError::ValidationError(e.to_string()))?
378            .unwrap_or_default();
379
380        let mut headers = HashMap::new();
381
382        if signed {
383            let cred = self
384                .credential
385                .as_ref()
386                .ok_or(BinanceFuturesHttpError::MissingCredentials)?;
387
388            if !query.is_empty() {
389                query.push('&');
390            }
391
392            let timestamp = Utc::now().timestamp_millis();
393            query.push_str(&format!("timestamp={timestamp}"));
394
395            if let Some(recv_window) = self.recv_window {
396                query.push_str(&format!("&recvWindow={recv_window}"));
397            }
398
399            // Percent-encode the signature: Ed25519 signatures are base64 and
400            // contain `+`, `/`, `=` which are not URL-safe. HMAC hex is
401            // already safe but percent-encoding it is a no-op.
402            let signature = Self::percent_encode(&cred.sign(&query));
403            query.push_str(&format!("&signature={signature}"));
404            headers.insert(
405                BINANCE_API_KEY_HEADER.to_string(),
406                cred.api_key().to_string(),
407            );
408        }
409
410        let url = self.build_url(path, &query);
411        let keys = self.rate_limit_keys(use_order_quota);
412
413        let response = self
414            .client
415            .request(
416                method,
417                url,
418                None::<&HashMap<String, Vec<String>>>,
419                Some(headers),
420                body,
421                None,
422                Some(keys),
423            )
424            .await?;
425
426        if !response.status.is_success() {
427            return self.parse_error_response(&response);
428        }
429
430        serde_json::from_slice::<T>(&response.body)
431            .map_err(|e| BinanceFuturesHttpError::JsonError(e.to_string()))
432    }
433
434    fn build_url(&self, path: &str, query: &str) -> String {
435        // Full API paths (e.g., /fapi/v2/account) bypass the default api_path
436        let url_path = if path.starts_with("/fapi/") || path.starts_with("/dapi/") {
437            path.to_string()
438        } else if path.starts_with('/') {
439            format!("{}{}", self.api_path, path)
440        } else {
441            format!("{}/{}", self.api_path, path)
442        };
443
444        let mut url = format!("{}{}", self.base_url, url_path);
445
446        if !query.is_empty() {
447            url.push('?');
448            url.push_str(query);
449        }
450        url
451    }
452
453    fn rate_limit_keys(&self, use_orders: bool) -> Vec<String> {
454        if use_orders {
455            let mut keys = Vec::with_capacity(1 + self.order_rate_keys.len());
456            keys.push(BINANCE_GLOBAL_RATE_KEY.to_string());
457            keys.extend(self.order_rate_keys.iter().cloned());
458            keys
459        } else {
460            vec![BINANCE_GLOBAL_RATE_KEY.to_string()]
461        }
462    }
463
464    fn parse_error_response<T>(&self, response: &HttpResponse) -> BinanceFuturesHttpResult<T> {
465        let status = response.status.as_u16();
466        let body = String::from_utf8_lossy(&response.body).to_string();
467
468        if let Ok(err) = serde_json::from_str::<BinanceErrorResponse>(&body) {
469            return Err(BinanceFuturesHttpError::BinanceError {
470                code: err.code,
471                message: err.msg,
472            });
473        }
474
475        Err(BinanceFuturesHttpError::UnexpectedStatus { status, body })
476    }
477
478    fn default_headers(credential: &Option<SigningCredential>) -> HashMap<String, String> {
479        let mut headers = HashMap::new();
480        headers.insert("User-Agent".to_string(), NAUTILUS_USER_AGENT.to_string());
481
482        if let Some(cred) = credential {
483            headers.insert(
484                BINANCE_API_KEY_HEADER.to_string(),
485                cred.api_key().to_string(),
486            );
487        }
488        headers
489    }
490
491    fn resolve_api_path(product_type: BinanceProductType) -> &'static str {
492        match product_type {
493            BinanceProductType::UsdM => BINANCE_FAPI_PATH,
494            BinanceProductType::CoinM => BINANCE_DAPI_PATH,
495            _ => BINANCE_FAPI_PATH, // Default to USD-M
496        }
497    }
498
499    fn rate_limit_config(product_type: BinanceProductType) -> RateLimitConfig {
500        let quotas = match product_type {
501            BinanceProductType::UsdM => BINANCE_FAPI_RATE_LIMITS,
502            BinanceProductType::CoinM => BINANCE_DAPI_RATE_LIMITS,
503            _ => BINANCE_FAPI_RATE_LIMITS,
504        };
505
506        let mut keyed = Vec::new();
507        let mut order_keys = Vec::new();
508        let mut default = None;
509
510        for quota in quotas {
511            if let Some(q) = Self::quota_from(quota) {
512                match quota.rate_limit_type {
513                    BinanceRateLimitType::RequestWeight if default.is_none() => {
514                        default = Some(q);
515                    }
516                    BinanceRateLimitType::Orders => {
517                        let key = format!("{}:{:?}", BINANCE_ORDERS_RATE_KEY, quota.interval);
518                        order_keys.push(key.clone());
519                        keyed.push((key, q));
520                    }
521                    _ => {}
522                }
523            }
524        }
525
526        let default_quota = default.unwrap_or_else(|| {
527            Quota::per_second(NonZeroU32::new(10).expect("non-zero")).expect("valid constant")
528        });
529
530        keyed.push((BINANCE_GLOBAL_RATE_KEY.to_string(), default_quota));
531
532        RateLimitConfig {
533            default_quota: Some(default_quota),
534            keyed_quotas: keyed,
535            order_keys,
536        }
537    }
538
539    fn quota_from(quota: &BinanceRateLimitQuota) -> Option<Quota> {
540        let burst = NonZeroU32::new(quota.limit)?;
541        match quota.interval {
542            BinanceRateLimitInterval::Second => Quota::per_second(burst),
543            BinanceRateLimitInterval::Minute => Some(Quota::per_minute(burst)),
544            BinanceRateLimitInterval::Day => {
545                Quota::with_period(Duration::from_secs(SECONDS_IN_DAY))
546                    .map(|q| q.allow_burst(burst))
547            }
548            BinanceRateLimitInterval::Unknown => None,
549        }
550    }
551
552    /// Fetches 24hr ticker statistics.
553    ///
554    /// # Errors
555    ///
556    /// Returns an error if the request fails.
557    pub async fn ticker_24h(
558        &self,
559        params: &BinanceTicker24hrParams,
560    ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesTicker24hr>> {
561        self.get("ticker/24hr", Some(params), false, false).await
562    }
563
564    /// Fetches best bid/ask prices.
565    ///
566    /// # Errors
567    ///
568    /// Returns an error if the request fails.
569    pub async fn book_ticker(
570        &self,
571        params: &BinanceBookTickerParams,
572    ) -> BinanceFuturesHttpResult<Vec<BinanceBookTicker>> {
573        self.get("ticker/bookTicker", Some(params), false, false)
574            .await
575    }
576
577    /// Fetches price ticker.
578    ///
579    /// # Errors
580    ///
581    /// Returns an error if the request fails.
582    pub async fn price_ticker(
583        &self,
584        symbol: Option<&str>,
585    ) -> BinanceFuturesHttpResult<Vec<BinancePriceTicker>> {
586        #[derive(Serialize)]
587        struct Params<'a> {
588            #[serde(skip_serializing_if = "Option::is_none")]
589            symbol: Option<&'a str>,
590        }
591        self.get("ticker/price", Some(&Params { symbol }), false, false)
592            .await
593    }
594
595    /// Fetches order book depth.
596    ///
597    /// # Errors
598    ///
599    /// Returns an error if the request fails.
600    pub async fn depth(
601        &self,
602        params: &BinanceDepthParams,
603    ) -> BinanceFuturesHttpResult<BinanceOrderBook> {
604        self.get("depth", Some(params), false, false).await
605    }
606
607    /// Fetches mark price and funding rate.
608    ///
609    /// # Errors
610    ///
611    /// Returns an error if the request fails.
612    pub async fn mark_price(
613        &self,
614        params: &BinanceMarkPriceParams,
615    ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesMarkPrice>> {
616        let response: MarkPriceResponse =
617            self.get("premiumIndex", Some(params), false, false).await?;
618        Ok(response.into())
619    }
620
621    /// Fetches funding rate history.
622    ///
623    /// # Errors
624    ///
625    /// Returns an error if the request fails.
626    pub async fn funding_rate(
627        &self,
628        params: &BinanceFundingRateParams,
629    ) -> BinanceFuturesHttpResult<Vec<BinanceFundingRate>> {
630        self.get("fundingRate", Some(params), false, false).await
631    }
632
633    /// Fetches current open interest for a symbol.
634    ///
635    /// # Errors
636    ///
637    /// Returns an error if the request fails.
638    pub async fn open_interest(
639        &self,
640        params: &BinanceOpenInterestParams,
641    ) -> BinanceFuturesHttpResult<BinanceOpenInterest> {
642        self.get("openInterest", Some(params), false, false).await
643    }
644
645    /// Fetches recent public trades for a symbol.
646    ///
647    /// # Errors
648    ///
649    /// Returns an error if the request fails.
650    pub async fn trades(
651        &self,
652        params: &BinanceTradesParams,
653    ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesTrade>> {
654        self.get("trades", Some(params), false, false).await
655    }
656
657    /// Fetches kline/candlestick data for a symbol.
658    ///
659    /// # Errors
660    ///
661    /// Returns an error if the request fails.
662    pub async fn klines(
663        &self,
664        params: &BinanceKlinesParams,
665    ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesKline>> {
666        self.get("klines", Some(params), false, false).await
667    }
668
669    /// Sets leverage for a symbol.
670    ///
671    /// # Errors
672    ///
673    /// Returns an error if the request fails.
674    pub async fn set_leverage(
675        &self,
676        params: &BinanceSetLeverageParams,
677    ) -> BinanceFuturesHttpResult<BinanceLeverageResponse> {
678        self.post("leverage", Some(params), None, true, false).await
679    }
680
681    /// Sets margin type for a symbol.
682    ///
683    /// # Errors
684    ///
685    /// Returns an error if the request fails.
686    pub async fn set_margin_type(
687        &self,
688        params: &BinanceSetMarginTypeParams,
689    ) -> BinanceFuturesHttpResult<serde_json::Value> {
690        self.post("marginType", Some(params), None, true, false)
691            .await
692    }
693
694    /// Queries hedge mode (dual side position) setting.
695    ///
696    /// # Errors
697    ///
698    /// Returns an error if the request fails.
699    pub async fn query_hedge_mode(&self) -> BinanceFuturesHttpResult<BinanceHedgeModeResponse> {
700        self.get::<(), _>("positionSide/dual", None, true, false)
701            .await
702    }
703
704    /// Creates a listen key for user data stream.
705    ///
706    /// # Errors
707    ///
708    /// Returns an error if the request fails.
709    pub async fn create_listen_key(&self) -> BinanceFuturesHttpResult<ListenKeyResponse> {
710        self.post::<(), ListenKeyResponse>("listenKey", None, None, true, false)
711            .await
712    }
713
714    /// Keeps alive an existing listen key.
715    ///
716    /// # Errors
717    ///
718    /// Returns an error if the request fails.
719    pub async fn keepalive_listen_key(&self, listen_key: &str) -> BinanceFuturesHttpResult<()> {
720        let params = ListenKeyParams {
721            listen_key: listen_key.to_string(),
722        };
723        let _: serde_json::Value = self
724            .request_put("listenKey", Some(&params), true, false)
725            .await?;
726        Ok(())
727    }
728
729    /// Closes an existing listen key.
730    ///
731    /// # Errors
732    ///
733    /// Returns an error if the request fails.
734    pub async fn close_listen_key(&self, listen_key: &str) -> BinanceFuturesHttpResult<()> {
735        let params = ListenKeyParams {
736            listen_key: listen_key.to_string(),
737        };
738        let _: serde_json::Value = self
739            .request_delete("listenKey", Some(&params), true, false)
740            .await?;
741        Ok(())
742    }
743
744    /// Fetches account information including balances and positions.
745    ///
746    /// # Errors
747    ///
748    /// Returns an error if the request fails.
749    pub async fn query_account(&self) -> BinanceFuturesHttpResult<BinanceFuturesAccountInfo> {
750        // USD-M uses /fapi/v2/account, COIN-M uses /dapi/v1/account
751        let path = if self.api_path.starts_with("/fapi") {
752            "/fapi/v2/account"
753        } else {
754            "/dapi/v1/account"
755        };
756        self.get::<(), _>(path, None, true, false).await
757    }
758
759    /// Fetches position risk information.
760    ///
761    /// # Errors
762    ///
763    /// Returns an error if the request fails.
764    pub async fn query_positions(
765        &self,
766        params: &BinancePositionRiskParams,
767    ) -> BinanceFuturesHttpResult<Vec<BinancePositionRisk>> {
768        // USD-M uses /fapi/v2/positionRisk, COIN-M uses /dapi/v1/positionRisk
769        let path = if self.api_path.starts_with("/fapi") {
770            "/fapi/v2/positionRisk"
771        } else {
772            "/dapi/v1/positionRisk"
773        };
774        self.get(path, Some(params), true, false).await
775    }
776
777    /// Fetches user trades for a symbol.
778    ///
779    /// # Errors
780    ///
781    /// Returns an error if the request fails.
782    pub async fn query_user_trades(
783        &self,
784        params: &BinanceUserTradesParams,
785    ) -> BinanceFuturesHttpResult<Vec<BinanceUserTrade>> {
786        self.get("userTrades", Some(params), true, false).await
787    }
788
789    /// Queries a single order by order ID or client order ID.
790    ///
791    /// # Errors
792    ///
793    /// Returns an error if the request fails.
794    pub async fn query_order(
795        &self,
796        params: &BinanceOrderQueryParams,
797    ) -> BinanceFuturesHttpResult<BinanceFuturesOrder> {
798        self.get("order", Some(params), true, false).await
799    }
800
801    /// Queries all open orders.
802    ///
803    /// # Errors
804    ///
805    /// Returns an error if the request fails.
806    pub async fn query_open_orders(
807        &self,
808        params: &BinanceOpenOrdersParams,
809    ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesOrder>> {
810        self.get("openOrders", Some(params), true, false).await
811    }
812
813    /// Queries all orders (including historical).
814    ///
815    /// # Errors
816    ///
817    /// Returns an error if the request fails.
818    pub async fn query_all_orders(
819        &self,
820        params: &BinanceAllOrdersParams,
821    ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesOrder>> {
822        self.get("allOrders", Some(params), true, false).await
823    }
824
825    /// Submits a new order.
826    ///
827    /// # Errors
828    ///
829    /// Returns an error if the request fails.
830    pub async fn submit_order(
831        &self,
832        params: &BinanceNewOrderParams,
833    ) -> BinanceFuturesHttpResult<BinanceFuturesOrder> {
834        self.post("order", Some(params), None, true, true).await
835    }
836
837    /// Submits multiple orders in a single request (up to 5 orders).
838    ///
839    /// # Errors
840    ///
841    /// Returns an error if the batch exceeds 5 orders or the request fails.
842    pub async fn submit_order_list(
843        &self,
844        orders: &[BatchOrderItem],
845    ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
846        if orders.is_empty() {
847            return Ok(Vec::new());
848        }
849
850        if orders.len() > 5 {
851            return Err(BinanceFuturesHttpError::ValidationError(
852                "Batch order limit is 5 orders maximum".to_string(),
853            ));
854        }
855
856        self.batch_request("batchOrders", orders, true).await
857    }
858
859    /// Modifies an existing order (price and quantity only).
860    ///
861    /// # Errors
862    ///
863    /// Returns an error if the request fails.
864    pub async fn modify_order(
865        &self,
866        params: &BinanceModifyOrderParams,
867    ) -> BinanceFuturesHttpResult<BinanceFuturesOrder> {
868        self.request_put("order", Some(params), true, true).await
869    }
870
871    /// Modifies multiple orders in a single request (up to 5 orders).
872    ///
873    /// # Errors
874    ///
875    /// Returns an error if the batch exceeds 5 orders or the request fails.
876    pub async fn batch_modify_orders(
877        &self,
878        modifies: &[BatchModifyItem],
879    ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
880        if modifies.is_empty() {
881            return Ok(Vec::new());
882        }
883
884        if modifies.len() > 5 {
885            return Err(BinanceFuturesHttpError::ValidationError(
886                "Batch modify limit is 5 orders maximum".to_string(),
887            ));
888        }
889
890        self.batch_request_put("batchOrders", modifies, true).await
891    }
892
893    /// Cancels an existing order.
894    ///
895    /// # Errors
896    ///
897    /// Returns an error if the request fails.
898    pub async fn cancel_order(
899        &self,
900        params: &BinanceCancelOrderParams,
901    ) -> BinanceFuturesHttpResult<BinanceFuturesOrder> {
902        self.request_delete("order", Some(params), true, true).await
903    }
904
905    /// Cancels all open orders for a symbol.
906    ///
907    /// # Errors
908    ///
909    /// Returns an error if the request fails.
910    pub async fn cancel_all_orders(
911        &self,
912        params: &BinanceCancelAllOrdersParams,
913    ) -> BinanceFuturesHttpResult<BinanceCancelAllOrdersResponse> {
914        self.request_delete("allOpenOrders", Some(params), true, true)
915            .await
916    }
917
918    /// Cancels multiple orders in a single request (up to 5 orders).
919    ///
920    /// # Errors
921    ///
922    /// Returns an error if the batch exceeds 5 orders or the request fails.
923    pub async fn batch_cancel_orders(
924        &self,
925        cancels: &[BatchCancelItem],
926    ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
927        if cancels.is_empty() {
928            return Ok(Vec::new());
929        }
930
931        if cancels.len() > 5 {
932            return Err(BinanceFuturesHttpError::ValidationError(
933                "Batch cancel limit is 5 orders maximum".to_string(),
934            ));
935        }
936
937        self.batch_request_delete("batchOrders", cancels, true)
938            .await
939    }
940
941    /// Submits a new algo order (conditional order).
942    ///
943    /// Algo orders include STOP_MARKET, STOP (stop-limit), TAKE_PROFIT, TAKE_PROFIT_MARKET,
944    /// and TRAILING_STOP_MARKET order types.
945    ///
946    /// # Errors
947    ///
948    /// Returns an error if the request fails.
949    pub async fn submit_algo_order(
950        &self,
951        params: &BinanceNewAlgoOrderParams,
952    ) -> BinanceFuturesHttpResult<BinanceFuturesAlgoOrder> {
953        self.post("algoOrder", Some(params), None, true, true).await
954    }
955
956    /// Cancels an algo order.
957    ///
958    /// Must provide either `algo_id` or `client_algo_id`.
959    ///
960    /// # Errors
961    ///
962    /// Returns an error if the request fails.
963    pub async fn cancel_algo_order(
964        &self,
965        params: &BinanceAlgoOrderQueryParams,
966    ) -> BinanceFuturesHttpResult<BinanceFuturesAlgoOrderCancelResponse> {
967        self.request_delete("algoOrder", Some(params), true, true)
968            .await
969    }
970
971    /// Queries a single algo order.
972    ///
973    /// Must provide either `algo_id` or `client_algo_id`.
974    ///
975    /// # Errors
976    ///
977    /// Returns an error if the request fails.
978    pub async fn query_algo_order(
979        &self,
980        params: &BinanceAlgoOrderQueryParams,
981    ) -> BinanceFuturesHttpResult<BinanceFuturesAlgoOrder> {
982        self.get("algoOrder", Some(params), true, false).await
983    }
984
985    /// Queries all open algo orders.
986    ///
987    /// # Errors
988    ///
989    /// Returns an error if the request fails.
990    pub async fn query_open_algo_orders(
991        &self,
992        params: &BinanceOpenAlgoOrdersParams,
993    ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesAlgoOrder>> {
994        self.get("openAlgoOrders", Some(params), true, false).await
995    }
996
997    /// Queries all algo orders including historical (7-day limit).
998    ///
999    /// # Errors
1000    ///
1001    /// Returns an error if the request fails.
1002    pub async fn query_all_algo_orders(
1003        &self,
1004        params: &BinanceAllAlgoOrdersParams,
1005    ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesAlgoOrder>> {
1006        self.get("allAlgoOrders", Some(params), true, false).await
1007    }
1008
1009    /// Cancels all open algo orders for a symbol.
1010    ///
1011    /// # Errors
1012    ///
1013    /// Returns an error if the request fails.
1014    pub async fn cancel_all_algo_orders(
1015        &self,
1016        params: &BinanceCancelAllAlgoOrdersParams,
1017    ) -> BinanceFuturesHttpResult<BinanceCancelAllOrdersResponse> {
1018        self.request_delete("algoOpenOrders", Some(params), true, true)
1019            .await
1020    }
1021}
1022
1023/// Response wrapper for mark price endpoint.
1024#[derive(Debug, Deserialize)]
1025#[serde(untagged)]
1026enum MarkPriceResponse {
1027    Single(BinanceFuturesMarkPrice),
1028    Multiple(Vec<BinanceFuturesMarkPrice>),
1029}
1030
1031impl From<MarkPriceResponse> for Vec<BinanceFuturesMarkPrice> {
1032    fn from(response: MarkPriceResponse) -> Self {
1033        match response {
1034            MarkPriceResponse::Single(price) => vec![price],
1035            MarkPriceResponse::Multiple(prices) => prices,
1036        }
1037    }
1038}
1039
1040struct RateLimitConfig {
1041    default_quota: Option<Quota>,
1042    keyed_quotas: Vec<(String, Quota)>,
1043    order_keys: Vec<String>,
1044}
1045
1046/// In-memory cache entry for Binance Futures instruments.
1047#[derive(Clone, Debug)]
1048pub enum BinanceFuturesInstrument {
1049    /// USD-M futures symbol.
1050    UsdM(BinanceFuturesUsdSymbol),
1051    /// COIN-M futures symbol.
1052    CoinM(BinanceFuturesCoinSymbol),
1053}
1054
1055impl BinanceFuturesInstrument {
1056    /// Returns the symbol name for the instrument.
1057    #[must_use]
1058    pub const fn symbol(&self) -> Ustr {
1059        match self {
1060            Self::UsdM(s) => s.symbol,
1061            Self::CoinM(s) => s.symbol,
1062        }
1063    }
1064
1065    /// Returns the price precision for the instrument.
1066    #[must_use]
1067    pub const fn price_precision(&self) -> i32 {
1068        match self {
1069            Self::UsdM(s) => s.price_precision,
1070            Self::CoinM(s) => s.price_precision,
1071        }
1072    }
1073
1074    /// Returns the quantity precision for the instrument.
1075    #[must_use]
1076    pub const fn quantity_precision(&self) -> i32 {
1077        match self {
1078            Self::UsdM(s) => s.quantity_precision,
1079            Self::CoinM(s) => s.quantity_precision,
1080        }
1081    }
1082
1083    /// Returns the Nautilus-formatted instrument ID.
1084    #[must_use]
1085    pub fn id(&self) -> InstrumentId {
1086        match self {
1087            Self::UsdM(s) => format_instrument_id(&s.symbol, BinanceProductType::UsdM),
1088            Self::CoinM(s) => format_instrument_id(&s.symbol, BinanceProductType::CoinM),
1089        }
1090    }
1091
1092    /// Returns the quote currency for the instrument.
1093    #[must_use]
1094    pub fn quote_currency(&self) -> Currency {
1095        let quote_asset = match self {
1096            Self::UsdM(s) => &s.quote_asset,
1097            Self::CoinM(s) => &s.quote_asset,
1098        };
1099        Currency::from(quote_asset.as_str())
1100    }
1101}
1102
1103/// Binance Futures HTTP client for USD-M and COIN-M perpetuals.
1104#[derive(Debug, Clone)]
1105pub struct BinanceFuturesHttpClient {
1106    inner: Arc<BinanceRawFuturesHttpClient>,
1107    product_type: BinanceProductType,
1108    clock: &'static AtomicTime,
1109    instruments: Arc<DashMap<Ustr, BinanceFuturesInstrument>>,
1110    treat_expired_as_canceled: bool,
1111}
1112
1113impl BinanceFuturesHttpClient {
1114    /// Creates a new [`BinanceFuturesHttpClient`] instance.
1115    ///
1116    /// # Errors
1117    ///
1118    /// Returns an error if the product type is invalid or HTTP client creation fails.
1119    #[expect(clippy::too_many_arguments)]
1120    pub fn new(
1121        product_type: BinanceProductType,
1122        environment: BinanceEnvironment,
1123        clock: &'static AtomicTime,
1124        api_key: Option<String>,
1125        api_secret: Option<String>,
1126        base_url_override: Option<String>,
1127        recv_window: Option<u64>,
1128        timeout_secs: Option<u64>,
1129        proxy_url: Option<String>,
1130        treat_expired_as_canceled: bool,
1131    ) -> BinanceFuturesHttpResult<Self> {
1132        match product_type {
1133            BinanceProductType::UsdM | BinanceProductType::CoinM => {}
1134            _ => {
1135                return Err(BinanceFuturesHttpError::ValidationError(format!(
1136                    "BinanceFuturesHttpClient requires UsdM or CoinM product type, was {product_type:?}"
1137                )));
1138            }
1139        }
1140
1141        let raw = BinanceRawFuturesHttpClient::new(
1142            product_type,
1143            environment,
1144            api_key,
1145            api_secret,
1146            base_url_override,
1147            recv_window,
1148            timeout_secs,
1149            proxy_url,
1150        )?;
1151
1152        Ok(Self {
1153            inner: Arc::new(raw),
1154            product_type,
1155            clock,
1156            instruments: Arc::new(DashMap::new()),
1157            treat_expired_as_canceled,
1158        })
1159    }
1160
1161    /// Returns the product type (UsdM or CoinM).
1162    #[must_use]
1163    pub const fn product_type(&self) -> BinanceProductType {
1164        self.product_type
1165    }
1166
1167    /// Returns a reference to the inner raw HTTP client.
1168    #[must_use]
1169    pub fn inner(&self) -> &BinanceRawFuturesHttpClient {
1170        &self.inner
1171    }
1172
1173    /// Returns a clone of the instruments cache Arc.
1174    #[must_use]
1175    pub fn instruments_cache(&self) -> Arc<DashMap<Ustr, BinanceFuturesInstrument>> {
1176        Arc::clone(&self.instruments)
1177    }
1178
1179    /// Returns server time.
1180    ///
1181    /// # Errors
1182    ///
1183    /// Returns an error if the request fails.
1184    pub async fn server_time(&self) -> BinanceFuturesHttpResult<BinanceServerTime> {
1185        self.inner
1186            .get::<_, BinanceServerTime>("time", None::<&()>, false, false)
1187            .await
1188    }
1189
1190    /// Sets leverage for a symbol.
1191    ///
1192    /// # Errors
1193    ///
1194    /// Returns an error if the request fails.
1195    pub async fn set_leverage(
1196        &self,
1197        params: &BinanceSetLeverageParams,
1198    ) -> BinanceFuturesHttpResult<BinanceLeverageResponse> {
1199        self.inner.set_leverage(params).await
1200    }
1201
1202    /// Sets margin type for a symbol.
1203    ///
1204    /// # Errors
1205    ///
1206    /// Returns an error if the request fails.
1207    pub async fn set_margin_type(
1208        &self,
1209        params: &BinanceSetMarginTypeParams,
1210    ) -> BinanceFuturesHttpResult<serde_json::Value> {
1211        self.inner.set_margin_type(params).await
1212    }
1213
1214    /// Queries hedge mode (dual side position) setting.
1215    ///
1216    /// # Errors
1217    ///
1218    /// Returns an error if the request fails.
1219    pub async fn query_hedge_mode(&self) -> BinanceFuturesHttpResult<BinanceHedgeModeResponse> {
1220        self.inner.query_hedge_mode().await
1221    }
1222
1223    /// Creates a listen key for user data stream.
1224    ///
1225    /// # Errors
1226    ///
1227    /// Returns an error if the request fails.
1228    pub async fn create_listen_key(&self) -> BinanceFuturesHttpResult<ListenKeyResponse> {
1229        self.inner.create_listen_key().await
1230    }
1231
1232    /// Keeps alive an existing listen key.
1233    ///
1234    /// # Errors
1235    ///
1236    /// Returns an error if the request fails.
1237    pub async fn keepalive_listen_key(&self, listen_key: &str) -> BinanceFuturesHttpResult<()> {
1238        self.inner.keepalive_listen_key(listen_key).await
1239    }
1240
1241    /// Closes an existing listen key.
1242    ///
1243    /// # Errors
1244    ///
1245    /// Returns an error if the request fails.
1246    pub async fn close_listen_key(&self, listen_key: &str) -> BinanceFuturesHttpResult<()> {
1247        self.inner.close_listen_key(listen_key).await
1248    }
1249
1250    /// Fetches exchange information and populates the instrument cache.
1251    ///
1252    /// # Errors
1253    ///
1254    /// Returns an error if the request fails or the product type is invalid.
1255    pub async fn exchange_info(&self) -> BinanceFuturesHttpResult<()> {
1256        match self.product_type {
1257            BinanceProductType::UsdM => {
1258                let info: BinanceFuturesUsdExchangeInfo = self
1259                    .inner
1260                    .get("exchangeInfo", None::<&()>, false, false)
1261                    .await?;
1262
1263                for symbol in info.symbols {
1264                    self.instruments
1265                        .insert(symbol.symbol, BinanceFuturesInstrument::UsdM(symbol));
1266                }
1267            }
1268            BinanceProductType::CoinM => {
1269                let info: BinanceFuturesCoinExchangeInfo = self
1270                    .inner
1271                    .get("exchangeInfo", None::<&()>, false, false)
1272                    .await?;
1273
1274                for symbol in info.symbols {
1275                    self.instruments
1276                        .insert(symbol.symbol, BinanceFuturesInstrument::CoinM(symbol));
1277                }
1278            }
1279            _ => {
1280                return Err(BinanceFuturesHttpError::ValidationError(
1281                    "Invalid product type for futures".to_string(),
1282                ));
1283            }
1284        }
1285
1286        Ok(())
1287    }
1288
1289    /// Fetches exchange info and returns the current status of each symbol.
1290    ///
1291    /// Builds a fresh status snapshot from the response without disturbing the
1292    /// shared instruments cache, so a transient failure does not break other
1293    /// HTTP operations that depend on cached precision data.
1294    ///
1295    /// # Errors
1296    ///
1297    /// Returns an error if the request fails or the product type is invalid.
1298    pub async fn request_symbol_statuses(
1299        &self,
1300    ) -> BinanceFuturesHttpResult<AHashMap<Ustr, MarketStatusAction>> {
1301        let mut statuses = AHashMap::new();
1302
1303        match self.product_type {
1304            BinanceProductType::UsdM => {
1305                let info: BinanceFuturesUsdExchangeInfo = self
1306                    .inner
1307                    .get("exchangeInfo", None::<&()>, false, false)
1308                    .await?;
1309
1310                for symbol in &info.symbols {
1311                    statuses.insert(symbol.symbol, MarketStatusAction::from(symbol.status));
1312                }
1313            }
1314            BinanceProductType::CoinM => {
1315                let info: BinanceFuturesCoinExchangeInfo = self
1316                    .inner
1317                    .get("exchangeInfo", None::<&()>, false, false)
1318                    .await?;
1319
1320                for symbol in &info.symbols {
1321                    let action = symbol
1322                        .contract_status
1323                        .map_or(MarketStatusAction::NotAvailableForTrading, Into::into);
1324                    statuses.insert(symbol.symbol, action);
1325                }
1326            }
1327            _ => {
1328                return Err(BinanceFuturesHttpError::ValidationError(
1329                    "Invalid product type for futures".to_string(),
1330                ));
1331            }
1332        }
1333
1334        Ok(statuses)
1335    }
1336
1337    /// Fetches exchange information and returns parsed Nautilus instruments.
1338    ///
1339    /// # Errors
1340    ///
1341    /// Returns an error if the request fails or the product type is invalid.
1342    pub async fn request_instruments(&self) -> BinanceFuturesHttpResult<Vec<InstrumentAny>> {
1343        let ts_init = UnixNanos::default();
1344
1345        let instruments = match self.product_type {
1346            BinanceProductType::UsdM => {
1347                let info: BinanceFuturesUsdExchangeInfo = self
1348                    .inner
1349                    .get("exchangeInfo", None::<&()>, false, false)
1350                    .await?;
1351
1352                let mut instruments = Vec::with_capacity(info.symbols.len());
1353
1354                for symbol in info.symbols {
1355                    // Cache symbol for precision lookups
1356                    self.instruments.insert(
1357                        symbol.symbol,
1358                        BinanceFuturesInstrument::UsdM(symbol.clone()),
1359                    );
1360
1361                    match parse_usdm_instrument(&symbol, ts_init, ts_init) {
1362                        Ok(instrument) => instruments.push(instrument),
1363                        Err(e) => {
1364                            log::debug!(
1365                                "Skipping symbol during instrument parsing: symbol={}, error={e}",
1366                                symbol.symbol
1367                            );
1368                        }
1369                    }
1370                }
1371
1372                log::info!(
1373                    "Loaded USD-M perpetual instruments: count={}",
1374                    instruments.len()
1375                );
1376                instruments
1377            }
1378            BinanceProductType::CoinM => {
1379                let info: BinanceFuturesCoinExchangeInfo = self
1380                    .inner
1381                    .get("exchangeInfo", None::<&()>, false, false)
1382                    .await?;
1383
1384                let mut instruments = Vec::with_capacity(info.symbols.len());
1385                for symbol in info.symbols {
1386                    // Cache symbol for precision lookups
1387                    self.instruments.insert(
1388                        symbol.symbol,
1389                        BinanceFuturesInstrument::CoinM(symbol.clone()),
1390                    );
1391
1392                    match parse_coinm_instrument(&symbol, ts_init, ts_init) {
1393                        Ok(instrument) => instruments.push(instrument),
1394                        Err(e) => {
1395                            log::debug!(
1396                                "Skipping symbol during instrument parsing: symbol={}, error={e}",
1397                                symbol.symbol
1398                            );
1399                        }
1400                    }
1401                }
1402
1403                log::info!(
1404                    "Loaded COIN-M perpetual instruments: count={}",
1405                    instruments.len()
1406                );
1407                instruments
1408            }
1409            _ => {
1410                return Err(BinanceFuturesHttpError::ValidationError(
1411                    "Invalid product type for futures".to_string(),
1412                ));
1413            }
1414        };
1415
1416        Ok(instruments)
1417    }
1418
1419    /// Fetches 24hr ticker statistics.
1420    ///
1421    /// # Errors
1422    ///
1423    /// Returns an error if the request fails.
1424    pub async fn ticker_24h(
1425        &self,
1426        params: &BinanceTicker24hrParams,
1427    ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesTicker24hr>> {
1428        self.inner.ticker_24h(params).await
1429    }
1430
1431    /// Fetches best bid/ask prices.
1432    ///
1433    /// # Errors
1434    ///
1435    /// Returns an error if the request fails.
1436    pub async fn book_ticker(
1437        &self,
1438        params: &BinanceBookTickerParams,
1439    ) -> BinanceFuturesHttpResult<Vec<BinanceBookTicker>> {
1440        self.inner.book_ticker(params).await
1441    }
1442
1443    /// Fetches price ticker.
1444    ///
1445    /// # Errors
1446    ///
1447    /// Returns an error if the request fails.
1448    pub async fn price_ticker(
1449        &self,
1450        symbol: Option<&str>,
1451    ) -> BinanceFuturesHttpResult<Vec<BinancePriceTicker>> {
1452        self.inner.price_ticker(symbol).await
1453    }
1454
1455    /// Fetches order book depth.
1456    ///
1457    /// # Errors
1458    ///
1459    /// Returns an error if the request fails.
1460    pub async fn depth(
1461        &self,
1462        params: &BinanceDepthParams,
1463    ) -> BinanceFuturesHttpResult<BinanceOrderBook> {
1464        self.inner.depth(params).await
1465    }
1466
1467    /// Fetches mark price and funding rate.
1468    ///
1469    /// # Errors
1470    ///
1471    /// Returns an error if the request fails.
1472    pub async fn mark_price(
1473        &self,
1474        params: &BinanceMarkPriceParams,
1475    ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesMarkPrice>> {
1476        self.inner.mark_price(params).await
1477    }
1478
1479    /// Fetches funding rate history.
1480    ///
1481    /// # Errors
1482    ///
1483    /// Returns an error if the request fails.
1484    pub async fn funding_rate(
1485        &self,
1486        params: &BinanceFundingRateParams,
1487    ) -> BinanceFuturesHttpResult<Vec<BinanceFundingRate>> {
1488        self.inner.funding_rate(params).await
1489    }
1490
1491    /// Fetches current open interest for a symbol.
1492    ///
1493    /// # Errors
1494    ///
1495    /// Returns an error if the request fails.
1496    pub async fn open_interest(
1497        &self,
1498        params: &BinanceOpenInterestParams,
1499    ) -> BinanceFuturesHttpResult<BinanceOpenInterest> {
1500        self.inner.open_interest(params).await
1501    }
1502
1503    /// Queries a single order by order ID or client order ID.
1504    ///
1505    /// # Errors
1506    ///
1507    /// Returns an error if the request fails.
1508    pub async fn query_order(
1509        &self,
1510        params: &BinanceOrderQueryParams,
1511    ) -> BinanceFuturesHttpResult<BinanceFuturesOrder> {
1512        self.inner.query_order(params).await
1513    }
1514
1515    /// Queries all open orders.
1516    ///
1517    /// # Errors
1518    ///
1519    /// Returns an error if the request fails.
1520    pub async fn query_open_orders(
1521        &self,
1522        params: &BinanceOpenOrdersParams,
1523    ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesOrder>> {
1524        self.inner.query_open_orders(params).await
1525    }
1526
1527    /// Queries all orders (including historical).
1528    ///
1529    /// # Errors
1530    ///
1531    /// Returns an error if the request fails.
1532    pub async fn query_all_orders(
1533        &self,
1534        params: &BinanceAllOrdersParams,
1535    ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesOrder>> {
1536        self.inner.query_all_orders(params).await
1537    }
1538
1539    /// Fetches account information including balances and positions.
1540    ///
1541    /// # Errors
1542    ///
1543    /// Returns an error if the request fails.
1544    pub async fn query_account(&self) -> BinanceFuturesHttpResult<BinanceFuturesAccountInfo> {
1545        self.inner.query_account().await
1546    }
1547
1548    /// Fetches position risk information.
1549    ///
1550    /// # Errors
1551    ///
1552    /// Returns an error if the request fails.
1553    pub async fn query_positions(
1554        &self,
1555        params: &BinancePositionRiskParams,
1556    ) -> BinanceFuturesHttpResult<Vec<BinancePositionRisk>> {
1557        self.inner.query_positions(params).await
1558    }
1559
1560    /// Fetches user trades for a symbol.
1561    ///
1562    /// # Errors
1563    ///
1564    /// Returns an error if the request fails.
1565    pub async fn query_user_trades(
1566        &self,
1567        params: &BinanceUserTradesParams,
1568    ) -> BinanceFuturesHttpResult<Vec<BinanceUserTrade>> {
1569        self.inner.query_user_trades(params).await
1570    }
1571
1572    /// Submits a new order.
1573    ///
1574    /// # Errors
1575    ///
1576    /// Returns an error if:
1577    /// - The instrument is not cached.
1578    /// - The order type or time-in-force is unsupported.
1579    /// - Stop orders are submitted without a trigger price.
1580    /// - The request fails.
1581    #[expect(clippy::too_many_arguments)]
1582    pub async fn submit_order(
1583        &self,
1584        account_id: AccountId,
1585        instrument_id: InstrumentId,
1586        client_order_id: ClientOrderId,
1587        order_side: OrderSide,
1588        order_type: OrderType,
1589        quantity: Quantity,
1590        time_in_force: TimeInForce,
1591        price: Option<Price>,
1592        trigger_price: Option<Price>,
1593        reduce_only: bool,
1594        post_only: bool,
1595        position_side: Option<BinancePositionSide>,
1596        price_match: Option<BinancePriceMatch>,
1597    ) -> anyhow::Result<OrderStatusReport> {
1598        let symbol = format_binance_symbol(&instrument_id);
1599        let size_precision = self.get_size_precision(&symbol)?;
1600
1601        let binance_side = BinanceSide::try_from(order_side)?;
1602        let binance_order_type = order_type_to_binance_futures(order_type)?;
1603        let binance_tif = if post_only {
1604            BinanceTimeInForce::Gtx
1605        } else {
1606            BinanceTimeInForce::try_from(time_in_force)?
1607        };
1608
1609        let requires_trigger_price = matches!(
1610            order_type,
1611            OrderType::StopMarket
1612                | OrderType::StopLimit
1613                | OrderType::TrailingStopMarket
1614                | OrderType::MarketIfTouched
1615                | OrderType::LimitIfTouched
1616        );
1617
1618        if requires_trigger_price && trigger_price.is_none() {
1619            anyhow::bail!("Order type {order_type:?} requires a trigger price");
1620        }
1621
1622        // MARKET and STOP_MARKET orders don't accept timeInForce
1623        let requires_time_in_force = matches!(
1624            order_type,
1625            OrderType::Limit | OrderType::StopLimit | OrderType::LimitIfTouched
1626        );
1627
1628        let qty_str = quantity.to_string();
1629        let price_str = if price_match.is_some() {
1630            None
1631        } else {
1632            price.map(|p| p.to_string())
1633        };
1634        let stop_price_str = trigger_price.map(|p| p.to_string());
1635        let client_id_str = encode_broker_id(&client_order_id, BINANCE_NAUTILUS_FUTURES_BROKER_ID);
1636
1637        let params = BinanceNewOrderParams {
1638            symbol,
1639            side: binance_side,
1640            order_type: binance_order_type,
1641            time_in_force: if requires_time_in_force {
1642                Some(binance_tif)
1643            } else {
1644                None
1645            },
1646            quantity: Some(qty_str),
1647            price: price_str,
1648            new_client_order_id: Some(client_id_str),
1649            stop_price: stop_price_str,
1650            reduce_only: if reduce_only { Some(true) } else { None },
1651            position_side,
1652            close_position: None,
1653            activation_price: None,
1654            callback_rate: None,
1655            working_type: None,
1656            price_protect: None,
1657            new_order_resp_type: None,
1658            good_till_date: None,
1659            recv_window: None,
1660            price_match,
1661            self_trade_prevention_mode: None,
1662        };
1663
1664        let order = self.inner.submit_order(&params).await?;
1665        let ts_init = self.clock.get_time_ns();
1666        order.to_order_status_report(
1667            account_id,
1668            instrument_id,
1669            size_precision,
1670            self.treat_expired_as_canceled,
1671            ts_init,
1672        )
1673    }
1674
1675    /// Submits an algo order (conditional order) to the Binance Algo Service.
1676    ///
1677    /// As of 2025-12-09, Binance migrated conditional order types to the Algo Service API.
1678    /// This method handles StopMarket, StopLimit, MarketIfTouched, LimitIfTouched,
1679    /// and TrailingStopMarket orders.
1680    ///
1681    /// # Errors
1682    ///
1683    /// Returns an error if:
1684    /// - The order type requires a trigger price but none is provided.
1685    /// - The instrument is not cached.
1686    /// - The request fails.
1687    #[expect(clippy::too_many_arguments)]
1688    pub async fn submit_algo_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        close_position: bool,
1701        position_side: Option<BinancePositionSide>,
1702        activation_price: Option<Price>,
1703        callback_rate: Option<String>,
1704        working_type: Option<BinanceWorkingType>,
1705    ) -> anyhow::Result<OrderStatusReport> {
1706        let symbol = format_binance_symbol(&instrument_id);
1707        let size_precision = self.get_size_precision(&symbol)?;
1708
1709        let binance_side = BinanceSide::try_from(order_side)?;
1710        let binance_order_type = order_type_to_binance_futures(order_type)?;
1711        let binance_tif = BinanceTimeInForce::try_from(time_in_force)?;
1712
1713        anyhow::ensure!(
1714            trigger_price.is_some(),
1715            "Algo order type {order_type:?} requires a trigger price"
1716        );
1717
1718        // Limit orders require time in force
1719        let requires_time_in_force =
1720            matches!(order_type, OrderType::StopLimit | OrderType::LimitIfTouched);
1721
1722        let price_str = price.map(|p| p.to_string());
1723        let trigger_price_str = trigger_price.map(|p| p.to_string());
1724        let client_id_str = encode_broker_id(&client_order_id, BINANCE_NAUTILUS_FUTURES_BROKER_ID);
1725
1726        // closePosition is mutually exclusive with quantity and reduceOnly
1727        let params = if close_position {
1728            BinanceNewAlgoOrderParams {
1729                symbol,
1730                side: binance_side,
1731                order_type: binance_order_type,
1732                algo_type: BinanceAlgoType::Conditional,
1733                position_side,
1734                quantity: None,
1735                price: price_str,
1736                trigger_price: trigger_price_str,
1737                time_in_force: if requires_time_in_force {
1738                    Some(binance_tif)
1739                } else {
1740                    None
1741                },
1742                working_type,
1743                close_position: Some(true),
1744                price_protect: None,
1745                reduce_only: None,
1746                activation_price: activation_price.map(|p| p.to_string()),
1747                callback_rate,
1748                client_algo_id: Some(client_id_str),
1749                good_till_date: None,
1750                recv_window: None,
1751            }
1752        } else {
1753            let qty_str = quantity.to_string();
1754            BinanceNewAlgoOrderParams {
1755                symbol,
1756                side: binance_side,
1757                order_type: binance_order_type,
1758                algo_type: BinanceAlgoType::Conditional,
1759                position_side,
1760                quantity: Some(qty_str),
1761                price: price_str,
1762                trigger_price: trigger_price_str,
1763                time_in_force: if requires_time_in_force {
1764                    Some(binance_tif)
1765                } else {
1766                    None
1767                },
1768                working_type,
1769                close_position: None,
1770                price_protect: None,
1771                reduce_only: if reduce_only { Some(true) } else { None },
1772                activation_price: activation_price.map(|p| p.to_string()),
1773                callback_rate,
1774                client_algo_id: Some(client_id_str),
1775                good_till_date: None,
1776                recv_window: None,
1777            }
1778        };
1779
1780        let order = self.inner.submit_algo_order(&params).await?;
1781        let ts_init = self.clock.get_time_ns();
1782        order.to_order_status_report(account_id, instrument_id, size_precision, ts_init)
1783    }
1784
1785    /// Submits multiple orders in a single request (up to 5 orders).
1786    ///
1787    /// Each order in the batch is processed independently. The response contains
1788    /// the result for each order, which can be either a success or an error.
1789    ///
1790    /// # Errors
1791    ///
1792    /// Returns an error if the batch exceeds 5 orders or the request fails.
1793    pub async fn submit_order_list(
1794        &self,
1795        orders: &[BatchOrderItem],
1796    ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
1797        self.inner.submit_order_list(orders).await
1798    }
1799
1800    /// Modifies an existing order (price and quantity only).
1801    ///
1802    /// Either `venue_order_id` or `client_order_id` must be provided.
1803    ///
1804    /// # Errors
1805    ///
1806    /// Returns an error if:
1807    /// - Neither venue_order_id nor client_order_id is provided.
1808    /// - The instrument is not cached.
1809    /// - The request fails.
1810    #[expect(clippy::too_many_arguments)]
1811    pub async fn modify_order(
1812        &self,
1813        account_id: AccountId,
1814        instrument_id: InstrumentId,
1815        venue_order_id: Option<VenueOrderId>,
1816        client_order_id: Option<ClientOrderId>,
1817        order_side: OrderSide,
1818        quantity: Quantity,
1819        price: Price,
1820    ) -> anyhow::Result<OrderStatusReport> {
1821        anyhow::ensure!(
1822            venue_order_id.is_some() || client_order_id.is_some(),
1823            "Either venue_order_id or client_order_id must be provided"
1824        );
1825
1826        let symbol = format_binance_symbol(&instrument_id);
1827        let size_precision = self.get_size_precision(&symbol)?;
1828
1829        let binance_side = BinanceSide::try_from(order_side)?;
1830
1831        let order_id = venue_order_id
1832            .map(|id| id.inner().parse::<i64>())
1833            .transpose()
1834            .map_err(|_| anyhow::anyhow!("Invalid venue order ID"))?;
1835
1836        let params = BinanceModifyOrderParams {
1837            symbol,
1838            order_id,
1839            orig_client_order_id: client_order_id
1840                .map(|id| encode_broker_id(&id, BINANCE_NAUTILUS_FUTURES_BROKER_ID)),
1841            side: binance_side,
1842            quantity: quantity.to_string(),
1843            price: price.to_string(),
1844            recv_window: None,
1845        };
1846
1847        let order = self.inner.modify_order(&params).await?;
1848        let ts_init = self.clock.get_time_ns();
1849        order.to_order_status_report(
1850            account_id,
1851            instrument_id,
1852            size_precision,
1853            self.treat_expired_as_canceled,
1854            ts_init,
1855        )
1856    }
1857
1858    /// Modifies multiple orders in a single request (up to 5 orders).
1859    ///
1860    /// Each modify in the batch is processed independently. The response contains
1861    /// the result for each modify, which can be either a success or an error.
1862    ///
1863    /// # Errors
1864    ///
1865    /// Returns an error if the batch exceeds 5 orders or the request fails.
1866    pub async fn batch_modify_orders(
1867        &self,
1868        modifies: &[BatchModifyItem],
1869    ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
1870        self.inner.batch_modify_orders(modifies).await
1871    }
1872
1873    /// Cancels an order by venue order ID or client order ID.
1874    ///
1875    /// Either `venue_order_id` or `client_order_id` must be provided.
1876    ///
1877    /// # Errors
1878    ///
1879    /// Returns an error if:
1880    /// - Neither venue_order_id nor client_order_id is provided.
1881    /// - The request fails.
1882    pub async fn cancel_order(
1883        &self,
1884        instrument_id: InstrumentId,
1885        venue_order_id: Option<VenueOrderId>,
1886        client_order_id: Option<ClientOrderId>,
1887    ) -> anyhow::Result<VenueOrderId> {
1888        anyhow::ensure!(
1889            venue_order_id.is_some() || client_order_id.is_some(),
1890            "Either venue_order_id or client_order_id must be provided"
1891        );
1892
1893        let symbol = format_binance_symbol(&instrument_id);
1894
1895        let order_id = venue_order_id
1896            .map(|id| id.inner().parse::<i64>())
1897            .transpose()
1898            .map_err(|_| anyhow::anyhow!("Invalid venue order ID"))?;
1899
1900        let params = BinanceCancelOrderParams {
1901            symbol,
1902            order_id,
1903            orig_client_order_id: client_order_id
1904                .map(|id| encode_broker_id(&id, BINANCE_NAUTILUS_FUTURES_BROKER_ID)),
1905            recv_window: None,
1906        };
1907
1908        let order = self.inner.cancel_order(&params).await?;
1909        Ok(VenueOrderId::new(order.order_id.to_string()))
1910    }
1911
1912    /// Cancels an algo order (conditional order) via the Binance Algo Service.
1913    ///
1914    /// Use the `client_algo_id` which corresponds to the `client_order_id` used
1915    /// when submitting the algo order.
1916    ///
1917    /// # Errors
1918    ///
1919    /// Returns an error if the request fails.
1920    pub async fn cancel_algo_order(&self, client_order_id: ClientOrderId) -> anyhow::Result<()> {
1921        let params = BinanceAlgoOrderQueryParams {
1922            algo_id: None,
1923            client_algo_id: Some(encode_broker_id(
1924                &client_order_id,
1925                BINANCE_NAUTILUS_FUTURES_BROKER_ID,
1926            )),
1927            recv_window: None,
1928        };
1929
1930        let response = self.inner.cancel_algo_order(&params).await?;
1931        if response.code.parse::<i32>().unwrap_or(0) == 200 {
1932            Ok(())
1933        } else {
1934            anyhow::bail!(
1935                "Cancel algo order failed: code={}, msg={}",
1936                response.code,
1937                response.msg
1938            )
1939        }
1940    }
1941
1942    /// Cancels all open orders for a symbol.
1943    ///
1944    /// # Errors
1945    ///
1946    /// Returns an error if the request fails.
1947    pub async fn cancel_all_orders(
1948        &self,
1949        instrument_id: InstrumentId,
1950    ) -> anyhow::Result<Vec<VenueOrderId>> {
1951        let symbol = format_binance_symbol(&instrument_id);
1952
1953        let params = BinanceCancelAllOrdersParams {
1954            symbol,
1955            recv_window: None,
1956        };
1957
1958        let response = self.inner.cancel_all_orders(&params).await?;
1959        if response.code == 200 {
1960            Ok(vec![])
1961        } else {
1962            anyhow::bail!("Cancel all orders failed: {}", response.msg);
1963        }
1964    }
1965
1966    /// Cancels all open algo orders for a symbol.
1967    ///
1968    /// # Errors
1969    ///
1970    /// Returns an error if the request fails.
1971    pub async fn cancel_all_algo_orders(&self, instrument_id: InstrumentId) -> anyhow::Result<()> {
1972        let symbol = format_binance_symbol(&instrument_id);
1973
1974        let params = BinanceCancelAllAlgoOrdersParams {
1975            symbol,
1976            recv_window: None,
1977        };
1978
1979        let response = self.inner.cancel_all_algo_orders(&params).await?;
1980        if response.code == 200 {
1981            Ok(())
1982        } else {
1983            anyhow::bail!("Cancel all algo orders failed: {}", response.msg);
1984        }
1985    }
1986
1987    /// Cancels multiple orders in a single request (up to 5 orders).
1988    ///
1989    /// Each cancel in the batch is processed independently. The response contains
1990    /// the result for each cancel, which can be either a success or an error.
1991    ///
1992    /// # Errors
1993    ///
1994    /// Returns an error if the batch exceeds 5 orders or the request fails.
1995    pub async fn batch_cancel_orders(
1996        &self,
1997        cancels: &[BatchCancelItem],
1998    ) -> BinanceFuturesHttpResult<Vec<BatchOrderResult>> {
1999        self.inner.batch_cancel_orders(cancels).await
2000    }
2001
2002    /// Queries open algo orders (conditional orders).
2003    ///
2004    /// Returns all open algo orders, optionally filtered by symbol.
2005    ///
2006    /// # Errors
2007    ///
2008    /// Returns an error if the request fails.
2009    pub async fn query_open_algo_orders(
2010        &self,
2011        instrument_id: Option<InstrumentId>,
2012    ) -> BinanceFuturesHttpResult<Vec<BinanceFuturesAlgoOrder>> {
2013        let symbol = instrument_id.map(|id| format_binance_symbol(&id));
2014
2015        let params = BinanceOpenAlgoOrdersParams {
2016            symbol,
2017            recv_window: None,
2018        };
2019
2020        self.inner.query_open_algo_orders(&params).await
2021    }
2022
2023    /// Queries a single algo order by client_order_id.
2024    ///
2025    /// # Errors
2026    ///
2027    /// Returns an error if the request fails.
2028    pub async fn query_algo_order(
2029        &self,
2030        client_order_id: ClientOrderId,
2031    ) -> BinanceFuturesHttpResult<BinanceFuturesAlgoOrder> {
2032        let params = BinanceAlgoOrderQueryParams {
2033            algo_id: None,
2034            client_algo_id: Some(encode_broker_id(
2035                &client_order_id,
2036                BINANCE_NAUTILUS_FUTURES_BROKER_ID,
2037            )),
2038            recv_window: None,
2039        };
2040
2041        self.inner.query_algo_order(&params).await
2042    }
2043
2044    /// Returns the size precision for an instrument from the cache.
2045    fn get_size_precision(&self, symbol: &str) -> anyhow::Result<u8> {
2046        let instrument = self
2047            .instruments
2048            .get(&Ustr::from(symbol))
2049            .ok_or_else(|| anyhow::anyhow!("Instrument not found in cache: {symbol}"))?;
2050
2051        let precision = match instrument.value() {
2052            BinanceFuturesInstrument::UsdM(s) => s.quantity_precision,
2053            BinanceFuturesInstrument::CoinM(s) => s.quantity_precision,
2054        };
2055
2056        Ok(precision as u8)
2057    }
2058
2059    /// Returns the price precision for an instrument from the cache.
2060    fn get_price_precision(&self, symbol: &str) -> anyhow::Result<u8> {
2061        let instrument = self
2062            .instruments
2063            .get(&Ustr::from(symbol))
2064            .ok_or_else(|| anyhow::anyhow!("Instrument not found in cache: {symbol}"))?;
2065
2066        let precision = match instrument.value() {
2067            BinanceFuturesInstrument::UsdM(s) => s.price_precision,
2068            BinanceFuturesInstrument::CoinM(s) => s.price_precision,
2069        };
2070
2071        Ok(precision as u8)
2072    }
2073
2074    /// Requests the current account state.
2075    ///
2076    /// # Errors
2077    ///
2078    /// Returns an error if the request fails or parsing fails.
2079    pub async fn request_account_state(
2080        &self,
2081        account_id: AccountId,
2082    ) -> anyhow::Result<AccountState> {
2083        let ts_init = UnixNanos::default();
2084        let account_info = self.inner.query_account().await?;
2085        account_info.to_account_state(account_id, ts_init)
2086    }
2087
2088    /// Requests a single order status report.
2089    ///
2090    /// Either `venue_order_id` or `client_order_id` must be provided.
2091    ///
2092    /// # Errors
2093    ///
2094    /// Returns an error if the request fails or parsing fails.
2095    pub async fn request_order_status_report(
2096        &self,
2097        account_id: AccountId,
2098        instrument_id: InstrumentId,
2099        venue_order_id: Option<VenueOrderId>,
2100        client_order_id: Option<ClientOrderId>,
2101    ) -> anyhow::Result<OrderStatusReport> {
2102        anyhow::ensure!(
2103            venue_order_id.is_some() || client_order_id.is_some(),
2104            "Either venue_order_id or client_order_id must be provided"
2105        );
2106
2107        let symbol = format_binance_symbol(&instrument_id);
2108        let size_precision = self.get_size_precision(&symbol)?;
2109
2110        let order_id = venue_order_id
2111            .map(|id| id.inner().parse::<i64>())
2112            .transpose()
2113            .map_err(|_| anyhow::anyhow!("Invalid venue order ID"))?;
2114
2115        let orig_client_order_id =
2116            client_order_id.map(|id| encode_broker_id(&id, BINANCE_NAUTILUS_FUTURES_BROKER_ID));
2117
2118        let params = BinanceOrderQueryParams {
2119            symbol,
2120            order_id,
2121            orig_client_order_id,
2122            recv_window: None,
2123        };
2124
2125        let order = self.inner.query_order(&params).await?;
2126        let ts_init = self.clock.get_time_ns();
2127        order.to_order_status_report(
2128            account_id,
2129            instrument_id,
2130            size_precision,
2131            self.treat_expired_as_canceled,
2132            ts_init,
2133        )
2134    }
2135
2136    /// Requests order status reports for open orders.
2137    ///
2138    /// If `instrument_id` is None, returns all open orders.
2139    ///
2140    /// # Errors
2141    ///
2142    /// Returns an error if the request fails or parsing fails.
2143    pub async fn request_order_status_reports(
2144        &self,
2145        account_id: AccountId,
2146        instrument_id: Option<InstrumentId>,
2147        open_only: bool,
2148    ) -> anyhow::Result<Vec<OrderStatusReport>> {
2149        let symbol = instrument_id.map(|id| format_binance_symbol(&id));
2150
2151        let orders = if open_only {
2152            let params = BinanceOpenOrdersParams {
2153                symbol: symbol.clone(),
2154                recv_window: None,
2155            };
2156            self.inner.query_open_orders(&params).await?
2157        } else {
2158            // For historical orders, symbol is required
2159            let symbol = symbol.ok_or_else(|| {
2160                anyhow::anyhow!("instrument_id is required for historical orders")
2161            })?;
2162            let params = BinanceAllOrdersParams {
2163                symbol,
2164                order_id: None,
2165                start_time: None,
2166                end_time: None,
2167                limit: None,
2168                recv_window: None,
2169            };
2170            self.inner.query_all_orders(&params).await?
2171        };
2172
2173        let ts_init = self.clock.get_time_ns();
2174        let mut reports = Vec::with_capacity(orders.len());
2175
2176        for order in orders {
2177            let order_instrument_id = instrument_id.unwrap_or_else(|| {
2178                // Build instrument ID from order symbol
2179                let suffix = self.product_type.suffix();
2180                InstrumentId::from(format!("{}{}.BINANCE", order.symbol, suffix))
2181            });
2182
2183            let size_precision = self.get_size_precision(&order.symbol).unwrap_or(8); // Default precision if not in cache
2184
2185            match order.to_order_status_report(
2186                account_id,
2187                order_instrument_id,
2188                size_precision,
2189                self.treat_expired_as_canceled,
2190                ts_init,
2191            ) {
2192                Ok(report) => reports.push(report),
2193                Err(e) => {
2194                    log::warn!("Failed to parse order status report: {e}");
2195                }
2196            }
2197        }
2198
2199        Ok(reports)
2200    }
2201
2202    /// Requests fill reports for a symbol.
2203    ///
2204    /// # Errors
2205    ///
2206    /// Returns an error if the request fails or parsing fails.
2207    pub async fn request_fill_reports(
2208        &self,
2209        account_id: AccountId,
2210        instrument_id: InstrumentId,
2211        venue_order_id: Option<VenueOrderId>,
2212        start: Option<i64>,
2213        end: Option<i64>,
2214        limit: Option<u32>,
2215    ) -> anyhow::Result<Vec<FillReport>> {
2216        let symbol = format_binance_symbol(&instrument_id);
2217        let size_precision = self.get_size_precision(&symbol)?;
2218        let price_precision = self.get_price_precision(&symbol)?;
2219
2220        let order_id = venue_order_id
2221            .map(|id| id.inner().parse::<i64>())
2222            .transpose()
2223            .map_err(|_| anyhow::anyhow!("Invalid venue order ID"))?;
2224
2225        let params = BinanceUserTradesParams {
2226            symbol,
2227            order_id,
2228            start_time: start,
2229            end_time: end,
2230            from_id: None,
2231            limit,
2232            recv_window: None,
2233        };
2234
2235        let trades = self.inner.query_user_trades(&params).await?;
2236
2237        let ts_init = self.clock.get_time_ns();
2238        let mut reports = Vec::with_capacity(trades.len());
2239
2240        for trade in trades {
2241            match trade.to_fill_report(
2242                account_id,
2243                instrument_id,
2244                price_precision,
2245                size_precision,
2246                ts_init,
2247            ) {
2248                Ok(report) => reports.push(report),
2249                Err(e) => {
2250                    log::warn!("Failed to parse fill report: {e}");
2251                }
2252            }
2253        }
2254
2255        Ok(reports)
2256    }
2257
2258    /// Requests recent public trades for an instrument.
2259    ///
2260    /// # Errors
2261    ///
2262    /// Returns an error if the request fails, instrument is not cached, or parsing fails.
2263    pub async fn request_trades(
2264        &self,
2265        instrument_id: InstrumentId,
2266        limit: Option<u32>,
2267    ) -> anyhow::Result<Vec<TradeTick>> {
2268        let symbol = format_binance_symbol(&instrument_id);
2269        let size_precision = self.get_size_precision(&symbol)?;
2270        let price_precision = self.get_price_precision(&symbol)?;
2271
2272        let params = BinanceTradesParams { symbol, limit };
2273
2274        let trades = self.inner.trades(&params).await?;
2275        let ts_init = UnixNanos::default();
2276
2277        let mut result = Vec::with_capacity(trades.len());
2278        for trade in trades {
2279            let price: f64 = trade.price.parse().unwrap_or(0.0);
2280            let size: f64 = trade.qty.parse().unwrap_or(0.0);
2281            let ts_event = UnixNanos::from_millis(trade.time as u64);
2282
2283            let aggressor_side = if trade.is_buyer_maker {
2284                AggressorSide::Seller
2285            } else {
2286                AggressorSide::Buyer
2287            };
2288
2289            let tick = TradeTick::new(
2290                instrument_id,
2291                Price::new(price, price_precision),
2292                Quantity::new(size, size_precision),
2293                aggressor_side,
2294                TradeId::new(trade.id.to_string()),
2295                ts_event,
2296                ts_init,
2297            );
2298            result.push(tick);
2299        }
2300
2301        Ok(result)
2302    }
2303
2304    /// Requests bar (kline/candlestick) data for an instrument.
2305    ///
2306    /// # Errors
2307    ///
2308    /// Returns an error if the bar type is not supported, instrument is not cached,
2309    /// or the request fails.
2310    pub async fn request_bars(
2311        &self,
2312        bar_type: BarType,
2313        start: Option<DateTime<Utc>>,
2314        end: Option<DateTime<Utc>>,
2315        limit: Option<u32>,
2316    ) -> anyhow::Result<Vec<Bar>> {
2317        anyhow::ensure!(
2318            bar_type.aggregation_source() == AggregationSource::External,
2319            "Only EXTERNAL aggregation is supported"
2320        );
2321
2322        let spec = bar_type.spec();
2323        let step = spec.step.get();
2324        let interval = match spec.aggregation {
2325            BarAggregation::Second => {
2326                anyhow::bail!("Binance Futures does not support second-level kline intervals")
2327            }
2328            BarAggregation::Minute => format!("{step}m"),
2329            BarAggregation::Hour => format!("{step}h"),
2330            BarAggregation::Day => format!("{step}d"),
2331            BarAggregation::Week => format!("{step}w"),
2332            BarAggregation::Month => format!("{step}M"),
2333            a => anyhow::bail!("Binance Futures does not support {a:?} aggregation"),
2334        };
2335
2336        let symbol = format_binance_symbol(&bar_type.instrument_id());
2337        let price_precision = self.get_price_precision(&symbol)?;
2338        let size_precision = self.get_size_precision(&symbol)?;
2339
2340        let params = BinanceKlinesParams {
2341            symbol,
2342            interval,
2343            start_time: start.map(|dt| dt.timestamp_millis()),
2344            end_time: end.map(|dt| dt.timestamp_millis()),
2345            limit,
2346        };
2347
2348        let klines = self.inner.klines(&params).await?;
2349        let ts_init = UnixNanos::default();
2350
2351        let mut result = Vec::with_capacity(klines.len());
2352        for kline in klines {
2353            let open: f64 = kline.open.parse().unwrap_or(0.0);
2354            let high: f64 = kline.high.parse().unwrap_or(0.0);
2355            let low: f64 = kline.low.parse().unwrap_or(0.0);
2356            let close: f64 = kline.close.parse().unwrap_or(0.0);
2357            let volume: f64 = kline.volume.parse().unwrap_or(0.0);
2358
2359            // close_time is end of interval, add 1ms for next bar's open
2360            let ts_event = UnixNanos::from_millis(kline.close_time as u64);
2361
2362            let bar = Bar::new(
2363                bar_type,
2364                Price::new(open, price_precision),
2365                Price::new(high, price_precision),
2366                Price::new(low, price_precision),
2367                Price::new(close, price_precision),
2368                Quantity::new(volume, size_precision),
2369                ts_event,
2370                ts_init,
2371            );
2372            result.push(bar);
2373        }
2374
2375        Ok(result)
2376    }
2377}
2378
2379/// Checks if an order type requires the Binance Algo Service API.
2380///
2381/// As of 2025-12-09, Binance migrated conditional order types to the Algo Service API.
2382/// The traditional `/fapi/v1/order` endpoint returns error `-4120` for these types.
2383#[must_use]
2384pub fn is_algo_order_type(order_type: OrderType) -> bool {
2385    matches!(
2386        order_type,
2387        OrderType::StopMarket
2388            | OrderType::StopLimit
2389            | OrderType::MarketIfTouched
2390            | OrderType::LimitIfTouched
2391            | OrderType::TrailingStopMarket
2392    )
2393}
2394
2395/// Converts a Nautilus order type to a Binance Futures order type.
2396pub(crate) fn order_type_to_binance_futures(
2397    order_type: OrderType,
2398) -> anyhow::Result<BinanceFuturesOrderType> {
2399    match order_type {
2400        OrderType::Market => Ok(BinanceFuturesOrderType::Market),
2401        OrderType::Limit => Ok(BinanceFuturesOrderType::Limit),
2402        OrderType::StopMarket => Ok(BinanceFuturesOrderType::StopMarket),
2403        OrderType::StopLimit => Ok(BinanceFuturesOrderType::Stop),
2404        OrderType::MarketIfTouched => Ok(BinanceFuturesOrderType::TakeProfitMarket),
2405        OrderType::LimitIfTouched => Ok(BinanceFuturesOrderType::TakeProfit),
2406        OrderType::TrailingStopMarket => Ok(BinanceFuturesOrderType::TrailingStopMarket),
2407        _ => anyhow::bail!("Unsupported order type for Binance Futures: {order_type:?}"),
2408    }
2409}
2410
2411#[cfg(test)]
2412mod tests {
2413    use nautilus_core::time::get_atomic_clock_realtime;
2414    use nautilus_network::http::{HttpStatus, StatusCode};
2415    use rstest::rstest;
2416    use tokio_util::bytes::Bytes;
2417
2418    use super::*;
2419
2420    #[rstest]
2421    fn test_rate_limit_config_usdm_has_request_weight_and_orders() {
2422        let config = BinanceRawFuturesHttpClient::rate_limit_config(BinanceProductType::UsdM);
2423
2424        assert!(config.default_quota.is_some());
2425        assert_eq!(config.order_keys.len(), 2);
2426        assert!(config.order_keys.iter().any(|k| k.contains("Second")));
2427        assert!(config.order_keys.iter().any(|k| k.contains("Minute")));
2428    }
2429
2430    #[rstest]
2431    fn test_rate_limit_config_coinm_has_request_weight_and_orders() {
2432        let config = BinanceRawFuturesHttpClient::rate_limit_config(BinanceProductType::CoinM);
2433
2434        assert!(config.default_quota.is_some());
2435        assert_eq!(config.order_keys.len(), 2);
2436    }
2437
2438    #[rstest]
2439    fn test_quota_from_unknown_interval_returns_none() {
2440        let quota = BinanceRateLimitQuota {
2441            rate_limit_type: BinanceRateLimitType::Orders,
2442            interval: BinanceRateLimitInterval::Unknown,
2443            interval_num: 1,
2444            limit: 10,
2445        };
2446
2447        assert!(BinanceRawFuturesHttpClient::quota_from(&quota).is_none());
2448    }
2449
2450    #[rstest]
2451    fn test_create_client_rejects_spot_product_type() {
2452        let result = BinanceFuturesHttpClient::new(
2453            BinanceProductType::Spot,
2454            BinanceEnvironment::Mainnet,
2455            get_atomic_clock_realtime(),
2456            None,
2457            None,
2458            None,
2459            None,
2460            None,
2461            None,
2462            false,
2463        );
2464
2465        assert!(result.is_err());
2466    }
2467
2468    fn create_test_raw_client() -> BinanceRawFuturesHttpClient {
2469        BinanceRawFuturesHttpClient::new(
2470            BinanceProductType::UsdM,
2471            BinanceEnvironment::Mainnet,
2472            None,
2473            None,
2474            None,
2475            None,
2476            None,
2477            None,
2478        )
2479        .expect("Failed to create test client")
2480    }
2481
2482    #[rstest]
2483    fn test_parse_error_response_binance_error() {
2484        let client = create_test_raw_client();
2485        let response = HttpResponse {
2486            status: HttpStatus::new(StatusCode::BAD_REQUEST),
2487            headers: HashMap::new(),
2488            body: Bytes::from(r#"{"code":-1121,"msg":"Invalid symbol."}"#),
2489        };
2490
2491        let result: BinanceFuturesHttpResult<()> = client.parse_error_response(&response);
2492
2493        match result {
2494            Err(BinanceFuturesHttpError::BinanceError { code, message }) => {
2495                assert_eq!(code, -1121);
2496                assert_eq!(message, "Invalid symbol.");
2497            }
2498            other => panic!("Expected BinanceError, was {other:?}"),
2499        }
2500    }
2501}