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, TraderId};
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(
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    /// Optional API key for authenticated REST/WebSocket requests.
40    pub api_key: Option<String>,
41    /// Optional API secret for authenticated REST/WebSocket requests.
42    pub api_secret: Option<String>,
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<String>,
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 (defaults to `Tungstenite`).
79    #[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    /// Creates a configuration with default values.
91    #[must_use]
92    pub fn new() -> Self {
93        Self::default()
94    }
95
96    /// Returns `true` if both API key and secret are available.
97    #[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    /// Returns the REST base URL, considering overrides and environment.
106    #[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    /// Returns the public WebSocket URL, considering overrides and environment.
114    #[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    /// Returns the private WebSocket URL, considering overrides and environment.
122    #[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/// Configuration for the AX Exchange live execution client.
131#[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    /// The trader ID for the client.
146    #[builder(default = TraderId::from("TRADER-001"))]
147    pub trader_id: TraderId,
148    /// The account ID for the client.
149    #[builder(default = AccountId::from("AX-001"))]
150    pub account_id: AccountId,
151    /// API key for authenticated requests.
152    pub api_key: Option<String>,
153    /// API secret for authenticated requests.
154    pub api_secret: Option<String>,
155    /// Trading environment (Sandbox or Production).
156    #[builder(default)]
157    pub environment: AxEnvironment,
158    /// Optional override for the REST base URL.
159    pub base_url_http: Option<String>,
160    /// Optional override for the orders REST base URL.
161    pub base_url_orders: Option<String>,
162    /// Optional override for the private WebSocket URL.
163    pub base_url_ws_private: Option<String>,
164    /// Optional proxy URL for HTTP and WebSocket transports.
165    pub proxy_url: Option<String>,
166    /// REST timeout in seconds.
167    #[builder(default = 60)]
168    pub http_timeout_secs: u64,
169    /// Maximum retry attempts for REST requests.
170    #[builder(default = 3)]
171    pub max_retries: u32,
172    /// Initial retry backoff in milliseconds.
173    #[builder(default = 1_000)]
174    pub retry_delay_initial_ms: u64,
175    /// Maximum retry backoff in milliseconds.
176    #[builder(default = 10_000)]
177    pub retry_delay_max_ms: u64,
178    /// Heartbeat interval (seconds) for WebSocket clients.
179    #[builder(default = 30)]
180    pub heartbeat_interval_secs: u64,
181    /// Receive window in milliseconds for signed requests.
182    #[builder(default = 5_000)]
183    pub recv_window_ms: u64,
184    /// Cancel all open orders when the orders WebSocket disconnects.
185    #[builder(default)]
186    pub cancel_on_disconnect: bool,
187    /// WebSocket transport backend (defaults to `Tungstenite`).
188    #[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    /// Creates a configuration with default values.
200    #[must_use]
201    pub fn new() -> Self {
202        Self::default()
203    }
204
205    /// Returns `true` if both API key and secret are available.
206    #[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    /// Returns the REST base URL, considering overrides and environment.
215    #[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    /// Returns the orders REST base URL, considering overrides and environment.
223    #[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    /// Returns the private WebSocket URL, considering overrides and environment.
231    #[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}