WIP: ts/perps (#220)

* ts: further fleshing out of perps code

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* cleanup scripts

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* Fixes from reviews

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>
This commit is contained in:
microwavedcola1 2022-09-20 12:57:01 +02:00 committed by GitHub
parent 11160ae2fe
commit b7e79a4663
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 1422 additions and 174 deletions

View File

@ -26,7 +26,7 @@ pub enum NodeTag {
/// Each InnerNode has exactly two children, which are either InnerNodes themselves,
/// or LeafNodes. The children share the top `prefix_len` bits of `key`. The left
/// child has a 0 in the next bit, and the right a 1.
#[derive(Copy, Clone, Pod)]
#[derive(Copy, Clone, Pod, AnchorSerialize, AnchorDeserialize)]
#[repr(C)]
pub struct InnerNode {
pub tag: u32,
@ -46,8 +46,10 @@ pub struct InnerNode {
/// iterate through the whole bookside.
pub child_earliest_expiry: [u64; 2],
pub reserved: [u8; NODE_SIZE - 48],
pub reserved: [u8; 48],
}
const_assert_eq!(size_of::<InnerNode>() % 8, 0);
const_assert_eq!(size_of::<InnerNode>(), NODE_SIZE);
impl InnerNode {
pub fn new(prefix_len: u32, key: i128) -> Self {
@ -77,13 +79,15 @@ impl InnerNode {
}
/// LeafNodes represent an order in the binary tree
#[derive(Debug, Copy, Clone, PartialEq, Eq, Pod)]
#[derive(Debug, Copy, Clone, PartialEq, Eq, Pod, AnchorSerialize, AnchorDeserialize)]
#[repr(C)]
pub struct LeafNode {
pub tag: u32,
pub owner_slot: u8,
pub order_type: OrderType, // this was added for TradingView move order
pub padding: [u8; 1],
/// Time in seconds after `timestamp` at which the order expires.
/// A value of 0 means no expiry.
pub time_in_force: u8,
@ -98,8 +102,10 @@ pub struct LeafNode {
// The time the order was placed
pub timestamp: u64,
pub reserved: [u8; NODE_SIZE - 81],
pub reserved: [u8; 16],
}
const_assert_eq!(size_of::<LeafNode>() % 8, 0);
const_assert_eq!(size_of::<LeafNode>(), NODE_SIZE);
#[inline(always)]
fn key_to_price(key: i128) -> i64 {
@ -122,13 +128,14 @@ impl LeafNode {
tag: NodeTag::LeafNode.into(),
owner_slot,
order_type,
padding: [0],
time_in_force,
key,
owner,
quantity,
client_order_id,
timestamp,
reserved: [0; NODE_SIZE - 81],
reserved: [0; 16],
}
}

View File

@ -174,7 +174,7 @@ pub enum EventType {
Liquidate,
}
#[derive(Copy, Clone, Debug, Pod)]
#[derive(Copy, Clone, Debug, Pod, AnchorSerialize, AnchorDeserialize)]
#[repr(C)]
pub struct FillEvent {
pub event_type: u8,
@ -203,6 +203,7 @@ pub struct FillEvent {
pub quantity: i64, // number of quote lots
pub reserved: [u8; 16],
}
const_assert_eq!(size_of::<FillEvent>() % 8, 0);
const_assert_eq!(size_of::<FillEvent>(), EVENT_SIZE);
impl FillEvent {
@ -264,7 +265,7 @@ impl FillEvent {
}
}
#[derive(Copy, Clone, Debug, Pod)]
#[derive(Copy, Clone, Debug, Pod, AnchorSerialize, AnchorDeserialize)]
#[repr(C)]
pub struct OutEvent {
pub event_type: u8,
@ -275,8 +276,9 @@ pub struct OutEvent {
pub seq_num: u64,
pub owner: Pubkey,
pub quantity: i64,
padding1: [u8; EVENT_SIZE - 64],
padding1: [u8; 144],
}
const_assert_eq!(size_of::<OutEvent>() % 8, 0);
const_assert_eq!(size_of::<OutEvent>(), EVENT_SIZE);
impl OutEvent {

View File

@ -7,7 +7,7 @@ import {
Orderbook,
} from '@project-serum/serum';
import { parsePriceData, PriceData } from '@pythnetwork/client';
import { PublicKey } from '@solana/web3.js';
import { AccountInfo, PublicKey } from '@solana/web3.js';
import BN from 'bn.js';
import { MangoClient } from '../client';
import { SERUM3_PROGRAM_ID } from '../constants';
@ -20,7 +20,7 @@ import {
isSwitchboardOracle,
parseSwitchboardOracle,
} from './oracle';
import { PerpMarket } from './perp';
import { BookSide, PerpMarket } from './perp';
import { Serum3Market } from './serum3';
export class Group {
@ -98,7 +98,7 @@ export class Group {
await Promise.all([
this.reloadBanks(client, ids).then(() =>
Promise.all([
this.reloadBankPrices(client),
this.reloadBankOraclePrices(client),
this.reloadVaults(client, ids),
]),
),
@ -106,7 +106,9 @@ export class Group {
this.reloadSerum3Markets(client, ids).then(() =>
this.reloadSerum3ExternalMarkets(client, ids),
),
this.reloadPerpMarkets(client, ids),
this.reloadPerpMarkets(client, ids).then(() =>
this.reloadPerpMarketOraclePrices(client),
),
]);
// console.timeEnd('group.reload');
}
@ -229,61 +231,95 @@ export class Group {
);
}
public async reloadBankPrices(client: MangoClient): Promise<void> {
public async reloadBankOraclePrices(client: MangoClient): Promise<void> {
const banks: Bank[][] = Array.from(
this.banksMapByMint,
([, value]) => value,
);
const oracles = banks.map((b) => b[0].oracle);
const prices =
const ais =
await client.program.provider.connection.getMultipleAccountsInfo(oracles);
const coder = new BorshAccountsCoder(client.program.idl);
for (const [index, price] of prices.entries()) {
for (const [index, ai] of ais.entries()) {
for (const bank of banks[index]) {
if (bank.name === 'USDC') {
bank.price = ONE_I80F48();
bank.uiPrice = 1;
} else {
// TODO: Implement switchboard oracle type
if (!price)
throw new Error('Undefined price object in reloadBankPrices');
if (
!BorshAccountsCoder.accountDiscriminator('stubOracle').compare(
price.data.slice(0, 8),
)
) {
const stubOracle = coder.decode('stubOracle', price.data);
bank.price = new I80F48(stubOracle.price.val);
bank.uiPrice = this?.toUiPrice(
bank.price,
bank.mint,
this.insuranceMint,
);
} else if (isPythOracle(price)) {
bank.uiPrice = parsePriceData(price.data).previousPrice;
bank.price = this?.toNativePrice(
bank.uiPrice,
bank.mint,
this.insuranceMint,
);
} else if (isSwitchboardOracle(price)) {
bank.uiPrice = await parseSwitchboardOracle(price);
bank.price = this?.toNativePrice(
bank.uiPrice,
bank.mint,
this.insuranceMint,
);
} else {
if (!ai)
throw new Error(
`Unknown oracle provider for oracle ${bank.oracle}, with owner ${price.owner}`,
`Undefined accountInfo object in reloadBankOraclePrices for ${bank.oracle}!`,
);
}
const { price, uiPrice } = await this.decodePriceFromOracleAi(
coder,
bank.oracle,
ai,
this.getMintDecimals(bank.mint),
this.getMintDecimals(this.insuranceMint),
);
bank.price = price;
bank.uiPrice = uiPrice;
}
}
}
}
public async reloadPerpMarketOraclePrices(
client: MangoClient,
): Promise<void> {
const perpMarkets: PerpMarket[] = Array.from(this.perpMarketsMap.values());
const oracles = perpMarkets.map((b) => b.oracle);
const ais =
await client.program.provider.connection.getMultipleAccountsInfo(oracles);
const coder = new BorshAccountsCoder(client.program.idl);
ais.forEach(async (ai, i) => {
const perpMarket = perpMarkets[i];
if (!ai)
throw new Error('Undefined ai object in reloadPerpMarketOraclePrices!');
const { price, uiPrice } = await this.decodePriceFromOracleAi(
coder,
perpMarket.oracle,
ai,
perpMarket.baseTokenDecimals,
this.getMintDecimals(this.insuranceMint),
);
perpMarket.price = price;
perpMarket.uiPrice = uiPrice;
});
}
private async decodePriceFromOracleAi(
coder: BorshAccountsCoder<string>,
oracle: PublicKey,
ai: AccountInfo<Buffer>,
baseDecimals: number,
quoteDecimals: number,
) {
let price, uiPrice;
if (
!BorshAccountsCoder.accountDiscriminator('stubOracle').compare(
ai.data.slice(0, 8),
)
) {
const stubOracle = coder.decode('stubOracle', ai.data);
price = new I80F48(stubOracle.price.val);
uiPrice = this?.toUiPrice(price, baseDecimals, quoteDecimals);
} else if (isPythOracle(ai)) {
uiPrice = parsePriceData(ai.data).previousPrice;
price = this?.toNativePrice(uiPrice, baseDecimals, quoteDecimals);
} else if (isSwitchboardOracle(ai)) {
uiPrice = await parseSwitchboardOracle(ai);
price = this?.toNativePrice(uiPrice, baseDecimals, quoteDecimals);
} else {
throw new Error(
`Unknown oracle provider for oracle ${oracle}, with owner ${ai.owner}`,
);
}
return { price, uiPrice };
}
public async reloadVaults(client: MangoClient, ids?: Id): Promise<void> {
const vaultPks = Array.from(this.banksMapByMint.values())
.flat()
@ -394,6 +430,28 @@ export class Group {
return maker ? rates.maker : rates.taker;
}
public async loadPerpBidsForMarket(
client: MangoClient,
marketName: string,
): Promise<BookSide> {
const perpMarket = this.perpMarketsMap.get(marketName);
if (!perpMarket) {
throw new Error(`Perp Market ${marketName} not found!`);
}
return await perpMarket.loadBids(client);
}
public async loadPerpAsksForMarket(
client: MangoClient,
marketName: string,
): Promise<BookSide> {
const perpMarket = this.perpMarketsMap.get(marketName);
if (!perpMarket) {
throw new Error(`Perp Market ${marketName} not found!`);
}
return await perpMarket.loadAsks(client);
}
/**
*
* @param mintPk
@ -416,25 +474,21 @@ export class Group {
public toUiPrice(
price: I80F48,
tokenMintPk: PublicKey,
quoteMintPk: PublicKey,
baseDecimals: number,
quoteDecimals: number,
): number {
const tokenDecimals = this.getMintDecimals(tokenMintPk);
const quoteDecimals = this.getMintDecimals(quoteMintPk);
return price
.mul(I80F48.fromNumber(Math.pow(10, tokenDecimals - quoteDecimals)))
.mul(I80F48.fromNumber(Math.pow(10, baseDecimals - quoteDecimals)))
.toNumber();
}
public toNativePrice(
uiPrice: number,
tokenMintPk: PublicKey,
quoteMintPk: PublicKey,
baseDecimals: number,
quoteDecimals: number,
): I80F48 {
const tokenDecimals = this.getMintDecimals(tokenMintPk);
const quoteDecimals = this.getMintDecimals(quoteMintPk);
return I80F48.fromNumber(uiPrice).mul(
I80F48.fromNumber(Math.pow(10, quoteDecimals - tokenDecimals)),
I80F48.fromNumber(Math.pow(10, quoteDecimals - baseDecimals)),
);
}

View File

@ -8,11 +8,13 @@ import { Bank } from './bank';
import { Group } from './group';
import { HealthCache, HealthCacheDto } from './healthCache';
import { I80F48, I80F48Dto, ONE_I80F48, ZERO_I80F48 } from './I80F48';
import { PerpOrder } from './perp';
import { Serum3Market, Serum3Side } from './serum3';
export class MangoAccount {
public tokens: TokenPosition[];
public serum3: Serum3Orders[];
public perps: PerpPosition[];
public perpOpenOrders: PerpOo[];
public name: string;
static from(
@ -49,7 +51,7 @@ export class MangoAccount {
obj.tokens as TokenPositionDto[],
obj.serum3 as Serum3PositionDto[],
obj.perps as PerpPositionDto[],
obj.perpOpenOrders as any,
obj.perpOpenOrders as PerpOoDto[],
{} as any,
);
}
@ -69,13 +71,14 @@ export class MangoAccount {
tokens: TokenPositionDto[],
serum3: Serum3PositionDto[],
perps: PerpPositionDto[],
perpOpenOrders: PerpPositionDto[],
perpOpenOrders: PerpOoDto[],
public accountData: undefined | MangoAccountData,
) {
this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0];
this.tokens = tokens.map((dto) => TokenPosition.from(dto));
this.serum3 = serum3.map((dto) => Serum3Orders.from(dto));
this.perps = perps.map((dto) => PerpPosition.from(dto));
this.perpOpenOrders = perpOpenOrders.map((dto) => PerpOo.from(dto));
this.accountData = undefined;
}
@ -105,6 +108,12 @@ export class MangoAccount {
return this.perps.filter((perp) => perp.isActive());
}
perpOrdersActive(): PerpOo[] {
return this.perpOpenOrders.filter(
(oo) => oo.orderMarket !== PerpOo.OrderMarketUnset,
);
}
findToken(tokenIndex: number): TokenPosition | undefined {
return this.tokens.find((ta) => ta.tokenIndex == tokenIndex);
}
@ -675,23 +684,22 @@ export class MangoAccount {
).toNumber();
}
/**
* The remaining native quote margin available for given market.
*
* TODO: this is a very bad estimation atm.
* It assumes quote asset is always quote,
* it assumes that there are no interaction effects,
* it assumes that there are no existing borrows for either of the tokens in the market.
*/
getPerpMarketMarginAvailable(
public async loadPerpOpenOrdersForMarket(
client: MangoClient,
group: Group,
marketName: string,
): I80F48 | undefined {
if (!this.accountData) return undefined;
const initHealth = this.accountData.initHealth;
const perpMarket = group.perpMarketsMap.get(marketName)!;
const marketAssetWeight = perpMarket.initAssetWeight;
return initHealth.div(ONE_I80F48().sub(marketAssetWeight));
perpMarketName: string,
): Promise<PerpOrder[]> {
const perpMarket = group.perpMarketsMap.get(perpMarketName);
if (!perpMarket) {
throw new Error(`Perp Market ${perpMarketName} not found!`);
}
const [bids, asks] = await Promise.all([
perpMarket.loadBids(client),
perpMarket.loadAsks(client),
]);
return [...Array.from(bids.items()), ...Array.from(asks.items())].filter(
(order) => order.owner.equals(this.publicKey),
);
}
toString(group?: Group): string {
@ -701,6 +709,9 @@ export class MangoAccount {
res = res + '\n owner: ' + this.owner;
res = res + '\n delegate: ' + this.delegate;
res =
res +
`\n max token slots ${this.tokens.length}, max serum3 slots ${this.serum3.length}, max perp slots ${this.perps.length}, max perp oo slots ${this.perpOpenOrders.length}`;
res =
this.tokensActive().length > 0
? res +
@ -726,6 +737,13 @@ export class MangoAccount {
? res + '\n perps:' + JSON.stringify(this.perpActive(), null, 4)
: res + '';
res =
this.perpOrdersActive().length > 0
? res +
'\n perps oo:' +
JSON.stringify(this.perpOrdersActive(), null, 4)
: res + '';
return res;
}
}
@ -884,7 +902,7 @@ export class PerpPosition {
return new PerpPosition(
dto.marketIndex,
dto.basePositionLots.toNumber(),
dto.quotePositionNative.val.toNumber(),
dto.quotePositionNative.val,
dto.bidsBaseLots.toNumber(),
dto.asksBaseLots.toNumber(),
dto.takerBaseLots.toNumber(),
@ -895,7 +913,7 @@ export class PerpPosition {
constructor(
public marketIndex: number,
public basePositionLots: number,
public quotePositionNative: number,
public quotePositionNative: BN,
public bidsBaseLots: number,
public asksBaseLots: number,
public takerBaseLots: number,
@ -920,6 +938,33 @@ export class PerpPositionDto {
) {}
}
export class PerpOo {
static OrderMarketUnset = 65535;
static from(dto: PerpOoDto) {
return new PerpOo(
dto.orderSide,
dto.orderMarket,
dto.clientOrderId.toNumber(),
dto.orderId,
);
}
constructor(
public orderSide: any,
public orderMarket: 0,
public clientOrderId: number,
public orderId: BN,
) {}
}
export class PerpOoDto {
constructor(
public orderSide: any,
public orderMarket: 0,
public clientOrderId: BN,
public orderId: BN,
) {}
}
export class HealthType {
static maint = { maint: {} };
static init = { init: {} };

View File

@ -1,6 +1,9 @@
import { BN } from '@project-serum/anchor';
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 { U64_MAX_BN } from '../utils';
import { OracleConfig, QUOTE_DECIMALS } from './bank';
import { I80F48, I80F48Dto } from './I80F48';
@ -13,9 +16,16 @@ export class PerpMarket {
public liquidationFee: I80F48;
public makerFee: I80F48;
public takerFee: I80F48;
public minFunding: I80F48;
public maxFunding: I80F48;
public openInterest: number;
public seqNum: number;
public feesAccrued: I80F48;
priceLotsToUiConverter: number;
baseLotsToUiConverter: number;
quoteLotsToUiConverter: number;
public price: number;
public uiPrice: number;
static from(
publicKey: PublicKey,
@ -111,8 +121,8 @@ export class PerpMarket {
makerFee: I80F48Dto,
takerFee: I80F48Dto,
minFunding: I80F48Dto,
maxFundingI80F48Dto,
impactQuantity: BN,
maxFunding: I80F48Dto,
public impactQuantity: BN,
longFunding: I80F48Dto,
shortFunding: I80F48Dto,
fundingLastUpdated: BN,
@ -131,24 +141,110 @@ export class PerpMarket {
this.liquidationFee = I80F48.from(liquidationFee);
this.makerFee = I80F48.from(makerFee);
this.takerFee = I80F48.from(takerFee);
this.minFunding = I80F48.from(minFunding);
this.maxFunding = I80F48.from(maxFunding);
this.openInterest = openInterest.toNumber();
this.seqNum = seqNum.toNumber();
this.feesAccrued = I80F48.from(feesAccrued);
this.priceLotsToUiConverter = new Big(10)
.pow(baseTokenDecimals - 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(baseTokenDecimals))
.toNumber();
this.quoteLotsToUiConverter = new Big(this.quoteLotSize.toString())
.div(new Big(10).pow(QUOTE_DECIMALS))
.toNumber();
}
uiToNativePriceQuantity(price: number, quantity: number): [BN, BN] {
const baseUnit = Math.pow(10, this.baseTokenDecimals);
const quoteUnit = Math.pow(10, QUOTE_DECIMALS);
const nativePrice = new BN(price * quoteUnit)
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);
}
public async loadFills(client: MangoClient, lastSeqNum: BN) {
const eventQueue = await this.loadEventQueue(client);
return eventQueue
.eventsSince(lastSeqNum)
.filter((event) => event.eventType == PerpEventQueue.FILL_EVENT_TYPE);
}
/**
*
* @param bids
* @param asks
* @returns returns funding rate per hour
*/
public getCurrentFundingRate(bids: BookSide, asks: BookSide) {
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;
}
public uiPriceToLots(price: number): BN {
return new BN(price * Math.pow(10, QUOTE_DECIMALS))
.mul(this.baseLotSize)
.div(this.quoteLotSize.mul(new BN(baseUnit)));
const nativeQuantity = new BN(quantity * baseUnit).div(this.baseLotSize);
return [nativePrice, nativeQuantity];
.div(this.quoteLotSize.mul(new BN(Math.pow(10, this.baseTokenDecimals))));
}
uiQuoteToLots(uiQuote: number): BN {
const quoteUnit = Math.pow(10, QUOTE_DECIMALS);
return new BN(uiQuote * quoteUnit).div(this.quoteLotSize);
public uiBaseToLots(quantity: number): BN {
return new BN(quantity * Math.pow(10, this.baseTokenDecimals)).div(
this.baseLotSize,
);
}
public uiQuoteToLots(uiQuote: number): BN {
return new BN(uiQuote * Math.pow(10, QUOTE_DECIMALS)).div(
this.quoteLotSize,
);
}
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;
}
toString(): string {
@ -174,15 +270,350 @@ export class PerpMarket {
}
}
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;
},
) {
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,
) {
// TODO why? Ask Daffy
// 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;
}
static getPriceFromKey(key: BN) {
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);
}
}
}
}
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.price;
}
}
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 { price, 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, 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 {
static from(obj: { children: [number] }) {
return new InnerNode(obj.children);
}
constructor(public children: [number]) {}
}
export class Side {
static bid = { bid: {} };
static ask = { ask: {} };
}
export class OrderType {
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) {
const side = type == BookSideType.bids ? Side.bid : Side.ask;
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,
public price: number,
public priceLots: BN,
public size: number,
public sizeLots: BN,
public side: Side,
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;
this.rawEvents = buf.slice(0, this.count).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));
}
}
interface Event {
eventType: number;
}
interface OutEvent extends Event {
side: PerpOrderType;
ownerSlot: number;
timestamp: BN;
seqNum: BN;
owner: PublicKey;
quantity: BN;
}
interface FillEvent extends Event {
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;
}
interface LiquidateEvent extends Event {
seqNum: BN;
}

View File

@ -33,7 +33,7 @@ import {
TokenPosition,
} from './accounts/mangoAccount';
import { StubOracle } from './accounts/oracle';
import { OrderType, PerpMarket, Side } from './accounts/perp';
import { PerpMarket, PerpOrderType, Side } from './accounts/perp';
import {
generateSerum3MarketExternalVaultSignerAddress,
Serum3Market,
@ -1472,12 +1472,11 @@ export class MangoClient {
quantity: number,
maxQuoteQuantity: number,
clientOrderId: number,
orderType: OrderType,
orderType: PerpOrderType,
expiryTimestamp: number,
limit: number,
) {
): Promise<TransactionSignature> {
const perpMarket = group.perpMarketsMap.get(perpMarketName)!;
const healthRemainingAccounts: PublicKey[] =
this.buildHealthRemainingAccounts(
AccountRetriever.Fixed,
@ -1486,22 +1485,14 @@ export class MangoClient {
[],
[perpMarket],
);
const [nativePrice, nativeQuantity] = perpMarket.uiToNativePriceQuantity(
price,
quantity,
);
const maxQuoteQuantityLots = maxQuoteQuantity
? perpMarket.uiQuoteToLots(maxQuoteQuantity)
: I64_MAX_BN;
await this.program.methods
return await this.program.methods
.perpPlaceOrder(
side,
nativePrice,
nativeQuantity,
maxQuoteQuantityLots,
perpMarket.uiPriceToLots(price),
perpMarket.uiBaseToLots(quantity),
maxQuoteQuantity
? perpMarket.uiQuoteToLots(maxQuoteQuantity)
: I64_MAX_BN,
new BN(clientOrderId),
orderType,
new BN(expiryTimestamp),
@ -1526,6 +1517,26 @@ export class MangoClient {
.rpc();
}
async perpCancelAllOrders(
group: Group,
mangoAccount: MangoAccount,
perpMarketName: string,
limit: number,
): Promise<TransactionSignature> {
const perpMarket = group.perpMarketsMap.get(perpMarketName)!;
return await this.program.methods
.perpCancelAllOrders(limit)
.accounts({
group: group.publicKey,
account: mangoAccount.publicKey,
perpMarket: perpMarket.publicKey,
asks: perpMarket.asks,
bids: perpMarket.bids,
owner: (this.program.provider as AnchorProvider).wallet.publicKey,
})
.rpc();
}
public async marginTrade({
group,
mangoAccount,

View File

@ -4609,6 +4609,150 @@ export type MangoV4 = {
]
}
},
{
"name": "InnerNode",
"docs": [
"InnerNodes and LeafNodes compose the binary tree of orders.",
"",
"Each InnerNode has exactly two children, which are either InnerNodes themselves,",
"or LeafNodes. The children share the top `prefix_len` bits of `key`. The left",
"child has a 0 in the next bit, and the right a 1."
],
"type": {
"kind": "struct",
"fields": [
{
"name": "tag",
"type": "u32"
},
{
"name": "prefixLen",
"docs": [
"number of highest `key` bits that all children share",
"e.g. if it's 2, the two highest bits of `key` will be the same on all children"
],
"type": "u32"
},
{
"name": "key",
"docs": [
"only the top `prefix_len` bits of `key` are relevant"
],
"type": "i128"
},
{
"name": "children",
"docs": [
"indexes into `BookSide::nodes`"
],
"type": {
"array": [
"u32",
2
]
}
},
{
"name": "childEarliestExpiry",
"docs": [
"The earliest expiry timestamp for the left and right subtrees.",
"",
"Needed to be able to find and remove expired orders without having to",
"iterate through the whole bookside."
],
"type": {
"array": [
"u64",
2
]
}
},
{
"name": "reserved",
"type": {
"array": [
"u8",
48
]
}
}
]
}
},
{
"name": "LeafNode",
"docs": [
"LeafNodes represent an order in the binary tree"
],
"type": {
"kind": "struct",
"fields": [
{
"name": "tag",
"type": "u32"
},
{
"name": "ownerSlot",
"type": "u8"
},
{
"name": "orderType",
"type": {
"defined": "OrderType"
}
},
{
"name": "padding",
"type": {
"array": [
"u8",
1
]
}
},
{
"name": "timeInForce",
"docs": [
"Time in seconds after `timestamp` at which the order expires.",
"A value of 0 means no expiry."
],
"type": "u8"
},
{
"name": "key",
"docs": [
"The binary tree key"
],
"type": "i128"
},
{
"name": "owner",
"type": "publicKey"
},
{
"name": "quantity",
"type": "i64"
},
{
"name": "clientOrderId",
"type": "u64"
},
{
"name": "timestamp",
"type": "u64"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
16
]
}
}
]
}
},
{
"name": "AnyNode",
"type": {
@ -4671,6 +4815,166 @@ export type MangoV4 = {
]
}
},
{
"name": "FillEvent",
"type": {
"kind": "struct",
"fields": [
{
"name": "eventType",
"type": "u8"
},
{
"name": "takerSide",
"type": {
"defined": "Side"
}
},
{
"name": "makerOut",
"type": "bool"
},
{
"name": "makerSlot",
"type": "u8"
},
{
"name": "marketFeesApplied",
"type": "bool"
},
{
"name": "padding",
"type": {
"array": [
"u8",
3
]
}
},
{
"name": "timestamp",
"type": "u64"
},
{
"name": "seqNum",
"type": "u64"
},
{
"name": "maker",
"type": "publicKey"
},
{
"name": "makerOrderId",
"type": "i128"
},
{
"name": "makerClientOrderId",
"type": "u64"
},
{
"name": "makerFee",
"type": {
"defined": "I80F48"
}
},
{
"name": "makerTimestamp",
"type": "u64"
},
{
"name": "taker",
"type": "publicKey"
},
{
"name": "takerOrderId",
"type": "i128"
},
{
"name": "takerClientOrderId",
"type": "u64"
},
{
"name": "takerFee",
"type": {
"defined": "I80F48"
}
},
{
"name": "price",
"type": "i64"
},
{
"name": "quantity",
"type": "i64"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
16
]
}
}
]
}
},
{
"name": "OutEvent",
"type": {
"kind": "struct",
"fields": [
{
"name": "eventType",
"type": "u8"
},
{
"name": "side",
"type": {
"defined": "Side"
}
},
{
"name": "ownerSlot",
"type": "u8"
},
{
"name": "padding0",
"type": {
"array": [
"u8",
5
]
}
},
{
"name": "timestamp",
"type": "u64"
},
{
"name": "seqNum",
"type": "u64"
},
{
"name": "owner",
"type": "publicKey"
},
{
"name": "quantity",
"type": "i64"
},
{
"name": "padding1",
"type": {
"array": [
"u8",
144
]
}
}
]
}
},
{
"name": "TokenIndex",
"docs": [
@ -10304,6 +10608,150 @@ export const IDL: MangoV4 = {
]
}
},
{
"name": "InnerNode",
"docs": [
"InnerNodes and LeafNodes compose the binary tree of orders.",
"",
"Each InnerNode has exactly two children, which are either InnerNodes themselves,",
"or LeafNodes. The children share the top `prefix_len` bits of `key`. The left",
"child has a 0 in the next bit, and the right a 1."
],
"type": {
"kind": "struct",
"fields": [
{
"name": "tag",
"type": "u32"
},
{
"name": "prefixLen",
"docs": [
"number of highest `key` bits that all children share",
"e.g. if it's 2, the two highest bits of `key` will be the same on all children"
],
"type": "u32"
},
{
"name": "key",
"docs": [
"only the top `prefix_len` bits of `key` are relevant"
],
"type": "i128"
},
{
"name": "children",
"docs": [
"indexes into `BookSide::nodes`"
],
"type": {
"array": [
"u32",
2
]
}
},
{
"name": "childEarliestExpiry",
"docs": [
"The earliest expiry timestamp for the left and right subtrees.",
"",
"Needed to be able to find and remove expired orders without having to",
"iterate through the whole bookside."
],
"type": {
"array": [
"u64",
2
]
}
},
{
"name": "reserved",
"type": {
"array": [
"u8",
48
]
}
}
]
}
},
{
"name": "LeafNode",
"docs": [
"LeafNodes represent an order in the binary tree"
],
"type": {
"kind": "struct",
"fields": [
{
"name": "tag",
"type": "u32"
},
{
"name": "ownerSlot",
"type": "u8"
},
{
"name": "orderType",
"type": {
"defined": "OrderType"
}
},
{
"name": "padding",
"type": {
"array": [
"u8",
1
]
}
},
{
"name": "timeInForce",
"docs": [
"Time in seconds after `timestamp` at which the order expires.",
"A value of 0 means no expiry."
],
"type": "u8"
},
{
"name": "key",
"docs": [
"The binary tree key"
],
"type": "i128"
},
{
"name": "owner",
"type": "publicKey"
},
{
"name": "quantity",
"type": "i64"
},
{
"name": "clientOrderId",
"type": "u64"
},
{
"name": "timestamp",
"type": "u64"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
16
]
}
}
]
}
},
{
"name": "AnyNode",
"type": {
@ -10366,6 +10814,166 @@ export const IDL: MangoV4 = {
]
}
},
{
"name": "FillEvent",
"type": {
"kind": "struct",
"fields": [
{
"name": "eventType",
"type": "u8"
},
{
"name": "takerSide",
"type": {
"defined": "Side"
}
},
{
"name": "makerOut",
"type": "bool"
},
{
"name": "makerSlot",
"type": "u8"
},
{
"name": "marketFeesApplied",
"type": "bool"
},
{
"name": "padding",
"type": {
"array": [
"u8",
3
]
}
},
{
"name": "timestamp",
"type": "u64"
},
{
"name": "seqNum",
"type": "u64"
},
{
"name": "maker",
"type": "publicKey"
},
{
"name": "makerOrderId",
"type": "i128"
},
{
"name": "makerClientOrderId",
"type": "u64"
},
{
"name": "makerFee",
"type": {
"defined": "I80F48"
}
},
{
"name": "makerTimestamp",
"type": "u64"
},
{
"name": "taker",
"type": "publicKey"
},
{
"name": "takerOrderId",
"type": "i128"
},
{
"name": "takerClientOrderId",
"type": "u64"
},
{
"name": "takerFee",
"type": {
"defined": "I80F48"
}
},
{
"name": "price",
"type": "i64"
},
{
"name": "quantity",
"type": "i64"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
16
]
}
}
]
}
},
{
"name": "OutEvent",
"type": {
"kind": "struct",
"fields": [
{
"name": "eventType",
"type": "u8"
},
{
"name": "side",
"type": {
"defined": "Side"
}
},
{
"name": "ownerSlot",
"type": "u8"
},
{
"name": "padding0",
"type": {
"array": [
"u8",
5
]
}
},
{
"name": "timestamp",
"type": "u64"
},
{
"name": "seqNum",
"type": "u64"
},
{
"name": "owner",
"type": "publicKey"
},
{
"name": "quantity",
"type": "i64"
},
{
"name": "padding1",
"type": {
"array": [
"u8",
144
]
}
}
]
}
},
{
"name": "TokenIndex",
"docs": [

View File

@ -327,7 +327,7 @@ async function main() {
serumMarketExternalPk,
group.getFirstBankByMint(ethDevnetMint),
group.getFirstBankByMint(usdcDevnetMint),
0,
1,
'ETH/USDC',
);
} catch (error) {
@ -341,7 +341,7 @@ async function main() {
serumMarketExternalPk,
group.getFirstBankByMint(srmDevnetMint),
group.getFirstBankByMint(usdcDevnetMint),
0,
2,
'SRM/USDC',
);
} catch (error) {

View File

@ -1,9 +1,9 @@
import { AnchorProvider, Wallet } from '@project-serum/anchor';
import { AnchorProvider, BN, Wallet } from '@project-serum/anchor';
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
import fs from 'fs';
import { I80F48 } from '../accounts/I80F48';
import { HealthType } from '../accounts/mangoAccount';
import { OrderType, Side } from '../accounts/perp';
import { BookSide, PerpOrderType, Side } from '../accounts/perp';
import {
Serum3OrderType,
Serum3SelfTradeBehavior,
@ -66,14 +66,13 @@ async function main() {
),
);
const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM);
console.log(`${group}`);
// create + fetch account
console.log(`Creating mangoaccount...`);
const mangoAccount = await client.getOrCreateMangoAccount(
let mangoAccount = (await client.getOrCreateMangoAccount(
group,
user.publicKey,
);
))!;
if (!mangoAccount) {
throw new Error(`MangoAccount not found for user ${user.publicKey}`);
}
@ -82,8 +81,8 @@ async function main() {
await mangoAccount.reload(client, group);
// set delegate, and change name
if (false) {
// set delegate, and change name
console.log(`...changing mango account name, and setting a delegate`);
const randomKey = new PublicKey(
'4ZkS7ZZkxfsC3GtvvsHP3DFcUeByU9zzZELS4r8HCELo',
@ -109,18 +108,17 @@ async function main() {
console.log(mangoAccount.toString());
}
// expand account
if (false) {
// expand account
console.log(
`...expanding mango account to have serum3 and perp position slots`,
);
await client.expandMangoAccount(group, mangoAccount, 16, 8, 8, 8);
await client.expandMangoAccount(group, mangoAccount, 8, 8, 8, 8);
await mangoAccount.reload(client, group);
}
// deposit and withdraw
if (false) {
// deposit and withdraw
try {
console.log(`...depositing 50 USDC, 1 SOL, 1 MNGO`);
await client.tokenDeposit(
@ -297,46 +295,46 @@ async function main() {
await mangoAccount.reload(client, group);
console.log(
'...mangoAccount.getEquity() ' +
toUiDecimalsForQuote(mangoAccount.getEquity().toNumber()),
toUiDecimalsForQuote(mangoAccount.getEquity()!.toNumber()),
);
console.log(
'...mangoAccount.getCollateralValue() ' +
toUiDecimalsForQuote(mangoAccount.getCollateralValue().toNumber()),
toUiDecimalsForQuote(mangoAccount.getCollateralValue()!.toNumber()),
);
console.log(
'...mangoAccount.accountData["healthCache"].health(HealthType.init) ' +
toUiDecimalsForQuote(
mangoAccount.accountData['healthCache']
.health(HealthType.init)
mangoAccount
.accountData!['healthCache'].health(HealthType.init)
.toNumber(),
),
);
console.log(
'...mangoAccount.getAssetsVal() ' +
toUiDecimalsForQuote(
mangoAccount.getAssetsValue(HealthType.init).toNumber(),
mangoAccount.getAssetsValue(HealthType.init)!.toNumber(),
),
);
console.log(
'...mangoAccount.getLiabsVal() ' +
toUiDecimalsForQuote(
mangoAccount.getLiabsValue(HealthType.init).toNumber(),
mangoAccount.getLiabsValue(HealthType.init)!.toNumber(),
),
);
console.log(
'...mangoAccount.getMaxWithdrawWithBorrowForToken(group, "SOL") ' +
toUiDecimalsForQuote(
(
await mangoAccount.getMaxWithdrawWithBorrowForToken(
mangoAccount
.getMaxWithdrawWithBorrowForToken(
group,
new PublicKey(DEVNET_MINTS.get('SOL')!),
)
).toNumber(),
)!
.toNumber(),
),
);
}
if (true) {
if (false) {
const asks = await group.loadSerum3AsksForMarket(
client,
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
@ -358,7 +356,7 @@ async function main() {
group.banksMapByName.get(src)![0].mint,
group.banksMapByName.get(tgt)![0].mint,
1,
)
)!
.div(
I80F48.fromNumber(
Math.pow(10, group.banksMapByName.get(src)![0].mintDecimals),
@ -407,61 +405,152 @@ async function main() {
);
}
if (false) {
console.log(
"...mangoAccount.getPerpMarketMarginAvailable(group, 'BTC-PERP') " +
toUiDecimalsForQuote(
mangoAccount
.getPerpMarketMarginAvailable(group, 'BTC-PERP')
.toNumber(),
),
// perps
if (true) {
const orders = await mangoAccount.loadPerpOpenOrdersForMarket(
client,
group,
'BTC-PERP',
);
}
for (const order of orders) {
console.log(`Current order - ${order.price} ${order.size} ${order.side}`);
}
console.log(`...cancelling all perp orders`);
let sig = await client.perpCancelAllOrders(
group,
mangoAccount,
'BTC-PERP',
10,
);
console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
if (false) {
// perps
console.log(`...placing perp bid`);
// // scenario 1
// // not going to be hit orders, far from each other
// try {
// const clientId = Math.floor(Math.random() * 99999);
// const price =
// group.banksMapByName.get('BTC')![0].uiPrice! -
// Math.floor(Math.random() * 100);
// console.log(`...placing perp bid ${clientId} at ${price}`);
// const sig = await client.perpPlaceOrder(
// group,
// mangoAccount,
// 'BTC-PERP',
// Side.bid,
// price,
// 0.01,
// price * 0.01,
// clientId,
// PerpOrderType.limit,
// 0, //Date.now() + 200,
// 1,
// );
// console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
// } catch (error) {
// console.log(error);
// }
// try {
// const clientId = Math.floor(Math.random() * 99999);
// const price =
// group.banksMapByName.get('BTC')![0].uiPrice! +
// Math.floor(Math.random() * 100);
// console.log(`...placing perp ask ${clientId} at ${price}`);
// const sig = await client.perpPlaceOrder(
// group,
// mangoAccount,
// 'BTC-PERP',
// Side.ask,
// price,
// 0.01,
// price * 0.01,
// clientId,
// PerpOrderType.limit,
// 0, //Date.now() + 200,
// 1,
// );
// console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
// } catch (error) {
// console.log(error);
// }
// // should be able to cancel them
// console.log(`...cancelling all perp orders`);
// sig = await client.perpCancelAllOrders(group, mangoAccount, 'BTC-PERP', 10);
// console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
// scenario 2
// make + take orders
try {
await client.perpPlaceOrder(
const clientId = Math.floor(Math.random() * 99999);
const price = group.banksMapByName.get('BTC')![0].uiPrice!;
console.log(`...placing perp bid ${clientId} at ${price}`);
const sig = await client.perpPlaceOrder(
group,
mangoAccount,
'BTC-PERP',
Side.bid,
30000,
0.000001,
30000 * 0.000001,
Math.floor(Math.random() * 99999),
OrderType.limit,
0,
price,
0.01,
price * 0.01,
clientId,
PerpOrderType.limit,
0, //Date.now() + 200,
1,
);
console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
} catch (error) {
console.log(error);
}
console.log(`...placing perp ask`);
await client.perpPlaceOrder(
group,
mangoAccount,
'BTC-PERP',
Side.ask,
30000,
0.000001,
30000 * 0.000001,
Math.floor(Math.random() * 99999),
OrderType.limit,
0,
1,
);
while (true) {
// TODO: quotePositionNative might be buggy on program side, investigate...
console.log(
`...waiting for self trade to consume (note: make sure keeper crank is running)`,
try {
const clientId = Math.floor(Math.random() * 99999);
const price = group.banksMapByName.get('BTC')![0].uiPrice!;
console.log(`...placing perp ask ${clientId} at ${price}`);
const sig = await client.perpPlaceOrder(
group,
mangoAccount,
'BTC-PERP',
Side.ask,
price,
0.01,
price * 0.01,
clientId,
PerpOrderType.limit,
0, //Date.now() + 200,
1,
);
await mangoAccount.reload(client, group);
console.log(mangoAccount.toString());
console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
} catch (error) {
console.log(error);
}
// should be able to cancel them
console.log(`...cancelling all perp orders`);
sig = await client.perpCancelAllOrders(group, mangoAccount, 'BTC-PERP', 10);
console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
const perpMarket = group.perpMarketsMap.get('BTC-PERP');
const bids: BookSide = await perpMarket?.loadBids(client)!;
console.log(Array.from(bids.items()));
const asks: BookSide = await perpMarket?.loadAsks(client)!;
console.log(Array.from(asks.items()));
await perpMarket?.loadEventQueue(client)!;
const fr = await perpMarket?.getCurrentFundingRate(
await perpMarket.loadBids(client),
await perpMarket.loadAsks(client),
);
console.log(`current funding rate per hour is ${fr}`);
const eq = await perpMarket?.loadEventQueue(client)!;
console.log(eq.rawEvents);
console.log(eq.eventsSince(new BN(0)));
// sleep so that keeper can catch up
await new Promise((r) => setTimeout(r, 2000));
// make+take orders should have cancelled each other, and if keeper has already cranked, then should not appear in position
await group.reloadAll(client);
await mangoAccount.reload(client, group);
console.log(`${mangoAccount.toString(group)}`);
}
process.exit();

View File

@ -32,7 +32,7 @@ const TOKEN_SCENARIOS: [string, string, number, string, number][] = [
['LIQTEST, LIQOR', 'USDC', 1000000, 'USDC', 0],
['LIQTEST, A: USDC, L: SOL', 'USDC', 1000 * PRICES.SOL, 'SOL', 920],
['LIQTEST, A: SOL, L: USDC', 'SOL', 1000, 'USDC', 920 * PRICES.SOL],
['LIQTEST, A: BTC, L: SOL', 'BTC', 20, 'SOL', 18 * PRICES.BTC / PRICES.SOL],
['LIQTEST, A: BTC, L: SOL', 'BTC', 20, 'SOL', (18 * PRICES.BTC) / PRICES.SOL],
];
async function main() {

View File

@ -15,6 +15,7 @@ import { I80F48 } from './accounts/I80F48';
import { MangoAccount, Serum3Orders } from './accounts/mangoAccount';
import { PerpMarket } from './accounts/perp';
export const U64_MAX_BN = new BN('18446744073709551615');
export const I64_MAX_BN = new BN('9223372036854775807').toTwos(64);
export function debugAccountMetas(ams: AccountMeta[]) {