mango-v4/programs/mango-v4/src/instructions/perp_liq_base_or_positive_p...

873 lines
32 KiB
Rust

use anchor_lang::prelude::*;
use checked_math as cm;
use fixed::types::I80F48;
use crate::accounts_zerocopy::*;
use crate::error::*;
use crate::health::*;
use crate::state::*;
use crate::accounts_ix::*;
use crate::logs::{emit_perp_balances, PerpLiqBaseOrPositivePnlLog, TokenBalanceLog};
/// This instruction deals with increasing health by:
/// - reducing the liqee's base position
/// - taking over the liqee's positive pnl
///
/// It's a combined instruction because reducing the base position is not necessarily
/// a health-increasing action when perp overall asset weight = 0. There, the pnl
/// takeover can allow further base position to be reduced.
///
/// Taking over negative pnl - or positive pnl when the unweighted perp health contributin
/// is negative - never increases liqee health. That's why it's relegated to the
/// separate liq_negative_pnl_or_bankruptcy instruction instead.
pub fn perp_liq_base_or_positive_pnl(
ctx: Context<PerpLiqBaseOrPositivePnl>,
mut max_base_transfer: i64,
max_pnl_transfer: u64,
) -> Result<()> {
// Ensure max_base_transfer can be negated
max_base_transfer = max_base_transfer.max(i64::MIN + 1);
let group_pk = &ctx.accounts.group.key();
require_keys_neq!(ctx.accounts.liqor.key(), ctx.accounts.liqee.key());
let mut liqor = ctx.accounts.liqor.load_full_mut()?;
// account constraint #1
require!(
liqor
.fixed
.is_owner_or_delegate(ctx.accounts.liqor_owner.key()),
MangoError::SomeError
);
require_msg_typed!(
!liqor.fixed.being_liquidated(),
MangoError::BeingLiquidated,
"liqor account"
);
let mut liqee = ctx.accounts.liqee.load_full_mut()?;
// Initial liqee health check
let mut liqee_health_cache = {
let account_retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, group_pk)
.context("create account retriever")?;
new_health_cache(&liqee.borrow(), &account_retriever)
.context("create liqee health cache")?
};
let liqee_liq_end_health = liqee_health_cache.health(HealthType::LiquidationEnd);
liqee_health_cache.require_after_phase1_liquidation()?;
if !liqee.check_liquidatable(&liqee_health_cache)? {
return Ok(());
}
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
let perp_market_index = perp_market.perp_market_index;
let settle_token_index = perp_market.settle_token_index;
let mut settle_bank = ctx.accounts.settle_bank.load_mut()?;
// account constraint #2
require!(
settle_bank.token_index == settle_token_index,
MangoError::InvalidBank
);
// Get oracle price for market. Price is validated inside
let oracle_price = perp_market.oracle_price(
&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?,
None, // checked in health
)?;
// Fetch perp positions for accounts, creating for the liqor if needed
let liqee_perp_position = liqee.perp_position_mut(perp_market_index)?;
require!(
!liqee_perp_position.has_open_taker_fills(),
MangoError::HasOpenPerpTakerFills
);
let liqor_perp_position = liqor
.ensure_perp_position(perp_market_index, perp_market.settle_token_index)?
.0;
// Settle funding, update limit
liqee_perp_position.settle_funding(&perp_market);
liqor_perp_position.settle_funding(&perp_market);
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
liqee_perp_position.update_settle_limit(&perp_market, now_ts);
//
// Perform the liquidation
//
let (base_transfer, quote_transfer, pnl_transfer, pnl_settle_limit_transfer) =
liquidation_action(
&mut perp_market,
&mut settle_bank,
&mut liqor.borrow_mut(),
&mut liqee.borrow_mut(),
&mut liqee_health_cache,
liqee_liq_end_health,
now_ts,
max_base_transfer,
max_pnl_transfer,
)?;
//
// Wrap up
//
let liqee_perp_position = liqee.perp_position_mut(perp_market_index)?;
let liqor_perp_position = liqor.perp_position_mut(perp_market_index)?;
emit_perp_balances(
ctx.accounts.group.key(),
ctx.accounts.liqor.key(),
liqor_perp_position,
&perp_market,
);
emit_perp_balances(
ctx.accounts.group.key(),
ctx.accounts.liqee.key(),
liqee_perp_position,
&perp_market,
);
if pnl_transfer != 0 {
let liqee_token_position = liqee.token_position(settle_token_index)?;
let liqor_token_position = liqor.token_position(settle_token_index)?;
emit!(TokenBalanceLog {
mango_group: ctx.accounts.group.key(),
mango_account: ctx.accounts.liqee.key(),
token_index: settle_token_index,
indexed_position: liqee_token_position.indexed_position.to_bits(),
deposit_index: settle_bank.deposit_index.to_bits(),
borrow_index: settle_bank.borrow_index.to_bits(),
});
emit!(TokenBalanceLog {
mango_group: ctx.accounts.group.key(),
mango_account: ctx.accounts.liqor.key(),
token_index: settle_token_index,
indexed_position: liqor_token_position.indexed_position.to_bits(),
deposit_index: settle_bank.deposit_index.to_bits(),
borrow_index: settle_bank.borrow_index.to_bits(),
});
}
if base_transfer != 0 || pnl_transfer != 0 {
emit!(PerpLiqBaseOrPositivePnlLog {
mango_group: ctx.accounts.group.key(),
perp_market_index: perp_market.perp_market_index,
liqor: ctx.accounts.liqor.key(),
liqee: ctx.accounts.liqee.key(),
base_transfer,
quote_transfer: quote_transfer.to_bits(),
pnl_transfer: pnl_transfer.to_bits(),
pnl_settle_limit_transfer: pnl_settle_limit_transfer.to_bits(),
price: oracle_price.to_bits(),
});
}
// Check liqee health again
let liqee_liq_end_health_after = liqee_health_cache.health(HealthType::LiquidationEnd);
liqee
.fixed
.maybe_recover_from_being_liquidated(liqee_liq_end_health_after);
require_gte!(liqee_liq_end_health_after, liqee_liq_end_health);
msg!(
"liqee liq end health: {} -> {}",
liqee_liq_end_health,
liqee_liq_end_health_after
);
drop(settle_bank);
drop(perp_market);
// Check liqor's health
if !liqor.fixed.is_in_health_region() {
let account_retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, group_pk)
.context("create account retriever end")?;
let liqor_health = compute_health(&liqor.borrow(), HealthType::Init, &account_retriever)
.context("compute liqor health")?;
require!(liqor_health >= 0, MangoError::HealthMustBePositive);
}
Ok(())
}
pub(crate) fn liquidation_action(
perp_market: &mut PerpMarket,
settle_bank: &mut Bank,
liqor: &mut MangoAccountRefMut,
liqee: &mut MangoAccountRefMut,
liqee_health_cache: &mut HealthCache,
liqee_liq_end_health: I80F48,
now_ts: u64,
max_base_transfer: i64,
max_pnl_transfer: u64,
) -> Result<(i64, I80F48, I80F48, I80F48)> {
let perp_market_index = perp_market.perp_market_index;
let settle_token_index = perp_market.settle_token_index;
let liqee_perp_position = liqee.perp_position_mut(perp_market_index)?;
let liqor_perp_position = liqor.perp_position_mut(perp_market_index)?;
let perp_info = liqee_health_cache.perp_info(perp_market_index)?;
let oracle_price = perp_info.prices.oracle;
let base_lot_size = I80F48::from(perp_market.base_lot_size);
let oracle_price_per_lot = cm!(base_lot_size * oracle_price);
let liqee_positive_settle_limit = liqee_perp_position.settle_limit(&perp_market).1;
// The max settleable amount does not need to be constrained by the liqor's perp settle health,
// because taking over perp quote decreases liqor health: every unit of quote taken costs
// (1-positive_pnl_liq_fee) USDC and only gains init_overall_asset_weight in perp health.
let max_pnl_transfer = I80F48::from(max_pnl_transfer);
// Take over the liqee's base in exchange for quote
let liqee_base_lots = liqee_perp_position.base_position_lots();
// Each lot the base position gets closer to 0, the unweighted perp health contribution
// increases by this amount.
let unweighted_health_per_lot;
// -1 (liqee base lots decrease) or +1 (liqee base lots increase)
let direction: i64;
// Either 1+fee or 1-fee, depending on direction.
let fee_factor;
if liqee_base_lots > 0 {
require_msg!(
max_base_transfer >= 0,
"max_base_transfer can't be negative when liqee's base_position is positive"
);
// the unweighted perp health contribution gets reduced by `base * price * perp_init_asset_weight`
// and increased by `base * price * (1 - liq_fee) * quote_init_asset_weight`
let quote_init_asset_weight = I80F48::ONE;
direction = -1;
fee_factor = cm!(I80F48::ONE - perp_market.base_liquidation_fee);
let asset_price = perp_info.prices.asset(HealthType::LiquidationEnd);
unweighted_health_per_lot = cm!(-asset_price
* base_lot_size
* perp_market.init_base_asset_weight
+ oracle_price_per_lot * quote_init_asset_weight * fee_factor);
} else {
// liqee_base_lots <= 0
require_msg!(
max_base_transfer <= 0,
"max_base_transfer can't be positive when liqee's base_position is negative"
);
// health gets increased by `base * price * perp_init_liab_weight`
// and reduced by `base * price * (1 + liq_fee) * quote_init_liab_weight`
let quote_init_liab_weight = I80F48::ONE;
direction = 1;
fee_factor = cm!(I80F48::ONE + perp_market.base_liquidation_fee);
let liab_price = perp_info.prices.liab(HealthType::LiquidationEnd);
unweighted_health_per_lot = cm!(liab_price
* base_lot_size
* perp_market.init_base_liab_weight
- oracle_price_per_lot * quote_init_liab_weight * fee_factor);
};
assert!(unweighted_health_per_lot > 0);
// Amount of settle token received for each token that is settled
let spot_gain_per_settled = cm!(I80F48::ONE - perp_market.positive_pnl_liquidation_fee);
let init_overall_asset_weight = perp_market.init_overall_asset_weight;
// The overall health contribution from perp including spot health increases from settling pnl.
// This is needed in order to reduce the base position the right amount when taking into
// account the settlement that will happen afterwards.
let expected_perp_health = |unweighted: I80F48| {
if unweighted < 0 {
unweighted
} else if unweighted < max_pnl_transfer {
cm!(unweighted * spot_gain_per_settled)
} else {
let unsettled = cm!(unweighted - max_pnl_transfer);
cm!(max_pnl_transfer * spot_gain_per_settled + unsettled * init_overall_asset_weight)
}
};
//
// Several steps of perp base position reduction will follow, and they'll update
// these variables
//
let mut base_reduction = 0;
let mut current_unweighted_perp_health =
perp_info.unweighted_health_contribution(HealthType::LiquidationEnd);
let initial_weighted_perp_health = perp_info
.weigh_health_contribution(current_unweighted_perp_health, HealthType::LiquidationEnd);
let mut current_expected_perp_health = expected_perp_health(current_unweighted_perp_health);
let mut current_expected_health =
cm!(liqee_liq_end_health + current_expected_perp_health - initial_weighted_perp_health);
let mut reduce_base = |step: &str,
health_amount: I80F48,
health_per_lot: I80F48,
current_unweighted_perp_health: &mut I80F48| {
// How much are we willing to increase the unweighted perp health?
let health_limit = health_amount
.min(-current_expected_health)
.max(I80F48::ZERO);
// How many lots to transfer?
let base_lots = cm!(health_limit / health_per_lot)
.checked_ceil() // overshoot to aim for init_health >= 0
.unwrap()
.checked_to_num::<i64>()
.unwrap()
.min(liqee_base_lots.abs() - base_reduction)
.min(max_base_transfer.abs() - base_reduction)
.max(0);
let unweighted_change = cm!(I80F48::from(base_lots) * unweighted_health_per_lot);
let current_unweighted = *current_unweighted_perp_health;
let new_unweighted_perp = cm!(current_unweighted + unweighted_change);
let new_expected_perp = expected_perp_health(new_unweighted_perp);
let new_expected_health =
cm!(current_expected_health + (new_expected_perp - current_expected_perp_health));
msg!(
"{}: {} lots, health {} -> {}, unweighted perp {} -> {}",
step,
base_lots,
current_expected_health,
new_expected_health,
current_unweighted,
new_unweighted_perp
);
base_reduction += base_lots;
current_expected_health = new_expected_health;
*current_unweighted_perp_health = new_unweighted_perp;
current_expected_perp_health = new_expected_perp;
};
//
// Step 1: While the perp unsettled health is negative, any perp base position reduction
// directly increases it for the full amount.
//
if current_unweighted_perp_health < 0 {
reduce_base(
"negative",
-current_unweighted_perp_health,
unweighted_health_per_lot,
&mut current_unweighted_perp_health,
);
}
//
// Step 2: If perp unsettled health is positive but below max_settle, perp base position reductions
// benefit account health slightly less because of the settlement liquidation fee.
//
if current_unweighted_perp_health >= 0 && current_unweighted_perp_health < max_pnl_transfer {
let settled_health_per_lot = cm!(unweighted_health_per_lot * spot_gain_per_settled);
reduce_base(
"settleable",
cm!(max_pnl_transfer - current_unweighted_perp_health),
settled_health_per_lot,
&mut current_unweighted_perp_health,
);
}
//
// Step 3: Above that, perp base positions only benefit account health if the pnl asset weight is positive
//
if current_unweighted_perp_health >= max_pnl_transfer && init_overall_asset_weight > 0 {
let weighted_health_per_lot = cm!(unweighted_health_per_lot * init_overall_asset_weight);
reduce_base(
"positive",
I80F48::MAX,
weighted_health_per_lot,
&mut current_unweighted_perp_health,
);
}
//
// Execute the base reduction. This is essentially a forced trade and updates the
// liqee and liqors entry and break even prices.
//
let base_transfer = cm!(direction * base_reduction);
let quote_transfer = cm!(-I80F48::from(base_transfer) * oracle_price_per_lot * fee_factor);
if base_transfer != 0 {
msg!(
"transfering: {} base lots and {} quote",
base_transfer,
quote_transfer
);
liqee_perp_position.record_trade(perp_market, base_transfer, quote_transfer);
liqor_perp_position.record_trade(perp_market, -base_transfer, -quote_transfer);
}
//
// Step 4: Let the liqor take over positive pnl until the account health is positive,
// but only while the unweighted perp health is positive (otherwise it would decrease liqee health!)
//
let final_weighted_perp_health = perp_info
.weigh_health_contribution(current_unweighted_perp_health, HealthType::LiquidationEnd);
let current_actual_health =
cm!(liqee_liq_end_health - initial_weighted_perp_health + final_weighted_perp_health);
let pnl_transfer_possible =
current_actual_health < 0 && current_unweighted_perp_health > 0 && max_pnl_transfer > 0;
let (pnl_transfer, limit_transfer) = if pnl_transfer_possible {
let health_per_transfer = cm!(spot_gain_per_settled - init_overall_asset_weight);
let transfer_for_zero = cm!(-current_actual_health / health_per_transfer)
.checked_ceil()
.unwrap();
let liqee_pnl = liqee_perp_position.unsettled_pnl(&perp_market, oracle_price)?;
// Allow taking over *more* than the liqee_positive_settle_limit. In exchange, the liqor
// also can't settle fully immediately and just takes over a fractional chunk of the limit.
//
// If this takeover were limited by the settle limit, then we couldn't always bring the liqee
// base position to zero and would need to deal with that in bankruptcy. Also, the settle
// limit changes with the base position price, so it'd be hard to say when this liquidation
// step is done.
let pnl_transfer = liqee_pnl
.min(max_pnl_transfer)
.min(transfer_for_zero)
.min(current_unweighted_perp_health)
.max(I80F48::ZERO);
let limit_transfer = {
// take care, liqee_limit may be i64::MAX
let liqee_limit: i128 = liqee_positive_settle_limit.into();
let settle = pnl_transfer.checked_floor().unwrap().to_num::<i128>();
let total = liqee_pnl.checked_ceil().unwrap().to_num::<i128>();
let liqor_limit: i64 = cm!(liqee_limit * settle / total).try_into().unwrap();
I80F48::from(liqor_limit).min(pnl_transfer).max(I80F48::ONE)
};
// The liqor pays less than the full amount to receive the positive pnl
let token_transfer = cm!(pnl_transfer * spot_gain_per_settled);
if pnl_transfer > 0 {
liqor_perp_position.record_liquidation_pnl_takeover(pnl_transfer, limit_transfer);
liqee_perp_position.record_settle(pnl_transfer);
// Update the accounts' perp_spot_transfer statistics.
let transfer_i64 = token_transfer
.round_to_zero()
.checked_to_num::<i64>()
.unwrap();
cm!(liqor_perp_position.perp_spot_transfers -= transfer_i64);
cm!(liqee_perp_position.perp_spot_transfers += transfer_i64);
cm!(liqor.fixed.perp_spot_transfers -= transfer_i64);
cm!(liqee.fixed.perp_spot_transfers += transfer_i64);
// Transfer token balance
let liqor_token_position = liqor.token_position_mut(settle_token_index)?.0;
let liqee_token_position = liqee.token_position_mut(settle_token_index)?.0;
settle_bank.deposit(liqee_token_position, token_transfer, now_ts)?;
settle_bank.withdraw_without_fee(
liqor_token_position,
token_transfer,
now_ts,
oracle_price,
)?;
liqee_health_cache.adjust_token_balance(&settle_bank, token_transfer)?;
}
msg!(
"pnl {} was transferred to liqor for quote {} with settle limit {}",
pnl_transfer,
token_transfer,
limit_transfer
);
(pnl_transfer, limit_transfer)
} else {
(I80F48::ZERO, I80F48::ZERO)
};
let liqee_perp_position = liqee.perp_position_mut(perp_market_index)?;
liqee_health_cache.recompute_perp_info(liqee_perp_position, &perp_market)?;
Ok((base_transfer, quote_transfer, pnl_transfer, limit_transfer))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::health::{self, test::*};
#[derive(Clone)]
struct TestSetup {
group: Pubkey,
settle_bank: TestAccount<Bank>,
settle_oracle: TestAccount<StubOracle>,
perp_market: TestAccount<PerpMarket>,
perp_oracle: TestAccount<StubOracle>,
liqee: MangoAccountValue,
liqor: MangoAccountValue,
}
impl TestSetup {
fn new() -> Self {
let group = Pubkey::new_unique();
let (settle_bank, settle_oracle) = mock_bank_and_oracle(group, 0, 1.0, 0.0, 0.0);
let (_bank2, perp_oracle) = mock_bank_and_oracle(group, 4, 1.0, 0.5, 0.3);
let mut perp_market =
mock_perp_market(group, perp_oracle.pubkey, 1.0, 9, (0.2, 0.1), (0.05, 0.02));
perp_market.data().base_lot_size = 1;
let liqee_buffer = MangoAccount::default_for_tests().try_to_vec().unwrap();
let mut liqee = MangoAccountValue::from_bytes(&liqee_buffer).unwrap();
{
liqee.ensure_token_position(0).unwrap();
liqee.ensure_perp_position(9, 0).unwrap();
}
let liqor_buffer = MangoAccount::default_for_tests().try_to_vec().unwrap();
let mut liqor = MangoAccountValue::from_bytes(&liqor_buffer).unwrap();
{
liqor.ensure_token_position(0).unwrap();
liqor.ensure_perp_position(9, 0).unwrap();
}
Self {
group,
settle_bank,
settle_oracle,
perp_market,
perp_oracle,
liqee,
liqor,
}
}
fn liqee_health_cache(&self) -> HealthCache {
let mut setup = self.clone();
let ais = vec![
setup.settle_bank.as_account_info(),
setup.settle_oracle.as_account_info(),
setup.perp_market.as_account_info(),
setup.perp_oracle.as_account_info(),
];
let retriever =
ScanningAccountRetriever::new_with_staleness(&ais, &setup.group, None).unwrap();
health::new_health_cache(&setup.liqee.borrow(), &retriever).unwrap()
}
fn run(&self, max_base: i64, max_pnl: u64) -> Result<Self> {
let mut setup = self.clone();
let mut liqee_health_cache = setup.liqee_health_cache();
let liqee_liq_end_health = liqee_health_cache.health(HealthType::LiquidationEnd);
liquidation_action(
setup.perp_market.data(),
setup.settle_bank.data(),
&mut setup.liqor.borrow_mut(),
&mut setup.liqee.borrow_mut(),
&mut liqee_health_cache,
liqee_liq_end_health,
0,
max_base,
max_pnl,
)?;
Ok(setup)
}
}
fn token_p(account: &mut MangoAccountValue) -> &mut TokenPosition {
account.token_position_mut(0).unwrap().0
}
fn perp_p(account: &mut MangoAccountValue) -> &mut PerpPosition {
account.perp_position_mut(9).unwrap()
}
macro_rules! assert_eq_f {
($value:expr, $expected:expr, $max_error:expr) => {
let value = $value;
let expected = $expected;
let ok = (value.to_num::<f64>() - expected).abs() < $max_error;
assert!(ok, "value: {value}, expected: {expected}");
};
}
#[test]
fn test_liq_base_or_positive_pnl() {
let test_cases = vec![
(
"nothing",
(0.9, 0.9),
(0.0, 0, 0.0),
(0.0, 0, 0.0),
(0, 100),
),
//
// liquidate base position when perp health is negative
//
(
"neg base liq 1: limited",
(0.5, 0.5),
(5.0, -10, 0.0),
(5.0, -9, -1.0),
(-1, 100),
),
(
"neg base liq 2: base to zero",
(0.5, 0.5),
(5.0, -10, 0.0),
(5.0, 0, -10.0),
(-20, 100),
),
(
"neg base liq 3: health positive",
(0.5, 0.5),
(5.0, -4, 0.0),
(5.0, -2, -2.0),
(-20, 100),
),
(
"pos base liq 1: limited",
(0.5, 0.5),
(5.0, 20, -20.0),
(5.0, 19, -19.0),
(1, 100),
),
(
"pos base liq 2: base to zero",
(0.5, 0.5),
(0.0, 20, -30.0),
(0.0, 0, -10.0),
(100, 100),
),
(
"pos base liq 3: health positive",
(0.5, 0.5),
(5.0, 20, -20.0),
(5.0, 10, -10.0),
(100, 100),
),
//
// liquidate base position when perp health is positive and overall asset weight is positive
//
(
"base liq, pos perp health 1: until health positive",
(0.5, 1.0),
(-20.0, 20, 5.0),
(-20.0, 10, 15.0),
(100, 100),
),
(
"base liq, pos perp health 2-1: settle until health positive",
(0.5, 0.5),
(-19.0, 20, 10.0),
(-1.0, 20, -8.0),
(100, 100),
),
(
"base liq, pos perp health 2-2: base+settle until health positive",
(0.5, 0.5),
(-25.0, 20, 10.0),
(0.0, 10, -5.0),
(100, 100),
),
(
"base liq, pos perp health 2-3: base+settle until pnl limit",
(0.5, 0.5),
(-23.0, 20, 10.0),
(-2.0, 10, -1.0),
(100, 21),
),
(
"base liq, pos perp health 2-4: base+settle until base limit",
(0.5, 0.5),
(-25.0, 20, 10.0),
(-4.0, 18, -9.0),
(2, 100),
),
(
"base liq, pos perp health 2-5: base+settle until both limits",
(0.5, 0.5),
(-25.0, 20, 10.0),
(-4.0, 16, -7.0),
(4, 21),
),
(
"base liq, pos perp health 4: liq some base, then settle some",
(0.5, 0.5),
(-20.0, 20, 10.0),
(-15.0, 10, 15.0),
(10, 5),
),
(
"base liq, pos perp health 5: base to zero even without settlement",
(0.5, 0.5),
(-20.0, 20, 10.0),
(-20.0, 0, 30.0),
(100, 0),
),
//
// liquidate base position when perp health is positive but overall asset weight is zero
//
(
"base liq, pos perp health 6: don't touch base without settlement",
(0.5, 0.0),
(-20.0, 20, 10.0),
(-20.0, 20, 10.0),
(10, 0),
),
(
"base liq, pos perp health 7: settlement without base",
(0.5, 0.0),
(-20.0, 20, 10.0),
(-15.0, 20, 5.0),
(10, 5),
),
(
"base liq, pos perp health 8: settlement enables base",
(0.5, 0.0),
(-30.0, 20, 10.0),
(-7.5, 15, -7.5),
(5, 30),
),
(
"base liq, pos perp health 9: until health positive",
(0.5, 0.0),
(-25.0, 20, 10.0),
(0.0, 10, -5.0),
(200, 200),
),
];
for (
name,
(base_weight, overall_weight),
(init_liqee_spot, init_liqee_base, init_liqee_quote),
(exp_liqee_spot, exp_liqee_base, exp_liqee_quote),
(max_base, max_pnl),
) in test_cases
{
println!("test: {name}");
let mut setup = TestSetup::new();
{
let pm = setup.perp_market.data();
pm.init_base_asset_weight = I80F48::from_num(base_weight);
pm.init_base_liab_weight = I80F48::from_num(2.0 - base_weight);
pm.init_overall_asset_weight = I80F48::from_num(overall_weight);
}
{
perp_p(&mut setup.liqee).record_trade(
setup.perp_market.data(),
init_liqee_base,
I80F48::from_num(init_liqee_quote),
);
let settle_bank = setup.settle_bank.data();
settle_bank
.change_without_fee(
token_p(&mut setup.liqee),
I80F48::from_num(init_liqee_spot),
0,
I80F48::from(1),
)
.unwrap();
settle_bank
.change_without_fee(
token_p(&mut setup.liqor),
I80F48::from_num(1000.0),
0,
I80F48::from(1),
)
.unwrap();
}
let mut result = setup.run(max_base, max_pnl).unwrap();
let liqee_perp = perp_p(&mut result.liqee);
assert_eq!(liqee_perp.base_position_lots(), exp_liqee_base);
assert_eq_f!(liqee_perp.quote_position_native(), exp_liqee_quote, 0.01);
let liqor_perp = perp_p(&mut result.liqor);
assert_eq!(
liqor_perp.base_position_lots(),
-(exp_liqee_base - init_liqee_base)
);
assert_eq_f!(
liqor_perp.quote_position_native(),
-(exp_liqee_quote - init_liqee_quote),
0.01
);
let settle_bank = result.settle_bank.data();
assert_eq_f!(
token_p(&mut result.liqee).native(settle_bank),
exp_liqee_spot,
0.01
);
assert_eq_f!(
token_p(&mut result.liqor).native(settle_bank),
1000.0 - (exp_liqee_spot - init_liqee_spot),
0.01
);
}
}
// Checks that the stable price does _not_ affect the liquidation target amount
#[test]
fn test_liq_base_or_positive_pnl_stable_price() {
let mut setup = TestSetup::new();
{
let pm = setup.perp_market.data();
pm.stable_price_model.stable_price = 0.5;
pm.init_base_asset_weight = I80F48::from_num(0.6);
pm.maint_base_asset_weight = I80F48::from_num(0.8);
}
{
perp_p(&mut setup.liqee).record_trade(
setup.perp_market.data(),
30,
I80F48::from_num(-30),
);
let settle_bank = setup.settle_bank.data();
settle_bank
.change_without_fee(
token_p(&mut setup.liqee),
I80F48::from_num(5.0),
0,
I80F48::from(1),
)
.unwrap();
settle_bank
.change_without_fee(
token_p(&mut setup.liqor),
I80F48::from_num(1000.0),
0,
I80F48::from(1),
)
.unwrap();
}
let hc = setup.liqee_health_cache();
assert_eq_f!(
hc.health(HealthType::Init),
5.0 + (-30.0 + 30.0 * 0.5 * 0.6), // init + stable
0.1
);
assert_eq_f!(
hc.health(HealthType::LiquidationEnd),
5.0 + (-30.0 + 30.0 * 0.6), // init + oracle
0.1
);
assert_eq_f!(
hc.health(HealthType::Maint),
5.0 + (-30.0 + 30.0 * 0.8), // maint + oracle
0.1
);
let mut result = setup.run(100, 0).unwrap();
let liqee_perp = perp_p(&mut result.liqee);
assert_eq!(liqee_perp.base_position_lots(), 12);
let hc = result.liqee_health_cache();
assert_eq_f!(
hc.health(HealthType::LiquidationEnd),
5.0 + (-12.0 + 12.0 * 0.6),
0.1
);
}
}