From 1d653d42a94ba87253876182807bceda589797c9 Mon Sep 17 00:00:00 2001 From: Serge Farny Date: Thu, 4 Apr 2024 12:18:08 +0200 Subject: [PATCH 1/5] liquidator: rebalance with limit order (draft) --- bin/liquidator/src/cli_args.rs | 3 + bin/liquidator/src/main.rs | 8 +- bin/liquidator/src/rebalance.rs | 368 +++++++++++++++++++++++++++----- lib/client/src/client.rs | 17 +- 4 files changed, 339 insertions(+), 57 deletions(-) diff --git a/bin/liquidator/src/cli_args.rs b/bin/liquidator/src/cli_args.rs index dc122c604..bd93fc58f 100644 --- a/bin/liquidator/src/cli_args.rs +++ b/bin/liquidator/src/cli_args.rs @@ -136,6 +136,9 @@ pub struct Cli { #[clap(long, env, default_value = "30")] pub(crate) rebalance_refresh_timeout_secs: u64, + #[clap(long, env, value_enum, default_value = "false")] + pub(crate) rebalance_using_limit_order: BoolArg, + /// if taking tcs orders is enabled /// /// typically only disabled for tests where swaps are unavailable diff --git a/bin/liquidator/src/main.rs b/bin/liquidator/src/main.rs index 1e1a6032e..f6f53784e 100644 --- a/bin/liquidator/src/main.rs +++ b/bin/liquidator/src/main.rs @@ -248,6 +248,11 @@ async fn main() -> anyhow::Result<()> { let (rebalance_trigger_sender, rebalance_trigger_receiver) = async_channel::bounded::<()>(1); let (tx_tcs_trigger_sender, tx_tcs_trigger_receiver) = async_channel::unbounded::<()>(); let (tx_liq_trigger_sender, tx_liq_trigger_receiver) = async_channel::unbounded::<()>(); + + if cli.rebalance_using_limit_order == BoolArg::True && !signer_is_owner { + warn!("Can't withdraw dust to liqor account if delegate and using limit orders for rebalancing"); + } + let rebalance_config = rebalance::Config { enabled: cli.rebalance == BoolArg::True, slippage_bps: cli.rebalance_slippage_bps, @@ -263,8 +268,9 @@ async fn main() -> anyhow::Result<()> { .rebalance_alternate_sanctum_route_tokens .clone() .unwrap_or_default(), - allow_withdraws: signer_is_owner, + allow_withdraws: cli.rebalance_using_limit_order == BoolArg::False || signer_is_owner, use_sanctum: cli.sanctum_enabled == BoolArg::True, + use_limit_order: cli.rebalance_using_limit_order == BoolArg::True, }; rebalance_config.validate(&mango_client.context); diff --git a/bin/liquidator/src/rebalance.rs b/bin/liquidator/src/rebalance.rs index e9e2860e6..ef0fbdce4 100644 --- a/bin/liquidator/src/rebalance.rs +++ b/bin/liquidator/src/rebalance.rs @@ -2,21 +2,27 @@ use itertools::Itertools; use mango_v4::accounts_zerocopy::KeyedAccountSharedData; use mango_v4::state::{ Bank, BookSide, MangoAccountValue, OracleAccountInfos, PerpMarket, PerpPosition, - PlaceOrderType, Side, TokenIndex, QUOTE_TOKEN_INDEX, + PlaceOrderType, Serum3MarketIndex, Side, TokenIndex, QUOTE_TOKEN_INDEX, }; use mango_v4_client::{ - chain_data, perp_pnl, swap, MangoClient, MangoGroupContext, PerpMarketContext, TokenContext, - TransactionBuilder, TransactionSize, + chain_data, perp_pnl, swap, MangoClient, MangoGroupContext, PerpMarketContext, + PreparedInstructions, Serum3MarketContext, TokenContext, TransactionBuilder, TransactionSize, }; use solana_client::nonblocking::rpc_client::RpcClient; 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 std::collections::{HashMap, HashSet}; use std::future::Future; use std::sync::Arc; -use std::time::Duration; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; use tracing::*; #[derive(Clone)] @@ -35,6 +41,7 @@ pub struct Config { pub alternate_sanctum_route_tokens: Vec, pub allow_withdraws: bool, pub use_sanctum: bool, + pub use_limit_order: bool, } impl Config { @@ -440,6 +447,7 @@ impl Rebalancer { } async fn rebalance_tokens(&self) -> anyhow::Result<()> { + self.settle_and_close_all_openbook_orders().await?; let account = self.mango_account()?; // TODO: configurable? @@ -466,7 +474,11 @@ impl Rebalancer { // 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 // again withdraw the dust at the end. - let dust_threshold = I80F48::from(2) / token_price; + let dust_threshold = if self.config.use_limit_order { + I80F48::from(self.dust_threshold_for_limit_order(token).await?) + } else { + I80F48::from(2) / token_price + }; // Some rebalancing can actually change non-USDC positions (rebalancing to SOL) // So re-fetch the current token position amount @@ -481,56 +493,28 @@ impl Rebalancer { let mut amount = fresh_amount()?; trace!(token_index, %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 { - // Sell - let (txsig, route) = self - .token_swap_sell(&account, token_mint, amount.to_num::()) + if self.config.use_limit_order { + self.unwind_using_limit_orders( + &account, + token, + token_price, + dust_threshold, + amount, + ) + .await?; + } else { + amount = self + .unwind_using_swap( + &account, + token, + token_mint, + token_price, + dust_threshold, + fresh_amount, + amount, + ) .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 @@ -565,6 +549,284 @@ impl Rebalancer { Ok(()) } + async fn dust_threshold_for_limit_order(&self, token: &TokenContext) -> anyhow::Result { + 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, + token: &TokenContext, + token_price: I80F48, + dust_threshold: FixedI128, + 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 limit_price = (token_price * I80F48::from_num(market.coin_lot_size)).to_num::() + / market.pc_lot_size; + let mut max_base_lots = + (native_amount.abs() / I80F48::from_num(market.coin_lot_size)).to_num::(); + + debug!( + token = token.name, + oracle_price = token_price.to_num::(), + coin_lot_size = market.coin_lot_size, + pc_lot_size = market.pc_lot_size, + limit_price = limit_price, + native_amount = native_amount.to_num::(), + 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_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::PostOnly, + SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() as u64, + 10, + ) + .await?; + + // TODO FAS Uncomment this after v0.24.0 is released + // 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_ixs); + ixs.append(cancel_ixs); + ixs.append(place_order_ixs); + + // TODO FAS Uncomment this after v0.24.0 is released + // ixs.append(seq_check_ixs); + + let txsig = self + .mango_client + .send_and_confirm_owner_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 settle_and_close_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.settle_and_close_openbook_orders(&account, token, &market_index, market, quote) + .await?; + } + Ok(()) + } + + async fn settle_and_close_openbook_orders( + &self, + account: &Box, + token: &TokenContext, + market_index: &Serum3MarketIndex, + market: &Serum3MarketContext, + quote: &TokenContext, + ) -> anyhow::Result<()> { + let open_orders_opt = account + .serum3_orders(*market_index) + .map(|x| x.open_orders) + .ok(); + + if open_orders_opt.is_none() { + return Ok(()); + } + + let open_orders = open_orders_opt.unwrap(); + 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 = PreparedInstructions::from_single( + self.mango_client + .serum3_settle_funds_instruction(market, token, quote, open_orders), + self.mango_client + .context + .compute_estimates + .cu_per_serum3_order_cancel, // TODO FAS + ); + + let close_ixs = self + .mango_client + .serum3_close_open_orders_instruction(*market_index); + + let mut ixs = PreparedInstructions::new(); + ixs.append(settle_ixs); + ixs.append(close_ixs); + + let txsig = self + .mango_client + .send_and_confirm_owner_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, + token: &TokenContext, + token_mint: Pubkey, + token_price: I80F48, + dust_threshold: FixedI128, + fresh_amount: impl Fn() -> anyhow::Result, + amount: I80F48, + ) -> anyhow::Result { + 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 + let (txsig, route) = self + .token_swap_sell(&account, token_mint, amount.to_num::()) + .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( skip_all, fields( diff --git a/lib/client/src/client.rs b/lib/client/src/client.rs index 7fef6259e..38fed62b6 100644 --- a/lib/client/src/client.rs +++ b/lib/client/src/client.rs @@ -30,11 +30,11 @@ use mango_v4::state::{ use crate::confirm_transaction::{wait_for_transaction_confirmation, RpcConfirmTransactionConfig}; use crate::context::MangoGroupContext; use crate::gpa::{fetch_anchor_account, fetch_mango_accounts}; -use crate::health_cache; use crate::priority_fees::{FixedPriorityFeeProvider, PriorityFeeProvider}; -use crate::util; use crate::util::PreparedInstructions; use crate::{account_fetcher::*, swap}; +use crate::{health_cache, Serum3MarketContext, TokenContext}; +use crate::util; use solana_address_lookup_table_program::state::AddressLookupTable; use solana_client::nonblocking::rpc_client::RpcClient as RpcClientAsync; use solana_client::rpc_client::SerializableTransaction; @@ -1171,6 +1171,17 @@ impl MangoClient { let account = self.mango_account().await?; 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_owner_tx(vec![ix]).await + } + + pub fn serum3_settle_funds_instruction( + &self, + s3: &Serum3MarketContext, + base: &TokenContext, + quote: &TokenContext, + open_orders: Pubkey, + ) -> Instruction { let ix = Instruction { program_id: mango_v4::id(), accounts: anchor_lang::ToAccountMetas::to_account_metas( @@ -1203,7 +1214,7 @@ impl MangoClient { fees_to_dao: true, }), }; - self.send_and_confirm_owner_tx(vec![ix]).await + ix } pub fn serum3_cancel_all_orders_instruction( From d5de562f57695915a148988ce1fd604b6c053796 Mon Sep 17 00:00:00 2001 From: Serge Farny Date: Tue, 9 Apr 2024 13:59:04 +0200 Subject: [PATCH 2/5] liquidator: rebalacing with limit order - add a spread --- bin/liquidator/src/cli_args.rs | 4 ++++ bin/liquidator/src/main.rs | 2 ++ bin/liquidator/src/rebalance.rs | 28 +++++++++++++++++++++++++--- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/bin/liquidator/src/cli_args.rs b/bin/liquidator/src/cli_args.rs index bd93fc58f..c6c6b0a28 100644 --- a/bin/liquidator/src/cli_args.rs +++ b/bin/liquidator/src/cli_args.rs @@ -139,6 +139,10 @@ pub struct Cli { #[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 /// /// typically only disabled for tests where swaps are unavailable diff --git a/bin/liquidator/src/main.rs b/bin/liquidator/src/main.rs index f6f53784e..846d8c9b2 100644 --- a/bin/liquidator/src/main.rs +++ b/bin/liquidator/src/main.rs @@ -271,6 +271,8 @@ async fn main() -> anyhow::Result<()> { allow_withdraws: cli.rebalance_using_limit_order == BoolArg::False || signer_is_owner, use_sanctum: cli.sanctum_enabled == BoolArg::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); diff --git a/bin/liquidator/src/rebalance.rs b/bin/liquidator/src/rebalance.rs index ef0fbdce4..09074f361 100644 --- a/bin/liquidator/src/rebalance.rs +++ b/bin/liquidator/src/rebalance.rs @@ -30,6 +30,8 @@ pub struct Config { pub enabled: bool, /// Maximum slippage allowed in Jupiter 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. /// 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. @@ -599,14 +601,30 @@ impl Rebalancer { Serum3Side::Ask }; - let limit_price = (token_price * I80F48::from_num(market.coin_lot_size)).to_num::() - / market.pc_lot_size; + 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::() + / market.pc_lot_size; let mut max_base_lots = (native_amount.abs() / I80F48::from_num(market.coin_lot_size)).to_num::(); debug!( + side = match side { + Serum3Side::Bid => "Buy", + Serum3Side::Ask => "Sell", + }, token = token.name, oracle_price = token_price.to_num::(), + price_adjustment_factor = price_adjustment_factor.to_num::(), coin_lot_size = market.coin_lot_size, pc_lot_size = market.pc_lot_size, limit_price = limit_price, @@ -647,7 +665,11 @@ impl Rebalancer { max_base_lots, ((limit_price * max_base_lots * market.pc_lot_size) as f64 * 1.01) as u64, Serum3SelfTradeBehavior::CancelProvide, - Serum3OrderType::PostOnly, + if self.config.limit_order_distance_from_oracle_price_bps == 0 { + Serum3OrderType::PostOnly + } else { + Serum3OrderType::Limit + }, SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() as u64, 10, ) From f3e0dda5e0928b59a2d8e8fce76796a809145374 Mon Sep 17 00:00:00 2001 From: Serge Farny Date: Tue, 9 Apr 2024 16:06:05 +0200 Subject: [PATCH 3/5] liquidator: enable sequence check for rebalancing using limit order --- bin/liquidator/src/rebalance.rs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/bin/liquidator/src/rebalance.rs b/bin/liquidator/src/rebalance.rs index 09074f361..62862f6d1 100644 --- a/bin/liquidator/src/rebalance.rs +++ b/bin/liquidator/src/rebalance.rs @@ -675,19 +675,16 @@ impl Rebalancer { ) .await?; - // TODO FAS Uncomment this after v0.24.0 is released - // let seq_check_ixs = self - // .mango_client - // .sequence_check_instruction(&self.mango_account_address, &account) - // .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_ixs); ixs.append(cancel_ixs); ixs.append(place_order_ixs); - - // TODO FAS Uncomment this after v0.24.0 is released - // ixs.append(seq_check_ixs); + ixs.append(seq_check_ixs); let txsig = self .mango_client From dcf6311e545cf7d96a957bf707f622ace30f70f4 Mon Sep 17 00:00:00 2001 From: Serge Farny Date: Wed, 10 Apr 2024 14:31:41 +0200 Subject: [PATCH 4/5] liquidator: fix after review --- bin/liquidator/src/rebalance.rs | 60 +++++++++++++++------------------ lib/client/src/client.rs | 12 ++++--- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/bin/liquidator/src/rebalance.rs b/bin/liquidator/src/rebalance.rs index 62862f6d1..36633c50b 100644 --- a/bin/liquidator/src/rebalance.rs +++ b/bin/liquidator/src/rebalance.rs @@ -449,7 +449,7 @@ impl Rebalancer { } async fn rebalance_tokens(&self) -> anyhow::Result<()> { - self.settle_and_close_all_openbook_orders().await?; + self.close_and_settle_all_openbook_orders().await?; let account = self.mango_account()?; // TODO: configurable? @@ -476,10 +476,19 @@ impl Rebalancer { // 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 // again withdraw the dust at the end. - let dust_threshold = if self.config.use_limit_order { - I80F48::from(self.dust_threshold_for_limit_order(token).await?) + let dust_threshold_res = if self.config.use_limit_order { + self.dust_threshold_for_limit_order(token) + .await + .map(|x| I80F48::from(x)) } else { - I80F48::from(2) / token_price + 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); + return Ok(()); }; // Some rebalancing can actually change non-USDC positions (rebalancing to SOL) @@ -494,7 +503,7 @@ impl Rebalancer { }; let mut amount = fresh_amount()?; - trace!(token_index, %amount, %dust_threshold, "checking"); + trace!(token_index, token.name, %amount, %dust_threshold, "checking"); if self.config.use_limit_order { self.unwind_using_limit_orders( @@ -627,7 +636,7 @@ impl Rebalancer { price_adjustment_factor = price_adjustment_factor.to_num::(), coin_lot_size = market.coin_lot_size, pc_lot_size = market.pc_lot_size, - limit_price = limit_price, + limit_price, native_amount = native_amount.to_num::(), max_base_lots = max_base_lots, "building order for rebalancing" @@ -648,7 +657,7 @@ impl Rebalancer { } let mut account = account.clone(); - let create_or_replace_ixs = self + let create_or_replace_account_ixs = self .mango_client .serum3_create_or_replace_account_instruction(&mut account, *market_index, side) .await?; @@ -665,11 +674,7 @@ impl Rebalancer { max_base_lots, ((limit_price * max_base_lots * market.pc_lot_size) as f64 * 1.01) as u64, Serum3SelfTradeBehavior::CancelProvide, - if self.config.limit_order_distance_from_oracle_price_bps == 0 { - Serum3OrderType::PostOnly - } else { - Serum3OrderType::Limit - }, + Serum3OrderType::Limit, SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() as u64, 10, ) @@ -681,7 +686,7 @@ impl Rebalancer { .await?; let mut ixs = PreparedInstructions::new(); - ixs.append(create_or_replace_ixs); + ixs.append(create_or_replace_account_ixs); ixs.append(cancel_ixs); ixs.append(place_order_ixs); ixs.append(seq_check_ixs); @@ -705,7 +710,7 @@ impl Rebalancer { Ok(()) } - async fn settle_and_close_all_openbook_orders(&self) -> anyhow::Result<()> { + 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()) { @@ -718,13 +723,14 @@ impl Rebalancer { .serum3_markets .get(&market_index) .expect("no openbook market found"); - self.settle_and_close_openbook_orders(&account, token, &market_index, market, quote) + self.close_and_settle_openbook_orders(&account, token, &market_index, market, quote) .await?; } Ok(()) } - async fn settle_and_close_openbook_orders( + /// 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, token: &TokenContext, @@ -732,16 +738,11 @@ impl Rebalancer { market: &Serum3MarketContext, quote: &TokenContext, ) -> anyhow::Result<()> { - let open_orders_opt = account - .serum3_orders(*market_index) - .map(|x| x.open_orders) - .ok(); - - if open_orders_opt.is_none() { + let Ok(open_orders) = account.serum3_orders(*market_index).map(|x| x.open_orders) + else { return Ok(()); - } + }; - let open_orders = open_orders_opt.unwrap(); 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); @@ -750,22 +751,17 @@ impl Rebalancer { return Ok(()); } - let settle_ixs = PreparedInstructions::from_single( - self.mango_client - .serum3_settle_funds_instruction(market, token, quote, open_orders), + let settle_ixs = self.mango_client - .context - .compute_estimates - .cu_per_serum3_order_cancel, // TODO FAS - ); + .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(settle_ixs); ixs.append(close_ixs); + ixs.append(settle_ixs); let txsig = self .mango_client diff --git a/lib/client/src/client.rs b/lib/client/src/client.rs index 38fed62b6..598b53e20 100644 --- a/lib/client/src/client.rs +++ b/lib/client/src/client.rs @@ -31,10 +31,10 @@ use crate::confirm_transaction::{wait_for_transaction_confirmation, RpcConfirmTr use crate::context::MangoGroupContext; use crate::gpa::{fetch_anchor_account, fetch_mango_accounts}; use crate::priority_fees::{FixedPriorityFeeProvider, PriorityFeeProvider}; +use crate::util; use crate::util::PreparedInstructions; use crate::{account_fetcher::*, swap}; use crate::{health_cache, Serum3MarketContext, TokenContext}; -use crate::util; use solana_address_lookup_table_program::state::AddressLookupTable; use solana_client::nonblocking::rpc_client::RpcClient as RpcClientAsync; use solana_client::rpc_client::SerializableTransaction; @@ -1172,7 +1172,7 @@ impl MangoClient { 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_owner_tx(vec![ix]).await + self.send_and_confirm_owner_tx(ix.to_instructions()).await } pub fn serum3_settle_funds_instruction( @@ -1181,7 +1181,7 @@ impl MangoClient { base: &TokenContext, quote: &TokenContext, open_orders: Pubkey, - ) -> Instruction { + ) -> PreparedInstructions { let ix = Instruction { program_id: mango_v4::id(), accounts: anchor_lang::ToAccountMetas::to_account_metas( @@ -1214,7 +1214,11 @@ impl MangoClient { fees_to_dao: true, }), }; - ix + + PreparedInstructions::from_single( + ix, + self.context.compute_estimates.cu_per_mango_instruction, + ) } pub fn serum3_cancel_all_orders_instruction( From 3e8cb44b87a99dcf06d224e4c57a92fd05eb7d82 Mon Sep 17 00:00:00 2001 From: Serge Farny Date: Wed, 10 Apr 2024 16:37:28 +0200 Subject: [PATCH 5/5] liquidator: continue on missing market instead of exiting rebalancing --- bin/liquidator/src/rebalance.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/liquidator/src/rebalance.rs b/bin/liquidator/src/rebalance.rs index 36633c50b..621106582 100644 --- a/bin/liquidator/src/rebalance.rs +++ b/bin/liquidator/src/rebalance.rs @@ -488,7 +488,7 @@ impl Rebalancer { else { let e = dust_threshold_res.unwrap_err(); error!("Cannot rebalance token {}, probably missing USDC market ? - error: {}", token.name, e); - return Ok(()); + continue; }; // Some rebalancing can actually change non-USDC positions (rebalancing to SOL)