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