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::{
22    UnixNanos,
23    serialization::{
24        deserialize_optional_decimal_str, serialize_decimal_as_str,
25        serialize_optional_decimal_as_str,
26    },
27};
28use nautilus_model::{
29    identifiers::{ClientOrderId, InstrumentId, StrategyId, TraderId, VenueOrderId},
30    types::{Currency, Price},
31};
32use rust_decimal::Decimal;
33use serde::{Deserialize, Serialize};
34use ustr::Ustr;
35
36use super::error::AxWsErrorResponse;
37use crate::{
38    common::{
39        enums::{
40            AxCancelReason, AxCancelRejectionReason, AxCandleWidth, AxInstrumentState,
41            AxMarketDataLevel, AxMdRequestType, AxOrderRequestType, AxOrderSide, AxOrderStatus,
42            AxOrderType, AxOrderWsMessageType, AxTimeInForce,
43        },
44        parse::{
45            deserialize_decimal_or_zero, deserialize_optional_decimal_from_str,
46            deserialize_optional_decimal_or_zero,
47        },
48    },
49    http::models::AxOrderRejectReason,
50};
51
52/// Market data WebSocket message emitted by the data handler.
53///
54/// Contains raw venue types for downstream consumers to parse
55/// into Nautilus domain objects.
56#[derive(Debug, Clone)]
57pub enum AxDataWsMessage {
58    /// Parsed market data message from the venue.
59    MdMessage(AxMdMessage),
60    /// WebSocket reconnected notification.
61    Reconnected,
62    /// A candle subscription was removed (clear cached state for this key).
63    CandleUnsubscribed {
64        /// Instrument symbol.
65        symbol: Ustr,
66        /// Candle width/interval.
67        width: AxCandleWidth,
68    },
69}
70
71/// Subscribe request for market data.
72///
73/// # References
74/// - <https://docs.architect.exchange/api-reference/marketdata/md-ws>
75#[derive(Clone, Debug, Serialize, Deserialize)]
76pub struct AxMdSubscribe {
77    /// Client request ID for correlation.
78    pub rid: i64,
79    /// Request type (always "subscribe").
80    #[serde(rename = "type")]
81    pub msg_type: AxMdRequestType,
82    /// Instrument symbol.
83    pub symbol: Ustr,
84    /// Market data level (LEVEL_1, LEVEL_2, LEVEL_3).
85    pub level: AxMarketDataLevel,
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.
232    #[serde(deserialize_with = "deserialize_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).
240    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
241    pub l: Decimal,
242    /// High price (24h).
243    #[serde(deserialize_with = "deserialize_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    /// Transaction number.
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/// # References
364/// - <https://docs.architect.exchange/api-reference/marketdata/md-ws>
365#[derive(Clone, Debug, Serialize, Deserialize)]
366pub struct AxMdBookL2 {
367    /// Timestamp (Unix epoch seconds).
368    pub ts: i64,
369    /// Transaction number.
370    pub tn: i64,
371    /// Instrument symbol.
372    pub s: Ustr,
373    /// Bid levels.
374    pub b: Vec<AxBookLevel>,
375    /// Ask levels.
376    pub a: Vec<AxBookLevel>,
377    /// Whether this update is a full snapshot.
378    #[serde(default)]
379    pub st: bool,
380}
381
382/// Level 3 order book update (individual order quantities).
383///
384/// # References
385/// - <https://docs.architect.exchange/api-reference/marketdata/md-ws>
386#[derive(Clone, Debug, Serialize, Deserialize)]
387pub struct AxMdBookL3 {
388    /// Timestamp (Unix epoch seconds).
389    pub ts: i64,
390    /// Transaction number.
391    pub tn: i64,
392    /// Instrument symbol.
393    pub s: Ustr,
394    /// Bid levels with order breakdown.
395    pub b: Vec<AxBookLevelL3>,
396    /// Ask levels with order breakdown.
397    pub a: Vec<AxBookLevelL3>,
398    /// Whether this update is a full snapshot.
399    #[serde(default)]
400    pub st: bool,
401}
402
403/// Place order request via WebSocket.
404///
405/// # References
406/// - <https://docs.architect.exchange/sdk-reference/order-entry>
407#[derive(Clone, Debug, Serialize, Deserialize)]
408pub struct AxWsPlaceOrder {
409    /// Request ID for correlation.
410    pub rid: i64,
411    /// Message type (always "p").
412    pub t: AxOrderRequestType,
413    /// Instrument symbol.
414    pub s: Ustr,
415    /// Order side: "B" (buy) or "S" (sell).
416    pub d: AxOrderSide,
417    /// Order quantity.
418    pub q: u64,
419    /// Order price (limit price).
420    #[serde(
421        serialize_with = "serialize_decimal_as_str",
422        deserialize_with = "deserialize_decimal_or_zero"
423    )]
424    pub p: Decimal,
425    /// Time in force.
426    pub tif: AxTimeInForce,
427    /// Post-only flag (maker-or-cancel).
428    pub po: bool,
429    /// Optional client order ID.
430    #[serde(skip_serializing_if = "Option::is_none")]
431    pub cid: Option<u64>,
432    /// Optional order tag (max 10 alphanumeric characters).
433    #[serde(skip_serializing_if = "Option::is_none")]
434    pub tag: Option<String>,
435    /// Order type (defaults to LIMIT if not specified).
436    #[serde(skip_serializing_if = "Option::is_none")]
437    pub order_type: Option<AxOrderType>,
438    /// Trigger price for stop-loss orders.
439    #[serde(
440        skip_serializing_if = "Option::is_none",
441        serialize_with = "serialize_optional_decimal_as_str",
442        deserialize_with = "deserialize_optional_decimal_str",
443        default
444    )]
445    pub trigger_price: Option<Decimal>,
446}
447
448/// Cancel order request via WebSocket.
449///
450/// # References
451/// - <https://docs.architect.exchange/api-reference/order-management/orders-ws>
452#[derive(Clone, Debug, Serialize, Deserialize)]
453pub struct AxWsCancelOrder {
454    /// Request ID for correlation.
455    pub rid: i64,
456    /// Message type (always "x").
457    pub t: AxOrderRequestType,
458    /// Order ID to cancel.
459    pub oid: String,
460}
461
462/// Get open orders request via WebSocket.
463///
464/// # References
465/// - <https://docs.architect.exchange/api-reference/order-management/orders-ws>
466#[derive(Clone, Debug, Serialize, Deserialize)]
467pub struct AxWsGetOpenOrders {
468    /// Request ID for correlation.
469    pub rid: i64,
470    /// Message type (always "o").
471    pub t: AxOrderRequestType,
472}
473
474/// Place order response from WebSocket.
475///
476/// # References
477/// - <https://docs.architect.exchange/api-reference/order-management/orders-ws>
478#[derive(Clone, Debug, Serialize, Deserialize)]
479pub struct AxWsPlaceOrderResponse {
480    /// Request ID matching the original request.
481    pub rid: i64,
482    /// Response result.
483    pub res: AxWsPlaceOrderResult,
484}
485
486/// Result payload for place order response.
487#[derive(Clone, Debug, Serialize, Deserialize)]
488pub struct AxWsPlaceOrderResult {
489    /// Order ID of the placed order.
490    pub oid: String,
491}
492
493/// Cancel order response from WebSocket.
494///
495/// # References
496/// - <https://docs.architect.exchange/api-reference/order-management/orders-ws>
497#[derive(Clone, Debug, Serialize, Deserialize)]
498pub struct AxWsCancelOrderResponse {
499    /// Request ID matching the original request.
500    pub rid: i64,
501    /// Response result.
502    pub res: AxWsCancelOrderResult,
503}
504
505/// Result payload for cancel order response.
506#[derive(Clone, Debug, Serialize, Deserialize)]
507pub struct AxWsCancelOrderResult {
508    /// Whether the cancel request was received.
509    pub cxl_rx: bool,
510}
511
512/// Open orders response from WebSocket.
513///
514/// # References
515/// - <https://docs.architect.exchange/api-reference/order-management/orders-ws>
516#[derive(Clone, Debug, Serialize, Deserialize)]
517pub struct AxWsOpenOrdersResponse {
518    /// Request ID matching the original request.
519    pub rid: i64,
520    /// List of open orders.
521    pub res: Vec<AxWsOrder>,
522}
523
524/// Error response from the Ax orders WebSocket.
525///
526/// Returned when a request fails (e.g., insufficient margin, invalid order).
527#[derive(Clone, Debug, Deserialize)]
528pub struct AxWsOrderErrorResponse {
529    /// Request ID matching the original request.
530    pub rid: i64,
531    /// Error details.
532    pub err: AxWsOrderError,
533}
534
535/// Error details in an error response.
536#[derive(Clone, Debug, Deserialize)]
537pub struct AxWsOrderError {
538    /// Error code (e.g., 400).
539    pub code: i64,
540    /// Error message.
541    pub msg: String,
542}
543
544/// List subscription response from the Ax orders WebSocket.
545///
546/// Returned when subscribing to order updates, contains a list ID for the subscription.
547#[derive(Clone, Debug, Deserialize)]
548pub struct AxWsListResponse {
549    /// Request ID matching the original request.
550    pub rid: i64,
551    /// Response result.
552    pub res: AxWsListResult,
553}
554
555/// List subscription result payload.
556#[derive(Clone, Debug, Deserialize)]
557pub struct AxWsListResult {
558    /// List subscription ID.
559    pub li: String,
560    /// Order data (null on initial subscription, array of orders otherwise).
561    #[serde(default)]
562    pub o: Option<Vec<AxWsOrder>>,
563}
564
565/// Order details in WebSocket messages.
566#[derive(Clone, Debug, Serialize, Deserialize)]
567pub struct AxWsOrder {
568    /// Order ID.
569    pub oid: String,
570    /// User ID.
571    pub u: String,
572    /// Instrument symbol.
573    pub s: Ustr,
574    /// Order price.
575    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
576    pub p: Decimal,
577    /// Order quantity.
578    pub q: u64,
579    /// Executed quantity.
580    pub xq: u64,
581    /// Remaining quantity.
582    pub rq: u64,
583    /// Order status.
584    pub o: AxOrderStatus,
585    /// Order side.
586    pub d: AxOrderSide,
587    /// Time in force.
588    pub tif: AxTimeInForce,
589    /// Timestamp (Unix epoch seconds).
590    pub ts: i64,
591    /// Transaction number.
592    pub tn: i64,
593    /// Optional client order ID.
594    #[serde(default)]
595    pub cid: Option<u64>,
596    /// Optional order tag.
597    #[serde(default)]
598    pub tag: Option<String>,
599    /// Optional text/description.
600    #[serde(default)]
601    pub txt: Option<String>,
602}
603
604/// Heartbeat event from orders WebSocket.
605///
606/// # References
607/// - <https://docs.architect.exchange/api-reference/order-management/orders-ws>
608#[derive(Clone, Debug, Serialize, Deserialize)]
609pub struct AxWsHeartbeat {
610    /// Message type (always "h").
611    pub t: AxOrderWsMessageType,
612    /// Timestamp (Unix epoch seconds).
613    pub ts: i64,
614    /// Transaction number.
615    pub tn: i64,
616}
617
618/// Order acknowledged event.
619///
620/// # References
621/// - <https://docs.architect.exchange/api-reference/order-management/orders-ws>
622#[derive(Clone, Debug, Serialize, Deserialize)]
623pub struct AxWsOrderAcknowledged {
624    /// Timestamp (Unix epoch seconds).
625    pub ts: i64,
626    /// Transaction number.
627    pub tn: i64,
628    /// Event ID.
629    pub eid: String,
630    /// Order details.
631    pub o: AxWsOrder,
632}
633
634/// Trade execution details for fill events.
635#[derive(Clone, Debug, Serialize, Deserialize)]
636pub struct AxWsTradeExecution {
637    /// Trade ID.
638    pub tid: String,
639    /// Instrument symbol.
640    pub s: Ustr,
641    /// Executed quantity.
642    pub q: u64,
643    /// Execution price.
644    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
645    pub p: Decimal,
646    /// Trade direction.
647    pub d: AxOrderSide,
648    /// Whether this was an aggressor (taker) order.
649    pub agg: bool,
650}
651
652/// Order partially filled event.
653///
654/// # References
655/// - <https://docs.architect.exchange/api-reference/order-management/orders-ws>
656#[derive(Clone, Debug, Serialize, Deserialize)]
657pub struct AxWsOrderPartiallyFilled {
658    /// Timestamp (Unix epoch seconds).
659    pub ts: i64,
660    /// Transaction number.
661    pub tn: i64,
662    /// Event ID.
663    pub eid: String,
664    /// Order details.
665    pub o: AxWsOrder,
666    /// Trade execution details.
667    pub xs: AxWsTradeExecution,
668}
669
670/// Order filled event.
671///
672/// # References
673/// - <https://docs.architect.exchange/api-reference/order-management/orders-ws>
674#[derive(Clone, Debug, Serialize, Deserialize)]
675pub struct AxWsOrderFilled {
676    /// Timestamp (Unix epoch seconds).
677    pub ts: i64,
678    /// Transaction number.
679    pub tn: i64,
680    /// Event ID.
681    pub eid: String,
682    /// Order details.
683    pub o: AxWsOrder,
684    /// Trade execution details.
685    pub xs: AxWsTradeExecution,
686}
687
688/// Order canceled event.
689///
690/// # References
691/// - <https://docs.architect.exchange/api-reference/order-management/orders-ws>
692#[derive(Clone, Debug, Serialize, Deserialize)]
693pub struct AxWsOrderCanceled {
694    /// Timestamp (Unix epoch seconds).
695    pub ts: i64,
696    /// Transaction number.
697    pub tn: i64,
698    /// Event ID.
699    pub eid: String,
700    /// Order details.
701    pub o: AxWsOrder,
702    /// Cancellation reason.
703    pub xr: AxCancelReason,
704    /// Cancellation text/description.
705    #[serde(default)]
706    pub txt: Option<String>,
707}
708
709/// Order rejected event.
710///
711/// # References
712/// - <https://docs.architect.exchange/api-reference/order-management/orders-ws>
713#[derive(Clone, Debug, Serialize, Deserialize)]
714pub struct AxWsOrderRejected {
715    /// Timestamp (Unix epoch seconds).
716    pub ts: i64,
717    /// Transaction number.
718    pub tn: i64,
719    /// Event ID.
720    pub eid: String,
721    /// Order details.
722    pub o: AxWsOrder,
723    /// Rejection reason code.
724    #[serde(default)]
725    pub r: Option<AxOrderRejectReason>,
726    /// Rejection text/description.
727    #[serde(default)]
728    pub txt: Option<String>,
729}
730
731/// Order expired event.
732///
733/// # References
734/// - <https://docs.architect.exchange/api-reference/order-management/orders-ws>
735#[derive(Clone, Debug, Serialize, Deserialize)]
736pub struct AxWsOrderExpired {
737    /// Timestamp (Unix epoch seconds).
738    pub ts: i64,
739    /// Transaction number.
740    pub tn: i64,
741    /// Event ID.
742    pub eid: String,
743    /// Order details.
744    pub o: AxWsOrder,
745}
746
747/// Order replaced/amended event.
748///
749/// # References
750/// - <https://docs.architect.exchange/api-reference/order-management/orders-ws>
751#[derive(Clone, Debug, Serialize, Deserialize)]
752pub struct AxWsOrderReplaced {
753    /// Timestamp (Unix epoch seconds).
754    pub ts: i64,
755    /// Transaction number.
756    pub tn: i64,
757    /// Event ID.
758    pub eid: String,
759    /// Order details.
760    pub o: AxWsOrder,
761}
762
763/// Order done for day event.
764///
765/// # References
766/// - <https://docs.architect.exchange/api-reference/order-management/orders-ws>
767#[derive(Clone, Debug, Serialize, Deserialize)]
768pub struct AxWsOrderDoneForDay {
769    /// Timestamp (Unix epoch seconds).
770    pub ts: i64,
771    /// Transaction number.
772    pub tn: i64,
773    /// Event ID.
774    pub eid: String,
775    /// Order details.
776    pub o: AxWsOrder,
777}
778
779/// Cancel rejected event.
780///
781/// # References
782/// - <https://docs.architect.exchange/api-reference/order-management/orders-ws>
783#[derive(Clone, Debug, Serialize, Deserialize)]
784pub struct AxWsCancelRejected {
785    /// Timestamp (Unix epoch seconds).
786    pub ts: i64,
787    /// Transaction number.
788    pub tn: i64,
789    /// Order ID that failed to cancel.
790    pub oid: String,
791    /// Rejection reason code.
792    pub r: AxCancelRejectionReason,
793    /// Rejection text/description.
794    #[serde(default)]
795    pub txt: Option<String>,
796}
797
798/// Venue-level order event from the Ax orders WebSocket.
799///
800/// This enum uses serde's tagged deserialization to automatically
801/// discriminate between different event types based on the "t" field.
802#[derive(Debug, Clone, Deserialize)]
803#[serde(tag = "t")]
804pub enum AxWsOrderEvent {
805    /// Heartbeat message.
806    #[serde(rename = "h")]
807    Heartbeat,
808    /// Order acknowledged.
809    #[serde(rename = "n")]
810    Acknowledged(AxWsOrderAcknowledged),
811    /// Order partially filled.
812    #[serde(rename = "p")]
813    PartiallyFilled(AxWsOrderPartiallyFilled),
814    /// Order filled.
815    #[serde(rename = "f")]
816    Filled(AxWsOrderFilled),
817    /// Order canceled.
818    #[serde(rename = "c")]
819    Canceled(AxWsOrderCanceled),
820    /// Order rejected.
821    #[serde(rename = "j")]
822    Rejected(AxWsOrderRejected),
823    /// Order expired.
824    #[serde(rename = "x")]
825    Expired(AxWsOrderExpired),
826    /// Order replaced.
827    #[serde(rename = "r")]
828    Replaced(AxWsOrderReplaced),
829    /// Order done for day.
830    #[serde(rename = "d")]
831    DoneForDay(AxWsOrderDoneForDay),
832    /// Cancel rejected.
833    #[serde(rename = "e")]
834    CancelRejected(AxWsCancelRejected),
835}
836
837/// Internal raw response from the Ax orders WebSocket.
838///
839/// Response messages have "rid" and "res" fields.
840#[derive(Debug, Clone)]
841pub(crate) enum AxWsOrderResponse {
842    /// Place order response (res has "oid").
843    PlaceOrder(AxWsPlaceOrderResponse),
844    /// Cancel order response (res has "cxl_rx").
845    CancelOrder(AxWsCancelOrderResponse),
846    /// Open orders response (res is array).
847    OpenOrders(AxWsOpenOrdersResponse),
848    /// List subscription response (res has "li").
849    List(AxWsListResponse),
850}
851
852/// Internal raw message from the Ax orders WebSocket.
853#[derive(Debug, Clone)]
854pub(crate) enum AxOrdersWsFrame {
855    /// Error response message (has "rid" and "err").
856    Error(AxWsOrderErrorResponse),
857    /// Response message (has "rid" and "res").
858    Response(AxWsOrderResponse),
859    /// Event message (has "t" field).
860    Event(Box<AxWsOrderEvent>),
861}
862
863/// Messages from the Ax orders WebSocket handler.
864///
865/// Contains venue-level events and responses for downstream consumers
866/// to parse into Nautilus domain objects.
867#[derive(Debug, Clone)]
868pub enum AxOrdersWsMessage {
869    /// Venue-level order event.
870    Event(Box<AxWsOrderEvent>),
871    /// Place order response.
872    PlaceOrderResponse(AxWsPlaceOrderResponse),
873    /// Cancel order response.
874    CancelOrderResponse(AxWsCancelOrderResponse),
875    /// Open orders response.
876    OpenOrdersResponse(AxWsOpenOrdersResponse),
877    /// Error from venue or client.
878    Error(AxWsError),
879    /// WebSocket reconnected notification.
880    Reconnected,
881    /// Authentication successful notification.
882    Authenticated,
883}
884
885/// Represents an error event surfaced by the WebSocket client.
886#[derive(Debug, Clone)]
887pub struct AxWsError {
888    /// Error code from Ax.
889    pub code: Option<String>,
890    /// Human readable message.
891    pub message: String,
892    /// Optional request ID related to the failure.
893    pub request_id: Option<i64>,
894}
895
896impl AxWsError {
897    /// Creates a new error with the provided message.
898    #[must_use]
899    pub fn new(message: impl Into<String>) -> Self {
900        Self {
901            code: None,
902            message: message.into(),
903            request_id: None,
904        }
905    }
906
907    /// Creates a new error with code and message.
908    #[must_use]
909    pub fn with_code(code: impl Into<String>, message: impl Into<String>) -> Self {
910        Self {
911            code: Some(code.into()),
912            message: message.into(),
913            request_id: None,
914        }
915    }
916}
917
918impl From<AxWsOrderErrorResponse> for AxWsError {
919    fn from(resp: AxWsOrderErrorResponse) -> Self {
920        Self {
921            code: Some(resp.err.code.to_string()),
922            message: resp.err.msg,
923            request_id: Some(resp.rid),
924        }
925    }
926}
927
928impl From<AxWsErrorResponse> for AxWsError {
929    fn from(resp: AxWsErrorResponse) -> Self {
930        Self {
931            code: resp.code,
932            message: resp.message.unwrap_or_else(|| "Unknown error".to_string()),
933            request_id: resp.rid,
934        }
935    }
936}
937
938/// Metadata for pending order operations.
939///
940/// Used to correlate order responses with the original request.
941#[derive(Debug, Clone)]
942pub struct OrderMetadata {
943    /// Trader ID for event generation.
944    pub trader_id: TraderId,
945    /// Strategy ID for event generation.
946    pub strategy_id: StrategyId,
947    /// Instrument ID for event generation.
948    pub instrument_id: InstrumentId,
949    /// Client order ID for correlation.
950    pub client_order_id: ClientOrderId,
951    /// Venue order ID (populated after acknowledgment).
952    pub venue_order_id: Option<VenueOrderId>,
953    /// Original order timestamp.
954    pub ts_init: UnixNanos,
955    /// Instrument size precision for quantity conversion.
956    pub size_precision: u8,
957    /// Instrument price precision for price conversion.
958    pub price_precision: u8,
959    /// Quote currency for the instrument.
960    pub quote_currency: Currency,
961    /// Pending trigger price from a modify command (WS does not carry this).
962    pub pending_trigger_price: Option<Price>,
963}
964
965#[cfg(test)]
966mod tests {
967    use rstest::rstest;
968    use rust_decimal_macros::dec;
969
970    use super::{
971        super::parse::{parse_md_message, parse_order_message},
972        *,
973    };
974
975    #[rstest]
976    fn test_md_subscribe_serialization() {
977        let msg = AxMdSubscribe {
978            rid: 2,
979            msg_type: AxMdRequestType::Subscribe,
980            symbol: Ustr::from("EURUSD-PERP"),
981            level: AxMarketDataLevel::Level2,
982        };
983        let json = serde_json::to_string(&msg).unwrap();
984        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
985
986        assert_eq!(parsed["rid"], 2);
987        assert_eq!(parsed["type"], "subscribe");
988        assert_eq!(parsed["symbol"], "EURUSD-PERP");
989        assert_eq!(parsed["level"], "LEVEL_2");
990    }
991
992    #[rstest]
993    fn test_md_unsubscribe_serialization() {
994        let msg = AxMdUnsubscribe {
995            rid: 3,
996            msg_type: AxMdRequestType::Unsubscribe,
997            symbol: Ustr::from("EURUSD-PERP"),
998        };
999        let json = serde_json::to_string(&msg).unwrap();
1000        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1001
1002        assert_eq!(parsed["rid"], 3);
1003        assert_eq!(parsed["type"], "unsubscribe");
1004        assert_eq!(parsed["symbol"], "EURUSD-PERP");
1005    }
1006
1007    #[rstest]
1008    fn test_md_subscribe_candles_serialization() {
1009        let msg = AxMdSubscribeCandles {
1010            rid: 4,
1011            msg_type: AxMdRequestType::SubscribeCandles,
1012            symbol: Ustr::from("EURUSD-PERP"),
1013            width: AxCandleWidth::Minutes1,
1014        };
1015        let json = serde_json::to_string(&msg).unwrap();
1016        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1017
1018        assert_eq!(parsed["rid"], 4);
1019        assert_eq!(parsed["type"], "subscribe_candles");
1020        assert_eq!(parsed["symbol"], "EURUSD-PERP");
1021        assert_eq!(parsed["width"], "1m");
1022    }
1023
1024    #[rstest]
1025    fn test_md_unsubscribe_candles_serialization() {
1026        let msg = AxMdUnsubscribeCandles {
1027            rid: 5,
1028            msg_type: AxMdRequestType::UnsubscribeCandles,
1029            symbol: Ustr::from("EURUSD-PERP"),
1030            width: AxCandleWidth::Minutes1,
1031        };
1032        let json = serde_json::to_string(&msg).unwrap();
1033        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1034
1035        assert_eq!(parsed["rid"], 5);
1036        assert_eq!(parsed["type"], "unsubscribe_candles");
1037        assert_eq!(parsed["symbol"], "EURUSD-PERP");
1038        assert_eq!(parsed["width"], "1m");
1039    }
1040
1041    #[rstest]
1042    fn test_ws_place_order_serialization() {
1043        let msg = AxWsPlaceOrder {
1044            rid: 1,
1045            t: AxOrderRequestType::PlaceOrder,
1046            s: Ustr::from("EURUSD-PERP"),
1047            d: AxOrderSide::Buy,
1048            q: 100,
1049            p: dec!(50000.50),
1050            tif: AxTimeInForce::Gtc,
1051            po: false,
1052            tag: Some("Nautilus".to_string()),
1053            cid: Some(1234567890),
1054            order_type: None,
1055            trigger_price: None,
1056        };
1057
1058        let json = serde_json::to_string(&msg).unwrap();
1059        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1060
1061        assert_eq!(parsed["rid"], 1);
1062        assert_eq!(parsed["t"], "p");
1063        assert_eq!(parsed["s"], "EURUSD-PERP");
1064        assert_eq!(parsed["d"], "B");
1065        assert_eq!(parsed["q"], 100);
1066        assert_eq!(parsed["p"], "50000.50");
1067        assert_eq!(parsed["tif"], "GTC");
1068        assert_eq!(parsed["po"], false);
1069        assert_eq!(parsed["tag"], "Nautilus");
1070        assert_eq!(parsed["cid"], 1234567890);
1071        assert!(parsed.get("order_type").is_none());
1072        assert!(parsed.get("trigger_price").is_none());
1073    }
1074
1075    #[rstest]
1076    fn test_ws_place_stop_loss_order_serialization() {
1077        let msg = AxWsPlaceOrder {
1078            rid: 2,
1079            t: AxOrderRequestType::PlaceOrder,
1080            s: Ustr::from("EURUSD-PERP"),
1081            d: AxOrderSide::Sell,
1082            q: 50,
1083            p: dec!(48000.00),
1084            tif: AxTimeInForce::Gtc,
1085            po: false,
1086            tag: None,
1087            cid: None,
1088            order_type: Some(AxOrderType::StopLossLimit),
1089            trigger_price: Some(dec!(49000.00)),
1090        };
1091
1092        let json = serde_json::to_string(&msg).unwrap();
1093        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1094
1095        assert_eq!(parsed["rid"], 2);
1096        assert_eq!(parsed["order_type"], "STOP_LOSS_LIMIT");
1097        assert_eq!(parsed["trigger_price"], "49000.00");
1098    }
1099
1100    #[rstest]
1101    fn test_ws_cancel_order_serialization() {
1102        let msg = AxWsCancelOrder {
1103            rid: 2,
1104            t: AxOrderRequestType::CancelOrder,
1105            oid: "O-01ARZ3NDEKTSV4RRFFQ69G5FAV".to_string(),
1106        };
1107        let json = serde_json::to_string(&msg).unwrap();
1108        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1109
1110        assert_eq!(parsed["rid"], 2);
1111        assert_eq!(parsed["t"], "x");
1112        assert_eq!(parsed["oid"], "O-01ARZ3NDEKTSV4RRFFQ69G5FAV");
1113    }
1114
1115    #[rstest]
1116    fn test_ws_get_open_orders_serialization() {
1117        let msg = AxWsGetOpenOrders {
1118            rid: 3,
1119            t: AxOrderRequestType::GetOpenOrders,
1120        };
1121        let json = serde_json::to_string(&msg).unwrap();
1122        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1123
1124        assert_eq!(parsed["rid"], 3);
1125        assert_eq!(parsed["t"], "o");
1126    }
1127
1128    #[rstest]
1129    fn test_load_md_heartbeat_from_file() {
1130        let json = include_str!("../../test_data/ws_md_heartbeat.json");
1131        let msg = parse_md_message(json).unwrap();
1132        assert!(matches!(msg, AxMdMessage::Heartbeat(_)));
1133    }
1134
1135    #[rstest]
1136    fn test_load_md_ticker_from_file() {
1137        let json = include_str!("../../test_data/ws_md_ticker.json");
1138        let msg: AxMdTicker = serde_json::from_str(json).unwrap();
1139        assert_eq!(msg.s.as_str(), "EURUSD-PERP");
1140        assert_eq!(msg.m, Some(dec!(50010.50)));
1141        assert_eq!(msg.i, Some(AxInstrumentState::Open));
1142    }
1143
1144    #[rstest]
1145    fn test_load_md_ticker_captured_optional_fields_default_to_none() {
1146        let json = include_str!("../../test_data/ws_md_ticker_captured.json");
1147        let msg: AxMdTicker = serde_json::from_str(json).unwrap();
1148        assert_eq!(msg.s.as_str(), "EURUSD-PERP");
1149        assert_eq!(msg.m, None);
1150        assert_eq!(msg.i, None);
1151    }
1152
1153    #[rstest]
1154    fn test_load_md_trade_from_file() {
1155        let json = include_str!("../../test_data/ws_md_trade.json");
1156        let msg: AxMdTrade = serde_json::from_str(json).unwrap();
1157        assert_eq!(msg.d, Some(AxOrderSide::Buy));
1158    }
1159
1160    #[rstest]
1161    fn test_load_md_candle_from_file() {
1162        let json = include_str!("../../test_data/ws_md_candle.json");
1163        let msg: AxMdCandle = serde_json::from_str(json).unwrap();
1164        assert_eq!(msg.width, AxCandleWidth::Minutes1);
1165    }
1166
1167    #[rstest]
1168    fn test_load_md_book_l1_from_file() {
1169        let json = include_str!("../../test_data/ws_md_book_l1.json");
1170        let msg: AxMdBookL1 = serde_json::from_str(json).unwrap();
1171        assert_eq!(msg.b.len(), 1);
1172        assert_eq!(msg.a.len(), 1);
1173    }
1174
1175    #[rstest]
1176    fn test_load_md_book_l2_from_file() {
1177        let json = include_str!("../../test_data/ws_md_book_l2.json");
1178        let msg: AxMdBookL2 = serde_json::from_str(json).unwrap();
1179        assert_eq!(msg.b.len(), 3);
1180        assert_eq!(msg.a.len(), 3);
1181    }
1182
1183    #[rstest]
1184    fn test_load_md_book_l3_from_file() {
1185        let json = include_str!("../../test_data/ws_md_book_l3.json");
1186        let msg: AxMdBookL3 = serde_json::from_str(json).unwrap();
1187        assert_eq!(msg.b.len(), 2);
1188        assert!(!msg.b[0].o.is_empty());
1189    }
1190
1191    #[rstest]
1192    fn test_load_order_place_response_from_file() {
1193        let json = include_str!("../../test_data/ws_order_place_response.json");
1194        let msg: AxWsPlaceOrderResponse = serde_json::from_str(json).unwrap();
1195        assert_eq!(msg.res.oid, "O-01ARZ3NDEKTSV4RRFFQ69G5FAV");
1196    }
1197
1198    #[rstest]
1199    fn test_load_order_cancel_response_from_file() {
1200        let json = include_str!("../../test_data/ws_order_cancel_response.json");
1201        let msg: AxWsCancelOrderResponse = serde_json::from_str(json).unwrap();
1202        assert!(msg.res.cxl_rx);
1203    }
1204
1205    #[rstest]
1206    fn test_load_order_open_orders_response_from_file() {
1207        let json = include_str!("../../test_data/ws_order_open_orders_response.json");
1208        let msg: AxWsOpenOrdersResponse = serde_json::from_str(json).unwrap();
1209        assert_eq!(msg.res.len(), 1);
1210    }
1211
1212    #[rstest]
1213    fn test_load_order_heartbeat_from_file() {
1214        let json = include_str!("../../test_data/ws_order_heartbeat.json");
1215        let msg: AxWsHeartbeat = serde_json::from_str(json).unwrap();
1216        assert_eq!(msg.ts, 1609459200);
1217    }
1218
1219    #[rstest]
1220    fn test_load_order_acknowledged_from_file() {
1221        let json = include_str!("../../test_data/ws_order_acknowledged.json");
1222        let msg: AxWsOrderAcknowledged = serde_json::from_str(json).unwrap();
1223        assert_eq!(msg.o.oid, "O-01ARZ3NDEKTSV4RRFFQ69G5FAV");
1224    }
1225
1226    #[rstest]
1227    fn test_load_order_filled_from_file() {
1228        let json = include_str!("../../test_data/ws_order_filled.json");
1229        let msg: AxWsOrderFilled = serde_json::from_str(json).unwrap();
1230        assert_eq!(msg.o.o, AxOrderStatus::Filled);
1231    }
1232
1233    #[rstest]
1234    fn test_load_order_partially_filled_from_file() {
1235        let json = include_str!("../../test_data/ws_order_partially_filled.json");
1236        let msg: AxWsOrderPartiallyFilled = serde_json::from_str(json).unwrap();
1237        assert_eq!(msg.xs.q, 50);
1238    }
1239
1240    #[rstest]
1241    fn test_load_order_canceled_from_file() {
1242        let json = include_str!("../../test_data/ws_order_canceled.json");
1243        let msg: AxWsOrderCanceled = serde_json::from_str(json).unwrap();
1244        assert_eq!(msg.xr, AxCancelReason::UserRequested);
1245    }
1246
1247    #[rstest]
1248    fn test_load_order_rejected_from_file() {
1249        let json = include_str!("../../test_data/ws_order_rejected.json");
1250        let msg: AxWsOrderRejected = serde_json::from_str(json).unwrap();
1251        assert_eq!(msg.r, Some(AxOrderRejectReason::InsufficientMargin));
1252    }
1253
1254    #[rstest]
1255    fn test_load_order_expired_from_file() {
1256        let json = include_str!("../../test_data/ws_order_expired.json");
1257        let msg: AxWsOrderExpired = serde_json::from_str(json).unwrap();
1258        assert_eq!(msg.o.tif, AxTimeInForce::Ioc);
1259    }
1260
1261    #[rstest]
1262    fn test_load_order_replaced_from_file() {
1263        let json = include_str!("../../test_data/ws_order_replaced.json");
1264        let msg: AxWsOrderReplaced = serde_json::from_str(json).unwrap();
1265        assert_eq!(msg.o.p, dec!(50500.00));
1266    }
1267
1268    #[rstest]
1269    fn test_load_order_done_for_day_from_file() {
1270        let json = include_str!("../../test_data/ws_order_done_for_day.json");
1271        let msg: AxWsOrderDoneForDay = serde_json::from_str(json).unwrap();
1272        assert_eq!(msg.o.xq, 50);
1273    }
1274
1275    #[rstest]
1276    fn test_load_cancel_rejected_from_file() {
1277        let json = include_str!("../../test_data/ws_cancel_rejected.json");
1278        let msg: AxWsCancelRejected = serde_json::from_str(json).unwrap();
1279        assert_eq!(msg.r, AxCancelRejectionReason::OrderNotFound);
1280    }
1281
1282    #[rstest]
1283    fn test_load_order_error_response_from_file() {
1284        let json = include_str!("../../test_data/ws_order_error_response.json");
1285        let msg: AxWsOrderErrorResponse = serde_json::from_str(json).unwrap();
1286        assert_eq!(msg.rid, 1);
1287        assert_eq!(msg.err.code, 400);
1288        assert!(msg.err.msg.contains("initial margin"));
1289    }
1290
1291    #[rstest]
1292    fn test_load_order_list_response_from_file() {
1293        let json = include_str!("../../test_data/ws_order_list_response.json");
1294        let msg: AxWsListResponse = serde_json::from_str(json).unwrap();
1295        assert_eq!(msg.rid, 0);
1296        assert_eq!(msg.res.li, "01KCQM-4WP1-0000");
1297        assert!(msg.res.o.is_none());
1298    }
1299
1300    #[rstest]
1301    fn test_load_order_list_response_with_orders_from_file() {
1302        let json = include_str!("../../test_data/ws_order_list_response_with_orders.json");
1303        let msg: AxWsListResponse = serde_json::from_str(json).unwrap();
1304        assert_eq!(msg.rid, 0);
1305        assert_eq!(msg.res.li, "01KCQM-4WP1-0000");
1306        let orders = msg.res.o.unwrap();
1307        assert_eq!(orders.len(), 2);
1308        assert_eq!(orders[0].oid, "O-01KF4QM3VVJEDH98ZVNS1PCSBB");
1309        assert_eq!(orders[1].oid, "O-01KF4QM3K9FJZWYA02JF9Y1FJA");
1310    }
1311
1312    #[derive(Debug, Eq, PartialEq)]
1313    enum FrameKind {
1314        Error,
1315        ListResponse,
1316        AcknowledgedEvent,
1317        PlaceResponse,
1318        CancelResponse,
1319        OpenOrdersResponse,
1320    }
1321
1322    fn classify(frame: &AxOrdersWsFrame) -> FrameKind {
1323        match frame {
1324            AxOrdersWsFrame::Error(_) => FrameKind::Error,
1325            AxOrdersWsFrame::Response(AxWsOrderResponse::List(_)) => FrameKind::ListResponse,
1326            AxOrdersWsFrame::Response(AxWsOrderResponse::PlaceOrder(_)) => FrameKind::PlaceResponse,
1327            AxOrdersWsFrame::Response(AxWsOrderResponse::CancelOrder(_)) => {
1328                FrameKind::CancelResponse
1329            }
1330            AxOrdersWsFrame::Response(AxWsOrderResponse::OpenOrders(_)) => {
1331                FrameKind::OpenOrdersResponse
1332            }
1333            AxOrdersWsFrame::Event(e) => match **e {
1334                AxWsOrderEvent::Acknowledged(_) => FrameKind::AcknowledgedEvent,
1335                _ => panic!("unexpected event variant"),
1336            },
1337        }
1338    }
1339
1340    #[rstest]
1341    #[case::error(
1342        include_str!("../../test_data/ws_order_error_response.json"),
1343        FrameKind::Error,
1344    )]
1345    #[case::list(
1346        include_str!("../../test_data/ws_order_list_response.json"),
1347        FrameKind::ListResponse,
1348    )]
1349    #[case::acknowledged_event(
1350        include_str!("../../test_data/ws_order_acknowledged.json"),
1351        FrameKind::AcknowledgedEvent,
1352    )]
1353    #[case::place_response(
1354        include_str!("../../test_data/ws_order_place_response.json"),
1355        FrameKind::PlaceResponse,
1356    )]
1357    #[case::cancel_response(
1358        include_str!("../../test_data/ws_order_cancel_response.json"),
1359        FrameKind::CancelResponse,
1360    )]
1361    #[case::open_orders(
1362        include_str!("../../test_data/ws_order_open_orders_response.json"),
1363        FrameKind::OpenOrdersResponse,
1364    )]
1365    fn test_parse_order_message_variants(#[case] json: &str, #[case] expected: FrameKind) {
1366        let msg = parse_order_message(json).expect("should parse");
1367        assert_eq!(classify(&msg), expected);
1368    }
1369}