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
This commit is contained in:
Christian Kamm 2024-02-01 11:23:45 +01:00 committed by GitHub
parent 719aee37ae
commit ae5907ba3a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 454 additions and 545 deletions

View File

@ -9334,36 +9334,44 @@
"type": "f64" "type": "f64"
}, },
{ {
"name": "realizedTradePnlNative", "name": "deprecatedRealizedTradePnlNative",
"docs": [ "docs": [
"Amount of pnl that was realized by bringing the base position closer to 0.", "Deprecated field: 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."
], ],
"type": { "type": {
"defined": "I80F48" "defined": "I80F48"
} }
}, },
{ {
"name": "realizedOtherPnlNative", "name": "oneshotSettlePnlAllowance",
"docs": [ "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.", "- The value is signed: a negative number means negative pnl can be settled.",
"Settling pnl reduces this value first." "- A settlement in the right direction will decrease this amount.",
"",
"Typically added for fees, funding and liquidation."
], ],
"type": { "type": {
"defined": "I80F48" "defined": "I80F48"
} }
}, },
{ {
"name": "settlePnlLimitRealizedTrade", "name": "recurringSettlePnlAllowance",
"docs": [ "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", "- Unsigned, the settlement can happen in both directions. Value is >= 0.",
"value of the realization. It magnitude decreases when realized pnl drops below its value." "- 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" "type": "i64"
}, },

View File

@ -74,40 +74,37 @@ pub fn perp_consume_events(ctx: Context<PerpConsumeEvents>, limit: usize) -> Res
group, group,
event_queue event_queue
); );
let before_pnl = maker_taker let maker_realized_pnl = maker_taker.execute_perp_maker(
.perp_position(perp_market_index)?
.realized_trade_pnl_native;
maker_taker.execute_perp_maker(
perp_market_index, perp_market_index,
&mut perp_market, &mut perp_market,
fill, fill,
&group, &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( emit_perp_balances(
group_key, group_key,
fill.maker, fill.maker,
maker_taker.perp_position(perp_market_index).unwrap(), maker_taker.perp_position(perp_market_index).unwrap(),
&perp_market, &perp_market,
); );
let after_pnl = maker_taker let closed_pnl = maker_realized_pnl + taker_realized_pnl;
.perp_position(perp_market_index)?
.realized_trade_pnl_native;
let closed_pnl = after_pnl - before_pnl;
(closed_pnl, closed_pnl) (closed_pnl, closed_pnl)
} else { } else {
load_mango_account!(maker, fill.maker, mango_account_ais, group, event_queue); load_mango_account!(maker, fill.maker, mango_account_ais, group, event_queue);
load_mango_account!(taker, fill.taker, mango_account_ais, group, event_queue); load_mango_account!(taker, fill.taker, mango_account_ais, group, event_queue);
let maker_before_pnl = maker let maker_realized_pnl = maker.execute_perp_maker(
.perp_position(perp_market_index)? perp_market_index,
.realized_trade_pnl_native; &mut perp_market,
let taker_before_pnl = taker fill,
.perp_position(perp_market_index)? &group,
.realized_trade_pnl_native; )?;
let taker_realized_pnl =
maker.execute_perp_maker(perp_market_index, &mut perp_market, fill, &group)?; taker.execute_perp_taker(perp_market_index, &mut perp_market, fill)?;
taker.execute_perp_taker(perp_market_index, &mut perp_market, fill)?;
emit_perp_balances( emit_perp_balances(
group_key, group_key,
fill.maker, fill.maker,
@ -120,16 +117,8 @@ pub fn perp_consume_events(ctx: Context<PerpConsumeEvents>, limit: usize) -> Res
taker.perp_position(perp_market_index).unwrap(), taker.perp_position(perp_market_index).unwrap(),
&perp_market, &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; (maker_realized_pnl, taker_realized_pnl)
let taker_closed_pnl = taker_after_pnl - taker_before_pnl;
(maker_closed_pnl, taker_closed_pnl)
}; };
emit_stack(FillLogV3 { emit_stack(FillLogV3 {
mango_group: group_key, mango_group: group_key,

View File

@ -598,7 +598,7 @@ pub(crate) fn liquidation_action(
let token_transfer = pnl_transfer * spot_gain_per_settled; let token_transfer = pnl_transfer * spot_gain_per_settled;
liqor_perp_position.record_liquidation_pnl_takeover(pnl_transfer, limit_transfer); 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. // Update the accounts' perp_spot_transfer statistics.
let transfer_i64 = token_transfer.round_to_zero().to_num::<i64>(); let transfer_i64 = token_transfer.round_to_zero().to_num::<i64>();
@ -1027,7 +1027,7 @@ mod tests {
init_liqee_base, init_liqee_base,
I80F48::from_num(init_liqee_quote), 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) .unsettled_pnl(setup.perp_market.data(), I80F48::ONE)
.unwrap(); .unwrap();
@ -1072,7 +1072,7 @@ mod tests {
// The settle limit taken over matches the quote pos when removing the // The settle limit taken over matches the quote pos when removing the
// quote gains from giving away base lots // quote gains from giving away base lots
assert_eq_f!( 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::<f64>() liqor_perp.quote_position_native.to_num::<f64>()
+ liqor_perp.base_position_lots as f64, + liqor_perp.base_position_lots as f64,
1.1 1.1

View File

@ -267,7 +267,7 @@ pub(crate) fn liquidation_action(
.max(I80F48::ZERO); .max(I80F48::ZERO);
if settlement > 0 { if settlement > 0 {
liqor_perp_position.record_liquidation_quote_change(-settlement); 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. // Update the accounts' perp_spot_transfer statistics.
let settlement_i64 = settlement.round_to_zero().to_num::<i64>(); let settlement_i64 = settlement.round_to_zero().to_num::<i64>();
@ -380,7 +380,7 @@ pub(crate) fn liquidation_action(
// transfer perp quote loss from the liqee to the liqor // transfer perp quote loss from the liqee to the liqor
let liqor_perp_position = liqor.perp_position_mut(perp_market_index)?; 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); liqor_perp_position.record_liquidation_quote_change(-insurance_liab_transfer);
msg!( msg!(
@ -399,7 +399,7 @@ pub(crate) fn liquidation_action(
(perp_market.long_funding, perp_market.short_funding); (perp_market.long_funding, perp_market.short_funding);
if insurance_fund_exhausted && remaining_liab > 0 { if insurance_fund_exhausted && remaining_liab > 0 {
perp_market.socialize_loss(-remaining_liab)?; 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; socialized_loss = remaining_liab;
msg!("socialized loss: {}", socialized_loss); msg!("socialized loss: {}", socialized_loss);
} }
@ -760,7 +760,7 @@ mod tests {
{ {
let p = perp_p(&mut setup.liqee); let p = perp_p(&mut setup.liqee);
p.quote_position_native = I80F48::from_num(init_perp); 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(); let settle_bank = setup.settle_bank.data();
settle_bank settle_bank

View File

@ -68,7 +68,7 @@ pub fn perp_settle_fees(ctx: Context<PerpSettleFees>, max_settle_amount: u64) ->
.min(I80F48::from(max_settle_amount)); .min(I80F48::from(max_settle_amount));
require!(settlement >= 0, MangoError::SettlementAmountMustBePositive); 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; perp_market.fees_accrued -= settlement;
emit_perp_balances( emit_perp_balances(

View File

@ -143,8 +143,8 @@ pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>) -> Result<()> {
b_max_settle, b_max_settle,
); );
a_perp_position.record_settle(settlement); a_perp_position.record_settle(settlement, &perp_market);
b_perp_position.record_settle(-settlement); b_perp_position.record_settle(-settlement, &perp_market);
emit_perp_balances( emit_perp_balances(
ctx.accounts.group.key(), ctx.accounts.group.key(),
ctx.accounts.account_a.key(), ctx.accounts.account_a.key(),

View File

@ -1237,13 +1237,14 @@ impl<
Ok(()) Ok(())
} }
/// Returns amount of realized trade pnl for the maker
pub fn execute_perp_maker( pub fn execute_perp_maker(
&mut self, &mut self,
perp_market_index: PerpMarketIndex, perp_market_index: PerpMarketIndex,
perp_market: &mut PerpMarket, perp_market: &mut PerpMarket,
fill: &FillEvent, fill: &FillEvent,
group: &Group, group: &Group,
) -> Result<()> { ) -> Result<I80F48> {
let side = fill.taker_side().invert_side(); let side = fill.taker_side().invert_side();
let (base_change, quote_change) = fill.base_quote_change(side); let (base_change, quote_change) = fill.base_quote_change(side);
let quote = I80F48::from(perp_market.quote_lot_size) * I80F48::from(quote_change); 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)?; let pa = self.perp_position_mut(perp_market_index)?;
pa.settle_funding(perp_market); pa.settle_funding(perp_market);
pa.record_trading_fee(fees); 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::<u64>(); pa.maker_volume += quote.abs().to_num::<u64>();
@ -1288,15 +1289,16 @@ impl<
} }
} }
Ok(()) Ok(realized_pnl)
} }
/// Returns amount of realized trade pnl for the taker
pub fn execute_perp_taker( pub fn execute_perp_taker(
&mut self, &mut self,
perp_market_index: PerpMarketIndex, perp_market_index: PerpMarketIndex,
perp_market: &mut PerpMarket, perp_market: &mut PerpMarket,
fill: &FillEvent, fill: &FillEvent,
) -> Result<()> { ) -> Result<I80F48> {
let pa = self.perp_position_mut(perp_market_index)?; let pa = self.perp_position_mut(perp_market_index)?;
pa.settle_funding(perp_market); pa.settle_funding(perp_market);
@ -1305,11 +1307,11 @@ impl<
// fees are assessed at time of trade; no need to assess fees here // fees are assessed at time of trade; no need to assess fees here
let quote_change_native = let quote_change_native =
I80F48::from(perp_market.quote_lot_size) * I80F48::from(quote_change); 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::<u64>(); pa.taker_volume += quote_change_native.abs().to_num::<u64>();
Ok(()) Ok(realized_pnl)
} }
pub fn execute_perp_out_event( pub fn execute_perp_out_event(

File diff suppressed because it is too large Load Diff

View File

@ -112,8 +112,8 @@ async fn test_liq_perps_bankruptcy() -> Result<(), TransportError> {
taker_fee: 0.0, taker_fee: 0.0,
group_insurance_fund: true, group_insurance_fund: true,
// adjust this factur such that we get the desired settle limit in the end // 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) settle_pnl_limit_factor: (settle_limit as f32 - 0.1).max(0.0)
/ (-1.0 * 100.0 * adj_price) as f32, / (1.0 * 100.0 * adj_price) as f32,
settle_pnl_limit_window_size_ts: 24 * 60 * 60, settle_pnl_limit_window_size_ts: 24 * 60 * 60,
..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, base_token).await ..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::<MangoAccount>(account).await; let account_data = solana.get_account::<MangoAccount>(account).await;
assert_eq!(account_data.perps[0].quote_position_native(), pnl); assert_eq!(account_data.perps[0].quote_position_native(), pnl);
assert_eq!( assert_eq!(
account_data.perps[0].settle_pnl_limit_realized_trade, account_data.perps[0].recurring_settle_pnl_allowance,
settle_limit settle_limit
); );
assert_eq!( 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; let liqor_quote_before = account_position(solana, liqor, quote_token.bank).await;
send_tx( 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; fund_insurance(2).await;
let liqor_quote_before = account_position(solana, liqor, quote_token.bank).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; fund_insurance(5).await;
send_tx( send_tx(
@ -371,7 +371,7 @@ async fn test_liq_perps_bankruptcy() -> Result<(), TransportError> {
// no insurance // 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( send_tx(
solana, solana,
@ -390,7 +390,7 @@ async fn test_liq_perps_bankruptcy() -> Result<(), TransportError> {
// no settlement: no settle health // 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; fund_insurance(5).await;
send_tx( send_tx(
@ -430,7 +430,7 @@ async fn test_liq_perps_bankruptcy() -> Result<(), TransportError> {
// no socialized loss: fully covered by insurance fund // 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; fund_insurance(42).await;
send_tx( send_tx(

View File

@ -230,12 +230,12 @@ async fn test_liq_perps_base_and_bankruptcy() -> Result<(), TransportError> {
0.1 0.1
)); ));
assert!(assert_equal( assert!(assert_equal(
liqee_data.perps[0].realized_trade_pnl_native, liqee_data.perps[0].realized_pnl_for_position_native,
liqee_amount - 1000.0, liqee_amount - 1000.0,
0.1 0.1
)); ));
// stable price is 1.0, so 0.2 * 1000 // 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( assert!(assert_equal(
perp_market_after.fees_accrued - perp_market_before.fees_accrued, perp_market_after.fees_accrued - perp_market_before.fees_accrued,
liqor_amount - liqee_amount, liqor_amount - liqee_amount,
@ -521,7 +521,7 @@ async fn test_liq_perps_base_and_bankruptcy() -> Result<(), TransportError> {
send_tx( send_tx(
solana, solana,
TokenWithdrawInstruction { TokenWithdrawInstruction {
amount: liqee_quote_deposits_before as u64 - 100, amount: liqee_quote_deposits_before as u64 - 200,
allow_borrow: false, allow_borrow: false,
account: account_1, account: account_1,
owner, owner,
@ -572,9 +572,9 @@ async fn test_liq_perps_base_and_bankruptcy() -> Result<(), TransportError> {
0.1 0.1
)); ));
assert!(assert_equal( assert!(assert_equal(
liqor_data.tokens[0].native(&settle_bank), liqor_data.tokens[1].native(&settle_bank),
liqor_before.tokens[0].native(&settle_bank).to_num::<f64>() liqor_before.tokens[1].native(&settle_bank).to_num::<f64>()
- liqee_settle_limit_before as f64 * 100.0, // 100 is base lot size - liqee_settle_limit_before as f64,
0.1 0.1
)); ));

View File

@ -1100,14 +1100,6 @@ async fn test_perp_pnl_settle_limit() -> Result<(), TransportError> {
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await; let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await; let mango_account_1 = solana.get_account::<MangoAccount>(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) // neither account has any settle limit left (check for 1 because of the ceil()ing)
assert_eq!( assert_eq!(
mango_account_0.perps[0].available_settle_limit(&market).1, 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 // check that realized pnl settle limit was set up correctly
assert_eq!( 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 (0.8 * 1.0 * 100.0 * 1000.0) as i64 + 1
); // +1 just for rounding ); // +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. // 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_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( send_tx(
solana, 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, mango_account_1.perps[0].quote_position_native() - account_1_quote_before,
I80F48::from(account_0_realized_limit) 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::<PerpMarket>(perp_market).await;
assert_eq!( 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] mango_account_0.perps[0]
.realized_trade_pnl_native .unsettled_pnl(&perp_market_data, I80F48::from_num(1.0))
.to_num::<i64>() .unwrap()
); );
// can't settle again // 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_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( send_tx(
solana, solana,
@ -1248,13 +1241,13 @@ async fn test_perp_pnl_settle_limit() -> Result<(), TransportError> {
I80F48::from(account_0_realized_limit) 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 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!( assert_eq!(
mango_account_0.perps[0].realized_trade_pnl_native, mango_account_0.perps[0].realized_pnl_for_position_native,
I80F48::from(0) I80F48::from(0)
); );
assert_eq!( assert_eq!(
mango_account_1.perps[0].realized_trade_pnl_native, mango_account_1.perps[0].realized_pnl_for_position_native,
I80F48::from(0) I80F48::from(0)
); );

View File

@ -1325,9 +1325,9 @@ export class PerpPosition {
dto.takerVolume, dto.takerVolume,
dto.perpSpotTransfers, dto.perpSpotTransfers,
dto.avgEntryPricePerBaseLot, dto.avgEntryPricePerBaseLot,
I80F48.from(dto.realizedTradePnlNative), I80F48.from(dto.deprecatedRealizedTradePnlNative),
I80F48.from(dto.realizedOtherPnlNative), I80F48.from(dto.oneshotSettlePnlAllowance),
dto.settlePnlLimitRealizedTrade, dto.recurringSettlePnlAllowance,
I80F48.from(dto.realizedPnlForPositionNative), I80F48.from(dto.realizedPnlForPositionNative),
); );
} }
@ -1380,9 +1380,9 @@ export class PerpPosition {
public takerVolume: BN, public takerVolume: BN,
public perpSpotTransfers: BN, public perpSpotTransfers: BN,
public avgEntryPricePerBaseLot: number, public avgEntryPricePerBaseLot: number,
public realizedTradePnlNative: I80F48, public deprecatedRealizedTradePnlNative: I80F48,
public realizedOtherPnlNative: I80F48, public oneshotSettlePnlAllowance: I80F48,
public settlePnlLimitRealizedTrade: BN, public recurringSettlePnlAllowance: BN,
public realizedPnlForPositionNative: I80F48, public realizedPnlForPositionNative: I80F48,
) {} ) {}
@ -1636,28 +1636,25 @@ export class PerpPosition {
.mul(baseNative) .mul(baseNative)
.toNumber(); .toNumber();
const unrealized = new BN(perpMarket.settlePnlLimitFactor * positionValue); 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( const used = new BN(
this.settlePnlLimitSettledInCurrentWindowNative.toNumber(), this.settlePnlLimitSettledInCurrentWindowNative.toNumber(),
); );
let minPnl = unrealized.neg().sub(used); const availableMin = BN.min(minPnl.sub(used), new BN(0));
let maxPnl = unrealized.sub(used); const availableMax = BN.max(maxPnl.sub(used), new BN(0));
const realizedTrade = this.settlePnlLimitRealizedTrade; return [availableMin, availableMax];
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))];
} }
public applyPnlSettleLimit(pnl: I80F48, perpMarket: PerpMarket): I80F48 { public applyPnlSettleLimit(pnl: I80F48, perpMarket: PerpMarket): I80F48 {
@ -1782,8 +1779,10 @@ export class PerpPosition {
this.getNotionalValueUi(perpMarket!).toString() + this.getNotionalValueUi(perpMarket!).toString() +
', cumulative pnl over position lifetime ui - ' + ', cumulative pnl over position lifetime ui - ' +
this.cumulativePnlOverPositionLifetimeUi(perpMarket!).toString() + this.cumulativePnlOverPositionLifetimeUi(perpMarket!).toString() +
', realized other pnl native ui - ' + ', oneshot settleable native ui - ' +
toUiDecimalsForQuote(this.realizedOtherPnlNative) + toUiDecimalsForQuote(this.oneshotSettlePnlAllowance) +
', recurring settleable native ui - ' +
toUiDecimalsForQuote(this.recurringSettlePnlAllowance) +
', cumulative long funding ui - ' + ', cumulative long funding ui - ' +
toUiDecimalsForQuote(this.cumulativeLongFunding) + toUiDecimalsForQuote(this.cumulativeLongFunding) +
', cumulative short funding ui - ' + ', cumulative short funding ui - ' +
@ -1812,9 +1811,9 @@ export class PerpPositionDto {
public takerVolume: BN, public takerVolume: BN,
public perpSpotTransfers: BN, public perpSpotTransfers: BN,
public avgEntryPricePerBaseLot: number, public avgEntryPricePerBaseLot: number,
public realizedTradePnlNative: I80F48Dto, public deprecatedRealizedTradePnlNative: I80F48Dto,
public realizedOtherPnlNative: I80F48Dto, public oneshotSettlePnlAllowance: I80F48Dto,
public settlePnlLimitRealizedTrade: BN, public recurringSettlePnlAllowance: BN,
public realizedPnlForPositionNative: I80F48Dto, public realizedPnlForPositionNative: I80F48Dto,
) {} ) {}
} }

View File

@ -9334,36 +9334,44 @@ export type MangoV4 = {
"type": "f64" "type": "f64"
}, },
{ {
"name": "realizedTradePnlNative", "name": "deprecatedRealizedTradePnlNative",
"docs": [ "docs": [
"Amount of pnl that was realized by bringing the base position closer to 0.", "Deprecated field: 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."
], ],
"type": { "type": {
"defined": "I80F48" "defined": "I80F48"
} }
}, },
{ {
"name": "realizedOtherPnlNative", "name": "oneshotSettlePnlAllowance",
"docs": [ "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.", "- The value is signed: a negative number means negative pnl can be settled.",
"Settling pnl reduces this value first." "- A settlement in the right direction will decrease this amount.",
"",
"Typically added for fees, funding and liquidation."
], ],
"type": { "type": {
"defined": "I80F48" "defined": "I80F48"
} }
}, },
{ {
"name": "settlePnlLimitRealizedTrade", "name": "recurringSettlePnlAllowance",
"docs": [ "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", "- Unsigned, the settlement can happen in both directions. Value is >= 0.",
"value of the realization. It magnitude decreases when realized pnl drops below its value." "- 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" "type": "i64"
}, },
@ -23360,36 +23368,44 @@ export const IDL: MangoV4 = {
"type": "f64" "type": "f64"
}, },
{ {
"name": "realizedTradePnlNative", "name": "deprecatedRealizedTradePnlNative",
"docs": [ "docs": [
"Amount of pnl that was realized by bringing the base position closer to 0.", "Deprecated field: 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."
], ],
"type": { "type": {
"defined": "I80F48" "defined": "I80F48"
} }
}, },
{ {
"name": "realizedOtherPnlNative", "name": "oneshotSettlePnlAllowance",
"docs": [ "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.", "- The value is signed: a negative number means negative pnl can be settled.",
"Settling pnl reduces this value first." "- A settlement in the right direction will decrease this amount.",
"",
"Typically added for fees, funding and liquidation."
], ],
"type": { "type": {
"defined": "I80F48" "defined": "I80F48"
} }
}, },
{ {
"name": "settlePnlLimitRealizedTrade", "name": "recurringSettlePnlAllowance",
"docs": [ "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", "- Unsigned, the settlement can happen in both directions. Value is >= 0.",
"value of the realization. It magnitude decreases when realized pnl drops below its value." "- 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" "type": "i64"
}, },