mango-v4/ts/client/src/accounts/perp.ts

1036 lines
29 KiB
TypeScript

import { BN } from '@coral-xyz/anchor';
import { utf8 } from '@coral-xyz/anchor/dist/cjs/utils/bytes';
import { PublicKey } from '@solana/web3.js';
import Big from 'big.js';
import { MangoClient } from '../client';
import { RUST_U64_MAX } from '../constants';
import { I80F48, I80F48Dto, ZERO_I80F48 } from '../numbers/I80F48';
import { Modify } from '../types';
import {
As,
U64_MAX_BN,
toNative,
toUiDecimals,
QUOTE_DECIMALS,
} from '../utils';
import {
OracleConfig,
OracleConfigDto,
StablePriceModel,
TokenIndex,
} from './bank';
import { Group } from './group';
import { MangoAccount } from './mangoAccount';
export type PerpMarketIndex = number & As<'perp-market-index'>;
export type ParsedFillEvent = Modify<
FillEvent,
{
price: number;
quantity: number;
}
>;
export class PerpMarket {
public name: string;
public oracleConfig: OracleConfig;
public maintBaseAssetWeight: I80F48;
public initBaseAssetWeight: I80F48;
public maintBaseLiabWeight: I80F48;
public initBaseLiabWeight: I80F48;
public baseLiquidationFee: I80F48;
public makerFee: I80F48;
public takerFee: I80F48;
public minFunding: I80F48;
public maxFunding: I80F48;
public longFunding: I80F48;
public shortFunding: I80F48;
public feesAccrued: I80F48;
public feesSettled: I80F48;
public maintOverallAssetWeight: I80F48;
public initOverallAssetWeight: I80F48;
public _price: I80F48;
public _uiPrice: number;
public _oracleLastUpdatedSlot: number;
public _bids: BookSide;
public _asks: BookSide;
private priceLotsToUiConverter: number;
private baseLotsToUiConverter: number;
private quoteLotsToUiConverter: number;
static from(
publicKey: PublicKey,
obj: {
group: PublicKey;
settleTokenIndex: number;
perpMarketIndex: number;
groupInsuranceFund: number;
baseDecimals: number;
name: number[];
bids: PublicKey;
asks: PublicKey;
eventQueue: PublicKey;
oracle: PublicKey;
oracleConfig: OracleConfigDto;
stablePriceModel: StablePriceModel;
quoteLotSize: BN;
baseLotSize: BN;
maintBaseAssetWeight: I80F48Dto;
initBaseAssetWeight: I80F48Dto;
maintBaseLiabWeight: I80F48Dto;
initBaseLiabWeight: I80F48Dto;
openInterest: BN;
seqNum: BN;
registrationTime: BN;
minFunding: I80F48Dto;
maxFunding: I80F48Dto;
impactQuantity: BN;
longFunding: I80F48Dto;
shortFunding: I80F48Dto;
fundingLastUpdated: BN;
baseLiquidationFee: I80F48Dto;
makerFee: I80F48Dto;
takerFee: I80F48Dto;
feesAccrued: I80F48Dto;
feesSettled: I80F48Dto;
feePenalty: number;
settleFeeFlat: number;
settleFeeAmountThreshold: number;
settleFeeFractionLowHealth: number;
settlePnlLimitFactor: number;
settlePnlLimitWindowSizeTs: BN;
reduceOnly: number;
maintOverallAssetWeight: I80F48Dto;
initOverallAssetWeight: I80F48Dto;
positivePnlLiquidationFee: I80F48Dto;
},
): PerpMarket {
return new PerpMarket(
publicKey,
obj.group,
obj.settleTokenIndex as TokenIndex,
obj.perpMarketIndex as PerpMarketIndex,
obj.groupInsuranceFund == 1,
obj.baseDecimals,
obj.name,
obj.bids,
obj.asks,
obj.eventQueue,
obj.oracle,
obj.oracleConfig,
obj.stablePriceModel,
obj.quoteLotSize,
obj.baseLotSize,
obj.maintBaseAssetWeight,
obj.initBaseAssetWeight,
obj.maintBaseLiabWeight,
obj.initBaseLiabWeight,
obj.openInterest,
obj.seqNum,
obj.registrationTime,
obj.minFunding,
obj.maxFunding,
obj.impactQuantity,
obj.longFunding,
obj.shortFunding,
obj.fundingLastUpdated,
obj.baseLiquidationFee,
obj.makerFee,
obj.takerFee,
obj.feesAccrued,
obj.feesSettled,
obj.feePenalty,
obj.settleFeeFlat,
obj.settleFeeAmountThreshold,
obj.settleFeeFractionLowHealth,
obj.settlePnlLimitFactor,
obj.settlePnlLimitWindowSizeTs,
obj.reduceOnly == 1,
obj.maintOverallAssetWeight,
obj.initOverallAssetWeight,
obj.positivePnlLiquidationFee,
);
}
constructor(
public publicKey: PublicKey,
public group: PublicKey,
public settleTokenIndex: TokenIndex,
public perpMarketIndex: PerpMarketIndex, // TODO rename to marketIndex?
public groupInsuranceFund: boolean,
public baseDecimals: number,
name: number[],
public bids: PublicKey,
public asks: PublicKey,
public eventQueue: PublicKey,
public oracle: PublicKey,
oracleConfig: OracleConfigDto,
public stablePriceModel: StablePriceModel,
public quoteLotSize: BN,
public baseLotSize: BN,
maintBaseAssetWeight: I80F48Dto,
initBaseAssetWeight: I80F48Dto,
maintBaseLiabWeight: I80F48Dto,
initBaseLiabWeight: I80F48Dto,
public openInterest: BN,
public seqNum: BN,
public registrationTime: BN,
minFunding: I80F48Dto,
maxFunding: I80F48Dto,
public impactQuantity: BN,
longFunding: I80F48Dto,
shortFunding: I80F48Dto,
public fundingLastUpdated: BN,
baseLiquidationFee: I80F48Dto,
makerFee: I80F48Dto,
takerFee: I80F48Dto,
feesAccrued: I80F48Dto,
feesSettled: I80F48Dto,
public feePenalty: number,
public settleFeeFlat: number,
public settleFeeAmountThreshold: number,
public settleFeeFractionLowHealth: number,
public settlePnlLimitFactor: number,
public settlePnlLimitWindowSizeTs: BN,
public reduceOnly: boolean,
maintOverallAssetWeight: I80F48Dto,
initOverallAssetWeight: I80F48Dto,
positivePnlLiquidationFee: I80F48Dto,
) {
this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0];
this.oracleConfig = {
confFilter: I80F48.from(oracleConfig.confFilter),
maxStalenessSlots: oracleConfig.maxStalenessSlots,
} as OracleConfig;
this.maintBaseAssetWeight = I80F48.from(maintBaseAssetWeight);
this.initBaseAssetWeight = I80F48.from(initBaseAssetWeight);
this.maintBaseLiabWeight = I80F48.from(maintBaseLiabWeight);
this.initBaseLiabWeight = I80F48.from(initBaseLiabWeight);
this.baseLiquidationFee = I80F48.from(baseLiquidationFee);
this.makerFee = I80F48.from(makerFee);
this.takerFee = I80F48.from(takerFee);
this.minFunding = I80F48.from(minFunding);
this.maxFunding = I80F48.from(maxFunding);
this.longFunding = I80F48.from(longFunding);
this.shortFunding = I80F48.from(shortFunding);
this.feesAccrued = I80F48.from(feesAccrued);
this.feesSettled = I80F48.from(feesSettled);
this.maintOverallAssetWeight = I80F48.from(maintOverallAssetWeight);
this.initOverallAssetWeight = I80F48.from(initOverallAssetWeight);
this.priceLotsToUiConverter = new Big(10)
.pow(baseDecimals - QUOTE_DECIMALS)
.mul(new Big(this.quoteLotSize.toString()))
.div(new Big(this.baseLotSize.toString()))
.toNumber();
this.baseLotsToUiConverter = new Big(this.baseLotSize.toString())
.div(new Big(10).pow(baseDecimals))
.toNumber();
this.quoteLotsToUiConverter = new Big(this.quoteLotSize.toString())
.div(new Big(10).pow(QUOTE_DECIMALS))
.toNumber();
}
get price(): I80F48 {
if (!this._price) {
throw new Error(
`Undefined price for perpMarket ${this.publicKey} with marketIndex ${this.perpMarketIndex}!`,
);
}
return this._price;
}
get uiPrice(): number {
if (!this._uiPrice) {
throw new Error(
`Undefined price for perpMarket ${this.publicKey} with marketIndex ${this.perpMarketIndex}!`,
);
}
return this._uiPrice;
}
get oracleLastUpdatedSlot(): number {
if (!this._oracleLastUpdatedSlot) {
throw new Error(
`Undefined oracleLastUpdatedSlot for perpMarket ${this.publicKey} with marketIndex ${this.perpMarketIndex}!`,
);
}
return this._oracleLastUpdatedSlot;
}
get minOrderSize(): number {
return this.baseLotsToUiConverter;
}
get tickSize(): number {
return this.priceLotsToUiConverter;
}
insidePriceLimit(side: PerpOrderSide, orderPrice: number): boolean {
return (
(side === PerpOrderSide.bid &&
orderPrice <= this.maintBaseLiabWeight.toNumber() * this.uiPrice) ||
(side === PerpOrderSide.ask &&
orderPrice >= this.maintBaseAssetWeight.toNumber() * this.uiPrice)
);
}
public async loadAsks(
client: MangoClient,
forceReload = false,
): Promise<BookSide> {
if (forceReload || !this._asks) {
const asks = await client.program.account.bookSide.fetch(this.asks);
this._asks = BookSide.from(client, this, BookSideType.asks, asks as any);
}
return this._asks;
}
public async loadBids(
client: MangoClient,
forceReload = false,
): Promise<BookSide> {
if (forceReload || !this._bids) {
const bids = await client.program.account.bookSide.fetch(this.bids);
this._bids = BookSide.from(client, this, BookSideType.bids, bids as any);
}
return this._bids;
}
public async loadEventQueue(client: MangoClient): Promise<PerpEventQueue> {
const eventQueue = await client.program.account.eventQueue.fetch(
this.eventQueue,
);
return new PerpEventQueue(client, eventQueue.header, eventQueue.buf);
}
public async loadFills(
client: MangoClient,
lastSeqNum: BN = new BN(0),
): Promise<(OutEvent | FillEvent | LiquidateEvent)[]> {
const eventQueue = await this.loadEventQueue(client);
return eventQueue
.eventsSince(lastSeqNum)
.filter((event) => event.eventType == PerpEventQueue.FILL_EVENT_TYPE)
.map(this.parseFillEvent.bind(this)) as ParsedFillEvent[];
}
public parseFillEvent(event): ParsedFillEvent {
const quantity = this.baseLotsToUi(event.quantity);
const price = this.priceLotsToUi(event.price);
return {
...event,
quantity,
size: quantity,
price,
};
}
public async logOb(client: MangoClient): Promise<string> {
let res = ``;
res += ` ${this.name} OrderBook`;
let orders = await this?.loadAsks(client);
for (const order of orders!.items()) {
res += `\n ${order.uiPrice.toFixed(5).padStart(10)}, ${order.uiSize
.toString()
.padStart(10)} ${
order.isOraclePegged && order.oraclePeggedProperties
? order.oraclePeggedProperties.pegLimit.toNumber() + ' (PegLimit)'
: ''
}`;
}
res += `\n asks ↑ --------- ↓ bids`;
orders = await this?.loadBids(client);
for (const order of orders!.items()) {
res += `\n ${order.uiPrice.toFixed(5).padStart(10)}, ${order.uiSize
.toString()
.padStart(10)} ${
order.isOraclePegged && order.oraclePeggedProperties
? order.oraclePeggedProperties.pegLimit.toNumber() + ' (PegLimit)'
: ''
}`;
}
return res;
}
/**
*
* @param bids
* @param asks
* @returns returns funding rate per hour
*/
public getCurrentFundingRate(bids: BookSide, asks: BookSide): number {
const MIN_FUNDING = this.minFunding.toNumber();
const MAX_FUNDING = this.maxFunding.toNumber();
const bid = bids.getImpactPriceUi(new BN(this.impactQuantity));
const ask = asks.getImpactPriceUi(new BN(this.impactQuantity));
const indexPrice = this._uiPrice;
let funding;
if (bid !== undefined && ask !== undefined) {
const bookPrice = (bid + ask) / 2;
funding = Math.min(
Math.max(bookPrice / indexPrice - 1, MIN_FUNDING),
MAX_FUNDING,
);
} else if (bid !== undefined) {
funding = MAX_FUNDING;
} else if (ask !== undefined) {
funding = MIN_FUNDING;
} else {
funding = 0;
}
return funding / 24 / Math.pow(10, QUOTE_DECIMALS);
}
public uiPriceToLots(price: number): BN {
return toNative(price, QUOTE_DECIMALS)
.mul(this.baseLotSize)
.div(this.quoteLotSize.mul(new BN(Math.pow(10, this.baseDecimals))));
}
public uiBaseToLots(quantity: number): BN {
return toNative(quantity, this.baseDecimals).div(this.baseLotSize);
}
public uiQuoteToLots(uiQuote: number): BN {
return toNative(uiQuote, QUOTE_DECIMALS).div(this.quoteLotSize);
}
public priceLotsToUi(price: BN): number {
return parseFloat(price.toString()) * this.priceLotsToUiConverter;
}
public priceNativeToUi(price: number): number {
return toUiDecimals(price, QUOTE_DECIMALS - this.baseDecimals);
}
public baseLotsToUi(quantity: BN): number {
return parseFloat(quantity.toString()) * this.baseLotsToUiConverter;
}
public quoteLotsToUi(quantity: BN): number {
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 accountsWithSettleablePnl = (
await client.getAllMangoAccounts(group, true)
)
.filter((acc) => acc.perpPositionExistsForMarket(this))
.map((acc) => {
const pp = acc
.perpActive()
.find((pp) => pp.marketIndex === this.perpMarketIndex)!;
return {
account: acc,
settleablePnl: pp.getSettleablePnl(group, this, acc),
};
});
accountsWithSettleablePnl = accountsWithSettleablePnl
.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 < accountsWithSettleablePnl.length; i++) {
const acc = accountsWithSettleablePnl[i];
const nextPnl =
i + 1 < accountsWithSettleablePnl.length
? accountsWithSettleablePnl[i + 1].settleablePnl
: ZERO_I80F48();
const perpSettleHealth = acc.account.getPerpSettleHealth(group);
acc.settleablePnl =
// need positive settle health to settle against +ve pnl
perpSettleHealth.gt(ZERO_I80F48())
? // 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;
}
}
}
}
accountsWithSettleablePnl.sort((a, b) =>
direction === 'negative'
? // most negative
a.settleablePnl.cmp(b.settleablePnl)
: // most positive
b.settleablePnl.cmp(a.settleablePnl),
);
return accountsWithSettleablePnl.slice(0, count);
}
toString(): string {
return (
'PerpMarket ' +
'\n perpMarketIndex -' +
this.perpMarketIndex +
'\n maintAssetWeight -' +
this.maintBaseAssetWeight.toString() +
'\n initAssetWeight -' +
this.initBaseAssetWeight.toString() +
'\n maintLiabWeight -' +
this.maintBaseLiabWeight.toString() +
'\n initLiabWeight -' +
this.initBaseLiabWeight.toString() +
'\n baseLiquidationFee -' +
this.baseLiquidationFee.toString() +
'\n makerFee -' +
this.makerFee.toString() +
'\n takerFee -' +
this.takerFee.toString()
);
}
}
interface OrderTreeNodes {
bumpIndex: number;
freeListLen: number;
freeListHead: number;
nodes: [any];
}
interface OrderTreeRoot {
maybeNode: number;
leafCount: number;
}
export class BookSide {
private static INNER_NODE_TAG = 1;
private static LEAF_NODE_TAG = 2;
now: BN;
static from(
client: MangoClient,
perpMarket: PerpMarket,
bookSideType: BookSideType,
obj: {
roots: OrderTreeRoot[];
nodes: OrderTreeNodes;
},
): BookSide {
return new BookSide(
client,
perpMarket,
bookSideType,
obj.roots[0],
obj.roots[1],
obj.nodes,
);
}
constructor(
public client: MangoClient,
public perpMarket: PerpMarket,
public type: BookSideType,
public rootFixed: OrderTreeRoot,
public rootOraclePegged: OrderTreeRoot,
public orderTreeNodes: OrderTreeNodes,
maxBookDelay?: number,
) {
// Determine the maxTimestamp found on the book to use for tif
// If maxBookDelay is not provided, use 3600 as a very large number
maxBookDelay = maxBookDelay === undefined ? 3600 : maxBookDelay;
let maxTimestamp = new BN(new Date().getTime() / 1000 - maxBookDelay);
for (const node of this.orderTreeNodes.nodes) {
if (node.tag !== BookSide.LEAF_NODE_TAG) {
continue;
}
const leafNode = BookSide.toLeafNode(client, node.data);
if (leafNode.timestamp.gt(maxTimestamp)) {
maxTimestamp = leafNode.timestamp;
}
}
this.now = maxTimestamp;
}
static getPriceFromKey(key: BN): BN {
return key.ushrn(64);
}
/**
* iterates over all orders
*/
public *items(): Generator<PerpOrder> {
function isBetter(
type: PerpOrderSide,
a: PerpOrder,
b: PerpOrder,
): boolean {
return a.priceLots.eq(b.priceLots)
? a.seqNum.lt(b.seqNum) // if prices are equal prefer perp orders in the order they are placed
: type === BookSideType.bids // else compare the actual prices
? a.priceLots.gt(b.priceLots)
: b.priceLots.gt(a.priceLots);
}
const fGen = this.fixedItems();
const oPegGen = this.oraclePeggedItems();
let fOrderRes = fGen.next();
let oPegOrderRes = oPegGen.next();
while (true) {
if (fOrderRes.value && oPegOrderRes.value) {
if (isBetter(this.type, fOrderRes.value, oPegOrderRes.value)) {
yield fOrderRes.value;
fOrderRes = fGen.next();
} else {
yield oPegOrderRes.value;
oPegOrderRes = oPegGen.next();
}
} else if (fOrderRes.value && !oPegOrderRes.value) {
yield fOrderRes.value;
fOrderRes = fGen.next();
} else if (!fOrderRes.value && oPegOrderRes.value) {
yield oPegOrderRes.value;
oPegOrderRes = oPegGen.next();
} else if (!fOrderRes.value && !oPegOrderRes.value) {
break;
}
}
}
/**
* iterates over all orders,
* skips oracle pegged orders which are invalid due to oracle price crossing the peg limit,
* skips tif orders which are invalid due to tif having elapsed,
*/
public *itemsValid(): Generator<PerpOrder> {
const itemsGen = this.items();
let itemsRes = itemsGen.next();
while (true) {
if (itemsRes.value) {
const val = itemsRes.value;
if (
!val.isExpired &&
(!val.isOraclePegged ||
(val.isOraclePegged && !val.oraclePeggedProperties.isInvalid))
) {
yield val;
}
itemsRes = itemsGen.next();
} else {
break;
}
}
}
public *fixedItems(): Generator<PerpOrder> {
if (this.rootFixed.leafCount === 0) {
return;
}
const now = this.now;
const stack = [this.rootFixed.maybeNode];
const [left, right] = this.type === BookSideType.bids ? [1, 0] : [0, 1];
while (stack.length > 0) {
const index = stack.pop()!;
const node = this.orderTreeNodes.nodes[index];
if (node.tag === BookSide.INNER_NODE_TAG) {
const innerNode = BookSide.toInnerNode(this.client, node.data);
stack.push(innerNode.children[right], innerNode.children[left]);
} else if (node.tag === BookSide.LEAF_NODE_TAG) {
const leafNode = BookSide.toLeafNode(this.client, node.data);
const expiryTimestamp = leafNode.timeInForce
? leafNode.timestamp.add(new BN(leafNode.timeInForce))
: U64_MAX_BN;
yield PerpOrder.from(
this.perpMarket,
leafNode,
this.type,
now.gt(expiryTimestamp),
);
}
}
}
public *oraclePeggedItems(): Generator<PerpOrder> {
if (this.rootOraclePegged.leafCount === 0) {
return;
}
const now = this.now;
const stack = [this.rootOraclePegged.maybeNode];
const [left, right] = this.type === BookSideType.bids ? [1, 0] : [0, 1];
while (stack.length > 0) {
const index = stack.pop()!;
const node = this.orderTreeNodes.nodes[index];
if (node.tag === BookSide.INNER_NODE_TAG) {
const innerNode = BookSide.toInnerNode(this.client, node.data);
stack.push(innerNode.children[right], innerNode.children[left]);
} else if (node.tag === BookSide.LEAF_NODE_TAG) {
const leafNode = BookSide.toLeafNode(this.client, node.data);
const expiryTimestamp = leafNode.timeInForce
? leafNode.timestamp.add(new BN(leafNode.timeInForce))
: U64_MAX_BN;
yield PerpOrder.from(
this.perpMarket,
leafNode,
this.type,
now.gt(expiryTimestamp),
true,
);
}
}
}
public best(): PerpOrder | undefined {
return this.items().next().value;
}
getImpactPriceUi(baseLots: BN): number | undefined {
const s = new BN(0);
for (const order of this.items()) {
s.iadd(order.sizeLots);
if (s.gte(baseLots)) {
return order.uiPrice;
}
}
return undefined;
}
public getL2(depth: number): [number, number, BN, BN][] {
const levels: [BN, BN][] = [];
for (const { priceLots, sizeLots } of this.items()) {
if (levels.length > 0 && levels[levels.length - 1][0].eq(priceLots)) {
levels[levels.length - 1][1].iadd(sizeLots);
} else if (levels.length === depth) {
break;
} else {
levels.push([priceLots, sizeLots]);
}
}
return levels.map(([priceLots, sizeLots]) => [
this.perpMarket.priceLotsToUi(priceLots),
this.perpMarket.baseLotsToUi(sizeLots),
priceLots,
sizeLots,
]);
}
public getL2Ui(depth: number): [number, number][] {
const levels: [number, number][] = [];
for (const { uiPrice: price, uiSize: size } of this.items()) {
if (levels.length > 0 && levels[levels.length - 1][0] === price) {
levels[levels.length - 1][1] += size;
} else if (levels.length === depth) {
break;
} else {
levels.push([price, size]);
}
}
return levels;
}
static toInnerNode(client: MangoClient, data: [number]): InnerNode {
return (client.program as any)._coder.types.typeLayouts
.get('InnerNode')
.decode(Buffer.from([BookSide.INNER_NODE_TAG].concat(data)));
}
static toLeafNode(client: MangoClient, data: [number]): LeafNode {
return LeafNode.from(
(client.program as any)._coder.types.typeLayouts
.get('LeafNode')
.decode(Buffer.from([BookSide.LEAF_NODE_TAG].concat(data))),
);
}
}
export class BookSideType {
static bids = { bids: {} };
static asks = { asks: {} };
}
export class LeafNode {
static from(obj: {
ownerSlot: number;
orderType: PerpOrderType;
timeInForce: number;
key: BN;
owner: PublicKey;
quantity: BN;
timestamp: BN;
pegLimit: BN;
}): LeafNode {
return new LeafNode(
obj.ownerSlot,
obj.orderType,
obj.timeInForce,
obj.key,
obj.owner,
obj.quantity,
obj.timestamp,
obj.pegLimit,
);
}
constructor(
public ownerSlot: number,
public orderType: PerpOrderType,
public timeInForce: number,
public key: BN,
public owner: PublicKey,
public quantity: BN,
public timestamp: BN,
public pegLimit: BN,
) {}
}
export class InnerNode {
static from(obj: { children: [number] }): InnerNode {
return new InnerNode(obj.children);
}
constructor(public children: [number]) {}
}
export class PerpOrderSide {
static bid = { bid: {} };
static ask = { ask: {} };
}
export class PerpOrderType {
static limit = { limit: {} };
static immediateOrCancel = { immediateOrCancel: {} };
static postOnly = { postOnly: {} };
static market = { market: {} };
static postOnlySlide = { postOnlySlide: {} };
}
export class PerpOrder {
static from(
perpMarket: PerpMarket,
leafNode: LeafNode,
type: BookSideType,
isExpired = false,
isOraclePegged = false,
): PerpOrder {
const side =
type == BookSideType.bids ? PerpOrderSide.bid : PerpOrderSide.ask;
let priceLots;
let oraclePeggedProperties;
if (isOraclePegged) {
const priceData = leafNode.key.ushrn(64);
const priceOffset = priceData.sub(new BN(1).ushln(63));
priceLots = perpMarket.uiPriceToLots(perpMarket.uiPrice).add(priceOffset);
const isInvalid =
type === BookSideType.bids
? priceLots.gt(leafNode.pegLimit)
: leafNode.pegLimit.gt(priceLots);
oraclePeggedProperties = {
isInvalid,
priceOffset,
uiPriceOffset: perpMarket.priceLotsToUi(priceOffset),
pegLimit: leafNode.pegLimit,
uiPegLimit: perpMarket.priceLotsToUi(leafNode.pegLimit),
} as OraclePeggedProperties;
} else {
priceLots = BookSide.getPriceFromKey(leafNode.key);
}
const expiryTimestamp = leafNode.timeInForce
? leafNode.timestamp.add(new BN(leafNode.timeInForce))
: U64_MAX_BN;
return new PerpOrder(
type === BookSideType.bids
? RUST_U64_MAX().sub(leafNode.key.maskn(64))
: leafNode.key.maskn(64),
leafNode.key,
leafNode.owner,
leafNode.ownerSlot,
0,
perpMarket.priceLotsToUi(priceLots),
priceLots,
perpMarket.baseLotsToUi(leafNode.quantity),
leafNode.quantity,
side,
leafNode.timestamp,
expiryTimestamp,
perpMarket.perpMarketIndex,
isExpired,
isOraclePegged,
oraclePeggedProperties,
);
}
constructor(
public seqNum: BN,
public orderId: BN,
public owner: PublicKey,
public openOrdersSlot: number,
public feeTier: 0,
public uiPrice: number,
public priceLots: BN,
public uiSize: number,
public sizeLots: BN,
public side: PerpOrderSide,
public timestamp: BN,
public expiryTimestamp: BN,
public perpMarketIndex: number,
public isExpired = false,
public isOraclePegged = false,
public oraclePeggedProperties?: OraclePeggedProperties,
) {}
get price(): number {
return this.uiPrice;
}
get size(): number {
return this.uiSize;
}
}
interface OraclePeggedProperties {
isInvalid: boolean;
priceOffset: BN;
uiPriceOffset: number;
pegLimit: BN;
uiPegLimit: number;
}
export class PerpEventQueue {
static FILL_EVENT_TYPE = 0;
static OUT_EVENT_TYPE = 1;
static LIQUIDATE_EVENT_TYPE = 2;
public head: number;
public count: number;
public seqNum: BN;
public rawEvents: (OutEvent | FillEvent | LiquidateEvent)[];
constructor(
client: MangoClient,
header: { head: number; count: number; seqNum: BN },
buf,
) {
this.head = header.head;
this.count = header.count;
this.seqNum = header.seqNum;
this.rawEvents = buf.map((event) => {
if (event.eventType === PerpEventQueue.FILL_EVENT_TYPE) {
return (client.program as any)._coder.types.typeLayouts
.get('FillEvent')
.decode(
Buffer.from([PerpEventQueue.FILL_EVENT_TYPE].concat(event.padding)),
);
} else if (event.eventType === PerpEventQueue.OUT_EVENT_TYPE) {
return (client.program as any)._coder.types.typeLayouts
.get('OutEvent')
.decode(
Buffer.from([PerpEventQueue.OUT_EVENT_TYPE].concat(event.padding)),
);
} else if (event.eventType === PerpEventQueue.LIQUIDATE_EVENT_TYPE) {
return (client.program as any)._coder.types.typeLayouts
.get('LiquidateEvent')
.decode(
Buffer.from(
[PerpEventQueue.LIQUIDATE_EVENT_TYPE].concat(event.padding),
),
);
}
throw new Error(`Unknown event with eventType ${event.eventType}!`);
});
}
public getUnconsumedEvents(): (OutEvent | FillEvent | LiquidateEvent)[] {
const events: (OutEvent | FillEvent | LiquidateEvent)[] = [];
const head = this.head;
for (let i = 0; i < this.count; i++) {
events.push(this.rawEvents[(head + i) % this.rawEvents.length]);
}
return events;
}
public eventsSince(
lastSeqNum?: BN,
): (OutEvent | FillEvent | LiquidateEvent)[] {
return this.rawEvents
.filter((e) =>
e.seqNum.gt(lastSeqNum === undefined ? new BN(0) : lastSeqNum),
)
.sort((a, b) => a.seqNum.cmp(b.seqNum));
}
}
export interface Event {
eventType: number;
}
export interface OutEvent extends Event {
side: PerpOrderType;
ownerSlot: number;
timestamp: BN;
seqNum: BN;
owner: PublicKey;
quantity: BN;
}
export interface FillEvent extends Event {
takerSide: PerpOrderType;
makerOut: boolean;
makerSlot: number;
timestamp: BN;
seqNum: BN;
maker: PublicKey;
makerOrderId: BN;
makerFee: number;
makerTimestamp: BN;
taker: PublicKey;
takerOrderId: BN;
takerClientOrderId: BN;
takerFee: number;
price: BN;
quantity: BN;
}
export interface LiquidateEvent extends Event {
seqNum: BN;
}