Serum: Health computation first steps
This commit is contained in:
parent
f5d2964f1d
commit
6aa4724b45
|
@ -1,11 +1,12 @@
|
|||
use anchor_lang::prelude::*;
|
||||
use fixed::types::I80F48;
|
||||
use pyth_client::load_price;
|
||||
use std::cell::Ref;
|
||||
|
||||
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::checked_math as cm;
|
||||
use crate::util::LoadZeroCopy;
|
||||
|
||||
pub fn compute_health(account: &MangoAccount, ais: &[AccountInfo]) -> Result<I80F48> {
|
||||
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)
|
||||
}
|
||||
|
||||
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(
|
||||
account: &MangoAccount,
|
||||
banks: &[AccountInfo],
|
||||
oracles: &[AccountInfo],
|
||||
_serum_oos: &[AccountInfo],
|
||||
serum_oos: &[AccountInfo],
|
||||
) -> Result<I80F48> {
|
||||
let mut assets = I80F48::ZERO;
|
||||
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!(
|
||||
bank.token_index == position.token_index,
|
||||
MangoError::SomeError
|
||||
|
@ -44,22 +66,7 @@ fn compute_health_detail(
|
|||
|
||||
// converts the token value to the basis token value for health computations
|
||||
// TODO: health basis token == USDC?
|
||||
let oracle_data = &oracle_ai.try_borrow_data()?;
|
||||
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 price = *price;
|
||||
let native_position = position.native(&bank);
|
||||
let native_basis = cm!(native_position * price);
|
||||
if native_basis.is_positive() {
|
||||
|
@ -69,11 +76,21 @@ fn compute_health_detail(
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Serum open orders
|
||||
// - for each active serum market, pass the OpenOrders in order
|
||||
// - store the base_token_index and quote_token_index in the account, so we don't
|
||||
// need to also pass SerumMarket
|
||||
// - find the bank and oracle for base and quote, and add appropriately
|
||||
// health contribution from serum accounts
|
||||
for (serum_account, oo_ai) in
|
||||
util::zip!(account.serum_account_map.iter_active(), serum_oos.iter())
|
||||
{
|
||||
// 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))
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ use anchor_lang::Discriminator;
|
|||
use fixed::types::I80F48;
|
||||
|
||||
use crate::error::MangoError;
|
||||
use crate::util::LoadZeroCopy;
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub enum OracleType {
|
||||
|
@ -26,6 +27,19 @@ pub fn determine_oracle_type(data: &[u8]) -> Result<OracleType> {
|
|||
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)]
|
||||
mod tests {
|
||||
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_rules! zip {
|
||||
($x: expr) => ($x);
|
||||
|
@ -15,3 +20,44 @@ macro_rules! 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