limit settleable pnl (#295)
Co-authored-by: Christian Kamm <mail@ckamm.de>
This commit is contained in:
parent
95b03aa5ac
commit
d64d9285f4
|
@ -69,6 +69,8 @@ pub fn perp_create_market(
|
|||
settle_fee_flat: f32,
|
||||
settle_fee_amount_threshold: f32,
|
||||
settle_fee_fraction_low_health: f32,
|
||||
settle_pnl_limit_factor: f32,
|
||||
settle_pnl_limit_factor_window_size_ts: u64,
|
||||
) -> Result<()> {
|
||||
// Settlement tokens that aren't USDC aren't fully implemented, the main missing steps are:
|
||||
// - In health: the perp health needs to be adjusted by the settlement token weights.
|
||||
|
@ -124,7 +126,9 @@ pub fn perp_create_market(
|
|||
settle_fee_amount_threshold,
|
||||
settle_fee_fraction_low_health,
|
||||
stable_price_model: StablePriceModel::default(),
|
||||
reserved: [0; 1956],
|
||||
settle_pnl_limit_factor,
|
||||
settle_pnl_limit_factor_window_size_ts,
|
||||
reserved: [0; 1944],
|
||||
};
|
||||
|
||||
let oracle_price =
|
||||
|
|
|
@ -47,6 +47,8 @@ pub fn perp_edit_market(
|
|||
stable_price_delay_interval_seconds_opt: Option<u32>,
|
||||
stable_price_delay_growth_limit_opt: Option<f32>,
|
||||
stable_price_growth_limit_opt: Option<f32>,
|
||||
settle_pnl_limit_factor_opt: Option<f32>,
|
||||
settle_pnl_limit_factor_window_size_ts_opt: Option<u64>,
|
||||
) -> Result<()> {
|
||||
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
|
||||
|
||||
|
@ -162,6 +164,14 @@ pub fn perp_edit_market(
|
|||
perp_market.stable_price_model.stable_growth_limit = stable_price_growth_limit;
|
||||
}
|
||||
|
||||
if let Some(settle_pnl_limit_factor_opt) = settle_pnl_limit_factor_opt {
|
||||
perp_market.settle_pnl_limit_factor = settle_pnl_limit_factor_opt;
|
||||
}
|
||||
if let Some(settle_pnl_limit_factor_window_size_ts) = settle_pnl_limit_factor_window_size_ts_opt
|
||||
{
|
||||
perp_market.settle_pnl_limit_factor_window_size_ts = settle_pnl_limit_factor_window_size_ts;
|
||||
}
|
||||
|
||||
emit!(PerpMarketMetaDataLog {
|
||||
mango_group: ctx.accounts.group.key(),
|
||||
perp_market: ctx.accounts.perp_market.key(),
|
||||
|
|
|
@ -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.change_quote_position(insurance_liab_transfer);
|
||||
liqor_perp_position.change_quote_position(-insurance_liab_transfer);
|
||||
liqee_perp_position.record_bankruptcy_quote_change(insurance_liab_transfer);
|
||||
liqor_perp_position.record_bankruptcy_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.change_quote_position(remaining_liab);
|
||||
liqee_perp_position.record_bankruptcy_quote_change(remaining_liab);
|
||||
require_eq!(liqee_perp_position.quote_position_native(), 0);
|
||||
socialized_loss = remaining_liab;
|
||||
}
|
||||
|
|
|
@ -80,7 +80,7 @@ pub fn perp_settle_fees(ctx: Context<PerpSettleFees>, max_settle_amount: u64) ->
|
|||
.abs()
|
||||
.min(perp_market.fees_accrued.abs())
|
||||
.min(I80F48::from(max_settle_amount));
|
||||
perp_position.change_quote_position(settlement);
|
||||
perp_position.record_settle(-settlement); // settle the negative pnl on the user perp position
|
||||
perp_market.fees_accrued = cm!(perp_market.fees_accrued - settlement);
|
||||
|
||||
emit_perp_balances(
|
||||
|
|
|
@ -123,10 +123,43 @@ pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>) -> Result<()> {
|
|||
require!(a_pnl.is_positive(), MangoError::ProfitabilityMismatch);
|
||||
require!(b_pnl.is_negative(), MangoError::ProfitabilityMismatch);
|
||||
|
||||
// Cap settlement of unrealized pnl
|
||||
// Settles at most x100% each hour
|
||||
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
|
||||
let a_settle_limit_used =
|
||||
a_perp_position.update_and_get_used_settle_limit(&perp_market, now_ts);
|
||||
b_perp_position.update_and_get_used_settle_limit(&perp_market, now_ts);
|
||||
|
||||
let a_settleable_pnl = {
|
||||
let realized_pnl = a_perp_position.realized_pnl_native;
|
||||
let unrealized_pnl = cm!(a_pnl - realized_pnl);
|
||||
let a_base_lots = I80F48::from(a_perp_position.base_position_lots());
|
||||
let avg_entry_price_lots = I80F48::from_num(a_perp_position.avg_entry_price_per_base_lot);
|
||||
let max_allowed_in_window =
|
||||
cm!(perp_market.settle_pnl_limit_factor() * a_base_lots * avg_entry_price_lots).abs();
|
||||
|
||||
let unrealized_pnl_capped_for_window = unrealized_pnl
|
||||
.min(cm!(
|
||||
max_allowed_in_window - I80F48::from_num(a_settle_limit_used)
|
||||
))
|
||||
.max(I80F48::ZERO);
|
||||
a_pnl
|
||||
.min(cm!(realized_pnl + unrealized_pnl_capped_for_window))
|
||||
.max(I80F48::ZERO)
|
||||
};
|
||||
|
||||
require!(
|
||||
a_settleable_pnl.is_positive(),
|
||||
MangoError::ProfitabilityMismatch
|
||||
);
|
||||
|
||||
// Settle for the maximum possible capped to b's settle health
|
||||
let settlement = a_pnl.abs().min(b_pnl.abs()).min(b_settle_health);
|
||||
a_perp_position.change_quote_position(-settlement);
|
||||
b_perp_position.change_quote_position(settlement);
|
||||
let settlement = a_settleable_pnl.abs().min(b_pnl.abs()).min(b_settle_health);
|
||||
require!(settlement >= 0, MangoError::SettlementAmountMustBePositive);
|
||||
|
||||
// Settle
|
||||
a_perp_position.record_settle(settlement);
|
||||
b_perp_position.record_settle(-settlement);
|
||||
|
||||
emit_perp_balances(
|
||||
ctx.accounts.group.key(),
|
||||
|
@ -168,13 +201,12 @@ pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>) -> Result<()> {
|
|||
};
|
||||
|
||||
// Safety check to prevent any accidental negative transfer
|
||||
require!(settlement >= 0, MangoError::SettlementAmountMustBePositive);
|
||||
require!(fee >= 0, MangoError::SettlementAmountMustBePositive);
|
||||
|
||||
// Update the account's net_settled with the new PnL.
|
||||
// Applying the fee here means that it decreases the displayed perp pnl.
|
||||
let settlement_i64 = settlement.checked_to_num::<i64>().unwrap();
|
||||
let fee_i64 = fee.checked_to_num::<i64>().unwrap();
|
||||
let settlement_i64 = settlement.round_to_zero().checked_to_num::<i64>().unwrap();
|
||||
let fee_i64 = fee.round_to_zero().checked_to_num::<i64>().unwrap();
|
||||
cm!(a_perp_position.perp_spot_transfers += settlement_i64 - fee_i64);
|
||||
cm!(b_perp_position.perp_spot_transfers -= settlement_i64);
|
||||
cm!(account_a.fixed.perp_spot_transfers += settlement_i64 - fee_i64);
|
||||
|
|
|
@ -424,6 +424,8 @@ pub mod mango_v4 {
|
|||
settle_fee_amount_threshold: f32,
|
||||
settle_fee_fraction_low_health: f32,
|
||||
settle_token_index: TokenIndex,
|
||||
settle_pnl_limit_factor: f32,
|
||||
settle_pnl_limit_factor_window_size_ts: u64,
|
||||
) -> Result<()> {
|
||||
instructions::perp_create_market(
|
||||
ctx,
|
||||
|
@ -450,6 +452,8 @@ pub mod mango_v4 {
|
|||
settle_fee_flat,
|
||||
settle_fee_amount_threshold,
|
||||
settle_fee_fraction_low_health,
|
||||
settle_pnl_limit_factor,
|
||||
settle_pnl_limit_factor_window_size_ts,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -478,6 +482,8 @@ pub mod mango_v4 {
|
|||
stable_price_delay_interval_seconds_opt: Option<u32>,
|
||||
stable_price_delay_growth_limit_opt: Option<f32>,
|
||||
stable_price_growth_limit_opt: Option<f32>,
|
||||
settle_pnl_limit_factor_opt: Option<f32>,
|
||||
settle_pnl_limit_factor_window_size_ts: Option<u64>,
|
||||
) -> Result<()> {
|
||||
instructions::perp_edit_market(
|
||||
ctx,
|
||||
|
@ -503,6 +509,8 @@ pub mod mango_v4 {
|
|||
stable_price_delay_interval_seconds_opt,
|
||||
stable_price_delay_growth_limit_opt,
|
||||
stable_price_growth_limit_opt,
|
||||
settle_pnl_limit_factor_opt,
|
||||
settle_pnl_limit_factor_window_size_ts,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -867,8 +867,8 @@ impl<
|
|||
if !fill.market_fees_applied {
|
||||
cm!(perp_market.fees_accrued += fees);
|
||||
}
|
||||
let quote_change_native = cm!(quote - fees);
|
||||
pa.record_trade(perp_market, base_change, quote_change_native);
|
||||
pa.record_trade(perp_market, base_change, quote);
|
||||
pa.record_fee(fees);
|
||||
|
||||
cm!(pa.maker_volume += quote.abs().to_num::<u64>());
|
||||
|
||||
|
|
|
@ -164,7 +164,9 @@ impl Default for Serum3Orders {
|
|||
pub struct PerpPosition {
|
||||
pub market_index: PerpMarketIndex,
|
||||
#[derivative(Debug = "ignore")]
|
||||
pub padding: [u8; 6],
|
||||
pub padding: [u8; 2],
|
||||
|
||||
pub settle_pnl_limit_window: u32,
|
||||
|
||||
/// Active position size, measured in base lots
|
||||
base_position_lots: i64,
|
||||
|
@ -172,8 +174,9 @@ pub struct PerpPosition {
|
|||
/// measured in native quote
|
||||
quote_position_native: I80F48,
|
||||
|
||||
pub settle_pnl_limit_settled_in_current_window_native: i64,
|
||||
|
||||
/// Tracks what the position is to calculate average entry & break even price
|
||||
pub padding2: [u8; 8],
|
||||
pub quote_running_native: i64,
|
||||
|
||||
/// Already settled funding
|
||||
|
@ -210,8 +213,9 @@ pub struct PerpPosition {
|
|||
|
||||
pub avg_entry_price_per_base_lot: f64,
|
||||
|
||||
#[derivative(Debug = "ignore")]
|
||||
pub reserved: [u8; 16],
|
||||
pub realized_pnl_native: I80F48,
|
||||
// #[derivative(Debug = "ignore")]
|
||||
// pub reserved: [u8; 8],
|
||||
}
|
||||
const_assert_eq!(size_of::<PerpPosition>(), 176);
|
||||
const_assert_eq!(size_of::<PerpPosition>() % 8, 0);
|
||||
|
@ -225,7 +229,6 @@ impl Default for PerpPosition {
|
|||
market_index: PerpMarketIndex::MAX,
|
||||
base_position_lots: 0,
|
||||
quote_position_native: I80F48::ZERO,
|
||||
padding2: Default::default(),
|
||||
quote_running_native: 0,
|
||||
bids_base_lots: 0,
|
||||
asks_base_lots: 0,
|
||||
|
@ -240,7 +243,10 @@ impl Default for PerpPosition {
|
|||
taker_volume: 0,
|
||||
perp_spot_transfers: 0,
|
||||
avg_entry_price_per_base_lot: 0.0,
|
||||
reserved: [0; 16],
|
||||
realized_pnl_native: I80F48::ZERO,
|
||||
settle_pnl_limit_window: 0,
|
||||
settle_pnl_limit_settled_in_current_window_native: 0,
|
||||
//reserved: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -312,6 +318,7 @@ impl PerpPosition {
|
|||
pub fn settle_funding(&mut self, perp_market: &PerpMarket) {
|
||||
let funding = self.unsettled_funding(perp_market);
|
||||
cm!(self.quote_position_native -= funding);
|
||||
cm!(self.realized_pnl_native -= funding);
|
||||
|
||||
if self.base_position_lots.is_positive() {
|
||||
self.cumulative_long_funding += funding.to_num::<f64>();
|
||||
|
@ -323,8 +330,8 @@ impl PerpPosition {
|
|||
self.short_settled_funding = perp_market.short_funding;
|
||||
}
|
||||
|
||||
/// Update the quote entry position
|
||||
fn update_entry_price(&mut self, base_change: i64, quote_change_native: i64) {
|
||||
/// Updates entry price, breakeven price, realized pnl
|
||||
fn update_trade_stats(&mut self, base_change: i64, quote_change_native: I80F48) {
|
||||
if base_change == 0 {
|
||||
return;
|
||||
}
|
||||
|
@ -333,30 +340,53 @@ impl PerpPosition {
|
|||
let new_position = cm!(old_position + base_change);
|
||||
|
||||
if new_position == 0 {
|
||||
// clear out entry and break-even prices
|
||||
self.avg_entry_price_per_base_lot = 0.0;
|
||||
self.quote_running_native = 0;
|
||||
return;
|
||||
|
||||
// There can't be unrealized pnl without a base position, so fix the
|
||||
// realized pnl to cover the whole quote position.
|
||||
self.realized_pnl_native = cm!(self.quote_position_native + quote_change_native);
|
||||
} else if old_position.signum() != new_position.signum() {
|
||||
// If the base position changes sign, reset
|
||||
self.avg_entry_price_per_base_lot =
|
||||
((quote_change_native as f64) / (base_change as f64)).abs();
|
||||
self.quote_running_native =
|
||||
-((new_position as f64) * self.avg_entry_price_per_base_lot).round() as i64;
|
||||
return;
|
||||
}
|
||||
// If the base position changes sign, we've crossed base_pos == 0 (or old_position == 0)
|
||||
let old_position = old_position as f64;
|
||||
let new_position = new_position as f64;
|
||||
let base_change = base_change as f64;
|
||||
let old_avg_entry = self.avg_entry_price_per_base_lot;
|
||||
let new_avg_entry = (quote_change_native.to_num::<f64>() / base_change).abs();
|
||||
|
||||
// Track all quote changes as long as the base position sign stays the same
|
||||
cm!(self.quote_running_native += quote_change_native);
|
||||
// Award realized pnl based on the old_position size
|
||||
let new_realized_pnl = I80F48::from_num(old_position * (new_avg_entry - old_avg_entry));
|
||||
cm!(self.realized_pnl_native += new_realized_pnl);
|
||||
|
||||
let is_increasing = old_position.signum() == base_change.signum();
|
||||
if is_increasing {
|
||||
let new_position_quote_value = (old_position.abs() as f64)
|
||||
* self.avg_entry_price_per_base_lot
|
||||
+ (quote_change_native.abs() as f64);
|
||||
self.avg_entry_price_per_base_lot =
|
||||
new_position_quote_value / (new_position.abs() as f64);
|
||||
// Set entry and break-even based on the new_position entered
|
||||
self.avg_entry_price_per_base_lot = new_avg_entry;
|
||||
self.quote_running_native = (-new_position * new_avg_entry) as i64;
|
||||
} else {
|
||||
// The old and new position have the same sign
|
||||
|
||||
cm!(self.quote_running_native += quote_change_native
|
||||
.round_to_zero()
|
||||
.checked_to_num::<i64>()
|
||||
.unwrap());
|
||||
|
||||
let is_increasing = old_position.signum() == base_change.signum();
|
||||
if is_increasing {
|
||||
// Increasing position: avg entry price updates, no new realized pnl
|
||||
let old_position_abs = old_position.abs() as f64;
|
||||
let new_position_abs = new_position.abs() as f64;
|
||||
let old_avg_entry = self.avg_entry_price_per_base_lot;
|
||||
let new_position_quote_value =
|
||||
old_position_abs * old_avg_entry + quote_change_native.to_num::<f64>().abs();
|
||||
self.avg_entry_price_per_base_lot = new_position_quote_value / new_position_abs;
|
||||
} else {
|
||||
// Decreasing position: pnl is realized, avg entry price does not change
|
||||
let avg_entry = I80F48::from_num(self.avg_entry_price_per_base_lot);
|
||||
let new_realized_pnl =
|
||||
cm!(quote_change_native + I80F48::from(base_change) * avg_entry);
|
||||
cm!(self.realized_pnl_native += new_realized_pnl);
|
||||
}
|
||||
}
|
||||
// The average entry price does not change when the position decreases while keeping sign.
|
||||
}
|
||||
|
||||
/// Change the base and quote positions as the result of a trade
|
||||
|
@ -367,15 +397,12 @@ impl PerpPosition {
|
|||
quote_change_native: I80F48,
|
||||
) {
|
||||
assert_eq!(perp_market.perp_market_index, self.market_index);
|
||||
self.update_entry_price(
|
||||
base_change,
|
||||
quote_change_native.round().checked_to_num().unwrap(),
|
||||
);
|
||||
self.update_trade_stats(base_change, quote_change_native);
|
||||
self.change_base_position(perp_market, base_change);
|
||||
self.change_quote_position(quote_change_native);
|
||||
}
|
||||
|
||||
pub fn change_quote_position(&mut self, quote_change_native: I80F48) {
|
||||
fn change_quote_position(&mut self, quote_change_native: I80F48) {
|
||||
cm!(self.quote_position_native += quote_change_native);
|
||||
}
|
||||
|
||||
|
@ -410,6 +437,49 @@ impl PerpPosition {
|
|||
let pnl: I80F48 = cm!(self.quote_position_native() + base_native * price);
|
||||
Ok(pnl)
|
||||
}
|
||||
|
||||
/// Side-effect: updates the windowing
|
||||
pub fn update_and_get_used_settle_limit(&mut self, market: &PerpMarket, now_ts: u64) -> i64 {
|
||||
assert_eq!(self.market_index, market.perp_market_index);
|
||||
let window_size = market.settle_pnl_limit_factor_window_size_ts;
|
||||
let new_window = now_ts >= cm!((self.settle_pnl_limit_window + 1) as u64 * window_size);
|
||||
if new_window {
|
||||
self.settle_pnl_limit_window = cm!(now_ts / window_size).try_into().unwrap();
|
||||
self.settle_pnl_limit_settled_in_current_window_native = 0;
|
||||
}
|
||||
self.settle_pnl_limit_settled_in_current_window_native
|
||||
}
|
||||
|
||||
/// Update the perp position for pnl settlement
|
||||
///
|
||||
/// If `pnl` is positive, then that is settled away, deducting from the quote position.
|
||||
pub fn record_settle(&mut self, pnl: I80F48) {
|
||||
self.change_quote_position(-pnl);
|
||||
|
||||
let pnl_i64 = pnl.round_to_zero().checked_to_num::<i64>().unwrap();
|
||||
cm!(self.settle_pnl_limit_settled_in_current_window_native += pnl_i64);
|
||||
|
||||
let used_realized = if pnl > 0 {
|
||||
// Example: settling 100 positive pnl, with 60 realized:
|
||||
// pnl = 100 -> used_realized = 60
|
||||
pnl.min(self.realized_pnl_native).max(I80F48::ZERO)
|
||||
} else {
|
||||
// Example: settling 100 negative pnl, with -60 realized:
|
||||
// pnl = -100 -> used_realized = -60
|
||||
pnl.max(self.realized_pnl_native).min(I80F48::ZERO)
|
||||
};
|
||||
cm!(self.realized_pnl_native -= used_realized);
|
||||
}
|
||||
|
||||
pub fn record_fee(&mut self, fee: I80F48) {
|
||||
self.change_quote_position(-fee);
|
||||
cm!(self.realized_pnl_native -= fee);
|
||||
}
|
||||
|
||||
pub fn record_bankruptcy_quote_change(&mut self, change: I80F48) {
|
||||
self.change_quote_position(change);
|
||||
cm!(self.realized_pnl_native += change);
|
||||
}
|
||||
}
|
||||
|
||||
#[zero_copy]
|
||||
|
@ -473,7 +543,11 @@ mod tests {
|
|||
pos.base_position_lots = base_pos;
|
||||
pos.quote_position_native = I80F48::from(quote_pos);
|
||||
pos.quote_running_native = quote_pos;
|
||||
pos.avg_entry_price_per_base_lot = ((quote_pos as f64) / (base_pos as f64)).abs();
|
||||
pos.avg_entry_price_per_base_lot = if base_pos != 0 {
|
||||
((quote_pos as f64) / (base_pos as f64)).abs()
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
pos
|
||||
}
|
||||
|
||||
|
@ -486,6 +560,7 @@ mod tests {
|
|||
assert_eq!(pos.quote_running_native, -100);
|
||||
assert_eq!(pos.avg_entry_price(&market), 10.0);
|
||||
assert_eq!(pos.break_even_price(&market), 10.0);
|
||||
assert_eq!(pos.realized_pnl_native, I80F48::from(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -497,6 +572,7 @@ mod tests {
|
|||
assert_eq!(pos.quote_running_native, 100);
|
||||
assert_eq!(pos.avg_entry_price(&market), 10.0);
|
||||
assert_eq!(pos.break_even_price(&market), 10.0);
|
||||
assert_eq!(pos.realized_pnl_native, I80F48::from(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -508,6 +584,7 @@ mod tests {
|
|||
assert_eq!(pos.quote_running_native, -400);
|
||||
assert_eq!(pos.avg_entry_price(&market), 20.0);
|
||||
assert_eq!(pos.break_even_price(&market), 20.0);
|
||||
assert_eq!(pos.realized_pnl_native, I80F48::from(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -519,6 +596,7 @@ mod tests {
|
|||
assert_eq!(pos.quote_running_native, 400);
|
||||
assert_eq!(pos.avg_entry_price(&market), 20.0);
|
||||
assert_eq!(pos.break_even_price(&market), 20.0);
|
||||
assert_eq!(pos.realized_pnl_native, I80F48::from(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -530,6 +608,7 @@ mod tests {
|
|||
assert_eq!(pos.quote_running_native, -150);
|
||||
assert_eq!(pos.avg_entry_price(&market), 10.0); // Entry price remains the same when decreasing
|
||||
assert_eq!(pos.break_even_price(&market), -30.0); // Already broke even
|
||||
assert_eq!(pos.realized_pnl_native, I80F48::from(-200));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -541,28 +620,31 @@ mod tests {
|
|||
assert_eq!(pos.quote_running_native, 150);
|
||||
assert_eq!(pos.avg_entry_price(&market), 10.0); // Entry price remains the same when decreasing
|
||||
assert_eq!(pos.break_even_price(&market), -30.0); // Already broke even
|
||||
assert_eq!(pos.realized_pnl_native, I80F48::from(200));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_quote_entry_long_close_with_short() {
|
||||
let mut market = PerpMarket::default_for_tests();
|
||||
let mut pos = create_perp_position(&market, 10, -100);
|
||||
// Go short 10 @ 50
|
||||
// Go short 10 @ 25
|
||||
pos.record_trade(&mut market, -10, I80F48::from(250));
|
||||
assert_eq!(pos.quote_running_native, 0);
|
||||
assert_eq!(pos.avg_entry_price(&market), 0.0); // Entry price zero when no position
|
||||
assert_eq!(pos.break_even_price(&market), 0.0);
|
||||
assert_eq!(pos.realized_pnl_native, I80F48::from(150));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_quote_entry_short_close_with_long() {
|
||||
let mut market = PerpMarket::default_for_tests();
|
||||
let mut pos = create_perp_position(&market, -10, 100);
|
||||
// Go long 10 @ 50
|
||||
// Go long 10 @ 25
|
||||
pos.record_trade(&mut market, 10, I80F48::from(-250));
|
||||
assert_eq!(pos.quote_running_native, 0);
|
||||
assert_eq!(pos.avg_entry_price(&market), 0.0); // Entry price zero when no position
|
||||
assert_eq!(pos.break_even_price(&market), 0.0);
|
||||
assert_eq!(pos.realized_pnl_native, I80F48::from(-150));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -574,17 +656,19 @@ mod tests {
|
|||
assert_eq!(pos.quote_running_native, 100);
|
||||
assert_eq!(pos.avg_entry_price(&market), 20.0);
|
||||
assert_eq!(pos.break_even_price(&market), 20.0);
|
||||
assert_eq!(pos.realized_pnl_native, I80F48::from(100));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_quote_entry_short_close_long_with_overflow() {
|
||||
let mut market = PerpMarket::default_for_tests();
|
||||
let mut pos = create_perp_position(&market, -10, 100);
|
||||
// Go short 15 @ 20
|
||||
// Go long 15 @ 20
|
||||
pos.record_trade(&mut market, 15, I80F48::from(-300));
|
||||
assert_eq!(pos.quote_running_native, -100);
|
||||
assert_eq!(pos.avg_entry_price(&market), 20.0);
|
||||
assert_eq!(pos.break_even_price(&market), 20.0);
|
||||
assert_eq!(pos.realized_pnl_native, I80F48::from(-100));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -598,6 +682,7 @@ mod tests {
|
|||
assert_eq!(pos.quote_running_native, -98_000);
|
||||
assert_eq!(pos.base_position_lots, 10);
|
||||
assert_eq!(pos.break_even_price(&market), 9_800.0); // We made 2k on the trade, so we can sell our contract up to a loss of 200 each
|
||||
assert_eq!(pos.realized_pnl_native, I80F48::from(2_000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -615,6 +700,35 @@ mod tests {
|
|||
assert_eq!(pos.avg_entry_price_per_base_lot, 100_000.0);
|
||||
assert_eq!(pos.avg_entry_price(&market), 10_000.0);
|
||||
assert_eq!(pos.break_even_price(&market), 9_800.0);
|
||||
assert_eq!(pos.realized_pnl_native, I80F48::from(20_000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_realized_pnl_fractional() {
|
||||
let mut market = PerpMarket::default_for_tests();
|
||||
let mut pos = create_perp_position(&market, 0, 0);
|
||||
pos.quote_position_native += I80F48::from_num(0.1);
|
||||
|
||||
// Buy 1 @ 1
|
||||
pos.record_trade(&mut market, 1, I80F48::from(-1));
|
||||
// Buy 2 @ 2
|
||||
pos.record_trade(&mut market, 2, I80F48::from(-2 * 2));
|
||||
|
||||
assert!((pos.avg_entry_price(&market) - 1.66666).abs() < 0.001);
|
||||
assert_eq!(pos.realized_pnl_native, I80F48::from(0));
|
||||
|
||||
// Sell 2 @ 4
|
||||
pos.record_trade(&mut market, -2, I80F48::from(2 * 4));
|
||||
|
||||
assert!((pos.avg_entry_price(&market) - 1.66666).abs() < 0.001);
|
||||
assert!((pos.realized_pnl_native.to_num::<f64>() - 4.6666).abs() < 0.01);
|
||||
|
||||
// Sell 1 @ 2
|
||||
pos.record_trade(&mut market, -1, I80F48::from(2));
|
||||
|
||||
assert_eq!(pos.avg_entry_price(&market), 0.0);
|
||||
assert!((pos.quote_position_native.to_num::<f64>() - 5.1).abs() < 0.001);
|
||||
assert!((pos.realized_pnl_native.to_num::<f64>() - 5.1).abs() < 0.01);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -670,4 +784,38 @@ mod tests {
|
|||
let pnl = short_pos.pnl_for_price(&market, I80F48::from(9)).unwrap();
|
||||
assert_eq!(pnl, I80F48::from(50 * 10 * 1), "short profitable");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_perp_realized_pnl_consumption() {
|
||||
let market = PerpMarket::default_for_tests();
|
||||
|
||||
let mut pos = create_perp_position(&market, 0, 0);
|
||||
assert_eq!(pos.realized_pnl_native, I80F48::from(0));
|
||||
|
||||
pos.record_settle(I80F48::from(10));
|
||||
assert_eq!(pos.realized_pnl_native, I80F48::from(0));
|
||||
|
||||
pos.record_settle(I80F48::from(-20));
|
||||
assert_eq!(pos.realized_pnl_native, I80F48::from(0));
|
||||
|
||||
pos.realized_pnl_native = I80F48::from(5);
|
||||
pos.record_settle(I80F48::from(-20));
|
||||
assert_eq!(pos.realized_pnl_native, I80F48::from(5));
|
||||
|
||||
pos.record_settle(I80F48::from(2));
|
||||
assert_eq!(pos.realized_pnl_native, I80F48::from(3));
|
||||
|
||||
pos.record_settle(I80F48::from(10));
|
||||
assert_eq!(pos.realized_pnl_native, I80F48::from(0));
|
||||
|
||||
pos.realized_pnl_native = I80F48::from(-5);
|
||||
pos.record_settle(I80F48::from(20));
|
||||
assert_eq!(pos.realized_pnl_native, I80F48::from(-5));
|
||||
|
||||
pos.record_settle(I80F48::from(-2));
|
||||
assert_eq!(pos.realized_pnl_native, I80F48::from(-3));
|
||||
|
||||
pos.record_settle(I80F48::from(-10));
|
||||
assert_eq!(pos.realized_pnl_native, I80F48::from(0));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -393,7 +393,7 @@ fn apply_fees(
|
|||
require_gte!(taker_fees, 0);
|
||||
|
||||
let perp_account = mango_account.perp_position_mut(market.perp_market_index)?;
|
||||
perp_account.change_quote_position(-taker_fees);
|
||||
perp_account.record_fee(taker_fees);
|
||||
cm!(market.fees_accrued += taker_fees + maker_fees);
|
||||
cm!(perp_account.taker_volume += taker_fees.to_num::<u64>());
|
||||
|
||||
|
@ -405,7 +405,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.change_quote_position(-fee_penalty);
|
||||
perp_account.record_fee(fee_penalty);
|
||||
cm!(market.fees_accrued += fee_penalty);
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -111,7 +111,10 @@ pub struct PerpMarket {
|
|||
|
||||
pub stable_price_model: StablePriceModel,
|
||||
|
||||
pub reserved: [u8; 1956],
|
||||
pub settle_pnl_limit_factor: f32,
|
||||
pub settle_pnl_limit_factor_window_size_ts: u64,
|
||||
|
||||
pub reserved: [u8; 1944],
|
||||
}
|
||||
|
||||
const_assert_eq!(size_of::<PerpMarket>(), 2784);
|
||||
|
@ -136,6 +139,10 @@ impl PerpMarket {
|
|||
self.trusted_market == 1
|
||||
}
|
||||
|
||||
pub fn settle_pnl_limit_factor(&self) -> I80F48 {
|
||||
I80F48::from_num(self.settle_pnl_limit_factor)
|
||||
}
|
||||
|
||||
pub fn gen_order_id(&mut self, side: Side, price_data: u64) -> u128 {
|
||||
self.seq_num += 1;
|
||||
orderbook::new_node_key(side, price_data, self.seq_num)
|
||||
|
@ -273,6 +280,9 @@ impl PerpMarket {
|
|||
group: Pubkey::new_unique(),
|
||||
settle_token_index: 0,
|
||||
perp_market_index: 0,
|
||||
trusted_market: 0,
|
||||
group_insurance_fund: 0,
|
||||
padding1: Default::default(),
|
||||
name: Default::default(),
|
||||
oracle: Pubkey::new_unique(),
|
||||
oracle_config: OracleConfig {
|
||||
|
@ -300,20 +310,19 @@ impl PerpMarket {
|
|||
open_interest: 0,
|
||||
seq_num: 0,
|
||||
fees_accrued: I80F48::ZERO,
|
||||
fees_settled: I80F48::ZERO,
|
||||
bump: 0,
|
||||
base_decimals: 0,
|
||||
reserved: [0; 1956],
|
||||
padding1: Default::default(),
|
||||
padding2: Default::default(),
|
||||
registration_time: 0,
|
||||
fees_settled: I80F48::ZERO,
|
||||
fee_penalty: 0.0,
|
||||
trusted_market: 0,
|
||||
group_insurance_fund: 0,
|
||||
settle_fee_flat: 0.0,
|
||||
settle_fee_amount_threshold: 0.0,
|
||||
settle_fee_fraction_low_health: 0.0,
|
||||
stable_price_model: StablePriceModel::default(),
|
||||
settle_pnl_limit_factor: 0.2,
|
||||
settle_pnl_limit_factor_window_size_ts: 24 * 60 * 60,
|
||||
reserved: [0; 1944],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2422,6 +2422,8 @@ pub struct PerpCreateMarketInstruction {
|
|||
pub settle_fee_flat: f32,
|
||||
pub settle_fee_amount_threshold: f32,
|
||||
pub settle_fee_fraction_low_health: f32,
|
||||
pub settle_pnl_limit_factor: f32,
|
||||
pub settle_pnl_limit_factor_window_size_ts: u64,
|
||||
}
|
||||
impl PerpCreateMarketInstruction {
|
||||
pub async fn with_new_book_and_queue(
|
||||
|
@ -2477,6 +2479,8 @@ impl ClientInstruction for PerpCreateMarketInstruction {
|
|||
settle_fee_flat: self.settle_fee_flat,
|
||||
settle_fee_amount_threshold: self.settle_fee_amount_threshold,
|
||||
settle_fee_fraction_low_health: self.settle_fee_fraction_low_health,
|
||||
settle_pnl_limit_factor: self.settle_pnl_limit_factor,
|
||||
settle_pnl_limit_factor_window_size_ts: self.settle_pnl_limit_factor_window_size_ts,
|
||||
};
|
||||
|
||||
let perp_market = Pubkey::find_program_address(
|
||||
|
@ -2550,6 +2554,8 @@ impl ClientInstruction for PerpResetStablePriceModel {
|
|||
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_factor_window_size_ts: None,
|
||||
};
|
||||
|
||||
let accounts = Self::Accounts {
|
||||
|
|
|
@ -219,6 +219,8 @@ async fn test_health_compute_perp() -> Result<(), TransportError> {
|
|||
liquidation_fee: 0.012,
|
||||
maker_fee: 0.0002,
|
||||
taker_fee: 0.000,
|
||||
settle_pnl_limit_factor: 0.2,
|
||||
settle_pnl_limit_factor_window_size_ts: 24 * 60 * 60,
|
||||
..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, &token).await
|
||||
},
|
||||
)
|
||||
|
|
|
@ -62,6 +62,8 @@ async fn test_liq_perps_force_cancel() -> Result<(), TransportError> {
|
|||
liquidation_fee: 0.05,
|
||||
maker_fee: 0.0,
|
||||
taker_fee: 0.0,
|
||||
settle_pnl_limit_factor: 0.2,
|
||||
settle_pnl_limit_factor_window_size_ts: 24 * 60 * 60,
|
||||
..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, base_token).await
|
||||
},
|
||||
)
|
||||
|
@ -263,6 +265,8 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr
|
|||
maker_fee: 0.0,
|
||||
taker_fee: 0.0,
|
||||
group_insurance_fund: true,
|
||||
settle_pnl_limit_factor: 0.2,
|
||||
settle_pnl_limit_factor_window_size_ts: 24 * 60 * 60,
|
||||
..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, base_token).await
|
||||
},
|
||||
)
|
||||
|
|
|
@ -85,6 +85,8 @@ 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_window_size_ts: 24 * 60 * 60,
|
||||
..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, &tokens[0]).await
|
||||
},
|
||||
)
|
||||
|
@ -521,6 +523,8 @@ async fn test_perp_oracle_peg() -> Result<(), TransportError> {
|
|||
liquidation_fee: 0.012,
|
||||
maker_fee: -0.0001,
|
||||
taker_fee: 0.0002,
|
||||
settle_pnl_limit_factor: 0.2,
|
||||
settle_pnl_limit_factor_window_size_ts: 24 * 60 * 60,
|
||||
..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, &tokens[0]).await
|
||||
},
|
||||
)
|
||||
|
|
|
@ -80,6 +80,8 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> {
|
|||
liquidation_fee: 0.012,
|
||||
maker_fee: 0.0002,
|
||||
taker_fee: 0.000,
|
||||
settle_pnl_limit_factor: 0.2,
|
||||
settle_pnl_limit_factor_window_size_ts: 24 * 60 * 60,
|
||||
..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, &tokens[1]).await
|
||||
},
|
||||
)
|
||||
|
@ -108,6 +110,8 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> {
|
|||
liquidation_fee: 0.012,
|
||||
maker_fee: 0.0002,
|
||||
taker_fee: 0.000,
|
||||
settle_pnl_limit_factor: 0.2,
|
||||
settle_pnl_limit_factor_window_size_ts: 24 * 60 * 60,
|
||||
..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, &tokens[2]).await
|
||||
},
|
||||
)
|
||||
|
@ -580,6 +584,8 @@ async fn test_perp_settle_pnl_fees() -> Result<(), TransportError> {
|
|||
settle_fee_flat: flat_fee as f32,
|
||||
settle_fee_amount_threshold: 2000.0,
|
||||
settle_fee_fraction_low_health: fee_low_health,
|
||||
settle_pnl_limit_factor: 0.2,
|
||||
settle_pnl_limit_factor_window_size_ts: 24 * 60 * 60,
|
||||
..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, &tokens[1]).await
|
||||
},
|
||||
)
|
||||
|
@ -772,3 +778,215 @@ async fn test_perp_settle_pnl_fees() -> Result<(), TransportError> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_perp_pnl_settle_limit() -> Result<(), TransportError> {
|
||||
let context = TestContext::new().await;
|
||||
let solana = &context.solana.clone();
|
||||
|
||||
let admin = TestKeypair::new();
|
||||
let owner = context.users[0].key;
|
||||
let payer = context.users[1].key;
|
||||
let mints = &context.mints[0..=2];
|
||||
|
||||
let initial_token_deposit = 1000_000;
|
||||
|
||||
//
|
||||
// SETUP: Create a group and an account
|
||||
//
|
||||
|
||||
let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
|
||||
admin,
|
||||
payer,
|
||||
mints: mints.to_vec(),
|
||||
..GroupWithTokensConfig::default()
|
||||
}
|
||||
.create(solana)
|
||||
.await;
|
||||
|
||||
let settler =
|
||||
create_funded_account(&solana, group, owner, 251, &context.users[1], &[], 0, 0).await;
|
||||
let settler_owner = owner.clone();
|
||||
|
||||
let account_0 = create_funded_account(
|
||||
&solana,
|
||||
group,
|
||||
owner,
|
||||
0,
|
||||
&context.users[1],
|
||||
&mints[0..1],
|
||||
initial_token_deposit,
|
||||
0,
|
||||
)
|
||||
.await;
|
||||
let account_1 = create_funded_account(
|
||||
&solana,
|
||||
group,
|
||||
owner,
|
||||
1,
|
||||
&context.users[1],
|
||||
&mints[0..1],
|
||||
initial_token_deposit * 100, // Fund 100x, so that this is not the bound for what account_0 can settle
|
||||
0,
|
||||
)
|
||||
.await;
|
||||
|
||||
//
|
||||
// TEST: Create a perp market
|
||||
//
|
||||
let mango_v4::accounts::PerpCreateMarket { perp_market, .. } = send_tx(
|
||||
solana,
|
||||
PerpCreateMarketInstruction {
|
||||
group,
|
||||
admin,
|
||||
payer,
|
||||
perp_market_index: 0,
|
||||
quote_lot_size: 10,
|
||||
base_lot_size: 100,
|
||||
maint_asset_weight: 0.975,
|
||||
init_asset_weight: 0.95,
|
||||
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,
|
||||
settle_pnl_limit_factor_window_size_ts: 24 * 60 * 60,
|
||||
..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, &tokens[1]).await
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let price_lots = {
|
||||
let perp_market = solana.get_account::<PerpMarket>(perp_market).await;
|
||||
perp_market.native_price_to_lot(I80F48::from(1000))
|
||||
};
|
||||
|
||||
// Set the initial oracle price
|
||||
send_tx(
|
||||
solana,
|
||||
StubOracleSetInstruction {
|
||||
group,
|
||||
admin,
|
||||
mint: mints[1].pubkey,
|
||||
price: 1000.0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
//
|
||||
// Place orders and create a position
|
||||
//
|
||||
send_tx(
|
||||
solana,
|
||||
PerpPlaceOrderInstruction {
|
||||
account: account_0,
|
||||
perp_market,
|
||||
owner,
|
||||
side: Side::Bid,
|
||||
price_lots,
|
||||
max_base_lots: 1,
|
||||
max_quote_lots: i64::MAX,
|
||||
client_order_id: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
PerpPlaceOrderInstruction {
|
||||
account: account_1,
|
||||
perp_market,
|
||||
owner,
|
||||
side: Side::Ask,
|
||||
price_lots,
|
||||
max_base_lots: 1,
|
||||
max_quote_lots: i64::MAX,
|
||||
client_order_id: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
PerpConsumeEventsInstruction {
|
||||
perp_market,
|
||||
mango_accounts: vec![account_0, account_1],
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Manipulate the price
|
||||
send_tx(
|
||||
solana,
|
||||
StubOracleSetInstruction {
|
||||
group,
|
||||
admin,
|
||||
mint: mints[1].pubkey,
|
||||
price: 10000.0, // 10x original price
|
||||
},
|
||||
)
|
||||
.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
|
||||
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))
|
||||
* mango_account_0.perps[0].base_position_native(&market))
|
||||
.abs()
|
||||
// fees
|
||||
- I80F48::from_num(1000.0 * 100.0 * 0.0002);
|
||||
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_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
|
||||
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_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()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -155,6 +155,8 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
|
|||
liquidation_fee: 0.012,
|
||||
maker_fee: 0.0002,
|
||||
taker_fee: 0.000,
|
||||
settle_pnl_limit_factor: 0.2,
|
||||
settle_pnl_limit_factor_window_size_ts: 24 * 60 * 60,
|
||||
..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, &tokens[0]).await
|
||||
},
|
||||
)
|
||||
|
@ -183,6 +185,8 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
|
|||
liquidation_fee: 0.012,
|
||||
maker_fee: 0.0002,
|
||||
taker_fee: 0.000,
|
||||
settle_pnl_limit_factor: 0.2,
|
||||
settle_pnl_limit_factor_window_size_ts: 24 * 60 * 60,
|
||||
..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, &tokens[1]).await
|
||||
},
|
||||
)
|
||||
|
|
|
@ -606,6 +606,24 @@ export type MangoV4 = {
|
|||
"type": {
|
||||
"option": "f32"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "minVaultToDepositsRatioOpt",
|
||||
"type": {
|
||||
"option": "f64"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "netBorrowsLimitNativeOpt",
|
||||
"type": {
|
||||
"option": "i64"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "netBorrowsWindowSizeTsOpt",
|
||||
"type": {
|
||||
"option": "u64"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -2426,6 +2444,14 @@ export type MangoV4 = {
|
|||
{
|
||||
"name": "settleTokenIndex",
|
||||
"type": "u16"
|
||||
},
|
||||
{
|
||||
"name": "settlePnlLimitFactor",
|
||||
"type": "f32"
|
||||
},
|
||||
{
|
||||
"name": "settlePnlLimitFactorWindowSizeTs",
|
||||
"type": "u64"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -2587,6 +2613,18 @@ export type MangoV4 = {
|
|||
"type": {
|
||||
"option": "f32"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "settlePnlLimitFactorOpt",
|
||||
"type": {
|
||||
"option": "f32"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "settlePnlLimitFactorWindowSizeTs",
|
||||
"type": {
|
||||
"option": "u64"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -4247,12 +4285,20 @@ export type MangoV4 = {
|
|||
"defined": "StablePriceModel"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "settlePnlLimitFactor",
|
||||
"type": "f32"
|
||||
},
|
||||
{
|
||||
"name": "settlePnlLimitFactorWindowSizeTs",
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "reserved",
|
||||
"type": {
|
||||
"array": [
|
||||
"u8",
|
||||
1956
|
||||
1944
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -4845,10 +4891,14 @@ export type MangoV4 = {
|
|||
"type": {
|
||||
"array": [
|
||||
"u8",
|
||||
6
|
||||
2
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "settlePnlLimitWindow",
|
||||
"type": "u32"
|
||||
},
|
||||
{
|
||||
"name": "basePositionLots",
|
||||
"docs": [
|
||||
|
@ -4867,11 +4917,16 @@ export type MangoV4 = {
|
|||
}
|
||||
},
|
||||
{
|
||||
"name": "quoteEntryNative",
|
||||
"name": "padding2",
|
||||
"docs": [
|
||||
"Tracks what the position is to calculate average entry & break even price"
|
||||
],
|
||||
"type": "i64"
|
||||
"type": {
|
||||
"array": [
|
||||
"u8",
|
||||
8
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "quoteRunningNative",
|
||||
|
@ -4939,13 +4994,16 @@ export type MangoV4 = {
|
|||
"type": "i64"
|
||||
},
|
||||
{
|
||||
"name": "reserved",
|
||||
"type": {
|
||||
"array": [
|
||||
"u8",
|
||||
24
|
||||
]
|
||||
}
|
||||
"name": "avgEntryPricePerBaseLot",
|
||||
"type": "f64"
|
||||
},
|
||||
{
|
||||
"name": "realizedPnlNative",
|
||||
"type": "i64"
|
||||
},
|
||||
{
|
||||
"name": "settlePnlLimitSettledInCurrentWindowNative",
|
||||
"type": "i64"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -7133,7 +7191,7 @@ export type MangoV4 = {
|
|||
{
|
||||
"code": 6027,
|
||||
"name": "BankNetBorrowsLimitReached",
|
||||
"msg": "bank net borrows has reached limit"
|
||||
"msg": "bank net borrows has reached limit - this is an intermittent error - the limit will reset regularly"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
@ -7746,6 +7804,24 @@ export const IDL: MangoV4 = {
|
|||
"type": {
|
||||
"option": "f32"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "minVaultToDepositsRatioOpt",
|
||||
"type": {
|
||||
"option": "f64"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "netBorrowsLimitNativeOpt",
|
||||
"type": {
|
||||
"option": "i64"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "netBorrowsWindowSizeTsOpt",
|
||||
"type": {
|
||||
"option": "u64"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -9566,6 +9642,14 @@ export const IDL: MangoV4 = {
|
|||
{
|
||||
"name": "settleTokenIndex",
|
||||
"type": "u16"
|
||||
},
|
||||
{
|
||||
"name": "settlePnlLimitFactor",
|
||||
"type": "f32"
|
||||
},
|
||||
{
|
||||
"name": "settlePnlLimitFactorWindowSizeTs",
|
||||
"type": "u64"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -9727,6 +9811,18 @@ export const IDL: MangoV4 = {
|
|||
"type": {
|
||||
"option": "f32"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "settlePnlLimitFactorOpt",
|
||||
"type": {
|
||||
"option": "f32"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "settlePnlLimitFactorWindowSizeTs",
|
||||
"type": {
|
||||
"option": "u64"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -11387,12 +11483,20 @@ export const IDL: MangoV4 = {
|
|||
"defined": "StablePriceModel"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "settlePnlLimitFactor",
|
||||
"type": "f32"
|
||||
},
|
||||
{
|
||||
"name": "settlePnlLimitFactorWindowSizeTs",
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "reserved",
|
||||
"type": {
|
||||
"array": [
|
||||
"u8",
|
||||
1956
|
||||
1944
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -11985,10 +12089,14 @@ export const IDL: MangoV4 = {
|
|||
"type": {
|
||||
"array": [
|
||||
"u8",
|
||||
6
|
||||
2
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "settlePnlLimitWindow",
|
||||
"type": "u32"
|
||||
},
|
||||
{
|
||||
"name": "basePositionLots",
|
||||
"docs": [
|
||||
|
@ -12007,11 +12115,16 @@ export const IDL: MangoV4 = {
|
|||
}
|
||||
},
|
||||
{
|
||||
"name": "quoteEntryNative",
|
||||
"name": "padding2",
|
||||
"docs": [
|
||||
"Tracks what the position is to calculate average entry & break even price"
|
||||
],
|
||||
"type": "i64"
|
||||
"type": {
|
||||
"array": [
|
||||
"u8",
|
||||
8
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "quoteRunningNative",
|
||||
|
@ -12079,13 +12192,16 @@ export const IDL: MangoV4 = {
|
|||
"type": "i64"
|
||||
},
|
||||
{
|
||||
"name": "reserved",
|
||||
"type": {
|
||||
"array": [
|
||||
"u8",
|
||||
24
|
||||
]
|
||||
}
|
||||
"name": "avgEntryPricePerBaseLot",
|
||||
"type": "f64"
|
||||
},
|
||||
{
|
||||
"name": "realizedPnlNative",
|
||||
"type": "i64"
|
||||
},
|
||||
{
|
||||
"name": "settlePnlLimitSettledInCurrentWindowNative",
|
||||
"type": "i64"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -14273,7 +14389,7 @@ export const IDL: MangoV4 = {
|
|||
{
|
||||
"code": 6027,
|
||||
"name": "BankNetBorrowsLimitReached",
|
||||
"msg": "bank net borrows has reached limit"
|
||||
"msg": "bank net borrows has reached limit - this is an intermittent error - the limit will reset regularly"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue