From ae5907ba3a431b95f1af0eb8357371cafbd81df2 Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Thu, 1 Feb 2024 11:23:45 +0100 Subject: [PATCH] fix perp settle limit materialization (#865) Previously, we tried to keep track of "other" and "trade" realized pnl. An issue occured when a perp base position went to zero: the way we computed the trade pnl included potential non-trade unsettled pnl. That caused follow-up trouble because the value could change sign and reset the settle limit for trade pnl. This change aims to simplify in some ways: - explicitly talk about oneshot-settleable pnl (fees, funding, liquidation) and recurring-settleable pnl (materialization of settle limit derived from the stable value of the base position when reducing the base position) - instead of directly tracking realized settleable amounts (which doesn't really work), just decrease the recurring settleable amount when it exceeds the remaining unsettled pnl - get rid of the directionality to avoid bugs of that kind - stop tracking unsettled-realized trade pnl (it was wrong before, and no client uses it) - we already track position-lifetime realized trade pnl --- mango_v4.json | 34 +- .../src/instructions/perp_consume_events.rs | 43 +- .../perp_liq_base_or_positive_pnl.rs | 6 +- .../perp_liq_negative_pnl_or_bankruptcy.rs | 8 +- .../src/instructions/perp_settle_fees.rs | 2 +- .../src/instructions/perp_settle_pnl.rs | 4 +- programs/mango-v4/src/state/mango_account.rs | 14 +- .../src/state/mango_account_components.rs | 706 ++++++++---------- .../tests/cases/test_liq_perps_bankruptcy.rs | 18 +- .../test_liq_perps_base_and_bankruptcy.rs | 12 +- .../mango-v4/tests/cases/test_perp_settle.rs | 29 +- ts/client/src/accounts/mangoAccount.ts | 55 +- ts/client/src/mango_v4.ts | 68 +- 13 files changed, 454 insertions(+), 545 deletions(-) diff --git a/mango_v4.json b/mango_v4.json index 27495ca7d..b3475028d 100644 --- a/mango_v4.json +++ b/mango_v4.json @@ -9334,36 +9334,44 @@ "type": "f64" }, { - "name": "realizedTradePnlNative", + "name": "deprecatedRealizedTradePnlNative", "docs": [ - "Amount of pnl that was realized by bringing the base position closer to 0.", - "", - "The settlement of this type of pnl is limited by settle_pnl_limit_realized_trade.", - "Settling pnl reduces this value once other_pnl below is exhausted." + "Deprecated field: Amount of pnl that was realized by bringing the base position closer to 0." ], "type": { "defined": "I80F48" } }, { - "name": "realizedOtherPnlNative", + "name": "oneshotSettlePnlAllowance", "docs": [ - "Amount of pnl realized from fees, funding and liquidation.", + "Amount of pnl that can be settled once.", "", - "This type of realized pnl is always settleable.", - "Settling pnl reduces this value first." + "- The value is signed: a negative number means negative pnl can be settled.", + "- A settlement in the right direction will decrease this amount.", + "", + "Typically added for fees, funding and liquidation." ], "type": { "defined": "I80F48" } }, { - "name": "settlePnlLimitRealizedTrade", + "name": "recurringSettlePnlAllowance", "docs": [ - "Settle limit contribution from realized pnl.", + "Amount of pnl that can be settled in each settle window.", "", - "Every time pnl is realized, this is increased by a fraction of the stable", - "value of the realization. It magnitude decreases when realized pnl drops below its value." + "- Unsigned, the settlement can happen in both directions. Value is >= 0.", + "- Previously stored a similar value that was signed, so in migration cases", + "this value can be negative and should be .abs()ed.", + "- If this value exceeds the current stable-upnl, it should be decreased,", + "see apply_recurring_settle_pnl_allowance_constraint()", + "", + "When the base position is reduced, the settle limit contribution from the reduced", + "base position is materialized into this value. When the base position increases,", + "some of the allowance is taken away.", + "", + "This also gets increased when a liquidator takes over pnl." ], "type": "i64" }, diff --git a/programs/mango-v4/src/instructions/perp_consume_events.rs b/programs/mango-v4/src/instructions/perp_consume_events.rs index d029fb43a..c8f6341a7 100644 --- a/programs/mango-v4/src/instructions/perp_consume_events.rs +++ b/programs/mango-v4/src/instructions/perp_consume_events.rs @@ -74,40 +74,37 @@ pub fn perp_consume_events(ctx: Context, limit: usize) -> Res group, event_queue ); - let before_pnl = maker_taker - .perp_position(perp_market_index)? - .realized_trade_pnl_native; - maker_taker.execute_perp_maker( + let maker_realized_pnl = maker_taker.execute_perp_maker( perp_market_index, &mut perp_market, fill, &group, )?; - maker_taker.execute_perp_taker(perp_market_index, &mut perp_market, fill)?; + let taker_realized_pnl = maker_taker.execute_perp_taker( + perp_market_index, + &mut perp_market, + fill, + )?; emit_perp_balances( group_key, fill.maker, maker_taker.perp_position(perp_market_index).unwrap(), &perp_market, ); - let after_pnl = maker_taker - .perp_position(perp_market_index)? - .realized_trade_pnl_native; - let closed_pnl = after_pnl - before_pnl; + let closed_pnl = maker_realized_pnl + taker_realized_pnl; (closed_pnl, closed_pnl) } else { load_mango_account!(maker, fill.maker, mango_account_ais, group, event_queue); load_mango_account!(taker, fill.taker, mango_account_ais, group, event_queue); - let maker_before_pnl = maker - .perp_position(perp_market_index)? - .realized_trade_pnl_native; - let taker_before_pnl = taker - .perp_position(perp_market_index)? - .realized_trade_pnl_native; - - maker.execute_perp_maker(perp_market_index, &mut perp_market, fill, &group)?; - taker.execute_perp_taker(perp_market_index, &mut perp_market, fill)?; + let maker_realized_pnl = maker.execute_perp_maker( + perp_market_index, + &mut perp_market, + fill, + &group, + )?; + let taker_realized_pnl = + taker.execute_perp_taker(perp_market_index, &mut perp_market, fill)?; emit_perp_balances( group_key, fill.maker, @@ -120,16 +117,8 @@ pub fn perp_consume_events(ctx: Context, limit: usize) -> Res taker.perp_position(perp_market_index).unwrap(), &perp_market, ); - let maker_after_pnl = maker - .perp_position(perp_market_index)? - .realized_trade_pnl_native; - let taker_after_pnl = taker - .perp_position(perp_market_index)? - .realized_trade_pnl_native; - let maker_closed_pnl = maker_after_pnl - maker_before_pnl; - let taker_closed_pnl = taker_after_pnl - taker_before_pnl; - (maker_closed_pnl, taker_closed_pnl) + (maker_realized_pnl, taker_realized_pnl) }; emit_stack(FillLogV3 { mango_group: group_key, diff --git a/programs/mango-v4/src/instructions/perp_liq_base_or_positive_pnl.rs b/programs/mango-v4/src/instructions/perp_liq_base_or_positive_pnl.rs index 246ddd9bf..93c867078 100644 --- a/programs/mango-v4/src/instructions/perp_liq_base_or_positive_pnl.rs +++ b/programs/mango-v4/src/instructions/perp_liq_base_or_positive_pnl.rs @@ -598,7 +598,7 @@ pub(crate) fn liquidation_action( let token_transfer = pnl_transfer * spot_gain_per_settled; liqor_perp_position.record_liquidation_pnl_takeover(pnl_transfer, limit_transfer); - liqee_perp_position.record_settle(pnl_transfer); + liqee_perp_position.record_settle(pnl_transfer, &perp_market); // Update the accounts' perp_spot_transfer statistics. let transfer_i64 = token_transfer.round_to_zero().to_num::(); @@ -1027,7 +1027,7 @@ mod tests { init_liqee_base, I80F48::from_num(init_liqee_quote), ); - p.realized_other_pnl_native = p + p.oneshot_settle_pnl_allowance = p .unsettled_pnl(setup.perp_market.data(), I80F48::ONE) .unwrap(); @@ -1072,7 +1072,7 @@ mod tests { // The settle limit taken over matches the quote pos when removing the // quote gains from giving away base lots assert_eq_f!( - I80F48::from_num(liqor_perp.settle_pnl_limit_realized_trade), + I80F48::from_num(liqor_perp.recurring_settle_pnl_allowance), liqor_perp.quote_position_native.to_num::() + liqor_perp.base_position_lots as f64, 1.1 diff --git a/programs/mango-v4/src/instructions/perp_liq_negative_pnl_or_bankruptcy.rs b/programs/mango-v4/src/instructions/perp_liq_negative_pnl_or_bankruptcy.rs index 73afa64e2..49b8416f9 100644 --- a/programs/mango-v4/src/instructions/perp_liq_negative_pnl_or_bankruptcy.rs +++ b/programs/mango-v4/src/instructions/perp_liq_negative_pnl_or_bankruptcy.rs @@ -267,7 +267,7 @@ pub(crate) fn liquidation_action( .max(I80F48::ZERO); if settlement > 0 { liqor_perp_position.record_liquidation_quote_change(-settlement); - liqee_perp_position.record_settle(-settlement); + liqee_perp_position.record_settle(-settlement, &perp_market); // Update the accounts' perp_spot_transfer statistics. let settlement_i64 = settlement.round_to_zero().to_num::(); @@ -380,7 +380,7 @@ pub(crate) fn liquidation_action( // transfer perp quote loss from the liqee to the liqor let liqor_perp_position = liqor.perp_position_mut(perp_market_index)?; - liqee_perp_position.record_settle(-insurance_liab_transfer); + liqee_perp_position.record_settle(-insurance_liab_transfer, &perp_market); liqor_perp_position.record_liquidation_quote_change(-insurance_liab_transfer); msg!( @@ -399,7 +399,7 @@ pub(crate) fn liquidation_action( (perp_market.long_funding, perp_market.short_funding); if insurance_fund_exhausted && remaining_liab > 0 { perp_market.socialize_loss(-remaining_liab)?; - liqee_perp_position.record_settle(-remaining_liab); + liqee_perp_position.record_settle(-remaining_liab, &perp_market); socialized_loss = remaining_liab; msg!("socialized loss: {}", socialized_loss); } @@ -760,7 +760,7 @@ mod tests { { let p = perp_p(&mut setup.liqee); p.quote_position_native = I80F48::from_num(init_perp); - p.settle_pnl_limit_realized_trade = -settle_limit; + p.recurring_settle_pnl_allowance = (settle_limit as i64).abs(); let settle_bank = setup.settle_bank.data(); settle_bank diff --git a/programs/mango-v4/src/instructions/perp_settle_fees.rs b/programs/mango-v4/src/instructions/perp_settle_fees.rs index f268c62b2..d7c31cf82 100644 --- a/programs/mango-v4/src/instructions/perp_settle_fees.rs +++ b/programs/mango-v4/src/instructions/perp_settle_fees.rs @@ -68,7 +68,7 @@ pub fn perp_settle_fees(ctx: Context, max_settle_amount: u64) -> .min(I80F48::from(max_settle_amount)); require!(settlement >= 0, MangoError::SettlementAmountMustBePositive); - perp_position.record_settle(-settlement); // settle the negative pnl on the user perp position + perp_position.record_settle(-settlement, &perp_market); // settle the negative pnl on the user perp position perp_market.fees_accrued -= settlement; emit_perp_balances( diff --git a/programs/mango-v4/src/instructions/perp_settle_pnl.rs b/programs/mango-v4/src/instructions/perp_settle_pnl.rs index 4fba7233a..3e8bc7927 100644 --- a/programs/mango-v4/src/instructions/perp_settle_pnl.rs +++ b/programs/mango-v4/src/instructions/perp_settle_pnl.rs @@ -143,8 +143,8 @@ pub fn perp_settle_pnl(ctx: Context) -> Result<()> { b_max_settle, ); - a_perp_position.record_settle(settlement); - b_perp_position.record_settle(-settlement); + a_perp_position.record_settle(settlement, &perp_market); + b_perp_position.record_settle(-settlement, &perp_market); emit_perp_balances( ctx.accounts.group.key(), ctx.accounts.account_a.key(), diff --git a/programs/mango-v4/src/state/mango_account.rs b/programs/mango-v4/src/state/mango_account.rs index 656e93f72..9a759b250 100644 --- a/programs/mango-v4/src/state/mango_account.rs +++ b/programs/mango-v4/src/state/mango_account.rs @@ -1237,13 +1237,14 @@ impl< Ok(()) } + /// Returns amount of realized trade pnl for the maker pub fn execute_perp_maker( &mut self, perp_market_index: PerpMarketIndex, perp_market: &mut PerpMarket, fill: &FillEvent, group: &Group, - ) -> Result<()> { + ) -> Result { let side = fill.taker_side().invert_side(); let (base_change, quote_change) = fill.base_quote_change(side); let quote = I80F48::from(perp_market.quote_lot_size) * I80F48::from(quote_change); @@ -1257,7 +1258,7 @@ impl< let pa = self.perp_position_mut(perp_market_index)?; pa.settle_funding(perp_market); pa.record_trading_fee(fees); - pa.record_trade(perp_market, base_change, quote); + let realized_pnl = pa.record_trade(perp_market, base_change, quote); pa.maker_volume += quote.abs().to_num::(); @@ -1288,15 +1289,16 @@ impl< } } - Ok(()) + Ok(realized_pnl) } + /// Returns amount of realized trade pnl for the taker pub fn execute_perp_taker( &mut self, perp_market_index: PerpMarketIndex, perp_market: &mut PerpMarket, fill: &FillEvent, - ) -> Result<()> { + ) -> Result { let pa = self.perp_position_mut(perp_market_index)?; pa.settle_funding(perp_market); @@ -1305,11 +1307,11 @@ impl< // fees are assessed at time of trade; no need to assess fees here let quote_change_native = I80F48::from(perp_market.quote_lot_size) * I80F48::from(quote_change); - pa.record_trade(perp_market, base_change, quote_change_native); + let realized_pnl = pa.record_trade(perp_market, base_change, quote_change_native); pa.taker_volume += quote_change_native.abs().to_num::(); - Ok(()) + Ok(realized_pnl) } pub fn execute_perp_out_event( diff --git a/programs/mango-v4/src/state/mango_account_components.rs b/programs/mango-v4/src/state/mango_account_components.rs index 10e8dc50f..06d30efc5 100644 --- a/programs/mango-v4/src/state/mango_account_components.rs +++ b/programs/mango-v4/src/state/mango_account_components.rs @@ -288,23 +288,31 @@ pub struct PerpPosition { /// Reset to 0 when the base position reaches or crosses 0. pub avg_entry_price_per_base_lot: f64, - /// Amount of pnl that was realized by bringing the base position closer to 0. - /// - /// The settlement of this type of pnl is limited by settle_pnl_limit_realized_trade. - /// Settling pnl reduces this value once other_pnl below is exhausted. - pub realized_trade_pnl_native: I80F48, + /// Deprecated field: Amount of pnl that was realized by bringing the base position closer to 0. + pub deprecated_realized_trade_pnl_native: I80F48, - /// Amount of pnl realized from fees, funding and liquidation. + /// Amount of pnl that can be settled once. /// - /// This type of realized pnl is always settleable. - /// Settling pnl reduces this value first. - pub realized_other_pnl_native: I80F48, + /// - The value is signed: a negative number means negative pnl can be settled. + /// - A settlement in the right direction will decrease this amount. + /// + /// Typically added for fees, funding and liquidation. + pub oneshot_settle_pnl_allowance: I80F48, - /// Settle limit contribution from realized pnl. + /// Amount of pnl that can be settled in each settle window. /// - /// Every time pnl is realized, this is increased by a fraction of the stable - /// value of the realization. It magnitude decreases when realized pnl drops below its value. - pub settle_pnl_limit_realized_trade: i64, + /// - Unsigned, the settlement can happen in both directions. Value is >= 0. + /// - Previously stored a similar value that was signed, so in migration cases + /// this value can be negative and should be .abs()ed. + /// - If this value exceeds the current stable-upnl, it should be decreased, + /// see apply_recurring_settle_pnl_allowance_constraint() + /// + /// When the base position is reduced, the settle limit contribution from the reduced + /// base position is materialized into this value. When the base position increases, + /// some of the allowance is taken away. + /// + /// This also gets increased when a liquidator takes over pnl. + pub recurring_settle_pnl_allowance: i64, /// Trade pnl, fees, funding that were added over the current position's lifetime. /// @@ -345,11 +353,11 @@ impl Default for PerpPosition { taker_volume: 0, perp_spot_transfers: 0, avg_entry_price_per_base_lot: 0.0, - realized_trade_pnl_native: I80F48::ZERO, - realized_other_pnl_native: I80F48::ZERO, + deprecated_realized_trade_pnl_native: I80F48::ZERO, + oneshot_settle_pnl_allowance: I80F48::ZERO, settle_pnl_limit_window: 0, settle_pnl_limit_settled_in_current_window_native: 0, - settle_pnl_limit_realized_trade: 0, + recurring_settle_pnl_allowance: 0, realized_pnl_for_position_native: I80F48::ZERO, reserved: [0; 88], } @@ -439,7 +447,7 @@ impl PerpPosition { pub fn settle_funding(&mut self, perp_market: &PerpMarket) { let funding = self.unsettled_funding(perp_market); self.quote_position_native -= funding; - self.realized_other_pnl_native -= funding; + self.oneshot_settle_pnl_allowance -= funding; self.realized_pnl_for_position_native -= funding; if self.base_position_lots.is_positive() { @@ -453,41 +461,47 @@ impl PerpPosition { } /// Updates avg entry price, breakeven price, realized pnl, realized pnl limit + /// + /// Returns realized trade pnl fn update_trade_stats( &mut self, base_change: i64, quote_change_native: I80F48, perp_market: &PerpMarket, - ) { + ) -> I80F48 { if base_change == 0 { - return; + return I80F48::ZERO; } let old_position = self.base_position_lots; let new_position = old_position + base_change; - // amount of lots that were reduced (so going from -5 to 10 lots is a reduction of 5) + // abs amount of lots that were reduced: + // - going from -5 to 10 lots is a reduction of 5 + // - going from 10 to -5 is a reduction of 10 let reduced_lots; + // same for increases + // - going from -5 to 10 lots is an increase of 10 + // - going from 10 to -5 is an increase of 5 + let increased_lots; // amount of pnl that was realized by the reduction (signed) let newly_realized_pnl; if new_position == 0 { - reduced_lots = -old_position; + reduced_lots = old_position.abs(); + increased_lots = 0; + + let avg_entry = I80F48::from_num(self.avg_entry_price_per_base_lot); + newly_realized_pnl = quote_change_native + I80F48::from(base_change) * avg_entry; // clear out display fields that live only while the position lasts self.avg_entry_price_per_base_lot = 0.0; self.quote_running_native = 0; self.realized_pnl_for_position_native = I80F48::ZERO; - - // There can't be unrealized pnl without a base position, so fix the - // realized_trade_pnl to cover everything that isn't realized_other_pnl. - let total_realized_pnl = self.quote_position_native + quote_change_native; - let new_realized_trade_pnl = total_realized_pnl - self.realized_other_pnl_native; - newly_realized_pnl = new_realized_trade_pnl - self.realized_trade_pnl_native; - self.realized_trade_pnl_native = new_realized_trade_pnl; } else if old_position.signum() != new_position.signum() { // If the base position changes sign, we've crossed base_pos == 0 (or old_position == 0) - reduced_lots = -old_position; + reduced_lots = old_position.abs(); + increased_lots = new_position.abs(); let old_position = old_position as f64; let new_position = new_position as f64; let base_change = base_change as f64; @@ -496,7 +510,6 @@ impl PerpPosition { // Award realized pnl based on the old_position size newly_realized_pnl = I80F48::from_num(old_position * (new_avg_entry - old_avg_entry)); - self.realized_trade_pnl_native += newly_realized_pnl; // Set entry and break-even based on the new_position entered self.avg_entry_price_per_base_lot = new_avg_entry; @@ -513,6 +526,7 @@ impl PerpPosition { if is_increasing { // Increasing position: avg entry price updates, no new realized pnl reduced_lots = 0; + increased_lots = base_change.abs(); newly_realized_pnl = I80F48::ZERO; let old_position_abs = old_position.abs() as f64; let new_position_abs = new_position.abs() as f64; @@ -522,128 +536,60 @@ impl PerpPosition { self.avg_entry_price_per_base_lot = new_position_quote_value / new_position_abs; } else { // Decreasing position: pnl is realized, avg entry price does not change - reduced_lots = base_change; + reduced_lots = base_change.abs(); + increased_lots = 0; let avg_entry = I80F48::from_num(self.avg_entry_price_per_base_lot); newly_realized_pnl = quote_change_native + I80F48::from(base_change) * avg_entry; - self.realized_trade_pnl_native += newly_realized_pnl; self.realized_pnl_for_position_native += newly_realized_pnl; } } - // Bump the realized trade pnl settle limit for a fraction of the stable price value, - // allowing gradual settlement of very high-pnl trades. - let realized_stable_value = I80F48::from(reduced_lots.abs() * perp_market.base_lot_size) - * perp_market.stable_price(); - let stable_value_fraction = - I80F48::from_num(perp_market.settle_pnl_limit_factor) * realized_stable_value; - self.increase_realized_trade_pnl_settle_limit(newly_realized_pnl, stable_value_fraction); + let net_base_increase = increased_lots - reduced_lots; + self.recurring_settle_pnl_allowance = self.recurring_settle_pnl_allowance.abs(); + self.recurring_settle_pnl_allowance -= + (I80F48::from(net_base_increase * perp_market.base_lot_size) + * perp_market.stable_price() + * I80F48::from_num(perp_market.settle_pnl_limit_factor)) + .clamp_to_i64(); + self.recurring_settle_pnl_allowance = self.recurring_settle_pnl_allowance.max(0); + + newly_realized_pnl } - fn increase_realized_trade_pnl_settle_limit( - &mut self, - newly_realized_pnl: I80F48, - limit: I80F48, - ) { - // When realized limit has a different sign from realized pnl, reset it completely - if (self.settle_pnl_limit_realized_trade > 0 && self.realized_trade_pnl_native <= 0) - || (self.settle_pnl_limit_realized_trade < 0 && self.realized_trade_pnl_native >= 0) - { - self.settle_pnl_limit_realized_trade = 0; - } + /// Returns the change in recurring settle allowance + fn apply_recurring_settle_pnl_allowance_constraint(&mut self, perp_market: &PerpMarket) -> i64 { + // deprecation/migration + self.recurring_settle_pnl_allowance = self.recurring_settle_pnl_allowance.abs(); + self.deprecated_realized_trade_pnl_native = I80F48::ZERO; - // Whenever realized pnl increases in magnitude, also increase realized pnl settle limit - // magnitude. - if newly_realized_pnl.signum() == self.realized_trade_pnl_native.signum() { - // The realized pnl settle limit change is restricted to actually realized pnl: - // buying and then selling some base lots at the same price shouldn't affect - // the settle limit. - let limit_change = if newly_realized_pnl > 0 { - newly_realized_pnl.min(limit).ceil().clamp_to_i64() - } else { - newly_realized_pnl.max(-limit).floor().clamp_to_i64() - }; - self.settle_pnl_limit_realized_trade += limit_change; - } + let before = self.recurring_settle_pnl_allowance; - // Ensure the realized limit doesn't exceed the realized pnl - self.apply_realized_trade_pnl_settle_limit_constraint(newly_realized_pnl); - } + // The recurring allowance is always >= 0 and <= stable-upnl + let upnl = self + .unsettled_pnl(perp_market, perp_market.stable_price()) + .unwrap(); + let upnl_abs = upnl.abs().ceil().to_num::(); + self.recurring_settle_pnl_allowance = + self.recurring_settle_pnl_allowance.max(0).min(upnl_abs); - /// The abs(realized pnl settle limit) should be roughly < abs(realized pnl). - /// - /// It's not always true, since realized_pnl can change with fees and funding - /// without updating the realized pnl settle limit. And rounding also breaks it. - /// - /// This function applies that constraint and deals with bookkeeping. - fn apply_realized_trade_pnl_settle_limit_constraint( - &mut self, - realized_trade_pnl_change: I80F48, - ) { - let new_limit = if self.realized_trade_pnl_native > 0 { - self.settle_pnl_limit_realized_trade - .min(self.realized_trade_pnl_native.ceil().clamp_to_i64()) - .max(0) - } else { - self.settle_pnl_limit_realized_trade - .max(self.realized_trade_pnl_native.floor().clamp_to_i64()) - .min(0) - }; - let limit_change = new_limit - self.settle_pnl_limit_realized_trade; - self.settle_pnl_limit_realized_trade = new_limit; - - // If we reduce the budget for realized pnl settling we also need to decrease the - // used-up settle amount to keep the freely settleable amount the same. - // - // Example: Settling the last remaining 50 realized pnl adds 50 to settled and brings the - // realized pnl settle budget to 0 above. That means we reduced the budget _and_ used - // up a part of it: it was double-counted. Instead bring the budget to 0 and don't increase - // settled. - // - // Example: The same thing can happen with the opposite sign. Say you have - // -50 realized pnl - // -80 pnl overall - // +-30 unrealized pnl settle limit - // -40 realized pnl settle limit - // 0 settle limit used - // -70 available settle limit - // Settling -60 would result in - // 0 realized pnl - // -20 pnl overall - // +-30 unrealized pnl settle limit - // 0 realized pnl settle limit - // -60 settle limit used - // 0 available settle limit - // Which would mean no more unrealized pnl could be settled, when -10 more should be settleable! - // This function notices the realized pnl limit_change was 40 and adjusts the settle limit: - // +-30 unrealized pnl settle limit - // 0 realized pnl settle limit - // -20 settle limit used - // -10 available settle limit - - // Sometimes realized_pnl gets reduced by non-settles such as funding or fees. - // To avoid overcorrecting, the adjustment is limited to the realized_pnl change - // passed into this function. - let realized_pnl_change = realized_trade_pnl_change.round_to_zero().clamp_to_i64(); - let used_change = if limit_change >= 0 { - limit_change.min(realized_pnl_change).max(0) - } else { - limit_change.max(realized_pnl_change).min(0) - }; - - self.settle_pnl_limit_settled_in_current_window_native += used_change; + self.recurring_settle_pnl_allowance - before } /// Change the base and quote positions as the result of a trade + /// + /// Returns realized trade pnl pub fn record_trade( &mut self, perp_market: &mut PerpMarket, base_change: i64, quote_change_native: I80F48, - ) { + ) -> I80F48 { assert_eq!(perp_market.perp_market_index, self.market_index); - self.update_trade_stats(base_change, quote_change_native, perp_market); + let realized_pnl = self.update_trade_stats(base_change, quote_change_native, perp_market); self.change_base_position(perp_market, base_change); self.change_quote_position(quote_change_native); + self.apply_recurring_settle_pnl_allowance_constraint(perp_market); + realized_pnl } fn change_quote_position(&mut self, quote_change_native: I80F48) { @@ -709,13 +655,11 @@ impl PerpPosition { /// Returns the (min_pnl, max_pnl) range of quote-native pnl that can be settled this window. /// - /// It contains contributions from three factors: - /// - a fraction of the base position stable value, which gives settlement limit - /// equally in both directions - /// - the stored realized trade settle limit, which adds an extra settlement allowance - /// in a single direction - /// - the stored realized other settle limit, which adds an extra settlement allowance - /// in a single direction + /// 1. a fraction of the base position stable value, which gives settlement limit + /// equally in both directions + /// 2. the stored recurring settle allowance, which is mostly allowance from 1. that was + /// materialized when the position was reduced (see recurring_settle_pnl_allowance) + /// 3. once-only settlement allowance in a single direction (see oneshot_settle_pnl_allowance) pub fn settle_limit(&self, market: &PerpMarket) -> (i64, i64) { assert_eq!(self.market_index, market.perp_market_index); if market.settle_pnl_limit_factor < 0.0 { @@ -726,21 +670,16 @@ impl PerpPosition { let position_value = (market.stable_price() * base_native).abs().to_num::(); let unrealized = (market.settle_pnl_limit_factor as f64 * position_value).clamp_to_i64(); - let mut min_pnl = -unrealized; - let mut max_pnl = unrealized; + let mut max_pnl = unrealized + // abs() because of potential migration + + self.recurring_settle_pnl_allowance.abs(); + let mut min_pnl = -max_pnl; - let realized_trade = self.settle_pnl_limit_realized_trade; - if realized_trade >= 0 { - max_pnl = max_pnl.saturating_add(realized_trade); + let oneshot = self.oneshot_settle_pnl_allowance; + if oneshot >= 0 { + max_pnl = max_pnl.saturating_add(oneshot.ceil().clamp_to_i64()); } else { - min_pnl = min_pnl.saturating_add(realized_trade); - }; - - let realized_other = self.realized_other_pnl_native; - if realized_other >= 0 { - max_pnl = max_pnl.saturating_add(realized_other.ceil().clamp_to_i64()); - } else { - min_pnl = min_pnl.saturating_add(realized_other.floor().clamp_to_i64()); + min_pnl = min_pnl.saturating_add(oneshot.floor().clamp_to_i64()); }; // the min/max here is just for safety @@ -784,63 +723,64 @@ impl PerpPosition { /// Update the perp position for pnl settlement /// /// If `pnl` is positive, then that is settled away, deducting from the quote position. - pub fn record_settle(&mut self, settled_pnl: I80F48) { + pub fn record_settle(&mut self, settled_pnl: I80F48, perp_market: &PerpMarket) { self.change_quote_position(-settled_pnl); - // Settlement reduces realized_other_pnl first. - // Reduction only happens if settled_pnl has the same sign as realized_other_pnl. - let other_reduction = if settled_pnl > 0 { + // Settlement reduces oneshot_settle_pnl_allowance if available. + // Reduction only happens if settled_pnl has the same sign as oneshot_settle_pnl_allowance. + let oneshot_reduction = if settled_pnl > 0 { settled_pnl - .min(self.realized_other_pnl_native) + .min(self.oneshot_settle_pnl_allowance) .max(I80F48::ZERO) } else { settled_pnl - .max(self.realized_other_pnl_native) + .max(self.oneshot_settle_pnl_allowance) .min(I80F48::ZERO) }; - self.realized_other_pnl_native -= other_reduction; - let trade_and_unrealized_settlement = settled_pnl - other_reduction; + self.oneshot_settle_pnl_allowance -= oneshot_reduction; - // Then reduces realized_trade_pnl, similar to other_pnl above. - let trade_reduction = if trade_and_unrealized_settlement > 0 { - trade_and_unrealized_settlement - .min(self.realized_trade_pnl_native) - .max(I80F48::ZERO) - } else { - trade_and_unrealized_settlement - .max(self.realized_trade_pnl_native) - .min(I80F48::ZERO) - }; - self.realized_trade_pnl_native -= trade_reduction; - - // Consume settle limit budget: We don't track consumption of realized_other_pnl - // because settling it directly reduces its budget as well. - let settled_pnl_i64 = trade_and_unrealized_settlement + // Consume settle limit budget: + // We don't track consumption of oneshot_settle_pnl_allowance because settling already + // reduces the available budget for subsequent settlesas well. + let mut used_settle_limit = (settled_pnl - oneshot_reduction) .round_to_zero() .clamp_to_i64(); - self.settle_pnl_limit_settled_in_current_window_native += settled_pnl_i64; - self.apply_realized_trade_pnl_settle_limit_constraint(-trade_reduction) + // Similarly, if the recurring budget gets reduced (because stable-upnl is lower than it), + // don't also increase settle_pnl_limit_settled_in_current_window_native. + // Example: Settle 500 on a 1000 upnl, 1000 recurring limit account: + // -> 500 upnl and 500 recurring limit, if we also had 500 settled_in_current_window + // then no more settlement would be allowed + let recurring_allowance_change = + self.apply_recurring_settle_pnl_allowance_constraint(perp_market); + if recurring_allowance_change < 0 { + if used_settle_limit > 0 { + used_settle_limit = (used_settle_limit + recurring_allowance_change).max(0); + } else { + used_settle_limit = (used_settle_limit - recurring_allowance_change).min(0); + } + } + + self.settle_pnl_limit_settled_in_current_window_native += used_settle_limit; } /// Update perp position for a maker/taker fee payment pub fn record_trading_fee(&mut self, fee: I80F48) { self.change_quote_position(-fee); - self.realized_other_pnl_native -= fee; + self.oneshot_settle_pnl_allowance -= fee; self.realized_pnl_for_position_native -= fee; } /// Adds immediately-settleable realized pnl when a liqor takes over pnl during liquidation pub fn record_liquidation_quote_change(&mut self, change: I80F48) { self.change_quote_position(change); - self.realized_other_pnl_native += change; + self.oneshot_settle_pnl_allowance += change; } /// Adds to the quote position and adds a recurring ("realized trade") settle limit pub fn record_liquidation_pnl_takeover(&mut self, change: I80F48, recurring_limit: I80F48) { self.change_quote_position(change); - self.realized_trade_pnl_native += change; - self.increase_realized_trade_pnl_settle_limit(change, recurring_limit); + self.recurring_settle_pnl_allowance += recurring_limit.abs().ceil().to_num::(); } } @@ -956,13 +896,15 @@ mod tests { let mut market = test_perp_market(10.0); let mut pos = create_perp_position(&market, 0, 0); // Go long 10 @ 10 - pos.record_trade(&mut market, 10, I80F48::from(-100)); + let realized = pos.record_trade(&mut market, 10, I80F48::from(-100)); assert_eq!(pos.quote_running_native, -100); assert_eq!(pos.avg_entry_price(&market), 10.0); assert_eq!(pos.break_even_price(&market), 10.0); - assert_eq!(pos.realized_trade_pnl_native, I80F48::from(0)); - assert_eq!(pos.realized_pnl_for_position_native, I80F48::from(0)); - assert_eq!(pos.settle_pnl_limit_realized_trade, 0); + assert_eq!(pos.realized_pnl_for_position_native, realized); + assert_eq!(realized, I80F48::ZERO); + assert_eq!(pos.oneshot_settle_pnl_allowance, I80F48::ZERO); + assert_eq!(pos.recurring_settle_pnl_allowance, 0); + assert_eq!(pos.deprecated_realized_trade_pnl_native, I80F48::from(0)); } #[test] @@ -970,13 +912,15 @@ mod tests { let mut market = test_perp_market(10.0); let mut pos = create_perp_position(&market, 0, 0); // Go short 10 @ 10 - pos.record_trade(&mut market, -10, I80F48::from(100)); + let realized = pos.record_trade(&mut market, -10, I80F48::from(100)); assert_eq!(pos.quote_running_native, 100); assert_eq!(pos.avg_entry_price(&market), 10.0); assert_eq!(pos.break_even_price(&market), 10.0); - assert_eq!(pos.realized_trade_pnl_native, I80F48::from(0)); - assert_eq!(pos.realized_pnl_for_position_native, I80F48::from(0)); - assert_eq!(pos.settle_pnl_limit_realized_trade, 0); + assert_eq!(pos.realized_pnl_for_position_native, realized); + assert_eq!(realized, I80F48::ZERO); + assert_eq!(pos.oneshot_settle_pnl_allowance, I80F48::ZERO); + assert_eq!(pos.recurring_settle_pnl_allowance, 0); + assert_eq!(pos.deprecated_realized_trade_pnl_native, I80F48::from(0)); } #[test] @@ -984,13 +928,15 @@ mod tests { let mut market = test_perp_market(10.0); let mut pos = create_perp_position(&market, 10, 10); // Go long 10 @ 30 - pos.record_trade(&mut market, 10, I80F48::from(-300)); + let realized = pos.record_trade(&mut market, 10, I80F48::from(-300)); assert_eq!(pos.quote_running_native, -400); assert_eq!(pos.avg_entry_price(&market), 20.0); assert_eq!(pos.break_even_price(&market), 20.0); - assert_eq!(pos.realized_trade_pnl_native, I80F48::from(0)); - assert_eq!(pos.realized_pnl_for_position_native, I80F48::from(0)); - assert_eq!(pos.settle_pnl_limit_realized_trade, 0); + assert_eq!(pos.realized_pnl_for_position_native, realized); + assert_eq!(realized, I80F48::ZERO); + assert_eq!(pos.oneshot_settle_pnl_allowance, I80F48::ZERO); + assert_eq!(pos.recurring_settle_pnl_allowance, 0); + assert_eq!(pos.deprecated_realized_trade_pnl_native, I80F48::from(0)); } #[test] @@ -998,13 +944,15 @@ mod tests { let mut market = test_perp_market(10.0); let mut pos = create_perp_position(&market, -10, 10); // Go short 10 @ 30 - pos.record_trade(&mut market, -10, I80F48::from(300)); + let realized = pos.record_trade(&mut market, -10, I80F48::from(300)); assert_eq!(pos.quote_running_native, 400); assert_eq!(pos.avg_entry_price(&market), 20.0); assert_eq!(pos.break_even_price(&market), 20.0); - assert_eq!(pos.realized_trade_pnl_native, I80F48::from(0)); - assert_eq!(pos.realized_pnl_for_position_native, I80F48::from(0)); - assert_eq!(pos.settle_pnl_limit_realized_trade, 0); + assert_eq!(pos.realized_pnl_for_position_native, realized); + assert_eq!(realized, I80F48::ZERO); + assert_eq!(pos.oneshot_settle_pnl_allowance, I80F48::ZERO); + assert_eq!(pos.recurring_settle_pnl_allowance, 0); + assert_eq!(pos.deprecated_realized_trade_pnl_native, I80F48::from(0)); } #[test] @@ -1012,13 +960,15 @@ mod tests { let mut market = test_perp_market(10.0); let mut pos = create_perp_position(&market, -10, 10); // Go long 5 @ 50 - pos.record_trade(&mut market, 5, I80F48::from(-250)); + let realized = pos.record_trade(&mut market, 5, I80F48::from(-250)); assert_eq!(pos.quote_running_native, -150); assert_eq!(pos.avg_entry_price(&market), 10.0); // Entry price remains the same when decreasing assert_eq!(pos.break_even_price(&market), -30.0); // The short can't break even anymore - assert_eq!(pos.realized_trade_pnl_native, I80F48::from(-200)); - assert_eq!(pos.realized_pnl_for_position_native, I80F48::from(-200)); - assert_eq!(pos.settle_pnl_limit_realized_trade, -5 * 10 / 5 - 1); + assert_eq!(pos.realized_pnl_for_position_native, realized); + assert_eq!(realized, I80F48::from(-200)); + assert_eq!(pos.oneshot_settle_pnl_allowance, I80F48::ZERO); + assert_eq!(pos.recurring_settle_pnl_allowance, 11); // 5 * 10 * 0.2 rounded up + assert_eq!(pos.deprecated_realized_trade_pnl_native, I80F48::ZERO); } #[test] @@ -1026,13 +976,15 @@ mod tests { let mut market = test_perp_market(10.0); let mut pos = create_perp_position(&market, 10, 10); // Go short 5 @ 50 - pos.record_trade(&mut market, -5, I80F48::from(250)); + let realized = pos.record_trade(&mut market, -5, I80F48::from(250)); assert_eq!(pos.quote_running_native, 150); assert_eq!(pos.avg_entry_price(&market), 10.0); // Entry price remains the same when decreasing assert_eq!(pos.break_even_price(&market), -30.0); // Already broke even - assert_eq!(pos.realized_trade_pnl_native, I80F48::from(200)); - assert_eq!(pos.realized_pnl_for_position_native, I80F48::from(200)); - assert_eq!(pos.settle_pnl_limit_realized_trade, 5 * 10 / 5 + 1); + assert_eq!(pos.realized_pnl_for_position_native, realized); + assert_eq!(realized, I80F48::from(200)); + assert_eq!(pos.oneshot_settle_pnl_allowance, I80F48::ZERO); + assert_eq!(pos.recurring_settle_pnl_allowance, 11); // 5 * 10 * 0.2 rounded up + assert_eq!(pos.deprecated_realized_trade_pnl_native, I80F48::ZERO); } #[test] @@ -1040,13 +992,15 @@ mod tests { let mut market = test_perp_market(10.0); let mut pos = create_perp_position(&market, 10, 10); // Go short 10 @ 25 - pos.record_trade(&mut market, -10, I80F48::from(250)); + let realized = pos.record_trade(&mut market, -10, I80F48::from(250)); assert_eq!(pos.quote_running_native, 0); assert_eq!(pos.avg_entry_price(&market), 0.0); // Entry price zero when no position assert_eq!(pos.break_even_price(&market), 0.0); - assert_eq!(pos.realized_trade_pnl_native, I80F48::from(150)); assert_eq!(pos.realized_pnl_for_position_native, I80F48::from(0)); - assert_eq!(pos.settle_pnl_limit_realized_trade, 10 * 10 / 5 + 1); + assert_eq!(realized, I80F48::from(150)); + assert_eq!(pos.oneshot_settle_pnl_allowance, I80F48::ZERO); + assert_eq!(pos.recurring_settle_pnl_allowance, 21); // 10 * 10 * 0.2 rounded up + assert_eq!(pos.deprecated_realized_trade_pnl_native, I80F48::ZERO); } #[test] @@ -1054,13 +1008,15 @@ mod tests { let mut market = test_perp_market(10.0); let mut pos = create_perp_position(&market, -10, 10); // Go long 10 @ 25 - pos.record_trade(&mut market, 10, I80F48::from(-250)); + let realized = pos.record_trade(&mut market, 10, I80F48::from(-250)); assert_eq!(pos.quote_running_native, 0); assert_eq!(pos.avg_entry_price(&market), 0.0); // Entry price zero when no position assert_eq!(pos.break_even_price(&market), 0.0); - assert_eq!(pos.realized_trade_pnl_native, I80F48::from(-150)); assert_eq!(pos.realized_pnl_for_position_native, I80F48::from(0)); - assert_eq!(pos.settle_pnl_limit_realized_trade, -10 * 10 / 5 - 1); + assert_eq!(realized, I80F48::from(-150)); + assert_eq!(pos.oneshot_settle_pnl_allowance, I80F48::ZERO); + assert_eq!(pos.recurring_settle_pnl_allowance, 21); // 10 * 10 * 0.2 rounded up + assert_eq!(pos.deprecated_realized_trade_pnl_native, I80F48::ZERO); } #[test] @@ -1068,13 +1024,15 @@ mod tests { let mut market = test_perp_market(10.0); let mut pos = create_perp_position(&market, 10, 10); // Go short 15 @ 20 - pos.record_trade(&mut market, -15, I80F48::from(300)); + let realized = pos.record_trade(&mut market, -15, I80F48::from(300)); assert_eq!(pos.quote_running_native, 100); assert_eq!(pos.avg_entry_price(&market), 20.0); assert_eq!(pos.break_even_price(&market), 20.0); - assert_eq!(pos.realized_trade_pnl_native, I80F48::from(100)); - assert_eq!(pos.realized_pnl_for_position_native, I80F48::from(0)); - assert_eq!(pos.settle_pnl_limit_realized_trade, 10 * 10 / 5 + 1); + assert_eq!(pos.realized_pnl_for_position_native, I80F48::ZERO); // new position + assert_eq!(realized, I80F48::from(100)); + assert_eq!(pos.oneshot_settle_pnl_allowance, I80F48::ZERO); + assert_eq!(pos.recurring_settle_pnl_allowance, 11); // 5 * 10 * 0.2 rounded up + assert_eq!(pos.deprecated_realized_trade_pnl_native, I80F48::ZERO); } #[test] @@ -1082,13 +1040,15 @@ mod tests { let mut market = test_perp_market(10.0); let mut pos = create_perp_position(&market, -10, 10); // Go long 15 @ 20 - pos.record_trade(&mut market, 15, I80F48::from(-300)); + let realized = pos.record_trade(&mut market, 15, I80F48::from(-300)); assert_eq!(pos.quote_running_native, -100); assert_eq!(pos.avg_entry_price(&market), 20.0); assert_eq!(pos.break_even_price(&market), 20.0); - assert_eq!(pos.realized_trade_pnl_native, I80F48::from(-100)); - assert_eq!(pos.realized_pnl_for_position_native, I80F48::from(0)); - assert_eq!(pos.settle_pnl_limit_realized_trade, -10 * 10 / 5 - 1); + assert_eq!(pos.realized_pnl_for_position_native, I80F48::ZERO); // new position + assert_eq!(realized, I80F48::from(-100)); + assert_eq!(pos.oneshot_settle_pnl_allowance, I80F48::ZERO); + assert_eq!(pos.recurring_settle_pnl_allowance, 11); // 5 * 10 * 0.2 rounded up + assert_eq!(pos.deprecated_realized_trade_pnl_native, I80F48::ZERO); } #[test] @@ -1096,15 +1056,21 @@ mod tests { let mut market = test_perp_market(10.0); let mut pos = create_perp_position(&market, 0, 0); // Buy 11 @ 10,000 - pos.record_trade(&mut market, 11, I80F48::from(-11 * 10_000)); + let realized_buy = pos.record_trade(&mut market, 11, I80F48::from(-11 * 10_000)); // Sell 1 @ 12,000 - pos.record_trade(&mut market, -1, I80F48::from(12_000)); + let realized_sell = pos.record_trade(&mut market, -1, I80F48::from(12_000)); assert_eq!(pos.quote_running_native, -98_000); assert_eq!(pos.base_position_lots, 10); assert_eq!(pos.break_even_price(&market), 9_800.0); // We made 2k on the trade, so we can sell our contract up to a loss of 200 each - assert_eq!(pos.realized_trade_pnl_native, I80F48::from(2_000)); - assert_eq!(pos.realized_pnl_for_position_native, I80F48::from(2_000)); - assert_eq!(pos.settle_pnl_limit_realized_trade, 1 * 10 / 5 + 1); + assert_eq!( + pos.realized_pnl_for_position_native, + realized_buy + realized_sell + ); + assert_eq!(realized_buy, I80F48::ZERO); + assert_eq!(realized_sell, I80F48::from(2_000)); + assert_eq!(pos.oneshot_settle_pnl_allowance, I80F48::ZERO); + assert_eq!(pos.recurring_settle_pnl_allowance, 3); // 1 * 10 * 0.2 rounded up + assert_eq!(pos.deprecated_realized_trade_pnl_native, I80F48::ZERO); } #[test] @@ -1114,84 +1080,91 @@ mod tests { let mut pos = create_perp_position(&market, 0, 0); // Buy 110 @ 10,000 - pos.record_trade(&mut market, 11, I80F48::from(-11 * 10 * 10_000)); + let realized_buy = pos.record_trade(&mut market, 11, I80F48::from(-11 * 10 * 10_000)); // Sell 10 @ 12,000 - pos.record_trade(&mut market, -1, I80F48::from(1 * 10 * 12_000)); + let realized_sell = pos.record_trade(&mut market, -1, I80F48::from(1 * 10 * 12_000)); assert_eq!(pos.quote_running_native, -980_000); assert_eq!(pos.base_position_lots, 10); assert_eq!(pos.avg_entry_price_per_base_lot, 100_000.0); assert_eq!(pos.avg_entry_price(&market), 10_000.0); assert_eq!(pos.break_even_price(&market), 9_800.0); - assert_eq!(pos.realized_trade_pnl_native, I80F48::from(20_000)); - assert_eq!(pos.realized_pnl_for_position_native, I80F48::from(20_000)); + + assert_eq!( + pos.realized_pnl_for_position_native, + realized_buy + realized_sell + ); + assert_eq!(realized_buy, I80F48::ZERO); + assert_eq!(realized_sell, I80F48::from(20_000)); + assert_eq!(pos.oneshot_settle_pnl_allowance, I80F48::ZERO); + assert_eq!(pos.recurring_settle_pnl_allowance, 21); // 10 * 10 * 0.2 rounded up + assert_eq!(pos.deprecated_realized_trade_pnl_native, I80F48::ZERO); } #[test] fn test_perp_realized_settle_limit_no_reduction() { - let mut market = test_perp_market(10.0); + let mut market = test_perp_market(10000.0); let mut pos = create_perp_position(&market, 0, 0); // Buy 11 @ 10,000 pos.record_trade(&mut market, 11, I80F48::from(-11 * 10_000)); // Sell 1 @ 11,000 pos.record_trade(&mut market, -1, I80F48::from(11_000)); - assert_eq!(pos.realized_trade_pnl_native, I80F48::from(1_000)); - assert_eq!(pos.realized_pnl_for_position_native, I80F48::from(1_000)); - assert_eq!(pos.settle_pnl_limit_realized_trade, 1 * 10 / 5 + 1); + assert_eq!(pos.recurring_settle_pnl_allowance, 1000); // 1 * 10000 * 0.2 rounded up, limited by upnl! - // Sell 1 @ 11,000 -- increases limit - pos.record_trade(&mut market, -1, I80F48::from(11_000)); - assert_eq!(pos.realized_trade_pnl_native, I80F48::from(2_000)); - assert_eq!(pos.realized_pnl_for_position_native, I80F48::from(2_000)); - assert_eq!(pos.settle_pnl_limit_realized_trade, 2 * (10 / 5 + 1)); + // Sell 1 @ 9,500 -- actually decreases because upnl goes down + pos.record_trade(&mut market, -1, I80F48::from(9_500)); + assert_eq!(pos.recurring_settle_pnl_allowance, 500); - // Sell 1 @ 9,000 -- a loss, but doesn't flip realized_trade_pnl_native sign, no change to limit - pos.record_trade(&mut market, -1, I80F48::from(9_000)); - assert_eq!(pos.realized_trade_pnl_native, I80F48::from(1_000)); - assert_eq!(pos.realized_pnl_for_position_native, I80F48::from(1_000)); - assert_eq!(pos.settle_pnl_limit_realized_trade, 2 * (10 / 5 + 1)); + // Sell 2 @ 20,000 each -- not limited this time + pos.record_trade(&mut market, -2, I80F48::from(40_000)); + assert_eq!(pos.recurring_settle_pnl_allowance, 4501); - // Sell 1 @ 8,000 -- flips sign, changes pnl limit + // Buy 1 @ 9,000 -- decreases allowance + pos.record_trade(&mut market, 1, I80F48::from(-9_000)); + assert_eq!(pos.recurring_settle_pnl_allowance, 2501); + + // Sell 1 @ 8,000 -- increases limit + market.stable_price_model.stable_price = 8000.0; pos.record_trade(&mut market, -1, I80F48::from(8_000)); - assert_eq!(pos.realized_trade_pnl_native, I80F48::from(-1_000)); - assert_eq!(pos.realized_pnl_for_position_native, I80F48::from(-1_000)); - assert_eq!(pos.settle_pnl_limit_realized_trade, -(1 * 10 / 5 + 1)); + assert_eq!(pos.recurring_settle_pnl_allowance, 4102); + + assert_eq!(pos.deprecated_realized_trade_pnl_native, I80F48::ZERO); } #[test] fn test_perp_trade_without_realized_pnl() { - let mut market = test_perp_market(10.0); + let mut market = test_perp_market(10_000.0); let mut pos = create_perp_position(&market, 0, 0); // Buy 11 @ 10,000 pos.record_trade(&mut market, 11, I80F48::from(-11 * 10_000)); // Sell 1 @ 10,000 - pos.record_trade(&mut market, -1, I80F48::from(10_000)); - assert_eq!(pos.realized_trade_pnl_native, I80F48::from(0)); + let realized = pos.record_trade(&mut market, -1, I80F48::from(10_000)); + assert_eq!(realized, I80F48::ZERO); assert_eq!(pos.realized_pnl_for_position_native, I80F48::from(0)); - assert_eq!(pos.settle_pnl_limit_realized_trade, 0); + assert_eq!(pos.recurring_settle_pnl_allowance, 0); // Sell 10 @ 10,000 - pos.record_trade(&mut market, -10, I80F48::from(10 * 10_000)); - assert_eq!(pos.realized_trade_pnl_native, I80F48::from(0)); + let realized = pos.record_trade(&mut market, -10, I80F48::from(10 * 10_000)); + assert_eq!(realized, I80F48::ZERO); assert_eq!(pos.realized_pnl_for_position_native, I80F48::from(0)); - assert_eq!(pos.settle_pnl_limit_realized_trade, 0); + assert_eq!(pos.recurring_settle_pnl_allowance, 0); assert_eq!(pos.base_position_lots, 0); assert_eq!(pos.quote_position_native, I80F48::ZERO); } #[test] - fn test_perp_realized_pnl_trade_other_separation() { - let mut market = test_perp_market(10.0); + fn test_perp_oneshot_settle_allowance() { + let mut market = test_perp_market(10_000.0); let mut pos = create_perp_position(&market, 0, 0); pos.record_trading_fee(I80F48::from(-70)); - assert_eq!(pos.realized_other_pnl_native, I80F48::from(70)); + assert_eq!(pos.oneshot_settle_pnl_allowance, I80F48::from(70)); pos.record_liquidation_quote_change(I80F48::from(30)); - assert_eq!(pos.realized_other_pnl_native, I80F48::from(100)); + assert_eq!(pos.oneshot_settle_pnl_allowance, I80F48::from(100)); // Buy 1 @ 10,000 pos.record_trade(&mut market, 1, I80F48::from(-1 * 10_000)); @@ -1199,10 +1172,16 @@ mod tests { // Sell 1 @ 11,000 pos.record_trade(&mut market, -1, I80F48::from(11_000)); - assert_eq!(pos.realized_other_pnl_native, I80F48::from(100)); - assert_eq!(pos.realized_trade_pnl_native, I80F48::from(1_000)); - assert_eq!(pos.realized_pnl_for_position_native, I80F48::from(0)); - assert_eq!(pos.settle_pnl_limit_realized_trade, 1 * 10 / 5 + 1); + assert_eq!(pos.oneshot_settle_pnl_allowance, I80F48::from(100)); + assert_eq!(pos.recurring_settle_pnl_allowance, 1100); // limited by upnl + + pos.record_settle(I80F48::from(50), &market); + assert_eq!(pos.oneshot_settle_pnl_allowance, I80F48::from(50)); + assert_eq!(pos.recurring_settle_pnl_allowance, 1050); + + pos.record_settle(I80F48::from(100), &market); + assert_eq!(pos.oneshot_settle_pnl_allowance, I80F48::from(0)); + assert_eq!(pos.recurring_settle_pnl_allowance, 950); } #[test] @@ -1217,20 +1196,19 @@ mod tests { pos.record_trade(&mut market, 2, I80F48::from(-2 * 2)); assert!((pos.avg_entry_price(&market) - 1.66666).abs() < 0.001); - assert_eq!(pos.realized_trade_pnl_native, I80F48::from(0)); // Sell 2 @ 4 - pos.record_trade(&mut market, -2, I80F48::from(2 * 4)); + let realized1 = pos.record_trade(&mut market, -2, I80F48::from(2 * 4)); assert!((pos.avg_entry_price(&market) - 1.66666).abs() < 0.001); - assert!((pos.realized_trade_pnl_native.to_num::() - 4.6666).abs() < 0.01); + assert!((realized1.to_num::() - 4.6666).abs() < 0.01); // Sell 1 @ 2 - pos.record_trade(&mut market, -1, I80F48::from(2)); + let realized2 = pos.record_trade(&mut market, -1, I80F48::from(2)); assert_eq!(pos.avg_entry_price(&market), 0.0); assert!((pos.quote_position_native.to_num::() - 5.1).abs() < 0.001); - assert!((pos.realized_trade_pnl_native.to_num::() - 5.1).abs() < 0.01); + assert!((realized2.to_num::() - 0.3333).abs() < 0.01); } #[test] @@ -1288,94 +1266,67 @@ mod tests { } #[test] - fn test_perp_realized_pnl_consumption() { + fn test_perp_settle_limit_allowance_consumption() { let market = test_perp_market(10.0); let mut pos = create_perp_position(&market, 0, 0); - assert_eq!(pos.realized_trade_pnl_native, I80F48::from(0)); - pos.settle_pnl_limit_realized_trade = 1000; - pos.realized_trade_pnl_native = I80F48::from(1500); - pos.record_settle(I80F48::from(10)); - assert_eq!(pos.realized_trade_pnl_native, I80F48::from(1490)); - assert_eq!(pos.settle_pnl_limit_realized_trade, 1000); + // setup some upnl so the recurring allowance isn't reduced immediately + pos.quote_position_native = I80F48::from(1100); + + pos.recurring_settle_pnl_allowance = 1000; + pos.record_settle(I80F48::from(10), &market); + assert_eq!(pos.recurring_settle_pnl_allowance, 1000); assert_eq!(pos.settle_pnl_limit_settled_in_current_window_native, 10); - pos.record_settle(I80F48::from(-2)); - assert_eq!(pos.realized_trade_pnl_native, I80F48::from(1490)); - assert_eq!(pos.settle_pnl_limit_realized_trade, 1000); + pos.record_settle(I80F48::from(-2), &market); + assert_eq!(pos.recurring_settle_pnl_allowance, 1000); assert_eq!(pos.settle_pnl_limit_settled_in_current_window_native, 8); - pos.record_settle(I80F48::from(1100)); - assert_eq!(pos.realized_trade_pnl_native, I80F48::from(390)); - assert_eq!(pos.settle_pnl_limit_realized_trade, 390); + pos.record_settle(I80F48::from(492), &market); + assert_eq!(pos.recurring_settle_pnl_allowance, 600); assert_eq!( pos.settle_pnl_limit_settled_in_current_window_native, - 8 + 1100 - (1000 - 390) + 8 + 492 - 400 ); - pos.settle_pnl_limit_realized_trade = 4; pos.settle_pnl_limit_settled_in_current_window_native = 0; - pos.realized_trade_pnl_native = I80F48::from(5); + pos.recurring_settle_pnl_allowance = 0; + pos.oneshot_settle_pnl_allowance = I80F48::from(4); assert_eq!(pos.available_settle_limit(&market), (0, 4)); - pos.record_settle(I80F48::from(-20)); - assert_eq!(pos.realized_trade_pnl_native, I80F48::from(5)); - assert_eq!(pos.settle_pnl_limit_realized_trade, 4); + pos.record_settle(I80F48::from(-20), &market); assert_eq!(pos.settle_pnl_limit_settled_in_current_window_native, -20); + assert_eq!(pos.oneshot_settle_pnl_allowance, I80F48::from(4)); assert_eq!(pos.available_settle_limit(&market), (0, 24)); - pos.record_settle(I80F48::from(2)); - assert_eq!(pos.realized_trade_pnl_native, I80F48::from(3)); - assert_eq!(pos.settle_pnl_limit_realized_trade, 3); - assert_eq!(pos.settle_pnl_limit_settled_in_current_window_native, -19); + pos.record_settle(I80F48::from(2), &market); + assert_eq!(pos.settle_pnl_limit_settled_in_current_window_native, -20); + assert_eq!(pos.oneshot_settle_pnl_allowance, I80F48::from(2)); assert_eq!(pos.available_settle_limit(&market), (0, 22)); - pos.record_settle(I80F48::from(10)); - assert_eq!(pos.realized_trade_pnl_native, I80F48::from(0)); - assert_eq!(pos.settle_pnl_limit_realized_trade, 0); - assert_eq!(pos.settle_pnl_limit_settled_in_current_window_native, -12); - assert_eq!(pos.available_settle_limit(&market), (0, 12)); + pos.record_settle(I80F48::from(4), &market); + assert_eq!(pos.settle_pnl_limit_settled_in_current_window_native, -18); + assert_eq!(pos.oneshot_settle_pnl_allowance, I80F48::from(0)); + assert_eq!(pos.available_settle_limit(&market), (0, 18)); - pos.realized_trade_pnl_native = I80F48::from(-5); - pos.settle_pnl_limit_realized_trade = -4; pos.settle_pnl_limit_settled_in_current_window_native = 0; - pos.record_settle(I80F48::from(20)); - assert_eq!(pos.realized_trade_pnl_native, I80F48::from(-5)); - assert_eq!(pos.settle_pnl_limit_realized_trade, -4); + pos.recurring_settle_pnl_allowance = 0; + pos.oneshot_settle_pnl_allowance = I80F48::from(-4); + assert_eq!(pos.available_settle_limit(&market), (-4, 0)); + pos.record_settle(I80F48::from(20), &market); assert_eq!(pos.settle_pnl_limit_settled_in_current_window_native, 20); + assert_eq!(pos.oneshot_settle_pnl_allowance, I80F48::from(-4)); + assert_eq!(pos.available_settle_limit(&market), (-24, 0)); - pos.record_settle(I80F48::from(-2)); - assert_eq!(pos.realized_trade_pnl_native, I80F48::from(-3)); - assert_eq!(pos.settle_pnl_limit_realized_trade, -3); - assert_eq!(pos.settle_pnl_limit_settled_in_current_window_native, 19); + pos.record_settle(I80F48::from(-2), &market); + assert_eq!(pos.settle_pnl_limit_settled_in_current_window_native, 20); + assert_eq!(pos.oneshot_settle_pnl_allowance, I80F48::from(-2)); + assert_eq!(pos.available_settle_limit(&market), (-22, 0)); - pos.record_settle(I80F48::from(-10)); - assert_eq!(pos.realized_trade_pnl_native, I80F48::from(0)); - assert_eq!(pos.settle_pnl_limit_realized_trade, 0); - assert_eq!(pos.settle_pnl_limit_settled_in_current_window_native, 12); - - pos.realized_other_pnl_native = I80F48::from(10); - pos.realized_trade_pnl_native = I80F48::from(25); - pos.settle_pnl_limit_realized_trade = 20; - pos.record_settle(I80F48::from(1)); - assert_eq!(pos.realized_other_pnl_native, I80F48::from(9)); - assert_eq!(pos.realized_trade_pnl_native, I80F48::from(25)); - assert_eq!(pos.settle_pnl_limit_realized_trade, 20); - assert_eq!(pos.settle_pnl_limit_settled_in_current_window_native, 12); - - pos.record_settle(I80F48::from(10)); - assert_eq!(pos.realized_other_pnl_native, I80F48::from(0)); - assert_eq!(pos.realized_trade_pnl_native, I80F48::from(24)); - assert_eq!(pos.settle_pnl_limit_realized_trade, 20); - assert_eq!(pos.settle_pnl_limit_settled_in_current_window_native, 13); - - pos.realized_other_pnl_native = I80F48::from(-10); - pos.realized_trade_pnl_native = I80F48::from(-25); - pos.settle_pnl_limit_realized_trade = -20; - pos.record_settle(I80F48::from(-1)); - assert_eq!(pos.realized_other_pnl_native, I80F48::from(-9)); - assert_eq!(pos.realized_trade_pnl_native, I80F48::from(-25)); - assert_eq!(pos.settle_pnl_limit_realized_trade, -20); + pos.record_settle(I80F48::from(-4), &market); + assert_eq!(pos.settle_pnl_limit_settled_in_current_window_native, 18); + assert_eq!(pos.oneshot_settle_pnl_allowance, I80F48::from(0)); + assert_eq!(pos.available_settle_limit(&market), (-18, 0)); } #[test] @@ -1411,94 +1362,45 @@ mod tests { let mut market = test_perp_market(0.5); let mut pos = create_perp_position(&market, 100, 1); - pos.realized_trade_pnl_native = I80F48::from(60); // no effect let limited_pnl = |pos: &PerpPosition, market: &PerpMarket, pnl: i64| { pos.apply_pnl_settle_limit(market, I80F48::from(pnl)) .to_num::() }; - pos.settle_pnl_limit_realized_trade = 5; - assert_eq!(pos.available_settle_limit(&market), (-10, 15)); // 0.2 factor * 0.5 stable price * 100 lots + 5 realized + assert_eq!(pos.available_settle_limit(&market), (-10, 10)); // 0.2 factor * 0.5 stable price * 100 lots + assert_eq!(limited_pnl(&pos, &market, 100), 10.0); + assert_eq!(limited_pnl(&pos, &market, -100), -10.0); + + pos.oneshot_settle_pnl_allowance = I80F48::from_num(-5); + assert_eq!(pos.available_settle_limit(&market), (-15, 10)); + assert_eq!(limited_pnl(&pos, &market, 100), 10.0); + assert_eq!(limited_pnl(&pos, &market, -100), -15.0); + + pos.oneshot_settle_pnl_allowance = I80F48::from_num(5); + assert_eq!(pos.available_settle_limit(&market), (-10, 15)); assert_eq!(limited_pnl(&pos, &market, 100), 15.0); assert_eq!(limited_pnl(&pos, &market, -100), -10.0); - pos.settle_pnl_limit_settled_in_current_window_native = 2; - assert_eq!(pos.available_settle_limit(&market), (-12, 13)); - assert_eq!(limited_pnl(&pos, &market, 100), 13.0); - assert_eq!(limited_pnl(&pos, &market, -100), -12.0); + pos.recurring_settle_pnl_allowance = 11; + assert_eq!(pos.available_settle_limit(&market), (-21, 26)); + assert_eq!(limited_pnl(&pos, &market, 100), 26.0); + assert_eq!(limited_pnl(&pos, &market, -100), -21.0); - pos.settle_pnl_limit_settled_in_current_window_native = 16; - assert_eq!(pos.available_settle_limit(&market), (-26, 0)); + pos.settle_pnl_limit_settled_in_current_window_native = 17; + assert_eq!(pos.available_settle_limit(&market), (-38, 9)); - pos.settle_pnl_limit_settled_in_current_window_native = -16; - assert_eq!(pos.available_settle_limit(&market), (0, 31)); + pos.settle_pnl_limit_settled_in_current_window_native = 27; + assert_eq!(pos.available_settle_limit(&market), (-48, 0)); - pos.settle_pnl_limit_realized_trade = 0; - pos.settle_pnl_limit_settled_in_current_window_native = 2; - assert_eq!(pos.available_settle_limit(&market), (-12, 8)); + pos.settle_pnl_limit_settled_in_current_window_native = -17; + assert_eq!(pos.available_settle_limit(&market), (-4, 43)); - pos.settle_pnl_limit_settled_in_current_window_native = -2; - assert_eq!(pos.available_settle_limit(&market), (-8, 12)); + pos.settle_pnl_limit_settled_in_current_window_native = -27; + assert_eq!(pos.available_settle_limit(&market), (0, 53)); + pos.settle_pnl_limit_settled_in_current_window_native = 0; market.stable_price_model.stable_price = 1.0; - assert_eq!(pos.available_settle_limit(&market), (-18, 22)); - - pos.settle_pnl_limit_realized_trade = 1000; - pos.settle_pnl_limit_settled_in_current_window_native = 2; - assert_eq!(pos.available_settle_limit(&market), (-22, 1018)); - - pos.realized_other_pnl_native = I80F48::from(5); - assert_eq!(pos.available_settle_limit(&market), (-22, 1023)); - - pos.realized_other_pnl_native = I80F48::from(-5); - assert_eq!(pos.available_settle_limit(&market), (-27, 1018)); - } - - #[test] - fn test_perp_reduced_realized_pnl_settle_limit() { - let market = test_perp_market(0.5); - let mut pos = create_perp_position(&market, 100, 1); - - let cases = vec![ - // No change if realized > limit - (0, (100, 50, 70, -200), (50, 70)), - // No change if realized > limit - (1, (100, 50, 70, 200), (50, 70)), - // No change if abs(realized) > abs(limit) - (2, (-100, -50, 70, -200), (-50, 70)), - // No change if abs(realized) > abs(limit) - (3, (-100, -50, 70, 200), (-50, 70)), - // reduction limited by realized change - (4, (40, 50, 70, -5), (40, 65)), - // reduction max - (5, (40, 50, 70, -15), (40, 60)), - // reduction, with realized change wrong direction - (6, (40, 50, 70, 15), (40, 70)), - // reduction limited by realized change - (7, (-40, -50, -70, 5), (-40, -65)), - // reduction max - (8, (-40, -50, -70, 15), (-40, -60)), - // reduction, with realized change wrong direction - (9, (-40, -50, -70, -15), (-40, -70)), - // reduction when used amount is opposite sign - (10, (-40, -50, 70, -15), (-40, 70)), - // reduction when used amount is opposite sign - (11, (-40, -50, 70, 15), (-40, 80)), - ]; - - for (i, (realized, realized_limit, used, change), (expected_limit, expected_used)) in cases - { - println!("test case {i}"); - pos.realized_trade_pnl_native = I80F48::from(realized); - pos.settle_pnl_limit_realized_trade = realized_limit; - pos.settle_pnl_limit_settled_in_current_window_native = used; - pos.apply_realized_trade_pnl_settle_limit_constraint(I80F48::from(change)); - assert_eq!(pos.settle_pnl_limit_realized_trade, expected_limit); - assert_eq!( - pos.settle_pnl_limit_settled_in_current_window_native, - expected_used - ); - } + assert_eq!(pos.available_settle_limit(&market), (-31, 36)); } } diff --git a/programs/mango-v4/tests/cases/test_liq_perps_bankruptcy.rs b/programs/mango-v4/tests/cases/test_liq_perps_bankruptcy.rs index d89980821..e3bb11f08 100644 --- a/programs/mango-v4/tests/cases/test_liq_perps_bankruptcy.rs +++ b/programs/mango-v4/tests/cases/test_liq_perps_bankruptcy.rs @@ -112,8 +112,8 @@ async fn test_liq_perps_bankruptcy() -> Result<(), TransportError> { taker_fee: 0.0, group_insurance_fund: true, // adjust this factur such that we get the desired settle limit in the end - settle_pnl_limit_factor: (settle_limit as f32 + 0.1).min(0.0) - / (-1.0 * 100.0 * adj_price) as f32, + settle_pnl_limit_factor: (settle_limit as f32 - 0.1).max(0.0) + / (1.0 * 100.0 * adj_price) as f32, settle_pnl_limit_window_size_ts: 24 * 60 * 60, ..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, base_token).await }, @@ -227,7 +227,7 @@ async fn test_liq_perps_bankruptcy() -> Result<(), TransportError> { let account_data = solana.get_account::(account).await; assert_eq!(account_data.perps[0].quote_position_native(), pnl); assert_eq!( - account_data.perps[0].settle_pnl_limit_realized_trade, + account_data.perps[0].recurring_settle_pnl_allowance, settle_limit ); assert_eq!( @@ -277,7 +277,7 @@ async fn test_liq_perps_bankruptcy() -> Result<(), TransportError> { }; { - let (perp_market, account, liqor) = setup_perp(-28, -50, -10).await; + let (perp_market, account, liqor) = setup_perp(-28, -50, 10).await; let liqor_quote_before = account_position(solana, liqor, quote_token.bank).await; send_tx( @@ -310,7 +310,7 @@ async fn test_liq_perps_bankruptcy() -> Result<(), TransportError> { } { - let (perp_market, account, liqor) = setup_perp(-28, -50, -10).await; + let (perp_market, account, liqor) = setup_perp(-28, -50, 10).await; fund_insurance(2).await; let liqor_quote_before = account_position(solana, liqor, quote_token.bank).await; @@ -348,7 +348,7 @@ async fn test_liq_perps_bankruptcy() -> Result<(), TransportError> { } { - let (perp_market, account, liqor) = setup_perp(-28, -50, -10).await; + let (perp_market, account, liqor) = setup_perp(-28, -50, 10).await; fund_insurance(5).await; send_tx( @@ -371,7 +371,7 @@ async fn test_liq_perps_bankruptcy() -> Result<(), TransportError> { // no insurance { - let (perp_market, account, liqor) = setup_perp(-28, -50, -10).await; + let (perp_market, account, liqor) = setup_perp(-28, -50, 10).await; send_tx( solana, @@ -390,7 +390,7 @@ async fn test_liq_perps_bankruptcy() -> Result<(), TransportError> { // no settlement: no settle health { - let (perp_market, account, liqor) = setup_perp(-200, -50, -10).await; + let (perp_market, account, liqor) = setup_perp(-200, -50, 10).await; fund_insurance(5).await; send_tx( @@ -430,7 +430,7 @@ async fn test_liq_perps_bankruptcy() -> Result<(), TransportError> { // no socialized loss: fully covered by insurance fund { - let (perp_market, account, liqor) = setup_perp(-40, -50, -5).await; + let (perp_market, account, liqor) = setup_perp(-40, -50, 5).await; fund_insurance(42).await; send_tx( diff --git a/programs/mango-v4/tests/cases/test_liq_perps_base_and_bankruptcy.rs b/programs/mango-v4/tests/cases/test_liq_perps_base_and_bankruptcy.rs index 9381e9249..2a0c4ee8e 100644 --- a/programs/mango-v4/tests/cases/test_liq_perps_base_and_bankruptcy.rs +++ b/programs/mango-v4/tests/cases/test_liq_perps_base_and_bankruptcy.rs @@ -230,12 +230,12 @@ async fn test_liq_perps_base_and_bankruptcy() -> Result<(), TransportError> { 0.1 )); assert!(assert_equal( - liqee_data.perps[0].realized_trade_pnl_native, + liqee_data.perps[0].realized_pnl_for_position_native, liqee_amount - 1000.0, 0.1 )); // stable price is 1.0, so 0.2 * 1000 - assert_eq!(liqee_data.perps[0].settle_pnl_limit_realized_trade, -201); + assert_eq!(liqee_data.perps[0].recurring_settle_pnl_allowance, 201); assert!(assert_equal( perp_market_after.fees_accrued - perp_market_before.fees_accrued, liqor_amount - liqee_amount, @@ -521,7 +521,7 @@ async fn test_liq_perps_base_and_bankruptcy() -> Result<(), TransportError> { send_tx( solana, TokenWithdrawInstruction { - amount: liqee_quote_deposits_before as u64 - 100, + amount: liqee_quote_deposits_before as u64 - 200, allow_borrow: false, account: account_1, owner, @@ -572,9 +572,9 @@ async fn test_liq_perps_base_and_bankruptcy() -> Result<(), TransportError> { 0.1 )); assert!(assert_equal( - liqor_data.tokens[0].native(&settle_bank), - liqor_before.tokens[0].native(&settle_bank).to_num::() - - liqee_settle_limit_before as f64 * 100.0, // 100 is base lot size + liqor_data.tokens[1].native(&settle_bank), + liqor_before.tokens[1].native(&settle_bank).to_num::() + - liqee_settle_limit_before as f64, 0.1 )); diff --git a/programs/mango-v4/tests/cases/test_perp_settle.rs b/programs/mango-v4/tests/cases/test_perp_settle.rs index 06aaa6c06..cc3fbb956 100644 --- a/programs/mango-v4/tests/cases/test_perp_settle.rs +++ b/programs/mango-v4/tests/cases/test_perp_settle.rs @@ -1100,14 +1100,6 @@ async fn test_perp_pnl_settle_limit() -> Result<(), TransportError> { let mango_account_0 = solana.get_account::(account_0).await; let mango_account_1 = solana.get_account::(account_1).await; - assert_eq!( - mango_account_0.perps[0].realized_trade_pnl_native, - I80F48::from(200_000 - 80_000) - ); - assert_eq!( - mango_account_1.perps[0].realized_trade_pnl_native, - I80F48::from(-200_000 + 80_000) - ); // neither account has any settle limit left (check for 1 because of the ceil()ing) assert_eq!( mango_account_0.perps[0].available_settle_limit(&market).1, @@ -1119,7 +1111,7 @@ async fn test_perp_pnl_settle_limit() -> Result<(), TransportError> { ); // check that realized pnl settle limit was set up correctly assert_eq!( - mango_account_0.perps[0].settle_pnl_limit_realized_trade, + mango_account_0.perps[0].recurring_settle_pnl_allowance, (0.8 * 1.0 * 100.0 * 1000.0) as i64 + 1 ); // +1 just for rounding @@ -1152,7 +1144,7 @@ async fn test_perp_pnl_settle_limit() -> Result<(), TransportError> { // This time account 0's realized pnl settle limit kicks in. // let account_1_quote_before = mango_account_1.perps[0].quote_position_native(); - let account_0_realized_limit = mango_account_0.perps[0].settle_pnl_limit_realized_trade; + let account_0_realized_limit = mango_account_0.perps[0].recurring_settle_pnl_allowance; send_tx( solana, @@ -1186,12 +1178,13 @@ async fn test_perp_pnl_settle_limit() -> Result<(), TransportError> { mango_account_1.perps[0].quote_position_native() - account_1_quote_before, I80F48::from(account_0_realized_limit) ); - // account0's limit gets reduced to the realized pnl amount left over + // account0's limit gets reduced to the pnl amount left over + let perp_market_data = solana.get_account::(perp_market).await; assert_eq!( - mango_account_0.perps[0].settle_pnl_limit_realized_trade, + mango_account_0.perps[0].recurring_settle_pnl_allowance, mango_account_0.perps[0] - .realized_trade_pnl_native - .to_num::() + .unsettled_pnl(&perp_market_data, I80F48::from_num(1.0)) + .unwrap() ); // can't settle again @@ -1213,7 +1206,7 @@ async fn test_perp_pnl_settle_limit() -> Result<(), TransportError> { // let account_1_quote_before = mango_account_1.perps[0].quote_position_native(); - let account_0_realized_limit = mango_account_0.perps[0].settle_pnl_limit_realized_trade; + let account_0_realized_limit = mango_account_0.perps[0].recurring_settle_pnl_allowance; send_tx( solana, @@ -1248,13 +1241,13 @@ async fn test_perp_pnl_settle_limit() -> Result<(), TransportError> { I80F48::from(account_0_realized_limit) ); // account0's limit gets reduced to the realized pnl amount left over - assert_eq!(mango_account_0.perps[0].settle_pnl_limit_realized_trade, 0); + assert_eq!(mango_account_0.perps[0].recurring_settle_pnl_allowance, 0); assert_eq!( - mango_account_0.perps[0].realized_trade_pnl_native, + mango_account_0.perps[0].realized_pnl_for_position_native, I80F48::from(0) ); assert_eq!( - mango_account_1.perps[0].realized_trade_pnl_native, + mango_account_1.perps[0].realized_pnl_for_position_native, I80F48::from(0) ); diff --git a/ts/client/src/accounts/mangoAccount.ts b/ts/client/src/accounts/mangoAccount.ts index b26f84ffd..656268ff6 100644 --- a/ts/client/src/accounts/mangoAccount.ts +++ b/ts/client/src/accounts/mangoAccount.ts @@ -1325,9 +1325,9 @@ export class PerpPosition { dto.takerVolume, dto.perpSpotTransfers, dto.avgEntryPricePerBaseLot, - I80F48.from(dto.realizedTradePnlNative), - I80F48.from(dto.realizedOtherPnlNative), - dto.settlePnlLimitRealizedTrade, + I80F48.from(dto.deprecatedRealizedTradePnlNative), + I80F48.from(dto.oneshotSettlePnlAllowance), + dto.recurringSettlePnlAllowance, I80F48.from(dto.realizedPnlForPositionNative), ); } @@ -1380,9 +1380,9 @@ export class PerpPosition { public takerVolume: BN, public perpSpotTransfers: BN, public avgEntryPricePerBaseLot: number, - public realizedTradePnlNative: I80F48, - public realizedOtherPnlNative: I80F48, - public settlePnlLimitRealizedTrade: BN, + public deprecatedRealizedTradePnlNative: I80F48, + public oneshotSettlePnlAllowance: I80F48, + public recurringSettlePnlAllowance: BN, public realizedPnlForPositionNative: I80F48, ) {} @@ -1636,28 +1636,25 @@ export class PerpPosition { .mul(baseNative) .toNumber(); const unrealized = new BN(perpMarket.settlePnlLimitFactor * positionValue); + + let maxPnl = unrealized.add(this.recurringSettlePnlAllowance.abs()); + let minPnl = maxPnl.neg(); + + const oneshot = this.oneshotSettlePnlAllowance; + if (!oneshot.isNeg()) { + maxPnl = maxPnl.add(new BN(oneshot.ceil().toNumber())); + } else { + minPnl = minPnl.add(new BN(oneshot.floor().toNumber())); + } + const used = new BN( this.settlePnlLimitSettledInCurrentWindowNative.toNumber(), ); - let minPnl = unrealized.neg().sub(used); - let maxPnl = unrealized.sub(used); + const availableMin = BN.min(minPnl.sub(used), new BN(0)); + const availableMax = BN.max(maxPnl.sub(used), new BN(0)); - const realizedTrade = this.settlePnlLimitRealizedTrade; - if (realizedTrade.gte(new BN(0))) { - maxPnl = maxPnl.add(realizedTrade); - } else { - minPnl = minPnl.add(realizedTrade); - } - - const realizedOther = new BN(this.realizedOtherPnlNative.toNumber()); - if (realizedOther.gte(new BN(0))) { - maxPnl = maxPnl.add(realizedOther); - } else { - minPnl = minPnl.add(realizedOther); - } - - return [BN.min(minPnl, new BN(0)), BN.max(maxPnl, new BN(0))]; + return [availableMin, availableMax]; } public applyPnlSettleLimit(pnl: I80F48, perpMarket: PerpMarket): I80F48 { @@ -1782,8 +1779,10 @@ export class PerpPosition { this.getNotionalValueUi(perpMarket!).toString() + ', cumulative pnl over position lifetime ui - ' + this.cumulativePnlOverPositionLifetimeUi(perpMarket!).toString() + - ', realized other pnl native ui - ' + - toUiDecimalsForQuote(this.realizedOtherPnlNative) + + ', oneshot settleable native ui - ' + + toUiDecimalsForQuote(this.oneshotSettlePnlAllowance) + + ', recurring settleable native ui - ' + + toUiDecimalsForQuote(this.recurringSettlePnlAllowance) + ', cumulative long funding ui - ' + toUiDecimalsForQuote(this.cumulativeLongFunding) + ', cumulative short funding ui - ' + @@ -1812,9 +1811,9 @@ export class PerpPositionDto { public takerVolume: BN, public perpSpotTransfers: BN, public avgEntryPricePerBaseLot: number, - public realizedTradePnlNative: I80F48Dto, - public realizedOtherPnlNative: I80F48Dto, - public settlePnlLimitRealizedTrade: BN, + public deprecatedRealizedTradePnlNative: I80F48Dto, + public oneshotSettlePnlAllowance: I80F48Dto, + public recurringSettlePnlAllowance: BN, public realizedPnlForPositionNative: I80F48Dto, ) {} } diff --git a/ts/client/src/mango_v4.ts b/ts/client/src/mango_v4.ts index 61243c5ca..463d43468 100644 --- a/ts/client/src/mango_v4.ts +++ b/ts/client/src/mango_v4.ts @@ -9334,36 +9334,44 @@ export type MangoV4 = { "type": "f64" }, { - "name": "realizedTradePnlNative", + "name": "deprecatedRealizedTradePnlNative", "docs": [ - "Amount of pnl that was realized by bringing the base position closer to 0.", - "", - "The settlement of this type of pnl is limited by settle_pnl_limit_realized_trade.", - "Settling pnl reduces this value once other_pnl below is exhausted." + "Deprecated field: Amount of pnl that was realized by bringing the base position closer to 0." ], "type": { "defined": "I80F48" } }, { - "name": "realizedOtherPnlNative", + "name": "oneshotSettlePnlAllowance", "docs": [ - "Amount of pnl realized from fees, funding and liquidation.", + "Amount of pnl that can be settled once.", "", - "This type of realized pnl is always settleable.", - "Settling pnl reduces this value first." + "- The value is signed: a negative number means negative pnl can be settled.", + "- A settlement in the right direction will decrease this amount.", + "", + "Typically added for fees, funding and liquidation." ], "type": { "defined": "I80F48" } }, { - "name": "settlePnlLimitRealizedTrade", + "name": "recurringSettlePnlAllowance", "docs": [ - "Settle limit contribution from realized pnl.", + "Amount of pnl that can be settled in each settle window.", "", - "Every time pnl is realized, this is increased by a fraction of the stable", - "value of the realization. It magnitude decreases when realized pnl drops below its value." + "- Unsigned, the settlement can happen in both directions. Value is >= 0.", + "- Previously stored a similar value that was signed, so in migration cases", + "this value can be negative and should be .abs()ed.", + "- If this value exceeds the current stable-upnl, it should be decreased,", + "see apply_recurring_settle_pnl_allowance_constraint()", + "", + "When the base position is reduced, the settle limit contribution from the reduced", + "base position is materialized into this value. When the base position increases,", + "some of the allowance is taken away.", + "", + "This also gets increased when a liquidator takes over pnl." ], "type": "i64" }, @@ -23360,36 +23368,44 @@ export const IDL: MangoV4 = { "type": "f64" }, { - "name": "realizedTradePnlNative", + "name": "deprecatedRealizedTradePnlNative", "docs": [ - "Amount of pnl that was realized by bringing the base position closer to 0.", - "", - "The settlement of this type of pnl is limited by settle_pnl_limit_realized_trade.", - "Settling pnl reduces this value once other_pnl below is exhausted." + "Deprecated field: Amount of pnl that was realized by bringing the base position closer to 0." ], "type": { "defined": "I80F48" } }, { - "name": "realizedOtherPnlNative", + "name": "oneshotSettlePnlAllowance", "docs": [ - "Amount of pnl realized from fees, funding and liquidation.", + "Amount of pnl that can be settled once.", "", - "This type of realized pnl is always settleable.", - "Settling pnl reduces this value first." + "- The value is signed: a negative number means negative pnl can be settled.", + "- A settlement in the right direction will decrease this amount.", + "", + "Typically added for fees, funding and liquidation." ], "type": { "defined": "I80F48" } }, { - "name": "settlePnlLimitRealizedTrade", + "name": "recurringSettlePnlAllowance", "docs": [ - "Settle limit contribution from realized pnl.", + "Amount of pnl that can be settled in each settle window.", "", - "Every time pnl is realized, this is increased by a fraction of the stable", - "value of the realization. It magnitude decreases when realized pnl drops below its value." + "- Unsigned, the settlement can happen in both directions. Value is >= 0.", + "- Previously stored a similar value that was signed, so in migration cases", + "this value can be negative and should be .abs()ed.", + "- If this value exceeds the current stable-upnl, it should be decreased,", + "see apply_recurring_settle_pnl_allowance_constraint()", + "", + "When the base position is reduced, the settle limit contribution from the reduced", + "base position is materialized into this value. When the base position increases,", + "some of the allowance is taken away.", + "", + "This also gets increased when a liquidator takes over pnl." ], "type": "i64" },