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 OKXAlgoOrderStatus, OKXAlgoOrderType, OKXBookAction, OKXCandleConfirm, OKXExecType,
37 OKXInstrumentType, OKXOrderCategory, OKXOrderStatus, OKXOrderType, OKXPositionSide,
38 OKXPriceType, OKXQuickMarginType, OKXSelfTradePreventionMode, OKXSettlementState,
39 OKXSide, 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 http::models::OKXSpreadOrder,
48 websocket::enums::OKXSubscriptionEvent,
49};
50
51#[derive(Debug, Clone)]
52pub enum NautilusWsMessage {
53 Data(Vec<Data>),
54 Deltas(OrderBookDeltas),
55 FundingRates(Vec<FundingRateUpdate>),
56 Instrument(Box<InstrumentAny>, Option<InstrumentStatus>),
57 InstrumentStatus(InstrumentStatus),
58 AccountUpdate(AccountState),
59 PositionUpdate(PositionStatusReport),
60 OrderAccepted(OrderAccepted),
61 OrderCanceled(OrderCanceled),
62 OrderExpired(OrderExpired),
63 OrderRejected(OrderRejected),
64 OrderCancelRejected(OrderCancelRejected),
65 OrderModifyRejected(OrderModifyRejected),
66 OrderTriggered(OrderTriggered),
67 OrderUpdated(OrderUpdated),
68 ExecutionReports(Vec<ExecutionReport>),
69 Error(OKXWebSocketError),
70 Raw(serde_json::Value), Reconnected,
72 Authenticated,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
77#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object))]
78#[cfg_attr(
79 feature = "python",
80 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.okx")
81)]
82pub struct OKXWebSocketError {
83 pub code: String,
85 pub message: String,
87 pub conn_id: Option<String>,
89 pub timestamp: u64,
91}
92
93#[derive(Debug, Clone)]
94#[expect(clippy::large_enum_variant)]
95pub enum ExecutionReport {
96 Order(OrderStatusReport),
97 Fill(FillReport),
98}
99
100#[derive(Debug)]
106pub enum OKXWsMessage {
107 BookData {
109 arg: OKXWebSocketArg,
110 action: OKXBookAction,
111 data: Vec<OKXBookMsg>,
112 },
113 ChannelData {
115 channel: OKXWsChannel,
116 inst_id: Option<Ustr>,
117 data: serde_json::Value,
118 },
119 OrderResponse {
121 id: Option<String>,
122 op: OKXWsOperation,
123 code: String,
124 msg: String,
125 data: Vec<serde_json::Value>,
126 },
127 Orders(Vec<OKXOrderMsg>),
129 SpreadOrders(Vec<OKXSpreadOrder>),
131 AlgoOrders(Vec<OKXAlgoOrderMsg>),
133 Account(serde_json::Value),
135 Positions(serde_json::Value),
137 Instruments(Vec<OKXInstrument>),
139 SendFailed {
141 request_id: String,
142 client_order_id: Option<ClientOrderId>,
143 op: Option<OKXWsOperation>,
144 error: String,
145 },
146 Error(OKXWebSocketError),
148 Reconnected,
150 Authenticated,
152}
153
154#[derive(Debug, Serialize)]
156#[serde(rename_all = "camelCase")]
157pub struct OKXWsRequest<T> {
158 #[serde(skip_serializing_if = "Option::is_none")]
160 pub id: Option<String>,
161 pub op: OKXWsOperation,
163 #[serde(skip_serializing_if = "Option::is_none")]
166 pub exp_time: Option<String>,
167 pub args: Vec<T>,
169}
170
171#[derive(Debug, Serialize)]
173pub struct OKXAuthentication {
174 pub op: &'static str,
175 pub args: Vec<OKXAuthenticationArg>,
176}
177
178#[derive(Debug, Serialize)]
180#[serde(rename_all = "camelCase")]
181pub struct OKXAuthenticationArg {
182 pub api_key: String,
183 pub passphrase: String,
184 pub timestamp: String,
185 pub sign: String,
186}
187
188#[derive(Debug, Serialize)]
189pub struct OKXSubscription {
190 pub op: OKXWsOperation,
191 pub args: Vec<OKXSubscriptionArg>,
192}
193
194#[derive(Clone, Debug)]
195pub struct OKXSubscriptionArg {
196 pub channel: OKXWsChannel,
197 pub inst_type: Option<OKXInstrumentType>,
198 pub inst_family: Option<Ustr>,
199 pub inst_id: Option<Ustr>,
200}
201
202impl Serialize for OKXSubscriptionArg {
203 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
204 use serde::ser::SerializeMap;
205
206 let mut map = serializer.serialize_map(None)?;
207 map.serialize_entry("channel", &self.channel)?;
208
209 if let Some(inst_type) = &self.inst_type {
210 map.serialize_entry("instType", inst_type)?;
211 }
212
213 if let Some(inst_family) = &self.inst_family {
214 map.serialize_entry("instFamily", inst_family)?;
215 }
216
217 if let Some(inst_id) = &self.inst_id {
218 let key = if self.channel.is_spread() {
219 "sprdId"
220 } else {
221 "instId"
222 };
223 map.serialize_entry(key, inst_id)?;
224 }
225
226 map.end()
227 }
228}
229
230#[derive(Debug)]
235pub enum OKXWsFrame {
236 Login {
237 event: String,
238 code: String,
239 msg: String,
240 conn_id: String,
241 },
242 Subscription {
243 event: OKXSubscriptionEvent,
244 arg: OKXWebSocketArg,
245 conn_id: String,
246 code: Option<String>,
247 msg: Option<String>,
248 },
249 ChannelConnCount {
250 event: String,
251 channel: OKXWsChannel,
252 conn_count: String,
253 conn_id: String,
254 },
255 OrderResponse {
256 id: Option<String>,
257 op: OKXWsOperation,
258 code: String,
259 msg: String,
260 data: Vec<serde_json::Value>,
261 },
262 BookData {
263 arg: OKXWebSocketArg,
264 action: OKXBookAction,
265 data: Vec<OKXBookMsg>,
266 },
267 Data {
268 arg: OKXWebSocketArg,
269 data: serde_json::Value,
270 },
271 Error {
272 code: String,
273 msg: String,
274 },
275 Ping,
276 Reconnected,
277}
278
279impl<'de> Deserialize<'de> for OKXWsFrame {
280 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
281 where
282 D: serde::Deserializer<'de>,
283 {
284 use serde::de::Error;
285
286 let mut value = serde_json::Value::deserialize(deserializer)?;
291 let obj = value
292 .as_object_mut()
293 .ok_or_else(|| D::Error::custom("expected JSON object for OKXWsFrame"))?;
294
295 if let Some(event) = obj.get("event").and_then(|v| v.as_str()) {
301 match event {
302 "login" => return parse_login(obj),
303 "subscribe" | "unsubscribe" => return parse_subscription(obj),
304 "error" => return parse_error(obj),
305 _ if obj.contains_key("channel") && obj.contains_key("connCount") => {
306 return parse_channel_conn_count(obj);
307 }
308 _ => {}
309 }
310 }
311
312 if obj.contains_key("op") {
314 return parse_order_response(obj);
315 }
316
317 if obj.contains_key("action") && obj.contains_key("arg") {
319 return parse_book_data(obj);
320 }
321
322 if obj.contains_key("arg") && obj.contains_key("data") {
324 return parse_data(obj);
325 }
326
327 if obj.contains_key("code") && obj.contains_key("msg") {
329 return parse_error(obj);
330 }
331
332 Err(D::Error::custom(format!(
336 "cannot determine OKXWsFrame variant from: {}",
337 serde_json::to_string(&value).unwrap_or_default()
338 )))
339 }
340}
341
342#[inline]
343fn take_str<E: serde::de::Error>(
344 obj: &mut serde_json::Map<String, serde_json::Value>,
345 key: &'static str,
346) -> Result<String, E> {
347 match obj.remove(key) {
348 Some(serde_json::Value::String(s)) => Ok(s),
349 Some(_) => Err(E::custom(format!("field `{key}` is not a string"))),
350 None => Err(E::missing_field(key)),
351 }
352}
353
354#[inline]
355fn take_optional_str(
356 obj: &mut serde_json::Map<String, serde_json::Value>,
357 key: &'static str,
358) -> Option<String> {
359 match obj.remove(key) {
360 Some(serde_json::Value::String(s)) => Some(s),
361 _ => None,
362 }
363}
364
365fn parse_login<E: serde::de::Error>(
366 obj: &mut serde_json::Map<String, serde_json::Value>,
367) -> Result<OKXWsFrame, E> {
368 Ok(OKXWsFrame::Login {
369 event: take_str(obj, "event")?,
370 code: take_str(obj, "code")?,
371 msg: take_str(obj, "msg")?,
372 conn_id: take_str(obj, "connId")?,
373 })
374}
375
376fn parse_subscription<E: serde::de::Error>(
377 obj: &mut serde_json::Map<String, serde_json::Value>,
378) -> Result<OKXWsFrame, E> {
379 let event_val = obj
380 .remove("event")
381 .ok_or_else(|| E::missing_field("event"))?;
382 let event: OKXSubscriptionEvent =
383 serde_json::from_value(event_val).map_err(|e| E::custom(format!("invalid event: {e}")))?;
384
385 let arg_val = obj.remove("arg").ok_or_else(|| E::missing_field("arg"))?;
386 let arg: OKXWebSocketArg =
387 serde_json::from_value(arg_val).map_err(|e| E::custom(format!("invalid arg: {e}")))?;
388
389 Ok(OKXWsFrame::Subscription {
390 event,
391 arg,
392 conn_id: take_str(obj, "connId")?,
393 code: take_optional_str(obj, "code"),
394 msg: take_optional_str(obj, "msg"),
395 })
396}
397
398fn parse_channel_conn_count<E: serde::de::Error>(
399 obj: &mut serde_json::Map<String, serde_json::Value>,
400) -> Result<OKXWsFrame, E> {
401 let channel_val = obj
402 .remove("channel")
403 .ok_or_else(|| E::missing_field("channel"))?;
404 let channel: OKXWsChannel = serde_json::from_value(channel_val)
405 .map_err(|e| E::custom(format!("invalid channel: {e}")))?;
406
407 Ok(OKXWsFrame::ChannelConnCount {
408 event: take_str(obj, "event")?,
409 channel,
410 conn_count: take_str(obj, "connCount")?,
411 conn_id: take_str(obj, "connId")?,
412 })
413}
414
415fn parse_order_response<E: serde::de::Error>(
416 obj: &mut serde_json::Map<String, serde_json::Value>,
417) -> Result<OKXWsFrame, E> {
418 let op_val = obj.remove("op").ok_or_else(|| E::missing_field("op"))?;
419 let op: OKXWsOperation =
420 serde_json::from_value(op_val).map_err(|e| E::custom(format!("invalid op: {e}")))?;
421
422 let data: Vec<serde_json::Value> = match obj.remove("data") {
423 Some(v) => {
424 serde_json::from_value(v).map_err(|e| E::custom(format!("invalid data: {e}")))?
425 }
426 None => Vec::new(),
427 };
428
429 Ok(OKXWsFrame::OrderResponse {
430 id: take_optional_str(obj, "id"),
431 op,
432 code: take_str(obj, "code")?,
433 msg: take_str(obj, "msg")?,
434 data,
435 })
436}
437
438fn parse_book_data<E: serde::de::Error>(
439 obj: &mut serde_json::Map<String, serde_json::Value>,
440) -> Result<OKXWsFrame, E> {
441 let arg_val = obj.remove("arg").ok_or_else(|| E::missing_field("arg"))?;
442 let arg: OKXWebSocketArg =
443 serde_json::from_value(arg_val).map_err(|e| E::custom(format!("invalid arg: {e}")))?;
444
445 let action_val = obj
446 .remove("action")
447 .ok_or_else(|| E::missing_field("action"))?;
448 let action: OKXBookAction = serde_json::from_value(action_val)
449 .map_err(|e| E::custom(format!("invalid action: {e}")))?;
450
451 let data_val = obj.remove("data").ok_or_else(|| E::missing_field("data"))?;
452 let data: Vec<OKXBookMsg> =
453 serde_json::from_value(data_val).map_err(|e| E::custom(format!("invalid data: {e}")))?;
454
455 Ok(OKXWsFrame::BookData { arg, action, data })
456}
457
458fn parse_data<E: serde::de::Error>(
459 obj: &mut serde_json::Map<String, serde_json::Value>,
460) -> Result<OKXWsFrame, E> {
461 let arg_val = obj.remove("arg").ok_or_else(|| E::missing_field("arg"))?;
462 let arg: OKXWebSocketArg =
463 serde_json::from_value(arg_val).map_err(|e| E::custom(format!("invalid arg: {e}")))?;
464
465 let data = obj.remove("data").ok_or_else(|| E::missing_field("data"))?;
466
467 Ok(OKXWsFrame::Data { arg, data })
468}
469
470fn parse_error<E: serde::de::Error>(
471 obj: &mut serde_json::Map<String, serde_json::Value>,
472) -> Result<OKXWsFrame, E> {
473 Ok(OKXWsFrame::Error {
474 code: take_str(obj, "code")?,
475 msg: take_str(obj, "msg")?,
476 })
477}
478
479#[derive(Debug, Serialize, Deserialize)]
480#[serde(rename_all = "camelCase")]
481pub struct OKXWebSocketArg {
482 pub channel: OKXWsChannel,
484 #[serde(default, alias = "sprdId")]
488 pub inst_id: Option<Ustr>,
489 #[serde(default)]
490 pub inst_type: Option<OKXInstrumentType>,
491 #[serde(default)]
492 pub inst_family: Option<Ustr>,
493 #[serde(default)]
494 pub bar: Option<Ustr>,
495}
496
497#[derive(Debug, Serialize, Deserialize)]
499#[serde(rename_all = "camelCase")]
500pub struct OKXTickerMsg {
501 pub inst_type: OKXInstrumentType,
503 pub inst_id: Ustr,
505 #[serde(rename = "last")]
507 pub last_px: String,
508 pub last_sz: String,
510 pub ask_px: String,
512 pub ask_sz: String,
514 pub bid_px: String,
516 pub bid_sz: String,
518 pub open24h: String,
520 pub high24h: String,
522 pub low24h: String,
524 pub vol_ccy_24h: String,
526 pub vol24h: String,
528 pub sod_utc0: String,
530 pub sod_utc8: String,
532 #[serde(deserialize_with = "deserialize_string_to_u64")]
534 pub ts: u64,
535 #[serde(default)]
537 pub source: Option<String>,
538}
539
540#[derive(Debug, Serialize, Deserialize)]
542pub struct OrderBookEntry {
543 pub price: String,
545 pub size: String,
547 #[serde(default)]
552 pub liquidated_orders_count: String,
553 #[serde(default)]
555 pub orders_count: String,
556}
557
558#[derive(Debug, Serialize, Deserialize)]
560#[serde(rename_all = "camelCase")]
561pub struct OKXBookMsg {
562 pub asks: Vec<OrderBookEntry>,
564 pub bids: Vec<OrderBookEntry>,
566 pub checksum: Option<i64>,
568 pub prev_seq_id: Option<i64>,
570 pub seq_id: u64,
572 #[serde(deserialize_with = "deserialize_string_to_u64")]
574 pub ts: u64,
575}
576
577#[derive(Debug, Serialize, Deserialize)]
579#[serde(rename_all = "camelCase")]
580pub struct OKXTradeMsg {
581 #[serde(default, alias = "sprdId")]
586 pub inst_id: Ustr,
587 pub trade_id: String,
589 pub px: String,
591 pub sz: String,
593 pub side: OKXSide,
595 #[serde(default)]
597 pub count: String,
598 #[serde(deserialize_with = "deserialize_string_to_u64")]
600 pub ts: u64,
601 #[serde(default)]
603 pub source: Option<String>,
604 #[serde(default)]
606 pub seq_id: Option<u64>,
607}
608
609#[derive(Debug, Serialize, Deserialize)]
611#[serde(rename_all = "camelCase")]
612pub struct OKXFundingRateMsg {
613 #[serde(default)]
615 pub inst_type: Option<OKXInstrumentType>,
616 pub inst_id: Ustr,
618 pub funding_rate: Ustr,
620 pub next_funding_rate: Ustr,
622 #[serde(default)]
624 pub min_funding_rate: Option<String>,
625 #[serde(default)]
627 pub max_funding_rate: Option<String>,
628 #[serde(default)]
630 pub sett_state: OKXSettlementState,
631 #[serde(default)]
633 pub sett_funding_rate: Option<String>,
634 #[serde(default)]
636 pub premium: Option<String>,
637 #[serde(default)]
639 pub method: Option<String>,
640 #[serde(deserialize_with = "deserialize_string_to_u64")]
642 pub funding_time: u64,
643 #[serde(deserialize_with = "deserialize_string_to_u64")]
645 pub next_funding_time: u64,
646 #[serde(deserialize_with = "deserialize_string_to_u64")]
648 pub ts: u64,
649}
650
651#[derive(Debug, Serialize, Deserialize)]
653#[serde(rename_all = "camelCase")]
654pub struct OKXMarkPriceMsg {
655 pub inst_id: Ustr,
657 pub mark_px: String,
659 #[serde(deserialize_with = "deserialize_string_to_u64")]
661 pub ts: u64,
662}
663
664#[derive(Debug, Serialize, Deserialize)]
666#[serde(rename_all = "camelCase")]
667pub struct OKXIndexPriceMsg {
668 pub inst_id: Ustr,
670 pub idx_px: String,
672 pub high24h: String,
674 pub low24h: String,
676 pub open24h: String,
678 pub sod_utc0: String,
680 pub sod_utc8: String,
682 #[serde(deserialize_with = "deserialize_string_to_u64")]
684 pub ts: u64,
685}
686
687#[derive(Debug, Serialize, Deserialize)]
689#[serde(rename_all = "camelCase")]
690pub struct OKXPriceLimitMsg {
691 pub inst_id: Ustr,
693 pub buy_lmt: String,
695 pub sell_lmt: String,
697 #[serde(deserialize_with = "deserialize_string_to_u64")]
699 pub ts: u64,
700}
701
702#[derive(Debug, Serialize, Deserialize)]
704#[serde(rename_all = "camelCase")]
705pub struct OKXCandleMsg {
706 #[serde(deserialize_with = "deserialize_string_to_u64")]
708 pub ts: u64,
709 pub o: String,
711 pub h: String,
713 pub l: String,
715 pub c: String,
717 pub vol: String,
719 pub vol_ccy: String,
721 pub vol_ccy_quote: String,
722 pub confirm: OKXCandleConfirm,
724}
725
726#[derive(Debug, Serialize, Deserialize)]
728#[serde(rename_all = "camelCase")]
729pub struct OKXOpenInterestMsg {
730 pub inst_id: Ustr,
732 pub oi: String,
734 pub oi_ccy: String,
736 #[serde(deserialize_with = "deserialize_string_to_u64")]
738 pub ts: u64,
739}
740
741#[derive(Debug, Serialize, Deserialize)]
743#[serde(rename_all = "camelCase")]
744pub struct OKXOptionSummaryMsg {
745 #[serde(default)]
747 pub inst_type: Option<OKXInstrumentType>,
748 pub inst_id: Ustr,
750 pub uly: String,
752 pub delta: String,
754 pub gamma: String,
756 pub theta: String,
758 pub vega: String,
760 #[serde(alias = "deltaBS")]
762 pub delta_bs: String,
763 #[serde(alias = "gammaBS")]
765 pub gamma_bs: String,
766 #[serde(alias = "thetaBS")]
768 pub theta_bs: String,
769 #[serde(alias = "vegaBS")]
771 pub vega_bs: String,
772 pub real_vol: String,
774 pub bid_vol: String,
776 pub ask_vol: String,
778 pub mark_vol: String,
780 pub lever: String,
782 #[serde(default)]
784 pub fwd_px: Option<String>,
785 #[serde(default)]
787 pub mark_px: Option<String>,
788 #[serde(default)]
790 pub vol_lv: Option<String>,
791 #[serde(deserialize_with = "deserialize_string_to_u64")]
793 pub ts: u64,
794}
795
796#[derive(Debug, Serialize, Deserialize)]
798#[serde(rename_all = "camelCase")]
799pub struct OKXEstimatedPriceMsg {
800 pub inst_id: Ustr,
802 pub settle_px: String,
804 #[serde(deserialize_with = "deserialize_string_to_u64")]
806 pub ts: u64,
807}
808
809#[derive(Debug, Serialize, Deserialize)]
811#[serde(rename_all = "camelCase")]
812pub struct OKXStatusMsg {
813 pub title: Ustr,
815 #[serde(rename = "type")]
817 pub status_type: Ustr,
818 pub state: Ustr,
820 pub end_time: Option<String>,
822 pub begin_time: Option<String>,
824 pub service_type: Option<Ustr>,
826 pub reason: Option<String>,
828 #[serde(deserialize_with = "deserialize_string_to_u64")]
830 pub ts: u64,
831}
832
833pub use crate::common::models::OKXAttachedAlgoOrd;
834
835#[derive(Clone, Debug, Default, Serialize, Deserialize)]
837#[serde(rename_all = "camelCase")]
838pub struct OKXLinkedAlgoOrd {
839 #[serde(default)]
841 pub algo_id: String,
842}
843
844#[derive(Clone, Debug, Serialize, Deserialize)]
846#[serde(rename_all = "camelCase")]
847pub struct OKXOrderMsg {
848 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
850 pub acc_fill_sz: Option<String>,
851 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
853 pub algo_id: Option<String>,
854 pub avg_px: String,
856 #[serde(deserialize_with = "deserialize_string_to_u64")]
858 pub c_time: u64,
859 #[serde(default)]
861 pub cancel_source: Option<String>,
862 #[serde(default)]
864 pub cancel_source_reason: Option<String>,
865 pub category: OKXOrderCategory,
867 pub ccy: Ustr,
869 pub cl_ord_id: String,
871 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
873 pub algo_cl_ord_id: Option<String>,
874 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
876 pub attach_algo_cl_ord_id: Option<String>,
877 #[serde(default)]
879 pub attach_algo_ords: Vec<OKXAttachedAlgoOrd>,
880 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
882 pub outcome: Option<String>,
883 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
885 pub fee: Option<String>,
886 pub fee_ccy: Ustr,
888 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
890 pub fill_fee: Option<String>,
891 #[serde(default, deserialize_with = "deserialize_empty_ustr_as_none")]
893 pub fill_fee_ccy: Option<Ustr>,
894 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
896 pub fill_mark_px: Option<String>,
897 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
899 pub fill_mark_vol: Option<String>,
900 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
902 pub fill_px_vol: Option<String>,
903 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
905 pub fill_px_usd: Option<String>,
906 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
908 pub fill_fwd_px: Option<String>,
909 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
911 pub fill_notional_usd: Option<String>,
912 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
914 pub fill_pnl: Option<String>,
915 pub fill_px: String,
917 pub fill_sz: String,
919 #[serde(deserialize_with = "deserialize_string_to_u64")]
921 pub fill_time: u64,
922 pub inst_id: Ustr,
924 pub inst_type: OKXInstrumentType,
926 #[serde(default)]
928 pub is_tp_limit: Option<String>,
929 pub lever: String,
931 #[serde(default)]
933 pub linked_algo_ord: Option<OKXLinkedAlgoOrd>,
934 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
936 pub notional_usd: Option<String>,
937 pub ord_id: Ustr,
939 pub ord_type: OKXOrderType,
941 pub pnl: String,
943 pub pos_side: OKXPositionSide,
945 #[serde(default)]
947 pub px: String,
948 #[serde(default)]
950 pub px_type: OKXPriceType,
951 #[serde(default)]
953 pub px_usd: Option<String>,
954 #[serde(default)]
956 pub px_vol: Option<String>,
957 #[serde(default)]
959 pub quick_mgn_type: OKXQuickMarginType,
960 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
962 pub rebate: Option<String>,
963 #[serde(default, deserialize_with = "deserialize_empty_ustr_as_none")]
965 pub rebate_ccy: Option<Ustr>,
966 pub reduce_only: String,
968 pub side: OKXSide,
970 #[serde(default)]
972 pub sl_ord_px: Option<String>,
973 #[serde(default)]
975 pub sl_trigger_px: Option<String>,
976 #[serde(default)]
978 pub sl_trigger_px_type: Option<OKXTriggerType>,
979 #[serde(default)]
981 pub source: Option<String>,
982 pub state: OKXOrderStatus,
984 #[serde(default)]
986 pub stp_id: Option<String>,
987 #[serde(default)]
989 pub stp_mode: OKXSelfTradePreventionMode,
990 pub exec_type: OKXExecType,
992 pub sz: String,
994 #[serde(default)]
996 pub tag: Option<String>,
997 pub td_mode: OKXTradeMode,
999 #[serde(default, deserialize_with = "deserialize_target_currency_as_none")]
1001 pub tgt_ccy: Option<OKXTargetCurrency>,
1002 #[serde(default)]
1004 pub tp_ord_px: Option<String>,
1005 #[serde(default)]
1007 pub tp_trigger_px: Option<String>,
1008 #[serde(default)]
1010 pub tp_trigger_px_type: Option<OKXTriggerType>,
1011 pub trade_id: String,
1013 #[serde(deserialize_with = "deserialize_string_to_u64")]
1015 pub u_time: u64,
1016 #[serde(default)]
1018 pub amend_result: Option<String>,
1019 #[serde(default)]
1021 pub req_id: Option<String>,
1022 #[serde(default)]
1024 pub code: Option<String>,
1025 #[serde(default)]
1027 pub msg: Option<String>,
1028}
1029
1030#[derive(Clone, Debug, Deserialize, Serialize)]
1032#[serde(rename_all = "camelCase")]
1033pub struct OKXAlgoOrderMsg {
1034 pub algo_id: String,
1036 #[serde(default)]
1038 pub algo_cl_ord_id: String,
1039 pub cl_ord_id: String,
1041 pub ord_id: String,
1043 pub inst_id: Ustr,
1045 pub inst_type: OKXInstrumentType,
1047 pub ord_type: OKXAlgoOrderType,
1049 pub state: OKXAlgoOrderStatus,
1051 pub side: OKXSide,
1053 pub pos_side: OKXPositionSide,
1055 #[serde(default)]
1057 pub sz: String,
1058 #[serde(default)]
1060 pub trigger_px: String,
1061 #[serde(default)]
1063 pub trigger_px_type: OKXTriggerType,
1064 #[serde(default)]
1066 pub sl_trigger_px: String,
1067 #[serde(default)]
1069 pub sl_ord_px: String,
1070 #[serde(default)]
1072 pub sl_trigger_px_type: OKXTriggerType,
1073 #[serde(default)]
1075 pub tp_trigger_px: String,
1076 #[serde(default)]
1078 pub tp_ord_px: String,
1079 #[serde(default)]
1081 pub tp_trigger_px_type: OKXTriggerType,
1082 #[serde(default)]
1084 pub ord_px: String,
1085 pub td_mode: OKXTradeMode,
1087 pub lever: String,
1089 #[serde(default)]
1091 pub reduce_only: String,
1092 #[serde(default)]
1094 pub close_fraction: String,
1095 #[serde(default)]
1097 pub actual_px: String,
1098 #[serde(default)]
1100 pub actual_sz: String,
1101 #[serde(default)]
1103 pub notional_usd: String,
1104 #[serde(deserialize_with = "deserialize_string_to_u64")]
1106 pub c_time: u64,
1107 #[serde(deserialize_with = "deserialize_string_to_u64")]
1109 pub u_time: u64,
1110 #[serde(default)]
1112 pub trigger_time: String,
1113 #[serde(default)]
1115 pub tag: String,
1116 #[serde(default)]
1118 pub callback_ratio: String,
1119 #[serde(default)]
1121 pub callback_spread: String,
1122 #[serde(default)]
1124 pub active_px: String,
1125 #[serde(default, deserialize_with = "deserialize_empty_ustr_as_none")]
1127 pub ccy: Option<Ustr>,
1128 #[serde(default, deserialize_with = "deserialize_target_currency_as_none")]
1130 pub tgt_ccy: Option<OKXTargetCurrency>,
1131 #[serde(default)]
1133 pub fee: Option<String>,
1134 #[serde(default, deserialize_with = "deserialize_empty_ustr_as_none")]
1136 pub fee_ccy: Option<Ustr>,
1137 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
1139 pub advance_ord_type: Option<String>,
1140}
1141
1142#[derive(Clone, Debug, Default, Deserialize, Serialize, Builder)]
1144#[builder(default)]
1145#[builder(setter(into, strip_option))]
1146#[serde(rename_all = "camelCase")]
1147pub struct WsAttachAlgoOrdParams {
1148 #[serde(skip_serializing_if = "Option::is_none")]
1150 pub attach_algo_cl_ord_id: Option<String>,
1151 #[serde(skip_serializing_if = "Option::is_none")]
1153 pub sl_trigger_px: Option<String>,
1154 #[serde(skip_serializing_if = "Option::is_none")]
1156 pub sl_ord_px: Option<String>,
1157 #[serde(skip_serializing_if = "Option::is_none")]
1159 pub sl_trigger_px_type: Option<OKXTriggerType>,
1160 #[serde(skip_serializing_if = "Option::is_none")]
1162 pub tp_trigger_px: Option<String>,
1163 #[serde(skip_serializing_if = "Option::is_none")]
1165 pub tp_ord_px: Option<String>,
1166 #[serde(skip_serializing_if = "Option::is_none")]
1168 pub tp_trigger_px_type: Option<OKXTriggerType>,
1169 #[serde(skip_serializing_if = "Option::is_none")]
1171 pub callback_ratio: Option<String>,
1172 #[serde(skip_serializing_if = "Option::is_none")]
1174 pub callback_spread: Option<String>,
1175 #[serde(skip_serializing_if = "Option::is_none")]
1177 pub active_px: Option<String>,
1178 #[serde(skip_serializing_if = "Option::is_none")]
1180 pub new_callback_ratio: Option<String>,
1181 #[serde(skip_serializing_if = "Option::is_none")]
1183 pub new_callback_spread: Option<String>,
1184 #[serde(skip_serializing_if = "Option::is_none")]
1186 pub new_active_px: Option<String>,
1187}
1188
1189#[derive(Clone, Debug, Deserialize, Serialize, Builder)]
1191#[builder(setter(into, strip_option))]
1192#[serde(rename_all = "camelCase")]
1193pub struct WsPostOrderParams {
1194 #[builder(default)]
1196 #[serde(skip_serializing_if = "Option::is_none")]
1197 pub inst_type: Option<OKXInstrumentType>,
1198 pub inst_id_code: u64,
1200 pub td_mode: OKXTradeMode,
1202 #[builder(default)]
1204 #[serde(skip_serializing_if = "Option::is_none")]
1205 pub ccy: Option<Ustr>,
1206 #[builder(default)]
1208 #[serde(skip_serializing_if = "Option::is_none")]
1209 pub cl_ord_id: Option<String>,
1210 pub side: OKXSide,
1212 #[builder(default)]
1214 #[serde(skip_serializing_if = "Option::is_none")]
1215 pub pos_side: Option<OKXPositionSide>,
1216 pub ord_type: OKXOrderType,
1218 pub sz: String,
1220 #[builder(default)]
1222 #[serde(skip_serializing_if = "Option::is_none")]
1223 pub px: Option<String>,
1224 #[builder(default)]
1226 #[serde(rename = "pxUsd", skip_serializing_if = "Option::is_none")]
1227 pub px_usd: Option<String>,
1228 #[builder(default)]
1231 #[serde(rename = "pxVol", skip_serializing_if = "Option::is_none")]
1232 pub px_vol: Option<String>,
1233 #[builder(default)]
1235 #[serde(skip_serializing_if = "Option::is_none")]
1236 pub reduce_only: Option<bool>,
1237 #[builder(default)]
1239 #[serde(rename = "closePosition", skip_serializing_if = "Option::is_none")]
1240 pub close_position: Option<bool>,
1241 #[builder(default)]
1243 #[serde(skip_serializing_if = "Option::is_none")]
1244 pub tgt_ccy: Option<OKXTargetCurrency>,
1245 #[builder(default)]
1247 #[serde(skip_serializing_if = "Option::is_none")]
1248 pub tag: Option<String>,
1249 #[builder(default)]
1251 #[serde(skip_serializing_if = "Option::is_none")]
1252 pub attach_algo_ords: Option<Vec<WsAttachAlgoOrdParams>>,
1253 #[builder(default)]
1255 #[serde(skip_serializing_if = "Option::is_none")]
1256 pub speed_bump: Option<String>,
1257 #[builder(default)]
1259 #[serde(skip_serializing_if = "Option::is_none")]
1260 pub outcome: Option<String>,
1261 #[builder(default)]
1266 #[serde(skip_serializing_if = "Option::is_none")]
1267 pub slippage_pct: Option<String>,
1268}
1269
1270#[derive(Clone, Debug, Default, Deserialize, Serialize, Builder)]
1272#[builder(default)]
1273#[builder(setter(into, strip_option))]
1274#[serde(rename_all = "camelCase")]
1275pub struct WsCancelOrderParams {
1276 pub inst_id_code: u64,
1278 #[serde(skip_serializing_if = "Option::is_none")]
1280 pub ord_id: Option<String>,
1281 #[serde(skip_serializing_if = "Option::is_none")]
1283 pub cl_ord_id: Option<String>,
1284}
1285
1286#[derive(Clone, Debug, Default, Deserialize, Serialize, Builder)]
1288#[builder(default)]
1289#[builder(setter(into, strip_option))]
1290#[serde(rename_all = "camelCase")]
1291pub struct WsMassCancelParams {
1292 pub inst_type: OKXInstrumentType,
1294 pub inst_family: Ustr,
1296}
1297
1298#[derive(Clone, Debug, Default, Deserialize, Serialize, Builder)]
1300#[builder(default)]
1301#[builder(setter(into, strip_option))]
1302#[serde(rename_all = "camelCase")]
1303pub struct WsAmendOrderParams {
1304 pub inst_id_code: u64,
1306 #[serde(skip_serializing_if = "Option::is_none")]
1308 pub ord_id: Option<String>,
1309 #[serde(skip_serializing_if = "Option::is_none")]
1311 pub cl_ord_id: Option<String>,
1312 #[serde(skip_serializing_if = "Option::is_none")]
1314 pub new_cl_ord_id: Option<String>,
1315 #[serde(skip_serializing_if = "Option::is_none")]
1317 pub new_px: Option<String>,
1318 #[serde(rename = "newPxUsd", skip_serializing_if = "Option::is_none")]
1320 pub new_px_usd: Option<String>,
1321 #[serde(rename = "newPxVol", skip_serializing_if = "Option::is_none")]
1324 pub new_px_vol: Option<String>,
1325 #[serde(skip_serializing_if = "Option::is_none")]
1327 pub new_sz: Option<String>,
1328 #[serde(skip_serializing_if = "Option::is_none")]
1330 pub speed_bump: Option<String>,
1331}
1332
1333#[derive(Clone, Debug, Deserialize, Serialize, Builder)]
1335#[builder(setter(into, strip_option))]
1336#[serde(rename_all = "camelCase")]
1337pub struct WsPostAlgoOrderParams {
1338 pub inst_id_code: u64,
1340 pub td_mode: OKXTradeMode,
1342 pub side: OKXSide,
1344 pub ord_type: OKXAlgoOrderType,
1346 pub sz: String,
1348 #[builder(default)]
1350 #[serde(skip_serializing_if = "Option::is_none")]
1351 pub cl_ord_id: Option<String>,
1352 #[builder(default)]
1354 #[serde(skip_serializing_if = "Option::is_none")]
1355 pub pos_side: Option<OKXPositionSide>,
1356 #[serde(skip_serializing_if = "Option::is_none")]
1358 pub trigger_px: Option<String>,
1359 #[builder(default)]
1361 #[serde(skip_serializing_if = "Option::is_none")]
1362 pub trigger_px_type: Option<OKXTriggerType>,
1363 #[builder(default)]
1365 #[serde(skip_serializing_if = "Option::is_none")]
1366 pub order_px: Option<String>,
1367 #[builder(default)]
1369 #[serde(skip_serializing_if = "Option::is_none")]
1370 pub reduce_only: Option<bool>,
1371 #[builder(default)]
1373 #[serde(skip_serializing_if = "Option::is_none")]
1374 pub tag: Option<String>,
1375 #[builder(default)]
1377 #[serde(skip_serializing_if = "Option::is_none")]
1378 pub callback_ratio: Option<String>,
1379 #[builder(default)]
1381 #[serde(skip_serializing_if = "Option::is_none")]
1382 pub callback_spread: Option<String>,
1383 #[builder(default)]
1385 #[serde(skip_serializing_if = "Option::is_none")]
1386 pub active_px: Option<String>,
1387}
1388
1389#[derive(Clone, Debug, Deserialize, Serialize, Builder)]
1391#[builder(setter(into, strip_option))]
1392#[serde(rename_all = "camelCase")]
1393pub struct WsCancelAlgoOrderParams {
1394 pub inst_id_code: u64,
1396 #[serde(skip_serializing_if = "Option::is_none")]
1398 pub algo_id: Option<String>,
1399 #[serde(skip_serializing_if = "Option::is_none")]
1401 pub algo_cl_ord_id: Option<String>,
1402}
1403
1404#[cfg(test)]
1405mod tests {
1406 use nautilus_core::time::get_atomic_clock_realtime;
1407 use rstest::rstest;
1408
1409 use super::*;
1410
1411 #[rstest]
1412 fn test_deserialize_websocket_arg() {
1413 let json_str = r#"{"channel":"instruments","instType":"SPOT"}"#;
1414
1415 let result: Result<OKXWebSocketArg, _> = serde_json::from_str(json_str);
1416 match result {
1417 Ok(arg) => {
1418 assert_eq!(arg.channel, OKXWsChannel::Instruments);
1419 assert_eq!(arg.inst_type, Some(OKXInstrumentType::Spot));
1420 assert_eq!(arg.inst_id, None);
1421 }
1422 Err(e) => {
1423 panic!("Failed to deserialize WebSocket arg: {e}");
1424 }
1425 }
1426 }
1427
1428 #[rstest]
1429 fn test_deserialize_subscribe_variant_direct() {
1430 #[derive(Debug, Deserialize)]
1431 #[serde(rename_all = "camelCase")]
1432 struct SubscribeMsg {
1433 event: String,
1434 arg: OKXWebSocketArg,
1435 conn_id: String,
1436 }
1437
1438 let json_str = r#"{"event":"subscribe","arg":{"channel":"instruments","instType":"SPOT"},"connId":"380cfa6a"}"#;
1439
1440 let result: Result<SubscribeMsg, _> = serde_json::from_str(json_str);
1441 match result {
1442 Ok(msg) => {
1443 assert_eq!(msg.event, "subscribe");
1444 assert_eq!(msg.arg.channel, OKXWsChannel::Instruments);
1445 assert_eq!(msg.conn_id, "380cfa6a");
1446 }
1447 Err(e) => {
1448 panic!("Failed to deserialize subscribe message directly: {e}");
1449 }
1450 }
1451 }
1452
1453 #[rstest]
1454 fn test_deserialize_subscribe_confirmation() {
1455 let json_str = r#"{"event":"subscribe","arg":{"channel":"instruments","instType":"SPOT"},"connId":"380cfa6a"}"#;
1456
1457 let result: Result<OKXWsFrame, _> = serde_json::from_str(json_str);
1458 match result {
1459 Ok(msg) => {
1460 if let OKXWsFrame::Subscription {
1461 event,
1462 arg,
1463 conn_id,
1464 ..
1465 } = msg
1466 {
1467 assert_eq!(event, OKXSubscriptionEvent::Subscribe);
1468 assert_eq!(arg.channel, OKXWsChannel::Instruments);
1469 assert_eq!(conn_id, "380cfa6a");
1470 } else {
1471 panic!("Expected Subscribe variant, was: {msg:?}");
1472 }
1473 }
1474 Err(e) => {
1475 panic!("Failed to deserialize subscription confirmation: {e}");
1476 }
1477 }
1478 }
1479
1480 #[rstest]
1481 fn test_deserialize_subscribe_with_inst_id() {
1482 let json_str = r#"{"event":"subscribe","arg":{"channel":"candle1m","instId":"ETH-USDT"},"connId":"358602f5"}"#;
1483
1484 let result: Result<OKXWsFrame, _> = serde_json::from_str(json_str);
1485 match result {
1486 Ok(msg) => {
1487 if let OKXWsFrame::Subscription {
1488 event,
1489 arg,
1490 conn_id,
1491 ..
1492 } = msg
1493 {
1494 assert_eq!(event, OKXSubscriptionEvent::Subscribe);
1495 assert_eq!(arg.channel, OKXWsChannel::Candle1Minute);
1496 assert_eq!(conn_id, "358602f5");
1497 } else {
1498 panic!("Expected Subscribe variant, was: {msg:?}");
1499 }
1500 }
1501 Err(e) => {
1502 panic!("Failed to deserialize subscription confirmation: {e}");
1503 }
1504 }
1505 }
1506
1507 #[rstest]
1508 fn test_channel_serialization_for_logging() {
1509 let channel = OKXWsChannel::Candle1Minute;
1510 let serialized = serde_json::to_string(&channel).unwrap();
1511 let cleaned = serialized.trim_matches('"').to_string();
1512 assert_eq!(cleaned, "candle1m");
1513
1514 let channel = OKXWsChannel::BboTbt;
1515 let serialized = serde_json::to_string(&channel).unwrap();
1516 let cleaned = serialized.trim_matches('"').to_string();
1517 assert_eq!(cleaned, "bbo-tbt");
1518
1519 let channel = OKXWsChannel::Trades;
1520 let serialized = serde_json::to_string(&channel).unwrap();
1521 let cleaned = serialized.trim_matches('"').to_string();
1522 assert_eq!(cleaned, "trades");
1523 }
1524
1525 #[rstest]
1526 fn test_order_response_with_enum_operation() {
1527 let json_str = r#"{"id":"req-123","op":"order","code":"0","msg":"","data":[]}"#;
1528 let result: Result<OKXWsFrame, _> = serde_json::from_str(json_str);
1529 match result {
1530 Ok(OKXWsFrame::OrderResponse {
1531 id,
1532 op,
1533 code,
1534 msg,
1535 data,
1536 }) => {
1537 assert_eq!(id, Some("req-123".to_string()));
1538 assert_eq!(op, OKXWsOperation::Order);
1539 assert_eq!(code, "0");
1540 assert_eq!(msg, "");
1541 assert!(data.is_empty());
1542 }
1543 Ok(other) => panic!("Expected OrderResponse, was: {other:?}"),
1544 Err(e) => panic!("Failed to deserialize: {e}"),
1545 }
1546
1547 let json_str = r#"{"id":"cancel-456","op":"cancel-order","code":"50001","msg":"Order not found","data":[]}"#;
1548 let result: Result<OKXWsFrame, _> = serde_json::from_str(json_str);
1549 match result {
1550 Ok(OKXWsFrame::OrderResponse {
1551 id,
1552 op,
1553 code,
1554 msg,
1555 data,
1556 }) => {
1557 assert_eq!(id, Some("cancel-456".to_string()));
1558 assert_eq!(op, OKXWsOperation::CancelOrder);
1559 assert_eq!(code, "50001");
1560 assert_eq!(msg, "Order not found");
1561 assert!(data.is_empty());
1562 }
1563 Ok(other) => panic!("Expected OrderResponse, was: {other:?}"),
1564 Err(e) => panic!("Failed to deserialize: {e}"),
1565 }
1566
1567 let json_str = r#"{"id":"amend-789","op":"amend-order","code":"50002","msg":"Invalid price","data":[]}"#;
1568 let result: Result<OKXWsFrame, _> = serde_json::from_str(json_str);
1569 match result {
1570 Ok(OKXWsFrame::OrderResponse {
1571 id,
1572 op,
1573 code,
1574 msg,
1575 data,
1576 }) => {
1577 assert_eq!(id, Some("amend-789".to_string()));
1578 assert_eq!(op, OKXWsOperation::AmendOrder);
1579 assert_eq!(code, "50002");
1580 assert_eq!(msg, "Invalid price");
1581 assert!(data.is_empty());
1582 }
1583 Ok(other) => panic!("Expected OrderResponse, was: {other:?}"),
1584 Err(e) => panic!("Failed to deserialize: {e}"),
1585 }
1586 }
1587
1588 #[rstest]
1589 fn test_operation_enum_serialization() {
1590 let op = OKXWsOperation::Order;
1591 let serialized = serde_json::to_string(&op).unwrap();
1592 assert_eq!(serialized, "\"order\"");
1593
1594 let op = OKXWsOperation::CancelOrder;
1595 let serialized = serde_json::to_string(&op).unwrap();
1596 assert_eq!(serialized, "\"cancel-order\"");
1597
1598 let op = OKXWsOperation::AmendOrder;
1599 let serialized = serde_json::to_string(&op).unwrap();
1600 assert_eq!(serialized, "\"amend-order\"");
1601
1602 let op = OKXWsOperation::Subscribe;
1603 let serialized = serde_json::to_string(&op).unwrap();
1604 assert_eq!(serialized, "\"subscribe\"");
1605 }
1606
1607 #[rstest]
1608 fn test_order_response_parsing() {
1609 let success_response = r#"{
1610 "id": "req-123",
1611 "op": "order",
1612 "code": "0",
1613 "msg": "",
1614 "data": [{"sMsg": "Order placed successfully"}]
1615 }"#;
1616
1617 let parsed: OKXWsFrame = serde_json::from_str(success_response).unwrap();
1618
1619 match parsed {
1620 OKXWsFrame::OrderResponse {
1621 id,
1622 op,
1623 code,
1624 msg,
1625 data,
1626 } => {
1627 assert_eq!(id, Some("req-123".to_string()));
1628 assert_eq!(op, OKXWsOperation::Order);
1629 assert_eq!(code, "0");
1630 assert_eq!(msg, "");
1631 assert_eq!(data.len(), 1);
1632 }
1633 _ => panic!("Expected OrderResponse variant"),
1634 }
1635
1636 let failure_response = r#"{
1637 "id": "req-456",
1638 "op": "cancel-order",
1639 "code": "50001",
1640 "msg": "Order not found",
1641 "data": [{"sMsg": "Order with client order ID not found"}]
1642 }"#;
1643
1644 let parsed: OKXWsFrame = serde_json::from_str(failure_response).unwrap();
1645
1646 match parsed {
1647 OKXWsFrame::OrderResponse {
1648 id,
1649 op,
1650 code,
1651 msg,
1652 data,
1653 } => {
1654 assert_eq!(id, Some("req-456".to_string()));
1655 assert_eq!(op, OKXWsOperation::CancelOrder);
1656 assert_eq!(code, "50001");
1657 assert_eq!(msg, "Order not found");
1658 assert_eq!(data.len(), 1);
1659 }
1660 _ => panic!("Expected OrderResponse variant"),
1661 }
1662 }
1663
1664 #[rstest]
1665 fn test_subscription_event_parsing() {
1666 let subscription_json = r#"{
1667 "event": "subscribe",
1668 "arg": {
1669 "channel": "tickers",
1670 "instId": "BTC-USDT"
1671 },
1672 "connId": "a4d3ae55"
1673 }"#;
1674
1675 let parsed: OKXWsFrame = serde_json::from_str(subscription_json).unwrap();
1676
1677 match parsed {
1678 OKXWsFrame::Subscription {
1679 event,
1680 arg,
1681 conn_id,
1682 ..
1683 } => {
1684 assert_eq!(
1685 event,
1686 crate::websocket::enums::OKXSubscriptionEvent::Subscribe
1687 );
1688 assert_eq!(arg.channel, OKXWsChannel::Tickers);
1689 assert_eq!(arg.inst_id, Some(Ustr::from("BTC-USDT")));
1690 assert_eq!(conn_id, "a4d3ae55");
1691 }
1692 _ => panic!("Expected Subscription variant"),
1693 }
1694 }
1695
1696 #[rstest]
1697 fn test_login_event_parsing() {
1698 let login_success = r#"{
1699 "event": "login",
1700 "code": "0",
1701 "msg": "Login successful",
1702 "connId": "a4d3ae55"
1703 }"#;
1704
1705 let parsed: OKXWsFrame = serde_json::from_str(login_success).unwrap();
1706
1707 match parsed {
1708 OKXWsFrame::Login {
1709 event,
1710 code,
1711 msg,
1712 conn_id,
1713 } => {
1714 assert_eq!(event, "login");
1715 assert_eq!(code, "0");
1716 assert_eq!(msg, "Login successful");
1717 assert_eq!(conn_id, "a4d3ae55");
1718 }
1719 _ => panic!("Expected Login variant, was: {parsed:?}"),
1720 }
1721 }
1722
1723 #[rstest]
1724 fn test_error_event_parsing() {
1725 let error_json = r#"{
1726 "code": "60012",
1727 "msg": "Invalid request"
1728 }"#;
1729
1730 let parsed: OKXWsFrame = serde_json::from_str(error_json).unwrap();
1731
1732 match parsed {
1733 OKXWsFrame::Error { code, msg } => {
1734 assert_eq!(code, "60012");
1735 assert_eq!(msg, "Invalid request");
1736 }
1737 _ => panic!("Expected Error variant"),
1738 }
1739 }
1740
1741 #[rstest]
1742 fn test_error_event_with_event_field_parsing() {
1743 let error_json = r#"{
1745 "event": "error",
1746 "code": "60018",
1747 "msg": "Invalid sign"
1748 }"#;
1749
1750 let parsed: OKXWsFrame = serde_json::from_str(error_json).unwrap();
1751
1752 match parsed {
1753 OKXWsFrame::Error { code, msg } => {
1754 assert_eq!(code, "60018");
1755 assert_eq!(msg, "Invalid sign");
1756 }
1757 _ => panic!("Expected Error variant, was: {parsed:?}"),
1758 }
1759 }
1760
1761 #[rstest]
1762 fn test_subscription_error_with_arg_field_parsing() {
1763 let error_json = r#"{
1765 "event": "error",
1766 "arg": {"channel": "tickers", "instId": "INVALID-INST"},
1767 "code": "60012",
1768 "msg": "Invalid request: channel not found",
1769 "connId": "a4d3ae55"
1770 }"#;
1771
1772 let parsed: OKXWsFrame = serde_json::from_str(error_json).unwrap();
1773
1774 match parsed {
1775 OKXWsFrame::Error { code, msg } => {
1776 assert_eq!(code, "60012");
1777 assert_eq!(msg, "Invalid request: channel not found");
1778 }
1779 _ => panic!("Expected Error variant, was: {parsed:?}"),
1780 }
1781 }
1782
1783 #[rstest]
1784 fn test_websocket_request_serialization() {
1785 let request = OKXWsRequest {
1786 id: Some("req-123".to_string()),
1787 op: OKXWsOperation::Order,
1788 args: vec![serde_json::json!({
1789 "instId": "BTC-USDT",
1790 "tdMode": "cash",
1791 "side": "buy",
1792 "ordType": "market",
1793 "sz": "0.1"
1794 })],
1795 exp_time: None,
1796 };
1797
1798 let serialized = serde_json::to_string(&request).unwrap();
1799 let parsed: serde_json::Value = serde_json::from_str(&serialized).unwrap();
1800
1801 assert_eq!(parsed["id"], "req-123");
1802 assert_eq!(parsed["op"], "order");
1803 assert!(parsed["args"].is_array());
1804 assert_eq!(parsed["args"].as_array().unwrap().len(), 1);
1805 }
1806
1807 #[rstest]
1808 fn test_subscription_request_serialization() {
1809 let subscription = OKXSubscription {
1810 op: OKXWsOperation::Subscribe,
1811 args: vec![OKXSubscriptionArg {
1812 channel: OKXWsChannel::Tickers,
1813 inst_type: Some(OKXInstrumentType::Spot),
1814 inst_family: None,
1815 inst_id: Some(Ustr::from("BTC-USDT")),
1816 }],
1817 };
1818
1819 let serialized = serde_json::to_string(&subscription).unwrap();
1820 let parsed: serde_json::Value = serde_json::from_str(&serialized).unwrap();
1821
1822 assert_eq!(parsed["op"], "subscribe");
1823 assert!(parsed["args"].is_array());
1824 assert_eq!(parsed["args"][0]["channel"], "tickers");
1825 assert_eq!(parsed["args"][0]["instType"], "SPOT");
1826 assert_eq!(parsed["args"][0]["instId"], "BTC-USDT");
1827 }
1828
1829 #[rstest]
1830 fn test_error_message_extraction() {
1831 let responses = vec![
1832 (
1833 r#"{
1834 "id": "req-123",
1835 "op": "order",
1836 "code": "50001",
1837 "msg": "Order failed",
1838 "data": [{"sMsg": "Insufficient balance"}]
1839 }"#,
1840 "Insufficient balance",
1841 ),
1842 (
1843 r#"{
1844 "id": "req-456",
1845 "op": "cancel-order",
1846 "code": "50002",
1847 "msg": "Cancel failed",
1848 "data": [{}]
1849 }"#,
1850 "Cancel failed",
1851 ),
1852 ];
1853
1854 for (response_json, expected_msg) in responses {
1855 let parsed: OKXWsFrame = serde_json::from_str(response_json).unwrap();
1856
1857 match parsed {
1858 OKXWsFrame::OrderResponse {
1859 id: _,
1860 op: _,
1861 code,
1862 msg,
1863 data,
1864 } => {
1865 assert_ne!(code, "0"); let error_msg = data
1869 .first()
1870 .and_then(|d| d.get("sMsg"))
1871 .and_then(|s| s.as_str())
1872 .filter(|s| !s.is_empty())
1873 .unwrap_or(&msg);
1874
1875 assert_eq!(error_msg, expected_msg);
1876 }
1877 _ => panic!("Expected OrderResponse variant"),
1878 }
1879 }
1880 }
1881
1882 #[rstest]
1883 fn test_book_data_parsing() {
1884 let book_data_json = r#"{
1885 "arg": {
1886 "channel": "books",
1887 "instId": "BTC-USDT"
1888 },
1889 "action": "snapshot",
1890 "data": [{
1891 "asks": [["50000.0", "0.1", "0", "1"]],
1892 "bids": [["49999.0", "0.2", "0", "1"]],
1893 "ts": "1640995200000",
1894 "checksum": 123456789,
1895 "seqId": 1000
1896 }]
1897 }"#;
1898
1899 let parsed: OKXWsFrame = serde_json::from_str(book_data_json).unwrap();
1900
1901 match parsed {
1902 OKXWsFrame::BookData { arg, action, data } => {
1903 assert_eq!(arg.channel, OKXWsChannel::Books);
1904 assert_eq!(arg.inst_id, Some(Ustr::from("BTC-USDT")));
1905 assert_eq!(
1906 action,
1907 super::super::super::common::enums::OKXBookAction::Snapshot
1908 );
1909 assert_eq!(data.len(), 1);
1910 }
1911 _ => panic!("Expected BookData variant"),
1912 }
1913 }
1914
1915 #[rstest]
1916 fn test_data_event_parsing() {
1917 let data_json = r#"{
1918 "arg": {
1919 "channel": "trades",
1920 "instId": "BTC-USDT"
1921 },
1922 "data": [{
1923 "instId": "BTC-USDT",
1924 "tradeId": "12345",
1925 "px": "50000.0",
1926 "sz": "0.1",
1927 "side": "buy",
1928 "ts": "1640995200000"
1929 }]
1930 }"#;
1931
1932 let parsed: OKXWsFrame = serde_json::from_str(data_json).unwrap();
1933
1934 match parsed {
1935 OKXWsFrame::Data { arg, data } => {
1936 assert_eq!(arg.channel, OKXWsChannel::Trades);
1937 assert_eq!(arg.inst_id, Some(Ustr::from("BTC-USDT")));
1938 assert!(data.is_array());
1939 }
1940 _ => panic!("Expected Data variant"),
1941 }
1942 }
1943
1944 #[rstest]
1945 fn test_nautilus_message_variants() {
1946 let clock = get_atomic_clock_realtime();
1947 let ts_init = clock.get_time_ns();
1948
1949 let error = OKXWebSocketError {
1950 code: "60012".to_string(),
1951 message: "Invalid request".to_string(),
1952 conn_id: None,
1953 timestamp: ts_init.as_u64(),
1954 };
1955 let error_msg = NautilusWsMessage::Error(error);
1956
1957 match error_msg {
1958 NautilusWsMessage::Error(e) => {
1959 assert_eq!(e.code, "60012");
1960 assert_eq!(e.message, "Invalid request");
1961 }
1962 _ => panic!("Expected Error variant"),
1963 }
1964
1965 let raw_scenarios = vec![
1966 ::serde_json::json!({"unknown": "data"}),
1967 ::serde_json::json!({"channel": "unsupported", "data": [1, 2, 3]}),
1968 ::serde_json::json!({"complex": {"nested": {"structure": true}}}),
1969 ];
1970
1971 for raw_data in raw_scenarios {
1972 let raw_msg = NautilusWsMessage::Raw(raw_data.clone());
1973
1974 match raw_msg {
1975 NautilusWsMessage::Raw(data) => {
1976 assert_eq!(data, raw_data);
1977 }
1978 _ => panic!("Expected Raw variant"),
1979 }
1980 }
1981 }
1982
1983 #[rstest]
1984 fn test_order_response_parsing_success() {
1985 let order_response_json = r#"{
1986 "id": "req-123",
1987 "op": "order",
1988 "code": "0",
1989 "msg": "",
1990 "data": [{"sMsg": "Order placed successfully"}]
1991 }"#;
1992
1993 let parsed: OKXWsFrame = serde_json::from_str(order_response_json).unwrap();
1994
1995 match parsed {
1996 OKXWsFrame::OrderResponse {
1997 id,
1998 op,
1999 code,
2000 msg,
2001 data,
2002 } => {
2003 assert_eq!(id, Some("req-123".to_string()));
2004 assert_eq!(op, OKXWsOperation::Order);
2005 assert_eq!(code, "0");
2006 assert_eq!(msg, "");
2007 assert_eq!(data.len(), 1);
2008 }
2009 _ => panic!("Expected OrderResponse variant"),
2010 }
2011 }
2012
2013 #[rstest]
2014 fn test_order_response_parsing_failure() {
2015 let order_response_json = r#"{
2016 "id": "req-456",
2017 "op": "cancel-order",
2018 "code": "50001",
2019 "msg": "Order not found",
2020 "data": [{"sMsg": "Order with client order ID not found"}]
2021 }"#;
2022
2023 let parsed: OKXWsFrame = serde_json::from_str(order_response_json).unwrap();
2024
2025 match parsed {
2026 OKXWsFrame::OrderResponse {
2027 id,
2028 op,
2029 code,
2030 msg,
2031 data,
2032 } => {
2033 assert_eq!(id, Some("req-456".to_string()));
2034 assert_eq!(op, OKXWsOperation::CancelOrder);
2035 assert_eq!(code, "50001");
2036 assert_eq!(msg, "Order not found");
2037 assert_eq!(data.len(), 1);
2038 }
2039 _ => panic!("Expected OrderResponse variant"),
2040 }
2041 }
2042
2043 #[rstest]
2044 fn test_message_request_serialization() {
2045 let request = OKXWsRequest {
2046 id: Some("req-123".to_string()),
2047 op: OKXWsOperation::Order,
2048 args: vec![::serde_json::json!({
2049 "instId": "BTC-USDT",
2050 "tdMode": "cash",
2051 "side": "buy",
2052 "ordType": "market",
2053 "sz": "0.1"
2054 })],
2055 exp_time: None,
2056 };
2057
2058 let serialized = serde_json::to_string(&request).unwrap();
2059 let parsed: serde_json::Value = serde_json::from_str(&serialized).unwrap();
2060
2061 assert_eq!(parsed["id"], "req-123");
2062 assert_eq!(parsed["op"], "order");
2063 assert!(parsed["args"].is_array());
2064 assert_eq!(parsed["args"].as_array().unwrap().len(), 1);
2065 }
2066
2067 #[rstest]
2068 fn test_ws_post_order_params_serializes_inst_id_code() {
2069 use super::WsPostOrderParamsBuilder;
2070 use crate::common::enums::{OKXOrderType, OKXSide, OKXTradeMode};
2071
2072 let params = WsPostOrderParamsBuilder::default()
2073 .inst_id_code(10459u64)
2074 .td_mode(OKXTradeMode::Cross)
2075 .side(OKXSide::Buy)
2076 .ord_type(OKXOrderType::Limit)
2077 .sz("0.01".to_string())
2078 .px("50000".to_string())
2079 .build()
2080 .unwrap();
2081
2082 let json = serde_json::to_string(¶ms).unwrap();
2083
2084 assert!(json.contains("\"instIdCode\":10459"));
2085 assert!(!json.contains("\"instId\""));
2086 }
2087
2088 #[rstest]
2089 fn test_ws_post_order_params_serializes_slippage_pct() {
2090 use super::WsPostOrderParamsBuilder;
2091 use crate::common::enums::{OKXOrderType, OKXSide, OKXTradeMode};
2092
2093 let params = WsPostOrderParamsBuilder::default()
2094 .inst_id_code(10459u64)
2095 .td_mode(OKXTradeMode::Cross)
2096 .side(OKXSide::Buy)
2097 .ord_type(OKXOrderType::Market)
2098 .sz("0.01".to_string())
2099 .slippage_pct("0.005".to_string())
2100 .build()
2101 .unwrap();
2102
2103 let json: serde_json::Value = serde_json::to_value(¶ms).unwrap();
2104 assert_eq!(json["slippagePct"], "0.005");
2105 }
2106
2107 #[rstest]
2108 fn test_ws_post_order_params_omits_slippage_pct_when_unset() {
2109 use super::WsPostOrderParamsBuilder;
2110 use crate::common::enums::{OKXOrderType, OKXSide, OKXTradeMode};
2111
2112 let params = WsPostOrderParamsBuilder::default()
2113 .inst_id_code(10459u64)
2114 .td_mode(OKXTradeMode::Cross)
2115 .side(OKXSide::Buy)
2116 .ord_type(OKXOrderType::Market)
2117 .sz("0.01".to_string())
2118 .build()
2119 .unwrap();
2120
2121 let json = serde_json::to_string(¶ms).unwrap();
2122 assert!(!json.contains("slippagePct"));
2123 }
2124
2125 #[rstest]
2126 fn test_ws_post_order_params_serializes_attached_tp_sl() {
2127 use super::{WsAttachAlgoOrdParamsBuilder, WsPostOrderParamsBuilder};
2128 use crate::common::enums::{OKXOrderType, OKXSide, OKXTradeMode, OKXTriggerType};
2129
2130 let params = WsPostOrderParamsBuilder::default()
2131 .inst_id_code(10459u64)
2132 .td_mode(OKXTradeMode::Cross)
2133 .side(OKXSide::Buy)
2134 .ord_type(OKXOrderType::Limit)
2135 .sz("0.01".to_string())
2136 .px("50000".to_string())
2137 .attach_algo_ords(vec![
2138 WsAttachAlgoOrdParamsBuilder::default()
2139 .attach_algo_cl_ord_id("O-bracket-sl")
2140 .sl_trigger_px("39000")
2141 .sl_ord_px("-1")
2142 .sl_trigger_px_type(OKXTriggerType::Last)
2143 .build()
2144 .unwrap(),
2145 WsAttachAlgoOrdParamsBuilder::default()
2146 .attach_algo_cl_ord_id("O-bracket-tp")
2147 .tp_trigger_px("41000")
2148 .tp_ord_px("-1")
2149 .tp_trigger_px_type(OKXTriggerType::Last)
2150 .build()
2151 .unwrap(),
2152 ])
2153 .build()
2154 .unwrap();
2155
2156 let json = serde_json::to_string(¶ms).unwrap();
2157
2158 assert!(json.contains("\"attachAlgoOrds\""));
2159 assert!(json.contains("\"attachAlgoClOrdId\":\"O-bracket-sl\""));
2160 assert!(json.contains("\"slTriggerPx\":\"39000\""));
2161 assert!(json.contains("\"slOrdPx\":\"-1\""));
2162 assert!(json.contains("\"attachAlgoClOrdId\":\"O-bracket-tp\""));
2163 assert!(json.contains("\"tpTriggerPx\":\"41000\""));
2164 assert!(json.contains("\"tpOrdPx\":\"-1\""));
2165 }
2166
2167 #[rstest]
2168 fn test_ws_cancel_order_params_serializes_inst_id_code() {
2169 use super::WsCancelOrderParamsBuilder;
2170
2171 let params = WsCancelOrderParamsBuilder::default()
2172 .inst_id_code(10461u64)
2173 .ord_id("12345678".to_string())
2174 .build()
2175 .unwrap();
2176
2177 let json = serde_json::to_string(¶ms).unwrap();
2178
2179 assert!(json.contains("\"instIdCode\":10461"));
2180 assert!(!json.contains("\"instId\""));
2181 assert!(json.contains("\"ordId\":\"12345678\""));
2182 }
2183
2184 #[rstest]
2185 fn test_ws_amend_order_params_serializes_inst_id_code() {
2186 use super::WsAmendOrderParamsBuilder;
2187
2188 let params = WsAmendOrderParamsBuilder::default()
2189 .inst_id_code(10459u64)
2190 .cl_ord_id("client123".to_string())
2191 .new_px("51000".to_string())
2192 .build()
2193 .unwrap();
2194
2195 let json = serde_json::to_string(¶ms).unwrap();
2196
2197 assert!(json.contains("\"instIdCode\":10459"));
2198 assert!(!json.contains("\"instId\""));
2199 assert!(json.contains("\"newPx\":\"51000\""));
2200 }
2201
2202 #[rstest]
2203 fn test_ws_post_algo_order_params_serializes_inst_id_code() {
2204 use super::WsPostAlgoOrderParamsBuilder;
2205 use crate::common::enums::{OKXAlgoOrderType, OKXSide, OKXTradeMode, OKXTriggerType};
2206
2207 let params = WsPostAlgoOrderParamsBuilder::default()
2208 .inst_id_code(10459u64)
2209 .td_mode(OKXTradeMode::Cross)
2210 .side(OKXSide::Buy)
2211 .ord_type(OKXAlgoOrderType::Trigger)
2212 .sz("0.01".to_string())
2213 .trigger_px("48000".to_string())
2214 .trigger_px_type(OKXTriggerType::Last)
2215 .build()
2216 .unwrap();
2217
2218 let json = serde_json::to_string(¶ms).unwrap();
2219
2220 assert!(json.contains("\"instIdCode\":10459"));
2221 assert!(!json.contains("\"instId\""));
2222 assert!(json.contains("\"triggerPx\":\"48000\""));
2223 }
2224
2225 #[rstest]
2226 fn test_ws_cancel_algo_order_params_serializes_inst_id_code() {
2227 let params = WsCancelAlgoOrderParams {
2228 inst_id_code: 10459,
2229 algo_id: Some("987654321".to_string()),
2230 algo_cl_ord_id: None,
2231 };
2232
2233 let json = serde_json::to_string(¶ms).unwrap();
2234
2235 assert!(json.contains("\"instIdCode\":10459"));
2236 assert!(!json.contains("\"instId\""));
2237 assert!(json.contains("\"algoId\":\"987654321\""));
2238 }
2239
2240 #[rstest]
2241 fn test_ws_post_order_params_serializes_px_usd() {
2242 use super::WsPostOrderParamsBuilder;
2243 use crate::common::enums::{OKXOrderType, OKXSide, OKXTradeMode};
2244
2245 let params = WsPostOrderParamsBuilder::default()
2246 .inst_id_code(10459u64)
2247 .td_mode(OKXTradeMode::Cross)
2248 .side(OKXSide::Buy)
2249 .ord_type(OKXOrderType::Limit)
2250 .sz("1".to_string())
2251 .px_usd("100.5".to_string())
2252 .build()
2253 .unwrap();
2254
2255 let json = serde_json::to_string(¶ms).unwrap();
2256 assert!(json.contains("\"pxUsd\":\"100.5\""));
2257 assert!(!json.contains("\"pxVol\""));
2258 assert!(!json.contains("\"px\":"));
2259 }
2260
2261 #[rstest]
2262 fn test_ws_post_order_params_serializes_px_vol() {
2263 use super::WsPostOrderParamsBuilder;
2264 use crate::common::enums::{OKXOrderType, OKXSide, OKXTradeMode};
2265
2266 let params = WsPostOrderParamsBuilder::default()
2267 .inst_id_code(10459u64)
2268 .td_mode(OKXTradeMode::Cross)
2269 .side(OKXSide::Buy)
2270 .ord_type(OKXOrderType::Limit)
2271 .sz("1".to_string())
2272 .px_vol("0.55".to_string())
2273 .build()
2274 .unwrap();
2275
2276 let json = serde_json::to_string(¶ms).unwrap();
2277 assert!(json.contains("\"pxVol\":\"0.55\""));
2278 assert!(!json.contains("\"pxUsd\""));
2279 assert!(!json.contains("\"px\":"));
2280 }
2281
2282 #[rstest]
2283 fn test_ws_amend_order_params_serializes_new_px_usd() {
2284 use super::WsAmendOrderParamsBuilder;
2285
2286 let params = WsAmendOrderParamsBuilder::default()
2287 .inst_id_code(10459u64)
2288 .cl_ord_id("client123".to_string())
2289 .new_px_usd("105.0".to_string())
2290 .build()
2291 .unwrap();
2292
2293 let json = serde_json::to_string(¶ms).unwrap();
2294 assert!(json.contains("\"newPxUsd\":\"105.0\""));
2295 assert!(!json.contains("\"newPx\":"));
2296 assert!(!json.contains("\"newPxVol\""));
2297 }
2298
2299 #[rstest]
2300 fn test_ws_amend_order_params_serializes_new_px_vol() {
2301 use super::WsAmendOrderParamsBuilder;
2302
2303 let params = WsAmendOrderParamsBuilder::default()
2304 .inst_id_code(10459u64)
2305 .cl_ord_id("client123".to_string())
2306 .new_px_vol("0.60".to_string())
2307 .build()
2308 .unwrap();
2309
2310 let json = serde_json::to_string(¶ms).unwrap();
2311 assert!(json.contains("\"newPxVol\":\"0.60\""));
2312 assert!(!json.contains("\"newPx\":"));
2313 assert!(!json.contains("\"newPxUsd\""));
2314 }
2315
2316 #[rstest]
2317 fn test_ws_event_contract_markets_channel_serialization() {
2318 let json = serde_json::to_string(&OKXWsChannel::EventContractMarkets).unwrap();
2319 let channel: OKXWsChannel = serde_json::from_str(&json).unwrap();
2320
2321 assert_eq!(json, "\"event-contract-markets\"");
2322 assert_eq!(channel, OKXWsChannel::EventContractMarkets);
2323 }
2324
2325 #[rstest]
2326 fn test_ws_post_order_params_serializes_event_contract_fields() {
2327 use super::WsPostOrderParamsBuilder;
2328 use crate::common::enums::{OKXOrderType, OKXSide, OKXTradeMode};
2329
2330 let params = WsPostOrderParamsBuilder::default()
2331 .inst_id_code(10459u64)
2332 .td_mode(OKXTradeMode::Cash)
2333 .side(OKXSide::Buy)
2334 .ord_type(OKXOrderType::Limit)
2335 .sz("10".to_string())
2336 .px("0.42".to_string())
2337 .speed_bump("1")
2338 .outcome("yes")
2339 .build()
2340 .unwrap();
2341
2342 let json: serde_json::Value = serde_json::to_value(¶ms).unwrap();
2343
2344 assert_eq!(json["speedBump"], "1");
2345 assert_eq!(json["outcome"], "yes");
2346 }
2347
2348 #[rstest]
2349 fn test_ws_amend_order_params_serializes_speed_bump() {
2350 use super::WsAmendOrderParamsBuilder;
2351
2352 let params = WsAmendOrderParamsBuilder::default()
2353 .inst_id_code(10459u64)
2354 .cl_ord_id("event-1".to_string())
2355 .new_px("0.43".to_string())
2356 .speed_bump("1")
2357 .build()
2358 .unwrap();
2359
2360 let json: serde_json::Value = serde_json::to_value(¶ms).unwrap();
2361
2362 assert_eq!(json["speedBump"], "1");
2363 }
2364
2365 #[rstest]
2366 fn test_ws_attach_algo_ord_params_serializes_trailing_fields() {
2367 use super::WsAttachAlgoOrdParamsBuilder;
2368
2369 let params = WsAttachAlgoOrdParamsBuilder::default()
2370 .attach_algo_cl_ord_id("trail-1")
2371 .callback_ratio("0.01")
2372 .active_px("64000")
2373 .new_callback_ratio("0.02")
2374 .new_callback_spread("25")
2375 .new_active_px("65000")
2376 .build()
2377 .unwrap();
2378
2379 let json: serde_json::Value = serde_json::to_value(¶ms).unwrap();
2380
2381 assert_eq!(json["callbackRatio"], "0.01");
2382 assert_eq!(json["activePx"], "64000");
2383 assert_eq!(json["newCallbackRatio"], "0.02");
2384 assert_eq!(json["newCallbackSpread"], "25");
2385 assert_eq!(json["newActivePx"], "65000");
2386 assert!(json.get("callbackSpread").is_none());
2387 }
2388
2389 #[rstest]
2390 fn test_subscription_arg_serializes_sprd_id_for_spread_channels() {
2391 let arg = OKXSubscriptionArg {
2392 channel: OKXWsChannel::SprdBooks5,
2393 inst_type: None,
2394 inst_family: None,
2395 inst_id: Some(Ustr::from("ETH-USD-260925_ETH-USD-261225")),
2396 };
2397 let json = serde_json::to_value(&arg).unwrap();
2398 assert_eq!(json["channel"], "sprd-books5");
2399 assert_eq!(json["sprdId"], "ETH-USD-260925_ETH-USD-261225");
2400 assert!(json.get("instId").is_none());
2401 }
2402
2403 #[rstest]
2404 fn test_subscription_arg_serializes_inst_id_for_standard_channels() {
2405 let arg = OKXSubscriptionArg {
2406 channel: OKXWsChannel::BboTbt,
2407 inst_type: None,
2408 inst_family: None,
2409 inst_id: Some(Ustr::from("BTC-USDT")),
2410 };
2411 let json = serde_json::to_value(&arg).unwrap();
2412 assert_eq!(json["instId"], "BTC-USDT");
2413 assert!(json.get("sprdId").is_none());
2414 }
2415
2416 #[rstest]
2417 fn test_websocket_arg_resolves_sprd_id_into_inst_id() {
2418 let arg: OKXWebSocketArg = serde_json::from_value(serde_json::json!({
2419 "channel": "sprd-bbo-tbt",
2420 "sprdId": "ETH-USD-260925_ETH-USD-261225",
2421 }))
2422 .unwrap();
2423 assert_eq!(arg.channel, OKXWsChannel::SprdBboTbt);
2424 assert_eq!(
2425 arg.inst_id,
2426 Some(Ustr::from("ETH-USD-260925_ETH-USD-261225"))
2427 );
2428 }
2429
2430 #[rstest]
2431 fn test_book_msg_parses_three_element_spread_levels() {
2432 let msg: OKXBookMsg = serde_json::from_value(serde_json::json!({
2435 "asks": [["16.7", "100", "1"]],
2436 "bids": [["16.65", "100", "1"]],
2437 "ts": "1780044924909",
2438 "seqId": 1779935772619784_u64,
2439 }))
2440 .unwrap();
2441 assert_eq!(msg.asks[0].price, "16.7");
2442 assert_eq!(msg.asks[0].size, "100");
2443 assert_eq!(msg.bids[0].price, "16.65");
2444 }
2445
2446 #[rstest]
2447 fn test_trade_msg_parses_spread_public_trade() {
2448 let msg: OKXTradeMsg = serde_json::from_value(serde_json::json!({
2450 "sprdId": "ETH-USD-260925_ETH-USD-261225",
2451 "tradeId": "3392538740127301632",
2452 "px": "16.9",
2453 "sz": "100",
2454 "side": "sell",
2455 "ts": "1780047866507",
2456 }))
2457 .unwrap();
2458 assert_eq!(msg.inst_id, Ustr::from("ETH-USD-260925_ETH-USD-261225"));
2459 assert_eq!(msg.px, "16.9");
2460 assert_eq!(msg.side, OKXSide::Sell);
2461 assert!(msg.count.is_empty());
2462 }
2463}