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

1717 lines
55 KiB
TypeScript

import { BN } from '@coral-xyz/anchor';
import { OpenOrders } from '@project-serum/serum';
import { PublicKey } from '@solana/web3.js';
import cloneDeep from 'lodash/cloneDeep';
import {
HUNDRED_I80F48,
I80F48,
I80F48Dto,
MAX_I80F48,
ONE_I80F48,
ZERO_I80F48,
} from '../numbers/I80F48';
import { toNativeI80F48ForQuote } from '../utils';
import { Bank, BankForHealth, TokenIndex } from './bank';
import { Group } from './group';
import { HealthType, MangoAccount, PerpPosition } from './mangoAccount';
import { PerpMarket, PerpOrderSide } from './perp';
import { MarketIndex, 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 fromMangoAccount(
group: Group,
mangoAccount: MangoAccount,
): HealthCache {
// token contribution from token accounts
const tokenInfos = mangoAccount.tokensActive().map((tokenPosition) => {
const bank = group.getFirstBankByTokenIndex(tokenPosition.tokenIndex);
return TokenInfo.fromBank(bank, tokenPosition.balance(bank));
});
// Fill the TokenInfo balance with free funds in serum3 oo accounts, and fill
// the serum3MaxReserved with their reserved funds. Also build Serum3Infos.
const serum3Infos = mangoAccount.serum3Active().map((serum3) => {
const oo = mangoAccount.getSerum3OoAccount(serum3.marketIndex);
// find the TokenInfos for the market's base and quote tokens
const baseIndex = tokenInfos.findIndex(
(tokenInfo) => tokenInfo.tokenIndex === serum3.baseTokenIndex,
);
const baseInfo = tokenInfos[baseIndex];
if (!baseInfo) {
throw new Error(
`BaseInfo not found for market with marketIndex ${serum3.marketIndex}!`,
);
}
const quoteIndex = tokenInfos.findIndex(
(tokenInfo) => tokenInfo.tokenIndex === serum3.quoteTokenIndex,
);
const quoteInfo = tokenInfos[quoteIndex];
if (!quoteInfo) {
throw new Error(
`QuoteInfo not found for market with marketIndex ${serum3.marketIndex}!`,
);
}
return Serum3Info.fromOoModifyingTokenInfos(
baseIndex,
baseInfo,
quoteIndex,
quoteInfo,
serum3.marketIndex,
oo,
);
});
// health contribution from perp accounts
const perpInfos = mangoAccount.perpActive().map((perpPosition) => {
const perpMarket = group.getPerpMarketByMarketIndex(
perpPosition.marketIndex,
);
return PerpInfo.fromPerpPosition(perpMarket, perpPosition);
});
return new HealthCache(tokenInfos, serum3Infos, perpInfos);
}
static fromDto(dto): HealthCache {
return new HealthCache(
dto.tokenInfos.map((dto) => TokenInfo.fromDto(dto)),
dto.serum3Infos.map((dto) => Serum3Info.fromDto(dto)),
dto.perpInfos.map((dto) => PerpInfo.fromDto(dto)),
);
}
computeSerum3Reservations(healthType: HealthType): {
tokenMaxReserved: I80F48[];
serum3Reserved: Serum3Reserved[];
} {
// For each token, compute the sum of serum-reserved amounts over all markets.
const tokenMaxReserved = new Array(this.tokenInfos.length)
.fill(null)
.map((ignored) => ZERO_I80F48());
// For each serum market, compute what happened if reserved_base was converted to quote
// or reserved_quote was converted to base.
const serum3Reserved: Serum3Reserved[] = [];
for (const info of this.serum3Infos) {
const quote = this.tokenInfos[info.quoteIndex];
const base = this.tokenInfos[info.baseIndex];
const reservedBase = info.reservedBase;
const reservedQuote = info.reservedQuote;
const quoteAsset = quote.prices.asset(healthType);
const baseLiab = base.prices.liab(healthType);
const allReservedAsBase = reservedBase.add(
reservedQuote.mul(quoteAsset).div(baseLiab),
);
const baseAsset = base.prices.asset(healthType);
const quoteLiab = quote.prices.liab(healthType);
const allReservedAsQuote = reservedQuote.add(
reservedBase.mul(baseAsset).div(quoteLiab),
);
const baseMaxReserved = tokenMaxReserved[info.baseIndex];
baseMaxReserved.iadd(allReservedAsBase);
const quoteMaxReserved = tokenMaxReserved[info.quoteIndex];
quoteMaxReserved.iadd(allReservedAsQuote);
serum3Reserved.push(
new Serum3Reserved(allReservedAsBase, allReservedAsQuote),
);
}
return {
tokenMaxReserved: tokenMaxReserved,
serum3Reserved: serum3Reserved,
};
}
public health(healthType: HealthType): I80F48 {
const health = ZERO_I80F48();
for (const tokenInfo of this.tokenInfos) {
const contrib = tokenInfo.healthContribution(healthType);
// console.log(` - ti ${contrib}`);
health.iadd(contrib);
}
const res = this.computeSerum3Reservations(healthType);
for (const [index, serum3Info] of this.serum3Infos.entries()) {
const contrib = serum3Info.healthContribution(
healthType,
this.tokenInfos,
res.tokenMaxReserved,
res.serum3Reserved[index],
);
// console.log(` - si ${contrib}`);
health.iadd(contrib);
}
for (const perpInfo of this.perpInfos) {
const contrib = perpInfo.healthContribution(healthType);
// console.log(` - pi ${contrib}`);
health.iadd(contrib);
}
return health;
}
// Note: only considers positive perp pnl contributions, see program code for more reasoning
public perpSettleHealth(): I80F48 {
const health = ZERO_I80F48();
for (const tokenInfo of this.tokenInfos) {
const contrib = tokenInfo.healthContribution(HealthType.maint);
// console.log(` - ti ${contrib}`);
health.iadd(contrib);
}
const res = this.computeSerum3Reservations(HealthType.maint);
for (const [index, serum3Info] of this.serum3Infos.entries()) {
const contrib = serum3Info.healthContribution(
HealthType.maint,
this.tokenInfos,
res.tokenMaxReserved,
res.serum3Reserved[index],
);
// console.log(` - si ${contrib}`);
health.iadd(contrib);
}
for (const perpInfo of this.perpInfos) {
const positiveContrib = perpInfo
.healthContribution(HealthType.maint)
.max(ZERO_I80F48());
// console.log(` - pi ${positiveContrib}`);
health.iadd(positiveContrib);
}
return health;
}
// An undefined HealthType will use an asset and liab weight of 1
public assets(healthType?: HealthType): I80F48 {
const assets = ZERO_I80F48();
for (const tokenInfo of this.tokenInfos) {
const contrib = tokenInfo.healthContribution(healthType);
if (contrib.isPos()) {
assets.iadd(contrib);
}
}
const res = this.computeSerum3Reservations(HealthType.maint);
for (const [index, serum3Info] of this.serum3Infos.entries()) {
const contrib = serum3Info.healthContribution(
healthType,
this.tokenInfos,
res.tokenMaxReserved,
res.serum3Reserved[index],
);
if (contrib.isPos()) {
assets.iadd(contrib);
}
}
for (const perpInfo of this.perpInfos) {
const contrib = perpInfo.healthContribution(healthType);
if (contrib.isPos()) {
assets.iadd(contrib);
}
}
return assets;
}
// An undefined HealthType will use an asset and liab weight of 1
public liabs(healthType?: HealthType): I80F48 {
const liabs = ZERO_I80F48();
for (const tokenInfo of this.tokenInfos) {
const contrib = tokenInfo.healthContribution(healthType);
if (contrib.isNeg()) {
liabs.isub(contrib);
}
}
const res = this.computeSerum3Reservations(HealthType.maint);
for (const [index, serum3Info] of this.serum3Infos.entries()) {
const contrib = serum3Info.healthContribution(
healthType,
this.tokenInfos,
res.tokenMaxReserved,
res.serum3Reserved[index],
);
if (contrib.isNeg()) {
liabs.isub(contrib);
}
}
for (const perpInfo of this.perpInfos) {
const contrib = perpInfo.healthContribution(healthType);
if (contrib.isNeg()) {
liabs.isub(contrib);
}
}
return liabs;
}
public healthRatio(healthType: HealthType): I80F48 {
const assets = ZERO_I80F48();
const liabs = ZERO_I80F48();
for (const tokenInfo of this.tokenInfos) {
const contrib = tokenInfo.healthContribution(healthType);
// console.log(` - ti contrib ${contrib.toLocaleString()}`);
if (contrib.isPos()) {
assets.iadd(contrib);
} else {
liabs.isub(contrib);
}
}
const res = this.computeSerum3Reservations(HealthType.maint);
for (const [index, serum3Info] of this.serum3Infos.entries()) {
const contrib = serum3Info.healthContribution(
healthType,
this.tokenInfos,
res.tokenMaxReserved,
res.serum3Reserved[index],
);
// console.log(` - si contrib ${contrib.toLocaleString()}`);
if (contrib.isPos()) {
assets.iadd(contrib);
} else {
liabs.isub(contrib);
}
}
for (const perpInfo of this.perpInfos) {
const contrib = perpInfo.healthContribution(healthType);
// console.log(` - pi contrib ${contrib.toLocaleString()}`);
if (contrib.isPos()) {
assets.iadd(contrib);
} else {
liabs.isub(contrib);
}
}
// console.log(
// ` - assets ${assets.toLocaleString()}, liabs ${liabs.toLocaleString()}`,
// );
if (liabs.gt(I80F48.fromNumber(0.001))) {
return HUNDRED_I80F48().mul(assets.sub(liabs).div(liabs));
} else {
return MAX_I80F48();
}
}
findTokenInfoIndex(tokenIndex: TokenIndex): number {
return this.tokenInfos.findIndex(
(tokenInfo) => tokenInfo.tokenIndex === tokenIndex,
);
}
getOrCreateTokenInfoIndex(bank: BankForHealth): number {
const index = this.findTokenInfoIndex(bank.tokenIndex);
if (index == -1) {
this.tokenInfos.push(TokenInfo.fromBank(bank));
}
return this.findTokenInfoIndex(bank.tokenIndex);
}
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);
// TODO: this will no longer work as easily because of the health weight changes
adjustedCache.tokenInfos[changeIndex].balanceNative.iadd(
change.nativeTokenAmount,
);
}
// HealthCache.logHealthCache('afterChange', adjustedCache);
return adjustedCache.healthRatio(healthType);
}
findSerum3InfoIndex(marketIndex: MarketIndex): number {
return this.serum3Infos.findIndex(
(serum3Info) => serum3Info.marketIndex === marketIndex,
);
}
getOrCreateSerum3InfoIndex(
baseBank: BankForHealth,
quoteBank: BankForHealth,
serum3Market: Serum3Market,
): number {
const index = this.findSerum3InfoIndex(serum3Market.marketIndex);
const baseEntryIndex = this.getOrCreateTokenInfoIndex(baseBank);
const quoteEntryIndex = this.getOrCreateTokenInfoIndex(quoteBank);
if (index == -1) {
this.serum3Infos.push(
Serum3Info.emptyFromSerum3Market(
serum3Market,
baseEntryIndex,
quoteEntryIndex,
),
);
}
return this.findSerum3InfoIndex(serum3Market.marketIndex);
}
adjustSerum3Reserved(
baseBank: BankForHealth,
quoteBank: BankForHealth,
serum3Market: Serum3Market,
reservedBaseChange: I80F48,
freeBaseChange: I80F48,
reservedQuoteChange: I80F48,
freeQuoteChange: I80F48,
): void {
const baseEntryIndex = this.getOrCreateTokenInfoIndex(baseBank);
const quoteEntryIndex = this.getOrCreateTokenInfoIndex(quoteBank);
const baseEntry = this.tokenInfos[baseEntryIndex];
const quoteEntry = this.tokenInfos[quoteEntryIndex];
// Apply it to the tokens
baseEntry.balanceNative.iadd(freeBaseChange);
quoteEntry.balanceNative.iadd(freeQuoteChange);
// Apply it to the serum3 info
const index = this.getOrCreateSerum3InfoIndex(
baseBank,
quoteBank,
serum3Market,
);
const serum3Info = this.serum3Infos[index];
serum3Info.reservedBase.iadd(reservedBaseChange);
serum3Info.reservedQuote.iadd(reservedQuoteChange);
}
simHealthRatioWithSerum3BidChanges(
baseBank: BankForHealth,
quoteBank: BankForHealth,
bidNativeQuoteAmount: I80F48,
serum3Market: Serum3Market,
healthType: HealthType = HealthType.init,
): I80F48 {
const adjustedCache: HealthCache = cloneDeep(this);
const quoteIndex = adjustedCache.getOrCreateTokenInfoIndex(quoteBank);
// Move token balance to reserved funds in open orders,
// essentially simulating a place order
// Reduce token balance for quote
adjustedCache.tokenInfos[quoteIndex].balanceNative.isub(
bidNativeQuoteAmount,
);
// Increase reserved in Serum3Info for quote
adjustedCache.adjustSerum3Reserved(
baseBank,
quoteBank,
serum3Market,
ZERO_I80F48(),
ZERO_I80F48(),
bidNativeQuoteAmount,
ZERO_I80F48(),
);
return adjustedCache.healthRatio(healthType);
}
simHealthRatioWithSerum3AskChanges(
baseBank: BankForHealth,
quoteBank: BankForHealth,
askNativeBaseAmount: I80F48,
serum3Market: Serum3Market,
healthType: HealthType = HealthType.init,
): I80F48 {
const adjustedCache: HealthCache = cloneDeep(this);
const baseIndex = adjustedCache.getOrCreateTokenInfoIndex(baseBank);
// Move token balance to reserved funds in open orders,
// essentially simulating a place order
// Reduce token balance for base
adjustedCache.tokenInfos[baseIndex].balanceNative.isub(askNativeBaseAmount);
// Increase reserved in Serum3Info for base
adjustedCache.adjustSerum3Reserved(
baseBank,
quoteBank,
serum3Market,
askNativeBaseAmount,
ZERO_I80F48(),
ZERO_I80F48(),
ZERO_I80F48(),
);
return adjustedCache.healthRatio(healthType);
}
findPerpInfoIndex(perpMarketIndex: number): number {
return this.perpInfos.findIndex(
(perpInfo) => perpInfo.perpMarketIndex === perpMarketIndex,
);
}
getOrCreatePerpInfoIndex(perpMarket: PerpMarket): number {
const index = this.findPerpInfoIndex(perpMarket.perpMarketIndex);
if (index == -1) {
this.perpInfos.push(PerpInfo.emptyFromPerpMarket(perpMarket));
}
return this.findPerpInfoIndex(perpMarket.perpMarketIndex);
}
adjustPerpInfo(
perpInfoIndex: number,
price: I80F48,
side: PerpOrderSide,
newOrderBaseLots: BN,
): void {
if (side == PerpOrderSide.bid) {
this.perpInfos[perpInfoIndex].baseLots.iadd(newOrderBaseLots);
this.perpInfos[perpInfoIndex].quote.isub(
I80F48.fromI64(newOrderBaseLots)
.mul(I80F48.fromI64(this.perpInfos[perpInfoIndex].baseLotSize))
.mul(price),
);
} else {
this.perpInfos[perpInfoIndex].baseLots.isub(newOrderBaseLots);
this.perpInfos[perpInfoIndex].quote.iadd(
I80F48.fromI64(newOrderBaseLots)
.mul(I80F48.fromI64(this.perpInfos[perpInfoIndex].baseLotSize))
.mul(price),
);
}
}
simHealthRatioWithPerpOrderChanges(
perpMarket: PerpMarket,
existingPerpPosition: PerpPosition,
side: PerpOrderSide,
baseLots: BN,
price: I80F48,
healthType: HealthType = HealthType.init,
): I80F48 {
const clonedHealthCache: HealthCache = cloneDeep(this);
const perpInfoIndex =
clonedHealthCache.getOrCreatePerpInfoIndex(perpMarket);
clonedHealthCache.adjustPerpInfo(perpInfoIndex, price, side, baseLots);
return clonedHealthCache.healthRatio(healthType);
}
public logHealthCache(debug: string): void {
if (debug) console.log(debug);
for (const token of this.tokenInfos) {
console.log(` ${token.toString()}`);
}
const res = this.computeSerum3Reservations(HealthType.maint);
for (const [index, serum3Info] of this.serum3Infos.entries()) {
console.log(
` ${serum3Info.toString(
this.tokenInfos,
res.tokenMaxReserved,
res.serum3Reserved[index],
)}`,
);
}
console.log(
` assets ${this.assets(HealthType.init)}, liabs ${this.liabs(
HealthType.init,
)}, `,
);
console.log(` health(HealthType.init) ${this.health(HealthType.init)}`);
console.log(
` healthRatio(HealthType.init) ${this.healthRatio(HealthType.init)}`,
);
}
private static scanRightUntilLessThan(
start: I80F48,
target: I80F48,
fun: (amount: I80F48) => I80F48,
): I80F48 {
const maxIterations = 20;
let current = start;
// console.log(`scanRightUntilLessThan, start ${start.toLocaleString()}`);
for (const key of Array(maxIterations).fill(0).keys()) {
const value = fun(current);
if (value.lt(target)) {
return current;
}
// console.log(
// ` - current ${current.toLocaleString()}, value ${value.toLocaleString()}, target ${target.toLocaleString()}`,
// );
current = current.max(ONE_I80F48()).mul(I80F48.fromNumber(2));
}
throw new Error('Could not find amount that led to health ratio <=0');
}
/// This is not a generic function. It assumes there is a unique maximum between left and right.
private static findMaximum(
left: I80F48,
right: I80F48,
minStep: I80F48,
fun: (I80F48) => I80F48,
): I80F48[] {
const half = I80F48.fromNumber(0.5);
let mid = half.mul(left.add(right));
let leftValue = fun(left);
let rightValue = fun(right);
let midValue = fun(mid);
while (right.sub(left).gt(minStep)) {
if (leftValue.gte(midValue)) {
// max must be between left and mid
right = mid;
rightValue = midValue;
mid = half.mul(left.add(mid));
midValue = fun(mid);
} else if (midValue.lte(rightValue)) {
// max must be between mid and right
left = mid;
leftValue = midValue;
mid = half.mul(mid.add(right));
midValue = fun(mid);
} else {
// mid is larger than both left and right, max could be on either side
const leftmid = half.mul(left.add(mid));
const leftMidValue = fun(leftmid);
if (leftMidValue.gte(midValue)) {
// max between left and mid
right = mid;
rightValue = midValue;
mid = leftmid;
midValue = leftMidValue;
continue;
}
const rightmid = half.mul(mid.add(right));
const rightMidValue = fun(rightmid);
if (rightMidValue.gte(midValue)) {
// max between mid and right
left = mid;
leftValue = midValue;
mid = rightmid;
midValue = rightMidValue;
continue;
}
// max between leftmid and rightmid
left = leftmid;
leftValue = leftMidValue;
right = rightmid;
rightValue = rightMidValue;
}
}
if (leftValue.gte(midValue)) {
return [left, leftValue];
} else if (midValue.gte(rightValue)) {
return [mid, midValue];
} else {
return [right, rightValue];
}
}
private static binaryApproximationSearch(
left: I80F48,
leftValue: I80F48,
right: I80F48,
targetValue: I80F48,
minStep: I80F48,
fun: (I80F48) => I80F48,
): I80F48 {
const maxIterations = 50;
const targetError = I80F48.fromNumber(0.1);
const rightValue = fun(right);
// console.log(
// ` - binaryApproximationSearch left ${left.toLocaleString()}, leftValue ${leftValue.toLocaleString()}, right ${right.toLocaleString()}, rightValue ${rightValue.toLocaleString()}, targetValue ${targetValue.toLocaleString()}`,
// );
if (
(leftValue.sub(targetValue).isPos() &&
rightValue.sub(targetValue).isPos()) ||
(leftValue.sub(targetValue).isNeg() &&
rightValue.sub(targetValue).isNeg())
) {
throw new Error(
`Internal error: left ${leftValue.toNumber()} and right ${rightValue.toNumber()} don't contain the target value ${targetValue.toNumber()}!`,
);
}
let newAmount, newAmountValue;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
for (const key of Array(maxIterations).fill(0).keys()) {
if (right.sub(left).abs().lt(minStep)) {
return left;
}
newAmount = left.add(right).mul(I80F48.fromNumber(0.5));
newAmountValue = fun(newAmount);
// console.log(
// ` - left ${left.toLocaleString()}, right ${right.toLocaleString()}, newAmount ${newAmount.toLocaleString()}, newAmountValue ${newAmountValue.toLocaleString()}, targetValue ${targetValue.toLocaleString()}`,
// );
const error = newAmountValue.sub(targetValue);
if (error.isPos() && error.lt(targetError)) {
return newAmount;
}
if (newAmountValue.gt(targetValue) != rightValue.gt(targetValue)) {
left = newAmount;
} else {
right = newAmount;
}
}
console.error(
`Unable to get targetValue within ${maxIterations} iterations, newAmount ${newAmount}, newAmountValue ${newAmountValue}, target ${targetValue}`,
);
return newAmount;
}
getMaxSwapSource(
sourceBank: BankForHealth,
targetBank: BankForHealth,
price: I80F48,
): I80F48 {
const health = this.health(HealthType.init);
if (health.isNeg()) {
return this.getMaxSwapSourceForHealth(
sourceBank,
targetBank,
price,
toNativeI80F48ForQuote(1), // target 1 ui usd worth health
);
}
return this.getMaxSwapSourceForHealthRatio(
sourceBank,
targetBank,
price,
I80F48.fromNumber(2), // target 2% health
);
}
getMaxSwapSourceForHealthRatio(
sourceBank: BankForHealth,
targetBank: BankForHealth,
price: I80F48,
minRatio: I80F48,
): I80F48 {
return this.getMaxSwapSourceForHealthFn(
sourceBank,
targetBank,
price,
minRatio,
function (hc: HealthCache): I80F48 {
return hc.healthRatio(HealthType.init);
},
);
}
getMaxSwapSourceForHealth(
sourceBank: BankForHealth,
targetBank: BankForHealth,
price: I80F48,
minHealth: I80F48,
): I80F48 {
return this.getMaxSwapSourceForHealthFn(
sourceBank,
targetBank,
price,
minHealth,
function (hc: HealthCache): I80F48 {
return hc.health(HealthType.init);
},
);
}
getMaxSwapSourceForHealthFn(
sourceBank: BankForHealth,
targetBank: BankForHealth,
price: I80F48,
minFnValue: I80F48,
targetFn: (cache) => I80F48,
): I80F48 {
if (
sourceBank.initLiabWeight
.sub(targetBank.initAssetWeight)
.abs()
.lte(ZERO_I80F48())
) {
return ZERO_I80F48();
}
// The health and health_ratio are 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 value is < minRatio it can be useful to swap to *increase* health
// - even if initial value is < 0, swapping can increase health (maybe above 0)
// - be careful about finding the minFnValue: the function isn't convex
const initialRatio = this.healthRatio(HealthType.init);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
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];
const res = healthCacheClone.computeSerum3Reservations(HealthType.init);
const sourceReserved = res.tokenMaxReserved[sourceIndex];
const targetReserved = res.tokenMaxReserved[targetIndex];
// If the price is sufficiently good, then health will just increase from swapping:
// once we've swapped enough, swapping x reduces health by x * source_liab_weight and
// increases it by x * target_asset_weight * price_factor.
const finalHealthSlope = source.initLiabWeight
.neg()
.mul(source.prices.liab(HealthType.init))
.add(
target.initAssetWeight
.mul(target.prices.asset(HealthType.init))
.mul(price),
);
if (finalHealthSlope.gte(ZERO_I80F48())) {
return MAX_I80F48();
}
// 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): HealthCache {
const adjustedCache: HealthCache = cloneDeep(healthCacheClone);
// adjustedCache.logHealthCache('beforeSwap', adjustedCache);
// TODO: make a copy of the bank, apply amount, recompute weights,
// and set the new weights on the tokenInfos
adjustedCache.tokenInfos[sourceIndex].balanceNative.isub(amount);
adjustedCache.tokenInfos[targetIndex].balanceNative.iadd(
amount.mul(price),
);
// adjustedCache.logHealthCache('afterSwap', adjustedCache);
return adjustedCache;
}
function fnValueAfterSwap(amount: I80F48): I80F48 {
return targetFn(cacheAfterSwap(amount));
}
// The function we're looking at has a unique maximum.
//
// If we discount serum3 reservations, there are two key slope changes:
// Assume source.balance > 0 and target.balance < 0.
// When these values flip sign, the health slope decreases, but could still be positive.
//
// The first thing we do is to find this maximum.
// The largest amount that the maximum could be at
const rightmost = source.balanceNative
.abs()
.add(sourceReserved)
.max(target.balanceNative.abs().add(targetReserved).div(price));
const [amountForMaxValue, maxValue] = HealthCache.findMaximum(
ZERO_I80F48(),
rightmost,
I80F48.fromNumber(0.1),
fnValueAfterSwap,
);
if (maxValue.lte(minFnValue)) {
// We cannot reach min_ratio, just return the max
return amountForMaxValue;
}
let amount: I80F48;
// Now max_value is bigger than minFnValue, the target amount must be >amountForMaxValue.
// Search to the right of amountForMaxValue: but how far?
// Use a simple estimation for the amount that would lead to zero health:
// health
// - source_liab_weight * source_liab_price * a
// + target_asset_weight * target_asset_price * price * a = 0.
// where a is the source token native amount.
// Note that this is just an estimate. Swapping can increase the amount that serum3
// reserved contributions offset, moving the actual zero point further to the right.
const healthAtMaxValue = cacheAfterSwap(amountForMaxValue).health(
HealthType.init,
);
if (healthAtMaxValue.lte(ZERO_I80F48())) {
return ZERO_I80F48();
}
const zeroHealthEstimate = amountForMaxValue.sub(
healthAtMaxValue.div(finalHealthSlope),
);
const rightBound = HealthCache.scanRightUntilLessThan(
zeroHealthEstimate,
minFnValue,
fnValueAfterSwap,
);
if (rightBound.eq(zeroHealthEstimate)) {
amount = HealthCache.binaryApproximationSearch(
amountForMaxValue,
maxValue,
rightBound,
minFnValue,
I80F48.fromNumber(0.1),
fnValueAfterSwap,
);
} else {
// Must be between 0 and point0_amount
amount = HealthCache.binaryApproximationSearch(
zeroHealthEstimate,
fnValueAfterSwap(zeroHealthEstimate),
rightBound,
minFnValue,
I80F48.fromNumber(0.1),
fnValueAfterSwap,
);
}
return amount;
}
getMaxSerum3OrderForHealthRatio(
baseBank: BankForHealth,
quoteBank: BankForHealth,
serum3Market: Serum3Market,
side: Serum3Side,
minRatio: I80F48,
): I80F48 {
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();
}
// console.log(`getMaxSerum3OrderForHealthRatio`);
// Amount which would bring health to 0
// where M = max(A_deposits, B_borrows)
// amount = M + (init_health + M * (B_init_liab - A_init_asset)) / (A_init_liab - B_init_asset);
// 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.balanceNative.lt(ZERO_I80F48())
? quote.balanceNative.abs().mul(quote.prices.liab(HealthType.init))
: ZERO_I80F48();
const max = base.balanceNative
.mul(base.prices.asset(HealthType.init))
.max(quoteBorrows);
zeroAmount = max.add(
initialHealth
.add(max.mul(quote.initLiabWeight.sub(base.initAssetWeight)))
.div(
base
.liabWeight(HealthType.init)
.sub(quote.assetWeight(HealthType.init)),
),
);
// console.log(` - quoteBorrows ${quoteBorrows.toLocaleString()}`);
// console.log(` - max ${max.toLocaleString()}`);
} else {
const baseBorrows = base.balanceNative.lt(ZERO_I80F48())
? base.balanceNative.abs().mul(base.prices.liab(HealthType.init))
: ZERO_I80F48();
const max = quote.balanceNative
.mul(quote.prices.asset(HealthType.init))
.max(baseBorrows);
zeroAmount = max.add(
initialHealth
.add(max.mul(base.initLiabWeight.sub(quote.initAssetWeight)))
.div(
quote
.liabWeight(HealthType.init)
.sub(base.assetWeight(HealthType.init)),
),
);
// console.log(` - baseBorrows ${baseBorrows.toLocaleString()}`);
// console.log(` - max ${max.toLocaleString()}`);
}
const cache = cacheAfterPlacingOrder(zeroAmount);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const zeroAmountHealth = cache.health(HealthType.init);
const zeroAmountRatio = cache.healthRatio(HealthType.init);
// console.log(` - zeroAmount ${zeroAmount.toLocaleString()}`);
// console.log(` - zeroAmountHealth ${zeroAmountHealth.toLocaleString()}`);
// console.log(` - zeroAmountRatio ${zeroAmountRatio.toLocaleString()}`);
function cacheAfterPlacingOrder(amount: I80F48): HealthCache {
const adjustedCache: HealthCache = cloneDeep(healthCacheClone);
// adjustedCache.logHealthCache(` before placing order ${amount}`);
// TODO: there should also be some issue with oracle vs stable price here;
// probably better to pass in not the quote amount but the base or quote native amount
side === Serum3Side.ask
? adjustedCache.tokenInfos[baseIndex].balanceNative.isub(
amount.div(base.prices.oracle),
)
: adjustedCache.tokenInfos[quoteIndex].balanceNative.isub(
amount.div(quote.prices.oracle),
);
adjustedCache.adjustSerum3Reserved(
baseBank,
quoteBank,
serum3Market,
side === Serum3Side.ask
? amount.div(base.prices.oracle)
: ZERO_I80F48(),
ZERO_I80F48(),
side === Serum3Side.bid
? amount.div(quote.prices.oracle)
: ZERO_I80F48(),
ZERO_I80F48(),
);
// adjustedCache.logHealthCache(' after placing order');
return adjustedCache;
}
function healthRatioAfterPlacingOrder(amount: I80F48): I80F48 {
return cacheAfterPlacingOrder(amount).healthRatio(HealthType.init);
}
const amount = HealthCache.binaryApproximationSearch(
initialAmount,
initialRatio,
zeroAmount,
minRatio,
ONE_I80F48(),
healthRatioAfterPlacingOrder,
);
return amount;
}
getMaxPerpForHealthRatio(
perpMarket: PerpMarket,
price,
side: PerpOrderSide,
minRatio: I80F48,
): I80F48 {
const healthCacheClone: HealthCache = cloneDeep(this);
const initialRatio = this.healthRatio(HealthType.init);
if (initialRatio.lt(ZERO_I80F48())) {
return ZERO_I80F48();
}
const direction = side == PerpOrderSide.bid ? 1 : -1;
const perpInfoIndex = healthCacheClone.getOrCreatePerpInfoIndex(perpMarket);
const perpInfo = healthCacheClone.perpInfos[perpInfoIndex];
const prices = perpInfo.prices;
const baseLotSize = I80F48.fromI64(perpMarket.baseLotSize);
// If the price is sufficiently good then health will just increase from trading
const finalHealthSlope =
direction == 1
? perpInfo.initBaseAssetWeight
.mul(prices.asset(HealthType.init))
.sub(price)
: price.sub(
perpInfo.initBaseLiabWeight.mul(prices.liab(HealthType.init)),
);
if (finalHealthSlope.gte(ZERO_I80F48())) {
return MAX_I80F48();
}
function cacheAfterTrade(baseLots: BN): HealthCache {
const adjustedCache: HealthCache = cloneDeep(healthCacheClone);
// adjustedCache.logHealthCache(' -- before trade');
adjustedCache.adjustPerpInfo(perpInfoIndex, price, side, baseLots);
// adjustedCache.logHealthCache(' -- after trade');
return adjustedCache;
}
function healthAfterTrade(baseLots: I80F48): I80F48 {
return cacheAfterTrade(new BN(baseLots.toNumber())).health(
HealthType.init,
);
}
function healthRatioAfterTrade(baseLots: I80F48): I80F48 {
return cacheAfterTrade(new BN(baseLots.toNumber())).healthRatio(
HealthType.init,
);
}
function healthRatioAfterTradeTrunc(baseLots: I80F48): I80F48 {
return healthRatioAfterTrade(baseLots.floor());
}
const initialBaseLots = I80F48.fromU64(perpInfo.baseLots);
// There are two cases:
// 1. We are increasing abs(baseLots)
// 2. We are bringing the base position to 0, and then going to case 1.
const hasCase2 =
(initialBaseLots.gt(ZERO_I80F48()) && direction == -1) ||
(initialBaseLots.lt(ZERO_I80F48()) && direction == 1);
let case1Start: I80F48, case1StartRatio: I80F48;
if (hasCase2) {
case1Start = initialBaseLots.abs();
case1StartRatio = healthRatioAfterTrade(case1Start);
} else {
case1Start = ZERO_I80F48();
case1StartRatio = initialRatio;
}
// If we start out below minRatio and can't go above, pick the best case
let baseLots: I80F48;
if (initialRatio.lte(minRatio) && case1StartRatio.lt(minRatio)) {
if (case1StartRatio.gte(initialRatio)) {
baseLots = case1Start;
} else {
baseLots = ZERO_I80F48();
}
} else if (case1StartRatio.gte(minRatio)) {
// Must reach minRatio to the right of case1Start
// Need to figure out how many lots to trade to reach zero health (zero_health_amount).
// We do this by looking at the starting health and the health slope per
// traded base lot (finalHealthSlope).
const startCache = cacheAfterTrade(new BN(case1Start.toNumber()));
const startHealth = startCache.health(HealthType.init);
if (startHealth.lte(ZERO_I80F48())) {
return ZERO_I80F48();
}
// The perp market's contribution to the health above may be capped. But we need to trade
// enough to fully reduce any positive-pnl buffer. Thus get the uncapped health:
const perpInfo = startCache.perpInfos[perpInfoIndex];
const startHealthUncapped = startHealth
.sub(perpInfo.healthContribution(HealthType.init))
.add(perpInfo.unweightedHealthContribution(HealthType.init));
const zeroHealthAmount = case1Start
.sub(startHealthUncapped.div(finalHealthSlope).div(baseLotSize))
.add(ONE_I80F48());
const zeroHealthRatio = healthRatioAfterTradeTrunc(zeroHealthAmount);
baseLots = HealthCache.binaryApproximationSearch(
case1Start,
case1StartRatio,
zeroHealthAmount,
minRatio,
ONE_I80F48(),
healthRatioAfterTradeTrunc,
);
} else {
// Between 0 and case1Start
baseLots = HealthCache.binaryApproximationSearch(
ZERO_I80F48(),
initialRatio,
case1Start,
minRatio,
ONE_I80F48(),
healthRatioAfterTradeTrunc,
);
}
return baseLots.floor();
}
}
export class Prices {
constructor(public oracle: I80F48, public stable: I80F48) {}
public liab(healthType: HealthType | undefined): I80F48 {
if (
healthType === HealthType.maint ||
healthType === HealthType.liquidationEnd ||
healthType === undefined
) {
return this.oracle;
}
return this.oracle.max(this.stable);
}
public asset(healthType: HealthType | undefined): I80F48 {
if (
healthType === HealthType.maint ||
healthType === HealthType.liquidationEnd ||
healthType === undefined
) {
return this.oracle;
}
return this.oracle.min(this.stable);
}
}
export class TokenInfo {
constructor(
public tokenIndex: TokenIndex,
public maintAssetWeight: I80F48,
public initAssetWeight: I80F48,
public initScaledAssetWeight: I80F48,
public maintLiabWeight: I80F48,
public initLiabWeight: I80F48,
public initScaledLiabWeight: I80F48,
public prices: Prices,
public balanceNative: I80F48,
) {}
static fromDto(dto: TokenInfoDto): TokenInfo {
return new TokenInfo(
dto.tokenIndex as TokenIndex,
I80F48.from(dto.maintAssetWeight),
I80F48.from(dto.initAssetWeight),
I80F48.from(dto.initScaledAssetWeight),
I80F48.from(dto.maintLiabWeight),
I80F48.from(dto.initLiabWeight),
I80F48.from(dto.initScaledLiabWeight),
new Prices(
I80F48.from(dto.prices.oracle),
I80F48.from(dto.prices.stable),
),
I80F48.from(dto.balanceNative),
);
}
static fromBank(bank: BankForHealth, nativeBalance?: I80F48): TokenInfo {
const p = new Prices(
bank.price,
I80F48.fromNumber(bank.stablePriceModel.stablePrice),
);
// Use the liab price for computing weight scaling, because it's pessimistic and
// causes the most unfavorable scaling.
const liabPrice = p.liab(HealthType.init);
return new TokenInfo(
bank.tokenIndex,
bank.maintAssetWeight,
bank.initAssetWeight,
bank.scaledInitAssetWeight(liabPrice),
bank.maintLiabWeight,
bank.initLiabWeight,
bank.scaledInitLiabWeight(liabPrice),
p,
nativeBalance ? nativeBalance : ZERO_I80F48(),
);
}
assetWeight(healthType: HealthType): I80F48 {
if (healthType == HealthType.init) {
return this.initScaledAssetWeight;
} else if (healthType == HealthType.liquidationEnd) {
return this.initAssetWeight;
}
// healthType == HealthType.maint
return this.maintAssetWeight;
}
liabWeight(healthType: HealthType): I80F48 {
if (healthType == HealthType.init) {
return this.initScaledLiabWeight;
} else if (healthType == HealthType.liquidationEnd) {
return this.initLiabWeight;
}
// healthType == HealthType.maint
return this.maintLiabWeight;
}
healthContribution(healthType?: HealthType): I80F48 {
let weight, price;
if (healthType === undefined) {
return this.balanceNative.mul(this.prices.oracle);
}
if (this.balanceNative.isNeg()) {
weight = this.liabWeight(healthType);
price = this.prices.liab(healthType);
} else {
weight = this.assetWeight(healthType);
price = this.prices.asset(healthType);
}
return this.balanceNative.mul(weight).mul(price);
}
toString(): string {
return ` tokenIndex: ${this.tokenIndex}, balanceNative: ${
this.balanceNative
}, initHealth ${this.healthContribution(HealthType.init)}`;
}
}
export class Serum3Reserved {
constructor(
public allReservedAsBase: I80F48,
public allReservedAsQuote: I80F48,
) {}
}
export class Serum3Info {
constructor(
public reservedBase: I80F48,
public reservedQuote: I80F48,
public baseIndex: number,
public quoteIndex: number,
public marketIndex: MarketIndex,
) {}
static fromDto(dto: Serum3InfoDto): Serum3Info {
return new Serum3Info(
I80F48.from(dto.reservedBase),
I80F48.from(dto.reservedQuote),
dto.baseIndex,
dto.quoteIndex,
dto.marketIndex as MarketIndex,
);
}
static emptyFromSerum3Market(
serum3Market: Serum3Market,
baseEntryIndex: number,
quoteEntryIndex: number,
): Serum3Info {
return new Serum3Info(
ZERO_I80F48(),
ZERO_I80F48(),
baseEntryIndex,
quoteEntryIndex,
serum3Market.marketIndex,
);
}
static fromOoModifyingTokenInfos(
baseIndex: number,
baseInfo: TokenInfo,
quoteIndex: number,
quoteInfo: TokenInfo,
marketIndex: MarketIndex,
oo: OpenOrders,
): Serum3Info {
// add the amounts that are freely settleable immediately to token balances
const baseFree = I80F48.fromI64(oo.baseTokenFree);
// NOTE: referrerRebatesAccrued is not declared on oo class, but the layout
// is aware of it
const quoteFree = I80F48.fromI64(
oo.quoteTokenFree.add((oo as any).referrerRebatesAccrued),
);
baseInfo.balanceNative.iadd(baseFree);
quoteInfo.balanceNative.iadd(quoteFree);
// track the reserved amounts
const reservedBase = I80F48.fromI64(
oo.baseTokenTotal.sub(oo.baseTokenFree),
);
const reservedQuote = I80F48.fromI64(
oo.quoteTokenTotal.sub(oo.quoteTokenFree),
);
return new Serum3Info(
reservedBase,
reservedQuote,
baseIndex,
quoteIndex,
marketIndex,
);
}
// An undefined HealthType will use an asset and liab weight of 1
healthContribution(
healthType: HealthType | undefined,
tokenInfos: TokenInfo[],
tokenMaxReserved: I80F48[],
marketReserved: Serum3Reserved,
): I80F48 {
if (
marketReserved.allReservedAsBase.isZero() ||
marketReserved.allReservedAsQuote.isZero()
) {
return ZERO_I80F48();
}
const baseInfo = tokenInfos[this.baseIndex];
const quoteInfo = tokenInfos[this.quoteIndex];
const baseMaxReserved = tokenMaxReserved[this.baseIndex];
const quoteMaxReserved = tokenMaxReserved[this.quoteIndex];
// How much the health would increase if the reserved balance were applied to the passed
// token info?
const computeHealthEffect = function (
tokenInfo: TokenInfo,
tokenMaxReserved: I80F48,
marketReserved: I80F48,
): I80F48 {
// This balance includes all possible reserved funds from markets that relate to the
// token, including this market itself: `tokenMaxReserved` is already included in `maxBalance`.
const maxBalance = tokenInfo.balanceNative.add(tokenMaxReserved);
// 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(marketReserved)) {
assetPart = marketReserved;
liabPart = ZERO_I80F48();
} else if (maxBalance.isNeg()) {
assetPart = ZERO_I80F48();
liabPart = marketReserved;
} else {
assetPart = maxBalance;
liabPart = marketReserved.sub(maxBalance);
}
if (healthType === undefined) {
return assetPart
.mul(tokenInfo.prices.oracle)
.add(liabPart.mul(tokenInfo.prices.oracle));
}
const assetWeight = tokenInfo.assetWeight(healthType);
const liabWeight = tokenInfo.liabWeight(healthType);
const assetPrice = tokenInfo.prices.asset(healthType);
const liabPrice = tokenInfo.prices.liab(healthType);
return assetWeight
.mul(assetPart)
.mul(assetPrice)
.add(liabWeight.mul(liabPart).mul(liabPrice));
};
const healthBase = computeHealthEffect(
baseInfo,
baseMaxReserved,
marketReserved.allReservedAsBase,
);
const healthQuote = computeHealthEffect(
quoteInfo,
quoteMaxReserved,
marketReserved.allReservedAsQuote,
);
// console.log(` - healthBase ${healthBase.toLocaleString()}`);
// console.log(` - healthQuote ${healthQuote.toLocaleString()}`);
return healthBase.min(healthQuote);
}
toString(
tokenInfos: TokenInfo[],
tokenMaxReserved: I80F48[],
marketReserved: Serum3Reserved,
): string {
return ` marketIndex: ${this.marketIndex}, baseIndex: ${
this.baseIndex
}, quoteIndex: ${this.quoteIndex}, reservedBase: ${
this.reservedBase
}, reservedQuote: ${
this.reservedQuote
}, initHealth ${this.healthContribution(
HealthType.init,
tokenInfos,
tokenMaxReserved,
marketReserved,
)}`;
}
}
export class PerpInfo {
constructor(
public perpMarketIndex: number,
public maintBaseAssetWeight: I80F48,
public initBaseAssetWeight: I80F48,
public maintBaseLiabWeight: I80F48,
public initBaseLiabWeight: I80F48,
public maintOverallAssetWeight: I80F48,
public initOverallAssetWeight: I80F48,
public baseLotSize: BN,
public baseLots: BN,
public bidsBaseLots: BN,
public asksBaseLots: BN,
public quote: I80F48,
public prices: Prices,
public hasOpenOrders: boolean,
) {}
static fromDto(dto: PerpInfoDto): PerpInfo {
return new PerpInfo(
dto.perpMarketIndex,
I80F48.from(dto.maintBaseAssetWeight),
I80F48.from(dto.initBaseAssetWeight),
I80F48.from(dto.maintBaseLiabWeight),
I80F48.from(dto.initBaseLiabWeight),
I80F48.from(dto.maintOverallAssetWeight),
I80F48.from(dto.initOverallAssetWeight),
dto.baseLotSize,
dto.baseLots,
dto.bidsBaseLots,
dto.asksBaseLots,
I80F48.from(dto.quote),
new Prices(
I80F48.from(dto.prices.oracle),
I80F48.from(dto.prices.stable),
),
dto.hasOpenOrders,
);
}
static fromPerpPosition(
perpMarket: PerpMarket,
perpPosition: PerpPosition,
): PerpInfo {
const baseLots = perpPosition.basePositionLots.add(
perpPosition.takerBaseLots,
);
const unsettledFunding = perpPosition.getUnsettledFunding(perpMarket);
const takerQuote = I80F48.fromI64(
new BN(perpPosition.takerQuoteLots).mul(perpMarket.quoteLotSize),
);
const quoteCurrent = perpPosition.quotePositionNative
.sub(unsettledFunding)
.add(takerQuote);
return new PerpInfo(
perpMarket.perpMarketIndex,
perpMarket.maintBaseAssetWeight,
perpMarket.initBaseAssetWeight,
perpMarket.maintBaseLiabWeight,
perpMarket.initBaseLiabWeight,
perpMarket.maintOverallAssetWeight,
perpMarket.initOverallAssetWeight,
perpMarket.baseLotSize,
baseLots,
perpPosition.bidsBaseLots,
perpPosition.asksBaseLots,
quoteCurrent,
new Prices(
perpMarket.price,
I80F48.fromNumber(perpMarket.stablePriceModel.stablePrice),
),
perpPosition.hasOpenOrders(),
);
}
healthContribution(healthType: HealthType | undefined): I80F48 {
const contrib = this.unweightedHealthContribution(healthType);
if (contrib.gt(ZERO_I80F48())) {
const assetWeight =
healthType == HealthType.init || healthType == HealthType.liquidationEnd
? this.initOverallAssetWeight
: this.maintOverallAssetWeight;
return assetWeight.mul(contrib);
}
return contrib;
}
unweightedHealthContribution(healthType: HealthType | undefined): I80F48 {
function orderExecutionCase(
pi: PerpInfo,
ordersBaseLots: BN,
orderPrice: I80F48,
): I80F48 {
const netBaseNative = I80F48.fromU64(
pi.baseLots.add(ordersBaseLots).mul(pi.baseLotSize),
);
let weight, basePrice;
if (
healthType == HealthType.init ||
healthType == HealthType.liquidationEnd
) {
if (netBaseNative.isNeg()) {
weight = pi.initBaseLiabWeight;
} else {
weight = pi.initBaseAssetWeight;
}
}
// healthType == HealthType.maint
else {
if (netBaseNative.isNeg()) {
weight = pi.maintBaseLiabWeight;
} else {
weight = pi.maintBaseAssetWeight;
}
}
if (netBaseNative.isNeg()) {
basePrice = pi.prices.liab(healthType);
} else {
basePrice = pi.prices.asset(healthType);
}
// Total value of the order-execution adjusted base position
const baseHealth = netBaseNative.mul(weight).mul(basePrice);
const ordersBaseNative = I80F48.fromU64(
ordersBaseLots.mul(pi.baseLotSize),
);
// The quote change from executing the bids/asks
const orderQuote = ordersBaseNative.neg().mul(orderPrice);
return baseHealth.add(orderQuote);
}
// What is worse: Executing all bids at oracle_price.liab, or executing all asks at oracle_price.asset?
const bidsCase = orderExecutionCase(
this,
this.bidsBaseLots,
this.prices.liab(healthType),
);
const asksCase = orderExecutionCase(
this,
this.asksBaseLots.neg(),
this.prices.asset(healthType),
);
const worstCase = bidsCase.min(asksCase);
return this.quote.add(worstCase);
}
static emptyFromPerpMarket(perpMarket: PerpMarket): PerpInfo {
return new PerpInfo(
perpMarket.perpMarketIndex,
perpMarket.maintBaseAssetWeight,
perpMarket.initBaseAssetWeight,
perpMarket.maintBaseLiabWeight,
perpMarket.initBaseLiabWeight,
perpMarket.maintOverallAssetWeight,
perpMarket.initOverallAssetWeight,
perpMarket.baseLotSize,
new BN(0),
new BN(0),
new BN(0),
ZERO_I80F48(),
new Prices(
perpMarket.price,
I80F48.fromNumber(perpMarket.stablePriceModel.stablePrice),
),
false,
);
}
toString(): string {
return ` perpMarketIndex: ${this.perpMarketIndex}, base: ${
this.baseLots
}, quote: ${this.quote}, oraclePrice: ${
this.prices.oracle
}, uncapped health contribution ${this.unweightedHealthContribution(
HealthType.init,
)}`;
}
}
export class HealthCacheDto {
tokenInfos: TokenInfoDto[];
serum3Infos: Serum3InfoDto[];
perpInfos: PerpInfoDto[];
}
export class TokenInfoDto {
tokenIndex: number;
maintAssetWeight: I80F48Dto;
initAssetWeight: I80F48Dto;
initScaledAssetWeight: I80F48Dto;
maintLiabWeight: I80F48Dto;
initLiabWeight: I80F48Dto;
initScaledLiabWeight: I80F48Dto;
prices: { oracle: I80F48Dto; stable: I80F48Dto };
balanceNative: I80F48Dto;
constructor(
tokenIndex: number,
maintAssetWeight: I80F48Dto,
initAssetWeight: I80F48Dto,
initScaledAssetWeight: I80F48Dto,
maintLiabWeight: I80F48Dto,
initLiabWeight: I80F48Dto,
initScaledLiabWeight: I80F48Dto,
prices: { oracle: I80F48Dto; stable: I80F48Dto },
balanceNative: I80F48Dto,
) {
this.tokenIndex = tokenIndex;
this.maintAssetWeight = maintAssetWeight;
this.initAssetWeight = initAssetWeight;
this.initScaledAssetWeight = initScaledAssetWeight;
this.maintLiabWeight = maintLiabWeight;
this.initLiabWeight = initLiabWeight;
this.initScaledLiabWeight = initScaledLiabWeight;
this.prices = prices;
this.balanceNative = balanceNative;
}
}
export class Serum3InfoDto {
reservedBase: I80F48Dto;
reservedQuote: I80F48Dto;
baseIndex: number;
quoteIndex: number;
marketIndex: number;
constructor(
reservedBase: I80F48Dto,
reservedQuote: I80F48Dto,
baseIndex: number,
quoteIndex: number,
) {
this.reservedBase = reservedBase;
this.reservedQuote = reservedQuote;
this.baseIndex = baseIndex;
this.quoteIndex = quoteIndex;
}
}
export class PerpInfoDto {
perpMarketIndex: number;
maintBaseAssetWeight: I80F48Dto;
initBaseAssetWeight: I80F48Dto;
maintBaseLiabWeight: I80F48Dto;
initBaseLiabWeight: I80F48Dto;
maintOverallAssetWeight: I80F48Dto;
initOverallAssetWeight: I80F48Dto;
public baseLotSize: BN;
public baseLots: BN;
public bidsBaseLots: BN;
public asksBaseLots: BN;
quote: I80F48Dto;
prices: { oracle: I80F48Dto; stable: I80F48Dto };
hasOpenOrders: boolean;
}