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