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