use std::collections::BTreeMap;
use crate::account::holdings::keeping::Keeping;
use crate::account::holdings::spot::Risk;
use crate::account::holdings::standing::Standing;
use crate::client::USDC_INDEX;
use crate::utils::Dec;
pub const LTV: [(&str, i128); 2] = [("HYPE", 65), ("UBTC", 50)];
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Collateral(BTreeMap<String, u32>);
impl Collateral {
#[must_use]
pub fn new() -> Collateral {
Collateral::default()
}
#[must_use]
pub fn with(mut self, dex: &str, token: u32) -> Collateral {
self.0.insert(dex.to_owned(), token);
self
}
#[must_use]
pub fn of(&self, dex: &str) -> u32 {
self.0.get(dex).copied().unwrap_or(USDC_INDEX)
}
#[must_use]
pub fn tokens(&self) -> Vec<u32> {
let mut tokens = vec![USDC_INDEX];
for token in self.0.values() {
if !tokens.contains(token) {
tokens.push(*token);
}
}
tokens[1..].sort_unstable();
tokens
}
}
impl FromIterator<(String, u32)> for Collateral {
fn from_iter<I: IntoIterator<Item = (String, u32)>>(iter: I) -> Collateral {
Collateral(iter.into_iter().collect())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Surface<'a> {
Perps { dex: &'a str },
Spot { quote: u32 },
Outcomes,
}
impl Surface<'_> {
#[must_use]
pub fn token(&self, collateral: &Collateral) -> u32 {
match self {
Surface::Perps { dex } => collateral.of(dex),
Surface::Spot { quote } => *quote,
Surface::Outcomes => USDC_INDEX,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Pays {
Perps { dex: String },
UsdcPerps,
Spot,
Any,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Balance {
pub token: u32,
pub total: Dec,
pub available: Dec,
pub pays: Pays,
}
#[must_use]
pub fn payer<'a>(balances: &'a [Balance], surface: Surface<'_>, collateral: &Collateral) -> Option<&'a Balance> {
let token = surface.token(collateral);
balances.iter().find(|balance| {
balance.token == token
&& match (&balance.pays, surface) {
(Pays::Any, _) => true,
(Pays::Spot, Surface::Spot { .. } | Surface::Outcomes) => true,
(Pays::Perps { dex }, Surface::Perps { dex: wanted }) => dex == wanted,
(Pays::UsdcPerps, Surface::Perps { .. }) => token == USDC_INDEX,
_ => false,
}
})
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Source {
Perps {
dex: String,
},
NativePerps,
Spot,
Unified,
PortfolioMargin,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Usable {
pub amount: Dec,
pub token: u32,
pub from: Source,
pub borrowable: Dec,
}
impl Standing {
#[must_use]
pub fn balances(&self, collateral: &Collateral) -> Vec<Balance> {
let keeping = self.keeping();
let mut out = Vec::new();
if !keeping.pools() {
for dex in &self.dexes {
let token = collateral.of(&dex.dex);
let pays = match (keeping, dex.dex.is_empty(), token == USDC_INDEX) {
(Keeping::Native, true, _) => Pays::UsdcPerps,
(Keeping::Native, false, true) => continue,
_ => Pays::Perps { dex: dex.dex.clone() },
};
let summary = &dex.state.margin_summary;
out.push(Balance { token, total: summary.account_value, available: dex.state.withdrawable, pays });
}
}
let pays = match keeping.pools() {
true => Pays::Any,
false => Pays::Spot,
};
let counted = collateral.tokens();
let rows = self.spot.iter().flat_map(|spot| &spot.balances).filter(|row| !row.is_outcome());
let mut usdc = false;
for row in rows {
let Some(token) = row.token.filter(|token| counted.contains(token)) else { continue };
let available = row.available().unwrap_or(Dec::ZERO);
if token != USDC_INDEX && row.total.is_zero() && available.is_zero() {
continue;
}
usdc |= token == USDC_INDEX;
out.push(Balance { token, total: row.total, available, pays: pays.clone() });
}
if !usdc {
let none = Balance { token: USDC_INDEX, total: Dec::ZERO, available: Dec::ZERO, pays };
let at = out.iter().position(|balance| balance.pays == Pays::Spot || balance.pays == Pays::Any);
out.insert(at.unwrap_or(out.len()), none);
}
out
}
#[must_use]
pub fn usable(&self, surface: Surface<'_>, collateral: &Collateral, price: impl Fn(&str) -> Option<Dec>) -> Usable {
let keeping = self.keeping();
let token = surface.token(collateral);
let balances = self.balances(collateral);
let paying = payer(&balances, surface, collateral);
let from = match (keeping, paying.map(|balance| &balance.pays), surface) {
(Keeping::Unified, _, _) => Source::Unified,
(Keeping::Portfolio, _, _) => Source::PortfolioMargin,
(_, Some(Pays::UsdcPerps), Surface::Perps { dex }) if !dex.is_empty() => Source::NativePerps,
(_, _, Surface::Perps { dex }) => Source::Perps { dex: dex.to_owned() },
(_, _, Surface::Spot { .. } | Surface::Outcomes) => Source::Spot,
};
let free = paying.map_or(Dec::ZERO, |balance| balance.available);
let borrowable = match (keeping, token == USDC_INDEX) {
(Keeping::Portfolio, true) => self.borrowable(&price),
_ => Dec::ZERO,
};
let amount = free.checked_add(borrowable).unwrap_or(free);
Usable { amount, token, from, borrowable }
}
#[must_use]
pub fn borrowable(&self, price: &impl Fn(&str) -> Option<Dec>) -> Dec {
let Some(spot) = self.spot.as_ref().filter(|_| self.keeping() == Keeping::Portfolio) else {
return Dec::ZERO;
};
if spot.risk() == Risk::Liquidatable {
return Dec::ZERO;
}
let rows = || spot.balances.iter().filter(|row| !row.is_outcome());
let room = rows()
.filter(|row| row.token != Some(USDC_INDEX))
.filter_map(|row| {
let ltv = row.ltv.or_else(|| {
LTV.iter().find(|(coin, _)| *coin == row.coin).map(|(_, hundredths)| Dec::lit::<2>(*hundredths))
})?;
row.total.checked_mul(price(&row.coin)?)?.checked_mul(ltv).filter(|lent| lent.is_positive())
})
.fold(Dec::ZERO, |sum, lent| sum.checked_add(lent).unwrap_or(sum));
let owed =
rows().filter_map(|row| row.borrowed).fold(Dec::ZERO, |sum, owed| sum.checked_add(owed).unwrap_or(sum));
let mut out = room.checked_sub(owed).unwrap_or(Dec::ZERO).max(Dec::ZERO);
if let Some(after) = spot.available_after_maintenance(USDC_INDEX) {
out = out.min(after.max(Dec::ZERO));
}
out
}
}
#[cfg(test)]
#[path = "tests/usable.rs"]
mod tests;