Change conditions for perp settle incentive (#771)

- Only ever give an incentive when pnl is at least 1% of the position
  value. That way large positions (like $100k in SOL-PERP) don't get
  settled on 0.1% price fluctuations. The price now needs to change by
  1% for settlement to occur.
- For low health incentives, cap the percentual incentive at 2x the flat
  settle fee. We want to give the settler incentive to use these first,
  but the settler doesn't take on risk, so the reward doesn't need to be
  large.
This commit is contained in:
Christian Kamm 2023-11-07 12:01:02 +01:00 committed by GitHub
parent 29fa27b76b
commit c49efb2213
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 129 additions and 38 deletions

View File

@ -26,30 +26,36 @@ pub struct Config {
fn perp_markets_and_prices(
mango_client: &MangoClient,
account_fetcher: &chain_data::AccountFetcher,
) -> HashMap<PerpMarketIndex, (PerpMarket, I80F48)> {
) -> HashMap<PerpMarketIndex, (PerpMarket, I80F48, I80F48)> {
mango_client
.context
.perp_markets
.iter()
.map(|(market_index, perp)| {
let perp_market = account_fetcher.fetch::<PerpMarket>(&perp.address)?;
let oracle_acc = account_fetcher.fetch_raw(&perp_market.oracle)?;
let oracle_price = perp_market.oracle_price(
&KeyedAccountSharedData::new(perp_market.oracle, oracle_acc),
None,
)?;
Ok((*market_index, (perp_market, oracle_price)))
let settle_token = mango_client.context.token(perp_market.settle_token_index);
let settle_token_price =
account_fetcher.fetch_bank_price(&settle_token.mint_info.first_bank())?;
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
}
})
.filter_map(
|v: anyhow::Result<(PerpMarketIndex, (PerpMarket, I80F48))>| match v {
Ok(v) => Some(v),
Err(err) => {
error!("error while retriving perp market and price: {:?}", err);
None
}
},
)
.collect()
}
@ -118,10 +124,11 @@ impl SettlementState {
let liq_end_health = health_cache.health(HealthType::LiquidationEnd);
for perp_market_index in perp_indexes {
let (perp_market, 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
};
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
};
let perp_max_settle =
health_cache.perp_max_settle(perp_market.settle_token_index)?;
@ -129,7 +136,7 @@ impl SettlementState {
perp_position.settle_funding(perp_market);
perp_position.update_settle_limit(perp_market, now_ts);
let unsettled = perp_position.unsettled_pnl(perp_market, *price)?;
let unsettled = perp_position.unsettled_pnl(perp_market, *perp_price)?;
let limited = perp_position.apply_pnl_settle_limit(perp_market, unsettled);
let settleable = if limited >= 0 {
limited
@ -145,10 +152,22 @@ impl SettlementState {
liq_end_health
};
let pnl_value = unsettled * settle_token_price;
let position_value =
perp_position.base_position_native(perp_market) * perp_price;
let fee = perp_market
.compute_settle_fee(settleable, liq_end_health, maint_health)
.compute_settle_fee(
settleable,
pnl_value,
position_value,
liq_end_health,
maint_health,
)
.unwrap();
if fee <= 0 {
// 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 {
continue;
}
@ -168,7 +187,7 @@ impl SettlementState {
let address_lookup_tables = mango_client.mango_address_lookup_tables().await?;
for (perp_market_index, mut positive_settleable) in all_positive_settleable {
let (perp_market, _) = perp_market_info.get(&perp_market_index).unwrap();
let (perp_market, _, _) = perp_market_info.get(&perp_market_index).unwrap();
let negative_settleable = match all_negative_settleable.get_mut(&perp_market_index) {
None => continue,
Some(v) => v,

View File

@ -140,8 +140,6 @@ pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>) -> Result<()> {
b_max_settle,
);
let fee = perp_market.compute_settle_fee(settlement, a_liq_end_health, a_maint_health)?;
a_perp_position.record_settle(settlement);
b_perp_position.record_settle(-settlement);
emit_perp_balances(
@ -157,6 +155,17 @@ pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>) -> Result<()> {
&perp_market,
);
// Compute fee
let a_position_value = a_perp_position.base_position_native(&perp_market).abs() * oracle_price;
let a_pnl_value = a_pnl * settle_token_oracle_price;
let fee = perp_market.compute_settle_fee(
settlement,
a_pnl_value,
a_position_value,
a_liq_end_health,
a_maint_health,
)?;
// Update the accounts' perp_spot_transfer statistics.
//
// Applying the fee here means that it decreases the displayed perp pnl.

View File

@ -135,11 +135,12 @@ pub struct PerpMarket {
// Settling incentives
/// In native units of settlement token, given to each settle call above the
/// settle_fee_amount_threshold.
/// settle_fee_amount_threshold if settling at least 1% of perp base pos value.
pub settle_fee_flat: f32,
/// Pnl settlement amount needed to be eligible for the flat fee.
pub settle_fee_amount_threshold: f32,
/// Fraction of pnl to pay out as fee if +pnl account has low health.
/// (limited to 2x settle_fee_flat)
pub settle_fee_fraction_low_health: f32,
// Pnl settling limits
@ -398,14 +399,27 @@ impl PerpMarket {
Ok(socialized_loss)
}
/// Returns the fee for settling `settlement` when the negative-pnl side has the given
/// health values.
/// Returns the fee for settling `settlement` when the account with positive unsettled pnl
/// has the given source pnl/position/health values.
pub fn compute_settle_fee(
&self,
settlement: I80F48,
source_pnl_value: I80F48,
source_position_value: I80F48,
source_liq_end_health: I80F48,
source_maint_health: I80F48,
) -> Result<I80F48> {
// Only incentivize if pnl is at least 1% of position.
//
// This avoids large positions being settled all the time when tiny price
// movements can bring the settlement amount over the settle_fee_amount_threshold.
//
// Always true when the source position is closed.
let pnl_at_least_one_percent = I80F48::from(100) * source_pnl_value > source_position_value;
if !pnl_at_least_one_percent {
return Ok(I80F48::ZERO);
}
assert!(source_maint_health >= source_liq_end_health);
// A percentage fee is paid to the settler when the source account's health is low.
@ -424,15 +438,18 @@ impl PerpMarket {
I80F48::ZERO
};
// The settler receives a flat fee
let flat_fee = if settlement >= self.settle_fee_amount_threshold {
I80F48::from_num(self.settle_fee_flat)
let flat_fee = I80F48::from_num(self.settle_fee_flat);
let mut fee = if settlement >= self.settle_fee_amount_threshold {
// If the settlement is big enough: give the flat fee
flat_fee
} else {
I80F48::ZERO
// Else give the low-health fee, but never more than twice flat fee
low_health_fee.min(flat_fee * I80F48::from(2))
};
// Fees only apply when the settlement is large enough
let fee = (low_health_fee + flat_fee).min(settlement);
// Fee can't exceed the settlement (just for safety)
fee = fee.min(settlement);
// Safety check to prevent any accidental negative transfer
require!(fee >= 0, MangoError::SettlementAmountMustBePositive);

View File

@ -582,7 +582,7 @@ async fn test_perp_settle_pnl_fees() -> Result<(), TransportError> {
// SETUP: Create a perp market
//
let flat_fee = 1000;
let fee_low_health = 0.05;
let fee_low_health = 0.10;
let mango_v4::accounts::PerpCreateMarket { perp_market, .. } = send_tx(
solana,
PerpCreateMarketInstruction {
@ -600,7 +600,7 @@ async fn test_perp_settle_pnl_fees() -> Result<(), TransportError> {
maker_fee: 0.0,
taker_fee: 0.0,
settle_fee_flat: flat_fee as f32,
settle_fee_amount_threshold: 2000.0,
settle_fee_amount_threshold: 4000.0,
settle_fee_fraction_low_health: fee_low_health,
settle_pnl_limit_factor: 0.2,
settle_pnl_limit_window_size_ts: 24 * 60 * 60,
@ -742,11 +742,11 @@ async fn test_perp_settle_pnl_fees() -> Result<(), TransportError> {
set_bank_stub_oracle_price(solana, group, &tokens[2], admin, 10700.0).await;
//
// TEST: Settle (health is low)
// TEST: Settle (health is low), percent fee
//
set_perp_stub_oracle_price(solana, group, perp_market, &tokens[1], admin, 1100.0).await;
set_perp_stub_oracle_price(solana, group, perp_market, &tokens[1], admin, 1070.0).await;
let expected_pnl = 5000;
let expected_pnl = 2000;
send_tx(
solana,
@ -762,9 +762,55 @@ async fn test_perp_settle_pnl_fees() -> Result<(), TransportError> {
.unwrap();
total_settled_pnl += expected_pnl;
total_fees_paid += flat_fee
+ (expected_pnl as f64 * fee_low_health as f64 * 980.0 / (1160.0 + 980.0)) as i64
+ 1;
total_fees_paid +=
(expected_pnl as f64 * fee_low_health as f64 * 980.0 / (1160.0 + 980.0)) as i64 + 1;
{
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
assert_eq!(
mango_account_0.perps[0].quote_position_native().round(),
I80F48::from(-100_000 - total_settled_pnl)
);
assert_eq!(
mango_account_1.perps[0].quote_position_native().round(),
I80F48::from(100_000 + total_settled_pnl),
);
assert_eq!(
account_position(solana, account_0, settle_bank).await,
initial_token_deposit as i64 + total_settled_pnl - total_fees_paid
);
assert_eq!(
account_position(solana, account_1, settle_bank).await,
initial_token_deposit as i64 - total_settled_pnl
);
assert_eq!(
account_position(solana, settler, settle_bank).await,
total_fees_paid
);
}
//
// TEST: Settle (health is low), no fee because pnl too small
//
set_perp_stub_oracle_price(solana, group, perp_market, &tokens[1], admin, 1071.0).await;
let expected_pnl = 100;
send_tx(
solana,
PerpSettlePnlInstruction {
settler,
settler_owner,
account_a: account_0,
account_b: account_1,
perp_market,
},
)
.await
.unwrap();
total_settled_pnl += expected_pnl;
total_fees_paid += 0;
{
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;