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::serialization::serialize_decimal_as_str;
22use nautilus_model::{
23    data::{Bar, Data, OrderBookDeltas},
24    events::{OrderCancelRejected, OrderRejected},
25    identifiers::ClientOrderId,
26    reports::{FillReport, OrderStatusReport},
27};
28use rust_decimal::Decimal;
29use serde::{Deserialize, Serialize};
30use ustr::Ustr;
31
32use super::error::AxWsErrorResponse;
33use crate::common::{
34    enums::{AxCandleWidth, AxMarketDataLevel, AxOrderSide, AxOrderStatus, AxTimeInForce},
35    parse::deserialize_decimal_or_zero,
36};
37
38/// Subscribe request for market data.
39///
40/// # References
41/// - <https://docs.sandbox.x.architect.co/api-reference/marketdata/md-ws>
42#[derive(Clone, Debug, Serialize, Deserialize)]
43pub struct AxMdSubscribe {
44    /// Client request ID for correlation.
45    pub request_id: i64,
46    /// Request type (always "subscribe").
47    #[serde(rename = "type")]
48    pub msg_type: String,
49    /// Instrument symbol.
50    pub symbol: String,
51    /// Market data level (LEVEL_1, LEVEL_2, LEVEL_3).
52    pub level: AxMarketDataLevel,
53}
54
55/// Unsubscribe request for market data.
56///
57/// # References
58/// - <https://docs.sandbox.x.architect.co/api-reference/marketdata/md-ws>
59#[derive(Clone, Debug, Serialize, Deserialize)]
60pub struct AxMdUnsubscribe {
61    /// Client request ID for correlation.
62    pub request_id: i64,
63    /// Request type (always "unsubscribe").
64    #[serde(rename = "type")]
65    pub msg_type: String,
66    /// Instrument symbol.
67    pub symbol: String,
68}
69
70/// Subscribe request for candle data.
71///
72/// # References
73/// - <https://docs.sandbox.x.architect.co/api-reference/marketdata/md-ws>
74#[derive(Clone, Debug, Serialize, Deserialize)]
75pub struct AxMdSubscribeCandles {
76    /// Client request ID for correlation.
77    pub request_id: i64,
78    /// Request type (always "subscribe_candles").
79    #[serde(rename = "type")]
80    pub msg_type: String,
81    /// Instrument symbol.
82    pub symbol: String,
83    /// Candle width/interval.
84    pub width: AxCandleWidth,
85}
86
87/// Unsubscribe request for candle data.
88///
89/// # References
90/// - <https://docs.sandbox.x.architect.co/api-reference/marketdata/md-ws>
91#[derive(Clone, Debug, Serialize, Deserialize)]
92pub struct AxMdUnsubscribeCandles {
93    /// Client request ID for correlation.
94    pub request_id: i64,
95    /// Request type (always "unsubscribe_candles").
96    #[serde(rename = "type")]
97    pub msg_type: String,
98    /// Instrument symbol.
99    pub symbol: String,
100    /// Candle width/interval.
101    pub width: AxCandleWidth,
102}
103
104/// Heartbeat message from market data WebSocket.
105///
106/// # References
107/// - <https://docs.sandbox.x.architect.co/api-reference/marketdata/md-ws>
108#[derive(Clone, Debug, Serialize, Deserialize)]
109pub struct AxMdHeartbeat {
110    /// Message type (always "h").
111    pub t: String,
112    /// Timestamp (Unix epoch seconds).
113    pub ts: i64,
114    /// Transaction number.
115    pub tn: i64,
116}
117
118/// Ticker/statistics message from market data WebSocket.
119///
120/// # References
121/// - <https://docs.sandbox.x.architect.co/api-reference/marketdata/md-ws>
122#[derive(Clone, Debug, Serialize, Deserialize)]
123pub struct AxMdTicker {
124    /// Message type (always "s").
125    pub t: String,
126    /// Timestamp (Unix epoch seconds).
127    pub ts: i64,
128    /// Transaction number.
129    pub tn: i64,
130    /// Instrument symbol.
131    pub s: Ustr,
132    /// Last price.
133    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
134    pub p: Decimal,
135    /// Last quantity.
136    pub q: i64,
137    /// Open price (24h).
138    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
139    pub o: Decimal,
140    /// Low price (24h).
141    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
142    pub l: Decimal,
143    /// High price (24h).
144    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
145    pub h: Decimal,
146    /// Volume (24h).
147    pub v: i64,
148    /// Open interest.
149    #[serde(default)]
150    pub oi: Option<i64>,
151}
152
153/// Trade message from market data WebSocket.
154///
155/// Note: Uses same "s" message type as ticker but with different fields.
156///
157/// # References
158/// - <https://docs.sandbox.x.architect.co/api-reference/marketdata/md-ws>
159#[derive(Clone, Debug, Serialize, Deserialize)]
160pub struct AxMdTrade {
161    /// Message type (always "s").
162    pub t: String,
163    /// Timestamp (Unix epoch seconds).
164    pub ts: i64,
165    /// Transaction number.
166    pub tn: i64,
167    /// Instrument symbol.
168    pub s: Ustr,
169    /// Trade price.
170    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
171    pub p: Decimal,
172    /// Trade quantity.
173    pub q: i64,
174    /// Trade direction: "B" (buy) or "S" (sell).
175    pub d: AxOrderSide,
176}
177
178/// Candle/OHLCV message from market data WebSocket.
179///
180/// # References
181/// - <https://docs.sandbox.x.architect.co/api-reference/marketdata/md-ws>
182#[derive(Clone, Debug, Serialize, Deserialize)]
183pub struct AxMdCandle {
184    /// Message type (always "c").
185    pub t: String,
186    /// Instrument symbol.
187    pub symbol: Ustr,
188    /// Candle timestamp (Unix epoch).
189    pub ts: i64,
190    /// Open price.
191    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
192    pub open: Decimal,
193    /// Low price.
194    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
195    pub low: Decimal,
196    /// High price.
197    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
198    pub high: Decimal,
199    /// Close price.
200    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
201    pub close: Decimal,
202    /// Total volume.
203    pub volume: i64,
204    /// Buy volume.
205    pub buy_volume: i64,
206    /// Sell volume.
207    pub sell_volume: i64,
208    /// Candle width/interval.
209    pub width: AxCandleWidth,
210}
211
212/// Price level entry in order book.
213#[derive(Clone, Debug, Serialize, Deserialize)]
214pub struct AxBookLevel {
215    /// Price at this level.
216    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
217    pub p: Decimal,
218    /// Quantity at this level.
219    pub q: i64,
220}
221
222/// Price level entry with individual order breakdown (L3).
223#[derive(Clone, Debug, Serialize, Deserialize)]
224pub struct AxBookLevelL3 {
225    /// Price at this level.
226    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
227    pub p: Decimal,
228    /// Total quantity at this level.
229    pub q: i64,
230    /// Individual order quantities at this price.
231    pub o: Vec<i64>,
232}
233
234/// Level 1 order book update (best bid/ask).
235///
236/// # References
237/// - <https://docs.sandbox.x.architect.co/api-reference/marketdata/md-ws>
238#[derive(Clone, Debug, Serialize, Deserialize)]
239pub struct AxMdBookL1 {
240    /// Message type (always "1").
241    pub t: String,
242    /// Timestamp (Unix epoch seconds).
243    pub ts: i64,
244    /// Transaction number.
245    pub tn: i64,
246    /// Instrument symbol.
247    pub s: Ustr,
248    /// Bid levels (typically just best bid).
249    pub b: Vec<AxBookLevel>,
250    /// Ask levels (typically just best ask).
251    pub a: Vec<AxBookLevel>,
252}
253
254/// Level 2 order book update (aggregated price levels).
255///
256/// # References
257/// - <https://docs.sandbox.x.architect.co/api-reference/marketdata/md-ws>
258#[derive(Clone, Debug, Serialize, Deserialize)]
259pub struct AxMdBookL2 {
260    /// Message type (always "2").
261    pub t: String,
262    /// Timestamp (Unix epoch seconds).
263    pub ts: i64,
264    /// Transaction number.
265    pub tn: i64,
266    /// Instrument symbol.
267    pub s: Ustr,
268    /// Bid levels.
269    pub b: Vec<AxBookLevel>,
270    /// Ask levels.
271    pub a: Vec<AxBookLevel>,
272}
273
274/// Level 3 order book update (individual order quantities).
275///
276/// # References
277/// - <https://docs.sandbox.x.architect.co/api-reference/marketdata/md-ws>
278#[derive(Clone, Debug, Serialize, Deserialize)]
279pub struct AxMdBookL3 {
280    /// Message type (always "3").
281    pub t: String,
282    /// Timestamp (Unix epoch seconds).
283    pub ts: i64,
284    /// Transaction number.
285    pub tn: i64,
286    /// Instrument symbol.
287    pub s: Ustr,
288    /// Bid levels with order breakdown.
289    pub b: Vec<AxBookLevelL3>,
290    /// Ask levels with order breakdown.
291    pub a: Vec<AxBookLevelL3>,
292}
293
294/// Place order request via WebSocket.
295///
296/// # References
297/// - <https://docs.sandbox.x.architect.co/api-reference/order-management/orders-ws>
298#[derive(Clone, Debug, Serialize, Deserialize)]
299pub struct AxWsPlaceOrder {
300    /// Request ID for correlation.
301    pub rid: i64,
302    /// Message type (always "p").
303    pub t: String,
304    /// Instrument symbol.
305    pub s: String,
306    /// Order side: "B" (buy) or "S" (sell).
307    pub d: AxOrderSide,
308    /// Order quantity.
309    pub q: i64,
310    /// Order price.
311    #[serde(
312        serialize_with = "serialize_decimal_as_str",
313        deserialize_with = "deserialize_decimal_or_zero"
314    )]
315    pub p: Decimal,
316    /// Time in force.
317    pub tif: AxTimeInForce,
318    /// Post-only flag (maker-or-cancel).
319    pub po: bool,
320    /// Optional order tag.
321    #[serde(skip_serializing_if = "Option::is_none")]
322    pub tag: Option<String>,
323}
324
325/// Cancel order request via WebSocket.
326///
327/// # References
328/// - <https://docs.sandbox.x.architect.co/api-reference/order-management/orders-ws>
329#[derive(Clone, Debug, Serialize, Deserialize)]
330pub struct AxWsCancelOrder {
331    /// Request ID for correlation.
332    pub rid: i64,
333    /// Message type (always "x").
334    pub t: String,
335    /// Order ID to cancel.
336    pub oid: String,
337}
338
339/// Get open orders request via WebSocket.
340///
341/// # References
342/// - <https://docs.sandbox.x.architect.co/api-reference/order-management/orders-ws>
343#[derive(Clone, Debug, Serialize, Deserialize)]
344pub struct AxWsGetOpenOrders {
345    /// Request ID for correlation.
346    pub rid: i64,
347    /// Message type (always "o").
348    pub t: String,
349}
350
351/// Place order response from WebSocket.
352///
353/// # References
354/// - <https://docs.sandbox.x.architect.co/api-reference/order-management/orders-ws>
355#[derive(Clone, Debug, Serialize, Deserialize)]
356pub struct AxWsPlaceOrderResponse {
357    /// Request ID matching the original request.
358    pub rid: i64,
359    /// Response result.
360    pub res: AxWsPlaceOrderResult,
361}
362
363/// Result payload for place order response.
364#[derive(Clone, Debug, Serialize, Deserialize)]
365pub struct AxWsPlaceOrderResult {
366    /// Order ID of the placed order.
367    pub oid: String,
368}
369
370/// Cancel order response from WebSocket.
371///
372/// # References
373/// - <https://docs.sandbox.x.architect.co/api-reference/order-management/orders-ws>
374#[derive(Clone, Debug, Serialize, Deserialize)]
375pub struct AxWsCancelOrderResponse {
376    /// Request ID matching the original request.
377    pub rid: i64,
378    /// Response result.
379    pub res: AxWsCancelOrderResult,
380}
381
382/// Result payload for cancel order response.
383#[derive(Clone, Debug, Serialize, Deserialize)]
384pub struct AxWsCancelOrderResult {
385    /// Whether the cancel request was received.
386    pub cxl_rx: bool,
387}
388
389/// Open orders response from WebSocket.
390///
391/// # References
392/// - <https://docs.sandbox.x.architect.co/api-reference/order-management/orders-ws>
393#[derive(Clone, Debug, Serialize, Deserialize)]
394pub struct AxWsOpenOrdersResponse {
395    /// Request ID matching the original request.
396    pub rid: i64,
397    /// List of open orders.
398    pub res: Vec<AxWsOrder>,
399}
400
401/// Order details in WebSocket messages.
402#[derive(Clone, Debug, Serialize, Deserialize)]
403pub struct AxWsOrder {
404    /// Order ID.
405    pub oid: String,
406    /// User ID.
407    pub u: String,
408    /// Instrument symbol.
409    pub s: Ustr,
410    /// Order price.
411    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
412    pub p: Decimal,
413    /// Order quantity.
414    pub q: i64,
415    /// Executed quantity.
416    pub xq: i64,
417    /// Remaining quantity.
418    pub rq: i64,
419    /// Order status.
420    pub o: AxOrderStatus,
421    /// Order side.
422    pub d: AxOrderSide,
423    /// Time in force.
424    pub tif: AxTimeInForce,
425    /// Timestamp (Unix epoch seconds).
426    pub ts: i64,
427    /// Transaction number.
428    pub tn: i64,
429    /// Optional order tag.
430    #[serde(default)]
431    pub tag: Option<String>,
432}
433
434/// Heartbeat event from orders WebSocket.
435///
436/// # References
437/// - <https://docs.sandbox.x.architect.co/api-reference/order-management/orders-ws>
438#[derive(Clone, Debug, Serialize, Deserialize)]
439pub struct AxWsHeartbeat {
440    /// Message type (always "h").
441    pub t: String,
442    /// Timestamp (Unix epoch seconds).
443    pub ts: i64,
444    /// Transaction number.
445    pub tn: i64,
446}
447
448/// Order acknowledged event.
449///
450/// # References
451/// - <https://docs.sandbox.x.architect.co/api-reference/order-management/orders-ws>
452#[derive(Clone, Debug, Serialize, Deserialize)]
453pub struct AxWsOrderAcknowledged {
454    /// Message type (always "n").
455    pub t: String,
456    /// Timestamp (Unix epoch seconds).
457    pub ts: i64,
458    /// Transaction number.
459    pub tn: i64,
460    /// Event ID.
461    pub eid: String,
462    /// Order details.
463    pub o: AxWsOrder,
464}
465
466/// Trade execution details for fill events.
467#[derive(Clone, Debug, Serialize, Deserialize)]
468pub struct AxWsTradeExecution {
469    /// Trade ID.
470    pub tid: String,
471    /// Instrument symbol.
472    pub s: Ustr,
473    /// Executed quantity.
474    pub q: i64,
475    /// Execution price.
476    #[serde(deserialize_with = "deserialize_decimal_or_zero")]
477    pub p: Decimal,
478    /// Trade direction.
479    pub d: AxOrderSide,
480    /// Whether this was an aggressor (taker) order.
481    pub agg: bool,
482}
483
484/// Order partially filled event.
485///
486/// # References
487/// - <https://docs.sandbox.x.architect.co/api-reference/order-management/orders-ws>
488#[derive(Clone, Debug, Serialize, Deserialize)]
489pub struct AxWsOrderPartiallyFilled {
490    /// Message type (always "p").
491    pub t: String,
492    /// Timestamp (Unix epoch seconds).
493    pub ts: i64,
494    /// Transaction number.
495    pub tn: i64,
496    /// Event ID.
497    pub eid: String,
498    /// Order details.
499    pub o: AxWsOrder,
500    /// Trade execution details.
501    pub xs: AxWsTradeExecution,
502}
503
504/// Order filled event.
505///
506/// # References
507/// - <https://docs.sandbox.x.architect.co/api-reference/order-management/orders-ws>
508#[derive(Clone, Debug, Serialize, Deserialize)]
509pub struct AxWsOrderFilled {
510    /// Message type (always "f").
511    pub t: String,
512    /// Timestamp (Unix epoch seconds).
513    pub ts: i64,
514    /// Transaction number.
515    pub tn: i64,
516    /// Event ID.
517    pub eid: String,
518    /// Order details.
519    pub o: AxWsOrder,
520    /// Trade execution details.
521    pub xs: AxWsTradeExecution,
522}
523
524/// Order canceled event.
525///
526/// # References
527/// - <https://docs.sandbox.x.architect.co/api-reference/order-management/orders-ws>
528#[derive(Clone, Debug, Serialize, Deserialize)]
529pub struct AxWsOrderCanceled {
530    /// Message type (always "c").
531    pub t: String,
532    /// Timestamp (Unix epoch seconds).
533    pub ts: i64,
534    /// Transaction number.
535    pub tn: i64,
536    /// Event ID.
537    pub eid: String,
538    /// Order details.
539    pub o: AxWsOrder,
540    /// Cancellation reason.
541    pub xr: String,
542    /// Cancellation text/description.
543    #[serde(default)]
544    pub txt: Option<String>,
545}
546
547/// Order rejected event.
548///
549/// # References
550/// - <https://docs.sandbox.x.architect.co/api-reference/order-management/orders-ws>
551#[derive(Clone, Debug, Serialize, Deserialize)]
552pub struct AxWsOrderRejected {
553    /// Message type (always "j").
554    pub t: String,
555    /// Timestamp (Unix epoch seconds).
556    pub ts: i64,
557    /// Transaction number.
558    pub tn: i64,
559    /// Event ID.
560    pub eid: String,
561    /// Order details.
562    pub o: AxWsOrder,
563    /// Rejection reason code.
564    pub r: String,
565    /// Rejection text/description.
566    #[serde(default)]
567    pub txt: Option<String>,
568}
569
570/// Order expired event.
571///
572/// # References
573/// - <https://docs.sandbox.x.architect.co/api-reference/order-management/orders-ws>
574#[derive(Clone, Debug, Serialize, Deserialize)]
575pub struct AxWsOrderExpired {
576    /// Message type (always "x").
577    pub t: String,
578    /// Timestamp (Unix epoch seconds).
579    pub ts: i64,
580    /// Transaction number.
581    pub tn: i64,
582    /// Event ID.
583    pub eid: String,
584    /// Order details.
585    pub o: AxWsOrder,
586}
587
588/// Order replaced/amended event.
589///
590/// # References
591/// - <https://docs.sandbox.x.architect.co/api-reference/order-management/orders-ws>
592#[derive(Clone, Debug, Serialize, Deserialize)]
593pub struct AxWsOrderReplaced {
594    /// Message type (always "r").
595    pub t: String,
596    /// Timestamp (Unix epoch seconds).
597    pub ts: i64,
598    /// Transaction number.
599    pub tn: i64,
600    /// Event ID.
601    pub eid: String,
602    /// Order details.
603    pub o: AxWsOrder,
604}
605
606/// Order done for day event.
607///
608/// # References
609/// - <https://docs.sandbox.x.architect.co/api-reference/order-management/orders-ws>
610#[derive(Clone, Debug, Serialize, Deserialize)]
611pub struct AxWsOrderDoneForDay {
612    /// Message type (always "d").
613    pub t: String,
614    /// Timestamp (Unix epoch seconds).
615    pub ts: i64,
616    /// Transaction number.
617    pub tn: i64,
618    /// Event ID.
619    pub eid: String,
620    /// Order details.
621    pub o: AxWsOrder,
622}
623
624/// Cancel rejected event.
625///
626/// # References
627/// - <https://docs.sandbox.x.architect.co/api-reference/order-management/orders-ws>
628#[derive(Clone, Debug, Serialize, Deserialize)]
629pub struct AxWsCancelRejected {
630    /// Message type (always "e").
631    pub t: String,
632    /// Timestamp (Unix epoch seconds).
633    pub ts: i64,
634    /// Transaction number.
635    pub tn: i64,
636    /// Order ID that failed to cancel.
637    pub oid: String,
638    /// Rejection reason code.
639    pub r: String,
640    /// Rejection text/description.
641    #[serde(default)]
642    pub txt: Option<String>,
643}
644
645/// Nautilus domain message emitted after parsing Ax WebSocket events.
646///
647/// This enum contains fully-parsed Nautilus domain objects ready for consumption
648/// by the DataClient without additional processing.
649#[derive(Debug, Clone)]
650pub enum NautilusWsMessage {
651    /// Market data (trades, quotes).
652    Data(Vec<Data>),
653    /// Order book deltas.
654    Deltas(OrderBookDeltas),
655    /// Bar/candle data.
656    Bar(Bar),
657    /// Heartbeat message.
658    Heartbeat,
659    /// Error from venue or client.
660    Error(AxWsError),
661    /// WebSocket reconnected notification.
662    Reconnected,
663}
664
665/// Nautilus domain message for Ax orders WebSocket.
666///
667/// This enum contains parsed messages from the WebSocket stream.
668/// Raw variants contain Ax-specific types for further processing.
669/// Nautilus variants contain fully-parsed domain objects.
670#[derive(Debug, Clone)]
671pub enum AxOrdersWsMessage {
672    /// Order status reports from order updates.
673    OrderStatusReports(Vec<OrderStatusReport>),
674    /// Fill reports from executions.
675    FillReports(Vec<FillReport>),
676    /// Order rejected event (from failed order submission).
677    OrderRejected(OrderRejected),
678    /// Order cancel rejected event (from failed cancel operation).
679    OrderCancelRejected(OrderCancelRejected),
680    /// Order acknowledged by exchange.
681    OrderAcknowledged(AxWsOrderAcknowledged),
682    /// Order partially filled.
683    OrderPartiallyFilled(AxWsOrderPartiallyFilled),
684    /// Order fully filled.
685    OrderFilled(AxWsOrderFilled),
686    /// Order canceled.
687    OrderCanceled(AxWsOrderCanceled),
688    /// Order rejected by exchange.
689    OrderRejectedRaw(AxWsOrderRejected),
690    /// Order expired.
691    OrderExpired(AxWsOrderExpired),
692    /// Order replaced/amended.
693    OrderReplaced(AxWsOrderReplaced),
694    /// Order done for day.
695    OrderDoneForDay(AxWsOrderDoneForDay),
696    /// Cancel request rejected.
697    CancelRejected(AxWsCancelRejected),
698    /// Place order response.
699    PlaceOrderResponse(AxWsPlaceOrderResponse),
700    /// Cancel order response.
701    CancelOrderResponse(AxWsCancelOrderResponse),
702    /// Open orders response.
703    OpenOrdersResponse(AxWsOpenOrdersResponse),
704    /// Error from venue or client.
705    Error(AxWsError),
706    /// WebSocket reconnected notification.
707    Reconnected,
708    /// Authentication successful notification.
709    Authenticated,
710}
711
712/// Represents an error event surfaced by the WebSocket client.
713#[derive(Debug, Clone)]
714pub struct AxWsError {
715    /// Error code from Ax.
716    pub code: Option<String>,
717    /// Human readable message.
718    pub message: String,
719    /// Optional request ID related to the failure.
720    pub request_id: Option<i64>,
721}
722
723impl AxWsError {
724    /// Creates a new error with the provided message.
725    #[must_use]
726    pub fn new(message: impl Into<String>) -> Self {
727        Self {
728            code: None,
729            message: message.into(),
730            request_id: None,
731        }
732    }
733
734    /// Creates a new error with code and message.
735    #[must_use]
736    pub fn with_code(code: impl Into<String>, message: impl Into<String>) -> Self {
737        Self {
738            code: Some(code.into()),
739            message: message.into(),
740            request_id: None,
741        }
742    }
743}
744
745impl From<AxWsErrorResponse> for AxWsError {
746    fn from(resp: AxWsErrorResponse) -> Self {
747        Self {
748            code: resp.code,
749            message: resp.message.unwrap_or_else(|| "Unknown error".to_string()),
750            request_id: resp.rid,
751        }
752    }
753}
754
755/// Metadata for pending order operations.
756///
757/// Used to correlate order responses with the original request.
758#[derive(Debug, Clone)]
759pub struct OrderMetadata {
760    /// Client order ID for correlation.
761    pub client_order_id: ClientOrderId,
762    /// Instrument symbol.
763    pub symbol: Ustr,
764}
765
766#[cfg(test)]
767mod tests {
768    use rstest::rstest;
769    use rust_decimal_macros::dec;
770
771    use super::*;
772
773    #[rstest]
774    fn test_md_subscribe_serialization() {
775        let msg = AxMdSubscribe {
776            request_id: 2,
777            msg_type: "subscribe".to_string(),
778            symbol: "BTCUSD-PERP".to_string(),
779            level: AxMarketDataLevel::Level2,
780        };
781        let json = serde_json::to_string(&msg).unwrap();
782        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
783
784        assert_eq!(parsed["request_id"], 2);
785        assert_eq!(parsed["type"], "subscribe");
786        assert_eq!(parsed["symbol"], "BTCUSD-PERP");
787        assert_eq!(parsed["level"], "LEVEL_2");
788    }
789
790    #[rstest]
791    fn test_md_unsubscribe_serialization() {
792        let msg = AxMdUnsubscribe {
793            request_id: 3,
794            msg_type: "unsubscribe".to_string(),
795            symbol: "BTCUSD-PERP".to_string(),
796        };
797        let json = serde_json::to_string(&msg).unwrap();
798        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
799
800        assert_eq!(parsed["request_id"], 3);
801        assert_eq!(parsed["type"], "unsubscribe");
802        assert_eq!(parsed["symbol"], "BTCUSD-PERP");
803    }
804
805    #[rstest]
806    fn test_md_subscribe_candles_serialization() {
807        let msg = AxMdSubscribeCandles {
808            request_id: 4,
809            msg_type: "subscribe_candles".to_string(),
810            symbol: "BTCUSD-PERP".to_string(),
811            width: AxCandleWidth::Minutes1,
812        };
813        let json = serde_json::to_string(&msg).unwrap();
814        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
815
816        assert_eq!(parsed["request_id"], 4);
817        assert_eq!(parsed["type"], "subscribe_candles");
818        assert_eq!(parsed["symbol"], "BTCUSD-PERP");
819        assert_eq!(parsed["width"], "1m");
820    }
821
822    #[rstest]
823    fn test_md_unsubscribe_candles_serialization() {
824        let msg = AxMdUnsubscribeCandles {
825            request_id: 5,
826            msg_type: "unsubscribe_candles".to_string(),
827            symbol: "BTCUSD-PERP".to_string(),
828            width: AxCandleWidth::Minutes1,
829        };
830        let json = serde_json::to_string(&msg).unwrap();
831        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
832
833        assert_eq!(parsed["request_id"], 5);
834        assert_eq!(parsed["type"], "unsubscribe_candles");
835        assert_eq!(parsed["symbol"], "BTCUSD-PERP");
836        assert_eq!(parsed["width"], "1m");
837    }
838
839    #[rstest]
840    fn test_ws_place_order_serialization() {
841        let msg = AxWsPlaceOrder {
842            rid: 1,
843            t: "p".to_string(),
844            s: "BTCUSD-PERP".to_string(),
845            d: AxOrderSide::Buy,
846            q: 100,
847            p: dec!(50000.50),
848            tif: AxTimeInForce::Gtc,
849            po: false,
850            tag: Some("trade001".to_string()),
851        };
852
853        let json = serde_json::to_string(&msg).unwrap();
854        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
855
856        assert_eq!(parsed["rid"], 1);
857        assert_eq!(parsed["t"], "p");
858        assert_eq!(parsed["s"], "BTCUSD-PERP");
859        assert_eq!(parsed["d"], "B");
860        assert_eq!(parsed["q"], 100);
861        assert_eq!(parsed["p"], "50000.5");
862        assert_eq!(parsed["tif"], "GTC");
863        assert_eq!(parsed["po"], false);
864        assert_eq!(parsed["tag"], "trade001");
865    }
866
867    #[rstest]
868    fn test_ws_cancel_order_serialization() {
869        let msg = AxWsCancelOrder {
870            rid: 2,
871            t: "x".to_string(),
872            oid: "O-01ARZ3NDEKTSV4RRFFQ69G5FAV".to_string(),
873        };
874        let json = serde_json::to_string(&msg).unwrap();
875        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
876
877        assert_eq!(parsed["rid"], 2);
878        assert_eq!(parsed["t"], "x");
879        assert_eq!(parsed["oid"], "O-01ARZ3NDEKTSV4RRFFQ69G5FAV");
880    }
881
882    #[rstest]
883    fn test_ws_get_open_orders_serialization() {
884        let msg = AxWsGetOpenOrders {
885            rid: 3,
886            t: "o".to_string(),
887        };
888        let json = serde_json::to_string(&msg).unwrap();
889        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
890
891        assert_eq!(parsed["rid"], 3);
892        assert_eq!(parsed["t"], "o");
893    }
894
895    #[rstest]
896    fn test_load_md_heartbeat_from_file() {
897        let json = include_str!("../../test_data/ws_md_heartbeat.json");
898        let msg: AxMdHeartbeat = serde_json::from_str(json).unwrap();
899        assert_eq!(msg.t, "h");
900    }
901
902    #[rstest]
903    fn test_load_md_ticker_from_file() {
904        let json = include_str!("../../test_data/ws_md_ticker.json");
905        let msg: AxMdTicker = serde_json::from_str(json).unwrap();
906        assert_eq!(msg.s.as_str(), "BTCUSD-PERP");
907    }
908
909    #[rstest]
910    fn test_load_md_trade_from_file() {
911        let json = include_str!("../../test_data/ws_md_trade.json");
912        let msg: AxMdTrade = serde_json::from_str(json).unwrap();
913        assert_eq!(msg.d, AxOrderSide::Buy);
914    }
915
916    #[rstest]
917    fn test_load_md_candle_from_file() {
918        let json = include_str!("../../test_data/ws_md_candle.json");
919        let msg: AxMdCandle = serde_json::from_str(json).unwrap();
920        assert_eq!(msg.width, AxCandleWidth::Minutes1);
921    }
922
923    #[rstest]
924    fn test_load_md_book_l1_from_file() {
925        let json = include_str!("../../test_data/ws_md_book_l1.json");
926        let msg: AxMdBookL1 = serde_json::from_str(json).unwrap();
927        assert_eq!(msg.b.len(), 1);
928        assert_eq!(msg.a.len(), 1);
929    }
930
931    #[rstest]
932    fn test_load_md_book_l2_from_file() {
933        let json = include_str!("../../test_data/ws_md_book_l2.json");
934        let msg: AxMdBookL2 = serde_json::from_str(json).unwrap();
935        assert_eq!(msg.b.len(), 3);
936        assert_eq!(msg.a.len(), 3);
937    }
938
939    #[rstest]
940    fn test_load_md_book_l3_from_file() {
941        let json = include_str!("../../test_data/ws_md_book_l3.json");
942        let msg: AxMdBookL3 = serde_json::from_str(json).unwrap();
943        assert_eq!(msg.b.len(), 2);
944        assert!(!msg.b[0].o.is_empty());
945    }
946
947    #[rstest]
948    fn test_load_order_place_response_from_file() {
949        let json = include_str!("../../test_data/ws_order_place_response.json");
950        let msg: AxWsPlaceOrderResponse = serde_json::from_str(json).unwrap();
951        assert_eq!(msg.res.oid, "O-01ARZ3NDEKTSV4RRFFQ69G5FAV");
952    }
953
954    #[rstest]
955    fn test_load_order_cancel_response_from_file() {
956        let json = include_str!("../../test_data/ws_order_cancel_response.json");
957        let msg: AxWsCancelOrderResponse = serde_json::from_str(json).unwrap();
958        assert!(msg.res.cxl_rx);
959    }
960
961    #[rstest]
962    fn test_load_order_open_orders_response_from_file() {
963        let json = include_str!("../../test_data/ws_order_open_orders_response.json");
964        let msg: AxWsOpenOrdersResponse = serde_json::from_str(json).unwrap();
965        assert_eq!(msg.res.len(), 1);
966    }
967
968    #[rstest]
969    fn test_load_order_heartbeat_from_file() {
970        let json = include_str!("../../test_data/ws_order_heartbeat.json");
971        let msg: AxWsHeartbeat = serde_json::from_str(json).unwrap();
972        assert_eq!(msg.t, "h");
973    }
974
975    #[rstest]
976    fn test_load_order_acknowledged_from_file() {
977        let json = include_str!("../../test_data/ws_order_acknowledged.json");
978        let msg: AxWsOrderAcknowledged = serde_json::from_str(json).unwrap();
979        assert_eq!(msg.t, "n");
980    }
981
982    #[rstest]
983    fn test_load_order_filled_from_file() {
984        let json = include_str!("../../test_data/ws_order_filled.json");
985        let msg: AxWsOrderFilled = serde_json::from_str(json).unwrap();
986        assert_eq!(msg.o.o, AxOrderStatus::Filled);
987    }
988
989    #[rstest]
990    fn test_load_order_partially_filled_from_file() {
991        let json = include_str!("../../test_data/ws_order_partially_filled.json");
992        let msg: AxWsOrderPartiallyFilled = serde_json::from_str(json).unwrap();
993        assert_eq!(msg.xs.q, 50);
994    }
995
996    #[rstest]
997    fn test_load_order_canceled_from_file() {
998        let json = include_str!("../../test_data/ws_order_canceled.json");
999        let msg: AxWsOrderCanceled = serde_json::from_str(json).unwrap();
1000        assert_eq!(msg.xr, "USER_REQUESTED");
1001    }
1002
1003    #[rstest]
1004    fn test_load_order_rejected_from_file() {
1005        let json = include_str!("../../test_data/ws_order_rejected.json");
1006        let msg: AxWsOrderRejected = serde_json::from_str(json).unwrap();
1007        assert_eq!(msg.r, "INSUFFICIENT_MARGIN");
1008    }
1009
1010    #[rstest]
1011    fn test_load_order_expired_from_file() {
1012        let json = include_str!("../../test_data/ws_order_expired.json");
1013        let msg: AxWsOrderExpired = serde_json::from_str(json).unwrap();
1014        assert_eq!(msg.o.tif, AxTimeInForce::Ioc);
1015    }
1016
1017    #[rstest]
1018    fn test_load_order_replaced_from_file() {
1019        let json = include_str!("../../test_data/ws_order_replaced.json");
1020        let msg: AxWsOrderReplaced = serde_json::from_str(json).unwrap();
1021        assert_eq!(msg.t, "r");
1022        assert_eq!(msg.o.p, dec!(50500.00));
1023    }
1024
1025    #[rstest]
1026    fn test_load_order_done_for_day_from_file() {
1027        let json = include_str!("../../test_data/ws_order_done_for_day.json");
1028        let msg: AxWsOrderDoneForDay = serde_json::from_str(json).unwrap();
1029        assert_eq!(msg.t, "d");
1030        assert_eq!(msg.o.xq, 50);
1031    }
1032
1033    #[rstest]
1034    fn test_load_cancel_rejected_from_file() {
1035        let json = include_str!("../../test_data/ws_cancel_rejected.json");
1036        let msg: AxWsCancelRejected = serde_json::from_str(json).unwrap();
1037        assert_eq!(msg.r, "ORDER_NOT_FOUND");
1038    }
1039}