1use std::{
19 collections::HashMap,
20 fmt::Debug,
21 num::NonZeroU32,
22 sync::{
23 Arc, LazyLock, RwLock,
24 atomic::{AtomicBool, Ordering},
25 },
26};
27
28use chrono::{DateTime, Utc};
29use nautilus_core::{
30 AtomicMap, AtomicTime, UUID4, consts::NAUTILUS_USER_AGENT, nanos::UnixNanos,
31 time::get_atomic_clock_realtime,
32};
33use nautilus_model::{
34 data::{Bar, BookOrder, FundingRateUpdate, TradeTick},
35 enums::{BookType, OrderSide, OrderType, TimeInForce},
36 events::AccountState,
37 identifiers::{AccountId, ClientOrderId, InstrumentId, VenueOrderId},
38 instruments::{Instrument, any::InstrumentAny},
39 orderbook::OrderBook,
40 reports::{FillReport, OrderStatusReport, PositionStatusReport},
41 types::{Price, Quantity},
42};
43use nautilus_network::{
44 http::HttpClient,
45 ratelimiter::quota::Quota,
46 retry::{RetryConfig, RetryManager},
47};
48use reqwest::{Method, header::USER_AGENT};
49use rust_decimal::Decimal;
50use serde::{Serialize, de::DeserializeOwned};
51use tokio_util::sync::CancellationToken;
52use ustr::Ustr;
53
54use super::{
55 error::AxHttpError,
56 models::{
57 AuthenticateApiKeyRequest, AxAuthenticateResponse, AxBalancesResponse, AxBookResponse,
58 AxCancelAllOrdersResponse, AxCancelOrderResponse, AxCandle, AxCandleResponse,
59 AxCandlesResponse, AxFillsResponse, AxFundingRatesResponse,
60 AxInitialMarginRequirementResponse, AxInstrument, AxInstrumentsResponse,
61 AxOpenOrdersResponse, AxOrderStatusQueryResponse, AxOrdersResponse, AxPlaceOrderResponse,
62 AxPositionsResponse, AxPreviewAggressiveLimitOrderResponse, AxReplaceOrderResponse,
63 AxRiskSnapshotResponse, AxTicker, AxTickersResponse, AxTradesResponse,
64 AxTransactionsResponse, AxWhoAmI, CancelAllOrdersRequest, CancelOrderRequest,
65 PlaceOrderRequest, PreviewAggressiveLimitOrderRequest, ReplaceOrderRequest,
66 },
67 parse::{
68 parse_account_state, parse_bar, parse_fill_report, parse_funding_rate,
69 parse_order_status_report, parse_perp_instrument, parse_position_status_report,
70 parse_trade_tick,
71 },
72 query::{
73 GetBookParams, GetCandleParams, GetCandlesParams, GetFillsParams, GetFundingRatesParams,
74 GetInstrumentParams, GetOrderStatusParams, GetOrdersParams, GetTickerParams,
75 GetTradesParams, GetTransactionsParams,
76 },
77};
78use crate::common::{
79 consts::{AX_FILLS_MAX_LOOKBACK_DAYS, AX_HTTP_URL, AX_ORDERS_URL},
80 credential::Credential,
81 enums::{AxCandleWidth, AxInstrumentState},
82 parse::{cid_to_client_order_id, client_order_id_to_cid},
83};
84
85pub static AX_REST_QUOTA: LazyLock<Quota> = LazyLock::new(|| {
89 Quota::per_second(NonZeroU32::new(10).expect("non-zero")).expect("valid constant")
90});
91
92const AX_GLOBAL_RATE_KEY: &str = "architect:global";
93
94pub struct AxRawHttpClient {
99 base_url: String,
100 orders_base_url: String,
101 client: HttpClient,
102 credential: Option<Credential>,
103 session_token: RwLock<Option<String>>,
104 retry_manager: RetryManager<AxHttpError>,
105 cancellation_token: RwLock<CancellationToken>,
106}
107
108impl Default for AxRawHttpClient {
109 fn default() -> Self {
110 Self::new(None, None, 60, 3, 1000, 10_000, None)
111 .expect("Failed to create default AxRawHttpClient")
112 }
113}
114
115impl Debug for AxRawHttpClient {
116 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117 let has_session_token = self.session_token.read().is_ok_and(|guard| guard.is_some());
118 f.debug_struct(stringify!(AxRawHttpClient))
119 .field("base_url", &self.base_url)
120 .field("orders_base_url", &self.orders_base_url)
121 .field("has_credentials", &self.credential.is_some())
122 .field("has_session_token", &has_session_token)
123 .finish()
124 }
125}
126
127impl AxRawHttpClient {
128 #[must_use]
130 pub fn base_url(&self) -> &str {
131 &self.base_url
132 }
133
134 #[must_use]
136 pub fn api_key_masked(&self) -> String {
137 self.credential
138 .as_ref()
139 .map_or_else(|| "None".to_string(), |c| c.masked_api_key())
140 }
141
142 pub fn cancel_all_requests(&self) {
148 self.cancellation_token
149 .read()
150 .expect("Lock poisoned")
151 .cancel();
152 }
153
154 pub fn reset_cancellation_token(&self) {
160 *self.cancellation_token.write().expect("Lock poisoned") = CancellationToken::new();
161 }
162
163 pub fn cancellation_token(&self) -> CancellationToken {
169 self.cancellation_token
170 .read()
171 .expect("Lock poisoned")
172 .clone()
173 }
174
175 pub fn new(
181 base_url: Option<String>,
182 orders_base_url: Option<String>,
183 timeout_secs: u64,
184 max_retries: u32,
185 retry_delay_ms: u64,
186 retry_delay_max_ms: u64,
187 proxy_url: Option<String>,
188 ) -> Result<Self, AxHttpError> {
189 let retry_config = RetryConfig {
190 max_retries,
191 initial_delay_ms: retry_delay_ms,
192 max_delay_ms: retry_delay_max_ms,
193 backoff_factor: 2.0,
194 jitter_ms: 1000,
195 operation_timeout_ms: Some(60_000),
196 immediate_first: false,
197 max_elapsed_ms: Some(180_000),
198 };
199
200 let retry_manager = RetryManager::new(retry_config);
201
202 Ok(Self {
203 base_url: base_url.unwrap_or_else(|| AX_HTTP_URL.to_string()),
204 orders_base_url: orders_base_url.unwrap_or_else(|| AX_ORDERS_URL.to_string()),
205 client: HttpClient::new(
206 Self::default_headers(),
207 vec![],
208 Self::rate_limiter_quotas(),
209 Some(*AX_REST_QUOTA),
210 Some(timeout_secs),
211 proxy_url,
212 )
213 .map_err(|e| AxHttpError::NetworkError(format!("Failed to create HTTP client: {e}")))?,
214 credential: None,
215 session_token: RwLock::new(None),
216 retry_manager,
217 cancellation_token: RwLock::new(CancellationToken::new()),
218 })
219 }
220
221 #[expect(clippy::too_many_arguments)]
227 pub fn with_credentials(
228 api_key: String,
229 api_secret: String,
230 base_url: Option<String>,
231 orders_base_url: Option<String>,
232 timeout_secs: u64,
233 max_retries: u32,
234 retry_delay_ms: u64,
235 retry_delay_max_ms: u64,
236 proxy_url: Option<String>,
237 ) -> Result<Self, AxHttpError> {
238 let retry_config = RetryConfig {
239 max_retries,
240 initial_delay_ms: retry_delay_ms,
241 max_delay_ms: retry_delay_max_ms,
242 backoff_factor: 2.0,
243 jitter_ms: 1000,
244 operation_timeout_ms: Some(60_000),
245 immediate_first: false,
246 max_elapsed_ms: Some(180_000),
247 };
248
249 let retry_manager = RetryManager::new(retry_config);
250
251 Ok(Self {
252 base_url: base_url.unwrap_or_else(|| AX_HTTP_URL.to_string()),
253 orders_base_url: orders_base_url.unwrap_or_else(|| AX_ORDERS_URL.to_string()),
254 client: HttpClient::new(
255 Self::default_headers(),
256 vec![],
257 Self::rate_limiter_quotas(),
258 Some(*AX_REST_QUOTA),
259 Some(timeout_secs),
260 proxy_url,
261 )
262 .map_err(|e| AxHttpError::NetworkError(format!("Failed to create HTTP client: {e}")))?,
263 credential: Some(Credential::new(api_key, api_secret)),
264 session_token: RwLock::new(None),
265 retry_manager,
266 cancellation_token: RwLock::new(CancellationToken::new()),
267 })
268 }
269
270 pub fn set_session_token(&self, token: String) {
278 *self.session_token.write().expect("Lock poisoned") = Some(token);
280 }
281
282 fn default_headers() -> HashMap<String, String> {
283 HashMap::from([
284 (USER_AGENT.to_string(), NAUTILUS_USER_AGENT.to_string()),
285 ("Accept".to_string(), "application/json".to_string()),
286 ])
287 }
288
289 fn rate_limiter_quotas() -> Vec<(String, Quota)> {
290 vec![(AX_GLOBAL_RATE_KEY.to_string(), *AX_REST_QUOTA)]
291 }
292
293 fn rate_limit_keys(endpoint: &str) -> Vec<String> {
294 let normalized = endpoint.split('?').next().unwrap_or(endpoint);
295 let route = format!("architect:{normalized}");
296
297 vec![AX_GLOBAL_RATE_KEY.to_string(), route]
298 }
299
300 fn auth_headers(&self) -> Result<HashMap<String, String>, AxHttpError> {
301 let guard = self.session_token.read().expect("Lock poisoned");
303 let session_token = guard.as_ref().ok_or(AxHttpError::MissingSessionToken)?;
304
305 let mut headers = HashMap::new();
306 headers.insert(
307 "Authorization".to_string(),
308 format!("Bearer {session_token}"),
309 );
310
311 Ok(headers)
312 }
313
314 async fn send_request<T: DeserializeOwned, P: Serialize>(
315 &self,
316 method: Method,
317 endpoint: &str,
318 params: Option<&P>,
319 body: Option<Vec<u8>>,
320 authenticate: bool,
321 ) -> Result<T, AxHttpError> {
322 self.send_request_to_url(&self.base_url, method, endpoint, params, body, authenticate)
323 .await
324 }
325
326 async fn send_request_to_url<T: DeserializeOwned, P: Serialize>(
327 &self,
328 base_url: &str,
329 method: Method,
330 endpoint: &str,
331 params: Option<&P>,
332 body: Option<Vec<u8>>,
333 authenticate: bool,
334 ) -> Result<T, AxHttpError> {
335 let endpoint = endpoint.to_string();
336 let url = format!("{base_url}{endpoint}");
337
338 let params_str = if method == Method::GET || method == Method::DELETE {
339 params
340 .map(serde_urlencoded::to_string)
341 .transpose()
342 .map_err(|e| AxHttpError::JsonError(format!("Failed to serialize params: {e}")))?
343 } else {
344 None
345 };
346
347 let operation = || {
348 let url = url.clone();
349 let method = method.clone();
350 let endpoint = endpoint.clone();
351 let params_str = params_str.clone();
352 let body = body.clone();
353
354 async move {
355 let mut headers = Self::default_headers();
356
357 if authenticate {
358 let auth_headers = self.auth_headers()?;
359 headers.extend(auth_headers);
360 }
361
362 if body.is_some() {
363 headers.insert("Content-Type".to_string(), "application/json".to_string());
364 }
365
366 let full_url = if let Some(ref query) = params_str {
367 if query.is_empty() {
368 url
369 } else {
370 format!("{url}?{query}")
371 }
372 } else {
373 url
374 };
375
376 let rate_limit_keys = Self::rate_limit_keys(&endpoint);
377
378 let response = self
379 .client
380 .request(
381 method,
382 full_url,
383 None,
384 Some(headers),
385 body,
386 None,
387 Some(rate_limit_keys),
388 )
389 .await?;
390
391 let status = response.status;
392 let response_body = String::from_utf8_lossy(&response.body).to_string();
393
394 if !status.is_success() {
395 return Err(AxHttpError::UnexpectedStatus {
396 status: status.as_u16(),
397 body: response_body,
398 });
399 }
400
401 serde_json::from_str(&response_body).map_err(|e| {
402 AxHttpError::JsonError(format!(
403 "Failed to deserialize response: {e}\nBody: {response_body}"
404 ))
405 })
406 }
407 };
408
409 let is_idempotent = matches!(method, Method::GET | Method::HEAD | Method::OPTIONS);
411 let should_retry = |error: &AxHttpError| -> bool { is_idempotent && error.is_retryable() };
412
413 let create_error = |msg: String| -> AxHttpError {
414 if msg == "canceled" {
415 AxHttpError::Canceled("Adapter disconnecting or shutting down".to_string())
416 } else {
417 AxHttpError::NetworkError(msg)
418 }
419 };
420
421 let cancel_token = self
422 .cancellation_token
423 .read()
424 .expect("Lock poisoned")
425 .clone();
426
427 self.retry_manager
428 .execute_with_retry_with_cancel(
429 endpoint.as_str(),
430 operation,
431 should_retry,
432 create_error,
433 &cancel_token,
434 )
435 .await
436 }
437
438 pub async fn get_whoami(&self) -> Result<AxWhoAmI, AxHttpError> {
447 self.send_request::<AxWhoAmI, ()>(Method::GET, "/whoami", None, None, true)
448 .await
449 }
450
451 pub async fn get_instruments(&self) -> Result<AxInstrumentsResponse, AxHttpError> {
460 self.send_request::<AxInstrumentsResponse, ()>(
461 Method::GET,
462 "/instruments",
463 None,
464 None,
465 false,
466 )
467 .await
468 }
469
470 pub async fn get_balances(&self) -> Result<AxBalancesResponse, AxHttpError> {
479 self.send_request::<AxBalancesResponse, ()>(Method::GET, "/balances", None, None, true)
480 .await
481 }
482
483 pub async fn get_positions(&self) -> Result<AxPositionsResponse, AxHttpError> {
492 self.send_request::<AxPositionsResponse, ()>(Method::GET, "/positions", None, None, true)
493 .await
494 }
495
496 pub async fn get_tickers(&self) -> Result<AxTickersResponse, AxHttpError> {
505 self.send_request::<AxTickersResponse, ()>(Method::GET, "/tickers", None, None, true)
506 .await
507 }
508
509 pub async fn get_ticker(&self, symbol: Ustr) -> Result<AxTicker, AxHttpError> {
518 let params = GetTickerParams::new(symbol);
519 self.send_request::<AxTicker, _>(Method::GET, "/ticker", Some(¶ms), None, true)
520 .await
521 }
522
523 pub async fn get_instrument(&self, symbol: Ustr) -> Result<AxInstrument, AxHttpError> {
532 let params = GetInstrumentParams::new(symbol);
533 self.send_request::<AxInstrument, _>(Method::GET, "/instrument", Some(¶ms), None, false)
534 .await
535 }
536
537 pub async fn authenticate(
546 &self,
547 api_key: &str,
548 api_secret: &str,
549 expiration_seconds: i32,
550 ) -> Result<AxAuthenticateResponse, AxHttpError> {
551 let request = AuthenticateApiKeyRequest::new(api_key, api_secret, expiration_seconds);
552
553 let body = serde_json::to_vec(&request)
554 .map_err(|e| AxHttpError::JsonError(format!("Failed to serialize request: {e}")))?;
555
556 self.send_request::<AxAuthenticateResponse, ()>(
557 Method::POST,
558 "/authenticate",
559 None,
560 Some(body),
561 false,
562 )
563 .await
564 }
565
566 pub async fn authenticate_auto(
581 &self,
582 expiration_seconds: i32,
583 ) -> Result<AxAuthenticateResponse, AxHttpError> {
584 let (api_key, api_secret) = self
585 .resolve_credentials()
586 .ok_or(AxHttpError::MissingCredentials)?;
587
588 self.authenticate(&api_key, &api_secret, expiration_seconds)
589 .await
590 }
591
592 fn resolve_credentials(&self) -> Option<(String, String)> {
593 if let Some(cred) = &self.credential {
594 return Some((cred.api_key().to_string(), cred.api_secret().to_string()));
595 }
596
597 let cred = Credential::resolve(None, None)?;
598 Some((cred.api_key().to_string(), cred.api_secret().to_string()))
599 }
600
601 pub async fn place_order(
610 &self,
611 request: &PlaceOrderRequest,
612 ) -> Result<AxPlaceOrderResponse, AxHttpError> {
613 let body = serde_json::to_vec(request)
614 .map_err(|e| AxHttpError::JsonError(format!("Failed to serialize request: {e}")))?;
615 self.send_request_to_url::<AxPlaceOrderResponse, ()>(
616 &self.orders_base_url,
617 Method::POST,
618 "/place_order",
619 None,
620 Some(body),
621 true,
622 )
623 .await
624 }
625
626 pub async fn cancel_order(&self, order_id: &str) -> Result<AxCancelOrderResponse, AxHttpError> {
635 let request = CancelOrderRequest::new(order_id);
636 let body = serde_json::to_vec(&request)
637 .map_err(|e| AxHttpError::JsonError(format!("Failed to serialize request: {e}")))?;
638 self.send_request_to_url::<AxCancelOrderResponse, ()>(
639 &self.orders_base_url,
640 Method::POST,
641 "/cancel_order",
642 None,
643 Some(body),
644 true,
645 )
646 .await
647 }
648
649 pub async fn replace_order(
661 &self,
662 request: &ReplaceOrderRequest,
663 ) -> Result<AxReplaceOrderResponse, AxHttpError> {
664 let body = serde_json::to_vec(request)
665 .map_err(|e| AxHttpError::JsonError(format!("Failed to serialize request: {e}")))?;
666 self.send_request_to_url::<AxReplaceOrderResponse, ()>(
667 &self.orders_base_url,
668 Method::POST,
669 "/replace_order",
670 None,
671 Some(body),
672 true,
673 )
674 .await
675 }
676
677 pub async fn cancel_all_orders(
686 &self,
687 request: &CancelAllOrdersRequest,
688 ) -> Result<AxCancelAllOrdersResponse, AxHttpError> {
689 let body = serde_json::to_vec(request)
690 .map_err(|e| AxHttpError::JsonError(format!("Failed to serialize request: {e}")))?;
691 self.send_request_to_url::<AxCancelAllOrdersResponse, ()>(
692 &self.orders_base_url,
693 Method::POST,
694 "/cancel_all_orders",
695 None,
696 Some(body),
697 true,
698 )
699 .await
700 }
701
702 pub async fn get_open_orders(&self) -> Result<AxOpenOrdersResponse, AxHttpError> {
711 self.send_request_to_url::<AxOpenOrdersResponse, ()>(
712 &self.orders_base_url,
713 Method::GET,
714 "/open_orders",
715 None,
716 None,
717 true,
718 )
719 .await
720 }
721
722 pub async fn get_fills(
731 &self,
732 start_timestamp_ns: i64,
733 end_timestamp_ns: i64,
734 ) -> Result<AxFillsResponse, AxHttpError> {
735 let params = GetFillsParams::new(start_timestamp_ns, end_timestamp_ns);
736 self.send_request::<AxFillsResponse, _>(Method::GET, "/fills", Some(¶ms), None, true)
737 .await
738 }
739
740 pub async fn get_candles(
749 &self,
750 symbol: Ustr,
751 start_timestamp_ns: i64,
752 end_timestamp_ns: i64,
753 candle_width: AxCandleWidth,
754 ) -> Result<AxCandlesResponse, AxHttpError> {
755 let params =
756 GetCandlesParams::new(symbol, start_timestamp_ns, end_timestamp_ns, candle_width);
757 self.send_request::<AxCandlesResponse, _>(
758 Method::GET,
759 "/candles",
760 Some(¶ms),
761 None,
762 true,
763 )
764 .await
765 }
766
767 pub async fn get_current_candle(
776 &self,
777 symbol: Ustr,
778 candle_width: AxCandleWidth,
779 ) -> Result<AxCandle, AxHttpError> {
780 let params = GetCandleParams::new(symbol, candle_width);
781 let response = self
782 .send_request::<AxCandleResponse, _>(
783 Method::GET,
784 "/candles/current",
785 Some(¶ms),
786 None,
787 true,
788 )
789 .await?;
790 Ok(response.candle)
791 }
792
793 pub async fn get_last_candle(
802 &self,
803 symbol: Ustr,
804 candle_width: AxCandleWidth,
805 ) -> Result<AxCandle, AxHttpError> {
806 let params = GetCandleParams::new(symbol, candle_width);
807 let response = self
808 .send_request::<AxCandleResponse, _>(
809 Method::GET,
810 "/candles/last",
811 Some(¶ms),
812 None,
813 true,
814 )
815 .await?;
816 Ok(response.candle)
817 }
818
819 pub async fn get_funding_rates(
828 &self,
829 symbol: Ustr,
830 start_timestamp_ns: i64,
831 end_timestamp_ns: i64,
832 ) -> Result<AxFundingRatesResponse, AxHttpError> {
833 let params = GetFundingRatesParams::new(symbol, start_timestamp_ns, end_timestamp_ns);
834 self.send_request::<AxFundingRatesResponse, _>(
835 Method::GET,
836 "/funding-rates",
837 Some(¶ms),
838 None,
839 true,
840 )
841 .await
842 }
843
844 pub async fn get_risk_snapshot(&self) -> Result<AxRiskSnapshotResponse, AxHttpError> {
853 self.send_request::<AxRiskSnapshotResponse, ()>(
854 Method::GET,
855 "/risk-snapshot",
856 None,
857 None,
858 true,
859 )
860 .await
861 }
862
863 pub async fn preview_aggressive_limit_order(
876 &self,
877 request: &PreviewAggressiveLimitOrderRequest,
878 ) -> Result<AxPreviewAggressiveLimitOrderResponse, AxHttpError> {
879 let body = serde_json::to_vec(request)
880 .map_err(|e| AxHttpError::JsonError(format!("Failed to serialize request: {e}")))?;
881 self.send_request::<AxPreviewAggressiveLimitOrderResponse, ()>(
882 Method::POST,
883 "/preview-aggressive-limit-order",
884 None,
885 Some(body),
886 true,
887 )
888 .await
889 }
890
891 pub async fn get_transactions(
900 &self,
901 transaction_types: Vec<String>,
902 ) -> Result<AxTransactionsResponse, AxHttpError> {
903 let params = GetTransactionsParams::new(transaction_types);
904 self.send_request::<AxTransactionsResponse, _>(
905 Method::GET,
906 "/transactions",
907 Some(¶ms),
908 None,
909 true,
910 )
911 .await
912 }
913
914 pub async fn get_trades(
923 &self,
924 symbol: Ustr,
925 limit: Option<i32>,
926 ) -> Result<AxTradesResponse, AxHttpError> {
927 let params = GetTradesParams::new(symbol, limit);
928 self.send_request::<AxTradesResponse, _>(Method::GET, "/trades", Some(¶ms), None, true)
929 .await
930 }
931
932 pub async fn get_book(
941 &self,
942 symbol: Ustr,
943 level: Option<i32>,
944 ) -> Result<AxBookResponse, AxHttpError> {
945 let params = GetBookParams::new(symbol, level);
946 self.send_request::<AxBookResponse, _>(Method::GET, "/book", Some(¶ms), None, true)
948 .await
949 }
950
951 pub async fn get_order_status_by_id(
960 &self,
961 order_id: &str,
962 ) -> Result<AxOrderStatusQueryResponse, AxHttpError> {
963 let params = GetOrderStatusParams::by_order_id(order_id);
964 self.send_request_to_url::<AxOrderStatusQueryResponse, _>(
965 &self.orders_base_url,
966 Method::GET,
967 "/order-status",
968 Some(¶ms),
969 None,
970 true,
971 )
972 .await
973 }
974
975 pub async fn get_order_status_by_cid(
984 &self,
985 client_order_id: u64,
986 ) -> Result<AxOrderStatusQueryResponse, AxHttpError> {
987 let params = GetOrderStatusParams::by_client_order_id(client_order_id);
988 self.send_request_to_url::<AxOrderStatusQueryResponse, _>(
989 &self.orders_base_url,
990 Method::GET,
991 "/order-status",
992 Some(¶ms),
993 None,
994 true,
995 )
996 .await
997 }
998
999 pub async fn get_orders(
1008 &self,
1009 params: &GetOrdersParams,
1010 ) -> Result<AxOrdersResponse, AxHttpError> {
1011 self.send_request_to_url::<AxOrdersResponse, _>(
1012 &self.orders_base_url,
1013 Method::GET,
1014 "/orders",
1015 Some(params),
1016 None,
1017 true,
1018 )
1019 .await
1020 }
1021
1022 pub async fn check_initial_margin(
1031 &self,
1032 request: &PlaceOrderRequest,
1033 ) -> Result<AxInitialMarginRequirementResponse, AxHttpError> {
1034 let body = serde_json::to_vec(request)
1035 .map_err(|e| AxHttpError::JsonError(format!("Failed to serialize request: {e}")))?;
1036 self.send_request_to_url::<AxInitialMarginRequirementResponse, ()>(
1037 &self.orders_base_url,
1038 Method::POST,
1039 "/initial-margin-requirement",
1040 None,
1041 Some(body),
1042 true,
1043 )
1044 .await
1045 }
1046}
1047
1048#[derive(Debug)]
1053#[cfg_attr(
1054 feature = "python",
1055 pyo3::pyclass(
1056 module = "nautilus_trader.core.nautilus_pyo3.architect_ax",
1057 from_py_object
1058 )
1059)]
1060#[cfg_attr(
1061 feature = "python",
1062 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.architect_ax")
1063)]
1064pub struct AxHttpClient {
1065 pub(crate) inner: Arc<AxRawHttpClient>,
1066 pub(crate) instruments_cache: Arc<AtomicMap<Ustr, InstrumentAny>>,
1067 clock: &'static AtomicTime,
1068 cache_initialized: Arc<AtomicBool>,
1069}
1070
1071impl Clone for AxHttpClient {
1072 fn clone(&self) -> Self {
1073 Self {
1074 inner: self.inner.clone(),
1075 instruments_cache: self.instruments_cache.clone(),
1076 cache_initialized: self.cache_initialized.clone(),
1077 clock: self.clock,
1078 }
1079 }
1080}
1081
1082impl Default for AxHttpClient {
1083 fn default() -> Self {
1084 Self::new(None, None, 60, 3, 1000, 10_000, None)
1085 .expect("Failed to create default AxHttpClient")
1086 }
1087}
1088
1089impl AxHttpClient {
1090 pub fn new(
1096 base_url: Option<String>,
1097 orders_base_url: Option<String>,
1098 timeout_secs: u64,
1099 max_retries: u32,
1100 retry_delay_ms: u64,
1101 retry_delay_max_ms: u64,
1102 proxy_url: Option<String>,
1103 ) -> Result<Self, AxHttpError> {
1104 Ok(Self {
1105 inner: Arc::new(AxRawHttpClient::new(
1106 base_url,
1107 orders_base_url,
1108 timeout_secs,
1109 max_retries,
1110 retry_delay_ms,
1111 retry_delay_max_ms,
1112 proxy_url,
1113 )?),
1114 instruments_cache: Arc::new(AtomicMap::new()),
1115 cache_initialized: Arc::new(AtomicBool::new(false)),
1116 clock: get_atomic_clock_realtime(),
1117 })
1118 }
1119
1120 #[expect(clippy::too_many_arguments)]
1126 pub fn with_credentials(
1127 api_key: String,
1128 api_secret: String,
1129 base_url: Option<String>,
1130 orders_base_url: Option<String>,
1131 timeout_secs: u64,
1132 max_retries: u32,
1133 retry_delay_ms: u64,
1134 retry_delay_max_ms: u64,
1135 proxy_url: Option<String>,
1136 ) -> Result<Self, AxHttpError> {
1137 Ok(Self {
1138 inner: Arc::new(AxRawHttpClient::with_credentials(
1139 api_key,
1140 api_secret,
1141 base_url,
1142 orders_base_url,
1143 timeout_secs,
1144 max_retries,
1145 retry_delay_ms,
1146 retry_delay_max_ms,
1147 proxy_url,
1148 )?),
1149 instruments_cache: Arc::new(AtomicMap::new()),
1150 cache_initialized: Arc::new(AtomicBool::new(false)),
1151 clock: get_atomic_clock_realtime(),
1152 })
1153 }
1154
1155 #[must_use]
1157 pub fn base_url(&self) -> &str {
1158 self.inner.base_url()
1159 }
1160
1161 #[must_use]
1163 pub fn api_key_masked(&self) -> String {
1164 self.inner.api_key_masked()
1165 }
1166
1167 pub fn cancel_all_requests(&self) {
1169 self.inner.cancel_all_requests();
1170 }
1171
1172 pub fn reset_cancellation_token(&self) {
1174 self.inner.reset_cancellation_token();
1175 }
1176
1177 pub fn set_session_token(&self, token: String) {
1181 self.inner.set_session_token(token);
1182 }
1183
1184 fn generate_ts_init(&self) -> UnixNanos {
1186 self.clock.get_time_ns()
1187 }
1188
1189 #[must_use]
1193 pub fn is_initialized(&self) -> bool {
1194 self.cache_initialized.load(Ordering::Acquire)
1195 }
1196
1197 #[must_use]
1199 pub fn get_cached_symbols(&self) -> Vec<String> {
1200 self.instruments_cache
1201 .load()
1202 .keys()
1203 .map(|k| k.to_string())
1204 .collect()
1205 }
1206
1207 pub fn cache_instruments(&self, instruments: &[InstrumentAny]) {
1211 self.instruments_cache.rcu(|m| {
1212 for inst in instruments {
1213 m.insert(inst.raw_symbol().inner(), inst.clone());
1214 }
1215 });
1216 self.cache_initialized.store(true, Ordering::Release);
1217 }
1218
1219 pub fn cache_instrument(&self, instrument: InstrumentAny) {
1223 self.instruments_cache
1224 .insert(instrument.raw_symbol().inner(), instrument);
1225 self.cache_initialized.store(true, Ordering::Release);
1226 }
1227
1228 pub async fn authenticate(
1236 &self,
1237 api_key: &str,
1238 api_secret: &str,
1239 expiration_seconds: i32,
1240 ) -> Result<String, AxHttpError> {
1241 let resp = self
1242 .inner
1243 .authenticate(api_key, api_secret, expiration_seconds)
1244 .await?;
1245 self.inner.set_session_token(resp.token.clone());
1246 Ok(resp.token)
1247 }
1248
1249 pub async fn authenticate_auto(&self, expiration_seconds: i32) -> Result<String, AxHttpError> {
1266 let resp = self.inner.authenticate_auto(expiration_seconds).await?;
1267 self.inner.set_session_token(resp.token.clone());
1268 Ok(resp.token)
1269 }
1270
1271 pub fn get_instrument(&self, symbol: &Ustr) -> Option<InstrumentAny> {
1273 self.instruments_cache.get_cloned(symbol)
1274 }
1275
1276 pub async fn request_instruments(
1282 &self,
1283 maker_fee: Option<Decimal>,
1284 taker_fee: Option<Decimal>,
1285 ) -> anyhow::Result<Vec<InstrumentAny>> {
1286 let resp = self
1287 .inner
1288 .get_instruments()
1289 .await
1290 .map_err(|e| anyhow::anyhow!(e))?;
1291
1292 let maker_fee = maker_fee.unwrap_or(Decimal::ZERO);
1293 let taker_fee = taker_fee.unwrap_or(Decimal::ZERO);
1294 let ts_init = self.generate_ts_init();
1295
1296 let mut instruments: Vec<InstrumentAny> = Vec::new();
1297 for inst in &resp.instruments {
1298 if inst.state == AxInstrumentState::Delisted {
1299 log::debug!("Skipping delisted instrument: {}", inst.symbol);
1300 continue;
1301 }
1302
1303 if inst.symbol.as_str().starts_with("TEST") {
1305 log::debug!("Skipping test instrument: {}", inst.symbol);
1306 continue;
1307 }
1308
1309 match parse_perp_instrument(inst, maker_fee, taker_fee, ts_init, ts_init) {
1310 Ok(instrument) => instruments.push(instrument),
1311 Err(e) => {
1312 log::warn!("Failed to parse instrument {}: {e}", inst.symbol);
1313 }
1314 }
1315 }
1316
1317 Ok(instruments)
1318 }
1319
1320 pub async fn request_instrument(
1326 &self,
1327 symbol: Ustr,
1328 maker_fee: Option<Decimal>,
1329 taker_fee: Option<Decimal>,
1330 ) -> anyhow::Result<InstrumentAny> {
1331 let resp = self
1332 .inner
1333 .get_instrument(symbol)
1334 .await
1335 .map_err(|e| anyhow::anyhow!(e))?;
1336
1337 let maker_fee = maker_fee.unwrap_or(Decimal::ZERO);
1338 let taker_fee = taker_fee.unwrap_or(Decimal::ZERO);
1339 let ts_init = self.generate_ts_init();
1340
1341 parse_perp_instrument(&resp, maker_fee, taker_fee, ts_init, ts_init)
1342 }
1343
1344 pub async fn request_book_snapshot(
1354 &self,
1355 symbol: Ustr,
1356 depth: Option<usize>,
1357 ) -> anyhow::Result<OrderBook> {
1358 let instrument = self
1359 .get_instrument(&symbol)
1360 .ok_or_else(|| anyhow::anyhow!("Instrument {symbol} not found in cache"))?;
1361
1362 let resp = self
1363 .inner
1364 .get_book(symbol, Some(2))
1365 .await
1366 .map_err(|e| anyhow::anyhow!(e))?;
1367
1368 let instrument_id = instrument.id();
1369 let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
1370
1371 let price_precision = instrument.price_precision();
1372 let size_precision = instrument.size_precision();
1373 let ts_event = UnixNanos::from(resp.book.ts as u64 * 1_000_000_000 + resp.book.tn as u64);
1374
1375 for (i, level) in resp.book.b.iter().enumerate() {
1376 if depth.is_some_and(|d| i >= d) {
1377 break;
1378 }
1379 let price = Price::from_decimal_dp(level.p, price_precision)
1380 .unwrap_or_else(|_| Price::from(level.p.to_string().as_str()));
1381 let size = Quantity::new(level.q as f64, size_precision);
1382 let order = BookOrder::new(OrderSide::Buy, price, size, i as u64);
1383 book.add(order, 0, i as u64, ts_event);
1384 }
1385
1386 let bids_len = resp.book.b.len();
1387 for (i, level) in resp.book.a.iter().enumerate() {
1388 if depth.is_some_and(|d| i >= d) {
1389 break;
1390 }
1391 let price = Price::from_decimal_dp(level.p, price_precision)
1392 .unwrap_or_else(|_| Price::from(level.p.to_string().as_str()));
1393 let size = Quantity::new(level.q as f64, size_precision);
1394 let order = BookOrder::new(OrderSide::Sell, price, size, (bids_len + i) as u64);
1395 book.add(order, 0, (bids_len + i) as u64, ts_event);
1396 }
1397
1398 Ok(book)
1399 }
1400
1401 pub async fn request_trade_ticks(
1415 &self,
1416 symbol: Ustr,
1417 limit: Option<i32>,
1418 start: Option<UnixNanos>,
1419 end: Option<UnixNanos>,
1420 ) -> anyhow::Result<Vec<TradeTick>> {
1421 let instrument = self
1422 .get_instrument(&symbol)
1423 .ok_or_else(|| anyhow::anyhow!("Instrument {symbol} not found in cache"))?;
1424
1425 let resp = self
1426 .inner
1427 .get_trades(symbol, limit)
1428 .await
1429 .map_err(|e| anyhow::anyhow!(e))?;
1430
1431 let ts_init = self.generate_ts_init();
1432 let mut ticks = Vec::with_capacity(resp.trades.len());
1433
1434 for trade in &resp.trades {
1435 match parse_trade_tick(trade, &instrument, ts_init) {
1436 Ok(tick) => {
1437 if start.is_some_and(|s| tick.ts_event < s) {
1438 continue;
1439 }
1440
1441 if end.is_some_and(|e| tick.ts_event > e) {
1442 continue;
1443 }
1444 ticks.push(tick);
1445 }
1446 Err(e) => {
1447 log::warn!("Failed to parse trade for {symbol}: {e}");
1448 }
1449 }
1450 }
1451
1452 Ok(ticks)
1453 }
1454
1455 pub async fn request_bars(
1466 &self,
1467 symbol: Ustr,
1468 start: Option<DateTime<Utc>>,
1469 end: Option<DateTime<Utc>>,
1470 width: AxCandleWidth,
1471 ) -> anyhow::Result<Vec<Bar>> {
1472 let instrument = self
1473 .get_instrument(&symbol)
1474 .ok_or_else(|| anyhow::anyhow!("Instrument {symbol} not found in cache"))?;
1475
1476 let start_ns = start.and_then(|dt| dt.timestamp_nanos_opt()).unwrap_or(0);
1477 let end_ns = end
1478 .and_then(|dt| dt.timestamp_nanos_opt())
1479 .unwrap_or_else(|| self.generate_ts_init().as_i64());
1480 let resp = self
1481 .inner
1482 .get_candles(symbol, start_ns, end_ns, width)
1483 .await
1484 .map_err(|e| anyhow::anyhow!(e))?;
1485
1486 let ts_init = self.generate_ts_init();
1487 let mut bars = Vec::with_capacity(resp.candles.len());
1488
1489 for candle in &resp.candles {
1490 match parse_bar(candle, &instrument, ts_init) {
1491 Ok(bar) => bars.push(bar),
1492 Err(e) => {
1493 log::warn!("Failed to parse bar for {symbol}: {e}");
1494 }
1495 }
1496 }
1497
1498 Ok(bars)
1499 }
1500
1501 pub async fn request_funding_rates(
1507 &self,
1508 instrument_id: InstrumentId,
1509 start: Option<DateTime<Utc>>,
1510 end: Option<DateTime<Utc>>,
1511 ) -> Result<Vec<FundingRateUpdate>, AxHttpError> {
1512 let symbol = instrument_id.symbol.inner();
1513 let start_ns = start.and_then(|dt| dt.timestamp_nanos_opt()).unwrap_or(0);
1514 let end_ns = end
1515 .and_then(|dt| dt.timestamp_nanos_opt())
1516 .unwrap_or_else(|| self.generate_ts_init().as_i64());
1517 let response = self
1518 .inner
1519 .get_funding_rates(symbol, start_ns, end_ns)
1520 .await?;
1521
1522 let ts_init = self.generate_ts_init();
1523 let funding_rates = response
1524 .funding_rates
1525 .iter()
1526 .map(|r| parse_funding_rate(r, instrument_id, ts_init))
1527 .collect::<anyhow::Result<Vec<_>>>()
1528 .map_err(|e| AxHttpError::from(e.to_string()))?;
1529
1530 Ok(funding_rates)
1531 }
1532
1533 pub async fn request_account_state(
1539 &self,
1540 account_id: AccountId,
1541 ) -> anyhow::Result<AccountState> {
1542 let response = self
1543 .inner
1544 .get_balances()
1545 .await
1546 .map_err(|e| anyhow::anyhow!(e))?;
1547
1548 let ts_init = self.generate_ts_init();
1549 parse_account_state(&response, account_id, ts_init, ts_init)
1550 }
1551
1552 pub async fn check_initial_margin(
1558 &self,
1559 request: &PlaceOrderRequest,
1560 ) -> anyhow::Result<Decimal> {
1561 let resp = self
1562 .inner
1563 .check_initial_margin(request)
1564 .await
1565 .map_err(|e| anyhow::anyhow!(e))?;
1566 Ok(resp.im)
1567 }
1568
1569 #[expect(clippy::too_many_arguments)]
1581 pub async fn request_order_status(
1582 &self,
1583 account_id: AccountId,
1584 instrument_id: InstrumentId,
1585 client_order_id: Option<ClientOrderId>,
1586 venue_order_id: Option<VenueOrderId>,
1587 order_side: OrderSide,
1588 order_type: OrderType,
1589 time_in_force: TimeInForce,
1590 ) -> anyhow::Result<OrderStatusReport> {
1591 let resp = if let Some(ref voi) = venue_order_id {
1592 self.inner.get_order_status_by_id(voi.as_str()).await
1593 } else if let Some(ref coid) = client_order_id {
1594 let cid = client_order_id_to_cid(coid);
1595 self.inner.get_order_status_by_cid(cid).await
1596 } else {
1597 anyhow::bail!("Either venue_order_id or client_order_id must be provided")
1598 }
1599 .map_err(|e| anyhow::anyhow!(e))?;
1600
1601 let detail = resp.status;
1602 let size_precision = self
1603 .get_instrument(&detail.symbol)
1604 .map_or(0, |i| i.size_precision());
1605
1606 let voi = VenueOrderId::new(&detail.order_id);
1607 let order_status = detail.state.into();
1608 let filled = detail.filled_quantity.unwrap_or(0);
1609 let remaining = detail.remaining_quantity.unwrap_or(0);
1610 let quantity = Quantity::new((filled + remaining) as f64, size_precision);
1611 let filled_qty = Quantity::new(filled as f64, size_precision);
1612 let ts_init = self.generate_ts_init();
1613
1614 let resolved_coid = client_order_id.or_else(|| detail.clord_id.map(cid_to_client_order_id));
1615
1616 Ok(OrderStatusReport::new(
1617 account_id,
1618 instrument_id,
1619 resolved_coid,
1620 voi,
1621 order_side,
1622 order_type,
1623 time_in_force,
1624 order_status,
1625 quantity,
1626 filled_qty,
1627 ts_init,
1628 ts_init,
1629 ts_init,
1630 Some(UUID4::new()),
1631 ))
1632 }
1633
1634 pub async fn request_order_status_reports<F>(
1648 &self,
1649 account_id: AccountId,
1650 cid_resolver: Option<F>,
1651 ) -> anyhow::Result<Vec<OrderStatusReport>>
1652 where
1653 F: Fn(u64) -> Option<ClientOrderId>,
1654 {
1655 let response = self
1656 .inner
1657 .get_open_orders()
1658 .await
1659 .map_err(|e| anyhow::anyhow!(e))?;
1660
1661 let ts_init = self.generate_ts_init();
1662 let mut reports = Vec::with_capacity(response.orders.len());
1663
1664 for order in &response.orders {
1665 let instrument = self
1666 .get_instrument(&order.s)
1667 .ok_or_else(|| anyhow::anyhow!("Instrument {} not found in cache", order.s))?;
1668
1669 match parse_order_status_report(
1670 order,
1671 account_id,
1672 &instrument,
1673 ts_init,
1674 cid_resolver.as_ref(),
1675 ) {
1676 Ok(report) => reports.push(report),
1677 Err(e) => {
1678 log::warn!("Failed to parse order {}: {e}", order.oid);
1679 }
1680 }
1681 }
1682
1683 Ok(reports)
1684 }
1685
1686 pub async fn request_fill_reports(
1697 &self,
1698 account_id: AccountId,
1699 start: Option<UnixNanos>,
1700 end: Option<UnixNanos>,
1701 ) -> anyhow::Result<Vec<FillReport>> {
1702 let max_span_ns = AX_FILLS_MAX_LOOKBACK_DAYS * 24 * 60 * 60 * 1_000_000_000;
1704 let end_ns = end.map_or_else(|| self.generate_ts_init().as_i64(), |e| e.as_i64());
1705 let floor_ns = end_ns - max_span_ns;
1706 let start_ns = start.map_or(floor_ns, |s| s.as_i64().max(floor_ns));
1707
1708 let response = self
1709 .inner
1710 .get_fills(start_ns, end_ns)
1711 .await
1712 .map_err(|e| anyhow::anyhow!(e))?;
1713
1714 let ts_init = self.generate_ts_init();
1715 let mut reports = Vec::with_capacity(response.fills.len());
1716
1717 for fill in &response.fills {
1718 let instrument = self
1719 .get_instrument(&fill.symbol)
1720 .ok_or_else(|| anyhow::anyhow!("Instrument {} not found in cache", fill.symbol))?;
1721
1722 match parse_fill_report(fill, account_id, &instrument, ts_init) {
1723 Ok(report) => reports.push(report),
1724 Err(e) => {
1725 log::warn!("Failed to parse fill {}: {e}", fill.trade_id);
1726 }
1727 }
1728 }
1729
1730 Ok(reports)
1731 }
1732
1733 pub async fn request_position_reports(
1744 &self,
1745 account_id: AccountId,
1746 ) -> anyhow::Result<Vec<PositionStatusReport>> {
1747 let response = self
1748 .inner
1749 .get_positions()
1750 .await
1751 .map_err(|e| anyhow::anyhow!(e))?;
1752
1753 let ts_init = self.generate_ts_init();
1754 let mut reports = Vec::with_capacity(response.positions.len());
1755
1756 for position in &response.positions {
1757 if position.signed_quantity == 0 {
1759 continue;
1760 }
1761
1762 let instrument = self.get_instrument(&position.symbol).ok_or_else(|| {
1763 anyhow::anyhow!("Instrument {} not found in cache", position.symbol)
1764 })?;
1765
1766 match parse_position_status_report(position, account_id, &instrument, ts_init) {
1767 Ok(report) => reports.push(report),
1768 Err(e) => {
1769 log::warn!("Failed to parse position for {}: {e}", position.symbol);
1770 }
1771 }
1772 }
1773
1774 Ok(reports)
1775 }
1776
1777 pub async fn cancel_all_orders(&self, instrument_id: InstrumentId) -> Result<(), AxHttpError> {
1783 let request = CancelAllOrdersRequest::new().with_symbol(instrument_id.symbol.inner());
1784 self.inner.cancel_all_orders(&request).await?;
1785 Ok(())
1786 }
1787}