Fix units
Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>
This commit is contained in:
parent
8d46679e56
commit
590cdb8f60
|
@ -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,10 @@ 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
|
||||
console.log(JSON.stringify(await getRiskStats(client, group), null, 2));
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
|
@ -65,6 +65,11 @@ export async function computePriceImpactOnJup(
|
|||
try {
|
||||
let res = await response.json();
|
||||
res = res.data[0];
|
||||
|
||||
if (parseFloat(res.priceImpactPct) < 0) {
|
||||
console.log(url);
|
||||
}
|
||||
|
||||
return {
|
||||
outAmount: parseFloat(res.outAmount),
|
||||
priceImpactPct: parseFloat(res.priceImpactPct),
|
||||
|
@ -198,11 +203,13 @@ export async function getPriceImpactForLiqor(
|
|||
'Oracle Price': bank['oldUiPrice'],
|
||||
'On-Chain Price': onChainPrice,
|
||||
'Future Price': bank._uiPrice!,
|
||||
'V4 Liq Fee': bank.liquidationFee.toNumber() * 10000,
|
||||
Liabs: toUiDecimalsForQuote(liabsInUsdc),
|
||||
'Liabs slippage': pi1.priceImpactPct * 10000,
|
||||
Assets: toUiDecimals(assets, bank.mintDecimals) * bank.uiPrice,
|
||||
'Assets Slippage': pi2.priceImpactPct * 10000,
|
||||
'V4 Liq Fee': Math.round(bank.liquidationFee.toNumber() * 10000),
|
||||
Liabs: Math.round(toUiDecimalsForQuote(liabsInUsdc)),
|
||||
'Liabs slippage': Math.round(pi1.priceImpactPct * 10000),
|
||||
Assets: Math.round(
|
||||
toUiDecimals(assets, bank.mintDecimals) * bank.uiPrice,
|
||||
),
|
||||
'Assets Slippage': Math.round(pi2.priceImpactPct * 10000),
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
@ -272,7 +279,7 @@ export async function getPerpPositionsToBeLiquidated(
|
|||
Market: pm.name,
|
||||
Price: pm['oldUiPrice'],
|
||||
'Future Price': pm._uiPrice,
|
||||
'Notional Position': notionalPositionUi,
|
||||
'Notional Position': Math.round(notionalPositionUi),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@ -299,7 +306,7 @@ export async function getEquityForMangoAccounts(
|
|||
const accountsWithEquity = liqorMangoAccounts.map((a: MangoAccount) => {
|
||||
return {
|
||||
Account: a.publicKey,
|
||||
Equity: toUiDecimalsForQuote(a.getEquity(group)),
|
||||
Equity: Math.round(toUiDecimalsForQuote(a.getEquity(group))),
|
||||
};
|
||||
});
|
||||
accountsWithEquity.sort((a, b) => b.Equity - a.Equity);
|
||||
|
|
Loading…
Reference in New Issue