use nautilus_core::serialization::deserialize_optional_decimal;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use ustr::Ustr;
use crate::common::{
models::OKXRpiBookLevel,
parse::{
deserialize_empty_string_as_none, deserialize_empty_ustr_as_none,
deserialize_optional_string_to_u64, deserialize_target_currency_as_none,
},
};
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OKXTrade {
pub inst_id: Ustr,
pub px: String,
pub sz: String,
pub side: OKXSide,
pub trade_id: Ustr,
#[serde(deserialize_with = "deserialize_string_to_u64")]
pub ts: u64,
#[serde(default)]
pub source: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct OKXCandlestick(
pub String,
pub String,
pub String,
pub String,
pub String,
pub String,
pub String,
pub String,
pub String,
);
use crate::common::{
enums::{
OKXAlgoOrderStatus, OKXAlgoOrderType, OKXExecType, OKXInstrumentType, OKXMarginMode,
OKXOrderCategory, OKXOrderStatus, OKXOrderType, OKXPositionSide, OKXSide, OKXSpreadState,
OKXSpreadType, OKXTargetCurrency, OKXTradeMode, OKXTriggerType, OKXVipLevel,
},
parse::deserialize_string_to_u64,
};
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OKXMarkPrice {
pub uly: Option<Ustr>,
pub inst_id: Ustr,
pub mark_px: String,
#[serde(deserialize_with = "deserialize_string_to_u64")]
pub ts: u64,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OKXPriceLimit {
pub inst_type: OKXInstrumentType,
pub inst_id: Ustr,
pub buy_lmt: String,
pub sell_lmt: String,
#[serde(deserialize_with = "deserialize_string_to_u64")]
pub ts: u64,
#[serde(default)]
pub enabled: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OKXOptionSummary {
pub inst_type: OKXInstrumentType,
pub inst_id: Ustr,
pub uly: Ustr,
pub bid_vol: String,
pub ask_vol: String,
pub mark_vol: String,
pub fwd_px: String,
#[serde(deserialize_with = "deserialize_string_to_u64")]
pub ts: u64,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OKXSpread {
pub sprd_id: Ustr,
pub sprd_type: OKXSpreadType,
pub state: OKXSpreadState,
pub base_ccy: Ustr,
pub sz_ccy: Ustr,
pub quote_ccy: Ustr,
pub tick_sz: String,
pub min_sz: String,
pub lot_sz: String,
#[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
pub list_time: Option<u64>,
#[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
pub exp_time: Option<u64>,
#[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
pub u_time: Option<u64>,
pub legs: Vec<OKXSpreadLeg>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OKXSpreadLeg {
pub inst_id: Ustr,
pub side: OKXSide,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OKXPlaceSpreadOrderRequest {
pub sprd_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub cl_ord_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tag: Option<String>,
pub side: OKXSide,
pub ord_type: OKXOrderType,
pub sz: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub px: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OKXCancelSpreadOrderRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub ord_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cl_ord_id: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OKXCancelAllSpreadOrdersRequest {
pub sprd_id: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OKXSpreadOrder {
pub sprd_id: Ustr,
pub ord_id: Ustr,
#[serde(default)]
pub cl_ord_id: Ustr,
#[serde(default)]
pub tag: String,
pub side: OKXSide,
pub ord_type: OKXOrderType,
pub sz: String,
#[serde(default)]
pub px: String,
#[serde(default)]
pub avg_px: String,
pub state: OKXOrderStatus,
#[serde(default)]
pub acc_fill_sz: String,
#[serde(default)]
pub pending_fill_sz: String,
#[serde(default)]
pub pending_settle_sz: String,
#[serde(default)]
pub canceled_sz: String,
#[serde(default)]
pub fill_sz: String,
#[serde(default)]
pub fill_px: String,
#[serde(default)]
pub trade_id: Ustr,
#[serde(default)]
pub cancel_source: String,
#[serde(default)]
pub req_id: String,
#[serde(default)]
pub amend_result: String,
#[serde(default)]
pub code: String,
#[serde(default)]
pub msg: String,
#[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
pub c_time: Option<u64>,
#[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
pub u_time: Option<u64>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OKXSpreadTrade {
pub sprd_id: Ustr,
pub trade_id: Ustr,
pub ord_id: Ustr,
#[serde(default)]
pub cl_ord_id: Ustr,
pub fill_px: String,
pub fill_sz: String,
pub side: OKXSide,
#[serde(default)]
pub exec_type: OKXExecType,
#[serde(default)]
pub fee_ccy: String,
#[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
pub fee: Option<String>,
#[serde(deserialize_with = "deserialize_string_to_u64")]
pub ts: u64,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OKXEventContractSettlement {
#[serde(default)]
pub method: String,
#[serde(default)]
pub close_early: bool,
#[serde(default)]
pub src_name: String,
#[serde(default)]
pub underlying: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OKXEventContractSeries {
pub series_id: String,
#[serde(default)]
pub freq: String,
#[serde(default)]
pub title: String,
#[serde(default)]
pub category: String,
#[serde(default)]
pub settlement: OKXEventContractSettlement,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OKXEventContractEvent {
pub series_id: String,
pub event_id: String,
#[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
pub fix_time: Option<u64>,
#[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
pub exp_time: Option<u64>,
pub state: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OKXEventContractMarket {
pub series_id: String,
pub event_id: String,
pub inst_id: Ustr,
#[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
pub list_time: Option<u64>,
#[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
pub fix_time: Option<u64>,
#[serde(default, deserialize_with = "deserialize_optional_string_to_u64")]
pub exp_time: Option<u64>,
pub state: String,
pub disputed: bool,
pub outcome: String,
pub floor_strike: String,
pub settle_value: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OKXIndexTicker {
pub inst_id: Ustr,
pub idx_px: String,
#[serde(deserialize_with = "deserialize_string_to_u64")]
pub ts: u64,
}
pub type OKXOrderBookLevel = (String, String, String, String);
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OKXOrderBookSnapshot {
pub asks: Vec<OKXOrderBookLevel>,
pub bids: Vec<OKXOrderBookLevel>,
#[serde(deserialize_with = "deserialize_string_to_u64")]
pub ts: u64,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OKXRpiOrderBookSnapshot {
pub asks: Vec<OKXRpiBookLevel>,
pub bids: Vec<OKXRpiBookLevel>,
pub seq_id: u64,
#[serde(deserialize_with = "deserialize_string_to_u64")]
pub ts: u64,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OKXFundingRateHistory {
pub inst_type: OKXInstrumentType,
pub inst_id: Ustr,
pub funding_rate: String,
pub realized_rate: String,
#[serde(deserialize_with = "deserialize_string_to_u64")]
pub funding_time: u64,
#[serde(default)]
pub method: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OKXPositionTier {
pub uly: Ustr,
pub inst_family: String,
pub inst_id: Ustr,
pub tier: String,
pub min_sz: String,
pub max_sz: String,
pub mmr: String,
pub imr: String,
pub max_lever: String,
pub opt_mgn_factor: String,
pub quote_max_loan: String,
pub base_max_loan: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OKXAccount {
pub adj_eq: String,
pub borrow_froz: String,
pub details: Vec<OKXBalanceDetail>,
pub imr: String,
pub iso_eq: String,
pub mgn_ratio: String,
pub mmr: String,
pub notional_usd_for_borrow: String,
pub notional_usd_for_futures: String,
pub notional_usd_for_option: String,
pub notional_usd_for_swap: String,
pub notional_usd: String,
pub ord_froz: String,
pub total_eq: String,
#[serde(deserialize_with = "deserialize_string_to_u64")]
pub u_time: u64,
pub upl: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object))]
#[cfg_attr(
feature = "python",
pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.okx")
)]
pub struct OKXBalanceDetail {
pub avail_bal: String,
pub avail_eq: String,
pub borrow_froz: String,
pub cash_bal: String,
pub ccy: Ustr,
pub cross_liab: String,
pub dis_eq: String,
pub eq: String,
pub eq_usd: String,
pub smt_sync_eq: String,
pub spot_copy_trading_eq: String,
pub fixed_bal: String,
pub frozen_bal: String,
pub imr: String,
pub interest: String,
pub iso_eq: String,
pub iso_liab: String,
pub iso_upl: String,
pub liab: String,
pub max_loan: String,
pub mgn_ratio: String,
pub mmr: String,
pub notional_lever: String,
pub ord_frozen: String,
pub reward_bal: String,
#[serde(alias = "spotInUse")]
pub spot_in_use_amt: String,
#[serde(alias = "clSpotInUse")]
pub cl_spot_in_use_amt: String,
#[serde(alias = "maxSpotInUse")]
pub max_spot_in_use_amt: String,
pub spot_iso_bal: String,
pub stgy_eq: String,
pub twap: String,
#[serde(deserialize_with = "deserialize_string_to_u64")]
pub u_time: u64,
pub upl: String,
pub upl_liab: String,
pub spot_bal: String,
pub open_avg_px: String,
pub acc_avg_px: String,
pub spot_upl: String,
pub spot_upl_ratio: String,
pub total_pnl: String,
pub total_pnl_ratio: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OKXPosition {
pub inst_id: Ustr,
pub inst_type: OKXInstrumentType,
pub mgn_mode: OKXMarginMode,
#[serde(default, deserialize_with = "deserialize_empty_ustr_as_none")]
pub pos_id: Option<Ustr>,
pub pos_side: OKXPositionSide,
pub pos: String,
pub base_bal: String,
pub ccy: String,
pub fee: String,
pub lever: String,
pub last: String,
pub mark_px: String,
pub liq_px: String,
pub mmr: String,
pub interest: String,
pub trade_id: Ustr,
pub notional_usd: String,
pub avg_px: String,
pub upl: String,
pub upl_ratio: String,
#[serde(deserialize_with = "deserialize_string_to_u64")]
pub u_time: u64,
pub margin: String,
pub mgn_ratio: String,
pub adl: String,
pub c_time: String,
pub realized_pnl: String,
pub upl_last_px: String,
pub upl_ratio_last_px: String,
pub avail_pos: String,
pub be_px: String,
pub funding_fee: String,
pub idx_px: String,
pub liq_penalty: String,
pub opt_val: String,
pub pending_close_ord_liab_val: String,
pub pnl: String,
pub pos_ccy: String,
pub quote_bal: String,
pub quote_borrowed: String,
pub quote_interest: String,
#[serde(alias = "spotInUse")]
pub spot_in_use_amt: String,
pub spot_in_use_ccy: String,
pub usd_px: String,
#[serde(default)]
pub delta_bs: String,
#[serde(default)]
pub gamma_bs: String,
#[serde(default)]
pub theta_bs: String,
#[serde(default)]
pub vega_bs: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OKXPlaceOrderResponse {
#[serde(default)]
pub ord_id: Option<Ustr>,
#[serde(default)]
pub cl_ord_id: Option<Ustr>,
#[serde(default)]
pub tag: Option<String>,
#[serde(default)]
pub inst_id: Option<Ustr>,
#[serde(default)]
pub side: Option<OKXSide>,
#[serde(default)]
pub ord_type: Option<OKXOrderType>,
#[serde(default)]
pub sz: Option<String>,
pub state: Option<OKXOrderStatus>,
#[serde(default)]
pub px: Option<String>,
#[serde(default)]
pub avg_px: Option<String>,
#[serde(default)]
pub acc_fill_sz: Option<String>,
#[serde(default)]
pub fill_sz: Option<String>,
#[serde(default)]
pub fill_px: Option<String>,
#[serde(default)]
pub trade_id: Option<Ustr>,
#[serde(default)]
pub fill_time: Option<String>,
#[serde(default)]
pub fee: Option<String>,
#[serde(default)]
pub fee_ccy: Option<String>,
#[serde(default)]
pub req_id: Option<Ustr>,
#[serde(default)]
pub pos_side: Option<OKXPositionSide>,
#[serde(default)]
pub reduce_only: Option<String>,
#[serde(default, deserialize_with = "deserialize_target_currency_as_none")]
pub tgt_ccy: Option<OKXTargetCurrency>,
#[serde(default)]
pub c_time: Option<String>,
#[serde(default)]
pub u_time: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub s_code: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub s_msg: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sub_code: Option<String>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OKXAttachAlgoOrdRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub attach_algo_cl_ord_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sl_trigger_px: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sl_ord_px: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sl_trigger_px_type: Option<OKXTriggerType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tp_trigger_px: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tp_ord_px: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tp_trigger_px_type: Option<OKXTriggerType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub callback_ratio: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub callback_spread: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub active_px: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub new_callback_ratio: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub new_callback_spread: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub new_active_px: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OKXPlaceOrderRequest {
pub inst_id: String,
pub td_mode: OKXTradeMode,
#[serde(skip_serializing_if = "Option::is_none")]
pub ccy: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cl_ord_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tag: Option<String>,
pub side: OKXSide,
#[serde(skip_serializing_if = "Option::is_none")]
pub pos_side: Option<OKXPositionSide>,
pub ord_type: OKXOrderType,
pub sz: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub px: Option<String>,
#[serde(rename = "pxUsd", skip_serializing_if = "Option::is_none")]
pub px_usd: Option<String>,
#[serde(rename = "pxVol", skip_serializing_if = "Option::is_none")]
pub px_vol: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reduce_only: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tgt_ccy: Option<OKXTargetCurrency>,
#[serde(skip_serializing_if = "Option::is_none")]
pub attach_algo_ords: Option<Vec<OKXAttachAlgoOrdRequest>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub speed_bump: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub outcome: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub slippage_pct: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rpi_taker_access: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rpi_px_round: Option<bool>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OKXAmendOrderRequest {
pub inst_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub ord_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cl_ord_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub req_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub new_sz: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub new_px: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rpi_taker_access: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rpi_px_round: Option<bool>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OKXCancelOrderRequest {
pub inst_id: String,
#[serde(rename = "instIdCode", skip_serializing_if = "Option::is_none")]
pub inst_id_code: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ord_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cl_ord_id: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OKXCancelOrderResponse {
pub ord_id: String,
#[serde(default)]
pub cl_ord_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub s_code: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub s_msg: Option<String>,
#[serde(default)]
pub ts: Option<String>,
}
pub use crate::common::models::OKXAttachedAlgoOrd;
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OKXOrderHistory {
pub ord_id: Ustr,
pub cl_ord_id: Ustr,
#[serde(default)]
pub algo_id: Option<Ustr>,
#[serde(default)]
pub algo_cl_ord_id: Option<Ustr>,
#[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
pub attach_algo_cl_ord_id: Option<String>,
#[serde(default)]
pub attach_algo_ords: Vec<OKXAttachedAlgoOrd>,
#[serde(default)]
pub cl_act_id: Option<Ustr>,
pub tag: String,
pub inst_type: OKXInstrumentType,
pub uly: Option<Ustr>,
pub inst_id: Ustr,
pub ord_type: OKXOrderType,
pub sz: String,
pub px: String,
#[serde(default)]
pub px_usd: String,
#[serde(default)]
pub px_vol: String,
pub side: OKXSide,
pub pos_side: OKXPositionSide,
pub td_mode: OKXTradeMode,
pub reduce_only: String,
#[serde(default, deserialize_with = "deserialize_target_currency_as_none")]
pub tgt_ccy: Option<OKXTargetCurrency>,
pub state: OKXOrderStatus,
pub avg_px: String,
pub fee: String,
pub fee_ccy: String,
pub fill_sz: String,
pub fill_px: String,
pub trade_id: Ustr,
#[serde(deserialize_with = "deserialize_string_to_u64")]
pub fill_time: u64,
pub acc_fill_sz: String,
#[serde(default)]
pub fill_fee: Option<String>,
#[serde(default)]
pub req_id: Option<Ustr>,
#[serde(default)]
pub cancel_fill_sz: Option<String>,
#[serde(default)]
pub cancel_total_sz: Option<String>,
#[serde(default)]
pub fee_discount: Option<String>,
pub category: OKXOrderCategory,
#[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
pub outcome: Option<String>,
#[serde(deserialize_with = "deserialize_string_to_u64")]
pub u_time: u64,
#[serde(deserialize_with = "deserialize_string_to_u64")]
pub c_time: u64,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OKXOrderAlgo {
pub algo_id: String,
#[serde(default)]
pub algo_cl_ord_id: String,
#[serde(default)]
pub cl_ord_id: String,
#[serde(default)]
pub ord_id: String,
pub inst_id: Ustr,
pub inst_type: OKXInstrumentType,
pub ord_type: OKXAlgoOrderType,
pub state: OKXAlgoOrderStatus,
pub side: OKXSide,
pub pos_side: OKXPositionSide,
#[serde(default)]
pub sz: String,
#[serde(default)]
pub trigger_px: String,
#[serde(default)]
pub trigger_px_type: Option<OKXTriggerType>,
#[serde(default)]
pub sl_trigger_px: String,
#[serde(default)]
pub sl_ord_px: String,
#[serde(default)]
pub sl_trigger_px_type: Option<OKXTriggerType>,
#[serde(default)]
pub tp_trigger_px: String,
#[serde(default)]
pub tp_ord_px: String,
#[serde(default)]
pub tp_trigger_px_type: Option<OKXTriggerType>,
#[serde(default)]
pub ord_px: String,
pub td_mode: OKXTradeMode,
#[serde(default)]
pub lever: String,
#[serde(default)]
pub reduce_only: String,
#[serde(default)]
pub close_fraction: String,
#[serde(default)]
pub actual_px: String,
#[serde(default)]
pub actual_sz: String,
#[serde(default)]
pub notional_usd: String,
#[serde(deserialize_with = "deserialize_string_to_u64")]
pub c_time: u64,
#[serde(deserialize_with = "deserialize_string_to_u64")]
pub u_time: u64,
#[serde(default)]
pub trigger_time: String,
#[serde(default)]
pub tag: String,
#[serde(default)]
pub callback_ratio: String,
#[serde(default)]
pub callback_spread: String,
#[serde(default)]
pub active_px: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OKXTransactionDetail {
pub inst_type: OKXInstrumentType,
pub inst_id: Ustr,
pub trade_id: Ustr,
pub ord_id: Ustr,
pub cl_ord_id: Ustr,
pub bill_id: Ustr,
pub fill_px: String,
pub fill_sz: String,
pub side: OKXSide,
pub exec_type: OKXExecType,
pub fee_ccy: String,
#[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
pub fee: Option<String>,
#[serde(deserialize_with = "deserialize_string_to_u64")]
pub ts: u64,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OKXPositionHistory {
pub inst_type: OKXInstrumentType,
pub inst_id: Ustr,
pub mgn_mode: OKXMarginMode,
#[serde(rename = "type")]
pub r#type: Ustr,
pub c_time: String,
#[serde(deserialize_with = "deserialize_string_to_u64")]
pub u_time: u64,
pub open_avg_px: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub close_avg_px: Option<String>,
#[serde(default, deserialize_with = "deserialize_empty_ustr_as_none")]
pub pos_id: Option<Ustr>,
#[serde(skip_serializing_if = "Option::is_none")]
pub open_max_pos: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub close_total_pos: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub realized_pnl: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fee: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub funding_fee: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub liq_penalty: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pnl: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pnl_ratio: Option<String>,
pub pos_side: OKXPositionSide,
pub lever: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub direction: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub trigger_px: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub uly: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ccy: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OKXPlaceAlgoOrderRequest {
#[serde(rename = "instId")]
pub inst_id: String,
#[serde(rename = "instIdCode", skip_serializing_if = "Option::is_none")]
pub inst_id_code: Option<u64>,
#[serde(rename = "tdMode")]
pub td_mode: OKXTradeMode,
pub side: OKXSide,
#[serde(rename = "ordType")]
pub ord_type: OKXAlgoOrderType,
#[serde(skip_serializing_if = "Option::is_none")]
pub sz: Option<String>,
#[serde(rename = "algoClOrdId", skip_serializing_if = "Option::is_none")]
pub algo_cl_ord_id: Option<String>,
#[serde(rename = "triggerPx", skip_serializing_if = "Option::is_none")]
pub trigger_px: Option<String>,
#[serde(rename = "orderPx", skip_serializing_if = "Option::is_none")]
pub order_px: Option<String>,
#[serde(rename = "triggerPxType", skip_serializing_if = "Option::is_none")]
pub trigger_px_type: Option<OKXTriggerType>,
#[serde(rename = "slTriggerPx", skip_serializing_if = "Option::is_none")]
pub sl_trigger_px: Option<String>,
#[serde(rename = "slOrdPx", skip_serializing_if = "Option::is_none")]
pub sl_ord_px: Option<String>,
#[serde(rename = "slTriggerPxType", skip_serializing_if = "Option::is_none")]
pub sl_trigger_px_type: Option<OKXTriggerType>,
#[serde(rename = "tpTriggerPx", skip_serializing_if = "Option::is_none")]
pub tp_trigger_px: Option<String>,
#[serde(rename = "tpOrdPx", skip_serializing_if = "Option::is_none")]
pub tp_ord_px: Option<String>,
#[serde(rename = "tpTriggerPxType", skip_serializing_if = "Option::is_none")]
pub tp_trigger_px_type: Option<OKXTriggerType>,
#[serde(rename = "tgtCcy", skip_serializing_if = "Option::is_none")]
pub tgt_ccy: Option<OKXTargetCurrency>,
#[serde(rename = "posSide", skip_serializing_if = "Option::is_none")]
pub pos_side: Option<OKXPositionSide>,
#[serde(rename = "closePosition", skip_serializing_if = "Option::is_none")]
pub close_position: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tag: Option<String>,
#[serde(rename = "reduceOnly", skip_serializing_if = "Option::is_none")]
pub reduce_only: Option<bool>,
#[serde(rename = "closeFraction", skip_serializing_if = "Option::is_none")]
pub close_fraction: Option<String>,
#[serde(rename = "callbackRatio", skip_serializing_if = "Option::is_none")]
pub callback_ratio: Option<String>,
#[serde(rename = "callbackSpread", skip_serializing_if = "Option::is_none")]
pub callback_spread: Option<String>,
#[serde(rename = "activePx", skip_serializing_if = "Option::is_none")]
pub active_px: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OKXPlaceAlgoOrderResponse {
pub algo_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub algo_cl_ord_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub s_code: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub s_msg: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub req_id: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OKXCancelAlgoOrderRequest {
pub inst_id: String,
#[serde(rename = "instIdCode", skip_serializing_if = "Option::is_none")]
pub inst_id_code: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub algo_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub algo_cl_ord_id: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OKXCancelAlgoOrderResponse {
pub algo_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub s_code: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub s_msg: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OKXAmendAlgoOrderRequest {
pub inst_id: String,
pub algo_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub algo_cl_ord_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub new_sz: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub new_trigger_px: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub new_tp_trigger_px: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub new_tp_ord_px: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub new_tp_trigger_px_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub new_sl_trigger_px: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub new_sl_ord_px: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub new_sl_trigger_px_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub new_order_px: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub new_callback_ratio: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub new_callback_spread: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub new_active_px: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OKXAmendAlgoOrderResponse {
pub algo_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub algo_cl_ord_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub s_code: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub s_msg: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub req_id: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OKXServerTime {
#[serde(deserialize_with = "deserialize_string_to_u64")]
pub ts: u64,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OKXFeeRate {
#[serde(deserialize_with = "crate::common::parse::deserialize_vip_level")]
pub level: OKXVipLevel,
pub taker: String,
pub maker: String,
pub taker_u: String,
pub maker_u: String,
#[serde(
default,
alias = "elpMaker",
deserialize_with = "deserialize_optional_decimal"
)]
pub rpi_maker: Option<Decimal>,
#[serde(default)]
pub delivery: String,
#[serde(default)]
pub exercise: String,
#[serde(default)]
pub settle: String,
pub inst_type: OKXInstrumentType,
#[serde(default)]
pub category: String,
#[serde(deserialize_with = "deserialize_string_to_u64")]
pub ts: u64,
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use serde_json;
use super::*;
#[rstest]
fn test_algo_order_request_serialization() {
let request = OKXPlaceAlgoOrderRequest {
inst_id: "ETH-USDT-SWAP".to_string(),
inst_id_code: None,
td_mode: OKXTradeMode::Isolated,
side: OKXSide::Buy,
ord_type: OKXAlgoOrderType::Trigger,
sz: Some("0.01".to_string()),
algo_cl_ord_id: Some("test123".to_string()),
trigger_px: Some("3000".to_string()),
order_px: Some("-1".to_string()),
trigger_px_type: Some(OKXTriggerType::Last),
sl_trigger_px: None,
sl_ord_px: None,
sl_trigger_px_type: None,
tp_trigger_px: None,
tp_ord_px: None,
tp_trigger_px_type: None,
tgt_ccy: None,
pos_side: None,
close_position: None,
tag: None,
reduce_only: None,
close_fraction: None,
callback_ratio: None,
callback_spread: None,
active_px: None,
};
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("\"instId\":\"ETH-USDT-SWAP\""));
assert!(json.contains("\"tdMode\":\"isolated\""));
assert!(json.contains("\"ordType\":\"trigger\""));
assert!(json.contains("\"algoClOrdId\":\"test123\""));
assert!(json.contains("\"triggerPx\":\"3000\""));
assert!(json.contains("\"orderPx\":\"-1\""));
assert!(json.contains("\"triggerPxType\":\"last\""));
assert!(!json.contains("tgtCcy"));
assert!(!json.contains("posSide"));
assert!(!json.contains("closePosition"));
assert!(!json.contains("closeFraction"));
}
#[rstest]
fn test_amend_algo_order_request_serializes_sl_trigger_px() {
let request = OKXAmendAlgoOrderRequest {
inst_id: "ETH-USDT-SWAP".to_string(),
algo_id: "123".to_string(),
algo_cl_ord_id: None,
new_sz: None,
new_trigger_px: None,
new_tp_trigger_px: None,
new_tp_ord_px: None,
new_tp_trigger_px_type: None,
new_sl_trigger_px: Some("850".to_string()),
new_sl_ord_px: None,
new_sl_trigger_px_type: None,
new_order_px: None,
new_callback_ratio: None,
new_callback_spread: None,
new_active_px: None,
};
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("\"algoId\":\"123\""));
assert!(json.contains("\"newSlTriggerPx\":\"850\""));
assert!(!json.contains("newTriggerPx"));
assert!(!json.contains("newSz"));
assert!(!json.contains("algoClOrdId"));
}
#[rstest]
fn test_amend_algo_order_request_serializes_oco_tp_sl_fields() {
let request = OKXAmendAlgoOrderRequest {
inst_id: "DOGE-USDT-SWAP".to_string(),
algo_id: "algo-oco-1".to_string(),
algo_cl_ord_id: None,
new_sz: None,
new_trigger_px: None,
new_tp_trigger_px: Some("0.10495".to_string()),
new_tp_ord_px: Some("-1".to_string()),
new_tp_trigger_px_type: Some("last".to_string()),
new_sl_trigger_px: Some("0.06297".to_string()),
new_sl_ord_px: Some("-1".to_string()),
new_sl_trigger_px_type: Some("last".to_string()),
new_order_px: None,
new_callback_ratio: None,
new_callback_spread: None,
new_active_px: None,
};
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("\"algoId\":\"algo-oco-1\""));
assert!(json.contains("\"newTpTriggerPx\":\"0.10495\""));
assert!(json.contains("\"newTpOrdPx\":\"-1\""));
assert!(json.contains("\"newTpTriggerPxType\":\"last\""));
assert!(json.contains("\"newSlTriggerPx\":\"0.06297\""));
assert!(json.contains("\"newSlOrdPx\":\"-1\""));
assert!(json.contains("\"newSlTriggerPxType\":\"last\""));
assert!(!json.contains("newTriggerPx"));
}
#[rstest]
fn test_algo_order_request_serializes_close_fraction() {
let request = OKXPlaceAlgoOrderRequest {
inst_id: "ETH-USDT-SWAP".to_string(),
inst_id_code: None,
td_mode: OKXTradeMode::Cross,
side: OKXSide::Sell,
ord_type: OKXAlgoOrderType::Conditional,
sz: None,
algo_cl_ord_id: Some("close-frac-123".to_string()),
trigger_px: None,
order_px: None,
trigger_px_type: None,
sl_trigger_px: Some("3000".to_string()),
sl_ord_px: Some("-1".to_string()),
sl_trigger_px_type: Some(OKXTriggerType::Last),
tp_trigger_px: None,
tp_ord_px: None,
tp_trigger_px_type: None,
tgt_ccy: None,
pos_side: Some(OKXPositionSide::Net),
close_position: None,
tag: None,
reduce_only: Some(true),
close_fraction: Some("1".to_string()),
callback_ratio: None,
callback_spread: None,
active_px: None,
};
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("\"ordType\":\"conditional\""));
assert!(json.contains("\"closeFraction\":\"1\""));
assert!(json.contains("\"slTriggerPx\":\"3000\""));
assert!(json.contains("\"slOrdPx\":\"-1\""));
assert!(json.contains("\"slTriggerPxType\":\"last\""));
assert!(json.contains("\"reduceOnly\":true"));
assert!(!json.contains("\"sz\""));
assert!(!json.contains("triggerPx"));
}
#[rstest]
fn test_algo_order_request_array_serialization() {
let request = OKXPlaceAlgoOrderRequest {
inst_id: "BTC-USDT".to_string(),
inst_id_code: Some(10459),
td_mode: OKXTradeMode::Cross,
side: OKXSide::Sell,
ord_type: OKXAlgoOrderType::Trigger,
sz: Some("0.1".to_string()),
algo_cl_ord_id: None,
trigger_px: Some("50000".to_string()),
order_px: Some("49900".to_string()),
trigger_px_type: Some(OKXTriggerType::Mark),
sl_trigger_px: None,
sl_ord_px: None,
sl_trigger_px_type: None,
tp_trigger_px: None,
tp_ord_px: None,
tp_trigger_px_type: None,
tgt_ccy: Some(OKXTargetCurrency::BaseCcy),
pos_side: Some(OKXPositionSide::Net),
close_position: None,
tag: None,
reduce_only: Some(true),
close_fraction: None,
callback_ratio: None,
callback_spread: None,
active_px: None,
};
let json = serde_json::to_string(&[request]).unwrap();
assert!(json.starts_with('['));
assert!(json.ends_with(']'));
assert!(json.contains("\"instId\":\"BTC-USDT\""));
assert!(json.contains("\"tdMode\":\"cross\""));
assert!(json.contains("\"triggerPx\":\"50000\""));
assert!(json.contains("\"orderPx\":\"49900\""));
assert!(json.contains("\"triggerPxType\":\"mark\""));
assert!(json.contains("\"tgtCcy\":\"base_ccy\""));
assert!(json.contains("\"posSide\":\"net\""));
assert!(json.contains("\"reduceOnly\":true"));
}
#[rstest]
fn test_cancel_algo_order_request_serialization() {
let request = OKXCancelAlgoOrderRequest {
inst_id: "ETH-USDT-SWAP".to_string(),
inst_id_code: None,
algo_id: Some("123456".to_string()),
algo_cl_ord_id: None,
};
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("\"instId\":\"ETH-USDT-SWAP\""));
assert!(json.contains("\"algoId\":\"123456\""));
assert!(!json.contains("algoClOrdId"));
}
#[rstest]
fn test_cancel_algo_order_with_client_id_serialization() {
let request = OKXCancelAlgoOrderRequest {
inst_id: "BTC-USDT".to_string(),
inst_id_code: Some(10459),
algo_id: None,
algo_cl_ord_id: Some("client123".to_string()),
};
let json = serde_json::to_string(&[request]).unwrap();
assert!(json.starts_with('['));
assert!(json.contains("\"instId\":\"BTC-USDT\""));
assert!(json.contains("\"algoClOrdId\":\"client123\""));
assert!(!json.contains("\"algoId\""));
}
#[rstest]
fn test_amend_algo_order_trigger_serialization() {
let request = OKXAmendAlgoOrderRequest {
inst_id: "ETH-USDT-SWAP".to_string(),
algo_id: "123456".to_string(),
algo_cl_ord_id: None,
new_sz: None,
new_trigger_px: Some("3500".to_string()),
new_tp_trigger_px: None,
new_tp_ord_px: None,
new_tp_trigger_px_type: None,
new_sl_trigger_px: None,
new_sl_ord_px: None,
new_sl_trigger_px_type: None,
new_order_px: Some("3490".to_string()),
new_callback_ratio: None,
new_callback_spread: None,
new_active_px: None,
};
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("\"instId\":\"ETH-USDT-SWAP\""));
assert!(json.contains("\"algoId\":\"123456\""));
assert!(json.contains("\"newTriggerPx\":\"3500\""));
assert!(json.contains("\"newOrderPx\":\"3490\""));
assert!(!json.contains("newSz"));
assert!(!json.contains("algoClOrdId"));
assert!(!json.contains("newCallbackRatio"));
}
#[rstest]
fn test_amend_algo_order_trailing_stop_serialization() {
let request = OKXAmendAlgoOrderRequest {
inst_id: "BTC-USDT-SWAP".to_string(),
algo_id: "789012".to_string(),
algo_cl_ord_id: Some("client456".to_string()),
new_sz: Some("0.1".to_string()),
new_trigger_px: None,
new_tp_trigger_px: None,
new_tp_ord_px: None,
new_tp_trigger_px_type: None,
new_sl_trigger_px: None,
new_sl_ord_px: None,
new_sl_trigger_px_type: None,
new_order_px: None,
new_callback_ratio: Some("0.02".to_string()),
new_callback_spread: None,
new_active_px: Some("50000".to_string()),
};
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("\"instId\":\"BTC-USDT-SWAP\""));
assert!(json.contains("\"algoId\":\"789012\""));
assert!(json.contains("\"algoClOrdId\":\"client456\""));
assert!(json.contains("\"newSz\":\"0.1\""));
assert!(json.contains("\"newCallbackRatio\":\"0.02\""));
assert!(json.contains("\"newActivePx\":\"50000\""));
assert!(!json.contains("newTriggerPx"));
assert!(!json.contains("newOrderPx"));
}
#[rstest]
fn test_trailing_stop_request_callback_ratio_serialization() {
let request = OKXPlaceAlgoOrderRequest {
inst_id: "BTC-USDT-SWAP".to_string(),
inst_id_code: None,
td_mode: OKXTradeMode::Cross,
side: OKXSide::Buy,
ord_type: OKXAlgoOrderType::MoveOrderStop,
sz: Some("0.1".to_string()),
algo_cl_ord_id: Some("trail-001".to_string()),
trigger_px: None,
order_px: None,
trigger_px_type: None,
sl_trigger_px: None,
sl_ord_px: None,
sl_trigger_px_type: None,
tp_trigger_px: None,
tp_ord_px: None,
tp_trigger_px_type: None,
tgt_ccy: None,
pos_side: None,
close_position: None,
tag: None,
reduce_only: None,
close_fraction: None,
callback_ratio: Some("0.01".to_string()),
callback_spread: None,
active_px: None,
};
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("\"ordType\":\"move_order_stop\""));
assert!(json.contains("\"callbackRatio\":\"0.01\""));
assert!(!json.contains("callbackSpread"));
assert!(!json.contains("activePx"));
}
#[rstest]
fn test_trailing_stop_request_callback_spread_serialization() {
let request = OKXPlaceAlgoOrderRequest {
inst_id: "ETH-USDT-SWAP".to_string(),
inst_id_code: None,
td_mode: OKXTradeMode::Isolated,
side: OKXSide::Sell,
ord_type: OKXAlgoOrderType::MoveOrderStop,
sz: Some("1.0".to_string()),
algo_cl_ord_id: None,
trigger_px: None,
order_px: None,
trigger_px_type: None,
sl_trigger_px: None,
sl_ord_px: None,
sl_trigger_px_type: None,
tp_trigger_px: None,
tp_ord_px: None,
tp_trigger_px_type: None,
tgt_ccy: None,
pos_side: None,
close_position: None,
tag: None,
reduce_only: Some(true),
close_fraction: None,
callback_ratio: None,
callback_spread: Some("50.5".to_string()),
active_px: None,
};
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("\"callbackSpread\":\"50.5\""));
assert!(!json.contains("callbackRatio"));
assert!(!json.contains("activePx"));
}
#[rstest]
fn test_trailing_stop_request_with_activation_price_serialization() {
let request = OKXPlaceAlgoOrderRequest {
inst_id: "BTC-USDT-SWAP".to_string(),
inst_id_code: None,
td_mode: OKXTradeMode::Cross,
side: OKXSide::Buy,
ord_type: OKXAlgoOrderType::MoveOrderStop,
sz: Some("0.5".to_string()),
algo_cl_ord_id: None,
trigger_px: None,
order_px: None,
trigger_px_type: None,
sl_trigger_px: None,
sl_ord_px: None,
sl_trigger_px_type: None,
tp_trigger_px: None,
tp_ord_px: None,
tp_trigger_px_type: None,
tgt_ccy: None,
pos_side: None,
close_position: None,
tag: None,
reduce_only: None,
close_fraction: None,
callback_ratio: Some("0.005".to_string()),
callback_spread: None,
active_px: Some("65000".to_string()),
};
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("\"callbackRatio\":\"0.005\""));
assert!(json.contains("\"activePx\":\"65000\""));
assert!(!json.contains("callbackSpread"));
}
#[rstest]
fn test_amend_algo_order_callback_spread_serialization() {
let request = OKXAmendAlgoOrderRequest {
inst_id: "ETH-USDT-SWAP".to_string(),
algo_id: "456789".to_string(),
algo_cl_ord_id: None,
new_sz: None,
new_trigger_px: None,
new_tp_trigger_px: None,
new_tp_ord_px: None,
new_tp_trigger_px_type: None,
new_sl_trigger_px: None,
new_sl_ord_px: None,
new_sl_trigger_px_type: None,
new_order_px: None,
new_callback_ratio: None,
new_callback_spread: Some("25.0".to_string()),
new_active_px: Some("4000".to_string()),
};
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("\"newCallbackSpread\":\"25.0\""));
assert!(json.contains("\"newActivePx\":\"4000\""));
assert!(!json.contains("newCallbackRatio"));
assert!(!json.contains("newTriggerPx"));
assert!(!json.contains("newSz"));
}
#[rstest]
fn test_amend_algo_order_size_only_serialization() {
let request = OKXAmendAlgoOrderRequest {
inst_id: "BTC-USDT-SWAP".to_string(),
algo_id: "111222".to_string(),
algo_cl_ord_id: None,
new_sz: Some("0.5".to_string()),
new_trigger_px: None,
new_tp_trigger_px: None,
new_tp_ord_px: None,
new_tp_trigger_px_type: None,
new_sl_trigger_px: None,
new_sl_ord_px: None,
new_sl_trigger_px_type: None,
new_order_px: None,
new_callback_ratio: None,
new_callback_spread: None,
new_active_px: None,
};
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("\"newSz\":\"0.5\""));
assert!(!json.contains("newTriggerPx"));
assert!(!json.contains("newOrderPx"));
assert!(!json.contains("newCallbackRatio"));
assert!(!json.contains("newCallbackSpread"));
assert!(!json.contains("newActivePx"));
}
#[rstest]
fn test_amend_algo_order_all_fields_serialization() {
let request = OKXAmendAlgoOrderRequest {
inst_id: "BTC-USDT-SWAP".to_string(),
algo_id: "333444".to_string(),
algo_cl_ord_id: Some("client789".to_string()),
new_sz: Some("1.0".to_string()),
new_trigger_px: Some("60000".to_string()),
new_tp_trigger_px: None,
new_tp_ord_px: None,
new_tp_trigger_px_type: None,
new_sl_trigger_px: None,
new_sl_ord_px: None,
new_sl_trigger_px_type: None,
new_order_px: Some("59900".to_string()),
new_callback_ratio: Some("0.015".to_string()),
new_callback_spread: Some("100".to_string()),
new_active_px: Some("62000".to_string()),
};
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("\"instId\":\"BTC-USDT-SWAP\""));
assert!(json.contains("\"algoId\":\"333444\""));
assert!(json.contains("\"algoClOrdId\":\"client789\""));
assert!(json.contains("\"newSz\":\"1.0\""));
assert!(json.contains("\"newTriggerPx\":\"60000\""));
assert!(json.contains("\"newOrderPx\":\"59900\""));
assert!(json.contains("\"newCallbackRatio\":\"0.015\""));
assert!(json.contains("\"newCallbackSpread\":\"100\""));
assert!(json.contains("\"newActivePx\":\"62000\""));
}
#[rstest]
fn test_place_order_request_serializes_px_usd() {
let request = OKXPlaceOrderRequest {
inst_id: "BTC-USD-250328-50000-C".to_string(),
td_mode: OKXTradeMode::Cross,
ccy: None,
cl_ord_id: Some("test-opt-1".to_string()),
tag: None,
side: OKXSide::Buy,
pos_side: Some(OKXPositionSide::Net),
ord_type: OKXOrderType::Limit,
sz: "1".to_string(),
px: None,
px_usd: Some("100.5".to_string()),
px_vol: None,
reduce_only: None,
tgt_ccy: None,
attach_algo_ords: None,
speed_bump: None,
outcome: None,
slippage_pct: None,
rpi_taker_access: None,
rpi_px_round: None,
};
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("\"pxUsd\":\"100.5\""));
assert!(!json.contains("\"pxVol\""));
assert!(!json.contains("\"px\":"));
assert!(!json.contains("slippagePct"));
}
#[rstest]
fn test_place_order_request_serializes_px_vol() {
let request = OKXPlaceOrderRequest {
inst_id: "BTC-USD-250328-50000-C".to_string(),
td_mode: OKXTradeMode::Cross,
ccy: None,
cl_ord_id: Some("test-opt-2".to_string()),
tag: None,
side: OKXSide::Buy,
pos_side: Some(OKXPositionSide::Net),
ord_type: OKXOrderType::Limit,
sz: "1".to_string(),
px: None,
px_usd: None,
px_vol: Some("0.55".to_string()),
reduce_only: None,
tgt_ccy: None,
attach_algo_ords: None,
speed_bump: None,
outcome: None,
slippage_pct: None,
rpi_taker_access: None,
rpi_px_round: None,
};
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("\"pxVol\":\"0.55\""));
assert!(!json.contains("\"pxUsd\""));
assert!(!json.contains("\"px\":"));
}
#[rstest]
fn test_place_order_request_serializes_slippage_pct() {
let request = OKXPlaceOrderRequest {
inst_id: "BTC-USDT-SWAP".to_string(),
td_mode: OKXTradeMode::Cross,
ccy: None,
cl_ord_id: Some("mkt-slip-1".to_string()),
tag: None,
side: OKXSide::Buy,
pos_side: Some(OKXPositionSide::Net),
ord_type: OKXOrderType::Market,
sz: "1".to_string(),
px: None,
px_usd: None,
px_vol: None,
reduce_only: None,
tgt_ccy: None,
attach_algo_ords: None,
speed_bump: None,
outcome: None,
slippage_pct: Some("0.005".to_string()),
rpi_taker_access: None,
rpi_px_round: None,
};
let json: serde_json::Value = serde_json::to_value(&request).unwrap();
assert_eq!(json["slippagePct"], "0.005");
}
#[rstest]
fn test_rpi_place_order_request_serialization() {
let request = OKXPlaceOrderRequest {
inst_id: "OMI-USD".to_string(),
td_mode: OKXTradeMode::Cash,
ccy: None,
cl_ord_id: Some("ORPI001".to_string()),
tag: Some("rpi-test".to_string()),
side: OKXSide::Sell,
pos_side: None,
ord_type: OKXOrderType::Rpi,
sz: "250000".to_string(),
px: Some("0.0001600".to_string()),
px_usd: None,
px_vol: None,
reduce_only: Some(false),
tgt_ccy: None,
attach_algo_ords: None,
speed_bump: None,
outcome: None,
slippage_pct: None,
rpi_taker_access: Some(true),
rpi_px_round: Some(false),
};
let value = serde_json::to_value(request).unwrap();
assert_eq!(
value,
serde_json::json!({
"instId": "OMI-USD",
"tdMode": "cash",
"clOrdId": "ORPI001",
"tag": "rpi-test",
"side": "sell",
"ordType": "rpi",
"sz": "250000",
"px": "0.0001600",
"reduceOnly": false,
"rpiTakerAccess": true,
"rpiPxRound": false
})
);
}
#[rstest]
fn test_rpi_amend_order_request_serialization() {
let request = OKXAmendOrderRequest {
inst_id: "OMI-USD".to_string(),
ord_id: Some("2500000000000000001".to_string()),
cl_ord_id: None,
req_id: Some("RPI-AMEND-1".to_string()),
new_sz: Some("275000".to_string()),
new_px: Some("0.0001599".to_string()),
rpi_taker_access: Some(false),
rpi_px_round: Some(true),
};
let value = serde_json::to_value(request).unwrap();
assert_eq!(
value,
serde_json::json!({
"instId": "OMI-USD",
"ordId": "2500000000000000001",
"reqId": "RPI-AMEND-1",
"newSz": "275000",
"newPx": "0.0001599",
"rpiTakerAccess": false,
"rpiPxRound": true
})
);
}
#[rstest]
fn test_rpi_place_order_response_parsing() {
let response: OKXPlaceOrderResponse = serde_json::from_value(serde_json::json!({
"ordId": "2500000000000000001",
"clOrdId": "ORPI001",
"tag": "rpi-test",
"instId": "OMI-USD",
"side": "sell",
"ordType": "rpi",
"sz": "250000",
"state": "live",
"px": "0.0001600",
"avgPx": "",
"sCode": "0",
"sMsg": ""
}))
.unwrap();
assert_eq!(response.ord_id, Some(Ustr::from("2500000000000000001")));
assert_eq!(response.cl_ord_id, Some(Ustr::from("ORPI001")));
assert_eq!(response.tag.as_deref(), Some("rpi-test"));
assert_eq!(response.inst_id, Some(Ustr::from("OMI-USD")));
assert_eq!(response.side, Some(OKXSide::Sell));
assert_eq!(response.ord_type, Some(OKXOrderType::Rpi));
assert_eq!(response.sz.as_deref(), Some("250000"));
assert_eq!(response.state, Some(OKXOrderStatus::Live));
assert_eq!(response.px.as_deref(), Some("0.0001600"));
assert_eq!(response.avg_px.as_deref(), Some(""));
assert_eq!(response.s_code.as_deref(), Some("0"));
assert_eq!(response.s_msg.as_deref(), Some(""));
}
#[rstest]
fn test_event_contract_models_deserialize() {
let series: OKXEventContractSeries = serde_json::from_value(serde_json::json!({
"seriesId": "BTC-ABOVE-DAILY",
"freq": "daily",
"title": "BTC above daily",
"category": "1",
"settlement": {
"method": "cash",
"closeEarly": false,
"srcName": "OKX BTC/USD Index",
"underlying": "BTC-USD"
}
}))
.unwrap();
let event: OKXEventContractEvent = serde_json::from_value(serde_json::json!({
"seriesId": "BTC-ABOVE-DAILY",
"eventId": "BTC-ABOVE-DAILY-260224-1600",
"fixTime": "",
"expTime": "1769697132335",
"state": "live"
}))
.unwrap();
let market: OKXEventContractMarket = serde_json::from_value(serde_json::json!({
"seriesId": "BTC-ABOVE-DAILY",
"eventId": "BTC-ABOVE-DAILY-260224-1600",
"instId": "BTC-ABOVE-DAILY-260224-1600-65000",
"listTime": "1769697132335",
"fixTime": "",
"expTime": "1769697132335",
"state": "live",
"disputed": false,
"outcome": "0",
"floorStrike": "120000",
"settleValue": ""
}))
.unwrap();
assert_eq!(series.series_id, "BTC-ABOVE-DAILY");
assert_eq!(series.settlement.underlying, "BTC-USD");
assert_eq!(event.fix_time, None);
assert_eq!(event.exp_time, Some(1_769_697_132_335));
assert_eq!(
market.inst_id,
Ustr::from("BTC-ABOVE-DAILY-260224-1600-65000")
);
assert_eq!(market.list_time, Some(1_769_697_132_335));
assert_eq!(market.exp_time, Some(1_769_697_132_335));
assert_eq!(market.outcome, "0");
}
#[rstest]
fn test_event_contract_models_accept_missing_optional_fields() {
let series: OKXEventContractSeries = serde_json::from_value(serde_json::json!({
"seriesId": "BTC-ABOVE-DAILY"
}))
.unwrap();
let event: OKXEventContractEvent = serde_json::from_value(serde_json::json!({
"seriesId": "BTC-ABOVE-DAILY",
"eventId": "BTC-ABOVE-DAILY-260224-1600",
"fixTime": "",
"expTime": "",
"state": "live"
}))
.unwrap();
let market: OKXEventContractMarket = serde_json::from_value(serde_json::json!({
"seriesId": "BTC-ABOVE-DAILY",
"eventId": "BTC-ABOVE-DAILY-260224-1600",
"instId": "BTC-ABOVE-DAILY-260224-1600-65000",
"listTime": "",
"fixTime": "",
"expTime": "",
"state": "live",
"disputed": false,
"outcome": "0",
"floorStrike": "120000",
"settleValue": ""
}))
.unwrap();
assert_eq!(series.freq, "");
assert_eq!(series.settlement.underlying, "");
assert_eq!(event.exp_time, None);
assert_eq!(market.list_time, None);
assert_eq!(market.exp_time, None);
}
#[rstest]
fn test_place_order_request_serializes_event_contract_fields() {
let request = OKXPlaceOrderRequest {
inst_id: "BTC-ABOVE-DAILY-260224-1600-65000".to_string(),
td_mode: OKXTradeMode::Cash,
ccy: None,
cl_ord_id: Some("event-1".to_string()),
tag: None,
side: OKXSide::Buy,
pos_side: None,
ord_type: OKXOrderType::Limit,
sz: "10".to_string(),
px: Some("0.42".to_string()),
px_usd: None,
px_vol: None,
reduce_only: None,
tgt_ccy: None,
attach_algo_ords: None,
speed_bump: Some("1".to_string()),
outcome: Some("yes".to_string()),
slippage_pct: None,
rpi_taker_access: None,
rpi_px_round: None,
};
let json: serde_json::Value = serde_json::to_value(&request).unwrap();
assert_eq!(json["speedBump"], "1");
assert_eq!(json["outcome"], "yes");
}
#[rstest]
fn test_attach_algo_ord_request_serializes_trailing_fields() {
let request = OKXAttachAlgoOrdRequest {
attach_algo_cl_ord_id: Some("trail-1".to_string()),
sl_trigger_px: None,
sl_ord_px: None,
sl_trigger_px_type: None,
tp_trigger_px: None,
tp_ord_px: None,
tp_trigger_px_type: None,
callback_ratio: Some("0.01".to_string()),
callback_spread: None,
active_px: Some("64000".to_string()),
new_callback_ratio: Some("0.02".to_string()),
new_callback_spread: Some("25".to_string()),
new_active_px: Some("65000".to_string()),
};
let json: serde_json::Value = serde_json::to_value(&request).unwrap();
assert_eq!(json["callbackRatio"], "0.01");
assert_eq!(json["activePx"], "64000");
assert_eq!(json["newCallbackRatio"], "0.02");
assert_eq!(json["newCallbackSpread"], "25");
assert_eq!(json["newActivePx"], "65000");
assert!(json.get("callbackSpread").is_none());
}
#[rstest]
fn test_place_order_response_deserializes_sub_code() {
let response: OKXPlaceOrderResponse = serde_json::from_value(serde_json::json!({
"ordId": "",
"clOrdId": "event-1",
"sCode": "51000",
"sMsg": "Parameter error",
"subCode": "51004"
}))
.unwrap();
assert_eq!(response.cl_ord_id, Some(Ustr::from("event-1")));
assert_eq!(response.s_code, Some("51000".to_string()));
assert_eq!(response.sub_code, Some("51004".to_string()));
}
#[rstest]
fn test_fee_rate_deserializes_settle() {
let fee_rate: OKXFeeRate = serde_json::from_value(serde_json::json!({
"level": "VIP1",
"taker": "-0.0005",
"maker": "-0.0002",
"takerU": "-0.0005",
"makerU": "-0.0002",
"settle": "-0.001",
"instType": "EVENTS",
"category": "1",
"ts": "1769697132335"
}))
.unwrap();
assert_eq!(fee_rate.settle, "-0.001");
assert_eq!(fee_rate.inst_type, OKXInstrumentType::Events);
}
#[rstest]
fn test_rpi_maker_fee_rate_deserializes_decimal_and_legacy_alias() {
let rpi: OKXFeeRate = serde_json::from_value(serde_json::json!({
"level": "VIP1",
"taker": "-0.0005",
"maker": "-0.0002",
"takerU": "-0.0005",
"makerU": "-0.0002",
"rpiMaker": "-0.00015",
"instType": "SPOT",
"category": "1",
"ts": "1785406500000"
}))
.unwrap();
let legacy: OKXFeeRate = serde_json::from_value(serde_json::json!({
"level": "VIP1",
"taker": "-0.0005",
"maker": "-0.0002",
"takerU": "-0.0005",
"makerU": "-0.0002",
"elpMaker": "-0.00016",
"instType": "SPOT",
"category": "1",
"ts": "1785406500001"
}))
.unwrap();
let not_applicable: OKXFeeRate = serde_json::from_value(serde_json::json!({
"level": "VIP1",
"taker": "-0.0005",
"maker": "-0.0002",
"takerU": "-0.0005",
"makerU": "-0.0002",
"rpiMaker": "",
"instType": "OPTION",
"category": "1",
"ts": "1785406500002"
}))
.unwrap();
assert_eq!(
rpi.rpi_maker,
Some(Decimal::from_str_exact("-0.00015").unwrap())
);
assert_eq!(
legacy.rpi_maker,
Some(Decimal::from_str_exact("-0.00016").unwrap())
);
assert_eq!(not_applicable.rpi_maker, None);
}
}