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//!
18//! This client wraps the generic [`BinanceHttpClient`] and provides futures-specific
19//! endpoints such as mark price, funding rate, and open interest.
20
21use nautilus_core::nanos::UnixNanos;
22use nautilus_model::instruments::any::InstrumentAny;
23use serde::{Deserialize, Serialize};
24
25use crate::{
26    common::{
27        enums::{BinanceEnvironment, BinanceProductType},
28        parse::parse_usdm_instrument,
29    },
30    http::{
31        client::BinanceHttpClient,
32        error::{BinanceHttpError, BinanceHttpResult},
33        models::{
34            BinanceBookTicker, BinanceFuturesMarkPrice, BinanceFuturesTicker24hr,
35            BinanceFuturesUsdExchangeInfo, BinanceOrderBook,
36        },
37        query::{BinanceBookTickerParams, BinanceDepthParams, BinanceTicker24hrParams},
38    },
39};
40
41/// Query parameters for mark price endpoints.
42#[derive(Debug, Clone, Serialize)]
43#[serde(rename_all = "camelCase")]
44pub struct MarkPriceParams {
45    /// Trading symbol (optional - if omitted, returns all symbols).
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub symbol: Option<String>,
48}
49
50/// Response wrapper for mark price endpoint.
51///
52/// Binance returns a single object when symbol is provided, or an array when omitted.
53#[derive(Debug, Deserialize)]
54#[serde(untagged)]
55enum MarkPriceResponse {
56    /// Single mark price when querying specific symbol.
57    Single(BinanceFuturesMarkPrice),
58    /// Multiple mark prices when querying all symbols.
59    Multiple(Vec<BinanceFuturesMarkPrice>),
60}
61
62impl From<MarkPriceResponse> for Vec<BinanceFuturesMarkPrice> {
63    fn from(response: MarkPriceResponse) -> Self {
64        match response {
65            MarkPriceResponse::Single(price) => vec![price],
66            MarkPriceResponse::Multiple(prices) => prices,
67        }
68    }
69}
70
71/// Query parameters for funding rate history.
72#[derive(Debug, Clone, Serialize)]
73#[serde(rename_all = "camelCase")]
74pub struct FundingRateParams {
75    /// Trading symbol.
76    pub symbol: String,
77    /// Start time in milliseconds (optional).
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub start_time: Option<i64>,
80    /// End time in milliseconds (optional).
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub end_time: Option<i64>,
83    /// Limit results (default 100, max 1000).
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub limit: Option<u32>,
86}
87
88/// Query parameters for open interest endpoints.
89#[derive(Debug, Clone, Serialize)]
90#[serde(rename_all = "camelCase")]
91pub struct OpenInterestParams {
92    /// Trading symbol.
93    pub symbol: String,
94}
95
96/// Open interest response from `/fapi/v1/openInterest`.
97#[derive(Debug, Clone, serde::Deserialize)]
98#[serde(rename_all = "camelCase")]
99pub struct BinanceOpenInterest {
100    /// Trading symbol.
101    pub symbol: String,
102    /// Total open interest.
103    pub open_interest: String,
104    /// Response timestamp.
105    pub time: i64,
106}
107
108/// Funding rate history entry from `/fapi/v1/fundingRate`.
109#[derive(Debug, Clone, serde::Deserialize)]
110#[serde(rename_all = "camelCase")]
111pub struct BinanceFundingRate {
112    /// Trading symbol.
113    pub symbol: String,
114    /// Funding rate value.
115    pub funding_rate: String,
116    /// Funding time in milliseconds.
117    pub funding_time: i64,
118    /// Mark price at funding time.
119    #[serde(default)]
120    pub mark_price: Option<String>,
121}
122
123/// Binance Futures HTTP client for USD-M and COIN-M perpetuals.
124///
125/// This client wraps the generic HTTP client and provides convenience methods
126/// for futures-specific endpoints. Use [`BinanceProductType::UsdM`] for USD-margined
127/// or [`BinanceProductType::CoinM`] for coin-margined futures.
128#[derive(Debug, Clone)]
129#[cfg_attr(
130    feature = "python",
131    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.binance")
132)]
133pub struct BinanceFuturesHttpClient {
134    inner: BinanceHttpClient,
135    product_type: BinanceProductType,
136}
137
138impl BinanceFuturesHttpClient {
139    /// Creates a new [`BinanceFuturesHttpClient`] instance.
140    ///
141    /// # Errors
142    ///
143    /// Returns an error if:
144    /// - `product_type` is not a futures type (UsdM or CoinM).
145    /// - Credential creation fails.
146    #[allow(clippy::too_many_arguments)]
147    pub fn new(
148        product_type: BinanceProductType,
149        environment: BinanceEnvironment,
150        api_key: Option<String>,
151        api_secret: Option<String>,
152        base_url_override: Option<String>,
153        recv_window: Option<u64>,
154        timeout_secs: Option<u64>,
155        proxy_url: Option<String>,
156    ) -> BinanceHttpResult<Self> {
157        match product_type {
158            BinanceProductType::UsdM | BinanceProductType::CoinM => {}
159            _ => {
160                return Err(BinanceHttpError::ValidationError(format!(
161                    "BinanceFuturesHttpClient requires UsdM or CoinM product type, got {product_type:?}"
162                )));
163            }
164        }
165
166        let inner = BinanceHttpClient::new(
167            product_type,
168            environment,
169            api_key,
170            api_secret,
171            base_url_override,
172            recv_window,
173            timeout_secs,
174            proxy_url,
175        )?;
176
177        Ok(Self {
178            inner,
179            product_type,
180        })
181    }
182
183    /// Returns the product type (UsdM or CoinM).
184    #[must_use]
185    pub const fn product_type(&self) -> BinanceProductType {
186        self.product_type
187    }
188
189    /// Returns a reference to the underlying generic HTTP client.
190    #[must_use]
191    pub const fn inner(&self) -> &BinanceHttpClient {
192        &self.inner
193    }
194
195    /// Fetches exchange information and populates the instrument cache.
196    ///
197    /// # Errors
198    ///
199    /// Returns an error if the HTTP request fails.
200    pub async fn exchange_info(&self) -> BinanceHttpResult<()> {
201        self.inner.exchange_info().await
202    }
203
204    /// Fetches exchange information and returns parsed Nautilus instruments.
205    ///
206    /// Only returns perpetual contracts. Non-perpetual contracts (quarterly futures)
207    /// are filtered out.
208    ///
209    /// # Errors
210    ///
211    /// Returns an error if the HTTP request fails or parsing fails.
212    pub async fn instruments(&self) -> BinanceHttpResult<Vec<InstrumentAny>> {
213        let info: BinanceFuturesUsdExchangeInfo = self
214            .inner
215            .raw()
216            .get("exchangeInfo", None::<&()>, false, false)
217            .await?;
218
219        let ts_init = UnixNanos::default();
220        let mut instruments = Vec::with_capacity(info.symbols.len());
221
222        for symbol in &info.symbols {
223            match parse_usdm_instrument(symbol, ts_init, ts_init) {
224                Ok(instrument) => instruments.push(instrument),
225                Err(e) => {
226                    // Log and skip non-perpetual or malformed symbols
227                    tracing::debug!(
228                        symbol = %symbol.symbol,
229                        error = %e,
230                        "Skipping symbol during instrument parsing"
231                    );
232                }
233            }
234        }
235
236        tracing::info!(
237            count = instruments.len(),
238            "Loaded USD-M perpetual instruments"
239        );
240        Ok(instruments)
241    }
242
243    /// Fetches 24hr ticker statistics.
244    ///
245    /// # Errors
246    ///
247    /// Returns an error if the HTTP request fails.
248    pub async fn ticker_24h(
249        &self,
250        params: &BinanceTicker24hrParams,
251    ) -> BinanceHttpResult<Vec<BinanceFuturesTicker24hr>> {
252        self.inner
253            .raw()
254            .get("ticker/24hr", Some(params), false, false)
255            .await
256    }
257
258    /// Fetches best bid/ask prices.
259    ///
260    /// # Errors
261    ///
262    /// Returns an error if the HTTP request fails.
263    pub async fn book_ticker(
264        &self,
265        params: &BinanceBookTickerParams,
266    ) -> BinanceHttpResult<Vec<BinanceBookTicker>> {
267        self.inner.book_ticker(params).await
268    }
269
270    /// Fetches order book depth.
271    ///
272    /// # Errors
273    ///
274    /// Returns an error if the HTTP request fails.
275    pub async fn depth(&self, params: &BinanceDepthParams) -> BinanceHttpResult<BinanceOrderBook> {
276        self.inner.depth(params).await
277    }
278
279    /// Fetches mark price and funding rate.
280    ///
281    /// If `symbol` is None, returns mark price for all symbols.
282    ///
283    /// # Errors
284    ///
285    /// Returns an error if the HTTP request fails.
286    pub async fn mark_price(
287        &self,
288        params: &MarkPriceParams,
289    ) -> BinanceHttpResult<Vec<BinanceFuturesMarkPrice>> {
290        let response: MarkPriceResponse = self
291            .inner
292            .raw()
293            .get("premiumIndex", Some(params), false, false)
294            .await?;
295        Ok(response.into())
296    }
297
298    /// Fetches funding rate history.
299    ///
300    /// # Errors
301    ///
302    /// Returns an error if the HTTP request fails.
303    pub async fn funding_rate(
304        &self,
305        params: &FundingRateParams,
306    ) -> BinanceHttpResult<Vec<BinanceFundingRate>> {
307        self.inner
308            .raw()
309            .get("fundingRate", Some(params), false, false)
310            .await
311    }
312
313    /// Fetches current open interest for a symbol.
314    ///
315    /// # Errors
316    ///
317    /// Returns an error if the HTTP request fails.
318    pub async fn open_interest(
319        &self,
320        params: &OpenInterestParams,
321    ) -> BinanceHttpResult<BinanceOpenInterest> {
322        self.inner
323            .raw()
324            .get("openInterest", Some(params), false, false)
325            .await
326    }
327
328    /// Creates a listen key for user data stream.
329    ///
330    /// # Errors
331    ///
332    /// Returns an error if the HTTP request fails or credentials are missing.
333    pub async fn create_listen_key(&self) -> BinanceHttpResult<ListenKeyResponse> {
334        self.inner
335            .raw()
336            .post::<(), ListenKeyResponse>("listenKey", None, None, true, false)
337            .await
338    }
339
340    /// Keeps alive an existing listen key.
341    ///
342    /// # Errors
343    ///
344    /// Returns an error if the HTTP request fails.
345    pub async fn keepalive_listen_key(&self, listen_key: &str) -> BinanceHttpResult<()> {
346        let params = ListenKeyParams {
347            listen_key: listen_key.to_string(),
348        };
349        let _: serde_json::Value = self
350            .inner
351            .raw()
352            .request_put("listenKey", Some(&params), true, false)
353            .await?;
354        Ok(())
355    }
356
357    /// Closes an existing listen key.
358    ///
359    /// # Errors
360    ///
361    /// Returns an error if the HTTP request fails.
362    pub async fn close_listen_key(&self, listen_key: &str) -> BinanceHttpResult<()> {
363        let params = ListenKeyParams {
364            listen_key: listen_key.to_string(),
365        };
366        let _: serde_json::Value = self
367            .inner
368            .raw()
369            .request_delete("listenKey", Some(&params), true, false)
370            .await?;
371        Ok(())
372    }
373}
374
375/// Listen key response from user data stream endpoints.
376#[derive(Debug, Clone, serde::Deserialize)]
377#[serde(rename_all = "camelCase")]
378pub struct ListenKeyResponse {
379    /// The listen key for WebSocket user data stream.
380    pub listen_key: String,
381}
382
383/// Listen key request parameters.
384#[derive(Debug, Clone, Serialize)]
385#[serde(rename_all = "camelCase")]
386struct ListenKeyParams {
387    listen_key: String,
388}