225 lines
8.7 KiB
TypeScript
225 lines
8.7 KiB
TypeScript
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;
|
|
}
|