Separate health code into multiple files
This commit is contained in:
parent
d64d9285f4
commit
7bbf045823
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,520 @@
|
|||
use anchor_lang::prelude::*;
|
||||
use anchor_lang::ZeroCopy;
|
||||
|
||||
use fixed::types::I80F48;
|
||||
use serum_dex::state::OpenOrders;
|
||||
|
||||
use std::cell::Ref;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::accounts_zerocopy::*;
|
||||
use crate::error::*;
|
||||
use crate::serum3_cpi;
|
||||
use crate::state::{Bank, MangoAccountRef, PerpMarket, PerpMarketIndex, TokenIndex};
|
||||
use crate::util::checked_math as cm;
|
||||
|
||||
/// This trait abstracts how to find accounts needed for the health computation.
|
||||
///
|
||||
/// There are different ways they are retrieved from remainingAccounts, based
|
||||
/// on the instruction:
|
||||
/// - FixedOrderAccountRetriever requires the remainingAccounts to be in a well
|
||||
/// defined order and is the fastest. It's used where possible.
|
||||
/// - ScanningAccountRetriever does a linear scan for each account it needs.
|
||||
/// It needs more compute, but works when a union of bank/oracle/market accounts
|
||||
/// are passed because health needs to be computed for different baskets in
|
||||
/// one instruction (such as for liquidation instructions).
|
||||
pub trait AccountRetriever {
|
||||
fn bank_and_oracle(
|
||||
&self,
|
||||
group: &Pubkey,
|
||||
active_token_position_index: usize,
|
||||
token_index: TokenIndex,
|
||||
) -> Result<(&Bank, I80F48)>;
|
||||
|
||||
fn serum_oo(&self, active_serum_oo_index: usize, key: &Pubkey) -> Result<&OpenOrders>;
|
||||
|
||||
fn perp_market_and_oracle_price(
|
||||
&self,
|
||||
group: &Pubkey,
|
||||
active_perp_position_index: usize,
|
||||
perp_market_index: PerpMarketIndex,
|
||||
) -> Result<(&PerpMarket, I80F48)>;
|
||||
}
|
||||
|
||||
/// Assumes the account infos needed for the health computation follow a strict order.
|
||||
///
|
||||
/// 1. n_banks Bank account, in the order of account.token_iter_active()
|
||||
/// 2. n_banks oracle accounts, one for each bank in the same order
|
||||
/// 3. PerpMarket accounts, in the order of account.perps.iter_active_accounts()
|
||||
/// 4. PerpMarket oracle accounts, in the order of the perp market accounts
|
||||
/// 5. serum3 OpenOrders accounts, in the order of account.serum3.iter_active()
|
||||
pub struct FixedOrderAccountRetriever<T: KeyedAccountReader> {
|
||||
pub ais: Vec<T>,
|
||||
pub n_banks: usize,
|
||||
pub n_perps: usize,
|
||||
pub begin_perp: usize,
|
||||
pub begin_serum3: usize,
|
||||
pub staleness_slot: Option<u64>,
|
||||
}
|
||||
|
||||
pub fn new_fixed_order_account_retriever<'a, 'info>(
|
||||
ais: &'a [AccountInfo<'info>],
|
||||
account: &MangoAccountRef,
|
||||
) -> Result<FixedOrderAccountRetriever<AccountInfoRef<'a, 'info>>> {
|
||||
let active_token_len = account.active_token_positions().count();
|
||||
let active_serum3_len = account.active_serum3_orders().count();
|
||||
let active_perp_len = account.active_perp_positions().count();
|
||||
let expected_ais = cm!(active_token_len * 2 // banks + oracles
|
||||
+ active_perp_len * 2 // PerpMarkets + Oracles
|
||||
+ active_serum3_len); // open_orders
|
||||
require_eq!(ais.len(), expected_ais);
|
||||
|
||||
Ok(FixedOrderAccountRetriever {
|
||||
ais: AccountInfoRef::borrow_slice(ais)?,
|
||||
n_banks: active_token_len,
|
||||
n_perps: active_perp_len,
|
||||
begin_perp: cm!(active_token_len * 2),
|
||||
begin_serum3: cm!(active_token_len * 2 + active_perp_len * 2),
|
||||
staleness_slot: Some(Clock::get()?.slot),
|
||||
})
|
||||
}
|
||||
|
||||
impl<T: KeyedAccountReader> FixedOrderAccountRetriever<T> {
|
||||
fn bank(&self, group: &Pubkey, account_index: usize, token_index: TokenIndex) -> Result<&Bank> {
|
||||
let bank = self.ais[account_index].load::<Bank>()?;
|
||||
require_keys_eq!(bank.group, *group);
|
||||
require_eq!(bank.token_index, token_index);
|
||||
Ok(bank)
|
||||
}
|
||||
|
||||
fn perp_market(
|
||||
&self,
|
||||
group: &Pubkey,
|
||||
account_index: usize,
|
||||
perp_market_index: PerpMarketIndex,
|
||||
) -> Result<&PerpMarket> {
|
||||
let market_ai = &self.ais[account_index];
|
||||
let market = market_ai.load::<PerpMarket>()?;
|
||||
require_keys_eq!(market.group, *group);
|
||||
require_eq!(market.perp_market_index, perp_market_index);
|
||||
Ok(market)
|
||||
}
|
||||
|
||||
fn oracle_price_bank(&self, account_index: usize, bank: &Bank) -> Result<I80F48> {
|
||||
let oracle = &self.ais[account_index];
|
||||
bank.oracle_price(oracle, self.staleness_slot)
|
||||
}
|
||||
|
||||
fn oracle_price_perp(&self, account_index: usize, perp_market: &PerpMarket) -> Result<I80F48> {
|
||||
let oracle = &self.ais[account_index];
|
||||
perp_market.oracle_price(oracle, self.staleness_slot)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: KeyedAccountReader> AccountRetriever for FixedOrderAccountRetriever<T> {
|
||||
fn bank_and_oracle(
|
||||
&self,
|
||||
group: &Pubkey,
|
||||
active_token_position_index: usize,
|
||||
token_index: TokenIndex,
|
||||
) -> Result<(&Bank, I80F48)> {
|
||||
let bank_account_index = active_token_position_index;
|
||||
let bank = self
|
||||
.bank(group, bank_account_index, token_index)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"loading bank with health account index {}, token index {}, passed account {}",
|
||||
bank_account_index,
|
||||
token_index,
|
||||
self.ais[bank_account_index].key(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let oracle_index = cm!(self.n_banks + active_token_position_index);
|
||||
let oracle_price = self.oracle_price_bank(oracle_index, bank).with_context(|| {
|
||||
format!(
|
||||
"getting oracle for bank with health account index {} and token index {}, passed account {}",
|
||||
bank_account_index,
|
||||
token_index,
|
||||
self.ais[oracle_index].key(),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok((bank, oracle_price))
|
||||
}
|
||||
|
||||
fn perp_market_and_oracle_price(
|
||||
&self,
|
||||
group: &Pubkey,
|
||||
active_perp_position_index: usize,
|
||||
perp_market_index: PerpMarketIndex,
|
||||
) -> Result<(&PerpMarket, I80F48)> {
|
||||
let perp_index = cm!(self.begin_perp + active_perp_position_index);
|
||||
let perp_market = self
|
||||
.perp_market(group, perp_index, perp_market_index)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"loading perp market with health account index {} and perp market index {}, passed account {}",
|
||||
perp_index,
|
||||
perp_market_index,
|
||||
self.ais[perp_index].key(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let oracle_index = cm!(perp_index + self.n_perps);
|
||||
let oracle_price = self.oracle_price_perp(oracle_index, perp_market).with_context(|| {
|
||||
format!(
|
||||
"getting oracle for perp market with health account index {} and perp market index {}, passed account {}",
|
||||
oracle_index,
|
||||
perp_market_index,
|
||||
self.ais[oracle_index].key(),
|
||||
)
|
||||
})?;
|
||||
Ok((perp_market, oracle_price))
|
||||
}
|
||||
|
||||
fn serum_oo(&self, active_serum_oo_index: usize, key: &Pubkey) -> Result<&OpenOrders> {
|
||||
let serum_oo_index = cm!(self.begin_serum3 + active_serum_oo_index);
|
||||
let ai = &self.ais[serum_oo_index];
|
||||
(|| {
|
||||
require_keys_eq!(*key, *ai.key());
|
||||
serum3_cpi::load_open_orders(ai)
|
||||
})()
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"loading serum open orders with health account index {}, passed account {}",
|
||||
serum_oo_index,
|
||||
ai.key(),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Takes a list of account infos containing
|
||||
/// - an unknown number of Banks in any order, followed by
|
||||
/// - the same number of oracles in the same order as the banks, followed by
|
||||
/// - an unknown number of PerpMarket accounts
|
||||
/// - the same number of oracles in the same order as the perp markets
|
||||
/// - an unknown number of serum3 OpenOrders accounts
|
||||
/// and retrieves accounts needed for the health computation by doing a linear
|
||||
/// scan for each request.
|
||||
pub struct ScanningAccountRetriever<'a, 'info> {
|
||||
banks: Vec<AccountInfoRefMut<'a, 'info>>,
|
||||
oracles: Vec<AccountInfoRef<'a, 'info>>,
|
||||
perp_markets: Vec<AccountInfoRef<'a, 'info>>,
|
||||
perp_oracles: Vec<AccountInfoRef<'a, 'info>>,
|
||||
serum3_oos: Vec<AccountInfoRef<'a, 'info>>,
|
||||
token_index_map: HashMap<TokenIndex, usize>,
|
||||
perp_index_map: HashMap<PerpMarketIndex, usize>,
|
||||
staleness_slot: Option<u64>,
|
||||
}
|
||||
|
||||
/// Returns None if `ai` doesn't have the owner or discriminator for T.
|
||||
/// Forwards "can't borrow" error, so it can be raised immediately.
|
||||
fn can_load_as<'a, T: ZeroCopy + Owner>(
|
||||
(i, ai): (usize, &'a AccountInfo),
|
||||
) -> Option<(usize, Result<Ref<'a, T>>)> {
|
||||
let load_result = ai.load::<T>();
|
||||
match load_result {
|
||||
Err(Error::AnchorError(error))
|
||||
if error.error_code_number == ErrorCode::AccountDiscriminatorMismatch as u32
|
||||
|| error.error_code_number == ErrorCode::AccountDiscriminatorNotFound as u32
|
||||
|| error.error_code_number == ErrorCode::AccountOwnedByWrongProgram as u32 =>
|
||||
{
|
||||
return None;
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
Some((i, load_result))
|
||||
}
|
||||
|
||||
impl<'a, 'info> ScanningAccountRetriever<'a, 'info> {
|
||||
pub fn new(ais: &'a [AccountInfo<'info>], group: &Pubkey) -> Result<Self> {
|
||||
Self::new_with_staleness(ais, group, Some(Clock::get()?.slot))
|
||||
}
|
||||
|
||||
pub fn new_with_staleness(
|
||||
ais: &'a [AccountInfo<'info>],
|
||||
group: &Pubkey,
|
||||
staleness_slot: Option<u64>,
|
||||
) -> Result<Self> {
|
||||
// find all Bank accounts
|
||||
let mut token_index_map = HashMap::with_capacity(ais.len() / 2);
|
||||
ais.iter()
|
||||
.enumerate()
|
||||
.map_while(can_load_as::<Bank>)
|
||||
.try_for_each(|(i, loaded)| {
|
||||
(|| {
|
||||
let bank = loaded?;
|
||||
require_keys_eq!(bank.group, *group);
|
||||
let previous = token_index_map.insert(bank.token_index, i);
|
||||
require_msg!(
|
||||
previous.is_none(),
|
||||
"duplicate bank for token index {}",
|
||||
bank.token_index
|
||||
);
|
||||
Ok(())
|
||||
})()
|
||||
.with_context(|| format!("scanning banks, health account index {}", i))
|
||||
})?;
|
||||
let n_banks = token_index_map.len();
|
||||
|
||||
// skip all banks and oracles, then find number of PerpMarket accounts
|
||||
let perps_start = n_banks * 2;
|
||||
let mut perp_index_map = HashMap::with_capacity(ais.len().saturating_sub(perps_start));
|
||||
ais[perps_start..]
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map_while(can_load_as::<PerpMarket>)
|
||||
.try_for_each(|(i, loaded)| {
|
||||
(|| {
|
||||
let perp_market = loaded?;
|
||||
require_keys_eq!(perp_market.group, *group);
|
||||
let previous = perp_index_map.insert(perp_market.perp_market_index, i);
|
||||
require_msg!(
|
||||
previous.is_none(),
|
||||
"duplicate perp market for perp market index {}",
|
||||
perp_market.perp_market_index
|
||||
);
|
||||
Ok(())
|
||||
})()
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"scanning perp markets, health account index {}",
|
||||
i + perps_start
|
||||
)
|
||||
})
|
||||
})?;
|
||||
let n_perps = perp_index_map.len();
|
||||
let perp_oracles_start = perps_start + n_perps;
|
||||
let serum3_start = perp_oracles_start + n_perps;
|
||||
|
||||
Ok(Self {
|
||||
banks: AccountInfoRefMut::borrow_slice(&ais[..n_banks])?,
|
||||
oracles: AccountInfoRef::borrow_slice(&ais[n_banks..perps_start])?,
|
||||
perp_markets: AccountInfoRef::borrow_slice(&ais[perps_start..perp_oracles_start])?,
|
||||
perp_oracles: AccountInfoRef::borrow_slice(&ais[perp_oracles_start..serum3_start])?,
|
||||
serum3_oos: AccountInfoRef::borrow_slice(&ais[serum3_start..])?,
|
||||
token_index_map,
|
||||
perp_index_map,
|
||||
staleness_slot,
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn bank_index(&self, token_index: TokenIndex) -> Result<usize> {
|
||||
Ok(*self
|
||||
.token_index_map
|
||||
.get(&token_index)
|
||||
.ok_or_else(|| error_msg!("token index {} not found", token_index))?)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn perp_market_index(&self, perp_market_index: PerpMarketIndex) -> Result<usize> {
|
||||
Ok(*self
|
||||
.perp_index_map
|
||||
.get(&perp_market_index)
|
||||
.ok_or_else(|| error_msg!("perp market index {} not found", perp_market_index))?)
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn banks_mut_and_oracles(
|
||||
&mut self,
|
||||
token_index1: TokenIndex,
|
||||
token_index2: TokenIndex,
|
||||
) -> Result<(&mut Bank, I80F48, Option<(&mut Bank, I80F48)>)> {
|
||||
if token_index1 == token_index2 {
|
||||
let index = self.bank_index(token_index1)?;
|
||||
let bank = self.banks[index].load_mut_fully_unchecked::<Bank>()?;
|
||||
let oracle = &self.oracles[index];
|
||||
let price = bank.oracle_price(oracle, self.staleness_slot)?;
|
||||
return Ok((bank, price, None));
|
||||
}
|
||||
let index1 = self.bank_index(token_index1)?;
|
||||
let index2 = self.bank_index(token_index2)?;
|
||||
let (first, second, swap) = if index1 < index2 {
|
||||
(index1, index2, false)
|
||||
} else {
|
||||
(index2, index1, true)
|
||||
};
|
||||
|
||||
// split_at_mut after the first bank and after the second bank
|
||||
let (first_bank_part, second_bank_part) = self.banks.split_at_mut(first + 1);
|
||||
|
||||
let bank1 = first_bank_part[first].load_mut_fully_unchecked::<Bank>()?;
|
||||
let bank2 = second_bank_part[second - (first + 1)].load_mut_fully_unchecked::<Bank>()?;
|
||||
let oracle1 = &self.oracles[first];
|
||||
let oracle2 = &self.oracles[second];
|
||||
let price1 = bank1.oracle_price(oracle1, self.staleness_slot)?;
|
||||
let price2 = bank2.oracle_price(oracle2, self.staleness_slot)?;
|
||||
if swap {
|
||||
Ok((bank2, price2, Some((bank1, price1))))
|
||||
} else {
|
||||
Ok((bank1, price1, Some((bank2, price2))))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scanned_bank_and_oracle(&self, token_index: TokenIndex) -> Result<(&Bank, I80F48)> {
|
||||
let index = self.bank_index(token_index)?;
|
||||
// The account was already loaded successfully during construction
|
||||
let bank = self.banks[index].load_fully_unchecked::<Bank>()?;
|
||||
let oracle = &self.oracles[index];
|
||||
let price = bank.oracle_price(oracle, self.staleness_slot)?;
|
||||
Ok((bank, price))
|
||||
}
|
||||
|
||||
pub fn scanned_perp_market_and_oracle(
|
||||
&self,
|
||||
perp_market_index: PerpMarketIndex,
|
||||
) -> Result<(&PerpMarket, I80F48)> {
|
||||
let index = self.perp_market_index(perp_market_index)?;
|
||||
// The account was already loaded successfully during construction
|
||||
let perp_market = self.perp_markets[index].load_fully_unchecked::<PerpMarket>()?;
|
||||
let oracle_acc = &self.perp_oracles[index];
|
||||
let price = perp_market.oracle_price(oracle_acc, self.staleness_slot)?;
|
||||
Ok((perp_market, price))
|
||||
}
|
||||
|
||||
pub fn scanned_serum_oo(&self, key: &Pubkey) -> Result<&OpenOrders> {
|
||||
let oo = self
|
||||
.serum3_oos
|
||||
.iter()
|
||||
.find(|ai| ai.key == key)
|
||||
.ok_or_else(|| error_msg!("no serum3 open orders for key {}", key))?;
|
||||
serum3_cpi::load_open_orders(oo)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'info> AccountRetriever for ScanningAccountRetriever<'a, 'info> {
|
||||
fn bank_and_oracle(
|
||||
&self,
|
||||
_group: &Pubkey,
|
||||
_account_index: usize,
|
||||
token_index: TokenIndex,
|
||||
) -> Result<(&Bank, I80F48)> {
|
||||
self.scanned_bank_and_oracle(token_index)
|
||||
}
|
||||
|
||||
fn perp_market_and_oracle_price(
|
||||
&self,
|
||||
_group: &Pubkey,
|
||||
_account_index: usize,
|
||||
perp_market_index: PerpMarketIndex,
|
||||
) -> Result<(&PerpMarket, I80F48)> {
|
||||
self.scanned_perp_market_and_oracle(perp_market_index)
|
||||
}
|
||||
|
||||
fn serum_oo(&self, _account_index: usize, key: &Pubkey) -> Result<&OpenOrders> {
|
||||
self.scanned_serum_oo(key)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::super::test::*;
|
||||
use super::*;
|
||||
use serum_dex::state::OpenOrders;
|
||||
use std::convert::identity;
|
||||
|
||||
#[test]
|
||||
fn test_scanning_account_retriever() {
|
||||
let oracle1_price = 1.0;
|
||||
let oracle2_price = 5.0;
|
||||
let group = Pubkey::new_unique();
|
||||
|
||||
let (mut bank1, mut oracle1) = mock_bank_and_oracle(group, 1, oracle1_price, 0.2, 0.1);
|
||||
let (mut bank2, mut oracle2) = mock_bank_and_oracle(group, 4, oracle2_price, 0.5, 0.3);
|
||||
let (mut bank3, _) = mock_bank_and_oracle(group, 5, 1.0, 0.5, 0.3);
|
||||
|
||||
// bank3 reuses the bank2 oracle, to ensure the ScanningAccountRetriever doesn't choke on that
|
||||
bank3.data().oracle = oracle2.pubkey;
|
||||
|
||||
let mut oo1 = TestAccount::<OpenOrders>::new_zeroed();
|
||||
let oo1key = oo1.pubkey;
|
||||
oo1.data().native_pc_total = 20;
|
||||
|
||||
let mut perp1 = mock_perp_market(group, oracle2.pubkey, oracle2_price, 9, 0.2, 0.1);
|
||||
let mut perp2 = mock_perp_market(group, oracle1.pubkey, oracle1_price, 8, 0.2, 0.1);
|
||||
|
||||
let oracle1_account_info = oracle1.as_account_info();
|
||||
let oracle2_account_info = oracle2.as_account_info();
|
||||
let ais = vec![
|
||||
bank1.as_account_info(),
|
||||
bank2.as_account_info(),
|
||||
bank3.as_account_info(),
|
||||
oracle1_account_info.clone(),
|
||||
oracle2_account_info.clone(),
|
||||
oracle2_account_info.clone(),
|
||||
perp1.as_account_info(),
|
||||
perp2.as_account_info(),
|
||||
oracle2_account_info,
|
||||
oracle1_account_info,
|
||||
oo1.as_account_info(),
|
||||
];
|
||||
|
||||
let mut retriever =
|
||||
ScanningAccountRetriever::new_with_staleness(&ais, &group, None).unwrap();
|
||||
|
||||
assert_eq!(retriever.banks.len(), 3);
|
||||
assert_eq!(retriever.token_index_map.len(), 3);
|
||||
assert_eq!(retriever.oracles.len(), 3);
|
||||
assert_eq!(retriever.perp_markets.len(), 2);
|
||||
assert_eq!(retriever.perp_oracles.len(), 2);
|
||||
assert_eq!(retriever.perp_index_map.len(), 2);
|
||||
assert_eq!(retriever.serum3_oos.len(), 1);
|
||||
|
||||
{
|
||||
let (b1, o1, opt_b2o2) = retriever.banks_mut_and_oracles(1, 4).unwrap();
|
||||
let (b2, o2) = opt_b2o2.unwrap();
|
||||
assert_eq!(b1.token_index, 1);
|
||||
assert_eq!(o1, I80F48::ONE);
|
||||
assert_eq!(b2.token_index, 4);
|
||||
assert_eq!(o2, 5 * I80F48::ONE);
|
||||
}
|
||||
|
||||
{
|
||||
let (b1, o1, opt_b2o2) = retriever.banks_mut_and_oracles(4, 1).unwrap();
|
||||
let (b2, o2) = opt_b2o2.unwrap();
|
||||
assert_eq!(b1.token_index, 4);
|
||||
assert_eq!(o1, 5 * I80F48::ONE);
|
||||
assert_eq!(b2.token_index, 1);
|
||||
assert_eq!(o2, I80F48::ONE);
|
||||
}
|
||||
|
||||
{
|
||||
let (b1, o1, opt_b2o2) = retriever.banks_mut_and_oracles(4, 4).unwrap();
|
||||
assert!(opt_b2o2.is_none());
|
||||
assert_eq!(b1.token_index, 4);
|
||||
assert_eq!(o1, 5 * I80F48::ONE);
|
||||
}
|
||||
|
||||
retriever.banks_mut_and_oracles(4, 2).unwrap_err();
|
||||
|
||||
{
|
||||
let (b, o) = retriever.scanned_bank_and_oracle(5).unwrap();
|
||||
assert_eq!(b.token_index, 5);
|
||||
assert_eq!(o, 5 * I80F48::ONE);
|
||||
}
|
||||
|
||||
let oo = retriever.serum_oo(0, &oo1key).unwrap();
|
||||
assert_eq!(identity(oo.native_pc_total), 20);
|
||||
|
||||
assert!(retriever.serum_oo(1, &Pubkey::default()).is_err());
|
||||
|
||||
let (perp, oracle_price) = retriever
|
||||
.perp_market_and_oracle_price(&group, 0, 9)
|
||||
.unwrap();
|
||||
assert_eq!(identity(perp.perp_market_index), 9);
|
||||
assert_eq!(oracle_price, oracle2_price);
|
||||
|
||||
let (perp, oracle_price) = retriever
|
||||
.perp_market_and_oracle_price(&group, 1, 8)
|
||||
.unwrap();
|
||||
assert_eq!(identity(perp.perp_market_index), 8);
|
||||
assert_eq!(oracle_price, oracle1_price);
|
||||
|
||||
assert!(retriever
|
||||
.perp_market_and_oracle_price(&group, 1, 5)
|
||||
.is_err());
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,827 @@
|
|||
#![cfg(feature = "client")]
|
||||
|
||||
use anchor_lang::prelude::*;
|
||||
|
||||
use fixed::types::I80F48;
|
||||
use fixed_macro::types::I80F48;
|
||||
|
||||
use crate::error::*;
|
||||
use crate::state::{PerpMarketIndex, TokenIndex};
|
||||
use crate::util::checked_math as cm;
|
||||
|
||||
use super::*;
|
||||
use crate::state::orderbook::Side as PerpOrderSide;
|
||||
|
||||
impl HealthCache {
|
||||
pub fn can_call_spot_bankruptcy(&self) -> bool {
|
||||
!self.has_liquidatable_assets() && self.has_spot_borrows()
|
||||
}
|
||||
|
||||
pub fn is_liquidatable(&self) -> bool {
|
||||
if self.being_liquidated {
|
||||
self.health(HealthType::Init).is_negative()
|
||||
} else {
|
||||
self.health(HealthType::Maint).is_negative()
|
||||
}
|
||||
}
|
||||
|
||||
/// The health ratio is
|
||||
/// - 0 if health is 0 - meaning assets = liabs
|
||||
/// - 100 if there's 2x as many assets as liabs
|
||||
/// - 200 if there's 3x as many assets as liabs
|
||||
/// - MAX if liabs = 0
|
||||
///
|
||||
/// Maybe talking about the collateralization ratio assets/liabs is more intuitive?
|
||||
pub fn health_ratio(&self, health_type: HealthType) -> I80F48 {
|
||||
let (assets, liabs) = self.health_assets_and_liabs(health_type);
|
||||
let hundred = I80F48::from(100);
|
||||
if liabs > 0 {
|
||||
// feel free to saturate to MAX for tiny liabs
|
||||
cm!(hundred * (assets - liabs)).saturating_div(liabs)
|
||||
} else {
|
||||
I80F48::MAX
|
||||
}
|
||||
}
|
||||
|
||||
/// How much source native tokens may be swapped for target tokens while staying
|
||||
/// above the min_ratio health ratio.
|
||||
///
|
||||
/// `price`: The amount of target native you receive for one source native. So if we
|
||||
/// swap BTC -> SOL and they're at ui prices of $20000 and $40, that means price
|
||||
/// should be 500000 native_SOL for a native_BTC. Because 1 BTC gives you 500 SOL
|
||||
/// so 1e6 native_BTC gives you 500e9 native_SOL.
|
||||
///
|
||||
/// NOTE: keep getMaxSourceForTokenSwap in ts/client in sync with changes here
|
||||
pub fn max_swap_source_for_health_ratio(
|
||||
&self,
|
||||
source: TokenIndex,
|
||||
target: TokenIndex,
|
||||
price: I80F48,
|
||||
min_ratio: I80F48,
|
||||
) -> Result<I80F48> {
|
||||
// The health_ratio is nonlinear based on swap amount.
|
||||
// For large swap amounts the slope is guaranteed to be negative (unless the price
|
||||
// is extremely good), but small amounts can have positive slope (e.g. using
|
||||
// source deposits to pay back target borrows).
|
||||
//
|
||||
// That means:
|
||||
// - even if the initial ratio is < min_ratio it can be useful to swap to *increase* health
|
||||
// - be careful about finding the min_ratio point: the function isn't convex
|
||||
|
||||
let health_type = HealthType::Init;
|
||||
let initial_ratio = self.health_ratio(health_type);
|
||||
if initial_ratio < 0 {
|
||||
return Ok(I80F48::ZERO);
|
||||
}
|
||||
|
||||
let source_index = find_token_info_index(&self.token_infos, source)?;
|
||||
let target_index = find_token_info_index(&self.token_infos, target)?;
|
||||
let source = &self.token_infos[source_index];
|
||||
let target = &self.token_infos[target_index];
|
||||
|
||||
// If the price is sufficiently good, then health will just increase from swapping:
|
||||
// once we've swapped enough, swapping x reduces health by x * source_liab_weight and
|
||||
// increases it by x * target_asset_weight * price_factor.
|
||||
let final_health_slope = -source.init_liab_weight * source.prices.liab(health_type)
|
||||
+ target.init_asset_weight * target.prices.asset(health_type) * price;
|
||||
if final_health_slope >= 0 {
|
||||
return Ok(I80F48::MAX);
|
||||
}
|
||||
|
||||
let cache_after_swap = |amount: I80F48| {
|
||||
let mut adjusted_cache = self.clone();
|
||||
adjusted_cache.token_infos[source_index].balance_native -= amount;
|
||||
adjusted_cache.token_infos[target_index].balance_native += cm!(amount * price);
|
||||
adjusted_cache
|
||||
};
|
||||
let health_ratio_after_swap =
|
||||
|amount| cache_after_swap(amount).health_ratio(HealthType::Init);
|
||||
|
||||
// There are two key slope changes: Assume source.balance > 0 and target.balance < 0.
|
||||
// When these values flip sign, the health slope decreases, but could still be positive.
|
||||
// After point1 it's definitely negative (due to final_health_slope check above).
|
||||
// The maximum health ratio will be at 0 or at one of these points (ignoring serum3 effects).
|
||||
let source_for_zero_target_balance = -target.balance_native / price;
|
||||
let point0_amount = source
|
||||
.balance_native
|
||||
.min(source_for_zero_target_balance)
|
||||
.max(I80F48::ZERO);
|
||||
let point1_amount = source
|
||||
.balance_native
|
||||
.max(source_for_zero_target_balance)
|
||||
.max(I80F48::ZERO);
|
||||
let point0_ratio = health_ratio_after_swap(point0_amount);
|
||||
let (point1_ratio, point1_health) = {
|
||||
let cache = cache_after_swap(point1_amount);
|
||||
(
|
||||
cache.health_ratio(HealthType::Init),
|
||||
cache.health(HealthType::Init),
|
||||
)
|
||||
};
|
||||
|
||||
let amount =
|
||||
if initial_ratio <= min_ratio && point0_ratio < min_ratio && point1_ratio < min_ratio {
|
||||
// If we have to stay below the target ratio, pick the highest one
|
||||
if point0_ratio > initial_ratio {
|
||||
if point1_ratio > point0_ratio {
|
||||
point1_amount
|
||||
} else {
|
||||
point0_amount
|
||||
}
|
||||
} else if point1_ratio > initial_ratio {
|
||||
point1_amount
|
||||
} else {
|
||||
I80F48::ZERO
|
||||
}
|
||||
} else if point1_ratio >= min_ratio {
|
||||
// If point1_ratio is still bigger than min_ratio, the target amount must be >point1_amount
|
||||
// search to the right of point1_amount: but how far?
|
||||
// At point1, source.balance < 0 and target.balance > 0, so use a simple estimation for
|
||||
// zero health: health
|
||||
// - source_liab_weight * source_liab_price * a
|
||||
// + target_asset_weight * target_asset_price * price * a = 0.
|
||||
// where a is the source token native amount.
|
||||
if point1_health <= 0 {
|
||||
return Ok(I80F48::ZERO);
|
||||
}
|
||||
let zero_health_amount = point1_amount - point1_health / final_health_slope;
|
||||
let zero_health_ratio = health_ratio_after_swap(zero_health_amount);
|
||||
binary_search(
|
||||
point1_amount,
|
||||
point1_ratio,
|
||||
zero_health_amount,
|
||||
zero_health_ratio,
|
||||
min_ratio,
|
||||
I80F48::ZERO,
|
||||
health_ratio_after_swap,
|
||||
)?
|
||||
} else if point0_ratio >= min_ratio {
|
||||
// Must be between point0_amount and point1_amount.
|
||||
binary_search(
|
||||
point0_amount,
|
||||
point0_ratio,
|
||||
point1_amount,
|
||||
point1_ratio,
|
||||
min_ratio,
|
||||
I80F48::ZERO,
|
||||
health_ratio_after_swap,
|
||||
)?
|
||||
} else {
|
||||
// Must be between 0 and point0_amount
|
||||
binary_search(
|
||||
I80F48::ZERO,
|
||||
initial_ratio,
|
||||
point0_amount,
|
||||
point0_ratio,
|
||||
min_ratio,
|
||||
I80F48::ZERO,
|
||||
health_ratio_after_swap,
|
||||
)?
|
||||
};
|
||||
|
||||
Ok(amount)
|
||||
}
|
||||
|
||||
fn perp_info_index(&self, perp_market_index: PerpMarketIndex) -> Result<usize> {
|
||||
self.perp_infos
|
||||
.iter()
|
||||
.position(|pi| pi.perp_market_index == perp_market_index)
|
||||
.ok_or_else(|| error_msg!("perp market index {} not found", perp_market_index))
|
||||
}
|
||||
|
||||
/// NOTE: keep getMaxSourceForTokenSwap in ts/client in sync with changes here
|
||||
pub fn max_perp_for_health_ratio(
|
||||
&self,
|
||||
perp_market_index: PerpMarketIndex,
|
||||
price: I80F48,
|
||||
side: PerpOrderSide,
|
||||
min_ratio: I80F48,
|
||||
) -> Result<i64> {
|
||||
let health_type = HealthType::Init;
|
||||
let initial_ratio = self.health_ratio(health_type);
|
||||
if initial_ratio < 0 {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
let direction = match side {
|
||||
PerpOrderSide::Bid => 1,
|
||||
PerpOrderSide::Ask => -1,
|
||||
};
|
||||
|
||||
let perp_info_index = self.perp_info_index(perp_market_index)?;
|
||||
let perp_info = &self.perp_infos[perp_info_index];
|
||||
let prices = &perp_info.prices;
|
||||
let base_lot_size = I80F48::from(perp_info.base_lot_size);
|
||||
|
||||
// If the price is sufficiently good then health will just increase from trading
|
||||
// TODO: This is not actually correct, since perp health for untrusted markets can't go above 0
|
||||
let final_health_slope = if direction == 1 {
|
||||
perp_info.init_asset_weight * prices.asset(health_type) - price
|
||||
} else {
|
||||
price - perp_info.init_liab_weight * prices.liab(health_type)
|
||||
};
|
||||
if final_health_slope >= 0 {
|
||||
return Ok(i64::MAX);
|
||||
}
|
||||
|
||||
let cache_after_trade = |base_lots: i64| {
|
||||
let mut adjusted_cache = self.clone();
|
||||
adjusted_cache.perp_infos[perp_info_index].base_lots += direction * base_lots;
|
||||
adjusted_cache.perp_infos[perp_info_index].quote -=
|
||||
I80F48::from(direction) * I80F48::from(base_lots) * base_lot_size * price;
|
||||
adjusted_cache
|
||||
};
|
||||
let health_ratio_after_trade =
|
||||
|base_lots: i64| cache_after_trade(base_lots).health_ratio(HealthType::Init);
|
||||
let health_ratio_after_trade_trunc =
|
||||
|base_lots: I80F48| health_ratio_after_trade(base_lots.round_to_zero().to_num());
|
||||
|
||||
let initial_base_lots = perp_info.base_lots;
|
||||
|
||||
// There are two cases:
|
||||
// 1. We are increasing abs(base_lots)
|
||||
// 2. We are bringing the base position to 0, and then going to case 1.
|
||||
let has_case2 =
|
||||
initial_base_lots > 0 && direction == -1 || initial_base_lots < 0 && direction == 1;
|
||||
|
||||
let (case1_start, case1_start_ratio) = if has_case2 {
|
||||
let case1_start = initial_base_lots.abs();
|
||||
let case1_start_ratio = health_ratio_after_trade(case1_start);
|
||||
(case1_start, case1_start_ratio)
|
||||
} else {
|
||||
(0, initial_ratio)
|
||||
};
|
||||
let case1_start_i80f48 = I80F48::from(case1_start);
|
||||
|
||||
// If we start out below min_ratio and can't go above, pick the best case
|
||||
let base_lots = if initial_ratio <= min_ratio && case1_start_ratio < min_ratio {
|
||||
if case1_start_ratio >= initial_ratio {
|
||||
case1_start_i80f48
|
||||
} else {
|
||||
I80F48::ZERO
|
||||
}
|
||||
} else if case1_start_ratio >= min_ratio {
|
||||
// Must reach min_ratio to the right of case1_start
|
||||
let case1_start_health = cache_after_trade(case1_start).health(HealthType::Init);
|
||||
if case1_start_health <= 0 {
|
||||
return Ok(0);
|
||||
}
|
||||
// We add 1 here because health is computed for truncated base_lots and we want to guarantee
|
||||
// zero_health_ratio <= 0.
|
||||
let zero_health_amount = case1_start_i80f48
|
||||
- case1_start_health / final_health_slope / base_lot_size
|
||||
+ I80F48::ONE;
|
||||
let zero_health_ratio = health_ratio_after_trade_trunc(zero_health_amount);
|
||||
|
||||
binary_search(
|
||||
case1_start_i80f48,
|
||||
case1_start_ratio,
|
||||
zero_health_amount,
|
||||
zero_health_ratio,
|
||||
min_ratio,
|
||||
I80F48::ONE,
|
||||
health_ratio_after_trade_trunc,
|
||||
)?
|
||||
} else {
|
||||
// Between 0 and case1_start
|
||||
binary_search(
|
||||
I80F48::ZERO,
|
||||
initial_ratio,
|
||||
case1_start_i80f48,
|
||||
case1_start_ratio,
|
||||
min_ratio,
|
||||
I80F48::ONE,
|
||||
health_ratio_after_trade_trunc,
|
||||
)?
|
||||
};
|
||||
|
||||
Ok(base_lots.round_to_zero().to_num())
|
||||
}
|
||||
}
|
||||
|
||||
fn binary_search(
|
||||
mut left: I80F48,
|
||||
left_value: I80F48,
|
||||
mut right: I80F48,
|
||||
right_value: I80F48,
|
||||
target_value: I80F48,
|
||||
min_step: I80F48,
|
||||
fun: impl Fn(I80F48) -> I80F48,
|
||||
) -> Result<I80F48> {
|
||||
let max_iterations = 20;
|
||||
let target_error = I80F48!(0.1);
|
||||
require_msg!(
|
||||
(left_value - target_value).signum() * (right_value - target_value).signum() != I80F48::ONE,
|
||||
"internal error: left {} and right {} don't contain the target value {}",
|
||||
left_value,
|
||||
right_value,
|
||||
target_value
|
||||
);
|
||||
for _ in 0..max_iterations {
|
||||
if (right - left).abs() < min_step {
|
||||
return Ok(left);
|
||||
}
|
||||
let new = I80F48::from_num(0.5) * (left + right);
|
||||
let new_value = fun(new);
|
||||
println!("l {} r {} v {}", left, right, new_value);
|
||||
let error = new_value - target_value;
|
||||
if error > 0 && error < target_error {
|
||||
return Ok(new);
|
||||
}
|
||||
|
||||
if (new_value > target_value) ^ (right_value > target_value) {
|
||||
left = new;
|
||||
} else {
|
||||
right = new;
|
||||
}
|
||||
}
|
||||
Err(error_msg!("binary search iterations exhausted"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::super::test::*;
|
||||
use super::*;
|
||||
use crate::state::*;
|
||||
use serum_dex::state::OpenOrders;
|
||||
|
||||
fn health_eq(a: I80F48, b: f64) -> bool {
|
||||
if (a - I80F48::from_num(b)).abs() < 0.001 {
|
||||
true
|
||||
} else {
|
||||
println!("health is {}, but expected {}", a, b);
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_max_swap() {
|
||||
let default_token_info = |x| TokenInfo {
|
||||
token_index: 0,
|
||||
maint_asset_weight: I80F48::from_num(1.0 - x),
|
||||
init_asset_weight: I80F48::from_num(1.0 - x),
|
||||
maint_liab_weight: I80F48::from_num(1.0 + x),
|
||||
init_liab_weight: I80F48::from_num(1.0 + x),
|
||||
prices: Prices::new_single_price(I80F48::from_num(2.0)),
|
||||
balance_native: I80F48::ZERO,
|
||||
};
|
||||
|
||||
let health_cache = HealthCache {
|
||||
token_infos: vec![
|
||||
TokenInfo {
|
||||
token_index: 0,
|
||||
prices: Prices::new_single_price(I80F48::from_num(2.0)),
|
||||
..default_token_info(0.1)
|
||||
},
|
||||
TokenInfo {
|
||||
token_index: 1,
|
||||
prices: Prices::new_single_price(I80F48::from_num(3.0)),
|
||||
..default_token_info(0.2)
|
||||
},
|
||||
TokenInfo {
|
||||
token_index: 2,
|
||||
prices: Prices::new_single_price(I80F48::from_num(4.0)),
|
||||
..default_token_info(0.3)
|
||||
},
|
||||
],
|
||||
serum3_infos: vec![],
|
||||
perp_infos: vec![],
|
||||
being_liquidated: false,
|
||||
};
|
||||
|
||||
assert_eq!(health_cache.health(HealthType::Init), I80F48::ZERO);
|
||||
assert_eq!(health_cache.health_ratio(HealthType::Init), I80F48::MAX);
|
||||
assert_eq!(
|
||||
health_cache
|
||||
.max_swap_source_for_health_ratio(
|
||||
0,
|
||||
1,
|
||||
I80F48::from_num(2.0 / 3.0),
|
||||
I80F48::from_num(50.0)
|
||||
)
|
||||
.unwrap(),
|
||||
I80F48::ZERO
|
||||
);
|
||||
|
||||
let adjust_by_usdc = |c: &mut HealthCache, ti: TokenIndex, usdc: f64| {
|
||||
let ti = &mut c.token_infos[ti as usize];
|
||||
ti.balance_native += I80F48::from_num(usdc) / ti.prices.oracle;
|
||||
};
|
||||
let find_max_swap_actual = |c: &HealthCache,
|
||||
source: TokenIndex,
|
||||
target: TokenIndex,
|
||||
ratio: f64,
|
||||
price_factor: f64| {
|
||||
let mut c = c.clone();
|
||||
let source_price = &c.token_infos[source as usize].prices;
|
||||
let target_price = &c.token_infos[target as usize].prices;
|
||||
let swap_price =
|
||||
I80F48::from_num(price_factor) * source_price.oracle / target_price.oracle;
|
||||
let source_amount = c
|
||||
.max_swap_source_for_health_ratio(
|
||||
source,
|
||||
target,
|
||||
swap_price,
|
||||
I80F48::from_num(ratio),
|
||||
)
|
||||
.unwrap();
|
||||
if source_amount == I80F48::MAX {
|
||||
return (f64::MAX, f64::MAX);
|
||||
}
|
||||
c.adjust_token_balance(source, -source_amount).unwrap();
|
||||
c.adjust_token_balance(target, source_amount * swap_price)
|
||||
.unwrap();
|
||||
(
|
||||
source_amount.to_num::<f64>(),
|
||||
c.health_ratio(HealthType::Init).to_num::<f64>(),
|
||||
)
|
||||
};
|
||||
let check_max_swap_result = |c: &HealthCache,
|
||||
source: TokenIndex,
|
||||
target: TokenIndex,
|
||||
ratio: f64,
|
||||
price_factor: f64| {
|
||||
let (source_amount, actual_ratio) =
|
||||
find_max_swap_actual(c, source, target, ratio, price_factor);
|
||||
println!(
|
||||
"checking {source} to {target} for price_factor: {price_factor}, target ratio {ratio}: actual ratio: {actual_ratio}, amount: {source_amount}",
|
||||
);
|
||||
assert!((ratio - actual_ratio).abs() < 1.0);
|
||||
};
|
||||
|
||||
{
|
||||
println!("test 0");
|
||||
let mut health_cache = health_cache.clone();
|
||||
adjust_by_usdc(&mut health_cache, 1, 100.0);
|
||||
|
||||
for price_factor in [0.1, 0.9, 1.1] {
|
||||
for target in 1..100 {
|
||||
let target = target as f64;
|
||||
check_max_swap_result(&health_cache, 0, 1, target, price_factor);
|
||||
check_max_swap_result(&health_cache, 1, 0, target, price_factor);
|
||||
check_max_swap_result(&health_cache, 0, 2, target, price_factor);
|
||||
}
|
||||
}
|
||||
|
||||
// At this unlikely price it's healthy to swap infinitely
|
||||
assert_eq!(
|
||||
find_max_swap_actual(&health_cache, 0, 1, 50.0, 1.5).0,
|
||||
f64::MAX
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
println!("test 1");
|
||||
let mut health_cache = health_cache.clone();
|
||||
adjust_by_usdc(&mut health_cache, 0, -20.0);
|
||||
adjust_by_usdc(&mut health_cache, 1, 100.0);
|
||||
|
||||
for price_factor in [0.1, 0.9, 1.1] {
|
||||
for target in 1..100 {
|
||||
let target = target as f64;
|
||||
check_max_swap_result(&health_cache, 0, 1, target, price_factor);
|
||||
check_max_swap_result(&health_cache, 1, 0, target, price_factor);
|
||||
check_max_swap_result(&health_cache, 0, 2, target, price_factor);
|
||||
check_max_swap_result(&health_cache, 2, 0, target, price_factor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
println!("test 2");
|
||||
let mut health_cache = health_cache.clone();
|
||||
adjust_by_usdc(&mut health_cache, 0, -50.0);
|
||||
adjust_by_usdc(&mut health_cache, 1, 100.0);
|
||||
// possible even though the init ratio is <100
|
||||
check_max_swap_result(&health_cache, 1, 0, 100.0, 1.0);
|
||||
}
|
||||
|
||||
{
|
||||
println!("test 3");
|
||||
let mut health_cache = health_cache.clone();
|
||||
adjust_by_usdc(&mut health_cache, 0, -30.0);
|
||||
adjust_by_usdc(&mut health_cache, 1, 100.0);
|
||||
adjust_by_usdc(&mut health_cache, 2, -30.0);
|
||||
|
||||
// swapping with a high ratio advises paying back all liabs
|
||||
// and then swapping even more because increasing assets in 0 has better asset weight
|
||||
let init_ratio = health_cache.health_ratio(HealthType::Init);
|
||||
let (amount, actual_ratio) = find_max_swap_actual(&health_cache, 1, 0, 100.0, 1.0);
|
||||
println!(
|
||||
"init {}, after {}, amount {}",
|
||||
init_ratio, actual_ratio, amount
|
||||
);
|
||||
assert!(actual_ratio / 2.0 > init_ratio);
|
||||
assert!((amount - 100.0 / 3.0).abs() < 1.0);
|
||||
}
|
||||
|
||||
{
|
||||
println!("test 4");
|
||||
let mut health_cache = health_cache.clone();
|
||||
adjust_by_usdc(&mut health_cache, 0, 100.0);
|
||||
adjust_by_usdc(&mut health_cache, 1, -2.0);
|
||||
adjust_by_usdc(&mut health_cache, 2, -65.0);
|
||||
|
||||
let init_ratio = health_cache.health_ratio(HealthType::Init);
|
||||
assert!(init_ratio > 3 && init_ratio < 4);
|
||||
|
||||
check_max_swap_result(&health_cache, 0, 1, 1.0, 1.0);
|
||||
check_max_swap_result(&health_cache, 0, 1, 3.0, 1.0);
|
||||
check_max_swap_result(&health_cache, 0, 1, 4.0, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_max_perp() {
|
||||
let default_token_info = |x| TokenInfo {
|
||||
token_index: 0,
|
||||
maint_asset_weight: I80F48::from_num(1.0 - x),
|
||||
init_asset_weight: I80F48::from_num(1.0 - x),
|
||||
maint_liab_weight: I80F48::from_num(1.0 + x),
|
||||
init_liab_weight: I80F48::from_num(1.0 + x),
|
||||
prices: Prices::new_single_price(I80F48::from_num(2.0)),
|
||||
balance_native: I80F48::ZERO,
|
||||
};
|
||||
let base_lot_size = 100;
|
||||
let default_perp_info = |x| PerpInfo {
|
||||
perp_market_index: 0,
|
||||
maint_asset_weight: I80F48::from_num(1.0 - x),
|
||||
init_asset_weight: I80F48::from_num(1.0 - x),
|
||||
maint_liab_weight: I80F48::from_num(1.0 + x),
|
||||
init_liab_weight: I80F48::from_num(1.0 + x),
|
||||
base_lot_size,
|
||||
base_lots: 0,
|
||||
bids_base_lots: 0,
|
||||
asks_base_lots: 0,
|
||||
quote: I80F48::ZERO,
|
||||
prices: Prices::new_single_price(I80F48::from_num(2.0)),
|
||||
has_open_orders: false,
|
||||
trusted_market: false,
|
||||
};
|
||||
|
||||
let health_cache = HealthCache {
|
||||
token_infos: vec![TokenInfo {
|
||||
token_index: 0,
|
||||
prices: Prices::new_single_price(I80F48::from_num(1.0)),
|
||||
balance_native: I80F48::ZERO,
|
||||
..default_token_info(0.0)
|
||||
}],
|
||||
serum3_infos: vec![],
|
||||
perp_infos: vec![PerpInfo {
|
||||
perp_market_index: 0,
|
||||
..default_perp_info(0.3)
|
||||
}],
|
||||
being_liquidated: false,
|
||||
};
|
||||
|
||||
assert_eq!(health_cache.health(HealthType::Init), I80F48::ZERO);
|
||||
assert_eq!(health_cache.health_ratio(HealthType::Init), I80F48::MAX);
|
||||
assert_eq!(
|
||||
health_cache
|
||||
.max_perp_for_health_ratio(
|
||||
0,
|
||||
I80F48::from(2),
|
||||
PerpOrderSide::Bid,
|
||||
I80F48::from_num(50.0)
|
||||
)
|
||||
.unwrap(),
|
||||
I80F48::ZERO
|
||||
);
|
||||
|
||||
let adjust_token = |c: &mut HealthCache, value: f64| {
|
||||
let ti = &mut c.token_infos[0];
|
||||
ti.balance_native += I80F48::from_num(value);
|
||||
};
|
||||
let find_max_trade =
|
||||
|c: &HealthCache, side: PerpOrderSide, ratio: f64, price_factor: f64| {
|
||||
let prices = &c.perp_infos[0].prices;
|
||||
let trade_price = I80F48::from_num(price_factor) * prices.oracle;
|
||||
let base_lots = c
|
||||
.max_perp_for_health_ratio(0, trade_price, side, I80F48::from_num(ratio))
|
||||
.unwrap();
|
||||
if base_lots == i64::MAX {
|
||||
return (i64::MAX, f64::MAX, f64::MAX);
|
||||
}
|
||||
|
||||
let direction = match side {
|
||||
PerpOrderSide::Bid => 1,
|
||||
PerpOrderSide::Ask => -1,
|
||||
};
|
||||
|
||||
// compute the health ratio we'd get when executing the trade
|
||||
let actual_ratio = {
|
||||
let base_lots = direction * base_lots;
|
||||
let base_native = I80F48::from(base_lots * base_lot_size);
|
||||
let mut c = c.clone();
|
||||
c.perp_infos[0].base_lots += base_lots;
|
||||
c.perp_infos[0].quote -= base_native * trade_price;
|
||||
c.health_ratio(HealthType::Init).to_num::<f64>()
|
||||
};
|
||||
// the ratio for trading just one base lot extra
|
||||
let plus_ratio = {
|
||||
let base_lots = direction * (base_lots + 1);
|
||||
let base_native = I80F48::from(base_lots * base_lot_size);
|
||||
let mut c = c.clone();
|
||||
c.perp_infos[0].base_lots += base_lots;
|
||||
c.perp_infos[0].quote -= base_native * trade_price;
|
||||
c.health_ratio(HealthType::Init).to_num::<f64>()
|
||||
};
|
||||
(base_lots, actual_ratio, plus_ratio)
|
||||
};
|
||||
let check_max_trade = |c: &HealthCache,
|
||||
side: PerpOrderSide,
|
||||
ratio: f64,
|
||||
price_factor: f64| {
|
||||
let (base_lots, actual_ratio, plus_ratio) =
|
||||
find_max_trade(c, side, ratio, price_factor);
|
||||
println!(
|
||||
"checking for price_factor: {price_factor}, target ratio {ratio}: actual ratio: {actual_ratio}, plus ratio: {plus_ratio}, base_lots: {base_lots}",
|
||||
);
|
||||
let max_binary_search_error = 0.1;
|
||||
assert!(ratio <= actual_ratio);
|
||||
assert!(plus_ratio - max_binary_search_error <= ratio);
|
||||
};
|
||||
|
||||
{
|
||||
let mut health_cache = health_cache.clone();
|
||||
adjust_token(&mut health_cache, 3000.0);
|
||||
|
||||
for existing in [-5, 0, 3] {
|
||||
let mut c = health_cache.clone();
|
||||
c.perp_infos[0].base_lots += existing;
|
||||
c.perp_infos[0].quote -= I80F48::from(existing * base_lot_size * 2);
|
||||
|
||||
for side in [PerpOrderSide::Bid, PerpOrderSide::Ask] {
|
||||
println!("test 0: existing {existing}, side {side:?}");
|
||||
for price_factor in [0.8, 1.0, 1.1] {
|
||||
for ratio in 1..=100 {
|
||||
check_max_trade(&health_cache, side, ratio as f64, price_factor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check some extremely bad prices
|
||||
check_max_trade(&health_cache, PerpOrderSide::Bid, 50.0, 2.0);
|
||||
check_max_trade(&health_cache, PerpOrderSide::Ask, 50.0, 0.1);
|
||||
|
||||
// and extremely good prices
|
||||
assert_eq!(
|
||||
find_max_trade(&health_cache, PerpOrderSide::Bid, 50.0, 0.1).0,
|
||||
i64::MAX
|
||||
);
|
||||
assert_eq!(
|
||||
find_max_trade(&health_cache, PerpOrderSide::Ask, 50.0, 1.5).0,
|
||||
i64::MAX
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_health_perp_funding() {
|
||||
let buffer = MangoAccount::default_for_tests().try_to_vec().unwrap();
|
||||
let mut account = MangoAccountValue::from_bytes(&buffer).unwrap();
|
||||
|
||||
let group = Pubkey::new_unique();
|
||||
|
||||
let (mut bank1, mut oracle1) = mock_bank_and_oracle(group, 1, 1.0, 0.2, 0.1);
|
||||
bank1
|
||||
.data()
|
||||
.change_without_fee(
|
||||
account.ensure_token_position(1).unwrap().0,
|
||||
I80F48::from(100),
|
||||
DUMMY_NOW_TS,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut perp1 = mock_perp_market(group, oracle1.pubkey, 1.0, 9, 0.2, 0.1);
|
||||
perp1.data().long_funding = I80F48::from_num(10.1);
|
||||
let perpaccount = account.ensure_perp_position(9, 1).unwrap().0;
|
||||
perpaccount.record_trade(perp1.data(), 10, I80F48::from(-110));
|
||||
perpaccount.long_settled_funding = I80F48::from_num(10.0);
|
||||
|
||||
let oracle1_ai = oracle1.as_account_info();
|
||||
let ais = vec![
|
||||
bank1.as_account_info(),
|
||||
oracle1_ai.clone(),
|
||||
perp1.as_account_info(),
|
||||
oracle1_ai,
|
||||
];
|
||||
|
||||
let retriever = ScanningAccountRetriever::new_with_staleness(&ais, &group, None).unwrap();
|
||||
|
||||
assert!(health_eq(
|
||||
compute_health(&account.borrow(), HealthType::Init, &retriever).unwrap(),
|
||||
// token
|
||||
0.8 * 100.0
|
||||
// perp base
|
||||
+ 0.8 * 100.0
|
||||
// perp quote
|
||||
- 110.0
|
||||
// perp funding (10 * (10.1 - 10.0))
|
||||
- 1.0
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scanning_retreiver_mismatched_oracle_for_perps_throws_error() {
|
||||
let group = Pubkey::new_unique();
|
||||
|
||||
let (mut bank1, mut oracle1) = mock_bank_and_oracle(group, 1, 1.0, 0.2, 0.1);
|
||||
let (mut bank2, mut oracle2) = mock_bank_and_oracle(group, 4, 5.0, 0.5, 0.3);
|
||||
|
||||
let mut oo1 = TestAccount::<OpenOrders>::new_zeroed();
|
||||
|
||||
let mut perp1 = mock_perp_market(group, oracle1.pubkey, 1.0, 9, 0.2, 0.1);
|
||||
let mut perp2 = mock_perp_market(group, oracle2.pubkey, 5.0, 8, 0.2, 0.1);
|
||||
|
||||
let oracle1_account_info = oracle1.as_account_info();
|
||||
let oracle2_account_info = oracle2.as_account_info();
|
||||
let ais = vec![
|
||||
bank1.as_account_info(),
|
||||
bank2.as_account_info(),
|
||||
oracle1_account_info.clone(),
|
||||
oracle2_account_info.clone(),
|
||||
perp1.as_account_info(),
|
||||
perp2.as_account_info(),
|
||||
oracle2_account_info, // Oracles wrong way around
|
||||
oracle1_account_info,
|
||||
oo1.as_account_info(),
|
||||
];
|
||||
|
||||
let retriever = ScanningAccountRetriever::new_with_staleness(&ais, &group, None).unwrap();
|
||||
let result = retriever.perp_market_and_oracle_price(&group, 0, 9);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_health_stable_price_token() {
|
||||
let buffer = MangoAccount::default_for_tests().try_to_vec().unwrap();
|
||||
let mut account = MangoAccountValue::from_bytes(&buffer).unwrap();
|
||||
let buffer2 = MangoAccount::default_for_tests().try_to_vec().unwrap();
|
||||
let mut account2 = MangoAccountValue::from_bytes(&buffer2).unwrap();
|
||||
let buffer3 = MangoAccount::default_for_tests().try_to_vec().unwrap();
|
||||
let mut account3 = MangoAccountValue::from_bytes(&buffer3).unwrap();
|
||||
|
||||
let group = Pubkey::new_unique();
|
||||
|
||||
let (mut bank1, mut oracle1) = mock_bank_and_oracle(group, 1, 1.0, 0.2, 0.1);
|
||||
bank1.data().stable_price_model.stable_price = 0.5;
|
||||
bank1
|
||||
.data()
|
||||
.change_without_fee(
|
||||
account.ensure_token_position(1).unwrap().0,
|
||||
I80F48::from(100),
|
||||
DUMMY_NOW_TS,
|
||||
)
|
||||
.unwrap();
|
||||
bank1
|
||||
.data()
|
||||
.change_without_fee(
|
||||
account2.ensure_token_position(1).unwrap().0,
|
||||
I80F48::from(-100),
|
||||
DUMMY_NOW_TS,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut perp1 = mock_perp_market(group, oracle1.pubkey, 1.0, 9, 0.2, 0.1);
|
||||
perp1.data().stable_price_model.stable_price = 0.5;
|
||||
let perpaccount = account3.ensure_perp_position(9, 1).unwrap().0;
|
||||
perpaccount.record_trade(perp1.data(), 10, I80F48::from(-100));
|
||||
|
||||
let oracle1_ai = oracle1.as_account_info();
|
||||
let ais = vec![
|
||||
bank1.as_account_info(),
|
||||
oracle1_ai.clone(),
|
||||
perp1.as_account_info(),
|
||||
oracle1_ai,
|
||||
];
|
||||
|
||||
let retriever = ScanningAccountRetriever::new_with_staleness(&ais, &group, None).unwrap();
|
||||
|
||||
assert!(health_eq(
|
||||
compute_health(&account.borrow(), HealthType::Init, &retriever).unwrap(),
|
||||
0.8 * 0.5 * 100.0
|
||||
));
|
||||
assert!(health_eq(
|
||||
compute_health(&account.borrow(), HealthType::Maint, &retriever).unwrap(),
|
||||
0.9 * 1.0 * 100.0
|
||||
));
|
||||
assert!(health_eq(
|
||||
compute_health(&account2.borrow(), HealthType::Init, &retriever).unwrap(),
|
||||
-1.2 * 1.0 * 100.0
|
||||
));
|
||||
assert!(health_eq(
|
||||
compute_health(&account2.borrow(), HealthType::Maint, &retriever).unwrap(),
|
||||
-1.1 * 1.0 * 100.0
|
||||
));
|
||||
assert!(health_eq(
|
||||
compute_health(&account3.borrow(), HealthType::Init, &retriever).unwrap(),
|
||||
0.8 * 0.5 * 10.0 * 10.0 - 100.0
|
||||
));
|
||||
assert!(health_eq(
|
||||
compute_health(&account3.borrow(), HealthType::Maint, &retriever).unwrap(),
|
||||
0.9 * 1.0 * 10.0 * 10.0 - 100.0
|
||||
));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
pub use account_retriever::*;
|
||||
pub use cache::*;
|
||||
#[cfg(feature = "client")]
|
||||
pub use client::*;
|
||||
|
||||
mod account_retriever;
|
||||
mod cache;
|
||||
mod client;
|
||||
mod test;
|
|
@ -0,0 +1,123 @@
|
|||
#![cfg(test)]
|
||||
|
||||
use anchor_lang::prelude::*;
|
||||
use fixed::types::I80F48;
|
||||
use serum_dex::state::OpenOrders;
|
||||
use std::cell::RefCell;
|
||||
use std::mem::size_of;
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::state::*;
|
||||
|
||||
pub const DUMMY_NOW_TS: u64 = 0;
|
||||
|
||||
// Implementing TestAccount directly for ZeroCopy + Owner leads to a conflict
|
||||
// because OpenOrders may add impls for those in the future.
|
||||
pub trait MyZeroCopy: anchor_lang::ZeroCopy + Owner {}
|
||||
impl MyZeroCopy for StubOracle {}
|
||||
impl MyZeroCopy for Bank {}
|
||||
impl MyZeroCopy for PerpMarket {}
|
||||
|
||||
pub struct TestAccount<T> {
|
||||
pub bytes: Vec<u8>,
|
||||
pub pubkey: Pubkey,
|
||||
pub owner: Pubkey,
|
||||
pub lamports: u64,
|
||||
_phantom: std::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T> TestAccount<T> {
|
||||
pub fn new(bytes: Vec<u8>, owner: Pubkey) -> Self {
|
||||
Self {
|
||||
bytes,
|
||||
owner,
|
||||
pubkey: Pubkey::new_unique(),
|
||||
lamports: 0,
|
||||
_phantom: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_account_info(&mut self) -> AccountInfo {
|
||||
AccountInfo {
|
||||
key: &self.pubkey,
|
||||
owner: &self.owner,
|
||||
lamports: Rc::new(RefCell::new(&mut self.lamports)),
|
||||
data: Rc::new(RefCell::new(&mut self.bytes)),
|
||||
is_signer: false,
|
||||
is_writable: false,
|
||||
executable: false,
|
||||
rent_epoch: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: MyZeroCopy> TestAccount<T> {
|
||||
pub fn new_zeroed() -> Self {
|
||||
let mut bytes = vec![0u8; 8 + size_of::<T>()];
|
||||
bytes[0..8].copy_from_slice(&T::discriminator());
|
||||
Self::new(bytes, T::owner())
|
||||
}
|
||||
|
||||
pub fn data(&mut self) -> &mut T {
|
||||
bytemuck::from_bytes_mut(&mut self.bytes[8..])
|
||||
}
|
||||
}
|
||||
|
||||
impl TestAccount<OpenOrders> {
|
||||
pub fn new_zeroed() -> Self {
|
||||
let mut bytes = vec![0u8; 12 + size_of::<OpenOrders>()];
|
||||
bytes[0..5].copy_from_slice(b"serum");
|
||||
Self::new(bytes, Pubkey::new_unique())
|
||||
}
|
||||
|
||||
pub fn data(&mut self) -> &mut OpenOrders {
|
||||
bytemuck::from_bytes_mut(&mut self.bytes[5..5 + size_of::<OpenOrders>()])
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mock_bank_and_oracle(
|
||||
group: Pubkey,
|
||||
token_index: TokenIndex,
|
||||
price: f64,
|
||||
init_weights: f64,
|
||||
maint_weights: f64,
|
||||
) -> (TestAccount<Bank>, TestAccount<StubOracle>) {
|
||||
let mut oracle = TestAccount::<StubOracle>::new_zeroed();
|
||||
oracle.data().price = I80F48::from_num(price);
|
||||
let mut bank = TestAccount::<Bank>::new_zeroed();
|
||||
bank.data().token_index = token_index;
|
||||
bank.data().group = group;
|
||||
bank.data().oracle = oracle.pubkey;
|
||||
bank.data().deposit_index = I80F48::from(1_000_000);
|
||||
bank.data().borrow_index = I80F48::from(1_000_000);
|
||||
bank.data().init_asset_weight = I80F48::from_num(1.0 - init_weights);
|
||||
bank.data().init_liab_weight = I80F48::from_num(1.0 + init_weights);
|
||||
bank.data().maint_asset_weight = I80F48::from_num(1.0 - maint_weights);
|
||||
bank.data().maint_liab_weight = I80F48::from_num(1.0 + maint_weights);
|
||||
bank.data().stable_price_model.reset_to_price(price, 0);
|
||||
bank.data().net_borrows_window_size_ts = 1; // dummy
|
||||
bank.data().net_borrows_limit_native = i64::MAX; // max since we don't want this to interfere
|
||||
(bank, oracle)
|
||||
}
|
||||
|
||||
pub fn mock_perp_market(
|
||||
group: Pubkey,
|
||||
oracle: Pubkey,
|
||||
price: f64,
|
||||
market_index: PerpMarketIndex,
|
||||
init_weights: f64,
|
||||
maint_weights: f64,
|
||||
) -> TestAccount<PerpMarket> {
|
||||
let mut pm = TestAccount::<PerpMarket>::new_zeroed();
|
||||
pm.data().group = group;
|
||||
pm.data().oracle = oracle;
|
||||
pm.data().perp_market_index = market_index;
|
||||
pm.data().init_asset_weight = I80F48::from_num(1.0 - init_weights);
|
||||
pm.data().init_liab_weight = I80F48::from_num(1.0 + init_weights);
|
||||
pm.data().maint_asset_weight = I80F48::from_num(1.0 - maint_weights);
|
||||
pm.data().maint_liab_weight = I80F48::from_num(1.0 + maint_weights);
|
||||
pm.data().quote_lot_size = 100;
|
||||
pm.data().base_lot_size = 10;
|
||||
pm.data().stable_price_model.reset_to_price(price, 0);
|
||||
pm
|
||||
}
|
Loading…
Reference in New Issue