nautilus_okx/websocket/
messages.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Data structures modelling OKX WebSocket request and response payloads.
17
18use derive_builder::Builder;
19use nautilus_model::{
20    data::{Data, FundingRateUpdate, OrderBookDeltas},
21    events::{AccountState, OrderCancelRejected, OrderModifyRejected, OrderRejected},
22    instruments::InstrumentAny,
23    reports::{FillReport, OrderStatusReport, PositionStatusReport},
24};
25use serde::{Deserialize, Serialize};
26use ustr::Ustr;
27
28use super::enums::{OKXWsChannel, OKXWsOperation};
29use crate::{
30    common::{
31        enums::{
32            OKXAlgoOrderType, OKXBookAction, OKXCandleConfirm, OKXExecType, OKXInstrumentType,
33            OKXOrderCategory, OKXOrderStatus, OKXOrderType, OKXPositionSide, OKXSide,
34            OKXTargetCurrency, OKXTradeMode, OKXTriggerType,
35        },
36        parse::{
37            deserialize_empty_string_as_none, deserialize_string_to_u64,
38            deserialize_target_currency_as_none,
39        },
40    },
41    websocket::enums::OKXSubscriptionEvent,
42};
43
44#[derive(Debug, Clone)]
45pub enum NautilusWsMessage {
46    Data(Vec<Data>),
47    Deltas(OrderBookDeltas),
48    FundingRates(Vec<FundingRateUpdate>),
49    Instrument(Box<InstrumentAny>),
50    AccountUpdate(AccountState),
51    PositionUpdate(PositionStatusReport),
52    OrderRejected(OrderRejected),
53    OrderCancelRejected(OrderCancelRejected),
54    OrderModifyRejected(OrderModifyRejected),
55    ExecutionReports(Vec<ExecutionReport>),
56    Error(OKXWebSocketError),
57    Raw(serde_json::Value), // Unhandled channels
58    Reconnected,
59    Authenticated,
60}
61
62/// Represents an OKX WebSocket error.
63#[derive(Debug, Clone, Serialize, Deserialize)]
64#[cfg_attr(feature = "python", pyo3::pyclass)]
65pub struct OKXWebSocketError {
66    /// Error code from OKX (e.g., "50101").
67    pub code: String,
68    /// Error message from OKX.
69    pub message: String,
70    /// Connection ID if available.
71    pub conn_id: Option<String>,
72    /// Timestamp when the error occurred.
73    pub timestamp: u64,
74}
75
76#[derive(Debug, Clone)]
77#[allow(clippy::large_enum_variant)]
78pub enum ExecutionReport {
79    Order(OrderStatusReport),
80    Fill(FillReport),
81}
82
83/// Generic WebSocket request for OKX trading commands.
84#[derive(Debug, Serialize)]
85#[serde(rename_all = "camelCase")]
86pub struct OKXWsRequest<T> {
87    /// Client request ID (required for order operations).
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub id: Option<String>,
90    /// Operation type (order, cancel-order, amend-order).
91    pub op: OKXWsOperation,
92    /// Request effective deadline. Unix timestamp format in milliseconds.
93    /// This is when the request itself expires, not related to order expiration.
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub exp_time: Option<String>,
96    /// Arguments payload for the operation.
97    pub args: Vec<T>,
98}
99
100/// OKX WebSocket authentication message.
101#[derive(Debug, Serialize)]
102pub struct OKXAuthentication {
103    pub op: &'static str,
104    pub args: Vec<OKXAuthenticationArg>,
105}
106
107/// OKX WebSocket authentication arguments.
108#[derive(Debug, Serialize)]
109#[serde(rename_all = "camelCase")]
110pub struct OKXAuthenticationArg {
111    pub api_key: String,
112    pub passphrase: String,
113    pub timestamp: String,
114    pub sign: String,
115}
116
117#[derive(Debug, Serialize)]
118pub struct OKXSubscription {
119    pub op: OKXWsOperation,
120    pub args: Vec<OKXSubscriptionArg>,
121}
122
123#[derive(Clone, Debug, Serialize)]
124#[serde(rename_all = "camelCase")]
125pub struct OKXSubscriptionArg {
126    pub channel: OKXWsChannel,
127    pub inst_type: Option<OKXInstrumentType>,
128    pub inst_family: Option<Ustr>,
129    pub inst_id: Option<Ustr>,
130}
131
132#[derive(Debug, Deserialize)]
133#[serde(untagged)]
134pub enum OKXWsMessage {
135    Login {
136        event: String,
137        code: String,
138        msg: String,
139        #[serde(rename = "connId")]
140        conn_id: String,
141    },
142    Subscription {
143        event: OKXSubscriptionEvent,
144        arg: OKXWebSocketArg,
145        #[serde(rename = "connId")]
146        conn_id: String,
147        #[serde(default)]
148        code: Option<String>,
149        #[serde(default)]
150        msg: Option<String>,
151    },
152    ChannelConnCount {
153        event: String,
154        channel: OKXWsChannel,
155        #[serde(rename = "connCount")]
156        conn_count: String,
157        #[serde(rename = "connId")]
158        conn_id: String,
159    },
160    OrderResponse {
161        id: Option<String>,
162        op: OKXWsOperation,
163        code: String,
164        msg: String,
165        data: Vec<serde_json::Value>,
166    },
167    BookData {
168        arg: OKXWebSocketArg,
169        action: OKXBookAction,
170        data: Vec<OKXBookMsg>,
171    },
172    Data {
173        arg: OKXWebSocketArg,
174        data: serde_json::Value,
175    },
176    Error {
177        code: String,
178        msg: String,
179    },
180    #[serde(skip)]
181    Ping,
182    #[serde(skip)]
183    Reconnected,
184}
185
186#[derive(Debug, Serialize, Deserialize)]
187#[serde(rename_all = "camelCase")]
188pub struct OKXWebSocketArg {
189    /// Channel name that pushed the data.
190    pub channel: OKXWsChannel,
191    #[serde(default)]
192    pub inst_id: Option<Ustr>,
193    #[serde(default)]
194    pub inst_type: Option<OKXInstrumentType>,
195    #[serde(default)]
196    pub inst_family: Option<Ustr>,
197    #[serde(default)]
198    pub bar: Option<Ustr>,
199}
200
201/// Ticker data for an instrument.
202#[derive(Debug, Serialize, Deserialize)]
203#[serde(rename_all = "camelCase")]
204pub struct OKXTickerMsg {
205    /// Instrument type, e.g. "SPOT", "SWAP".
206    pub inst_type: OKXInstrumentType,
207    /// Instrument ID, e.g. "BTC-USDT".
208    pub inst_id: Ustr,
209    /// Last traded price.
210    #[serde(rename = "last")]
211    pub last_px: String,
212    /// Last traded size.
213    pub last_sz: String,
214    /// Best ask price.
215    pub ask_px: String,
216    /// Best ask size.
217    pub ask_sz: String,
218    /// Best bid price.
219    pub bid_px: String,
220    /// Best bid size.
221    pub bid_sz: String,
222    /// 24-hour opening price.
223    pub open24h: String,
224    /// 24-hour highest price.
225    pub high24h: String,
226    /// 24-hour lowest price.
227    pub low24h: String,
228    /// 24-hour trading volume in quote currency.
229    pub vol_ccy_24h: String,
230    /// 24-hour trading volume.
231    pub vol24h: String,
232    /// The opening price of the day (UTC 0).
233    pub sod_utc0: String,
234    /// The opening price of the day (UTC 8).
235    pub sod_utc8: String,
236    /// Timestamp of the data generation, Unix timestamp format in milliseconds.
237    #[serde(deserialize_with = "deserialize_string_to_u64")]
238    pub ts: u64,
239}
240
241/// Represents a single order in the order book.
242#[derive(Debug, Serialize, Deserialize)]
243pub struct OrderBookEntry {
244    /// Price of the order.
245    pub price: String,
246    /// Size of the order.
247    pub size: String,
248    /// Number of liquidated orders.
249    pub liquidated_orders_count: String,
250    /// Total number of orders at this price.
251    pub orders_count: String,
252}
253
254/// Order book data for an instrument.
255#[derive(Debug, Serialize, Deserialize)]
256#[serde(rename_all = "camelCase")]
257pub struct OKXBookMsg {
258    /// Order book asks [price, size, liquidated orders count, orders count].
259    pub asks: Vec<OrderBookEntry>,
260    /// Order book bids [price, size, liquidated orders count, orders count].
261    pub bids: Vec<OrderBookEntry>,
262    /// Checksum value.
263    pub checksum: Option<i64>,
264    /// Sequence ID of the last sent message. Only applicable to books, books-l2-tbt, books50-l2-tbt.
265    pub prev_seq_id: Option<i64>,
266    /// Sequence ID of the current message, implementation details below.
267    pub seq_id: u64,
268    /// Order book generation time, Unix timestamp format in milliseconds, e.g. 1597026383085.
269    #[serde(deserialize_with = "deserialize_string_to_u64")]
270    pub ts: u64,
271}
272
273/// Trade data for an instrument.
274#[derive(Debug, Serialize, Deserialize)]
275#[serde(rename_all = "camelCase")]
276pub struct OKXTradeMsg {
277    /// Instrument ID.
278    pub inst_id: Ustr,
279    /// Trade ID.
280    pub trade_id: String,
281    /// Trade price.
282    pub px: String,
283    /// Trade size.
284    pub sz: String,
285    /// Trade direction (buy or sell).
286    pub side: OKXSide,
287    /// Count.
288    pub count: String,
289    /// Trade timestamp, Unix timestamp format in milliseconds.
290    #[serde(deserialize_with = "deserialize_string_to_u64")]
291    pub ts: u64,
292}
293
294/// Funding rate data for perpetual swaps.
295#[derive(Debug, Serialize, Deserialize)]
296#[serde(rename_all = "camelCase")]
297pub struct OKXFundingRateMsg {
298    /// Instrument ID.
299    pub inst_id: Ustr,
300    /// Current funding rate.
301    pub funding_rate: Ustr,
302    /// Predicted next funding rate.
303    pub next_funding_rate: Ustr,
304    /// Next funding time, Unix timestamp format in milliseconds.
305    #[serde(deserialize_with = "deserialize_string_to_u64")]
306    pub funding_time: u64,
307    /// Message timestamp, Unix timestamp format in milliseconds.
308    #[serde(deserialize_with = "deserialize_string_to_u64")]
309    pub ts: u64,
310}
311
312/// Mark price data for perpetual swaps.
313#[derive(Debug, Serialize, Deserialize)]
314#[serde(rename_all = "camelCase")]
315pub struct OKXMarkPriceMsg {
316    /// Instrument ID.
317    pub inst_id: Ustr,
318    /// Current mark price.
319    pub mark_px: String,
320    /// Timestamp of the data generation, Unix timestamp format in milliseconds.
321    #[serde(deserialize_with = "deserialize_string_to_u64")]
322    pub ts: u64,
323}
324
325/// Index price data.
326#[derive(Debug, Serialize, Deserialize)]
327#[serde(rename_all = "camelCase")]
328pub struct OKXIndexPriceMsg {
329    /// Index name, e.g. "BTC-USD".
330    pub inst_id: Ustr,
331    /// Latest index price.
332    pub idx_px: String,
333    /// 24-hour highest price.
334    pub high24h: String,
335    /// 24-hour lowest price.
336    pub low24h: String,
337    /// 24-hour opening price.
338    pub open24h: String,
339    /// The opening price of the day (UTC 0).
340    pub sod_utc0: String,
341    /// The opening price of the day (UTC 8).
342    pub sod_utc8: String,
343    /// Timestamp of the data generation, Unix timestamp format in milliseconds.
344    #[serde(deserialize_with = "deserialize_string_to_u64")]
345    pub ts: u64,
346}
347
348/// Price limit data (upper and lower limits).
349#[derive(Debug, Serialize, Deserialize)]
350#[serde(rename_all = "camelCase")]
351pub struct OKXPriceLimitMsg {
352    /// Instrument ID.
353    pub inst_id: Ustr,
354    /// Buy limit price.
355    pub buy_lmt: String,
356    /// Sell limit price.
357    pub sell_lmt: String,
358    /// Timestamp of the data generation, Unix timestamp format in milliseconds.
359    #[serde(deserialize_with = "deserialize_string_to_u64")]
360    pub ts: u64,
361}
362
363/// Candlestick data for an instrument.
364#[derive(Debug, Serialize, Deserialize)]
365#[serde(rename_all = "camelCase")]
366pub struct OKXCandleMsg {
367    /// Candlestick timestamp, Unix timestamp format in milliseconds.
368    #[serde(deserialize_with = "deserialize_string_to_u64")]
369    pub ts: u64,
370    /// Opening price.
371    pub o: String,
372    /// Highest price.
373    pub h: String,
374    /// Lowest price.
375    pub l: String,
376    /// Closing price.
377    pub c: String,
378    /// Trading volume in contracts.
379    pub vol: String,
380    /// Trading volume in quote currency.
381    pub vol_ccy: String,
382    pub vol_ccy_quote: String,
383    /// Whether this is a completed candle.
384    pub confirm: OKXCandleConfirm,
385}
386
387/// Open interest data.
388#[derive(Debug, Serialize, Deserialize)]
389#[serde(rename_all = "camelCase")]
390pub struct OKXOpenInterestMsg {
391    /// Instrument ID.
392    pub inst_id: Ustr,
393    /// Open interest in contracts.
394    pub oi: String,
395    /// Open interest in quote currency.
396    pub oi_ccy: String,
397    /// Timestamp of the data generation, Unix timestamp format in milliseconds.
398    #[serde(deserialize_with = "deserialize_string_to_u64")]
399    pub ts: u64,
400}
401
402/// Option market data summary.
403#[derive(Debug, Serialize, Deserialize)]
404#[serde(rename_all = "camelCase")]
405pub struct OKXOptionSummaryMsg {
406    /// Instrument ID.
407    pub inst_id: Ustr,
408    /// Underlying.
409    pub uly: String,
410    /// Delta.
411    pub delta: String,
412    /// Gamma.
413    pub gamma: String,
414    /// Theta.
415    pub theta: String,
416    /// Vega.
417    pub vega: String,
418    /// Black-Scholes implied volatility delta.
419    pub delta_bs: String,
420    /// Black-Scholes implied volatility gamma.
421    pub gamma_bs: String,
422    /// Black-Scholes implied volatility theta.
423    pub theta_bs: String,
424    /// Black-Scholes implied volatility vega.
425    pub vega_bs: String,
426    /// Realized volatility.
427    pub real_vol: String,
428    /// Bid volatility.
429    pub bid_vol: String,
430    /// Ask volatility.
431    pub ask_vol: String,
432    /// Mark volatility.
433    pub mark_vol: String,
434    /// Leverage.
435    pub lever: String,
436    /// Timestamp of the data generation, Unix timestamp format in milliseconds.
437    #[serde(deserialize_with = "deserialize_string_to_u64")]
438    pub ts: u64,
439}
440
441/// Estimated delivery/exercise price data.
442#[derive(Debug, Serialize, Deserialize)]
443#[serde(rename_all = "camelCase")]
444pub struct OKXEstimatedPriceMsg {
445    /// Instrument ID.
446    pub inst_id: Ustr,
447    /// Estimated settlement price.
448    pub settle_px: String,
449    /// Timestamp of the data generation, Unix timestamp format in milliseconds.
450    #[serde(deserialize_with = "deserialize_string_to_u64")]
451    pub ts: u64,
452}
453
454/// Platform status updates.
455#[derive(Debug, Serialize, Deserialize)]
456#[serde(rename_all = "camelCase")]
457pub struct OKXStatusMsg {
458    /// System maintenance status.
459    pub title: Ustr,
460    /// Status type: planned or scheduled.
461    #[serde(rename = "type")]
462    pub status_type: Ustr,
463    /// System maintenance state: canceled, completed, pending, ongoing.
464    pub state: Ustr,
465    /// Expected completion timestamp.
466    pub end_time: Option<String>,
467    /// Planned start timestamp.
468    pub begin_time: Option<String>,
469    /// Service involved.
470    pub service_type: Option<Ustr>,
471    /// Reason for status change.
472    pub reason: Option<String>,
473    /// Timestamp of the data generation, Unix timestamp format in milliseconds.
474    #[serde(deserialize_with = "deserialize_string_to_u64")]
475    pub ts: u64,
476}
477
478/// Order update message from WebSocket orders channel.
479#[derive(Clone, Debug, Serialize, Deserialize)]
480#[serde(rename_all = "camelCase")]
481pub struct OKXOrderMsg {
482    /// Accumulated filled size.
483    #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
484    pub acc_fill_sz: Option<String>,
485    /// Average price.
486    pub avg_px: String,
487    /// Creation time, Unix timestamp in milliseconds.
488    #[serde(deserialize_with = "deserialize_string_to_u64")]
489    pub c_time: u64,
490    /// Cancel source.
491    #[serde(default)]
492    pub cancel_source: Option<String>,
493    /// Cancel source reason.
494    #[serde(default)]
495    pub cancel_source_reason: Option<String>,
496    /// Order category (normal, liquidation, ADL, etc.).
497    pub category: OKXOrderCategory,
498    /// Currency.
499    pub ccy: Ustr,
500    /// Client order ID.
501    pub cl_ord_id: String,
502    /// Parent algo client order ID if present.
503    #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
504    pub algo_cl_ord_id: Option<String>,
505    /// Fee.
506    #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
507    pub fee: Option<String>,
508    /// Fee currency.
509    pub fee_ccy: Ustr,
510    /// Fill price.
511    pub fill_px: String,
512    /// Fill size.
513    pub fill_sz: String,
514    /// Fill time, Unix timestamp in milliseconds.
515    #[serde(deserialize_with = "deserialize_string_to_u64")]
516    pub fill_time: u64,
517    /// Instrument ID.
518    pub inst_id: Ustr,
519    /// Instrument type.
520    pub inst_type: OKXInstrumentType,
521    /// Leverage.
522    pub lever: String,
523    /// Order ID.
524    pub ord_id: Ustr,
525    /// Order type.
526    pub ord_type: OKXOrderType,
527    /// Profit and loss.
528    pub pnl: String,
529    /// Position side.
530    pub pos_side: OKXPositionSide,
531    /// Price (algo orders use ordPx instead).
532    #[serde(default)]
533    pub px: String,
534    /// Reduce only flag.
535    pub reduce_only: String,
536    /// Side.
537    pub side: OKXSide,
538    /// Order state.
539    pub state: OKXOrderStatus,
540    /// Execution type.
541    pub exec_type: OKXExecType,
542    /// Size.
543    pub sz: String,
544    /// Trade mode.
545    pub td_mode: OKXTradeMode,
546    /// Target currency (base_ccy or quote_ccy). Empty for margin modes.
547    #[serde(default, deserialize_with = "deserialize_target_currency_as_none")]
548    pub tgt_ccy: Option<OKXTargetCurrency>,
549    /// Trade ID.
550    pub trade_id: String,
551    /// Last update time, Unix timestamp in milliseconds.
552    #[serde(deserialize_with = "deserialize_string_to_u64")]
553    pub u_time: u64,
554}
555
556/// Represents an algo order message from WebSocket updates.
557#[derive(Clone, Debug, Deserialize, Serialize)]
558#[serde(rename_all = "camelCase")]
559pub struct OKXAlgoOrderMsg {
560    /// Algorithm ID.
561    pub algo_id: String,
562    /// Algorithm client order ID.
563    #[serde(default)]
564    pub algo_cl_ord_id: String,
565    /// Client order ID (empty for algo orders until triggered).
566    pub cl_ord_id: String,
567    /// Order ID (empty until algo order is triggered).
568    pub ord_id: String,
569    /// Instrument ID.
570    pub inst_id: Ustr,
571    /// Instrument type.
572    pub inst_type: OKXInstrumentType,
573    /// Order type (always "trigger" for conditional orders).
574    pub ord_type: OKXOrderType,
575    /// Order state.
576    pub state: OKXOrderStatus,
577    /// Side.
578    pub side: OKXSide,
579    /// Position side.
580    pub pos_side: OKXPositionSide,
581    /// Size.
582    pub sz: String,
583    /// Trigger price.
584    pub trigger_px: String,
585    /// Trigger price type (last, mark, index).
586    pub trigger_px_type: OKXTriggerType,
587    /// Order price (-1 for market orders).
588    pub ord_px: String,
589    /// Trade mode.
590    pub td_mode: OKXTradeMode,
591    /// Leverage.
592    pub lever: String,
593    /// Reduce only flag.
594    pub reduce_only: String,
595    /// Actual filled price.
596    pub actual_px: String,
597    /// Actual filled size.
598    pub actual_sz: String,
599    /// Notional USD value.
600    pub notional_usd: String,
601    /// Creation time, Unix timestamp in milliseconds.
602    #[serde(deserialize_with = "deserialize_string_to_u64")]
603    pub c_time: u64,
604    /// Update time, Unix timestamp in milliseconds.
605    #[serde(deserialize_with = "deserialize_string_to_u64")]
606    pub u_time: u64,
607    /// Trigger time (empty until triggered).
608    pub trigger_time: String,
609    /// Tag.
610    #[serde(default)]
611    pub tag: String,
612}
613
614/// Parameters for WebSocket place order operation.
615#[derive(Clone, Debug, Deserialize, Serialize, Builder)]
616#[builder(setter(into, strip_option))]
617#[serde(rename_all = "camelCase")]
618pub struct WsPostOrderParams {
619    /// Instrument type: SPOT, MARGIN, SWAP, FUTURES, OPTION (optional for WebSocket).
620    #[builder(default)]
621    #[serde(skip_serializing_if = "Option::is_none")]
622    pub inst_type: Option<OKXInstrumentType>,
623    /// Instrument ID, e.g. "BTC-USDT".
624    pub inst_id: Ustr,
625    /// Trading mode: cash, isolated, cross.
626    pub td_mode: OKXTradeMode,
627    /// Margin currency (only for isolated margin).
628    #[builder(default)]
629    #[serde(skip_serializing_if = "Option::is_none")]
630    pub ccy: Option<Ustr>,
631    /// Unique client order ID.
632    #[builder(default)]
633    #[serde(skip_serializing_if = "Option::is_none")]
634    pub cl_ord_id: Option<String>,
635    /// Order side: buy or sell.
636    pub side: OKXSide,
637    /// Position side: long, short, net (optional).
638    #[builder(default)]
639    #[serde(skip_serializing_if = "Option::is_none")]
640    pub pos_side: Option<OKXPositionSide>,
641    /// Order type: limit, market, post_only, fok, ioc, etc.
642    pub ord_type: OKXOrderType,
643    /// Order size.
644    pub sz: String,
645    /// Order price (required for limit orders).
646    #[builder(default)]
647    #[serde(skip_serializing_if = "Option::is_none")]
648    pub px: Option<String>,
649    /// Reduce-only flag.
650    #[builder(default)]
651    #[serde(skip_serializing_if = "Option::is_none")]
652    pub reduce_only: Option<bool>,
653    /// Whether to close the entire position.
654    #[builder(default)]
655    #[serde(rename = "closePosition", skip_serializing_if = "Option::is_none")]
656    pub close_position: Option<bool>,
657    /// Target currency for net orders.
658    #[builder(default)]
659    #[serde(skip_serializing_if = "Option::is_none")]
660    pub tgt_ccy: Option<OKXTargetCurrency>,
661    /// Order tag for categorization.
662    #[builder(default)]
663    #[serde(skip_serializing_if = "Option::is_none")]
664    pub tag: Option<String>,
665}
666
667/// Parameters for WebSocket cancel order operation (instType not included).
668#[derive(Clone, Debug, Default, Deserialize, Serialize, Builder)]
669#[builder(default)]
670#[builder(setter(into, strip_option))]
671#[serde(rename_all = "camelCase")]
672pub struct WsCancelOrderParams {
673    /// Instrument ID, e.g. "BTC-USDT".
674    pub inst_id: Ustr,
675    /// Exchange-assigned order ID.
676    #[serde(skip_serializing_if = "Option::is_none")]
677    pub ord_id: Option<String>,
678    /// User-assigned client order ID.
679    #[serde(skip_serializing_if = "Option::is_none")]
680    pub cl_ord_id: Option<String>,
681}
682
683/// Parameters for WebSocket mass cancel operation.
684#[derive(Clone, Debug, Default, Deserialize, Serialize, Builder)]
685#[builder(default)]
686#[builder(setter(into, strip_option))]
687#[serde(rename_all = "camelCase")]
688pub struct WsMassCancelParams {
689    /// Instrument type.
690    pub inst_type: OKXInstrumentType,
691    /// Instrument family, e.g. "BTC-USD", "BTC-USDT".
692    pub inst_family: Ustr,
693}
694
695/// Parameters for WebSocket amend order operation (instType not included).
696#[derive(Clone, Debug, Default, Deserialize, Serialize, Builder)]
697#[builder(default)]
698#[builder(setter(into, strip_option))]
699#[serde(rename_all = "camelCase")]
700pub struct WsAmendOrderParams {
701    /// Instrument ID, e.g. "BTC-USDT".
702    pub inst_id: Ustr,
703    /// Exchange-assigned order ID (optional if using clOrdId).
704    #[serde(skip_serializing_if = "Option::is_none")]
705    pub ord_id: Option<String>,
706    /// User-assigned client order ID (optional if using ordId).
707    #[serde(skip_serializing_if = "Option::is_none")]
708    pub cl_ord_id: Option<String>,
709    /// New client order ID for the amended order.
710    #[serde(skip_serializing_if = "Option::is_none")]
711    pub new_cl_ord_id: Option<String>,
712    /// New order price (optional).
713    #[serde(skip_serializing_if = "Option::is_none")]
714    pub new_px: Option<String>,
715    /// New order size (optional).
716    #[serde(skip_serializing_if = "Option::is_none")]
717    pub new_sz: Option<String>,
718}
719
720/// Parameters for WebSocket algo order placement.
721#[derive(Clone, Debug, Deserialize, Serialize, Builder)]
722#[builder(setter(into, strip_option))]
723#[serde(rename_all = "camelCase")]
724pub struct WsPostAlgoOrderParams {
725    /// Instrument ID, e.g. "BTC-USDT".
726    pub inst_id: Ustr,
727    /// Trading mode: cash, isolated, cross.
728    pub td_mode: OKXTradeMode,
729    /// Order side: buy or sell.
730    pub side: OKXSide,
731    /// Order type: trigger (for stop orders).
732    pub ord_type: OKXAlgoOrderType,
733    /// Order size.
734    pub sz: String,
735    /// Client order ID (optional).
736    #[builder(default)]
737    #[serde(skip_serializing_if = "Option::is_none")]
738    pub cl_ord_id: Option<String>,
739    /// Position side: long, short, net (optional).
740    #[builder(default)]
741    #[serde(skip_serializing_if = "Option::is_none")]
742    pub pos_side: Option<OKXPositionSide>,
743    /// Trigger price for stop/conditional orders.
744    #[serde(skip_serializing_if = "Option::is_none")]
745    pub trigger_px: Option<String>,
746    /// Trigger price type: last, index, mark.
747    #[builder(default)]
748    #[serde(skip_serializing_if = "Option::is_none")]
749    pub trigger_px_type: Option<OKXTriggerType>,
750    /// Order price (for limit orders after trigger).
751    #[builder(default)]
752    #[serde(skip_serializing_if = "Option::is_none")]
753    pub order_px: Option<String>,
754    /// Reduce-only flag.
755    #[builder(default)]
756    #[serde(skip_serializing_if = "Option::is_none")]
757    pub reduce_only: Option<bool>,
758    /// Order tag for categorization.
759    #[builder(default)]
760    #[serde(skip_serializing_if = "Option::is_none")]
761    pub tag: Option<String>,
762}
763
764/// Parameters for WebSocket cancel algo order operation.
765#[derive(Clone, Debug, Deserialize, Serialize, Builder)]
766#[builder(setter(into, strip_option))]
767#[serde(rename_all = "camelCase")]
768pub struct WsCancelAlgoOrderParams {
769    /// Instrument ID, e.g. "BTC-USDT".
770    pub inst_id: Ustr,
771    /// Algo order ID.
772    #[serde(skip_serializing_if = "Option::is_none")]
773    pub algo_id: Option<String>,
774    /// Client algo order ID.
775    #[serde(skip_serializing_if = "Option::is_none")]
776    pub algo_cl_ord_id: Option<String>,
777}
778
779////////////////////////////////////////////////////////////////////////////////
780// Tests
781////////////////////////////////////////////////////////////////////////////////
782
783#[cfg(test)]
784mod tests {
785    use nautilus_core::time::get_atomic_clock_realtime;
786    use rstest::rstest;
787
788    use super::*;
789
790    #[rstest]
791    fn test_deserialize_websocket_arg() {
792        let json_str = r#"{"channel":"instruments","instType":"SPOT"}"#;
793
794        let result: Result<OKXWebSocketArg, _> = serde_json::from_str(json_str);
795        match result {
796            Ok(arg) => {
797                assert_eq!(arg.channel, OKXWsChannel::Instruments);
798                assert_eq!(arg.inst_type, Some(OKXInstrumentType::Spot));
799                assert_eq!(arg.inst_id, None);
800            }
801            Err(e) => {
802                panic!("Failed to deserialize WebSocket arg: {e}");
803            }
804        }
805    }
806
807    #[rstest]
808    fn test_deserialize_subscribe_variant_direct() {
809        #[derive(Debug, Deserialize)]
810        #[serde(rename_all = "camelCase")]
811        struct SubscribeMsg {
812            event: String,
813            arg: OKXWebSocketArg,
814            conn_id: String,
815        }
816
817        let json_str = r#"{"event":"subscribe","arg":{"channel":"instruments","instType":"SPOT"},"connId":"380cfa6a"}"#;
818
819        let result: Result<SubscribeMsg, _> = serde_json::from_str(json_str);
820        match result {
821            Ok(msg) => {
822                assert_eq!(msg.event, "subscribe");
823                assert_eq!(msg.arg.channel, OKXWsChannel::Instruments);
824                assert_eq!(msg.conn_id, "380cfa6a");
825            }
826            Err(e) => {
827                panic!("Failed to deserialize subscribe message directly: {e}");
828            }
829        }
830    }
831
832    #[rstest]
833    fn test_deserialize_subscribe_confirmation() {
834        let json_str = r#"{"event":"subscribe","arg":{"channel":"instruments","instType":"SPOT"},"connId":"380cfa6a"}"#;
835
836        let result: Result<OKXWsMessage, _> = serde_json::from_str(json_str);
837        match result {
838            Ok(msg) => {
839                if let OKXWsMessage::Subscription {
840                    event,
841                    arg,
842                    conn_id,
843                    ..
844                } = msg
845                {
846                    assert_eq!(event, OKXSubscriptionEvent::Subscribe);
847                    assert_eq!(arg.channel, OKXWsChannel::Instruments);
848                    assert_eq!(conn_id, "380cfa6a");
849                } else {
850                    panic!("Expected Subscribe variant, was: {msg:?}");
851                }
852            }
853            Err(e) => {
854                panic!("Failed to deserialize subscription confirmation: {e}");
855            }
856        }
857    }
858
859    #[rstest]
860    fn test_deserialize_subscribe_with_inst_id() {
861        let json_str = r#"{"event":"subscribe","arg":{"channel":"candle1m","instId":"ETH-USDT"},"connId":"358602f5"}"#;
862
863        let result: Result<OKXWsMessage, _> = serde_json::from_str(json_str);
864        match result {
865            Ok(msg) => {
866                if let OKXWsMessage::Subscription {
867                    event,
868                    arg,
869                    conn_id,
870                    ..
871                } = msg
872                {
873                    assert_eq!(event, OKXSubscriptionEvent::Subscribe);
874                    assert_eq!(arg.channel, OKXWsChannel::Candle1Minute);
875                    assert_eq!(conn_id, "358602f5");
876                } else {
877                    panic!("Expected Subscribe variant, was: {msg:?}");
878                }
879            }
880            Err(e) => {
881                panic!("Failed to deserialize subscription confirmation: {e}");
882            }
883        }
884    }
885
886    #[rstest]
887    fn test_channel_serialization_for_logging() {
888        let channel = OKXWsChannel::Candle1Minute;
889        let serialized = serde_json::to_string(&channel).unwrap();
890        let cleaned = serialized.trim_matches('"').to_string();
891        assert_eq!(cleaned, "candle1m");
892
893        let channel = OKXWsChannel::BboTbt;
894        let serialized = serde_json::to_string(&channel).unwrap();
895        let cleaned = serialized.trim_matches('"').to_string();
896        assert_eq!(cleaned, "bbo-tbt");
897
898        let channel = OKXWsChannel::Trades;
899        let serialized = serde_json::to_string(&channel).unwrap();
900        let cleaned = serialized.trim_matches('"').to_string();
901        assert_eq!(cleaned, "trades");
902    }
903
904    #[rstest]
905    fn test_order_response_with_enum_operation() {
906        let json_str = r#"{"id":"req-123","op":"order","code":"0","msg":"","data":[]}"#;
907        let result: Result<OKXWsMessage, _> = serde_json::from_str(json_str);
908        match result {
909            Ok(OKXWsMessage::OrderResponse {
910                id,
911                op,
912                code,
913                msg,
914                data,
915            }) => {
916                assert_eq!(id, Some("req-123".to_string()));
917                assert_eq!(op, OKXWsOperation::Order);
918                assert_eq!(code, "0");
919                assert_eq!(msg, "");
920                assert!(data.is_empty());
921            }
922            Ok(other) => panic!("Expected OrderResponse, was: {other:?}"),
923            Err(e) => panic!("Failed to deserialize: {e}"),
924        }
925
926        let json_str = r#"{"id":"cancel-456","op":"cancel-order","code":"50001","msg":"Order not found","data":[]}"#;
927        let result: Result<OKXWsMessage, _> = serde_json::from_str(json_str);
928        match result {
929            Ok(OKXWsMessage::OrderResponse {
930                id,
931                op,
932                code,
933                msg,
934                data,
935            }) => {
936                assert_eq!(id, Some("cancel-456".to_string()));
937                assert_eq!(op, OKXWsOperation::CancelOrder);
938                assert_eq!(code, "50001");
939                assert_eq!(msg, "Order not found");
940                assert!(data.is_empty());
941            }
942            Ok(other) => panic!("Expected OrderResponse, was: {other:?}"),
943            Err(e) => panic!("Failed to deserialize: {e}"),
944        }
945
946        let json_str = r#"{"id":"amend-789","op":"amend-order","code":"50002","msg":"Invalid price","data":[]}"#;
947        let result: Result<OKXWsMessage, _> = serde_json::from_str(json_str);
948        match result {
949            Ok(OKXWsMessage::OrderResponse {
950                id,
951                op,
952                code,
953                msg,
954                data,
955            }) => {
956                assert_eq!(id, Some("amend-789".to_string()));
957                assert_eq!(op, OKXWsOperation::AmendOrder);
958                assert_eq!(code, "50002");
959                assert_eq!(msg, "Invalid price");
960                assert!(data.is_empty());
961            }
962            Ok(other) => panic!("Expected OrderResponse, was: {other:?}"),
963            Err(e) => panic!("Failed to deserialize: {e}"),
964        }
965    }
966
967    #[rstest]
968    fn test_operation_enum_serialization() {
969        let op = OKXWsOperation::Order;
970        let serialized = serde_json::to_string(&op).unwrap();
971        assert_eq!(serialized, "\"order\"");
972
973        let op = OKXWsOperation::CancelOrder;
974        let serialized = serde_json::to_string(&op).unwrap();
975        assert_eq!(serialized, "\"cancel-order\"");
976
977        let op = OKXWsOperation::AmendOrder;
978        let serialized = serde_json::to_string(&op).unwrap();
979        assert_eq!(serialized, "\"amend-order\"");
980
981        let op = OKXWsOperation::Subscribe;
982        let serialized = serde_json::to_string(&op).unwrap();
983        assert_eq!(serialized, "\"subscribe\"");
984    }
985
986    #[rstest]
987    fn test_order_response_parsing() {
988        let success_response = r#"{
989            "id": "req-123",
990            "op": "order",
991            "code": "0",
992            "msg": "",
993            "data": [{"sMsg": "Order placed successfully"}]
994        }"#;
995
996        let parsed: OKXWsMessage = serde_json::from_str(success_response).unwrap();
997
998        match parsed {
999            OKXWsMessage::OrderResponse {
1000                id,
1001                op,
1002                code,
1003                msg,
1004                data,
1005            } => {
1006                assert_eq!(id, Some("req-123".to_string()));
1007                assert_eq!(op, OKXWsOperation::Order);
1008                assert_eq!(code, "0");
1009                assert_eq!(msg, "");
1010                assert_eq!(data.len(), 1);
1011            }
1012            _ => panic!("Expected OrderResponse variant"),
1013        }
1014
1015        let failure_response = r#"{
1016            "id": "req-456",
1017            "op": "cancel-order",
1018            "code": "50001",
1019            "msg": "Order not found",
1020            "data": [{"sMsg": "Order with client order ID not found"}]
1021        }"#;
1022
1023        let parsed: OKXWsMessage = serde_json::from_str(failure_response).unwrap();
1024
1025        match parsed {
1026            OKXWsMessage::OrderResponse {
1027                id,
1028                op,
1029                code,
1030                msg,
1031                data,
1032            } => {
1033                assert_eq!(id, Some("req-456".to_string()));
1034                assert_eq!(op, OKXWsOperation::CancelOrder);
1035                assert_eq!(code, "50001");
1036                assert_eq!(msg, "Order not found");
1037                assert_eq!(data.len(), 1);
1038            }
1039            _ => panic!("Expected OrderResponse variant"),
1040        }
1041    }
1042
1043    #[rstest]
1044    fn test_subscription_event_parsing() {
1045        let subscription_json = r#"{
1046            "event": "subscribe",
1047            "arg": {
1048                "channel": "tickers",
1049                "instId": "BTC-USDT"
1050            },
1051            "connId": "a4d3ae55"
1052        }"#;
1053
1054        let parsed: OKXWsMessage = serde_json::from_str(subscription_json).unwrap();
1055
1056        match parsed {
1057            OKXWsMessage::Subscription {
1058                event,
1059                arg,
1060                conn_id,
1061                ..
1062            } => {
1063                assert_eq!(
1064                    event,
1065                    crate::websocket::enums::OKXSubscriptionEvent::Subscribe
1066                );
1067                assert_eq!(arg.channel, OKXWsChannel::Tickers);
1068                assert_eq!(arg.inst_id, Some(Ustr::from("BTC-USDT")));
1069                assert_eq!(conn_id, "a4d3ae55");
1070            }
1071            _ => panic!("Expected Subscription variant"),
1072        }
1073    }
1074
1075    #[rstest]
1076    fn test_login_event_parsing() {
1077        let login_success = r#"{
1078            "event": "login",
1079            "code": "0",
1080            "msg": "Login successful",
1081            "connId": "a4d3ae55"
1082        }"#;
1083
1084        let parsed: OKXWsMessage = serde_json::from_str(login_success).unwrap();
1085
1086        match parsed {
1087            OKXWsMessage::Login {
1088                event,
1089                code,
1090                msg,
1091                conn_id,
1092            } => {
1093                assert_eq!(event, "login");
1094                assert_eq!(code, "0");
1095                assert_eq!(msg, "Login successful");
1096                assert_eq!(conn_id, "a4d3ae55");
1097            }
1098            _ => panic!("Expected Login variant, was: {:?}", parsed),
1099        }
1100    }
1101
1102    #[rstest]
1103    fn test_error_event_parsing() {
1104        let error_json = r#"{
1105            "code": "60012",
1106            "msg": "Invalid request"
1107        }"#;
1108
1109        let parsed: OKXWsMessage = serde_json::from_str(error_json).unwrap();
1110
1111        match parsed {
1112            OKXWsMessage::Error { code, msg } => {
1113                assert_eq!(code, "60012");
1114                assert_eq!(msg, "Invalid request");
1115            }
1116            _ => panic!("Expected Error variant"),
1117        }
1118    }
1119
1120    #[rstest]
1121    fn test_websocket_request_serialization() {
1122        let request = OKXWsRequest {
1123            id: Some("req-123".to_string()),
1124            op: OKXWsOperation::Order,
1125            args: vec![serde_json::json!({
1126                "instId": "BTC-USDT",
1127                "tdMode": "cash",
1128                "side": "buy",
1129                "ordType": "market",
1130                "sz": "0.1"
1131            })],
1132            exp_time: None,
1133        };
1134
1135        let serialized = serde_json::to_string(&request).unwrap();
1136        let parsed: serde_json::Value = serde_json::from_str(&serialized).unwrap();
1137
1138        assert_eq!(parsed["id"], "req-123");
1139        assert_eq!(parsed["op"], "order");
1140        assert!(parsed["args"].is_array());
1141        assert_eq!(parsed["args"].as_array().unwrap().len(), 1);
1142    }
1143
1144    #[rstest]
1145    fn test_subscription_request_serialization() {
1146        let subscription = OKXSubscription {
1147            op: OKXWsOperation::Subscribe,
1148            args: vec![OKXSubscriptionArg {
1149                channel: OKXWsChannel::Tickers,
1150                inst_type: Some(OKXInstrumentType::Spot),
1151                inst_family: None,
1152                inst_id: Some(Ustr::from("BTC-USDT")),
1153            }],
1154        };
1155
1156        let serialized = serde_json::to_string(&subscription).unwrap();
1157        let parsed: serde_json::Value = serde_json::from_str(&serialized).unwrap();
1158
1159        assert_eq!(parsed["op"], "subscribe");
1160        assert!(parsed["args"].is_array());
1161        assert_eq!(parsed["args"][0]["channel"], "tickers");
1162        assert_eq!(parsed["args"][0]["instType"], "SPOT");
1163        assert_eq!(parsed["args"][0]["instId"], "BTC-USDT");
1164    }
1165
1166    #[rstest]
1167    fn test_error_message_extraction() {
1168        let responses = vec![
1169            (
1170                r#"{
1171                "id": "req-123",
1172                "op": "order",
1173                "code": "50001",
1174                "msg": "Order failed",
1175                "data": [{"sMsg": "Insufficient balance"}]
1176            }"#,
1177                "Insufficient balance",
1178            ),
1179            (
1180                r#"{
1181                "id": "req-456",
1182                "op": "cancel-order",
1183                "code": "50002",
1184                "msg": "Cancel failed",
1185                "data": [{}]
1186            }"#,
1187                "Cancel failed",
1188            ),
1189        ];
1190
1191        for (response_json, expected_msg) in responses {
1192            let parsed: OKXWsMessage = serde_json::from_str(response_json).unwrap();
1193
1194            match parsed {
1195                OKXWsMessage::OrderResponse {
1196                    id: _,
1197                    op: _,
1198                    code,
1199                    msg,
1200                    data,
1201                } => {
1202                    assert_ne!(code, "0"); // Error response
1203
1204                    // Extract error message with fallback logic
1205                    let error_msg = data
1206                        .first()
1207                        .and_then(|d| d.get("sMsg"))
1208                        .and_then(|s| s.as_str())
1209                        .filter(|s| !s.is_empty())
1210                        .unwrap_or(&msg);
1211
1212                    assert_eq!(error_msg, expected_msg);
1213                }
1214                _ => panic!("Expected OrderResponse variant"),
1215            }
1216        }
1217    }
1218
1219    #[rstest]
1220    fn test_book_data_parsing() {
1221        let book_data_json = r#"{
1222            "arg": {
1223                "channel": "books",
1224                "instId": "BTC-USDT"
1225            },
1226            "action": "snapshot",
1227            "data": [{
1228                "asks": [["50000.0", "0.1", "0", "1"]],
1229                "bids": [["49999.0", "0.2", "0", "1"]],
1230                "ts": "1640995200000",
1231                "checksum": 123456789,
1232                "seqId": 1000
1233            }]
1234        }"#;
1235
1236        let parsed: OKXWsMessage = serde_json::from_str(book_data_json).unwrap();
1237
1238        match parsed {
1239            OKXWsMessage::BookData { arg, action, data } => {
1240                assert_eq!(arg.channel, OKXWsChannel::Books);
1241                assert_eq!(arg.inst_id, Some(Ustr::from("BTC-USDT")));
1242                assert_eq!(
1243                    action,
1244                    super::super::super::common::enums::OKXBookAction::Snapshot
1245                );
1246                assert_eq!(data.len(), 1);
1247            }
1248            _ => panic!("Expected BookData variant"),
1249        }
1250    }
1251
1252    #[rstest]
1253    fn test_data_event_parsing() {
1254        let data_json = r#"{
1255            "arg": {
1256                "channel": "trades",
1257                "instId": "BTC-USDT"
1258            },
1259            "data": [{
1260                "instId": "BTC-USDT",
1261                "tradeId": "12345",
1262                "px": "50000.0",
1263                "sz": "0.1",
1264                "side": "buy",
1265                "ts": "1640995200000"
1266            }]
1267        }"#;
1268
1269        let parsed: OKXWsMessage = serde_json::from_str(data_json).unwrap();
1270
1271        match parsed {
1272            OKXWsMessage::Data { arg, data } => {
1273                assert_eq!(arg.channel, OKXWsChannel::Trades);
1274                assert_eq!(arg.inst_id, Some(Ustr::from("BTC-USDT")));
1275                assert!(data.is_array());
1276            }
1277            _ => panic!("Expected Data variant"),
1278        }
1279    }
1280
1281    #[rstest]
1282    fn test_nautilus_message_variants() {
1283        let clock = get_atomic_clock_realtime();
1284        let ts_init = clock.get_time_ns();
1285
1286        let error = OKXWebSocketError {
1287            code: "60012".to_string(),
1288            message: "Invalid request".to_string(),
1289            conn_id: None,
1290            timestamp: ts_init.as_u64(),
1291        };
1292        let error_msg = NautilusWsMessage::Error(error);
1293
1294        match error_msg {
1295            NautilusWsMessage::Error(e) => {
1296                assert_eq!(e.code, "60012");
1297                assert_eq!(e.message, "Invalid request");
1298            }
1299            _ => panic!("Expected Error variant"),
1300        }
1301
1302        let raw_scenarios = vec![
1303            ::serde_json::json!({"unknown": "data"}),
1304            ::serde_json::json!({"channel": "unsupported", "data": [1, 2, 3]}),
1305            ::serde_json::json!({"complex": {"nested": {"structure": true}}}),
1306        ];
1307
1308        for raw_data in raw_scenarios {
1309            let raw_msg = NautilusWsMessage::Raw(raw_data.clone());
1310
1311            match raw_msg {
1312                NautilusWsMessage::Raw(data) => {
1313                    assert_eq!(data, raw_data);
1314                }
1315                _ => panic!("Expected Raw variant"),
1316            }
1317        }
1318    }
1319
1320    #[rstest]
1321    fn test_order_response_parsing_success() {
1322        let order_response_json = r#"{
1323            "id": "req-123",
1324            "op": "order",
1325            "code": "0",
1326            "msg": "",
1327            "data": [{"sMsg": "Order placed successfully"}]
1328        }"#;
1329
1330        let parsed: OKXWsMessage = serde_json::from_str(order_response_json).unwrap();
1331
1332        match parsed {
1333            OKXWsMessage::OrderResponse {
1334                id,
1335                op,
1336                code,
1337                msg,
1338                data,
1339            } => {
1340                assert_eq!(id, Some("req-123".to_string()));
1341                assert_eq!(op, OKXWsOperation::Order);
1342                assert_eq!(code, "0");
1343                assert_eq!(msg, "");
1344                assert_eq!(data.len(), 1);
1345            }
1346            _ => panic!("Expected OrderResponse variant"),
1347        }
1348    }
1349
1350    #[rstest]
1351    fn test_order_response_parsing_failure() {
1352        let order_response_json = r#"{
1353            "id": "req-456",
1354            "op": "cancel-order",
1355            "code": "50001",
1356            "msg": "Order not found",
1357            "data": [{"sMsg": "Order with client order ID not found"}]
1358        }"#;
1359
1360        let parsed: OKXWsMessage = serde_json::from_str(order_response_json).unwrap();
1361
1362        match parsed {
1363            OKXWsMessage::OrderResponse {
1364                id,
1365                op,
1366                code,
1367                msg,
1368                data,
1369            } => {
1370                assert_eq!(id, Some("req-456".to_string()));
1371                assert_eq!(op, OKXWsOperation::CancelOrder);
1372                assert_eq!(code, "50001");
1373                assert_eq!(msg, "Order not found");
1374                assert_eq!(data.len(), 1);
1375            }
1376            _ => panic!("Expected OrderResponse variant"),
1377        }
1378    }
1379
1380    #[rstest]
1381    fn test_message_request_serialization() {
1382        let request = OKXWsRequest {
1383            id: Some("req-123".to_string()),
1384            op: OKXWsOperation::Order,
1385            args: vec![::serde_json::json!({
1386                "instId": "BTC-USDT",
1387                "tdMode": "cash",
1388                "side": "buy",
1389                "ordType": "market",
1390                "sz": "0.1"
1391            })],
1392            exp_time: None,
1393        };
1394
1395        let serialized = serde_json::to_string(&request).unwrap();
1396        let parsed: serde_json::Value = serde_json::from_str(&serialized).unwrap();
1397
1398        assert_eq!(parsed["id"], "req-123");
1399        assert_eq!(parsed["op"], "order");
1400        assert!(parsed["args"].is_array());
1401        assert_eq!(parsed["args"].as_array().unwrap().len(), 1);
1402    }
1403}