Perp: Clarify has_open_orders use during liquidation (#412)
This commit is contained in:
parent
308bc307fe
commit
b3aabfadfc
|
@ -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)
|
||||
|
|
|
@ -83,6 +83,8 @@ pub enum MangoError {
|
|||
HasLiquidatableTrustedPerpPnl,
|
||||
#[msg("account is frozen")]
|
||||
AccountIsFrozen,
|
||||
#[msg("has open perp taker fills")]
|
||||
HasOpenPerpTakerFills,
|
||||
}
|
||||
|
||||
impl MangoError {
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(())
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue