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