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