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:
microwavedcola1 2023-06-26 16:45:52 +02:00 committed by GitHub
parent 18e39bc197
commit 0e180ed380
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 150 additions and 51 deletions

View File

@ -1,23 +1,20 @@
import { AnchorProvider, Wallet } from '@coral-xyz/anchor';
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 { HealthCache } from '../../src/accounts/healthCache';
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 { MANGO_V4_ID } from '../../src/constants';
import { ZERO_I80F48 } from '../../src/numbers/I80F48';
import { toUiDecimalsForQuote } from '../../src/utils';
const 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 =
process.env.USER_KEYPAIR_OVERRIDE || process.env.MB_PAYER_KEYPAIR;
const GROUP_NUM = Number(process.env.GROUP_NUM || 0);
const MANGO_ACCOUNT_PK = process.env.MANGO_ACCOUNT_PK;
const MANGO_ACCOUNT_PK = new PublicKey(
process.env.MANGO_ACCOUNT_PK || PublicKey.default.toBase58(),
);
const CLUSTER: Cluster =
(process.env.CLUSTER_OVERRIDE as Cluster) || 'mainnet-beta';
@ -222,57 +219,82 @@ async function debugUser(
)) {
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> {
const options = AnchorProvider.defaultOptions();
const connection = new Connection(CLUSTER_URL!, options);
const wallet = new Wallet(new Keypair());
const provider = new AnchorProvider(connection, wallet, options);
const client = MangoClient.connect(provider, CLUSTER, MANGO_V4_ID[CLUSTER], {
idsSource: 'api',
});
const admin = Keypair.fromSecretKey(
Buffer.from(JSON.parse(fs.readFileSync(PAYER_KEYPAIR!, 'utf-8'))),
);
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',
},
const group = await client.getGroup(
new PublicKey('78b8f4cGCwmZ9ysPFMWLaLTkkaYnUjwMJYStWe5RTSSX'),
);
const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM);
const mangoAccounts = await client.getAllMangoAccounts(group, true);
mangoAccounts.sort((a, b) => b.getEquity(group).cmp(a.getEquity(group)));
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);
for (const mangoAccount of mangoAccounts) {
if (
!MANGO_ACCOUNT_PK ||
mangoAccount.publicKey.equals(new PublicKey(MANGO_ACCOUNT_PK))
) {
// 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);
}
for (const mangoAccount of mangoAccounts) {
if (
true &&
(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(
`account https://app.mango.markets/?address=${mangoAccount.publicKey}`,
);
await debugUser(client, group, mangoAccount);
}
}

View File

@ -654,7 +654,7 @@ export class HealthCache {
const rightValue = fun(right);
// 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 (
@ -1159,6 +1159,52 @@ export class HealthCache {
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 {

View File

@ -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 {
if (perpMarket.perpMarketIndex !== this.marketIndex) {
throw new Error("PerpPosition doesn't belong to the given market!");

View File

@ -436,6 +436,10 @@ export class PerpMarket {
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 {
return parseFloat(price.toString()) * this.priceLotsToUiConverter;
}

View File

@ -232,6 +232,10 @@ export function ONE_I80F48(): I80F48 {
return I80F48.fromNumber(1);
}
export function MINUS_ONE_I80F48(): I80F48 {
return I80F48.fromNumber(-1);
}
export function ZERO_I80F48(): I80F48 {
return I80F48.fromNumber(0);
}