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