Perp settle limit extension to realized pnl (#359)
Co-authored-by: microwavedcola1 <microwavedcola@gmail.com>
This commit is contained in:
parent
75593925aa
commit
c5d875e04d
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
@ -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(())
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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
|
||||
},
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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,
|
||||
) {}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue