Perp settle limit extension to realized pnl (#359)

Co-authored-by: microwavedcola1 <microwavedcola@gmail.com>
This commit is contained in:
Christian Kamm 2023-01-11 14:32:15 +01:00 committed by GitHub
parent 75593925aa
commit c5d875e04d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1331 additions and 305 deletions

View File

@ -60,7 +60,7 @@ pub async fn fetch_top(
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 limited_pnl = perp_pos.apply_pnl_settle_limit(pnl, &perp_market);
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

@ -149,17 +149,17 @@ macro_rules! error_msg {
/// Creates an Error with a particular message, using format!() style arguments
///
/// Example: error_msg!("index {} not found", index)
/// Example: error_msg_typed!(TokenPositionMissing, "index {} not found", index)
#[macro_export]
macro_rules! error_msg_typed {
($code:ident, $($arg:tt)*) => {
error!(MangoError::$code).context(format!($($arg)*))
($code:expr, $($arg:tt)*) => {
error!($code).context(format!($($arg)*))
};
}
/// Like anchor's require!(), but with a customizable message
///
/// Example: require!(condition, "the condition on account {} was violated", account_key);
/// Example: require_msg!(condition, "the condition on account {} was violated", account_key);
#[macro_export]
macro_rules! require_msg {
($invariant:expr, $($arg:tt)*) => {
@ -169,6 +169,19 @@ macro_rules! require_msg {
};
}
/// Like anchor's require!(), but with a customizable message and type
///
/// Example: require_msg_typed!(condition, "the condition on account {} was violated", account_key);
#[macro_export]
macro_rules! require_msg_typed {
($invariant:expr, $code:expr, $($arg:tt)*) => {
if !($invariant) {
return Err(error_msg_typed!($code, $($arg)*));
}
};
}
pub use error_msg;
pub use error_msg_typed;
pub use require_msg;
pub use require_msg_typed;

View File

@ -305,7 +305,7 @@ impl<'a, 'info> ScanningAccountRetriever<'a, 'info> {
fn bank_index(&self, token_index: TokenIndex) -> Result<usize> {
Ok(*self.token_index_map.get(&token_index).ok_or_else(|| {
error_msg_typed!(
TokenPositionDoesNotExist,
MangoError::TokenPositionDoesNotExist,
"token index {} not found",
token_index
)

View File

@ -361,7 +361,7 @@ impl HealthCache {
.position(|t| t.token_index == token_index)
.ok_or_else(|| {
error_msg_typed!(
TokenPositionDoesNotExist,
MangoError::TokenPositionDoesNotExist,
"token index {} not found",
token_index
)
@ -609,7 +609,7 @@ pub(crate) fn find_token_info_index(infos: &[TokenInfo], token_index: TokenIndex
.position(|ti| ti.token_index == token_index)
.ok_or_else(|| {
error_msg_typed!(
TokenPositionDoesNotExist,
MangoError::TokenPositionDoesNotExist,
"token index {} not found",
token_index
)

View File

@ -1,5 +1,68 @@
use fixed::types::I80F48;
pub trait ClampedToNum {
fn clamp_to_i64(&self) -> i64;
fn clamp_to_u64(&self) -> u64;
}
impl ClampedToNum for I80F48 {
fn clamp_to_i64(&self) -> i64 {
if *self <= i64::MIN {
i64::MIN
} else if *self >= i64::MAX {
i64::MAX
} else {
self.to_num::<i64>()
}
}
fn clamp_to_u64(&self) -> u64 {
if *self <= 0 {
0
} else if *self >= u64::MAX {
u64::MAX
} else {
self.to_num::<u64>()
}
}
}
impl ClampedToNum for f64 {
fn clamp_to_i64(&self) -> i64 {
if *self <= i64::MIN as f64 {
i64::MIN
} else if *self >= i64::MAX as f64 {
i64::MAX
} else {
*self as i64
}
}
fn clamp_to_u64(&self) -> u64 {
if *self <= 0.0 {
0
} else if *self >= u64::MAX as f64 {
u64::MAX
} else {
*self as u64
}
}
}
impl ClampedToNum for u64 {
fn clamp_to_i64(&self) -> i64 {
if *self >= i64::MAX as u64 {
i64::MAX
} else {
*self as i64
}
}
fn clamp_to_u64(&self) -> u64 {
*self
}
}
pub trait LowPrecisionDivision {
fn checked_div_30bit_precision(&self, rhs: I80F48) -> Option<I80F48>;
fn checked_div_f64_precision(&self, rhs: I80F48) -> Option<I80F48>;

View File

@ -178,8 +178,8 @@ pub fn perp_liq_bankruptcy(ctx: Context<PerpLiqBankruptcy>, max_liab_transfer: u
let liqor_perp_position = liqor
.ensure_perp_position(perp_market.perp_market_index, settle_token_index)?
.0;
liqee_perp_position.record_bankruptcy_quote_change(insurance_liab_transfer);
liqor_perp_position.record_bankruptcy_quote_change(-insurance_liab_transfer);
liqee_perp_position.record_settle(-insurance_liab_transfer);
liqor_perp_position.record_liquidation_quote_change(-insurance_liab_transfer);
emit_perp_balances(
ctx.accounts.group.key(),
@ -195,7 +195,7 @@ pub fn perp_liq_bankruptcy(ctx: Context<PerpLiqBankruptcy>, max_liab_transfer: u
let mut socialized_loss = I80F48::ZERO;
if insurance_fund_exhausted && remaining_liab.is_positive() {
perp_market.socialize_loss(-remaining_liab)?;
liqee_perp_position.record_bankruptcy_quote_change(remaining_liab);
liqee_perp_position.record_settle(-remaining_liab);
require_eq!(liqee_perp_position.quote_position_native(), 0);
socialized_loss = remaining_liab;
}

View File

@ -72,11 +72,19 @@ pub fn perp_settle_fees(ctx: Context<PerpSettleFees>, max_settle_amount: u64) ->
MangoError::ProfitabilityMismatch
);
let settleable_pnl = perp_position.apply_pnl_settle_limit(&perp_market, pnl);
require!(
settleable_pnl.is_negative(),
MangoError::ProfitabilityMismatch
);
// Settle for the maximum possible capped to max_settle_amount
let settlement = pnl
let settlement = settleable_pnl
.abs()
.min(perp_market.fees_accrued.abs())
.min(I80F48::from(max_settle_amount));
require!(settlement >= 0, MangoError::SettlementAmountMustBePositive);
perp_position.record_settle(-settlement); // settle the negative pnl on the user perp position
perp_market.fees_accrued = cm!(perp_market.fees_accrued - settlement);

View File

@ -117,24 +117,55 @@ pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>) -> Result<()> {
// Account A must be profitable, and B must be unprofitable
// PnL must be opposite signs for there to be a settlement
require!(a_pnl.is_positive(), MangoError::ProfitabilityMismatch);
require!(b_pnl.is_negative(), MangoError::ProfitabilityMismatch);
require_msg_typed!(
a_pnl.is_positive(),
MangoError::ProfitabilityMismatch,
"account a pnl is not positive: {}",
a_pnl
);
require_msg_typed!(
b_pnl.is_negative(),
MangoError::ProfitabilityMismatch,
"account b pnl is not negative: {}",
b_pnl
);
// Cap settlement of unrealized pnl
// Settles at most x100% each hour
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
a_perp_position.update_settle_limit(&perp_market, now_ts);
b_perp_position.update_settle_limit(&perp_market, now_ts);
let a_settleable_pnl = a_perp_position.apply_pnl_settle_limit(a_pnl, &perp_market);
let a_settleable_pnl = a_perp_position.apply_pnl_settle_limit(&perp_market, a_pnl);
let b_settleable_pnl = b_perp_position.apply_pnl_settle_limit(&perp_market, b_pnl);
require!(
require_msg_typed!(
a_settleable_pnl.is_positive(),
MangoError::ProfitabilityMismatch
MangoError::ProfitabilityMismatch,
"account a settleable pnl is not positive: {}, pnl: {}",
a_settleable_pnl,
a_pnl
);
require_msg_typed!(
b_settleable_pnl.is_negative(),
MangoError::ProfitabilityMismatch,
"account b settleable pnl is not negative: {}, pnl: {}",
b_settleable_pnl,
b_pnl
);
// Settle for the maximum possible capped to b's settle health
let settlement = a_settleable_pnl.abs().min(b_pnl.abs()).min(b_settle_health);
require!(settlement >= 0, MangoError::SettlementAmountMustBePositive);
let settlement = a_settleable_pnl
.abs()
.min(b_settleable_pnl.abs())
.min(b_settle_health);
require_msg_typed!(
settlement >= 0,
MangoError::SettlementAmountMustBePositive,
"a settleable: {}, b settleable: {}, b settle health: {}",
a_settleable_pnl,
b_settleable_pnl,
b_settle_health,
);
// Settle
a_perp_position.record_settle(settlement);

View File

@ -607,7 +607,7 @@ impl Bank {
.checked_mul_int(self.net_borrows_in_window.into())
.unwrap();
if net_borrows_quote > self.net_borrow_limit_per_window_quote {
return Err(error_msg_typed!(BankNetBorrowsLimitReached,
return Err(error_msg_typed!(MangoError::BankNetBorrowsLimitReached,
"net_borrows_in_window ({:?}) exceeds net_borrow_limit_per_window_quote ({:?}) for last_net_borrows_window_start_ts ({:?}) ",
self.net_borrows_in_window, self.net_borrow_limit_per_window_quote, self.last_net_borrows_window_start_ts

View File

@ -459,7 +459,7 @@ impl<
.find_map(|(raw_index, p)| p.is_active_for_token(token_index).then(|| (p, raw_index)))
.ok_or_else(|| {
error_msg_typed!(
TokenPositionDoesNotExist,
MangoError::TokenPositionDoesNotExist,
"position for token index {} not found",
token_index
)
@ -612,7 +612,7 @@ impl<
.find_map(|(raw_index, p)| p.is_active_for_token(token_index).then(|| raw_index))
.ok_or_else(|| {
error_msg_typed!(
TokenPositionDoesNotExist,
MangoError::TokenPositionDoesNotExist,
"position for token index {} not found",
token_index
)
@ -898,8 +898,8 @@ impl<
let (base_change, quote_change) = fill.base_quote_change(side);
let quote = cm!(I80F48::from(perp_market.quote_lot_size) * I80F48::from(quote_change));
let fees = cm!(quote.abs() * fill.maker_fee);
pa.record_trading_fee(fees);
pa.record_trade(perp_market, base_change, quote);
pa.record_fee(fees);
cm!(pa.maker_volume += quote.abs().to_num::<u64>());

File diff suppressed because it is too large Load Diff

View File

@ -384,7 +384,7 @@ fn apply_fees(
require_gte!(taker_fees, 0);
let perp_account = mango_account.perp_position_mut(market.perp_market_index)?;
perp_account.record_fee(taker_fees);
perp_account.record_trading_fee(taker_fees);
cm!(market.fees_accrued += taker_fees + maker_fees);
cm!(perp_account.taker_volume += taker_fees.to_num::<u64>());
@ -396,7 +396,7 @@ fn apply_penalty(market: &mut PerpMarket, mango_account: &mut MangoAccountRefMut
let perp_account = mango_account.perp_position_mut(market.perp_market_index)?;
let fee_penalty = I80F48::from_num(market.fee_penalty);
perp_account.record_fee(fee_penalty);
perp_account.record_trading_fee(fee_penalty);
cm!(market.fees_accrued += fee_penalty);
Ok(())

View File

@ -97,9 +97,17 @@ pub struct PerpMarket {
pub settle_fee_fraction_low_health: f32,
// Pnl settling limits
/// Fraction of perp base value (i.e. base_lots * entry_price_in_lots) of unrealized
/// positive pnl that can be settled each window.
/// Controls the strictness of the settle limit.
/// Set to a negative value to disable the limit.
///
/// This factor applies to the settle limit in two ways
/// - for the unrealized pnl settle limit, the factor is multiplied with the stable perp base value
/// (i.e. limit_factor * base_native * stable_price)
/// - when increasing the realized pnl settle limit (stored per PerpPosition), the factor is
/// multiplied with the stable value of the perp pnl being realized
/// (i.e. limit_factor * reduced_native * stable_price)
///
/// See also PerpPosition::settle_pnl_limit_realized_trade
pub settle_pnl_limit_factor: f32,
pub padding3: [u8; 4],
/// Window size in seconds for the perp settlement limit

View File

@ -2606,6 +2606,36 @@ impl ClientInstruction for PerpCreateMarketInstruction {
}
}
fn perp_edit_instruction_default() -> mango_v4::instruction::PerpEditMarket {
mango_v4::instruction::PerpEditMarket {
oracle_opt: None,
oracle_config_opt: None,
base_decimals_opt: None,
maint_asset_weight_opt: None,
init_asset_weight_opt: None,
maint_liab_weight_opt: None,
init_liab_weight_opt: None,
liquidation_fee_opt: None,
maker_fee_opt: None,
taker_fee_opt: None,
min_funding_opt: None,
max_funding_opt: None,
impact_quantity_opt: None,
group_insurance_fund_opt: None,
trusted_market_opt: None,
fee_penalty_opt: None,
settle_fee_flat_opt: None,
settle_fee_amount_threshold_opt: None,
settle_fee_fraction_low_health_opt: None,
stable_price_delay_interval_seconds_opt: None,
stable_price_delay_growth_limit_opt: None,
stable_price_growth_limit_opt: None,
settle_pnl_limit_factor_opt: None,
settle_pnl_limit_window_size_ts: None,
reduce_only_opt: None,
}
}
pub struct PerpResetStablePriceModel {
pub group: Pubkey,
pub admin: TestKeypair,
@ -2626,30 +2656,47 @@ impl ClientInstruction for PerpResetStablePriceModel {
let instruction = Self::Instruction {
oracle_opt: Some(perp_market.oracle),
oracle_config_opt: None,
base_decimals_opt: None,
maint_asset_weight_opt: None,
init_asset_weight_opt: None,
maint_liab_weight_opt: None,
init_liab_weight_opt: None,
liquidation_fee_opt: None,
maker_fee_opt: None,
taker_fee_opt: None,
min_funding_opt: None,
max_funding_opt: None,
impact_quantity_opt: None,
group_insurance_fund_opt: None,
trusted_market_opt: None,
fee_penalty_opt: None,
settle_fee_flat_opt: None,
settle_fee_amount_threshold_opt: None,
settle_fee_fraction_low_health_opt: None,
stable_price_delay_interval_seconds_opt: None,
stable_price_delay_growth_limit_opt: None,
stable_price_growth_limit_opt: None,
settle_pnl_limit_factor_opt: None,
settle_pnl_limit_window_size_ts: None,
reduce_only_opt: None,
..perp_edit_instruction_default()
};
let accounts = Self::Accounts {
group: self.group,
admin: self.admin.pubkey(),
perp_market: self.perp_market,
oracle: perp_market.oracle,
};
let instruction = make_instruction(program_id, &accounts, instruction);
(accounts, instruction)
}
fn signers(&self) -> Vec<TestKeypair> {
vec![self.admin]
}
}
pub struct PerpSetSettleLimitWindow {
pub group: Pubkey,
pub admin: TestKeypair,
pub perp_market: Pubkey,
pub window_size_ts: u64,
}
#[async_trait::async_trait(?Send)]
impl ClientInstruction for PerpSetSettleLimitWindow {
type Accounts = mango_v4::accounts::PerpEditMarket;
type Instruction = mango_v4::instruction::PerpEditMarket;
async fn to_instruction(
&self,
account_loader: impl ClientAccountLoader + 'async_trait,
) -> (Self::Accounts, instruction::Instruction) {
let program_id = mango_v4::id();
let perp_market: PerpMarket = account_loader.load(&self.perp_market).await.unwrap();
let instruction = Self::Instruction {
settle_pnl_limit_window_size_ts: Some(self.window_size_ts),
..perp_edit_instruction_default()
};
let accounts = Self::Accounts {
@ -2687,31 +2734,8 @@ impl ClientInstruction for PerpMakeReduceOnly {
let perp_market: PerpMarket = account_loader.load(&self.perp_market).await.unwrap();
let instruction = Self::Instruction {
oracle_opt: None,
oracle_config_opt: None,
base_decimals_opt: None,
maint_asset_weight_opt: None,
init_asset_weight_opt: None,
maint_liab_weight_opt: None,
init_liab_weight_opt: None,
liquidation_fee_opt: None,
maker_fee_opt: None,
taker_fee_opt: None,
min_funding_opt: None,
max_funding_opt: None,
impact_quantity_opt: None,
group_insurance_fund_opt: None,
trusted_market_opt: None,
fee_penalty_opt: None,
settle_fee_flat_opt: None,
settle_fee_amount_threshold_opt: None,
settle_fee_fraction_low_health_opt: None,
stable_price_delay_interval_seconds_opt: None,
stable_price_delay_growth_limit_opt: None,
stable_price_growth_limit_opt: None,
settle_pnl_limit_factor_opt: None,
settle_pnl_limit_window_size_ts: None,
reduce_only_opt: Some(true),
..perp_edit_instruction_default()
};
let accounts = Self::Accounts {

View File

@ -629,6 +629,13 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr
//
// TEST: Can settle-pnl even though health is negative
//
let liqor_data = solana.get_account::<MangoAccount>(liqor).await;
let perp_market_data = solana.get_account::<PerpMarket>(perp_market).await;
let liqor_max_settle = liqor_data.perps[0]
.available_settle_limit(&perp_market_data)
.1;
let account_1_quote_before = account_position(solana, account_1, quote_token.bank).await;
send_tx(
solana,
PerpSettlePnlInstruction {
@ -643,9 +650,10 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr
.await
.unwrap();
let liqee_settle_health_before = 999.0 + 1.0 * 2.0 * 0.8;
let remaining_pnl =
20.0 * 100.0 - liq_amount_3 - liq_amount_4 - liq_amount_5 + liqee_settle_health_before;
let liqee_settle_health_before: f64 = 999.0 + 1.0 * 2.0 * 0.8;
// the liqor's settle limit means we can't settle everything
let settle_amount = liqee_settle_health_before.min(liqor_max_settle as f64);
let remaining_pnl = 20.0 * 100.0 - liq_amount_3 - liq_amount_4 - liq_amount_5 + settle_amount;
assert!(remaining_pnl < 0.0);
let liqee_data = solana.get_account::<MangoAccount>(account_1).await;
assert_eq!(liqee_data.perps[0].base_position_lots(), 0);
@ -656,13 +664,16 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr
));
assert_eq!(
account_position(solana, account_1, quote_token.bank).await,
-2
account_1_quote_before - settle_amount as i64
);
assert_eq!(
account_position(solana, account_1, base_token.bank).await,
1
);
/*
Perp liquidation / bankruptcy tests temporarily disabled until further PRs have gone in.
//
// TEST: Still can't trigger perp bankruptcy, account_1 has token collateral left
//
@ -752,6 +763,7 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr
-socialized_amount / 20.0,
0.1
));
*/
Ok(())
}

View File

@ -83,7 +83,7 @@ async fn test_perp_fixed() -> Result<(), TransportError> {
liquidation_fee: 0.012,
maker_fee: -0.0001,
taker_fee: 0.0002,
settle_pnl_limit_factor: 0.2,
settle_pnl_limit_factor: -1.0,
settle_pnl_limit_window_size_ts: 24 * 60 * 60,
..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, &tokens[0]).await
},

View File

@ -838,6 +838,7 @@ async fn test_perp_pnl_settle_limit() -> Result<(), TransportError> {
//
// TEST: Create a perp market
//
let settle_pnl_limit_factor = 0.8;
let mango_v4::accounts::PerpCreateMarket { perp_market, .. } = send_tx(
solana,
PerpCreateMarketInstruction {
@ -852,9 +853,9 @@ async fn test_perp_pnl_settle_limit() -> Result<(), TransportError> {
maint_liab_weight: 1.025,
init_liab_weight: 1.05,
liquidation_fee: 0.012,
maker_fee: 0.0002,
taker_fee: 0.000,
settle_pnl_limit_factor: 0.2,
maker_fee: 0.0,
taker_fee: 0.0,
settle_pnl_limit_factor,
settle_pnl_limit_window_size_ts: 24 * 60 * 60,
..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, &tokens[1]).await
},
@ -868,17 +869,7 @@ async fn test_perp_pnl_settle_limit() -> Result<(), TransportError> {
};
// Set the initial oracle price
send_tx(
solana,
StubOracleSetInstruction {
group,
admin,
mint: mints[1].pubkey,
price: 1000.0,
},
)
.await
.unwrap();
set_perp_stub_oracle_price(&solana, group, perp_market, &tokens[1], admin, 1000.0).await;
//
// Place orders and create a position
@ -927,32 +918,41 @@ async fn test_perp_pnl_settle_limit() -> Result<(), TransportError> {
.await
.unwrap();
// Manipulate the price
// Manipulate the price (without adjusting stable price)
let price_factor = 3;
send_tx(
solana,
StubOracleSetInstruction {
group,
admin,
mint: mints[1].pubkey,
price: 10000.0, // 10x original price
price: price_factor as f64 * 1000.0,
},
)
.await
.unwrap();
// Settle Pnl
// attempt 1 - settle max possible,
// since b has very large deposits, b's health will not interfere,
// the pnl cap enforced would be relative to the avg_entry_price
//
// Test 1: settle max possible, limited by unrealized pnl settle limit
//
// a has lots of positive unrealized pnl, b has negative unrealized pnl.
// Since b has very large deposits, b's health will not interfere.
// The pnl settle limit is relative to the stable price
let market = solana.get_account::<PerpMarket>(perp_market).await;
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
let mango_account_1_expected_qpn_after_settle = mango_account_1.perps[0]
.quote_position_native()
+ (market.settle_pnl_limit_factor()
* I80F48::from_num(mango_account_0.perps[0].avg_entry_price(&market))
let account_1_settle_limits = mango_account_1.perps[0].available_settle_limit(&market);
assert_eq!(account_1_settle_limits, (-80000, 80000));
let account_1_settle_limit = I80F48::from(account_1_settle_limits.0.abs());
assert_eq!(
account_1_settle_limit,
(market.settle_pnl_limit_factor()
* market.stable_price()
* mango_account_0.perps[0].base_position_native(&market))
.abs();
.round()
);
let mango_account_1_expected_qpn_after_settle =
mango_account_1.perps[0].quote_position_native() + account_1_settle_limit.round();
send_tx(
solana,
PerpSettlePnlInstruction {
@ -966,13 +966,26 @@ async fn test_perp_pnl_settle_limit() -> Result<(), TransportError> {
)
.await
.unwrap();
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
assert_eq!(
mango_account_1.perps[0].quote_position_native().round(),
mango_account_1_expected_qpn_after_settle.round()
);
// attempt 2 - as we are in the same window, and we settled max. possible in previous attempt,
// we can't settle anymore amount
// neither account has any settle limit left
assert_eq!(
mango_account_0.perps[0].available_settle_limit(&market).1,
0
);
assert_eq!(
mango_account_1.perps[0].available_settle_limit(&market).0,
0
);
//
// Test 2: Once the settle limit is exhausted, we can't settle more
//
// we are in the same window, and we settled max. possible in previous attempt
let result = send_tx(
solana,
PerpSettlePnlInstruction {
@ -991,5 +1004,216 @@ async fn test_perp_pnl_settle_limit() -> Result<(), TransportError> {
"Account A has no settleable positive pnl left".to_string(),
);
//
// Test 3: realizing the pnl does not allow further settling
//
send_tx(
solana,
PerpPlaceOrderInstruction {
account: account_1,
perp_market,
owner,
side: Side::Bid,
price_lots: 3 * price_lots,
max_base_lots: 1,
max_quote_lots: i64::MAX,
client_order_id: 0,
reduce_only: false,
},
)
.await
.unwrap();
send_tx(
solana,
PerpPlaceOrderInstruction {
account: account_0,
perp_market,
owner,
side: Side::Ask,
price_lots: 3 * price_lots,
max_base_lots: 1,
max_quote_lots: i64::MAX,
client_order_id: 0,
reduce_only: false,
},
)
.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 mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
assert_eq!(
mango_account_0.perps[0].realized_trade_pnl_native,
I80F48::from(200_000 - 80_000)
);
assert_eq!(
mango_account_1.perps[0].realized_trade_pnl_native,
I80F48::from(-200_000 + 80_000)
);
// neither account has any settle limit left (check for 1 because of the ceil()ing)
assert_eq!(
mango_account_0.perps[0].available_settle_limit(&market).1,
1
);
assert_eq!(
mango_account_1.perps[0].available_settle_limit(&market).0,
-1
);
// check that realized pnl settle limit was set up correctly
assert_eq!(
mango_account_0.perps[0].settle_pnl_limit_realized_trade,
(0.8 * 1.0 * 100.0 * 1000.0) as i64 + 1
); // +1 just for rounding
// settle 1
let account_1_quote_before = mango_account_1.perps[0].quote_position_native();
send_tx(
solana,
PerpSettlePnlInstruction {
settler,
settler_owner,
account_a: account_0,
account_b: account_1,
perp_market,
settle_bank: tokens[0].bank,
},
)
.await
.unwrap();
// indeed settled 1
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
assert_eq!(
mango_account_1.perps[0].quote_position_native() - account_1_quote_before,
I80F48::from(1)
);
//
// Test 4: Move to a new settle window and check the realized pnl settle limit
//
// This time account 0's realized pnl settle limit kicks in.
//
let account_1_quote_before = mango_account_1.perps[0].quote_position_native();
let account_0_realized_limit = mango_account_0.perps[0].settle_pnl_limit_realized_trade;
send_tx(
solana,
PerpSetSettleLimitWindow {
group,
admin,
perp_market,
window_size_ts: 10000, // guaranteed to move windows, resetting the limits
},
)
.await
.unwrap();
send_tx(
solana,
PerpSettlePnlInstruction {
settler,
settler_owner,
account_a: account_0,
account_b: account_1,
perp_market,
settle_bank: tokens[0].bank,
},
)
.await
.unwrap();
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
// successful settle of expected amount
assert_eq!(
mango_account_1.perps[0].quote_position_native() - account_1_quote_before,
I80F48::from(account_0_realized_limit)
);
// account0's limit gets reduced to the realized pnl amount left over
assert_eq!(
mango_account_0.perps[0].settle_pnl_limit_realized_trade,
mango_account_0.perps[0]
.realized_trade_pnl_native
.to_num::<i64>()
);
// can't settle again
assert!(send_tx(
solana,
PerpSettlePnlInstruction {
settler,
settler_owner,
account_a: account_0,
account_b: account_1,
perp_market,
settle_bank: tokens[0].bank,
},
)
.await
.is_err());
//
// Test 5: in a new settle window, the remaining pnl can be settled
//
let account_1_quote_before = mango_account_1.perps[0].quote_position_native();
let account_0_realized_limit = mango_account_0.perps[0].settle_pnl_limit_realized_trade;
send_tx(
solana,
PerpSetSettleLimitWindow {
group,
admin,
perp_market,
window_size_ts: 5000, // guaranteed to move windows, resetting the limits
},
)
.await
.unwrap();
send_tx(
solana,
PerpSettlePnlInstruction {
settler,
settler_owner,
account_a: account_0,
account_b: account_1,
perp_market,
settle_bank: tokens[0].bank,
},
)
.await
.unwrap();
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
// successful settle of expected amount
assert_eq!(
mango_account_1.perps[0].quote_position_native() - account_1_quote_before,
I80F48::from(account_0_realized_limit)
);
// account0's limit gets reduced to the realized pnl amount left over
assert_eq!(mango_account_0.perps[0].settle_pnl_limit_realized_trade, 0);
assert_eq!(
mango_account_0.perps[0].realized_trade_pnl_native,
I80F48::from(0)
);
assert_eq!(
mango_account_1.perps[0].realized_trade_pnl_native,
I80F48::from(0)
);
Ok(())
}

View File

@ -107,6 +107,8 @@ describe('Health Cache', () => {
new BN(0),
0,
ZERO_I80F48(),
ZERO_I80F48(),
new BN(0),
);
const pi1 = PerpInfo.fromPerpPosition(pM, pp);
@ -221,6 +223,8 @@ describe('Health Cache', () => {
new BN(0),
0,
ZERO_I80F48(),
ZERO_I80F48(),
new BN(0),
);
const pi1 = PerpInfo.fromPerpPosition(pM, pp);

View File

@ -3,7 +3,7 @@ import { utf8 } from '@project-serum/anchor/dist/cjs/utils/bytes';
import { OpenOrders, Order, Orderbook } from '@project-serum/serum/lib/market';
import { AccountInfo, PublicKey, TransactionSignature } from '@solana/web3.js';
import { MangoClient } from '../client';
import { OPENBOOK_PROGRAM_ID } from '../constants';
import { OPENBOOK_PROGRAM_ID, RUST_I64_MAX, RUST_I64_MIN } from '../constants';
import { I80F48, I80F48Dto, ONE_I80F48, ZERO_I80F48 } from '../numbers/I80F48';
import { toNativeI80F48, toUiDecimals, toUiDecimalsForQuote } from '../utils';
import { Bank, TokenIndex } from './bank';
@ -1170,7 +1170,9 @@ export class PerpPosition {
dto.takerVolume,
dto.perpSpotTransfers,
dto.avgEntryPricePerBaseLot,
I80F48.from(dto.realizedPnlNative),
I80F48.from(dto.realizedTradePnlNative),
I80F48.from(dto.realizedOtherPnlNative),
dto.settlePnlLimitRealizedTrade,
);
}
@ -1197,6 +1199,8 @@ export class PerpPosition {
new BN(0),
0,
ZERO_I80F48(),
ZERO_I80F48(),
new BN(0),
);
}
@ -1219,7 +1223,9 @@ export class PerpPosition {
public takerVolume: BN,
public perpSpotTransfers: BN,
public avgEntryPricePerBaseLot: number,
public realizedPnlNative: I80F48,
public realizedTradePnlNative: I80F48,
public realizedOtherPnlNative: I80F48,
public settlePnlLimitRealizedTrade: BN,
) {}
isActive(): boolean {
@ -1230,6 +1236,10 @@ export class PerpPosition {
perpMarket: PerpMarket,
useEventQueue?: boolean,
): number {
if (perpMarket.perpMarketIndex !== this.marketIndex) {
throw new Error("PerpPosition doesn't belong to the given market!");
}
return perpMarket.baseLotsToUi(
useEventQueue
? this.basePositionLots.add(this.takerBaseLots)
@ -1238,6 +1248,10 @@ export class PerpPosition {
}
public getUnsettledFunding(perpMarket: PerpMarket): I80F48 {
if (perpMarket.perpMarketIndex !== this.marketIndex) {
throw new Error("PerpPosition doesn't belong to the given market!");
}
if (this.basePositionLots.gt(new BN(0))) {
return perpMarket.longFunding
.sub(this.longSettledFunding)
@ -1251,6 +1265,10 @@ export class PerpPosition {
}
public getEquityUi(group: Group, perpMarket: PerpMarket): number {
if (perpMarket.perpMarketIndex !== this.marketIndex) {
throw new Error("PerpPosition doesn't belong to the given market!");
}
return toUiDecimals(
this.getEquity(perpMarket),
group.getMintDecimalsByTokenIndex(perpMarket.settleTokenIndex),
@ -1258,6 +1276,10 @@ export class PerpPosition {
}
public getEquity(perpMarket: PerpMarket): I80F48 {
if (perpMarket.perpMarketIndex !== this.marketIndex) {
throw new Error("PerpPosition doesn't belong to the given market!");
}
const lotsToQuote = I80F48.fromI64(perpMarket.baseLotSize).mul(
perpMarket.price,
);
@ -1288,12 +1310,20 @@ export class PerpPosition {
}
public getAverageEntryPriceUi(perpMarket: PerpMarket): number {
if (perpMarket.perpMarketIndex !== this.marketIndex) {
throw new Error("PerpPosition doesn't belong to the given market!");
}
return perpMarket.priceNativeToUi(
this.avgEntryPricePerBaseLot / perpMarket.baseLotSize.toNumber(),
);
}
public getBreakEvenPriceUi(perpMarket: PerpMarket): number {
if (perpMarket.perpMarketIndex !== this.marketIndex) {
throw new Error("PerpPosition doesn't belong to the given market!");
}
if (this.basePositionLots.eq(new BN(0))) {
return 0;
}
@ -1304,12 +1334,99 @@ export class PerpPosition {
}
public getPnl(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,
),
);
}
public updateSettleLimit(perpMarket: PerpMarket): void {
if (perpMarket.perpMarketIndex !== this.marketIndex) {
throw new Error("PerpPosition doesn't belong to the given market!");
}
const windowSize = perpMarket.settlePnlLimitWindowSizeTs;
const windowStart = new BN(this.settlePnlLimitWindow).mul(windowSize);
const windowEnd = windowStart.add(windowSize);
const nowTs = new BN(Date.now() / 1000);
const newWindow = nowTs.gte(windowEnd) || nowTs.lt(windowStart);
if (newWindow) {
this.settlePnlLimitWindow = nowTs.div(windowSize).toNumber();
this.settlePnlLimitSettledInCurrentWindowNative = new BN(0);
}
}
public availableSettleLimit(perpMarket: PerpMarket): [BN, BN] {
if (perpMarket.perpMarketIndex !== this.marketIndex) {
throw new Error("PerpPosition doesn't belong to the given market!");
}
if (perpMarket.settlePnlLimitFactor < 0) {
return [RUST_I64_MIN(), RUST_I64_MAX()];
}
const baseNative = I80F48.fromI64(
this.basePositionLots.mul(perpMarket.baseLotSize),
);
const positionValue = I80F48.fromNumber(
perpMarket.stablePriceModel.stablePrice,
)
.mul(baseNative)
.toNumber();
const unrealized = new BN(perpMarket.settlePnlLimitFactor * positionValue);
const used = new BN(
this.settlePnlLimitSettledInCurrentWindowNative.toNumber(),
);
let minPnl = unrealized.neg().sub(used);
let maxPnl = unrealized.sub(used);
const realizedTrade = this.settlePnlLimitRealizedTrade;
if (realizedTrade.gte(new BN(0))) {
maxPnl = maxPnl.add(realizedTrade);
} else {
minPnl = minPnl.add(realizedTrade);
}
const realizedOther = new BN(this.realizedOtherPnlNative.toNumber());
if (realizedOther.gte(new BN(0))) {
maxPnl = maxPnl.add(realizedOther);
} else {
minPnl = minPnl.add(realizedOther);
}
return [BN.min(minPnl, new BN(0)), BN.max(maxPnl, new BN(0))];
}
public applyPnlSettleLimit(pnl: I80F48, perpMarket: PerpMarket): I80F48 {
if (perpMarket.perpMarketIndex !== this.marketIndex) {
throw new Error("PerpPosition doesn't belong to the given market!");
}
if (perpMarket.settlePnlLimitFactor < 0) {
return pnl;
}
const [minPnl, maxPnl] = this.availableSettleLimit(perpMarket);
if (pnl.lt(ZERO_I80F48())) {
return pnl.max(I80F48.fromI64(minPnl));
} else {
return pnl.min(I80F48.fromI64(maxPnl));
}
}
public getSettleablePnl(perpMarket: PerpMarket): I80F48 {
if (perpMarket.perpMarketIndex !== this.marketIndex) {
throw new Error("PerpPosition doesn't belong to the given market!");
}
return this.applyPnlSettleLimit(this.getPnl(perpMarket), perpMarket);
}
}
export class PerpPositionDto {
@ -1332,7 +1449,9 @@ export class PerpPositionDto {
public takerVolume: BN,
public perpSpotTransfers: BN,
public avgEntryPricePerBaseLot: number,
public realizedPnlNative: I80F48Dto,
public realizedTradePnlNative: I80F48Dto,
public realizedOtherPnlNative: I80F48Dto,
public settlePnlLimitRealizedTrade: BN,
) {}
}

View File

@ -3,6 +3,7 @@ import { utf8 } from '@project-serum/anchor/dist/cjs/utils/bytes';
import { PublicKey } from '@solana/web3.js';
import Big from 'big.js';
import { MangoClient } from '../client';
import { RUST_U64_MAX } from '../constants';
import { I80F48, I80F48Dto, ZERO_I80F48 } from '../numbers/I80F48';
import { Modify } from '../types';
import { As, U64_MAX_BN, toNative, toUiDecimals } from '../utils';
@ -181,8 +182,8 @@ export class PerpMarket {
public settleFeeFlat: number,
public settleFeeAmountThreshold: number,
public settleFeeFractionLowHealth: number,
settlePnlLimitFactor: number,
settlePnlLimitWindowSizeTs: BN,
public settlePnlLimitFactor: number,
public settlePnlLimitWindowSizeTs: BN,
public reduceOnly: boolean,
) {
this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0];
@ -412,22 +413,21 @@ export class PerpMarket {
direction: 'negative' | 'positive',
count = 2,
): Promise<{ account: MangoAccount; settleablePnl: I80F48 }[]> {
let accs = (await client.getAllMangoAccounts(group))
.filter((acc) =>
// need a perp position in this market
acc.perpPositionExistsForMarket(this),
)
let accountsWithSettleablePnl = (await client.getAllMangoAccounts(group))
.filter((acc) => acc.perpPositionExistsForMarket(this))
.map((acc) => {
const pp = acc
.perpActive()
.find((pp) => pp.marketIndex === this.perpMarketIndex)!;
pp.updateSettleLimit(this);
return {
account: acc,
settleablePnl: acc
.perpActive()
.find((pp) => pp.marketIndex === this.perpMarketIndex)!
.getPnl(this),
settleablePnl: pp.getSettleablePnl(this),
};
});
accs = accs
accountsWithSettleablePnl = accountsWithSettleablePnl
.filter(
(acc) =>
// need perp positions with -ve pnl to settle +ve pnl and vice versa
@ -444,10 +444,12 @@ export class PerpMarket {
if (direction === 'negative') {
let stable = 0;
for (let i = 0; i < accs.length; i++) {
const acc = accs[i];
for (let i = 0; i < accountsWithSettleablePnl.length; i++) {
const acc = accountsWithSettleablePnl[i];
const nextPnl =
i + 1 < accs.length ? accs[i + 1].settleablePnl : ZERO_I80F48();
i + 1 < accountsWithSettleablePnl.length
? accountsWithSettleablePnl[i + 1].settleablePnl
: ZERO_I80F48();
const perpSettleHealth = acc.account.getPerpSettleHealth(group);
acc.settleablePnl =
@ -467,7 +469,7 @@ export class PerpMarket {
}
}
accs.sort((a, b) =>
accountsWithSettleablePnl.sort((a, b) =>
direction === 'negative'
? // most negative
a.settleablePnl.cmp(b.settleablePnl)
@ -475,7 +477,7 @@ export class PerpMarket {
b.settleablePnl.cmp(a.settleablePnl),
);
return accs.slice(0, count);
return accountsWithSettleablePnl.slice(0, count);
}
toString(): string {
@ -853,7 +855,7 @@ export class PerpOrder {
return new PerpOrder(
type === BookSideType.bids
? new BN('18446744073709551615').sub(leafNode.key.maskn(64))
? RUST_U64_MAX().sub(leafNode.key.maskn(64))
: leafNode.key.maskn(64),
leafNode.key,
leafNode.owner,

View File

@ -1,5 +1,16 @@
import { BN } from '@project-serum/anchor';
import { PublicKey } from '@solana/web3.js';
export const RUST_U64_MAX = (): BN => {
return new BN('18446744073709551615');
};
export const RUST_I64_MAX = (): BN => {
return new BN('9223372036854775807');
};
export const RUST_I64_MIN = (): BN => {
return new BN('-9223372036854775807');
};
export const OPENBOOK_PROGRAM_ID = {
devnet: new PublicKey('EoTcMgcDRTJVZDMZWBoU6rhYHZfkNTVEAfz3uUJRcYGj'),
'mainnet-beta': new PublicKey('srmqPvymJeFKQ4zGQed1GFppgkRHL9kaELCbyksJtPX'),

View File

@ -4460,9 +4460,17 @@ export type MangoV4 = {
{
"name": "settlePnlLimitFactor",
"docs": [
"Fraction of perp base value (i.e. base_lots * entry_price_in_lots) of unrealized",
"positive pnl that can be settled each window.",
"Set to a negative value to disable the limit."
"Controls the strictness of the settle limit.",
"Set to a negative value to disable the limit.",
"",
"This factor applies to the settle limit in two ways",
"- for the unrealized pnl settle limit, the factor is multiplied with the stable perp base value",
"(i.e. limit_factor * base_native * stable_price)",
"- when increasing the realized pnl settle limit (stored per PerpPosition), the factor is",
"multiplied with the stable value of the perp pnl being realized",
"(i.e. limit_factor * reduced_native * stable_price)",
"",
"See also PerpPosition::settle_pnl_limit_realized_trade"
],
"type": "f32"
},
@ -5094,10 +5102,22 @@ export type MangoV4 = {
},
{
"name": "settlePnlLimitWindow",
"docs": [
"Index of the current settle pnl limit window"
],
"type": "u32"
},
{
"name": "settlePnlLimitSettledInCurrentWindowNative",
"docs": [
"Amount of realized trade pnl and unrealized pnl that was already settled this window.",
"",
"Will be negative when negative pnl was settled.",
"",
"Note that this will be adjusted for bookkeeping reasons when the realized_trade settle",
"limitchanges and is not useable for actually tracking how much pnl was settled",
"on balance."
],
"type": "i64"
},
{
@ -5127,7 +5147,7 @@ export type MangoV4 = {
{
"name": "longSettledFunding",
"docs": [
"Already settled funding"
"Already settled long funding"
],
"type": {
"defined": "I80F48"
@ -5135,6 +5155,9 @@ export type MangoV4 = {
},
{
"name": "shortSettledFunding",
"docs": [
"Already settled short funding"
],
"type": {
"defined": "I80F48"
}
@ -5142,26 +5165,29 @@ export type MangoV4 = {
{
"name": "bidsBaseLots",
"docs": [
"Base lots in bids"
"Base lots in open bids"
],
"type": "i64"
},
{
"name": "asksBaseLots",
"docs": [
"Base lots in asks"
"Base lots in open asks"
],
"type": "i64"
},
{
"name": "takerBaseLots",
"docs": [
"Amount that's on EventQueue waiting to be processed"
"Amount of base lots on the EventQueue waiting to be processed"
],
"type": "i64"
},
{
"name": "takerQuoteLots",
"docs": [
"Amount of quote lots on the EventQueue waiting to be processed"
],
"type": "i64"
},
{
@ -5186,20 +5212,52 @@ export type MangoV4 = {
},
{
"name": "avgEntryPricePerBaseLot",
"docs": [
"The native average entry price for the base lots of the current position.",
"Reset to 0 when the base position reaches or crosses 0."
],
"type": "f64"
},
{
"name": "realizedPnlNative",
"name": "realizedTradePnlNative",
"docs": [
"Amount of pnl that was realized by bringing the base position closer to 0.",
"",
"The settlement of this type of pnl is limited by settle_pnl_limit_realized_trade.",
"Settling pnl reduces this value once other_pnl below is exhausted."
],
"type": {
"defined": "I80F48"
}
},
{
"name": "realizedOtherPnlNative",
"docs": [
"Amount of pnl realized from fees, funding and liquidation.",
"",
"This type of realized pnl is always settleable.",
"Settling pnl reduces this value first."
],
"type": {
"defined": "I80F48"
}
},
{
"name": "settlePnlLimitRealizedTrade",
"docs": [
"Settle limit contribution from realized pnl.",
"",
"Every time pnl is realized, this is increased by a fraction of the stable",
"value of the realization. It magnitude decreases when realized pnl drops below its value."
],
"type": "i64"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
128
104
]
}
}
@ -11944,9 +12002,17 @@ export const IDL: MangoV4 = {
{
"name": "settlePnlLimitFactor",
"docs": [
"Fraction of perp base value (i.e. base_lots * entry_price_in_lots) of unrealized",
"positive pnl that can be settled each window.",
"Set to a negative value to disable the limit."
"Controls the strictness of the settle limit.",
"Set to a negative value to disable the limit.",
"",
"This factor applies to the settle limit in two ways",
"- for the unrealized pnl settle limit, the factor is multiplied with the stable perp base value",
"(i.e. limit_factor * base_native * stable_price)",
"- when increasing the realized pnl settle limit (stored per PerpPosition), the factor is",
"multiplied with the stable value of the perp pnl being realized",
"(i.e. limit_factor * reduced_native * stable_price)",
"",
"See also PerpPosition::settle_pnl_limit_realized_trade"
],
"type": "f32"
},
@ -12578,10 +12644,22 @@ export const IDL: MangoV4 = {
},
{
"name": "settlePnlLimitWindow",
"docs": [
"Index of the current settle pnl limit window"
],
"type": "u32"
},
{
"name": "settlePnlLimitSettledInCurrentWindowNative",
"docs": [
"Amount of realized trade pnl and unrealized pnl that was already settled this window.",
"",
"Will be negative when negative pnl was settled.",
"",
"Note that this will be adjusted for bookkeeping reasons when the realized_trade settle",
"limitchanges and is not useable for actually tracking how much pnl was settled",
"on balance."
],
"type": "i64"
},
{
@ -12611,7 +12689,7 @@ export const IDL: MangoV4 = {
{
"name": "longSettledFunding",
"docs": [
"Already settled funding"
"Already settled long funding"
],
"type": {
"defined": "I80F48"
@ -12619,6 +12697,9 @@ export const IDL: MangoV4 = {
},
{
"name": "shortSettledFunding",
"docs": [
"Already settled short funding"
],
"type": {
"defined": "I80F48"
}
@ -12626,26 +12707,29 @@ export const IDL: MangoV4 = {
{
"name": "bidsBaseLots",
"docs": [
"Base lots in bids"
"Base lots in open bids"
],
"type": "i64"
},
{
"name": "asksBaseLots",
"docs": [
"Base lots in asks"
"Base lots in open asks"
],
"type": "i64"
},
{
"name": "takerBaseLots",
"docs": [
"Amount that's on EventQueue waiting to be processed"
"Amount of base lots on the EventQueue waiting to be processed"
],
"type": "i64"
},
{
"name": "takerQuoteLots",
"docs": [
"Amount of quote lots on the EventQueue waiting to be processed"
],
"type": "i64"
},
{
@ -12670,20 +12754,52 @@ export const IDL: MangoV4 = {
},
{
"name": "avgEntryPricePerBaseLot",
"docs": [
"The native average entry price for the base lots of the current position.",
"Reset to 0 when the base position reaches or crosses 0."
],
"type": "f64"
},
{
"name": "realizedPnlNative",
"name": "realizedTradePnlNative",
"docs": [
"Amount of pnl that was realized by bringing the base position closer to 0.",
"",
"The settlement of this type of pnl is limited by settle_pnl_limit_realized_trade.",
"Settling pnl reduces this value once other_pnl below is exhausted."
],
"type": {
"defined": "I80F48"
}
},
{
"name": "realizedOtherPnlNative",
"docs": [
"Amount of pnl realized from fees, funding and liquidation.",
"",
"This type of realized pnl is always settleable.",
"Settling pnl reduces this value first."
],
"type": {
"defined": "I80F48"
}
},
{
"name": "settlePnlLimitRealizedTrade",
"docs": [
"Settle limit contribution from realized pnl.",
"",
"Every time pnl is realized, this is increased by a fraction of the stable",
"value of the realization. It magnitude decreases when realized pnl drops below its value."
],
"type": "i64"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
128
104
]
}
}