1use derive_builder::Builder;
19use nautilus_model::{
20 data::{Data, FundingRateUpdate, OrderBookDeltas},
21 events::{
22 AccountState, OrderAccepted, OrderCancelRejected, OrderCanceled, OrderExpired,
23 OrderModifyRejected, OrderRejected, OrderTriggered, OrderUpdated,
24 },
25 instruments::InstrumentAny,
26 reports::{FillReport, OrderStatusReport, PositionStatusReport},
27};
28use serde::{Deserialize, Serialize};
29use ustr::Ustr;
30
31use super::enums::{OKXWsChannel, OKXWsOperation};
32use crate::{
33 common::{
34 enums::{
35 OKXAlgoOrderType, OKXBookAction, OKXCandleConfirm, OKXExecType, OKXInstrumentType,
36 OKXOrderCategory, OKXOrderStatus, OKXOrderType, OKXPositionSide, OKXSide,
37 OKXTargetCurrency, OKXTradeMode, OKXTriggerType,
38 },
39 parse::{
40 deserialize_empty_string_as_none, deserialize_string_to_u64,
41 deserialize_target_currency_as_none,
42 },
43 },
44 websocket::enums::OKXSubscriptionEvent,
45};
46
47#[derive(Debug, Clone)]
48pub enum NautilusWsMessage {
49 Data(Vec<Data>),
50 Deltas(OrderBookDeltas),
51 FundingRates(Vec<FundingRateUpdate>),
52 Instrument(Box<InstrumentAny>),
53 AccountUpdate(AccountState),
54 PositionUpdate(PositionStatusReport),
55 OrderAccepted(OrderAccepted),
56 OrderCanceled(OrderCanceled),
57 OrderExpired(OrderExpired),
58 OrderRejected(OrderRejected),
59 OrderCancelRejected(OrderCancelRejected),
60 OrderModifyRejected(OrderModifyRejected),
61 OrderTriggered(OrderTriggered),
62 OrderUpdated(OrderUpdated),
63 ExecutionReports(Vec<ExecutionReport>),
64 Error(OKXWebSocketError),
65 Raw(serde_json::Value), Reconnected,
67 Authenticated,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
72#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object))]
73pub struct OKXWebSocketError {
74 pub code: String,
76 pub message: String,
78 pub conn_id: Option<String>,
80 pub timestamp: u64,
82}
83
84#[derive(Debug, Clone)]
85#[allow(clippy::large_enum_variant)]
86pub enum ExecutionReport {
87 Order(OrderStatusReport),
88 Fill(FillReport),
89}
90
91#[derive(Debug, Serialize)]
93#[serde(rename_all = "camelCase")]
94pub struct OKXWsRequest<T> {
95 #[serde(skip_serializing_if = "Option::is_none")]
97 pub id: Option<String>,
98 pub op: OKXWsOperation,
100 #[serde(skip_serializing_if = "Option::is_none")]
103 pub exp_time: Option<String>,
104 pub args: Vec<T>,
106}
107
108#[derive(Debug, Serialize)]
110pub struct OKXAuthentication {
111 pub op: &'static str,
112 pub args: Vec<OKXAuthenticationArg>,
113}
114
115#[derive(Debug, Serialize)]
117#[serde(rename_all = "camelCase")]
118pub struct OKXAuthenticationArg {
119 pub api_key: String,
120 pub passphrase: String,
121 pub timestamp: String,
122 pub sign: String,
123}
124
125#[derive(Debug, Serialize)]
126pub struct OKXSubscription {
127 pub op: OKXWsOperation,
128 pub args: Vec<OKXSubscriptionArg>,
129}
130
131#[derive(Clone, Debug, Serialize)]
132#[serde(rename_all = "camelCase")]
133pub struct OKXSubscriptionArg {
134 pub channel: OKXWsChannel,
135 pub inst_type: Option<OKXInstrumentType>,
136 pub inst_family: Option<Ustr>,
137 pub inst_id: Option<Ustr>,
138}
139
140#[derive(Debug)]
145pub enum OKXWsMessage {
146 Login {
147 event: String,
148 code: String,
149 msg: String,
150 conn_id: String,
151 },
152 Subscription {
153 event: OKXSubscriptionEvent,
154 arg: OKXWebSocketArg,
155 conn_id: String,
156 code: Option<String>,
157 msg: Option<String>,
158 },
159 ChannelConnCount {
160 event: String,
161 channel: OKXWsChannel,
162 conn_count: String,
163 conn_id: String,
164 },
165 OrderResponse {
166 id: Option<String>,
167 op: OKXWsOperation,
168 code: String,
169 msg: String,
170 data: Vec<serde_json::Value>,
171 },
172 BookData {
173 arg: OKXWebSocketArg,
174 action: OKXBookAction,
175 data: Vec<OKXBookMsg>,
176 },
177 Data {
178 arg: OKXWebSocketArg,
179 data: serde_json::Value,
180 },
181 Error {
182 code: String,
183 msg: String,
184 },
185 Ping,
186 Reconnected,
187}
188
189impl<'de> Deserialize<'de> for OKXWsMessage {
190 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
191 where
192 D: serde::Deserializer<'de>,
193 {
194 use serde::de::Error;
195
196 let value = serde_json::Value::deserialize(deserializer)?;
198 let obj = value
199 .as_object()
200 .ok_or_else(|| D::Error::custom("expected JSON object for OKXWsMessage"))?;
201
202 if let Some(event) = obj.get("event").and_then(|v| v.as_str()) {
206 if event == "login" {
207 return parse_login(obj);
208 } else if event == "subscribe" || event == "unsubscribe" {
209 return parse_subscription(obj);
210 } else if event == "error" {
211 return parse_error(obj);
214 } else if obj.contains_key("channel") && obj.contains_key("connCount") {
215 return parse_channel_conn_count(obj);
216 }
217 }
218
219 if obj.contains_key("op") {
221 return parse_order_response(obj);
222 }
223
224 if obj.contains_key("action") && obj.contains_key("arg") {
226 return parse_book_data(obj);
227 }
228
229 if obj.contains_key("arg") && obj.contains_key("data") {
231 return parse_data(obj);
232 }
233
234 if obj.contains_key("code") && obj.contains_key("msg") {
236 return parse_error(obj);
237 }
238
239 Err(D::Error::custom(format!(
240 "cannot determine OKXWsMessage variant from: {}",
241 serde_json::to_string(&value).unwrap_or_default()
242 )))
243 }
244}
245
246fn parse_login<E: serde::de::Error>(
247 obj: &serde_json::Map<String, serde_json::Value>,
248) -> Result<OKXWsMessage, E> {
249 Ok(OKXWsMessage::Login {
250 event: obj
251 .get("event")
252 .and_then(|v| v.as_str())
253 .map(String::from)
254 .ok_or_else(|| E::missing_field("event"))?,
255 code: obj
256 .get("code")
257 .and_then(|v| v.as_str())
258 .map(String::from)
259 .ok_or_else(|| E::missing_field("code"))?,
260 msg: obj
261 .get("msg")
262 .and_then(|v| v.as_str())
263 .map(String::from)
264 .ok_or_else(|| E::missing_field("msg"))?,
265 conn_id: obj
266 .get("connId")
267 .and_then(|v| v.as_str())
268 .map(String::from)
269 .ok_or_else(|| E::missing_field("connId"))?,
270 })
271}
272
273fn parse_subscription<E: serde::de::Error>(
274 obj: &serde_json::Map<String, serde_json::Value>,
275) -> Result<OKXWsMessage, E> {
276 let event_str = obj
277 .get("event")
278 .and_then(|v| v.as_str())
279 .ok_or_else(|| E::missing_field("event"))?;
280
281 let event: OKXSubscriptionEvent =
282 serde_json::from_value(serde_json::Value::String(event_str.to_string()))
283 .map_err(|e| E::custom(format!("invalid event: {e}")))?;
284
285 let arg: OKXWebSocketArg = obj
286 .get("arg")
287 .cloned()
288 .map(serde_json::from_value)
289 .transpose()
290 .map_err(|e| E::custom(format!("invalid arg: {e}")))?
291 .ok_or_else(|| E::missing_field("arg"))?;
292
293 Ok(OKXWsMessage::Subscription {
294 event,
295 arg,
296 conn_id: obj
297 .get("connId")
298 .and_then(|v| v.as_str())
299 .map(String::from)
300 .ok_or_else(|| E::missing_field("connId"))?,
301 code: obj.get("code").and_then(|v| v.as_str()).map(String::from),
302 msg: obj.get("msg").and_then(|v| v.as_str()).map(String::from),
303 })
304}
305
306fn parse_channel_conn_count<E: serde::de::Error>(
307 obj: &serde_json::Map<String, serde_json::Value>,
308) -> Result<OKXWsMessage, E> {
309 let channel: OKXWsChannel = obj
310 .get("channel")
311 .cloned()
312 .map(serde_json::from_value)
313 .transpose()
314 .map_err(|e| E::custom(format!("invalid channel: {e}")))?
315 .ok_or_else(|| E::missing_field("channel"))?;
316
317 Ok(OKXWsMessage::ChannelConnCount {
318 event: obj
319 .get("event")
320 .and_then(|v| v.as_str())
321 .map(String::from)
322 .ok_or_else(|| E::missing_field("event"))?,
323 channel,
324 conn_count: obj
325 .get("connCount")
326 .and_then(|v| v.as_str())
327 .map(String::from)
328 .ok_or_else(|| E::missing_field("connCount"))?,
329 conn_id: obj
330 .get("connId")
331 .and_then(|v| v.as_str())
332 .map(String::from)
333 .ok_or_else(|| E::missing_field("connId"))?,
334 })
335}
336
337fn parse_order_response<E: serde::de::Error>(
338 obj: &serde_json::Map<String, serde_json::Value>,
339) -> Result<OKXWsMessage, E> {
340 let op: OKXWsOperation = obj
341 .get("op")
342 .cloned()
343 .map(serde_json::from_value)
344 .transpose()
345 .map_err(|e| E::custom(format!("invalid op: {e}")))?
346 .ok_or_else(|| E::missing_field("op"))?;
347
348 let data: Vec<serde_json::Value> = obj
349 .get("data")
350 .cloned()
351 .map(serde_json::from_value)
352 .transpose()
353 .map_err(|e| E::custom(format!("invalid data: {e}")))?
354 .unwrap_or_default();
355
356 Ok(OKXWsMessage::OrderResponse {
357 id: obj.get("id").and_then(|v| v.as_str()).map(String::from),
358 op,
359 code: obj
360 .get("code")
361 .and_then(|v| v.as_str())
362 .map(String::from)
363 .ok_or_else(|| E::missing_field("code"))?,
364 msg: obj
365 .get("msg")
366 .and_then(|v| v.as_str())
367 .map(String::from)
368 .ok_or_else(|| E::missing_field("msg"))?,
369 data,
370 })
371}
372
373fn parse_book_data<E: serde::de::Error>(
374 obj: &serde_json::Map<String, serde_json::Value>,
375) -> Result<OKXWsMessage, E> {
376 let arg: OKXWebSocketArg = obj
377 .get("arg")
378 .cloned()
379 .map(serde_json::from_value)
380 .transpose()
381 .map_err(|e| E::custom(format!("invalid arg: {e}")))?
382 .ok_or_else(|| E::missing_field("arg"))?;
383
384 let action: OKXBookAction = obj
385 .get("action")
386 .cloned()
387 .map(serde_json::from_value)
388 .transpose()
389 .map_err(|e| E::custom(format!("invalid action: {e}")))?
390 .ok_or_else(|| E::missing_field("action"))?;
391
392 let data: Vec<OKXBookMsg> = obj
393 .get("data")
394 .cloned()
395 .map(serde_json::from_value)
396 .transpose()
397 .map_err(|e| E::custom(format!("invalid data: {e}")))?
398 .ok_or_else(|| E::missing_field("data"))?;
399
400 Ok(OKXWsMessage::BookData { arg, action, data })
401}
402
403fn parse_data<E: serde::de::Error>(
404 obj: &serde_json::Map<String, serde_json::Value>,
405) -> Result<OKXWsMessage, E> {
406 let arg: OKXWebSocketArg = obj
407 .get("arg")
408 .cloned()
409 .map(serde_json::from_value)
410 .transpose()
411 .map_err(|e| E::custom(format!("invalid arg: {e}")))?
412 .ok_or_else(|| E::missing_field("arg"))?;
413
414 let data = obj
415 .get("data")
416 .cloned()
417 .ok_or_else(|| E::missing_field("data"))?;
418
419 Ok(OKXWsMessage::Data { arg, data })
420}
421
422fn parse_error<E: serde::de::Error>(
423 obj: &serde_json::Map<String, serde_json::Value>,
424) -> Result<OKXWsMessage, E> {
425 Ok(OKXWsMessage::Error {
426 code: obj
427 .get("code")
428 .and_then(|v| v.as_str())
429 .map(String::from)
430 .ok_or_else(|| E::missing_field("code"))?,
431 msg: obj
432 .get("msg")
433 .and_then(|v| v.as_str())
434 .map(String::from)
435 .ok_or_else(|| E::missing_field("msg"))?,
436 })
437}
438
439#[derive(Debug, Serialize, Deserialize)]
440#[serde(rename_all = "camelCase")]
441pub struct OKXWebSocketArg {
442 pub channel: OKXWsChannel,
444 #[serde(default)]
445 pub inst_id: Option<Ustr>,
446 #[serde(default)]
447 pub inst_type: Option<OKXInstrumentType>,
448 #[serde(default)]
449 pub inst_family: Option<Ustr>,
450 #[serde(default)]
451 pub bar: Option<Ustr>,
452}
453
454#[derive(Debug, Serialize, Deserialize)]
456#[serde(rename_all = "camelCase")]
457pub struct OKXTickerMsg {
458 pub inst_type: OKXInstrumentType,
460 pub inst_id: Ustr,
462 #[serde(rename = "last")]
464 pub last_px: String,
465 pub last_sz: String,
467 pub ask_px: String,
469 pub ask_sz: String,
471 pub bid_px: String,
473 pub bid_sz: String,
475 pub open24h: String,
477 pub high24h: String,
479 pub low24h: String,
481 pub vol_ccy_24h: String,
483 pub vol24h: String,
485 pub sod_utc0: String,
487 pub sod_utc8: String,
489 #[serde(deserialize_with = "deserialize_string_to_u64")]
491 pub ts: u64,
492}
493
494#[derive(Debug, Serialize, Deserialize)]
496pub struct OrderBookEntry {
497 pub price: String,
499 pub size: String,
501 pub liquidated_orders_count: String,
503 pub orders_count: String,
505}
506
507#[derive(Debug, Serialize, Deserialize)]
509#[serde(rename_all = "camelCase")]
510pub struct OKXBookMsg {
511 pub asks: Vec<OrderBookEntry>,
513 pub bids: Vec<OrderBookEntry>,
515 pub checksum: Option<i64>,
517 pub prev_seq_id: Option<i64>,
519 pub seq_id: u64,
521 #[serde(deserialize_with = "deserialize_string_to_u64")]
523 pub ts: u64,
524}
525
526#[derive(Debug, Serialize, Deserialize)]
528#[serde(rename_all = "camelCase")]
529pub struct OKXTradeMsg {
530 pub inst_id: Ustr,
532 pub trade_id: String,
534 pub px: String,
536 pub sz: String,
538 pub side: OKXSide,
540 pub count: String,
542 #[serde(deserialize_with = "deserialize_string_to_u64")]
544 pub ts: u64,
545}
546
547#[derive(Debug, Serialize, Deserialize)]
549#[serde(rename_all = "camelCase")]
550pub struct OKXFundingRateMsg {
551 pub inst_id: Ustr,
553 pub funding_rate: Ustr,
555 pub next_funding_rate: Ustr,
557 #[serde(deserialize_with = "deserialize_string_to_u64")]
559 pub funding_time: u64,
560 #[serde(deserialize_with = "deserialize_string_to_u64")]
562 pub ts: u64,
563}
564
565#[derive(Debug, Serialize, Deserialize)]
567#[serde(rename_all = "camelCase")]
568pub struct OKXMarkPriceMsg {
569 pub inst_id: Ustr,
571 pub mark_px: String,
573 #[serde(deserialize_with = "deserialize_string_to_u64")]
575 pub ts: u64,
576}
577
578#[derive(Debug, Serialize, Deserialize)]
580#[serde(rename_all = "camelCase")]
581pub struct OKXIndexPriceMsg {
582 pub inst_id: Ustr,
584 pub idx_px: String,
586 pub high24h: String,
588 pub low24h: String,
590 pub open24h: String,
592 pub sod_utc0: String,
594 pub sod_utc8: String,
596 #[serde(deserialize_with = "deserialize_string_to_u64")]
598 pub ts: u64,
599}
600
601#[derive(Debug, Serialize, Deserialize)]
603#[serde(rename_all = "camelCase")]
604pub struct OKXPriceLimitMsg {
605 pub inst_id: Ustr,
607 pub buy_lmt: String,
609 pub sell_lmt: String,
611 #[serde(deserialize_with = "deserialize_string_to_u64")]
613 pub ts: u64,
614}
615
616#[derive(Debug, Serialize, Deserialize)]
618#[serde(rename_all = "camelCase")]
619pub struct OKXCandleMsg {
620 #[serde(deserialize_with = "deserialize_string_to_u64")]
622 pub ts: u64,
623 pub o: String,
625 pub h: String,
627 pub l: String,
629 pub c: String,
631 pub vol: String,
633 pub vol_ccy: String,
635 pub vol_ccy_quote: String,
636 pub confirm: OKXCandleConfirm,
638}
639
640#[derive(Debug, Serialize, Deserialize)]
642#[serde(rename_all = "camelCase")]
643pub struct OKXOpenInterestMsg {
644 pub inst_id: Ustr,
646 pub oi: String,
648 pub oi_ccy: String,
650 #[serde(deserialize_with = "deserialize_string_to_u64")]
652 pub ts: u64,
653}
654
655#[derive(Debug, Serialize, Deserialize)]
657#[serde(rename_all = "camelCase")]
658pub struct OKXOptionSummaryMsg {
659 pub inst_id: Ustr,
661 pub uly: String,
663 pub delta: String,
665 pub gamma: String,
667 pub theta: String,
669 pub vega: String,
671 pub delta_bs: String,
673 pub gamma_bs: String,
675 pub theta_bs: String,
677 pub vega_bs: String,
679 pub real_vol: String,
681 pub bid_vol: String,
683 pub ask_vol: String,
685 pub mark_vol: String,
687 pub lever: String,
689 #[serde(deserialize_with = "deserialize_string_to_u64")]
691 pub ts: u64,
692}
693
694#[derive(Debug, Serialize, Deserialize)]
696#[serde(rename_all = "camelCase")]
697pub struct OKXEstimatedPriceMsg {
698 pub inst_id: Ustr,
700 pub settle_px: String,
702 #[serde(deserialize_with = "deserialize_string_to_u64")]
704 pub ts: u64,
705}
706
707#[derive(Debug, Serialize, Deserialize)]
709#[serde(rename_all = "camelCase")]
710pub struct OKXStatusMsg {
711 pub title: Ustr,
713 #[serde(rename = "type")]
715 pub status_type: Ustr,
716 pub state: Ustr,
718 pub end_time: Option<String>,
720 pub begin_time: Option<String>,
722 pub service_type: Option<Ustr>,
724 pub reason: Option<String>,
726 #[serde(deserialize_with = "deserialize_string_to_u64")]
728 pub ts: u64,
729}
730
731#[derive(Clone, Debug, Serialize, Deserialize)]
733#[serde(rename_all = "camelCase")]
734pub struct OKXOrderMsg {
735 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
737 pub acc_fill_sz: Option<String>,
738 pub avg_px: String,
740 #[serde(deserialize_with = "deserialize_string_to_u64")]
742 pub c_time: u64,
743 #[serde(default)]
745 pub cancel_source: Option<String>,
746 #[serde(default)]
748 pub cancel_source_reason: Option<String>,
749 pub category: OKXOrderCategory,
751 pub ccy: Ustr,
753 pub cl_ord_id: String,
755 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
757 pub algo_cl_ord_id: Option<String>,
758 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
760 pub fee: Option<String>,
761 pub fee_ccy: Ustr,
763 pub fill_px: String,
765 pub fill_sz: String,
767 #[serde(deserialize_with = "deserialize_string_to_u64")]
769 pub fill_time: u64,
770 pub inst_id: Ustr,
772 pub inst_type: OKXInstrumentType,
774 pub lever: String,
776 pub ord_id: Ustr,
778 pub ord_type: OKXOrderType,
780 pub pnl: String,
782 pub pos_side: OKXPositionSide,
784 #[serde(default)]
786 pub px: String,
787 pub reduce_only: String,
789 pub side: OKXSide,
791 pub state: OKXOrderStatus,
793 pub exec_type: OKXExecType,
795 pub sz: String,
797 pub td_mode: OKXTradeMode,
799 #[serde(default, deserialize_with = "deserialize_target_currency_as_none")]
801 pub tgt_ccy: Option<OKXTargetCurrency>,
802 pub trade_id: String,
804 #[serde(deserialize_with = "deserialize_string_to_u64")]
806 pub u_time: u64,
807}
808
809#[derive(Clone, Debug, Deserialize, Serialize)]
811#[serde(rename_all = "camelCase")]
812pub struct OKXAlgoOrderMsg {
813 pub algo_id: String,
815 #[serde(default)]
817 pub algo_cl_ord_id: String,
818 pub cl_ord_id: String,
820 pub ord_id: String,
822 pub inst_id: Ustr,
824 pub inst_type: OKXInstrumentType,
826 pub ord_type: OKXOrderType,
828 pub state: OKXOrderStatus,
830 pub side: OKXSide,
832 pub pos_side: OKXPositionSide,
834 pub sz: String,
836 pub trigger_px: String,
838 pub trigger_px_type: OKXTriggerType,
840 pub ord_px: String,
842 pub td_mode: OKXTradeMode,
844 pub lever: String,
846 pub reduce_only: String,
848 pub actual_px: String,
850 pub actual_sz: String,
852 pub notional_usd: String,
854 #[serde(deserialize_with = "deserialize_string_to_u64")]
856 pub c_time: u64,
857 #[serde(deserialize_with = "deserialize_string_to_u64")]
859 pub u_time: u64,
860 pub trigger_time: String,
862 #[serde(default)]
864 pub tag: String,
865}
866
867#[derive(Clone, Debug, Deserialize, Serialize, Builder)]
869#[builder(setter(into, strip_option))]
870#[serde(rename_all = "camelCase")]
871pub struct WsPostOrderParams {
872 #[builder(default)]
874 #[serde(skip_serializing_if = "Option::is_none")]
875 pub inst_type: Option<OKXInstrumentType>,
876 pub inst_id: Ustr,
878 #[builder(default)]
880 #[serde(skip_serializing_if = "Option::is_none")]
881 pub inst_id_code: Option<u64>,
882 pub td_mode: OKXTradeMode,
884 #[builder(default)]
886 #[serde(skip_serializing_if = "Option::is_none")]
887 pub ccy: Option<Ustr>,
888 #[builder(default)]
890 #[serde(skip_serializing_if = "Option::is_none")]
891 pub cl_ord_id: Option<String>,
892 pub side: OKXSide,
894 #[builder(default)]
896 #[serde(skip_serializing_if = "Option::is_none")]
897 pub pos_side: Option<OKXPositionSide>,
898 pub ord_type: OKXOrderType,
900 pub sz: String,
902 #[builder(default)]
904 #[serde(skip_serializing_if = "Option::is_none")]
905 pub px: Option<String>,
906 #[builder(default)]
908 #[serde(skip_serializing_if = "Option::is_none")]
909 pub reduce_only: Option<bool>,
910 #[builder(default)]
912 #[serde(rename = "closePosition", skip_serializing_if = "Option::is_none")]
913 pub close_position: Option<bool>,
914 #[builder(default)]
916 #[serde(skip_serializing_if = "Option::is_none")]
917 pub tgt_ccy: Option<OKXTargetCurrency>,
918 #[builder(default)]
920 #[serde(skip_serializing_if = "Option::is_none")]
921 pub tag: Option<String>,
922}
923
924#[derive(Clone, Debug, Default, Deserialize, Serialize, Builder)]
926#[builder(default)]
927#[builder(setter(into, strip_option))]
928#[serde(rename_all = "camelCase")]
929pub struct WsCancelOrderParams {
930 pub inst_id: Ustr,
932 #[serde(skip_serializing_if = "Option::is_none")]
934 pub inst_id_code: Option<u64>,
935 #[serde(skip_serializing_if = "Option::is_none")]
937 pub ord_id: Option<String>,
938 #[serde(skip_serializing_if = "Option::is_none")]
940 pub cl_ord_id: Option<String>,
941}
942
943#[derive(Clone, Debug, Default, Deserialize, Serialize, Builder)]
945#[builder(default)]
946#[builder(setter(into, strip_option))]
947#[serde(rename_all = "camelCase")]
948pub struct WsMassCancelParams {
949 pub inst_type: OKXInstrumentType,
951 pub inst_family: Ustr,
953}
954
955#[derive(Clone, Debug, Default, Deserialize, Serialize, Builder)]
957#[builder(default)]
958#[builder(setter(into, strip_option))]
959#[serde(rename_all = "camelCase")]
960pub struct WsAmendOrderParams {
961 pub inst_id: Ustr,
963 #[serde(skip_serializing_if = "Option::is_none")]
965 pub inst_id_code: Option<u64>,
966 #[serde(skip_serializing_if = "Option::is_none")]
968 pub ord_id: Option<String>,
969 #[serde(skip_serializing_if = "Option::is_none")]
971 pub cl_ord_id: Option<String>,
972 #[serde(skip_serializing_if = "Option::is_none")]
974 pub new_cl_ord_id: Option<String>,
975 #[serde(skip_serializing_if = "Option::is_none")]
977 pub new_px: Option<String>,
978 #[serde(skip_serializing_if = "Option::is_none")]
980 pub new_sz: Option<String>,
981}
982
983#[derive(Clone, Debug, Deserialize, Serialize, Builder)]
985#[builder(setter(into, strip_option))]
986#[serde(rename_all = "camelCase")]
987pub struct WsPostAlgoOrderParams {
988 pub inst_id: Ustr,
990 #[builder(default)]
992 #[serde(skip_serializing_if = "Option::is_none")]
993 pub inst_id_code: Option<u64>,
994 pub td_mode: OKXTradeMode,
996 pub side: OKXSide,
998 pub ord_type: OKXAlgoOrderType,
1000 pub sz: String,
1002 #[builder(default)]
1004 #[serde(skip_serializing_if = "Option::is_none")]
1005 pub cl_ord_id: Option<String>,
1006 #[builder(default)]
1008 #[serde(skip_serializing_if = "Option::is_none")]
1009 pub pos_side: Option<OKXPositionSide>,
1010 #[serde(skip_serializing_if = "Option::is_none")]
1012 pub trigger_px: Option<String>,
1013 #[builder(default)]
1015 #[serde(skip_serializing_if = "Option::is_none")]
1016 pub trigger_px_type: Option<OKXTriggerType>,
1017 #[builder(default)]
1019 #[serde(skip_serializing_if = "Option::is_none")]
1020 pub order_px: Option<String>,
1021 #[builder(default)]
1023 #[serde(skip_serializing_if = "Option::is_none")]
1024 pub reduce_only: Option<bool>,
1025 #[builder(default)]
1027 #[serde(skip_serializing_if = "Option::is_none")]
1028 pub tag: Option<String>,
1029}
1030
1031#[derive(Clone, Debug, Deserialize, Serialize, Builder)]
1033#[builder(setter(into, strip_option))]
1034#[serde(rename_all = "camelCase")]
1035pub struct WsCancelAlgoOrderParams {
1036 pub inst_id: Ustr,
1038 #[builder(default)]
1040 #[serde(skip_serializing_if = "Option::is_none")]
1041 pub inst_id_code: Option<u64>,
1042 #[serde(skip_serializing_if = "Option::is_none")]
1044 pub algo_id: Option<String>,
1045 #[serde(skip_serializing_if = "Option::is_none")]
1047 pub algo_cl_ord_id: Option<String>,
1048}
1049
1050#[cfg(test)]
1051mod tests {
1052 use nautilus_core::time::get_atomic_clock_realtime;
1053 use rstest::rstest;
1054
1055 use super::*;
1056
1057 #[rstest]
1058 fn test_deserialize_websocket_arg() {
1059 let json_str = r#"{"channel":"instruments","instType":"SPOT"}"#;
1060
1061 let result: Result<OKXWebSocketArg, _> = serde_json::from_str(json_str);
1062 match result {
1063 Ok(arg) => {
1064 assert_eq!(arg.channel, OKXWsChannel::Instruments);
1065 assert_eq!(arg.inst_type, Some(OKXInstrumentType::Spot));
1066 assert_eq!(arg.inst_id, None);
1067 }
1068 Err(e) => {
1069 panic!("Failed to deserialize WebSocket arg: {e}");
1070 }
1071 }
1072 }
1073
1074 #[rstest]
1075 fn test_deserialize_subscribe_variant_direct() {
1076 #[derive(Debug, Deserialize)]
1077 #[serde(rename_all = "camelCase")]
1078 struct SubscribeMsg {
1079 event: String,
1080 arg: OKXWebSocketArg,
1081 conn_id: String,
1082 }
1083
1084 let json_str = r#"{"event":"subscribe","arg":{"channel":"instruments","instType":"SPOT"},"connId":"380cfa6a"}"#;
1085
1086 let result: Result<SubscribeMsg, _> = serde_json::from_str(json_str);
1087 match result {
1088 Ok(msg) => {
1089 assert_eq!(msg.event, "subscribe");
1090 assert_eq!(msg.arg.channel, OKXWsChannel::Instruments);
1091 assert_eq!(msg.conn_id, "380cfa6a");
1092 }
1093 Err(e) => {
1094 panic!("Failed to deserialize subscribe message directly: {e}");
1095 }
1096 }
1097 }
1098
1099 #[rstest]
1100 fn test_deserialize_subscribe_confirmation() {
1101 let json_str = r#"{"event":"subscribe","arg":{"channel":"instruments","instType":"SPOT"},"connId":"380cfa6a"}"#;
1102
1103 let result: Result<OKXWsMessage, _> = serde_json::from_str(json_str);
1104 match result {
1105 Ok(msg) => {
1106 if let OKXWsMessage::Subscription {
1107 event,
1108 arg,
1109 conn_id,
1110 ..
1111 } = msg
1112 {
1113 assert_eq!(event, OKXSubscriptionEvent::Subscribe);
1114 assert_eq!(arg.channel, OKXWsChannel::Instruments);
1115 assert_eq!(conn_id, "380cfa6a");
1116 } else {
1117 panic!("Expected Subscribe variant, was: {msg:?}");
1118 }
1119 }
1120 Err(e) => {
1121 panic!("Failed to deserialize subscription confirmation: {e}");
1122 }
1123 }
1124 }
1125
1126 #[rstest]
1127 fn test_deserialize_subscribe_with_inst_id() {
1128 let json_str = r#"{"event":"subscribe","arg":{"channel":"candle1m","instId":"ETH-USDT"},"connId":"358602f5"}"#;
1129
1130 let result: Result<OKXWsMessage, _> = serde_json::from_str(json_str);
1131 match result {
1132 Ok(msg) => {
1133 if let OKXWsMessage::Subscription {
1134 event,
1135 arg,
1136 conn_id,
1137 ..
1138 } = msg
1139 {
1140 assert_eq!(event, OKXSubscriptionEvent::Subscribe);
1141 assert_eq!(arg.channel, OKXWsChannel::Candle1Minute);
1142 assert_eq!(conn_id, "358602f5");
1143 } else {
1144 panic!("Expected Subscribe variant, was: {msg:?}");
1145 }
1146 }
1147 Err(e) => {
1148 panic!("Failed to deserialize subscription confirmation: {e}");
1149 }
1150 }
1151 }
1152
1153 #[rstest]
1154 fn test_channel_serialization_for_logging() {
1155 let channel = OKXWsChannel::Candle1Minute;
1156 let serialized = serde_json::to_string(&channel).unwrap();
1157 let cleaned = serialized.trim_matches('"').to_string();
1158 assert_eq!(cleaned, "candle1m");
1159
1160 let channel = OKXWsChannel::BboTbt;
1161 let serialized = serde_json::to_string(&channel).unwrap();
1162 let cleaned = serialized.trim_matches('"').to_string();
1163 assert_eq!(cleaned, "bbo-tbt");
1164
1165 let channel = OKXWsChannel::Trades;
1166 let serialized = serde_json::to_string(&channel).unwrap();
1167 let cleaned = serialized.trim_matches('"').to_string();
1168 assert_eq!(cleaned, "trades");
1169 }
1170
1171 #[rstest]
1172 fn test_order_response_with_enum_operation() {
1173 let json_str = r#"{"id":"req-123","op":"order","code":"0","msg":"","data":[]}"#;
1174 let result: Result<OKXWsMessage, _> = serde_json::from_str(json_str);
1175 match result {
1176 Ok(OKXWsMessage::OrderResponse {
1177 id,
1178 op,
1179 code,
1180 msg,
1181 data,
1182 }) => {
1183 assert_eq!(id, Some("req-123".to_string()));
1184 assert_eq!(op, OKXWsOperation::Order);
1185 assert_eq!(code, "0");
1186 assert_eq!(msg, "");
1187 assert!(data.is_empty());
1188 }
1189 Ok(other) => panic!("Expected OrderResponse, was: {other:?}"),
1190 Err(e) => panic!("Failed to deserialize: {e}"),
1191 }
1192
1193 let json_str = r#"{"id":"cancel-456","op":"cancel-order","code":"50001","msg":"Order not found","data":[]}"#;
1194 let result: Result<OKXWsMessage, _> = serde_json::from_str(json_str);
1195 match result {
1196 Ok(OKXWsMessage::OrderResponse {
1197 id,
1198 op,
1199 code,
1200 msg,
1201 data,
1202 }) => {
1203 assert_eq!(id, Some("cancel-456".to_string()));
1204 assert_eq!(op, OKXWsOperation::CancelOrder);
1205 assert_eq!(code, "50001");
1206 assert_eq!(msg, "Order not found");
1207 assert!(data.is_empty());
1208 }
1209 Ok(other) => panic!("Expected OrderResponse, was: {other:?}"),
1210 Err(e) => panic!("Failed to deserialize: {e}"),
1211 }
1212
1213 let json_str = r#"{"id":"amend-789","op":"amend-order","code":"50002","msg":"Invalid price","data":[]}"#;
1214 let result: Result<OKXWsMessage, _> = serde_json::from_str(json_str);
1215 match result {
1216 Ok(OKXWsMessage::OrderResponse {
1217 id,
1218 op,
1219 code,
1220 msg,
1221 data,
1222 }) => {
1223 assert_eq!(id, Some("amend-789".to_string()));
1224 assert_eq!(op, OKXWsOperation::AmendOrder);
1225 assert_eq!(code, "50002");
1226 assert_eq!(msg, "Invalid price");
1227 assert!(data.is_empty());
1228 }
1229 Ok(other) => panic!("Expected OrderResponse, was: {other:?}"),
1230 Err(e) => panic!("Failed to deserialize: {e}"),
1231 }
1232 }
1233
1234 #[rstest]
1235 fn test_operation_enum_serialization() {
1236 let op = OKXWsOperation::Order;
1237 let serialized = serde_json::to_string(&op).unwrap();
1238 assert_eq!(serialized, "\"order\"");
1239
1240 let op = OKXWsOperation::CancelOrder;
1241 let serialized = serde_json::to_string(&op).unwrap();
1242 assert_eq!(serialized, "\"cancel-order\"");
1243
1244 let op = OKXWsOperation::AmendOrder;
1245 let serialized = serde_json::to_string(&op).unwrap();
1246 assert_eq!(serialized, "\"amend-order\"");
1247
1248 let op = OKXWsOperation::Subscribe;
1249 let serialized = serde_json::to_string(&op).unwrap();
1250 assert_eq!(serialized, "\"subscribe\"");
1251 }
1252
1253 #[rstest]
1254 fn test_order_response_parsing() {
1255 let success_response = r#"{
1256 "id": "req-123",
1257 "op": "order",
1258 "code": "0",
1259 "msg": "",
1260 "data": [{"sMsg": "Order placed successfully"}]
1261 }"#;
1262
1263 let parsed: OKXWsMessage = serde_json::from_str(success_response).unwrap();
1264
1265 match parsed {
1266 OKXWsMessage::OrderResponse {
1267 id,
1268 op,
1269 code,
1270 msg,
1271 data,
1272 } => {
1273 assert_eq!(id, Some("req-123".to_string()));
1274 assert_eq!(op, OKXWsOperation::Order);
1275 assert_eq!(code, "0");
1276 assert_eq!(msg, "");
1277 assert_eq!(data.len(), 1);
1278 }
1279 _ => panic!("Expected OrderResponse variant"),
1280 }
1281
1282 let failure_response = r#"{
1283 "id": "req-456",
1284 "op": "cancel-order",
1285 "code": "50001",
1286 "msg": "Order not found",
1287 "data": [{"sMsg": "Order with client order ID not found"}]
1288 }"#;
1289
1290 let parsed: OKXWsMessage = serde_json::from_str(failure_response).unwrap();
1291
1292 match parsed {
1293 OKXWsMessage::OrderResponse {
1294 id,
1295 op,
1296 code,
1297 msg,
1298 data,
1299 } => {
1300 assert_eq!(id, Some("req-456".to_string()));
1301 assert_eq!(op, OKXWsOperation::CancelOrder);
1302 assert_eq!(code, "50001");
1303 assert_eq!(msg, "Order not found");
1304 assert_eq!(data.len(), 1);
1305 }
1306 _ => panic!("Expected OrderResponse variant"),
1307 }
1308 }
1309
1310 #[rstest]
1311 fn test_subscription_event_parsing() {
1312 let subscription_json = r#"{
1313 "event": "subscribe",
1314 "arg": {
1315 "channel": "tickers",
1316 "instId": "BTC-USDT"
1317 },
1318 "connId": "a4d3ae55"
1319 }"#;
1320
1321 let parsed: OKXWsMessage = serde_json::from_str(subscription_json).unwrap();
1322
1323 match parsed {
1324 OKXWsMessage::Subscription {
1325 event,
1326 arg,
1327 conn_id,
1328 ..
1329 } => {
1330 assert_eq!(
1331 event,
1332 crate::websocket::enums::OKXSubscriptionEvent::Subscribe
1333 );
1334 assert_eq!(arg.channel, OKXWsChannel::Tickers);
1335 assert_eq!(arg.inst_id, Some(Ustr::from("BTC-USDT")));
1336 assert_eq!(conn_id, "a4d3ae55");
1337 }
1338 _ => panic!("Expected Subscription variant"),
1339 }
1340 }
1341
1342 #[rstest]
1343 fn test_login_event_parsing() {
1344 let login_success = r#"{
1345 "event": "login",
1346 "code": "0",
1347 "msg": "Login successful",
1348 "connId": "a4d3ae55"
1349 }"#;
1350
1351 let parsed: OKXWsMessage = serde_json::from_str(login_success).unwrap();
1352
1353 match parsed {
1354 OKXWsMessage::Login {
1355 event,
1356 code,
1357 msg,
1358 conn_id,
1359 } => {
1360 assert_eq!(event, "login");
1361 assert_eq!(code, "0");
1362 assert_eq!(msg, "Login successful");
1363 assert_eq!(conn_id, "a4d3ae55");
1364 }
1365 _ => panic!("Expected Login variant, was: {parsed:?}"),
1366 }
1367 }
1368
1369 #[rstest]
1370 fn test_error_event_parsing() {
1371 let error_json = r#"{
1372 "code": "60012",
1373 "msg": "Invalid request"
1374 }"#;
1375
1376 let parsed: OKXWsMessage = serde_json::from_str(error_json).unwrap();
1377
1378 match parsed {
1379 OKXWsMessage::Error { code, msg } => {
1380 assert_eq!(code, "60012");
1381 assert_eq!(msg, "Invalid request");
1382 }
1383 _ => panic!("Expected Error variant"),
1384 }
1385 }
1386
1387 #[rstest]
1388 fn test_error_event_with_event_field_parsing() {
1389 let error_json = r#"{
1391 "event": "error",
1392 "code": "60018",
1393 "msg": "Invalid sign"
1394 }"#;
1395
1396 let parsed: OKXWsMessage = serde_json::from_str(error_json).unwrap();
1397
1398 match parsed {
1399 OKXWsMessage::Error { code, msg } => {
1400 assert_eq!(code, "60018");
1401 assert_eq!(msg, "Invalid sign");
1402 }
1403 _ => panic!("Expected Error variant, was: {parsed:?}"),
1404 }
1405 }
1406
1407 #[rstest]
1408 fn test_subscription_error_with_arg_field_parsing() {
1409 let error_json = r#"{
1411 "event": "error",
1412 "arg": {"channel": "tickers", "instId": "INVALID-INST"},
1413 "code": "60012",
1414 "msg": "Invalid request: channel not found",
1415 "connId": "a4d3ae55"
1416 }"#;
1417
1418 let parsed: OKXWsMessage = serde_json::from_str(error_json).unwrap();
1419
1420 match parsed {
1421 OKXWsMessage::Error { code, msg } => {
1422 assert_eq!(code, "60012");
1423 assert_eq!(msg, "Invalid request: channel not found");
1424 }
1425 _ => panic!("Expected Error variant, was: {parsed:?}"),
1426 }
1427 }
1428
1429 #[rstest]
1430 fn test_websocket_request_serialization() {
1431 let request = OKXWsRequest {
1432 id: Some("req-123".to_string()),
1433 op: OKXWsOperation::Order,
1434 args: vec![serde_json::json!({
1435 "instId": "BTC-USDT",
1436 "tdMode": "cash",
1437 "side": "buy",
1438 "ordType": "market",
1439 "sz": "0.1"
1440 })],
1441 exp_time: None,
1442 };
1443
1444 let serialized = serde_json::to_string(&request).unwrap();
1445 let parsed: serde_json::Value = serde_json::from_str(&serialized).unwrap();
1446
1447 assert_eq!(parsed["id"], "req-123");
1448 assert_eq!(parsed["op"], "order");
1449 assert!(parsed["args"].is_array());
1450 assert_eq!(parsed["args"].as_array().unwrap().len(), 1);
1451 }
1452
1453 #[rstest]
1454 fn test_subscription_request_serialization() {
1455 let subscription = OKXSubscription {
1456 op: OKXWsOperation::Subscribe,
1457 args: vec![OKXSubscriptionArg {
1458 channel: OKXWsChannel::Tickers,
1459 inst_type: Some(OKXInstrumentType::Spot),
1460 inst_family: None,
1461 inst_id: Some(Ustr::from("BTC-USDT")),
1462 }],
1463 };
1464
1465 let serialized = serde_json::to_string(&subscription).unwrap();
1466 let parsed: serde_json::Value = serde_json::from_str(&serialized).unwrap();
1467
1468 assert_eq!(parsed["op"], "subscribe");
1469 assert!(parsed["args"].is_array());
1470 assert_eq!(parsed["args"][0]["channel"], "tickers");
1471 assert_eq!(parsed["args"][0]["instType"], "SPOT");
1472 assert_eq!(parsed["args"][0]["instId"], "BTC-USDT");
1473 }
1474
1475 #[rstest]
1476 fn test_error_message_extraction() {
1477 let responses = vec![
1478 (
1479 r#"{
1480 "id": "req-123",
1481 "op": "order",
1482 "code": "50001",
1483 "msg": "Order failed",
1484 "data": [{"sMsg": "Insufficient balance"}]
1485 }"#,
1486 "Insufficient balance",
1487 ),
1488 (
1489 r#"{
1490 "id": "req-456",
1491 "op": "cancel-order",
1492 "code": "50002",
1493 "msg": "Cancel failed",
1494 "data": [{}]
1495 }"#,
1496 "Cancel failed",
1497 ),
1498 ];
1499
1500 for (response_json, expected_msg) in responses {
1501 let parsed: OKXWsMessage = serde_json::from_str(response_json).unwrap();
1502
1503 match parsed {
1504 OKXWsMessage::OrderResponse {
1505 id: _,
1506 op: _,
1507 code,
1508 msg,
1509 data,
1510 } => {
1511 assert_ne!(code, "0"); let error_msg = data
1515 .first()
1516 .and_then(|d| d.get("sMsg"))
1517 .and_then(|s| s.as_str())
1518 .filter(|s| !s.is_empty())
1519 .unwrap_or(&msg);
1520
1521 assert_eq!(error_msg, expected_msg);
1522 }
1523 _ => panic!("Expected OrderResponse variant"),
1524 }
1525 }
1526 }
1527
1528 #[rstest]
1529 fn test_book_data_parsing() {
1530 let book_data_json = r#"{
1531 "arg": {
1532 "channel": "books",
1533 "instId": "BTC-USDT"
1534 },
1535 "action": "snapshot",
1536 "data": [{
1537 "asks": [["50000.0", "0.1", "0", "1"]],
1538 "bids": [["49999.0", "0.2", "0", "1"]],
1539 "ts": "1640995200000",
1540 "checksum": 123456789,
1541 "seqId": 1000
1542 }]
1543 }"#;
1544
1545 let parsed: OKXWsMessage = serde_json::from_str(book_data_json).unwrap();
1546
1547 match parsed {
1548 OKXWsMessage::BookData { arg, action, data } => {
1549 assert_eq!(arg.channel, OKXWsChannel::Books);
1550 assert_eq!(arg.inst_id, Some(Ustr::from("BTC-USDT")));
1551 assert_eq!(
1552 action,
1553 super::super::super::common::enums::OKXBookAction::Snapshot
1554 );
1555 assert_eq!(data.len(), 1);
1556 }
1557 _ => panic!("Expected BookData variant"),
1558 }
1559 }
1560
1561 #[rstest]
1562 fn test_data_event_parsing() {
1563 let data_json = r#"{
1564 "arg": {
1565 "channel": "trades",
1566 "instId": "BTC-USDT"
1567 },
1568 "data": [{
1569 "instId": "BTC-USDT",
1570 "tradeId": "12345",
1571 "px": "50000.0",
1572 "sz": "0.1",
1573 "side": "buy",
1574 "ts": "1640995200000"
1575 }]
1576 }"#;
1577
1578 let parsed: OKXWsMessage = serde_json::from_str(data_json).unwrap();
1579
1580 match parsed {
1581 OKXWsMessage::Data { arg, data } => {
1582 assert_eq!(arg.channel, OKXWsChannel::Trades);
1583 assert_eq!(arg.inst_id, Some(Ustr::from("BTC-USDT")));
1584 assert!(data.is_array());
1585 }
1586 _ => panic!("Expected Data variant"),
1587 }
1588 }
1589
1590 #[rstest]
1591 fn test_nautilus_message_variants() {
1592 let clock = get_atomic_clock_realtime();
1593 let ts_init = clock.get_time_ns();
1594
1595 let error = OKXWebSocketError {
1596 code: "60012".to_string(),
1597 message: "Invalid request".to_string(),
1598 conn_id: None,
1599 timestamp: ts_init.as_u64(),
1600 };
1601 let error_msg = NautilusWsMessage::Error(error);
1602
1603 match error_msg {
1604 NautilusWsMessage::Error(e) => {
1605 assert_eq!(e.code, "60012");
1606 assert_eq!(e.message, "Invalid request");
1607 }
1608 _ => panic!("Expected Error variant"),
1609 }
1610
1611 let raw_scenarios = vec![
1612 ::serde_json::json!({"unknown": "data"}),
1613 ::serde_json::json!({"channel": "unsupported", "data": [1, 2, 3]}),
1614 ::serde_json::json!({"complex": {"nested": {"structure": true}}}),
1615 ];
1616
1617 for raw_data in raw_scenarios {
1618 let raw_msg = NautilusWsMessage::Raw(raw_data.clone());
1619
1620 match raw_msg {
1621 NautilusWsMessage::Raw(data) => {
1622 assert_eq!(data, raw_data);
1623 }
1624 _ => panic!("Expected Raw variant"),
1625 }
1626 }
1627 }
1628
1629 #[rstest]
1630 fn test_order_response_parsing_success() {
1631 let order_response_json = r#"{
1632 "id": "req-123",
1633 "op": "order",
1634 "code": "0",
1635 "msg": "",
1636 "data": [{"sMsg": "Order placed successfully"}]
1637 }"#;
1638
1639 let parsed: OKXWsMessage = serde_json::from_str(order_response_json).unwrap();
1640
1641 match parsed {
1642 OKXWsMessage::OrderResponse {
1643 id,
1644 op,
1645 code,
1646 msg,
1647 data,
1648 } => {
1649 assert_eq!(id, Some("req-123".to_string()));
1650 assert_eq!(op, OKXWsOperation::Order);
1651 assert_eq!(code, "0");
1652 assert_eq!(msg, "");
1653 assert_eq!(data.len(), 1);
1654 }
1655 _ => panic!("Expected OrderResponse variant"),
1656 }
1657 }
1658
1659 #[rstest]
1660 fn test_order_response_parsing_failure() {
1661 let order_response_json = r#"{
1662 "id": "req-456",
1663 "op": "cancel-order",
1664 "code": "50001",
1665 "msg": "Order not found",
1666 "data": [{"sMsg": "Order with client order ID not found"}]
1667 }"#;
1668
1669 let parsed: OKXWsMessage = serde_json::from_str(order_response_json).unwrap();
1670
1671 match parsed {
1672 OKXWsMessage::OrderResponse {
1673 id,
1674 op,
1675 code,
1676 msg,
1677 data,
1678 } => {
1679 assert_eq!(id, Some("req-456".to_string()));
1680 assert_eq!(op, OKXWsOperation::CancelOrder);
1681 assert_eq!(code, "50001");
1682 assert_eq!(msg, "Order not found");
1683 assert_eq!(data.len(), 1);
1684 }
1685 _ => panic!("Expected OrderResponse variant"),
1686 }
1687 }
1688
1689 #[rstest]
1690 fn test_message_request_serialization() {
1691 let request = OKXWsRequest {
1692 id: Some("req-123".to_string()),
1693 op: OKXWsOperation::Order,
1694 args: vec![::serde_json::json!({
1695 "instId": "BTC-USDT",
1696 "tdMode": "cash",
1697 "side": "buy",
1698 "ordType": "market",
1699 "sz": "0.1"
1700 })],
1701 exp_time: None,
1702 };
1703
1704 let serialized = serde_json::to_string(&request).unwrap();
1705 let parsed: serde_json::Value = serde_json::from_str(&serialized).unwrap();
1706
1707 assert_eq!(parsed["id"], "req-123");
1708 assert_eq!(parsed["op"], "order");
1709 assert!(parsed["args"].is_array());
1710 assert_eq!(parsed["args"].as_array().unwrap().len(), 1);
1711 }
1712
1713 #[rstest]
1714 fn test_ws_post_order_params_with_inst_id_code() {
1715 use super::WsPostOrderParamsBuilder;
1716 use crate::common::enums::{OKXOrderType, OKXSide, OKXTradeMode};
1717
1718 let params = WsPostOrderParamsBuilder::default()
1719 .inst_id(Ustr::from("BTC-USDT-SWAP"))
1720 .inst_id_code(10459u64)
1721 .td_mode(OKXTradeMode::Cross)
1722 .side(OKXSide::Buy)
1723 .ord_type(OKXOrderType::Limit)
1724 .sz("0.01".to_string())
1725 .px("50000".to_string())
1726 .build()
1727 .unwrap();
1728
1729 let json = serde_json::to_string(¶ms).unwrap();
1730
1731 assert!(json.contains("\"instIdCode\":10459"));
1733 assert!(json.contains("\"instId\":\"BTC-USDT-SWAP\""));
1734 }
1735
1736 #[rstest]
1737 fn test_ws_post_order_params_without_inst_id_code() {
1738 use super::WsPostOrderParamsBuilder;
1739 use crate::common::enums::{OKXOrderType, OKXSide, OKXTradeMode};
1740
1741 let params = WsPostOrderParamsBuilder::default()
1742 .inst_id(Ustr::from("BTC-USDT"))
1743 .td_mode(OKXTradeMode::Cash)
1744 .side(OKXSide::Buy)
1745 .ord_type(OKXOrderType::Market)
1746 .sz("0.01".to_string())
1747 .build()
1748 .unwrap();
1749
1750 let json = serde_json::to_string(¶ms).unwrap();
1751
1752 assert!(!json.contains("instIdCode"));
1754 assert!(json.contains("\"instId\":\"BTC-USDT\""));
1755 }
1756
1757 #[rstest]
1758 fn test_ws_cancel_order_params_with_inst_id_code() {
1759 use super::WsCancelOrderParamsBuilder;
1760
1761 let params = WsCancelOrderParamsBuilder::default()
1762 .inst_id(Ustr::from("ETH-USDT-SWAP"))
1763 .inst_id_code(10461u64)
1764 .ord_id("12345678".to_string())
1765 .build()
1766 .unwrap();
1767
1768 let json = serde_json::to_string(¶ms).unwrap();
1769
1770 assert!(json.contains("\"instIdCode\":10461"));
1771 assert!(json.contains("\"instId\":\"ETH-USDT-SWAP\""));
1772 assert!(json.contains("\"ordId\":\"12345678\""));
1773 }
1774
1775 #[rstest]
1776 fn test_ws_amend_order_params_with_inst_id_code() {
1777 use super::WsAmendOrderParamsBuilder;
1778
1779 let params = WsAmendOrderParamsBuilder::default()
1780 .inst_id(Ustr::from("BTC-USDT-SWAP"))
1781 .inst_id_code(10459u64)
1782 .cl_ord_id("client123".to_string())
1783 .new_px("51000".to_string())
1784 .build()
1785 .unwrap();
1786
1787 let json = serde_json::to_string(¶ms).unwrap();
1788
1789 assert!(json.contains("\"instIdCode\":10459"));
1790 assert!(json.contains("\"instId\":\"BTC-USDT-SWAP\""));
1791 assert!(json.contains("\"newPx\":\"51000\""));
1792 }
1793
1794 #[rstest]
1795 fn test_ws_post_algo_order_params_with_inst_id_code() {
1796 use super::WsPostAlgoOrderParamsBuilder;
1797 use crate::common::enums::{OKXAlgoOrderType, OKXSide, OKXTradeMode, OKXTriggerType};
1798
1799 let params = WsPostAlgoOrderParamsBuilder::default()
1800 .inst_id(Ustr::from("BTC-USDT-SWAP"))
1801 .inst_id_code(10459u64)
1802 .td_mode(OKXTradeMode::Cross)
1803 .side(OKXSide::Buy)
1804 .ord_type(OKXAlgoOrderType::Trigger)
1805 .sz("0.01".to_string())
1806 .trigger_px("48000".to_string())
1807 .trigger_px_type(OKXTriggerType::Last)
1808 .build()
1809 .unwrap();
1810
1811 let json = serde_json::to_string(¶ms).unwrap();
1812
1813 assert!(json.contains("\"instIdCode\":10459"));
1814 assert!(json.contains("\"instId\":\"BTC-USDT-SWAP\""));
1815 assert!(json.contains("\"triggerPx\":\"48000\""));
1816 }
1817
1818 #[rstest]
1819 fn test_ws_cancel_algo_order_params_with_inst_id_code() {
1820 let params = WsCancelAlgoOrderParams {
1822 inst_id: Ustr::from("BTC-USDT-SWAP"),
1823 inst_id_code: Some(10459),
1824 algo_id: Some("987654321".to_string()),
1825 algo_cl_ord_id: None,
1826 };
1827
1828 let json = serde_json::to_string(¶ms).unwrap();
1829
1830 assert!(json.contains("\"instIdCode\":10459"));
1831 assert!(json.contains("\"instId\":\"BTC-USDT-SWAP\""));
1832 assert!(json.contains("\"algoId\":\"987654321\""));
1833 }
1834}