2023-02-20 03:09:17 -08:00
|
|
|
use std::collections::HashMap;
|
|
|
|
use std::sync::Arc;
|
|
|
|
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
|
|
|
|
|
|
|
use mango_v4::accounts_zerocopy::KeyedAccountSharedData;
|
|
|
|
use mango_v4::health::HealthType;
|
2024-01-04 09:29:54 -08:00
|
|
|
use mango_v4::state::{OracleAccountInfos, PerpMarket, PerpMarketIndex};
|
2023-02-20 05:08:38 -08:00
|
|
|
use mango_v4_client::{
|
2023-11-03 03:20:37 -07:00
|
|
|
chain_data, health_cache, prettify_solana_client_error, MangoClient, PreparedInstructions,
|
|
|
|
TransactionBuilder,
|
2023-02-20 05:08:38 -08:00
|
|
|
};
|
2023-02-20 03:09:17 -08:00
|
|
|
use solana_sdk::address_lookup_table_account::AddressLookupTableAccount;
|
|
|
|
use solana_sdk::commitment_config::CommitmentConfig;
|
|
|
|
use solana_sdk::signature::Signature;
|
|
|
|
|
|
|
|
use solana_sdk::signer::Signer;
|
|
|
|
use solana_sdk::transaction::VersionedTransaction;
|
2023-07-11 23:38:38 -07:00
|
|
|
use tracing::*;
|
2023-02-20 03:09:17 -08:00
|
|
|
use {anyhow::Context, fixed::types::I80F48, solana_sdk::pubkey::Pubkey};
|
|
|
|
|
|
|
|
pub struct Config {
|
|
|
|
/// Amount of time to wait before reusing a positive-pnl account
|
|
|
|
pub settle_cooldown: Duration,
|
|
|
|
}
|
|
|
|
|
|
|
|
fn perp_markets_and_prices(
|
|
|
|
mango_client: &MangoClient,
|
|
|
|
account_fetcher: &chain_data::AccountFetcher,
|
2023-11-07 03:01:02 -08:00
|
|
|
) -> HashMap<PerpMarketIndex, (PerpMarket, I80F48, I80F48)> {
|
2023-02-20 03:09:17 -08:00
|
|
|
mango_client
|
|
|
|
.context
|
|
|
|
.perp_markets
|
|
|
|
.iter()
|
|
|
|
.map(|(market_index, perp)| {
|
|
|
|
let perp_market = account_fetcher.fetch::<PerpMarket>(&perp.address)?;
|
2023-11-07 03:01:02 -08:00
|
|
|
|
2024-01-04 09:29:54 -08:00
|
|
|
let oracle = account_fetcher.fetch_raw(&perp_market.oracle)?;
|
|
|
|
let oracle_acc = &KeyedAccountSharedData::new(perp_market.oracle, oracle);
|
|
|
|
let oracle_price =
|
|
|
|
perp_market.oracle_price(&OracleAccountInfos::from_reader(oracle_acc), None)?;
|
2023-02-20 03:09:17 -08:00
|
|
|
|
2023-11-07 03:01:02 -08:00
|
|
|
let settle_token = mango_client.context.token(perp_market.settle_token_index);
|
|
|
|
let settle_token_price =
|
2023-12-05 04:22:24 -08:00
|
|
|
account_fetcher.fetch_bank_price(&settle_token.first_bank())?;
|
2023-11-07 03:01:02 -08:00
|
|
|
|
|
|
|
Ok((
|
|
|
|
*market_index,
|
|
|
|
(perp_market, oracle_price, settle_token_price),
|
|
|
|
))
|
|
|
|
})
|
|
|
|
.filter_map(|v: anyhow::Result<_>| match v {
|
|
|
|
Ok(v) => Some(v),
|
|
|
|
Err(err) => {
|
|
|
|
error!("error while retriving perp market and price: {:?}", err);
|
|
|
|
None
|
|
|
|
}
|
2023-02-20 03:09:17 -08:00
|
|
|
})
|
|
|
|
.collect()
|
|
|
|
}
|
|
|
|
|
|
|
|
pub struct SettlementState {
|
|
|
|
pub mango_client: Arc<MangoClient>,
|
|
|
|
pub account_fetcher: Arc<chain_data::AccountFetcher>,
|
|
|
|
pub config: Config,
|
|
|
|
|
|
|
|
pub recently_settled: HashMap<Pubkey, Instant>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl SettlementState {
|
|
|
|
pub async fn settle(&mut self, mut accounts: Vec<Pubkey>) -> anyhow::Result<()> {
|
|
|
|
{
|
|
|
|
use rand::seq::SliceRandom;
|
|
|
|
let mut rng = rand::thread_rng();
|
|
|
|
accounts.shuffle(&mut rng);
|
|
|
|
}
|
|
|
|
|
|
|
|
self.expire_recently_settled();
|
|
|
|
|
|
|
|
self.run_settles(&accounts).await
|
|
|
|
}
|
|
|
|
|
|
|
|
fn expire_recently_settled(&mut self) {
|
|
|
|
let now = Instant::now();
|
|
|
|
self.recently_settled.retain(|_, last_settle| {
|
|
|
|
now.duration_since(*last_settle) < self.config.settle_cooldown
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-11-08 00:51:36 -08:00
|
|
|
async fn run_settles(&mut self, accounts: &[Pubkey]) -> anyhow::Result<()> {
|
|
|
|
let now_ts: u64 = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
|
2023-02-20 03:09:17 -08:00
|
|
|
|
|
|
|
let mango_client = &*self.mango_client;
|
|
|
|
let account_fetcher = &*self.account_fetcher;
|
|
|
|
let perp_market_info = perp_markets_and_prices(mango_client, account_fetcher);
|
|
|
|
|
|
|
|
// Get settleable pnl for all accounts and markets
|
|
|
|
let mut all_positive_settleable =
|
|
|
|
HashMap::<PerpMarketIndex, Vec<(Pubkey, I80F48, I80F48)>>::new();
|
|
|
|
let mut all_negative_settleable =
|
|
|
|
HashMap::<PerpMarketIndex, priority_queue::PriorityQueue<Pubkey, I80F48>>::new();
|
|
|
|
for account_key in accounts.iter() {
|
|
|
|
let mut account = account_fetcher.fetch_mango_account(account_key)?;
|
|
|
|
if account.fixed.group != mango_client.group() {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if self.recently_settled.contains_key(account_key) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
let perp_indexes = account
|
|
|
|
.active_perp_positions()
|
|
|
|
.map(|pp| pp.market_index)
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
if perp_indexes.is_empty() {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
let health_cache = health_cache::new(&mango_client.context, account_fetcher, &account)
|
|
|
|
.await
|
|
|
|
.context("creating health cache")?;
|
|
|
|
let liq_end_health = health_cache.health(HealthType::LiquidationEnd);
|
|
|
|
|
|
|
|
for perp_market_index in perp_indexes {
|
2023-11-07 03:01:02 -08:00
|
|
|
let (perp_market, perp_price, settle_token_price) =
|
|
|
|
match perp_market_info.get(&perp_market_index) {
|
|
|
|
Some(v) => v,
|
|
|
|
None => continue, // skip accounts with perp positions where we couldn't get the price and market
|
|
|
|
};
|
2023-05-17 06:50:05 -07:00
|
|
|
let perp_max_settle =
|
|
|
|
health_cache.perp_max_settle(perp_market.settle_token_index)?;
|
2023-02-20 03:09:17 -08:00
|
|
|
|
|
|
|
let perp_position = account.perp_position_mut(perp_market_index).unwrap();
|
|
|
|
perp_position.settle_funding(perp_market);
|
|
|
|
perp_position.update_settle_limit(perp_market, now_ts);
|
|
|
|
|
2023-11-07 03:01:02 -08:00
|
|
|
let unsettled = perp_position.unsettled_pnl(perp_market, *perp_price)?;
|
2023-02-20 03:09:17 -08:00
|
|
|
let limited = perp_position.apply_pnl_settle_limit(perp_market, unsettled);
|
|
|
|
let settleable = if limited >= 0 {
|
|
|
|
limited
|
|
|
|
} else {
|
2023-05-17 06:50:05 -07:00
|
|
|
limited.max(-perp_max_settle).min(I80F48::ZERO)
|
2023-02-20 03:09:17 -08:00
|
|
|
};
|
|
|
|
|
|
|
|
if settleable > 0 {
|
|
|
|
// compute maint health only when needed
|
|
|
|
let maint_health = if liq_end_health < 0 {
|
|
|
|
health_cache.health(HealthType::Maint)
|
|
|
|
} else {
|
|
|
|
liq_end_health
|
|
|
|
};
|
|
|
|
|
2023-11-07 03:01:02 -08:00
|
|
|
let pnl_value = unsettled * settle_token_price;
|
|
|
|
let position_value =
|
|
|
|
perp_position.base_position_native(perp_market) * perp_price;
|
2023-02-20 03:09:17 -08:00
|
|
|
let fee = perp_market
|
2023-11-07 03:01:02 -08:00
|
|
|
.compute_settle_fee(
|
|
|
|
settleable,
|
|
|
|
pnl_value,
|
|
|
|
position_value,
|
|
|
|
liq_end_health,
|
|
|
|
maint_health,
|
|
|
|
)
|
2023-02-20 03:09:17 -08:00
|
|
|
.unwrap();
|
2023-11-07 03:01:02 -08:00
|
|
|
|
|
|
|
// Assume that settle_fee_flat is near the tx fee, and if we can't possibly
|
|
|
|
// make up for the tx fee even with multiple settle ix in one tx, skip.
|
|
|
|
if fee <= perp_market.settle_fee_flat / 10.0 {
|
2023-02-20 03:09:17 -08:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
all_positive_settleable
|
|
|
|
.entry(perp_market_index)
|
|
|
|
.or_default()
|
|
|
|
.push((*account_key, settleable, fee));
|
|
|
|
} else if settleable < 0 {
|
|
|
|
all_negative_settleable
|
|
|
|
.entry(perp_market_index)
|
|
|
|
.or_default()
|
|
|
|
.push(*account_key, -settleable);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let address_lookup_tables = mango_client.mango_address_lookup_tables().await?;
|
|
|
|
|
|
|
|
for (perp_market_index, mut positive_settleable) in all_positive_settleable {
|
2023-11-07 03:01:02 -08:00
|
|
|
let (perp_market, _, _) = perp_market_info.get(&perp_market_index).unwrap();
|
2023-02-20 03:09:17 -08:00
|
|
|
let negative_settleable = match all_negative_settleable.get_mut(&perp_market_index) {
|
|
|
|
None => continue,
|
|
|
|
Some(v) => v,
|
|
|
|
};
|
|
|
|
// sort by fee, descending
|
|
|
|
positive_settleable.sort_by_key(|v| -v.2);
|
|
|
|
|
|
|
|
let mut batch_processor = SettleBatchProcessor {
|
|
|
|
mango_client,
|
|
|
|
account_fetcher,
|
|
|
|
perp_market_index,
|
2023-11-03 03:20:37 -07:00
|
|
|
instructions: PreparedInstructions::new(),
|
2023-02-20 03:09:17 -08:00
|
|
|
max_batch_size: 8, // the 1.4M max CU limit if we assume settle ix can be up to around 150k
|
|
|
|
blockhash: mango_client
|
|
|
|
.client
|
|
|
|
.rpc_async()
|
|
|
|
.get_latest_blockhash_with_commitment(CommitmentConfig::confirmed())
|
|
|
|
.await?
|
|
|
|
.0,
|
|
|
|
address_lookup_tables: &address_lookup_tables,
|
|
|
|
};
|
|
|
|
|
|
|
|
for (account_a, mut settleable_a, fee) in positive_settleable {
|
|
|
|
// Settle account_a as much as we can while still getting a fee for it:
|
|
|
|
// Could be that all counterparties are small and we can settle multiple times
|
|
|
|
// until account_a is exhausted.
|
|
|
|
let mut settled_a_once = false;
|
|
|
|
while settleable_a > perp_market.settle_fee_amount_threshold
|
|
|
|
|| (settleable_a > 0 && fee != perp_market.settle_fee_flat)
|
|
|
|
{
|
|
|
|
// find the best remaining counterparty
|
|
|
|
let (&account_b, &settleable_b) = match negative_settleable.peek() {
|
|
|
|
None => break,
|
|
|
|
Some(v) => v,
|
|
|
|
};
|
|
|
|
|
|
|
|
let settleable = settleable_a.min(settleable_b);
|
|
|
|
if settleable <= 0
|
|
|
|
|| (settleable < perp_market.settle_fee_amount_threshold
|
|
|
|
&& fee == perp_market.settle_fee_flat)
|
|
|
|
{
|
|
|
|
// no more interesting pairs that would produce fees
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
batch_processor
|
|
|
|
.add_and_maybe_send(account_a, account_b)
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
settled_a_once = true;
|
|
|
|
settleable_a -= settleable;
|
|
|
|
negative_settleable.change_priority(&account_b, settleable_b - settleable);
|
|
|
|
}
|
|
|
|
if settled_a_once {
|
|
|
|
let now = Instant::now();
|
|
|
|
self.recently_settled.insert(account_a, now);
|
|
|
|
} else {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// send final batch, if any
|
|
|
|
batch_processor.send().await?;
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
struct SettleBatchProcessor<'a> {
|
|
|
|
mango_client: &'a MangoClient,
|
|
|
|
account_fetcher: &'a chain_data::AccountFetcher,
|
|
|
|
perp_market_index: PerpMarketIndex,
|
2023-11-03 03:20:37 -07:00
|
|
|
instructions: PreparedInstructions,
|
2023-02-20 03:09:17 -08:00
|
|
|
max_batch_size: usize,
|
|
|
|
blockhash: solana_sdk::hash::Hash,
|
|
|
|
address_lookup_tables: &'a Vec<AddressLookupTableAccount>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a> SettleBatchProcessor<'a> {
|
|
|
|
fn transaction(&self) -> anyhow::Result<VersionedTransaction> {
|
|
|
|
let client = &self.mango_client.client;
|
2023-12-05 04:23:11 -08:00
|
|
|
let fee_payer = client.fee_payer();
|
2023-02-20 03:09:17 -08:00
|
|
|
|
|
|
|
TransactionBuilder {
|
2023-11-03 03:20:37 -07:00
|
|
|
instructions: self.instructions.clone().to_instructions(),
|
2023-02-20 03:09:17 -08:00
|
|
|
address_lookup_tables: self.address_lookup_tables.clone(),
|
|
|
|
payer: fee_payer.pubkey(),
|
|
|
|
signers: vec![fee_payer],
|
|
|
|
config: client.transaction_builder_config,
|
|
|
|
}
|
|
|
|
.transaction_with_blockhash(self.blockhash)
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn send(&mut self) -> anyhow::Result<Option<Signature>> {
|
|
|
|
if self.instructions.is_empty() {
|
|
|
|
return Ok(None);
|
|
|
|
}
|
|
|
|
|
|
|
|
let tx = self.transaction()?;
|
|
|
|
self.instructions.clear();
|
|
|
|
|
|
|
|
let send_result = self
|
|
|
|
.mango_client
|
|
|
|
.client
|
|
|
|
.rpc_async()
|
|
|
|
.send_transaction_with_config(&tx, self.mango_client.client.rpc_send_transaction_config)
|
|
|
|
.await
|
2023-11-08 00:51:36 -08:00
|
|
|
.map_err(prettify_solana_client_error);
|
2023-02-20 03:09:17 -08:00
|
|
|
|
|
|
|
if let Err(err) = send_result {
|
2023-07-11 23:38:38 -07:00
|
|
|
info!("error while sending settle batch: {}", err);
|
2023-02-20 03:09:17 -08:00
|
|
|
return Ok(None);
|
|
|
|
}
|
|
|
|
|
|
|
|
let txsig = send_result.unwrap();
|
2023-07-11 23:38:38 -07:00
|
|
|
info!("sent settle tx: {txsig}");
|
2023-02-20 03:09:17 -08:00
|
|
|
Ok(Some(txsig))
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn add_and_maybe_send(
|
|
|
|
&mut self,
|
|
|
|
account_a: Pubkey,
|
|
|
|
account_b: Pubkey,
|
|
|
|
) -> anyhow::Result<Option<Signature>> {
|
|
|
|
let a_value = self.account_fetcher.fetch_mango_account(&account_a)?;
|
|
|
|
let b_value = self.account_fetcher.fetch_mango_account(&account_b)?;
|
2023-11-03 03:20:37 -07:00
|
|
|
let new_ixs = self.mango_client.perp_settle_pnl_instruction(
|
2023-02-20 03:09:17 -08:00
|
|
|
self.perp_market_index,
|
|
|
|
(&account_a, &a_value),
|
|
|
|
(&account_b, &b_value),
|
|
|
|
)?;
|
2023-11-03 03:20:37 -07:00
|
|
|
let previous = self.instructions.clone();
|
|
|
|
self.instructions.append(new_ixs.clone());
|
2023-02-20 03:09:17 -08:00
|
|
|
|
|
|
|
// if we exceed the batch limit or tx size limit, send a batch without the new ix
|
2023-11-03 03:20:37 -07:00
|
|
|
let max_cu_per_tx = 1_400_000;
|
|
|
|
let needs_send = if self.instructions.len() > self.max_batch_size
|
|
|
|
|| self.instructions.cu >= max_cu_per_tx
|
|
|
|
{
|
2023-02-20 03:09:17 -08:00
|
|
|
true
|
|
|
|
} else {
|
|
|
|
let tx = self.transaction()?;
|
|
|
|
let serialized = bincode::serialize(&tx)?;
|
|
|
|
let too_big = serialized.len() >= solana_sdk::packet::PACKET_DATA_SIZE;
|
|
|
|
if too_big && self.instructions.len() == 1 {
|
|
|
|
anyhow::bail!(
|
|
|
|
"settle instruction for accounts {} and {} does not fit tx size: {} bytes",
|
|
|
|
account_a,
|
|
|
|
account_b,
|
|
|
|
serialized.len()
|
|
|
|
);
|
|
|
|
}
|
|
|
|
too_big
|
|
|
|
};
|
|
|
|
if needs_send {
|
2023-11-03 03:20:37 -07:00
|
|
|
self.instructions = previous;
|
2023-02-20 03:09:17 -08:00
|
|
|
let txsig = self.send().await?;
|
2023-11-03 03:20:37 -07:00
|
|
|
self.instructions.append(new_ixs);
|
2023-02-20 03:09:17 -08:00
|
|
|
return Ok(txsig);
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(None)
|
|
|
|
}
|
|
|
|
}
|