Serum: Health computation first steps
This commit is contained in:
parent
f5d2964f1d
commit
6aa4724b45
|
@ -1,11 +1,12 @@
|
||||||
use anchor_lang::prelude::*;
|
use anchor_lang::prelude::*;
|
||||||
use fixed::types::I80F48;
|
use fixed::types::I80F48;
|
||||||
use pyth_client::load_price;
|
use std::cell::Ref;
|
||||||
|
|
||||||
use crate::error::MangoError;
|
use crate::error::MangoError;
|
||||||
use crate::state::{determine_oracle_type, Bank, MangoAccount, OracleType, StubOracle};
|
use crate::state::{oracle_price, Bank, MangoAccount, TokenIndex};
|
||||||
use crate::util;
|
use crate::util;
|
||||||
use crate::util::checked_math as cm;
|
use crate::util::checked_math as cm;
|
||||||
|
use crate::util::LoadZeroCopy;
|
||||||
|
|
||||||
pub fn compute_health(account: &MangoAccount, ais: &[AccountInfo]) -> Result<I80F48> {
|
pub fn compute_health(account: &MangoAccount, ais: &[AccountInfo]) -> Result<I80F48> {
|
||||||
let active_token_len = account.token_account_map.iter_active().count();
|
let active_token_len = account.token_account_map.iter_active().count();
|
||||||
|
@ -20,23 +21,44 @@ pub fn compute_health(account: &MangoAccount, ais: &[AccountInfo]) -> Result<I80
|
||||||
compute_health_detail(account, banks, oracles, serum_oos)
|
compute_health_detail(account, banks, oracles, serum_oos)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct BankAndPrice<'a> {
|
||||||
|
bank: Ref<'a, Bank>,
|
||||||
|
price: I80F48,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_price(token_index: TokenIndex, banks_and_prices: &[BankAndPrice]) -> Result<I80F48> {
|
||||||
|
Ok(banks_and_prices
|
||||||
|
.iter()
|
||||||
|
.find(|b| b.bank.token_index == token_index)
|
||||||
|
.ok_or(error!(MangoError::SomeError))?
|
||||||
|
.price)
|
||||||
|
}
|
||||||
|
|
||||||
fn compute_health_detail(
|
fn compute_health_detail(
|
||||||
account: &MangoAccount,
|
account: &MangoAccount,
|
||||||
banks: &[AccountInfo],
|
banks: &[AccountInfo],
|
||||||
oracles: &[AccountInfo],
|
oracles: &[AccountInfo],
|
||||||
_serum_oos: &[AccountInfo],
|
serum_oos: &[AccountInfo],
|
||||||
) -> Result<I80F48> {
|
) -> Result<I80F48> {
|
||||||
let mut assets = I80F48::ZERO;
|
let mut assets = I80F48::ZERO;
|
||||||
let mut liabilities = I80F48::ZERO; // absolute value
|
let mut liabilities = I80F48::ZERO; // absolute value
|
||||||
for (position, (bank_ai, oracle_ai)) in util::zip!(
|
|
||||||
account.token_account_map.iter_active(),
|
|
||||||
banks.iter(),
|
|
||||||
oracles.iter()
|
|
||||||
) {
|
|
||||||
let bank_loader = AccountLoader::<'_, Bank>::try_from(bank_ai)?;
|
|
||||||
let bank = bank_loader.load()?;
|
|
||||||
|
|
||||||
// TODO: This assumes banks are passed in order - is that an ok assumption?
|
// collect the bank and oracle data once
|
||||||
|
let banks_and_prices = util::zip!(banks.iter(), oracles.iter())
|
||||||
|
.map(|(bank_ai, oracle_ai)| {
|
||||||
|
let bank = bank_ai.load::<Bank>()?;
|
||||||
|
require!(bank.oracle == oracle_ai.key(), MangoError::UnexpectedOracle);
|
||||||
|
let price = oracle_price(oracle_ai)?;
|
||||||
|
Ok(BankAndPrice { bank, price })
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<BankAndPrice>>>()?;
|
||||||
|
|
||||||
|
// health contribution from token accounts
|
||||||
|
for (position, BankAndPrice { bank, price }) in util::zip!(
|
||||||
|
account.token_account_map.iter_active(),
|
||||||
|
banks_and_prices.iter()
|
||||||
|
) {
|
||||||
|
// This assumes banks are passed in order
|
||||||
require!(
|
require!(
|
||||||
bank.token_index == position.token_index,
|
bank.token_index == position.token_index,
|
||||||
MangoError::SomeError
|
MangoError::SomeError
|
||||||
|
@ -44,22 +66,7 @@ fn compute_health_detail(
|
||||||
|
|
||||||
// converts the token value to the basis token value for health computations
|
// converts the token value to the basis token value for health computations
|
||||||
// TODO: health basis token == USDC?
|
// TODO: health basis token == USDC?
|
||||||
let oracle_data = &oracle_ai.try_borrow_data()?;
|
let price = *price;
|
||||||
let oracle_type = determine_oracle_type(oracle_data)?;
|
|
||||||
require!(bank.oracle == oracle_ai.key(), MangoError::UnexpectedOracle);
|
|
||||||
|
|
||||||
let price = match oracle_type {
|
|
||||||
OracleType::Stub => {
|
|
||||||
AccountLoader::<'_, StubOracle>::try_from(oracle_ai)?
|
|
||||||
.load()?
|
|
||||||
.price
|
|
||||||
}
|
|
||||||
OracleType::Pyth => {
|
|
||||||
let price_struct = load_price(&oracle_data).unwrap();
|
|
||||||
I80F48::from_num(price_struct.agg.price)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let native_position = position.native(&bank);
|
let native_position = position.native(&bank);
|
||||||
let native_basis = cm!(native_position * price);
|
let native_basis = cm!(native_position * price);
|
||||||
if native_basis.is_positive() {
|
if native_basis.is_positive() {
|
||||||
|
@ -69,11 +76,21 @@ fn compute_health_detail(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Serum open orders
|
// health contribution from serum accounts
|
||||||
// - for each active serum market, pass the OpenOrders in order
|
for (serum_account, oo_ai) in
|
||||||
// - store the base_token_index and quote_token_index in the account, so we don't
|
util::zip!(account.serum_account_map.iter_active(), serum_oos.iter())
|
||||||
// need to also pass SerumMarket
|
{
|
||||||
// - find the bank and oracle for base and quote, and add appropriately
|
// This assumes serum open orders are passed in order
|
||||||
|
require!(
|
||||||
|
&serum_account.open_orders == oo_ai.key,
|
||||||
|
MangoError::SomeError
|
||||||
|
);
|
||||||
|
|
||||||
|
// find the prices for the market
|
||||||
|
// TODO: each of these is a linear scan through banks_and_prices - is that too expensive?
|
||||||
|
let _base_price = find_price(serum_account.base_token_index, &banks_and_prices)?;
|
||||||
|
let _quote_price = find_price(serum_account.quote_token_index, &banks_and_prices)?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(cm!(assets - liabilities))
|
Ok(cm!(assets - liabilities))
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ use anchor_lang::Discriminator;
|
||||||
use fixed::types::I80F48;
|
use fixed::types::I80F48;
|
||||||
|
|
||||||
use crate::error::MangoError;
|
use crate::error::MangoError;
|
||||||
|
use crate::util::LoadZeroCopy;
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
pub enum OracleType {
|
pub enum OracleType {
|
||||||
|
@ -26,6 +27,19 @@ pub fn determine_oracle_type(data: &[u8]) -> Result<OracleType> {
|
||||||
Err(MangoError::UnknownOracleType.into())
|
Err(MangoError::UnknownOracleType.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn oracle_price(acc_info: &AccountInfo) -> Result<I80F48> {
|
||||||
|
let data = &acc_info.try_borrow_data()?;
|
||||||
|
let oracle_type = determine_oracle_type(data)?;
|
||||||
|
|
||||||
|
Ok(match oracle_type {
|
||||||
|
OracleType::Stub => acc_info.load::<StubOracle>()?.price,
|
||||||
|
OracleType::Pyth => {
|
||||||
|
let price_struct = pyth_client::load_price(&data).unwrap();
|
||||||
|
I80F48::from_num(price_struct.agg.price)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
use anchor_lang::prelude::*;
|
||||||
|
use anchor_lang::ZeroCopy;
|
||||||
|
use arrayref::array_ref;
|
||||||
|
use std::{cell::Ref, mem};
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! zip {
|
macro_rules! zip {
|
||||||
($x: expr) => ($x);
|
($x: expr) => ($x);
|
||||||
|
@ -15,3 +20,44 @@ macro_rules! checked_math {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
pub(crate) use checked_math;
|
pub(crate) use checked_math;
|
||||||
|
|
||||||
|
pub trait LoadZeroCopy {
|
||||||
|
/// Using AccountLoader forces a AccountInfo.clone() and then binds the loaded
|
||||||
|
/// lifetime to the AccountLoader's lifetime. This function avoids both.
|
||||||
|
/// It checks the account owner and discriminator, then casts the data.
|
||||||
|
fn load<T: ZeroCopy + Owner>(&self) -> Result<Ref<T>>;
|
||||||
|
|
||||||
|
/// Same as load(), but doesn't check the discriminator.
|
||||||
|
fn load_unchecked<T: ZeroCopy + Owner>(&self) -> Result<Ref<T>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'info> LoadZeroCopy for AccountInfo<'info> {
|
||||||
|
fn load<T: ZeroCopy + Owner>(&self) -> Result<Ref<T>> {
|
||||||
|
if self.owner != &T::owner() {
|
||||||
|
return Err(ErrorCode::AccountOwnedByWrongProgram.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = self.try_borrow_data()?;
|
||||||
|
|
||||||
|
let disc_bytes = array_ref![data, 0, 8];
|
||||||
|
if disc_bytes != &T::discriminator() {
|
||||||
|
return Err(ErrorCode::AccountDiscriminatorMismatch.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Ref::map(data, |data| {
|
||||||
|
bytemuck::from_bytes(&data[8..mem::size_of::<T>() + 8])
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_unchecked<T: ZeroCopy + Owner>(&self) -> Result<Ref<T>> {
|
||||||
|
if self.owner != &T::owner() {
|
||||||
|
return Err(ErrorCode::AccountOwnedByWrongProgram.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = self.try_borrow_data()?;
|
||||||
|
|
||||||
|
Ok(Ref::map(data, |data| {
|
||||||
|
bytemuck::from_bytes(&data[8..mem::size_of::<T>() + 8])
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue