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:
parent
6206bbb953
commit
9346c8e546
|
@ -59,7 +59,7 @@ pub async fn fetch_top(
|
||||||
let mut perp_pos = perp_pos.unwrap().clone();
|
let mut perp_pos = perp_pos.unwrap().clone();
|
||||||
perp_pos.settle_funding(&perp_market);
|
perp_pos.settle_funding(&perp_market);
|
||||||
perp_pos.update_settle_limit(&perp_market, now_ts);
|
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);
|
let limited_pnl = perp_pos.apply_pnl_settle_limit(&perp_market, pnl);
|
||||||
if limited_pnl >= 0 && direction == Direction::MaxNegative
|
if limited_pnl >= 0 && direction == Direction::MaxNegative
|
||||||
|| limited_pnl <= 0 && direction == Direction::MaxPositive
|
|| limited_pnl <= 0 && direction == Direction::MaxPositive
|
||||||
|
|
|
@ -159,7 +159,7 @@ pub fn perp_liq_quote_and_bankruptcy(
|
||||||
liqee_perp_position.settle_funding(&perp_market);
|
liqee_perp_position.settle_funding(&perp_market);
|
||||||
liqor_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?
|
// TODO: deal with positive liqee pnl! Maybe another instruction?
|
||||||
require!(liqee_pnl < 0, MangoError::ProfitabilityMismatch);
|
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 insurance_transfer = if settlement == max_settlement_liqee {
|
||||||
let liqee_perp_position = liqee.perp_position_mut(perp_market_index)?;
|
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 max_liab_transfer_from_liqee = (-liqee_pnl).min(-liqee_init_health).max(I80F48::ZERO);
|
||||||
let liab_transfer = max_liab_transfer_from_liqee
|
let liab_transfer = max_liab_transfer_from_liqee
|
||||||
|
|
|
@ -70,7 +70,7 @@ pub fn perp_settle_fees(ctx: Context<PerpSettleFees>, max_settle_amount: u64) ->
|
||||||
perp_position.settle_funding(&perp_market);
|
perp_position.settle_funding(&perp_market);
|
||||||
|
|
||||||
// Calculate PnL
|
// 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
|
// Account perp position must have a loss to be able to settle against the fee account
|
||||||
require!(pnl.is_negative(), MangoError::ProfitabilityMismatch);
|
require!(pnl.is_negative(), MangoError::ProfitabilityMismatch);
|
||||||
|
|
|
@ -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)?;
|
let b_perp_position = account_b.perp_position_mut(perp_market_index)?;
|
||||||
a_perp_position.settle_funding(&perp_market);
|
a_perp_position.settle_funding(&perp_market);
|
||||||
b_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 a_pnl = a_perp_position.unsettled_pnl(&perp_market, oracle_price)?;
|
||||||
let b_pnl = b_perp_position.pnl_for_price(&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:
|
// PnL must have opposite signs for there to be a settlement:
|
||||||
// Account A must be profitable, and B must be unprofitable.
|
// Account A must be profitable, and B must be unprofitable.
|
||||||
|
|
|
@ -240,12 +240,21 @@ pub struct PerpPosition {
|
||||||
/// value of the realization. It magnitude decreases when realized pnl drops below its value.
|
/// value of the realization. It magnitude decreases when realized pnl drops below its value.
|
||||||
pub settle_pnl_limit_realized_trade: i64,
|
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")]
|
#[derivative(Debug = "ignore")]
|
||||||
pub reserved: [u8; 104],
|
pub reserved: [u8; 88],
|
||||||
}
|
}
|
||||||
const_assert_eq!(
|
const_assert_eq!(
|
||||||
size_of::<PerpPosition>(),
|
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>(), 304);
|
||||||
const_assert_eq!(size_of::<PerpPosition>() % 8, 0);
|
const_assert_eq!(size_of::<PerpPosition>() % 8, 0);
|
||||||
|
@ -275,7 +284,8 @@ impl Default for PerpPosition {
|
||||||
settle_pnl_limit_window: 0,
|
settle_pnl_limit_window: 0,
|
||||||
settle_pnl_limit_settled_in_current_window_native: 0,
|
settle_pnl_limit_settled_in_current_window_native: 0,
|
||||||
settle_pnl_limit_realized_trade: 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);
|
let funding = self.unsettled_funding(perp_market);
|
||||||
cm!(self.quote_position_native -= funding);
|
cm!(self.quote_position_native -= funding);
|
||||||
cm!(self.realized_other_pnl_native -= funding);
|
cm!(self.realized_other_pnl_native -= funding);
|
||||||
|
cm!(self.realized_pnl_for_position_native -= funding);
|
||||||
|
|
||||||
if self.base_position_lots.is_positive() {
|
if self.base_position_lots.is_positive() {
|
||||||
self.cumulative_long_funding += funding.to_num::<f64>();
|
self.cumulative_long_funding += funding.to_num::<f64>();
|
||||||
|
@ -386,9 +397,10 @@ impl PerpPosition {
|
||||||
if new_position == 0 {
|
if new_position == 0 {
|
||||||
reduced_lots = -old_position;
|
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.avg_entry_price_per_base_lot = 0.0;
|
||||||
self.quote_running_native = 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
|
// 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.
|
// 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
|
// Set entry and break-even based on the new_position entered
|
||||||
self.avg_entry_price_per_base_lot = new_avg_entry;
|
self.avg_entry_price_per_base_lot = new_avg_entry;
|
||||||
self.quote_running_native = (-new_position * new_avg_entry) as i64;
|
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 {
|
} else {
|
||||||
// The old and new position have the same sign
|
// The old and new position have the same sign
|
||||||
|
|
||||||
|
@ -438,6 +453,7 @@ impl PerpPosition {
|
||||||
newly_realized_pnl =
|
newly_realized_pnl =
|
||||||
cm!(quote_change_native + I80F48::from(base_change) * avg_entry);
|
cm!(quote_change_native + I80F48::from(base_change) * avg_entry);
|
||||||
cm!(self.realized_trade_pnl_native += newly_realized_pnl);
|
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
|
/// 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);
|
require_eq!(self.market_index, perp_market.perp_market_index);
|
||||||
let base_native = self.base_position_native(perp_market);
|
let base_native = self.base_position_native(perp_market);
|
||||||
let pnl = cm!(self.quote_position_native() + base_native * price);
|
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) {
|
pub fn record_trading_fee(&mut self, fee: I80F48) {
|
||||||
self.change_quote_position(-fee);
|
self.change_quote_position(-fee);
|
||||||
cm!(self.realized_other_pnl_native -= 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
|
/// 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.avg_entry_price(&market), 10.0);
|
||||||
assert_eq!(pos.break_even_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_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.settle_pnl_limit_realized_trade, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -818,6 +836,7 @@ mod tests {
|
||||||
assert_eq!(pos.avg_entry_price(&market), 10.0);
|
assert_eq!(pos.avg_entry_price(&market), 10.0);
|
||||||
assert_eq!(pos.break_even_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_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.settle_pnl_limit_realized_trade, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -831,6 +850,7 @@ mod tests {
|
||||||
assert_eq!(pos.avg_entry_price(&market), 20.0);
|
assert_eq!(pos.avg_entry_price(&market), 20.0);
|
||||||
assert_eq!(pos.break_even_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_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.settle_pnl_limit_realized_trade, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -844,6 +864,7 @@ mod tests {
|
||||||
assert_eq!(pos.avg_entry_price(&market), 20.0);
|
assert_eq!(pos.avg_entry_price(&market), 20.0);
|
||||||
assert_eq!(pos.break_even_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_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.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.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.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_trade_pnl_native, I80F48::from(-200));
|
||||||
|
assert_eq!(pos.realized_pnl_for_position_native, I80F48::from(-200));
|
||||||
assert_eq!(pos.settle_pnl_limit_realized_trade, -5 * 10 / 5 - 1);
|
assert_eq!(pos.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.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.break_even_price(&market), -30.0); // Already broke even
|
||||||
assert_eq!(pos.realized_trade_pnl_native, I80F48::from(200));
|
assert_eq!(pos.realized_trade_pnl_native, I80F48::from(200));
|
||||||
|
assert_eq!(pos.realized_pnl_for_position_native, I80F48::from(200));
|
||||||
assert_eq!(pos.settle_pnl_limit_realized_trade, 5 * 10 / 5 + 1);
|
assert_eq!(pos.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.avg_entry_price(&market), 0.0); // Entry price zero when no position
|
||||||
assert_eq!(pos.break_even_price(&market), 0.0);
|
assert_eq!(pos.break_even_price(&market), 0.0);
|
||||||
assert_eq!(pos.realized_trade_pnl_native, I80F48::from(150));
|
assert_eq!(pos.realized_trade_pnl_native, I80F48::from(150));
|
||||||
|
assert_eq!(pos.realized_pnl_for_position_native, I80F48::from(0));
|
||||||
assert_eq!(pos.settle_pnl_limit_realized_trade, 10 * 10 / 5 + 1);
|
assert_eq!(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.avg_entry_price(&market), 0.0); // Entry price zero when no position
|
||||||
assert_eq!(pos.break_even_price(&market), 0.0);
|
assert_eq!(pos.break_even_price(&market), 0.0);
|
||||||
assert_eq!(pos.realized_trade_pnl_native, I80F48::from(-150));
|
assert_eq!(pos.realized_trade_pnl_native, I80F48::from(-150));
|
||||||
|
assert_eq!(pos.realized_pnl_for_position_native, I80F48::from(0));
|
||||||
assert_eq!(pos.settle_pnl_limit_realized_trade, -10 * 10 / 5 - 1);
|
assert_eq!(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.avg_entry_price(&market), 20.0);
|
||||||
assert_eq!(pos.break_even_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_trade_pnl_native, I80F48::from(100));
|
||||||
|
assert_eq!(pos.realized_pnl_for_position_native, I80F48::from(0));
|
||||||
assert_eq!(pos.settle_pnl_limit_realized_trade, 10 * 10 / 5 + 1);
|
assert_eq!(pos.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.avg_entry_price(&market), 20.0);
|
||||||
assert_eq!(pos.break_even_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_trade_pnl_native, I80F48::from(-100));
|
||||||
|
assert_eq!(pos.realized_pnl_for_position_native, I80F48::from(0));
|
||||||
assert_eq!(pos.settle_pnl_limit_realized_trade, -10 * 10 / 5 - 1);
|
assert_eq!(pos.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.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.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_trade_pnl_native, I80F48::from(2_000));
|
||||||
|
assert_eq!(pos.realized_pnl_for_position_native, I80F48::from(2_000));
|
||||||
assert_eq!(pos.settle_pnl_limit_realized_trade, 1 * 10 / 5 + 1);
|
assert_eq!(pos.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.avg_entry_price(&market), 10_000.0);
|
||||||
assert_eq!(pos.break_even_price(&market), 9_800.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_trade_pnl_native, I80F48::from(20_000));
|
||||||
|
assert_eq!(pos.realized_pnl_for_position_native, I80F48::from(20_000));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -968,21 +997,25 @@ mod tests {
|
||||||
// Sell 1 @ 11,000
|
// Sell 1 @ 11,000
|
||||||
pos.record_trade(&mut market, -1, I80F48::from(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_trade_pnl_native, I80F48::from(1_000));
|
||||||
|
assert_eq!(pos.realized_pnl_for_position_native, I80F48::from(1_000));
|
||||||
assert_eq!(pos.settle_pnl_limit_realized_trade, 1 * 10 / 5 + 1);
|
assert_eq!(pos.settle_pnl_limit_realized_trade, 1 * 10 / 5 + 1);
|
||||||
|
|
||||||
// Sell 1 @ 11,000 -- increases limit
|
// Sell 1 @ 11,000 -- increases limit
|
||||||
pos.record_trade(&mut market, -1, I80F48::from(11_000));
|
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_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));
|
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
|
// 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));
|
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_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));
|
assert_eq!(pos.settle_pnl_limit_realized_trade, 2 * (10 / 5 + 1));
|
||||||
|
|
||||||
// Sell 1 @ 8,000 -- flips sign, changes pnl limit
|
// Sell 1 @ 8,000 -- flips sign, changes pnl limit
|
||||||
pos.record_trade(&mut market, -1, I80F48::from(8_000));
|
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_trade_pnl_native, I80F48::from(-1_000));
|
||||||
|
assert_eq!(pos.realized_pnl_for_position_native, I80F48::from(-1_000));
|
||||||
assert_eq!(pos.settle_pnl_limit_realized_trade, -(1 * 10 / 5 + 1));
|
assert_eq!(pos.settle_pnl_limit_realized_trade, -(1 * 10 / 5 + 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -997,11 +1030,13 @@ mod tests {
|
||||||
// Sell 1 @ 10,000
|
// Sell 1 @ 10,000
|
||||||
pos.record_trade(&mut market, -1, I80F48::from(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_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.settle_pnl_limit_realized_trade, 0);
|
||||||
|
|
||||||
// Sell 10 @ 10,000
|
// Sell 10 @ 10,000
|
||||||
pos.record_trade(&mut market, -10, I80F48::from(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_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.settle_pnl_limit_realized_trade, 0);
|
||||||
|
|
||||||
assert_eq!(pos.base_position_lots, 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_other_pnl_native, I80F48::from(100));
|
||||||
assert_eq!(pos.realized_trade_pnl_native, I80F48::from(1_000));
|
assert_eq!(pos.realized_trade_pnl_native, I80F48::from(1_000));
|
||||||
|
assert_eq!(pos.realized_pnl_for_position_native, I80F48::from(0));
|
||||||
assert_eq!(pos.settle_pnl_limit_realized_trade, 1 * 10 / 5 + 1);
|
assert_eq!(pos.settle_pnl_limit_realized_trade, 1 * 10 / 5 + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1100,15 +1136,15 @@ mod tests {
|
||||||
market.base_lot_size = 10;
|
market.base_lot_size = 10;
|
||||||
|
|
||||||
let long_pos = create_perp_position(&market, 50, 100);
|
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");
|
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");
|
assert_eq!(pnl, I80F48::from(50 * 10 * -1), "long unprofitable");
|
||||||
|
|
||||||
let short_pos = create_perp_position(&market, -50, 100);
|
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");
|
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");
|
assert_eq!(pnl, I80F48::from(50 * 10 * 1), "short profitable");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -719,10 +719,10 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr
|
||||||
// the remainder got socialized via funding payments
|
// the remainder got socialized via funding payments
|
||||||
let perp_market = solana.get_account::<PerpMarket>(perp_market).await;
|
let perp_market = solana.get_account::<PerpMarket>(perp_market).await;
|
||||||
let pnl_before = liqee_before.perps[0]
|
let pnl_before = liqee_before.perps[0]
|
||||||
.pnl_for_price(&perp_market, I80F48::ONE)
|
.unsettled_pnl(&perp_market, I80F48::ONE)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let pnl_after = liqee_after.perps[0]
|
let pnl_after = liqee_after.perps[0]
|
||||||
.pnl_for_price(&perp_market, I80F48::ONE)
|
.unsettled_pnl(&perp_market, I80F48::ONE)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let socialized_amount = (pnl_after - pnl_before).to_num::<f64>() - liq_perp_quote_amount;
|
let socialized_amount = (pnl_after - pnl_before).to_num::<f64>() - liq_perp_quote_amount;
|
||||||
assert!(assert_equal(
|
assert!(assert_equal(
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
#![cfg(all(feature = "test-bpf"))]
|
#![cfg(all(feature = "test-bpf"))]
|
||||||
|
|
||||||
use anchor_lang::prelude::Pubkey;
|
use anchor_lang::prelude::Pubkey;
|
||||||
|
use fixed::types::I80F48;
|
||||||
use fixed_macro::types::I80F48;
|
use fixed_macro::types::I80F48;
|
||||||
use mango_v4::state::*;
|
use mango_v4::state::*;
|
||||||
use program_test::*;
|
use program_test::*;
|
||||||
|
@ -843,6 +844,218 @@ async fn test_perp_oracle_peg() -> Result<(), TransportError> {
|
||||||
Ok(())
|
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) {
|
async fn assert_no_perp_orders(solana: &SolanaCookie, account_0: Pubkey) {
|
||||||
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
|
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
|
||||||
|
|
||||||
|
|
|
@ -390,6 +390,14 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> {
|
||||||
mango_account_1.perp_spot_transfers, -expected_total_settle,
|
mango_account_1.perp_spot_transfers, -expected_total_settle,
|
||||||
"net_settled on account 1 updated with loss from settlement"
|
"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
|
// Change the oracle to a reasonable price in other direction
|
||||||
|
|
|
@ -111,6 +111,7 @@ describe('Health Cache', () => {
|
||||||
ZERO_I80F48(),
|
ZERO_I80F48(),
|
||||||
ZERO_I80F48(),
|
ZERO_I80F48(),
|
||||||
new BN(0),
|
new BN(0),
|
||||||
|
ZERO_I80F48(),
|
||||||
);
|
);
|
||||||
const pi1 = PerpInfo.fromPerpPosition(pM, pp);
|
const pi1 = PerpInfo.fromPerpPosition(pM, pp);
|
||||||
|
|
||||||
|
@ -227,6 +228,7 @@ describe('Health Cache', () => {
|
||||||
ZERO_I80F48(),
|
ZERO_I80F48(),
|
||||||
ZERO_I80F48(),
|
ZERO_I80F48(),
|
||||||
new BN(0),
|
new BN(0),
|
||||||
|
ZERO_I80F48(),
|
||||||
);
|
);
|
||||||
const pi1 = PerpInfo.fromPerpPosition(pM, pp);
|
const pi1 = PerpInfo.fromPerpPosition(pM, pp);
|
||||||
|
|
||||||
|
|
|
@ -175,7 +175,6 @@ export class MangoAccount {
|
||||||
|
|
||||||
public getPerpPosition(
|
public getPerpPosition(
|
||||||
perpMarketIndex: PerpMarketIndex,
|
perpMarketIndex: PerpMarketIndex,
|
||||||
useEventQueue?: boolean,
|
|
||||||
): PerpPosition | undefined {
|
): PerpPosition | undefined {
|
||||||
return this.perps.find((pp) => pp.marketIndex == perpMarketIndex);
|
return this.perps.find((pp) => pp.marketIndex == perpMarketIndex);
|
||||||
}
|
}
|
||||||
|
@ -1180,6 +1179,7 @@ export class PerpPosition {
|
||||||
I80F48.from(dto.realizedTradePnlNative),
|
I80F48.from(dto.realizedTradePnlNative),
|
||||||
I80F48.from(dto.realizedOtherPnlNative),
|
I80F48.from(dto.realizedOtherPnlNative),
|
||||||
dto.settlePnlLimitRealizedTrade,
|
dto.settlePnlLimitRealizedTrade,
|
||||||
|
I80F48.from(dto.realizedPnlForPositionNative),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1208,6 +1208,7 @@ export class PerpPosition {
|
||||||
ZERO_I80F48(),
|
ZERO_I80F48(),
|
||||||
ZERO_I80F48(),
|
ZERO_I80F48(),
|
||||||
new BN(0),
|
new BN(0),
|
||||||
|
ZERO_I80F48(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1233,12 +1234,17 @@ export class PerpPosition {
|
||||||
public realizedTradePnlNative: I80F48,
|
public realizedTradePnlNative: I80F48,
|
||||||
public realizedOtherPnlNative: I80F48,
|
public realizedOtherPnlNative: I80F48,
|
||||||
public settlePnlLimitRealizedTrade: BN,
|
public settlePnlLimitRealizedTrade: BN,
|
||||||
|
public realizedPnlForPositionNative: I80F48,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
isActive(): boolean {
|
isActive(): boolean {
|
||||||
return this.marketIndex !== PerpPosition.PerpMarketIndexUnset;
|
return this.marketIndex !== PerpPosition.PerpMarketIndexUnset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getBasePositionNative(perpMarket: PerpMarket): I80F48 {
|
||||||
|
return I80F48.fromI64(this.basePositionLots.mul(perpMarket.baseLotSize));
|
||||||
|
}
|
||||||
|
|
||||||
public getBasePositionUi(
|
public getBasePositionUi(
|
||||||
perpMarket: PerpMarket,
|
perpMarket: PerpMarket,
|
||||||
useEventQueue?: boolean,
|
useEventQueue?: boolean,
|
||||||
|
@ -1316,13 +1322,15 @@ export class PerpPosition {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getAverageEntryPriceUi(perpMarket: PerpMarket): number {
|
public getAverageEntryPrice(perpMarket: PerpMarket): I80F48 {
|
||||||
if (perpMarket.perpMarketIndex !== this.marketIndex) {
|
return I80F48.fromNumber(this.avgEntryPricePerBaseLot).mul(
|
||||||
throw new Error("PerpPosition doesn't belong to the given market!");
|
I80F48.fromI64(perpMarket.baseLotSize),
|
||||||
}
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getAverageEntryPriceUi(perpMarket: PerpMarket): number {
|
||||||
return perpMarket.priceNativeToUi(
|
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) {
|
if (perpMarket.perpMarketIndex !== this.marketIndex) {
|
||||||
throw new Error("PerpPosition doesn't belong to the given market!");
|
throw new Error("PerpPosition doesn't belong to the given market!");
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.quotePositionNative.add(
|
return this.quotePositionNative.add(
|
||||||
I80F48.fromI64(this.basePositionLots.mul(perpMarket.baseLotSize)).mul(
|
this.getBasePositionNative(perpMarket).mul(perpMarket.price),
|
||||||
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!");
|
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 realizedTradePnlNative: I80F48Dto,
|
||||||
public realizedOtherPnlNative: I80F48Dto,
|
public realizedOtherPnlNative: I80F48Dto,
|
||||||
public settlePnlLimitRealizedTrade: BN,
|
public settlePnlLimitRealizedTrade: BN,
|
||||||
|
public realizedPnlForPositionNative: I80F48Dto,
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5365,12 +5365,27 @@ export type MangoV4 = {
|
||||||
],
|
],
|
||||||
"type": "i64"
|
"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",
|
"name": "reserved",
|
||||||
"type": {
|
"type": {
|
||||||
"array": [
|
"array": [
|
||||||
"u8",
|
"u8",
|
||||||
104
|
88
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13119,12 +13134,27 @@ export const IDL: MangoV4 = {
|
||||||
],
|
],
|
||||||
"type": "i64"
|
"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",
|
"name": "reserved",
|
||||||
"type": {
|
"type": {
|
||||||
"array": [
|
"array": [
|
||||||
"u8",
|
"u8",
|
||||||
104
|
88
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,7 @@ async function settlePnl(
|
||||||
const pp = mangoAccount
|
const pp = mangoAccount
|
||||||
.perpActive()
|
.perpActive()
|
||||||
.find((pp) => pp.marketIndex === perpMarket.perpMarketIndex)!;
|
.find((pp) => pp.marketIndex === perpMarket.perpMarketIndex)!;
|
||||||
const pnl = pp.getPnl(perpMarket);
|
const pnl = pp.getUnsettledPnl(perpMarket);
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`Avg entry price - ${pp.getAverageEntryPriceUi(
|
`Avg entry price - ${pp.getAverageEntryPriceUi(
|
||||||
|
|
Loading…
Reference in New Issue