Fix perp liq computation; doc liq fee in token liq
This commit is contained in:
parent
524fe110e3
commit
9757f7a509
|
@ -106,66 +106,63 @@ pub fn perp_liq_base_position(
|
||||||
|
|
||||||
// Take over the liqee's base in exchange for quote
|
// Take over the liqee's base in exchange for quote
|
||||||
require_msg!(liqee_base_lots != 0, "liqee base position is zero");
|
require_msg!(liqee_base_lots != 0, "liqee base position is zero");
|
||||||
let (base_transfer, quote_transfer) = if liqee_base_lots > 0 {
|
let (base_transfer, quote_transfer) =
|
||||||
require_msg!(
|
if liqee_base_lots > 0 {
|
||||||
max_base_transfer > 0,
|
require_msg!(
|
||||||
"max_base_transfer must be positive when liqee's base_position is positive"
|
max_base_transfer > 0,
|
||||||
);
|
"max_base_transfer must be positive when liqee's base_position is positive"
|
||||||
|
);
|
||||||
|
|
||||||
// health gets reduced by `base * price * perp_init_asset_weight`
|
// health gets reduced by `base * price * perp_init_asset_weight`
|
||||||
// and increased by `base * price * (1 - liq_fee) * quote_init_asset_weight`
|
// and increased by `base * price * (1 - liq_fee) * quote_init_asset_weight`
|
||||||
let quote_asset_weight = I80F48::ONE;
|
let quote_init_asset_weight = I80F48::ONE;
|
||||||
let health_per_lot = cm!(price_per_lot
|
let fee_factor = cm!(I80F48::ONE - perp_market.liquidation_fee);
|
||||||
* (quote_asset_weight * (I80F48::ONE - perp_market.liquidation_fee)
|
let health_per_lot = cm!(price_per_lot
|
||||||
- perp_market.init_asset_weight));
|
* (-perp_market.init_asset_weight + quote_init_asset_weight * fee_factor));
|
||||||
|
|
||||||
// number of lots to transfer to bring health to zero, rounded up
|
// number of lots to transfer to bring health to zero, rounded up
|
||||||
let base_transfer_for_zero: i64 = cm!(-liqee_init_health / health_per_lot)
|
let base_transfer_for_zero: i64 = cm!(-liqee_init_health / health_per_lot)
|
||||||
.checked_ceil()
|
.checked_ceil()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.checked_to_num()
|
.checked_to_num()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let base_transfer = base_transfer_for_zero
|
let base_transfer = base_transfer_for_zero
|
||||||
.min(liqee_base_lots)
|
.min(liqee_base_lots)
|
||||||
.min(max_base_transfer)
|
.min(max_base_transfer)
|
||||||
.max(0);
|
.max(0);
|
||||||
let quote_transfer = cm!(-I80F48::from(base_transfer)
|
let quote_transfer = cm!(-I80F48::from(base_transfer) * price_per_lot * fee_factor);
|
||||||
* price_per_lot
|
|
||||||
* (I80F48::ONE - perp_market.liquidation_fee));
|
|
||||||
|
|
||||||
(base_transfer, quote_transfer) // base > 0, quote < 0
|
(base_transfer, quote_transfer) // base > 0, quote < 0
|
||||||
} else {
|
} else {
|
||||||
// liqee_base_lots < 0
|
// liqee_base_lots < 0
|
||||||
require_msg!(
|
require_msg!(
|
||||||
max_base_transfer < 0,
|
max_base_transfer < 0,
|
||||||
"max_base_transfer must be negative when liqee's base_position is positive"
|
"max_base_transfer must be negative when liqee's base_position is positive"
|
||||||
);
|
);
|
||||||
|
|
||||||
// health gets increased by `base * price * perp_init_liab_weight`
|
// health gets increased by `base * price * perp_init_liab_weight`
|
||||||
// and reduced by `base * price * (1 + liq_fee) * quote_init_liab_weight`
|
// and reduced by `base * price * (1 + liq_fee) * quote_init_liab_weight`
|
||||||
let quote_liab_weight = I80F48::ONE;
|
let quote_init_liab_weight = I80F48::ONE;
|
||||||
let health_per_lot = cm!(price_per_lot
|
let fee_factor = cm!(I80F48::ONE + perp_market.liquidation_fee);
|
||||||
* (perp_market.init_liab_weight * (I80F48::ONE + perp_market.liquidation_fee)
|
let health_per_lot = cm!(price_per_lot
|
||||||
- quote_liab_weight));
|
* (perp_market.init_liab_weight - quote_init_liab_weight * fee_factor));
|
||||||
|
|
||||||
// (negative) number of lots to transfer to bring health to zero, rounded away from zero
|
// (negative) number of lots to transfer to bring health to zero, rounded away from zero
|
||||||
let base_transfer_for_zero: i64 = cm!(liqee_init_health / health_per_lot)
|
let base_transfer_for_zero: i64 = cm!(liqee_init_health / health_per_lot)
|
||||||
.checked_floor()
|
.checked_floor()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.checked_to_num()
|
.checked_to_num()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let base_transfer = base_transfer_for_zero
|
let base_transfer = base_transfer_for_zero
|
||||||
.max(liqee_base_lots)
|
.max(liqee_base_lots)
|
||||||
.max(max_base_transfer)
|
.max(max_base_transfer)
|
||||||
.min(0);
|
.min(0);
|
||||||
let quote_transfer = cm!(-I80F48::from(base_transfer)
|
let quote_transfer = cm!(-I80F48::from(base_transfer) * price_per_lot * fee_factor);
|
||||||
* price_per_lot
|
|
||||||
* (I80F48::ONE + perp_market.liquidation_fee));
|
|
||||||
|
|
||||||
(base_transfer, quote_transfer) // base < 0, quote > 0
|
(base_transfer, quote_transfer) // base < 0, quote > 0
|
||||||
};
|
};
|
||||||
|
|
||||||
// Execute the transfer. This is essentially a forced trade and updates the
|
// Execute the transfer. This is essentially a forced trade and updates the
|
||||||
// liqee and liqors entry and break even prices.
|
// liqee and liqors entry and break even prices.
|
||||||
|
|
|
@ -101,9 +101,18 @@ pub fn token_liq_with_token(
|
||||||
let liqee_liab_native = liqee_liab_position.native(liab_bank);
|
let liqee_liab_native = liqee_liab_position.native(liab_bank);
|
||||||
require!(liqee_liab_native.is_negative(), MangoError::SomeError);
|
require!(liqee_liab_native.is_negative(), MangoError::SomeError);
|
||||||
|
|
||||||
// TODO why sum of both tokens liquidation fees? Add comment
|
// Liquidation fees work by giving the liqor more assets than the oracle price would
|
||||||
let fee_factor = I80F48::ONE + asset_bank.liquidation_fee + liab_bank.liquidation_fee;
|
// indicate. Specifically we choose
|
||||||
let liab_price_adjusted = liab_price * fee_factor;
|
// assets =
|
||||||
|
// liabs * liab_price/asset_price * (1 + liab_liq_fee + asset_liq_fee)
|
||||||
|
// Which means that we use a increased liab price and reduced asset price for the conversion.
|
||||||
|
// It would be more fully correct to use (1+liab_liq_fee)*(1+asset_liq_fee), but for small
|
||||||
|
// fee amounts that is nearly identical.
|
||||||
|
// For simplicity we write
|
||||||
|
// assets = liabs * liab_price / asset_price * fee_factor
|
||||||
|
// assets = liabs * liab_price_adjusted / asset_price
|
||||||
|
let fee_factor = cm!(I80F48::ONE + asset_bank.liquidation_fee + liab_bank.liquidation_fee);
|
||||||
|
let liab_price_adjusted = cm!(liab_price * fee_factor);
|
||||||
|
|
||||||
let init_asset_weight = asset_bank.init_asset_weight;
|
let init_asset_weight = asset_bank.init_asset_weight;
|
||||||
let init_liab_weight = liab_bank.init_liab_weight;
|
let init_liab_weight = liab_bank.init_liab_weight;
|
||||||
|
|
|
@ -296,20 +296,6 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
send_tx(
|
|
||||||
solana,
|
|
||||||
TokenDepositInstruction {
|
|
||||||
amount: 1,
|
|
||||||
account,
|
|
||||||
owner,
|
|
||||||
token_account: payer_mint_accounts[1],
|
|
||||||
token_authority: payer,
|
|
||||||
bank_index: 0,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
account
|
account
|
||||||
};
|
};
|
||||||
let account_0 = make_account(0).await;
|
let account_0 = make_account(0).await;
|
||||||
|
@ -365,7 +351,7 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr
|
||||||
//
|
//
|
||||||
// SETUP: Change the oracle to make health go negative for account_0
|
// SETUP: Change the oracle to make health go negative for account_0
|
||||||
//
|
//
|
||||||
set_bank_stub_oracle_price(solana, group, base_token, admin, 0.5).await;
|
set_bank_stub_oracle_price(solana, group, base_token, admin, 0.6).await;
|
||||||
|
|
||||||
// verify health is bad: can't withdraw
|
// verify health is bad: can't withdraw
|
||||||
assert!(send_tx(
|
assert!(send_tx(
|
||||||
|
@ -375,7 +361,7 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr
|
||||||
allow_borrow: false,
|
allow_borrow: false,
|
||||||
account: account_0,
|
account: account_0,
|
||||||
owner,
|
owner,
|
||||||
token_account: payer_mint_accounts[1],
|
token_account: payer_mint_accounts[0],
|
||||||
bank_index: 0,
|
bank_index: 0,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -383,7 +369,7 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr
|
||||||
.is_err());
|
.is_err());
|
||||||
|
|
||||||
//
|
//
|
||||||
// TEST: Liquidate base position
|
// TEST: Liquidate base position with limit
|
||||||
//
|
//
|
||||||
send_tx(
|
send_tx(
|
||||||
solana,
|
solana,
|
||||||
|
@ -398,7 +384,7 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let liq_amount = 10.0 * 100.0 * 0.5 * (1.0 - 0.05);
|
let liq_amount = 10.0 * 100.0 * 0.6 * (1.0 - 0.05);
|
||||||
let liqor_data = solana.get_account::<MangoAccount>(liqor).await;
|
let liqor_data = solana.get_account::<MangoAccount>(liqor).await;
|
||||||
assert_eq!(liqor_data.perps[0].base_position_lots(), 10);
|
assert_eq!(liqor_data.perps[0].base_position_lots(), 10);
|
||||||
assert!(assert_equal(
|
assert!(assert_equal(
|
||||||
|
@ -414,10 +400,57 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr
|
||||||
0.1
|
0.1
|
||||||
));
|
));
|
||||||
|
|
||||||
|
//
|
||||||
|
// TEST: Liquidate base position max
|
||||||
|
//
|
||||||
|
send_tx(
|
||||||
|
solana,
|
||||||
|
PerpLiqBasePositionInstruction {
|
||||||
|
liqor,
|
||||||
|
liqor_owner: owner,
|
||||||
|
liqee: account_0,
|
||||||
|
perp_market,
|
||||||
|
max_base_transfer: i64::MAX,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let liq_amount_2 = 4.0 * 100.0 * 0.6 * (1.0 - 0.05);
|
||||||
|
let liqor_data = solana.get_account::<MangoAccount>(liqor).await;
|
||||||
|
assert_eq!(liqor_data.perps[0].base_position_lots(), 10 + 4);
|
||||||
|
assert!(assert_equal(
|
||||||
|
liqor_data.perps[0].quote_position_native(),
|
||||||
|
-liq_amount - liq_amount_2,
|
||||||
|
0.1
|
||||||
|
));
|
||||||
|
let liqee_data = solana.get_account::<MangoAccount>(account_0).await;
|
||||||
|
assert_eq!(liqee_data.perps[0].base_position_lots(), 6);
|
||||||
|
assert!(assert_equal(
|
||||||
|
liqee_data.perps[0].quote_position_native(),
|
||||||
|
-20.0 * 100.0 + liq_amount + liq_amount_2,
|
||||||
|
0.1
|
||||||
|
));
|
||||||
|
|
||||||
|
// verify health is good again
|
||||||
|
send_tx(
|
||||||
|
solana,
|
||||||
|
TokenWithdrawInstruction {
|
||||||
|
amount: 1,
|
||||||
|
allow_borrow: false,
|
||||||
|
account: account_0,
|
||||||
|
owner,
|
||||||
|
token_account: payer_mint_accounts[0],
|
||||||
|
bank_index: 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
//
|
//
|
||||||
// SETUP: Change the oracle to make health go negative for account_1
|
// SETUP: Change the oracle to make health go negative for account_1
|
||||||
//
|
//
|
||||||
set_bank_stub_oracle_price(solana, group, base_token, admin, 2.0).await;
|
set_bank_stub_oracle_price(solana, group, base_token, admin, 1.3).await;
|
||||||
|
|
||||||
// verify health is bad: can't withdraw
|
// verify health is bad: can't withdraw
|
||||||
assert!(send_tx(
|
assert!(send_tx(
|
||||||
|
@ -427,7 +460,7 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr
|
||||||
allow_borrow: false,
|
allow_borrow: false,
|
||||||
account: account_1,
|
account: account_1,
|
||||||
owner,
|
owner,
|
||||||
token_account: payer_mint_accounts[1],
|
token_account: payer_mint_accounts[0],
|
||||||
bank_index: 0,
|
bank_index: 0,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -437,6 +470,38 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr
|
||||||
//
|
//
|
||||||
// TEST: Liquidate base position
|
// TEST: Liquidate base position
|
||||||
//
|
//
|
||||||
|
send_tx(
|
||||||
|
solana,
|
||||||
|
PerpLiqBasePositionInstruction {
|
||||||
|
liqor,
|
||||||
|
liqor_owner: owner,
|
||||||
|
liqee: account_1,
|
||||||
|
perp_market,
|
||||||
|
max_base_transfer: -10,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let liq_amount_3 = 10.0 * 100.0 * 1.3 * (1.0 + 0.05);
|
||||||
|
let liqor_data = solana.get_account::<MangoAccount>(liqor).await;
|
||||||
|
assert_eq!(liqor_data.perps[0].base_position_lots(), 14 - 10);
|
||||||
|
assert!(assert_equal(
|
||||||
|
liqor_data.perps[0].quote_position_native(),
|
||||||
|
-liq_amount - liq_amount_2 + liq_amount_3,
|
||||||
|
0.1
|
||||||
|
));
|
||||||
|
let liqee_data = solana.get_account::<MangoAccount>(account_1).await;
|
||||||
|
assert_eq!(liqee_data.perps[0].base_position_lots(), -10);
|
||||||
|
assert!(assert_equal(
|
||||||
|
liqee_data.perps[0].quote_position_native(),
|
||||||
|
20.0 * 100.0 - liq_amount_3,
|
||||||
|
0.1
|
||||||
|
));
|
||||||
|
|
||||||
|
//
|
||||||
|
// TEST: Liquidate base position max
|
||||||
|
//
|
||||||
send_tx(
|
send_tx(
|
||||||
solana,
|
solana,
|
||||||
PerpLiqBasePositionInstruction {
|
PerpLiqBasePositionInstruction {
|
||||||
|
@ -450,19 +515,71 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let liq_amount_2 = 20.0 * 100.0 * 2.0 * (1.0 + 0.05);
|
let liq_amount_4 = 5.0 * 100.0 * 1.3 * (1.0 + 0.05);
|
||||||
let liqor_data = solana.get_account::<MangoAccount>(liqor).await;
|
let liqor_data = solana.get_account::<MangoAccount>(liqor).await;
|
||||||
assert_eq!(liqor_data.perps[0].base_position_lots(), 10 - 20);
|
assert_eq!(liqor_data.perps[0].base_position_lots(), 4 - 5);
|
||||||
assert!(assert_equal(
|
assert!(assert_equal(
|
||||||
liqor_data.perps[0].quote_position_native(),
|
liqor_data.perps[0].quote_position_native(),
|
||||||
-liq_amount + liq_amount_2,
|
-liq_amount - liq_amount_2 + liq_amount_3 + liq_amount_4,
|
||||||
|
0.1
|
||||||
|
));
|
||||||
|
let liqee_data = solana.get_account::<MangoAccount>(account_1).await;
|
||||||
|
assert_eq!(liqee_data.perps[0].base_position_lots(), -5);
|
||||||
|
assert!(assert_equal(
|
||||||
|
liqee_data.perps[0].quote_position_native(),
|
||||||
|
20.0 * 100.0 - liq_amount_3 - liq_amount_4,
|
||||||
|
0.1
|
||||||
|
));
|
||||||
|
|
||||||
|
// verify health is good again
|
||||||
|
send_tx(
|
||||||
|
solana,
|
||||||
|
TokenWithdrawInstruction {
|
||||||
|
amount: 1,
|
||||||
|
allow_borrow: false,
|
||||||
|
account: account_1,
|
||||||
|
owner,
|
||||||
|
token_account: payer_mint_accounts[0],
|
||||||
|
bank_index: 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
//
|
||||||
|
// SETUP: liquidate base position to 0, so bankruptcy can be tested
|
||||||
|
//
|
||||||
|
set_bank_stub_oracle_price(solana, group, base_token, admin, 2.0).await;
|
||||||
|
|
||||||
|
//
|
||||||
|
// TEST: Liquidate base position max
|
||||||
|
//
|
||||||
|
send_tx(
|
||||||
|
solana,
|
||||||
|
PerpLiqBasePositionInstruction {
|
||||||
|
liqor,
|
||||||
|
liqor_owner: owner,
|
||||||
|
liqee: account_1,
|
||||||
|
perp_market,
|
||||||
|
max_base_transfer: i64::MIN,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let liq_amount_5 = 5.0 * 100.0 * 2.0 * (1.0 + 0.05);
|
||||||
|
let liqor_data = solana.get_account::<MangoAccount>(liqor).await;
|
||||||
|
assert_eq!(liqor_data.perps[0].base_position_lots(), -1 - 5);
|
||||||
|
assert!(assert_equal(
|
||||||
|
liqor_data.perps[0].quote_position_native(),
|
||||||
|
-liq_amount - liq_amount_2 + liq_amount_3 + liq_amount_4 + liq_amount_5,
|
||||||
0.1
|
0.1
|
||||||
));
|
));
|
||||||
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);
|
||||||
assert!(assert_equal(
|
assert!(assert_equal(
|
||||||
liqee_data.perps[0].quote_position_native(),
|
liqee_data.perps[0].quote_position_native(),
|
||||||
20.0 * 100.0 - liq_amount_2,
|
20.0 * 100.0 - liq_amount_3 - liq_amount_4 - liq_amount_5,
|
||||||
0.1
|
0.1
|
||||||
));
|
));
|
||||||
|
|
||||||
|
@ -482,6 +599,24 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr
|
||||||
.await
|
.await
|
||||||
.is_err());
|
.is_err());
|
||||||
|
|
||||||
|
//
|
||||||
|
// SETUP: We want pnl settling to cause a negative quote position,
|
||||||
|
// thus we deposit some base token collateral
|
||||||
|
//
|
||||||
|
send_tx(
|
||||||
|
solana,
|
||||||
|
TokenDepositInstruction {
|
||||||
|
amount: 1,
|
||||||
|
account: account_1,
|
||||||
|
owner,
|
||||||
|
token_account: payer_mint_accounts[1],
|
||||||
|
token_authority: payer,
|
||||||
|
bank_index: 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
//
|
//
|
||||||
// TEST: Can settle-pnl even though health is negative
|
// TEST: Can settle-pnl even though health is negative
|
||||||
//
|
//
|
||||||
|
@ -499,8 +634,9 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let liqee_settle_health_before = 1000.0 + 1.0 * 2.0 * 0.8;
|
let liqee_settle_health_before = 999.0 + 1.0 * 2.0 * 0.8;
|
||||||
let remaining_pnl = 20.0 * 100.0 - liq_amount_2 + liqee_settle_health_before;
|
let remaining_pnl =
|
||||||
|
20.0 * 100.0 - liq_amount_3 - liq_amount_4 - liq_amount_5 + liqee_settle_health_before;
|
||||||
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);
|
||||||
|
|
Loading…
Reference in New Issue