PlacePerpOrder: pre-health computation

This commit is contained in:
Christian Kamm 2022-08-24 16:07:22 +02:00
parent 38b349a401
commit bba27ed6f0
9 changed files with 211 additions and 142 deletions

View File

@ -19,6 +19,8 @@ pub enum MangoError {
InvalidFlashLoanTargetCpiProgram,
#[msg("health must be positive")]
HealthMustBePositive,
#[msg("health must be positive or increase")]
HealthMustBePositiveOrIncrease,
#[msg("health must be negative")]
HealthMustBeNegative,
#[msg("the account is bankrupt")]

View File

@ -401,7 +401,7 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>(
msg!("post_health {:?}", post_health);
require!(
post_health >= 0 || post_health > pre_health,
MangoError::HealthMustBePositive
MangoError::HealthMustBePositiveOrIncrease
);
account
.fixed

View File

@ -115,7 +115,7 @@ pub fn health_region_end<'key, 'accounts, 'remaining, 'info>(
msg!("post_health {:?}", post_health);
require!(
post_health >= 0 || post_health > account.fixed.health_region_begin_init_health,
MangoError::HealthMustBePositive
MangoError::HealthMustBePositiveOrIncrease
);
account
.fixed

View File

@ -4,7 +4,7 @@ use crate::accounts_zerocopy::*;
use crate::error::*;
use crate::state::MangoAccount;
use crate::state::{
compute_health, new_fixed_order_account_retriever, oracle_price, AccountLoaderDynamic, Book,
new_fixed_order_account_retriever, new_health_cache, oracle_price, AccountLoaderDynamic, Book,
BookSide, EventQueue, Group, HealthType, OrderType, PerpMarket, Side,
};
@ -83,62 +83,97 @@ pub fn perp_place_order(
let account_pk = ctx.accounts.account.key();
{
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
let bids = ctx.accounts.bids.load_mut()?;
let asks = ctx.accounts.asks.load_mut()?;
let mut book = Book::new(bids, asks);
let perp_market_index = {
let perp_market = ctx.accounts.perp_market.load()?;
perp_market.perp_market_index
};
let (_, perp_position_raw_index) = account.ensure_perp_position(perp_market_index)?;
let mut event_queue = ctx.accounts.event_queue.load_mut()?;
let oracle_price = oracle_price(
&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?,
perp_market.oracle_config.conf_filter,
perp_market.base_token_decimals,
)?;
let now_ts = Clock::get()?.unix_timestamp as u64;
let time_in_force = if expiry_timestamp != 0 {
// If expiry is far in the future, clamp to 255 seconds
let tif = expiry_timestamp.saturating_sub(now_ts).min(255);
if tif == 0 {
// If expiry is in the past, ignore the order
msg!("Order is already expired");
return Ok(());
}
tif as u8
} else {
// Never expire
0
};
// TODO reduce_only based on event queue
book.new_order(
side,
&mut perp_market,
&mut event_queue,
oracle_price,
&mut account.borrow_mut(),
&account_pk,
price_lots,
max_base_lots,
max_quote_lots,
order_type,
time_in_force,
client_order_id,
now_ts,
limit,
)?;
}
if !account.fixed.is_in_health_region() {
//
// Pre-health computation, _after_ perp position is created
//
let pre_health_opt = if !account.fixed.is_in_health_region() {
let retriever =
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
let health = compute_health(&account.borrow(), HealthType::Init, &retriever)?;
msg!("health: {}", health);
require!(health >= 0, MangoError::HealthMustBePositive);
account.fixed.maybe_recover_from_being_liquidated(health);
let health_cache =
new_health_cache(&account.borrow(), &retriever).context("pre-withdraw init health")?;
let pre_health = health_cache.health(HealthType::Init);
msg!("pre_health: {}", pre_health);
account
.fixed
.maybe_recover_from_being_liquidated(pre_health);
require!(
!account.fixed.being_liquidated(),
MangoError::BeingLiquidated
);
Some((health_cache, pre_health))
} else {
None
};
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
let bids = ctx.accounts.bids.load_mut()?;
let asks = ctx.accounts.asks.load_mut()?;
let mut book = Book::new(bids, asks);
let mut event_queue = ctx.accounts.event_queue.load_mut()?;
let oracle_price = oracle_price(
&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?,
perp_market.oracle_config.conf_filter,
perp_market.base_token_decimals,
)?;
let now_ts = Clock::get()?.unix_timestamp as u64;
let time_in_force = if expiry_timestamp != 0 {
// If expiry is far in the future, clamp to 255 seconds
let tif = expiry_timestamp.saturating_sub(now_ts).min(255);
if tif == 0 {
// If expiry is in the past, ignore the order
msg!("Order is already expired");
return Ok(());
}
tif as u8
} else {
// Never expire
0
};
// TODO reduce_only based on event queue
book.new_order(
side,
&mut perp_market,
&mut event_queue,
oracle_price,
&mut account.borrow_mut(),
&account_pk,
price_lots,
max_base_lots,
max_quote_lots,
order_type,
time_in_force,
client_order_id,
now_ts,
limit,
)?;
//
// Health check
//
if let Some((mut health_cache, pre_health)) = pre_health_opt {
let perp_position = account.perp_position_by_raw_index(perp_position_raw_index);
health_cache.recompute_perp_info(perp_position, &perp_market)?;
let post_health = health_cache.health(HealthType::Init);
msg!("post_health: {}", post_health);
require!(
post_health >= 0 || post_health > pre_health,
MangoError::HealthMustBePositiveOrIncrease
);
account
.fixed
.maybe_recover_from_being_liquidated(post_health);
}
Ok(())

View File

@ -337,7 +337,7 @@ pub fn serum3_place_order(
msg!("post_health: {}", post_health);
require!(
post_health >= 0 || post_health > pre_health,
MangoError::HealthMustBePositive
MangoError::HealthMustBePositiveOrIncrease
);
account
.fixed

View File

@ -156,7 +156,7 @@ pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bo
msg!("post_health: {}", post_health);
require!(
post_health >= 0 || post_health > pre_health,
MangoError::HealthMustBePositive
MangoError::HealthMustBePositiveOrIncrease
);
account
.fixed

View File

@ -11,7 +11,9 @@ use std::collections::HashMap;
use crate::accounts_zerocopy::*;
use crate::error::*;
use crate::serum3_cpi;
use crate::state::{Bank, PerpMarket, PerpMarketIndex, Serum3MarketIndex, TokenIndex};
use crate::state::{
Bank, PerpMarket, PerpMarketIndex, PerpPosition, Serum3MarketIndex, TokenIndex,
};
use crate::util::checked_math as cm;
use super::MangoAccountRef;
@ -471,6 +473,7 @@ impl Serum3Info {
#[derive(Clone, AnchorDeserialize, AnchorSerialize)]
pub struct PerpInfo {
perp_market_index: PerpMarketIndex,
maint_asset_weight: I80F48,
init_asset_weight: I80F48,
maint_liab_weight: I80F48,
@ -482,6 +485,93 @@ pub struct PerpInfo {
}
impl PerpInfo {
fn new(
perp_position: &PerpPosition,
perp_market: &PerpMarket,
token_infos: &[TokenInfo],
) -> Result<Self> {
// find the TokenInfos for the market's base and quote tokens
let base_index = find_token_info_index(&token_infos, perp_market.base_token_index)?;
// TODO: base_index could be unset
let base_info = &token_infos[base_index];
let base_lot_size = I80F48::from(perp_market.base_lot_size);
let base_lots = cm!(perp_position.base_position_lots + perp_position.taker_base_lots);
let taker_quote = I80F48::from(cm!(
perp_position.taker_quote_lots * perp_market.quote_lot_size
));
let quote_current = cm!(perp_position.quote_position_native + taker_quote);
// Two scenarios:
// 1. The price goes low and all bids execute, converting to base.
// That means the perp position is increased by `bids` and the quote position
// is decreased by `bids * base_lot_size * price`.
// The health for this case is:
// (weighted(base_lots + bids) - bids) * base_lot_size * price + quote
// 2. The price goes high and all asks execute, converting to quote.
// The health for this case is:
// (weighted(base_lots - asks) + asks) * base_lot_size * price + quote
//
// Comparing these makes it clear we need to pick the worse subfactor
// weighted(base_lots + bids) - bids =: scenario1
// or
// weighted(base_lots - asks) + asks =: scenario2
//
// Additionally, we want this scenario choice to be the same no matter whether we're
// computing init or maint health. This can be guaranteed by requiring the weights
// to satisfy the property (P):
//
// (1 - init_asset_weight) / (init_liab_weight - 1)
// == (1 - maint_asset_weight) / (maint_liab_weight - 1)
//
// Derivation:
// Set asks_net_lots := base_lots - asks, bids_net_lots := base_lots + bids.
// Now
// scenario1 = weighted(bids_net_lots) - bids_net_lots + base_lots and
// scenario2 = weighted(asks_net_lots) - asks_net_lots + base_lots
// So with expanding weigthed(a) = weight_factor_for_a * a, the question
// scenario1 < scenario2
// becomes:
// (weight_factor_for_bids_net_lots - 1) * bids_net_lots
// < (weight_factor_for_asks_net_lots - 1) * asks_net_lots
// Since asks_net_lots < 0 and bids_net_lots > 0 is the only interesting case, (P) follows.
//
// We satisfy (P) by requiring
// asset_weight = 1 - x and liab_weight = 1 + x
//
// And with that assumption the scenario choice condition further simplifies to:
// scenario1 < scenario2
// iff abs(bids_net_lots) > abs(asks_net_lots)
let bids_net_lots = cm!(base_lots + perp_position.bids_base_lots);
let asks_net_lots = cm!(base_lots - perp_position.asks_base_lots);
let lots_to_quote = base_lot_size * base_info.oracle_price;
let base;
let quote;
if cm!(bids_net_lots.abs()) > cm!(asks_net_lots.abs()) {
let bids_net_lots = I80F48::from(bids_net_lots);
let bids_base_lots = I80F48::from(perp_position.bids_base_lots);
base = cm!(bids_net_lots * lots_to_quote);
quote = cm!(quote_current - bids_base_lots * lots_to_quote);
} else {
let asks_net_lots = I80F48::from(asks_net_lots);
let asks_base_lots = I80F48::from(perp_position.asks_base_lots);
base = cm!(asks_net_lots * lots_to_quote);
quote = cm!(quote_current + asks_base_lots * lots_to_quote);
};
Ok(Self {
perp_market_index: perp_market.perp_market_index,
init_asset_weight: perp_market.init_asset_weight,
init_liab_weight: perp_market.init_liab_weight,
maint_asset_weight: perp_market.maint_asset_weight,
maint_liab_weight: perp_market.maint_liab_weight,
base,
quote,
})
}
/// Total health contribution from perp balances
///
/// Due to isolation of perp markets, users may never borrow against perp
@ -591,6 +681,20 @@ impl HealthCache {
Ok(())
}
pub fn recompute_perp_info(
&mut self,
perp_position: &PerpPosition,
perp_market: &PerpMarket,
) -> Result<()> {
let perp_entry = self
.perp_infos
.iter_mut()
.find(|m| m.perp_market_index == perp_market.perp_market_index)
.ok_or_else(|| error_msg!("perp market {} not found", perp_market.perp_market_index))?;
*perp_entry = PerpInfo::new(perp_position, perp_market, &self.token_infos)?;
Ok(())
}
pub fn has_liquidatable_assets(&self) -> bool {
let spot_liquidatable = self
.token_infos
@ -894,89 +998,10 @@ pub fn new_health_cache(
// TODO: also account for perp funding in health
// health contribution from perp accounts
let mut perp_infos = Vec::with_capacity(account.active_perp_positions().count());
for (i, perp_account) in account.active_perp_positions().enumerate() {
for (i, perp_position) in account.active_perp_positions().enumerate() {
let perp_market =
retriever.perp_market(&account.fixed.group, i, perp_account.market_index)?;
// find the TokenInfos for the market's base and quote tokens
let base_index = find_token_info_index(&token_infos, perp_market.base_token_index)?;
// TODO: base_index could be unset
let base_info = &token_infos[base_index];
let base_lot_size = I80F48::from(perp_market.base_lot_size);
let base_lots = cm!(perp_account.base_position_lots + perp_account.taker_base_lots);
let taker_quote = I80F48::from(cm!(
perp_account.taker_quote_lots * perp_market.quote_lot_size
));
let quote_current = cm!(perp_account.quote_position_native + taker_quote);
// Two scenarios:
// 1. The price goes low and all bids execute, converting to base.
// That means the perp position is increased by `bids` and the quote position
// is decreased by `bids * base_lot_size * price`.
// The health for this case is:
// (weighted(base_lots + bids) - bids) * base_lot_size * price + quote
// 2. The price goes high and all asks execute, converting to quote.
// The health for this case is:
// (weighted(base_lots - asks) + asks) * base_lot_size * price + quote
//
// Comparing these makes it clear we need to pick the worse subfactor
// weighted(base_lots + bids) - bids =: scenario1
// or
// weighted(base_lots - asks) + asks =: scenario2
//
// Additionally, we want this scenario choice to be the same no matter whether we're
// computing init or maint health. This can be guaranteed by requiring the weights
// to satisfy the property (P):
//
// (1 - init_asset_weight) / (init_liab_weight - 1)
// == (1 - maint_asset_weight) / (maint_liab_weight - 1)
//
// Derivation:
// Set asks_net_lots := base_lots - asks, bids_net_lots := base_lots + bids.
// Now
// scenario1 = weighted(bids_net_lots) - bids_net_lots + base_lots and
// scenario2 = weighted(asks_net_lots) - asks_net_lots + base_lots
// So with expanding weigthed(a) = weight_factor_for_a * a, the question
// scenario1 < scenario2
// becomes:
// (weight_factor_for_bids_net_lots - 1) * bids_net_lots
// < (weight_factor_for_asks_net_lots - 1) * asks_net_lots
// Since asks_net_lots < 0 and bids_net_lots > 0 is the only interesting case, (P) follows.
//
// We satisfy (P) by requiring
// asset_weight = 1 - x and liab_weight = 1 + x
//
// And with that assumption the scenario choice condition further simplifies to:
// scenario1 < scenario2
// iff abs(bids_net_lots) > abs(asks_net_lots)
let bids_net_lots = cm!(base_lots + perp_account.bids_base_lots);
let asks_net_lots = cm!(base_lots - perp_account.asks_base_lots);
let lots_to_quote = base_lot_size * base_info.oracle_price;
let base;
let quote;
if cm!(bids_net_lots.abs()) > cm!(asks_net_lots.abs()) {
let bids_net_lots = I80F48::from(bids_net_lots);
let bids_base_lots = I80F48::from(perp_account.bids_base_lots);
base = cm!(bids_net_lots * lots_to_quote);
quote = cm!(quote_current - bids_base_lots * lots_to_quote);
} else {
let asks_net_lots = I80F48::from(asks_net_lots);
let asks_base_lots = I80F48::from(perp_account.asks_base_lots);
base = cm!(asks_net_lots * lots_to_quote);
quote = cm!(quote_current + asks_base_lots * lots_to_quote);
};
perp_infos.push(PerpInfo {
init_asset_weight: perp_market.init_asset_weight,
init_liab_weight: perp_market.init_liab_weight,
maint_asset_weight: perp_market.maint_asset_weight,
maint_liab_weight: perp_market.maint_liab_weight,
base,
quote,
});
retriever.perp_market(&account.fixed.group, i, perp_position.market_index)?;
perp_infos.push(PerpInfo::new(perp_position, perp_market, &token_infos)?);
}
Ok(HealthCache {

View File

@ -141,7 +141,7 @@ async fn test_health_wrap() -> Result<(), TransportError> {
// errors due to health
assert!(logs
.iter()
.any(|line| line.contains("Error Code: HealthMustBePositive")));
.any(|line| line.contains("Error Code: HealthMustBePositiveOrIncrease")));
}
//

View File

@ -204,6 +204,7 @@ async fn test_perp() -> Result<(), TransportError> {
)
.await
.unwrap();
check_prev_instruction_post_health(&solana, account_0).await;
let order_id_to_cancel = solana
.get_account::<MangoAccount>(account_0)
@ -250,6 +251,7 @@ async fn test_perp() -> Result<(), TransportError> {
)
.await
.unwrap();
check_prev_instruction_post_health(&solana, account_0).await;
send_tx(
solana,
@ -291,6 +293,7 @@ async fn test_perp() -> Result<(), TransportError> {
)
.await
.unwrap();
check_prev_instruction_post_health(&solana, account_0).await;
send_tx(
solana,
PerpPlaceOrderInstruction {
@ -311,6 +314,7 @@ async fn test_perp() -> Result<(), TransportError> {
)
.await
.unwrap();
check_prev_instruction_post_health(&solana, account_0).await;
send_tx(
solana,
PerpPlaceOrderInstruction {
@ -331,6 +335,7 @@ async fn test_perp() -> Result<(), TransportError> {
)
.await
.unwrap();
check_prev_instruction_post_health(&solana, account_0).await;
send_tx(
solana,
@ -371,6 +376,7 @@ async fn test_perp() -> Result<(), TransportError> {
)
.await
.unwrap();
check_prev_instruction_post_health(&solana, account_0).await;
send_tx(
solana,
@ -392,6 +398,7 @@ async fn test_perp() -> Result<(), TransportError> {
)
.await
.unwrap();
check_prev_instruction_post_health(&solana, account_1).await;
send_tx(
solana,