1use 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#[derive(Debug, Clone)]
57pub enum AxDataWsMessage {
58 MdMessage(AxMdMessage),
60 Reconnected,
62 CandleUnsubscribed {
64 symbol: Ustr,
66 width: AxCandleWidth,
68 },
69}
70
71#[derive(Clone, Debug, Serialize, Deserialize)]
76pub struct AxMdSubscribe {
77 pub rid: i64,
79 #[serde(rename = "type")]
81 pub msg_type: AxMdRequestType,
82 pub symbol: Ustr,
84 pub level: AxMarketDataLevel,
86}
87
88#[derive(Clone, Debug, Serialize, Deserialize)]
93pub struct AxMdUnsubscribe {
94 pub rid: i64,
96 #[serde(rename = "type")]
98 pub msg_type: AxMdRequestType,
99 pub symbol: Ustr,
101}
102
103#[derive(Clone, Debug, Serialize, Deserialize)]
108pub struct AxMdSubscribeCandles {
109 pub rid: i64,
111 #[serde(rename = "type")]
113 pub msg_type: AxMdRequestType,
114 pub symbol: Ustr,
116 pub width: AxCandleWidth,
118}
119
120#[derive(Clone, Debug, Serialize, Deserialize)]
125pub struct AxMdUnsubscribeCandles {
126 pub rid: i64,
128 #[serde(rename = "type")]
130 pub msg_type: AxMdRequestType,
131 pub symbol: Ustr,
133 pub width: AxCandleWidth,
135}
136
137#[derive(Clone, Debug, Serialize, Deserialize)]
142pub struct AxMdHeartbeat {
143 pub ts: i64,
145 pub tn: i64,
147}
148
149#[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#[derive(Clone, Debug, Deserialize)]
167pub struct AxMdSubscriptionResponse {
168 pub rid: i64,
170 pub result: AxMdSubscriptionResult,
172}
173
174#[derive(Clone, Debug, Deserialize)]
176pub struct AxMdSubscriptionResult {
177 #[serde(default)]
179 pub subscribed: Option<String>,
180 #[serde(default)]
182 pub subscribed_candle: Option<String>,
183 #[serde(default)]
185 pub unsubscribed: Option<String>,
186 #[serde(default)]
188 pub unsubscribed_candle: Option<String>,
189}
190
191#[derive(Clone, Debug, Deserialize)]
193pub struct AxMdErrorResponse {
194 pub rid: Option<i64>,
196 pub error: AxMdErrorInner,
198}
199
200#[derive(Clone, Debug, Deserialize)]
202pub struct AxMdErrorInner {
203 pub code: i32,
205 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#[derive(Clone, Debug, Serialize, Deserialize)]
224pub struct AxMdTicker {
225 pub ts: i64,
227 pub tn: i64,
229 pub s: Ustr,
231 #[serde(deserialize_with = "deserialize_optional_decimal_or_zero")]
233 pub p: Decimal,
234 pub q: u64,
236 #[serde(deserialize_with = "deserialize_optional_decimal_or_zero")]
238 pub o: Decimal,
239 #[serde(deserialize_with = "deserialize_optional_decimal_or_zero")]
241 pub l: Decimal,
242 #[serde(deserialize_with = "deserialize_optional_decimal_or_zero")]
244 pub h: Decimal,
245 pub v: u64,
247 #[serde(default)]
249 pub oi: Option<i64>,
250 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
252 pub m: Option<Decimal>,
253 #[serde(default)]
255 pub i: Option<AxInstrumentState>,
256 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
258 pub pl: Option<Decimal>,
259 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
261 pub pu: Option<Decimal>,
262 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
264 pub lsp: Option<Decimal>,
265}
266
267#[derive(Clone, Debug, Serialize, Deserialize)]
272pub struct AxMdTrade {
273 pub ts: i64,
275 pub tn: i64,
277 pub s: Ustr,
279 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
281 pub p: Decimal,
282 pub q: u64,
284 #[serde(default)]
286 pub d: Option<AxOrderSide>,
287}
288
289#[derive(Clone, Debug, Serialize, Deserialize)]
294pub struct AxMdCandle {
295 pub symbol: Ustr,
297 pub ts: i64,
299 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
301 pub open: Decimal,
302 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
304 pub low: Decimal,
305 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
307 pub high: Decimal,
308 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
310 pub close: Decimal,
311 pub volume: u64,
313 pub buy_volume: u64,
315 pub sell_volume: u64,
317 pub width: AxCandleWidth,
319}
320
321#[derive(Clone, Debug, Serialize, Deserialize)]
323pub struct AxBookLevel {
324 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
326 pub p: Decimal,
327 pub q: u64,
329}
330
331#[derive(Clone, Debug, Serialize, Deserialize)]
333pub struct AxBookLevelL3 {
334 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
336 pub p: Decimal,
337 pub q: u64,
339 pub o: Vec<u64>,
341}
342
343#[derive(Clone, Debug, Serialize, Deserialize)]
348pub struct AxMdBookL1 {
349 pub ts: i64,
351 pub tn: i64,
353 pub s: Ustr,
355 pub b: Vec<AxBookLevel>,
357 pub a: Vec<AxBookLevel>,
359}
360
361#[derive(Clone, Debug, Serialize, Deserialize)]
366pub struct AxMdBookL2 {
367 pub ts: i64,
369 pub tn: i64,
371 pub s: Ustr,
373 pub b: Vec<AxBookLevel>,
375 pub a: Vec<AxBookLevel>,
377 #[serde(default)]
379 pub st: bool,
380}
381
382#[derive(Clone, Debug, Serialize, Deserialize)]
387pub struct AxMdBookL3 {
388 pub ts: i64,
390 pub tn: i64,
392 pub s: Ustr,
394 pub b: Vec<AxBookLevelL3>,
396 pub a: Vec<AxBookLevelL3>,
398 #[serde(default)]
400 pub st: bool,
401}
402
403#[derive(Clone, Debug, Serialize, Deserialize)]
408pub struct AxWsPlaceOrder {
409 pub rid: i64,
411 pub t: AxOrderRequestType,
413 pub s: Ustr,
415 pub d: AxOrderSide,
417 pub q: u64,
419 #[serde(
421 serialize_with = "serialize_decimal_as_str",
422 deserialize_with = "deserialize_decimal_or_zero"
423 )]
424 pub p: Decimal,
425 pub tif: AxTimeInForce,
427 pub po: bool,
429 #[serde(skip_serializing_if = "Option::is_none")]
431 pub cid: Option<u64>,
432 #[serde(skip_serializing_if = "Option::is_none")]
434 pub tag: Option<String>,
435 #[serde(skip_serializing_if = "Option::is_none")]
437 pub order_type: Option<AxOrderType>,
438 #[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#[derive(Clone, Debug, Serialize, Deserialize)]
453pub struct AxWsCancelOrder {
454 pub rid: i64,
456 pub t: AxOrderRequestType,
458 pub oid: String,
460}
461
462#[derive(Clone, Debug, Serialize, Deserialize)]
467pub struct AxWsGetOpenOrders {
468 pub rid: i64,
470 pub t: AxOrderRequestType,
472}
473
474#[derive(Clone, Debug, Serialize, Deserialize)]
479pub struct AxWsPlaceOrderResponse {
480 pub rid: i64,
482 pub res: AxWsPlaceOrderResult,
484}
485
486#[derive(Clone, Debug, Serialize, Deserialize)]
488pub struct AxWsPlaceOrderResult {
489 pub oid: String,
491}
492
493#[derive(Clone, Debug, Serialize, Deserialize)]
498pub struct AxWsCancelOrderResponse {
499 pub rid: i64,
501 pub res: AxWsCancelOrderResult,
503}
504
505#[derive(Clone, Debug, Serialize, Deserialize)]
507pub struct AxWsCancelOrderResult {
508 pub cxl_rx: bool,
510}
511
512#[derive(Clone, Debug, Serialize, Deserialize)]
517pub struct AxWsOpenOrdersResponse {
518 pub rid: i64,
520 pub res: Vec<AxWsOrder>,
522}
523
524#[derive(Clone, Debug, Deserialize)]
528pub struct AxWsOrderErrorResponse {
529 pub rid: i64,
531 pub err: AxWsOrderError,
533}
534
535#[derive(Clone, Debug, Deserialize)]
537pub struct AxWsOrderError {
538 pub code: i64,
540 pub msg: String,
542}
543
544#[derive(Clone, Debug, Deserialize)]
548pub struct AxWsListResponse {
549 pub rid: i64,
551 pub res: AxWsListResult,
553}
554
555#[derive(Clone, Debug, Deserialize)]
557pub struct AxWsListResult {
558 pub li: String,
560 #[serde(default)]
562 pub o: Option<Vec<AxWsOrder>>,
563}
564
565#[derive(Clone, Debug, Serialize, Deserialize)]
567pub struct AxWsOrder {
568 pub oid: String,
570 pub u: String,
572 pub s: Ustr,
574 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
576 pub p: Decimal,
577 pub q: u64,
579 pub xq: u64,
581 pub rq: u64,
583 pub o: AxOrderStatus,
585 pub d: AxOrderSide,
587 pub tif: AxTimeInForce,
589 pub ts: i64,
591 pub tn: i64,
593 #[serde(default)]
595 pub cid: Option<u64>,
596 #[serde(default)]
598 pub tag: Option<String>,
599 #[serde(default)]
601 pub txt: Option<String>,
602}
603
604#[derive(Clone, Debug, Serialize, Deserialize)]
609pub struct AxWsHeartbeat {
610 pub t: AxOrderWsMessageType,
612 pub ts: i64,
614 pub tn: i64,
616}
617
618#[derive(Clone, Debug, Serialize, Deserialize)]
623pub struct AxWsOrderAcknowledged {
624 pub ts: i64,
626 pub tn: i64,
628 pub eid: String,
630 pub o: AxWsOrder,
632}
633
634#[derive(Clone, Debug, Serialize, Deserialize)]
636pub struct AxWsTradeExecution {
637 pub tid: String,
639 pub s: Ustr,
641 pub q: u64,
643 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
645 pub p: Decimal,
646 pub d: AxOrderSide,
648 pub agg: bool,
650}
651
652#[derive(Clone, Debug, Serialize, Deserialize)]
657pub struct AxWsOrderPartiallyFilled {
658 pub ts: i64,
660 pub tn: i64,
662 pub eid: String,
664 pub o: AxWsOrder,
666 pub xs: AxWsTradeExecution,
668}
669
670#[derive(Clone, Debug, Serialize, Deserialize)]
675pub struct AxWsOrderFilled {
676 pub ts: i64,
678 pub tn: i64,
680 pub eid: String,
682 pub o: AxWsOrder,
684 pub xs: AxWsTradeExecution,
686}
687
688#[derive(Clone, Debug, Serialize, Deserialize)]
693pub struct AxWsOrderCanceled {
694 pub ts: i64,
696 pub tn: i64,
698 pub eid: String,
700 pub o: AxWsOrder,
702 pub xr: AxCancelReason,
704 #[serde(default)]
706 pub txt: Option<String>,
707}
708
709#[derive(Clone, Debug, Serialize, Deserialize)]
714pub struct AxWsOrderRejected {
715 pub ts: i64,
717 pub tn: i64,
719 pub eid: String,
721 pub o: AxWsOrder,
723 #[serde(default)]
725 pub r: Option<AxOrderRejectReason>,
726 #[serde(default)]
728 pub txt: Option<String>,
729}
730
731#[derive(Clone, Debug, Serialize, Deserialize)]
736pub struct AxWsOrderExpired {
737 pub ts: i64,
739 pub tn: i64,
741 pub eid: String,
743 pub o: AxWsOrder,
745}
746
747#[derive(Clone, Debug, Serialize, Deserialize)]
752pub struct AxWsOrderReplaced {
753 pub ts: i64,
755 pub tn: i64,
757 pub eid: String,
759 pub o: AxWsOrder,
761}
762
763#[derive(Clone, Debug, Serialize, Deserialize)]
768pub struct AxWsOrderDoneForDay {
769 pub ts: i64,
771 pub tn: i64,
773 pub eid: String,
775 pub o: AxWsOrder,
777}
778
779#[derive(Clone, Debug, Serialize, Deserialize)]
784pub struct AxWsCancelRejected {
785 pub ts: i64,
787 pub tn: i64,
789 pub oid: String,
791 pub r: AxCancelRejectionReason,
793 #[serde(default)]
795 pub txt: Option<String>,
796}
797
798#[derive(Debug, Clone, Deserialize)]
803#[serde(tag = "t")]
804pub enum AxWsOrderEvent {
805 #[serde(rename = "h")]
807 Heartbeat,
808 #[serde(rename = "n")]
810 Acknowledged(AxWsOrderAcknowledged),
811 #[serde(rename = "p")]
813 PartiallyFilled(AxWsOrderPartiallyFilled),
814 #[serde(rename = "f")]
816 Filled(AxWsOrderFilled),
817 #[serde(rename = "c")]
819 Canceled(AxWsOrderCanceled),
820 #[serde(rename = "j")]
822 Rejected(AxWsOrderRejected),
823 #[serde(rename = "x")]
825 Expired(AxWsOrderExpired),
826 #[serde(rename = "r")]
828 Replaced(AxWsOrderReplaced),
829 #[serde(rename = "d")]
831 DoneForDay(AxWsOrderDoneForDay),
832 #[serde(rename = "e")]
834 CancelRejected(AxWsCancelRejected),
835}
836
837#[derive(Debug, Clone)]
841pub(crate) enum AxWsOrderResponse {
842 PlaceOrder(AxWsPlaceOrderResponse),
844 CancelOrder(AxWsCancelOrderResponse),
846 OpenOrders(AxWsOpenOrdersResponse),
848 List(AxWsListResponse),
850}
851
852#[derive(Debug, Clone)]
854pub(crate) enum AxOrdersWsFrame {
855 Error(AxWsOrderErrorResponse),
857 Response(AxWsOrderResponse),
859 Event(Box<AxWsOrderEvent>),
861}
862
863#[derive(Debug, Clone)]
868pub enum AxOrdersWsMessage {
869 Event(Box<AxWsOrderEvent>),
871 PlaceOrderResponse(AxWsPlaceOrderResponse),
873 CancelOrderResponse(AxWsCancelOrderResponse),
875 OpenOrdersResponse(AxWsOpenOrdersResponse),
877 Error(AxWsError),
879 Reconnected,
881 Authenticated,
883}
884
885#[derive(Debug, Clone)]
887pub struct AxWsError {
888 pub code: Option<String>,
890 pub message: String,
892 pub request_id: Option<i64>,
894}
895
896impl AxWsError {
897 #[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 #[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#[derive(Debug, Clone)]
942pub struct OrderMetadata {
943 pub trader_id: TraderId,
945 pub strategy_id: StrategyId,
947 pub instrument_id: InstrumentId,
949 pub client_order_id: ClientOrderId,
951 pub venue_order_id: Option<VenueOrderId>,
953 pub ts_init: UnixNanos,
955 pub size_precision: u8,
957 pub price_precision: u8,
959 pub quote_currency: Currency,
961 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_ticker_null_prices_decode() {
1155 let json = include_str!("../../test_data/ws_md_ticker_null_prices.json");
1157 let msg = parse_md_message(json).unwrap();
1158 let AxMdMessage::Ticker(ticker) = msg else {
1159 panic!("expected ticker message");
1160 };
1161 assert_eq!(ticker.s.as_str(), "QQQ-PERP");
1162 assert_eq!(ticker.p, Decimal::ZERO);
1163 assert_eq!(ticker.o, Decimal::ZERO);
1164 assert_eq!(ticker.l, Decimal::ZERO);
1165 assert_eq!(ticker.h, Decimal::ZERO);
1166 assert_eq!(ticker.m, Some(dec!(716.38)));
1167 assert_eq!(ticker.i, Some(AxInstrumentState::Open));
1168 }
1169
1170 #[rstest]
1171 fn test_load_md_trade_from_file() {
1172 let json = include_str!("../../test_data/ws_md_trade.json");
1173 let msg: AxMdTrade = serde_json::from_str(json).unwrap();
1174 assert_eq!(msg.d, Some(AxOrderSide::Buy));
1175 }
1176
1177 #[rstest]
1178 fn test_load_md_candle_from_file() {
1179 let json = include_str!("../../test_data/ws_md_candle.json");
1180 let msg: AxMdCandle = serde_json::from_str(json).unwrap();
1181 assert_eq!(msg.width, AxCandleWidth::Minutes1);
1182 }
1183
1184 #[rstest]
1185 fn test_load_md_book_l1_from_file() {
1186 let json = include_str!("../../test_data/ws_md_book_l1.json");
1187 let msg: AxMdBookL1 = serde_json::from_str(json).unwrap();
1188 assert_eq!(msg.b.len(), 1);
1189 assert_eq!(msg.a.len(), 1);
1190 }
1191
1192 #[rstest]
1193 fn test_load_md_book_l2_from_file() {
1194 let json = include_str!("../../test_data/ws_md_book_l2.json");
1195 let msg: AxMdBookL2 = serde_json::from_str(json).unwrap();
1196 assert_eq!(msg.b.len(), 3);
1197 assert_eq!(msg.a.len(), 3);
1198 }
1199
1200 #[rstest]
1201 fn test_load_md_book_l3_from_file() {
1202 let json = include_str!("../../test_data/ws_md_book_l3.json");
1203 let msg: AxMdBookL3 = serde_json::from_str(json).unwrap();
1204 assert_eq!(msg.b.len(), 2);
1205 assert!(!msg.b[0].o.is_empty());
1206 }
1207
1208 #[rstest]
1209 fn test_load_order_place_response_from_file() {
1210 let json = include_str!("../../test_data/ws_order_place_response.json");
1211 let msg: AxWsPlaceOrderResponse = serde_json::from_str(json).unwrap();
1212 assert_eq!(msg.res.oid, "O-01ARZ3NDEKTSV4RRFFQ69G5FAV");
1213 }
1214
1215 #[rstest]
1216 fn test_load_order_cancel_response_from_file() {
1217 let json = include_str!("../../test_data/ws_order_cancel_response.json");
1218 let msg: AxWsCancelOrderResponse = serde_json::from_str(json).unwrap();
1219 assert!(msg.res.cxl_rx);
1220 }
1221
1222 #[rstest]
1223 fn test_load_order_open_orders_response_from_file() {
1224 let json = include_str!("../../test_data/ws_order_open_orders_response.json");
1225 let msg: AxWsOpenOrdersResponse = serde_json::from_str(json).unwrap();
1226 assert_eq!(msg.res.len(), 1);
1227 }
1228
1229 #[rstest]
1230 fn test_load_order_heartbeat_from_file() {
1231 let json = include_str!("../../test_data/ws_order_heartbeat.json");
1232 let msg: AxWsHeartbeat = serde_json::from_str(json).unwrap();
1233 assert_eq!(msg.ts, 1609459200);
1234 }
1235
1236 #[rstest]
1237 fn test_load_order_acknowledged_from_file() {
1238 let json = include_str!("../../test_data/ws_order_acknowledged.json");
1239 let msg: AxWsOrderAcknowledged = serde_json::from_str(json).unwrap();
1240 assert_eq!(msg.o.oid, "O-01ARZ3NDEKTSV4RRFFQ69G5FAV");
1241 }
1242
1243 #[rstest]
1244 fn test_load_order_filled_from_file() {
1245 let json = include_str!("../../test_data/ws_order_filled.json");
1246 let msg: AxWsOrderFilled = serde_json::from_str(json).unwrap();
1247 assert_eq!(msg.o.o, AxOrderStatus::Filled);
1248 }
1249
1250 #[rstest]
1251 fn test_load_order_partially_filled_from_file() {
1252 let json = include_str!("../../test_data/ws_order_partially_filled.json");
1253 let msg: AxWsOrderPartiallyFilled = serde_json::from_str(json).unwrap();
1254 assert_eq!(msg.xs.q, 50);
1255 }
1256
1257 #[rstest]
1258 fn test_load_order_canceled_from_file() {
1259 let json = include_str!("../../test_data/ws_order_canceled.json");
1260 let msg: AxWsOrderCanceled = serde_json::from_str(json).unwrap();
1261 assert_eq!(msg.xr, AxCancelReason::UserRequested);
1262 }
1263
1264 #[rstest]
1265 fn test_load_order_rejected_from_file() {
1266 let json = include_str!("../../test_data/ws_order_rejected.json");
1267 let msg: AxWsOrderRejected = serde_json::from_str(json).unwrap();
1268 assert_eq!(msg.r, Some(AxOrderRejectReason::InsufficientMargin));
1269 }
1270
1271 #[rstest]
1272 fn test_load_order_expired_from_file() {
1273 let json = include_str!("../../test_data/ws_order_expired.json");
1274 let msg: AxWsOrderExpired = serde_json::from_str(json).unwrap();
1275 assert_eq!(msg.o.tif, AxTimeInForce::Ioc);
1276 }
1277
1278 #[rstest]
1279 fn test_load_order_replaced_from_file() {
1280 let json = include_str!("../../test_data/ws_order_replaced.json");
1281 let msg: AxWsOrderReplaced = serde_json::from_str(json).unwrap();
1282 assert_eq!(msg.o.p, dec!(50500.00));
1283 }
1284
1285 #[rstest]
1286 fn test_load_order_done_for_day_from_file() {
1287 let json = include_str!("../../test_data/ws_order_done_for_day.json");
1288 let msg: AxWsOrderDoneForDay = serde_json::from_str(json).unwrap();
1289 assert_eq!(msg.o.xq, 50);
1290 }
1291
1292 #[rstest]
1293 fn test_load_cancel_rejected_from_file() {
1294 let json = include_str!("../../test_data/ws_cancel_rejected.json");
1295 let msg: AxWsCancelRejected = serde_json::from_str(json).unwrap();
1296 assert_eq!(msg.r, AxCancelRejectionReason::OrderNotFound);
1297 }
1298
1299 #[rstest]
1300 fn test_load_order_error_response_from_file() {
1301 let json = include_str!("../../test_data/ws_order_error_response.json");
1302 let msg: AxWsOrderErrorResponse = serde_json::from_str(json).unwrap();
1303 assert_eq!(msg.rid, 1);
1304 assert_eq!(msg.err.code, 400);
1305 assert!(msg.err.msg.contains("initial margin"));
1306 }
1307
1308 #[rstest]
1309 fn test_load_order_list_response_from_file() {
1310 let json = include_str!("../../test_data/ws_order_list_response.json");
1311 let msg: AxWsListResponse = serde_json::from_str(json).unwrap();
1312 assert_eq!(msg.rid, 0);
1313 assert_eq!(msg.res.li, "01KCQM-4WP1-0000");
1314 assert!(msg.res.o.is_none());
1315 }
1316
1317 #[rstest]
1318 fn test_load_order_list_response_with_orders_from_file() {
1319 let json = include_str!("../../test_data/ws_order_list_response_with_orders.json");
1320 let msg: AxWsListResponse = serde_json::from_str(json).unwrap();
1321 assert_eq!(msg.rid, 0);
1322 assert_eq!(msg.res.li, "01KCQM-4WP1-0000");
1323 let orders = msg.res.o.unwrap();
1324 assert_eq!(orders.len(), 2);
1325 assert_eq!(orders[0].oid, "O-01KF4QM3VVJEDH98ZVNS1PCSBB");
1326 assert_eq!(orders[1].oid, "O-01KF4QM3K9FJZWYA02JF9Y1FJA");
1327 }
1328
1329 #[derive(Debug, Eq, PartialEq)]
1330 enum FrameKind {
1331 Error,
1332 ListResponse,
1333 AcknowledgedEvent,
1334 PlaceResponse,
1335 CancelResponse,
1336 OpenOrdersResponse,
1337 }
1338
1339 fn classify(frame: &AxOrdersWsFrame) -> FrameKind {
1340 match frame {
1341 AxOrdersWsFrame::Error(_) => FrameKind::Error,
1342 AxOrdersWsFrame::Response(AxWsOrderResponse::List(_)) => FrameKind::ListResponse,
1343 AxOrdersWsFrame::Response(AxWsOrderResponse::PlaceOrder(_)) => FrameKind::PlaceResponse,
1344 AxOrdersWsFrame::Response(AxWsOrderResponse::CancelOrder(_)) => {
1345 FrameKind::CancelResponse
1346 }
1347 AxOrdersWsFrame::Response(AxWsOrderResponse::OpenOrders(_)) => {
1348 FrameKind::OpenOrdersResponse
1349 }
1350 AxOrdersWsFrame::Event(e) => match **e {
1351 AxWsOrderEvent::Acknowledged(_) => FrameKind::AcknowledgedEvent,
1352 _ => panic!("unexpected event variant"),
1353 },
1354 }
1355 }
1356
1357 #[rstest]
1358 #[case::error(
1359 include_str!("../../test_data/ws_order_error_response.json"),
1360 FrameKind::Error,
1361 )]
1362 #[case::list(
1363 include_str!("../../test_data/ws_order_list_response.json"),
1364 FrameKind::ListResponse,
1365 )]
1366 #[case::acknowledged_event(
1367 include_str!("../../test_data/ws_order_acknowledged.json"),
1368 FrameKind::AcknowledgedEvent,
1369 )]
1370 #[case::place_response(
1371 include_str!("../../test_data/ws_order_place_response.json"),
1372 FrameKind::PlaceResponse,
1373 )]
1374 #[case::cancel_response(
1375 include_str!("../../test_data/ws_order_cancel_response.json"),
1376 FrameKind::CancelResponse,
1377 )]
1378 #[case::open_orders(
1379 include_str!("../../test_data/ws_order_open_orders_response.json"),
1380 FrameKind::OpenOrdersResponse,
1381 )]
1382 fn test_parse_order_message_variants(#[case] json: &str, #[case] expected: FrameKind) {
1383 let msg = parse_order_message(json).expect("should parse");
1384 assert_eq!(classify(&msg), expected);
1385 }
1386}