Merge branch 'dev'
This commit is contained in:
commit
00c533d9d7
|
@ -108,11 +108,12 @@ pub fn maybe_liquidate_account(
|
|||
let quote_token_index = 0;
|
||||
|
||||
let account = account_fetcher.fetch_mango_account(pubkey)?;
|
||||
let maint_health = new_health_cache_(&mango_client.context, account_fetcher, &account)
|
||||
.expect("always ok")
|
||||
.health(HealthType::Maint);
|
||||
let health_cache =
|
||||
new_health_cache_(&mango_client.context, account_fetcher, &account).expect("always ok");
|
||||
let maint_health = health_cache.health(HealthType::Maint);
|
||||
let is_bankrupt = !health_cache.has_liquidatable_assets();
|
||||
|
||||
if maint_health >= 0 && !account.is_bankrupt() {
|
||||
if maint_health >= 0 && !is_bankrupt {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
|
@ -121,16 +122,17 @@ pub fn maybe_liquidate_account(
|
|||
pubkey,
|
||||
account.fixed.owner,
|
||||
maint_health,
|
||||
account.is_bankrupt(),
|
||||
is_bankrupt,
|
||||
);
|
||||
|
||||
// Fetch a fresh account and re-compute
|
||||
// This is -- unfortunately -- needed because the websocket streams seem to not
|
||||
// be great at providing timely updates to the account data.
|
||||
let account = account_fetcher.fetch_fresh_mango_account(pubkey)?;
|
||||
let maint_health = new_health_cache_(&mango_client.context, account_fetcher, &account)
|
||||
.expect("always ok")
|
||||
.health(HealthType::Maint);
|
||||
let health_cache =
|
||||
new_health_cache_(&mango_client.context, account_fetcher, &account).expect("always ok");
|
||||
let maint_health = health_cache.health(HealthType::Maint);
|
||||
let is_bankrupt = !health_cache.has_liquidatable_assets();
|
||||
|
||||
// find asset and liab tokens
|
||||
let mut tokens = account
|
||||
|
@ -172,7 +174,7 @@ pub fn maybe_liquidate_account(
|
|||
};
|
||||
|
||||
// try liquidating
|
||||
let txsig = if account.is_bankrupt() {
|
||||
let txsig = if is_bankrupt {
|
||||
if tokens.is_empty() {
|
||||
anyhow::bail!("mango account {}, is bankrupt has no active tokens", pubkey);
|
||||
}
|
||||
|
|
|
@ -33,7 +33,6 @@ pub fn account_close(ctx: Context<AccountClose>) -> Result<()> {
|
|||
// don't perform checks if group is just testing
|
||||
if group.testing == 0 {
|
||||
require!(!account.fixed.being_liquidated(), MangoError::SomeError);
|
||||
require!(!account.fixed.is_bankrupt(), MangoError::SomeError);
|
||||
require_eq!(account.fixed.delegate, Pubkey::default());
|
||||
for ele in account.token_iter() {
|
||||
require_eq!(ele.is_active(), false);
|
||||
|
|
|
@ -48,7 +48,6 @@ pub fn account_create(
|
|||
account.fixed.bump = *ctx.bumps.get("account").ok_or(MangoError::SomeError)?;
|
||||
account.fixed.delegate = Pubkey::default();
|
||||
account.fixed.set_being_liquidated(false);
|
||||
account.fixed.set_bankrupt(false);
|
||||
|
||||
account.expand_dynamic_content(token_count, serum3_count, perp_count, perp_oo_count)?;
|
||||
|
||||
|
|
|
@ -184,8 +184,6 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>(
|
|||
let mut account = ctx.accounts.account.load_mut()?;
|
||||
let group = account.fixed.group;
|
||||
|
||||
require!(!account.fixed.is_bankrupt(), MangoError::IsBankrupt);
|
||||
|
||||
// Find index at which vaults start
|
||||
let vaults_index = ctx
|
||||
.remaining_accounts
|
||||
|
|
|
@ -90,23 +90,27 @@ pub fn liq_token_bankruptcy(
|
|||
.is_owner_or_delegate(ctx.accounts.liqor_owner.key()),
|
||||
MangoError::SomeError
|
||||
);
|
||||
require!(!liqor.fixed.is_bankrupt(), MangoError::IsBankrupt);
|
||||
|
||||
let mut liqee = ctx.accounts.liqee.load_mut()?;
|
||||
require!(liqee.fixed.is_bankrupt(), MangoError::IsBankrupt);
|
||||
|
||||
let liab_bank = bank_ais[0].load::<Bank>()?;
|
||||
let liab_deposit_index = liab_bank.deposit_index;
|
||||
let (liqee_liab, liqee_raw_token_index) = liqee.token_get_mut(liab_token_index)?;
|
||||
let mut remaining_liab_loss = -liqee_liab.native(&liab_bank);
|
||||
require_gt!(remaining_liab_loss, I80F48::ZERO);
|
||||
drop(liab_bank);
|
||||
|
||||
let mut account_retriever = ScanningAccountRetriever::new(health_ais, group_pk)?;
|
||||
|
||||
// find insurance transfer amount
|
||||
let mut liqee = ctx.accounts.liqee.load_mut()?;
|
||||
let mut liqee_health_cache = new_health_cache(&liqee.borrow(), &account_retriever)
|
||||
.context("create liqee health cache")?;
|
||||
require!(
|
||||
!liqee_health_cache.has_liquidatable_assets(),
|
||||
MangoError::IsNotBankrupt
|
||||
);
|
||||
liqee.fixed.set_being_liquidated(true);
|
||||
|
||||
let (liab_bank, liab_price, opt_quote_bank_and_price) =
|
||||
account_retriever.banks_mut_and_oracles(liab_token_index, QUOTE_TOKEN_INDEX)?;
|
||||
let liab_deposit_index = liab_bank.deposit_index;
|
||||
let (liqee_liab, liqee_raw_token_index) = liqee.token_get_mut(liab_token_index)?;
|
||||
let initial_liab_native = liqee_liab.native(&liab_bank);
|
||||
let mut remaining_liab_loss = -initial_liab_native;
|
||||
require_gt!(remaining_liab_loss, I80F48::ZERO);
|
||||
|
||||
// find insurance transfer amount
|
||||
let liab_fee_factor = if liab_token_index == QUOTE_TOKEN_INDEX {
|
||||
I80F48::ONE
|
||||
} else {
|
||||
|
@ -226,14 +230,16 @@ pub fn liq_token_bankruptcy(
|
|||
liqee_liab_active = false;
|
||||
}
|
||||
|
||||
// If the account has no more borrows then it's no longer bankrupt
|
||||
// and should (always?) no longer be liquidated.
|
||||
let account_retriever = ScanningAccountRetriever::new(health_ais, group_pk)?;
|
||||
let liqee_health_cache = new_health_cache(&liqee.borrow(), &account_retriever)?;
|
||||
liqee.fixed.set_bankrupt(liqee_health_cache.has_borrows());
|
||||
if !liqee.is_bankrupt() && liqee_health_cache.health(HealthType::Init) >= 0 {
|
||||
liqee.fixed.set_being_liquidated(false);
|
||||
}
|
||||
let liab_bank = bank_ais[0].load::<Bank>()?;
|
||||
let end_liab_native = liqee_liab.native(&liab_bank);
|
||||
liqee_health_cache
|
||||
.adjust_token_balance(liab_token_index, cm!(end_liab_native - initial_liab_native))?;
|
||||
|
||||
// Check liqee health again
|
||||
let liqee_init_health = liqee_health_cache.health(HealthType::Init);
|
||||
liqee
|
||||
.fixed
|
||||
.maybe_recover_from_being_liquidated(liqee_init_health);
|
||||
|
||||
if !liqee_liab_active {
|
||||
liqee.token_deactivate(liqee_raw_token_index);
|
||||
|
|
|
@ -47,10 +47,8 @@ pub fn liq_token_with_token(
|
|||
.is_owner_or_delegate(ctx.accounts.liqor_owner.key()),
|
||||
MangoError::SomeError
|
||||
);
|
||||
require!(!liqor.fixed.is_bankrupt(), MangoError::IsBankrupt);
|
||||
|
||||
let mut liqee = ctx.accounts.liqee.load_mut()?;
|
||||
require!(!liqee.fixed.is_bankrupt(), MangoError::IsBankrupt);
|
||||
|
||||
// Initial liqee health check
|
||||
let mut liqee_health_cache = new_health_cache(&liqee.borrow(), &account_retriever)
|
||||
|
@ -236,18 +234,10 @@ pub fn liq_token_with_token(
|
|||
}
|
||||
|
||||
// Check liqee health again
|
||||
let maint_health = liqee_health_cache.health(HealthType::Maint);
|
||||
if maint_health < I80F48::ZERO {
|
||||
liqee
|
||||
.fixed
|
||||
.set_bankrupt(!liqee_health_cache.has_liquidatable_assets());
|
||||
} else {
|
||||
let init_health = liqee_health_cache.health(HealthType::Init);
|
||||
|
||||
// this is equivalent to one native USDC or 1e-6 USDC
|
||||
// This is used as threshold to flip flag instead of 0 because of dust issues
|
||||
liqee.fixed.set_being_liquidated(init_health < -I80F48::ONE);
|
||||
}
|
||||
let liqee_init_health = liqee_health_cache.health(HealthType::Init);
|
||||
liqee
|
||||
.fixed
|
||||
.maybe_recover_from_being_liquidated(liqee_init_health);
|
||||
|
||||
// Check liqor's health
|
||||
let liqor_health = compute_health(&liqor.borrow(), HealthType::Init, &account_retriever)
|
||||
|
|
|
@ -31,8 +31,6 @@ pub fn perp_cancel_all_orders(ctx: Context<PerpCancelAllOrders>, limit: u8) -> R
|
|||
MangoError::SomeError
|
||||
);
|
||||
|
||||
require!(!account.fixed.is_bankrupt(), MangoError::IsBankrupt);
|
||||
|
||||
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
|
||||
let bids = ctx.accounts.bids.load_mut()?;
|
||||
let asks = ctx.accounts.asks.load_mut()?;
|
||||
|
|
|
@ -36,8 +36,6 @@ pub fn perp_cancel_all_orders_by_side(
|
|||
MangoError::SomeError
|
||||
);
|
||||
|
||||
require!(!account.fixed.is_bankrupt(), MangoError::IsBankrupt);
|
||||
|
||||
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
|
||||
let bids = ctx.accounts.bids.load_mut()?;
|
||||
let asks = ctx.accounts.asks.load_mut()?;
|
||||
|
|
|
@ -31,8 +31,6 @@ pub fn perp_cancel_order(ctx: Context<PerpCancelOrder>, order_id: i128) -> Resul
|
|||
MangoError::SomeError
|
||||
);
|
||||
|
||||
require!(!account.fixed.is_bankrupt(), MangoError::IsBankrupt);
|
||||
|
||||
let perp_market = ctx.accounts.perp_market.load_mut()?;
|
||||
let bids = ctx.accounts.bids.load_mut()?;
|
||||
let asks = ctx.accounts.asks.load_mut()?;
|
||||
|
|
|
@ -34,8 +34,6 @@ pub fn perp_cancel_order_by_client_order_id(
|
|||
MangoError::SomeError
|
||||
);
|
||||
|
||||
require!(!account.fixed.is_bankrupt(), MangoError::IsBankrupt);
|
||||
|
||||
let perp_market = ctx.accounts.perp_market.load_mut()?;
|
||||
let bids = ctx.accounts.bids.load_mut()?;
|
||||
let asks = ctx.accounts.asks.load_mut()?;
|
||||
|
|
|
@ -81,7 +81,6 @@ pub fn perp_place_order(
|
|||
MangoError::SomeError
|
||||
);
|
||||
|
||||
require!(!account.fixed.is_bankrupt(), MangoError::IsBankrupt);
|
||||
let account_pk = ctx.accounts.account.key();
|
||||
|
||||
{
|
||||
|
|
|
@ -51,8 +51,6 @@ pub fn serum3_cancel_all_orders(ctx: Context<Serum3CancelAllOrders>, limit: u8)
|
|||
MangoError::SomeError
|
||||
);
|
||||
|
||||
require!(!account.fixed.is_bankrupt(), MangoError::IsBankrupt);
|
||||
|
||||
let serum_market = ctx.accounts.serum_market.load()?;
|
||||
|
||||
// Validate open_orders
|
||||
|
|
|
@ -64,8 +64,6 @@ pub fn serum3_cancel_order(
|
|||
MangoError::SomeError
|
||||
);
|
||||
|
||||
require!(!account.fixed.is_bankrupt(), MangoError::IsBankrupt);
|
||||
|
||||
// Validate open_orders
|
||||
require!(
|
||||
account
|
||||
|
|
|
@ -41,8 +41,6 @@ pub fn serum3_close_open_orders(ctx: Context<Serum3CloseOpenOrders>) -> Result<(
|
|||
MangoError::SomeError
|
||||
);
|
||||
|
||||
require!(!account.fixed.is_bankrupt(), MangoError::IsBankrupt);
|
||||
|
||||
let serum_market = ctx.accounts.serum_market.load()?;
|
||||
|
||||
// Validate open_orders
|
||||
|
|
|
@ -53,8 +53,6 @@ pub fn serum3_create_open_orders(ctx: Context<Serum3CreateOpenOrders>) -> Result
|
|||
MangoError::SomeError
|
||||
);
|
||||
|
||||
require!(!account.fixed.is_bankrupt(), MangoError::IsBankrupt);
|
||||
|
||||
let serum_account = account.serum3_create(serum_market.market_index)?;
|
||||
serum_account.open_orders = ctx.accounts.open_orders.key();
|
||||
serum_account.base_token_index = serum_market.base_token_index;
|
||||
|
|
|
@ -70,7 +70,6 @@ pub fn serum3_liq_force_cancel_orders(
|
|||
//
|
||||
{
|
||||
let account = ctx.accounts.account.load()?;
|
||||
require!(!account.fixed.is_bankrupt(), MangoError::IsBankrupt);
|
||||
let serum_market = ctx.accounts.serum_market.load()?;
|
||||
|
||||
// Validate open_orders
|
||||
|
|
|
@ -168,7 +168,6 @@ pub fn serum3_place_order(
|
|||
account.fixed.is_owner_or_delegate(ctx.accounts.owner.key()),
|
||||
MangoError::SomeError
|
||||
);
|
||||
require!(!account.fixed.is_bankrupt(), MangoError::IsBankrupt);
|
||||
|
||||
// Validate open_orders
|
||||
require!(
|
||||
|
|
|
@ -78,8 +78,6 @@ pub fn serum3_settle_funds(ctx: Context<Serum3SettleFunds>) -> Result<()> {
|
|||
MangoError::SomeError
|
||||
);
|
||||
|
||||
require!(!account.fixed.is_bankrupt(), MangoError::IsBankrupt);
|
||||
|
||||
// Validate open_orders
|
||||
require!(
|
||||
account
|
||||
|
|
|
@ -58,7 +58,6 @@ pub fn token_deposit(ctx: Context<TokenDeposit>, amount: u64) -> Result<()> {
|
|||
|
||||
// Get the account's position for that token index
|
||||
let mut account = ctx.accounts.account.load_mut()?;
|
||||
require!(!account.fixed.is_bankrupt(), MangoError::IsBankrupt);
|
||||
|
||||
let (position, raw_token_index, active_token_index) =
|
||||
account.token_get_mut_or_create(token_index)?;
|
||||
|
@ -94,12 +93,11 @@ pub fn token_deposit(ctx: Context<TokenDeposit>, amount: u64) -> Result<()> {
|
|||
|
||||
//
|
||||
// Health computation
|
||||
// TODO: This will be used to disable is_bankrupt or being_liquidated
|
||||
// when health recovers sufficiently
|
||||
//
|
||||
let health = compute_health(&account.borrow(), HealthType::Init, &retriever)
|
||||
.context("post-deposit init health")?;
|
||||
msg!("health: {}", health);
|
||||
account.fixed.maybe_recover_from_being_liquidated(health);
|
||||
|
||||
//
|
||||
// Deactivate the position only after the health check because the user passed in
|
||||
|
|
|
@ -61,7 +61,6 @@ pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bo
|
|||
// Get the account's position for that token index
|
||||
let mut account = ctx.accounts.account.load_mut()?;
|
||||
|
||||
require!(!account.fixed.is_bankrupt(), MangoError::IsBankrupt);
|
||||
let (position, raw_token_index, active_token_index) =
|
||||
account.token_get_mut_or_create(token_index)?;
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ use crate::util::checked_math as cm;
|
|||
|
||||
use super::MangoAccountRef;
|
||||
|
||||
const BANKRUPTCY_DUST_THRESHOLD: I80F48 = I80F48!(0.000001);
|
||||
const ONE_NATIVE_USDC_IN_USD: I80F48 = I80F48!(0.000001);
|
||||
|
||||
/// This trait abstracts how to find accounts needed for the health computation.
|
||||
///
|
||||
|
@ -565,13 +565,14 @@ impl HealthCache {
|
|||
}
|
||||
|
||||
pub fn has_liquidatable_assets(&self) -> bool {
|
||||
let spot_liquidatable = self.token_infos.iter().any(|ti| {
|
||||
ti.balance > BANKRUPTCY_DUST_THRESHOLD || ti.serum3_max_reserved.is_positive()
|
||||
});
|
||||
let spot_liquidatable = self
|
||||
.token_infos
|
||||
.iter()
|
||||
.any(|ti| ti.balance.is_positive() || ti.serum3_max_reserved.is_positive());
|
||||
let perp_liquidatable = self
|
||||
.perp_infos
|
||||
.iter()
|
||||
.any(|p| p.base != 0 || p.quote > BANKRUPTCY_DUST_THRESHOLD);
|
||||
.any(|p| p.base != 0 || p.quote > ONE_NATIVE_USDC_IN_USD);
|
||||
spot_liquidatable || perp_liquidatable
|
||||
}
|
||||
|
||||
|
|
|
@ -54,8 +54,7 @@ pub struct MangoAccount {
|
|||
/// This account cannot open new positions or borrow until `init_health >= 0`
|
||||
being_liquidated: u8,
|
||||
|
||||
/// This account cannot do anything except go through `resolve_bankruptcy`
|
||||
is_bankrupt: u8,
|
||||
padding5: u8,
|
||||
|
||||
pub bump: u8,
|
||||
|
||||
|
@ -97,7 +96,7 @@ impl Default for MangoAccount {
|
|||
owner: Pubkey::default(),
|
||||
delegate: Pubkey::default(),
|
||||
being_liquidated: 0,
|
||||
is_bankrupt: 0,
|
||||
padding5: 0,
|
||||
account_num: 0,
|
||||
bump: 0,
|
||||
padding: Default::default(),
|
||||
|
@ -193,7 +192,7 @@ pub struct MangoAccountFixed {
|
|||
pub delegate: Pubkey,
|
||||
pub account_num: u32,
|
||||
being_liquidated: u8,
|
||||
is_bankrupt: u8,
|
||||
padding2: u8,
|
||||
pub bump: u8,
|
||||
pub padding: [u8; 1],
|
||||
pub net_deposits: f32,
|
||||
|
@ -214,14 +213,6 @@ impl MangoAccountFixed {
|
|||
self.owner == ix_signer || self.delegate == ix_signer
|
||||
}
|
||||
|
||||
pub fn is_bankrupt(&self) -> bool {
|
||||
self.is_bankrupt != 0
|
||||
}
|
||||
|
||||
pub fn set_bankrupt(&mut self, b: bool) {
|
||||
self.is_bankrupt = if b { 1 } else { 0 };
|
||||
}
|
||||
|
||||
pub fn being_liquidated(&self) -> bool {
|
||||
self.being_liquidated != 0
|
||||
}
|
||||
|
@ -229,6 +220,14 @@ impl MangoAccountFixed {
|
|||
pub fn set_being_liquidated(&mut self, b: bool) {
|
||||
self.being_liquidated = if b { 1 } else { 0 };
|
||||
}
|
||||
|
||||
pub fn maybe_recover_from_being_liquidated(&mut self, init_health: I80F48) {
|
||||
// This is used as threshold to flip flag instead of 0 because of dust issues
|
||||
let one_native_usdc = I80F48::ONE;
|
||||
if self.being_liquidated() && init_health > -one_native_usdc {
|
||||
self.set_being_liquidated(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DynamicAccountType for MangoAccount {
|
||||
|
@ -522,10 +521,6 @@ impl<
|
|||
self.fixed().being_liquidated()
|
||||
}
|
||||
|
||||
pub fn is_bankrupt(&self) -> bool {
|
||||
self.fixed().is_bankrupt()
|
||||
}
|
||||
|
||||
pub fn borrow(&self) -> DynamicAccountRef<MangoAccount> {
|
||||
DynamicAccount {
|
||||
header: self.header(),
|
||||
|
|
|
@ -148,10 +148,9 @@ pub struct PerpPositions {
|
|||
/// measured in native quote
|
||||
pub quote_position_native: I80F48,
|
||||
|
||||
/// Tracks what the position is to calculate average entry & break even price
|
||||
pub base_entry_lots: i64,
|
||||
/// Tracks what the position is to calculate average entry & break even price
|
||||
pub quote_entry_native: i64,
|
||||
pub quote_exit_native: i64,
|
||||
pub quote_running_native: i64,
|
||||
|
||||
/// Already settled funding
|
||||
pub long_settled_funding: I80F48,
|
||||
|
@ -185,7 +184,7 @@ impl std::fmt::Debug for PerpPositions {
|
|||
.finish()
|
||||
}
|
||||
}
|
||||
const_assert_eq!(size_of::<PerpPositions>(), 8 + 8 * 8 + 3 * 16 + 64);
|
||||
const_assert_eq!(size_of::<PerpPositions>(), 8 + 7 * 8 + 3 * 16 + 64);
|
||||
const_assert_eq!(size_of::<PerpPositions>() % 8, 0);
|
||||
|
||||
unsafe impl bytemuck::Pod for PerpPositions {}
|
||||
|
@ -197,9 +196,8 @@ impl Default for PerpPositions {
|
|||
market_index: PerpMarketIndex::MAX,
|
||||
base_position_lots: 0,
|
||||
quote_position_native: I80F48::ZERO,
|
||||
base_entry_lots: 0,
|
||||
quote_entry_native: 0,
|
||||
quote_exit_native: 0,
|
||||
quote_running_native: 0,
|
||||
bids_base_lots: 0,
|
||||
asks_base_lots: 0,
|
||||
taker_base_lots: 0,
|
||||
|
@ -273,26 +271,24 @@ impl PerpPositions {
|
|||
}
|
||||
let old_position = self.base_position_lots;
|
||||
let is_increasing = old_position == 0 || old_position.signum() == base_change.signum();
|
||||
self.quote_running_native = cm!(self.quote_running_native + quote_change);
|
||||
match is_increasing {
|
||||
true => {
|
||||
self.quote_entry_native = cm!(self.quote_entry_native + quote_change);
|
||||
self.base_entry_lots = cm!(self.base_entry_lots + base_change);
|
||||
}
|
||||
false => {
|
||||
let new_position = cm!(old_position + base_change);
|
||||
self.quote_exit_native = cm!(self.quote_exit_native + quote_change);
|
||||
let is_overflow = old_position.signum() == -new_position.signum();
|
||||
if new_position == 0 {
|
||||
self.quote_entry_native = 0;
|
||||
self.quote_exit_native = 0;
|
||||
self.base_entry_lots = 0;
|
||||
}
|
||||
if is_overflow {
|
||||
self.quote_entry_native = cm!(((new_position as f64) * (quote_change as f64)
|
||||
/ (base_change as f64))
|
||||
.round()) as i64;
|
||||
self.quote_exit_native = 0;
|
||||
self.base_entry_lots = new_position;
|
||||
let changes_side = old_position.signum() == -new_position.signum();
|
||||
self.quote_entry_native = if changes_side {
|
||||
cm!(
|
||||
((new_position as f64) * (quote_change as f64) / (base_change as f64))
|
||||
.round()
|
||||
) as i64
|
||||
} else {
|
||||
let remaining_frac =
|
||||
(1f64 - (base_change.abs() as f64) / (old_position.abs() as f64)).max(0f64);
|
||||
let initial_entry = self.quote_entry_native as f64;
|
||||
(initial_entry * remaining_frac).round() as i64
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -311,10 +307,10 @@ impl PerpPositions {
|
|||
|
||||
/// Calculate the average entry price of the position
|
||||
pub fn get_avg_entry_price(&self) -> I80F48 {
|
||||
if self.base_entry_lots == 0 {
|
||||
if self.base_position_lots == 0 {
|
||||
return I80F48::ZERO; // TODO: What should this actually return? Error? NaN?
|
||||
}
|
||||
(I80F48::from(self.quote_entry_native) / I80F48::from(self.base_entry_lots)).abs()
|
||||
(I80F48::from(self.quote_entry_native) / I80F48::from(self.base_position_lots)).abs()
|
||||
}
|
||||
|
||||
/// Calculate the break even price of the position
|
||||
|
@ -322,9 +318,7 @@ impl PerpPositions {
|
|||
if self.base_position_lots == 0 {
|
||||
return I80F48::ZERO; // TODO: What should this actually return? Error? NaN?
|
||||
}
|
||||
(I80F48::from(self.quote_entry_native + self.quote_exit_native)
|
||||
/ I80F48::from(self.base_position_lots))
|
||||
.abs()
|
||||
(I80F48::from(self.quote_running_native) / I80F48::from(self.base_position_lots)).abs()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -389,8 +383,7 @@ mod tests {
|
|||
pos.base_position_lots = base_pos;
|
||||
pos.quote_position_native = I80F48::from(quote_pos);
|
||||
pos.quote_entry_native = entry_pos;
|
||||
pos.quote_exit_native = 0;
|
||||
pos.base_entry_lots = base_pos;
|
||||
pos.quote_running_native = quote_pos;
|
||||
pos
|
||||
}
|
||||
|
||||
|
@ -441,6 +434,7 @@ mod tests {
|
|||
// Go long 10 @ 10
|
||||
pos.change_base_and_entry_positions(&mut market, 10, -100);
|
||||
assert_eq!(pos.quote_entry_native, -100);
|
||||
assert_eq!(pos.quote_running_native, -100);
|
||||
assert_eq!(pos.get_avg_entry_price(), I80F48::from(10));
|
||||
}
|
||||
|
||||
|
@ -451,6 +445,7 @@ mod tests {
|
|||
// Go short 10 @ 10
|
||||
pos.change_base_and_entry_positions(&mut market, -10, 100);
|
||||
assert_eq!(pos.quote_entry_native, 100);
|
||||
assert_eq!(pos.quote_running_native, 100);
|
||||
assert_eq!(pos.get_avg_entry_price(), I80F48::from(10));
|
||||
}
|
||||
|
||||
|
@ -461,6 +456,7 @@ mod tests {
|
|||
// Go long 10 @ 30
|
||||
pos.change_base_and_entry_positions(&mut market, 10, -300);
|
||||
assert_eq!(pos.quote_entry_native, -400);
|
||||
assert_eq!(pos.quote_running_native, -400);
|
||||
assert_eq!(pos.get_avg_entry_price(), I80F48::from(20));
|
||||
}
|
||||
|
||||
|
@ -471,6 +467,7 @@ mod tests {
|
|||
// Go short 10 @ 10
|
||||
pos.change_base_and_entry_positions(&mut market, -10, 300);
|
||||
assert_eq!(pos.quote_entry_native, 400);
|
||||
assert_eq!(pos.quote_running_native, 400);
|
||||
assert_eq!(pos.get_avg_entry_price(), I80F48::from(20));
|
||||
}
|
||||
|
||||
|
@ -479,10 +476,9 @@ mod tests {
|
|||
let mut market = create_perp_market();
|
||||
let mut pos = create_perp_position(-10, 100, 100);
|
||||
// Go long 5 @ 50
|
||||
pos.change_base_and_entry_positions(&mut market, 5, 250);
|
||||
assert_eq!(pos.quote_entry_native, 100);
|
||||
assert_eq!(pos.base_entry_lots, -10);
|
||||
assert_eq!(pos.quote_exit_native, 250);
|
||||
pos.change_base_and_entry_positions(&mut market, 5, -250);
|
||||
assert_eq!(pos.quote_entry_native, 50);
|
||||
assert_eq!(pos.quote_running_native, -150);
|
||||
assert_eq!(pos.get_avg_entry_price(), I80F48::from(10)); // Entry price remains the same when decreasing
|
||||
}
|
||||
|
||||
|
@ -491,10 +487,9 @@ mod tests {
|
|||
let mut market = create_perp_market();
|
||||
let mut pos = create_perp_position(10, -100, -100);
|
||||
// Go short 5 @ 50
|
||||
pos.change_base_and_entry_positions(&mut market, -5, -250);
|
||||
assert_eq!(pos.quote_entry_native, -100);
|
||||
assert_eq!(pos.base_entry_lots, 10);
|
||||
assert_eq!(pos.quote_exit_native, -250);
|
||||
pos.change_base_and_entry_positions(&mut market, -5, 250);
|
||||
assert_eq!(pos.quote_entry_native, -50);
|
||||
assert_eq!(pos.quote_running_native, 150);
|
||||
assert_eq!(pos.get_avg_entry_price(), I80F48::from(10)); // Entry price remains the same when decreasing
|
||||
}
|
||||
|
||||
|
@ -505,8 +500,7 @@ mod tests {
|
|||
// Go short 10 @ 50
|
||||
pos.change_base_and_entry_positions(&mut market, -10, 250);
|
||||
assert_eq!(pos.quote_entry_native, 0);
|
||||
assert_eq!(pos.quote_exit_native, 0);
|
||||
assert_eq!(pos.base_entry_lots, 0);
|
||||
assert_eq!(pos.quote_running_native, 150);
|
||||
assert_eq!(pos.get_avg_entry_price(), I80F48::from(0)); // Entry price zero when no position
|
||||
}
|
||||
|
||||
|
@ -517,8 +511,7 @@ mod tests {
|
|||
// Go long 10 @ 50
|
||||
pos.change_base_and_entry_positions(&mut market, 10, -250);
|
||||
assert_eq!(pos.quote_entry_native, 0);
|
||||
assert_eq!(pos.quote_exit_native, 0);
|
||||
assert_eq!(pos.base_entry_lots, 0);
|
||||
assert_eq!(pos.quote_running_native, -150);
|
||||
assert_eq!(pos.get_avg_entry_price(), I80F48::from(0)); // Entry price zero when no position
|
||||
}
|
||||
|
||||
|
@ -529,8 +522,7 @@ mod tests {
|
|||
// Go short 15 @ 20
|
||||
pos.change_base_and_entry_positions(&mut market, -15, 300);
|
||||
assert_eq!(pos.quote_entry_native, 100);
|
||||
assert_eq!(pos.quote_exit_native, 0);
|
||||
assert_eq!(pos.base_entry_lots, -5);
|
||||
assert_eq!(pos.quote_running_native, 200);
|
||||
assert_eq!(pos.get_avg_entry_price(), I80F48::from(20)); // Entry price zero when no position
|
||||
}
|
||||
|
||||
|
@ -541,8 +533,7 @@ mod tests {
|
|||
// Go short 15 @ 20
|
||||
pos.change_base_and_entry_positions(&mut market, 15, -300);
|
||||
assert_eq!(pos.quote_entry_native, -100);
|
||||
assert_eq!(pos.quote_exit_native, 0);
|
||||
assert_eq!(pos.base_entry_lots, 5);
|
||||
assert_eq!(pos.quote_running_native, -200);
|
||||
assert_eq!(pos.get_avg_entry_price(), I80F48::from(20)); // Entry price zero when no position
|
||||
}
|
||||
|
||||
|
@ -554,9 +545,8 @@ mod tests {
|
|||
pos.change_base_and_entry_positions(&mut market, 11, -11 * 10_000);
|
||||
// Sell 1 @ 12,000
|
||||
pos.change_base_and_entry_positions(&mut market, -1, 12_000);
|
||||
assert_eq!(pos.quote_entry_native, -11 * 10_000);
|
||||
assert_eq!(pos.quote_exit_native, 12_000);
|
||||
assert_eq!(pos.base_entry_lots, 11);
|
||||
assert_eq!(pos.quote_entry_native, -10 * 10_000);
|
||||
assert_eq!(pos.quote_running_native, -98_000);
|
||||
assert_eq!(pos.base_position_lots, 10);
|
||||
assert_eq!(pos.get_break_even_price(), I80F48::from(9_800)); // We made 2k on the trade, so we can sell our contract up to a loss of 200 each
|
||||
}
|
||||
|
@ -592,9 +582,7 @@ mod tests {
|
|||
assert_eq!(pos.base_position_lots, 0);
|
||||
// quote entry position should be 0
|
||||
assert_eq!(pos.quote_entry_native, 0);
|
||||
// quote exit should be 0
|
||||
assert_eq!(pos.quote_exit_native, 0);
|
||||
// base entry lots should be 0
|
||||
assert_eq!(pos.base_entry_lots, 0);
|
||||
// running quote should be 0
|
||||
assert_eq!(pos.quote_running_native, 0);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -220,7 +220,6 @@ async fn test_bankrupt_tokens_socialize_loss() -> Result<(), TransportError> {
|
|||
);
|
||||
let liqee = get_mango_account(solana, account).await;
|
||||
assert!(liqee.being_liquidated());
|
||||
assert!(!liqee.is_bankrupt());
|
||||
|
||||
// eat collateral2, leaving the account bankrupt
|
||||
send_tx(
|
||||
|
@ -246,7 +245,6 @@ async fn test_bankrupt_tokens_socialize_loss() -> Result<(), TransportError> {
|
|||
);
|
||||
let liqee = get_mango_account(solana, account).await;
|
||||
assert!(liqee.being_liquidated());
|
||||
assert!(liqee.is_bankrupt());
|
||||
|
||||
//
|
||||
// TEST: socialize loss on borrow1 and 2
|
||||
|
@ -272,7 +270,6 @@ async fn test_bankrupt_tokens_socialize_loss() -> Result<(), TransportError> {
|
|||
);
|
||||
let liqee = get_mango_account(solana, account).await;
|
||||
assert!(liqee.being_liquidated());
|
||||
assert!(liqee.is_bankrupt());
|
||||
assert!(account_position_closed(solana, account, borrow_token1.bank).await);
|
||||
// both bank's borrows were completely wiped: no one else borrowed
|
||||
let borrow1_bank0: Bank = solana.get_account(borrow_token1.bank).await;
|
||||
|
@ -299,7 +296,6 @@ async fn test_bankrupt_tokens_socialize_loss() -> Result<(), TransportError> {
|
|||
);
|
||||
let liqee = get_mango_account(solana, account).await;
|
||||
assert!(!liqee.being_liquidated());
|
||||
assert!(!liqee.is_bankrupt());
|
||||
assert!(account_position_closed(solana, account, borrow_token2.bank).await);
|
||||
|
||||
Ok(())
|
||||
|
@ -531,7 +527,6 @@ async fn test_bankrupt_tokens_insurance_fund() -> Result<(), TransportError> {
|
|||
assert!(account_position_closed(solana, account, collateral_token1.bank).await);
|
||||
let liqee = get_mango_account(solana, account).await;
|
||||
assert!(liqee.being_liquidated());
|
||||
assert!(!liqee.is_bankrupt());
|
||||
|
||||
// eat collateral2, leaving the account bankrupt
|
||||
send_tx(
|
||||
|
@ -552,7 +547,6 @@ async fn test_bankrupt_tokens_insurance_fund() -> Result<(), TransportError> {
|
|||
assert!(account_position_closed(solana, account, collateral_token2.bank).await,);
|
||||
let liqee = get_mango_account(solana, account).await;
|
||||
assert!(liqee.being_liquidated());
|
||||
assert!(liqee.is_bankrupt());
|
||||
|
||||
//
|
||||
// TEST: use the insurance fund to liquidate borrow1 and borrow2
|
||||
|
@ -577,7 +571,6 @@ async fn test_bankrupt_tokens_insurance_fund() -> Result<(), TransportError> {
|
|||
.unwrap();
|
||||
let liqee = get_mango_account(solana, account).await;
|
||||
assert!(liqee.being_liquidated());
|
||||
assert!(liqee.is_bankrupt());
|
||||
assert!(account_position_closed(solana, account, borrow_token1.bank).await);
|
||||
assert_eq!(
|
||||
solana.token_account_balance(insurance_vault).await,
|
||||
|
@ -610,7 +603,6 @@ async fn test_bankrupt_tokens_insurance_fund() -> Result<(), TransportError> {
|
|||
.unwrap();
|
||||
let liqee = get_mango_account(solana, account).await;
|
||||
assert!(liqee.being_liquidated());
|
||||
assert!(liqee.is_bankrupt());
|
||||
assert!(account_position_closed(solana, account, borrow_token1.bank).await);
|
||||
assert_eq!(
|
||||
account_position(solana, account, borrow_token2.bank).await,
|
||||
|
@ -645,7 +637,6 @@ async fn test_bankrupt_tokens_insurance_fund() -> Result<(), TransportError> {
|
|||
.unwrap();
|
||||
let liqee = get_mango_account(solana, account).await;
|
||||
assert!(!liqee.being_liquidated());
|
||||
assert!(!liqee.is_bankrupt());
|
||||
assert!(account_position_closed(solana, account, borrow_token1.bank).await);
|
||||
assert!(account_position_closed(solana, account, borrow_token2.bank).await);
|
||||
assert_eq!(solana.token_account_balance(insurance_vault).await, 0);
|
||||
|
|
|
@ -406,7 +406,6 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
|
|||
assert!(account_position_closed(solana, account, collateral_token2.bank).await,);
|
||||
let liqee = get_mango_account(solana, account).await;
|
||||
assert!(liqee.being_liquidated());
|
||||
assert!(!liqee.is_bankrupt());
|
||||
|
||||
//
|
||||
// TEST: liquidate the remaining borrow2 against collateral1,
|
||||
|
@ -436,7 +435,6 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
|
|||
);
|
||||
let liqee = get_mango_account(solana, account).await;
|
||||
assert!(liqee.being_liquidated());
|
||||
assert!(!liqee.is_bankrupt());
|
||||
|
||||
//
|
||||
// TEST: liquidate borrow1 with collateral1, but place a limit
|
||||
|
@ -468,7 +466,6 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
|
|||
);
|
||||
let liqee = get_mango_account(solana, account).await;
|
||||
assert!(liqee.being_liquidated());
|
||||
assert!(!liqee.is_bankrupt());
|
||||
|
||||
//
|
||||
// TEST: liquidate borrow1 with collateral1, making the account healthy again
|
||||
|
@ -503,7 +500,6 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
|
|||
);
|
||||
let liqee = get_mango_account(solana, account).await;
|
||||
assert!(!liqee.being_liquidated());
|
||||
assert!(!liqee.is_bankrupt());
|
||||
|
||||
//
|
||||
// TEST: bankruptcy when collateral is dusted
|
||||
|
@ -567,11 +563,12 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
// Liqee's remaining collateral got dusted, only borrows remain: bankrupt
|
||||
// Liqee's remaining collateral got dusted, only borrows remain
|
||||
// but the borrow amount is so tiny, that being_liquidated is already switched off
|
||||
let liqee = get_mango_account(solana, account).await;
|
||||
assert_eq!(liqee.token_iter_active().count(), 1);
|
||||
assert!(liqee.is_bankrupt());
|
||||
assert!(liqee.being_liquidated());
|
||||
assert!(account_position_f64(solana, account, borrow_token1.bank).await > -1.0);
|
||||
assert!(!liqee.being_liquidated());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -162,6 +162,22 @@ export class HealthCache {
|
|||
);
|
||||
}
|
||||
|
||||
simHealthRatioWithTokenPositionChanges(
|
||||
group: Group,
|
||||
tokenChanges: { tokenName: string; tokenAmount: number }[],
|
||||
healthType: HealthType = HealthType.init,
|
||||
): I80F48 {
|
||||
const adjustedCache: HealthCache = _.cloneDeep(this);
|
||||
for (const change of tokenChanges) {
|
||||
const bank = group.banksMap.get(change.tokenName);
|
||||
adjustedCache.tokenInfos[bank.tokenIndex].balance =
|
||||
adjustedCache.tokenInfos[bank.tokenIndex].balance.add(
|
||||
I80F48.fromNumber(change.tokenAmount).mul(bank.price),
|
||||
);
|
||||
}
|
||||
return adjustedCache.healthRatio(healthType);
|
||||
}
|
||||
|
||||
getMaxSourceForTokenSwap(
|
||||
group: Group,
|
||||
sourceTokenName: string,
|
||||
|
|
|
@ -27,7 +27,6 @@ export class MangoAccount {
|
|||
name: number[];
|
||||
delegate: PublicKey;
|
||||
beingLiquidated: number;
|
||||
isBankrupt: number;
|
||||
accountNum: number;
|
||||
bump: number;
|
||||
netDeposits: number;
|
||||
|
@ -46,7 +45,6 @@ export class MangoAccount {
|
|||
obj.name,
|
||||
obj.delegate,
|
||||
obj.beingLiquidated,
|
||||
obj.isBankrupt,
|
||||
obj.accountNum,
|
||||
obj.bump,
|
||||
obj.netDeposits,
|
||||
|
@ -67,7 +65,6 @@ export class MangoAccount {
|
|||
name: number[],
|
||||
public delegate: PublicKey,
|
||||
beingLiquidated: number,
|
||||
isBankrupt: number,
|
||||
public accountNum: number,
|
||||
bump: number,
|
||||
netDeposits: number,
|
||||
|
@ -248,34 +245,19 @@ export class MangoAccount {
|
|||
}
|
||||
|
||||
/**
|
||||
* Simulates new health after applying tokenChanges to the token positions. Useful to simulate health after a potential swap.
|
||||
* Simulates new health ratio after applying tokenChanges to the token positions.
|
||||
* e.g. useful to simulate health after a potential swap.
|
||||
*/
|
||||
simHealthWithTokenPositionChanges(
|
||||
simHealthRatioWithTokenPositionChanges(
|
||||
group: Group,
|
||||
tokenChanges: { tokenName: string; tokenAmount: number }[],
|
||||
healthType: HealthType = HealthType.init,
|
||||
): I80F48 {
|
||||
// This is a approximation of the easy case, where
|
||||
// mango account has no token positions for tokens in changes list, or
|
||||
// the change is in direction e.g. deposits for deposits, borrows for borrows, of existing token position.
|
||||
// TODO: recompute entire health using components.
|
||||
let initHealth = (this.accountData as MangoAccountData).initHealth;
|
||||
for (const change of tokenChanges) {
|
||||
const bank = group.banksMap.get(change.tokenName);
|
||||
if (change.tokenAmount >= 0) {
|
||||
initHealth = initHealth.add(
|
||||
bank.initAssetWeight
|
||||
.mul(I80F48.fromNumber(change.tokenAmount))
|
||||
.mul(bank.price),
|
||||
);
|
||||
} else {
|
||||
initHealth = initHealth.sub(
|
||||
bank.initLiabWeight
|
||||
.mul(I80F48.fromNumber(change.tokenAmount))
|
||||
.mul(bank.price),
|
||||
);
|
||||
}
|
||||
}
|
||||
return initHealth;
|
||||
return this.accountData.healthCache.simHealthRatioWithTokenPositionChanges(
|
||||
group,
|
||||
tokenChanges,
|
||||
healthType,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -60,24 +60,21 @@ async function debugUser(client, group, mangoAccount) {
|
|||
);
|
||||
|
||||
console.log(
|
||||
'mangoAccount.simHealthWithTokenPositionChanges ' +
|
||||
toUiDecimalsForQuote(
|
||||
(
|
||||
await mangoAccount.simHealthWithTokenPositionChanges(group, [
|
||||
{
|
||||
tokenName: 'USDC',
|
||||
tokenAmount:
|
||||
-20_000 *
|
||||
Math.pow(10, group.banksMap.get('BTC')!.mintDecimals!),
|
||||
},
|
||||
{
|
||||
tokenName: 'BTC',
|
||||
tokenAmount:
|
||||
1 * Math.pow(10, group.banksMap.get('BTC')!.mintDecimals!),
|
||||
},
|
||||
])
|
||||
).toNumber(),
|
||||
),
|
||||
'mangoAccount.simHealthRatioWithTokenPositionChanges ' +
|
||||
(
|
||||
await mangoAccount.simHealthRatioWithTokenPositionChanges(group, [
|
||||
{
|
||||
tokenName: 'USDC',
|
||||
tokenAmount:
|
||||
-95_000 * Math.pow(10, group.banksMap.get('USDC')!.mintDecimals!),
|
||||
},
|
||||
{
|
||||
tokenName: 'BTC',
|
||||
tokenAmount:
|
||||
4 * Math.pow(10, group.banksMap.get('BTC')!.mintDecimals!),
|
||||
},
|
||||
])
|
||||
).toNumber(),
|
||||
);
|
||||
|
||||
function getMaxSourceForTokenSwapWrapper(src, tgt) {
|
||||
|
|
|
@ -3007,10 +3007,7 @@ export type MangoV4 = {
|
|||
"type": "u8"
|
||||
},
|
||||
{
|
||||
"name": "isBankrupt",
|
||||
"docs": [
|
||||
"This account cannot do anything except go through `resolve_bankruptcy`"
|
||||
],
|
||||
"name": "padding5",
|
||||
"type": "u8"
|
||||
},
|
||||
{
|
||||
|
@ -8054,10 +8051,7 @@ export const IDL: MangoV4 = {
|
|||
"type": "u8"
|
||||
},
|
||||
{
|
||||
"name": "isBankrupt",
|
||||
"docs": [
|
||||
"This account cannot do anything except go through `resolve_bankruptcy`"
|
||||
],
|
||||
"name": "padding5",
|
||||
"type": "u8"
|
||||
},
|
||||
{
|
||||
|
|
Loading…
Reference in New Issue