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())
|
> = 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)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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(())
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue