1use derive_builder::Builder;
31use serde::{self, Deserialize, Serialize};
32
33use crate::{
34 common::enums::{
35 OKXAlgoOrderType, OKXInstrumentType, OKXOrderStatus, OKXOrderType, OKXPositionMode,
36 OKXPositionSide, 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 #[serde(skip_serializing_if = "Option::is_none")]
101 pub series_id: Option<String>,
102}
103
104#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
106#[builder(default)]
107#[builder(setter(into, strip_option))]
108#[serde(rename_all = "camelCase")]
109pub struct GetSpreadsParams {
110 #[serde(skip_serializing_if = "Option::is_none")]
112 pub base_ccy: Option<String>,
113 #[serde(skip_serializing_if = "Option::is_none")]
115 pub inst_id: Option<String>,
116 #[serde(skip_serializing_if = "Option::is_none")]
118 pub sprd_id: Option<String>,
119 #[serde(skip_serializing_if = "Option::is_none")]
121 pub state: Option<String>,
122}
123
124#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
126#[builder(default)]
127#[builder(setter(into, strip_option))]
128#[serde(rename_all = "camelCase")]
129pub struct GetSpreadOrderParams {
130 #[serde(skip_serializing_if = "Option::is_none")]
132 pub ord_id: Option<String>,
133 #[serde(skip_serializing_if = "Option::is_none")]
135 pub cl_ord_id: Option<String>,
136}
137
138#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
140#[builder(default)]
141#[builder(setter(into, strip_option))]
142#[serde(rename_all = "camelCase")]
143pub struct GetSpreadOrdersParams {
144 #[serde(skip_serializing_if = "Option::is_none")]
146 pub sprd_id: Option<String>,
147 #[serde(skip_serializing_if = "Option::is_none")]
149 pub ord_type: Option<OKXOrderType>,
150 #[serde(skip_serializing_if = "Option::is_none")]
152 pub state: Option<OKXOrderStatus>,
153 #[serde(skip_serializing_if = "Option::is_none")]
155 pub begin_id: Option<String>,
156 #[serde(skip_serializing_if = "Option::is_none")]
158 pub end_id: Option<String>,
159 #[serde(skip_serializing_if = "Option::is_none")]
161 pub begin: Option<String>,
162 #[serde(skip_serializing_if = "Option::is_none")]
164 pub end: Option<String>,
165 #[serde(skip_serializing_if = "Option::is_none")]
167 pub limit: Option<u32>,
168}
169
170#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
172#[builder(default)]
173#[builder(setter(into, strip_option))]
174#[serde(rename_all = "camelCase")]
175pub struct GetSpreadTradesParams {
176 #[serde(skip_serializing_if = "Option::is_none")]
178 pub sprd_id: Option<String>,
179 #[serde(skip_serializing_if = "Option::is_none")]
181 pub trade_id: Option<String>,
182 #[serde(skip_serializing_if = "Option::is_none")]
184 pub ord_id: Option<String>,
185 #[serde(skip_serializing_if = "Option::is_none")]
187 pub begin_id: Option<String>,
188 #[serde(skip_serializing_if = "Option::is_none")]
190 pub end_id: Option<String>,
191 #[serde(skip_serializing_if = "Option::is_none")]
193 pub begin: Option<String>,
194 #[serde(skip_serializing_if = "Option::is_none")]
196 pub end: Option<String>,
197 #[serde(skip_serializing_if = "Option::is_none")]
199 pub limit: Option<u32>,
200}
201
202#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
204#[builder(default)]
205#[builder(setter(into, strip_option))]
206#[serde(rename_all = "camelCase")]
207pub struct GetEventContractSeriesParams {
208 #[serde(skip_serializing_if = "Option::is_none")]
210 pub series_id: Option<String>,
211}
212
213#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
215#[builder(default)]
216#[builder(setter(into, strip_option))]
217#[serde(rename_all = "camelCase")]
218pub struct GetEventContractEventsParams {
219 pub series_id: String,
221 #[serde(skip_serializing_if = "Option::is_none")]
223 pub event_id: Option<String>,
224 #[serde(skip_serializing_if = "Option::is_none")]
226 pub state: Option<String>,
227 #[serde(skip_serializing_if = "Option::is_none")]
229 pub limit: Option<String>,
230 #[serde(skip_serializing_if = "Option::is_none")]
232 pub before: Option<String>,
233 #[serde(skip_serializing_if = "Option::is_none")]
235 pub after: Option<String>,
236}
237
238#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
240#[builder(default)]
241#[builder(setter(into, strip_option))]
242#[serde(rename_all = "camelCase")]
243pub struct GetEventContractMarketsParams {
244 pub series_id: String,
246 #[serde(skip_serializing_if = "Option::is_none")]
248 pub event_id: Option<String>,
249 #[serde(skip_serializing_if = "Option::is_none")]
251 pub inst_id: Option<String>,
252 #[serde(skip_serializing_if = "Option::is_none")]
254 pub state: Option<String>,
255 #[serde(skip_serializing_if = "Option::is_none")]
257 pub limit: Option<String>,
258 #[serde(skip_serializing_if = "Option::is_none")]
260 pub before: Option<String>,
261 #[serde(skip_serializing_if = "Option::is_none")]
263 pub after: Option<String>,
264}
265
266#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
268#[builder(default)]
269#[builder(setter(into, strip_option))]
270#[serde(rename_all = "camelCase")]
271pub struct GetOptionSummaryParams {
272 pub inst_family: String,
274 #[serde(skip_serializing_if = "Option::is_none")]
276 pub exp_time: Option<String>,
277}
278
279#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
281#[builder(default)]
282#[builder(setter(into, strip_option))]
283#[serde(rename_all = "camelCase")]
284pub struct GetTradesParams {
285 pub inst_id: String,
287 #[serde(rename = "type")]
289 #[serde(skip_serializing_if = "Option::is_none")]
290 pub pagination_type: Option<u8>,
291 #[serde(skip_serializing_if = "Option::is_none")]
293 pub after: Option<String>,
294 #[serde(skip_serializing_if = "Option::is_none")]
296 pub before: Option<String>,
297 #[serde(skip_serializing_if = "Option::is_none")]
299 pub limit: Option<u32>,
300}
301
302#[derive(Clone, Debug, Deserialize, Serialize)]
304#[serde(rename_all = "camelCase")]
305pub struct GetCandlesticksParams {
306 pub inst_id: String,
308 pub bar: String,
310 #[serde(skip_serializing_if = "Option::is_none")]
312 #[serde(rename = "after")]
313 pub after_ms: Option<i64>,
314 #[serde(skip_serializing_if = "Option::is_none")]
316 #[serde(rename = "before")]
317 pub before_ms: Option<i64>,
318 #[serde(skip_serializing_if = "Option::is_none")]
320 pub limit: Option<u32>,
321}
322
323#[derive(Debug, Default)]
325pub struct GetCandlesticksParamsBuilder {
326 inst_id: Option<String>,
327 bar: Option<String>,
328 after_ms: Option<i64>,
329 before_ms: Option<i64>,
330 limit: Option<u32>,
331}
332
333impl GetCandlesticksParamsBuilder {
334 pub fn inst_id(&mut self, inst_id: impl Into<String>) -> &mut Self {
336 self.inst_id = Some(inst_id.into());
337 self
338 }
339
340 pub fn bar(&mut self, bar: impl Into<String>) -> &mut Self {
342 self.bar = Some(bar.into());
343 self
344 }
345
346 pub fn after_ms(&mut self, after_ms: i64) -> &mut Self {
348 self.after_ms = Some(after_ms);
349 self
350 }
351
352 pub fn before_ms(&mut self, before_ms: i64) -> &mut Self {
354 self.before_ms = Some(before_ms);
355 self
356 }
357
358 pub fn limit(&mut self, limit: u32) -> &mut Self {
360 self.limit = Some(limit);
361 self
362 }
363
364 pub fn build(&mut self) -> Result<GetCandlesticksParams, BuildError> {
370 let inst_id = self.inst_id.clone().ok_or(BuildError::MissingInstId)?;
372 let bar = self.bar.clone().ok_or(BuildError::MissingBar)?;
373 let after_ms = self.after_ms;
374 let before_ms = self.before_ms;
375 let limit = self.limit;
376
377 if let (Some(after), Some(before)) = (after_ms, before_ms)
391 && before >= after
392 {
393 return Err(BuildError::InvalidTimeRange {
394 after_ms: after,
395 before_ms: before,
396 });
397 }
398
399 if let Some(nanos) = after_ms
401 && nanos.abs() > 9_999_999_999_999
402 {
403 return Err(BuildError::CursorIsNanoseconds);
404 }
405
406 if let Some(nanos) = before_ms
407 && nanos.abs() > 9_999_999_999_999
408 {
409 return Err(BuildError::CursorIsNanoseconds);
410 }
411
412 if let Some(limit) = limit
416 && limit > 300
417 {
418 return Err(BuildError::LimitTooHigh);
419 }
420
421 Ok(GetCandlesticksParams {
422 inst_id,
423 bar,
424 after_ms,
425 before_ms,
426 limit,
427 })
428 }
429}
430
431#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
433#[builder(default)]
434#[builder(setter(into, strip_option))]
435#[serde(rename_all = "camelCase")]
436pub struct GetMarkPriceParams {
437 pub inst_type: OKXInstrumentType,
439 #[serde(skip_serializing_if = "Option::is_none")]
442 pub uly: Option<String>,
443 #[serde(skip_serializing_if = "Option::is_none")]
446 pub inst_family: Option<String>,
447 #[serde(skip_serializing_if = "Option::is_none")]
449 pub inst_id: Option<String>,
450}
451
452#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
454#[builder(default)]
455#[builder(setter(into, strip_option))]
456#[serde(rename_all = "camelCase")]
457pub struct GetIndexTickerParams {
458 #[serde(skip_serializing_if = "Option::is_none")]
460 pub inst_id: Option<String>,
461 #[serde(skip_serializing_if = "Option::is_none")]
463 pub quote_ccy: Option<String>,
464}
465
466#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
468#[builder(default)]
469#[builder(setter(into, strip_option))]
470#[serde(rename_all = "camelCase")]
471pub struct GetOrderBookParams {
472 pub inst_id: String,
474 #[serde(skip_serializing_if = "Option::is_none")]
476 pub sz: Option<u32>,
477}
478
479#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
481#[builder(default)]
482#[builder(setter(into, strip_option))]
483#[serde(rename_all = "camelCase")]
484pub struct GetFundingRateHistoryParams {
485 pub inst_id: String,
487 #[serde(skip_serializing_if = "Option::is_none")]
489 pub before: Option<String>,
490 #[serde(skip_serializing_if = "Option::is_none")]
492 pub after: Option<String>,
493 #[serde(skip_serializing_if = "Option::is_none")]
495 pub limit: Option<u32>,
496}
497
498#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
500#[builder(default)]
501#[builder(setter(into, strip_option))]
502#[serde(rename_all = "camelCase")]
503pub struct GetOrderHistoryParams {
504 pub inst_type: OKXInstrumentType,
506 #[serde(skip_serializing_if = "Option::is_none")]
508 pub uly: Option<String>,
509 #[serde(skip_serializing_if = "Option::is_none")]
511 pub inst_family: Option<String>,
512 #[serde(skip_serializing_if = "Option::is_none")]
514 pub inst_id: Option<String>,
515 #[serde(skip_serializing_if = "Option::is_none")]
517 pub ord_type: Option<OKXOrderType>,
518 #[serde(skip_serializing_if = "Option::is_none")]
520 pub state: Option<String>,
521 #[serde(skip_serializing_if = "Option::is_none")]
523 pub after: Option<String>,
524 #[serde(skip_serializing_if = "Option::is_none")]
526 pub before: Option<String>,
527 #[serde(skip_serializing_if = "Option::is_none")]
529 pub limit: Option<u32>,
530}
531
532#[derive(Clone, Debug, Default, Deserialize, Serialize, Builder)]
534#[builder(default)]
535#[builder(setter(into, strip_option))]
536#[serde(rename_all = "camelCase")]
537pub struct GetOrderListParams {
538 #[serde(skip_serializing_if = "Option::is_none")]
540 pub inst_type: Option<OKXInstrumentType>,
541 #[serde(skip_serializing_if = "Option::is_none")]
543 pub inst_id: Option<String>,
544 #[serde(skip_serializing_if = "Option::is_none")]
546 pub inst_family: Option<String>,
547 #[serde(skip_serializing_if = "Option::is_none")]
549 pub state: Option<OKXOrderStatus>,
550 #[serde(skip_serializing_if = "Option::is_none")]
552 pub after: Option<String>,
553 #[serde(skip_serializing_if = "Option::is_none")]
555 pub before: Option<String>,
556 #[serde(skip_serializing_if = "Option::is_none")]
558 pub limit: Option<u32>,
559}
560
561#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
563#[builder(default)]
564#[builder(setter(into, strip_option))]
565#[serde(rename_all = "camelCase")]
566pub struct GetAlgoOrdersParams {
567 #[serde(rename = "algoId", skip_serializing_if = "Option::is_none")]
569 pub algo_id: Option<String>,
570 #[serde(rename = "algoClOrdId", skip_serializing_if = "Option::is_none")]
572 pub algo_cl_ord_id: Option<String>,
573 pub inst_type: OKXInstrumentType,
575 #[serde(rename = "instId", skip_serializing_if = "Option::is_none")]
577 pub inst_id: Option<String>,
578 #[serde(rename = "ordType", skip_serializing_if = "Option::is_none")]
580 pub ord_type: Option<OKXAlgoOrderType>,
581 #[serde(skip_serializing_if = "Option::is_none")]
583 pub state: Option<OKXOrderStatus>,
584 #[serde(skip_serializing_if = "Option::is_none")]
586 pub after: Option<String>,
587 #[serde(skip_serializing_if = "Option::is_none")]
589 pub before: Option<String>,
590 #[serde(skip_serializing_if = "Option::is_none")]
592 pub limit: Option<u32>,
593}
594
595#[derive(Clone, Debug, Default, Deserialize, Serialize, Builder)]
597#[builder(default)]
598#[builder(setter(into, strip_option))]
599#[serde(rename_all = "camelCase")]
600pub struct GetTransactionDetailsParams {
601 #[serde(skip_serializing_if = "Option::is_none")]
603 pub inst_type: Option<OKXInstrumentType>,
604 #[serde(skip_serializing_if = "Option::is_none")]
606 pub inst_id: Option<String>,
607 #[serde(skip_serializing_if = "Option::is_none")]
609 pub ord_id: Option<String>,
610 #[serde(skip_serializing_if = "Option::is_none")]
612 pub after: Option<String>,
613 #[serde(skip_serializing_if = "Option::is_none")]
615 pub before: Option<String>,
616 #[serde(skip_serializing_if = "Option::is_none")]
618 pub limit: Option<u32>,
619}
620
621#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
623#[builder(default)]
624#[builder(setter(into, strip_option))]
625#[serde(rename_all = "camelCase")]
626pub struct GetPositionsParams {
627 #[serde(skip_serializing_if = "Option::is_none")]
629 pub inst_type: Option<OKXInstrumentType>,
630 #[serde(skip_serializing_if = "Option::is_none")]
632 pub inst_id: Option<String>,
633 #[serde(skip_serializing_if = "Option::is_none")]
635 pub pos_id: Option<String>,
636}
637
638#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
640#[builder(default)]
641#[builder(setter(into, strip_option))]
642#[serde(rename_all = "camelCase")]
643pub struct GetPositionsHistoryParams {
644 pub inst_type: OKXInstrumentType,
646 #[serde(skip_serializing_if = "Option::is_none")]
648 pub inst_id: Option<String>,
649 #[serde(skip_serializing_if = "Option::is_none")]
651 pub pos_id: Option<String>,
652 #[serde(skip_serializing_if = "Option::is_none")]
654 pub after: Option<String>,
655 #[serde(skip_serializing_if = "Option::is_none")]
657 pub before: Option<String>,
658 #[serde(skip_serializing_if = "Option::is_none")]
660 pub limit: Option<u32>,
661}
662
663#[derive(Clone, Debug, Default, Deserialize, Serialize, Builder)]
665#[builder(default)]
666#[builder(setter(into, strip_option))]
667#[serde(rename_all = "camelCase")]
668pub struct GetOrderParams {
669 pub inst_type: OKXInstrumentType,
671 pub inst_id: String,
673 #[serde(skip_serializing_if = "Option::is_none")]
675 pub ord_id: Option<String>,
676 #[serde(skip_serializing_if = "Option::is_none")]
678 pub cl_ord_id: Option<String>,
679 #[serde(skip_serializing_if = "Option::is_none")]
681 pub pos_side: Option<OKXPositionSide>,
682}
683
684#[derive(Clone, Debug, Deserialize, Serialize, Builder)]
686#[builder(setter(into, strip_option))]
687#[serde(rename_all = "camelCase")]
688pub struct GetTradeFeeParams {
689 pub inst_type: OKXInstrumentType,
691 #[serde(skip_serializing_if = "Option::is_none")]
693 pub uly: Option<String>,
694 #[serde(skip_serializing_if = "Option::is_none")]
696 pub inst_family: Option<String>,
697}
698
699#[cfg(test)]
700mod tests {
701 use rstest::rstest;
702
703 use super::*;
704
705 #[rstest]
706 fn test_optional_parameters_are_omitted_when_none() {
707 let mut builder = GetCandlesticksParamsBuilder::default();
708 builder.inst_id("BTC-USDT-SWAP");
709 builder.bar("1m");
710
711 let params = builder.build().unwrap();
712 let qs = serde_urlencoded::to_string(¶ms).unwrap();
713 assert_eq!(
714 qs, "instId=BTC-USDT-SWAP&bar=1m",
715 "unexpected optional parameters were serialized: {qs}",
716 );
717 }
718
719 #[rstest]
720 fn test_no_literal_none_strings_leak_into_query_string() {
721 let mut builder = GetCandlesticksParamsBuilder::default();
722 builder.inst_id("BTC-USDT-SWAP");
723 builder.bar("1m");
724
725 let params = builder.build().unwrap();
726 let qs = serde_urlencoded::to_string(¶ms).unwrap();
727 assert!(
728 !qs.contains("None"),
729 "found literal \"None\" in query string: {qs}",
730 );
731 assert!(
732 !qs.contains("after=") && !qs.contains("before=") && !qs.contains("limit="),
733 "empty optional parameters must be omitted entirely: {qs}",
734 );
735 }
736
737 #[rstest]
738 fn test_cursor_nanoseconds_rejected() {
739 let after_nanos = 1_725_307_200_000_000_000i64;
741
742 let mut builder = GetCandlesticksParamsBuilder::default();
743 builder.inst_id("BTC-USDT-SWAP");
744 builder.bar("1m");
745 builder.after_ms(after_nanos);
746
747 let result = builder.build();
749 assert!(result.is_err());
750 assert!(result.unwrap_err().to_string().contains("nanoseconds"));
751 }
752
753 #[rstest]
754 fn test_both_cursors_rejected() {
755 let mut builder = GetCandlesticksParamsBuilder::default();
756 builder.inst_id("BTC-USDT-SWAP");
757 builder.bar("1m");
758 builder.after_ms(1725307200000);
761 builder.before_ms(1725393600000);
762
763 let result = builder.build();
764 assert!(result.is_err());
765 assert!(result.unwrap_err().to_string().contains("time range"));
766 }
767
768 #[rstest]
769 fn test_limit_exceeds_maximum_rejected() {
770 let mut builder = GetCandlesticksParamsBuilder::default();
771 builder.inst_id("BTC-USDT-SWAP");
772 builder.bar("1m");
773 builder.limit(301u32); let result = builder.build();
777 assert!(result.is_err());
778 assert!(result.unwrap_err().to_string().contains("300"));
779 }
780
781 #[rstest]
782 #[case(1725307200000, "after=1725307200000")] #[case(1725307200, "after=1725307200")] #[case(1725307, "after=1725307")] fn test_valid_millisecond_cursor_passes(#[case] timestamp: i64, #[case] expected: &str) {
786 let mut builder = GetCandlesticksParamsBuilder::default();
787 builder.inst_id("BTC-USDT-SWAP");
788 builder.bar("1m");
789 builder.after_ms(timestamp);
790
791 let params = builder.build().unwrap();
792 let qs = serde_urlencoded::to_string(¶ms).unwrap();
793 assert!(qs.contains(expected));
794 }
795
796 #[rstest]
797 #[case(1, "limit=1")]
798 #[case(50, "limit=50")]
799 #[case(100, "limit=100")]
800 #[case(300, "limit=300")] fn test_valid_limit_passes(#[case] limit: u32, #[case] expected: &str) {
802 let mut builder = GetCandlesticksParamsBuilder::default();
803 builder.inst_id("BTC-USDT-SWAP");
804 builder.bar("1m");
805 builder.limit(limit);
806
807 let params = builder.build().unwrap();
808 let qs = serde_urlencoded::to_string(¶ms).unwrap();
809 assert!(qs.contains(expected));
810 }
811}