1use nautilus_core::{UnixNanos, serialization::serialize_decimal_as_str};
22use nautilus_model::{
23 events::OrderInitialized,
24 identifiers::{ClientOrderId, InstrumentId, StrategyId, TraderId, VenueOrderId},
25 types::Currency,
26};
27use rust_decimal::Decimal;
28use serde::{Deserialize, Serialize};
29use ustr::Ustr;
30
31use super::error::AxWsErrorResponse;
32use crate::{
33 common::{
34 enums::{
35 AxCancelReason, AxCancelRejectionReason, AxCandleWidth, AxInstrumentState,
36 AxMarketDataLevel, AxMdRequestType, AxOrderRequestType, AxOrderSide, AxOrderStatus,
37 AxOrderWsMessageType, AxTimeInForce,
38 },
39 parse::{deserialize_decimal_or_zero, deserialize_optional_decimal_from_str},
40 },
41 http::models::{AxEstimatedFundingRate, AxOrderRejectReason, AxRepriceBehavior},
42};
43
44#[derive(Debug, Clone)]
49pub enum AxDataWsMessage {
50 MdMessage(AxMdMessage),
52 Reconnected,
54 CandleUnsubscribed {
56 symbol: Ustr,
58 width: AxCandleWidth,
60 },
61}
62
63#[derive(Clone, Debug, Serialize, Deserialize)]
68pub struct AxMdSubscribe {
69 #[serde(alias = "request_id")]
71 pub rid: i64,
72 #[serde(rename = "type")]
74 pub msg_type: AxMdRequestType,
75 pub symbol: Ustr,
77 pub level: AxMarketDataLevel,
79 #[serde(skip_serializing_if = "Option::is_none")]
81 pub trades: Option<bool>,
82 #[serde(skip_serializing_if = "Option::is_none")]
84 pub ticker: Option<bool>,
85}
86
87#[derive(Clone, Debug, Serialize, Deserialize)]
92pub struct AxMdUnsubscribe {
93 #[serde(alias = "request_id")]
95 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 #[serde(alias = "request_id")]
111 pub rid: i64,
112 #[serde(rename = "type")]
114 pub msg_type: AxMdRequestType,
115 pub symbol: Ustr,
117 pub width: AxCandleWidth,
119}
120
121#[derive(Clone, Debug, Serialize, Deserialize)]
126pub struct AxMdUnsubscribeCandles {
127 #[serde(alias = "request_id")]
129 pub rid: i64,
130 #[serde(rename = "type")]
132 pub msg_type: AxMdRequestType,
133 pub symbol: Ustr,
135 pub width: AxCandleWidth,
137}
138
139#[derive(Clone, Debug, Serialize, Deserialize)]
144pub struct AxMdHeartbeat {
145 pub ts: i64,
147 pub tn: i64,
149}
150
151#[derive(Clone, Debug)]
155pub enum AxMdMessage {
156 BookL1(AxMdBookL1),
157 BookL2(AxMdBookL2),
158 BookL3(AxMdBookL3),
159 Ticker(Box<AxMdTicker>),
160 Trade(AxMdTrade),
161 Candle(AxMdCandle),
162 Heartbeat(AxMdHeartbeat),
163 SubscriptionResponse(AxMdSubscriptionResponse),
164 Error(AxWsError),
165}
166
167#[derive(Clone, Debug, Deserialize)]
169pub struct AxMdSubscriptionResponse {
170 #[serde(alias = "request_id")]
172 pub rid: i64,
173 #[serde(alias = "res")]
175 pub result: AxMdSubscriptionResult,
176}
177
178#[derive(Clone, Debug, Deserialize)]
180pub struct AxMdSubscriptionResult {
181 #[serde(default)]
183 pub subscribed: Option<Ustr>,
184 #[serde(default)]
186 pub subscribed_candle: Option<Ustr>,
187 #[serde(default)]
189 pub unsubscribed: Option<Ustr>,
190 #[serde(default)]
192 pub unsubscribed_candle: Option<Ustr>,
193}
194
195#[derive(Clone, Debug, Deserialize)]
197pub struct AxMdErrorResponse {
198 #[serde(alias = "request_id")]
200 pub rid: Option<i64>,
201 #[serde(alias = "err")]
203 pub error: AxMdErrorInner,
204}
205
206#[derive(Clone, Debug, Deserialize)]
208pub struct AxMdErrorInner {
209 pub code: i32,
211 pub message: String,
213}
214
215impl From<AxMdErrorResponse> for AxWsError {
216 fn from(resp: AxMdErrorResponse) -> Self {
217 Self {
218 code: Some(resp.error.code.to_string()),
219 message: resp.error.message,
220 request_id: resp.rid,
221 }
222 }
223}
224
225#[derive(Clone, Debug, Serialize, Deserialize)]
230pub struct AxMdTicker {
231 pub ts: i64,
233 pub tn: i64,
235 pub s: Ustr,
237 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
239 pub p: Option<Decimal>,
240 pub q: u64,
242 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
244 pub o: Option<Decimal>,
245 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
247 pub l: Option<Decimal>,
248 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
250 pub h: Option<Decimal>,
251 pub v: u64,
253 #[serde(default)]
255 pub oi: Option<i64>,
256 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
258 pub m: Option<Decimal>,
259 #[serde(default)]
261 pub i: Option<AxInstrumentState>,
262 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
264 pub pl: Option<Decimal>,
265 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
267 pub pu: Option<Decimal>,
268 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
270 pub lsp: Option<Decimal>,
271 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
273 pub bp: Option<Decimal>,
274 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_str")]
276 pub ap: Option<Decimal>,
277 #[serde(default)]
279 pub lst: Option<i64>,
280 #[serde(default)]
282 pub ef: Option<AxEstimatedFundingRate>,
283}
284
285#[derive(Clone, Debug, Serialize, Deserialize)]
290pub struct AxMdTrade {
291 pub ts: i64,
293 pub tn: i64,
295 pub s: Ustr,
297 #[serde(deserialize_with = "deserialize_decimal_or_zero")]
299 pub p: Decimal,
300 pub q: u64,
302 pub d: 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 order_init: Option<OrderInitialized>,
1022 pub is_accepted: bool,
1024 pub venue_order_id: Option<VenueOrderId>,
1026 pub ts_init: UnixNanos,
1028 pub size_precision: u8,
1030 pub price_precision: u8,
1032 pub quote_currency: Currency,
1034}
1035
1036#[cfg(test)]
1037mod tests {
1038 use rstest::rstest;
1039 use rust_decimal_macros::dec;
1040
1041 use super::{
1042 super::parse::{parse_md_message, parse_order_message},
1043 *,
1044 };
1045
1046 #[rstest]
1047 fn test_md_subscribe_serialization() {
1048 let msg = AxMdSubscribe {
1049 rid: 2,
1050 msg_type: AxMdRequestType::Subscribe,
1051 symbol: Ustr::from("EURUSD-PERP"),
1052 level: AxMarketDataLevel::Level2,
1053 trades: None,
1054 ticker: None,
1055 };
1056 let json = serde_json::to_string(&msg).unwrap();
1057 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1058
1059 assert_eq!(parsed["rid"], 2);
1060 assert_eq!(parsed["type"], "subscribe");
1061 assert_eq!(parsed["symbol"], "EURUSD-PERP");
1062 assert_eq!(parsed["level"], "LEVEL_2");
1063 assert!(parsed.get("trades").is_none());
1064 assert!(parsed.get("ticker").is_none());
1065 }
1066
1067 #[rstest]
1068 fn test_md_subscribe_book_only_serialization() {
1069 let msg = AxMdSubscribe {
1070 rid: 2,
1071 msg_type: AxMdRequestType::Subscribe,
1072 symbol: Ustr::from("EURUSD-PERP"),
1073 level: AxMarketDataLevel::Level2,
1074 trades: Some(false),
1075 ticker: Some(false),
1076 };
1077 let json = serde_json::to_string(&msg).unwrap();
1078 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1079
1080 assert_eq!(parsed["rid"], 2);
1081 assert_eq!(parsed["type"], "subscribe");
1082 assert_eq!(parsed["symbol"], "EURUSD-PERP");
1083 assert_eq!(parsed["level"], "LEVEL_2");
1084 assert_eq!(parsed["trades"], false);
1085 assert_eq!(parsed["ticker"], false);
1086 }
1087
1088 #[rstest]
1089 fn test_md_subscribe_trades_serialization() {
1090 let msg = AxMdSubscribe {
1091 rid: 2,
1092 msg_type: AxMdRequestType::Subscribe,
1093 symbol: Ustr::from("EURUSD-PERP"),
1094 level: AxMarketDataLevel::Trades,
1095 trades: None,
1096 ticker: None,
1097 };
1098 let json = serde_json::to_string(&msg).unwrap();
1099 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1100
1101 assert_eq!(parsed["rid"], 2);
1102 assert_eq!(parsed["type"], "subscribe");
1103 assert_eq!(parsed["symbol"], "EURUSD-PERP");
1104 assert_eq!(parsed["level"], "TRADES");
1105 assert!(parsed.get("trades").is_none());
1106 assert!(parsed.get("ticker").is_none());
1107 }
1108
1109 #[rstest]
1110 fn test_md_unsubscribe_serialization() {
1111 let msg = AxMdUnsubscribe {
1112 rid: 3,
1113 msg_type: AxMdRequestType::Unsubscribe,
1114 symbol: Ustr::from("EURUSD-PERP"),
1115 };
1116 let json = serde_json::to_string(&msg).unwrap();
1117 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1118
1119 assert_eq!(parsed["rid"], 3);
1120 assert_eq!(parsed["type"], "unsubscribe");
1121 assert_eq!(parsed["symbol"], "EURUSD-PERP");
1122 }
1123
1124 #[rstest]
1125 fn test_md_subscribe_candles_serialization() {
1126 let msg = AxMdSubscribeCandles {
1127 rid: 4,
1128 msg_type: AxMdRequestType::SubscribeCandles,
1129 symbol: Ustr::from("EURUSD-PERP"),
1130 width: AxCandleWidth::Minutes1,
1131 };
1132 let json = serde_json::to_string(&msg).unwrap();
1133 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1134
1135 assert_eq!(parsed["rid"], 4);
1136 assert_eq!(parsed["type"], "subscribe_candles");
1137 assert_eq!(parsed["symbol"], "EURUSD-PERP");
1138 assert_eq!(parsed["width"], "1m");
1139 }
1140
1141 #[rstest]
1142 fn test_md_unsubscribe_candles_serialization() {
1143 let msg = AxMdUnsubscribeCandles {
1144 rid: 5,
1145 msg_type: AxMdRequestType::UnsubscribeCandles,
1146 symbol: Ustr::from("EURUSD-PERP"),
1147 width: AxCandleWidth::Minutes1,
1148 };
1149 let json = serde_json::to_string(&msg).unwrap();
1150 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1151
1152 assert_eq!(parsed["rid"], 5);
1153 assert_eq!(parsed["type"], "unsubscribe_candles");
1154 assert_eq!(parsed["symbol"], "EURUSD-PERP");
1155 assert_eq!(parsed["width"], "1m");
1156 }
1157
1158 #[rstest]
1159 fn test_ws_place_order_serialization() {
1160 let msg = AxWsPlaceOrder {
1161 rid: 1,
1162 t: AxOrderRequestType::PlaceOrder,
1163 s: Ustr::from("EURUSD-PERP"),
1164 d: AxOrderSide::Buy,
1165 q: 100,
1166 p: dec!(50000.50),
1167 tif: AxTimeInForce::Gtc,
1168 po: false,
1169 tag: Some("Nautilus".to_string()),
1170 cid: Some(1234567890),
1171 };
1172
1173 let json = serde_json::to_string(&msg).unwrap();
1174 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1175
1176 assert_eq!(parsed["rid"], 1);
1177 assert_eq!(parsed["t"], "p");
1178 assert_eq!(parsed["s"], "EURUSD-PERP");
1179 assert_eq!(parsed["d"], "B");
1180 assert_eq!(parsed["q"], 100);
1181 assert_eq!(parsed["p"], "50000.50");
1182 assert_eq!(parsed["tif"], "GTC");
1183 assert_eq!(parsed["po"], false);
1184 assert_eq!(parsed["tag"], "Nautilus");
1185 assert_eq!(parsed["cid"], 1234567890);
1186 assert!(parsed.get("order_type").is_none());
1187 assert!(parsed.get("trigger_price").is_none());
1188 }
1189
1190 #[rstest]
1191 fn test_ws_cancel_order_serialization() {
1192 let msg = AxWsCancelOrder {
1193 rid: 2,
1194 t: AxOrderRequestType::CancelOrder,
1195 oid: "O-01ARZ3NDEKTSV4RRFFQ69G5FAV".to_string(),
1196 };
1197 let json = serde_json::to_string(&msg).unwrap();
1198 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1199
1200 assert_eq!(parsed["rid"], 2);
1201 assert_eq!(parsed["t"], "x");
1202 assert_eq!(parsed["oid"], "O-01ARZ3NDEKTSV4RRFFQ69G5FAV");
1203 }
1204
1205 #[rstest]
1206 fn test_ws_get_open_orders_serialization() {
1207 let msg = AxWsGetOpenOrders {
1208 rid: 3,
1209 t: AxOrderRequestType::GetOpenOrders,
1210 };
1211 let json = serde_json::to_string(&msg).unwrap();
1212 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1213
1214 assert_eq!(parsed["rid"], 3);
1215 assert_eq!(parsed["t"], "o");
1216 }
1217
1218 #[rstest]
1219 fn test_load_md_heartbeat_from_file() {
1220 let json = include_str!("../../test_data/ws_md_heartbeat.json");
1221 let msg = parse_md_message(json).unwrap();
1222 assert!(matches!(msg, AxMdMessage::Heartbeat(_)));
1223 }
1224
1225 #[rstest]
1226 fn test_load_md_ticker_from_file() {
1227 let json = include_str!("../../test_data/ws_md_ticker.json");
1228 let msg: AxMdTicker = serde_json::from_str(json).unwrap();
1229 assert_eq!(msg.s, "EURUSD-PERP");
1230 assert_eq!(msg.m, Some(dec!(50010.50)));
1231 assert_eq!(msg.i, Some(AxInstrumentState::Open));
1232 }
1233
1234 #[rstest]
1235 fn test_load_md_ticker_captured_optional_fields_default_to_none() {
1236 let json = include_str!("../../test_data/ws_md_ticker_captured.json");
1237 let msg: AxMdTicker = serde_json::from_str(json).unwrap();
1238 assert_eq!(msg.s, "EURUSD-PERP");
1239 assert_eq!(msg.m, None);
1240 assert_eq!(msg.i, None);
1241 }
1242
1243 #[rstest]
1244 fn test_load_md_ticker_null_prices_decode() {
1245 let json = include_str!("../../test_data/ws_md_ticker_null_prices.json");
1247 let msg = parse_md_message(json).unwrap();
1248 let AxMdMessage::Ticker(ticker) = msg else {
1249 panic!("expected ticker message");
1250 };
1251 assert_eq!(ticker.s, "QQQ-PERP");
1252 assert_eq!(ticker.p, None);
1253 assert_eq!(ticker.o, None);
1254 assert_eq!(ticker.l, None);
1255 assert_eq!(ticker.h, None);
1256 assert_eq!(ticker.m, Some(dec!(716.38)));
1257 assert_eq!(ticker.i, Some(AxInstrumentState::Open));
1258 }
1259
1260 #[rstest]
1261 fn test_load_md_trade_from_file() {
1262 let json = include_str!("../../test_data/ws_md_trade.json");
1263 let msg: AxMdTrade = serde_json::from_str(json).unwrap();
1264 assert_eq!(msg.d, AxOrderSide::Buy);
1265 }
1266
1267 #[rstest]
1268 #[case(None, "missing field `d`")]
1269 #[case(Some(serde_json::Value::Null), "invalid type: null")]
1270 #[case(Some(serde_json::json!("X")), "unknown variant `X`")]
1271 #[case(Some(serde_json::json!("b")), "unknown variant `b`")]
1272 fn test_md_trade_rejects_missing_or_invalid_side(
1273 #[case] side: Option<serde_json::Value>,
1274 #[case] expected_error: &str,
1275 ) {
1276 let json = include_str!("../../test_data/ws_md_trade.json");
1277 let mut value: serde_json::Value = serde_json::from_str(json).unwrap();
1278 let fields = value.as_object_mut().unwrap();
1279 fields.remove("t");
1280 fields.remove("d");
1281 if let Some(side) = side {
1282 fields.insert("d".to_string(), side);
1283 }
1284
1285 let raw = serde_json::to_string(&value).unwrap();
1286 let fallback_raw = format!("{},\"t\":\"t\"}}", &raw[..raw.len() - 1]);
1287 let fast_raw = format!("{{\"t\":\"t\",{}", &raw[1..]);
1288
1289 serde_json::from_str::<serde_json::Value>(&fast_raw).unwrap();
1290
1291 let model_error = serde_json::from_value::<AxMdTrade>(value).unwrap_err();
1292 let message_error = parse_md_message(&fallback_raw).unwrap_err();
1293 parse_md_message(&fast_raw).unwrap_err();
1294
1295 assert!(
1296 model_error.to_string().contains(expected_error),
1297 "{model_error}"
1298 );
1299 assert!(
1300 message_error.to_string().contains(expected_error),
1301 "{message_error}"
1302 );
1303 }
1304
1305 #[rstest]
1306 fn test_load_md_candle_from_file() {
1307 let json = include_str!("../../test_data/ws_md_candle.json");
1308 let msg: AxMdCandle = serde_json::from_str(json).unwrap();
1309 assert_eq!(msg.width, AxCandleWidth::Minutes1);
1310 }
1311
1312 #[rstest]
1313 fn test_load_md_book_l1_from_file() {
1314 let json = include_str!("../../test_data/ws_md_book_l1.json");
1315 let msg: AxMdBookL1 = serde_json::from_str(json).unwrap();
1316 assert_eq!(msg.b.len(), 1);
1317 assert_eq!(msg.a.len(), 1);
1318 }
1319
1320 #[rstest]
1321 fn test_load_md_book_l2_from_file() {
1322 let json = include_str!("../../test_data/ws_md_book_l2.json");
1323 let msg: AxMdBookL2 = serde_json::from_str(json).unwrap();
1324 assert_eq!(msg.b.len(), 3);
1325 assert_eq!(msg.a.len(), 3);
1326 }
1327
1328 #[rstest]
1329 fn test_load_md_book_l3_from_file() {
1330 let json = include_str!("../../test_data/ws_md_book_l3.json");
1331 let msg: AxMdBookL3 = serde_json::from_str(json).unwrap();
1332 assert_eq!(msg.b.len(), 2);
1333 assert!(!msg.b[0].o.is_empty());
1334 }
1335
1336 #[rstest]
1337 fn test_load_order_place_response_from_file() {
1338 let json = include_str!("../../test_data/ws_order_place_response.json");
1339 let msg: AxWsPlaceOrderResponse = serde_json::from_str(json).unwrap();
1340 assert_eq!(msg.res.oid, "O-01ARZ3NDEKTSV4RRFFQ69G5FAV");
1341 }
1342
1343 #[rstest]
1344 fn test_load_order_cancel_response_from_file() {
1345 let json = include_str!("../../test_data/ws_order_cancel_response.json");
1346 let msg: AxWsCancelOrderResponse = serde_json::from_str(json).unwrap();
1347 assert!(msg.res.cxl_rx);
1348 }
1349
1350 #[rstest]
1351 fn test_load_order_open_orders_response_from_file() {
1352 let json = include_str!("../../test_data/ws_order_open_orders_response.json");
1353 let msg: AxWsOpenOrdersResponse = serde_json::from_str(json).unwrap();
1354 assert_eq!(msg.res.orders.len(), 1);
1355 }
1356
1357 #[rstest]
1358 fn test_load_order_heartbeat_from_file() {
1359 let json = include_str!("../../test_data/ws_order_heartbeat.json");
1360 let msg: AxWsHeartbeat = serde_json::from_str(json).unwrap();
1361 assert_eq!(msg.ts, 1609459200);
1362 }
1363
1364 #[rstest]
1365 fn test_load_order_acknowledged_from_file() {
1366 let json = include_str!("../../test_data/ws_order_acknowledged.json");
1367 let msg: AxWsOrderAcknowledged = serde_json::from_str(json).unwrap();
1368 assert_eq!(msg.o.oid, "O-01ARZ3NDEKTSV4RRFFQ69G5FAV");
1369 }
1370
1371 #[rstest]
1372 fn test_load_order_filled_from_file() {
1373 let json = include_str!("../../test_data/ws_order_filled.json");
1374 let msg: AxWsOrderFilled = serde_json::from_str(json).unwrap();
1375 assert_eq!(msg.o.o, AxOrderStatus::Filled);
1376 assert_eq!(msg.xs.d, AxOrderSide::Buy);
1377 }
1378
1379 #[rstest]
1380 fn test_load_order_partially_filled_from_file() {
1381 let json = include_str!("../../test_data/ws_order_partially_filled.json");
1382 let msg: AxWsOrderPartiallyFilled = serde_json::from_str(json).unwrap();
1383 assert_eq!(msg.xs.q, 50);
1384 assert_eq!(msg.xs.d, AxOrderSide::Buy);
1385 }
1386
1387 #[rstest]
1388 fn test_load_order_canceled_from_file() {
1389 let json = include_str!("../../test_data/ws_order_canceled.json");
1390 let msg: AxWsOrderCanceled = serde_json::from_str(json).unwrap();
1391 assert_eq!(msg.xr, AxCancelReason::UserRequested);
1392 }
1393
1394 #[rstest]
1395 fn test_load_order_rejected_from_file() {
1396 let json = include_str!("../../test_data/ws_order_rejected.json");
1397 let msg: AxWsOrderRejected = serde_json::from_str(json).unwrap();
1398 assert_eq!(msg.r, Some(AxOrderRejectReason::InsufficientMargin));
1399 }
1400
1401 #[rstest]
1402 fn test_load_order_expired_from_file() {
1403 let json = include_str!("../../test_data/ws_order_expired.json");
1404 let msg: AxWsOrderExpired = serde_json::from_str(json).unwrap();
1405 assert_eq!(msg.o.tif, AxTimeInForce::Ioc);
1406 }
1407
1408 #[rstest]
1409 fn test_load_order_replaced_live_shape_from_file() {
1410 let json = include_str!("../../test_data/ws_order_replaced_live.json");
1411 let msg: AxWsOrderReplaced = serde_json::from_str(json).unwrap();
1412
1413 assert_eq!(msg.noid.as_deref(), Some("O-01KWY01WX8JT4DABKC6FRS5NT4"));
1414 assert_eq!(msg.no.as_ref().unwrap().oid, "O-01KWY01WX8JT4DABKC6FRS5NT4");
1415 assert_eq!(msg.no.as_ref().unwrap().p, dec!(1.0926));
1416 assert_eq!(msg.ro.o, AxOrderStatus::Replaced);
1417 }
1418
1419 #[rstest]
1420 fn test_load_order_done_for_day_from_file() {
1421 let json = include_str!("../../test_data/ws_order_done_for_day.json");
1422 let msg: AxWsOrderDoneForDay = serde_json::from_str(json).unwrap();
1423 assert_eq!(msg.o.xq, 50);
1424 }
1425
1426 #[rstest]
1427 fn test_load_cancel_rejected_from_file() {
1428 let json = include_str!("../../test_data/ws_cancel_rejected.json");
1429 let msg: AxWsCancelRejected = serde_json::from_str(json).unwrap();
1430 assert_eq!(msg.r, AxCancelRejectionReason::OrderNotFound);
1431 }
1432
1433 #[rstest]
1434 fn test_load_order_error_response_from_file() {
1435 let json = include_str!("../../test_data/ws_order_error_response.json");
1436 let msg: AxWsOrderErrorResponse = serde_json::from_str(json).unwrap();
1437 assert_eq!(msg.rid, 1);
1438 assert_eq!(msg.err.code, Some(400));
1439 assert!(msg.err.msg.as_deref().unwrap().contains("initial margin"));
1440 }
1441
1442 #[rstest]
1443 fn test_load_order_list_response_from_file() {
1444 let json = include_str!("../../test_data/ws_order_list_response.json");
1445 let msg: AxWsListResponse = serde_json::from_str(json).unwrap();
1446 assert_eq!(msg.rid, 0);
1447 assert_eq!(msg.res.li, "01KCQM-4WP1-0000");
1448 assert!(msg.res.o.is_none());
1449 }
1450
1451 #[rstest]
1452 fn test_load_order_list_response_with_orders_from_file() {
1453 let json = include_str!("../../test_data/ws_order_list_response_with_orders.json");
1454 let msg: AxWsListResponse = serde_json::from_str(json).unwrap();
1455 assert_eq!(msg.rid, 0);
1456 assert_eq!(msg.res.li, "01KCQM-4WP1-0000");
1457 let orders = msg.res.o.unwrap();
1458 assert_eq!(orders.len(), 2);
1459 assert_eq!(orders[0].oid, "O-01KF4QM3VVJEDH98ZVNS1PCSBB");
1460 assert_eq!(orders[1].oid, "O-01KF4QM3K9FJZWYA02JF9Y1FJA");
1461 }
1462
1463 #[derive(Debug, Eq, PartialEq)]
1464 enum FrameKind {
1465 Error,
1466 ListResponse,
1467 AcknowledgedEvent,
1468 PlaceResponse,
1469 CancelResponse,
1470 OpenOrdersResponse,
1471 }
1472
1473 fn classify(frame: &AxOrdersWsFrame) -> FrameKind {
1474 match frame {
1475 AxOrdersWsFrame::Error(_) => FrameKind::Error,
1476 AxOrdersWsFrame::Response(AxWsOrderResponse::List(_)) => FrameKind::ListResponse,
1477 AxOrdersWsFrame::Response(AxWsOrderResponse::PlaceOrder(_)) => FrameKind::PlaceResponse,
1478 AxOrdersWsFrame::Response(AxWsOrderResponse::CancelOrder(_)) => {
1479 FrameKind::CancelResponse
1480 }
1481 AxOrdersWsFrame::Response(AxWsOrderResponse::OpenOrders(_)) => {
1482 FrameKind::OpenOrdersResponse
1483 }
1484 AxOrdersWsFrame::Event(e) => match **e {
1485 AxWsOrderEvent::Acknowledged(_) => FrameKind::AcknowledgedEvent,
1486 _ => panic!("unexpected event variant"),
1487 },
1488 }
1489 }
1490
1491 #[rstest]
1492 #[case::error(
1493 include_str!("../../test_data/ws_order_error_response.json"),
1494 FrameKind::Error,
1495 )]
1496 #[case::list(
1497 include_str!("../../test_data/ws_order_list_response.json"),
1498 FrameKind::ListResponse,
1499 )]
1500 #[case::acknowledged_event(
1501 include_str!("../../test_data/ws_order_acknowledged.json"),
1502 FrameKind::AcknowledgedEvent,
1503 )]
1504 #[case::place_response(
1505 include_str!("../../test_data/ws_order_place_response.json"),
1506 FrameKind::PlaceResponse,
1507 )]
1508 #[case::cancel_response(
1509 include_str!("../../test_data/ws_order_cancel_response.json"),
1510 FrameKind::CancelResponse,
1511 )]
1512 #[case::open_orders(
1513 include_str!("../../test_data/ws_order_open_orders_response.json"),
1514 FrameKind::OpenOrdersResponse,
1515 )]
1516 fn test_parse_order_message_variants(#[case] json: &str, #[case] expected: FrameKind) {
1517 let msg = parse_order_message(json).expect("should parse");
1518 assert_eq!(classify(&msg), expected);
1519 }
1520 #[rstest]
1521 fn test_ticker_missing_prices_remain_absent() {
1522 let mut value: serde_json::Value = serde_json::from_str(include_str!(
1523 "../../test_data/ws_md_ticker_null_prices.json"
1524 ))
1525 .unwrap();
1526
1527 for field in ["p", "o", "l", "h"] {
1528 value.as_object_mut().unwrap().remove(field);
1529 }
1530
1531 let ticker: AxMdTicker = serde_json::from_value(value).unwrap();
1532 assert_eq!(
1533 (ticker.p, ticker.o, ticker.l, ticker.h),
1534 (None, None, None, None)
1535 );
1536 assert_eq!(ticker.m, Some(dec!(716.38)));
1537 }
1538
1539 #[rstest]
1540 #[case(include_str!("../../test_data/captured/open-reject.json"))]
1541 #[case(include_str!("../../test_data/captured/open-backoff.json"))]
1542 #[case(include_str!("../../test_data/captured/open-top.json"))]
1543 #[case(include_str!("../../test_data/captured/open-inactive.json"))]
1544 fn test_ws_order_preserves_captured_metadata(#[case] raw: &str) {
1545 let response: serde_json::Value = serde_json::from_str(raw).unwrap();
1546 for value in response["orders"].as_array().unwrap() {
1547 let order: AxWsOrder = serde_json::from_value(value.clone()).unwrap();
1548 let actual = serde_json::to_value(order).unwrap();
1549
1550 for field in ["aid", "po", "rb", "ts", "tn", "oid", "q", "xq", "rq"] {
1551 assert_eq!(actual[field], value[field], "{field}");
1552 }
1553 }
1554 }
1555}