From 68ef58248ce8dc2566f34dc2565e4c3006036a72 Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Mon, 4 Apr 2022 15:17:27 +0200 Subject: [PATCH] Perp health: Some code and comments --- programs/mango-v4/src/state/health.rs | 89 +++++++++++++++++++++- programs/mango-v4/src/state/perp_market.rs | 7 +- 2 files changed, 94 insertions(+), 2 deletions(-) diff --git a/programs/mango-v4/src/state/health.rs b/programs/mango-v4/src/state/health.rs index 07e9e878b..6d4d9651a 100644 --- a/programs/mango-v4/src/state/health.rs +++ b/programs/mango-v4/src/state/health.rs @@ -2,6 +2,7 @@ use anchor_lang::prelude::*; use fixed::types::I80F48; use serum_dex::state::OpenOrders; use std::cell::{Ref, RefMut}; +use std::cmp::min; use std::collections::HashMap; use crate::error::MangoError; @@ -347,6 +348,28 @@ fn health_contribution( Ok(cm!(balance * weight)) } +/// Weigh a perp base balance (in lots) with the appropriate health weight +#[inline(always)] +fn health_weighted_perp_base_lots( + health_type: HealthType, + market: &PerpMarket, + lots: i64, +) -> Result { + let weight = if lots.is_negative() { + match health_type { + HealthType::Init => market.init_liab_weight, + HealthType::Maint => market.maint_liab_weight, + } + } else { + match health_type { + HealthType::Init => market.init_asset_weight, + HealthType::Maint => market.maint_asset_weight, + } + }; + let lots = I80F48::from(lots); + Ok(cm!(weight * lots)) +} + /// Compute health contribution of two tokens - pure convenience #[inline(always)] fn pair_health( @@ -449,7 +472,71 @@ fn compute_health_detail<'a, 'b: 'a>( // health contribution from perp accounts for (i, perp_account) in account.perps.iter_active_accounts().enumerate() { - let _perp_market = retriever.perp_market(&account.group, i, perp_account.market_index)?; + let perp_market = retriever.perp_market(&account.group, i, perp_account.market_index)?; + + // find the TokenInfos for the market's base and quote tokens + let base_index = token_infos + .iter() + .position(|ti| ti.token_index == perp_market.base_token_index) + .ok_or_else(|| error!(MangoError::SomeError))?; + let base_info = &token_infos[base_index]; + + let base_lots = cm!(perp_account.base_position_lots + perp_account.taker_base_lots); + let taker_quote = I80F48::from(cm!( + perp_account.taker_quote_lots * perp_market.quote_lot_size + )); + let quote = cm!(perp_account.quote_position_native + taker_quote); + + // Two scenarios: + // 1. The price goes low and all bids execute, converting to base. + // The health for this case is: + // (weighted(base_lots + bids) - bids) * base_lots * price + quote + // 2. The price goes high and all asks execute, converting to quote. + // The health for this case is: + // (weighted(base_lots - asks) + asks) * base_lots * price + quote + // + // Comparing these makes it clear we need to pick the worse subfactor + // weighted(base_lots + bids) - bids + // or + // weighted(base_lots - asks) + asks + let weighted_base_lots_bids = health_weighted_perp_base_lots( + health_type, + &perp_market, + cm!(base_lots + perp_account.bids_base_lots), + )?; + let bids_base_lots = I80F48::from(perp_account.bids_base_lots); + let scenario1 = cm!(weighted_base_lots_bids - bids_base_lots); + let weighted_base_lots_asks = health_weighted_perp_base_lots( + health_type, + &perp_market, + cm!(base_lots - perp_account.asks_base_lots), + )?; + let asks_base_lots = I80F48::from(perp_account.asks_base_lots); + let scenario2 = cm!(weighted_base_lots_asks + asks_base_lots); + let worse_scenario = min(scenario1, scenario2); + let base_lot_size = I80F48::from(perp_market.base_lot_size); + let _health = cm!(worse_scenario * base_lot_size * base_info.oracle_price + quote); + + // The above choice between scenario1 and 2 depends on the asset_weight and + // liab weight. Thus it needs to be redone for init and maint health. + // + // The condition for the choice to be the same is: + // (1 - init_asset_weight) / (init_liab_weight - 1) + // == (1 - maint_asset_weight) / (maint_liab_weight - 1) + // + // Which can be derived by noticing that health for both scenarios is + // weighted(x) + y - x + // and that the only interesting case is + // asks_net = base_lots - asks < 0 and + // bids_net = base_lots + bids > 0. + // Then + // health_bids_scenario < health_asks_scenario + // iff (asset_weight - 1) * bids_net < (liab_weight - 1) * asks_net + // iff (1 - asset_weightt) / (liab_weight - 1) bids_net > abs(asks_net) + + // Probably the resolution here is to go to v3's assumption that there's an x + // such that asset_weight = 1-x and liab_weight = 1+x. + // This is ok as long as perp markets are strictly isolated. } Ok(HealthCache { token_infos }) diff --git a/programs/mango-v4/src/state/perp_market.rs b/programs/mango-v4/src/state/perp_market.rs index 8a88d6340..1fc1e3abe 100644 --- a/programs/mango-v4/src/state/perp_market.rs +++ b/programs/mango-v4/src/state/perp_market.rs @@ -27,11 +27,14 @@ pub struct PerpMarket { /// UI position is 1 pub base_lot_size: i64, - // TODO docs + // These weights apply to the base asset, the quote token is always assumed to be + // the health-reference token and have 1 for price and weights pub maint_asset_weight: I80F48, pub init_asset_weight: I80F48, pub maint_liab_weight: I80F48, pub init_liab_weight: I80F48, + + // TODO docs pub liquidation_fee: I80F48, pub maker_fee: I80F48, pub taker_fee: I80F48, @@ -61,6 +64,8 @@ pub struct PerpMarket { /// Lookup indices pub perp_market_index: PerpMarketIndex, pub base_token_index: TokenIndex, + + /// Cannot be chosen freely, must be the health-reference token, same for all PerpMarkets pub quote_token_index: TokenIndex, }