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:
parent
719aee37ae
commit
ae5907ba3a
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
));
|
));
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue