df_hyperliquid_core/account/holdings/
usable.rs1use std::collections::BTreeMap;
6
7use crate::account::holdings::keeping::Keeping;
8use crate::account::holdings::spot::Risk;
9use crate::account::holdings::standing::Standing;
10use crate::client::USDC_INDEX;
11use crate::utils::Dec;
12
13pub const LTV: [(&str, i128); 2] = [("HYPE", 65), ("UBTC", 50)];
17
18#[derive(Debug, Clone, Default, PartialEq, Eq)]
21pub struct Collateral(BTreeMap<String, u32>);
22
23impl Collateral {
24 #[must_use]
25 pub fn new() -> Collateral {
26 Collateral::default()
27 }
28
29 #[must_use]
30 pub fn with(mut self, dex: &str, token: u32) -> Collateral {
31 self.0.insert(dex.to_owned(), token);
32 self
33 }
34
35 #[must_use]
37 pub fn of(&self, dex: &str) -> u32 {
38 self.0.get(dex).copied().unwrap_or(USDC_INDEX)
39 }
40
41 #[must_use]
43 pub fn tokens(&self) -> Vec<u32> {
44 let mut tokens = vec![USDC_INDEX];
45 for token in self.0.values() {
46 if !tokens.contains(token) {
47 tokens.push(*token);
48 }
49 }
50 tokens[1..].sort_unstable();
51 tokens
52 }
53}
54
55impl FromIterator<(String, u32)> for Collateral {
56 fn from_iter<I: IntoIterator<Item = (String, u32)>>(iter: I) -> Collateral {
57 Collateral(iter.into_iter().collect())
58 }
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
63pub enum Surface<'a> {
64 Perps { dex: &'a str },
66 Spot { quote: u32 },
68 Outcomes,
70}
71
72impl Surface<'_> {
73 #[must_use]
75 pub fn token(&self, collateral: &Collateral) -> u32 {
76 match self {
77 Surface::Perps { dex } => collateral.of(dex),
78 Surface::Spot { quote } => *quote,
79 Surface::Outcomes => USDC_INDEX,
80 }
81 }
82}
83
84#[derive(Debug, Clone, PartialEq, Eq, Hash)]
86pub enum Pays {
87 Perps { dex: String },
89 UsdcPerps,
91 Spot,
93 Any,
95}
96
97#[derive(Debug, Clone, PartialEq, Eq)]
100pub struct Balance {
101 pub token: u32,
102 pub total: Dec,
104 pub available: Dec,
106 pub pays: Pays,
107}
108
109#[must_use]
112pub fn payer<'a>(balances: &'a [Balance], surface: Surface<'_>, collateral: &Collateral) -> Option<&'a Balance> {
113 let token = surface.token(collateral);
114 balances.iter().find(|balance| {
115 balance.token == token
116 && match (&balance.pays, surface) {
117 (Pays::Any, _) => true,
118 (Pays::Spot, Surface::Spot { .. } | Surface::Outcomes) => true,
119 (Pays::Perps { dex }, Surface::Perps { dex: wanted }) => dex == wanted,
120 (Pays::UsdcPerps, Surface::Perps { .. }) => token == USDC_INDEX,
121 _ => false,
122 }
123 })
124}
125
126#[derive(Debug, Clone, PartialEq, Eq, Hash)]
128pub enum Source {
129 Perps {
131 dex: String,
132 },
133 NativePerps,
135 Spot,
136 Unified,
137 PortfolioMargin,
138}
139
140#[derive(Debug, Clone, PartialEq, Eq)]
142pub struct Usable {
143 pub amount: Dec,
144 pub token: u32,
146 pub from: Source,
147 pub borrowable: Dec,
149}
150
151impl Standing {
152 #[must_use]
156 pub fn balances(&self, collateral: &Collateral) -> Vec<Balance> {
157 let keeping = self.keeping();
158 let mut out = Vec::new();
159 if !keeping.pools() {
160 for dex in &self.dexes {
161 let token = collateral.of(&dex.dex);
162 let pays = match (keeping, dex.dex.is_empty(), token == USDC_INDEX) {
163 (Keeping::Native, true, _) => Pays::UsdcPerps,
164 (Keeping::Native, false, true) => continue,
165 _ => Pays::Perps { dex: dex.dex.clone() },
166 };
167 let summary = &dex.state.margin_summary;
168 out.push(Balance { token, total: summary.account_value, available: dex.state.withdrawable, pays });
169 }
170 }
171 let pays = match keeping.pools() {
172 true => Pays::Any,
173 false => Pays::Spot,
174 };
175 let counted = collateral.tokens();
176 let rows = self.spot.iter().flat_map(|spot| &spot.balances).filter(|row| !row.is_outcome());
177 let mut usdc = false;
178 for row in rows {
179 let Some(token) = row.token.filter(|token| counted.contains(token)) else { continue };
180 let available = row.available().unwrap_or(Dec::ZERO);
181 if token != USDC_INDEX && row.total.is_zero() && available.is_zero() {
182 continue;
183 }
184 usdc |= token == USDC_INDEX;
185 out.push(Balance { token, total: row.total, available, pays: pays.clone() });
186 }
187 if !usdc {
188 let none = Balance { token: USDC_INDEX, total: Dec::ZERO, available: Dec::ZERO, pays };
189 let at = out.iter().position(|balance| balance.pays == Pays::Spot || balance.pays == Pays::Any);
190 out.insert(at.unwrap_or(out.len()), none);
191 }
192 out
193 }
194
195 #[must_use]
199 pub fn usable(&self, surface: Surface<'_>, collateral: &Collateral, price: impl Fn(&str) -> Option<Dec>) -> Usable {
200 let keeping = self.keeping();
201 let token = surface.token(collateral);
202 let balances = self.balances(collateral);
203 let paying = payer(&balances, surface, collateral);
204 let from = match (keeping, paying.map(|balance| &balance.pays), surface) {
205 (Keeping::Unified, _, _) => Source::Unified,
206 (Keeping::Portfolio, _, _) => Source::PortfolioMargin,
207 (_, Some(Pays::UsdcPerps), Surface::Perps { dex }) if !dex.is_empty() => Source::NativePerps,
208 (_, _, Surface::Perps { dex }) => Source::Perps { dex: dex.to_owned() },
209 (_, _, Surface::Spot { .. } | Surface::Outcomes) => Source::Spot,
210 };
211 let free = paying.map_or(Dec::ZERO, |balance| balance.available);
212 let borrowable = match (keeping, token == USDC_INDEX) {
213 (Keeping::Portfolio, true) => self.borrowable(&price),
214 _ => Dec::ZERO,
215 };
216 let amount = free.checked_add(borrowable).unwrap_or(free);
217 Usable { amount, token, from, borrowable }
218 }
219
220 #[must_use]
225 pub fn borrowable(&self, price: &impl Fn(&str) -> Option<Dec>) -> Dec {
226 let Some(spot) = self.spot.as_ref().filter(|_| self.keeping() == Keeping::Portfolio) else {
227 return Dec::ZERO;
228 };
229 if spot.risk() == Risk::Liquidatable {
230 return Dec::ZERO;
231 }
232 let rows = || spot.balances.iter().filter(|row| !row.is_outcome());
233 let room = rows()
234 .filter(|row| row.token != Some(USDC_INDEX))
235 .filter_map(|row| {
236 let ltv = row.ltv.or_else(|| {
237 LTV.iter().find(|(coin, _)| *coin == row.coin).map(|(_, hundredths)| Dec::lit::<2>(*hundredths))
238 })?;
239 row.total.checked_mul(price(&row.coin)?)?.checked_mul(ltv).filter(|lent| lent.is_positive())
240 })
241 .fold(Dec::ZERO, |sum, lent| sum.checked_add(lent).unwrap_or(sum));
242 let owed =
243 rows().filter_map(|row| row.borrowed).fold(Dec::ZERO, |sum, owed| sum.checked_add(owed).unwrap_or(sum));
244 let mut out = room.checked_sub(owed).unwrap_or(Dec::ZERO).max(Dec::ZERO);
245 if let Some(after) = spot.available_after_maintenance(USDC_INDEX) {
246 out = out.min(after.max(Dec::ZERO));
247 }
248 out
249 }
250}
251
252#[cfg(test)]
253#[path = "tests/usable.rs"]
254mod tests;