use serde::Deserialize;
use crate::client::{ApiError, Transport, info, info_request};
use crate::markets::{Dex, Side};
use crate::utils::{Address, Dec, Json, Round};
const MARK_PLACES: u32 = 12;
#[must_use]
pub fn clearinghouse_state_request(user: Address, dex: Dex<'_>) -> String {
let mut fields = vec![("user", Json::String(user.to_hex()))];
if let Some(name) = dex.name() {
fields.push(("dex", Json::String(name.to_owned())));
}
info_request("clearinghouseState", &fields)
}
pub async fn clearinghouse_state(
transport: &impl Transport,
user: Address,
dex: Dex<'_>,
) -> Result<ClearinghouseState, ApiError> {
info(transport, clearinghouse_state_request(user, dex)).await
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum MarginMode {
Cross,
Isolated,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ClearinghouseState {
pub margin_summary: MarginSummary,
pub cross_margin_summary: MarginSummary,
pub cross_maintenance_margin_used: Dec,
pub withdrawable: Dec,
pub asset_positions: Vec<AssetPosition>,
#[serde(default)]
pub time: Option<u64>,
}
impl ClearinghouseState {
pub fn positions(&self) -> impl Iterator<Item = &Position> {
self.asset_positions.iter().map(|entry| &entry.position).filter(|position| !position.szi.is_zero())
}
#[must_use]
pub fn position(&self, coin: &str) -> Option<&Position> {
self.positions().find(|position| position.coin == coin)
}
#[must_use]
pub fn holds_anything(&self) -> bool {
!self.margin_summary.account_value.is_zero() || self.positions().next().is_some()
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MarginSummary {
pub account_value: Dec,
pub total_ntl_pos: Dec,
pub total_raw_usd: Dec,
pub total_margin_used: Dec,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct AssetPosition {
#[serde(rename = "type")]
pub kind: String,
pub position: Position,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Position {
pub coin: String,
pub szi: Dec,
pub leverage: Leverage,
#[serde(default)]
pub entry_px: Option<Dec>,
pub position_value: Dec,
pub unrealized_pnl: Dec,
#[serde(default)]
pub return_on_equity: Option<Dec>,
#[serde(default)]
pub liquidation_px: Option<Dec>,
pub margin_used: Dec,
pub max_leverage: u32,
#[serde(default)]
pub cum_funding: Option<CumFunding>,
}
impl Position {
#[must_use]
pub const fn is_long(&self) -> bool {
self.szi.is_positive()
}
#[must_use]
pub const fn is_short(&self) -> bool {
self.szi.is_negative()
}
#[must_use]
pub const fn size(&self) -> Dec {
self.szi.abs()
}
#[must_use]
pub const fn closing_side(&self) -> Option<Side> {
if self.szi.is_positive() {
Some(Side::Sell)
} else if self.szi.is_negative() {
Some(Side::Buy)
} else {
None
}
}
#[must_use]
pub fn mark(&self) -> Option<Dec> {
self.position_value.checked_div(self.size(), MARK_PLACES, Round::Nearest)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum Leverage {
Cross {
value: u32,
},
Isolated {
value: u32,
#[serde(rename = "rawUsd")]
raw_usd: Dec,
},
}
impl Leverage {
#[must_use]
pub const fn value(&self) -> u32 {
match self {
Leverage::Cross { value } | Leverage::Isolated { value, .. } => *value,
}
}
#[must_use]
pub const fn mode(&self) -> MarginMode {
match self {
Leverage::Cross { .. } => MarginMode::Cross,
Leverage::Isolated { .. } => MarginMode::Isolated,
}
}
#[must_use]
pub const fn raw_usd(&self) -> Option<Dec> {
match self {
Leverage::Cross { .. } => None,
Leverage::Isolated { raw_usd, .. } => Some(*raw_usd),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CumFunding {
pub all_time: Dec,
pub since_open: Dec,
pub since_change: Dec,
}
#[cfg(test)]
#[path = "tests/perps.rs"]
mod tests;