1use derive_builder::Builder;
42use serde::{self, Deserialize, Serialize};
43
44use crate::{
45 common::enums::{
46 OKXInstrumentType, OKXOrderStatus, OKXOrderType, OKXPositionMode, OKXPositionSide,
47 OKXTradeMode,
48 },
49 http::error::BuildError,
50};
51
52#[allow(dead_code, reason = "Under development")]
53fn serialize_string_vec<S>(values: &Option<Vec<String>>, serializer: S) -> Result<S::Ok, S::Error>
54where
55 S: serde::Serializer,
56{
57 match values {
58 Some(vec) => serializer.serialize_str(&vec.join(",")),
59 None => serializer.serialize_none(),
60 }
61}
62
63#[derive(Clone, Debug, Deserialize, Serialize, Builder)]
65#[builder(setter(into, strip_option))]
66#[serde(rename_all = "camelCase")]
67pub struct SetPositionModeParams {
68 #[serde(rename = "posMode")]
70 pub pos_mode: OKXPositionMode,
71}
72
73#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
75#[builder(default)]
76#[builder(setter(into, strip_option))]
77#[serde(rename_all = "camelCase")]
78pub struct GetPositionTiersParams {
79 pub inst_type: OKXInstrumentType,
81 pub td_mode: OKXTradeMode,
83 #[serde(skip_serializing_if = "Option::is_none")]
86 pub uly: Option<String>,
87 #[serde(skip_serializing_if = "Option::is_none")]
90 pub inst_family: Option<String>,
91 #[serde(skip_serializing_if = "Option::is_none")]
93 pub inst_id: Option<String>,
94 #[serde(skip_serializing_if = "Option::is_none")]
96 pub ccy: Option<String>,
97 #[serde(skip_serializing_if = "Option::is_none")]
99 pub tier: Option<String>,
100}
101
102#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
104#[builder(default)]
105#[builder(setter(into, strip_option))]
106#[serde(rename_all = "camelCase")]
107pub struct GetInstrumentsParams {
108 pub inst_type: OKXInstrumentType,
110 #[serde(skip_serializing_if = "Option::is_none")]
113 pub uly: Option<String>,
114 #[serde(skip_serializing_if = "Option::is_none")]
117 pub inst_family: Option<String>,
118 #[serde(skip_serializing_if = "Option::is_none")]
120 pub inst_id: Option<String>,
121}
122
123#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
125#[builder(default)]
126#[builder(setter(into, strip_option))]
127#[serde(rename_all = "camelCase")]
128pub struct GetTradesParams {
129 pub inst_id: String,
131 #[serde(skip_serializing_if = "Option::is_none")]
133 pub after: Option<String>,
134 #[serde(skip_serializing_if = "Option::is_none")]
136 pub before: Option<String>,
137 #[serde(skip_serializing_if = "Option::is_none")]
139 pub limit: Option<u32>,
140}
141
142#[derive(Clone, Debug, Deserialize, Serialize)]
144#[serde(rename_all = "camelCase")]
145pub struct GetCandlesticksParams {
146 pub inst_id: String,
148 pub bar: String,
150 #[serde(skip_serializing_if = "Option::is_none")]
152 #[serde(rename = "after")]
153 pub after_ms: Option<i64>,
154 #[serde(skip_serializing_if = "Option::is_none")]
156 #[serde(rename = "before")]
157 pub before_ms: Option<i64>,
158 #[serde(skip_serializing_if = "Option::is_none")]
160 pub limit: Option<u32>,
161}
162
163#[derive(Debug, Default)]
165pub struct GetCandlesticksParamsBuilder {
166 inst_id: Option<String>,
167 bar: Option<String>,
168 after_ms: Option<i64>,
169 before_ms: Option<i64>,
170 limit: Option<u32>,
171}
172
173impl GetCandlesticksParamsBuilder {
174 pub fn inst_id(&mut self, inst_id: impl Into<String>) -> &mut Self {
176 self.inst_id = Some(inst_id.into());
177 self
178 }
179
180 pub fn bar(&mut self, bar: impl Into<String>) -> &mut Self {
182 self.bar = Some(bar.into());
183 self
184 }
185
186 pub fn after_ms(&mut self, after_ms: i64) -> &mut Self {
188 self.after_ms = Some(after_ms);
189 self
190 }
191
192 pub fn before_ms(&mut self, before_ms: i64) -> &mut Self {
194 self.before_ms = Some(before_ms);
195 self
196 }
197
198 pub fn limit(&mut self, limit: u32) -> &mut Self {
200 self.limit = Some(limit);
201 self
202 }
203
204 pub fn build(&mut self) -> Result<GetCandlesticksParams, BuildError> {
210 let inst_id = self.inst_id.clone().ok_or(BuildError::MissingInstId)?;
212 let bar = self.bar.clone().ok_or(BuildError::MissingBar)?;
213 let after_ms = self.after_ms;
214 let before_ms = self.before_ms;
215 let limit = self.limit;
216
217 if after_ms.is_some() && before_ms.is_some() {
220 return Err(BuildError::BothCursors);
221 }
222
223 if let (Some(after), Some(before)) = (after_ms, before_ms)
229 && after >= before
230 {
231 return Err(BuildError::InvalidTimeRange {
232 after_ms: after,
233 before_ms: before,
234 });
235 }
236
237 if let Some(nanos) = after_ms
239 && nanos.abs() > 9_999_999_999_999
240 {
241 return Err(BuildError::CursorIsNanoseconds);
242 }
243
244 if let Some(nanos) = before_ms
245 && nanos.abs() > 9_999_999_999_999
246 {
247 return Err(BuildError::CursorIsNanoseconds);
248 }
249
250 if let Some(limit) = limit
254 && limit > 300
255 {
256 return Err(BuildError::LimitTooHigh);
257 }
258
259 Ok(GetCandlesticksParams {
260 inst_id,
261 bar,
262 after_ms,
263 before_ms,
264 limit,
265 })
266 }
267}
268
269#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
271#[builder(default)]
272#[builder(setter(into, strip_option))]
273#[serde(rename_all = "camelCase")]
274pub struct GetMarkPriceParams {
275 pub inst_type: OKXInstrumentType,
277 #[serde(skip_serializing_if = "Option::is_none")]
280 pub uly: Option<String>,
281 #[serde(skip_serializing_if = "Option::is_none")]
284 pub inst_family: Option<String>,
285 #[serde(skip_serializing_if = "Option::is_none")]
287 pub inst_id: Option<String>,
288}
289
290#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
292#[builder(default)]
293#[builder(setter(into, strip_option))]
294#[serde(rename_all = "camelCase")]
295pub struct GetIndexTickerParams {
296 #[serde(skip_serializing_if = "Option::is_none")]
298 pub inst_id: Option<String>,
299 #[serde(skip_serializing_if = "Option::is_none")]
301 pub quote_ccy: Option<String>,
302}
303
304#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
306#[builder(default)]
307#[builder(setter(into, strip_option))]
308#[serde(rename_all = "camelCase")]
309pub struct GetOrderHistoryParams {
310 pub inst_type: OKXInstrumentType,
312 #[serde(skip_serializing_if = "Option::is_none")]
314 pub uly: Option<String>,
315 #[serde(skip_serializing_if = "Option::is_none")]
317 pub inst_family: Option<String>,
318 #[serde(skip_serializing_if = "Option::is_none")]
320 pub inst_id: Option<String>,
321 #[serde(skip_serializing_if = "Option::is_none")]
323 pub ord_type: Option<OKXOrderType>,
324 #[serde(skip_serializing_if = "Option::is_none")]
326 pub state: Option<String>,
327 #[serde(skip_serializing_if = "Option::is_none")]
329 pub after: Option<String>,
330 #[serde(skip_serializing_if = "Option::is_none")]
332 pub before: Option<String>,
333 #[serde(skip_serializing_if = "Option::is_none")]
335 pub limit: Option<u32>,
336}
337
338#[derive(Clone, Debug, Default, Deserialize, Serialize, Builder)]
340#[builder(default)]
341#[builder(setter(into, strip_option))]
342#[serde(rename_all = "camelCase")]
343pub struct GetOrderListParams {
344 #[serde(skip_serializing_if = "Option::is_none")]
346 pub inst_type: Option<OKXInstrumentType>,
347 #[serde(skip_serializing_if = "Option::is_none")]
349 pub inst_id: Option<String>,
350 #[serde(skip_serializing_if = "Option::is_none")]
352 pub inst_family: Option<String>,
353 #[serde(skip_serializing_if = "Option::is_none")]
355 pub state: Option<OKXOrderStatus>,
356 #[serde(skip_serializing_if = "Option::is_none")]
358 pub after: Option<String>,
359 #[serde(skip_serializing_if = "Option::is_none")]
361 pub before: Option<String>,
362 #[serde(skip_serializing_if = "Option::is_none")]
364 pub limit: Option<u32>,
365}
366
367#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
369#[builder(default)]
370#[builder(setter(into, strip_option))]
371#[serde(rename_all = "camelCase")]
372pub struct GetAlgoOrdersParams {
373 #[serde(rename = "algoId", skip_serializing_if = "Option::is_none")]
375 pub algo_id: Option<String>,
376 #[serde(rename = "algoClOrdId", skip_serializing_if = "Option::is_none")]
378 pub algo_cl_ord_id: Option<String>,
379 pub inst_type: OKXInstrumentType,
381 #[serde(rename = "instId", skip_serializing_if = "Option::is_none")]
383 pub inst_id: Option<String>,
384 #[serde(rename = "ordType", skip_serializing_if = "Option::is_none")]
386 pub ord_type: Option<OKXOrderType>,
387 #[serde(skip_serializing_if = "Option::is_none")]
389 pub state: Option<OKXOrderStatus>,
390 #[serde(skip_serializing_if = "Option::is_none")]
392 pub after: Option<String>,
393 #[serde(skip_serializing_if = "Option::is_none")]
395 pub before: Option<String>,
396 #[serde(skip_serializing_if = "Option::is_none")]
398 pub limit: Option<u32>,
399}
400
401#[derive(Clone, Debug, Default, Deserialize, Serialize, Builder)]
403#[builder(default)]
404#[builder(setter(into, strip_option))]
405#[serde(rename_all = "camelCase")]
406pub struct GetTransactionDetailsParams {
407 #[serde(skip_serializing_if = "Option::is_none")]
409 pub inst_type: Option<OKXInstrumentType>,
410 #[serde(skip_serializing_if = "Option::is_none")]
412 pub inst_id: Option<String>,
413 #[serde(skip_serializing_if = "Option::is_none")]
415 pub ord_id: Option<String>,
416 #[serde(skip_serializing_if = "Option::is_none")]
418 pub after: Option<String>,
419 #[serde(skip_serializing_if = "Option::is_none")]
421 pub before: Option<String>,
422 #[serde(skip_serializing_if = "Option::is_none")]
424 pub limit: Option<u32>,
425}
426
427#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
429#[builder(default)]
430#[builder(setter(into, strip_option))]
431#[serde(rename_all = "camelCase")]
432pub struct GetPositionsParams {
433 #[serde(skip_serializing_if = "Option::is_none")]
435 pub inst_type: Option<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}
443
444#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
446#[builder(default)]
447#[builder(setter(into, strip_option))]
448#[serde(rename_all = "camelCase")]
449pub struct GetPositionsHistoryParams {
450 pub inst_type: OKXInstrumentType,
452 #[serde(skip_serializing_if = "Option::is_none")]
454 pub inst_id: Option<String>,
455 #[serde(skip_serializing_if = "Option::is_none")]
457 pub pos_id: Option<String>,
458 #[serde(skip_serializing_if = "Option::is_none")]
460 pub after: Option<String>,
461 #[serde(skip_serializing_if = "Option::is_none")]
463 pub before: Option<String>,
464 #[serde(skip_serializing_if = "Option::is_none")]
466 pub limit: Option<u32>,
467}
468
469#[derive(Clone, Debug, Default, Deserialize, Serialize, Builder)]
471#[builder(default)]
472#[builder(setter(into, strip_option))]
473#[serde(rename_all = "camelCase")]
474pub struct GetPendingOrdersParams {
475 pub inst_type: OKXInstrumentType,
477 pub inst_id: String,
479 #[serde(skip_serializing_if = "Option::is_none")]
481 pub pos_side: Option<OKXPositionSide>,
482}
483
484#[derive(Clone, Debug, Default, Deserialize, Serialize, Builder)]
486#[builder(default)]
487#[builder(setter(into, strip_option))]
488#[serde(rename_all = "camelCase")]
489pub struct GetOrderParams {
490 pub inst_type: OKXInstrumentType,
492 pub inst_id: String,
494 #[serde(skip_serializing_if = "Option::is_none")]
496 pub ord_id: Option<String>,
497 #[serde(skip_serializing_if = "Option::is_none")]
499 pub cl_ord_id: Option<String>,
500 #[serde(skip_serializing_if = "Option::is_none")]
502 pub pos_side: Option<OKXPositionSide>,
503}
504
505#[derive(Clone, Debug, Deserialize, Serialize, Builder)]
507#[builder(setter(into, strip_option))]
508#[serde(rename_all = "camelCase")]
509pub struct GetTradeFeeParams {
510 pub inst_type: OKXInstrumentType,
512 #[serde(skip_serializing_if = "Option::is_none")]
514 pub uly: Option<String>,
515 #[serde(skip_serializing_if = "Option::is_none")]
517 pub inst_family: Option<String>,
518}
519
520#[cfg(test)]
525mod tests {
526 use rstest::rstest;
527
528 use super::*;
529
530 #[rstest]
531 fn test_optional_parameters_are_omitted_when_none() {
532 let mut builder = GetCandlesticksParamsBuilder::default();
533 builder.inst_id("BTC-USDT-SWAP");
534 builder.bar("1m");
535
536 let params = builder.build().unwrap();
537 let qs = serde_urlencoded::to_string(¶ms).unwrap();
538 assert_eq!(
539 qs, "instId=BTC-USDT-SWAP&bar=1m",
540 "unexpected optional parameters were serialized: {qs}",
541 );
542 }
543
544 #[rstest]
545 fn test_no_literal_none_strings_leak_into_query_string() {
546 let mut builder = GetCandlesticksParamsBuilder::default();
547 builder.inst_id("BTC-USDT-SWAP");
548 builder.bar("1m");
549
550 let params = builder.build().unwrap();
551 let qs = serde_urlencoded::to_string(¶ms).unwrap();
552 assert!(
553 !qs.contains("None"),
554 "found literal \"None\" in query string: {qs}",
555 );
556 assert!(
557 !qs.contains("after=") && !qs.contains("before=") && !qs.contains("limit="),
558 "empty optional parameters must be omitted entirely: {qs}",
559 );
560 }
561
562 #[rstest]
563 fn test_cursor_nanoseconds_rejected() {
564 let after_nanos = 1_725_307_200_000_000_000i64;
566
567 let mut builder = GetCandlesticksParamsBuilder::default();
568 builder.inst_id("BTC-USDT-SWAP");
569 builder.bar("1m");
570 builder.after_ms(after_nanos);
571
572 let result = builder.build();
574 assert!(result.is_err());
575 assert!(result.unwrap_err().to_string().contains("nanoseconds"));
576 }
577
578 #[rstest]
579 fn test_both_cursors_rejected() {
580 let mut builder = GetCandlesticksParamsBuilder::default();
581 builder.inst_id("BTC-USDT-SWAP");
582 builder.bar("1m");
583 builder.after_ms(1725307200000);
584 builder.before_ms(1725393600000);
585
586 let result = builder.build();
588 assert!(result.is_err());
589 assert!(result.unwrap_err().to_string().contains("both"));
590 }
591
592 #[rstest]
593 fn test_limit_exceeds_maximum_rejected() {
594 let mut builder = GetCandlesticksParamsBuilder::default();
595 builder.inst_id("BTC-USDT-SWAP");
596 builder.bar("1m");
597 builder.limit(301u32); let result = builder.build();
601 assert!(result.is_err());
602 assert!(result.unwrap_err().to_string().contains("300"));
603 }
604
605 #[rstest]
606 #[case(1725307200000, "after=1725307200000")] #[case(1725307200, "after=1725307200")] #[case(1725307, "after=1725307")] fn test_valid_millisecond_cursor_passes(#[case] timestamp: i64, #[case] expected: &str) {
610 let mut builder = GetCandlesticksParamsBuilder::default();
611 builder.inst_id("BTC-USDT-SWAP");
612 builder.bar("1m");
613 builder.after_ms(timestamp);
614
615 let params = builder.build().unwrap();
616 let qs = serde_urlencoded::to_string(¶ms).unwrap();
617 assert!(qs.contains(expected));
618 }
619
620 #[rstest]
621 #[case(1, "limit=1")]
622 #[case(50, "limit=50")]
623 #[case(100, "limit=100")]
624 #[case(300, "limit=300")] fn test_valid_limit_passes(#[case] limit: u32, #[case] expected: &str) {
626 let mut builder = GetCandlesticksParamsBuilder::default();
627 builder.inst_id("BTC-USDT-SWAP");
628 builder.bar("1m");
629 builder.limit(limit);
630
631 let params = builder.build().unwrap();
632 let qs = serde_urlencoded::to_string(¶ms).unwrap();
633 assert!(qs.contains(expected));
634 }
635}