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_model::identifiers::{AccountId, TraderId};
21use pyo3::prelude::*;
22use rust_decimal::Decimal;
23
24use crate::{
25    common::enums::{BinanceEnvironment, BinanceMarginType, BinanceProductType},
26    config::{BinanceDataClientConfig, BinanceExecClientConfig},
27};
28
29#[pymethods]
30#[pyo3_stub_gen::derive::gen_stub_pymethods]
31impl BinanceDataClientConfig {
32    /// Configuration for Binance data client.
33    ///
34    /// Ed25519 API keys are required for SBE WebSocket streams.
35    #[new]
36    #[pyo3(signature = (
37        product_types = None,
38        environment = None,
39        base_url_http = None,
40        base_url_ws = None,
41        api_key = None,
42        api_secret = None,
43        instrument_status_poll_secs = None,
44    ))]
45    fn py_new(
46        product_types: Option<Vec<BinanceProductType>>,
47        environment: Option<BinanceEnvironment>,
48        base_url_http: Option<String>,
49        base_url_ws: Option<String>,
50        api_key: Option<String>,
51        api_secret: Option<String>,
52        instrument_status_poll_secs: Option<u64>,
53    ) -> Self {
54        let defaults = Self::default();
55        Self {
56            product_types: product_types.unwrap_or(defaults.product_types),
57            environment: environment.unwrap_or(defaults.environment),
58            base_url_http: base_url_http.or(defaults.base_url_http),
59            base_url_ws: base_url_ws.or(defaults.base_url_ws),
60            api_key: api_key.or(defaults.api_key),
61            api_secret: api_secret.or(defaults.api_secret),
62            instrument_status_poll_secs: instrument_status_poll_secs
63                .unwrap_or(defaults.instrument_status_poll_secs),
64            transport_backend: defaults.transport_backend,
65        }
66    }
67
68    fn __repr__(&self) -> String {
69        format!("{self:?}")
70    }
71}
72
73#[pymethods]
74#[pyo3_stub_gen::derive::gen_stub_pymethods]
75impl BinanceExecClientConfig {
76    /// Configuration for Binance execution client.
77    ///
78    /// Ed25519 API keys are required for execution clients. Binance deprecated
79    /// listenKey-based user data streams in favor of WebSocket API authentication,
80    /// which only supports Ed25519.
81    #[new]
82    #[pyo3(signature = (
83        trader_id,
84        account_id,
85        product_types = None,
86        environment = None,
87        base_url_http = None,
88        base_url_ws = None,
89        base_url_ws_trading = None,
90        use_ws_trading = true,
91        use_position_ids = true,
92        default_taker_fee = None,
93        api_key = None,
94        api_secret = None,
95        futures_leverages = None,
96        futures_margin_types = None,
97        treat_expired_as_canceled = false,
98        use_trade_lite = false,
99    ))]
100    #[expect(clippy::too_many_arguments)]
101    fn py_new(
102        trader_id: TraderId,
103        account_id: AccountId,
104        product_types: Option<Vec<BinanceProductType>>,
105        environment: Option<BinanceEnvironment>,
106        base_url_http: Option<String>,
107        base_url_ws: Option<String>,
108        base_url_ws_trading: Option<String>,
109        use_ws_trading: bool,
110        use_position_ids: bool,
111        default_taker_fee: Option<f64>,
112        api_key: Option<String>,
113        api_secret: Option<String>,
114        futures_leverages: Option<HashMap<String, u32>>,
115        futures_margin_types: Option<HashMap<String, BinanceMarginType>>,
116        treat_expired_as_canceled: bool,
117        use_trade_lite: bool,
118    ) -> Self {
119        let defaults = Self::default();
120        Self {
121            trader_id,
122            account_id,
123            product_types: product_types.unwrap_or(defaults.product_types),
124            environment: environment.unwrap_or(defaults.environment),
125            base_url_http: base_url_http.or(defaults.base_url_http),
126            base_url_ws: base_url_ws.or(defaults.base_url_ws),
127            base_url_ws_trading: base_url_ws_trading.or(defaults.base_url_ws_trading),
128            use_ws_trading,
129            use_position_ids,
130            default_taker_fee: default_taker_fee
131                .map_or_else(|| Ok(defaults.default_taker_fee), Decimal::try_from)
132                .unwrap_or(defaults.default_taker_fee),
133            api_key: api_key.or(defaults.api_key),
134            api_secret: api_secret.or(defaults.api_secret),
135            futures_leverages,
136            futures_margin_types,
137            treat_expired_as_canceled,
138            use_trade_lite,
139            transport_backend: defaults.transport_backend,
140        }
141    }
142
143    fn __repr__(&self) -> String {
144        format!("{self:?}")
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use rstest::rstest;
151    use rust_decimal::Decimal;
152
153    use super::*;
154
155    #[rstest]
156    fn test_data_client_py_new_uses_defaults_for_omitted_fields() {
157        let config = BinanceDataClientConfig::py_new(None, None, None, None, None, None, None);
158        let defaults = BinanceDataClientConfig::default();
159
160        assert_eq!(config.product_types, defaults.product_types);
161        assert_eq!(config.environment, defaults.environment);
162        assert_eq!(config.base_url_http, defaults.base_url_http);
163        assert_eq!(config.base_url_ws, defaults.base_url_ws);
164        assert_eq!(config.api_key, defaults.api_key);
165        assert_eq!(config.api_secret, defaults.api_secret);
166        assert_eq!(
167            config.instrument_status_poll_secs,
168            defaults.instrument_status_poll_secs
169        );
170    }
171
172    #[rstest]
173    fn test_data_client_py_new_uses_explicit_overrides() {
174        let config = BinanceDataClientConfig::py_new(
175            Some(vec![BinanceProductType::UsdM]),
176            Some(BinanceEnvironment::Testnet),
177            Some("https://http.example".to_string()),
178            Some("wss://ws.example".to_string()),
179            Some("api-key".to_string()),
180            Some("api-secret".to_string()),
181            Some(15),
182        );
183
184        assert_eq!(config.product_types, vec![BinanceProductType::UsdM]);
185        assert_eq!(config.environment, BinanceEnvironment::Testnet);
186        assert_eq!(
187            config.base_url_http.as_deref(),
188            Some("https://http.example")
189        );
190        assert_eq!(config.base_url_ws.as_deref(), Some("wss://ws.example"));
191        assert_eq!(config.api_key.as_deref(), Some("api-key"));
192        assert_eq!(config.api_secret.as_deref(), Some("api-secret"));
193        assert_eq!(config.instrument_status_poll_secs, 15);
194    }
195
196    #[rstest]
197    fn test_exec_client_py_new_uses_defaults_for_optional_fields() {
198        let trader_id = TraderId::from("TRADER-001");
199        let account_id = AccountId::from("BINANCE-001");
200        let config = BinanceExecClientConfig::py_new(
201            trader_id, account_id, None, None, None, None, None, true, true, None, None, None,
202            None, None, false, false,
203        );
204        let defaults = BinanceExecClientConfig::default();
205
206        assert_eq!(config.trader_id, trader_id);
207        assert_eq!(config.account_id, account_id);
208        assert_eq!(config.product_types, defaults.product_types);
209        assert_eq!(config.environment, defaults.environment);
210        assert_eq!(config.base_url_http, defaults.base_url_http);
211        assert_eq!(config.base_url_ws, defaults.base_url_ws);
212        assert_eq!(config.base_url_ws_trading, defaults.base_url_ws_trading);
213        assert_eq!(config.default_taker_fee, defaults.default_taker_fee);
214        assert_eq!(config.api_key, defaults.api_key);
215        assert_eq!(config.api_secret, defaults.api_secret);
216        assert_eq!(config.futures_leverages, defaults.futures_leverages);
217        assert_eq!(config.futures_margin_types, defaults.futures_margin_types);
218        assert_eq!(
219            config.treat_expired_as_canceled,
220            defaults.treat_expired_as_canceled
221        );
222    }
223
224    #[rstest]
225    fn test_exec_client_py_new_preserves_explicit_overrides() {
226        use std::collections::HashMap;
227
228        use crate::common::enums::BinanceMarginType;
229
230        let leverages = HashMap::from([("BTCUSDT".to_string(), 20)]);
231        let margin_types = HashMap::from([("BTCUSDT".to_string(), BinanceMarginType::Cross)]);
232
233        let config = BinanceExecClientConfig::py_new(
234            TraderId::from("TRADER-002"),
235            AccountId::from("BINANCE-002"),
236            Some(vec![BinanceProductType::UsdM]),
237            Some(BinanceEnvironment::Demo),
238            Some("https://http.example".to_string()),
239            Some("wss://stream.example".to_string()),
240            Some("wss://trade.example".to_string()),
241            false,
242            false,
243            Some(0.0015),
244            Some("api-key".to_string()),
245            Some("api-secret".to_string()),
246            Some(leverages.clone()),
247            Some(margin_types.clone()),
248            true,
249            true,
250        );
251
252        assert_eq!(config.product_types, vec![BinanceProductType::UsdM]);
253        assert_eq!(config.environment, BinanceEnvironment::Demo);
254        assert_eq!(
255            config.base_url_http.as_deref(),
256            Some("https://http.example")
257        );
258        assert_eq!(config.base_url_ws.as_deref(), Some("wss://stream.example"));
259        assert_eq!(
260            config.base_url_ws_trading.as_deref(),
261            Some("wss://trade.example")
262        );
263        assert!(!config.use_ws_trading);
264        assert!(!config.use_position_ids);
265        assert_eq!(config.default_taker_fee, Decimal::try_from(0.0015).unwrap());
266        assert_eq!(config.api_key.as_deref(), Some("api-key"));
267        assert_eq!(config.api_secret.as_deref(), Some("api-secret"));
268        assert_eq!(config.futures_leverages, Some(leverages));
269        assert_eq!(config.futures_margin_types, Some(margin_types));
270        assert!(config.treat_expired_as_canceled);
271        assert!(config.use_trade_lite);
272    }
273
274    #[rstest]
275    fn test_exec_client_py_new_uses_default_fee_for_invalid_float() {
276        let defaults = BinanceExecClientConfig::default();
277        let config = BinanceExecClientConfig::py_new(
278            TraderId::from("TRADER-003"),
279            AccountId::from("BINANCE-003"),
280            None,
281            None,
282            None,
283            None,
284            None,
285            true,
286            true,
287            Some(f64::NAN),
288            None,
289            None,
290            None,
291            None,
292            false,
293            false,
294        );
295
296        assert_eq!(config.default_taker_fee, defaults.default_taker_fee);
297    }
298}