1use derive_builder::Builder;
31use serde::{self, Deserialize, Serialize};
32
33use crate::{
34 common::enums::{
35 OKXInstrumentType, OKXOrderStatus, OKXOrderType, OKXPositionMode, OKXPositionSide,
36 OKXTradeMode,
37 },
38 http::error::BuildError,
39};
40
41#[derive(Clone, Debug, Deserialize, Serialize, Builder)]
43#[builder(setter(into, strip_option))]
44#[serde(rename_all = "camelCase")]
45pub struct SetPositionModeParams {
46 #[serde(rename = "posMode")]
48 pub pos_mode: OKXPositionMode,
49}
50
51#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
53#[builder(default)]
54#[builder(setter(into, strip_option))]
55#[serde(rename_all = "camelCase")]
56pub struct GetPositionTiersParams {
57 pub inst_type: OKXInstrumentType,
59 pub td_mode: OKXTradeMode,
61 #[serde(skip_serializing_if = "Option::is_none")]
64 pub uly: Option<String>,
65 #[serde(skip_serializing_if = "Option::is_none")]
68 pub inst_family: Option<String>,
69 #[serde(skip_serializing_if = "Option::is_none")]
71 pub inst_id: Option<String>,
72 #[serde(skip_serializing_if = "Option::is_none")]
74 pub ccy: Option<String>,
75 #[serde(skip_serializing_if = "Option::is_none")]
77 pub tier: Option<String>,
78}
79
80#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
82#[builder(default)]
83#[builder(setter(into, strip_option))]
84#[serde(rename_all = "camelCase")]
85pub struct GetInstrumentsParams {
86 pub inst_type: OKXInstrumentType,
88 #[serde(skip_serializing_if = "Option::is_none")]
91 pub uly: Option<String>,
92 #[serde(skip_serializing_if = "Option::is_none")]
95 pub inst_family: Option<String>,
96 #[serde(skip_serializing_if = "Option::is_none")]
98 pub inst_id: Option<String>,
99}
100
101#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
103#[builder(default)]
104#[builder(setter(into, strip_option))]
105#[serde(rename_all = "camelCase")]
106pub struct GetTradesParams {
107 pub inst_id: String,
109 #[serde(rename = "type")]
111 #[serde(skip_serializing_if = "Option::is_none")]
112 pub pagination_type: Option<u8>,
113 #[serde(skip_serializing_if = "Option::is_none")]
115 pub after: Option<String>,
116 #[serde(skip_serializing_if = "Option::is_none")]
118 pub before: Option<String>,
119 #[serde(skip_serializing_if = "Option::is_none")]
121 pub limit: Option<u32>,
122}
123
124#[derive(Clone, Debug, Deserialize, Serialize)]
126#[serde(rename_all = "camelCase")]
127pub struct GetCandlesticksParams {
128 pub inst_id: String,
130 pub bar: String,
132 #[serde(skip_serializing_if = "Option::is_none")]
134 #[serde(rename = "after")]
135 pub after_ms: Option<i64>,
136 #[serde(skip_serializing_if = "Option::is_none")]
138 #[serde(rename = "before")]
139 pub before_ms: Option<i64>,
140 #[serde(skip_serializing_if = "Option::is_none")]
142 pub limit: Option<u32>,
143}
144
145#[derive(Debug, Default)]
147pub struct GetCandlesticksParamsBuilder {
148 inst_id: Option<String>,
149 bar: Option<String>,
150 after_ms: Option<i64>,
151 before_ms: Option<i64>,
152 limit: Option<u32>,
153}
154
155impl GetCandlesticksParamsBuilder {
156 pub fn inst_id(&mut self, inst_id: impl Into<String>) -> &mut Self {
158 self.inst_id = Some(inst_id.into());
159 self
160 }
161
162 pub fn bar(&mut self, bar: impl Into<String>) -> &mut Self {
164 self.bar = Some(bar.into());
165 self
166 }
167
168 pub fn after_ms(&mut self, after_ms: i64) -> &mut Self {
170 self.after_ms = Some(after_ms);
171 self
172 }
173
174 pub fn before_ms(&mut self, before_ms: i64) -> &mut Self {
176 self.before_ms = Some(before_ms);
177 self
178 }
179
180 pub fn limit(&mut self, limit: u32) -> &mut Self {
182 self.limit = Some(limit);
183 self
184 }
185
186 pub fn build(&mut self) -> Result<GetCandlesticksParams, BuildError> {
192 let inst_id = self.inst_id.clone().ok_or(BuildError::MissingInstId)?;
194 let bar = self.bar.clone().ok_or(BuildError::MissingBar)?;
195 let after_ms = self.after_ms;
196 let before_ms = self.before_ms;
197 let limit = self.limit;
198
199 if let (Some(after), Some(before)) = (after_ms, before_ms)
213 && before >= after
214 {
215 return Err(BuildError::InvalidTimeRange {
216 after_ms: after,
217 before_ms: before,
218 });
219 }
220
221 if let Some(nanos) = after_ms
223 && nanos.abs() > 9_999_999_999_999
224 {
225 return Err(BuildError::CursorIsNanoseconds);
226 }
227
228 if let Some(nanos) = before_ms
229 && nanos.abs() > 9_999_999_999_999
230 {
231 return Err(BuildError::CursorIsNanoseconds);
232 }
233
234 if let Some(limit) = limit
238 && limit > 300
239 {
240 return Err(BuildError::LimitTooHigh);
241 }
242
243 Ok(GetCandlesticksParams {
244 inst_id,
245 bar,
246 after_ms,
247 before_ms,
248 limit,
249 })
250 }
251}
252
253#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
255#[builder(default)]
256#[builder(setter(into, strip_option))]
257#[serde(rename_all = "camelCase")]
258pub struct GetMarkPriceParams {
259 pub inst_type: OKXInstrumentType,
261 #[serde(skip_serializing_if = "Option::is_none")]
264 pub uly: Option<String>,
265 #[serde(skip_serializing_if = "Option::is_none")]
268 pub inst_family: Option<String>,
269 #[serde(skip_serializing_if = "Option::is_none")]
271 pub inst_id: Option<String>,
272}
273
274#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
276#[builder(default)]
277#[builder(setter(into, strip_option))]
278#[serde(rename_all = "camelCase")]
279pub struct GetIndexTickerParams {
280 #[serde(skip_serializing_if = "Option::is_none")]
282 pub inst_id: Option<String>,
283 #[serde(skip_serializing_if = "Option::is_none")]
285 pub quote_ccy: Option<String>,
286}
287
288#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
290#[builder(default)]
291#[builder(setter(into, strip_option))]
292#[serde(rename_all = "camelCase")]
293pub struct GetOrderHistoryParams {
294 pub inst_type: OKXInstrumentType,
296 #[serde(skip_serializing_if = "Option::is_none")]
298 pub uly: Option<String>,
299 #[serde(skip_serializing_if = "Option::is_none")]
301 pub inst_family: Option<String>,
302 #[serde(skip_serializing_if = "Option::is_none")]
304 pub inst_id: Option<String>,
305 #[serde(skip_serializing_if = "Option::is_none")]
307 pub ord_type: Option<OKXOrderType>,
308 #[serde(skip_serializing_if = "Option::is_none")]
310 pub state: Option<String>,
311 #[serde(skip_serializing_if = "Option::is_none")]
313 pub after: Option<String>,
314 #[serde(skip_serializing_if = "Option::is_none")]
316 pub before: Option<String>,
317 #[serde(skip_serializing_if = "Option::is_none")]
319 pub limit: Option<u32>,
320}
321
322#[derive(Clone, Debug, Default, Deserialize, Serialize, Builder)]
324#[builder(default)]
325#[builder(setter(into, strip_option))]
326#[serde(rename_all = "camelCase")]
327pub struct GetOrderListParams {
328 #[serde(skip_serializing_if = "Option::is_none")]
330 pub inst_type: Option<OKXInstrumentType>,
331 #[serde(skip_serializing_if = "Option::is_none")]
333 pub inst_id: Option<String>,
334 #[serde(skip_serializing_if = "Option::is_none")]
336 pub inst_family: Option<String>,
337 #[serde(skip_serializing_if = "Option::is_none")]
339 pub state: Option<OKXOrderStatus>,
340 #[serde(skip_serializing_if = "Option::is_none")]
342 pub after: Option<String>,
343 #[serde(skip_serializing_if = "Option::is_none")]
345 pub before: Option<String>,
346 #[serde(skip_serializing_if = "Option::is_none")]
348 pub limit: Option<u32>,
349}
350
351#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
353#[builder(default)]
354#[builder(setter(into, strip_option))]
355#[serde(rename_all = "camelCase")]
356pub struct GetAlgoOrdersParams {
357 #[serde(rename = "algoId", skip_serializing_if = "Option::is_none")]
359 pub algo_id: Option<String>,
360 #[serde(rename = "algoClOrdId", skip_serializing_if = "Option::is_none")]
362 pub algo_cl_ord_id: Option<String>,
363 pub inst_type: OKXInstrumentType,
365 #[serde(rename = "instId", skip_serializing_if = "Option::is_none")]
367 pub inst_id: Option<String>,
368 #[serde(rename = "ordType", skip_serializing_if = "Option::is_none")]
370 pub ord_type: Option<OKXOrderType>,
371 #[serde(skip_serializing_if = "Option::is_none")]
373 pub state: Option<OKXOrderStatus>,
374 #[serde(skip_serializing_if = "Option::is_none")]
376 pub after: Option<String>,
377 #[serde(skip_serializing_if = "Option::is_none")]
379 pub before: Option<String>,
380 #[serde(skip_serializing_if = "Option::is_none")]
382 pub limit: Option<u32>,
383}
384
385#[derive(Clone, Debug, Default, Deserialize, Serialize, Builder)]
387#[builder(default)]
388#[builder(setter(into, strip_option))]
389#[serde(rename_all = "camelCase")]
390pub struct GetTransactionDetailsParams {
391 #[serde(skip_serializing_if = "Option::is_none")]
393 pub inst_type: Option<OKXInstrumentType>,
394 #[serde(skip_serializing_if = "Option::is_none")]
396 pub inst_id: Option<String>,
397 #[serde(skip_serializing_if = "Option::is_none")]
399 pub ord_id: Option<String>,
400 #[serde(skip_serializing_if = "Option::is_none")]
402 pub after: Option<String>,
403 #[serde(skip_serializing_if = "Option::is_none")]
405 pub before: Option<String>,
406 #[serde(skip_serializing_if = "Option::is_none")]
408 pub limit: Option<u32>,
409}
410
411#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
413#[builder(default)]
414#[builder(setter(into, strip_option))]
415#[serde(rename_all = "camelCase")]
416pub struct GetPositionsParams {
417 #[serde(skip_serializing_if = "Option::is_none")]
419 pub inst_type: Option<OKXInstrumentType>,
420 #[serde(skip_serializing_if = "Option::is_none")]
422 pub inst_id: Option<String>,
423 #[serde(skip_serializing_if = "Option::is_none")]
425 pub pos_id: Option<String>,
426}
427
428#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
430#[builder(default)]
431#[builder(setter(into, strip_option))]
432#[serde(rename_all = "camelCase")]
433pub struct GetPositionsHistoryParams {
434 pub inst_type: OKXInstrumentType,
436 #[serde(skip_serializing_if = "Option::is_none")]
438 pub inst_id: Option<String>,
439 #[serde(skip_serializing_if = "Option::is_none")]
441 pub pos_id: Option<String>,
442 #[serde(skip_serializing_if = "Option::is_none")]
444 pub after: Option<String>,
445 #[serde(skip_serializing_if = "Option::is_none")]
447 pub before: Option<String>,
448 #[serde(skip_serializing_if = "Option::is_none")]
450 pub limit: Option<u32>,
451}
452
453#[derive(Clone, Debug, Default, Deserialize, Serialize, Builder)]
455#[builder(default)]
456#[builder(setter(into, strip_option))]
457#[serde(rename_all = "camelCase")]
458pub struct GetOrderParams {
459 pub inst_type: OKXInstrumentType,
461 pub inst_id: String,
463 #[serde(skip_serializing_if = "Option::is_none")]
465 pub ord_id: Option<String>,
466 #[serde(skip_serializing_if = "Option::is_none")]
468 pub cl_ord_id: Option<String>,
469 #[serde(skip_serializing_if = "Option::is_none")]
471 pub pos_side: Option<OKXPositionSide>,
472}
473
474#[derive(Clone, Debug, Deserialize, Serialize, Builder)]
476#[builder(setter(into, strip_option))]
477#[serde(rename_all = "camelCase")]
478pub struct GetTradeFeeParams {
479 pub inst_type: OKXInstrumentType,
481 #[serde(skip_serializing_if = "Option::is_none")]
483 pub uly: Option<String>,
484 #[serde(skip_serializing_if = "Option::is_none")]
486 pub inst_family: Option<String>,
487}
488
489#[cfg(test)]
490mod tests {
491 use rstest::rstest;
492
493 use super::*;
494
495 #[rstest]
496 fn test_optional_parameters_are_omitted_when_none() {
497 let mut builder = GetCandlesticksParamsBuilder::default();
498 builder.inst_id("BTC-USDT-SWAP");
499 builder.bar("1m");
500
501 let params = builder.build().unwrap();
502 let qs = serde_urlencoded::to_string(¶ms).unwrap();
503 assert_eq!(
504 qs, "instId=BTC-USDT-SWAP&bar=1m",
505 "unexpected optional parameters were serialized: {qs}",
506 );
507 }
508
509 #[rstest]
510 fn test_no_literal_none_strings_leak_into_query_string() {
511 let mut builder = GetCandlesticksParamsBuilder::default();
512 builder.inst_id("BTC-USDT-SWAP");
513 builder.bar("1m");
514
515 let params = builder.build().unwrap();
516 let qs = serde_urlencoded::to_string(¶ms).unwrap();
517 assert!(
518 !qs.contains("None"),
519 "found literal \"None\" in query string: {qs}",
520 );
521 assert!(
522 !qs.contains("after=") && !qs.contains("before=") && !qs.contains("limit="),
523 "empty optional parameters must be omitted entirely: {qs}",
524 );
525 }
526
527 #[rstest]
528 fn test_cursor_nanoseconds_rejected() {
529 let after_nanos = 1_725_307_200_000_000_000i64;
531
532 let mut builder = GetCandlesticksParamsBuilder::default();
533 builder.inst_id("BTC-USDT-SWAP");
534 builder.bar("1m");
535 builder.after_ms(after_nanos);
536
537 let result = builder.build();
539 assert!(result.is_err());
540 assert!(result.unwrap_err().to_string().contains("nanoseconds"));
541 }
542
543 #[rstest]
544 fn test_both_cursors_rejected() {
545 let mut builder = GetCandlesticksParamsBuilder::default();
546 builder.inst_id("BTC-USDT-SWAP");
547 builder.bar("1m");
548 builder.after_ms(1725307200000);
551 builder.before_ms(1725393600000);
552
553 let result = builder.build();
554 assert!(result.is_err());
555 assert!(result.unwrap_err().to_string().contains("time range"));
556 }
557
558 #[rstest]
559 fn test_limit_exceeds_maximum_rejected() {
560 let mut builder = GetCandlesticksParamsBuilder::default();
561 builder.inst_id("BTC-USDT-SWAP");
562 builder.bar("1m");
563 builder.limit(301u32); let result = builder.build();
567 assert!(result.is_err());
568 assert!(result.unwrap_err().to_string().contains("300"));
569 }
570
571 #[rstest]
572 #[case(1725307200000, "after=1725307200000")] #[case(1725307200, "after=1725307200")] #[case(1725307, "after=1725307")] fn test_valid_millisecond_cursor_passes(#[case] timestamp: i64, #[case] expected: &str) {
576 let mut builder = GetCandlesticksParamsBuilder::default();
577 builder.inst_id("BTC-USDT-SWAP");
578 builder.bar("1m");
579 builder.after_ms(timestamp);
580
581 let params = builder.build().unwrap();
582 let qs = serde_urlencoded::to_string(¶ms).unwrap();
583 assert!(qs.contains(expected));
584 }
585
586 #[rstest]
587 #[case(1, "limit=1")]
588 #[case(50, "limit=50")]
589 #[case(100, "limit=100")]
590 #[case(300, "limit=300")] fn test_valid_limit_passes(#[case] limit: u32, #[case] expected: &str) {
592 let mut builder = GetCandlesticksParamsBuilder::default();
593 builder.inst_id("BTC-USDT-SWAP");
594 builder.bar("1m");
595 builder.limit(limit);
596
597 let params = builder.build().unwrap();
598 let qs = serde_urlencoded::to_string(¶ms).unwrap();
599 assert!(qs.contains(expected));
600 }
601}