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.settle_funding(&perp_market);
|
||||||
perp_pos.update_settle_limit(&perp_market, now_ts);
|
perp_pos.update_settle_limit(&perp_market, now_ts);
|
||||||
let pnl = perp_pos.pnl_for_price(&perp_market, oracle_price).unwrap();
|
let pnl = perp_pos.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
|
if limited_pnl >= 0 && direction == Direction::MaxNegative
|
||||||
|| limited_pnl <= 0 && direction == Direction::MaxPositive
|
|| 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
|
/// 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_export]
|
||||||
macro_rules! error_msg_typed {
|
macro_rules! error_msg_typed {
|
||||||
($code:ident, $($arg:tt)*) => {
|
($code:expr, $($arg:tt)*) => {
|
||||||
error!(MangoError::$code).context(format!($($arg)*))
|
error!($code).context(format!($($arg)*))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Like anchor's require!(), but with a customizable message
|
/// 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_export]
|
||||||
macro_rules! require_msg {
|
macro_rules! require_msg {
|
||||||
($invariant:expr, $($arg:tt)*) => {
|
($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;
|
||||||
pub use error_msg_typed;
|
pub use error_msg_typed;
|
||||||
pub use require_msg;
|
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> {
|
fn bank_index(&self, token_index: TokenIndex) -> Result<usize> {
|
||||||
Ok(*self.token_index_map.get(&token_index).ok_or_else(|| {
|
Ok(*self.token_index_map.get(&token_index).ok_or_else(|| {
|
||||||
error_msg_typed!(
|
error_msg_typed!(
|
||||||
TokenPositionDoesNotExist,
|
MangoError::TokenPositionDoesNotExist,
|
||||||
"token index {} not found",
|
"token index {} not found",
|
||||||
token_index
|
token_index
|
||||||
)
|
)
|
||||||
|
|
|
@ -361,7 +361,7 @@ impl HealthCache {
|
||||||
.position(|t| t.token_index == token_index)
|
.position(|t| t.token_index == token_index)
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
error_msg_typed!(
|
error_msg_typed!(
|
||||||
TokenPositionDoesNotExist,
|
MangoError::TokenPositionDoesNotExist,
|
||||||
"token index {} not found",
|
"token index {} not found",
|
||||||
token_index
|
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)
|
.position(|ti| ti.token_index == token_index)
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
error_msg_typed!(
|
error_msg_typed!(
|
||||||
TokenPositionDoesNotExist,
|
MangoError::TokenPositionDoesNotExist,
|
||||||
"token index {} not found",
|
"token index {} not found",
|
||||||
token_index
|
token_index
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,5 +1,68 @@
|
||||||
use fixed::types::I80F48;
|
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 {
|
pub trait LowPrecisionDivision {
|
||||||
fn checked_div_30bit_precision(&self, rhs: I80F48) -> Option<I80F48>;
|
fn checked_div_30bit_precision(&self, rhs: I80F48) -> Option<I80F48>;
|
||||||
fn checked_div_f64_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
|
let liqor_perp_position = liqor
|
||||||
.ensure_perp_position(perp_market.perp_market_index, settle_token_index)?
|
.ensure_perp_position(perp_market.perp_market_index, settle_token_index)?
|
||||||
.0;
|
.0;
|
||||||
liqee_perp_position.record_bankruptcy_quote_change(insurance_liab_transfer);
|
liqee_perp_position.record_settle(-insurance_liab_transfer);
|
||||||
liqor_perp_position.record_bankruptcy_quote_change(-insurance_liab_transfer);
|
liqor_perp_position.record_liquidation_quote_change(-insurance_liab_transfer);
|
||||||
|
|
||||||
emit_perp_balances(
|
emit_perp_balances(
|
||||||
ctx.accounts.group.key(),
|
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;
|
let mut socialized_loss = I80F48::ZERO;
|
||||||
if insurance_fund_exhausted && remaining_liab.is_positive() {
|
if insurance_fund_exhausted && remaining_liab.is_positive() {
|
||||||
perp_market.socialize_loss(-remaining_liab)?;
|
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);
|
require_eq!(liqee_perp_position.quote_position_native(), 0);
|
||||||
socialized_loss = remaining_liab;
|
socialized_loss = remaining_liab;
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,11 +72,19 @@ pub fn perp_settle_fees(ctx: Context<PerpSettleFees>, max_settle_amount: u64) ->
|
||||||
MangoError::ProfitabilityMismatch
|
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
|
// Settle for the maximum possible capped to max_settle_amount
|
||||||
let settlement = pnl
|
let settlement = settleable_pnl
|
||||||
.abs()
|
.abs()
|
||||||
.min(perp_market.fees_accrued.abs())
|
.min(perp_market.fees_accrued.abs())
|
||||||
.min(I80F48::from(max_settle_amount));
|
.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_position.record_settle(-settlement); // settle the negative pnl on the user perp position
|
||||||
perp_market.fees_accrued = cm!(perp_market.fees_accrued - settlement);
|
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
|
// Account A must be profitable, and B must be unprofitable
|
||||||
// PnL must be opposite signs for there to be a settlement
|
// PnL must be opposite signs for there to be a settlement
|
||||||
require!(a_pnl.is_positive(), MangoError::ProfitabilityMismatch);
|
require_msg_typed!(
|
||||||
require!(b_pnl.is_negative(), MangoError::ProfitabilityMismatch);
|
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
|
// Cap settlement of unrealized pnl
|
||||||
// Settles at most x100% each hour
|
// Settles at most x100% each hour
|
||||||
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
|
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
|
||||||
a_perp_position.update_settle_limit(&perp_market, now_ts);
|
a_perp_position.update_settle_limit(&perp_market, now_ts);
|
||||||
b_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(),
|
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
|
// 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);
|
let settlement = a_settleable_pnl
|
||||||
require!(settlement >= 0, MangoError::SettlementAmountMustBePositive);
|
.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
|
// Settle
|
||||||
a_perp_position.record_settle(settlement);
|
a_perp_position.record_settle(settlement);
|
||||||
|
|
|
@ -607,7 +607,7 @@ impl Bank {
|
||||||
.checked_mul_int(self.net_borrows_in_window.into())
|
.checked_mul_int(self.net_borrows_in_window.into())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
if net_borrows_quote > self.net_borrow_limit_per_window_quote {
|
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 ({:?}) ",
|
"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
|
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)))
|
.find_map(|(raw_index, p)| p.is_active_for_token(token_index).then(|| (p, raw_index)))
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
error_msg_typed!(
|
error_msg_typed!(
|
||||||
TokenPositionDoesNotExist,
|
MangoError::TokenPositionDoesNotExist,
|
||||||
"position for token index {} not found",
|
"position for token index {} not found",
|
||||||
token_index
|
token_index
|
||||||
)
|
)
|
||||||
|
@ -612,7 +612,7 @@ impl<
|
||||||
.find_map(|(raw_index, p)| p.is_active_for_token(token_index).then(|| raw_index))
|
.find_map(|(raw_index, p)| p.is_active_for_token(token_index).then(|| raw_index))
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
error_msg_typed!(
|
error_msg_typed!(
|
||||||
TokenPositionDoesNotExist,
|
MangoError::TokenPositionDoesNotExist,
|
||||||
"position for token index {} not found",
|
"position for token index {} not found",
|
||||||
token_index
|
token_index
|
||||||
)
|
)
|
||||||
|
@ -898,8 +898,8 @@ impl<
|
||||||
let (base_change, quote_change) = fill.base_quote_change(side);
|
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 quote = cm!(I80F48::from(perp_market.quote_lot_size) * I80F48::from(quote_change));
|
||||||
let fees = cm!(quote.abs() * fill.maker_fee);
|
let fees = cm!(quote.abs() * fill.maker_fee);
|
||||||
|
pa.record_trading_fee(fees);
|
||||||
pa.record_trade(perp_market, base_change, quote);
|
pa.record_trade(perp_market, base_change, quote);
|
||||||
pa.record_fee(fees);
|
|
||||||
|
|
||||||
cm!(pa.maker_volume += quote.abs().to_num::<u64>());
|
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);
|
require_gte!(taker_fees, 0);
|
||||||
|
|
||||||
let perp_account = mango_account.perp_position_mut(market.perp_market_index)?;
|
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!(market.fees_accrued += taker_fees + maker_fees);
|
||||||
cm!(perp_account.taker_volume += taker_fees.to_num::<u64>());
|
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 perp_account = mango_account.perp_position_mut(market.perp_market_index)?;
|
||||||
let fee_penalty = I80F48::from_num(market.fee_penalty);
|
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);
|
cm!(market.fees_accrued += fee_penalty);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -97,9 +97,17 @@ pub struct PerpMarket {
|
||||||
pub settle_fee_fraction_low_health: f32,
|
pub settle_fee_fraction_low_health: f32,
|
||||||
|
|
||||||
// Pnl settling limits
|
// Pnl settling limits
|
||||||
/// Fraction of perp base value (i.e. base_lots * entry_price_in_lots) of unrealized
|
/// Controls the strictness of the settle limit.
|
||||||
/// positive pnl that can be settled each window.
|
|
||||||
/// Set to a negative value to disable the 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 settle_pnl_limit_factor: f32,
|
||||||
pub padding3: [u8; 4],
|
pub padding3: [u8; 4],
|
||||||
/// Window size in seconds for the perp settlement limit
|
/// 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 struct PerpResetStablePriceModel {
|
||||||
pub group: Pubkey,
|
pub group: Pubkey,
|
||||||
pub admin: TestKeypair,
|
pub admin: TestKeypair,
|
||||||
|
@ -2626,30 +2656,47 @@ impl ClientInstruction for PerpResetStablePriceModel {
|
||||||
|
|
||||||
let instruction = Self::Instruction {
|
let instruction = Self::Instruction {
|
||||||
oracle_opt: Some(perp_market.oracle),
|
oracle_opt: Some(perp_market.oracle),
|
||||||
oracle_config_opt: None,
|
..perp_edit_instruction_default()
|
||||||
base_decimals_opt: None,
|
};
|
||||||
maint_asset_weight_opt: None,
|
|
||||||
init_asset_weight_opt: None,
|
let accounts = Self::Accounts {
|
||||||
maint_liab_weight_opt: None,
|
group: self.group,
|
||||||
init_liab_weight_opt: None,
|
admin: self.admin.pubkey(),
|
||||||
liquidation_fee_opt: None,
|
perp_market: self.perp_market,
|
||||||
maker_fee_opt: None,
|
oracle: perp_market.oracle,
|
||||||
taker_fee_opt: None,
|
};
|
||||||
min_funding_opt: None,
|
|
||||||
max_funding_opt: None,
|
let instruction = make_instruction(program_id, &accounts, instruction);
|
||||||
impact_quantity_opt: None,
|
(accounts, instruction)
|
||||||
group_insurance_fund_opt: None,
|
}
|
||||||
trusted_market_opt: None,
|
|
||||||
fee_penalty_opt: None,
|
fn signers(&self) -> Vec<TestKeypair> {
|
||||||
settle_fee_flat_opt: None,
|
vec![self.admin]
|
||||||
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,
|
pub struct PerpSetSettleLimitWindow {
|
||||||
stable_price_growth_limit_opt: None,
|
pub group: Pubkey,
|
||||||
settle_pnl_limit_factor_opt: None,
|
pub admin: TestKeypair,
|
||||||
settle_pnl_limit_window_size_ts: None,
|
pub perp_market: Pubkey,
|
||||||
reduce_only_opt: None,
|
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 {
|
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 perp_market: PerpMarket = account_loader.load(&self.perp_market).await.unwrap();
|
||||||
|
|
||||||
let instruction = Self::Instruction {
|
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),
|
reduce_only_opt: Some(true),
|
||||||
|
..perp_edit_instruction_default()
|
||||||
};
|
};
|
||||||
|
|
||||||
let accounts = Self::Accounts {
|
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
|
// 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(
|
send_tx(
|
||||||
solana,
|
solana,
|
||||||
PerpSettlePnlInstruction {
|
PerpSettlePnlInstruction {
|
||||||
|
@ -643,9 +650,10 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let liqee_settle_health_before = 999.0 + 1.0 * 2.0 * 0.8;
|
let liqee_settle_health_before: f64 = 999.0 + 1.0 * 2.0 * 0.8;
|
||||||
let remaining_pnl =
|
// the liqor's settle limit means we can't settle everything
|
||||||
20.0 * 100.0 - liq_amount_3 - liq_amount_4 - liq_amount_5 + liqee_settle_health_before;
|
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);
|
assert!(remaining_pnl < 0.0);
|
||||||
let liqee_data = solana.get_account::<MangoAccount>(account_1).await;
|
let liqee_data = solana.get_account::<MangoAccount>(account_1).await;
|
||||||
assert_eq!(liqee_data.perps[0].base_position_lots(), 0);
|
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!(
|
assert_eq!(
|
||||||
account_position(solana, account_1, quote_token.bank).await,
|
account_position(solana, account_1, quote_token.bank).await,
|
||||||
-2
|
account_1_quote_before - settle_amount as i64
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
account_position(solana, account_1, base_token.bank).await,
|
account_position(solana, account_1, base_token.bank).await,
|
||||||
1
|
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
|
// 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,
|
-socialized_amount / 20.0,
|
||||||
0.1
|
0.1
|
||||||
));
|
));
|
||||||
|
*/
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -83,7 +83,7 @@ async fn test_perp_fixed() -> Result<(), TransportError> {
|
||||||
liquidation_fee: 0.012,
|
liquidation_fee: 0.012,
|
||||||
maker_fee: -0.0001,
|
maker_fee: -0.0001,
|
||||||
taker_fee: 0.0002,
|
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,
|
settle_pnl_limit_window_size_ts: 24 * 60 * 60,
|
||||||
..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, &tokens[0]).await
|
..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
|
// TEST: Create a perp market
|
||||||
//
|
//
|
||||||
|
let settle_pnl_limit_factor = 0.8;
|
||||||
let mango_v4::accounts::PerpCreateMarket { perp_market, .. } = send_tx(
|
let mango_v4::accounts::PerpCreateMarket { perp_market, .. } = send_tx(
|
||||||
solana,
|
solana,
|
||||||
PerpCreateMarketInstruction {
|
PerpCreateMarketInstruction {
|
||||||
|
@ -852,9 +853,9 @@ async fn test_perp_pnl_settle_limit() -> Result<(), TransportError> {
|
||||||
maint_liab_weight: 1.025,
|
maint_liab_weight: 1.025,
|
||||||
init_liab_weight: 1.05,
|
init_liab_weight: 1.05,
|
||||||
liquidation_fee: 0.012,
|
liquidation_fee: 0.012,
|
||||||
maker_fee: 0.0002,
|
maker_fee: 0.0,
|
||||||
taker_fee: 0.000,
|
taker_fee: 0.0,
|
||||||
settle_pnl_limit_factor: 0.2,
|
settle_pnl_limit_factor,
|
||||||
settle_pnl_limit_window_size_ts: 24 * 60 * 60,
|
settle_pnl_limit_window_size_ts: 24 * 60 * 60,
|
||||||
..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, &tokens[1]).await
|
..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
|
// Set the initial oracle price
|
||||||
send_tx(
|
set_perp_stub_oracle_price(&solana, group, perp_market, &tokens[1], admin, 1000.0).await;
|
||||||
solana,
|
|
||||||
StubOracleSetInstruction {
|
|
||||||
group,
|
|
||||||
admin,
|
|
||||||
mint: mints[1].pubkey,
|
|
||||||
price: 1000.0,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Place orders and create a position
|
// Place orders and create a position
|
||||||
|
@ -927,32 +918,41 @@ async fn test_perp_pnl_settle_limit() -> Result<(), TransportError> {
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Manipulate the price
|
// Manipulate the price (without adjusting stable price)
|
||||||
|
let price_factor = 3;
|
||||||
send_tx(
|
send_tx(
|
||||||
solana,
|
solana,
|
||||||
StubOracleSetInstruction {
|
StubOracleSetInstruction {
|
||||||
group,
|
group,
|
||||||
admin,
|
admin,
|
||||||
mint: mints[1].pubkey,
|
mint: mints[1].pubkey,
|
||||||
price: 10000.0, // 10x original price
|
price: price_factor as f64 * 1000.0,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Settle Pnl
|
//
|
||||||
// attempt 1 - settle max possible,
|
// Test 1: settle max possible, limited by unrealized pnl settle limit
|
||||||
// since b has very large deposits, b's health will not interfere,
|
//
|
||||||
// the pnl cap enforced would be relative to the avg_entry_price
|
// 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 market = solana.get_account::<PerpMarket>(perp_market).await;
|
||||||
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).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 = solana.get_account::<MangoAccount>(account_1).await;
|
||||||
let mango_account_1_expected_qpn_after_settle = mango_account_1.perps[0]
|
let account_1_settle_limits = mango_account_1.perps[0].available_settle_limit(&market);
|
||||||
.quote_position_native()
|
assert_eq!(account_1_settle_limits, (-80000, 80000));
|
||||||
+ (market.settle_pnl_limit_factor()
|
let account_1_settle_limit = I80F48::from(account_1_settle_limits.0.abs());
|
||||||
* I80F48::from_num(mango_account_0.perps[0].avg_entry_price(&market))
|
assert_eq!(
|
||||||
|
account_1_settle_limit,
|
||||||
|
(market.settle_pnl_limit_factor()
|
||||||
|
* market.stable_price()
|
||||||
* mango_account_0.perps[0].base_position_native(&market))
|
* 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(
|
send_tx(
|
||||||
solana,
|
solana,
|
||||||
PerpSettlePnlInstruction {
|
PerpSettlePnlInstruction {
|
||||||
|
@ -966,13 +966,26 @@ async fn test_perp_pnl_settle_limit() -> Result<(), TransportError> {
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
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 = solana.get_account::<MangoAccount>(account_1).await;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
mango_account_1.perps[0].quote_position_native().round(),
|
mango_account_1.perps[0].quote_position_native().round(),
|
||||||
mango_account_1_expected_qpn_after_settle.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,
|
// neither account has any settle limit left
|
||||||
// we can't settle anymore amount
|
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(
|
let result = send_tx(
|
||||||
solana,
|
solana,
|
||||||
PerpSettlePnlInstruction {
|
PerpSettlePnlInstruction {
|
||||||
|
@ -991,5 +1004,216 @@ async fn test_perp_pnl_settle_limit() -> Result<(), TransportError> {
|
||||||
"Account A has no settleable positive pnl left".to_string(),
|
"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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -107,6 +107,8 @@ describe('Health Cache', () => {
|
||||||
new BN(0),
|
new BN(0),
|
||||||
0,
|
0,
|
||||||
ZERO_I80F48(),
|
ZERO_I80F48(),
|
||||||
|
ZERO_I80F48(),
|
||||||
|
new BN(0),
|
||||||
);
|
);
|
||||||
const pi1 = PerpInfo.fromPerpPosition(pM, pp);
|
const pi1 = PerpInfo.fromPerpPosition(pM, pp);
|
||||||
|
|
||||||
|
@ -221,6 +223,8 @@ describe('Health Cache', () => {
|
||||||
new BN(0),
|
new BN(0),
|
||||||
0,
|
0,
|
||||||
ZERO_I80F48(),
|
ZERO_I80F48(),
|
||||||
|
ZERO_I80F48(),
|
||||||
|
new BN(0),
|
||||||
);
|
);
|
||||||
const pi1 = PerpInfo.fromPerpPosition(pM, pp);
|
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 { OpenOrders, Order, Orderbook } from '@project-serum/serum/lib/market';
|
||||||
import { AccountInfo, PublicKey, TransactionSignature } from '@solana/web3.js';
|
import { AccountInfo, PublicKey, TransactionSignature } from '@solana/web3.js';
|
||||||
import { MangoClient } from '../client';
|
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 { I80F48, I80F48Dto, ONE_I80F48, ZERO_I80F48 } from '../numbers/I80F48';
|
||||||
import { toNativeI80F48, toUiDecimals, toUiDecimalsForQuote } from '../utils';
|
import { toNativeI80F48, toUiDecimals, toUiDecimalsForQuote } from '../utils';
|
||||||
import { Bank, TokenIndex } from './bank';
|
import { Bank, TokenIndex } from './bank';
|
||||||
|
@ -1170,7 +1170,9 @@ export class PerpPosition {
|
||||||
dto.takerVolume,
|
dto.takerVolume,
|
||||||
dto.perpSpotTransfers,
|
dto.perpSpotTransfers,
|
||||||
dto.avgEntryPricePerBaseLot,
|
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),
|
new BN(0),
|
||||||
0,
|
0,
|
||||||
ZERO_I80F48(),
|
ZERO_I80F48(),
|
||||||
|
ZERO_I80F48(),
|
||||||
|
new BN(0),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1219,7 +1223,9 @@ export class PerpPosition {
|
||||||
public takerVolume: BN,
|
public takerVolume: BN,
|
||||||
public perpSpotTransfers: BN,
|
public perpSpotTransfers: BN,
|
||||||
public avgEntryPricePerBaseLot: number,
|
public avgEntryPricePerBaseLot: number,
|
||||||
public realizedPnlNative: I80F48,
|
public realizedTradePnlNative: I80F48,
|
||||||
|
public realizedOtherPnlNative: I80F48,
|
||||||
|
public settlePnlLimitRealizedTrade: BN,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
isActive(): boolean {
|
isActive(): boolean {
|
||||||
|
@ -1230,6 +1236,10 @@ export class PerpPosition {
|
||||||
perpMarket: PerpMarket,
|
perpMarket: PerpMarket,
|
||||||
useEventQueue?: boolean,
|
useEventQueue?: boolean,
|
||||||
): number {
|
): number {
|
||||||
|
if (perpMarket.perpMarketIndex !== this.marketIndex) {
|
||||||
|
throw new Error("PerpPosition doesn't belong to the given market!");
|
||||||
|
}
|
||||||
|
|
||||||
return perpMarket.baseLotsToUi(
|
return perpMarket.baseLotsToUi(
|
||||||
useEventQueue
|
useEventQueue
|
||||||
? this.basePositionLots.add(this.takerBaseLots)
|
? this.basePositionLots.add(this.takerBaseLots)
|
||||||
|
@ -1238,6 +1248,10 @@ export class PerpPosition {
|
||||||
}
|
}
|
||||||
|
|
||||||
public getUnsettledFunding(perpMarket: PerpMarket): I80F48 {
|
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))) {
|
if (this.basePositionLots.gt(new BN(0))) {
|
||||||
return perpMarket.longFunding
|
return perpMarket.longFunding
|
||||||
.sub(this.longSettledFunding)
|
.sub(this.longSettledFunding)
|
||||||
|
@ -1251,6 +1265,10 @@ export class PerpPosition {
|
||||||
}
|
}
|
||||||
|
|
||||||
public getEquityUi(group: Group, perpMarket: PerpMarket): number {
|
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(
|
return toUiDecimals(
|
||||||
this.getEquity(perpMarket),
|
this.getEquity(perpMarket),
|
||||||
group.getMintDecimalsByTokenIndex(perpMarket.settleTokenIndex),
|
group.getMintDecimalsByTokenIndex(perpMarket.settleTokenIndex),
|
||||||
|
@ -1258,6 +1276,10 @@ export class PerpPosition {
|
||||||
}
|
}
|
||||||
|
|
||||||
public getEquity(perpMarket: PerpMarket): I80F48 {
|
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(
|
const lotsToQuote = I80F48.fromI64(perpMarket.baseLotSize).mul(
|
||||||
perpMarket.price,
|
perpMarket.price,
|
||||||
);
|
);
|
||||||
|
@ -1288,12 +1310,20 @@ export class PerpPosition {
|
||||||
}
|
}
|
||||||
|
|
||||||
public getAverageEntryPriceUi(perpMarket: PerpMarket): number {
|
public getAverageEntryPriceUi(perpMarket: PerpMarket): number {
|
||||||
|
if (perpMarket.perpMarketIndex !== this.marketIndex) {
|
||||||
|
throw new Error("PerpPosition doesn't belong to the given market!");
|
||||||
|
}
|
||||||
|
|
||||||
return perpMarket.priceNativeToUi(
|
return perpMarket.priceNativeToUi(
|
||||||
this.avgEntryPricePerBaseLot / perpMarket.baseLotSize.toNumber(),
|
this.avgEntryPricePerBaseLot / perpMarket.baseLotSize.toNumber(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getBreakEvenPriceUi(perpMarket: PerpMarket): number {
|
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))) {
|
if (this.basePositionLots.eq(new BN(0))) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
@ -1304,12 +1334,99 @@ export class PerpPosition {
|
||||||
}
|
}
|
||||||
|
|
||||||
public getPnl(perpMarket: PerpMarket): I80F48 {
|
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(
|
return this.quotePositionNative.add(
|
||||||
I80F48.fromI64(this.basePositionLots.mul(perpMarket.baseLotSize)).mul(
|
I80F48.fromI64(this.basePositionLots.mul(perpMarket.baseLotSize)).mul(
|
||||||
perpMarket.price,
|
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 {
|
export class PerpPositionDto {
|
||||||
|
@ -1332,7 +1449,9 @@ export class PerpPositionDto {
|
||||||
public takerVolume: BN,
|
public takerVolume: BN,
|
||||||
public perpSpotTransfers: BN,
|
public perpSpotTransfers: BN,
|
||||||
public avgEntryPricePerBaseLot: number,
|
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 { PublicKey } from '@solana/web3.js';
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
import { MangoClient } from '../client';
|
import { MangoClient } from '../client';
|
||||||
|
import { RUST_U64_MAX } from '../constants';
|
||||||
import { I80F48, I80F48Dto, ZERO_I80F48 } from '../numbers/I80F48';
|
import { I80F48, I80F48Dto, ZERO_I80F48 } from '../numbers/I80F48';
|
||||||
import { Modify } from '../types';
|
import { Modify } from '../types';
|
||||||
import { As, U64_MAX_BN, toNative, toUiDecimals } from '../utils';
|
import { As, U64_MAX_BN, toNative, toUiDecimals } from '../utils';
|
||||||
|
@ -181,8 +182,8 @@ export class PerpMarket {
|
||||||
public settleFeeFlat: number,
|
public settleFeeFlat: number,
|
||||||
public settleFeeAmountThreshold: number,
|
public settleFeeAmountThreshold: number,
|
||||||
public settleFeeFractionLowHealth: number,
|
public settleFeeFractionLowHealth: number,
|
||||||
settlePnlLimitFactor: number,
|
public settlePnlLimitFactor: number,
|
||||||
settlePnlLimitWindowSizeTs: BN,
|
public settlePnlLimitWindowSizeTs: BN,
|
||||||
public reduceOnly: boolean,
|
public reduceOnly: boolean,
|
||||||
) {
|
) {
|
||||||
this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0];
|
this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0];
|
||||||
|
@ -412,22 +413,21 @@ export class PerpMarket {
|
||||||
direction: 'negative' | 'positive',
|
direction: 'negative' | 'positive',
|
||||||
count = 2,
|
count = 2,
|
||||||
): Promise<{ account: MangoAccount; settleablePnl: I80F48 }[]> {
|
): Promise<{ account: MangoAccount; settleablePnl: I80F48 }[]> {
|
||||||
let accs = (await client.getAllMangoAccounts(group))
|
let accountsWithSettleablePnl = (await client.getAllMangoAccounts(group))
|
||||||
.filter((acc) =>
|
.filter((acc) => acc.perpPositionExistsForMarket(this))
|
||||||
// need a perp position in this market
|
|
||||||
acc.perpPositionExistsForMarket(this),
|
|
||||||
)
|
|
||||||
.map((acc) => {
|
.map((acc) => {
|
||||||
|
const pp = acc
|
||||||
|
.perpActive()
|
||||||
|
.find((pp) => pp.marketIndex === this.perpMarketIndex)!;
|
||||||
|
pp.updateSettleLimit(this);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
account: acc,
|
account: acc,
|
||||||
settleablePnl: acc
|
settleablePnl: pp.getSettleablePnl(this),
|
||||||
.perpActive()
|
|
||||||
.find((pp) => pp.marketIndex === this.perpMarketIndex)!
|
|
||||||
.getPnl(this),
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
accs = accs
|
accountsWithSettleablePnl = accountsWithSettleablePnl
|
||||||
.filter(
|
.filter(
|
||||||
(acc) =>
|
(acc) =>
|
||||||
// need perp positions with -ve pnl to settle +ve pnl and vice versa
|
// need perp positions with -ve pnl to settle +ve pnl and vice versa
|
||||||
|
@ -444,10 +444,12 @@ export class PerpMarket {
|
||||||
|
|
||||||
if (direction === 'negative') {
|
if (direction === 'negative') {
|
||||||
let stable = 0;
|
let stable = 0;
|
||||||
for (let i = 0; i < accs.length; i++) {
|
for (let i = 0; i < accountsWithSettleablePnl.length; i++) {
|
||||||
const acc = accs[i];
|
const acc = accountsWithSettleablePnl[i];
|
||||||
const nextPnl =
|
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);
|
const perpSettleHealth = acc.account.getPerpSettleHealth(group);
|
||||||
acc.settleablePnl =
|
acc.settleablePnl =
|
||||||
|
@ -467,7 +469,7 @@ export class PerpMarket {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
accs.sort((a, b) =>
|
accountsWithSettleablePnl.sort((a, b) =>
|
||||||
direction === 'negative'
|
direction === 'negative'
|
||||||
? // most negative
|
? // most negative
|
||||||
a.settleablePnl.cmp(b.settleablePnl)
|
a.settleablePnl.cmp(b.settleablePnl)
|
||||||
|
@ -475,7 +477,7 @@ export class PerpMarket {
|
||||||
b.settleablePnl.cmp(a.settleablePnl),
|
b.settleablePnl.cmp(a.settleablePnl),
|
||||||
);
|
);
|
||||||
|
|
||||||
return accs.slice(0, count);
|
return accountsWithSettleablePnl.slice(0, count);
|
||||||
}
|
}
|
||||||
|
|
||||||
toString(): string {
|
toString(): string {
|
||||||
|
@ -853,7 +855,7 @@ export class PerpOrder {
|
||||||
|
|
||||||
return new PerpOrder(
|
return new PerpOrder(
|
||||||
type === BookSideType.bids
|
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.maskn(64),
|
||||||
leafNode.key,
|
leafNode.key,
|
||||||
leafNode.owner,
|
leafNode.owner,
|
||||||
|
|
|
@ -1,5 +1,16 @@
|
||||||
|
import { BN } from '@project-serum/anchor';
|
||||||
import { PublicKey } from '@solana/web3.js';
|
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 = {
|
export const OPENBOOK_PROGRAM_ID = {
|
||||||
devnet: new PublicKey('EoTcMgcDRTJVZDMZWBoU6rhYHZfkNTVEAfz3uUJRcYGj'),
|
devnet: new PublicKey('EoTcMgcDRTJVZDMZWBoU6rhYHZfkNTVEAfz3uUJRcYGj'),
|
||||||
'mainnet-beta': new PublicKey('srmqPvymJeFKQ4zGQed1GFppgkRHL9kaELCbyksJtPX'),
|
'mainnet-beta': new PublicKey('srmqPvymJeFKQ4zGQed1GFppgkRHL9kaELCbyksJtPX'),
|
||||||
|
|
|
@ -4460,9 +4460,17 @@ export type MangoV4 = {
|
||||||
{
|
{
|
||||||
"name": "settlePnlLimitFactor",
|
"name": "settlePnlLimitFactor",
|
||||||
"docs": [
|
"docs": [
|
||||||
"Fraction of perp base value (i.e. base_lots * entry_price_in_lots) of unrealized",
|
"Controls the strictness of the settle limit.",
|
||||||
"positive pnl that can be settled each window.",
|
"Set to a negative value to disable the 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"
|
"type": "f32"
|
||||||
},
|
},
|
||||||
|
@ -5094,10 +5102,22 @@ export type MangoV4 = {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "settlePnlLimitWindow",
|
"name": "settlePnlLimitWindow",
|
||||||
|
"docs": [
|
||||||
|
"Index of the current settle pnl limit window"
|
||||||
|
],
|
||||||
"type": "u32"
|
"type": "u32"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "settlePnlLimitSettledInCurrentWindowNative",
|
"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"
|
"type": "i64"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -5127,7 +5147,7 @@ export type MangoV4 = {
|
||||||
{
|
{
|
||||||
"name": "longSettledFunding",
|
"name": "longSettledFunding",
|
||||||
"docs": [
|
"docs": [
|
||||||
"Already settled funding"
|
"Already settled long funding"
|
||||||
],
|
],
|
||||||
"type": {
|
"type": {
|
||||||
"defined": "I80F48"
|
"defined": "I80F48"
|
||||||
|
@ -5135,6 +5155,9 @@ export type MangoV4 = {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "shortSettledFunding",
|
"name": "shortSettledFunding",
|
||||||
|
"docs": [
|
||||||
|
"Already settled short funding"
|
||||||
|
],
|
||||||
"type": {
|
"type": {
|
||||||
"defined": "I80F48"
|
"defined": "I80F48"
|
||||||
}
|
}
|
||||||
|
@ -5142,26 +5165,29 @@ export type MangoV4 = {
|
||||||
{
|
{
|
||||||
"name": "bidsBaseLots",
|
"name": "bidsBaseLots",
|
||||||
"docs": [
|
"docs": [
|
||||||
"Base lots in bids"
|
"Base lots in open bids"
|
||||||
],
|
],
|
||||||
"type": "i64"
|
"type": "i64"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "asksBaseLots",
|
"name": "asksBaseLots",
|
||||||
"docs": [
|
"docs": [
|
||||||
"Base lots in asks"
|
"Base lots in open asks"
|
||||||
],
|
],
|
||||||
"type": "i64"
|
"type": "i64"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "takerBaseLots",
|
"name": "takerBaseLots",
|
||||||
"docs": [
|
"docs": [
|
||||||
"Amount that's on EventQueue waiting to be processed"
|
"Amount of base lots on the EventQueue waiting to be processed"
|
||||||
],
|
],
|
||||||
"type": "i64"
|
"type": "i64"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "takerQuoteLots",
|
"name": "takerQuoteLots",
|
||||||
|
"docs": [
|
||||||
|
"Amount of quote lots on the EventQueue waiting to be processed"
|
||||||
|
],
|
||||||
"type": "i64"
|
"type": "i64"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -5186,20 +5212,52 @@ export type MangoV4 = {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "avgEntryPricePerBaseLot",
|
"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"
|
"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": {
|
"type": {
|
||||||
"defined": "I80F48"
|
"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",
|
"name": "reserved",
|
||||||
"type": {
|
"type": {
|
||||||
"array": [
|
"array": [
|
||||||
"u8",
|
"u8",
|
||||||
128
|
104
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11944,9 +12002,17 @@ export const IDL: MangoV4 = {
|
||||||
{
|
{
|
||||||
"name": "settlePnlLimitFactor",
|
"name": "settlePnlLimitFactor",
|
||||||
"docs": [
|
"docs": [
|
||||||
"Fraction of perp base value (i.e. base_lots * entry_price_in_lots) of unrealized",
|
"Controls the strictness of the settle limit.",
|
||||||
"positive pnl that can be settled each window.",
|
"Set to a negative value to disable the 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"
|
"type": "f32"
|
||||||
},
|
},
|
||||||
|
@ -12578,10 +12644,22 @@ export const IDL: MangoV4 = {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "settlePnlLimitWindow",
|
"name": "settlePnlLimitWindow",
|
||||||
|
"docs": [
|
||||||
|
"Index of the current settle pnl limit window"
|
||||||
|
],
|
||||||
"type": "u32"
|
"type": "u32"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "settlePnlLimitSettledInCurrentWindowNative",
|
"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"
|
"type": "i64"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -12611,7 +12689,7 @@ export const IDL: MangoV4 = {
|
||||||
{
|
{
|
||||||
"name": "longSettledFunding",
|
"name": "longSettledFunding",
|
||||||
"docs": [
|
"docs": [
|
||||||
"Already settled funding"
|
"Already settled long funding"
|
||||||
],
|
],
|
||||||
"type": {
|
"type": {
|
||||||
"defined": "I80F48"
|
"defined": "I80F48"
|
||||||
|
@ -12619,6 +12697,9 @@ export const IDL: MangoV4 = {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "shortSettledFunding",
|
"name": "shortSettledFunding",
|
||||||
|
"docs": [
|
||||||
|
"Already settled short funding"
|
||||||
|
],
|
||||||
"type": {
|
"type": {
|
||||||
"defined": "I80F48"
|
"defined": "I80F48"
|
||||||
}
|
}
|
||||||
|
@ -12626,26 +12707,29 @@ export const IDL: MangoV4 = {
|
||||||
{
|
{
|
||||||
"name": "bidsBaseLots",
|
"name": "bidsBaseLots",
|
||||||
"docs": [
|
"docs": [
|
||||||
"Base lots in bids"
|
"Base lots in open bids"
|
||||||
],
|
],
|
||||||
"type": "i64"
|
"type": "i64"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "asksBaseLots",
|
"name": "asksBaseLots",
|
||||||
"docs": [
|
"docs": [
|
||||||
"Base lots in asks"
|
"Base lots in open asks"
|
||||||
],
|
],
|
||||||
"type": "i64"
|
"type": "i64"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "takerBaseLots",
|
"name": "takerBaseLots",
|
||||||
"docs": [
|
"docs": [
|
||||||
"Amount that's on EventQueue waiting to be processed"
|
"Amount of base lots on the EventQueue waiting to be processed"
|
||||||
],
|
],
|
||||||
"type": "i64"
|
"type": "i64"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "takerQuoteLots",
|
"name": "takerQuoteLots",
|
||||||
|
"docs": [
|
||||||
|
"Amount of quote lots on the EventQueue waiting to be processed"
|
||||||
|
],
|
||||||
"type": "i64"
|
"type": "i64"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -12670,20 +12754,52 @@ export const IDL: MangoV4 = {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "avgEntryPricePerBaseLot",
|
"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"
|
"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": {
|
"type": {
|
||||||
"defined": "I80F48"
|
"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",
|
"name": "reserved",
|
||||||
"type": {
|
"type": {
|
||||||
"array": [
|
"array": [
|
||||||
"u8",
|
"u8",
|
||||||
128
|
104
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue