liquidator: rebalance with openbook (limit order) (#938)
liquidator: rebalance with limit order
This commit is contained in:
parent
49b461f32d
commit
2eb94b5286
|
@ -136,6 +136,13 @@ pub struct Cli {
|
||||||
#[clap(long, env, default_value = "30")]
|
#[clap(long, env, default_value = "30")]
|
||||||
pub(crate) rebalance_refresh_timeout_secs: u64,
|
pub(crate) rebalance_refresh_timeout_secs: u64,
|
||||||
|
|
||||||
|
#[clap(long, env, value_enum, default_value = "false")]
|
||||||
|
pub(crate) rebalance_using_limit_order: BoolArg,
|
||||||
|
|
||||||
|
/// distance (in bps) from oracle price at which to place order for rebalancing
|
||||||
|
#[clap(long, env, default_value = "100")]
|
||||||
|
pub(crate) rebalance_limit_order_distance_from_oracle_price_bps: u64,
|
||||||
|
|
||||||
/// if taking tcs orders is enabled
|
/// if taking tcs orders is enabled
|
||||||
///
|
///
|
||||||
/// typically only disabled for tests where swaps are unavailable
|
/// typically only disabled for tests where swaps are unavailable
|
||||||
|
|
|
@ -291,6 +291,9 @@ async fn main() -> anyhow::Result<()> {
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
use_sanctum: cli.sanctum_enabled == BoolArg::True,
|
use_sanctum: cli.sanctum_enabled == BoolArg::True,
|
||||||
allow_withdraws: true,
|
allow_withdraws: true,
|
||||||
|
use_limit_order: cli.rebalance_using_limit_order == BoolArg::True,
|
||||||
|
limit_order_distance_from_oracle_price_bps: cli
|
||||||
|
.rebalance_limit_order_distance_from_oracle_price_bps,
|
||||||
};
|
};
|
||||||
rebalance_config.validate(&mango_client.context);
|
rebalance_config.validate(&mango_client.context);
|
||||||
|
|
||||||
|
|
|
@ -2,21 +2,27 @@ use itertools::Itertools;
|
||||||
use mango_v4::accounts_zerocopy::KeyedAccountSharedData;
|
use mango_v4::accounts_zerocopy::KeyedAccountSharedData;
|
||||||
use mango_v4::state::{
|
use mango_v4::state::{
|
||||||
Bank, BookSide, MangoAccountValue, OracleAccountInfos, PerpMarket, PerpPosition,
|
Bank, BookSide, MangoAccountValue, OracleAccountInfos, PerpMarket, PerpPosition,
|
||||||
PlaceOrderType, Side, TokenIndex, QUOTE_TOKEN_INDEX,
|
PlaceOrderType, Serum3MarketIndex, Side, TokenIndex, QUOTE_TOKEN_INDEX,
|
||||||
};
|
};
|
||||||
use mango_v4_client::{
|
use mango_v4_client::{
|
||||||
chain_data, perp_pnl, swap, MangoClient, MangoGroupContext, PerpMarketContext, TokenContext,
|
chain_data, perp_pnl, swap, MangoClient, MangoGroupContext, PerpMarketContext,
|
||||||
TransactionBuilder, TransactionSize,
|
PreparedInstructions, Serum3MarketContext, TokenContext, TransactionBuilder, TransactionSize,
|
||||||
};
|
};
|
||||||
use solana_client::nonblocking::rpc_client::RpcClient;
|
use solana_client::nonblocking::rpc_client::RpcClient;
|
||||||
|
|
||||||
use {fixed::types::I80F48, solana_sdk::pubkey::Pubkey};
|
use {fixed::types::I80F48, solana_sdk::pubkey::Pubkey};
|
||||||
|
|
||||||
|
use fixed::types::extra::U48;
|
||||||
|
use fixed::FixedI128;
|
||||||
|
use mango_v4::accounts_ix::{Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side};
|
||||||
|
use mango_v4::serum3_cpi;
|
||||||
|
use mango_v4::serum3_cpi::{OpenOrdersAmounts, OpenOrdersSlim};
|
||||||
|
use solana_sdk::account::ReadableAccount;
|
||||||
use solana_sdk::signature::Signature;
|
use solana_sdk::signature::Signature;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||||
use tracing::*;
|
use tracing::*;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
@ -24,6 +30,8 @@ pub struct Config {
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
/// Maximum slippage allowed in Jupiter
|
/// Maximum slippage allowed in Jupiter
|
||||||
pub slippage_bps: u64,
|
pub slippage_bps: u64,
|
||||||
|
/// Maximum slippage from oracle price for limit orders
|
||||||
|
pub limit_order_distance_from_oracle_price_bps: u64,
|
||||||
/// When closing borrows, the rebalancer can't close token positions exactly.
|
/// 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.
|
/// Instead it purchases too much and then gets rid of the excess in a second step.
|
||||||
/// If this is 1.05, then it'll swap borrow_value * 1.05 quote token into borrow token.
|
/// If this is 1.05, then it'll swap borrow_value * 1.05 quote token into borrow token.
|
||||||
|
@ -35,6 +43,7 @@ pub struct Config {
|
||||||
pub alternate_sanctum_route_tokens: Vec<TokenIndex>,
|
pub alternate_sanctum_route_tokens: Vec<TokenIndex>,
|
||||||
pub allow_withdraws: bool,
|
pub allow_withdraws: bool,
|
||||||
pub use_sanctum: bool,
|
pub use_sanctum: bool,
|
||||||
|
pub use_limit_order: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
|
@ -440,6 +449,7 @@ impl Rebalancer {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn rebalance_tokens(&self) -> anyhow::Result<()> {
|
async fn rebalance_tokens(&self) -> anyhow::Result<()> {
|
||||||
|
self.close_and_settle_all_openbook_orders().await?;
|
||||||
let account = self.mango_account()?;
|
let account = self.mango_account()?;
|
||||||
|
|
||||||
// TODO: configurable?
|
// TODO: configurable?
|
||||||
|
@ -462,12 +472,24 @@ impl Rebalancer {
|
||||||
// Imagine SOL at 0.04 USDC-native per SOL-native: Any amounts below 25 SOL-native
|
// Imagine SOL at 0.04 USDC-native per SOL-native: Any amounts below 25 SOL-native
|
||||||
// would not be worth a single USDC-native.
|
// would not be worth a single USDC-native.
|
||||||
//
|
//
|
||||||
// To avoid errors, we consider all amounts below 1000 * (1/oracle) dust and don't try
|
// To avoid errors, we consider all amounts below 2 * (1/oracle) dust and don't try
|
||||||
// to sell them. Instead they will be withdrawn at the end.
|
// to sell them. Instead they will be withdrawn at the end.
|
||||||
// Purchases will aim to purchase slightly more than is needed, such that we can
|
// Purchases will aim to purchase slightly more than is needed, such that we can
|
||||||
// again withdraw the dust at the end.
|
// again withdraw the dust at the end.
|
||||||
// 1000 USD-native is $0.001; note that delegates are allowed to withdraw up to DELEGATE_WITHDRAW_MAX ($0.1)
|
let dust_threshold_res = if self.config.use_limit_order {
|
||||||
let dust_threshold = I80F48::from(1_000) / token_price;
|
self.dust_threshold_for_limit_order(token)
|
||||||
|
.await
|
||||||
|
.map(|x| I80F48::from(x))
|
||||||
|
} else {
|
||||||
|
Ok(I80F48::from(2) / token_price)
|
||||||
|
};
|
||||||
|
|
||||||
|
let Ok(dust_threshold) = dust_threshold_res
|
||||||
|
else {
|
||||||
|
let e = dust_threshold_res.unwrap_err();
|
||||||
|
error!("Cannot rebalance token {}, probably missing USDC market ? - error: {}", token.name, e);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
// Some rebalancing can actually change non-USDC positions (rebalancing to SOL)
|
// Some rebalancing can actually change non-USDC positions (rebalancing to SOL)
|
||||||
// So re-fetch the current token position amount
|
// So re-fetch the current token position amount
|
||||||
|
@ -481,60 +503,29 @@ impl Rebalancer {
|
||||||
};
|
};
|
||||||
let mut amount = fresh_amount()?;
|
let mut amount = fresh_amount()?;
|
||||||
|
|
||||||
trace!(token_index, %amount, %dust_threshold, "checking");
|
trace!(token_index, token.name, %amount, %dust_threshold, "checking");
|
||||||
if amount < 0 {
|
|
||||||
// Buy
|
|
||||||
let buy_amount =
|
|
||||||
amount.abs().ceil() + (dust_threshold - I80F48::ONE).max(I80F48::ZERO);
|
|
||||||
let input_amount =
|
|
||||||
buy_amount * token_price * I80F48::from_num(self.config.borrow_settle_excess);
|
|
||||||
let (txsig, route) = self
|
|
||||||
.token_swap_buy(&account, token_mint, input_amount.to_num())
|
|
||||||
.await?;
|
|
||||||
let in_token = self
|
|
||||||
.mango_client
|
|
||||||
.context
|
|
||||||
.token_by_mint(&route.input_mint)
|
|
||||||
.unwrap();
|
|
||||||
info!(
|
|
||||||
%txsig,
|
|
||||||
"bought {} {} for {} {}",
|
|
||||||
token.native_to_ui(I80F48::from(route.out_amount)),
|
|
||||||
token.name,
|
|
||||||
in_token.native_to_ui(I80F48::from(route.in_amount)),
|
|
||||||
in_token.name,
|
|
||||||
);
|
|
||||||
if !self.refresh_mango_account_after_tx(txsig).await? {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
amount = fresh_amount()?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if amount > dust_threshold {
|
if self.config.use_limit_order {
|
||||||
// Sell
|
self.unwind_using_limit_orders(
|
||||||
|
&account,
|
||||||
// To avoid creating a borrow when paying flash loan fees, sell only a fraction
|
token,
|
||||||
let input_amount = amount * I80F48::from_num(0.99);
|
token_price,
|
||||||
let (txsig, route) = self
|
dust_threshold,
|
||||||
.token_swap_sell(&account, token_mint, input_amount.to_num::<u64>())
|
amount,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
} else {
|
||||||
|
amount = self
|
||||||
|
.unwind_using_swap(
|
||||||
|
&account,
|
||||||
|
token,
|
||||||
|
token_mint,
|
||||||
|
token_price,
|
||||||
|
dust_threshold,
|
||||||
|
fresh_amount,
|
||||||
|
amount,
|
||||||
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let out_token = self
|
|
||||||
.mango_client
|
|
||||||
.context
|
|
||||||
.token_by_mint(&route.output_mint)
|
|
||||||
.unwrap();
|
|
||||||
info!(
|
|
||||||
%txsig,
|
|
||||||
"sold {} {} for {} {}",
|
|
||||||
token.native_to_ui(I80F48::from(route.in_amount)),
|
|
||||||
token.name,
|
|
||||||
out_token.native_to_ui(I80F48::from(route.out_amount)),
|
|
||||||
out_token.name,
|
|
||||||
);
|
|
||||||
if !self.refresh_mango_account_after_tx(txsig).await? {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
amount = fresh_amount()?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Any remainder that could not be sold just gets withdrawn to ensure the
|
// Any remainder that could not be sold just gets withdrawn to ensure the
|
||||||
|
@ -569,6 +560,291 @@ impl Rebalancer {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn dust_threshold_for_limit_order(&self, token: &TokenContext) -> anyhow::Result<u64> {
|
||||||
|
let (_, market) = self
|
||||||
|
.mango_client
|
||||||
|
.context
|
||||||
|
.serum3_markets
|
||||||
|
.iter()
|
||||||
|
.find(|(_, context)| {
|
||||||
|
context.base_token_index == token.token_index
|
||||||
|
&& context.quote_token_index == QUOTE_TOKEN_INDEX
|
||||||
|
})
|
||||||
|
.ok_or(anyhow::format_err!(
|
||||||
|
"could not find market for token {}",
|
||||||
|
token.name
|
||||||
|
))?;
|
||||||
|
|
||||||
|
Ok(market.coin_lot_size - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn unwind_using_limit_orders(
|
||||||
|
&self,
|
||||||
|
account: &Box<MangoAccountValue>,
|
||||||
|
token: &TokenContext,
|
||||||
|
token_price: I80F48,
|
||||||
|
dust_threshold: FixedI128<U48>,
|
||||||
|
native_amount: I80F48,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
if native_amount >= 0 && native_amount < dust_threshold {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let (market_index, market) = self
|
||||||
|
.mango_client
|
||||||
|
.context
|
||||||
|
.serum3_markets
|
||||||
|
.iter()
|
||||||
|
.find(|(_, context)| {
|
||||||
|
context.base_token_index == token.token_index
|
||||||
|
&& context.quote_token_index == QUOTE_TOKEN_INDEX
|
||||||
|
})
|
||||||
|
.ok_or(anyhow::format_err!(
|
||||||
|
"could not find market for token {}",
|
||||||
|
token.name
|
||||||
|
))?;
|
||||||
|
|
||||||
|
let side = if native_amount < 0 {
|
||||||
|
Serum3Side::Bid
|
||||||
|
} else {
|
||||||
|
Serum3Side::Ask
|
||||||
|
};
|
||||||
|
|
||||||
|
let distance_from_oracle_price_bp =
|
||||||
|
I80F48::from_num(self.config.limit_order_distance_from_oracle_price_bps)
|
||||||
|
* match side {
|
||||||
|
Serum3Side::Bid => 1,
|
||||||
|
Serum3Side::Ask => -1,
|
||||||
|
};
|
||||||
|
let price_adjustment_factor =
|
||||||
|
(I80F48::from_num(10_000) + distance_from_oracle_price_bp) / I80F48::from_num(10_000);
|
||||||
|
|
||||||
|
let limit_price =
|
||||||
|
(token_price * price_adjustment_factor * I80F48::from_num(market.coin_lot_size))
|
||||||
|
.to_num::<u64>()
|
||||||
|
/ market.pc_lot_size;
|
||||||
|
let mut max_base_lots =
|
||||||
|
(native_amount.abs() / I80F48::from_num(market.coin_lot_size)).to_num::<u64>();
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
side = match side {
|
||||||
|
Serum3Side::Bid => "Buy",
|
||||||
|
Serum3Side::Ask => "Sell",
|
||||||
|
},
|
||||||
|
token = token.name,
|
||||||
|
oracle_price = token_price.to_num::<f64>(),
|
||||||
|
price_adjustment_factor = price_adjustment_factor.to_num::<f64>(),
|
||||||
|
coin_lot_size = market.coin_lot_size,
|
||||||
|
pc_lot_size = market.pc_lot_size,
|
||||||
|
limit_price,
|
||||||
|
native_amount = native_amount.to_num::<f64>(),
|
||||||
|
max_base_lots = max_base_lots,
|
||||||
|
"building order for rebalancing"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Try to buy enough to close the borrow
|
||||||
|
if max_base_lots == 0 && native_amount < 0 {
|
||||||
|
info!(
|
||||||
|
"Buying a whole lot for token {} to cover borrow of {}",
|
||||||
|
token.name, native_amount
|
||||||
|
);
|
||||||
|
max_base_lots = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if max_base_lots == 0 {
|
||||||
|
warn!("Could not rebalance token '{}' (native_amount={}) using limit order, below base lot size", token.name, native_amount);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut account = account.clone();
|
||||||
|
let create_or_replace_account_ixs = self
|
||||||
|
.mango_client
|
||||||
|
.serum3_create_or_replace_account_instruction(&mut account, *market_index, side)
|
||||||
|
.await?;
|
||||||
|
let cancel_ixs =
|
||||||
|
self.mango_client
|
||||||
|
.serum3_cancel_all_orders_instruction(&account, *market_index, 10)?;
|
||||||
|
let place_order_ixs = self
|
||||||
|
.mango_client
|
||||||
|
.serum3_place_order_instruction(
|
||||||
|
&account,
|
||||||
|
*market_index,
|
||||||
|
side,
|
||||||
|
limit_price,
|
||||||
|
max_base_lots,
|
||||||
|
((limit_price * max_base_lots * market.pc_lot_size) as f64 * 1.01) as u64,
|
||||||
|
Serum3SelfTradeBehavior::CancelProvide,
|
||||||
|
Serum3OrderType::Limit,
|
||||||
|
SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() as u64,
|
||||||
|
10,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let seq_check_ixs = self
|
||||||
|
.mango_client
|
||||||
|
.sequence_check_instruction(&self.mango_account_address, &account)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut ixs = PreparedInstructions::new();
|
||||||
|
ixs.append(create_or_replace_account_ixs);
|
||||||
|
ixs.append(cancel_ixs);
|
||||||
|
ixs.append(place_order_ixs);
|
||||||
|
ixs.append(seq_check_ixs);
|
||||||
|
|
||||||
|
let txsig = self
|
||||||
|
.mango_client
|
||||||
|
.send_and_confirm_authority_tx(ixs.to_instructions())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
info!(
|
||||||
|
%txsig,
|
||||||
|
"placed order for {} {} at price = {}",
|
||||||
|
token.native_to_ui(I80F48::from(native_amount)),
|
||||||
|
token.name,
|
||||||
|
limit_price,
|
||||||
|
);
|
||||||
|
if !self.refresh_mango_account_after_tx(txsig).await? {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn close_and_settle_all_openbook_orders(&self) -> anyhow::Result<()> {
|
||||||
|
let account = self.mango_account()?;
|
||||||
|
|
||||||
|
for x in Self::shuffle(account.active_serum3_orders()) {
|
||||||
|
let token = self.mango_client.context.token(x.base_token_index);
|
||||||
|
let quote = self.mango_client.context.token(x.quote_token_index);
|
||||||
|
let market_index = x.market_index;
|
||||||
|
let market = self
|
||||||
|
.mango_client
|
||||||
|
.context
|
||||||
|
.serum3_markets
|
||||||
|
.get(&market_index)
|
||||||
|
.expect("no openbook market found");
|
||||||
|
self.close_and_settle_openbook_orders(&account, token, &market_index, market, quote)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This will only settle funds when there is no more active orders (avoid doing too many settle tx)
|
||||||
|
async fn close_and_settle_openbook_orders(
|
||||||
|
&self,
|
||||||
|
account: &Box<MangoAccountValue>,
|
||||||
|
token: &TokenContext,
|
||||||
|
market_index: &Serum3MarketIndex,
|
||||||
|
market: &Serum3MarketContext,
|
||||||
|
quote: &TokenContext,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let Ok(open_orders) = account.serum3_orders(*market_index).map(|x| x.open_orders)
|
||||||
|
else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
let oo_acc = self.account_fetcher.fetch_raw(&open_orders)?;
|
||||||
|
let oo = serum3_cpi::load_open_orders_bytes(oo_acc.data())?;
|
||||||
|
let oo_slim = OpenOrdersSlim::from_oo(oo);
|
||||||
|
|
||||||
|
if oo_slim.native_base_reserved() != 0 || oo_slim.native_quote_reserved() != 0 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let settle_ixs =
|
||||||
|
self.mango_client
|
||||||
|
.serum3_settle_funds_instruction(market, token, quote, open_orders);
|
||||||
|
|
||||||
|
let close_ixs = self
|
||||||
|
.mango_client
|
||||||
|
.serum3_close_open_orders_instruction(*market_index);
|
||||||
|
|
||||||
|
let mut ixs = PreparedInstructions::new();
|
||||||
|
ixs.append(close_ixs);
|
||||||
|
ixs.append(settle_ixs);
|
||||||
|
|
||||||
|
let txsig = self
|
||||||
|
.mango_client
|
||||||
|
.send_and_confirm_authority_tx(ixs.to_instructions())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
info!(
|
||||||
|
%txsig,
|
||||||
|
"settle spot funds for {}",
|
||||||
|
token.name,
|
||||||
|
);
|
||||||
|
if !self.refresh_mango_account_after_tx(txsig).await? {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn unwind_using_swap(
|
||||||
|
&self,
|
||||||
|
account: &Box<MangoAccountValue>,
|
||||||
|
token: &TokenContext,
|
||||||
|
token_mint: Pubkey,
|
||||||
|
token_price: I80F48,
|
||||||
|
dust_threshold: FixedI128<U48>,
|
||||||
|
fresh_amount: impl Fn() -> anyhow::Result<I80F48>,
|
||||||
|
amount: I80F48,
|
||||||
|
) -> anyhow::Result<I80F48> {
|
||||||
|
if amount < 0 {
|
||||||
|
// Buy
|
||||||
|
let buy_amount = amount.abs().ceil() + (dust_threshold - I80F48::ONE).max(I80F48::ZERO);
|
||||||
|
let input_amount =
|
||||||
|
buy_amount * token_price * I80F48::from_num(self.config.borrow_settle_excess);
|
||||||
|
let (txsig, route) = self
|
||||||
|
.token_swap_buy(&account, token_mint, input_amount.to_num())
|
||||||
|
.await?;
|
||||||
|
let in_token = self
|
||||||
|
.mango_client
|
||||||
|
.context
|
||||||
|
.token_by_mint(&route.input_mint)
|
||||||
|
.unwrap();
|
||||||
|
info!(
|
||||||
|
%txsig,
|
||||||
|
"bought {} {} for {} {}",
|
||||||
|
token.native_to_ui(I80F48::from(route.out_amount)),
|
||||||
|
token.name,
|
||||||
|
in_token.native_to_ui(I80F48::from(route.in_amount)),
|
||||||
|
in_token.name,
|
||||||
|
);
|
||||||
|
if !self.refresh_mango_account_after_tx(txsig).await? {
|
||||||
|
return Ok(amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if amount > dust_threshold {
|
||||||
|
// Sell
|
||||||
|
|
||||||
|
// To avoid creating a borrow when paying flash loan fees, sell only a fraction
|
||||||
|
let input_amount = amount * I80F48::from_num(0.99);
|
||||||
|
let (txsig, route) = self
|
||||||
|
.token_swap_sell(&account, token_mint, input_amount.to_num::<u64>())
|
||||||
|
.await?;
|
||||||
|
let out_token = self
|
||||||
|
.mango_client
|
||||||
|
.context
|
||||||
|
.token_by_mint(&route.output_mint)
|
||||||
|
.unwrap();
|
||||||
|
info!(
|
||||||
|
%txsig,
|
||||||
|
"sold {} {} for {} {}",
|
||||||
|
token.native_to_ui(I80F48::from(route.in_amount)),
|
||||||
|
token.name,
|
||||||
|
out_token.native_to_ui(I80F48::from(route.out_amount)),
|
||||||
|
out_token.name,
|
||||||
|
);
|
||||||
|
if !self.refresh_mango_account_after_tx(txsig).await? {
|
||||||
|
return Ok(amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(fresh_amount()?)
|
||||||
|
}
|
||||||
|
|
||||||
#[instrument(
|
#[instrument(
|
||||||
skip_all,
|
skip_all,
|
||||||
fields(
|
fields(
|
||||||
|
|
|
@ -30,11 +30,11 @@ use mango_v4::state::{
|
||||||
use crate::confirm_transaction::{wait_for_transaction_confirmation, RpcConfirmTransactionConfig};
|
use crate::confirm_transaction::{wait_for_transaction_confirmation, RpcConfirmTransactionConfig};
|
||||||
use crate::context::MangoGroupContext;
|
use crate::context::MangoGroupContext;
|
||||||
use crate::gpa::{fetch_anchor_account, fetch_mango_accounts};
|
use crate::gpa::{fetch_anchor_account, fetch_mango_accounts};
|
||||||
use crate::health_cache;
|
|
||||||
use crate::priority_fees::{FixedPriorityFeeProvider, PriorityFeeProvider};
|
use crate::priority_fees::{FixedPriorityFeeProvider, PriorityFeeProvider};
|
||||||
use crate::util;
|
use crate::util;
|
||||||
use crate::util::PreparedInstructions;
|
use crate::util::PreparedInstructions;
|
||||||
use crate::{account_fetcher::*, swap};
|
use crate::{account_fetcher::*, swap};
|
||||||
|
use crate::{health_cache, Serum3MarketContext, TokenContext};
|
||||||
use solana_address_lookup_table_program::state::AddressLookupTable;
|
use solana_address_lookup_table_program::state::AddressLookupTable;
|
||||||
use solana_client::nonblocking::rpc_client::RpcClient as RpcClientAsync;
|
use solana_client::nonblocking::rpc_client::RpcClient as RpcClientAsync;
|
||||||
use solana_client::rpc_client::SerializableTransaction;
|
use solana_client::rpc_client::SerializableTransaction;
|
||||||
|
@ -1178,6 +1178,17 @@ impl MangoClient {
|
||||||
let account = self.mango_account().await?;
|
let account = self.mango_account().await?;
|
||||||
let open_orders = account.serum3_orders(market_index).unwrap().open_orders;
|
let open_orders = account.serum3_orders(market_index).unwrap().open_orders;
|
||||||
|
|
||||||
|
let ix = self.serum3_settle_funds_instruction(s3, base, quote, open_orders);
|
||||||
|
self.send_and_confirm_authority_tx(ix.to_instructions()).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn serum3_settle_funds_instruction(
|
||||||
|
&self,
|
||||||
|
s3: &Serum3MarketContext,
|
||||||
|
base: &TokenContext,
|
||||||
|
quote: &TokenContext,
|
||||||
|
open_orders: Pubkey,
|
||||||
|
) -> PreparedInstructions {
|
||||||
let ix = Instruction {
|
let ix = Instruction {
|
||||||
program_id: mango_v4::id(),
|
program_id: mango_v4::id(),
|
||||||
accounts: anchor_lang::ToAccountMetas::to_account_metas(
|
accounts: anchor_lang::ToAccountMetas::to_account_metas(
|
||||||
|
@ -1210,7 +1221,11 @@ impl MangoClient {
|
||||||
fees_to_dao: true,
|
fees_to_dao: true,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
self.send_and_confirm_authority_tx(vec![ix]).await
|
|
||||||
|
PreparedInstructions::from_single(
|
||||||
|
ix,
|
||||||
|
self.context.compute_estimates.cu_per_mango_instruction,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn serum3_cancel_all_orders_instruction(
|
pub fn serum3_cancel_all_orders_instruction(
|
||||||
|
|
Loading…
Reference in New Issue