1use nautilus_model::identifiers::{AccountId, TraderId};
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(
28 module = "nautilus_trader.core.nautilus_pyo3.architect_ax",
29 from_py_object
30 )
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<String>,
41 pub api_secret: Option<String>,
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<String>,
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)]
80 pub transport_backend: TransportBackend,
81}
82
83impl Default for AxDataClientConfig {
84 fn default() -> Self {
85 Self::builder().build()
86 }
87}
88
89impl AxDataClientConfig {
90 #[must_use]
92 pub fn new() -> Self {
93 Self::default()
94 }
95
96 #[must_use]
98 pub fn has_api_credentials(&self) -> bool {
99 let (key_var, secret_var) = credential_env_vars();
100 let has_key = self.api_key.is_some() || std::env::var(key_var).is_ok();
101 let has_secret = self.api_secret.is_some() || std::env::var(secret_var).is_ok();
102 has_key && has_secret
103 }
104
105 #[must_use]
107 pub fn http_base_url(&self) -> String {
108 self.base_url_http
109 .clone()
110 .unwrap_or_else(|| self.environment.http_url().to_string())
111 }
112
113 #[must_use]
115 pub fn ws_public_url(&self) -> String {
116 self.base_url_ws_public
117 .clone()
118 .unwrap_or_else(|| self.environment.ws_md_url().to_string())
119 }
120
121 #[must_use]
123 pub fn ws_private_url(&self) -> String {
124 self.base_url_ws_private
125 .clone()
126 .unwrap_or_else(|| self.environment.ws_orders_url().to_string())
127 }
128}
129
130#[cfg_attr(
132 feature = "python",
133 pyo3::pyclass(
134 module = "nautilus_trader.core.nautilus_pyo3.architect_ax",
135 from_py_object
136 )
137)]
138#[cfg_attr(
139 feature = "python",
140 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.architect_ax")
141)]
142#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
143#[serde(default, deny_unknown_fields)]
144pub struct AxExecClientConfig {
145 #[builder(default = TraderId::from("TRADER-001"))]
147 pub trader_id: TraderId,
148 #[builder(default = AccountId::from("AX-001"))]
150 pub account_id: AccountId,
151 pub api_key: Option<String>,
153 pub api_secret: Option<String>,
155 #[builder(default)]
157 pub environment: AxEnvironment,
158 pub base_url_http: Option<String>,
160 pub base_url_orders: Option<String>,
162 pub base_url_ws_private: Option<String>,
164 pub proxy_url: Option<String>,
166 #[builder(default = 60)]
168 pub http_timeout_secs: u64,
169 #[builder(default = 3)]
171 pub max_retries: u32,
172 #[builder(default = 1_000)]
174 pub retry_delay_initial_ms: u64,
175 #[builder(default = 10_000)]
177 pub retry_delay_max_ms: u64,
178 #[builder(default = 30)]
180 pub heartbeat_interval_secs: u64,
181 #[builder(default = 5_000)]
183 pub recv_window_ms: u64,
184 #[builder(default)]
186 pub cancel_on_disconnect: bool,
187 #[builder(default)]
189 pub transport_backend: TransportBackend,
190}
191
192impl Default for AxExecClientConfig {
193 fn default() -> Self {
194 Self::builder().build()
195 }
196}
197
198impl AxExecClientConfig {
199 #[must_use]
201 pub fn new() -> Self {
202 Self::default()
203 }
204
205 #[must_use]
207 pub fn has_api_credentials(&self) -> bool {
208 let (key_var, secret_var) = credential_env_vars();
209 let has_key = self.api_key.is_some() || std::env::var(key_var).is_ok();
210 let has_secret = self.api_secret.is_some() || std::env::var(secret_var).is_ok();
211 has_key && has_secret
212 }
213
214 #[must_use]
216 pub fn http_base_url(&self) -> String {
217 self.base_url_http
218 .clone()
219 .unwrap_or_else(|| self.environment.http_url().to_string())
220 }
221
222 #[must_use]
224 pub fn orders_base_url(&self) -> String {
225 self.base_url_orders
226 .clone()
227 .unwrap_or_else(|| self.environment.orders_url().to_string())
228 }
229
230 #[must_use]
232 pub fn ws_private_url(&self) -> String {
233 self.base_url_ws_private
234 .clone()
235 .unwrap_or_else(|| self.environment.ws_orders_url().to_string())
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use rstest::rstest;
242
243 use super::*;
244 use crate::common::consts::{
245 AX_HTTP_SANDBOX_URL, AX_HTTP_URL, AX_ORDERS_SANDBOX_URL, AX_ORDERS_URL, AX_WS_PRIVATE_URL,
246 AX_WS_PUBLIC_URL, AX_WS_SANDBOX_PRIVATE_URL, AX_WS_SANDBOX_PUBLIC_URL,
247 };
248
249 #[rstest]
250 fn test_data_config_sandbox_urls_match_consts() {
251 let config = AxDataClientConfig::builder()
252 .environment(AxEnvironment::Sandbox)
253 .build();
254 assert_eq!(config.http_base_url(), AX_HTTP_SANDBOX_URL);
255 assert_eq!(config.ws_public_url(), AX_WS_SANDBOX_PUBLIC_URL);
256 assert_eq!(config.ws_private_url(), AX_WS_SANDBOX_PRIVATE_URL);
257 }
258
259 #[rstest]
260 fn test_data_config_production_urls_match_consts() {
261 let config = AxDataClientConfig::builder()
262 .environment(AxEnvironment::Production)
263 .build();
264 assert_eq!(config.http_base_url(), AX_HTTP_URL);
265 assert_eq!(config.ws_public_url(), AX_WS_PUBLIC_URL);
266 assert_eq!(config.ws_private_url(), AX_WS_PRIVATE_URL);
267 }
268
269 #[rstest]
270 fn test_data_config_url_overrides() {
271 let config = AxDataClientConfig::builder()
272 .base_url_http("http://custom".to_string())
273 .base_url_ws_public("ws://custom-pub".to_string())
274 .base_url_ws_private("ws://custom-priv".to_string())
275 .build();
276 assert_eq!(config.http_base_url(), "http://custom");
277 assert_eq!(config.ws_public_url(), "ws://custom-pub");
278 assert_eq!(config.ws_private_url(), "ws://custom-priv");
279 }
280
281 #[rstest]
282 fn test_exec_config_sandbox_urls_match_consts() {
283 let config = AxExecClientConfig::builder()
284 .environment(AxEnvironment::Sandbox)
285 .build();
286 assert_eq!(config.http_base_url(), AX_HTTP_SANDBOX_URL);
287 assert_eq!(config.orders_base_url(), AX_ORDERS_SANDBOX_URL);
288 assert_eq!(config.ws_private_url(), AX_WS_SANDBOX_PRIVATE_URL);
289 }
290
291 #[rstest]
292 fn test_exec_config_production_urls_match_consts() {
293 let config = AxExecClientConfig::builder()
294 .environment(AxEnvironment::Production)
295 .build();
296 assert_eq!(config.http_base_url(), AX_HTTP_URL);
297 assert_eq!(config.orders_base_url(), AX_ORDERS_URL);
298 assert_eq!(config.ws_private_url(), AX_WS_PRIVATE_URL);
299 }
300
301 #[rstest]
302 fn test_exec_config_cancel_on_disconnect_default_false() {
303 let config = AxExecClientConfig::default();
304 assert!(!config.cancel_on_disconnect);
305 }
306
307 #[rstest]
308 fn test_exec_config_cancel_on_disconnect_enabled() {
309 let config = AxExecClientConfig::builder()
310 .cancel_on_disconnect(true)
311 .build();
312 assert!(config.cancel_on_disconnect);
313 }
314
315 #[rstest]
316 fn test_default_environment_is_sandbox() {
317 let data = AxDataClientConfig::default();
318 assert_eq!(data.environment, AxEnvironment::Sandbox);
319
320 let exec = AxExecClientConfig::default();
321 assert_eq!(exec.environment, AxEnvironment::Sandbox);
322 }
323
324 #[rstest]
325 fn test_data_config_toml_minimal() {
326 let config: AxDataClientConfig = toml::from_str(
327 r#"
328environment = "PRODUCTION"
329http_timeout_secs = 30
330heartbeat_interval_secs = 10
331update_instruments_interval_mins = 5
332"#,
333 )
334 .unwrap();
335
336 assert_eq!(config.environment, AxEnvironment::Production);
337 assert_eq!(config.http_timeout_secs, 30);
338 assert_eq!(config.heartbeat_interval_secs, 10);
339 assert_eq!(config.update_instruments_interval_mins, 5);
340 }
341
342 #[rstest]
343 fn test_exec_config_toml_empty_uses_defaults() {
344 let config: AxExecClientConfig = toml::from_str("").unwrap();
345 let expected = AxExecClientConfig::default();
346
347 assert_eq!(config.trader_id, expected.trader_id);
348 assert_eq!(config.account_id, expected.account_id);
349 assert_eq!(config.environment, expected.environment);
350 assert_eq!(config.http_timeout_secs, expected.http_timeout_secs);
351 assert_eq!(
352 config.heartbeat_interval_secs,
353 expected.heartbeat_interval_secs,
354 );
355 assert_eq!(config.recv_window_ms, expected.recv_window_ms);
356 assert_eq!(config.cancel_on_disconnect, expected.cancel_on_disconnect);
357 assert_eq!(config.transport_backend, expected.transport_backend);
358 }
359}