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:
parent
11160ae2fe
commit
b7e79a4663
|
@ -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],
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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: {} };
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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": [
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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[]) {
|
||||
|
|
Loading…
Reference in New Issue