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)]
501mod tests {
502 use rstest::rstest;
503
504 use super::*;
505
506 #[rstest]
507 fn test_optional_parameters_are_omitted_when_none() {
508 let mut builder = GetCandlesticksParamsBuilder::default();
509 builder.inst_id("BTC-USDT-SWAP");
510 builder.bar("1m");
511
512 let params = builder.build().unwrap();
513 let qs = serde_urlencoded::to_string(¶ms).unwrap();
514 assert_eq!(
515 qs, "instId=BTC-USDT-SWAP&bar=1m",
516 "unexpected optional parameters were serialized: {qs}",
517 );
518 }
519
520 #[rstest]
521 fn test_no_literal_none_strings_leak_into_query_string() {
522 let mut builder = GetCandlesticksParamsBuilder::default();
523 builder.inst_id("BTC-USDT-SWAP");
524 builder.bar("1m");
525
526 let params = builder.build().unwrap();
527 let qs = serde_urlencoded::to_string(¶ms).unwrap();
528 assert!(
529 !qs.contains("None"),
530 "found literal \"None\" in query string: {qs}",
531 );
532 assert!(
533 !qs.contains("after=") && !qs.contains("before=") && !qs.contains("limit="),
534 "empty optional parameters must be omitted entirely: {qs}",
535 );
536 }
537
538 #[rstest]
539 fn test_cursor_nanoseconds_rejected() {
540 let after_nanos = 1_725_307_200_000_000_000i64;
542
543 let mut builder = GetCandlesticksParamsBuilder::default();
544 builder.inst_id("BTC-USDT-SWAP");
545 builder.bar("1m");
546 builder.after_ms(after_nanos);
547
548 let result = builder.build();
550 assert!(result.is_err());
551 assert!(result.unwrap_err().to_string().contains("nanoseconds"));
552 }
553
554 #[rstest]
555 fn test_both_cursors_rejected() {
556 let mut builder = GetCandlesticksParamsBuilder::default();
557 builder.inst_id("BTC-USDT-SWAP");
558 builder.bar("1m");
559 builder.after_ms(1725307200000);
562 builder.before_ms(1725393600000);
563
564 let result = builder.build();
565 assert!(result.is_err());
566 assert!(result.unwrap_err().to_string().contains("time range"));
567 }
568
569 #[rstest]
570 fn test_limit_exceeds_maximum_rejected() {
571 let mut builder = GetCandlesticksParamsBuilder::default();
572 builder.inst_id("BTC-USDT-SWAP");
573 builder.bar("1m");
574 builder.limit(301u32); let result = builder.build();
578 assert!(result.is_err());
579 assert!(result.unwrap_err().to_string().contains("300"));
580 }
581
582 #[rstest]
583 #[case(1725307200000, "after=1725307200000")] #[case(1725307200, "after=1725307200")] #[case(1725307, "after=1725307")] fn test_valid_millisecond_cursor_passes(#[case] timestamp: i64, #[case] expected: &str) {
587 let mut builder = GetCandlesticksParamsBuilder::default();
588 builder.inst_id("BTC-USDT-SWAP");
589 builder.bar("1m");
590 builder.after_ms(timestamp);
591
592 let params = builder.build().unwrap();
593 let qs = serde_urlencoded::to_string(¶ms).unwrap();
594 assert!(qs.contains(expected));
595 }
596
597 #[rstest]
598 #[case(1, "limit=1")]
599 #[case(50, "limit=50")]
600 #[case(100, "limit=100")]
601 #[case(300, "limit=300")] fn test_valid_limit_passes(#[case] limit: u32, #[case] expected: &str) {
603 let mut builder = GetCandlesticksParamsBuilder::default();
604 builder.inst_id("BTC-USDT-SWAP");
605 builder.bar("1m");
606 builder.limit(limit);
607
608 let params = builder.build().unwrap();
609 let qs = serde_urlencoded::to_string(¶ms).unwrap();
610 assert!(qs.contains(expected));
611 }
612}