df_hyperliquid_core/account/
worth.rs1use std::collections::{BTreeMap, BTreeSet};
6
7use crate::account::state::{Risk, SpotState};
8use crate::account::usable::{Collateral, Standing, USDC};
9use crate::utils::{Address, Dec};
10
11const STAKED: &str = "HYPE";
13
14#[derive(Debug, Clone, Default, PartialEq, Eq)]
16pub struct Prices(BTreeMap<String, Dec>);
17
18impl Prices {
19 #[must_use]
20 pub fn new() -> Prices {
21 Prices::default()
22 }
23
24 #[must_use]
26 pub fn with(mut self, name: &str, price: Dec) -> Prices {
27 self.0.insert(name.to_owned(), price);
28 self
29 }
30
31 #[must_use]
36 pub fn of(pairs: &BTreeMap<String, String>, mids: &BTreeMap<String, Dec>) -> Prices {
37 let priced = |mid: &Dec| Some(*mid).filter(|price| price.is_positive());
38 let mut prices: BTreeMap<String, Dec> = mids
39 .iter()
40 .filter(|(name, _)| name.starts_with('#'))
41 .filter_map(|(name, mid)| Some((name.clone(), priced(mid)?)))
42 .collect();
43 for (token, pair) in pairs {
44 if let Some(price) = mids.get(pair).and_then(priced) {
45 prices.insert(token.clone(), price);
46 }
47 }
48 Prices(prices)
49 }
50
51 #[must_use]
54 pub fn price(&self, coin: &str) -> Option<Dec> {
55 if coin == "USDC" {
56 return Some(Dec::ONE);
57 }
58 match coin.strip_prefix('+') {
59 Some(side) => self.0.get(&format!("#{side}")).copied(),
60 None => self.0.get(coin).copied(),
61 }
62 }
63}
64
65#[derive(Debug, Clone, Default, PartialEq, Eq)]
67pub struct Beside {
68 pub staked: Dec,
70 pub vaults: Vec<VaultShare>,
72 pub lending: Vec<Reserve>,
74 pub health: Option<LendingHealth>,
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub struct VaultShare {
79 pub vault: Address,
80 pub equity: Dec,
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub struct Reserve {
87 pub token: u32,
88 pub supplied: Dec,
89 pub borrowed: Dec,
90}
91
92#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct LendingHealth {
95 pub word: String,
97 pub factor: Option<Dec>,
98}
99
100impl Beside {
101 #[must_use]
104 pub fn lending_health(&self) -> Option<Health> {
105 let health = self.health.as_ref()?;
106 if health.factor.is_none() && health.word == "healthy" {
107 return None;
108 }
109 let risk = match health.word.as_str() {
110 "healthy" => Risk::Safe,
111 "liquidatable" => Risk::Liquidatable,
112 _ => Risk::Near,
113 };
114 Some(Health { factor: health.factor, risk, word: capitals(&health.word) })
115 }
116}
117
118fn capitals(camel: &str) -> String {
120 let mut out = String::new();
121 for letter in camel.chars() {
122 if letter.is_ascii_uppercase() && !out.is_empty() {
123 out.push(' ');
124 }
125 out.push(letter.to_ascii_uppercase());
126 }
127 out
128}
129
130#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
132pub struct Worth {
133 pub perps: Dec,
135 pub spot: Dec,
138 pub lending: Dec,
140 pub outcomes: Dec,
142 pub staking: Dec,
143 pub vaults: Dec,
144 pub margin: Dec,
146}
147
148impl Worth {
149 #[must_use]
151 pub fn total(&self) -> Option<Dec> {
152 Dec::checked_sum([self.perps, self.spot, self.lending, self.outcomes, self.staking, self.vaults])
153 }
154
155 #[must_use]
156 pub fn plus(&self, other: &Worth) -> Option<Worth> {
157 Some(Worth {
158 perps: self.perps.checked_add(other.perps)?,
159 spot: self.spot.checked_add(other.spot)?,
160 lending: self.lending.checked_add(other.lending)?,
161 outcomes: self.outcomes.checked_add(other.outcomes)?,
162 staking: self.staking.checked_add(other.staking)?,
163 vaults: self.vaults.checked_add(other.vaults)?,
164 margin: self.margin.checked_add(other.margin)?,
165 })
166 }
167}
168
169impl Standing {
170 #[must_use]
173 pub fn worth(&self, prices: &Prices, beside: &Beside) -> Option<Worth> {
174 let mut worth = Worth::default();
175 for dex in &self.dexes {
176 worth.perps = worth.perps.checked_add(dex.state.margin_summary.account_value)?;
177 worth.margin = worth.margin.checked_add(dex.state.margin_summary.total_margin_used)?;
178 }
179 let mut spot = Dec::ZERO;
180 for row in self.spot.iter().flat_map(|spot| &spot.balances) {
181 let Some(price) = prices.price(&row.coin) else { continue };
182 let value = row.total.checked_mul(price)?;
183 if row.is_outcome() {
184 worth.outcomes = worth.outcomes.checked_add(value)?;
185 continue;
186 }
187 spot = spot.checked_add(value)?;
188 let lent = row.supplied.unwrap_or(Dec::ZERO).checked_mul(price)?;
189 let owed = row.borrowed.unwrap_or(Dec::ZERO).checked_mul(price)?;
190 worth.lending = worth.lending.checked_add(lent)?.checked_sub(owed)?;
191 }
192 spot = spot.checked_sub(worth.lending)?;
193 if self.pools() {
194 spot = spot.checked_sub(worth.perps)?;
195 }
196 worth.spot = spot;
197 worth.staking = match beside.staked.is_zero() {
198 true => Dec::ZERO,
199 false => beside.staked.checked_mul(prices.price(STAKED).unwrap_or(Dec::ZERO))?,
200 };
201 worth.vaults = Dec::checked_sum(beside.vaults.iter().map(|share| share.equity))?;
202 Some(worth)
203 }
204}
205
206#[must_use]
210pub fn combined(accounts: &[(Address, &Standing, &Beside)], prices: &Prices) -> Option<Worth> {
211 let listed: BTreeSet<Address> = accounts.iter().map(|(address, _, _)| *address).collect();
212 let mut worth = Worth::default();
213 for (_, standing, beside) in accounts {
214 worth = worth.plus(&standing.worth(prices, beside)?)?;
215 for share in beside.vaults.iter().filter(|share| listed.contains(&share.vault)) {
216 worth.vaults = worth.vaults.checked_sub(share.equity)?;
217 }
218 }
219 Some(worth)
220}
221
222#[derive(Debug, Clone, PartialEq, Eq)]
224pub struct Holding {
225 pub coin: String,
226 pub token: Option<u32>,
227 pub amount: Dec,
228 pub price: Option<Dec>,
229 pub place: Option<Address>,
231}
232
233#[derive(Debug, Clone, PartialEq, Eq)]
235pub struct Health {
236 pub factor: Option<Dec>,
237 pub risk: Risk,
238 pub word: String,
240}
241
242#[derive(Debug, Clone, Default, PartialEq, Eq)]
246pub struct Holdings {
247 pub tokens: Vec<Holding>,
249 pub staked: Vec<Holding>,
250 pub vaults: Vec<Holding>,
251 pub supplied: Vec<Holding>,
252 pub borrowed: Vec<Holding>,
253 pub health: Option<Health>,
254}
255
256impl Holdings {
257 #[must_use]
260 pub fn worth(&self) -> Option<Dec> {
261 self.tokens.iter().chain(&self.staked).chain(&self.vaults).try_fold(Dec::ZERO, |sum, held| match held.price {
262 Some(price) => sum.checked_add(held.amount.checked_mul(price)?),
263 None => Some(sum),
264 })
265 }
266}
267
268#[must_use]
271pub fn holdings(
272 standing: &Standing,
273 collateral: &Collateral,
274 beside: &Beside,
275 names: &BTreeMap<u32, String>,
276 prices: &Prices,
277) -> Holdings {
278 let counted = collateral.tokens();
279 let held = |coin: &str, token: Option<u32>, amount: Dec| Holding {
280 coin: coin.to_owned(),
281 token,
282 amount,
283 price: prices.price(coin),
284 place: None,
285 };
286 let tokens = standing
287 .spot
288 .iter()
289 .flat_map(|spot: &SpotState| &spot.balances)
290 .filter(|row| !row.is_outcome() && !row.total.is_zero())
291 .filter(|row| row.token.is_some_and(|token| !counted.contains(&token)))
292 .map(|row| held(&row.coin, row.token, row.total))
293 .collect();
294 let index_of = |name: &str| names.iter().find(|(_, known)| known.as_str() == name).map(|(index, _)| *index);
295 let staked = match beside.staked.is_positive() {
296 true => vec![held(STAKED, index_of(STAKED), beside.staked)],
297 false => Vec::new(),
298 };
299 let vaults = beside
300 .vaults
301 .iter()
302 .filter(|share| share.equity.is_positive())
303 .map(|share| Holding { place: Some(share.vault), ..held("USDC", Some(USDC), share.equity) })
304 .collect();
305 let name = |token: u32| names.get(&token).cloned().unwrap_or_else(|| format!("#{token}"));
306 let mut supplied = Vec::new();
307 let mut borrowed = Vec::new();
308 for reserve in &beside.lending {
309 if reserve.supplied.is_positive() {
310 supplied.push(held(&name(reserve.token), Some(reserve.token), reserve.supplied));
311 }
312 if reserve.borrowed.is_positive() {
313 borrowed.push(held(&name(reserve.token), Some(reserve.token), reserve.borrowed));
314 }
315 }
316 Holdings { tokens, staked, vaults, supplied, borrowed, health: beside.lending_health() }
317}
318
319#[cfg(test)]
320#[path = "tests/worth.rs"]
321mod tests;