use serde::Deserialize;
use crate::client::network::USDC_INDEX;
use crate::client::{ApiError, Transport, info, user_request};
use crate::utils::{Address, Dec};
#[must_use]
pub fn spot_state_request(user: Address) -> String {
user_request("spotClearinghouseState", user)
}
pub async fn spot_state(transport: &impl Transport, user: Address) -> Result<SpotState, ApiError> {
info(transport, spot_state_request(user)).await
}
#[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)
}
#[must_use]
pub fn risk(&self) -> Risk {
Risk::of(self.portfolio_margin_ratio)
}
}
#[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()))
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub enum Risk {
#[default]
Safe,
Near,
Liquidatable,
}
impl Risk {
#[must_use]
pub fn of(ratio: Option<Dec>) -> Risk {
let (near, liquidatable) = (Dec::lit::<2>(85), Dec::lit::<2>(95));
match ratio {
Some(ratio) if ratio > liquidatable => Risk::Liquidatable,
Some(ratio) if ratio >= near => Risk::Near,
_ => Risk::Safe,
}
}
}
#[cfg(test)]
#[path = "tests/spot.rs"]
mod tests;