1use serde::Serialize;
2
3use crate::model::{
4 InstType, PositionSide, RequestValidationError, TradeMode, ValidateRequest, at_least_one,
5 non_empty, one_of, optional_non_empty, optional_one_of, optional_positive_decimal_string,
6 positive_decimal_string, reject_when_present, require_when,
7};
8
9#[derive(Debug, Clone, Serialize)]
11pub struct SetLeverageRequest {
12 lever: String,
13 #[serde(rename = "mgnMode")]
14 mgn_mode: TradeMode,
15 #[serde(rename = "instId", skip_serializing_if = "Option::is_none")]
16 inst_id: Option<String>,
17 #[serde(skip_serializing_if = "Option::is_none")]
18 ccy: Option<String>,
19 #[serde(rename = "posSide", skip_serializing_if = "Option::is_none")]
20 pos_side: Option<PositionSide>,
21}
22
23impl SetLeverageRequest {
24 pub fn new(lever: impl Into<String>, mgn_mode: TradeMode) -> Self {
26 Self {
27 lever: lever.into(),
28 mgn_mode,
29 inst_id: None,
30 ccy: None,
31 pos_side: None,
32 }
33 }
34
35 pub fn inst_id(mut self, inst_id: impl Into<String>) -> Self {
37 self.inst_id = Some(inst_id.into());
38 self
39 }
40
41 pub fn currency(mut self, ccy: impl Into<String>) -> Self {
43 self.ccy = Some(ccy.into());
44 self
45 }
46
47 pub fn position_side(mut self, pos_side: PositionSide) -> Self {
49 self.pos_side = Some(pos_side);
50 self
51 }
52}
53
54#[derive(Debug, Clone, Serialize)]
56pub struct LeverageRequest {
57 #[serde(rename = "mgnMode")]
58 mgn_mode: TradeMode,
59 #[serde(rename = "instId", skip_serializing_if = "Option::is_none")]
60 inst_id: Option<String>,
61 #[serde(skip_serializing_if = "Option::is_none")]
62 ccy: Option<String>,
63}
64
65impl LeverageRequest {
66 pub fn new(mgn_mode: TradeMode) -> Self {
68 Self {
69 mgn_mode,
70 inst_id: None,
71 ccy: None,
72 }
73 }
74
75 pub fn inst_id(mut self, inst_id: impl Into<String>) -> Self {
77 self.inst_id = Some(inst_id.into());
78 self
79 }
80
81 pub fn currency(mut self, ccy: impl Into<String>) -> Self {
83 self.ccy = Some(ccy.into());
84 self
85 }
86}
87
88#[derive(Debug, Clone, Serialize)]
90pub struct MaxOrderSizeRequest {
91 #[serde(rename = "instId")]
92 inst_id: String,
93 #[serde(rename = "tdMode")]
94 td_mode: TradeMode,
95 #[serde(skip_serializing_if = "Option::is_none")]
96 ccy: Option<String>,
97 #[serde(skip_serializing_if = "Option::is_none")]
98 px: Option<String>,
99 #[serde(rename = "tradeQuoteCcy", skip_serializing_if = "Option::is_none")]
100 trade_quote_ccy: Option<String>,
101}
102
103impl MaxOrderSizeRequest {
104 pub fn new(inst_id: impl Into<String>, td_mode: TradeMode) -> Self {
106 Self {
107 inst_id: inst_id.into(),
108 td_mode,
109 ccy: None,
110 px: None,
111 trade_quote_ccy: None,
112 }
113 }
114
115 pub fn currency(mut self, ccy: impl Into<String>) -> Self {
117 self.ccy = Some(ccy.into());
118 self
119 }
120
121 pub fn price(mut self, px: impl Into<String>) -> Self {
123 self.px = Some(px.into());
124 self
125 }
126
127 pub fn trade_quote_currency(mut self, trade_quote_ccy: impl Into<String>) -> Self {
129 self.trade_quote_ccy = Some(trade_quote_ccy.into());
130 self
131 }
132}
133
134#[derive(Debug, Clone, Serialize)]
136pub struct MaxAvailableSizeRequest {
137 #[serde(rename = "instId")]
138 inst_id: String,
139 #[serde(rename = "tdMode")]
140 td_mode: TradeMode,
141 #[serde(skip_serializing_if = "Option::is_none")]
142 ccy: Option<String>,
143 #[serde(rename = "reduceOnly", skip_serializing_if = "Option::is_none")]
144 reduce_only: Option<bool>,
145 #[serde(rename = "unSpotOffset", skip_serializing_if = "Option::is_none")]
146 un_spot_offset: Option<bool>,
147 #[serde(rename = "quickMgnType", skip_serializing_if = "Option::is_none")]
148 quick_mgn_type: Option<String>,
149 #[serde(rename = "tradeQuoteCcy", skip_serializing_if = "Option::is_none")]
150 trade_quote_ccy: Option<String>,
151}
152
153impl MaxAvailableSizeRequest {
154 pub fn new(inst_id: impl Into<String>, td_mode: TradeMode) -> Self {
156 Self {
157 inst_id: inst_id.into(),
158 td_mode,
159 ccy: None,
160 reduce_only: None,
161 un_spot_offset: None,
162 quick_mgn_type: None,
163 trade_quote_ccy: None,
164 }
165 }
166
167 pub fn currency(mut self, ccy: impl Into<String>) -> Self {
169 self.ccy = Some(ccy.into());
170 self
171 }
172
173 pub fn reduce_only(mut self, reduce_only: bool) -> Self {
175 self.reduce_only = Some(reduce_only);
176 self
177 }
178
179 pub fn un_spot_offset(mut self, un_spot_offset: bool) -> Self {
181 self.un_spot_offset = Some(un_spot_offset);
182 self
183 }
184
185 pub fn quick_margin_type(mut self, quick_mgn_type: impl Into<String>) -> Self {
187 self.quick_mgn_type = Some(quick_mgn_type.into());
188 self
189 }
190
191 pub fn trade_quote_currency(mut self, trade_quote_ccy: impl Into<String>) -> Self {
193 self.trade_quote_ccy = Some(trade_quote_ccy.into());
194 self
195 }
196}
197
198#[derive(Debug, Clone, Serialize)]
200pub struct AdjustMarginRequest {
201 #[serde(rename = "instId")]
202 inst_id: String,
203 #[serde(rename = "posSide")]
204 pos_side: PositionSide,
205 #[serde(rename = "type")]
206 action: String,
207 amt: String,
208 #[serde(rename = "loanTrans", skip_serializing_if = "Option::is_none")]
209 loan_trans: Option<bool>,
210}
211
212impl AdjustMarginRequest {
213 pub fn new(
215 inst_id: impl Into<String>,
216 pos_side: PositionSide,
217 action: impl Into<String>,
218 amt: impl Into<String>,
219 ) -> Self {
220 Self {
221 inst_id: inst_id.into(),
222 pos_side,
223 action: action.into(),
224 amt: amt.into(),
225 loan_trans: None,
226 }
227 }
228
229 pub fn loan_transfer(mut self, loan_trans: bool) -> Self {
231 self.loan_trans = Some(loan_trans);
232 self
233 }
234}
235
236#[derive(Debug, Clone, Serialize)]
238pub struct FeeRatesRequest {
239 #[serde(rename = "instType")]
240 inst_type: InstType,
241 #[serde(rename = "instId", skip_serializing_if = "Option::is_none")]
242 inst_id: Option<String>,
243 #[serde(rename = "uly", skip_serializing_if = "Option::is_none")]
244 underlying: Option<String>,
245 #[serde(skip_serializing_if = "Option::is_none")]
246 category: Option<String>,
247 #[serde(rename = "instFamily", skip_serializing_if = "Option::is_none")]
248 inst_family: Option<String>,
249}
250
251impl FeeRatesRequest {
252 pub fn new(inst_type: InstType) -> Self {
254 Self {
255 inst_type,
256 inst_id: None,
257 underlying: None,
258 category: None,
259 inst_family: None,
260 }
261 }
262
263 pub fn inst_id(mut self, inst_id: impl Into<String>) -> Self {
265 self.inst_id = Some(inst_id.into());
266 self
267 }
268
269 pub fn underlying(mut self, underlying: impl Into<String>) -> Self {
271 self.underlying = Some(underlying.into());
272 self
273 }
274
275 pub fn category(mut self, category: impl Into<String>) -> Self {
277 self.category = Some(category.into());
278 self
279 }
280
281 pub fn inst_family(mut self, inst_family: impl Into<String>) -> Self {
283 self.inst_family = Some(inst_family.into());
284 self
285 }
286}
287
288#[derive(Debug, Clone, Serialize)]
290pub struct AccountInstrumentsRequest {
291 #[serde(rename = "instType")]
292 inst_type: InstType,
293 #[serde(rename = "seriesId", skip_serializing_if = "Option::is_none")]
294 series_id: Option<String>,
295 #[serde(rename = "instFamily", skip_serializing_if = "Option::is_none")]
296 inst_family: Option<String>,
297 #[serde(rename = "instId", skip_serializing_if = "Option::is_none")]
298 inst_id: Option<String>,
299}
300
301impl AccountInstrumentsRequest {
302 pub fn new(inst_type: InstType) -> Self {
304 Self {
305 inst_type,
306 series_id: None,
307 inst_family: None,
308 inst_id: None,
309 }
310 }
311
312 pub fn series_id(mut self, series_id: impl Into<String>) -> Self {
314 self.series_id = Some(series_id.into());
315 self
316 }
317
318 pub fn inst_family(mut self, inst_family: impl Into<String>) -> Self {
320 self.inst_family = Some(inst_family.into());
321 self
322 }
323
324 pub fn inst_id(mut self, inst_id: impl Into<String>) -> Self {
326 self.inst_id = Some(inst_id.into());
327 self
328 }
329}
330
331impl ValidateRequest for AccountInstrumentsRequest {
332 fn validate(&self) -> Result<(), RequestValidationError> {
333 optional_non_empty("seriesId", self.series_id.as_deref())?;
335 optional_non_empty("instFamily", self.inst_family.as_deref())?;
336 optional_non_empty("instId", self.inst_id.as_deref())?;
337
338 match &self.inst_type {
339 InstType::Events => {
340 require_when("seriesId", self.series_id.as_deref(), "instType is EVENTS")?;
342 reject_when_present(
343 "instFamily",
344 self.inst_family.as_ref(),
345 "instType is EVENTS",
346 )?;
347 }
348
349 InstType::Option => {
351 require_when(
352 "instFamily",
353 self.inst_family.as_deref(),
354 "instType is OPTION",
355 )?;
356 }
357
358 InstType::Futures | InstType::Swap => {}
359
360 InstType::Spot | InstType::Margin => {
361 reject_when_present(
363 "instFamily",
364 self.inst_family.as_ref(),
365 "instType is SPOT or MARGIN",
366 )?;
367 }
368
369 InstType::Unknown(_) => {
370 return Err(RequestValidationError::InvalidFormat {
371 field: "instType",
372 expected: "SPOT, MARGIN, SWAP, FUTURES, OPTION, or EVENTS",
373 });
374 }
375 }
376
377 Ok(())
378 }
379}
380
381fn validate_trade_mode(
382 field: &'static str,
383 mode: &TradeMode,
384 allow_cash: bool,
385 allow_spot_isolated: bool,
386) -> Result<(), RequestValidationError> {
387 match mode {
388 TradeMode::Cross | TradeMode::Isolated => Ok(()),
389 TradeMode::Cash if allow_cash => Ok(()),
390 TradeMode::SpotIsolated if allow_spot_isolated => Ok(()),
391 _ => Err(RequestValidationError::InvalidFormat {
392 field,
393 expected: if allow_spot_isolated {
394 "cash, cross, isolated, or spot_isolated"
395 } else if allow_cash {
396 "cash, cross, or isolated"
397 } else {
398 "cross or isolated"
399 },
400 }),
401 }
402}
403
404fn validate_position_side(value: Option<&PositionSide>) -> Result<(), RequestValidationError> {
405 if matches!(value, Some(PositionSide::Unknown(_))) {
406 return Err(RequestValidationError::InvalidFormat {
407 field: "posSide",
408 expected: "long, short, or net",
409 });
410 }
411 Ok(())
412}
413
414impl ValidateRequest for SetLeverageRequest {
415 fn validate(&self) -> Result<(), RequestValidationError> {
416 positive_decimal_string("lever", &self.lever)?;
417 validate_trade_mode("mgnMode", &self.mgn_mode, false, false)?;
418 optional_non_empty("instId", self.inst_id.as_deref())?;
419 optional_non_empty("ccy", self.ccy.as_deref())?;
420 validate_position_side(self.pos_side.as_ref())?;
421 at_least_one("instId, ccy", &[self.inst_id.is_some(), self.ccy.is_some()])?;
422 if matches!(self.mgn_mode, TradeMode::Isolated) {
423 require_when("instId", self.inst_id.as_deref(), "mgnMode is isolated")?;
424 reject_when_present("ccy", self.ccy.as_ref(), "mgnMode is isolated")?;
425 }
426 Ok(())
427 }
428}
429
430impl ValidateRequest for LeverageRequest {
431 fn validate(&self) -> Result<(), RequestValidationError> {
432 validate_trade_mode("mgnMode", &self.mgn_mode, false, false)?;
433 optional_non_empty("instId", self.inst_id.as_deref())?;
434 optional_non_empty("ccy", self.ccy.as_deref())?;
435 at_least_one("instId, ccy", &[self.inst_id.is_some(), self.ccy.is_some()])?;
436 if matches!(self.mgn_mode, TradeMode::Isolated) {
437 require_when("instId", self.inst_id.as_deref(), "mgnMode is isolated")?;
438 reject_when_present("ccy", self.ccy.as_ref(), "mgnMode is isolated")?;
439 }
440 Ok(())
441 }
442}
443
444impl ValidateRequest for MaxOrderSizeRequest {
445 fn validate(&self) -> Result<(), RequestValidationError> {
446 non_empty("instId", &self.inst_id)?;
447 validate_trade_mode("tdMode", &self.td_mode, true, true)?;
448 optional_non_empty("ccy", self.ccy.as_deref())?;
449 optional_positive_decimal_string("px", self.px.as_deref())?;
450 optional_non_empty("tradeQuoteCcy", self.trade_quote_ccy.as_deref())?;
451 Ok(())
452 }
453}
454
455impl ValidateRequest for MaxAvailableSizeRequest {
456 fn validate(&self) -> Result<(), RequestValidationError> {
457 non_empty("instId", &self.inst_id)?;
458 validate_trade_mode("tdMode", &self.td_mode, true, true)?;
459 optional_non_empty("ccy", self.ccy.as_deref())?;
460 optional_one_of(
461 "quickMgnType",
462 self.quick_mgn_type.as_deref(),
463 &["manual", "auto_borrow", "auto_repay"],
464 "manual, auto_borrow, or auto_repay",
465 )?;
466 optional_non_empty("tradeQuoteCcy", self.trade_quote_ccy.as_deref())?;
467 Ok(())
468 }
469}
470
471impl ValidateRequest for AdjustMarginRequest {
472 fn validate(&self) -> Result<(), RequestValidationError> {
473 non_empty("instId", &self.inst_id)?;
474 validate_position_side(Some(&self.pos_side))?;
475 one_of("type", &self.action, &["add", "reduce"], "add or reduce")?;
476 positive_decimal_string("amt", &self.amt)
477 }
478}
479
480impl ValidateRequest for FeeRatesRequest {
481 fn validate(&self) -> Result<(), RequestValidationError> {
482 if matches!(self.inst_type, InstType::Events | InstType::Unknown(_)) {
483 return Err(RequestValidationError::InvalidFormat {
484 field: "instType",
485 expected: "SPOT, MARGIN, SWAP, FUTURES, or OPTION",
486 });
487 }
488 optional_non_empty("instId", self.inst_id.as_deref())?;
489 optional_non_empty("uly", self.underlying.as_deref())?;
490 optional_one_of("category", self.category.as_deref(), &["1", "2"], "1 or 2")?;
491 optional_non_empty("instFamily", self.inst_family.as_deref())?;
492 if matches!(self.inst_type, InstType::Option) {
493 at_least_one(
494 "instId, uly, instFamily",
495 &[
496 self.inst_id.is_some(),
497 self.underlying.is_some(),
498 self.inst_family.is_some(),
499 ],
500 )?;
501 }
502 Ok(())
503 }
504}