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_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/// Configuration for the AX Exchange live data client.
29#[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    /// Optional API key for authenticated REST/WebSocket requests.
41    pub api_key: Option<SecretString>,
42    /// Optional API secret for authenticated REST/WebSocket requests.
43    pub api_secret: Option<SecretString>,
44    /// Trading environment (Sandbox or Production).
45    #[builder(default)]
46    pub environment: AxEnvironment,
47    /// Optional override for the REST base URL.
48    pub base_url_http: Option<String>,
49    /// Optional override for the public WebSocket URL.
50    pub base_url_ws_public: Option<String>,
51    /// Optional override for the private WebSocket URL.
52    pub base_url_ws_private: Option<String>,
53    /// Optional proxy URL for HTTP and WebSocket transports.
54    pub proxy_url: Option<SecretString>,
55    /// REST timeout in seconds.
56    #[builder(default = 60)]
57    pub http_timeout_secs: u64,
58    /// Maximum retry attempts for REST requests.
59    #[builder(default = 3)]
60    pub max_retries: u32,
61    /// Initial retry backoff in milliseconds.
62    #[builder(default = 1_000)]
63    pub retry_delay_initial_ms: u64,
64    /// Maximum retry backoff in milliseconds.
65    #[builder(default = 10_000)]
66    pub retry_delay_max_ms: u64,
67    /// Heartbeat interval (seconds) for WebSocket clients.
68    #[builder(default = 20)]
69    pub heartbeat_interval_secs: u64,
70    /// Receive window in milliseconds for signed requests.
71    #[builder(default = 5_000)]
72    pub recv_window_ms: u64,
73    /// Interval (minutes) for instrument refresh from REST.
74    #[builder(default = 60)]
75    pub update_instruments_interval_mins: u64,
76    /// Funding rate poll interval in minutes.
77    #[builder(default = 15)]
78    pub funding_rate_poll_interval_mins: u64,
79    /// Maximum time to wait for an initial, post-reconnect, or recovery order book
80    /// snapshot in seconds.
81    ///
82    /// Set to 0 to disable snapshot deadlines.
83    #[builder(default = DEFAULT_BOOK_SNAPSHOT_TIMEOUT_SECS)]
84    pub book_snapshot_timeout_secs: u64,
85    /// WebSocket transport backend.
86    ///
87    /// Defaults to `Sockudo` when `transport-sockudo` is enabled, otherwise `Tungstenite`.
88    #[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    /// Creates a configuration with default values.
118    #[must_use]
119    pub fn new() -> Self {
120        Self::default()
121    }
122
123    /// Returns `true` if both API key and secret are available.
124    #[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    /// Returns the REST base URL, considering overrides and environment.
133    #[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    /// Returns the public WebSocket URL, considering overrides and environment.
141    #[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    /// Returns the private WebSocket URL, considering overrides and environment.
149    #[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/// Configuration for the AX Exchange live execution client.
158#[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    /// The account ID for the client.
170    #[builder(default = AccountId::from("AX-001"))]
171    pub account_id: AccountId,
172    /// API key for authenticated requests.
173    pub api_key: Option<SecretString>,
174    /// API secret for authenticated requests.
175    pub api_secret: Option<SecretString>,
176    /// Trading environment (Sandbox or Production).
177    #[builder(default)]
178    pub environment: AxEnvironment,
179    /// Optional override for the REST base URL.
180    pub base_url_http: Option<String>,
181    /// Optional override for the orders REST base URL.
182    pub base_url_orders: Option<String>,
183    /// Optional override for the private WebSocket URL.
184    pub base_url_ws_private: Option<String>,
185    /// Optional proxy URL for HTTP and WebSocket transports.
186    pub proxy_url: Option<SecretString>,
187    /// REST timeout in seconds.
188    #[builder(default = 60)]
189    pub http_timeout_secs: u64,
190    /// Maximum retry attempts for REST requests.
191    #[builder(default = 3)]
192    pub max_retries: u32,
193    /// Initial retry backoff in milliseconds.
194    #[builder(default = 1_000)]
195    pub retry_delay_initial_ms: u64,
196    /// Maximum retry backoff in milliseconds.
197    #[builder(default = 10_000)]
198    pub retry_delay_max_ms: u64,
199    /// Heartbeat interval (seconds) for WebSocket clients.
200    #[builder(default = 30)]
201    pub heartbeat_interval_secs: u64,
202    /// Receive window in milliseconds for signed requests.
203    #[builder(default = 5_000)]
204    pub recv_window_ms: u64,
205    /// Cancel all open orders when the orders WebSocket disconnects.
206    #[builder(default)]
207    pub cancel_on_disconnect: bool,
208    /// WebSocket transport backend.
209    ///
210    /// Defaults to `Sockudo` when `transport-sockudo` is enabled, otherwise `Tungstenite`.
211    #[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    /// Creates a configuration with default values.
240    #[must_use]
241    pub fn new() -> Self {
242        Self::default()
243    }
244
245    /// Returns `true` if both API key and secret are available.
246    #[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    /// Returns the REST base URL, considering overrides and environment.
255    #[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    /// Returns the orders REST base URL, considering overrides and environment.
263    #[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    /// Returns the private WebSocket URL, considering overrides and environment.
271    #[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}