Skip to main content

nautilus_architect_ax/
config.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Configuration structures for the AX Exchange adapter.
17
18#[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/// Configuration for the AX Exchange live data client.
28#[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    /// Optional API key for authenticated REST/WebSocket requests.
40    pub api_key: Option<SecretString>,
41    /// Optional API secret for authenticated REST/WebSocket requests.
42    pub api_secret: Option<SecretString>,
43    /// Trading environment (Sandbox or Production).
44    #[builder(default)]
45    pub environment: AxEnvironment,
46    /// Optional override for the REST base URL.
47    pub base_url_http: Option<String>,
48    /// Optional override for the public WebSocket URL.
49    pub base_url_ws_public: Option<String>,
50    /// Optional override for the private WebSocket URL.
51    pub base_url_ws_private: Option<String>,
52    /// Optional proxy URL for HTTP and WebSocket transports.
53    pub proxy_url: Option<SecretString>,
54    /// REST timeout in seconds.
55    #[builder(default = 60)]
56    pub http_timeout_secs: u64,
57    /// Maximum retry attempts for REST requests.
58    #[builder(default = 3)]
59    pub max_retries: u32,
60    /// Initial retry backoff in milliseconds.
61    #[builder(default = 1_000)]
62    pub retry_delay_initial_ms: u64,
63    /// Maximum retry backoff in milliseconds.
64    #[builder(default = 10_000)]
65    pub retry_delay_max_ms: u64,
66    /// Heartbeat interval (seconds) for WebSocket clients.
67    #[builder(default = 20)]
68    pub heartbeat_interval_secs: u64,
69    /// Receive window in milliseconds for signed requests.
70    #[builder(default = 5_000)]
71    pub recv_window_ms: u64,
72    /// Interval (minutes) for instrument refresh from REST.
73    #[builder(default = 60)]
74    pub update_instruments_interval_mins: u64,
75    /// Funding rate poll interval in minutes.
76    #[builder(default = 15)]
77    pub funding_rate_poll_interval_mins: u64,
78    /// WebSocket transport backend.
79    ///
80    /// Defaults to `Sockudo` when `transport-sockudo` is enabled, otherwise `Tungstenite`.
81    #[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    /// Creates a configuration with default values.
110    #[must_use]
111    pub fn new() -> Self {
112        Self::default()
113    }
114
115    /// Returns `true` if both API key and secret are available.
116    #[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    /// Returns the REST base URL, considering overrides and environment.
125    #[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    /// Returns the public WebSocket URL, considering overrides and environment.
133    #[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    /// Returns the private WebSocket URL, considering overrides and environment.
141    #[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/// Configuration for the AX Exchange live execution client.
150#[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    /// The account ID for the client.
162    #[builder(default = AccountId::from("AX-001"))]
163    pub account_id: AccountId,
164    /// API key for authenticated requests.
165    pub api_key: Option<SecretString>,
166    /// API secret for authenticated requests.
167    pub api_secret: Option<SecretString>,
168    /// Trading environment (Sandbox or Production).
169    #[builder(default)]
170    pub environment: AxEnvironment,
171    /// Optional override for the REST base URL.
172    pub base_url_http: Option<String>,
173    /// Optional override for the orders REST base URL.
174    pub base_url_orders: Option<String>,
175    /// Optional override for the private WebSocket URL.
176    pub base_url_ws_private: Option<String>,
177    /// Optional proxy URL for HTTP and WebSocket transports.
178    pub proxy_url: Option<SecretString>,
179    /// REST timeout in seconds.
180    #[builder(default = 60)]
181    pub http_timeout_secs: u64,
182    /// Maximum retry attempts for REST requests.
183    #[builder(default = 3)]
184    pub max_retries: u32,
185    /// Initial retry backoff in milliseconds.
186    #[builder(default = 1_000)]
187    pub retry_delay_initial_ms: u64,
188    /// Maximum retry backoff in milliseconds.
189    #[builder(default = 10_000)]
190    pub retry_delay_max_ms: u64,
191    /// Heartbeat interval (seconds) for WebSocket clients.
192    #[builder(default = 30)]
193    pub heartbeat_interval_secs: u64,
194    /// Receive window in milliseconds for signed requests.
195    #[builder(default = 5_000)]
196    pub recv_window_ms: u64,
197    /// Cancel all open orders when the orders WebSocket disconnects.
198    #[builder(default)]
199    pub cancel_on_disconnect: bool,
200    /// WebSocket transport backend.
201    ///
202    /// Defaults to `Sockudo` when `transport-sockudo` is enabled, otherwise `Tungstenite`.
203    #[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    /// Creates a configuration with default values.
232    #[must_use]
233    pub fn new() -> Self {
234        Self::default()
235    }
236
237    /// Returns `true` if both API key and secret are available.
238    #[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    /// Returns the REST base URL, considering overrides and environment.
247    #[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    /// Returns the orders REST base URL, considering overrides and environment.
255    #[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    /// Returns the private WebSocket URL, considering overrides and environment.
263    #[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}