1use 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 #[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 #[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 #[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}