1use ahash::AHashMap;
19use jiff::{Timestamp, civil::Date};
20use nautilus_core::string::secret::SecretString;
21use rust_decimal::Decimal;
22use serde::{Deserialize, Serialize};
23use strum::{AsRefStr, Display};
24use ustr::Ustr;
25use zeroize::{Zeroize, ZeroizeOnDrop};
26
27use crate::common::{
28 enums::{
29 AxCandleWidth, AxCategory, AxFundingSlotStatus, AxFundingVariant, AxInstrumentState,
30 AxOrderSide, AxOrderStatus, AxTimeInForce,
31 },
32 parse::{
33 deserialize_decimal_or_zero, deserialize_optional_decimal,
34 deserialize_optional_decimal_from_str, serialize_decimal_as_str,
35 serialize_optional_decimal_as_str,
36 },
37};
38
39fn default_instrument_state() -> AxInstrumentState {
41 AxInstrumentState::Open
42}
43
44#[derive(Clone, Debug, Serialize, Deserialize)]
51#[serde(rename_all = "snake_case")]
52pub struct AxWhoAmIAccount {
53 pub id: Ustr,
55 pub name: Option<String>,
57 pub is_close_only: bool,
59 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
61 pub maker_fee: Option<Decimal>,
62 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
64 pub taker_fee: Option<Decimal>,
65 pub can_list: bool,
67 pub can_read: bool,
69 pub can_set_limits: bool,
71 pub can_reduce_or_close: bool,
73 pub can_trade: bool,
75}
76
77#[derive(Clone, Debug, Serialize, Deserialize)]
82#[serde(rename_all = "snake_case")]
83pub struct AxWhoAmI {
84 pub id: Ustr,
86 pub username: Ustr,
88 pub created_at: Timestamp,
90 pub require_2fa: bool,
92 pub is_onboarded: bool,
94 pub is_frozen: bool,
96 pub is_admin: bool,
98 #[serde(default)]
100 pub accounts: Vec<AxWhoAmIAccount>,
101 #[serde(default)]
103 pub pseudonym: Option<String>,
104 #[serde(default)]
106 pub fiat_deposit_code: Option<String>,
107}
108
109#[derive(Clone, Debug, Serialize, Deserialize)]
114#[serde(rename_all = "snake_case")]
115pub struct AxInstrument {
116 pub symbol: Ustr,
118 #[serde(default)]
120 pub product: Option<Ustr>,
121 #[serde(default = "default_instrument_state")]
123 pub state: AxInstrumentState,
124 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
126 pub multiplier: Decimal,
127 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
129 pub minimum_order_size: Decimal,
130 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
132 pub tick_size: Decimal,
133 pub quote_currency: Ustr,
135 pub funding_settlement_currency: Ustr,
137 pub category: AxCategory,
139 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
141 pub maintenance_margin_pct: Decimal,
142 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
144 pub initial_margin_pct: Decimal,
145 #[serde(default)]
147 pub contract_mark_price: Option<String>,
148 #[serde(default)]
150 pub contract_size: Option<String>,
151 #[serde(default)]
153 pub description: Option<String>,
154 #[serde(default)]
156 pub expiration: Option<Timestamp>,
157 #[serde(default)]
159 pub funding_calendar_schedule: Option<String>,
160 #[serde(default)]
162 pub funding_frequency: Option<String>,
163 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
165 pub funding_rate_cap_lower_pct: Option<Decimal>,
166 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
168 pub funding_rate_cap_upper_pct: Option<Decimal>,
169 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
171 pub price_band_lower_deviation_pct: Option<Decimal>,
172 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
174 pub price_band_upper_deviation_pct: Option<Decimal>,
175 #[serde(default)]
177 pub price_bands: Option<String>,
178 #[serde(default)]
180 pub price_quotation: Option<String>,
181 #[serde(default)]
183 pub underlying_benchmark_price: Option<String>,
184 #[serde(default)]
186 pub price_scale: Option<i64>,
187 #[serde(default)]
189 pub additional_product_specs: Option<serde_json::Map<String, serde_json::Value>>,
190 #[serde(default)]
192 pub delisted_at: Option<Timestamp>,
193 #[serde(default)]
195 pub is_closing: bool,
196 #[serde(default)]
198 pub estimated_funding_supported: bool,
199 #[serde(default)]
201 pub funding_schedule_calendar_description: Option<String>,
202 #[serde(default)]
204 pub funding_schedule_time_description: Option<String>,
205 #[serde(default)]
207 pub funding_schedule: Option<AxFundingSchedule>,
208 #[serde(default)]
210 pub trading_schedule: Option<AxTradingSchedule>,
211}
212
213#[derive(Clone, Debug, Serialize, Deserialize)]
215pub struct AxTimeOfDay {
216 pub hours: u32,
217 pub minutes: u32,
218 #[serde(default)]
219 pub seconds: u32,
220}
221
222#[derive(Clone, Debug, Serialize, Deserialize)]
224pub struct AxFundingTime {
225 pub days_of_week: Vec<u32>,
226 pub time_of_day: AxTimeOfDay,
227}
228
229#[derive(Clone, Debug, Serialize, Deserialize)]
231pub struct AxFundingException {
232 pub date: Date,
233 pub times: Vec<AxTimeOfDay>,
234 pub reason: Option<String>,
235}
236
237#[derive(Clone, Debug, Serialize, Deserialize)]
239pub struct AxFundingSchedule {
240 pub timezone: Ustr,
241 pub times: Vec<AxFundingTime>,
242 pub exceptions: Vec<AxFundingException>,
243}
244
245#[derive(Clone, Debug, Serialize, Deserialize)]
247pub struct AxTradingSegment {
248 pub days_of_week: Vec<u32>,
249 pub time_of_day: AxTimeOfDay,
250 pub duration_seconds: u64,
251 pub state: AxInstrumentState,
252 pub hide_market_data: bool,
253 pub expire_all_orders: bool,
254}
255
256#[derive(Clone, Debug, Serialize, Deserialize)]
258pub struct AxTradingSchedule {
259 pub segments: Vec<AxTradingSegment>,
260}
261
262#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
264#[serde(rename_all = "snake_case")]
265pub enum AxEstimatedFundingStatus {
266 Ready,
267 SettlementPending,
268 Unavailable,
269 #[serde(other)]
270 Unknown,
271}
272
273#[derive(Clone, Debug, Serialize, Deserialize)]
275pub struct AxEstimatedFundingRate {
276 pub symbol: Ustr,
277 pub status: AxEstimatedFundingStatus,
278 pub timestamp: Timestamp,
279 pub reason: Option<String>,
280 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
281 pub funding_rate: Option<Decimal>,
282 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
283 pub funding_amount: Option<Decimal>,
284 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
285 pub benchmark_price: Option<Decimal>,
286 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
287 pub settlement_price: Option<Decimal>,
288}
289
290#[derive(Clone, Debug, Serialize, Deserialize)]
295#[serde(rename_all = "snake_case")]
296pub struct AxInstrumentsResponse {
297 pub instruments: Vec<AxInstrument>,
299}
300
301#[derive(Clone, Debug, Serialize, Deserialize)]
306#[serde(rename_all = "snake_case")]
307pub struct AxBalance {
308 pub symbol: Ustr,
310 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
312 pub amount: Decimal,
313 #[serde(default)]
315 pub account_id: Option<Ustr>,
316}
317
318#[derive(Clone, Debug, Serialize, Deserialize)]
323#[serde(rename_all = "snake_case")]
324pub struct AxBalancesResponse {
325 pub balances: Vec<AxBalance>,
327 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
329 pub usd_borrow: Option<Decimal>,
330}
331
332#[derive(Clone, Debug, Serialize, Deserialize)]
337#[serde(rename_all = "snake_case")]
338pub struct AxPosition {
339 pub account_id: Ustr,
341 pub symbol: Ustr,
343 pub signed_quantity: i64,
345 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
347 pub signed_notional: Decimal,
348 pub timestamp: Timestamp,
350 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
352 pub realized_pnl: Decimal,
353 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
355 pub signed_cost_basis: Option<Decimal>,
356}
357
358#[derive(Clone, Debug, Serialize, Deserialize)]
363#[serde(rename_all = "snake_case")]
364pub struct AxPositionsResponse {
365 pub positions: Vec<AxPosition>,
367}
368
369#[derive(Clone, Debug, Serialize, Deserialize)]
374#[serde(rename_all = "snake_case")]
375pub struct AxTicker {
376 #[serde(rename = "s")]
378 pub symbol: Ustr,
379 #[serde(
381 default,
382 rename = "bp",
383 deserialize_with = "deserialize_optional_decimal"
384 )]
385 pub bid: Option<Decimal>,
386 #[serde(
388 default,
389 rename = "ap",
390 deserialize_with = "deserialize_optional_decimal"
391 )]
392 pub ask: Option<Decimal>,
393 #[serde(
395 default,
396 rename = "p",
397 deserialize_with = "deserialize_optional_decimal"
398 )]
399 pub last: Option<Decimal>,
400 #[serde(
402 default,
403 rename = "m",
404 deserialize_with = "deserialize_optional_decimal"
405 )]
406 pub mark: Option<Decimal>,
407 #[serde(
409 default,
410 rename = "v",
411 deserialize_with = "deserialize_optional_decimal"
412 )]
413 pub volume_24h: Option<Decimal>,
414 #[serde(
416 default,
417 rename = "h",
418 deserialize_with = "deserialize_optional_decimal"
419 )]
420 pub high_24h: Option<Decimal>,
421 #[serde(
423 default,
424 rename = "l",
425 deserialize_with = "deserialize_optional_decimal"
426 )]
427 pub low_24h: Option<Decimal>,
428 #[serde(default)]
430 pub ts: Option<i64>,
431 #[serde(default)]
433 pub tn: Option<i64>,
434 #[serde(default, rename = "q")]
436 pub last_quantity: Option<u64>,
437 #[serde(default, rename = "oi")]
439 pub open_interest: Option<i64>,
440 #[serde(default, rename = "i")]
442 pub instrument_state: Option<AxInstrumentState>,
443 #[serde(
445 default,
446 rename = "pl",
447 deserialize_with = "deserialize_optional_decimal"
448 )]
449 pub price_band_lower: Option<Decimal>,
450 #[serde(
452 default,
453 rename = "pu",
454 deserialize_with = "deserialize_optional_decimal"
455 )]
456 pub price_band_upper: Option<Decimal>,
457 #[serde(
459 default,
460 rename = "lsp",
461 deserialize_with = "deserialize_optional_decimal"
462 )]
463 pub last_settlement_price: Option<Decimal>,
464 #[serde(default, rename = "lst")]
466 pub last_settlement_time: Option<i64>,
467 #[serde(
469 default,
470 rename = "o",
471 deserialize_with = "deserialize_optional_decimal_from_str"
472 )]
473 pub open: Option<Decimal>,
474 #[serde(default, rename = "ef")]
476 pub estimated_funding: Option<AxEstimatedFundingRate>,
477}
478
479#[derive(Clone, Debug, Serialize, Deserialize)]
484#[serde(rename_all = "snake_case")]
485pub struct AxTickersResponse {
486 pub tickers: Vec<AxTicker>,
488 pub total_count: i64,
490 pub limit: i32,
492 pub offset: i32,
494}
495
496#[derive(Clone, Debug, Serialize, Deserialize)]
501#[serde(rename_all = "snake_case")]
502pub struct AxTickerResponse {
503 pub ticker: AxTicker,
505}
506
507#[derive(Debug, Clone, Serialize, Deserialize, Zeroize, ZeroizeOnDrop)]
512#[serde(rename_all = "snake_case")]
513pub struct AxAuthenticateResponse {
514 pub token: SecretString,
516}
517
518impl AxAuthenticateResponse {
519 #[must_use]
521 pub fn into_token(mut self) -> SecretString {
522 std::mem::take(&mut self.token)
523 }
524}
525
526#[derive(Clone, Debug, Serialize, Deserialize)]
531pub struct AxPlaceOrderResponse {
532 pub oid: String,
534}
535
536#[derive(Clone, Debug, Serialize, Deserialize)]
541pub struct AxCancelOrderResponse {
542 pub cxl_rx: bool,
544}
545
546#[derive(Clone, Debug, Serialize, Deserialize)]
551pub struct AxRestTrade {
552 pub ts: i64,
554 pub tn: i64,
556 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
558 pub p: Decimal,
559 pub q: i64,
561 pub s: Ustr,
563 pub d: AxOrderSide,
565}
566
567#[derive(Clone, Debug, Serialize, Deserialize)]
572pub struct AxTradesResponse {
573 pub trades: Vec<AxRestTrade>,
575}
576
577#[derive(Clone, Debug, Serialize, Deserialize)]
582pub struct AxBookLevel {
583 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
585 pub p: Decimal,
586 pub q: i64,
588 #[serde(default)]
590 pub o: Option<Vec<i64>>,
591}
592
593#[derive(Clone, Debug, Serialize, Deserialize)]
598pub struct AxBook {
599 pub ts: i64,
601 pub tn: i64,
603 pub s: Ustr,
605 pub b: Vec<AxBookLevel>,
607 pub a: Vec<AxBookLevel>,
609}
610
611#[derive(Clone, Debug, Serialize, Deserialize)]
616pub struct AxBookResponse {
617 pub book: AxBook,
619}
620
621#[derive(Clone, Debug, Serialize, Deserialize)]
626pub struct AxOrderStatusDetail {
627 pub symbol: Ustr,
629 pub order_id: String,
631 pub state: AxOrderStatus,
633 #[serde(default)]
635 pub clord_id: Option<u64>,
636 #[serde(default)]
638 pub filled_quantity: Option<i64>,
639 #[serde(default)]
641 pub remaining_quantity: Option<i64>,
642 #[serde(default)]
644 pub reject_reason: Option<AxOrderRejectReason>,
645 #[serde(default)]
647 pub reject_message: Option<String>,
648}
649
650#[derive(Clone, Debug, Serialize, Deserialize)]
655pub struct AxOrderStatusQueryResponse {
656 pub status: AxOrderStatusDetail,
658}
659
660#[derive(Clone, Copy, Debug, Display, Eq, PartialEq, Hash, AsRefStr, Serialize, Deserialize)]
665#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
666#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
667pub enum AxOrderRejectReason {
668 CloseOnly,
669 InsufficientMargin,
670 MaxOpenOrdersExceeded,
671 UnknownSymbol,
672 ExchangeClosed,
673 IncorrectQuantity,
674 InvalidPriceIncrement,
675 IncorrectOrderType,
676 PriceOutOfBounds,
677 NoLiquidity,
678 InsufficientCreditLimit,
679 OriginalOrderTerminated,
680 DuplicateClientOrderId,
681 #[serde(other)]
682 Unknown,
683}
684
685impl AxOrderRejectReason {
686 #[must_use]
690 pub fn reason_str(reason: Option<Self>, text: Option<&str>) -> Option<String> {
691 if let Some(known) = reason.filter(|r| !matches!(r, Self::Unknown)) {
692 return Some(known.to_string());
693 }
694
695 text.filter(|text| !text.trim().is_empty())
696 .map(str::to_string)
697 }
698}
699
700#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
702pub enum AxRepriceBehavior {
703 #[serde(rename = "rej")]
704 Reject,
705 #[serde(rename = "bo")]
706 BackOff,
707 #[serde(rename = "tbl")]
708 TopOfBook,
709 #[serde(other)]
710 Unknown,
711}
712
713#[derive(Clone, Debug, Serialize, Deserialize)]
718pub struct AxOrderDetail {
719 pub ts: i64,
721 #[serde(default)]
723 pub tn: i64,
724 pub oid: String,
726 #[serde(default)]
728 pub aid: Option<Ustr>,
729 pub u: Ustr,
731 pub s: Ustr,
733 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
735 pub p: Decimal,
736 pub q: u64,
738 pub xq: u64,
740 pub rq: u64,
742 pub o: AxOrderStatus,
744 pub d: AxOrderSide,
746 pub tif: AxTimeInForce,
748 #[serde(default)]
750 pub cid: Option<u64>,
751 #[serde(default)]
753 pub r: Option<AxOrderRejectReason>,
754 #[serde(default)]
756 pub tag: Option<String>,
757 #[serde(default)]
759 pub txt: Option<String>,
760 #[serde(default)]
762 pub po: bool,
763 #[serde(default)]
765 pub rb: Option<AxRepriceBehavior>,
766}
767
768#[derive(Clone, Debug, Serialize, Deserialize)]
773pub struct AxOrdersResponse {
774 pub orders: Vec<AxOrderDetail>,
776 #[serde(default)]
778 pub total_count: Option<i64>,
779 #[serde(default)]
781 pub limit: Option<i32>,
782 #[serde(default)]
784 pub offset: Option<i32>,
785 #[serde(default)]
787 pub next_cursor: Option<String>,
788}
789
790#[derive(Clone, Debug, Serialize, Deserialize)]
795pub struct AxInitialMarginRequirementResponse {
796 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
798 pub im: Decimal,
799 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
801 pub im_pct: Option<Decimal>,
802 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
804 pub mult: Option<Decimal>,
805 #[serde(default)]
807 pub pos: Option<i64>,
808}
809
810#[derive(Clone, Debug, Serialize, Deserialize)]
815pub struct AxOpenOrder {
816 pub tn: i64,
818 pub ts: i64,
820 pub d: AxOrderSide,
822 pub o: AxOrderStatus,
824 pub oid: String,
826 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
828 pub p: Decimal,
829 pub q: u64,
831 pub rq: u64,
833 pub s: Ustr,
835 pub tif: AxTimeInForce,
837 pub u: Ustr,
839 pub xq: u64,
841 #[serde(default)]
843 pub cid: Option<u64>,
844 #[serde(default)]
846 pub tag: Option<String>,
847 #[serde(default)]
849 pub po: bool,
850 #[serde(default)]
852 pub rb: Option<AxRepriceBehavior>,
853 #[serde(default)]
855 pub aid: Option<Ustr>,
856 #[serde(default)]
858 pub r: Option<AxOrderRejectReason>,
859 #[serde(default)]
861 pub txt: Option<String>,
862}
863
864#[derive(Clone, Debug, Serialize, Deserialize)]
869pub struct AxOpenOrdersResponse {
870 pub orders: Vec<AxOpenOrder>,
872 pub total_count: i64,
874 pub limit: i32,
876 pub offset: i32,
878}
879
880#[derive(Clone, Debug, Serialize, Deserialize)]
885#[serde(rename_all = "snake_case")]
886pub struct AxFill {
887 pub trade_id: String,
889 pub order_id: Option<String>,
891 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
893 pub fee: Decimal,
894 pub is_taker: bool,
896 pub is_block_trade: Option<bool>,
898 pub is_final_settlement: Option<bool>,
900 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
902 pub price: Decimal,
903 pub quantity: u64,
905 pub side: AxOrderSide,
907 pub symbol: Ustr,
909 pub timestamp: Timestamp,
911 pub account_id: Ustr,
913 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
915 pub realized_pnl: Option<Decimal>,
916}
917
918#[derive(Clone, Debug, Serialize, Deserialize)]
923#[serde(rename_all = "snake_case")]
924pub struct AxFillsResponse {
925 pub fills: Vec<AxFill>,
927 #[serde(default)]
929 pub total_count: Option<i64>,
930 #[serde(default)]
932 pub limit: Option<i32>,
933 #[serde(default)]
935 pub next_cursor: Option<String>,
936}
937
938#[derive(Clone, Debug, Serialize, Deserialize)]
943#[serde(rename_all = "snake_case")]
944pub struct AxCandle {
945 pub symbol: Ustr,
947 pub ts: i64,
949 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
951 pub open: Decimal,
952 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
954 pub high: Decimal,
955 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
957 pub low: Decimal,
958 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
960 pub close: Decimal,
961 pub buy_volume: u64,
963 pub sell_volume: u64,
965 pub volume: u64,
967 pub width: AxCandleWidth,
969}
970
971#[derive(Clone, Debug, Serialize, Deserialize)]
976#[serde(rename_all = "snake_case")]
977pub struct AxCandlesResponse {
978 pub candles: Vec<AxCandle>,
980}
981
982#[derive(Clone, Debug, Serialize, Deserialize)]
988#[serde(rename_all = "snake_case")]
989pub struct AxCandleResponse {
990 pub candle: AxCandle,
992}
993
994#[derive(Clone, Debug, Serialize, Deserialize)]
999#[serde(rename_all = "snake_case")]
1000pub struct AxFundingRate {
1001 pub symbol: Ustr,
1003 pub timestamp_ns: i64,
1005 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
1007 pub funding_rate: Decimal,
1008 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
1010 pub funding_amount: Option<Decimal>,
1011 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
1013 pub benchmark_price: Option<Decimal>,
1014 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
1016 pub settlement_price: Decimal,
1017}
1018
1019#[derive(Clone, Debug, Serialize, Deserialize)]
1024#[serde(rename_all = "snake_case")]
1025pub struct AxFundingRatesResponse {
1026 pub funding_rates: Vec<AxFundingRate>,
1028 #[serde(default)]
1030 pub total_count: Option<i64>,
1031 #[serde(default)]
1033 pub limit: Option<i32>,
1034 #[serde(default)]
1036 pub next_cursor: Option<String>,
1037}
1038
1039#[derive(Clone, Debug, Serialize, Deserialize)]
1044#[serde(rename_all = "snake_case")]
1045pub struct AxFundingSlot {
1046 pub index: i32,
1048 pub funding_time: Timestamp,
1050 pub status: AxFundingSlotStatus,
1052 pub capped: bool,
1054 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
1056 pub mark_twap: Option<Decimal>,
1057 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
1059 pub underlying_twap: Option<Decimal>,
1060 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
1062 pub premium_bps: Option<Decimal>,
1063 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
1065 pub funding_rate_bps: Option<Decimal>,
1066 #[serde(default)]
1068 pub reason: Option<String>,
1069}
1070
1071#[derive(Clone, Debug, Serialize, Deserialize)]
1079#[serde(rename_all = "snake_case")]
1080pub struct AxFundingSlotsResponse {
1081 pub symbol: Ustr,
1083 pub date: Date,
1085 pub timezone: Ustr,
1087 pub variant: AxFundingVariant,
1089 pub interval_count: i32,
1091 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
1093 pub cap_bps: Option<Decimal>,
1094 pub slots: Vec<AxFundingSlot>,
1096 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
1098 pub realized_sum_bps: Decimal,
1099 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
1101 pub projected_eod_bps: Decimal,
1102 #[serde(default)]
1104 pub slot_interval_minutes: Option<u32>,
1105}
1106
1107#[derive(Clone, Debug, Serialize, Deserialize)]
1112#[serde(rename_all = "snake_case")]
1113pub struct AxPerSymbolRisk {
1114 pub signed_quantity: i64,
1116 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
1118 pub signed_notional: Decimal,
1119 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
1121 pub average_price: Option<Decimal>,
1122 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
1124 pub liquidation_price: Option<Decimal>,
1125 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
1127 pub initial_margin_required_position: Decimal,
1128 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
1130 pub initial_margin_required_open_orders: Decimal,
1131 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
1133 pub initial_margin_required_total: Decimal,
1134 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
1136 pub maintenance_margin_required: Decimal,
1137 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
1139 pub unrealized_pnl: Decimal,
1140 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
1142 pub signed_cost_basis: Option<Decimal>,
1143 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
1145 pub signed_usd_notional: Option<Decimal>,
1146 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
1148 pub mark_price: Option<Decimal>,
1149}
1150
1151#[derive(Clone, Debug, Serialize, Deserialize)]
1156#[serde(rename_all = "snake_case")]
1157pub struct AxRiskSnapshot {
1158 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
1160 pub balance_usd: Decimal,
1161 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
1163 pub equity: Decimal,
1164 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
1166 pub initial_margin_available: Decimal,
1167 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
1169 pub initial_margin_required_for_open_orders: Decimal,
1170 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
1172 pub initial_margin_required_for_positions: Decimal,
1173 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
1175 pub initial_margin_required_total: Decimal,
1176 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
1178 pub maintenance_margin_available: Decimal,
1179 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
1181 pub maintenance_margin_required: Decimal,
1182 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
1184 pub unrealized_pnl: Decimal,
1185 pub timestamp_ns: Timestamp,
1187 pub account_id: Ustr,
1189 #[serde(default)]
1191 pub per_symbol: AHashMap<Ustr, AxPerSymbolRisk>,
1192}
1193
1194#[derive(Clone, Debug, Serialize, Deserialize)]
1199#[serde(rename_all = "snake_case")]
1200pub struct AxRiskSnapshotResponse {
1201 pub risk_snapshot: AxRiskSnapshot,
1203}
1204
1205#[derive(Clone, Debug, Serialize, Deserialize)]
1210#[serde(rename_all = "snake_case")]
1211pub struct AxTransaction {
1212 pub account_id: Ustr,
1214 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
1216 pub amount: Decimal,
1217 pub event_id: String,
1219 pub symbol: Ustr,
1221 pub timestamp: Timestamp,
1223 pub transaction_type: Ustr,
1225 #[serde(default)]
1227 pub initiated_by_user_id: Option<Ustr>,
1228 #[serde(default)]
1230 pub reference_id: Option<String>,
1231}
1232
1233#[derive(Clone, Debug, Serialize, Deserialize)]
1238#[serde(rename_all = "snake_case")]
1239pub struct AxTransactionsResponse {
1240 pub transactions: Vec<AxTransaction>,
1242 #[serde(default)]
1244 pub total_count: Option<i64>,
1245 #[serde(default)]
1247 pub limit: Option<i32>,
1248 #[serde(default)]
1250 pub next_cursor: Option<String>,
1251}
1252
1253#[derive(Debug, Clone, Serialize, Deserialize, Zeroize)]
1258#[serde(rename_all = "snake_case")]
1259pub struct AuthenticateApiKeyRequest {
1260 pub api_key: SecretString,
1262 pub api_secret: SecretString,
1264 pub expiration_seconds: i32,
1266}
1267
1268impl AuthenticateApiKeyRequest {
1269 #[must_use]
1271 pub fn new(
1272 api_key: impl Into<SecretString>,
1273 api_secret: impl Into<SecretString>,
1274 expiration_seconds: i32,
1275 ) -> Self {
1276 Self {
1277 api_key: api_key.into(),
1278 api_secret: api_secret.into(),
1279 expiration_seconds,
1280 }
1281 }
1282}
1283
1284#[derive(Clone, Debug, Serialize, Deserialize)]
1289pub struct PlaceOrderRequest {
1290 pub d: AxOrderSide,
1292 #[serde(serialize_with = "serialize_decimal_as_str")]
1294 pub p: Decimal,
1295 pub po: bool,
1297 pub q: u64,
1299 pub s: Ustr,
1301 pub tif: AxTimeInForce,
1303 #[serde(skip_serializing_if = "Option::is_none")]
1305 pub tag: Option<String>,
1306}
1307
1308impl PlaceOrderRequest {
1309 #[must_use]
1311 pub fn new(
1312 side: AxOrderSide,
1313 price: Decimal,
1314 quantity: u64,
1315 symbol: Ustr,
1316 time_in_force: AxTimeInForce,
1317 post_only: bool,
1318 ) -> Self {
1319 Self {
1320 d: side,
1321 p: price,
1322 po: post_only,
1323 q: quantity,
1324 s: symbol,
1325 tif: time_in_force,
1326 tag: None,
1327 }
1328 }
1329
1330 #[must_use]
1332 pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
1333 self.tag = Some(tag.into());
1334 self
1335 }
1336}
1337
1338#[derive(Clone, Debug, Serialize, Deserialize)]
1343pub struct PreviewAggressiveLimitOrderRequest {
1344 pub symbol: Ustr,
1346 pub quantity: u64,
1348 pub side: AxOrderSide,
1350}
1351
1352impl PreviewAggressiveLimitOrderRequest {
1353 #[must_use]
1355 pub fn new(symbol: Ustr, quantity: u64, side: AxOrderSide) -> Self {
1356 Self {
1357 symbol,
1358 quantity,
1359 side,
1360 }
1361 }
1362}
1363
1364#[derive(Clone, Debug, Serialize, Deserialize)]
1369pub struct AxPreviewAggressiveLimitOrderResponse {
1370 pub filled_quantity: u64,
1372 pub remaining_quantity: u64,
1374 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
1376 pub limit_price: Option<Decimal>,
1377 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
1379 pub vwap: Option<Decimal>,
1380}
1381
1382#[derive(Clone, Debug, Serialize, Deserialize)]
1387pub struct CancelOrderRequest {
1388 pub oid: String,
1390}
1391
1392impl CancelOrderRequest {
1393 #[must_use]
1395 pub fn new(order_id: impl Into<String>) -> Self {
1396 Self {
1397 oid: order_id.into(),
1398 }
1399 }
1400}
1401
1402#[derive(Clone, Debug, Serialize, Deserialize)]
1410pub struct ReplaceOrderRequest {
1411 pub oid: String,
1413 #[serde(
1415 skip_serializing_if = "Option::is_none",
1416 serialize_with = "serialize_optional_decimal_as_str"
1417 )]
1418 pub p: Option<Decimal>,
1419 #[serde(skip_serializing_if = "Option::is_none")]
1421 pub q: Option<u64>,
1422 #[serde(skip_serializing_if = "Option::is_none")]
1424 pub po: Option<bool>,
1425 #[serde(skip_serializing_if = "Option::is_none")]
1427 pub tif: Option<AxTimeInForce>,
1428}
1429
1430impl ReplaceOrderRequest {
1431 #[must_use]
1435 pub fn new(order_id: impl Into<String>) -> Self {
1436 Self {
1437 oid: order_id.into(),
1438 p: None,
1439 q: None,
1440 po: None,
1441 tif: None,
1442 }
1443 }
1444
1445 #[must_use]
1447 pub fn with_price(mut self, price: Decimal) -> Self {
1448 self.p = Some(price);
1449 self
1450 }
1451
1452 #[must_use]
1454 pub fn with_quantity(mut self, quantity: u64) -> Self {
1455 self.q = Some(quantity);
1456 self
1457 }
1458}
1459
1460#[derive(Clone, Debug, Serialize, Deserialize)]
1465pub struct AxReplaceOrderResponse {
1466 pub oid: String,
1468}
1469
1470#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1475pub struct CancelAllOrdersRequest {
1476 #[serde(skip_serializing_if = "Option::is_none")]
1478 pub account_id: Option<Ustr>,
1479 #[serde(skip_serializing_if = "Option::is_none")]
1481 pub symbol: Option<Ustr>,
1482}
1483
1484impl CancelAllOrdersRequest {
1485 #[must_use]
1487 pub fn new() -> Self {
1488 Self::default()
1489 }
1490
1491 #[must_use]
1493 pub fn with_account_id(mut self, account_id: Ustr) -> Self {
1494 self.account_id = Some(account_id);
1495 self
1496 }
1497
1498 #[must_use]
1500 pub fn with_symbol(mut self, symbol: Ustr) -> Self {
1501 self.symbol = Some(symbol);
1502 self
1503 }
1504}
1505
1506#[derive(Clone, Debug, Serialize, Deserialize)]
1511pub struct AxCancelAllOrdersResponse {}
1512
1513#[cfg(test)]
1514mod tests {
1515 use rstest::rstest;
1516 use rust_decimal_macros::dec;
1517 use serde_json::json;
1518
1519 use super::*;
1520 use crate::websocket::{
1521 messages::{AxOrdersWsFrame, AxWsOrderEvent},
1522 parse::parse_order_message,
1523 };
1524
1525 fn assert_zeroize_on_drop<T: ZeroizeOnDrop>() {}
1526
1527 #[rstest]
1528 fn test_deserialize_authenticate_response() {
1529 let json = include_str!("../../test_data/http_authenticate.json");
1530 let response: AxAuthenticateResponse = serde_json::from_str(json).unwrap();
1531 assert!(response.token.expose_secret().starts_with("test-token"));
1532 }
1533
1534 #[rstest]
1535 fn test_reason_str_prefers_known_reason_over_text() {
1536 assert_eq!(
1537 AxOrderRejectReason::reason_str(
1538 Some(AxOrderRejectReason::PriceOutOfBounds),
1539 Some("order price is above the exchange's maximum price limit"),
1540 ),
1541 Some("PRICE_OUT_OF_BOUNDS".to_string()),
1542 );
1543 }
1544
1545 #[rstest]
1546 fn test_reason_str_falls_back_to_text() {
1547 assert_eq!(
1548 AxOrderRejectReason::reason_str(
1549 Some(AxOrderRejectReason::Unknown),
1550 Some("risk blocked")
1551 ),
1552 Some("risk blocked".to_string()),
1553 );
1554 assert_eq!(
1555 AxOrderRejectReason::reason_str(None, Some("risk blocked")),
1556 Some("risk blocked".to_string()),
1557 );
1558 }
1559
1560 #[rstest]
1561 fn test_reason_str_rejects_blank_text() {
1562 assert_eq!(AxOrderRejectReason::reason_str(None, Some("")), None);
1563 assert_eq!(AxOrderRejectReason::reason_str(None, Some(" ")), None);
1564 assert_eq!(AxOrderRejectReason::reason_str(None, None), None);
1565 assert_eq!(
1566 AxOrderRejectReason::reason_str(Some(AxOrderRejectReason::Unknown), None),
1567 None,
1568 );
1569 }
1570
1571 #[rstest]
1572 fn test_serialize_cancel_all_orders_request() {
1573 let request = CancelAllOrdersRequest::new()
1574 .with_account_id(Ustr::from("account-1"))
1575 .with_symbol(Ustr::from("XAU-PERP"));
1576
1577 let value = serde_json::to_value(request).unwrap();
1578
1579 assert_eq!(value["account_id"], "account-1");
1580 assert_eq!(value["symbol"], "XAU-PERP");
1581 assert!(value.get("execution_venue").is_none());
1582 }
1583
1584 #[rstest]
1585 fn test_deserialize_whoami_response() {
1586 let json = include_str!("../../test_data/http_get_whoami.json");
1587
1588 let response: AxWhoAmI = serde_json::from_str(json).unwrap();
1589
1590 assert_eq!(response.id, "01JBXR-7QK2-0000");
1591 assert_eq!(response.username, "trader@example.com");
1592 assert_eq!(response.pseudonym.as_deref(), Some("quiet-amber-heron"));
1593 assert_eq!(
1594 response.created_at,
1595 "2025-12-18T02:20:42.675817Z".parse::<Timestamp>().unwrap()
1596 );
1597 assert!(!response.require_2fa);
1598 assert!(response.is_onboarded);
1599 assert!(!response.is_frozen);
1600 assert!(!response.is_admin);
1601 assert_eq!(
1602 response.fiat_deposit_code.as_deref(),
1603 Some("01JBXR7QK20000Y")
1604 );
1605 assert_eq!(response.accounts.len(), 1);
1606
1607 let account = &response.accounts[0];
1608
1609 assert_eq!(account.id, "01JBXR-7QK2-0000");
1610 assert_eq!(account.name.as_deref(), Some("trader@example.com"));
1611 assert!(!account.is_close_only);
1612 assert_eq!(account.maker_fee, Some(dec!(0.0002)));
1613 assert_eq!(account.taker_fee, Some(dec!(0.0025)));
1614 assert!(account.can_list);
1615 assert!(account.can_read);
1616 assert!(account.can_set_limits);
1617 assert!(account.can_reduce_or_close);
1618 assert!(account.can_trade);
1619 }
1620
1621 #[rstest]
1622 #[case(json!(""), None)]
1623 #[case(json!(null), None)]
1624 #[case(json!("0"), Some(Decimal::ZERO))]
1625 #[case(json!("0.0002"), Some(dec!(0.0002)))]
1626 fn test_deserialize_whoami_account_fee_distinguishes_absent_from_zero(
1627 #[case] wire_value: serde_json::Value,
1628 #[case] expected: Option<Decimal>,
1629 ) {
1630 let json = json!({
1632 "id": "01JBXR-7QK2-0000",
1633 "name": "trader@example.com",
1634 "is_close_only": false,
1635 "maker_fee": wire_value,
1636 "taker_fee": wire_value,
1637 "can_list": true,
1638 "can_read": true,
1639 "can_set_limits": true,
1640 "can_reduce_or_close": true,
1641 "can_trade": true,
1642 })
1643 .to_string();
1644
1645 let account: AxWhoAmIAccount = serde_json::from_str(&json).unwrap();
1646
1647 assert_eq!(account.maker_fee, expected);
1648 assert_eq!(account.taker_fee, expected);
1649 }
1650
1651 #[rstest]
1652 fn test_deserialize_whoami_account_rejects_malformed_fee() {
1653 let json = json!({
1654 "id": "01JBXR-7QK2-0000",
1655 "name": "trader@example.com",
1656 "is_close_only": false,
1657 "maker_fee": "not-a-decimal",
1658 "taker_fee": "0.0025",
1659 "can_list": true,
1660 "can_read": true,
1661 "can_set_limits": true,
1662 "can_reduce_or_close": true,
1663 "can_trade": true,
1664 })
1665 .to_string();
1666
1667 let error = serde_json::from_str::<AxWhoAmIAccount>(&json).unwrap_err();
1668
1669 assert!(
1670 error.to_string().contains("Invalid decimal"),
1671 "unexpected error: {error}"
1672 );
1673 }
1674
1675 #[rstest]
1676 fn test_deserialize_whoami_response_without_optional_profile_fields() {
1677 let json = json!({
1678 "id": "01JBXR-7QK2-0001",
1679 "username": "sub@example.com",
1680 "created_at": "2025-12-18T02:20:42.675817Z",
1681 "is_onboarded": true,
1682 "is_frozen": false,
1683 "is_admin": false,
1684 "require_2fa": true,
1685 "accounts": [],
1686 })
1687 .to_string();
1688
1689 let response: AxWhoAmI = serde_json::from_str(&json).unwrap();
1690
1691 assert!(response.require_2fa);
1692 assert_eq!(response.pseudonym, None);
1693 assert_eq!(response.fiat_deposit_code, None);
1694 assert!(response.accounts.is_empty());
1695 }
1696
1697 #[rstest]
1698 fn test_deserialize_instruments_response() {
1699 let json = include_str!("../../test_data/http_get_instruments.json");
1700 let response: AxInstrumentsResponse = serde_json::from_str(json).unwrap();
1701 assert_eq!(response.instruments.len(), 3);
1702 assert_eq!(response.instruments[0].symbol, "EURUSD-PERP");
1703 }
1704
1705 #[rstest]
1706 fn test_deserialize_balances_response() {
1707 let json = include_str!("../../test_data/http_get_balances.json");
1708 let response: AxBalancesResponse = serde_json::from_str(json).unwrap();
1709 assert_eq!(response.balances.len(), 3);
1710 assert_eq!(response.balances[0].symbol, "USD");
1711 }
1712
1713 #[rstest]
1714 fn test_deserialize_positions_response() {
1715 let json = include_str!("../../test_data/http_get_positions.json");
1716 let response: AxPositionsResponse = serde_json::from_str(json).unwrap();
1717 assert_eq!(response.positions.len(), 2);
1718 assert_eq!(response.positions[0].symbol, "BTC-PERP");
1719 assert_eq!(response.positions[1].signed_quantity, -5);
1720 }
1721
1722 #[rstest]
1723 fn test_deserialize_tickers_response() {
1724 let json = include_str!("../../test_data/http_get_tickers.json");
1725 let response: AxTickersResponse = serde_json::from_str(json).unwrap();
1726 assert_eq!(response.tickers.len(), 3);
1727 assert_eq!(response.total_count, 3);
1728 assert_eq!(response.limit, 100);
1729 assert_eq!(response.offset, 0);
1730 assert_eq!(response.tickers[0].symbol, "EURUSD-PERP");
1731 assert!(response.tickers[0].bid.is_some());
1732 assert!(response.tickers[2].bid.is_none());
1733 }
1734
1735 #[rstest]
1736 fn test_deserialize_funding_rates_response() {
1737 let json = include_str!("../../test_data/http_get_funding_rates.json");
1738 let response: AxFundingRatesResponse = serde_json::from_str(json).unwrap();
1739 assert_eq!(response.funding_rates.len(), 2);
1740 assert_eq!(response.funding_rates[0].symbol, "JPYUSD-PERP");
1741 }
1742
1743 #[rstest]
1744 fn test_deserialize_funding_slots_response() {
1745 let json = include_str!("../../test_data/http_get_funding_slots.json");
1746 let response: AxFundingSlotsResponse = serde_json::from_str(json).unwrap();
1747 assert_eq!(response.symbol, "EURUSD-PERP");
1748 assert_eq!(response.date, Date::new(2026, 7, 6).unwrap());
1749 assert_eq!(response.timezone, "America/New_York");
1750 assert_eq!(response.variant, AxFundingVariant::IntradayTwap);
1751 assert_eq!(response.interval_count, 4);
1752 assert_eq!(
1753 response.cap_bps.map(|d| d.to_string()),
1754 Some("5.0".to_string())
1755 );
1756 assert_eq!(response.slots.len(), 4);
1757
1758 let first = &response.slots[0];
1759 assert_eq!(first.index, 1);
1760 assert_eq!(first.status, AxFundingSlotStatus::Realized);
1761 assert!(!first.capped);
1762 assert_eq!(
1763 first.funding_rate_bps.map(|d| d.to_string()),
1764 Some("0.0921".to_string())
1765 );
1766 assert!(first.reason.is_none());
1767
1768 let capped = &response.slots[1];
1769 assert!(capped.capped);
1770 assert_eq!(
1771 capped.funding_rate_bps.map(|d| d.to_string()),
1772 Some("5.0000".to_string())
1773 );
1774
1775 let projected = &response.slots[2];
1776 assert_eq!(projected.status, AxFundingSlotStatus::Projected);
1777
1778 let skipped = &response.slots[3];
1779 assert_eq!(skipped.status, AxFundingSlotStatus::Skipped);
1780 assert!(skipped.mark_twap.is_none());
1781 assert!(skipped.funding_rate_bps.is_none());
1782 assert_eq!(skipped.reason.as_deref(), Some("holiday"));
1783
1784 assert_eq!(response.realized_sum_bps.to_string(), "5.0921");
1785 assert_eq!(response.projected_eod_bps.to_string(), "5.1842");
1786 }
1787
1788 #[rstest]
1789 fn test_funding_variant_and_slot_status_deserialization() {
1790 let daily: AxFundingVariant =
1791 serde_json::from_value(serde_json::json!("daily_close")).unwrap();
1792 let twap: AxFundingVariant =
1793 serde_json::from_value(serde_json::json!("intraday_twap")).unwrap();
1794 assert_eq!(daily, AxFundingVariant::DailyClose);
1795 assert_eq!(twap, AxFundingVariant::IntradayTwap);
1796
1797 let statuses = [
1798 ("realized", AxFundingSlotStatus::Realized),
1799 ("projected", AxFundingSlotStatus::Projected),
1800 ("skipped", AxFundingSlotStatus::Skipped),
1801 ("pending", AxFundingSlotStatus::Pending),
1802 ];
1803
1804 for (raw, expected) in statuses {
1805 let parsed: AxFundingSlotStatus =
1806 serde_json::from_value(serde_json::json!(raw)).unwrap();
1807 assert_eq!(parsed, expected);
1808 }
1809 }
1810
1811 #[rstest]
1812 #[case::reject(Some("rej"), Some(AxRepriceBehavior::Reject))]
1813 #[case::back_off(Some("bo"), Some(AxRepriceBehavior::BackOff))]
1814 #[case::top_of_book(Some("tbl"), Some(AxRepriceBehavior::TopOfBook))]
1815 #[case::unknown(Some("future_behavior"), Some(AxRepriceBehavior::Unknown))]
1816 #[case::null(None, None)]
1817 fn test_deserialize_order_reprice_behavior(
1818 #[case] raw: Option<&str>,
1819 #[case] expected: Option<AxRepriceBehavior>,
1820 #[values(true, false)] post_only: bool,
1821 ) {
1822 let mut history: serde_json::Value =
1823 serde_json::from_str(include_str!("../../test_data/http_get_orders.json")).unwrap();
1824 let mut open: serde_json::Value =
1825 serde_json::from_str(include_str!("../../test_data/http_get_open_orders.json"))
1826 .unwrap();
1827
1828 for response in [&mut history, &mut open] {
1829 response["orders"][0]["po"] = serde_json::json!(post_only);
1830 response["orders"][0]["rb"] = serde_json::json!(raw);
1831 }
1832
1833 let history: AxOrdersResponse = serde_json::from_value(history).unwrap();
1834 let open: AxOpenOrdersResponse = serde_json::from_value(open).unwrap();
1835
1836 assert_eq!(history.orders[0].po, post_only);
1837 assert_eq!(history.orders[0].rb, expected);
1838 assert_eq!(open.orders[0].po, post_only);
1839 assert_eq!(open.orders[0].rb, expected);
1840 assert_eq!(history.orders[1].rb, None);
1841 assert_eq!(open.orders[1].rb, None);
1842 }
1843
1844 #[rstest]
1845 fn test_deserialize_open_orders_response() {
1846 let json = include_str!("../../test_data/http_get_open_orders.json");
1847 let response: AxOpenOrdersResponse = serde_json::from_str(json).unwrap();
1848 assert_eq!(response.orders.len(), 2);
1849 assert_eq!(response.orders[0].oid, "O-01ARZ3NDEKTSV4RRFFQ69G5FAV");
1850 assert_eq!(response.orders[0].d, AxOrderSide::Buy);
1851 assert_eq!(response.orders[0].o, AxOrderStatus::Accepted);
1852 assert_eq!(response.orders[1].xq, 300);
1853 assert_eq!(response.total_count, 2);
1854 assert_eq!(response.limit, 100);
1855 assert_eq!(response.offset, 0);
1856 assert!(response.orders[0].po);
1857 assert_eq!(response.orders[0].rb, Some(AxRepriceBehavior::Reject));
1858 assert!(!response.orders[1].po);
1859 assert_eq!(response.orders[1].rb, None);
1860 }
1861
1862 #[rstest]
1863 fn test_deserialize_fills_response() {
1864 let json = include_str!("../../test_data/http_get_fills.json");
1865 let response: AxFillsResponse = serde_json::from_str(json).unwrap();
1866 assert_eq!(response.fills.len(), 2);
1867 assert_eq!(response.fills[0].side, AxOrderSide::Buy);
1868 assert!(response.fills[0].is_taker);
1869 assert!(!response.fills[1].is_taker);
1870 assert_eq!(response.fills[0].is_block_trade, Some(false));
1871 assert_eq!(response.fills[0].is_final_settlement, Some(false));
1872 assert_eq!(response.total_count, Some(2));
1873 assert_eq!(response.limit, Some(100));
1874 assert_eq!(response.next_cursor, None);
1875 }
1876
1877 #[rstest]
1878 fn test_deserialize_candles_response() {
1879 let json = include_str!("../../test_data/http_get_candles.json");
1880 let response: AxCandlesResponse = serde_json::from_str(json).unwrap();
1881 assert_eq!(response.candles.len(), 2);
1882 assert_eq!(response.candles[0].symbol, "EURUSD-PERP");
1883 assert_eq!(response.candles[0].width, AxCandleWidth::Minutes1);
1884 }
1885
1886 #[rstest]
1887 fn test_deserialize_candle_response() {
1888 let json = include_str!("../../test_data/http_get_candle.json");
1889 let response: AxCandleResponse = serde_json::from_str(json).unwrap();
1890 assert_eq!(response.candle.symbol, "EURUSD-PERP");
1891 assert_eq!(response.candle.width, AxCandleWidth::Minutes1);
1892 }
1893
1894 #[rstest]
1895 fn test_deserialize_risk_snapshot_response() {
1896 let json = include_str!("../../test_data/http_get_risk_snapshot.json");
1897 let response: AxRiskSnapshotResponse = serde_json::from_str(json).unwrap();
1898 assert_eq!(
1899 response.risk_snapshot.account_id,
1900 Ustr::from("3c90c3cc-0d44-4b50-8888-8dd25736052a")
1901 );
1902 assert_eq!(response.risk_snapshot.per_symbol.len(), 2);
1903 assert!(
1904 response
1905 .risk_snapshot
1906 .per_symbol
1907 .contains_key(&Ustr::from("EURUSD-PERP"))
1908 );
1909 assert_eq!(
1910 response.risk_snapshot.per_symbol[&Ustr::from("GBPUSD-PERP")].average_price,
1911 None
1912 );
1913 }
1914
1915 #[rstest]
1916 fn test_deserialize_transactions_response() {
1917 let json = include_str!("../../test_data/http_get_transactions.json");
1918 let response: AxTransactionsResponse = serde_json::from_str(json).unwrap();
1919 assert_eq!(response.transactions.len(), 2);
1920 assert_eq!(response.total_count, Some(2));
1921 assert_eq!(response.limit, Some(100));
1922 assert_eq!(response.transactions[0].account_id, Ustr::from("account-1"));
1923 assert_eq!(response.transactions[0].transaction_type, "deposit");
1924 assert!(response.transactions[0].initiated_by_user_id.is_some());
1925 assert!(response.transactions[1].reference_id.is_none());
1926 }
1927
1928 #[rstest]
1929 fn test_deserialize_preview_aggressive_limit_order_response() {
1930 let json = include_str!("../../test_data/http_preview_aggressive_limit_order.json");
1931 let response: AxPreviewAggressiveLimitOrderResponse = serde_json::from_str(json).unwrap();
1932 assert_eq!(response.filled_quantity, 1000);
1933 assert_eq!(response.remaining_quantity, 0);
1934 assert!(response.limit_price.is_some());
1935 assert!(response.vwap.is_some());
1936 }
1937
1938 #[rstest]
1939 fn test_deserialize_place_order_response() {
1940 let json = include_str!("../../test_data/http_place_order.json");
1941 let response: AxPlaceOrderResponse = serde_json::from_str(json).unwrap();
1942 assert_eq!(response.oid, "O-01ARZ3NDEKTSV4RRFFQ69G5FAV");
1943 }
1944
1945 #[rstest]
1946 fn test_deserialize_cancel_order_response() {
1947 let json = include_str!("../../test_data/http_cancel_order.json");
1948 let response: AxCancelOrderResponse = serde_json::from_str(json).unwrap();
1949 assert!(response.cxl_rx);
1950 }
1951
1952 #[rstest]
1953 fn test_deserialize_cancel_all_orders_response() {
1954 let json = include_str!("../../test_data/http_cancel_all_orders.json");
1955 let _response: AxCancelAllOrdersResponse = serde_json::from_str(json).unwrap();
1956 }
1957
1958 #[rstest]
1959 fn test_deserialize_trades_response() {
1960 let json = include_str!("../../test_data/http_get_trades.json");
1961 let response: AxTradesResponse = serde_json::from_str(json).unwrap();
1962 assert_eq!(response.trades.len(), 2);
1963 assert_eq!(response.trades[0].s, "EURUSD-PERP");
1964 assert_eq!(response.trades[0].d, AxOrderSide::Buy);
1965 assert_eq!(response.trades[0].q, 100);
1966 assert_eq!(response.trades[1].d, AxOrderSide::Sell);
1967 }
1968
1969 #[rstest]
1970 fn test_deserialize_book_response() {
1971 let json = include_str!("../../test_data/http_get_book.json");
1972 let response: AxBookResponse = serde_json::from_str(json).unwrap();
1973 assert_eq!(response.book.s, "EURUSD-PERP");
1974 assert_eq!(response.book.b.len(), 3);
1975 assert_eq!(response.book.a.len(), 3);
1976 assert_eq!(response.book.b[0].q, 500);
1977 assert_eq!(response.book.a[0].q, 400);
1978 }
1979
1980 #[rstest]
1981 fn test_deserialize_order_status_query_response() {
1982 let json = include_str!("../../test_data/http_get_order_status.json");
1983 let response: AxOrderStatusQueryResponse = serde_json::from_str(json).unwrap();
1984 assert_eq!(response.status.symbol, "EURUSD-PERP");
1985 assert_eq!(response.status.order_id, "O-01ARZ3NDEKTSV4RRFFQ69G5FAV");
1986 assert_eq!(response.status.state, AxOrderStatus::PartiallyFilled);
1987 assert_eq!(response.status.clord_id, Some(12345));
1988 assert_eq!(response.status.filled_quantity, Some(300));
1989 assert_eq!(response.status.remaining_quantity, Some(700));
1990 assert_eq!(response.status.reject_reason, None);
1991 assert_eq!(response.status.reject_message, None);
1992 }
1993
1994 #[rstest]
1995 fn test_deserialize_orders_response() {
1996 let json = include_str!("../../test_data/http_get_orders.json");
1997 let response: AxOrdersResponse = serde_json::from_str(json).unwrap();
1998 assert_eq!(response.orders.len(), 2);
1999 assert_eq!(response.total_count, Some(2));
2000 assert_eq!(response.limit, Some(100));
2001 assert_eq!(response.next_cursor, None);
2002 assert_eq!(response.orders[0].aid.as_deref(), Some("account-1"));
2003 assert_eq!(response.orders[0].o, AxOrderStatus::PartiallyFilled);
2004 assert_eq!(response.orders[0].xq, 300);
2005 assert_eq!(response.orders[1].o, AxOrderStatus::Filled);
2006 assert_eq!(response.orders[1].d, AxOrderSide::Sell);
2007 assert!(response.orders[0].po);
2008 assert_eq!(response.orders[0].rb, Some(AxRepriceBehavior::Reject));
2009 assert!(!response.orders[1].po);
2010 assert_eq!(response.orders[1].rb, None);
2011 }
2012
2013 #[rstest]
2014 fn test_deserialize_initial_margin_requirement_response() {
2015 let json = include_str!("../../test_data/http_initial_margin_requirement.json");
2016 let response: AxInitialMarginRequirementResponse = serde_json::from_str(json).unwrap();
2017 assert_eq!(response.im, Decimal::new(125050, 2));
2018 }
2019
2020 #[rstest]
2021 fn test_deserialize_replace_order_response() {
2022 let json = include_str!("../../test_data/http_replace_order.json");
2023 let response: AxReplaceOrderResponse = serde_json::from_str(json).unwrap();
2024 assert_eq!(response.oid, "O-01ARZ3NDEKTSV4RRFFQ69G5NEW");
2025 }
2026
2027 #[rstest]
2028 fn test_replace_order_request_serialization() {
2029 let request = ReplaceOrderRequest::new("O-01ARZ3NDEKTSV4RRFFQ69G5FAV")
2030 .with_price(Decimal::new(10550, 4))
2031 .with_quantity(200);
2032
2033 let json = serde_json::to_value(&request).unwrap();
2034 assert_eq!(json["oid"], "O-01ARZ3NDEKTSV4RRFFQ69G5FAV");
2035 assert_eq!(json["p"], "1.0550");
2036 assert_eq!(json["q"], 200);
2037 assert!(json.get("po").is_none());
2038 assert!(json.get("tif").is_none());
2039 assert!(json.get("trigger_price").is_none());
2040 }
2041
2042 #[rstest]
2043 fn test_replace_order_request_minimal() {
2044 let request = ReplaceOrderRequest::new("O-TEST");
2045 let json = serde_json::to_value(&request).unwrap();
2046 assert_eq!(json["oid"], "O-TEST");
2047 assert!(json.get("p").is_none());
2048 assert!(json.get("q").is_none());
2049 }
2050
2051 #[rstest]
2052 fn test_authenticate_request_serializes_and_redacts_debug() {
2053 let request = AuthenticateApiKeyRequest::new("api-key-token", "api-secret-value", 3600);
2054
2055 let json = serde_json::to_value(&request).unwrap();
2056 let formatted = format!("{request:?}");
2057
2058 assert_eq!(json["api_key"], "api-key-token");
2059 assert_eq!(json["api_secret"], "api-secret-value");
2060 assert_eq!(json["expiration_seconds"], 3600);
2061 assert_eq!(
2062 formatted,
2063 "AuthenticateApiKeyRequest { api_key: <redacted>, api_secret: <redacted>, expiration_seconds: 3600 }",
2064 );
2065 assert!(!formatted.contains("api-key-token"));
2066 assert!(!formatted.contains("api-secret-value"));
2067 }
2068
2069 #[rstest]
2070 fn test_authenticate_response_redacts_debug() {
2071 assert_zeroize_on_drop::<AxAuthenticateResponse>();
2072
2073 let response = AxAuthenticateResponse {
2074 token: SecretString::from("session-token-value"),
2075 };
2076
2077 let formatted = format!("{response:?}");
2078
2079 assert_eq!(formatted, "AxAuthenticateResponse { token: <redacted> }");
2080 assert!(!formatted.contains("session-token-value"));
2081 }
2082
2083 #[rstest]
2084 #[case(false)]
2085 #[case(true)]
2086 fn test_optional_account_names_and_funding_prices(#[case] explicit_null: bool) {
2087 let mut who: serde_json::Value =
2088 serde_json::from_str(include_str!("../../test_data/http_get_whoami.json")).unwrap();
2089 let mut funding: serde_json::Value =
2090 serde_json::from_str(include_str!("../../test_data/http_get_funding_rates.json"))
2091 .unwrap();
2092
2093 for (value, field) in [
2094 (&mut who["accounts"][0], "name"),
2095 (&mut funding["funding_rates"][0], "funding_amount"),
2096 ] {
2097 if explicit_null {
2098 value[field] = serde_json::Value::Null;
2099 } else {
2100 value.as_object_mut().unwrap().remove(field);
2101 }
2102 }
2103
2104 if explicit_null {
2105 funding["funding_rates"][0]["benchmark_price"] = serde_json::Value::Null;
2106 } else {
2107 funding["funding_rates"][0]
2108 .as_object_mut()
2109 .unwrap()
2110 .remove("benchmark_price");
2111 }
2112
2113 let account: AxWhoAmI = serde_json::from_value(who.clone()).unwrap();
2114 who.as_object_mut().unwrap().remove("accounts");
2115 let without_accounts: AxWhoAmI = serde_json::from_value(who).unwrap();
2116 let rates: AxFundingRatesResponse = serde_json::from_value(funding).unwrap();
2117
2118 assert_eq!(account.accounts[0].name, None);
2119 assert!(without_accounts.accounts.is_empty());
2120 assert_eq!(rates.funding_rates[0].funding_amount, None);
2121 assert_eq!(rates.funding_rates[0].benchmark_price, None);
2122 }
2123
2124 #[rstest]
2125 #[case(
2126 "ORIGINAL_ORDER_TERMINATED",
2127 AxOrderRejectReason::OriginalOrderTerminated
2128 )]
2129 #[case(
2130 "DUPLICATE_CLIENT_ORDER_ID",
2131 AxOrderRejectReason::DuplicateClientOrderId
2132 )]
2133 fn test_additional_reject_reasons(#[case] wire: &str, #[case] expected: AxOrderRejectReason) {
2134 let value = serde_json::Value::String(wire.to_owned());
2135 let reason: AxOrderRejectReason = serde_json::from_value(value.clone()).unwrap();
2136 assert_eq!(reason, expected);
2137 assert_eq!(serde_json::to_value(reason).unwrap(), value);
2138 assert_eq!(
2139 AxOrderRejectReason::reason_str(Some(reason), None),
2140 Some(wire.to_owned())
2141 );
2142 }
2143
2144 #[rstest]
2145 #[case(include_str!("../../test_data/captured/whoami.json"), captured_response::<AxWhoAmI>)]
2146 #[case(include_str!("../../test_data/captured/balances.json"), captured_response::<AxBalancesResponse>)]
2147 #[case(include_str!("../../test_data/captured/positions.json"), captured_response::<AxPositionsResponse>)]
2148 #[case(include_str!("../../test_data/captured/risk.json"), captured_response::<AxRiskSnapshotResponse>)]
2149 #[case(include_str!("../../test_data/captured/instruments.json"), captured_response::<AxInstrumentsResponse>)]
2150 #[case(include_str!("../../test_data/captured/ticker.json"), captured_response::<AxTickerResponse>)]
2151 #[case(include_str!("../../test_data/captured/tickers.json"), captured_response::<AxTickersResponse>)]
2152 #[case(include_str!("../../test_data/captured/book.json"), captured_response::<AxBookResponse>)]
2153 #[case(include_str!("../../test_data/captured/trades.json"), captured_response::<AxTradesResponse>)]
2154 #[case(include_str!("../../test_data/captured/candles.json"), captured_response::<AxCandlesResponse>)]
2155 #[case(include_str!("../../test_data/captured/current-candle.json"), captured_response::<AxCandleResponse>)]
2156 #[case(include_str!("../../test_data/captured/last-candle.json"), captured_response::<AxCandleResponse>)]
2157 #[case(include_str!("../../test_data/captured/funding-rates.json"), captured_response::<AxFundingRatesResponse>)]
2158 #[case(include_str!("../../test_data/captured/funding-slots.json"), captured_response::<AxFundingSlotsResponse>)]
2159 #[case(include_str!("../../test_data/captured/transactions.json"), captured_response::<AxTransactionsResponse>)]
2160 #[case(include_str!("../../test_data/captured/fills.json"), captured_response::<AxFillsResponse>)]
2161 #[case(include_str!("../../test_data/captured/orders.json"), captured_response::<AxOrdersResponse>)]
2162 #[case(include_str!("../../test_data/captured/open-orders.json"), captured_response::<AxOpenOrdersResponse>)]
2163 #[case(include_str!("../../test_data/captured/open-reject.json"), captured_response::<AxOpenOrdersResponse>)]
2164 #[case(include_str!("../../test_data/captured/open-backoff.json"), captured_response::<AxOpenOrdersResponse>)]
2165 #[case(include_str!("../../test_data/captured/open-top.json"), captured_response::<AxOpenOrdersResponse>)]
2166 #[case(include_str!("../../test_data/captured/open-default.json"), captured_response::<AxOpenOrdersResponse>)]
2167 #[case(include_str!("../../test_data/captured/open-inactive.json"), captured_response::<AxOpenOrdersResponse>)]
2168 #[case(include_str!("../../test_data/captured/fills-live.json"), captured_response::<AxFillsResponse>)]
2169 #[case(include_str!("../../test_data/captured/orders-live.json"), captured_response::<AxOrdersResponse>)]
2170 #[case(include_str!("../../test_data/captured/positions-long.json"), captured_response::<AxPositionsResponse>)]
2171 #[case(include_str!("../../test_data/captured/positions-short.json"), captured_response::<AxPositionsResponse>)]
2172 #[case(include_str!("../../test_data/captured/initial-margin.json"), captured_response::<AxInitialMarginRequirementResponse>)]
2173 #[case(include_str!("../../test_data/captured/preview.json"), captured_response::<AxPreviewAggressiveLimitOrderResponse>)]
2174 #[case(include_str!("../../test_data/captured/order-status-canceled.json"), captured_response::<AxOrderStatusQueryResponse>)]
2175 #[case(include_str!("../../test_data/captured/order-status-filled.json"), captured_response::<AxOrderStatusQueryResponse>)]
2176 #[case(include_str!("../../test_data/captured/order-status-replaced.json"), captured_response::<AxOrderStatusQueryResponse>)]
2177 #[case(include_str!("../../test_data/captured/ws-orders-acknowledged.json"), captured_order_event)]
2178 #[case(include_str!("../../test_data/captured/ws-orders-replaced.json"), captured_order_event)]
2179 #[case(include_str!("../../test_data/captured/ws-orders-canceled.json"), captured_order_event)]
2180 #[case(include_str!("../../test_data/captured/ws-orders-expired.json"), captured_order_event)]
2181 #[case(include_str!("../../test_data/captured/ws-orders-rejected.json"), captured_order_event)]
2182 #[case(include_str!("../../test_data/captured/ws-orders-filled-b.json"), captured_order_event)]
2183 #[case(include_str!("../../test_data/captured/ws-orders-filled-s.json"), captured_order_event)]
2184 fn test_captured_response_preserves_every_field(
2185 #[case] raw: &str,
2186 #[case] decode: fn(&serde_json::Value) -> serde_json::Value,
2187 ) {
2188 let mut expected: serde_json::Value = serde_json::from_str(raw).unwrap();
2189 let actual = decode(&expected);
2190 if expected["t"] == "c" && expected["xr"] == "" {
2191 expected["xr"] = "UNKNOWN".into();
2192 }
2193
2194 assert_captured_fields(&expected, &actual, "$");
2195 }
2196
2197 fn captured_response<T: serde::de::DeserializeOwned + Serialize>(
2198 raw: &serde_json::Value,
2199 ) -> serde_json::Value {
2200 serde_json::to_value(serde_json::from_value::<T>(raw.clone()).unwrap()).unwrap()
2201 }
2202
2203 fn captured_order_event(raw: &serde_json::Value) -> serde_json::Value {
2204 let AxOrdersWsFrame::Event(event) = parse_order_message(&raw.to_string()).unwrap() else {
2205 panic!("expected order event");
2206 };
2207
2208 let (tag, actual) = match *event {
2209 AxWsOrderEvent::Acknowledged(message) => ("n", serde_json::to_value(message)),
2210 AxWsOrderEvent::Replaced(message) => ("r", serde_json::to_value(message)),
2211 AxWsOrderEvent::Canceled(message) => ("c", serde_json::to_value(message)),
2212 AxWsOrderEvent::Expired(message) => ("x", serde_json::to_value(message)),
2213 AxWsOrderEvent::Rejected(message) => ("j", serde_json::to_value(message)),
2214 AxWsOrderEvent::Filled(message) => ("f", serde_json::to_value(message)),
2215 AxWsOrderEvent::PartiallyFilled(message) => ("p", serde_json::to_value(message)),
2216 AxWsOrderEvent::DoneForDay(message) => ("d", serde_json::to_value(message)),
2217 AxWsOrderEvent::CancelRejected(message) => ("e", serde_json::to_value(message)),
2218 AxWsOrderEvent::Heartbeat => panic!("expected lifecycle event"),
2219 };
2220
2221 let mut actual = actual.unwrap();
2222 actual["t"] = tag.into();
2223 actual
2224 }
2225
2226 fn assert_captured_fields(
2227 expected: &serde_json::Value,
2228 actual: &serde_json::Value,
2229 path: &str,
2230 ) {
2231 match expected {
2232 serde_json::Value::Object(fields) => {
2233 for (key, value) in fields {
2234 let child = actual
2235 .get(key)
2236 .unwrap_or_else(|| panic!("missing {path}.{key}"));
2237 assert_captured_fields(value, child, &format!("{path}.{key}"));
2238 }
2239 }
2240 serde_json::Value::Array(values) => {
2241 let children = actual.as_array().unwrap();
2242 assert_eq!(children.len(), values.len(), "{path}");
2243
2244 for (i, (value, child)) in values.iter().zip(children).enumerate() {
2245 assert_captured_fields(value, child, &format!("{path}[{i}]"));
2246 }
2247 }
2248 serde_json::Value::String(value) if expected != actual => {
2249 let text = actual
2250 .as_str()
2251 .unwrap_or_else(|| panic!("expected string at {path}: {actual}"));
2252
2253 if let Ok(decimal) = value.parse::<rust_decimal::Decimal>() {
2254 assert_eq!(
2255 text.parse::<rust_decimal::Decimal>().unwrap(),
2256 decimal,
2257 "{path}"
2258 );
2259 } else if let Ok(timestamp) = value.parse::<Timestamp>() {
2260 assert_eq!(text.parse::<Timestamp>().unwrap(), timestamp, "{path}");
2261 } else {
2262 assert_eq!(expected, actual, "{path}");
2263 }
2264 }
2265 serde_json::Value::Number(value) if actual.is_string() => {
2266 assert_eq!(
2267 value.to_string().parse::<rust_decimal::Decimal>().unwrap(),
2268 actual
2269 .as_str()
2270 .unwrap()
2271 .parse::<rust_decimal::Decimal>()
2272 .unwrap(),
2273 "{path}"
2274 );
2275 }
2276 _ => assert_eq!(expected, actual, "{path}"),
2277 }
2278 }
2279}