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