use std::collections::{BTreeMap, BTreeSet};
use crate::account::holdings::standing::Standing;
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>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct VaultShare {
pub vault: Address,
pub equity: Dec,
}
#[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)
}
#[cfg(test)]
#[path = "tests/worth.rs"]
mod tests;