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