1use std::{
19 collections::{HashMap, HashSet},
20 fmt::Debug,
21 num::NonZeroU32,
22 sync::{
23 Arc, LazyLock,
24 atomic::{AtomicBool, Ordering},
25 },
26};
27
28use anyhow::Context;
29use arc_swap::ArcSwapOption;
30use jiff::{Timestamp, civil::Date};
31use nautilus_core::{
32 AtomicMap, AtomicTime, UUID4, consts::NAUTILUS_USER_AGENT, nanos::UnixNanos,
33 time::get_atomic_clock_realtime,
34};
35use nautilus_model::{
36 data::{Bar, BookOrder, FundingRateUpdate, TradeTick},
37 enums::{BookType, OrderSide, OrderType, TimeInForce},
38 events::AccountState,
39 identifiers::{AccountId, ClientOrderId, InstrumentId, VenueOrderId},
40 instruments::{Instrument, any::InstrumentAny},
41 orderbook::OrderBook,
42 reports::{FillReport, OrderStatusReport, PositionStatusReport},
43 types::{Price, Quantity},
44};
45use nautilus_network::{
46 http::HttpClient,
47 ratelimiter::quota::Quota,
48 retry::{RetryConfig, RetryError, RetryManager},
49};
50use parking_lot::RwLock;
51use reqwest::{Method, header::USER_AGENT};
52use rust_decimal::Decimal;
53use serde::{Serialize, de::DeserializeOwned};
54use tokio_util::sync::CancellationToken;
55use ustr::Ustr;
56
57use super::{
58 error::AxHttpError,
59 models::{
60 AuthenticateApiKeyRequest, AxAuthenticateResponse, AxBalancesResponse, AxBookResponse,
61 AxCancelAllOrdersResponse, AxCancelOrderResponse, AxCandle, AxCandleResponse,
62 AxCandlesResponse, AxFillsResponse, AxFundingRatesResponse, AxFundingSlotsResponse,
63 AxInitialMarginRequirementResponse, AxInstrument, AxInstrumentsResponse,
64 AxOpenOrdersResponse, AxOrderStatusQueryResponse, AxOrdersResponse, AxPlaceOrderResponse,
65 AxPositionsResponse, AxPreviewAggressiveLimitOrderResponse, AxReplaceOrderResponse,
66 AxRiskSnapshotResponse, AxTicker, AxTickerResponse, AxTickersResponse, AxTradesResponse,
67 AxTransactionsResponse, AxWhoAmI, CancelAllOrdersRequest, CancelOrderRequest,
68 PlaceOrderRequest, PreviewAggressiveLimitOrderRequest, ReplaceOrderRequest,
69 },
70 parse::{
71 parse_account_state, parse_bar, parse_fill_report, parse_funding_rate, parse_instrument,
72 parse_order_detail_status_report, parse_order_status_report, parse_position_status_report,
73 parse_trade_tick,
74 },
75 query::{
76 GetBookParams, GetCandleParams, GetCandlesParams, GetFillsParams, GetFundingRatesParams,
77 GetFundingSlotsParams, GetInstrumentParams, GetOpenOrdersParams, GetOrderStatusParams,
78 GetOrdersParams, GetTickerParams, GetTickersParams, GetTradesParams, GetTransactionsParams,
79 },
80};
81use crate::common::{
82 consts::{AX_FILLS_MAX_LOOKBACK_DAYS, AX_HTTP_URL, AX_ORDERS_URL},
83 credential::Credential,
84 enums::{AxCandleWidth, AxInstrumentState},
85 parse::{ax_timestamp_stn_to_unix_nanos, cid_to_client_order_id, client_order_id_to_cid},
86};
87
88pub static AX_REST_QUOTA: LazyLock<Quota> = LazyLock::new(|| {
92 Quota::per_second(NonZeroU32::new(10).expect("non-zero")).expect("valid constant")
93});
94
95const AX_GLOBAL_RATE_KEY: &str = "architect:global";
96
97pub struct AxRawHttpClient {
102 base_url: String,
103 orders_base_url: String,
104 client: HttpClient,
105 credential: Option<Credential>,
106 session_token: RwLock<Option<String>>,
107 retry_manager: RetryManager<AxHttpError>,
108 cancellation_token: RwLock<CancellationToken>,
109}
110
111impl Default for AxRawHttpClient {
112 fn default() -> Self {
113 Self::new(None, None, 60, 3, 1000, 10_000, None)
114 .expect("Failed to create default AxRawHttpClient")
115 }
116}
117
118impl Debug for AxRawHttpClient {
119 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120 let has_session_token = self.session_token.read().is_some();
121 f.debug_struct(stringify!(AxRawHttpClient))
122 .field("base_url", &self.base_url)
123 .field("orders_base_url", &self.orders_base_url)
124 .field("has_credentials", &self.credential.is_some())
125 .field("has_session_token", &has_session_token)
126 .finish()
127 }
128}
129
130impl AxRawHttpClient {
131 #[must_use]
133 pub fn base_url(&self) -> &str {
134 &self.base_url
135 }
136
137 #[must_use]
139 pub fn api_key_masked(&self) -> String {
140 self.credential
141 .as_ref()
142 .map_or_else(|| "None".to_string(), |c| c.masked_api_key())
143 }
144
145 pub fn cancel_all_requests(&self) {
147 self.cancellation_token.read().cancel();
148 }
149
150 pub fn reset_cancellation_token(&self) {
152 *self.cancellation_token.write() = CancellationToken::new();
153 }
154
155 pub fn cancellation_token(&self) -> CancellationToken {
157 self.cancellation_token.read().clone()
158 }
159
160 pub fn new(
166 base_url: Option<String>,
167 orders_base_url: Option<String>,
168 timeout_secs: u64,
169 max_retries: u32,
170 retry_delay_ms: u64,
171 retry_delay_max_ms: u64,
172 proxy_url: Option<String>,
173 ) -> Result<Self, AxHttpError> {
174 let retry_config = RetryConfig {
175 max_retries,
176 initial_delay_ms: retry_delay_ms,
177 max_delay_ms: retry_delay_max_ms,
178 backoff_factor: 2.0,
179 jitter_ms: 1000,
180 operation_timeout_ms: Some(60_000),
181 immediate_first: false,
182 max_elapsed_ms: Some(180_000),
183 };
184
185 let retry_manager = RetryManager::new(retry_config);
186
187 Ok(Self {
188 base_url: base_url.unwrap_or_else(|| AX_HTTP_URL.to_string()),
189 orders_base_url: orders_base_url.unwrap_or_else(|| AX_ORDERS_URL.to_string()),
190 client: HttpClient::builder()
191 .headers(Self::default_headers())
192 .keyed_quotas(Self::rate_limiter_quotas())
193 .default_quota(*AX_REST_QUOTA)
194 .timeout_secs(timeout_secs)
195 .maybe_proxy_url(proxy_url)
196 .build()
197 .map_err(|e| {
198 AxHttpError::NetworkError(format!("Failed to create HTTP client: {e}"))
199 })?,
200 credential: None,
201 session_token: RwLock::new(None),
202 retry_manager,
203 cancellation_token: RwLock::new(CancellationToken::new()),
204 })
205 }
206
207 #[expect(clippy::too_many_arguments)]
213 pub fn with_credentials(
214 api_key: String,
215 api_secret: String,
216 base_url: Option<String>,
217 orders_base_url: Option<String>,
218 timeout_secs: u64,
219 max_retries: u32,
220 retry_delay_ms: u64,
221 retry_delay_max_ms: u64,
222 proxy_url: Option<String>,
223 ) -> Result<Self, AxHttpError> {
224 let retry_config = RetryConfig {
225 max_retries,
226 initial_delay_ms: retry_delay_ms,
227 max_delay_ms: retry_delay_max_ms,
228 backoff_factor: 2.0,
229 jitter_ms: 1000,
230 operation_timeout_ms: Some(60_000),
231 immediate_first: false,
232 max_elapsed_ms: Some(180_000),
233 };
234
235 let retry_manager = RetryManager::new(retry_config);
236
237 Ok(Self {
238 base_url: base_url.unwrap_or_else(|| AX_HTTP_URL.to_string()),
239 orders_base_url: orders_base_url.unwrap_or_else(|| AX_ORDERS_URL.to_string()),
240 client: HttpClient::builder()
241 .headers(Self::default_headers())
242 .keyed_quotas(Self::rate_limiter_quotas())
243 .default_quota(*AX_REST_QUOTA)
244 .timeout_secs(timeout_secs)
245 .maybe_proxy_url(proxy_url)
246 .build()
247 .map_err(|e| {
248 AxHttpError::NetworkError(format!("Failed to create HTTP client: {e}"))
249 })?,
250 credential: Some(Credential::new(api_key, api_secret)),
251 session_token: RwLock::new(None),
252 retry_manager,
253 cancellation_token: RwLock::new(CancellationToken::new()),
254 })
255 }
256
257 pub fn set_session_token(&self, token: String) {
261 *self.session_token.write() = Some(token);
262 }
263
264 pub(crate) fn has_session_token(&self) -> bool {
265 self.session_token.read().is_some()
266 }
267
268 fn default_headers() -> HashMap<String, String> {
269 HashMap::from([
270 (USER_AGENT.to_string(), NAUTILUS_USER_AGENT.to_string()),
271 ("Accept".to_string(), "application/json".to_string()),
272 ])
273 }
274
275 fn rate_limiter_quotas() -> Vec<(String, Quota)> {
276 vec![(AX_GLOBAL_RATE_KEY.to_string(), *AX_REST_QUOTA)]
277 }
278
279 fn rate_limit_keys(endpoint: &str) -> Vec<String> {
280 let normalized = endpoint.split('?').next().unwrap_or(endpoint);
281 let route = format!("architect:{normalized}");
282
283 vec![AX_GLOBAL_RATE_KEY.to_string(), route]
284 }
285
286 fn auth_headers(&self) -> Result<HashMap<String, String>, AxHttpError> {
287 let guard = self.session_token.read();
288 let session_token = guard.as_ref().ok_or(AxHttpError::MissingSessionToken)?;
289
290 let mut headers = HashMap::new();
291 headers.insert(
292 "Authorization".to_string(),
293 format!("Bearer {session_token}"),
294 );
295
296 Ok(headers)
297 }
298
299 async fn send_request<T: DeserializeOwned, P: Serialize>(
300 &self,
301 method: Method,
302 endpoint: &str,
303 params: Option<&P>,
304 body: Option<Vec<u8>>,
305 authenticate: bool,
306 ) -> Result<T, AxHttpError> {
307 self.send_request_to_url(&self.base_url, method, endpoint, params, body, authenticate)
308 .await
309 }
310
311 async fn send_request_to_url<T: DeserializeOwned, P: Serialize>(
312 &self,
313 base_url: &str,
314 method: Method,
315 endpoint: &str,
316 params: Option<&P>,
317 body: Option<Vec<u8>>,
318 authenticate: bool,
319 ) -> Result<T, AxHttpError> {
320 let endpoint = endpoint.to_string();
321 let url = format!("{base_url}{endpoint}");
322
323 let params_str = if method == Method::GET || method == Method::DELETE {
324 params
325 .map(serde_urlencoded::to_string)
326 .transpose()
327 .map_err(|e| AxHttpError::JsonError(format!("Failed to serialize params: {e}")))?
328 } else {
329 None
330 };
331
332 let operation = || {
333 let url = url.clone();
334 let method = method.clone();
335 let endpoint = endpoint.clone();
336 let params_str = params_str.clone();
337 let body = body.clone();
338
339 async move {
340 let mut headers = Self::default_headers();
341
342 if authenticate {
343 let auth_headers = self.auth_headers()?;
344 headers.extend(auth_headers);
345 }
346
347 if body.is_some() {
348 headers.insert("Content-Type".to_string(), "application/json".to_string());
349 }
350
351 let full_url = if let Some(ref query) = params_str {
352 if query.is_empty() {
353 url
354 } else {
355 format!("{url}?{query}")
356 }
357 } else {
358 url
359 };
360
361 let rate_limit_keys = Self::rate_limit_keys(&endpoint);
362
363 let response = self
364 .client
365 .request(
366 method,
367 full_url,
368 None,
369 Some(headers),
370 body,
371 None,
372 Some(rate_limit_keys),
373 )
374 .await?;
375
376 let status = response.status;
377 let response_body = String::from_utf8_lossy(&response.body).to_string();
378
379 if !status.is_success() {
380 return Err(AxHttpError::UnexpectedStatus {
381 status: status.as_u16(),
382 body: response_body,
383 });
384 }
385
386 serde_json::from_str(&response_body).map_err(|e| {
387 AxHttpError::JsonError(format!(
388 "Failed to deserialize response: {e}\nBody: {response_body}"
389 ))
390 })
391 }
392 };
393
394 let is_idempotent = matches!(method, Method::GET | Method::HEAD | Method::OPTIONS);
396 let should_retry = |error: &AxHttpError| -> bool { is_idempotent && error.is_retryable() };
397
398 let create_error = |error: RetryError| -> AxHttpError {
399 match error {
400 RetryError::Canceled => {
401 AxHttpError::Canceled("Adapter disconnecting or shutting down".to_string())
402 }
403 error => AxHttpError::NetworkError(error.to_string()),
404 }
405 };
406
407 let cancel_token = self.cancellation_token.read().clone();
408
409 self.retry_manager
410 .execute_with_retry_with_cancel(
411 endpoint.as_str(),
412 operation,
413 should_retry,
414 create_error,
415 &cancel_token,
416 )
417 .await
418 }
419
420 pub async fn get_whoami(&self) -> Result<AxWhoAmI, AxHttpError> {
429 self.send_request::<AxWhoAmI, ()>(Method::GET, "/whoami", None, None, true)
430 .await
431 }
432
433 pub async fn get_instruments(&self) -> Result<AxInstrumentsResponse, AxHttpError> {
442 self.send_request::<AxInstrumentsResponse, ()>(
443 Method::GET,
444 "/instruments",
445 None,
446 None,
447 false,
448 )
449 .await
450 }
451
452 pub async fn get_balances(&self) -> Result<AxBalancesResponse, AxHttpError> {
461 self.send_request::<AxBalancesResponse, ()>(Method::GET, "/balances", None, None, true)
462 .await
463 }
464
465 pub async fn get_positions(&self) -> Result<AxPositionsResponse, AxHttpError> {
474 self.send_request::<AxPositionsResponse, ()>(Method::GET, "/positions", None, None, true)
475 .await
476 }
477
478 pub async fn get_tickers(&self) -> Result<AxTickersResponse, AxHttpError> {
487 self.send_request::<AxTickersResponse, ()>(Method::GET, "/tickers", None, None, true)
488 .await
489 }
490
491 pub async fn get_tickers_with_params(
500 &self,
501 params: &GetTickersParams,
502 ) -> Result<AxTickersResponse, AxHttpError> {
503 self.send_request::<AxTickersResponse, _>(Method::GET, "/tickers", Some(params), None, true)
504 .await
505 }
506
507 pub async fn get_ticker(&self, symbol: Ustr) -> Result<AxTicker, AxHttpError> {
516 let params = GetTickerParams::new(symbol);
517 self.send_request::<AxTickerResponse, _>(Method::GET, "/ticker", Some(¶ms), None, true)
518 .await
519 .map(|response| response.ticker)
520 }
521
522 pub async fn get_instrument(&self, symbol: Ustr) -> Result<AxInstrument, AxHttpError> {
531 let params = GetInstrumentParams::new(symbol);
532 self.send_request::<AxInstrument, _>(Method::GET, "/instrument", Some(¶ms), None, false)
533 .await
534 }
535
536 pub async fn authenticate(
545 &self,
546 api_key: &str,
547 api_secret: &str,
548 expiration_seconds: i32,
549 ) -> Result<AxAuthenticateResponse, AxHttpError> {
550 let request = AuthenticateApiKeyRequest::new(api_key, api_secret, expiration_seconds);
551
552 let body = serde_json::to_vec(&request)
553 .map_err(|e| AxHttpError::JsonError(format!("Failed to serialize request: {e}")))?;
554
555 self.send_request::<AxAuthenticateResponse, ()>(
556 Method::POST,
557 "/authenticate",
558 None,
559 Some(body),
560 false,
561 )
562 .await
563 }
564
565 pub async fn authenticate_auto(
580 &self,
581 expiration_seconds: i32,
582 ) -> Result<AxAuthenticateResponse, AxHttpError> {
583 let (api_key, api_secret) = self
584 .resolve_credentials()
585 .ok_or(AxHttpError::MissingCredentials)?;
586
587 self.authenticate(&api_key, &api_secret, expiration_seconds)
588 .await
589 }
590
591 fn resolve_credentials(&self) -> Option<(String, String)> {
592 if let Some(cred) = &self.credential {
593 return Some((cred.api_key().to_string(), cred.api_secret().to_string()));
594 }
595
596 let cred = Credential::resolve(None, None)?;
597 Some((cred.api_key().to_string(), cred.api_secret().to_string()))
598 }
599
600 pub async fn place_order(
609 &self,
610 request: &PlaceOrderRequest,
611 ) -> Result<AxPlaceOrderResponse, AxHttpError> {
612 let body = serde_json::to_vec(request)
613 .map_err(|e| AxHttpError::JsonError(format!("Failed to serialize request: {e}")))?;
614 self.send_request_to_url::<AxPlaceOrderResponse, ()>(
615 &self.orders_base_url,
616 Method::POST,
617 "/place-order",
618 None,
619 Some(body),
620 true,
621 )
622 .await
623 }
624
625 pub async fn cancel_order(&self, order_id: &str) -> Result<AxCancelOrderResponse, AxHttpError> {
634 let request = CancelOrderRequest::new(order_id);
635 let body = serde_json::to_vec(&request)
636 .map_err(|e| AxHttpError::JsonError(format!("Failed to serialize request: {e}")))?;
637 self.send_request_to_url::<AxCancelOrderResponse, ()>(
638 &self.orders_base_url,
639 Method::POST,
640 "/cancel-order",
641 None,
642 Some(body),
643 true,
644 )
645 .await
646 }
647
648 pub async fn replace_order(
660 &self,
661 request: &ReplaceOrderRequest,
662 ) -> Result<AxReplaceOrderResponse, AxHttpError> {
663 let body = serde_json::to_vec(request)
664 .map_err(|e| AxHttpError::JsonError(format!("Failed to serialize request: {e}")))?;
665 self.send_request_to_url::<AxReplaceOrderResponse, ()>(
666 &self.orders_base_url,
667 Method::POST,
668 "/replace-order",
669 None,
670 Some(body),
671 true,
672 )
673 .await
674 }
675
676 pub async fn cancel_all_orders(
685 &self,
686 request: &CancelAllOrdersRequest,
687 ) -> Result<AxCancelAllOrdersResponse, AxHttpError> {
688 let body = serde_json::to_vec(request)
689 .map_err(|e| AxHttpError::JsonError(format!("Failed to serialize request: {e}")))?;
690 self.send_request_to_url::<AxCancelAllOrdersResponse, ()>(
691 &self.orders_base_url,
692 Method::POST,
693 "/cancel-all-orders",
694 None,
695 Some(body),
696 true,
697 )
698 .await
699 }
700
701 pub async fn get_open_orders(&self) -> Result<AxOpenOrdersResponse, AxHttpError> {
710 self.get_open_orders_page(&GetOpenOrdersParams::new()).await
711 }
712
713 pub async fn get_open_orders_page(
719 &self,
720 params: &GetOpenOrdersParams,
721 ) -> Result<AxOpenOrdersResponse, AxHttpError> {
722 self.send_request_to_url::<AxOpenOrdersResponse, _>(
723 &self.orders_base_url,
724 Method::GET,
725 "/open-orders",
726 Some(params),
727 None,
728 true,
729 )
730 .await
731 }
732
733 pub async fn get_fills(
742 &self,
743 start_timestamp_ns: i64,
744 end_timestamp_ns: i64,
745 ) -> Result<AxFillsResponse, AxHttpError> {
746 let params = GetFillsParams::new(start_timestamp_ns, end_timestamp_ns);
747 self.get_fills_page(¶ms).await
748 }
749
750 pub async fn get_fills_page(
756 &self,
757 params: &GetFillsParams,
758 ) -> Result<AxFillsResponse, AxHttpError> {
759 self.send_request::<AxFillsResponse, _>(Method::GET, "/fills", Some(params), None, true)
760 .await
761 }
762
763 pub async fn get_candles(
772 &self,
773 symbol: Ustr,
774 start_timestamp_ns: i64,
775 end_timestamp_ns: i64,
776 candle_width: AxCandleWidth,
777 ) -> Result<AxCandlesResponse, AxHttpError> {
778 let params =
779 GetCandlesParams::new(symbol, start_timestamp_ns, end_timestamp_ns, candle_width);
780 self.send_request::<AxCandlesResponse, _>(
781 Method::GET,
782 "/candles",
783 Some(¶ms),
784 None,
785 true,
786 )
787 .await
788 }
789
790 pub async fn get_current_candle(
799 &self,
800 symbol: Ustr,
801 candle_width: AxCandleWidth,
802 ) -> Result<AxCandle, AxHttpError> {
803 let params = GetCandleParams::new(symbol, candle_width);
804 let response = self
805 .send_request::<AxCandleResponse, _>(
806 Method::GET,
807 "/candles/current",
808 Some(¶ms),
809 None,
810 true,
811 )
812 .await?;
813 Ok(response.candle)
814 }
815
816 pub async fn get_last_candle(
825 &self,
826 symbol: Ustr,
827 candle_width: AxCandleWidth,
828 ) -> Result<AxCandle, AxHttpError> {
829 let params = GetCandleParams::new(symbol, candle_width);
830 let response = self
831 .send_request::<AxCandleResponse, _>(
832 Method::GET,
833 "/candles/last",
834 Some(¶ms),
835 None,
836 true,
837 )
838 .await?;
839 Ok(response.candle)
840 }
841
842 pub async fn get_funding_rates(
851 &self,
852 symbol: Ustr,
853 start_timestamp_ns: i64,
854 end_timestamp_ns: i64,
855 ) -> Result<AxFundingRatesResponse, AxHttpError> {
856 let params = GetFundingRatesParams::new(symbol, start_timestamp_ns, end_timestamp_ns);
857 self.get_funding_rates_page(¶ms).await
858 }
859
860 pub async fn get_funding_rates_page(
866 &self,
867 params: &GetFundingRatesParams,
868 ) -> Result<AxFundingRatesResponse, AxHttpError> {
869 self.send_request::<AxFundingRatesResponse, _>(
870 Method::GET,
871 "/funding-rates",
872 Some(params),
873 None,
874 true,
875 )
876 .await
877 }
878
879 pub async fn get_funding_slots(
888 &self,
889 params: &GetFundingSlotsParams,
890 ) -> Result<AxFundingSlotsResponse, AxHttpError> {
891 self.send_request::<AxFundingSlotsResponse, _>(
892 Method::GET,
893 "/funding-slots",
894 Some(params),
895 None,
896 true,
897 )
898 .await
899 }
900
901 pub async fn get_risk_snapshot(&self) -> Result<AxRiskSnapshotResponse, AxHttpError> {
910 self.send_request::<AxRiskSnapshotResponse, ()>(
911 Method::GET,
912 "/risk-snapshot",
913 None,
914 None,
915 true,
916 )
917 .await
918 }
919
920 pub async fn preview_aggressive_limit_order(
933 &self,
934 request: &PreviewAggressiveLimitOrderRequest,
935 ) -> Result<AxPreviewAggressiveLimitOrderResponse, AxHttpError> {
936 let body = serde_json::to_vec(request)
937 .map_err(|e| AxHttpError::JsonError(format!("Failed to serialize request: {e}")))?;
938 self.send_request::<AxPreviewAggressiveLimitOrderResponse, ()>(
939 Method::POST,
940 "/preview-aggressive-limit-order",
941 None,
942 Some(body),
943 true,
944 )
945 .await
946 }
947
948 pub async fn get_transactions(
957 &self,
958 transaction_types: Vec<String>,
959 start_timestamp_ns: i64,
960 end_timestamp_ns: i64,
961 ) -> Result<AxTransactionsResponse, AxHttpError> {
962 let params =
963 GetTransactionsParams::new(transaction_types, start_timestamp_ns, end_timestamp_ns);
964 self.get_transactions_page(¶ms).await
965 }
966
967 pub async fn get_transactions_page(
973 &self,
974 params: &GetTransactionsParams,
975 ) -> Result<AxTransactionsResponse, AxHttpError> {
976 self.send_request::<AxTransactionsResponse, _>(
977 Method::GET,
978 "/transactions",
979 Some(params),
980 None,
981 true,
982 )
983 .await
984 }
985
986 pub async fn get_trades(
995 &self,
996 symbol: Ustr,
997 limit: Option<i32>,
998 ) -> Result<AxTradesResponse, AxHttpError> {
999 let params = GetTradesParams::new(symbol, limit);
1000 self.send_request::<AxTradesResponse, _>(Method::GET, "/trades", Some(¶ms), None, true)
1001 .await
1002 }
1003
1004 pub async fn get_book(
1013 &self,
1014 symbol: Ustr,
1015 level: Option<i32>,
1016 ) -> Result<AxBookResponse, AxHttpError> {
1017 let params = GetBookParams::new(symbol, level);
1018 self.send_request::<AxBookResponse, _>(Method::GET, "/book", Some(¶ms), None, true)
1020 .await
1021 }
1022
1023 pub async fn get_order_status_by_id(
1032 &self,
1033 order_id: &str,
1034 ) -> Result<AxOrderStatusQueryResponse, AxHttpError> {
1035 let params = GetOrderStatusParams::by_order_id(order_id);
1036 self.send_request_to_url::<AxOrderStatusQueryResponse, _>(
1037 &self.orders_base_url,
1038 Method::GET,
1039 "/order-status",
1040 Some(¶ms),
1041 None,
1042 true,
1043 )
1044 .await
1045 }
1046
1047 pub async fn get_order_status_by_cid(
1056 &self,
1057 client_order_id: u64,
1058 ) -> Result<AxOrderStatusQueryResponse, AxHttpError> {
1059 let params = GetOrderStatusParams::by_client_order_id(client_order_id);
1060 self.send_request_to_url::<AxOrderStatusQueryResponse, _>(
1061 &self.orders_base_url,
1062 Method::GET,
1063 "/order-status",
1064 Some(¶ms),
1065 None,
1066 true,
1067 )
1068 .await
1069 }
1070
1071 pub async fn get_orders(
1080 &self,
1081 params: &GetOrdersParams,
1082 ) -> Result<AxOrdersResponse, AxHttpError> {
1083 self.send_request_to_url::<AxOrdersResponse, _>(
1084 &self.orders_base_url,
1085 Method::GET,
1086 "/orders",
1087 Some(params),
1088 None,
1089 true,
1090 )
1091 .await
1092 }
1093
1094 pub async fn check_initial_margin(
1103 &self,
1104 request: &PlaceOrderRequest,
1105 ) -> Result<AxInitialMarginRequirementResponse, AxHttpError> {
1106 let body = serde_json::to_vec(request)
1107 .map_err(|e| AxHttpError::JsonError(format!("Failed to serialize request: {e}")))?;
1108 self.send_request_to_url::<AxInitialMarginRequirementResponse, ()>(
1109 &self.orders_base_url,
1110 Method::POST,
1111 "/initial-margin-requirement",
1112 None,
1113 Some(body),
1114 true,
1115 )
1116 .await
1117 }
1118}
1119
1120#[derive(Debug)]
1125#[cfg_attr(
1126 feature = "python",
1127 pyo3::pyclass(module = "nautilus_trader.adapters.architect_ax", from_py_object)
1128)]
1129#[cfg_attr(
1130 feature = "python",
1131 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.architect_ax")
1132)]
1133pub struct AxHttpClient {
1134 pub(crate) inner: Arc<AxRawHttpClient>,
1135 pub(crate) instruments_cache: Arc<AtomicMap<Ustr, InstrumentAny>>,
1136 clock: &'static AtomicTime,
1137 cache_initialized: Arc<AtomicBool>,
1138 account_fees: Arc<ArcSwapOption<(Decimal, Decimal)>>,
1139}
1140
1141impl Clone for AxHttpClient {
1142 fn clone(&self) -> Self {
1143 Self {
1144 inner: self.inner.clone(),
1145 instruments_cache: self.instruments_cache.clone(),
1146 cache_initialized: self.cache_initialized.clone(),
1147 clock: self.clock,
1148 account_fees: self.account_fees.clone(),
1149 }
1150 }
1151}
1152
1153impl Default for AxHttpClient {
1154 fn default() -> Self {
1155 Self::new(None, None, 60, 3, 1000, 10_000, None)
1156 .expect("Failed to create default AxHttpClient")
1157 }
1158}
1159
1160impl AxHttpClient {
1161 pub fn new(
1167 base_url: Option<String>,
1168 orders_base_url: Option<String>,
1169 timeout_secs: u64,
1170 max_retries: u32,
1171 retry_delay_ms: u64,
1172 retry_delay_max_ms: u64,
1173 proxy_url: Option<String>,
1174 ) -> Result<Self, AxHttpError> {
1175 Ok(Self {
1176 inner: Arc::new(AxRawHttpClient::new(
1177 base_url,
1178 orders_base_url,
1179 timeout_secs,
1180 max_retries,
1181 retry_delay_ms,
1182 retry_delay_max_ms,
1183 proxy_url,
1184 )?),
1185 instruments_cache: Arc::new(AtomicMap::new()),
1186 cache_initialized: Arc::new(AtomicBool::new(false)),
1187 clock: get_atomic_clock_realtime(),
1188 account_fees: Arc::new(ArcSwapOption::empty()),
1189 })
1190 }
1191
1192 #[expect(clippy::too_many_arguments)]
1198 pub fn with_credentials(
1199 api_key: String,
1200 api_secret: String,
1201 base_url: Option<String>,
1202 orders_base_url: Option<String>,
1203 timeout_secs: u64,
1204 max_retries: u32,
1205 retry_delay_ms: u64,
1206 retry_delay_max_ms: u64,
1207 proxy_url: Option<String>,
1208 ) -> Result<Self, AxHttpError> {
1209 Ok(Self {
1210 inner: Arc::new(AxRawHttpClient::with_credentials(
1211 api_key,
1212 api_secret,
1213 base_url,
1214 orders_base_url,
1215 timeout_secs,
1216 max_retries,
1217 retry_delay_ms,
1218 retry_delay_max_ms,
1219 proxy_url,
1220 )?),
1221 instruments_cache: Arc::new(AtomicMap::new()),
1222 cache_initialized: Arc::new(AtomicBool::new(false)),
1223 clock: get_atomic_clock_realtime(),
1224 account_fees: Arc::new(ArcSwapOption::empty()),
1225 })
1226 }
1227
1228 #[must_use]
1230 pub fn base_url(&self) -> &str {
1231 self.inner.base_url()
1232 }
1233
1234 #[must_use]
1236 pub fn api_key_masked(&self) -> String {
1237 self.inner.api_key_masked()
1238 }
1239
1240 pub fn cancel_all_requests(&self) {
1242 self.inner.cancel_all_requests();
1243 }
1244
1245 pub fn reset_cancellation_token(&self) {
1247 self.inner.reset_cancellation_token();
1248 }
1249
1250 pub fn set_session_token(&self, token: String) {
1254 self.inner.set_session_token(token);
1255 }
1256
1257 fn generate_ts_init(&self) -> UnixNanos {
1259 self.clock.get_time_ns()
1260 }
1261
1262 #[must_use]
1266 pub fn is_initialized(&self) -> bool {
1267 self.cache_initialized.load(Ordering::Acquire)
1268 }
1269
1270 #[must_use]
1272 pub fn get_cached_symbols(&self) -> Vec<String> {
1273 self.instruments_cache
1274 .load()
1275 .keys()
1276 .map(|k| k.to_string())
1277 .collect()
1278 }
1279
1280 pub fn cache_instruments(&self, instruments: &[InstrumentAny]) {
1284 self.instruments_cache.rcu(|m| {
1285 for inst in instruments {
1286 m.insert(inst.raw_symbol().inner(), inst.clone());
1287 }
1288 });
1289 self.cache_initialized.store(true, Ordering::Release);
1290 }
1291
1292 pub fn cache_instrument(&self, instrument: InstrumentAny) {
1296 self.instruments_cache
1297 .insert(instrument.raw_symbol().inner(), instrument);
1298 self.cache_initialized.store(true, Ordering::Release);
1299 }
1300
1301 pub async fn authenticate(
1309 &self,
1310 api_key: &str,
1311 api_secret: &str,
1312 expiration_seconds: i32,
1313 ) -> Result<String, AxHttpError> {
1314 let resp = self
1315 .inner
1316 .authenticate(api_key, api_secret, expiration_seconds)
1317 .await?;
1318 self.inner.set_session_token(resp.token.clone());
1319 Ok(resp.token)
1320 }
1321
1322 pub async fn authenticate_auto(&self, expiration_seconds: i32) -> Result<String, AxHttpError> {
1339 let resp = self.inner.authenticate_auto(expiration_seconds).await?;
1340 self.inner.set_session_token(resp.token.clone());
1341 Ok(resp.token)
1342 }
1343
1344 pub fn get_instrument(&self, symbol: &Ustr) -> Option<InstrumentAny> {
1346 self.instruments_cache.get_cloned(symbol)
1347 }
1348
1349 pub async fn request_account_fees(&self) -> anyhow::Result<(Decimal, Decimal)> {
1364 let whoami = self
1365 .inner
1366 .get_whoami()
1367 .await
1368 .map_err(|e| anyhow::anyhow!(e))
1369 .context("failed to request AX whoami")?;
1370
1371 let Some(account) = whoami.accounts.first() else {
1372 anyhow::bail!("AX whoami returned no accounts to resolve fees from");
1373 };
1374
1375 if whoami.accounts.len() > 1 {
1376 log::warn!(
1377 "AX credentials cover {} accounts, using fee rates from {}",
1378 whoami.accounts.len(),
1379 account.id,
1380 );
1381 }
1382
1383 let (Some(maker_fee), Some(taker_fee)) = (account.maker_fee, account.taker_fee) else {
1384 anyhow::bail!("AX whoami account {} supplied no fee rates", account.id);
1385 };
1386
1387 let fees = (maker_fee, taker_fee);
1388 self.account_fees.store(Some(Arc::new(fees)));
1389
1390 Ok(fees)
1391 }
1392
1393 pub async fn request_instruments(
1402 &self,
1403 maker_fee: Option<Decimal>,
1404 taker_fee: Option<Decimal>,
1405 ) -> anyhow::Result<Vec<InstrumentAny>> {
1406 let resp = self
1407 .inner
1408 .get_instruments()
1409 .await
1410 .map_err(|e| anyhow::anyhow!(e))?;
1411
1412 let (maker_fee, taker_fee) = self.resolve_fees(maker_fee, taker_fee);
1413 let ts_init = self.generate_ts_init();
1414
1415 let mut instruments: Vec<InstrumentAny> = Vec::new();
1416 for inst in &resp.instruments {
1417 if inst.state == AxInstrumentState::Delisted {
1418 log::debug!("Skipping delisted instrument: {}", inst.symbol);
1419 continue;
1420 }
1421
1422 if inst.symbol.as_str().starts_with("TEST") {
1424 log::debug!("Skipping test instrument: {}", inst.symbol);
1425 continue;
1426 }
1427
1428 match parse_instrument(inst, maker_fee, taker_fee, ts_init, ts_init) {
1429 Ok(instrument) => instruments.push(instrument),
1430 Err(e) => {
1431 log::warn!("Failed to parse instrument {}: {e}", inst.symbol);
1432 }
1433 }
1434 }
1435
1436 Ok(instruments)
1437 }
1438
1439 pub async fn request_instrument(
1448 &self,
1449 symbol: Ustr,
1450 maker_fee: Option<Decimal>,
1451 taker_fee: Option<Decimal>,
1452 ) -> anyhow::Result<InstrumentAny> {
1453 let resp = self
1454 .inner
1455 .get_instrument(symbol)
1456 .await
1457 .map_err(|e| anyhow::anyhow!(e))?;
1458
1459 let (maker_fee, taker_fee) = self.resolve_fees(maker_fee, taker_fee);
1460 let ts_init = self.generate_ts_init();
1461
1462 parse_instrument(&resp, maker_fee, taker_fee, ts_init, ts_init)
1463 }
1464
1465 fn resolve_fees(
1466 &self,
1467 maker_fee: Option<Decimal>,
1468 taker_fee: Option<Decimal>,
1469 ) -> (Decimal, Decimal) {
1470 let resolved = self.account_fees.load();
1471
1472 let Some(&(resolved_maker, resolved_taker)) = resolved.as_deref() else {
1473 if (maker_fee.is_none() || taker_fee.is_none()) && self.inner.has_session_token() {
1475 log::warn!(
1476 "Building instruments with zero fees: authenticated but account fee rates \
1477 were never resolved"
1478 );
1479 }
1480
1481 return (
1482 maker_fee.unwrap_or(Decimal::ZERO),
1483 taker_fee.unwrap_or(Decimal::ZERO),
1484 );
1485 };
1486
1487 (
1488 maker_fee.unwrap_or(resolved_maker),
1489 taker_fee.unwrap_or(resolved_taker),
1490 )
1491 }
1492
1493 pub async fn request_book_snapshot(
1503 &self,
1504 symbol: Ustr,
1505 depth: Option<usize>,
1506 ) -> anyhow::Result<OrderBook> {
1507 let instrument = self
1508 .get_instrument(&symbol)
1509 .ok_or_else(|| anyhow::anyhow!("Instrument {symbol} not found in cache"))?;
1510
1511 let resp = self
1512 .inner
1513 .get_book(symbol, Some(2))
1514 .await
1515 .map_err(|e| anyhow::anyhow!(e))?;
1516
1517 let instrument_id = instrument.id();
1518 let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
1519
1520 let price_precision = instrument.price_precision();
1521 let size_precision = instrument.size_precision();
1522 let ts_event = ax_timestamp_stn_to_unix_nanos(resp.book.ts, resp.book.tn)?;
1523
1524 for (i, level) in resp.book.b.iter().enumerate() {
1525 if depth.is_some_and(|d| i >= d) {
1526 break;
1527 }
1528 let price = Price::from_decimal_dp(level.p, price_precision).with_context(|| {
1529 format!(
1530 "Failed to convert AX book bid price {} for {symbol}",
1531 level.p
1532 )
1533 })?;
1534 let size = Quantity::new(level.q as f64, size_precision);
1535 let order = BookOrder::new(OrderSide::Buy, price, size, i as u64);
1536 book.add(order, 0, i as u64, ts_event);
1537 }
1538
1539 let bids_len = resp.book.b.len();
1540 for (i, level) in resp.book.a.iter().enumerate() {
1541 if depth.is_some_and(|d| i >= d) {
1542 break;
1543 }
1544 let price = Price::from_decimal_dp(level.p, price_precision).with_context(|| {
1545 format!(
1546 "Failed to convert AX book ask price {} for {symbol}",
1547 level.p
1548 )
1549 })?;
1550 let size = Quantity::new(level.q as f64, size_precision);
1551 let order = BookOrder::new(OrderSide::Sell, price, size, (bids_len + i) as u64);
1552 book.add(order, 0, (bids_len + i) as u64, ts_event);
1553 }
1554
1555 Ok(book)
1556 }
1557
1558 pub async fn request_trade_ticks(
1572 &self,
1573 symbol: Ustr,
1574 limit: Option<i32>,
1575 start: Option<UnixNanos>,
1576 end: Option<UnixNanos>,
1577 ) -> anyhow::Result<Vec<TradeTick>> {
1578 let instrument = self
1579 .get_instrument(&symbol)
1580 .ok_or_else(|| anyhow::anyhow!("Instrument {symbol} not found in cache"))?;
1581
1582 let resp = self
1583 .inner
1584 .get_trades(symbol, limit)
1585 .await
1586 .map_err(|e| anyhow::anyhow!(e))?;
1587
1588 let ts_init = self.generate_ts_init();
1589 let mut ticks = Vec::with_capacity(resp.trades.len());
1590
1591 for trade in &resp.trades {
1592 match parse_trade_tick(trade, &instrument, ts_init) {
1593 Ok(tick) => {
1594 if start.is_some_and(|s| tick.ts_event < s) {
1595 continue;
1596 }
1597
1598 if end.is_some_and(|e| tick.ts_event > e) {
1599 continue;
1600 }
1601 ticks.push(tick);
1602 }
1603 Err(e) => {
1604 log::warn!("Failed to parse trade for {symbol}: {e}");
1605 }
1606 }
1607 }
1608
1609 Ok(ticks)
1610 }
1611
1612 pub async fn request_bars(
1623 &self,
1624 symbol: Ustr,
1625 start: Option<Timestamp>,
1626 end: Option<Timestamp>,
1627 width: AxCandleWidth,
1628 ) -> anyhow::Result<Vec<Bar>> {
1629 let instrument = self
1630 .get_instrument(&symbol)
1631 .ok_or_else(|| anyhow::anyhow!("Instrument {symbol} not found in cache"))?;
1632
1633 let start_ns = start
1634 .and_then(|dt| i64::try_from(dt.as_nanosecond()).ok())
1635 .unwrap_or(0);
1636 let end_ns = end
1637 .and_then(|dt| i64::try_from(dt.as_nanosecond()).ok())
1638 .unwrap_or_else(|| self.generate_ts_init().as_i64());
1639 let resp = self
1640 .inner
1641 .get_candles(symbol, start_ns, end_ns, width)
1642 .await
1643 .map_err(|e| anyhow::anyhow!(e))?;
1644
1645 let ts_init = self.generate_ts_init();
1646 let mut bars = Vec::with_capacity(resp.candles.len());
1647
1648 for candle in &resp.candles {
1649 match parse_bar(candle, &instrument, ts_init) {
1650 Ok(bar) => bars.push(bar),
1651 Err(e) => {
1652 log::warn!("Failed to parse bar for {symbol}: {e}");
1653 }
1654 }
1655 }
1656
1657 Ok(bars)
1658 }
1659
1660 pub async fn request_funding_rates(
1669 &self,
1670 instrument_id: InstrumentId,
1671 start: Option<Timestamp>,
1672 end: Option<Timestamp>,
1673 ) -> Result<Vec<FundingRateUpdate>, AxHttpError> {
1674 const PAGE_SIZE: i32 = 100;
1675
1676 let symbol = instrument_id.symbol.inner();
1677 let start_ns = start
1678 .and_then(|dt| i64::try_from(dt.as_nanosecond()).ok())
1679 .unwrap_or(0);
1680 let end_ns = end
1681 .and_then(|dt| i64::try_from(dt.as_nanosecond()).ok())
1682 .unwrap_or_else(|| self.generate_ts_init().as_i64());
1683 let mut params = GetFundingRatesParams::new(symbol, start_ns, end_ns);
1684 params.limit = Some(PAGE_SIZE);
1685 params.sort_ts = Some("desc".to_string());
1686
1687 let mut funding_rates = Vec::new();
1688 let mut seen_rows = HashSet::new();
1689 let mut seen_cursors = HashSet::new();
1690 let mut expected_total = None;
1691
1692 loop {
1693 let response = self.inner.get_funding_rates_page(¶ms).await?;
1694 let page_len = response.funding_rates.len();
1695
1696 if page_len > PAGE_SIZE as usize {
1697 return Err(format!(
1698 "AX funding-rates page length {page_len} exceeds requested limit {PAGE_SIZE}"
1699 )
1700 .into());
1701 }
1702
1703 if let Some(limit) = response.limit {
1704 if !(0..=PAGE_SIZE).contains(&limit) {
1705 return Err(format!(
1706 "AX funding-rates applied limit must be between 0 and {PAGE_SIZE}, was {limit}"
1707 )
1708 .into());
1709 }
1710
1711 if page_len > limit as usize {
1712 return Err(format!(
1713 "AX funding-rates page length {page_len} exceeds applied limit {limit}"
1714 )
1715 .into());
1716 }
1717 }
1718
1719 if let Some(total_count) = response.total_count {
1720 if total_count < 0 {
1721 return Err(format!(
1722 "AX funding-rates total_count must be non-negative, was {total_count}"
1723 )
1724 .into());
1725 }
1726
1727 if let Some(expected) = expected_total {
1728 if total_count != expected {
1729 return Err(format!(
1730 "AX funding-rates total_count changed during pagination: expected {expected}, was {total_count}"
1731 )
1732 .into());
1733 }
1734 } else {
1735 expected_total = Some(total_count);
1736 }
1737 }
1738
1739 for rate in response.funding_rates {
1740 let identity = (
1741 rate.symbol,
1742 rate.timestamp_ns,
1743 rate.funding_rate,
1744 rate.funding_amount,
1745 rate.benchmark_price,
1746 rate.settlement_price,
1747 );
1748
1749 if !seen_rows.insert(identity) {
1750 return Err(format!(
1751 "AX funding-rates pagination returned an exact duplicate row for {} at {}",
1752 rate.symbol, rate.timestamp_ns
1753 )
1754 .into());
1755 }
1756 funding_rates.push(rate);
1757 }
1758
1759 if let Some(total_count) = expected_total
1760 && funding_rates.len() as i64 > total_count
1761 {
1762 return Err(format!(
1763 "AX funding-rates pagination returned more unique rows ({}) than total_count {total_count}",
1764 funding_rates.len()
1765 )
1766 .into());
1767 }
1768
1769 match response.next_cursor {
1770 Some(next_cursor) => {
1771 if next_cursor.is_empty() {
1772 return Err("AX funding-rates returned an empty next_cursor"
1773 .to_string()
1774 .into());
1775 }
1776
1777 if page_len == 0 {
1778 return Err("AX funding-rates returned an empty page with a next_cursor"
1779 .to_string()
1780 .into());
1781 }
1782
1783 if !seen_cursors.insert(next_cursor.clone()) {
1784 return Err(format!(
1785 "AX funding-rates pagination repeated cursor {next_cursor:?}"
1786 )
1787 .into());
1788 }
1789 params.cursor = Some(next_cursor);
1790 }
1791 None => break,
1792 }
1793 }
1794
1795 if let Some(total_count) = expected_total
1796 && funding_rates.len() as i64 != total_count
1797 {
1798 return Err(format!(
1799 "AX funding-rates pagination returned {} unique rows, expected {total_count}",
1800 funding_rates.len()
1801 )
1802 .into());
1803 }
1804
1805 let ts_init = self.generate_ts_init();
1806 let updates = funding_rates
1807 .iter()
1808 .map(|r| parse_funding_rate(r, instrument_id, ts_init))
1809 .collect::<anyhow::Result<Vec<_>>>()
1810 .map_err(|e| AxHttpError::from(e.to_string()))?;
1811
1812 Ok(updates)
1813 }
1814
1815 pub async fn request_funding_slots(
1825 &self,
1826 instrument_id: InstrumentId,
1827 date: Option<Date>,
1828 ) -> Result<AxFundingSlotsResponse, AxHttpError> {
1829 let symbol = instrument_id.symbol.inner();
1830 let mut params = GetFundingSlotsParams::new(symbol);
1831
1832 if let Some(date) = date {
1833 params.date = Some(date.strftime("%Y-%m-%d").to_string());
1834 }
1835
1836 self.inner.get_funding_slots(¶ms).await
1837 }
1838
1839 pub async fn request_account_state(
1845 &self,
1846 account_id: AccountId,
1847 ) -> anyhow::Result<AccountState> {
1848 let response = self
1849 .inner
1850 .get_balances()
1851 .await
1852 .map_err(|e| anyhow::anyhow!(e))?;
1853
1854 let ts_init = self.generate_ts_init();
1855 parse_account_state(&response, account_id, ts_init, ts_init)
1856 }
1857
1858 pub async fn check_initial_margin(
1864 &self,
1865 request: &PlaceOrderRequest,
1866 ) -> anyhow::Result<Decimal> {
1867 let resp = self
1868 .inner
1869 .check_initial_margin(request)
1870 .await
1871 .map_err(|e| anyhow::anyhow!(e))?;
1872 Ok(resp.im)
1873 }
1874
1875 #[expect(clippy::too_many_arguments)]
1887 pub async fn request_order_status(
1888 &self,
1889 account_id: AccountId,
1890 instrument_id: InstrumentId,
1891 client_order_id: Option<ClientOrderId>,
1892 venue_order_id: Option<VenueOrderId>,
1893 order_side: Option<OrderSide>,
1894 order_type: OrderType,
1895 time_in_force: TimeInForce,
1896 ) -> anyhow::Result<OrderStatusReport> {
1897 let resp = if let Some(ref voi) = venue_order_id {
1898 self.inner.get_order_status_by_id(voi.as_str()).await
1899 } else if let Some(ref coid) = client_order_id {
1900 let cid = client_order_id_to_cid(coid);
1901 self.inner.get_order_status_by_cid(cid).await
1902 } else {
1903 anyhow::bail!("Either venue_order_id or client_order_id must be provided")
1904 }
1905 .map_err(|e| anyhow::anyhow!(e))?;
1906
1907 let detail = resp.status;
1908 let size_precision = self
1909 .get_instrument(&detail.symbol)
1910 .map_or(0, |i| i.size_precision());
1911
1912 let voi = VenueOrderId::new(&detail.order_id);
1913 let order_status = detail.state.into();
1914 let filled = detail.filled_quantity.unwrap_or(0);
1915 let remaining = detail.remaining_quantity.unwrap_or(0);
1916 let quantity = Quantity::new((filled + remaining) as f64, size_precision);
1917 let filled_qty = Quantity::new(filled as f64, size_precision);
1918 let ts_init = self.generate_ts_init();
1919
1920 let resolved_coid = client_order_id.or_else(|| detail.clord_id.map(cid_to_client_order_id));
1921
1922 Ok(OrderStatusReport::new(
1923 account_id,
1924 instrument_id,
1925 resolved_coid,
1926 voi,
1927 order_side,
1928 order_type,
1929 time_in_force,
1930 order_status,
1931 quantity,
1932 filled_qty,
1933 ts_init,
1934 ts_init,
1935 ts_init,
1936 Some(UUID4::new()),
1937 ))
1938 }
1939
1940 pub async fn request_order_status_reports<F>(
1957 &self,
1958 account_id: AccountId,
1959 cid_resolver: Option<F>,
1960 ) -> anyhow::Result<Vec<OrderStatusReport>>
1961 where
1962 F: Fn(u64) -> Option<ClientOrderId>,
1963 {
1964 const PAGE_SIZE: i32 = 100;
1965
1966 let mut orders = Vec::new();
1967 let mut seen_order_ids = HashSet::new();
1968 let mut offset = 0_i64;
1969 let mut expected_total = None;
1970
1971 loop {
1972 let request_offset = i32::try_from(offset)
1973 .context("AX open-orders offset exceeds the documented int32 range")?;
1974 let params = GetOpenOrdersParams {
1975 account_id: None,
1976 limit: Some(PAGE_SIZE),
1977 offset: Some(request_offset),
1978 sort_ts: Some("desc".to_string()),
1979 };
1980 let response = self
1981 .inner
1982 .get_open_orders_page(¶ms)
1983 .await
1984 .map_err(|e| anyhow::anyhow!(e))?;
1985
1986 anyhow::ensure!(
1987 response.total_count >= 0,
1988 "AX open-orders total_count must be non-negative, was {}",
1989 response.total_count
1990 );
1991 anyhow::ensure!(
1992 response.limit >= 0 && response.limit <= PAGE_SIZE,
1993 "AX open-orders applied limit must be between 0 and {PAGE_SIZE}, was {}",
1994 response.limit
1995 );
1996 anyhow::ensure!(
1997 i64::from(response.offset) == offset,
1998 "AX open-orders response offset mismatch: requested {offset}, was {}",
1999 response.offset
2000 );
2001
2002 let total_count = *expected_total.get_or_insert(response.total_count);
2003 anyhow::ensure!(
2004 response.total_count == total_count,
2005 "AX open-orders total_count changed during pagination: expected {total_count}, was {}",
2006 response.total_count
2007 );
2008
2009 let page_len = i64::try_from(response.orders.len())
2010 .context("AX open-orders page length exceeds i64")?;
2011 anyhow::ensure!(
2012 page_len <= i64::from(response.limit),
2013 "AX open-orders page length {page_len} exceeds applied limit {}",
2014 response.limit
2015 );
2016 let next_offset = offset
2017 .checked_add(page_len)
2018 .context("AX open-orders offset overflow")?;
2019 anyhow::ensure!(
2020 next_offset <= total_count,
2021 "AX open-orders page exceeds total_count: next offset {next_offset}, total {total_count}"
2022 );
2023
2024 if total_count == 0 {
2025 anyhow::ensure!(
2026 response.orders.is_empty(),
2027 "AX open-orders returned rows with total_count zero"
2028 );
2029 break;
2030 }
2031
2032 anyhow::ensure!(
2033 !response.orders.is_empty(),
2034 "AX open-orders returned an empty page before offset {offset} reached total {total_count}"
2035 );
2036
2037 for order in response.orders {
2038 anyhow::ensure!(
2039 seen_order_ids.insert(order.oid.clone()),
2040 "AX open-orders pagination returned duplicate order ID {}",
2041 order.oid
2042 );
2043 orders.push(order);
2044 }
2045
2046 if next_offset == total_count {
2047 break;
2048 }
2049
2050 offset = next_offset;
2051 }
2052
2053 anyhow::ensure!(
2054 i64::try_from(orders.len()).context("AX open-orders result length exceeds i64")?
2055 == expected_total.unwrap_or_default(),
2056 "AX open-orders pagination did not return the advertised number of unique orders"
2057 );
2058
2059 let ts_init = self.generate_ts_init();
2060 let mut reports = Vec::with_capacity(orders.len());
2061
2062 for order in &orders {
2063 let instrument = self.resolve_report_instrument(order.s).await?;
2064
2065 match parse_order_status_report(
2066 order,
2067 account_id,
2068 &instrument,
2069 ts_init,
2070 cid_resolver.as_ref(),
2071 ) {
2072 Ok(report) => reports.push(report),
2073 Err(e) => {
2074 log::warn!("Failed to parse order {}: {e}", order.oid);
2075 }
2076 }
2077 }
2078
2079 Ok(reports)
2080 }
2081
2082 pub async fn request_historical_order_status_reports<F>(
2100 &self,
2101 account_id: AccountId,
2102 start: Option<UnixNanos>,
2103 end: Option<UnixNanos>,
2104 cid_resolver: Option<F>,
2105 ) -> anyhow::Result<Vec<OrderStatusReport>>
2106 where
2107 F: Fn(u64) -> Option<ClientOrderId>,
2108 {
2109 const PAGE_SIZE: i32 = 100;
2110
2111 let mut params = GetOrdersParams {
2112 start_timestamp_ns: start.map(|timestamp| timestamp.as_i64()),
2113 end_timestamp_ns: end.map(|timestamp| timestamp.as_i64()),
2114 limit: Some(PAGE_SIZE),
2115 ..Default::default()
2116 };
2117 let mut orders = Vec::new();
2118 let mut seen_cursors = HashSet::new();
2119 let mut seen_order_ids = HashSet::new();
2120
2121 loop {
2122 let response = self
2123 .inner
2124 .get_orders(¶ms)
2125 .await
2126 .map_err(|e| anyhow::anyhow!(e))?;
2127
2128 for order in response.orders {
2129 anyhow::ensure!(
2130 seen_order_ids.insert(order.oid.clone()),
2131 "AX orders pagination returned duplicate order ID {}",
2132 order.oid
2133 );
2134 orders.push(order);
2135 }
2136
2137 match response.next_cursor {
2138 Some(next_cursor) => {
2139 anyhow::ensure!(
2140 seen_cursors.insert(next_cursor.clone()),
2141 "AX orders pagination repeated cursor {next_cursor:?}"
2142 );
2143 params.cursor = Some(next_cursor);
2144 }
2145 None => break,
2146 }
2147 }
2148
2149 let ts_init = self.generate_ts_init();
2150 let mut reports = Vec::with_capacity(orders.len());
2151
2152 for order in &orders {
2153 let instrument = self.resolve_report_instrument(order.s).await?;
2154
2155 match parse_order_detail_status_report(
2156 order,
2157 account_id,
2158 &instrument,
2159 ts_init,
2160 cid_resolver.as_ref(),
2161 ) {
2162 Ok(report) => reports.push(report),
2163 Err(e) => {
2164 log::warn!("Failed to parse order {}: {e}", order.oid);
2165 }
2166 }
2167 }
2168
2169 Ok(reports)
2170 }
2171
2172 pub async fn request_fill_reports(
2185 &self,
2186 account_id: AccountId,
2187 start: Option<UnixNanos>,
2188 end: Option<UnixNanos>,
2189 ) -> anyhow::Result<Vec<FillReport>> {
2190 const PAGE_SIZE: i32 = 100;
2191
2192 let max_span_ns = AX_FILLS_MAX_LOOKBACK_DAYS * 24 * 60 * 60 * 1_000_000_000;
2194 let end_ns = end.map_or_else(|| self.generate_ts_init().as_i64(), |e| e.as_i64());
2195 let floor_ns = end_ns - max_span_ns;
2196 let start_ns = start.map_or(floor_ns, |s| s.as_i64().max(floor_ns));
2197 let mut params = GetFillsParams::new(start_ns, end_ns);
2198 params.limit = Some(PAGE_SIZE);
2199 params.sort_ts = Some("desc".to_string());
2200
2201 let mut fills = Vec::new();
2202 let mut seen_trade_ids = HashSet::new();
2203 let mut seen_cursors = HashSet::new();
2204 let mut expected_total = None;
2205
2206 loop {
2207 let response = self
2208 .inner
2209 .get_fills_page(¶ms)
2210 .await
2211 .map_err(|e| anyhow::anyhow!(e))?;
2212 let page_len = response.fills.len();
2213
2214 anyhow::ensure!(
2215 page_len <= PAGE_SIZE as usize,
2216 "AX fills page length {page_len} exceeds requested limit {PAGE_SIZE}"
2217 );
2218
2219 if let Some(limit) = response.limit {
2220 anyhow::ensure!(
2221 (0..=PAGE_SIZE).contains(&limit),
2222 "AX fills applied limit must be between 0 and {PAGE_SIZE}, was {limit}"
2223 );
2224 anyhow::ensure!(
2225 page_len <= limit as usize,
2226 "AX fills page length {page_len} exceeds applied limit {limit}"
2227 );
2228 }
2229
2230 if let Some(total_count) = response.total_count {
2231 anyhow::ensure!(
2232 total_count >= 0,
2233 "AX fills total_count must be non-negative, was {total_count}"
2234 );
2235
2236 if let Some(expected) = expected_total {
2237 anyhow::ensure!(
2238 total_count == expected,
2239 "AX fills total_count changed during pagination: expected {expected}, was {total_count}"
2240 );
2241 } else {
2242 expected_total = Some(total_count);
2243 }
2244 }
2245
2246 for fill in response.fills {
2247 anyhow::ensure!(
2248 seen_trade_ids.insert(fill.trade_id.clone()),
2249 "AX fills pagination returned duplicate trade ID {}",
2250 fill.trade_id
2251 );
2252 fills.push(fill);
2253 }
2254
2255 if let Some(total_count) = expected_total {
2256 anyhow::ensure!(
2257 fills.len() as i64 <= total_count,
2258 "AX fills pagination returned more unique rows ({}) than total_count {total_count}",
2259 fills.len()
2260 );
2261 }
2262
2263 match response.next_cursor {
2264 Some(next_cursor) => {
2265 anyhow::ensure!(
2266 !next_cursor.is_empty(),
2267 "AX fills returned an empty next_cursor"
2268 );
2269 anyhow::ensure!(
2270 page_len > 0,
2271 "AX fills returned an empty page with a next_cursor"
2272 );
2273 anyhow::ensure!(
2274 seen_cursors.insert(next_cursor.clone()),
2275 "AX fills pagination repeated cursor {next_cursor:?}"
2276 );
2277 params.cursor = Some(next_cursor);
2278 }
2279 None => break,
2280 }
2281 }
2282
2283 if let Some(total_count) = expected_total {
2284 anyhow::ensure!(
2285 fills.len() as i64 == total_count,
2286 "AX fills pagination returned {} unique rows, expected {total_count}",
2287 fills.len()
2288 );
2289 }
2290
2291 let ts_init = self.generate_ts_init();
2292 let mut reports = Vec::with_capacity(fills.len());
2293
2294 for fill in &fills {
2295 let instrument = self.resolve_report_instrument(fill.symbol).await?;
2296 let report = parse_fill_report(fill, account_id, &instrument, ts_init)
2297 .with_context(|| format!("Failed to parse AX fill {}", fill.trade_id))?;
2298 reports.push(report);
2299 }
2300
2301 Ok(reports)
2302 }
2303
2304 pub async fn request_position_reports(
2318 &self,
2319 account_id: AccountId,
2320 ) -> anyhow::Result<Vec<PositionStatusReport>> {
2321 let response = self
2322 .inner
2323 .get_positions()
2324 .await
2325 .map_err(|e| anyhow::anyhow!(e))?;
2326
2327 let ts_init = self.generate_ts_init();
2328 let mut reports = Vec::with_capacity(response.positions.len());
2329
2330 for position in &response.positions {
2331 if position.signed_quantity == 0 {
2333 continue;
2334 }
2335
2336 let instrument = self.resolve_report_instrument(position.symbol).await?;
2337
2338 match parse_position_status_report(position, account_id, &instrument, ts_init) {
2339 Ok(report) => reports.push(report),
2340 Err(e) => {
2341 log::warn!("Failed to parse position for {}: {e}", position.symbol);
2342 }
2343 }
2344 }
2345
2346 Ok(reports)
2347 }
2348
2349 async fn resolve_report_instrument(&self, symbol: Ustr) -> anyhow::Result<InstrumentAny> {
2350 if let Some(instrument) = self.get_instrument(&symbol) {
2351 return Ok(instrument);
2352 }
2353
2354 let instrument = self
2355 .request_instrument(symbol, None, None)
2356 .await
2357 .map_err(|e| {
2358 anyhow::anyhow!("Failed to resolve AX instrument {symbol} via GET /instrument: {e}")
2359 })?;
2360 self.cache_instrument(instrument.clone());
2361 Ok(instrument)
2362 }
2363
2364 pub async fn cancel_all_orders(&self, instrument_id: InstrumentId) -> Result<(), AxHttpError> {
2370 let request = CancelAllOrdersRequest::new().with_symbol(instrument_id.symbol.inner());
2371 self.inner.cancel_all_orders(&request).await?;
2372 Ok(())
2373 }
2374}