use serde::Serialize;
use crate::model::{
RequestValidationError, TradeMode, ValidateRequest, collection_length, exactly_one, non_empty,
non_empty_items, one_of, optional_non_empty, optional_one_of, optional_unsigned_integer_string,
positive_decimal_string, range_u64, reject_when_present,
};
#[derive(Debug, Clone, Serialize)]
pub struct MaxLoanRequest {
#[serde(rename = "instId", skip_serializing_if = "Option::is_none")]
inst_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
ccy: Option<String>,
#[serde(rename = "mgnMode")]
mgn_mode: TradeMode,
#[serde(rename = "mgnCcy", skip_serializing_if = "Option::is_none")]
mgn_ccy: Option<String>,
#[serde(rename = "tradeQuoteCcy", skip_serializing_if = "Option::is_none")]
trade_quote_ccy: Option<String>,
}
impl MaxLoanRequest {
pub fn new(inst_id: impl Into<String>, mgn_mode: TradeMode) -> Self {
Self::by_instrument(inst_id, mgn_mode)
}
pub fn by_instrument(inst_id: impl Into<String>, mgn_mode: TradeMode) -> Self {
Self {
mgn_mode,
inst_id: Some(inst_id.into()),
ccy: None,
mgn_ccy: None,
trade_quote_ccy: None,
}
}
pub fn by_currency(ccy: impl Into<String>) -> Self {
Self {
mgn_mode: TradeMode::Cross,
inst_id: None,
ccy: Some(ccy.into()),
mgn_ccy: None,
trade_quote_ccy: None,
}
}
pub fn currency(mut self, ccy: impl Into<String>) -> Self {
self.inst_id = None;
self.ccy = Some(ccy.into());
self
}
pub fn margin_currency(mut self, mgn_ccy: impl Into<String>) -> Self {
self.mgn_ccy = Some(mgn_ccy.into());
self
}
pub fn trade_quote_currency(mut self, trade_quote_ccy: impl Into<String>) -> Self {
self.trade_quote_ccy = Some(trade_quote_ccy.into());
self
}
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct InterestAccruedRequest {
#[serde(rename = "instId", skip_serializing_if = "Option::is_none")]
inst_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
ccy: Option<String>,
#[serde(rename = "mgnMode", skip_serializing_if = "Option::is_none")]
mgn_mode: Option<TradeMode>,
#[serde(skip_serializing_if = "Option::is_none")]
after: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
before: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
limit: Option<u32>,
}
impl InterestAccruedRequest {
pub fn new() -> Self {
Self::default()
}
pub fn inst_id(mut self, inst_id: impl Into<String>) -> Self {
self.inst_id = Some(inst_id.into());
self
}
pub fn currency(mut self, ccy: impl Into<String>) -> Self {
self.ccy = Some(ccy.into());
self
}
pub fn margin_mode(mut self, mgn_mode: TradeMode) -> Self {
self.mgn_mode = Some(mgn_mode);
self
}
pub fn after(mut self, after: impl Into<String>) -> Self {
self.after = Some(after.into());
self
}
pub fn before(mut self, before: impl Into<String>) -> Self {
self.before = Some(before.into());
self
}
pub fn limit(mut self, limit: u32) -> Self {
self.limit = Some(limit);
self
}
}
#[derive(Debug, Clone, Serialize)]
pub struct BorrowRepayRequest {
ccy: String,
side: String,
amt: String,
#[serde(rename = "ordId", skip_serializing_if = "Option::is_none")]
ord_id: Option<String>,
}
impl BorrowRepayRequest {
pub fn new(ccy: impl Into<String>, side: impl Into<String>, amt: impl Into<String>) -> Self {
Self {
ccy: ccy.into(),
side: side.into(),
amt: amt.into(),
ord_id: None,
}
}
pub fn order_id(mut self, ord_id: impl Into<String>) -> Self {
self.ord_id = Some(ord_id.into());
self
}
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct BorrowRepayHistoryRequest {
#[serde(skip_serializing_if = "Option::is_none")]
ccy: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
after: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
before: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
limit: Option<u32>,
}
impl BorrowRepayHistoryRequest {
pub fn new() -> Self {
Self::default()
}
pub fn currency(mut self, ccy: impl Into<String>) -> Self {
self.ccy = Some(ccy.into());
self
}
pub fn after(mut self, after: impl Into<String>) -> Self {
self.after = Some(after.into());
self
}
pub fn before(mut self, before: impl Into<String>) -> Self {
self.before = Some(before.into());
self
}
pub fn limit(mut self, limit: u32) -> Self {
self.limit = Some(limit);
self
}
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct InterestLimitsRequest {
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
limit_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
ccy: Option<String>,
}
impl InterestLimitsRequest {
pub fn new() -> Self {
Self::default()
}
pub fn limit_type(mut self, limit_type: impl Into<String>) -> Self {
self.limit_type = Some(limit_type.into());
self
}
pub fn currency(mut self, ccy: impl Into<String>) -> Self {
self.ccy = Some(ccy.into());
self
}
}
fn validate_pagination(
after: Option<&str>,
before: Option<&str>,
limit: Option<u32>,
) -> Result<(), RequestValidationError> {
optional_unsigned_integer_string("after", after)?;
optional_unsigned_integer_string("before", before)?;
if let Some(limit) = limit {
range_u64("limit", u64::from(limit), 1, 100)?;
}
Ok(())
}
impl ValidateRequest for MaxLoanRequest {
fn validate(&self) -> Result<(), RequestValidationError> {
match &self.mgn_mode {
TradeMode::Cross | TradeMode::Isolated => {}
_ => {
return Err(RequestValidationError::InvalidFormat {
field: "mgnMode",
expected: "cross or isolated",
});
}
}
optional_non_empty("instId", self.inst_id.as_deref())?;
optional_non_empty("ccy", self.ccy.as_deref())?;
exactly_one("instId, ccy", &[self.inst_id.is_some(), self.ccy.is_some()])?;
if let Some(inst_ids) = self.inst_id.as_deref() {
let instruments: Vec<_> = inst_ids.split(',').collect();
collection_length("instId", instruments.len(), 1, 5)?;
non_empty_items("instId", instruments)?;
}
optional_non_empty("mgnCcy", self.mgn_ccy.as_deref())?;
optional_non_empty("tradeQuoteCcy", self.trade_quote_ccy.as_deref())?;
if self.ccy.is_some() {
if !matches!(self.mgn_mode, TradeMode::Cross) {
return Err(RequestValidationError::InvalidFormat {
field: "mgnMode",
expected: "cross when ccy is used",
});
}
reject_when_present("mgnCcy", self.mgn_ccy.as_ref(), "ccy is used")?;
reject_when_present(
"tradeQuoteCcy",
self.trade_quote_ccy.as_ref(),
"ccy is used",
)?;
}
Ok(())
}
}
impl ValidateRequest for InterestAccruedRequest {
fn validate(&self) -> Result<(), RequestValidationError> {
optional_non_empty("instId", self.inst_id.as_deref())?;
optional_non_empty("ccy", self.ccy.as_deref())?;
if let Some(mode) = &self.mgn_mode {
match mode {
TradeMode::Cross | TradeMode::Isolated => {}
_ => {
return Err(RequestValidationError::InvalidFormat {
field: "mgnMode",
expected: "cross or isolated",
});
}
}
}
validate_pagination(self.after.as_deref(), self.before.as_deref(), self.limit)
}
}
impl ValidateRequest for BorrowRepayRequest {
fn validate(&self) -> Result<(), RequestValidationError> {
non_empty("ccy", &self.ccy)?;
one_of("side", &self.side, &["borrow", "repay"], "borrow or repay")?;
positive_decimal_string("amt", &self.amt)?;
optional_non_empty("ordId", self.ord_id.as_deref())
}
}
impl ValidateRequest for BorrowRepayHistoryRequest {
fn validate(&self) -> Result<(), RequestValidationError> {
optional_non_empty("ccy", self.ccy.as_deref())?;
validate_pagination(self.after.as_deref(), self.before.as_deref(), self.limit)
}
}
impl ValidateRequest for InterestLimitsRequest {
fn validate(&self) -> Result<(), RequestValidationError> {
optional_one_of(
"type",
self.limit_type.as_deref(),
&["1", "2"],
"1 (loan quota) or 2 (interest rate)",
)?;
optional_non_empty("ccy", self.ccy.as_deref())?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn max_loan_accepts_currency_selector() {
MaxLoanRequest::by_currency("USDT").validate().unwrap();
}
#[test]
fn max_loan_rejects_more_than_five_instruments() {
let request = MaxLoanRequest::new("A-B,B-C,C-D,D-E,E-F,F-G", TradeMode::Cross);
assert!(request.validate().is_err());
}
}