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
18use 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/// Configuration for the AX Exchange live data client.
25#[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    /// Optional API key for authenticated REST/WebSocket requests.
37    pub api_key: Option<String>,
38    /// Optional API secret for authenticated REST/WebSocket requests.
39    pub api_secret: Option<String>,
40    /// Trading environment (Sandbox or Production).
41    #[builder(default)]
42    pub environment: AxEnvironment,
43    /// Optional override for the REST base URL.
44    pub base_url_http: Option<String>,
45    /// Optional override for the public WebSocket URL.
46    pub base_url_ws_public: Option<String>,
47    /// Optional override for the private WebSocket URL.
48    pub base_url_ws_private: Option<String>,
49    /// Optional proxy URL for HTTP and WebSocket transports.
50    pub proxy_url: Option<String>,
51    /// REST timeout in seconds.
52    #[builder(default = 60)]
53    pub http_timeout_secs: u64,
54    /// Maximum retry attempts for REST requests.
55    #[builder(default = 3)]
56    pub max_retries: u32,
57    /// Initial retry backoff in milliseconds.
58    #[builder(default = 1_000)]
59    pub retry_delay_initial_ms: u64,
60    /// Maximum retry backoff in milliseconds.
61    #[builder(default = 10_000)]
62    pub retry_delay_max_ms: u64,
63    /// Heartbeat interval (seconds) for WebSocket clients.
64    #[builder(default = 20)]
65    pub heartbeat_interval_secs: u64,
66    /// Receive window in milliseconds for signed requests.
67    #[builder(default = 5_000)]
68    pub recv_window_ms: u64,
69    /// Interval (minutes) for instrument refresh from REST.
70    #[builder(default = 60)]
71    pub update_instruments_interval_mins: u64,
72    /// Funding rate poll interval in minutes.
73    #[builder(default = 15)]
74    pub funding_rate_poll_interval_mins: u64,
75    /// WebSocket transport backend.
76    ///
77    /// Defaults to `Sockudo` when `transport-sockudo` is enabled, otherwise `Tungstenite`.
78    #[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    /// Creates a configuration with default values.
107    #[must_use]
108    pub fn new() -> Self {
109        Self::default()
110    }
111
112    /// Returns `true` if both API key and secret are available.
113    #[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    /// Returns the REST base URL, considering overrides and environment.
122    #[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    /// Returns the public WebSocket URL, considering overrides and environment.
130    #[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    /// Returns the private WebSocket URL, considering overrides and environment.
138    #[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/// Configuration for the AX Exchange live execution client.
147#[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    /// The account ID for the client.
159    #[builder(default = AccountId::from("AX-001"))]
160    pub account_id: AccountId,
161    /// API key for authenticated requests.
162    pub api_key: Option<String>,
163    /// API secret for authenticated requests.
164    pub api_secret: Option<String>,
165    /// Trading environment (Sandbox or Production).
166    #[builder(default)]
167    pub environment: AxEnvironment,
168    /// Optional override for the REST base URL.
169    pub base_url_http: Option<String>,
170    /// Optional override for the orders REST base URL.
171    pub base_url_orders: Option<String>,
172    /// Optional override for the private WebSocket URL.
173    pub base_url_ws_private: Option<String>,
174    /// Optional proxy URL for HTTP and WebSocket transports.
175    pub proxy_url: Option<String>,
176    /// REST timeout in seconds.
177    #[builder(default = 60)]
178    pub http_timeout_secs: u64,
179    /// Maximum retry attempts for REST requests.
180    #[builder(default = 3)]
181    pub max_retries: u32,
182    /// Initial retry backoff in milliseconds.
183    #[builder(default = 1_000)]
184    pub retry_delay_initial_ms: u64,
185    /// Maximum retry backoff in milliseconds.
186    #[builder(default = 10_000)]
187    pub retry_delay_max_ms: u64,
188    /// Heartbeat interval (seconds) for WebSocket clients.
189    #[builder(default = 30)]
190    pub heartbeat_interval_secs: u64,
191    /// Receive window in milliseconds for signed requests.
192    #[builder(default = 5_000)]
193    pub recv_window_ms: u64,
194    /// Cancel all open orders when the orders WebSocket disconnects.
195    #[builder(default)]
196    pub cancel_on_disconnect: bool,
197    /// WebSocket transport backend.
198    ///
199    /// Defaults to `Sockudo` when `transport-sockudo` is enabled, otherwise `Tungstenite`.
200    #[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    /// Creates a configuration with default values.
229    #[must_use]
230    pub fn new() -> Self {
231        Self::default()
232    }
233
234    /// Returns `true` if both API key and secret are available.
235    #[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    /// Returns the REST base URL, considering overrides and environment.
244    #[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    /// Returns the orders REST base URL, considering overrides and environment.
252    #[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    /// Returns the private WebSocket URL, considering overrides and environment.
260    #[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}