Perp oracle peg feature (#264)

This introduces the ability to use oracle peg orders on perp markets.

This PR has significant non-backwards compatible changes, for example all
order trees are now in a single account instead of separate.
This commit is contained in:
Christian Kamm 2022-11-08 15:27:56 +01:00 committed by GitHub
parent 2b8e976956
commit 5731ce8faa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 3033 additions and 1459 deletions

View File

@ -1,7 +1,7 @@
use anchor_lang::prelude::*;
use crate::error::MangoError;
use crate::state::{AccountLoaderDynamic, Book, BookSide, Group, MangoAccount, PerpMarket};
use crate::state::{AccountLoaderDynamic, Group, MangoAccount, OrderBook, PerpMarket};
#[derive(Accounts)]
pub struct PerpCancelAllOrders<'info> {
@ -14,14 +14,11 @@ pub struct PerpCancelAllOrders<'info> {
#[account(
mut,
has_one = group,
has_one = bids,
has_one = asks
has_one = orderbook,
)]
pub perp_market: AccountLoader<'info, PerpMarket>,
#[account(mut)]
pub asks: AccountLoader<'info, BookSide>,
#[account(mut)]
pub bids: AccountLoader<'info, BookSide>,
pub orderbook: AccountLoader<'info, OrderBook>,
}
pub fn perp_cancel_all_orders(ctx: Context<PerpCancelAllOrders>, limit: u8) -> Result<()> {
@ -32,9 +29,7 @@ pub fn perp_cancel_all_orders(ctx: Context<PerpCancelAllOrders>, limit: u8) -> R
);
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
let bids = ctx.accounts.bids.load_mut()?;
let asks = ctx.accounts.asks.load_mut()?;
let mut book = Book::new(bids, asks);
let mut book = ctx.accounts.orderbook.load_mut()?;
book.cancel_all_orders(&mut account.borrow_mut(), &mut perp_market, limit, None)?;

View File

@ -1,7 +1,7 @@
use anchor_lang::prelude::*;
use crate::error::MangoError;
use crate::state::{AccountLoaderDynamic, Book, BookSide, Group, MangoAccount, PerpMarket, Side};
use crate::state::{AccountLoaderDynamic, Group, MangoAccount, OrderBook, PerpMarket, Side};
#[derive(Accounts)]
pub struct PerpCancelAllOrdersBySide<'info> {
@ -14,14 +14,11 @@ pub struct PerpCancelAllOrdersBySide<'info> {
#[account(
mut,
has_one = group,
has_one = bids,
has_one = asks
has_one = orderbook,
)]
pub perp_market: AccountLoader<'info, PerpMarket>,
#[account(mut)]
pub asks: AccountLoader<'info, BookSide>,
#[account(mut)]
pub bids: AccountLoader<'info, BookSide>,
pub orderbook: AccountLoader<'info, OrderBook>,
}
pub fn perp_cancel_all_orders_by_side(
@ -37,9 +34,7 @@ pub fn perp_cancel_all_orders_by_side(
);
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
let bids = ctx.accounts.bids.load_mut()?;
let asks = ctx.accounts.asks.load_mut()?;
let mut book = Book::new(bids, asks);
let mut book = ctx.accounts.orderbook.load_mut()?;
book.cancel_all_orders(
&mut account.borrow_mut(),

View File

@ -1,7 +1,7 @@
use anchor_lang::prelude::*;
use crate::error::*;
use crate::state::{AccountLoaderDynamic, Book, BookSide, Group, MangoAccount, PerpMarket};
use crate::state::{AccountLoaderDynamic, Group, MangoAccount, OrderBook, PerpMarket};
#[derive(Accounts)]
pub struct PerpCancelOrder<'info> {
@ -14,17 +14,14 @@ pub struct PerpCancelOrder<'info> {
#[account(
mut,
has_one = group,
has_one = bids,
has_one = asks
has_one = orderbook,
)]
pub perp_market: AccountLoader<'info, PerpMarket>,
#[account(mut)]
pub asks: AccountLoader<'info, BookSide>,
#[account(mut)]
pub bids: AccountLoader<'info, BookSide>,
pub orderbook: AccountLoader<'info, OrderBook>,
}
pub fn perp_cancel_order(ctx: Context<PerpCancelOrder>, order_id: i128) -> Result<()> {
pub fn perp_cancel_order(ctx: Context<PerpCancelOrder>, order_id: u128) -> Result<()> {
let mut account = ctx.accounts.account.load_mut()?;
require!(
account.fixed.is_owner_or_delegate(ctx.accounts.owner.key()),
@ -32,20 +29,20 @@ pub fn perp_cancel_order(ctx: Context<PerpCancelOrder>, order_id: i128) -> Resul
);
let perp_market = ctx.accounts.perp_market.load_mut()?;
let bids = ctx.accounts.bids.load_mut()?;
let asks = ctx.accounts.asks.load_mut()?;
let mut book = Book::new(bids, asks);
let mut book = ctx.accounts.orderbook.load_mut()?;
let side = account
.perp_find_order_side(perp_market.perp_market_index, order_id)
let oo = account
.perp_find_order_with_order_id(perp_market.perp_market_index, order_id)
.ok_or_else(|| {
error_msg!("could not find perp order with id {order_id} in perp market orderbook")
})?;
let order_id = oo.id;
let order_side_and_tree = oo.side_and_tree;
book.cancel_order(
&mut account.borrow_mut(),
order_id,
side,
order_side_and_tree,
Some(ctx.accounts.account.key()),
)?;

View File

@ -1,7 +1,7 @@
use anchor_lang::prelude::*;
use crate::error::*;
use crate::state::{AccountLoaderDynamic, Book, BookSide, Group, MangoAccount, PerpMarket};
use crate::state::{AccountLoaderDynamic, Group, MangoAccount, OrderBook, PerpMarket};
#[derive(Accounts)]
pub struct PerpCancelOrderByClientOrderId<'info> {
@ -14,14 +14,11 @@ pub struct PerpCancelOrderByClientOrderId<'info> {
#[account(
mut,
has_one = group,
has_one = bids,
has_one = asks
has_one = orderbook,
)]
pub perp_market: AccountLoader<'info, PerpMarket>,
#[account(mut)]
pub asks: AccountLoader<'info, BookSide>,
#[account(mut)]
pub bids: AccountLoader<'info, BookSide>,
pub orderbook: AccountLoader<'info, OrderBook>,
}
pub fn perp_cancel_order_by_client_order_id(
@ -35,18 +32,18 @@ pub fn perp_cancel_order_by_client_order_id(
);
let perp_market = ctx.accounts.perp_market.load_mut()?;
let bids = ctx.accounts.bids.load_mut()?;
let asks = ctx.accounts.asks.load_mut()?;
let mut book = Book::new(bids, asks);
let mut book = ctx.accounts.orderbook.load_mut()?;
let (order_id, side) = account
let oo = account
.perp_find_order_with_client_order_id(perp_market.perp_market_index, client_order_id)
.ok_or_else(|| error_msg!("could not find perp order with client order id {client_order_id} in perp order books"))?;
let order_id = oo.id;
let order_side_and_tree = oo.side_and_tree;
book.cancel_order(
&mut account.borrow_mut(),
order_id,
side,
order_side_and_tree,
Some(ctx.accounts.account.key()),
)?;

View File

@ -15,8 +15,7 @@ pub struct PerpCloseMarket<'info> {
#[account(
mut,
has_one = group,
has_one = bids,
has_one = asks,
has_one = orderbook,
has_one = event_queue,
close = sol_destination
)]
@ -26,13 +25,7 @@ pub struct PerpCloseMarket<'info> {
mut,
close = sol_destination
)]
pub bids: AccountLoader<'info, BookSide>,
#[account(
mut,
close = sol_destination
)]
pub asks: AccountLoader<'info, BookSide>,
pub orderbook: AccountLoader<'info, OrderBook>,
#[account(
mut,

View File

@ -32,9 +32,7 @@ pub struct PerpCreateMarket<'info> {
/// Accounts are initialised by client,
/// anchor discriminator is set first when ix exits,
#[account(zero)]
pub bids: AccountLoader<'info, BookSide>,
#[account(zero)]
pub asks: AccountLoader<'info, BookSide>,
pub orderbook: AccountLoader<'info, OrderBook>,
#[account(zero)]
pub event_queue: AccountLoader<'info, EventQueue>,
@ -92,8 +90,7 @@ pub fn perp_create_market(
name: fill_from_str(&name)?,
oracle: ctx.accounts.oracle.key(),
oracle_config,
bids: ctx.accounts.bids.key(),
asks: ctx.accounts.asks.key(),
orderbook: ctx.accounts.orderbook.key(),
event_queue: ctx.accounts.event_queue.key(),
quote_lot_size,
base_lot_size,
@ -119,6 +116,7 @@ pub fn perp_create_market(
registration_time: Clock::get()?.unix_timestamp,
padding1: Default::default(),
padding2: Default::default(),
padding3: Default::default(),
fee_penalty,
settle_fee_flat,
settle_fee_amount_threshold,
@ -126,11 +124,8 @@ pub fn perp_create_market(
reserved: [0; 92],
};
let mut bids = ctx.accounts.bids.load_init()?;
bids.book_side_type = BookSideType::Bids;
let mut asks = ctx.accounts.asks.load_init()?;
asks.book_side_type = BookSideType::Asks;
let mut orderbook = ctx.accounts.orderbook.load_init()?;
orderbook.init();
emit!(PerpMarketMetaDataLog {
mango_group: ctx.accounts.group.key(),

View File

@ -14,14 +14,11 @@ pub struct PerpLiqForceCancelOrders<'info> {
#[account(
mut,
has_one = group,
has_one = bids,
has_one = asks
has_one = orderbook,
)]
pub perp_market: AccountLoader<'info, PerpMarket>,
#[account(mut)]
pub asks: AccountLoader<'info, BookSide>,
#[account(mut)]
pub bids: AccountLoader<'info, BookSide>,
pub orderbook: AccountLoader<'info, OrderBook>,
/// CHECK: Oracle can have different account types, constrained by address in perp_market
pub oracle: UncheckedAccount<'info>,
@ -68,9 +65,7 @@ pub fn perp_liq_force_cancel_orders(
//
{
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
let bids = ctx.accounts.bids.load_mut()?;
let asks = ctx.accounts.asks.load_mut()?;
let mut book = Book::new(bids, asks);
let mut book = ctx.accounts.orderbook.load_mut()?;
book.cancel_all_orders(&mut account.borrow_mut(), &mut perp_market, limit, None)?;

View File

@ -4,8 +4,8 @@ use crate::accounts_zerocopy::*;
use crate::error::*;
use crate::state::MangoAccount;
use crate::state::{
new_fixed_order_account_retriever, new_health_cache, AccountLoaderDynamic, Book, BookSide,
EventQueue, Group, OrderType, PerpMarket, Side,
new_fixed_order_account_retriever, new_health_cache, AccountLoaderDynamic, EventQueue, Group,
Order, OrderBook, PerpMarket,
};
#[derive(Accounts)]
@ -19,16 +19,13 @@ pub struct PerpPlaceOrder<'info> {
#[account(
mut,
has_one = group,
has_one = bids,
has_one = asks,
has_one = orderbook,
has_one = event_queue,
has_one = oracle,
)]
pub perp_market: AccountLoader<'info, PerpMarket>,
#[account(mut)]
pub asks: AccountLoader<'info, BookSide>,
#[account(mut)]
pub bids: AccountLoader<'info, BookSide>,
pub orderbook: AccountLoader<'info, OrderBook>,
#[account(mut)]
pub event_queue: AccountLoader<'info, EventQueue>,
@ -38,43 +35,10 @@ pub struct PerpPlaceOrder<'info> {
// TODO
#[allow(clippy::too_many_arguments)]
pub fn perp_place_order(
ctx: Context<PerpPlaceOrder>,
side: Side,
pub fn perp_place_order(ctx: Context<PerpPlaceOrder>, order: Order, limit: u8) -> Result<()> {
require_gte!(order.max_base_lots, 0);
require_gte!(order.max_quote_lots, 0);
// Price in quote lots per base lots.
//
// Effect is based on order type, it's usually
// - fill orders on the book up to this price or
// - place an order on the book at this price.
//
// Ignored for Market orders and potentially adjusted for PostOnlySlide orders.
price_lots: i64,
// Max base lots to buy/sell.
max_base_lots: i64,
// Max quote lots to pay/receive (not taking fees into account).
max_quote_lots: i64,
// Arbitrary user-controlled order id.
client_order_id: u64,
order_type: OrderType,
// Timestamp of when order expires
//
// Send 0 if you want the order to never expire.
// Timestamps in the past mean the instruction is skipped.
// Timestamps in the future are reduced to now + 255s.
expiry_timestamp: u64,
// Maximum number of orders from the book to fill.
//
// Use this to limit compute used during order matching.
// When the limit is reached, processing stops and the instruction succeeds.
limit: u8,
) -> Result<()> {
let mut account = ctx.accounts.account.load_mut()?;
require!(
account.fixed.is_owner_or_delegate(ctx.accounts.owner.key()),
@ -111,9 +75,7 @@ pub fn perp_place_order(
};
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
let bids = ctx.accounts.bids.load_mut()?;
let asks = ctx.accounts.asks.load_mut()?;
let mut book = Book::new(bids, asks);
let mut book = ctx.accounts.orderbook.load_mut()?;
let mut event_queue = ctx.accounts.event_queue.load_mut()?;
@ -121,35 +83,16 @@ pub fn perp_place_order(
perp_market.oracle_price(&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?)?;
let now_ts = Clock::get()?.unix_timestamp as u64;
let time_in_force = if expiry_timestamp != 0 {
// If expiry is far in the future, clamp to 255 seconds
let tif = expiry_timestamp.saturating_sub(now_ts).min(255);
if tif == 0 {
// If expiry is in the past, ignore the order
msg!("Order is already expired");
return Ok(());
}
tif as u8
} else {
// Never expire
0
};
// TODO reduce_only based on event queue
book.new_order(
side,
order,
&mut perp_market,
&mut event_queue,
oracle_price,
&mut account.borrow_mut(),
&account_pk,
price_lots,
max_base_lots,
max_quote_lots,
order_type,
time_in_force,
client_order_id,
now_ts,
limit,
)?;

View File

@ -1,7 +1,7 @@
use anchor_lang::prelude::*;
use crate::accounts_zerocopy::*;
use crate::state::{Book, BookSide, Group, PerpMarket};
use crate::state::{Group, OrderBook, PerpMarket};
use crate::logs::PerpUpdateFundingLog;
@ -11,16 +11,13 @@ pub struct PerpUpdateFunding<'info> {
#[account(
mut,
has_one = bids,
has_one = asks,
has_one = orderbook,
has_one = oracle,
constraint = perp_market.load()?.group.key() == group.key(),
)]
pub perp_market: AccountLoader<'info, PerpMarket>,
#[account(mut)]
pub asks: AccountLoader<'info, BookSide>,
#[account(mut)]
pub bids: AccountLoader<'info, BookSide>,
pub orderbook: AccountLoader<'info, OrderBook>,
/// CHECK: The oracle can be one of several different account types and the pubkey is checked above
pub oracle: UncheckedAccount<'info>,
@ -30,9 +27,7 @@ pub fn perp_update_funding(ctx: Context<PerpUpdateFunding>) -> Result<()> {
let now_ts = Clock::get()?.unix_timestamp;
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
let bids = ctx.accounts.bids.load_mut()?;
let asks = ctx.accounts.asks.load_mut()?;
let book = Book::new(bids, asks);
let book = ctx.accounts.orderbook.load_mut()?;
let oracle_price =
perp_market.oracle_price(&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?)?;

View File

@ -3,6 +3,7 @@ use fixed::types::I80F48;
#[macro_use]
pub mod util;
extern crate core;
extern crate static_assertions;
use anchor_lang::prelude::*;
@ -19,7 +20,7 @@ pub mod serum3_cpi;
pub mod state;
pub mod types;
use state::{OracleConfig, OrderType, PerpMarketIndex, Serum3MarketIndex, Side, TokenIndex};
use state::{OracleConfig, PerpMarketIndex, PlaceOrderType, Serum3MarketIndex, Side, TokenIndex};
declare_id!("m43thNJ58XCjL798ZSq6JGAG1BnWskhdq5or6kcnfsD");
@ -485,8 +486,6 @@ pub mod mango_v4 {
instructions::perp_close_market(ctx)
}
// TODO perp_change_perp_market_params
pub fn perp_deactivate_position(ctx: Context<PerpDeactivatePosition>) -> Result<()> {
instructions::perp_deactivate_position(ctx)
}
@ -495,28 +494,118 @@ pub mod mango_v4 {
pub fn perp_place_order(
ctx: Context<PerpPlaceOrder>,
side: Side,
// The price in lots (quote lots per base lots)
// - fill orders on the book up to this price or
// - place an order on the book at this price.
// - ignored for Market orders and potentially adjusted for PostOnlySlide orders.
price_lots: i64,
max_base_lots: i64,
max_quote_lots: i64,
client_order_id: u64,
order_type: OrderType,
order_type: PlaceOrderType,
// Timestamp of when order expires
//
// Send 0 if you want the order to never expire.
// Timestamps in the past mean the instruction is skipped.
// Timestamps in the future are reduced to now + 255s.
expiry_timestamp: u64,
// Maximum number of orders from the book to fill.
//
// Use this to limit compute used during order matching.
// When the limit is reached, processing stops and the instruction succeeds.
limit: u8,
) -> Result<()> {
instructions::perp_place_order(
ctx,
require_gte!(price_lots, 0);
use crate::state::{Order, OrderParams};
let time_in_force = match Order::tif_from_expiry(expiry_timestamp) {
Some(t) => t,
None => {
msg!("Order is already expired");
return Ok(());
}
};
let order = Order {
side,
price_lots,
max_base_lots,
max_quote_lots,
client_order_id,
order_type,
expiry_timestamp,
limit,
)
time_in_force,
params: match order_type {
PlaceOrderType::Market => OrderParams::Market,
PlaceOrderType::ImmediateOrCancel => OrderParams::ImmediateOrCancel { price_lots },
_ => OrderParams::Fixed {
price_lots,
order_type: order_type.to_post_order_type()?,
},
},
};
instructions::perp_place_order(ctx, order, limit)
}
pub fn perp_cancel_order(ctx: Context<PerpCancelOrder>, order_id: i128) -> Result<()> {
#[allow(clippy::too_many_arguments)]
pub fn perp_place_order_pegged(
ctx: Context<PerpPlaceOrder>,
side: Side,
// The adjustment from the oracle price, in lots (quote lots per base lots).
// Orders on the book may be filled at oracle + adjustment (depends on order type).
price_offset_lots: i64,
// The limit at which the pegged order shall expire.
// May be -1 to denote no peg limit.
//
// Example: An bid pegged to -20 with peg_limit 100 would expire if the oracle hits 121.
peg_limit: i64,
max_base_lots: i64,
max_quote_lots: i64,
client_order_id: u64,
order_type: PlaceOrderType,
// Timestamp of when order expires
//
// Send 0 if you want the order to never expire.
// Timestamps in the past mean the instruction is skipped.
// Timestamps in the future are reduced to now + 255s.
expiry_timestamp: u64,
// Maximum number of orders from the book to fill.
//
// Use this to limit compute used during order matching.
// When the limit is reached, processing stops and the instruction succeeds.
limit: u8,
) -> Result<()> {
require_gte!(peg_limit, -1);
use crate::state::{Order, OrderParams};
let time_in_force = match Order::tif_from_expiry(expiry_timestamp) {
Some(t) => t,
None => {
msg!("Order is already expired");
return Ok(());
}
};
let order = Order {
side,
max_base_lots,
max_quote_lots,
client_order_id,
time_in_force,
params: OrderParams::OraclePegged {
price_offset_lots,
order_type: order_type.to_post_order_type()?,
peg_limit,
},
};
instructions::perp_place_order(ctx, order, limit)
}
pub fn perp_cancel_order(ctx: Context<PerpCancelOrder>, order_id: u128) -> Result<()> {
instructions::perp_cancel_order(ctx, order_id)
}

View File

@ -99,7 +99,7 @@ pub struct FillLog {
pub seq_num: u64, // note: usize same as u64
pub maker: Pubkey,
pub maker_order_id: i128,
pub maker_order_id: u128,
pub maker_client_order_id: u64,
pub maker_fee: i128,
@ -107,7 +107,7 @@ pub struct FillLog {
pub maker_timestamp: u64,
pub taker: Pubkey,
pub taker_order_id: i128,
pub taker_order_id: u128,
pub taker_client_order_id: u64,
pub taker_fee: i128,

View File

@ -18,7 +18,7 @@ use crate::state::{
use crate::util::checked_math as cm;
#[cfg(feature = "client")]
use crate::state::orderbook::order_type::Side as PerpOrderSide;
use crate::state::orderbook::Side as PerpOrderSide;
use super::MangoAccountRef;

View File

@ -13,17 +13,18 @@ use crate::error::MangoError;
use crate::error_msg;
use super::dynamic_account::*;
use super::BookSideOrderTree;
use super::FillEvent;
use super::LeafNode;
use super::PerpMarket;
use super::PerpMarketIndex;
use super::PerpOpenOrder;
use super::Serum3MarketIndex;
use super::Side;
use super::TokenIndex;
use super::FREE_ORDER_SLOT;
use super::{HealthCache, HealthType};
use super::{PerpPosition, Serum3Orders, TokenPosition};
use super::{Side, SideAndOrderTree};
use crate::logs::{DeactivatePerpPositionLog, DeactivateTokenPositionLog};
use checked_math as cm;
@ -500,7 +501,7 @@ impl<
pub fn perp_next_order_slot(&self) -> Result<usize> {
self.all_perp_orders()
.position(|&oo| oo.order_market == FREE_ORDER_SLOT)
.position(|&oo| oo.market == FREE_ORDER_SLOT)
.ok_or_else(|| error_msg!("no free perp order index"))
}
@ -508,23 +509,23 @@ impl<
&self,
market_index: PerpMarketIndex,
client_order_id: u64,
) -> Option<(i128, Side)> {
) -> Option<&PerpOpenOrder> {
for oo in self.all_perp_orders() {
if oo.order_market == market_index && oo.client_order_id == client_order_id {
return Some((oo.order_id, oo.order_side));
if oo.market == market_index && oo.client_id == client_order_id {
return Some(&oo);
}
}
None
}
pub fn perp_find_order_side(
pub fn perp_find_order_with_order_id(
&self,
market_index: PerpMarketIndex,
order_id: i128,
) -> Option<Side> {
order_id: u128,
) -> Option<&PerpOpenOrder> {
for oo in self.all_perp_orders() {
if oo.order_market == market_index && oo.order_id == order_id {
return Some(oo.order_side);
if oo.market == market_index && oo.id == order_id {
return Some(&oo);
}
}
None
@ -796,6 +797,7 @@ impl<
&mut self,
perp_market_index: PerpMarketIndex,
side: Side,
order_tree: BookSideOrderTree,
order: &LeafNode,
) -> Result<()> {
let mut perp_account = self.perp_position_mut(perp_market_index)?;
@ -810,19 +812,19 @@ impl<
let slot = order.owner_slot as usize;
let mut oo = self.perp_order_mut_by_raw_index(slot);
oo.order_market = perp_market_index;
oo.order_side = side;
oo.order_id = order.key;
oo.client_order_id = order.client_order_id;
oo.market = perp_market_index;
oo.side_and_tree = SideAndOrderTree::new(side, order_tree);
oo.id = order.key;
oo.client_id = order.client_order_id;
Ok(())
}
pub fn remove_perp_order(&mut self, slot: usize, quantity: i64) -> Result<()> {
{
let oo = self.perp_order_mut_by_raw_index(slot);
require_neq!(oo.order_market, FREE_ORDER_SLOT);
let order_side = oo.order_side;
let perp_market_index = oo.order_market;
require_neq!(oo.market, FREE_ORDER_SLOT);
let order_side = oo.side_and_tree.side();
let perp_market_index = oo.market;
let perp_account = self.perp_position_mut(perp_market_index)?;
// accounting
@ -838,10 +840,10 @@ impl<
// release space
let oo = self.perp_order_mut_by_raw_index(slot);
oo.order_market = FREE_ORDER_SLOT;
oo.order_side = Side::Bid;
oo.order_id = 0i128;
oo.client_order_id = 0u64;
oo.market = FREE_ORDER_SLOT;
oo.side_and_tree = SideAndOrderTree::BidFixed;
oo.id = 0;
oo.client_id = 0;
Ok(())
}

View File

@ -395,24 +395,24 @@ impl PerpPosition {
#[zero_copy]
#[derive(AnchorSerialize, AnchorDeserialize, Debug)]
pub struct PerpOpenOrder {
pub order_side: Side, // TODO: storing enums isn't POD
pub side_and_tree: SideAndOrderTree, // TODO: storing enums isn't POD
pub padding1: [u8; 1],
pub order_market: PerpMarketIndex,
pub market: PerpMarketIndex,
pub padding2: [u8; 4],
pub client_order_id: u64,
pub order_id: i128,
pub client_id: u64,
pub id: u128,
pub reserved: [u8; 64],
}
impl Default for PerpOpenOrder {
fn default() -> Self {
Self {
order_side: Side::Bid,
side_and_tree: SideAndOrderTree::BidFixed,
padding1: Default::default(),
order_market: FREE_ORDER_SLOT,
market: FREE_ORDER_SLOT,
padding2: Default::default(),
client_order_id: 0,
order_id: 0,
client_id: 0,
id: 0,
reserved: [0; 64],
}
}

View File

@ -1,250 +1,163 @@
use std::cell::RefMut;
use crate::accounts_zerocopy::*;
use crate::state::MangoAccountRefMut;
use crate::{
error::*,
state::{
orderbook::{bookside::BookSide, nodes::LeafNode},
EventQueue, PerpMarket, FREE_ORDER_SLOT,
},
state::{orderbook::bookside::*, EventQueue, PerpMarket, FREE_ORDER_SLOT},
};
use anchor_lang::prelude::*;
use bytemuck::cast;
use fixed::types::I80F48;
use static_assertions::const_assert_eq;
use super::{
nodes::NodeHandle,
order_type::{OrderType, Side},
FillEvent, OutEvent,
};
use super::*;
use crate::util::checked_math as cm;
/// Drop at most this many expired orders from a BookSide when trying to match orders.
/// This exists as a guard against excessive compute use.
const DROP_EXPIRED_ORDER_LIMIT: usize = 5;
/// The implicit limit price to use for market orders
fn market_order_limit_for_side(side: Side) -> i64 {
match side {
Side::Bid => i64::MAX,
Side::Ask => 1,
}
#[account(zero_copy)]
pub struct OrderBook {
pub bids: BookSide,
pub asks: BookSide,
}
const_assert_eq!(
std::mem::size_of::<OrderBook>(),
2 * std::mem::size_of::<BookSide>()
);
const_assert_eq!(std::mem::size_of::<OrderBook>() % 8, 0);
/// The limit to use for PostOnlySlide orders: the tinyest bit better than
/// the best opposing order
fn post_only_slide_limit(side: Side, best_other_side: i64, limit: i64) -> i64 {
match side {
Side::Bid => limit.min(cm!(best_other_side - 1)),
Side::Ask => limit.max(cm!(best_other_side + 1)),
}
}
pub struct Book<'a> {
pub bids: RefMut<'a, BookSide>, // todo: why refmut?
pub asks: RefMut<'a, BookSide>,
}
impl<'a> Book<'a> {
pub fn new(bids: RefMut<'a, BookSide>, asks: RefMut<'a, BookSide>) -> Self {
Self { bids, asks }
impl OrderBook {
pub fn init(&mut self) {
self.bids.fixed.order_tree_type = OrderTreeType::Bids;
self.bids.oracle_pegged.order_tree_type = OrderTreeType::Bids;
self.asks.fixed.order_tree_type = OrderTreeType::Asks;
self.asks.oracle_pegged.order_tree_type = OrderTreeType::Asks;
}
pub fn load_mut(
bids_ai: &'a AccountInfo,
asks_ai: &'a AccountInfo,
perp_market: &PerpMarket,
) -> std::result::Result<Self, Error> {
require!(bids_ai.key == &perp_market.bids, MangoError::SomeError);
require!(asks_ai.key == &perp_market.asks, MangoError::SomeError);
Ok(Self::new(
bids_ai.load_mut::<BookSide>()?,
asks_ai.load_mut::<BookSide>()?,
))
}
pub fn bookside(&mut self, side: Side) -> &mut BookSide {
pub fn bookside_mut(&mut self, side: Side) -> &mut BookSide {
match side {
Side::Bid => &mut self.bids,
Side::Ask => &mut self.asks,
}
}
/// Returns best valid bid
pub fn best_bid_price(&self, now_ts: u64) -> Option<i64> {
Some(self.bids.iter_valid(now_ts).next()?.1.price())
}
/// Returns best valid ask
pub fn best_ask_price(&self, now_ts: u64) -> Option<i64> {
Some(self.asks.iter_valid(now_ts).next()?.1.price())
}
pub fn best_price(&self, now_ts: u64, side: Side) -> Option<i64> {
pub fn bookside(&self, side: Side) -> &BookSide {
match side {
Side::Bid => self.best_bid_price(now_ts),
Side::Ask => self.best_ask_price(now_ts),
Side::Bid => &self.bids,
Side::Ask => &self.asks,
}
}
/// Get the quantity of valid bids above and including the price
pub fn bids_size_above(&self, price: i64, max_depth: i64, now_ts: u64) -> i64 {
let mut sum: i64 = 0;
for (_, bid) in self.bids.iter_valid(now_ts) {
if price > bid.price() || sum >= max_depth {
break;
}
sum = sum.checked_add(bid.quantity).unwrap();
}
sum.min(max_depth)
pub fn best_price(&self, now_ts: u64, oracle_price_lots: i64, side: Side) -> Option<i64> {
Some(
self.bookside(side)
.iter_valid(now_ts, oracle_price_lots)
.next()?
.price_lots,
)
}
/// Walk up the book `quantity` units and return the price at that level. If `quantity` units
/// not on book, return None
pub fn impact_price(&self, side: Side, quantity: i64, now_ts: u64) -> Option<i64> {
pub fn impact_price(
&self,
side: Side,
quantity: i64,
now_ts: u64,
oracle_price_lots: i64,
) -> Option<i64> {
let mut sum: i64 = 0;
let book_side = match side {
Side::Bid => self.bids.iter_valid(now_ts),
Side::Ask => self.asks.iter_valid(now_ts),
};
for (_, order) in book_side {
sum = sum.checked_add(order.quantity).unwrap();
let bookside = self.bookside(side);
let iter = bookside.iter_valid(now_ts, oracle_price_lots);
for order in iter {
cm!(sum += order.node.quantity);
if sum >= quantity {
return Some(order.price());
return Some(order.price_lots);
}
}
None
}
/// Get the quantity of valid asks below and including the price
pub fn asks_size_below(&self, price: i64, max_depth: i64, now_ts: u64) -> i64 {
let mut s = 0;
for (_, ask) in self.asks.iter_valid(now_ts) {
if price < ask.price() || s >= max_depth {
break;
}
s += ask.quantity;
}
s.min(max_depth)
}
/// Get the quantity of valid bids above this order id. Will return full size of book if order id not found
pub fn bids_size_above_order(&self, order_id: i128, max_depth: i64, now_ts: u64) -> i64 {
let mut s = 0;
for (_, bid) in self.bids.iter_valid(now_ts) {
if bid.key == order_id || s >= max_depth {
break;
}
s += bid.quantity;
}
s.min(max_depth)
}
/// Get the quantity of valid asks above this order id. Will return full size of book if order id not found
pub fn asks_size_below_order(&self, order_id: i128, max_depth: i64, now_ts: u64) -> i64 {
let mut s = 0;
for (_, ask) in self.asks.iter_valid(now_ts) {
if ask.key == order_id || s >= max_depth {
break;
}
s += ask.quantity;
}
s.min(max_depth)
}
#[allow(clippy::too_many_arguments)]
pub fn new_order(
&mut self,
side: Side,
order: Order,
perp_market: &mut PerpMarket,
event_queue: &mut EventQueue,
oracle_price: I80F48,
mango_account: &mut MangoAccountRefMut,
mango_account_pk: &Pubkey,
price_lots: i64,
max_base_lots: i64,
max_quote_lots: i64,
order_type: OrderType,
time_in_force: u8,
client_order_id: u64,
now_ts: u64,
mut limit: u8,
) -> std::result::Result<(), Error> {
let side = order.side;
let other_side = side.invert_side();
let market = perp_market;
let (post_only, mut post_allowed, price_lots) = match order_type {
OrderType::Limit => (false, true, price_lots),
OrderType::ImmediateOrCancel => (false, false, price_lots),
OrderType::PostOnly => (true, true, price_lots),
OrderType::Market => (false, false, market_order_limit_for_side(side)),
OrderType::PostOnlySlide => {
let price = if let Some(best_other_price) = self.best_price(now_ts, other_side) {
post_only_slide_limit(side, best_other_price, price_lots)
} else {
price_lots
};
(true, true, price)
}
};
let oracle_price_lots = market.native_price_to_lot(oracle_price);
let post_only = order.is_post_only();
let mut post_target = order.post_target();
let (price_lots, price_data) = order.price(now_ts, oracle_price_lots, self)?;
if post_allowed {
if post_target.is_some() {
// price limit check computed lazily to save CU on average
let native_price = market.lot_to_native_price(price_lots);
if !market.inside_price_limit(side, native_price, oracle_price) {
msg!("Posting on book disallowed due to price limits");
post_allowed = false;
post_target = None;
}
}
// generate new order id
let order_id = market.gen_order_id(side, price_lots);
let order_id = market.gen_order_id(side, price_data);
// Iterate through book and match against this new order.
//
// Any changes to matching orders on the other side of the book are collected in
// matched_changes/matched_deletes and then applied after this loop.
let mut remaining_base_lots = max_base_lots;
let mut remaining_quote_lots = max_quote_lots;
let mut matched_order_changes: Vec<(NodeHandle, i64)> = vec![];
let mut matched_order_deletes: Vec<i128> = vec![];
let mut remaining_base_lots = order.max_base_lots;
let mut remaining_quote_lots = order.max_quote_lots;
let mut matched_order_changes: Vec<(BookSideOrderHandle, i64)> = vec![];
let mut matched_order_deletes: Vec<(BookSideOrderTree, u128)> = vec![];
let mut number_of_dropped_expired_orders = 0;
let opposing_bookside = self.bookside(other_side);
for (best_opposing_h, best_opposing) in opposing_bookside.iter_all_including_invalid() {
if !best_opposing.is_valid(now_ts) {
let opposing_bookside = self.bookside_mut(other_side);
for best_opposing in opposing_bookside.iter_all_including_invalid(now_ts, oracle_price_lots)
{
if !best_opposing.is_valid {
// Remove the order from the book unless we've done that enough
if number_of_dropped_expired_orders < DROP_EXPIRED_ORDER_LIMIT {
number_of_dropped_expired_orders += 1;
let event = OutEvent::new(
other_side,
best_opposing.owner_slot,
best_opposing.node.owner_slot,
now_ts,
event_queue.header.seq_num,
best_opposing.owner,
best_opposing.quantity,
best_opposing.node.owner,
best_opposing.node.quantity,
);
event_queue.push_back(cast(event)).unwrap();
matched_order_deletes.push(best_opposing.key);
matched_order_deletes
.push((best_opposing.handle.order_tree, best_opposing.node.key));
}
continue;
}
let best_opposing_price = best_opposing.price();
let best_opposing_price = best_opposing.price_lots;
if !side.is_price_within_limit(best_opposing_price, price_lots) {
break;
} else if post_only {
msg!("Order could not be placed due to PostOnly");
post_allowed = false;
post_target = None;
break; // return silently to not fail other instructions in tx
} else if limit == 0 {
msg!("Order matching limit reached");
post_allowed = false;
post_target = None;
break;
}
let max_match_by_quote = remaining_quote_lots / best_opposing_price;
let match_base_lots = remaining_base_lots
.min(best_opposing.quantity)
.min(best_opposing.node.quantity)
.min(max_match_by_quote);
let done =
match_base_lots == max_match_by_quote || match_base_lots == remaining_base_lots;
@ -253,12 +166,13 @@ impl<'a> Book<'a> {
cm!(remaining_base_lots -= match_base_lots);
cm!(remaining_quote_lots -= match_quote_lots);
let new_best_opposing_quantity = cm!(best_opposing.quantity - match_base_lots);
let new_best_opposing_quantity = cm!(best_opposing.node.quantity - match_base_lots);
let maker_out = new_best_opposing_quantity == 0;
if maker_out {
matched_order_deletes.push(best_opposing.key);
matched_order_deletes
.push((best_opposing.handle.order_tree, best_opposing.node.key));
} else {
matched_order_changes.push((best_opposing_h, new_best_opposing_quantity));
matched_order_changes.push((best_opposing.handle, new_best_opposing_quantity));
}
// Record the taker trade in the account already, even though it will only be
@ -269,17 +183,17 @@ impl<'a> Book<'a> {
let fill = FillEvent::new(
side,
maker_out,
best_opposing.owner_slot,
best_opposing.node.owner_slot,
now_ts,
event_queue.header.seq_num,
best_opposing.owner,
best_opposing.key,
best_opposing.client_order_id,
best_opposing.node.owner,
best_opposing.node.key,
best_opposing.node.client_order_id,
market.maker_fee,
best_opposing.timestamp,
best_opposing.node.timestamp,
*mango_account_pk,
order_id,
client_order_id,
order.client_order_id,
market.taker_fee,
best_opposing_price,
match_base_lots,
@ -291,7 +205,7 @@ impl<'a> Book<'a> {
break;
}
}
let total_quote_lots_taken = cm!(max_quote_lots - remaining_quote_lots);
let total_quote_lots_taken = cm!(order.max_quote_lots - remaining_quote_lots);
// Apply changes to matched asks (handles invalidate on delete!)
for (handle, new_quantity) in matched_order_changes {
@ -302,17 +216,21 @@ impl<'a> Book<'a> {
.unwrap()
.quantity = new_quantity;
}
for key in matched_order_deletes {
let _removed_leaf = opposing_bookside.remove_by_key(key).unwrap();
for (component, key) in matched_order_deletes {
let _removed_leaf = opposing_bookside.remove_by_key(component, key).unwrap();
}
// If there are still quantity unmatched, place on the book
let book_base_quantity = remaining_base_lots.min(remaining_quote_lots / price_lots);
msg!("{:?}", post_allowed);
if post_allowed && book_base_quantity > 0 {
if book_base_quantity <= 0 {
post_target = None;
}
if let Some(order_tree_target) = post_target {
let bookside = self.bookside_mut(side);
let order_tree = bookside.orders_mut(order_tree_target);
// Drop an expired order if possible
let bookside = self.bookside(side);
if let Some(expired_order) = bookside.remove_one_expired(now_ts) {
if let Some(expired_order) = order_tree.remove_one_expired(now_ts) {
let event = OutEvent::new(
side,
expired_order.owner_slot,
@ -324,12 +242,12 @@ impl<'a> Book<'a> {
event_queue.push_back(cast(event)).unwrap();
}
if bookside.is_full() {
if order_tree.is_full() {
// If this bid is higher than lowest bid, boot that bid and insert this one
let worst_order = bookside.remove_worst().unwrap();
let worst_order = order_tree.remove_worst().unwrap();
// MangoErrorCode::OutOfSpace
require!(
side.is_price_better(price_lots, worst_order.price()),
side.is_price_data_better(price_data, worst_order.price_data()),
MangoError::SomeError
);
let event = OutEvent::new(
@ -349,12 +267,13 @@ impl<'a> Book<'a> {
order_id,
*mango_account_pk,
book_base_quantity,
client_order_id,
order.client_order_id,
now_ts,
order_type,
time_in_force,
PostOrderType::Limit, // TODO: Support order types? needed?
order.time_in_force,
order.peg_limit(),
);
let _result = bookside.insert_leaf(&new_order)?;
let _result = order_tree.insert_leaf(&new_order)?;
// TODO OPT remove if PlacePerpOrder needs more compute
msg!(
@ -368,7 +287,12 @@ impl<'a> Book<'a> {
price_lots
);
mango_account.add_perp_order(market.perp_market_index, side, &new_order)?;
mango_account.add_perp_order(
market.perp_market_index,
side,
order_tree_target,
&new_order,
)?;
}
// if there were matched taker quote apply ref fees
@ -378,7 +302,7 @@ impl<'a> Book<'a> {
}
// IOC orders have a fee penalty applied regardless of match
if order_type == OrderType::ImmediateOrCancel {
if order.needs_penalty_fee() {
apply_penalty(market, mango_account)?;
}
@ -397,21 +321,20 @@ impl<'a> Book<'a> {
) -> Result<()> {
for i in 0..mango_account.header.perp_oo_count() {
let oo = mango_account.perp_order_by_raw_index(i);
if oo.order_market == FREE_ORDER_SLOT
|| oo.order_market != perp_market.perp_market_index
{
if oo.market == FREE_ORDER_SLOT || oo.market != perp_market.perp_market_index {
continue;
}
let order_side = oo.order_side;
let order_side_and_tree = oo.side_and_tree;
if let Some(side_to_cancel) = side_to_cancel_option {
if side_to_cancel != order_side {
if side_to_cancel != order_side_and_tree.side() {
continue;
}
}
let order_id = oo.order_id;
self.cancel_order(mango_account, order_id, order_side, None)?;
let order_id = oo.id;
self.cancel_order(mango_account, order_id, order_side_and_tree, None)?;
limit -= 1;
if limit == 0 {
@ -426,19 +349,16 @@ impl<'a> Book<'a> {
pub fn cancel_order(
&mut self,
mango_account: &mut MangoAccountRefMut,
order_id: i128,
side: Side,
order_id: u128,
side_and_tree: SideAndOrderTree,
expected_owner: Option<Pubkey>,
) -> Result<LeafNode> {
let leaf_node =
match side {
Side::Bid => self.bids.remove_by_key(order_id).ok_or_else(|| {
error_msg!("invalid perp order id {order_id} for side {side:?}")
}),
Side::Ask => self.asks.remove_by_key(order_id).ok_or_else(|| {
error_msg!("invalid perp order id {order_id} for side {side:?}")
}),
}?;
let side = side_and_tree.side();
let book_component = side_and_tree.order_tree();
let leaf_node = self.bookside_mut(side).orders_mut(book_component).
remove_by_key(order_id).ok_or_else(|| {
error_msg!("invalid perp order id {order_id} for side {side:?} and component {book_component:?}")
})?;
if let Some(owner) = expected_owner {
require_keys_eq!(leaf_node.owner, owner);
}

View File

@ -1,17 +1,7 @@
use anchor_lang::prelude::*;
use bytemuck::{cast, cast_mut, cast_ref};
use num_enum::{IntoPrimitive, TryFromPrimitive};
use static_assertions::const_assert_eq;
use crate::state::orderbook::bookside_iterator::BookSideIter;
use crate::error::MangoError;
use crate::state::orderbook::nodes::{
AnyNode, FreeNode, InnerNode, LeafNode, NodeHandle, NodeRef, NodeTag,
};
pub const MAX_BOOK_NODES: usize = 1024;
use super::*;
#[derive(
Eq,
@ -25,619 +15,261 @@ pub const MAX_BOOK_NODES: usize = 1024;
AnchorDeserialize,
)]
#[repr(u8)]
pub enum BookSideType {
Bids,
Asks,
pub enum BookSideOrderTree {
Fixed,
OraclePegged,
}
/// Reference to a node in a book side component
pub struct BookSideOrderHandle {
pub node: NodeHandle,
pub order_tree: BookSideOrderTree,
}
/// A binary tree on AnyNode::key()
///
/// The key encodes the price in the top 64 bits.
#[account(zero_copy)]
pub struct BookSide {
// pub meta_data: MetaData,
// todo: do we want this type at this level?
pub book_side_type: BookSideType,
pub padding: [u8; 3],
pub bump_index: u32,
pub free_list_len: u32,
pub free_list_head: NodeHandle,
pub root_node: NodeHandle,
pub leaf_count: u32,
pub nodes: [AnyNode; MAX_BOOK_NODES],
pub reserved: [u8; 256],
pub fixed: OrderTree,
pub oracle_pegged: OrderTree,
}
const_assert_eq!(
std::mem::size_of::<BookSide>(),
1 + 3 + 4 * 2 + 4 + 4 + 4 + 96 * 1024 + 256 // 98584
);
const_assert_eq!(std::mem::size_of::<BookSide>() % 8, 0);
impl BookSide {
/// Iterate over all entries in the book filtering out invalid orders
///
/// smallest to highest for asks
/// highest to smallest for bids
pub fn iter_valid(&self, now_ts: u64) -> BookSideIter {
BookSideIter::new(self, now_ts)
pub fn iter_valid(
&self,
now_ts: u64,
oracle_price_lots: i64,
) -> impl Iterator<Item = BookSideIterItem> {
BookSideIter::new(self, now_ts, oracle_price_lots).filter(|it| it.is_valid)
}
/// Iterate over all entries, including invalid orders
pub fn iter_all_including_invalid(&self) -> BookSideIter {
BookSideIter::new(self, 0)
pub fn iter_all_including_invalid(&self, now_ts: u64, oracle_price_lots: i64) -> BookSideIter {
BookSideIter::new(self, now_ts, oracle_price_lots)
}
pub fn node_mut(&mut self, key: NodeHandle) -> Option<&mut AnyNode> {
let node = &mut self.nodes[key as usize];
let tag = NodeTag::try_from(node.tag);
match tag {
Ok(NodeTag::InnerNode) | Ok(NodeTag::LeafNode) => Some(node),
_ => None,
}
}
pub fn node(&self, key: NodeHandle) -> Option<&AnyNode> {
let node = &self.nodes[key as usize];
let tag = NodeTag::try_from(node.tag);
match tag {
Ok(NodeTag::InnerNode) | Ok(NodeTag::LeafNode) => Some(node),
_ => None,
pub fn orders(&self, component: BookSideOrderTree) -> &OrderTree {
match component {
BookSideOrderTree::Fixed => &self.fixed,
BookSideOrderTree::OraclePegged => &self.oracle_pegged,
}
}
pub fn remove_min(&mut self) -> Option<LeafNode> {
self.remove_by_key(self.min_leaf()?.key)
}
pub fn remove_max(&mut self) -> Option<LeafNode> {
self.remove_by_key(self.max_leaf()?.key)
}
pub fn remove_worst(&mut self) -> Option<LeafNode> {
match self.book_side_type {
BookSideType::Bids => self.remove_min(),
BookSideType::Asks => self.remove_max(),
pub fn orders_mut(&mut self, component: BookSideOrderTree) -> &mut OrderTree {
match component {
BookSideOrderTree::Fixed => &mut self.fixed,
BookSideOrderTree::OraclePegged => &mut self.oracle_pegged,
}
}
pub fn node(&self, key: BookSideOrderHandle) -> Option<&AnyNode> {
self.orders(key.order_tree).node(key.node)
}
pub fn node_mut(&mut self, key: BookSideOrderHandle) -> Option<&mut AnyNode> {
self.orders_mut(key.order_tree).node_mut(key.node)
}
pub fn is_full(&self, component: BookSideOrderTree) -> bool {
self.orders(component).is_full()
}
pub fn remove_worst(&mut self, component: BookSideOrderTree) -> Option<LeafNode> {
self.orders_mut(component).remove_worst()
}
/// Remove the order with the lowest expiry timestamp, if that's < now_ts.
pub fn remove_one_expired(&mut self, now_ts: u64) -> Option<LeafNode> {
let (expired_h, expires_at) = self.find_earliest_expiry()?;
if expires_at < now_ts {
self.remove_by_key(self.node(expired_h)?.key()?)
} else {
None
}
}
pub fn root(&self) -> Option<NodeHandle> {
if self.leaf_count == 0 {
None
} else {
Some(self.root_node)
}
}
#[cfg(test)]
#[allow(dead_code)]
fn to_price_quantity_vec(&self, reverse: bool) -> Vec<(i64, i64)> {
let mut pqs = vec![];
let mut current: NodeHandle = match self.root() {
None => return pqs,
Some(node_handle) => node_handle,
};
let left = reverse as usize;
let right = !reverse as usize;
let mut stack = vec![];
loop {
let root_contents = self.node(current).unwrap(); // should never fail unless book is already fucked
match root_contents.case().unwrap() {
NodeRef::Inner(inner) => {
stack.push(inner);
current = inner.children[left];
}
NodeRef::Leaf(leaf) => {
// if you hit leaf then pop stack and go right
// all inner nodes on stack have already been visited to the left
pqs.push((leaf.price(), leaf.quantity));
match stack.pop() {
None => return pqs,
Some(inner) => {
current = inner.children[right];
}
}
}
}
}
}
pub fn min_leaf(&self) -> Option<&LeafNode> {
self.leaf_min_max(false)
}
pub fn max_leaf(&self) -> Option<&LeafNode> {
self.leaf_min_max(true)
}
fn leaf_min_max(&self, find_max: bool) -> Option<&LeafNode> {
let mut root: NodeHandle = self.root()?;
let i = if find_max { 1 } else { 0 };
loop {
let root_contents = self.node(root)?;
match root_contents.case()? {
NodeRef::Inner(inner) => {
root = inner.children[i];
}
NodeRef::Leaf(leaf) => {
return Some(leaf);
}
}
}
}
pub fn remove_by_key(&mut self, search_key: i128) -> Option<LeafNode> {
// path of InnerNode handles that lead to the removed leaf
let mut stack: Vec<(NodeHandle, bool)> = vec![];
// special case potentially removing the root
let mut parent_h = self.root()?;
let (mut child_h, mut crit_bit) = match self.node(parent_h).unwrap().case().unwrap() {
NodeRef::Leaf(&leaf) if leaf.key == search_key => {
assert_eq!(self.leaf_count, 1);
self.root_node = 0;
self.leaf_count = 0;
let _old_root = self.remove(parent_h).unwrap();
return Some(leaf);
}
NodeRef::Leaf(_) => return None,
NodeRef::Inner(inner) => inner.walk_down(search_key),
};
stack.push((parent_h, crit_bit));
// walk down the tree until finding the key
loop {
match self.node(child_h).unwrap().case().unwrap() {
NodeRef::Inner(inner) => {
parent_h = child_h;
let (new_child_h, new_crit_bit) = inner.walk_down(search_key);
child_h = new_child_h;
crit_bit = new_crit_bit;
stack.push((parent_h, crit_bit));
}
NodeRef::Leaf(leaf) => {
if leaf.key != search_key {
return None;
}
break;
}
}
}
// replace parent with its remaining child node
// free child_h, replace *parent_h with *other_child_h, free other_child_h
let other_child_h = self.node(parent_h).unwrap().children().unwrap()[!crit_bit as usize];
let other_child_node_contents = self.remove(other_child_h).unwrap();
let new_expiry = other_child_node_contents.earliest_expiry();
*self.node_mut(parent_h).unwrap() = other_child_node_contents;
self.leaf_count -= 1;
let removed_leaf: LeafNode = cast(self.remove(child_h).unwrap());
// update child min expiry back up to the root
let outdated_expiry = removed_leaf.expiry();
stack.pop(); // the final parent has been replaced by the remaining leaf
self.update_parent_earliest_expiry(&stack, outdated_expiry, new_expiry);
Some(removed_leaf)
}
pub fn remove(&mut self, key: NodeHandle) -> Option<AnyNode> {
let val = *self.node(key)?;
self.nodes[key as usize] = cast(FreeNode {
tag: if self.free_list_len == 0 {
NodeTag::LastFreeNode.into()
} else {
NodeTag::FreeNode.into()
},
next: self.free_list_head,
reserved: [0; 88],
});
self.free_list_len += 1;
self.free_list_head = key;
Some(val)
}
pub fn insert(&mut self, val: &AnyNode) -> Result<NodeHandle> {
match NodeTag::try_from(val.tag) {
Ok(NodeTag::InnerNode) | Ok(NodeTag::LeafNode) => (),
_ => unreachable!(),
};
if self.free_list_len == 0 {
require!(
(self.bump_index as usize) < self.nodes.len() && self.bump_index < u32::MAX,
MangoError::SomeError // todo
);
self.nodes[self.bump_index as usize] = *val;
let key = self.bump_index;
self.bump_index += 1;
return Ok(key);
}
let key = self.free_list_head;
let node = &mut self.nodes[key as usize];
// TODO OPT possibly unnecessary require here - remove if we need compute
match NodeTag::try_from(node.tag) {
Ok(NodeTag::FreeNode) => assert!(self.free_list_len > 1),
Ok(NodeTag::LastFreeNode) => assert_eq!(self.free_list_len, 1),
_ => unreachable!(),
};
// TODO - test borrow requireer
self.free_list_head = cast_ref::<AnyNode, FreeNode>(node).next;
self.free_list_len -= 1;
*node = *val;
Ok(key)
}
pub fn insert_leaf(&mut self, new_leaf: &LeafNode) -> Result<(NodeHandle, Option<LeafNode>)> {
// path of InnerNode handles that lead to the new leaf
let mut stack: Vec<(NodeHandle, bool)> = vec![];
// deal with inserts into an empty tree
let mut root: NodeHandle = match self.root() {
Some(h) => h,
None => {
// create a new root if none exists
let handle = self.insert(new_leaf.as_ref())?;
self.root_node = handle;
self.leaf_count = 1;
return Ok((handle, None));
}
};
// walk down the tree until we find the insert location
loop {
// require if the new node will be a child of the root
let root_contents = *self.node(root).unwrap();
let root_key = root_contents.key().unwrap();
if root_key == new_leaf.key {
// This should never happen because key should never match
if let Some(NodeRef::Leaf(&old_root_as_leaf)) = root_contents.case() {
// clobber the existing leaf
*self.node_mut(root).unwrap() = *new_leaf.as_ref();
self.update_parent_earliest_expiry(
&stack,
old_root_as_leaf.expiry(),
new_leaf.expiry(),
);
return Ok((root, Some(old_root_as_leaf)));
}
// InnerNodes have a random child's key, so matching can happen and is fine
}
let shared_prefix_len: u32 = (root_key ^ new_leaf.key).leading_zeros();
match root_contents.case() {
None => unreachable!(),
Some(NodeRef::Inner(inner)) => {
let keep_old_root = shared_prefix_len >= inner.prefix_len;
if keep_old_root {
let (child, crit_bit) = inner.walk_down(new_leaf.key);
stack.push((root, crit_bit));
root = child;
continue;
};
}
_ => (),
};
// implies root is a Leaf or Inner where shared_prefix_len < prefix_len
// we'll replace root with a new InnerNode that has new_leaf and root as children
// change the root in place to represent the LCA of [new_leaf] and [root]
let crit_bit_mask: i128 = 1i128 << (127 - shared_prefix_len);
let new_leaf_crit_bit = (crit_bit_mask & new_leaf.key) != 0;
let old_root_crit_bit = !new_leaf_crit_bit;
let new_leaf_handle = self.insert(new_leaf.as_ref())?;
let moved_root_handle = match self.insert(&root_contents) {
Ok(h) => h,
Err(e) => {
self.remove(new_leaf_handle).unwrap();
return Err(e);
}
};
let new_root: &mut InnerNode = cast_mut(self.node_mut(root).unwrap());
*new_root = InnerNode::new(shared_prefix_len, new_leaf.key);
new_root.children[new_leaf_crit_bit as usize] = new_leaf_handle;
new_root.children[old_root_crit_bit as usize] = moved_root_handle;
let new_leaf_expiry = new_leaf.expiry();
let old_root_expiry = root_contents.earliest_expiry();
new_root.child_earliest_expiry[new_leaf_crit_bit as usize] = new_leaf_expiry;
new_root.child_earliest_expiry[old_root_crit_bit as usize] = old_root_expiry;
// walk up the stack and fix up the new min if needed
if new_leaf_expiry < old_root_expiry {
self.update_parent_earliest_expiry(&stack, old_root_expiry, new_leaf_expiry);
}
self.leaf_count += 1;
return Ok((new_leaf_handle, None));
}
}
pub fn is_full(&self) -> bool {
self.free_list_len <= 1 && (self.bump_index as usize) >= self.nodes.len() - 1
}
/// When a node changes, the parents' child_earliest_expiry may need to be updated.
///
/// This function walks up the `stack` of parents and applies the change where the
/// previous child's `outdated_expiry` is replaced by `new_expiry`.
pub fn update_parent_earliest_expiry(
pub fn remove_one_expired(
&mut self,
stack: &[(NodeHandle, bool)],
mut outdated_expiry: u64,
mut new_expiry: u64,
) {
// Walk from the top of the stack to the root of the tree.
// Since the stack grows by appending, we need to iterate the slice in reverse order.
for (parent_h, crit_bit) in stack.iter().rev() {
let parent = self.node_mut(*parent_h).unwrap().as_inner_mut().unwrap();
if parent.child_earliest_expiry[*crit_bit as usize] != outdated_expiry {
break;
}
outdated_expiry = parent.earliest_expiry();
parent.child_earliest_expiry[*crit_bit as usize] = new_expiry;
new_expiry = parent.earliest_expiry();
}
component: BookSideOrderTree,
now_ts: u64,
) -> Option<LeafNode> {
self.orders_mut(component).remove_one_expired(now_ts)
}
/// Returns the handle of the node with the lowest expiry timestamp, and this timestamp
pub fn find_earliest_expiry(&self) -> Option<(NodeHandle, u64)> {
let mut current: NodeHandle = match self.root() {
Some(h) => h,
None => return None,
};
pub fn remove_by_key(
&mut self,
component: BookSideOrderTree,
search_key: u128,
) -> Option<LeafNode> {
self.orders_mut(component).remove_by_key(search_key)
}
loop {
let contents = *self.node(current).unwrap();
match contents.case() {
None => unreachable!(),
Some(NodeRef::Inner(inner)) => {
current = inner.children[(inner.child_earliest_expiry[0]
> inner.child_earliest_expiry[1])
as usize];
}
_ => {
return Some((current, contents.earliest_expiry()));
}
};
}
pub fn remove(&mut self, key: BookSideOrderHandle) -> Option<AnyNode> {
self.orders_mut(key.order_tree).remove(key.node)
}
}
#[cfg(test)]
mod tests {
use super::super::order_type::OrderType;
use super::*;
use bytemuck::Zeroable;
fn new_bookside(book_side_type: BookSideType) -> BookSide {
BookSide {
book_side_type,
fn new_order_tree(order_tree_type: OrderTreeType) -> OrderTree {
OrderTree {
order_tree_type,
padding: [0u8; 3],
bump_index: 0,
free_list_len: 0,
free_list_head: 0,
root_node: 0,
leaf_count: 0,
nodes: [AnyNode::zeroed(); MAX_BOOK_NODES],
nodes: [AnyNode::zeroed(); MAX_ORDERTREE_NODES],
reserved: [0; 256],
}
}
fn verify_bookside(bookside: &BookSide) {
verify_bookside_invariant(bookside);
verify_bookside_iteration(bookside);
verify_bookside_expiry(bookside);
}
// check that BookSide binary tree key invariant holds
fn verify_bookside_invariant(bookside: &BookSide) {
let r = match bookside.root() {
Some(h) => h,
None => return,
};
fn recursive_check(bookside: &BookSide, h: NodeHandle) {
match bookside.node(h).unwrap().case().unwrap() {
NodeRef::Inner(&inner) => {
let left = bookside.node(inner.children[0]).unwrap().key().unwrap();
let right = bookside.node(inner.children[1]).unwrap().key().unwrap();
// the left and right keys share the InnerNode's prefix
assert!((inner.key ^ left).leading_zeros() >= inner.prefix_len);
assert!((inner.key ^ right).leading_zeros() >= inner.prefix_len);
// the left and right node key have the critbit unset and set respectively
let crit_bit_mask: i128 = 1i128 << (127 - inner.prefix_len);
assert!(left & crit_bit_mask == 0);
assert!(right & crit_bit_mask != 0);
recursive_check(bookside, inner.children[0]);
recursive_check(bookside, inner.children[1]);
}
_ => {}
}
}
recursive_check(bookside, r);
}
// check that iteration of bookside has the right order and misses no leaves
fn verify_bookside_iteration(bookside: &BookSide) {
let mut total = 0;
let ascending = bookside.book_side_type == BookSideType::Asks;
let mut last_key = if ascending { 0 } else { i128::MAX };
for (_, node) in bookside.iter_all_including_invalid() {
let key = node.key;
if ascending {
assert!(key >= last_key);
} else {
assert!(key <= last_key);
}
last_key = key;
total += 1;
}
assert_eq!(bookside.leaf_count, total);
}
// check that BookSide::child_expiry invariant holds
fn verify_bookside_expiry(bookside: &BookSide) {
let r = match bookside.root() {
Some(h) => h,
None => return,
};
fn recursive_check(bookside: &BookSide, h: NodeHandle) {
match bookside.node(h).unwrap().case().unwrap() {
NodeRef::Inner(&inner) => {
let left = bookside.node(inner.children[0]).unwrap().earliest_expiry();
let right = bookside.node(inner.children[1]).unwrap().earliest_expiry();
// child_expiry must hold the expiry of the children
assert_eq!(inner.child_earliest_expiry[0], left);
assert_eq!(inner.child_earliest_expiry[1], right);
recursive_check(bookside, inner.children[0]);
recursive_check(bookside, inner.children[1]);
}
_ => {}
}
}
recursive_check(bookside, r);
}
#[test]
fn bookside_expiry_manual() {
let mut bids = new_bookside(BookSideType::Bids);
let new_expiring_leaf = |key: i128, expiry: u64| {
LeafNode::new(
0,
key,
Pubkey::default(),
0,
0,
expiry - 1,
OrderType::Limit,
1,
)
};
assert!(bids.find_earliest_expiry().is_none());
bids.insert_leaf(&new_expiring_leaf(0, 5000)).unwrap();
assert_eq!(bids.find_earliest_expiry().unwrap(), (bids.root_node, 5000));
verify_bookside(&bids);
let (new4000_h, _) = bids.insert_leaf(&new_expiring_leaf(1, 4000)).unwrap();
assert_eq!(bids.find_earliest_expiry().unwrap(), (new4000_h, 4000));
verify_bookside(&bids);
let (_new4500_h, _) = bids.insert_leaf(&new_expiring_leaf(2, 4500)).unwrap();
assert_eq!(bids.find_earliest_expiry().unwrap(), (new4000_h, 4000));
verify_bookside(&bids);
let (new3500_h, _) = bids.insert_leaf(&new_expiring_leaf(3, 3500)).unwrap();
assert_eq!(bids.find_earliest_expiry().unwrap(), (new3500_h, 3500));
verify_bookside(&bids);
// the first two levels of the tree are innernodes, with 0;1 on one side and 2;3 on the other
assert_eq!(
bids.node_mut(bids.root_node)
.unwrap()
.as_inner_mut()
.unwrap()
.child_earliest_expiry,
[4000, 3500]
);
bids.remove_by_key(3).unwrap();
verify_bookside(&bids);
assert_eq!(
bids.node_mut(bids.root_node)
.unwrap()
.as_inner_mut()
.unwrap()
.child_earliest_expiry,
[4000, 4500]
);
assert_eq!(bids.find_earliest_expiry().unwrap().1, 4000);
bids.remove_by_key(0).unwrap();
verify_bookside(&bids);
assert_eq!(
bids.node_mut(bids.root_node)
.unwrap()
.as_inner_mut()
.unwrap()
.child_earliest_expiry,
[4000, 4500]
);
assert_eq!(bids.find_earliest_expiry().unwrap().1, 4000);
bids.remove_by_key(1).unwrap();
verify_bookside(&bids);
assert_eq!(bids.find_earliest_expiry().unwrap().1, 4500);
bids.remove_by_key(2).unwrap();
verify_bookside(&bids);
assert!(bids.find_earliest_expiry().is_none());
}
#[test]
fn bookside_expiry_random() {
fn bookside_iteration_random_helper(side: Side) {
use rand::Rng;
let mut rng = rand::thread_rng();
let mut bids = new_bookside(BookSideType::Bids);
let new_expiring_leaf = |key: i128, expiry: u64| {
let order_tree_type = match side {
Side::Bid => OrderTreeType::Bids,
Side::Ask => OrderTreeType::Asks,
};
let mut fixed = new_order_tree(order_tree_type);
let mut oracle_pegged = new_order_tree(order_tree_type);
let new_leaf = |key: u128| {
LeafNode::new(
0,
key,
Pubkey::default(),
0,
0,
expiry - 1,
OrderType::Limit,
1,
PostOrderType::Limit,
0,
-1,
)
};
// add 200 random leaves
// add 100 leaves to each BookSide, mostly random
let mut keys = vec![];
for _ in 0..200 {
let key: i128 = rng.gen_range(0..10000); // overlap in key bits
// ensure at least one oracle pegged order visible even at oracle price 1
let key = new_node_key(side, oracle_pegged_price_data(20), 0);
keys.push(key);
oracle_pegged.insert_leaf(&new_leaf(key)).unwrap();
while oracle_pegged.leaf_count < 100 {
let price_data: u64 = oracle_pegged_price_data(rng.gen_range(-20..20));
let seq_num: u64 = rng.gen_range(0..1000);
let key = new_node_key(side, price_data, seq_num);
if keys.contains(&key) {
continue;
}
let expiry = rng.gen_range(1..200); // give good chance of duplicate expiry times
keys.push(key);
bids.insert_leaf(&new_expiring_leaf(key, expiry)).unwrap();
verify_bookside(&bids);
oracle_pegged.insert_leaf(&new_leaf(key)).unwrap();
}
// remove 50 at random
for _ in 0..50 {
if keys.len() == 0 {
break;
while fixed.leaf_count < 100 {
let price_data: u64 = rng.gen_range(1..50);
let seq_num: u64 = rng.gen_range(0..1000);
let key = new_node_key(side, price_data, seq_num);
if keys.contains(&key) {
continue;
}
keys.push(key);
fixed.insert_leaf(&new_leaf(key)).unwrap();
}
let bookside = BookSide {
fixed,
oracle_pegged,
};
// verify iteration order for different oracle prices
for oracle_price_lots in 1..40 {
println!("oracle {oracle_price_lots}");
let mut total = 0;
let ascending = order_tree_type == OrderTreeType::Asks;
let mut last_price = if ascending { 0 } else { i64::MAX };
for order in bookside.iter_all_including_invalid(0, oracle_price_lots) {
let price = order.price_lots;
println!("{} {:?} {price}", order.node.key, order.handle.order_tree);
if ascending {
assert!(price >= last_price);
} else {
assert!(price <= last_price);
}
last_price = price;
total += 1;
}
assert!(total >= 101); // some oracle peg orders could be skipped
if oracle_price_lots > 20 {
assert_eq!(total, 200);
}
let k = keys[rng.gen_range(0..keys.len())];
bids.remove_by_key(k).unwrap();
keys.retain(|v| *v != k);
verify_bookside(&bids);
}
}
#[test]
fn bookside_iteration_random() {
bookside_iteration_random_helper(Side::Bid);
bookside_iteration_random_helper(Side::Ask);
}
#[test]
fn bookside_order_filtering() {
let side = Side::Bid;
let order_tree_type = OrderTreeType::Bids;
let mut fixed = new_order_tree(order_tree_type);
let mut oracle_pegged = new_order_tree(order_tree_type);
let new_node = |key: u128, tif: u8, peg_limit: i64| {
LeafNode::new(
0,
key,
Pubkey::default(),
0,
0,
1000,
PostOrderType::Limit,
tif,
peg_limit,
)
};
let mut add_fixed = |price: i64, tif: u8| {
let key = new_node_key(side, fixed_price_data(price).unwrap(), 0);
fixed.insert_leaf(&new_node(key, tif, -1)).unwrap();
};
let mut add_pegged = |price_offset: i64, tif: u8, peg_limit: i64| {
let key = new_node_key(side, oracle_pegged_price_data(price_offset), 0);
oracle_pegged
.insert_leaf(&new_node(key, tif, peg_limit))
.unwrap();
};
add_fixed(100, 0);
add_fixed(120, 5);
add_pegged(-10, 0, 100);
add_pegged(-15, 0, -1);
add_pegged(-20, 7, 95);
let bookside = BookSide {
fixed,
oracle_pegged,
};
let order_prices = |now_ts: u64, oracle: i64| -> Vec<i64> {
bookside
.iter_valid(now_ts, oracle)
.map(|it| it.price_lots)
.collect()
};
assert_eq!(order_prices(0, 100), vec![120, 100, 90, 85, 80]);
assert_eq!(order_prices(1004, 100), vec![120, 100, 90, 85, 80]);
assert_eq!(order_prices(1005, 100), vec![100, 90, 85, 80]);
assert_eq!(order_prices(1006, 100), vec![100, 90, 85, 80]);
assert_eq!(order_prices(1007, 100), vec![100, 90, 85]);
assert_eq!(order_prices(0, 110), vec![120, 100, 100, 95, 90]);
assert_eq!(order_prices(0, 111), vec![120, 100, 96, 91]);
assert_eq!(order_prices(0, 115), vec![120, 100, 100, 95]);
assert_eq!(order_prices(0, 116), vec![120, 101, 100]);
assert_eq!(order_prices(0, 2015), vec![2000, 120, 100]);
assert_eq!(order_prices(1010, 2015), vec![2000, 100]);
}
}

View File

@ -1,92 +1,153 @@
use crate::state::orderbook::bookside::{BookSide, BookSideType};
use crate::state::orderbook::nodes::{InnerNode, LeafNode, NodeHandle, NodeRef};
use super::*;
/// Iterate over orders in order (bids=descending, asks=ascending)
pub struct BookSideIterItem<'a> {
pub handle: BookSideOrderHandle,
pub node: &'a LeafNode,
pub price_lots: i64,
pub is_valid: bool,
}
/// Iterates the fixed and oracle_pegged OrderTrees simultaneously, allowing users to
/// walk the orderbook without caring about where an order came from.
///
/// This will skip over orders that are not currently matchable, but might be valid
/// in the future.
///
/// This may return invalid orders (tif expired, peg_limit exceeded; see is_valid) which
/// users are supposed to remove from the orderbook if they can.
pub struct BookSideIter<'a> {
book_side: &'a BookSide,
/// InnerNodes where the right side still needs to be iterated on
stack: Vec<&'a InnerNode>,
/// To be returned on `next()`
next_leaf: Option<(NodeHandle, &'a LeafNode)>,
/// either 0, 1 to iterate low-to-high, or 1, 0 to iterate high-to-low
left: usize,
right: usize,
fixed_iter: OrderTreeIter<'a>,
oracle_pegged_iter: OrderTreeIter<'a>,
now_ts: u64,
oracle_price_lots: i64,
}
impl<'a> BookSideIter<'a> {
pub fn new(book_side: &'a BookSide, now_ts: u64) -> Self {
let (left, right) = if book_side.book_side_type == BookSideType::Bids {
(1, 0)
} else {
(0, 1)
};
let stack = vec![];
let mut iter = Self {
book_side,
stack,
next_leaf: None,
left,
right,
pub fn new(book_side: &'a BookSide, now_ts: u64, oracle_price_lots: i64) -> Self {
Self {
fixed_iter: book_side.fixed.iter(),
oracle_pegged_iter: book_side.oracle_pegged.iter(),
now_ts,
};
if book_side.leaf_count != 0 {
iter.next_leaf = iter.find_leftmost_valid_leaf(book_side.root_node);
oracle_price_lots,
}
iter
}
}
fn find_leftmost_valid_leaf(
&mut self,
start: NodeHandle,
) -> Option<(NodeHandle, &'a LeafNode)> {
let mut current = start;
loop {
match self.book_side.node(current).unwrap().case().unwrap() {
NodeRef::Inner(inner) => {
self.stack.push(inner);
current = inner.children[self.left];
}
NodeRef::Leaf(leaf) => {
if leaf.is_valid(self.now_ts) {
return Some((current, leaf));
} else {
match self.stack.pop() {
None => {
return None;
}
Some(inner) => {
current = inner.children[self.right];
}
}
}
}
#[derive(Clone, Copy, PartialEq)]
enum OrderState {
Valid,
Invalid,
Skipped,
}
fn oracle_pegged_price(
oracle_price_lots: i64,
node: &LeafNode,
side: Side,
) -> (OrderState, Option<i64>) {
let price_data = node.price_data();
let price_offset = oracle_pegged_price_offset(price_data);
if let Some(price) = oracle_price_lots.checked_add(price_offset) {
if price >= 1 {
if node.peg_limit != -1 && side.is_price_better(price, node.peg_limit) {
return (OrderState::Invalid, Some(price));
} else {
return (OrderState::Valid, Some(price));
}
}
}
(OrderState::Skipped, None)
}
fn key_for_price(key: u128, price_lots: i64) -> u128 {
// We know this can never fail, because oracle pegged price will always be >= 1
assert!(price_lots >= 1);
let price_data = fixed_price_data(price_lots).unwrap();
let upper = (price_data as u128) << 64;
let lower = (key as u64) as u128;
upper | lower
}
impl<'a> Iterator for BookSideIter<'a> {
type Item = (NodeHandle, &'a LeafNode);
type Item = BookSideIterItem<'a>;
fn next(&mut self) -> Option<Self::Item> {
// if next leaf is None just return it
self.next_leaf?;
let side = self.fixed_iter.side();
// start popping from stack and get the other child
let current_leaf = self.next_leaf;
self.next_leaf = match self.stack.pop() {
None => None,
Some(inner) => {
let start = inner.children[self.right];
// go down the left branch as much as possible until reaching a valid leaf
self.find_leftmost_valid_leaf(start)
// Skip all the oracle pegged orders that aren't representable with the current oracle
// price. Example: iterating asks, but the best ask is at offset -100 with the oracle at 50.
// We need to skip asks until we find the first that has a price >= 1.
let mut o_peek = self.oracle_pegged_iter.peek();
while let Some((_, o_node)) = o_peek {
if oracle_pegged_price(self.oracle_price_lots, o_node, side).0 != OrderState::Skipped {
break;
}
};
o_peek = self.oracle_pegged_iter.next()
}
current_leaf
match (self.fixed_iter.peek(), o_peek) {
(Some((d_handle, d_node)), Some((o_handle, o_node))) => {
let is_better = if side == Side::Bid {
|a, b| a > b
} else {
|a, b| a < b
};
let (o_valid, o_price_maybe) =
oracle_pegged_price(self.oracle_price_lots, o_node, side);
let o_price = o_price_maybe.unwrap(); // Skipped orders are skipped above
if is_better(d_node.key, key_for_price(o_node.key, o_price)) {
self.fixed_iter.next();
Some(Self::Item {
handle: BookSideOrderHandle {
order_tree: BookSideOrderTree::Fixed,
node: d_handle,
},
node: d_node,
price_lots: fixed_price_lots(d_node.price_data()),
is_valid: d_node.is_not_expired(self.now_ts),
})
} else {
self.oracle_pegged_iter.next();
Some(Self::Item {
handle: BookSideOrderHandle {
order_tree: BookSideOrderTree::OraclePegged,
node: o_handle,
},
node: o_node,
price_lots: o_price,
is_valid: o_valid == OrderState::Valid
&& o_node.is_not_expired(self.now_ts),
})
}
}
(None, Some((handle, node))) => {
self.oracle_pegged_iter.next();
let (valid, price_maybe) = oracle_pegged_price(self.oracle_price_lots, node, side);
let price_lots = price_maybe.unwrap(); // Skipped orders are skipped above
Some(Self::Item {
handle: BookSideOrderHandle {
order_tree: BookSideOrderTree::OraclePegged,
node: handle,
},
node,
price_lots,
is_valid: valid == OrderState::Valid && node.is_not_expired(self.now_ts),
})
}
(Some((handle, node)), None) => {
self.fixed_iter.next();
Some(Self::Item {
handle: BookSideOrderHandle {
order_tree: BookSideOrderTree::Fixed,
node: handle,
},
node,
price_lots: fixed_price_lots(node.price_data()),
is_valid: node.is_not_expired(self.now_ts),
})
}
(None, None) => None,
}
}
}

View File

@ -2,15 +2,21 @@ pub use book::*;
pub use bookside::*;
pub use bookside_iterator::*;
pub use nodes::*;
pub use order::*;
pub use order_type::*;
pub use ordertree::*;
pub use ordertree_iterator::*;
pub use queue::*;
pub mod book;
pub mod bookside;
pub mod bookside_iterator;
pub mod nodes;
pub mod order_type;
pub mod queue;
mod book;
mod bookside;
mod bookside_iterator;
mod nodes;
mod order;
mod order_type;
mod ordertree;
mod ordertree_iterator;
mod queue;
#[cfg(test)]
mod tests {
@ -20,24 +26,9 @@ mod tests {
use bytemuck::Zeroable;
use fixed::types::I80F48;
use solana_program::pubkey::Pubkey;
use std::cell::RefCell;
fn new_bookside(book_side_type: BookSideType) -> BookSide {
BookSide {
book_side_type,
padding: [0u8; 3],
bump_index: 0,
free_list_len: 0,
free_list_head: 0,
root_node: 0,
leaf_count: 0,
nodes: [AnyNode::zeroed(); MAX_BOOK_NODES],
reserved: [0; 256],
}
}
fn bookside_leaf_by_key(bookside: &BookSide, key: i128) -> Option<&LeafNode> {
for (_, leaf) in bookside.iter_all_including_invalid() {
fn order_tree_leaf_by_key(order_tree: &OrderTree, key: u128) -> Option<&LeafNode> {
for (_, leaf) in order_tree.iter() {
if leaf.key == key {
return Some(leaf);
}
@ -45,8 +36,8 @@ mod tests {
None
}
fn bookside_contains_key(bookside: &BookSide, key: i128) -> bool {
for (_, leaf) in bookside.iter_all_including_invalid() {
fn order_tree_contains_key(order_tree: &OrderTree, key: u128) -> bool {
for (_, leaf) in order_tree.iter() {
if leaf.key == key {
return true;
}
@ -54,26 +45,18 @@ mod tests {
false
}
fn bookside_contains_price(bookside: &BookSide, price: i64) -> bool {
for (_, leaf) in bookside.iter_all_including_invalid() {
if leaf.price() == price {
fn order_tree_contains_price(order_tree: &OrderTree, price_data: u64) -> bool {
for (_, leaf) in order_tree.iter() {
if leaf.price_data() == price_data {
return true;
}
}
false
}
fn test_setup(
price: f64,
) -> (
PerpMarket,
I80F48,
EventQueue,
RefCell<BookSide>,
RefCell<BookSide>,
) {
let bids = RefCell::new(new_bookside(BookSideType::Bids));
let asks = RefCell::new(new_bookside(BookSideType::Asks));
fn test_setup(price: f64) -> (PerpMarket, I80F48, EventQueue, Box<OrderBook>) {
let mut book = Box::new(OrderBook::zeroed());
book.init();
let event_queue = EventQueue::zeroed();
@ -87,49 +70,53 @@ mod tests {
perp_market.init_asset_weight = I80F48::ONE;
perp_market.init_liab_weight = I80F48::ONE;
(perp_market, oracle_price, event_queue, bids, asks)
(perp_market, oracle_price, event_queue, book)
}
// Check what happens when one side of the book fills up
#[test]
fn book_bids_full() {
let (mut perp_market, oracle_price, mut event_queue, bids, asks) = test_setup(5000.0);
let mut book = Book {
bids: bids.borrow_mut(),
asks: asks.borrow_mut(),
};
let (mut perp_market, oracle_price, mut event_queue, mut book) = test_setup(5000.0);
let settle_token_index = 0;
let mut new_order =
|book: &mut Book, event_queue: &mut EventQueue, side, price, now_ts| -> i128 {
let buffer = MangoAccount::default_for_tests().try_to_vec().unwrap();
let mut account = MangoAccountValue::from_bytes(&buffer).unwrap();
account
.ensure_perp_position(perp_market.perp_market_index, settle_token_index)
.unwrap();
let quantity = 1;
let tif = 100;
book.new_order(
side,
&mut perp_market,
event_queue,
oracle_price,
&mut account.borrow_mut(),
&Pubkey::default(),
price,
quantity,
i64::MAX,
OrderType::Limit,
tif,
0,
now_ts,
u8::MAX,
)
let mut new_order = |book: &mut OrderBook,
event_queue: &mut EventQueue,
side,
price_lots,
now_ts|
-> u128 {
let buffer = MangoAccount::default_for_tests().try_to_vec().unwrap();
let mut account = MangoAccountValue::from_bytes(&buffer).unwrap();
account
.ensure_perp_position(perp_market.perp_market_index, settle_token_index)
.unwrap();
account.perp_order_by_raw_index(0).order_id
};
let max_base_lots = 1;
let time_in_force = 100;
book.new_order(
Order {
side,
max_base_lots,
max_quote_lots: i64::MAX,
client_order_id: 0,
time_in_force,
params: OrderParams::Fixed {
price_lots,
order_type: PostOrderType::Limit,
},
},
&mut perp_market,
event_queue,
oracle_price,
&mut account.borrow_mut(),
&Pubkey::default(),
now_ts,
u8::MAX,
)
.unwrap();
account.perp_order_by_raw_index(0).id
};
// insert bids until book side is full
for i in 1..10 {
@ -149,50 +136,46 @@ mod tests {
1000 + i as i64,
1000011 as u64,
);
if book.bids.is_full() {
if book.bids.fixed.is_full() {
break;
}
}
assert!(book.bids.is_full());
assert_eq!(book.bids.min_leaf().unwrap().price(), 1001);
assert!(book.bids.fixed.is_full());
assert_eq!(book.bids.fixed.min_leaf().unwrap().price(), 1001);
assert_eq!(
book.bids.max_leaf().unwrap().price(),
(1000 + book.bids.leaf_count) as i64
book.bids.fixed.max_leaf().unwrap().price(),
(1000 + book.bids.fixed.leaf_count) as i64
);
// add another bid at a higher price before expiry, replacing the lowest-price one (1001)
new_order(&mut book, &mut event_queue, Side::Bid, 1005, 1000000 - 1);
assert_eq!(book.bids.min_leaf().unwrap().price(), 1002);
assert_eq!(book.bids.fixed.min_leaf().unwrap().price(), 1002);
assert_eq!(event_queue.len(), 1);
// adding another bid after expiry removes the soonest-expiring order (1005)
new_order(&mut book, &mut event_queue, Side::Bid, 999, 2000000);
assert_eq!(book.bids.min_leaf().unwrap().price(), 999);
assert!(!bookside_contains_key(&book.bids, 1005));
assert_eq!(book.bids.fixed.min_leaf().unwrap().price(), 999);
assert!(!order_tree_contains_key(&book.bids.fixed, 1005));
assert_eq!(event_queue.len(), 2);
// adding an ask will wipe up to three expired bids at the top of the book
let bids_max = book.bids.max_leaf().unwrap().price();
let bids_count = book.bids.leaf_count;
let bids_max = book.bids.fixed.max_leaf().unwrap().price_data();
let bids_count = book.bids.fixed.leaf_count;
new_order(&mut book, &mut event_queue, Side::Ask, 6000, 1500000);
assert_eq!(book.bids.leaf_count, bids_count - 5);
assert_eq!(book.asks.leaf_count, 1);
assert_eq!(book.bids.fixed.leaf_count, bids_count - 5);
assert_eq!(book.asks.fixed.leaf_count, 1);
assert_eq!(event_queue.len(), 2 + 5);
assert!(!bookside_contains_price(&book.bids, bids_max));
assert!(!bookside_contains_price(&book.bids, bids_max - 1));
assert!(!bookside_contains_price(&book.bids, bids_max - 2));
assert!(!bookside_contains_price(&book.bids, bids_max - 3));
assert!(!bookside_contains_price(&book.bids, bids_max - 4));
assert!(bookside_contains_price(&book.bids, bids_max - 5));
assert!(!order_tree_contains_price(&book.bids.fixed, bids_max));
assert!(!order_tree_contains_price(&book.bids.fixed, bids_max - 1));
assert!(!order_tree_contains_price(&book.bids.fixed, bids_max - 2));
assert!(!order_tree_contains_price(&book.bids.fixed, bids_max - 3));
assert!(!order_tree_contains_price(&book.bids.fixed, bids_max - 4));
assert!(order_tree_contains_price(&book.bids.fixed, bids_max - 5));
}
#[test]
fn book_new_order() {
let (mut market, oracle_price, mut event_queue, bids, asks) = test_setup(1000.0);
let mut book = Book {
bids: bids.borrow_mut(),
asks: asks.borrow_mut(),
};
let (mut market, oracle_price, mut event_queue, mut book) = test_setup(1000.0);
let settle_token_index = 0;
// Add lots and fees to make sure to exercise unit conversion
@ -216,41 +199,48 @@ mod tests {
let now_ts = 1000000;
// Place a maker-bid
let price = 1000 * market.base_lot_size / market.quote_lot_size;
let price_lots = 1000 * market.base_lot_size / market.quote_lot_size;
let bid_quantity = 10;
book.new_order(
Side::Bid,
Order {
side: Side::Bid,
max_base_lots: bid_quantity,
max_quote_lots: i64::MAX,
client_order_id: 42,
time_in_force: 0,
params: OrderParams::Fixed {
price_lots,
order_type: PostOrderType::Limit,
},
},
&mut market,
&mut event_queue,
oracle_price,
&mut maker.borrow_mut(),
&maker_pk,
price,
bid_quantity,
i64::MAX,
OrderType::Limit,
0,
42,
now_ts,
u8::MAX,
)
.unwrap();
assert_eq!(
maker.perp_order_mut_by_raw_index(0).order_market,
maker.perp_order_mut_by_raw_index(0).market,
market.perp_market_index
);
assert_eq!(maker.perp_order_mut_by_raw_index(1).market, FREE_ORDER_SLOT);
assert_ne!(maker.perp_order_mut_by_raw_index(0).id, 0);
assert_eq!(maker.perp_order_mut_by_raw_index(0).client_id, 42);
assert_eq!(
maker.perp_order_mut_by_raw_index(1).order_market,
FREE_ORDER_SLOT
maker.perp_order_mut_by_raw_index(0).side_and_tree,
SideAndOrderTree::BidFixed
);
assert_ne!(maker.perp_order_mut_by_raw_index(0).order_id, 0);
assert_eq!(maker.perp_order_mut_by_raw_index(0).client_order_id, 42);
assert_eq!(maker.perp_order_mut_by_raw_index(0).order_side, Side::Bid);
assert!(bookside_contains_key(
&book.bids,
maker.perp_order_mut_by_raw_index(0).order_id
assert!(order_tree_contains_key(
&book.bids.fixed,
maker.perp_order_mut_by_raw_index(0).id
));
assert!(order_tree_contains_price(
&book.bids.fixed,
price_lots as u64
));
assert!(bookside_contains_price(&book.bids, price));
assert_eq!(
maker.perp_position_by_raw_index(0).bids_base_lots,
bid_quantity
@ -271,18 +261,22 @@ mod tests {
// Take the order partially
let match_quantity = 5;
book.new_order(
Side::Ask,
Order {
side: Side::Ask,
max_base_lots: match_quantity,
max_quote_lots: i64::MAX,
client_order_id: 43,
time_in_force: 0,
params: OrderParams::Fixed {
price_lots,
order_type: PostOrderType::Limit,
},
},
&mut market,
&mut event_queue,
oracle_price,
&mut taker.borrow_mut(),
&taker_pk,
price,
match_quantity,
i64::MAX,
OrderType::Limit,
0,
43,
now_ts,
u8::MAX,
)
@ -290,22 +284,19 @@ mod tests {
// the remainder of the maker order is still on the book
// (the maker account is unchanged: it was not even passed in)
let order =
bookside_leaf_by_key(&book.bids, maker.perp_order_by_raw_index(0).order_id).unwrap();
assert_eq!(order.price(), price);
order_tree_leaf_by_key(&book.bids.fixed, maker.perp_order_by_raw_index(0).id).unwrap();
assert_eq!(order.price(), price_lots);
assert_eq!(order.quantity, bid_quantity - match_quantity);
// fees were immediately accrued
let match_quote = I80F48::from(match_quantity * price * market.quote_lot_size);
let match_quote = I80F48::from(match_quantity * price_lots * market.quote_lot_size);
assert_eq!(
market.fees_accrued,
match_quote * (market.maker_fee + market.taker_fee)
);
// the taker account is updated
assert_eq!(
taker.perp_order_by_raw_index(0).order_market,
FREE_ORDER_SLOT
);
assert_eq!(taker.perp_order_by_raw_index(0).market, FREE_ORDER_SLOT);
assert_eq!(taker.perp_position_by_raw_index(0).bids_base_lots, 0);
assert_eq!(taker.perp_position_by_raw_index(0).asks_base_lots, 0);
assert_eq!(
@ -314,7 +305,7 @@ mod tests {
);
assert_eq!(
taker.perp_position_by_raw_index(0).taker_quote_lots,
match_quantity * price
match_quantity * price_lots
);
assert_eq!(taker.perp_position_by_raw_index(0).base_position_lots(), 0);
assert_eq!(
@ -328,7 +319,7 @@ mod tests {
assert_eq!(event.event_type, EventType::Fill as u8);
let fill: &FillEvent = bytemuck::cast_ref(event);
assert_eq!(fill.quantity, match_quantity);
assert_eq!(fill.price, price);
assert_eq!(fill.price, price_lots);
assert_eq!(fill.taker_client_order_id, 43);
assert_eq!(fill.maker_client_order_id, 42);
assert_eq!(fill.maker, maker_pk);
@ -345,7 +336,7 @@ mod tests {
.unwrap();
assert_eq!(market.open_interest, 2 * match_quantity);
assert_eq!(maker.perp_order_by_raw_index(0).order_market, 0);
assert_eq!(maker.perp_order_by_raw_index(0).market, 0);
assert_eq!(
maker.perp_position_by_raw_index(0).bids_base_lots,
bid_quantity - match_quantity
@ -378,37 +369,35 @@ mod tests {
#[test]
fn test_fee_penalty_applied_only_on_limit_order() -> Result<()> {
let (mut market, oracle_price, mut event_queue, bids, asks) = test_setup(1000.0);
let mut book = Book {
bids: bids.borrow_mut(),
asks: asks.borrow_mut(),
};
let (mut market, oracle_price, mut event_queue, mut book) = test_setup(1000.0);
let buffer = MangoAccount::default_for_tests().try_to_vec().unwrap();
let mut account = MangoAccountValue::from_bytes(&buffer).unwrap();
let taker_pk = Pubkey::new_unique();
let now_ts = 1000000;
market.base_lot_size = 1;
market.quote_lot_size = 1;
market.taker_fee = I80F48::from_num(0.01);
market.fee_penalty = 5.0;
account.ensure_perp_position(market.perp_market_index, 0)?;
// Passive order
book.new_order(
Side::Ask,
Order {
side: Side::Ask,
max_base_lots: 2,
max_quote_lots: i64::MAX,
client_order_id: 43,
time_in_force: 0,
params: OrderParams::Fixed {
price_lots: 1000,
order_type: PostOrderType::Limit,
},
},
&mut market,
&mut event_queue,
oracle_price,
&mut account.borrow_mut(),
&taker_pk,
1000,
2,
i64::MAX,
OrderType::Limit,
0,
43,
now_ts,
u8::MAX,
)
@ -416,18 +405,22 @@ mod tests {
// Partial taker
book.new_order(
Side::Bid,
Order {
side: Side::Bid,
max_base_lots: 1,
max_quote_lots: i64::MAX,
client_order_id: 43,
time_in_force: 0,
params: OrderParams::Fixed {
price_lots: 1000,
order_type: PostOrderType::Limit,
},
},
&mut market,
&mut event_queue,
oracle_price,
&mut account.borrow_mut(),
&taker_pk,
1000,
1,
i64::MAX,
OrderType::Limit,
0,
43,
now_ts,
u8::MAX,
)
@ -449,18 +442,19 @@ mod tests {
// Full taker
book.new_order(
Side::Bid,
Order {
side: Side::Bid,
max_base_lots: 1,
max_quote_lots: i64::MAX,
client_order_id: 43,
time_in_force: 0,
params: OrderParams::ImmediateOrCancel { price_lots: 1000 },
},
&mut market,
&mut event_queue,
oracle_price,
&mut account.borrow_mut(),
&taker_pk,
1000,
1,
i64::MAX,
OrderType::ImmediateOrCancel,
0,
43,
now_ts,
u8::MAX,
)

View File

@ -6,7 +6,7 @@ use mango_macro::Pod;
use num_enum::{IntoPrimitive, TryFromPrimitive};
use static_assertions::const_assert_eq;
use super::order_type::OrderType;
use super::order_type::{PostOrderType, Side};
pub type NodeHandle = u32;
const NODE_SIZE: usize = 96;
@ -21,6 +21,43 @@ pub enum NodeTag {
LastFreeNode = 4,
}
/// Creates a binary tree node key.
///
/// It's used for sorting nodes (ascending for asks, descending for bids)
/// and encodes price data in the top 64 bits followed by an ordering number
/// in the lower bits.
///
/// The `seq_num` that's passed should monotonically increase. It's used to choose
/// the ordering number such that orders placed later for the same price data
/// are ordered after earlier orders.
pub fn new_node_key(side: Side, price_data: u64, seq_num: u64) -> u128 {
let seq_num = if side == Side::Bid { !seq_num } else { seq_num };
let upper = (price_data as u128) << 64;
upper | (seq_num as u128)
}
pub fn oracle_pegged_price_data(price_offset_lots: i64) -> u64 {
// Price data is used for ordering in the bookside's top bits of the u128 key.
// Map i64::MIN to be 0 and i64::MAX to u64::MAX, that way comparisons on the
// u64 produce the same result as on the source i64.
(price_offset_lots as u64).wrapping_add(u64::MAX / 2 + 1)
}
pub fn oracle_pegged_price_offset(price_data: u64) -> i64 {
price_data.wrapping_sub(u64::MAX / 2 + 1) as i64
}
pub fn fixed_price_data(price_lots: i64) -> Result<u64> {
require_gte!(price_lots, 1);
Ok(price_lots as u64)
}
pub fn fixed_price_lots(price_data: u64) -> i64 {
assert!(price_data <= i64::MAX as u64);
price_data as i64
}
/// InnerNodes and LeafNodes compose the binary tree of orders.
///
/// Each InnerNode has exactly two children, which are either InnerNodes themselves,
@ -35,7 +72,7 @@ pub struct InnerNode {
pub prefix_len: u32,
/// only the top `prefix_len` bits of `key` are relevant
pub key: i128,
pub key: u128,
/// indexes into `BookSide::nodes`
pub children: [NodeHandle; 2],
@ -52,7 +89,7 @@ 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 {
pub fn new(prefix_len: u32, key: u128) -> Self {
Self {
tag: NodeTag::InnerNode.into(),
prefix_len,
@ -65,8 +102,8 @@ impl InnerNode {
/// Returns the handle of the child that may contain the search key
/// and 0 or 1 depending on which child it was.
pub(crate) fn walk_down(&self, search_key: i128) -> (NodeHandle, bool) {
let crit_bit_mask = 1i128 << (127 - self.prefix_len);
pub(crate) fn walk_down(&self, search_key: u128) -> (NodeHandle, bool) {
let crit_bit_mask = 1u128 << (127 - self.prefix_len);
let crit_bit = (search_key & crit_bit_mask) != 0;
(self.children[crit_bit as usize], crit_bit)
}
@ -84,7 +121,7 @@ impl InnerNode {
pub struct LeafNode {
pub tag: u32,
pub owner_slot: u8,
pub order_type: OrderType, // this was added for TradingView move order
pub order_type: PostOrderType, // this was added for TradingView move order
pub padding: [u8; 1],
@ -93,7 +130,7 @@ pub struct LeafNode {
pub time_in_force: u8,
/// The binary tree key
pub key: i128,
pub key: u128,
pub owner: Pubkey,
pub quantity: i64,
@ -102,27 +139,26 @@ pub struct LeafNode {
// The time the order was placed
pub timestamp: u64,
pub reserved: [u8; 16],
// Only applicable in the oracle_pegged OrderTree
pub peg_limit: i64,
pub reserved: [u8; 8],
}
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 {
(key >> 64) as i64
}
impl LeafNode {
#[allow(clippy::too_many_arguments)]
pub fn new(
owner_slot: u8,
key: i128,
key: u128,
owner: Pubkey,
quantity: i64,
client_order_id: u64,
timestamp: u64,
order_type: OrderType,
order_type: PostOrderType,
time_in_force: u8,
peg_limit: i64,
) -> Self {
Self {
tag: NodeTag::LeafNode.into(),
@ -135,13 +171,20 @@ impl LeafNode {
quantity,
client_order_id,
timestamp,
reserved: [0; 16],
peg_limit,
reserved: [0; 8],
}
}
// TODO: remove, it's not always the price
#[inline(always)]
pub fn price(&self) -> i64 {
key_to_price(self.key)
self.price_data() as i64
}
#[inline(always)]
pub fn price_data(&self) -> u64 {
(self.key >> 64) as u64
}
/// Time at which this order will expire, u64::MAX if never
@ -155,7 +198,7 @@ impl LeafNode {
}
#[inline(always)]
pub fn is_valid(&self, now_ts: u64) -> bool {
pub fn is_not_expired(&self, now_ts: u64) -> bool {
self.time_in_force == 0 || now_ts < self.timestamp + self.time_in_force as u64
}
}
@ -191,7 +234,7 @@ pub(crate) enum NodeRefMut<'a> {
}
impl AnyNode {
pub fn key(&self) -> Option<i128> {
pub fn key(&self) -> Option<u128> {
match self.case()? {
NodeRef::Inner(inner) => Some(inner.key),
NodeRef::Leaf(leaf) => Some(leaf.key),
@ -274,3 +317,70 @@ impl AsRef<AnyNode> for LeafNode {
cast_ref(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
use itertools::Itertools;
#[test]
fn order_tree_price_data() {
for price in [1, 42, i64::MAX] {
assert_eq!(price, fixed_price_lots(fixed_price_data(price).unwrap()));
}
let seq = [-i64::MAX, -i64::MAX + 1, 0, i64::MAX - 1, i64::MAX];
for price_offset in seq {
assert_eq!(
price_offset,
oracle_pegged_price_offset(oracle_pegged_price_data(price_offset))
);
}
for (lhs, rhs) in seq.iter().tuple_windows() {
let l_price_data = oracle_pegged_price_data(*lhs);
let r_price_data = oracle_pegged_price_data(*rhs);
assert!(l_price_data < r_price_data);
}
}
#[test]
fn order_tree_key_ordering() {
let bid_seq: Vec<(i64, u64)> = vec![
(-5, 15),
(-5, 10),
(-4, 6),
(-4, 5),
(0, 20),
(0, 1),
(4, 6),
(4, 5),
(5, 3),
];
for (lhs, rhs) in bid_seq.iter().tuple_windows() {
let l_price_data = oracle_pegged_price_data(lhs.0);
let r_price_data = oracle_pegged_price_data(rhs.0);
let l_key = new_node_key(Side::Bid, l_price_data, lhs.1);
let r_key = new_node_key(Side::Bid, r_price_data, rhs.1);
assert!(l_key < r_key);
}
let ask_seq: Vec<(i64, u64)> = vec![
(-5, 10),
(-5, 15),
(-4, 6),
(-4, 7),
(0, 1),
(0, 20),
(4, 5),
(4, 6),
(5, 3),
];
for (lhs, rhs) in ask_seq.iter().tuple_windows() {
let l_price_data = oracle_pegged_price_data(lhs.0);
let r_price_data = oracle_pegged_price_data(rhs.0);
let l_key = new_node_key(Side::Ask, l_price_data, lhs.1);
let r_key = new_node_key(Side::Ask, r_price_data, rhs.1);
assert!(l_key < r_key);
}
}
}

View File

@ -0,0 +1,180 @@
use anchor_lang::prelude::*;
use super::*;
use crate::util::checked_math as cm;
/// Perp order parameters
pub struct Order {
pub side: Side,
/// Max base lots to buy/sell.
pub max_base_lots: i64,
/// Max quote lots to pay/receive (not taking fees into account).
pub max_quote_lots: i64,
/// Arbitrary user-controlled order id.
pub client_order_id: u64,
/// Number of seconds the order shall live, 0 meaning forever
pub time_in_force: u8,
/// Order type specific params
pub params: OrderParams,
}
pub enum OrderParams {
Market,
ImmediateOrCancel {
price_lots: i64,
},
Fixed {
price_lots: i64,
order_type: PostOrderType,
},
OraclePegged {
price_offset_lots: i64,
order_type: PostOrderType,
peg_limit: i64,
// TODO: oracle_staleness
},
}
impl Order {
/// Convert an input expiry timestamp to a time_in_force value
pub fn tif_from_expiry(expiry_timestamp: u64) -> Option<u8> {
let now_ts = Clock::get().unwrap().unix_timestamp as u64;
if expiry_timestamp != 0 {
// If expiry is far in the future, clamp to 255 seconds
let tif = expiry_timestamp.saturating_sub(now_ts).min(255);
if tif == 0 {
// If expiry is in the past, ignore the order
return None;
}
Some(tif as u8)
} else {
// Never expire
Some(0)
}
}
/// Should this order be penalized with an extra fee?
///
/// Some programs opportunistically call ioc orders, wasting lots of compute. This
/// is intended to encourage people to be smarter about it.
pub fn needs_penalty_fee(&self) -> bool {
matches!(self.params, OrderParams::ImmediateOrCancel { .. })
}
/// Is this order required to be posted to the orderbook? It will fail if it would take.
pub fn is_post_only(&self) -> bool {
let order_type = match self.params {
OrderParams::Fixed { order_type, .. } => order_type,
OrderParams::OraclePegged { order_type, .. } => order_type,
_ => return false,
};
order_type == PostOrderType::PostOnly || order_type == PostOrderType::PostOnlySlide
}
/// Order tree that this order should be added to
pub fn post_target(&self) -> Option<BookSideOrderTree> {
match self.params {
OrderParams::Fixed { .. } => Some(BookSideOrderTree::Fixed),
OrderParams::OraclePegged { .. } => Some(BookSideOrderTree::OraclePegged),
_ => None,
}
}
/// Some order types (PostOnlySlide) may override the price that is passed in,
/// this function computes the order-type-adjusted price.
fn price_for_order_type(
&self,
now_ts: u64,
oracle_price_lots: i64,
price_lots: i64,
order_type: PostOrderType,
order_book: &OrderBook,
) -> i64 {
if order_type == PostOrderType::PostOnlySlide {
if let Some(best_other_price) =
order_book.best_price(now_ts, oracle_price_lots, self.side.invert_side())
{
post_only_slide_limit(self.side, best_other_price, price_lots)
} else {
price_lots
}
} else {
price_lots
}
}
/// Compute the price_lots this order is currently at, as well as the price_data that
/// would be stored in its OrderTree node if the order is posted to the orderbook.
pub fn price(
&self,
now_ts: u64,
oracle_price_lots: i64,
order_book: &OrderBook,
) -> Result<(i64, u64)> {
let price_lots = match self.params {
OrderParams::Market => market_order_limit_for_side(self.side),
OrderParams::ImmediateOrCancel { price_lots } => price_lots,
OrderParams::Fixed {
price_lots,
order_type,
} => self.price_for_order_type(
now_ts,
oracle_price_lots,
price_lots,
order_type,
order_book,
),
OrderParams::OraclePegged {
price_offset_lots,
order_type,
..
} => {
let price_lots = cm!(oracle_price_lots + price_offset_lots);
self.price_for_order_type(
now_ts,
oracle_price_lots,
price_lots,
order_type,
order_book,
)
}
};
let price_data = match self.params {
OrderParams::OraclePegged { .. } => {
oracle_pegged_price_data(cm!(price_lots - oracle_price_lots))
}
_ => fixed_price_data(price_lots)?,
};
Ok((price_lots, price_data))
}
/// pegging limit for oracle peg orders, otherwise -1
pub fn peg_limit(&self) -> i64 {
match self.params {
OrderParams::OraclePegged { peg_limit, .. } => peg_limit,
_ => -1,
}
}
}
/// The implicit limit price to use for market orders
fn market_order_limit_for_side(side: Side) -> i64 {
match side {
Side::Bid => i64::MAX,
Side::Ask => 1,
}
}
/// The limit to use for PostOnlySlide orders: the tinyest bit better than
/// the best opposing order
fn post_only_slide_limit(side: Side, best_other_side: i64, limit: i64) -> i64 {
match side {
Side::Bid => limit.min(cm!(best_other_side - 1)),
Side::Ask => limit.max(cm!(best_other_side + 1)),
}
}

View File

@ -1,6 +1,10 @@
use anchor_lang::prelude::*;
use num_enum::{IntoPrimitive, TryFromPrimitive};
use super::*;
use crate::error::*;
use crate::error_msg;
#[derive(
Eq,
PartialEq,
@ -13,7 +17,7 @@ use num_enum::{IntoPrimitive, TryFromPrimitive};
AnchorDeserialize,
)]
#[repr(u8)]
pub enum OrderType {
pub enum PlaceOrderType {
/// Take existing orders up to price, max_base_quantity and max_quote_quantity.
/// If any base_quantity or quote_quantity remains, place an order on the book
Limit = 0,
@ -37,6 +41,44 @@ pub enum OrderType {
PostOnlySlide = 4,
}
impl PlaceOrderType {
pub fn to_post_order_type(&self) -> Result<PostOrderType> {
match *self {
Self::Market => Err(error_msg!("Market is not a PostOrderType")),
Self::ImmediateOrCancel => Err(error_msg!("ImmediateOrCancel is not a PostOrderType")),
Self::Limit => Ok(PostOrderType::Limit),
Self::PostOnly => Ok(PostOrderType::PostOnly),
Self::PostOnlySlide => Ok(PostOrderType::PostOnlySlide),
}
}
}
#[derive(
Eq,
PartialEq,
Copy,
Clone,
TryFromPrimitive,
IntoPrimitive,
Debug,
AnchorSerialize,
AnchorDeserialize,
)]
#[repr(u8)]
pub enum PostOrderType {
/// Take existing orders up to price, max_base_quantity and max_quote_quantity.
/// If any base_quantity or quote_quantity remains, place an order on the book
Limit = 0,
/// Never take any existing orders, post the order on the book if possible.
/// If existing orders can match with this order, do nothing.
PostOnly = 2,
/// If existing orders match with this order, adjust the price to just barely
/// not match. Always places an order on the book.
PostOnlySlide = 4,
}
#[derive(
Eq,
PartialEq,
@ -62,6 +104,14 @@ impl Side {
}
}
/// Is `lhs` is a better order for `side` than `rhs`?
pub fn is_price_data_better(self: &Side, lhs: u64, rhs: u64) -> bool {
match self {
Side::Bid => lhs > rhs,
Side::Ask => lhs < rhs,
}
}
/// Is `lhs` is a better order for `side` than `rhs`?
pub fn is_price_better(self: &Side, lhs: i64, rhs: i64) -> bool {
match self {
@ -78,3 +128,48 @@ impl Side {
}
}
}
/// SideAndOrderTree is a storage optimization, so we don't need two bytes for the data
#[derive(
Eq,
PartialEq,
Copy,
Clone,
TryFromPrimitive,
IntoPrimitive,
Debug,
AnchorSerialize,
AnchorDeserialize,
)]
#[repr(u8)]
pub enum SideAndOrderTree {
BidFixed = 0,
AskFixed = 1,
BidOraclePegged = 2,
AskOraclePegged = 3,
}
impl SideAndOrderTree {
pub fn new(side: Side, order_tree: BookSideOrderTree) -> Self {
match (side, order_tree) {
(Side::Bid, BookSideOrderTree::Fixed) => Self::BidFixed,
(Side::Ask, BookSideOrderTree::Fixed) => Self::AskFixed,
(Side::Bid, BookSideOrderTree::OraclePegged) => Self::BidOraclePegged,
(Side::Ask, BookSideOrderTree::OraclePegged) => Self::AskOraclePegged,
}
}
pub fn side(&self) -> Side {
match self {
Self::BidFixed | Self::BidOraclePegged => Side::Bid,
Self::AskFixed | Self::AskOraclePegged => Side::Ask,
}
}
pub fn order_tree(&self) -> BookSideOrderTree {
match self {
Self::BidFixed | Self::AskFixed => BookSideOrderTree::Fixed,
Self::BidOraclePegged | Self::AskOraclePegged => BookSideOrderTree::OraclePegged,
}
}
}

View File

@ -0,0 +1,642 @@
use anchor_lang::prelude::*;
use bytemuck::{cast, cast_mut, cast_ref};
use num_enum::{IntoPrimitive, TryFromPrimitive};
use static_assertions::const_assert_eq;
use super::*;
use crate::error::MangoError;
pub const MAX_ORDERTREE_NODES: usize = 1024;
#[derive(
Eq,
PartialEq,
Copy,
Clone,
TryFromPrimitive,
IntoPrimitive,
Debug,
AnchorSerialize,
AnchorDeserialize,
)]
#[repr(u8)]
pub enum OrderTreeType {
Bids,
Asks,
}
/// A binary tree on AnyNode::key()
///
/// The key encodes the price in the top 64 bits.
#[account(zero_copy)]
pub struct OrderTree {
// pub meta_data: MetaData,
// todo: do we want this type at this level?
pub order_tree_type: OrderTreeType,
pub padding: [u8; 3],
pub bump_index: u32,
pub free_list_len: u32,
pub free_list_head: NodeHandle,
pub root_node: NodeHandle,
pub leaf_count: u32,
pub nodes: [AnyNode; MAX_ORDERTREE_NODES],
pub reserved: [u8; 256],
}
const_assert_eq!(
std::mem::size_of::<OrderTree>(),
1 + 3 + 4 * 2 + 4 + 4 + 4 + 96 * 1024 + 256 // 98584
);
const_assert_eq!(std::mem::size_of::<OrderTree>() % 8, 0);
impl OrderTree {
/// Iterate over all entries, including invalid orders
///
/// smallest to highest for asks
/// highest to smallest for bids
pub fn iter(&self) -> OrderTreeIter {
OrderTreeIter::new(self)
}
pub fn node_mut(&mut self, key: NodeHandle) -> Option<&mut AnyNode> {
let node = &mut self.nodes[key as usize];
let tag = NodeTag::try_from(node.tag);
match tag {
Ok(NodeTag::InnerNode) | Ok(NodeTag::LeafNode) => Some(node),
_ => None,
}
}
pub fn node(&self, key: NodeHandle) -> Option<&AnyNode> {
let node = &self.nodes[key as usize];
let tag = NodeTag::try_from(node.tag);
match tag {
Ok(NodeTag::InnerNode) | Ok(NodeTag::LeafNode) => Some(node),
_ => None,
}
}
pub fn remove_min(&mut self) -> Option<LeafNode> {
self.remove_by_key(self.min_leaf()?.key)
}
pub fn remove_max(&mut self) -> Option<LeafNode> {
self.remove_by_key(self.max_leaf()?.key)
}
pub fn remove_worst(&mut self) -> Option<LeafNode> {
match self.order_tree_type {
OrderTreeType::Bids => self.remove_min(),
OrderTreeType::Asks => self.remove_max(),
}
}
/// Remove the order with the lowest expiry timestamp, if that's < now_ts.
pub fn remove_one_expired(&mut self, now_ts: u64) -> Option<LeafNode> {
let (expired_h, expires_at) = self.find_earliest_expiry()?;
if expires_at < now_ts {
self.remove_by_key(self.node(expired_h)?.key()?)
} else {
None
}
}
pub fn root(&self) -> Option<NodeHandle> {
if self.leaf_count == 0 {
None
} else {
Some(self.root_node)
}
}
#[cfg(test)]
#[allow(dead_code)]
fn to_price_quantity_vec(&self, reverse: bool) -> Vec<(i64, i64)> {
let mut pqs = vec![];
let mut current: NodeHandle = match self.root() {
None => return pqs,
Some(node_handle) => node_handle,
};
let left = reverse as usize;
let right = !reverse as usize;
let mut stack = vec![];
loop {
let root_contents = self.node(current).unwrap(); // should never fail unless book is already fucked
match root_contents.case().unwrap() {
NodeRef::Inner(inner) => {
stack.push(inner);
current = inner.children[left];
}
NodeRef::Leaf(leaf) => {
// if you hit leaf then pop stack and go right
// all inner nodes on stack have already been visited to the left
pqs.push((leaf.price(), leaf.quantity));
match stack.pop() {
None => return pqs,
Some(inner) => {
current = inner.children[right];
}
}
}
}
}
}
pub fn min_leaf(&self) -> Option<&LeafNode> {
self.leaf_min_max(false)
}
pub fn max_leaf(&self) -> Option<&LeafNode> {
self.leaf_min_max(true)
}
fn leaf_min_max(&self, find_max: bool) -> Option<&LeafNode> {
let mut root: NodeHandle = self.root()?;
let i = if find_max { 1 } else { 0 };
loop {
let root_contents = self.node(root)?;
match root_contents.case()? {
NodeRef::Inner(inner) => {
root = inner.children[i];
}
NodeRef::Leaf(leaf) => {
return Some(leaf);
}
}
}
}
pub fn remove_by_key(&mut self, search_key: u128) -> Option<LeafNode> {
// path of InnerNode handles that lead to the removed leaf
let mut stack: Vec<(NodeHandle, bool)> = vec![];
// special case potentially removing the root
let mut parent_h = self.root()?;
let (mut child_h, mut crit_bit) = match self.node(parent_h).unwrap().case().unwrap() {
NodeRef::Leaf(&leaf) if leaf.key == search_key => {
assert_eq!(self.leaf_count, 1);
self.root_node = 0;
self.leaf_count = 0;
let _old_root = self.remove(parent_h).unwrap();
return Some(leaf);
}
NodeRef::Leaf(_) => return None,
NodeRef::Inner(inner) => inner.walk_down(search_key),
};
stack.push((parent_h, crit_bit));
// walk down the tree until finding the key
loop {
match self.node(child_h).unwrap().case().unwrap() {
NodeRef::Inner(inner) => {
parent_h = child_h;
let (new_child_h, new_crit_bit) = inner.walk_down(search_key);
child_h = new_child_h;
crit_bit = new_crit_bit;
stack.push((parent_h, crit_bit));
}
NodeRef::Leaf(leaf) => {
if leaf.key != search_key {
return None;
}
break;
}
}
}
// replace parent with its remaining child node
// free child_h, replace *parent_h with *other_child_h, free other_child_h
let other_child_h = self.node(parent_h).unwrap().children().unwrap()[!crit_bit as usize];
let other_child_node_contents = self.remove(other_child_h).unwrap();
let new_expiry = other_child_node_contents.earliest_expiry();
*self.node_mut(parent_h).unwrap() = other_child_node_contents;
self.leaf_count -= 1;
let removed_leaf: LeafNode = cast(self.remove(child_h).unwrap());
// update child min expiry back up to the root
let outdated_expiry = removed_leaf.expiry();
stack.pop(); // the final parent has been replaced by the remaining leaf
self.update_parent_earliest_expiry(&stack, outdated_expiry, new_expiry);
Some(removed_leaf)
}
pub fn remove(&mut self, key: NodeHandle) -> Option<AnyNode> {
let val = *self.node(key)?;
self.nodes[key as usize] = cast(FreeNode {
tag: if self.free_list_len == 0 {
NodeTag::LastFreeNode.into()
} else {
NodeTag::FreeNode.into()
},
next: self.free_list_head,
reserved: [0; 88],
});
self.free_list_len += 1;
self.free_list_head = key;
Some(val)
}
pub fn insert(&mut self, val: &AnyNode) -> Result<NodeHandle> {
match NodeTag::try_from(val.tag) {
Ok(NodeTag::InnerNode) | Ok(NodeTag::LeafNode) => (),
_ => unreachable!(),
};
if self.free_list_len == 0 {
require!(
(self.bump_index as usize) < self.nodes.len() && self.bump_index < u32::MAX,
MangoError::SomeError // todo
);
self.nodes[self.bump_index as usize] = *val;
let key = self.bump_index;
self.bump_index += 1;
return Ok(key);
}
let key = self.free_list_head;
let node = &mut self.nodes[key as usize];
// TODO OPT possibly unnecessary require here - remove if we need compute
match NodeTag::try_from(node.tag) {
Ok(NodeTag::FreeNode) => assert!(self.free_list_len > 1),
Ok(NodeTag::LastFreeNode) => assert_eq!(self.free_list_len, 1),
_ => unreachable!(),
};
// TODO - test borrow requireer
self.free_list_head = cast_ref::<AnyNode, FreeNode>(node).next;
self.free_list_len -= 1;
*node = *val;
Ok(key)
}
pub fn insert_leaf(&mut self, new_leaf: &LeafNode) -> Result<(NodeHandle, Option<LeafNode>)> {
// path of InnerNode handles that lead to the new leaf
let mut stack: Vec<(NodeHandle, bool)> = vec![];
// deal with inserts into an empty tree
let mut root: NodeHandle = match self.root() {
Some(h) => h,
None => {
// create a new root if none exists
let handle = self.insert(new_leaf.as_ref())?;
self.root_node = handle;
self.leaf_count = 1;
return Ok((handle, None));
}
};
// walk down the tree until we find the insert location
loop {
// require if the new node will be a child of the root
let root_contents = *self.node(root).unwrap();
let root_key = root_contents.key().unwrap();
if root_key == new_leaf.key {
// This should never happen because key should never match
if let Some(NodeRef::Leaf(&old_root_as_leaf)) = root_contents.case() {
// clobber the existing leaf
*self.node_mut(root).unwrap() = *new_leaf.as_ref();
self.update_parent_earliest_expiry(
&stack,
old_root_as_leaf.expiry(),
new_leaf.expiry(),
);
return Ok((root, Some(old_root_as_leaf)));
}
// InnerNodes have a random child's key, so matching can happen and is fine
}
let shared_prefix_len: u32 = (root_key ^ new_leaf.key).leading_zeros();
match root_contents.case() {
None => unreachable!(),
Some(NodeRef::Inner(inner)) => {
let keep_old_root = shared_prefix_len >= inner.prefix_len;
if keep_old_root {
let (child, crit_bit) = inner.walk_down(new_leaf.key);
stack.push((root, crit_bit));
root = child;
continue;
};
}
_ => (),
};
// implies root is a Leaf or Inner where shared_prefix_len < prefix_len
// we'll replace root with a new InnerNode that has new_leaf and root as children
// change the root in place to represent the LCA of [new_leaf] and [root]
let crit_bit_mask: u128 = 1u128 << (127 - shared_prefix_len);
let new_leaf_crit_bit = (crit_bit_mask & new_leaf.key) != 0;
let old_root_crit_bit = !new_leaf_crit_bit;
let new_leaf_handle = self.insert(new_leaf.as_ref())?;
let moved_root_handle = match self.insert(&root_contents) {
Ok(h) => h,
Err(e) => {
self.remove(new_leaf_handle).unwrap();
return Err(e);
}
};
let new_root: &mut InnerNode = cast_mut(self.node_mut(root).unwrap());
*new_root = InnerNode::new(shared_prefix_len, new_leaf.key);
new_root.children[new_leaf_crit_bit as usize] = new_leaf_handle;
new_root.children[old_root_crit_bit as usize] = moved_root_handle;
let new_leaf_expiry = new_leaf.expiry();
let old_root_expiry = root_contents.earliest_expiry();
new_root.child_earliest_expiry[new_leaf_crit_bit as usize] = new_leaf_expiry;
new_root.child_earliest_expiry[old_root_crit_bit as usize] = old_root_expiry;
// walk up the stack and fix up the new min if needed
if new_leaf_expiry < old_root_expiry {
self.update_parent_earliest_expiry(&stack, old_root_expiry, new_leaf_expiry);
}
self.leaf_count += 1;
return Ok((new_leaf_handle, None));
}
}
pub fn is_full(&self) -> bool {
self.free_list_len <= 1 && (self.bump_index as usize) >= self.nodes.len() - 1
}
/// When a node changes, the parents' child_earliest_expiry may need to be updated.
///
/// This function walks up the `stack` of parents and applies the change where the
/// previous child's `outdated_expiry` is replaced by `new_expiry`.
pub fn update_parent_earliest_expiry(
&mut self,
stack: &[(NodeHandle, bool)],
mut outdated_expiry: u64,
mut new_expiry: u64,
) {
// Walk from the top of the stack to the root of the tree.
// Since the stack grows by appending, we need to iterate the slice in reverse order.
for (parent_h, crit_bit) in stack.iter().rev() {
let parent = self.node_mut(*parent_h).unwrap().as_inner_mut().unwrap();
if parent.child_earliest_expiry[*crit_bit as usize] != outdated_expiry {
break;
}
outdated_expiry = parent.earliest_expiry();
parent.child_earliest_expiry[*crit_bit as usize] = new_expiry;
new_expiry = parent.earliest_expiry();
}
}
/// Returns the handle of the node with the lowest expiry timestamp, and this timestamp
pub fn find_earliest_expiry(&self) -> Option<(NodeHandle, u64)> {
let mut current: NodeHandle = match self.root() {
Some(h) => h,
None => return None,
};
loop {
let contents = *self.node(current).unwrap();
match contents.case() {
None => unreachable!(),
Some(NodeRef::Inner(inner)) => {
current = inner.children[(inner.child_earliest_expiry[0]
> inner.child_earliest_expiry[1])
as usize];
}
_ => {
return Some((current, contents.earliest_expiry()));
}
};
}
}
}
#[cfg(test)]
mod tests {
use super::super::*;
use super::*;
use bytemuck::Zeroable;
fn new_order_tree(order_tree_type: OrderTreeType) -> OrderTree {
OrderTree {
order_tree_type,
padding: [0u8; 3],
bump_index: 0,
free_list_len: 0,
free_list_head: 0,
root_node: 0,
leaf_count: 0,
nodes: [AnyNode::zeroed(); MAX_ORDERTREE_NODES],
reserved: [0; 256],
}
}
fn verify_order_tree(order_tree: &OrderTree) {
verify_order_tree_invariant(order_tree);
verify_order_tree_iteration(order_tree);
verify_order_tree_expiry(order_tree);
}
// check that BookSide binary tree key invariant holds
fn verify_order_tree_invariant(order_tree: &OrderTree) {
let r = match order_tree.root() {
Some(h) => h,
None => return,
};
fn recursive_check(order_tree: &OrderTree, h: NodeHandle) {
match order_tree.node(h).unwrap().case().unwrap() {
NodeRef::Inner(&inner) => {
let left = order_tree.node(inner.children[0]).unwrap().key().unwrap();
let right = order_tree.node(inner.children[1]).unwrap().key().unwrap();
// the left and right keys share the InnerNode's prefix
assert!((inner.key ^ left).leading_zeros() >= inner.prefix_len);
assert!((inner.key ^ right).leading_zeros() >= inner.prefix_len);
// the left and right node key have the critbit unset and set respectively
let crit_bit_mask: u128 = 1u128 << (127 - inner.prefix_len);
assert!(left & crit_bit_mask == 0);
assert!(right & crit_bit_mask != 0);
recursive_check(order_tree, inner.children[0]);
recursive_check(order_tree, inner.children[1]);
}
_ => {}
}
}
recursive_check(order_tree, r);
}
// check that iteration of order tree has the right order and misses no leaves
fn verify_order_tree_iteration(order_tree: &OrderTree) {
let mut total = 0;
let ascending = order_tree.order_tree_type == OrderTreeType::Asks;
let mut last_key = if ascending { 0 } else { u128::MAX };
for (_, node) in order_tree.iter() {
let key = node.key;
if ascending {
assert!(key >= last_key);
} else {
assert!(key <= last_key);
}
last_key = key;
total += 1;
}
assert_eq!(order_tree.leaf_count, total);
}
// check that BookSide::child_expiry invariant holds
fn verify_order_tree_expiry(order_tree: &OrderTree) {
let r = match order_tree.root() {
Some(h) => h,
None => return,
};
fn recursive_check(order_tree: &OrderTree, h: NodeHandle) {
match order_tree.node(h).unwrap().case().unwrap() {
NodeRef::Inner(&inner) => {
let left = order_tree
.node(inner.children[0])
.unwrap()
.earliest_expiry();
let right = order_tree
.node(inner.children[1])
.unwrap()
.earliest_expiry();
// child_expiry must hold the expiry of the children
assert_eq!(inner.child_earliest_expiry[0], left);
assert_eq!(inner.child_earliest_expiry[1], right);
recursive_check(order_tree, inner.children[0]);
recursive_check(order_tree, inner.children[1]);
}
_ => {}
}
}
recursive_check(order_tree, r);
}
#[test]
fn order_tree_expiry_manual() {
let mut bids = new_order_tree(OrderTreeType::Bids);
let new_expiring_leaf = |key: u128, expiry: u64| {
LeafNode::new(
0,
key,
Pubkey::default(),
0,
0,
expiry - 1,
PostOrderType::Limit,
1,
-1,
)
};
assert!(bids.find_earliest_expiry().is_none());
bids.insert_leaf(&new_expiring_leaf(0, 5000)).unwrap();
assert_eq!(bids.find_earliest_expiry().unwrap(), (bids.root_node, 5000));
verify_order_tree(&bids);
let (new4000_h, _) = bids.insert_leaf(&new_expiring_leaf(1, 4000)).unwrap();
assert_eq!(bids.find_earliest_expiry().unwrap(), (new4000_h, 4000));
verify_order_tree(&bids);
let (_new4500_h, _) = bids.insert_leaf(&new_expiring_leaf(2, 4500)).unwrap();
assert_eq!(bids.find_earliest_expiry().unwrap(), (new4000_h, 4000));
verify_order_tree(&bids);
let (new3500_h, _) = bids.insert_leaf(&new_expiring_leaf(3, 3500)).unwrap();
assert_eq!(bids.find_earliest_expiry().unwrap(), (new3500_h, 3500));
verify_order_tree(&bids);
// the first two levels of the tree are innernodes, with 0;1 on one side and 2;3 on the other
assert_eq!(
bids.node_mut(bids.root_node)
.unwrap()
.as_inner_mut()
.unwrap()
.child_earliest_expiry,
[4000, 3500]
);
bids.remove_by_key(3).unwrap();
verify_order_tree(&bids);
assert_eq!(
bids.node_mut(bids.root_node)
.unwrap()
.as_inner_mut()
.unwrap()
.child_earliest_expiry,
[4000, 4500]
);
assert_eq!(bids.find_earliest_expiry().unwrap().1, 4000);
bids.remove_by_key(0).unwrap();
verify_order_tree(&bids);
assert_eq!(
bids.node_mut(bids.root_node)
.unwrap()
.as_inner_mut()
.unwrap()
.child_earliest_expiry,
[4000, 4500]
);
assert_eq!(bids.find_earliest_expiry().unwrap().1, 4000);
bids.remove_by_key(1).unwrap();
verify_order_tree(&bids);
assert_eq!(bids.find_earliest_expiry().unwrap().1, 4500);
bids.remove_by_key(2).unwrap();
verify_order_tree(&bids);
assert!(bids.find_earliest_expiry().is_none());
}
#[test]
fn order_tree_expiry_random() {
use rand::Rng;
let mut rng = rand::thread_rng();
let mut bids = new_order_tree(OrderTreeType::Bids);
let new_expiring_leaf = |key: u128, expiry: u64| {
LeafNode::new(
0,
key,
Pubkey::default(),
0,
0,
expiry - 1,
PostOrderType::Limit,
1,
-1,
)
};
// add 200 random leaves
let mut keys = vec![];
for _ in 0..200 {
let key: u128 = rng.gen_range(0..10000); // overlap in key bits
if keys.contains(&key) {
continue;
}
let expiry = rng.gen_range(1..200); // give good chance of duplicate expiry times
keys.push(key);
bids.insert_leaf(&new_expiring_leaf(key, expiry)).unwrap();
verify_order_tree(&bids);
}
// remove 50 at random
for _ in 0..50 {
if keys.len() == 0 {
break;
}
let k = keys[rng.gen_range(0..keys.len())];
bids.remove_by_key(k).unwrap();
keys.retain(|v| *v != k);
verify_order_tree(&bids);
}
}
}

View File

@ -0,0 +1,88 @@
use super::*;
/// Iterate over orders in order (bids=descending, asks=ascending)
pub struct OrderTreeIter<'a> {
order_tree: &'a OrderTree,
/// InnerNodes where the right side still needs to be iterated on
stack: Vec<&'a InnerNode>,
/// To be returned on `next()`
next_leaf: Option<(NodeHandle, &'a LeafNode)>,
/// either 0, 1 to iterate low-to-high, or 1, 0 to iterate high-to-low
left: usize,
right: usize,
}
impl<'a> OrderTreeIter<'a> {
pub fn new(order_tree: &'a OrderTree) -> Self {
let (left, right) = if order_tree.order_tree_type == OrderTreeType::Bids {
(1, 0)
} else {
(0, 1)
};
let stack = vec![];
let mut iter = Self {
order_tree,
stack,
next_leaf: None,
left,
right,
};
if order_tree.leaf_count != 0 {
iter.next_leaf = iter.find_leftmost_leaf(order_tree.root_node);
}
iter
}
pub fn side(&self) -> Side {
if self.left == 1 {
Side::Bid
} else {
Side::Ask
}
}
pub fn peek(&self) -> Option<(NodeHandle, &'a LeafNode)> {
self.next_leaf
}
fn find_leftmost_leaf(&mut self, start: NodeHandle) -> Option<(NodeHandle, &'a LeafNode)> {
let mut current = start;
loop {
match self.order_tree.node(current).unwrap().case().unwrap() {
NodeRef::Inner(inner) => {
self.stack.push(inner);
current = inner.children[self.left];
}
NodeRef::Leaf(leaf) => {
return Some((current, leaf));
}
}
}
}
}
impl<'a> Iterator for OrderTreeIter<'a> {
type Item = (NodeHandle, &'a LeafNode);
fn next(&mut self) -> Option<Self::Item> {
// no next leaf? done
if self.next_leaf.is_none() {
return None;
}
// start popping from stack and get the other child
let current_leaf = self.next_leaf;
self.next_leaf = match self.stack.pop() {
None => None,
Some(inner) => {
let start = inner.children[self.right];
// go down the left branch as much as possible until reaching a leaf
self.find_leftmost_leaf(start)
}
};
current_leaf
}
}

View File

@ -187,7 +187,7 @@ pub struct FillEvent {
pub seq_num: u64,
pub maker: Pubkey,
pub maker_order_id: i128,
pub maker_order_id: u128,
pub maker_client_order_id: u64,
pub maker_fee: I80F48,
@ -195,7 +195,7 @@ pub struct FillEvent {
pub maker_timestamp: u64,
pub taker: Pubkey,
pub taker_order_id: i128,
pub taker_order_id: u128,
pub taker_client_order_id: u64,
pub taker_fee: I80F48,
@ -215,13 +215,13 @@ impl FillEvent {
timestamp: u64,
seq_num: u64,
maker: Pubkey,
maker_order_id: i128,
maker_order_id: u128,
maker_client_order_id: u64,
maker_fee: I80F48,
maker_timestamp: u64,
taker: Pubkey,
taker_order_id: i128,
taker_order_id: u128,
taker_client_order_id: u64,
taker_fee: I80F48,
price: i64,

View File

@ -6,11 +6,11 @@ use fixed::types::I80F48;
use static_assertions::const_assert_eq;
use crate::accounts_zerocopy::KeyedAccountReader;
use crate::state::orderbook::order_type::Side;
use crate::state::orderbook::Side;
use crate::state::{oracle, TokenIndex};
use crate::util::checked_math as cm;
use super::{Book, OracleConfig, DAY_I80F48};
use super::{orderbook, OracleConfig, OrderBook, DAY_I80F48};
pub type PerpMarketIndex = u16;
@ -39,8 +39,8 @@ pub struct PerpMarket {
pub oracle_config: OracleConfig,
pub bids: Pubkey,
pub asks: Pubkey,
pub orderbook: Pubkey,
pub padding3: [u8; 32],
pub event_queue: Pubkey,
@ -133,14 +133,9 @@ impl PerpMarket {
self.trusted_market == 1
}
pub fn gen_order_id(&mut self, side: Side, price: i64) -> i128 {
pub fn gen_order_id(&mut self, side: Side, price_data: u64) -> u128 {
self.seq_num += 1;
let upper = (price as i128) << 64;
match side {
Side::Bid => upper | (!self.seq_num as i128),
Side::Ask => upper | (self.seq_num as i128),
}
orderbook::new_node_key(side, price_data, self.seq_num)
}
pub fn oracle_price(&self, oracle_acc: &impl KeyedAccountReader) -> Result<I80F48> {
@ -153,12 +148,18 @@ impl PerpMarket {
}
/// Use current order book price and index price to update the instantaneous funding
pub fn update_funding(&mut self, book: &Book, oracle_price: I80F48, now_ts: u64) -> Result<()> {
pub fn update_funding(
&mut self,
book: &OrderBook,
oracle_price: I80F48,
now_ts: u64,
) -> Result<()> {
let index_price = oracle_price;
let oracle_price_lots = self.native_price_to_lot(oracle_price);
// Get current book price & compare it to index price
let bid = book.impact_price(Side::Bid, self.impact_quantity, now_ts);
let ask = book.impact_price(Side::Ask, self.impact_quantity, now_ts);
let bid = book.impact_price(Side::Bid, self.impact_quantity, now_ts, oracle_price_lots);
let ask = book.impact_price(Side::Ask, self.impact_quantity, now_ts, oracle_price_lots);
let diff_price = match (bid, ask) {
(Some(bid), Some(ask)) => {
@ -247,8 +248,7 @@ impl PerpMarket {
oracle_config: OracleConfig {
conf_filter: I80F48::ZERO,
},
bids: Pubkey::new_unique(),
asks: Pubkey::new_unique(),
orderbook: Pubkey::new_unique(),
event_queue: Pubkey::new_unique(),
quote_lot_size: 1,
base_lot_size: 1,
@ -274,6 +274,7 @@ impl PerpMarket {
reserved: [0; 92],
padding1: Default::default(),
padding2: Default::default(),
padding3: Default::default(),
registration_time: 0,
fee_penalty: 0.0,
trusted_market: 0,

View File

@ -2197,8 +2197,7 @@ pub struct PerpCreateMarketInstruction {
pub group: Pubkey,
pub admin: TestKeypair,
pub oracle: Pubkey,
pub asks: Pubkey,
pub bids: Pubkey,
pub orderbook: Pubkey,
pub event_queue: Pubkey,
pub payer: TestKeypair,
pub settle_token_index: TokenIndex,
@ -2226,11 +2225,8 @@ impl PerpCreateMarketInstruction {
base: &crate::mango_setup::Token,
) -> Self {
PerpCreateMarketInstruction {
asks: solana
.create_account_for_type::<BookSide>(&mango_v4::id())
.await,
bids: solana
.create_account_for_type::<BookSide>(&mango_v4::id())
orderbook: solana
.create_account_for_type::<OrderBook>(&mango_v4::id())
.await,
event_queue: solana
.create_account_for_type::<EventQueue>(&mango_v4::id())
@ -2293,8 +2289,7 @@ impl ClientInstruction for PerpCreateMarketInstruction {
admin: self.admin.pubkey(),
oracle: self.oracle,
perp_market,
asks: self.asks,
bids: self.bids,
orderbook: self.orderbook,
event_queue: self.event_queue,
payer: self.payer.pubkey(),
system_program: System::id(),
@ -2310,12 +2305,8 @@ impl ClientInstruction for PerpCreateMarketInstruction {
}
pub struct PerpCloseMarketInstruction {
pub group: Pubkey,
pub admin: TestKeypair,
pub perp_market: Pubkey,
pub asks: Pubkey,
pub bids: Pubkey,
pub event_queue: Pubkey,
pub sol_destination: Pubkey,
}
#[async_trait::async_trait(?Send)]
@ -2324,18 +2315,18 @@ impl ClientInstruction for PerpCloseMarketInstruction {
type Instruction = mango_v4::instruction::PerpCloseMarket;
async fn to_instruction(
&self,
_loader: impl ClientAccountLoader + 'async_trait,
account_loader: impl ClientAccountLoader + 'async_trait,
) -> (Self::Accounts, instruction::Instruction) {
let program_id = mango_v4::id();
let instruction = Self::Instruction {};
let perp_market: PerpMarket = account_loader.load(&self.perp_market).await.unwrap();
let accounts = Self::Accounts {
group: self.group,
group: perp_market.group,
admin: self.admin.pubkey(),
perp_market: self.perp_market,
asks: self.asks,
bids: self.bids,
event_queue: self.event_queue,
orderbook: perp_market.orderbook,
event_queue: perp_market.event_queue,
token_program: Token::id(),
sol_destination: self.sol_destination,
};
@ -2407,9 +2398,9 @@ impl ClientInstruction for PerpPlaceOrderInstruction {
max_base_lots: self.max_base_lots,
max_quote_lots: self.max_quote_lots,
client_order_id: self.client_order_id,
order_type: OrderType::Limit,
order_type: PlaceOrderType::Limit,
expiry_timestamp: 0,
limit: 1,
limit: 10,
};
let perp_market: PerpMarket = account_loader.load(&self.perp_market).await.unwrap();
@ -2430,8 +2421,73 @@ impl ClientInstruction for PerpPlaceOrderInstruction {
group: account.fixed.group,
account: self.account,
perp_market: self.perp_market,
asks: perp_market.asks,
bids: perp_market.bids,
orderbook: perp_market.orderbook,
event_queue: perp_market.event_queue,
oracle: perp_market.oracle,
owner: self.owner.pubkey(),
};
let mut instruction = make_instruction(program_id, &accounts, instruction);
instruction.accounts.extend(health_check_metas);
(accounts, instruction)
}
fn signers(&self) -> Vec<TestKeypair> {
vec![self.owner]
}
}
pub struct PerpPlaceOrderPeggedInstruction {
pub account: Pubkey,
pub perp_market: Pubkey,
pub owner: TestKeypair,
pub side: Side,
pub price_offset: i64,
pub max_base_lots: i64,
pub max_quote_lots: i64,
pub client_order_id: u64,
pub peg_limit: i64,
}
#[async_trait::async_trait(?Send)]
impl ClientInstruction for PerpPlaceOrderPeggedInstruction {
type Accounts = mango_v4::accounts::PerpPlaceOrder;
type Instruction = mango_v4::instruction::PerpPlaceOrderPegged;
async fn to_instruction(
&self,
account_loader: impl ClientAccountLoader + 'async_trait,
) -> (Self::Accounts, instruction::Instruction) {
let program_id = mango_v4::id();
let instruction = Self::Instruction {
side: self.side,
price_offset_lots: self.price_offset,
peg_limit: self.peg_limit,
max_base_lots: self.max_base_lots,
max_quote_lots: self.max_quote_lots,
client_order_id: self.client_order_id,
order_type: PlaceOrderType::Limit,
expiry_timestamp: 0,
limit: 10,
};
let perp_market: PerpMarket = account_loader.load(&self.perp_market).await.unwrap();
let account = account_loader
.load_mango_account(&self.account)
.await
.unwrap();
let health_check_metas = derive_health_check_remaining_account_metas(
&account_loader,
&account,
None,
false,
Some(perp_market.perp_market_index),
)
.await;
let accounts = Self::Accounts {
group: account.fixed.group,
account: self.account,
perp_market: self.perp_market,
orderbook: perp_market.orderbook,
event_queue: perp_market.event_queue,
oracle: perp_market.oracle,
owner: self.owner.pubkey(),
@ -2448,13 +2504,10 @@ impl ClientInstruction for PerpPlaceOrderInstruction {
}
pub struct PerpCancelOrderInstruction {
pub group: Pubkey,
pub account: Pubkey,
pub perp_market: Pubkey,
pub asks: Pubkey,
pub bids: Pubkey,
pub owner: TestKeypair,
pub order_id: i128,
pub order_id: u128,
}
#[async_trait::async_trait(?Send)]
impl ClientInstruction for PerpCancelOrderInstruction {
@ -2462,18 +2515,18 @@ impl ClientInstruction for PerpCancelOrderInstruction {
type Instruction = mango_v4::instruction::PerpCancelOrder;
async fn to_instruction(
&self,
_loader: impl ClientAccountLoader + 'async_trait,
account_loader: impl ClientAccountLoader + 'async_trait,
) -> (Self::Accounts, instruction::Instruction) {
let program_id = mango_v4::id();
let instruction = Self::Instruction {
order_id: self.order_id,
};
let perp_market: PerpMarket = account_loader.load(&self.perp_market).await.unwrap();
let accounts = Self::Accounts {
group: self.group,
group: perp_market.group,
account: self.account,
perp_market: self.perp_market,
asks: self.asks,
bids: self.bids,
orderbook: perp_market.orderbook,
owner: self.owner.pubkey(),
};
@ -2487,11 +2540,8 @@ impl ClientInstruction for PerpCancelOrderInstruction {
}
pub struct PerpCancelOrderByClientOrderIdInstruction {
pub group: Pubkey,
pub account: Pubkey,
pub perp_market: Pubkey,
pub asks: Pubkey,
pub bids: Pubkey,
pub owner: TestKeypair,
pub client_order_id: u64,
}
@ -2501,18 +2551,18 @@ impl ClientInstruction for PerpCancelOrderByClientOrderIdInstruction {
type Instruction = mango_v4::instruction::PerpCancelOrderByClientOrderId;
async fn to_instruction(
&self,
_loader: impl ClientAccountLoader + 'async_trait,
account_loader: impl ClientAccountLoader + 'async_trait,
) -> (Self::Accounts, instruction::Instruction) {
let program_id = mango_v4::id();
let instruction = Self::Instruction {
client_order_id: self.client_order_id,
};
let perp_market: PerpMarket = account_loader.load(&self.perp_market).await.unwrap();
let accounts = Self::Accounts {
group: self.group,
group: perp_market.group,
account: self.account,
perp_market: self.perp_market,
asks: self.asks,
bids: self.bids,
orderbook: perp_market.orderbook,
owner: self.owner.pubkey(),
};
@ -2545,8 +2595,7 @@ impl ClientInstruction for PerpCancelAllOrdersInstruction {
group: perp_market.group,
account: self.account,
perp_market: self.perp_market,
asks: perp_market.asks,
bids: perp_market.bids,
orderbook: perp_market.orderbook,
owner: self.owner.pubkey(),
};
@ -2598,10 +2647,7 @@ impl ClientInstruction for PerpConsumeEventsInstruction {
}
pub struct PerpUpdateFundingInstruction {
pub group: Pubkey,
pub perp_market: Pubkey,
pub bids: Pubkey,
pub asks: Pubkey,
pub bank: Pubkey,
pub oracle: Pubkey,
}
@ -2611,15 +2657,15 @@ impl ClientInstruction for PerpUpdateFundingInstruction {
type Instruction = mango_v4::instruction::PerpUpdateFunding;
async fn to_instruction(
&self,
_loader: impl ClientAccountLoader + 'async_trait,
account_loader: impl ClientAccountLoader + 'async_trait,
) -> (Self::Accounts, instruction::Instruction) {
let program_id = mango_v4::id();
let instruction = Self::Instruction {};
let perp_market: PerpMarket = account_loader.load(&self.perp_market).await.unwrap();
let accounts = Self::Accounts {
group: self.group,
group: perp_market.group,
perp_market: self.perp_market,
bids: self.bids,
asks: self.asks,
orderbook: perp_market.orderbook,
oracle: self.oracle,
};
@ -2781,8 +2827,7 @@ impl ClientInstruction for PerpLiqForceCancelOrdersInstruction {
group: account.fixed.group,
perp_market: self.perp_market,
account: self.account,
bids: perp_market.bids,
asks: perp_market.asks,
orderbook: perp_market.orderbook,
oracle: perp_market.oracle,
};
let mut instruction = make_instruction(program_id, &accounts, instruction);

View File

@ -217,19 +217,18 @@ impl SolanaCookie {
}
pub async fn get_account_opt<T: AccountDeserialize>(&self, address: Pubkey) -> Option<T> {
self.context
.borrow_mut()
.banks_client
.get_account(address)
.await
.unwrap()
.unwrap();
let data = self.get_account_data(address).await?;
let mut data_slice: &[u8] = &data;
AccountDeserialize::try_deserialize(&mut data_slice).ok()
}
// Use when accounts are too big for the stack
pub async fn get_account_boxed<T: AccountDeserialize>(&self, address: Pubkey) -> Box<T> {
let data = self.get_account_data(address).await.unwrap();
let mut data_slice: &[u8] = &data;
Box::new(AccountDeserialize::try_deserialize(&mut data_slice).unwrap())
}
pub async fn get_account<T: AccountDeserialize>(&self, address: Pubkey) -> T {
self.get_account_opt(address).await.unwrap()
}

View File

@ -203,13 +203,7 @@ async fn test_health_compute_perp() -> Result<(), TransportError> {
//
let mut perp_markets = vec![];
for (perp_market_index, token) in tokens[1..].iter().enumerate() {
let mango_v4::accounts::PerpCreateMarket {
perp_market,
asks,
bids,
event_queue,
..
} = send_tx(
let mango_v4::accounts::PerpCreateMarket { perp_market, .. } = send_tx(
solana,
PerpCreateMarketInstruction {
group,
@ -231,18 +225,18 @@ async fn test_health_compute_perp() -> Result<(), TransportError> {
.await
.unwrap();
perp_markets.push((perp_market, asks, bids, event_queue));
perp_markets.push(perp_market);
}
let price_lots = {
let perp_market = solana.get_account::<PerpMarket>(perp_markets[0].0).await;
let perp_market = solana.get_account::<PerpMarket>(perp_markets[0]).await;
perp_market.native_price_to_lot(I80F48::from(1))
};
//
// TEST: Create a perp order for each market
//
for (i, &(perp_market, _asks, _bids, _event_queue)) in perp_markets.iter().enumerate() {
for (i, &perp_market) in perp_markets.iter().enumerate() {
println!("adding market {}", i);
send_tx(
solana,

View File

@ -13,7 +13,7 @@ use utils::assert_equal_fixed_f64 as assert_equal;
mod program_test;
#[tokio::test]
async fn test_perp() -> Result<(), TransportError> {
async fn test_perp_fixed() -> Result<(), TransportError> {
let context = TestContext::new().await;
let solana = &context.solana.clone();
@ -67,9 +67,7 @@ async fn test_perp() -> Result<(), TransportError> {
//
let mango_v4::accounts::PerpCreateMarket {
perp_market,
asks,
bids,
event_queue,
orderbook,
..
} = send_tx(
solana,
@ -118,19 +116,18 @@ async fn test_perp() -> Result<(), TransportError> {
.unwrap();
check_prev_instruction_post_health(&solana, account_0).await;
let orderbook_data = solana.get_account_boxed::<OrderBook>(orderbook).await;
assert_eq!(orderbook_data.bids.fixed.leaf_count, 1);
let order_id_to_cancel = solana
.get_account::<MangoAccount>(account_0)
.await
.perp_open_orders[0]
.order_id;
.id;
send_tx(
solana,
PerpCancelOrderInstruction {
group,
account: account_0,
perp_market,
asks,
bids,
owner,
order_id: order_id_to_cancel,
},
@ -163,11 +160,8 @@ async fn test_perp() -> Result<(), TransportError> {
send_tx(
solana,
PerpCancelOrderByClientOrderIdInstruction {
group,
account: account_0,
perp_market,
asks,
bids,
owner,
client_order_id: 1,
},
@ -198,12 +192,13 @@ async fn test_perp() -> Result<(), TransportError> {
check_prev_instruction_post_health(&solana, account_0).await;
send_tx(
solana,
PerpPlaceOrderInstruction {
PerpPlaceOrderPeggedInstruction {
account: account_0,
perp_market,
owner,
side: Side::Bid,
price_lots,
price_offset: -1,
peg_limit: -1,
max_base_lots: 1,
max_quote_lots: i64::MAX,
client_order_id: 3,
@ -445,12 +440,8 @@ async fn test_perp() -> Result<(), TransportError> {
send_tx(
solana,
PerpCloseMarketInstruction {
group,
admin,
perp_market,
asks,
bids,
event_queue,
sol_destination: payer.pubkey(),
},
)
@ -460,13 +451,433 @@ async fn test_perp() -> Result<(), TransportError> {
Ok(())
}
#[tokio::test]
async fn test_perp_oracle_peg() -> Result<(), TransportError> {
let context = TestContext::new().await;
let solana = &context.solana.clone();
let admin = TestKeypair::new();
let owner = context.users[0].key;
let payer = context.users[1].key;
let mints = &context.mints[0..2];
//
// SETUP: Create a group and an account
//
let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
admin,
payer,
mints: mints.to_vec(),
..GroupWithTokensConfig::default()
}
.create(solana)
.await;
let deposit_amount = 100000;
let account_0 = create_funded_account(
&solana,
group,
owner,
0,
&context.users[1],
mints,
deposit_amount,
0,
)
.await;
let account_1 = create_funded_account(
&solana,
group,
owner,
1,
&context.users[1],
mints,
deposit_amount,
0,
)
.await;
//
// SETUP: Create a perp market
//
let mango_v4::accounts::PerpCreateMarket {
perp_market,
orderbook,
..
} = send_tx(
solana,
PerpCreateMarketInstruction {
group,
admin,
payer,
perp_market_index: 0,
quote_lot_size: 10,
base_lot_size: 10000,
maint_asset_weight: 0.975,
init_asset_weight: 0.95,
maint_liab_weight: 1.025,
init_liab_weight: 1.05,
liquidation_fee: 0.012,
maker_fee: -0.0001,
taker_fee: 0.0002,
..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, &tokens[0]).await
},
)
.await
.unwrap();
let price_lots = {
let perp_market = solana.get_account::<PerpMarket>(perp_market).await;
perp_market.native_price_to_lot(I80F48!(1))
};
assert_eq!(price_lots, 1000);
//
// TEST: Place and cancel order with order_id
//
send_tx(
solana,
PerpPlaceOrderPeggedInstruction {
account: account_0,
perp_market,
owner,
side: Side::Bid,
price_offset: -1,
peg_limit: -1,
max_base_lots: 1,
max_quote_lots: i64::MAX,
client_order_id: 0,
},
)
.await
.unwrap();
check_prev_instruction_post_health(&solana, account_0).await;
let orderbook_data = solana.get_account_boxed::<OrderBook>(orderbook).await;
assert_eq!(orderbook_data.bids.oracle_pegged.leaf_count, 1);
let perp_order = solana
.get_account::<MangoAccount>(account_0)
.await
.perp_open_orders[0];
assert_eq!(perp_order.side_and_tree, SideAndOrderTree::BidOraclePegged);
send_tx(
solana,
PerpCancelOrderInstruction {
account: account_0,
perp_market,
owner,
order_id: perp_order.id,
},
)
.await
.unwrap();
assert_no_perp_orders(solana, account_0).await;
//
// TEST: Place a pegged bid, take it with a direct and pegged ask, and consume events
//
send_tx(
solana,
PerpPlaceOrderPeggedInstruction {
account: account_0,
perp_market,
owner,
side: Side::Bid,
price_offset: 0,
peg_limit: -1,
max_base_lots: 2,
max_quote_lots: i64::MAX,
client_order_id: 5,
},
)
.await
.unwrap();
check_prev_instruction_post_health(&solana, account_0).await;
send_tx(
solana,
PerpPlaceOrderInstruction {
account: account_1,
perp_market,
owner,
side: Side::Ask,
price_lots,
max_base_lots: 1,
max_quote_lots: i64::MAX,
client_order_id: 6,
},
)
.await
.unwrap();
check_prev_instruction_post_health(&solana, account_1).await;
send_tx(
solana,
PerpPlaceOrderPeggedInstruction {
account: account_1,
perp_market,
owner,
side: Side::Ask,
price_offset: 0,
peg_limit: -1,
max_base_lots: 1,
max_quote_lots: i64::MAX,
client_order_id: 7,
},
)
.await
.unwrap();
check_prev_instruction_post_health(&solana, account_1).await;
send_tx(
solana,
PerpConsumeEventsInstruction {
perp_market,
mango_accounts: vec![account_0, account_1],
},
)
.await
.unwrap();
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
assert_eq!(mango_account_0.perps[0].base_position_lots(), 2);
assert!(assert_equal(
mango_account_0.perps[0].quote_position_native(),
-19998.0,
0.001
));
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
assert_eq!(mango_account_1.perps[0].base_position_lots(), -2);
assert!(assert_equal(
mango_account_1.perps[0].quote_position_native(),
19996.0,
0.001
));
//
// TEST: Place a pegged order and check how it behaves with oracle changes
//
send_tx(
solana,
PerpPlaceOrderPeggedInstruction {
account: account_0,
perp_market,
owner,
side: Side::Bid,
price_offset: -1,
peg_limit: -1,
max_base_lots: 2,
max_quote_lots: i64::MAX,
client_order_id: 5,
},
)
.await
.unwrap();
// TEST: an ask at current oracle price does not match
send_tx(
solana,
PerpPlaceOrderInstruction {
account: account_1,
perp_market,
owner,
side: Side::Ask,
price_lots,
max_base_lots: 1,
max_quote_lots: i64::MAX,
client_order_id: 60,
},
)
.await
.unwrap();
send_tx(
solana,
PerpCancelOrderByClientOrderIdInstruction {
account: account_1,
perp_market,
owner,
client_order_id: 60,
},
)
.await
.unwrap();
// TEST: Change the oracle, now the ask matches
send_tx(
solana,
StubOracleSetInstruction {
group,
admin,
mint: mints[0].pubkey,
payer,
price: "1.002",
},
)
.await
.unwrap();
send_tx(
solana,
PerpPlaceOrderInstruction {
account: account_1,
perp_market,
owner,
side: Side::Ask,
price_lots,
max_base_lots: 2,
max_quote_lots: i64::MAX,
client_order_id: 61,
},
)
.await
.unwrap();
send_tx(
solana,
PerpConsumeEventsInstruction {
perp_market,
mango_accounts: vec![account_0, account_1],
},
)
.await
.unwrap();
assert_no_perp_orders(solana, account_0).await;
// restore the oracle to default
send_tx(
solana,
StubOracleSetInstruction {
group,
admin,
mint: mints[0].pubkey,
payer,
price: "1.0",
},
)
.await
.unwrap();
//
// TEST: order is cancelled when the price exceeds the peg limit
//
send_tx(
solana,
PerpPlaceOrderPeggedInstruction {
account: account_0,
perp_market,
owner,
side: Side::Bid,
price_offset: -1,
peg_limit: price_lots + 2,
max_base_lots: 2,
max_quote_lots: i64::MAX,
client_order_id: 5,
},
)
.await
.unwrap();
// order is still matchable when exactly at the peg limit
send_tx(
solana,
StubOracleSetInstruction {
group,
admin,
mint: mints[0].pubkey,
payer,
price: "1.003",
},
)
.await
.unwrap();
send_tx(
solana,
PerpPlaceOrderInstruction {
account: account_1,
perp_market,
owner,
side: Side::Ask,
price_lots: price_lots + 2,
max_base_lots: 1,
max_quote_lots: i64::MAX,
client_order_id: 62,
},
)
.await
.unwrap();
assert!(send_tx(
solana,
PerpCancelOrderByClientOrderIdInstruction {
account: account_1,
perp_market,
owner,
client_order_id: 62,
},
)
.await
.is_err());
// but once the adjusted price is > the peg limit, it's gone
send_tx(
solana,
StubOracleSetInstruction {
group,
admin,
mint: mints[0].pubkey,
payer,
price: "1.004",
},
)
.await
.unwrap();
send_tx(
solana,
PerpPlaceOrderInstruction {
account: account_1,
perp_market,
owner,
side: Side::Ask,
price_lots: price_lots + 3,
max_base_lots: 1,
max_quote_lots: i64::MAX,
client_order_id: 63,
},
)
.await
.unwrap();
send_tx(
solana,
PerpCancelOrderByClientOrderIdInstruction {
account: account_1,
perp_market,
owner,
client_order_id: 63,
},
)
.await
.unwrap();
send_tx(
solana,
PerpConsumeEventsInstruction {
perp_market,
mango_accounts: vec![account_0, account_1],
},
)
.await
.unwrap();
assert_no_perp_orders(solana, account_0).await;
Ok(())
}
async fn assert_no_perp_orders(solana: &SolanaCookie, account_0: Pubkey) {
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
for oo in mango_account_0.perp_open_orders.iter() {
assert!(oo.order_id == 0);
assert!(oo.order_side == Side::Bid);
assert!(oo.client_order_id == 0);
assert!(oo.order_market == FREE_ORDER_SLOT);
assert!(oo.id == 0);
assert!(oo.side_and_tree == SideAndOrderTree::BidFixed);
assert!(oo.client_id == 0);
assert!(oo.market == FREE_ORDER_SLOT);
}
}

File diff suppressed because it is too large Load Diff