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