From 0e180ed38001f3ce0c81813ac4aab4c329219b09 Mon Sep 17 00:00:00 2001 From: microwavedcola1 <89031858+microwavedcola1@users.noreply.github.com> Date: Mon, 26 Jun 2023 16:45:52 +0200 Subject: [PATCH] Mc/perp liq price 2 (#625) * perp position liquidation price calculator Signed-off-by: microwavedcola1 * refactor Signed-off-by: microwavedcola1 * ui method Signed-off-by: microwavedcola1 * Fixes from review Signed-off-by: microwavedcola1 --------- Signed-off-by: microwavedcola1 --- ts/client/scripts/archive/debug-user.ts | 122 ++++++++++++++---------- ts/client/src/accounts/healthCache.ts | 48 +++++++++- ts/client/src/accounts/mangoAccount.ts | 23 +++++ ts/client/src/accounts/perp.ts | 4 + ts/client/src/numbers/I80F48.ts | 4 + 5 files changed, 150 insertions(+), 51 deletions(-) diff --git a/ts/client/scripts/archive/debug-user.ts b/ts/client/scripts/archive/debug-user.ts index fecadbf75..2ec20934c 100644 --- a/ts/client/scripts/archive/debug-user.ts +++ b/ts/client/scripts/archive/debug-user.ts @@ -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 { 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); } } diff --git a/ts/client/src/accounts/healthCache.ts b/ts/client/src/accounts/healthCache.ts index 59fb553ca..57dabeb9e 100644 --- a/ts/client/src/accounts/healthCache.ts +++ b/ts/client/src/accounts/healthCache.ts @@ -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 { diff --git a/ts/client/src/accounts/mangoAccount.ts b/ts/client/src/accounts/mangoAccount.ts index f2b42eb29..b3ea331cd 100644 --- a/ts/client/src/accounts/mangoAccount.ts +++ b/ts/client/src/accounts/mangoAccount.ts @@ -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!"); diff --git a/ts/client/src/accounts/perp.ts b/ts/client/src/accounts/perp.ts index 2149bf2e2..6a755c725 100644 --- a/ts/client/src/accounts/perp.ts +++ b/ts/client/src/accounts/perp.ts @@ -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; } diff --git a/ts/client/src/numbers/I80F48.ts b/ts/client/src/numbers/I80F48.ts index ee4ce316a..d683ae6a5 100644 --- a/ts/client/src/numbers/I80F48.ts +++ b/ts/client/src/numbers/I80F48.ts @@ -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); }