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: String,
55 pub name: 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: String,
86 pub username: String,
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 pub accounts: Vec<AxWhoAmIAccount>,
100 #[serde(default)]
102 pub pseudonym: Option<String>,
103 #[serde(default)]
105 pub fiat_deposit_code: Option<String>,
106}
107
108#[derive(Clone, Debug, Serialize, Deserialize)]
113#[serde(rename_all = "snake_case")]
114pub struct AxInstrument {
115 pub symbol: Ustr,
117 #[serde(default)]
119 pub product: Option<Ustr>,
120 #[serde(default = "default_instrument_state")]
122 pub state: AxInstrumentState,
123 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
125 pub multiplier: Decimal,
126 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
128 pub minimum_order_size: Decimal,
129 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
131 pub tick_size: Decimal,
132 pub quote_currency: Ustr,
134 pub funding_settlement_currency: Ustr,
136 pub category: AxCategory,
138 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
140 pub maintenance_margin_pct: Decimal,
141 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
143 pub initial_margin_pct: Decimal,
144 #[serde(default)]
146 pub contract_mark_price: Option<String>,
147 #[serde(default)]
149 pub contract_size: Option<String>,
150 #[serde(default)]
152 pub description: Option<String>,
153 #[serde(default)]
155 pub expiration: Option<Timestamp>,
156 #[serde(default)]
158 pub funding_calendar_schedule: Option<String>,
159 #[serde(default)]
161 pub funding_frequency: Option<String>,
162 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
164 pub funding_rate_cap_lower_pct: Option<Decimal>,
165 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
167 pub funding_rate_cap_upper_pct: Option<Decimal>,
168 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
170 pub price_band_lower_deviation_pct: Option<Decimal>,
171 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
173 pub price_band_upper_deviation_pct: Option<Decimal>,
174 #[serde(default)]
176 pub price_bands: Option<String>,
177 #[serde(default)]
179 pub price_quotation: Option<String>,
180 #[serde(default)]
182 pub underlying_benchmark_price: Option<String>,
183}
184
185#[derive(Clone, Debug, Serialize, Deserialize)]
190#[serde(rename_all = "snake_case")]
191pub struct AxInstrumentsResponse {
192 pub instruments: Vec<AxInstrument>,
194}
195
196#[derive(Clone, Debug, Serialize, Deserialize)]
201#[serde(rename_all = "snake_case")]
202pub struct AxBalance {
203 pub symbol: Ustr,
205 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
207 pub amount: Decimal,
208}
209
210#[derive(Clone, Debug, Serialize, Deserialize)]
215#[serde(rename_all = "snake_case")]
216pub struct AxBalancesResponse {
217 pub balances: Vec<AxBalance>,
219}
220
221#[derive(Clone, Debug, Serialize, Deserialize)]
226#[serde(rename_all = "snake_case")]
227pub struct AxPosition {
228 pub account_id: Ustr,
230 pub symbol: Ustr,
232 pub signed_quantity: i64,
234 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
236 pub signed_notional: Decimal,
237 pub timestamp: Timestamp,
239 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
241 pub realized_pnl: Decimal,
242}
243
244#[derive(Clone, Debug, Serialize, Deserialize)]
249#[serde(rename_all = "snake_case")]
250pub struct AxPositionsResponse {
251 pub positions: Vec<AxPosition>,
253}
254
255#[derive(Clone, Debug, Serialize, Deserialize)]
260#[serde(rename_all = "snake_case")]
261pub struct AxTicker {
262 #[serde(rename = "s")]
264 pub symbol: Ustr,
265 #[serde(
267 default,
268 rename = "bp",
269 deserialize_with = "deserialize_optional_decimal"
270 )]
271 pub bid: Option<Decimal>,
272 #[serde(
274 default,
275 rename = "ap",
276 deserialize_with = "deserialize_optional_decimal"
277 )]
278 pub ask: Option<Decimal>,
279 #[serde(
281 default,
282 rename = "p",
283 deserialize_with = "deserialize_optional_decimal"
284 )]
285 pub last: Option<Decimal>,
286 #[serde(
288 default,
289 rename = "m",
290 deserialize_with = "deserialize_optional_decimal"
291 )]
292 pub mark: Option<Decimal>,
293 #[serde(
295 default,
296 rename = "v",
297 deserialize_with = "deserialize_optional_decimal"
298 )]
299 pub volume_24h: Option<Decimal>,
300 #[serde(
302 default,
303 rename = "h",
304 deserialize_with = "deserialize_optional_decimal"
305 )]
306 pub high_24h: Option<Decimal>,
307 #[serde(
309 default,
310 rename = "l",
311 deserialize_with = "deserialize_optional_decimal"
312 )]
313 pub low_24h: Option<Decimal>,
314 #[serde(default)]
316 pub ts: Option<i64>,
317 #[serde(default)]
319 pub tn: Option<i64>,
320 #[serde(default, rename = "q")]
322 pub last_quantity: Option<u64>,
323 #[serde(default, rename = "oi")]
325 pub open_interest: Option<i64>,
326 #[serde(default, rename = "i")]
328 pub instrument_state: Option<AxInstrumentState>,
329 #[serde(
331 default,
332 rename = "pl",
333 deserialize_with = "deserialize_optional_decimal"
334 )]
335 pub price_band_lower: Option<Decimal>,
336 #[serde(
338 default,
339 rename = "pu",
340 deserialize_with = "deserialize_optional_decimal"
341 )]
342 pub price_band_upper: Option<Decimal>,
343 #[serde(
345 default,
346 rename = "lsp",
347 deserialize_with = "deserialize_optional_decimal"
348 )]
349 pub last_settlement_price: Option<Decimal>,
350 #[serde(default, rename = "lst")]
352 pub last_settlement_time: Option<i64>,
353}
354
355#[derive(Clone, Debug, Serialize, Deserialize)]
360#[serde(rename_all = "snake_case")]
361pub struct AxTickersResponse {
362 pub tickers: Vec<AxTicker>,
364 pub total_count: i64,
366 pub limit: i32,
368 pub offset: i32,
370}
371
372#[derive(Clone, Debug, Serialize, Deserialize)]
377#[serde(rename_all = "snake_case")]
378pub struct AxTickerResponse {
379 pub ticker: AxTicker,
381}
382
383#[derive(Debug, Clone, Serialize, Deserialize, Zeroize, ZeroizeOnDrop)]
388#[serde(rename_all = "snake_case")]
389pub struct AxAuthenticateResponse {
390 pub token: SecretString,
392}
393
394impl AxAuthenticateResponse {
395 #[must_use]
397 pub fn into_token(mut self) -> SecretString {
398 std::mem::take(&mut self.token)
399 }
400}
401
402#[derive(Clone, Debug, Serialize, Deserialize)]
407pub struct AxPlaceOrderResponse {
408 pub oid: String,
410}
411
412#[derive(Clone, Debug, Serialize, Deserialize)]
417pub struct AxCancelOrderResponse {
418 pub cxl_rx: bool,
420}
421
422#[derive(Clone, Debug, Serialize, Deserialize)]
427pub struct AxRestTrade {
428 pub ts: i64,
430 pub tn: i64,
432 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
434 pub p: Decimal,
435 pub q: i64,
437 pub s: Ustr,
439 pub d: AxOrderSide,
441}
442
443#[derive(Clone, Debug, Serialize, Deserialize)]
448pub struct AxTradesResponse {
449 pub trades: Vec<AxRestTrade>,
451}
452
453#[derive(Clone, Debug, Serialize, Deserialize)]
458pub struct AxBookLevel {
459 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
461 pub p: Decimal,
462 pub q: i64,
464 #[serde(default)]
466 pub o: Option<Vec<i64>>,
467}
468
469#[derive(Clone, Debug, Serialize, Deserialize)]
474pub struct AxBook {
475 pub ts: i64,
477 pub tn: i64,
479 pub s: Ustr,
481 pub b: Vec<AxBookLevel>,
483 pub a: Vec<AxBookLevel>,
485}
486
487#[derive(Clone, Debug, Serialize, Deserialize)]
492pub struct AxBookResponse {
493 pub book: AxBook,
495}
496
497#[derive(Clone, Debug, Serialize, Deserialize)]
502pub struct AxOrderStatusDetail {
503 pub symbol: Ustr,
505 pub order_id: String,
507 pub state: AxOrderStatus,
509 #[serde(default)]
511 pub clord_id: Option<u64>,
512 #[serde(default)]
514 pub filled_quantity: Option<i64>,
515 #[serde(default)]
517 pub remaining_quantity: Option<i64>,
518 #[serde(default)]
520 pub reject_reason: Option<AxOrderRejectReason>,
521 #[serde(default)]
523 pub reject_message: Option<String>,
524}
525
526#[derive(Clone, Debug, Serialize, Deserialize)]
531pub struct AxOrderStatusQueryResponse {
532 pub status: AxOrderStatusDetail,
534}
535
536#[derive(Clone, Copy, Debug, Display, Eq, PartialEq, Hash, AsRefStr, Serialize, Deserialize)]
541#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
542#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
543pub enum AxOrderRejectReason {
544 CloseOnly,
545 InsufficientMargin,
546 MaxOpenOrdersExceeded,
547 UnknownSymbol,
548 ExchangeClosed,
549 IncorrectQuantity,
550 InvalidPriceIncrement,
551 IncorrectOrderType,
552 PriceOutOfBounds,
553 NoLiquidity,
554 InsufficientCreditLimit,
555 #[serde(other)]
556 Unknown,
557}
558
559#[derive(Clone, Debug, Serialize, Deserialize)]
564pub struct AxOrderDetail {
565 pub ts: i64,
567 #[serde(default)]
569 pub tn: i64,
570 pub oid: String,
572 #[serde(default)]
574 pub aid: Option<String>,
575 pub u: String,
577 pub s: Ustr,
579 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
581 pub p: Decimal,
582 pub q: u64,
584 pub xq: u64,
586 pub rq: u64,
588 pub o: AxOrderStatus,
590 pub d: AxOrderSide,
592 pub tif: AxTimeInForce,
594 #[serde(default)]
596 pub cid: Option<u64>,
597 #[serde(default)]
599 pub r: Option<AxOrderRejectReason>,
600 #[serde(default)]
602 pub tag: Option<String>,
603 #[serde(default)]
605 pub txt: Option<String>,
606 #[serde(default)]
608 pub po: bool,
609}
610
611#[derive(Clone, Debug, Serialize, Deserialize)]
616pub struct AxOrdersResponse {
617 pub orders: Vec<AxOrderDetail>,
619 #[serde(default)]
621 pub total_count: Option<i64>,
622 #[serde(default)]
624 pub limit: Option<i32>,
625 #[serde(default)]
627 pub offset: Option<i32>,
628 #[serde(default)]
630 pub next_cursor: Option<String>,
631}
632
633#[derive(Clone, Debug, Serialize, Deserialize)]
638pub struct AxInitialMarginRequirementResponse {
639 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
641 pub im: Decimal,
642}
643
644#[derive(Clone, Debug, Serialize, Deserialize)]
649pub struct AxOpenOrder {
650 pub tn: i64,
652 pub ts: i64,
654 pub d: AxOrderSide,
656 pub o: AxOrderStatus,
658 pub oid: String,
660 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
662 pub p: Decimal,
663 pub q: u64,
665 pub rq: u64,
667 pub s: Ustr,
669 pub tif: AxTimeInForce,
671 pub u: String,
673 pub xq: u64,
675 #[serde(default)]
677 pub cid: Option<u64>,
678 #[serde(default)]
680 pub tag: Option<String>,
681 #[serde(default)]
683 pub po: bool,
684}
685
686#[derive(Clone, Debug, Serialize, Deserialize)]
691pub struct AxOpenOrdersResponse {
692 pub orders: Vec<AxOpenOrder>,
694 pub total_count: i64,
696 pub limit: i32,
698 pub offset: i32,
700}
701
702#[derive(Clone, Debug, Serialize, Deserialize)]
707#[serde(rename_all = "snake_case")]
708pub struct AxFill {
709 pub trade_id: String,
711 pub order_id: Option<String>,
713 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
715 pub fee: Decimal,
716 pub is_taker: bool,
718 pub is_block_trade: Option<bool>,
720 pub is_final_settlement: Option<bool>,
722 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
724 pub price: Decimal,
725 pub quantity: u64,
727 pub side: AxOrderSide,
729 pub symbol: Ustr,
731 pub timestamp: Timestamp,
733 pub account_id: Ustr,
735 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
737 pub realized_pnl: Option<Decimal>,
738}
739
740#[derive(Clone, Debug, Serialize, Deserialize)]
745#[serde(rename_all = "snake_case")]
746pub struct AxFillsResponse {
747 pub fills: Vec<AxFill>,
749 #[serde(default)]
751 pub total_count: Option<i64>,
752 #[serde(default)]
754 pub limit: Option<i32>,
755 #[serde(default)]
757 pub next_cursor: Option<String>,
758}
759
760#[derive(Clone, Debug, Serialize, Deserialize)]
765#[serde(rename_all = "snake_case")]
766pub struct AxCandle {
767 pub symbol: Ustr,
769 pub ts: i64,
771 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
773 pub open: Decimal,
774 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
776 pub high: Decimal,
777 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
779 pub low: Decimal,
780 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
782 pub close: Decimal,
783 pub buy_volume: u64,
785 pub sell_volume: u64,
787 pub volume: u64,
789 pub width: AxCandleWidth,
791}
792
793#[derive(Clone, Debug, Serialize, Deserialize)]
798#[serde(rename_all = "snake_case")]
799pub struct AxCandlesResponse {
800 pub candles: Vec<AxCandle>,
802}
803
804#[derive(Clone, Debug, Serialize, Deserialize)]
810#[serde(rename_all = "snake_case")]
811pub struct AxCandleResponse {
812 pub candle: AxCandle,
814}
815
816#[derive(Clone, Debug, Serialize, Deserialize)]
821#[serde(rename_all = "snake_case")]
822pub struct AxFundingRate {
823 pub symbol: Ustr,
825 pub timestamp_ns: i64,
827 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
829 pub funding_rate: Decimal,
830 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
832 pub funding_amount: Decimal,
833 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
835 pub benchmark_price: Decimal,
836 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
838 pub settlement_price: Decimal,
839}
840
841#[derive(Clone, Debug, Serialize, Deserialize)]
846#[serde(rename_all = "snake_case")]
847pub struct AxFundingRatesResponse {
848 pub funding_rates: Vec<AxFundingRate>,
850 #[serde(default)]
852 pub total_count: Option<i64>,
853 #[serde(default)]
855 pub limit: Option<i32>,
856 #[serde(default)]
858 pub next_cursor: Option<String>,
859}
860
861#[derive(Clone, Debug, Serialize, Deserialize)]
866#[serde(rename_all = "snake_case")]
867pub struct AxFundingSlot {
868 pub index: i32,
870 pub funding_time: Timestamp,
872 pub status: AxFundingSlotStatus,
874 pub capped: bool,
876 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
878 pub mark_twap: Option<Decimal>,
879 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
881 pub underlying_twap: Option<Decimal>,
882 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
884 pub premium_bps: Option<Decimal>,
885 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
887 pub funding_rate_bps: Option<Decimal>,
888 #[serde(default)]
890 pub reason: Option<String>,
891}
892
893#[derive(Clone, Debug, Serialize, Deserialize)]
901#[serde(rename_all = "snake_case")]
902pub struct AxFundingSlotsResponse {
903 pub symbol: Ustr,
905 pub date: Date,
907 pub timezone: String,
909 pub variant: AxFundingVariant,
911 pub interval_count: i32,
913 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
915 pub cap_bps: Option<Decimal>,
916 pub slots: Vec<AxFundingSlot>,
918 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
920 pub realized_sum_bps: Decimal,
921 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
923 pub projected_eod_bps: Decimal,
924}
925
926#[derive(Clone, Debug, Serialize, Deserialize)]
931#[serde(rename_all = "snake_case")]
932pub struct AxPerSymbolRisk {
933 pub signed_quantity: i64,
935 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
937 pub signed_notional: Decimal,
938 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
940 pub average_price: Option<Decimal>,
941 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
943 pub liquidation_price: Option<Decimal>,
944 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
946 pub initial_margin_required_position: Decimal,
947 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
949 pub initial_margin_required_open_orders: Decimal,
950 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
952 pub initial_margin_required_total: Decimal,
953 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
955 pub maintenance_margin_required: Decimal,
956 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
958 pub unrealized_pnl: Decimal,
959}
960
961#[derive(Clone, Debug, Serialize, Deserialize)]
966#[serde(rename_all = "snake_case")]
967pub struct AxRiskSnapshot {
968 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
970 pub balance_usd: Decimal,
971 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
973 pub equity: Decimal,
974 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
976 pub initial_margin_available: Decimal,
977 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
979 pub initial_margin_required_for_open_orders: Decimal,
980 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
982 pub initial_margin_required_for_positions: Decimal,
983 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
985 pub initial_margin_required_total: Decimal,
986 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
988 pub maintenance_margin_available: Decimal,
989 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
991 pub maintenance_margin_required: Decimal,
992 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
994 pub unrealized_pnl: Decimal,
995 pub timestamp_ns: Timestamp,
997 pub account_id: Ustr,
999 #[serde(default)]
1001 pub per_symbol: AHashMap<String, AxPerSymbolRisk>,
1002}
1003
1004#[derive(Clone, Debug, Serialize, Deserialize)]
1009#[serde(rename_all = "snake_case")]
1010pub struct AxRiskSnapshotResponse {
1011 pub risk_snapshot: AxRiskSnapshot,
1013}
1014
1015#[derive(Clone, Debug, Serialize, Deserialize)]
1020#[serde(rename_all = "snake_case")]
1021pub struct AxTransaction {
1022 pub account_id: Ustr,
1024 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
1026 pub amount: Decimal,
1027 pub event_id: String,
1029 pub symbol: Ustr,
1031 pub timestamp: Timestamp,
1033 pub transaction_type: Ustr,
1035 #[serde(default)]
1037 pub initiated_by_user_id: Option<String>,
1038 #[serde(default)]
1040 pub reference_id: Option<String>,
1041}
1042
1043#[derive(Clone, Debug, Serialize, Deserialize)]
1048#[serde(rename_all = "snake_case")]
1049pub struct AxTransactionsResponse {
1050 pub transactions: Vec<AxTransaction>,
1052 #[serde(default)]
1054 pub total_count: Option<i64>,
1055 #[serde(default)]
1057 pub limit: Option<i32>,
1058 #[serde(default)]
1060 pub next_cursor: Option<String>,
1061}
1062
1063#[derive(Debug, Clone, Serialize, Deserialize, Zeroize)]
1068#[serde(rename_all = "snake_case")]
1069pub struct AuthenticateApiKeyRequest {
1070 pub api_key: SecretString,
1072 pub api_secret: SecretString,
1074 pub expiration_seconds: i32,
1076}
1077
1078impl AuthenticateApiKeyRequest {
1079 #[must_use]
1081 pub fn new(
1082 api_key: impl Into<SecretString>,
1083 api_secret: impl Into<SecretString>,
1084 expiration_seconds: i32,
1085 ) -> Self {
1086 Self {
1087 api_key: api_key.into(),
1088 api_secret: api_secret.into(),
1089 expiration_seconds,
1090 }
1091 }
1092}
1093
1094#[derive(Clone, Debug, Serialize, Deserialize)]
1099pub struct PlaceOrderRequest {
1100 pub d: AxOrderSide,
1102 #[serde(serialize_with = "serialize_decimal_as_str")]
1104 pub p: Decimal,
1105 pub po: bool,
1107 pub q: u64,
1109 pub s: Ustr,
1111 pub tif: AxTimeInForce,
1113 #[serde(skip_serializing_if = "Option::is_none")]
1115 pub tag: Option<String>,
1116}
1117
1118impl PlaceOrderRequest {
1119 #[must_use]
1121 pub fn new(
1122 side: AxOrderSide,
1123 price: Decimal,
1124 quantity: u64,
1125 symbol: Ustr,
1126 time_in_force: AxTimeInForce,
1127 post_only: bool,
1128 ) -> Self {
1129 Self {
1130 d: side,
1131 p: price,
1132 po: post_only,
1133 q: quantity,
1134 s: symbol,
1135 tif: time_in_force,
1136 tag: None,
1137 }
1138 }
1139
1140 #[must_use]
1142 pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
1143 self.tag = Some(tag.into());
1144 self
1145 }
1146}
1147
1148#[derive(Clone, Debug, Serialize, Deserialize)]
1153pub struct PreviewAggressiveLimitOrderRequest {
1154 pub symbol: Ustr,
1156 pub quantity: u64,
1158 pub side: AxOrderSide,
1160}
1161
1162impl PreviewAggressiveLimitOrderRequest {
1163 #[must_use]
1165 pub fn new(symbol: Ustr, quantity: u64, side: AxOrderSide) -> Self {
1166 Self {
1167 symbol,
1168 quantity,
1169 side,
1170 }
1171 }
1172}
1173
1174#[derive(Clone, Debug, Serialize, Deserialize)]
1179pub struct AxPreviewAggressiveLimitOrderResponse {
1180 pub filled_quantity: u64,
1182 pub remaining_quantity: u64,
1184 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
1186 pub limit_price: Option<Decimal>,
1187 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
1189 pub vwap: Option<Decimal>,
1190}
1191
1192#[derive(Clone, Debug, Serialize, Deserialize)]
1197pub struct CancelOrderRequest {
1198 pub oid: String,
1200}
1201
1202impl CancelOrderRequest {
1203 #[must_use]
1205 pub fn new(order_id: impl Into<String>) -> Self {
1206 Self {
1207 oid: order_id.into(),
1208 }
1209 }
1210}
1211
1212#[derive(Clone, Debug, Serialize, Deserialize)]
1220pub struct ReplaceOrderRequest {
1221 pub oid: String,
1223 #[serde(
1225 skip_serializing_if = "Option::is_none",
1226 serialize_with = "serialize_optional_decimal_as_str"
1227 )]
1228 pub p: Option<Decimal>,
1229 #[serde(skip_serializing_if = "Option::is_none")]
1231 pub q: Option<u64>,
1232 #[serde(skip_serializing_if = "Option::is_none")]
1234 pub po: Option<bool>,
1235 #[serde(skip_serializing_if = "Option::is_none")]
1237 pub tif: Option<AxTimeInForce>,
1238}
1239
1240impl ReplaceOrderRequest {
1241 #[must_use]
1245 pub fn new(order_id: impl Into<String>) -> Self {
1246 Self {
1247 oid: order_id.into(),
1248 p: None,
1249 q: None,
1250 po: None,
1251 tif: None,
1252 }
1253 }
1254
1255 #[must_use]
1257 pub fn with_price(mut self, price: Decimal) -> Self {
1258 self.p = Some(price);
1259 self
1260 }
1261
1262 #[must_use]
1264 pub fn with_quantity(mut self, quantity: u64) -> Self {
1265 self.q = Some(quantity);
1266 self
1267 }
1268}
1269
1270#[derive(Clone, Debug, Serialize, Deserialize)]
1275pub struct AxReplaceOrderResponse {
1276 pub oid: String,
1278}
1279
1280#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1285pub struct CancelAllOrdersRequest {
1286 #[serde(skip_serializing_if = "Option::is_none")]
1288 pub account_id: Option<Ustr>,
1289 #[serde(skip_serializing_if = "Option::is_none")]
1291 pub symbol: Option<Ustr>,
1292}
1293
1294impl CancelAllOrdersRequest {
1295 #[must_use]
1297 pub fn new() -> Self {
1298 Self::default()
1299 }
1300
1301 #[must_use]
1303 pub fn with_account_id(mut self, account_id: Ustr) -> Self {
1304 self.account_id = Some(account_id);
1305 self
1306 }
1307
1308 #[must_use]
1310 pub fn with_symbol(mut self, symbol: Ustr) -> Self {
1311 self.symbol = Some(symbol);
1312 self
1313 }
1314}
1315
1316#[derive(Clone, Debug, Serialize, Deserialize)]
1321pub struct AxCancelAllOrdersResponse {}
1322
1323#[cfg(test)]
1324mod tests {
1325 use rstest::rstest;
1326 use rust_decimal_macros::dec;
1327 use serde_json::json;
1328
1329 use super::*;
1330
1331 fn assert_zeroize_on_drop<T: ZeroizeOnDrop>() {}
1332
1333 #[rstest]
1334 fn test_deserialize_authenticate_response() {
1335 let json = include_str!("../../test_data/http_authenticate.json");
1336 let response: AxAuthenticateResponse = serde_json::from_str(json).unwrap();
1337 assert!(response.token.expose_secret().starts_with("test-token"));
1338 }
1339
1340 #[rstest]
1341 fn test_serialize_cancel_all_orders_request() {
1342 let request = CancelAllOrdersRequest::new()
1343 .with_account_id(Ustr::from("account-1"))
1344 .with_symbol(Ustr::from("XAU-PERP"));
1345
1346 let value = serde_json::to_value(request).unwrap();
1347
1348 assert_eq!(value["account_id"], "account-1");
1349 assert_eq!(value["symbol"], "XAU-PERP");
1350 assert!(value.get("execution_venue").is_none());
1351 }
1352
1353 #[rstest]
1354 fn test_deserialize_whoami_response() {
1355 let json = include_str!("../../test_data/http_get_whoami.json");
1356
1357 let response: AxWhoAmI = serde_json::from_str(json).unwrap();
1358
1359 assert_eq!(response.id, "01JBXR-7QK2-0000");
1360 assert_eq!(response.username, "trader@example.com");
1361 assert_eq!(response.pseudonym.as_deref(), Some("quiet-amber-heron"));
1362 assert_eq!(
1363 response.created_at,
1364 "2025-12-18T02:20:42.675817Z".parse::<Timestamp>().unwrap()
1365 );
1366 assert!(!response.require_2fa);
1367 assert!(response.is_onboarded);
1368 assert!(!response.is_frozen);
1369 assert!(!response.is_admin);
1370 assert_eq!(
1371 response.fiat_deposit_code.as_deref(),
1372 Some("01JBXR7QK20000Y")
1373 );
1374 assert_eq!(response.accounts.len(), 1);
1375
1376 let account = &response.accounts[0];
1377
1378 assert_eq!(account.id, "01JBXR-7QK2-0000");
1379 assert_eq!(account.name, "trader@example.com");
1380 assert!(!account.is_close_only);
1381 assert_eq!(account.maker_fee, Some(dec!(0.0002)));
1382 assert_eq!(account.taker_fee, Some(dec!(0.0025)));
1383 assert!(account.can_list);
1384 assert!(account.can_read);
1385 assert!(account.can_set_limits);
1386 assert!(account.can_reduce_or_close);
1387 assert!(account.can_trade);
1388 }
1389
1390 #[rstest]
1391 #[case(json!(""), None)]
1392 #[case(json!(null), None)]
1393 #[case(json!("0"), Some(Decimal::ZERO))]
1394 #[case(json!("0.0002"), Some(dec!(0.0002)))]
1395 fn test_deserialize_whoami_account_fee_distinguishes_absent_from_zero(
1396 #[case] wire_value: serde_json::Value,
1397 #[case] expected: Option<Decimal>,
1398 ) {
1399 let json = json!({
1401 "id": "01JBXR-7QK2-0000",
1402 "name": "trader@example.com",
1403 "is_close_only": false,
1404 "maker_fee": wire_value,
1405 "taker_fee": wire_value,
1406 "can_list": true,
1407 "can_read": true,
1408 "can_set_limits": true,
1409 "can_reduce_or_close": true,
1410 "can_trade": true,
1411 })
1412 .to_string();
1413
1414 let account: AxWhoAmIAccount = serde_json::from_str(&json).unwrap();
1415
1416 assert_eq!(account.maker_fee, expected);
1417 assert_eq!(account.taker_fee, expected);
1418 }
1419
1420 #[rstest]
1421 fn test_deserialize_whoami_account_rejects_malformed_fee() {
1422 let json = json!({
1423 "id": "01JBXR-7QK2-0000",
1424 "name": "trader@example.com",
1425 "is_close_only": false,
1426 "maker_fee": "not-a-decimal",
1427 "taker_fee": "0.0025",
1428 "can_list": true,
1429 "can_read": true,
1430 "can_set_limits": true,
1431 "can_reduce_or_close": true,
1432 "can_trade": true,
1433 })
1434 .to_string();
1435
1436 let error = serde_json::from_str::<AxWhoAmIAccount>(&json).unwrap_err();
1437
1438 assert!(
1439 error.to_string().contains("Invalid decimal"),
1440 "unexpected error: {error}"
1441 );
1442 }
1443
1444 #[rstest]
1445 fn test_deserialize_whoami_response_without_optional_profile_fields() {
1446 let json = json!({
1447 "id": "01JBXR-7QK2-0001",
1448 "username": "sub@example.com",
1449 "created_at": "2025-12-18T02:20:42.675817Z",
1450 "is_onboarded": true,
1451 "is_frozen": false,
1452 "is_admin": false,
1453 "require_2fa": true,
1454 "accounts": [],
1455 })
1456 .to_string();
1457
1458 let response: AxWhoAmI = serde_json::from_str(&json).unwrap();
1459
1460 assert!(response.require_2fa);
1461 assert_eq!(response.pseudonym, None);
1462 assert_eq!(response.fiat_deposit_code, None);
1463 assert!(response.accounts.is_empty());
1464 }
1465
1466 #[rstest]
1467 fn test_deserialize_instruments_response() {
1468 let json = include_str!("../../test_data/http_get_instruments.json");
1469 let response: AxInstrumentsResponse = serde_json::from_str(json).unwrap();
1470 assert_eq!(response.instruments.len(), 3);
1471 assert_eq!(response.instruments[0].symbol, "EURUSD-PERP");
1472 }
1473
1474 #[rstest]
1475 fn test_deserialize_balances_response() {
1476 let json = include_str!("../../test_data/http_get_balances.json");
1477 let response: AxBalancesResponse = serde_json::from_str(json).unwrap();
1478 assert_eq!(response.balances.len(), 3);
1479 assert_eq!(response.balances[0].symbol, "USD");
1480 }
1481
1482 #[rstest]
1483 fn test_deserialize_positions_response() {
1484 let json = include_str!("../../test_data/http_get_positions.json");
1485 let response: AxPositionsResponse = serde_json::from_str(json).unwrap();
1486 assert_eq!(response.positions.len(), 2);
1487 assert_eq!(response.positions[0].symbol, "BTC-PERP");
1488 assert_eq!(response.positions[1].signed_quantity, -5);
1489 }
1490
1491 #[rstest]
1492 fn test_deserialize_tickers_response() {
1493 let json = include_str!("../../test_data/http_get_tickers.json");
1494 let response: AxTickersResponse = serde_json::from_str(json).unwrap();
1495 assert_eq!(response.tickers.len(), 3);
1496 assert_eq!(response.total_count, 3);
1497 assert_eq!(response.limit, 100);
1498 assert_eq!(response.offset, 0);
1499 assert_eq!(response.tickers[0].symbol, "EURUSD-PERP");
1500 assert!(response.tickers[0].bid.is_some());
1501 assert!(response.tickers[2].bid.is_none());
1502 }
1503
1504 #[rstest]
1505 fn test_deserialize_funding_rates_response() {
1506 let json = include_str!("../../test_data/http_get_funding_rates.json");
1507 let response: AxFundingRatesResponse = serde_json::from_str(json).unwrap();
1508 assert_eq!(response.funding_rates.len(), 2);
1509 assert_eq!(response.funding_rates[0].symbol, "JPYUSD-PERP");
1510 }
1511
1512 #[rstest]
1513 fn test_deserialize_funding_slots_response() {
1514 let json = include_str!("../../test_data/http_get_funding_slots.json");
1515 let response: AxFundingSlotsResponse = serde_json::from_str(json).unwrap();
1516 assert_eq!(response.symbol, "EURUSD-PERP");
1517 assert_eq!(response.date, Date::new(2026, 7, 6).unwrap());
1518 assert_eq!(response.timezone, "America/New_York");
1519 assert_eq!(response.variant, AxFundingVariant::IntradayTwap);
1520 assert_eq!(response.interval_count, 4);
1521 assert_eq!(
1522 response.cap_bps.map(|d| d.to_string()),
1523 Some("5.0".to_string())
1524 );
1525 assert_eq!(response.slots.len(), 4);
1526
1527 let first = &response.slots[0];
1528 assert_eq!(first.index, 1);
1529 assert_eq!(first.status, AxFundingSlotStatus::Realized);
1530 assert!(!first.capped);
1531 assert_eq!(
1532 first.funding_rate_bps.map(|d| d.to_string()),
1533 Some("0.0921".to_string())
1534 );
1535 assert!(first.reason.is_none());
1536
1537 let capped = &response.slots[1];
1538 assert!(capped.capped);
1539 assert_eq!(
1540 capped.funding_rate_bps.map(|d| d.to_string()),
1541 Some("5.0000".to_string())
1542 );
1543
1544 let projected = &response.slots[2];
1545 assert_eq!(projected.status, AxFundingSlotStatus::Projected);
1546
1547 let skipped = &response.slots[3];
1548 assert_eq!(skipped.status, AxFundingSlotStatus::Skipped);
1549 assert!(skipped.mark_twap.is_none());
1550 assert!(skipped.funding_rate_bps.is_none());
1551 assert_eq!(skipped.reason.as_deref(), Some("holiday"));
1552
1553 assert_eq!(response.realized_sum_bps.to_string(), "5.0921");
1554 assert_eq!(response.projected_eod_bps.to_string(), "5.1842");
1555 }
1556
1557 #[rstest]
1558 fn test_funding_variant_and_slot_status_deserialization() {
1559 let daily: AxFundingVariant =
1560 serde_json::from_value(serde_json::json!("daily_close")).unwrap();
1561 let twap: AxFundingVariant =
1562 serde_json::from_value(serde_json::json!("intraday_twap")).unwrap();
1563 assert_eq!(daily, AxFundingVariant::DailyClose);
1564 assert_eq!(twap, AxFundingVariant::IntradayTwap);
1565
1566 let statuses = [
1567 ("realized", AxFundingSlotStatus::Realized),
1568 ("projected", AxFundingSlotStatus::Projected),
1569 ("skipped", AxFundingSlotStatus::Skipped),
1570 ("pending", AxFundingSlotStatus::Pending),
1571 ];
1572
1573 for (raw, expected) in statuses {
1574 let parsed: AxFundingSlotStatus =
1575 serde_json::from_value(serde_json::json!(raw)).unwrap();
1576 assert_eq!(parsed, expected);
1577 }
1578 }
1579
1580 #[rstest]
1581 fn test_deserialize_open_orders_response() {
1582 let json = include_str!("../../test_data/http_get_open_orders.json");
1583 let response: AxOpenOrdersResponse = serde_json::from_str(json).unwrap();
1584 assert_eq!(response.orders.len(), 2);
1585 assert_eq!(response.orders[0].oid, "O-01ARZ3NDEKTSV4RRFFQ69G5FAV");
1586 assert_eq!(response.orders[0].d, AxOrderSide::Buy);
1587 assert_eq!(response.orders[0].o, AxOrderStatus::Accepted);
1588 assert_eq!(response.orders[1].xq, 300);
1589 assert_eq!(response.total_count, 2);
1590 assert_eq!(response.limit, 100);
1591 assert_eq!(response.offset, 0);
1592 }
1593
1594 #[rstest]
1595 fn test_deserialize_fills_response() {
1596 let json = include_str!("../../test_data/http_get_fills.json");
1597 let response: AxFillsResponse = serde_json::from_str(json).unwrap();
1598 assert_eq!(response.fills.len(), 2);
1599 assert_eq!(response.fills[0].side, AxOrderSide::Buy);
1600 assert!(response.fills[0].is_taker);
1601 assert!(!response.fills[1].is_taker);
1602 assert_eq!(response.fills[0].is_block_trade, Some(false));
1603 assert_eq!(response.fills[0].is_final_settlement, Some(false));
1604 assert_eq!(response.total_count, Some(2));
1605 assert_eq!(response.limit, Some(100));
1606 assert_eq!(response.next_cursor, None);
1607 }
1608
1609 #[rstest]
1610 fn test_deserialize_candles_response() {
1611 let json = include_str!("../../test_data/http_get_candles.json");
1612 let response: AxCandlesResponse = serde_json::from_str(json).unwrap();
1613 assert_eq!(response.candles.len(), 2);
1614 assert_eq!(response.candles[0].symbol, "EURUSD-PERP");
1615 assert_eq!(response.candles[0].width, AxCandleWidth::Minutes1);
1616 }
1617
1618 #[rstest]
1619 fn test_deserialize_candle_response() {
1620 let json = include_str!("../../test_data/http_get_candle.json");
1621 let response: AxCandleResponse = serde_json::from_str(json).unwrap();
1622 assert_eq!(response.candle.symbol, "EURUSD-PERP");
1623 assert_eq!(response.candle.width, AxCandleWidth::Minutes1);
1624 }
1625
1626 #[rstest]
1627 fn test_deserialize_risk_snapshot_response() {
1628 let json = include_str!("../../test_data/http_get_risk_snapshot.json");
1629 let response: AxRiskSnapshotResponse = serde_json::from_str(json).unwrap();
1630 assert_eq!(
1631 response.risk_snapshot.account_id,
1632 Ustr::from("3c90c3cc-0d44-4b50-8888-8dd25736052a")
1633 );
1634 assert_eq!(response.risk_snapshot.per_symbol.len(), 2);
1635 assert!(
1636 response
1637 .risk_snapshot
1638 .per_symbol
1639 .contains_key("EURUSD-PERP")
1640 );
1641 assert_eq!(
1642 response.risk_snapshot.per_symbol["GBPUSD-PERP"].average_price,
1643 None
1644 );
1645 }
1646
1647 #[rstest]
1648 fn test_deserialize_transactions_response() {
1649 let json = include_str!("../../test_data/http_get_transactions.json");
1650 let response: AxTransactionsResponse = serde_json::from_str(json).unwrap();
1651 assert_eq!(response.transactions.len(), 2);
1652 assert_eq!(response.total_count, Some(2));
1653 assert_eq!(response.limit, Some(100));
1654 assert_eq!(response.transactions[0].account_id, Ustr::from("account-1"));
1655 assert_eq!(response.transactions[0].transaction_type, "deposit");
1656 assert!(response.transactions[0].initiated_by_user_id.is_some());
1657 assert!(response.transactions[1].reference_id.is_none());
1658 }
1659
1660 #[rstest]
1661 fn test_deserialize_preview_aggressive_limit_order_response() {
1662 let json = include_str!("../../test_data/http_preview_aggressive_limit_order.json");
1663 let response: AxPreviewAggressiveLimitOrderResponse = serde_json::from_str(json).unwrap();
1664 assert_eq!(response.filled_quantity, 1000);
1665 assert_eq!(response.remaining_quantity, 0);
1666 assert!(response.limit_price.is_some());
1667 assert!(response.vwap.is_some());
1668 }
1669
1670 #[rstest]
1671 fn test_deserialize_place_order_response() {
1672 let json = include_str!("../../test_data/http_place_order.json");
1673 let response: AxPlaceOrderResponse = serde_json::from_str(json).unwrap();
1674 assert_eq!(response.oid, "O-01ARZ3NDEKTSV4RRFFQ69G5FAV");
1675 }
1676
1677 #[rstest]
1678 fn test_deserialize_cancel_order_response() {
1679 let json = include_str!("../../test_data/http_cancel_order.json");
1680 let response: AxCancelOrderResponse = serde_json::from_str(json).unwrap();
1681 assert!(response.cxl_rx);
1682 }
1683
1684 #[rstest]
1685 fn test_deserialize_cancel_all_orders_response() {
1686 let json = include_str!("../../test_data/http_cancel_all_orders.json");
1687 let _response: AxCancelAllOrdersResponse = serde_json::from_str(json).unwrap();
1688 }
1689
1690 #[rstest]
1691 fn test_deserialize_trades_response() {
1692 let json = include_str!("../../test_data/http_get_trades.json");
1693 let response: AxTradesResponse = serde_json::from_str(json).unwrap();
1694 assert_eq!(response.trades.len(), 2);
1695 assert_eq!(response.trades[0].s, "EURUSD-PERP");
1696 assert_eq!(response.trades[0].d, AxOrderSide::Buy);
1697 assert_eq!(response.trades[0].q, 100);
1698 assert_eq!(response.trades[1].d, AxOrderSide::Sell);
1699 }
1700
1701 #[rstest]
1702 fn test_deserialize_book_response() {
1703 let json = include_str!("../../test_data/http_get_book.json");
1704 let response: AxBookResponse = serde_json::from_str(json).unwrap();
1705 assert_eq!(response.book.s, "EURUSD-PERP");
1706 assert_eq!(response.book.b.len(), 3);
1707 assert_eq!(response.book.a.len(), 3);
1708 assert_eq!(response.book.b[0].q, 500);
1709 assert_eq!(response.book.a[0].q, 400);
1710 }
1711
1712 #[rstest]
1713 fn test_deserialize_order_status_query_response() {
1714 let json = include_str!("../../test_data/http_get_order_status.json");
1715 let response: AxOrderStatusQueryResponse = serde_json::from_str(json).unwrap();
1716 assert_eq!(response.status.symbol, "EURUSD-PERP");
1717 assert_eq!(response.status.order_id, "O-01ARZ3NDEKTSV4RRFFQ69G5FAV");
1718 assert_eq!(response.status.state, AxOrderStatus::PartiallyFilled);
1719 assert_eq!(response.status.clord_id, Some(12345));
1720 assert_eq!(response.status.filled_quantity, Some(300));
1721 assert_eq!(response.status.remaining_quantity, Some(700));
1722 assert_eq!(response.status.reject_reason, None);
1723 assert_eq!(response.status.reject_message, None);
1724 }
1725
1726 #[rstest]
1727 fn test_deserialize_orders_response() {
1728 let json = include_str!("../../test_data/http_get_orders.json");
1729 let response: AxOrdersResponse = serde_json::from_str(json).unwrap();
1730 assert_eq!(response.orders.len(), 2);
1731 assert_eq!(response.total_count, Some(2));
1732 assert_eq!(response.limit, Some(100));
1733 assert_eq!(response.next_cursor, None);
1734 assert_eq!(response.orders[0].aid.as_deref(), Some("account-1"));
1735 assert_eq!(response.orders[0].o, AxOrderStatus::PartiallyFilled);
1736 assert_eq!(response.orders[0].xq, 300);
1737 assert_eq!(response.orders[1].o, AxOrderStatus::Filled);
1738 assert_eq!(response.orders[1].d, AxOrderSide::Sell);
1739 }
1740
1741 #[rstest]
1742 fn test_deserialize_initial_margin_requirement_response() {
1743 let json = include_str!("../../test_data/http_initial_margin_requirement.json");
1744 let response: AxInitialMarginRequirementResponse = serde_json::from_str(json).unwrap();
1745 assert_eq!(response.im, Decimal::new(125050, 2));
1746 }
1747
1748 #[rstest]
1749 fn test_deserialize_replace_order_response() {
1750 let json = include_str!("../../test_data/http_replace_order.json");
1751 let response: AxReplaceOrderResponse = serde_json::from_str(json).unwrap();
1752 assert_eq!(response.oid, "O-01ARZ3NDEKTSV4RRFFQ69G5NEW");
1753 }
1754
1755 #[rstest]
1756 fn test_replace_order_request_serialization() {
1757 let request = ReplaceOrderRequest::new("O-01ARZ3NDEKTSV4RRFFQ69G5FAV")
1758 .with_price(Decimal::new(10550, 4))
1759 .with_quantity(200);
1760
1761 let json = serde_json::to_value(&request).unwrap();
1762 assert_eq!(json["oid"], "O-01ARZ3NDEKTSV4RRFFQ69G5FAV");
1763 assert_eq!(json["p"], "1.0550");
1764 assert_eq!(json["q"], 200);
1765 assert!(json.get("po").is_none());
1766 assert!(json.get("tif").is_none());
1767 assert!(json.get("trigger_price").is_none());
1768 }
1769
1770 #[rstest]
1771 fn test_replace_order_request_minimal() {
1772 let request = ReplaceOrderRequest::new("O-TEST");
1773 let json = serde_json::to_value(&request).unwrap();
1774 assert_eq!(json["oid"], "O-TEST");
1775 assert!(json.get("p").is_none());
1776 assert!(json.get("q").is_none());
1777 }
1778
1779 #[rstest]
1780 fn test_authenticate_request_serializes_and_redacts_debug() {
1781 let request = AuthenticateApiKeyRequest::new("api-key-token", "api-secret-value", 3600);
1782
1783 let json = serde_json::to_value(&request).unwrap();
1784 let formatted = format!("{request:?}");
1785
1786 assert_eq!(json["api_key"], "api-key-token");
1787 assert_eq!(json["api_secret"], "api-secret-value");
1788 assert_eq!(json["expiration_seconds"], 3600);
1789 assert_eq!(
1790 formatted,
1791 "AuthenticateApiKeyRequest { api_key: <redacted>, api_secret: <redacted>, expiration_seconds: 3600 }",
1792 );
1793 assert!(!formatted.contains("api-key-token"));
1794 assert!(!formatted.contains("api-secret-value"));
1795 }
1796
1797 #[rstest]
1798 fn test_authenticate_response_redacts_debug() {
1799 assert_zeroize_on_drop::<AxAuthenticateResponse>();
1800
1801 let response = AxAuthenticateResponse {
1802 token: SecretString::from("session-token-value"),
1803 };
1804
1805 let formatted = format!("{response:?}");
1806
1807 assert_eq!(formatted, "AxAuthenticateResponse { token: <redacted> }");
1808 assert!(!formatted.contains("session-token-value"));
1809 }
1810}