diff --git a/package.json b/package.json index 2bf807c5a..d83a7825f 100644 --- a/package.json +++ b/package.json @@ -42,8 +42,6 @@ "@typescript-eslint/parser": "^8.0.1", "axios": "^1.4.0", "chai": "^4.3.4", - "cli-table3": "^0.6.3", - "console-table-printer": "^2.11.1", "eslint": "^9.8.0", "eslint-config-prettier": "^9.1.0", "fast-csv": "^4.3.6", diff --git a/ts/client/scripts/update-risk-params.ts b/ts/client/scripts/update-risk-params.ts index da458191e..2f664351c 100644 --- a/ts/client/scripts/update-risk-params.ts +++ b/ts/client/scripts/update-risk-params.ts @@ -106,6 +106,7 @@ async function setupVsr( return vsrClient; } +/** unused async function getTotalLiqorEquity( client: MangoClient, group: Group, @@ -127,9 +128,8 @@ async function getTotalLiqorEquity( ) ).json() ).map((data) => new PublicKey(data['liqor'])); - const ttlLiqorEquity = ( - await getEquityForMangoAccounts(client, group, liqors, mangoAccounts) - ).reduce((partialSum, ae) => partialSum + ae.Equity.val, 0); + const ttlLiqorEquity = getEquityForMangoAccounts(client, group, liqors, mangoAccounts) + .reduce((partialSum, ae) => partialSum + ae.Equity.val, 0); return ttlLiqorEquity; } @@ -152,6 +152,8 @@ function getPriceImpactForBank( const priceImpact = tokenToPriceImpact[getApiTokenName(bank.name)]; return priceImpact; } + */ + async function updateTokenParams(): Promise { const [client, wallet] = await Promise.all([buildClient(), setupWallet()]); @@ -208,7 +210,8 @@ async function updateTokenParams(): Promise { const builder = Builder(NullTokenEditParams); let change = false; - // try { + // unused + /* const tier = Object.values(LISTING_PRESETS).find((x) => x.initLiabWeight.toFixed(1) === '1.8' ? x.initLiabWeight.toFixed(1) === @@ -217,6 +220,7 @@ async function updateTokenParams(): Promise { : x.initLiabWeight.toFixed(1) === bank?.initLiabWeight.toNumber().toFixed(1), ); + */ // eslint-disable-next-line no-constant-condition if (true) { diff --git a/ts/client/src/accounts/healthCache.ts b/ts/client/src/accounts/healthCache.ts index 744f10cf2..72dee8daf 100644 --- a/ts/client/src/accounts/healthCache.ts +++ b/ts/client/src/accounts/healthCache.ts @@ -44,19 +44,23 @@ import { MarketIndex, Serum3Market, Serum3Side } from './serum3'; // ██████████████████████████████████████████ // warning: this code is copy pasta from rust, keep in sync with health.rs + +/** + * WARNING this potentially modifies health & starting spot inplace + */ function spotAmountTakenForHealthZero( health: I80F48, startingSpot: I80F48, assetWeightedPrice: I80F48, liabWeightedPrice: I80F48, ): I80F48 { - if (health.lte(ZERO_I80F48())) { + if (!health.isPos()) { return ZERO_I80F48(); } let takenSpot = ZERO_I80F48(); - if (startingSpot.gt(ZERO_I80F48())) { - if (assetWeightedPrice.gt(ZERO_I80F48())) { + if (startingSpot.isPos()) { + if (assetWeightedPrice.isPos()) { const assetMax = health.div(assetWeightedPrice); if (assetMax.lte(startingSpot)) { return assetMax; @@ -65,8 +69,8 @@ function spotAmountTakenForHealthZero( takenSpot = startingSpot; health.isub(startingSpot.mul(assetWeightedPrice)); } - if (health.gt(ZERO_I80F48())) { - if (liabWeightedPrice.lte(ZERO_I80F48())) { + if (health.isPos()) { + if (!liabWeightedPrice.isPos()) { throw new Error('LiabWeightedPrice must be greater than 0!'); } takenSpot.iadd(health.div(liabWeightedPrice)); @@ -74,20 +78,6 @@ function spotAmountTakenForHealthZero( return takenSpot; } -function spotAmountGivenForHealthZero( - health: I80F48, - startingSpot: I80F48, - assetWeightedPrice: I80F48, - liabWeightedPrice: I80F48, -): I80F48 { - return spotAmountTakenForHealthZero( - health.neg(), - startingSpot.neg(), - liabWeightedPrice, - assetWeightedPrice, - ); -} - export class HealthCache { constructor( public tokenInfos: TokenInfo[], @@ -193,7 +183,7 @@ export class HealthCache { quoteAsset.div(baseLiab), ); let allReservedAsBase; - if (!info.reservedQuoteAsBaseHighestBid.eq(ZERO_I80F48())) { + if (!info.reservedQuoteAsBaseHighestBid.isZero()) { allReservedAsBase = reservedBase.add( reservedQuoteAsBaseOracle.min(info.reservedQuoteAsBaseHighestBid), ); @@ -207,7 +197,7 @@ export class HealthCache { baseAsset.div(quoteLiab), ); let allReservedAsQuote; - if (!info.reservedBaseAsQuoteLowestAsk.eq(ZERO_I80F48())) { + if (!info.reservedBaseAsQuoteLowestAsk.isZero()) { allReservedAsQuote = reservedQuote.add( reservedBaseAsQuoteOracle.min(info.reservedBaseAsQuoteLowestAsk), ); @@ -249,7 +239,7 @@ export class HealthCache { ); const perpSettleToken = tokenBalances[settleTokenIndex]; const healthUnsettled = perpInfo.healthUnsettledPnl(healthType); - if (!ignoreNegativePerp || healthUnsettled.gt(ZERO_I80F48())) { + if (!ignoreNegativePerp || healthUnsettled.isPos()) { perpSettleToken.spotAndPerp.iadd(healthUnsettled); } } @@ -287,7 +277,7 @@ export class HealthCache { group.getMintDecimalsByTokenIndex(perpInfo.settleTokenIndex), ), }); - if (!ignoreNegativePerp || healthUnsettled.gt(ZERO_I80F48())) { + if (!ignoreNegativePerp || healthUnsettled.isPos()) { perpSettleToken.spotAndPerp.iadd(healthUnsettled); } } @@ -509,11 +499,11 @@ export class HealthCache { public healthRatio(healthType: HealthType): I80F48 { const res = this.healthAssetsAndLiabsStableLiabs(healthType); - const hundred = I80F48.fromNumber(100); // console.log(`assets ${res.assets}`); // console.log(`liabs ${res.liabs}`); if (res.liabs.gt(I80F48.fromNumber(0.001))) { - return hundred.mul(res.assets.sub(res.liabs)).div(res.liabs); + const hundred = I80F48.fromNumber(100); + return hundred.imul(res.assets.sub(res.liabs)).idiv(res.liabs); } return MAX_I80F48(); } @@ -931,10 +921,10 @@ export class HealthCache { targetFn: (cache) => I80F48, ): I80F48 { if ( - sourceBank.initLiabWeight + !sourceBank.initLiabWeight .sub(targetBank.initAssetWeight) .abs() - .lte(ZERO_I80F48()) + .isPos() ) { return ZERO_I80F48(); } @@ -979,7 +969,7 @@ export class HealthCache { .mul(price), ); - if (finalHealthSlope.gte(ZERO_I80F48())) { + if (!finalHealthSlope.isNeg()) { return MAX_I80F48(); } @@ -1044,9 +1034,9 @@ export class HealthCache { const healthAtMaxValue = cacheAfterSwap(amountForMaxValue).health( HealthType.init, ); - if (healthAtMaxValue.eq(ZERO_I80F48())) { + if (healthAtMaxValue.isZero()) { return amountForMaxValue; - } else if (healthAtMaxValue.lt(ZERO_I80F48())) { + } else if (healthAtMaxValue.isNeg()) { return ZERO_I80F48(); } const zeroHealthEstimate = amountForMaxValue.sub( @@ -1106,7 +1096,7 @@ export class HealthCache { const initialAmount = ZERO_I80F48(); const initialHealth = this.health(HealthType.init); const initialRatio = this.healthRatio(HealthType.init); - if (initialRatio.lte(ZERO_I80F48())) { + if (!initialRatio.isPos()) { return ZERO_I80F48(); } @@ -1121,7 +1111,7 @@ export class HealthCache { // and when its a bid, then quote->bid let zeroAmount; if (side == Serum3Side.ask) { - const quoteBorrows = quote.balanceSpot.lt(ZERO_I80F48()) + const quoteBorrows = quote.balanceSpot.isNeg() ? quote.balanceSpot.abs().mul(quote.prices.liab(HealthType.init)) : ZERO_I80F48(); const max = base.balanceSpot.mul(base.prices.oracle).max(quoteBorrows); @@ -1138,7 +1128,7 @@ export class HealthCache { // console.log(` - quoteBorrows ${quoteBorrows.toLocaleString()}`); // console.log(` - max ${max.toLocaleString()}`); } else { - const baseBorrows = base.balanceSpot.lt(ZERO_I80F48()) + const baseBorrows = base.balanceSpot.isNeg() ? base.balanceSpot.abs().mul(base.prices.liab(HealthType.init)) : ZERO_I80F48(); const max = quote.balanceSpot.mul(quote.prices.oracle).max(baseBorrows); @@ -1221,7 +1211,7 @@ export class HealthCache { const healthCacheClone: HealthCache = deepClone(this); const initialRatio = this.healthRatio(HealthType.init); - if (initialRatio.lt(ZERO_I80F48())) { + if (initialRatio.isNeg()) { return ZERO_I80F48(); } @@ -1244,7 +1234,7 @@ export class HealthCache { .neg() .mul(prices.liab(HealthType.init)) .add(price); - if (finalHealthSlope.gte(ZERO_I80F48())) { + if (!finalHealthSlope.isNeg()) { return MAX_I80F48(); } finalHealthSlope.imul(settleInfo.liabWeightedPrice(HealthType.init)); @@ -1278,8 +1268,8 @@ export class HealthCache { // 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); + (initialBaseLots.isPos() && direction == -1) || + (initialBaseLots.isNeg() && direction == 1); let case1Start: I80F48, case1StartRatio: I80F48; if (hasCase2) { @@ -1310,7 +1300,7 @@ export class HealthCache { settleInfo.initAssetWeight = settleInfo.initLiabWeight; settleInfo.initScaledAssetWeight = settleInfo.initScaledLiabWeight; const startHealth = startCache.health(HealthType.init); - if (startHealth.lte(ZERO_I80F48())) { + if (!startHealth.isPos()) { return ZERO_I80F48(); } @@ -1373,7 +1363,7 @@ export class HealthCache { if (perpPosition.getBasePosition(perpMarket).isPos()) { const zero = ZERO_I80F48(); const healthAtPriceZero = healthAfterPriceChange(zero); - if (healthAtPriceZero.gt(ZERO_I80F48())) { + if (healthAtPriceZero.isPos()) { return null; } @@ -1794,7 +1784,7 @@ export class PerpInfo { if (this.settleTokenIndex !== settleToken.tokenIndex) { throw new Error('Settle token index should match!'); } - if (unweighted.gt(ZERO_I80F48())) { + if (unweighted.isPos()) { return ( healthType == HealthType.init ? settleToken.initScaledAssetWeight @@ -1820,7 +1810,7 @@ export class PerpInfo { unweighted: I80F48, healthType: HealthType | undefined, ): I80F48 { - if (unweighted.gt(ZERO_I80F48())) { + if (unweighted.isPos()) { return ( healthType == HealthType.init || healthType == HealthType.liquidationEnd ? this.initOverallAssetWeight diff --git a/ts/client/src/accounts/mangoAccount.ts b/ts/client/src/accounts/mangoAccount.ts index 8565f7322..7172f2e6c 100644 --- a/ts/client/src/accounts/mangoAccount.ts +++ b/ts/client/src/accounts/mangoAccount.ts @@ -311,10 +311,10 @@ export class MangoAccount { )) { const oo = this.serum3OosMapByMarketIndex.get(serum3Market.marketIndex); if (serum3Market.baseTokenIndex == bank.tokenIndex && oo) { - bal.add(I80F48.fromI64(oo.baseTokenFree)); + bal.iadd(I80F48.fromI64(oo.baseTokenFree)); } if (serum3Market.quoteTokenIndex == bank.tokenIndex && oo) { - bal.add(I80F48.fromI64(oo.quoteTokenFree)); + bal.iadd(I80F48.fromI64(oo.quoteTokenFree)); } } return bal; diff --git a/ts/client/src/risk.ts b/ts/client/src/risk.ts index 34e832c59..39465dd28 100644 --- a/ts/client/src/risk.ts +++ b/ts/client/src/risk.ts @@ -7,6 +7,8 @@ import { HealthType, MangoAccount } from './accounts/mangoAccount'; import { MangoClient } from './client'; import { I80F48, ONE_I80F48, ZERO_I80F48 } from './numbers/I80F48'; import { buildFetch, toUiDecimals, toUiDecimalsForQuote } from './utils'; +import { HealthCache } from './accounts/healthCache'; +import { ZERO } from '@raydium-io/raydium-sdk'; export interface LiqorPriceImpact { Coin: { val: string; highlight: boolean }; @@ -120,255 +122,259 @@ export async function getOnChainPriceForMints( ); } -export async function getPriceImpactForLiqor( +const maxLiabsValue = I80F48.fromNumber(99_999_999_999); + +export function getPriceImpactForLiqor( group: Group, pis: PriceImpact[], mangoAccounts: MangoAccount[], -): Promise { - const mangoAccounts_ = mangoAccounts.filter((a) => - a.getHealth(group, HealthType.maint).lt(ZERO_I80F48()), - ); +): LiqorPriceImpact[] { - const mangoAccountsWithHealth = mangoAccounts_.map((a: MangoAccount) => { - return { - account: a, - health: a.getHealth(group, HealthType.liquidationEnd), - healthRatio: a.getHealthRatioUi(group, HealthType.liquidationEnd), - }; - }); + + const mangoAccountsWithHealth = mangoAccounts.map((a: MangoAccount) => { + const hc = HealthCache.fromMangoAccount(group, a); + if (hc.health(HealthType.maint).isPos()) { + return { + account: a, + health: hc.health(HealthType.liquidationEnd), + }; + } + }).filter(a => !!a); const usdcBank = group.getFirstBankByTokenIndex(0 as TokenIndex); const usdcMint = usdcBank.mint; - return await Promise.all( - Array.from(group.banksMapByMint.values()) - .sort((a, b) => a[0].name.localeCompare(b[0].name)) - // .filter((banks) => banks[0].name == 'MSOL') - .map(async (banks) => { - const bank = banks[0]; - - // Sum of all liabs, these liabs would be acquired by liqor, - // who would immediately want to reduce them to 0 - // Assuming liabs need to be bought using USDC - const liabs = - // Max liab of a particular token that would be liquidated to bring health above 0 - mangoAccountsWithHealth.reduce((sum, a) => { - // How much would health increase for every unit liab moved to liqor - // liabprice * (liabweight - (1+liabfees)*(1+assetfees)*assetweight) - // Choose the most valuable asset the user has - const assetBank = Array.from(group.banksMapByTokenIndex.values()) - .flat() - .reduce((prev, curr) => - prev.initAssetWeight - .mul(a.account.getEffectiveTokenBalance(group, prev)) - .mul(prev._price!) - .gt( - curr.initAssetWeight.mul( - a.account - .getEffectiveTokenBalance(group, curr) - .mul(curr._price!), - ), - ) - ? prev - : curr, - ); - const feeFactor = ONE_I80F48() - .add(bank.liquidationFee) - .add(bank.platformLiquidationFee) - .mul( - ONE_I80F48() - .add(assetBank.liquidationFee) - .add(assetBank.platformLiquidationFee), - ); - const tokenLiabHealthContrib = bank.price.mul( - bank.initLiabWeight.sub(feeFactor.mul(assetBank.initAssetWeight)), - ); - // Abs liab/borrow - const maxTokenLiab = a.account - .getEffectiveTokenBalance(group, bank) - .min(ZERO_I80F48()) - .abs(); - - if (tokenLiabHealthContrib.eq(ZERO_I80F48())) { - return sum.add(maxTokenLiab); - } - - // Health under 0 - const maxLiab = a.health - .min(ZERO_I80F48()) - .abs() - .div(tokenLiabHealthContrib) - .min(maxTokenLiab); - - return sum.add(maxLiab); - }, ZERO_I80F48()); - const liabsInUsdc = - // convert to usdc, this is an approximation - liabs - .mul(bank.price) - .floor() - // jup oddity - .min(I80F48.fromNumber(99999999999)); - - // Sum of all assets which would be acquired in exchange for also acquiring - // liabs by the liqor, who would immediately want to reduce to 0 - // Assuming assets need to be sold to USDC - const assets = mangoAccountsWithHealth.reduce((sum, a) => { + return Array.from(group.banksMapByMint.values()) + .sort((a, b) => a[0].name.localeCompare(b[0].name)) + .map((banks) => { + const bank = banks[0]; + + // Sum of all liabs, these liabs would be acquired by liqor, + // who would immediately want to reduce them to 0 + // Assuming liabs need to be bought using USDC + const liabs = + // Max liab of a particular token that would be liquidated to bring health above 0 + mangoAccountsWithHealth.reduce((sum, a) => { // How much would health increase for every unit liab moved to liqor - // assetprice * (liabweight/(1+liabliqfee) - assetweight) - // Choose the smallest liability the user has - const liabBank = Array.from(group.banksMapByTokenIndex.values()) + // liabprice * (liabweight - (1+liabfees)*(1+assetfees)*assetweight) + // Choose the most valuable asset the user has + const assetBank = Array.from(group.banksMapByTokenIndex.values()) .flat() .reduce((prev, curr) => - prev.initLiabWeight - .mul(a.account.getEffectiveTokenBalance(group, prev)) - .mul(prev._price!) - .lt( - curr.initLiabWeight.mul( - a.account - .getEffectiveTokenBalance(group, curr) - .mul(curr._price!), - ), + a.account.getEffectiveTokenBalance(group, prev) + .imul(prev.initAssetWeight) + .imul(prev._price!) + .gt( + a.account + .getEffectiveTokenBalance(group, curr) + .imul(curr.initAssetWeight) + .imul(curr._price!) ) ? prev : curr, ); - const tokenAssetHealthContrib = bank.price.mul( - liabBank.initLiabWeight - .div(ONE_I80F48().add(liabBank.liquidationFee)) - .sub(bank.initAssetWeight), + + const feeFactor = ONE_I80F48() + .iadd(bank.liquidationFee) + .iadd(bank.platformLiquidationFee) + .imul( + ONE_I80F48() + .iadd(assetBank.liquidationFee) + .iadd(assetBank.platformLiquidationFee), + ); + const tokenLiabHealthContrib = bank.price.mul( + bank.initLiabWeight.sub(feeFactor.mul(assetBank.initAssetWeight)), ); - // Abs collateral/asset - const maxTokenHealthAsset = a.account + // Abs liab/borrow + const maxTokenLiab = a.account .getEffectiveTokenBalance(group, bank) - .max(ZERO_I80F48()); + .min(ZERO_I80F48()) + .abs(); - if (tokenAssetHealthContrib.eq(ZERO_I80F48())) { - return sum.add(maxTokenHealthAsset); + if (tokenLiabHealthContrib.isZero()) { + return sum.iadd(maxTokenLiab); } - const maxAsset = a.health + // Health under 0 + const maxLiab = a.health .min(ZERO_I80F48()) .abs() - .div(tokenAssetHealthContrib) - .min(maxTokenHealthAsset); + .div(tokenLiabHealthContrib) + .min(maxTokenLiab); - return sum.add(maxAsset); + return sum.iadd(maxLiab); }, ZERO_I80F48()); - - const pi1 = - !liabsInUsdc.eq(ZERO_I80F48()) && - usdcMint.toBase58() !== bank.mint.toBase58() - ? computePriceImpactOnJup( - pis, - toUiDecimalsForQuote(liabsInUsdc), - bank.name, - ) - : 0; - const pi2 = - !assets.eq(ZERO_I80F48()) && - usdcMint.toBase58() !== bank.mint.toBase58() - ? computePriceImpactOnJup( - pis, - toUiDecimals(assets.mul(bank.price), bank.mintDecimals), - bank.name, + const liabsInUsdc = + // convert to usdc, this is an approximation + liabs + .mul(bank.price) + .floor() + // jup oddity + .min(maxLiabsValue); + + // Sum of all assets which would be acquired in exchange for also acquiring + // liabs by the liqor, who would immediately want to reduce to 0 + // Assuming assets need to be sold to USDC + const assets = mangoAccountsWithHealth.reduce((sum, a) => { + // How much would health increase for every unit liab moved to liqor + // assetprice * (liabweight/(1+liabliqfee) - assetweight) + // Choose the smallest liability the user has + const liabBank = Array.from(group.banksMapByTokenIndex.values()) + .flat() + .reduce((prev, curr) => + a.account.getEffectiveTokenBalance(group, prev) + .imul(prev.initLiabWeight) + .imul(prev._price!) + .lt( + a.account + .getEffectiveTokenBalance(group, curr) + .imul(curr.initLiabWeight) + .imul(curr._price!) ) - : 0; + ? prev + : curr, + ); + const tokenAssetHealthContrib = bank.price.mul( + liabBank.initLiabWeight + .div(ONE_I80F48().add(liabBank.liquidationFee)) + .sub(bank.initAssetWeight), + ); + + // Abs collateral/asset + const maxTokenHealthAsset = a.account + .getEffectiveTokenBalance(group, bank) + .max(ZERO_I80F48()); + + if (tokenAssetHealthContrib.isZero()) { + return sum.iadd(maxTokenHealthAsset); + } + + const maxAsset = a.health + .min(ZERO_I80F48()) + .abs() + .div(tokenAssetHealthContrib) + .min(maxTokenHealthAsset); + + return sum.iadd(maxAsset); + }, ZERO_I80F48()); + + const pi1 = + !liabsInUsdc.isZero() && + !usdcMint.equals(bank.mint) + ? computePriceImpactOnJup( + pis, + toUiDecimalsForQuote(liabsInUsdc), + bank.name, + ) + : 0; + const pi2 = + !assets.isZero() && + !usdcMint.equals(bank.mint) + ? computePriceImpactOnJup( + pis, + toUiDecimals(assets.mul(bank.price), bank.mintDecimals), + bank.name, + ) + : 0; - return { - Coin: { val: bank.name, highlight: false }, - 'Oracle Price': { - val: bank['oldUiPrice'] ? bank['oldUiPrice'] : bank._uiPrice!, - highlight: false, - }, - 'Jup Price': { - val: bank['onChainPrice'], - highlight: - Math.abs( - (bank['onChainPrice'] - - (bank['oldUiPrice'] ? bank['oldUiPrice'] : bank._uiPrice!)) / - (bank['oldUiPrice'] ? bank['oldUiPrice'] : bank._uiPrice!), - ) > 0.05, - }, - 'Future Price': { val: bank._uiPrice!, highlight: false }, - 'V4 Liq Fee': { - val: Math.round(bank.liquidationFee.toNumber() * 10000), - highlight: false, - }, - Liabs: { - val: Math.round(toUiDecimalsForQuote(liabsInUsdc)), - highlight: Math.round(toUiDecimalsForQuote(liabsInUsdc)) > 5000, - }, - 'Liabs Slippage': { - val: Math.round(pi1), - highlight: - Math.round(pi1) > - Math.round(bank.liquidationFee.toNumber() * 10000), - }, - Assets: { - val: Math.round( - toUiDecimals(assets, bank.mintDecimals) * bank.uiPrice, - ), - highlight: - Math.round( - toUiDecimals(assets, bank.mintDecimals) * bank.uiPrice, - ) > 5000, - }, - 'Assets Slippage': { - val: Math.round(pi2), - highlight: - Math.round(pi2) > - Math.round(bank.liquidationFee.toNumber() * 10000), - }, - }; - }), - ); + return { + Coin: { val: bank.name, highlight: false }, + 'Oracle Price': { + val: bank['oldUiPrice'] ? bank['oldUiPrice'] : bank._uiPrice!, + highlight: false, + }, + 'Jup Price': { + val: bank['onChainPrice'], + highlight: + Math.abs( + (bank['onChainPrice'] - + (bank['oldUiPrice'] ? bank['oldUiPrice'] : bank._uiPrice!)) / + (bank['oldUiPrice'] ? bank['oldUiPrice'] : bank._uiPrice!), + ) > 0.05, + }, + 'Future Price': { val: bank._uiPrice!, highlight: false }, + 'V4 Liq Fee': { + val: Math.round(bank.liquidationFee.toNumber() * 10000), + highlight: false, + }, + Liabs: { + val: Math.round(toUiDecimalsForQuote(liabsInUsdc)), + highlight: Math.round(toUiDecimalsForQuote(liabsInUsdc)) > 5000, + }, + 'Liabs Slippage': { + val: Math.round(pi1), + highlight: + Math.round(pi1) > + Math.round(bank.liquidationFee.toNumber() * 10000), + }, + Assets: { + val: Math.round( + toUiDecimals(assets, bank.mintDecimals) * bank.uiPrice, + ), + highlight: + Math.round(toUiDecimals(assets, bank.mintDecimals) * bank.uiPrice) > + 5000, + }, + 'Assets Slippage': { + val: Math.round(pi2), + highlight: + Math.round(pi2) > + Math.round(bank.liquidationFee.toNumber() * 10000), + }, + }; + }); } -export async function getPerpPositionsToBeLiquidated( +export function getPerpPositionsToBeLiquidated( group: Group, mangoAccounts: MangoAccount[], -): Promise { +): PerpPositionsToBeLiquidated[] { const mangoAccountsWithHealth = mangoAccounts.map((a: MangoAccount) => { return { account: a, health: a.getHealth(group, HealthType.liquidationEnd), - healthRatio: a.getHealthRatioUi(group, HealthType.liquidationEnd), }; }); return Array.from(group.perpMarketsMapByMarketIndex.values()) .filter((pm) => !pm.name.includes('OLD')) .map((pm) => { + + const assetHealthPerLot = + I80F48.fromNumber(-1) + .mul(pm.price) + .mul(I80F48.fromU64(pm.baseLotSize)) + .mul(pm.initBaseAssetWeight) + .add( + I80F48.fromU64(pm.baseLotSize) + .mul(pm.price) + .mul( + ONE_I80F48() // quoteInitAssetWeight + .mul(ONE_I80F48().sub(pm.baseLiquidationFee)), + ), + ); + + const liabWeightPerLot = + pm.price + .mul(I80F48.fromU64(pm.baseLotSize)) + .mul(pm.initBaseLiabWeight) + .sub( + I80F48.fromU64(pm.baseLotSize) + .mul(pm.price) + .mul(ONE_I80F48()) // quoteInitLiabWeight + .mul(ONE_I80F48().add(pm.baseLiquidationFee)), + ); + const baseLots = mangoAccountsWithHealth .filter((a) => a.account.getPerpPosition(pm.perpMarketIndex)) .reduce((sum, a) => { const baseLots = a.account.getPerpPosition( pm.perpMarketIndex, )!.basePositionLots; - const unweightedHealthPerLot = baseLots.gt(new BN(0)) - ? I80F48.fromNumber(-1) - .mul(pm.price) - .mul(I80F48.fromU64(pm.baseLotSize)) - .mul(pm.initBaseAssetWeight) - .add( - I80F48.fromU64(pm.baseLotSize) - .mul(pm.price) - .mul( - ONE_I80F48() // quoteInitAssetWeight - .mul(ONE_I80F48().sub(pm.baseLiquidationFee)), - ), - ) - : pm.price - .mul(I80F48.fromU64(pm.baseLotSize)) - .mul(pm.initBaseLiabWeight) - .sub( - I80F48.fromU64(pm.baseLotSize) - .mul(pm.price) - .mul(ONE_I80F48()) // quoteInitLiabWeight - .mul(ONE_I80F48().add(pm.baseLiquidationFee)), - ); + const unweightedHealthPerLot = baseLots.gt(ZERO) + ? assetHealthPerLot + : liabWeightPerLot; const maxBaseLots = a.health .min(ZERO_I80F48()) @@ -376,7 +382,7 @@ export async function getPerpPositionsToBeLiquidated( .div(unweightedHealthPerLot.abs()) .min(I80F48.fromU64(baseLots).abs()); - return sum.add(maxBaseLots); + return sum.iadd(maxBaseLots); }, ONE_I80F48()); const notionalPositionUi = toUiDecimalsForQuote( @@ -395,12 +401,12 @@ export async function getPerpPositionsToBeLiquidated( }); } -export async function getEquityForMangoAccounts( +export function getEquityForMangoAccounts( client: MangoClient, group: Group, mangoAccountPks: PublicKey[], allMangoAccounts: MangoAccount[], -): Promise { +): AccountEquity[] { const mangoAccounts = allMangoAccounts.filter((a) => mangoAccountPks.find((pk) => pk.equals(a.publicKey)), ); @@ -542,8 +548,8 @@ export async function buildGroupGrid( // Compute how much of an asset would need to be liquidated // when group (i.e. asset prices) reach a specific state - return await Promise.all( - groups.map((g) => getPriceImpactForLiqor(g, pis, mangoAccountsSubset)), + return groups.map((g) => + getPriceImpactForLiqor(g, pis, mangoAccountsSubset), ); } } @@ -660,13 +666,13 @@ export async function getRiskStats( const mangoAccounts = await client.getAllMangoAccounts(group, true); // Get on chain prices - const mints = [ - ...new Set( - Array.from(group.banksMapByTokenIndex.values()) - .flat() - .map((bank) => bank.mint.toString()), - ), - ]; + // const mints = [ + // ...new Set( + // Array.from(group.banksMapByTokenIndex.values()) + // .flat() + // .map((bank) => bank.mint.toString()), + // ), + // ]; // Note: // Disable for now @@ -744,25 +750,26 @@ export async function getRiskStats( p._price = p._price?.mul(I80F48.fromNumber(rally)); }); - const [ - assetDrop, - assetRally, - usdcDepeg, - usdtDepeg, - perpDrop, - perpRally, - liqorEquity, - marketMakerEquity, - ] = await Promise.all([ - getPriceImpactForLiqor(groupDrop, pis, mangoAccounts), - getPriceImpactForLiqor(groupRally, pis, mangoAccounts), - getPriceImpactForLiqor(groupUsdcDepeg, pis, mangoAccounts), - getPriceImpactForLiqor(groupUsdtDepeg, pis, mangoAccounts), - getPerpPositionsToBeLiquidated(groupDrop, mangoAccounts), - getPerpPositionsToBeLiquidated(groupRally, mangoAccounts), - getEquityForMangoAccounts(client, group, liqors, mangoAccounts), - getEquityForMangoAccounts(client, group, mms, mangoAccounts), - ]); + const assetDrop = getPriceImpactForLiqor(groupDrop, pis, mangoAccounts); + const assetRally = getPriceImpactForLiqor(groupRally, pis, mangoAccounts); + const usdcDepeg = getPriceImpactForLiqor(groupUsdcDepeg, pis, mangoAccounts); + const usdtDepeg = getPriceImpactForLiqor(groupUsdtDepeg, pis, mangoAccounts); + + const perpDrop = getPerpPositionsToBeLiquidated(groupDrop, mangoAccounts); + const perpRally = getPerpPositionsToBeLiquidated(groupRally, mangoAccounts); + + const liqorEquity = getEquityForMangoAccounts( + client, + group, + liqors, + mangoAccounts, + ); + const marketMakerEquity = getEquityForMangoAccounts( + client, + group, + mms, + mangoAccounts, + ); return { assetDrop: { diff --git a/yarn.lock b/yarn.lock index 418d9a1e5..2abff84a2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -28,11 +28,6 @@ node-fetch "3.3.2" ws "^8.18.0" -"@colors/colors@1.5.0": - version "1.5.0" - resolved "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz" - integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== - "@coral-xyz/anchor-30@npm:@coral-xyz/anchor@0.30.1", "switchboard-anchor@npm:@coral-xyz/anchor@0.30.1": version "0.30.1" resolved "https://registry.npmjs.org/@coral-xyz/anchor/-/anchor-0.30.1.tgz" @@ -1039,7 +1034,7 @@ "@ungap/promise-all-settled@1.1.2": version "1.1.2" - resolved "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz" + resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44" integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q== JSONStream@^1.3.5: @@ -1093,12 +1088,12 @@ ajv@^6.12.4: ansi-colors@4.1.1: version "4.1.1" - resolved "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== ansi-regex@^5.0.1: version "5.0.1" - resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== ansi-regex@^6.0.1: @@ -1287,7 +1282,7 @@ brorand@^1.1.0: browser-stdout@1.3.1: version "1.3.1" - resolved "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== bs58@^4.0.0, bs58@^4.0.1: @@ -1379,7 +1374,7 @@ check-error@^1.0.3: chokidar@3.5.3: version "3.5.3" - resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== dependencies: anymatch "~3.1.2" @@ -1392,15 +1387,6 @@ chokidar@3.5.3: optionalDependencies: fsevents "~2.3.2" -cli-table3@^0.6.3: - version "0.6.5" - resolved "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz" - integrity sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ== - dependencies: - string-width "^4.2.0" - optionalDependencies: - "@colors/colors" "1.5.0" - cliui@^7.0.2: version "7.0.4" resolved "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz" @@ -1449,13 +1435,6 @@ concat-map@0.0.1: resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== -console-table-printer@^2.11.1: - version "2.12.1" - resolved "https://registry.npmjs.org/console-table-printer/-/console-table-printer-2.12.1.tgz" - integrity sha512-wKGOQRRvdnd89pCeH96e2Fn4wkbenSP6LMHfjfyNLMbGuHEFbMqQNuxXqd0oXG9caIOQ1FTvc5Uijp9/4jujnQ== - dependencies: - simple-wcswidth "^1.0.1" - crc@^4.1.0: version "4.3.2" resolved "https://registry.npmjs.org/crc/-/crc-4.3.2.tgz" @@ -1508,7 +1487,7 @@ debug@4, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: debug@4.3.3: version "4.3.3" - resolved "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== dependencies: ms "2.1.2" @@ -1552,7 +1531,7 @@ delayed-stream@~1.0.0: diff@5.0.0: version "5.0.0" - resolved "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== diff@^3.1.0: @@ -1958,7 +1937,7 @@ glob-parent@^6.0.2: glob@7.2.0: version "7.2.0" - resolved "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== dependencies: fs.realpath "^1.0.0" @@ -2004,7 +1983,7 @@ graphemer@^1.4.0: growl@1.10.5: version "1.10.5" - resolved "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz" + resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== has-flag@^4.0.0: @@ -2022,7 +2001,7 @@ hash.js@^1.0.0, hash.js@^1.0.3: he@1.2.0: version "1.2.0" - resolved "https://registry.npmjs.org/he/-/he-1.2.0.tgz" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== hmac-drbg@^1.0.1: @@ -2321,7 +2300,7 @@ lodash@^4.17.21: log-symbols@4.1.0: version "4.1.0" - resolved "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== dependencies: chalk "^4.1.0" @@ -2415,7 +2394,7 @@ minimalistic-crypto-utils@^1.0.1: minimatch@4.2.1: version "4.2.1" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-4.2.1.tgz" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-4.2.1.tgz#40d9d511a46bdc4e563c22c3080cde9c0d8299b4" integrity sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g== dependencies: brace-expansion "^1.1.7" @@ -2460,7 +2439,7 @@ mkdirp@^0.5.1: mocha@^9.1.3: version "9.2.2" - resolved "https://registry.npmjs.org/mocha/-/mocha-9.2.2.tgz" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-9.2.2.tgz#d70db46bdb93ca57402c809333e5a84977a88fb9" integrity sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g== dependencies: "@ungap/promise-all-settled" "1.1.2" @@ -2500,7 +2479,7 @@ ms@2.1.3, ms@^2.0.0: nanoid@3.3.1: version "3.3.1" - resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.1.tgz#6347a18cac88af88f58af0b3594b723d5e99bb35" integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw== natural-compare@^1.4.0: @@ -2614,7 +2593,7 @@ path-exists@^4.0.0: path-is-absolute@^1.0.0: version "1.0.1" - resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== path-key@^3.1.0: @@ -2789,7 +2768,7 @@ semver@^7.6.0: serialize-javascript@6.0.0: version "6.0.0" - resolved "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8" integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag== dependencies: randombytes "^2.1.0" @@ -2819,11 +2798,6 @@ signal-exit@^4.0.1: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== -simple-wcswidth@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/simple-wcswidth/-/simple-wcswidth-1.0.1.tgz" - integrity sha512-xMO/8eNREtaROt7tJvWJqHBDTMFN4eiQ5I4JRMuilwfnFcV5W9u7RUkueNkdw0jPqGMX36iCywelS5yilTuOxg== - slash@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz" @@ -2913,7 +2887,7 @@ superstruct@^2.0.2: supports-color@8.1.1: version "8.1.1" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== dependencies: has-flag "^4.0.0" @@ -2979,7 +2953,7 @@ ts-ev@^0.4.0: ts-mocha@^10.0.0: version "10.0.0" - resolved "https://registry.npmjs.org/ts-mocha/-/ts-mocha-10.0.0.tgz" + resolved "https://registry.yarnpkg.com/ts-mocha/-/ts-mocha-10.0.0.tgz#41a8d099ac90dbbc64b06976c5025ffaebc53cb9" integrity sha512-VRfgDO+iiuJFlNB18tzOfypJ21xn2xbuZyDvJvqpTbWgkAgD17ONGr8t+Tl8rcBtOBdjXp5e/Rk+d39f7XBHRw== dependencies: ts-node "7.0.1" @@ -3148,7 +3122,7 @@ word-wrap@^1.2.5: workerpool@6.2.0: version "6.2.0" - resolved "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.0.tgz#827d93c9ba23ee2019c3ffaff5c27fccea289e8b" integrity sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A== "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": @@ -3210,7 +3184,7 @@ yargs-parser@20.2.4, yargs-parser@^20.2.2: yargs-unparser@2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz" + resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb" integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA== dependencies: camelcase "^6.0.0" @@ -3220,7 +3194,7 @@ yargs-unparser@2.0.0: yargs@16.2.0: version "16.2.0" - resolved "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== dependencies: cliui "^7.0.2"