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.
|
||||
/// Note: keep in sync with perp.ts:getSettlePnlCandidates
|
||||
pub fn fetch_top(
|
||||
context: &crate::context::MangoGroupContext,
|
||||
account_fetcher: &impl AccountFetcher,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 ' +
|
||||
|
|
|
@ -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}`,
|
||||
// );
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue