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();
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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,
|
||||
) {}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Reference in New Issue