Mc/perp liq price 2 (#625)
* perp position liquidation price calculator Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * refactor Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * ui method Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * Fixes from review Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> --------- Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>
This commit is contained in:
parent
18e39bc197
commit
0e180ed380
|
@ -1,23 +1,20 @@
|
||||||
import { AnchorProvider, Wallet } from '@coral-xyz/anchor';
|
import { AnchorProvider, Wallet } from '@coral-xyz/anchor';
|
||||||
import { Cluster, Connection, Keypair, PublicKey } from '@solana/web3.js';
|
import { Cluster, Connection, Keypair, PublicKey } from '@solana/web3.js';
|
||||||
import fs from 'fs';
|
import cloneDeep from 'lodash/cloneDeep';
|
||||||
import { Group } from '../../src/accounts/group';
|
import { Group } from '../../src/accounts/group';
|
||||||
import { HealthCache } from '../../src/accounts/healthCache';
|
|
||||||
import { HealthType, MangoAccount } from '../../src/accounts/mangoAccount';
|
import { HealthType, MangoAccount } from '../../src/accounts/mangoAccount';
|
||||||
import { PerpMarket } from '../../src/accounts/perp';
|
|
||||||
import { Serum3Market } from '../../src/accounts/serum3';
|
|
||||||
import { MangoClient } from '../../src/client';
|
import { MangoClient } from '../../src/client';
|
||||||
import { MANGO_V4_ID } from '../../src/constants';
|
import { MANGO_V4_ID } from '../../src/constants';
|
||||||
|
import { ZERO_I80F48 } from '../../src/numbers/I80F48';
|
||||||
import { toUiDecimalsForQuote } from '../../src/utils';
|
import { toUiDecimalsForQuote } from '../../src/utils';
|
||||||
|
|
||||||
const CLUSTER_URL =
|
const CLUSTER_URL =
|
||||||
process.env.CLUSTER_URL_OVERRIDE || process.env.MB_CLUSTER_URL;
|
process.env.CLUSTER_URL_OVERRIDE || process.env.MB_CLUSTER_URL;
|
||||||
const PAYER_KEYPAIR =
|
|
||||||
process.env.PAYER_KEYPAIR_OVERRIDE || process.env.MB_PAYER_KEYPAIR;
|
|
||||||
const USER_KEYPAIR =
|
const USER_KEYPAIR =
|
||||||
process.env.USER_KEYPAIR_OVERRIDE || process.env.MB_PAYER_KEYPAIR;
|
process.env.USER_KEYPAIR_OVERRIDE || process.env.MB_PAYER_KEYPAIR;
|
||||||
const GROUP_NUM = Number(process.env.GROUP_NUM || 0);
|
const MANGO_ACCOUNT_PK = new PublicKey(
|
||||||
const MANGO_ACCOUNT_PK = process.env.MANGO_ACCOUNT_PK;
|
process.env.MANGO_ACCOUNT_PK || PublicKey.default.toBase58(),
|
||||||
|
);
|
||||||
const CLUSTER: Cluster =
|
const CLUSTER: Cluster =
|
||||||
(process.env.CLUSTER_OVERRIDE as Cluster) || 'mainnet-beta';
|
(process.env.CLUSTER_OVERRIDE as Cluster) || 'mainnet-beta';
|
||||||
|
|
||||||
|
@ -222,57 +219,82 @@ async function debugUser(
|
||||||
)) {
|
)) {
|
||||||
getMaxForSerum3Wrapper(serum3Market);
|
getMaxForSerum3Wrapper(serum3Market);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Liquidation price for perp positions
|
||||||
|
for (const pp of mangoAccount.perpActive()) {
|
||||||
|
const pm = group.getPerpMarketByMarketIndex(pp.marketIndex);
|
||||||
|
const health = toUiDecimalsForQuote(
|
||||||
|
mangoAccount.getHealth(group, HealthType.maint),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
// pp.getNotionalValueUi(pm) > 1000 &&
|
||||||
|
// !(pp.getNotionalValueUi(pm) < health && pp.getBasePosition(pm).isPos())
|
||||||
|
// eslint-disable-next-line no-constant-condition
|
||||||
|
true
|
||||||
|
) {
|
||||||
|
const lp = await pp.getLiquidationPrice(group, mangoAccount);
|
||||||
|
if (lp.lt(ZERO_I80F48())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const lpUi = group
|
||||||
|
.getPerpMarketByMarketIndex(pp.marketIndex)
|
||||||
|
.priceNativeToUi(lp.toNumber());
|
||||||
|
|
||||||
|
const gClone: Group = cloneDeep(group);
|
||||||
|
gClone.getPerpMarketByMarketIndex(pm.perpMarketIndex)._price = lp;
|
||||||
|
|
||||||
|
const simHealth = toUiDecimalsForQuote(
|
||||||
|
mangoAccount.getHealth(gClone, HealthType.maint),
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
` - ${pm.name}, health: ${health.toLocaleString()}, side: ${
|
||||||
|
pp.getBasePosition(pm).isPos() ? 'LONG' : 'SHORT'
|
||||||
|
}, notional: ${pp
|
||||||
|
.getNotionalValueUi(pm)
|
||||||
|
.toLocaleString()}, liq price: ${lpUi.toLocaleString()}, sim health: ${simHealth.toLocaleString()}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main(): Promise<void> {
|
async function main(): Promise<void> {
|
||||||
const options = AnchorProvider.defaultOptions();
|
const options = AnchorProvider.defaultOptions();
|
||||||
const connection = new Connection(CLUSTER_URL!, options);
|
const connection = new Connection(CLUSTER_URL!, options);
|
||||||
|
const wallet = new Wallet(new Keypair());
|
||||||
const admin = Keypair.fromSecretKey(
|
const provider = new AnchorProvider(connection, wallet, options);
|
||||||
Buffer.from(JSON.parse(fs.readFileSync(PAYER_KEYPAIR!, 'utf-8'))),
|
const client = MangoClient.connect(provider, CLUSTER, MANGO_V4_ID[CLUSTER], {
|
||||||
);
|
|
||||||
console.log(`Admin ${admin.publicKey.toBase58()}`);
|
|
||||||
|
|
||||||
const adminWallet = new Wallet(admin);
|
|
||||||
const adminProvider = new AnchorProvider(connection, adminWallet, options);
|
|
||||||
const client = MangoClient.connect(
|
|
||||||
adminProvider,
|
|
||||||
CLUSTER,
|
|
||||||
MANGO_V4_ID[CLUSTER],
|
|
||||||
{
|
|
||||||
idsSource: 'api',
|
idsSource: 'api',
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM);
|
const group = await client.getGroup(
|
||||||
|
new PublicKey('78b8f4cGCwmZ9ysPFMWLaLTkkaYnUjwMJYStWe5RTSSX'),
|
||||||
for (const keypair of [USER_KEYPAIR!]) {
|
|
||||||
console.log();
|
|
||||||
const user = Keypair.fromSecretKey(
|
|
||||||
Buffer.from(JSON.parse(fs.readFileSync(keypair, 'utf-8'))),
|
|
||||||
);
|
);
|
||||||
const userWallet = new Wallet(user);
|
|
||||||
console.log(`User ${userWallet.publicKey.toBase58()}`);
|
|
||||||
|
|
||||||
const mangoAccounts = await client.getAllMangoAccounts(group, true);
|
const mangoAccounts = await client.getAllMangoAccounts(group, true);
|
||||||
|
mangoAccounts.sort((a, b) => b.getEquity(group).cmp(a.getEquity(group)));
|
||||||
|
|
||||||
for (const mangoAccount of mangoAccounts) {
|
for (const mangoAccount of mangoAccounts) {
|
||||||
if (
|
if (
|
||||||
!MANGO_ACCOUNT_PK ||
|
true &&
|
||||||
mangoAccount.publicKey.equals(new PublicKey(MANGO_ACCOUNT_PK))
|
(MANGO_ACCOUNT_PK!.equals(PublicKey.default) ||
|
||||||
|
// For specific account
|
||||||
|
mangoAccount.publicKey.equals(new PublicKey(MANGO_ACCOUNT_PK!))) &&
|
||||||
|
// Only interesting perp liq price candidates
|
||||||
|
mangoAccount.perpActive().length > 0 &&
|
||||||
|
mangoAccount
|
||||||
|
.perpActive()
|
||||||
|
.filter((pp) =>
|
||||||
|
pp
|
||||||
|
.getBasePosition(group.getPerpMarketByMarketIndex(pp.marketIndex))
|
||||||
|
.gt(ZERO_I80F48()),
|
||||||
|
).length > 0
|
||||||
) {
|
) {
|
||||||
// console.log();
|
|
||||||
console.log(
|
console.log(
|
||||||
`${mangoAccount.publicKey
|
`account https://app.mango.markets/?address=${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);
|
await debugUser(client, group, mangoAccount);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -654,7 +654,7 @@ export class HealthCache {
|
||||||
const rightValue = fun(right);
|
const rightValue = fun(right);
|
||||||
|
|
||||||
// console.log(
|
// console.log(
|
||||||
// ` - binaryApproximationSearch left ${left.toLocaleString()}, leftValue ${leftValue.toLocaleString()}, right ${right.toLocaleString()}, rightValue ${rightValue.toLocaleString()}, targetValue ${targetValue.toLocaleString()}`,
|
// ` - binaryApproximationSearch left ${left.toLocaleString()}, leftValue ${leftValue.toLocaleString()}, right ${right.toLocaleString()}, rightValue ${rightValue.toLocaleString()}, targetValue ${targetValue.toLocaleString()}, minStep ${minStep}`,
|
||||||
// );
|
// );
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
@ -1159,6 +1159,52 @@ export class HealthCache {
|
||||||
|
|
||||||
return baseLots.floor();
|
return baseLots.floor();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getPerpPositionLiquidationPrice(
|
||||||
|
group: Group,
|
||||||
|
mangoAccount: MangoAccount,
|
||||||
|
perpPosition: PerpPosition,
|
||||||
|
): I80F48 | null {
|
||||||
|
const hc = HealthCache.fromMangoAccount(group, mangoAccount);
|
||||||
|
const perpMarket = group.getPerpMarketByMarketIndex(
|
||||||
|
perpPosition.marketIndex,
|
||||||
|
);
|
||||||
|
|
||||||
|
function healthAfterPriceChange(newPrice: I80F48): I80F48 {
|
||||||
|
const gClone = cloneDeep(group);
|
||||||
|
gClone.getPerpMarketByMarketIndex(perpMarket.perpMarketIndex)._price =
|
||||||
|
newPrice;
|
||||||
|
const hc = HealthCache.fromMangoAccount(gClone, mangoAccount);
|
||||||
|
return hc.health(HealthType.maint);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (perpPosition.getBasePosition(perpMarket).isPos()) {
|
||||||
|
const zero = ZERO_I80F48();
|
||||||
|
const healthAtPriceZero = healthAfterPriceChange(zero);
|
||||||
|
if (healthAtPriceZero.gt(ZERO_I80F48())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return HealthCache.binaryApproximationSearch(
|
||||||
|
zero,
|
||||||
|
healthAtPriceZero,
|
||||||
|
perpMarket.price,
|
||||||
|
ZERO_I80F48(),
|
||||||
|
perpMarket.priceLotsToNative(new BN(1)),
|
||||||
|
healthAfterPriceChange,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const price1000x = perpMarket.price.mul(I80F48.fromNumber(1000));
|
||||||
|
return HealthCache.binaryApproximationSearch(
|
||||||
|
perpMarket.price,
|
||||||
|
hc.health(HealthType.maint),
|
||||||
|
price1000x,
|
||||||
|
ZERO_I80F48(),
|
||||||
|
perpMarket.priceLotsToNative(new BN(1)),
|
||||||
|
healthAfterPriceChange,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Prices {
|
export class Prices {
|
||||||
|
|
|
@ -1432,6 +1432,29 @@ export class PerpPosition {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getLiquidationPrice(
|
||||||
|
group: Group,
|
||||||
|
mangoAccount: MangoAccount,
|
||||||
|
): I80F48 | null {
|
||||||
|
if (this.basePositionLots.eq(new BN(0))) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return HealthCache.fromMangoAccount(
|
||||||
|
group,
|
||||||
|
mangoAccount,
|
||||||
|
).getPerpPositionLiquidationPrice(group, mangoAccount, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getLiquidationPriceUi(
|
||||||
|
group: Group,
|
||||||
|
mangoAccount: MangoAccount,
|
||||||
|
): number | null {
|
||||||
|
const pm = group.getPerpMarketByMarketIndex(this.marketIndex);
|
||||||
|
const lp = this.getLiquidationPrice(group, mangoAccount);
|
||||||
|
return lp == null ? null : pm.priceNativeToUi(lp.toNumber());
|
||||||
|
}
|
||||||
|
|
||||||
public getBreakEvenPrice(perpMarket: PerpMarket): I80F48 {
|
public getBreakEvenPrice(perpMarket: PerpMarket): I80F48 {
|
||||||
if (perpMarket.perpMarketIndex !== this.marketIndex) {
|
if (perpMarket.perpMarketIndex !== this.marketIndex) {
|
||||||
throw new Error("PerpPosition doesn't belong to the given market!");
|
throw new Error("PerpPosition doesn't belong to the given market!");
|
||||||
|
|
|
@ -436,6 +436,10 @@ export class PerpMarket {
|
||||||
return toNative(uiQuote, QUOTE_DECIMALS).div(this.quoteLotSize);
|
return toNative(uiQuote, QUOTE_DECIMALS).div(this.quoteLotSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public priceLotsToNative(price: BN): I80F48 {
|
||||||
|
return I80F48.fromI64(price.mul(this.quoteLotSize).div(this.baseLotSize));
|
||||||
|
}
|
||||||
|
|
||||||
public priceLotsToUi(price: BN): number {
|
public priceLotsToUi(price: BN): number {
|
||||||
return parseFloat(price.toString()) * this.priceLotsToUiConverter;
|
return parseFloat(price.toString()) * this.priceLotsToUiConverter;
|
||||||
}
|
}
|
||||||
|
|
|
@ -232,6 +232,10 @@ export function ONE_I80F48(): I80F48 {
|
||||||
return I80F48.fromNumber(1);
|
return I80F48.fromNumber(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function MINUS_ONE_I80F48(): I80F48 {
|
||||||
|
return I80F48.fromNumber(-1);
|
||||||
|
}
|
||||||
|
|
||||||
export function ZERO_I80F48(): I80F48 {
|
export function ZERO_I80F48(): I80F48 {
|
||||||
return I80F48.fromNumber(0);
|
return I80F48.fromNumber(0);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue