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-11-21 11:36:13 -08:00
|
|
|
import { I80F48, I80F48Dto, ZERO_I80F48 } from '../numbers/I80F48';
|
2022-12-08 11:00:12 -08:00
|
|
|
import { Modify } from '../types';
|
2022-12-20 10:19:18 -08:00
|
|
|
import { As, U64_MAX_BN, toNative, toUiDecimals } from '../utils';
|
2022-12-02 06:48:43 -08:00
|
|
|
import {
|
|
|
|
OracleConfig,
|
2022-12-08 01:16:06 -08:00
|
|
|
OracleConfigDto,
|
2022-12-02 06:48:43 -08:00
|
|
|
QUOTE_DECIMALS,
|
|
|
|
StablePriceModel,
|
|
|
|
TokenIndex,
|
|
|
|
} from './bank';
|
2022-11-21 11:36:13 -08:00
|
|
|
import { Group } from './group';
|
|
|
|
import { MangoAccount } from './mangoAccount';
|
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-12-08 11:00:12 -08:00
|
|
|
export type ParsedFillEvent = Modify<
|
|
|
|
FillEvent,
|
|
|
|
{
|
|
|
|
price: number;
|
|
|
|
quantity: number;
|
|
|
|
}
|
|
|
|
>;
|
|
|
|
|
2022-05-11 04:33:01 -07:00
|
|
|
export class PerpMarket {
|
|
|
|
public name: string;
|
2022-12-08 01:16:06 -08:00
|
|
|
public oracleConfig: OracleConfig;
|
2022-05-11 04:33:01 -07:00
|
|
|
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-12-07 12:03:28 -08:00
|
|
|
private _bids: BookSide;
|
|
|
|
private _asks: BookSide;
|
2022-11-21 10:34:41 -08:00
|
|
|
|
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-12-06 05:05:12 -08:00
|
|
|
baseDecimals: number;
|
2022-08-04 01:41:54 -07:00
|
|
|
name: number[];
|
2022-05-11 04:33:01 -07:00
|
|
|
bids: PublicKey;
|
|
|
|
asks: PublicKey;
|
|
|
|
eventQueue: PublicKey;
|
2022-12-06 05:05:12 -08:00
|
|
|
oracle: PublicKey;
|
2022-12-08 01:16:06 -08:00
|
|
|
oracleConfig: OracleConfigDto;
|
2022-12-06 05:05:12 -08:00
|
|
|
stablePriceModel: StablePriceModel;
|
2022-05-11 04:33:01 -07:00
|
|
|
quoteLotSize: BN;
|
|
|
|
baseLotSize: BN;
|
|
|
|
maintAssetWeight: I80F48Dto;
|
|
|
|
initAssetWeight: I80F48Dto;
|
|
|
|
maintLiabWeight: I80F48Dto;
|
|
|
|
initLiabWeight: I80F48Dto;
|
2022-12-06 05:05:12 -08:00
|
|
|
openInterest: BN;
|
|
|
|
seqNum: BN;
|
|
|
|
registrationTime: BN;
|
2022-08-04 01:41:54 -07:00
|
|
|
minFunding: I80F48Dto;
|
|
|
|
maxFunding: I80F48Dto;
|
|
|
|
impactQuantity: BN;
|
|
|
|
longFunding: I80F48Dto;
|
|
|
|
shortFunding: I80F48Dto;
|
|
|
|
fundingLastUpdated: BN;
|
2022-12-06 05:05:12 -08:00
|
|
|
liquidationFee: I80F48Dto;
|
|
|
|
makerFee: I80F48Dto;
|
|
|
|
takerFee: I80F48Dto;
|
2022-05-11 04:33:01 -07:00
|
|
|
feesAccrued: I80F48Dto;
|
2022-10-11 00:34:02 -07:00
|
|
|
feesSettled: I80F48Dto;
|
|
|
|
feePenalty: number;
|
|
|
|
settleFeeFlat: number;
|
|
|
|
settleFeeAmountThreshold: number;
|
|
|
|
settleFeeFractionLowHealth: number;
|
2022-12-06 05:05:12 -08:00
|
|
|
settlePnlLimitFactor: number;
|
|
|
|
settlePnlLimitWindowSizeTs: BN;
|
2023-01-04 04:14:05 -08:00
|
|
|
reduceOnly: 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-12-06 05:05:12 -08:00
|
|
|
obj.baseDecimals,
|
2022-08-04 01:41:54 -07:00
|
|
|
obj.name,
|
2022-05-11 04:33:01 -07:00
|
|
|
obj.bids,
|
|
|
|
obj.asks,
|
|
|
|
obj.eventQueue,
|
2022-12-06 05:05:12 -08:00
|
|
|
obj.oracle,
|
|
|
|
obj.oracleConfig,
|
|
|
|
obj.stablePriceModel,
|
2022-05-11 04:33:01 -07:00
|
|
|
obj.quoteLotSize,
|
|
|
|
obj.baseLotSize,
|
|
|
|
obj.maintAssetWeight,
|
|
|
|
obj.initAssetWeight,
|
|
|
|
obj.maintLiabWeight,
|
|
|
|
obj.initLiabWeight,
|
2022-12-06 05:05:12 -08:00
|
|
|
obj.openInterest,
|
|
|
|
obj.seqNum,
|
|
|
|
obj.registrationTime,
|
2022-08-04 01:41:54 -07:00
|
|
|
obj.minFunding,
|
|
|
|
obj.maxFunding,
|
|
|
|
obj.impactQuantity,
|
|
|
|
obj.longFunding,
|
|
|
|
obj.shortFunding,
|
|
|
|
obj.fundingLastUpdated,
|
2022-12-06 05:05:12 -08:00
|
|
|
obj.liquidationFee,
|
|
|
|
obj.makerFee,
|
|
|
|
obj.takerFee,
|
2022-05-11 04:33:01 -07:00
|
|
|
obj.feesAccrued,
|
2022-10-11 00:34:02 -07:00
|
|
|
obj.feesSettled,
|
|
|
|
obj.feePenalty,
|
|
|
|
obj.settleFeeFlat,
|
|
|
|
obj.settleFeeAmountThreshold,
|
|
|
|
obj.settleFeeFractionLowHealth,
|
2022-12-06 05:05:12 -08:00
|
|
|
obj.settlePnlLimitFactor,
|
|
|
|
obj.settlePnlLimitWindowSizeTs,
|
2023-01-04 04:14:05 -08:00
|
|
|
obj.reduceOnly == 1,
|
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-12-06 05:05:12 -08:00
|
|
|
public baseDecimals: number,
|
2022-08-04 01:41:54 -07:00
|
|
|
name: number[],
|
2022-05-11 04:33:01 -07:00
|
|
|
public bids: PublicKey,
|
|
|
|
public asks: PublicKey,
|
|
|
|
public eventQueue: PublicKey,
|
2022-12-06 05:05:12 -08:00
|
|
|
public oracle: PublicKey,
|
2022-12-08 01:16:06 -08:00
|
|
|
oracleConfig: OracleConfigDto,
|
2022-12-06 05:05:12 -08:00
|
|
|
public stablePriceModel: StablePriceModel,
|
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,
|
2022-12-06 05:05:12 -08:00
|
|
|
public openInterest: BN,
|
|
|
|
public seqNum: BN,
|
|
|
|
public registrationTime: BN,
|
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,
|
2022-12-06 05:05:12 -08:00
|
|
|
liquidationFee: I80F48Dto,
|
|
|
|
makerFee: I80F48Dto,
|
|
|
|
takerFee: I80F48Dto,
|
2022-05-11 04:33:01 -07:00
|
|
|
feesAccrued: I80F48Dto,
|
2022-10-11 00:34:02 -07:00
|
|
|
feesSettled: I80F48Dto,
|
|
|
|
public feePenalty: number,
|
|
|
|
public settleFeeFlat: number,
|
|
|
|
public settleFeeAmountThreshold: number,
|
|
|
|
public settleFeeFractionLowHealth: number,
|
2022-12-06 05:05:12 -08:00
|
|
|
settlePnlLimitFactor: number,
|
|
|
|
settlePnlLimitWindowSizeTs: BN,
|
2023-01-04 04:14:05 -08:00
|
|
|
public reduceOnly: boolean,
|
2022-05-11 04:33:01 -07:00
|
|
|
) {
|
|
|
|
this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0];
|
2022-12-08 01:16:06 -08:00
|
|
|
this.oracleConfig = {
|
|
|
|
confFilter: I80F48.from(oracleConfig.confFilter),
|
|
|
|
maxStalenessSlots: oracleConfig.maxStalenessSlots,
|
|
|
|
} as OracleConfig;
|
2022-05-11 04:33:01 -07:00
|
|
|
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-12-20 10:19:18 -08:00
|
|
|
insidePriceLimit(side: PerpOrderSide, orderPrice: number): boolean {
|
|
|
|
return (
|
|
|
|
(side === PerpOrderSide.bid &&
|
|
|
|
orderPrice <= this.maintLiabWeight.toNumber() * this.uiPrice) ||
|
|
|
|
(side === PerpOrderSide.ask &&
|
|
|
|
orderPrice >= this.maintAssetWeight.toNumber() * this.uiPrice)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-12-07 12:03:28 -08:00
|
|
|
public async loadAsks(
|
2022-11-21 10:34:41 -08:00
|
|
|
client: MangoClient,
|
|
|
|
forceReload = false,
|
2022-12-07 12:03:28 -08:00
|
|
|
): 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;
|
2022-09-20 03:57:01 -07:00
|
|
|
}
|
|
|
|
|
2022-12-07 12:03:28 -08:00
|
|
|
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;
|
2022-09-20 03:57:01 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
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,
|
2022-12-08 11:00:12 -08:00
|
|
|
lastSeqNum: BN = new BN(0),
|
2022-09-29 06:51:09 -07:00
|
|
|
): Promise<(OutEvent | FillEvent | LiquidateEvent)[]> {
|
2022-09-20 03:57:01 -07:00
|
|
|
const eventQueue = await this.loadEventQueue(client);
|
|
|
|
return eventQueue
|
|
|
|
.eventsSince(lastSeqNum)
|
2022-12-08 11:00:12 -08:00
|
|
|
.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,
|
|
|
|
};
|
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()) {
|
2022-12-09 11:23:30 -08:00
|
|
|
res += `\n ${order.uiPrice.toFixed(5).padStart(10)}, ${order.uiSize
|
|
|
|
.toString()
|
|
|
|
.padStart(10)} ${
|
2022-11-21 10:34:41 -08:00
|
|
|
order.isOraclePegged && order.oraclePeggedProperties
|
|
|
|
? order.oraclePeggedProperties.pegLimit.toNumber() + ' (PegLimit)'
|
|
|
|
: ''
|
|
|
|
}`;
|
2022-10-07 04:52:04 -07:00
|
|
|
}
|
|
|
|
res += `\n asks ↑ --------- ↓ bids`;
|
|
|
|
orders = await this?.loadBids(client);
|
|
|
|
for (const order of orders!.items()) {
|
2022-12-09 11:23:30 -08:00
|
|
|
res += `\n ${order.uiPrice.toFixed(5).padStart(10)}, ${order.uiSize
|
|
|
|
.toString()
|
|
|
|
.padStart(10)} ${
|
2022-11-21 10:34:41 -08:00
|
|
|
order.isOraclePegged && order.oraclePeggedProperties
|
|
|
|
? order.oraclePeggedProperties.pegLimit.toNumber() + ' (PegLimit)'
|
|
|
|
: ''
|
|
|
|
}`;
|
2022-10-07 04:52:04 -07:00
|
|
|
}
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
2022-09-20 03:57:01 -07:00
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @param bids
|
|
|
|
* @param asks
|
|
|
|
* @returns returns funding rate per hour
|
|
|
|
*/
|
2022-12-08 07:26:12 -08: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;
|
|
|
|
}
|
2022-12-06 13:41:42 -08:00
|
|
|
return funding / 24 / Math.pow(10, QUOTE_DECIMALS);
|
2022-09-20 03:57:01 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2022-12-19 04:20:23 -08:00
|
|
|
public priceNativeToUi(price: number): number {
|
|
|
|
return toUiDecimals(price, QUOTE_DECIMALS - this.baseDecimals);
|
|
|
|
}
|
|
|
|
|
2022-09-20 03:57:01 -07:00
|
|
|
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
|
|
|
|
2022-11-21 11:36:13 -08:00
|
|
|
/**
|
|
|
|
* 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);
|
|
|
|
}
|
|
|
|
|
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-12-07 12:03:28 -08:00
|
|
|
interface OrderTreeNodes {
|
|
|
|
bumpIndex: number;
|
|
|
|
freeListLen: number;
|
|
|
|
freeListHead: number;
|
|
|
|
nodes: [any];
|
2022-11-21 10:34:41 -08:00
|
|
|
}
|
|
|
|
|
2022-12-07 12:03:28 -08:00
|
|
|
interface OrderTreeRoot {
|
|
|
|
maybeNode: number;
|
|
|
|
leafCount: number;
|
2022-11-21 10:34:41 -08: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: {
|
2022-12-07 12:03:28 -08:00
|
|
|
roots: OrderTreeRoot[];
|
2022-12-09 11:23:30 -08:00
|
|
|
nodes: OrderTreeNodes;
|
2022-09-20 03:57:01 -07:00
|
|
|
},
|
2022-09-29 06:51:09 -07:00
|
|
|
): BookSide {
|
2022-09-20 03:57:01 -07:00
|
|
|
return new BookSide(
|
|
|
|
client,
|
|
|
|
perpMarket,
|
|
|
|
bookSideType,
|
2022-12-07 12:03:28 -08:00
|
|
|
obj.roots[0],
|
|
|
|
obj.roots[1],
|
2022-12-09 11:23:30 -08:00
|
|
|
obj.nodes,
|
2022-09-20 03:57:01 -07:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
public client: MangoClient,
|
|
|
|
public perpMarket: PerpMarket,
|
|
|
|
public type: BookSideType,
|
2022-12-07 12:03:28 -08:00
|
|
|
public rootFixed: OrderTreeRoot,
|
|
|
|
public rootOraclePegged: OrderTreeRoot,
|
2022-12-09 11:23:30 -08:00
|
|
|
public orderTreeNodes: OrderTreeNodes,
|
2022-09-20 03:57:01 -07:00
|
|
|
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);
|
2022-12-09 11:23:30 -08:00
|
|
|
for (const node of this.orderTreeNodes.nodes) {
|
2022-09-20 03:57:01 -07:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2022-11-21 10:34:41 -08:00
|
|
|
/**
|
|
|
|
* iterates over all orders
|
|
|
|
*/
|
2022-09-20 03:57:01 -07:00
|
|
|
public *items(): Generator<PerpOrder> {
|
2022-11-21 10:34:41 -08:00
|
|
|
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> {
|
2022-12-07 12:03:28 -08:00
|
|
|
if (this.rootFixed.leafCount === 0) {
|
2022-09-20 03:57:01 -07:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
const now = this.now;
|
2022-12-07 12:03:28 -08:00
|
|
|
const stack = [this.rootFixed.maybeNode];
|
2022-09-20 03:57:01 -07:00
|
|
|
const [left, right] = this.type === BookSideType.bids ? [1, 0] : [0, 1];
|
|
|
|
|
|
|
|
while (stack.length > 0) {
|
2022-11-21 10:34:41 -08:00
|
|
|
const index = stack.pop()!;
|
2022-12-09 11:23:30 -08:00
|
|
|
const node = this.orderTreeNodes.nodes[index];
|
2022-09-20 03:57:01 -07:00
|
|
|
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;
|
2022-11-21 10:34:41 -08:00
|
|
|
|
|
|
|
yield PerpOrder.from(
|
|
|
|
this.perpMarket,
|
|
|
|
leafNode,
|
|
|
|
this.type,
|
|
|
|
now.lt(expiryTimestamp),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public *oraclePeggedItems(): Generator<PerpOrder> {
|
2022-12-07 12:03:28 -08:00
|
|
|
if (this.rootOraclePegged.leafCount === 0) {
|
2022-11-21 10:34:41 -08:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
const now = this.now;
|
2022-12-07 12:03:28 -08:00
|
|
|
const stack = [this.rootOraclePegged.maybeNode];
|
2022-11-21 10:34:41 -08:00
|
|
|
const [left, right] = this.type === BookSideType.bids ? [1, 0] : [0, 1];
|
|
|
|
|
|
|
|
while (stack.length > 0) {
|
|
|
|
const index = stack.pop()!;
|
2022-12-09 11:23:30 -08:00
|
|
|
const node = this.orderTreeNodes.nodes[index];
|
2022-11-21 10:34:41 -08:00
|
|
|
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.lt(expiryTimestamp),
|
|
|
|
true,
|
|
|
|
);
|
2022-09-20 03:57:01 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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')
|
2022-12-08 01:16:06 -08:00
|
|
|
.decode(Buffer.from([BookSide.INNER_NODE_TAG].concat(data)));
|
2022-09-20 03:57:01 -07:00
|
|
|
}
|
|
|
|
static toLeafNode(client: MangoClient, data: [number]): LeafNode {
|
|
|
|
return LeafNode.from(
|
|
|
|
(client.program as any)._coder.types.typeLayouts
|
|
|
|
.get('LeafNode')
|
2022-12-08 01:16:06 -08:00
|
|
|
.decode(Buffer.from([BookSide.LEAF_NODE_TAG].concat(data))),
|
2022-09-20 03:57:01 -07:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
2022-11-21 10:34:41 -08:00
|
|
|
pegLimit: BN;
|
2022-09-20 03:57:01 -07:00
|
|
|
}): LeafNode {
|
|
|
|
return new LeafNode(
|
|
|
|
obj.ownerSlot,
|
|
|
|
obj.orderType,
|
|
|
|
obj.timeInForce,
|
|
|
|
obj.key,
|
|
|
|
obj.owner,
|
|
|
|
obj.quantity,
|
|
|
|
obj.timestamp,
|
2022-11-21 10:34:41 -08:00
|
|
|
obj.pegLimit,
|
2022-09-20 03:57:01 -07:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
public ownerSlot: number,
|
|
|
|
public orderType: PerpOrderType,
|
|
|
|
public timeInForce: number,
|
|
|
|
public key: BN,
|
|
|
|
public owner: PublicKey,
|
|
|
|
public quantity: BN,
|
|
|
|
public timestamp: BN,
|
2022-11-21 10:34:41 -08:00
|
|
|
public pegLimit: BN,
|
2022-09-20 03:57:01 -07:00
|
|
|
) {}
|
|
|
|
}
|
|
|
|
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,
|
2022-11-21 10:34:41 -08:00
|
|
|
isExpired = false,
|
|
|
|
isOraclePegged = false,
|
2022-09-29 06:51:09 -07:00
|
|
|
): PerpOrder {
|
2022-09-23 02:43:26 -07:00
|
|
|
const side =
|
|
|
|
type == BookSideType.bids ? PerpOrderSide.bid : PerpOrderSide.ask;
|
2022-11-21 10:34:41 -08:00
|
|
|
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);
|
|
|
|
}
|
2022-09-20 03:57:01 -07:00
|
|
|
const expiryTimestamp = leafNode.timeInForce
|
|
|
|
? leafNode.timestamp.add(new BN(leafNode.timeInForce))
|
|
|
|
: U64_MAX_BN;
|
|
|
|
|
|
|
|
return new PerpOrder(
|
2022-11-21 10:34:41 -08:00
|
|
|
type === BookSideType.bids
|
|
|
|
? new BN('18446744073709551615').sub(leafNode.key.maskn(64))
|
|
|
|
: leafNode.key.maskn(64),
|
2022-09-20 03:57:01 -07:00
|
|
|
leafNode.key,
|
|
|
|
leafNode.owner,
|
|
|
|
leafNode.ownerSlot,
|
|
|
|
0,
|
2022-11-21 10:34:41 -08:00
|
|
|
perpMarket.priceLotsToUi(priceLots),
|
|
|
|
priceLots,
|
2022-09-20 03:57:01 -07:00
|
|
|
perpMarket.baseLotsToUi(leafNode.quantity),
|
|
|
|
leafNode.quantity,
|
|
|
|
side,
|
|
|
|
leafNode.timestamp,
|
|
|
|
expiryTimestamp,
|
2022-10-31 10:39:03 -07:00
|
|
|
perpMarket.perpMarketIndex,
|
2022-11-21 10:34:41 -08:00
|
|
|
isExpired,
|
|
|
|
isOraclePegged,
|
|
|
|
oraclePeggedProperties,
|
2022-09-20 03:57:01 -07:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
constructor(
|
2022-11-21 10:34:41 -08:00
|
|
|
public seqNum: BN,
|
2022-09-20 03:57:01 -07:00
|
|
|
public orderId: 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,
|
2022-10-31 10:39:03 -07:00
|
|
|
public perpMarketIndex: number,
|
2022-11-21 10:34:41 -08:00
|
|
|
public isExpired = false,
|
|
|
|
public isOraclePegged = false,
|
|
|
|
public oraclePeggedProperties?: OraclePeggedProperties,
|
2022-09-20 03:57:01 -07:00
|
|
|
) {}
|
2022-10-31 10:07:58 -07:00
|
|
|
|
|
|
|
get price(): number {
|
|
|
|
return this.uiPrice;
|
|
|
|
}
|
|
|
|
|
|
|
|
get size(): number {
|
|
|
|
return this.uiSize;
|
|
|
|
}
|
2022-09-20 03:57:01 -07:00
|
|
|
}
|
|
|
|
|
2022-11-21 10:34:41 -08:00
|
|
|
interface OraclePeggedProperties {
|
|
|
|
isInvalid: boolean;
|
|
|
|
priceOffset: BN;
|
|
|
|
uiPriceOffset: number;
|
|
|
|
pegLimit: BN;
|
|
|
|
uiPegLimit: number;
|
|
|
|
}
|
|
|
|
|
2022-09-20 03:57:01 -07:00
|
|
|
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;
|
|
|
|
timestamp: BN;
|
|
|
|
seqNum: BN;
|
|
|
|
maker: PublicKey;
|
|
|
|
makerOrderId: 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;
|
|
|
|
}
|