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