use rust_decimal::{Decimal, serde::str_option::deserialize as option_decimal};
use serde::{Deserialize, Serialize};
use serde_aux::prelude::{
deserialize_number_from_string as number,
deserialize_option_number_from_string as option_number,
};
use crate::{
AdlRankIndicator, ExecType, OrderType, PositionIdx, PositionMode, PositionStatus, Side,
Timestamp, TpslMode, TradeMode, TriggerBy,
enums::{Category, StopOrderType},
serde::{decimal_or_zero, empty_string_as_none, int_to_bool, timestamp_or_zero},
ws::PositionMsg,
};
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GetPositionInfoParams {
pub category: Category,
#[serde(skip_serializing_if = "Option::is_none")]
pub symbol: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub base_coin: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub settle_coin: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cursor: Option<String>,
}
impl GetPositionInfoParams {
pub fn new(category: Category) -> Self {
Self {
category,
symbol: None,
base_coin: None,
settle_coin: None,
limit: None,
cursor: None,
}
}
pub fn with_symbol(mut self, v: impl Into<String>) -> Self {
self.symbol = Some(v.into());
self
}
pub fn with_base_coin(mut self, v: impl Into<String>) -> Self {
self.base_coin = Some(v.into());
self
}
pub fn with_settle_coin(mut self, v: impl Into<String>) -> Self {
self.settle_coin = Some(v.into());
self
}
pub fn with_limit(mut self, v: u64) -> Self {
self.limit = Some(v);
self
}
pub fn with_cursor(mut self, v: impl Into<String>) -> Self {
self.cursor = Some(v.into());
self
}
}
#[derive(Debug, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Position {
pub position_idx: PositionIdx,
pub risk_id: i64,
#[serde(default, deserialize_with = "option_decimal")]
pub risk_limit_value: Option<Decimal>,
pub symbol: String,
#[serde(default, deserialize_with = "empty_string_as_none")]
pub side: Option<Side>,
pub size: Decimal,
#[serde(deserialize_with = "decimal_or_zero")]
pub avg_price: Decimal,
#[serde(default, deserialize_with = "option_decimal")]
pub position_value: Option<Decimal>,
#[serde(deserialize_with = "int_to_bool")]
pub auto_add_margin: bool,
#[serde(default, deserialize_with = "empty_string_as_none")]
pub position_status: Option<PositionStatus>,
pub leverage: Decimal,
#[serde(deserialize_with = "decimal_or_zero")]
pub mark_price: Decimal,
#[serde(default, deserialize_with = "option_decimal")]
pub liq_price: Option<Decimal>,
#[serde(rename = "positionIM", default, deserialize_with = "option_decimal")]
pub position_im: Option<Decimal>,
#[serde(
rename = "positionIMByMp",
default,
deserialize_with = "option_decimal"
)]
pub position_im_by_mp: Option<Decimal>,
#[serde(rename = "positionMM", default, deserialize_with = "option_decimal")]
pub position_mm: Option<Decimal>,
#[serde(
rename = "positionMMByMp",
default,
deserialize_with = "option_decimal"
)]
pub position_mm_by_mp: Option<Decimal>,
#[serde(default, deserialize_with = "option_decimal")]
pub take_profit: Option<Decimal>,
#[serde(default, deserialize_with = "option_decimal")]
pub stop_loss: Option<Decimal>,
#[serde(default, deserialize_with = "option_decimal")]
pub trailing_stop: Option<Decimal>,
#[serde(default, deserialize_with = "option_decimal")]
pub session_avg_price: Option<Decimal>,
#[serde(default, deserialize_with = "option_decimal")]
pub break_even_price: Option<Decimal>,
#[serde(default, deserialize_with = "option_decimal")]
pub net_delta_ratio: Option<Decimal>,
#[serde(default, deserialize_with = "option_number")]
pub open_time: Option<Timestamp>,
#[serde(default, deserialize_with = "empty_string_as_none")]
pub delta: Option<String>,
#[serde(default, deserialize_with = "empty_string_as_none")]
pub gamma: Option<String>,
#[serde(default, deserialize_with = "empty_string_as_none")]
pub vega: Option<String>,
#[serde(default, deserialize_with = "empty_string_as_none")]
pub theta: Option<String>,
#[serde(default, deserialize_with = "option_decimal")]
pub unrealised_pnl: Option<Decimal>,
#[serde(deserialize_with = "decimal_or_zero")]
pub cur_realised_pnl: Decimal,
#[serde(deserialize_with = "decimal_or_zero")]
pub cum_realised_pnl: Decimal,
pub adl_rank_indicator: AdlRankIndicator,
#[serde(deserialize_with = "timestamp_or_zero")]
pub created_time: Timestamp,
#[serde(deserialize_with = "timestamp_or_zero")]
pub updated_time: Timestamp,
pub seq: i64,
pub is_reduce_only: bool,
#[serde(deserialize_with = "option_number")]
pub mmr_sys_updated_time: Option<Timestamp>,
#[serde(deserialize_with = "option_number")]
pub leverage_sys_updated_time: Option<Timestamp>,
}
impl Position {
pub fn update(&mut self, msg: PositionMsg) {
*self = msg.into();
}
}
impl From<PositionMsg> for Position {
fn from(msg: PositionMsg) -> Self {
Self {
position_idx: msg.position_idx,
risk_id: msg.risk_id,
risk_limit_value: msg.risk_limit_value,
symbol: msg.symbol,
side: msg.side,
size: msg.size,
avg_price: msg.entry_price,
position_value: Some(msg.position_value),
auto_add_margin: msg.auto_add_margin,
position_status: Some(msg.position_status),
leverage: msg.leverage,
mark_price: msg.mark_price,
liq_price: msg.liq_price,
position_im: msg.position_im,
position_im_by_mp: msg.position_im_by_mp,
position_mm: msg.position_mm,
position_mm_by_mp: msg.position_mm_by_mp,
take_profit: Some(msg.take_profit),
stop_loss: Some(msg.stop_loss),
trailing_stop: Some(msg.trailing_stop),
session_avg_price: msg.session_avg_price,
break_even_price: None,
net_delta_ratio: None,
open_time: None,
delta: msg.delta,
gamma: msg.gamma,
vega: msg.vega,
theta: msg.theta,
unrealised_pnl: Some(msg.unrealised_pnl),
cur_realised_pnl: msg.cur_realised_pnl,
cum_realised_pnl: msg.cum_realised_pnl,
adl_rank_indicator: msg.adl_rank_indicator,
created_time: msg.created_time,
updated_time: msg.updated_time,
seq: msg.seq,
is_reduce_only: msg.is_reduce_only,
mmr_sys_updated_time: msg.mmr_sys_updated_time,
leverage_sys_updated_time: msg.leverage_sys_updated_time,
}
}
}
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct SetLeverageRequest {
pub category: Category,
pub symbol: String,
pub buy_leverage: Decimal,
pub sell_leverage: Decimal,
}
impl SetLeverageRequest {
pub fn new(category: Category, symbol: impl Into<String>, leverage: Decimal) -> Self {
let symbol: String = symbol.into();
Self {
category,
symbol,
buy_leverage: leverage,
sell_leverage: leverage,
}
}
pub fn with_asymmetric(mut self, buy_leverage: Decimal, sell_leverage: Decimal) -> Self {
self.buy_leverage = buy_leverage;
self.sell_leverage = sell_leverage;
self
}
}
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct SetTradingStopRequest {
pub category: Category,
pub symbol: String,
pub position_idx: PositionIdx,
#[serde(skip_serializing_if = "Option::is_none")]
pub take_profit: Option<Decimal>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop_loss: Option<Decimal>,
#[serde(skip_serializing_if = "Option::is_none")]
pub trailing_stop: Option<Decimal>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tp_trigger_by: Option<TriggerBy>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sl_trigger_by: Option<TriggerBy>,
#[serde(skip_serializing_if = "Option::is_none")]
pub active_price: Option<Decimal>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tp_size: Option<Decimal>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sl_size: Option<Decimal>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tp_limit_price: Option<Decimal>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sl_limit_price: Option<Decimal>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tp_order_type: Option<OrderType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sl_order_type: Option<OrderType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tpsl_mode: Option<TpslMode>,
}
impl SetTradingStopRequest {
pub fn new(category: Category, symbol: impl Into<String>, position_idx: PositionIdx) -> Self {
let symbol: String = symbol.into();
Self {
category,
symbol,
position_idx,
take_profit: None,
stop_loss: None,
trailing_stop: None,
tp_trigger_by: None,
sl_trigger_by: None,
active_price: None,
tp_size: None,
sl_size: None,
tp_limit_price: None,
sl_limit_price: None,
tp_order_type: None,
sl_order_type: None,
tpsl_mode: None,
}
}
pub fn with_take_profit(mut self, v: Decimal) -> Self {
self.take_profit = Some(v);
self
}
pub fn with_stop_loss(mut self, v: Decimal) -> Self {
self.stop_loss = Some(v);
self
}
pub fn with_trailing_stop(mut self, v: Decimal) -> Self {
self.trailing_stop = Some(v);
self
}
pub fn with_tp_trigger_by(mut self, v: TriggerBy) -> Self {
self.tp_trigger_by = Some(v);
self
}
pub fn with_sl_trigger_by(mut self, v: TriggerBy) -> Self {
self.sl_trigger_by = Some(v);
self
}
pub fn with_active_price(mut self, v: Decimal) -> Self {
self.active_price = Some(v);
self
}
pub fn with_tp_size(mut self, v: Decimal) -> Self {
self.tp_size = Some(v);
self
}
pub fn with_sl_size(mut self, v: Decimal) -> Self {
self.sl_size = Some(v);
self
}
pub fn with_tp_limit_price(mut self, v: Decimal) -> Self {
self.tp_limit_price = Some(v);
self
}
pub fn with_sl_limit_price(mut self, v: Decimal) -> Self {
self.sl_limit_price = Some(v);
self
}
pub fn with_tp_order_type(mut self, v: OrderType) -> Self {
self.tp_order_type = Some(v);
self
}
pub fn with_sl_order_type(mut self, v: OrderType) -> Self {
self.sl_order_type = Some(v);
self
}
pub fn with_tpsl_mode(mut self, v: TpslMode) -> Self {
self.tpsl_mode = Some(v);
self
}
}
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct SwitchCrossIsolatedMarginRequest {
pub category: Category,
pub symbol: String,
pub trade_mode: TradeMode,
pub buy_leverage: Decimal,
pub sell_leverage: Decimal,
}
impl SwitchCrossIsolatedMarginRequest {
pub fn cross(category: Category, symbol: impl Into<String>, leverage: Decimal) -> Self {
Self {
category,
symbol: symbol.into(),
trade_mode: TradeMode::CrossMargin,
buy_leverage: leverage,
sell_leverage: leverage,
}
}
pub fn isolated(
category: Category,
symbol: impl Into<String>,
buy_leverage: Decimal,
sell_leverage: Decimal,
) -> Self {
Self {
category,
symbol: symbol.into(),
trade_mode: TradeMode::IsolatedMargin,
buy_leverage,
sell_leverage,
}
}
}
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct SwitchPositionModeRequest {
pub category: Category,
#[serde(skip_serializing_if = "Option::is_none")]
pub symbol: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub coin: Option<String>,
pub mode: PositionMode,
}
impl SwitchPositionModeRequest {
pub fn one_way(category: Category, symbol: impl Into<String>) -> Self {
Self {
category,
symbol: Some(symbol.into()),
coin: None,
mode: PositionMode::OneWay,
}
}
pub fn hedge(category: Category, symbol: impl Into<String>) -> Self {
Self {
category,
symbol: Some(symbol.into()),
coin: None,
mode: PositionMode::Hedge,
}
}
pub fn with_coin(mut self, v: impl Into<String>) -> Self {
self.symbol = None;
self.coin = Some(v.into());
self
}
}
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct SetAutoAddMarginRequest {
pub category: Category,
pub symbol: String,
pub auto_add_margin: i32,
#[serde(skip_serializing_if = "Option::is_none")]
pub position_idx: Option<PositionIdx>,
}
impl SetAutoAddMarginRequest {
pub fn new(category: Category, symbol: impl Into<String>, enabled: bool) -> Self {
let symbol: String = symbol.into();
Self {
category,
symbol,
auto_add_margin: if enabled { 1 } else { 0 },
position_idx: None,
}
}
pub fn with_position_idx(mut self, v: PositionIdx) -> Self {
self.position_idx = Some(v);
self
}
}
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct SetRiskLimitRequest {
pub category: Category,
pub symbol: String,
pub risk_id: i64,
#[serde(skip_serializing_if = "Option::is_none")]
pub position_idx: Option<PositionIdx>,
}
impl SetRiskLimitRequest {
pub fn new(category: Category, symbol: impl Into<String>, risk_id: i64) -> Self {
let symbol: String = symbol.into();
Self {
category,
symbol,
risk_id,
position_idx: None,
}
}
pub fn with_position_idx(mut self, v: PositionIdx) -> Self {
self.position_idx = Some(v);
self
}
}
#[derive(Debug, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct SetRiskLimitResponse {
pub risk_id: i64,
pub risk_limit_value: String,
pub category: String,
#[serde(default, deserialize_with = "empty_string_as_none")]
pub message: Option<String>,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GetClosedPnlParams {
pub category: Category,
#[serde(skip_serializing_if = "Option::is_none")]
pub symbol: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub start_time: Option<Timestamp>,
#[serde(skip_serializing_if = "Option::is_none")]
pub end_time: Option<Timestamp>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cursor: Option<String>,
}
impl GetClosedPnlParams {
pub fn new(category: Category) -> Self {
Self {
category,
symbol: None,
start_time: None,
end_time: None,
limit: None,
cursor: None,
}
}
pub fn with_symbol(mut self, v: impl Into<String>) -> Self {
self.symbol = Some(v.into());
self
}
pub fn with_start_time(mut self, v: Timestamp) -> Self {
self.start_time = Some(v);
self
}
pub fn with_end_time(mut self, v: Timestamp) -> Self {
self.end_time = Some(v);
self
}
pub fn with_limit(mut self, v: i32) -> Self {
self.limit = Some(v);
self
}
pub fn with_cursor(mut self, v: impl Into<String>) -> Self {
self.cursor = Some(v.into());
self
}
}
#[derive(Debug, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ClosedPnl {
pub symbol: String,
pub order_id: String,
#[serde(default, deserialize_with = "empty_string_as_none")]
pub order_link_id: Option<String>,
pub side: Side,
#[serde(deserialize_with = "number")]
pub qty: Decimal,
#[serde(deserialize_with = "number")]
pub order_price: Decimal,
pub order_type: OrderType,
pub exec_type: ExecType,
#[serde(deserialize_with = "number")]
pub closed_size: Decimal,
pub cum_entry_value: Decimal,
pub avg_entry_price: Decimal,
pub cum_exit_value: Decimal,
pub avg_exit_price: Decimal,
pub closed_pnl: Decimal,
#[serde(deserialize_with = "number")]
pub fill_count: i64,
pub leverage: Decimal,
#[serde(deserialize_with = "number")]
pub created_time: Timestamp,
#[serde(deserialize_with = "number")]
pub updated_time: Timestamp,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GetExecutionListParams {
pub category: Category,
#[serde(skip_serializing_if = "Option::is_none")]
pub symbol: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub order_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub order_link_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub base_coin: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub start_time: Option<Timestamp>,
#[serde(skip_serializing_if = "Option::is_none")]
pub end_time: Option<Timestamp>,
#[serde(skip_serializing_if = "Option::is_none")]
pub exec_type: Option<ExecType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cursor: Option<String>,
}
impl GetExecutionListParams {
pub fn new(category: Category) -> Self {
Self {
category,
symbol: None,
order_id: None,
order_link_id: None,
base_coin: None,
start_time: None,
end_time: None,
exec_type: None,
limit: None,
cursor: None,
}
}
pub fn with_symbol(mut self, v: impl Into<String>) -> Self {
self.symbol = Some(v.into());
self
}
pub fn with_order_id(mut self, v: impl Into<String>) -> Self {
self.order_id = Some(v.into());
self
}
pub fn with_order_link_id(mut self, v: impl Into<String>) -> Self {
self.order_link_id = Some(v.into());
self
}
pub fn with_base_coin(mut self, v: impl Into<String>) -> Self {
self.base_coin = Some(v.into());
self
}
pub fn with_start_time(mut self, v: Timestamp) -> Self {
self.start_time = Some(v);
self
}
pub fn with_end_time(mut self, v: Timestamp) -> Self {
self.end_time = Some(v);
self
}
pub fn with_exec_type(mut self, v: ExecType) -> Self {
self.exec_type = Some(v);
self
}
pub fn with_limit(mut self, v: i32) -> Self {
self.limit = Some(v);
self
}
pub fn with_cursor(mut self, v: impl Into<String>) -> Self {
self.cursor = Some(v.into());
self
}
}
#[derive(Debug, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ExecutionEntry {
pub symbol: String,
pub order_id: String,
#[serde(default, deserialize_with = "empty_string_as_none")]
pub order_link_id: Option<String>,
pub side: Side,
pub order_price: Decimal,
pub order_qty: Decimal,
pub order_type: OrderType,
#[serde(default, deserialize_with = "empty_string_as_none")]
pub stop_order_type: Option<StopOrderType>,
pub exec_fee: Decimal,
pub exec_id: String,
pub exec_price: Decimal,
pub exec_qty: Decimal,
pub exec_type: ExecType,
pub exec_value: Decimal,
#[serde(deserialize_with = "number")]
pub exec_time: Timestamp,
pub fee_rate: Decimal,
#[serde(default, deserialize_with = "option_decimal")]
pub trade_iv: Option<Decimal>,
#[serde(default, deserialize_with = "option_decimal")]
pub mark_iv: Option<Decimal>,
pub mark_price: Decimal,
#[serde(default, deserialize_with = "option_decimal")]
pub index_price: Option<Decimal>,
#[serde(default, deserialize_with = "option_decimal")]
pub underlying_price: Option<Decimal>,
#[serde(default, deserialize_with = "empty_string_as_none")]
pub block_trade_id: Option<String>,
pub closed_size: Decimal,
pub seq: i64,
pub is_maker: bool,
}
#[cfg(test)]
mod tests {
use rust_decimal::dec;
use super::*;
#[test]
fn mode_constructors_accept_a_str_symbol() {
let req = SwitchCrossIsolatedMarginRequest::cross(Category::Linear, "BTCUSDT", dec!(10));
assert_eq!(
serde_json::to_value(&req).unwrap(),
serde_json::json!({
"category": "linear",
"symbol": "BTCUSDT",
"tradeMode": 0,
"buyLeverage": "10",
"sellLeverage": "10",
})
);
let req = SwitchCrossIsolatedMarginRequest::isolated(
Category::Linear,
"ETHUSDT",
dec!(5),
dec!(3),
);
assert_eq!(
(req.symbol.as_str(), req.trade_mode),
("ETHUSDT", TradeMode::IsolatedMargin)
);
let req = SwitchPositionModeRequest::hedge(Category::Linear, "BTCUSDT");
assert_eq!(
serde_json::to_value(&req).unwrap(),
serde_json::json!({ "category": "linear", "symbol": "BTCUSDT", "mode": 3 })
);
let req = SwitchPositionModeRequest::one_way(Category::Inverse, String::from("BTCUSD"));
assert_eq!(req.symbol.as_deref(), Some("BTCUSD"));
assert_eq!(req.mode, PositionMode::OneWay);
}
}