1use derive_builder::Builder;
19use nautilus_model::{
20 data::{Data, FundingRateUpdate, InstrumentStatus, OrderBookDeltas},
21 events::{
22 AccountState, OrderAccepted, OrderCancelRejected, OrderCanceled, OrderExpired,
23 OrderModifyRejected, OrderRejected, OrderTriggered, OrderUpdated,
24 },
25 identifiers::ClientOrderId,
26 instruments::InstrumentAny,
27 reports::{FillReport, OrderStatusReport, PositionStatusReport},
28};
29use serde::{Deserialize, Serialize};
30use ustr::Ustr;
31
32use super::enums::{OKXWsChannel, OKXWsOperation};
33use crate::{
34 common::{
35 enums::{
36 OKXAlgoOrderType, OKXBookAction, OKXCandleConfirm, OKXExecType, OKXInstrumentType,
37 OKXOrderCategory, OKXOrderStatus, OKXOrderType, OKXPositionSide, OKXPriceType,
38 OKXQuickMarginType, OKXSelfTradePreventionMode, OKXSettlementState, OKXSide,
39 OKXTargetCurrency, OKXTradeMode, OKXTriggerType,
40 },
41 models::OKXInstrument,
42 parse::{
43 deserialize_empty_string_as_none, deserialize_empty_ustr_as_none,
44 deserialize_string_to_u64, deserialize_target_currency_as_none,
45 },
46 },
47 websocket::enums::OKXSubscriptionEvent,
48};
49
50#[derive(Debug, Clone)]
51pub enum NautilusWsMessage {
52 Data(Vec<Data>),
53 Deltas(OrderBookDeltas),
54 FundingRates(Vec<FundingRateUpdate>),
55 Instrument(Box<InstrumentAny>, Option<InstrumentStatus>),
56 InstrumentStatus(InstrumentStatus),
57 AccountUpdate(AccountState),
58 PositionUpdate(PositionStatusReport),
59 OrderAccepted(OrderAccepted),
60 OrderCanceled(OrderCanceled),
61 OrderExpired(OrderExpired),
62 OrderRejected(OrderRejected),
63 OrderCancelRejected(OrderCancelRejected),
64 OrderModifyRejected(OrderModifyRejected),
65 OrderTriggered(OrderTriggered),
66 OrderUpdated(OrderUpdated),
67 ExecutionReports(Vec<ExecutionReport>),
68 Error(OKXWebSocketError),
69 Raw(serde_json::Value), Reconnected,
71 Authenticated,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
76#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object))]
77#[cfg_attr(
78 feature = "python",
79 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.okx")
80)]
81pub struct OKXWebSocketError {
82 pub code: String,
84 pub message: String,
86 pub conn_id: Option<String>,
88 pub timestamp: u64,
90}
91
92#[derive(Debug, Clone)]
93#[expect(clippy::large_enum_variant)]
94pub enum ExecutionReport {
95 Order(OrderStatusReport),
96 Fill(FillReport),
97}
98
99#[derive(Debug)]
105pub enum OKXWsMessage {
106 BookData {
108 arg: OKXWebSocketArg,
109 action: OKXBookAction,
110 data: Vec<OKXBookMsg>,
111 },
112 ChannelData {
114 channel: OKXWsChannel,
115 inst_id: Option<Ustr>,
116 data: serde_json::Value,
117 },
118 OrderResponse {
120 id: Option<String>,
121 op: OKXWsOperation,
122 code: String,
123 msg: String,
124 data: Vec<serde_json::Value>,
125 },
126 Orders(Vec<OKXOrderMsg>),
128 AlgoOrders(Vec<OKXAlgoOrderMsg>),
130 Account(serde_json::Value),
132 Positions(serde_json::Value),
134 Instruments(Vec<OKXInstrument>),
136 SendFailed {
138 request_id: String,
139 client_order_id: Option<ClientOrderId>,
140 op: Option<OKXWsOperation>,
141 error: String,
142 },
143 Error(OKXWebSocketError),
145 Reconnected,
147 Authenticated,
149}
150
151#[derive(Debug, Serialize)]
153#[serde(rename_all = "camelCase")]
154pub struct OKXWsRequest<T> {
155 #[serde(skip_serializing_if = "Option::is_none")]
157 pub id: Option<String>,
158 pub op: OKXWsOperation,
160 #[serde(skip_serializing_if = "Option::is_none")]
163 pub exp_time: Option<String>,
164 pub args: Vec<T>,
166}
167
168#[derive(Debug, Serialize)]
170pub struct OKXAuthentication {
171 pub op: &'static str,
172 pub args: Vec<OKXAuthenticationArg>,
173}
174
175#[derive(Debug, Serialize)]
177#[serde(rename_all = "camelCase")]
178pub struct OKXAuthenticationArg {
179 pub api_key: String,
180 pub passphrase: String,
181 pub timestamp: String,
182 pub sign: String,
183}
184
185#[derive(Debug, Serialize)]
186pub struct OKXSubscription {
187 pub op: OKXWsOperation,
188 pub args: Vec<OKXSubscriptionArg>,
189}
190
191#[derive(Clone, Debug, Serialize)]
192#[serde(rename_all = "camelCase")]
193pub struct OKXSubscriptionArg {
194 pub channel: OKXWsChannel,
195 pub inst_type: Option<OKXInstrumentType>,
196 pub inst_family: Option<Ustr>,
197 pub inst_id: Option<Ustr>,
198}
199
200#[derive(Debug)]
205pub enum OKXWsFrame {
206 Login {
207 event: String,
208 code: String,
209 msg: String,
210 conn_id: String,
211 },
212 Subscription {
213 event: OKXSubscriptionEvent,
214 arg: OKXWebSocketArg,
215 conn_id: String,
216 code: Option<String>,
217 msg: Option<String>,
218 },
219 ChannelConnCount {
220 event: String,
221 channel: OKXWsChannel,
222 conn_count: String,
223 conn_id: String,
224 },
225 OrderResponse {
226 id: Option<String>,
227 op: OKXWsOperation,
228 code: String,
229 msg: String,
230 data: Vec<serde_json::Value>,
231 },
232 BookData {
233 arg: OKXWebSocketArg,
234 action: OKXBookAction,
235 data: Vec<OKXBookMsg>,
236 },
237 Data {
238 arg: OKXWebSocketArg,
239 data: serde_json::Value,
240 },
241 Error {
242 code: String,
243 msg: String,
244 },
245 Ping,
246 Reconnected,
247}
248
249impl<'de> Deserialize<'de> for OKXWsFrame {
250 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
251 where
252 D: serde::Deserializer<'de>,
253 {
254 use serde::de::Error;
255
256 let value = serde_json::Value::deserialize(deserializer)?;
258 let obj = value
259 .as_object()
260 .ok_or_else(|| D::Error::custom("expected JSON object for OKXWsFrame"))?;
261
262 if let Some(event) = obj.get("event").and_then(|v| v.as_str()) {
266 if event == "login" {
267 return parse_login(obj);
268 } else if event == "subscribe" || event == "unsubscribe" {
269 return parse_subscription(obj);
270 } else if event == "error" {
271 return parse_error(obj);
274 } else if obj.contains_key("channel") && obj.contains_key("connCount") {
275 return parse_channel_conn_count(obj);
276 }
277 }
278
279 if obj.contains_key("op") {
281 return parse_order_response(obj);
282 }
283
284 if obj.contains_key("action") && obj.contains_key("arg") {
286 return parse_book_data(obj);
287 }
288
289 if obj.contains_key("arg") && obj.contains_key("data") {
291 return parse_data(obj);
292 }
293
294 if obj.contains_key("code") && obj.contains_key("msg") {
296 return parse_error(obj);
297 }
298
299 Err(D::Error::custom(format!(
300 "cannot determine OKXWsFrame variant from: {}",
301 serde_json::to_string(&value).unwrap_or_default()
302 )))
303 }
304}
305
306fn parse_login<E: serde::de::Error>(
307 obj: &serde_json::Map<String, serde_json::Value>,
308) -> Result<OKXWsFrame, E> {
309 Ok(OKXWsFrame::Login {
310 event: obj
311 .get("event")
312 .and_then(|v| v.as_str())
313 .map(String::from)
314 .ok_or_else(|| E::missing_field("event"))?,
315 code: obj
316 .get("code")
317 .and_then(|v| v.as_str())
318 .map(String::from)
319 .ok_or_else(|| E::missing_field("code"))?,
320 msg: obj
321 .get("msg")
322 .and_then(|v| v.as_str())
323 .map(String::from)
324 .ok_or_else(|| E::missing_field("msg"))?,
325 conn_id: obj
326 .get("connId")
327 .and_then(|v| v.as_str())
328 .map(String::from)
329 .ok_or_else(|| E::missing_field("connId"))?,
330 })
331}
332
333fn parse_subscription<E: serde::de::Error>(
334 obj: &serde_json::Map<String, serde_json::Value>,
335) -> Result<OKXWsFrame, E> {
336 let event_str = obj
337 .get("event")
338 .and_then(|v| v.as_str())
339 .ok_or_else(|| E::missing_field("event"))?;
340
341 let event: OKXSubscriptionEvent =
342 serde_json::from_value(serde_json::Value::String(event_str.to_string()))
343 .map_err(|e| E::custom(format!("invalid event: {e}")))?;
344
345 let arg: OKXWebSocketArg = obj
346 .get("arg")
347 .cloned()
348 .map(serde_json::from_value)
349 .transpose()
350 .map_err(|e| E::custom(format!("invalid arg: {e}")))?
351 .ok_or_else(|| E::missing_field("arg"))?;
352
353 Ok(OKXWsFrame::Subscription {
354 event,
355 arg,
356 conn_id: obj
357 .get("connId")
358 .and_then(|v| v.as_str())
359 .map(String::from)
360 .ok_or_else(|| E::missing_field("connId"))?,
361 code: obj.get("code").and_then(|v| v.as_str()).map(String::from),
362 msg: obj.get("msg").and_then(|v| v.as_str()).map(String::from),
363 })
364}
365
366fn parse_channel_conn_count<E: serde::de::Error>(
367 obj: &serde_json::Map<String, serde_json::Value>,
368) -> Result<OKXWsFrame, E> {
369 let channel: OKXWsChannel = obj
370 .get("channel")
371 .cloned()
372 .map(serde_json::from_value)
373 .transpose()
374 .map_err(|e| E::custom(format!("invalid channel: {e}")))?
375 .ok_or_else(|| E::missing_field("channel"))?;
376
377 Ok(OKXWsFrame::ChannelConnCount {
378 event: obj
379 .get("event")
380 .and_then(|v| v.as_str())
381 .map(String::from)
382 .ok_or_else(|| E::missing_field("event"))?,
383 channel,
384 conn_count: obj
385 .get("connCount")
386 .and_then(|v| v.as_str())
387 .map(String::from)
388 .ok_or_else(|| E::missing_field("connCount"))?,
389 conn_id: obj
390 .get("connId")
391 .and_then(|v| v.as_str())
392 .map(String::from)
393 .ok_or_else(|| E::missing_field("connId"))?,
394 })
395}
396
397fn parse_order_response<E: serde::de::Error>(
398 obj: &serde_json::Map<String, serde_json::Value>,
399) -> Result<OKXWsFrame, E> {
400 let op: OKXWsOperation = obj
401 .get("op")
402 .cloned()
403 .map(serde_json::from_value)
404 .transpose()
405 .map_err(|e| E::custom(format!("invalid op: {e}")))?
406 .ok_or_else(|| E::missing_field("op"))?;
407
408 let data: Vec<serde_json::Value> = obj
409 .get("data")
410 .cloned()
411 .map(serde_json::from_value)
412 .transpose()
413 .map_err(|e| E::custom(format!("invalid data: {e}")))?
414 .unwrap_or_default();
415
416 Ok(OKXWsFrame::OrderResponse {
417 id: obj.get("id").and_then(|v| v.as_str()).map(String::from),
418 op,
419 code: obj
420 .get("code")
421 .and_then(|v| v.as_str())
422 .map(String::from)
423 .ok_or_else(|| E::missing_field("code"))?,
424 msg: obj
425 .get("msg")
426 .and_then(|v| v.as_str())
427 .map(String::from)
428 .ok_or_else(|| E::missing_field("msg"))?,
429 data,
430 })
431}
432
433fn parse_book_data<E: serde::de::Error>(
434 obj: &serde_json::Map<String, serde_json::Value>,
435) -> Result<OKXWsFrame, E> {
436 let arg: OKXWebSocketArg = obj
437 .get("arg")
438 .cloned()
439 .map(serde_json::from_value)
440 .transpose()
441 .map_err(|e| E::custom(format!("invalid arg: {e}")))?
442 .ok_or_else(|| E::missing_field("arg"))?;
443
444 let action: OKXBookAction = obj
445 .get("action")
446 .cloned()
447 .map(serde_json::from_value)
448 .transpose()
449 .map_err(|e| E::custom(format!("invalid action: {e}")))?
450 .ok_or_else(|| E::missing_field("action"))?;
451
452 let data: Vec<OKXBookMsg> = obj
453 .get("data")
454 .cloned()
455 .map(serde_json::from_value)
456 .transpose()
457 .map_err(|e| E::custom(format!("invalid data: {e}")))?
458 .ok_or_else(|| E::missing_field("data"))?;
459
460 Ok(OKXWsFrame::BookData { arg, action, data })
461}
462
463fn parse_data<E: serde::de::Error>(
464 obj: &serde_json::Map<String, serde_json::Value>,
465) -> Result<OKXWsFrame, E> {
466 let arg: OKXWebSocketArg = obj
467 .get("arg")
468 .cloned()
469 .map(serde_json::from_value)
470 .transpose()
471 .map_err(|e| E::custom(format!("invalid arg: {e}")))?
472 .ok_or_else(|| E::missing_field("arg"))?;
473
474 let data = obj
475 .get("data")
476 .cloned()
477 .ok_or_else(|| E::missing_field("data"))?;
478
479 Ok(OKXWsFrame::Data { arg, data })
480}
481
482fn parse_error<E: serde::de::Error>(
483 obj: &serde_json::Map<String, serde_json::Value>,
484) -> Result<OKXWsFrame, E> {
485 Ok(OKXWsFrame::Error {
486 code: obj
487 .get("code")
488 .and_then(|v| v.as_str())
489 .map(String::from)
490 .ok_or_else(|| E::missing_field("code"))?,
491 msg: obj
492 .get("msg")
493 .and_then(|v| v.as_str())
494 .map(String::from)
495 .ok_or_else(|| E::missing_field("msg"))?,
496 })
497}
498
499#[derive(Debug, Serialize, Deserialize)]
500#[serde(rename_all = "camelCase")]
501pub struct OKXWebSocketArg {
502 pub channel: OKXWsChannel,
504 #[serde(default)]
505 pub inst_id: Option<Ustr>,
506 #[serde(default)]
507 pub inst_type: Option<OKXInstrumentType>,
508 #[serde(default)]
509 pub inst_family: Option<Ustr>,
510 #[serde(default)]
511 pub bar: Option<Ustr>,
512}
513
514#[derive(Debug, Serialize, Deserialize)]
516#[serde(rename_all = "camelCase")]
517pub struct OKXTickerMsg {
518 pub inst_type: OKXInstrumentType,
520 pub inst_id: Ustr,
522 #[serde(rename = "last")]
524 pub last_px: String,
525 pub last_sz: String,
527 pub ask_px: String,
529 pub ask_sz: String,
531 pub bid_px: String,
533 pub bid_sz: String,
535 pub open24h: String,
537 pub high24h: String,
539 pub low24h: String,
541 pub vol_ccy_24h: String,
543 pub vol24h: String,
545 pub sod_utc0: String,
547 pub sod_utc8: String,
549 #[serde(deserialize_with = "deserialize_string_to_u64")]
551 pub ts: u64,
552 #[serde(default)]
554 pub source: Option<String>,
555}
556
557#[derive(Debug, Serialize, Deserialize)]
559pub struct OrderBookEntry {
560 pub price: String,
562 pub size: String,
564 pub liquidated_orders_count: String,
566 pub orders_count: String,
568}
569
570#[derive(Debug, Serialize, Deserialize)]
572#[serde(rename_all = "camelCase")]
573pub struct OKXBookMsg {
574 pub asks: Vec<OrderBookEntry>,
576 pub bids: Vec<OrderBookEntry>,
578 pub checksum: Option<i64>,
580 pub prev_seq_id: Option<i64>,
582 pub seq_id: u64,
584 #[serde(deserialize_with = "deserialize_string_to_u64")]
586 pub ts: u64,
587}
588
589#[derive(Debug, Serialize, Deserialize)]
591#[serde(rename_all = "camelCase")]
592pub struct OKXTradeMsg {
593 pub inst_id: Ustr,
595 pub trade_id: String,
597 pub px: String,
599 pub sz: String,
601 pub side: OKXSide,
603 pub count: String,
605 #[serde(deserialize_with = "deserialize_string_to_u64")]
607 pub ts: u64,
608 #[serde(default)]
610 pub source: Option<String>,
611 #[serde(default)]
613 pub seq_id: Option<u64>,
614}
615
616#[derive(Debug, Serialize, Deserialize)]
618#[serde(rename_all = "camelCase")]
619pub struct OKXFundingRateMsg {
620 #[serde(default)]
622 pub inst_type: Option<OKXInstrumentType>,
623 pub inst_id: Ustr,
625 pub funding_rate: Ustr,
627 pub next_funding_rate: Ustr,
629 #[serde(default)]
631 pub min_funding_rate: Option<String>,
632 #[serde(default)]
634 pub max_funding_rate: Option<String>,
635 #[serde(default)]
637 pub sett_state: OKXSettlementState,
638 #[serde(default)]
640 pub sett_funding_rate: Option<String>,
641 #[serde(default)]
643 pub premium: Option<String>,
644 #[serde(default)]
646 pub method: Option<String>,
647 #[serde(deserialize_with = "deserialize_string_to_u64")]
649 pub funding_time: u64,
650 #[serde(deserialize_with = "deserialize_string_to_u64")]
652 pub next_funding_time: u64,
653 #[serde(deserialize_with = "deserialize_string_to_u64")]
655 pub ts: u64,
656}
657
658#[derive(Debug, Serialize, Deserialize)]
660#[serde(rename_all = "camelCase")]
661pub struct OKXMarkPriceMsg {
662 pub inst_id: Ustr,
664 pub mark_px: String,
666 #[serde(deserialize_with = "deserialize_string_to_u64")]
668 pub ts: u64,
669}
670
671#[derive(Debug, Serialize, Deserialize)]
673#[serde(rename_all = "camelCase")]
674pub struct OKXIndexPriceMsg {
675 pub inst_id: Ustr,
677 pub idx_px: String,
679 pub high24h: String,
681 pub low24h: String,
683 pub open24h: String,
685 pub sod_utc0: String,
687 pub sod_utc8: 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 OKXPriceLimitMsg {
698 pub inst_id: Ustr,
700 pub buy_lmt: String,
702 pub sell_lmt: String,
704 #[serde(deserialize_with = "deserialize_string_to_u64")]
706 pub ts: u64,
707}
708
709#[derive(Debug, Serialize, Deserialize)]
711#[serde(rename_all = "camelCase")]
712pub struct OKXCandleMsg {
713 #[serde(deserialize_with = "deserialize_string_to_u64")]
715 pub ts: u64,
716 pub o: String,
718 pub h: String,
720 pub l: String,
722 pub c: String,
724 pub vol: String,
726 pub vol_ccy: String,
728 pub vol_ccy_quote: String,
729 pub confirm: OKXCandleConfirm,
731}
732
733#[derive(Debug, Serialize, Deserialize)]
735#[serde(rename_all = "camelCase")]
736pub struct OKXOpenInterestMsg {
737 pub inst_id: Ustr,
739 pub oi: String,
741 pub oi_ccy: String,
743 #[serde(deserialize_with = "deserialize_string_to_u64")]
745 pub ts: u64,
746}
747
748#[derive(Debug, Serialize, Deserialize)]
750#[serde(rename_all = "camelCase")]
751pub struct OKXOptionSummaryMsg {
752 #[serde(default)]
754 pub inst_type: Option<OKXInstrumentType>,
755 pub inst_id: Ustr,
757 pub uly: String,
759 pub delta: String,
761 pub gamma: String,
763 pub theta: String,
765 pub vega: String,
767 #[serde(alias = "deltaBS")]
769 pub delta_bs: String,
770 #[serde(alias = "gammaBS")]
772 pub gamma_bs: String,
773 #[serde(alias = "thetaBS")]
775 pub theta_bs: String,
776 #[serde(alias = "vegaBS")]
778 pub vega_bs: String,
779 pub real_vol: String,
781 pub bid_vol: String,
783 pub ask_vol: String,
785 pub mark_vol: String,
787 pub lever: String,
789 #[serde(default)]
791 pub fwd_px: Option<String>,
792 #[serde(default)]
794 pub mark_px: Option<String>,
795 #[serde(default)]
797 pub vol_lv: Option<String>,
798 #[serde(deserialize_with = "deserialize_string_to_u64")]
800 pub ts: u64,
801}
802
803#[derive(Debug, Serialize, Deserialize)]
805#[serde(rename_all = "camelCase")]
806pub struct OKXEstimatedPriceMsg {
807 pub inst_id: Ustr,
809 pub settle_px: String,
811 #[serde(deserialize_with = "deserialize_string_to_u64")]
813 pub ts: u64,
814}
815
816#[derive(Debug, Serialize, Deserialize)]
818#[serde(rename_all = "camelCase")]
819pub struct OKXStatusMsg {
820 pub title: Ustr,
822 #[serde(rename = "type")]
824 pub status_type: Ustr,
825 pub state: Ustr,
827 pub end_time: Option<String>,
829 pub begin_time: Option<String>,
831 pub service_type: Option<Ustr>,
833 pub reason: Option<String>,
835 #[serde(deserialize_with = "deserialize_string_to_u64")]
837 pub ts: u64,
838}
839
840pub use crate::common::models::OKXAttachedAlgoOrd;
841
842#[derive(Clone, Debug, Default, Serialize, Deserialize)]
844#[serde(rename_all = "camelCase")]
845pub struct OKXLinkedAlgoOrd {
846 #[serde(default)]
848 pub algo_id: String,
849}
850
851#[derive(Clone, Debug, Serialize, Deserialize)]
853#[serde(rename_all = "camelCase")]
854pub struct OKXOrderMsg {
855 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
857 pub acc_fill_sz: Option<String>,
858 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
860 pub algo_id: Option<String>,
861 pub avg_px: String,
863 #[serde(deserialize_with = "deserialize_string_to_u64")]
865 pub c_time: u64,
866 #[serde(default)]
868 pub cancel_source: Option<String>,
869 #[serde(default)]
871 pub cancel_source_reason: Option<String>,
872 pub category: OKXOrderCategory,
874 pub ccy: Ustr,
876 pub cl_ord_id: String,
878 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
880 pub algo_cl_ord_id: Option<String>,
881 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
883 pub attach_algo_cl_ord_id: Option<String>,
884 #[serde(default)]
886 pub attach_algo_ords: Vec<OKXAttachedAlgoOrd>,
887 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
889 pub fee: Option<String>,
890 pub fee_ccy: Ustr,
892 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
894 pub fill_fee: Option<String>,
895 #[serde(default, deserialize_with = "deserialize_empty_ustr_as_none")]
897 pub fill_fee_ccy: Option<Ustr>,
898 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
900 pub fill_mark_px: Option<String>,
901 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
903 pub fill_mark_vol: Option<String>,
904 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
906 pub fill_px_vol: Option<String>,
907 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
909 pub fill_px_usd: Option<String>,
910 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
912 pub fill_fwd_px: Option<String>,
913 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
915 pub fill_notional_usd: Option<String>,
916 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
918 pub fill_pnl: Option<String>,
919 pub fill_px: String,
921 pub fill_sz: String,
923 #[serde(deserialize_with = "deserialize_string_to_u64")]
925 pub fill_time: u64,
926 pub inst_id: Ustr,
928 pub inst_type: OKXInstrumentType,
930 #[serde(default)]
932 pub is_tp_limit: Option<String>,
933 pub lever: String,
935 #[serde(default)]
937 pub linked_algo_ord: Option<OKXLinkedAlgoOrd>,
938 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
940 pub notional_usd: Option<String>,
941 pub ord_id: Ustr,
943 pub ord_type: OKXOrderType,
945 pub pnl: String,
947 pub pos_side: OKXPositionSide,
949 #[serde(default)]
951 pub px: String,
952 #[serde(default)]
954 pub px_type: OKXPriceType,
955 #[serde(default)]
957 pub px_usd: Option<String>,
958 #[serde(default)]
960 pub px_vol: Option<String>,
961 #[serde(default)]
963 pub quick_mgn_type: OKXQuickMarginType,
964 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
966 pub rebate: Option<String>,
967 #[serde(default, deserialize_with = "deserialize_empty_ustr_as_none")]
969 pub rebate_ccy: Option<Ustr>,
970 pub reduce_only: String,
972 pub side: OKXSide,
974 #[serde(default)]
976 pub sl_ord_px: Option<String>,
977 #[serde(default)]
979 pub sl_trigger_px: Option<String>,
980 #[serde(default)]
982 pub sl_trigger_px_type: Option<OKXTriggerType>,
983 #[serde(default)]
985 pub source: Option<String>,
986 pub state: OKXOrderStatus,
988 #[serde(default)]
990 pub stp_id: Option<String>,
991 #[serde(default)]
993 pub stp_mode: OKXSelfTradePreventionMode,
994 pub exec_type: OKXExecType,
996 pub sz: String,
998 #[serde(default)]
1000 pub tag: Option<String>,
1001 pub td_mode: OKXTradeMode,
1003 #[serde(default, deserialize_with = "deserialize_target_currency_as_none")]
1005 pub tgt_ccy: Option<OKXTargetCurrency>,
1006 #[serde(default)]
1008 pub tp_ord_px: Option<String>,
1009 #[serde(default)]
1011 pub tp_trigger_px: Option<String>,
1012 #[serde(default)]
1014 pub tp_trigger_px_type: Option<OKXTriggerType>,
1015 pub trade_id: String,
1017 #[serde(deserialize_with = "deserialize_string_to_u64")]
1019 pub u_time: u64,
1020 #[serde(default)]
1022 pub amend_result: Option<String>,
1023 #[serde(default)]
1025 pub req_id: Option<String>,
1026 #[serde(default)]
1028 pub code: Option<String>,
1029 #[serde(default)]
1031 pub msg: Option<String>,
1032}
1033
1034#[derive(Clone, Debug, Deserialize, Serialize)]
1036#[serde(rename_all = "camelCase")]
1037pub struct OKXAlgoOrderMsg {
1038 pub algo_id: String,
1040 #[serde(default)]
1042 pub algo_cl_ord_id: String,
1043 pub cl_ord_id: String,
1045 pub ord_id: String,
1047 pub inst_id: Ustr,
1049 pub inst_type: OKXInstrumentType,
1051 pub ord_type: OKXAlgoOrderType,
1053 pub state: OKXOrderStatus,
1055 pub side: OKXSide,
1057 pub pos_side: OKXPositionSide,
1059 #[serde(default)]
1061 pub sz: String,
1062 #[serde(default)]
1064 pub trigger_px: String,
1065 #[serde(default)]
1067 pub trigger_px_type: OKXTriggerType,
1068 #[serde(default)]
1070 pub sl_trigger_px: String,
1071 #[serde(default)]
1073 pub sl_ord_px: String,
1074 #[serde(default)]
1076 pub sl_trigger_px_type: OKXTriggerType,
1077 #[serde(default)]
1079 pub tp_trigger_px: String,
1080 #[serde(default)]
1082 pub tp_ord_px: String,
1083 #[serde(default)]
1085 pub tp_trigger_px_type: OKXTriggerType,
1086 #[serde(default)]
1088 pub ord_px: String,
1089 pub td_mode: OKXTradeMode,
1091 pub lever: String,
1093 #[serde(default)]
1095 pub reduce_only: String,
1096 #[serde(default)]
1098 pub close_fraction: String,
1099 #[serde(default)]
1101 pub actual_px: String,
1102 #[serde(default)]
1104 pub actual_sz: String,
1105 #[serde(default)]
1107 pub notional_usd: String,
1108 #[serde(deserialize_with = "deserialize_string_to_u64")]
1110 pub c_time: u64,
1111 #[serde(deserialize_with = "deserialize_string_to_u64")]
1113 pub u_time: u64,
1114 #[serde(default)]
1116 pub trigger_time: String,
1117 #[serde(default)]
1119 pub tag: String,
1120 #[serde(default)]
1122 pub callback_ratio: String,
1123 #[serde(default)]
1125 pub callback_spread: String,
1126 #[serde(default)]
1128 pub active_px: String,
1129 #[serde(default, deserialize_with = "deserialize_empty_ustr_as_none")]
1131 pub ccy: Option<Ustr>,
1132 #[serde(default, deserialize_with = "deserialize_target_currency_as_none")]
1134 pub tgt_ccy: Option<OKXTargetCurrency>,
1135 #[serde(default)]
1137 pub fee: Option<String>,
1138 #[serde(default, deserialize_with = "deserialize_empty_ustr_as_none")]
1140 pub fee_ccy: Option<Ustr>,
1141 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
1143 pub advance_ord_type: Option<String>,
1144}
1145
1146#[derive(Clone, Debug, Default, Deserialize, Serialize, Builder)]
1148#[builder(default)]
1149#[builder(setter(into, strip_option))]
1150#[serde(rename_all = "camelCase")]
1151pub struct WsAttachAlgoOrdParams {
1152 #[serde(skip_serializing_if = "Option::is_none")]
1154 pub attach_algo_cl_ord_id: Option<String>,
1155 #[serde(skip_serializing_if = "Option::is_none")]
1157 pub sl_trigger_px: Option<String>,
1158 #[serde(skip_serializing_if = "Option::is_none")]
1160 pub sl_ord_px: Option<String>,
1161 #[serde(skip_serializing_if = "Option::is_none")]
1163 pub sl_trigger_px_type: Option<OKXTriggerType>,
1164 #[serde(skip_serializing_if = "Option::is_none")]
1166 pub tp_trigger_px: Option<String>,
1167 #[serde(skip_serializing_if = "Option::is_none")]
1169 pub tp_ord_px: Option<String>,
1170 #[serde(skip_serializing_if = "Option::is_none")]
1172 pub tp_trigger_px_type: Option<OKXTriggerType>,
1173}
1174
1175#[derive(Clone, Debug, Deserialize, Serialize, Builder)]
1177#[builder(setter(into, strip_option))]
1178#[serde(rename_all = "camelCase")]
1179pub struct WsPostOrderParams {
1180 #[builder(default)]
1182 #[serde(skip_serializing_if = "Option::is_none")]
1183 pub inst_type: Option<OKXInstrumentType>,
1184 pub inst_id_code: u64,
1186 pub td_mode: OKXTradeMode,
1188 #[builder(default)]
1190 #[serde(skip_serializing_if = "Option::is_none")]
1191 pub ccy: Option<Ustr>,
1192 #[builder(default)]
1194 #[serde(skip_serializing_if = "Option::is_none")]
1195 pub cl_ord_id: Option<String>,
1196 pub side: OKXSide,
1198 #[builder(default)]
1200 #[serde(skip_serializing_if = "Option::is_none")]
1201 pub pos_side: Option<OKXPositionSide>,
1202 pub ord_type: OKXOrderType,
1204 pub sz: String,
1206 #[builder(default)]
1208 #[serde(skip_serializing_if = "Option::is_none")]
1209 pub px: Option<String>,
1210 #[builder(default)]
1212 #[serde(rename = "pxUsd", skip_serializing_if = "Option::is_none")]
1213 pub px_usd: Option<String>,
1214 #[builder(default)]
1217 #[serde(rename = "pxVol", skip_serializing_if = "Option::is_none")]
1218 pub px_vol: Option<String>,
1219 #[builder(default)]
1221 #[serde(skip_serializing_if = "Option::is_none")]
1222 pub reduce_only: Option<bool>,
1223 #[builder(default)]
1225 #[serde(rename = "closePosition", skip_serializing_if = "Option::is_none")]
1226 pub close_position: Option<bool>,
1227 #[builder(default)]
1229 #[serde(skip_serializing_if = "Option::is_none")]
1230 pub tgt_ccy: Option<OKXTargetCurrency>,
1231 #[builder(default)]
1233 #[serde(skip_serializing_if = "Option::is_none")]
1234 pub tag: Option<String>,
1235 #[builder(default)]
1237 #[serde(skip_serializing_if = "Option::is_none")]
1238 pub attach_algo_ords: Option<Vec<WsAttachAlgoOrdParams>>,
1239}
1240
1241#[derive(Clone, Debug, Default, Deserialize, Serialize, Builder)]
1243#[builder(default)]
1244#[builder(setter(into, strip_option))]
1245#[serde(rename_all = "camelCase")]
1246pub struct WsCancelOrderParams {
1247 pub inst_id_code: u64,
1249 #[serde(skip_serializing_if = "Option::is_none")]
1251 pub ord_id: Option<String>,
1252 #[serde(skip_serializing_if = "Option::is_none")]
1254 pub cl_ord_id: Option<String>,
1255}
1256
1257#[derive(Clone, Debug, Default, Deserialize, Serialize, Builder)]
1259#[builder(default)]
1260#[builder(setter(into, strip_option))]
1261#[serde(rename_all = "camelCase")]
1262pub struct WsMassCancelParams {
1263 pub inst_type: OKXInstrumentType,
1265 pub inst_family: Ustr,
1267}
1268
1269#[derive(Clone, Debug, Default, Deserialize, Serialize, Builder)]
1271#[builder(default)]
1272#[builder(setter(into, strip_option))]
1273#[serde(rename_all = "camelCase")]
1274pub struct WsAmendOrderParams {
1275 pub inst_id_code: u64,
1277 #[serde(skip_serializing_if = "Option::is_none")]
1279 pub ord_id: Option<String>,
1280 #[serde(skip_serializing_if = "Option::is_none")]
1282 pub cl_ord_id: Option<String>,
1283 #[serde(skip_serializing_if = "Option::is_none")]
1285 pub new_cl_ord_id: Option<String>,
1286 #[serde(skip_serializing_if = "Option::is_none")]
1288 pub new_px: Option<String>,
1289 #[serde(rename = "newPxUsd", skip_serializing_if = "Option::is_none")]
1291 pub new_px_usd: Option<String>,
1292 #[serde(rename = "newPxVol", skip_serializing_if = "Option::is_none")]
1295 pub new_px_vol: Option<String>,
1296 #[serde(skip_serializing_if = "Option::is_none")]
1298 pub new_sz: Option<String>,
1299}
1300
1301#[derive(Clone, Debug, Deserialize, Serialize, Builder)]
1303#[builder(setter(into, strip_option))]
1304#[serde(rename_all = "camelCase")]
1305pub struct WsPostAlgoOrderParams {
1306 pub inst_id_code: u64,
1308 pub td_mode: OKXTradeMode,
1310 pub side: OKXSide,
1312 pub ord_type: OKXAlgoOrderType,
1314 pub sz: String,
1316 #[builder(default)]
1318 #[serde(skip_serializing_if = "Option::is_none")]
1319 pub cl_ord_id: Option<String>,
1320 #[builder(default)]
1322 #[serde(skip_serializing_if = "Option::is_none")]
1323 pub pos_side: Option<OKXPositionSide>,
1324 #[serde(skip_serializing_if = "Option::is_none")]
1326 pub trigger_px: Option<String>,
1327 #[builder(default)]
1329 #[serde(skip_serializing_if = "Option::is_none")]
1330 pub trigger_px_type: Option<OKXTriggerType>,
1331 #[builder(default)]
1333 #[serde(skip_serializing_if = "Option::is_none")]
1334 pub order_px: Option<String>,
1335 #[builder(default)]
1337 #[serde(skip_serializing_if = "Option::is_none")]
1338 pub reduce_only: Option<bool>,
1339 #[builder(default)]
1341 #[serde(skip_serializing_if = "Option::is_none")]
1342 pub tag: Option<String>,
1343 #[builder(default)]
1345 #[serde(skip_serializing_if = "Option::is_none")]
1346 pub callback_ratio: Option<String>,
1347 #[builder(default)]
1349 #[serde(skip_serializing_if = "Option::is_none")]
1350 pub callback_spread: Option<String>,
1351 #[builder(default)]
1353 #[serde(skip_serializing_if = "Option::is_none")]
1354 pub active_px: Option<String>,
1355}
1356
1357#[derive(Clone, Debug, Deserialize, Serialize, Builder)]
1359#[builder(setter(into, strip_option))]
1360#[serde(rename_all = "camelCase")]
1361pub struct WsCancelAlgoOrderParams {
1362 pub inst_id_code: u64,
1364 #[serde(skip_serializing_if = "Option::is_none")]
1366 pub algo_id: Option<String>,
1367 #[serde(skip_serializing_if = "Option::is_none")]
1369 pub algo_cl_ord_id: Option<String>,
1370}
1371
1372#[cfg(test)]
1373mod tests {
1374 use nautilus_core::time::get_atomic_clock_realtime;
1375 use rstest::rstest;
1376
1377 use super::*;
1378
1379 #[rstest]
1380 fn test_deserialize_websocket_arg() {
1381 let json_str = r#"{"channel":"instruments","instType":"SPOT"}"#;
1382
1383 let result: Result<OKXWebSocketArg, _> = serde_json::from_str(json_str);
1384 match result {
1385 Ok(arg) => {
1386 assert_eq!(arg.channel, OKXWsChannel::Instruments);
1387 assert_eq!(arg.inst_type, Some(OKXInstrumentType::Spot));
1388 assert_eq!(arg.inst_id, None);
1389 }
1390 Err(e) => {
1391 panic!("Failed to deserialize WebSocket arg: {e}");
1392 }
1393 }
1394 }
1395
1396 #[rstest]
1397 fn test_deserialize_subscribe_variant_direct() {
1398 #[derive(Debug, Deserialize)]
1399 #[serde(rename_all = "camelCase")]
1400 struct SubscribeMsg {
1401 event: String,
1402 arg: OKXWebSocketArg,
1403 conn_id: String,
1404 }
1405
1406 let json_str = r#"{"event":"subscribe","arg":{"channel":"instruments","instType":"SPOT"},"connId":"380cfa6a"}"#;
1407
1408 let result: Result<SubscribeMsg, _> = serde_json::from_str(json_str);
1409 match result {
1410 Ok(msg) => {
1411 assert_eq!(msg.event, "subscribe");
1412 assert_eq!(msg.arg.channel, OKXWsChannel::Instruments);
1413 assert_eq!(msg.conn_id, "380cfa6a");
1414 }
1415 Err(e) => {
1416 panic!("Failed to deserialize subscribe message directly: {e}");
1417 }
1418 }
1419 }
1420
1421 #[rstest]
1422 fn test_deserialize_subscribe_confirmation() {
1423 let json_str = r#"{"event":"subscribe","arg":{"channel":"instruments","instType":"SPOT"},"connId":"380cfa6a"}"#;
1424
1425 let result: Result<OKXWsFrame, _> = serde_json::from_str(json_str);
1426 match result {
1427 Ok(msg) => {
1428 if let OKXWsFrame::Subscription {
1429 event,
1430 arg,
1431 conn_id,
1432 ..
1433 } = msg
1434 {
1435 assert_eq!(event, OKXSubscriptionEvent::Subscribe);
1436 assert_eq!(arg.channel, OKXWsChannel::Instruments);
1437 assert_eq!(conn_id, "380cfa6a");
1438 } else {
1439 panic!("Expected Subscribe variant, was: {msg:?}");
1440 }
1441 }
1442 Err(e) => {
1443 panic!("Failed to deserialize subscription confirmation: {e}");
1444 }
1445 }
1446 }
1447
1448 #[rstest]
1449 fn test_deserialize_subscribe_with_inst_id() {
1450 let json_str = r#"{"event":"subscribe","arg":{"channel":"candle1m","instId":"ETH-USDT"},"connId":"358602f5"}"#;
1451
1452 let result: Result<OKXWsFrame, _> = serde_json::from_str(json_str);
1453 match result {
1454 Ok(msg) => {
1455 if let OKXWsFrame::Subscription {
1456 event,
1457 arg,
1458 conn_id,
1459 ..
1460 } = msg
1461 {
1462 assert_eq!(event, OKXSubscriptionEvent::Subscribe);
1463 assert_eq!(arg.channel, OKXWsChannel::Candle1Minute);
1464 assert_eq!(conn_id, "358602f5");
1465 } else {
1466 panic!("Expected Subscribe variant, was: {msg:?}");
1467 }
1468 }
1469 Err(e) => {
1470 panic!("Failed to deserialize subscription confirmation: {e}");
1471 }
1472 }
1473 }
1474
1475 #[rstest]
1476 fn test_channel_serialization_for_logging() {
1477 let channel = OKXWsChannel::Candle1Minute;
1478 let serialized = serde_json::to_string(&channel).unwrap();
1479 let cleaned = serialized.trim_matches('"').to_string();
1480 assert_eq!(cleaned, "candle1m");
1481
1482 let channel = OKXWsChannel::BboTbt;
1483 let serialized = serde_json::to_string(&channel).unwrap();
1484 let cleaned = serialized.trim_matches('"').to_string();
1485 assert_eq!(cleaned, "bbo-tbt");
1486
1487 let channel = OKXWsChannel::Trades;
1488 let serialized = serde_json::to_string(&channel).unwrap();
1489 let cleaned = serialized.trim_matches('"').to_string();
1490 assert_eq!(cleaned, "trades");
1491 }
1492
1493 #[rstest]
1494 fn test_order_response_with_enum_operation() {
1495 let json_str = r#"{"id":"req-123","op":"order","code":"0","msg":"","data":[]}"#;
1496 let result: Result<OKXWsFrame, _> = serde_json::from_str(json_str);
1497 match result {
1498 Ok(OKXWsFrame::OrderResponse {
1499 id,
1500 op,
1501 code,
1502 msg,
1503 data,
1504 }) => {
1505 assert_eq!(id, Some("req-123".to_string()));
1506 assert_eq!(op, OKXWsOperation::Order);
1507 assert_eq!(code, "0");
1508 assert_eq!(msg, "");
1509 assert!(data.is_empty());
1510 }
1511 Ok(other) => panic!("Expected OrderResponse, was: {other:?}"),
1512 Err(e) => panic!("Failed to deserialize: {e}"),
1513 }
1514
1515 let json_str = r#"{"id":"cancel-456","op":"cancel-order","code":"50001","msg":"Order not found","data":[]}"#;
1516 let result: Result<OKXWsFrame, _> = serde_json::from_str(json_str);
1517 match result {
1518 Ok(OKXWsFrame::OrderResponse {
1519 id,
1520 op,
1521 code,
1522 msg,
1523 data,
1524 }) => {
1525 assert_eq!(id, Some("cancel-456".to_string()));
1526 assert_eq!(op, OKXWsOperation::CancelOrder);
1527 assert_eq!(code, "50001");
1528 assert_eq!(msg, "Order not found");
1529 assert!(data.is_empty());
1530 }
1531 Ok(other) => panic!("Expected OrderResponse, was: {other:?}"),
1532 Err(e) => panic!("Failed to deserialize: {e}"),
1533 }
1534
1535 let json_str = r#"{"id":"amend-789","op":"amend-order","code":"50002","msg":"Invalid price","data":[]}"#;
1536 let result: Result<OKXWsFrame, _> = serde_json::from_str(json_str);
1537 match result {
1538 Ok(OKXWsFrame::OrderResponse {
1539 id,
1540 op,
1541 code,
1542 msg,
1543 data,
1544 }) => {
1545 assert_eq!(id, Some("amend-789".to_string()));
1546 assert_eq!(op, OKXWsOperation::AmendOrder);
1547 assert_eq!(code, "50002");
1548 assert_eq!(msg, "Invalid price");
1549 assert!(data.is_empty());
1550 }
1551 Ok(other) => panic!("Expected OrderResponse, was: {other:?}"),
1552 Err(e) => panic!("Failed to deserialize: {e}"),
1553 }
1554 }
1555
1556 #[rstest]
1557 fn test_operation_enum_serialization() {
1558 let op = OKXWsOperation::Order;
1559 let serialized = serde_json::to_string(&op).unwrap();
1560 assert_eq!(serialized, "\"order\"");
1561
1562 let op = OKXWsOperation::CancelOrder;
1563 let serialized = serde_json::to_string(&op).unwrap();
1564 assert_eq!(serialized, "\"cancel-order\"");
1565
1566 let op = OKXWsOperation::AmendOrder;
1567 let serialized = serde_json::to_string(&op).unwrap();
1568 assert_eq!(serialized, "\"amend-order\"");
1569
1570 let op = OKXWsOperation::Subscribe;
1571 let serialized = serde_json::to_string(&op).unwrap();
1572 assert_eq!(serialized, "\"subscribe\"");
1573 }
1574
1575 #[rstest]
1576 fn test_order_response_parsing() {
1577 let success_response = r#"{
1578 "id": "req-123",
1579 "op": "order",
1580 "code": "0",
1581 "msg": "",
1582 "data": [{"sMsg": "Order placed successfully"}]
1583 }"#;
1584
1585 let parsed: OKXWsFrame = serde_json::from_str(success_response).unwrap();
1586
1587 match parsed {
1588 OKXWsFrame::OrderResponse {
1589 id,
1590 op,
1591 code,
1592 msg,
1593 data,
1594 } => {
1595 assert_eq!(id, Some("req-123".to_string()));
1596 assert_eq!(op, OKXWsOperation::Order);
1597 assert_eq!(code, "0");
1598 assert_eq!(msg, "");
1599 assert_eq!(data.len(), 1);
1600 }
1601 _ => panic!("Expected OrderResponse variant"),
1602 }
1603
1604 let failure_response = r#"{
1605 "id": "req-456",
1606 "op": "cancel-order",
1607 "code": "50001",
1608 "msg": "Order not found",
1609 "data": [{"sMsg": "Order with client order ID not found"}]
1610 }"#;
1611
1612 let parsed: OKXWsFrame = serde_json::from_str(failure_response).unwrap();
1613
1614 match parsed {
1615 OKXWsFrame::OrderResponse {
1616 id,
1617 op,
1618 code,
1619 msg,
1620 data,
1621 } => {
1622 assert_eq!(id, Some("req-456".to_string()));
1623 assert_eq!(op, OKXWsOperation::CancelOrder);
1624 assert_eq!(code, "50001");
1625 assert_eq!(msg, "Order not found");
1626 assert_eq!(data.len(), 1);
1627 }
1628 _ => panic!("Expected OrderResponse variant"),
1629 }
1630 }
1631
1632 #[rstest]
1633 fn test_subscription_event_parsing() {
1634 let subscription_json = r#"{
1635 "event": "subscribe",
1636 "arg": {
1637 "channel": "tickers",
1638 "instId": "BTC-USDT"
1639 },
1640 "connId": "a4d3ae55"
1641 }"#;
1642
1643 let parsed: OKXWsFrame = serde_json::from_str(subscription_json).unwrap();
1644
1645 match parsed {
1646 OKXWsFrame::Subscription {
1647 event,
1648 arg,
1649 conn_id,
1650 ..
1651 } => {
1652 assert_eq!(
1653 event,
1654 crate::websocket::enums::OKXSubscriptionEvent::Subscribe
1655 );
1656 assert_eq!(arg.channel, OKXWsChannel::Tickers);
1657 assert_eq!(arg.inst_id, Some(Ustr::from("BTC-USDT")));
1658 assert_eq!(conn_id, "a4d3ae55");
1659 }
1660 _ => panic!("Expected Subscription variant"),
1661 }
1662 }
1663
1664 #[rstest]
1665 fn test_login_event_parsing() {
1666 let login_success = r#"{
1667 "event": "login",
1668 "code": "0",
1669 "msg": "Login successful",
1670 "connId": "a4d3ae55"
1671 }"#;
1672
1673 let parsed: OKXWsFrame = serde_json::from_str(login_success).unwrap();
1674
1675 match parsed {
1676 OKXWsFrame::Login {
1677 event,
1678 code,
1679 msg,
1680 conn_id,
1681 } => {
1682 assert_eq!(event, "login");
1683 assert_eq!(code, "0");
1684 assert_eq!(msg, "Login successful");
1685 assert_eq!(conn_id, "a4d3ae55");
1686 }
1687 _ => panic!("Expected Login variant, was: {parsed:?}"),
1688 }
1689 }
1690
1691 #[rstest]
1692 fn test_error_event_parsing() {
1693 let error_json = r#"{
1694 "code": "60012",
1695 "msg": "Invalid request"
1696 }"#;
1697
1698 let parsed: OKXWsFrame = serde_json::from_str(error_json).unwrap();
1699
1700 match parsed {
1701 OKXWsFrame::Error { code, msg } => {
1702 assert_eq!(code, "60012");
1703 assert_eq!(msg, "Invalid request");
1704 }
1705 _ => panic!("Expected Error variant"),
1706 }
1707 }
1708
1709 #[rstest]
1710 fn test_error_event_with_event_field_parsing() {
1711 let error_json = r#"{
1713 "event": "error",
1714 "code": "60018",
1715 "msg": "Invalid sign"
1716 }"#;
1717
1718 let parsed: OKXWsFrame = serde_json::from_str(error_json).unwrap();
1719
1720 match parsed {
1721 OKXWsFrame::Error { code, msg } => {
1722 assert_eq!(code, "60018");
1723 assert_eq!(msg, "Invalid sign");
1724 }
1725 _ => panic!("Expected Error variant, was: {parsed:?}"),
1726 }
1727 }
1728
1729 #[rstest]
1730 fn test_subscription_error_with_arg_field_parsing() {
1731 let error_json = r#"{
1733 "event": "error",
1734 "arg": {"channel": "tickers", "instId": "INVALID-INST"},
1735 "code": "60012",
1736 "msg": "Invalid request: channel not found",
1737 "connId": "a4d3ae55"
1738 }"#;
1739
1740 let parsed: OKXWsFrame = serde_json::from_str(error_json).unwrap();
1741
1742 match parsed {
1743 OKXWsFrame::Error { code, msg } => {
1744 assert_eq!(code, "60012");
1745 assert_eq!(msg, "Invalid request: channel not found");
1746 }
1747 _ => panic!("Expected Error variant, was: {parsed:?}"),
1748 }
1749 }
1750
1751 #[rstest]
1752 fn test_websocket_request_serialization() {
1753 let request = OKXWsRequest {
1754 id: Some("req-123".to_string()),
1755 op: OKXWsOperation::Order,
1756 args: vec![serde_json::json!({
1757 "instId": "BTC-USDT",
1758 "tdMode": "cash",
1759 "side": "buy",
1760 "ordType": "market",
1761 "sz": "0.1"
1762 })],
1763 exp_time: None,
1764 };
1765
1766 let serialized = serde_json::to_string(&request).unwrap();
1767 let parsed: serde_json::Value = serde_json::from_str(&serialized).unwrap();
1768
1769 assert_eq!(parsed["id"], "req-123");
1770 assert_eq!(parsed["op"], "order");
1771 assert!(parsed["args"].is_array());
1772 assert_eq!(parsed["args"].as_array().unwrap().len(), 1);
1773 }
1774
1775 #[rstest]
1776 fn test_subscription_request_serialization() {
1777 let subscription = OKXSubscription {
1778 op: OKXWsOperation::Subscribe,
1779 args: vec![OKXSubscriptionArg {
1780 channel: OKXWsChannel::Tickers,
1781 inst_type: Some(OKXInstrumentType::Spot),
1782 inst_family: None,
1783 inst_id: Some(Ustr::from("BTC-USDT")),
1784 }],
1785 };
1786
1787 let serialized = serde_json::to_string(&subscription).unwrap();
1788 let parsed: serde_json::Value = serde_json::from_str(&serialized).unwrap();
1789
1790 assert_eq!(parsed["op"], "subscribe");
1791 assert!(parsed["args"].is_array());
1792 assert_eq!(parsed["args"][0]["channel"], "tickers");
1793 assert_eq!(parsed["args"][0]["instType"], "SPOT");
1794 assert_eq!(parsed["args"][0]["instId"], "BTC-USDT");
1795 }
1796
1797 #[rstest]
1798 fn test_error_message_extraction() {
1799 let responses = vec![
1800 (
1801 r#"{
1802 "id": "req-123",
1803 "op": "order",
1804 "code": "50001",
1805 "msg": "Order failed",
1806 "data": [{"sMsg": "Insufficient balance"}]
1807 }"#,
1808 "Insufficient balance",
1809 ),
1810 (
1811 r#"{
1812 "id": "req-456",
1813 "op": "cancel-order",
1814 "code": "50002",
1815 "msg": "Cancel failed",
1816 "data": [{}]
1817 }"#,
1818 "Cancel failed",
1819 ),
1820 ];
1821
1822 for (response_json, expected_msg) in responses {
1823 let parsed: OKXWsFrame = serde_json::from_str(response_json).unwrap();
1824
1825 match parsed {
1826 OKXWsFrame::OrderResponse {
1827 id: _,
1828 op: _,
1829 code,
1830 msg,
1831 data,
1832 } => {
1833 assert_ne!(code, "0"); let error_msg = data
1837 .first()
1838 .and_then(|d| d.get("sMsg"))
1839 .and_then(|s| s.as_str())
1840 .filter(|s| !s.is_empty())
1841 .unwrap_or(&msg);
1842
1843 assert_eq!(error_msg, expected_msg);
1844 }
1845 _ => panic!("Expected OrderResponse variant"),
1846 }
1847 }
1848 }
1849
1850 #[rstest]
1851 fn test_book_data_parsing() {
1852 let book_data_json = r#"{
1853 "arg": {
1854 "channel": "books",
1855 "instId": "BTC-USDT"
1856 },
1857 "action": "snapshot",
1858 "data": [{
1859 "asks": [["50000.0", "0.1", "0", "1"]],
1860 "bids": [["49999.0", "0.2", "0", "1"]],
1861 "ts": "1640995200000",
1862 "checksum": 123456789,
1863 "seqId": 1000
1864 }]
1865 }"#;
1866
1867 let parsed: OKXWsFrame = serde_json::from_str(book_data_json).unwrap();
1868
1869 match parsed {
1870 OKXWsFrame::BookData { arg, action, data } => {
1871 assert_eq!(arg.channel, OKXWsChannel::Books);
1872 assert_eq!(arg.inst_id, Some(Ustr::from("BTC-USDT")));
1873 assert_eq!(
1874 action,
1875 super::super::super::common::enums::OKXBookAction::Snapshot
1876 );
1877 assert_eq!(data.len(), 1);
1878 }
1879 _ => panic!("Expected BookData variant"),
1880 }
1881 }
1882
1883 #[rstest]
1884 fn test_data_event_parsing() {
1885 let data_json = r#"{
1886 "arg": {
1887 "channel": "trades",
1888 "instId": "BTC-USDT"
1889 },
1890 "data": [{
1891 "instId": "BTC-USDT",
1892 "tradeId": "12345",
1893 "px": "50000.0",
1894 "sz": "0.1",
1895 "side": "buy",
1896 "ts": "1640995200000"
1897 }]
1898 }"#;
1899
1900 let parsed: OKXWsFrame = serde_json::from_str(data_json).unwrap();
1901
1902 match parsed {
1903 OKXWsFrame::Data { arg, data } => {
1904 assert_eq!(arg.channel, OKXWsChannel::Trades);
1905 assert_eq!(arg.inst_id, Some(Ustr::from("BTC-USDT")));
1906 assert!(data.is_array());
1907 }
1908 _ => panic!("Expected Data variant"),
1909 }
1910 }
1911
1912 #[rstest]
1913 fn test_nautilus_message_variants() {
1914 let clock = get_atomic_clock_realtime();
1915 let ts_init = clock.get_time_ns();
1916
1917 let error = OKXWebSocketError {
1918 code: "60012".to_string(),
1919 message: "Invalid request".to_string(),
1920 conn_id: None,
1921 timestamp: ts_init.as_u64(),
1922 };
1923 let error_msg = NautilusWsMessage::Error(error);
1924
1925 match error_msg {
1926 NautilusWsMessage::Error(e) => {
1927 assert_eq!(e.code, "60012");
1928 assert_eq!(e.message, "Invalid request");
1929 }
1930 _ => panic!("Expected Error variant"),
1931 }
1932
1933 let raw_scenarios = vec![
1934 ::serde_json::json!({"unknown": "data"}),
1935 ::serde_json::json!({"channel": "unsupported", "data": [1, 2, 3]}),
1936 ::serde_json::json!({"complex": {"nested": {"structure": true}}}),
1937 ];
1938
1939 for raw_data in raw_scenarios {
1940 let raw_msg = NautilusWsMessage::Raw(raw_data.clone());
1941
1942 match raw_msg {
1943 NautilusWsMessage::Raw(data) => {
1944 assert_eq!(data, raw_data);
1945 }
1946 _ => panic!("Expected Raw variant"),
1947 }
1948 }
1949 }
1950
1951 #[rstest]
1952 fn test_order_response_parsing_success() {
1953 let order_response_json = r#"{
1954 "id": "req-123",
1955 "op": "order",
1956 "code": "0",
1957 "msg": "",
1958 "data": [{"sMsg": "Order placed successfully"}]
1959 }"#;
1960
1961 let parsed: OKXWsFrame = serde_json::from_str(order_response_json).unwrap();
1962
1963 match parsed {
1964 OKXWsFrame::OrderResponse {
1965 id,
1966 op,
1967 code,
1968 msg,
1969 data,
1970 } => {
1971 assert_eq!(id, Some("req-123".to_string()));
1972 assert_eq!(op, OKXWsOperation::Order);
1973 assert_eq!(code, "0");
1974 assert_eq!(msg, "");
1975 assert_eq!(data.len(), 1);
1976 }
1977 _ => panic!("Expected OrderResponse variant"),
1978 }
1979 }
1980
1981 #[rstest]
1982 fn test_order_response_parsing_failure() {
1983 let order_response_json = r#"{
1984 "id": "req-456",
1985 "op": "cancel-order",
1986 "code": "50001",
1987 "msg": "Order not found",
1988 "data": [{"sMsg": "Order with client order ID not found"}]
1989 }"#;
1990
1991 let parsed: OKXWsFrame = serde_json::from_str(order_response_json).unwrap();
1992
1993 match parsed {
1994 OKXWsFrame::OrderResponse {
1995 id,
1996 op,
1997 code,
1998 msg,
1999 data,
2000 } => {
2001 assert_eq!(id, Some("req-456".to_string()));
2002 assert_eq!(op, OKXWsOperation::CancelOrder);
2003 assert_eq!(code, "50001");
2004 assert_eq!(msg, "Order not found");
2005 assert_eq!(data.len(), 1);
2006 }
2007 _ => panic!("Expected OrderResponse variant"),
2008 }
2009 }
2010
2011 #[rstest]
2012 fn test_message_request_serialization() {
2013 let request = OKXWsRequest {
2014 id: Some("req-123".to_string()),
2015 op: OKXWsOperation::Order,
2016 args: vec![::serde_json::json!({
2017 "instId": "BTC-USDT",
2018 "tdMode": "cash",
2019 "side": "buy",
2020 "ordType": "market",
2021 "sz": "0.1"
2022 })],
2023 exp_time: None,
2024 };
2025
2026 let serialized = serde_json::to_string(&request).unwrap();
2027 let parsed: serde_json::Value = serde_json::from_str(&serialized).unwrap();
2028
2029 assert_eq!(parsed["id"], "req-123");
2030 assert_eq!(parsed["op"], "order");
2031 assert!(parsed["args"].is_array());
2032 assert_eq!(parsed["args"].as_array().unwrap().len(), 1);
2033 }
2034
2035 #[rstest]
2036 fn test_ws_post_order_params_serializes_inst_id_code() {
2037 use super::WsPostOrderParamsBuilder;
2038 use crate::common::enums::{OKXOrderType, OKXSide, OKXTradeMode};
2039
2040 let params = WsPostOrderParamsBuilder::default()
2041 .inst_id_code(10459u64)
2042 .td_mode(OKXTradeMode::Cross)
2043 .side(OKXSide::Buy)
2044 .ord_type(OKXOrderType::Limit)
2045 .sz("0.01".to_string())
2046 .px("50000".to_string())
2047 .build()
2048 .unwrap();
2049
2050 let json = serde_json::to_string(¶ms).unwrap();
2051
2052 assert!(json.contains("\"instIdCode\":10459"));
2053 assert!(!json.contains("\"instId\""));
2054 }
2055
2056 #[rstest]
2057 fn test_ws_post_order_params_serializes_attached_tp_sl() {
2058 use super::{WsAttachAlgoOrdParamsBuilder, WsPostOrderParamsBuilder};
2059 use crate::common::enums::{OKXOrderType, OKXSide, OKXTradeMode, OKXTriggerType};
2060
2061 let params = WsPostOrderParamsBuilder::default()
2062 .inst_id_code(10459u64)
2063 .td_mode(OKXTradeMode::Cross)
2064 .side(OKXSide::Buy)
2065 .ord_type(OKXOrderType::Limit)
2066 .sz("0.01".to_string())
2067 .px("50000".to_string())
2068 .attach_algo_ords(vec![
2069 WsAttachAlgoOrdParamsBuilder::default()
2070 .attach_algo_cl_ord_id("O-bracket-sl")
2071 .sl_trigger_px("39000")
2072 .sl_ord_px("-1")
2073 .sl_trigger_px_type(OKXTriggerType::Last)
2074 .build()
2075 .unwrap(),
2076 WsAttachAlgoOrdParamsBuilder::default()
2077 .attach_algo_cl_ord_id("O-bracket-tp")
2078 .tp_trigger_px("41000")
2079 .tp_ord_px("-1")
2080 .tp_trigger_px_type(OKXTriggerType::Last)
2081 .build()
2082 .unwrap(),
2083 ])
2084 .build()
2085 .unwrap();
2086
2087 let json = serde_json::to_string(¶ms).unwrap();
2088
2089 assert!(json.contains("\"attachAlgoOrds\""));
2090 assert!(json.contains("\"attachAlgoClOrdId\":\"O-bracket-sl\""));
2091 assert!(json.contains("\"slTriggerPx\":\"39000\""));
2092 assert!(json.contains("\"slOrdPx\":\"-1\""));
2093 assert!(json.contains("\"attachAlgoClOrdId\":\"O-bracket-tp\""));
2094 assert!(json.contains("\"tpTriggerPx\":\"41000\""));
2095 assert!(json.contains("\"tpOrdPx\":\"-1\""));
2096 }
2097
2098 #[rstest]
2099 fn test_ws_cancel_order_params_serializes_inst_id_code() {
2100 use super::WsCancelOrderParamsBuilder;
2101
2102 let params = WsCancelOrderParamsBuilder::default()
2103 .inst_id_code(10461u64)
2104 .ord_id("12345678".to_string())
2105 .build()
2106 .unwrap();
2107
2108 let json = serde_json::to_string(¶ms).unwrap();
2109
2110 assert!(json.contains("\"instIdCode\":10461"));
2111 assert!(!json.contains("\"instId\""));
2112 assert!(json.contains("\"ordId\":\"12345678\""));
2113 }
2114
2115 #[rstest]
2116 fn test_ws_amend_order_params_serializes_inst_id_code() {
2117 use super::WsAmendOrderParamsBuilder;
2118
2119 let params = WsAmendOrderParamsBuilder::default()
2120 .inst_id_code(10459u64)
2121 .cl_ord_id("client123".to_string())
2122 .new_px("51000".to_string())
2123 .build()
2124 .unwrap();
2125
2126 let json = serde_json::to_string(¶ms).unwrap();
2127
2128 assert!(json.contains("\"instIdCode\":10459"));
2129 assert!(!json.contains("\"instId\""));
2130 assert!(json.contains("\"newPx\":\"51000\""));
2131 }
2132
2133 #[rstest]
2134 fn test_ws_post_algo_order_params_serializes_inst_id_code() {
2135 use super::WsPostAlgoOrderParamsBuilder;
2136 use crate::common::enums::{OKXAlgoOrderType, OKXSide, OKXTradeMode, OKXTriggerType};
2137
2138 let params = WsPostAlgoOrderParamsBuilder::default()
2139 .inst_id_code(10459u64)
2140 .td_mode(OKXTradeMode::Cross)
2141 .side(OKXSide::Buy)
2142 .ord_type(OKXAlgoOrderType::Trigger)
2143 .sz("0.01".to_string())
2144 .trigger_px("48000".to_string())
2145 .trigger_px_type(OKXTriggerType::Last)
2146 .build()
2147 .unwrap();
2148
2149 let json = serde_json::to_string(¶ms).unwrap();
2150
2151 assert!(json.contains("\"instIdCode\":10459"));
2152 assert!(!json.contains("\"instId\""));
2153 assert!(json.contains("\"triggerPx\":\"48000\""));
2154 }
2155
2156 #[rstest]
2157 fn test_ws_cancel_algo_order_params_serializes_inst_id_code() {
2158 let params = WsCancelAlgoOrderParams {
2159 inst_id_code: 10459,
2160 algo_id: Some("987654321".to_string()),
2161 algo_cl_ord_id: None,
2162 };
2163
2164 let json = serde_json::to_string(¶ms).unwrap();
2165
2166 assert!(json.contains("\"instIdCode\":10459"));
2167 assert!(!json.contains("\"instId\""));
2168 assert!(json.contains("\"algoId\":\"987654321\""));
2169 }
2170
2171 #[rstest]
2172 fn test_ws_post_order_params_serializes_px_usd() {
2173 use super::WsPostOrderParamsBuilder;
2174 use crate::common::enums::{OKXOrderType, OKXSide, OKXTradeMode};
2175
2176 let params = WsPostOrderParamsBuilder::default()
2177 .inst_id_code(10459u64)
2178 .td_mode(OKXTradeMode::Cross)
2179 .side(OKXSide::Buy)
2180 .ord_type(OKXOrderType::Limit)
2181 .sz("1".to_string())
2182 .px_usd("100.5".to_string())
2183 .build()
2184 .unwrap();
2185
2186 let json = serde_json::to_string(¶ms).unwrap();
2187 assert!(json.contains("\"pxUsd\":\"100.5\""));
2188 assert!(!json.contains("\"pxVol\""));
2189 assert!(!json.contains("\"px\":"));
2190 }
2191
2192 #[rstest]
2193 fn test_ws_post_order_params_serializes_px_vol() {
2194 use super::WsPostOrderParamsBuilder;
2195 use crate::common::enums::{OKXOrderType, OKXSide, OKXTradeMode};
2196
2197 let params = WsPostOrderParamsBuilder::default()
2198 .inst_id_code(10459u64)
2199 .td_mode(OKXTradeMode::Cross)
2200 .side(OKXSide::Buy)
2201 .ord_type(OKXOrderType::Limit)
2202 .sz("1".to_string())
2203 .px_vol("0.55".to_string())
2204 .build()
2205 .unwrap();
2206
2207 let json = serde_json::to_string(¶ms).unwrap();
2208 assert!(json.contains("\"pxVol\":\"0.55\""));
2209 assert!(!json.contains("\"pxUsd\""));
2210 assert!(!json.contains("\"px\":"));
2211 }
2212
2213 #[rstest]
2214 fn test_ws_amend_order_params_serializes_new_px_usd() {
2215 use super::WsAmendOrderParamsBuilder;
2216
2217 let params = WsAmendOrderParamsBuilder::default()
2218 .inst_id_code(10459u64)
2219 .cl_ord_id("client123".to_string())
2220 .new_px_usd("105.0".to_string())
2221 .build()
2222 .unwrap();
2223
2224 let json = serde_json::to_string(¶ms).unwrap();
2225 assert!(json.contains("\"newPxUsd\":\"105.0\""));
2226 assert!(!json.contains("\"newPx\":"));
2227 assert!(!json.contains("\"newPxVol\""));
2228 }
2229
2230 #[rstest]
2231 fn test_ws_amend_order_params_serializes_new_px_vol() {
2232 use super::WsAmendOrderParamsBuilder;
2233
2234 let params = WsAmendOrderParamsBuilder::default()
2235 .inst_id_code(10459u64)
2236 .cl_ord_id("client123".to_string())
2237 .new_px_vol("0.60".to_string())
2238 .build()
2239 .unwrap();
2240
2241 let json = serde_json::to_string(¶ms).unwrap();
2242 assert!(json.contains("\"newPxVol\":\"0.60\""));
2243 assert!(!json.contains("\"newPx\":"));
2244 assert!(!json.contains("\"newPxUsd\""));
2245 }
2246}