Perps: track overall realized pnl relating to a position (#392)

This includes trade pnl, funding and fees. Tracking this makes it easier
for uis to display a consistent position overall pnl value that doesn't
decrease by settling.

Co-authored-by: microwavedcola1 <microwavedcola@gmail.com>
This commit is contained in:
Christian Kamm 2023-01-17 14:07:58 +01:00 committed by GitHub
parent 6206bbb953
commit 9346c8e546
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 357 additions and 31 deletions

View File

@ -59,7 +59,7 @@ pub async fn fetch_top(
let mut perp_pos = perp_pos.unwrap().clone();
perp_pos.settle_funding(&perp_market);
perp_pos.update_settle_limit(&perp_market, now_ts);
let pnl = perp_pos.pnl_for_price(&perp_market, oracle_price).unwrap();
let pnl = perp_pos.unsettled_pnl(&perp_market, oracle_price).unwrap();
let limited_pnl = perp_pos.apply_pnl_settle_limit(&perp_market, pnl);
if limited_pnl >= 0 && direction == Direction::MaxNegative
|| limited_pnl <= 0 && direction == Direction::MaxPositive

View File

@ -159,7 +159,7 @@ pub fn perp_liq_quote_and_bankruptcy(
liqee_perp_position.settle_funding(&perp_market);
liqor_perp_position.settle_funding(&perp_market);
let liqee_pnl = liqee_perp_position.pnl_for_price(&perp_market, oracle_price)?;
let liqee_pnl = liqee_perp_position.unsettled_pnl(&perp_market, oracle_price)?;
// TODO: deal with positive liqee pnl! Maybe another instruction?
require!(liqee_pnl < 0, MangoError::ProfitabilityMismatch);
@ -221,7 +221,7 @@ pub fn perp_liq_quote_and_bankruptcy(
//
let insurance_transfer = if settlement == max_settlement_liqee {
let liqee_perp_position = liqee.perp_position_mut(perp_market_index)?;
let liqee_pnl = liqee_perp_position.pnl_for_price(&perp_market, oracle_price)?;
let liqee_pnl = liqee_perp_position.unsettled_pnl(&perp_market, oracle_price)?;
let max_liab_transfer_from_liqee = (-liqee_pnl).min(-liqee_init_health).max(I80F48::ZERO);
let liab_transfer = max_liab_transfer_from_liqee

View File

@ -70,7 +70,7 @@ pub fn perp_settle_fees(ctx: Context<PerpSettleFees>, max_settle_amount: u64) ->
perp_position.settle_funding(&perp_market);
// Calculate PnL
let pnl = perp_position.pnl_for_price(&perp_market, oracle_price)?;
let pnl = perp_position.unsettled_pnl(&perp_market, oracle_price)?;
// Account perp position must have a loss to be able to settle against the fee account
require!(pnl.is_negative(), MangoError::ProfitabilityMismatch);

View File

@ -112,8 +112,8 @@ pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>) -> Result<()> {
let b_perp_position = account_b.perp_position_mut(perp_market_index)?;
a_perp_position.settle_funding(&perp_market);
b_perp_position.settle_funding(&perp_market);
let a_pnl = a_perp_position.pnl_for_price(&perp_market, oracle_price)?;
let b_pnl = b_perp_position.pnl_for_price(&perp_market, oracle_price)?;
let a_pnl = a_perp_position.unsettled_pnl(&perp_market, oracle_price)?;
let b_pnl = b_perp_position.unsettled_pnl(&perp_market, oracle_price)?;
// PnL must have opposite signs for there to be a settlement:
// Account A must be profitable, and B must be unprofitable.

View File

@ -240,12 +240,21 @@ pub struct PerpPosition {
/// value of the realization. It magnitude decreases when realized pnl drops below its value.
pub settle_pnl_limit_realized_trade: i64,
/// Trade pnl, fees, funding that were added over the current position's lifetime.
///
/// Reset when the position changes sign or goes to zero.
/// Not decreased by settling.
///
/// This is tracked for display purposes: this value plus the difference between entry
/// price and current price of the base position is the overall pnl.
pub realized_pnl_for_position_native: I80F48,
#[derivative(Debug = "ignore")]
pub reserved: [u8; 104],
pub reserved: [u8; 88],
}
const_assert_eq!(
size_of::<PerpPosition>(),
2 + 2 + 4 + 8 + 8 + 16 + 8 + 16 * 2 + 8 * 2 + 8 * 2 + 8 * 5 + 8 + 2 * 16 + 8 + 104
2 + 2 + 4 + 8 + 8 + 16 + 8 + 16 * 2 + 8 * 2 + 8 * 2 + 8 * 5 + 8 + 2 * 16 + 8 + 16 + 88
);
const_assert_eq!(size_of::<PerpPosition>(), 304);
const_assert_eq!(size_of::<PerpPosition>() % 8, 0);
@ -275,7 +284,8 @@ impl Default for PerpPosition {
settle_pnl_limit_window: 0,
settle_pnl_limit_settled_in_current_window_native: 0,
settle_pnl_limit_realized_trade: 0,
reserved: [0; 104],
realized_pnl_for_position_native: I80F48::ZERO,
reserved: [0; 88],
}
}
}
@ -353,6 +363,7 @@ impl PerpPosition {
let funding = self.unsettled_funding(perp_market);
cm!(self.quote_position_native -= funding);
cm!(self.realized_other_pnl_native -= funding);
cm!(self.realized_pnl_for_position_native -= funding);
if self.base_position_lots.is_positive() {
self.cumulative_long_funding += funding.to_num::<f64>();
@ -386,9 +397,10 @@ impl PerpPosition {
if new_position == 0 {
reduced_lots = -old_position;
// clear out entry and break-even prices
// 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.
@ -412,6 +424,9 @@ impl PerpPosition {
// Set entry and break-even based on the new_position entered
self.avg_entry_price_per_base_lot = new_avg_entry;
self.quote_running_native = (-new_position * new_avg_entry) as i64;
// New position without realized pnl
self.realized_pnl_for_position_native = I80F48::ZERO;
} else {
// The old and new position have the same sign
@ -438,6 +453,7 @@ impl PerpPosition {
newly_realized_pnl =
cm!(quote_change_native + I80F48::from(base_change) * avg_entry);
cm!(self.realized_trade_pnl_native += newly_realized_pnl);
cm!(self.realized_pnl_for_position_native += newly_realized_pnl);
}
}
@ -584,7 +600,7 @@ impl PerpPosition {
}
/// Calculate the PnL of the position for a given price
pub fn pnl_for_price(&self, perp_market: &PerpMarket, price: I80F48) -> Result<I80F48> {
pub fn unsettled_pnl(&self, perp_market: &PerpMarket, price: I80F48) -> Result<I80F48> {
require_eq!(self.market_index, perp_market.perp_market_index);
let base_native = self.base_position_native(perp_market);
let pnl = cm!(self.quote_position_native() + base_native * price);
@ -708,6 +724,7 @@ impl PerpPosition {
pub fn record_trading_fee(&mut self, fee: I80F48) {
self.change_quote_position(-fee);
cm!(self.realized_other_pnl_native -= fee);
cm!(self.realized_pnl_for_position_native -= fee);
}
/// Adds immediately-settleable realized pnl when a liqor takes over pnl during liquidation
@ -805,6 +822,7 @@ mod tests {
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);
}
@ -818,6 +836,7 @@ mod tests {
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);
}
@ -831,6 +850,7 @@ mod tests {
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);
}
@ -844,6 +864,7 @@ mod tests {
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);
}
@ -857,6 +878,7 @@ mod tests {
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);
}
@ -870,6 +892,7 @@ mod tests {
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);
}
@ -883,6 +906,7 @@ mod tests {
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);
}
@ -896,6 +920,7 @@ mod tests {
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);
}
@ -909,6 +934,7 @@ mod tests {
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);
}
@ -922,6 +948,7 @@ mod tests {
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);
}
@ -937,6 +964,7 @@ mod tests {
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);
}
@ -956,6 +984,7 @@ mod tests {
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));
}
#[test]
@ -968,21 +997,25 @@ mod tests {
// 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);
// 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,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 1 @ 8,000 -- flips sign, changes pnl limit
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));
}
@ -997,11 +1030,13 @@ mod tests {
// Sell 1 @ 10,000
pos.record_trade(&mut market, -1, I80F48::from(10_000));
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);
// 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));
assert_eq!(pos.realized_pnl_for_position_native, I80F48::from(0));
assert_eq!(pos.settle_pnl_limit_realized_trade, 0);
assert_eq!(pos.base_position_lots, 0);
@ -1027,6 +1062,7 @@ mod tests {
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);
}
@ -1100,15 +1136,15 @@ mod tests {
market.base_lot_size = 10;
let long_pos = create_perp_position(&market, 50, 100);
let pnl = long_pos.pnl_for_price(&market, I80F48::from(11)).unwrap();
let pnl = long_pos.unsettled_pnl(&market, I80F48::from(11)).unwrap();
assert_eq!(pnl, I80F48::from(50 * 10 * 1), "long profitable");
let pnl = long_pos.pnl_for_price(&market, I80F48::from(9)).unwrap();
let pnl = long_pos.unsettled_pnl(&market, I80F48::from(9)).unwrap();
assert_eq!(pnl, I80F48::from(50 * 10 * -1), "long unprofitable");
let short_pos = create_perp_position(&market, -50, 100);
let pnl = short_pos.pnl_for_price(&market, I80F48::from(11)).unwrap();
let pnl = short_pos.unsettled_pnl(&market, I80F48::from(11)).unwrap();
assert_eq!(pnl, I80F48::from(50 * 10 * -1), "short unprofitable");
let pnl = short_pos.pnl_for_price(&market, I80F48::from(9)).unwrap();
let pnl = short_pos.unsettled_pnl(&market, I80F48::from(9)).unwrap();
assert_eq!(pnl, I80F48::from(50 * 10 * 1), "short profitable");
}

