use derive_builder::Builder;
use serde::{self, Deserialize, Serialize};
use crate::{
common::enums::{
OKXAlgoOrderStatus, OKXAlgoOrderType, OKXInstrumentType, OKXOrderStatus, OKXOrderType,
OKXPositionMode, OKXPositionSide, OKXTradeMode,
},
http::error::BuildError,
};
#[derive(Clone, Debug, Deserialize, Serialize, Builder)]
#[builder(setter(into, strip_option))]
#[serde(rename_all = "camelCase")]
pub struct SetPositionModeParams {
#[serde(rename = "posMode")]
pub pos_mode: OKXPositionMode,
}
#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
#[builder(default)]
#[builder(setter(into, strip_option))]
#[serde(rename_all = "camelCase")]
pub struct GetPositionTiersParams {
pub inst_type: OKXInstrumentType,
pub td_mode: OKXTradeMode,
#[serde(skip_serializing_if = "Option::is_none")]
pub uly: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub inst_family: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub inst_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ccy: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tier: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
#[builder(default)]
#[builder(setter(into, strip_option))]
#[serde(rename_all = "camelCase")]
pub struct GetInstrumentsParams {
pub inst_type: OKXInstrumentType,
#[serde(skip_serializing_if = "Option::is_none")]
pub uly: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub inst_family: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub inst_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub series_id: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
#[builder(default)]
#[builder(setter(into, strip_option))]
#[serde(rename_all = "camelCase")]
pub struct GetSpreadsParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub base_ccy: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub inst_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sprd_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub state: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
#[builder(default)]
#[builder(setter(into, strip_option))]
#[serde(rename_all = "camelCase")]
pub struct GetSpreadOrderParams {
#[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, Deserialize, Serialize, Default, Builder)]
#[builder(default)]
#[builder(setter(into, strip_option))]
#[serde(rename_all = "camelCase")]
pub struct GetSpreadOrdersParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub sprd_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ord_type: Option<OKXOrderType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub state: Option<OKXOrderStatus>,
#[serde(skip_serializing_if = "Option::is_none")]
pub begin_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub end_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub begin: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub end: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
}
#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
#[builder(default)]
#[builder(setter(into, strip_option))]
#[serde(rename_all = "camelCase")]
pub struct GetSpreadTradesParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub sprd_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub trade_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ord_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub begin_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub end_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub begin: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub end: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
}
#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
#[builder(default)]
#[builder(setter(into, strip_option))]
#[serde(rename_all = "camelCase")]
pub struct GetEventContractSeriesParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub series_id: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
#[builder(default)]
#[builder(setter(into, strip_option))]
#[serde(rename_all = "camelCase")]
pub struct GetEventContractEventsParams {
pub series_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub event_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub state: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub before: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub after: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
#[builder(default)]
#[builder(setter(into, strip_option))]
#[serde(rename_all = "camelCase")]
pub struct GetEventContractMarketsParams {
pub series_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub event_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub inst_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub state: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub before: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub after: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
#[builder(default)]
#[builder(setter(into, strip_option))]
#[serde(rename_all = "camelCase")]
pub struct GetOptionSummaryParams {
pub inst_family: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub exp_time: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
#[builder(default)]
#[builder(setter(into, strip_option))]
#[serde(rename_all = "camelCase")]
pub struct GetTradesParams {
pub inst_id: String,
#[serde(rename = "type")]
#[serde(skip_serializing_if = "Option::is_none")]
pub pagination_type: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub after: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub before: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GetCandlesticksParams {
pub inst_id: String,
pub bar: String,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "after")]
pub after_ms: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "before")]
pub before_ms: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
}
#[derive(Debug, Default)]
pub struct GetCandlesticksParamsBuilder {
inst_id: Option<String>,
bar: Option<String>,
after_ms: Option<i64>,
before_ms: Option<i64>,
limit: Option<u32>,
}
impl GetCandlesticksParamsBuilder {
pub fn inst_id(&mut self, inst_id: impl Into<String>) -> &mut Self {
self.inst_id = Some(inst_id.into());
self
}
pub fn bar(&mut self, bar: impl Into<String>) -> &mut Self {
self.bar = Some(bar.into());
self
}
pub fn after_ms(&mut self, after_ms: i64) -> &mut Self {
self.after_ms = Some(after_ms);
self
}
pub fn before_ms(&mut self, before_ms: i64) -> &mut Self {
self.before_ms = Some(before_ms);
self
}
pub fn limit(&mut self, limit: u32) -> &mut Self {
self.limit = Some(limit);
self
}
pub fn build(&mut self) -> Result<GetCandlesticksParams, BuildError> {
let inst_id = self.inst_id.clone().ok_or(BuildError::MissingInstId)?;
let bar = self.bar.clone().ok_or(BuildError::MissingBar)?;
let after_ms = self.after_ms;
let before_ms = self.before_ms;
let limit = self.limit;
if let (Some(after), Some(before)) = (after_ms, before_ms)
&& before >= after
{
return Err(BuildError::InvalidTimeRange {
after_ms: after,
before_ms: before,
});
}
if let Some(nanos) = after_ms
&& nanos.abs() > 9_999_999_999_999
{
return Err(BuildError::CursorIsNanoseconds);
}
if let Some(nanos) = before_ms
&& nanos.abs() > 9_999_999_999_999
{
return Err(BuildError::CursorIsNanoseconds);
}
if let Some(limit) = limit
&& limit > 300
{
return Err(BuildError::LimitTooHigh);
}
Ok(GetCandlesticksParams {
inst_id,
bar,
after_ms,
before_ms,
limit,
})
}
}
#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
#[builder(default)]
#[builder(setter(into, strip_option))]
#[serde(rename_all = "camelCase")]
pub struct GetMarkPriceParams {
pub inst_type: OKXInstrumentType,
#[serde(skip_serializing_if = "Option::is_none")]
pub uly: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub inst_family: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub inst_id: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
#[builder(default)]
#[builder(setter(into, strip_option))]
#[serde(rename_all = "camelCase")]
pub struct GetPriceLimitParams {
pub inst_id: String,
}
#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
#[builder(default)]
#[builder(setter(into, strip_option))]
#[serde(rename_all = "camelCase")]
pub struct GetIndexTickerParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub inst_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub quote_ccy: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
#[builder(default)]
#[builder(setter(into, strip_option))]
#[serde(rename_all = "camelCase")]
pub struct GetOrderBookParams {
pub inst_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub sz: Option<u32>,
}
#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
#[builder(default)]
#[builder(setter(into, strip_option))]
#[serde(rename_all = "camelCase")]
pub struct GetRpiOrderBookParams {
pub inst_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub sz: Option<u32>,
}
#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
#[builder(default)]
#[builder(setter(into, strip_option))]
#[serde(rename_all = "camelCase")]
pub struct GetFundingRateHistoryParams {
pub inst_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub before: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub after: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
}
#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
#[builder(default)]
#[builder(setter(into, strip_option))]
#[serde(rename_all = "camelCase")]
pub struct GetOrderHistoryParams {
pub inst_type: OKXInstrumentType,
#[serde(skip_serializing_if = "Option::is_none")]
pub uly: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub inst_family: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub inst_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ord_type: Option<OKXOrderType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub state: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub after: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub before: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, Builder)]
#[builder(default)]
#[builder(setter(into, strip_option))]
#[serde(rename_all = "camelCase")]
pub struct GetOrderListParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub inst_type: Option<OKXInstrumentType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub inst_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub inst_family: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub state: Option<OKXOrderStatus>,
#[serde(skip_serializing_if = "Option::is_none")]
pub after: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub before: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
}
#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
#[builder(default)]
#[builder(setter(into, strip_option))]
#[serde(rename_all = "camelCase")]
pub struct GetAlgoOrderParams {
#[serde(rename = "algoId", skip_serializing_if = "Option::is_none")]
pub algo_id: Option<String>,
#[serde(rename = "algoClOrdId", skip_serializing_if = "Option::is_none")]
pub algo_cl_ord_id: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
#[builder(default)]
#[builder(setter(into, strip_option))]
#[serde(rename_all = "camelCase")]
pub struct GetAlgoOrdersParams {
#[serde(rename = "algoId", skip_serializing_if = "Option::is_none")]
pub algo_id: Option<String>,
#[serde(rename = "algoClOrdId", skip_serializing_if = "Option::is_none")]
pub algo_cl_ord_id: Option<String>,
pub inst_type: OKXInstrumentType,
#[serde(rename = "instId", skip_serializing_if = "Option::is_none")]
pub inst_id: Option<String>,
#[serde(rename = "ordType", skip_serializing_if = "Option::is_none")]
pub ord_type: Option<OKXAlgoOrderType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub state: Option<OKXAlgoOrderStatus>,
#[serde(skip_serializing_if = "Option::is_none")]
pub after: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub before: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, Builder)]
#[builder(default)]
#[builder(setter(into, strip_option))]
#[serde(rename_all = "camelCase")]
pub struct GetTransactionDetailsParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub inst_type: Option<OKXInstrumentType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub inst_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ord_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub after: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub before: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
}
#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
#[builder(default)]
#[builder(setter(into, strip_option))]
#[serde(rename_all = "camelCase")]
pub struct GetPositionsParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub inst_type: Option<OKXInstrumentType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub inst_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pos_id: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
#[builder(default)]
#[builder(setter(into, strip_option))]
#[serde(rename_all = "camelCase")]
pub struct GetPositionsHistoryParams {
pub inst_type: OKXInstrumentType,
#[serde(skip_serializing_if = "Option::is_none")]
pub inst_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pos_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub after: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub before: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, Builder)]
#[builder(default)]
#[builder(setter(into, strip_option))]
#[serde(rename_all = "camelCase")]
pub struct GetOrderParams {
pub inst_type: OKXInstrumentType,
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 pos_side: Option<OKXPositionSide>,
}
#[derive(Clone, Debug, Deserialize, Serialize, Builder)]
#[builder(setter(into, strip_option))]
#[serde(rename_all = "camelCase")]
pub struct GetTradeFeeParams {
pub inst_type: OKXInstrumentType,
#[serde(skip_serializing_if = "Option::is_none")]
pub uly: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub inst_family: Option<String>,
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[rstest]
fn test_optional_parameters_are_omitted_when_none() {
let mut builder = GetCandlesticksParamsBuilder::default();
builder.inst_id("BTC-USDT-SWAP");
builder.bar("1m");
let params = builder.build().unwrap();
let qs = serde_urlencoded::to_string(¶ms).unwrap();
assert_eq!(
qs, "instId=BTC-USDT-SWAP&bar=1m",
"unexpected optional parameters were serialized: {qs}",
);
}
#[rstest]
fn test_no_literal_none_strings_leak_into_query_string() {
let mut builder = GetCandlesticksParamsBuilder::default();
builder.inst_id("BTC-USDT-SWAP");
builder.bar("1m");
let params = builder.build().unwrap();
let qs = serde_urlencoded::to_string(¶ms).unwrap();
assert!(
!qs.contains("None"),
"found literal \"None\" in query string: {qs}",
);
assert!(
!qs.contains("after=") && !qs.contains("before=") && !qs.contains("limit="),
"empty optional parameters must be omitted entirely: {qs}",
);
}
#[rstest]
fn test_cursor_nanoseconds_rejected() {
let after_nanos = 1_725_307_200_000_000_000i64;
let mut builder = GetCandlesticksParamsBuilder::default();
builder.inst_id("BTC-USDT-SWAP");
builder.bar("1m");
builder.after_ms(after_nanos);
let result = builder.build();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("nanoseconds"));
}
#[rstest]
fn test_both_cursors_rejected() {
let mut builder = GetCandlesticksParamsBuilder::default();
builder.inst_id("BTC-USDT-SWAP");
builder.bar("1m");
builder.after_ms(1725307200000);
builder.before_ms(1725393600000);
let result = builder.build();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("time range"));
}
#[rstest]
fn test_limit_exceeds_maximum_rejected() {
let mut builder = GetCandlesticksParamsBuilder::default();
builder.inst_id("BTC-USDT-SWAP");
builder.bar("1m");
builder.limit(301u32);
let result = builder.build();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("300"));
}
#[rstest]
#[case(1725307200000, "after=1725307200000")] #[case(1725307200, "after=1725307200")] #[case(1725307, "after=1725307")] fn test_valid_millisecond_cursor_passes(#[case] timestamp: i64, #[case] expected: &str) {
let mut builder = GetCandlesticksParamsBuilder::default();
builder.inst_id("BTC-USDT-SWAP");
builder.bar("1m");
builder.after_ms(timestamp);
let params = builder.build().unwrap();
let qs = serde_urlencoded::to_string(¶ms).unwrap();
assert!(qs.contains(expected));
}
#[rstest]
#[case(1, "limit=1")]
#[case(50, "limit=50")]
#[case(100, "limit=100")]
#[case(300, "limit=300")] fn test_valid_limit_passes(#[case] limit: u32, #[case] expected: &str) {
let mut builder = GetCandlesticksParamsBuilder::default();
builder.inst_id("BTC-USDT-SWAP");
builder.bar("1m");
builder.limit(limit);
let params = builder.build().unwrap();
let qs = serde_urlencoded::to_string(¶ms).unwrap();
assert!(qs.contains(expected));
}
}