From e0f50bf0d4d62207a5bb4552010e52fe28535932 Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Sat, 10 Dec 2022 17:56:56 +0100 Subject: [PATCH] pnl limit: Clean up by adding functions, using it in get-settleable (#329) --- client/src/perp_pnl.rs | 16 +++- .../src/instructions/perp_settle_pnl.rs | 26 +----- .../src/state/mango_account_components.rs | 88 ++++++++++++++++++- programs/mango-v4/src/state/perp_market.rs | 3 +- programs/mango-v4/tests/test_perp_settle.rs | 17 ++-- 5 files changed, 109 insertions(+), 41 deletions(-) diff --git a/client/src/perp_pnl.rs b/client/src/perp_pnl.rs index 105691bb8..a0406a85a 100644 --- a/client/src/perp_pnl.rs +++ b/client/src/perp_pnl.rs @@ -22,6 +22,12 @@ pub fn fetch_top( direction: Direction, count: usize, ) -> anyhow::Result> { + 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::(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::>(); diff --git a/programs/mango-v4/src/instructions/perp_settle_pnl.rs b/programs/mango-v4/src/instructions/perp_settle_pnl.rs index e5cb71b3f..d51fe8b8f 100644 --- a/programs/mango-v4/src/instructions/perp_settle_pnl.rs +++ b/programs/mango-v4/src/instructions/perp_settle_pnl.rs @@ -124,29 +124,9 @@ pub fn perp_settle_pnl(ctx: Context) -> 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(), diff --git a/programs/mango-v4/src/state/mango_account_components.rs b/programs/mango-v4/src/state/mango_account_components.rs index 8224ed8ce..5ee36b568 100644 --- a/programs/mango-v4/src/state/mango_account_components.rs +++ b/programs/mango-v4/src/state/mango_account_components.rs @@ -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::() + }; + + 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); + } } diff --git a/programs/mango-v4/src/state/perp_market.rs b/programs/mango-v4/src/state/perp_market.rs index 356c82b0d..785f7865f 100644 --- a/programs/mango-v4/src/state/perp_market.rs +++ b/programs/mango-v4/src/state/perp_market.rs @@ -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], diff --git a/programs/mango-v4/tests/test_perp_settle.rs b/programs/mango-v4/tests/test_perp_settle.rs index ce67e94bd..776365d46 100644 --- a/programs/mango-v4/tests/test_perp_settle.rs +++ b/programs/mango-v4/tests/test_perp_settle.rs @@ -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::(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(())