Skip to main content

nautilus_architect_ax/
factories.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//! Factory functions for creating AX Exchange clients and components.
17
18use std::{any::Any, cell::RefCell, rc::Rc};
19
20use nautilus_common::{
21    cache::CacheView,
22    clients::{DataClient, ExecutionClient},
23    clock::Clock,
24    factories::{ClientConfig, DataClientFactory, ExecutionClientFactory},
25    live::runner::try_get_exec_event_sender,
26};
27use nautilus_live::ExecutionClientCore;
28use nautilus_model::{
29    enums::{AccountType, OmsType},
30    identifiers::{ClientId, TraderId},
31};
32
33use crate::{
34    common::{
35        consts::{AX, AX_VENUE},
36        credential::Credential,
37    },
38    config::{AxDataClientConfig, AxExecutionClientConfig},
39    data::AxDataClient,
40    execution::AxExecutionClient,
41    http::client::AxHttpClient,
42    websocket::data::AxMdWebSocketClient,
43};
44
45impl ClientConfig for AxDataClientConfig {
46    fn as_any(&self) -> &dyn Any {
47        self
48    }
49}
50
51impl ClientConfig for AxExecutionClientConfig {
52    fn as_any(&self) -> &dyn Any {
53        self
54    }
55}
56
57/// Factory for creating AX Exchange data clients.
58#[derive(Debug, Clone)]
59#[cfg_attr(
60    feature = "python",
61    pyo3::pyclass(module = "nautilus_trader.adapters.architect_ax", from_py_object)
62)]
63#[cfg_attr(
64    feature = "python",
65    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.architect_ax")
66)]
67pub struct AxDataClientFactory;
68
69impl AxDataClientFactory {
70    /// Creates a new [`AxDataClientFactory`] instance.
71    #[must_use]
72    pub const fn new() -> Self {
73        Self
74    }
75}
76
77impl Default for AxDataClientFactory {
78    fn default() -> Self {
79        Self::new()
80    }
81}
82
83impl DataClientFactory for AxDataClientFactory {
84    fn create(
85        &self,
86        name: &str,
87        config: &dyn ClientConfig,
88        _cache: CacheView,
89        _clock: Rc<RefCell<dyn Clock>>,
90    ) -> anyhow::Result<Box<dyn DataClient>> {
91        let ax_config = config
92            .as_any()
93            .downcast_ref::<AxDataClientConfig>()
94            .ok_or_else(|| {
95                anyhow::anyhow!(
96                    "Invalid config type for AxDataClientFactory. Expected AxDataClientConfig, was {config:?}",
97                )
98            })?
99            .clone();
100
101        let client_id = ClientId::from(name);
102
103        let http_client = if ax_config.has_api_credentials() {
104            let credential = Credential::resolve(
105                ax_config.api_key.clone().map(|value| value.into_inner()),
106                ax_config.api_secret.clone().map(|value| value.into_inner()),
107            )
108            .ok_or_else(|| anyhow::anyhow!("API credentials not configured"))?;
109
110            AxHttpClient::with_credentials(
111                credential.api_key().to_string(),
112                credential.api_secret().to_string(),
113                Some(ax_config.http_base_url()),
114                None, // orders_base_url
115                ax_config.http_timeout_secs,
116                ax_config.max_retries,
117                ax_config.retry_delay_initial_ms,
118                ax_config.retry_delay_max_ms,
119                ax_config
120                    .proxy_url
121                    .as_ref()
122                    .map(|url| url.expose_secret().to_owned()),
123            )
124            .map_err(|e| anyhow::anyhow!("Failed to create HTTP client: {e}"))?
125        } else {
126            AxHttpClient::new(
127                Some(ax_config.http_base_url()),
128                None, // orders_base_url
129                ax_config.http_timeout_secs,
130                ax_config.max_retries,
131                ax_config.retry_delay_initial_ms,
132                ax_config.retry_delay_max_ms,
133                ax_config
134                    .proxy_url
135                    .as_ref()
136                    .map(|url| url.expose_secret().to_owned()),
137            )
138            .map_err(|e| anyhow::anyhow!("Failed to create HTTP client: {e}"))?
139        };
140
141        let ws_url = ax_config.ws_public_url();
142
143        // Token set during connect
144        let ws_client = AxMdWebSocketClient::without_auth(
145            ws_url,
146            ax_config.heartbeat_interval_secs,
147            ax_config.transport_backend,
148            ax_config
149                .proxy_url
150                .as_ref()
151                .map(|url| url.expose_secret().to_owned()),
152        );
153
154        let client = AxDataClient::new(client_id, ax_config, http_client, ws_client)?;
155        Ok(Box::new(client))
156    }
157
158    fn name(&self) -> &'static str {
159        AX
160    }
161
162    fn config_type(&self) -> &'static str {
163        "AxDataClientConfig"
164    }
165}
166
167/// Factory for creating AX Exchange execution clients.
168#[derive(Debug, Clone)]
169#[cfg_attr(
170    feature = "python",
171    pyo3::pyclass(module = "nautilus_trader.adapters.architect_ax", from_py_object)
172)]
173#[cfg_attr(
174    feature = "python",
175    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.architect_ax")
176)]
177pub struct AxExecutionClientFactory;
178
179impl AxExecutionClientFactory {
180    /// Creates a new [`AxExecutionClientFactory`] instance.
181    #[must_use]
182    pub const fn new() -> Self {
183        Self
184    }
185}
186
187impl Default for AxExecutionClientFactory {
188    fn default() -> Self {
189        Self::new()
190    }
191}
192
193impl ExecutionClientFactory for AxExecutionClientFactory {
194    fn create(
195        &self,
196        trader_id: TraderId,
197        name: &str,
198        config: &dyn ClientConfig,
199        cache: CacheView,
200        _clock: Rc<RefCell<dyn Clock>>,
201    ) -> anyhow::Result<Box<dyn ExecutionClient>> {
202        let ax_config = config
203            .as_any()
204            .downcast_ref::<AxExecutionClientConfig>()
205            .ok_or_else(|| {
206                anyhow::anyhow!(
207                    "Invalid config type for AxExecutionClientFactory. Expected AxExecutionClientConfig, was {config:?}",
208                )
209            })?
210            .clone();
211
212        // AX uses netting for perpetual futures
213        let oms_type = OmsType::Netting;
214        let account_type = AccountType::Margin;
215
216        let core = ExecutionClientCore::new(
217            trader_id,
218            ClientId::from(name),
219            *AX_VENUE,
220            oms_type,
221            ax_config.account_id,
222            account_type,
223            None, // base_currency
224            cache,
225        );
226
227        let mut client = AxExecutionClient::new(core, ax_config)?;
228
229        if let Some(sender) = try_get_exec_event_sender() {
230            client.set_event_sender(sender);
231        }
232
233        Ok(Box::new(client))
234    }
235
236    fn name(&self) -> &'static str {
237        AX
238    }
239
240    fn config_type(&self) -> &'static str {
241        "AxExecutionClientConfig"
242    }
243}
244
245#[cfg(test)]
246mod tests {
247    use nautilus_common::{
248        cache::Cache, clock::VirtualClock, factories::ClientConfig,
249        live::runner::replace_exec_event_sender, messages::ExecutionEvent,
250    };
251    use nautilus_core::UnixNanos;
252    use nautilus_model::{
253        identifiers::AccountId,
254        types::{AccountBalance, Money},
255    };
256    use rstest::rstest;
257
258    use super::*;
259    use crate::config::AxDataClientConfig;
260
261    #[rstest]
262    fn test_ax_data_client_config_implements_client_config() {
263        let config = AxDataClientConfig::default();
264
265        let boxed_config: Box<dyn ClientConfig> = Box::new(config);
266        let downcasted = boxed_config.as_any().downcast_ref::<AxDataClientConfig>();
267
268        assert!(downcasted.is_some());
269    }
270
271    #[rstest]
272    fn test_ax_data_client_factory_creation() {
273        let factory = AxDataClientFactory::new();
274        assert_eq!(factory.name(), AX);
275        assert_eq!(factory.config_type(), "AxDataClientConfig");
276    }
277
278    #[rstest]
279    fn test_ax_data_client_factory_default() {
280        let factory = AxDataClientFactory;
281        assert_eq!(factory.name(), AX);
282    }
283
284    #[rstest]
285    #[case(false)]
286    #[case(true)]
287    fn test_ax_execution_client_factory_sender(#[case] sender_bound: bool) {
288        std::thread::spawn(move || {
289            let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
290            if sender_bound {
291                replace_exec_event_sender(sender.clone());
292            }
293
294            let account_id = AccountId::from("AX-FACTORY-002");
295            let config = AxExecutionClientConfig {
296                account_id,
297                api_key: Some("test_api_key".into()),
298                api_secret: Some("test_api_secret".into()),
299                ..Default::default()
300            };
301            let cache = Rc::new(RefCell::new(Cache::default()));
302            let clock = Rc::new(RefCell::new(VirtualClock::new()));
303            let mut client = AxExecutionClientFactory::new()
304                .create(
305                    TraderId::from("TESTER-002"),
306                    "AX",
307                    &config,
308                    cache.into(),
309                    clock,
310                )
311                .unwrap();
312            let balances = vec![AccountBalance::new(
313                Money::from("1234.50 USD"),
314                Money::from("234.25 USD"),
315                Money::from("1000.25 USD"),
316            )];
317            let ts_event = UnixNanos::from(123_456);
318
319            if !sender_bound {
320                assert!(try_get_exec_event_sender().is_none());
321                replace_exec_event_sender(sender);
322                client.start().unwrap();
323            }
324
325            client
326                .generate_account_state(balances.clone(), vec![], true, ts_event, None)
327                .unwrap();
328
329            let ExecutionEvent::Account(state) = receiver.try_recv().unwrap() else {
330                panic!("Expected account state");
331            };
332            assert_eq!(state.account_id, account_id);
333            assert_eq!(state.account_type, AccountType::Margin);
334            assert_eq!(state.base_currency, None);
335            assert_eq!(state.balances, balances);
336            assert_eq!(state.margins, vec![]);
337            assert!(state.is_reported);
338            assert_eq!(state.ts_event, ts_event);
339            assert_eq!(state.info, None);
340            assert_eq!(
341                receiver.try_recv().unwrap_err(),
342                tokio::sync::mpsc::error::TryRecvError::Empty
343            );
344        })
345        .join()
346        .unwrap();
347    }
348}