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}
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 GetOptionSummaryParams {
107 pub inst_family: String,
109 #[serde(skip_serializing_if = "Option::is_none")]
111 pub exp_time: Option<String>,
112}
113
114#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
116#[builder(default)]
117#[builder(setter(into, strip_option))]
118#[serde(rename_all = "camelCase")]
119pub struct GetTradesParams {
120 pub inst_id: String,
122 #[serde(rename = "type")]
124 #[serde(skip_serializing_if = "Option::is_none")]
125 pub pagination_type: Option<u8>,
126 #[serde(skip_serializing_if = "Option::is_none")]
128 pub after: Option<String>,
129 #[serde(skip_serializing_if = "Option::is_none")]
131 pub before: Option<String>,
132 #[serde(skip_serializing_if = "Option::is_none")]
134 pub limit: Option<u32>,
135}
136
137#[derive(Clone, Debug, Deserialize, Serialize)]
139#[serde(rename_all = "camelCase")]
140pub struct GetCandlesticksParams {
141 pub inst_id: String,
143 pub bar: String,
145 #[serde(skip_serializing_if = "Option::is_none")]
147 #[serde(rename = "after")]
148 pub after_ms: Option<i64>,
149 #[serde(skip_serializing_if = "Option::is_none")]
151 #[serde(rename = "before")]
152 pub before_ms: Option<i64>,
153 #[serde(skip_serializing_if = "Option::is_none")]
155 pub limit: Option<u32>,
156}
157
158#[derive(Debug, Default)]
160pub struct GetCandlesticksParamsBuilder {
161 inst_id: Option<String>,
162 bar: Option<String>,
163 after_ms: Option<i64>,
164 before_ms: Option<i64>,
165 limit: Option<u32>,
166}
167
168impl GetCandlesticksParamsBuilder {
169 pub fn inst_id(&mut self, inst_id: impl Into<String>) -> &mut Self {
171 self.inst_id = Some(inst_id.into());
172 self
173 }
174
175 pub fn bar(&mut self, bar: impl Into<String>) -> &mut Self {
177 self.bar = Some(bar.into());
178 self
179 }
180
181 pub fn after_ms(&mut self, after_ms: i64) -> &mut Self {
183 self.after_ms = Some(after_ms);
184 self
185 }
186
187 pub fn before_ms(&mut self, before_ms: i64) -> &mut Self {
189 self.before_ms = Some(before_ms);
190 self
191 }
192
193 pub fn limit(&mut self, limit: u32) -> &mut Self {
195 self.limit = Some(limit);
196 self
197 }
198
199 pub fn build(&mut self) -> Result<GetCandlesticksParams, BuildError> {
205 let inst_id = self.inst_id.clone().ok_or(BuildError::MissingInstId)?;
207 let bar = self.bar.clone().ok_or(BuildError::MissingBar)?;
208 let after_ms = self.after_ms;
209 let before_ms = self.before_ms;
210 let limit = self.limit;
211
212 if let (Some(after), Some(before)) = (after_ms, before_ms)
226 && before >= after
227 {
228 return Err(BuildError::InvalidTimeRange {
229 after_ms: after,
230 before_ms: before,
231 });
232 }
233
234 if let Some(nanos) = after_ms
236 && nanos.abs() > 9_999_999_999_999
237 {
238 return Err(BuildError::CursorIsNanoseconds);
239 }
240
241 if let Some(nanos) = before_ms
242 && nanos.abs() > 9_999_999_999_999
243 {
244 return Err(BuildError::CursorIsNanoseconds);
245 }
246
247 if let Some(limit) = limit
251 && limit > 300
252 {
253 return Err(BuildError::LimitTooHigh);
254 }
255
256 Ok(GetCandlesticksParams {
257 inst_id,
258 bar,
259 after_ms,
260 before_ms,
261 limit,
262 })
263 }
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 GetMarkPriceParams {
272 pub inst_type: OKXInstrumentType,
274 #[serde(skip_serializing_if = "Option::is_none")]
277 pub uly: Option<String>,
278 #[serde(skip_serializing_if = "Option::is_none")]
281 pub inst_family: Option<String>,
282 #[serde(skip_serializing_if = "Option::is_none")]
284 pub inst_id: Option<String>,
285}
286
287#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
289#[builder(default)]
290#[builder(setter(into, strip_option))]
291#[serde(rename_all = "camelCase")]
292pub struct GetIndexTickerParams {
293 #[serde(skip_serializing_if = "Option::is_none")]
295 pub inst_id: Option<String>,
296 #[serde(skip_serializing_if = "Option::is_none")]
298 pub quote_ccy: Option<String>,
299}
300
301#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
303#[builder(default)]
304#[builder(setter(into, strip_option))]
305#[serde(rename_all = "camelCase")]
306pub struct GetOrderBookParams {
307 pub inst_id: String,
309 #[serde(skip_serializing_if = "Option::is_none")]
311 pub sz: Option<u32>,
312}
313
314#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
316#[builder(default)]
317#[builder(setter(into, strip_option))]
318#[serde(rename_all = "camelCase")]
319pub struct GetFundingRateHistoryParams {
320 pub inst_id: String,
322 #[serde(skip_serializing_if = "Option::is_none")]
324 pub before: Option<String>,
325 #[serde(skip_serializing_if = "Option::is_none")]
327 pub after: Option<String>,
328 #[serde(skip_serializing_if = "Option::is_none")]
330 pub limit: Option<u32>,
331}
332
333#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
335#[builder(default)]
336#[builder(setter(into, strip_option))]
337#[serde(rename_all = "camelCase")]
338pub struct GetOrderHistoryParams {
339 pub inst_type: OKXInstrumentType,
341 #[serde(skip_serializing_if = "Option::is_none")]
343 pub uly: Option<String>,
344 #[serde(skip_serializing_if = "Option::is_none")]
346 pub inst_family: Option<String>,
347 #[serde(skip_serializing_if = "Option::is_none")]
349 pub inst_id: Option<String>,
350 #[serde(skip_serializing_if = "Option::is_none")]
352 pub ord_type: Option<OKXOrderType>,
353 #[serde(skip_serializing_if = "Option::is_none")]
355 pub state: Option<String>,
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, Default, Deserialize, Serialize, Builder)]
369#[builder(default)]
370#[builder(setter(into, strip_option))]
371#[serde(rename_all = "camelCase")]
372pub struct GetOrderListParams {
373 #[serde(skip_serializing_if = "Option::is_none")]
375 pub inst_type: Option<OKXInstrumentType>,
376 #[serde(skip_serializing_if = "Option::is_none")]
378 pub inst_id: Option<String>,
379 #[serde(skip_serializing_if = "Option::is_none")]
381 pub inst_family: Option<String>,
382 #[serde(skip_serializing_if = "Option::is_none")]
384 pub state: Option<OKXOrderStatus>,
385 #[serde(skip_serializing_if = "Option::is_none")]
387 pub after: Option<String>,
388 #[serde(skip_serializing_if = "Option::is_none")]
390 pub before: Option<String>,
391 #[serde(skip_serializing_if = "Option::is_none")]
393 pub limit: Option<u32>,
394}
395
396#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
398#[builder(default)]
399#[builder(setter(into, strip_option))]
400#[serde(rename_all = "camelCase")]
401pub struct GetAlgoOrdersParams {
402 #[serde(rename = "algoId", skip_serializing_if = "Option::is_none")]
404 pub algo_id: Option<String>,
405 #[serde(rename = "algoClOrdId", skip_serializing_if = "Option::is_none")]
407 pub algo_cl_ord_id: Option<String>,
408 pub inst_type: OKXInstrumentType,
410 #[serde(rename = "instId", skip_serializing_if = "Option::is_none")]
412 pub inst_id: Option<String>,
413 #[serde(rename = "ordType", skip_serializing_if = "Option::is_none")]
415 pub ord_type: Option<OKXAlgoOrderType>,
416 #[serde(skip_serializing_if = "Option::is_none")]
418 pub state: Option<OKXOrderStatus>,
419 #[serde(skip_serializing_if = "Option::is_none")]
421 pub after: Option<String>,
422 #[serde(skip_serializing_if = "Option::is_none")]
424 pub before: Option<String>,
425 #[serde(skip_serializing_if = "Option::is_none")]
427 pub limit: Option<u32>,
428}
429
430#[derive(Clone, Debug, Default, Deserialize, Serialize, Builder)]
432#[builder(default)]
433#[builder(setter(into, strip_option))]
434#[serde(rename_all = "camelCase")]
435pub struct GetTransactionDetailsParams {
436 #[serde(skip_serializing_if = "Option::is_none")]
438 pub inst_type: Option<OKXInstrumentType>,
439 #[serde(skip_serializing_if = "Option::is_none")]
441 pub inst_id: Option<String>,
442 #[serde(skip_serializing_if = "Option::is_none")]
444 pub ord_id: Option<String>,
445 #[serde(skip_serializing_if = "Option::is_none")]
447 pub after: Option<String>,
448 #[serde(skip_serializing_if = "Option::is_none")]
450 pub before: Option<String>,
451 #[serde(skip_serializing_if = "Option::is_none")]
453 pub limit: Option<u32>,
454}
455
456#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
458#[builder(default)]
459#[builder(setter(into, strip_option))]
460#[serde(rename_all = "camelCase")]
461pub struct GetPositionsParams {
462 #[serde(skip_serializing_if = "Option::is_none")]
464 pub inst_type: Option<OKXInstrumentType>,
465 #[serde(skip_serializing_if = "Option::is_none")]
467 pub inst_id: Option<String>,
468 #[serde(skip_serializing_if = "Option::is_none")]
470 pub pos_id: Option<String>,
471}
472
473#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
475#[builder(default)]
476#[builder(setter(into, strip_option))]
477#[serde(rename_all = "camelCase")]
478pub struct GetPositionsHistoryParams {
479 pub inst_type: OKXInstrumentType,
481 #[serde(skip_serializing_if = "Option::is_none")]
483 pub inst_id: Option<String>,
484 #[serde(skip_serializing_if = "Option::is_none")]
486 pub pos_id: Option<String>,
487 #[serde(skip_serializing_if = "Option::is_none")]
489 pub after: Option<String>,
490 #[serde(skip_serializing_if = "Option::is_none")]
492 pub before: Option<String>,
493 #[serde(skip_serializing_if = "Option::is_none")]
495 pub limit: Option<u32>,
496}
497
498#[derive(Clone, Debug, Default, Deserialize, Serialize, Builder)]
500#[builder(default)]
501#[builder(setter(into, strip_option))]
502#[serde(rename_all = "camelCase")]
503pub struct GetOrderParams {
504 pub inst_type: OKXInstrumentType,
506 pub inst_id: String,
508 #[serde(skip_serializing_if = "Option::is_none")]
510 pub ord_id: Option<String>,
511 #[serde(skip_serializing_if = "Option::is_none")]
513 pub cl_ord_id: Option<String>,
514 #[serde(skip_serializing_if = "Option::is_none")]
516 pub pos_side: Option<OKXPositionSide>,
517}
518
519#[derive(Clone, Debug, Deserialize, Serialize, Builder)]
521#[builder(setter(into, strip_option))]
522#[serde(rename_all = "camelCase")]
523pub struct GetTradeFeeParams {
524 pub inst_type: OKXInstrumentType,
526 #[serde(skip_serializing_if = "Option::is_none")]
528 pub uly: Option<String>,
529 #[serde(skip_serializing_if = "Option::is_none")]
531 pub inst_family: Option<String>,
532}
533
534#[cfg(test)]
535mod tests {
536 use rstest::rstest;
537
538 use super::*;
539
540 #[rstest]
541 fn test_optional_parameters_are_omitted_when_none() {
542 let mut builder = GetCandlesticksParamsBuilder::default();
543 builder.inst_id("BTC-USDT-SWAP");
544 builder.bar("1m");
545
546 let params = builder.build().unwrap();
547 let qs = serde_urlencoded::to_string(¶ms).unwrap();
548 assert_eq!(
549 qs, "instId=BTC-USDT-SWAP&bar=1m",
550 "unexpected optional parameters were serialized: {qs}",
551 );
552 }
553
554 #[rstest]
555 fn test_no_literal_none_strings_leak_into_query_string() {
556 let mut builder = GetCandlesticksParamsBuilder::default();
557 builder.inst_id("BTC-USDT-SWAP");
558 builder.bar("1m");
559
560 let params = builder.build().unwrap();
561 let qs = serde_urlencoded::to_string(¶ms).unwrap();
562 assert!(
563 !qs.contains("None"),
564 "found literal \"None\" in query string: {qs}",
565 );
566 assert!(
567 !qs.contains("after=") && !qs.contains("before=") && !qs.contains("limit="),
568 "empty optional parameters must be omitted entirely: {qs}",
569 );
570 }
571
572 #[rstest]
573 fn test_cursor_nanoseconds_rejected() {
574 let after_nanos = 1_725_307_200_000_000_000i64;
576
577 let mut builder = GetCandlesticksParamsBuilder::default();
578 builder.inst_id("BTC-USDT-SWAP");
579 builder.bar("1m");
580 builder.after_ms(after_nanos);
581
582 let result = builder.build();
584 assert!(result.is_err());
585 assert!(result.unwrap_err().to_string().contains("nanoseconds"));
586 }
587
588 #[rstest]
589 fn test_both_cursors_rejected() {
590 let mut builder = GetCandlesticksParamsBuilder::default();
591 builder.inst_id("BTC-USDT-SWAP");
592 builder.bar("1m");
593 builder.after_ms(1725307200000);
596 builder.before_ms(1725393600000);
597
598 let result = builder.build();
599 assert!(result.is_err());
600 assert!(result.unwrap_err().to_string().contains("time range"));
601 }
602
603 #[rstest]
604 fn test_limit_exceeds_maximum_rejected() {
605 let mut builder = GetCandlesticksParamsBuilder::default();
606 builder.inst_id("BTC-USDT-SWAP");
607 builder.bar("1m");
608 builder.limit(301u32); let result = builder.build();
612 assert!(result.is_err());
613 assert!(result.unwrap_err().to_string().contains("300"));
614 }
615
616 #[rstest]
617 #[case(1725307200000, "after=1725307200000")] #[case(1725307200, "after=1725307200")] #[case(1725307, "after=1725307")] fn test_valid_millisecond_cursor_passes(#[case] timestamp: i64, #[case] expected: &str) {
621 let mut builder = GetCandlesticksParamsBuilder::default();
622 builder.inst_id("BTC-USDT-SWAP");
623 builder.bar("1m");
624 builder.after_ms(timestamp);
625
626 let params = builder.build().unwrap();
627 let qs = serde_urlencoded::to_string(¶ms).unwrap();
628 assert!(qs.contains(expected));
629 }
630
631 #[rstest]
632 #[case(1, "limit=1")]
633 #[case(50, "limit=50")]
634 #[case(100, "limit=100")]
635 #[case(300, "limit=300")] fn test_valid_limit_passes(#[case] limit: u32, #[case] expected: &str) {
637 let mut builder = GetCandlesticksParamsBuilder::default();
638 builder.inst_id("BTC-USDT-SWAP");
639 builder.bar("1m");
640 builder.limit(limit);
641
642 let params = builder.build().unwrap();
643 let qs = serde_urlencoded::to_string(¶ms).unwrap();
644 assert!(qs.contains(expected));
645 }
646}