2023-05-13 02:55:08 -07:00
|
|
|
import { PublicKey } from '@solana/web3.js';
|
|
|
|
import BN from 'bn.js';
|
|
|
|
import cloneDeep from 'lodash/cloneDeep';
|
|
|
|
import { TokenIndex } from './accounts/bank';
|
|
|
|
import { Group } from './accounts/group';
|
|
|
|
import { HealthType, MangoAccount } from './accounts/mangoAccount';
|
|
|
|
import { MangoClient } from './client';
|
|
|
|
import { I80F48, ONE_I80F48, ZERO_I80F48 } from './numbers/I80F48';
|
|
|
|
import { toUiDecimals, toUiDecimalsForQuote } from './utils';
|
|
|
|
|
|
|
|
async function buildFetch(): Promise<
|
|
|
|
(
|
|
|
|
input: RequestInfo | URL,
|
|
|
|
init?: RequestInit | undefined,
|
|
|
|
) => Promise<Response>
|
|
|
|
> {
|
|
|
|
let fetch = globalThis?.fetch;
|
|
|
|
if (!fetch && process?.versions?.node) {
|
|
|
|
fetch = (await import('node-fetch')).default;
|
|
|
|
}
|
|
|
|
return fetch;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface LiqorPriceImpact {
|
2023-05-16 02:22:50 -07:00
|
|
|
Coin: { val: string; highlight: boolean };
|
|
|
|
'Oracle Price': { val: number; highlight: boolean };
|
2023-05-16 02:46:42 -07:00
|
|
|
'Jup Price': { val: number; highlight: boolean };
|
2023-05-16 02:22:50 -07:00
|
|
|
'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 };
|
2023-05-13 02:55:08 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
export interface PerpPositionsToBeLiquidated {
|
2023-05-16 02:22:50 -07:00
|
|
|
Market: { val: string; highlight: boolean };
|
|
|
|
Price: { val: number; highlight: boolean };
|
|
|
|
'Future Price': { val: number; highlight: boolean };
|
|
|
|
'Notional Position': { val: number; highlight: boolean };
|
2023-05-13 02:55:08 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
export interface AccountEquity {
|
2023-05-16 02:22:50 -07:00
|
|
|
Account: { val: PublicKey; highlight: boolean };
|
|
|
|
Equity: { val: number; highlight: boolean };
|
2023-05-13 02:55:08 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
export interface Risk {
|
|
|
|
assetRally: { title: string; data: LiqorPriceImpact[] };
|
|
|
|
assetDrop: { title: string; data: LiqorPriceImpact[] };
|
2023-05-16 01:45:38 -07:00
|
|
|
usdcDepeg: { title: string; data: LiqorPriceImpact[] };
|
|
|
|
usdtDepeg: { title: string; data: LiqorPriceImpact[] };
|
2023-05-13 02:55:08 -07:00
|
|
|
perpRally: { title: string; data: PerpPositionsToBeLiquidated[] };
|
|
|
|
perpDrop: { title: string; data: PerpPositionsToBeLiquidated[] };
|
|
|
|
marketMakerEquity: { title: string; data: AccountEquity[] };
|
|
|
|
liqorEquity: { title: string; data: AccountEquity[] };
|
|
|
|
}
|
|
|
|
|
2023-07-12 05:30:47 -07:00
|
|
|
type PriceImpact = {
|
|
|
|
symbol: string;
|
|
|
|
side: 'bid' | 'ask';
|
|
|
|
target_amount: number;
|
|
|
|
avg_price_impact_percent: number;
|
|
|
|
min_price_impact_percent: number;
|
|
|
|
max_price_impact_percent: number;
|
|
|
|
};
|
2023-05-13 02:55:08 -07:00
|
|
|
|
2023-07-12 05:30:47 -07:00
|
|
|
export async function computePriceImpactOnJup(
|
|
|
|
pis: PriceImpact[],
|
|
|
|
usdcAmount: number,
|
|
|
|
tokenName: string,
|
|
|
|
): Promise<number> {
|
|
|
|
console.log(pis);
|
2023-05-13 02:55:08 -07:00
|
|
|
try {
|
2023-07-12 05:30:47 -07:00
|
|
|
const closestTo = [1000, 5000, 20000, 100000].reduce((prev, curr) =>
|
|
|
|
Math.abs(curr - usdcAmount) < Math.abs(prev - usdcAmount) ? curr : prev,
|
|
|
|
);
|
|
|
|
// Workaround api
|
|
|
|
if (tokenName == 'ETH (Portal)') {
|
|
|
|
tokenName = 'ETH';
|
|
|
|
}
|
|
|
|
const filteredPis: PriceImpact[] = pis.filter(
|
|
|
|
(pi) => pi.symbol == tokenName && pi.target_amount == closestTo,
|
|
|
|
);
|
|
|
|
if (filteredPis.length > 0) {
|
|
|
|
return (filteredPis[0].max_price_impact_percent * 10000) / 100;
|
2023-05-16 01:05:10 -07:00
|
|
|
} else {
|
2023-07-12 05:30:47 -07:00
|
|
|
return -1;
|
2023-05-16 00:52:21 -07:00
|
|
|
}
|
2023-05-13 02:55:08 -07:00
|
|
|
} catch (e) {
|
2023-07-12 05:30:47 -07:00
|
|
|
return -1;
|
2023-05-13 02:55:08 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-16 02:04:26 -07:00
|
|
|
export async function getOnChainPriceForMints(
|
|
|
|
mints: string[],
|
|
|
|
): Promise<number[]> {
|
|
|
|
return await Promise.all(
|
|
|
|
mints.map(async (mint) => {
|
2023-06-07 17:38:38 -07:00
|
|
|
const resp = await (
|
2023-05-16 02:04:26 -07:00
|
|
|
await buildFetch()
|
2023-06-07 17:38:38 -07:00
|
|
|
)(`https://public-api.birdeye.so/public/price?address=${mint}`, {
|
|
|
|
headers: {
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
const data = await resp.json();
|
|
|
|
return data?.data?.value;
|
2023-05-16 02:04:26 -07:00
|
|
|
}),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-05-13 02:55:08 -07:00
|
|
|
export async function getPriceImpactForLiqor(
|
|
|
|
group: Group,
|
2023-07-12 05:30:47 -07:00
|
|
|
pis: PriceImpact[],
|
2023-05-13 02:55:08 -07:00
|
|
|
mangoAccounts: MangoAccount[],
|
|
|
|
): Promise<LiqorPriceImpact[]> {
|
|
|
|
const mangoAccountsWithHealth = mangoAccounts.map((a: MangoAccount) => {
|
|
|
|
return {
|
|
|
|
account: a,
|
|
|
|
health: a.getHealth(group, HealthType.liquidationEnd),
|
|
|
|
healthRatio: a.getHealthRatioUi(group, HealthType.liquidationEnd),
|
|
|
|
};
|
|
|
|
});
|
|
|
|
|
|
|
|
const usdcBank = group.getFirstBankByTokenIndex(0 as TokenIndex);
|
|
|
|
const usdcMint = usdcBank.mint;
|
|
|
|
|
|
|
|
return await Promise.all(
|
|
|
|
Array.from(group.banksMapByMint.values())
|
2023-05-15 02:52:35 -07:00
|
|
|
.sort((a, b) => a[0].name.localeCompare(b[0].name))
|
2023-05-13 02:55:08 -07:00
|
|
|
.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+fee)*assetweight)
|
2023-05-16 06:37:50 -07:00
|
|
|
// 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,
|
|
|
|
);
|
2023-05-13 02:55:08 -07:00
|
|
|
const tokenLiabHealthContrib = bank.price.mul(
|
|
|
|
bank.initLiabWeight.sub(
|
|
|
|
ONE_I80F48()
|
|
|
|
.add(bank.liquidationFee)
|
2023-05-16 06:37:50 -07:00
|
|
|
.mul(assetBank.initAssetWeight),
|
2023-05-13 02:55:08 -07:00
|
|
|
),
|
|
|
|
);
|
|
|
|
// Abs liab/borrow
|
|
|
|
const maxTokenLiab = a.account
|
|
|
|
.getEffectiveTokenBalance(group, bank)
|
|
|
|
.min(ZERO_I80F48())
|
|
|
|
.abs();
|
2023-05-16 01:09:12 -07:00
|
|
|
|
|
|
|
if (tokenLiabHealthContrib.eq(ZERO_I80F48())) {
|
|
|
|
return sum.add(maxTokenLiab);
|
|
|
|
}
|
|
|
|
|
2023-05-13 02:55:08 -07:00
|
|
|
// 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) => {
|
|
|
|
// How much would health increase for every unit liab moved to liqor
|
|
|
|
// assetprice * (liabweight/(1+liabliqfee) - assetweight)
|
2023-05-16 06:37:50 -07:00
|
|
|
// Choose the smallest liability the user has
|
2023-05-13 02:55:08 -07:00
|
|
|
const liabBank = Array.from(group.banksMapByTokenIndex.values())
|
|
|
|
.flat()
|
|
|
|
.reduce((prev, curr) =>
|
2023-05-16 06:37:50 -07:00
|
|
|
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,
|
2023-05-13 02:55:08 -07:00
|
|
|
);
|
|
|
|
const tokenAssetHealthContrib = bank.price.mul(
|
|
|
|
liabBank.initLiabWeight
|
|
|
|
.div(ONE_I80F48().add(liabBank.liquidationFee))
|
|
|
|
.sub(bank.initAssetWeight),
|
|
|
|
);
|
2023-05-16 01:09:12 -07:00
|
|
|
|
2023-05-13 02:55:08 -07:00
|
|
|
// Abs collateral/asset
|
|
|
|
const maxTokenHealthAsset = a.account
|
|
|
|
.getEffectiveTokenBalance(group, bank)
|
|
|
|
.max(ZERO_I80F48());
|
2023-05-16 01:09:12 -07:00
|
|
|
|
|
|
|
if (tokenAssetHealthContrib.eq(ZERO_I80F48())) {
|
|
|
|
return sum.add(maxTokenHealthAsset);
|
|
|
|
}
|
|
|
|
|
2023-05-13 02:55:08 -07:00
|
|
|
const maxAsset = a.health
|
|
|
|
.min(ZERO_I80F48())
|
|
|
|
.abs()
|
|
|
|
.div(tokenAssetHealthContrib)
|
|
|
|
.min(maxTokenHealthAsset);
|
|
|
|
|
|
|
|
return sum.add(maxAsset);
|
|
|
|
}, ZERO_I80F48());
|
|
|
|
|
2023-05-16 02:04:26 -07:00
|
|
|
const [pi1, pi2] = await Promise.all([
|
2023-06-07 17:38:38 -07:00
|
|
|
!liabsInUsdc.eq(ZERO_I80F48()) &&
|
|
|
|
usdcMint.toBase58() !== bank.mint.toBase58()
|
2023-05-13 02:55:08 -07:00
|
|
|
? computePriceImpactOnJup(
|
2023-07-12 05:30:47 -07:00
|
|
|
pis,
|
|
|
|
toUiDecimalsForQuote(liabsInUsdc),
|
|
|
|
bank.name,
|
2023-05-13 02:55:08 -07:00
|
|
|
)
|
2023-07-12 05:30:47 -07:00
|
|
|
: Promise.resolve(0),
|
2023-05-13 02:55:08 -07:00
|
|
|
|
2023-06-07 17:38:38 -07:00
|
|
|
!assets.eq(ZERO_I80F48()) &&
|
|
|
|
usdcMint.toBase58() !== bank.mint.toBase58()
|
2023-05-13 02:55:08 -07:00
|
|
|
? computePriceImpactOnJup(
|
2023-07-12 05:30:47 -07:00
|
|
|
pis,
|
|
|
|
toUiDecimals(assets.mul(bank.price), bank.mintDecimals),
|
|
|
|
bank.name,
|
2023-05-13 02:55:08 -07:00
|
|
|
)
|
2023-07-12 05:30:47 -07:00
|
|
|
: Promise.resolve(0),
|
2023-05-13 02:55:08 -07:00
|
|
|
]);
|
|
|
|
|
|
|
|
return {
|
2023-05-16 02:22:50 -07:00
|
|
|
Coin: { val: bank.name, highlight: false },
|
2023-05-16 01:45:38 -07:00
|
|
|
'Oracle Price': {
|
|
|
|
val: bank['oldUiPrice'] ? bank['oldUiPrice'] : bank._uiPrice!,
|
2023-05-16 02:22:50 -07:00
|
|
|
highlight: false,
|
2023-05-16 01:45:38 -07:00
|
|
|
},
|
2023-05-16 02:46:42 -07:00
|
|
|
'Jup Price': {
|
2023-05-16 02:22:50 -07:00
|
|
|
val: bank['onChainPrice'],
|
|
|
|
highlight:
|
|
|
|
Math.abs(
|
2023-05-16 02:46:42 -07:00
|
|
|
(bank['onChainPrice'] -
|
|
|
|
(bank['oldUiPrice'] ? bank['oldUiPrice'] : bank._uiPrice!)) /
|
|
|
|
(bank['oldUiPrice'] ? bank['oldUiPrice'] : bank._uiPrice!),
|
2023-05-16 02:22:50 -07:00
|
|
|
) > 0.05,
|
|
|
|
},
|
|
|
|
'Future Price': { val: bank._uiPrice!, highlight: false },
|
2023-05-16 01:17:00 -07:00
|
|
|
'V4 Liq Fee': {
|
|
|
|
val: Math.round(bank.liquidationFee.toNumber() * 10000),
|
2023-05-16 02:22:50 -07:00
|
|
|
highlight: false,
|
|
|
|
},
|
|
|
|
Liabs: {
|
|
|
|
val: Math.round(toUiDecimalsForQuote(liabsInUsdc)),
|
|
|
|
highlight: Math.round(toUiDecimalsForQuote(liabsInUsdc)) > 5000,
|
|
|
|
},
|
|
|
|
'Liabs Slippage': {
|
2023-07-12 05:30:47 -07:00
|
|
|
val: Math.round(pi1),
|
2023-05-16 02:22:50 -07:00
|
|
|
highlight:
|
2023-07-12 05:30:47 -07:00
|
|
|
Math.round(pi1) >
|
2023-05-16 02:22:50 -07:00
|
|
|
Math.round(bank.liquidationFee.toNumber() * 10000),
|
2023-05-16 01:17:00 -07:00
|
|
|
},
|
|
|
|
Assets: {
|
|
|
|
val: Math.round(
|
|
|
|
toUiDecimals(assets, bank.mintDecimals) * bank.uiPrice,
|
|
|
|
),
|
2023-05-16 02:46:42 -07:00
|
|
|
highlight:
|
|
|
|
Math.round(
|
|
|
|
toUiDecimals(assets, bank.mintDecimals) * bank.uiPrice,
|
|
|
|
) > 5000,
|
2023-05-16 02:22:50 -07:00
|
|
|
},
|
|
|
|
'Assets Slippage': {
|
2023-07-12 05:30:47 -07:00
|
|
|
val: Math.round(pi2),
|
2023-05-16 02:22:50 -07:00
|
|
|
highlight:
|
2023-07-12 05:30:47 -07:00
|
|
|
Math.round(pi2) >
|
2023-05-16 02:22:50 -07:00
|
|
|
Math.round(bank.liquidationFee.toNumber() * 10000),
|
2023-05-16 01:17:00 -07:00
|
|
|
},
|
2023-05-13 02:55:08 -07:00
|
|
|
};
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
export async 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 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)),
|
|
|
|
);
|
|
|
|
|
|
|
|
return {
|
2023-05-16 02:22:50 -07:00
|
|
|
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,
|
|
|
|
},
|
2023-05-13 02:55:08 -07:00
|
|
|
};
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function getEquityForMangoAccounts(
|
|
|
|
client: MangoClient,
|
|
|
|
group: Group,
|
2023-07-06 09:45:42 -07:00
|
|
|
mangoAccountPks: PublicKey[],
|
|
|
|
allMangoAccounts: MangoAccount[],
|
2023-05-13 02:55:08 -07:00
|
|
|
): Promise<AccountEquity[]> {
|
2023-07-12 05:30:47 -07:00
|
|
|
const mangoAccounts = allMangoAccounts.filter((a) =>
|
2023-07-06 09:45:42 -07:00
|
|
|
mangoAccountPks.find((pk) => pk.equals(a.publicKey)),
|
2023-05-13 02:55:08 -07:00
|
|
|
);
|
|
|
|
|
2023-07-12 05:30:47 -07:00
|
|
|
const accountsWithEquity = mangoAccounts.map((a: MangoAccount) => {
|
2023-05-13 02:55:08 -07:00
|
|
|
return {
|
2023-05-16 02:22:50 -07:00
|
|
|
Account: { val: a.publicKey, highlight: false },
|
|
|
|
Equity: {
|
|
|
|
val: Math.round(toUiDecimalsForQuote(a.getEquity(group))),
|
|
|
|
highlight: false,
|
|
|
|
},
|
2023-05-13 02:55:08 -07:00
|
|
|
};
|
|
|
|
});
|
2023-05-16 01:17:00 -07:00
|
|
|
accountsWithEquity.sort((a, b) => b.Equity.val - a.Equity.val);
|
2023-05-15 02:52:35 -07:00
|
|
|
return accountsWithEquity;
|
2023-05-13 02:55:08 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
export async function getRiskStats(
|
|
|
|
client: MangoClient,
|
|
|
|
group: Group,
|
|
|
|
change = 0.4, // simulates 40% price rally and price drop on tokens and markets
|
|
|
|
): Promise<Risk> {
|
2023-07-12 05:30:47 -07:00
|
|
|
let pis;
|
|
|
|
try {
|
|
|
|
pis = await (
|
|
|
|
await (
|
|
|
|
await buildFetch()
|
|
|
|
)(
|
|
|
|
`https://api.mngo.cloud/data/v4/risk/listed-tokens-one-week-price-impacts`,
|
2023-07-12 06:28:20 -07:00
|
|
|
{
|
2023-07-12 06:45:42 -07:00
|
|
|
mode: 'cors',
|
2023-07-12 06:28:20 -07:00
|
|
|
headers: {
|
|
|
|
'Content-Type': 'application/json',
|
2023-07-12 06:45:42 -07:00
|
|
|
'Access-Control-Allow-Origin': '*',
|
2023-07-12 06:28:20 -07:00
|
|
|
},
|
|
|
|
},
|
2023-07-12 05:30:47 -07:00
|
|
|
)
|
|
|
|
).json();
|
|
|
|
} catch (error) {
|
|
|
|
pis = [];
|
|
|
|
}
|
|
|
|
|
2023-05-13 02:55:08 -07:00
|
|
|
// Get known liqors
|
|
|
|
let liqors: PublicKey[];
|
|
|
|
try {
|
|
|
|
liqors = (
|
|
|
|
await (
|
|
|
|
await (
|
|
|
|
await buildFetch()
|
|
|
|
)(
|
|
|
|
`https://api.mngo.cloud/data/v4/stats/liqors-over_period?over_period=1MONTH`,
|
2023-07-12 06:28:20 -07:00
|
|
|
{
|
2023-07-12 06:45:42 -07:00
|
|
|
mode: 'cors',
|
2023-07-12 06:28:20 -07:00
|
|
|
headers: {
|
|
|
|
'Content-Type': 'application/json',
|
2023-07-12 06:45:42 -07:00
|
|
|
'Access-Control-Allow-Origin': '*',
|
2023-07-12 06:28:20 -07:00
|
|
|
},
|
|
|
|
},
|
2023-05-13 02:55:08 -07:00
|
|
|
)
|
|
|
|
).json()
|
|
|
|
).map((data) => new PublicKey(data['liqor']));
|
|
|
|
} catch (error) {
|
|
|
|
liqors = [];
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get known mms
|
|
|
|
const mms = [
|
2023-06-18 01:13:10 -07:00
|
|
|
new PublicKey('BLgb4NFwhpurMrGX5LQfb8D8dBpGSGtBqqew2Em8uyRT'),
|
|
|
|
new PublicKey('DA5G4mUdFmQXyKuFqvGGZGBzPAPYDYQsdjmiEJAYzUXQ'),
|
2023-05-13 02:55:08 -07:00
|
|
|
new PublicKey('BGYWnqfaauCeebFQXEfYuDCktiVG8pqpprrsD4qfqL53'),
|
2023-06-18 01:13:10 -07:00
|
|
|
new PublicKey('F1SZxEDxxCSLVjEBbMEjDYqajWRJQRCZBwPQnmcVvTLV'),
|
2023-05-13 02:55:08 -07:00
|
|
|
];
|
|
|
|
|
|
|
|
// Get all mango accounts
|
|
|
|
const mangoAccounts = await client.getAllMangoAccounts(group, true);
|
|
|
|
|
2023-05-16 02:04:26 -07:00
|
|
|
// 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()];
|
|
|
|
});
|
|
|
|
|
2023-05-16 01:45:38 -07:00
|
|
|
// Clone group, and simulate change % price drop for all assets except stables
|
2023-05-13 02:55:08 -07:00
|
|
|
const drop = 1 - change;
|
|
|
|
const groupDrop: Group = cloneDeep(group);
|
|
|
|
Array.from(groupDrop.banksMapByTokenIndex.values())
|
|
|
|
.flat()
|
2023-05-16 01:05:10 -07:00
|
|
|
.filter((b) => !b.name.includes('USD'))
|
2023-05-13 02:55:08 -07:00
|
|
|
.forEach((b) => {
|
|
|
|
b['oldUiPrice'] = b._uiPrice;
|
|
|
|
b._uiPrice = b._uiPrice! * drop;
|
|
|
|
b._price = b._price?.mul(I80F48.fromNumber(drop));
|
|
|
|
});
|
|
|
|
Array.from(groupDrop.perpMarketsMapByMarketIndex.values()).forEach((p) => {
|
|
|
|
p['oldUiPrice'] = p._uiPrice;
|
|
|
|
p._uiPrice = p._uiPrice! * drop;
|
|
|
|
p._price = p._price?.mul(I80F48.fromNumber(drop));
|
|
|
|
});
|
|
|
|
|
2023-05-16 01:45:38 -07:00
|
|
|
// Clone group, and simulate change % price drop for usdc
|
|
|
|
const groupUsdcDepeg: Group = cloneDeep(group);
|
2023-06-20 23:45:27 -07:00
|
|
|
Array.from(groupUsdcDepeg.banksMapByTokenIndex.values())
|
2023-05-16 01:45:38 -07:00
|
|
|
.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);
|
2023-06-20 23:45:27 -07:00
|
|
|
Array.from(groupUsdtDepeg.banksMapByTokenIndex.values())
|
2023-05-16 01:45:38 -07:00
|
|
|
.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
|
2023-05-13 02:55:08 -07:00
|
|
|
const rally = 1 + change;
|
|
|
|
const groupRally: Group = cloneDeep(group);
|
|
|
|
Array.from(groupRally.banksMapByTokenIndex.values())
|
|
|
|
.flat()
|
2023-05-16 01:05:10 -07:00
|
|
|
.filter((b) => !b.name.includes('USD'))
|
2023-05-13 02:55:08 -07:00
|
|
|
.forEach((b) => {
|
|
|
|
b['oldUiPrice'] = b._uiPrice;
|
|
|
|
b._uiPrice = b._uiPrice! * rally;
|
|
|
|
b._price = b._price?.mul(I80F48.fromNumber(rally));
|
|
|
|
});
|
|
|
|
Array.from(groupRally.perpMarketsMapByMarketIndex.values()).forEach((p) => {
|
|
|
|
p['oldUiPrice'] = p._uiPrice;
|
|
|
|
p._uiPrice = p._uiPrice! * rally;
|
|
|
|
p._price = p._price?.mul(I80F48.fromNumber(rally));
|
|
|
|
});
|
|
|
|
|
|
|
|
const [
|
|
|
|
assetDrop,
|
|
|
|
assetRally,
|
2023-05-16 01:45:38 -07:00
|
|
|
usdcDepeg,
|
|
|
|
usdtDepeg,
|
2023-05-13 02:55:08 -07:00
|
|
|
perpDrop,
|
|
|
|
perpRally,
|
|
|
|
liqorEquity,
|
|
|
|
marketMakerEquity,
|
|
|
|
] = await Promise.all([
|
2023-07-12 05:30:47 -07:00
|
|
|
getPriceImpactForLiqor(groupDrop, pis, mangoAccounts),
|
|
|
|
getPriceImpactForLiqor(groupRally, pis, mangoAccounts),
|
|
|
|
getPriceImpactForLiqor(groupUsdcDepeg, pis, mangoAccounts),
|
|
|
|
getPriceImpactForLiqor(groupUsdtDepeg, pis, mangoAccounts),
|
2023-05-13 02:55:08 -07:00
|
|
|
getPerpPositionsToBeLiquidated(groupDrop, mangoAccounts),
|
|
|
|
getPerpPositionsToBeLiquidated(groupRally, mangoAccounts),
|
2023-07-06 09:45:42 -07:00
|
|
|
getEquityForMangoAccounts(client, group, liqors, mangoAccounts),
|
|
|
|
getEquityForMangoAccounts(client, group, mms, mangoAccounts),
|
2023-05-13 02:55:08 -07:00
|
|
|
]);
|
|
|
|
|
|
|
|
return {
|
|
|
|
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.
|
2023-05-16 01:17:00 -07:00
|
|
|
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`,
|
2023-05-13 02:55:08 -07:00
|
|
|
data: assetDrop,
|
|
|
|
},
|
|
|
|
assetRally: {
|
2023-05-16 02:04:26 -07:00
|
|
|
title: `Table 1b: ... same as above but with a 40% rally to all non-stable oracles instead of drop`,
|
2023-05-13 02:55:08 -07:00
|
|
|
data: assetRally,
|
|
|
|
},
|
2023-05-16 01:45:38 -07:00
|
|
|
usdcDepeg: {
|
2023-05-16 02:04:26 -07:00
|
|
|
title: `Table 1c: ... same as above but with a 40% drop to only usdc oracle`,
|
2023-05-16 01:45:38 -07:00
|
|
|
data: usdcDepeg,
|
|
|
|
},
|
|
|
|
usdtDepeg: {
|
2023-05-16 02:04:26 -07:00
|
|
|
title: `Table 1d: ... same as above but with a 40% drop to only usdt oracle`,
|
2023-05-16 01:45:38 -07:00
|
|
|
data: usdtDepeg,
|
|
|
|
},
|
2023-05-13 02:55:08 -07:00
|
|
|
perpDrop: {
|
2023-05-15 02:52:35 -07:00
|
|
|
title: `Table 2a: Perp notional that liqor need to liquidate after a 40% drop`,
|
2023-05-13 02:55:08 -07:00
|
|
|
data: perpDrop,
|
|
|
|
},
|
|
|
|
perpRally: {
|
2023-05-15 02:52:35 -07:00
|
|
|
title: `Table 2b: Perp notional that liqor need to liquidate after a 40% rally`,
|
2023-05-13 02:55:08 -07:00
|
|
|
data: perpRally,
|
|
|
|
},
|
|
|
|
liqorEquity: {
|
|
|
|
title: `Table 3: Equity of known liqors from last month`,
|
|
|
|
data: liqorEquity,
|
|
|
|
},
|
|
|
|
marketMakerEquity: {
|
|
|
|
title: `Table 4: Equity of known makers from last month`,
|
|
|
|
data: marketMakerEquity,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|