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