liquidator: add allow/forbid token list (#883)
liquidator: add a way to restrict token accepted by the liquidator - add allow/forbid list of token for liquidation & conditional token swap triggering - add allow/forbid list for perp market liquidation - housekeeping: extract cli args to a dedicated file - move more hardcoded thing to config and stop using token name (replace with token index)
This commit is contained in:
parent
8a3a3bf70b
commit
338a9cb7b8
|
@ -0,0 +1,207 @@
|
|||
use crate::trigger_tcs;
|
||||
use anchor_lang::prelude::Pubkey;
|
||||
use clap::Parser;
|
||||
use mango_v4_client::{jupiter, priority_fees_cli};
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[clap()]
|
||||
pub(crate) struct CliDotenv {
|
||||
// When --dotenv <file> is passed, read the specified dotenv file before parsing args
|
||||
#[clap(long)]
|
||||
pub(crate) dotenv: std::path::PathBuf,
|
||||
|
||||
pub(crate) remaining_args: Vec<std::ffi::OsString>,
|
||||
}
|
||||
|
||||
// Prefer "--rebalance false" over "--no-rebalance" because it works
|
||||
// better with REBALANCE=false env values.
|
||||
#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum BoolArg {
|
||||
True,
|
||||
False,
|
||||
}
|
||||
|
||||
#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum JupiterVersionArg {
|
||||
Mock,
|
||||
V6,
|
||||
}
|
||||
|
||||
impl From<JupiterVersionArg> for jupiter::Version {
|
||||
fn from(a: JupiterVersionArg) -> Self {
|
||||
match a {
|
||||
JupiterVersionArg::Mock => jupiter::Version::Mock,
|
||||
JupiterVersionArg::V6 => jupiter::Version::V6,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum TcsMode {
|
||||
BorrowBuy,
|
||||
SwapSellIntoBuy,
|
||||
SwapCollateralIntoBuy,
|
||||
}
|
||||
|
||||
impl From<TcsMode> for trigger_tcs::Mode {
|
||||
fn from(a: TcsMode) -> Self {
|
||||
match a {
|
||||
TcsMode::BorrowBuy => trigger_tcs::Mode::BorrowBuyToken,
|
||||
TcsMode::SwapSellIntoBuy => trigger_tcs::Mode::SwapSellIntoBuy,
|
||||
TcsMode::SwapCollateralIntoBuy => trigger_tcs::Mode::SwapCollateralIntoBuy,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn cli_to_hashset<T: Eq + std::hash::Hash + From<u16>>(
|
||||
str_list: Option<Vec<u16>>,
|
||||
) -> HashSet<T> {
|
||||
return str_list
|
||||
.map(|v| v.iter().map(|x| T::from(*x)).collect::<HashSet<T>>())
|
||||
.unwrap_or_default();
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[clap()]
|
||||
pub struct Cli {
|
||||
#[clap(short, long, env)]
|
||||
pub(crate) rpc_url: String,
|
||||
|
||||
#[clap(long, env, value_delimiter = ';')]
|
||||
pub(crate) override_send_transaction_url: Option<Vec<String>>,
|
||||
|
||||
#[clap(long, env)]
|
||||
pub(crate) liqor_mango_account: Pubkey,
|
||||
|
||||
#[clap(long, env)]
|
||||
pub(crate) liqor_owner: String,
|
||||
|
||||
#[clap(long, env, default_value = "1000")]
|
||||
pub(crate) check_interval_ms: u64,
|
||||
|
||||
#[clap(long, env, default_value = "300")]
|
||||
pub(crate) snapshot_interval_secs: u64,
|
||||
|
||||
// how often do we refresh token swap route/prices
|
||||
#[clap(long, env, default_value = "30")]
|
||||
pub(crate) token_swap_refresh_interval_secs: u64,
|
||||
|
||||
/// how many getMultipleAccounts requests to send in parallel
|
||||
#[clap(long, env, default_value = "10")]
|
||||
pub(crate) parallel_rpc_requests: usize,
|
||||
|
||||
/// typically 100 is the max number of accounts getMultipleAccounts will retrieve at once
|
||||
#[clap(long, env, default_value = "100")]
|
||||
pub(crate) get_multiple_accounts_count: usize,
|
||||
|
||||
/// liquidator health ratio should not fall below this value
|
||||
#[clap(long, env, default_value = "50")]
|
||||
pub(crate) min_health_ratio: f64,
|
||||
|
||||
/// if rebalancing is enabled
|
||||
///
|
||||
/// typically only disabled for tests where swaps are unavailable
|
||||
#[clap(long, env, value_enum, default_value = "true")]
|
||||
pub(crate) rebalance: BoolArg,
|
||||
|
||||
/// max slippage to request on swaps to rebalance spot tokens
|
||||
#[clap(long, env, default_value = "100")]
|
||||
pub(crate) rebalance_slippage_bps: u64,
|
||||
|
||||
/// tokens to not rebalance (in addition to USDC=0); use a comma separated list of token index
|
||||
#[clap(long, env, value_parser, value_delimiter = ',')]
|
||||
pub(crate) rebalance_skip_tokens: Option<Vec<u16>>,
|
||||
|
||||
/// When closing borrows, the rebalancer can't close token positions exactly.
|
||||
/// Instead it purchases too much and then gets rid of the excess in a second step.
|
||||
/// If this is 0.05, then it'll swap borrow_value * (1 + 0.05) quote token into borrow token.
|
||||
#[clap(long, env, default_value = "0.05")]
|
||||
pub(crate) rebalance_borrow_settle_excess: f64,
|
||||
|
||||
#[clap(long, env, default_value = "30")]
|
||||
pub(crate) rebalance_refresh_timeout_secs: u64,
|
||||
|
||||
/// if taking tcs orders is enabled
|
||||
///
|
||||
/// typically only disabled for tests where swaps are unavailable
|
||||
#[clap(long, env, value_enum, default_value = "true")]
|
||||
pub(crate) take_tcs: BoolArg,
|
||||
|
||||
/// profit margin at which to take tcs orders
|
||||
#[clap(long, env, default_value = "0.0005")]
|
||||
pub(crate) tcs_profit_fraction: f64,
|
||||
|
||||
/// control how tcs triggering provides buy tokens
|
||||
#[clap(long, env, value_enum, default_value = "swap-sell-into-buy")]
|
||||
pub(crate) tcs_mode: TcsMode,
|
||||
|
||||
/// largest tcs amount to trigger in one transaction, in dollar
|
||||
#[clap(long, env, default_value = "1000.0")]
|
||||
pub(crate) tcs_max_trigger_amount: f64,
|
||||
|
||||
/// Minimum fraction of max_buy to buy for success when triggering,
|
||||
/// useful in conjunction with jupiter swaps in same tx to avoid over-buying.
|
||||
///
|
||||
/// Can be set to 0 to allow executions of any size.
|
||||
#[clap(long, env, default_value = "0.7")]
|
||||
pub(crate) tcs_min_buy_fraction: f64,
|
||||
|
||||
#[clap(flatten)]
|
||||
pub(crate) prioritization_fee_cli: priority_fees_cli::PriorityFeeArgs,
|
||||
|
||||
/// url to the lite-rpc websocket, optional
|
||||
#[clap(long, env, default_value = "")]
|
||||
pub(crate) lite_rpc_url: String,
|
||||
|
||||
/// compute limit requested for liquidation instructions
|
||||
#[clap(long, env, default_value = "250000")]
|
||||
pub(crate) compute_limit_for_liquidation: u32,
|
||||
|
||||
/// compute limit requested for tcs trigger instructions
|
||||
#[clap(long, env, default_value = "300000")]
|
||||
pub(crate) compute_limit_for_tcs: u32,
|
||||
|
||||
/// control which version of jupiter to use
|
||||
#[clap(long, env, value_enum, default_value = "v6")]
|
||||
pub(crate) jupiter_version: JupiterVersionArg,
|
||||
|
||||
/// override the url to jupiter v6
|
||||
#[clap(long, env, default_value = "https://quote-api.jup.ag/v6")]
|
||||
pub(crate) jupiter_v6_url: String,
|
||||
|
||||
/// provide a jupiter token, currently only for jup v6
|
||||
#[clap(long, env, default_value = "")]
|
||||
pub(crate) jupiter_token: String,
|
||||
|
||||
/// size of the swap to quote via jupiter to get slippage info, in dollar
|
||||
/// should be larger than tcs_max_trigger_amount
|
||||
#[clap(long, env, default_value = "1000.0")]
|
||||
pub(crate) jupiter_swap_info_amount: f64,
|
||||
|
||||
/// report liquidator's existence and pubkey
|
||||
#[clap(long, env, value_enum, default_value = "true")]
|
||||
pub(crate) telemetry: BoolArg,
|
||||
|
||||
/// liquidation refresh timeout in secs
|
||||
#[clap(long, env, default_value = "30")]
|
||||
pub(crate) liquidation_refresh_timeout_secs: u8,
|
||||
|
||||
/// tokens to exclude for liquidation/tcs (never liquidate any pair where base or quote is in this list)
|
||||
#[clap(long, env, value_parser, value_delimiter = ' ')]
|
||||
pub(crate) forbidden_tokens: Option<Vec<u16>>,
|
||||
|
||||
/// tokens to allow for liquidation/tcs (only liquidate a pair if base or quote is in this list)
|
||||
/// when empty, allows all pairs
|
||||
#[clap(long, env, value_parser, value_delimiter = ' ')]
|
||||
pub(crate) only_allow_tokens: Option<Vec<u16>>,
|
||||
|
||||
/// perp market to exclude for liquidation
|
||||
#[clap(long, env, value_parser, value_delimiter = ' ')]
|
||||
pub(crate) liquidation_forbidden_perp_markets: Option<Vec<u16>>,
|
||||
|
||||
/// perp market to allow for liquidation (only liquidate if is in this list)
|
||||
/// when empty, allows all pairs
|
||||
#[clap(long, env, value_parser, value_delimiter = ' ')]
|
||||
pub(crate) liquidation_only_allow_perp_markets: Option<Vec<u16>>,
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
use std::cmp::Reverse;
|
||||
use std::collections::HashSet;
|
||||
use std::time::Duration;
|
||||
|
||||
|
@ -20,6 +21,12 @@ pub struct Config {
|
|||
pub refresh_timeout: Duration,
|
||||
pub compute_limit_for_liq_ix: u32,
|
||||
|
||||
pub only_allowed_tokens: HashSet<TokenIndex>,
|
||||
pub forbidden_tokens: HashSet<TokenIndex>,
|
||||
|
||||
pub only_allowed_perp_markets: HashSet<PerpMarketIndex>,
|
||||
pub forbidden_perp_markets: HashSet<PerpMarketIndex>,
|
||||
|
||||
/// If we cram multiple ix into a transaction, don't exceed this level
|
||||
/// of expected-cu.
|
||||
pub max_cu_per_transaction: u32,
|
||||
|
@ -33,8 +40,6 @@ struct LiquidateHelper<'a> {
|
|||
health_cache: &'a HealthCache,
|
||||
maint_health: I80F48,
|
||||
liqor_min_health_ratio: I80F48,
|
||||
allowed_asset_tokens: HashSet<Pubkey>,
|
||||
allowed_liab_tokens: HashSet<Pubkey>,
|
||||
config: Config,
|
||||
}
|
||||
|
||||
|
@ -136,6 +141,25 @@ impl<'a> LiquidateHelper<'a> {
|
|||
let all_perp_base_positions: anyhow::Result<
|
||||
Vec<Option<(PerpMarketIndex, i64, I80F48, I80F48)>>,
|
||||
> = stream::iter(self.liqee.active_perp_positions())
|
||||
.filter(|pp| async {
|
||||
if self
|
||||
.config
|
||||
.forbidden_perp_markets
|
||||
.contains(&pp.market_index)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if !self.config.only_allowed_perp_markets.is_empty()
|
||||
&& !self
|
||||
.config
|
||||
.only_allowed_perp_markets
|
||||
.contains(&pp.market_index)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
})
|
||||
.then(|pp| async {
|
||||
let base_lots = pp.base_position_lots();
|
||||
if (base_lots == 0 && pp.quote_position_native() <= 0) || pp.has_open_taker_fills()
|
||||
|
@ -353,6 +377,7 @@ impl<'a> LiquidateHelper<'a> {
|
|||
.health_cache
|
||||
.token_infos
|
||||
.iter()
|
||||
.filter(|p| !self.config.forbidden_tokens.contains(&p.token_index))
|
||||
.zip(
|
||||
self.health_cache
|
||||
.effective_token_balances(HealthType::LiquidationEnd)
|
||||
|
@ -378,26 +403,9 @@ impl<'a> LiquidateHelper<'a> {
|
|||
is_valid_asset.then_some((ti.token_index, is_preferred, quote_value))
|
||||
})
|
||||
.collect_vec();
|
||||
// sort such that preferred tokens are at the end, and the one with the larget quote value is
|
||||
// at the very end
|
||||
potential_assets.sort_by_key(|(_, is_preferred, amount)| (*is_preferred, *amount));
|
||||
|
||||
// filter only allowed assets
|
||||
let potential_allowed_assets = potential_assets.iter().filter_map(|(ti, _, _)| {
|
||||
let is_allowed = self
|
||||
.allowed_asset_tokens
|
||||
.contains(&self.client.context.token(*ti).mint);
|
||||
is_allowed.then_some(*ti)
|
||||
});
|
||||
|
||||
let asset_token_index = match potential_allowed_assets.last() {
|
||||
Some(token_index) => token_index,
|
||||
None => anyhow::bail!(
|
||||
"mango account {}, has no allowed asset tokens that are liquidatable: {:?}",
|
||||
self.pubkey,
|
||||
potential_assets,
|
||||
),
|
||||
};
|
||||
// sort such that preferred tokens are at the start, and the one with the larget quote value is
|
||||
// at 0
|
||||
potential_assets.sort_by_key(|(_, is_preferred, amount)| Reverse((*is_preferred, *amount)));
|
||||
|
||||
//
|
||||
// find a good liab, same as for assets
|
||||
|
@ -410,29 +418,69 @@ impl<'a> LiquidateHelper<'a> {
|
|||
let tokens = (-ti.balance_spot).min(-effective.spot_and_perp);
|
||||
let is_valid_liab = tokens > 0;
|
||||
let quote_value = tokens * ti.prices.oracle;
|
||||
is_valid_liab.then_some((ti.token_index, quote_value))
|
||||
is_valid_liab.then_some((ti.token_index, false, quote_value))
|
||||
})
|
||||
.collect_vec();
|
||||
// largest liquidatable liability at the end
|
||||
potential_liabs.sort_by_key(|(_, amount)| *amount);
|
||||
// largest liquidatable liability at the start
|
||||
potential_liabs.sort_by_key(|(_, is_preferred, amount)| Reverse((*is_preferred, *amount)));
|
||||
|
||||
// filter only allowed liabs
|
||||
let potential_allowed_liabs = potential_liabs.iter().filter_map(|(ti, _)| {
|
||||
let is_allowed = self
|
||||
.allowed_liab_tokens
|
||||
.contains(&self.client.context.token(*ti).mint);
|
||||
is_allowed.then_some(*ti)
|
||||
});
|
||||
//
|
||||
// Find a pair
|
||||
//
|
||||
|
||||
let liab_token_index = match potential_allowed_liabs.last() {
|
||||
Some(token_index) => token_index,
|
||||
None => anyhow::bail!(
|
||||
"mango account {}, has no liab tokens that are liquidatable: {:?}",
|
||||
fn find_best_token(
|
||||
lh: &LiquidateHelper,
|
||||
token_list: &Vec<(TokenIndex, bool, I80F48)>,
|
||||
) -> (Option<TokenIndex>, Option<TokenIndex>) {
|
||||
let mut best_whitelisted = None;
|
||||
let mut best = None;
|
||||
|
||||
let allowed_token_list = token_list
|
||||
.iter()
|
||||
.filter_map(|(ti, _, _)| (!lh.config.forbidden_tokens.contains(ti)).then_some(ti));
|
||||
|
||||
for ti in allowed_token_list {
|
||||
let whitelisted = lh.config.only_allowed_tokens.is_empty()
|
||||
|| lh.config.only_allowed_tokens.contains(ti);
|
||||
if best.is_none() {
|
||||
best = Some(*ti);
|
||||
}
|
||||
|
||||
if best_whitelisted.is_none() && whitelisted {
|
||||
best_whitelisted = Some(*ti);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (best, best_whitelisted);
|
||||
}
|
||||
|
||||
let (best_asset, best_whitelisted_asset) = find_best_token(self, &potential_assets);
|
||||
let (best_liab, best_whitelisted_liab) = find_best_token(self, &potential_liabs);
|
||||
|
||||
let best_pair_opt = [
|
||||
(best_whitelisted_asset, best_liab),
|
||||
(best_asset, best_whitelisted_liab),
|
||||
]
|
||||
.iter()
|
||||
.filter_map(|(a, l)| (a.is_some() && l.is_some()).then_some((a.unwrap(), l.unwrap())))
|
||||
.next();
|
||||
|
||||
if best_pair_opt.is_none() {
|
||||
anyhow::bail!(
|
||||
"mango account {}, has no allowed asset/liab tokens pair that are liquidatable: assets={:?}; liabs={:?}",
|
||||
self.pubkey,
|
||||
potential_assets,
|
||||
potential_liabs,
|
||||
),
|
||||
)
|
||||
};
|
||||
|
||||
let (asset_token_index, liab_token_index) = best_pair_opt.unwrap();
|
||||
|
||||
//
|
||||
// Compute max transfer size
|
||||
//
|
||||
|
||||
let max_liab_transfer = self
|
||||
.max_token_liab_transfer(liab_token_index, asset_token_index)
|
||||
.await
|
||||
|
@ -484,9 +532,7 @@ impl<'a> LiquidateHelper<'a> {
|
|||
.iter()
|
||||
.find(|(liab_token_index, _liab_price, liab_usdc_equivalent)| {
|
||||
liab_usdc_equivalent.is_negative()
|
||||
&& self
|
||||
.allowed_liab_tokens
|
||||
.contains(&self.client.context.token(*liab_token_index).mint)
|
||||
&& !self.config.forbidden_tokens.contains(liab_token_index)
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
|
@ -643,8 +689,6 @@ pub async fn maybe_liquidate_account(
|
|||
|
||||
let maint_health = health_cache.health(HealthType::Maint);
|
||||
|
||||
let all_token_mints = HashSet::from_iter(mango_client.context.tokens.values().map(|c| c.mint));
|
||||
|
||||
// try liquidating
|
||||
let maybe_txsig = LiquidateHelper {
|
||||
client: mango_client,
|
||||
|
@ -654,8 +698,6 @@ pub async fn maybe_liquidate_account(
|
|||
health_cache: &health_cache,
|
||||
maint_health,
|
||||
liqor_min_health_ratio,
|
||||
allowed_asset_tokens: all_token_mints.clone(),
|
||||
allowed_liab_tokens: all_token_mints,
|
||||
config: config.clone(),
|
||||
}
|
||||
.send_liq_tx()
|
||||
|
|
|
@ -7,10 +7,9 @@ use anchor_client::Cluster;
|
|||
use anyhow::Context;
|
||||
use clap::Parser;
|
||||
use mango_v4::state::{PerpMarketIndex, TokenIndex};
|
||||
use mango_v4_client::priority_fees_cli;
|
||||
use mango_v4_client::AsyncChannelSendUnlessFull;
|
||||
use mango_v4_client::{
|
||||
account_update_stream, chain_data, error_tracking::ErrorTracking, jupiter, keypair_from_cli,
|
||||
account_update_stream, chain_data, error_tracking::ErrorTracking, keypair_from_cli,
|
||||
snapshot_source, websocket_source, Client, MangoClient, MangoClientError, MangoGroupContext,
|
||||
TransactionBuilderConfig,
|
||||
};
|
||||
|
@ -21,6 +20,7 @@ use solana_sdk::pubkey::Pubkey;
|
|||
use solana_sdk::signer::Signer;
|
||||
use tracing::*;
|
||||
|
||||
pub mod cli_args;
|
||||
pub mod liquidate;
|
||||
pub mod metrics;
|
||||
pub mod rebalance;
|
||||
|
@ -36,158 +36,6 @@ use crate::util::{is_mango_account, is_mint_info, is_perp_market};
|
|||
#[global_allocator]
|
||||
static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[clap()]
|
||||
struct CliDotenv {
|
||||
// When --dotenv <file> is passed, read the specified dotenv file before parsing args
|
||||
#[clap(long)]
|
||||
dotenv: std::path::PathBuf,
|
||||
|
||||
remaining_args: Vec<std::ffi::OsString>,
|
||||
}
|
||||
|
||||
// Prefer "--rebalance false" over "--no-rebalance" because it works
|
||||
// better with REBALANCE=false env values.
|
||||
#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum BoolArg {
|
||||
True,
|
||||
False,
|
||||
}
|
||||
|
||||
#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum JupiterVersionArg {
|
||||
Mock,
|
||||
V6,
|
||||
}
|
||||
|
||||
impl From<JupiterVersionArg> for jupiter::Version {
|
||||
fn from(a: JupiterVersionArg) -> Self {
|
||||
match a {
|
||||
JupiterVersionArg::Mock => jupiter::Version::Mock,
|
||||
JupiterVersionArg::V6 => jupiter::Version::V6,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum TcsMode {
|
||||
BorrowBuy,
|
||||
SwapSellIntoBuy,
|
||||
SwapCollateralIntoBuy,
|
||||
}
|
||||
|
||||
impl From<TcsMode> for trigger_tcs::Mode {
|
||||
fn from(a: TcsMode) -> Self {
|
||||
match a {
|
||||
TcsMode::BorrowBuy => trigger_tcs::Mode::BorrowBuyToken,
|
||||
TcsMode::SwapSellIntoBuy => trigger_tcs::Mode::SwapSellIntoBuy,
|
||||
TcsMode::SwapCollateralIntoBuy => trigger_tcs::Mode::SwapCollateralIntoBuy,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[clap()]
|
||||
struct Cli {
|
||||
#[clap(short, long, env)]
|
||||
rpc_url: String,
|
||||
|
||||
#[clap(long, env, value_delimiter = ';')]
|
||||
override_send_transaction_url: Option<Vec<String>>,
|
||||
|
||||
#[clap(long, env)]
|
||||
liqor_mango_account: Pubkey,
|
||||
|
||||
#[clap(long, env)]
|
||||
liqor_owner: String,
|
||||
|
||||
#[clap(long, env, default_value = "1000")]
|
||||
check_interval_ms: u64,
|
||||
|
||||
#[clap(long, env, default_value = "300")]
|
||||
snapshot_interval_secs: u64,
|
||||
|
||||
/// how many getMultipleAccounts requests to send in parallel
|
||||
#[clap(long, env, default_value = "10")]
|
||||
parallel_rpc_requests: usize,
|
||||
|
||||
/// typically 100 is the max number of accounts getMultipleAccounts will retrieve at once
|
||||
#[clap(long, env, default_value = "100")]
|
||||
get_multiple_accounts_count: usize,
|
||||
|
||||
/// liquidator health ratio should not fall below this value
|
||||
#[clap(long, env, default_value = "50")]
|
||||
min_health_ratio: f64,
|
||||
|
||||
/// if rebalancing is enabled
|
||||
///
|
||||
/// typically only disabled for tests where swaps are unavailable
|
||||
#[clap(long, env, value_enum, default_value = "true")]
|
||||
rebalance: BoolArg,
|
||||
|
||||
/// max slippage to request on swaps to rebalance spot tokens
|
||||
#[clap(long, env, default_value = "100")]
|
||||
rebalance_slippage_bps: u64,
|
||||
|
||||
/// tokens to not rebalance (in addition to USDC); use a comma separated list of names
|
||||
#[clap(long, env, default_value = "")]
|
||||
rebalance_skip_tokens: String,
|
||||
|
||||
/// if taking tcs orders is enabled
|
||||
///
|
||||
/// typically only disabled for tests where swaps are unavailable
|
||||
#[clap(long, env, value_enum, default_value = "true")]
|
||||
take_tcs: BoolArg,
|
||||
|
||||
/// profit margin at which to take tcs orders
|
||||
#[clap(long, env, default_value = "0.0005")]
|
||||
tcs_profit_fraction: f64,
|
||||
|
||||
/// control how tcs triggering provides buy tokens
|
||||
#[clap(long, env, value_enum, default_value = "swap-sell-into-buy")]
|
||||
tcs_mode: TcsMode,
|
||||
|
||||
/// largest tcs amount to trigger in one transaction, in dollar
|
||||
#[clap(long, env, default_value = "1000.0")]
|
||||
tcs_max_trigger_amount: f64,
|
||||
|
||||
#[clap(flatten)]
|
||||
prioritization_fee_cli: priority_fees_cli::PriorityFeeArgs,
|
||||
|
||||
/// url to the lite-rpc websocket, optional
|
||||
#[clap(long, env, default_value = "")]
|
||||
lite_rpc_url: String,
|
||||
|
||||
/// compute limit requested for liquidation instructions
|
||||
#[clap(long, env, default_value = "250000")]
|
||||
compute_limit_for_liquidation: u32,
|
||||
|
||||
/// compute limit requested for tcs trigger instructions
|
||||
#[clap(long, env, default_value = "300000")]
|
||||
compute_limit_for_tcs: u32,
|
||||
|
||||
/// control which version of jupiter to use
|
||||
#[clap(long, env, value_enum, default_value = "v6")]
|
||||
jupiter_version: JupiterVersionArg,
|
||||
|
||||
/// override the url to jupiter v6
|
||||
#[clap(long, env, default_value = "https://quote-api.jup.ag/v6")]
|
||||
jupiter_v6_url: String,
|
||||
|
||||
/// provide a jupiter token, currently only for jup v6
|
||||
#[clap(long, env, default_value = "")]
|
||||
jupiter_token: String,
|
||||
|
||||
/// size of the swap to quote via jupiter to get slippage info, in dollar
|
||||
/// should be larger than tcs_max_trigger_amount
|
||||
#[clap(long, env, default_value = "1000.0")]
|
||||
jupiter_swap_info_amount: f64,
|
||||
|
||||
/// report liquidator's existence and pubkey
|
||||
#[clap(long, env, value_enum, default_value = "true")]
|
||||
telemetry: BoolArg,
|
||||
}
|
||||
|
||||
pub fn encode_address(addr: &Pubkey) -> String {
|
||||
bs58::encode(&addr.to_bytes()).into_string()
|
||||
}
|
||||
|
@ -356,8 +204,15 @@ async fn main() -> anyhow::Result<()> {
|
|||
min_health_ratio: cli.min_health_ratio,
|
||||
compute_limit_for_liq_ix: cli.compute_limit_for_liquidation,
|
||||
max_cu_per_transaction: 1_000_000,
|
||||
// TODO: config
|
||||
refresh_timeout: Duration::from_secs(30),
|
||||
refresh_timeout: Duration::from_secs(cli.liquidation_refresh_timeout_secs as u64),
|
||||
only_allowed_tokens: cli_args::cli_to_hashset::<TokenIndex>(cli.only_allow_tokens),
|
||||
forbidden_tokens: cli_args::cli_to_hashset::<TokenIndex>(cli.forbidden_tokens),
|
||||
only_allowed_perp_markets: cli_args::cli_to_hashset::<PerpMarketIndex>(
|
||||
cli.liquidation_only_allow_perp_markets,
|
||||
),
|
||||
forbidden_perp_markets: cli_args::cli_to_hashset::<PerpMarketIndex>(
|
||||
cli.liquidation_forbidden_perp_markets,
|
||||
),
|
||||
};
|
||||
|
||||
let tcs_config = trigger_tcs::Config {
|
||||
|
@ -366,14 +221,15 @@ async fn main() -> anyhow::Result<()> {
|
|||
compute_limit_for_trigger: cli.compute_limit_for_tcs,
|
||||
profit_fraction: cli.tcs_profit_fraction,
|
||||
collateral_token_index: 0, // USDC
|
||||
// TODO: config
|
||||
refresh_timeout: Duration::from_secs(30),
|
||||
|
||||
jupiter_version: cli.jupiter_version.into(),
|
||||
jupiter_slippage_bps: cli.rebalance_slippage_bps,
|
||||
|
||||
mode: cli.tcs_mode.into(),
|
||||
min_buy_fraction: 0.7,
|
||||
min_buy_fraction: cli.tcs_min_buy_fraction,
|
||||
|
||||
only_allowed_tokens: liq_config.only_allowed_tokens.clone(),
|
||||
forbidden_tokens: liq_config.forbidden_tokens.clone(),
|
||||
};
|
||||
|
||||
let mut rebalance_interval = tokio::time::interval(Duration::from_secs(30));
|
||||
|
@ -381,16 +237,10 @@ async fn main() -> anyhow::Result<()> {
|
|||
let rebalance_config = rebalance::Config {
|
||||
enabled: cli.rebalance == BoolArg::True,
|
||||
slippage_bps: cli.rebalance_slippage_bps,
|
||||
// TODO: config
|
||||
borrow_settle_excess: 1.05,
|
||||
refresh_timeout: Duration::from_secs(30),
|
||||
borrow_settle_excess: (1f64 + cli.rebalance_borrow_settle_excess).max(1f64),
|
||||
refresh_timeout: Duration::from_secs(cli.rebalance_refresh_timeout_secs),
|
||||
jupiter_version: cli.jupiter_version.into(),
|
||||
skip_tokens: cli
|
||||
.rebalance_skip_tokens
|
||||
.split(',')
|
||||
.filter(|v| !v.is_empty())
|
||||
.map(|name| mango_client.context.token_by_name(name).token_index)
|
||||
.collect(),
|
||||
skip_tokens: cli.rebalance_skip_tokens.unwrap_or(Vec::new()),
|
||||
allow_withdraws: signer_is_owner,
|
||||
};
|
||||
|
||||
|
@ -532,16 +382,13 @@ async fn main() -> anyhow::Result<()> {
|
|||
|
||||
let mut took_tcs = false;
|
||||
if !liquidated && cli.take_tcs == BoolArg::True {
|
||||
took_tcs = match liquidation
|
||||
took_tcs = liquidation
|
||||
.maybe_take_token_conditional_swap(account_addresses.iter())
|
||||
.await
|
||||
{
|
||||
Ok(v) => v,
|
||||
Err(err) => {
|
||||
.unwrap_or_else(|err| {
|
||||
error!("error during maybe_take_token_conditional_swap: {err}");
|
||||
false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if liquidated || took_tcs {
|
||||
|
@ -552,14 +399,15 @@ async fn main() -> anyhow::Result<()> {
|
|||
});
|
||||
|
||||
let token_swap_info_job = tokio::spawn({
|
||||
// TODO: configurable interval
|
||||
let mut interval = mango_v4_client::delay_interval(Duration::from_secs(60));
|
||||
let mut interval = mango_v4_client::delay_interval(Duration::from_secs(
|
||||
cli.token_swap_refresh_interval_secs,
|
||||
));
|
||||
let mut startup_wait = mango_v4_client::delay_interval(Duration::from_secs(1));
|
||||
let shared_state = shared_state.clone();
|
||||
async move {
|
||||
loop {
|
||||
startup_wait.tick().await;
|
||||
if !shared_state.read().unwrap().one_snapshot_done {
|
||||
startup_wait.tick().await;
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -594,6 +442,7 @@ async fn main() -> anyhow::Result<()> {
|
|||
));
|
||||
}
|
||||
|
||||
use cli_args::{BoolArg, Cli, CliDotenv};
|
||||
use futures::StreamExt;
|
||||
let mut jobs: futures::stream::FuturesUnordered<_> = vec![
|
||||
data_job,
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
use std::collections::HashSet;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
pin::Pin,
|
||||
sync::{Arc, RwLock},
|
||||
time::{Duration, Instant},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use futures_core::Future;
|
||||
|
@ -56,7 +57,6 @@ pub enum Mode {
|
|||
pub struct Config {
|
||||
pub min_health_ratio: f64,
|
||||
pub max_trigger_quote_amount: u64,
|
||||
pub refresh_timeout: Duration,
|
||||
pub compute_limit_for_trigger: u32,
|
||||
pub collateral_token_index: TokenIndex,
|
||||
|
||||
|
@ -73,6 +73,9 @@ pub struct Config {
|
|||
pub jupiter_version: jupiter::Version,
|
||||
pub jupiter_slippage_bps: u64,
|
||||
pub mode: Mode,
|
||||
|
||||
pub only_allowed_tokens: HashSet<TokenIndex>,
|
||||
pub forbidden_tokens: HashSet<TokenIndex>,
|
||||
}
|
||||
|
||||
pub enum JupiterQuoteCacheResult<T> {
|
||||
|
@ -401,11 +404,43 @@ impl Context {
|
|||
Ok(taker_price >= base_price * cost_over_oracle * (1.0 + self.config.profit_fraction))
|
||||
}
|
||||
|
||||
// excluded by config
|
||||
fn tcs_pair_is_allowed(
|
||||
&self,
|
||||
buy_token_index: TokenIndex,
|
||||
sell_token_index: TokenIndex,
|
||||
) -> bool {
|
||||
if self.config.forbidden_tokens.contains(&buy_token_index) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if self.config.forbidden_tokens.contains(&sell_token_index) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if self.config.only_allowed_tokens.is_empty() {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.config.only_allowed_tokens.contains(&buy_token_index) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.config.only_allowed_tokens.contains(&sell_token_index) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Either expired or triggerable with ok-looking price.
|
||||
fn tcs_is_interesting(&self, tcs: &TokenConditionalSwap) -> anyhow::Result<bool> {
|
||||
if tcs.is_expired(self.now_ts) {
|
||||
return Ok(true);
|
||||
}
|
||||
if !self.tcs_pair_is_allowed(tcs.buy_token_index, tcs.buy_token_index) {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let (_, buy_token_price, _) = self.token_bank_price_mint(tcs.buy_token_index)?;
|
||||
let (_, sell_token_price, _) = self.token_bank_price_mint(tcs.sell_token_index)?;
|
||||
|
|
Loading…
Reference in New Issue