rust_okx/api/account/requests/
borrowing.rs1use serde::Serialize;
2
3use crate::model::{
4 RequestValidationError, TradeMode, ValidateRequest, collection_length, exactly_one, non_empty,
5 non_empty_items, one_of, optional_non_empty, optional_one_of, optional_unsigned_integer_string,
6 positive_decimal_string, range_u64, reject_when_present,
7};
8
9#[derive(Debug, Clone, Serialize)]
11pub struct MaxLoanRequest {
12 #[serde(rename = "instId", skip_serializing_if = "Option::is_none")]
13 inst_id: Option<String>,
14 #[serde(skip_serializing_if = "Option::is_none")]
15 ccy: Option<String>,
16 #[serde(rename = "mgnMode")]
17 mgn_mode: TradeMode,
18 #[serde(rename = "mgnCcy", skip_serializing_if = "Option::is_none")]
19 mgn_ccy: Option<String>,
20 #[serde(rename = "tradeQuoteCcy", skip_serializing_if = "Option::is_none")]
21 trade_quote_ccy: Option<String>,
22}
23
24impl MaxLoanRequest {
25 pub fn new(inst_id: impl Into<String>, mgn_mode: TradeMode) -> Self {
31 Self::by_instrument(inst_id, mgn_mode)
32 }
33
34 pub fn by_instrument(inst_id: impl Into<String>, mgn_mode: TradeMode) -> Self {
36 Self {
37 mgn_mode,
38 inst_id: Some(inst_id.into()),
39 ccy: None,
40 mgn_ccy: None,
41 trade_quote_ccy: None,
42 }
43 }
44
45 pub fn by_currency(ccy: impl Into<String>) -> Self {
47 Self {
48 mgn_mode: TradeMode::Cross,
49 inst_id: None,
50 ccy: Some(ccy.into()),
51 mgn_ccy: None,
52 trade_quote_ccy: None,
53 }
54 }
55
56 pub fn currency(mut self, ccy: impl Into<String>) -> Self {
58 self.inst_id = None;
59 self.ccy = Some(ccy.into());
60 self
61 }
62
63 pub fn margin_currency(mut self, mgn_ccy: impl Into<String>) -> Self {
65 self.mgn_ccy = Some(mgn_ccy.into());
66 self
67 }
68
69 pub fn trade_quote_currency(mut self, trade_quote_ccy: impl Into<String>) -> Self {
71 self.trade_quote_ccy = Some(trade_quote_ccy.into());
72 self
73 }
74}
75
76#[derive(Debug, Clone, Default, Serialize)]
78pub struct InterestAccruedRequest {
79 #[serde(rename = "instId", skip_serializing_if = "Option::is_none")]
80 inst_id: Option<String>,
81 #[serde(skip_serializing_if = "Option::is_none")]
82 ccy: Option<String>,
83 #[serde(rename = "mgnMode", skip_serializing_if = "Option::is_none")]
84 mgn_mode: Option<TradeMode>,
85 #[serde(skip_serializing_if = "Option::is_none")]
86 after: Option<String>,
87 #[serde(skip_serializing_if = "Option::is_none")]
88 before: Option<String>,
89 #[serde(skip_serializing_if = "Option::is_none")]
90 limit: Option<u32>,
91}
92
93impl InterestAccruedRequest {
94 pub fn new() -> Self {
96 Self::default()
97 }
98
99 pub fn inst_id(mut self, inst_id: impl Into<String>) -> Self {
101 self.inst_id = Some(inst_id.into());
102 self
103 }
104
105 pub fn currency(mut self, ccy: impl Into<String>) -> Self {
107 self.ccy = Some(ccy.into());
108 self
109 }
110
111 pub fn margin_mode(mut self, mgn_mode: TradeMode) -> Self {
113 self.mgn_mode = Some(mgn_mode);
114 self
115 }
116
117 pub fn after(mut self, after: impl Into<String>) -> Self {
119 self.after = Some(after.into());
120 self
121 }
122
123 pub fn before(mut self, before: impl Into<String>) -> Self {
125 self.before = Some(before.into());
126 self
127 }
128
129 pub fn limit(mut self, limit: u32) -> Self {
131 self.limit = Some(limit);
132 self
133 }
134}
135
136#[derive(Debug, Clone, Serialize)]
138pub struct BorrowRepayRequest {
139 ccy: String,
140 side: String,
141 amt: String,
142 #[serde(rename = "ordId", skip_serializing_if = "Option::is_none")]
143 ord_id: Option<String>,
144}
145
146impl BorrowRepayRequest {
147 pub fn new(ccy: impl Into<String>, side: impl Into<String>, amt: impl Into<String>) -> Self {
149 Self {
150 ccy: ccy.into(),
151 side: side.into(),
152 amt: amt.into(),
153 ord_id: None,
154 }
155 }
156
157 pub fn order_id(mut self, ord_id: impl Into<String>) -> Self {
159 self.ord_id = Some(ord_id.into());
160 self
161 }
162}
163
164#[derive(Debug, Clone, Default, Serialize)]
166pub struct BorrowRepayHistoryRequest {
167 #[serde(skip_serializing_if = "Option::is_none")]
168 ccy: Option<String>,
169 #[serde(skip_serializing_if = "Option::is_none")]
170 after: Option<String>,
171 #[serde(skip_serializing_if = "Option::is_none")]
172 before: Option<String>,
173 #[serde(skip_serializing_if = "Option::is_none")]
174 limit: Option<u32>,
175}
176
177impl BorrowRepayHistoryRequest {
178 pub fn new() -> Self {
180 Self::default()
181 }
182
183 pub fn currency(mut self, ccy: impl Into<String>) -> Self {
185 self.ccy = Some(ccy.into());
186 self
187 }
188
189 pub fn after(mut self, after: impl Into<String>) -> Self {
191 self.after = Some(after.into());
192 self
193 }
194
195 pub fn before(mut self, before: impl Into<String>) -> Self {
197 self.before = Some(before.into());
198 self
199 }
200
201 pub fn limit(mut self, limit: u32) -> Self {
203 self.limit = Some(limit);
204 self
205 }
206}
207
208#[derive(Debug, Clone, Default, Serialize)]
210pub struct InterestLimitsRequest {
211 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
212 limit_type: Option<String>,
213 #[serde(skip_serializing_if = "Option::is_none")]
214 ccy: Option<String>,
215}
216
217impl InterestLimitsRequest {
218 pub fn new() -> Self {
220 Self::default()
221 }
222
223 pub fn limit_type(mut self, limit_type: impl Into<String>) -> Self {
225 self.limit_type = Some(limit_type.into());
226 self
227 }
228
229 pub fn currency(mut self, ccy: impl Into<String>) -> Self {
231 self.ccy = Some(ccy.into());
232 self
233 }
234}
235
236fn validate_pagination(
237 after: Option<&str>,
238 before: Option<&str>,
239 limit: Option<u32>,
240) -> Result<(), RequestValidationError> {
241 optional_unsigned_integer_string("after", after)?;
242 optional_unsigned_integer_string("before", before)?;
243 if let Some(limit) = limit {
244 range_u64("limit", u64::from(limit), 1, 100)?;
245 }
246 Ok(())
247}
248
249impl ValidateRequest for MaxLoanRequest {
250 fn validate(&self) -> Result<(), RequestValidationError> {
251 match &self.mgn_mode {
252 TradeMode::Cross | TradeMode::Isolated => {}
253 _ => {
254 return Err(RequestValidationError::InvalidFormat {
255 field: "mgnMode",
256 expected: "cross or isolated",
257 });
258 }
259 }
260 optional_non_empty("instId", self.inst_id.as_deref())?;
261 optional_non_empty("ccy", self.ccy.as_deref())?;
262 exactly_one("instId, ccy", &[self.inst_id.is_some(), self.ccy.is_some()])?;
263
264 if let Some(inst_ids) = self.inst_id.as_deref() {
265 let instruments: Vec<_> = inst_ids.split(',').collect();
266 collection_length("instId", instruments.len(), 1, 5)?;
267 non_empty_items("instId", instruments)?;
268 }
269
270 optional_non_empty("mgnCcy", self.mgn_ccy.as_deref())?;
271 optional_non_empty("tradeQuoteCcy", self.trade_quote_ccy.as_deref())?;
272 if self.ccy.is_some() {
273 if !matches!(self.mgn_mode, TradeMode::Cross) {
274 return Err(RequestValidationError::InvalidFormat {
275 field: "mgnMode",
276 expected: "cross when ccy is used",
277 });
278 }
279 reject_when_present("mgnCcy", self.mgn_ccy.as_ref(), "ccy is used")?;
280 reject_when_present(
281 "tradeQuoteCcy",
282 self.trade_quote_ccy.as_ref(),
283 "ccy is used",
284 )?;
285 }
286 Ok(())
287 }
288}
289
290impl ValidateRequest for InterestAccruedRequest {
291 fn validate(&self) -> Result<(), RequestValidationError> {
292 optional_non_empty("instId", self.inst_id.as_deref())?;
293 optional_non_empty("ccy", self.ccy.as_deref())?;
294 if let Some(mode) = &self.mgn_mode {
295 match mode {
296 TradeMode::Cross | TradeMode::Isolated => {}
297 _ => {
298 return Err(RequestValidationError::InvalidFormat {
299 field: "mgnMode",
300 expected: "cross or isolated",
301 });
302 }
303 }
304 }
305 validate_pagination(self.after.as_deref(), self.before.as_deref(), self.limit)
306 }
307}
308
309impl ValidateRequest for BorrowRepayRequest {
310 fn validate(&self) -> Result<(), RequestValidationError> {
311 non_empty("ccy", &self.ccy)?;
312 one_of("side", &self.side, &["borrow", "repay"], "borrow or repay")?;
313 positive_decimal_string("amt", &self.amt)?;
314 optional_non_empty("ordId", self.ord_id.as_deref())
315 }
316}
317
318impl ValidateRequest for BorrowRepayHistoryRequest {
319 fn validate(&self) -> Result<(), RequestValidationError> {
320 optional_non_empty("ccy", self.ccy.as_deref())?;
321 validate_pagination(self.after.as_deref(), self.before.as_deref(), self.limit)
322 }
323}
324
325impl ValidateRequest for InterestLimitsRequest {
326 fn validate(&self) -> Result<(), RequestValidationError> {
327 optional_one_of(
328 "type",
329 self.limit_type.as_deref(),
330 &["1", "2"],
331 "1 (loan quota) or 2 (interest rate)",
332 )?;
333 optional_non_empty("ccy", self.ccy.as_deref())?;
334 Ok(())
335 }
336}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341
342 #[test]
343 fn max_loan_accepts_currency_selector() {
344 MaxLoanRequest::by_currency("USDT").validate().unwrap();
345 }
346
347 #[test]
348 fn max_loan_rejects_more_than_five_instruments() {
349 let request = MaxLoanRequest::new("A-B,B-C,C-D,D-E,E-F,F-G", TradeMode::Cross);
350 assert!(request.validate().is_err());
351 }
352}