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()) > = stream::iter(self.liqee.active_perp_positions())
.then(|pp| async { .then(|pp| async {
let base_lots = pp.base_position_lots(); let base_lots = pp.base_position_lots();
if base_lots == 0 { if base_lots == 0 || pp.has_open_taker_fills() {
return Ok(None); return Ok(None);
} }
let perp = self.client.context.perp(pp.market_index); 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>> { 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); return Ok(None);
} }
@ -557,7 +557,7 @@ impl<'a> LiquidateHelper<'a> {
Ok(Some(sig)) 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? // TODO: Should we make an attempt to settle positive PNL first?
// The problem with it is that small market movements can continuously create // The problem with it is that small market movements can continuously create
// small amounts of new positive PNL while base_position > 0. // 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. // TODO: All these close ix could be in one transaction.
if let Some(txsig) = self.perp_close_orders().await? { 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? { 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? { 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 // 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? { 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 // Negative pnl: take over (paid by liqee or insurance) or socialize the loss
if let Some(txsig) = self.perp_liq_quote_and_bankruptcy().await? { if let Some(txsig) = self.perp_liq_quote_and_bankruptcy().await? {
return Ok(txsig); return Ok(Some(txsig));
} }
// Socialize/insurance fund unliquidatable borrows // Socialize/insurance fund unliquidatable borrows
if let Some(txsig) = self.token_liq_bankruptcy().await? { if let Some(txsig) = self.token_liq_bankruptcy().await? {
return Ok(txsig); return Ok(Some(txsig));
} }
// TODO: What about unliquidatable positive perp pnl? // TODO: What about unliquidatable positive perp pnl?
@ -670,7 +695,7 @@ pub async fn maybe_liquidate_account(
); );
// try liquidating // try liquidating
let txsig = LiquidateHelper { let maybe_txsig = LiquidateHelper {
client: mango_client, client: mango_client,
account_fetcher, account_fetcher,
pubkey, pubkey,
@ -684,16 +709,18 @@ pub async fn maybe_liquidate_account(
.send_liq_tx() .send_liq_tx()
.await?; .await?;
let slot = account_fetcher.transaction_max_slot(&[txsig]).await?; if let Some(txsig) = maybe_txsig {
if let Err(e) = account_fetcher let slot = account_fetcher.transaction_max_slot(&[txsig]).await?;
.refresh_accounts_via_rpc_until_slot( if let Err(e) = account_fetcher
&[*pubkey, mango_client.mango_account_address], .refresh_accounts_via_rpc_until_slot(
slot, &[*pubkey, mango_client.mango_account_address],
config.refresh_timeout, slot,
) config.refresh_timeout,
.await )
{ .await
log::info!("could not refresh after liquidation: {}", e); {
log::info!("could not refresh after liquidation: {}", e);
}
} }
Ok(true) Ok(true)

View File

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

View File

@ -223,6 +223,7 @@ pub struct PerpInfo {
pub quote: I80F48, pub quote: I80F48,
pub prices: Prices, pub prices: Prices,
pub has_open_orders: bool, pub has_open_orders: bool,
pub has_open_fills: bool,
} }
impl PerpInfo { impl PerpInfo {
@ -251,6 +252,7 @@ impl PerpInfo {
quote: quote_current, quote: quote_current,
prices, prices,
has_open_orders: perp_position.has_open_orders(), 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) 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 { pub fn has_perp_positive_maint_pnl_without_base_position(&self) -> bool {
self.perp_infos self.perp_infos
.iter() .iter()
@ -483,11 +489,13 @@ impl HealthCache {
/// Phase2 is for: /// Phase2 is for:
/// - token-token liquidation /// - 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 /// - bringing positive trusted perp pnl into the spot realm
pub fn has_phase2_liquidatable(&self) -> bool { pub fn has_phase2_liquidatable(&self) -> bool {
self.has_spot_assets() && self.has_spot_borrows() self.has_spot_assets() && self.has_spot_borrows()
|| self.has_perp_base_positions() || self.has_perp_base_positions()
|| self.has_perp_open_fills()
|| self.has_perp_positive_maint_pnl_without_base_position() || self.has_perp_positive_maint_pnl_without_base_position()
} }
@ -501,6 +509,10 @@ impl HealthCache {
!self.has_perp_base_positions(), !self.has_perp_base_positions(),
MangoError::HasLiquidatablePerpBasePosition MangoError::HasLiquidatablePerpBasePosition
); );
require!(
!self.has_perp_open_fills(),
MangoError::HasOpenPerpTakerFills
);
require!( require!(
!self.has_perp_positive_maint_pnl_without_base_position(), !self.has_perp_positive_maint_pnl_without_base_position(),
MangoError::HasLiquidatableTrustedPerpPnl MangoError::HasLiquidatableTrustedPerpPnl
@ -525,33 +537,10 @@ impl HealthCache {
&& self.has_phase3_liquidatable() && 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 { pub fn has_spot_borrows(&self) -> bool {
self.token_infos.iter().any(|ti| ti.balance_native < 0) 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( pub(crate) fn compute_serum3_reservations(
&self, &self,
health_type: HealthType, health_type: HealthType,
@ -598,7 +587,7 @@ impl HealthCache {
(token_max_reserved, serum3_reserved) (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() { for token_info in self.token_infos.iter() {
let contrib = token_info.health_contribution(health_type); let contrib = token_info.health_contribution(health_type);
action(contrib); action(contrib);
@ -656,22 +645,6 @@ impl HealthCache {
} }
health 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> { 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::*; use super::*;
impl HealthCache { impl HealthCache {
pub fn can_call_spot_bankruptcy(&self) -> bool {
!self.has_liquidatable_assets() && self.has_spot_borrows()
}
pub fn is_liquidatable(&self) -> bool { pub fn is_liquidatable(&self) -> bool {
if self.being_liquidated { if self.being_liquidated {
self.health(HealthType::Init).is_negative() 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 /// The health ratio is
/// - 0 if health is 0 - meaning assets = liabs /// - 0 if health is 0 - meaning assets = liabs
/// - 100 if there's 2x as many assets as liabs /// - 100 if there's 2x as many assets as liabs
@ -951,6 +963,7 @@ mod tests {
quote: I80F48::ZERO, quote: I80F48::ZERO,
prices: Prices::new_single_price(I80F48::from_num(2.0)), prices: Prices::new_single_price(I80F48::from_num(2.0)),
has_open_orders: false, has_open_orders: false,
has_open_fills: false,
}; };
let health_cache = HealthCache { 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 // Fetch perp positions for accounts, creating for the liqor if needed
let liqee_perp_position = liqee.perp_position_mut(perp_market_index)?; 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 let liqor_perp_position = liqor
.ensure_perp_position(perp_market_index, perp_market.settle_token_index)? .ensure_perp_position(perp_market_index, perp_market.settle_token_index)?
.0; .0;

View File

@ -283,7 +283,7 @@ pub fn token_liq_with_token(
liab_transfer: liab_transfer.to_bits(), liab_transfer: liab_transfer.to_bits(),
asset_price: asset_price.to_bits(), asset_price: asset_price.to_bits(),
liab_price: liab_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(()) Ok(())

View File

@ -575,12 +575,22 @@ impl PerpPosition {
cm!(self.quote_position_native += quote_change_native); 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 { pub fn has_open_orders(&self) -> bool {
self.asks_base_lots != 0 self.asks_base_lots != 0 || self.bids_base_lots != 0
|| self.bids_base_lots != 0 }
|| self.taker_base_lots != 0
|| self.taker_quote_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 /// Calculate the average entry price of the position, in native/native units