merge deploy changes to dev (#586)
* expose perp order type on perp order Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * v0.9.17 * Fix funding rate method Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * Fix scrript Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * v0.9.18 * ts-client v0.9.19 * fix script * update reduce only and force close flags in ts client Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * v0.13.1 * expose underlying property Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * v0.13.2 * Fix bug in closing mango account (#559) * reafactor code for collecting health accounts, fix bug where bank oracle was skipped while closing account Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * Fixes from review Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * Fixes from review Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * Fixes from review Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * Fixes from review Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * Fixes from review Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> --------- Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * v0.13.3 * fix client code for building health accounts Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * v0.13.4 * Fix bug in sim max serum3 bid Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * v0.13.5 * increase charge Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * ts-client v0.14.0 * Fix getBorrowRate() to include loan upkeep * ts-client v0.14.1 * Client: Move jup's CU ix outside of flash loan That makes a flash loan based jup swap usable with delegates. * liquidator: Don't attempt to close in-use token positions This could happen if the user manually used serum on the liquidator account. * Mc/ci cd (#570) * prettier Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * Fix branch Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> --------- Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * rename Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * Increase iterations for max swap to fix some edge case, fix debug script since fees are already accounted for Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * v0.14.2 * Risk notification bot (#565) * risk stuff Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * Fixes from review Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * Fixes from review Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * Fixes from review Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * cleanup Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * client function Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> --------- Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * fix Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * v0.15.0 * fix risk computati Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * Fixes from review Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * v0.15.2 * Fix units Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * dont drop or rally stable assets Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * dont skip usdc Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * Fixes from review Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * v0.15.3 * Fix Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * v0.15.4 * update Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * v0.15.5 * update Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * v0.15.6 * add highlight Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * v0.15.7 * Fix math Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * Fix Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * v0.15.10 * Fix Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * v0.15.12 --------- Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> Co-authored-by: Christian Kamm <mail@ckamm.de>
This commit is contained in:
parent
163f42e998
commit
5d31d6bf32
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@blockworks-foundation/mango-v4",
|
||||
"version": "0.13.1",
|
||||
"version": "0.15.12",
|
||||
"description": "Typescript Client for mango-v4 program.",
|
||||
"repository": "https://github.com/blockworks-foundation/mango-v4",
|
||||
"author": {
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
"requoteThresh": 0.0002,
|
||||
"takeSpammers": true,
|
||||
"spammerCharge": 2,
|
||||
"charge": 0.002,
|
||||
"charge": 0.003,
|
||||
"krakenCode": "XXBTZUSD"
|
||||
}
|
||||
},
|
||||
|
@ -23,7 +23,7 @@
|
|||
"requoteThresh": 0.0002,
|
||||
"takeSpammers": true,
|
||||
"spammerCharge": 2,
|
||||
"charge": 0.002,
|
||||
"charge": 0.003,
|
||||
"krakenCode": "SOLUSD"
|
||||
}
|
||||
},
|
||||
|
@ -35,7 +35,7 @@
|
|||
"requoteThresh": 0.0002,
|
||||
"takeSpammers": true,
|
||||
"spammerCharge": 2,
|
||||
"charge": 0.002,
|
||||
"charge": 0.003,
|
||||
"krakenCode": "XETHZUSD"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,8 @@
|
|||
import { AnchorProvider, BN, Wallet } from '@coral-xyz/anchor';
|
||||
import { AnchorProvider, Wallet } from '@coral-xyz/anchor';
|
||||
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
|
||||
import { Table } from 'console-table-printer';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import fetch from 'node-fetch';
|
||||
import { Group } from '../src/accounts/group';
|
||||
import { HealthType, MangoAccount } from '../src/accounts/mangoAccount';
|
||||
import { MangoClient } from '../src/client';
|
||||
import { MANGO_V4_ID } from '../src/constants';
|
||||
import { I80F48, ONE_I80F48, ZERO_I80F48 } from '../src/numbers/I80F48';
|
||||
import { toUiDecimals, toUiDecimalsForQuote } from '../src/utils';
|
||||
import { getRiskStats } from '../src/risk';
|
||||
|
||||
const { MB_CLUSTER_URL } = process.env;
|
||||
|
||||
|
@ -33,408 +27,14 @@ async function buildClient(): Promise<MangoClient> {
|
|||
);
|
||||
}
|
||||
|
||||
async function computePriceImpactOnJup(
|
||||
amount: string,
|
||||
inputMint: string,
|
||||
outputMint: string,
|
||||
): Promise<{ outAmount: number; priceImpactPct: number }> {
|
||||
const url = `https://quote-api.jup.ag/v4/quote?inputMint=${inputMint}&outputMint=${outputMint}&amount=${amount}&swapMode=ExactIn&slippageBps=10000&onlyDirectRoutes=false&asLegacyTransaction=false`;
|
||||
const response = await fetch(url);
|
||||
|
||||
try {
|
||||
let res = await response.json();
|
||||
res = res.data[0];
|
||||
return {
|
||||
outAmount: parseFloat(res.outAmount),
|
||||
priceImpactPct: parseFloat(res.priceImpactPct),
|
||||
};
|
||||
} catch (e) {
|
||||
console.log(url);
|
||||
console.log(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async function computePriceImpactForLiqor(
|
||||
group: Group,
|
||||
mangoAccounts: MangoAccount[],
|
||||
healthThresh: number,
|
||||
title: string,
|
||||
): Promise<void> {
|
||||
// Filter mango accounts below a certain health ration threshold
|
||||
const mangoAccountsWithHealth = mangoAccounts
|
||||
.map((a: MangoAccount) => {
|
||||
return {
|
||||
account: a,
|
||||
health: a.getHealth(group, HealthType.liquidationEnd),
|
||||
healthRatio: a.getHealthRatioUi(group, HealthType.liquidationEnd),
|
||||
liabs: toUiDecimalsForQuote(
|
||||
a.getLiabsValue(group, HealthType.liquidationEnd),
|
||||
),
|
||||
};
|
||||
})
|
||||
.filter((a) => a.healthRatio < healthThresh);
|
||||
|
||||
const table = new Table({
|
||||
columns: [
|
||||
{ name: 'Coin', alignment: 'right' },
|
||||
{ name: 'Oracle Price', alignment: 'right' },
|
||||
{ name: 'On-Chain Price', alignment: 'right' },
|
||||
{ name: 'Future Price', alignment: 'right' },
|
||||
{ name: 'V4 Liq Fee', alignment: 'right' },
|
||||
{ name: 'Liabs', alignment: 'right' },
|
||||
{ name: 'Liabs slippage', alignment: 'right' },
|
||||
{ name: 'Assets Sum', alignment: 'right' },
|
||||
{ name: 'Assets Slippage', alignment: 'right' },
|
||||
],
|
||||
});
|
||||
|
||||
const USDC_MINT = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v';
|
||||
const usdcBank = group.getFirstBankByMint(new PublicKey(USDC_MINT));
|
||||
|
||||
// For each token
|
||||
for (const banks of group.banksMapByMint.values()) {
|
||||
const bank = banks[0];
|
||||
|
||||
const onChainPrice = (
|
||||
await (
|
||||
await fetch(`https://price.jup.ag/v4/price?ids=${bank.mint}`)
|
||||
).json()
|
||||
)['data'][bank.mint.toBase58()]['price'];
|
||||
|
||||
if (bank.tokenIndex === usdcBank.tokenIndex) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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+fee)*assetweight)
|
||||
const tokenLiabHealthContrib = bank.price.mul(
|
||||
bank.initLiabWeight.sub(
|
||||
ONE_I80F48().add(bank.liquidationFee).mul(usdcBank.initAssetWeight),
|
||||
),
|
||||
);
|
||||
// Abs liab/borrow
|
||||
const maxTokenLiab = a.account
|
||||
.getEffectiveTokenBalance(group, bank)
|
||||
.min(ZERO_I80F48())
|
||||
.abs();
|
||||
// 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));
|
||||
const pi1 = !liabsInUsdc.eq(ZERO_I80F48())
|
||||
? await computePriceImpactOnJup(
|
||||
liabsInUsdc.toString(),
|
||||
USDC_MINT,
|
||||
bank.mint.toBase58(),
|
||||
)
|
||||
: { priceImpactPct: 0, outAmount: 0 };
|
||||
|
||||
// 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)
|
||||
const liabBank = Array.from(group.banksMapByTokenIndex.values())
|
||||
.flat()
|
||||
.reduce((prev, curr) =>
|
||||
prev.initLiabWeight.lt(curr.initLiabWeight) ? 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());
|
||||
const maxAsset = a.health
|
||||
.min(ZERO_I80F48())
|
||||
.abs()
|
||||
.div(tokenAssetHealthContrib)
|
||||
.min(maxTokenHealthAsset);
|
||||
|
||||
return sum.add(maxAsset);
|
||||
}, ZERO_I80F48());
|
||||
|
||||
const pi2 = !assets.eq(ZERO_I80F48())
|
||||
? await computePriceImpactOnJup(
|
||||
assets.floor().toString(),
|
||||
bank.mint.toBase58(),
|
||||
USDC_MINT,
|
||||
)
|
||||
: { priceImpactPct: 0 };
|
||||
|
||||
table.addRow({
|
||||
Coin: bank.name,
|
||||
'Oracle Price':
|
||||
bank['oldUiPrice'] < 0.1
|
||||
? bank['oldUiPrice']
|
||||
: bank['oldUiPrice'].toFixed(2),
|
||||
'On-Chain Price':
|
||||
onChainPrice < 0.1 ? onChainPrice : onChainPrice.toFixed(2),
|
||||
'Future Price':
|
||||
bank._uiPrice! < 0.1 ? bank._uiPrice! : bank._uiPrice!.toFixed(2),
|
||||
|
||||
'V4 Liq Fee': (bank.liquidationFee.toNumber() * 100).toFixed(2) + '%',
|
||||
Liabs: toUiDecimalsForQuote(liabsInUsdc).toLocaleString() + '$',
|
||||
'Liabs slippage': (pi1.priceImpactPct * 100).toFixed(2) + '%',
|
||||
'Assets Sum':
|
||||
(
|
||||
toUiDecimals(assets, bank.mintDecimals) * bank.uiPrice
|
||||
).toLocaleString() + '$',
|
||||
'Assets Slippage': (pi2.priceImpactPct * 100).toFixed(2) + '%',
|
||||
});
|
||||
}
|
||||
|
||||
const msg = title + '\n```\n' + table.render() + '\n```';
|
||||
console.log(msg);
|
||||
console.log();
|
||||
}
|
||||
|
||||
async function computePerpPositionsToBeLiquidated(
|
||||
group: Group,
|
||||
mangoAccounts: MangoAccount[],
|
||||
healthThresh: number,
|
||||
title: string,
|
||||
): Promise<void> {
|
||||
const mangoAccountsWithHealth = mangoAccounts
|
||||
.map((a: MangoAccount) => {
|
||||
return {
|
||||
account: a,
|
||||
health: a.getHealth(group, HealthType.liquidationEnd),
|
||||
healthRatio: a.getHealthRatioUi(group, HealthType.liquidationEnd),
|
||||
liabs: toUiDecimalsForQuote(
|
||||
a.getLiabsValue(group, HealthType.liquidationEnd),
|
||||
),
|
||||
};
|
||||
})
|
||||
.filter((a) => a.healthRatio < healthThresh);
|
||||
|
||||
const table = new Table({
|
||||
columns: [
|
||||
{ name: 'Market', alignment: 'right' },
|
||||
{ name: 'Price', alignment: 'right' },
|
||||
{ name: 'Future Price', alignment: 'right' },
|
||||
{ name: 'Notional Position', alignment: 'right' },
|
||||
],
|
||||
});
|
||||
|
||||
for (const pm of Array.from(
|
||||
group.perpMarketsMapByMarketIndex.values(),
|
||||
).filter((pm) => !pm.name.includes('OLD'))) {
|
||||
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 maxBaseLots = a.health
|
||||
.min(ZERO_I80F48())
|
||||
.abs()
|
||||
.div(unweightedHealthPerLot.abs())
|
||||
.min(I80F48.fromU64(baseLots).abs());
|
||||
|
||||
return sum.add(maxBaseLots);
|
||||
}, ONE_I80F48());
|
||||
|
||||
const notionalPositionUi = toUiDecimalsForQuote(
|
||||
baseLots.mul(I80F48.fromU64(pm.baseLotSize).mul(pm.price)),
|
||||
);
|
||||
|
||||
table.addRow({
|
||||
Market: pm.name,
|
||||
Price:
|
||||
pm['oldUiPrice'] < 0.1 ? pm['oldUiPrice'] : pm['oldUiPrice'].toFixed(2),
|
||||
'Future Price':
|
||||
pm._uiPrice! < 0.1 ? pm._uiPrice! : pm._uiPrice!.toFixed(2),
|
||||
'Notional Position': notionalPositionUi.toLocaleString() + '$',
|
||||
});
|
||||
}
|
||||
const msg = title + '\n```\n' + table.render() + '\n```';
|
||||
console.log(msg);
|
||||
console.log();
|
||||
}
|
||||
|
||||
async function logLiqorEquity(
|
||||
client: MangoClient,
|
||||
group: Group,
|
||||
mangoAccounts: PublicKey[],
|
||||
title: string,
|
||||
): Promise<void> {
|
||||
const table = new Table({
|
||||
columns: [
|
||||
{ name: 'Account', alignment: 'right' },
|
||||
{ name: 'Equity', alignment: 'right' },
|
||||
],
|
||||
});
|
||||
|
||||
// Filter mango accounts which might be closed
|
||||
const liqors = (
|
||||
await client.connection.getMultipleAccountsInfo(mangoAccounts)
|
||||
)
|
||||
.map((ai, i) => {
|
||||
return { ai: ai, pk: mangoAccounts[i] };
|
||||
})
|
||||
.filter((val) => val.ai)
|
||||
.map((val) => val.pk);
|
||||
const liqorMangoAccounts = await Promise.all(
|
||||
liqors.map((liqor) => client.getMangoAccount(liqor, true)),
|
||||
);
|
||||
liqorMangoAccounts.forEach((a: MangoAccount) => {
|
||||
table.addRow({
|
||||
Account: a.publicKey.toBase58(),
|
||||
Equity: toUiDecimalsForQuote(a.getEquity(group)).toLocaleString() + '$',
|
||||
});
|
||||
});
|
||||
const msg = title + '\n```\n' + table.render() + '\n```';
|
||||
console.log(msg);
|
||||
console.log();
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const client = await buildClient();
|
||||
const group = await client.getGroup(new PublicKey(GROUP_PK));
|
||||
const mangoAccounts = await client.getAllMangoAccounts(group, true);
|
||||
|
||||
const change = 0.4;
|
||||
|
||||
const drop = 1 - change;
|
||||
const groupBear: Group = cloneDeep(group);
|
||||
Array.from(groupBear.banksMapByTokenIndex.values())
|
||||
.flat()
|
||||
.forEach((b) => {
|
||||
b['oldUiPrice'] = b._uiPrice;
|
||||
b._uiPrice = b._uiPrice! * drop;
|
||||
b._price = b._price?.mul(I80F48.fromNumber(drop));
|
||||
});
|
||||
Array.from(groupBear.perpMarketsMapByMarketIndex.values()).forEach((p) => {
|
||||
p['oldUiPrice'] = p._uiPrice;
|
||||
p._uiPrice = p._uiPrice! * drop;
|
||||
p._price = p._price?.mul(I80F48.fromNumber(drop));
|
||||
});
|
||||
|
||||
const rally = 1 + change;
|
||||
const groupBull: Group = cloneDeep(group);
|
||||
Array.from(groupBull.banksMapByTokenIndex.values())
|
||||
.flat()
|
||||
.forEach((b) => {
|
||||
b['oldUiPrice'] = b._uiPrice;
|
||||
b._uiPrice = b._uiPrice! * rally;
|
||||
b._price = b._price?.mul(I80F48.fromNumber(rally));
|
||||
});
|
||||
Array.from(groupBull.perpMarketsMapByMarketIndex.values()).forEach((p) => {
|
||||
p['oldUiPrice'] = p._uiPrice;
|
||||
p._uiPrice = p._uiPrice! * rally;
|
||||
p._price = p._price?.mul(I80F48.fromNumber(rally));
|
||||
});
|
||||
|
||||
const healthThresh = 0;
|
||||
|
||||
let tableName = `Liqors acquire liabs and assets. The assets and liabs are sum of max assets and max
|
||||
liabs for any token which would be liquidated to fix the health of a mango account.
|
||||
This would be the slippage they would face on buying-liabs/offloading-assets tokens acquired from unhealth accounts after a`;
|
||||
await computePriceImpactForLiqor(
|
||||
groupBear,
|
||||
mangoAccounts,
|
||||
healthThresh,
|
||||
`Table 1a: ${tableName} 20% drop`,
|
||||
);
|
||||
await computePriceImpactForLiqor(
|
||||
groupBull,
|
||||
mangoAccounts,
|
||||
healthThresh,
|
||||
`Table 1b: ${tableName} 20% rally`,
|
||||
);
|
||||
|
||||
tableName = 'Perp notional that liqor need to liquidate after a ';
|
||||
await computePerpPositionsToBeLiquidated(
|
||||
groupBear,
|
||||
mangoAccounts,
|
||||
healthThresh,
|
||||
`Table 2a: ${tableName} 20% drop`,
|
||||
);
|
||||
await computePerpPositionsToBeLiquidated(
|
||||
groupBull,
|
||||
mangoAccounts,
|
||||
healthThresh,
|
||||
`Table 2b: ${tableName} 20% rally`,
|
||||
);
|
||||
|
||||
await logLiqorEquity(
|
||||
client,
|
||||
group,
|
||||
(
|
||||
await (
|
||||
await fetch(
|
||||
`https://api.mngo.cloud/data/v4/stats/liqors-over_period?over_period=1MONTH`, // alternative - 1WEEK,
|
||||
)
|
||||
).json()
|
||||
).map((data) => new PublicKey(data['liqor'])),
|
||||
`Table 3: Equity of known liqors from last month`,
|
||||
);
|
||||
|
||||
await logLiqorEquity(
|
||||
client,
|
||||
group,
|
||||
[
|
||||
new PublicKey('CtHuPg2ctVVV7nqmvVEcMtcWyJAgtZw9YcNHFQidjPgF'),
|
||||
new PublicKey('F1SZxEDxxCSLVjEBbMEjDYqajWRJQRCZBwPQnmcVvTLV'),
|
||||
new PublicKey('BGYWnqfaauCeebFQXEfYuDCktiVG8pqpprrsD4qfqL53'),
|
||||
new PublicKey('9XJt2tvSZghsMAhWto1VuPBrwXsiimPtsTR8XwGgDxK2'),
|
||||
],
|
||||
`Table 4: Equity of known makers from last month`,
|
||||
);
|
||||
|
||||
// TODO warning when wrapper asset on chain price has too much difference to oracle
|
||||
// TODO warning when slippage is higher than liquidation fee
|
||||
// TODO warning when liqors equity is too low
|
||||
// TODO warning when mm equity is too low
|
||||
|
||||
// TODO all awaits are linear, should be parallelised to speed up script
|
||||
try {
|
||||
console.log(JSON.stringify(await getRiskStats(client, group), null, 2));
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
|
@ -407,18 +407,15 @@ export class Bank implements BankForHealth {
|
|||
|
||||
/**
|
||||
*
|
||||
* @returns borrow rate, 0 is 0% where 1 is 100%
|
||||
* @returns borrow rate, 0 is 0% where 1 is 100%; not including loan upkeep rate
|
||||
*/
|
||||
getBorrowRate(): I80F48 {
|
||||
getBorrowRateWithoutUpkeepRate(): I80F48 {
|
||||
const totalBorrows = this.nativeBorrows();
|
||||
const totalDeposits = this.nativeDeposits();
|
||||
|
||||
if (totalDeposits.isZero() && totalBorrows.isZero()) {
|
||||
return ZERO_I80F48();
|
||||
}
|
||||
if (totalDeposits.lte(totalBorrows)) {
|
||||
return this.maxRate;
|
||||
}
|
||||
|
||||
const utilization = totalBorrows.div(totalDeposits);
|
||||
if (utilization.lte(this.util0)) {
|
||||
|
@ -439,7 +436,15 @@ export class Bank implements BankForHealth {
|
|||
|
||||
/**
|
||||
*
|
||||
* @returns borrow rate percentage
|
||||
* @returns total borrow rate, 0 is 0% where 1 is 100% (including loan upkeep rate)
|
||||
*/
|
||||
getBorrowRate(): I80F48 {
|
||||
return this.getBorrowRateWithoutUpkeepRate().add(this.loanFeeRate);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns total borrow rate percentage (including loan upkeep rate)
|
||||
*/
|
||||
getBorrowRateUi(): number {
|
||||
return this.getBorrowRate().toNumber() * 100;
|
||||
|
|
|
@ -717,7 +717,7 @@ export class MangoAccount {
|
|||
quoteAmount = quoteAmount.div(
|
||||
ONE_I80F48().add(I80F48.fromNumber(serum3Market.getFeeRates(true))),
|
||||
);
|
||||
return toUiDecimals(nativeAmount, quoteBank.mintDecimals);
|
||||
return toUiDecimals(quoteAmount, quoteBank.mintDecimals);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1582,12 +1582,8 @@ export class MangoClient {
|
|||
const baseTokenIndex = serum3Market.baseTokenIndex;
|
||||
const quoteTokenIndex = serum3Market.quoteTokenIndex;
|
||||
// only include banks if no deposit has been previously made for same token
|
||||
if (!mangoAccount.getToken(baseTokenIndex)?.isActive()) {
|
||||
banks.push(group.getFirstBankByTokenIndex(baseTokenIndex));
|
||||
}
|
||||
if (!mangoAccount.getToken(quoteTokenIndex)?.isActive()) {
|
||||
banks.push(group.getFirstBankByTokenIndex(quoteTokenIndex));
|
||||
}
|
||||
banks.push(group.getFirstBankByTokenIndex(quoteTokenIndex));
|
||||
banks.push(group.getFirstBankByTokenIndex(baseTokenIndex));
|
||||
}
|
||||
|
||||
const healthRemainingAccounts: PublicKey[] =
|
||||
|
@ -3113,11 +3109,9 @@ export class MangoClient {
|
|||
): PublicKey[] {
|
||||
const healthRemainingAccounts: PublicKey[] = [];
|
||||
|
||||
const tokenPositionIndices = uniq(
|
||||
mangoAccounts
|
||||
.map((mangoAccount) => mangoAccount.tokens.map((t) => t.tokenIndex))
|
||||
.flat(),
|
||||
);
|
||||
const tokenPositionIndices = mangoAccounts
|
||||
.map((mangoAccount) => mangoAccount.tokens.map((t) => t.tokenIndex))
|
||||
.flat();
|
||||
for (const bank of banks) {
|
||||
const tokenPositionExists =
|
||||
tokenPositionIndices.indexOf(bank.tokenIndex) > -1;
|
||||
|
@ -3130,9 +3124,14 @@ export class MangoClient {
|
|||
}
|
||||
}
|
||||
}
|
||||
const mintInfos = tokenPositionIndices
|
||||
.filter((tokenIndex) => tokenIndex !== TokenPosition.TokenIndexUnset)
|
||||
.map((tokenIndex) => group.mintInfosMapByTokenIndex.get(tokenIndex)!);
|
||||
const mintInfos = uniq(
|
||||
tokenPositionIndices
|
||||
.filter((tokenIndex) => tokenIndex !== TokenPosition.TokenIndexUnset)
|
||||
.map((tokenIndex) => group.mintInfosMapByTokenIndex.get(tokenIndex)!),
|
||||
(mintInfo) => {
|
||||
mintInfo.tokenIndex;
|
||||
},
|
||||
);
|
||||
healthRemainingAccounts.push(
|
||||
...mintInfos.map((mintInfo) => mintInfo.firstBank()),
|
||||
);
|
||||
|
@ -3141,11 +3140,9 @@ export class MangoClient {
|
|||
);
|
||||
|
||||
// Insert any extra perp markets in the free perp position slots
|
||||
const perpPositionsMarketIndices = uniq(
|
||||
mangoAccounts
|
||||
.map((mangoAccount) => mangoAccount.perps.map((p) => p.marketIndex))
|
||||
.flat(),
|
||||
);
|
||||
const perpPositionsMarketIndices = mangoAccounts
|
||||
.map((mangoAccount) => mangoAccount.perps.map((p) => p.marketIndex))
|
||||
.flat();
|
||||
for (const perpMarket of perpMarkets) {
|
||||
const perpPositionExists =
|
||||
perpPositionsMarketIndices.indexOf(perpMarket.perpMarketIndex) > -1;
|
||||
|
@ -3159,12 +3156,15 @@ export class MangoClient {
|
|||
}
|
||||
}
|
||||
}
|
||||
const allPerpMarkets = perpPositionsMarketIndices
|
||||
.filter(
|
||||
(perpMarktIndex) =>
|
||||
perpMarktIndex !== PerpPosition.PerpMarketIndexUnset,
|
||||
)
|
||||
.map((perpIdx) => group.getPerpMarketByMarketIndex(perpIdx)!);
|
||||
const allPerpMarkets = uniq(
|
||||
perpPositionsMarketIndices
|
||||
.filter(
|
||||
(perpMarktIndex) =>
|
||||
perpMarktIndex !== PerpPosition.PerpMarketIndexUnset,
|
||||
)
|
||||
.map((perpIdx) => group.getPerpMarketByMarketIndex(perpIdx)!),
|
||||
(pm) => pm.perpMarketIndex,
|
||||
);
|
||||
healthRemainingAccounts.push(
|
||||
...allPerpMarkets.map((perp) => perp.publicKey),
|
||||
);
|
||||
|
|
|
@ -22,32 +22,34 @@ async function buildFetch(): Promise<
|
|||
}
|
||||
|
||||
export interface LiqorPriceImpact {
|
||||
Coin: string;
|
||||
'Oracle Price': number;
|
||||
'On-Chain Price': number;
|
||||
'Future Price': number;
|
||||
'V4 Liq Fee': number;
|
||||
Liabs: number;
|
||||
'Liabs slippage': number;
|
||||
Assets: number;
|
||||
'Assets Slippage': number;
|
||||
Coin: { val: string; highlight: boolean };
|
||||
'Oracle Price': { val: number; highlight: boolean };
|
||||
'Jup Price': { val: number; highlight: boolean };
|
||||
'Future Price': { val: number; highlight: boolean };
|
||||
'V4 Liq Fee': { val: number; highlight: boolean };
|
||||
Liabs: { val: number; highlight: boolean };
|
||||
'Liabs Slippage': { val: number; highlight: boolean };
|
||||
Assets: { val: number; highlight: boolean };
|
||||
'Assets Slippage': { val: number; highlight: boolean };
|
||||
}
|
||||
|
||||
export interface PerpPositionsToBeLiquidated {
|
||||
Market: string;
|
||||
Price: number;
|
||||
'Future Price': number;
|
||||
'Notional Position': number;
|
||||
Market: { val: string; highlight: boolean };
|
||||
Price: { val: number; highlight: boolean };
|
||||
'Future Price': { val: number; highlight: boolean };
|
||||
'Notional Position': { val: number; highlight: boolean };
|
||||
}
|
||||
|
||||
export interface AccountEquity {
|
||||
Account: PublicKey;
|
||||
Equity: number;
|
||||
Account: { val: PublicKey; highlight: boolean };
|
||||
Equity: { val: number; highlight: boolean };
|
||||
}
|
||||
|
||||
export interface Risk {
|
||||
assetRally: { title: string; data: LiqorPriceImpact[] };
|
||||
assetDrop: { title: string; data: LiqorPriceImpact[] };
|
||||
usdcDepeg: { title: string; data: LiqorPriceImpact[] };
|
||||
usdtDepeg: { title: string; data: LiqorPriceImpact[] };
|
||||
perpRally: { title: string; data: PerpPositionsToBeLiquidated[] };
|
||||
perpDrop: { title: string; data: PerpPositionsToBeLiquidated[] };
|
||||
marketMakerEquity: { title: string; data: AccountEquity[] };
|
||||
|
@ -63,18 +65,42 @@ export async function computePriceImpactOnJup(
|
|||
const response = await (await buildFetch())(url);
|
||||
|
||||
try {
|
||||
let res = await response.json();
|
||||
res = res.data[0];
|
||||
return {
|
||||
outAmount: parseFloat(res.outAmount),
|
||||
priceImpactPct: parseFloat(res.priceImpactPct),
|
||||
};
|
||||
const res = await response.json();
|
||||
if (res['data'] && res.data.length > 0 && res.data[0].outAmount) {
|
||||
return {
|
||||
outAmount: parseFloat(res.data[0].outAmount),
|
||||
priceImpactPct: parseFloat(res.data[0].priceImpactPct),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
outAmount: -1 / 10000,
|
||||
priceImpactPct: -1 / 10000,
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
throw e;
|
||||
return {
|
||||
outAmount: -1 / 10000,
|
||||
priceImpactPct: -1 / 10000,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function getOnChainPriceForMints(
|
||||
mints: string[],
|
||||
): Promise<number[]> {
|
||||
return await Promise.all(
|
||||
mints.map(async (mint) => {
|
||||
let data = await (
|
||||
await buildFetch()
|
||||
)(`https://price.jup.ag/v4/price?ids=${mint}`);
|
||||
data = await data.json();
|
||||
data = data['data'];
|
||||
return data[mint]['price'];
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export async function getPriceImpactForLiqor(
|
||||
group: Group,
|
||||
mangoAccounts: MangoAccount[],
|
||||
|
@ -95,7 +121,7 @@ export async function getPriceImpactForLiqor(
|
|||
|
||||
return await Promise.all(
|
||||
Array.from(group.banksMapByMint.values())
|
||||
.filter((banks) => banks[0].tokenIndex !== usdcBank.tokenIndex)
|
||||
.sort((a, b) => a[0].name.localeCompare(b[0].name))
|
||||
.map(async (banks) => {
|
||||
const bank = banks[0];
|
||||
|
||||
|
@ -107,11 +133,28 @@ export async function getPriceImpactForLiqor(
|
|||
mangoAccountsWithHealth.reduce((sum, a) => {
|
||||
// How much would health increase for every unit liab moved to liqor
|
||||
// liabprice * (liabweight - (1+fee)*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 tokenLiabHealthContrib = bank.price.mul(
|
||||
bank.initLiabWeight.sub(
|
||||
ONE_I80F48()
|
||||
.add(bank.liquidationFee)
|
||||
.mul(usdcBank.initAssetWeight),
|
||||
.mul(assetBank.initAssetWeight),
|
||||
),
|
||||
);
|
||||
// Abs liab/borrow
|
||||
|
@ -119,6 +162,11 @@ export async function getPriceImpactForLiqor(
|
|||
.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())
|
||||
|
@ -142,20 +190,38 @@ export async function getPriceImpactForLiqor(
|
|||
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) =>
|
||||
prev.initLiabWeight.lt(curr.initLiabWeight) ? 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!),
|
||||
),
|
||||
)
|
||||
? 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.eq(ZERO_I80F48())) {
|
||||
return sum.add(maxTokenHealthAsset);
|
||||
}
|
||||
|
||||
const maxAsset = a.health
|
||||
.min(ZERO_I80F48())
|
||||
.abs()
|
||||
|
@ -165,16 +231,7 @@ export async function getPriceImpactForLiqor(
|
|||
return sum.add(maxAsset);
|
||||
}, ZERO_I80F48());
|
||||
|
||||
let data;
|
||||
data = await (
|
||||
await buildFetch()
|
||||
)(`https://price.jup.ag/v4/price?ids=${bank.mint}`);
|
||||
data = await data.json();
|
||||
data = data['data'];
|
||||
|
||||
const [onChainPrice, pi1, pi2] = await Promise.all([
|
||||
data[bank.mint.toBase58()]['price'],
|
||||
|
||||
const [pi1, pi2] = await Promise.all([
|
||||
!liabsInUsdc.eq(ZERO_I80F48())
|
||||
? computePriceImpactOnJup(
|
||||
liabsInUsdc.toString(),
|
||||
|
@ -193,15 +250,50 @@ export async function getPriceImpactForLiqor(
|
|||
]);
|
||||
|
||||
return {
|
||||
Coin: bank.name,
|
||||
'Oracle Price': bank['oldUiPrice'],
|
||||
'On-Chain Price': onChainPrice,
|
||||
'Future Price': bank._uiPrice!,
|
||||
'V4 Liq Fee': bank.liquidationFee.toNumber() * 100,
|
||||
Liabs: toUiDecimalsForQuote(liabsInUsdc),
|
||||
'Liabs slippage': pi1.priceImpactPct * 100,
|
||||
Assets: toUiDecimals(assets, bank.mintDecimals) * bank.uiPrice,
|
||||
'Assets Slippage': pi2.priceImpactPct * 100,
|
||||
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.priceImpactPct * 10000),
|
||||
highlight:
|
||||
Math.round(pi1.priceImpactPct * 10000) >
|
||||
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.priceImpactPct * 10000),
|
||||
highlight:
|
||||
Math.round(pi2.priceImpactPct * 10000) >
|
||||
Math.round(bank.liquidationFee.toNumber() * 10000),
|
||||
},
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
@ -268,10 +360,13 @@ export async function getPerpPositionsToBeLiquidated(
|
|||
);
|
||||
|
||||
return {
|
||||
Market: pm.name,
|
||||
Price: pm['oldUiPrice'],
|
||||
'Future Price': pm._uiPrice,
|
||||
'Notional Position': notionalPositionUi,
|
||||
Market: { val: pm.name, highlight: false },
|
||||
Price: { val: pm['oldUiPrice'], highlight: false },
|
||||
'Future Price': { val: pm._uiPrice, highlight: false },
|
||||
'Notional Position': {
|
||||
val: Math.round(notionalPositionUi),
|
||||
highlight: Math.round(notionalPositionUi) > 5000,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@ -295,12 +390,17 @@ export async function getEquityForMangoAccounts(
|
|||
liqors.map((liqor) => client.getMangoAccount(liqor, true)),
|
||||
);
|
||||
|
||||
return liqorMangoAccounts.map((a: MangoAccount) => {
|
||||
const accountsWithEquity = liqorMangoAccounts.map((a: MangoAccount) => {
|
||||
return {
|
||||
Account: a.publicKey,
|
||||
Equity: toUiDecimalsForQuote(a.getEquity(group)),
|
||||
Account: { val: a.publicKey, highlight: false },
|
||||
Equity: {
|
||||
val: Math.round(toUiDecimalsForQuote(a.getEquity(group))),
|
||||
highlight: false,
|
||||
},
|
||||
};
|
||||
});
|
||||
accountsWithEquity.sort((a, b) => b.Equity.val - a.Equity.val);
|
||||
return accountsWithEquity;
|
||||
}
|
||||
|
||||
export async function getRiskStats(
|
||||
|
@ -334,12 +434,43 @@ export async function getRiskStats(
|
|||
|
||||
// Get all mango accounts
|
||||
const mangoAccounts = await client.getAllMangoAccounts(group, true);
|
||||
// const mangoAccounts = [
|
||||
// await client.getMangoAccount(
|
||||
// new PublicKey('5G9XriaoqQy1V4s9RmnbczWAozzbv6h2RuEeAHk4R6Lb'), // https://app.mango.markets/stats?token=SOL
|
||||
// true,
|
||||
// ),
|
||||
// ];
|
||||
|
||||
// Clone group, and simulate change % price drop for all assets
|
||||
// Get on chain prices
|
||||
const mints = [
|
||||
...new Set(
|
||||
Array.from(group.banksMapByTokenIndex.values())
|
||||
.flat()
|
||||
.map((bank) => bank.mint.toString()),
|
||||
),
|
||||
];
|
||||
const prices = await getOnChainPriceForMints([
|
||||
...new Set(
|
||||
Array.from(group.banksMapByTokenIndex.values())
|
||||
.flat()
|
||||
.map((bank) => bank.mint.toString()),
|
||||
),
|
||||
]);
|
||||
const onChainPrices = Object.fromEntries(
|
||||
prices.map((price, i) => [mints[i], price]),
|
||||
);
|
||||
Array.from(group.banksMapByTokenIndex.values())
|
||||
.flat()
|
||||
.forEach((b) => {
|
||||
b['onChainPrice'] = onChainPrices[b.mint.toBase58()];
|
||||
});
|
||||
|
||||
// Clone group, and simulate change % price drop for all assets except stables
|
||||
const drop = 1 - change;
|
||||
const groupDrop: Group = cloneDeep(group);
|
||||
Array.from(groupDrop.banksMapByTokenIndex.values())
|
||||
.flat()
|
||||
.filter((b) => !b.name.includes('USD'))
|
||||
.forEach((b) => {
|
||||
b['oldUiPrice'] = b._uiPrice;
|
||||
b._uiPrice = b._uiPrice! * drop;
|
||||
|
@ -351,11 +482,34 @@ export async function getRiskStats(
|
|||
p._price = p._price?.mul(I80F48.fromNumber(drop));
|
||||
});
|
||||
|
||||
// Clone group, and simulate change % price rally for all assets
|
||||
// Clone group, and simulate change % price drop for usdc
|
||||
const groupUsdcDepeg: Group = cloneDeep(group);
|
||||
Array.from(groupDrop.banksMapByTokenIndex.values())
|
||||
.flat()
|
||||
.filter((b) => b.name.includes('USDC'))
|
||||
.forEach((b) => {
|
||||
b['oldUiPrice'] = b._uiPrice;
|
||||
b._uiPrice = b._uiPrice! * drop;
|
||||
b._price = b._price?.mul(I80F48.fromNumber(drop));
|
||||
});
|
||||
|
||||
// Clone group, and simulate change % price drop for usdt
|
||||
const groupUsdtDepeg: Group = cloneDeep(group);
|
||||
Array.from(groupDrop.banksMapByTokenIndex.values())
|
||||
.flat()
|
||||
.filter((b) => b.name.includes('USDT'))
|
||||
.forEach((b) => {
|
||||
b['oldUiPrice'] = b._uiPrice;
|
||||
b._uiPrice = b._uiPrice! * drop;
|
||||
b._price = b._price?.mul(I80F48.fromNumber(drop));
|
||||
});
|
||||
|
||||
// Clone group, and simulate change % price rally for all assets except stables
|
||||
const rally = 1 + change;
|
||||
const groupRally: Group = cloneDeep(group);
|
||||
Array.from(groupRally.banksMapByTokenIndex.values())
|
||||
.flat()
|
||||
.filter((b) => !b.name.includes('USD'))
|
||||
.forEach((b) => {
|
||||
b['oldUiPrice'] = b._uiPrice;
|
||||
b._uiPrice = b._uiPrice! * rally;
|
||||
|
@ -370,13 +524,17 @@ export async function getRiskStats(
|
|||
const [
|
||||
assetDrop,
|
||||
assetRally,
|
||||
usdcDepeg,
|
||||
usdtDepeg,
|
||||
perpDrop,
|
||||
perpRally,
|
||||
liqorEquity,
|
||||
marketMakerEquity,
|
||||
] = await Promise.all([
|
||||
getPriceImpactForLiqor(groupDrop, mangoAccounts),
|
||||
getPriceImpactForLiqor(groupDrop, mangoAccounts),
|
||||
getPriceImpactForLiqor(groupRally, mangoAccounts),
|
||||
getPriceImpactForLiqor(groupUsdcDepeg, mangoAccounts),
|
||||
getPriceImpactForLiqor(groupUsdtDepeg, mangoAccounts),
|
||||
getPerpPositionsToBeLiquidated(groupDrop, mangoAccounts),
|
||||
getPerpPositionsToBeLiquidated(groupRally, mangoAccounts),
|
||||
getEquityForMangoAccounts(client, group, liqors),
|
||||
|
@ -387,21 +545,27 @@ export async function getRiskStats(
|
|||
assetDrop: {
|
||||
title: `Table 1a: Liqors acquire liabs and assets. The assets and liabs are sum of max assets and max
|
||||
liabs for any token which would be liquidated to fix the health of a mango account.
|
||||
This would be the slippage they would face on buying-liabs/offloading-assets tokens acquired from unhealth accounts after a 20% drop`,
|
||||
This would be the slippage they would face on buying-liabs/offloading-assets tokens acquired from unhealth accounts after a 40% drop to all non-stable oracles`,
|
||||
data: assetDrop,
|
||||
},
|
||||
assetRally: {
|
||||
title: `Table 1b: Liqors acquire liabs and assets. The assets and liabs are sum of max assets and max
|
||||
liabs for any token which would be liquidated to fix the health of a mango account.
|
||||
This would be the slippage they would face on buying-liabs/offloading-assets tokens acquired from unhealth accounts after a 20% rally`,
|
||||
title: `Table 1b: ... same as above but with a 40% rally to all non-stable oracles instead of drop`,
|
||||
data: assetRally,
|
||||
},
|
||||
usdcDepeg: {
|
||||
title: `Table 1c: ... same as above but with a 40% drop to only usdc oracle`,
|
||||
data: usdcDepeg,
|
||||
},
|
||||
usdtDepeg: {
|
||||
title: `Table 1d: ... same as above but with a 40% drop to only usdt oracle`,
|
||||
data: usdtDepeg,
|
||||
},
|
||||
perpDrop: {
|
||||
title: `Table 2a: Perp notional that liqor need to liquidate after a 20% drop`,
|
||||
title: `Table 2a: Perp notional that liqor need to liquidate after a 40% drop`,
|
||||
data: perpDrop,
|
||||
},
|
||||
perpRally: {
|
||||
title: `Table 2b: Perp notional that liqor need to liquidate after a 20% rally`,
|
||||
title: `Table 2b: Perp notional that liqor need to liquidate after a 40% rally`,
|
||||
data: perpRally,
|
||||
},
|
||||
liqorEquity: {
|
||||
|
|
Loading…
Reference in New Issue