pnl limit: Clean up by adding functions, using it in get-settleable (#329)

This commit is contained in:
Christian Kamm 2022-12-10 17:56:56 +01:00 committed by GitHub
parent d3db44e7ba
commit e0f50bf0d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 109 additions and 41 deletions

View File

@ -22,6 +22,12 @@ pub fn fetch_top(
direction: Direction,
count: usize,
) -> anyhow::Result<Vec<(Pubkey, MangoAccountValue, I80F48)>> {
use std::time::{SystemTime, UNIX_EPOCH};
let now_ts: u64 = SystemTime::now()
.duration_since(UNIX_EPOCH)?
.as_millis()
.try_into()?;
let perp = context.perp(perp_market_index);
let perp_market =
account_fetcher_fetch_anchor_account::<PerpMarket>(account_fetcher, &perp.address)?;
@ -47,14 +53,16 @@ pub fn fetch_top(
if perp_pos.is_err() {
return None;
}
let perp_pos = perp_pos.unwrap();
let mut perp_pos = perp_pos.unwrap().clone();
perp_pos.update_settle_limit(&perp_market, now_ts);
let pnl = perp_pos.pnl_for_price(&perp_market, oracle_price).unwrap();
if pnl >= 0 && direction == Direction::MaxNegative
|| pnl <= 0 && direction == Direction::MaxPositive
let limited_pnl = perp_pos.apply_pnl_settle_limit(pnl, &perp_market);
if limited_pnl >= 0 && direction == Direction::MaxNegative
|| limited_pnl <= 0 && direction == Direction::MaxPositive
{
return None;
}
Some((*pk, mango_acc, pnl))
Some((*pk, mango_acc, limited_pnl))
})
.collect::<Vec<_>>();

View File

@ -124,29 +124,9 @@ pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>) -> Result<()> {
// Cap settlement of unrealized pnl
// Settles at most x100% each hour
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
let a_settle_limit_used =
a_perp_position.update_and_get_used_settle_limit(&perp_market, now_ts);
b_perp_position.update_and_get_used_settle_limit(&perp_market, now_ts);
let a_settleable_pnl = if perp_market.settle_pnl_limit_factor >= 0.0 {
let realized_pnl = a_perp_position.realized_pnl_native;
let unrealized_pnl = cm!(a_pnl - realized_pnl);
let a_base_lots = I80F48::from(a_perp_position.base_position_lots());
let avg_entry_price_lots = I80F48::from_num(a_perp_position.avg_entry_price_per_base_lot);
let max_allowed_in_window =
cm!(perp_market.settle_pnl_limit_factor() * a_base_lots * avg_entry_price_lots).abs();
let unrealized_pnl_capped_for_window = unrealized_pnl
.min(cm!(
max_allowed_in_window - I80F48::from_num(a_settle_limit_used)
))
.max(I80F48::ZERO);
a_pnl
.min(cm!(realized_pnl + unrealized_pnl_capped_for_window))
.max(I80F48::ZERO)
} else {
a_pnl
};
a_perp_position.update_settle_limit(&perp_market, now_ts);
b_perp_position.update_settle_limit(&perp_market, now_ts);
let a_settleable_pnl = a_perp_position.apply_pnl_settle_limit(a_pnl, &perp_market);
require!(
a_settleable_pnl.is_positive(),

View File

@ -434,8 +434,9 @@ impl PerpPosition {
Ok(pnl)
}
/// Side-effect: updates the windowing
pub fn update_and_get_used_settle_limit(&mut self, market: &PerpMarket, now_ts: u64) -> i64 {
/// Updates the perp pnl limit time windowing, resetting the amount
/// of used settle-pnl budget if necessary
pub fn update_settle_limit(&mut self, market: &PerpMarket, now_ts: u64) {
assert_eq!(self.market_index, market.perp_market_index);
let window_size = market.settle_pnl_limit_window_size_ts;
let new_window = now_ts >= cm!((self.settle_pnl_limit_window + 1) as u64 * window_size);
@ -443,7 +444,43 @@ impl PerpPosition {
self.settle_pnl_limit_window = cm!(now_ts / window_size).try_into().unwrap();
self.settle_pnl_limit_settled_in_current_window_native = 0;
}
self.settle_pnl_limit_settled_in_current_window_native
}
/// Returns the quote-native amount of unrealized pnl that may still be settled.
/// Always >= 0.
pub fn available_settle_limit(&self, market: &PerpMarket) -> i64 {
assert_eq!(self.market_index, market.perp_market_index);
if market.settle_pnl_limit_factor < 0.0 {
return i64::MAX;
}
let position_value =
(self.avg_entry_price_per_base_lot * self.base_position_lots as f64).abs();
let max_allowed_in_window =
(market.settle_pnl_limit_factor as f64 * position_value).min(i64::MAX as f64) as i64;
(max_allowed_in_window - self.settle_pnl_limit_settled_in_current_window_native).max(0)
}
/// Given some pnl, applies the positive unrealized pnl settle limit and returns the reduced pnl.
pub fn apply_pnl_settle_limit(&self, pnl: I80F48, market: &PerpMarket) -> I80F48 {
if market.settle_pnl_limit_factor < 0.0 {
return pnl;
}
let available_settle_limit = I80F48::from(self.available_settle_limit(&market));
let realized_pnl = self.realized_pnl_native;
if realized_pnl < 0 {
// If realized pnl is negative, we just need to cap the total pnl to the
// settle limit if it ends up positive
pnl.min(available_settle_limit)
} else {
// If realized is positive, apply the limit only to the unrealized part
let unrealized_pnl = cm!(pnl - realized_pnl);
let unrealized_pnl_capped_for_window =
unrealized_pnl.min(I80F48::from(available_settle_limit));
cm!(realized_pnl + unrealized_pnl_capped_for_window)
}
}
/// Update the perp position for pnl settlement
@ -817,4 +854,49 @@ mod tests {
pos.record_settle(I80F48::from(-10));
assert_eq!(pos.realized_pnl_native, I80F48::from(0));
}
#[test]
fn test_perp_settle_limit() {
let market = PerpMarket::default_for_tests();
let mut pos = create_perp_position(&market, 100, -50);
pos.realized_pnl_native = I80F48::from(5);
let limited_pnl = |pos: &PerpPosition, pnl: i64| {
pos.apply_pnl_settle_limit(I80F48::from(pnl), &market)
.to_num::<f64>()
};
assert_eq!(pos.available_settle_limit(&market), 10); // 0.2 factor * 0.5 entry price * 100 lots
assert_eq!(limited_pnl(&pos, 100), 15.0);
assert_eq!(limited_pnl(&pos, -100), -100.0);
pos.settle_pnl_limit_settled_in_current_window_native = 2;
assert_eq!(pos.available_settle_limit(&market), 8);
assert_eq!(limited_pnl(&pos, 100), 13.0);
assert_eq!(limited_pnl(&pos, -100), -100.0);
pos.settle_pnl_limit_settled_in_current_window_native = 11;
assert_eq!(pos.available_settle_limit(&market), 0);
assert_eq!(limited_pnl(&pos, 100), 5.0);
assert_eq!(limited_pnl(&pos, -100), -100.0);
pos.realized_pnl_native = I80F48::from(-10);
pos.settle_pnl_limit_settled_in_current_window_native = 2;
assert_eq!(limited_pnl(&pos, 100), 8.0);
assert_eq!(limited_pnl(&pos, -100), -100.0);
pos.settle_pnl_limit_settled_in_current_window_native = -2;
assert_eq!(limited_pnl(&pos, 100), 12.0);
assert_eq!(limited_pnl(&pos, -100), -100.0);
pos.settle_pnl_limit_settled_in_current_window_native = 2;
pos.realized_pnl_native = I80F48::from(-100);
assert_eq!(limited_pnl(&pos, 10), 8.0);
assert_eq!(limited_pnl(&pos, -10), -10.0);
pos.realized_pnl_native = I80F48::from(100);
assert_eq!(limited_pnl(&pos, 10), 10.0);
assert_eq!(limited_pnl(&pos, -10), -10.0);
}
}

View File

@ -97,7 +97,8 @@ pub struct PerpMarket {
pub settle_fee_fraction_low_health: f32,
// Pnl settling limits
/// Fraction of perp base value that can be settled each window.
/// Fraction of perp base value (i.e. base_lots * entry_price_in_lots) of unrealized
/// positive pnl that can be settled each window.
/// Set to a negative value to disable the limit.
pub settle_pnl_limit_factor: f32,
pub padding3: [u8; 4],

View File

@ -946,9 +946,7 @@ async fn test_perp_pnl_settle_limit() -> Result<(), TransportError> {
+ (market.settle_pnl_limit_factor()
* I80F48::from_num(mango_account_0.perps[0].avg_entry_price(&market))
* mango_account_0.perps[0].base_position_native(&market))
.abs()
// fees
- I80F48::from_num(1000.0 * 100.0 * 0.0002);
.abs();
send_tx(
solana,
PerpSettlePnlInstruction {
@ -969,7 +967,7 @@ async fn test_perp_pnl_settle_limit() -> Result<(), TransportError> {
);
// attempt 2 - as we are in the same window, and we settled max. possible in previous attempt,
// we can't settle anymore amount
send_tx(
let result = send_tx(
solana,
PerpSettlePnlInstruction {
settler,
@ -980,12 +978,11 @@ async fn test_perp_pnl_settle_limit() -> Result<(), TransportError> {
settle_bank: tokens[0].bank,
},
)
.await
.unwrap();
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
assert_eq!(
mango_account_1.perps[0].quote_position_native().round(),
mango_account_1_expected_qpn_after_settle.round()
.await;
assert_mango_error(
&result,
MangoError::ProfitabilityMismatch.into(),
"Account A has no settleable positive pnl left".to_string(),
);
Ok(())