2022-05-11 04:33:01 -07:00
|
|
|
import { BN } from '@project-serum/anchor';
|
|
|
|
import { utf8 } from '@project-serum/anchor/dist/cjs/utils/bytes';
|
|
|
|
import { PublicKey } from '@solana/web3.js';
|
2022-09-20 03:57:01 -07:00
|
|
|
import Big from 'big.js';
|
|
|
|
import { MangoClient } from '../client';
|
2022-09-30 06:07:43 -07:00
|
|
|
import { I80F48, I80F48Dto } from '../numbers/I80F48';
|
|
|
|
import { As, toNative, U64_MAX_BN } from '../utils';
|
2022-10-11 00:34:02 -07:00
|
|
|
import { OracleConfig, QUOTE_DECIMALS, TokenIndex } from './bank';
|
2022-05-11 04:33:01 -07:00
|
|
|
|
2022-09-29 06:51:09 -07:00
|
|
|
export type PerpMarketIndex = number & As<'perp-market-index'>;
|
|
|
|
|
2022-05-11 04:33:01 -07:00
|
|
|
export class PerpMarket {
|
|
|
|
public name: string;
|
|
|
|
public maintAssetWeight: I80F48;
|
|
|
|
public initAssetWeight: I80F48;
|
|
|
|
public maintLiabWeight: I80F48;
|
|
|
|
public initLiabWeight: I80F48;
|
|
|
|
public liquidationFee: I80F48;
|
|
|
|
public makerFee: I80F48;
|
|
|
|
public takerFee: I80F48;
|
2022-09-20 03:57:01 -07:00
|
|
|
public minFunding: I80F48;
|
|
|
|
public maxFunding: I80F48;
|
2022-09-29 06:51:09 -07:00
|
|
|
public longFunding: I80F48;
|
|
|
|
public shortFunding: I80F48;
|
2022-05-11 04:33:01 -07:00
|
|
|
public feesAccrued: I80F48;
|
2022-10-11 00:34:02 -07:00
|
|
|
public feesSettled: I80F48;
|
|
|
|
|
2022-09-29 06:51:09 -07:00
|
|
|
public _price: I80F48;
|
|
|
|
public _uiPrice: number;
|
2022-05-11 04:33:01 -07:00
|
|
|
|
2022-10-11 00:34:02 -07:00
|
|
|
private priceLotsToUiConverter: number;
|
|
|
|
private baseLotsToUiConverter: number;
|
|
|
|
private quoteLotsToUiConverter: number;
|
|
|
|
|
2022-05-11 04:33:01 -07:00
|
|
|
static from(
|
|
|
|
publicKey: PublicKey,
|
|
|
|
obj: {
|
|
|
|
group: PublicKey;
|
2022-10-11 00:34:02 -07:00
|
|
|
settleTokenIndex: number;
|
2022-08-04 01:41:54 -07:00
|
|
|
perpMarketIndex: number;
|
2022-09-29 06:51:09 -07:00
|
|
|
trustedMarket: number;
|
2022-10-11 00:34:02 -07:00
|
|
|
groupInsuranceFund: number;
|
2022-08-04 01:41:54 -07:00
|
|
|
name: number[];
|
2022-05-11 04:33:01 -07:00
|
|
|
oracle: PublicKey;
|
2022-08-04 01:41:54 -07:00
|
|
|
oracleConfig: OracleConfig;
|
2022-05-11 04:33:01 -07:00
|
|
|
bids: PublicKey;
|
|
|
|
asks: PublicKey;
|
|
|
|
eventQueue: PublicKey;
|
|
|
|
quoteLotSize: BN;
|
|
|
|
baseLotSize: BN;
|
|
|
|
maintAssetWeight: I80F48Dto;
|
|
|
|
initAssetWeight: I80F48Dto;
|
|
|
|
maintLiabWeight: I80F48Dto;
|
|
|
|
initLiabWeight: I80F48Dto;
|
|
|
|
liquidationFee: I80F48Dto;
|
|
|
|
makerFee: I80F48Dto;
|
|
|
|
takerFee: I80F48Dto;
|
2022-08-04 01:41:54 -07:00
|
|
|
minFunding: I80F48Dto;
|
|
|
|
maxFunding: I80F48Dto;
|
|
|
|
impactQuantity: BN;
|
|
|
|
longFunding: I80F48Dto;
|
|
|
|
shortFunding: I80F48Dto;
|
|
|
|
fundingLastUpdated: BN;
|
2022-05-11 04:33:01 -07:00
|
|
|
openInterest: BN;
|
2022-09-29 06:51:09 -07:00
|
|
|
seqNum: BN;
|
2022-05-11 04:33:01 -07:00
|
|
|
feesAccrued: I80F48Dto;
|
2022-09-21 00:42:45 -07:00
|
|
|
baseDecimals: number;
|
2022-08-01 09:46:45 -07:00
|
|
|
registrationTime: BN;
|
2022-10-11 00:34:02 -07:00
|
|
|
feesSettled: I80F48Dto;
|
|
|
|
feePenalty: number;
|
|
|
|
settleFeeFlat: number;
|
|
|
|
settleFeeAmountThreshold: number;
|
|
|
|
settleFeeFractionLowHealth: number;
|
2022-05-11 04:33:01 -07:00
|
|
|
},
|
|
|
|
): PerpMarket {
|
|
|
|
return new PerpMarket(
|
|
|
|
publicKey,
|
|
|
|
obj.group,
|
2022-10-11 00:34:02 -07:00
|
|
|
obj.settleTokenIndex as TokenIndex,
|
2022-09-29 06:51:09 -07:00
|
|
|
obj.perpMarketIndex as PerpMarketIndex,
|
|
|
|
obj.trustedMarket == 1,
|
2022-10-11 00:34:02 -07:00
|
|
|
obj.groupInsuranceFund == 1,
|
2022-08-04 01:41:54 -07:00
|
|
|
obj.name,
|
2022-05-11 04:33:01 -07:00
|
|
|
obj.oracle,
|
2022-08-04 01:41:54 -07:00
|
|
|
obj.oracleConfig,
|
2022-05-11 04:33:01 -07:00
|
|
|
obj.bids,
|
|
|
|
obj.asks,
|
|
|
|
obj.eventQueue,
|
|
|
|
obj.quoteLotSize,
|
|
|
|
obj.baseLotSize,
|
|
|
|
obj.maintAssetWeight,
|
|
|
|
obj.initAssetWeight,
|
|
|
|
obj.maintLiabWeight,
|
|
|
|
obj.initLiabWeight,
|
|
|
|
obj.liquidationFee,
|
|
|
|
obj.makerFee,
|
|
|
|
obj.takerFee,
|
2022-08-04 01:41:54 -07:00
|
|
|
obj.minFunding,
|
|
|
|
obj.maxFunding,
|
|
|
|
obj.impactQuantity,
|
|
|
|
obj.longFunding,
|
|
|
|
obj.shortFunding,
|
|
|
|
obj.fundingLastUpdated,
|
2022-05-11 04:33:01 -07:00
|
|
|
obj.openInterest,
|
|
|
|
obj.seqNum,
|
|
|
|
obj.feesAccrued,
|
2022-09-21 00:42:45 -07:00
|
|
|
obj.baseDecimals,
|
2022-08-01 09:46:45 -07:00
|
|
|
obj.registrationTime,
|
2022-10-11 00:34:02 -07:00
|
|
|
obj.feesSettled,
|
|
|
|
obj.feePenalty,
|
|
|
|
obj.settleFeeFlat,
|
|
|
|
obj.settleFeeAmountThreshold,
|
|
|
|
obj.settleFeeFractionLowHealth,
|
2022-05-11 04:33:01 -07:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
public publicKey: PublicKey,
|
|
|
|
public group: PublicKey,
|
2022-10-11 00:34:02 -07:00
|
|
|
public settleTokenIndex: TokenIndex,
|
2022-09-29 06:51:09 -07:00
|
|
|
public perpMarketIndex: PerpMarketIndex, // TODO rename to marketIndex?
|
|
|
|
public trustedMarket: boolean,
|
2022-10-11 00:34:02 -07:00
|
|
|
public groupInsuranceFund: boolean,
|
2022-08-04 01:41:54 -07:00
|
|
|
name: number[],
|
2022-05-11 04:33:01 -07:00
|
|
|
public oracle: PublicKey,
|
2022-08-04 01:41:54 -07:00
|
|
|
oracleConfig: OracleConfig,
|
2022-05-11 04:33:01 -07:00
|
|
|
public bids: PublicKey,
|
|
|
|
public asks: PublicKey,
|
|
|
|
public eventQueue: PublicKey,
|
2022-06-02 10:30:39 -07:00
|
|
|
public quoteLotSize: BN,
|
|
|
|
public baseLotSize: BN,
|
2022-05-11 04:33:01 -07:00
|
|
|
maintAssetWeight: I80F48Dto,
|
|
|
|
initAssetWeight: I80F48Dto,
|
|
|
|
maintLiabWeight: I80F48Dto,
|
|
|
|
initLiabWeight: I80F48Dto,
|
|
|
|
liquidationFee: I80F48Dto,
|
|
|
|
makerFee: I80F48Dto,
|
|
|
|
takerFee: I80F48Dto,
|
2022-08-04 01:41:54 -07:00
|
|
|
minFunding: I80F48Dto,
|
2022-09-20 03:57:01 -07:00
|
|
|
maxFunding: I80F48Dto,
|
|
|
|
public impactQuantity: BN,
|
2022-08-04 01:41:54 -07:00
|
|
|
longFunding: I80F48Dto,
|
|
|
|
shortFunding: I80F48Dto,
|
2022-10-11 00:34:02 -07:00
|
|
|
public fundingLastUpdated: BN,
|
|
|
|
public openInterest: BN,
|
|
|
|
public seqNum: BN,
|
2022-05-11 04:33:01 -07:00
|
|
|
feesAccrued: I80F48Dto,
|
2022-09-21 00:42:45 -07:00
|
|
|
public baseDecimals: number,
|
2022-08-01 09:46:45 -07:00
|
|
|
public registrationTime: BN,
|
2022-10-11 00:34:02 -07:00
|
|
|
feesSettled: I80F48Dto,
|
|
|
|
public feePenalty: number,
|
|
|
|
public settleFeeFlat: number,
|
|
|
|
public settleFeeAmountThreshold: number,
|
|
|
|
public settleFeeFractionLowHealth: number,
|
2022-05-11 04:33:01 -07:00
|
|
|
) {
|
|
|
|
this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0];
|
|
|
|
this.maintAssetWeight = I80F48.from(maintAssetWeight);
|
|
|
|
this.initAssetWeight = I80F48.from(initAssetWeight);
|
|
|
|
this.maintLiabWeight = I80F48.from(maintLiabWeight);
|
|
|
|
this.initLiabWeight = I80F48.from(initLiabWeight);
|
|
|
|
this.liquidationFee = I80F48.from(liquidationFee);
|
|
|
|
this.makerFee = I80F48.from(makerFee);
|
|
|
|
this.takerFee = I80F48.from(takerFee);
|
2022-09-20 03:57:01 -07:00
|
|
|
this.minFunding = I80F48.from(minFunding);
|
|
|
|
this.maxFunding = I80F48.from(maxFunding);
|
2022-09-29 06:51:09 -07:00
|
|
|
this.longFunding = I80F48.from(longFunding);
|
|
|
|
this.shortFunding = I80F48.from(shortFunding);
|
2022-05-11 04:33:01 -07:00
|
|
|
this.feesAccrued = I80F48.from(feesAccrued);
|
2022-10-11 00:34:02 -07:00
|
|
|
this.feesSettled = I80F48.from(feesSettled);
|
2022-09-20 03:57:01 -07:00
|
|
|
|
|
|
|
this.priceLotsToUiConverter = new Big(10)
|
2022-09-21 01:19:23 -07:00
|
|
|
.pow(baseDecimals - QUOTE_DECIMALS)
|
2022-09-20 03:57:01 -07:00
|
|
|
.mul(new Big(this.quoteLotSize.toString()))
|
|
|
|
.div(new Big(this.baseLotSize.toString()))
|
|
|
|
.toNumber();
|
|
|
|
|
|
|
|
this.baseLotsToUiConverter = new Big(this.baseLotSize.toString())
|
2022-09-21 01:19:23 -07:00
|
|
|
.div(new Big(10).pow(baseDecimals))
|
2022-09-20 03:57:01 -07:00
|
|
|
.toNumber();
|
|
|
|
|
|
|
|
this.quoteLotsToUiConverter = new Big(this.quoteLotSize.toString())
|
|
|
|
.div(new Big(10).pow(QUOTE_DECIMALS))
|
|
|
|
.toNumber();
|
|
|
|
}
|
|
|
|
|
2022-09-29 06:51:09 -07:00
|
|
|
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;
|
|
|
|
}
|
2022-10-10 15:59:44 -07:00
|
|
|
|
|
|
|
get minOrderSize(): number {
|
|
|
|
return this.baseLotsToUiConverter;
|
|
|
|
}
|
|
|
|
|
|
|
|
get tickSize(): number {
|
|
|
|
return this.priceLotsToUiConverter;
|
|
|
|
}
|
|
|
|
|
2022-09-20 03:57:01 -07:00
|
|
|
public async loadAsks(client: MangoClient): Promise<BookSide> {
|
|
|
|
const asks = await client.program.account.bookSide.fetch(this.asks);
|
|
|
|
return BookSide.from(client, this, BookSideType.asks, asks);
|
|
|
|
}
|
|
|
|
|
|
|
|
public async loadBids(client: MangoClient): Promise<BookSide> {
|
|
|
|
const bids = await client.program.account.bookSide.fetch(this.bids);
|
|
|
|
return BookSide.from(client, this, BookSideType.bids, 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);
|
|
|
|
}
|
|
|
|
|
2022-09-29 06:51:09 -07:00
|
|
|
public async loadFills(
|
|
|
|
client: MangoClient,
|
|
|
|
lastSeqNum: BN,
|
|
|
|
): Promise<(OutEvent | FillEvent | LiquidateEvent)[]> {
|
2022-09-20 03:57:01 -07:00
|
|
|
const eventQueue = await this.loadEventQueue(client);
|
|
|
|
return eventQueue
|
|
|
|
.eventsSince(lastSeqNum)
|
|
|
|
.filter((event) => event.eventType == PerpEventQueue.FILL_EVENT_TYPE);
|
2022-05-11 04:33:01 -07:00
|
|
|
}
|
2022-06-02 10:30:39 -07:00
|
|
|
|
2022-10-07 04:52:04 -07:00
|
|
|
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)}`;
|
|
|
|
}
|
|
|
|
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)}`;
|
|
|
|
}
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
2022-09-20 03:57:01 -07:00
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @param bids
|
|
|
|
* @param asks
|
|
|
|
* @returns returns funding rate per hour
|
|
|
|
*/
|
2022-09-29 06:51:09 -07:00
|
|
|
public getCurrentFundingRate(bids: BookSide, asks: BookSide): number {
|
2022-09-20 03:57:01 -07:00
|
|
|
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));
|
2022-09-29 06:51:09 -07:00
|
|
|
const indexPrice = this._uiPrice;
|
2022-09-20 03:57:01 -07:00
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
public uiPriceToLots(price: number): BN {
|
2022-09-30 06:07:43 -07:00
|
|
|
return toNative(price, QUOTE_DECIMALS)
|
2022-06-02 10:30:39 -07:00
|
|
|
.mul(this.baseLotSize)
|
2022-09-21 01:19:23 -07:00
|
|
|
.div(this.quoteLotSize.mul(new BN(Math.pow(10, this.baseDecimals))));
|
2022-06-02 10:30:39 -07:00
|
|
|
}
|
|
|
|
|
2022-09-20 03:57:01 -07:00
|
|
|
public uiBaseToLots(quantity: number): BN {
|
2022-09-30 06:07:43 -07:00
|
|
|
return toNative(quantity, this.baseDecimals).div(this.baseLotSize);
|
2022-09-20 03:57:01 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
public uiQuoteToLots(uiQuote: number): BN {
|
2022-09-30 06:07:43 -07:00
|
|
|
return toNative(uiQuote, QUOTE_DECIMALS).div(this.quoteLotSize);
|
2022-09-20 03:57:01 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
public priceLotsToUi(price: BN): number {
|
|
|
|
return parseFloat(price.toString()) * this.priceLotsToUiConverter;
|
|
|
|
}
|
|
|
|
|
|
|
|
public baseLotsToUi(quantity: BN): number {
|
|
|
|
return parseFloat(quantity.toString()) * this.baseLotsToUiConverter;
|
|
|
|
}
|
|
|
|
|
|
|
|
public quoteLotsToUi(quantity: BN): number {
|
|
|
|
return parseFloat(quantity.toString()) * this.quoteLotsToUiConverter;
|
2022-06-02 10:30:39 -07:00
|
|
|
}
|
2022-07-05 10:31:47 -07:00
|
|
|
|
|
|
|
toString(): string {
|
|
|
|
return (
|
|
|
|
'PerpMarket ' +
|
|
|
|
'\n perpMarketIndex -' +
|
|
|
|
this.perpMarketIndex +
|
|
|
|
'\n maintAssetWeight -' +
|
2022-09-30 06:07:43 -07:00
|
|
|
this.maintAssetWeight.toString() +
|
2022-07-05 10:31:47 -07:00
|
|
|
'\n initAssetWeight -' +
|
2022-09-30 06:07:43 -07:00
|
|
|
this.initAssetWeight.toString() +
|
2022-07-05 10:31:47 -07:00
|
|
|
'\n maintLiabWeight -' +
|
2022-09-30 06:07:43 -07:00
|
|
|
this.maintLiabWeight.toString() +
|
2022-07-05 10:31:47 -07:00
|
|
|
'\n initLiabWeight -' +
|
2022-09-30 06:07:43 -07:00
|
|
|
this.initLiabWeight.toString() +
|
2022-07-05 10:31:47 -07:00
|
|
|
'\n liquidationFee -' +
|
2022-09-30 06:07:43 -07:00
|
|
|
this.liquidationFee.toString() +
|
2022-07-05 10:31:47 -07:00
|
|
|
'\n makerFee -' +
|
2022-09-30 06:07:43 -07:00
|
|
|
this.makerFee.toString() +
|
2022-07-05 10:31:47 -07:00
|
|
|
'\n takerFee -' +
|
2022-09-30 06:07:43 -07:00
|
|
|
this.takerFee.toString()
|
2022-07-05 10:31:47 -07:00
|
|
|
);
|
|
|
|
}
|
2022-05-11 04:33:01 -07:00
|
|
|
}
|
|
|
|
|
2022-09-20 03:57:01 -07:00
|
|
|
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: {
|
|
|
|
bumpIndex: number;
|
|
|
|
freeListLen: number;
|
|
|
|
freeListHead: number;
|
|
|
|
rootNode: number;
|
|
|
|
leafCount: number;
|
|
|
|
nodes: unknown;
|
|
|
|
},
|
2022-09-29 06:51:09 -07:00
|
|
|
): BookSide {
|
2022-09-20 03:57:01 -07:00
|
|
|
return new BookSide(
|
|
|
|
client,
|
|
|
|
perpMarket,
|
|
|
|
bookSideType,
|
|
|
|
obj.bumpIndex,
|
|
|
|
obj.freeListLen,
|
|
|
|
obj.freeListHead,
|
|
|
|
obj.rootNode,
|
|
|
|
obj.leafCount,
|
|
|
|
obj.nodes,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
public client: MangoClient,
|
|
|
|
public perpMarket: PerpMarket,
|
|
|
|
public type: BookSideType,
|
|
|
|
public bumpIndex,
|
|
|
|
public freeListLen,
|
|
|
|
public freeListHead,
|
|
|
|
public rootNode,
|
|
|
|
public leafCount,
|
|
|
|
public nodes,
|
|
|
|
public includeExpired = false,
|
|
|
|
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.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;
|
|
|
|
}
|
|
|
|
|
2022-09-29 06:51:09 -07:00
|
|
|
static getPriceFromKey(key: BN): BN {
|
2022-09-20 03:57:01 -07:00
|
|
|
return key.ushrn(64);
|
|
|
|
}
|
|
|
|
|
|
|
|
public *items(): Generator<PerpOrder> {
|
|
|
|
if (this.leafCount === 0) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const now = this.now;
|
|
|
|
const stack = [this.rootNode];
|
|
|
|
const [left, right] = this.type === BookSideType.bids ? [1, 0] : [0, 1];
|
|
|
|
|
|
|
|
while (stack.length > 0) {
|
|
|
|
const index = stack.pop();
|
|
|
|
const node = this.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;
|
|
|
|
if (now.lt(expiryTimestamp) || this.includeExpired) {
|
|
|
|
yield PerpOrder.from(this.perpMarket, leafNode, this.type);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-07 04:52:04 -07:00
|
|
|
public best(): PerpOrder | undefined {
|
|
|
|
return this.items().next().value;
|
|
|
|
}
|
|
|
|
|
2022-09-20 03:57:01 -07:00
|
|
|
getImpactPriceUi(baseLots: BN): number | undefined {
|
|
|
|
const s = new BN(0);
|
|
|
|
for (const order of this.items()) {
|
|
|
|
s.iadd(order.sizeLots);
|
|
|
|
if (s.gte(baseLots)) {
|
2022-09-23 02:43:26 -07:00
|
|
|
return order.uiPrice;
|
2022-09-20 03:57:01 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
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][] = [];
|
2022-09-23 02:43:26 -07:00
|
|
|
for (const { uiPrice: price, uiSize: size } of this.items()) {
|
2022-09-20 03:57:01 -07:00
|
|
|
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, 0, 0, 0].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, 0, 0, 0].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;
|
|
|
|
clientOrderId: BN;
|
|
|
|
timestamp: BN;
|
|
|
|
}): LeafNode {
|
|
|
|
return new LeafNode(
|
|
|
|
obj.ownerSlot,
|
|
|
|
obj.orderType,
|
|
|
|
obj.timeInForce,
|
|
|
|
obj.key,
|
|
|
|
obj.owner,
|
|
|
|
obj.quantity,
|
|
|
|
obj.clientOrderId,
|
|
|
|
obj.timestamp,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
public ownerSlot: number,
|
|
|
|
public orderType: PerpOrderType,
|
|
|
|
public timeInForce: number,
|
|
|
|
public key: BN,
|
|
|
|
public owner: PublicKey,
|
|
|
|
public quantity: BN,
|
|
|
|
public clientOrderId: BN,
|
|
|
|
public timestamp: BN,
|
|
|
|
) {}
|
|
|
|
}
|
|
|
|
export class InnerNode {
|
2022-09-29 06:51:09 -07:00
|
|
|
static from(obj: { children: [number] }): InnerNode {
|
2022-09-20 03:57:01 -07:00
|
|
|
return new InnerNode(obj.children);
|
|
|
|
}
|
|
|
|
|
|
|
|
constructor(public children: [number]) {}
|
|
|
|
}
|
|
|
|
|
2022-09-23 02:43:26 -07:00
|
|
|
export class PerpOrderSide {
|
2022-05-11 04:33:01 -07:00
|
|
|
static bid = { bid: {} };
|
|
|
|
static ask = { ask: {} };
|
|
|
|
}
|
|
|
|
|
2022-09-20 03:57:01 -07:00
|
|
|
export class PerpOrderType {
|
2022-05-11 04:33:01 -07:00
|
|
|
static limit = { limit: {} };
|
2022-10-07 04:52:04 -07:00
|
|
|
static immediateOrCancel = { immediateOrCancel: {} };
|
|
|
|
static postOnly = { postOnly: {} };
|
2022-05-11 04:33:01 -07:00
|
|
|
static market = { market: {} };
|
2022-10-07 04:52:04 -07:00
|
|
|
static postOnlySlide = { postOnlySlide: {} };
|
2022-05-11 04:33:01 -07:00
|
|
|
}
|
2022-09-20 03:57:01 -07:00
|
|
|
|
|
|
|
export class PerpOrder {
|
2022-09-29 06:51:09 -07:00
|
|
|
static from(
|
|
|
|
perpMarket: PerpMarket,
|
|
|
|
leafNode: LeafNode,
|
|
|
|
type: BookSideType,
|
|
|
|
): PerpOrder {
|
2022-09-23 02:43:26 -07:00
|
|
|
const side =
|
|
|
|
type == BookSideType.bids ? PerpOrderSide.bid : PerpOrderSide.ask;
|
2022-09-20 03:57:01 -07:00
|
|
|
const price = BookSide.getPriceFromKey(leafNode.key);
|
|
|
|
const expiryTimestamp = leafNode.timeInForce
|
|
|
|
? leafNode.timestamp.add(new BN(leafNode.timeInForce))
|
|
|
|
: U64_MAX_BN;
|
|
|
|
|
|
|
|
return new PerpOrder(
|
|
|
|
leafNode.key,
|
|
|
|
leafNode.clientOrderId,
|
|
|
|
leafNode.owner,
|
|
|
|
leafNode.ownerSlot,
|
|
|
|
0,
|
|
|
|
perpMarket.priceLotsToUi(price),
|
|
|
|
price,
|
|
|
|
perpMarket.baseLotsToUi(leafNode.quantity),
|
|
|
|
leafNode.quantity,
|
|
|
|
side,
|
|
|
|
leafNode.timestamp,
|
|
|
|
expiryTimestamp,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
public orderId: BN,
|
|
|
|
public clientId: BN,
|
|
|
|
public owner: PublicKey,
|
|
|
|
public openOrdersSlot: number,
|
|
|
|
public feeTier: 0,
|
2022-09-23 02:43:26 -07:00
|
|
|
public uiPrice: number,
|
2022-09-20 03:57:01 -07:00
|
|
|
public priceLots: BN,
|
2022-09-23 02:43:26 -07:00
|
|
|
public uiSize: number,
|
2022-09-20 03:57:01 -07:00
|
|
|
public sizeLots: BN,
|
2022-09-23 02:43:26 -07:00
|
|
|
public side: PerpOrderSide,
|
2022-09-20 03:57:01 -07:00
|
|
|
public timestamp: BN,
|
|
|
|
public expiryTimestamp: BN,
|
|
|
|
) {}
|
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
2022-09-26 06:58:26 -07:00
|
|
|
this.rawEvents = buf.map((event) => {
|
2022-09-20 03:57:01 -07:00
|
|
|
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),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
2022-09-29 06:51:09 -07:00
|
|
|
throw new Error(`Unknown event with eventType ${event.eventType}!`);
|
2022-09-20 03:57:01 -07:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
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));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-20 10:50:21 -07:00
|
|
|
export interface Event {
|
2022-09-20 03:57:01 -07:00
|
|
|
eventType: number;
|
|
|
|
}
|
|
|
|
|
2022-09-20 10:50:21 -07:00
|
|
|
export interface OutEvent extends Event {
|
2022-09-20 03:57:01 -07:00
|
|
|
side: PerpOrderType;
|
|
|
|
ownerSlot: number;
|
|
|
|
timestamp: BN;
|
|
|
|
seqNum: BN;
|
|
|
|
owner: PublicKey;
|
|
|
|
quantity: BN;
|
|
|
|
}
|
|
|
|
|
2022-09-20 10:50:21 -07:00
|
|
|
export interface FillEvent extends Event {
|
2022-09-20 03:57:01 -07:00
|
|
|
takerSide: PerpOrderType;
|
|
|
|
makerOut: boolean;
|
|
|
|
makerSlot: number;
|
|
|
|
marketFeesApplied: boolean;
|
|
|
|
timestamp: BN;
|
|
|
|
seqNum: BN;
|
|
|
|
maker: PublicKey;
|
|
|
|
makerOrderId: BN;
|
|
|
|
makerClientOrderId: BN;
|
|
|
|
makerFee: I80F48;
|
|
|
|
makerTimestamp: BN;
|
|
|
|
taker: PublicKey;
|
|
|
|
takerOrderId: BN;
|
|
|
|
takerClientOrderId: BN;
|
|
|
|
takerFee: I80F48;
|
|
|
|
price: BN;
|
|
|
|
quantity: BN;
|
|
|
|
}
|
|
|
|
|
2022-09-20 10:50:21 -07:00
|
|
|
export interface LiquidateEvent extends Event {
|
2022-09-20 03:57:01 -07:00
|
|
|
seqNum: BN;
|
|
|
|
}
|