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