pnl limit: Clean up by adding functions, using it in get-settleable (#329)
This commit is contained in:
parent
d3db44e7ba
commit
e0f50bf0d4
|
@ -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<_>>();
|
||||
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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(())
|
||||
|
|
Loading…
Reference in New Issue