use serde::Deserialize;
use crate::client::network::USDC_INDEX;
use crate::markets::side::Side;
use crate::utils::address::Address;
use crate::utils::dec::{Dec, Round};
const MARK_PLACES: u32 = 12;
#[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 Clearinghouse {
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 Clearinghouse {
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()
}
#[must_use]
pub fn is_used(&self) -> bool {
self.holds_anything()
}
}
#[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, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ActiveAssetData {
pub user: Address,
pub coin: String,
pub leverage: Leverage,
pub max_trade_szs: [Dec; 2],
pub available_to_trade: [Dec; 2],
pub mark_px: Dec,
}
impl ActiveAssetData {
#[must_use]
pub const fn max_size(&self, side: Side) -> Dec {
self.max_trade_szs[at(side)]
}
#[must_use]
pub const fn available(&self, side: Side) -> Dec {
self.available_to_trade[at(side)]
}
}
const fn at(side: Side) -> usize {
match side {
Side::Buy => 0,
Side::Sell => 1,
}
}
#[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,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SpotState {
pub balances: Vec<SpotBalance>,
#[serde(default)]
pub portfolio_margin_enabled: Option<bool>,
#[serde(default)]
pub portfolio_margin_ratio: Option<Dec>,
#[serde(default)]
pub token_to_available_after_maintenance: Vec<(u32, Dec)>,
}
impl SpotState {
#[must_use]
pub fn balance(&self, token: u32) -> Option<&SpotBalance> {
self.balances.iter().find(|balance| balance.token == Some(token) && !balance.is_outcome())
}
#[must_use]
pub fn usdc(&self) -> Option<&SpotBalance> {
self.balance(USDC_INDEX)
}
pub fn held(&self) -> impl Iterator<Item = &SpotBalance> {
let mut held: Vec<&SpotBalance> = self.balances.iter().filter(|row| !row.total.is_zero()).collect();
held.sort_by(|a, b| b.total.cmp(&a.total).then_with(|| a.coin.cmp(&b.coin)));
held.into_iter()
}
#[must_use]
pub fn free(&self, token: u32) -> Dec {
self.balance(token).and_then(SpotBalance::available).unwrap_or(Dec::ZERO)
}
#[must_use]
pub fn available_after_maintenance(&self, token: u32) -> Option<Dec> {
self.token_to_available_after_maintenance.iter().find(|(held, _)| *held == token).map(|(_, free)| *free)
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SpotBalance {
pub coin: String,
#[serde(default)]
pub token: Option<u32>,
pub total: Dec,
pub hold: Dec,
pub entry_ntl: Dec,
#[serde(default)]
pub spot_hold: Option<Dec>,
#[serde(default)]
pub borrowed: Option<Dec>,
#[serde(default)]
pub supplied: Option<Dec>,
#[serde(default)]
pub ltv: Option<Dec>,
}
impl SpotBalance {
#[must_use]
pub fn available(&self) -> Option<Dec> {
let held = [Some(self.hold), self.spot_hold].into_iter().flatten().filter(|h| h.is_positive()).max();
let free = self.total.checked_sub(held.unwrap_or(Dec::ZERO))?;
Some(free.max(Dec::ZERO).min(self.total.max(Dec::ZERO)))
}
#[must_use]
pub fn is_outcome(&self) -> bool {
let coin = self.coin.as_str();
coin.starts_with(['+', '#'])
|| coin.strip_prefix('o').is_some_and(|id| !id.is_empty() && id.bytes().all(|b| b.is_ascii_digit()))
}
}