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, string::secret::SecretString};
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        max_retries = None,
137        retry_delay_initial_ms = None,
138        retry_delay_max_ms = None,
139        book_snapshot_timeout_secs = None,
140    ))]
141    #[expect(clippy::too_many_arguments)]
142    fn py_new(
143        product_type: Option<BinanceProductType>,
144        environment: Option<BinanceEnvironment>,
145        base_url_http: Option<String>,
146        base_url_ws: Option<String>,
147        api_key: Option<String>,
148        api_secret: Option<String>,
149        spot_market_data_mode: Option<BinanceSpotMarketDataMode>,
150        instrument_provider: Option<BinanceInstrumentProviderConfig>,
151        instrument_refresh_interval_secs: Option<u64>,
152        instrument_status_poll_secs: Option<u64>,
153        proxy_url: Option<String>,
154        recv_window_ms: Option<u64>,
155        us: bool,
156        transport_backend: Option<TransportBackend>,
157        max_retries: Option<u32>,
158        retry_delay_initial_ms: Option<u64>,
159        retry_delay_max_ms: Option<u64>,
160        book_snapshot_timeout_secs: Option<u64>,
161    ) -> PyResult<Self> {
162        let defaults = Self::default();
163        let config = Self {
164            product_type: product_type.unwrap_or(defaults.product_type),
165            environment: environment.unwrap_or(defaults.environment),
166            base_url_http: base_url_http.or(defaults.base_url_http),
167            base_url_ws: base_url_ws.or(defaults.base_url_ws),
168            api_key: api_key.map(SecretString::from).or(defaults.api_key),
169            api_secret: api_secret.map(SecretString::from).or(defaults.api_secret),
170            spot_market_data_mode: spot_market_data_mode.unwrap_or(defaults.spot_market_data_mode),
171            instrument_provider: instrument_provider.unwrap_or(defaults.instrument_provider),
172            instrument_refresh_interval_secs: instrument_refresh_interval_secs
173                .unwrap_or(defaults.instrument_refresh_interval_secs),
174            instrument_status_poll_secs: instrument_status_poll_secs
175                .unwrap_or(defaults.instrument_status_poll_secs),
176            book_snapshot_timeout_secs: book_snapshot_timeout_secs
177                .unwrap_or(defaults.book_snapshot_timeout_secs),
178            proxy_url: proxy_url.map(SecretString::from).or(defaults.proxy_url),
179            recv_window_ms: recv_window_ms.unwrap_or(defaults.recv_window_ms),
180            max_retries: max_retries.unwrap_or(defaults.max_retries),
181            retry_delay_initial_ms: retry_delay_initial_ms
182                .unwrap_or(defaults.retry_delay_initial_ms),
183            retry_delay_max_ms: retry_delay_max_ms.unwrap_or(defaults.retry_delay_max_ms),
184            us,
185            transport_backend: transport_backend.unwrap_or(defaults.transport_backend),
186        };
187        config.validate().map_err(to_pyvalue_err)?;
188        Ok(config)
189    }
190
191    #[getter]
192    const fn has_proxy_url(&self) -> bool {
193        self.proxy_url.is_some()
194    }
195
196    fn __repr__(&self) -> String {
197        stringify!(BinanceDataClientConfig).to_string()
198    }
199}
200
201#[pymethods]
202#[pyo3_stub_gen::derive::gen_stub_pymethods]
203impl BinanceExecutionClientConfig {
204    /// Configuration for Binance execution client.
205    ///
206    /// Global execution uses WebSocket API authentication with Ed25519 credentials.
207    /// Binance US uses HMAC-signed HTTP requests and listen-key user data streams.
208    #[new]
209    #[pyo3(signature = (
210        account_id,
211        product_type = None,
212        environment = None,
213        base_url_http = None,
214        base_url_ws = None,
215        base_url_ws_trading = None,
216        use_ws_trading = true,
217        ws_trading_setup_timeout_ms = None,
218        instrument_provider = None,
219        instrument_refresh_interval_secs = None,
220        use_gtd = true,
221        use_position_ids = true,
222        oms_type = None,
223        default_taker_fee = None,
224        proxy_url = None,
225        recv_window_ms = None,
226        us = false,
227        api_key = None,
228        api_secret = None,
229        futures_leverages = None,
230        futures_margin_types = None,
231        treat_expired_as_canceled = false,
232        use_trade_lite = false,
233        bnfcr_currency = None,
234        transport_backend = None,
235        max_retries = None,
236        retry_delay_initial_ms = None,
237        retry_delay_max_ms = None,
238    ))]
239    #[expect(clippy::too_many_arguments)]
240    fn py_new(
241        account_id: AccountId,
242        product_type: Option<BinanceProductType>,
243        environment: Option<BinanceEnvironment>,
244        base_url_http: Option<String>,
245        base_url_ws: Option<String>,
246        base_url_ws_trading: Option<String>,
247        use_ws_trading: bool,
248        ws_trading_setup_timeout_ms: Option<u64>,
249        instrument_provider: Option<BinanceInstrumentProviderConfig>,
250        instrument_refresh_interval_secs: Option<u64>,
251        use_gtd: bool,
252        use_position_ids: bool,
253        oms_type: Option<OmsType>,
254        default_taker_fee: Option<f64>,
255        proxy_url: Option<String>,
256        recv_window_ms: Option<u64>,
257        us: bool,
258        api_key: Option<String>,
259        api_secret: Option<String>,
260        futures_leverages: Option<HashMap<String, u32>>,
261        futures_margin_types: Option<HashMap<String, BinanceMarginType>>,
262        treat_expired_as_canceled: bool,
263        use_trade_lite: bool,
264        bnfcr_currency: Option<Currency>,
265        transport_backend: Option<TransportBackend>,
266        max_retries: Option<u32>,
267        retry_delay_initial_ms: Option<u64>,
268        retry_delay_max_ms: Option<u64>,
269    ) -> PyResult<Self> {
270        let defaults = Self::default();
271        let config = Self {
272            account_id,
273            product_type: product_type.unwrap_or(defaults.product_type),
274            environment: environment.unwrap_or(defaults.environment),
275            base_url_http: base_url_http.or(defaults.base_url_http),
276            base_url_ws: base_url_ws.or(defaults.base_url_ws),
277            base_url_ws_trading: base_url_ws_trading.or(defaults.base_url_ws_trading),
278            use_ws_trading,
279            ws_trading_setup_timeout_ms: ws_trading_setup_timeout_ms
280                .unwrap_or(defaults.ws_trading_setup_timeout_ms),
281            instrument_provider: instrument_provider.unwrap_or(defaults.instrument_provider),
282            instrument_refresh_interval_secs: instrument_refresh_interval_secs
283                .unwrap_or(defaults.instrument_refresh_interval_secs),
284            use_gtd,
285            use_position_ids,
286            oms_type,
287            default_taker_fee: default_taker_fee
288                .map_or_else(|| Ok(defaults.default_taker_fee), Decimal::try_from)
289                .unwrap_or(defaults.default_taker_fee),
290            proxy_url: proxy_url.map(SecretString::from).or(defaults.proxy_url),
291            recv_window_ms: recv_window_ms.unwrap_or(defaults.recv_window_ms),
292            max_retries: max_retries.unwrap_or(defaults.max_retries),
293            retry_delay_initial_ms: retry_delay_initial_ms
294                .unwrap_or(defaults.retry_delay_initial_ms),
295            retry_delay_max_ms: retry_delay_max_ms.unwrap_or(defaults.retry_delay_max_ms),
296            us,
297            api_key: api_key.map(SecretString::from).or(defaults.api_key),
298            api_secret: api_secret.map(SecretString::from).or(defaults.api_secret),
299            futures_leverages,
300            futures_margin_types,
301            bnfcr_currency: bnfcr_currency.unwrap_or(defaults.bnfcr_currency),
302            treat_expired_as_canceled,
303            use_trade_lite,
304            transport_backend: transport_backend.unwrap_or(defaults.transport_backend),
305        };
306        config.validate().map_err(to_pyvalue_err)?;
307        Ok(config)
308    }
309
310    #[getter]
311    const fn has_proxy_url(&self) -> bool {
312        self.proxy_url.is_some()
313    }
314
315    fn __repr__(&self) -> String {
316        stringify!(BinanceExecutionClientConfig).to_string()
317    }
318}
319
320#[cfg(test)]
321mod tests {
322    use rstest::rstest;
323    use rust_decimal::Decimal;
324
325    use super::*;
326
327    #[rstest]
328    fn test_python_constructors_preserve_existing_positional_arguments() {
329        Python::initialize();
330        Python::attach(|py| {
331            let locals = PyDict::new(py);
332            locals
333                .set_item("DataConfig", py.get_type::<BinanceDataClientConfig>())
334                .unwrap();
335            locals
336                .set_item("ExecConfig", py.get_type::<BinanceExecutionClientConfig>())
337                .unwrap();
338            locals
339                .set_item(
340                    "account_id",
341                    Py::new(py, AccountId::from("BINANCE-001")).unwrap(),
342                )
343                .unwrap();
344            let data = py.eval(
345                c"DataConfig(None, None, None, None, None, None, None, None, None, None, None, None, False, None)",
346                None, Some(&locals),
347            ).unwrap();
348            let execution = py.eval(
349                c"ExecConfig(account_id, None, None, None, None, None, True, None, None, None, True, True, None, None, None, None, False, None, None, None, None, False, False, None, None)",
350                None, Some(&locals),
351            ).unwrap();
352            let data = data.extract::<PyRef<BinanceDataClientConfig>>().unwrap();
353            let execution = execution
354                .extract::<PyRef<BinanceExecutionClientConfig>>()
355                .unwrap();
356
357            assert_eq!(
358                data.max_retries,
359                BinanceDataClientConfig::default().max_retries
360            );
361            assert!(!data.us);
362            assert_eq!(
363                execution.max_retries,
364                BinanceExecutionClientConfig::default().max_retries
365            );
366            assert!(!execution.us);
367            assert_eq!(execution.account_id, AccountId::from("BINANCE-001"));
368        });
369    }
370
371    #[rstest]
372    fn test_data_client_py_new_uses_defaults_for_omitted_fields() {
373        let config = BinanceDataClientConfig::py_new(
374            None, None, None, None, None, None, None, None, None, None, None, None, false, None,
375            None, None, None, None,
376        )
377        .unwrap();
378        let defaults = BinanceDataClientConfig::default();
379
380        assert_eq!(config.product_type, defaults.product_type);
381        assert_eq!(config.environment, defaults.environment);
382        assert_eq!(config.base_url_http, defaults.base_url_http);
383        assert_eq!(config.base_url_ws, defaults.base_url_ws);
384        assert_eq!(config.api_key, defaults.api_key);
385        assert_eq!(config.api_secret, defaults.api_secret);
386        assert_eq!(config.spot_market_data_mode, defaults.spot_market_data_mode);
387        assert_eq!(config.instrument_provider, defaults.instrument_provider);
388        assert_eq!(
389            config.instrument_refresh_interval_secs,
390            defaults.instrument_refresh_interval_secs
391        );
392        assert_eq!(
393            config.instrument_status_poll_secs,
394            defaults.instrument_status_poll_secs
395        );
396        assert_eq!(
397            config.book_snapshot_timeout_secs,
398            defaults.book_snapshot_timeout_secs
399        );
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    }
404
405    #[rstest]
406    fn test_data_client_py_new_uses_explicit_overrides() {
407        let config = BinanceDataClientConfig::py_new(
408            Some(BinanceProductType::UsdM),
409            Some(BinanceEnvironment::Testnet),
410            Some("https://http.example".to_string()),
411            Some("wss://ws.example".to_string()),
412            Some("api-key".to_string()),
413            Some("api-secret".to_string()),
414            Some(BinanceSpotMarketDataMode::Json),
415            None,
416            Some(30),
417            Some(15),
418            Some("http://proxy.example:8080".to_string()),
419            Some(45_000),
420            false,
421            None,
422            Some(7),
423            Some(123),
424            Some(456),
425            Some(0),
426        )
427        .unwrap();
428
429        assert_eq!(config.product_type, BinanceProductType::UsdM);
430        assert_eq!(config.environment, BinanceEnvironment::Testnet);
431        assert_eq!(
432            config.base_url_http.as_deref(),
433            Some("https://http.example")
434        );
435        assert_eq!(config.base_url_ws.as_deref(), Some("wss://ws.example"));
436        assert_eq!(
437            config.api_key.as_ref().map(SecretString::expose_secret),
438            Some("api-key"),
439        );
440        assert_eq!(
441            config.api_secret.as_ref().map(SecretString::expose_secret),
442            Some("api-secret"),
443        );
444        assert_eq!(
445            config.spot_market_data_mode,
446            BinanceSpotMarketDataMode::Json
447        );
448        assert_eq!(config.instrument_refresh_interval_secs, 30);
449        assert_eq!(config.instrument_status_poll_secs, 15);
450        assert_eq!(
451            config.proxy_url.as_ref().map(SecretString::expose_secret),
452            Some("http://proxy.example:8080")
453        );
454        assert_eq!(config.recv_window_ms, 45_000);
455        assert_eq!(config.max_retries, 7);
456        assert_eq!(config.retry_delay_initial_ms, 123);
457        assert_eq!(config.retry_delay_max_ms, 456);
458        assert_eq!(config.book_snapshot_timeout_secs, 0);
459    }
460
461    #[rstest]
462    fn test_exec_client_py_new_uses_defaults_for_optional_fields() {
463        let account_id = AccountId::from("BINANCE-001");
464        let config = BinanceExecutionClientConfig::py_new(
465            account_id, None, None, None, None, None, true, None, None, None, true, true, None,
466            None, None, None, false, None, None, None, None, false, false, None, None, None, None,
467            None,
468        )
469        .unwrap();
470        let defaults = BinanceExecutionClientConfig::default();
471
472        assert_eq!(config.account_id, account_id);
473        assert_eq!(config.product_type, defaults.product_type);
474        assert_eq!(config.environment, defaults.environment);
475        assert_eq!(config.base_url_http, defaults.base_url_http);
476        assert_eq!(config.base_url_ws, defaults.base_url_ws);
477        assert_eq!(config.base_url_ws_trading, defaults.base_url_ws_trading);
478        assert!(config.use_ws_trading);
479        assert_eq!(config.ws_trading_setup_timeout_ms, 10_000);
480        assert_eq!(config.instrument_provider, defaults.instrument_provider);
481        assert_eq!(
482            config.instrument_refresh_interval_secs,
483            defaults.instrument_refresh_interval_secs
484        );
485        assert!(config.use_gtd);
486        assert_eq!(config.oms_type, defaults.oms_type);
487        assert_eq!(config.default_taker_fee, defaults.default_taker_fee);
488        assert_eq!(config.proxy_url, defaults.proxy_url);
489        assert_eq!(config.recv_window_ms, defaults.recv_window_ms);
490        assert!(!config.us);
491        assert_eq!(config.api_key, defaults.api_key);
492        assert_eq!(config.api_secret, defaults.api_secret);
493        assert_eq!(config.futures_leverages, defaults.futures_leverages);
494        assert_eq!(config.futures_margin_types, defaults.futures_margin_types);
495        assert_eq!(config.bnfcr_currency, defaults.bnfcr_currency);
496        assert_eq!(config.bnfcr_currency, Currency::USDT());
497        assert_eq!(
498            config.treat_expired_as_canceled,
499            defaults.treat_expired_as_canceled
500        );
501    }
502
503    #[rstest]
504    fn test_exec_client_py_new_preserves_explicit_overrides() {
505        use std::collections::HashMap;
506
507        use crate::common::enums::BinanceMarginType;
508
509        let leverages = HashMap::from([("BTCUSDT".to_string(), 20)]);
510        let margin_types = HashMap::from([("BTCUSDT".to_string(), BinanceMarginType::Cross)]);
511
512        let config = BinanceExecutionClientConfig::py_new(
513            AccountId::from("BINANCE-002"),
514            Some(BinanceProductType::UsdM),
515            Some(BinanceEnvironment::Demo),
516            Some("https://http.example".to_string()),
517            Some("wss://stream.example".to_string()),
518            Some("wss://trade.example".to_string()),
519            false,
520            Some(250),
521            None,
522            Some(45),
523            false,
524            false,
525            Some(OmsType::Hedging),
526            Some(0.0015),
527            Some("http://proxy.example:8080".to_string()),
528            Some(60_000),
529            false,
530            Some("api-key".to_string()),
531            Some("api-secret".to_string()),
532            Some(leverages.clone()),
533            Some(margin_types.clone()),
534            true,
535            true,
536            Some(Currency::USDC()),
537            None,
538            None,
539            None,
540            None,
541        )
542        .unwrap();
543
544        assert_eq!(config.product_type, BinanceProductType::UsdM);
545        assert_eq!(config.environment, BinanceEnvironment::Demo);
546        assert_eq!(
547            config.base_url_http.as_deref(),
548            Some("https://http.example")
549        );
550        assert_eq!(config.base_url_ws.as_deref(), Some("wss://stream.example"));
551        assert_eq!(
552            config.base_url_ws_trading.as_deref(),
553            Some("wss://trade.example")
554        );
555        assert!(!config.use_ws_trading);
556        assert_eq!(config.ws_trading_setup_timeout_ms, 250);
557        assert_eq!(config.instrument_refresh_interval_secs, 45);
558        assert!(!config.use_gtd);
559        assert!(!config.use_position_ids);
560        assert_eq!(config.oms_type, Some(OmsType::Hedging));
561        assert_eq!(config.default_taker_fee, Decimal::try_from(0.0015).unwrap());
562        assert_eq!(
563            config.proxy_url.as_ref().map(SecretString::expose_secret),
564            Some("http://proxy.example:8080")
565        );
566        assert_eq!(config.recv_window_ms, 60_000);
567        assert_eq!(
568            config.api_key.as_ref().map(SecretString::expose_secret),
569            Some("api-key"),
570        );
571        assert_eq!(
572            config.api_secret.as_ref().map(SecretString::expose_secret),
573            Some("api-secret"),
574        );
575        assert_eq!(config.futures_leverages, Some(leverages));
576        assert_eq!(config.futures_margin_types, Some(margin_types));
577        assert_eq!(config.bnfcr_currency, Currency::USDC());
578        assert!(config.treat_expired_as_canceled);
579        assert!(config.use_trade_lite);
580    }
581
582    #[rstest]
583    fn test_exec_client_py_new_uses_default_fee_for_invalid_float() {
584        let defaults = BinanceExecutionClientConfig::default();
585        let config = BinanceExecutionClientConfig::py_new(
586            AccountId::from("BINANCE-003"),
587            None,
588            None,
589            None,
590            None,
591            None,
592            true,
593            None,
594            None,
595            None,
596            true,
597            true,
598            None,
599            Some(f64::NAN),
600            None,
601            None,
602            false,
603            None,
604            None,
605            None,
606            None,
607            false,
608            false,
609            None,
610            None,
611            None,
612            None,
613            None,
614        )
615        .unwrap();
616
617        assert_eq!(config.default_taker_fee, defaults.default_taker_fee);
618    }
619
620    #[rstest]
621    fn test_instrument_provider_py_new_preserves_filters() {
622        Python::initialize();
623        Python::attach(|py| {
624            let symbols = vec!["BTCUSDT", "ETHUSDT"].into_pyobject(py).unwrap();
625            let filters = HashMap::from([("symbols".to_string(), symbols.into_any().unbind())]);
626
627            let config = BinanceInstrumentProviderConfig::py_new(
628                false,
629                Some(vec!["BTCUSDT.BINANCE".to_string()]),
630                Some(filters),
631                None,
632                false,
633                true,
634            )
635            .unwrap();
636
637            assert!(!config.load_all);
638            assert_eq!(config.load_ids, Some(vec!["BTCUSDT.BINANCE".to_string()]));
639            assert_eq!(
640                config.filters["symbols"],
641                serde_json::json!(["BTCUSDT", "ETHUSDT"])
642            );
643            assert!(!config.log_warnings);
644            assert!(config.query_commission_rates);
645        });
646    }
647}