Skip to main content

nautilus_okx/http/
models.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 transfer objects for deserializing OKX HTTP API payloads.
17
18use nautilus_core::serialization::deserialize_optional_decimal;
19use rust_decimal::Decimal;
20use serde::{Deserialize, Serialize};
21use ustr::Ustr;
22
23use crate::common::{
24    models::OKXRpiBookLevel,
25    parse::{
26        deserialize_empty_string_as_none, deserialize_empty_ustr_as_none,
27        deserialize_optional_string_to_u64, deserialize_target_currency_as_none,
28    },
29};
30
31/// Represents a trade tick from the GET /api/v5/market/trades endpoint.
32#[derive(Clone, Debug, Serialize, Deserialize)]
33#[serde(rename_all = "camelCase")]
34pub struct OKXTrade {
35    /// Instrument ID.
36    pub inst_id: Ustr,
37    /// Trade price.
38    pub px: String,
39    /// Trade size.
40    pub sz: String,
41    /// Trade side: buy or sell.
42    pub side: OKXSide,
43    /// Trade ID assigned by OKX.
44    pub trade_id: Ustr,
45    /// Trade timestamp in milliseconds.
46    #[serde(deserialize_with = "deserialize_string_to_u64")]
47    pub ts: u64,
48    /// Trade source (0: normal order, 1: RPI order).
49    #[serde(default)]
50    pub source: Option<String>,
51}
52
53/// Represents a candlestick from the GET /api/v5/market/history-candles endpoint.
54/// The tuple contains [timestamp(ms), open, high, low, close, volume, turnover, base_volume, count].
55#[derive(Clone, Debug, Serialize, Deserialize)]
56pub struct OKXCandlestick(
57    /// Timestamp in milliseconds.
58    pub String,
59    /// Open price.
60    pub String,
61    /// High price.
62    pub String,
63    /// Low price.
64    pub String,
65    /// Close price.
66    pub String,
67    /// Volume.
68    pub String,
69    /// Turnover in quote currency.
70    pub String,
71    /// Base volume.
72    pub String,
73    /// Record count.
74    pub String,
75);
76
77use crate::common::{
78    enums::{
79        OKXAlgoOrderStatus, OKXAlgoOrderType, OKXExecType, OKXInstrumentType, OKXMarginMode,
80        OKXOrderCategory, OKXOrderStatus, OKXOrderType, OKXPositionSide, OKXSide, OKXSpreadState,
81        OKXSpreadType, OKXTargetCurrency, OKXTradeMode, OKXTriggerType, OKXVipLevel,
82    },
83    parse::deserialize_string_to_u64,
84};
85
86/// Represents a mark price from the GET /api/v5/public/mark-price endpoint.
87#[derive(Clone, Debug, Serialize, Deserialize)]
88#[serde(rename_all = "camelCase")]
89pub struct OKXMarkPrice {
90    /// Underlying.
91    pub uly: Option<Ustr>,
92    /// Instrument ID.
93    pub inst_id: Ustr,
94    /// The mark price.
95    pub mark_px: String,
96    /// The timestamp for the mark price.
97    #[serde(deserialize_with = "deserialize_string_to_u64")]
98    pub ts: u64,
99}
100
101/// Represents a price-limit row from the GET /api/v5/public/price-limit endpoint.
102#[derive(Clone, Debug, Serialize, Deserialize)]
103#[serde(rename_all = "camelCase")]
104pub struct OKXPriceLimit {
105    /// Instrument type.
106    pub inst_type: OKXInstrumentType,
107    /// Instrument ID.
108    pub inst_id: Ustr,
109    /// Highest buy limit price.
110    pub buy_lmt: String,
111    /// Lowest sell limit price.
112    pub sell_lmt: String,
113    /// Data timestamp in milliseconds.
114    #[serde(deserialize_with = "deserialize_string_to_u64")]
115    pub ts: u64,
116    /// Whether the price limit is effective.
117    #[serde(default)]
118    pub enabled: bool,
119}
120
121/// Represents an option summary row from the GET /api/v5/public/opt-summary endpoint.
122#[derive(Clone, Debug, Serialize, Deserialize)]
123#[serde(rename_all = "camelCase")]
124pub struct OKXOptionSummary {
125    /// Instrument type.
126    pub inst_type: OKXInstrumentType,
127    /// Instrument ID.
128    pub inst_id: Ustr,
129    /// Underlying index.
130    pub uly: Ustr,
131    /// Bid volatility.
132    pub bid_vol: String,
133    /// Ask volatility.
134    pub ask_vol: String,
135    /// Mark volatility.
136    pub mark_vol: String,
137    /// Forward price.
138    pub fwd_px: String,
139    /// Data timestamp in milliseconds.
140    #[serde(deserialize_with = "deserialize_string_to_u64")]
141    pub ts: u64,
142}
143
144/// Represents a spread from the GET /api/v5/sprd/spreads endpoint.
145#[derive(Clone, Debug, Serialize, Deserialize)]
146#[serde(rename_all = "camelCase")]
147pub struct OKXSpread {
148    /// Spread ID.
149    pub sprd_id: Ustr,
150    /// Spread type.
151    pub sprd_type: OKXSpreadType,
152    /// Spread status.
153    pub state: OKXSpreadState,
154    /// Base currency.
155    pub base_ccy: Ustr,
156    /// Size currency.
157    pub sz_ccy: Ustr,
158    /// Quote currency.
159    pub quote_ccy: Ustr,
160    /// Tick size in quote currency.
161    pub tick_sz: String,
162    /// Minimum order size in size currency.
163    pub min_sz: String,
164    /// Order size increment in size currency.
165    pub lot_sz: String,
166    /// Listing time in milliseconds.
167    #[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
168    pub list_time: Option<u64>,
169    /// Expiry time in milliseconds.
170    #[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
171    pub exp_time: Option<u64>,
172    /// Last update time in milliseconds.
173    #[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
174    pub u_time: Option<u64>,
175    /// Spread legs.
176    pub legs: Vec<OKXSpreadLeg>,
177}
178
179/// Represents a leg in an OKX spread.
180#[derive(Clone, Debug, Serialize, Deserialize)]
181#[serde(rename_all = "camelCase")]
182pub struct OKXSpreadLeg {
183    /// Instrument ID.
184    pub inst_id: Ustr,
185    /// Leg side.
186    pub side: OKXSide,
187}
188
189/// Represents the request body for `POST /api/v5/sprd/order`.
190#[derive(Clone, Debug, Serialize, Deserialize)]
191#[serde(rename_all = "camelCase")]
192pub struct OKXPlaceSpreadOrderRequest {
193    /// Spread ID.
194    pub sprd_id: String,
195    /// Client-supplied order ID.
196    #[serde(skip_serializing_if = "Option::is_none")]
197    pub cl_ord_id: Option<String>,
198    /// Order tag.
199    #[serde(skip_serializing_if = "Option::is_none")]
200    pub tag: Option<String>,
201    /// Order side.
202    pub side: OKXSide,
203    /// Order type.
204    pub ord_type: OKXOrderType,
205    /// Order size.
206    pub sz: String,
207    /// Limit price.
208    #[serde(skip_serializing_if = "Option::is_none")]
209    pub px: Option<String>,
210}
211
212/// Represents the request body for `POST /api/v5/sprd/cancel-order`.
213#[derive(Clone, Debug, Serialize, Deserialize)]
214#[serde(rename_all = "camelCase")]
215pub struct OKXCancelSpreadOrderRequest {
216    /// Order ID.
217    #[serde(skip_serializing_if = "Option::is_none")]
218    pub ord_id: Option<String>,
219    /// Client-supplied order ID.
220    #[serde(skip_serializing_if = "Option::is_none")]
221    pub cl_ord_id: Option<String>,
222}
223
224/// Represents the request body for `POST /api/v5/sprd/mass-cancel`.
225#[derive(Clone, Debug, Serialize, Deserialize)]
226#[serde(rename_all = "camelCase")]
227pub struct OKXCancelAllSpreadOrdersRequest {
228    /// Spread ID.
229    pub sprd_id: String,
230}
231
232/// Represents a spread order from `GET /api/v5/sprd/order` and history endpoints.
233#[derive(Clone, Debug, Serialize, Deserialize)]
234#[serde(rename_all = "camelCase")]
235pub struct OKXSpreadOrder {
236    /// Spread ID.
237    pub sprd_id: Ustr,
238    /// Order ID.
239    pub ord_id: Ustr,
240    /// Client order ID.
241    #[serde(default)]
242    pub cl_ord_id: Ustr,
243    /// Order tag.
244    #[serde(default)]
245    pub tag: String,
246    /// Order side.
247    pub side: OKXSide,
248    /// Order type.
249    pub ord_type: OKXOrderType,
250    /// Order size.
251    pub sz: String,
252    /// Order price.
253    #[serde(default)]
254    pub px: String,
255    /// Average fill price.
256    #[serde(default)]
257    pub avg_px: String,
258    /// Order state.
259    pub state: OKXOrderStatus,
260    /// Accumulated filled size.
261    #[serde(default)]
262    pub acc_fill_sz: String,
263    /// Pending fill size.
264    #[serde(default)]
265    pub pending_fill_sz: String,
266    /// Pending settlement size.
267    #[serde(default)]
268    pub pending_settle_sz: String,
269    /// Canceled size.
270    #[serde(default)]
271    pub canceled_sz: String,
272    /// Last fill size.
273    #[serde(default)]
274    pub fill_sz: String,
275    /// Last fill price.
276    #[serde(default)]
277    pub fill_px: String,
278    /// Trade ID for the last fill, if provided.
279    #[serde(default)]
280    pub trade_id: Ustr,
281    /// Cancel source.
282    #[serde(default)]
283    pub cancel_source: String,
284    /// Request ID for amend responses.
285    #[serde(default)]
286    pub req_id: String,
287    /// Amend result.
288    #[serde(default)]
289    pub amend_result: String,
290    /// Response code.
291    #[serde(default)]
292    pub code: String,
293    /// Response message.
294    #[serde(default)]
295    pub msg: String,
296    /// Creation time in milliseconds.
297    #[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
298    pub c_time: Option<u64>,
299    /// Last update time in milliseconds.
300    #[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
301    pub u_time: Option<u64>,
302}
303
304/// Represents a spread trade from `GET /api/v5/sprd/trades`.
305#[derive(Clone, Debug, Serialize, Deserialize)]
306#[serde(rename_all = "camelCase")]
307pub struct OKXSpreadTrade {
308    /// Spread ID.
309    pub sprd_id: Ustr,
310    /// Trade ID.
311    pub trade_id: Ustr,
312    /// Order ID.
313    pub ord_id: Ustr,
314    /// Client order ID.
315    #[serde(default)]
316    pub cl_ord_id: Ustr,
317    /// Last filled price.
318    pub fill_px: String,
319    /// Last filled quantity.
320    pub fill_sz: String,
321    /// Trade side.
322    pub side: OKXSide,
323    /// Execution type.
324    #[serde(default)]
325    pub exec_type: OKXExecType,
326    /// Fee currency.
327    #[serde(default)]
328    pub fee_ccy: String,
329    /// Fee amount.
330    #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
331    pub fee: Option<String>,
332    /// Timestamp in milliseconds.
333    #[serde(deserialize_with = "deserialize_string_to_u64")]
334    pub ts: u64,
335}
336
337/// Settlement configuration for an OKX event contract series.
338#[derive(Clone, Debug, Default, Serialize, Deserialize)]
339#[serde(rename_all = "camelCase")]
340pub struct OKXEventContractSettlement {
341    /// Settlement method.
342    #[serde(default)]
343    pub method: String,
344    /// Whether the market can settle before expiry.
345    #[serde(default)]
346    pub close_early: bool,
347    /// Settlement source name.
348    #[serde(default)]
349    pub src_name: String,
350    /// Price underlying in OKX symbol format.
351    #[serde(default)]
352    pub underlying: String,
353}
354
355/// Represents an event contract series from the GET /api/v5/public/event-contract/series endpoint.
356#[derive(Clone, Debug, Serialize, Deserialize)]
357#[serde(rename_all = "camelCase")]
358pub struct OKXEventContractSeries {
359    /// Series ID.
360    pub series_id: String,
361    /// Series frequency.
362    #[serde(default)]
363    pub freq: String,
364    /// Series title.
365    #[serde(default)]
366    pub title: String,
367    /// Series category.
368    #[serde(default)]
369    pub category: String,
370    /// Settlement information.
371    #[serde(default)]
372    pub settlement: OKXEventContractSettlement,
373}
374
375/// Represents an event from the GET /api/v5/public/event-contract/events endpoint.
376#[derive(Clone, Debug, Serialize, Deserialize)]
377#[serde(rename_all = "camelCase")]
378pub struct OKXEventContractEvent {
379    /// Series ID.
380    pub series_id: String,
381    /// Event ID.
382    pub event_id: String,
383    /// Fixing time in milliseconds, if available.
384    #[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
385    pub fix_time: Option<u64>,
386    /// Expiry time in milliseconds.
387    #[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
388    pub exp_time: Option<u64>,
389    /// Event state.
390    pub state: String,
391}
392
393/// Represents an event market from the GET /api/v5/public/event-contract/markets endpoint.
394#[derive(Clone, Debug, Serialize, Deserialize)]
395#[serde(rename_all = "camelCase")]
396pub struct OKXEventContractMarket {
397    /// Series ID.
398    pub series_id: String,
399    /// Event ID.
400    pub event_id: String,
401    /// Instrument ID.
402    pub inst_id: Ustr,
403    /// Listing time in milliseconds.
404    #[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
405    pub list_time: Option<u64>,
406    /// Fixing time in milliseconds, if available.
407    #[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
408    pub fix_time: Option<u64>,
409    /// Expiry time in milliseconds.
410    #[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
411    pub exp_time: Option<u64>,
412    /// Market state.
413    pub state: String,
414    /// Whether the market has been disputed.
415    pub disputed: bool,
416    /// Market outcome: 0 unavailable, 1 yes, 2 no.
417    pub outcome: String,
418    /// Minimum expiration value for a yes outcome.
419    pub floor_strike: String,
420    /// Maximum expiration value for a yes outcome, INF when unbounded.
421    #[serde(default)]
422    pub cap_strike: String,
423    /// Settlement value when expired.
424    pub settle_value: String,
425    /// Hit direction: up or dn, empty when not applicable.
426    #[serde(default)]
427    pub hit_dir: String,
428}
429
430/// Represents an index price from the GET /api/v5/public/index-tickers endpoint.
431#[derive(Clone, Debug, Serialize, Deserialize)]
432#[serde(rename_all = "camelCase")]
433pub struct OKXIndexTicker {
434    /// Instrument ID.
435    pub inst_id: Ustr,
436    /// The index price.
437    pub idx_px: String,
438    /// The timestamp for the index price.
439    #[serde(deserialize_with = "deserialize_string_to_u64")]
440    pub ts: u64,
441}
442
443/// Represents an order book level from the GET /api/v5/market/books endpoint.
444/// Each entry is a 4-element tuple: [price, size, liquidated_orders, num_orders].
445pub type OKXOrderBookLevel = (String, String, String, String);
446
447/// Represents an order book snapshot from the GET /api/v5/market/books endpoint.
448#[derive(Clone, Debug, Serialize, Deserialize)]
449#[serde(rename_all = "camelCase")]
450pub struct OKXOrderBookSnapshot {
451    /// Ask levels [price, size, liquidated_orders_count, orders_count].
452    pub asks: Vec<OKXOrderBookLevel>,
453    /// Bid levels [price, size, liquidated_orders_count, orders_count].
454    pub bids: Vec<OKXOrderBookLevel>,
455    /// Timestamp in milliseconds.
456    #[serde(deserialize_with = "deserialize_string_to_u64")]
457    pub ts: u64,
458}
459
460/// Represents an order book snapshot from the GET /api/v5/market/books-rpi endpoint.
461#[derive(Clone, Debug, Serialize, Deserialize)]
462#[serde(rename_all = "camelCase")]
463pub struct OKXRpiOrderBookSnapshot {
464    /// Ask levels [price, total quantity, non-RPI quantity, order count].
465    pub asks: Vec<OKXRpiBookLevel>,
466    /// Bid levels [price, total quantity, non-RPI quantity, order count].
467    pub bids: Vec<OKXRpiBookLevel>,
468    /// Sequence ID of the snapshot.
469    pub seq_id: u64,
470    /// Timestamp in milliseconds.
471    #[serde(deserialize_with = "deserialize_string_to_u64")]
472    pub ts: u64,
473}
474
475/// Represents a funding rate history entry from the GET /api/v5/public/funding-rate-history endpoint.
476#[derive(Clone, Debug, Serialize, Deserialize)]
477#[serde(rename_all = "camelCase")]
478pub struct OKXFundingRateHistory {
479    /// Instrument type.
480    pub inst_type: OKXInstrumentType,
481    /// Instrument ID.
482    pub inst_id: Ustr,
483    /// Funding rate.
484    pub funding_rate: String,
485    /// Realized rate.
486    pub realized_rate: String,
487    /// Funding time, Unix timestamp in milliseconds.
488    #[serde(deserialize_with = "deserialize_string_to_u64")]
489    pub funding_time: u64,
490    /// Funding rate calculation method.
491    #[serde(default)]
492    pub method: Option<String>,
493}
494
495/// Represents a position tier from the GET /api/v5/public/position-tiers endpoint.
496#[derive(Clone, Debug, Serialize, Deserialize)]
497#[serde(rename_all = "camelCase")]
498pub struct OKXPositionTier {
499    /// Underlying.
500    pub uly: Ustr,
501    /// Instrument family.
502    pub inst_family: String,
503    /// Instrument ID.
504    pub inst_id: Ustr,
505    /// Tier level.
506    pub tier: String,
507    /// Minimum size/amount for the tier.
508    pub min_sz: String,
509    /// Maximum size/amount for the tier.
510    pub max_sz: String,
511    /// Maintenance margin requirement rate.
512    pub mmr: String,
513    /// Initial margin requirement rate.
514    pub imr: String,
515    /// Maximum available leverage.
516    pub max_lever: String,
517    /// Option Margin Coefficient (only applicable to options).
518    pub opt_mgn_factor: String,
519    /// Quote currency borrowing amount.
520    pub quote_max_loan: String,
521    /// Base currency borrowing amount.
522    pub base_max_loan: String,
523}
524
525/// Represents an account balance snapshot from `GET /api/v5/account/balance`.
526#[derive(Clone, Debug, Serialize, Deserialize)]
527#[serde(rename_all = "camelCase")]
528pub struct OKXAccount {
529    /// Adjusted/Effective equity in USD.
530    pub adj_eq: String,
531    /// Borrow frozen amount.
532    pub borrow_froz: String,
533    /// Account details by currency.
534    pub details: Vec<OKXBalanceDetail>,
535    /// Initial margin requirement.
536    pub imr: String,
537    /// Isolated margin equity.
538    pub iso_eq: String,
539    /// Margin ratio.
540    pub mgn_ratio: String,
541    /// Maintenance margin requirement.
542    pub mmr: String,
543    /// Notional value in USD for borrow.
544    pub notional_usd_for_borrow: String,
545    /// Notional value in USD for futures.
546    pub notional_usd_for_futures: String,
547    /// Notional value in USD for option.
548    pub notional_usd_for_option: String,
549    /// Notional value in USD for swap.
550    pub notional_usd_for_swap: String,
551    /// Notional value in USD.
552    pub notional_usd: String,
553    /// Order frozen.
554    pub ord_froz: String,
555    /// Total equity in USD.
556    pub total_eq: String,
557    /// Last update time, Unix timestamp in milliseconds.
558    #[serde(deserialize_with = "deserialize_string_to_u64")]
559    pub u_time: u64,
560    /// Unrealized profit and loss.
561    pub upl: String,
562}
563
564/// Represents a balance detail for a single currency in an OKX account.
565#[derive(Clone, Debug, Serialize, Deserialize)]
566#[serde(rename_all = "camelCase")]
567#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object))]
568#[cfg_attr(
569    feature = "python",
570    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.okx")
571)]
572pub struct OKXBalanceDetail {
573    /// Available balance.
574    pub avail_bal: String,
575    /// Available equity.
576    pub avail_eq: String,
577    /// Borrow frozen amount.
578    pub borrow_froz: String,
579    /// Cash balance.
580    pub cash_bal: String,
581    /// Currency.
582    pub ccy: Ustr,
583    /// Cross liability.
584    pub cross_liab: String,
585    /// Discount equity in USD.
586    pub dis_eq: String,
587    /// Equity.
588    pub eq: String,
589    /// Equity in USD.
590    pub eq_usd: String,
591    /// Same-token equity.
592    pub smt_sync_eq: String,
593    /// Copy trading equity.
594    pub spot_copy_trading_eq: String,
595    /// Fixed balance.
596    pub fixed_bal: String,
597    /// Frozen balance.
598    pub frozen_bal: String,
599    /// Initial margin requirement.
600    pub imr: String,
601    /// Interest.
602    pub interest: String,
603    /// Isolated margin equity.
604    pub iso_eq: String,
605    /// Isolated margin liability.
606    pub iso_liab: String,
607    /// Isolated unrealized profit and loss.
608    pub iso_upl: String,
609    /// Liability.
610    pub liab: String,
611    /// Maximum loan amount.
612    pub max_loan: String,
613    /// Margin ratio.
614    pub mgn_ratio: String,
615    /// Maintenance margin requirement.
616    pub mmr: String,
617    /// Notional leverage.
618    pub notional_lever: String,
619    /// Order frozen.
620    pub ord_frozen: String,
621    /// Reward balance.
622    pub reward_bal: String,
623    /// Spot in use amount.
624    #[serde(alias = "spotInUse")]
625    pub spot_in_use_amt: String,
626    /// Cross liability spot in use amount.
627    #[serde(alias = "clSpotInUse")]
628    pub cl_spot_in_use_amt: String,
629    /// Maximum spot in use amount.
630    #[serde(alias = "maxSpotInUse")]
631    pub max_spot_in_use_amt: String,
632    /// Spot isolated balance.
633    pub spot_iso_bal: String,
634    /// Strategy equity.
635    pub stgy_eq: String,
636    /// Time-weighted average price.
637    pub twap: String,
638    /// Last update time, Unix timestamp in milliseconds.
639    #[serde(deserialize_with = "deserialize_string_to_u64")]
640    pub u_time: u64,
641    /// Unrealized profit and loss.
642    pub upl: String,
643    /// Unrealized profit and loss liability.
644    pub upl_liab: String,
645    /// Spot balance.
646    pub spot_bal: String,
647    /// Open average price.
648    pub open_avg_px: String,
649    /// Accumulated average price.
650    pub acc_avg_px: String,
651    /// Spot unrealized profit and loss.
652    pub spot_upl: String,
653    /// Spot unrealized profit and loss ratio.
654    pub spot_upl_ratio: String,
655    /// Total profit and loss.
656    pub total_pnl: String,
657    /// Total profit and loss ratio.
658    pub total_pnl_ratio: String,
659}
660
661/// Represents a single open position from `GET /api/v5/account/positions`.
662#[derive(Clone, Debug, Serialize, Deserialize)]
663#[serde(rename_all = "camelCase")]
664pub struct OKXPosition {
665    /// Instrument ID.
666    pub inst_id: Ustr,
667    /// Instrument type.
668    pub inst_type: OKXInstrumentType,
669    /// Margin mode: isolated/cross.
670    pub mgn_mode: OKXMarginMode,
671    /// Position ID.
672    #[serde(default, deserialize_with = "deserialize_empty_ustr_as_none")]
673    pub pos_id: Option<Ustr>,
674    /// Position side: long/short.
675    pub pos_side: OKXPositionSide,
676    /// Position size.
677    pub pos: String,
678    /// Base currency balance.
679    pub base_bal: String,
680    /// Position currency.
681    pub ccy: String,
682    /// Trading fee.
683    pub fee: String,
684    /// Position leverage.
685    pub lever: String,
686    /// Last traded price.
687    pub last: String,
688    /// Mark price.
689    pub mark_px: String,
690    /// Liquidation price.
691    pub liq_px: String,
692    /// Maintenance margin requirement.
693    pub mmr: String,
694    /// Interest.
695    pub interest: String,
696    /// Trade ID.
697    pub trade_id: Ustr,
698    /// Notional value of position in USD.
699    pub notional_usd: String,
700    /// Average entry price.
701    pub avg_px: String,
702    /// Unrealized profit and loss.
703    pub upl: String,
704    /// Unrealized profit and loss ratio.
705    pub upl_ratio: String,
706    /// Last update time, Unix timestamp in milliseconds.
707    #[serde(deserialize_with = "deserialize_string_to_u64")]
708    pub u_time: u64,
709    /// Position margin.
710    pub margin: String,
711    /// Margin ratio.
712    pub mgn_ratio: String,
713    /// Auto-deleveraging (ADL) ranking.
714    pub adl: String,
715    /// Creation time, Unix timestamp in milliseconds.
716    pub c_time: String,
717    /// Realized profit and loss.
718    pub realized_pnl: String,
719    /// Unrealized profit and loss at last price.
720    pub upl_last_px: String,
721    /// Unrealized profit and loss ratio at last price.
722    pub upl_ratio_last_px: String,
723    /// Available position that can be closed.
724    pub avail_pos: String,
725    /// Breakeven price.
726    pub be_px: String,
727    /// Funding fee.
728    pub funding_fee: String,
729    /// Index price.
730    pub idx_px: String,
731    /// Liquidation penalty.
732    pub liq_penalty: String,
733    /// Option value.
734    pub opt_val: String,
735    /// Pending close order liability value.
736    pub pending_close_ord_liab_val: String,
737    /// Total profit and loss.
738    pub pnl: String,
739    /// Position currency.
740    pub pos_ccy: String,
741    /// Quote currency balance.
742    pub quote_bal: String,
743    /// Borrowed amount in quote currency.
744    pub quote_borrowed: String,
745    /// Interest on quote currency.
746    pub quote_interest: String,
747    /// Amount in use for spot trading.
748    #[serde(alias = "spotInUse")]
749    pub spot_in_use_amt: String,
750    /// Currency in use for spot trading.
751    pub spot_in_use_ccy: String,
752    /// USD price.
753    pub usd_px: String,
754    /// Black-Scholes delta in dollars, only applicable to OPTION.
755    #[serde(default)]
756    pub delta_bs: String,
757    /// Black-Scholes gamma in dollars, only applicable to OPTION.
758    #[serde(default)]
759    pub gamma_bs: String,
760    /// Black-Scholes theta in dollars, only applicable to OPTION.
761    #[serde(default)]
762    pub theta_bs: String,
763    /// Black-Scholes vega in dollars, only applicable to OPTION.
764    #[serde(default)]
765    pub vega_bs: String,
766}
767
768/// Represents the response from `POST /api/v5/trade/order` (place order).
769/// This model is designed to be flexible and handle the minimal fields that the API returns.
770#[derive(Clone, Debug, Serialize, Deserialize)]
771#[serde(rename_all = "camelCase")]
772pub struct OKXPlaceOrderResponse {
773    /// Order ID.
774    #[serde(default)]
775    pub ord_id: Option<Ustr>,
776    /// Client order ID.
777    #[serde(default)]
778    pub cl_ord_id: Option<Ustr>,
779    /// Order tag.
780    #[serde(default)]
781    pub tag: Option<String>,
782    /// Instrument ID (optional - might not be in response).
783    #[serde(default)]
784    pub inst_id: Option<Ustr>,
785    /// Order side (optional).
786    #[serde(default)]
787    pub side: Option<OKXSide>,
788    /// Order type (optional).
789    #[serde(default)]
790    pub ord_type: Option<OKXOrderType>,
791    /// Order size (optional).
792    #[serde(default)]
793    pub sz: Option<String>,
794    /// Order state (optional).
795    pub state: Option<OKXOrderStatus>,
796    /// Price (optional).
797    #[serde(default)]
798    pub px: Option<String>,
799    /// Average price (optional).
800    #[serde(default)]
801    pub avg_px: Option<String>,
802    /// Accumulated filled size.
803    #[serde(default)]
804    pub acc_fill_sz: Option<String>,
805    /// Fill size (optional).
806    #[serde(default)]
807    pub fill_sz: Option<String>,
808    /// Fill price (optional).
809    #[serde(default)]
810    pub fill_px: Option<String>,
811    /// Trade ID (optional).
812    #[serde(default)]
813    pub trade_id: Option<Ustr>,
814    /// Fill time (optional).
815    #[serde(default)]
816    pub fill_time: Option<String>,
817    /// Fee (optional).
818    #[serde(default)]
819    pub fee: Option<String>,
820    /// Fee currency (optional).
821    #[serde(default)]
822    pub fee_ccy: Option<String>,
823    /// Request ID (optional).
824    #[serde(default)]
825    pub req_id: Option<Ustr>,
826    /// Position side (optional).
827    #[serde(default)]
828    pub pos_side: Option<OKXPositionSide>,
829    /// Reduce-only flag (optional).
830    #[serde(default)]
831    pub reduce_only: Option<String>,
832    /// Target currency (optional).
833    #[serde(default, deserialize_with = "deserialize_target_currency_as_none")]
834    pub tgt_ccy: Option<OKXTargetCurrency>,
835    /// Creation time.
836    #[serde(default)]
837    pub c_time: Option<String>,
838    /// Last update time (optional).
839    #[serde(default)]
840    pub u_time: Option<String>,
841    /// The result of the request.
842    #[serde(skip_serializing_if = "Option::is_none")]
843    pub s_code: Option<String>,
844    /// Error message if the request failed.
845    #[serde(skip_serializing_if = "Option::is_none")]
846    pub s_msg: Option<String>,
847    /// Detailed error code if the request failed.
848    #[serde(skip_serializing_if = "Option::is_none")]
849    pub sub_code: Option<String>,
850}
851
852/// Represents an attached TP/SL instruction on `POST /api/v5/trade/order`.
853#[derive(Clone, Debug, Default, Serialize, Deserialize)]
854#[serde(rename_all = "camelCase")]
855pub struct OKXAttachAlgoOrdRequest {
856    /// Client order ID for the attached TP/SL OCO object.
857    #[serde(skip_serializing_if = "Option::is_none")]
858    pub attach_algo_cl_ord_id: Option<String>,
859    /// Stop-loss trigger price.
860    #[serde(skip_serializing_if = "Option::is_none")]
861    pub sl_trigger_px: Option<String>,
862    /// Stop-loss order price.
863    #[serde(skip_serializing_if = "Option::is_none")]
864    pub sl_ord_px: Option<String>,
865    /// Stop-loss trigger price type.
866    #[serde(skip_serializing_if = "Option::is_none")]
867    pub sl_trigger_px_type: Option<OKXTriggerType>,
868    /// Take-profit trigger price.
869    #[serde(skip_serializing_if = "Option::is_none")]
870    pub tp_trigger_px: Option<String>,
871    /// Take-profit order price.
872    #[serde(skip_serializing_if = "Option::is_none")]
873    pub tp_ord_px: Option<String>,
874    /// Take-profit trigger price type.
875    #[serde(skip_serializing_if = "Option::is_none")]
876    pub tp_trigger_px_type: Option<OKXTriggerType>,
877    /// Callback ratio for attached trailing stop orders.
878    #[serde(skip_serializing_if = "Option::is_none")]
879    pub callback_ratio: Option<String>,
880    /// Callback spread for attached trailing stop orders.
881    #[serde(skip_serializing_if = "Option::is_none")]
882    pub callback_spread: Option<String>,
883    /// Activation price for attached trailing stop orders.
884    #[serde(skip_serializing_if = "Option::is_none")]
885    pub active_px: Option<String>,
886    /// New callback ratio for amended attached trailing stop orders.
887    #[serde(skip_serializing_if = "Option::is_none")]
888    pub new_callback_ratio: Option<String>,
889    /// New callback spread for amended attached trailing stop orders.
890    #[serde(skip_serializing_if = "Option::is_none")]
891    pub new_callback_spread: Option<String>,
892    /// New activation price for amended attached trailing stop orders.
893    #[serde(skip_serializing_if = "Option::is_none")]
894    pub new_active_px: Option<String>,
895}
896
897/// Represents the request body for `POST /api/v5/trade/order` (place order).
898#[derive(Clone, Debug, Serialize, Deserialize)]
899#[serde(rename_all = "camelCase")]
900pub struct OKXPlaceOrderRequest {
901    /// Instrument ID.
902    pub inst_id: String,
903    /// Trade mode (cash, cross, isolated).
904    pub td_mode: OKXTradeMode,
905    /// Currency used for margin trading when required by OKX.
906    #[serde(skip_serializing_if = "Option::is_none")]
907    pub ccy: Option<String>,
908    /// Client-supplied order ID.
909    #[serde(skip_serializing_if = "Option::is_none")]
910    pub cl_ord_id: Option<String>,
911    /// Order tag.
912    #[serde(skip_serializing_if = "Option::is_none")]
913    pub tag: Option<String>,
914    /// Order side (buy, sell).
915    pub side: OKXSide,
916    /// Position side for derivatives.
917    #[serde(skip_serializing_if = "Option::is_none")]
918    pub pos_side: Option<OKXPositionSide>,
919    /// Order type.
920    pub ord_type: OKXOrderType,
921    /// Order size.
922    pub sz: String,
923    /// Limit price when required by the order type.
924    #[serde(skip_serializing_if = "Option::is_none")]
925    pub px: Option<String>,
926    /// Price in USD, only applicable to options. Mutually exclusive with `px` and `px_vol`.
927    #[serde(rename = "pxUsd", skip_serializing_if = "Option::is_none")]
928    pub px_usd: Option<String>,
929    /// Price in implied volatility (1 = 100%), only applicable to options.
930    /// Mutually exclusive with `px` and `px_usd`.
931    #[serde(rename = "pxVol", skip_serializing_if = "Option::is_none")]
932    pub px_vol: Option<String>,
933    /// Reduce-only flag.
934    #[serde(skip_serializing_if = "Option::is_none")]
935    pub reduce_only: Option<bool>,
936    /// Target currency for spot market orders.
937    #[serde(skip_serializing_if = "Option::is_none")]
938    pub tgt_ccy: Option<OKXTargetCurrency>,
939    /// Attached TP/SL OCO instructions.
940    #[serde(skip_serializing_if = "Option::is_none")]
941    pub attach_algo_ords: Option<Vec<OKXAttachAlgoOrdRequest>>,
942    /// Event contract speed bump flag. Use "1" for non-post-only EVENTS orders.
943    #[serde(skip_serializing_if = "Option::is_none")]
944    pub speed_bump: Option<String>,
945    /// Event contract market outcome: yes or no.
946    #[serde(skip_serializing_if = "Option::is_none")]
947    pub outcome: Option<String>,
948    /// Slippage tolerance for market orders, expressed as a decimal fraction
949    /// (e.g., "0.005" for 0.5%). Supported instrument/order-type scope is
950    /// venue-controlled; rejected with `54084`/`54085` if exceeded or out of
951    /// the venue's accepted range. See the OKX v5 docs for the current matrix.
952    #[serde(skip_serializing_if = "Option::is_none")]
953    pub slippage_pct: Option<String>,
954    /// Whether the order may take RPI liquidity.
955    #[serde(skip_serializing_if = "Option::is_none")]
956    pub rpi_taker_access: Option<bool>,
957    /// Whether OKX may round the order price to an eligible RPI price.
958    #[serde(skip_serializing_if = "Option::is_none")]
959    pub rpi_px_round: Option<bool>,
960}
961
962/// Represents the request body for `POST /api/v5/trade/amend-order`.
963#[derive(Clone, Debug, Default, Serialize, Deserialize)]
964#[serde(rename_all = "camelCase")]
965pub struct OKXAmendOrderRequest {
966    /// Instrument ID.
967    pub inst_id: String,
968    /// Order ID.
969    #[serde(skip_serializing_if = "Option::is_none")]
970    pub ord_id: Option<String>,
971    /// Client-supplied order ID.
972    #[serde(skip_serializing_if = "Option::is_none")]
973    pub cl_ord_id: Option<String>,
974    /// Client-supplied request ID.
975    #[serde(skip_serializing_if = "Option::is_none")]
976    pub req_id: Option<String>,
977    /// New order size.
978    #[serde(skip_serializing_if = "Option::is_none")]
979    pub new_sz: Option<String>,
980    /// New order price.
981    #[serde(skip_serializing_if = "Option::is_none")]
982    pub new_px: Option<String>,
983    /// Whether the order may take RPI liquidity after amendment.
984    #[serde(skip_serializing_if = "Option::is_none")]
985    pub rpi_taker_access: Option<bool>,
986    /// Whether OKX may round the amended price to an eligible RPI price.
987    #[serde(skip_serializing_if = "Option::is_none")]
988    pub rpi_px_round: Option<bool>,
989}
990
991/// Represents the request body for `POST /api/v5/trade/cancel-batch-orders`.
992#[derive(Clone, Debug, Serialize, Deserialize)]
993#[serde(rename_all = "camelCase")]
994pub struct OKXCancelOrderRequest {
995    /// Instrument ID.
996    pub inst_id: String,
997    /// Instrument ID code (numeric). May be required per OKX deprecation notice.
998    #[serde(rename = "instIdCode", skip_serializing_if = "Option::is_none")]
999    pub inst_id_code: Option<u64>,
1000    /// Order ID.
1001    #[serde(skip_serializing_if = "Option::is_none")]
1002    pub ord_id: Option<String>,
1003    /// Client-supplied order ID.
1004    #[serde(skip_serializing_if = "Option::is_none")]
1005    pub cl_ord_id: Option<String>,
1006}
1007
1008/// Represents a single response item from `POST /api/v5/trade/cancel-batch-orders`.
1009#[derive(Clone, Debug, Serialize, Deserialize)]
1010#[serde(rename_all = "camelCase")]
1011pub struct OKXCancelOrderResponse {
1012    /// Order ID.
1013    pub ord_id: String,
1014    /// Client-supplied order ID.
1015    #[serde(default)]
1016    pub cl_ord_id: Option<String>,
1017    /// The result of the request.
1018    #[serde(skip_serializing_if = "Option::is_none")]
1019    pub s_code: Option<String>,
1020    /// Error message if the request failed.
1021    #[serde(skip_serializing_if = "Option::is_none")]
1022    pub s_msg: Option<String>,
1023    /// Response timestamp.
1024    #[serde(default)]
1025    pub ts: Option<String>,
1026}
1027
1028pub use crate::common::models::OKXAttachedAlgoOrd;
1029
1030/// Represents a single historical order record from `GET /api/v5/trade/orders-history`.
1031#[derive(Clone, Debug, Serialize, Deserialize)]
1032#[serde(rename_all = "camelCase")]
1033pub struct OKXOrderHistory {
1034    /// Order ID.
1035    pub ord_id: Ustr,
1036    /// Client order ID.
1037    pub cl_ord_id: Ustr,
1038    /// Algo order ID (for conditional orders).
1039    #[serde(default)]
1040    pub algo_id: Option<Ustr>,
1041    /// Client-supplied algo order ID (for conditional orders).
1042    #[serde(default)]
1043    pub algo_cl_ord_id: Option<Ustr>,
1044    /// Attached child client order ID if OKX surfaces one at the top level.
1045    #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
1046    pub attach_algo_cl_ord_id: Option<String>,
1047    /// Attached TP/SL child orders associated with the parent order.
1048    #[serde(default)]
1049    pub attach_algo_ords: Vec<OKXAttachedAlgoOrd>,
1050    /// Client account ID (may be omitted by OKX).
1051    #[serde(default)]
1052    pub cl_act_id: Option<Ustr>,
1053    /// Order tag.
1054    pub tag: String,
1055    /// Instrument type.
1056    pub inst_type: OKXInstrumentType,
1057    /// Underlying (optional).
1058    pub uly: Option<Ustr>,
1059    /// Instrument ID.
1060    pub inst_id: Ustr,
1061    /// Order type.
1062    pub ord_type: OKXOrderType,
1063    /// Order size.
1064    pub sz: String,
1065    /// Price (optional).
1066    pub px: String,
1067    /// Price in USD (options only).
1068    #[serde(default)]
1069    pub px_usd: String,
1070    /// Price in implied volatility (options only).
1071    #[serde(default)]
1072    pub px_vol: String,
1073    /// Side.
1074    pub side: OKXSide,
1075    /// Position side.
1076    pub pos_side: OKXPositionSide,
1077    /// Trade mode.
1078    pub td_mode: OKXTradeMode,
1079    /// Reduce-only flag.
1080    pub reduce_only: String,
1081    /// Target currency (optional).
1082    #[serde(default, deserialize_with = "deserialize_target_currency_as_none")]
1083    pub tgt_ccy: Option<OKXTargetCurrency>,
1084    /// Order state.
1085    pub state: OKXOrderStatus,
1086    /// Average price (optional).
1087    pub avg_px: String,
1088    /// Execution fee.
1089    pub fee: String,
1090    /// Fee currency.
1091    pub fee_ccy: String,
1092    /// Filled size (optional).
1093    pub fill_sz: String,
1094    /// Fill price (optional).
1095    pub fill_px: String,
1096    /// Trade ID (optional).
1097    pub trade_id: Ustr,
1098    /// Fill time, Unix timestamp in milliseconds.
1099    #[serde(deserialize_with = "deserialize_string_to_u64")]
1100    pub fill_time: u64,
1101    /// Accumulated filled size.
1102    pub acc_fill_sz: String,
1103    /// Fill fee (optional, may be omitted).
1104    #[serde(default)]
1105    pub fill_fee: Option<String>,
1106    /// Request ID (optional).
1107    #[serde(default)]
1108    pub req_id: Option<Ustr>,
1109    /// Cancelled filled size (optional).
1110    #[serde(default)]
1111    pub cancel_fill_sz: Option<String>,
1112    /// Cancelled total size (optional).
1113    #[serde(default)]
1114    pub cancel_total_sz: Option<String>,
1115    /// Venue cancellation source code.
1116    #[serde(default)]
1117    pub cancel_source: String,
1118    /// Venue cancellation reason.
1119    #[serde(default)]
1120    pub cancel_source_reason: String,
1121    /// Fee discount (optional).
1122    #[serde(default)]
1123    pub fee_discount: Option<String>,
1124    /// Order category (normal, liquidation, ADL, etc.).
1125    pub category: OKXOrderCategory,
1126    /// Event contract market outcome, if applicable.
1127    #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
1128    pub outcome: Option<String>,
1129    /// Last update time, Unix timestamp in milliseconds.
1130    #[serde(deserialize_with = "deserialize_string_to_u64")]
1131    pub u_time: u64,
1132    /// Creation time.
1133    #[serde(deserialize_with = "deserialize_string_to_u64")]
1134    pub c_time: u64,
1135}
1136
1137/// Represents an algo order response from `/trade/order-algo-*` endpoints.
1138#[derive(Clone, Debug, Serialize, Deserialize)]
1139#[serde(rename_all = "camelCase")]
1140pub struct OKXOrderAlgo {
1141    /// Algo order ID assigned by OKX.
1142    pub algo_id: String,
1143    /// Client-specified algo order ID.
1144    #[serde(default)]
1145    pub algo_cl_ord_id: String,
1146    /// Client order ID (empty until triggered).
1147    #[serde(default)]
1148    pub cl_ord_id: String,
1149    /// Latest regular order ID (deprecated by OKX; empty until triggered).
1150    #[serde(default)]
1151    pub ord_id: String,
1152    /// Regular order IDs created after the algo order triggers.
1153    #[serde(default)]
1154    pub ord_id_list: Vec<String>,
1155    /// Child algo order IDs created for split take-profit orders.
1156    #[serde(default)]
1157    pub sub_algo_id_list: Vec<String>,
1158    /// Instrument ID, e.g. `ETH-USDT-SWAP`.
1159    pub inst_id: Ustr,
1160    /// Instrument type.
1161    pub inst_type: OKXInstrumentType,
1162    /// Algo order type.
1163    pub ord_type: OKXAlgoOrderType,
1164    /// Current order state.
1165    pub state: OKXAlgoOrderStatus,
1166    /// Order side.
1167    pub side: OKXSide,
1168    /// Position side.
1169    pub pos_side: OKXPositionSide,
1170    /// Submitted size.
1171    #[serde(default)]
1172    pub sz: String,
1173    /// Trigger price (empty for certain algo styles).
1174    #[serde(default)]
1175    pub trigger_px: String,
1176    /// Trigger price type (last/mark/index).
1177    #[serde(default)]
1178    pub trigger_px_type: Option<OKXTriggerType>,
1179    /// Stop-loss trigger price for conditional close orders.
1180    #[serde(default)]
1181    pub sl_trigger_px: String,
1182    /// Stop-loss order price for conditional close orders.
1183    #[serde(default)]
1184    pub sl_ord_px: String,
1185    /// Stop-loss trigger price type (last/mark/index).
1186    #[serde(default)]
1187    pub sl_trigger_px_type: Option<OKXTriggerType>,
1188    /// Take-profit trigger price for conditional close orders.
1189    #[serde(default)]
1190    pub tp_trigger_px: String,
1191    /// Take-profit order price for conditional close orders.
1192    #[serde(default)]
1193    pub tp_ord_px: String,
1194    /// Take-profit trigger price type (last/mark/index).
1195    #[serde(default)]
1196    pub tp_trigger_px_type: Option<OKXTriggerType>,
1197    /// Order price (-1 indicates market execution once triggered).
1198    #[serde(default)]
1199    pub ord_px: String,
1200    /// Trade mode (cash/cross/isolated).
1201    pub td_mode: OKXTradeMode,
1202    /// Algo leverage configuration.
1203    #[serde(default)]
1204    pub lever: String,
1205    /// Reduce-only flag.
1206    #[serde(default)]
1207    pub reduce_only: String,
1208    /// Fraction of the position to close for close-order algos.
1209    #[serde(default)]
1210    pub close_fraction: String,
1211    /// Executed price (if triggered).
1212    #[serde(default)]
1213    pub actual_px: String,
1214    /// Executed size (if triggered).
1215    #[serde(default)]
1216    pub actual_sz: String,
1217    /// Notional value in USD.
1218    #[serde(default)]
1219    pub notional_usd: String,
1220    /// Creation time (milliseconds).
1221    #[serde(deserialize_with = "deserialize_string_to_u64")]
1222    pub c_time: u64,
1223    /// Last update time (milliseconds).
1224    #[serde(deserialize_with = "deserialize_string_to_u64")]
1225    pub u_time: u64,
1226    /// Trigger timestamp (if triggered).
1227    #[serde(default)]
1228    pub trigger_time: String,
1229    /// Optional tag supplied during submission.
1230    #[serde(default)]
1231    pub tag: String,
1232    /// Callback price ratio for trailing stop (e.g. "0.01" for 1%).
1233    #[serde(default)]
1234    pub callback_ratio: String,
1235    /// Callback price spread for trailing stop (absolute distance).
1236    #[serde(default)]
1237    pub callback_spread: String,
1238    /// Activation price for trailing stop.
1239    #[serde(default)]
1240    pub active_px: String,
1241}
1242
1243/// Represents a transaction detail (fill) from `GET /api/v5/trade/fills`.
1244#[derive(Clone, Debug, Serialize, Deserialize)]
1245#[serde(rename_all = "camelCase")]
1246pub struct OKXTransactionDetail {
1247    /// Product type (SPOT, MARGIN, SWAP, FUTURES, OPTION).
1248    pub inst_type: OKXInstrumentType,
1249    /// Instrument ID, e.g. "BTC-USDT".
1250    pub inst_id: Ustr,
1251    /// Trade ID.
1252    pub trade_id: Ustr,
1253    /// Order ID.
1254    pub ord_id: Ustr,
1255    /// Client order ID.
1256    pub cl_ord_id: Ustr,
1257    /// Bill ID.
1258    pub bill_id: Ustr,
1259    /// Last filled price.
1260    pub fill_px: String,
1261    /// Last filled quantity.
1262    pub fill_sz: String,
1263    /// Trade side: buy or sell.
1264    pub side: OKXSide,
1265    /// Execution type.
1266    pub exec_type: OKXExecType,
1267    /// Fee currency.
1268    pub fee_ccy: String,
1269    /// Fee amount.
1270    #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
1271    pub fee: Option<String>,
1272    /// Timestamp, Unix timestamp format in milliseconds.
1273    #[serde(deserialize_with = "deserialize_string_to_u64")]
1274    pub ts: u64,
1275}
1276
1277/// Represents a single historical position record from `GET /api/v5/account/positions-history`.
1278#[derive(Clone, Debug, Serialize, Deserialize)]
1279#[serde(rename_all = "camelCase")]
1280pub struct OKXPositionHistory {
1281    /// Instrument type (e.g. "SWAP", "FUTURES", etc.).
1282    pub inst_type: OKXInstrumentType,
1283    /// Instrument ID (e.g. "BTC-USD-SWAP").
1284    pub inst_id: Ustr,
1285    /// Margin mode: e.g. "cross", "isolated".
1286    pub mgn_mode: OKXMarginMode,
1287    /// The type of the last close, e.g. "1" (close partially), "2" (close all), etc.
1288    /// See OKX docs for the meaning of each numeric code.
1289    #[serde(rename = "type")]
1290    pub r#type: Ustr,
1291    /// Creation time of the position (Unix timestamp in milliseconds).
1292    pub c_time: String,
1293    /// Last update time, Unix timestamp in milliseconds.
1294    #[serde(deserialize_with = "deserialize_string_to_u64")]
1295    pub u_time: u64,
1296    /// Average price of opening position.
1297    pub open_avg_px: String,
1298    /// Average price of closing position (if applicable).
1299    #[serde(skip_serializing_if = "Option::is_none")]
1300    pub close_avg_px: Option<String>,
1301    /// The position ID.
1302    #[serde(default, deserialize_with = "deserialize_empty_ustr_as_none")]
1303    pub pos_id: Option<Ustr>,
1304    /// Max quantity of the position at open time.
1305    #[serde(skip_serializing_if = "Option::is_none")]
1306    pub open_max_pos: Option<String>,
1307    /// Cumulative closed volume of the position.
1308    #[serde(skip_serializing_if = "Option::is_none")]
1309    pub close_total_pos: Option<String>,
1310    /// Realized profit and loss (only for FUTURES/SWAP/OPTION).
1311    #[serde(skip_serializing_if = "Option::is_none")]
1312    pub realized_pnl: Option<String>,
1313    /// Accumulated fee for the position.
1314    #[serde(skip_serializing_if = "Option::is_none")]
1315    pub fee: Option<String>,
1316    /// Accumulated funding fee (for perpetual swaps).
1317    #[serde(skip_serializing_if = "Option::is_none")]
1318    pub funding_fee: Option<String>,
1319    /// Accumulated liquidation penalty. Negative if there was a penalty.
1320    #[serde(skip_serializing_if = "Option::is_none")]
1321    pub liq_penalty: Option<String>,
1322    /// Profit and loss (realized or unrealized depending on status).
1323    #[serde(skip_serializing_if = "Option::is_none")]
1324    pub pnl: Option<String>,
1325    /// PnL ratio.
1326    #[serde(skip_serializing_if = "Option::is_none")]
1327    pub pnl_ratio: Option<String>,
1328    /// Position side: "long" / "short" / "net".
1329    pub pos_side: OKXPositionSide,
1330    /// Leverage used (the JSON field is "lev", but we rename it in Rust).
1331    pub lever: String,
1332    /// Direction: "long" or "short" (only for MARGIN/FUTURES/SWAP/OPTION).
1333    #[serde(skip_serializing_if = "Option::is_none")]
1334    pub direction: Option<String>,
1335    /// Trigger mark price. Populated if `type` indicates liquidation or ADL.
1336    #[serde(skip_serializing_if = "Option::is_none")]
1337    pub trigger_px: Option<String>,
1338    /// The underlying (e.g. "BTC-USD" for futures or swap).
1339    #[serde(skip_serializing_if = "Option::is_none")]
1340    pub uly: Option<String>,
1341    /// Currency (e.g. "BTC"). May or may not appear in all responses.
1342    #[serde(skip_serializing_if = "Option::is_none")]
1343    pub ccy: Option<String>,
1344}
1345
1346/// Represents the request body for `POST /api/v5/trade/order-algo` (place algo order).
1347#[derive(Clone, Debug, Serialize, Deserialize)]
1348#[serde(rename_all = "camelCase")]
1349pub struct OKXPlaceAlgoOrderRequest {
1350    /// Instrument ID.
1351    #[serde(rename = "instId")]
1352    pub inst_id: String,
1353    /// Instrument ID code (numeric). May be required per OKX deprecation notice.
1354    #[serde(rename = "instIdCode", skip_serializing_if = "Option::is_none")]
1355    pub inst_id_code: Option<u64>,
1356    /// Trade mode (isolated, cross, cash).
1357    #[serde(rename = "tdMode")]
1358    pub td_mode: OKXTradeMode,
1359    /// Order side (buy, sell).
1360    pub side: OKXSide,
1361    /// Algo order type (trigger, conditional, move_order_stop, etc.).
1362    #[serde(rename = "ordType")]
1363    pub ord_type: OKXAlgoOrderType,
1364    /// Order size. Omitted for `closeFraction` close orders.
1365    #[serde(skip_serializing_if = "Option::is_none")]
1366    pub sz: Option<String>,
1367    /// Client-supplied algo order ID.
1368    #[serde(rename = "algoClOrdId", skip_serializing_if = "Option::is_none")]
1369    pub algo_cl_ord_id: Option<String>,
1370    /// Trigger price.
1371    #[serde(rename = "triggerPx", skip_serializing_if = "Option::is_none")]
1372    pub trigger_px: Option<String>,
1373    /// Order price (for limit orders).
1374    #[serde(rename = "orderPx", skip_serializing_if = "Option::is_none")]
1375    pub order_px: Option<String>,
1376    /// Trigger type (last, mark, index).
1377    #[serde(rename = "triggerPxType", skip_serializing_if = "Option::is_none")]
1378    pub trigger_px_type: Option<OKXTriggerType>,
1379    /// Stop-loss trigger price for conditional close orders.
1380    #[serde(rename = "slTriggerPx", skip_serializing_if = "Option::is_none")]
1381    pub sl_trigger_px: Option<String>,
1382    /// Stop-loss order price for conditional close orders.
1383    #[serde(rename = "slOrdPx", skip_serializing_if = "Option::is_none")]
1384    pub sl_ord_px: Option<String>,
1385    /// Stop-loss trigger type (last, mark, index).
1386    #[serde(rename = "slTriggerPxType", skip_serializing_if = "Option::is_none")]
1387    pub sl_trigger_px_type: Option<OKXTriggerType>,
1388    /// Take-profit trigger price for conditional close orders.
1389    #[serde(rename = "tpTriggerPx", skip_serializing_if = "Option::is_none")]
1390    pub tp_trigger_px: Option<String>,
1391    /// Take-profit order price for conditional close orders.
1392    #[serde(rename = "tpOrdPx", skip_serializing_if = "Option::is_none")]
1393    pub tp_ord_px: Option<String>,
1394    /// Take-profit trigger type (last, mark, index).
1395    #[serde(rename = "tpTriggerPxType", skip_serializing_if = "Option::is_none")]
1396    pub tp_trigger_px_type: Option<OKXTriggerType>,
1397    /// Target currency (base_ccy or quote_ccy).
1398    #[serde(rename = "tgtCcy", skip_serializing_if = "Option::is_none")]
1399    pub tgt_ccy: Option<OKXTargetCurrency>,
1400    /// Position side (net, long, short).
1401    #[serde(rename = "posSide", skip_serializing_if = "Option::is_none")]
1402    pub pos_side: Option<OKXPositionSide>,
1403    /// Whether to close position.
1404    #[serde(rename = "closePosition", skip_serializing_if = "Option::is_none")]
1405    pub close_position: Option<bool>,
1406    /// Order tag.
1407    #[serde(skip_serializing_if = "Option::is_none")]
1408    pub tag: Option<String>,
1409    /// Whether it's a reduce-only order.
1410    #[serde(rename = "reduceOnly", skip_serializing_if = "Option::is_none")]
1411    pub reduce_only: Option<bool>,
1412    /// Fraction of the position to close for eligible algo close orders.
1413    #[serde(rename = "closeFraction", skip_serializing_if = "Option::is_none")]
1414    pub close_fraction: Option<String>,
1415    /// Callback rate for trailing stop (e.g., "0.01" for 1%). Either this or
1416    /// `callback_spread` is required for `move_order_stop` orders.
1417    #[serde(rename = "callbackRatio", skip_serializing_if = "Option::is_none")]
1418    pub callback_ratio: Option<String>,
1419    /// Callback spread for trailing stop (fixed price distance). Either this or
1420    /// `callback_ratio` is required for `move_order_stop` orders.
1421    #[serde(rename = "callbackSpread", skip_serializing_if = "Option::is_none")]
1422    pub callback_spread: Option<String>,
1423    /// Activation price for trailing stop. If empty, the trailing stop
1424    /// activates immediately when placed.
1425    #[serde(rename = "activePx", skip_serializing_if = "Option::is_none")]
1426    pub active_px: Option<String>,
1427}
1428
1429/// Represents the response from `POST /api/v5/trade/order-algo` (place algo order).
1430#[derive(Clone, Debug, Serialize, Deserialize)]
1431#[serde(rename_all = "camelCase")]
1432pub struct OKXPlaceAlgoOrderResponse {
1433    /// Algo order ID.
1434    pub algo_id: String,
1435    /// Client-supplied algo order ID.
1436    #[serde(skip_serializing_if = "Option::is_none")]
1437    pub algo_cl_ord_id: Option<String>,
1438    /// The result of the request.
1439    #[serde(skip_serializing_if = "Option::is_none")]
1440    pub s_code: Option<String>,
1441    /// Error message if the request failed.
1442    #[serde(skip_serializing_if = "Option::is_none")]
1443    pub s_msg: Option<String>,
1444    /// Request ID.
1445    #[serde(skip_serializing_if = "Option::is_none")]
1446    pub req_id: Option<String>,
1447}
1448
1449/// Represents the request body for `POST /api/v5/trade/cancel-algos` (cancel algo order).
1450#[derive(Clone, Debug, Serialize, Deserialize)]
1451#[serde(rename_all = "camelCase")]
1452pub struct OKXCancelAlgoOrderRequest {
1453    /// Instrument ID.
1454    pub inst_id: String,
1455    /// Instrument ID code (numeric). May be required per OKX deprecation notice.
1456    #[serde(rename = "instIdCode", skip_serializing_if = "Option::is_none")]
1457    pub inst_id_code: Option<u64>,
1458    /// Algo order ID.
1459    #[serde(skip_serializing_if = "Option::is_none")]
1460    pub algo_id: Option<String>,
1461    /// Client-supplied algo order ID.
1462    #[serde(skip_serializing_if = "Option::is_none")]
1463    pub algo_cl_ord_id: Option<String>,
1464}
1465
1466/// Represents the response from `POST /api/v5/trade/cancel-algos` (cancel algo order).
1467#[derive(Clone, Debug, Serialize, Deserialize)]
1468#[serde(rename_all = "camelCase")]
1469pub struct OKXCancelAlgoOrderResponse {
1470    /// Algo order ID.
1471    pub algo_id: String,
1472    /// The result of the request.
1473    #[serde(skip_serializing_if = "Option::is_none")]
1474    pub s_code: Option<String>,
1475    /// Error message if the request failed.
1476    #[serde(skip_serializing_if = "Option::is_none")]
1477    pub s_msg: Option<String>,
1478}
1479
1480/// Represents the request body for `POST /api/v5/trade/amend-algos` (amend algo order).
1481#[derive(Clone, Debug, Serialize, Deserialize)]
1482#[serde(rename_all = "camelCase")]
1483pub struct OKXAmendAlgoOrderRequest {
1484    /// Instrument ID.
1485    pub inst_id: String,
1486    /// Algo order ID.
1487    pub algo_id: String,
1488    /// Client-supplied algo order ID.
1489    #[serde(skip_serializing_if = "Option::is_none")]
1490    pub algo_cl_ord_id: Option<String>,
1491    /// New order size.
1492    #[serde(skip_serializing_if = "Option::is_none")]
1493    pub new_sz: Option<String>,
1494    /// New trigger price (for `trigger` algo orders).
1495    #[serde(skip_serializing_if = "Option::is_none")]
1496    pub new_trigger_px: Option<String>,
1497    /// New take-profit trigger price (for attached/OCO TP legs).
1498    #[serde(skip_serializing_if = "Option::is_none")]
1499    pub new_tp_trigger_px: Option<String>,
1500    /// New take-profit order price (`-1` for market).
1501    #[serde(skip_serializing_if = "Option::is_none")]
1502    pub new_tp_ord_px: Option<String>,
1503    /// New take-profit trigger price type (last, mark, index).
1504    #[serde(skip_serializing_if = "Option::is_none")]
1505    pub new_tp_trigger_px_type: Option<String>,
1506    /// New stop-loss trigger price (for `conditional` SL algo orders, incl.
1507    /// `closeFraction` close-position stops, which OKX amends via `newSlTriggerPx`
1508    /// rather than `newTriggerPx`).
1509    #[serde(skip_serializing_if = "Option::is_none")]
1510    pub new_sl_trigger_px: Option<String>,
1511    /// New stop-loss order price (`-1` for market).
1512    #[serde(skip_serializing_if = "Option::is_none")]
1513    pub new_sl_ord_px: Option<String>,
1514    /// New stop-loss trigger price type (last, mark, index).
1515    #[serde(skip_serializing_if = "Option::is_none")]
1516    pub new_sl_trigger_px_type: Option<String>,
1517    /// New order price (for limit orders after trigger).
1518    #[serde(skip_serializing_if = "Option::is_none")]
1519    pub new_order_px: Option<String>,
1520    /// New callback ratio for trailing stop (e.g., "0.01" for 1%).
1521    #[serde(skip_serializing_if = "Option::is_none")]
1522    pub new_callback_ratio: Option<String>,
1523    /// New callback spread for trailing stop (fixed price distance).
1524    #[serde(skip_serializing_if = "Option::is_none")]
1525    pub new_callback_spread: Option<String>,
1526    /// New activation price for trailing stop.
1527    #[serde(skip_serializing_if = "Option::is_none")]
1528    pub new_active_px: Option<String>,
1529}
1530
1531/// Represents the response from `POST /api/v5/trade/amend-algos` (amend algo order).
1532#[derive(Clone, Debug, Serialize, Deserialize)]
1533#[serde(rename_all = "camelCase")]
1534pub struct OKXAmendAlgoOrderResponse {
1535    /// Algo order ID.
1536    pub algo_id: String,
1537    /// Client-supplied algo order ID.
1538    #[serde(skip_serializing_if = "Option::is_none")]
1539    pub algo_cl_ord_id: Option<String>,
1540    /// The result of the request.
1541    #[serde(skip_serializing_if = "Option::is_none")]
1542    pub s_code: Option<String>,
1543    /// Error message if the request failed.
1544    #[serde(skip_serializing_if = "Option::is_none")]
1545    pub s_msg: Option<String>,
1546    /// Request ID.
1547    #[serde(skip_serializing_if = "Option::is_none")]
1548    pub req_id: Option<String>,
1549}
1550
1551/// Represents the response from `GET /api/v5/public/time` (get system time).
1552#[derive(Clone, Debug, Serialize, Deserialize)]
1553#[serde(rename_all = "camelCase")]
1554pub struct OKXServerTime {
1555    /// Server timestamp in milliseconds.
1556    #[serde(deserialize_with = "deserialize_string_to_u64")]
1557    pub ts: u64,
1558}
1559
1560/// Represents a fee rate entry from `GET /api/v5/account/trade-fee`.
1561#[derive(Clone, Debug, Serialize, Deserialize)]
1562#[serde(rename_all = "camelCase")]
1563pub struct OKXFeeRate {
1564    /// Fee level (VIP tier) - indicates the user's VIP tier (0-9).
1565    #[serde(deserialize_with = "crate::common::parse::deserialize_vip_level")]
1566    pub level: OKXVipLevel,
1567    /// Taker fee rate for crypto-margined contracts.
1568    pub taker: String,
1569    /// Maker fee rate for crypto-margined contracts.
1570    pub maker: String,
1571    /// Taker fee rate for USDT-margined contracts.
1572    pub taker_u: String,
1573    /// Maker fee rate for USDT-margined contracts.
1574    pub maker_u: String,
1575    /// Maker fee rate for RPI orders.
1576    #[serde(
1577        default,
1578        alias = "elpMaker",
1579        deserialize_with = "deserialize_optional_decimal"
1580    )]
1581    pub rpi_maker: Option<Decimal>,
1582    /// Delivery fee rate.
1583    #[serde(default)]
1584    pub delivery: String,
1585    /// Option exercise fee rate.
1586    #[serde(default)]
1587    pub exercise: String,
1588    /// Event contract settlement fee rate.
1589    #[serde(default)]
1590    pub settle: String,
1591    /// Instrument type (SPOT, MARGIN, SWAP, FUTURES, OPTION).
1592    pub inst_type: OKXInstrumentType,
1593    /// Fee schedule category (being deprecated).
1594    #[serde(default)]
1595    pub category: String,
1596    /// Data return timestamp (Unix timestamp in milliseconds).
1597    #[serde(deserialize_with = "deserialize_string_to_u64")]
1598    pub ts: u64,
1599}
1600
1601#[cfg(test)]
1602mod tests {
1603    use rstest::rstest;
1604    use serde_json;
1605
1606    use super::*;
1607
1608    #[rstest]
1609    fn test_algo_order_deserializes_current_child_identifier_lists() {
1610        let order: OKXOrderAlgo = serde_json::from_value(serde_json::json!({
1611            "algoId": "123",
1612            "algoClOrdId": "algo-client-1",
1613            "ordId": "456",
1614            "ordIdList": ["456", "457"],
1615            "subAlgoIdList": ["789"],
1616            "instId": "ETH-USDT-SWAP",
1617            "instType": "SWAP",
1618            "ordType": "conditional",
1619            "state": "effective",
1620            "side": "sell",
1621            "posSide": "net",
1622            "tdMode": "cross",
1623            "cTime": "1700000000000",
1624            "uTime": "1700000001000"
1625        }))
1626        .unwrap();
1627
1628        assert_eq!(order.ord_id, "456");
1629        assert_eq!(order.ord_id_list, ["456", "457"]);
1630        assert_eq!(order.sub_algo_id_list, ["789"]);
1631    }
1632
1633    #[rstest]
1634    fn test_algo_order_request_serialization() {
1635        let request = OKXPlaceAlgoOrderRequest {
1636            inst_id: "ETH-USDT-SWAP".to_string(),
1637            inst_id_code: None,
1638            td_mode: OKXTradeMode::Isolated,
1639            side: OKXSide::Buy,
1640            ord_type: OKXAlgoOrderType::Trigger,
1641            sz: Some("0.01".to_string()),
1642            algo_cl_ord_id: Some("test123".to_string()),
1643            trigger_px: Some("3000".to_string()),
1644            order_px: Some("-1".to_string()),
1645            trigger_px_type: Some(OKXTriggerType::Last),
1646            sl_trigger_px: None,
1647            sl_ord_px: None,
1648            sl_trigger_px_type: None,
1649            tp_trigger_px: None,
1650            tp_ord_px: None,
1651            tp_trigger_px_type: None,
1652            tgt_ccy: None,
1653            pos_side: None,
1654            close_position: None,
1655            tag: None,
1656            reduce_only: None,
1657            close_fraction: None,
1658            callback_ratio: None,
1659            callback_spread: None,
1660            active_px: None,
1661        };
1662
1663        let json = serde_json::to_string(&request).unwrap();
1664
1665        // Verify that fields are serialized with correct camelCase names
1666        assert!(json.contains("\"instId\":\"ETH-USDT-SWAP\""));
1667        assert!(json.contains("\"tdMode\":\"isolated\""));
1668        assert!(json.contains("\"ordType\":\"trigger\""));
1669        assert!(json.contains("\"algoClOrdId\":\"test123\""));
1670        assert!(json.contains("\"triggerPx\":\"3000\""));
1671        assert!(json.contains("\"orderPx\":\"-1\""));
1672        assert!(json.contains("\"triggerPxType\":\"last\""));
1673
1674        // Verify that None fields are not included
1675        assert!(!json.contains("tgtCcy"));
1676        assert!(!json.contains("posSide"));
1677        assert!(!json.contains("closePosition"));
1678        assert!(!json.contains("closeFraction"));
1679    }
1680
1681    #[rstest]
1682    fn test_amend_algo_order_request_serializes_sl_trigger_px() {
1683        // A conditional stop-loss algo order (incl. closeFraction stops) is amended
1684        // via `newSlTriggerPx`, not `newTriggerPx`. Verify the camelCase field name
1685        // and that an unset `new_trigger_px` is omitted.
1686        let request = OKXAmendAlgoOrderRequest {
1687            inst_id: "ETH-USDT-SWAP".to_string(),
1688            algo_id: "123".to_string(),
1689            algo_cl_ord_id: None,
1690            new_sz: None,
1691            new_trigger_px: None,
1692            new_tp_trigger_px: None,
1693            new_tp_ord_px: None,
1694            new_tp_trigger_px_type: None,
1695            new_sl_trigger_px: Some("850".to_string()),
1696            new_sl_ord_px: None,
1697            new_sl_trigger_px_type: None,
1698            new_order_px: None,
1699            new_callback_ratio: None,
1700            new_callback_spread: None,
1701            new_active_px: None,
1702        };
1703
1704        let json = serde_json::to_string(&request).unwrap();
1705
1706        assert!(json.contains("\"algoId\":\"123\""));
1707        assert!(json.contains("\"newSlTriggerPx\":\"850\""));
1708        // Unset optional fields must be omitted (OKX rejects an empty newTriggerPx).
1709        assert!(!json.contains("newTriggerPx"));
1710        assert!(!json.contains("newSz"));
1711        assert!(!json.contains("algoClOrdId"));
1712    }
1713
1714    #[rstest]
1715    fn test_amend_algo_order_request_serializes_oco_tp_sl_fields() {
1716        let request = OKXAmendAlgoOrderRequest {
1717            inst_id: "DOGE-USDT-SWAP".to_string(),
1718            algo_id: "algo-oco-1".to_string(),
1719            algo_cl_ord_id: None,
1720            new_sz: None,
1721            new_trigger_px: None,
1722            new_tp_trigger_px: Some("0.10495".to_string()),
1723            new_tp_ord_px: Some("-1".to_string()),
1724            new_tp_trigger_px_type: Some("last".to_string()),
1725            new_sl_trigger_px: Some("0.06297".to_string()),
1726            new_sl_ord_px: Some("-1".to_string()),
1727            new_sl_trigger_px_type: Some("last".to_string()),
1728            new_order_px: None,
1729            new_callback_ratio: None,
1730            new_callback_spread: None,
1731            new_active_px: None,
1732        };
1733
1734        let json = serde_json::to_string(&request).unwrap();
1735
1736        assert!(json.contains("\"algoId\":\"algo-oco-1\""));
1737        assert!(json.contains("\"newTpTriggerPx\":\"0.10495\""));
1738        assert!(json.contains("\"newTpOrdPx\":\"-1\""));
1739        assert!(json.contains("\"newTpTriggerPxType\":\"last\""));
1740        assert!(json.contains("\"newSlTriggerPx\":\"0.06297\""));
1741        assert!(json.contains("\"newSlOrdPx\":\"-1\""));
1742        assert!(json.contains("\"newSlTriggerPxType\":\"last\""));
1743        assert!(!json.contains("newTriggerPx"));
1744    }
1745
1746    #[rstest]
1747    fn test_algo_order_request_serializes_close_fraction() {
1748        let request = OKXPlaceAlgoOrderRequest {
1749            inst_id: "ETH-USDT-SWAP".to_string(),
1750            inst_id_code: None,
1751            td_mode: OKXTradeMode::Cross,
1752            side: OKXSide::Sell,
1753            ord_type: OKXAlgoOrderType::Conditional,
1754            sz: None,
1755            algo_cl_ord_id: Some("close-frac-123".to_string()),
1756            trigger_px: None,
1757            order_px: None,
1758            trigger_px_type: None,
1759            sl_trigger_px: Some("3000".to_string()),
1760            sl_ord_px: Some("-1".to_string()),
1761            sl_trigger_px_type: Some(OKXTriggerType::Last),
1762            tp_trigger_px: None,
1763            tp_ord_px: None,
1764            tp_trigger_px_type: None,
1765            tgt_ccy: None,
1766            pos_side: Some(OKXPositionSide::Net),
1767            close_position: None,
1768            tag: None,
1769            reduce_only: Some(true),
1770            close_fraction: Some("1".to_string()),
1771            callback_ratio: None,
1772            callback_spread: None,
1773            active_px: None,
1774        };
1775
1776        let json = serde_json::to_string(&request).unwrap();
1777
1778        assert!(json.contains("\"ordType\":\"conditional\""));
1779        assert!(json.contains("\"closeFraction\":\"1\""));
1780        assert!(json.contains("\"slTriggerPx\":\"3000\""));
1781        assert!(json.contains("\"slOrdPx\":\"-1\""));
1782        assert!(json.contains("\"slTriggerPxType\":\"last\""));
1783        assert!(json.contains("\"reduceOnly\":true"));
1784        assert!(!json.contains("\"sz\""));
1785        assert!(!json.contains("triggerPx"));
1786    }
1787
1788    #[rstest]
1789    fn test_algo_order_request_array_serialization() {
1790        let request = OKXPlaceAlgoOrderRequest {
1791            inst_id: "BTC-USDT".to_string(),
1792            inst_id_code: Some(10459),
1793            td_mode: OKXTradeMode::Cross,
1794            side: OKXSide::Sell,
1795            ord_type: OKXAlgoOrderType::Trigger,
1796            sz: Some("0.1".to_string()),
1797            algo_cl_ord_id: None,
1798            trigger_px: Some("50000".to_string()),
1799            order_px: Some("49900".to_string()),
1800            trigger_px_type: Some(OKXTriggerType::Mark),
1801            sl_trigger_px: None,
1802            sl_ord_px: None,
1803            sl_trigger_px_type: None,
1804            tp_trigger_px: None,
1805            tp_ord_px: None,
1806            tp_trigger_px_type: None,
1807            tgt_ccy: Some(OKXTargetCurrency::BaseCcy),
1808            pos_side: Some(OKXPositionSide::Net),
1809            close_position: None,
1810            tag: None,
1811            reduce_only: Some(true),
1812            close_fraction: None,
1813            callback_ratio: None,
1814            callback_spread: None,
1815            active_px: None,
1816        };
1817
1818        // OKX expects an array of requests
1819        let json = serde_json::to_string(&[request]).unwrap();
1820
1821        // Verify array format
1822        assert!(json.starts_with('['));
1823        assert!(json.ends_with(']'));
1824
1825        // Verify correct field names
1826        assert!(json.contains("\"instId\":\"BTC-USDT\""));
1827        assert!(json.contains("\"tdMode\":\"cross\""));
1828        assert!(json.contains("\"triggerPx\":\"50000\""));
1829        assert!(json.contains("\"orderPx\":\"49900\""));
1830        assert!(json.contains("\"triggerPxType\":\"mark\""));
1831        assert!(json.contains("\"tgtCcy\":\"base_ccy\""));
1832        assert!(json.contains("\"posSide\":\"net\""));
1833        assert!(json.contains("\"reduceOnly\":true"));
1834    }
1835
1836    #[rstest]
1837    fn test_cancel_algo_order_request_serialization() {
1838        let request = OKXCancelAlgoOrderRequest {
1839            inst_id: "ETH-USDT-SWAP".to_string(),
1840            inst_id_code: None,
1841            algo_id: Some("123456".to_string()),
1842            algo_cl_ord_id: None,
1843        };
1844
1845        let json = serde_json::to_string(&request).unwrap();
1846
1847        // Verify correct field names
1848        assert!(json.contains("\"instId\":\"ETH-USDT-SWAP\""));
1849        assert!(json.contains("\"algoId\":\"123456\""));
1850        assert!(!json.contains("algoClOrdId"));
1851    }
1852
1853    #[rstest]
1854    fn test_cancel_algo_order_with_client_id_serialization() {
1855        let request = OKXCancelAlgoOrderRequest {
1856            inst_id: "BTC-USDT".to_string(),
1857            inst_id_code: Some(10459),
1858            algo_id: None,
1859            algo_cl_ord_id: Some("client123".to_string()),
1860        };
1861
1862        // OKX expects an array of requests
1863        let json = serde_json::to_string(&[request]).unwrap();
1864
1865        // Verify array format and field names
1866        assert!(json.starts_with('['));
1867        assert!(json.contains("\"instId\":\"BTC-USDT\""));
1868        assert!(json.contains("\"algoClOrdId\":\"client123\""));
1869        assert!(!json.contains("\"algoId\""));
1870    }
1871
1872    #[rstest]
1873    fn test_amend_algo_order_trigger_serialization() {
1874        let request = OKXAmendAlgoOrderRequest {
1875            inst_id: "ETH-USDT-SWAP".to_string(),
1876            algo_id: "123456".to_string(),
1877            algo_cl_ord_id: None,
1878            new_sz: None,
1879            new_trigger_px: Some("3500".to_string()),
1880            new_tp_trigger_px: None,
1881            new_tp_ord_px: None,
1882            new_tp_trigger_px_type: None,
1883            new_sl_trigger_px: None,
1884            new_sl_ord_px: None,
1885            new_sl_trigger_px_type: None,
1886            new_order_px: Some("3490".to_string()),
1887            new_callback_ratio: None,
1888            new_callback_spread: None,
1889            new_active_px: None,
1890        };
1891
1892        let json = serde_json::to_string(&request).unwrap();
1893
1894        assert!(json.contains("\"instId\":\"ETH-USDT-SWAP\""));
1895        assert!(json.contains("\"algoId\":\"123456\""));
1896        assert!(json.contains("\"newTriggerPx\":\"3500\""));
1897        assert!(json.contains("\"newOrderPx\":\"3490\""));
1898        assert!(!json.contains("newSz"));
1899        assert!(!json.contains("algoClOrdId"));
1900        assert!(!json.contains("newCallbackRatio"));
1901    }
1902
1903    #[rstest]
1904    fn test_amend_algo_order_trailing_stop_serialization() {
1905        let request = OKXAmendAlgoOrderRequest {
1906            inst_id: "BTC-USDT-SWAP".to_string(),
1907            algo_id: "789012".to_string(),
1908            algo_cl_ord_id: Some("client456".to_string()),
1909            new_sz: Some("0.1".to_string()),
1910            new_trigger_px: None,
1911            new_tp_trigger_px: None,
1912            new_tp_ord_px: None,
1913            new_tp_trigger_px_type: None,
1914            new_sl_trigger_px: None,
1915            new_sl_ord_px: None,
1916            new_sl_trigger_px_type: None,
1917            new_order_px: None,
1918            new_callback_ratio: Some("0.02".to_string()),
1919            new_callback_spread: None,
1920            new_active_px: Some("50000".to_string()),
1921        };
1922
1923        let json = serde_json::to_string(&request).unwrap();
1924
1925        assert!(json.contains("\"instId\":\"BTC-USDT-SWAP\""));
1926        assert!(json.contains("\"algoId\":\"789012\""));
1927        assert!(json.contains("\"algoClOrdId\":\"client456\""));
1928        assert!(json.contains("\"newSz\":\"0.1\""));
1929        assert!(json.contains("\"newCallbackRatio\":\"0.02\""));
1930        assert!(json.contains("\"newActivePx\":\"50000\""));
1931        assert!(!json.contains("newTriggerPx"));
1932        assert!(!json.contains("newOrderPx"));
1933    }
1934
1935    #[rstest]
1936    fn test_trailing_stop_request_callback_ratio_serialization() {
1937        let request = OKXPlaceAlgoOrderRequest {
1938            inst_id: "BTC-USDT-SWAP".to_string(),
1939            inst_id_code: None,
1940            td_mode: OKXTradeMode::Cross,
1941            side: OKXSide::Buy,
1942            ord_type: OKXAlgoOrderType::MoveOrderStop,
1943            sz: Some("0.1".to_string()),
1944            algo_cl_ord_id: Some("trail-001".to_string()),
1945            trigger_px: None,
1946            order_px: None,
1947            trigger_px_type: None,
1948            sl_trigger_px: None,
1949            sl_ord_px: None,
1950            sl_trigger_px_type: None,
1951            tp_trigger_px: None,
1952            tp_ord_px: None,
1953            tp_trigger_px_type: None,
1954            tgt_ccy: None,
1955            pos_side: None,
1956            close_position: None,
1957            tag: None,
1958            reduce_only: None,
1959            close_fraction: None,
1960            callback_ratio: Some("0.01".to_string()),
1961            callback_spread: None,
1962            active_px: None,
1963        };
1964
1965        let json = serde_json::to_string(&request).unwrap();
1966
1967        assert!(json.contains("\"ordType\":\"move_order_stop\""));
1968        assert!(json.contains("\"callbackRatio\":\"0.01\""));
1969        assert!(!json.contains("callbackSpread"));
1970        assert!(!json.contains("activePx"));
1971    }
1972
1973    #[rstest]
1974    fn test_trailing_stop_request_callback_spread_serialization() {
1975        let request = OKXPlaceAlgoOrderRequest {
1976            inst_id: "ETH-USDT-SWAP".to_string(),
1977            inst_id_code: None,
1978            td_mode: OKXTradeMode::Isolated,
1979            side: OKXSide::Sell,
1980            ord_type: OKXAlgoOrderType::MoveOrderStop,
1981            sz: Some("1.0".to_string()),
1982            algo_cl_ord_id: None,
1983            trigger_px: None,
1984            order_px: None,
1985            trigger_px_type: None,
1986            sl_trigger_px: None,
1987            sl_ord_px: None,
1988            sl_trigger_px_type: None,
1989            tp_trigger_px: None,
1990            tp_ord_px: None,
1991            tp_trigger_px_type: None,
1992            tgt_ccy: None,
1993            pos_side: None,
1994            close_position: None,
1995            tag: None,
1996            reduce_only: Some(true),
1997            close_fraction: None,
1998            callback_ratio: None,
1999            callback_spread: Some("50.5".to_string()),
2000            active_px: None,
2001        };
2002
2003        let json = serde_json::to_string(&request).unwrap();
2004
2005        assert!(json.contains("\"callbackSpread\":\"50.5\""));
2006        assert!(!json.contains("callbackRatio"));
2007        assert!(!json.contains("activePx"));
2008    }
2009
2010    #[rstest]
2011    fn test_trailing_stop_request_with_activation_price_serialization() {
2012        let request = OKXPlaceAlgoOrderRequest {
2013            inst_id: "BTC-USDT-SWAP".to_string(),
2014            inst_id_code: None,
2015            td_mode: OKXTradeMode::Cross,
2016            side: OKXSide::Buy,
2017            ord_type: OKXAlgoOrderType::MoveOrderStop,
2018            sz: Some("0.5".to_string()),
2019            algo_cl_ord_id: None,
2020            trigger_px: None,
2021            order_px: None,
2022            trigger_px_type: None,
2023            sl_trigger_px: None,
2024            sl_ord_px: None,
2025            sl_trigger_px_type: None,
2026            tp_trigger_px: None,
2027            tp_ord_px: None,
2028            tp_trigger_px_type: None,
2029            tgt_ccy: None,
2030            pos_side: None,
2031            close_position: None,
2032            tag: None,
2033            reduce_only: None,
2034            close_fraction: None,
2035            callback_ratio: Some("0.005".to_string()),
2036            callback_spread: None,
2037            active_px: Some("65000".to_string()),
2038        };
2039
2040        let json = serde_json::to_string(&request).unwrap();
2041
2042        assert!(json.contains("\"callbackRatio\":\"0.005\""));
2043        assert!(json.contains("\"activePx\":\"65000\""));
2044        assert!(!json.contains("callbackSpread"));
2045    }
2046
2047    #[rstest]
2048    fn test_amend_algo_order_callback_spread_serialization() {
2049        let request = OKXAmendAlgoOrderRequest {
2050            inst_id: "ETH-USDT-SWAP".to_string(),
2051            algo_id: "456789".to_string(),
2052            algo_cl_ord_id: None,
2053            new_sz: None,
2054            new_trigger_px: None,
2055            new_tp_trigger_px: None,
2056            new_tp_ord_px: None,
2057            new_tp_trigger_px_type: None,
2058            new_sl_trigger_px: None,
2059            new_sl_ord_px: None,
2060            new_sl_trigger_px_type: None,
2061            new_order_px: None,
2062            new_callback_ratio: None,
2063            new_callback_spread: Some("25.0".to_string()),
2064            new_active_px: Some("4000".to_string()),
2065        };
2066
2067        let json = serde_json::to_string(&request).unwrap();
2068
2069        assert!(json.contains("\"newCallbackSpread\":\"25.0\""));
2070        assert!(json.contains("\"newActivePx\":\"4000\""));
2071        assert!(!json.contains("newCallbackRatio"));
2072        assert!(!json.contains("newTriggerPx"));
2073        assert!(!json.contains("newSz"));
2074    }
2075
2076    #[rstest]
2077    fn test_amend_algo_order_size_only_serialization() {
2078        let request = OKXAmendAlgoOrderRequest {
2079            inst_id: "BTC-USDT-SWAP".to_string(),
2080            algo_id: "111222".to_string(),
2081            algo_cl_ord_id: None,
2082            new_sz: Some("0.5".to_string()),
2083            new_trigger_px: None,
2084            new_tp_trigger_px: None,
2085            new_tp_ord_px: None,
2086            new_tp_trigger_px_type: None,
2087            new_sl_trigger_px: None,
2088            new_sl_ord_px: None,
2089            new_sl_trigger_px_type: None,
2090            new_order_px: None,
2091            new_callback_ratio: None,
2092            new_callback_spread: None,
2093            new_active_px: None,
2094        };
2095
2096        let json = serde_json::to_string(&request).unwrap();
2097
2098        assert!(json.contains("\"newSz\":\"0.5\""));
2099        assert!(!json.contains("newTriggerPx"));
2100        assert!(!json.contains("newOrderPx"));
2101        assert!(!json.contains("newCallbackRatio"));
2102        assert!(!json.contains("newCallbackSpread"));
2103        assert!(!json.contains("newActivePx"));
2104    }
2105
2106    #[rstest]
2107    fn test_amend_algo_order_all_fields_serialization() {
2108        let request = OKXAmendAlgoOrderRequest {
2109            inst_id: "BTC-USDT-SWAP".to_string(),
2110            algo_id: "333444".to_string(),
2111            algo_cl_ord_id: Some("client789".to_string()),
2112            new_sz: Some("1.0".to_string()),
2113            new_trigger_px: Some("60000".to_string()),
2114            new_tp_trigger_px: None,
2115            new_tp_ord_px: None,
2116            new_tp_trigger_px_type: None,
2117            new_sl_trigger_px: None,
2118            new_sl_ord_px: None,
2119            new_sl_trigger_px_type: None,
2120            new_order_px: Some("59900".to_string()),
2121            new_callback_ratio: Some("0.015".to_string()),
2122            new_callback_spread: Some("100".to_string()),
2123            new_active_px: Some("62000".to_string()),
2124        };
2125
2126        let json = serde_json::to_string(&request).unwrap();
2127
2128        assert!(json.contains("\"instId\":\"BTC-USDT-SWAP\""));
2129        assert!(json.contains("\"algoId\":\"333444\""));
2130        assert!(json.contains("\"algoClOrdId\":\"client789\""));
2131        assert!(json.contains("\"newSz\":\"1.0\""));
2132        assert!(json.contains("\"newTriggerPx\":\"60000\""));
2133        assert!(json.contains("\"newOrderPx\":\"59900\""));
2134        assert!(json.contains("\"newCallbackRatio\":\"0.015\""));
2135        assert!(json.contains("\"newCallbackSpread\":\"100\""));
2136        assert!(json.contains("\"newActivePx\":\"62000\""));
2137    }
2138
2139    #[rstest]
2140    fn test_place_order_request_serializes_px_usd() {
2141        let request = OKXPlaceOrderRequest {
2142            inst_id: "BTC-USD-250328-50000-C".to_string(),
2143            td_mode: OKXTradeMode::Cross,
2144            ccy: None,
2145            cl_ord_id: Some("test-opt-1".to_string()),
2146            tag: None,
2147            side: OKXSide::Buy,
2148            pos_side: Some(OKXPositionSide::Net),
2149            ord_type: OKXOrderType::Limit,
2150            sz: "1".to_string(),
2151            px: None,
2152            px_usd: Some("100.5".to_string()),
2153            px_vol: None,
2154            reduce_only: None,
2155            tgt_ccy: None,
2156            attach_algo_ords: None,
2157            speed_bump: None,
2158            outcome: None,
2159            slippage_pct: None,
2160            rpi_taker_access: None,
2161            rpi_px_round: None,
2162        };
2163
2164        let json = serde_json::to_string(&request).unwrap();
2165        assert!(json.contains("\"pxUsd\":\"100.5\""));
2166        assert!(!json.contains("\"pxVol\""));
2167        assert!(!json.contains("\"px\":"));
2168        assert!(!json.contains("slippagePct"));
2169    }
2170
2171    #[rstest]
2172    fn test_place_order_request_serializes_px_vol() {
2173        let request = OKXPlaceOrderRequest {
2174            inst_id: "BTC-USD-250328-50000-C".to_string(),
2175            td_mode: OKXTradeMode::Cross,
2176            ccy: None,
2177            cl_ord_id: Some("test-opt-2".to_string()),
2178            tag: None,
2179            side: OKXSide::Buy,
2180            pos_side: Some(OKXPositionSide::Net),
2181            ord_type: OKXOrderType::Limit,
2182            sz: "1".to_string(),
2183            px: None,
2184            px_usd: None,
2185            px_vol: Some("0.55".to_string()),
2186            reduce_only: None,
2187            tgt_ccy: None,
2188            attach_algo_ords: None,
2189            speed_bump: None,
2190            outcome: None,
2191            slippage_pct: None,
2192            rpi_taker_access: None,
2193            rpi_px_round: None,
2194        };
2195
2196        let json = serde_json::to_string(&request).unwrap();
2197        assert!(json.contains("\"pxVol\":\"0.55\""));
2198        assert!(!json.contains("\"pxUsd\""));
2199        assert!(!json.contains("\"px\":"));
2200    }
2201
2202    #[rstest]
2203    fn test_place_order_request_serializes_slippage_pct() {
2204        let request = OKXPlaceOrderRequest {
2205            inst_id: "BTC-USDT-SWAP".to_string(),
2206            td_mode: OKXTradeMode::Cross,
2207            ccy: None,
2208            cl_ord_id: Some("mkt-slip-1".to_string()),
2209            tag: None,
2210            side: OKXSide::Buy,
2211            pos_side: Some(OKXPositionSide::Net),
2212            ord_type: OKXOrderType::Market,
2213            sz: "1".to_string(),
2214            px: None,
2215            px_usd: None,
2216            px_vol: None,
2217            reduce_only: None,
2218            tgt_ccy: None,
2219            attach_algo_ords: None,
2220            speed_bump: None,
2221            outcome: None,
2222            slippage_pct: Some("0.005".to_string()),
2223            rpi_taker_access: None,
2224            rpi_px_round: None,
2225        };
2226
2227        let json: serde_json::Value = serde_json::to_value(&request).unwrap();
2228        assert_eq!(json["slippagePct"], "0.005");
2229    }
2230
2231    #[rstest]
2232    fn test_rpi_place_order_request_serialization() {
2233        let request = OKXPlaceOrderRequest {
2234            inst_id: "OMI-USD".to_string(),
2235            td_mode: OKXTradeMode::Cash,
2236            ccy: None,
2237            cl_ord_id: Some("ORPI001".to_string()),
2238            tag: Some("rpi-test".to_string()),
2239            side: OKXSide::Sell,
2240            pos_side: None,
2241            ord_type: OKXOrderType::Rpi,
2242            sz: "250000".to_string(),
2243            px: Some("0.0001600".to_string()),
2244            px_usd: None,
2245            px_vol: None,
2246            reduce_only: Some(false),
2247            tgt_ccy: None,
2248            attach_algo_ords: None,
2249            speed_bump: None,
2250            outcome: None,
2251            slippage_pct: None,
2252            rpi_taker_access: Some(true),
2253            rpi_px_round: Some(false),
2254        };
2255
2256        let value = serde_json::to_value(request).unwrap();
2257
2258        assert_eq!(
2259            value,
2260            serde_json::json!({
2261                "instId": "OMI-USD",
2262                "tdMode": "cash",
2263                "clOrdId": "ORPI001",
2264                "tag": "rpi-test",
2265                "side": "sell",
2266                "ordType": "rpi",
2267                "sz": "250000",
2268                "px": "0.0001600",
2269                "reduceOnly": false,
2270                "rpiTakerAccess": true,
2271                "rpiPxRound": false
2272            })
2273        );
2274    }
2275
2276    #[rstest]
2277    fn test_rpi_amend_order_request_serialization() {
2278        let request = OKXAmendOrderRequest {
2279            inst_id: "OMI-USD".to_string(),
2280            ord_id: Some("2500000000000000001".to_string()),
2281            cl_ord_id: None,
2282            req_id: Some("RPI-AMEND-1".to_string()),
2283            new_sz: Some("275000".to_string()),
2284            new_px: Some("0.0001599".to_string()),
2285            rpi_taker_access: Some(false),
2286            rpi_px_round: Some(true),
2287        };
2288
2289        let value = serde_json::to_value(request).unwrap();
2290
2291        assert_eq!(
2292            value,
2293            serde_json::json!({
2294                "instId": "OMI-USD",
2295                "ordId": "2500000000000000001",
2296                "reqId": "RPI-AMEND-1",
2297                "newSz": "275000",
2298                "newPx": "0.0001599",
2299                "rpiTakerAccess": false,
2300                "rpiPxRound": true
2301            })
2302        );
2303    }
2304
2305    #[rstest]
2306    fn test_rpi_place_order_response_parsing() {
2307        let response: OKXPlaceOrderResponse = serde_json::from_value(serde_json::json!({
2308            "ordId": "2500000000000000001",
2309            "clOrdId": "ORPI001",
2310            "tag": "rpi-test",
2311            "instId": "OMI-USD",
2312            "side": "sell",
2313            "ordType": "rpi",
2314            "sz": "250000",
2315            "state": "live",
2316            "px": "0.0001600",
2317            "avgPx": "",
2318            "sCode": "0",
2319            "sMsg": ""
2320        }))
2321        .unwrap();
2322
2323        assert_eq!(response.ord_id, Some(Ustr::from("2500000000000000001")));
2324        assert_eq!(response.cl_ord_id, Some(Ustr::from("ORPI001")));
2325        assert_eq!(response.tag.as_deref(), Some("rpi-test"));
2326        assert_eq!(response.inst_id, Some(Ustr::from("OMI-USD")));
2327        assert_eq!(response.side, Some(OKXSide::Sell));
2328        assert_eq!(response.ord_type, Some(OKXOrderType::Rpi));
2329        assert_eq!(response.sz.as_deref(), Some("250000"));
2330        assert_eq!(response.state, Some(OKXOrderStatus::Live));
2331        assert_eq!(response.px.as_deref(), Some("0.0001600"));
2332        assert_eq!(response.avg_px.as_deref(), Some(""));
2333        assert_eq!(response.s_code.as_deref(), Some("0"));
2334        assert_eq!(response.s_msg.as_deref(), Some(""));
2335    }
2336
2337    #[rstest]
2338    fn test_event_contract_models_deserialize() {
2339        let series: OKXEventContractSeries = serde_json::from_value(serde_json::json!({
2340            "seriesId": "BTC-ABOVE-DAILY",
2341            "freq": "daily",
2342            "title": "BTC above daily",
2343            "category": "1",
2344            "settlement": {
2345                "method": "cash",
2346                "closeEarly": false,
2347                "srcName": "OKX BTC/USD Index",
2348                "underlying": "BTC-USD"
2349            }
2350        }))
2351        .unwrap();
2352        let event: OKXEventContractEvent = serde_json::from_value(serde_json::json!({
2353            "seriesId": "BTC-ABOVE-DAILY",
2354            "eventId": "BTC-ABOVE-DAILY-260224-1600",
2355            "fixTime": "",
2356            "expTime": "1769697132335",
2357            "state": "live"
2358        }))
2359        .unwrap();
2360        let market: OKXEventContractMarket = serde_json::from_value(serde_json::json!({
2361            "seriesId": "BTC-ABOVE-DAILY",
2362            "eventId": "BTC-ABOVE-DAILY-260224-1600",
2363            "instId": "BTC-ABOVE-DAILY-260224-1600-65000",
2364            "listTime": "1769697132335",
2365            "fixTime": "",
2366            "expTime": "1769697132335",
2367            "state": "live",
2368            "disputed": false,
2369            "outcome": "0",
2370            "floorStrike": "120000",
2371            "capStrike": "INF",
2372            "settleValue": "",
2373            "hitDir": ""
2374        }))
2375        .unwrap();
2376
2377        assert_eq!(series.series_id, "BTC-ABOVE-DAILY");
2378        assert_eq!(series.settlement.underlying, "BTC-USD");
2379        assert_eq!(event.fix_time, None);
2380        assert_eq!(event.exp_time, Some(1_769_697_132_335));
2381        assert_eq!(
2382            market.inst_id,
2383            Ustr::from("BTC-ABOVE-DAILY-260224-1600-65000")
2384        );
2385        assert_eq!(market.list_time, Some(1_769_697_132_335));
2386        assert_eq!(market.exp_time, Some(1_769_697_132_335));
2387        assert_eq!(market.outcome, "0");
2388        assert_eq!(market.cap_strike, "INF");
2389        assert_eq!(market.hit_dir, "");
2390
2391        let serialized = serde_json::to_value(&market).unwrap();
2392        assert_eq!(serialized["capStrike"], "INF");
2393        assert_eq!(serialized["hitDir"], "");
2394    }
2395
2396    #[rstest]
2397    fn test_event_contract_models_accept_missing_optional_fields() {
2398        let series: OKXEventContractSeries = serde_json::from_value(serde_json::json!({
2399            "seriesId": "BTC-ABOVE-DAILY"
2400        }))
2401        .unwrap();
2402        let event: OKXEventContractEvent = serde_json::from_value(serde_json::json!({
2403            "seriesId": "BTC-ABOVE-DAILY",
2404            "eventId": "BTC-ABOVE-DAILY-260224-1600",
2405            "fixTime": "",
2406            "expTime": "",
2407            "state": "live"
2408        }))
2409        .unwrap();
2410        let market: OKXEventContractMarket = serde_json::from_value(serde_json::json!({
2411            "seriesId": "BTC-ABOVE-DAILY",
2412            "eventId": "BTC-ABOVE-DAILY-260224-1600",
2413            "instId": "BTC-ABOVE-DAILY-260224-1600-65000",
2414            "listTime": "",
2415            "fixTime": "",
2416            "expTime": "",
2417            "state": "live",
2418            "disputed": false,
2419            "outcome": "0",
2420            "floorStrike": "120000",
2421            "settleValue": ""
2422        }))
2423        .unwrap();
2424
2425        assert_eq!(series.freq, "");
2426        assert_eq!(series.settlement.underlying, "");
2427        assert_eq!(event.exp_time, None);
2428        assert_eq!(market.list_time, None);
2429        assert_eq!(market.exp_time, None);
2430        assert_eq!(market.cap_strike, "");
2431        assert_eq!(market.hit_dir, "");
2432    }
2433
2434    #[rstest]
2435    fn test_place_order_request_serializes_event_contract_fields() {
2436        let request = OKXPlaceOrderRequest {
2437            inst_id: "BTC-ABOVE-DAILY-260224-1600-65000".to_string(),
2438            td_mode: OKXTradeMode::Cash,
2439            ccy: None,
2440            cl_ord_id: Some("event-1".to_string()),
2441            tag: None,
2442            side: OKXSide::Buy,
2443            pos_side: None,
2444            ord_type: OKXOrderType::Limit,
2445            sz: "10".to_string(),
2446            px: Some("0.42".to_string()),
2447            px_usd: None,
2448            px_vol: None,
2449            reduce_only: None,
2450            tgt_ccy: None,
2451            attach_algo_ords: None,
2452            speed_bump: Some("1".to_string()),
2453            outcome: Some("yes".to_string()),
2454            slippage_pct: None,
2455            rpi_taker_access: None,
2456            rpi_px_round: None,
2457        };
2458
2459        let json: serde_json::Value = serde_json::to_value(&request).unwrap();
2460
2461        assert_eq!(json["speedBump"], "1");
2462        assert_eq!(json["outcome"], "yes");
2463    }
2464
2465    #[rstest]
2466    fn test_attach_algo_ord_request_serializes_trailing_fields() {
2467        let request = OKXAttachAlgoOrdRequest {
2468            attach_algo_cl_ord_id: Some("trail-1".to_string()),
2469            sl_trigger_px: None,
2470            sl_ord_px: None,
2471            sl_trigger_px_type: None,
2472            tp_trigger_px: None,
2473            tp_ord_px: None,
2474            tp_trigger_px_type: None,
2475            callback_ratio: Some("0.01".to_string()),
2476            callback_spread: None,
2477            active_px: Some("64000".to_string()),
2478            new_callback_ratio: Some("0.02".to_string()),
2479            new_callback_spread: Some("25".to_string()),
2480            new_active_px: Some("65000".to_string()),
2481        };
2482
2483        let json: serde_json::Value = serde_json::to_value(&request).unwrap();
2484
2485        assert_eq!(json["callbackRatio"], "0.01");
2486        assert_eq!(json["activePx"], "64000");
2487        assert_eq!(json["newCallbackRatio"], "0.02");
2488        assert_eq!(json["newCallbackSpread"], "25");
2489        assert_eq!(json["newActivePx"], "65000");
2490        assert!(json.get("callbackSpread").is_none());
2491    }
2492
2493    #[rstest]
2494    fn test_place_order_response_deserializes_sub_code() {
2495        let response: OKXPlaceOrderResponse = serde_json::from_value(serde_json::json!({
2496            "ordId": "",
2497            "clOrdId": "event-1",
2498            "sCode": "51000",
2499            "sMsg": "Parameter error",
2500            "subCode": "51004"
2501        }))
2502        .unwrap();
2503
2504        assert_eq!(response.cl_ord_id, Some(Ustr::from("event-1")));
2505        assert_eq!(response.s_code, Some("51000".to_string()));
2506        assert_eq!(response.sub_code, Some("51004".to_string()));
2507    }
2508
2509    #[rstest]
2510    fn test_fee_rate_deserializes_settle() {
2511        let fee_rate: OKXFeeRate = serde_json::from_value(serde_json::json!({
2512            "level": "VIP1",
2513            "taker": "-0.0005",
2514            "maker": "-0.0002",
2515            "takerU": "-0.0005",
2516            "makerU": "-0.0002",
2517            "settle": "-0.001",
2518            "instType": "EVENTS",
2519            "category": "1",
2520            "ts": "1769697132335"
2521        }))
2522        .unwrap();
2523
2524        assert_eq!(fee_rate.settle, "-0.001");
2525        assert_eq!(fee_rate.inst_type, OKXInstrumentType::Events);
2526    }
2527
2528    #[rstest]
2529    fn test_rpi_maker_fee_rate_deserializes_decimal_and_legacy_alias() {
2530        let rpi: OKXFeeRate = serde_json::from_value(serde_json::json!({
2531            "level": "VIP1",
2532            "taker": "-0.0005",
2533            "maker": "-0.0002",
2534            "takerU": "-0.0005",
2535            "makerU": "-0.0002",
2536            "rpiMaker": "-0.00015",
2537            "instType": "SPOT",
2538            "category": "1",
2539            "ts": "1785406500000"
2540        }))
2541        .unwrap();
2542        let legacy: OKXFeeRate = serde_json::from_value(serde_json::json!({
2543            "level": "VIP1",
2544            "taker": "-0.0005",
2545            "maker": "-0.0002",
2546            "takerU": "-0.0005",
2547            "makerU": "-0.0002",
2548            "elpMaker": "-0.00016",
2549            "instType": "SPOT",
2550            "category": "1",
2551            "ts": "1785406500001"
2552        }))
2553        .unwrap();
2554        let not_applicable: OKXFeeRate = serde_json::from_value(serde_json::json!({
2555            "level": "VIP1",
2556            "taker": "-0.0005",
2557            "maker": "-0.0002",
2558            "takerU": "-0.0005",
2559            "makerU": "-0.0002",
2560            "rpiMaker": "",
2561            "instType": "OPTION",
2562            "category": "1",
2563            "ts": "1785406500002"
2564        }))
2565        .unwrap();
2566
2567        assert_eq!(
2568            rpi.rpi_maker,
2569            Some(Decimal::from_str_exact("-0.00015").unwrap())
2570        );
2571        assert_eq!(
2572            legacy.rpi_maker,
2573            Some(Decimal::from_str_exact("-0.00016").unwrap())
2574        );
2575        assert_eq!(not_applicable.rpi_maker, None);
2576    }
2577}