1use nautilus_core::{UnixNanos, serialization::serialize_decimal_as_str};
22use nautilus_model::{
23 identifiers::{ClientOrderId, InstrumentId, StrategyId, TraderId, VenueOrderId},
24 types::Currency,
25};
26use rust_decimal::Decimal;
27use serde::{Deserialize, Serialize};
28use ustr::Ustr;
29
30use super::error::AxWsErrorResponse;
31use crate::{
32 common::{
33 enums::{
34 AxCancelReason, AxCancelRejectionReason, AxCandleWidth, AxInstrumentState,
35 AxMarketDataLevel, AxMdRequestType, AxOrderRequestType, AxOrderSide, AxOrderStatus,
36 AxOrderWsMessageType, AxTimeInForce,
37 },
38 parse::{
39 deserialize_decimal_or_zero, deserialize_optional_decimal_from_str,
40 deserialize_optional_decimal_or_zero,
41 },
42 },
43 http::models::AxOrderRejectReason,
44};
45
46#[derive(Debug, Clone)]
51pub enum AxDataWsMessage {
52 MdMessage(AxMdMessage),
54 Reconnected,
56 CandleUnsubscribed {
58 symbol: Ustr,
60 width: AxCandleWidth,
62 },
63}
64
65#[derive(Clone, Debug, Serialize, Deserialize)]
70pub struct AxMdSubscribe {
71 pub rid: i64,
73 #[serde(rename = "type")]
75 pub msg_type: AxMdRequestType,
76 pub symbol: Ustr,
78 pub level: AxMarketDataLevel,
80 #[serde(skip_serializing_if = "Option::is_none")]
82 pub trades: Option<bool>,
83 #[serde(skip_serializing_if = "Option::is_none")]
85 pub ticker: Option<bool>,
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)]
369pub struct AxMdBookL2 {
370 pub ts: i64,
372 pub tn: i64,
374 pub s: Ustr,
376 pub b: Vec<AxBookLevel>,
378 pub a: Vec<AxBookLevel>,
380 #[serde(default)]
382 pub st: bool,
383}
384
385#[derive(Clone, Debug, Serialize, Deserialize)]
393pub struct AxMdBookL3 {
394 pub ts: i64,
396 pub tn: i64,
398 pub s: Ustr,
400 pub b: Vec<AxBookLevelL3>,
402 pub a: Vec<AxBookLevelL3>,
404 #[serde(default)]
406 pub st: bool,
407}
408
409#[derive(Clone, Debug, Serialize, Deserialize)]
414pub struct AxWsPlaceOrder {
415 pub rid: i64,
417 pub t: AxOrderRequestType,
419 pub s: Ustr,
421 pub d: AxOrderSide,
423 pub q: u64,
425 #[serde(
427 serialize_with = "serialize_decimal_as_str",
428 deserialize_with = "deserialize_decimal_or_zero"
429 )]
430 pub p: Decimal,
431 pub tif: AxTimeInForce,
433 pub po: bool,
435 #[serde(skip_serializing_if = "Option::is_none")]
437 pub cid: Option<u64>,
438 #[serde(skip_serializing_if = "Option::is_none")]
440 pub tag: Option<String>,
441}
442
443#[derive(Clone, Debug, Serialize, Deserialize)]
448pub struct AxWsCancelOrder {
449 pub rid: i64,
451 pub t: AxOrderRequestType,
453 pub oid: String,
455}
456
457#[derive(Clone, Debug, Serialize, Deserialize)]
462pub struct AxWsGetOpenOrders {
463 pub rid: i64,
465 pub t: AxOrderRequestType,
467}
468
469#[derive(Clone, Debug, Serialize, Deserialize)]
474pub struct AxWsPlaceOrderResponse {
475 pub rid: i64,
477 pub res: AxWsPlaceOrderResult,
479}
480
481#[derive(Clone, Debug, Serialize, Deserialize)]
483pub struct AxWsPlaceOrderResult {
484 pub oid: String,
486}
487
488#[derive(Clone, Debug, Serialize, Deserialize)]
493pub struct AxWsCancelOrderResponse {
494 pub rid: i64,
496 pub res: AxWsCancelOrderResult,
498}
499
500#[derive(Clone, Debug, Serialize, Deserialize)]
502pub struct AxWsCancelOrderResult {
503 pub cxl_rx: bool,
505}
506
507#[derive(Clone, Debug, Serialize, Deserialize)]
512pub struct AxWsOpenOrdersResponse {
513 pub rid: i64,
515 pub res: AxWsOpenOrdersResult,
517}
518
519#[derive(Clone, Debug, Serialize, Deserialize)]
521pub struct AxWsOpenOrdersResult {
522 pub orders: Vec<AxWsOrder>,
524}
525
526#[derive(Clone, Debug, Deserialize)]
530pub struct AxWsOrderErrorResponse {
531 pub rid: i64,
533 pub err: AxWsOrderError,
535}
536
537#[derive(Clone, Debug, Deserialize)]
539pub struct AxWsOrderError {
540 pub code: i64,
542 pub msg: String,
544}
545
546#[derive(Clone, Debug, Deserialize)]
550pub struct AxWsListResponse {
551 pub rid: i64,
553 pub res: AxWsListResult,
555}
556
557#[derive(Clone, Debug, Deserialize)]
559pub struct AxWsListResult {
560 pub li: String,
562 #[serde(default)]
564 pub o: Option<Vec<AxWsOrder>>,
565}
566
567#[derive(Clone, Debug, Serialize, Deserialize)]
569pub struct AxWsOrder {
570 pub oid: String,
572 pub u: String,
574 pub s: Ustr,
576 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
578 pub p: Decimal,
579 pub q: u64,
581 pub xq: u64,
583 pub rq: u64,
585 pub o: AxOrderStatus,
587 pub d: AxOrderSide,
589 pub tif: AxTimeInForce,
591 pub ts: i64,
593 pub tn: i64,
595 #[serde(default)]
597 pub cid: Option<u64>,
598 #[serde(default)]
600 pub tag: Option<String>,
601 #[serde(default)]
603 pub txt: Option<String>,
604}
605
606#[derive(Clone, Debug, Serialize, Deserialize)]
611pub struct AxWsHeartbeat {
612 pub t: AxOrderWsMessageType,
614 pub ts: i64,
616 pub tn: i64,
618}
619
620#[derive(Clone, Debug, Serialize, Deserialize)]
625pub struct AxWsOrderAcknowledged {
626 pub ts: i64,
628 pub tn: i64,
630 pub eid: String,
632 pub o: AxWsOrder,
634}
635
636#[derive(Clone, Debug, Serialize, Deserialize)]
638pub struct AxWsTradeExecution {
639 pub tid: String,
641 pub s: Ustr,
643 pub q: u64,
645 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
647 pub p: Decimal,
648 pub d: AxOrderSide,
650 pub agg: bool,
652}
653
654#[derive(Clone, Debug, Serialize, Deserialize)]
659pub struct AxWsOrderPartiallyFilled {
660 pub ts: i64,
662 pub tn: i64,
664 pub eid: String,
666 pub o: AxWsOrder,
668 pub xs: AxWsTradeExecution,
670}
671
672#[derive(Clone, Debug, Serialize, Deserialize)]
677pub struct AxWsOrderFilled {
678 pub ts: i64,
680 pub tn: i64,
682 pub eid: String,
684 pub o: AxWsOrder,
686 pub xs: AxWsTradeExecution,
688}
689
690#[derive(Clone, Debug, Serialize, Deserialize)]
695pub struct AxWsOrderCanceled {
696 pub ts: i64,
698 pub tn: i64,
700 pub eid: String,
702 pub o: AxWsOrder,
704 pub xr: AxCancelReason,
706 #[serde(default)]
708 pub txt: Option<String>,
709}
710
711#[derive(Clone, Debug, Serialize, Deserialize)]
716pub struct AxWsOrderRejected {
717 pub ts: i64,
719 pub tn: i64,
721 pub eid: String,
723 pub o: AxWsOrder,
725 #[serde(default)]
727 pub r: Option<AxOrderRejectReason>,
728 #[serde(default)]
730 pub txt: Option<String>,
731}
732
733#[derive(Clone, Debug, Serialize, Deserialize)]
738pub struct AxWsOrderExpired {
739 pub ts: i64,
741 pub tn: i64,
743 pub eid: String,
745 pub o: AxWsOrder,
747}
748
749#[derive(Clone, Debug, Serialize, Deserialize)]
754pub struct AxWsOrderReplaced {
755 pub ts: i64,
757 pub tn: i64,
759 pub eid: String,
761 pub ro: Box<AxWsOrder>,
763 pub noid: String,
765 pub no: Box<AxWsOrder>,
767}
768
769#[derive(Clone, Debug, Serialize, Deserialize)]
774pub struct AxWsOrderDoneForDay {
775 pub ts: i64,
777 pub tn: i64,
779 pub eid: String,
781 pub o: AxWsOrder,
783}
784
785#[derive(Clone, Debug, Serialize, Deserialize)]
790pub struct AxWsCancelRejected {
791 pub ts: i64,
793 pub tn: i64,
795 pub oid: String,
797 pub r: AxCancelRejectionReason,
799 #[serde(default)]
801 pub txt: Option<String>,
802}
803
804#[derive(Debug, Clone, Deserialize)]
809#[serde(tag = "t")]
810pub enum AxWsOrderEvent {
811 #[serde(rename = "h")]
813 Heartbeat,
814 #[serde(rename = "n")]
816 Acknowledged(AxWsOrderAcknowledged),
817 #[serde(rename = "p")]
819 PartiallyFilled(AxWsOrderPartiallyFilled),
820 #[serde(rename = "f")]
822 Filled(AxWsOrderFilled),
823 #[serde(rename = "c")]
825 Canceled(AxWsOrderCanceled),
826 #[serde(rename = "j")]
828 Rejected(AxWsOrderRejected),
829 #[serde(rename = "x")]
831 Expired(AxWsOrderExpired),
832 #[serde(rename = "r")]
834 Replaced(AxWsOrderReplaced),
835 #[serde(rename = "d")]
837 DoneForDay(AxWsOrderDoneForDay),
838 #[serde(rename = "e")]
840 CancelRejected(AxWsCancelRejected),
841}
842
843#[derive(Debug, Clone)]
847pub(crate) enum AxWsOrderResponse {
848 PlaceOrder(AxWsPlaceOrderResponse),
850 CancelOrder(AxWsCancelOrderResponse),
852 OpenOrders(AxWsOpenOrdersResponse),
854 List(AxWsListResponse),
856}
857
858#[derive(Debug, Clone)]
860pub(crate) enum AxOrdersWsFrame {
861 Error(AxWsOrderErrorResponse),
863 Response(AxWsOrderResponse),
865 Event(Box<AxWsOrderEvent>),
867}
868
869#[derive(Debug, Clone)]
874pub enum AxOrdersWsMessage {
875 Event(Box<AxWsOrderEvent>),
877 PlaceOrderResponse(AxWsPlaceOrderResponse),
879 CancelOrderResponse(AxWsCancelOrderResponse),
881 OpenOrdersResponse(AxWsOpenOrdersResponse),
883 Error(AxWsError),
885 Reconnected,
887 Authenticated,
889}
890
891#[derive(Debug, Clone)]
893pub struct AxWsError {
894 pub code: Option<String>,
896 pub message: String,
898 pub request_id: Option<i64>,
900}
901
902impl AxWsError {
903 #[must_use]
905 pub fn new(message: impl Into<String>) -> Self {
906 Self {
907 code: None,
908 message: message.into(),
909 request_id: None,
910 }
911 }
912
913 #[must_use]
915 pub fn with_code(code: impl Into<String>, message: impl Into<String>) -> Self {
916 Self {
917 code: Some(code.into()),
918 message: message.into(),
919 request_id: None,
920 }
921 }
922}
923
924impl From<AxWsOrderErrorResponse> for AxWsError {
925 fn from(resp: AxWsOrderErrorResponse) -> Self {
926 Self {
927 code: Some(resp.err.code.to_string()),
928 message: resp.err.msg,
929 request_id: Some(resp.rid),
930 }
931 }
932}
933
934impl From<AxWsErrorResponse> for AxWsError {
935 fn from(resp: AxWsErrorResponse) -> Self {
936 Self {
937 code: resp.code,
938 message: resp.message.unwrap_or_else(|| "Unknown error".to_string()),
939 request_id: resp.rid,
940 }
941 }
942}
943
944#[derive(Debug, Clone)]
948pub struct OrderMetadata {
949 pub trader_id: TraderId,
951 pub strategy_id: StrategyId,
953 pub instrument_id: InstrumentId,
955 pub client_order_id: ClientOrderId,
957 pub venue_order_id: Option<VenueOrderId>,
959 pub ts_init: UnixNanos,
961 pub size_precision: u8,
963 pub price_precision: u8,
965 pub quote_currency: Currency,
967}
968
969#[cfg(test)]
970mod tests {
971 use rstest::rstest;
972 use rust_decimal_macros::dec;
973
974 use super::{
975 super::parse::{parse_md_message, parse_order_message},
976 *,
977 };
978
979 #[rstest]
980 fn test_md_subscribe_serialization() {
981 let msg = AxMdSubscribe {
982 rid: 2,
983 msg_type: AxMdRequestType::Subscribe,
984 symbol: Ustr::from("EURUSD-PERP"),
985 level: AxMarketDataLevel::Level2,
986 trades: None,
987 ticker: None,
988 };
989 let json = serde_json::to_string(&msg).unwrap();
990 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
991
992 assert_eq!(parsed["rid"], 2);
993 assert_eq!(parsed["type"], "subscribe");
994 assert_eq!(parsed["symbol"], "EURUSD-PERP");
995 assert_eq!(parsed["level"], "LEVEL_2");
996 assert!(parsed.get("trades").is_none());
997 assert!(parsed.get("ticker").is_none());
998 }
999
1000 #[rstest]
1001 fn test_md_subscribe_book_only_serialization() {
1002 let msg = AxMdSubscribe {
1003 rid: 2,
1004 msg_type: AxMdRequestType::Subscribe,
1005 symbol: Ustr::from("EURUSD-PERP"),
1006 level: AxMarketDataLevel::Level2,
1007 trades: Some(false),
1008 ticker: Some(false),
1009 };
1010 let json = serde_json::to_string(&msg).unwrap();
1011 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1012
1013 assert_eq!(parsed["rid"], 2);
1014 assert_eq!(parsed["type"], "subscribe");
1015 assert_eq!(parsed["symbol"], "EURUSD-PERP");
1016 assert_eq!(parsed["level"], "LEVEL_2");
1017 assert_eq!(parsed["trades"], false);
1018 assert_eq!(parsed["ticker"], false);
1019 }
1020
1021 #[rstest]
1022 fn test_md_subscribe_trades_serialization() {
1023 let msg = AxMdSubscribe {
1024 rid: 2,
1025 msg_type: AxMdRequestType::Subscribe,
1026 symbol: Ustr::from("EURUSD-PERP"),
1027 level: AxMarketDataLevel::Trades,
1028 trades: None,
1029 ticker: None,
1030 };
1031 let json = serde_json::to_string(&msg).unwrap();
1032 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1033
1034 assert_eq!(parsed["rid"], 2);
1035 assert_eq!(parsed["type"], "subscribe");
1036 assert_eq!(parsed["symbol"], "EURUSD-PERP");
1037 assert_eq!(parsed["level"], "TRADES");
1038 assert!(parsed.get("trades").is_none());
1039 assert!(parsed.get("ticker").is_none());
1040 }
1041
1042 #[rstest]
1043 fn test_md_unsubscribe_serialization() {
1044 let msg = AxMdUnsubscribe {
1045 rid: 3,
1046 msg_type: AxMdRequestType::Unsubscribe,
1047 symbol: Ustr::from("EURUSD-PERP"),
1048 };
1049 let json = serde_json::to_string(&msg).unwrap();
1050 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1051
1052 assert_eq!(parsed["rid"], 3);
1053 assert_eq!(parsed["type"], "unsubscribe");
1054 assert_eq!(parsed["symbol"], "EURUSD-PERP");
1055 }
1056
1057 #[rstest]
1058 fn test_md_subscribe_candles_serialization() {
1059 let msg = AxMdSubscribeCandles {
1060 rid: 4,
1061 msg_type: AxMdRequestType::SubscribeCandles,
1062 symbol: Ustr::from("EURUSD-PERP"),
1063 width: AxCandleWidth::Minutes1,
1064 };
1065 let json = serde_json::to_string(&msg).unwrap();
1066 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1067
1068 assert_eq!(parsed["rid"], 4);
1069 assert_eq!(parsed["type"], "subscribe_candles");
1070 assert_eq!(parsed["symbol"], "EURUSD-PERP");
1071 assert_eq!(parsed["width"], "1m");
1072 }
1073
1074 #[rstest]
1075 fn test_md_unsubscribe_candles_serialization() {
1076 let msg = AxMdUnsubscribeCandles {
1077 rid: 5,
1078 msg_type: AxMdRequestType::UnsubscribeCandles,
1079 symbol: Ustr::from("EURUSD-PERP"),
1080 width: AxCandleWidth::Minutes1,
1081 };
1082 let json = serde_json::to_string(&msg).unwrap();
1083 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1084
1085 assert_eq!(parsed["rid"], 5);
1086 assert_eq!(parsed["type"], "unsubscribe_candles");
1087 assert_eq!(parsed["symbol"], "EURUSD-PERP");
1088 assert_eq!(parsed["width"], "1m");
1089 }
1090
1091 #[rstest]
1092 fn test_ws_place_order_serialization() {
1093 let msg = AxWsPlaceOrder {
1094 rid: 1,
1095 t: AxOrderRequestType::PlaceOrder,
1096 s: Ustr::from("EURUSD-PERP"),
1097 d: AxOrderSide::Buy,
1098 q: 100,
1099 p: dec!(50000.50),
1100 tif: AxTimeInForce::Gtc,
1101 po: false,
1102 tag: Some("Nautilus".to_string()),
1103 cid: Some(1234567890),
1104 };
1105
1106 let json = serde_json::to_string(&msg).unwrap();
1107 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1108
1109 assert_eq!(parsed["rid"], 1);
1110 assert_eq!(parsed["t"], "p");
1111 assert_eq!(parsed["s"], "EURUSD-PERP");
1112 assert_eq!(parsed["d"], "B");
1113 assert_eq!(parsed["q"], 100);
1114 assert_eq!(parsed["p"], "50000.50");
1115 assert_eq!(parsed["tif"], "GTC");
1116 assert_eq!(parsed["po"], false);
1117 assert_eq!(parsed["tag"], "Nautilus");
1118 assert_eq!(parsed["cid"], 1234567890);
1119 assert!(parsed.get("order_type").is_none());
1120 assert!(parsed.get("trigger_price").is_none());
1121 }
1122
1123 #[rstest]
1124 fn test_ws_cancel_order_serialization() {
1125 let msg = AxWsCancelOrder {
1126 rid: 2,
1127 t: AxOrderRequestType::CancelOrder,
1128 oid: "O-01ARZ3NDEKTSV4RRFFQ69G5FAV".to_string(),
1129 };
1130 let json = serde_json::to_string(&msg).unwrap();
1131 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1132
1133 assert_eq!(parsed["rid"], 2);
1134 assert_eq!(parsed["t"], "x");
1135 assert_eq!(parsed["oid"], "O-01ARZ3NDEKTSV4RRFFQ69G5FAV");
1136 }
1137
1138 #[rstest]
1139 fn test_ws_get_open_orders_serialization() {
1140 let msg = AxWsGetOpenOrders {
1141 rid: 3,
1142 t: AxOrderRequestType::GetOpenOrders,
1143 };
1144 let json = serde_json::to_string(&msg).unwrap();
1145 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1146
1147 assert_eq!(parsed["rid"], 3);
1148 assert_eq!(parsed["t"], "o");
1149 }
1150
1151 #[rstest]
1152 fn test_load_md_heartbeat_from_file() {
1153 let json = include_str!("../../test_data/ws_md_heartbeat.json");
1154 let msg = parse_md_message(json).unwrap();
1155 assert!(matches!(msg, AxMdMessage::Heartbeat(_)));
1156 }
1157
1158 #[rstest]
1159 fn test_load_md_ticker_from_file() {
1160 let json = include_str!("../../test_data/ws_md_ticker.json");
1161 let msg: AxMdTicker = serde_json::from_str(json).unwrap();
1162 assert_eq!(msg.s.as_str(), "EURUSD-PERP");
1163 assert_eq!(msg.m, Some(dec!(50010.50)));
1164 assert_eq!(msg.i, Some(AxInstrumentState::Open));
1165 }
1166
1167 #[rstest]
1168 fn test_load_md_ticker_captured_optional_fields_default_to_none() {
1169 let json = include_str!("../../test_data/ws_md_ticker_captured.json");
1170 let msg: AxMdTicker = serde_json::from_str(json).unwrap();
1171 assert_eq!(msg.s.as_str(), "EURUSD-PERP");
1172 assert_eq!(msg.m, None);
1173 assert_eq!(msg.i, None);
1174 }
1175
1176 #[rstest]
1177 fn test_load_md_ticker_null_prices_decode() {
1178 let json = include_str!("../../test_data/ws_md_ticker_null_prices.json");
1180 let msg = parse_md_message(json).unwrap();
1181 let AxMdMessage::Ticker(ticker) = msg else {
1182 panic!("expected ticker message");
1183 };
1184 assert_eq!(ticker.s.as_str(), "QQQ-PERP");
1185 assert_eq!(ticker.p, Decimal::ZERO);
1186 assert_eq!(ticker.o, Decimal::ZERO);
1187 assert_eq!(ticker.l, Decimal::ZERO);
1188 assert_eq!(ticker.h, Decimal::ZERO);
1189 assert_eq!(ticker.m, Some(dec!(716.38)));
1190 assert_eq!(ticker.i, Some(AxInstrumentState::Open));
1191 }
1192
1193 #[rstest]
1194 fn test_load_md_trade_from_file() {
1195 let json = include_str!("../../test_data/ws_md_trade.json");
1196 let msg: AxMdTrade = serde_json::from_str(json).unwrap();
1197 assert_eq!(msg.d, Some(AxOrderSide::Buy));
1198 }
1199
1200 #[rstest]
1201 fn test_load_md_candle_from_file() {
1202 let json = include_str!("../../test_data/ws_md_candle.json");
1203 let msg: AxMdCandle = serde_json::from_str(json).unwrap();
1204 assert_eq!(msg.width, AxCandleWidth::Minutes1);
1205 }
1206
1207 #[rstest]
1208 fn test_load_md_book_l1_from_file() {
1209 let json = include_str!("../../test_data/ws_md_book_l1.json");
1210 let msg: AxMdBookL1 = serde_json::from_str(json).unwrap();
1211 assert_eq!(msg.b.len(), 1);
1212 assert_eq!(msg.a.len(), 1);
1213 }
1214
1215 #[rstest]
1216 fn test_load_md_book_l2_from_file() {
1217 let json = include_str!("../../test_data/ws_md_book_l2.json");
1218 let msg: AxMdBookL2 = serde_json::from_str(json).unwrap();
1219 assert_eq!(msg.b.len(), 3);
1220 assert_eq!(msg.a.len(), 3);
1221 }
1222
1223 #[rstest]
1224 fn test_load_md_book_l3_from_file() {
1225 let json = include_str!("../../test_data/ws_md_book_l3.json");
1226 let msg: AxMdBookL3 = serde_json::from_str(json).unwrap();
1227 assert_eq!(msg.b.len(), 2);
1228 assert!(!msg.b[0].o.is_empty());
1229 }
1230
1231 #[rstest]
1232 fn test_load_order_place_response_from_file() {
1233 let json = include_str!("../../test_data/ws_order_place_response.json");
1234 let msg: AxWsPlaceOrderResponse = serde_json::from_str(json).unwrap();
1235 assert_eq!(msg.res.oid, "O-01ARZ3NDEKTSV4RRFFQ69G5FAV");
1236 }
1237
1238 #[rstest]
1239 fn test_load_order_cancel_response_from_file() {
1240 let json = include_str!("../../test_data/ws_order_cancel_response.json");
1241 let msg: AxWsCancelOrderResponse = serde_json::from_str(json).unwrap();
1242 assert!(msg.res.cxl_rx);
1243 }
1244
1245 #[rstest]
1246 fn test_load_order_open_orders_response_from_file() {
1247 let json = include_str!("../../test_data/ws_order_open_orders_response.json");
1248 let msg: AxWsOpenOrdersResponse = serde_json::from_str(json).unwrap();
1249 assert_eq!(msg.res.orders.len(), 1);
1250 }
1251
1252 #[rstest]
1253 fn test_load_order_heartbeat_from_file() {
1254 let json = include_str!("../../test_data/ws_order_heartbeat.json");
1255 let msg: AxWsHeartbeat = serde_json::from_str(json).unwrap();
1256 assert_eq!(msg.ts, 1609459200);
1257 }
1258
1259 #[rstest]
1260 fn test_load_order_acknowledged_from_file() {
1261 let json = include_str!("../../test_data/ws_order_acknowledged.json");
1262 let msg: AxWsOrderAcknowledged = serde_json::from_str(json).unwrap();
1263 assert_eq!(msg.o.oid, "O-01ARZ3NDEKTSV4RRFFQ69G5FAV");
1264 }
1265
1266 #[rstest]
1267 fn test_load_order_filled_from_file() {
1268 let json = include_str!("../../test_data/ws_order_filled.json");
1269 let msg: AxWsOrderFilled = serde_json::from_str(json).unwrap();
1270 assert_eq!(msg.o.o, AxOrderStatus::Filled);
1271 assert_eq!(msg.xs.d, AxOrderSide::Buy);
1272 }
1273
1274 #[rstest]
1275 fn test_load_order_partially_filled_from_file() {
1276 let json = include_str!("../../test_data/ws_order_partially_filled.json");
1277 let msg: AxWsOrderPartiallyFilled = serde_json::from_str(json).unwrap();
1278 assert_eq!(msg.xs.q, 50);
1279 assert_eq!(msg.xs.d, AxOrderSide::Buy);
1280 }
1281
1282 #[rstest]
1283 fn test_load_order_canceled_from_file() {
1284 let json = include_str!("../../test_data/ws_order_canceled.json");
1285 let msg: AxWsOrderCanceled = serde_json::from_str(json).unwrap();
1286 assert_eq!(msg.xr, AxCancelReason::UserRequested);
1287 }
1288
1289 #[rstest]
1290 fn test_load_order_rejected_from_file() {
1291 let json = include_str!("../../test_data/ws_order_rejected.json");
1292 let msg: AxWsOrderRejected = serde_json::from_str(json).unwrap();
1293 assert_eq!(msg.r, Some(AxOrderRejectReason::InsufficientMargin));
1294 }
1295
1296 #[rstest]
1297 fn test_load_order_expired_from_file() {
1298 let json = include_str!("../../test_data/ws_order_expired.json");
1299 let msg: AxWsOrderExpired = serde_json::from_str(json).unwrap();
1300 assert_eq!(msg.o.tif, AxTimeInForce::Ioc);
1301 }
1302
1303 #[rstest]
1304 fn test_load_order_replaced_live_shape_from_file() {
1305 let json = include_str!("../../test_data/ws_order_replaced_live.json");
1306 let msg: AxWsOrderReplaced = serde_json::from_str(json).unwrap();
1307
1308 assert_eq!(msg.noid, "O-01KWY01WX8JT4DABKC6FRS5NT4");
1309 assert_eq!(msg.no.oid, "O-01KWY01WX8JT4DABKC6FRS5NT4");
1310 assert_eq!(msg.no.p, dec!(1.0926));
1311 assert_eq!(msg.ro.o, AxOrderStatus::Replaced);
1312 }
1313
1314 #[rstest]
1315 fn test_load_order_done_for_day_from_file() {
1316 let json = include_str!("../../test_data/ws_order_done_for_day.json");
1317 let msg: AxWsOrderDoneForDay = serde_json::from_str(json).unwrap();
1318 assert_eq!(msg.o.xq, 50);
1319 }
1320
1321 #[rstest]
1322 fn test_load_cancel_rejected_from_file() {
1323 let json = include_str!("../../test_data/ws_cancel_rejected.json");
1324 let msg: AxWsCancelRejected = serde_json::from_str(json).unwrap();
1325 assert_eq!(msg.r, AxCancelRejectionReason::OrderNotFound);
1326 }
1327
1328 #[rstest]
1329 fn test_load_order_error_response_from_file() {
1330 let json = include_str!("../../test_data/ws_order_error_response.json");
1331 let msg: AxWsOrderErrorResponse = serde_json::from_str(json).unwrap();
1332 assert_eq!(msg.rid, 1);
1333 assert_eq!(msg.err.code, 400);
1334 assert!(msg.err.msg.contains("initial margin"));
1335 }
1336
1337 #[rstest]
1338 fn test_load_order_list_response_from_file() {
1339 let json = include_str!("../../test_data/ws_order_list_response.json");
1340 let msg: AxWsListResponse = serde_json::from_str(json).unwrap();
1341 assert_eq!(msg.rid, 0);
1342 assert_eq!(msg.res.li, "01KCQM-4WP1-0000");
1343 assert!(msg.res.o.is_none());
1344 }
1345
1346 #[rstest]
1347 fn test_load_order_list_response_with_orders_from_file() {
1348 let json = include_str!("../../test_data/ws_order_list_response_with_orders.json");
1349 let msg: AxWsListResponse = serde_json::from_str(json).unwrap();
1350 assert_eq!(msg.rid, 0);
1351 assert_eq!(msg.res.li, "01KCQM-4WP1-0000");
1352 let orders = msg.res.o.unwrap();
1353 assert_eq!(orders.len(), 2);
1354 assert_eq!(orders[0].oid, "O-01KF4QM3VVJEDH98ZVNS1PCSBB");
1355 assert_eq!(orders[1].oid, "O-01KF4QM3K9FJZWYA02JF9Y1FJA");
1356 }
1357
1358 #[derive(Debug, Eq, PartialEq)]
1359 enum FrameKind {
1360 Error,
1361 ListResponse,
1362 AcknowledgedEvent,
1363 PlaceResponse,
1364 CancelResponse,
1365 OpenOrdersResponse,
1366 }
1367
1368 fn classify(frame: &AxOrdersWsFrame) -> FrameKind {
1369 match frame {
1370 AxOrdersWsFrame::Error(_) => FrameKind::Error,
1371 AxOrdersWsFrame::Response(AxWsOrderResponse::List(_)) => FrameKind::ListResponse,
1372 AxOrdersWsFrame::Response(AxWsOrderResponse::PlaceOrder(_)) => FrameKind::PlaceResponse,
1373 AxOrdersWsFrame::Response(AxWsOrderResponse::CancelOrder(_)) => {
1374 FrameKind::CancelResponse
1375 }
1376 AxOrdersWsFrame::Response(AxWsOrderResponse::OpenOrders(_)) => {
1377 FrameKind::OpenOrdersResponse
1378 }
1379 AxOrdersWsFrame::Event(e) => match **e {
1380 AxWsOrderEvent::Acknowledged(_) => FrameKind::AcknowledgedEvent,
1381 _ => panic!("unexpected event variant"),
1382 },
1383 }
1384 }
1385
1386 #[rstest]
1387 #[case::error(
1388 include_str!("../../test_data/ws_order_error_response.json"),
1389 FrameKind::Error,
1390 )]
1391 #[case::list(
1392 include_str!("../../test_data/ws_order_list_response.json"),
1393 FrameKind::ListResponse,
1394 )]
1395 #[case::acknowledged_event(
1396 include_str!("../../test_data/ws_order_acknowledged.json"),
1397 FrameKind::AcknowledgedEvent,
1398 )]
1399 #[case::place_response(
1400 include_str!("../../test_data/ws_order_place_response.json"),
1401 FrameKind::PlaceResponse,
1402 )]
1403 #[case::cancel_response(
1404 include_str!("../../test_data/ws_order_cancel_response.json"),
1405 FrameKind::CancelResponse,
1406 )]
1407 #[case::open_orders(
1408 include_str!("../../test_data/ws_order_open_orders_response.json"),
1409 FrameKind::OpenOrdersResponse,
1410 )]
1411 fn test_parse_order_message_variants(#[case] json: &str, #[case] expected: FrameKind) {
1412 let msg = parse_order_message(json).expect("should parse");
1413 assert_eq!(classify(&msg), expected);
1414 }
1415}