2022-12-16 04:10:46 -08:00
|
|
|
use std::collections::HashSet;
|
2022-08-07 11:04:19 -07:00
|
|
|
use std::time::Duration;
|
|
|
|
|
2022-09-23 02:59:18 -07:00
|
|
|
use client::{chain_data, health_cache, AccountFetcher, MangoClient, MangoClientError};
|
|
|
|
use mango_v4::accounts_zerocopy::KeyedAccountSharedData;
|
2022-12-08 04:12:43 -08:00
|
|
|
use mango_v4::health::{HealthCache, HealthType};
|
2022-06-18 07:31:28 -07:00
|
|
|
use mango_v4::state::{
|
2022-12-16 04:10:46 -08:00
|
|
|
Bank, MangoAccountValue, PerpMarketIndex, Side, TokenIndex, QUOTE_TOKEN_INDEX,
|
2022-06-18 07:31:28 -07:00
|
|
|
};
|
2022-09-26 04:27:31 -07:00
|
|
|
use solana_sdk::signature::Signature;
|
2022-06-18 07:31:28 -07:00
|
|
|
|
2022-12-16 04:10:46 -08:00
|
|
|
use futures::{stream, StreamExt, TryStreamExt};
|
2022-08-31 05:41:04 -07:00
|
|
|
use rand::seq::SliceRandom;
|
2022-07-16 05:37:15 -07:00
|
|
|
use {anyhow::Context, fixed::types::I80F48, solana_sdk::pubkey::Pubkey};
|
2022-06-18 07:31:28 -07:00
|
|
|
|
2022-08-05 04:50:44 -07:00
|
|
|
pub struct Config {
|
|
|
|
pub min_health_ratio: f64,
|
2022-08-07 11:04:19 -07:00
|
|
|
pub refresh_timeout: Duration,
|
2022-08-05 04:50:44 -07:00
|
|
|
}
|
|
|
|
|
2022-12-16 04:10:46 -08:00
|
|
|
pub async fn jupiter_market_can_buy(
|
2022-08-05 11:28:14 -07:00
|
|
|
mango_client: &MangoClient,
|
|
|
|
token: TokenIndex,
|
|
|
|
quote_token: TokenIndex,
|
|
|
|
) -> bool {
|
|
|
|
if token == quote_token {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
let token_mint = mango_client.context.token(token).mint_info.mint;
|
|
|
|
let quote_token_mint = mango_client.context.token(quote_token).mint_info.mint;
|
|
|
|
|
|
|
|
// Consider a market alive if we can swap $10 worth at 1% slippage
|
|
|
|
// TODO: configurable
|
|
|
|
// TODO: cache this, no need to recheck often
|
|
|
|
let quote_amount = 10_000_000u64;
|
|
|
|
let slippage = 1.0;
|
|
|
|
mango_client
|
|
|
|
.jupiter_route(
|
|
|
|
quote_token_mint,
|
|
|
|
token_mint,
|
|
|
|
quote_amount,
|
|
|
|
slippage,
|
|
|
|
client::JupiterSwapMode::ExactIn,
|
|
|
|
)
|
2022-12-16 04:10:46 -08:00
|
|
|
.await
|
2022-08-05 11:28:14 -07:00
|
|
|
.is_ok()
|
|
|
|
}
|
|
|
|
|
2022-12-16 04:10:46 -08:00
|
|
|
pub async fn jupiter_market_can_sell(
|
2022-08-05 11:28:14 -07:00
|
|
|
mango_client: &MangoClient,
|
|
|
|
token: TokenIndex,
|
|
|
|
quote_token: TokenIndex,
|
|
|
|
) -> bool {
|
|
|
|
if token == quote_token {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
let token_mint = mango_client.context.token(token).mint_info.mint;
|
|
|
|
let quote_token_mint = mango_client.context.token(quote_token).mint_info.mint;
|
|
|
|
|
|
|
|
// Consider a market alive if we can swap $10 worth at 1% slippage
|
|
|
|
// TODO: configurable
|
|
|
|
// TODO: cache this, no need to recheck often
|
|
|
|
let quote_amount = 10_000_000u64;
|
|
|
|
let slippage = 1.0;
|
|
|
|
mango_client
|
|
|
|
.jupiter_route(
|
|
|
|
token_mint,
|
|
|
|
quote_token_mint,
|
|
|
|
quote_amount,
|
|
|
|
slippage,
|
|
|
|
client::JupiterSwapMode::ExactOut,
|
|
|
|
)
|
2022-12-16 04:10:46 -08:00
|
|
|
.await
|
2022-08-05 11:28:14 -07:00
|
|
|
.is_ok()
|
|
|
|
}
|
|
|
|
|
2022-09-26 04:27:31 -07:00
|
|
|
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,
|
2022-12-16 04:10:46 -08:00
|
|
|
allowed_asset_tokens: HashSet<Pubkey>,
|
|
|
|
allowed_liab_tokens: HashSet<Pubkey>,
|
2022-09-26 04:27:31 -07:00
|
|
|
}
|
2022-07-13 05:04:20 -07:00
|
|
|
|
2022-09-26 04:27:31 -07:00
|
|
|
impl<'a> LiquidateHelper<'a> {
|
2022-12-16 04:10:46 -08:00
|
|
|
async fn serum3_close_orders(&self) -> anyhow::Result<Option<Signature>> {
|
2022-09-26 04:27:31 -07:00
|
|
|
// look for any open serum orders or settleable balances
|
2022-12-16 04:10:46 -08:00
|
|
|
let serum_oos: anyhow::Result<Vec<_>> = stream::iter(self.liqee.active_serum3_orders())
|
|
|
|
.then(|orders| async {
|
2022-09-26 04:27:31 -07:00
|
|
|
let open_orders_account = self
|
|
|
|
.account_fetcher
|
2022-12-16 04:10:46 -08:00
|
|
|
.fetch_raw_account(&orders.open_orders)
|
|
|
|
.await?;
|
2022-09-26 04:27:31 -07:00
|
|
|
let open_orders = mango_v4::serum3_cpi::load_open_orders(&open_orders_account)?;
|
2022-12-16 04:10:46 -08:00
|
|
|
Ok((*orders, *open_orders))
|
|
|
|
})
|
|
|
|
.try_collect()
|
|
|
|
.await;
|
|
|
|
let serum_force_cancels = serum_oos?
|
|
|
|
.into_iter()
|
|
|
|
.filter_map(|(orders, open_orders)| {
|
2022-09-26 04:27:31 -07:00
|
|
|
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 {
|
2022-12-16 04:10:46 -08:00
|
|
|
Some(orders)
|
2022-09-26 04:27:31 -07:00
|
|
|
} else {
|
2022-12-16 04:10:46 -08:00
|
|
|
None
|
2022-09-26 04:27:31 -07:00
|
|
|
}
|
|
|
|
})
|
2022-12-16 04:10:46 -08:00
|
|
|
.collect::<Vec<_>>();
|
2022-09-26 04:27:31 -07:00
|
|
|
if serum_force_cancels.is_empty() {
|
|
|
|
return Ok(None);
|
|
|
|
}
|
2022-09-15 00:57:48 -07:00
|
|
|
// Cancel all orders on a random serum market
|
2022-08-31 05:41:04 -07:00
|
|
|
let serum_orders = serum_force_cancels.choose(&mut rand::thread_rng()).unwrap();
|
2022-12-16 04:10:46 -08:00
|
|
|
let sig = self
|
|
|
|
.client
|
|
|
|
.serum3_liq_force_cancel_orders(
|
|
|
|
(self.pubkey, &self.liqee),
|
|
|
|
serum_orders.market_index,
|
|
|
|
&serum_orders.open_orders,
|
|
|
|
)
|
|
|
|
.await?;
|
2022-08-31 05:41:04 -07:00
|
|
|
log::info!(
|
2022-09-15 00:57:48 -07:00
|
|
|
"Force cancelled serum orders on account {}, market index {}, maint_health was {}, tx sig {:?}",
|
2022-09-26 04:27:31 -07:00
|
|
|
self.pubkey,
|
2022-08-31 05:41:04 -07:00
|
|
|
serum_orders.market_index,
|
2022-09-26 04:27:31 -07:00
|
|
|
self.maint_health,
|
2022-08-31 05:41:04 -07:00
|
|
|
sig
|
|
|
|
);
|
2022-09-26 04:27:31 -07:00
|
|
|
Ok(Some(sig))
|
|
|
|
}
|
|
|
|
|
2022-12-16 04:10:46 -08:00
|
|
|
async fn perp_close_orders(&self) -> anyhow::Result<Option<Signature>> {
|
2022-09-26 04:27:31 -07:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2022-09-15 00:57:48 -07:00
|
|
|
// Cancel all orders on a random perp market
|
|
|
|
let perp_market_index = *perp_force_cancels.choose(&mut rand::thread_rng()).unwrap();
|
2022-09-26 04:27:31 -07:00
|
|
|
let sig = self
|
|
|
|
.client
|
2022-12-16 04:10:46 -08:00
|
|
|
.perp_liq_force_cancel_orders((self.pubkey, &self.liqee), perp_market_index)
|
|
|
|
.await?;
|
2022-09-15 00:57:48 -07:00
|
|
|
log::info!(
|
|
|
|
"Force cancelled perp orders on account {}, market index {}, maint_health was {}, tx sig {:?}",
|
2022-09-26 04:27:31 -07:00
|
|
|
self.pubkey,
|
2022-09-15 00:57:48 -07:00
|
|
|
perp_market_index,
|
2022-09-26 04:27:31 -07:00
|
|
|
self.maint_health,
|
2022-09-15 00:57:48 -07:00
|
|
|
sig
|
|
|
|
);
|
2022-09-26 04:27:31 -07:00
|
|
|
Ok(Some(sig))
|
|
|
|
}
|
|
|
|
|
2022-12-16 04:10:46 -08:00
|
|
|
async fn perp_liq_base_position(&self) -> anyhow::Result<Option<Signature>> {
|
|
|
|
let all_perp_base_positions: anyhow::Result<
|
|
|
|
Vec<Option<(PerpMarketIndex, i64, I80F48, I80F48)>>,
|
|
|
|
> = stream::iter(self.liqee.active_perp_positions())
|
|
|
|
.then(|pp| async {
|
2022-09-26 04:27:31 -07:00
|
|
|
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
|
2022-12-16 04:10:46 -08:00
|
|
|
.fetch_raw_account(&perp.market.oracle)
|
|
|
|
.await?;
|
2022-11-10 06:47:11 -08:00
|
|
|
let price = perp.market.oracle_price(
|
|
|
|
&KeyedAccountSharedData::new(perp.market.oracle, oracle.into()),
|
|
|
|
None,
|
|
|
|
)?;
|
2022-09-26 04:27:31 -07:00
|
|
|
Ok(Some((
|
|
|
|
pp.market_index,
|
|
|
|
base_lots,
|
|
|
|
price,
|
|
|
|
I80F48::from(base_lots.abs()) * price,
|
|
|
|
)))
|
|
|
|
})
|
2022-12-16 04:10:46 -08:00
|
|
|
.try_collect()
|
|
|
|
.await;
|
|
|
|
let mut perp_base_positions = all_perp_base_positions?
|
|
|
|
.into_iter()
|
|
|
|
.filter_map(|x| x)
|
|
|
|
.collect::<Vec<_>>();
|
2022-09-26 04:27:31 -07:00
|
|
|
perp_base_positions.sort_by(|a, b| a.3.cmp(&b.3));
|
|
|
|
|
|
|
|
if perp_base_positions.is_empty() {
|
|
|
|
return Ok(None);
|
|
|
|
}
|
|
|
|
|
2022-09-15 00:57:48 -07:00
|
|
|
// Liquidate the highest-value perp base position
|
|
|
|
let (perp_market_index, base_lots, price, _) = perp_base_positions.last().unwrap();
|
|
|
|
|
|
|
|
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 = {
|
2022-09-26 04:27:31 -07:00
|
|
|
let mut liqor = self
|
|
|
|
.account_fetcher
|
|
|
|
.fetch_fresh_mango_account(&self.client.mango_account_address)
|
2022-12-16 04:10:46 -08:00
|
|
|
.await
|
2022-09-15 00:57:48 -07:00
|
|
|
.context("getting liquidator account")?;
|
|
|
|
liqor.ensure_perp_position(*perp_market_index, QUOTE_TOKEN_INDEX)?;
|
2022-09-26 04:27:31 -07:00
|
|
|
let health_cache =
|
|
|
|
health_cache::new(&self.client.context, self.account_fetcher, &liqor)
|
2022-12-16 04:10:46 -08:00
|
|
|
.await
|
2022-09-26 04:27:31 -07:00
|
|
|
.expect("always ok");
|
2022-09-15 00:57:48 -07:00
|
|
|
health_cache.max_perp_for_health_ratio(
|
|
|
|
*perp_market_index,
|
|
|
|
*price,
|
|
|
|
side,
|
2022-09-26 04:27:31 -07:00
|
|
|
self.liqor_min_health_ratio,
|
2022-09-15 00:57:48 -07:00
|
|
|
)?
|
|
|
|
};
|
|
|
|
log::info!("computed max_base_transfer to be {max_base_transfer_abs}");
|
|
|
|
|
2022-12-16 04:10:46 -08:00
|
|
|
let sig = self
|
|
|
|
.client
|
|
|
|
.perp_liq_base_position(
|
|
|
|
(self.pubkey, &self.liqee),
|
|
|
|
*perp_market_index,
|
|
|
|
side_signum * max_base_transfer_abs,
|
|
|
|
)
|
|
|
|
.await?;
|
2022-09-15 00:57:48 -07:00
|
|
|
log::info!(
|
|
|
|
"Liquidated base position for perp market on account {}, market index {}, maint_health was {}, tx sig {:?}",
|
2022-09-26 04:27:31 -07:00
|
|
|
self.pubkey,
|
2022-09-15 00:57:48 -07:00
|
|
|
perp_market_index,
|
2022-09-26 04:27:31 -07:00
|
|
|
self.maint_health,
|
2022-09-15 00:57:48 -07:00
|
|
|
sig
|
|
|
|
);
|
2022-09-26 04:27:31 -07:00
|
|
|
Ok(Some(sig))
|
|
|
|
}
|
|
|
|
|
2022-12-16 04:10:46 -08:00
|
|
|
async fn perp_settle_pnl(&self) -> anyhow::Result<Option<Signature>> {
|
2022-09-29 05:35:01 -07:00
|
|
|
let perp_settle_health = self.health_cache.perp_settle_health();
|
2022-09-26 04:27:31 -07:00
|
|
|
let mut perp_settleable_pnl = self
|
|
|
|
.liqee
|
|
|
|
.active_perp_positions()
|
2022-09-27 02:36:58 -07:00
|
|
|
.filter_map(|pp| {
|
2022-09-26 04:27:31 -07:00
|
|
|
if pp.base_position_lots() != 0 {
|
2022-09-27 02:36:58 -07:00
|
|
|
return None;
|
2022-09-26 04:27:31 -07:00
|
|
|
}
|
|
|
|
let pnl = pp.quote_position_native();
|
|
|
|
let settleable_pnl = if pnl > 0 {
|
|
|
|
pnl
|
2022-09-29 05:35:01 -07:00
|
|
|
} else if pnl < 0 && perp_settle_health > 0 {
|
|
|
|
pnl.max(-perp_settle_health)
|
2022-09-26 04:27:31 -07:00
|
|
|
} else {
|
2022-09-27 02:36:58 -07:00
|
|
|
return None;
|
2022-09-26 04:27:31 -07:00
|
|
|
};
|
2022-12-19 02:47:28 -08:00
|
|
|
if settleable_pnl.abs() < 1 {
|
|
|
|
return None;
|
|
|
|
}
|
2022-09-27 02:36:58 -07:00
|
|
|
Some((pp.market_index, settleable_pnl))
|
2022-09-26 04:27:31 -07:00
|
|
|
})
|
2022-09-27 02:36:58 -07:00
|
|
|
.collect::<Vec<(PerpMarketIndex, I80F48)>>();
|
2022-09-26 04:27:31 -07:00
|
|
|
// 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);
|
2022-07-13 05:04:20 -07:00
|
|
|
}
|
2022-09-26 04:27:31 -07:00
|
|
|
|
|
|
|
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,
|
2022-12-16 04:10:46 -08:00
|
|
|
)
|
|
|
|
.await?;
|
2022-09-26 04:27:31 -07:00
|
|
|
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;
|
|
|
|
}
|
2022-12-16 04:10:46 -08:00
|
|
|
let (counter_key, counter_acc, counter_pnl) = counters.first().unwrap();
|
|
|
|
|
|
|
|
log::info!("Trying to settle perp pnl account: {} market_index: {perp_index} amount: {pnl} against {counter_key} with pnl: {counter_pnl}", self.pubkey);
|
2022-09-26 04:27:31 -07:00
|
|
|
|
|
|
|
let (account_a, account_b) = if pnl > 0 {
|
2022-09-29 03:59:55 -07:00
|
|
|
((self.pubkey, self.liqee), (counter_key, counter_acc))
|
2022-09-26 04:27:31 -07:00
|
|
|
} else {
|
2022-09-29 03:59:55 -07:00
|
|
|
((counter_key, counter_acc), (self.pubkey, self.liqee))
|
2022-09-26 04:27:31 -07:00
|
|
|
};
|
|
|
|
let sig = self
|
|
|
|
.client
|
2022-12-16 04:10:46 -08:00
|
|
|
.perp_settle_pnl(perp_index, account_a, account_b)
|
|
|
|
.await?;
|
2022-09-26 04:27:31 -07:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2022-12-16 04:10:46 -08:00
|
|
|
async fn perp_liq_bankruptcy(&self) -> anyhow::Result<Option<Signature>> {
|
2022-09-27 02:36:58 -07:00
|
|
|
if self.health_cache.has_liquidatable_assets() {
|
|
|
|
return Ok(None);
|
|
|
|
}
|
|
|
|
let mut perp_bankruptcies = self
|
|
|
|
.liqee
|
|
|
|
.active_perp_positions()
|
|
|
|
.filter_map(|pp| {
|
|
|
|
let quote = pp.quote_position_native();
|
|
|
|
if quote >= 0 {
|
|
|
|
return None;
|
|
|
|
}
|
|
|
|
Some((pp.market_index, quote))
|
|
|
|
})
|
|
|
|
.collect::<Vec<(PerpMarketIndex, I80F48)>>();
|
|
|
|
perp_bankruptcies.sort_by(|a, b| a.1.cmp(&b.1));
|
|
|
|
|
|
|
|
if perp_bankruptcies.is_empty() {
|
|
|
|
return Ok(None);
|
|
|
|
}
|
|
|
|
let (perp_market_index, _) = perp_bankruptcies.first().unwrap();
|
|
|
|
|
2022-12-16 04:10:46 -08:00
|
|
|
let sig = self
|
|
|
|
.client
|
|
|
|
.perp_liq_bankruptcy(
|
|
|
|
(self.pubkey, &self.liqee),
|
|
|
|
*perp_market_index,
|
|
|
|
// Always use the max amount, since the health effect is always positive
|
|
|
|
u64::MAX,
|
|
|
|
)
|
|
|
|
.await?;
|
2022-09-27 02:36:58 -07:00
|
|
|
log::info!(
|
|
|
|
"Liquidated bankruptcy for perp market on account {}, market index {}, maint_health was {}, tx sig {:?}",
|
|
|
|
self.pubkey,
|
|
|
|
perp_market_index,
|
|
|
|
self.maint_health,
|
|
|
|
sig
|
|
|
|
);
|
|
|
|
Ok(Some(sig))
|
|
|
|
}
|
|
|
|
|
2022-12-16 04:10:46 -08:00
|
|
|
async fn tokens(&self) -> anyhow::Result<Vec<(TokenIndex, I80F48, I80F48)>> {
|
|
|
|
let tokens_maybe: anyhow::Result<Vec<(TokenIndex, I80F48, I80F48)>> =
|
|
|
|
stream::iter(self.liqee.active_token_positions())
|
|
|
|
.then(|token_position| async {
|
|
|
|
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)
|
|
|
|
.await?;
|
|
|
|
let price = bank.oracle_price(
|
|
|
|
&KeyedAccountSharedData::new(token.mint_info.oracle, oracle.into()),
|
|
|
|
None,
|
|
|
|
)?;
|
|
|
|
Ok((
|
|
|
|
token_position.token_index,
|
|
|
|
price,
|
|
|
|
token_position.native(&bank) * price,
|
|
|
|
))
|
|
|
|
})
|
|
|
|
.try_collect()
|
|
|
|
.await;
|
|
|
|
let mut tokens = tokens_maybe?;
|
2022-09-26 04:27:31 -07:00
|
|
|
tokens.sort_by(|a, b| a.2.cmp(&b.2));
|
|
|
|
Ok(tokens)
|
|
|
|
}
|
2022-07-13 05:04:20 -07:00
|
|
|
|
2022-12-16 04:10:46 -08:00
|
|
|
async fn max_token_liab_transfer(
|
2022-09-26 04:27:31 -07:00
|
|
|
&self,
|
|
|
|
source: TokenIndex,
|
|
|
|
target: TokenIndex,
|
|
|
|
) -> anyhow::Result<I80F48> {
|
|
|
|
let mut liqor = self
|
|
|
|
.account_fetcher
|
|
|
|
.fetch_fresh_mango_account(&self.client.mango_account_address)
|
2022-12-16 04:10:46 -08:00
|
|
|
.await
|
2022-09-26 04:27:31 -07:00
|
|
|
.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)
|
2022-12-16 04:10:46 -08:00
|
|
|
.await
|
2022-09-26 04:27:31 -07:00
|
|
|
.expect("always ok");
|
|
|
|
|
2022-12-16 04:10:46 -08:00
|
|
|
let source_bank = self.client.first_bank(source).await?;
|
|
|
|
let target_bank = self.client.first_bank(target).await?;
|
2022-12-06 00:25:24 -08:00
|
|
|
|
2022-11-21 22:10:23 -08:00
|
|
|
let source_price = health_cache.token_info(source).unwrap().prices.oracle;
|
|
|
|
let target_price = health_cache.token_info(target).unwrap().prices.oracle;
|
2022-09-26 04:27:31 -07:00
|
|
|
// 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(
|
2022-12-06 00:25:24 -08:00
|
|
|
&liqor,
|
|
|
|
&source_bank,
|
|
|
|
source_price,
|
|
|
|
&target_bank,
|
2022-09-26 04:27:31 -07:00
|
|
|
oracle_swap_price,
|
|
|
|
self.liqor_min_health_ratio,
|
|
|
|
)
|
|
|
|
.context("getting max_swap_source")?;
|
|
|
|
Ok(amount)
|
|
|
|
}
|
|
|
|
|
2022-12-16 04:10:46 -08:00
|
|
|
async fn token_liq(&self) -> anyhow::Result<Option<Signature>> {
|
|
|
|
if !self.health_cache.has_spot_assets() || !self.health_cache.has_spot_borrows() {
|
2022-09-26 04:27:31 -07:00
|
|
|
return Ok(None);
|
|
|
|
}
|
|
|
|
|
2022-12-16 04:10:46 -08:00
|
|
|
let tokens = self.tokens().await?;
|
2022-07-13 05:04:20 -07:00
|
|
|
|
2022-08-05 11:28:14 -07:00
|
|
|
let asset_token_index = tokens
|
|
|
|
.iter()
|
|
|
|
.rev()
|
|
|
|
.find(|(asset_token_index, _asset_price, asset_usdc_equivalent)| {
|
|
|
|
asset_usdc_equivalent.is_positive()
|
2022-12-16 04:10:46 -08:00
|
|
|
&& self
|
|
|
|
.allowed_asset_tokens
|
|
|
|
.contains(&self.client.context.token(*asset_token_index).mint_info.mint)
|
2022-08-05 11:28:14 -07:00
|
|
|
})
|
|
|
|
.ok_or_else(|| {
|
|
|
|
anyhow::anyhow!(
|
|
|
|
"mango account {}, has no asset tokens that are sellable for USDC: {:?}",
|
2022-09-26 04:27:31 -07:00
|
|
|
self.pubkey,
|
2022-08-05 11:28:14 -07:00
|
|
|
tokens
|
|
|
|
)
|
|
|
|
})?
|
|
|
|
.0;
|
|
|
|
let liab_token_index = tokens
|
|
|
|
.iter()
|
|
|
|
.find(|(liab_token_index, _liab_price, liab_usdc_equivalent)| {
|
|
|
|
liab_usdc_equivalent.is_negative()
|
2022-12-16 04:10:46 -08:00
|
|
|
&& self
|
|
|
|
.allowed_liab_tokens
|
|
|
|
.contains(&self.client.context.token(*liab_token_index).mint_info.mint)
|
2022-08-05 11:28:14 -07:00
|
|
|
})
|
|
|
|
.ok_or_else(|| {
|
|
|
|
anyhow::anyhow!(
|
|
|
|
"mango account {}, has no liab tokens that are purchasable for USDC: {:?}",
|
2022-09-26 04:27:31 -07:00
|
|
|
self.pubkey,
|
2022-08-05 11:28:14 -07:00
|
|
|
tokens
|
|
|
|
)
|
|
|
|
})?
|
|
|
|
.0;
|
2022-07-13 04:02:25 -07:00
|
|
|
|
2022-09-26 04:27:31 -07:00
|
|
|
let max_liab_transfer = self
|
|
|
|
.max_token_liab_transfer(liab_token_index, asset_token_index)
|
2022-12-16 04:10:46 -08:00
|
|
|
.await
|
2022-07-15 01:01:32 -07:00
|
|
|
.context("getting max_liab_transfer")?;
|
2022-07-13 04:02:25 -07:00
|
|
|
|
|
|
|
//
|
|
|
|
// 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
|
|
|
|
//
|
2022-09-26 04:27:31 -07:00
|
|
|
let sig = self
|
|
|
|
.client
|
2022-09-15 00:57:48 -07:00
|
|
|
.token_liq_with_token(
|
2022-09-26 04:27:31 -07:00
|
|
|
(self.pubkey, &self.liqee),
|
2022-08-05 11:28:14 -07:00
|
|
|
asset_token_index,
|
|
|
|
liab_token_index,
|
2022-07-13 03:21:02 -07:00
|
|
|
max_liab_transfer,
|
2022-07-13 04:02:25 -07:00
|
|
|
)
|
2022-12-16 04:10:46 -08:00
|
|
|
.await
|
2022-07-13 04:02:25 -07:00
|
|
|
.context("sending liq_token_with_token")?;
|
|
|
|
log::info!(
|
2022-08-05 11:28:14 -07:00
|
|
|
"Liquidated token with token for {}, maint_health was {}, tx sig {:?}",
|
2022-09-26 04:27:31 -07:00
|
|
|
self.pubkey,
|
|
|
|
self.maint_health,
|
2022-07-13 04:02:25 -07:00
|
|
|
sig
|
|
|
|
);
|
2022-09-26 04:27:31 -07:00
|
|
|
Ok(Some(sig))
|
|
|
|
}
|
|
|
|
|
2022-12-16 04:10:46 -08:00
|
|
|
async fn token_liq_bankruptcy(&self) -> anyhow::Result<Option<Signature>> {
|
2022-09-26 04:27:31 -07:00
|
|
|
if !self.health_cache.can_call_spot_bankruptcy() {
|
|
|
|
return Ok(None);
|
|
|
|
}
|
|
|
|
|
2022-12-16 04:10:46 -08:00
|
|
|
let tokens = self.tokens().await?;
|
2022-09-26 04:27:31 -07:00
|
|
|
|
|
|
|
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()
|
2022-12-16 04:10:46 -08:00
|
|
|
&& self
|
|
|
|
.allowed_liab_tokens
|
|
|
|
.contains(&self.client.context.token(*liab_token_index).mint_info.mint)
|
2022-09-26 04:27:31 -07:00
|
|
|
})
|
|
|
|
.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;
|
2022-12-16 04:10:46 -08:00
|
|
|
let max_liab_transfer = self
|
|
|
|
.max_token_liab_transfer(liab_token_index, quote_token_index)
|
|
|
|
.await?;
|
2022-09-26 04:27:31 -07:00
|
|
|
|
|
|
|
let sig = self
|
|
|
|
.client
|
|
|
|
.token_liq_bankruptcy(
|
|
|
|
(self.pubkey, &self.liqee),
|
|
|
|
liab_token_index,
|
|
|
|
max_liab_transfer,
|
|
|
|
)
|
2022-12-16 04:10:46 -08:00
|
|
|
.await
|
2022-09-26 04:27:31 -07:00
|
|
|
.context("sending liq_token_bankruptcy")?;
|
|
|
|
log::info!(
|
|
|
|
"Liquidated bankruptcy for {}, maint_health was {}, tx sig {:?}",
|
|
|
|
self.pubkey,
|
|
|
|
self.maint_health,
|
|
|
|
sig
|
|
|
|
);
|
|
|
|
Ok(Some(sig))
|
|
|
|
}
|
|
|
|
|
2022-12-16 04:10:46 -08:00
|
|
|
async fn send_liq_tx(&self) -> anyhow::Result<Signature> {
|
2022-09-26 04:27:31 -07:00
|
|
|
// 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
|
2022-12-16 04:10:46 -08:00
|
|
|
if let Some(txsig) = self.perp_close_orders().await? {
|
2022-09-26 04:27:31 -07:00
|
|
|
return Ok(txsig);
|
|
|
|
}
|
2022-12-16 04:10:46 -08:00
|
|
|
if let Some(txsig) = self.serum3_close_orders().await? {
|
2022-09-26 04:27:31 -07:00
|
|
|
return Ok(txsig);
|
|
|
|
}
|
|
|
|
|
2022-12-16 04:10:46 -08:00
|
|
|
if let Some(txsig) = self.perp_liq_base_position().await? {
|
2022-09-26 04:27:31 -07:00
|
|
|
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).
|
2022-12-16 04:10:46 -08:00
|
|
|
if let Some(txsig) = self.perp_settle_pnl().await? {
|
2022-09-26 04:27:31 -07:00
|
|
|
return Ok(txsig);
|
|
|
|
}
|
|
|
|
|
2022-12-16 04:10:46 -08:00
|
|
|
if let Some(txsig) = self.token_liq().await? {
|
2022-09-26 04:27:31 -07:00
|
|
|
return Ok(txsig);
|
|
|
|
}
|
|
|
|
|
2022-09-27 02:36:58 -07:00
|
|
|
// Socialize/insurance fund unsettleable negative pnl
|
2022-12-16 04:10:46 -08:00
|
|
|
if let Some(txsig) = self.perp_liq_bankruptcy().await? {
|
2022-09-27 02:36:58 -07:00
|
|
|
return Ok(txsig);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Socialize/insurance fund unliquidatable borrows
|
2022-12-16 04:10:46 -08:00
|
|
|
if let Some(txsig) = self.token_liq_bankruptcy().await? {
|
2022-09-26 04:27:31 -07:00
|
|
|
return Ok(txsig);
|
|
|
|
}
|
2022-09-27 02:36:58 -07:00
|
|
|
|
|
|
|
// TODO: What about unliquidatable positive perp pnl?
|
|
|
|
|
2022-09-12 06:25:50 -07:00
|
|
|
anyhow::bail!(
|
|
|
|
"Don't know what to do with liquidatable account {}, maint_health was {}",
|
2022-09-26 04:27:31 -07:00
|
|
|
self.pubkey,
|
|
|
|
self.maint_health
|
2022-09-12 06:25:50 -07:00
|
|
|
);
|
2022-09-26 04:27:31 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
2022-12-16 04:10:46 -08:00
|
|
|
pub async fn maybe_liquidate_account(
|
2022-09-26 04:27:31 -07:00
|
|
|
mango_client: &MangoClient,
|
|
|
|
account_fetcher: &chain_data::AccountFetcher,
|
|
|
|
pubkey: &Pubkey,
|
|
|
|
config: &Config,
|
|
|
|
) -> anyhow::Result<bool> {
|
|
|
|
let liqor_min_health_ratio = I80F48::from_num(config.min_health_ratio);
|
|
|
|
|
|
|
|
let account = account_fetcher.fetch_mango_account(pubkey)?;
|
2022-12-16 04:10:46 -08:00
|
|
|
let health_cache = health_cache::new(&mango_client.context, account_fetcher, &account)
|
|
|
|
.await
|
2022-12-20 03:20:31 -08:00
|
|
|
.context("creating health cache 1")?;
|
2022-09-26 04:27:31 -07:00
|
|
|
let maint_health = health_cache.health(HealthType::Maint);
|
|
|
|
if !health_cache.is_liquidatable() {
|
|
|
|
return Ok(false);
|
|
|
|
}
|
|
|
|
|
|
|
|
log::trace!(
|
|
|
|
"possible candidate: {}, with owner: {}, maint health: {}",
|
|
|
|
pubkey,
|
|
|
|
account.fixed.owner,
|
|
|
|
maint_health,
|
|
|
|
);
|
|
|
|
|
|
|
|
// Fetch a fresh account and re-compute
|
|
|
|
// This is -- unfortunately -- needed because the websocket streams seem to not
|
|
|
|
// be great at providing timely updates to the account data.
|
2022-12-16 04:10:46 -08:00
|
|
|
let account = account_fetcher.fetch_fresh_mango_account(pubkey).await?;
|
|
|
|
let health_cache = health_cache::new(&mango_client.context, account_fetcher, &account)
|
|
|
|
.await
|
2022-12-20 03:20:31 -08:00
|
|
|
.context("creating health cache 2")?;
|
2022-09-26 04:27:31 -07:00
|
|
|
if !health_cache.is_liquidatable() {
|
|
|
|
return Ok(false);
|
|
|
|
}
|
|
|
|
|
|
|
|
let maint_health = health_cache.health(HealthType::Maint);
|
|
|
|
|
2022-12-16 04:10:46 -08:00
|
|
|
let all_token_mints = HashSet::from_iter(
|
|
|
|
mango_client
|
|
|
|
.context
|
|
|
|
.tokens
|
|
|
|
.values()
|
|
|
|
.map(|c| c.mint_info.mint),
|
|
|
|
);
|
|
|
|
|
2022-09-26 04:27:31 -07:00
|
|
|
// try liquidating
|
|
|
|
let txsig = LiquidateHelper {
|
|
|
|
client: mango_client,
|
|
|
|
account_fetcher,
|
|
|
|
pubkey,
|
|
|
|
liqee: &account,
|
|
|
|
health_cache: &health_cache,
|
|
|
|
maint_health,
|
|
|
|
liqor_min_health_ratio,
|
2022-12-16 04:10:46 -08:00
|
|
|
allowed_asset_tokens: all_token_mints.clone(),
|
|
|
|
allowed_liab_tokens: all_token_mints,
|
2022-09-26 04:27:31 -07:00
|
|
|
}
|
2022-12-16 04:10:46 -08:00
|
|
|
.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
|
|
|
|
{
|
2022-08-07 11:04:19 -07:00
|
|
|
log::info!("could not refresh after liquidation: {}", e);
|
2022-07-13 04:02:25 -07:00
|
|
|
}
|
2022-08-07 11:04:19 -07:00
|
|
|
|
|
|
|
Ok(true)
|
2022-07-13 04:02:25 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
2022-12-16 04:10:46 -08:00
|
|
|
pub async fn maybe_liquidate_one<'a>(
|
2022-07-13 04:02:25 -07:00
|
|
|
mango_client: &MangoClient,
|
2022-07-21 04:03:28 -07:00
|
|
|
account_fetcher: &chain_data::AccountFetcher,
|
2022-07-13 04:02:25 -07:00
|
|
|
accounts: impl Iterator<Item = &'a Pubkey>,
|
2022-08-05 04:50:44 -07:00
|
|
|
config: &Config,
|
2022-08-05 11:28:14 -07:00
|
|
|
) -> bool {
|
2022-07-13 04:02:25 -07:00
|
|
|
for pubkey in accounts {
|
2022-12-16 04:10:46 -08:00
|
|
|
match maybe_liquidate_account(mango_client, account_fetcher, pubkey, config).await {
|
2022-07-19 05:56:26 -07:00
|
|
|
Err(err) => {
|
|
|
|
// Not all errors need to be raised to the user's attention.
|
|
|
|
let mut log_level = log::Level::Error;
|
|
|
|
|
|
|
|
// Simulation errors due to liqee precondition failures on the liquidation instructions
|
|
|
|
// will commonly happen if our liquidator is late or if there are chain forks.
|
|
|
|
match err.downcast_ref::<MangoClientError>() {
|
|
|
|
Some(MangoClientError::SendTransactionPreflightFailure { logs }) => {
|
|
|
|
if logs.contains("HealthMustBeNegative") || logs.contains("IsNotBankrupt") {
|
|
|
|
log_level = log::Level::Trace;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
_ => {}
|
|
|
|
};
|
|
|
|
log::log!(log_level, "liquidating account {}: {:?}", pubkey, err);
|
|
|
|
}
|
2022-08-05 11:28:14 -07:00
|
|
|
Ok(true) => return true,
|
2022-07-13 04:02:25 -07:00
|
|
|
_ => {}
|
|
|
|
};
|
2022-06-18 07:31:28 -07:00
|
|
|
}
|
|
|
|
|
2022-08-05 11:28:14 -07:00
|
|
|
false
|
2022-06-18 07:31:28 -07:00
|
|
|
}
|