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