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