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:
Serge Farny 2024-02-19 10:20:12 +01:00 committed by GitHub
parent 8a3a3bf70b
commit 338a9cb7b8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 356 additions and 223 deletions

View File

@ -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>>,
}

View File

@ -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()

View File

@ -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,

View File

@ -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)?;