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#[allow(dead_code)]
42fn serialize_string_vec<S>(values: &Option<Vec<String>>, serializer: S) -> Result<S::Ok, S::Error>
43where
44 S: serde::Serializer,
45{
46 match values {
47 Some(vec) => serializer.serialize_str(&vec.join(",")),
48 None => serializer.serialize_none(),
49 }
50}
51
52#[derive(Clone, Debug, Deserialize, Serialize, Builder)]
54#[builder(setter(into, strip_option))]
55#[serde(rename_all = "camelCase")]
56pub struct SetPositionModeParams {
57 #[serde(rename = "posMode")]
59 pub pos_mode: OKXPositionMode,
60}
61
62#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
64#[builder(default)]
65#[builder(setter(into, strip_option))]
66#[serde(rename_all = "camelCase")]
67pub struct GetPositionTiersParams {
68 pub inst_type: OKXInstrumentType,
70 pub td_mode: OKXTradeMode,
72 #[serde(skip_serializing_if = "Option::is_none")]
75 pub uly: Option<String>,
76 #[serde(skip_serializing_if = "Option::is_none")]
79 pub inst_family: Option<String>,
80 #[serde(skip_serializing_if = "Option::is_none")]
82 pub inst_id: Option<String>,
83 #[serde(skip_serializing_if = "Option::is_none")]
85 pub ccy: Option<String>,
86 #[serde(skip_serializing_if = "Option::is_none")]
88 pub tier: Option<String>,
89}
90
91#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
93#[builder(default)]
94#[builder(setter(into, strip_option))]
95#[serde(rename_all = "camelCase")]
96pub struct GetInstrumentsParams {
97 pub inst_type: OKXInstrumentType,
99 #[serde(skip_serializing_if = "Option::is_none")]
102 pub uly: Option<String>,
103 #[serde(skip_serializing_if = "Option::is_none")]
106 pub inst_family: Option<String>,
107 #[serde(skip_serializing_if = "Option::is_none")]
109 pub inst_id: Option<String>,
110}
111
112#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
114#[builder(default)]
115#[builder(setter(into, strip_option))]
116#[serde(rename_all = "camelCase")]
117pub struct GetTradesParams {
118 pub inst_id: String,
120 #[serde(rename = "type")]
122 #[serde(skip_serializing_if = "Option::is_none")]
123 pub pagination_type: Option<u8>,
124 #[serde(skip_serializing_if = "Option::is_none")]
126 pub after: Option<String>,
127 #[serde(skip_serializing_if = "Option::is_none")]
129 pub before: Option<String>,
130 #[serde(skip_serializing_if = "Option::is_none")]
132 pub limit: Option<u32>,
133}
134
135#[derive(Clone, Debug, Deserialize, Serialize)]
137#[serde(rename_all = "camelCase")]
138pub struct GetCandlesticksParams {
139 pub inst_id: String,
141 pub bar: String,
143 #[serde(skip_serializing_if = "Option::is_none")]
145 #[serde(rename = "after")]
146 pub after_ms: Option<i64>,
147 #[serde(skip_serializing_if = "Option::is_none")]
149 #[serde(rename = "before")]
150 pub before_ms: Option<i64>,
151 #[serde(skip_serializing_if = "Option::is_none")]
153 pub limit: Option<u32>,
154}
155
156#[derive(Debug, Default)]
158pub struct GetCandlesticksParamsBuilder {
159 inst_id: Option<String>,
160 bar: Option<String>,
161 after_ms: Option<i64>,
162 before_ms: Option<i64>,
163 limit: Option<u32>,
164}
165
166impl GetCandlesticksParamsBuilder {
167 pub fn inst_id(&mut self, inst_id: impl Into<String>) -> &mut Self {
169 self.inst_id = Some(inst_id.into());
170 self
171 }
172
173 pub fn bar(&mut self, bar: impl Into<String>) -> &mut Self {
175 self.bar = Some(bar.into());
176 self
177 }
178
179 pub fn after_ms(&mut self, after_ms: i64) -> &mut Self {
181 self.after_ms = Some(after_ms);
182 self
183 }
184
185 pub fn before_ms(&mut self, before_ms: i64) -> &mut Self {
187 self.before_ms = Some(before_ms);
188 self
189 }
190
191 pub fn limit(&mut self, limit: u32) -> &mut Self {
193 self.limit = Some(limit);
194 self
195 }
196
197 pub fn build(&mut self) -> Result<GetCandlesticksParams, BuildError> {
203 let inst_id = self.inst_id.clone().ok_or(BuildError::MissingInstId)?;
205 let bar = self.bar.clone().ok_or(BuildError::MissingBar)?;
206 let after_ms = self.after_ms;
207 let before_ms = self.before_ms;
208 let limit = self.limit;
209
210 if let (Some(after), Some(before)) = (after_ms, before_ms)
224 && before >= after
225 {
226 return Err(BuildError::InvalidTimeRange {
227 after_ms: after,
228 before_ms: before,
229 });
230 }
231
232 if let Some(nanos) = after_ms
234 && nanos.abs() > 9_999_999_999_999
235 {
236 return Err(BuildError::CursorIsNanoseconds);
237 }
238
239 if let Some(nanos) = before_ms
240 && nanos.abs() > 9_999_999_999_999
241 {
242 return Err(BuildError::CursorIsNanoseconds);
243 }
244
245 if let Some(limit) = limit
249 && limit > 300
250 {
251 return Err(BuildError::LimitTooHigh);
252 }
253
254 Ok(GetCandlesticksParams {
255 inst_id,
256 bar,
257 after_ms,
258 before_ms,
259 limit,
260 })
261 }
262}
263
264#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
266#[builder(default)]
267#[builder(setter(into, strip_option))]
268#[serde(rename_all = "camelCase")]
269pub struct GetMarkPriceParams {
270 pub inst_type: OKXInstrumentType,
272 #[serde(skip_serializing_if = "Option::is_none")]
275 pub uly: Option<String>,
276 #[serde(skip_serializing_if = "Option::is_none")]
279 pub inst_family: Option<String>,
280 #[serde(skip_serializing_if = "Option::is_none")]
282 pub inst_id: Option<String>,
283}
284
285#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
287#[builder(default)]
288#[builder(setter(into, strip_option))]
289#[serde(rename_all = "camelCase")]
290pub struct GetIndexTickerParams {
291 #[serde(skip_serializing_if = "Option::is_none")]
293 pub inst_id: Option<String>,
294 #[serde(skip_serializing_if = "Option::is_none")]
296 pub quote_ccy: Option<String>,
297}
298
299#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
301#[builder(default)]
302#[builder(setter(into, strip_option))]
303#[serde(rename_all = "camelCase")]
304pub struct GetOrderHistoryParams {
305 pub inst_type: OKXInstrumentType,
307 #[serde(skip_serializing_if = "Option::is_none")]
309 pub uly: Option<String>,
310 #[serde(skip_serializing_if = "Option::is_none")]
312 pub inst_family: Option<String>,
313 #[serde(skip_serializing_if = "Option::is_none")]
315 pub inst_id: Option<String>,
316 #[serde(skip_serializing_if = "Option::is_none")]
318 pub ord_type: Option<OKXOrderType>,
319 #[serde(skip_serializing_if = "Option::is_none")]
321 pub state: Option<String>,
322 #[serde(skip_serializing_if = "Option::is_none")]
324 pub after: Option<String>,
325 #[serde(skip_serializing_if = "Option::is_none")]
327 pub before: Option<String>,
328 #[serde(skip_serializing_if = "Option::is_none")]
330 pub limit: Option<u32>,
331}
332
333#[derive(Clone, Debug, Default, Deserialize, Serialize, Builder)]
335#[builder(default)]
336#[builder(setter(into, strip_option))]
337#[serde(rename_all = "camelCase")]
338pub struct GetOrderListParams {
339 #[serde(skip_serializing_if = "Option::is_none")]
341 pub inst_type: Option<OKXInstrumentType>,
342 #[serde(skip_serializing_if = "Option::is_none")]
344 pub inst_id: Option<String>,
345 #[serde(skip_serializing_if = "Option::is_none")]
347 pub inst_family: Option<String>,
348 #[serde(skip_serializing_if = "Option::is_none")]
350 pub state: Option<OKXOrderStatus>,
351 #[serde(skip_serializing_if = "Option::is_none")]
353 pub after: Option<String>,
354 #[serde(skip_serializing_if = "Option::is_none")]
356 pub before: Option<String>,
357 #[serde(skip_serializing_if = "Option::is_none")]
359 pub limit: Option<u32>,
360}
361
362#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
364#[builder(default)]
365#[builder(setter(into, strip_option))]
366#[serde(rename_all = "camelCase")]
367pub struct GetAlgoOrdersParams {
368 #[serde(rename = "algoId", skip_serializing_if = "Option::is_none")]
370 pub algo_id: Option<String>,
371 #[serde(rename = "algoClOrdId", skip_serializing_if = "Option::is_none")]
373 pub algo_cl_ord_id: Option<String>,
374 pub inst_type: OKXInstrumentType,
376 #[serde(rename = "instId", skip_serializing_if = "Option::is_none")]
378 pub inst_id: Option<String>,
379 #[serde(rename = "ordType", skip_serializing_if = "Option::is_none")]
381 pub ord_type: Option<OKXOrderType>,
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, Default, Deserialize, Serialize, Builder)]
398#[builder(default)]
399#[builder(setter(into, strip_option))]
400#[serde(rename_all = "camelCase")]
401pub struct GetTransactionDetailsParams {
402 #[serde(skip_serializing_if = "Option::is_none")]
404 pub inst_type: Option<OKXInstrumentType>,
405 #[serde(skip_serializing_if = "Option::is_none")]
407 pub inst_id: Option<String>,
408 #[serde(skip_serializing_if = "Option::is_none")]
410 pub ord_id: Option<String>,
411 #[serde(skip_serializing_if = "Option::is_none")]
413 pub after: Option<String>,
414 #[serde(skip_serializing_if = "Option::is_none")]
416 pub before: Option<String>,
417 #[serde(skip_serializing_if = "Option::is_none")]
419 pub limit: Option<u32>,
420}
421
422#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
424#[builder(default)]
425#[builder(setter(into, strip_option))]
426#[serde(rename_all = "camelCase")]
427pub struct GetPositionsParams {
428 #[serde(skip_serializing_if = "Option::is_none")]
430 pub inst_type: Option<OKXInstrumentType>,
431 #[serde(skip_serializing_if = "Option::is_none")]
433 pub inst_id: Option<String>,
434 #[serde(skip_serializing_if = "Option::is_none")]
436 pub pos_id: Option<String>,
437}
438
439#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
441#[builder(default)]
442#[builder(setter(into, strip_option))]
443#[serde(rename_all = "camelCase")]
444pub struct GetPositionsHistoryParams {
445 pub inst_type: OKXInstrumentType,
447 #[serde(skip_serializing_if = "Option::is_none")]
449 pub inst_id: Option<String>,
450 #[serde(skip_serializing_if = "Option::is_none")]
452 pub pos_id: Option<String>,
453 #[serde(skip_serializing_if = "Option::is_none")]
455 pub after: Option<String>,
456 #[serde(skip_serializing_if = "Option::is_none")]
458 pub before: Option<String>,
459 #[serde(skip_serializing_if = "Option::is_none")]
461 pub limit: Option<u32>,
462}
463
464#[derive(Clone, Debug, Default, Deserialize, Serialize, Builder)]
466#[builder(default)]
467#[builder(setter(into, strip_option))]
468#[serde(rename_all = "camelCase")]
469pub struct GetOrderParams {
470 pub inst_type: OKXInstrumentType,
472 pub inst_id: String,
474 #[serde(skip_serializing_if = "Option::is_none")]
476 pub ord_id: Option<String>,
477 #[serde(skip_serializing_if = "Option::is_none")]
479 pub cl_ord_id: Option<String>,
480 #[serde(skip_serializing_if = "Option::is_none")]
482 pub pos_side: Option<OKXPositionSide>,
483}
484
485#[derive(Clone, Debug, Deserialize, Serialize, Builder)]
487#[builder(setter(into, strip_option))]
488#[serde(rename_all = "camelCase")]
489pub struct GetTradeFeeParams {
490 pub inst_type: OKXInstrumentType,
492 #[serde(skip_serializing_if = "Option::is_none")]
494 pub uly: Option<String>,
495 #[serde(skip_serializing_if = "Option::is_none")]
497 pub inst_family: Option<String>,
498}
499
500#[cfg(test)]
505mod tests {
506 use rstest::rstest;
507
508 use super::*;
509
510 #[rstest]
511 fn test_optional_parameters_are_omitted_when_none() {
512 let mut builder = GetCandlesticksParamsBuilder::default();
513 builder.inst_id("BTC-USDT-SWAP");
514 builder.bar("1m");
515
516 let params = builder.build().unwrap();
517 let qs = serde_urlencoded::to_string(¶ms).unwrap();
518 assert_eq!(
519 qs, "instId=BTC-USDT-SWAP&bar=1m",
520 "unexpected optional parameters were serialized: {qs}",
521 );
522 }
523
524 #[rstest]
525 fn test_no_literal_none_strings_leak_into_query_string() {
526 let mut builder = GetCandlesticksParamsBuilder::default();
527 builder.inst_id("BTC-USDT-SWAP");
528 builder.bar("1m");
529
530 let params = builder.build().unwrap();
531 let qs = serde_urlencoded::to_string(¶ms).unwrap();
532 assert!(
533 !qs.contains("None"),
534 "found literal \"None\" in query string: {qs}",
535 );
536 assert!(
537 !qs.contains("after=") && !qs.contains("before=") && !qs.contains("limit="),
538 "empty optional parameters must be omitted entirely: {qs}",
539 );
540 }
541
542 #[rstest]
543 fn test_cursor_nanoseconds_rejected() {
544 let after_nanos = 1_725_307_200_000_000_000i64;
546
547 let mut builder = GetCandlesticksParamsBuilder::default();
548 builder.inst_id("BTC-USDT-SWAP");
549 builder.bar("1m");
550 builder.after_ms(after_nanos);
551
552 let result = builder.build();
554 assert!(result.is_err());
555 assert!(result.unwrap_err().to_string().contains("nanoseconds"));
556 }
557
558 #[rstest]
559 fn test_both_cursors_rejected() {
560 let mut builder = GetCandlesticksParamsBuilder::default();
561 builder.inst_id("BTC-USDT-SWAP");
562 builder.bar("1m");
563 builder.after_ms(1725307200000);
566 builder.before_ms(1725393600000);
567
568 let result = builder.build();
569 assert!(result.is_err());
570 assert!(result.unwrap_err().to_string().contains("time range"));
571 }
572
573 #[rstest]
574 fn test_limit_exceeds_maximum_rejected() {
575 let mut builder = GetCandlesticksParamsBuilder::default();
576 builder.inst_id("BTC-USDT-SWAP");
577 builder.bar("1m");
578 builder.limit(301u32); let result = builder.build();
582 assert!(result.is_err());
583 assert!(result.unwrap_err().to_string().contains("300"));
584 }
585
586 #[rstest]
587 #[case(1725307200000, "after=1725307200000")] #[case(1725307200, "after=1725307200")] #[case(1725307, "after=1725307")] fn test_valid_millisecond_cursor_passes(#[case] timestamp: i64, #[case] expected: &str) {
591 let mut builder = GetCandlesticksParamsBuilder::default();
592 builder.inst_id("BTC-USDT-SWAP");
593 builder.bar("1m");
594 builder.after_ms(timestamp);
595
596 let params = builder.build().unwrap();
597 let qs = serde_urlencoded::to_string(¶ms).unwrap();
598 assert!(qs.contains(expected));
599 }
600
601 #[rstest]
602 #[case(1, "limit=1")]
603 #[case(50, "limit=50")]
604 #[case(100, "limit=100")]
605 #[case(300, "limit=300")] fn test_valid_limit_passes(#[case] limit: u32, #[case] expected: &str) {
607 let mut builder = GetCandlesticksParamsBuilder::default();
608 builder.inst_id("BTC-USDT-SWAP");
609 builder.bar("1m");
610 builder.limit(limit);
611
612 let params = builder.build().unwrap();
613 let qs = serde_urlencoded::to_string(¶ms).unwrap();
614 assert!(qs.contains(expected));
615 }
616}