View File

@ -719,10 +719,10 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr
// the remainder got socialized via funding payments
let perp_market = solana.get_account::<PerpMarket>(perp_market).await;
let pnl_before = liqee_before.perps[0]
.pnl_for_price(&perp_market, I80F48::ONE)
.unsettled_pnl(&perp_market, I80F48::ONE)
.unwrap();
let pnl_after = liqee_after.perps[0]
.pnl_for_price(&perp_market, I80F48::ONE)
.unsettled_pnl(&perp_market, I80F48::ONE)
.unwrap();
let socialized_amount = (pnl_after - pnl_before).to_num::<f64>() - liq_perp_quote_amount;
assert!(assert_equal(

View File

@ -1,6 +1,7 @@
#![cfg(all(feature = "test-bpf"))]
use anchor_lang::prelude::Pubkey;
use fixed::types::I80F48;
use fixed_macro::types::I80F48;
use mango_v4::state::*;
use program_test::*;
@ -843,6 +844,218 @@ async fn test_perp_oracle_peg() -> Result<(), TransportError> {
Ok(())
}
#[tokio::test]
async fn test_perp_realize_partially() -> Result<(), TransportError> {
let context = TestContext::new().await;
let solana = &context.solana.clone();
let admin = TestKeypair::new();
let owner = context.users[0].key;
let payer = context.users[1].key;
let mints = &context.mints[0..2];
//
// SETUP: Create a group and an account
//
let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
admin,
payer,
mints: mints.to_vec(),
..GroupWithTokensConfig::default()
}
.create(solana)
.await;
let deposit_amount = 1000;
let account_0 = create_funded_account(
&solana,
group,
owner,
0,
&context.users[1],
mints,
deposit_amount,
0,
)
.await;
let account_1 = create_funded_account(
&solana,
group,
owner,
1,
&context.users[1],
mints,
deposit_amount,
0,
)
.await;
//
// TEST: Create a perp market
//
let mango_v4::accounts::PerpCreateMarket { perp_market, .. } = send_tx(
solana,
PerpCreateMarketInstruction {
group,
admin,
payer,
perp_market_index: 0,
quote_lot_size: 10,
base_lot_size: 100,
maint_base_asset_weight: 0.975,
init_base_asset_weight: 0.95,
maint_base_liab_weight: 1.025,
init_base_liab_weight: 1.05,
liquidation_fee: 0.012,
maker_fee: 0.0000,
taker_fee: 0.0000,
settle_pnl_limit_factor: -1.0,
settle_pnl_limit_window_size_ts: 24 * 60 * 60,
..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, &tokens[1]).await
},
)
.await
.unwrap();
let perp_market_data = solana.get_account::<PerpMarket>(perp_market).await;
let price_lots = perp_market_data.native_price_to_lot(I80F48!(1000));
set_perp_stub_oracle_price(solana, group, perp_market, &tokens[1], admin, 1000.0).await;
//
// SETUP: Place a bid, corresponding ask, and consume event
//
send_tx(
solana,
PerpPlaceOrderInstruction {
account: account_0,
perp_market,
owner,
side: Side::Bid,
price_lots,
max_base_lots: 2,
max_quote_lots: i64::MAX,
reduce_only: false,
client_order_id: 5,
},
)
.await
.unwrap();
send_tx(
solana,
PerpPlaceOrderInstruction {
account: account_1,
perp_market,
owner,
side: Side::Ask,
price_lots,
max_base_lots: 2,
max_quote_lots: i64::MAX,
reduce_only: false,
client_order_id: 6,
},
)
.await
.unwrap();
send_tx(
solana,
PerpConsumeEventsInstruction {
perp_market,
mango_accounts: vec![account_0, account_1],
},
)
.await
.unwrap();
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
let perp_0 = mango_account_0.perps[0];
assert_eq!(perp_0.base_position_lots(), 2);
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
let perp_1 = mango_account_1.perps[0];
assert_eq!(perp_1.base_position_lots(), -2);
//
// SETUP: Sell one lot again at increased price
//
set_perp_stub_oracle_price(solana, group, perp_market, &tokens[1], admin, 1500.0).await;
send_tx(
solana,
PerpPlaceOrderInstruction {
account: account_0,
perp_market,
owner,
side: Side::Ask,
price_lots: perp_market_data.native_price_to_lot(I80F48::from_num(1500)),
max_base_lots: 1,
max_quote_lots: i64::MAX,
reduce_only: false,
client_order_id: 5,
},
)
.await
.unwrap();
send_tx(
solana,
PerpPlaceOrderInstruction {
account: account_1,
perp_market,
owner,
side: Side::Bid,
price_lots: perp_market_data.native_price_to_lot(I80F48::from_num(1500)),
max_base_lots: 1,
max_quote_lots: i64::MAX,
reduce_only: false,
client_order_id: 6,
},
)
.await
.unwrap();
send_tx(
solana,
PerpConsumeEventsInstruction {
perp_market,
mango_accounts: vec![account_0, account_1],
},
)
.await
.unwrap();
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
let perp_0 = mango_account_0.perps[0];
assert_eq!(perp_0.base_position_lots(), 1);
assert!(assert_equal(
perp_0.quote_position_native(),
-200_000.0 + 150_000.0,
0.001
));
assert!(assert_equal(
perp_0.realized_pnl_for_position_native,
50_000.0,
0.001
));
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
let perp_1 = mango_account_1.perps[0];
assert_eq!(perp_1.base_position_lots(), -1);
assert!(assert_equal(
perp_1.quote_position_native(),
200_000.0 - 150_000.0,
0.001
));
assert!(assert_equal(
perp_1.realized_pnl_for_position_native,
-50_000.0,
0.001
));
Ok(())
}
async fn assert_no_perp_orders(solana: &SolanaCookie, account_0: Pubkey) {
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;

View File

@ -390,6 +390,14 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> {
mango_account_1.perp_spot_transfers, -expected_total_settle,
"net_settled on account 1 updated with loss from settlement"
);
assert_eq!(
mango_account_0.perps[0].perp_spot_transfers, expected_total_settle,
"net_settled on account 0 updated with profit from settlement"
);
assert_eq!(
mango_account_1.perps[0].perp_spot_transfers, -expected_total_settle,
"net_settled on account 1 updated with loss from settlement"
);
}
// Change the oracle to a reasonable price in other direction

