Skip to main content

nautilus_architect_ax/http/
models.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Data transfer objects for deserializing Ax HTTP API payloads.
17
18use ahash::AHashMap;
19use chrono::{DateTime, Utc};
20use rust_decimal::Decimal;
21use serde::{Deserialize, Serialize};
22use strum::{AsRefStr, Display};
23use ustr::Ustr;
24
25use crate::common::{
26    enums::{
27        AxCandleWidth, AxCategory, AxInstrumentState, AxOrderSide, AxOrderStatus, AxOrderType,
28        AxTimeInForce,
29    },
30    parse::{
31        deserialize_decimal_or_zero, deserialize_optional_decimal_from_str,
32        serialize_decimal_as_str, serialize_optional_decimal_as_str,
33    },
34};
35
36/// Default instrument state when not provided by API.
37fn default_instrument_state() -> AxInstrumentState {
38    AxInstrumentState::Open
39}
40
41/// Response payload returned by `GET /whoami`.
42///
43/// # References
44/// - <https://docs.architect.exchange/api-reference/user-management/whoami>
45#[derive(Clone, Debug, Serialize, Deserialize)]
46#[serde(rename_all = "snake_case")]
47pub struct AxWhoAmI {
48    /// User account UUID.
49    pub id: String,
50    /// Username for the account.
51    pub username: String,
52    /// Account creation timestamp.
53    pub created_at: DateTime<Utc>,
54    /// Whether two-factor authentication is enabled.
55    pub enabled_2fa: bool,
56    /// Whether the user has completed onboarding.
57    pub is_onboarded: bool,
58    /// Whether the account is frozen.
59    pub is_frozen: bool,
60    /// Whether the user has admin privileges.
61    pub is_admin: bool,
62    /// Whether the account is in close-only mode.
63    pub is_close_only: bool,
64    /// Maker fee rate.
65    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
66    pub maker_fee: Decimal,
67    /// Taker fee rate.
68    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
69    pub taker_fee: Decimal,
70}
71
72/// Individual instrument definition.
73///
74/// # References
75/// - <https://docs.architect.exchange/api-reference/symbols-instruments/get-instruments>
76#[derive(Clone, Debug, Serialize, Deserialize)]
77#[serde(rename_all = "snake_case")]
78pub struct AxInstrument {
79    /// Trading symbol for the instrument.
80    pub symbol: Ustr,
81    /// Current trading state of the instrument (defaults to Open if not provided).
82    #[serde(default = "default_instrument_state")]
83    pub state: AxInstrumentState,
84    /// Contract multiplier.
85    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
86    pub multiplier: Decimal,
87    /// Minimum order size.
88    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
89    pub minimum_order_size: Decimal,
90    /// Price tick size.
91    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
92    pub tick_size: Decimal,
93    /// Quote currency symbol.
94    pub quote_currency: Ustr,
95    /// Funding settlement currency.
96    pub funding_settlement_currency: Ustr,
97    /// Instrument category (e.g. fx, equities, metals).
98    #[serde(default)]
99    pub category: Option<AxCategory>,
100    /// Maintenance margin percentage.
101    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
102    pub maintenance_margin_pct: Decimal,
103    /// Initial margin percentage.
104    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
105    pub initial_margin_pct: Decimal,
106    /// Contract mark price description (optional).
107    #[serde(default)]
108    pub contract_mark_price: Option<String>,
109    /// Contract size description (optional).
110    #[serde(default)]
111    pub contract_size: Option<String>,
112    /// Instrument description (optional).
113    #[serde(default)]
114    pub description: Option<String>,
115    /// Funding calendar schedule (optional).
116    #[serde(default)]
117    pub funding_calendar_schedule: Option<String>,
118    /// Funding frequency (optional).
119    #[serde(default)]
120    pub funding_frequency: Option<String>,
121    /// Lower cap for funding rate percentage (optional).
122    #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
123    pub funding_rate_cap_lower_pct: Option<Decimal>,
124    /// Upper cap for funding rate percentage (optional).
125    #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
126    pub funding_rate_cap_upper_pct: Option<Decimal>,
127    /// Lower deviation percentage for price bands (optional).
128    #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
129    pub price_band_lower_deviation_pct: Option<Decimal>,
130    /// Upper deviation percentage for price bands (optional).
131    #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
132    pub price_band_upper_deviation_pct: Option<Decimal>,
133    /// Price bands configuration (optional).
134    #[serde(default)]
135    pub price_bands: Option<String>,
136    /// Price quotation format (optional).
137    #[serde(default)]
138    pub price_quotation: Option<String>,
139    /// Underlying benchmark price description (optional).
140    #[serde(default)]
141    pub underlying_benchmark_price: Option<String>,
142}
143
144/// Response payload returned by `GET /instruments`.
145///
146/// # References
147/// - <https://docs.architect.exchange/api-reference/symbols-instruments/get-instruments>
148#[derive(Clone, Debug, Serialize, Deserialize)]
149#[serde(rename_all = "snake_case")]
150pub struct AxInstrumentsResponse {
151    /// List of instruments.
152    pub instruments: Vec<AxInstrument>,
153}
154
155/// Individual balance entry.
156///
157/// # References
158/// - <https://docs.architect.exchange/api-reference/portfolio-management/get-balances>
159#[derive(Clone, Debug, Serialize, Deserialize)]
160#[serde(rename_all = "snake_case")]
161pub struct AxBalance {
162    /// Asset symbol.
163    pub symbol: Ustr,
164    /// Available balance amount.
165    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
166    pub amount: Decimal,
167}
168
169/// Response payload returned by `GET /balances`.
170///
171/// # References
172/// - <https://docs.architect.exchange/api-reference/portfolio-management/get-balances>
173#[derive(Clone, Debug, Serialize, Deserialize)]
174#[serde(rename_all = "snake_case")]
175pub struct AxBalancesResponse {
176    /// List of balances.
177    pub balances: Vec<AxBalance>,
178}
179
180/// Individual position entry.
181///
182/// # References
183/// - <https://docs.architect.exchange/api-reference/portfolio-management/get-positions>
184#[derive(Clone, Debug, Serialize, Deserialize)]
185#[serde(rename_all = "snake_case")]
186pub struct AxPosition {
187    /// User account UUID.
188    pub user_id: String,
189    /// Instrument symbol.
190    pub symbol: Ustr,
191    /// Signed quantity (positive for long, negative for short).
192    pub signed_quantity: i64,
193    /// Signed notional value.
194    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
195    pub signed_notional: Decimal,
196    /// Position timestamp.
197    pub timestamp: DateTime<Utc>,
198    /// Realized profit and loss.
199    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
200    pub realized_pnl: Decimal,
201}
202
203/// Response payload returned by `GET /positions`.
204///
205/// # References
206/// - <https://docs.architect.exchange/api-reference/portfolio-management/get-positions>
207#[derive(Clone, Debug, Serialize, Deserialize)]
208#[serde(rename_all = "snake_case")]
209pub struct AxPositionsResponse {
210    /// List of positions.
211    pub positions: Vec<AxPosition>,
212}
213
214/// Individual ticker entry.
215///
216/// # References
217/// - <https://docs.architect.exchange/api-reference/marketdata/get-ticker>
218#[derive(Clone, Debug, Serialize, Deserialize)]
219#[serde(rename_all = "snake_case")]
220pub struct AxTicker {
221    /// Instrument symbol.
222    pub symbol: Ustr,
223    /// Best bid price.
224    #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
225    pub bid: Option<Decimal>,
226    /// Best ask price.
227    #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
228    pub ask: Option<Decimal>,
229    /// Last trade price.
230    #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
231    pub last: Option<Decimal>,
232    /// Mark price.
233    #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
234    pub mark: Option<Decimal>,
235    /// Index price.
236    #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
237    pub index: Option<Decimal>,
238    /// 24-hour volume.
239    #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
240    pub volume_24h: Option<Decimal>,
241    /// 24-hour high price.
242    #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
243    pub high_24h: Option<Decimal>,
244    /// 24-hour low price.
245    #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
246    pub low_24h: Option<Decimal>,
247    /// Ticker timestamp.
248    #[serde(default)]
249    pub timestamp: Option<DateTime<Utc>>,
250}
251
252/// Response payload returned by `GET /tickers`.
253///
254/// # References
255/// - <https://docs.architect.exchange/api-reference/marketdata/get-tickers>
256#[derive(Clone, Debug, Serialize, Deserialize)]
257#[serde(rename_all = "snake_case")]
258pub struct AxTickersResponse {
259    /// List of tickers.
260    pub tickers: Vec<AxTicker>,
261}
262
263/// Response payload returned by `POST /authenticate`.
264///
265/// # References
266/// - <https://docs.architect.exchange/api-reference/user-management/get-user-token>
267#[derive(Clone, Debug, Serialize, Deserialize)]
268#[serde(rename_all = "snake_case")]
269pub struct AxAuthenticateResponse {
270    /// Session token for authenticated requests.
271    pub token: String,
272}
273
274/// Response payload returned by `POST /place_order`.
275///
276/// # References
277/// - <https://docs.architect.exchange/api-reference/order-management/place-order>
278#[derive(Clone, Debug, Serialize, Deserialize)]
279pub struct AxPlaceOrderResponse {
280    /// Order ID of the placed order.
281    pub oid: String,
282}
283
284/// Response payload returned by `POST /cancel_order`.
285///
286/// # References
287/// - <https://docs.architect.exchange/api-reference/order-management/cancel-order>
288#[derive(Clone, Debug, Serialize, Deserialize)]
289pub struct AxCancelOrderResponse {
290    /// Whether the cancel request has been accepted.
291    pub cxl_rx: bool,
292}
293
294/// Individual trade entry from the REST API.
295///
296/// # References
297/// - <https://docs.architect.exchange/api-reference/market-data/get-trades>
298#[derive(Clone, Debug, Serialize, Deserialize)]
299pub struct AxRestTrade {
300    /// Timestamp (Unix epoch seconds).
301    pub ts: i64,
302    /// Nanosecond component of the timestamp.
303    pub tn: i64,
304    /// Trade price (decimal string).
305    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
306    pub p: Decimal,
307    /// Trade quantity.
308    pub q: i64,
309    /// Symbol.
310    pub s: Ustr,
311    /// Trade direction (aggressor side).
312    pub d: AxOrderSide,
313}
314
315/// Response payload returned by `GET /trades`.
316///
317/// # References
318/// - <https://docs.architect.exchange/api-reference/market-data/get-trades>
319#[derive(Clone, Debug, Serialize, Deserialize)]
320pub struct AxTradesResponse {
321    /// List of trades.
322    pub trades: Vec<AxRestTrade>,
323}
324
325/// Individual price level in the order book.
326///
327/// # References
328/// - <https://docs.architect.exchange/api-reference/market-data/get-book>
329#[derive(Clone, Debug, Serialize, Deserialize)]
330pub struct AxBookLevel {
331    /// Price (decimal string).
332    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
333    pub p: Decimal,
334    /// Quantity at this price level.
335    pub q: i64,
336    /// Individual order IDs (Level 3 only).
337    #[serde(default)]
338    pub o: Option<Vec<i64>>,
339}
340
341/// Order book snapshot.
342///
343/// # References
344/// - <https://docs.architect.exchange/api-reference/market-data/get-book>
345#[derive(Clone, Debug, Serialize, Deserialize)]
346pub struct AxBook {
347    /// Timestamp (Unix epoch seconds).
348    pub ts: i64,
349    /// Nanosecond component of the timestamp.
350    pub tn: i64,
351    /// Symbol.
352    pub s: Ustr,
353    /// Bid levels (best to worst).
354    pub b: Vec<AxBookLevel>,
355    /// Ask levels (best to worst).
356    pub a: Vec<AxBookLevel>,
357}
358
359/// Response payload returned by `GET /book`.
360///
361/// # References
362/// - <https://docs.architect.exchange/api-reference/market-data/get-book>
363#[derive(Clone, Debug, Serialize, Deserialize)]
364pub struct AxBookResponse {
365    /// The order book snapshot.
366    pub book: AxBook,
367}
368
369/// Detailed order status from single-order lookup.
370///
371/// # References
372/// - <https://docs.architect.exchange/api-reference/order-management/get-order-status>
373#[derive(Clone, Debug, Serialize, Deserialize)]
374pub struct AxOrderStatusDetail {
375    /// Trading symbol.
376    pub symbol: Ustr,
377    /// Order ID.
378    pub order_id: String,
379    /// Current order state.
380    pub state: AxOrderStatus,
381    /// Client order ID.
382    #[serde(default)]
383    pub clord_id: Option<u64>,
384    /// Filled quantity.
385    #[serde(default)]
386    pub filled_quantity: Option<i64>,
387    /// Remaining quantity.
388    #[serde(default)]
389    pub remaining_quantity: Option<i64>,
390}
391
392/// Response payload returned by `GET /order-status`.
393///
394/// # References
395/// - <https://docs.architect.exchange/api-reference/order-management/get-order-status>
396#[derive(Clone, Debug, Serialize, Deserialize)]
397pub struct AxOrderStatusQueryResponse {
398    /// The order status detail.
399    pub status: AxOrderStatusDetail,
400}
401
402/// Reason for order rejection from the exchange.
403///
404/// # References
405/// - <https://docs.architect.exchange/api-reference/order-management/get-orders>
406#[derive(Clone, Copy, Debug, Display, Eq, PartialEq, Hash, AsRefStr, Serialize, Deserialize)]
407#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
408#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
409pub enum AxOrderRejectReason {
410    CloseOnly,
411    InsufficientMargin,
412    MaxOpenOrdersExceeded,
413    UnknownSymbol,
414    ExchangeClosed,
415    IncorrectQuantity,
416    InvalidPriceIncrement,
417    IncorrectOrderType,
418    PriceOutOfBounds,
419    NoLiquidity,
420    InsufficientCreditLimit,
421    #[serde(other)]
422    Unknown,
423}
424
425/// Detailed order entry from historical orders query.
426///
427/// # References
428/// - <https://docs.architect.exchange/api-reference/order-management/get-orders>
429#[derive(Clone, Debug, Serialize, Deserialize)]
430pub struct AxOrderDetail {
431    /// Timestamp (Unix epoch seconds).
432    pub ts: i64,
433    /// Nanosecond component.
434    #[serde(default)]
435    pub tn: i64,
436    /// Order ID.
437    pub oid: String,
438    /// User ID.
439    pub u: String,
440    /// Symbol.
441    pub s: Ustr,
442    /// Price.
443    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
444    pub p: Decimal,
445    /// Order quantity.
446    pub q: u64,
447    /// Executed quantity.
448    pub xq: u64,
449    /// Remaining quantity.
450    pub rq: u64,
451    /// Order state.
452    pub o: AxOrderStatus,
453    /// Order side.
454    pub d: AxOrderSide,
455    /// Time in force.
456    pub tif: AxTimeInForce,
457    /// Client order ID.
458    #[serde(default)]
459    pub cid: Option<u64>,
460    /// Reject reason.
461    #[serde(default)]
462    pub r: Option<AxOrderRejectReason>,
463    /// Order tag.
464    #[serde(default)]
465    pub tag: Option<String>,
466    /// Text note.
467    #[serde(default)]
468    pub txt: Option<String>,
469    /// Whether the order is post-only.
470    #[serde(default)]
471    pub po: bool,
472}
473
474/// Response payload returned by `GET /orders`.
475///
476/// # References
477/// - <https://docs.architect.exchange/api-reference/order-management/get-orders>
478#[derive(Clone, Debug, Serialize, Deserialize)]
479pub struct AxOrdersResponse {
480    /// List of order details.
481    pub orders: Vec<AxOrderDetail>,
482    /// Total matching records (for pagination).
483    pub total_count: i64,
484    /// Applied limit.
485    pub limit: i32,
486    /// Applied offset.
487    pub offset: i32,
488}
489
490/// Response payload returned by `POST /initial-margin-requirement`.
491///
492/// # References
493/// - <https://docs.architect.exchange/api-reference/portfolio-management/post-initial-margin-requirement>
494#[derive(Clone, Debug, Serialize, Deserialize)]
495pub struct AxInitialMarginRequirementResponse {
496    /// Initial margin requirement.
497    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
498    pub im: Decimal,
499}
500
501/// Individual open order entry.
502///
503/// # References
504/// - <https://docs.architect.exchange/api-reference/order-management/get-open-orders>
505#[derive(Clone, Debug, Serialize, Deserialize)]
506pub struct AxOpenOrder {
507    /// Trade number.
508    pub tn: i64,
509    /// Timestamp (Unix epoch).
510    pub ts: i64,
511    /// Order side: "B" (buy) or "S" (sell).
512    pub d: AxOrderSide,
513    /// Order status.
514    pub o: AxOrderStatus,
515    /// Order ID.
516    pub oid: String,
517    /// Price.
518    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
519    pub p: Decimal,
520    /// Quantity.
521    pub q: u64,
522    /// Remaining quantity.
523    pub rq: u64,
524    /// Symbol.
525    pub s: Ustr,
526    /// Time in force.
527    pub tif: AxTimeInForce,
528    /// User ID.
529    pub u: String,
530    /// Executed quantity.
531    pub xq: u64,
532    /// Optional client ID for order correlation.
533    #[serde(default)]
534    pub cid: Option<u64>,
535    /// Optional order tag.
536    #[serde(default)]
537    pub tag: Option<String>,
538    /// Whether the order is post-only.
539    #[serde(default)]
540    pub po: bool,
541}
542
543/// Response payload returned by `GET /open_orders`.
544///
545/// # References
546/// - <https://docs.architect.exchange/api-reference/order-management/get-open-orders>
547#[derive(Clone, Debug, Serialize, Deserialize)]
548pub struct AxOpenOrdersResponse {
549    /// List of open orders.
550    pub orders: Vec<AxOpenOrder>,
551}
552
553/// Individual fill/trade entry.
554///
555/// # References
556/// - <https://docs.architect.exchange/api-reference/portfolio-management/get-fills>
557#[derive(Clone, Debug, Serialize, Deserialize)]
558#[serde(rename_all = "snake_case")]
559pub struct AxFill {
560    /// Trade ID (execution identifier).
561    pub trade_id: String,
562    /// Order ID.
563    pub order_id: String,
564    /// Fee amount.
565    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
566    pub fee: Decimal,
567    /// Whether this was a taker order.
568    pub is_taker: bool,
569    /// Execution price.
570    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
571    pub price: Decimal,
572    /// Executed quantity (always non-negative).
573    pub quantity: u64,
574    /// Order side.
575    pub side: AxOrderSide,
576    /// Instrument symbol.
577    pub symbol: Ustr,
578    /// Execution timestamp.
579    pub timestamp: DateTime<Utc>,
580    /// User ID.
581    pub user_id: String,
582    /// Realized PnL for this fill.
583    #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
584    pub realized_pnl: Option<Decimal>,
585}
586
587/// Response payload returned by `GET /fills`.
588///
589/// # References
590/// - <https://docs.architect.exchange/api-reference/portfolio-management/get-fills>
591#[derive(Clone, Debug, Serialize, Deserialize)]
592#[serde(rename_all = "snake_case")]
593pub struct AxFillsResponse {
594    /// List of fills.
595    pub fills: Vec<AxFill>,
596}
597
598/// Individual candle/OHLCV entry.
599///
600/// # References
601/// - <https://docs.architect.exchange/api-reference/marketdata/get-candles>
602#[derive(Clone, Debug, Serialize, Deserialize)]
603#[serde(rename_all = "snake_case")]
604pub struct AxCandle {
605    /// Instrument symbol.
606    pub symbol: Ustr,
607    /// Candle timestamp (Unix epoch seconds).
608    pub ts: i64,
609    /// Open price.
610    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
611    pub open: Decimal,
612    /// High price.
613    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
614    pub high: Decimal,
615    /// Low price.
616    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
617    pub low: Decimal,
618    /// Close price.
619    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
620    pub close: Decimal,
621    /// Buy volume.
622    pub buy_volume: u64,
623    /// Sell volume.
624    pub sell_volume: u64,
625    /// Total volume.
626    pub volume: u64,
627    /// Candle width/interval.
628    pub width: AxCandleWidth,
629}
630
631/// Response payload returned by `GET /candles`.
632///
633/// # References
634/// - <https://docs.architect.exchange/api-reference/marketdata/get-candles>
635#[derive(Clone, Debug, Serialize, Deserialize)]
636#[serde(rename_all = "snake_case")]
637pub struct AxCandlesResponse {
638    /// List of candles.
639    pub candles: Vec<AxCandle>,
640}
641
642/// Response payload returned by `GET /candles/current` and `GET /candles/last`.
643///
644/// # References
645/// - <https://docs.architect.exchange/api-reference/marketdata/get-current-candle>
646/// - <https://docs.architect.exchange/api-reference/marketdata/get-last-candle>
647#[derive(Clone, Debug, Serialize, Deserialize)]
648#[serde(rename_all = "snake_case")]
649pub struct AxCandleResponse {
650    /// The candle data.
651    pub candle: AxCandle,
652}
653
654/// Individual funding rate entry.
655///
656/// # References
657/// - <https://docs.architect.exchange/api-reference/marketdata/get-funding-rates>
658#[derive(Clone, Debug, Serialize, Deserialize)]
659#[serde(rename_all = "snake_case")]
660pub struct AxFundingRate {
661    /// Instrument symbol.
662    pub symbol: Ustr,
663    /// Timestamp in nanoseconds.
664    pub timestamp_ns: i64,
665    /// Funding rate.
666    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
667    pub funding_rate: Decimal,
668    /// Funding amount.
669    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
670    pub funding_amount: Decimal,
671    /// Benchmark price.
672    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
673    pub benchmark_price: Decimal,
674    /// Settlement price.
675    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
676    pub settlement_price: Decimal,
677}
678
679/// Response payload returned by `GET /funding-rates`.
680///
681/// # References
682/// - <https://docs.architect.exchange/api-reference/marketdata/get-funding-rates>
683#[derive(Clone, Debug, Serialize, Deserialize)]
684#[serde(rename_all = "snake_case")]
685pub struct AxFundingRatesResponse {
686    /// List of funding rates.
687    pub funding_rates: Vec<AxFundingRate>,
688}
689
690/// Per-symbol risk metrics.
691///
692/// # References
693/// - <https://docs.architect.exchange/api-reference/portfolio-management/get-risk-snapshot>
694#[derive(Clone, Debug, Serialize, Deserialize)]
695#[serde(rename_all = "snake_case")]
696pub struct AxPerSymbolRisk {
697    /// Signed quantity (positive for long, negative for short).
698    pub signed_quantity: i64,
699    /// Signed notional value.
700    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
701    pub signed_notional: Decimal,
702    /// Average entry price.
703    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
704    pub average_price: Decimal,
705    /// Liquidation price.
706    #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
707    pub liquidation_price: Option<Decimal>,
708    /// Initial margin required.
709    #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
710    pub initial_margin_required: Option<Decimal>,
711    /// Maintenance margin required.
712    #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
713    pub maintenance_margin_required: Option<Decimal>,
714    /// Unrealized P&L.
715    #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
716    pub unrealized_pnl: Option<Decimal>,
717}
718
719/// Risk snapshot data.
720///
721/// # References
722/// - <https://docs.architect.exchange/api-reference/portfolio-management/get-risk-snapshot>
723#[derive(Clone, Debug, Serialize, Deserialize)]
724#[serde(rename_all = "snake_case")]
725pub struct AxRiskSnapshot {
726    /// USD account balance.
727    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
728    pub balance_usd: Decimal,
729    /// Total equity value.
730    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
731    pub equity: Decimal,
732    /// Available initial margin.
733    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
734    pub initial_margin_available: Decimal,
735    /// Margin required for open orders.
736    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
737    pub initial_margin_required_for_open_orders: Decimal,
738    /// Margin required for positions.
739    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
740    pub initial_margin_required_for_positions: Decimal,
741    /// Total initial margin requirement.
742    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
743    pub initial_margin_required_total: Decimal,
744    /// Available maintenance margin.
745    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
746    pub maintenance_margin_available: Decimal,
747    /// Required maintenance margin.
748    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
749    pub maintenance_margin_required: Decimal,
750    /// Unrealized profit/loss.
751    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
752    pub unrealized_pnl: Decimal,
753    /// Snapshot timestamp.
754    pub timestamp_ns: DateTime<Utc>,
755    /// User identifier.
756    pub user_id: String,
757    /// Per-symbol risk data.
758    #[serde(default)]
759    pub per_symbol: AHashMap<String, AxPerSymbolRisk>,
760}
761
762/// Response payload returned by `GET /risk-snapshot`.
763///
764/// # References
765/// - <https://docs.architect.exchange/api-reference/portfolio-management/get-risk-snapshot>
766#[derive(Clone, Debug, Serialize, Deserialize)]
767#[serde(rename_all = "snake_case")]
768pub struct AxRiskSnapshotResponse {
769    /// The risk snapshot data.
770    pub risk_snapshot: AxRiskSnapshot,
771}
772
773/// Individual transaction entry.
774///
775/// # References
776/// - <https://docs.architect.exchange/api-reference/portfolio-management/get-transactions>
777#[derive(Clone, Debug, Serialize, Deserialize)]
778#[serde(rename_all = "snake_case")]
779pub struct AxTransaction {
780    /// Transaction amount.
781    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
782    pub amount: Decimal,
783    /// Unique event identifier.
784    pub event_id: String,
785    /// Asset symbol.
786    pub symbol: Ustr,
787    /// Transaction timestamp.
788    pub timestamp: DateTime<Utc>,
789    /// Type of transaction.
790    pub transaction_type: Ustr,
791    /// User identifier.
792    pub user_id: String,
793    /// Optional reference identifier.
794    #[serde(default)]
795    pub reference_id: Option<String>,
796}
797
798/// Response payload returned by `GET /transactions`.
799///
800/// # References
801/// - <https://docs.architect.exchange/api-reference/portfolio-management/get-transactions>
802#[derive(Clone, Debug, Serialize, Deserialize)]
803#[serde(rename_all = "snake_case")]
804pub struct AxTransactionsResponse {
805    /// List of transactions.
806    pub transactions: Vec<AxTransaction>,
807}
808
809/// Request body for `POST /authenticate` using API key and secret.
810///
811/// # References
812/// - <https://docs.architect.exchange/api-reference/user-management/get-user-token>
813#[derive(Clone, Debug, Serialize, Deserialize)]
814#[serde(rename_all = "snake_case")]
815pub struct AuthenticateApiKeyRequest {
816    /// API key.
817    pub api_key: String,
818    /// API secret.
819    pub api_secret: String,
820    /// Token expiration in seconds.
821    pub expiration_seconds: i32,
822}
823
824impl AuthenticateApiKeyRequest {
825    /// Creates a new [`AuthenticateApiKeyRequest`].
826    #[must_use]
827    pub fn new(
828        api_key: impl Into<String>,
829        api_secret: impl Into<String>,
830        expiration_seconds: i32,
831    ) -> Self {
832        Self {
833            api_key: api_key.into(),
834            api_secret: api_secret.into(),
835            expiration_seconds,
836        }
837    }
838}
839
840/// Request body for `POST /authenticate` using username and password.
841///
842/// # References
843/// - <https://docs.architect.exchange/api-reference/user-management/get-user-token>
844#[derive(Clone, Debug, Serialize, Deserialize)]
845#[serde(rename_all = "snake_case")]
846pub struct AuthenticateUserRequest {
847    /// Username.
848    pub username: String,
849    /// Password.
850    pub password: String,
851    /// Token expiration in seconds.
852    pub expiration_seconds: i32,
853}
854
855impl AuthenticateUserRequest {
856    /// Creates a new [`AuthenticateUserRequest`].
857    #[must_use]
858    pub fn new(
859        username: impl Into<String>,
860        password: impl Into<String>,
861        expiration_seconds: i32,
862    ) -> Self {
863        Self {
864            username: username.into(),
865            password: password.into(),
866            expiration_seconds,
867        }
868    }
869}
870
871/// Request body for `POST /place_order`.
872///
873/// # References
874/// - <https://docs.architect.exchange/api-reference/order-management/place-order>
875#[derive(Clone, Debug, Serialize, Deserialize)]
876pub struct PlaceOrderRequest {
877    /// Order side: "B" (buy) or "S" (sell).
878    pub d: AxOrderSide,
879    /// Order price (limit price).
880    #[serde(serialize_with = "serialize_decimal_as_str")]
881    pub p: Decimal,
882    /// Post-only flag (maker-or-cancel).
883    pub po: bool,
884    /// Order quantity in contracts.
885    pub q: u64,
886    /// Order symbol.
887    pub s: Ustr,
888    /// Time in force.
889    pub tif: AxTimeInForce,
890    /// Optional order tag (max 10 alphanumeric characters).
891    #[serde(skip_serializing_if = "Option::is_none")]
892    pub tag: Option<String>,
893    /// Order type (defaults to LIMIT if not specified).
894    #[serde(skip_serializing_if = "Option::is_none")]
895    pub order_type: Option<AxOrderType>,
896    /// Trigger price for stop-loss orders (required for STOP_LOSS_LIMIT).
897    #[serde(
898        skip_serializing_if = "Option::is_none",
899        serialize_with = "serialize_optional_decimal_as_str"
900    )]
901    pub trigger_price: Option<Decimal>,
902}
903
904impl PlaceOrderRequest {
905    /// Creates a new [`PlaceOrderRequest`] for a limit order.
906    #[must_use]
907    pub fn new(
908        side: AxOrderSide,
909        price: Decimal,
910        quantity: u64,
911        symbol: Ustr,
912        time_in_force: AxTimeInForce,
913        post_only: bool,
914    ) -> Self {
915        Self {
916            d: side,
917            p: price,
918            po: post_only,
919            q: quantity,
920            s: symbol,
921            tif: time_in_force,
922            tag: None,
923            order_type: None,
924            trigger_price: None,
925        }
926    }
927
928    /// Creates a new [`PlaceOrderRequest`] for a stop-loss limit order.
929    #[must_use]
930    pub fn new_stop_loss(
931        side: AxOrderSide,
932        limit_price: Decimal,
933        trigger_price: Decimal,
934        quantity: u64,
935        symbol: Ustr,
936        time_in_force: AxTimeInForce,
937    ) -> Self {
938        Self {
939            d: side,
940            p: limit_price,
941            po: false,
942            q: quantity,
943            s: symbol,
944            tif: time_in_force,
945            tag: None,
946            order_type: Some(AxOrderType::StopLossLimit),
947            trigger_price: Some(trigger_price),
948        }
949    }
950
951    /// Sets the optional order tag.
952    #[must_use]
953    pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
954        self.tag = Some(tag.into());
955        self
956    }
957
958    /// Sets the order type.
959    #[must_use]
960    pub fn with_order_type(mut self, order_type: AxOrderType) -> Self {
961        self.order_type = Some(order_type);
962        self
963    }
964
965    /// Sets the trigger price for stop orders.
966    #[must_use]
967    pub fn with_trigger_price(mut self, trigger_price: Decimal) -> Self {
968        self.trigger_price = Some(trigger_price);
969        self
970    }
971}
972
973/// Request body for `POST /preview-aggressive-limit-order`.
974///
975/// # References
976/// - <https://docs.architect.exchange/api-reference/marketdata/preview-aggressive-limit-order>
977#[derive(Clone, Debug, Serialize, Deserialize)]
978pub struct PreviewAggressiveLimitOrderRequest {
979    /// Trading symbol.
980    pub symbol: Ustr,
981    /// Order quantity in contracts.
982    pub quantity: u64,
983    /// Order side: "B" (buy) or "S" (sell).
984    pub side: AxOrderSide,
985}
986
987impl PreviewAggressiveLimitOrderRequest {
988    /// Creates a new [`PreviewAggressiveLimitOrderRequest`].
989    #[must_use]
990    pub fn new(symbol: Ustr, quantity: u64, side: AxOrderSide) -> Self {
991        Self {
992            symbol,
993            quantity,
994            side,
995        }
996    }
997}
998
999/// Response payload returned by `POST /preview-aggressive-limit-order`.
1000///
1001/// # References
1002/// - <https://docs.architect.exchange/api-reference/marketdata/preview-aggressive-limit-order>
1003#[derive(Clone, Debug, Serialize, Deserialize)]
1004pub struct AxPreviewAggressiveLimitOrderResponse {
1005    /// Quantity that would be filled at the aggressive price.
1006    pub filled_quantity: u64,
1007    /// Quantity that cannot be filled (insufficient book depth).
1008    pub remaining_quantity: u64,
1009    /// The aggressive limit price ("take through" price), or None if no liquidity.
1010    #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
1011    pub limit_price: Option<Decimal>,
1012    /// Volume-weighted average price of expected fills.
1013    #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
1014    pub vwap: Option<Decimal>,
1015}
1016
1017/// Request body for `POST /cancel_order`.
1018///
1019/// # References
1020/// - <https://docs.architect.exchange/api-reference/order-management/cancel-order>
1021#[derive(Clone, Debug, Serialize, Deserialize)]
1022pub struct CancelOrderRequest {
1023    /// Order ID to cancel.
1024    pub oid: String,
1025}
1026
1027impl CancelOrderRequest {
1028    /// Creates a new [`CancelOrderRequest`].
1029    #[must_use]
1030    pub fn new(order_id: impl Into<String>) -> Self {
1031        Self {
1032            oid: order_id.into(),
1033        }
1034    }
1035}
1036
1037/// Request body for `POST /replace_order`.
1038///
1039/// Replaces (amends) an existing order. Unspecified optional fields inherit
1040/// from the original order. The exchange returns a new order ID.
1041///
1042/// # References
1043/// - <https://docs.architect.exchange/api-reference/order-management/replace-order>
1044#[derive(Clone, Debug, Serialize, Deserialize)]
1045pub struct ReplaceOrderRequest {
1046    /// Order ID to replace.
1047    pub oid: String,
1048    /// New limit price (optional, inherits from original if omitted).
1049    #[serde(
1050        skip_serializing_if = "Option::is_none",
1051        serialize_with = "serialize_optional_decimal_as_str"
1052    )]
1053    pub p: Option<Decimal>,
1054    /// New quantity in contracts (optional, inherits from original if omitted).
1055    #[serde(skip_serializing_if = "Option::is_none")]
1056    pub q: Option<u64>,
1057    /// New post-only flag (optional, inherits from original if omitted).
1058    #[serde(skip_serializing_if = "Option::is_none")]
1059    pub po: Option<bool>,
1060    /// New time-in-force (optional, inherits from original if omitted).
1061    #[serde(skip_serializing_if = "Option::is_none")]
1062    pub tif: Option<AxTimeInForce>,
1063    /// New trigger price for stop orders (optional).
1064    #[serde(
1065        skip_serializing_if = "Option::is_none",
1066        serialize_with = "serialize_optional_decimal_as_str"
1067    )]
1068    pub trigger_price: Option<Decimal>,
1069}
1070
1071impl ReplaceOrderRequest {
1072    /// Creates a new [`ReplaceOrderRequest`] with only the order ID.
1073    ///
1074    /// Use the builder methods to set the fields to amend.
1075    #[must_use]
1076    pub fn new(order_id: impl Into<String>) -> Self {
1077        Self {
1078            oid: order_id.into(),
1079            p: None,
1080            q: None,
1081            po: None,
1082            tif: None,
1083            trigger_price: None,
1084        }
1085    }
1086
1087    /// Sets the new limit price.
1088    #[must_use]
1089    pub fn with_price(mut self, price: Decimal) -> Self {
1090        self.p = Some(price);
1091        self
1092    }
1093
1094    /// Sets the new quantity.
1095    #[must_use]
1096    pub fn with_quantity(mut self, quantity: u64) -> Self {
1097        self.q = Some(quantity);
1098        self
1099    }
1100
1101    /// Sets the new trigger price.
1102    #[must_use]
1103    pub fn with_trigger_price(mut self, trigger_price: Decimal) -> Self {
1104        self.trigger_price = Some(trigger_price);
1105        self
1106    }
1107}
1108
1109/// Response payload returned by `POST /replace_order`.
1110///
1111/// # References
1112/// - <https://docs.architect.exchange/api-reference/order-management/replace-order>
1113#[derive(Clone, Debug, Serialize, Deserialize)]
1114pub struct AxReplaceOrderResponse {
1115    /// New order ID assigned to the replacement order.
1116    pub oid: String,
1117}
1118
1119/// Request body for `POST /cancel_all_orders`.
1120///
1121/// # References
1122/// - <https://docs.architect.exchange/api-reference/order-management/place-order>
1123#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1124pub struct CancelAllOrdersRequest {
1125    /// Optional symbol filter - only cancel orders for this symbol.
1126    #[serde(skip_serializing_if = "Option::is_none")]
1127    pub symbol: Option<Ustr>,
1128    /// Optional execution venue filter.
1129    #[serde(skip_serializing_if = "Option::is_none")]
1130    pub execution_venue: Option<Ustr>,
1131}
1132
1133impl CancelAllOrdersRequest {
1134    /// Creates a new [`CancelAllOrdersRequest`] to cancel all orders.
1135    #[must_use]
1136    pub fn new() -> Self {
1137        Self::default()
1138    }
1139
1140    /// Sets the symbol filter.
1141    #[must_use]
1142    pub fn with_symbol(mut self, symbol: Ustr) -> Self {
1143        self.symbol = Some(symbol);
1144        self
1145    }
1146
1147    /// Sets the execution venue filter.
1148    #[must_use]
1149    pub fn with_venue(mut self, venue: Ustr) -> Self {
1150        self.execution_venue = Some(venue);
1151        self
1152    }
1153}
1154
1155/// Response payload returned by `POST /cancel_all_orders`.
1156///
1157/// # References
1158/// - <https://docs.architect.exchange/api-reference/order-management/place-order>
1159#[derive(Clone, Debug, Serialize, Deserialize)]
1160pub struct AxCancelAllOrdersResponse {}
1161
1162#[cfg(test)]
1163mod tests {
1164    use rstest::rstest;
1165
1166    use super::*;
1167
1168    #[rstest]
1169    fn test_deserialize_authenticate_response() {
1170        let json = include_str!("../../test_data/http_authenticate.json");
1171        let response: AxAuthenticateResponse = serde_json::from_str(json).unwrap();
1172        assert!(response.token.starts_with("test-token"));
1173    }
1174
1175    #[rstest]
1176    fn test_deserialize_whoami_response() {
1177        let json = include_str!("../../test_data/http_get_whoami.json");
1178        let response: AxWhoAmI = serde_json::from_str(json).unwrap();
1179        assert_eq!(response.username, "test_user");
1180        assert!(response.enabled_2fa);
1181    }
1182
1183    #[rstest]
1184    fn test_deserialize_instruments_response() {
1185        let json = include_str!("../../test_data/http_get_instruments.json");
1186        let response: AxInstrumentsResponse = serde_json::from_str(json).unwrap();
1187        assert_eq!(response.instruments.len(), 3);
1188        assert_eq!(response.instruments[0].symbol, "EURUSD-PERP");
1189    }
1190
1191    #[rstest]
1192    fn test_deserialize_balances_response() {
1193        let json = include_str!("../../test_data/http_get_balances.json");
1194        let response: AxBalancesResponse = serde_json::from_str(json).unwrap();
1195        assert_eq!(response.balances.len(), 3);
1196        assert_eq!(response.balances[0].symbol, "USD");
1197    }
1198
1199    #[rstest]
1200    fn test_deserialize_positions_response() {
1201        let json = include_str!("../../test_data/http_get_positions.json");
1202        let response: AxPositionsResponse = serde_json::from_str(json).unwrap();
1203        assert_eq!(response.positions.len(), 2);
1204        assert_eq!(response.positions[0].symbol, "BTC-PERP");
1205        assert_eq!(response.positions[1].signed_quantity, -5);
1206    }
1207
1208    #[rstest]
1209    fn test_deserialize_tickers_response() {
1210        let json = include_str!("../../test_data/http_get_tickers.json");
1211        let response: AxTickersResponse = serde_json::from_str(json).unwrap();
1212        assert_eq!(response.tickers.len(), 3);
1213        assert_eq!(response.tickers[0].symbol, "EURUSD-PERP");
1214        assert!(response.tickers[0].bid.is_some());
1215        assert!(response.tickers[2].bid.is_none());
1216    }
1217
1218    #[rstest]
1219    fn test_deserialize_funding_rates_response() {
1220        let json = include_str!("../../test_data/http_get_funding_rates.json");
1221        let response: AxFundingRatesResponse = serde_json::from_str(json).unwrap();
1222        assert_eq!(response.funding_rates.len(), 2);
1223        assert_eq!(response.funding_rates[0].symbol, "JPYUSD-PERP");
1224    }
1225
1226    #[rstest]
1227    fn test_deserialize_open_orders_response() {
1228        let json = include_str!("../../test_data/http_get_open_orders.json");
1229        let response: AxOpenOrdersResponse = serde_json::from_str(json).unwrap();
1230        assert_eq!(response.orders.len(), 2);
1231        assert_eq!(response.orders[0].oid, "O-01ARZ3NDEKTSV4RRFFQ69G5FAV");
1232        assert_eq!(response.orders[0].d, AxOrderSide::Buy);
1233        assert_eq!(response.orders[0].o, AxOrderStatus::Accepted);
1234        assert_eq!(response.orders[1].xq, 300);
1235    }
1236
1237    #[rstest]
1238    fn test_deserialize_fills_response() {
1239        let json = include_str!("../../test_data/http_get_fills.json");
1240        let response: AxFillsResponse = serde_json::from_str(json).unwrap();
1241        assert_eq!(response.fills.len(), 2);
1242        assert_eq!(response.fills[0].side, AxOrderSide::Buy);
1243        assert!(response.fills[0].is_taker);
1244        assert!(!response.fills[1].is_taker);
1245    }
1246
1247    #[rstest]
1248    fn test_deserialize_candles_response() {
1249        let json = include_str!("../../test_data/http_get_candles.json");
1250        let response: AxCandlesResponse = serde_json::from_str(json).unwrap();
1251        assert_eq!(response.candles.len(), 2);
1252        assert_eq!(response.candles[0].symbol, "EURUSD-PERP");
1253        assert_eq!(response.candles[0].width, AxCandleWidth::Minutes1);
1254    }
1255
1256    #[rstest]
1257    fn test_deserialize_candle_response() {
1258        let json = include_str!("../../test_data/http_get_candle.json");
1259        let response: AxCandleResponse = serde_json::from_str(json).unwrap();
1260        assert_eq!(response.candle.symbol, "EURUSD-PERP");
1261        assert_eq!(response.candle.width, AxCandleWidth::Minutes1);
1262    }
1263
1264    #[rstest]
1265    fn test_deserialize_risk_snapshot_response() {
1266        let json = include_str!("../../test_data/http_get_risk_snapshot.json");
1267        let response: AxRiskSnapshotResponse = serde_json::from_str(json).unwrap();
1268        assert_eq!(
1269            response.risk_snapshot.user_id,
1270            "3c90c3cc-0d44-4b50-8888-8dd25736052a"
1271        );
1272        assert_eq!(response.risk_snapshot.per_symbol.len(), 2);
1273        assert!(
1274            response
1275                .risk_snapshot
1276                .per_symbol
1277                .contains_key("EURUSD-PERP")
1278        );
1279    }
1280
1281    #[rstest]
1282    fn test_deserialize_transactions_response() {
1283        let json = include_str!("../../test_data/http_get_transactions.json");
1284        let response: AxTransactionsResponse = serde_json::from_str(json).unwrap();
1285        assert_eq!(response.transactions.len(), 2);
1286        assert_eq!(response.transactions[0].transaction_type, "deposit");
1287        assert!(response.transactions[1].reference_id.is_none());
1288    }
1289
1290    #[rstest]
1291    fn test_deserialize_preview_aggressive_limit_order_response() {
1292        let json = include_str!("../../test_data/http_preview_aggressive_limit_order.json");
1293        let response: AxPreviewAggressiveLimitOrderResponse = serde_json::from_str(json).unwrap();
1294        assert_eq!(response.filled_quantity, 1000);
1295        assert_eq!(response.remaining_quantity, 0);
1296        assert!(response.limit_price.is_some());
1297        assert!(response.vwap.is_some());
1298    }
1299
1300    #[rstest]
1301    fn test_deserialize_place_order_response() {
1302        let json = include_str!("../../test_data/http_place_order.json");
1303        let response: AxPlaceOrderResponse = serde_json::from_str(json).unwrap();
1304        assert_eq!(response.oid, "O-01ARZ3NDEKTSV4RRFFQ69G5FAV");
1305    }
1306
1307    #[rstest]
1308    fn test_deserialize_cancel_order_response() {
1309        let json = include_str!("../../test_data/http_cancel_order.json");
1310        let response: AxCancelOrderResponse = serde_json::from_str(json).unwrap();
1311        assert!(response.cxl_rx);
1312    }
1313
1314    #[rstest]
1315    fn test_deserialize_cancel_all_orders_response() {
1316        let json = include_str!("../../test_data/http_cancel_all_orders.json");
1317        let _response: AxCancelAllOrdersResponse = serde_json::from_str(json).unwrap();
1318    }
1319
1320    #[rstest]
1321    fn test_deserialize_trades_response() {
1322        let json = include_str!("../../test_data/http_get_trades.json");
1323        let response: AxTradesResponse = serde_json::from_str(json).unwrap();
1324        assert_eq!(response.trades.len(), 2);
1325        assert_eq!(response.trades[0].s, "EURUSD-PERP");
1326        assert_eq!(response.trades[0].d, AxOrderSide::Buy);
1327        assert_eq!(response.trades[0].q, 100);
1328        assert_eq!(response.trades[1].d, AxOrderSide::Sell);
1329    }
1330
1331    #[rstest]
1332    fn test_deserialize_book_response() {
1333        let json = include_str!("../../test_data/http_get_book.json");
1334        let response: AxBookResponse = serde_json::from_str(json).unwrap();
1335        assert_eq!(response.book.s, "EURUSD-PERP");
1336        assert_eq!(response.book.b.len(), 3);
1337        assert_eq!(response.book.a.len(), 3);
1338        assert_eq!(response.book.b[0].q, 500);
1339        assert_eq!(response.book.a[0].q, 400);
1340    }
1341
1342    #[rstest]
1343    fn test_deserialize_order_status_query_response() {
1344        let json = include_str!("../../test_data/http_get_order_status.json");
1345        let response: AxOrderStatusQueryResponse = serde_json::from_str(json).unwrap();
1346        assert_eq!(response.status.symbol, "EURUSD-PERP");
1347        assert_eq!(response.status.order_id, "O-01ARZ3NDEKTSV4RRFFQ69G5FAV");
1348        assert_eq!(response.status.state, AxOrderStatus::PartiallyFilled);
1349        assert_eq!(response.status.clord_id, Some(12345));
1350        assert_eq!(response.status.filled_quantity, Some(300));
1351        assert_eq!(response.status.remaining_quantity, Some(700));
1352    }
1353
1354    #[rstest]
1355    fn test_deserialize_orders_response() {
1356        let json = include_str!("../../test_data/http_get_orders.json");
1357        let response: AxOrdersResponse = serde_json::from_str(json).unwrap();
1358        assert_eq!(response.orders.len(), 2);
1359        assert_eq!(response.total_count, 2);
1360        assert_eq!(response.orders[0].o, AxOrderStatus::PartiallyFilled);
1361        assert_eq!(response.orders[0].xq, 300);
1362        assert_eq!(response.orders[1].o, AxOrderStatus::Filled);
1363        assert_eq!(response.orders[1].d, AxOrderSide::Sell);
1364    }
1365
1366    #[rstest]
1367    fn test_deserialize_initial_margin_requirement_response() {
1368        let json = include_str!("../../test_data/http_initial_margin_requirement.json");
1369        let response: AxInitialMarginRequirementResponse = serde_json::from_str(json).unwrap();
1370        assert_eq!(response.im, Decimal::new(125050, 2));
1371    }
1372
1373    #[rstest]
1374    fn test_deserialize_replace_order_response() {
1375        let json = include_str!("../../test_data/http_replace_order.json");
1376        let response: AxReplaceOrderResponse = serde_json::from_str(json).unwrap();
1377        assert_eq!(response.oid, "O-01ARZ3NDEKTSV4RRFFQ69G5NEW");
1378    }
1379
1380    #[rstest]
1381    fn test_replace_order_request_serialization() {
1382        let request = ReplaceOrderRequest::new("O-01ARZ3NDEKTSV4RRFFQ69G5FAV")
1383            .with_price(Decimal::new(10550, 4))
1384            .with_quantity(200);
1385
1386        let json = serde_json::to_value(&request).unwrap();
1387        assert_eq!(json["oid"], "O-01ARZ3NDEKTSV4RRFFQ69G5FAV");
1388        assert_eq!(json["p"], "1.0550");
1389        assert_eq!(json["q"], 200);
1390        assert!(json.get("po").is_none());
1391        assert!(json.get("tif").is_none());
1392        assert!(json.get("trigger_price").is_none());
1393    }
1394
1395    #[rstest]
1396    fn test_replace_order_request_minimal() {
1397        let request = ReplaceOrderRequest::new("O-TEST");
1398        let json = serde_json::to_value(&request).unwrap();
1399        assert_eq!(json["oid"], "O-TEST");
1400        assert!(json.get("p").is_none());
1401        assert!(json.get("q").is_none());
1402    }
1403
1404    #[rstest]
1405    fn test_replace_order_request_with_trigger_price() {
1406        let request = ReplaceOrderRequest::new("O-STOP").with_trigger_price(Decimal::new(49000, 0));
1407        let json = serde_json::to_value(&request).unwrap();
1408        assert_eq!(json["oid"], "O-STOP");
1409        assert_eq!(json["trigger_price"], "49000");
1410        assert!(json.get("p").is_none());
1411        assert!(json.get("q").is_none());
1412    }
1413}