1use derive_builder::Builder;
19use nautilus_model::{
20 data::{Data, FundingRateUpdate, OrderBookDeltas},
21 events::{AccountState, OrderCancelRejected, OrderModifyRejected, OrderRejected},
22 instruments::InstrumentAny,
23 reports::{FillReport, OrderStatusReport},
24};
25use serde::{Deserialize, Serialize};
26use ustr::Ustr;
27
28use super::enums::{OKXWsChannel, OKXWsOperation};
29use crate::{
30 common::{
31 enums::{
32 OKXAlgoOrderType, OKXBookAction, OKXCandleConfirm, OKXExecType, OKXInstrumentType,
33 OKXOrderStatus, OKXOrderType, OKXPositionSide, OKXSide, OKXTradeMode, OKXTriggerType,
34 },
35 parse::{deserialize_empty_string_as_none, deserialize_string_to_u64},
36 },
37 websocket::enums::OKXSubscriptionEvent,
38};
39
40#[derive(Debug, Clone)]
41pub enum NautilusWsMessage {
42 Data(Vec<Data>),
43 Deltas(OrderBookDeltas),
44 FundingRates(Vec<FundingRateUpdate>),
45 Instrument(Box<InstrumentAny>),
46 AccountUpdate(AccountState),
47 OrderRejected(OrderRejected),
48 OrderCancelRejected(OrderCancelRejected),
49 OrderModifyRejected(OrderModifyRejected),
50 ExecutionReports(Vec<ExecutionReport>),
51 Error(OKXWebSocketError),
52 Raw(serde_json::Value), Reconnected,
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
58#[cfg_attr(feature = "python", pyo3::pyclass)]
59pub struct OKXWebSocketError {
60 pub code: String,
62 pub message: String,
64 pub conn_id: Option<String>,
66 pub timestamp: u64,
68}
69
70#[derive(Debug, Clone)]
71#[allow(clippy::large_enum_variant)]
72pub enum ExecutionReport {
73 Order(OrderStatusReport),
74 Fill(FillReport),
75}
76
77#[derive(Debug, Serialize)]
79#[serde(rename_all = "camelCase")]
80pub struct OKXWsRequest<T> {
81 #[serde(skip_serializing_if = "Option::is_none")]
83 pub id: Option<String>,
84 pub op: OKXWsOperation,
86 #[serde(skip_serializing_if = "Option::is_none")]
89 pub exp_time: Option<String>,
90 pub args: Vec<T>,
92}
93
94#[derive(Debug, Serialize)]
96pub struct OKXAuthentication {
97 pub op: &'static str,
98 pub args: Vec<OKXAuthenticationArg>,
99}
100
101#[derive(Debug, Serialize)]
103#[serde(rename_all = "camelCase")]
104pub struct OKXAuthenticationArg {
105 pub api_key: String,
106 pub passphrase: String,
107 pub timestamp: String,
108 pub sign: String,
109}
110
111#[derive(Debug, Serialize)]
112pub struct OKXSubscription {
113 pub op: OKXWsOperation,
114 pub args: Vec<OKXSubscriptionArg>,
115}
116
117#[derive(Debug, Serialize)]
118#[serde(rename_all = "camelCase")]
119pub struct OKXSubscriptionArg {
120 pub channel: OKXWsChannel,
121 pub inst_type: Option<OKXInstrumentType>,
122 pub inst_family: Option<Ustr>,
123 pub inst_id: Option<Ustr>,
124}
125
126#[derive(Debug, Deserialize)]
127#[serde(untagged)]
128pub enum OKXWebSocketEvent {
129 Login {
130 event: String,
131 code: String,
132 msg: String,
133 #[serde(rename = "connId")]
134 conn_id: String,
135 },
136 Subscription {
137 event: OKXSubscriptionEvent,
138 arg: OKXWebSocketArg,
139 #[serde(rename = "connId")]
140 conn_id: String,
141 #[serde(default)]
142 code: Option<String>,
143 #[serde(default)]
144 msg: Option<String>,
145 },
146 ChannelConnCount {
147 event: String,
148 channel: OKXWsChannel,
149 #[serde(rename = "connCount")]
150 conn_count: String,
151 #[serde(rename = "connId")]
152 conn_id: String,
153 },
154 OrderResponse {
155 id: Option<String>,
156 op: OKXWsOperation,
157 code: String,
158 msg: String,
159 data: Vec<serde_json::Value>,
160 },
161 BookData {
162 arg: OKXWebSocketArg,
163 action: OKXBookAction,
164 data: Vec<OKXBookMsg>,
165 },
166 Data {
167 arg: OKXWebSocketArg,
168 data: serde_json::Value,
169 },
170 Error {
171 code: String,
172 msg: String,
173 },
174 #[serde(skip)]
175 Ping,
176 #[serde(skip)]
177 Reconnected,
178}
179
180#[derive(Debug, Serialize, Deserialize)]
181#[serde(rename_all = "camelCase")]
182pub struct OKXWebSocketArg {
183 pub channel: OKXWsChannel,
185 #[serde(default)]
186 pub inst_id: Option<Ustr>,
187 #[serde(default)]
188 pub inst_type: Option<OKXInstrumentType>,
189 #[serde(default)]
190 pub inst_family: Option<Ustr>,
191 #[serde(default)]
192 pub bar: Option<Ustr>,
193}
194
195#[derive(Debug, Serialize, Deserialize)]
197#[serde(rename_all = "camelCase")]
198pub struct OKXTickerMsg {
199 pub inst_type: OKXInstrumentType,
201 pub inst_id: Ustr,
203 #[serde(rename = "last")]
205 pub last_px: String,
206 pub last_sz: String,
208 pub ask_px: String,
210 pub ask_sz: String,
212 pub bid_px: String,
214 pub bid_sz: String,
216 pub open24h: String,
218 pub high24h: String,
220 pub low24h: String,
222 pub vol_ccy_24h: String,
224 pub vol24h: String,
226 pub sod_utc0: String,
228 pub sod_utc8: String,
230 #[serde(deserialize_with = "deserialize_string_to_u64")]
232 pub ts: u64,
233}
234
235#[derive(Debug, Serialize, Deserialize)]
237pub struct OrderBookEntry {
238 pub price: String,
240 pub size: String,
242 pub liquidated_orders_count: String,
244 pub orders_count: String,
246}
247
248#[derive(Debug, Serialize, Deserialize)]
250#[serde(rename_all = "camelCase")]
251pub struct OKXBookMsg {
252 pub asks: Vec<OrderBookEntry>,
254 pub bids: Vec<OrderBookEntry>,
256 pub checksum: Option<i64>,
258 pub prev_seq_id: Option<i64>,
260 pub seq_id: u64,
262 #[serde(deserialize_with = "deserialize_string_to_u64")]
264 pub ts: u64,
265}
266
267#[derive(Debug, Serialize, Deserialize)]
269#[serde(rename_all = "camelCase")]
270pub struct OKXTradeMsg {
271 pub inst_id: Ustr,
273 pub trade_id: String,
275 pub px: String,
277 pub sz: String,
279 pub side: OKXSide,
281 pub count: String,
283 #[serde(deserialize_with = "deserialize_string_to_u64")]
285 pub ts: u64,
286}
287
288#[derive(Debug, Serialize, Deserialize)]
290#[serde(rename_all = "camelCase")]
291pub struct OKXFundingRateMsg {
292 pub inst_id: Ustr,
294 pub funding_rate: Ustr,
296 pub next_funding_rate: Ustr,
298 #[serde(deserialize_with = "deserialize_string_to_u64")]
300 pub funding_time: u64,
301 #[serde(deserialize_with = "deserialize_string_to_u64")]
303 pub ts: u64,
304}
305
306#[derive(Debug, Serialize, Deserialize)]
308#[serde(rename_all = "camelCase")]
309pub struct OKXMarkPriceMsg {
310 pub inst_id: Ustr,
312 pub mark_px: String,
314 #[serde(deserialize_with = "deserialize_string_to_u64")]
316 pub ts: u64,
317}
318
319#[derive(Debug, Serialize, Deserialize)]
321#[serde(rename_all = "camelCase")]
322pub struct OKXIndexPriceMsg {
323 pub inst_id: Ustr,
325 pub idx_px: String,
327 pub high24h: String,
329 pub low24h: String,
331 pub open24h: String,
333 pub sod_utc0: String,
335 pub sod_utc8: String,
337 #[serde(deserialize_with = "deserialize_string_to_u64")]
339 pub ts: u64,
340}
341
342#[derive(Debug, Serialize, Deserialize)]
344#[serde(rename_all = "camelCase")]
345pub struct OKXPriceLimitMsg {
346 pub inst_id: Ustr,
348 pub buy_lmt: String,
350 pub sell_lmt: String,
352 #[serde(deserialize_with = "deserialize_string_to_u64")]
354 pub ts: u64,
355}
356
357#[derive(Debug, Serialize, Deserialize)]
359#[serde(rename_all = "camelCase")]
360pub struct OKXCandleMsg {
361 #[serde(deserialize_with = "deserialize_string_to_u64")]
363 pub ts: u64,
364 pub o: String,
366 pub h: String,
368 pub l: String,
370 pub c: String,
372 pub vol: String,
374 pub vol_ccy: String,
376 pub vol_ccy_quote: String,
377 pub confirm: OKXCandleConfirm,
379}
380
381#[derive(Debug, Serialize, Deserialize)]
383#[serde(rename_all = "camelCase")]
384pub struct OKXOpenInterestMsg {
385 pub inst_id: Ustr,
387 pub oi: String,
389 pub oi_ccy: String,
391 #[serde(deserialize_with = "deserialize_string_to_u64")]
393 pub ts: u64,
394}
395
396#[derive(Debug, Serialize, Deserialize)]
398#[serde(rename_all = "camelCase")]
399pub struct OKXOptionSummaryMsg {
400 pub inst_id: Ustr,
402 pub uly: String,
404 pub delta: String,
406 pub gamma: String,
408 pub theta: String,
410 pub vega: String,
412 pub delta_bs: String,
414 pub gamma_bs: String,
416 pub theta_bs: String,
418 pub vega_bs: String,
420 pub real_vol: String,
422 pub bid_vol: String,
424 pub ask_vol: String,
426 pub mark_vol: String,
428 pub lever: String,
430 #[serde(deserialize_with = "deserialize_string_to_u64")]
432 pub ts: u64,
433}
434
435#[derive(Debug, Serialize, Deserialize)]
437#[serde(rename_all = "camelCase")]
438pub struct OKXEstimatedPriceMsg {
439 pub inst_id: Ustr,
441 pub settle_px: String,
443 #[serde(deserialize_with = "deserialize_string_to_u64")]
445 pub ts: u64,
446}
447
448#[derive(Debug, Serialize, Deserialize)]
450#[serde(rename_all = "camelCase")]
451pub struct OKXStatusMsg {
452 pub title: Ustr,
454 #[serde(rename = "type")]
456 pub status_type: Ustr,
457 pub state: Ustr,
459 pub end_time: Option<String>,
461 pub begin_time: Option<String>,
463 pub service_type: Option<Ustr>,
465 pub reason: Option<String>,
467 #[serde(deserialize_with = "deserialize_string_to_u64")]
469 pub ts: u64,
470}
471
472#[derive(Clone, Debug, Serialize, Deserialize)]
474#[serde(rename_all = "camelCase")]
475pub struct OKXOrderMsg {
476 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
478 pub acc_fill_sz: Option<String>,
479 pub avg_px: String,
481 #[serde(deserialize_with = "deserialize_string_to_u64")]
483 pub c_time: u64,
484 #[serde(default)]
486 pub cancel_source: Option<String>,
487 #[serde(default)]
489 pub cancel_source_reason: Option<String>,
490 pub category: Ustr,
492 pub ccy: Ustr,
494 pub cl_ord_id: String,
496 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
498 pub algo_cl_ord_id: Option<String>,
499 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
501 pub fee: Option<String>,
502 pub fee_ccy: Ustr,
504 pub fill_px: String,
506 pub fill_sz: String,
508 #[serde(deserialize_with = "deserialize_string_to_u64")]
510 pub fill_time: u64,
511 pub inst_id: Ustr,
513 pub inst_type: OKXInstrumentType,
515 pub lever: String,
517 pub ord_id: Ustr,
519 pub ord_type: OKXOrderType,
521 pub pnl: String,
523 pub pos_side: OKXPositionSide,
525 #[serde(default)]
527 pub px: String,
528 pub reduce_only: String,
530 pub side: OKXSide,
532 pub state: OKXOrderStatus,
534 pub exec_type: OKXExecType,
536 pub sz: String,
538 pub td_mode: OKXTradeMode,
540 pub trade_id: String,
542 #[serde(deserialize_with = "deserialize_string_to_u64")]
544 pub u_time: u64,
545}
546
547#[derive(Clone, Debug, Deserialize, Serialize)]
549#[serde(rename_all = "camelCase")]
550pub struct OKXAlgoOrderMsg {
551 pub algo_id: String,
553 #[serde(default)]
555 pub algo_cl_ord_id: String,
556 pub cl_ord_id: String,
558 pub ord_id: String,
560 pub inst_id: Ustr,
562 pub inst_type: OKXInstrumentType,
564 pub ord_type: OKXOrderType,
566 pub state: OKXOrderStatus,
568 pub side: OKXSide,
570 pub pos_side: OKXPositionSide,
572 pub sz: String,
574 pub trigger_px: String,
576 pub trigger_px_type: OKXTriggerType,
578 pub ord_px: String,
580 pub td_mode: OKXTradeMode,
582 pub lever: String,
584 pub reduce_only: String,
586 pub actual_px: String,
588 pub actual_sz: String,
590 pub notional_usd: String,
592 #[serde(deserialize_with = "deserialize_string_to_u64")]
594 pub c_time: u64,
595 #[serde(deserialize_with = "deserialize_string_to_u64")]
597 pub u_time: u64,
598 pub trigger_time: String,
600 #[serde(default)]
602 pub tag: String,
603}
604
605#[derive(Clone, Debug, Deserialize, Serialize, Builder)]
607#[builder(setter(into, strip_option))]
608#[serde(rename_all = "camelCase")]
609pub struct WsPostOrderParams {
610 #[builder(default)]
612 #[serde(skip_serializing_if = "Option::is_none")]
613 pub inst_type: Option<OKXInstrumentType>,
614 pub inst_id: Ustr,
616 pub td_mode: OKXTradeMode,
618 #[builder(default)]
620 #[serde(skip_serializing_if = "Option::is_none")]
621 pub ccy: Option<Ustr>,
622 #[serde(skip_serializing_if = "Option::is_none")]
624 pub cl_ord_id: Option<String>,
625 pub side: OKXSide,
627 #[builder(default)]
629 #[serde(skip_serializing_if = "Option::is_none")]
630 pub pos_side: Option<OKXPositionSide>,
631 pub ord_type: OKXOrderType,
633 pub sz: String,
635 #[builder(default)]
637 #[serde(skip_serializing_if = "Option::is_none")]
638 pub px: Option<String>,
639 #[builder(default)]
641 #[serde(skip_serializing_if = "Option::is_none")]
642 pub reduce_only: Option<bool>,
643 #[builder(default)]
645 #[serde(skip_serializing_if = "Option::is_none")]
646 pub tgt_ccy: Option<String>,
647 #[builder(default)]
649 #[serde(skip_serializing_if = "Option::is_none")]
650 pub tag: Option<String>,
651}
652
653#[derive(Clone, Debug, Default, Deserialize, Serialize, Builder)]
655#[builder(default)]
656#[builder(setter(into, strip_option))]
657#[serde(rename_all = "camelCase")]
658pub struct WsCancelOrderParams {
659 pub inst_id: Ustr,
661 #[serde(skip_serializing_if = "Option::is_none")]
663 pub ord_id: Option<String>,
664 #[serde(skip_serializing_if = "Option::is_none")]
666 pub cl_ord_id: Option<String>,
667}
668
669#[derive(Clone, Debug, Default, Deserialize, Serialize, Builder)]
671#[builder(default)]
672#[builder(setter(into, strip_option))]
673#[serde(rename_all = "camelCase")]
674pub struct WsMassCancelParams {
675 pub inst_type: OKXInstrumentType,
677 pub inst_family: Ustr,
679}
680
681#[derive(Clone, Debug, Default, Deserialize, Serialize, Builder)]
683#[builder(default)]
684#[builder(setter(into, strip_option))]
685#[serde(rename_all = "camelCase")]
686pub struct WsAmendOrderParams {
687 pub inst_id: Ustr,
689 #[serde(skip_serializing_if = "Option::is_none")]
691 pub ord_id: Option<String>,
692 #[serde(skip_serializing_if = "Option::is_none")]
694 pub cl_ord_id: Option<String>,
695 #[serde(skip_serializing_if = "Option::is_none")]
697 pub new_cl_ord_id: Option<String>,
698 #[serde(skip_serializing_if = "Option::is_none")]
700 pub new_px: Option<String>,
701 #[serde(skip_serializing_if = "Option::is_none")]
703 pub new_sz: Option<String>,
704}
705
706#[derive(Clone, Debug, Deserialize, Serialize, Builder)]
708#[builder(setter(into, strip_option))]
709#[serde(rename_all = "camelCase")]
710pub struct WsPostAlgoOrderParams {
711 pub inst_id: Ustr,
713 pub td_mode: OKXTradeMode,
715 pub side: OKXSide,
717 pub ord_type: OKXAlgoOrderType,
719 pub sz: String,
721 #[builder(default)]
723 #[serde(skip_serializing_if = "Option::is_none")]
724 pub cl_ord_id: Option<String>,
725 #[builder(default)]
727 #[serde(skip_serializing_if = "Option::is_none")]
728 pub pos_side: Option<OKXPositionSide>,
729 #[serde(skip_serializing_if = "Option::is_none")]
731 pub trigger_px: Option<String>,
732 #[builder(default)]
734 #[serde(skip_serializing_if = "Option::is_none")]
735 pub trigger_px_type: Option<OKXTriggerType>,
736 #[builder(default)]
738 #[serde(skip_serializing_if = "Option::is_none")]
739 pub order_px: Option<String>,
740 #[builder(default)]
742 #[serde(skip_serializing_if = "Option::is_none")]
743 pub reduce_only: Option<bool>,
744 #[builder(default)]
746 #[serde(skip_serializing_if = "Option::is_none")]
747 pub tag: Option<String>,
748}
749
750#[derive(Clone, Debug, Deserialize, Serialize, Builder)]
752#[builder(setter(into, strip_option))]
753#[serde(rename_all = "camelCase")]
754pub struct WsCancelAlgoOrderParams {
755 pub inst_id: Ustr,
757 #[serde(skip_serializing_if = "Option::is_none")]
759 pub algo_id: Option<String>,
760 #[serde(skip_serializing_if = "Option::is_none")]
762 pub algo_cl_ord_id: Option<String>,
763}
764
765#[cfg(test)]
770mod tests {
771 use nautilus_core::time::get_atomic_clock_realtime;
772 use rstest::rstest;
773
774 use super::*;
775
776 #[rstest]
777 fn test_deserialize_websocket_arg() {
778 let json_str = r#"{"channel":"instruments","instType":"SPOT"}"#;
779
780 let result: Result<OKXWebSocketArg, _> = serde_json::from_str(json_str);
781 match result {
782 Ok(arg) => {
783 assert_eq!(arg.channel, OKXWsChannel::Instruments);
784 assert_eq!(arg.inst_type, Some(OKXInstrumentType::Spot));
785 assert_eq!(arg.inst_id, None);
786 }
787 Err(e) => {
788 panic!("Failed to deserialize WebSocket arg: {e}");
789 }
790 }
791 }
792
793 #[rstest]
794 fn test_deserialize_subscribe_variant_direct() {
795 #[derive(Debug, Deserialize)]
796 #[serde(rename_all = "camelCase")]
797 struct SubscribeMsg {
798 event: String,
799 arg: OKXWebSocketArg,
800 conn_id: String,
801 }
802
803 let json_str = r#"{"event":"subscribe","arg":{"channel":"instruments","instType":"SPOT"},"connId":"380cfa6a"}"#;
804
805 let result: Result<SubscribeMsg, _> = serde_json::from_str(json_str);
806 match result {
807 Ok(msg) => {
808 assert_eq!(msg.event, "subscribe");
809 assert_eq!(msg.arg.channel, OKXWsChannel::Instruments);
810 assert_eq!(msg.conn_id, "380cfa6a");
811 }
812 Err(e) => {
813 panic!("Failed to deserialize subscribe message directly: {e}");
814 }
815 }
816 }
817
818 #[rstest]
819 fn test_deserialize_subscribe_confirmation() {
820 let json_str = r#"{"event":"subscribe","arg":{"channel":"instruments","instType":"SPOT"},"connId":"380cfa6a"}"#;
821
822 let result: Result<OKXWebSocketEvent, _> = serde_json::from_str(json_str);
823 match result {
824 Ok(msg) => {
825 if let OKXWebSocketEvent::Subscription {
826 event,
827 arg,
828 conn_id,
829 ..
830 } = msg
831 {
832 assert_eq!(event, OKXSubscriptionEvent::Subscribe);
833 assert_eq!(arg.channel, OKXWsChannel::Instruments);
834 assert_eq!(conn_id, "380cfa6a");
835 } else {
836 panic!("Expected Subscribe variant, was: {msg:?}");
837 }
838 }
839 Err(e) => {
840 panic!("Failed to deserialize subscription confirmation: {e}");
841 }
842 }
843 }
844
845 #[rstest]
846 fn test_deserialize_subscribe_with_inst_id() {
847 let json_str = r#"{"event":"subscribe","arg":{"channel":"candle1m","instId":"ETH-USDT"},"connId":"358602f5"}"#;
848
849 let result: Result<OKXWebSocketEvent, _> = serde_json::from_str(json_str);
850 match result {
851 Ok(msg) => {
852 if let OKXWebSocketEvent::Subscription {
853 event,
854 arg,
855 conn_id,
856 ..
857 } = msg
858 {
859 assert_eq!(event, OKXSubscriptionEvent::Subscribe);
860 assert_eq!(arg.channel, OKXWsChannel::Candle1Minute);
861 assert_eq!(conn_id, "358602f5");
862 } else {
863 panic!("Expected Subscribe variant, was: {msg:?}");
864 }
865 }
866 Err(e) => {
867 panic!("Failed to deserialize subscription confirmation: {e}");
868 }
869 }
870 }
871
872 #[rstest]
873 fn test_channel_serialization_for_logging() {
874 let channel = OKXWsChannel::Candle1Minute;
875 let serialized = serde_json::to_string(&channel).unwrap();
876 let cleaned = serialized.trim_matches('"').to_string();
877 assert_eq!(cleaned, "candle1m");
878
879 let channel = OKXWsChannel::BboTbt;
880 let serialized = serde_json::to_string(&channel).unwrap();
881 let cleaned = serialized.trim_matches('"').to_string();
882 assert_eq!(cleaned, "bbo-tbt");
883
884 let channel = OKXWsChannel::Trades;
885 let serialized = serde_json::to_string(&channel).unwrap();
886 let cleaned = serialized.trim_matches('"').to_string();
887 assert_eq!(cleaned, "trades");
888 }
889
890 #[rstest]
891 fn test_order_response_with_enum_operation() {
892 let json_str = r#"{"id":"req-123","op":"order","code":"0","msg":"","data":[]}"#;
893 let result: Result<OKXWebSocketEvent, _> = serde_json::from_str(json_str);
894 match result {
895 Ok(OKXWebSocketEvent::OrderResponse {
896 id,
897 op,
898 code,
899 msg,
900 data,
901 }) => {
902 assert_eq!(id, Some("req-123".to_string()));
903 assert_eq!(op, OKXWsOperation::Order);
904 assert_eq!(code, "0");
905 assert_eq!(msg, "");
906 assert!(data.is_empty());
907 }
908 Ok(other) => panic!("Expected OrderResponse, was: {other:?}"),
909 Err(e) => panic!("Failed to deserialize: {e}"),
910 }
911
912 let json_str = r#"{"id":"cancel-456","op":"cancel-order","code":"50001","msg":"Order not found","data":[]}"#;
913 let result: Result<OKXWebSocketEvent, _> = serde_json::from_str(json_str);
914 match result {
915 Ok(OKXWebSocketEvent::OrderResponse {
916 id,
917 op,
918 code,
919 msg,
920 data,
921 }) => {
922 assert_eq!(id, Some("cancel-456".to_string()));
923 assert_eq!(op, OKXWsOperation::CancelOrder);
924 assert_eq!(code, "50001");
925 assert_eq!(msg, "Order not found");
926 assert!(data.is_empty());
927 }
928 Ok(other) => panic!("Expected OrderResponse, was: {other:?}"),
929 Err(e) => panic!("Failed to deserialize: {e}"),
930 }
931
932 let json_str = r#"{"id":"amend-789","op":"amend-order","code":"50002","msg":"Invalid price","data":[]}"#;
933 let result: Result<OKXWebSocketEvent, _> = serde_json::from_str(json_str);
934 match result {
935 Ok(OKXWebSocketEvent::OrderResponse {
936 id,
937 op,
938 code,
939 msg,
940 data,
941 }) => {
942 assert_eq!(id, Some("amend-789".to_string()));
943 assert_eq!(op, OKXWsOperation::AmendOrder);
944 assert_eq!(code, "50002");
945 assert_eq!(msg, "Invalid price");
946 assert!(data.is_empty());
947 }
948 Ok(other) => panic!("Expected OrderResponse, was: {other:?}"),
949 Err(e) => panic!("Failed to deserialize: {e}"),
950 }
951 }
952
953 #[rstest]
954 fn test_operation_enum_serialization() {
955 let op = OKXWsOperation::Order;
956 let serialized = serde_json::to_string(&op).unwrap();
957 assert_eq!(serialized, "\"order\"");
958
959 let op = OKXWsOperation::CancelOrder;
960 let serialized = serde_json::to_string(&op).unwrap();
961 assert_eq!(serialized, "\"cancel-order\"");
962
963 let op = OKXWsOperation::AmendOrder;
964 let serialized = serde_json::to_string(&op).unwrap();
965 assert_eq!(serialized, "\"amend-order\"");
966
967 let op = OKXWsOperation::Subscribe;
968 let serialized = serde_json::to_string(&op).unwrap();
969 assert_eq!(serialized, "\"subscribe\"");
970 }
971
972 #[rstest]
973 fn test_order_response_parsing() {
974 let success_response = r#"{
975 "id": "req-123",
976 "op": "order",
977 "code": "0",
978 "msg": "",
979 "data": [{"sMsg": "Order placed successfully"}]
980 }"#;
981
982 let parsed: OKXWebSocketEvent = serde_json::from_str(success_response).unwrap();
983
984 match parsed {
985 OKXWebSocketEvent::OrderResponse {
986 id,
987 op,
988 code,
989 msg,
990 data,
991 } => {
992 assert_eq!(id, Some("req-123".to_string()));
993 assert_eq!(op, OKXWsOperation::Order);
994 assert_eq!(code, "0");
995 assert_eq!(msg, "");
996 assert_eq!(data.len(), 1);
997 }
998 _ => panic!("Expected OrderResponse variant"),
999 }
1000
1001 let failure_response = r#"{
1002 "id": "req-456",
1003 "op": "cancel-order",
1004 "code": "50001",
1005 "msg": "Order not found",
1006 "data": [{"sMsg": "Order with client order ID not found"}]
1007 }"#;
1008
1009 let parsed: OKXWebSocketEvent = serde_json::from_str(failure_response).unwrap();
1010
1011 match parsed {
1012 OKXWebSocketEvent::OrderResponse {
1013 id,
1014 op,
1015 code,
1016 msg,
1017 data,
1018 } => {
1019 assert_eq!(id, Some("req-456".to_string()));
1020 assert_eq!(op, OKXWsOperation::CancelOrder);
1021 assert_eq!(code, "50001");
1022 assert_eq!(msg, "Order not found");
1023 assert_eq!(data.len(), 1);
1024 }
1025 _ => panic!("Expected OrderResponse variant"),
1026 }
1027 }
1028
1029 #[rstest]
1030 fn test_subscription_event_parsing() {
1031 let subscription_json = r#"{
1032 "event": "subscribe",
1033 "arg": {
1034 "channel": "tickers",
1035 "instId": "BTC-USDT"
1036 },
1037 "connId": "a4d3ae55"
1038 }"#;
1039
1040 let parsed: OKXWebSocketEvent = serde_json::from_str(subscription_json).unwrap();
1041
1042 match parsed {
1043 OKXWebSocketEvent::Subscription {
1044 event,
1045 arg,
1046 conn_id,
1047 ..
1048 } => {
1049 assert_eq!(
1050 event,
1051 crate::websocket::enums::OKXSubscriptionEvent::Subscribe
1052 );
1053 assert_eq!(arg.channel, OKXWsChannel::Tickers);
1054 assert_eq!(arg.inst_id, Some(Ustr::from("BTC-USDT")));
1055 assert_eq!(conn_id, "a4d3ae55");
1056 }
1057 _ => panic!("Expected Subscription variant"),
1058 }
1059 }
1060
1061 #[rstest]
1062 fn test_login_event_parsing() {
1063 let login_success = r#"{
1064 "event": "login",
1065 "code": "0",
1066 "msg": "Login successful",
1067 "connId": "a4d3ae55"
1068 }"#;
1069
1070 let parsed: OKXWebSocketEvent = serde_json::from_str(login_success).unwrap();
1071
1072 match parsed {
1073 OKXWebSocketEvent::Login {
1074 event,
1075 code,
1076 msg,
1077 conn_id,
1078 } => {
1079 assert_eq!(event, "login");
1080 assert_eq!(code, "0");
1081 assert_eq!(msg, "Login successful");
1082 assert_eq!(conn_id, "a4d3ae55");
1083 }
1084 _ => panic!("Expected Login variant, was: {:?}", parsed),
1085 }
1086 }
1087
1088 #[rstest]
1089 fn test_error_event_parsing() {
1090 let error_json = r#"{
1091 "code": "60012",
1092 "msg": "Invalid request"
1093 }"#;
1094
1095 let parsed: OKXWebSocketEvent = serde_json::from_str(error_json).unwrap();
1096
1097 match parsed {
1098 OKXWebSocketEvent::Error { code, msg } => {
1099 assert_eq!(code, "60012");
1100 assert_eq!(msg, "Invalid request");
1101 }
1102 _ => panic!("Expected Error variant"),
1103 }
1104 }
1105
1106 #[rstest]
1107 fn test_websocket_request_serialization() {
1108 let request = OKXWsRequest {
1109 id: Some("req-123".to_string()),
1110 op: OKXWsOperation::Order,
1111 args: vec![serde_json::json!({
1112 "instId": "BTC-USDT",
1113 "tdMode": "cash",
1114 "side": "buy",
1115 "ordType": "market",
1116 "sz": "0.1"
1117 })],
1118 exp_time: None,
1119 };
1120
1121 let serialized = serde_json::to_string(&request).unwrap();
1122 let parsed: serde_json::Value = serde_json::from_str(&serialized).unwrap();
1123
1124 assert_eq!(parsed["id"], "req-123");
1125 assert_eq!(parsed["op"], "order");
1126 assert!(parsed["args"].is_array());
1127 assert_eq!(parsed["args"].as_array().unwrap().len(), 1);
1128 }
1129
1130 #[rstest]
1131 fn test_subscription_request_serialization() {
1132 let subscription = OKXSubscription {
1133 op: OKXWsOperation::Subscribe,
1134 args: vec![OKXSubscriptionArg {
1135 channel: OKXWsChannel::Tickers,
1136 inst_type: Some(crate::common::enums::OKXInstrumentType::Spot),
1137 inst_family: None,
1138 inst_id: Some(Ustr::from("BTC-USDT")),
1139 }],
1140 };
1141
1142 let serialized = serde_json::to_string(&subscription).unwrap();
1143 let parsed: serde_json::Value = serde_json::from_str(&serialized).unwrap();
1144
1145 assert_eq!(parsed["op"], "subscribe");
1146 assert!(parsed["args"].is_array());
1147 assert_eq!(parsed["args"][0]["channel"], "tickers");
1148 assert_eq!(parsed["args"][0]["instType"], "SPOT");
1149 assert_eq!(parsed["args"][0]["instId"], "BTC-USDT");
1150 }
1151
1152 #[rstest]
1153 fn test_error_message_extraction() {
1154 let responses = vec![
1155 (
1156 r#"{
1157 "id": "req-123",
1158 "op": "order",
1159 "code": "50001",
1160 "msg": "Order failed",
1161 "data": [{"sMsg": "Insufficient balance"}]
1162 }"#,
1163 "Insufficient balance",
1164 ),
1165 (
1166 r#"{
1167 "id": "req-456",
1168 "op": "cancel-order",
1169 "code": "50002",
1170 "msg": "Cancel failed",
1171 "data": [{}]
1172 }"#,
1173 "Cancel failed",
1174 ),
1175 ];
1176
1177 for (response_json, expected_msg) in responses {
1178 let parsed: OKXWebSocketEvent = serde_json::from_str(response_json).unwrap();
1179
1180 match parsed {
1181 OKXWebSocketEvent::OrderResponse {
1182 id: _,
1183 op: _,
1184 code,
1185 msg,
1186 data,
1187 } => {
1188 assert_ne!(code, "0"); let error_msg = data
1192 .first()
1193 .and_then(|d| d.get("sMsg"))
1194 .and_then(|s| s.as_str())
1195 .filter(|s| !s.is_empty())
1196 .unwrap_or(&msg);
1197
1198 assert_eq!(error_msg, expected_msg);
1199 }
1200 _ => panic!("Expected OrderResponse variant"),
1201 }
1202 }
1203 }
1204
1205 #[rstest]
1206 fn test_book_data_parsing() {
1207 let book_data_json = r#"{
1208 "arg": {
1209 "channel": "books",
1210 "instId": "BTC-USDT"
1211 },
1212 "action": "snapshot",
1213 "data": [{
1214 "asks": [["50000.0", "0.1", "0", "1"]],
1215 "bids": [["49999.0", "0.2", "0", "1"]],
1216 "ts": "1640995200000",
1217 "checksum": 123456789,
1218 "seqId": 1000
1219 }]
1220 }"#;
1221
1222 let parsed: OKXWebSocketEvent = serde_json::from_str(book_data_json).unwrap();
1223
1224 match parsed {
1225 OKXWebSocketEvent::BookData { arg, action, data } => {
1226 assert_eq!(arg.channel, OKXWsChannel::Books);
1227 assert_eq!(arg.inst_id, Some(Ustr::from("BTC-USDT")));
1228 assert_eq!(
1229 action,
1230 super::super::super::common::enums::OKXBookAction::Snapshot
1231 );
1232 assert_eq!(data.len(), 1);
1233 }
1234 _ => panic!("Expected BookData variant"),
1235 }
1236 }
1237
1238 #[rstest]
1239 fn test_data_event_parsing() {
1240 let data_json = r#"{
1241 "arg": {
1242 "channel": "trades",
1243 "instId": "BTC-USDT"
1244 },
1245 "data": [{
1246 "instId": "BTC-USDT",
1247 "tradeId": "12345",
1248 "px": "50000.0",
1249 "sz": "0.1",
1250 "side": "buy",
1251 "ts": "1640995200000"
1252 }]
1253 }"#;
1254
1255 let parsed: OKXWebSocketEvent = serde_json::from_str(data_json).unwrap();
1256
1257 match parsed {
1258 OKXWebSocketEvent::Data { arg, data } => {
1259 assert_eq!(arg.channel, OKXWsChannel::Trades);
1260 assert_eq!(arg.inst_id, Some(Ustr::from("BTC-USDT")));
1261 assert!(data.is_array());
1262 }
1263 _ => panic!("Expected Data variant"),
1264 }
1265 }
1266
1267 #[rstest]
1268 fn test_nautilus_message_variants() {
1269 let clock = get_atomic_clock_realtime();
1270 let ts_init = clock.get_time_ns();
1271
1272 let error = OKXWebSocketError {
1273 code: "60012".to_string(),
1274 message: "Invalid request".to_string(),
1275 conn_id: None,
1276 timestamp: ts_init.as_u64(),
1277 };
1278 let error_msg = NautilusWsMessage::Error(error);
1279
1280 match error_msg {
1281 NautilusWsMessage::Error(err) => {
1282 assert_eq!(err.code, "60012");
1283 assert_eq!(err.message, "Invalid request");
1284 }
1285 _ => panic!("Expected Error variant"),
1286 }
1287
1288 let raw_scenarios = vec![
1289 ::serde_json::json!({"unknown": "data"}),
1290 ::serde_json::json!({"channel": "unsupported", "data": [1, 2, 3]}),
1291 ::serde_json::json!({"complex": {"nested": {"structure": true}}}),
1292 ];
1293
1294 for raw_data in raw_scenarios {
1295 let raw_msg = NautilusWsMessage::Raw(raw_data.clone());
1296
1297 match raw_msg {
1298 NautilusWsMessage::Raw(data) => {
1299 assert_eq!(data, raw_data);
1300 }
1301 _ => panic!("Expected Raw variant"),
1302 }
1303 }
1304 }
1305
1306 #[rstest]
1307 fn test_order_response_parsing_success() {
1308 let order_response_json = r#"{
1309 "id": "req-123",
1310 "op": "order",
1311 "code": "0",
1312 "msg": "",
1313 "data": [{"sMsg": "Order placed successfully"}]
1314 }"#;
1315
1316 let parsed: OKXWebSocketEvent = serde_json::from_str(order_response_json).unwrap();
1317
1318 match parsed {
1319 OKXWebSocketEvent::OrderResponse {
1320 id,
1321 op,
1322 code,
1323 msg,
1324 data,
1325 } => {
1326 assert_eq!(id, Some("req-123".to_string()));
1327 assert_eq!(op, OKXWsOperation::Order);
1328 assert_eq!(code, "0");
1329 assert_eq!(msg, "");
1330 assert_eq!(data.len(), 1);
1331 }
1332 _ => panic!("Expected OrderResponse variant"),
1333 }
1334 }
1335
1336 #[rstest]
1337 fn test_order_response_parsing_failure() {
1338 let order_response_json = r#"{
1339 "id": "req-456",
1340 "op": "cancel-order",
1341 "code": "50001",
1342 "msg": "Order not found",
1343 "data": [{"sMsg": "Order with client order ID not found"}]
1344 }"#;
1345
1346 let parsed: OKXWebSocketEvent = serde_json::from_str(order_response_json).unwrap();
1347
1348 match parsed {
1349 OKXWebSocketEvent::OrderResponse {
1350 id,
1351 op,
1352 code,
1353 msg,
1354 data,
1355 } => {
1356 assert_eq!(id, Some("req-456".to_string()));
1357 assert_eq!(op, OKXWsOperation::CancelOrder);
1358 assert_eq!(code, "50001");
1359 assert_eq!(msg, "Order not found");
1360 assert_eq!(data.len(), 1);
1361 }
1362 _ => panic!("Expected OrderResponse variant"),
1363 }
1364 }
1365
1366 #[rstest]
1367 fn test_message_request_serialization() {
1368 let request = OKXWsRequest {
1369 id: Some("req-123".to_string()),
1370 op: OKXWsOperation::Order,
1371 args: vec![::serde_json::json!({
1372 "instId": "BTC-USDT",
1373 "tdMode": "cash",
1374 "side": "buy",
1375 "ordType": "market",
1376 "sz": "0.1"
1377 })],
1378 exp_time: None,
1379 };
1380
1381 let serialized = serde_json::to_string(&request).unwrap();
1382 let parsed: serde_json::Value = serde_json::from_str(&serialized).unwrap();
1383
1384 assert_eq!(parsed["id"], "req-123");
1385 assert_eq!(parsed["op"], "order");
1386 assert!(parsed["args"].is_array());
1387 assert_eq!(parsed["args"].as_array().unwrap().len(), 1);
1388 }
1389}