Skip to main content

rust_okx/api/finance/requests/
flexible_loan.rs

1use serde::Serialize;
2
3use crate::model::{
4    RequestValidationError, ValidateRequest, non_empty, one_of, optional_non_empty,
5    optional_positive_decimal_string, optional_unsigned_integer_string, positive_decimal_string,
6    range_u64,
7};
8
9fn validate_order_id(ord_id: Option<&str>) -> Result<(), RequestValidationError> {
10    optional_non_empty("ordId", ord_id)
11}
12
13fn validate_history_page(
14    after: Option<&str>,
15    before: Option<&str>,
16    limit: Option<u32>,
17) -> Result<(), RequestValidationError> {
18    optional_unsigned_integer_string("after", after)?;
19    optional_unsigned_integer_string("before", before)?;
20    if let Some(limit) = limit {
21        range_u64("limit", u64::from(limit), 1, 100)?;
22    }
23    Ok(())
24}
25
26/// Query parameters for `GET /api/v5/finance/flexible-loan/collateral-assets`.
27#[derive(Debug, Clone, Default, Serialize)]
28pub struct FlexibleLoanCollateralAssetsRequest {
29    #[serde(skip_serializing_if = "Option::is_none")]
30    ccy: Option<String>,
31    #[serde(rename = "ordId", skip_serializing_if = "Option::is_none")]
32    ord_id: Option<String>,
33}
34
35impl FlexibleLoanCollateralAssetsRequest {
36    /// Create an unfiltered collateral-assets query.
37    pub fn new() -> Self {
38        Self::default()
39    }
40
41    /// Filter by collateral currency.
42    pub fn currency(mut self, value: impl Into<String>) -> Self {
43        self.ccy = Some(value.into());
44        self
45    }
46
47    /// Scope the query to one flexible-loan order.
48    pub fn order_id(mut self, value: impl Into<String>) -> Self {
49        self.ord_id = Some(value.into());
50        self
51    }
52}
53
54impl ValidateRequest for FlexibleLoanCollateralAssetsRequest {
55    fn validate(&self) -> Result<(), RequestValidationError> {
56        optional_non_empty("ccy", self.ccy.as_deref())?;
57        validate_order_id(self.ord_id.as_deref())
58    }
59}
60
61/// Request body for `POST /api/v5/finance/flexible-loan/max-loan`.
62#[derive(Debug, Clone, Serialize)]
63pub struct FlexibleLoanMaxLoanRequest {
64    #[serde(rename = "borrowCcy")]
65    borrow_ccy: String,
66    #[serde(rename = "collateralCcy", skip_serializing_if = "Option::is_none")]
67    collateral_ccy: Option<String>,
68    #[serde(rename = "collateralAmt", skip_serializing_if = "Option::is_none")]
69    collateral_amt: Option<String>,
70    #[serde(rename = "ordId", skip_serializing_if = "Option::is_none")]
71    ord_id: Option<String>,
72}
73
74impl FlexibleLoanMaxLoanRequest {
75    /// Create a maximum-loan estimate for a borrowing currency.
76    pub fn new(borrow_ccy: impl Into<String>) -> Self {
77        Self {
78            borrow_ccy: borrow_ccy.into(),
79            collateral_ccy: None,
80            collateral_amt: None,
81            ord_id: None,
82        }
83    }
84
85    /// Estimate using a collateral currency and amount.
86    pub fn collateral(mut self, ccy: impl Into<String>, amt: impl Into<String>) -> Self {
87        self.collateral_ccy = Some(ccy.into());
88        self.collateral_amt = Some(amt.into());
89        self
90    }
91
92    /// Estimate additional borrowing for one existing flexible-loan order.
93    pub fn order_id(mut self, value: impl Into<String>) -> Self {
94        self.ord_id = Some(value.into());
95        self
96    }
97}
98
99impl ValidateRequest for FlexibleLoanMaxLoanRequest {
100    fn validate(&self) -> Result<(), RequestValidationError> {
101        non_empty("borrowCcy", &self.borrow_ccy)?;
102        optional_non_empty("collateralCcy", self.collateral_ccy.as_deref())?;
103        optional_positive_decimal_string("collateralAmt", self.collateral_amt.as_deref())?;
104        validate_order_id(self.ord_id.as_deref())?;
105
106        if self.collateral_ccy.is_some() != self.collateral_amt.is_some() {
107            return Err(RequestValidationError::RequiredWhen {
108                field: if self.collateral_ccy.is_some() {
109                    "collateralAmt"
110                } else {
111                    "collateralCcy"
112                },
113                condition: "the other collateral field is present",
114            });
115        }
116        Ok(())
117    }
118}
119
120/// Query parameters for `GET /api/v5/finance/flexible-loan/max-collateral-redeem-amount`.
121#[derive(Debug, Clone, Default, Serialize)]
122pub struct FlexibleLoanMaxRedeemRequest {
123    #[serde(skip_serializing_if = "Option::is_none")]
124    ccy: Option<String>,
125    #[serde(rename = "ordId", skip_serializing_if = "Option::is_none")]
126    ord_id: Option<String>,
127}
128
129impl FlexibleLoanMaxRedeemRequest {
130    /// Create an unfiltered maximum-redeem query.
131    pub fn new() -> Self {
132        Self::default()
133    }
134
135    /// Filter by collateral currency.
136    pub fn currency(mut self, value: impl Into<String>) -> Self {
137        self.ccy = Some(value.into());
138        self
139    }
140
141    /// Scope the calculation to one flexible-loan order.
142    pub fn order_id(mut self, value: impl Into<String>) -> Self {
143        self.ord_id = Some(value.into());
144        self
145    }
146}
147
148impl ValidateRequest for FlexibleLoanMaxRedeemRequest {
149    fn validate(&self) -> Result<(), RequestValidationError> {
150        optional_non_empty("ccy", self.ccy.as_deref())?;
151        validate_order_id(self.ord_id.as_deref())
152    }
153}
154
155/// Request body for `POST /api/v5/finance/flexible-loan/adjust-collateral`.
156#[derive(Debug, Clone, Serialize)]
157pub struct FlexibleLoanAdjustCollateralRequest {
158    #[serde(rename = "ordId")]
159    ord_id: String,
160    #[serde(rename = "collateralCcy")]
161    collateral_ccy: String,
162    amt: String,
163    #[serde(rename = "type")]
164    adjustment_type: String,
165}
166
167impl FlexibleLoanAdjustCollateralRequest {
168    /// Create a collateral adjustment; `type` must be `add` or `reduce`.
169    pub fn new(
170        ord_id: impl Into<String>,
171        collateral_ccy: impl Into<String>,
172        amt: impl Into<String>,
173        adjustment_type: impl Into<String>,
174    ) -> Self {
175        Self {
176            ord_id: ord_id.into(),
177            collateral_ccy: collateral_ccy.into(),
178            amt: amt.into(),
179            adjustment_type: adjustment_type.into(),
180        }
181    }
182}
183
184impl ValidateRequest for FlexibleLoanAdjustCollateralRequest {
185    fn validate(&self) -> Result<(), RequestValidationError> {
186        non_empty("ordId", &self.ord_id)?;
187        non_empty("collateralCcy", &self.collateral_ccy)?;
188        positive_decimal_string("amt", &self.amt)?;
189        one_of(
190            "type",
191            &self.adjustment_type,
192            &["add", "reduce"],
193            "add or reduce",
194        )
195    }
196}
197
198/// Query parameters for `GET /api/v5/finance/flexible-loan/loan-info`.
199#[derive(Debug, Clone, Default, Serialize)]
200pub struct FlexibleLoanInfoRequest {
201    #[serde(rename = "ordId", skip_serializing_if = "Option::is_none")]
202    ord_id: Option<String>,
203}
204
205impl FlexibleLoanInfoRequest {
206    /// Create a query for all current flexible-loan orders.
207    pub fn new() -> Self {
208        Self::default()
209    }
210
211    /// Restrict the result to one flexible-loan order.
212    pub fn order_id(mut self, value: impl Into<String>) -> Self {
213        self.ord_id = Some(value.into());
214        self
215    }
216}
217
218impl ValidateRequest for FlexibleLoanInfoRequest {
219    fn validate(&self) -> Result<(), RequestValidationError> {
220        validate_order_id(self.ord_id.as_deref())
221    }
222}
223
224/// Query parameters for `GET /api/v5/finance/flexible-loan/loan-history`.
225#[derive(Debug, Clone, Default, Serialize)]
226pub struct FlexibleLoanHistoryRequest {
227    #[serde(rename = "ordId", skip_serializing_if = "Option::is_none")]
228    ord_id: Option<String>,
229    #[serde(skip_serializing_if = "Option::is_none")]
230    after: Option<String>,
231    #[serde(skip_serializing_if = "Option::is_none")]
232    before: Option<String>,
233    #[serde(skip_serializing_if = "Option::is_none")]
234    limit: Option<u32>,
235}
236
237impl FlexibleLoanHistoryRequest {
238    /// Create an unfiltered loan-history query.
239    pub fn new() -> Self {
240        Self::default()
241    }
242
243    /// Restrict the result to one flexible-loan order.
244    pub fn order_id(mut self, value: impl Into<String>) -> Self {
245        self.ord_id = Some(value.into());
246        self
247    }
248
249    /// Set the endpoint's `after` cursor.
250    pub fn after(mut self, value: impl Into<String>) -> Self {
251        self.after = Some(value.into());
252        self
253    }
254
255    /// Set the endpoint's `before` cursor.
256    pub fn before(mut self, value: impl Into<String>) -> Self {
257        self.before = Some(value.into());
258        self
259    }
260
261    /// Set the result count from 1 through 100.
262    pub fn limit(mut self, value: u32) -> Self {
263        self.limit = Some(value);
264        self
265    }
266}
267
268impl ValidateRequest for FlexibleLoanHistoryRequest {
269    fn validate(&self) -> Result<(), RequestValidationError> {
270        validate_order_id(self.ord_id.as_deref())?;
271        validate_history_page(self.after.as_deref(), self.before.as_deref(), self.limit)
272    }
273}
274
275/// Query parameters for `GET /api/v5/finance/flexible-loan/interest-accrued`.
276#[derive(Debug, Clone, Default, Serialize)]
277pub struct FlexibleLoanInterestAccruedRequest {
278    #[serde(rename = "ordId", skip_serializing_if = "Option::is_none")]
279    ord_id: Option<String>,
280    #[serde(skip_serializing_if = "Option::is_none")]
281    ccy: Option<String>,
282    #[serde(skip_serializing_if = "Option::is_none")]
283    after: Option<String>,
284    #[serde(skip_serializing_if = "Option::is_none")]
285    before: Option<String>,
286    #[serde(skip_serializing_if = "Option::is_none")]
287    limit: Option<u32>,
288}
289
290impl FlexibleLoanInterestAccruedRequest {
291    /// Create an unfiltered accrued-interest query.
292    pub fn new() -> Self {
293        Self::default()
294    }
295
296    /// Restrict the result to one flexible-loan order.
297    pub fn order_id(mut self, value: impl Into<String>) -> Self {
298        self.ord_id = Some(value.into());
299        self
300    }
301
302    /// Restrict the result to one currency.
303    pub fn currency(mut self, value: impl Into<String>) -> Self {
304        self.ccy = Some(value.into());
305        self
306    }
307
308    /// Set the endpoint's `after` cursor.
309    pub fn after(mut self, value: impl Into<String>) -> Self {
310        self.after = Some(value.into());
311        self
312    }
313
314    /// Set the endpoint's `before` cursor.
315    pub fn before(mut self, value: impl Into<String>) -> Self {
316        self.before = Some(value.into());
317        self
318    }
319
320    /// Set the result count from 1 through 100.
321    pub fn limit(mut self, value: u32) -> Self {
322        self.limit = Some(value);
323        self
324    }
325}
326
327impl ValidateRequest for FlexibleLoanInterestAccruedRequest {
328    fn validate(&self) -> Result<(), RequestValidationError> {
329        validate_order_id(self.ord_id.as_deref())?;
330        optional_non_empty("ccy", self.ccy.as_deref())?;
331        validate_history_page(self.after.as_deref(), self.before.as_deref(), self.limit)
332    }
333}
334
335#[cfg(test)]
336mod tests {
337    use super::*;
338
339    #[test]
340    fn max_loan_requires_complete_collateral_pair() {
341        let request = FlexibleLoanMaxLoanRequest::new("USDT");
342        request.validate().unwrap();
343
344        let mut value = serde_json::to_value(request).unwrap();
345        value["collateralCcy"] = serde_json::Value::String("BTC".into());
346        assert_eq!(value["borrowCcy"], "USDT");
347    }
348
349    #[test]
350    fn adjustment_rejects_unknown_type() {
351        let request = FlexibleLoanAdjustCollateralRequest::new("1", "BTC", "1", "withdraw");
352        assert!(request.validate().is_err());
353    }
354}