Skip to main content

nautilus_binance/python/
config.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//! Python bindings for Binance configuration.
17
18use std::collections::HashMap;
19
20use nautilus_core::python::to_pyvalue_err;
21use nautilus_model::{enums::OmsType, identifiers::AccountId, types::Currency};
22use nautilus_network::websocket::TransportBackend;
23use pyo3::{
24    prelude::*,
25    types::{PyDict, PyDictMethods},
26};
27use rust_decimal::Decimal;
28
29use crate::{
30    common::enums::{BinanceEnvironment, BinanceMarginType, BinanceProductType},
31    config::{
32        BinanceDataClientConfig, BinanceExecutionClientConfig, BinanceInstrumentProviderConfig,
33        BinanceSpotMarketDataMode,
34    },
35};
36
37#[pymethods]
38#[pyo3_stub_gen::derive::gen_stub_pymethods]
39impl BinanceInstrumentProviderConfig {
40    /// Configuration for Binance instrument loading.
41    #[new]
42    #[pyo3(signature = (
43        load_all = true,
44        load_ids = None,
45        filters = None,
46        filter_callable = None,
47        log_warnings = true,
48        query_commission_rates = false,
49    ))]
50    fn py_new(
51        load_all: bool,
52        load_ids: Option<Vec<String>>,
53        filters: Option<HashMap<String, Py<PyAny>>>,
54        filter_callable: Option<String>,
55        log_warnings: bool,
56        query_commission_rates: bool,
57    ) -> PyResult<Self> {
58        let filters = filters
59            .map(nautilus_live::python::config::coerce_json_config)
60            .transpose()?
61            .unwrap_or_default();
62        Ok(Self {
63            load_all,
64            load_ids,
65            filters,
66            filter_callable,
67            log_warnings,
68            query_commission_rates,
69        })
70    }
71
72    fn __repr__(&self) -> String {
73        stringify!(BinanceInstrumentProviderConfig).to_string()
74    }
75
76    #[getter]
77    fn load_all(&self) -> bool {
78        self.load_all
79    }
80
81    #[getter]
82    fn load_ids(&self) -> Option<Vec<String>> {
83        self.load_ids.clone()
84    }
85
86    #[getter]
87    fn filters(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
88        let dict = PyDict::new(py);
89        for (key, value) in &self.filters {
90            dict.set_item(
91                key,
92                nautilus_live::python::config::json_value_to_py(py, value)?,
93            )?;
94        }
95        Ok(dict.into_any().unbind())
96    }
97
98    #[getter]
99    fn filter_callable(&self) -> Option<String> {
100        self.filter_callable.clone()
101    }
102
103    #[getter]
104    fn log_warnings(&self) -> bool {
105        self.log_warnings
106    }
107
108    #[getter]
109    fn query_commission_rates(&self) -> bool {
110        self.query_commission_rates
111    }
112}
113
114#[pymethods]
115#[pyo3_stub_gen::derive::gen_stub_pymethods]
116impl BinanceDataClientConfig {
117    /// Configuration for Binance data client.
118    ///
119    /// Ed25519 API keys are required for SBE WebSocket streams.
120    #[new]
121    #[pyo3(signature = (
122        product_type = None,
123        environment = None,
124        base_url_http = None,
125        base_url_ws = None,
126        api_key = None,
127        api_secret = None,
128        spot_market_data_mode = None,
129        instrument_provider = None,
130        instrument_refresh_interval_secs = None,
131        instrument_status_poll_secs = None,
132        proxy_url = None,
133        recv_window_ms = None,
134        us = false,
135        transport_backend = None,
136    ))]
137    #[expect(clippy::too_many_arguments)]
138    fn py_new(
139        product_type: Option<BinanceProductType>,
140        environment: Option<BinanceEnvironment>,
141        base_url_http: Option<String>,
142        base_url_ws: Option<String>,
143        api_key: Option<String>,
144        api_secret: Option<String>,
145        spot_market_data_mode: Option<BinanceSpotMarketDataMode>,
146        instrument_provider: Option<BinanceInstrumentProviderConfig>,
147        instrument_refresh_interval_secs: Option<u64>,
148        instrument_status_poll_secs: Option<u64>,
149        proxy_url: Option<String>,
150        recv_window_ms: Option<u64>,
151        us: bool,
152        transport_backend: Option<TransportBackend>,
153    ) -> PyResult<Self> {
154        let defaults = Self::default();
155        let config = Self {
156            product_type: product_type.unwrap_or(defaults.product_type),
157            environment: environment.unwrap_or(defaults.environment),
158            base_url_http: base_url_http.or(defaults.base_url_http),
159            base_url_ws: base_url_ws.or(defaults.base_url_ws),
160            api_key: api_key.or(defaults.api_key),
161            api_secret: api_secret.or(defaults.api_secret),
162            spot_market_data_mode: spot_market_data_mode.unwrap_or(defaults.spot_market_data_mode),
163            instrument_provider: instrument_provider.unwrap_or(defaults.instrument_provider),
164            instrument_refresh_interval_secs: instrument_refresh_interval_secs
165                .unwrap_or(defaults.instrument_refresh_interval_secs),
166            instrument_status_poll_secs: instrument_status_poll_secs
167                .unwrap_or(defaults.instrument_status_poll_secs),
168            proxy_url: proxy_url.or(defaults.proxy_url),
169            recv_window_ms: recv_window_ms.unwrap_or(defaults.recv_window_ms),
170            us,
171            transport_backend: transport_backend.unwrap_or(defaults.transport_backend),
172        };
173        config.validate().map_err(to_pyvalue_err)?;
174        Ok(config)
175    }
176
177    #[getter]
178    const fn has_proxy_url(&self) -> bool {
179        self.proxy_url.is_some()
180    }
181
182    fn __repr__(&self) -> String {
183        stringify!(BinanceDataClientConfig).to_string()
184    }
185}
186
187#[pymethods]
188#[pyo3_stub_gen::derive::gen_stub_pymethods]
189impl BinanceExecutionClientConfig {
190    /// Configuration for Binance execution client.
191    ///
192    /// Global execution uses WebSocket API authentication with Ed25519 credentials.
193    /// Binance US uses HMAC-signed HTTP requests and listen-key user data streams.
194    #[new]
195    #[pyo3(signature = (
196        account_id,
197        product_type = None,
198        environment = None,
199        base_url_http = None,
200        base_url_ws = None,
201        base_url_ws_trading = None,
202        use_ws_trading = true,
203        ws_trading_setup_timeout_ms = None,
204        instrument_provider = None,
205        instrument_refresh_interval_secs = None,
206        use_gtd = true,
207        use_position_ids = true,
208        oms_type = None,
209        default_taker_fee = None,
210        proxy_url = None,
211        recv_window_ms = None,
212        us = false,
213        api_key = None,
214        api_secret = None,
215        futures_leverages = None,
216        futures_margin_types = None,
217        treat_expired_as_canceled = false,
218        use_trade_lite = false,
219        bnfcr_currency = None,
220        transport_backend = None,
221    ))]
222    #[expect(clippy::too_many_arguments)]
223    fn py_new(
224        account_id: AccountId,
225        product_type: Option<BinanceProductType>,
226        environment: Option<BinanceEnvironment>,
227        base_url_http: Option<String>,
228        base_url_ws: Option<String>,
229        base_url_ws_trading: Option<String>,
230        use_ws_trading: bool,
231        ws_trading_setup_timeout_ms: Option<u64>,
232        instrument_provider: Option<BinanceInstrumentProviderConfig>,
233        instrument_refresh_interval_secs: Option<u64>,
234        use_gtd: bool,
235        use_position_ids: bool,
236        oms_type: Option<OmsType>,
237        default_taker_fee: Option<f64>,
238        proxy_url: Option<String>,
239        recv_window_ms: Option<u64>,
240        us: bool,
241        api_key: Option<String>,
242        api_secret: Option<String>,
243        futures_leverages: Option<HashMap<String, u32>>,
244        futures_margin_types: Option<HashMap<String, BinanceMarginType>>,
245        treat_expired_as_canceled: bool,
246        use_trade_lite: bool,
247        bnfcr_currency: Option<Currency>,
248        transport_backend: Option<TransportBackend>,
249    ) -> PyResult<Self> {
250        let defaults = Self::default();
251        let config = Self {
252            account_id,
253            product_type: product_type.unwrap_or(defaults.product_type),
254            environment: environment.unwrap_or(defaults.environment),
255            base_url_http: base_url_http.or(defaults.base_url_http),
256            base_url_ws: base_url_ws.or(defaults.base_url_ws),
257            base_url_ws_trading: base_url_ws_trading.or(defaults.base_url_ws_trading),
258            use_ws_trading,
259            ws_trading_setup_timeout_ms: ws_trading_setup_timeout_ms
260                .unwrap_or(defaults.ws_trading_setup_timeout_ms),
261            instrument_provider: instrument_provider.unwrap_or(defaults.instrument_provider),
262            instrument_refresh_interval_secs: instrument_refresh_interval_secs
263                .unwrap_or(defaults.instrument_refresh_interval_secs),
264            use_gtd,
265            use_position_ids,
266            oms_type,
267            default_taker_fee: default_taker_fee
268                .map_or_else(|| Ok(defaults.default_taker_fee), Decimal::try_from)
269                .unwrap_or(defaults.default_taker_fee),
270            proxy_url: proxy_url.or(defaults.proxy_url),
271            recv_window_ms: recv_window_ms.unwrap_or(defaults.recv_window_ms),
272            us,
273            api_key: api_key.or(defaults.api_key),
274            api_secret: api_secret.or(defaults.api_secret),
275            futures_leverages,
276            futures_margin_types,
277            bnfcr_currency: bnfcr_currency.unwrap_or(defaults.bnfcr_currency),
278            treat_expired_as_canceled,
279            use_trade_lite,
280            transport_backend: transport_backend.unwrap_or(defaults.transport_backend),
281        };
282        config.validate().map_err(to_pyvalue_err)?;
283        Ok(config)
284    }
285
286    #[getter]
287    const fn has_proxy_url(&self) -> bool {
288        self.proxy_url.is_some()
289    }
290
291    fn __repr__(&self) -> String {
292        stringify!(BinanceExecutionClientConfig).to_string()
293    }
294}
295
296#[cfg(test)]
297mod tests {
298    use rstest::rstest;
299    use rust_decimal::Decimal;
300
301    use super::*;
302
303    #[rstest]
304    fn test_data_client_py_new_uses_defaults_for_omitted_fields() {
305        let config = BinanceDataClientConfig::py_new(
306            None, None, None, None, None, None, None, None, None, None, None, None, false, None,
307        )
308        .unwrap();
309        let defaults = BinanceDataClientConfig::default();
310
311        assert_eq!(config.product_type, defaults.product_type);
312        assert_eq!(config.environment, defaults.environment);
313        assert_eq!(config.base_url_http, defaults.base_url_http);
314        assert_eq!(config.base_url_ws, defaults.base_url_ws);
315        assert_eq!(config.api_key, defaults.api_key);
316        assert_eq!(config.api_secret, defaults.api_secret);
317        assert_eq!(config.spot_market_data_mode, defaults.spot_market_data_mode);
318        assert_eq!(config.instrument_provider, defaults.instrument_provider);
319        assert_eq!(
320            config.instrument_refresh_interval_secs,
321            defaults.instrument_refresh_interval_secs
322        );
323        assert_eq!(
324            config.instrument_status_poll_secs,
325            defaults.instrument_status_poll_secs
326        );
327        assert_eq!(config.proxy_url, defaults.proxy_url);
328        assert_eq!(config.recv_window_ms, defaults.recv_window_ms);
329        assert!(!config.us);
330    }
331
332    #[rstest]
333    fn test_data_client_py_new_uses_explicit_overrides() {
334        let config = BinanceDataClientConfig::py_new(
335            Some(BinanceProductType::UsdM),
336            Some(BinanceEnvironment::Testnet),
337            Some("https://http.example".to_string()),
338            Some("wss://ws.example".to_string()),
339            Some("api-key".to_string()),
340            Some("api-secret".to_string()),
341            Some(BinanceSpotMarketDataMode::Json),
342            None,
343            Some(30),
344            Some(15),
345            Some("http://proxy.example:8080".to_string()),
346            Some(45_000),
347            false,
348            None,
349        )
350        .unwrap();
351
352        assert_eq!(config.product_type, BinanceProductType::UsdM);
353        assert_eq!(config.environment, BinanceEnvironment::Testnet);
354        assert_eq!(
355            config.base_url_http.as_deref(),
356            Some("https://http.example")
357        );
358        assert_eq!(config.base_url_ws.as_deref(), Some("wss://ws.example"));
359        assert_eq!(config.api_key.as_deref(), Some("api-key"));
360        assert_eq!(config.api_secret.as_deref(), Some("api-secret"));
361        assert_eq!(
362            config.spot_market_data_mode,
363            BinanceSpotMarketDataMode::Json
364        );
365        assert_eq!(config.instrument_refresh_interval_secs, 30);
366        assert_eq!(config.instrument_status_poll_secs, 15);
367        assert_eq!(
368            config.proxy_url.as_deref(),
369            Some("http://proxy.example:8080")
370        );
371        assert_eq!(config.recv_window_ms, 45_000);
372    }
373
374    #[rstest]
375    fn test_exec_client_py_new_uses_defaults_for_optional_fields() {
376        let account_id = AccountId::from("BINANCE-001");
377        let config = BinanceExecutionClientConfig::py_new(
378            account_id, None, None, None, None, None, true, None, None, None, true, true, None,
379            None, None, None, false, None, None, None, None, false, false, None, None,
380        )
381        .unwrap();
382        let defaults = BinanceExecutionClientConfig::default();
383
384        assert_eq!(config.account_id, account_id);
385        assert_eq!(config.product_type, defaults.product_type);
386        assert_eq!(config.environment, defaults.environment);
387        assert_eq!(config.base_url_http, defaults.base_url_http);
388        assert_eq!(config.base_url_ws, defaults.base_url_ws);
389        assert_eq!(config.base_url_ws_trading, defaults.base_url_ws_trading);
390        assert!(config.use_ws_trading);
391        assert_eq!(config.ws_trading_setup_timeout_ms, 10_000);
392        assert_eq!(config.instrument_provider, defaults.instrument_provider);
393        assert_eq!(
394            config.instrument_refresh_interval_secs,
395            defaults.instrument_refresh_interval_secs
396        );
397        assert!(config.use_gtd);
398        assert_eq!(config.oms_type, defaults.oms_type);
399        assert_eq!(config.default_taker_fee, defaults.default_taker_fee);
400        assert_eq!(config.proxy_url, defaults.proxy_url);
401        assert_eq!(config.recv_window_ms, defaults.recv_window_ms);
402        assert!(!config.us);
403        assert_eq!(config.api_key, defaults.api_key);
404        assert_eq!(config.api_secret, defaults.api_secret);
405        assert_eq!(config.futures_leverages, defaults.futures_leverages);
406        assert_eq!(config.futures_margin_types, defaults.futures_margin_types);
407        assert_eq!(config.bnfcr_currency, defaults.bnfcr_currency);
408        assert_eq!(config.bnfcr_currency, Currency::USDT());
409        assert_eq!(
410            config.treat_expired_as_canceled,
411            defaults.treat_expired_as_canceled
412        );
413    }
414
415    #[rstest]
416    fn test_exec_client_py_new_preserves_explicit_overrides() {
417        use std::collections::HashMap;
418
419        use crate::common::enums::BinanceMarginType;
420
421        let leverages = HashMap::from([("BTCUSDT".to_string(), 20)]);
422        let margin_types = HashMap::from([("BTCUSDT".to_string(), BinanceMarginType::Cross)]);
423
424        let config = BinanceExecutionClientConfig::py_new(
425            AccountId::from("BINANCE-002"),
426            Some(BinanceProductType::UsdM),
427            Some(BinanceEnvironment::Demo),
428            Some("https://http.example".to_string()),
429            Some("wss://stream.example".to_string()),
430            Some("wss://trade.example".to_string()),
431            false,
432            Some(250),
433            None,
434            Some(45),
435            false,
436            false,
437            Some(OmsType::Hedging),
438            Some(0.0015),
439            Some("http://proxy.example:8080".to_string()),
440            Some(60_000),
441            false,
442            Some("api-key".to_string()),
443            Some("api-secret".to_string()),
444            Some(leverages.clone()),
445            Some(margin_types.clone()),
446            true,
447            true,
448            Some(Currency::USDC()),
449            None,
450        )
451        .unwrap();
452
453        assert_eq!(config.product_type, BinanceProductType::UsdM);
454        assert_eq!(config.environment, BinanceEnvironment::Demo);
455        assert_eq!(
456            config.base_url_http.as_deref(),
457            Some("https://http.example")
458        );
459        assert_eq!(config.base_url_ws.as_deref(), Some("wss://stream.example"));
460        assert_eq!(
461            config.base_url_ws_trading.as_deref(),
462            Some("wss://trade.example")
463        );
464        assert!(!config.use_ws_trading);
465        assert_eq!(config.ws_trading_setup_timeout_ms, 250);
466        assert_eq!(config.instrument_refresh_interval_secs, 45);
467        assert!(!config.use_gtd);
468        assert!(!config.use_position_ids);
469        assert_eq!(config.oms_type, Some(OmsType::Hedging));
470        assert_eq!(config.default_taker_fee, Decimal::try_from(0.0015).unwrap());
471        assert_eq!(
472            config.proxy_url.as_deref(),
473            Some("http://proxy.example:8080")
474        );
475        assert_eq!(config.recv_window_ms, 60_000);
476        assert_eq!(config.api_key.as_deref(), Some("api-key"));
477        assert_eq!(config.api_secret.as_deref(), Some("api-secret"));
478        assert_eq!(config.futures_leverages, Some(leverages));
479        assert_eq!(config.futures_margin_types, Some(margin_types));
480        assert_eq!(config.bnfcr_currency, Currency::USDC());
481        assert!(config.treat_expired_as_canceled);
482        assert!(config.use_trade_lite);
483    }
484
485    #[rstest]
486    fn test_exec_client_py_new_uses_default_fee_for_invalid_float() {
487        let defaults = BinanceExecutionClientConfig::default();
488        let config = BinanceExecutionClientConfig::py_new(
489            AccountId::from("BINANCE-003"),
490            None,
491            None,
492            None,
493            None,
494            None,
495            true,
496            None,
497            None,
498            None,
499            true,
500            true,
501            None,
502            Some(f64::NAN),
503            None,
504            None,
505            false,
506            None,
507            None,
508            None,
509            None,
510            false,
511            false,
512            None,
513            None,
514        )
515        .unwrap();
516
517        assert_eq!(config.default_taker_fee, defaults.default_taker_fee);
518    }
519
520    #[rstest]
521    fn test_instrument_provider_py_new_preserves_filters() {
522        Python::initialize();
523        Python::attach(|py| {
524            let symbols = vec!["BTCUSDT", "ETHUSDT"].into_pyobject(py).unwrap();
525            let filters = HashMap::from([("symbols".to_string(), symbols.into_any().unbind())]);
526
527            let config = BinanceInstrumentProviderConfig::py_new(
528                false,
529                Some(vec!["BTCUSDT.BINANCE".to_string()]),
530                Some(filters),
531                None,
532                false,
533                true,
534            )
535            .unwrap();
536
537            assert!(!config.load_all);
538            assert_eq!(config.load_ids, Some(vec!["BTCUSDT.BINANCE".to_string()]));
539            assert_eq!(
540                config.filters["symbols"],
541                serde_json::json!(["BTCUSDT", "ETHUSDT"])
542            );
543            assert!(!config.log_warnings);
544            assert!(config.query_commission_rates);
545        });
546    }
547}