mc/settle pnl ts client (#292)

* oracle peg client support

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>

* perp pnl settle, candidate finder, and example

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>
This commit is contained in:
microwavedcola1 2022-11-21 20:36:13 +01:00 committed by GitHub
parent 65362cb4de
commit 44d0170ea9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 205 additions and 7 deletions

2
anchor

@ -1 +1 @@
Subproject commit fbd238fb7c5a99557faa89620499858cb9806d0e
Subproject commit b52f23614601652a99ec6c27aec77bd327363b31

View File

@ -14,6 +14,7 @@ pub enum Direction {
}
/// Returns up to `count` accounts with highest abs pnl (by `direction`) in descending order.
/// Note: keep in sync with perp.ts:getSettlePnlCandidates
pub fn fetch_top(
context: &crate::context::MangoGroupContext,
account_fetcher: &impl AccountFetcher,

View File

@ -134,6 +134,34 @@ export class HealthCache {
return health;
}
// Note: only considers positive perp pnl contributions, see program code for more reasoning
public perpSettleHealth(): I80F48 {
const health = ZERO_I80F48();
for (const tokenInfo of this.tokenInfos) {
const contrib = tokenInfo.healthContribution(HealthType.maint);
// console.log(` - ti ${contrib}`);
health.iadd(contrib);
}
for (const serum3Info of this.serum3Infos) {
const contrib = serum3Info.healthContribution(
HealthType.maint,
this.tokenInfos,
);
// console.log(` - si ${contrib}`);
health.iadd(contrib);
}
for (const perpInfo of this.perpInfos) {
if (perpInfo.trustedMarket) {
const positiveContrib = perpInfo
.healthContribution(HealthType.maint)
.max(ZERO_I80F48());
// console.log(` - pi ${positiveContrib}`);
health.iadd(positiveContrib);
}
}
return health;
}
public assets(healthType: HealthType): I80F48 {
const assets = ZERO_I80F48();
for (const tokenInfo of this.tokenInfos) {

View File

@ -142,6 +142,12 @@ export class MangoAccount {
return this.serum3.filter((serum3) => serum3.isActive());
}
public perpPositionExistsForMarket(perpMarket: PerpMarket): boolean {
return this.perps.some(
(pp) => pp.isActive() && pp.marketIndex == perpMarket.perpMarketIndex,
);
}
public perpActive(): PerpPosition[] {
return this.perps.filter((perp) => perp.isActive());
}
@ -268,6 +274,11 @@ export class MangoAccount {
return hc.health(healthType);
}
public getPerpSettleHealth(group: Group): I80F48 {
const hc = HealthCache.fromMangoAccount(group, this);
return hc.perpSettleHealth();
}
/**
* Health ratio, which is computed so `100 * (assets-liabs)/liabs`
* Note: health ratio is technically if liabs are 0
@ -1234,6 +1245,14 @@ export class PerpPosition {
.div(this.basePositionLots.mul(perpMarket.baseLotSize))
.abs();
}
public getPnl(perpMarket: PerpMarket): I80F48 {
return this.quotePositionNative.add(
I80F48.fromI64(this.basePositionLots.mul(perpMarket.baseLotSize)).mul(
perpMarket.price,
),
);
}
}
export class PerpPositionDto {

View File

@ -3,9 +3,11 @@ import { utf8 } from '@project-serum/anchor/dist/cjs/utils/bytes';
import { PublicKey } from '@solana/web3.js';
import Big from 'big.js';
import { MangoClient } from '../client';
import { I80F48, I80F48Dto } from '../numbers/I80F48';
import { I80F48, I80F48Dto, ZERO_I80F48 } from '../numbers/I80F48';
import { As, toNative, U64_MAX_BN } from '../utils';
import { OracleConfig, QUOTE_DECIMALS, TokenIndex } from './bank';
import { Group } from './group';
import { MangoAccount } from './mangoAccount';
export type PerpMarketIndex = number & As<'perp-market-index'>;
@ -329,6 +331,90 @@ export class PerpMarket {
return parseFloat(quantity.toString()) * this.quoteLotsToUiConverter;
}
/**
* Returns a list of (upto count) accounts, and the pnl that is settle'able on this perp market,
* the list is sorted ascending for 'negative' direction and descending for 'positive' direction.
*
* NOTE: keep in sync with perp_pnl.rs:fetch_top
*
* TODO: replace with a more performant offchain service call
* @param client
* @param group
* @param direction
* @returns
*/
public async getSettlePnlCandidates(
client: MangoClient,
group: Group,
direction: 'negative' | 'positive',
count = 2,
): Promise<{ account: MangoAccount; settleablePnl: I80F48 }[]> {
let accs = (await client.getAllMangoAccounts(group))
.filter((acc) =>
// need a perp position in this market
acc.perpPositionExistsForMarket(this),
)
.map((acc) => {
return {
account: acc,
settleablePnl: acc
.perpActive()
.find((pp) => pp.marketIndex === this.perpMarketIndex)!
.getPnl(this),
};
});
accs = accs
.filter(
(acc) =>
// need perp positions with -ve pnl to settle +ve pnl and vice versa
(direction === 'negative' && acc.settleablePnl.lt(ZERO_I80F48())) ||
(direction === 'positive' && acc.settleablePnl.gt(ZERO_I80F48())),
)
.sort((a, b) =>
direction === 'negative'
? // most negative
a.settleablePnl.cmp(b.settleablePnl)
: // most positive
b.settleablePnl.cmp(a.settleablePnl),
);
if (direction === 'negative') {
let stable = 0;
for (let i = 0; i < accs.length; i++) {
const acc = accs[i];
const nextPnl =
i + 1 < accs.length ? accs[i + 1].settleablePnl : ZERO_I80F48();
const perpSettleHealth = acc.account.getPerpSettleHealth(group);
acc.settleablePnl =
// need positive health to settle against +ve pnl
perpSettleHealth.gt(ZERO_I80F48()) && !acc.account.beingLiquidated
? // can only settle min
acc.settleablePnl.max(perpSettleHealth.neg())
: ZERO_I80F48();
// If the ordering was unchanged `count` times we know we have the top `count` accounts
if (acc.settleablePnl.lte(nextPnl)) {
stable += 1;
if (stable >= count) {
break;
}
}
}
}
accs.sort((a, b) =>
direction === 'negative'
? // most negative
a.settleablePnl.cmp(b.settleablePnl)
: // most positive
b.settleablePnl.cmp(a.settleablePnl),
);
return accs.slice(0, count);
}
toString(): string {
return (
'PerpMarket ' +

View File

@ -319,10 +319,9 @@ async function fullMarketMaker() {
// Calculate pf level values
let pfQuoteValue: number | undefined = 0;
for (const mc of Array.from(marketContexts.values())) {
const pos = mangoAccount.getPerpPositionUi(
group,
mc.perpMarket.perpMarketIndex,
);
const pos = mangoAccount.perpPositionExistsForMarket(mc.perpMarket)
? mangoAccount.getPerpPositionUi(group, mc.perpMarket.perpMarketIndex)
: 0;
const mid = (mc.binanceBid! + mc.binanceAsk!) / 2;
if (mid) {
pfQuoteValue += pos * mid;
@ -405,11 +404,15 @@ async function makeMarketUpdateInstructions(
const sizePerc = mc.params.sizePerc;
const quoteSize = equity * sizePerc;
const size = quoteSize / fairValue;
// console.log(`equity ${equity}`);
// console.log(`sizePerc ${sizePerc}`);
// console.log(`fairValue ${fairValue}`);
// console.log(`size ${size}`);
const basePos = mangoAccount.getPerpPositionUi(group, perpMarketIndex, true);
const basePos = mangoAccount.perpPositionExistsForMarket(mc.perpMarket)
? mangoAccount.getPerpPositionUi(group, perpMarketIndex, true)
: 0;
const lean = (-leanCoeff * basePos) / size;
const pfQuoteLeanCoeff = params.pfQuoteLeanCoeff || 0.001; // How much to move if pf pos is equal to equity
const pfQuoteLean = (pfQuoteValue / equity) * -pfQuoteLeanCoeff;
@ -508,6 +511,7 @@ async function makeMarketUpdateInstructions(
);
const posAsTradeSizes = basePos / size;
// console.log(
// `basePos ${basePos}, posAsTradeSizes ${posAsTradeSizes}, size ${size}`,
// );

View File

@ -6,6 +6,7 @@ import { MangoAccount } from '../../accounts/mangoAccount';
import { PerpMarket, PerpOrderSide, PerpOrderType } from '../../accounts/perp';
import { MangoClient } from '../../client';
import { MANGO_V4_ID } from '../../constants';
import { ZERO_I80F48 } from '../../numbers/I80F48';
import { toUiDecimalsForQuote } from '../../utils';
// For easy switching between mainnet and devnet, default is mainnet
@ -17,6 +18,60 @@ const USER_KEYPAIR =
process.env.USER_KEYPAIR_OVERRIDE || process.env.MB_PAYER_KEYPAIR;
const MANGO_ACCOUNT_PK = process.env.MANGO_ACCOUNT_PK || '';
async function settlePnl(
mangoAccount: MangoAccount,
perpMarket: PerpMarket,
client: MangoClient,
group: Group,
) {
if (!mangoAccount.perpPositionExistsForMarket(perpMarket)) {
return;
}
const pp = mangoAccount
.perpActive()
.find((pp) => pp.marketIndex === perpMarket.perpMarketIndex)!;
const pnl = pp.getPnl(perpMarket);
let profitableAccount, unprofitableAccount;
if (pnl.gt(ZERO_I80F48())) {
profitableAccount = mangoAccount;
unprofitableAccount = (
await perpMarket.getSettlePnlCandidates(client, group, 'negative')
)[0].account;
const sig = await client.perpSettlePnl(
group,
profitableAccount,
unprofitableAccount,
mangoAccount,
perpMarket.perpMarketIndex,
);
console.log(
`Settled pnl, sig https://explorer.solana.com/tx/${sig}?cluster=${
CLUSTER == 'devnet' ? 'devnet' : ''
}`,
);
} else if (pnl.lt(ZERO_I80F48())) {
unprofitableAccount = mangoAccount;
profitableAccount = (
await perpMarket.getSettlePnlCandidates(client, group, 'positive')
)[0].account;
const sig = await client.perpSettlePnl(
group,
profitableAccount,
unprofitableAccount,
mangoAccount,
perpMarket.perpMarketIndex,
);
console.log(
`Settled pnl, sig https://explorer.solana.com/tx/${sig}?cluster=${
CLUSTER == 'devnet' ? 'devnet' : ''
}`,
);
}
}
async function takeOrder(
client: MangoClient,
group: Group,
@ -105,6 +160,11 @@ async function main() {
// Take on OB
const perpMarket = group.getPerpMarketByName('BTC-PERP');
while (true) {
await group.reloadAll(client);
// Settle pnl
await settlePnl(mangoAccount, perpMarket, client, group);
await takeOrder(client, group, mangoAccount, perpMarket, PerpOrderSide.bid);
await takeOrder(client, group, mangoAccount, perpMarket, PerpOrderSide.ask);
}