1use 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#[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 #[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, 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, 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 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#[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 #[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 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, 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}