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