liquidator: settle pnl on liquidatable accounts
This commit is contained in:
parent
11daf4d0eb
commit
15d0a98c94
|
@ -18,12 +18,12 @@ pub fn fetch_top(
|
||||||
context: &crate::context::MangoGroupContext,
|
context: &crate::context::MangoGroupContext,
|
||||||
account_fetcher: &impl AccountFetcher,
|
account_fetcher: &impl AccountFetcher,
|
||||||
perp_market_index: PerpMarketIndex,
|
perp_market_index: PerpMarketIndex,
|
||||||
perp_market_address: &Pubkey,
|
|
||||||
direction: Direction,
|
direction: Direction,
|
||||||
count: usize,
|
count: usize,
|
||||||
) -> anyhow::Result<Vec<(Pubkey, MangoAccountValue, I80F48)>> {
|
) -> anyhow::Result<Vec<(Pubkey, MangoAccountValue, I80F48)>> {
|
||||||
|
let perp = context.perp(perp_market_index);
|
||||||
let perp_market =
|
let perp_market =
|
||||||
account_fetcher_fetch_anchor_account::<PerpMarket>(account_fetcher, perp_market_address)?;
|
account_fetcher_fetch_anchor_account::<PerpMarket>(account_fetcher, &perp.address)?;
|
||||||
let oracle_acc = account_fetcher.fetch_raw_account(&perp_market.oracle)?;
|
let oracle_acc = account_fetcher.fetch_raw_account(&perp_market.oracle)?;
|
||||||
let oracle_price =
|
let oracle_price =
|
||||||
perp_market.oracle_price(&KeyedAccountSharedData::new(perp_market.oracle, oracle_acc))?;
|
perp_market.oracle_price(&KeyedAccountSharedData::new(perp_market.oracle, oracle_acc))?;
|
||||||
|
@ -99,5 +99,5 @@ pub fn fetch_top(
|
||||||
}
|
}
|
||||||
|
|
||||||
// return highest abs pnl accounts
|
// return highest abs pnl accounts
|
||||||
Ok(accounts_pnl[0..count].to_vec())
|
Ok(accounts_pnl.into_iter().take(count).collect::<Vec<_>>())
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,10 @@ use std::time::Duration;
|
||||||
use client::{chain_data, health_cache, AccountFetcher, MangoClient, MangoClientError};
|
use client::{chain_data, health_cache, AccountFetcher, MangoClient, MangoClientError};
|
||||||
use mango_v4::accounts_zerocopy::KeyedAccountSharedData;
|
use mango_v4::accounts_zerocopy::KeyedAccountSharedData;
|
||||||
use mango_v4::state::{
|
use mango_v4::state::{
|
||||||
Bank, HealthType, PerpMarketIndex, Serum3Orders, Side, TokenIndex, QUOTE_TOKEN_INDEX,
|
Bank, HealthCache, HealthType, MangoAccountValue, PerpMarketIndex, Serum3Orders, Side,
|
||||||
|
TokenIndex, QUOTE_TOKEN_INDEX,
|
||||||
};
|
};
|
||||||
|
use solana_sdk::signature::Signature;
|
||||||
|
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use rand::seq::SliceRandom;
|
use rand::seq::SliceRandom;
|
||||||
|
@ -69,6 +71,451 @@ pub fn jupiter_market_can_sell(
|
||||||
.is_ok()
|
.is_ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct LiquidateHelper<'a> {
|
||||||
|
client: &'a MangoClient,
|
||||||
|
account_fetcher: &'a chain_data::AccountFetcher,
|
||||||
|
pubkey: &'a Pubkey,
|
||||||
|
liqee: &'a MangoAccountValue,
|
||||||
|
health_cache: &'a HealthCache,
|
||||||
|
maint_health: I80F48,
|
||||||
|
liqor_min_health_ratio: I80F48,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> LiquidateHelper<'a> {
|
||||||
|
fn serum3_close_orders(&self) -> anyhow::Result<Option<Signature>> {
|
||||||
|
// look for any open serum orders or settleable balances
|
||||||
|
let serum_force_cancels = self
|
||||||
|
.liqee
|
||||||
|
.active_serum3_orders()
|
||||||
|
.map(|orders| {
|
||||||
|
let open_orders_account = self
|
||||||
|
.account_fetcher
|
||||||
|
.fetch_raw_account(&orders.open_orders)?;
|
||||||
|
let open_orders = mango_v4::serum3_cpi::load_open_orders(&open_orders_account)?;
|
||||||
|
let can_force_cancel = open_orders.native_coin_total > 0
|
||||||
|
|| open_orders.native_pc_total > 0
|
||||||
|
|| open_orders.referrer_rebates_accrued > 0;
|
||||||
|
if can_force_cancel {
|
||||||
|
Ok(Some(*orders))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter_map_ok(|v| v)
|
||||||
|
.collect::<anyhow::Result<Vec<Serum3Orders>>>()?;
|
||||||
|
if serum_force_cancels.is_empty() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
// Cancel all orders on a random serum market
|
||||||
|
let serum_orders = serum_force_cancels.choose(&mut rand::thread_rng()).unwrap();
|
||||||
|
let sig = self.client.serum3_liq_force_cancel_orders(
|
||||||
|
(self.pubkey, &self.liqee),
|
||||||
|
serum_orders.market_index,
|
||||||
|
&serum_orders.open_orders,
|
||||||
|
)?;
|
||||||
|
log::info!(
|
||||||
|
"Force cancelled serum orders on account {}, market index {}, maint_health was {}, tx sig {:?}",
|
||||||
|
self.pubkey,
|
||||||
|
serum_orders.market_index,
|
||||||
|
self.maint_health,
|
||||||
|
sig
|
||||||
|
);
|
||||||
|
Ok(Some(sig))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn perp_close_orders(&self) -> anyhow::Result<Option<Signature>> {
|
||||||
|
let perp_force_cancels = self
|
||||||
|
.liqee
|
||||||
|
.active_perp_positions()
|
||||||
|
.filter_map(|pp| pp.has_open_orders().then(|| pp.market_index))
|
||||||
|
.collect::<Vec<PerpMarketIndex>>();
|
||||||
|
if perp_force_cancels.is_empty() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel all orders on a random perp market
|
||||||
|
let perp_market_index = *perp_force_cancels.choose(&mut rand::thread_rng()).unwrap();
|
||||||
|
let sig = self
|
||||||
|
.client
|
||||||
|
.perp_liq_force_cancel_orders((self.pubkey, &self.liqee), perp_market_index)?;
|
||||||
|
log::info!(
|
||||||
|
"Force cancelled perp orders on account {}, market index {}, maint_health was {}, tx sig {:?}",
|
||||||
|
self.pubkey,
|
||||||
|
perp_market_index,
|
||||||
|
self.maint_health,
|
||||||
|
sig
|
||||||
|
);
|
||||||
|
Ok(Some(sig))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn liq_perp_base_position(&self) -> anyhow::Result<Option<Signature>> {
|
||||||
|
let mut perp_base_positions = self
|
||||||
|
.liqee
|
||||||
|
.active_perp_positions()
|
||||||
|
.map(|pp| {
|
||||||
|
let base_lots = pp.base_position_lots();
|
||||||
|
if base_lots == 0 {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let perp = self.client.context.perp(pp.market_index);
|
||||||
|
let oracle = self
|
||||||
|
.account_fetcher
|
||||||
|
.fetch_raw_account(&perp.market.oracle)?;
|
||||||
|
let price = perp.market.oracle_price(&KeyedAccountSharedData::new(
|
||||||
|
perp.market.oracle,
|
||||||
|
oracle.into(),
|
||||||
|
))?;
|
||||||
|
Ok(Some((
|
||||||
|
pp.market_index,
|
||||||
|
base_lots,
|
||||||
|
price,
|
||||||
|
I80F48::from(base_lots.abs()) * price,
|
||||||
|
)))
|
||||||
|
})
|
||||||
|
.filter_map_ok(|v| v)
|
||||||
|
.collect::<anyhow::Result<Vec<(PerpMarketIndex, i64, I80F48, I80F48)>>>()?;
|
||||||
|
perp_base_positions.sort_by(|a, b| a.3.cmp(&b.3));
|
||||||
|
|
||||||
|
if perp_base_positions.is_empty() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Liquidate the highest-value perp base position
|
||||||
|
let (perp_market_index, base_lots, price, _) = perp_base_positions.last().unwrap();
|
||||||
|
let perp = self.client.context.perp(*perp_market_index);
|
||||||
|
|
||||||
|
let (side, side_signum) = if *base_lots > 0 {
|
||||||
|
(Side::Bid, 1)
|
||||||
|
} else {
|
||||||
|
(Side::Ask, -1)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Compute the max number of base_lots the liqor is willing to take
|
||||||
|
let max_base_transfer_abs = {
|
||||||
|
let mut liqor = self
|
||||||
|
.account_fetcher
|
||||||
|
.fetch_fresh_mango_account(&self.client.mango_account_address)
|
||||||
|
.context("getting liquidator account")?;
|
||||||
|
liqor.ensure_perp_position(*perp_market_index, QUOTE_TOKEN_INDEX)?;
|
||||||
|
let health_cache =
|
||||||
|
health_cache::new(&self.client.context, self.account_fetcher, &liqor)
|
||||||
|
.expect("always ok");
|
||||||
|
health_cache.max_perp_for_health_ratio(
|
||||||
|
*perp_market_index,
|
||||||
|
*price,
|
||||||
|
perp.market.base_lot_size,
|
||||||
|
side,
|
||||||
|
self.liqor_min_health_ratio,
|
||||||
|
)?
|
||||||
|
};
|
||||||
|
log::info!("computed max_base_transfer to be {max_base_transfer_abs}");
|
||||||
|
|
||||||
|
let sig = self.client.perp_liq_base_position(
|
||||||
|
(self.pubkey, &self.liqee),
|
||||||
|
*perp_market_index,
|
||||||
|
side_signum * max_base_transfer_abs,
|
||||||
|
)?;
|
||||||
|
log::info!(
|
||||||
|
"Liquidated base position for perp market on account {}, market index {}, maint_health was {}, tx sig {:?}",
|
||||||
|
self.pubkey,
|
||||||
|
perp_market_index,
|
||||||
|
self.maint_health,
|
||||||
|
sig
|
||||||
|
);
|
||||||
|
Ok(Some(sig))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn settle_perp_pnl(&self) -> anyhow::Result<Option<Signature>> {
|
||||||
|
let spot_health = self.health_cache.spot_health(HealthType::Maint);
|
||||||
|
let mut perp_settleable_pnl = self
|
||||||
|
.liqee
|
||||||
|
.active_perp_positions()
|
||||||
|
.map(|pp| {
|
||||||
|
if pp.base_position_lots() != 0 {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let pnl = pp.quote_position_native();
|
||||||
|
let settleable_pnl = if pnl > 0 {
|
||||||
|
pnl
|
||||||
|
} else if pnl < 0 && spot_health > 0 {
|
||||||
|
pnl.max(-spot_health)
|
||||||
|
} else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
Ok(Some((pp.market_index, settleable_pnl)))
|
||||||
|
})
|
||||||
|
.filter_map_ok(|v| v)
|
||||||
|
.collect::<anyhow::Result<Vec<(PerpMarketIndex, I80F48)>>>()?;
|
||||||
|
// sort by pnl, descending
|
||||||
|
perp_settleable_pnl.sort_by(|a, b| b.1.cmp(&a.1));
|
||||||
|
|
||||||
|
if perp_settleable_pnl.is_empty() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (perp_index, pnl) in perp_settleable_pnl {
|
||||||
|
let direction = if pnl > 0 {
|
||||||
|
client::perp_pnl::Direction::MaxNegative
|
||||||
|
} else {
|
||||||
|
client::perp_pnl::Direction::MaxPositive
|
||||||
|
};
|
||||||
|
let counters = client::perp_pnl::fetch_top(
|
||||||
|
&self.client.context,
|
||||||
|
self.account_fetcher,
|
||||||
|
perp_index,
|
||||||
|
direction,
|
||||||
|
2,
|
||||||
|
)?;
|
||||||
|
if counters.is_empty() {
|
||||||
|
// If we can't settle some positive PNL because we're lacking a suitable counterparty,
|
||||||
|
// then liquidation should continue, even though this step produced no transaction
|
||||||
|
log::info!("Could not settle perp pnl {pnl} for account {}, perp market {perp_index}: no counterparty",
|
||||||
|
self.pubkey);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let (counter_key, counter_acc, _) = counters.first().unwrap();
|
||||||
|
|
||||||
|
let (account_a, account_b) = if pnl > 0 {
|
||||||
|
(self.pubkey, (counter_key, counter_acc))
|
||||||
|
} else {
|
||||||
|
(counter_key, (self.pubkey, self.liqee))
|
||||||
|
};
|
||||||
|
let sig = self
|
||||||
|
.client
|
||||||
|
.perp_settle_pnl(perp_index, account_a, account_b)?;
|
||||||
|
log::info!(
|
||||||
|
"Settled perp pnl for perp market on account {}, market index {perp_index}, maint_health was {}, tx sig {sig:?}",
|
||||||
|
self.pubkey,
|
||||||
|
self.maint_health,
|
||||||
|
);
|
||||||
|
return Ok(Some(sig));
|
||||||
|
}
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tokens(&self) -> anyhow::Result<Vec<(TokenIndex, I80F48, I80F48)>> {
|
||||||
|
let mut tokens = self
|
||||||
|
.liqee
|
||||||
|
.active_token_positions()
|
||||||
|
.map(|token_position| {
|
||||||
|
let token = self.client.context.token(token_position.token_index);
|
||||||
|
let bank = self
|
||||||
|
.account_fetcher
|
||||||
|
.fetch::<Bank>(&token.mint_info.first_bank())?;
|
||||||
|
let oracle = self
|
||||||
|
.account_fetcher
|
||||||
|
.fetch_raw_account(&token.mint_info.oracle)?;
|
||||||
|
let price = bank.oracle_price(&KeyedAccountSharedData::new(
|
||||||
|
token.mint_info.oracle,
|
||||||
|
oracle.into(),
|
||||||
|
))?;
|
||||||
|
Ok((
|
||||||
|
token_position.token_index,
|
||||||
|
price,
|
||||||
|
token_position.native(&bank) * price,
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.collect::<anyhow::Result<Vec<(TokenIndex, I80F48, I80F48)>>>()?;
|
||||||
|
tokens.sort_by(|a, b| a.2.cmp(&b.2));
|
||||||
|
Ok(tokens)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn max_token_liab_transfer(
|
||||||
|
&self,
|
||||||
|
source: TokenIndex,
|
||||||
|
target: TokenIndex,
|
||||||
|
) -> anyhow::Result<I80F48> {
|
||||||
|
let mut liqor = self
|
||||||
|
.account_fetcher
|
||||||
|
.fetch_fresh_mango_account(&self.client.mango_account_address)
|
||||||
|
.context("getting liquidator account")?;
|
||||||
|
|
||||||
|
// Ensure the tokens are activated, so they appear in the health cache and
|
||||||
|
// max_swap_source() will work.
|
||||||
|
liqor.ensure_token_position(source)?;
|
||||||
|
liqor.ensure_token_position(target)?;
|
||||||
|
|
||||||
|
let health_cache = health_cache::new(&self.client.context, self.account_fetcher, &liqor)
|
||||||
|
.expect("always ok");
|
||||||
|
|
||||||
|
let source_price = health_cache.token_info(source).unwrap().oracle_price;
|
||||||
|
let target_price = health_cache.token_info(target).unwrap().oracle_price;
|
||||||
|
// TODO: This is where we could multiply in the liquidation fee factors
|
||||||
|
let oracle_swap_price = source_price / target_price;
|
||||||
|
|
||||||
|
let amount = health_cache
|
||||||
|
.max_swap_source_for_health_ratio(
|
||||||
|
source,
|
||||||
|
target,
|
||||||
|
oracle_swap_price,
|
||||||
|
self.liqor_min_health_ratio,
|
||||||
|
)
|
||||||
|
.context("getting max_swap_source")?;
|
||||||
|
Ok(amount)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn liq_spot(&self) -> anyhow::Result<Option<Signature>> {
|
||||||
|
if !self.health_cache.has_borrows() || self.health_cache.can_call_spot_bankruptcy() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let tokens = self.tokens()?;
|
||||||
|
|
||||||
|
let asset_token_index = tokens
|
||||||
|
.iter()
|
||||||
|
.rev()
|
||||||
|
.find(|(asset_token_index, _asset_price, asset_usdc_equivalent)| {
|
||||||
|
asset_usdc_equivalent.is_positive()
|
||||||
|
&& jupiter_market_can_sell(self.client, *asset_token_index, QUOTE_TOKEN_INDEX)
|
||||||
|
})
|
||||||
|
.ok_or_else(|| {
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"mango account {}, has no asset tokens that are sellable for USDC: {:?}",
|
||||||
|
self.pubkey,
|
||||||
|
tokens
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
.0;
|
||||||
|
let liab_token_index = tokens
|
||||||
|
.iter()
|
||||||
|
.find(|(liab_token_index, _liab_price, liab_usdc_equivalent)| {
|
||||||
|
liab_usdc_equivalent.is_negative()
|
||||||
|
&& jupiter_market_can_buy(self.client, *liab_token_index, QUOTE_TOKEN_INDEX)
|
||||||
|
})
|
||||||
|
.ok_or_else(|| {
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"mango account {}, has no liab tokens that are purchasable for USDC: {:?}",
|
||||||
|
self.pubkey,
|
||||||
|
tokens
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
.0;
|
||||||
|
|
||||||
|
let max_liab_transfer = self
|
||||||
|
.max_token_liab_transfer(liab_token_index, asset_token_index)
|
||||||
|
.context("getting max_liab_transfer")?;
|
||||||
|
|
||||||
|
//
|
||||||
|
// TODO: log liqor's assets in UI form
|
||||||
|
// TODO: log liquee's liab_needed, need to refactor program code to be able to be accessed from client side
|
||||||
|
//
|
||||||
|
let sig = self
|
||||||
|
.client
|
||||||
|
.token_liq_with_token(
|
||||||
|
(self.pubkey, &self.liqee),
|
||||||
|
asset_token_index,
|
||||||
|
liab_token_index,
|
||||||
|
max_liab_transfer,
|
||||||
|
)
|
||||||
|
.context("sending liq_token_with_token")?;
|
||||||
|
log::info!(
|
||||||
|
"Liquidated token with token for {}, maint_health was {}, tx sig {:?}",
|
||||||
|
self.pubkey,
|
||||||
|
self.maint_health,
|
||||||
|
sig
|
||||||
|
);
|
||||||
|
Ok(Some(sig))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bankrupt_spot(&self) -> anyhow::Result<Option<Signature>> {
|
||||||
|
if !self.health_cache.can_call_spot_bankruptcy() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let tokens = self.tokens()?;
|
||||||
|
|
||||||
|
if tokens.is_empty() {
|
||||||
|
anyhow::bail!(
|
||||||
|
"mango account {}, is bankrupt has no active tokens",
|
||||||
|
self.pubkey
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let liab_token_index = tokens
|
||||||
|
.iter()
|
||||||
|
.find(|(liab_token_index, _liab_price, liab_usdc_equivalent)| {
|
||||||
|
liab_usdc_equivalent.is_negative()
|
||||||
|
&& jupiter_market_can_buy(self.client, *liab_token_index, QUOTE_TOKEN_INDEX)
|
||||||
|
})
|
||||||
|
.ok_or_else(|| {
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"mango account {}, has no liab tokens that are purchasable for USDC: {:?}",
|
||||||
|
self.pubkey,
|
||||||
|
tokens
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
.0;
|
||||||
|
|
||||||
|
let quote_token_index = 0;
|
||||||
|
let max_liab_transfer =
|
||||||
|
self.max_token_liab_transfer(liab_token_index, quote_token_index)?;
|
||||||
|
|
||||||
|
let sig = self
|
||||||
|
.client
|
||||||
|
.token_liq_bankruptcy(
|
||||||
|
(self.pubkey, &self.liqee),
|
||||||
|
liab_token_index,
|
||||||
|
max_liab_transfer,
|
||||||
|
)
|
||||||
|
.context("sending liq_token_bankruptcy")?;
|
||||||
|
log::info!(
|
||||||
|
"Liquidated bankruptcy for {}, maint_health was {}, tx sig {:?}",
|
||||||
|
self.pubkey,
|
||||||
|
self.maint_health,
|
||||||
|
sig
|
||||||
|
);
|
||||||
|
Ok(Some(sig))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_liq_tx(&self) -> anyhow::Result<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.
|
||||||
|
// We shouldn't get stuck on this step, particularly if it's of limited value
|
||||||
|
// to the liquidators.
|
||||||
|
// if let Some(txsig) = self.perp_settle_positive_pnl()? {
|
||||||
|
// return Ok(txsig);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Try to close orders before touching the user's positions
|
||||||
|
if let Some(txsig) = self.perp_close_orders()? {
|
||||||
|
return Ok(txsig);
|
||||||
|
}
|
||||||
|
if let Some(txsig) = self.serum3_close_orders()? {
|
||||||
|
return Ok(txsig);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(txsig) = self.liq_perp_base_position()? {
|
||||||
|
return Ok(txsig);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now that the perp base positions are zeroed the perp pnl won't
|
||||||
|
// fluctuate with the oracle price anymore.
|
||||||
|
// It's possible that some positive pnl can't be settled (if there's
|
||||||
|
// no liquid counterparty) and that some negative pnl can't be settled
|
||||||
|
// (if the liqee isn't liquid enough).
|
||||||
|
if let Some(txsig) = self.settle_perp_pnl()? {
|
||||||
|
return Ok(txsig);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(txsig) = self.liq_spot()? {
|
||||||
|
return Ok(txsig);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: socialize unsettleable negative pnl
|
||||||
|
// if let Some(txsig) = self.bankrupt_perp()? {
|
||||||
|
// return Ok(txsig);
|
||||||
|
// }
|
||||||
|
if let Some(txsig) = self.bankrupt_spot()? {
|
||||||
|
return Ok(txsig);
|
||||||
|
}
|
||||||
|
anyhow::bail!(
|
||||||
|
"Don't know what to do with liquidatable account {}, maint_health was {}",
|
||||||
|
self.pubkey,
|
||||||
|
self.maint_health
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn maybe_liquidate_account(
|
pub fn maybe_liquidate_account(
|
||||||
mango_client: &MangoClient,
|
mango_client: &MangoClient,
|
||||||
|
@ -76,8 +523,7 @@ pub fn maybe_liquidate_account(
|
||||||
pubkey: &Pubkey,
|
pubkey: &Pubkey,
|
||||||
config: &Config,
|
config: &Config,
|
||||||
) -> anyhow::Result<bool> {
|
) -> anyhow::Result<bool> {
|
||||||
let min_health_ratio = I80F48::from_num(config.min_health_ratio);
|
let liqor_min_health_ratio = I80F48::from_num(config.min_health_ratio);
|
||||||
let quote_token_index = 0;
|
|
||||||
|
|
||||||
let account = account_fetcher.fetch_mango_account(pubkey)?;
|
let account = account_fetcher.fetch_mango_account(pubkey)?;
|
||||||
let health_cache =
|
let health_cache =
|
||||||
|
@ -105,264 +551,18 @@ pub fn maybe_liquidate_account(
|
||||||
}
|
}
|
||||||
|
|
||||||
let maint_health = health_cache.health(HealthType::Maint);
|
let maint_health = health_cache.health(HealthType::Maint);
|
||||||
let is_spot_bankrupt = health_cache.can_call_spot_bankruptcy();
|
|
||||||
let is_spot_liquidatable = health_cache.has_borrows() && !is_spot_bankrupt;
|
|
||||||
|
|
||||||
// find asset and liab tokens
|
|
||||||
let mut tokens = account
|
|
||||||
.active_token_positions()
|
|
||||||
.map(|token_position| {
|
|
||||||
let token = mango_client.context.token(token_position.token_index);
|
|
||||||
let bank = account_fetcher.fetch::<Bank>(&token.mint_info.first_bank())?;
|
|
||||||
let oracle = account_fetcher.fetch_raw_account(&token.mint_info.oracle)?;
|
|
||||||
let price = bank.oracle_price(&KeyedAccountSharedData::new(
|
|
||||||
token.mint_info.oracle,
|
|
||||||
oracle.into(),
|
|
||||||
))?;
|
|
||||||
Ok((
|
|
||||||
token_position.token_index,
|
|
||||||
price,
|
|
||||||
token_position.native(&bank) * price,
|
|
||||||
))
|
|
||||||
})
|
|
||||||
.collect::<anyhow::Result<Vec<(TokenIndex, I80F48, I80F48)>>>()?;
|
|
||||||
tokens.sort_by(|a, b| a.2.cmp(&b.2));
|
|
||||||
|
|
||||||
// look for any open serum orders or settleable balances
|
|
||||||
let serum_force_cancels = account
|
|
||||||
.active_serum3_orders()
|
|
||||||
.map(|orders| {
|
|
||||||
let open_orders_account = account_fetcher.fetch_raw_account(&orders.open_orders)?;
|
|
||||||
let open_orders = mango_v4::serum3_cpi::load_open_orders(&open_orders_account)?;
|
|
||||||
let can_force_cancel = open_orders.native_coin_total > 0
|
|
||||||
|| open_orders.native_pc_total > 0
|
|
||||||
|| open_orders.referrer_rebates_accrued > 0;
|
|
||||||
if can_force_cancel {
|
|
||||||
Ok(Some(*orders))
|
|
||||||
} else {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter_map_ok(|v| v)
|
|
||||||
.collect::<anyhow::Result<Vec<Serum3Orders>>>()?;
|
|
||||||
|
|
||||||
// look for any perp open orders and base positions
|
|
||||||
let perp_force_cancels = account
|
|
||||||
.active_perp_positions()
|
|
||||||
.filter_map(|pp| pp.has_open_orders().then(|| pp.market_index))
|
|
||||||
.collect::<Vec<PerpMarketIndex>>();
|
|
||||||
let mut perp_base_positions = account
|
|
||||||
.active_perp_positions()
|
|
||||||
.map(|pp| {
|
|
||||||
let base_lots = pp.base_position_lots();
|
|
||||||
if base_lots == 0 {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
let perp = mango_client.context.perp(pp.market_index);
|
|
||||||
let oracle = account_fetcher.fetch_raw_account(&perp.market.oracle)?;
|
|
||||||
let price = perp.market.oracle_price(&KeyedAccountSharedData::new(
|
|
||||||
perp.market.oracle,
|
|
||||||
oracle.into(),
|
|
||||||
))?;
|
|
||||||
Ok(Some((
|
|
||||||
pp.market_index,
|
|
||||||
base_lots,
|
|
||||||
price,
|
|
||||||
I80F48::from(base_lots.abs()) * price,
|
|
||||||
)))
|
|
||||||
})
|
|
||||||
.filter_map_ok(|v| v)
|
|
||||||
.collect::<anyhow::Result<Vec<(PerpMarketIndex, i64, I80F48, I80F48)>>>()?;
|
|
||||||
// sort by base_position_value, ascending
|
|
||||||
perp_base_positions.sort_by(|a, b| a.3.cmp(&b.3));
|
|
||||||
|
|
||||||
let get_max_liab_transfer = |source, target| -> anyhow::Result<I80F48> {
|
|
||||||
let mut liqor = account_fetcher
|
|
||||||
.fetch_fresh_mango_account(&mango_client.mango_account_address)
|
|
||||||
.context("getting liquidator account")?;
|
|
||||||
|
|
||||||
// Ensure the tokens are activated, so they appear in the health cache and
|
|
||||||
// max_swap_source() will work.
|
|
||||||
liqor.ensure_token_position(source)?;
|
|
||||||
liqor.ensure_token_position(target)?;
|
|
||||||
|
|
||||||
let health_cache =
|
|
||||||
health_cache::new(&mango_client.context, account_fetcher, &liqor).expect("always ok");
|
|
||||||
|
|
||||||
let source_price = health_cache.token_info(source).unwrap().oracle_price;
|
|
||||||
let target_price = health_cache.token_info(target).unwrap().oracle_price;
|
|
||||||
// TODO: This is where we could multiply in the liquidation fee factors
|
|
||||||
let oracle_swap_price = source_price / target_price;
|
|
||||||
|
|
||||||
let amount = health_cache
|
|
||||||
.max_swap_source_for_health_ratio(source, target, oracle_swap_price, min_health_ratio)
|
|
||||||
.context("getting max_swap_source")?;
|
|
||||||
Ok(amount)
|
|
||||||
};
|
|
||||||
|
|
||||||
// try liquidating
|
// try liquidating
|
||||||
let txsig = if !serum_force_cancels.is_empty() {
|
let txsig = LiquidateHelper {
|
||||||
// Cancel all orders on a random serum market
|
client: mango_client,
|
||||||
let serum_orders = serum_force_cancels.choose(&mut rand::thread_rng()).unwrap();
|
account_fetcher,
|
||||||
let sig = mango_client.serum3_liq_force_cancel_orders(
|
pubkey,
|
||||||
(pubkey, &account),
|
liqee: &account,
|
||||||
serum_orders.market_index,
|
health_cache: &health_cache,
|
||||||
&serum_orders.open_orders,
|
maint_health,
|
||||||
)?;
|
liqor_min_health_ratio,
|
||||||
log::info!(
|
}
|
||||||
"Force cancelled serum orders on account {}, market index {}, maint_health was {}, tx sig {:?}",
|
.send_liq_tx()?;
|
||||||
pubkey,
|
|
||||||
serum_orders.market_index,
|
|
||||||
maint_health,
|
|
||||||
sig
|
|
||||||
);
|
|
||||||
sig
|
|
||||||
} else if !perp_force_cancels.is_empty() {
|
|
||||||
// Cancel all orders on a random perp market
|
|
||||||
let perp_market_index = *perp_force_cancels.choose(&mut rand::thread_rng()).unwrap();
|
|
||||||
let sig =
|
|
||||||
mango_client.perp_liq_force_cancel_orders((pubkey, &account), perp_market_index)?;
|
|
||||||
log::info!(
|
|
||||||
"Force cancelled perp orders on account {}, market index {}, maint_health was {}, tx sig {:?}",
|
|
||||||
pubkey,
|
|
||||||
perp_market_index,
|
|
||||||
maint_health,
|
|
||||||
sig
|
|
||||||
);
|
|
||||||
sig
|
|
||||||
} else if !perp_base_positions.is_empty() {
|
|
||||||
// Liquidate the highest-value perp base position
|
|
||||||
let (perp_market_index, base_lots, price, _) = perp_base_positions.last().unwrap();
|
|
||||||
let perp = mango_client.context.perp(*perp_market_index);
|
|
||||||
|
|
||||||
let (side, side_signum) = if *base_lots > 0 {
|
|
||||||
(Side::Bid, 1)
|
|
||||||
} else {
|
|
||||||
(Side::Ask, -1)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Compute the max number of base_lots the liqor is willing to take
|
|
||||||
let max_base_transfer_abs = {
|
|
||||||
let mut liqor = account_fetcher
|
|
||||||
.fetch_fresh_mango_account(&mango_client.mango_account_address)
|
|
||||||
.context("getting liquidator account")?;
|
|
||||||
liqor.ensure_perp_position(*perp_market_index, QUOTE_TOKEN_INDEX)?;
|
|
||||||
let health_cache = health_cache::new(&mango_client.context, account_fetcher, &liqor)
|
|
||||||
.expect("always ok");
|
|
||||||
health_cache.max_perp_for_health_ratio(
|
|
||||||
*perp_market_index,
|
|
||||||
*price,
|
|
||||||
perp.market.base_lot_size,
|
|
||||||
side,
|
|
||||||
min_health_ratio,
|
|
||||||
)?
|
|
||||||
};
|
|
||||||
log::info!("computed max_base_transfer to be {max_base_transfer_abs}");
|
|
||||||
|
|
||||||
let sig = mango_client.perp_liq_base_position(
|
|
||||||
(pubkey, &account),
|
|
||||||
*perp_market_index,
|
|
||||||
side_signum * max_base_transfer_abs,
|
|
||||||
)?;
|
|
||||||
log::info!(
|
|
||||||
"Liquidated base position for perp market on account {}, market index {}, maint_health was {}, tx sig {:?}",
|
|
||||||
pubkey,
|
|
||||||
perp_market_index,
|
|
||||||
maint_health,
|
|
||||||
sig
|
|
||||||
);
|
|
||||||
sig
|
|
||||||
} else if is_spot_bankrupt {
|
|
||||||
if tokens.is_empty() {
|
|
||||||
anyhow::bail!("mango account {}, is bankrupt has no active tokens", pubkey);
|
|
||||||
}
|
|
||||||
let liab_token_index = tokens
|
|
||||||
.iter()
|
|
||||||
.find(|(liab_token_index, _liab_price, liab_usdc_equivalent)| {
|
|
||||||
liab_usdc_equivalent.is_negative()
|
|
||||||
&& jupiter_market_can_buy(mango_client, *liab_token_index, QUOTE_TOKEN_INDEX)
|
|
||||||
})
|
|
||||||
.ok_or_else(|| {
|
|
||||||
anyhow::anyhow!(
|
|
||||||
"mango account {}, has no liab tokens that are purchasable for USDC: {:?}",
|
|
||||||
pubkey,
|
|
||||||
tokens
|
|
||||||
)
|
|
||||||
})?
|
|
||||||
.0;
|
|
||||||
|
|
||||||
let max_liab_transfer = get_max_liab_transfer(liab_token_index, quote_token_index)?;
|
|
||||||
|
|
||||||
let sig = mango_client
|
|
||||||
.token_liq_bankruptcy((pubkey, &account), liab_token_index, max_liab_transfer)
|
|
||||||
.context("sending liq_token_bankruptcy")?;
|
|
||||||
log::info!(
|
|
||||||
"Liquidated bankruptcy for {}, maint_health was {}, tx sig {:?}",
|
|
||||||
pubkey,
|
|
||||||
maint_health,
|
|
||||||
sig
|
|
||||||
);
|
|
||||||
sig
|
|
||||||
} else if is_spot_liquidatable {
|
|
||||||
let asset_token_index = tokens
|
|
||||||
.iter()
|
|
||||||
.rev()
|
|
||||||
.find(|(asset_token_index, _asset_price, asset_usdc_equivalent)| {
|
|
||||||
asset_usdc_equivalent.is_positive()
|
|
||||||
&& jupiter_market_can_sell(mango_client, *asset_token_index, QUOTE_TOKEN_INDEX)
|
|
||||||
})
|
|
||||||
.ok_or_else(|| {
|
|
||||||
anyhow::anyhow!(
|
|
||||||
"mango account {}, has no asset tokens that are sellable for USDC: {:?}",
|
|
||||||
pubkey,
|
|
||||||
tokens
|
|
||||||
)
|
|
||||||
})?
|
|
||||||
.0;
|
|
||||||
let liab_token_index = tokens
|
|
||||||
.iter()
|
|
||||||
.find(|(liab_token_index, _liab_price, liab_usdc_equivalent)| {
|
|
||||||
liab_usdc_equivalent.is_negative()
|
|
||||||
&& jupiter_market_can_buy(mango_client, *liab_token_index, QUOTE_TOKEN_INDEX)
|
|
||||||
})
|
|
||||||
.ok_or_else(|| {
|
|
||||||
anyhow::anyhow!(
|
|
||||||
"mango account {}, has no liab tokens that are purchasable for USDC: {:?}",
|
|
||||||
pubkey,
|
|
||||||
tokens
|
|
||||||
)
|
|
||||||
})?
|
|
||||||
.0;
|
|
||||||
|
|
||||||
let max_liab_transfer = get_max_liab_transfer(liab_token_index, asset_token_index)
|
|
||||||
.context("getting max_liab_transfer")?;
|
|
||||||
|
|
||||||
//
|
|
||||||
// TODO: log liqor's assets in UI form
|
|
||||||
// TODO: log liquee's liab_needed, need to refactor program code to be able to be accessed from client side
|
|
||||||
//
|
|
||||||
let sig = mango_client
|
|
||||||
.token_liq_with_token(
|
|
||||||
(pubkey, &account),
|
|
||||||
asset_token_index,
|
|
||||||
liab_token_index,
|
|
||||||
max_liab_transfer,
|
|
||||||
)
|
|
||||||
.context("sending liq_token_with_token")?;
|
|
||||||
log::info!(
|
|
||||||
"Liquidated token with token for {}, maint_health was {}, tx sig {:?}",
|
|
||||||
pubkey,
|
|
||||||
maint_health,
|
|
||||||
sig
|
|
||||||
);
|
|
||||||
sig
|
|
||||||
} else {
|
|
||||||
anyhow::bail!(
|
|
||||||
"Don't know what to do with liquidatable account {}, maint_health was {}",
|
|
||||||
pubkey,
|
|
||||||
maint_health
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
let slot = account_fetcher.transaction_max_slot(&[txsig])?;
|
let slot = account_fetcher.transaction_max_slot(&[txsig])?;
|
||||||
if let Err(e) = account_fetcher.refresh_accounts_via_rpc_until_slot(
|
if let Err(e) = account_fetcher.refresh_accounts_via_rpc_until_slot(
|
||||||
|
|
|
@ -22,6 +22,7 @@ const PRICES = {
|
||||||
BTC: 20000.0,
|
BTC: 20000.0,
|
||||||
SOL: 0.04,
|
SOL: 0.04,
|
||||||
USDC: 1,
|
USDC: 1,
|
||||||
|
MNGO: 0.04,
|
||||||
};
|
};
|
||||||
|
|
||||||
const MAINNET_MINTS = new Map([
|
const MAINNET_MINTS = new Map([
|
||||||
|
@ -298,6 +299,109 @@ async function main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// borrows and positive perp pnl (but no position)
|
||||||
|
{
|
||||||
|
const name = 'LIQTEST, perp positive pnl';
|
||||||
|
|
||||||
|
console.log(`Creating mangoaccount...`);
|
||||||
|
let mangoAccount = await createMangoAccount(name);
|
||||||
|
console.log(
|
||||||
|
`...created mangoAccount ${mangoAccount.publicKey} for ${name}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const baseMint = new PublicKey(MAINNET_MINTS.get('MNGO')!);
|
||||||
|
const baseOracle = (await client.getStubOracle(group, baseMint))[0]
|
||||||
|
.publicKey;
|
||||||
|
const liabMint = new PublicKey(MAINNET_MINTS.get('USDC')!);
|
||||||
|
const collateralMint = new PublicKey(MAINNET_MINTS.get('SOL')!);
|
||||||
|
const collateralOracle = group.banksMapByName.get('SOL')![0].oracle;
|
||||||
|
|
||||||
|
await client.tokenDepositNative(
|
||||||
|
group,
|
||||||
|
mangoAccount,
|
||||||
|
collateralMint,
|
||||||
|
100000,
|
||||||
|
); // valued as $0.004 maint collateral
|
||||||
|
await mangoAccount.reload(client, group);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.stubOracleSet(group, collateralOracle, PRICES['SOL'] * 10);
|
||||||
|
|
||||||
|
// Spot-borrow more than the collateral is worth
|
||||||
|
await client.tokenWithdrawNative(
|
||||||
|
group,
|
||||||
|
mangoAccount,
|
||||||
|
liabMint,
|
||||||
|
-5000,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
await mangoAccount.reload(client, group);
|
||||||
|
|
||||||
|
// Execute two trades that leave the account with +$0.022 positive pnl
|
||||||
|
await client.stubOracleSet(group, baseOracle, PRICES['MNGO'] / 2);
|
||||||
|
await client.perpPlaceOrder(
|
||||||
|
group,
|
||||||
|
fundingAccount,
|
||||||
|
'MNGO-PERP',
|
||||||
|
Side.ask,
|
||||||
|
20,
|
||||||
|
0.0011, // ui base quantity, 11 base lots, $0.022
|
||||||
|
0.022, // ui quote quantity
|
||||||
|
4200,
|
||||||
|
PerpOrderType.limit,
|
||||||
|
0,
|
||||||
|
5,
|
||||||
|
);
|
||||||
|
await client.perpPlaceOrder(
|
||||||
|
group,
|
||||||
|
mangoAccount,
|
||||||
|
'MNGO-PERP',
|
||||||
|
Side.bid,
|
||||||
|
20,
|
||||||
|
0.0011, // ui base quantity, 11 base lots, $0.022
|
||||||
|
0.022, // ui quote quantity
|
||||||
|
4200,
|
||||||
|
PerpOrderType.market,
|
||||||
|
0,
|
||||||
|
5,
|
||||||
|
);
|
||||||
|
await client.perpConsumeAllEvents(group, 'MNGO-PERP');
|
||||||
|
|
||||||
|
await client.stubOracleSet(group, baseOracle, PRICES['MNGO']);
|
||||||
|
|
||||||
|
await client.perpPlaceOrder(
|
||||||
|
group,
|
||||||
|
fundingAccount,
|
||||||
|
'MNGO-PERP',
|
||||||
|
Side.bid,
|
||||||
|
40,
|
||||||
|
0.0011, // ui base quantity, 11 base lots, $0.044
|
||||||
|
0.044, // ui quote quantity
|
||||||
|
4201,
|
||||||
|
PerpOrderType.limit,
|
||||||
|
0,
|
||||||
|
5,
|
||||||
|
);
|
||||||
|
await client.perpPlaceOrder(
|
||||||
|
group,
|
||||||
|
mangoAccount,
|
||||||
|
'MNGO-PERP',
|
||||||
|
Side.ask,
|
||||||
|
40,
|
||||||
|
0.0011, // ui base quantity, 11 base lots, $0.044
|
||||||
|
0.044, // ui quote quantity
|
||||||
|
4201,
|
||||||
|
PerpOrderType.market,
|
||||||
|
0,
|
||||||
|
5,
|
||||||
|
);
|
||||||
|
await client.perpConsumeAllEvents(group, 'MNGO-PERP');
|
||||||
|
} finally {
|
||||||
|
await client.stubOracleSet(group, collateralOracle, PRICES['SOL']);
|
||||||
|
await client.stubOracleSet(group, baseOracle, PRICES['MNGO']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
process.exit();
|
process.exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue