Perp: Clarify has_open_orders use during liquidation (#412)

This commit is contained in:
Christian Kamm 2023-01-23 10:45:45 +01:00 committed by GitHub
parent 308bc307fe
commit b3aabfadfc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 101 additions and 71 deletions

View File

@ -168,7 +168,7 @@ impl<'a> LiquidateHelper<'a> {
> = stream::iter(self.liqee.active_perp_positions())
.then(|pp| async {
let base_lots = pp.base_position_lots();
if base_lots == 0 {
if base_lots == 0 || pp.has_open_taker_fills() {
return Ok(None);
}
let perp = self.client.context.perp(pp.market_index);
@ -505,7 +505,7 @@ impl<'a> LiquidateHelper<'a> {
}
async fn token_liq_bankruptcy(&self) -> anyhow::Result<Option<Signature>> {
if !self.health_cache.can_call_spot_bankruptcy() {
if !self.health_cache.in_phase3_liquidation() || !self.health_cache.has_spot_borrows() {
return Ok(None);
}
@ -557,7 +557,7 @@ impl<'a> LiquidateHelper<'a> {
Ok(Some(sig))
}
async fn send_liq_tx(&self) -> anyhow::Result<Signature> {
async fn send_liq_tx(&self) -> anyhow::Result<Option<Signature>> {
// TODO: Should we make an attempt to settle positive PNL first?
// The problem with it is that small market movements can continuously create
// small amounts of new positive PNL while base_position > 0.
@ -572,10 +572,18 @@ impl<'a> LiquidateHelper<'a> {
//
// TODO: All these close ix could be in one transaction.
if let Some(txsig) = self.perp_close_orders().await? {
return Ok(txsig);
return Ok(Some(txsig));
}
if let Some(txsig) = self.serum3_close_orders().await? {
return Ok(txsig);
return Ok(Some(txsig));
}
if self.health_cache.has_phase1_liquidatable() {
anyhow::bail!(
"Don't know what to do with phase1 liquidatable account {}, maint_health was {}",
self.pubkey,
self.maint_health
);
}
//
@ -583,7 +591,7 @@ impl<'a> LiquidateHelper<'a> {
//
if let Some(txsig) = self.perp_liq_base_position().await? {
return Ok(txsig);
return Ok(Some(txsig));
}
// Now that the perp base positions are zeroed the perp pnl won't
@ -596,7 +604,24 @@ impl<'a> LiquidateHelper<'a> {
// }
if let Some(txsig) = self.token_liq().await? {
return Ok(txsig);
return Ok(Some(txsig));
}
if self.health_cache.has_perp_open_fills() {
log::info!(
"Account {} has open perp fills, maint_health {}, waiting...",
self.pubkey,
self.maint_health
);
return Ok(None);
}
if self.health_cache.has_phase2_liquidatable() {
anyhow::bail!(
"Don't know what to do with phase2 liquidatable account {}, maint_health was {}",
self.pubkey,
self.maint_health
);
}
//
@ -605,12 +630,12 @@ impl<'a> LiquidateHelper<'a> {
// Negative pnl: take over (paid by liqee or insurance) or socialize the loss
if let Some(txsig) = self.perp_liq_quote_and_bankruptcy().await? {
return Ok(txsig);
return Ok(Some(txsig));
}
// Socialize/insurance fund unliquidatable borrows
if let Some(txsig) = self.token_liq_bankruptcy().await? {
return Ok(txsig);
return Ok(Some(txsig));
}
// TODO: What about unliquidatable positive perp pnl?
@ -670,7 +695,7 @@ pub async fn maybe_liquidate_account(
);
// try liquidating
let txsig = LiquidateHelper {
let maybe_txsig = LiquidateHelper {
client: mango_client,
account_fetcher,
pubkey,
@ -684,16 +709,18 @@ pub async fn maybe_liquidate_account(
.send_liq_tx()
.await?;
let slot = account_fetcher.transaction_max_slot(&[txsig]).await?;
if let Err(e) = account_fetcher
.refresh_accounts_via_rpc_until_slot(
&[*pubkey, mango_client.mango_account_address],
slot,
config.refresh_timeout,
)
.await
{
log::info!("could not refresh after liquidation: {}", e);
if let Some(txsig) = maybe_txsig {
let slot = account_fetcher.transaction_max_slot(&[txsig]).await?;
if let Err(e) = account_fetcher
.refresh_accounts_via_rpc_until_slot(
&[*pubkey, mango_client.mango_account_address],
slot,
config.refresh_timeout,
)
.await
{
log::info!("could not refresh after liquidation: {}", e);
}
}
Ok(true)

View File

@ -83,6 +83,8 @@ pub enum MangoError {
HasLiquidatableTrustedPerpPnl,
#[msg("account is frozen")]
AccountIsFrozen,
#[msg("has open perp taker fills")]
HasOpenPerpTakerFills,
}
impl MangoError {

View File

@ -223,6 +223,7 @@ pub struct PerpInfo {
pub quote: I80F48,
pub prices: Prices,
pub has_open_orders: bool,
pub has_open_fills: bool,
}
impl PerpInfo {
@ -251,6 +252,7 @@ impl PerpInfo {
quote: quote_current,
prices,
has_open_orders: perp_position.has_open_orders(),
has_open_fills: perp_position.has_open_taker_fills(),
})
}
@ -452,6 +454,10 @@ impl HealthCache {
self.perp_infos.iter().any(|p| p.base_lots != 0)
}
pub fn has_perp_open_fills(&self) -> bool {
self.perp_infos.iter().any(|p| p.has_open_fills)
}
pub fn has_perp_positive_maint_pnl_without_base_position(&self) -> bool {
self.perp_infos
.iter()
@ -483,11 +489,13 @@ impl HealthCache {
/// Phase2 is for:
/// - token-token liquidation
/// - liquidation of perp base positions
/// - liquidation of perp base positions (an open fill isn't liquidatable, but
/// it changes the base position, so need to wait for it to be processed...)
/// - bringing positive trusted perp pnl into the spot realm
pub fn has_phase2_liquidatable(&self) -> bool {
self.has_spot_assets() && self.has_spot_borrows()
|| self.has_perp_base_positions()
|| self.has_perp_open_fills()
|| self.has_perp_positive_maint_pnl_without_base_position()
}
@ -501,6 +509,10 @@ impl HealthCache {
!self.has_perp_base_positions(),
MangoError::HasLiquidatablePerpBasePosition
);
require!(
!self.has_perp_open_fills(),
MangoError::HasOpenPerpTakerFills
);
require!(
!self.has_perp_positive_maint_pnl_without_base_position(),
MangoError::HasLiquidatableTrustedPerpPnl
@ -525,33 +537,10 @@ impl HealthCache {
&& self.has_phase3_liquidatable()
}
pub fn has_liquidatable_assets(&self) -> bool {
let spot_liquidatable = self.has_spot_assets();
let serum3_cancelable = self.has_serum3_open_orders_funds();
let perp_liquidatable = self.perp_infos.iter().any(|p| {
// can use perp_liq_base_position
p.base_lots != 0
// can use perp_liq_force_cancel_orders
|| p.has_open_orders
// A remaining quote position can be reduced with perp_settle_pnl and that can improve health.
// However, since it's not guaranteed that there is a counterparty, a positive perp quote position
// does not prevent bankruptcy.
});
spot_liquidatable || serum3_cancelable || perp_liquidatable
}
pub fn has_spot_borrows(&self) -> bool {
self.token_infos.iter().any(|ti| ti.balance_native < 0)
}
pub fn has_borrows(&self) -> bool {
let perp_borrows = self
.perp_infos
.iter()
.any(|p| p.quote.is_negative() || p.base_lots != 0);
self.has_spot_borrows() || perp_borrows
}
pub(crate) fn compute_serum3_reservations(
&self,
health_type: HealthType,
@ -598,7 +587,7 @@ impl HealthCache {
(token_max_reserved, serum3_reserved)
}
fn health_sum(&self, health_type: HealthType, mut action: impl FnMut(I80F48)) {
pub(crate) fn health_sum(&self, health_type: HealthType, mut action: impl FnMut(I80F48)) {
for token_info in self.token_infos.iter() {
let contrib = token_info.health_contribution(health_type);
action(contrib);
@ -656,22 +645,6 @@ impl HealthCache {
}
health
}
/// Sum of only the positive health components (assets) and
/// sum of absolute values of all negative health components (liabs, always >= 0)
pub fn health_assets_and_liabs(&self, health_type: HealthType) -> (I80F48, I80F48) {
let mut assets = I80F48::ZERO;
let mut liabs = I80F48::ZERO;
let sum = |contrib| {
if contrib > 0 {
cm!(assets += contrib);
} else {
cm!(liabs -= contrib);
}
};
self.health_sum(health_type, sum);
(assets, liabs)
}
}
pub(crate) fn find_token_info_index(infos: &[TokenInfo], token_index: TokenIndex) -> Result<usize> {

View File

@ -13,10 +13,6 @@ use crate::util::checked_math as cm;
use super::*;
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()
@ -25,6 +21,22 @@ impl HealthCache {
}
}
/// Sum of only the positive health components (assets) and
/// sum of absolute values of all negative health components (liabs, always >= 0)
pub fn health_assets_and_liabs(&self, health_type: HealthType) -> (I80F48, I80F48) {
let mut assets = I80F48::ZERO;
let mut liabs = I80F48::ZERO;
let sum = |contrib| {
if contrib > 0 {
cm!(assets += contrib);
} else {
cm!(liabs -= contrib);
}
};
self.health_sum(health_type, sum);
(assets, liabs)
}
/// The health ratio is
/// - 0 if health is 0 - meaning assets = liabs
/// - 100 if there's 2x as many assets as liabs
@ -951,6 +963,7 @@ mod tests {
quote: I80F48::ZERO,
prices: Prices::new_single_price(I80F48::from_num(2.0)),
has_open_orders: false,
has_open_fills: false,
};
let health_cache = HealthCache {

View File

@ -88,6 +88,11 @@ pub fn perp_liq_base_position(
// Fetch perp positions for accounts, creating for the liqor if needed
let liqee_perp_position = liqee.perp_position_mut(perp_market_index)?;
require!(
!liqee_perp_position.has_open_taker_fills(),
MangoError::HasOpenPerpTakerFills
);
let liqor_perp_position = liqor
.ensure_perp_position(perp_market_index, perp_market.settle_token_index)?
.0;

View File

@ -283,7 +283,7 @@ pub fn token_liq_with_token(
liab_transfer: liab_transfer.to_bits(),
asset_price: asset_price.to_bits(),
liab_price: liab_price.to_bits(),
bankruptcy: !liqee_health_cache.has_liquidatable_assets() & liqee_init_health.is_negative()
bankruptcy: !liqee_health_cache.has_phase2_liquidatable() & liqee_init_health.is_negative()
});
Ok(())

View File

@ -575,12 +575,22 @@ impl PerpPosition {
cm!(self.quote_position_native += quote_change_native);
}
/// Does the perp position have any open orders or fill events?
/// Does the user have any orders on the book?
///
/// Note that it's possible they were matched already: This only becomes
/// false when the fill event is processed or the orders are cancelled.
pub fn has_open_orders(&self) -> bool {
self.asks_base_lots != 0
|| self.bids_base_lots != 0
|| self.taker_base_lots != 0
|| self.taker_quote_lots != 0
self.asks_base_lots != 0 || self.bids_base_lots != 0
}
// Did the user take orders and hasn't been filled yet?
pub fn has_open_taker_fills(&self) -> bool {
self.taker_base_lots != 0 || self.taker_quote_lots != 0
}
/// Are there any open orders or fills that haven't been processed yet?
pub fn has_open_orders_or_fills(&self) -> bool {
self.has_open_orders() || self.has_open_taker_fills()
}
/// Calculate the average entry price of the position, in native/native units