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:
parent
65362cb4de
commit
44d0170ea9
2
anchor
2
anchor
|
@ -1 +1 @@
|
||||||
Subproject commit fbd238fb7c5a99557faa89620499858cb9806d0e
|
Subproject commit b52f23614601652a99ec6c27aec77bd327363b31
|
|
@ -14,6 +14,7 @@ pub enum Direction {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns up to `count` accounts with highest abs pnl (by `direction`) in descending order.
|
/// 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(
|
pub fn fetch_top(
|
||||||
context: &crate::context::MangoGroupContext,
|
context: &crate::context::MangoGroupContext,
|
||||||
account_fetcher: &impl AccountFetcher,
|
account_fetcher: &impl AccountFetcher,
|
||||||
|
|
|
@ -134,6 +134,34 @@ export class HealthCache {
|
||||||
return health;
|
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 {
|
public assets(healthType: HealthType): I80F48 {
|
||||||
const assets = ZERO_I80F48();
|
const assets = ZERO_I80F48();
|
||||||
for (const tokenInfo of this.tokenInfos) {
|
for (const tokenInfo of this.tokenInfos) {
|
||||||
|
|
|
@ -142,6 +142,12 @@ export class MangoAccount {
|
||||||
return this.serum3.filter((serum3) => serum3.isActive());
|
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[] {
|
public perpActive(): PerpPosition[] {
|
||||||
return this.perps.filter((perp) => perp.isActive());
|
return this.perps.filter((perp) => perp.isActive());
|
||||||
}
|
}
|
||||||
|
@ -268,6 +274,11 @@ export class MangoAccount {
|
||||||
return hc.health(healthType);
|
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`
|
* Health ratio, which is computed so `100 * (assets-liabs)/liabs`
|
||||||
* Note: health ratio is technically ∞ if liabs are 0
|
* Note: health ratio is technically ∞ if liabs are 0
|
||||||
|
@ -1234,6 +1245,14 @@ export class PerpPosition {
|
||||||
.div(this.basePositionLots.mul(perpMarket.baseLotSize))
|
.div(this.basePositionLots.mul(perpMarket.baseLotSize))
|
||||||
.abs();
|
.abs();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getPnl(perpMarket: PerpMarket): I80F48 {
|
||||||
|
return this.quotePositionNative.add(
|
||||||
|
I80F48.fromI64(this.basePositionLots.mul(perpMarket.baseLotSize)).mul(
|
||||||
|
perpMarket.price,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PerpPositionDto {
|
export class PerpPositionDto {
|
||||||
|
|
|
@ -3,9 +3,11 @@ import { utf8 } from '@project-serum/anchor/dist/cjs/utils/bytes';
|
||||||
import { PublicKey } from '@solana/web3.js';
|
import { PublicKey } from '@solana/web3.js';
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
import { MangoClient } from '../client';
|
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 { As, toNative, U64_MAX_BN } from '../utils';
|
||||||
import { OracleConfig, QUOTE_DECIMALS, TokenIndex } from './bank';
|
import { OracleConfig, QUOTE_DECIMALS, TokenIndex } from './bank';
|
||||||
|
import { Group } from './group';
|
||||||
|
import { MangoAccount } from './mangoAccount';
|
||||||
|
|
||||||
export type PerpMarketIndex = number & As<'perp-market-index'>;
|
export type PerpMarketIndex = number & As<'perp-market-index'>;
|
||||||
|
|
||||||
|
@ -329,6 +331,90 @@ export class PerpMarket {
|
||||||
return parseFloat(quantity.toString()) * this.quoteLotsToUiConverter;
|
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 {
|
toString(): string {
|
||||||
return (
|
return (
|
||||||
'PerpMarket ' +
|
'PerpMarket ' +
|
||||||
|
|
|
@ -319,10 +319,9 @@ async function fullMarketMaker() {
|
||||||
// Calculate pf level values
|
// Calculate pf level values
|
||||||
let pfQuoteValue: number | undefined = 0;
|
let pfQuoteValue: number | undefined = 0;
|
||||||
for (const mc of Array.from(marketContexts.values())) {
|
for (const mc of Array.from(marketContexts.values())) {
|
||||||
const pos = mangoAccount.getPerpPositionUi(
|
const pos = mangoAccount.perpPositionExistsForMarket(mc.perpMarket)
|
||||||
group,
|
? mangoAccount.getPerpPositionUi(group, mc.perpMarket.perpMarketIndex)
|
||||||
mc.perpMarket.perpMarketIndex,
|
: 0;
|
||||||
);
|
|
||||||
const mid = (mc.binanceBid! + mc.binanceAsk!) / 2;
|
const mid = (mc.binanceBid! + mc.binanceAsk!) / 2;
|
||||||
if (mid) {
|
if (mid) {
|
||||||
pfQuoteValue += pos * mid;
|
pfQuoteValue += pos * mid;
|
||||||
|
@ -405,11 +404,15 @@ async function makeMarketUpdateInstructions(
|
||||||
const sizePerc = mc.params.sizePerc;
|
const sizePerc = mc.params.sizePerc;
|
||||||
const quoteSize = equity * sizePerc;
|
const quoteSize = equity * sizePerc;
|
||||||
const size = quoteSize / fairValue;
|
const size = quoteSize / fairValue;
|
||||||
|
|
||||||
// console.log(`equity ${equity}`);
|
// console.log(`equity ${equity}`);
|
||||||
// console.log(`sizePerc ${sizePerc}`);
|
// console.log(`sizePerc ${sizePerc}`);
|
||||||
// console.log(`fairValue ${fairValue}`);
|
// console.log(`fairValue ${fairValue}`);
|
||||||
// console.log(`size ${size}`);
|
// 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 lean = (-leanCoeff * basePos) / size;
|
||||||
const pfQuoteLeanCoeff = params.pfQuoteLeanCoeff || 0.001; // How much to move if pf pos is equal to equity
|
const pfQuoteLeanCoeff = params.pfQuoteLeanCoeff || 0.001; // How much to move if pf pos is equal to equity
|
||||||
const pfQuoteLean = (pfQuoteValue / equity) * -pfQuoteLeanCoeff;
|
const pfQuoteLean = (pfQuoteValue / equity) * -pfQuoteLeanCoeff;
|
||||||
|
@ -508,6 +511,7 @@ async function makeMarketUpdateInstructions(
|
||||||
);
|
);
|
||||||
|
|
||||||
const posAsTradeSizes = basePos / size;
|
const posAsTradeSizes = basePos / size;
|
||||||
|
|
||||||
// console.log(
|
// console.log(
|
||||||
// `basePos ${basePos}, posAsTradeSizes ${posAsTradeSizes}, size ${size}`,
|
// `basePos ${basePos}, posAsTradeSizes ${posAsTradeSizes}, size ${size}`,
|
||||||
// );
|
// );
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { MangoAccount } from '../../accounts/mangoAccount';
|
||||||
import { PerpMarket, PerpOrderSide, PerpOrderType } from '../../accounts/perp';
|
import { PerpMarket, PerpOrderSide, PerpOrderType } from '../../accounts/perp';
|
||||||
import { MangoClient } from '../../client';
|
import { MangoClient } from '../../client';
|
||||||
import { MANGO_V4_ID } from '../../constants';
|
import { MANGO_V4_ID } from '../../constants';
|
||||||
|
import { ZERO_I80F48 } from '../../numbers/I80F48';
|
||||||
import { toUiDecimalsForQuote } from '../../utils';
|
import { toUiDecimalsForQuote } from '../../utils';
|
||||||
|
|
||||||
// For easy switching between mainnet and devnet, default is mainnet
|
// 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;
|
process.env.USER_KEYPAIR_OVERRIDE || process.env.MB_PAYER_KEYPAIR;
|
||||||
const MANGO_ACCOUNT_PK = process.env.MANGO_ACCOUNT_PK || '';
|
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(
|
async function takeOrder(
|
||||||
client: MangoClient,
|
client: MangoClient,
|
||||||
group: Group,
|
group: Group,
|
||||||
|
@ -105,6 +160,11 @@ async function main() {
|
||||||
// Take on OB
|
// Take on OB
|
||||||
const perpMarket = group.getPerpMarketByName('BTC-PERP');
|
const perpMarket = group.getPerpMarketByName('BTC-PERP');
|
||||||
while (true) {
|
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.bid);
|
||||||
await takeOrder(client, group, mangoAccount, perpMarket, PerpOrderSide.ask);
|
await takeOrder(client, group, mangoAccount, perpMarket, PerpOrderSide.ask);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue