use std::collections::{BTreeMap, BTreeSet};
use crate::account::state::{Risk, SpotState};
use crate::account::usable::{Collateral, Standing, USDC};
use crate::utils::{Address, Dec};
const STAKED: &str = "HYPE";
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Prices(BTreeMap<String, Dec>);
impl Prices {
#[must_use]
pub fn new() -> Prices {
Prices::default()
}
#[must_use]
pub fn with(mut self, name: &str, price: Dec) -> Prices {
self.0.insert(name.to_owned(), price);
self
}
#[must_use]
pub fn of(pairs: &BTreeMap<String, String>, mids: &BTreeMap<String, Dec>) -> Prices {
let priced = |mid: &Dec| Some(*mid).filter(|price| price.is_positive());
let mut prices: BTreeMap<String, Dec> = mids
.iter()
.filter(|(name, _)| name.starts_with('#'))
.filter_map(|(name, mid)| Some((name.clone(), priced(mid)?)))
.collect();
for (token, pair) in pairs {
if let Some(price) = mids.get(pair).and_then(priced) {
prices.insert(token.clone(), price);
}
}
Prices(prices)
}
#[must_use]
pub fn price(&self, coin: &str) -> Option<Dec> {
if coin == "USDC" {
return Some(Dec::ONE);
}
match coin.strip_prefix('+') {
Some(side) => self.0.get(&format!("#{side}")).copied(),
None => self.0.get(coin).copied(),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Beside {
pub staked: Dec,
pub vaults: Vec<VaultShare>,
pub lending: Vec<Reserve>,
pub health: Option<LendingHealth>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct VaultShare {
pub vault: Address,
pub equity: Dec,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Reserve {
pub token: u32,
pub supplied: Dec,
pub borrowed: Dec,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LendingHealth {
pub word: String,
pub factor: Option<Dec>,
}
impl Beside {
#[must_use]
pub fn lending_health(&self) -> Option<Health> {
let health = self.health.as_ref()?;
if health.factor.is_none() && health.word == "healthy" {
return None;
}
let risk = match health.word.as_str() {
"healthy" => Risk::Safe,
"liquidatable" => Risk::Liquidatable,
_ => Risk::Near,
};
Some(Health { factor: health.factor, risk, word: capitals(&health.word) })
}
}
fn capitals(camel: &str) -> String {
let mut out = String::new();
for letter in camel.chars() {
if letter.is_ascii_uppercase() && !out.is_empty() {
out.push(' ');
}
out.push(letter.to_ascii_uppercase());
}
out
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Worth {
pub perps: Dec,
pub spot: Dec,
pub lending: Dec,
pub outcomes: Dec,
pub staking: Dec,
pub vaults: Dec,
pub margin: Dec,
}
impl Worth {
#[must_use]
pub fn total(&self) -> Option<Dec> {
Dec::checked_sum([self.perps, self.spot, self.lending, self.outcomes, self.staking, self.vaults])
}
#[must_use]
pub fn plus(&self, other: &Worth) -> Option<Worth> {
Some(Worth {
perps: self.perps.checked_add(other.perps)?,
spot: self.spot.checked_add(other.spot)?,
lending: self.lending.checked_add(other.lending)?,
outcomes: self.outcomes.checked_add(other.outcomes)?,
staking: self.staking.checked_add(other.staking)?,
vaults: self.vaults.checked_add(other.vaults)?,
margin: self.margin.checked_add(other.margin)?,
})
}
}
impl Standing {
#[must_use]
pub fn worth(&self, prices: &Prices, beside: &Beside) -> Option<Worth> {
let mut worth = Worth::default();
for dex in &self.dexes {
worth.perps = worth.perps.checked_add(dex.state.margin_summary.account_value)?;
worth.margin = worth.margin.checked_add(dex.state.margin_summary.total_margin_used)?;
}
let mut spot = Dec::ZERO;
for row in self.spot.iter().flat_map(|spot| &spot.balances) {
let Some(price) = prices.price(&row.coin) else { continue };
let value = row.total.checked_mul(price)?;
if row.is_outcome() {
worth.outcomes = worth.outcomes.checked_add(value)?;
continue;
}
spot = spot.checked_add(value)?;
let lent = row.supplied.unwrap_or(Dec::ZERO).checked_mul(price)?;
let owed = row.borrowed.unwrap_or(Dec::ZERO).checked_mul(price)?;
worth.lending = worth.lending.checked_add(lent)?.checked_sub(owed)?;
}
spot = spot.checked_sub(worth.lending)?;
if self.pools() {
spot = spot.checked_sub(worth.perps)?;
}
worth.spot = spot;
worth.staking = match beside.staked.is_zero() {
true => Dec::ZERO,
false => beside.staked.checked_mul(prices.price(STAKED).unwrap_or(Dec::ZERO))?,
};
worth.vaults = Dec::checked_sum(beside.vaults.iter().map(|share| share.equity))?;
Some(worth)
}
}
#[must_use]
pub fn combined(accounts: &[(Address, &Standing, &Beside)], prices: &Prices) -> Option<Worth> {
let listed: BTreeSet<Address> = accounts.iter().map(|(address, _, _)| *address).collect();
let mut worth = Worth::default();
for (_, standing, beside) in accounts {
worth = worth.plus(&standing.worth(prices, beside)?)?;
for share in beside.vaults.iter().filter(|share| listed.contains(&share.vault)) {
worth.vaults = worth.vaults.checked_sub(share.equity)?;
}
}
Some(worth)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Holding {
pub coin: String,
pub token: Option<u32>,
pub amount: Dec,
pub price: Option<Dec>,
pub place: Option<Address>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Health {
pub factor: Option<Dec>,
pub risk: Risk,
pub word: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Holdings {
pub tokens: Vec<Holding>,
pub staked: Vec<Holding>,
pub vaults: Vec<Holding>,
pub supplied: Vec<Holding>,
pub borrowed: Vec<Holding>,
pub health: Option<Health>,
}
impl Holdings {
#[must_use]
pub fn worth(&self) -> Option<Dec> {
self.tokens.iter().chain(&self.staked).chain(&self.vaults).try_fold(Dec::ZERO, |sum, held| match held.price {
Some(price) => sum.checked_add(held.amount.checked_mul(price)?),
None => Some(sum),
})
}
}
#[must_use]
pub fn holdings(
standing: &Standing,
collateral: &Collateral,
beside: &Beside,
names: &BTreeMap<u32, String>,
prices: &Prices,
) -> Holdings {
let counted = collateral.tokens();
let held = |coin: &str, token: Option<u32>, amount: Dec| Holding {
coin: coin.to_owned(),
token,
amount,
price: prices.price(coin),
place: None,
};
let tokens = standing
.spot
.iter()
.flat_map(|spot: &SpotState| &spot.balances)
.filter(|row| !row.is_outcome() && !row.total.is_zero())
.filter(|row| row.token.is_some_and(|token| !counted.contains(&token)))
.map(|row| held(&row.coin, row.token, row.total))
.collect();
let index_of = |name: &str| names.iter().find(|(_, known)| known.as_str() == name).map(|(index, _)| *index);
let staked = match beside.staked.is_positive() {
true => vec![held(STAKED, index_of(STAKED), beside.staked)],
false => Vec::new(),
};
let vaults = beside
.vaults
.iter()
.filter(|share| share.equity.is_positive())
.map(|share| Holding { place: Some(share.vault), ..held("USDC", Some(USDC), share.equity) })
.collect();
let name = |token: u32| names.get(&token).cloned().unwrap_or_else(|| format!("#{token}"));
let mut supplied = Vec::new();
let mut borrowed = Vec::new();
for reserve in &beside.lending {
if reserve.supplied.is_positive() {
supplied.push(held(&name(reserve.token), Some(reserve.token), reserve.supplied));
}
if reserve.borrowed.is_positive() {
borrowed.push(held(&name(reserve.token), Some(reserve.token), reserve.borrowed));
}
}
Holdings { tokens, staked, vaults, supplied, borrowed, health: beside.lending_health() }
}
#[cfg(test)]
#[path = "tests/worth.rs"]
mod tests;