Skip to main content

nautilus_okx/websocket/
messages.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 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::{
22        AccountState, OrderAccepted, OrderCancelRejected, OrderCanceled, OrderExpired,
23        OrderModifyRejected, OrderRejected, OrderTriggered, OrderUpdated,
24    },
25    instruments::InstrumentAny,
26    reports::{FillReport, OrderStatusReport, PositionStatusReport},
27};
28use serde::{Deserialize, Serialize};
29use ustr::Ustr;
30
31use super::enums::{OKXWsChannel, OKXWsOperation};
32use crate::{
33    common::{
34        enums::{
35            OKXAlgoOrderType, OKXBookAction, OKXCandleConfirm, OKXExecType, OKXInstrumentType,
36            OKXOrderCategory, OKXOrderStatus, OKXOrderType, OKXPositionSide, OKXSide,
37            OKXTargetCurrency, OKXTradeMode, OKXTriggerType,
38        },
39        parse::{
40            deserialize_empty_string_as_none, deserialize_string_to_u64,
41            deserialize_target_currency_as_none,
42        },
43    },
44    websocket::enums::OKXSubscriptionEvent,
45};
46
47#[derive(Debug, Clone)]
48pub enum NautilusWsMessage {
49    Data(Vec<Data>),
50    Deltas(OrderBookDeltas),
51    FundingRates(Vec<FundingRateUpdate>),
52    Instrument(Box<InstrumentAny>),
53    AccountUpdate(AccountState),
54    PositionUpdate(PositionStatusReport),
55    OrderAccepted(OrderAccepted),
56    OrderCanceled(OrderCanceled),
57    OrderExpired(OrderExpired),
58    OrderRejected(OrderRejected),
59    OrderCancelRejected(OrderCancelRejected),
60    OrderModifyRejected(OrderModifyRejected),
61    OrderTriggered(OrderTriggered),
62    OrderUpdated(OrderUpdated),
63    ExecutionReports(Vec<ExecutionReport>),
64    Error(OKXWebSocketError),
65    Raw(serde_json::Value), // Unhandled channels
66    Reconnected,
67    Authenticated,
68}
69
70/// Represents an OKX WebSocket error.
71#[derive(Debug, Clone, Serialize, Deserialize)]
72#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object))]
73pub struct OKXWebSocketError {
74    /// Error code from OKX (e.g., "50101").
75    pub code: String,
76    /// Error message from OKX.
77    pub message: String,
78    /// Connection ID if available.
79    pub conn_id: Option<String>,
80    /// Timestamp when the error occurred.
81    pub timestamp: u64,
82}
83
84#[derive(Debug, Clone)]
85#[allow(clippy::large_enum_variant)]
86pub enum ExecutionReport {
87    Order(OrderStatusReport),
88    Fill(FillReport),
89}
90
91/// Generic WebSocket request for OKX trading commands.
92#[derive(Debug, Serialize)]
93#[serde(rename_all = "camelCase")]
94pub struct OKXWsRequest<T> {
95    /// Client request ID (required for order operations).
96    #[serde(skip_serializing_if = "Option::is_none")]
97    pub id: Option<String>,
98    /// Operation type (order, cancel-order, amend-order).
99    pub op: OKXWsOperation,
100    /// Request effective deadline. Unix timestamp format in milliseconds.
101    /// This is when the request itself expires, not related to order expiration.
102    #[serde(skip_serializing_if = "Option::is_none")]
103    pub exp_time: Option<String>,
104    /// Arguments payload for the operation.
105    pub args: Vec<T>,
106}
107
108/// OKX WebSocket authentication message.
109#[derive(Debug, Serialize)]
110pub struct OKXAuthentication {
111    pub op: &'static str,
112    pub args: Vec<OKXAuthenticationArg>,
113}
114
115/// OKX WebSocket authentication arguments.
116#[derive(Debug, Serialize)]
117#[serde(rename_all = "camelCase")]
118pub struct OKXAuthenticationArg {
119    pub api_key: String,
120    pub passphrase: String,
121    pub timestamp: String,
122    pub sign: String,
123}
124
125#[derive(Debug, Serialize)]
126pub struct OKXSubscription {
127    pub op: OKXWsOperation,
128    pub args: Vec<OKXSubscriptionArg>,
129}
130
131#[derive(Clone, Debug, Serialize)]
132#[serde(rename_all = "camelCase")]
133pub struct OKXSubscriptionArg {
134    pub channel: OKXWsChannel,
135    pub inst_type: Option<OKXInstrumentType>,
136    pub inst_family: Option<Ustr>,
137    pub inst_id: Option<Ustr>,
138}
139
140/// OKX WebSocket message variants.
141///
142/// Uses custom deserialization that checks discriminant fields (event, op, action)
143/// to determine the correct variant.
144#[derive(Debug)]
145pub enum OKXWsMessage {
146    Login {
147        event: String,
148        code: String,
149        msg: String,
150        conn_id: String,
151    },
152    Subscription {
153        event: OKXSubscriptionEvent,
154        arg: OKXWebSocketArg,
155        conn_id: String,
156        code: Option<String>,
157        msg: Option<String>,
158    },
159    ChannelConnCount {
160        event: String,
161        channel: OKXWsChannel,
162        conn_count: String,
163        conn_id: String,
164    },
165    OrderResponse {
166        id: Option<String>,
167        op: OKXWsOperation,
168        code: String,
169        msg: String,
170        data: Vec<serde_json::Value>,
171    },
172    BookData {
173        arg: OKXWebSocketArg,
174        action: OKXBookAction,
175        data: Vec<OKXBookMsg>,
176    },
177    Data {
178        arg: OKXWebSocketArg,
179        data: serde_json::Value,
180    },
181    Error {
182        code: String,
183        msg: String,
184    },
185    Ping,
186    Reconnected,
187}
188
189impl<'de> Deserialize<'de> for OKXWsMessage {
190    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
191    where
192        D: serde::Deserializer<'de>,
193    {
194        use serde::de::Error;
195
196        // Deserialize to a map to inspect discriminant fields first
197        let value = serde_json::Value::deserialize(deserializer)?;
198        let obj = value
199            .as_object()
200            .ok_or_else(|| D::Error::custom("expected JSON object for OKXWsMessage"))?;
201
202        // Check discriminant fields in priority order
203
204        // 1. Check for "event" field - Login, Subscription, ChannelConnCount, or Error
205        if let Some(event) = obj.get("event").and_then(|v| v.as_str()) {
206            if event == "login" {
207                return parse_login(obj);
208            } else if event == "subscribe" || event == "unsubscribe" {
209                return parse_subscription(obj);
210            } else if event == "error" {
211                // All error events (simple or subscription-related) go to parse_error
212                // Extra fields like "arg" and "connId" are ignored
213                return parse_error(obj);
214            } else if obj.contains_key("channel") && obj.contains_key("connCount") {
215                return parse_channel_conn_count(obj);
216            }
217        }
218
219        // 2. Check for "op" field - OrderResponse
220        if obj.contains_key("op") {
221            return parse_order_response(obj);
222        }
223
224        // 3. Check for "action" field with "arg" - BookData
225        if obj.contains_key("action") && obj.contains_key("arg") {
226            return parse_book_data(obj);
227        }
228
229        // 4. Check for "arg" and "data" without "action" - Data
230        if obj.contains_key("arg") && obj.contains_key("data") {
231            return parse_data(obj);
232        }
233
234        // 5. Fallback to Error if it has "code" and "msg"
235        if obj.contains_key("code") && obj.contains_key("msg") {
236            return parse_error(obj);
237        }
238
239        Err(D::Error::custom(format!(
240            "cannot determine OKXWsMessage variant from: {}",
241            serde_json::to_string(&value).unwrap_or_default()
242        )))
243    }
244}
245
246fn parse_login<E: serde::de::Error>(
247    obj: &serde_json::Map<String, serde_json::Value>,
248) -> Result<OKXWsMessage, E> {
249    Ok(OKXWsMessage::Login {
250        event: obj
251            .get("event")
252            .and_then(|v| v.as_str())
253            .map(String::from)
254            .ok_or_else(|| E::missing_field("event"))?,
255        code: obj
256            .get("code")
257            .and_then(|v| v.as_str())
258            .map(String::from)
259            .ok_or_else(|| E::missing_field("code"))?,
260        msg: obj
261            .get("msg")
262            .and_then(|v| v.as_str())
263            .map(String::from)
264            .ok_or_else(|| E::missing_field("msg"))?,
265        conn_id: obj
266            .get("connId")
267            .and_then(|v| v.as_str())
268            .map(String::from)
269            .ok_or_else(|| E::missing_field("connId"))?,
270    })
271}
272
273fn parse_subscription<E: serde::de::Error>(
274    obj: &serde_json::Map<String, serde_json::Value>,
275) -> Result<OKXWsMessage, E> {
276    let event_str = obj
277        .get("event")
278        .and_then(|v| v.as_str())
279        .ok_or_else(|| E::missing_field("event"))?;
280
281    let event: OKXSubscriptionEvent =
282        serde_json::from_value(serde_json::Value::String(event_str.to_string()))
283            .map_err(|e| E::custom(format!("invalid event: {e}")))?;
284
285    let arg: OKXWebSocketArg = obj
286        .get("arg")
287        .cloned()
288        .map(serde_json::from_value)
289        .transpose()
290        .map_err(|e| E::custom(format!("invalid arg: {e}")))?
291        .ok_or_else(|| E::missing_field("arg"))?;
292
293    Ok(OKXWsMessage::Subscription {
294        event,
295        arg,
296        conn_id: obj
297            .get("connId")
298            .and_then(|v| v.as_str())
299            .map(String::from)
300            .ok_or_else(|| E::missing_field("connId"))?,
301        code: obj.get("code").and_then(|v| v.as_str()).map(String::from),
302        msg: obj.get("msg").and_then(|v| v.as_str()).map(String::from),
303    })
304}
305
306fn parse_channel_conn_count<E: serde::de::Error>(
307    obj: &serde_json::Map<String, serde_json::Value>,
308) -> Result<OKXWsMessage, E> {
309    let channel: OKXWsChannel = obj
310        .get("channel")
311        .cloned()
312        .map(serde_json::from_value)
313        .transpose()
314        .map_err(|e| E::custom(format!("invalid channel: {e}")))?
315        .ok_or_else(|| E::missing_field("channel"))?;
316
317    Ok(OKXWsMessage::ChannelConnCount {
318        event: obj
319            .get("event")
320            .and_then(|v| v.as_str())
321            .map(String::from)
322            .ok_or_else(|| E::missing_field("event"))?,
323        channel,
324        conn_count: obj
325            .get("connCount")
326            .and_then(|v| v.as_str())
327            .map(String::from)
328            .ok_or_else(|| E::missing_field("connCount"))?,
329        conn_id: obj
330            .get("connId")
331            .and_then(|v| v.as_str())
332            .map(String::from)
333            .ok_or_else(|| E::missing_field("connId"))?,
334    })
335}
336
337fn parse_order_response<E: serde::de::Error>(
338    obj: &serde_json::Map<String, serde_json::Value>,
339) -> Result<OKXWsMessage, E> {
340    let op: OKXWsOperation = obj
341        .get("op")
342        .cloned()
343        .map(serde_json::from_value)
344        .transpose()
345        .map_err(|e| E::custom(format!("invalid op: {e}")))?
346        .ok_or_else(|| E::missing_field("op"))?;
347
348    let data: Vec<serde_json::Value> = obj
349        .get("data")
350        .cloned()
351        .map(serde_json::from_value)
352        .transpose()
353        .map_err(|e| E::custom(format!("invalid data: {e}")))?
354        .unwrap_or_default();
355
356    Ok(OKXWsMessage::OrderResponse {
357        id: obj.get("id").and_then(|v| v.as_str()).map(String::from),
358        op,
359        code: obj
360            .get("code")
361            .and_then(|v| v.as_str())
362            .map(String::from)
363            .ok_or_else(|| E::missing_field("code"))?,
364        msg: obj
365            .get("msg")
366            .and_then(|v| v.as_str())
367            .map(String::from)
368            .ok_or_else(|| E::missing_field("msg"))?,
369        data,
370    })
371}
372
373fn parse_book_data<E: serde::de::Error>(
374    obj: &serde_json::Map<String, serde_json::Value>,
375) -> Result<OKXWsMessage, E> {
376    let arg: OKXWebSocketArg = obj
377        .get("arg")
378        .cloned()
379        .map(serde_json::from_value)
380        .transpose()
381        .map_err(|e| E::custom(format!("invalid arg: {e}")))?
382        .ok_or_else(|| E::missing_field("arg"))?;
383
384    let action: OKXBookAction = obj
385        .get("action")
386        .cloned()
387        .map(serde_json::from_value)
388        .transpose()
389        .map_err(|e| E::custom(format!("invalid action: {e}")))?
390        .ok_or_else(|| E::missing_field("action"))?;
391
392    let data: Vec<OKXBookMsg> = obj
393        .get("data")
394        .cloned()
395        .map(serde_json::from_value)
396        .transpose()
397        .map_err(|e| E::custom(format!("invalid data: {e}")))?
398        .ok_or_else(|| E::missing_field("data"))?;
399
400    Ok(OKXWsMessage::BookData { arg, action, data })
401}
402
403fn parse_data<E: serde::de::Error>(
404    obj: &serde_json::Map<String, serde_json::Value>,
405) -> Result<OKXWsMessage, E> {
406    let arg: OKXWebSocketArg = obj
407        .get("arg")
408        .cloned()
409        .map(serde_json::from_value)
410        .transpose()
411        .map_err(|e| E::custom(format!("invalid arg: {e}")))?
412        .ok_or_else(|| E::missing_field("arg"))?;
413
414    let data = obj
415        .get("data")
416        .cloned()
417        .ok_or_else(|| E::missing_field("data"))?;
418
419    Ok(OKXWsMessage::Data { arg, data })
420}
421
422fn parse_error<E: serde::de::Error>(
423    obj: &serde_json::Map<String, serde_json::Value>,
424) -> Result<OKXWsMessage, E> {
425    Ok(OKXWsMessage::Error {
426        code: obj
427            .get("code")
428            .and_then(|v| v.as_str())
429            .map(String::from)
430            .ok_or_else(|| E::missing_field("code"))?,
431        msg: obj
432            .get("msg")
433            .and_then(|v| v.as_str())
434            .map(String::from)
435            .ok_or_else(|| E::missing_field("msg"))?,
436    })
437}
438
439#[derive(Debug, Serialize, Deserialize)]
440#[serde(rename_all = "camelCase")]
441pub struct OKXWebSocketArg {
442    /// Channel name that pushed the data.
443    pub channel: OKXWsChannel,
444    #[serde(default)]
445    pub inst_id: Option<Ustr>,
446    #[serde(default)]
447    pub inst_type: Option<OKXInstrumentType>,
448    #[serde(default)]
449    pub inst_family: Option<Ustr>,
450    #[serde(default)]
451    pub bar: Option<Ustr>,
452}
453
454/// Ticker data for an instrument.
455#[derive(Debug, Serialize, Deserialize)]
456#[serde(rename_all = "camelCase")]
457pub struct OKXTickerMsg {
458    /// Instrument type, e.g. "SPOT", "SWAP".
459    pub inst_type: OKXInstrumentType,
460    /// Instrument ID, e.g. "BTC-USDT".
461    pub inst_id: Ustr,
462    /// Last traded price.
463    #[serde(rename = "last")]
464    pub last_px: String,
465    /// Last traded size.
466    pub last_sz: String,
467    /// Best ask price.
468    pub ask_px: String,
469    /// Best ask size.
470    pub ask_sz: String,
471    /// Best bid price.
472    pub bid_px: String,
473    /// Best bid size.
474    pub bid_sz: String,
475    /// 24-hour opening price.
476    pub open24h: String,
477    /// 24-hour highest price.
478    pub high24h: String,
479    /// 24-hour lowest price.
480    pub low24h: String,
481    /// 24-hour trading volume in quote currency.
482    pub vol_ccy_24h: String,
483    /// 24-hour trading volume.
484    pub vol24h: String,
485    /// The opening price of the day (UTC 0).
486    pub sod_utc0: String,
487    /// The opening price of the day (UTC 8).
488    pub sod_utc8: String,
489    /// Timestamp of the data generation, Unix timestamp format in milliseconds.
490    #[serde(deserialize_with = "deserialize_string_to_u64")]
491    pub ts: u64,
492}
493
494/// Represents a single order in the order book.
495#[derive(Debug, Serialize, Deserialize)]
496pub struct OrderBookEntry {
497    /// Price of the order.
498    pub price: String,
499    /// Size of the order.
500    pub size: String,
501    /// Number of liquidated orders.
502    pub liquidated_orders_count: String,
503    /// Total number of orders at this price.
504    pub orders_count: String,
505}
506
507/// Order book data for an instrument.
508#[derive(Debug, Serialize, Deserialize)]
509#[serde(rename_all = "camelCase")]
510pub struct OKXBookMsg {
511    /// Order book asks [price, size, liquidated orders count, orders count].
512    pub asks: Vec<OrderBookEntry>,
513    /// Order book bids [price, size, liquidated orders count, orders count].
514    pub bids: Vec<OrderBookEntry>,
515    /// Checksum value.
516    pub checksum: Option<i64>,
517    /// Sequence ID of the last sent message. Only applicable to books, books-l2-tbt, books50-l2-tbt.
518    pub prev_seq_id: Option<i64>,
519    /// Sequence ID of the current message, implementation details below.
520    pub seq_id: u64,
521    /// Order book generation time, Unix timestamp format in milliseconds, e.g. 1597026383085.
522    #[serde(deserialize_with = "deserialize_string_to_u64")]
523    pub ts: u64,
524}
525
526/// Trade data for an instrument.
527#[derive(Debug, Serialize, Deserialize)]
528#[serde(rename_all = "camelCase")]
529pub struct OKXTradeMsg {
530    /// Instrument ID.
531    pub inst_id: Ustr,
532    /// Trade ID.
533    pub trade_id: String,
534    /// Trade price.
535    pub px: String,
536    /// Trade size.
537    pub sz: String,
538    /// Trade direction (buy or sell).
539    pub side: OKXSide,
540    /// Count.
541    pub count: String,
542    /// Trade timestamp, Unix timestamp format in milliseconds.
543    #[serde(deserialize_with = "deserialize_string_to_u64")]
544    pub ts: u64,
545}
546
547/// Funding rate data for perpetual swaps.
548#[derive(Debug, Serialize, Deserialize)]
549#[serde(rename_all = "camelCase")]
550pub struct OKXFundingRateMsg {
551    /// Instrument ID.
552    pub inst_id: Ustr,
553    /// Current funding rate.
554    pub funding_rate: Ustr,
555    /// Predicted next funding rate.
556    pub next_funding_rate: Ustr,
557    /// Next funding time, Unix timestamp format in milliseconds.
558    #[serde(deserialize_with = "deserialize_string_to_u64")]
559    pub funding_time: u64,
560    /// Message timestamp, Unix timestamp format in milliseconds.
561    #[serde(deserialize_with = "deserialize_string_to_u64")]
562    pub ts: u64,
563}
564
565/// Mark price data for perpetual swaps.
566#[derive(Debug, Serialize, Deserialize)]
567#[serde(rename_all = "camelCase")]
568pub struct OKXMarkPriceMsg {
569    /// Instrument ID.
570    pub inst_id: Ustr,
571    /// Current mark price.
572    pub mark_px: String,
573    /// Timestamp of the data generation, Unix timestamp format in milliseconds.
574    #[serde(deserialize_with = "deserialize_string_to_u64")]
575    pub ts: u64,
576}
577
578/// Index price data.
579#[derive(Debug, Serialize, Deserialize)]
580#[serde(rename_all = "camelCase")]
581pub struct OKXIndexPriceMsg {
582    /// Index name, e.g. "BTC-USD".
583    pub inst_id: Ustr,
584    /// Latest index price.
585    pub idx_px: String,
586    /// 24-hour highest price.
587    pub high24h: String,
588    /// 24-hour lowest price.
589    pub low24h: String,
590    /// 24-hour opening price.
591    pub open24h: String,
592    /// The opening price of the day (UTC 0).
593    pub sod_utc0: String,
594    /// The opening price of the day (UTC 8).
595    pub sod_utc8: String,
596    /// Timestamp of the data generation, Unix timestamp format in milliseconds.
597    #[serde(deserialize_with = "deserialize_string_to_u64")]
598    pub ts: u64,
599}
600
601/// Price limit data (upper and lower limits).
602#[derive(Debug, Serialize, Deserialize)]
603#[serde(rename_all = "camelCase")]
604pub struct OKXPriceLimitMsg {
605    /// Instrument ID.
606    pub inst_id: Ustr,
607    /// Buy limit price.
608    pub buy_lmt: String,
609    /// Sell limit price.
610    pub sell_lmt: String,
611    /// Timestamp of the data generation, Unix timestamp format in milliseconds.
612    #[serde(deserialize_with = "deserialize_string_to_u64")]
613    pub ts: u64,
614}
615
616/// Candlestick data for an instrument.
617#[derive(Debug, Serialize, Deserialize)]
618#[serde(rename_all = "camelCase")]
619pub struct OKXCandleMsg {
620    /// Candlestick timestamp, Unix timestamp format in milliseconds.
621    #[serde(deserialize_with = "deserialize_string_to_u64")]
622    pub ts: u64,
623    /// Opening price.
624    pub o: String,
625    /// Highest price.
626    pub h: String,
627    /// Lowest price.
628    pub l: String,
629    /// Closing price.
630    pub c: String,
631    /// Trading volume in contracts.
632    pub vol: String,
633    /// Trading volume in quote currency.
634    pub vol_ccy: String,
635    pub vol_ccy_quote: String,
636    /// Whether this is a completed candle.
637    pub confirm: OKXCandleConfirm,
638}
639
640/// Open interest data.
641#[derive(Debug, Serialize, Deserialize)]
642#[serde(rename_all = "camelCase")]
643pub struct OKXOpenInterestMsg {
644    /// Instrument ID.
645    pub inst_id: Ustr,
646    /// Open interest in contracts.
647    pub oi: String,
648    /// Open interest in quote currency.
649    pub oi_ccy: String,
650    /// Timestamp of the data generation, Unix timestamp format in milliseconds.
651    #[serde(deserialize_with = "deserialize_string_to_u64")]
652    pub ts: u64,
653}
654
655/// Option market data summary.
656#[derive(Debug, Serialize, Deserialize)]
657#[serde(rename_all = "camelCase")]
658pub struct OKXOptionSummaryMsg {
659    /// Instrument ID.
660    pub inst_id: Ustr,
661    /// Underlying.
662    pub uly: String,
663    /// Delta.
664    pub delta: String,
665    /// Gamma.
666    pub gamma: String,
667    /// Theta.
668    pub theta: String,
669    /// Vega.
670    pub vega: String,
671    /// Black-Scholes implied volatility delta.
672    pub delta_bs: String,
673    /// Black-Scholes implied volatility gamma.
674    pub gamma_bs: String,
675    /// Black-Scholes implied volatility theta.
676    pub theta_bs: String,
677    /// Black-Scholes implied volatility vega.
678    pub vega_bs: String,
679    /// Realized volatility.
680    pub real_vol: String,
681    /// Bid volatility.
682    pub bid_vol: String,
683    /// Ask volatility.
684    pub ask_vol: String,
685    /// Mark volatility.
686    pub mark_vol: String,
687    /// Leverage.
688    pub lever: String,
689    /// Timestamp of the data generation, Unix timestamp format in milliseconds.
690    #[serde(deserialize_with = "deserialize_string_to_u64")]
691    pub ts: u64,
692}
693
694/// Estimated delivery/exercise price data.
695#[derive(Debug, Serialize, Deserialize)]
696#[serde(rename_all = "camelCase")]
697pub struct OKXEstimatedPriceMsg {
698    /// Instrument ID.
699    pub inst_id: Ustr,
700    /// Estimated settlement price.
701    pub settle_px: String,
702    /// Timestamp of the data generation, Unix timestamp format in milliseconds.
703    #[serde(deserialize_with = "deserialize_string_to_u64")]
704    pub ts: u64,
705}
706
707/// Platform status updates.
708#[derive(Debug, Serialize, Deserialize)]
709#[serde(rename_all = "camelCase")]
710pub struct OKXStatusMsg {
711    /// System maintenance status.
712    pub title: Ustr,
713    /// Status type: planned or scheduled.
714    #[serde(rename = "type")]
715    pub status_type: Ustr,
716    /// System maintenance state: canceled, completed, pending, ongoing.
717    pub state: Ustr,
718    /// Expected completion timestamp.
719    pub end_time: Option<String>,
720    /// Planned start timestamp.
721    pub begin_time: Option<String>,
722    /// Service involved.
723    pub service_type: Option<Ustr>,
724    /// Reason for status change.
725    pub reason: Option<String>,
726    /// Timestamp of the data generation, Unix timestamp format in milliseconds.
727    #[serde(deserialize_with = "deserialize_string_to_u64")]
728    pub ts: u64,
729}
730
731/// Order update message from WebSocket orders channel.
732#[derive(Clone, Debug, Serialize, Deserialize)]
733#[serde(rename_all = "camelCase")]
734pub struct OKXOrderMsg {
735    /// Accumulated filled size.
736    #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
737    pub acc_fill_sz: Option<String>,
738    /// Average price.
739    pub avg_px: String,
740    /// Creation time, Unix timestamp in milliseconds.
741    #[serde(deserialize_with = "deserialize_string_to_u64")]
742    pub c_time: u64,
743    /// Cancel source.
744    #[serde(default)]
745    pub cancel_source: Option<String>,
746    /// Cancel source reason.
747    #[serde(default)]
748    pub cancel_source_reason: Option<String>,
749    /// Order category (normal, liquidation, ADL, etc.).
750    pub category: OKXOrderCategory,
751    /// Currency.
752    pub ccy: Ustr,
753    /// Client order ID.
754    pub cl_ord_id: String,
755    /// Parent algo client order ID if present.
756    #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
757    pub algo_cl_ord_id: Option<String>,
758    /// Fee.
759    #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
760    pub fee: Option<String>,
761    /// Fee currency.
762    pub fee_ccy: Ustr,
763    /// Fill price.
764    pub fill_px: String,
765    /// Fill size.
766    pub fill_sz: String,
767    /// Fill time, Unix timestamp in milliseconds.
768    #[serde(deserialize_with = "deserialize_string_to_u64")]
769    pub fill_time: u64,
770    /// Instrument ID.
771    pub inst_id: Ustr,
772    /// Instrument type.
773    pub inst_type: OKXInstrumentType,
774    /// Leverage.
775    pub lever: String,
776    /// Order ID.
777    pub ord_id: Ustr,
778    /// Order type.
779    pub ord_type: OKXOrderType,
780    /// Profit and loss.
781    pub pnl: String,
782    /// Position side.
783    pub pos_side: OKXPositionSide,
784    /// Price (algo orders use ordPx instead).
785    #[serde(default)]
786    pub px: String,
787    /// Reduce only flag.
788    pub reduce_only: String,
789    /// Side.
790    pub side: OKXSide,
791    /// Order state.
792    pub state: OKXOrderStatus,
793    /// Execution type.
794    pub exec_type: OKXExecType,
795    /// Size.
796    pub sz: String,
797    /// Trade mode.
798    pub td_mode: OKXTradeMode,
799    /// Target currency (base_ccy or quote_ccy). Empty for margin modes.
800    #[serde(default, deserialize_with = "deserialize_target_currency_as_none")]
801    pub tgt_ccy: Option<OKXTargetCurrency>,
802    /// Trade ID.
803    pub trade_id: String,
804    /// Last update time, Unix timestamp in milliseconds.
805    #[serde(deserialize_with = "deserialize_string_to_u64")]
806    pub u_time: u64,
807}
808
809/// Represents an algo order message from WebSocket updates.
810#[derive(Clone, Debug, Deserialize, Serialize)]
811#[serde(rename_all = "camelCase")]
812pub struct OKXAlgoOrderMsg {
813    /// Algorithm ID.
814    pub algo_id: String,
815    /// Algorithm client order ID.
816    #[serde(default)]
817    pub algo_cl_ord_id: String,
818    /// Client order ID (empty for algo orders until triggered).
819    pub cl_ord_id: String,
820    /// Order ID (empty until algo order is triggered).
821    pub ord_id: String,
822    /// Instrument ID.
823    pub inst_id: Ustr,
824    /// Instrument type.
825    pub inst_type: OKXInstrumentType,
826    /// Order type (always "trigger" for conditional orders).
827    pub ord_type: OKXOrderType,
828    /// Order state.
829    pub state: OKXOrderStatus,
830    /// Side.
831    pub side: OKXSide,
832    /// Position side.
833    pub pos_side: OKXPositionSide,
834    /// Size.
835    pub sz: String,
836    /// Trigger price.
837    pub trigger_px: String,
838    /// Trigger price type (last, mark, index).
839    pub trigger_px_type: OKXTriggerType,
840    /// Order price (-1 for market orders).
841    pub ord_px: String,
842    /// Trade mode.
843    pub td_mode: OKXTradeMode,
844    /// Leverage.
845    pub lever: String,
846    /// Reduce only flag.
847    pub reduce_only: String,
848    /// Actual filled price.
849    pub actual_px: String,
850    /// Actual filled size.
851    pub actual_sz: String,
852    /// Notional USD value.
853    pub notional_usd: String,
854    /// Creation time, Unix timestamp in milliseconds.
855    #[serde(deserialize_with = "deserialize_string_to_u64")]
856    pub c_time: u64,
857    /// Update time, Unix timestamp in milliseconds.
858    #[serde(deserialize_with = "deserialize_string_to_u64")]
859    pub u_time: u64,
860    /// Trigger time (empty until triggered).
861    pub trigger_time: String,
862    /// Tag.
863    #[serde(default)]
864    pub tag: String,
865}
866
867/// Parameters for WebSocket place order operation.
868#[derive(Clone, Debug, Deserialize, Serialize, Builder)]
869#[builder(setter(into, strip_option))]
870#[serde(rename_all = "camelCase")]
871pub struct WsPostOrderParams {
872    /// Instrument type: SPOT, MARGIN, SWAP, FUTURES, OPTION (optional for WebSocket).
873    #[builder(default)]
874    #[serde(skip_serializing_if = "Option::is_none")]
875    pub inst_type: Option<OKXInstrumentType>,
876    /// Instrument ID, e.g. "BTC-USDT".
877    pub inst_id: Ustr,
878    /// Instrument ID code (numeric). Required for WebSocket order operations per OKX deprecation.
879    #[builder(default)]
880    #[serde(skip_serializing_if = "Option::is_none")]
881    pub inst_id_code: Option<u64>,
882    /// Trading mode: cash, isolated, cross.
883    pub td_mode: OKXTradeMode,
884    /// Margin currency (only for isolated margin).
885    #[builder(default)]
886    #[serde(skip_serializing_if = "Option::is_none")]
887    pub ccy: Option<Ustr>,
888    /// Unique client order ID.
889    #[builder(default)]
890    #[serde(skip_serializing_if = "Option::is_none")]
891    pub cl_ord_id: Option<String>,
892    /// Order side: buy or sell.
893    pub side: OKXSide,
894    /// Position side: long, short, net (optional).
895    #[builder(default)]
896    #[serde(skip_serializing_if = "Option::is_none")]
897    pub pos_side: Option<OKXPositionSide>,
898    /// Order type: limit, market, post_only, fok, ioc, etc.
899    pub ord_type: OKXOrderType,
900    /// Order size.
901    pub sz: String,
902    /// Order price (required for limit orders).
903    #[builder(default)]
904    #[serde(skip_serializing_if = "Option::is_none")]
905    pub px: Option<String>,
906    /// Reduce-only flag.
907    #[builder(default)]
908    #[serde(skip_serializing_if = "Option::is_none")]
909    pub reduce_only: Option<bool>,
910    /// Whether to close the entire position.
911    #[builder(default)]
912    #[serde(rename = "closePosition", skip_serializing_if = "Option::is_none")]
913    pub close_position: Option<bool>,
914    /// Target currency for net orders.
915    #[builder(default)]
916    #[serde(skip_serializing_if = "Option::is_none")]
917    pub tgt_ccy: Option<OKXTargetCurrency>,
918    /// Order tag for categorization.
919    #[builder(default)]
920    #[serde(skip_serializing_if = "Option::is_none")]
921    pub tag: Option<String>,
922}
923
924/// Parameters for WebSocket cancel order operation (instType not included).
925#[derive(Clone, Debug, Default, Deserialize, Serialize, Builder)]
926#[builder(default)]
927#[builder(setter(into, strip_option))]
928#[serde(rename_all = "camelCase")]
929pub struct WsCancelOrderParams {
930    /// Instrument ID, e.g. "BTC-USDT".
931    pub inst_id: Ustr,
932    /// Instrument ID code (numeric). Required for WebSocket order operations per OKX deprecation.
933    #[serde(skip_serializing_if = "Option::is_none")]
934    pub inst_id_code: Option<u64>,
935    /// Exchange-assigned order ID.
936    #[serde(skip_serializing_if = "Option::is_none")]
937    pub ord_id: Option<String>,
938    /// User-assigned client order ID.
939    #[serde(skip_serializing_if = "Option::is_none")]
940    pub cl_ord_id: Option<String>,
941}
942
943/// Parameters for WebSocket mass cancel operation.
944#[derive(Clone, Debug, Default, Deserialize, Serialize, Builder)]
945#[builder(default)]
946#[builder(setter(into, strip_option))]
947#[serde(rename_all = "camelCase")]
948pub struct WsMassCancelParams {
949    /// Instrument type.
950    pub inst_type: OKXInstrumentType,
951    /// Instrument family, e.g. "BTC-USD", "BTC-USDT".
952    pub inst_family: Ustr,
953}
954
955/// Parameters for WebSocket amend order operation (instType not included).
956#[derive(Clone, Debug, Default, Deserialize, Serialize, Builder)]
957#[builder(default)]
958#[builder(setter(into, strip_option))]
959#[serde(rename_all = "camelCase")]
960pub struct WsAmendOrderParams {
961    /// Instrument ID, e.g. "BTC-USDT".
962    pub inst_id: Ustr,
963    /// Instrument ID code (numeric). Required for WebSocket order operations per OKX deprecation.
964    #[serde(skip_serializing_if = "Option::is_none")]
965    pub inst_id_code: Option<u64>,
966    /// Exchange-assigned order ID (optional if using clOrdId).
967    #[serde(skip_serializing_if = "Option::is_none")]
968    pub ord_id: Option<String>,
969    /// User-assigned client order ID (optional if using ordId).
970    #[serde(skip_serializing_if = "Option::is_none")]
971    pub cl_ord_id: Option<String>,
972    /// New client order ID for the amended order.
973    #[serde(skip_serializing_if = "Option::is_none")]
974    pub new_cl_ord_id: Option<String>,
975    /// New order price (optional).
976    #[serde(skip_serializing_if = "Option::is_none")]
977    pub new_px: Option<String>,
978    /// New order size (optional).
979    #[serde(skip_serializing_if = "Option::is_none")]
980    pub new_sz: Option<String>,
981}
982
983/// Parameters for WebSocket algo order placement.
984#[derive(Clone, Debug, Deserialize, Serialize, Builder)]
985#[builder(setter(into, strip_option))]
986#[serde(rename_all = "camelCase")]
987pub struct WsPostAlgoOrderParams {
988    /// Instrument ID, e.g. "BTC-USDT".
989    pub inst_id: Ustr,
990    /// Instrument ID code (numeric). Required for WebSocket order operations per OKX deprecation.
991    #[builder(default)]
992    #[serde(skip_serializing_if = "Option::is_none")]
993    pub inst_id_code: Option<u64>,
994    /// Trading mode: cash, isolated, cross.
995    pub td_mode: OKXTradeMode,
996    /// Order side: buy or sell.
997    pub side: OKXSide,
998    /// Order type: trigger (for stop orders).
999    pub ord_type: OKXAlgoOrderType,
1000    /// Order size.
1001    pub sz: String,
1002    /// Client order ID (optional).
1003    #[builder(default)]
1004    #[serde(skip_serializing_if = "Option::is_none")]
1005    pub cl_ord_id: Option<String>,
1006    /// Position side: long, short, net (optional).
1007    #[builder(default)]
1008    #[serde(skip_serializing_if = "Option::is_none")]
1009    pub pos_side: Option<OKXPositionSide>,
1010    /// Trigger price for stop/conditional orders.
1011    #[serde(skip_serializing_if = "Option::is_none")]
1012    pub trigger_px: Option<String>,
1013    /// Trigger price type: last, index, mark.
1014    #[builder(default)]
1015    #[serde(skip_serializing_if = "Option::is_none")]
1016    pub trigger_px_type: Option<OKXTriggerType>,
1017    /// Order price (for limit orders after trigger).
1018    #[builder(default)]
1019    #[serde(skip_serializing_if = "Option::is_none")]
1020    pub order_px: Option<String>,
1021    /// Reduce-only flag.
1022    #[builder(default)]
1023    #[serde(skip_serializing_if = "Option::is_none")]
1024    pub reduce_only: Option<bool>,
1025    /// Order tag for categorization.
1026    #[builder(default)]
1027    #[serde(skip_serializing_if = "Option::is_none")]
1028    pub tag: Option<String>,
1029}
1030
1031/// Parameters for WebSocket cancel algo order operation.
1032#[derive(Clone, Debug, Deserialize, Serialize, Builder)]
1033#[builder(setter(into, strip_option))]
1034#[serde(rename_all = "camelCase")]
1035pub struct WsCancelAlgoOrderParams {
1036    /// Instrument ID, e.g. "BTC-USDT".
1037    pub inst_id: Ustr,
1038    /// Instrument ID code (numeric). Required for WebSocket order operations per OKX deprecation.
1039    #[builder(default)]
1040    #[serde(skip_serializing_if = "Option::is_none")]
1041    pub inst_id_code: Option<u64>,
1042    /// Algo order ID.
1043    #[serde(skip_serializing_if = "Option::is_none")]
1044    pub algo_id: Option<String>,
1045    /// Client algo order ID.
1046    #[serde(skip_serializing_if = "Option::is_none")]
1047    pub algo_cl_ord_id: Option<String>,
1048}
1049
1050#[cfg(test)]
1051mod tests {
1052    use nautilus_core::time::get_atomic_clock_realtime;
1053    use rstest::rstest;
1054
1055    use super::*;
1056
1057    #[rstest]
1058    fn test_deserialize_websocket_arg() {
1059        let json_str = r#"{"channel":"instruments","instType":"SPOT"}"#;
1060
1061        let result: Result<OKXWebSocketArg, _> = serde_json::from_str(json_str);
1062        match result {
1063            Ok(arg) => {
1064                assert_eq!(arg.channel, OKXWsChannel::Instruments);
1065                assert_eq!(arg.inst_type, Some(OKXInstrumentType::Spot));
1066                assert_eq!(arg.inst_id, None);
1067            }
1068            Err(e) => {
1069                panic!("Failed to deserialize WebSocket arg: {e}");
1070            }
1071        }
1072    }
1073
1074    #[rstest]
1075    fn test_deserialize_subscribe_variant_direct() {
1076        #[derive(Debug, Deserialize)]
1077        #[serde(rename_all = "camelCase")]
1078        struct SubscribeMsg {
1079            event: String,
1080            arg: OKXWebSocketArg,
1081            conn_id: String,
1082        }
1083
1084        let json_str = r#"{"event":"subscribe","arg":{"channel":"instruments","instType":"SPOT"},"connId":"380cfa6a"}"#;
1085
1086        let result: Result<SubscribeMsg, _> = serde_json::from_str(json_str);
1087        match result {
1088            Ok(msg) => {
1089                assert_eq!(msg.event, "subscribe");
1090                assert_eq!(msg.arg.channel, OKXWsChannel::Instruments);
1091                assert_eq!(msg.conn_id, "380cfa6a");
1092            }
1093            Err(e) => {
1094                panic!("Failed to deserialize subscribe message directly: {e}");
1095            }
1096        }
1097    }
1098
1099    #[rstest]
1100    fn test_deserialize_subscribe_confirmation() {
1101        let json_str = r#"{"event":"subscribe","arg":{"channel":"instruments","instType":"SPOT"},"connId":"380cfa6a"}"#;
1102
1103        let result: Result<OKXWsMessage, _> = serde_json::from_str(json_str);
1104        match result {
1105            Ok(msg) => {
1106                if let OKXWsMessage::Subscription {
1107                    event,
1108                    arg,
1109                    conn_id,
1110                    ..
1111                } = msg
1112                {
1113                    assert_eq!(event, OKXSubscriptionEvent::Subscribe);
1114                    assert_eq!(arg.channel, OKXWsChannel::Instruments);
1115                    assert_eq!(conn_id, "380cfa6a");
1116                } else {
1117                    panic!("Expected Subscribe variant, was: {msg:?}");
1118                }
1119            }
1120            Err(e) => {
1121                panic!("Failed to deserialize subscription confirmation: {e}");
1122            }
1123        }
1124    }
1125
1126    #[rstest]
1127    fn test_deserialize_subscribe_with_inst_id() {
1128        let json_str = r#"{"event":"subscribe","arg":{"channel":"candle1m","instId":"ETH-USDT"},"connId":"358602f5"}"#;
1129
1130        let result: Result<OKXWsMessage, _> = serde_json::from_str(json_str);
1131        match result {
1132            Ok(msg) => {
1133                if let OKXWsMessage::Subscription {
1134                    event,
1135                    arg,
1136                    conn_id,
1137                    ..
1138                } = msg
1139                {
1140                    assert_eq!(event, OKXSubscriptionEvent::Subscribe);
1141                    assert_eq!(arg.channel, OKXWsChannel::Candle1Minute);
1142                    assert_eq!(conn_id, "358602f5");
1143                } else {
1144                    panic!("Expected Subscribe variant, was: {msg:?}");
1145                }
1146            }
1147            Err(e) => {
1148                panic!("Failed to deserialize subscription confirmation: {e}");
1149            }
1150        }
1151    }
1152
1153    #[rstest]
1154    fn test_channel_serialization_for_logging() {
1155        let channel = OKXWsChannel::Candle1Minute;
1156        let serialized = serde_json::to_string(&channel).unwrap();
1157        let cleaned = serialized.trim_matches('"').to_string();
1158        assert_eq!(cleaned, "candle1m");
1159
1160        let channel = OKXWsChannel::BboTbt;
1161        let serialized = serde_json::to_string(&channel).unwrap();
1162        let cleaned = serialized.trim_matches('"').to_string();
1163        assert_eq!(cleaned, "bbo-tbt");
1164
1165        let channel = OKXWsChannel::Trades;
1166        let serialized = serde_json::to_string(&channel).unwrap();
1167        let cleaned = serialized.trim_matches('"').to_string();
1168        assert_eq!(cleaned, "trades");
1169    }
1170
1171    #[rstest]
1172    fn test_order_response_with_enum_operation() {
1173        let json_str = r#"{"id":"req-123","op":"order","code":"0","msg":"","data":[]}"#;
1174        let result: Result<OKXWsMessage, _> = serde_json::from_str(json_str);
1175        match result {
1176            Ok(OKXWsMessage::OrderResponse {
1177                id,
1178                op,
1179                code,
1180                msg,
1181                data,
1182            }) => {
1183                assert_eq!(id, Some("req-123".to_string()));
1184                assert_eq!(op, OKXWsOperation::Order);
1185                assert_eq!(code, "0");
1186                assert_eq!(msg, "");
1187                assert!(data.is_empty());
1188            }
1189            Ok(other) => panic!("Expected OrderResponse, was: {other:?}"),
1190            Err(e) => panic!("Failed to deserialize: {e}"),
1191        }
1192
1193        let json_str = r#"{"id":"cancel-456","op":"cancel-order","code":"50001","msg":"Order not found","data":[]}"#;
1194        let result: Result<OKXWsMessage, _> = serde_json::from_str(json_str);
1195        match result {
1196            Ok(OKXWsMessage::OrderResponse {
1197                id,
1198                op,
1199                code,
1200                msg,
1201                data,
1202            }) => {
1203                assert_eq!(id, Some("cancel-456".to_string()));
1204                assert_eq!(op, OKXWsOperation::CancelOrder);
1205                assert_eq!(code, "50001");
1206                assert_eq!(msg, "Order not found");
1207                assert!(data.is_empty());
1208            }
1209            Ok(other) => panic!("Expected OrderResponse, was: {other:?}"),
1210            Err(e) => panic!("Failed to deserialize: {e}"),
1211        }
1212
1213        let json_str = r#"{"id":"amend-789","op":"amend-order","code":"50002","msg":"Invalid price","data":[]}"#;
1214        let result: Result<OKXWsMessage, _> = serde_json::from_str(json_str);
1215        match result {
1216            Ok(OKXWsMessage::OrderResponse {
1217                id,
1218                op,
1219                code,
1220                msg,
1221                data,
1222            }) => {
1223                assert_eq!(id, Some("amend-789".to_string()));
1224                assert_eq!(op, OKXWsOperation::AmendOrder);
1225                assert_eq!(code, "50002");
1226                assert_eq!(msg, "Invalid price");
1227                assert!(data.is_empty());
1228            }
1229            Ok(other) => panic!("Expected OrderResponse, was: {other:?}"),
1230            Err(e) => panic!("Failed to deserialize: {e}"),
1231        }
1232    }
1233
1234    #[rstest]
1235    fn test_operation_enum_serialization() {
1236        let op = OKXWsOperation::Order;
1237        let serialized = serde_json::to_string(&op).unwrap();
1238        assert_eq!(serialized, "\"order\"");
1239
1240        let op = OKXWsOperation::CancelOrder;
1241        let serialized = serde_json::to_string(&op).unwrap();
1242        assert_eq!(serialized, "\"cancel-order\"");
1243
1244        let op = OKXWsOperation::AmendOrder;
1245        let serialized = serde_json::to_string(&op).unwrap();
1246        assert_eq!(serialized, "\"amend-order\"");
1247
1248        let op = OKXWsOperation::Subscribe;
1249        let serialized = serde_json::to_string(&op).unwrap();
1250        assert_eq!(serialized, "\"subscribe\"");
1251    }
1252
1253    #[rstest]
1254    fn test_order_response_parsing() {
1255        let success_response = r#"{
1256            "id": "req-123",
1257            "op": "order",
1258            "code": "0",
1259            "msg": "",
1260            "data": [{"sMsg": "Order placed successfully"}]
1261        }"#;
1262
1263        let parsed: OKXWsMessage = serde_json::from_str(success_response).unwrap();
1264
1265        match parsed {
1266            OKXWsMessage::OrderResponse {
1267                id,
1268                op,
1269                code,
1270                msg,
1271                data,
1272            } => {
1273                assert_eq!(id, Some("req-123".to_string()));
1274                assert_eq!(op, OKXWsOperation::Order);
1275                assert_eq!(code, "0");
1276                assert_eq!(msg, "");
1277                assert_eq!(data.len(), 1);
1278            }
1279            _ => panic!("Expected OrderResponse variant"),
1280        }
1281
1282        let failure_response = r#"{
1283            "id": "req-456",
1284            "op": "cancel-order",
1285            "code": "50001",
1286            "msg": "Order not found",
1287            "data": [{"sMsg": "Order with client order ID not found"}]
1288        }"#;
1289
1290        let parsed: OKXWsMessage = serde_json::from_str(failure_response).unwrap();
1291
1292        match parsed {
1293            OKXWsMessage::OrderResponse {
1294                id,
1295                op,
1296                code,
1297                msg,
1298                data,
1299            } => {
1300                assert_eq!(id, Some("req-456".to_string()));
1301                assert_eq!(op, OKXWsOperation::CancelOrder);
1302                assert_eq!(code, "50001");
1303                assert_eq!(msg, "Order not found");
1304                assert_eq!(data.len(), 1);
1305            }
1306            _ => panic!("Expected OrderResponse variant"),
1307        }
1308    }
1309
1310    #[rstest]
1311    fn test_subscription_event_parsing() {
1312        let subscription_json = r#"{
1313            "event": "subscribe",
1314            "arg": {
1315                "channel": "tickers",
1316                "instId": "BTC-USDT"
1317            },
1318            "connId": "a4d3ae55"
1319        }"#;
1320
1321        let parsed: OKXWsMessage = serde_json::from_str(subscription_json).unwrap();
1322
1323        match parsed {
1324            OKXWsMessage::Subscription {
1325                event,
1326                arg,
1327                conn_id,
1328                ..
1329            } => {
1330                assert_eq!(
1331                    event,
1332                    crate::websocket::enums::OKXSubscriptionEvent::Subscribe
1333                );
1334                assert_eq!(arg.channel, OKXWsChannel::Tickers);
1335                assert_eq!(arg.inst_id, Some(Ustr::from("BTC-USDT")));
1336                assert_eq!(conn_id, "a4d3ae55");
1337            }
1338            _ => panic!("Expected Subscription variant"),
1339        }
1340    }
1341
1342    #[rstest]
1343    fn test_login_event_parsing() {
1344        let login_success = r#"{
1345            "event": "login",
1346            "code": "0",
1347            "msg": "Login successful",
1348            "connId": "a4d3ae55"
1349        }"#;
1350
1351        let parsed: OKXWsMessage = serde_json::from_str(login_success).unwrap();
1352
1353        match parsed {
1354            OKXWsMessage::Login {
1355                event,
1356                code,
1357                msg,
1358                conn_id,
1359            } => {
1360                assert_eq!(event, "login");
1361                assert_eq!(code, "0");
1362                assert_eq!(msg, "Login successful");
1363                assert_eq!(conn_id, "a4d3ae55");
1364            }
1365            _ => panic!("Expected Login variant, was: {parsed:?}"),
1366        }
1367    }
1368
1369    #[rstest]
1370    fn test_error_event_parsing() {
1371        let error_json = r#"{
1372            "code": "60012",
1373            "msg": "Invalid request"
1374        }"#;
1375
1376        let parsed: OKXWsMessage = serde_json::from_str(error_json).unwrap();
1377
1378        match parsed {
1379            OKXWsMessage::Error { code, msg } => {
1380                assert_eq!(code, "60012");
1381                assert_eq!(msg, "Invalid request");
1382            }
1383            _ => panic!("Expected Error variant"),
1384        }
1385    }
1386
1387    #[rstest]
1388    fn test_error_event_with_event_field_parsing() {
1389        // OKX sends error events with "event":"error" field (e.g., login failures)
1390        let error_json = r#"{
1391            "event": "error",
1392            "code": "60018",
1393            "msg": "Invalid sign"
1394        }"#;
1395
1396        let parsed: OKXWsMessage = serde_json::from_str(error_json).unwrap();
1397
1398        match parsed {
1399            OKXWsMessage::Error { code, msg } => {
1400                assert_eq!(code, "60018");
1401                assert_eq!(msg, "Invalid sign");
1402            }
1403            _ => panic!("Expected Error variant, was: {parsed:?}"),
1404        }
1405    }
1406
1407    #[rstest]
1408    fn test_subscription_error_with_arg_field_parsing() {
1409        // OKX sends subscription errors with arg field (channel subscription failures)
1410        let error_json = r#"{
1411            "event": "error",
1412            "arg": {"channel": "tickers", "instId": "INVALID-INST"},
1413            "code": "60012",
1414            "msg": "Invalid request: channel not found",
1415            "connId": "a4d3ae55"
1416        }"#;
1417
1418        let parsed: OKXWsMessage = serde_json::from_str(error_json).unwrap();
1419
1420        match parsed {
1421            OKXWsMessage::Error { code, msg } => {
1422                assert_eq!(code, "60012");
1423                assert_eq!(msg, "Invalid request: channel not found");
1424            }
1425            _ => panic!("Expected Error variant, was: {parsed:?}"),
1426        }
1427    }
1428
1429    #[rstest]
1430    fn test_websocket_request_serialization() {
1431        let request = OKXWsRequest {
1432            id: Some("req-123".to_string()),
1433            op: OKXWsOperation::Order,
1434            args: vec![serde_json::json!({
1435                "instId": "BTC-USDT",
1436                "tdMode": "cash",
1437                "side": "buy",
1438                "ordType": "market",
1439                "sz": "0.1"
1440            })],
1441            exp_time: None,
1442        };
1443
1444        let serialized = serde_json::to_string(&request).unwrap();
1445        let parsed: serde_json::Value = serde_json::from_str(&serialized).unwrap();
1446
1447        assert_eq!(parsed["id"], "req-123");
1448        assert_eq!(parsed["op"], "order");
1449        assert!(parsed["args"].is_array());
1450        assert_eq!(parsed["args"].as_array().unwrap().len(), 1);
1451    }
1452
1453    #[rstest]
1454    fn test_subscription_request_serialization() {
1455        let subscription = OKXSubscription {
1456            op: OKXWsOperation::Subscribe,
1457            args: vec![OKXSubscriptionArg {
1458                channel: OKXWsChannel::Tickers,
1459                inst_type: Some(OKXInstrumentType::Spot),
1460                inst_family: None,
1461                inst_id: Some(Ustr::from("BTC-USDT")),
1462            }],
1463        };
1464
1465        let serialized = serde_json::to_string(&subscription).unwrap();
1466        let parsed: serde_json::Value = serde_json::from_str(&serialized).unwrap();
1467
1468        assert_eq!(parsed["op"], "subscribe");
1469        assert!(parsed["args"].is_array());
1470        assert_eq!(parsed["args"][0]["channel"], "tickers");
1471        assert_eq!(parsed["args"][0]["instType"], "SPOT");
1472        assert_eq!(parsed["args"][0]["instId"], "BTC-USDT");
1473    }
1474
1475    #[rstest]
1476    fn test_error_message_extraction() {
1477        let responses = vec![
1478            (
1479                r#"{
1480                "id": "req-123",
1481                "op": "order",
1482                "code": "50001",
1483                "msg": "Order failed",
1484                "data": [{"sMsg": "Insufficient balance"}]
1485            }"#,
1486                "Insufficient balance",
1487            ),
1488            (
1489                r#"{
1490                "id": "req-456",
1491                "op": "cancel-order",
1492                "code": "50002",
1493                "msg": "Cancel failed",
1494                "data": [{}]
1495            }"#,
1496                "Cancel failed",
1497            ),
1498        ];
1499
1500        for (response_json, expected_msg) in responses {
1501            let parsed: OKXWsMessage = serde_json::from_str(response_json).unwrap();
1502
1503            match parsed {
1504                OKXWsMessage::OrderResponse {
1505                    id: _,
1506                    op: _,
1507                    code,
1508                    msg,
1509                    data,
1510                } => {
1511                    assert_ne!(code, "0"); // Error response
1512
1513                    // Extract error message with fallback logic
1514                    let error_msg = data
1515                        .first()
1516                        .and_then(|d| d.get("sMsg"))
1517                        .and_then(|s| s.as_str())
1518                        .filter(|s| !s.is_empty())
1519                        .unwrap_or(&msg);
1520
1521                    assert_eq!(error_msg, expected_msg);
1522                }
1523                _ => panic!("Expected OrderResponse variant"),
1524            }
1525        }
1526    }
1527
1528    #[rstest]
1529    fn test_book_data_parsing() {
1530        let book_data_json = r#"{
1531            "arg": {
1532                "channel": "books",
1533                "instId": "BTC-USDT"
1534            },
1535            "action": "snapshot",
1536            "data": [{
1537                "asks": [["50000.0", "0.1", "0", "1"]],
1538                "bids": [["49999.0", "0.2", "0", "1"]],
1539                "ts": "1640995200000",
1540                "checksum": 123456789,
1541                "seqId": 1000
1542            }]
1543        }"#;
1544
1545        let parsed: OKXWsMessage = serde_json::from_str(book_data_json).unwrap();
1546
1547        match parsed {
1548            OKXWsMessage::BookData { arg, action, data } => {
1549                assert_eq!(arg.channel, OKXWsChannel::Books);
1550                assert_eq!(arg.inst_id, Some(Ustr::from("BTC-USDT")));
1551                assert_eq!(
1552                    action,
1553                    super::super::super::common::enums::OKXBookAction::Snapshot
1554                );
1555                assert_eq!(data.len(), 1);
1556            }
1557            _ => panic!("Expected BookData variant"),
1558        }
1559    }
1560
1561    #[rstest]
1562    fn test_data_event_parsing() {
1563        let data_json = r#"{
1564            "arg": {
1565                "channel": "trades",
1566                "instId": "BTC-USDT"
1567            },
1568            "data": [{
1569                "instId": "BTC-USDT",
1570                "tradeId": "12345",
1571                "px": "50000.0",
1572                "sz": "0.1",
1573                "side": "buy",
1574                "ts": "1640995200000"
1575            }]
1576        }"#;
1577
1578        let parsed: OKXWsMessage = serde_json::from_str(data_json).unwrap();
1579
1580        match parsed {
1581            OKXWsMessage::Data { arg, data } => {
1582                assert_eq!(arg.channel, OKXWsChannel::Trades);
1583                assert_eq!(arg.inst_id, Some(Ustr::from("BTC-USDT")));
1584                assert!(data.is_array());
1585            }
1586            _ => panic!("Expected Data variant"),
1587        }
1588    }
1589
1590    #[rstest]
1591    fn test_nautilus_message_variants() {
1592        let clock = get_atomic_clock_realtime();
1593        let ts_init = clock.get_time_ns();
1594
1595        let error = OKXWebSocketError {
1596            code: "60012".to_string(),
1597            message: "Invalid request".to_string(),
1598            conn_id: None,
1599            timestamp: ts_init.as_u64(),
1600        };
1601        let error_msg = NautilusWsMessage::Error(error);
1602
1603        match error_msg {
1604            NautilusWsMessage::Error(e) => {
1605                assert_eq!(e.code, "60012");
1606                assert_eq!(e.message, "Invalid request");
1607            }
1608            _ => panic!("Expected Error variant"),
1609        }
1610
1611        let raw_scenarios = vec![
1612            ::serde_json::json!({"unknown": "data"}),
1613            ::serde_json::json!({"channel": "unsupported", "data": [1, 2, 3]}),
1614            ::serde_json::json!({"complex": {"nested": {"structure": true}}}),
1615        ];
1616
1617        for raw_data in raw_scenarios {
1618            let raw_msg = NautilusWsMessage::Raw(raw_data.clone());
1619
1620            match raw_msg {
1621                NautilusWsMessage::Raw(data) => {
1622                    assert_eq!(data, raw_data);
1623                }
1624                _ => panic!("Expected Raw variant"),
1625            }
1626        }
1627    }
1628
1629    #[rstest]
1630    fn test_order_response_parsing_success() {
1631        let order_response_json = r#"{
1632            "id": "req-123",
1633            "op": "order",
1634            "code": "0",
1635            "msg": "",
1636            "data": [{"sMsg": "Order placed successfully"}]
1637        }"#;
1638
1639        let parsed: OKXWsMessage = serde_json::from_str(order_response_json).unwrap();
1640
1641        match parsed {
1642            OKXWsMessage::OrderResponse {
1643                id,
1644                op,
1645                code,
1646                msg,
1647                data,
1648            } => {
1649                assert_eq!(id, Some("req-123".to_string()));
1650                assert_eq!(op, OKXWsOperation::Order);
1651                assert_eq!(code, "0");
1652                assert_eq!(msg, "");
1653                assert_eq!(data.len(), 1);
1654            }
1655            _ => panic!("Expected OrderResponse variant"),
1656        }
1657    }
1658
1659    #[rstest]
1660    fn test_order_response_parsing_failure() {
1661        let order_response_json = r#"{
1662            "id": "req-456",
1663            "op": "cancel-order",
1664            "code": "50001",
1665            "msg": "Order not found",
1666            "data": [{"sMsg": "Order with client order ID not found"}]
1667        }"#;
1668
1669        let parsed: OKXWsMessage = serde_json::from_str(order_response_json).unwrap();
1670
1671        match parsed {
1672            OKXWsMessage::OrderResponse {
1673                id,
1674                op,
1675                code,
1676                msg,
1677                data,
1678            } => {
1679                assert_eq!(id, Some("req-456".to_string()));
1680                assert_eq!(op, OKXWsOperation::CancelOrder);
1681                assert_eq!(code, "50001");
1682                assert_eq!(msg, "Order not found");
1683                assert_eq!(data.len(), 1);
1684            }
1685            _ => panic!("Expected OrderResponse variant"),
1686        }
1687    }
1688
1689    #[rstest]
1690    fn test_message_request_serialization() {
1691        let request = OKXWsRequest {
1692            id: Some("req-123".to_string()),
1693            op: OKXWsOperation::Order,
1694            args: vec![::serde_json::json!({
1695                "instId": "BTC-USDT",
1696                "tdMode": "cash",
1697                "side": "buy",
1698                "ordType": "market",
1699                "sz": "0.1"
1700            })],
1701            exp_time: None,
1702        };
1703
1704        let serialized = serde_json::to_string(&request).unwrap();
1705        let parsed: serde_json::Value = serde_json::from_str(&serialized).unwrap();
1706
1707        assert_eq!(parsed["id"], "req-123");
1708        assert_eq!(parsed["op"], "order");
1709        assert!(parsed["args"].is_array());
1710        assert_eq!(parsed["args"].as_array().unwrap().len(), 1);
1711    }
1712
1713    #[rstest]
1714    fn test_ws_post_order_params_with_inst_id_code() {
1715        use super::WsPostOrderParamsBuilder;
1716        use crate::common::enums::{OKXOrderType, OKXSide, OKXTradeMode};
1717
1718        let params = WsPostOrderParamsBuilder::default()
1719            .inst_id(Ustr::from("BTC-USDT-SWAP"))
1720            .inst_id_code(10459u64)
1721            .td_mode(OKXTradeMode::Cross)
1722            .side(OKXSide::Buy)
1723            .ord_type(OKXOrderType::Limit)
1724            .sz("0.01".to_string())
1725            .px("50000".to_string())
1726            .build()
1727            .unwrap();
1728
1729        let json = serde_json::to_string(&params).unwrap();
1730
1731        // Verify instIdCode is serialized correctly
1732        assert!(json.contains("\"instIdCode\":10459"));
1733        assert!(json.contains("\"instId\":\"BTC-USDT-SWAP\""));
1734    }
1735
1736    #[rstest]
1737    fn test_ws_post_order_params_without_inst_id_code() {
1738        use super::WsPostOrderParamsBuilder;
1739        use crate::common::enums::{OKXOrderType, OKXSide, OKXTradeMode};
1740
1741        let params = WsPostOrderParamsBuilder::default()
1742            .inst_id(Ustr::from("BTC-USDT"))
1743            .td_mode(OKXTradeMode::Cash)
1744            .side(OKXSide::Buy)
1745            .ord_type(OKXOrderType::Market)
1746            .sz("0.01".to_string())
1747            .build()
1748            .unwrap();
1749
1750        let json = serde_json::to_string(&params).unwrap();
1751
1752        // Verify instIdCode is NOT included when None
1753        assert!(!json.contains("instIdCode"));
1754        assert!(json.contains("\"instId\":\"BTC-USDT\""));
1755    }
1756
1757    #[rstest]
1758    fn test_ws_cancel_order_params_with_inst_id_code() {
1759        use super::WsCancelOrderParamsBuilder;
1760
1761        let params = WsCancelOrderParamsBuilder::default()
1762            .inst_id(Ustr::from("ETH-USDT-SWAP"))
1763            .inst_id_code(10461u64)
1764            .ord_id("12345678".to_string())
1765            .build()
1766            .unwrap();
1767
1768        let json = serde_json::to_string(&params).unwrap();
1769
1770        assert!(json.contains("\"instIdCode\":10461"));
1771        assert!(json.contains("\"instId\":\"ETH-USDT-SWAP\""));
1772        assert!(json.contains("\"ordId\":\"12345678\""));
1773    }
1774
1775    #[rstest]
1776    fn test_ws_amend_order_params_with_inst_id_code() {
1777        use super::WsAmendOrderParamsBuilder;
1778
1779        let params = WsAmendOrderParamsBuilder::default()
1780            .inst_id(Ustr::from("BTC-USDT-SWAP"))
1781            .inst_id_code(10459u64)
1782            .cl_ord_id("client123".to_string())
1783            .new_px("51000".to_string())
1784            .build()
1785            .unwrap();
1786
1787        let json = serde_json::to_string(&params).unwrap();
1788
1789        assert!(json.contains("\"instIdCode\":10459"));
1790        assert!(json.contains("\"instId\":\"BTC-USDT-SWAP\""));
1791        assert!(json.contains("\"newPx\":\"51000\""));
1792    }
1793
1794    #[rstest]
1795    fn test_ws_post_algo_order_params_with_inst_id_code() {
1796        use super::WsPostAlgoOrderParamsBuilder;
1797        use crate::common::enums::{OKXAlgoOrderType, OKXSide, OKXTradeMode, OKXTriggerType};
1798
1799        let params = WsPostAlgoOrderParamsBuilder::default()
1800            .inst_id(Ustr::from("BTC-USDT-SWAP"))
1801            .inst_id_code(10459u64)
1802            .td_mode(OKXTradeMode::Cross)
1803            .side(OKXSide::Buy)
1804            .ord_type(OKXAlgoOrderType::Trigger)
1805            .sz("0.01".to_string())
1806            .trigger_px("48000".to_string())
1807            .trigger_px_type(OKXTriggerType::Last)
1808            .build()
1809            .unwrap();
1810
1811        let json = serde_json::to_string(&params).unwrap();
1812
1813        assert!(json.contains("\"instIdCode\":10459"));
1814        assert!(json.contains("\"instId\":\"BTC-USDT-SWAP\""));
1815        assert!(json.contains("\"triggerPx\":\"48000\""));
1816    }
1817
1818    #[rstest]
1819    fn test_ws_cancel_algo_order_params_with_inst_id_code() {
1820        // Test using direct struct construction since builder requires both algo_id and algo_cl_ord_id
1821        let params = WsCancelAlgoOrderParams {
1822            inst_id: Ustr::from("BTC-USDT-SWAP"),
1823            inst_id_code: Some(10459),
1824            algo_id: Some("987654321".to_string()),
1825            algo_cl_ord_id: None,
1826        };
1827
1828        let json = serde_json::to_string(&params).unwrap();
1829
1830        assert!(json.contains("\"instIdCode\":10459"));
1831        assert!(json.contains("\"instId\":\"BTC-USDT-SWAP\""));
1832        assert!(json.contains("\"algoId\":\"987654321\""));
1833    }
1834}