Skip to main content

nautilus_architect_ax/websocket/
messages.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//! WebSocket message types for the AX Exchange API.
17//!
18//! This module contains request and response message structures for both
19//! market data and order management WebSocket streams.
20
21use nautilus_core::{UnixNanos, serialization::serialize_decimal_as_str};
22use nautilus_model::{
23    events::OrderInitialized,
24    identifiers::{ClientOrderId, InstrumentId, StrategyId, TraderId, VenueOrderId},
25    types::Currency,
26};
27use rust_decimal::Decimal;
28use serde::{Deserialize, Serialize};
29use ustr::Ustr;
30
31use super::error::AxWsErrorResponse;
32use crate::{
33    common::{
34        enums::{
35            AxCancelReason, AxCancelRejectionReason, AxCandleWidth, AxInstrumentState,
36            AxMarketDataLevel, AxMdRequestType, AxOrderRequestType, AxOrderSide, AxOrderStatus,
37            AxOrderWsMessageType, AxTimeInForce,
38        },
39        parse::{deserialize_decimal_or_zero, deserialize_optional_decimal_from_str},
40    },
41    http::models::{AxEstimatedFundingRate, AxOrderRejectReason, AxRepriceBehavior},
42};
43
44/// Market data WebSocket message emitted by the data handler.
45///
46/// Contains raw venue types for downstream consumers to parse
47/// into Nautilus domain objects.
48#[derive(Debug, Clone)]
49pub enum AxDataWsMessage {
50    /// Parsed market data message from the venue.
51    MdMessage(AxMdMessage),
52    /// WebSocket reconnected notification.
53    Reconnected,
54    /// A candle subscription was removed (clear cached state for this key).
55    CandleUnsubscribed {
56        /// Instrument symbol.
57        symbol: Ustr,
58        /// Candle width/interval.
59        width: AxCandleWidth,
60    },
61}
62
63/// Subscribe request for market data.
64///
65/// # References
66/// - <https://docs.architect.exchange/api-reference/marketdata/md-ws>
67#[derive(Clone, Debug, Serialize, Deserialize)]
68pub struct AxMdSubscribe {
69    /// Client request ID for correlation.
70    #[serde(alias = "request_id")]
71    pub rid: i64,
72    /// Request type (always "subscribe").
73    #[serde(rename = "type")]
74    pub msg_type: AxMdRequestType,
75    /// Instrument symbol.
76    pub symbol: Ustr,
77    /// Market data level (LEVEL_1, LEVEL_2, LEVEL_3, TRADES).
78    pub level: AxMarketDataLevel,
79    /// Whether book-level subscriptions should include trade prints.
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub trades: Option<bool>,
82    /// Whether book-level subscriptions should include ticker updates.
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub ticker: Option<bool>,
85}
86
87/// Unsubscribe request for market data.
88///
89/// # References
90/// - <https://docs.architect.exchange/api-reference/marketdata/md-ws>
91#[derive(Clone, Debug, Serialize, Deserialize)]
92pub struct AxMdUnsubscribe {
93    /// Client request ID for correlation.
94    #[serde(alias = "request_id")]
95    pub rid: i64,
96    /// Request type (always "unsubscribe").
97    #[serde(rename = "type")]
98    pub msg_type: AxMdRequestType,
99    /// Instrument symbol.
100    pub symbol: Ustr,
101}
102
103/// Subscribe request for candle data.
104///
105/// # References
106/// - <https://docs.architect.exchange/api-reference/marketdata/md-ws>
107#[derive(Clone, Debug, Serialize, Deserialize)]
108pub struct AxMdSubscribeCandles {
109    /// Client request ID for correlation.
110    #[serde(alias = "request_id")]
111    pub rid: i64,
112    /// Request type (always "subscribe_candles").
113    #[serde(rename = "type")]
114    pub msg_type: AxMdRequestType,
115    /// Instrument symbol.
116    pub symbol: Ustr,
117    /// Candle width/interval.
118    pub width: AxCandleWidth,
119}
120
121/// Unsubscribe request for candle data.
122///
123/// # References
124/// - <https://docs.architect.exchange/api-reference/marketdata/md-ws>
125#[derive(Clone, Debug, Serialize, Deserialize)]
126pub struct AxMdUnsubscribeCandles {
127    /// Client request ID for correlation.
128    #[serde(alias = "request_id")]
129    pub rid: i64,
130    /// Request type (always "unsubscribe_candles").
131    #[serde(rename = "type")]
132    pub msg_type: AxMdRequestType,
133    /// Instrument symbol.
134    pub symbol: Ustr,
135    /// Candle width/interval.
136    pub width: AxCandleWidth,
137}
138
139/// Heartbeat message from market data WebSocket.
140///
141/// # References
142/// - <https://docs.architect.exchange/api-reference/marketdata/md-ws>
143#[derive(Clone, Debug, Serialize, Deserialize)]
144pub struct AxMdHeartbeat {
145    /// Timestamp (Unix epoch seconds).
146    pub ts: i64,
147    /// Subsecond timestamp nanoseconds.
148    pub tn: i64,
149}
150
151/// Incoming market data WebSocket message.
152///
153/// Deserializes directly from JSON using the "t" field as discriminator.
154#[derive(Clone, Debug)]
155pub enum AxMdMessage {
156    BookL1(AxMdBookL1),
157    BookL2(AxMdBookL2),
158    BookL3(AxMdBookL3),
159    Ticker(Box<AxMdTicker>),
160    Trade(AxMdTrade),
161    Candle(AxMdCandle),
162    Heartbeat(AxMdHeartbeat),
163    SubscriptionResponse(AxMdSubscriptionResponse),
164    Error(AxWsError),
165}
166
167/// Subscription response from market data WebSocket.
168#[derive(Clone, Debug, Deserialize)]
169pub struct AxMdSubscriptionResponse {
170    /// Request ID for correlation.
171    #[serde(alias = "request_id")]
172    pub rid: i64,
173    /// Result payload (contains subscribed symbol or candle info).
174    #[serde(alias = "res")]
175    pub result: AxMdSubscriptionResult,
176}
177
178/// Result payload for subscription response.
179#[derive(Clone, Debug, Deserialize)]
180pub struct AxMdSubscriptionResult {
181    /// Subscribed symbol (for regular subscriptions).
182    #[serde(default)]
183    pub subscribed: Option<Ustr>,
184    /// Subscribed candle info (for candle subscriptions).
185    #[serde(default)]
186    pub subscribed_candle: Option<Ustr>,
187    /// Unsubscribed symbol (for unsubscription responses).
188    #[serde(default)]
189    pub unsubscribed: Option<Ustr>,
190    /// Unsubscribed candle info (for candle unsubscription responses).
191    #[serde(default)]
192    pub unsubscribed_candle: Option<Ustr>,
193}
194
195/// Error response from market data WebSocket with nested error object.
196#[derive(Clone, Debug, Deserialize)]
197pub struct AxMdErrorResponse {
198    /// Request ID for correlation.
199    #[serde(alias = "request_id")]
200    pub rid: Option<i64>,
201    /// Nested error object containing code and message.
202    #[serde(alias = "err")]
203    pub error: AxMdErrorInner,
204}
205
206/// Inner error object for market data WebSocket errors.
207#[derive(Clone, Debug, Deserialize)]
208pub struct AxMdErrorInner {
209    /// Error code.
210    pub code: i32,
211    /// Error message.
212    pub message: String,
213}
214
215impl From<AxMdErrorResponse> for AxWsError {
216    fn from(resp: AxMdErrorResponse) -> Self {
217        Self {
218            code: Some(resp.error.code.to_string()),
219            message: resp.error.message,
220            request_id: resp.rid,
221        }
222    }
223}
224
225/// Ticker/statistics message from market data WebSocket.
226///
227/// # References
228/// - <https://docs.architect.exchange/api-reference/marketdata/md-ws>
229#[derive(Clone, Debug, Serialize, Deserialize)]
230pub struct AxMdTicker {
231    /// Timestamp (Unix epoch seconds).
232    pub ts: i64,
233    /// Subsecond timestamp nanoseconds.
234    pub tn: i64,
235    /// Instrument symbol.
236    pub s: Ustr,
237    /// Last price (null when no recent price data, e.g. before first trade).
238    #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
239    pub p: Option<Decimal>,
240    /// Last quantity.
241    pub q: u64,
242    /// Open price (24h), null before first session open.
243    #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
244    pub o: Option<Decimal>,
245    /// Low price (24h, null when no recent price data).
246    #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
247    pub l: Option<Decimal>,
248    /// High price (24h, null when no recent price data).
249    #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
250    pub h: Option<Decimal>,
251    /// Volume (24h).
252    pub v: u64,
253    /// Open interest.
254    #[serde(default)]
255    pub oi: Option<i64>,
256    /// Mark price.
257    #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
258    pub m: Option<Decimal>,
259    /// Instrument state.
260    #[serde(default)]
261    pub i: Option<AxInstrumentState>,
262    /// Price band lower limit.
263    #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
264    pub pl: Option<Decimal>,
265    /// Price band upper limit.
266    #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
267    pub pu: Option<Decimal>,
268    /// Last settlement price.
269    #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
270    pub lsp: Option<Decimal>,
271    /// Best bid price.
272    #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
273    pub bp: Option<Decimal>,
274    /// Best ask price.
275    #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
276    pub ap: Option<Decimal>,
277    /// Last settlement timestamp (Unix epoch seconds).
278    #[serde(default)]
279    pub lst: Option<i64>,
280    /// Live funding estimate.
281    #[serde(default)]
282    pub ef: Option<AxEstimatedFundingRate>,
283}
284
285/// Trade message from market data WebSocket.
286///
287/// # References
288/// - <https://docs.architect.exchange/api-reference/marketdata/md-ws>
289#[derive(Clone, Debug, Serialize, Deserialize)]
290pub struct AxMdTrade {
291    /// Timestamp (Unix epoch seconds).
292    pub ts: i64,
293    /// Nanosecond component of the timestamp.
294    pub tn: i64,
295    /// Instrument symbol.
296    pub s: Ustr,
297    /// Trade price.
298    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
299    pub p: Decimal,
300    /// Trade quantity.
301    pub q: u64,
302    /// Aggressor side of the trade: "B" (buy) or "S" (sell).
303    pub d: AxOrderSide,
304}
305
306/// Candle/OHLCV message from market data WebSocket.
307///
308/// # References
309/// - <https://docs.architect.exchange/api-reference/marketdata/md-ws>
310#[derive(Clone, Debug, Serialize, Deserialize)]
311pub struct AxMdCandle {
312    /// Instrument symbol.
313    pub symbol: Ustr,
314    /// Candle timestamp (Unix epoch).
315    pub ts: i64,
316    /// Open price.
317    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
318    pub open: Decimal,
319    /// Low price.
320    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
321    pub low: Decimal,
322    /// High price.
323    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
324    pub high: Decimal,
325    /// Close price.
326    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
327    pub close: Decimal,
328    /// Total volume.
329    pub volume: u64,
330    /// Buy volume.
331    pub buy_volume: u64,
332    /// Sell volume.
333    pub sell_volume: u64,
334    /// Candle width/interval.
335    pub width: AxCandleWidth,
336}
337
338/// Price level entry in order book.
339#[derive(Clone, Debug, Serialize, Deserialize)]
340pub struct AxBookLevel {
341    /// Price at this level.
342    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
343    pub p: Decimal,
344    /// Quantity at this level.
345    pub q: u64,
346}
347
348/// Price level entry with individual order breakdown (L3).
349#[derive(Clone, Debug, Serialize, Deserialize)]
350pub struct AxBookLevelL3 {
351    /// Price at this level.
352    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
353    pub p: Decimal,
354    /// Total quantity at this level.
355    pub q: u64,
356    /// Individual order quantities at this price.
357    pub o: Vec<u64>,
358}
359
360/// Level 1 order book update (best bid/ask).
361///
362/// # References
363/// - <https://docs.architect.exchange/api-reference/marketdata/md-ws>
364#[derive(Clone, Debug, Serialize, Deserialize)]
365pub struct AxMdBookL1 {
366    /// Timestamp (Unix epoch seconds).
367    pub ts: i64,
368    /// Subsecond timestamp nanoseconds.
369    pub tn: i64,
370    /// Instrument symbol.
371    pub s: Ustr,
372    /// Bid levels (typically just best bid).
373    pub b: Vec<AxBookLevel>,
374    /// Ask levels (typically just best ask).
375    pub a: Vec<AxBookLevel>,
376}
377
378/// Level 2 order book update (aggregated price levels).
379///
380/// AX flags every observed frame as a full snapshot (`st: true`), so the parser rebuilds the book
381/// from each message. Incremental frames (`st: false`) are not handled.
382///
383/// # References
384/// - <https://docs.architect.exchange/api-reference/marketdata/md-ws>
385#[derive(Clone, Debug, Serialize, Deserialize)]
386pub struct AxMdBookL2 {
387    /// Timestamp (Unix epoch seconds).
388    pub ts: i64,
389    /// Subsecond timestamp nanoseconds.
390    pub tn: i64,
391    /// Instrument symbol.
392    pub s: Ustr,
393    /// Bid levels.
394    pub b: Vec<AxBookLevel>,
395    /// Ask levels.
396    pub a: Vec<AxBookLevel>,
397    /// Whether this update is a full snapshot.
398    #[serde(default)]
399    pub st: bool,
400}
401
402/// Level 3 order book update (individual order quantities).
403///
404/// AX flags every observed frame as a full snapshot (`st: true`), so the parser rebuilds the book
405/// from each message. Incremental frames (`st: false`) are not handled.
406///
407/// # References
408/// - <https://docs.architect.exchange/api-reference/marketdata/md-ws>
409#[derive(Clone, Debug, Serialize, Deserialize)]
410pub struct AxMdBookL3 {
411    /// Timestamp (Unix epoch seconds).
412    pub ts: i64,
413    /// Subsecond timestamp nanoseconds.
414    pub tn: i64,
415    /// Instrument symbol.
416    pub s: Ustr,
417    /// Bid levels with order breakdown.
418    pub b: Vec<AxBookLevelL3>,
419    /// Ask levels with order breakdown.
420    pub a: Vec<AxBookLevelL3>,
421    /// Whether this update is a full snapshot.
422    #[serde(default)]
423    pub st: bool,
424}
425
426/// Place order request via WebSocket.
427///
428/// # References
429/// - <https://docs.architect.exchange/api-reference/order-management/orders-ws>
430#[derive(Clone, Debug, Serialize, Deserialize)]
431pub struct AxWsPlaceOrder {
432    /// Request ID for correlation.
433    #[serde(alias = "request_id")]
434    pub rid: i64,
435    /// Message type (always "p").
436    pub t: AxOrderRequestType,
437    /// Instrument symbol.
438    pub s: Ustr,
439    /// Order side: "B" (buy) or "S" (sell).
440    pub d: AxOrderSide,
441    /// Order quantity.
442    pub q: u64,
443    /// Order price (limit price).
444    #[serde(
445        serialize_with = "serialize_decimal_as_str",
446        deserialize_with = "deserialize_decimal_or_zero"
447    )]
448    pub p: Decimal,
449    /// Time in force.
450    pub tif: AxTimeInForce,
451    /// Post-only flag (maker-or-cancel).
452    pub po: bool,
453    /// Optional client order ID.
454    #[serde(skip_serializing_if = "Option::is_none")]
455    pub cid: Option<u64>,
456    /// Optional order tag (max 10 alphanumeric characters).
457    #[serde(skip_serializing_if = "Option::is_none")]
458    pub tag: Option<String>,
459}
460
461/// Cancel order request via WebSocket.
462///
463/// # References
464/// - <https://docs.architect.exchange/api-reference/order-management/orders-ws>
465#[derive(Clone, Debug, Serialize, Deserialize)]
466pub struct AxWsCancelOrder {
467    /// Request ID for correlation.
468    #[serde(alias = "request_id")]
469    pub rid: i64,
470    /// Message type (always "x").
471    pub t: AxOrderRequestType,
472    /// Order ID to cancel.
473    pub oid: String,
474}
475
476/// Get open orders request via WebSocket.
477///
478/// # References
479/// - <https://docs.architect.exchange/api-reference/order-management/orders-ws>
480#[derive(Clone, Debug, Serialize, Deserialize)]
481pub struct AxWsGetOpenOrders {
482    /// Request ID for correlation.
483    #[serde(alias = "request_id")]
484    pub rid: i64,
485    /// Message type (always "o").
486    pub t: AxOrderRequestType,
487}
488
489/// Place order response from WebSocket.
490///
491/// # References
492/// - <https://docs.architect.exchange/api-reference/order-management/orders-ws>
493#[derive(Clone, Debug, Serialize, Deserialize)]
494pub struct AxWsPlaceOrderResponse {
495    /// Request ID matching the original request.
496    #[serde(alias = "request_id")]
497    pub rid: i64,
498    /// Response result.
499    #[serde(alias = "result")]
500    pub res: AxWsPlaceOrderResult,
501}
502
503/// Result payload for place order response.
504#[derive(Clone, Debug, Serialize, Deserialize)]
505pub struct AxWsPlaceOrderResult {
506    /// Order ID of the placed order.
507    pub oid: String,
508}
509
510/// Cancel order response from WebSocket.
511///
512/// # References
513/// - <https://docs.architect.exchange/api-reference/order-management/orders-ws>
514#[derive(Clone, Debug, Serialize, Deserialize)]
515pub struct AxWsCancelOrderResponse {
516    /// Request ID matching the original request.
517    #[serde(alias = "request_id")]
518    pub rid: i64,
519    /// Response result.
520    #[serde(alias = "result")]
521    pub res: AxWsCancelOrderResult,
522}
523
524/// Result payload for cancel order response.
525#[derive(Clone, Debug, Serialize, Deserialize)]
526pub struct AxWsCancelOrderResult {
527    /// Whether the cancel request was received.
528    pub cxl_rx: bool,
529}
530
531/// Open orders response from WebSocket.
532///
533/// # References
534/// - <https://docs.architect.exchange/api-reference/order-management/orders-ws>
535#[derive(Clone, Debug, Serialize, Deserialize)]
536pub struct AxWsOpenOrdersResponse {
537    /// Request ID matching the original request.
538    #[serde(alias = "request_id")]
539    pub rid: i64,
540    /// Open orders result.
541    #[serde(alias = "result")]
542    pub res: AxWsOpenOrdersResult,
543}
544
545/// Result payload for an open orders response.
546#[derive(Clone, Debug, Serialize, Deserialize)]
547pub struct AxWsOpenOrdersResult {
548    /// List of open orders.
549    pub orders: Vec<AxWsOrder>,
550}
551
552/// Error response from the Ax orders WebSocket.
553///
554/// Returned when a request fails (e.g., insufficient margin, invalid order).
555#[derive(Clone, Debug, Deserialize)]
556pub struct AxWsOrderErrorResponse {
557    /// Request ID matching the original request.
558    #[serde(alias = "request_id")]
559    pub rid: i64,
560    /// Error details.
561    #[serde(alias = "error")]
562    pub err: AxWsOrderError,
563}
564
565/// Error details in an error response.
566#[derive(Clone, Debug, Deserialize)]
567pub struct AxWsOrderError {
568    /// Error code (e.g., 400).
569    pub code: Option<i64>,
570    /// Error message.
571    pub msg: Option<String>,
572}
573
574/// List subscription response from the Ax orders WebSocket.
575///
576/// Returned when subscribing to order updates, contains a list ID for the subscription.
577#[derive(Clone, Debug, Deserialize)]
578pub struct AxWsListResponse {
579    /// Request ID matching the original request.
580    #[serde(alias = "request_id")]
581    pub rid: i64,
582    /// Response result.
583    #[serde(alias = "result")]
584    pub res: AxWsListResult,
585}
586
587/// List subscription result payload.
588#[derive(Clone, Debug, Deserialize)]
589pub struct AxWsListResult {
590    /// List subscription ID.
591    pub li: String,
592    /// Order data (null on initial subscription, array of orders otherwise).
593    #[serde(default)]
594    pub o: Option<Vec<AxWsOrder>>,
595    /// Whether orders cancel on disconnect.
596    #[serde(default)]
597    pub cod: Option<bool>,
598    /// Client heartbeat timeout accepted by the server, in seconds.
599    #[serde(default)]
600    pub chb: Option<u64>,
601}
602
603/// Order details in WebSocket messages.
604#[derive(Clone, Debug, Serialize, Deserialize)]
605pub struct AxWsOrder {
606    /// Order ID.
607    pub oid: String,
608    /// User ID.
609    pub u: Ustr,
610    /// Instrument symbol.
611    pub s: Ustr,
612    /// Order price.
613    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
614    pub p: Decimal,
615    /// Order quantity.
616    pub q: u64,
617    /// Executed quantity.
618    pub xq: u64,
619    /// Remaining quantity.
620    pub rq: u64,
621    /// Order status.
622    pub o: AxOrderStatus,
623    /// Order side.
624    pub d: AxOrderSide,
625    /// Time in force.
626    pub tif: AxTimeInForce,
627    /// Timestamp (Unix epoch seconds).
628    pub ts: i64,
629    /// Subsecond timestamp nanoseconds.
630    pub tn: i64,
631    /// Optional client order ID.
632    #[serde(default)]
633    pub cid: Option<u64>,
634    /// Optional order tag.
635    #[serde(default)]
636    pub tag: Option<String>,
637    /// Optional text/description.
638    #[serde(default)]
639    pub txt: Option<String>,
640    /// Account identifier.
641    #[serde(default)]
642    pub aid: Option<Ustr>,
643    /// Whether the order is post-only.
644    #[serde(default)]
645    pub po: bool,
646    /// Post-only reprice behavior reported by AX.
647    #[serde(default)]
648    pub rb: Option<AxRepriceBehavior>,
649    /// Order rejection reason.
650    #[serde(default)]
651    pub r: Option<AxOrderRejectReason>,
652}
653
654/// Heartbeat event from orders WebSocket.
655///
656/// # References
657/// - <https://docs.architect.exchange/api-reference/order-management/orders-ws>
658#[derive(Clone, Debug, Serialize, Deserialize)]
659pub struct AxWsHeartbeat {
660    /// Message type (always "h").
661    pub t: AxOrderWsMessageType,
662    /// Timestamp (Unix epoch seconds).
663    pub ts: i64,
664    /// Subsecond timestamp nanoseconds.
665    pub tn: i64,
666}
667
668/// Order acknowledged event.
669///
670/// # References
671/// - <https://docs.architect.exchange/api-reference/order-management/orders-ws>
672#[derive(Clone, Debug, Serialize, Deserialize)]
673pub struct AxWsOrderAcknowledged {
674    /// Timestamp (Unix epoch seconds).
675    pub ts: i64,
676    /// Subsecond timestamp nanoseconds.
677    pub tn: i64,
678    /// Event ID.
679    pub eid: String,
680    /// Order details.
681    pub o: AxWsOrder,
682}
683
684/// Trade execution details for fill events.
685#[derive(Clone, Debug, Serialize, Deserialize)]
686pub struct AxWsTradeExecution {
687    /// Trade ID.
688    pub tid: String,
689    /// Instrument symbol.
690    pub s: Ustr,
691    /// Executed quantity.
692    pub q: u64,
693    /// Execution price.
694    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
695    pub p: Decimal,
696    /// Trade direction.
697    pub d: AxOrderSide,
698    /// Whether this was an aggressor (taker) order.
699    pub agg: bool,
700    /// Account identifier.
701    #[serde(default)]
702    pub aid: Option<Ustr>,
703}
704
705/// Order partially filled event.
706///
707/// # References
708/// - <https://docs.architect.exchange/api-reference/order-management/orders-ws>
709#[derive(Clone, Debug, Serialize, Deserialize)]
710pub struct AxWsOrderPartiallyFilled {
711    /// Timestamp (Unix epoch seconds).
712    pub ts: i64,
713    /// Subsecond timestamp nanoseconds.
714    pub tn: i64,
715    /// Event ID.
716    pub eid: String,
717    /// Order details.
718    pub o: AxWsOrder,
719    /// Trade execution details.
720    pub xs: AxWsTradeExecution,
721}
722
723/// Order filled event.
724///
725/// # References
726/// - <https://docs.architect.exchange/api-reference/order-management/orders-ws>
727#[derive(Clone, Debug, Serialize, Deserialize)]
728pub struct AxWsOrderFilled {
729    /// Timestamp (Unix epoch seconds).
730    pub ts: i64,
731    /// Subsecond timestamp nanoseconds.
732    pub tn: i64,
733    /// Event ID.
734    pub eid: String,
735    /// Order details.
736    pub o: AxWsOrder,
737    /// Trade execution details.
738    pub xs: AxWsTradeExecution,
739}
740
741/// Order canceled event.
742///
743/// # References
744/// - <https://docs.architect.exchange/api-reference/order-management/orders-ws>
745#[derive(Clone, Debug, Serialize, Deserialize)]
746pub struct AxWsOrderCanceled {
747    /// Timestamp (Unix epoch seconds).
748    pub ts: i64,
749    /// Subsecond timestamp nanoseconds.
750    pub tn: i64,
751    /// Event ID.
752    pub eid: String,
753    /// Order details.
754    pub o: AxWsOrder,
755    /// Cancellation reason.
756    pub xr: AxCancelReason,
757    /// Cancellation text/description.
758    #[serde(default)]
759    pub txt: Option<String>,
760}
761
762/// Order rejected event.
763///
764/// # References
765/// - <https://docs.architect.exchange/api-reference/order-management/orders-ws>
766#[derive(Clone, Debug, Serialize, Deserialize)]
767pub struct AxWsOrderRejected {
768    /// Timestamp (Unix epoch seconds).
769    pub ts: i64,
770    /// Subsecond timestamp nanoseconds.
771    pub tn: i64,
772    /// Event ID.
773    pub eid: String,
774    /// Order details.
775    pub o: AxWsOrder,
776    /// Rejection reason code.
777    #[serde(default)]
778    pub r: Option<AxOrderRejectReason>,
779    /// Rejection text/description.
780    #[serde(default)]
781    pub txt: Option<String>,
782}
783
784/// Order expired event.
785///
786/// # References
787/// - <https://docs.architect.exchange/api-reference/order-management/orders-ws>
788#[derive(Clone, Debug, Serialize, Deserialize)]
789pub struct AxWsOrderExpired {
790    /// Timestamp (Unix epoch seconds).
791    pub ts: i64,
792    /// Subsecond timestamp nanoseconds.
793    pub tn: i64,
794    /// Event ID.
795    pub eid: String,
796    /// Order details.
797    pub o: AxWsOrder,
798}
799
800/// Order replaced/amended event.
801///
802/// # References
803/// - <https://docs.architect.exchange/api-reference/order-management/orders-ws>
804#[derive(Clone, Debug, Serialize, Deserialize)]
805pub struct AxWsOrderReplaced {
806    /// Timestamp (Unix epoch seconds).
807    pub ts: i64,
808    /// Subsecond timestamp nanoseconds.
809    pub tn: i64,
810    /// Event ID.
811    pub eid: String,
812    /// Replaced order details.
813    pub ro: Box<AxWsOrder>,
814    /// New order ID for cancel-replace; absent for an in-place amend.
815    #[serde(default)]
816    pub noid: Option<String>,
817    /// New order details for cancel-replace; absent for an in-place amend.
818    #[serde(default)]
819    pub no: Option<Box<AxWsOrder>>,
820}
821
822/// Order done for day event.
823///
824/// # References
825/// - <https://docs.architect.exchange/api-reference/order-management/orders-ws>
826#[derive(Clone, Debug, Serialize, Deserialize)]
827pub struct AxWsOrderDoneForDay {
828    /// Timestamp (Unix epoch seconds).
829    pub ts: i64,
830    /// Subsecond timestamp nanoseconds.
831    pub tn: i64,
832    /// Event ID.
833    pub eid: String,
834    /// Order details.
835    pub o: AxWsOrder,
836}
837
838/// Cancel rejected event.
839///
840/// # References
841/// - <https://docs.architect.exchange/api-reference/order-management/orders-ws>
842#[derive(Clone, Debug, Serialize, Deserialize)]
843pub struct AxWsCancelRejected {
844    /// Timestamp (Unix epoch seconds).
845    pub ts: i64,
846    /// Subsecond timestamp nanoseconds.
847    pub tn: i64,
848    /// Order ID that failed to cancel.
849    pub oid: String,
850    /// Rejection reason code.
851    pub r: AxCancelRejectionReason,
852    /// Rejection text/description.
853    #[serde(default)]
854    pub txt: Option<String>,
855    /// Client order identifier.
856    #[serde(default)]
857    pub cid: Option<u64>,
858    /// Order details, when provided by the venue.
859    #[serde(default)]
860    pub o: Option<AxWsOrder>,
861}
862
863/// Venue-level order event from the Ax orders WebSocket.
864///
865/// This enum uses serde's tagged deserialization to automatically
866/// discriminate between different event types based on the "t" field.
867#[derive(Debug, Clone, Deserialize)]
868#[serde(tag = "t")]
869pub enum AxWsOrderEvent {
870    /// Heartbeat message.
871    #[serde(rename = "h")]
872    Heartbeat,
873    /// Order acknowledged.
874    #[serde(rename = "n")]
875    Acknowledged(AxWsOrderAcknowledged),
876    /// Order partially filled.
877    #[serde(rename = "p")]
878    PartiallyFilled(AxWsOrderPartiallyFilled),
879    /// Order filled.
880    #[serde(rename = "f")]
881    Filled(AxWsOrderFilled),
882    /// Order canceled.
883    #[serde(rename = "c")]
884    Canceled(AxWsOrderCanceled),
885    /// Order rejected.
886    #[serde(rename = "j")]
887    Rejected(AxWsOrderRejected),
888    /// Order expired.
889    #[serde(rename = "x")]
890    Expired(AxWsOrderExpired),
891    /// Order replaced.
892    #[serde(rename = "r")]
893    Replaced(AxWsOrderReplaced),
894    /// Order done for day.
895    #[serde(rename = "d")]
896    DoneForDay(AxWsOrderDoneForDay),
897    /// Cancel rejected.
898    #[serde(rename = "e")]
899    CancelRejected(AxWsCancelRejected),
900}
901
902/// Internal raw response from the Ax orders WebSocket.
903///
904/// Response messages have "rid" and "res" fields.
905#[derive(Debug, Clone)]
906pub(crate) enum AxWsOrderResponse {
907    /// Place order response (res has "oid").
908    PlaceOrder(AxWsPlaceOrderResponse),
909    /// Cancel order response (res has "cxl_rx").
910    CancelOrder(AxWsCancelOrderResponse),
911    /// Open orders response (res has "orders").
912    OpenOrders(AxWsOpenOrdersResponse),
913    /// List subscription response (res has "li").
914    List(AxWsListResponse),
915}
916
917/// Internal raw message from the Ax orders WebSocket.
918#[derive(Debug, Clone)]
919pub(crate) enum AxOrdersWsFrame {
920    /// Error response message (has "rid" and "err").
921    Error(AxWsOrderErrorResponse),
922    /// Response message (has "rid" and "res").
923    Response(AxWsOrderResponse),
924    /// Event message (has "t" field).
925    Event(Box<AxWsOrderEvent>),
926}
927
928/// Messages from the Ax orders WebSocket handler.
929///
930/// Contains venue-level events and responses for downstream consumers
931/// to parse into Nautilus domain objects.
932#[derive(Debug, Clone)]
933pub enum AxOrdersWsMessage {
934    /// Venue-level order event.
935    Event(Box<AxWsOrderEvent>),
936    /// Place order response.
937    PlaceOrderResponse(AxWsPlaceOrderResponse),
938    /// Cancel order response.
939    CancelOrderResponse(AxWsCancelOrderResponse),
940    /// Open orders response.
941    OpenOrdersResponse(AxWsOpenOrdersResponse),
942    /// Error from venue or client.
943    Error(AxWsError),
944    /// WebSocket reconnected notification.
945    Reconnected,
946    /// Authentication successful notification.
947    Authenticated,
948}
949
950/// Represents an error event surfaced by the WebSocket client.
951#[derive(Debug, Clone)]
952pub struct AxWsError {
953    /// Error code from Ax.
954    pub code: Option<String>,
955    /// Human readable message.
956    pub message: String,
957    /// Optional request ID related to the failure.
958    pub request_id: Option<i64>,
959}
960
961impl AxWsError {
962    /// Creates a new error with the provided message.
963    #[must_use]
964    pub fn new(message: impl Into<String>) -> Self {
965        Self {
966            code: None,
967            message: message.into(),
968            request_id: None,
969        }
970    }
971
972    /// Creates a new error with code and message.
973    #[must_use]
974    pub fn with_code(code: impl Into<String>, message: impl Into<String>) -> Self {
975        Self {
976            code: Some(code.into()),
977            message: message.into(),
978            request_id: None,
979        }
980    }
981}
982
983impl From<AxWsOrderErrorResponse> for AxWsError {
984    fn from(resp: AxWsOrderErrorResponse) -> Self {
985        Self {
986            code: resp.err.code.map(|code| code.to_string()),
987            message: resp
988                .err
989                .msg
990                .unwrap_or_else(|| "AX request failed without an error message".to_owned()),
991            request_id: Some(resp.rid),
992        }
993    }
994}
995
996impl From<AxWsErrorResponse> for AxWsError {
997    fn from(resp: AxWsErrorResponse) -> Self {
998        Self {
999            code: resp.code,
1000            message: resp.message.unwrap_or_else(|| "Unknown error".to_string()),
1001            request_id: resp.rid,
1002        }
1003    }
1004}
1005
1006/// Identity and event context for a tracked order.
1007///
1008/// Retained through uncertain request outcomes until definitive terminal evidence.
1009#[derive(Debug, Clone)]
1010pub struct OrderMetadata {
1011    /// Trader ID for event generation.
1012    pub trader_id: TraderId,
1013    /// Strategy ID for event generation.
1014    pub strategy_id: StrategyId,
1015    /// Instrument ID for event generation.
1016    pub instrument_id: InstrumentId,
1017    /// Client order ID for correlation.
1018    pub client_order_id: ClientOrderId,
1019    /// Submitted Nautilus order, before AX-specific transformations.
1020    /// Absent for external orders registered without a cached Nautilus order.
1021    pub order_init: Option<OrderInitialized>,
1022    /// Whether acceptance is already known.
1023    pub is_accepted: bool,
1024    /// Current venue order ID, including a promoted replacement.
1025    pub venue_order_id: Option<VenueOrderId>,
1026    /// Original order timestamp.
1027    pub ts_init: UnixNanos,
1028    /// Instrument size precision for quantity conversion.
1029    pub size_precision: u8,
1030    /// Instrument price precision for price conversion.
1031    pub price_precision: u8,
1032    /// Quote currency for the instrument.
1033    pub quote_currency: Currency,
1034}
1035
1036#[cfg(test)]
1037mod tests {
1038    use rstest::rstest;
1039    use rust_decimal_macros::dec;
1040
1041    use super::{
1042        super::parse::{parse_md_message, parse_order_message},
1043        *,
1044    };
1045
1046    #[rstest]
1047    fn test_md_subscribe_serialization() {
1048        let msg = AxMdSubscribe {
1049            rid: 2,
1050            msg_type: AxMdRequestType::Subscribe,
1051            symbol: Ustr::from("EURUSD-PERP"),
1052            level: AxMarketDataLevel::Level2,
1053            trades: None,
1054            ticker: None,
1055        };
1056        let json = serde_json::to_string(&msg).unwrap();
1057        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1058
1059        assert_eq!(parsed["rid"], 2);
1060        assert_eq!(parsed["type"], "subscribe");
1061        assert_eq!(parsed["symbol"], "EURUSD-PERP");
1062        assert_eq!(parsed["level"], "LEVEL_2");
1063        assert!(parsed.get("trades").is_none());
1064        assert!(parsed.get("ticker").is_none());
1065    }
1066
1067    #[rstest]
1068    fn test_md_subscribe_book_only_serialization() {
1069        let msg = AxMdSubscribe {
1070            rid: 2,
1071            msg_type: AxMdRequestType::Subscribe,
1072            symbol: Ustr::from("EURUSD-PERP"),
1073            level: AxMarketDataLevel::Level2,
1074            trades: Some(false),
1075            ticker: Some(false),
1076        };
1077        let json = serde_json::to_string(&msg).unwrap();
1078        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1079
1080        assert_eq!(parsed["rid"], 2);
1081        assert_eq!(parsed["type"], "subscribe");
1082        assert_eq!(parsed["symbol"], "EURUSD-PERP");
1083        assert_eq!(parsed["level"], "LEVEL_2");
1084        assert_eq!(parsed["trades"], false);
1085        assert_eq!(parsed["ticker"], false);
1086    }
1087
1088    #[rstest]
1089    fn test_md_subscribe_trades_serialization() {
1090        let msg = AxMdSubscribe {
1091            rid: 2,
1092            msg_type: AxMdRequestType::Subscribe,
1093            symbol: Ustr::from("EURUSD-PERP"),
1094            level: AxMarketDataLevel::Trades,
1095            trades: None,
1096            ticker: None,
1097        };
1098        let json = serde_json::to_string(&msg).unwrap();
1099        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1100
1101        assert_eq!(parsed["rid"], 2);
1102        assert_eq!(parsed["type"], "subscribe");
1103        assert_eq!(parsed["symbol"], "EURUSD-PERP");
1104        assert_eq!(parsed["level"], "TRADES");
1105        assert!(parsed.get("trades").is_none());
1106        assert!(parsed.get("ticker").is_none());
1107    }
1108
1109    #[rstest]
1110    fn test_md_unsubscribe_serialization() {
1111        let msg = AxMdUnsubscribe {
1112            rid: 3,
1113            msg_type: AxMdRequestType::Unsubscribe,
1114            symbol: Ustr::from("EURUSD-PERP"),
1115        };
1116        let json = serde_json::to_string(&msg).unwrap();
1117        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1118
1119        assert_eq!(parsed["rid"], 3);
1120        assert_eq!(parsed["type"], "unsubscribe");
1121        assert_eq!(parsed["symbol"], "EURUSD-PERP");
1122    }
1123
1124    #[rstest]
1125    fn test_md_subscribe_candles_serialization() {
1126        let msg = AxMdSubscribeCandles {
1127            rid: 4,
1128            msg_type: AxMdRequestType::SubscribeCandles,
1129            symbol: Ustr::from("EURUSD-PERP"),
1130            width: AxCandleWidth::Minutes1,
1131        };
1132        let json = serde_json::to_string(&msg).unwrap();
1133        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1134
1135        assert_eq!(parsed["rid"], 4);
1136        assert_eq!(parsed["type"], "subscribe_candles");
1137        assert_eq!(parsed["symbol"], "EURUSD-PERP");
1138        assert_eq!(parsed["width"], "1m");
1139    }
1140
1141    #[rstest]
1142    fn test_md_unsubscribe_candles_serialization() {
1143        let msg = AxMdUnsubscribeCandles {
1144            rid: 5,
1145            msg_type: AxMdRequestType::UnsubscribeCandles,
1146            symbol: Ustr::from("EURUSD-PERP"),
1147            width: AxCandleWidth::Minutes1,
1148        };
1149        let json = serde_json::to_string(&msg).unwrap();
1150        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1151
1152        assert_eq!(parsed["rid"], 5);
1153        assert_eq!(parsed["type"], "unsubscribe_candles");
1154        assert_eq!(parsed["symbol"], "EURUSD-PERP");
1155        assert_eq!(parsed["width"], "1m");
1156    }
1157
1158    #[rstest]
1159    fn test_ws_place_order_serialization() {
1160        let msg = AxWsPlaceOrder {
1161            rid: 1,
1162            t: AxOrderRequestType::PlaceOrder,
1163            s: Ustr::from("EURUSD-PERP"),
1164            d: AxOrderSide::Buy,
1165            q: 100,
1166            p: dec!(50000.50),
1167            tif: AxTimeInForce::Gtc,
1168            po: false,
1169            tag: Some("Nautilus".to_string()),
1170            cid: Some(1234567890),
1171        };
1172
1173        let json = serde_json::to_string(&msg).unwrap();
1174        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1175
1176        assert_eq!(parsed["rid"], 1);
1177        assert_eq!(parsed["t"], "p");
1178        assert_eq!(parsed["s"], "EURUSD-PERP");
1179        assert_eq!(parsed["d"], "B");
1180        assert_eq!(parsed["q"], 100);
1181        assert_eq!(parsed["p"], "50000.50");
1182        assert_eq!(parsed["tif"], "GTC");
1183        assert_eq!(parsed["po"], false);
1184        assert_eq!(parsed["tag"], "Nautilus");
1185        assert_eq!(parsed["cid"], 1234567890);
1186        assert!(parsed.get("order_type").is_none());
1187        assert!(parsed.get("trigger_price").is_none());
1188    }
1189
1190    #[rstest]
1191    fn test_ws_cancel_order_serialization() {
1192        let msg = AxWsCancelOrder {
1193            rid: 2,
1194            t: AxOrderRequestType::CancelOrder,
1195            oid: "O-01ARZ3NDEKTSV4RRFFQ69G5FAV".to_string(),
1196        };
1197        let json = serde_json::to_string(&msg).unwrap();
1198        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1199
1200        assert_eq!(parsed["rid"], 2);
1201        assert_eq!(parsed["t"], "x");
1202        assert_eq!(parsed["oid"], "O-01ARZ3NDEKTSV4RRFFQ69G5FAV");
1203    }
1204
1205    #[rstest]
1206    fn test_ws_get_open_orders_serialization() {
1207        let msg = AxWsGetOpenOrders {
1208            rid: 3,
1209            t: AxOrderRequestType::GetOpenOrders,
1210        };
1211        let json = serde_json::to_string(&msg).unwrap();
1212        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1213
1214        assert_eq!(parsed["rid"], 3);
1215        assert_eq!(parsed["t"], "o");
1216    }
1217
1218    #[rstest]
1219    fn test_load_md_heartbeat_from_file() {
1220        let json = include_str!("../../test_data/ws_md_heartbeat.json");
1221        let msg = parse_md_message(json).unwrap();
1222        assert!(matches!(msg, AxMdMessage::Heartbeat(_)));
1223    }
1224
1225    #[rstest]
1226    fn test_load_md_ticker_from_file() {
1227        let json = include_str!("../../test_data/ws_md_ticker.json");
1228        let msg: AxMdTicker = serde_json::from_str(json).unwrap();
1229        assert_eq!(msg.s, "EURUSD-PERP");
1230        assert_eq!(msg.m, Some(dec!(50010.50)));
1231        assert_eq!(msg.i, Some(AxInstrumentState::Open));
1232    }
1233
1234    #[rstest]
1235    fn test_load_md_ticker_captured_optional_fields_default_to_none() {
1236        let json = include_str!("../../test_data/ws_md_ticker_captured.json");
1237        let msg: AxMdTicker = serde_json::from_str(json).unwrap();
1238        assert_eq!(msg.s, "EURUSD-PERP");
1239        assert_eq!(msg.m, None);
1240        assert_eq!(msg.i, None);
1241    }
1242
1243    #[rstest]
1244    fn test_load_md_ticker_null_prices_decode() {
1245        // Null p/o/l/h must still decode; mark price arrives via `m`
1246        let json = include_str!("../../test_data/ws_md_ticker_null_prices.json");
1247        let msg = parse_md_message(json).unwrap();
1248        let AxMdMessage::Ticker(ticker) = msg else {
1249            panic!("expected ticker message");
1250        };
1251        assert_eq!(ticker.s, "QQQ-PERP");
1252        assert_eq!(ticker.p, None);
1253        assert_eq!(ticker.o, None);
1254        assert_eq!(ticker.l, None);
1255        assert_eq!(ticker.h, None);
1256        assert_eq!(ticker.m, Some(dec!(716.38)));
1257        assert_eq!(ticker.i, Some(AxInstrumentState::Open));
1258    }
1259
1260    #[rstest]
1261    fn test_load_md_trade_from_file() {
1262        let json = include_str!("../../test_data/ws_md_trade.json");
1263        let msg: AxMdTrade = serde_json::from_str(json).unwrap();
1264        assert_eq!(msg.d, AxOrderSide::Buy);
1265    }
1266
1267    #[rstest]
1268    #[case(None, "missing field `d`")]
1269    #[case(Some(serde_json::Value::Null), "invalid type: null")]
1270    #[case(Some(serde_json::json!("X")), "unknown variant `X`")]
1271    #[case(Some(serde_json::json!("b")), "unknown variant `b`")]
1272    fn test_md_trade_rejects_missing_or_invalid_side(
1273        #[case] side: Option<serde_json::Value>,
1274        #[case] expected_error: &str,
1275    ) {
1276        let json = include_str!("../../test_data/ws_md_trade.json");
1277        let mut value: serde_json::Value = serde_json::from_str(json).unwrap();
1278        let fields = value.as_object_mut().unwrap();
1279        fields.remove("t");
1280        fields.remove("d");
1281        if let Some(side) = side {
1282            fields.insert("d".to_string(), side);
1283        }
1284
1285        let raw = serde_json::to_string(&value).unwrap();
1286        let fallback_raw = format!("{},\"t\":\"t\"}}", &raw[..raw.len() - 1]);
1287        let fast_raw = format!("{{\"t\":\"t\",{}", &raw[1..]);
1288
1289        serde_json::from_str::<serde_json::Value>(&fast_raw).unwrap();
1290
1291        let model_error = serde_json::from_value::<AxMdTrade>(value).unwrap_err();
1292        let message_error = parse_md_message(&fallback_raw).unwrap_err();
1293        parse_md_message(&fast_raw).unwrap_err();
1294
1295        assert!(
1296            model_error.to_string().contains(expected_error),
1297            "{model_error}"
1298        );
1299        assert!(
1300            message_error.to_string().contains(expected_error),
1301            "{message_error}"
1302        );
1303    }
1304
1305    #[rstest]
1306    fn test_load_md_candle_from_file() {
1307        let json = include_str!("../../test_data/ws_md_candle.json");
1308        let msg: AxMdCandle = serde_json::from_str(json).unwrap();
1309        assert_eq!(msg.width, AxCandleWidth::Minutes1);
1310    }
1311
1312    #[rstest]
1313    fn test_load_md_book_l1_from_file() {
1314        let json = include_str!("../../test_data/ws_md_book_l1.json");
1315        let msg: AxMdBookL1 = serde_json::from_str(json).unwrap();
1316        assert_eq!(msg.b.len(), 1);
1317        assert_eq!(msg.a.len(), 1);
1318    }
1319
1320    #[rstest]
1321    fn test_load_md_book_l2_from_file() {
1322        let json = include_str!("../../test_data/ws_md_book_l2.json");
1323        let msg: AxMdBookL2 = serde_json::from_str(json).unwrap();
1324        assert_eq!(msg.b.len(), 3);
1325        assert_eq!(msg.a.len(), 3);
1326    }
1327
1328    #[rstest]
1329    fn test_load_md_book_l3_from_file() {
1330        let json = include_str!("../../test_data/ws_md_book_l3.json");
1331        let msg: AxMdBookL3 = serde_json::from_str(json).unwrap();
1332        assert_eq!(msg.b.len(), 2);
1333        assert!(!msg.b[0].o.is_empty());
1334    }
1335
1336    #[rstest]
1337    fn test_load_order_place_response_from_file() {
1338        let json = include_str!("../../test_data/ws_order_place_response.json");
1339        let msg: AxWsPlaceOrderResponse = serde_json::from_str(json).unwrap();
1340        assert_eq!(msg.res.oid, "O-01ARZ3NDEKTSV4RRFFQ69G5FAV");
1341    }
1342
1343    #[rstest]
1344    fn test_load_order_cancel_response_from_file() {
1345        let json = include_str!("../../test_data/ws_order_cancel_response.json");
1346        let msg: AxWsCancelOrderResponse = serde_json::from_str(json).unwrap();
1347        assert!(msg.res.cxl_rx);
1348    }
1349
1350    #[rstest]
1351    fn test_load_order_open_orders_response_from_file() {
1352        let json = include_str!("../../test_data/ws_order_open_orders_response.json");
1353        let msg: AxWsOpenOrdersResponse = serde_json::from_str(json).unwrap();
1354        assert_eq!(msg.res.orders.len(), 1);
1355    }
1356
1357    #[rstest]
1358    fn test_load_order_heartbeat_from_file() {
1359        let json = include_str!("../../test_data/ws_order_heartbeat.json");
1360        let msg: AxWsHeartbeat = serde_json::from_str(json).unwrap();
1361        assert_eq!(msg.ts, 1609459200);
1362    }
1363
1364    #[rstest]
1365    fn test_load_order_acknowledged_from_file() {
1366        let json = include_str!("../../test_data/ws_order_acknowledged.json");
1367        let msg: AxWsOrderAcknowledged = serde_json::from_str(json).unwrap();
1368        assert_eq!(msg.o.oid, "O-01ARZ3NDEKTSV4RRFFQ69G5FAV");
1369    }
1370
1371    #[rstest]
1372    fn test_load_order_filled_from_file() {
1373        let json = include_str!("../../test_data/ws_order_filled.json");
1374        let msg: AxWsOrderFilled = serde_json::from_str(json).unwrap();
1375        assert_eq!(msg.o.o, AxOrderStatus::Filled);
1376        assert_eq!(msg.xs.d, AxOrderSide::Buy);
1377    }
1378
1379    #[rstest]
1380    fn test_load_order_partially_filled_from_file() {
1381        let json = include_str!("../../test_data/ws_order_partially_filled.json");
1382        let msg: AxWsOrderPartiallyFilled = serde_json::from_str(json).unwrap();
1383        assert_eq!(msg.xs.q, 50);
1384        assert_eq!(msg.xs.d, AxOrderSide::Buy);
1385    }
1386
1387    #[rstest]
1388    fn test_load_order_canceled_from_file() {
1389        let json = include_str!("../../test_data/ws_order_canceled.json");
1390        let msg: AxWsOrderCanceled = serde_json::from_str(json).unwrap();
1391        assert_eq!(msg.xr, AxCancelReason::UserRequested);
1392    }
1393
1394    #[rstest]
1395    fn test_load_order_rejected_from_file() {
1396        let json = include_str!("../../test_data/ws_order_rejected.json");
1397        let msg: AxWsOrderRejected = serde_json::from_str(json).unwrap();
1398        assert_eq!(msg.r, Some(AxOrderRejectReason::InsufficientMargin));
1399    }
1400
1401    #[rstest]
1402    fn test_load_order_expired_from_file() {
1403        let json = include_str!("../../test_data/ws_order_expired.json");
1404        let msg: AxWsOrderExpired = serde_json::from_str(json).unwrap();
1405        assert_eq!(msg.o.tif, AxTimeInForce::Ioc);
1406    }
1407
1408    #[rstest]
1409    fn test_load_order_replaced_live_shape_from_file() {
1410        let json = include_str!("../../test_data/ws_order_replaced_live.json");
1411        let msg: AxWsOrderReplaced = serde_json::from_str(json).unwrap();
1412
1413        assert_eq!(msg.noid.as_deref(), Some("O-01KWY01WX8JT4DABKC6FRS5NT4"));
1414        assert_eq!(msg.no.as_ref().unwrap().oid, "O-01KWY01WX8JT4DABKC6FRS5NT4");
1415        assert_eq!(msg.no.as_ref().unwrap().p, dec!(1.0926));
1416        assert_eq!(msg.ro.o, AxOrderStatus::Replaced);
1417    }
1418
1419    #[rstest]
1420    fn test_load_order_done_for_day_from_file() {
1421        let json = include_str!("../../test_data/ws_order_done_for_day.json");
1422        let msg: AxWsOrderDoneForDay = serde_json::from_str(json).unwrap();
1423        assert_eq!(msg.o.xq, 50);
1424    }
1425
1426    #[rstest]
1427    fn test_load_cancel_rejected_from_file() {
1428        let json = include_str!("../../test_data/ws_cancel_rejected.json");
1429        let msg: AxWsCancelRejected = serde_json::from_str(json).unwrap();
1430        assert_eq!(msg.r, AxCancelRejectionReason::OrderNotFound);
1431    }
1432
1433    #[rstest]
1434    fn test_load_order_error_response_from_file() {
1435        let json = include_str!("../../test_data/ws_order_error_response.json");
1436        let msg: AxWsOrderErrorResponse = serde_json::from_str(json).unwrap();
1437        assert_eq!(msg.rid, 1);
1438        assert_eq!(msg.err.code, Some(400));
1439        assert!(msg.err.msg.as_deref().unwrap().contains("initial margin"));
1440    }
1441
1442    #[rstest]
1443    fn test_load_order_list_response_from_file() {
1444        let json = include_str!("../../test_data/ws_order_list_response.json");
1445        let msg: AxWsListResponse = serde_json::from_str(json).unwrap();
1446        assert_eq!(msg.rid, 0);
1447        assert_eq!(msg.res.li, "01KCQM-4WP1-0000");
1448        assert!(msg.res.o.is_none());
1449    }
1450
1451    #[rstest]
1452    fn test_load_order_list_response_with_orders_from_file() {
1453        let json = include_str!("../../test_data/ws_order_list_response_with_orders.json");
1454        let msg: AxWsListResponse = serde_json::from_str(json).unwrap();
1455        assert_eq!(msg.rid, 0);
1456        assert_eq!(msg.res.li, "01KCQM-4WP1-0000");
1457        let orders = msg.res.o.unwrap();
1458        assert_eq!(orders.len(), 2);
1459        assert_eq!(orders[0].oid, "O-01KF4QM3VVJEDH98ZVNS1PCSBB");
1460        assert_eq!(orders[1].oid, "O-01KF4QM3K9FJZWYA02JF9Y1FJA");
1461    }
1462
1463    #[derive(Debug, Eq, PartialEq)]
1464    enum FrameKind {
1465        Error,
1466        ListResponse,
1467        AcknowledgedEvent,
1468        PlaceResponse,
1469        CancelResponse,
1470        OpenOrdersResponse,
1471    }
1472
1473    fn classify(frame: &AxOrdersWsFrame) -> FrameKind {
1474        match frame {
1475            AxOrdersWsFrame::Error(_) => FrameKind::Error,
1476            AxOrdersWsFrame::Response(AxWsOrderResponse::List(_)) => FrameKind::ListResponse,
1477            AxOrdersWsFrame::Response(AxWsOrderResponse::PlaceOrder(_)) => FrameKind::PlaceResponse,
1478            AxOrdersWsFrame::Response(AxWsOrderResponse::CancelOrder(_)) => {
1479                FrameKind::CancelResponse
1480            }
1481            AxOrdersWsFrame::Response(AxWsOrderResponse::OpenOrders(_)) => {
1482                FrameKind::OpenOrdersResponse
1483            }
1484            AxOrdersWsFrame::Event(e) => match **e {
1485                AxWsOrderEvent::Acknowledged(_) => FrameKind::AcknowledgedEvent,
1486                _ => panic!("unexpected event variant"),
1487            },
1488        }
1489    }
1490
1491    #[rstest]
1492    #[case::error(
1493        include_str!("../../test_data/ws_order_error_response.json"),
1494        FrameKind::Error,
1495    )]
1496    #[case::list(
1497        include_str!("../../test_data/ws_order_list_response.json"),
1498        FrameKind::ListResponse,
1499    )]
1500    #[case::acknowledged_event(
1501        include_str!("../../test_data/ws_order_acknowledged.json"),
1502        FrameKind::AcknowledgedEvent,
1503    )]
1504    #[case::place_response(
1505        include_str!("../../test_data/ws_order_place_response.json"),
1506        FrameKind::PlaceResponse,
1507    )]
1508    #[case::cancel_response(
1509        include_str!("../../test_data/ws_order_cancel_response.json"),
1510        FrameKind::CancelResponse,
1511    )]
1512    #[case::open_orders(
1513        include_str!("../../test_data/ws_order_open_orders_response.json"),
1514        FrameKind::OpenOrdersResponse,
1515    )]
1516    fn test_parse_order_message_variants(#[case] json: &str, #[case] expected: FrameKind) {
1517        let msg = parse_order_message(json).expect("should parse");
1518        assert_eq!(classify(&msg), expected);
1519    }
1520    #[rstest]
1521    fn test_ticker_missing_prices_remain_absent() {
1522        let mut value: serde_json::Value = serde_json::from_str(include_str!(
1523            "../../test_data/ws_md_ticker_null_prices.json"
1524        ))
1525        .unwrap();
1526
1527        for field in ["p", "o", "l", "h"] {
1528            value.as_object_mut().unwrap().remove(field);
1529        }
1530
1531        let ticker: AxMdTicker = serde_json::from_value(value).unwrap();
1532        assert_eq!(
1533            (ticker.p, ticker.o, ticker.l, ticker.h),
1534            (None, None, None, None)
1535        );
1536        assert_eq!(ticker.m, Some(dec!(716.38)));
1537    }
1538
1539    #[rstest]
1540    #[case(include_str!("../../test_data/captured/open-reject.json"))]
1541    #[case(include_str!("../../test_data/captured/open-backoff.json"))]
1542    #[case(include_str!("../../test_data/captured/open-top.json"))]
1543    #[case(include_str!("../../test_data/captured/open-inactive.json"))]
1544    fn test_ws_order_preserves_captured_metadata(#[case] raw: &str) {
1545        let response: serde_json::Value = serde_json::from_str(raw).unwrap();
1546        for value in response["orders"].as_array().unwrap() {
1547            let order: AxWsOrder = serde_json::from_value(value.clone()).unwrap();
1548            let actual = serde_json::to_value(order).unwrap();
1549
1550            for field in ["aid", "po", "rb", "ts", "tn", "oid", "q", "xq", "rq"] {
1551                assert_eq!(actual[field], value[field], "{field}");
1552            }
1553        }
1554    }
1555}