View File

@ -111,6 +111,7 @@ describe('Health Cache', () => {
ZERO_I80F48(),
ZERO_I80F48(),
new BN(0),
ZERO_I80F48(),
);
const pi1 = PerpInfo.fromPerpPosition(pM, pp);
@ -227,6 +228,7 @@ describe('Health Cache', () => {
ZERO_I80F48(),
ZERO_I80F48(),
new BN(0),
ZERO_I80F48(),
);
const pi1 = PerpInfo.fromPerpPosition(pM, pp);

View File

@ -175,7 +175,6 @@ export class MangoAccount {
public getPerpPosition(
perpMarketIndex: PerpMarketIndex,
useEventQueue?: boolean,
): PerpPosition | undefined {
return this.perps.find((pp) => pp.marketIndex == perpMarketIndex);
}
@ -1180,6 +1179,7 @@ export class PerpPosition {
I80F48.from(dto.realizedTradePnlNative),
I80F48.from(dto.realizedOtherPnlNative),
dto.settlePnlLimitRealizedTrade,
I80F48.from(dto.realizedPnlForPositionNative),
);
}
@ -1208,6 +1208,7 @@ export class PerpPosition {
ZERO_I80F48(),
ZERO_I80F48(),
new BN(0),
ZERO_I80F48(),
);
}
@ -1233,12 +1234,17 @@ export class PerpPosition {
public realizedTradePnlNative: I80F48,
public realizedOtherPnlNative: I80F48,
public settlePnlLimitRealizedTrade: BN,
public realizedPnlForPositionNative: I80F48,
) {}
isActive(): boolean {
return this.marketIndex !== PerpPosition.PerpMarketIndexUnset;
}
public getBasePositionNative(perpMarket: PerpMarket): I80F48 {
return I80F48.fromI64(this.basePositionLots.mul(perpMarket.baseLotSize));
}
public getBasePositionUi(
perpMarket: PerpMarket,
useEventQueue?: boolean,
@ -1316,13 +1322,15 @@ export class PerpPosition {
);
}
public getAverageEntryPriceUi(perpMarket: PerpMarket): number {
if (perpMarket.perpMarketIndex !== this.marketIndex) {
throw new Error("PerpPosition doesn't belong to the given market!");
}
public getAverageEntryPrice(perpMarket: PerpMarket): I80F48 {
return I80F48.fromNumber(this.avgEntryPricePerBaseLot).mul(
I80F48.fromI64(perpMarket.baseLotSize),
);
}
public getAverageEntryPriceUi(perpMarket: PerpMarket): number {
return perpMarket.priceNativeToUi(
this.avgEntryPricePerBaseLot / perpMarket.baseLotSize.toNumber(),
this.getAverageEntryPrice(perpMarket).toNumber(),
);
}
@ -1340,15 +1348,40 @@ export class PerpPosition {
);
}
public getPnl(perpMarket: PerpMarket): I80F48 {
public cumulativePnlOverPositionLifetimeUi(
group: Group,
perpMarket: PerpMarket,
): number {
if (perpMarket.perpMarketIndex !== this.marketIndex) {
throw new Error("PerpPosition doesn't belong to the given market!");
}
const priceChange = perpMarket.price.sub(
this.getAverageEntryPrice(perpMarket),
);
return toUiDecimals(
this.realizedPnlForPositionNative.add(
this.getBasePositionNative(perpMarket).mul(priceChange),
),
group.getMintDecimalsByTokenIndex(perpMarket.settleTokenIndex),
);
}
public getUnsettledPnl(perpMarket: PerpMarket): I80F48 {
if (perpMarket.perpMarketIndex !== this.marketIndex) {
throw new Error("PerpPosition doesn't belong to the given market!");
}
return this.quotePositionNative.add(
I80F48.fromI64(this.basePositionLots.mul(perpMarket.baseLotSize)).mul(
perpMarket.price,
),
this.getBasePositionNative(perpMarket).mul(perpMarket.price),
);
}
public getUnsettledPnlUi(group: Group, perpMarket: PerpMarket): number {
return toUiDecimals(
this.getUnsettledPnl(perpMarket),
group.getMintDecimalsByTokenIndex(perpMarket.settleTokenIndex),
);
}
@ -1432,7 +1465,10 @@ export class PerpPosition {
throw new Error("PerpPosition doesn't belong to the given market!");
}
return this.applyPnlSettleLimit(this.getPnl(perpMarket), perpMarket);
return this.applyPnlSettleLimit(
this.getUnsettledPnl(perpMarket),
perpMarket,
);
}
}
@ -1459,6 +1495,7 @@ export class PerpPositionDto {
public realizedTradePnlNative: I80F48Dto,
public realizedOtherPnlNative: I80F48Dto,
public settlePnlLimitRealizedTrade: BN,
public realizedPnlForPositionNative: I80F48Dto,
) {}
}

