1#[cfg(test)]
19use nautilus_core::string::secret::REDACTED;
20use nautilus_core::string::secret::SecretString;
21use nautilus_model::identifiers::AccountId;
22use nautilus_network::websocket::TransportBackend;
23use serde::{Deserialize, Serialize};
24
25use crate::common::{credential::credential_env_vars, enums::AxEnvironment};
26
27#[cfg_attr(
29 feature = "python",
30 pyo3::pyclass(module = "nautilus_trader.adapters.architect_ax", from_py_object)
31)]
32#[cfg_attr(
33 feature = "python",
34 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.architect_ax")
35)]
36#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
37#[serde(default, deny_unknown_fields)]
38pub struct AxDataClientConfig {
39 pub api_key: Option<SecretString>,
41 pub api_secret: Option<SecretString>,
43 #[builder(default)]
45 pub environment: AxEnvironment,
46 pub base_url_http: Option<String>,
48 pub base_url_ws_public: Option<String>,
50 pub base_url_ws_private: Option<String>,
52 pub proxy_url: Option<SecretString>,
54 #[builder(default = 60)]
56 pub http_timeout_secs: u64,
57 #[builder(default = 3)]
59 pub max_retries: u32,
60 #[builder(default = 1_000)]
62 pub retry_delay_initial_ms: u64,
63 #[builder(default = 10_000)]
65 pub retry_delay_max_ms: u64,
66 #[builder(default = 20)]
68 pub heartbeat_interval_secs: u64,
69 #[builder(default = 5_000)]
71 pub recv_window_ms: u64,
72 #[builder(default = 60)]
74 pub update_instruments_interval_mins: u64,
75 #[builder(default = 15)]
77 pub funding_rate_poll_interval_mins: u64,
78 #[builder(default)]
82 pub transport_backend: TransportBackend,
83}
84
85#[cfg(feature = "python")]
86nautilus_core::impl_pyo3_config_getters!(AxDataClientConfig {
87 environment: AxEnvironment,
88 base_url_http: Option<String>,
89 base_url_ws_public: Option<String>,
90 base_url_ws_private: Option<String>,
91 http_timeout_secs: u64,
92 max_retries: u32,
93 retry_delay_initial_ms: u64,
94 retry_delay_max_ms: u64,
95 heartbeat_interval_secs: u64,
96 recv_window_ms: u64,
97 update_instruments_interval_mins: u64,
98 funding_rate_poll_interval_mins: u64,
99 transport_backend: TransportBackend,
100});
101
102impl Default for AxDataClientConfig {
103 fn default() -> Self {
104 Self::builder().build()
105 }
106}
107
108impl AxDataClientConfig {
109 #[must_use]
111 pub fn new() -> Self {
112 Self::default()
113 }
114
115 #[must_use]
117 pub fn has_api_credentials(&self) -> bool {
118 let (key_var, secret_var) = credential_env_vars();
119 let has_key = self.api_key.is_some() || std::env::var(key_var).is_ok();
120 let has_secret = self.api_secret.is_some() || std::env::var(secret_var).is_ok();
121 has_key && has_secret
122 }
123
124 #[must_use]
126 pub fn http_base_url(&self) -> String {
127 self.base_url_http
128 .clone()
129 .unwrap_or_else(|| self.environment.http_url().to_string())
130 }
131
132 #[must_use]
134 pub fn ws_public_url(&self) -> String {
135 self.base_url_ws_public
136 .clone()
137 .unwrap_or_else(|| self.environment.ws_md_url().to_string())
138 }
139
140 #[must_use]
142 pub fn ws_private_url(&self) -> String {
143 self.base_url_ws_private
144 .clone()
145 .unwrap_or_else(|| self.environment.ws_orders_url().to_string())
146 }
147}
148
149#[cfg_attr(
151 feature = "python",
152 pyo3::pyclass(module = "nautilus_trader.adapters.architect_ax", from_py_object)
153)]
154#[cfg_attr(
155 feature = "python",
156 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.architect_ax")
157)]
158#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
159#[serde(default, deny_unknown_fields)]
160pub struct AxExecutionClientConfig {
161 #[builder(default = AccountId::from("AX-001"))]
163 pub account_id: AccountId,
164 pub api_key: Option<SecretString>,
166 pub api_secret: Option<SecretString>,
168 #[builder(default)]
170 pub environment: AxEnvironment,
171 pub base_url_http: Option<String>,
173 pub base_url_orders: Option<String>,
175 pub base_url_ws_private: Option<String>,
177 pub proxy_url: Option<SecretString>,
179 #[builder(default = 60)]
181 pub http_timeout_secs: u64,
182 #[builder(default = 3)]
184 pub max_retries: u32,
185 #[builder(default = 1_000)]
187 pub retry_delay_initial_ms: u64,
188 #[builder(default = 10_000)]
190 pub retry_delay_max_ms: u64,
191 #[builder(default = 30)]
193 pub heartbeat_interval_secs: u64,
194 #[builder(default = 5_000)]
196 pub recv_window_ms: u64,
197 #[builder(default)]
199 pub cancel_on_disconnect: bool,
200 #[builder(default)]
204 pub transport_backend: TransportBackend,
205}
206
207#[cfg(feature = "python")]
208nautilus_core::impl_pyo3_config_getters!(AxExecutionClientConfig {
209 account_id: AccountId,
210 environment: AxEnvironment,
211 base_url_http: Option<String>,
212 base_url_orders: Option<String>,
213 base_url_ws_private: Option<String>,
214 http_timeout_secs: u64,
215 max_retries: u32,
216 retry_delay_initial_ms: u64,
217 retry_delay_max_ms: u64,
218 heartbeat_interval_secs: u64,
219 recv_window_ms: u64,
220 cancel_on_disconnect: bool,
221 transport_backend: TransportBackend,
222});
223
224impl Default for AxExecutionClientConfig {
225 fn default() -> Self {
226 Self::builder().build()
227 }
228}
229
230impl AxExecutionClientConfig {
231 #[must_use]
233 pub fn new() -> Self {
234 Self::default()
235 }
236
237 #[must_use]
239 pub fn has_api_credentials(&self) -> bool {
240 let (key_var, secret_var) = credential_env_vars();
241 let has_key = self.api_key.is_some() || std::env::var(key_var).is_ok();
242 let has_secret = self.api_secret.is_some() || std::env::var(secret_var).is_ok();
243 has_key && has_secret
244 }
245
246 #[must_use]
248 pub fn http_base_url(&self) -> String {
249 self.base_url_http
250 .clone()
251 .unwrap_or_else(|| self.environment.http_url().to_string())
252 }
253
254 #[must_use]
256 pub fn orders_base_url(&self) -> String {
257 self.base_url_orders
258 .clone()
259 .unwrap_or_else(|| self.environment.orders_url().to_string())
260 }
261
262 #[must_use]
264 pub fn ws_private_url(&self) -> String {
265 self.base_url_ws_private
266 .clone()
267 .unwrap_or_else(|| self.environment.ws_orders_url().to_string())
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use rstest::rstest;
274
275 use super::*;
276 use crate::common::consts::{
277 AX_HTTP_SANDBOX_URL, AX_HTTP_URL, AX_ORDERS_SANDBOX_URL, AX_ORDERS_URL, AX_WS_PRIVATE_URL,
278 AX_WS_PUBLIC_URL, AX_WS_SANDBOX_PRIVATE_URL, AX_WS_SANDBOX_PUBLIC_URL,
279 };
280
281 #[rstest]
282 fn test_data_config_sandbox_urls_match_consts() {
283 let config = AxDataClientConfig::builder()
284 .environment(AxEnvironment::Sandbox)
285 .build();
286 assert_eq!(config.http_base_url(), AX_HTTP_SANDBOX_URL);
287 assert_eq!(config.ws_public_url(), AX_WS_SANDBOX_PUBLIC_URL);
288 assert_eq!(config.ws_private_url(), AX_WS_SANDBOX_PRIVATE_URL);
289 }
290
291 #[rstest]
292 fn test_data_config_production_urls_match_consts() {
293 let config = AxDataClientConfig::builder()
294 .environment(AxEnvironment::Production)
295 .build();
296 assert_eq!(config.http_base_url(), AX_HTTP_URL);
297 assert_eq!(config.ws_public_url(), AX_WS_PUBLIC_URL);
298 assert_eq!(config.ws_private_url(), AX_WS_PRIVATE_URL);
299 }
300
301 #[rstest]
302 fn test_data_config_url_overrides() {
303 let config = AxDataClientConfig::builder()
304 .base_url_http("http://custom".to_string())
305 .base_url_ws_public("ws://custom-pub".to_string())
306 .base_url_ws_private("ws://custom-priv".to_string())
307 .build();
308 assert_eq!(config.http_base_url(), "http://custom");
309 assert_eq!(config.ws_public_url(), "ws://custom-pub");
310 assert_eq!(config.ws_private_url(), "ws://custom-priv");
311 }
312
313 #[rstest]
314 fn test_exec_config_sandbox_urls_match_consts() {
315 let config = AxExecutionClientConfig::builder()
316 .environment(AxEnvironment::Sandbox)
317 .build();
318 assert_eq!(config.http_base_url(), AX_HTTP_SANDBOX_URL);
319 assert_eq!(config.orders_base_url(), AX_ORDERS_SANDBOX_URL);
320 assert_eq!(config.ws_private_url(), AX_WS_SANDBOX_PRIVATE_URL);
321 }
322
323 #[rstest]
324 fn test_exec_config_production_urls_match_consts() {
325 let config = AxExecutionClientConfig::builder()
326 .environment(AxEnvironment::Production)
327 .build();
328 assert_eq!(config.http_base_url(), AX_HTTP_URL);
329 assert_eq!(config.orders_base_url(), AX_ORDERS_URL);
330 assert_eq!(config.ws_private_url(), AX_WS_PRIVATE_URL);
331 }
332
333 #[rstest]
334 fn test_exec_config_cancel_on_disconnect_default_false() {
335 let config = AxExecutionClientConfig::default();
336 assert!(!config.cancel_on_disconnect);
337 }
338
339 #[rstest]
340 fn test_exec_config_cancel_on_disconnect_enabled() {
341 let config = AxExecutionClientConfig::builder()
342 .cancel_on_disconnect(true)
343 .build();
344 assert!(config.cancel_on_disconnect);
345 }
346
347 #[rstest]
348 fn test_default_environment_is_sandbox() {
349 let data = AxDataClientConfig::default();
350 assert_eq!(data.environment, AxEnvironment::Sandbox);
351
352 let exec = AxExecutionClientConfig::default();
353 assert_eq!(exec.environment, AxEnvironment::Sandbox);
354 }
355
356 #[rstest]
357 fn test_data_config_toml_minimal() {
358 let config: AxDataClientConfig = toml::from_str(
359 r#"
360environment = "PRODUCTION"
361http_timeout_secs = 30
362heartbeat_interval_secs = 10
363update_instruments_interval_mins = 5
364"#,
365 )
366 .unwrap();
367
368 assert_eq!(config.environment, AxEnvironment::Production);
369 assert_eq!(config.http_timeout_secs, 30);
370 assert_eq!(config.heartbeat_interval_secs, 10);
371 assert_eq!(config.update_instruments_interval_mins, 5);
372 }
373
374 #[rstest]
375 fn test_exec_config_toml_empty_uses_defaults() {
376 let config: AxExecutionClientConfig = toml::from_str("").unwrap();
377 let expected = AxExecutionClientConfig::default();
378 assert_eq!(config.account_id, expected.account_id);
379 assert_eq!(config.environment, expected.environment);
380 assert_eq!(config.http_timeout_secs, expected.http_timeout_secs);
381 assert_eq!(
382 config.heartbeat_interval_secs,
383 expected.heartbeat_interval_secs,
384 );
385 assert_eq!(config.recv_window_ms, expected.recv_window_ms);
386 assert_eq!(config.cancel_on_disconnect, expected.cancel_on_disconnect);
387 assert_eq!(config.transport_backend, expected.transport_backend);
388 }
389
390 #[rstest]
391 fn test_config_debug_redacts_credentials() {
392 let data = AxDataClientConfig {
393 api_key: Some("data-key".into()),
394 api_secret: Some("data-secret".into()),
395 proxy_url: Some("http://user:data-proxy@localhost".into()),
396 ..Default::default()
397 };
398 let execution = AxExecutionClientConfig {
399 api_key: Some("exec-key".into()),
400 api_secret: Some("exec-secret".into()),
401 proxy_url: Some("http://user:exec-proxy@localhost".into()),
402 ..Default::default()
403 };
404
405 let formatted = format!("{data:?} {execution:?}");
406
407 assert_eq!(formatted.matches(REDACTED).count(), 6);
408
409 for secret in [
410 "data-key",
411 "data-secret",
412 "data-proxy",
413 "exec-key",
414 "exec-secret",
415 "exec-proxy",
416 ] {
417 assert!(!formatted.contains(secret));
418 }
419 }
420}