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