mango-v4/ts/client/src/accounts/healthCache.ts

870 lines
29 KiB
TypeScript

import { PublicKey } from '@solana/web3.js';
import _ from 'lodash';
import { Bank } from './bank';
import { Group } from './group';
import {
HUNDRED_I80F48,
I80F48,
I80F48Dto,
MAX_I80F48,
ONE_I80F48,
ZERO_I80F48,
} from './I80F48';
import { HealthType } from './mangoAccount';
import { Serum3Market, Serum3Side } from './serum3';
// ░░░░
//
// ██
// ██░░██
// ░░ ░░ ██░░░░░░██ ░░░░
// ██░░░░░░░░░░██
// ██░░░░░░░░░░██
// ██░░░░░░░░░░░░░░██
// ██░░░░░░██████░░░░░░██
// ██░░░░░░██████░░░░░░██
// ██░░░░░░░░██████░░░░░░░░██
// ██░░░░░░░░██████░░░░░░░░██
// ██░░░░░░░░░░██████░░░░░░░░░░██
// ██░░░░░░░░░░░░██████░░░░░░░░░░░░██
// ██░░░░░░░░░░░░██████░░░░░░░░░░░░██
// ██░░░░░░░░░░░░░░██████░░░░░░░░░░░░░░██
// ██░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░██
// ██░░░░░░░░░░░░░░░░██████░░░░░░░░░░░░░░░░██
// ██░░░░░░░░░░░░░░░░██████░░░░░░░░░░░░░░░░██
// ██░░░░░░░░░░░░░░░░░░██████░░░░░░░░░░░░░░░░░░██
// ░░ ██░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░██
// ██████████████████████████████████████████
// warning: this code is copy pasta from rust, keep in sync with health.rs
export class HealthCache {
constructor(
public tokenInfos: TokenInfo[],
public serum3Infos: Serum3Info[],
public perpInfos: PerpInfo[],
) {}
static fromDto(dto) {
return new HealthCache(
dto.tokenInfos.map((dto) => TokenInfo.fromDto(dto)),
dto.serum3Infos.map((dto) => Serum3Info.fromDto(dto)),
dto.perpInfos.map((dto) => new PerpInfo(dto)),
);
}
public health(healthType: HealthType): I80F48 {
let health = ZERO_I80F48;
for (const tokenInfo of this.tokenInfos) {
const contrib = tokenInfo.healthContribution(healthType);
health = health.add(contrib);
}
for (const serum3Info of this.serum3Infos) {
const contrib = serum3Info.healthContribution(
healthType,
this.tokenInfos,
);
health = health.add(contrib);
}
for (const perpInfo of this.perpInfos) {
const contrib = perpInfo.healthContribution(healthType);
health = health.add(contrib);
}
return health;
}
public assets(healthType: HealthType): I80F48 {
let assets = ZERO_I80F48;
for (const tokenInfo of this.tokenInfos) {
const contrib = tokenInfo.healthContribution(healthType);
if (contrib.isPos()) {
assets = assets.add(contrib);
}
}
for (const serum3Info of this.serum3Infos) {
const contrib = serum3Info.healthContribution(
healthType,
this.tokenInfos,
);
if (contrib.isPos()) {
assets = assets.add(contrib);
}
}
for (const perpInfo of this.perpInfos) {
const contrib = perpInfo.healthContribution(healthType);
if (contrib.isPos()) {
assets = assets.add(contrib);
}
}
return assets;
}
public liabs(healthType: HealthType): I80F48 {
let liabs = ZERO_I80F48;
for (const tokenInfo of this.tokenInfos) {
const contrib = tokenInfo.healthContribution(healthType);
if (contrib.isNeg()) {
liabs = liabs.sub(contrib);
}
}
for (const serum3Info of this.serum3Infos) {
const contrib = serum3Info.healthContribution(
healthType,
this.tokenInfos,
);
if (contrib.isNeg()) {
liabs = liabs.sub(contrib);
}
}
for (const perpInfo of this.perpInfos) {
const contrib = perpInfo.healthContribution(healthType);
if (contrib.isNeg()) {
liabs = liabs.sub(contrib);
}
}
return liabs;
}
public healthRatio(healthType: HealthType): I80F48 {
let assets = ZERO_I80F48;
let liabs = ZERO_I80F48;
for (const tokenInfo of this.tokenInfos) {
const contrib = tokenInfo.healthContribution(healthType);
if (contrib.isPos()) {
assets = assets.add(contrib);
} else {
liabs = liabs.sub(contrib);
}
}
for (const serum3Info of this.serum3Infos) {
const contrib = serum3Info.healthContribution(
healthType,
this.tokenInfos,
);
if (contrib.isPos()) {
assets = assets.add(contrib);
} else {
liabs = liabs.sub(contrib);
}
}
for (const perpInfo of this.perpInfos) {
const contrib = perpInfo.healthContribution(healthType);
if (contrib.isPos()) {
assets = assets.add(contrib);
} else {
liabs = liabs.sub(contrib);
}
}
if (liabs.isPos()) {
return HUNDRED_I80F48.mul(assets.sub(liabs).div(liabs));
} else {
return MAX_I80F48;
}
}
findTokenInfoIndex(tokenIndex: number): number {
return this.tokenInfos.findIndex(
(tokenInfo) => tokenInfo.tokenIndex == tokenIndex,
);
}
getOrCreateTokenInfoIndex(bank: Bank): number {
const index = this.findTokenInfoIndex(bank.tokenIndex);
if (index == -1) {
this.tokenInfos.push(TokenInfo.emptyFromBank(bank));
}
return this.findTokenInfoIndex(bank.tokenIndex);
}
adjustSerum3Reserved(
// todo change indices to types from numbers
marketIndex: number,
baseTokenIndex: number,
reservedBaseChange: I80F48,
freeBaseChange: I80F48,
quoteTokenIndex: number,
reservedQuoteChange: I80F48,
freeQuoteChange: I80F48,
) {
const baseEntryIndex = this.findTokenInfoIndex(baseTokenIndex);
const quoteEntryIndex = this.findTokenInfoIndex(quoteTokenIndex);
let reservedAmount = ZERO_I80F48;
const baseEntry = this.tokenInfos[baseEntryIndex];
reservedAmount = reservedBaseChange.mul(baseEntry.oraclePrice);
const quoteEntry = this.tokenInfos[quoteEntryIndex];
reservedAmount = reservedAmount.add(
reservedQuoteChange.mul(quoteEntry.oraclePrice),
);
// Apply it to the tokens
baseEntry.serum3MaxReserved =
baseEntry.serum3MaxReserved.add(reservedAmount);
baseEntry.balance = baseEntry.balance.add(
freeBaseChange.mul(baseEntry.oraclePrice),
);
quoteEntry.serum3MaxReserved =
quoteEntry.serum3MaxReserved.add(reservedAmount);
quoteEntry.balance = quoteEntry.balance.add(
freeQuoteChange.mul(quoteEntry.oraclePrice),
);
// Apply it to the serum3 info
const serum3Info = this.serum3Infos.find(
(serum3Info) => serum3Info.marketIndex === marketIndex,
);
serum3Info.reserved = serum3Info.reserved.add(reservedAmount);
}
public static logHealthCache(debug: string, healthCache: HealthCache) {
if (debug) console.log(debug);
for (const token of healthCache.tokenInfos) {
console.log(` {token.toString()}`);
}
for (const serum3Info of healthCache.serum3Infos) {
console.log(` {serum3Info.toString(healthCache.tokenInfos)}`);
}
console.log(
` assets ${healthCache.assets(
HealthType.init,
)}, liabs ${healthCache.liabs(HealthType.init)}, `,
);
console.log(
` health(HealthType.init) ${healthCache.health(HealthType.init)}`,
);
console.log(
` healthRatio(HealthType.init) ${healthCache.healthRatio(
HealthType.init,
)}`,
);
}
simHealthRatioWithTokenPositionChanges(
group: Group,
nativeTokenChanges: {
nativeTokenAmount: I80F48;
mintPk: PublicKey;
}[],
healthType: HealthType = HealthType.init,
): I80F48 {
const adjustedCache: HealthCache = _.cloneDeep(this);
// HealthCache.logHealthCache('beforeChange', adjustedCache);
for (const change of nativeTokenChanges) {
const bank: Bank = group.getFirstBankByMint(change.mintPk);
const changeIndex = adjustedCache.getOrCreateTokenInfoIndex(bank);
adjustedCache.tokenInfos[changeIndex].balance = adjustedCache.tokenInfos[
changeIndex
].balance.add(change.nativeTokenAmount.mul(bank.price));
}
// HealthCache.logHealthCache('afterChange', adjustedCache);
return adjustedCache.healthRatio(healthType);
}
simHealthRatioWithSerum3BidChanges(
group: Group,
bidNativeQuoteAmount: I80F48,
serum3Market: Serum3Market,
healthType: HealthType = HealthType.init,
): I80F48 {
const adjustedCache: HealthCache = _.cloneDeep(this);
const quoteBank = group.banksMapByTokenIndex.get(
serum3Market.quoteTokenIndex,
)[0];
const quoteIndex = adjustedCache.getOrCreateTokenInfoIndex(quoteBank);
const quote = adjustedCache.tokenInfos[quoteIndex];
// Move token balance to reserved funds in open orders,
// essentially simulating a place order
// Reduce token balance for quote
adjustedCache.tokenInfos[quoteIndex].balance = adjustedCache.tokenInfos[
quoteIndex
].balance.sub(bidNativeQuoteAmount.mul(quote.oraclePrice));
// Increase reserved in Serum3Info for quote
adjustedCache.adjustSerum3Reserved(
serum3Market.marketIndex,
serum3Market.baseTokenIndex,
ZERO_I80F48,
ZERO_I80F48,
serum3Market.quoteTokenIndex,
bidNativeQuoteAmount,
ZERO_I80F48,
);
return adjustedCache.healthRatio(healthType);
}
simHealthRatioWithSerum3AskChanges(
group: Group,
askNativeBaseAmount: I80F48,
serum3Market: Serum3Market,
healthType: HealthType = HealthType.init,
): I80F48 {
const adjustedCache: HealthCache = _.cloneDeep(this);
const baseBank = group.banksMapByTokenIndex.get(
serum3Market.baseTokenIndex,
)[0];
const baseIndex = adjustedCache.getOrCreateTokenInfoIndex(baseBank);
const base = adjustedCache.tokenInfos[baseIndex];
// Move token balance to reserved funds in open orders,
// essentially simulating a place order
// Reduce token balance for base
adjustedCache.tokenInfos[baseIndex].balance = adjustedCache.tokenInfos[
baseIndex
].balance.sub(askNativeBaseAmount.mul(base.oraclePrice));
// Increase reserved in Serum3Info for base
adjustedCache.adjustSerum3Reserved(
serum3Market.marketIndex,
serum3Market.baseTokenIndex,
askNativeBaseAmount,
ZERO_I80F48,
serum3Market.quoteTokenIndex,
ZERO_I80F48,
ZERO_I80F48,
);
return adjustedCache.healthRatio(healthType);
}
private static binaryApproximationSearch(
left: I80F48,
leftRatio: I80F48,
right: I80F48,
rightRatio: I80F48,
targetRatio: I80F48,
healthRatioAfterActionFn: (I80F48) => I80F48,
) {
const maxIterations = 40;
// TODO: make relative to health ratio decimals? Might be over engineering
const targetError = I80F48.fromNumber(0.001);
if (
(leftRatio.sub(targetRatio).isPos() &&
rightRatio.sub(targetRatio).isPos()) ||
(leftRatio.sub(targetRatio).isNeg() &&
rightRatio.sub(targetRatio).isNeg())
) {
throw new Error(
`internal error: left ${leftRatio.toNumber()} and right ${rightRatio.toNumber()} don't contain the target value ${targetRatio.toNumber()}`,
);
}
let newAmount;
for (const key of Array(maxIterations).fill(0).keys()) {
newAmount = left.add(right).mul(I80F48.fromNumber(0.5));
const newAmountRatio = healthRatioAfterActionFn(newAmount);
const error = newAmountRatio.sub(targetRatio);
if (error.isPos() && error.lt(targetError)) {
return newAmount;
}
if (newAmountRatio.gt(targetRatio) != rightRatio.gt(targetRatio)) {
left = newAmount;
} else {
right = newAmount;
rightRatio = newAmountRatio;
}
}
console.error(
`Unable to get targetRatio within ${maxIterations} iterations`,
);
return newAmount;
}
getMaxSourceForTokenSwap(
group: Group,
sourceMintPk: PublicKey,
targetMintPk: PublicKey,
minRatio: I80F48,
): I80F48 {
const sourceBank: Bank = group.getFirstBankByMint(sourceMintPk);
const targetBank: Bank = group.getFirstBankByMint(targetMintPk);
if (sourceMintPk.equals(targetMintPk)) {
return ZERO_I80F48;
}
if (!sourceBank.price || sourceBank.price.lte(ZERO_I80F48)) {
return ZERO_I80F48;
}
if (
sourceBank.initLiabWeight
.sub(targetBank.initAssetWeight)
.abs()
.lte(ZERO_I80F48)
) {
return ZERO_I80F48;
}
// The health_ratio is a nonlinear based on swap amount.
// For large swap amounts the slope is guaranteed to be negative, but small amounts
// can have positive slope (e.g. using source deposits to pay back target borrows).
//
// That means:
// - even if the initial ratio is < minRatio it can be useful to swap to *increase* health
// - be careful about finding the minRatio point: the function isn't convex
const initialRatio = this.healthRatio(HealthType.init);
if (initialRatio.lte(ZERO_I80F48)) {
return ZERO_I80F48;
}
const healthCacheClone: HealthCache = _.cloneDeep(this);
const sourceIndex = healthCacheClone.getOrCreateTokenInfoIndex(sourceBank);
const targetIndex = healthCacheClone.getOrCreateTokenInfoIndex(targetBank);
const source = healthCacheClone.tokenInfos[sourceIndex];
const target = healthCacheClone.tokenInfos[targetIndex];
// There are two key slope changes: Assume source.balance > 0 and target.balance < 0. Then
// initially health ratio goes up. When one of balances flips sign, the health ratio slope
// may be positive or negative for a bit, until both balances have flipped and the slope is
// negative.
// The maximum will be at one of these points (ignoring serum3 effects).
function cacheAfterSwap(amount: I80F48) {
const adjustedCache: HealthCache = _.cloneDeep(healthCacheClone);
// HealthCache.logHealthCache('beforeSwap', adjustedCache);
adjustedCache.tokenInfos[sourceIndex].balance =
adjustedCache.tokenInfos[sourceIndex].balance.sub(amount);
adjustedCache.tokenInfos[targetIndex].balance =
adjustedCache.tokenInfos[targetIndex].balance.add(amount);
// HealthCache.logHealthCache('afterSwap', adjustedCache);
return adjustedCache;
}
function healthRatioAfterSwap(amount: I80F48): I80F48 {
return cacheAfterSwap(amount).healthRatio(HealthType.init);
}
const point0Amount = source.balance
.min(target.balance.neg())
.max(ZERO_I80F48);
const point1Amount = source.balance
.max(target.balance.neg())
.max(ZERO_I80F48);
const cache0 = cacheAfterSwap(point0Amount);
const point0Ratio = cache0.healthRatio(HealthType.init);
const cache1 = cacheAfterSwap(point1Amount);
const point1Ratio = cache1.healthRatio(HealthType.init);
const point1Health = cache1.health(HealthType.init);
let amount: I80F48;
if (
initialRatio.lte(minRatio) &&
point0Ratio.lt(minRatio) &&
point1Ratio.lt(minRatio)
) {
// If we have to stay below the target ratio, pick the highest one
if (point0Ratio.gt(initialRatio)) {
if (point1Ratio.gt(point0Ratio)) {
amount = point1Amount;
} else {
amount = point0Amount;
}
} else if (point1Ratio.gt(initialRatio)) {
amount = point1Amount;
} else {
amount = ZERO_I80F48;
}
} else if (point1Ratio.gte(minRatio)) {
// If point1Ratio is still bigger than minRatio, the target amount must be >point1Amount
// search to the right of point1Amount: but how far?
// At point1, source.balance < 0 and target.balance > 0, so use a simple estimation for
// zero health: health - source_liab_weight * a + target_asset_weight * a = 0.
if (point1Health.lte(ZERO_I80F48)) {
return ZERO_I80F48;
}
const zeroHealthAmount = point1Amount.add(
point1Health.div(source.initLiabWeight.sub(target.initAssetWeight)),
);
const zeroHealthRatio = healthRatioAfterSwap(zeroHealthAmount);
amount = HealthCache.binaryApproximationSearch(
point1Amount,
point1Ratio,
zeroHealthAmount,
zeroHealthRatio,
minRatio,
healthRatioAfterSwap,
);
} else if (point0Ratio.gte(minRatio)) {
// Must be between point0Amount and point1Amount.
amount = HealthCache.binaryApproximationSearch(
point0Amount,
point0Ratio,
point1Amount,
point1Ratio,
minRatio,
healthRatioAfterSwap,
);
} else {
throw new Error(
`internal error: assert that init ratio ${initialRatio.toNumber()} <= point0 ratio ${point0Ratio.toNumber()}`,
);
}
return amount
.div(source.oraclePrice)
.div(
ONE_I80F48.add(
group.getFirstBankByMint(sourceMintPk).loanOriginationFeeRate,
),
);
}
getMaxForSerum3Order(
group: Group,
serum3Market: Serum3Market,
side: Serum3Side,
minRatio: I80F48,
) {
const baseBank = group.banksMapByTokenIndex.get(
serum3Market.baseTokenIndex,
)[0];
const quoteBank = group.banksMapByTokenIndex.get(
serum3Market.quoteTokenIndex,
)[0];
const healthCacheClone: HealthCache = _.cloneDeep(this);
const baseIndex = healthCacheClone.getOrCreateTokenInfoIndex(baseBank);
const quoteIndex = healthCacheClone.getOrCreateTokenInfoIndex(quoteBank);
const base = healthCacheClone.tokenInfos[baseIndex];
const quote = healthCacheClone.tokenInfos[quoteIndex];
// Binary search between current health (0 sized new order) and
// an amount to trade which will bring health to 0.
// Current health and amount i.e. 0
const initialAmount = ZERO_I80F48;
const initialHealth = this.health(HealthType.init);
const initialRatio = this.healthRatio(HealthType.init);
if (initialRatio.lte(ZERO_I80F48)) {
return ZERO_I80F48;
}
// Amount which would bring health to 0
// amount = max(A_deposits, B_borrows) + init_health / (A_liab_weight - B_asset_weight)
// A is what we would be essentially swapping for B
// So when its an ask, then base->quote,
// and when its a bid, then quote->bid
let zeroAmount;
if (side == Serum3Side.ask) {
const quoteBorrows = quote.balance.lt(ZERO_I80F48)
? quote.balance.abs()
: ZERO_I80F48;
zeroAmount = base.balance
.max(quoteBorrows)
.add(
initialHealth.div(
base
.liabWeight(HealthType.init)
.sub(quote.assetWeight(HealthType.init)),
),
);
} else {
const baseBorrows = base.balance.lt(ZERO_I80F48)
? base.balance.abs()
: ZERO_I80F48;
zeroAmount = quote.balance
.max(baseBorrows)
.add(
initialHealth.div(
quote
.liabWeight(HealthType.init)
.sub(base.assetWeight(HealthType.init)),
),
);
}
const cache = cacheAfterPlacingOrder(zeroAmount);
const zeroAmountRatio = cache.healthRatio(HealthType.init);
function cacheAfterPlacingOrder(amount: I80F48) {
const adjustedCache: HealthCache = _.cloneDeep(healthCacheClone);
side === Serum3Side.ask
? (adjustedCache.tokenInfos[baseIndex].balance =
adjustedCache.tokenInfos[baseIndex].balance.sub(amount))
: (adjustedCache.tokenInfos[quoteIndex].balance =
adjustedCache.tokenInfos[quoteIndex].balance.sub(amount));
adjustedCache.adjustSerum3Reserved(
serum3Market.marketIndex,
serum3Market.baseTokenIndex,
side === Serum3Side.ask ? amount.div(base.oraclePrice) : ZERO_I80F48,
ZERO_I80F48,
serum3Market.quoteTokenIndex,
side === Serum3Side.bid ? amount.div(quote.oraclePrice) : ZERO_I80F48,
ZERO_I80F48,
);
return adjustedCache;
}
function healthRatioAfterPlacingOrder(amount: I80F48): I80F48 {
return cacheAfterPlacingOrder(amount).healthRatio(HealthType.init);
}
const amount = HealthCache.binaryApproximationSearch(
initialAmount,
initialRatio,
zeroAmount,
zeroAmountRatio,
minRatio,
healthRatioAfterPlacingOrder,
);
// If its a bid then the reserved fund and potential loan is in quote,
// If its a ask then the reserved fund and potential loan is in base,
// also keep some buffer for fees, use taker fees for worst case simulation.
return side === Serum3Side.bid
? amount
.div(quote.oraclePrice)
.div(ONE_I80F48.add(baseBank.loanOriginationFeeRate))
.div(ONE_I80F48.add(I80F48.fromNumber(group.getFeeRate(false))))
: amount
.div(base.oraclePrice)
.div(ONE_I80F48.add(quoteBank.loanOriginationFeeRate))
.div(ONE_I80F48.add(I80F48.fromNumber(group.getFeeRate(false))));
}
}
export class TokenInfo {
constructor(
public tokenIndex: number,
public maintAssetWeight: I80F48,
public initAssetWeight: I80F48,
public maintLiabWeight: I80F48,
public initLiabWeight: I80F48,
// native/native
public oraclePrice: I80F48,
// in health-reference-token native units
public balance: I80F48,
// in health-reference-token native units
public serum3MaxReserved: I80F48,
) {}
static fromDto(dto: TokenInfoDto): TokenInfo {
return new TokenInfo(
dto.tokenIndex,
I80F48.from(dto.maintAssetWeight),
I80F48.from(dto.initAssetWeight),
I80F48.from(dto.maintLiabWeight),
I80F48.from(dto.initLiabWeight),
I80F48.from(dto.oraclePrice),
I80F48.from(dto.balance),
I80F48.from(dto.serum3MaxReserved),
);
}
static emptyFromBank(bank: Bank): TokenInfo {
return new TokenInfo(
bank.tokenIndex,
bank.maintAssetWeight,
bank.initAssetWeight,
bank.maintLiabWeight,
bank.initLiabWeight,
bank.price,
ZERO_I80F48,
ZERO_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);
}
toString() {
return ` tokenIndex: ${this.tokenIndex}, balance: ${
this.balance
}, serum3MaxReserved: ${
this.serum3MaxReserved
}, initHealth ${this.healthContribution(HealthType.init)}`;
}
}
export class Serum3Info {
constructor(
public reserved: I80F48,
public baseIndex: number,
public quoteIndex: number,
public marketIndex: number,
) {}
static fromDto(dto: Serum3InfoDto) {
return new Serum3Info(
I80F48.from(dto.reserved),
dto.baseIndex,
dto.quoteIndex,
dto.marketIndex,
);
}
healthContribution(healthType: HealthType, tokenInfos: TokenInfo[]): I80F48 {
const baseInfo = tokenInfos[this.baseIndex];
const quoteInfo = tokenInfos[this.quoteIndex];
const 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?
const 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`.
const 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);
}
const assetWeight = tokenInfo.assetWeight(healthType);
const liabWeight = tokenInfo.liabWeight(healthType);
return assetWeight.mul(assetPart).add(liabWeight.mul(liabPart));
};
const reservedAsBase = computeHealthEffect(baseInfo);
const reservedAsQuote = computeHealthEffect(quoteInfo);
return reservedAsBase.min(reservedAsQuote);
}
toString(tokenInfos: TokenInfo[]) {
return ` marketIndex: ${this.marketIndex}, baseIndex: ${
this.baseIndex
}, quoteIndex: ${this.quoteIndex}, reserved: ${
this.reserved
}, initHealth ${this.healthContribution(HealthType.init, tokenInfos)}`;
}
}
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;
constructor(
tokenIndex: number,
maintAssetWeight: I80F48Dto,
initAssetWeight: I80F48Dto,
maintLiabWeight: I80F48Dto,
initLiabWeight: I80F48Dto,
oraclePrice: I80F48Dto,
balance: I80F48Dto,
serum3MaxReserved: I80F48Dto,
) {
this.tokenIndex = tokenIndex;
this.maintAssetWeight = maintAssetWeight;
this.initAssetWeight = initAssetWeight;
this.maintLiabWeight = maintLiabWeight;
this.initLiabWeight = initLiabWeight;
this.oraclePrice = oraclePrice;
this.balance = balance;
this.serum3MaxReserved = serum3MaxReserved;
}
}
export class Serum3InfoDto {
reserved: I80F48Dto;
baseIndex: number;
quoteIndex: number;
marketIndex: number;
constructor(reserved: I80F48Dto, baseIndex: number, quoteIndex: number) {
this.reserved = reserved;
this.baseIndex = baseIndex;
this.quoteIndex = quoteIndex;
}
}
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;
}