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>
This commit is contained in:
parent
784ef88927
commit
08dfb0ddba
|
@ -39,9 +39,13 @@
|
|||
"@types/node": "^18.11.18",
|
||||
"@typescript-eslint/eslint-plugin": "^5.32.0",
|
||||
"@typescript-eslint/parser": "^5.32.0",
|
||||
"axios": "^1.4.0",
|
||||
"chai": "^4.3.4",
|
||||
"cli-table3": "^0.6.3",
|
||||
"console-table-printer": "^2.11.1",
|
||||
"eslint": "^7.28.0",
|
||||
"eslint-config-prettier": "^7.2.0",
|
||||
"fast-csv": "^4.3.6",
|
||||
"mocha": "^9.1.3",
|
||||
"prettier": "^2.0.5",
|
||||
"ts-mocha": "^10.0.0",
|
||||
|
|
|
@ -254,16 +254,24 @@ async function main(): Promise<void> {
|
|||
const userWallet = new Wallet(user);
|
||||
console.log(`User ${userWallet.publicKey.toBase58()}`);
|
||||
|
||||
const mangoAccounts = await client.getAllMangoAccounts(group);
|
||||
const mangoAccounts = await client.getAllMangoAccounts(group, true);
|
||||
|
||||
for (const mangoAccount of mangoAccounts) {
|
||||
if (
|
||||
!MANGO_ACCOUNT_PK ||
|
||||
mangoAccount.publicKey.equals(new PublicKey(MANGO_ACCOUNT_PK))
|
||||
) {
|
||||
console.log();
|
||||
console.log(`MangoAccount ${mangoAccount.publicKey}`);
|
||||
await debugUser(client, group, mangoAccount);
|
||||
// console.log();
|
||||
console.log(
|
||||
`${mangoAccount.publicKey
|
||||
.toBase58()
|
||||
.padStart(48)}, health ${toUiDecimalsForQuote(
|
||||
mangoAccount.getHealth(group, HealthType.maint),
|
||||
).toFixed(2)}, ${toUiDecimalsForQuote(
|
||||
mangoAccount.getHealth(group, HealthType.init),
|
||||
).toFixed(2)}`,
|
||||
);
|
||||
// await debugUser(client, group, mangoAccount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,16 +4,7 @@ import { expect } from 'chai';
|
|||
import fs from 'fs';
|
||||
import { Group } from '../../src/accounts/group';
|
||||
import { HealthType } from '../../src/accounts/mangoAccount';
|
||||
import {
|
||||
PerpMarketIndex,
|
||||
PerpOrderSide,
|
||||
PerpOrderType,
|
||||
} from '../../src/accounts/perp';
|
||||
import {
|
||||
Serum3OrderType,
|
||||
Serum3SelfTradeBehavior,
|
||||
Serum3Side,
|
||||
} from '../../src/accounts/serum3';
|
||||
import { PerpOrderSide, PerpOrderType } from '../../src/accounts/perp';
|
||||
import { MangoClient } from '../../src/client';
|
||||
import { MANGO_V4_ID } from '../../src/constants';
|
||||
import { toUiDecimalsForQuote } from '../../src/utils';
|
||||
|
@ -130,6 +121,7 @@ async function main(): Promise<void> {
|
|||
|
||||
// deposit USDC
|
||||
let oldBalance = mangoAccount.getTokenBalance(
|
||||
group,
|
||||
group.getFirstBankByMint(new PublicKey(DEVNET_MINTS.get('USDC')!)),
|
||||
);
|
||||
await client.tokenDeposit(
|
||||
|
@ -140,6 +132,7 @@ async function main(): Promise<void> {
|
|||
);
|
||||
await mangoAccount.reload(client);
|
||||
let newBalance = mangoAccount.getTokenBalance(
|
||||
group,
|
||||
group.getFirstBankByMint(new PublicKey(DEVNET_MINTS.get('USDC')!)),
|
||||
);
|
||||
expect(toUiDecimalsForQuote(newBalance.sub(oldBalance)).toString()).equals(
|
||||
|
@ -167,6 +160,7 @@ async function main(): Promise<void> {
|
|||
// withdraw USDC
|
||||
console.log(`...withdrawing 1 USDC`);
|
||||
oldBalance = mangoAccount.getTokenBalance(
|
||||
group,
|
||||
group.getFirstBankByMint(new PublicKey(DEVNET_MINTS.get('USDC')!)),
|
||||
);
|
||||
await client.tokenWithdraw(
|
||||
|
@ -178,6 +172,7 @@ async function main(): Promise<void> {
|
|||
);
|
||||
await mangoAccount.reload(client);
|
||||
newBalance = mangoAccount.getTokenBalance(
|
||||
group,
|
||||
group.getFirstBankByMint(new PublicKey(DEVNET_MINTS.get('USDC')!)),
|
||||
);
|
||||
expect(toUiDecimalsForQuote(oldBalance.sub(newBalance)).toString()).equals(
|
||||
|
|
|
@ -1,74 +0,0 @@
|
|||
import { AnchorProvider, Wallet } from '@coral-xyz/anchor';
|
||||
import { Cluster, Connection, Keypair, PublicKey } from '@solana/web3.js';
|
||||
import fs from 'fs';
|
||||
import { HealthType, MangoAccount } from '../src/accounts/mangoAccount';
|
||||
import { MangoClient } from '../src/client';
|
||||
import { MANGO_V4_ID } from '../src/constants';
|
||||
import { toUiDecimalsForQuote } from '../src/utils';
|
||||
|
||||
const CLUSTER_URL =
|
||||
process.env.CLUSTER_URL_OVERRIDE || process.env.MB_CLUSTER_URL;
|
||||
const SOME_KEYPAIR =
|
||||
process.env.PAYER_KEYPAIR_OVERRIDE || process.env.MB_PAYER_KEYPAIR;
|
||||
const CLUSTER: Cluster =
|
||||
(process.env.CLUSTER_OVERRIDE as Cluster) || 'mainnet-beta';
|
||||
|
||||
const GROUP_PK = '78b8f4cGCwmZ9ysPFMWLaLTkkaYnUjwMJYStWe5RTSSX';
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const options = AnchorProvider.defaultOptions();
|
||||
const connection = new Connection(CLUSTER_URL!, options);
|
||||
|
||||
const someKeypair = Keypair.fromSecretKey(
|
||||
Buffer.from(JSON.parse(fs.readFileSync(SOME_KEYPAIR!, 'utf-8'))),
|
||||
);
|
||||
|
||||
const someWallet = new Wallet(someKeypair);
|
||||
const someProvider = new AnchorProvider(connection, someWallet, options);
|
||||
const client = MangoClient.connect(
|
||||
someProvider,
|
||||
CLUSTER,
|
||||
MANGO_V4_ID[CLUSTER],
|
||||
{
|
||||
idsSource: 'api',
|
||||
},
|
||||
);
|
||||
|
||||
const group = await client.getGroup(new PublicKey(GROUP_PK));
|
||||
const mangoAccountsWithHealth = (
|
||||
await client.getAllMangoAccounts(group, true)
|
||||
)
|
||||
.map((a: MangoAccount) => {
|
||||
return {
|
||||
account: a,
|
||||
healthRatio: a.getHealthRatioUi(group, HealthType.maint),
|
||||
equity: toUiDecimalsForQuote(a.getEquity(group)),
|
||||
};
|
||||
})
|
||||
.filter((a) => a.equity > 1000)
|
||||
.filter((a) => a.healthRatio < 50)
|
||||
.sort((a, b) => a.healthRatio - b.healthRatio);
|
||||
|
||||
console.log(
|
||||
`${'Owner'.padStart(45)}, ${'Account'.padStart(
|
||||
45,
|
||||
)}, ${'Health Ratio'.padStart(10)}, ${'Equity'.padStart(10)}`,
|
||||
);
|
||||
for (const obj of mangoAccountsWithHealth) {
|
||||
console.log(
|
||||
`${obj.account.owner.toBase58().padStart(45)} ${obj.account.publicKey
|
||||
.toBase58()
|
||||
.padStart(45)}: ${obj.healthRatio
|
||||
.toFixed(2)
|
||||
.padStart(8)} %, ${obj.equity.toLocaleString().padStart(10)} $`,
|
||||
);
|
||||
}
|
||||
|
||||
process.exit();
|
||||
}
|
||||
|
||||
try {
|
||||
main();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
|
@ -1,95 +0,0 @@
|
|||
import { AnchorProvider, Wallet } from '@coral-xyz/anchor';
|
||||
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
|
||||
import fetch from 'node-fetch';
|
||||
import { MangoClient } from '../src/client';
|
||||
import { MANGO_V4_ID } from '../src/constants';
|
||||
import { toNative, toUiDecimalsForQuote } from '../src/utils';
|
||||
|
||||
const { MB_CLUSTER_URL } = process.env;
|
||||
|
||||
const GROUP_PK = '78b8f4cGCwmZ9ysPFMWLaLTkkaYnUjwMJYStWe5RTSSX';
|
||||
|
||||
async function buildClient(): Promise<MangoClient> {
|
||||
const clientKeypair = new Keypair();
|
||||
|
||||
const options = AnchorProvider.defaultOptions();
|
||||
const connection = new Connection(MB_CLUSTER_URL!, options);
|
||||
|
||||
const clientWallet = new Wallet(clientKeypair);
|
||||
const clientProvider = new AnchorProvider(connection, clientWallet, options);
|
||||
|
||||
return await MangoClient.connect(
|
||||
clientProvider,
|
||||
'mainnet-beta',
|
||||
MANGO_V4_ID['mainnet-beta'],
|
||||
{
|
||||
idsSource: 'get-program-accounts',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function computePriceImpact(
|
||||
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);
|
||||
|
||||
let res = await response.json();
|
||||
res = res.data[0];
|
||||
|
||||
return {
|
||||
outAmount: parseFloat(res.outAmount),
|
||||
priceImpactPct: parseFloat(res.priceImpactPct),
|
||||
};
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const client = await buildClient();
|
||||
const group = await client.getGroup(new PublicKey(GROUP_PK));
|
||||
await group.reloadAll(client);
|
||||
|
||||
console.log(
|
||||
`${'COIN'.padStart(20)}, ${'Scale'.padStart(8)}, ${'Liq Fee'.padStart(
|
||||
6,
|
||||
)}, ${'$->coin'.padStart(6)}, ${'coin-$'.padStart(6)}`,
|
||||
);
|
||||
|
||||
for (const bank of Array.from(group.banksMapByMint.values())) {
|
||||
const usdcMint = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v';
|
||||
|
||||
const pi1 = await computePriceImpact(
|
||||
bank[0].depositWeightScaleStartQuote.toString(),
|
||||
usdcMint,
|
||||
bank[0].mint.toBase58(),
|
||||
);
|
||||
const inAmount = toNative(
|
||||
Math.min(
|
||||
Math.floor(
|
||||
toUiDecimalsForQuote(bank[0].depositWeightScaleStartQuote) /
|
||||
bank[0].uiPrice,
|
||||
),
|
||||
99999999999,
|
||||
),
|
||||
bank[0].mintDecimals,
|
||||
);
|
||||
const pi2 = await computePriceImpact(
|
||||
inAmount.toString(),
|
||||
bank[0].mint.toBase58(),
|
||||
usdcMint,
|
||||
);
|
||||
console.log(
|
||||
`${bank[0].name.padStart(20)}, ${(
|
||||
'$' +
|
||||
toUiDecimalsForQuote(bank[0].depositWeightScaleStartQuote).toString()
|
||||
).padStart(8)}, ${(bank[0].liquidationFee.toNumber() * 100)
|
||||
.toFixed(3)
|
||||
.padStart(6)}%, ${(pi1.priceImpactPct * 100).toFixed(2)}%, ${(
|
||||
pi2.priceImpactPct * 100
|
||||
).toFixed(2)}%`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
|
@ -0,0 +1,440 @@
|
|||
import { AnchorProvider, BN, 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';
|
||||
|
||||
const { MB_CLUSTER_URL } = process.env;
|
||||
|
||||
const GROUP_PK = '78b8f4cGCwmZ9ysPFMWLaLTkkaYnUjwMJYStWe5RTSSX';
|
||||
|
||||
async function buildClient(): Promise<MangoClient> {
|
||||
const clientKeypair = new Keypair();
|
||||
|
||||
const options = AnchorProvider.defaultOptions();
|
||||
const connection = new Connection(MB_CLUSTER_URL!, options);
|
||||
|
||||
const clientWallet = new Wallet(clientKeypair);
|
||||
const clientProvider = new AnchorProvider(connection, clientWallet, options);
|
||||
|
||||
return await MangoClient.connect(
|
||||
clientProvider,
|
||||
'mainnet-beta',
|
||||
MANGO_V4_ID['mainnet-beta'],
|
||||
{
|
||||
idsSource: 'get-program-accounts',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
main();
|
|
@ -236,6 +236,27 @@ export class MangoAccount {
|
|||
return tp ? tp.balance(bank) : ZERO_I80F48();
|
||||
}
|
||||
|
||||
// TODO: once perp quote is merged, also add in the settle token balance if relevant
|
||||
public getEffectiveTokenBalance(group: Group, bank: Bank): I80F48 {
|
||||
const tp = this.getToken(bank.tokenIndex);
|
||||
if (tp) {
|
||||
const bal = tp.balance(bank);
|
||||
for (const serum3Market of Array.from(
|
||||
group.serum3MarketsMapByMarketIndex.values(),
|
||||
)) {
|
||||
const oo = this.serum3OosMapByMarketIndex.get(serum3Market.marketIndex);
|
||||
if (serum3Market.baseTokenIndex == bank.tokenIndex && oo) {
|
||||
bal.add(I80F48.fromI64(oo.baseTokenFree));
|
||||
}
|
||||
if (serum3Market.quoteTokenIndex == bank.tokenIndex && oo) {
|
||||
bal.add(I80F48.fromI64(oo.quoteTokenFree));
|
||||
}
|
||||
}
|
||||
return bal;
|
||||
}
|
||||
return ZERO_I80F48();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param bank
|
||||
|
@ -550,7 +571,7 @@ export class MangoAccount {
|
|||
Math.pow(10, targetBank.mintDecimals - sourceBank.mintDecimals)),
|
||||
),
|
||||
);
|
||||
const sourceBalance = this.getTokenBalance(sourceBank);
|
||||
const sourceBalance = this.getEffectiveTokenBalance(group, sourceBank);
|
||||
if (maxSource.gt(sourceBalance)) {
|
||||
const sourceBorrow = maxSource.sub(sourceBalance);
|
||||
maxSource = sourceBalance.add(
|
||||
|
@ -686,7 +707,7 @@ export class MangoAccount {
|
|||
let quoteAmount = nativeAmount.div(quoteBank.price);
|
||||
// If its a bid then the reserved fund and potential loan is in base
|
||||
// also keep some buffer for fees, use taker fees for worst case simulation.
|
||||
const quoteBalance = this.getTokenBalance(quoteBank);
|
||||
const quoteBalance = this.getEffectiveTokenBalance(group, quoteBank);
|
||||
if (quoteAmount.gt(quoteBalance)) {
|
||||
const quoteBorrow = quoteAmount.sub(quoteBalance);
|
||||
quoteAmount = quoteBalance.add(
|
||||
|
@ -728,7 +749,7 @@ export class MangoAccount {
|
|||
let baseAmount = nativeAmount.div(baseBank.price);
|
||||
// If its a ask then the reserved fund and potential loan is in base
|
||||
// also keep some buffer for fees, use taker fees for worst case simulation.
|
||||
const baseBalance = this.getTokenBalance(baseBank);
|
||||
const baseBalance = this.getEffectiveTokenBalance(group, baseBank);
|
||||
if (baseAmount.gt(baseBalance)) {
|
||||
const baseBorrow = baseAmount.sub(baseBalance);
|
||||
baseAmount = baseBalance.add(
|
||||
|
|
|
@ -20,6 +20,7 @@ export {
|
|||
} from './clientIxParamBuilder';
|
||||
export * from './constants';
|
||||
export * from './numbers/I80F48';
|
||||
export * from './risk';
|
||||
export * from './router';
|
||||
export * from './types';
|
||||
export * from './utils';
|
||||
|
|
|
@ -0,0 +1,416 @@
|
|||
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 {
|
||||
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;
|
||||
}
|
||||
|
||||
export interface PerpPositionsToBeLiquidated {
|
||||
Market: string;
|
||||
Price: number;
|
||||
'Future Price': number;
|
||||
'Notional Position': number;
|
||||
}
|
||||
|
||||
export interface AccountEquity {
|
||||
Account: PublicKey;
|
||||
Equity: number;
|
||||
}
|
||||
|
||||
export interface Risk {
|
||||
assetRally: { title: string; data: LiqorPriceImpact[] };
|
||||
assetDrop: { title: string; data: LiqorPriceImpact[] };
|
||||
perpRally: { title: string; data: PerpPositionsToBeLiquidated[] };
|
||||
perpDrop: { title: string; data: PerpPositionsToBeLiquidated[] };
|
||||
marketMakerEquity: { title: string; data: AccountEquity[] };
|
||||
liqorEquity: { title: string; data: AccountEquity[] };
|
||||
}
|
||||
|
||||
export 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 (await buildFetch())(url);
|
||||
|
||||
try {
|
||||
let res = await response.json();
|
||||
res = res.data[0];
|
||||
return {
|
||||
outAmount: parseFloat(res.outAmount),
|
||||
priceImpactPct: parseFloat(res.priceImpactPct),
|
||||
};
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getPriceImpactForLiqor(
|
||||
group: Group,
|
||||
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),
|
||||
liabs: toUiDecimalsForQuote(
|
||||
a.getLiabsValue(group, HealthType.liquidationEnd),
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
const usdcBank = group.getFirstBankByTokenIndex(0 as TokenIndex);
|
||||
const usdcMint = usdcBank.mint;
|
||||
|
||||
return await Promise.all(
|
||||
Array.from(group.banksMapByMint.values())
|
||||
.filter((banks) => banks[0].tokenIndex !== usdcBank.tokenIndex)
|
||||
.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)
|
||||
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));
|
||||
|
||||
// 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());
|
||||
|
||||
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'],
|
||||
|
||||
!liabsInUsdc.eq(ZERO_I80F48())
|
||||
? computePriceImpactOnJup(
|
||||
liabsInUsdc.toString(),
|
||||
usdcMint.toBase58(),
|
||||
bank.mint.toBase58(),
|
||||
)
|
||||
: Promise.resolve({ priceImpactPct: 0, outAmount: 0 }),
|
||||
|
||||
!assets.eq(ZERO_I80F48())
|
||||
? computePriceImpactOnJup(
|
||||
assets.floor().toString(),
|
||||
bank.mint.toBase58(),
|
||||
usdcMint.toBase58(),
|
||||
)
|
||||
: Promise.resolve({ priceImpactPct: 0, outAmount: 0 }),
|
||||
]);
|
||||
|
||||
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,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
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),
|
||||
liabs: toUiDecimalsForQuote(
|
||||
a.getLiabsValue(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 {
|
||||
Market: pm.name,
|
||||
Price: pm['oldUiPrice'],
|
||||
'Future Price': pm._uiPrice,
|
||||
'Notional Position': notionalPositionUi,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export async function getEquityForMangoAccounts(
|
||||
client: MangoClient,
|
||||
group: Group,
|
||||
mangoAccounts: PublicKey[],
|
||||
): Promise<AccountEquity[]> {
|
||||
// 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)),
|
||||
);
|
||||
|
||||
return liqorMangoAccounts.map((a: MangoAccount) => {
|
||||
return {
|
||||
Account: a.publicKey,
|
||||
Equity: toUiDecimalsForQuote(a.getEquity(group)),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export async function getRiskStats(
|
||||
client: MangoClient,
|
||||
group: Group,
|
||||
change = 0.4, // simulates 40% price rally and price drop on tokens and markets
|
||||
): Promise<Risk> {
|
||||
// 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`,
|
||||
)
|
||||
).json()
|
||||
).map((data) => new PublicKey(data['liqor']));
|
||||
} catch (error) {
|
||||
liqors = [];
|
||||
}
|
||||
|
||||
// Get known mms
|
||||
const mms = [
|
||||
new PublicKey('CtHuPg2ctVVV7nqmvVEcMtcWyJAgtZw9YcNHFQidjPgF'),
|
||||
new PublicKey('F1SZxEDxxCSLVjEBbMEjDYqajWRJQRCZBwPQnmcVvTLV'),
|
||||
new PublicKey('BGYWnqfaauCeebFQXEfYuDCktiVG8pqpprrsD4qfqL53'),
|
||||
new PublicKey('9XJt2tvSZghsMAhWto1VuPBrwXsiimPtsTR8XwGgDxK2'),
|
||||
];
|
||||
|
||||
// Get all mango accounts
|
||||
const mangoAccounts = await client.getAllMangoAccounts(group, true);
|
||||
|
||||
// Clone group, and simulate change % price drop for all assets
|
||||
const drop = 1 - change;
|
||||
const groupDrop: Group = cloneDeep(group);
|
||||
Array.from(groupDrop.banksMapByTokenIndex.values())
|
||||
.flat()
|
||||
.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));
|
||||
});
|
||||
|
||||
// Clone group, and simulate change % price rally for all assets
|
||||
const rally = 1 + change;
|
||||
const groupRally: Group = cloneDeep(group);
|
||||
Array.from(groupRally.banksMapByTokenIndex.values())
|
||||
.flat()
|
||||
.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,
|
||||
perpDrop,
|
||||
perpRally,
|
||||
liqorEquity,
|
||||
marketMakerEquity,
|
||||
] = await Promise.all([
|
||||
getPriceImpactForLiqor(groupDrop, mangoAccounts),
|
||||
getPriceImpactForLiqor(groupDrop, mangoAccounts),
|
||||
getPerpPositionsToBeLiquidated(groupDrop, mangoAccounts),
|
||||
getPerpPositionsToBeLiquidated(groupRally, mangoAccounts),
|
||||
getEquityForMangoAccounts(client, group, liqors),
|
||||
getEquityForMangoAccounts(client, group, mms),
|
||||
]);
|
||||
|
||||
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.
|
||||
This would be the slippage they would face on buying-liabs/offloading-assets tokens acquired from unhealth accounts after a 20% drop`,
|
||||
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`,
|
||||
data: assetRally,
|
||||
},
|
||||
perpDrop: {
|
||||
title: `Table 2a: Perp notional that liqor need to liquidate after a 20% drop`,
|
||||
data: perpDrop,
|
||||
},
|
||||
perpRally: {
|
||||
title: `Table 2b: Perp notional that liqor need to liquidate after a 20% rally`,
|
||||
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,
|
||||
},
|
||||
};
|
||||
}
|
113
yarn.lock
113
yarn.lock
|
@ -37,6 +37,11 @@
|
|||
dependencies:
|
||||
regenerator-runtime "^0.13.11"
|
||||
|
||||
"@colors/colors@1.5.0":
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
|
||||
integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==
|
||||
|
||||
"@coral-xyz/anchor@^0.26.0":
|
||||
version "0.26.0"
|
||||
resolved "https://registry.yarnpkg.com/@coral-xyz/anchor/-/anchor-0.26.0.tgz#c8e4f7177e93441afd030f22d777d54d0194d7d1"
|
||||
|
@ -109,6 +114,31 @@
|
|||
"@ethersproject/logger" "^5.6.0"
|
||||
hash.js "1.1.7"
|
||||
|
||||
"@fast-csv/format@4.3.5":
|
||||
version "4.3.5"
|
||||
resolved "https://registry.yarnpkg.com/@fast-csv/format/-/format-4.3.5.tgz#90d83d1b47b6aaf67be70d6118f84f3e12ee1ff3"
|
||||
integrity sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==
|
||||
dependencies:
|
||||
"@types/node" "^14.0.1"
|
||||
lodash.escaperegexp "^4.1.2"
|
||||
lodash.isboolean "^3.0.3"
|
||||
lodash.isequal "^4.5.0"
|
||||
lodash.isfunction "^3.0.9"
|
||||
lodash.isnil "^4.0.0"
|
||||
|
||||
"@fast-csv/parse@4.3.6":
|
||||
version "4.3.6"
|
||||
resolved "https://registry.yarnpkg.com/@fast-csv/parse/-/parse-4.3.6.tgz#ee47d0640ca0291034c7aa94039a744cfb019264"
|
||||
integrity sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==
|
||||
dependencies:
|
||||
"@types/node" "^14.0.1"
|
||||
lodash.escaperegexp "^4.1.2"
|
||||
lodash.groupby "^4.6.0"
|
||||
lodash.isfunction "^3.0.9"
|
||||
lodash.isnil "^4.0.0"
|
||||
lodash.isundefined "^3.0.1"
|
||||
lodash.uniq "^4.5.0"
|
||||
|
||||
"@humanwhocodes/config-array@^0.5.0":
|
||||
version "0.5.0"
|
||||
resolved "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz"
|
||||
|
@ -496,6 +526,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.55.tgz#c329cbd434c42164f846b909bd6f85b5537f6240"
|
||||
integrity sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==
|
||||
|
||||
"@types/node@^14.0.1":
|
||||
version "14.18.43"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.43.tgz#679e000d9f1d914132ea295b4a1ffdf20370ec49"
|
||||
integrity sha512-n3eFEaoem0WNwLux+k272P0+aq++5o05bA9CfiwKPdYPB5ZambWKdWoeHy7/OJiizMhzg27NLaZ6uzjLTzXceQ==
|
||||
|
||||
"@types/node@^18.11.18":
|
||||
version "18.11.18"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.18.tgz#8dfb97f0da23c2293e554c5a50d61ef134d7697f"
|
||||
|
@ -740,6 +775,15 @@ axios@^1.1.3:
|
|||
form-data "^4.0.0"
|
||||
proxy-from-env "^1.1.0"
|
||||
|
||||
axios@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-1.4.0.tgz#38a7bf1224cd308de271146038b551d725f0be1f"
|
||||
integrity sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==
|
||||
dependencies:
|
||||
follow-redirects "^1.15.0"
|
||||
form-data "^4.0.0"
|
||||
proxy-from-env "^1.1.0"
|
||||
|
||||
balanced-match@^1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz"
|
||||
|
@ -983,6 +1027,15 @@ chokidar@3.5.3:
|
|||
optionalDependencies:
|
||||
fsevents "~2.3.2"
|
||||
|
||||
cli-table3@^0.6.3:
|
||||
version "0.6.3"
|
||||
resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.3.tgz#61ab765aac156b52f222954ffc607a6f01dbeeb2"
|
||||
integrity sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==
|
||||
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"
|
||||
|
@ -1033,6 +1086,13 @@ concat-map@0.0.1:
|
|||
resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz"
|
||||
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
|
||||
|
||||
console-table-printer@^2.11.1:
|
||||
version "2.11.1"
|
||||
resolved "https://registry.yarnpkg.com/console-table-printer/-/console-table-printer-2.11.1.tgz#c2dfe56e6343ea5bcfa3701a4be29fe912dbd9c7"
|
||||
integrity sha512-8LfFpbF/BczoxPwo2oltto5bph8bJkGOATXsg3E9ddMJOGnWJciKHldx2zDj5XIBflaKzPfVCjOTl6tMh7lErg==
|
||||
dependencies:
|
||||
simple-wcswidth "^1.0.1"
|
||||
|
||||
crc@^4.1.0:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/crc/-/crc-4.1.1.tgz#cb926237b56739f82c8533da1b66925ed33e011f"
|
||||
|
@ -1350,6 +1410,14 @@ eyes@^0.1.8:
|
|||
resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0"
|
||||
integrity sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==
|
||||
|
||||
fast-csv@^4.3.6:
|
||||
version "4.3.6"
|
||||
resolved "https://registry.yarnpkg.com/fast-csv/-/fast-csv-4.3.6.tgz#70349bdd8fe4d66b1130d8c91820b64a21bc4a63"
|
||||
integrity sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==
|
||||
dependencies:
|
||||
"@fast-csv/format" "4.3.5"
|
||||
"@fast-csv/parse" "4.3.6"
|
||||
|
||||
fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
|
||||
version "3.1.3"
|
||||
resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz"
|
||||
|
@ -1775,6 +1843,41 @@ locate-path@^6.0.0:
|
|||
dependencies:
|
||||
p-locate "^5.0.0"
|
||||
|
||||
lodash.escaperegexp@^4.1.2:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347"
|
||||
integrity sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==
|
||||
|
||||
lodash.groupby@^4.6.0:
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.groupby/-/lodash.groupby-4.6.0.tgz#0b08a1dcf68397c397855c3239783832df7403d1"
|
||||
integrity sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==
|
||||
|
||||
lodash.isboolean@^3.0.3:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6"
|
||||
integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==
|
||||
|
||||
lodash.isequal@^4.5.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
|
||||
integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==
|
||||
|
||||
lodash.isfunction@^3.0.9:
|
||||
version "3.0.9"
|
||||
resolved "https://registry.yarnpkg.com/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz#06de25df4db327ac931981d1bdb067e5af68d051"
|
||||
integrity sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==
|
||||
|
||||
lodash.isnil@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.isnil/-/lodash.isnil-4.0.0.tgz#49e28cd559013458c814c5479d3c663a21bfaa6c"
|
||||
integrity sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==
|
||||
|
||||
lodash.isundefined@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz#23ef3d9535565203a66cefd5b830f848911afb48"
|
||||
integrity sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==
|
||||
|
||||
lodash.merge@^4.6.2:
|
||||
version "4.6.2"
|
||||
resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz"
|
||||
|
@ -1785,6 +1888,11 @@ lodash.truncate@^4.4.2:
|
|||
resolved "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz"
|
||||
integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=
|
||||
|
||||
lodash.uniq@^4.5.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
|
||||
integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==
|
||||
|
||||
lodash.zipobject@^4.1.3:
|
||||
version "4.1.3"
|
||||
resolved "https://registry.yarnpkg.com/lodash.zipobject/-/lodash.zipobject-4.1.3.tgz#b399f5aba8ff62a746f6979bf20b214f964dbef8"
|
||||
|
@ -2254,6 +2362,11 @@ shiki@^0.10.0:
|
|||
vscode-oniguruma "^1.6.1"
|
||||
vscode-textmate "5.2.0"
|
||||
|
||||
simple-wcswidth@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/simple-wcswidth/-/simple-wcswidth-1.0.1.tgz#8ab18ac0ae342f9d9b629604e54d2aa1ecb018b2"
|
||||
integrity sha512-xMO/8eNREtaROt7tJvWJqHBDTMFN4eiQ5I4JRMuilwfnFcV5W9u7RUkueNkdw0jPqGMX36iCywelS5yilTuOxg==
|
||||
|
||||
slash@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz"
|
||||
|
|
Loading…
Reference in New Issue