1use derive_builder::Builder;
19use nautilus_core::string::secret::SecretString;
20use nautilus_model::{
21 data::{Data, FundingRateUpdate, InstrumentStatus, OrderBookDeltas},
22 events::{
23 AccountState, OrderAccepted, OrderCancelRejected, OrderCanceled, OrderExpired,
24 OrderModifyRejected, OrderRejected, OrderTriggered, OrderUpdated,
25 },
26 identifiers::ClientOrderId,
27 instruments::InstrumentAny,
28 reports::{FillReport, OrderStatusReport, PositionStatusReport},
29};
30use serde::{
31 Deserialize, Serialize,
32 de::{DeserializeOwned, Error, IgnoredAny, MapAccess, Visitor},
33};
34use serde_json::value::RawValue;
35use ustr::Ustr;
36use zeroize::Zeroize;
37
38use super::enums::{OKXWsChannel, OKXWsOperation};
39use crate::{
40 common::{
41 enums::{
42 OKXAlgoOrderStatus, OKXAlgoOrderType, OKXBookAction, OKXCandleConfirm, OKXExecType,
43 OKXInstrumentType, OKXMarginMode, OKXOrderCategory, OKXOrderStatus, OKXOrderType,
44 OKXPositionSide, OKXPriceType, OKXQuickMarginType, OKXSelfTradePreventionMode,
45 OKXSettlementState, OKXSide, OKXTargetCurrency, OKXTradeMode, OKXTriggerType,
46 },
47 models::{OKXInstrument, OKXRpiBookLevel},
48 parse::{
49 deserialize_empty_string_as_none, deserialize_empty_ustr_as_none,
50 deserialize_string_to_u64, deserialize_target_currency_as_none,
51 },
52 },
53 http::models::OKXSpreadOrder,
54 websocket::enums::OKXSubscriptionEvent,
55};
56
57#[derive(Debug, Clone)]
58pub enum NautilusWsMessage {
59 Data(Vec<Data>),
60 Deltas(OrderBookDeltas),
61 FundingRates(Vec<FundingRateUpdate>),
62 Instrument(Box<InstrumentAny>, Option<InstrumentStatus>),
63 InstrumentStatus(InstrumentStatus),
64 AccountUpdate(AccountState),
65 PositionUpdate(PositionStatusReport),
66 OrderAccepted(OrderAccepted),
67 OrderCanceled(OrderCanceled),
68 OrderExpired(OrderExpired),
69 OrderRejected(OrderRejected),
70 OrderCancelRejected(OrderCancelRejected),
71 OrderModifyRejected(OrderModifyRejected),
72 OrderTriggered(OrderTriggered),
73 OrderUpdated(OrderUpdated),
74 ExecutionReports(Vec<ExecutionReport>),
75 Error(OKXWebSocketError),
76 Raw(serde_json::Value), Reconnected,
78 Authenticated,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct OKXWebSocketError {
84 pub code: String,
86 pub message: String,
88 pub conn_id: Option<String>,
90 pub timestamp: u64,
92}
93
94#[derive(Debug, Clone)]
95#[allow(
96 clippy::large_enum_variant,
97 reason = "the variant size gap only crosses the threshold when high-precision widens the raw types"
98)]
99pub enum ExecutionReport {
100 Order(OrderStatusReport),
101 Fill(FillReport),
102}
103
104#[derive(Debug)]
110pub enum OKXWsMessage {
111 BookData {
113 arg: OKXWebSocketArg,
114 action: OKXBookAction,
115 data: Vec<OKXBookMsg>,
116 },
117 RpiBookData {
119 arg: OKXWebSocketArg,
120 action: OKXBookAction,
121 data: Vec<OKXRpiBookMsg>,
122 },
123 ChannelData {
125 channel: OKXWsChannel,
126 inst_id: Option<Ustr>,
127 data: serde_json::Value,
128 },
129 OrderResponse {
131 id: Option<String>,
132 op: OKXWsOperation,
133 code: String,
134 msg: String,
135 data: Vec<serde_json::Value>,
136 },
137 Orders(Vec<OKXOrderMsg>),
139 SpreadOrders(Vec<OKXSpreadOrder>),
141 AlgoOrders(Vec<OKXAlgoOrderMsg>),
143 Account(serde_json::Value),
145 Positions(serde_json::Value),
147 LiquidationWarnings(Vec<OKXLiquidationWarningMsg>),
149 Instruments(Vec<OKXInstrument>),
151 SendFailed {
153 request_id: String,
154 client_order_ids: Vec<ClientOrderId>,
155 op: Option<OKXWsOperation>,
156 error: super::error::OKXWsError,
157 },
158 SubscriptionFailed {
160 channel: OKXWsChannel,
161 inst_id: Option<Ustr>,
162 code: String,
163 msg: String,
164 },
165 Error(OKXWebSocketError),
167 Reconnected,
169 Authenticated,
171}
172
173#[derive(Debug, Serialize)]
175#[serde(rename_all = "camelCase")]
176pub struct OKXWsRequest<T> {
177 #[serde(skip_serializing_if = "Option::is_none")]
179 pub id: Option<String>,
180 pub op: OKXWsOperation,
182 #[serde(skip_serializing_if = "Option::is_none")]
185 pub exp_time: Option<String>,
186 pub args: Vec<T>,
188}
189
190#[derive(Debug, Serialize, Zeroize)]
192pub struct OKXAuthentication {
193 #[zeroize(skip)]
194 pub op: &'static str,
195 pub args: Vec<OKXAuthenticationArg>,
196}
197
198#[derive(Debug, Serialize, Zeroize)]
200#[serde(rename_all = "camelCase")]
201pub struct OKXAuthenticationArg {
202 pub api_key: SecretString,
203 pub passphrase: SecretString,
204 pub timestamp: String,
205 pub sign: SecretString,
206}
207
208#[derive(Debug, Serialize)]
209pub struct OKXSubscription {
210 pub op: OKXWsOperation,
211 pub args: Vec<OKXSubscriptionArg>,
212}
213
214#[derive(Clone, Debug)]
215pub struct OKXSubscriptionArg {
216 pub channel: OKXWsChannel,
217 pub inst_type: Option<OKXInstrumentType>,
218 pub inst_family: Option<Ustr>,
219 pub inst_id: Option<Ustr>,
220}
221
222impl Serialize for OKXSubscriptionArg {
223 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
224 use serde::ser::SerializeMap;
225
226 let mut map = serializer.serialize_map(None)?;
227 map.serialize_entry("channel", &self.channel)?;
228
229 if let Some(inst_type) = &self.inst_type {
230 map.serialize_entry("instType", inst_type)?;
231 }
232
233 if let Some(inst_family) = &self.inst_family {
234 map.serialize_entry("instFamily", inst_family)?;
235 }
236
237 if let Some(inst_id) = &self.inst_id {
238 let key = if self.channel.is_spread() {
239 "sprdId"
240 } else {
241 "instId"
242 };
243 map.serialize_entry(key, inst_id)?;
244 }
245
246 map.end()
247 }
248}
249
250#[derive(Debug)]
255pub enum OKXWsFrame {
256 Login {
257 event: String,
258 code: String,
259 msg: String,
260 conn_id: String,
261 },
262 Subscription {
263 event: OKXSubscriptionEvent,
264 arg: OKXWebSocketArg,
265 conn_id: String,
266 code: Option<String>,
267 msg: Option<String>,
268 },
269 ChannelConnCount {
270 event: String,
271 channel: OKXWsChannel,
272 conn_count: String,
273 conn_id: String,
274 },
275 OrderResponse {
276 id: Option<String>,
277 op: OKXWsOperation,
278 code: String,
279 msg: String,
280 data: Vec<serde_json::Value>,
281 },
282 BookData {
283 arg: OKXWebSocketArg,
284 action: OKXBookAction,
285 data: Vec<OKXBookMsg>,
286 },
287 RpiBookData {
288 arg: OKXWebSocketArg,
289 action: OKXBookAction,
290 data: Vec<OKXRpiBookMsg>,
291 },
292 Data {
293 arg: OKXWebSocketArg,
294 data: serde_json::Value,
295 },
296 Error {
297 arg: Option<OKXWebSocketArg>,
298 code: String,
299 msg: String,
300 },
301 Ping,
302 Reconnected,
303}
304
305impl<'de> Deserialize<'de> for OKXWsFrame {
306 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
307 where
308 D: serde::Deserializer<'de>,
309 {
310 deserializer.deserialize_map(OKXWsFrameVisitor)
311 }
312}
313
314#[derive(Deserialize)]
315#[serde(field_identifier, rename_all = "camelCase")]
316enum OKXWsFrameKey {
317 Event,
318 Op,
319 Action,
320 Arg,
321 Data,
322 Code,
323 Msg,
324 ConnId,
325 Id,
326 Channel,
327 ConnCount,
328 #[serde(other)]
329 Other,
330}
331
332#[derive(Default)]
339struct OKXWsFrameFields {
340 event: Option<Box<RawValue>>,
341 op: Option<Box<RawValue>>,
342 action: Option<Box<RawValue>>,
343 arg: Option<Box<RawValue>>,
344 arg_decoded: Option<OKXWebSocketArg>,
345 data: Option<OKXWsFrameData>,
346 code: Option<Box<RawValue>>,
347 msg: Option<Box<RawValue>>,
348 conn_id: Option<Box<RawValue>>,
349 id: Option<Box<RawValue>>,
350 channel: Option<Box<RawValue>>,
351 conn_count: Option<Box<RawValue>>,
352}
353
354enum OKXWsFrameData {
355 Value(serde_json::Value),
356 Book(Vec<OKXBookMsg>),
357 RpiBook(Vec<OKXRpiBookMsg>),
358}
359
360impl OKXWsFrameData {
361 fn into_value<E: serde::de::Error>(self) -> Result<serde_json::Value, E> {
362 match self {
363 Self::Value(value) => Ok(value),
364 Self::Book(_) | Self::RpiBook(_) => {
365 Err(E::custom("invalid data: book levels in a non-book frame"))
366 }
367 }
368 }
369}
370
371struct OKXWsFrameVisitor;
372
373impl<'de> Visitor<'de> for OKXWsFrameVisitor {
374 type Value = OKXWsFrame;
375
376 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
377 formatter.write_str("JSON object for OKXWsFrame")
378 }
379
380 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
381 where
382 A: MapAccess<'de>,
383 {
384 let mut fields = OKXWsFrameFields::default();
385
386 while let Some(key) = map.next_key::<OKXWsFrameKey>()? {
387 let slot = match key {
388 OKXWsFrameKey::Event => &mut fields.event,
389 OKXWsFrameKey::Op => &mut fields.op,
390 OKXWsFrameKey::Action => &mut fields.action,
391 OKXWsFrameKey::Arg => {
392 fields.arg_decoded = None;
393 &mut fields.arg
394 }
395 OKXWsFrameKey::Data => {
396 fields.data = Some(fields.next_data(&mut map)?);
397 continue;
398 }
399 OKXWsFrameKey::Code => &mut fields.code,
400 OKXWsFrameKey::Msg => &mut fields.msg,
401 OKXWsFrameKey::ConnId => &mut fields.conn_id,
402 OKXWsFrameKey::Id => &mut fields.id,
403 OKXWsFrameKey::Channel => &mut fields.channel,
404 OKXWsFrameKey::ConnCount => &mut fields.conn_count,
405 OKXWsFrameKey::Other => {
406 map.next_value::<IgnoredAny>()?;
407 continue;
408 }
409 };
410
411 *slot = Some(map.next_value()?);
412 }
413
414 fields.into_frame()
415 }
416}
417
418impl OKXWsFrameFields {
419 fn next_data<'de, A: MapAccess<'de>>(
420 &mut self,
421 map: &mut A,
422 ) -> Result<OKXWsFrameData, A::Error> {
423 let invalid_data = |e: A::Error| A::Error::custom(format!("invalid data: {e}"));
424
425 if self.event.is_none()
426 && self.op.is_none()
427 && self.action.is_some()
428 && let Some(raw) = self.arg.as_deref()
429 {
430 let arg: OKXWebSocketArg = parse_raw(raw, "arg")?;
431
432 let data = if arg.channel == OKXWsChannel::BooksRpi {
433 OKXWsFrameData::RpiBook(map.next_value().map_err(invalid_data)?)
434 } else {
435 OKXWsFrameData::Book(map.next_value().map_err(invalid_data)?)
436 };
437
438 self.arg_decoded = Some(arg);
439 return Ok(data);
440 }
441
442 Ok(OKXWsFrameData::Value(map.next_value()?))
443 }
444
445 fn take_arg<E: serde::de::Error>(&mut self) -> Result<OKXWebSocketArg, E> {
446 match self.arg_decoded.take() {
447 Some(arg) => Ok(arg),
448 None => take_typed(self.arg.take(), "arg"),
449 }
450 }
451
452 fn into_frame<E: serde::de::Error>(self) -> Result<OKXWsFrame, E> {
453 if let Some(event) = self.event.as_deref().and_then(raw_as_string) {
457 match event.as_str() {
458 "login" => return parse_login(self),
459 "subscribe" | "unsubscribe" => return parse_subscription(self),
460 "error" => return parse_error(self),
461 _ if self.channel.is_some() && self.conn_count.is_some() => {
462 return parse_channel_conn_count(self);
463 }
464 _ => {}
465 }
466 }
467
468 if self.op.is_some() {
470 return parse_order_response(self);
471 }
472
473 if self.action.is_some() && self.arg.is_some() {
475 return parse_book_data(self);
476 }
477
478 if self.arg.is_some() && self.data.is_some() {
480 return parse_data(self);
481 }
482
483 if self.code.is_some() && self.msg.is_some() {
485 return parse_error(self);
486 }
487
488 Err(E::custom("cannot determine OKXWsFrame variant"))
489 }
490}
491
492fn raw_as_string(raw: &RawValue) -> Option<String> {
493 serde_json::from_str(raw.get()).ok()
494}
495
496fn parse_raw<T: DeserializeOwned, E: serde::de::Error>(
497 raw: &RawValue,
498 key: &'static str,
499) -> Result<T, E> {
500 serde_json::from_str(raw.get()).map_err(|e| E::custom(format!("invalid {key}: {e}")))
501}
502
503fn take_typed<T: DeserializeOwned, E: serde::de::Error>(
504 raw: Option<Box<RawValue>>,
505 key: &'static str,
506) -> Result<T, E> {
507 let raw = raw.ok_or_else(|| E::missing_field(key))?;
508 parse_raw(&raw, key)
509}
510
511fn take_data_typed<T: DeserializeOwned, E: serde::de::Error>(data: OKXWsFrameData) -> Result<T, E> {
512 serde_json::from_value(data.into_value()?).map_err(|e| E::custom(format!("invalid data: {e}")))
513}
514
515fn take_str<E: serde::de::Error>(
516 raw: Option<Box<RawValue>>,
517 key: &'static str,
518) -> Result<String, E> {
519 let raw = raw.ok_or_else(|| E::missing_field(key))?;
520 raw_as_string(&raw).ok_or_else(|| E::custom(format!("field `{key}` is not a string")))
521}
522
523fn take_optional_str(raw: Option<Box<RawValue>>) -> Option<String> {
524 raw.and_then(|raw| raw_as_string(&raw))
525}
526
527fn parse_login<E: serde::de::Error>(fields: OKXWsFrameFields) -> Result<OKXWsFrame, E> {
528 Ok(OKXWsFrame::Login {
529 event: take_str(fields.event, "event")?,
530 code: take_str(fields.code, "code")?,
531 msg: take_str(fields.msg, "msg")?,
532 conn_id: take_str(fields.conn_id, "connId")?,
533 })
534}
535
536fn parse_subscription<E: serde::de::Error>(mut fields: OKXWsFrameFields) -> Result<OKXWsFrame, E> {
537 let event: OKXSubscriptionEvent = take_typed(fields.event.take(), "event")?;
538 let arg = fields.take_arg()?;
539
540 Ok(OKXWsFrame::Subscription {
541 event,
542 arg,
543 conn_id: take_str(fields.conn_id, "connId")?,
544 code: take_optional_str(fields.code),
545 msg: take_optional_str(fields.msg),
546 })
547}
548
549fn parse_channel_conn_count<E: serde::de::Error>(
550 fields: OKXWsFrameFields,
551) -> Result<OKXWsFrame, E> {
552 let channel: OKXWsChannel = take_typed(fields.channel, "channel")?;
553
554 Ok(OKXWsFrame::ChannelConnCount {
555 event: take_str(fields.event, "event")?,
556 channel,
557 conn_count: take_str(fields.conn_count, "connCount")?,
558 conn_id: take_str(fields.conn_id, "connId")?,
559 })
560}
561
562fn parse_order_response<E: serde::de::Error>(fields: OKXWsFrameFields) -> Result<OKXWsFrame, E> {
563 let op: OKXWsOperation = take_typed(fields.op, "op")?;
564
565 let data: Vec<serde_json::Value> = match fields.data {
566 Some(OKXWsFrameData::Value(serde_json::Value::Array(values))) => values,
567 Some(data) => take_data_typed(data)?,
568 None => Vec::new(),
569 };
570
571 Ok(OKXWsFrame::OrderResponse {
572 id: take_optional_str(fields.id),
573 op,
574 code: take_str(fields.code, "code")?,
575 msg: take_str(fields.msg, "msg")?,
576 data,
577 })
578}
579
580fn parse_book_data<E: serde::de::Error>(mut fields: OKXWsFrameFields) -> Result<OKXWsFrame, E> {
581 let arg = fields.take_arg()?;
582 let action: OKXBookAction = take_typed(fields.action, "action")?;
583 let data = fields.data.ok_or_else(|| E::missing_field("data"))?;
584
585 if arg.channel == OKXWsChannel::BooksRpi {
586 let data: Vec<OKXRpiBookMsg> = match data {
587 OKXWsFrameData::RpiBook(data) => data,
588 data => take_data_typed(data)?,
589 };
590
591 return Ok(OKXWsFrame::RpiBookData { arg, action, data });
592 }
593
594 let data: Vec<OKXBookMsg> = match data {
595 OKXWsFrameData::Book(data) => data,
596 data => take_data_typed(data)?,
597 };
598
599 Ok(OKXWsFrame::BookData { arg, action, data })
600}
601
602fn parse_data<E: serde::de::Error>(mut fields: OKXWsFrameFields) -> Result<OKXWsFrame, E> {
603 let arg = fields.take_arg()?;
604 let data = fields
605 .data
606 .ok_or_else(|| E::missing_field("data"))?
607 .into_value()?;
608
609 Ok(OKXWsFrame::Data { arg, data })
610}
611
612fn parse_error<E: serde::de::Error>(mut fields: OKXWsFrameFields) -> Result<OKXWsFrame, E> {
613 let arg = if fields.arg.is_some() {
614 Some(fields.take_arg()?)
615 } else {
616 None
617 };
618
619 Ok(OKXWsFrame::Error {
620 arg,
621 code: take_str(fields.code, "code")?,
622 msg: take_str(fields.msg, "msg")?,
623 })
624}
625
626#[derive(Debug, Serialize, Deserialize)]
627#[serde(rename_all = "camelCase")]
628pub struct OKXWebSocketArg {
629 pub channel: OKXWsChannel,
631 #[serde(default, alias = "sprdId")]
635 pub inst_id: Option<Ustr>,
636 #[serde(default)]
637 pub inst_type: Option<OKXInstrumentType>,
638 #[serde(default)]
639 pub inst_family: Option<Ustr>,
640 #[serde(default)]
641 pub bar: Option<Ustr>,
642}
643
644#[derive(Debug, Serialize, Deserialize)]
646#[serde(rename_all = "camelCase")]
647pub struct OKXTickerMsg {
648 pub inst_type: OKXInstrumentType,
650 pub inst_id: Ustr,
652 #[serde(rename = "last")]
654 pub last_px: String,
655 pub last_sz: String,
657 pub ask_px: String,
659 pub ask_sz: String,
661 pub bid_px: String,
663 pub bid_sz: String,
665 pub open24h: String,
667 pub high24h: String,
669 pub low24h: String,
671 pub vol_ccy_24h: String,
673 pub vol24h: String,
675 pub sod_utc0: String,
677 pub sod_utc8: String,
679 #[serde(deserialize_with = "deserialize_string_to_u64")]
681 pub ts: u64,
682 #[serde(default)]
684 pub source: Option<String>,
685}
686
687#[derive(Debug, Serialize, Deserialize)]
689pub struct OrderBookEntry {
690 pub price: String,
692 pub size: String,
694 #[serde(default)]
700 pub liquidated_orders_count: String,
701 #[serde(default)]
703 pub orders_count: String,
704}
705
706#[derive(Debug, Serialize, Deserialize)]
708#[serde(rename_all = "camelCase")]
709pub struct OKXBookMsg {
710 pub asks: Vec<OrderBookEntry>,
712 pub bids: Vec<OrderBookEntry>,
714 pub checksum: Option<i64>,
716 pub prev_seq_id: Option<i64>,
718 pub seq_id: u64,
720 #[serde(deserialize_with = "deserialize_string_to_u64")]
722 pub ts: u64,
723}
724
725#[derive(Debug, Serialize, Deserialize)]
727#[serde(rename_all = "camelCase", deny_unknown_fields)]
728pub struct OKXRpiBookMsg {
729 pub asks: Vec<OKXRpiBookLevel>,
731 pub bids: Vec<OKXRpiBookLevel>,
733 pub prev_seq_id: i64,
735 pub seq_id: u64,
737 #[serde(deserialize_with = "deserialize_string_to_u64")]
739 pub ts: u64,
740}
741
742#[derive(Debug, Serialize, Deserialize)]
744#[serde(rename_all = "camelCase")]
745pub struct OKXTradeMsg {
746 #[serde(default, alias = "sprdId")]
751 pub inst_id: Ustr,
752 pub trade_id: String,
754 pub px: String,
756 pub sz: String,
758 pub side: OKXSide,
760 #[serde(default)]
762 pub count: String,
763 #[serde(deserialize_with = "deserialize_string_to_u64")]
765 pub ts: u64,
766 #[serde(default)]
768 pub source: Option<String>,
769 #[serde(default)]
771 pub seq_id: Option<u64>,
772}
773
774#[derive(Debug, Serialize, Deserialize)]
776#[serde(rename_all = "camelCase")]
777pub struct OKXFundingRateMsg {
778 #[serde(default)]
780 pub inst_type: Option<OKXInstrumentType>,
781 pub inst_id: Ustr,
783 pub funding_rate: Ustr,
785 pub next_funding_rate: Ustr,
787 #[serde(default)]
789 pub min_funding_rate: Option<String>,
790 #[serde(default)]
792 pub max_funding_rate: Option<String>,
793 #[serde(default)]
795 pub sett_state: OKXSettlementState,
796 #[serde(default)]
798 pub sett_funding_rate: Option<String>,
799 #[serde(default)]
801 pub premium: Option<String>,
802 #[serde(default)]
804 pub method: Option<String>,
805 #[serde(deserialize_with = "deserialize_string_to_u64")]
807 pub funding_time: u64,
808 #[serde(deserialize_with = "deserialize_string_to_u64")]
810 pub next_funding_time: u64,
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 OKXMarkPriceMsg {
820 pub inst_id: Ustr,
822 pub mark_px: String,
824 #[serde(deserialize_with = "deserialize_string_to_u64")]
826 pub ts: u64,
827}
828
829#[derive(Debug, Serialize, Deserialize)]
831#[serde(rename_all = "camelCase")]
832pub struct OKXIndexPriceMsg {
833 pub inst_id: Ustr,
835 pub idx_px: String,
837 pub high24h: String,
839 pub low24h: String,
841 pub open24h: String,
843 pub sod_utc0: String,
845 pub sod_utc8: String,
847 #[serde(deserialize_with = "deserialize_string_to_u64")]
849 pub ts: u64,
850}
851
852#[derive(Debug, Serialize, Deserialize)]
854#[serde(rename_all = "camelCase")]
855pub struct OKXPriceLimitMsg {
856 pub inst_id: Ustr,
858 pub buy_lmt: String,
860 pub sell_lmt: String,
862 #[serde(deserialize_with = "deserialize_string_to_u64")]
864 pub ts: u64,
865}
866
867#[derive(Debug, Serialize, Deserialize)]
869#[serde(rename_all = "camelCase")]
870pub struct OKXCandleMsg {
871 #[serde(deserialize_with = "deserialize_string_to_u64")]
873 pub ts: u64,
874 pub o: String,
876 pub h: String,
878 pub l: String,
880 pub c: String,
882 pub vol: String,
884 pub vol_ccy: String,
886 pub vol_ccy_quote: String,
887 pub confirm: OKXCandleConfirm,
889}
890
891#[derive(Debug, Serialize, Deserialize)]
893#[serde(rename_all = "camelCase")]
894pub struct OKXOpenInterestMsg {
895 pub inst_id: Ustr,
897 pub oi: String,
899 pub oi_ccy: String,
901 #[serde(deserialize_with = "deserialize_string_to_u64")]
903 pub ts: u64,
904}
905
906#[derive(Debug, Serialize, Deserialize)]
908#[serde(rename_all = "camelCase")]
909pub struct OKXOptionSummaryMsg {
910 #[serde(default)]
912 pub inst_type: Option<OKXInstrumentType>,
913 pub inst_id: Ustr,
915 pub uly: String,
917 pub delta: String,
919 pub gamma: String,
921 pub theta: String,
923 pub vega: String,
925 #[serde(alias = "deltaBS")]
927 pub delta_bs: String,
928 #[serde(alias = "gammaBS")]
930 pub gamma_bs: String,
931 #[serde(alias = "thetaBS")]
933 pub theta_bs: String,
934 #[serde(alias = "vegaBS")]
936 pub vega_bs: String,
937 pub real_vol: String,
939 pub bid_vol: String,
941 pub ask_vol: String,
943 pub mark_vol: String,
945 pub lever: String,
947 #[serde(default)]
949 pub fwd_px: Option<String>,
950 #[serde(default)]
952 pub mark_px: Option<String>,
953 #[serde(default)]
955 pub vol_lv: Option<String>,
956 #[serde(deserialize_with = "deserialize_string_to_u64")]
958 pub ts: u64,
959}
960
961#[derive(Debug, Serialize, Deserialize)]
963#[serde(rename_all = "camelCase")]
964pub struct OKXEstimatedPriceMsg {
965 pub inst_id: Ustr,
967 pub settle_px: String,
969 #[serde(deserialize_with = "deserialize_string_to_u64")]
971 pub ts: u64,
972}
973
974#[derive(Debug, Serialize, Deserialize)]
976#[serde(rename_all = "camelCase")]
977pub struct OKXStatusMsg {
978 pub title: Ustr,
980 #[serde(rename = "type")]
982 pub status_type: Ustr,
983 pub state: Ustr,
985 pub end_time: Option<String>,
987 pub begin_time: Option<String>,
989 pub service_type: Option<Ustr>,
991 pub reason: Option<String>,
993 #[serde(deserialize_with = "deserialize_string_to_u64")]
995 pub ts: u64,
996}
997
998pub use crate::common::models::OKXAttachedAlgoOrd;
999
1000#[derive(Clone, Debug, Serialize, Deserialize)]
1006#[serde(rename_all = "camelCase")]
1007pub struct OKXLiquidationWarningMsg {
1008 pub inst_type: OKXInstrumentType,
1010 #[serde(default)]
1012 pub inst_family: Option<Ustr>,
1013 pub inst_id: Ustr,
1015 pub mgn_mode: OKXMarginMode,
1017 #[serde(default)]
1019 pub pos_id: Option<Ustr>,
1020 pub pos_side: OKXPositionSide,
1022 pub pos: String,
1024 #[serde(default)]
1026 pub pos_ccy: Option<Ustr>,
1027 pub lever: String,
1029 pub mark_px: String,
1031 pub mgn_ratio: String,
1033 pub ccy: Ustr,
1035 #[serde(deserialize_with = "deserialize_string_to_u64")]
1037 pub c_time: u64,
1038 #[serde(deserialize_with = "deserialize_string_to_u64")]
1040 pub u_time: u64,
1041 #[serde(default)]
1043 pub p_time: Option<String>,
1044}
1045
1046#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1048#[serde(rename_all = "camelCase")]
1049pub struct OKXLinkedAlgoOrd {
1050 #[serde(default)]
1052 pub algo_id: String,
1053}
1054
1055#[derive(Clone, Debug, Serialize, Deserialize)]
1057#[serde(rename_all = "camelCase")]
1058pub struct OKXOrderMsg {
1059 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
1061 pub acc_fill_sz: Option<String>,
1062 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
1064 pub algo_id: Option<String>,
1065 pub avg_px: String,
1067 #[serde(deserialize_with = "deserialize_string_to_u64")]
1069 pub c_time: u64,
1070 #[serde(default)]
1072 pub cancel_source: Option<String>,
1073 #[serde(default)]
1075 pub cancel_source_reason: Option<String>,
1076 pub category: OKXOrderCategory,
1078 pub ccy: Ustr,
1080 pub cl_ord_id: String,
1082 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
1084 pub algo_cl_ord_id: Option<String>,
1085 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
1087 pub attach_algo_cl_ord_id: Option<String>,
1088 #[serde(default)]
1090 pub attach_algo_ords: Vec<OKXAttachedAlgoOrd>,
1091 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
1093 pub outcome: Option<String>,
1094 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
1096 pub fee: Option<String>,
1097 pub fee_ccy: Ustr,
1099 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
1101 pub fill_fee: Option<String>,
1102 #[serde(default, deserialize_with = "deserialize_empty_ustr_as_none")]
1104 pub fill_fee_ccy: Option<Ustr>,
1105 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
1107 pub fill_mark_px: Option<String>,
1108 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
1110 pub fill_mark_vol: Option<String>,
1111 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
1113 pub fill_px_vol: Option<String>,
1114 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
1116 pub fill_px_usd: Option<String>,
1117 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
1119 pub fill_fwd_px: Option<String>,
1120 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
1122 pub fill_notional_usd: Option<String>,
1123 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
1125 pub fill_pnl: Option<String>,
1126 pub fill_px: String,
1128 pub fill_sz: String,
1130 #[serde(deserialize_with = "deserialize_string_to_u64")]
1132 pub fill_time: u64,
1133 pub inst_id: Ustr,
1135 pub inst_type: OKXInstrumentType,
1137 #[serde(default)]
1139 pub is_tp_limit: Option<String>,
1140 pub lever: String,
1142 #[serde(default)]
1144 pub linked_algo_ord: Option<OKXLinkedAlgoOrd>,
1145 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
1147 pub notional_usd: Option<String>,
1148 pub ord_id: Ustr,
1150 pub ord_type: OKXOrderType,
1152 pub pnl: String,
1154 pub pos_side: OKXPositionSide,
1156 #[serde(default)]
1158 pub px: String,
1159 #[serde(default)]
1161 pub px_type: OKXPriceType,
1162 #[serde(default)]
1164 pub px_usd: Option<String>,
1165 #[serde(default)]
1167 pub px_vol: Option<String>,
1168 #[serde(default)]
1170 pub quick_mgn_type: OKXQuickMarginType,
1171 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
1173 pub rebate: Option<String>,
1174 #[serde(default, deserialize_with = "deserialize_empty_ustr_as_none")]
1176 pub rebate_ccy: Option<Ustr>,
1177 pub reduce_only: String,
1179 pub side: OKXSide,
1181 #[serde(default)]
1183 pub sl_ord_px: Option<String>,
1184 #[serde(default)]
1186 pub sl_trigger_px: Option<String>,
1187 #[serde(default)]
1189 pub sl_trigger_px_type: Option<OKXTriggerType>,
1190 #[serde(default)]
1192 pub source: Option<String>,
1193 pub state: OKXOrderStatus,
1195 #[serde(default)]
1197 pub stp_id: Option<String>,
1198 #[serde(default)]
1200 pub stp_mode: OKXSelfTradePreventionMode,
1201 pub exec_type: OKXExecType,
1203 pub sz: String,
1205 #[serde(default)]
1207 pub tag: Option<String>,
1208 pub td_mode: OKXTradeMode,
1210 #[serde(default, deserialize_with = "deserialize_target_currency_as_none")]
1212 pub tgt_ccy: Option<OKXTargetCurrency>,
1213 #[serde(default)]
1215 pub tp_ord_px: Option<String>,
1216 #[serde(default)]
1218 pub tp_trigger_px: Option<String>,
1219 #[serde(default)]
1221 pub tp_trigger_px_type: Option<OKXTriggerType>,
1222 pub trade_id: String,
1224 #[serde(deserialize_with = "deserialize_string_to_u64")]
1226 pub u_time: u64,
1227 #[serde(default)]
1229 pub amend_result: Option<String>,
1230 #[serde(default)]
1232 pub req_id: Option<String>,
1233 #[serde(default)]
1235 pub code: Option<String>,
1236 #[serde(default)]
1238 pub msg: Option<String>,
1239}
1240
1241#[derive(Clone, Debug, Deserialize, Serialize)]
1243#[serde(rename_all = "camelCase")]
1244pub struct OKXAlgoOrderMsg {
1245 pub algo_id: String,
1247 #[serde(default)]
1249 pub algo_cl_ord_id: String,
1250 pub cl_ord_id: String,
1252 pub ord_id: String,
1254 #[serde(default)]
1256 pub ord_id_list: Vec<String>,
1257 pub inst_id: Ustr,
1259 pub inst_type: OKXInstrumentType,
1261 pub ord_type: OKXAlgoOrderType,
1263 pub state: OKXAlgoOrderStatus,
1265 pub side: OKXSide,
1267 pub pos_side: OKXPositionSide,
1269 #[serde(default)]
1271 pub sz: String,
1272 #[serde(default)]
1274 pub trigger_px: String,
1275 #[serde(default)]
1277 pub trigger_px_type: OKXTriggerType,
1278 #[serde(default)]
1280 pub sl_trigger_px: String,
1281 #[serde(default)]
1283 pub sl_ord_px: String,
1284 #[serde(default)]
1286 pub sl_trigger_px_type: OKXTriggerType,
1287 #[serde(default)]
1289 pub tp_trigger_px: String,
1290 #[serde(default)]
1292 pub tp_ord_px: String,
1293 #[serde(default)]
1295 pub tp_trigger_px_type: OKXTriggerType,
1296 #[serde(default)]
1298 pub ord_px: String,
1299 pub td_mode: OKXTradeMode,
1301 pub lever: String,
1303 #[serde(default)]
1305 pub reduce_only: String,
1306 #[serde(default)]
1308 pub close_fraction: String,
1309 #[serde(default)]
1311 pub actual_px: String,
1312 #[serde(default)]
1314 pub actual_sz: String,
1315 #[serde(default)]
1317 pub notional_usd: String,
1318 #[serde(deserialize_with = "deserialize_string_to_u64")]
1320 pub c_time: u64,
1321 #[serde(deserialize_with = "deserialize_string_to_u64")]
1323 pub u_time: u64,
1324 #[serde(default)]
1326 pub trigger_time: String,
1327 #[serde(default)]
1329 pub fail_code: String,
1330 #[serde(default)]
1332 pub tag: String,
1333 #[serde(default)]
1335 pub callback_ratio: String,
1336 #[serde(default)]
1338 pub callback_spread: String,
1339 #[serde(default)]
1341 pub active_px: String,
1342 #[serde(default, deserialize_with = "deserialize_empty_ustr_as_none")]
1344 pub ccy: Option<Ustr>,
1345 #[serde(default, deserialize_with = "deserialize_target_currency_as_none")]
1347 pub tgt_ccy: Option<OKXTargetCurrency>,
1348 #[serde(default)]
1350 pub fee: Option<String>,
1351 #[serde(default, deserialize_with = "deserialize_empty_ustr_as_none")]
1353 pub fee_ccy: Option<Ustr>,
1354 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
1356 pub advance_ord_type: Option<String>,
1357}
1358
1359#[derive(Clone, Debug, Default, Deserialize, Serialize, Builder)]
1361#[builder(default)]
1362#[builder(setter(into, strip_option))]
1363#[serde(rename_all = "camelCase")]
1364pub struct WsAttachAlgoOrdParams {
1365 #[serde(skip_serializing_if = "Option::is_none")]
1367 pub attach_algo_cl_ord_id: Option<String>,
1368 #[serde(skip_serializing_if = "Option::is_none")]
1370 pub sl_trigger_px: Option<String>,
1371 #[serde(skip_serializing_if = "Option::is_none")]
1373 pub sl_ord_px: Option<String>,
1374 #[serde(skip_serializing_if = "Option::is_none")]
1376 pub sl_trigger_px_type: Option<OKXTriggerType>,
1377 #[serde(skip_serializing_if = "Option::is_none")]
1379 pub tp_trigger_px: Option<String>,
1380 #[serde(skip_serializing_if = "Option::is_none")]
1382 pub tp_ord_px: Option<String>,
1383 #[serde(skip_serializing_if = "Option::is_none")]
1385 pub tp_trigger_px_type: Option<OKXTriggerType>,
1386 #[serde(skip_serializing_if = "Option::is_none")]
1388 pub callback_ratio: Option<String>,
1389 #[serde(skip_serializing_if = "Option::is_none")]
1391 pub callback_spread: Option<String>,
1392 #[serde(skip_serializing_if = "Option::is_none")]
1394 pub active_px: Option<String>,
1395 #[serde(skip_serializing_if = "Option::is_none")]
1397 pub new_callback_ratio: Option<String>,
1398 #[serde(skip_serializing_if = "Option::is_none")]
1400 pub new_callback_spread: Option<String>,
1401 #[serde(skip_serializing_if = "Option::is_none")]
1403 pub new_active_px: Option<String>,
1404}
1405
1406#[derive(Clone, Debug, Deserialize, Serialize, Builder)]
1408#[builder(setter(into, strip_option))]
1409#[serde(rename_all = "camelCase")]
1410pub struct WsPostOrderParams {
1411 #[builder(default)]
1413 #[serde(skip_serializing_if = "Option::is_none")]
1414 pub inst_type: Option<OKXInstrumentType>,
1415 pub inst_id_code: u64,
1417 pub td_mode: OKXTradeMode,
1419 #[builder(default)]
1421 #[serde(skip_serializing_if = "Option::is_none")]
1422 pub ccy: Option<Ustr>,
1423 #[builder(default)]
1425 #[serde(skip_serializing_if = "Option::is_none")]
1426 pub cl_ord_id: Option<String>,
1427 pub side: OKXSide,
1429 #[builder(default)]
1431 #[serde(skip_serializing_if = "Option::is_none")]
1432 pub pos_side: Option<OKXPositionSide>,
1433 pub ord_type: OKXOrderType,
1435 pub sz: String,
1437 #[builder(default)]
1439 #[serde(skip_serializing_if = "Option::is_none")]
1440 pub px: Option<String>,
1441 #[builder(default)]
1443 #[serde(rename = "pxUsd", skip_serializing_if = "Option::is_none")]
1444 pub px_usd: Option<String>,
1445 #[builder(default)]
1448 #[serde(rename = "pxVol", skip_serializing_if = "Option::is_none")]
1449 pub px_vol: Option<String>,
1450 #[builder(default)]
1452 #[serde(skip_serializing_if = "Option::is_none")]
1453 pub reduce_only: Option<bool>,
1454 #[builder(default)]
1456 #[serde(rename = "closePosition", skip_serializing_if = "Option::is_none")]
1457 pub close_position: Option<bool>,
1458 #[builder(default)]
1460 #[serde(skip_serializing_if = "Option::is_none")]
1461 pub tgt_ccy: Option<OKXTargetCurrency>,
1462 #[builder(default)]
1464 #[serde(skip_serializing_if = "Option::is_none")]
1465 pub trade_quote_ccy: Option<Ustr>,
1466 #[builder(default)]
1468 #[serde(skip_serializing_if = "Option::is_none")]
1469 pub tag: Option<String>,
1470 #[builder(default)]
1472 #[serde(skip_serializing_if = "Option::is_none")]
1473 pub attach_algo_ords: Option<Vec<WsAttachAlgoOrdParams>>,
1474 #[builder(default)]
1476 #[serde(skip_serializing_if = "Option::is_none")]
1477 pub outcome: Option<String>,
1478 #[builder(default)]
1483 #[serde(skip_serializing_if = "Option::is_none")]
1484 pub slippage_pct: Option<String>,
1485 #[builder(default)]
1487 #[serde(skip_serializing_if = "Option::is_none")]
1488 pub rpi_taker_access: Option<bool>,
1489 #[builder(default)]
1491 #[serde(skip_serializing_if = "Option::is_none")]
1492 pub rpi_px_round: Option<bool>,
1493}
1494
1495#[derive(Clone, Debug, Default, Deserialize, Serialize, Builder)]
1497#[builder(default)]
1498#[builder(setter(into, strip_option))]
1499#[serde(rename_all = "camelCase")]
1500pub struct WsCancelOrderParams {
1501 pub inst_id_code: u64,
1503 #[serde(skip_serializing_if = "Option::is_none")]
1505 pub ord_id: Option<String>,
1506 #[serde(skip_serializing_if = "Option::is_none")]
1508 pub cl_ord_id: Option<String>,
1509}
1510
1511#[derive(Clone, Debug, Default, Deserialize, Serialize, Builder)]
1513#[builder(default)]
1514#[builder(setter(into, strip_option))]
1515#[serde(rename_all = "camelCase")]
1516pub struct WsMassCancelParams {
1517 pub inst_type: OKXInstrumentType,
1519 pub inst_family: Ustr,
1521}
1522
1523#[derive(Clone, Debug, Default, Deserialize, Serialize, Builder)]
1525#[builder(default)]
1526#[builder(setter(into, strip_option))]
1527#[serde(rename_all = "camelCase")]
1528pub struct WsAmendOrderParams {
1529 pub inst_id_code: u64,
1531 #[serde(skip_serializing_if = "Option::is_none")]
1533 pub ord_id: Option<String>,
1534 #[serde(skip_serializing_if = "Option::is_none")]
1536 pub cl_ord_id: Option<String>,
1537 #[serde(skip_serializing_if = "Option::is_none")]
1539 pub req_id: Option<String>,
1540 #[serde(skip_serializing_if = "Option::is_none")]
1542 pub new_px: Option<String>,
1543 #[serde(rename = "newPxUsd", skip_serializing_if = "Option::is_none")]
1545 pub new_px_usd: Option<String>,
1546 #[serde(rename = "newPxVol", skip_serializing_if = "Option::is_none")]
1549 pub new_px_vol: Option<String>,
1550 #[serde(skip_serializing_if = "Option::is_none")]
1552 pub new_sz: Option<String>,
1553 #[serde(skip_serializing_if = "Option::is_none")]
1555 pub rpi_taker_access: Option<bool>,
1556 #[serde(skip_serializing_if = "Option::is_none")]
1558 pub rpi_px_round: Option<bool>,
1559}
1560
1561#[derive(Clone, Debug, Deserialize, Serialize, Builder)]
1563#[builder(setter(into, strip_option))]
1564#[serde(rename_all = "camelCase")]
1565pub struct WsPostAlgoOrderParams {
1566 pub inst_id_code: u64,
1568 pub td_mode: OKXTradeMode,
1570 pub side: OKXSide,
1572 pub ord_type: OKXAlgoOrderType,
1574 pub sz: String,
1576 #[builder(default)]
1578 #[serde(skip_serializing_if = "Option::is_none")]
1579 pub cl_ord_id: Option<String>,
1580 #[builder(default)]
1582 #[serde(skip_serializing_if = "Option::is_none")]
1583 pub pos_side: Option<OKXPositionSide>,
1584 #[serde(skip_serializing_if = "Option::is_none")]
1586 pub trigger_px: Option<String>,
1587 #[builder(default)]
1589 #[serde(skip_serializing_if = "Option::is_none")]
1590 pub trigger_px_type: Option<OKXTriggerType>,
1591 #[builder(default)]
1593 #[serde(skip_serializing_if = "Option::is_none")]
1594 pub order_px: Option<String>,
1595 #[builder(default)]
1597 #[serde(skip_serializing_if = "Option::is_none")]
1598 pub reduce_only: Option<bool>,
1599 #[builder(default)]
1601 #[serde(skip_serializing_if = "Option::is_none")]
1602 pub tag: Option<String>,
1603 #[builder(default)]
1605 #[serde(skip_serializing_if = "Option::is_none")]
1606 pub callback_ratio: Option<String>,
1607 #[builder(default)]
1609 #[serde(skip_serializing_if = "Option::is_none")]
1610 pub callback_spread: Option<String>,
1611 #[builder(default)]
1613 #[serde(skip_serializing_if = "Option::is_none")]
1614 pub active_px: Option<String>,
1615}
1616
1617#[derive(Clone, Debug, Deserialize, Serialize, Builder)]
1619#[builder(setter(into, strip_option))]
1620#[serde(rename_all = "camelCase")]
1621pub struct WsCancelAlgoOrderParams {
1622 pub inst_id_code: u64,
1624 #[builder(default)]
1626 #[serde(skip_serializing_if = "Option::is_none")]
1627 pub algo_id: Option<String>,
1628 #[builder(default)]
1630 #[serde(skip_serializing_if = "Option::is_none")]
1631 pub algo_cl_ord_id: Option<String>,
1632}
1633
1634#[cfg(test)]
1635mod tests {
1636 use nautilus_core::{string::secret::REDACTED, time::get_atomic_clock_realtime};
1637 use rstest::rstest;
1638 use rust_decimal::Decimal;
1639
1640 use super::*;
1641
1642 #[rstest]
1643 fn authentication_preserves_wire_values_and_redacts_debug() {
1644 let authentication = OKXAuthentication {
1645 op: "login",
1646 args: vec![OKXAuthenticationArg {
1647 api_key: SecretString::from("api-key-value"),
1648 passphrase: SecretString::from("passphrase-value"),
1649 timestamp: "1700000000".to_string(),
1650 sign: SecretString::from("signature-value"),
1651 }],
1652 };
1653
1654 let json = serde_json::to_value(&authentication).unwrap();
1655 let formatted = format!("{authentication:?}");
1656
1657 assert_eq!(json["op"], "login");
1658 assert_eq!(json["args"][0]["apiKey"], "api-key-value");
1659 assert_eq!(json["args"][0]["passphrase"], "passphrase-value");
1660 assert_eq!(json["args"][0]["sign"], "signature-value");
1661 assert!(formatted.contains(REDACTED));
1662 assert!(!formatted.contains("api-key-value"));
1663 assert!(!formatted.contains("passphrase-value"));
1664 assert!(!formatted.contains("signature-value"));
1665 }
1666 use crate::common::testing::load_test_json;
1667
1668 #[rstest]
1669 fn test_deserialize_websocket_arg() {
1670 let json_str = r#"{"channel":"instruments","instType":"SPOT"}"#;
1671
1672 let result: Result<OKXWebSocketArg, _> = serde_json::from_str(json_str);
1673 match result {
1674 Ok(arg) => {
1675 assert_eq!(arg.channel, OKXWsChannel::Instruments);
1676 assert_eq!(arg.inst_type, Some(OKXInstrumentType::Spot));
1677 assert_eq!(arg.inst_id, None);
1678 }
1679 Err(e) => {
1680 panic!("Failed to deserialize WebSocket arg: {e}");
1681 }
1682 }
1683 }
1684
1685 #[rstest]
1686 fn test_deserialize_subscribe_variant_direct() {
1687 #[derive(Debug, Deserialize)]
1688 #[serde(rename_all = "camelCase")]
1689 struct SubscribeMsg {
1690 event: String,
1691 arg: OKXWebSocketArg,
1692 conn_id: String,
1693 }
1694
1695 let json_str = r#"{"event":"subscribe","arg":{"channel":"instruments","instType":"SPOT"},"connId":"380cfa6a"}"#;
1696
1697 let result: Result<SubscribeMsg, _> = serde_json::from_str(json_str);
1698 match result {
1699 Ok(msg) => {
1700 assert_eq!(msg.event, "subscribe");
1701 assert_eq!(msg.arg.channel, OKXWsChannel::Instruments);
1702 assert_eq!(msg.conn_id, "380cfa6a");
1703 }
1704 Err(e) => {
1705 panic!("Failed to deserialize subscribe message directly: {e}");
1706 }
1707 }
1708 }
1709
1710 #[rstest]
1711 fn test_deserialize_subscribe_confirmation() {
1712 let json_str = r#"{"event":"subscribe","arg":{"channel":"instruments","instType":"SPOT"},"connId":"380cfa6a"}"#;
1713
1714 let result: Result<OKXWsFrame, _> = serde_json::from_str(json_str);
1715 match result {
1716 Ok(msg) => {
1717 if let OKXWsFrame::Subscription {
1718 event,
1719 arg,
1720 conn_id,
1721 ..
1722 } = msg
1723 {
1724 assert_eq!(event, OKXSubscriptionEvent::Subscribe);
1725 assert_eq!(arg.channel, OKXWsChannel::Instruments);
1726 assert_eq!(conn_id, "380cfa6a");
1727 } else {
1728 panic!("Expected Subscribe variant, was: {msg:?}");
1729 }
1730 }
1731 Err(e) => {
1732 panic!("Failed to deserialize subscription confirmation: {e}");
1733 }
1734 }
1735 }
1736
1737 #[rstest]
1738 fn test_deserialize_subscribe_with_inst_id() {
1739 let json_str = r#"{"event":"subscribe","arg":{"channel":"candle1m","instId":"ETH-USDT"},"connId":"358602f5"}"#;
1740
1741 let result: Result<OKXWsFrame, _> = serde_json::from_str(json_str);
1742 match result {
1743 Ok(msg) => {
1744 if let OKXWsFrame::Subscription {
1745 event,
1746 arg,
1747 conn_id,
1748 ..
1749 } = msg
1750 {
1751 assert_eq!(event, OKXSubscriptionEvent::Subscribe);
1752 assert_eq!(arg.channel, OKXWsChannel::Candle1Minute);
1753 assert_eq!(conn_id, "358602f5");
1754 } else {
1755 panic!("Expected Subscribe variant, was: {msg:?}");
1756 }
1757 }
1758 Err(e) => {
1759 panic!("Failed to deserialize subscription confirmation: {e}");
1760 }
1761 }
1762 }
1763
1764 #[rstest]
1765 fn test_channel_serialization_for_logging() {
1766 let channel = OKXWsChannel::Candle1Minute;
1767 let serialized = serde_json::to_string(&channel).unwrap();
1768 let cleaned = serialized.trim_matches('"').to_string();
1769 assert_eq!(cleaned, "candle1m");
1770
1771 let channel = OKXWsChannel::BboTbt;
1772 let serialized = serde_json::to_string(&channel).unwrap();
1773 let cleaned = serialized.trim_matches('"').to_string();
1774 assert_eq!(cleaned, "bbo-tbt");
1775
1776 let channel = OKXWsChannel::Trades;
1777 let serialized = serde_json::to_string(&channel).unwrap();
1778 let cleaned = serialized.trim_matches('"').to_string();
1779 assert_eq!(cleaned, "trades");
1780 }
1781
1782 #[rstest]
1783 fn test_order_response_with_enum_operation() {
1784 let json_str = r#"{"id":"req-123","op":"order","code":"0","msg":"","data":[]}"#;
1785 let result: Result<OKXWsFrame, _> = serde_json::from_str(json_str);
1786 match result {
1787 Ok(OKXWsFrame::OrderResponse {
1788 id,
1789 op,
1790 code,
1791 msg,
1792 data,
1793 }) => {
1794 assert_eq!(id, Some("req-123".to_string()));
1795 assert_eq!(op, OKXWsOperation::Order);
1796 assert_eq!(code, "0");
1797 assert_eq!(msg, "");
1798 assert!(data.is_empty());
1799 }
1800 Ok(other) => panic!("Expected OrderResponse, was: {other:?}"),
1801 Err(e) => panic!("Failed to deserialize: {e}"),
1802 }
1803
1804 let json_str = r#"{"id":"cancel-456","op":"cancel-order","code":"50001","msg":"Order not found","data":[]}"#;
1805 let result: Result<OKXWsFrame, _> = serde_json::from_str(json_str);
1806 match result {
1807 Ok(OKXWsFrame::OrderResponse {
1808 id,
1809 op,
1810 code,
1811 msg,
1812 data,
1813 }) => {
1814 assert_eq!(id, Some("cancel-456".to_string()));
1815 assert_eq!(op, OKXWsOperation::CancelOrder);
1816 assert_eq!(code, "50001");
1817 assert_eq!(msg, "Order not found");
1818 assert!(data.is_empty());
1819 }
1820 Ok(other) => panic!("Expected OrderResponse, was: {other:?}"),
1821 Err(e) => panic!("Failed to deserialize: {e}"),
1822 }
1823
1824 let json_str = r#"{"id":"amend-789","op":"amend-order","code":"50002","msg":"Invalid price","data":[]}"#;
1825 let result: Result<OKXWsFrame, _> = serde_json::from_str(json_str);
1826 match result {
1827 Ok(OKXWsFrame::OrderResponse {
1828 id,
1829 op,
1830 code,
1831 msg,
1832 data,
1833 }) => {
1834 assert_eq!(id, Some("amend-789".to_string()));
1835 assert_eq!(op, OKXWsOperation::AmendOrder);
1836 assert_eq!(code, "50002");
1837 assert_eq!(msg, "Invalid price");
1838 assert!(data.is_empty());
1839 }
1840 Ok(other) => panic!("Expected OrderResponse, was: {other:?}"),
1841 Err(e) => panic!("Failed to deserialize: {e}"),
1842 }
1843 }
1844
1845 #[rstest]
1846 fn test_operation_enum_serialization() {
1847 let op = OKXWsOperation::Order;
1848 let serialized = serde_json::to_string(&op).unwrap();
1849 assert_eq!(serialized, "\"order\"");
1850
1851 let op = OKXWsOperation::CancelOrder;
1852 let serialized = serde_json::to_string(&op).unwrap();
1853 assert_eq!(serialized, "\"cancel-order\"");
1854
1855 let op = OKXWsOperation::AmendOrder;
1856 let serialized = serde_json::to_string(&op).unwrap();
1857 assert_eq!(serialized, "\"amend-order\"");
1858
1859 let op = OKXWsOperation::Subscribe;
1860 let serialized = serde_json::to_string(&op).unwrap();
1861 assert_eq!(serialized, "\"subscribe\"");
1862 }
1863
1864 #[rstest]
1865 fn test_order_response_parsing() {
1866 let success_response = r#"{
1867 "id": "req-123",
1868 "op": "order",
1869 "code": "0",
1870 "msg": "",
1871 "data": [{"sMsg": "Order placed successfully"}]
1872 }"#;
1873
1874 let parsed: OKXWsFrame = serde_json::from_str(success_response).unwrap();
1875
1876 match parsed {
1877 OKXWsFrame::OrderResponse {
1878 id,
1879 op,
1880 code,
1881 msg,
1882 data,
1883 } => {
1884 assert_eq!(id, Some("req-123".to_string()));
1885 assert_eq!(op, OKXWsOperation::Order);
1886 assert_eq!(code, "0");
1887 assert_eq!(msg, "");
1888 assert_eq!(data.len(), 1);
1889 }
1890 _ => panic!("Expected OrderResponse variant"),
1891 }
1892
1893 let failure_response = r#"{
1894 "id": "req-456",
1895 "op": "cancel-order",
1896 "code": "50001",
1897 "msg": "Order not found",
1898 "data": [{"sMsg": "Order with client order ID not found"}]
1899 }"#;
1900
1901 let parsed: OKXWsFrame = serde_json::from_str(failure_response).unwrap();
1902
1903 match parsed {
1904 OKXWsFrame::OrderResponse {
1905 id,
1906 op,
1907 code,
1908 msg,
1909 data,
1910 } => {
1911 assert_eq!(id, Some("req-456".to_string()));
1912 assert_eq!(op, OKXWsOperation::CancelOrder);
1913 assert_eq!(code, "50001");
1914 assert_eq!(msg, "Order not found");
1915 assert_eq!(data.len(), 1);
1916 }
1917 _ => panic!("Expected OrderResponse variant"),
1918 }
1919 }
1920
1921 #[rstest]
1922 fn test_subscription_event_parsing() {
1923 let subscription_json = r#"{
1924 "event": "subscribe",
1925 "arg": {
1926 "channel": "tickers",
1927 "instId": "BTC-USDT"
1928 },
1929 "connId": "a4d3ae55"
1930 }"#;
1931
1932 let parsed: OKXWsFrame = serde_json::from_str(subscription_json).unwrap();
1933
1934 match parsed {
1935 OKXWsFrame::Subscription {
1936 event,
1937 arg,
1938 conn_id,
1939 ..
1940 } => {
1941 assert_eq!(
1942 event,
1943 crate::websocket::enums::OKXSubscriptionEvent::Subscribe
1944 );
1945 assert_eq!(arg.channel, OKXWsChannel::Tickers);
1946 assert_eq!(arg.inst_id, Some(Ustr::from("BTC-USDT")));
1947 assert_eq!(conn_id, "a4d3ae55");
1948 }
1949 _ => panic!("Expected Subscription variant"),
1950 }
1951 }
1952
1953 #[rstest]
1954 fn test_login_event_parsing() {
1955 let login_success = r#"{
1956 "event": "login",
1957 "code": "0",
1958 "msg": "Login successful",
1959 "connId": "a4d3ae55"
1960 }"#;
1961
1962 let parsed: OKXWsFrame = serde_json::from_str(login_success).unwrap();
1963
1964 match parsed {
1965 OKXWsFrame::Login {
1966 event,
1967 code,
1968 msg,
1969 conn_id,
1970 } => {
1971 assert_eq!(event, "login");
1972 assert_eq!(code, "0");
1973 assert_eq!(msg, "Login successful");
1974 assert_eq!(conn_id, "a4d3ae55");
1975 }
1976 _ => panic!("Expected Login variant, was: {parsed:?}"),
1977 }
1978 }
1979
1980 #[rstest]
1981 fn test_error_event_parsing() {
1982 let error_json = r#"{
1983 "code": "60012",
1984 "msg": "Invalid request"
1985 }"#;
1986
1987 let parsed: OKXWsFrame = serde_json::from_str(error_json).unwrap();
1988
1989 match parsed {
1990 OKXWsFrame::Error { arg, code, msg } => {
1991 assert!(arg.is_none());
1992 assert_eq!(code, "60012");
1993 assert_eq!(msg, "Invalid request");
1994 }
1995 _ => panic!("Expected Error variant"),
1996 }
1997 }
1998
1999 #[rstest]
2000 fn test_error_event_with_event_field_parsing() {
2001 let error_json = r#"{
2003 "event": "error",
2004 "code": "60018",
2005 "msg": "Invalid sign"
2006 }"#;
2007
2008 let parsed: OKXWsFrame = serde_json::from_str(error_json).unwrap();
2009
2010 match parsed {
2011 OKXWsFrame::Error { arg, code, msg } => {
2012 assert!(arg.is_none());
2013 assert_eq!(code, "60018");
2014 assert_eq!(msg, "Invalid sign");
2015 }
2016 _ => panic!("Expected Error variant, was: {parsed:?}"),
2017 }
2018 }
2019
2020 #[rstest]
2021 fn test_subscription_error_with_arg_field_parsing() {
2022 let error_json = r#"{
2024 "event": "error",
2025 "arg": {"channel": "tickers", "instId": "INVALID-INST"},
2026 "code": "60012",
2027 "msg": "Invalid request: channel not found",
2028 "connId": "a4d3ae55"
2029 }"#;
2030
2031 let parsed: OKXWsFrame = serde_json::from_str(error_json).unwrap();
2032
2033 match parsed {
2034 OKXWsFrame::Error { arg, code, msg } => {
2035 let arg = arg.expect("subscription error arg");
2036 assert_eq!(arg.channel, OKXWsChannel::Tickers);
2037 assert_eq!(arg.inst_id, Some(Ustr::from("INVALID-INST")));
2038 assert_eq!(code, "60012");
2039 assert_eq!(msg, "Invalid request: channel not found");
2040 }
2041 _ => panic!("Expected Error variant, was: {parsed:?}"),
2042 }
2043 }
2044
2045 #[rstest]
2046 fn test_websocket_request_serialization() {
2047 let request = OKXWsRequest {
2048 id: Some("req-123".to_string()),
2049 op: OKXWsOperation::Order,
2050 args: vec![serde_json::json!({
2051 "instId": "BTC-USDT",
2052 "tdMode": "cash",
2053 "side": "buy",
2054 "ordType": "market",
2055 "sz": "0.1"
2056 })],
2057 exp_time: None,
2058 };
2059
2060 let serialized = serde_json::to_string(&request).unwrap();
2061 let parsed: serde_json::Value = serde_json::from_str(&serialized).unwrap();
2062
2063 assert_eq!(parsed["id"], "req-123");
2064 assert_eq!(parsed["op"], "order");
2065 assert!(parsed["args"].is_array());
2066 assert_eq!(parsed["args"].as_array().unwrap().len(), 1);
2067 }
2068
2069 #[rstest]
2070 fn test_subscription_request_serialization() {
2071 let subscription = OKXSubscription {
2072 op: OKXWsOperation::Subscribe,
2073 args: vec![OKXSubscriptionArg {
2074 channel: OKXWsChannel::Tickers,
2075 inst_type: Some(OKXInstrumentType::Spot),
2076 inst_family: None,
2077 inst_id: Some(Ustr::from("BTC-USDT")),
2078 }],
2079 };
2080
2081 let serialized = serde_json::to_string(&subscription).unwrap();
2082 let parsed: serde_json::Value = serde_json::from_str(&serialized).unwrap();
2083
2084 assert_eq!(parsed["op"], "subscribe");
2085 assert!(parsed["args"].is_array());
2086 assert_eq!(parsed["args"][0]["channel"], "tickers");
2087 assert_eq!(parsed["args"][0]["instType"], "SPOT");
2088 assert_eq!(parsed["args"][0]["instId"], "BTC-USDT");
2089 }
2090
2091 #[rstest]
2092 fn test_error_message_extraction() {
2093 let responses = vec![
2094 (
2095 r#"{
2096 "id": "req-123",
2097 "op": "order",
2098 "code": "50001",
2099 "msg": "Order failed",
2100 "data": [{"sMsg": "Insufficient balance"}]
2101 }"#,
2102 "Insufficient balance",
2103 ),
2104 (
2105 r#"{
2106 "id": "req-456",
2107 "op": "cancel-order",
2108 "code": "50002",
2109 "msg": "Cancel failed",
2110 "data": [{}]
2111 }"#,
2112 "Cancel failed",
2113 ),
2114 ];
2115
2116 for (response_json, expected_msg) in responses {
2117 let parsed: OKXWsFrame = serde_json::from_str(response_json).unwrap();
2118
2119 match parsed {
2120 OKXWsFrame::OrderResponse {
2121 id: _,
2122 op: _,
2123 code,
2124 msg,
2125 data,
2126 } => {
2127 assert_ne!(code, "0"); let error_msg = data
2131 .first()
2132 .and_then(|d| d.get("sMsg"))
2133 .and_then(|s| s.as_str())
2134 .filter(|s| !s.is_empty())
2135 .unwrap_or(&msg);
2136
2137 assert_eq!(error_msg, expected_msg);
2138 }
2139 _ => panic!("Expected OrderResponse variant"),
2140 }
2141 }
2142 }
2143
2144 #[rstest]
2145 fn test_book_data_parsing() {
2146 let book_data_json = r#"{
2147 "arg": {
2148 "channel": "books",
2149 "instId": "BTC-USDT"
2150 },
2151 "action": "snapshot",
2152 "data": [{
2153 "asks": [["50000.0", "0.1", "0", "1"]],
2154 "bids": [["49999.0", "0.2", "0", "1"]],
2155 "ts": "1640995200000",
2156 "checksum": 123456789,
2157 "seqId": 1000
2158 }]
2159 }"#;
2160
2161 let parsed: OKXWsFrame = serde_json::from_str(book_data_json).unwrap();
2162
2163 match parsed {
2164 OKXWsFrame::BookData { arg, action, data } => {
2165 assert_eq!(arg.channel, OKXWsChannel::Books);
2166 assert_eq!(arg.inst_id, Some(Ustr::from("BTC-USDT")));
2167 assert_eq!(
2168 action,
2169 super::super::super::common::enums::OKXBookAction::Snapshot
2170 );
2171 assert_eq!(data.len(), 1);
2172 }
2173 _ => panic!("Expected BookData variant"),
2174 }
2175 }
2176
2177 #[rstest]
2178 #[case::arg_first(
2179 r#"{"arg":{"channel":"books","instId":"ETH-USDT"},"action":"update","data":[{"asks":[["3000.5","2","0","3"]],"bids":[["2999.5","4","1","5"]],"ts":"1640995200001","checksum":-42,"prevSeqId":1001,"seqId":1002}]}"#
2180 )]
2181 #[case::data_first(
2182 r#"{"data":[{"asks":[["3000.5","2","0","3"]],"bids":[["2999.5","4","1","5"]],"ts":"1640995200001","checksum":-42,"prevSeqId":1001,"seqId":1002}],"action":"update","arg":{"channel":"books","instId":"ETH-USDT"}}"#
2183 )]
2184 fn test_book_data_parsing_is_independent_of_key_order(#[case] json: &str) {
2185 let parsed: OKXWsFrame = serde_json::from_str(json).unwrap();
2186
2187 let OKXWsFrame::BookData { arg, action, data } = parsed else {
2188 panic!("Expected BookData variant");
2189 };
2190
2191 assert_eq!(arg.channel, OKXWsChannel::Books);
2192 assert_eq!(arg.inst_id, Some(Ustr::from("ETH-USDT")));
2193 assert_eq!(action, OKXBookAction::Update);
2194 assert_eq!(data.len(), 1);
2195 let msg = &data[0];
2196 assert_eq!(msg.asks.len(), 1);
2197 assert_eq!(msg.asks[0].price, "3000.5");
2198 assert_eq!(msg.asks[0].size, "2");
2199 assert_eq!(msg.asks[0].liquidated_orders_count, "0");
2200 assert_eq!(msg.asks[0].orders_count, "3");
2201 assert_eq!(msg.bids.len(), 1);
2202 assert_eq!(msg.bids[0].price, "2999.5");
2203 assert_eq!(msg.bids[0].size, "4");
2204 assert_eq!(msg.bids[0].liquidated_orders_count, "1");
2205 assert_eq!(msg.bids[0].orders_count, "5");
2206 assert_eq!(msg.checksum, Some(-42));
2207 assert_eq!(msg.prev_seq_id, Some(1001));
2208 assert_eq!(msg.seq_id, 1002);
2209 assert_eq!(msg.ts, 1_640_995_200_001);
2210 }
2211
2212 #[rstest]
2213 #[case::op_first(
2214 r#"{"id":"req-7","op":"cancel-order","data":[{"sCode":"51400","sMsg":"Cancellation failed"}],"code":"1","msg":"Operation failed"}"#
2215 )]
2216 #[case::data_first(
2217 r#"{"data":[{"sCode":"51400","sMsg":"Cancellation failed"}],"code":"1","msg":"Operation failed","op":"cancel-order","id":"req-7"}"#
2218 )]
2219 fn test_order_response_parsing_is_independent_of_key_order(#[case] json: &str) {
2220 let parsed: OKXWsFrame = serde_json::from_str(json).unwrap();
2221
2222 let OKXWsFrame::OrderResponse {
2223 id,
2224 op,
2225 code,
2226 msg,
2227 data,
2228 } = parsed
2229 else {
2230 panic!("Expected OrderResponse variant");
2231 };
2232
2233 assert_eq!(id, Some("req-7".to_string()));
2234 assert_eq!(op, OKXWsOperation::CancelOrder);
2235 assert_eq!(code, "1");
2236 assert_eq!(msg, "Operation failed");
2237 assert_eq!(
2238 data,
2239 vec![serde_json::json!({"sCode": "51400", "sMsg": "Cancellation failed"})]
2240 );
2241 }
2242
2243 #[rstest]
2244 #[case::op_first(r#"{"op":"order","data":null,"event":"error","code":"60012","msg":"Bad"}"#)]
2245 #[case::event_first(r#"{"event":"error","code":"60012","msg":"Bad","op":"order","data":null}"#)]
2246 fn test_error_event_takes_priority_over_op_in_any_key_order(#[case] json: &str) {
2247 let parsed: OKXWsFrame = serde_json::from_str(json).unwrap();
2248
2249 let OKXWsFrame::Error { arg, code, msg } = parsed else {
2250 panic!("Expected Error variant");
2251 };
2252
2253 assert!(arg.is_none());
2254 assert_eq!(code, "60012");
2255 assert_eq!(msg, "Bad");
2256 }
2257
2258 #[rstest]
2259 fn test_rpi_book_fixtures_preserve_depth_types_and_sequence() {
2260 let snapshot: OKXWsFrame =
2261 serde_json::from_str(&load_test_json("ws_books_rpi_snapshot.json")).unwrap();
2262 let update: OKXWsFrame =
2263 serde_json::from_str(&load_test_json("ws_books_rpi_update.json")).unwrap();
2264
2265 let OKXWsFrame::RpiBookData { arg, action, data } = snapshot else {
2266 panic!("Expected RPI book snapshot");
2267 };
2268 let snapshot = &data[0];
2269 assert_eq!(arg.channel, OKXWsChannel::BooksRpi);
2270 assert_eq!(arg.inst_id, Some(Ustr::from("OMI-USD")));
2271 assert_eq!(action, OKXBookAction::Snapshot);
2272 assert_eq!(data.len(), 1);
2273 assert_eq!(snapshot.asks.len(), 4);
2274 assert_eq!(snapshot.bids.len(), 10);
2275 assert_eq!(
2276 snapshot.asks[0],
2277 OKXRpiBookLevel(
2278 Decimal::from_str_exact("0.0001617").unwrap(),
2279 Decimal::from_str_exact("12325166.992").unwrap(),
2280 Decimal::from(1000),
2281 2,
2282 )
2283 );
2284 assert_eq!(snapshot.prev_seq_id, -1);
2285 assert_eq!(snapshot.seq_id, 1_082_831_226);
2286 assert_eq!(snapshot.ts, 1_785_406_442_403);
2287
2288 let OKXWsFrame::RpiBookData { arg, action, data } = update else {
2289 panic!("Expected RPI book update");
2290 };
2291 let update = &data[0];
2292 assert_eq!(arg.channel, OKXWsChannel::BooksRpi);
2293 assert_eq!(arg.inst_id, Some(Ustr::from("OMI-USD")));
2294 assert_eq!(action, OKXBookAction::Update);
2295 assert_eq!(data.len(), 1);
2296 assert_eq!(update.asks.len(), 2);
2297 assert!(update.bids.is_empty());
2298 assert_eq!(
2299 update.asks[1],
2300 OKXRpiBookLevel(
2301 Decimal::from_str_exact("0.0001625").unwrap(),
2302 Decimal::from_str_exact("12324367.786").unwrap(),
2303 Decimal::from(1000),
2304 2,
2305 )
2306 );
2307 assert_eq!(update.prev_seq_id, snapshot.seq_id as i64);
2308 assert_eq!(update.seq_id, 1_082_831_230);
2309 assert_eq!(update.ts, 1_785_406_443_903);
2310 }
2311
2312 #[rstest]
2313 fn test_rpi_book_rejects_checksum_field() {
2314 let mut payload: serde_json::Value =
2315 serde_json::from_str(&load_test_json("ws_books_rpi_update.json")).unwrap();
2316 payload["data"][0]["checksum"] = serde_json::json!(0);
2317
2318 let error = serde_json::from_value::<OKXWsFrame>(payload).unwrap_err();
2319
2320 assert!(error.to_string().contains("checksum"));
2321 }
2322
2323 #[rstest]
2324 fn test_data_event_parsing() {
2325 let data_json = r#"{
2326 "arg": {
2327 "channel": "trades",
2328 "instId": "BTC-USDT"
2329 },
2330 "data": [{
2331 "instId": "BTC-USDT",
2332 "tradeId": "12345",
2333 "px": "50000.0",
2334 "sz": "0.1",
2335 "side": "buy",
2336 "ts": "1640995200000"
2337 }]
2338 }"#;
2339
2340 let parsed: OKXWsFrame = serde_json::from_str(data_json).unwrap();
2341
2342 match parsed {
2343 OKXWsFrame::Data { arg, data } => {
2344 assert_eq!(arg.channel, OKXWsChannel::Trades);
2345 assert_eq!(arg.inst_id, Some(Ustr::from("BTC-USDT")));
2346 assert!(data.is_array());
2347 }
2348 _ => panic!("Expected Data variant"),
2349 }
2350 }
2351
2352 #[rstest]
2353 fn test_nautilus_message_variants() {
2354 let clock = get_atomic_clock_realtime();
2355 let ts_init = clock.get_time_ns();
2356
2357 let error = OKXWebSocketError {
2358 code: "60012".to_string(),
2359 message: "Invalid request".to_string(),
2360 conn_id: None,
2361 timestamp: ts_init.as_u64(),
2362 };
2363 let error_msg = NautilusWsMessage::Error(error);
2364
2365 match error_msg {
2366 NautilusWsMessage::Error(e) => {
2367 assert_eq!(e.code, "60012");
2368 assert_eq!(e.message, "Invalid request");
2369 }
2370 _ => panic!("Expected Error variant"),
2371 }
2372
2373 let raw_scenarios = vec![
2374 ::serde_json::json!({"unknown": "data"}),
2375 ::serde_json::json!({"channel": "unsupported", "data": [1, 2, 3]}),
2376 ::serde_json::json!({"complex": {"nested": {"structure": true}}}),
2377 ];
2378
2379 for raw_data in raw_scenarios {
2380 let raw_msg = NautilusWsMessage::Raw(raw_data.clone());
2381
2382 match raw_msg {
2383 NautilusWsMessage::Raw(data) => {
2384 assert_eq!(data, raw_data);
2385 }
2386 _ => panic!("Expected Raw variant"),
2387 }
2388 }
2389 }
2390
2391 #[rstest]
2392 fn test_order_response_parsing_success() {
2393 let order_response_json = r#"{
2394 "id": "req-123",
2395 "op": "order",
2396 "code": "0",
2397 "msg": "",
2398 "data": [{"sMsg": "Order placed successfully"}]
2399 }"#;
2400
2401 let parsed: OKXWsFrame = serde_json::from_str(order_response_json).unwrap();
2402
2403 match parsed {
2404 OKXWsFrame::OrderResponse {
2405 id,
2406 op,
2407 code,
2408 msg,
2409 data,
2410 } => {
2411 assert_eq!(id, Some("req-123".to_string()));
2412 assert_eq!(op, OKXWsOperation::Order);
2413 assert_eq!(code, "0");
2414 assert_eq!(msg, "");
2415 assert_eq!(data.len(), 1);
2416 }
2417 _ => panic!("Expected OrderResponse variant"),
2418 }
2419 }
2420
2421 #[rstest]
2422 fn test_order_response_parsing_failure() {
2423 let order_response_json = r#"{
2424 "id": "req-456",
2425 "op": "cancel-order",
2426 "code": "50001",
2427 "msg": "Order not found",
2428 "data": [{"sMsg": "Order with client order ID not found"}]
2429 }"#;
2430
2431 let parsed: OKXWsFrame = serde_json::from_str(order_response_json).unwrap();
2432
2433 match parsed {
2434 OKXWsFrame::OrderResponse {
2435 id,
2436 op,
2437 code,
2438 msg,
2439 data,
2440 } => {
2441 assert_eq!(id, Some("req-456".to_string()));
2442 assert_eq!(op, OKXWsOperation::CancelOrder);
2443 assert_eq!(code, "50001");
2444 assert_eq!(msg, "Order not found");
2445 assert_eq!(data.len(), 1);
2446 }
2447 _ => panic!("Expected OrderResponse variant"),
2448 }
2449 }
2450
2451 #[rstest]
2452 fn test_message_request_serialization() {
2453 let request = OKXWsRequest {
2454 id: Some("req-123".to_string()),
2455 op: OKXWsOperation::Order,
2456 args: vec![::serde_json::json!({
2457 "instId": "BTC-USDT",
2458 "tdMode": "cash",
2459 "side": "buy",
2460 "ordType": "market",
2461 "sz": "0.1"
2462 })],
2463 exp_time: None,
2464 };
2465
2466 let serialized = serde_json::to_string(&request).unwrap();
2467 let parsed: serde_json::Value = serde_json::from_str(&serialized).unwrap();
2468
2469 assert_eq!(parsed["id"], "req-123");
2470 assert_eq!(parsed["op"], "order");
2471 assert!(parsed["args"].is_array());
2472 assert_eq!(parsed["args"].as_array().unwrap().len(), 1);
2473 }
2474
2475 #[rstest]
2476 fn test_ws_post_order_params_serializes_inst_id_code() {
2477 use super::WsPostOrderParamsBuilder;
2478 use crate::common::enums::{OKXOrderType, OKXSide, OKXTradeMode};
2479
2480 let params = WsPostOrderParamsBuilder::default()
2481 .inst_id_code(10459u64)
2482 .td_mode(OKXTradeMode::Cross)
2483 .side(OKXSide::Buy)
2484 .ord_type(OKXOrderType::Limit)
2485 .sz("0.01".to_string())
2486 .px("50000".to_string())
2487 .build()
2488 .unwrap();
2489
2490 let json = serde_json::to_string(¶ms).unwrap();
2491
2492 assert!(json.contains("\"instIdCode\":10459"));
2493 assert!(!json.contains("\"instId\""));
2494 }
2495
2496 #[rstest]
2497 fn test_ws_post_order_params_serializes_slippage_pct() {
2498 use super::WsPostOrderParamsBuilder;
2499 use crate::common::enums::{OKXOrderType, OKXSide, OKXTradeMode};
2500
2501 let params = WsPostOrderParamsBuilder::default()
2502 .inst_id_code(10459u64)
2503 .td_mode(OKXTradeMode::Cross)
2504 .side(OKXSide::Buy)
2505 .ord_type(OKXOrderType::Market)
2506 .sz("0.01".to_string())
2507 .slippage_pct("0.005".to_string())
2508 .build()
2509 .unwrap();
2510
2511 let json: serde_json::Value = serde_json::to_value(¶ms).unwrap();
2512 assert_eq!(json["slippagePct"], "0.005");
2513 }
2514
2515 #[rstest]
2516 fn test_ws_post_order_params_omits_slippage_pct_when_unset() {
2517 use super::WsPostOrderParamsBuilder;
2518 use crate::common::enums::{OKXOrderType, OKXSide, OKXTradeMode};
2519
2520 let params = WsPostOrderParamsBuilder::default()
2521 .inst_id_code(10459u64)
2522 .td_mode(OKXTradeMode::Cross)
2523 .side(OKXSide::Buy)
2524 .ord_type(OKXOrderType::Market)
2525 .sz("0.01".to_string())
2526 .build()
2527 .unwrap();
2528
2529 let json = serde_json::to_string(¶ms).unwrap();
2530 assert!(!json.contains("slippagePct"));
2531 assert!(!json.contains("tradeQuoteCcy"));
2532 }
2533
2534 #[rstest]
2535 fn test_ws_post_order_params_serializes_trade_quote_ccy_usd() {
2536 use super::WsPostOrderParamsBuilder;
2537 use crate::common::enums::{OKXOrderType, OKXSide, OKXTradeMode};
2538
2539 let params = WsPostOrderParamsBuilder::default()
2540 .inst_id_code(20459u64)
2541 .td_mode(OKXTradeMode::Cash)
2542 .side(OKXSide::Buy)
2543 .ord_type(OKXOrderType::Limit)
2544 .sz("0.01".to_string())
2545 .px("100000".to_string())
2546 .trade_quote_ccy("USD")
2547 .build()
2548 .unwrap();
2549
2550 let json: serde_json::Value = serde_json::to_value(¶ms).unwrap();
2551 assert_eq!(json["instIdCode"], 20459);
2552 assert_eq!(json["tradeQuoteCcy"], "USD");
2553 assert!(json.get("instId").is_none());
2554 }
2555
2556 #[rstest]
2557 fn test_ws_post_order_params_serializes_trade_quote_ccy_usdc() {
2558 use super::WsPostOrderParamsBuilder;
2559 use crate::common::enums::{OKXOrderType, OKXSide, OKXTradeMode};
2560
2561 let params = WsPostOrderParamsBuilder::default()
2562 .inst_id_code(20459u64)
2563 .td_mode(OKXTradeMode::Cash)
2564 .side(OKXSide::Buy)
2565 .ord_type(OKXOrderType::Limit)
2566 .sz("0.01".to_string())
2567 .px("100000".to_string())
2568 .trade_quote_ccy("USDC")
2569 .build()
2570 .unwrap();
2571
2572 let json: serde_json::Value = serde_json::to_value(¶ms).unwrap();
2573 assert_eq!(json["tradeQuoteCcy"], "USDC");
2574 }
2575
2576 #[rstest]
2577 fn test_ws_post_order_params_serializes_attached_tp_sl() {
2578 use super::{WsAttachAlgoOrdParamsBuilder, WsPostOrderParamsBuilder};
2579 use crate::common::enums::{OKXOrderType, OKXSide, OKXTradeMode, OKXTriggerType};
2580
2581 let params = WsPostOrderParamsBuilder::default()
2582 .inst_id_code(10459u64)
2583 .td_mode(OKXTradeMode::Cross)
2584 .side(OKXSide::Buy)
2585 .ord_type(OKXOrderType::Limit)
2586 .sz("0.01".to_string())
2587 .px("50000".to_string())
2588 .attach_algo_ords(vec![
2589 WsAttachAlgoOrdParamsBuilder::default()
2590 .attach_algo_cl_ord_id("O-bracket-sl")
2591 .sl_trigger_px("39000")
2592 .sl_ord_px("-1")
2593 .sl_trigger_px_type(OKXTriggerType::Last)
2594 .build()
2595 .unwrap(),
2596 WsAttachAlgoOrdParamsBuilder::default()
2597 .attach_algo_cl_ord_id("O-bracket-tp")
2598 .tp_trigger_px("41000")
2599 .tp_ord_px("-1")
2600 .tp_trigger_px_type(OKXTriggerType::Last)
2601 .build()
2602 .unwrap(),
2603 ])
2604 .build()
2605 .unwrap();
2606
2607 let json = serde_json::to_string(¶ms).unwrap();
2608
2609 assert!(json.contains("\"attachAlgoOrds\""));
2610 assert!(json.contains("\"attachAlgoClOrdId\":\"O-bracket-sl\""));
2611 assert!(json.contains("\"slTriggerPx\":\"39000\""));
2612 assert!(json.contains("\"slOrdPx\":\"-1\""));
2613 assert!(json.contains("\"attachAlgoClOrdId\":\"O-bracket-tp\""));
2614 assert!(json.contains("\"tpTriggerPx\":\"41000\""));
2615 assert!(json.contains("\"tpOrdPx\":\"-1\""));
2616 }
2617
2618 #[rstest]
2619 fn test_ws_cancel_order_params_serializes_inst_id_code() {
2620 use super::WsCancelOrderParamsBuilder;
2621
2622 let params = WsCancelOrderParamsBuilder::default()
2623 .inst_id_code(10461u64)
2624 .ord_id("12345678".to_string())
2625 .build()
2626 .unwrap();
2627
2628 let json = serde_json::to_string(¶ms).unwrap();
2629
2630 assert!(json.contains("\"instIdCode\":10461"));
2631 assert!(!json.contains("\"instId\""));
2632 assert!(json.contains("\"ordId\":\"12345678\""));
2633 }
2634
2635 #[rstest]
2636 fn test_ws_amend_order_params_serializes_inst_id_code() {
2637 use super::WsAmendOrderParamsBuilder;
2638
2639 let params = WsAmendOrderParamsBuilder::default()
2640 .inst_id_code(10459u64)
2641 .cl_ord_id("client123".to_string())
2642 .new_px("51000".to_string())
2643 .build()
2644 .unwrap();
2645
2646 let json = serde_json::to_string(¶ms).unwrap();
2647
2648 assert!(json.contains("\"instIdCode\":10459"));
2649 assert!(!json.contains("\"instId\""));
2650 assert!(json.contains("\"newPx\":\"51000\""));
2651 }
2652
2653 #[rstest]
2654 fn test_ws_post_algo_order_params_serializes_inst_id_code() {
2655 use super::WsPostAlgoOrderParamsBuilder;
2656 use crate::common::enums::{OKXAlgoOrderType, OKXSide, OKXTradeMode, OKXTriggerType};
2657
2658 let params = WsPostAlgoOrderParamsBuilder::default()
2659 .inst_id_code(10459u64)
2660 .td_mode(OKXTradeMode::Cross)
2661 .side(OKXSide::Buy)
2662 .ord_type(OKXAlgoOrderType::Trigger)
2663 .sz("0.01".to_string())
2664 .trigger_px("48000".to_string())
2665 .trigger_px_type(OKXTriggerType::Last)
2666 .build()
2667 .unwrap();
2668
2669 let json = serde_json::to_string(¶ms).unwrap();
2670
2671 assert!(json.contains("\"instIdCode\":10459"));
2672 assert!(!json.contains("\"instId\""));
2673 assert!(json.contains("\"triggerPx\":\"48000\""));
2674 }
2675
2676 #[rstest]
2677 fn test_ws_cancel_algo_order_params_serializes_inst_id_code() {
2678 let params = WsCancelAlgoOrderParams {
2679 inst_id_code: 10459,
2680 algo_id: Some("987654321".to_string()),
2681 algo_cl_ord_id: None,
2682 };
2683
2684 let json = serde_json::to_string(¶ms).unwrap();
2685
2686 assert!(json.contains("\"instIdCode\":10459"));
2687 assert!(!json.contains("\"instId\""));
2688 assert!(json.contains("\"algoId\":\"987654321\""));
2689 }
2690
2691 #[rstest]
2692 fn test_ws_cancel_algo_order_params_builder_allows_either_identifier() {
2693 use super::WsCancelAlgoOrderParamsBuilder;
2694
2695 let by_cl_ord_id = WsCancelAlgoOrderParamsBuilder::default()
2696 .inst_id_code(10459u64)
2697 .algo_cl_ord_id("Odstalgocancel0000001".to_string())
2698 .build()
2699 .unwrap();
2700 let json = serde_json::to_value(&by_cl_ord_id).unwrap();
2701 assert_eq!(json["algoClOrdId"], "Odstalgocancel0000001");
2702 assert!(json.get("algoId").is_none());
2703
2704 let by_algo_id = WsCancelAlgoOrderParamsBuilder::default()
2705 .inst_id_code(10459u64)
2706 .algo_id("987654321".to_string())
2707 .build()
2708 .unwrap();
2709 let json = serde_json::to_value(&by_algo_id).unwrap();
2710 assert_eq!(json["algoId"], "987654321");
2711 assert!(json.get("algoClOrdId").is_none());
2712 }
2713
2714 #[rstest]
2715 fn test_ws_post_order_params_serializes_px_usd() {
2716 use super::WsPostOrderParamsBuilder;
2717 use crate::common::enums::{OKXOrderType, OKXSide, OKXTradeMode};
2718
2719 let params = WsPostOrderParamsBuilder::default()
2720 .inst_id_code(10459u64)
2721 .td_mode(OKXTradeMode::Cross)
2722 .side(OKXSide::Buy)
2723 .ord_type(OKXOrderType::Limit)
2724 .sz("1".to_string())
2725 .px_usd("100.5".to_string())
2726 .build()
2727 .unwrap();
2728
2729 let json = serde_json::to_string(¶ms).unwrap();
2730 assert!(json.contains("\"pxUsd\":\"100.5\""));
2731 assert!(!json.contains("\"pxVol\""));
2732 assert!(!json.contains("\"px\":"));
2733 }
2734
2735 #[rstest]
2736 fn test_ws_post_order_params_serializes_px_vol() {
2737 use super::WsPostOrderParamsBuilder;
2738 use crate::common::enums::{OKXOrderType, OKXSide, OKXTradeMode};
2739
2740 let params = WsPostOrderParamsBuilder::default()
2741 .inst_id_code(10459u64)
2742 .td_mode(OKXTradeMode::Cross)
2743 .side(OKXSide::Buy)
2744 .ord_type(OKXOrderType::Limit)
2745 .sz("1".to_string())
2746 .px_vol("0.55".to_string())
2747 .build()
2748 .unwrap();
2749
2750 let json = serde_json::to_string(¶ms).unwrap();
2751 assert!(json.contains("\"pxVol\":\"0.55\""));
2752 assert!(!json.contains("\"pxUsd\""));
2753 assert!(!json.contains("\"px\":"));
2754 }
2755
2756 #[rstest]
2757 fn test_ws_amend_order_params_serializes_new_px_usd() {
2758 use super::WsAmendOrderParamsBuilder;
2759
2760 let params = WsAmendOrderParamsBuilder::default()
2761 .inst_id_code(10459u64)
2762 .cl_ord_id("client123".to_string())
2763 .new_px_usd("105.0".to_string())
2764 .build()
2765 .unwrap();
2766
2767 let json = serde_json::to_string(¶ms).unwrap();
2768 assert!(json.contains("\"newPxUsd\":\"105.0\""));
2769 assert!(!json.contains("\"newPx\":"));
2770 assert!(!json.contains("\"newPxVol\""));
2771 }
2772
2773 #[rstest]
2774 fn test_ws_amend_order_params_serializes_new_px_vol() {
2775 use super::WsAmendOrderParamsBuilder;
2776
2777 let params = WsAmendOrderParamsBuilder::default()
2778 .inst_id_code(10459u64)
2779 .cl_ord_id("client123".to_string())
2780 .new_px_vol("0.60".to_string())
2781 .build()
2782 .unwrap();
2783
2784 let json = serde_json::to_string(¶ms).unwrap();
2785 assert!(json.contains("\"newPxVol\":\"0.60\""));
2786 assert!(!json.contains("\"newPx\":"));
2787 assert!(!json.contains("\"newPxUsd\""));
2788 }
2789
2790 #[rstest]
2791 fn test_ws_event_contract_markets_channel_serialization() {
2792 let json = serde_json::to_string(&OKXWsChannel::EventContractMarkets).unwrap();
2793 let channel: OKXWsChannel = serde_json::from_str(&json).unwrap();
2794
2795 assert_eq!(json, "\"event-contract-markets\"");
2796 assert_eq!(channel, OKXWsChannel::EventContractMarkets);
2797 }
2798
2799 #[rstest]
2800 fn test_ws_post_order_params_serializes_event_contract_fields() {
2801 use super::WsPostOrderParamsBuilder;
2802 use crate::common::enums::{OKXOrderType, OKXSide, OKXTradeMode};
2803
2804 let params = WsPostOrderParamsBuilder::default()
2805 .inst_id_code(10459u64)
2806 .td_mode(OKXTradeMode::Cash)
2807 .side(OKXSide::Buy)
2808 .ord_type(OKXOrderType::Limit)
2809 .sz("10".to_string())
2810 .px("0.42".to_string())
2811 .outcome("yes")
2812 .build()
2813 .unwrap();
2814
2815 let json: serde_json::Value = serde_json::to_value(¶ms).unwrap();
2816
2817 assert!(json.get("speedBump").is_none());
2818 assert_eq!(json["outcome"], "yes");
2819 }
2820
2821 #[rstest]
2822 fn test_ws_amend_order_params_omits_speed_bump() {
2823 use super::WsAmendOrderParamsBuilder;
2824
2825 let params = WsAmendOrderParamsBuilder::default()
2826 .inst_id_code(10459u64)
2827 .cl_ord_id("event-1".to_string())
2828 .new_px("0.43".to_string())
2829 .build()
2830 .unwrap();
2831
2832 let json: serde_json::Value = serde_json::to_value(¶ms).unwrap();
2833
2834 assert_eq!(
2835 json,
2836 serde_json::json!({
2837 "instIdCode": 10459,
2838 "clOrdId": "event-1",
2839 "newPx": "0.43",
2840 })
2841 );
2842 }
2843
2844 #[rstest]
2845 fn test_ws_attach_algo_ord_params_serializes_trailing_fields() {
2846 use super::WsAttachAlgoOrdParamsBuilder;
2847
2848 let params = WsAttachAlgoOrdParamsBuilder::default()
2849 .attach_algo_cl_ord_id("trail-1")
2850 .callback_ratio("0.01")
2851 .active_px("64000")
2852 .new_callback_ratio("0.02")
2853 .new_callback_spread("25")
2854 .new_active_px("65000")
2855 .build()
2856 .unwrap();
2857
2858 let json: serde_json::Value = serde_json::to_value(¶ms).unwrap();
2859
2860 assert_eq!(json["callbackRatio"], "0.01");
2861 assert_eq!(json["activePx"], "64000");
2862 assert_eq!(json["newCallbackRatio"], "0.02");
2863 assert_eq!(json["newCallbackSpread"], "25");
2864 assert_eq!(json["newActivePx"], "65000");
2865 assert!(json.get("callbackSpread").is_none());
2866 }
2867
2868 #[rstest]
2869 fn test_subscription_arg_serializes_sprd_id_for_spread_channels() {
2870 let arg = OKXSubscriptionArg {
2871 channel: OKXWsChannel::SprdBooks5,
2872 inst_type: None,
2873 inst_family: None,
2874 inst_id: Some(Ustr::from("ETH-USD-260925_ETH-USD-261225")),
2875 };
2876 let json = serde_json::to_value(&arg).unwrap();
2877 assert_eq!(json["channel"], "sprd-books5");
2878 assert_eq!(json["sprdId"], "ETH-USD-260925_ETH-USD-261225");
2879 assert!(json.get("instId").is_none());
2880 }
2881
2882 #[rstest]
2883 fn test_subscription_arg_serializes_inst_id_for_standard_channels() {
2884 let arg = OKXSubscriptionArg {
2885 channel: OKXWsChannel::BboTbt,
2886 inst_type: None,
2887 inst_family: None,
2888 inst_id: Some(Ustr::from("BTC-USDT")),
2889 };
2890 let json = serde_json::to_value(&arg).unwrap();
2891 assert_eq!(json["instId"], "BTC-USDT");
2892 assert!(json.get("sprdId").is_none());
2893 }
2894
2895 #[rstest]
2896 fn test_websocket_arg_resolves_sprd_id_into_inst_id() {
2897 let arg: OKXWebSocketArg = serde_json::from_value(serde_json::json!({
2898 "channel": "sprd-bbo-tbt",
2899 "sprdId": "ETH-USD-260925_ETH-USD-261225",
2900 }))
2901 .unwrap();
2902 assert_eq!(arg.channel, OKXWsChannel::SprdBboTbt);
2903 assert_eq!(
2904 arg.inst_id,
2905 Some(Ustr::from("ETH-USD-260925_ETH-USD-261225"))
2906 );
2907 }
2908
2909 #[rstest]
2910 fn test_book_msg_parses_three_element_spread_levels() {
2911 let msg: OKXBookMsg = serde_json::from_value(serde_json::json!({
2914 "asks": [["16.7", "100", "1"]],
2915 "bids": [["16.65", "100", "1"]],
2916 "ts": "1780044924909",
2917 "seqId": 1_779_935_772_619_784_u64,
2918 }))
2919 .unwrap();
2920 assert_eq!(msg.asks[0].price, "16.7");
2921 assert_eq!(msg.asks[0].size, "100");
2922 assert_eq!(msg.bids[0].price, "16.65");
2923 }
2924
2925 #[rstest]
2926 fn test_trade_msg_parses_spread_public_trade() {
2927 let msg: OKXTradeMsg = serde_json::from_value(serde_json::json!({
2929 "sprdId": "ETH-USD-260925_ETH-USD-261225",
2930 "tradeId": "3392538740127301632",
2931 "px": "16.9",
2932 "sz": "100",
2933 "side": "sell",
2934 "ts": "1780047866507",
2935 }))
2936 .unwrap();
2937 assert_eq!(msg.inst_id, Ustr::from("ETH-USD-260925_ETH-USD-261225"));
2938 assert_eq!(msg.px, "16.9");
2939 assert_eq!(msg.side, OKXSide::Sell);
2940 assert!(msg.count.is_empty());
2941 }
2942}