Health: order-independent serum3 health

Now all the reserved funds in serum3 open orders accounts are added into
each possible token at the same time. Then the worse case from applying
the reserved funds to either quote or base is selected.

This is reasonably cheap to compute, leads to unchanged results when no
markets share (non USDC) base or quote tokens, but can underestimate
the "true" health value when markets do.

An additional advantage is that HealthCache is now indepenent of serum
open orders and can compute the init and maint health when the user has
active OpenOrders accounts.
This commit is contained in:
Christian Kamm 2022-06-21 12:56:55 +02:00
parent d4cec8dfa4
commit ba79995c01
3 changed files with 154 additions and 67 deletions

View File

@ -44,7 +44,7 @@ pub fn liq_token_with_token(
require!(liqee.is_bankrupt == 0, MangoError::IsBankrupt);
// Initial liqee health check
let mut liqee_health_cache = health_cache_for_liqee(&liqee, &account_retriever)?;
let mut liqee_health_cache = new_health_cache(&liqee, &account_retriever)?;
let init_health = liqee_health_cache.health(HealthType::Init)?;
if liqee.being_liquidated != 0 {
if init_health > I80F48::ZERO {

View File

@ -289,7 +289,7 @@ pub fn compute_health_from_fixed_accounts(
begin_perp: cm!(active_token_len * 2),
begin_serum3: cm!(active_token_len * 2 + active_perp_len),
};
compute_health_detail(account, &retriever, health_type, true)?.health(health_type)
new_health_cache(account, &retriever)?.health(health_type)
}
/// Compute health with an arbitrary AccountRetriever
@ -298,21 +298,7 @@ pub fn compute_health(
health_type: HealthType,
retriever: &impl AccountRetriever,
) -> Result<I80F48> {
compute_health_detail(account, retriever, health_type, true)?.health(health_type)
}
/// Compute health for a liqee.
///
/// This has the advantage of returning a HealthCache, allowing for health
/// to be recomputed after token balance changes due to liquidation.
///
/// However, this only works if the serum3 open orders accounts have been
/// fully settled (like via serum3_liq_force_cancel_orders).
pub fn health_cache_for_liqee(
account: &MangoAccount,
retriever: &impl AccountRetriever,
) -> Result<HealthCache> {
compute_health_detail(account, retriever, HealthType::Init, false)
new_health_cache(account, retriever)?.health(health_type)
}
struct TokenInfo {
@ -324,6 +310,8 @@ struct TokenInfo {
oracle_price: I80F48, // native/native
// in health-reference-token native units
balance: I80F48,
// in health-reference-token native units
serum3_max_reserved: I80F48,
}
impl TokenInfo {
@ -344,6 +332,42 @@ impl TokenInfo {
}
}
struct Serum3Info {
reserved: I80F48,
base_index: usize,
quote_index: usize,
}
impl Serum3Info {
#[inline(always)]
fn health_contribution(&self, health_type: HealthType, token_infos: &[TokenInfo]) -> I80F48 {
let base_info = &token_infos[self.base_index];
let quote_info = &token_infos[self.quote_index];
let reserved = self.reserved;
// compute how much the health would increase if the reserved balance were
// applied to the passed token info
let compute_health_effect = |token_info: &TokenInfo| {
let token_balance = cm!(token_info.balance + token_info.serum3_max_reserved);
let (asset_part, liab_part) = if token_balance >= reserved {
(reserved, I80F48::ZERO)
} else if token_balance.is_negative() {
(I80F48::ZERO, reserved)
} else {
(token_balance, cm!(reserved - token_balance))
};
let asset_weight = token_info.asset_weight(health_type);
let liab_weight = token_info.liab_weight(health_type);
cm!(asset_weight * asset_part + liab_weight * liab_part)
};
let reserved_as_base = compute_health_effect(base_info);
let reserved_as_quote = compute_health_effect(quote_info);
reserved_as_base.min(reserved_as_quote)
}
}
struct PerpInfo {
maint_asset_weight: I80F48,
init_asset_weight: I80F48,
@ -384,6 +408,7 @@ impl PerpInfo {
pub struct HealthCache {
token_infos: Vec<TokenInfo>,
serum3_infos: Vec<Serum3Info>,
perp_infos: Vec<PerpInfo>,
}
@ -394,6 +419,10 @@ impl HealthCache {
let contrib = health_contribution(health_type, token_info, token_info.balance)?;
health = cm!(health + contrib);
}
for serum3_info in self.serum3_infos.iter() {
let contrib = serum3_info.health_contribution(health_type, &self.token_infos);
health = cm!(health + contrib);
}
for perp_info in self.perp_infos.iter() {
let contrib = perp_info.health_contribution(health_type);
health = cm!(health + contrib);
@ -429,29 +458,10 @@ fn health_contribution(
Ok(cm!(balance * weight))
}
/// Compute health contribution of two tokens - pure convenience
#[inline(always)]
fn pair_health(
health_type: HealthType,
info1: &TokenInfo,
balance1: I80F48,
info2: &TokenInfo,
) -> Result<I80F48> {
let health1 = health_contribution(health_type, info1, balance1)?;
let health2 = health_contribution(health_type, info2, info2.balance)?;
Ok(cm!(health1 + health2))
}
/// The HealthInfo returned from this function is specialized for the health_type
/// unless called with allow_serum3=false.
///
/// The reason is that the health type used can affect the way funds reserved for
/// orders get distributed to the token balances.
fn compute_health_detail(
/// Generate a HealthCache for an account and its health accounts.
pub fn new_health_cache(
account: &MangoAccount,
retriever: &impl AccountRetriever,
health_type: HealthType,
allow_serum3: bool,
) -> Result<HealthCache> {
// token contribution from token accounts
let mut token_infos = vec![];
@ -478,21 +488,15 @@ fn compute_health_detail(
init_liab_weight: bank.init_liab_weight,
oracle_price,
balance: cm!(native * oracle_price),
serum3_max_reserved: I80F48::ZERO,
});
}
// token contribution from serum accounts
// Fill the TokenInfo balance with free funds in serum3 oo accounts, and fill
// the serum3_max_reserved with their reserved funds. Also build Serum3Infos.
let mut serum3_infos = vec![];
for (i, serum_account) in account.serum3.iter_active().enumerate() {
let oo = retriever.serum_oo(i, &serum_account.open_orders)?;
if !allow_serum3 {
require!(
oo.native_coin_total == 0
&& oo.native_pc_total == 0
&& oo.referrer_rebates_accrued == 0,
MangoError::SomeError
);
continue;
}
// find the TokenInfos for the market's base and quote tokens
let base_index = find_token_info_index(&token_infos, serum_account.base_token_index)?;
@ -511,22 +515,19 @@ fn compute_health_detail(
base_info.balance = cm!(base_info.balance + base_free * base_info.oracle_price);
quote_info.balance = cm!(quote_info.balance + quote_free * quote_info.oracle_price);
// for the amounts that are reserved for orders, compute the worst case for health
// by checking if everything-is-base or everything-is-quote produces worse
// outcomes
// add the reserved amount to both sides, to have the worst-case covered
let reserved_base = I80F48::from_num(cm!(oo.native_coin_total - oo.native_coin_free));
let reserved_quote = I80F48::from_num(cm!(oo.native_pc_total - oo.native_pc_free));
let reserved_balance =
cm!(reserved_base * base_info.oracle_price + reserved_quote * quote_info.oracle_price);
let all_in_base = cm!(base_info.balance + reserved_balance);
let all_in_quote = cm!(quote_info.balance + reserved_balance);
if pair_health(health_type, base_info, all_in_base, quote_info)?
< pair_health(health_type, quote_info, all_in_quote, base_info)?
{
base_info.balance = all_in_base;
} else {
quote_info.balance = all_in_quote;
}
base_info.serum3_max_reserved = cm!(base_info.serum3_max_reserved + reserved_balance);
quote_info.serum3_max_reserved = cm!(quote_info.serum3_max_reserved + reserved_balance);
serum3_infos.push(Serum3Info {
reserved: reserved_balance,
base_index,
quote_index,
});
}
// health contribution from perp accounts
@ -616,6 +617,7 @@ fn compute_health_detail(
Ok(HealthCache {
token_infos,
serum3_infos,
perp_infos,
})
}
@ -888,7 +890,9 @@ mod tests {
struct TestHealth1Case {
token1: i64,
token2: i64,
token3: i64,
oo_1_2: (u64, u64),
oo_1_3: (u64, u64),
perp1: (i64, i64, i64, i64),
expected_health: f64,
}
@ -898,6 +902,7 @@ mod tests {
let (mut bank1, mut oracle1) = mock_bank_and_oracle(group, 1, 1.0, 0.2, 0.1);
let (mut bank2, mut oracle2) = mock_bank_and_oracle(group, 4, 5.0, 0.5, 0.3);
let (mut bank3, mut oracle3) = mock_bank_and_oracle(group, 5, 10.0, 0.5, 0.3);
bank1
.data()
.change_without_fee(
@ -912,15 +917,30 @@ mod tests {
I80F48::from(testcase.token2),
)
.unwrap();
bank3
.data()
.change_without_fee(
account.tokens.get_mut_or_create(5).unwrap().0,
I80F48::from(testcase.token3),
)
.unwrap();
let mut oo1 = TestAccount::<OpenOrders>::new_zeroed();
let serum3account = account.serum3.create(2).unwrap();
serum3account.open_orders = oo1.pubkey;
serum3account.base_token_index = 4;
serum3account.quote_token_index = 1;
let serum3account1 = account.serum3.create(2).unwrap();
serum3account1.open_orders = oo1.pubkey;
serum3account1.base_token_index = 4;
serum3account1.quote_token_index = 1;
oo1.data().native_pc_total = testcase.oo_1_2.0;
oo1.data().native_coin_total = testcase.oo_1_2.1;
let mut oo2 = TestAccount::<OpenOrders>::new_zeroed();
let serum3account2 = account.serum3.create(3).unwrap();
serum3account2.open_orders = oo2.pubkey;
serum3account2.base_token_index = 5;
serum3account2.quote_token_index = 1;
oo2.data().native_pc_total = testcase.oo_1_3.0;
oo2.data().native_coin_total = testcase.oo_1_3.1;
let mut perp1 = TestAccount::<PerpMarket>::new_zeroed();
perp1.data().group = group;
perp1.data().perp_market_index = 9;
@ -941,10 +961,13 @@ mod tests {
let ais = vec![
bank1.as_account_info(),
bank2.as_account_info(),
bank3.as_account_info(),
oracle1.as_account_info(),
oracle2.as_account_info(),
oracle3.as_account_info(),
perp1.as_account_info(),
oo1.as_account_info(),
oo2.as_account_info(),
];
let retriever = ScanningAccountRetriever::new(&ais, &group).unwrap();
@ -970,7 +993,7 @@ mod tests {
let base_price = 5.0;
let base_lots_to_quote = 10.0 * base_price;
let testcases = vec![
TestHealth1Case {
TestHealth1Case { // 0
token1: 100,
token2: -10,
oo_1_2: (20, 15),
@ -982,8 +1005,9 @@ mod tests {
- 10.0 * base_price * 1.5
// for perp (scenario: bids execute)
+ (3.0 + 7.0) * base_lots_to_quote * 0.8 + (-131.0 - 7.0 * base_lots_to_quote),
..Default::default()
},
TestHealth1Case {
TestHealth1Case { // 1
token1: -100,
token2: 10,
oo_1_2: (20, 15),
@ -995,27 +1019,90 @@ mod tests {
+ (10.0 * base_price + (20.0 + 15.0 * base_price)) * 0.5
// for perp (scenario: asks execute)
+ (-10.0 - 11.0) * base_lots_to_quote * 1.2 + (-131.0 + 11.0 * base_lots_to_quote),
..Default::default()
},
TestHealth1Case {
// 2
perp1: (-1, 100, 0, 0),
expected_health: 0.0,
..Default::default()
},
TestHealth1Case {
// 3
perp1: (1, -100, 0, 0),
expected_health: -100.0 + 0.8 * 1.0 * base_lots_to_quote,
..Default::default()
},
TestHealth1Case {
// 4
perp1: (10, 100, 0, 0),
expected_health: 0.0,
..Default::default()
},
TestHealth1Case {
// 5
perp1: (30, -100, 0, 0),
expected_health: 0.0,
..Default::default()
},
TestHealth1Case { // 6, reserved oo funds
token1: -100,
token2: -10,
token3: -10,
oo_1_2: (1, 1),
oo_1_3: (1, 1),
expected_health:
// tokens
-100.0 * 1.2 - 10.0 * 5.0 * 1.5 - 10.0 * 10.0 * 1.5
// oo_1_2 (-> token1)
+ (1.0 + 5.0) * 1.2
// oo_1_3 (-> token1)
+ (1.0 + 10.0) * 1.2,
..Default::default()
},
TestHealth1Case { // 7, reserved oo funds cross the zero balance level
token1: -14,
token2: -10,
token3: -10,
oo_1_2: (1, 1),
oo_1_3: (1, 1),
expected_health:
// tokens
-14.0 * 1.2 - 10.0 * 5.0 * 1.5 - 10.0 * 10.0 * 1.5
// oo_1_2 (-> token1)
+ 3.0 * 1.2 + 3.0 * 0.8
// oo_1_3 (-> token1)
+ 8.0 * 1.2 + 3.0 * 0.8,
..Default::default()
},
TestHealth1Case { // 8, reserved oo funds in a non-quote currency
token1: -100,
token2: -100,
token3: -1,
oo_1_2: (0, 0),
oo_1_3: (10, 1),
expected_health:
// tokens
-100.0 * 1.2 - 100.0 * 5.0 * 1.5 - 10.0 * 1.5
// oo_1_3 (-> token3)
+ 10.0 * 1.5 + 10.0 * 0.5,
..Default::default()
},
TestHealth1Case { // 9, like 8 but oo_1_2 flips the oo_1_3 target
token1: -100,
token2: -100,
token3: -1,
oo_1_2: (100, 0),
oo_1_3: (10, 1),
expected_health:
// tokens
-100.0 * 1.2 - 100.0 * 5.0 * 1.5 - 10.0 * 1.5
// oo_1_2 (-> token1)
+ 80.0 * 1.2 + 20.0 * 0.8
// oo_1_3 (-> token1)
+ 20.0 * 0.8,
..Default::default()
},
];
for (i, testcase) in testcases.iter().enumerate() {

View File

@ -177,7 +177,7 @@ async fn test_health_compute_serum() -> Result<(), TransportError> {
}
// TODO: actual explicit CU comparisons.
// On 2022-5-25 the final deposit costs 52252 CU and each new market increases it by roughly 4400 CU
// On 2022-6-21 the final deposit costs 54074 CU and each new market increases it by roughly 4500 CU
Ok(())
}