View File

@ -5365,12 +5365,27 @@ export type MangoV4 = {
],
"type": "i64"
},
{
"name": "realizedPnlForPositionNative",
"docs": [
"Trade pnl, fees, funding that were added over the current position's lifetime.",
"",
"Reset when the position changes sign or goes to zero.",
"Not decreased by settling.",
"",
"This is tracked for display purposes: this value plus the difference between entry",
"price and current price of the base position is the overall pnl."
],
"type": {
"defined": "I80F48"
}
},
{
"name": "reserved",
"type": {
"array": [
"u8",
104
88
]
}
}
@ -13119,12 +13134,27 @@ export const IDL: MangoV4 = {
],
"type": "i64"
},
{
"name": "realizedPnlForPositionNative",
"docs": [
"Trade pnl, fees, funding that were added over the current position's lifetime.",
"",
"Reset when the position changes sign or goes to zero.",
"Not decreased by settling.",
"",
"This is tracked for display purposes: this value plus the difference between entry",
"price and current price of the base position is the overall pnl."
],
"type": {
"defined": "I80F48"
}
},
{
"name": "reserved",
"type": {
"array": [
"u8",
104
88
]
}
}

View File

@ -31,7 +31,7 @@ async function settlePnl(
const pp = mangoAccount
.perpActive()
.find((pp) => pp.marketIndex === perpMarket.perpMarketIndex)!;
const pnl = pp.getPnl(perpMarket);
const pnl = pp.getUnsettledPnl(perpMarket);
console.log(
`Avg entry price - ${pp.getAverageEntryPriceUi(