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::{deserialize_decimal_or_zero, deserialize_optional_decimal_from_str},
39 },
40 http::models::{AxEstimatedFundingRate, AxOrderRejectReason, AxRepriceBehavior},
41};
42
43#[derive(Debug, Clone)]
48pub enum AxDataWsMessage {
49 MdMessage(AxMdMessage),
51 Reconnected,
53 CandleUnsubscribed {
55 symbol: Ustr,
57 width: AxCandleWidth,
59 },
60}
61
62#[derive(Clone, Debug, Serialize, Deserialize)]
67pub struct AxMdSubscribe {
68 #[serde(alias = "request_id")]
70 pub rid: i64,
71 #[serde(rename = "type")]
73 pub msg_type: AxMdRequestType,
74 pub symbol: Ustr,
76 pub level: AxMarketDataLevel,
78 #[serde(skip_serializing_if = "Option::is_none")]
80 pub trades: Option<bool>,
81 #[serde(skip_serializing_if = "Option::is_none")]
83 pub ticker: Option<bool>,
84}
85
86#[derive(Clone, Debug, Serialize, Deserialize)]
91pub struct AxMdUnsubscribe {
92 #[serde(alias = "request_id")]
94 pub rid: i64,
95 #[serde(rename = "type")]
97 pub msg_type: AxMdRequestType,
98 pub symbol: Ustr,
100}
101
102#[derive(Clone, Debug, Serialize, Deserialize)]
107pub struct AxMdSubscribeCandles {
108 #[serde(alias = "request_id")]
110 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 #[serde(alias = "request_id")]
128 pub rid: i64,
129 #[serde(rename = "type")]
131 pub msg_type: AxMdRequestType,
132 pub symbol: Ustr,
134 pub width: AxCandleWidth,
136}
137
138#[derive(Clone, Debug, Serialize, Deserialize)]
143pub struct AxMdHeartbeat {
144 pub ts: i64,
146 pub tn: i64,
148}
149
150#[derive(Clone, Debug)]
154pub enum AxMdMessage {
155 BookL1(AxMdBookL1),
156 BookL2(AxMdBookL2),
157 BookL3(AxMdBookL3),
158 Ticker(Box<AxMdTicker>),
159 Trade(AxMdTrade),
160 Candle(AxMdCandle),
161 Heartbeat(AxMdHeartbeat),
162 SubscriptionResponse(AxMdSubscriptionResponse),
163 Error(AxWsError),
164}
165
166#[derive(Clone, Debug, Deserialize)]
168pub struct AxMdSubscriptionResponse {
169 #[serde(alias = "request_id")]
171 pub rid: i64,
172 #[serde(alias = "res")]
174 pub result: AxMdSubscriptionResult,
175}
176
177#[derive(Clone, Debug, Deserialize)]
179pub struct AxMdSubscriptionResult {
180 #[serde(default)]
182 pub subscribed: Option<Ustr>,
183 #[serde(default)]
185 pub subscribed_candle: Option<Ustr>,
186 #[serde(default)]
188 pub unsubscribed: Option<Ustr>,
189 #[serde(default)]
191 pub unsubscribed_candle: Option<Ustr>,
192}
193
194#[derive(Clone, Debug, Deserialize)]
196pub struct AxMdErrorResponse {
197 #[serde(alias = "request_id")]
199 pub rid: Option<i64>,
200 #[serde(alias = "err")]
202 pub error: AxMdErrorInner,
203}
204
205#[derive(Clone, Debug, Deserialize)]
207pub struct AxMdErrorInner {
208 pub code: i32,
210 pub message: String,
212}
213
214impl From<AxMdErrorResponse> for AxWsError {
215 fn from(resp: AxMdErrorResponse) -> Self {
216 Self {
217 code: Some(resp.error.code.to_string()),
218 message: resp.error.message,
219 request_id: resp.rid,
220 }
221 }
222}
223
224#[derive(Clone, Debug, Serialize, Deserialize)]
229pub struct AxMdTicker {
230 pub ts: i64,
232 pub tn: i64,
234 pub s: Ustr,
236 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
238 pub p: Option<Decimal>,
239 pub q: u64,
241 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
243 pub o: Option<Decimal>,
244 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
246 pub l: Option<Decimal>,
247 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
249 pub h: Option<Decimal>,
250 pub v: u64,
252 #[serde(default)]
254 pub oi: Option<i64>,
255 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
257 pub m: Option<Decimal>,
258 #[serde(default)]
260 pub i: Option<AxInstrumentState>,
261 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
263 pub pl: Option<Decimal>,
264 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
266 pub pu: Option<Decimal>,
267 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
269 pub lsp: Option<Decimal>,
270 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
272 pub bp: Option<Decimal>,
273 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
275 pub ap: Option<Decimal>,
276 #[serde(default)]
278 pub lst: Option<i64>,
279 #[serde(default)]
281 pub ef: Option<AxEstimatedFundingRate>,
282}
283
284#[derive(Clone, Debug, Serialize, Deserialize)]
289pub struct AxMdTrade {
290 pub ts: i64,
292 pub tn: i64,
294 pub s: Ustr,
296 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
298 pub p: Decimal,
299 pub q: u64,
301 #[serde(default)]
303 pub d: Option<AxOrderSide>,
304}
305
306#[derive(Clone, Debug, Serialize, Deserialize)]
311pub struct AxMdCandle {
312 pub symbol: Ustr,
314 pub ts: i64,
316 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
318 pub open: Decimal,
319 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
321 pub low: Decimal,
322 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
324 pub high: Decimal,
325 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
327 pub close: Decimal,
328 pub volume: u64,
330 pub buy_volume: u64,
332 pub sell_volume: u64,
334 pub width: AxCandleWidth,
336}
337
338#[derive(Clone, Debug, Serialize, Deserialize)]
340pub struct AxBookLevel {
341 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
343 pub p: Decimal,
344 pub q: u64,
346}
347
348#[derive(Clone, Debug, Serialize, Deserialize)]
350pub struct AxBookLevelL3 {
351 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
353 pub p: Decimal,
354 pub q: u64,
356 pub o: Vec<u64>,
358}
359
360#[derive(Clone, Debug, Serialize, Deserialize)]
365pub struct AxMdBookL1 {
366 pub ts: i64,
368 pub tn: i64,
370 pub s: Ustr,
372 pub b: Vec<AxBookLevel>,
374 pub a: Vec<AxBookLevel>,
376}
377
378#[derive(Clone, Debug, Serialize, Deserialize)]
386pub struct AxMdBookL2 {
387 pub ts: i64,
389 pub tn: i64,
391 pub s: Ustr,
393 pub b: Vec<AxBookLevel>,
395 pub a: Vec<AxBookLevel>,
397 #[serde(default)]
399 pub st: bool,
400}
401
402#[derive(Clone, Debug, Serialize, Deserialize)]
410pub struct AxMdBookL3 {
411 pub ts: i64,
413 pub tn: i64,
415 pub s: Ustr,
417 pub b: Vec<AxBookLevelL3>,
419 pub a: Vec<AxBookLevelL3>,
421 #[serde(default)]
423 pub st: bool,
424}
425
426#[derive(Clone, Debug, Serialize, Deserialize)]
431pub struct AxWsPlaceOrder {
432 #[serde(alias = "request_id")]
434 pub rid: i64,
435 pub t: AxOrderRequestType,
437 pub s: Ustr,
439 pub d: AxOrderSide,
441 pub q: u64,
443 #[serde(
445 serialize_with = "serialize_decimal_as_str",
446 deserialize_with = "deserialize_decimal_or_zero"
447 )]
448 pub p: Decimal,
449 pub tif: AxTimeInForce,
451 pub po: bool,
453 #[serde(skip_serializing_if = "Option::is_none")]
455 pub cid: Option<u64>,
456 #[serde(skip_serializing_if = "Option::is_none")]
458 pub tag: Option<String>,
459}
460
461#[derive(Clone, Debug, Serialize, Deserialize)]
466pub struct AxWsCancelOrder {
467 #[serde(alias = "request_id")]
469 pub rid: i64,
470 pub t: AxOrderRequestType,
472 pub oid: String,
474}
475
476#[derive(Clone, Debug, Serialize, Deserialize)]
481pub struct AxWsGetOpenOrders {
482 #[serde(alias = "request_id")]
484 pub rid: i64,
485 pub t: AxOrderRequestType,
487}
488
489#[derive(Clone, Debug, Serialize, Deserialize)]
494pub struct AxWsPlaceOrderResponse {
495 #[serde(alias = "request_id")]
497 pub rid: i64,
498 #[serde(alias = "result")]
500 pub res: AxWsPlaceOrderResult,
501}
502
503#[derive(Clone, Debug, Serialize, Deserialize)]
505pub struct AxWsPlaceOrderResult {
506 pub oid: String,
508}
509
510#[derive(Clone, Debug, Serialize, Deserialize)]
515pub struct AxWsCancelOrderResponse {
516 #[serde(alias = "request_id")]
518 pub rid: i64,
519 #[serde(alias = "result")]
521 pub res: AxWsCancelOrderResult,
522}
523
524#[derive(Clone, Debug, Serialize, Deserialize)]
526pub struct AxWsCancelOrderResult {
527 pub cxl_rx: bool,
529}
530
531#[derive(Clone, Debug, Serialize, Deserialize)]
536pub struct AxWsOpenOrdersResponse {
537 #[serde(alias = "request_id")]
539 pub rid: i64,
540 #[serde(alias = "result")]
542 pub res: AxWsOpenOrdersResult,
543}
544
545#[derive(Clone, Debug, Serialize, Deserialize)]
547pub struct AxWsOpenOrdersResult {
548 pub orders: Vec<AxWsOrder>,
550}
551
552#[derive(Clone, Debug, Deserialize)]
556pub struct AxWsOrderErrorResponse {
557 #[serde(alias = "request_id")]
559 pub rid: i64,
560 #[serde(alias = "error")]
562 pub err: AxWsOrderError,
563}
564
565#[derive(Clone, Debug, Deserialize)]
567pub struct AxWsOrderError {
568 pub code: Option<i64>,
570 pub msg: Option<String>,
572}
573
574#[derive(Clone, Debug, Deserialize)]
578pub struct AxWsListResponse {
579 #[serde(alias = "request_id")]
581 pub rid: i64,
582 #[serde(alias = "result")]
584 pub res: AxWsListResult,
585}
586
587#[derive(Clone, Debug, Deserialize)]
589pub struct AxWsListResult {
590 pub li: String,
592 #[serde(default)]
594 pub o: Option<Vec<AxWsOrder>>,
595 #[serde(default)]
597 pub cod: Option<bool>,
598 #[serde(default)]
600 pub chb: Option<u64>,
601}
602
603#[derive(Clone, Debug, Serialize, Deserialize)]
605pub struct AxWsOrder {
606 pub oid: String,
608 pub u: Ustr,
610 pub s: Ustr,
612 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
614 pub p: Decimal,
615 pub q: u64,
617 pub xq: u64,
619 pub rq: u64,
621 pub o: AxOrderStatus,
623 pub d: AxOrderSide,
625 pub tif: AxTimeInForce,
627 pub ts: i64,
629 pub tn: i64,
631 #[serde(default)]
633 pub cid: Option<u64>,
634 #[serde(default)]
636 pub tag: Option<String>,
637 #[serde(default)]
639 pub txt: Option<String>,
640 #[serde(default)]
642 pub aid: Option<Ustr>,
643 #[serde(default)]
645 pub po: bool,
646 #[serde(default)]
648 pub rb: Option<AxRepriceBehavior>,
649 #[serde(default)]
651 pub r: Option<AxOrderRejectReason>,
652}
653
654#[derive(Clone, Debug, Serialize, Deserialize)]
659pub struct AxWsHeartbeat {
660 pub t: AxOrderWsMessageType,
662 pub ts: i64,
664 pub tn: i64,
666}
667
668#[derive(Clone, Debug, Serialize, Deserialize)]
673pub struct AxWsOrderAcknowledged {
674 pub ts: i64,
676 pub tn: i64,
678 pub eid: String,
680 pub o: AxWsOrder,
682}
683
684#[derive(Clone, Debug, Serialize, Deserialize)]
686pub struct AxWsTradeExecution {
687 pub tid: String,
689 pub s: Ustr,
691 pub q: u64,
693 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
695 pub p: Decimal,
696 pub d: AxOrderSide,
698 pub agg: bool,
700 #[serde(default)]
702 pub aid: Option<Ustr>,
703}
704
705#[derive(Clone, Debug, Serialize, Deserialize)]
710pub struct AxWsOrderPartiallyFilled {
711 pub ts: i64,
713 pub tn: i64,
715 pub eid: String,
717 pub o: AxWsOrder,
719 pub xs: AxWsTradeExecution,
721}
722
723#[derive(Clone, Debug, Serialize, Deserialize)]
728pub struct AxWsOrderFilled {
729 pub ts: i64,
731 pub tn: i64,
733 pub eid: String,
735 pub o: AxWsOrder,
737 pub xs: AxWsTradeExecution,
739}
740
741#[derive(Clone, Debug, Serialize, Deserialize)]
746pub struct AxWsOrderCanceled {
747 pub ts: i64,
749 pub tn: i64,
751 pub eid: String,
753 pub o: AxWsOrder,
755 pub xr: AxCancelReason,
757 #[serde(default)]
759 pub txt: Option<String>,
760}
761
762#[derive(Clone, Debug, Serialize, Deserialize)]
767pub struct AxWsOrderRejected {
768 pub ts: i64,
770 pub tn: i64,
772 pub eid: String,
774 pub o: AxWsOrder,
776 #[serde(default)]
778 pub r: Option<AxOrderRejectReason>,
779 #[serde(default)]
781 pub txt: Option<String>,
782}
783
784#[derive(Clone, Debug, Serialize, Deserialize)]
789pub struct AxWsOrderExpired {
790 pub ts: i64,
792 pub tn: i64,
794 pub eid: String,
796 pub o: AxWsOrder,
798}
799
800#[derive(Clone, Debug, Serialize, Deserialize)]
805pub struct AxWsOrderReplaced {
806 pub ts: i64,
808 pub tn: i64,
810 pub eid: String,
812 pub ro: Box<AxWsOrder>,
814 #[serde(default)]
816 pub noid: Option<String>,
817 #[serde(default)]
819 pub no: Option<Box<AxWsOrder>>,
820}
821
822#[derive(Clone, Debug, Serialize, Deserialize)]
827pub struct AxWsOrderDoneForDay {
828 pub ts: i64,
830 pub tn: i64,
832 pub eid: String,
834 pub o: AxWsOrder,
836}
837
838#[derive(Clone, Debug, Serialize, Deserialize)]
843pub struct AxWsCancelRejected {
844 pub ts: i64,
846 pub tn: i64,
848 pub oid: String,
850 pub r: AxCancelRejectionReason,
852 #[serde(default)]
854 pub txt: Option<String>,
855 #[serde(default)]
857 pub cid: Option<u64>,
858 #[serde(default)]
860 pub o: Option<AxWsOrder>,
861}
862
863#[derive(Debug, Clone, Deserialize)]
868#[serde(tag = "t")]
869pub enum AxWsOrderEvent {
870 #[serde(rename = "h")]
872 Heartbeat,
873 #[serde(rename = "n")]
875 Acknowledged(AxWsOrderAcknowledged),
876 #[serde(rename = "p")]
878 PartiallyFilled(AxWsOrderPartiallyFilled),
879 #[serde(rename = "f")]
881 Filled(AxWsOrderFilled),
882 #[serde(rename = "c")]
884 Canceled(AxWsOrderCanceled),
885 #[serde(rename = "j")]
887 Rejected(AxWsOrderRejected),
888 #[serde(rename = "x")]
890 Expired(AxWsOrderExpired),
891 #[serde(rename = "r")]
893 Replaced(AxWsOrderReplaced),
894 #[serde(rename = "d")]
896 DoneForDay(AxWsOrderDoneForDay),
897 #[serde(rename = "e")]
899 CancelRejected(AxWsCancelRejected),
900}
901
902#[derive(Debug, Clone)]
906pub(crate) enum AxWsOrderResponse {
907 PlaceOrder(AxWsPlaceOrderResponse),
909 CancelOrder(AxWsCancelOrderResponse),
911 OpenOrders(AxWsOpenOrdersResponse),
913 List(AxWsListResponse),
915}
916
917#[derive(Debug, Clone)]
919pub(crate) enum AxOrdersWsFrame {
920 Error(AxWsOrderErrorResponse),
922 Response(AxWsOrderResponse),
924 Event(Box<AxWsOrderEvent>),
926}
927
928#[derive(Debug, Clone)]
933pub enum AxOrdersWsMessage {
934 Event(Box<AxWsOrderEvent>),
936 PlaceOrderResponse(AxWsPlaceOrderResponse),
938 CancelOrderResponse(AxWsCancelOrderResponse),
940 OpenOrdersResponse(AxWsOpenOrdersResponse),
942 Error(AxWsError),
944 Reconnected,
946 Authenticated,
948}
949
950#[derive(Debug, Clone)]
952pub struct AxWsError {
953 pub code: Option<String>,
955 pub message: String,
957 pub request_id: Option<i64>,
959}
960
961impl AxWsError {
962 #[must_use]
964 pub fn new(message: impl Into<String>) -> Self {
965 Self {
966 code: None,
967 message: message.into(),
968 request_id: None,
969 }
970 }
971
972 #[must_use]
974 pub fn with_code(code: impl Into<String>, message: impl Into<String>) -> Self {
975 Self {
976 code: Some(code.into()),
977 message: message.into(),
978 request_id: None,
979 }
980 }
981}
982
983impl From<AxWsOrderErrorResponse> for AxWsError {
984 fn from(resp: AxWsOrderErrorResponse) -> Self {
985 Self {
986 code: resp.err.code.map(|code| code.to_string()),
987 message: resp
988 .err
989 .msg
990 .unwrap_or_else(|| "AX request failed without an error message".to_owned()),
991 request_id: Some(resp.rid),
992 }
993 }
994}
995
996impl From<AxWsErrorResponse> for AxWsError {
997 fn from(resp: AxWsErrorResponse) -> Self {
998 Self {
999 code: resp.code,
1000 message: resp.message.unwrap_or_else(|| "Unknown error".to_string()),
1001 request_id: resp.rid,
1002 }
1003 }
1004}
1005
1006#[derive(Debug, Clone)]
1010pub struct OrderMetadata {
1011 pub trader_id: TraderId,
1013 pub strategy_id: StrategyId,
1015 pub instrument_id: InstrumentId,
1017 pub client_order_id: ClientOrderId,
1019 pub venue_order_id: Option<VenueOrderId>,
1021 pub ts_init: UnixNanos,
1023 pub size_precision: u8,
1025 pub price_precision: u8,
1027 pub quote_currency: Currency,
1029}
1030
1031#[cfg(test)]
1032mod tests {
1033 use rstest::rstest;
1034 use rust_decimal_macros::dec;
1035
1036 use super::{
1037 super::parse::{parse_md_message, parse_order_message},
1038 *,
1039 };
1040
1041 #[rstest]
1042 fn test_md_subscribe_serialization() {
1043 let msg = AxMdSubscribe {
1044 rid: 2,
1045 msg_type: AxMdRequestType::Subscribe,
1046 symbol: Ustr::from("EURUSD-PERP"),
1047 level: AxMarketDataLevel::Level2,
1048 trades: None,
1049 ticker: None,
1050 };
1051 let json = serde_json::to_string(&msg).unwrap();
1052 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1053
1054 assert_eq!(parsed["rid"], 2);
1055 assert_eq!(parsed["type"], "subscribe");
1056 assert_eq!(parsed["symbol"], "EURUSD-PERP");
1057 assert_eq!(parsed["level"], "LEVEL_2");
1058 assert!(parsed.get("trades").is_none());
1059 assert!(parsed.get("ticker").is_none());
1060 }
1061
1062 #[rstest]
1063 fn test_md_subscribe_book_only_serialization() {
1064 let msg = AxMdSubscribe {
1065 rid: 2,
1066 msg_type: AxMdRequestType::Subscribe,
1067 symbol: Ustr::from("EURUSD-PERP"),
1068 level: AxMarketDataLevel::Level2,
1069 trades: Some(false),
1070 ticker: Some(false),
1071 };
1072 let json = serde_json::to_string(&msg).unwrap();
1073 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1074
1075 assert_eq!(parsed["rid"], 2);
1076 assert_eq!(parsed["type"], "subscribe");
1077 assert_eq!(parsed["symbol"], "EURUSD-PERP");
1078 assert_eq!(parsed["level"], "LEVEL_2");
1079 assert_eq!(parsed["trades"], false);
1080 assert_eq!(parsed["ticker"], false);
1081 }
1082
1083 #[rstest]
1084 fn test_md_subscribe_trades_serialization() {
1085 let msg = AxMdSubscribe {
1086 rid: 2,
1087 msg_type: AxMdRequestType::Subscribe,
1088 symbol: Ustr::from("EURUSD-PERP"),
1089 level: AxMarketDataLevel::Trades,
1090 trades: None,
1091 ticker: None,
1092 };
1093 let json = serde_json::to_string(&msg).unwrap();
1094 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1095
1096 assert_eq!(parsed["rid"], 2);
1097 assert_eq!(parsed["type"], "subscribe");
1098 assert_eq!(parsed["symbol"], "EURUSD-PERP");
1099 assert_eq!(parsed["level"], "TRADES");
1100 assert!(parsed.get("trades").is_none());
1101 assert!(parsed.get("ticker").is_none());
1102 }
1103
1104 #[rstest]
1105 fn test_md_unsubscribe_serialization() {
1106 let msg = AxMdUnsubscribe {
1107 rid: 3,
1108 msg_type: AxMdRequestType::Unsubscribe,
1109 symbol: Ustr::from("EURUSD-PERP"),
1110 };
1111 let json = serde_json::to_string(&msg).unwrap();
1112 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1113
1114 assert_eq!(parsed["rid"], 3);
1115 assert_eq!(parsed["type"], "unsubscribe");
1116 assert_eq!(parsed["symbol"], "EURUSD-PERP");
1117 }
1118
1119 #[rstest]
1120 fn test_md_subscribe_candles_serialization() {
1121 let msg = AxMdSubscribeCandles {
1122 rid: 4,
1123 msg_type: AxMdRequestType::SubscribeCandles,
1124 symbol: Ustr::from("EURUSD-PERP"),
1125 width: AxCandleWidth::Minutes1,
1126 };
1127 let json = serde_json::to_string(&msg).unwrap();
1128 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1129
1130 assert_eq!(parsed["rid"], 4);
1131 assert_eq!(parsed["type"], "subscribe_candles");
1132 assert_eq!(parsed["symbol"], "EURUSD-PERP");
1133 assert_eq!(parsed["width"], "1m");
1134 }
1135
1136 #[rstest]
1137 fn test_md_unsubscribe_candles_serialization() {
1138 let msg = AxMdUnsubscribeCandles {
1139 rid: 5,
1140 msg_type: AxMdRequestType::UnsubscribeCandles,
1141 symbol: Ustr::from("EURUSD-PERP"),
1142 width: AxCandleWidth::Minutes1,
1143 };
1144 let json = serde_json::to_string(&msg).unwrap();
1145 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1146
1147 assert_eq!(parsed["rid"], 5);
1148 assert_eq!(parsed["type"], "unsubscribe_candles");
1149 assert_eq!(parsed["symbol"], "EURUSD-PERP");
1150 assert_eq!(parsed["width"], "1m");
1151 }
1152
1153 #[rstest]
1154 fn test_ws_place_order_serialization() {
1155 let msg = AxWsPlaceOrder {
1156 rid: 1,
1157 t: AxOrderRequestType::PlaceOrder,
1158 s: Ustr::from("EURUSD-PERP"),
1159 d: AxOrderSide::Buy,
1160 q: 100,
1161 p: dec!(50000.50),
1162 tif: AxTimeInForce::Gtc,
1163 po: false,
1164 tag: Some("Nautilus".to_string()),
1165 cid: Some(1234567890),
1166 };
1167
1168 let json = serde_json::to_string(&msg).unwrap();
1169 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1170
1171 assert_eq!(parsed["rid"], 1);
1172 assert_eq!(parsed["t"], "p");
1173 assert_eq!(parsed["s"], "EURUSD-PERP");
1174 assert_eq!(parsed["d"], "B");
1175 assert_eq!(parsed["q"], 100);
1176 assert_eq!(parsed["p"], "50000.50");
1177 assert_eq!(parsed["tif"], "GTC");
1178 assert_eq!(parsed["po"], false);
1179 assert_eq!(parsed["tag"], "Nautilus");
1180 assert_eq!(parsed["cid"], 1234567890);
1181 assert!(parsed.get("order_type").is_none());
1182 assert!(parsed.get("trigger_price").is_none());
1183 }
1184
1185 #[rstest]
1186 fn test_ws_cancel_order_serialization() {
1187 let msg = AxWsCancelOrder {
1188 rid: 2,
1189 t: AxOrderRequestType::CancelOrder,
1190 oid: "O-01ARZ3NDEKTSV4RRFFQ69G5FAV".to_string(),
1191 };
1192 let json = serde_json::to_string(&msg).unwrap();
1193 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1194
1195 assert_eq!(parsed["rid"], 2);
1196 assert_eq!(parsed["t"], "x");
1197 assert_eq!(parsed["oid"], "O-01ARZ3NDEKTSV4RRFFQ69G5FAV");
1198 }
1199
1200 #[rstest]
1201 fn test_ws_get_open_orders_serialization() {
1202 let msg = AxWsGetOpenOrders {
1203 rid: 3,
1204 t: AxOrderRequestType::GetOpenOrders,
1205 };
1206 let json = serde_json::to_string(&msg).unwrap();
1207 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1208
1209 assert_eq!(parsed["rid"], 3);
1210 assert_eq!(parsed["t"], "o");
1211 }
1212
1213 #[rstest]
1214 fn test_load_md_heartbeat_from_file() {
1215 let json = include_str!("../../test_data/ws_md_heartbeat.json");
1216 let msg = parse_md_message(json).unwrap();
1217 assert!(matches!(msg, AxMdMessage::Heartbeat(_)));
1218 }
1219
1220 #[rstest]
1221 fn test_load_md_ticker_from_file() {
1222 let json = include_str!("../../test_data/ws_md_ticker.json");
1223 let msg: AxMdTicker = serde_json::from_str(json).unwrap();
1224 assert_eq!(msg.s, "EURUSD-PERP");
1225 assert_eq!(msg.m, Some(dec!(50010.50)));
1226 assert_eq!(msg.i, Some(AxInstrumentState::Open));
1227 }
1228
1229 #[rstest]
1230 fn test_load_md_ticker_captured_optional_fields_default_to_none() {
1231 let json = include_str!("../../test_data/ws_md_ticker_captured.json");
1232 let msg: AxMdTicker = serde_json::from_str(json).unwrap();
1233 assert_eq!(msg.s, "EURUSD-PERP");
1234 assert_eq!(msg.m, None);
1235 assert_eq!(msg.i, None);
1236 }
1237
1238 #[rstest]
1239 fn test_load_md_ticker_null_prices_decode() {
1240 let json = include_str!("../../test_data/ws_md_ticker_null_prices.json");
1242 let msg = parse_md_message(json).unwrap();
1243 let AxMdMessage::Ticker(ticker) = msg else {
1244 panic!("expected ticker message");
1245 };
1246 assert_eq!(ticker.s, "QQQ-PERP");
1247 assert_eq!(ticker.p, None);
1248 assert_eq!(ticker.o, None);
1249 assert_eq!(ticker.l, None);
1250 assert_eq!(ticker.h, None);
1251 assert_eq!(ticker.m, Some(dec!(716.38)));
1252 assert_eq!(ticker.i, Some(AxInstrumentState::Open));
1253 }
1254
1255 #[rstest]
1256 fn test_load_md_trade_from_file() {
1257 let json = include_str!("../../test_data/ws_md_trade.json");
1258 let msg: AxMdTrade = serde_json::from_str(json).unwrap();
1259 assert_eq!(msg.d, Some(AxOrderSide::Buy));
1260 }
1261
1262 #[rstest]
1263 fn test_load_md_candle_from_file() {
1264 let json = include_str!("../../test_data/ws_md_candle.json");
1265 let msg: AxMdCandle = serde_json::from_str(json).unwrap();
1266 assert_eq!(msg.width, AxCandleWidth::Minutes1);
1267 }
1268
1269 #[rstest]
1270 fn test_load_md_book_l1_from_file() {
1271 let json = include_str!("../../test_data/ws_md_book_l1.json");
1272 let msg: AxMdBookL1 = serde_json::from_str(json).unwrap();
1273 assert_eq!(msg.b.len(), 1);
1274 assert_eq!(msg.a.len(), 1);
1275 }
1276
1277 #[rstest]
1278 fn test_load_md_book_l2_from_file() {
1279 let json = include_str!("../../test_data/ws_md_book_l2.json");
1280 let msg: AxMdBookL2 = serde_json::from_str(json).unwrap();
1281 assert_eq!(msg.b.len(), 3);
1282 assert_eq!(msg.a.len(), 3);
1283 }
1284
1285 #[rstest]
1286 fn test_load_md_book_l3_from_file() {
1287 let json = include_str!("../../test_data/ws_md_book_l3.json");
1288 let msg: AxMdBookL3 = serde_json::from_str(json).unwrap();
1289 assert_eq!(msg.b.len(), 2);
1290 assert!(!msg.b[0].o.is_empty());
1291 }
1292
1293 #[rstest]
1294 fn test_load_order_place_response_from_file() {
1295 let json = include_str!("../../test_data/ws_order_place_response.json");
1296 let msg: AxWsPlaceOrderResponse = serde_json::from_str(json).unwrap();
1297 assert_eq!(msg.res.oid, "O-01ARZ3NDEKTSV4RRFFQ69G5FAV");
1298 }
1299
1300 #[rstest]
1301 fn test_load_order_cancel_response_from_file() {
1302 let json = include_str!("../../test_data/ws_order_cancel_response.json");
1303 let msg: AxWsCancelOrderResponse = serde_json::from_str(json).unwrap();
1304 assert!(msg.res.cxl_rx);
1305 }
1306
1307 #[rstest]
1308 fn test_load_order_open_orders_response_from_file() {
1309 let json = include_str!("../../test_data/ws_order_open_orders_response.json");
1310 let msg: AxWsOpenOrdersResponse = serde_json::from_str(json).unwrap();
1311 assert_eq!(msg.res.orders.len(), 1);
1312 }
1313
1314 #[rstest]
1315 fn test_load_order_heartbeat_from_file() {
1316 let json = include_str!("../../test_data/ws_order_heartbeat.json");
1317 let msg: AxWsHeartbeat = serde_json::from_str(json).unwrap();
1318 assert_eq!(msg.ts, 1609459200);
1319 }
1320
1321 #[rstest]
1322 fn test_load_order_acknowledged_from_file() {
1323 let json = include_str!("../../test_data/ws_order_acknowledged.json");
1324 let msg: AxWsOrderAcknowledged = serde_json::from_str(json).unwrap();
1325 assert_eq!(msg.o.oid, "O-01ARZ3NDEKTSV4RRFFQ69G5FAV");
1326 }
1327
1328 #[rstest]
1329 fn test_load_order_filled_from_file() {
1330 let json = include_str!("../../test_data/ws_order_filled.json");
1331 let msg: AxWsOrderFilled = serde_json::from_str(json).unwrap();
1332 assert_eq!(msg.o.o, AxOrderStatus::Filled);
1333 assert_eq!(msg.xs.d, AxOrderSide::Buy);
1334 }
1335
1336 #[rstest]
1337 fn test_load_order_partially_filled_from_file() {
1338 let json = include_str!("../../test_data/ws_order_partially_filled.json");
1339 let msg: AxWsOrderPartiallyFilled = serde_json::from_str(json).unwrap();
1340 assert_eq!(msg.xs.q, 50);
1341 assert_eq!(msg.xs.d, AxOrderSide::Buy);
1342 }
1343
1344 #[rstest]
1345 fn test_load_order_canceled_from_file() {
1346 let json = include_str!("../../test_data/ws_order_canceled.json");
1347 let msg: AxWsOrderCanceled = serde_json::from_str(json).unwrap();
1348 assert_eq!(msg.xr, AxCancelReason::UserRequested);
1349 }
1350
1351 #[rstest]
1352 fn test_load_order_rejected_from_file() {
1353 let json = include_str!("../../test_data/ws_order_rejected.json");
1354 let msg: AxWsOrderRejected = serde_json::from_str(json).unwrap();
1355 assert_eq!(msg.r, Some(AxOrderRejectReason::InsufficientMargin));
1356 }
1357
1358 #[rstest]
1359 fn test_load_order_expired_from_file() {
1360 let json = include_str!("../../test_data/ws_order_expired.json");
1361 let msg: AxWsOrderExpired = serde_json::from_str(json).unwrap();
1362 assert_eq!(msg.o.tif, AxTimeInForce::Ioc);
1363 }
1364
1365 #[rstest]
1366 fn test_load_order_replaced_live_shape_from_file() {
1367 let json = include_str!("../../test_data/ws_order_replaced_live.json");
1368 let msg: AxWsOrderReplaced = serde_json::from_str(json).unwrap();
1369
1370 assert_eq!(msg.noid.as_deref(), Some("O-01KWY01WX8JT4DABKC6FRS5NT4"));
1371 assert_eq!(msg.no.as_ref().unwrap().oid, "O-01KWY01WX8JT4DABKC6FRS5NT4");
1372 assert_eq!(msg.no.as_ref().unwrap().p, dec!(1.0926));
1373 assert_eq!(msg.ro.o, AxOrderStatus::Replaced);
1374 }
1375
1376 #[rstest]
1377 fn test_load_order_done_for_day_from_file() {
1378 let json = include_str!("../../test_data/ws_order_done_for_day.json");
1379 let msg: AxWsOrderDoneForDay = serde_json::from_str(json).unwrap();
1380 assert_eq!(msg.o.xq, 50);
1381 }
1382
1383 #[rstest]
1384 fn test_load_cancel_rejected_from_file() {
1385 let json = include_str!("../../test_data/ws_cancel_rejected.json");
1386 let msg: AxWsCancelRejected = serde_json::from_str(json).unwrap();
1387 assert_eq!(msg.r, AxCancelRejectionReason::OrderNotFound);
1388 }
1389
1390 #[rstest]
1391 fn test_load_order_error_response_from_file() {
1392 let json = include_str!("../../test_data/ws_order_error_response.json");
1393 let msg: AxWsOrderErrorResponse = serde_json::from_str(json).unwrap();
1394 assert_eq!(msg.rid, 1);
1395 assert_eq!(msg.err.code, Some(400));
1396 assert!(msg.err.msg.as_deref().unwrap().contains("initial margin"));
1397 }
1398
1399 #[rstest]
1400 fn test_load_order_list_response_from_file() {
1401 let json = include_str!("../../test_data/ws_order_list_response.json");
1402 let msg: AxWsListResponse = serde_json::from_str(json).unwrap();
1403 assert_eq!(msg.rid, 0);
1404 assert_eq!(msg.res.li, "01KCQM-4WP1-0000");
1405 assert!(msg.res.o.is_none());
1406 }
1407
1408 #[rstest]
1409 fn test_load_order_list_response_with_orders_from_file() {
1410 let json = include_str!("../../test_data/ws_order_list_response_with_orders.json");
1411 let msg: AxWsListResponse = serde_json::from_str(json).unwrap();
1412 assert_eq!(msg.rid, 0);
1413 assert_eq!(msg.res.li, "01KCQM-4WP1-0000");
1414 let orders = msg.res.o.unwrap();
1415 assert_eq!(orders.len(), 2);
1416 assert_eq!(orders[0].oid, "O-01KF4QM3VVJEDH98ZVNS1PCSBB");
1417 assert_eq!(orders[1].oid, "O-01KF4QM3K9FJZWYA02JF9Y1FJA");
1418 }
1419
1420 #[derive(Debug, Eq, PartialEq)]
1421 enum FrameKind {
1422 Error,
1423 ListResponse,
1424 AcknowledgedEvent,
1425 PlaceResponse,
1426 CancelResponse,
1427 OpenOrdersResponse,
1428 }
1429
1430 fn classify(frame: &AxOrdersWsFrame) -> FrameKind {
1431 match frame {
1432 AxOrdersWsFrame::Error(_) => FrameKind::Error,
1433 AxOrdersWsFrame::Response(AxWsOrderResponse::List(_)) => FrameKind::ListResponse,
1434 AxOrdersWsFrame::Response(AxWsOrderResponse::PlaceOrder(_)) => FrameKind::PlaceResponse,
1435 AxOrdersWsFrame::Response(AxWsOrderResponse::CancelOrder(_)) => {
1436 FrameKind::CancelResponse
1437 }
1438 AxOrdersWsFrame::Response(AxWsOrderResponse::OpenOrders(_)) => {
1439 FrameKind::OpenOrdersResponse
1440 }
1441 AxOrdersWsFrame::Event(e) => match **e {
1442 AxWsOrderEvent::Acknowledged(_) => FrameKind::AcknowledgedEvent,
1443 _ => panic!("unexpected event variant"),
1444 },
1445 }
1446 }
1447
1448 #[rstest]
1449 #[case::error(
1450 include_str!("../../test_data/ws_order_error_response.json"),
1451 FrameKind::Error,
1452 )]
1453 #[case::list(
1454 include_str!("../../test_data/ws_order_list_response.json"),
1455 FrameKind::ListResponse,
1456 )]
1457 #[case::acknowledged_event(
1458 include_str!("../../test_data/ws_order_acknowledged.json"),
1459 FrameKind::AcknowledgedEvent,
1460 )]
1461 #[case::place_response(
1462 include_str!("../../test_data/ws_order_place_response.json"),
1463 FrameKind::PlaceResponse,
1464 )]
1465 #[case::cancel_response(
1466 include_str!("../../test_data/ws_order_cancel_response.json"),
1467 FrameKind::CancelResponse,
1468 )]
1469 #[case::open_orders(
1470 include_str!("../../test_data/ws_order_open_orders_response.json"),
1471 FrameKind::OpenOrdersResponse,
1472 )]
1473 fn test_parse_order_message_variants(#[case] json: &str, #[case] expected: FrameKind) {
1474 let msg = parse_order_message(json).expect("should parse");
1475 assert_eq!(classify(&msg), expected);
1476 }
1477 #[rstest]
1478 fn test_ticker_missing_prices_remain_absent() {
1479 let mut value: serde_json::Value = serde_json::from_str(include_str!(
1480 "../../test_data/ws_md_ticker_null_prices.json"
1481 ))
1482 .unwrap();
1483
1484 for field in ["p", "o", "l", "h"] {
1485 value.as_object_mut().unwrap().remove(field);
1486 }
1487
1488 let ticker: AxMdTicker = serde_json::from_value(value).unwrap();
1489 assert_eq!(
1490 (ticker.p, ticker.o, ticker.l, ticker.h),
1491 (None, None, None, None)
1492 );
1493 assert_eq!(ticker.m, Some(dec!(716.38)));
1494 }
1495
1496 #[rstest]
1497 #[case(include_str!("../../test_data/captured/open-reject.json"))]
1498 #[case(include_str!("../../test_data/captured/open-backoff.json"))]
1499 #[case(include_str!("../../test_data/captured/open-top.json"))]
1500 #[case(include_str!("../../test_data/captured/open-inactive.json"))]
1501 fn test_ws_order_preserves_captured_metadata(#[case] raw: &str) {
1502 let response: serde_json::Value = serde_json::from_str(raw).unwrap();
1503 for value in response["orders"].as_array().unwrap() {
1504 let order: AxWsOrder = serde_json::from_value(value.clone()).unwrap();
1505 let actual = serde_json::to_value(order).unwrap();
1506
1507 for field in ["aid", "po", "rb", "ts", "tn", "oid", "q", "xq", "rq"] {
1508 assert_eq!(actual[field], value[field], "{field}");
1509 }
1510 }
1511 }
1512}