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_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_decimal_or_zero")]
241 pub l: Decimal,
242 #[serde(deserialize_with = "deserialize_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_trade_from_file() {
1155 let json = include_str!("../../test_data/ws_md_trade.json");
1156 let msg: AxMdTrade = serde_json::from_str(json).unwrap();
1157 assert_eq!(msg.d, Some(AxOrderSide::Buy));
1158 }
1159
1160 #[rstest]
1161 fn test_load_md_candle_from_file() {
1162 let json = include_str!("../../test_data/ws_md_candle.json");
1163 let msg: AxMdCandle = serde_json::from_str(json).unwrap();
1164 assert_eq!(msg.width, AxCandleWidth::Minutes1);
1165 }
1166
1167 #[rstest]
1168 fn test_load_md_book_l1_from_file() {
1169 let json = include_str!("../../test_data/ws_md_book_l1.json");
1170 let msg: AxMdBookL1 = serde_json::from_str(json).unwrap();
1171 assert_eq!(msg.b.len(), 1);
1172 assert_eq!(msg.a.len(), 1);
1173 }
1174
1175 #[rstest]
1176 fn test_load_md_book_l2_from_file() {
1177 let json = include_str!("../../test_data/ws_md_book_l2.json");
1178 let msg: AxMdBookL2 = serde_json::from_str(json).unwrap();
1179 assert_eq!(msg.b.len(), 3);
1180 assert_eq!(msg.a.len(), 3);
1181 }
1182
1183 #[rstest]
1184 fn test_load_md_book_l3_from_file() {
1185 let json = include_str!("../../test_data/ws_md_book_l3.json");
1186 let msg: AxMdBookL3 = serde_json::from_str(json).unwrap();
1187 assert_eq!(msg.b.len(), 2);
1188 assert!(!msg.b[0].o.is_empty());
1189 }
1190
1191 #[rstest]
1192 fn test_load_order_place_response_from_file() {
1193 let json = include_str!("../../test_data/ws_order_place_response.json");
1194 let msg: AxWsPlaceOrderResponse = serde_json::from_str(json).unwrap();
1195 assert_eq!(msg.res.oid, "O-01ARZ3NDEKTSV4RRFFQ69G5FAV");
1196 }
1197
1198 #[rstest]
1199 fn test_load_order_cancel_response_from_file() {
1200 let json = include_str!("../../test_data/ws_order_cancel_response.json");
1201 let msg: AxWsCancelOrderResponse = serde_json::from_str(json).unwrap();
1202 assert!(msg.res.cxl_rx);
1203 }
1204
1205 #[rstest]
1206 fn test_load_order_open_orders_response_from_file() {
1207 let json = include_str!("../../test_data/ws_order_open_orders_response.json");
1208 let msg: AxWsOpenOrdersResponse = serde_json::from_str(json).unwrap();
1209 assert_eq!(msg.res.len(), 1);
1210 }
1211
1212 #[rstest]
1213 fn test_load_order_heartbeat_from_file() {
1214 let json = include_str!("../../test_data/ws_order_heartbeat.json");
1215 let msg: AxWsHeartbeat = serde_json::from_str(json).unwrap();
1216 assert_eq!(msg.ts, 1609459200);
1217 }
1218
1219 #[rstest]
1220 fn test_load_order_acknowledged_from_file() {
1221 let json = include_str!("../../test_data/ws_order_acknowledged.json");
1222 let msg: AxWsOrderAcknowledged = serde_json::from_str(json).unwrap();
1223 assert_eq!(msg.o.oid, "O-01ARZ3NDEKTSV4RRFFQ69G5FAV");
1224 }
1225
1226 #[rstest]
1227 fn test_load_order_filled_from_file() {
1228 let json = include_str!("../../test_data/ws_order_filled.json");
1229 let msg: AxWsOrderFilled = serde_json::from_str(json).unwrap();
1230 assert_eq!(msg.o.o, AxOrderStatus::Filled);
1231 }
1232
1233 #[rstest]
1234 fn test_load_order_partially_filled_from_file() {
1235 let json = include_str!("../../test_data/ws_order_partially_filled.json");
1236 let msg: AxWsOrderPartiallyFilled = serde_json::from_str(json).unwrap();
1237 assert_eq!(msg.xs.q, 50);
1238 }
1239
1240 #[rstest]
1241 fn test_load_order_canceled_from_file() {
1242 let json = include_str!("../../test_data/ws_order_canceled.json");
1243 let msg: AxWsOrderCanceled = serde_json::from_str(json).unwrap();
1244 assert_eq!(msg.xr, AxCancelReason::UserRequested);
1245 }
1246
1247 #[rstest]
1248 fn test_load_order_rejected_from_file() {
1249 let json = include_str!("../../test_data/ws_order_rejected.json");
1250 let msg: AxWsOrderRejected = serde_json::from_str(json).unwrap();
1251 assert_eq!(msg.r, Some(AxOrderRejectReason::InsufficientMargin));
1252 }
1253
1254 #[rstest]
1255 fn test_load_order_expired_from_file() {
1256 let json = include_str!("../../test_data/ws_order_expired.json");
1257 let msg: AxWsOrderExpired = serde_json::from_str(json).unwrap();
1258 assert_eq!(msg.o.tif, AxTimeInForce::Ioc);
1259 }
1260
1261 #[rstest]
1262 fn test_load_order_replaced_from_file() {
1263 let json = include_str!("../../test_data/ws_order_replaced.json");
1264 let msg: AxWsOrderReplaced = serde_json::from_str(json).unwrap();
1265 assert_eq!(msg.o.p, dec!(50500.00));
1266 }
1267
1268 #[rstest]
1269 fn test_load_order_done_for_day_from_file() {
1270 let json = include_str!("../../test_data/ws_order_done_for_day.json");
1271 let msg: AxWsOrderDoneForDay = serde_json::from_str(json).unwrap();
1272 assert_eq!(msg.o.xq, 50);
1273 }
1274
1275 #[rstest]
1276 fn test_load_cancel_rejected_from_file() {
1277 let json = include_str!("../../test_data/ws_cancel_rejected.json");
1278 let msg: AxWsCancelRejected = serde_json::from_str(json).unwrap();
1279 assert_eq!(msg.r, AxCancelRejectionReason::OrderNotFound);
1280 }
1281
1282 #[rstest]
1283 fn test_load_order_error_response_from_file() {
1284 let json = include_str!("../../test_data/ws_order_error_response.json");
1285 let msg: AxWsOrderErrorResponse = serde_json::from_str(json).unwrap();
1286 assert_eq!(msg.rid, 1);
1287 assert_eq!(msg.err.code, 400);
1288 assert!(msg.err.msg.contains("initial margin"));
1289 }
1290
1291 #[rstest]
1292 fn test_load_order_list_response_from_file() {
1293 let json = include_str!("../../test_data/ws_order_list_response.json");
1294 let msg: AxWsListResponse = serde_json::from_str(json).unwrap();
1295 assert_eq!(msg.rid, 0);
1296 assert_eq!(msg.res.li, "01KCQM-4WP1-0000");
1297 assert!(msg.res.o.is_none());
1298 }
1299
1300 #[rstest]
1301 fn test_load_order_list_response_with_orders_from_file() {
1302 let json = include_str!("../../test_data/ws_order_list_response_with_orders.json");
1303 let msg: AxWsListResponse = serde_json::from_str(json).unwrap();
1304 assert_eq!(msg.rid, 0);
1305 assert_eq!(msg.res.li, "01KCQM-4WP1-0000");
1306 let orders = msg.res.o.unwrap();
1307 assert_eq!(orders.len(), 2);
1308 assert_eq!(orders[0].oid, "O-01KF4QM3VVJEDH98ZVNS1PCSBB");
1309 assert_eq!(orders[1].oid, "O-01KF4QM3K9FJZWYA02JF9Y1FJA");
1310 }
1311
1312 #[derive(Debug, Eq, PartialEq)]
1313 enum FrameKind {
1314 Error,
1315 ListResponse,
1316 AcknowledgedEvent,
1317 PlaceResponse,
1318 CancelResponse,
1319 OpenOrdersResponse,
1320 }
1321
1322 fn classify(frame: &AxOrdersWsFrame) -> FrameKind {
1323 match frame {
1324 AxOrdersWsFrame::Error(_) => FrameKind::Error,
1325 AxOrdersWsFrame::Response(AxWsOrderResponse::List(_)) => FrameKind::ListResponse,
1326 AxOrdersWsFrame::Response(AxWsOrderResponse::PlaceOrder(_)) => FrameKind::PlaceResponse,
1327 AxOrdersWsFrame::Response(AxWsOrderResponse::CancelOrder(_)) => {
1328 FrameKind::CancelResponse
1329 }
1330 AxOrdersWsFrame::Response(AxWsOrderResponse::OpenOrders(_)) => {
1331 FrameKind::OpenOrdersResponse
1332 }
1333 AxOrdersWsFrame::Event(e) => match **e {
1334 AxWsOrderEvent::Acknowledged(_) => FrameKind::AcknowledgedEvent,
1335 _ => panic!("unexpected event variant"),
1336 },
1337 }
1338 }
1339
1340 #[rstest]
1341 #[case::error(
1342 include_str!("../../test_data/ws_order_error_response.json"),
1343 FrameKind::Error,
1344 )]
1345 #[case::list(
1346 include_str!("../../test_data/ws_order_list_response.json"),
1347 FrameKind::ListResponse,
1348 )]
1349 #[case::acknowledged_event(
1350 include_str!("../../test_data/ws_order_acknowledged.json"),
1351 FrameKind::AcknowledgedEvent,
1352 )]
1353 #[case::place_response(
1354 include_str!("../../test_data/ws_order_place_response.json"),
1355 FrameKind::PlaceResponse,
1356 )]
1357 #[case::cancel_response(
1358 include_str!("../../test_data/ws_order_cancel_response.json"),
1359 FrameKind::CancelResponse,
1360 )]
1361 #[case::open_orders(
1362 include_str!("../../test_data/ws_order_open_orders_response.json"),
1363 FrameKind::OpenOrdersResponse,
1364 )]
1365 fn test_parse_order_message_variants(#[case] json: &str, #[case] expected: FrameKind) {
1366 let msg = parse_order_message(json).expect("should parse");
1367 assert_eq!(classify(&msg), expected);
1368 }
1369}