Separate health code into multiple files

This commit is contained in:
Christian Kamm 2022-11-29 09:43:59 +01:00
parent d64d9285f4
commit 7bbf045823
6 changed files with 2484 additions and 2431 deletions

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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
));
}
}

View File

@ -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;

View File

@ -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
}