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