From bc4c57911ae766edf9e1934370955c32c2ccd68c Mon Sep 17 00:00:00 2001 From: microwavedcola1 <89031858+microwavedcola1@users.noreply.github.com> Date: Wed, 13 Jul 2022 19:18:55 +0200 Subject: [PATCH] Health from health components in ts/client (#104) --- programs/mango-v4/src/events.rs | 4 +- .../src/instructions/compute_account_data.rs | 3 +- programs/mango-v4/src/state/health.rs | 10 +- ts/client/src/accounts/healthCache.ts | 224 ++++++++++++ ts/client/src/accounts/mangoAccount.ts | 4 + ts/client/src/mango_v4.ts | 318 ++++++++++++++++++ ts/client/src/scripts/example1-user.ts | 23 +- 7 files changed, 578 insertions(+), 8 deletions(-) create mode 100644 ts/client/src/accounts/healthCache.ts diff --git a/programs/mango-v4/src/events.rs b/programs/mango-v4/src/events.rs index 21a43c41b..9de2e9b9e 100644 --- a/programs/mango-v4/src/events.rs +++ b/programs/mango-v4/src/events.rs @@ -1,11 +1,11 @@ use anchor_lang::prelude::*; use fixed::types::I80F48; -use crate::state::{PerpMarketIndex, TokenIndex}; +use crate::state::{HealthCache, PerpMarketIndex, TokenIndex}; #[event] -#[derive(Debug)] pub struct MangoAccountData { + pub health_cache: HealthCache, pub init_health: I80F48, pub maint_health: I80F48, pub equity: Equity, diff --git a/programs/mango-v4/src/instructions/compute_account_data.rs b/programs/mango-v4/src/instructions/compute_account_data.rs index 0b3d226cb..e774c83be 100644 --- a/programs/mango-v4/src/instructions/compute_account_data.rs +++ b/programs/mango-v4/src/instructions/compute_account_data.rs @@ -24,9 +24,10 @@ pub fn compute_account_data(ctx: Context) -> Result<()> { let equity = compute_equity(&account, &account_retriever)?; emit!(MangoAccountData { + health_cache, init_health, maint_health, - equity + equity, }); Ok(()) diff --git a/programs/mango-v4/src/state/health.rs b/programs/mango-v4/src/state/health.rs index 41068d774..29f0af7cd 100644 --- a/programs/mango-v4/src/state/health.rs +++ b/programs/mango-v4/src/state/health.rs @@ -410,7 +410,8 @@ pub fn compute_health( Ok(new_health_cache(account, retriever)?.health(health_type)) } -struct TokenInfo { +#[derive(AnchorDeserialize, AnchorSerialize)] +pub struct TokenInfo { token_index: TokenIndex, maint_asset_weight: I80F48, init_asset_weight: I80F48, @@ -451,7 +452,8 @@ impl TokenInfo { } } -struct Serum3Info { +#[derive(AnchorSerialize, AnchorDeserialize)] +pub struct Serum3Info { reserved: I80F48, base_index: usize, quote_index: usize, @@ -496,7 +498,8 @@ impl Serum3Info { } } -struct PerpInfo { +#[derive(AnchorSerialize, AnchorDeserialize)] +pub struct PerpInfo { maint_asset_weight: I80F48, init_asset_weight: I80F48, maint_liab_weight: I80F48, @@ -535,6 +538,7 @@ impl PerpInfo { } } +#[derive(AnchorSerialize, AnchorDeserialize)] pub struct HealthCache { token_infos: Vec, serum3_infos: Vec, diff --git a/ts/client/src/accounts/healthCache.ts b/ts/client/src/accounts/healthCache.ts new file mode 100644 index 000000000..06ac7f470 --- /dev/null +++ b/ts/client/src/accounts/healthCache.ts @@ -0,0 +1,224 @@ +import { I80F48, I80F48Dto, ZERO_I80F48 } from './I80F48'; +import { HealthType } from './mangoAccount'; + +// ░░░░ +// +// ██ +// ██░░██ +// ░░ ░░ ██░░░░░░██ ░░░░ +// ██░░░░░░░░░░██ +// ██░░░░░░░░░░██ +// ██░░░░░░░░░░░░░░██ +// ██░░░░░░██████░░░░░░██ +// ██░░░░░░██████░░░░░░██ +// ██░░░░░░░░██████░░░░░░░░██ +// ██░░░░░░░░██████░░░░░░░░██ +// ██░░░░░░░░░░██████░░░░░░░░░░██ +// ██░░░░░░░░░░░░██████░░░░░░░░░░░░██ +// ██░░░░░░░░░░░░██████░░░░░░░░░░░░██ +// ██░░░░░░░░░░░░░░██████░░░░░░░░░░░░░░██ +// ██░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░██ +// ██░░░░░░░░░░░░░░░░██████░░░░░░░░░░░░░░░░██ +// ██░░░░░░░░░░░░░░░░██████░░░░░░░░░░░░░░░░██ +// ██░░░░░░░░░░░░░░░░░░██████░░░░░░░░░░░░░░░░░░██ +// ░░ ██░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░██ +// ██████████████████████████████████████████ +// warning: this code is copy pasta from rust, keep in sync with health.rs + +export class HealthCache { + tokenInfos: TokenInfo[]; + serum3Infos: Serum3Info[]; + perpInfos: PerpInfo[]; + + constructor(dto: HealthCacheDto) { + this.tokenInfos = dto.tokenInfos.map((dto) => new TokenInfo(dto)); + this.serum3Infos = dto.serum3Infos.map((dto) => new Serum3Info(dto)); + this.perpInfos = dto.perpInfos.map((dto) => new PerpInfo(dto)); + } + + public health(healthType: HealthType): I80F48 { + let health = ZERO_I80F48; + for (const tokenInfo of this.tokenInfos) { + let contrib = tokenInfo.healthContribution(healthType); + health = health.add(contrib); + } + for (const serum3Info of this.serum3Infos) { + let contrib = serum3Info.healthContribution(healthType, this.tokenInfos); + health = health.add(contrib); + } + for (const perpInfo of this.perpInfos) { + let contrib = perpInfo.healthContribution(healthType); + health = health.add(contrib); + } + return health; + } +} + +export class TokenInfo { + constructor(dto: TokenInfoDto) { + this.tokenIndex = dto.tokenIndex; + this.maintAssetWeight = I80F48.from(dto.maintAssetWeight); + this.initAssetWeight = I80F48.from(dto.initAssetWeight); + this.maintLiabWeight = I80F48.from(dto.maintLiabWeight); + this.initLiabWeight = I80F48.from(dto.initLiabWeight); + this.oraclePrice = I80F48.from(dto.oraclePrice); + this.balance = I80F48.from(dto.balance); + this.serum3MaxReserved = I80F48.from(dto.serum3MaxReserved); + } + + tokenIndex: number; + maintAssetWeight: I80F48; + initAssetWeight: I80F48; + maintLiabWeight: I80F48; + initLiabWeight: I80F48; + oraclePrice: I80F48; // native/native + // in health-reference-token native units + balance: I80F48; + // in health-reference-token native units + serum3MaxReserved: I80F48; + + assetWeight(healthType: HealthType): I80F48 { + return healthType == HealthType.init + ? this.initAssetWeight + : this.maintAssetWeight; + } + + liabWeight(healthType: HealthType): I80F48 { + return healthType == HealthType.init + ? this.initLiabWeight + : this.maintLiabWeight; + } + + healthContribution(healthType: HealthType): I80F48 { + return ( + this.balance.isNeg() + ? this.liabWeight(healthType) + : this.assetWeight(healthType) + ).mul(this.balance); + } +} + +export class Serum3Info { + constructor(dto: Serum3InfoDto) { + this.reserved = I80F48.from(dto.reserved); + this.baseIndex = dto.baseIndex; + this.quoteIndex = dto.quoteIndex; + } + + reserved: I80F48; + baseIndex: number; + quoteIndex: number; + + healthContribution(healthType: HealthType, tokenInfos: TokenInfo[]): I80F48 { + let baseInfo = tokenInfos[this.baseIndex]; + let quoteInfo = tokenInfos[this.quoteIndex]; + let reserved = this.reserved; + + if (reserved.isZero()) { + return ZERO_I80F48; + } + + // How much the health would increase if the reserved balance were applied to the passed + // token info? + let computeHealthEffect = function (tokenInfo: TokenInfo) { + // This balance includes all possible reserved funds from markets that relate to the + // token, including this market itself: `reserved` is already included in `max_balance`. + let maxBalance = tokenInfo.balance.add(tokenInfo.serum3MaxReserved); + + // Assuming `reserved` was added to `max_balance` last (because that gives the smallest + // health effects): how much did health change because of it? + let assetPart, liabPart; + if (maxBalance.gte(reserved)) { + assetPart = reserved; + liabPart = ZERO_I80F48; + } else if (maxBalance.isNeg()) { + assetPart = ZERO_I80F48; + liabPart = reserved; + } else { + assetPart = maxBalance; + liabPart = reserved.sub(maxBalance); + } + + let assetWeight = tokenInfo.assetWeight(healthType); + let liabWeight = tokenInfo.liabWeight(healthType); + return assetWeight.mul(assetPart).add(liabWeight.mul(liabPart)); + }; + + let reservedAsBase = computeHealthEffect(baseInfo); + let reservedAsQuote = computeHealthEffect(quoteInfo); + return reservedAsBase.min(reservedAsQuote); + } +} + +export class PerpInfo { + constructor(dto: PerpInfoDto) { + this.maintAssetWeight = I80F48.from(dto.maintAssetWeight); + this.initAssetWeight = I80F48.from(dto.initAssetWeight); + this.maintLiabWeight = I80F48.from(dto.maintLiabWeight); + this.initLiabWeight = I80F48.from(dto.initLiabWeight); + this.base = I80F48.from(dto.base); + this.quote = I80F48.from(dto.quote); + } + maintAssetWeight: I80F48; + initAssetWeight: I80F48; + maintLiabWeight: I80F48; + initLiabWeight: I80F48; + // in health-reference-token native units, needs scaling by asset/liab + base: I80F48; + // in health-reference-token native units, no asset/liab factor needed + quote: I80F48; + + healthContribution(healthType: HealthType): I80F48 { + let weight; + if (healthType == HealthType.init && this.base.isNeg()) { + weight = this.initLiabWeight; + } else if (healthType == HealthType.init && !this.base.isNeg()) { + weight = this.initAssetWeight; + } + if (healthType == HealthType.maint && this.base.isNeg()) { + weight = this.maintLiabWeight; + } + if (healthType == HealthType.maint && !this.base.isNeg()) { + weight = this.maintAssetWeight; + } + + // FUTURE: Allow v3-style "reliable" markets where we can return + // `self.quote + weight * self.base` here + return this.quote.add(weight.mul(this.base)).min(ZERO_I80F48); + } +} + +export class HealthCacheDto { + tokenInfos: TokenInfoDto[]; + serum3Infos: Serum3InfoDto[]; + perpInfos: PerpInfoDto[]; +} +export class TokenInfoDto { + tokenIndex: number; + maintAssetWeight: I80F48Dto; + initAssetWeight: I80F48Dto; + maintLiabWeight: I80F48Dto; + initLiabWeight: I80F48Dto; + oraclePrice: I80F48Dto; // native/native + // in health-reference-token native units + balance: I80F48Dto; + // in health-reference-token native units + serum3MaxReserved: I80F48Dto; +} + +export class Serum3InfoDto { + reserved: I80F48Dto; + baseIndex: number; + quoteIndex: number; +} + +export class PerpInfoDto { + maintAssetWeight: I80F48Dto; + initAssetWeight: I80F48Dto; + maintLiabWeight: I80F48Dto; + initLiabWeight: I80F48Dto; + // in health-reference-token native units, needs scaling by asset/liab + base: I80F48Dto; + // in health-reference-token native units, no asset/liab factor needed + quote: I80F48Dto; +} diff --git a/ts/client/src/accounts/mangoAccount.ts b/ts/client/src/accounts/mangoAccount.ts index ae3958920..f464d5297 100644 --- a/ts/client/src/accounts/mangoAccount.ts +++ b/ts/client/src/accounts/mangoAccount.ts @@ -5,6 +5,7 @@ import { MangoClient } from '../client'; import { nativeI80F48ToUi } from '../utils'; import { Bank, QUOTE_DECIMALS } from './bank'; import { Group } from './group'; +import { HealthCache, HealthCacheDto } from './healthCache'; import { I80F48, I80F48Dto, ONE_I80F48, ZERO_I80F48 } from './I80F48'; export class MangoAccount { public tokens: TokenPosition[]; @@ -422,12 +423,14 @@ export class HealthType { export class MangoAccountData { constructor( + public healthCache: HealthCache, public initHealth: I80F48, public maintHealth: I80F48, public equity: Equity, ) {} static from(event: { + healthCache: HealthCacheDto; initHealth: I80F48Dto; maintHealth: I80F48Dto; equity: { @@ -438,6 +441,7 @@ export class MangoAccountData { tokenAssets: any; }) { return new MangoAccountData( + new HealthCache(event.healthCache), I80F48.from(event.initHealth), I80F48.from(event.maintHealth), Equity.from(event.equity), diff --git a/ts/client/src/mango_v4.ts b/ts/client/src/mango_v4.ts index 071f0e09c..9e1baf983 100644 --- a/ts/client/src/mango_v4.ts +++ b/ts/client/src/mango_v4.ts @@ -3427,6 +3427,158 @@ export type MangoV4 = { ] } }, + { + "name": "TokenInfo", + "type": { + "kind": "struct", + "fields": [ + { + "name": "tokenIndex", + "type": "u16" + }, + { + "name": "maintAssetWeight", + "type": { + "defined": "I80F48" + } + }, + { + "name": "initAssetWeight", + "type": { + "defined": "I80F48" + } + }, + { + "name": "maintLiabWeight", + "type": { + "defined": "I80F48" + } + }, + { + "name": "initLiabWeight", + "type": { + "defined": "I80F48" + } + }, + { + "name": "oraclePrice", + "type": { + "defined": "I80F48" + } + }, + { + "name": "balance", + "type": { + "defined": "I80F48" + } + }, + { + "name": "serum3MaxReserved", + "type": { + "defined": "I80F48" + } + } + ] + } + }, + { + "name": "Serum3Info", + "type": { + "kind": "struct", + "fields": [ + { + "name": "reserved", + "type": { + "defined": "I80F48" + } + }, + { + "name": "baseIndex", + "type": "u64" + }, + { + "name": "quoteIndex", + "type": "u64" + } + ] + } + }, + { + "name": "PerpInfo", + "type": { + "kind": "struct", + "fields": [ + { + "name": "maintAssetWeight", + "type": { + "defined": "I80F48" + } + }, + { + "name": "initAssetWeight", + "type": { + "defined": "I80F48" + } + }, + { + "name": "maintLiabWeight", + "type": { + "defined": "I80F48" + } + }, + { + "name": "initLiabWeight", + "type": { + "defined": "I80F48" + } + }, + { + "name": "base", + "type": { + "defined": "I80F48" + } + }, + { + "name": "quote", + "type": { + "defined": "I80F48" + } + } + ] + } + }, + { + "name": "HealthCache", + "type": { + "kind": "struct", + "fields": [ + { + "name": "tokenInfos", + "type": { + "vec": { + "defined": "TokenInfo" + } + } + }, + { + "name": "serum3Infos", + "type": { + "vec": { + "defined": "Serum3Info" + } + } + }, + { + "name": "perpInfos", + "type": { + "vec": { + "defined": "PerpInfo" + } + } + } + ] + } + }, { "name": "TokenPosition", "type": { @@ -4046,6 +4198,13 @@ export type MangoV4 = { { "name": "MangoAccountData", "fields": [ + { + "name": "healthCache", + "type": { + "defined": "HealthCache" + }, + "index": false + }, { "name": "initHealth", "type": { @@ -8048,6 +8207,158 @@ export const IDL: MangoV4 = { ] } }, + { + "name": "TokenInfo", + "type": { + "kind": "struct", + "fields": [ + { + "name": "tokenIndex", + "type": "u16" + }, + { + "name": "maintAssetWeight", + "type": { + "defined": "I80F48" + } + }, + { + "name": "initAssetWeight", + "type": { + "defined": "I80F48" + } + }, + { + "name": "maintLiabWeight", + "type": { + "defined": "I80F48" + } + }, + { + "name": "initLiabWeight", + "type": { + "defined": "I80F48" + } + }, + { + "name": "oraclePrice", + "type": { + "defined": "I80F48" + } + }, + { + "name": "balance", + "type": { + "defined": "I80F48" + } + }, + { + "name": "serum3MaxReserved", + "type": { + "defined": "I80F48" + } + } + ] + } + }, + { + "name": "Serum3Info", + "type": { + "kind": "struct", + "fields": [ + { + "name": "reserved", + "type": { + "defined": "I80F48" + } + }, + { + "name": "baseIndex", + "type": "u64" + }, + { + "name": "quoteIndex", + "type": "u64" + } + ] + } + }, + { + "name": "PerpInfo", + "type": { + "kind": "struct", + "fields": [ + { + "name": "maintAssetWeight", + "type": { + "defined": "I80F48" + } + }, + { + "name": "initAssetWeight", + "type": { + "defined": "I80F48" + } + }, + { + "name": "maintLiabWeight", + "type": { + "defined": "I80F48" + } + }, + { + "name": "initLiabWeight", + "type": { + "defined": "I80F48" + } + }, + { + "name": "base", + "type": { + "defined": "I80F48" + } + }, + { + "name": "quote", + "type": { + "defined": "I80F48" + } + } + ] + } + }, + { + "name": "HealthCache", + "type": { + "kind": "struct", + "fields": [ + { + "name": "tokenInfos", + "type": { + "vec": { + "defined": "TokenInfo" + } + } + }, + { + "name": "serum3Infos", + "type": { + "vec": { + "defined": "Serum3Info" + } + } + }, + { + "name": "perpInfos", + "type": { + "vec": { + "defined": "PerpInfo" + } + } + } + ] + } + }, { "name": "TokenPosition", "type": { @@ -8667,6 +8978,13 @@ export const IDL: MangoV4 = { { "name": "MangoAccountData", "fields": [ + { + "name": "healthCache", + "type": { + "defined": "HealthCache" + }, + "index": false + }, { "name": "initHealth", "type": { diff --git a/ts/client/src/scripts/example1-user.ts b/ts/client/src/scripts/example1-user.ts index d7a8634cd..33caecc39 100644 --- a/ts/client/src/scripts/example1-user.ts +++ b/ts/client/src/scripts/example1-user.ts @@ -1,6 +1,7 @@ import { AnchorProvider, Wallet } from '@project-serum/anchor'; import { Connection, Keypair, PublicKey } from '@solana/web3.js'; import fs from 'fs'; +import { HealthType } from '../accounts/mangoAccount'; import { OrderType, Side } from '../accounts/perp'; import { Serum3OrderType, @@ -67,12 +68,22 @@ async function main() { const randomKey = new PublicKey( '4ZkS7ZZkxfsC3GtvvsHP3DFcUeByU9zzZELS4r8HCELo', ); - await client.editMangoAccount(group, mangoAccount, 'my_changed_name', randomKey); + await client.editMangoAccount( + group, + mangoAccount, + 'my_changed_name', + randomKey, + ); await mangoAccount.reload(client, group); console.log(mangoAccount.toString()); console.log(`...resetting mango account name, and re-setting a delegate`); - await client.editMangoAccount(group, mangoAccount, 'my_mango_account', PublicKey.default); + await client.editMangoAccount( + group, + mangoAccount, + 'my_mango_account', + PublicKey.default, + ); await mangoAccount.reload(client, group); console.log(mangoAccount.toString()); } @@ -199,6 +210,14 @@ async function main() { '...mangoAccount.getCollateralValue() ' + toUiDecimals(mangoAccount.getCollateralValue().toNumber()), ); + console.log( + '...mangoAccount.accountData["healthCache"].health(HealthType.init) ' + + toUiDecimals( + mangoAccount.accountData['healthCache'] + .health(HealthType.init) + .toNumber(), + ), + ); console.log( '...mangoAccount.getAssetsVal() ' + toUiDecimals(mangoAccount.getAssetsVal().toNumber()),