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:
parent
2b8e976956
commit
5731ce8faa
|
@ -1,7 +1,7 @@
|
||||||
use anchor_lang::prelude::*;
|
use anchor_lang::prelude::*;
|
||||||
|
|
||||||
use crate::error::MangoError;
|
use crate::error::MangoError;
|
||||||
use crate::state::{AccountLoaderDynamic, Book, BookSide, Group, MangoAccount, PerpMarket};
|
use crate::state::{AccountLoaderDynamic, Group, MangoAccount, OrderBook, PerpMarket};
|
||||||
|
|
||||||
#[derive(Accounts)]
|
#[derive(Accounts)]
|
||||||
pub struct PerpCancelAllOrders<'info> {
|
pub struct PerpCancelAllOrders<'info> {
|
||||||
|
@ -14,14 +14,11 @@ pub struct PerpCancelAllOrders<'info> {
|
||||||
#[account(
|
#[account(
|
||||||
mut,
|
mut,
|
||||||
has_one = group,
|
has_one = group,
|
||||||
has_one = bids,
|
has_one = orderbook,
|
||||||
has_one = asks
|
|
||||||
)]
|
)]
|
||||||
pub perp_market: AccountLoader<'info, PerpMarket>,
|
pub perp_market: AccountLoader<'info, PerpMarket>,
|
||||||
#[account(mut)]
|
#[account(mut)]
|
||||||
pub asks: AccountLoader<'info, BookSide>,
|
pub orderbook: AccountLoader<'info, OrderBook>,
|
||||||
#[account(mut)]
|
|
||||||
pub bids: AccountLoader<'info, BookSide>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn perp_cancel_all_orders(ctx: Context<PerpCancelAllOrders>, limit: u8) -> Result<()> {
|
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 mut perp_market = ctx.accounts.perp_market.load_mut()?;
|
||||||
let bids = ctx.accounts.bids.load_mut()?;
|
let mut book = ctx.accounts.orderbook.load_mut()?;
|
||||||
let asks = ctx.accounts.asks.load_mut()?;
|
|
||||||
let mut book = Book::new(bids, asks);
|
|
||||||
|
|
||||||
book.cancel_all_orders(&mut account.borrow_mut(), &mut perp_market, limit, None)?;
|
book.cancel_all_orders(&mut account.borrow_mut(), &mut perp_market, limit, None)?;
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use anchor_lang::prelude::*;
|
use anchor_lang::prelude::*;
|
||||||
|
|
||||||
use crate::error::MangoError;
|
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)]
|
#[derive(Accounts)]
|
||||||
pub struct PerpCancelAllOrdersBySide<'info> {
|
pub struct PerpCancelAllOrdersBySide<'info> {
|
||||||
|
@ -14,14 +14,11 @@ pub struct PerpCancelAllOrdersBySide<'info> {
|
||||||
#[account(
|
#[account(
|
||||||
mut,
|
mut,
|
||||||
has_one = group,
|
has_one = group,
|
||||||
has_one = bids,
|
has_one = orderbook,
|
||||||
has_one = asks
|
|
||||||
)]
|
)]
|
||||||
pub perp_market: AccountLoader<'info, PerpMarket>,
|
pub perp_market: AccountLoader<'info, PerpMarket>,
|
||||||
#[account(mut)]
|
#[account(mut)]
|
||||||
pub asks: AccountLoader<'info, BookSide>,
|
pub orderbook: AccountLoader<'info, OrderBook>,
|
||||||
#[account(mut)]
|
|
||||||
pub bids: AccountLoader<'info, BookSide>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn perp_cancel_all_orders_by_side(
|
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 mut perp_market = ctx.accounts.perp_market.load_mut()?;
|
||||||
let bids = ctx.accounts.bids.load_mut()?;
|
let mut book = ctx.accounts.orderbook.load_mut()?;
|
||||||
let asks = ctx.accounts.asks.load_mut()?;
|
|
||||||
let mut book = Book::new(bids, asks);
|
|
||||||
|
|
||||||
book.cancel_all_orders(
|
book.cancel_all_orders(
|
||||||
&mut account.borrow_mut(),
|
&mut account.borrow_mut(),
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use anchor_lang::prelude::*;
|
use anchor_lang::prelude::*;
|
||||||
|
|
||||||
use crate::error::*;
|
use crate::error::*;
|
||||||
use crate::state::{AccountLoaderDynamic, Book, BookSide, Group, MangoAccount, PerpMarket};
|
use crate::state::{AccountLoaderDynamic, Group, MangoAccount, OrderBook, PerpMarket};
|
||||||
|
|
||||||
#[derive(Accounts)]
|
#[derive(Accounts)]
|
||||||
pub struct PerpCancelOrder<'info> {
|
pub struct PerpCancelOrder<'info> {
|
||||||
|
@ -14,17 +14,14 @@ pub struct PerpCancelOrder<'info> {
|
||||||
#[account(
|
#[account(
|
||||||
mut,
|
mut,
|
||||||
has_one = group,
|
has_one = group,
|
||||||
has_one = bids,
|
has_one = orderbook,
|
||||||
has_one = asks
|
|
||||||
)]
|
)]
|
||||||
pub perp_market: AccountLoader<'info, PerpMarket>,
|
pub perp_market: AccountLoader<'info, PerpMarket>,
|
||||||
#[account(mut)]
|
#[account(mut)]
|
||||||
pub asks: AccountLoader<'info, BookSide>,
|
pub orderbook: AccountLoader<'info, OrderBook>,
|
||||||
#[account(mut)]
|
|
||||||
pub bids: AccountLoader<'info, BookSide>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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()?;
|
let mut account = ctx.accounts.account.load_mut()?;
|
||||||
require!(
|
require!(
|
||||||
account.fixed.is_owner_or_delegate(ctx.accounts.owner.key()),
|
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 perp_market = ctx.accounts.perp_market.load_mut()?;
|
||||||
let bids = ctx.accounts.bids.load_mut()?;
|
let mut book = ctx.accounts.orderbook.load_mut()?;
|
||||||
let asks = ctx.accounts.asks.load_mut()?;
|
|
||||||
let mut book = Book::new(bids, asks);
|
|
||||||
|
|
||||||
let side = account
|
let oo = account
|
||||||
.perp_find_order_side(perp_market.perp_market_index, order_id)
|
.perp_find_order_with_order_id(perp_market.perp_market_index, order_id)
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
error_msg!("could not find perp order with id {order_id} in perp market orderbook")
|
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(
|
book.cancel_order(
|
||||||
&mut account.borrow_mut(),
|
&mut account.borrow_mut(),
|
||||||
order_id,
|
order_id,
|
||||||
side,
|
order_side_and_tree,
|
||||||
Some(ctx.accounts.account.key()),
|
Some(ctx.accounts.account.key()),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use anchor_lang::prelude::*;
|
use anchor_lang::prelude::*;
|
||||||
|
|
||||||
use crate::error::*;
|
use crate::error::*;
|
||||||
use crate::state::{AccountLoaderDynamic, Book, BookSide, Group, MangoAccount, PerpMarket};
|
use crate::state::{AccountLoaderDynamic, Group, MangoAccount, OrderBook, PerpMarket};
|
||||||
|
|
||||||
#[derive(Accounts)]
|
#[derive(Accounts)]
|
||||||
pub struct PerpCancelOrderByClientOrderId<'info> {
|
pub struct PerpCancelOrderByClientOrderId<'info> {
|
||||||
|
@ -14,14 +14,11 @@ pub struct PerpCancelOrderByClientOrderId<'info> {
|
||||||
#[account(
|
#[account(
|
||||||
mut,
|
mut,
|
||||||
has_one = group,
|
has_one = group,
|
||||||
has_one = bids,
|
has_one = orderbook,
|
||||||
has_one = asks
|
|
||||||
)]
|
)]
|
||||||
pub perp_market: AccountLoader<'info, PerpMarket>,
|
pub perp_market: AccountLoader<'info, PerpMarket>,
|
||||||
#[account(mut)]
|
#[account(mut)]
|
||||||
pub asks: AccountLoader<'info, BookSide>,
|
pub orderbook: AccountLoader<'info, OrderBook>,
|
||||||
#[account(mut)]
|
|
||||||
pub bids: AccountLoader<'info, BookSide>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn perp_cancel_order_by_client_order_id(
|
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 perp_market = ctx.accounts.perp_market.load_mut()?;
|
||||||
let bids = ctx.accounts.bids.load_mut()?;
|
let mut book = ctx.accounts.orderbook.load_mut()?;
|
||||||
let asks = ctx.accounts.asks.load_mut()?;
|
|
||||||
let mut book = Book::new(bids, asks);
|
|
||||||
|
|
||||||
let (order_id, side) = account
|
let oo = account
|
||||||
.perp_find_order_with_client_order_id(perp_market.perp_market_index, client_order_id)
|
.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"))?;
|
.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(
|
book.cancel_order(
|
||||||
&mut account.borrow_mut(),
|
&mut account.borrow_mut(),
|
||||||
order_id,
|
order_id,
|
||||||
side,
|
order_side_and_tree,
|
||||||
Some(ctx.accounts.account.key()),
|
Some(ctx.accounts.account.key()),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
|
|
@ -15,8 +15,7 @@ pub struct PerpCloseMarket<'info> {
|
||||||
#[account(
|
#[account(
|
||||||
mut,
|
mut,
|
||||||
has_one = group,
|
has_one = group,
|
||||||
has_one = bids,
|
has_one = orderbook,
|
||||||
has_one = asks,
|
|
||||||
has_one = event_queue,
|
has_one = event_queue,
|
||||||
close = sol_destination
|
close = sol_destination
|
||||||
)]
|
)]
|
||||||
|
@ -26,13 +25,7 @@ pub struct PerpCloseMarket<'info> {
|
||||||
mut,
|
mut,
|
||||||
close = sol_destination
|
close = sol_destination
|
||||||
)]
|
)]
|
||||||
pub bids: AccountLoader<'info, BookSide>,
|
pub orderbook: AccountLoader<'info, OrderBook>,
|
||||||
|
|
||||||
#[account(
|
|
||||||
mut,
|
|
||||||
close = sol_destination
|
|
||||||
)]
|
|
||||||
pub asks: AccountLoader<'info, BookSide>,
|
|
||||||
|
|
||||||
#[account(
|
#[account(
|
||||||
mut,
|
mut,
|
||||||
|
|
|
@ -32,9 +32,7 @@ pub struct PerpCreateMarket<'info> {
|
||||||
/// Accounts are initialised by client,
|
/// Accounts are initialised by client,
|
||||||
/// anchor discriminator is set first when ix exits,
|
/// anchor discriminator is set first when ix exits,
|
||||||
#[account(zero)]
|
#[account(zero)]
|
||||||
pub bids: AccountLoader<'info, BookSide>,
|
pub orderbook: AccountLoader<'info, OrderBook>,
|
||||||
#[account(zero)]
|
|
||||||
pub asks: AccountLoader<'info, BookSide>,
|
|
||||||
#[account(zero)]
|
#[account(zero)]
|
||||||
pub event_queue: AccountLoader<'info, EventQueue>,
|
pub event_queue: AccountLoader<'info, EventQueue>,
|
||||||
|
|
||||||
|
@ -92,8 +90,7 @@ pub fn perp_create_market(
|
||||||
name: fill_from_str(&name)?,
|
name: fill_from_str(&name)?,
|
||||||
oracle: ctx.accounts.oracle.key(),
|
oracle: ctx.accounts.oracle.key(),
|
||||||
oracle_config,
|
oracle_config,
|
||||||
bids: ctx.accounts.bids.key(),
|
orderbook: ctx.accounts.orderbook.key(),
|
||||||
asks: ctx.accounts.asks.key(),
|
|
||||||
event_queue: ctx.accounts.event_queue.key(),
|
event_queue: ctx.accounts.event_queue.key(),
|
||||||
quote_lot_size,
|
quote_lot_size,
|
||||||
base_lot_size,
|
base_lot_size,
|
||||||
|
@ -119,6 +116,7 @@ pub fn perp_create_market(
|
||||||
registration_time: Clock::get()?.unix_timestamp,
|
registration_time: Clock::get()?.unix_timestamp,
|
||||||
padding1: Default::default(),
|
padding1: Default::default(),
|
||||||
padding2: Default::default(),
|
padding2: Default::default(),
|
||||||
|
padding3: Default::default(),
|
||||||
fee_penalty,
|
fee_penalty,
|
||||||
settle_fee_flat,
|
settle_fee_flat,
|
||||||
settle_fee_amount_threshold,
|
settle_fee_amount_threshold,
|
||||||
|
@ -126,11 +124,8 @@ pub fn perp_create_market(
|
||||||
reserved: [0; 92],
|
reserved: [0; 92],
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut bids = ctx.accounts.bids.load_init()?;
|
let mut orderbook = ctx.accounts.orderbook.load_init()?;
|
||||||
bids.book_side_type = BookSideType::Bids;
|
orderbook.init();
|
||||||
|
|
||||||
let mut asks = ctx.accounts.asks.load_init()?;
|
|
||||||
asks.book_side_type = BookSideType::Asks;
|
|
||||||
|
|
||||||
emit!(PerpMarketMetaDataLog {
|
emit!(PerpMarketMetaDataLog {
|
||||||
mango_group: ctx.accounts.group.key(),
|
mango_group: ctx.accounts.group.key(),
|
||||||
|
|
|
@ -14,14 +14,11 @@ pub struct PerpLiqForceCancelOrders<'info> {
|
||||||
#[account(
|
#[account(
|
||||||
mut,
|
mut,
|
||||||
has_one = group,
|
has_one = group,
|
||||||
has_one = bids,
|
has_one = orderbook,
|
||||||
has_one = asks
|
|
||||||
)]
|
)]
|
||||||
pub perp_market: AccountLoader<'info, PerpMarket>,
|
pub perp_market: AccountLoader<'info, PerpMarket>,
|
||||||
#[account(mut)]
|
#[account(mut)]
|
||||||
pub asks: AccountLoader<'info, BookSide>,
|
pub orderbook: AccountLoader<'info, OrderBook>,
|
||||||
#[account(mut)]
|
|
||||||
pub bids: AccountLoader<'info, BookSide>,
|
|
||||||
|
|
||||||
/// CHECK: Oracle can have different account types, constrained by address in perp_market
|
/// CHECK: Oracle can have different account types, constrained by address in perp_market
|
||||||
pub oracle: UncheckedAccount<'info>,
|
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 mut perp_market = ctx.accounts.perp_market.load_mut()?;
|
||||||
let bids = ctx.accounts.bids.load_mut()?;
|
let mut book = ctx.accounts.orderbook.load_mut()?;
|
||||||
let asks = ctx.accounts.asks.load_mut()?;
|
|
||||||
let mut book = Book::new(bids, asks);
|
|
||||||
|
|
||||||
book.cancel_all_orders(&mut account.borrow_mut(), &mut perp_market, limit, None)?;
|
book.cancel_all_orders(&mut account.borrow_mut(), &mut perp_market, limit, None)?;
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,8 @@ use crate::accounts_zerocopy::*;
|
||||||
use crate::error::*;
|
use crate::error::*;
|
||||||
use crate::state::MangoAccount;
|
use crate::state::MangoAccount;
|
||||||
use crate::state::{
|
use crate::state::{
|
||||||
new_fixed_order_account_retriever, new_health_cache, AccountLoaderDynamic, Book, BookSide,
|
new_fixed_order_account_retriever, new_health_cache, AccountLoaderDynamic, EventQueue, Group,
|
||||||
EventQueue, Group, OrderType, PerpMarket, Side,
|
Order, OrderBook, PerpMarket,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Accounts)]
|
#[derive(Accounts)]
|
||||||
|
@ -19,16 +19,13 @@ pub struct PerpPlaceOrder<'info> {
|
||||||
#[account(
|
#[account(
|
||||||
mut,
|
mut,
|
||||||
has_one = group,
|
has_one = group,
|
||||||
has_one = bids,
|
has_one = orderbook,
|
||||||
has_one = asks,
|
|
||||||
has_one = event_queue,
|
has_one = event_queue,
|
||||||
has_one = oracle,
|
has_one = oracle,
|
||||||
)]
|
)]
|
||||||
pub perp_market: AccountLoader<'info, PerpMarket>,
|
pub perp_market: AccountLoader<'info, PerpMarket>,
|
||||||
#[account(mut)]
|
#[account(mut)]
|
||||||
pub asks: AccountLoader<'info, BookSide>,
|
pub orderbook: AccountLoader<'info, OrderBook>,
|
||||||
#[account(mut)]
|
|
||||||
pub bids: AccountLoader<'info, BookSide>,
|
|
||||||
#[account(mut)]
|
#[account(mut)]
|
||||||
pub event_queue: AccountLoader<'info, EventQueue>,
|
pub event_queue: AccountLoader<'info, EventQueue>,
|
||||||
|
|
||||||
|
@ -38,43 +35,10 @@ pub struct PerpPlaceOrder<'info> {
|
||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn perp_place_order(
|
pub fn perp_place_order(ctx: Context<PerpPlaceOrder>, order: Order, limit: u8) -> Result<()> {
|
||||||
ctx: Context<PerpPlaceOrder>,
|
require_gte!(order.max_base_lots, 0);
|
||||||
side: Side,
|
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()?;
|
let mut account = ctx.accounts.account.load_mut()?;
|
||||||
require!(
|
require!(
|
||||||
account.fixed.is_owner_or_delegate(ctx.accounts.owner.key()),
|
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 mut perp_market = ctx.accounts.perp_market.load_mut()?;
|
||||||
let bids = ctx.accounts.bids.load_mut()?;
|
let mut book = ctx.accounts.orderbook.load_mut()?;
|
||||||
let asks = ctx.accounts.asks.load_mut()?;
|
|
||||||
let mut book = Book::new(bids, asks);
|
|
||||||
|
|
||||||
let mut event_queue = ctx.accounts.event_queue.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())?)?;
|
perp_market.oracle_price(&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?)?;
|
||||||
|
|
||||||
let now_ts = Clock::get()?.unix_timestamp as u64;
|
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
|
// TODO reduce_only based on event queue
|
||||||
|
|
||||||
book.new_order(
|
book.new_order(
|
||||||
side,
|
order,
|
||||||
&mut perp_market,
|
&mut perp_market,
|
||||||
&mut event_queue,
|
&mut event_queue,
|
||||||
oracle_price,
|
oracle_price,
|
||||||
&mut account.borrow_mut(),
|
&mut account.borrow_mut(),
|
||||||
&account_pk,
|
&account_pk,
|
||||||
price_lots,
|
|
||||||
max_base_lots,
|
|
||||||
max_quote_lots,
|
|
||||||
order_type,
|
|
||||||
time_in_force,
|
|
||||||
client_order_id,
|
|
||||||
now_ts,
|
now_ts,
|
||||||
limit,
|
limit,
|
||||||
)?;
|
)?;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use anchor_lang::prelude::*;
|
use anchor_lang::prelude::*;
|
||||||
|
|
||||||
use crate::accounts_zerocopy::*;
|
use crate::accounts_zerocopy::*;
|
||||||
use crate::state::{Book, BookSide, Group, PerpMarket};
|
use crate::state::{Group, OrderBook, PerpMarket};
|
||||||
|
|
||||||
use crate::logs::PerpUpdateFundingLog;
|
use crate::logs::PerpUpdateFundingLog;
|
||||||
|
|
||||||
|
@ -11,16 +11,13 @@ pub struct PerpUpdateFunding<'info> {
|
||||||
|
|
||||||
#[account(
|
#[account(
|
||||||
mut,
|
mut,
|
||||||
has_one = bids,
|
has_one = orderbook,
|
||||||
has_one = asks,
|
|
||||||
has_one = oracle,
|
has_one = oracle,
|
||||||
constraint = perp_market.load()?.group.key() == group.key(),
|
constraint = perp_market.load()?.group.key() == group.key(),
|
||||||
)]
|
)]
|
||||||
pub perp_market: AccountLoader<'info, PerpMarket>,
|
pub perp_market: AccountLoader<'info, PerpMarket>,
|
||||||
#[account(mut)]
|
#[account(mut)]
|
||||||
pub asks: AccountLoader<'info, BookSide>,
|
pub orderbook: AccountLoader<'info, OrderBook>,
|
||||||
#[account(mut)]
|
|
||||||
pub bids: AccountLoader<'info, BookSide>,
|
|
||||||
|
|
||||||
/// CHECK: The oracle can be one of several different account types and the pubkey is checked above
|
/// CHECK: The oracle can be one of several different account types and the pubkey is checked above
|
||||||
pub oracle: UncheckedAccount<'info>,
|
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 now_ts = Clock::get()?.unix_timestamp;
|
||||||
|
|
||||||
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
|
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
|
||||||
let bids = ctx.accounts.bids.load_mut()?;
|
let book = ctx.accounts.orderbook.load_mut()?;
|
||||||
let asks = ctx.accounts.asks.load_mut()?;
|
|
||||||
let book = Book::new(bids, asks);
|
|
||||||
|
|
||||||
let oracle_price =
|
let oracle_price =
|
||||||
perp_market.oracle_price(&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?)?;
|
perp_market.oracle_price(&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?)?;
|
||||||
|
|
|
@ -3,6 +3,7 @@ use fixed::types::I80F48;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
pub mod util;
|
pub mod util;
|
||||||
|
|
||||||
|
extern crate core;
|
||||||
extern crate static_assertions;
|
extern crate static_assertions;
|
||||||
|
|
||||||
use anchor_lang::prelude::*;
|
use anchor_lang::prelude::*;
|
||||||
|
@ -19,7 +20,7 @@ pub mod serum3_cpi;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
pub mod types;
|
pub mod types;
|
||||||
|
|
||||||
use state::{OracleConfig, OrderType, PerpMarketIndex, Serum3MarketIndex, Side, TokenIndex};
|
use state::{OracleConfig, PerpMarketIndex, PlaceOrderType, Serum3MarketIndex, Side, TokenIndex};
|
||||||
|
|
||||||
declare_id!("m43thNJ58XCjL798ZSq6JGAG1BnWskhdq5or6kcnfsD");
|
declare_id!("m43thNJ58XCjL798ZSq6JGAG1BnWskhdq5or6kcnfsD");
|
||||||
|
|
||||||
|
@ -485,8 +486,6 @@ pub mod mango_v4 {
|
||||||
instructions::perp_close_market(ctx)
|
instructions::perp_close_market(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO perp_change_perp_market_params
|
|
||||||
|
|
||||||
pub fn perp_deactivate_position(ctx: Context<PerpDeactivatePosition>) -> Result<()> {
|
pub fn perp_deactivate_position(ctx: Context<PerpDeactivatePosition>) -> Result<()> {
|
||||||
instructions::perp_deactivate_position(ctx)
|
instructions::perp_deactivate_position(ctx)
|
||||||
}
|
}
|
||||||
|
@ -495,28 +494,118 @@ pub mod mango_v4 {
|
||||||
pub fn perp_place_order(
|
pub fn perp_place_order(
|
||||||
ctx: Context<PerpPlaceOrder>,
|
ctx: Context<PerpPlaceOrder>,
|
||||||
side: Side,
|
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,
|
price_lots: i64,
|
||||||
|
|
||||||
max_base_lots: i64,
|
max_base_lots: i64,
|
||||||
max_quote_lots: i64,
|
max_quote_lots: i64,
|
||||||
client_order_id: u64,
|
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,
|
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,
|
limit: u8,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
instructions::perp_place_order(
|
require_gte!(price_lots, 0);
|
||||||
ctx,
|
|
||||||
|
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,
|
side,
|
||||||
price_lots,
|
|
||||||
max_base_lots,
|
max_base_lots,
|
||||||
max_quote_lots,
|
max_quote_lots,
|
||||||
client_order_id,
|
client_order_id,
|
||||||
order_type,
|
time_in_force,
|
||||||
expiry_timestamp,
|
params: match order_type {
|
||||||
limit,
|
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)
|
instructions::perp_cancel_order(ctx, order_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -99,7 +99,7 @@ pub struct FillLog {
|
||||||
pub seq_num: u64, // note: usize same as u64
|
pub seq_num: u64, // note: usize same as u64
|
||||||
|
|
||||||
pub maker: Pubkey,
|
pub maker: Pubkey,
|
||||||
pub maker_order_id: i128,
|
pub maker_order_id: u128,
|
||||||
pub maker_client_order_id: u64,
|
pub maker_client_order_id: u64,
|
||||||
pub maker_fee: i128,
|
pub maker_fee: i128,
|
||||||
|
|
||||||
|
@ -107,7 +107,7 @@ pub struct FillLog {
|
||||||
pub maker_timestamp: u64,
|
pub maker_timestamp: u64,
|
||||||
|
|
||||||
pub taker: Pubkey,
|
pub taker: Pubkey,
|
||||||
pub taker_order_id: i128,
|
pub taker_order_id: u128,
|
||||||
pub taker_client_order_id: u64,
|
pub taker_client_order_id: u64,
|
||||||
pub taker_fee: i128,
|
pub taker_fee: i128,
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ use crate::state::{
|
||||||
use crate::util::checked_math as cm;
|
use crate::util::checked_math as cm;
|
||||||
|
|
||||||
#[cfg(feature = "client")]
|
#[cfg(feature = "client")]
|
||||||
use crate::state::orderbook::order_type::Side as PerpOrderSide;
|
use crate::state::orderbook::Side as PerpOrderSide;
|
||||||
|
|
||||||
use super::MangoAccountRef;
|
use super::MangoAccountRef;
|
||||||
|
|
||||||
|
|
|
@ -13,17 +13,18 @@ use crate::error::MangoError;
|
||||||
use crate::error_msg;
|
use crate::error_msg;
|
||||||
|
|
||||||
use super::dynamic_account::*;
|
use super::dynamic_account::*;
|
||||||
|
use super::BookSideOrderTree;
|
||||||
use super::FillEvent;
|
use super::FillEvent;
|
||||||
use super::LeafNode;
|
use super::LeafNode;
|
||||||
use super::PerpMarket;
|
use super::PerpMarket;
|
||||||
use super::PerpMarketIndex;
|
use super::PerpMarketIndex;
|
||||||
use super::PerpOpenOrder;
|
use super::PerpOpenOrder;
|
||||||
use super::Serum3MarketIndex;
|
use super::Serum3MarketIndex;
|
||||||
use super::Side;
|
|
||||||
use super::TokenIndex;
|
use super::TokenIndex;
|
||||||
use super::FREE_ORDER_SLOT;
|
use super::FREE_ORDER_SLOT;
|
||||||
use super::{HealthCache, HealthType};
|
use super::{HealthCache, HealthType};
|
||||||
use super::{PerpPosition, Serum3Orders, TokenPosition};
|
use super::{PerpPosition, Serum3Orders, TokenPosition};
|
||||||
|
use super::{Side, SideAndOrderTree};
|
||||||
use crate::logs::{DeactivatePerpPositionLog, DeactivateTokenPositionLog};
|
use crate::logs::{DeactivatePerpPositionLog, DeactivateTokenPositionLog};
|
||||||
use checked_math as cm;
|
use checked_math as cm;
|
||||||
|
|
||||||
|
@ -500,7 +501,7 @@ impl<
|
||||||
|
|
||||||
pub fn perp_next_order_slot(&self) -> Result<usize> {
|
pub fn perp_next_order_slot(&self) -> Result<usize> {
|
||||||
self.all_perp_orders()
|
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"))
|
.ok_or_else(|| error_msg!("no free perp order index"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -508,23 +509,23 @@ impl<
|
||||||
&self,
|
&self,
|
||||||
market_index: PerpMarketIndex,
|
market_index: PerpMarketIndex,
|
||||||
client_order_id: u64,
|
client_order_id: u64,
|
||||||
) -> Option<(i128, Side)> {
|
) -> Option<&PerpOpenOrder> {
|
||||||
for oo in self.all_perp_orders() {
|
for oo in self.all_perp_orders() {
|
||||||
if oo.order_market == market_index && oo.client_order_id == client_order_id {
|
if oo.market == market_index && oo.client_id == client_order_id {
|
||||||
return Some((oo.order_id, oo.order_side));
|
return Some(&oo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn perp_find_order_side(
|
pub fn perp_find_order_with_order_id(
|
||||||
&self,
|
&self,
|
||||||
market_index: PerpMarketIndex,
|
market_index: PerpMarketIndex,
|
||||||
order_id: i128,
|
order_id: u128,
|
||||||
) -> Option<Side> {
|
) -> Option<&PerpOpenOrder> {
|
||||||
for oo in self.all_perp_orders() {
|
for oo in self.all_perp_orders() {
|
||||||
if oo.order_market == market_index && oo.order_id == order_id {
|
if oo.market == market_index && oo.id == order_id {
|
||||||
return Some(oo.order_side);
|
return Some(&oo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
|
@ -796,6 +797,7 @@ impl<
|
||||||
&mut self,
|
&mut self,
|
||||||
perp_market_index: PerpMarketIndex,
|
perp_market_index: PerpMarketIndex,
|
||||||
side: Side,
|
side: Side,
|
||||||
|
order_tree: BookSideOrderTree,
|
||||||
order: &LeafNode,
|
order: &LeafNode,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let mut perp_account = self.perp_position_mut(perp_market_index)?;
|
let mut perp_account = self.perp_position_mut(perp_market_index)?;
|
||||||
|
@ -810,19 +812,19 @@ impl<
|
||||||
let slot = order.owner_slot as usize;
|
let slot = order.owner_slot as usize;
|
||||||
|
|
||||||
let mut oo = self.perp_order_mut_by_raw_index(slot);
|
let mut oo = self.perp_order_mut_by_raw_index(slot);
|
||||||
oo.order_market = perp_market_index;
|
oo.market = perp_market_index;
|
||||||
oo.order_side = side;
|
oo.side_and_tree = SideAndOrderTree::new(side, order_tree);
|
||||||
oo.order_id = order.key;
|
oo.id = order.key;
|
||||||
oo.client_order_id = order.client_order_id;
|
oo.client_id = order.client_order_id;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_perp_order(&mut self, slot: usize, quantity: i64) -> Result<()> {
|
pub fn remove_perp_order(&mut self, slot: usize, quantity: i64) -> Result<()> {
|
||||||
{
|
{
|
||||||
let oo = self.perp_order_mut_by_raw_index(slot);
|
let oo = self.perp_order_mut_by_raw_index(slot);
|
||||||
require_neq!(oo.order_market, FREE_ORDER_SLOT);
|
require_neq!(oo.market, FREE_ORDER_SLOT);
|
||||||
let order_side = oo.order_side;
|
let order_side = oo.side_and_tree.side();
|
||||||
let perp_market_index = oo.order_market;
|
let perp_market_index = oo.market;
|
||||||
let perp_account = self.perp_position_mut(perp_market_index)?;
|
let perp_account = self.perp_position_mut(perp_market_index)?;
|
||||||
|
|
||||||
// accounting
|
// accounting
|
||||||
|
@ -838,10 +840,10 @@ impl<
|
||||||
|
|
||||||
// release space
|
// release space
|
||||||
let oo = self.perp_order_mut_by_raw_index(slot);
|
let oo = self.perp_order_mut_by_raw_index(slot);
|
||||||
oo.order_market = FREE_ORDER_SLOT;
|
oo.market = FREE_ORDER_SLOT;
|
||||||
oo.order_side = Side::Bid;
|
oo.side_and_tree = SideAndOrderTree::BidFixed;
|
||||||
oo.order_id = 0i128;
|
oo.id = 0;
|
||||||
oo.client_order_id = 0u64;
|
oo.client_id = 0;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -395,24 +395,24 @@ impl PerpPosition {
|
||||||
#[zero_copy]
|
#[zero_copy]
|
||||||
#[derive(AnchorSerialize, AnchorDeserialize, Debug)]
|
#[derive(AnchorSerialize, AnchorDeserialize, Debug)]
|
||||||
pub struct PerpOpenOrder {
|
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 padding1: [u8; 1],
|
||||||
pub order_market: PerpMarketIndex,
|
pub market: PerpMarketIndex,
|
||||||
pub padding2: [u8; 4],
|
pub padding2: [u8; 4],
|
||||||
pub client_order_id: u64,
|
pub client_id: u64,
|
||||||
pub order_id: i128,
|
pub id: u128,
|
||||||
pub reserved: [u8; 64],
|
pub reserved: [u8; 64],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for PerpOpenOrder {
|
impl Default for PerpOpenOrder {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
order_side: Side::Bid,
|
side_and_tree: SideAndOrderTree::BidFixed,
|
||||||
padding1: Default::default(),
|
padding1: Default::default(),
|
||||||
order_market: FREE_ORDER_SLOT,
|
market: FREE_ORDER_SLOT,
|
||||||
padding2: Default::default(),
|
padding2: Default::default(),
|
||||||
client_order_id: 0,
|
client_id: 0,
|
||||||
order_id: 0,
|
id: 0,
|
||||||
reserved: [0; 64],
|
reserved: [0; 64],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,250 +1,163 @@
|
||||||
use std::cell::RefMut;
|
|
||||||
|
|
||||||
use crate::accounts_zerocopy::*;
|
|
||||||
use crate::state::MangoAccountRefMut;
|
use crate::state::MangoAccountRefMut;
|
||||||
use crate::{
|
use crate::{
|
||||||
error::*,
|
error::*,
|
||||||
state::{
|
state::{orderbook::bookside::*, EventQueue, PerpMarket, FREE_ORDER_SLOT},
|
||||||
orderbook::{bookside::BookSide, nodes::LeafNode},
|
|
||||||
EventQueue, PerpMarket, FREE_ORDER_SLOT,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
use anchor_lang::prelude::*;
|
use anchor_lang::prelude::*;
|
||||||
use bytemuck::cast;
|
use bytemuck::cast;
|
||||||
use fixed::types::I80F48;
|
use fixed::types::I80F48;
|
||||||
|
use static_assertions::const_assert_eq;
|
||||||
|
|
||||||
use super::{
|
use super::*;
|
||||||
nodes::NodeHandle,
|
|
||||||
order_type::{OrderType, Side},
|
|
||||||
FillEvent, OutEvent,
|
|
||||||
};
|
|
||||||
use crate::util::checked_math as cm;
|
use crate::util::checked_math as cm;
|
||||||
|
|
||||||
/// Drop at most this many expired orders from a BookSide when trying to match orders.
|
/// Drop at most this many expired orders from a BookSide when trying to match orders.
|
||||||
/// This exists as a guard against excessive compute use.
|
/// This exists as a guard against excessive compute use.
|
||||||
const DROP_EXPIRED_ORDER_LIMIT: usize = 5;
|
const DROP_EXPIRED_ORDER_LIMIT: usize = 5;
|
||||||
|
|
||||||
/// The implicit limit price to use for market orders
|
#[account(zero_copy)]
|
||||||
fn market_order_limit_for_side(side: Side) -> i64 {
|
pub struct OrderBook {
|
||||||
match side {
|
pub bids: BookSide,
|
||||||
Side::Bid => i64::MAX,
|
pub asks: BookSide,
|
||||||
Side::Ask => 1,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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
|
impl OrderBook {
|
||||||
/// the best opposing order
|
pub fn init(&mut self) {
|
||||||
fn post_only_slide_limit(side: Side, best_other_side: i64, limit: i64) -> i64 {
|
self.bids.fixed.order_tree_type = OrderTreeType::Bids;
|
||||||
match side {
|
self.bids.oracle_pegged.order_tree_type = OrderTreeType::Bids;
|
||||||
Side::Bid => limit.min(cm!(best_other_side - 1)),
|
self.asks.fixed.order_tree_type = OrderTreeType::Asks;
|
||||||
Side::Ask => limit.max(cm!(best_other_side + 1)),
|
self.asks.oracle_pegged.order_tree_type = OrderTreeType::Asks;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_mut(
|
pub fn bookside_mut(&mut self, side: Side) -> &mut BookSide {
|
||||||
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 {
|
|
||||||
match side {
|
match side {
|
||||||
Side::Bid => &mut self.bids,
|
Side::Bid => &mut self.bids,
|
||||||
Side::Ask => &mut self.asks,
|
Side::Ask => &mut self.asks,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns best valid bid
|
pub fn bookside(&self, side: Side) -> &BookSide {
|
||||||
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> {
|
|
||||||
match side {
|
match side {
|
||||||
Side::Bid => self.best_bid_price(now_ts),
|
Side::Bid => &self.bids,
|
||||||
Side::Ask => self.best_ask_price(now_ts),
|
Side::Ask => &self.asks,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the quantity of valid bids above and including the price
|
pub fn best_price(&self, now_ts: u64, oracle_price_lots: i64, side: Side) -> Option<i64> {
|
||||||
pub fn bids_size_above(&self, price: i64, max_depth: i64, now_ts: u64) -> i64 {
|
Some(
|
||||||
let mut sum: i64 = 0;
|
self.bookside(side)
|
||||||
for (_, bid) in self.bids.iter_valid(now_ts) {
|
.iter_valid(now_ts, oracle_price_lots)
|
||||||
if price > bid.price() || sum >= max_depth {
|
.next()?
|
||||||
break;
|
.price_lots,
|
||||||
}
|
)
|
||||||
sum = sum.checked_add(bid.quantity).unwrap();
|
|
||||||
}
|
|
||||||
sum.min(max_depth)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Walk up the book `quantity` units and return the price at that level. If `quantity` units
|
/// Walk up the book `quantity` units and return the price at that level. If `quantity` units
|
||||||
/// not on book, return None
|
/// 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 mut sum: i64 = 0;
|
||||||
let book_side = match side {
|
let bookside = self.bookside(side);
|
||||||
Side::Bid => self.bids.iter_valid(now_ts),
|
let iter = bookside.iter_valid(now_ts, oracle_price_lots);
|
||||||
Side::Ask => self.asks.iter_valid(now_ts),
|
for order in iter {
|
||||||
};
|
cm!(sum += order.node.quantity);
|
||||||
for (_, order) in book_side {
|
|
||||||
sum = sum.checked_add(order.quantity).unwrap();
|
|
||||||
if sum >= quantity {
|
if sum >= quantity {
|
||||||
return Some(order.price());
|
return Some(order.price_lots);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
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)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn new_order(
|
pub fn new_order(
|
||||||
&mut self,
|
&mut self,
|
||||||
side: Side,
|
order: Order,
|
||||||
perp_market: &mut PerpMarket,
|
perp_market: &mut PerpMarket,
|
||||||
event_queue: &mut EventQueue,
|
event_queue: &mut EventQueue,
|
||||||
oracle_price: I80F48,
|
oracle_price: I80F48,
|
||||||
mango_account: &mut MangoAccountRefMut,
|
mango_account: &mut MangoAccountRefMut,
|
||||||
mango_account_pk: &Pubkey,
|
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,
|
now_ts: u64,
|
||||||
mut limit: u8,
|
mut limit: u8,
|
||||||
) -> std::result::Result<(), Error> {
|
) -> std::result::Result<(), Error> {
|
||||||
|
let side = order.side;
|
||||||
let other_side = side.invert_side();
|
let other_side = side.invert_side();
|
||||||
let market = perp_market;
|
let market = perp_market;
|
||||||
let (post_only, mut post_allowed, price_lots) = match order_type {
|
let oracle_price_lots = market.native_price_to_lot(oracle_price);
|
||||||
OrderType::Limit => (false, true, price_lots),
|
let post_only = order.is_post_only();
|
||||||
OrderType::ImmediateOrCancel => (false, false, price_lots),
|
let mut post_target = order.post_target();
|
||||||
OrderType::PostOnly => (true, true, price_lots),
|
let (price_lots, price_data) = order.price(now_ts, oracle_price_lots, self)?;
|
||||||
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)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if post_allowed {
|
if post_target.is_some() {
|
||||||
// price limit check computed lazily to save CU on average
|
// price limit check computed lazily to save CU on average
|
||||||
let native_price = market.lot_to_native_price(price_lots);
|
let native_price = market.lot_to_native_price(price_lots);
|
||||||
if !market.inside_price_limit(side, native_price, oracle_price) {
|
if !market.inside_price_limit(side, native_price, oracle_price) {
|
||||||
msg!("Posting on book disallowed due to price limits");
|
msg!("Posting on book disallowed due to price limits");
|
||||||
post_allowed = false;
|
post_target = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// generate new order id
|
// 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.
|
// Iterate through book and match against this new order.
|
||||||
//
|
//
|
||||||
// Any changes to matching orders on the other side of the book are collected in
|
// 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.
|
// matched_changes/matched_deletes and then applied after this loop.
|
||||||
let mut remaining_base_lots = max_base_lots;
|
let mut remaining_base_lots = order.max_base_lots;
|
||||||
let mut remaining_quote_lots = max_quote_lots;
|
let mut remaining_quote_lots = order.max_quote_lots;
|
||||||
let mut matched_order_changes: Vec<(NodeHandle, i64)> = vec![];
|
let mut matched_order_changes: Vec<(BookSideOrderHandle, i64)> = vec![];
|
||||||
let mut matched_order_deletes: Vec<i128> = vec![];
|
let mut matched_order_deletes: Vec<(BookSideOrderTree, u128)> = vec![];
|
||||||
let mut number_of_dropped_expired_orders = 0;
|
let mut number_of_dropped_expired_orders = 0;
|
||||||
let opposing_bookside = self.bookside(other_side);
|
let opposing_bookside = self.bookside_mut(other_side);
|
||||||
for (best_opposing_h, best_opposing) in opposing_bookside.iter_all_including_invalid() {
|
for best_opposing in opposing_bookside.iter_all_including_invalid(now_ts, oracle_price_lots)
|
||||||
if !best_opposing.is_valid(now_ts) {
|
{
|
||||||
|
if !best_opposing.is_valid {
|
||||||
// Remove the order from the book unless we've done that enough
|
// Remove the order from the book unless we've done that enough
|
||||||
if number_of_dropped_expired_orders < DROP_EXPIRED_ORDER_LIMIT {
|
if number_of_dropped_expired_orders < DROP_EXPIRED_ORDER_LIMIT {
|
||||||
number_of_dropped_expired_orders += 1;
|
number_of_dropped_expired_orders += 1;
|
||||||
let event = OutEvent::new(
|
let event = OutEvent::new(
|
||||||
other_side,
|
other_side,
|
||||||
best_opposing.owner_slot,
|
best_opposing.node.owner_slot,
|
||||||
now_ts,
|
now_ts,
|
||||||
event_queue.header.seq_num,
|
event_queue.header.seq_num,
|
||||||
best_opposing.owner,
|
best_opposing.node.owner,
|
||||||
best_opposing.quantity,
|
best_opposing.node.quantity,
|
||||||
);
|
);
|
||||||
event_queue.push_back(cast(event)).unwrap();
|
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;
|
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) {
|
if !side.is_price_within_limit(best_opposing_price, price_lots) {
|
||||||
break;
|
break;
|
||||||
} else if post_only {
|
} else if post_only {
|
||||||
msg!("Order could not be placed due to PostOnly");
|
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
|
break; // return silently to not fail other instructions in tx
|
||||||
} else if limit == 0 {
|
} else if limit == 0 {
|
||||||
msg!("Order matching limit reached");
|
msg!("Order matching limit reached");
|
||||||
post_allowed = false;
|
post_target = None;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
let max_match_by_quote = remaining_quote_lots / best_opposing_price;
|
let max_match_by_quote = remaining_quote_lots / best_opposing_price;
|
||||||
let match_base_lots = remaining_base_lots
|
let match_base_lots = remaining_base_lots
|
||||||
.min(best_opposing.quantity)
|
.min(best_opposing.node.quantity)
|
||||||
.min(max_match_by_quote);
|
.min(max_match_by_quote);
|
||||||
let done =
|
let done =
|
||||||
match_base_lots == max_match_by_quote || match_base_lots == remaining_base_lots;
|
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_base_lots -= match_base_lots);
|
||||||
cm!(remaining_quote_lots -= match_quote_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;
|
let maker_out = new_best_opposing_quantity == 0;
|
||||||
if maker_out {
|
if maker_out {
|
||||||
matched_order_deletes.push(best_opposing.key);
|
matched_order_deletes
|
||||||
|
.push((best_opposing.handle.order_tree, best_opposing.node.key));
|
||||||
} else {
|
} 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
|
// 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(
|
let fill = FillEvent::new(
|
||||||
side,
|
side,
|
||||||
maker_out,
|
maker_out,
|
||||||
best_opposing.owner_slot,
|
best_opposing.node.owner_slot,
|
||||||
now_ts,
|
now_ts,
|
||||||
event_queue.header.seq_num,
|
event_queue.header.seq_num,
|
||||||
best_opposing.owner,
|
best_opposing.node.owner,
|
||||||
best_opposing.key,
|
best_opposing.node.key,
|
||||||
best_opposing.client_order_id,
|
best_opposing.node.client_order_id,
|
||||||
market.maker_fee,
|
market.maker_fee,
|
||||||
best_opposing.timestamp,
|
best_opposing.node.timestamp,
|
||||||
*mango_account_pk,
|
*mango_account_pk,
|
||||||
order_id,
|
order_id,
|
||||||
client_order_id,
|
order.client_order_id,
|
||||||
market.taker_fee,
|
market.taker_fee,
|
||||||
best_opposing_price,
|
best_opposing_price,
|
||||||
match_base_lots,
|
match_base_lots,
|
||||||
|
@ -291,7 +205,7 @@ impl<'a> Book<'a> {
|
||||||
break;
|
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!)
|
// Apply changes to matched asks (handles invalidate on delete!)
|
||||||
for (handle, new_quantity) in matched_order_changes {
|
for (handle, new_quantity) in matched_order_changes {
|
||||||
|
@ -302,17 +216,21 @@ impl<'a> Book<'a> {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.quantity = new_quantity;
|
.quantity = new_quantity;
|
||||||
}
|
}
|
||||||
for key in matched_order_deletes {
|
for (component, key) in matched_order_deletes {
|
||||||
let _removed_leaf = opposing_bookside.remove_by_key(key).unwrap();
|
let _removed_leaf = opposing_bookside.remove_by_key(component, key).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there are still quantity unmatched, place on the book
|
// If there are still quantity unmatched, place on the book
|
||||||
let book_base_quantity = remaining_base_lots.min(remaining_quote_lots / price_lots);
|
let book_base_quantity = remaining_base_lots.min(remaining_quote_lots / price_lots);
|
||||||
msg!("{:?}", post_allowed);
|
if book_base_quantity <= 0 {
|
||||||
if post_allowed && 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
|
// Drop an expired order if possible
|
||||||
let bookside = self.bookside(side);
|
if let Some(expired_order) = order_tree.remove_one_expired(now_ts) {
|
||||||
if let Some(expired_order) = bookside.remove_one_expired(now_ts) {
|
|
||||||
let event = OutEvent::new(
|
let event = OutEvent::new(
|
||||||
side,
|
side,
|
||||||
expired_order.owner_slot,
|
expired_order.owner_slot,
|
||||||
|
@ -324,12 +242,12 @@ impl<'a> Book<'a> {
|
||||||
event_queue.push_back(cast(event)).unwrap();
|
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
|
// 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
|
// MangoErrorCode::OutOfSpace
|
||||||
require!(
|
require!(
|
||||||
side.is_price_better(price_lots, worst_order.price()),
|
side.is_price_data_better(price_data, worst_order.price_data()),
|
||||||
MangoError::SomeError
|
MangoError::SomeError
|
||||||
);
|
);
|
||||||
let event = OutEvent::new(
|
let event = OutEvent::new(
|
||||||
|
@ -349,12 +267,13 @@ impl<'a> Book<'a> {
|
||||||
order_id,
|
order_id,
|
||||||
*mango_account_pk,
|
*mango_account_pk,
|
||||||
book_base_quantity,
|
book_base_quantity,
|
||||||
client_order_id,
|
order.client_order_id,
|
||||||
now_ts,
|
now_ts,
|
||||||
order_type,
|
PostOrderType::Limit, // TODO: Support order types? needed?
|
||||||
time_in_force,
|
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
|
// TODO OPT remove if PlacePerpOrder needs more compute
|
||||||
msg!(
|
msg!(
|
||||||
|
@ -368,7 +287,12 @@ impl<'a> Book<'a> {
|
||||||
price_lots
|
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
|
// 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
|
// 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)?;
|
apply_penalty(market, mango_account)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -397,21 +321,20 @@ impl<'a> Book<'a> {
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
for i in 0..mango_account.header.perp_oo_count() {
|
for i in 0..mango_account.header.perp_oo_count() {
|
||||||
let oo = mango_account.perp_order_by_raw_index(i);
|
let oo = mango_account.perp_order_by_raw_index(i);
|
||||||
if oo.order_market == FREE_ORDER_SLOT
|
if oo.market == FREE_ORDER_SLOT || oo.market != perp_market.perp_market_index {
|
||||||
|| oo.order_market != perp_market.perp_market_index
|
|
||||||
{
|
|
||||||
continue;
|
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 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;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let order_id = oo.order_id;
|
let order_id = oo.id;
|
||||||
self.cancel_order(mango_account, order_id, order_side, None)?;
|
|
||||||
|
self.cancel_order(mango_account, order_id, order_side_and_tree, None)?;
|
||||||
|
|
||||||
limit -= 1;
|
limit -= 1;
|
||||||
if limit == 0 {
|
if limit == 0 {
|
||||||
|
@ -426,19 +349,16 @@ impl<'a> Book<'a> {
|
||||||
pub fn cancel_order(
|
pub fn cancel_order(
|
||||||
&mut self,
|
&mut self,
|
||||||
mango_account: &mut MangoAccountRefMut,
|
mango_account: &mut MangoAccountRefMut,
|
||||||
order_id: i128,
|
order_id: u128,
|
||||||
side: Side,
|
side_and_tree: SideAndOrderTree,
|
||||||
expected_owner: Option<Pubkey>,
|
expected_owner: Option<Pubkey>,
|
||||||
) -> Result<LeafNode> {
|
) -> Result<LeafNode> {
|
||||||
let leaf_node =
|
let side = side_and_tree.side();
|
||||||
match side {
|
let book_component = side_and_tree.order_tree();
|
||||||
Side::Bid => self.bids.remove_by_key(order_id).ok_or_else(|| {
|
let leaf_node = self.bookside_mut(side).orders_mut(book_component).
|
||||||
error_msg!("invalid perp order id {order_id} for side {side:?}")
|
remove_by_key(order_id).ok_or_else(|| {
|
||||||
}),
|
error_msg!("invalid perp order id {order_id} for side {side:?} and component {book_component:?}")
|
||||||
Side::Ask => self.asks.remove_by_key(order_id).ok_or_else(|| {
|
})?;
|
||||||
error_msg!("invalid perp order id {order_id} for side {side:?}")
|
|
||||||
}),
|
|
||||||
}?;
|
|
||||||
if let Some(owner) = expected_owner {
|
if let Some(owner) = expected_owner {
|
||||||
require_keys_eq!(leaf_node.owner, owner);
|
require_keys_eq!(leaf_node.owner, owner);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,7 @@
|
||||||
use anchor_lang::prelude::*;
|
use anchor_lang::prelude::*;
|
||||||
use bytemuck::{cast, cast_mut, cast_ref};
|
|
||||||
|
|
||||||
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
||||||
use static_assertions::const_assert_eq;
|
|
||||||
|
|
||||||
use crate::state::orderbook::bookside_iterator::BookSideIter;
|
use super::*;
|
||||||
|
|
||||||
use crate::error::MangoError;
|
|
||||||
use crate::state::orderbook::nodes::{
|
|
||||||
AnyNode, FreeNode, InnerNode, LeafNode, NodeHandle, NodeRef, NodeTag,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const MAX_BOOK_NODES: usize = 1024;
|
|
||||||
|
|
||||||
#[derive(
|
#[derive(
|
||||||
Eq,
|
Eq,
|
||||||
|
@ -25,619 +15,261 @@ pub const MAX_BOOK_NODES: usize = 1024;
|
||||||
AnchorDeserialize,
|
AnchorDeserialize,
|
||||||
)]
|
)]
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
pub enum BookSideType {
|
pub enum BookSideOrderTree {
|
||||||
Bids,
|
Fixed,
|
||||||
Asks,
|
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)]
|
#[account(zero_copy)]
|
||||||
pub struct BookSide {
|
pub struct BookSide {
|
||||||
// pub meta_data: MetaData,
|
pub fixed: OrderTree,
|
||||||
// todo: do we want this type at this level?
|
pub oracle_pegged: OrderTree,
|
||||||
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],
|
|
||||||
}
|
}
|
||||||
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 {
|
impl BookSide {
|
||||||
/// Iterate over all entries in the book filtering out invalid orders
|
/// Iterate over all entries in the book filtering out invalid orders
|
||||||
///
|
///
|
||||||
/// smallest to highest for asks
|
/// smallest to highest for asks
|
||||||
/// highest to smallest for bids
|
/// highest to smallest for bids
|
||||||
pub fn iter_valid(&self, now_ts: u64) -> BookSideIter {
|
pub fn iter_valid(
|
||||||
BookSideIter::new(self, now_ts)
|
&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
|
/// Iterate over all entries, including invalid orders
|
||||||
pub fn iter_all_including_invalid(&self) -> BookSideIter {
|
pub fn iter_all_including_invalid(&self, now_ts: u64, oracle_price_lots: i64) -> BookSideIter {
|
||||||
BookSideIter::new(self, 0)
|
BookSideIter::new(self, now_ts, oracle_price_lots)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn node_mut(&mut self, key: NodeHandle) -> Option<&mut AnyNode> {
|
pub fn orders(&self, component: BookSideOrderTree) -> &OrderTree {
|
||||||
let node = &mut self.nodes[key as usize];
|
match component {
|
||||||
let tag = NodeTag::try_from(node.tag);
|
BookSideOrderTree::Fixed => &self.fixed,
|
||||||
match tag {
|
BookSideOrderTree::OraclePegged => &self.oracle_pegged,
|
||||||
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> {
|
pub fn orders_mut(&mut self, component: BookSideOrderTree) -> &mut OrderTree {
|
||||||
self.remove_by_key(self.min_leaf()?.key)
|
match component {
|
||||||
}
|
BookSideOrderTree::Fixed => &mut self.fixed,
|
||||||
|
BookSideOrderTree::OraclePegged => &mut self.oracle_pegged,
|
||||||
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 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.
|
/// 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> {
|
pub fn remove_one_expired(
|
||||||
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(
|
|
||||||
&mut self,
|
&mut self,
|
||||||
stack: &[(NodeHandle, bool)],
|
component: BookSideOrderTree,
|
||||||
mut outdated_expiry: u64,
|
now_ts: u64,
|
||||||
mut new_expiry: u64,
|
) -> Option<LeafNode> {
|
||||||
) {
|
self.orders_mut(component).remove_one_expired(now_ts)
|
||||||
// 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 remove_by_key(
|
||||||
pub fn find_earliest_expiry(&self) -> Option<(NodeHandle, u64)> {
|
&mut self,
|
||||||
let mut current: NodeHandle = match self.root() {
|
component: BookSideOrderTree,
|
||||||
Some(h) => h,
|
search_key: u128,
|
||||||
None => return None,
|
) -> Option<LeafNode> {
|
||||||
};
|
self.orders_mut(component).remove_by_key(search_key)
|
||||||
|
}
|
||||||
|
|
||||||
loop {
|
pub fn remove(&mut self, key: BookSideOrderHandle) -> Option<AnyNode> {
|
||||||
let contents = *self.node(current).unwrap();
|
self.orders_mut(key.order_tree).remove(key.node)
|
||||||
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::super::order_type::OrderType;
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use bytemuck::Zeroable;
|
use bytemuck::Zeroable;
|
||||||
|
|
||||||
fn new_bookside(book_side_type: BookSideType) -> BookSide {
|
fn new_order_tree(order_tree_type: OrderTreeType) -> OrderTree {
|
||||||
BookSide {
|
OrderTree {
|
||||||
book_side_type,
|
order_tree_type,
|
||||||
padding: [0u8; 3],
|
padding: [0u8; 3],
|
||||||
bump_index: 0,
|
bump_index: 0,
|
||||||
free_list_len: 0,
|
free_list_len: 0,
|
||||||
free_list_head: 0,
|
free_list_head: 0,
|
||||||
root_node: 0,
|
root_node: 0,
|
||||||
leaf_count: 0,
|
leaf_count: 0,
|
||||||
nodes: [AnyNode::zeroed(); MAX_BOOK_NODES],
|
nodes: [AnyNode::zeroed(); MAX_ORDERTREE_NODES],
|
||||||
reserved: [0; 256],
|
reserved: [0; 256],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn verify_bookside(bookside: &BookSide) {
|
fn bookside_iteration_random_helper(side: Side) {
|
||||||
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() {
|
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
let mut rng = rand::thread_rng();
|
let mut rng = rand::thread_rng();
|
||||||
|
|
||||||
let mut bids = new_bookside(BookSideType::Bids);
|
let order_tree_type = match side {
|
||||||
let new_expiring_leaf = |key: i128, expiry: u64| {
|
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(
|
LeafNode::new(
|
||||||
0,
|
0,
|
||||||
key,
|
key,
|
||||||
Pubkey::default(),
|
Pubkey::default(),
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
expiry - 1,
|
|
||||||
OrderType::Limit,
|
|
||||||
1,
|
1,
|
||||||
|
PostOrderType::Limit,
|
||||||
|
0,
|
||||||
|
-1,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
// add 200 random leaves
|
// add 100 leaves to each BookSide, mostly random
|
||||||
let mut keys = vec![];
|
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) {
|
if keys.contains(&key) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let expiry = rng.gen_range(1..200); // give good chance of duplicate expiry times
|
|
||||||
keys.push(key);
|
keys.push(key);
|
||||||
bids.insert_leaf(&new_expiring_leaf(key, expiry)).unwrap();
|
oracle_pegged.insert_leaf(&new_leaf(key)).unwrap();
|
||||||
verify_bookside(&bids);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove 50 at random
|
while fixed.leaf_count < 100 {
|
||||||
for _ in 0..50 {
|
let price_data: u64 = rng.gen_range(1..50);
|
||||||
if keys.len() == 0 {
|
let seq_num: u64 = rng.gen_range(0..1000);
|
||||||
break;
|
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]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,92 +1,153 @@
|
||||||
use crate::state::orderbook::bookside::{BookSide, BookSideType};
|
use super::*;
|
||||||
use crate::state::orderbook::nodes::{InnerNode, LeafNode, NodeHandle, NodeRef};
|
|
||||||
|
|
||||||
/// 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> {
|
pub struct BookSideIter<'a> {
|
||||||
book_side: &'a BookSide,
|
fixed_iter: OrderTreeIter<'a>,
|
||||||
/// InnerNodes where the right side still needs to be iterated on
|
oracle_pegged_iter: OrderTreeIter<'a>,
|
||||||
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,
|
|
||||||
|
|
||||||
now_ts: u64,
|
now_ts: u64,
|
||||||
|
oracle_price_lots: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> BookSideIter<'a> {
|
impl<'a> BookSideIter<'a> {
|
||||||
pub fn new(book_side: &'a BookSide, now_ts: u64) -> Self {
|
pub fn new(book_side: &'a BookSide, now_ts: u64, oracle_price_lots: i64) -> Self {
|
||||||
let (left, right) = if book_side.book_side_type == BookSideType::Bids {
|
Self {
|
||||||
(1, 0)
|
fixed_iter: book_side.fixed.iter(),
|
||||||
} else {
|
oracle_pegged_iter: book_side.oracle_pegged.iter(),
|
||||||
(0, 1)
|
|
||||||
};
|
|
||||||
let stack = vec![];
|
|
||||||
|
|
||||||
let mut iter = Self {
|
|
||||||
book_side,
|
|
||||||
stack,
|
|
||||||
next_leaf: None,
|
|
||||||
left,
|
|
||||||
right,
|
|
||||||
now_ts,
|
now_ts,
|
||||||
};
|
oracle_price_lots,
|
||||||
if book_side.leaf_count != 0 {
|
|
||||||
iter.next_leaf = iter.find_leftmost_valid_leaf(book_side.root_node);
|
|
||||||
}
|
}
|
||||||
iter
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn find_leftmost_valid_leaf(
|
#[derive(Clone, Copy, PartialEq)]
|
||||||
&mut self,
|
enum OrderState {
|
||||||
start: NodeHandle,
|
Valid,
|
||||||
) -> Option<(NodeHandle, &'a LeafNode)> {
|
Invalid,
|
||||||
let mut current = start;
|
Skipped,
|
||||||
loop {
|
}
|
||||||
match self.book_side.node(current).unwrap().case().unwrap() {
|
|
||||||
NodeRef::Inner(inner) => {
|
fn oracle_pegged_price(
|
||||||
self.stack.push(inner);
|
oracle_price_lots: i64,
|
||||||
current = inner.children[self.left];
|
node: &LeafNode,
|
||||||
}
|
side: Side,
|
||||||
NodeRef::Leaf(leaf) => {
|
) -> (OrderState, Option<i64>) {
|
||||||
if leaf.is_valid(self.now_ts) {
|
let price_data = node.price_data();
|
||||||
return Some((current, leaf));
|
let price_offset = oracle_pegged_price_offset(price_data);
|
||||||
} else {
|
if let Some(price) = oracle_price_lots.checked_add(price_offset) {
|
||||||
match self.stack.pop() {
|
if price >= 1 {
|
||||||
None => {
|
if node.peg_limit != -1 && side.is_price_better(price, node.peg_limit) {
|
||||||
return None;
|
return (OrderState::Invalid, Some(price));
|
||||||
}
|
} else {
|
||||||
Some(inner) => {
|
return (OrderState::Valid, Some(price));
|
||||||
current = inner.children[self.right];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
(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> {
|
impl<'a> Iterator for BookSideIter<'a> {
|
||||||
type Item = (NodeHandle, &'a LeafNode);
|
type Item = BookSideIterItem<'a>;
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
// if next leaf is None just return it
|
let side = self.fixed_iter.side();
|
||||||
self.next_leaf?;
|
|
||||||
|
|
||||||
// start popping from stack and get the other child
|
// Skip all the oracle pegged orders that aren't representable with the current oracle
|
||||||
let current_leaf = self.next_leaf;
|
// price. Example: iterating asks, but the best ask is at offset -100 with the oracle at 50.
|
||||||
self.next_leaf = match self.stack.pop() {
|
// We need to skip asks until we find the first that has a price >= 1.
|
||||||
None => None,
|
let mut o_peek = self.oracle_pegged_iter.peek();
|
||||||
Some(inner) => {
|
while let Some((_, o_node)) = o_peek {
|
||||||
let start = inner.children[self.right];
|
if oracle_pegged_price(self.oracle_price_lots, o_node, side).0 != OrderState::Skipped {
|
||||||
// go down the left branch as much as possible until reaching a valid leaf
|
break;
|
||||||
self.find_leftmost_valid_leaf(start)
|
|
||||||
}
|
}
|
||||||
};
|
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,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,15 +2,21 @@ pub use book::*;
|
||||||
pub use bookside::*;
|
pub use bookside::*;
|
||||||
pub use bookside_iterator::*;
|
pub use bookside_iterator::*;
|
||||||
pub use nodes::*;
|
pub use nodes::*;
|
||||||
|
pub use order::*;
|
||||||
pub use order_type::*;
|
pub use order_type::*;
|
||||||
|
pub use ordertree::*;
|
||||||
|
pub use ordertree_iterator::*;
|
||||||
pub use queue::*;
|
pub use queue::*;
|
||||||
|
|
||||||
pub mod book;
|
mod book;
|
||||||
pub mod bookside;
|
mod bookside;
|
||||||
pub mod bookside_iterator;
|
mod bookside_iterator;
|
||||||
pub mod nodes;
|
mod nodes;
|
||||||
pub mod order_type;
|
mod order;
|
||||||
pub mod queue;
|
mod order_type;
|
||||||
|
mod ordertree;
|
||||||
|
mod ordertree_iterator;
|
||||||
|
mod queue;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
@ -20,24 +26,9 @@ mod tests {
|
||||||
use bytemuck::Zeroable;
|
use bytemuck::Zeroable;
|
||||||
use fixed::types::I80F48;
|
use fixed::types::I80F48;
|
||||||
use solana_program::pubkey::Pubkey;
|
use solana_program::pubkey::Pubkey;
|
||||||
use std::cell::RefCell;
|
|
||||||
|
|
||||||
fn new_bookside(book_side_type: BookSideType) -> BookSide {
|
fn order_tree_leaf_by_key(order_tree: &OrderTree, key: u128) -> Option<&LeafNode> {
|
||||||
BookSide {
|
for (_, leaf) in order_tree.iter() {
|
||||||
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() {
|
|
||||||
if leaf.key == key {
|
if leaf.key == key {
|
||||||
return Some(leaf);
|
return Some(leaf);
|
||||||
}
|
}
|
||||||
|
@ -45,8 +36,8 @@ mod tests {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bookside_contains_key(bookside: &BookSide, key: i128) -> bool {
|
fn order_tree_contains_key(order_tree: &OrderTree, key: u128) -> bool {
|
||||||
for (_, leaf) in bookside.iter_all_including_invalid() {
|
for (_, leaf) in order_tree.iter() {
|
||||||
if leaf.key == key {
|
if leaf.key == key {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -54,26 +45,18 @@ mod tests {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bookside_contains_price(bookside: &BookSide, price: i64) -> bool {
|
fn order_tree_contains_price(order_tree: &OrderTree, price_data: u64) -> bool {
|
||||||
for (_, leaf) in bookside.iter_all_including_invalid() {
|
for (_, leaf) in order_tree.iter() {
|
||||||
if leaf.price() == price {
|
if leaf.price_data() == price_data {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_setup(
|
fn test_setup(price: f64) -> (PerpMarket, I80F48, EventQueue, Box<OrderBook>) {
|
||||||
price: f64,
|
let mut book = Box::new(OrderBook::zeroed());
|
||||||
) -> (
|
book.init();
|
||||||
PerpMarket,
|
|
||||||
I80F48,
|
|
||||||
EventQueue,
|
|
||||||
RefCell<BookSide>,
|
|
||||||
RefCell<BookSide>,
|
|
||||||
) {
|
|
||||||
let bids = RefCell::new(new_bookside(BookSideType::Bids));
|
|
||||||
let asks = RefCell::new(new_bookside(BookSideType::Asks));
|
|
||||||
|
|
||||||
let event_queue = EventQueue::zeroed();
|
let event_queue = EventQueue::zeroed();
|
||||||
|
|
||||||
|
@ -87,49 +70,53 @@ mod tests {
|
||||||
perp_market.init_asset_weight = I80F48::ONE;
|
perp_market.init_asset_weight = I80F48::ONE;
|
||||||
perp_market.init_liab_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
|
// Check what happens when one side of the book fills up
|
||||||
#[test]
|
#[test]
|
||||||
fn book_bids_full() {
|
fn book_bids_full() {
|
||||||
let (mut perp_market, oracle_price, mut event_queue, bids, asks) = test_setup(5000.0);
|
let (mut perp_market, oracle_price, mut event_queue, mut book) = test_setup(5000.0);
|
||||||
let mut book = Book {
|
|
||||||
bids: bids.borrow_mut(),
|
|
||||||
asks: asks.borrow_mut(),
|
|
||||||
};
|
|
||||||
let settle_token_index = 0;
|
let settle_token_index = 0;
|
||||||
|
|
||||||
let mut new_order =
|
let mut new_order = |book: &mut OrderBook,
|
||||||
|book: &mut Book, event_queue: &mut EventQueue, side, price, now_ts| -> i128 {
|
event_queue: &mut EventQueue,
|
||||||
let buffer = MangoAccount::default_for_tests().try_to_vec().unwrap();
|
side,
|
||||||
let mut account = MangoAccountValue::from_bytes(&buffer).unwrap();
|
price_lots,
|
||||||
account
|
now_ts|
|
||||||
.ensure_perp_position(perp_market.perp_market_index, settle_token_index)
|
-> u128 {
|
||||||
.unwrap();
|
let buffer = MangoAccount::default_for_tests().try_to_vec().unwrap();
|
||||||
|
let mut account = MangoAccountValue::from_bytes(&buffer).unwrap();
|
||||||
let quantity = 1;
|
account
|
||||||
let tif = 100;
|
.ensure_perp_position(perp_market.perp_market_index, settle_token_index)
|
||||||
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
.unwrap();
|
.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
|
// insert bids until book side is full
|
||||||
for i in 1..10 {
|
for i in 1..10 {
|
||||||
|
@ -149,50 +136,46 @@ mod tests {
|
||||||
1000 + i as i64,
|
1000 + i as i64,
|
||||||
1000011 as u64,
|
1000011 as u64,
|
||||||
);
|
);
|
||||||
if book.bids.is_full() {
|
if book.bids.fixed.is_full() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assert!(book.bids.is_full());
|
assert!(book.bids.fixed.is_full());
|
||||||
assert_eq!(book.bids.min_leaf().unwrap().price(), 1001);
|
assert_eq!(book.bids.fixed.min_leaf().unwrap().price(), 1001);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
book.bids.max_leaf().unwrap().price(),
|
book.bids.fixed.max_leaf().unwrap().price(),
|
||||||
(1000 + book.bids.leaf_count) as i64
|
(1000 + book.bids.fixed.leaf_count) as i64
|
||||||
);
|
);
|
||||||
|
|
||||||
// add another bid at a higher price before expiry, replacing the lowest-price one (1001)
|
// 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);
|
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);
|
assert_eq!(event_queue.len(), 1);
|
||||||
|
|
||||||
// adding another bid after expiry removes the soonest-expiring order (1005)
|
// adding another bid after expiry removes the soonest-expiring order (1005)
|
||||||
new_order(&mut book, &mut event_queue, Side::Bid, 999, 2000000);
|
new_order(&mut book, &mut event_queue, Side::Bid, 999, 2000000);
|
||||||
assert_eq!(book.bids.min_leaf().unwrap().price(), 999);
|
assert_eq!(book.bids.fixed.min_leaf().unwrap().price(), 999);
|
||||||
assert!(!bookside_contains_key(&book.bids, 1005));
|
assert!(!order_tree_contains_key(&book.bids.fixed, 1005));
|
||||||
assert_eq!(event_queue.len(), 2);
|
assert_eq!(event_queue.len(), 2);
|
||||||
|
|
||||||
// adding an ask will wipe up to three expired bids at the top of the book
|
// 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_max = book.bids.fixed.max_leaf().unwrap().price_data();
|
||||||
let bids_count = book.bids.leaf_count;
|
let bids_count = book.bids.fixed.leaf_count;
|
||||||
new_order(&mut book, &mut event_queue, Side::Ask, 6000, 1500000);
|
new_order(&mut book, &mut event_queue, Side::Ask, 6000, 1500000);
|
||||||
assert_eq!(book.bids.leaf_count, bids_count - 5);
|
assert_eq!(book.bids.fixed.leaf_count, bids_count - 5);
|
||||||
assert_eq!(book.asks.leaf_count, 1);
|
assert_eq!(book.asks.fixed.leaf_count, 1);
|
||||||
assert_eq!(event_queue.len(), 2 + 5);
|
assert_eq!(event_queue.len(), 2 + 5);
|
||||||
assert!(!bookside_contains_price(&book.bids, bids_max));
|
assert!(!order_tree_contains_price(&book.bids.fixed, bids_max));
|
||||||
assert!(!bookside_contains_price(&book.bids, bids_max - 1));
|
assert!(!order_tree_contains_price(&book.bids.fixed, bids_max - 1));
|
||||||
assert!(!bookside_contains_price(&book.bids, bids_max - 2));
|
assert!(!order_tree_contains_price(&book.bids.fixed, bids_max - 2));
|
||||||
assert!(!bookside_contains_price(&book.bids, bids_max - 3));
|
assert!(!order_tree_contains_price(&book.bids.fixed, bids_max - 3));
|
||||||
assert!(!bookside_contains_price(&book.bids, bids_max - 4));
|
assert!(!order_tree_contains_price(&book.bids.fixed, bids_max - 4));
|
||||||
assert!(bookside_contains_price(&book.bids, bids_max - 5));
|
assert!(order_tree_contains_price(&book.bids.fixed, bids_max - 5));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn book_new_order() {
|
fn book_new_order() {
|
||||||
let (mut market, oracle_price, mut event_queue, bids, asks) = test_setup(1000.0);
|
let (mut market, oracle_price, mut event_queue, mut book) = test_setup(1000.0);
|
||||||
let mut book = Book {
|
|
||||||
bids: bids.borrow_mut(),
|
|
||||||
asks: asks.borrow_mut(),
|
|
||||||
};
|
|
||||||
let settle_token_index = 0;
|
let settle_token_index = 0;
|
||||||
|
|
||||||
// Add lots and fees to make sure to exercise unit conversion
|
// Add lots and fees to make sure to exercise unit conversion
|
||||||
|
@ -216,41 +199,48 @@ mod tests {
|
||||||
let now_ts = 1000000;
|
let now_ts = 1000000;
|
||||||
|
|
||||||
// Place a maker-bid
|
// 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;
|
let bid_quantity = 10;
|
||||||
book.new_order(
|
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 market,
|
||||||
&mut event_queue,
|
&mut event_queue,
|
||||||
oracle_price,
|
oracle_price,
|
||||||
&mut maker.borrow_mut(),
|
&mut maker.borrow_mut(),
|
||||||
&maker_pk,
|
&maker_pk,
|
||||||
price,
|
|
||||||
bid_quantity,
|
|
||||||
i64::MAX,
|
|
||||||
OrderType::Limit,
|
|
||||||
0,
|
|
||||||
42,
|
|
||||||
now_ts,
|
now_ts,
|
||||||
u8::MAX,
|
u8::MAX,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(
|
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
|
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!(
|
assert_eq!(
|
||||||
maker.perp_order_mut_by_raw_index(1).order_market,
|
maker.perp_order_mut_by_raw_index(0).side_and_tree,
|
||||||
FREE_ORDER_SLOT
|
SideAndOrderTree::BidFixed
|
||||||
);
|
);
|
||||||
assert_ne!(maker.perp_order_mut_by_raw_index(0).order_id, 0);
|
assert!(order_tree_contains_key(
|
||||||
assert_eq!(maker.perp_order_mut_by_raw_index(0).client_order_id, 42);
|
&book.bids.fixed,
|
||||||
assert_eq!(maker.perp_order_mut_by_raw_index(0).order_side, Side::Bid);
|
maker.perp_order_mut_by_raw_index(0).id
|
||||||
assert!(bookside_contains_key(
|
));
|
||||||
&book.bids,
|
assert!(order_tree_contains_price(
|
||||||
maker.perp_order_mut_by_raw_index(0).order_id
|
&book.bids.fixed,
|
||||||
|
price_lots as u64
|
||||||
));
|
));
|
||||||
assert!(bookside_contains_price(&book.bids, price));
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
maker.perp_position_by_raw_index(0).bids_base_lots,
|
maker.perp_position_by_raw_index(0).bids_base_lots,
|
||||||
bid_quantity
|
bid_quantity
|
||||||
|
@ -271,18 +261,22 @@ mod tests {
|
||||||
// Take the order partially
|
// Take the order partially
|
||||||
let match_quantity = 5;
|
let match_quantity = 5;
|
||||||
book.new_order(
|
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 market,
|
||||||
&mut event_queue,
|
&mut event_queue,
|
||||||
oracle_price,
|
oracle_price,
|
||||||
&mut taker.borrow_mut(),
|
&mut taker.borrow_mut(),
|
||||||
&taker_pk,
|
&taker_pk,
|
||||||
price,
|
|
||||||
match_quantity,
|
|
||||||
i64::MAX,
|
|
||||||
OrderType::Limit,
|
|
||||||
0,
|
|
||||||
43,
|
|
||||||
now_ts,
|
now_ts,
|
||||||
u8::MAX,
|
u8::MAX,
|
||||||
)
|
)
|
||||||
|
@ -290,22 +284,19 @@ mod tests {
|
||||||
// the remainder of the maker order is still on the book
|
// the remainder of the maker order is still on the book
|
||||||
// (the maker account is unchanged: it was not even passed in)
|
// (the maker account is unchanged: it was not even passed in)
|
||||||
let order =
|
let order =
|
||||||
bookside_leaf_by_key(&book.bids, maker.perp_order_by_raw_index(0).order_id).unwrap();
|
order_tree_leaf_by_key(&book.bids.fixed, maker.perp_order_by_raw_index(0).id).unwrap();
|
||||||
assert_eq!(order.price(), price);
|
assert_eq!(order.price(), price_lots);
|
||||||
assert_eq!(order.quantity, bid_quantity - match_quantity);
|
assert_eq!(order.quantity, bid_quantity - match_quantity);
|
||||||
|
|
||||||
// fees were immediately accrued
|
// 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!(
|
assert_eq!(
|
||||||
market.fees_accrued,
|
market.fees_accrued,
|
||||||
match_quote * (market.maker_fee + market.taker_fee)
|
match_quote * (market.maker_fee + market.taker_fee)
|
||||||
);
|
);
|
||||||
|
|
||||||
// the taker account is updated
|
// the taker account is updated
|
||||||
assert_eq!(
|
assert_eq!(taker.perp_order_by_raw_index(0).market, FREE_ORDER_SLOT);
|
||||||
taker.perp_order_by_raw_index(0).order_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).bids_base_lots, 0);
|
||||||
assert_eq!(taker.perp_position_by_raw_index(0).asks_base_lots, 0);
|
assert_eq!(taker.perp_position_by_raw_index(0).asks_base_lots, 0);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -314,7 +305,7 @@ mod tests {
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
taker.perp_position_by_raw_index(0).taker_quote_lots,
|
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!(taker.perp_position_by_raw_index(0).base_position_lots(), 0);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -328,7 +319,7 @@ mod tests {
|
||||||
assert_eq!(event.event_type, EventType::Fill as u8);
|
assert_eq!(event.event_type, EventType::Fill as u8);
|
||||||
let fill: &FillEvent = bytemuck::cast_ref(event);
|
let fill: &FillEvent = bytemuck::cast_ref(event);
|
||||||
assert_eq!(fill.quantity, match_quantity);
|
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.taker_client_order_id, 43);
|
||||||
assert_eq!(fill.maker_client_order_id, 42);
|
assert_eq!(fill.maker_client_order_id, 42);
|
||||||
assert_eq!(fill.maker, maker_pk);
|
assert_eq!(fill.maker, maker_pk);
|
||||||
|
@ -345,7 +336,7 @@ mod tests {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(market.open_interest, 2 * match_quantity);
|
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!(
|
assert_eq!(
|
||||||
maker.perp_position_by_raw_index(0).bids_base_lots,
|
maker.perp_position_by_raw_index(0).bids_base_lots,
|
||||||
bid_quantity - match_quantity
|
bid_quantity - match_quantity
|
||||||
|
@ -378,37 +369,35 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_fee_penalty_applied_only_on_limit_order() -> Result<()> {
|
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 market, oracle_price, mut event_queue, mut book) = test_setup(1000.0);
|
||||||
let mut book = Book {
|
|
||||||
bids: bids.borrow_mut(),
|
|
||||||
asks: asks.borrow_mut(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let buffer = MangoAccount::default_for_tests().try_to_vec().unwrap();
|
let buffer = MangoAccount::default_for_tests().try_to_vec().unwrap();
|
||||||
let mut account = MangoAccountValue::from_bytes(&buffer).unwrap();
|
let mut account = MangoAccountValue::from_bytes(&buffer).unwrap();
|
||||||
let taker_pk = Pubkey::new_unique();
|
let taker_pk = Pubkey::new_unique();
|
||||||
let now_ts = 1000000;
|
let now_ts = 1000000;
|
||||||
|
|
||||||
market.base_lot_size = 1;
|
|
||||||
market.quote_lot_size = 1;
|
|
||||||
market.taker_fee = I80F48::from_num(0.01);
|
market.taker_fee = I80F48::from_num(0.01);
|
||||||
market.fee_penalty = 5.0;
|
market.fee_penalty = 5.0;
|
||||||
account.ensure_perp_position(market.perp_market_index, 0)?;
|
account.ensure_perp_position(market.perp_market_index, 0)?;
|
||||||
|
|
||||||
// Passive order
|
// Passive order
|
||||||
book.new_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 market,
|
||||||
&mut event_queue,
|
&mut event_queue,
|
||||||
oracle_price,
|
oracle_price,
|
||||||
&mut account.borrow_mut(),
|
&mut account.borrow_mut(),
|
||||||
&taker_pk,
|
&taker_pk,
|
||||||
1000,
|
|
||||||
2,
|
|
||||||
i64::MAX,
|
|
||||||
OrderType::Limit,
|
|
||||||
0,
|
|
||||||
43,
|
|
||||||
now_ts,
|
now_ts,
|
||||||
u8::MAX,
|
u8::MAX,
|
||||||
)
|
)
|
||||||
|
@ -416,18 +405,22 @@ mod tests {
|
||||||
|
|
||||||
// Partial taker
|
// Partial taker
|
||||||
book.new_order(
|
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 market,
|
||||||
&mut event_queue,
|
&mut event_queue,
|
||||||
oracle_price,
|
oracle_price,
|
||||||
&mut account.borrow_mut(),
|
&mut account.borrow_mut(),
|
||||||
&taker_pk,
|
&taker_pk,
|
||||||
1000,
|
|
||||||
1,
|
|
||||||
i64::MAX,
|
|
||||||
OrderType::Limit,
|
|
||||||
0,
|
|
||||||
43,
|
|
||||||
now_ts,
|
now_ts,
|
||||||
u8::MAX,
|
u8::MAX,
|
||||||
)
|
)
|
||||||
|
@ -449,18 +442,19 @@ mod tests {
|
||||||
|
|
||||||
// Full taker
|
// Full taker
|
||||||
book.new_order(
|
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 market,
|
||||||
&mut event_queue,
|
&mut event_queue,
|
||||||
oracle_price,
|
oracle_price,
|
||||||
&mut account.borrow_mut(),
|
&mut account.borrow_mut(),
|
||||||
&taker_pk,
|
&taker_pk,
|
||||||
1000,
|
|
||||||
1,
|
|
||||||
i64::MAX,
|
|
||||||
OrderType::ImmediateOrCancel,
|
|
||||||
0,
|
|
||||||
43,
|
|
||||||
now_ts,
|
now_ts,
|
||||||
u8::MAX,
|
u8::MAX,
|
||||||
)
|
)
|
||||||
|
|
|
@ -6,7 +6,7 @@ use mango_macro::Pod;
|
||||||
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
||||||
use static_assertions::const_assert_eq;
|
use static_assertions::const_assert_eq;
|
||||||
|
|
||||||
use super::order_type::OrderType;
|
use super::order_type::{PostOrderType, Side};
|
||||||
|
|
||||||
pub type NodeHandle = u32;
|
pub type NodeHandle = u32;
|
||||||
const NODE_SIZE: usize = 96;
|
const NODE_SIZE: usize = 96;
|
||||||
|
@ -21,6 +21,43 @@ pub enum NodeTag {
|
||||||
LastFreeNode = 4,
|
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.
|
/// InnerNodes and LeafNodes compose the binary tree of orders.
|
||||||
///
|
///
|
||||||
/// Each InnerNode has exactly two children, which are either InnerNodes themselves,
|
/// Each InnerNode has exactly two children, which are either InnerNodes themselves,
|
||||||
|
@ -35,7 +72,7 @@ pub struct InnerNode {
|
||||||
pub prefix_len: u32,
|
pub prefix_len: u32,
|
||||||
|
|
||||||
/// only the top `prefix_len` bits of `key` are relevant
|
/// only the top `prefix_len` bits of `key` are relevant
|
||||||
pub key: i128,
|
pub key: u128,
|
||||||
|
|
||||||
/// indexes into `BookSide::nodes`
|
/// indexes into `BookSide::nodes`
|
||||||
pub children: [NodeHandle; 2],
|
pub children: [NodeHandle; 2],
|
||||||
|
@ -52,7 +89,7 @@ const_assert_eq!(size_of::<InnerNode>() % 8, 0);
|
||||||
const_assert_eq!(size_of::<InnerNode>(), NODE_SIZE);
|
const_assert_eq!(size_of::<InnerNode>(), NODE_SIZE);
|
||||||
|
|
||||||
impl InnerNode {
|
impl InnerNode {
|
||||||
pub fn new(prefix_len: u32, key: i128) -> Self {
|
pub fn new(prefix_len: u32, key: u128) -> Self {
|
||||||
Self {
|
Self {
|
||||||
tag: NodeTag::InnerNode.into(),
|
tag: NodeTag::InnerNode.into(),
|
||||||
prefix_len,
|
prefix_len,
|
||||||
|
@ -65,8 +102,8 @@ impl InnerNode {
|
||||||
|
|
||||||
/// Returns the handle of the child that may contain the search key
|
/// Returns the handle of the child that may contain the search key
|
||||||
/// and 0 or 1 depending on which child it was.
|
/// and 0 or 1 depending on which child it was.
|
||||||
pub(crate) fn walk_down(&self, search_key: i128) -> (NodeHandle, bool) {
|
pub(crate) fn walk_down(&self, search_key: u128) -> (NodeHandle, bool) {
|
||||||
let crit_bit_mask = 1i128 << (127 - self.prefix_len);
|
let crit_bit_mask = 1u128 << (127 - self.prefix_len);
|
||||||
let crit_bit = (search_key & crit_bit_mask) != 0;
|
let crit_bit = (search_key & crit_bit_mask) != 0;
|
||||||
(self.children[crit_bit as usize], crit_bit)
|
(self.children[crit_bit as usize], crit_bit)
|
||||||
}
|
}
|
||||||
|
@ -84,7 +121,7 @@ impl InnerNode {
|
||||||
pub struct LeafNode {
|
pub struct LeafNode {
|
||||||
pub tag: u32,
|
pub tag: u32,
|
||||||
pub owner_slot: u8,
|
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],
|
pub padding: [u8; 1],
|
||||||
|
|
||||||
|
@ -93,7 +130,7 @@ pub struct LeafNode {
|
||||||
pub time_in_force: u8,
|
pub time_in_force: u8,
|
||||||
|
|
||||||
/// The binary tree key
|
/// The binary tree key
|
||||||
pub key: i128,
|
pub key: u128,
|
||||||
|
|
||||||
pub owner: Pubkey,
|
pub owner: Pubkey,
|
||||||
pub quantity: i64,
|
pub quantity: i64,
|
||||||
|
@ -102,27 +139,26 @@ pub struct LeafNode {
|
||||||
// The time the order was placed
|
// The time the order was placed
|
||||||
pub timestamp: u64,
|
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>() % 8, 0);
|
||||||
const_assert_eq!(size_of::<LeafNode>(), NODE_SIZE);
|
const_assert_eq!(size_of::<LeafNode>(), NODE_SIZE);
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
fn key_to_price(key: i128) -> i64 {
|
|
||||||
(key >> 64) as i64
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LeafNode {
|
impl LeafNode {
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn new(
|
pub fn new(
|
||||||
owner_slot: u8,
|
owner_slot: u8,
|
||||||
key: i128,
|
key: u128,
|
||||||
owner: Pubkey,
|
owner: Pubkey,
|
||||||
quantity: i64,
|
quantity: i64,
|
||||||
client_order_id: u64,
|
client_order_id: u64,
|
||||||
timestamp: u64,
|
timestamp: u64,
|
||||||
order_type: OrderType,
|
order_type: PostOrderType,
|
||||||
time_in_force: u8,
|
time_in_force: u8,
|
||||||
|
peg_limit: i64,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
tag: NodeTag::LeafNode.into(),
|
tag: NodeTag::LeafNode.into(),
|
||||||
|
@ -135,13 +171,20 @@ impl LeafNode {
|
||||||
quantity,
|
quantity,
|
||||||
client_order_id,
|
client_order_id,
|
||||||
timestamp,
|
timestamp,
|
||||||
reserved: [0; 16],
|
peg_limit,
|
||||||
|
reserved: [0; 8],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: remove, it's not always the price
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn price(&self) -> i64 {
|
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
|
/// Time at which this order will expire, u64::MAX if never
|
||||||
|
@ -155,7 +198,7 @@ impl LeafNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline(always)]
|
#[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
|
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 {
|
impl AnyNode {
|
||||||
pub fn key(&self) -> Option<i128> {
|
pub fn key(&self) -> Option<u128> {
|
||||||
match self.case()? {
|
match self.case()? {
|
||||||
NodeRef::Inner(inner) => Some(inner.key),
|
NodeRef::Inner(inner) => Some(inner.key),
|
||||||
NodeRef::Leaf(leaf) => Some(leaf.key),
|
NodeRef::Leaf(leaf) => Some(leaf.key),
|
||||||
|
@ -274,3 +317,70 @@ impl AsRef<AnyNode> for LeafNode {
|
||||||
cast_ref(self)
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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)),
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,10 @@
|
||||||
use anchor_lang::prelude::*;
|
use anchor_lang::prelude::*;
|
||||||
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::error::*;
|
||||||
|
use crate::error_msg;
|
||||||
|
|
||||||
#[derive(
|
#[derive(
|
||||||
Eq,
|
Eq,
|
||||||
PartialEq,
|
PartialEq,
|
||||||
|
@ -13,7 +17,7 @@ use num_enum::{IntoPrimitive, TryFromPrimitive};
|
||||||
AnchorDeserialize,
|
AnchorDeserialize,
|
||||||
)]
|
)]
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
pub enum OrderType {
|
pub enum PlaceOrderType {
|
||||||
/// Take existing orders up to price, max_base_quantity and max_quote_quantity.
|
/// 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
|
/// If any base_quantity or quote_quantity remains, place an order on the book
|
||||||
Limit = 0,
|
Limit = 0,
|
||||||
|
@ -37,6 +41,44 @@ pub enum OrderType {
|
||||||
PostOnlySlide = 4,
|
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(
|
#[derive(
|
||||||
Eq,
|
Eq,
|
||||||
PartialEq,
|
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`?
|
/// Is `lhs` is a better order for `side` than `rhs`?
|
||||||
pub fn is_price_better(self: &Side, lhs: i64, rhs: i64) -> bool {
|
pub fn is_price_better(self: &Side, lhs: i64, rhs: i64) -> bool {
|
||||||
match self {
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -187,7 +187,7 @@ pub struct FillEvent {
|
||||||
pub seq_num: u64,
|
pub seq_num: u64,
|
||||||
|
|
||||||
pub maker: Pubkey,
|
pub maker: Pubkey,
|
||||||
pub maker_order_id: i128,
|
pub maker_order_id: u128,
|
||||||
pub maker_client_order_id: u64,
|
pub maker_client_order_id: u64,
|
||||||
pub maker_fee: I80F48,
|
pub maker_fee: I80F48,
|
||||||
|
|
||||||
|
@ -195,7 +195,7 @@ pub struct FillEvent {
|
||||||
pub maker_timestamp: u64,
|
pub maker_timestamp: u64,
|
||||||
|
|
||||||
pub taker: Pubkey,
|
pub taker: Pubkey,
|
||||||
pub taker_order_id: i128,
|
pub taker_order_id: u128,
|
||||||
pub taker_client_order_id: u64,
|
pub taker_client_order_id: u64,
|
||||||
pub taker_fee: I80F48,
|
pub taker_fee: I80F48,
|
||||||
|
|
||||||
|
@ -215,13 +215,13 @@ impl FillEvent {
|
||||||
timestamp: u64,
|
timestamp: u64,
|
||||||
seq_num: u64,
|
seq_num: u64,
|
||||||
maker: Pubkey,
|
maker: Pubkey,
|
||||||
maker_order_id: i128,
|
maker_order_id: u128,
|
||||||
maker_client_order_id: u64,
|
maker_client_order_id: u64,
|
||||||
maker_fee: I80F48,
|
maker_fee: I80F48,
|
||||||
maker_timestamp: u64,
|
maker_timestamp: u64,
|
||||||
|
|
||||||
taker: Pubkey,
|
taker: Pubkey,
|
||||||
taker_order_id: i128,
|
taker_order_id: u128,
|
||||||
taker_client_order_id: u64,
|
taker_client_order_id: u64,
|
||||||
taker_fee: I80F48,
|
taker_fee: I80F48,
|
||||||
price: i64,
|
price: i64,
|
||||||
|
|
|
@ -6,11 +6,11 @@ use fixed::types::I80F48;
|
||||||
use static_assertions::const_assert_eq;
|
use static_assertions::const_assert_eq;
|
||||||
|
|
||||||
use crate::accounts_zerocopy::KeyedAccountReader;
|
use crate::accounts_zerocopy::KeyedAccountReader;
|
||||||
use crate::state::orderbook::order_type::Side;
|
use crate::state::orderbook::Side;
|
||||||
use crate::state::{oracle, TokenIndex};
|
use crate::state::{oracle, TokenIndex};
|
||||||
use crate::util::checked_math as cm;
|
use crate::util::checked_math as cm;
|
||||||
|
|
||||||
use super::{Book, OracleConfig, DAY_I80F48};
|
use super::{orderbook, OracleConfig, OrderBook, DAY_I80F48};
|
||||||
|
|
||||||
pub type PerpMarketIndex = u16;
|
pub type PerpMarketIndex = u16;
|
||||||
|
|
||||||
|
@ -39,8 +39,8 @@ pub struct PerpMarket {
|
||||||
|
|
||||||
pub oracle_config: OracleConfig,
|
pub oracle_config: OracleConfig,
|
||||||
|
|
||||||
pub bids: Pubkey,
|
pub orderbook: Pubkey,
|
||||||
pub asks: Pubkey,
|
pub padding3: [u8; 32],
|
||||||
|
|
||||||
pub event_queue: Pubkey,
|
pub event_queue: Pubkey,
|
||||||
|
|
||||||
|
@ -133,14 +133,9 @@ impl PerpMarket {
|
||||||
self.trusted_market == 1
|
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;
|
self.seq_num += 1;
|
||||||
|
orderbook::new_node_key(side, price_data, self.seq_num)
|
||||||
let upper = (price as i128) << 64;
|
|
||||||
match side {
|
|
||||||
Side::Bid => upper | (!self.seq_num as i128),
|
|
||||||
Side::Ask => upper | (self.seq_num as i128),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn oracle_price(&self, oracle_acc: &impl KeyedAccountReader) -> Result<I80F48> {
|
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
|
/// 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 index_price = oracle_price;
|
||||||
|
let oracle_price_lots = self.native_price_to_lot(oracle_price);
|
||||||
|
|
||||||
// Get current book price & compare it to index price
|
// Get current book price & compare it to index price
|
||||||
let bid = book.impact_price(Side::Bid, 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);
|
let ask = book.impact_price(Side::Ask, self.impact_quantity, now_ts, oracle_price_lots);
|
||||||
|
|
||||||
let diff_price = match (bid, ask) {
|
let diff_price = match (bid, ask) {
|
||||||
(Some(bid), Some(ask)) => {
|
(Some(bid), Some(ask)) => {
|
||||||
|
@ -247,8 +248,7 @@ impl PerpMarket {
|
||||||
oracle_config: OracleConfig {
|
oracle_config: OracleConfig {
|
||||||
conf_filter: I80F48::ZERO,
|
conf_filter: I80F48::ZERO,
|
||||||
},
|
},
|
||||||
bids: Pubkey::new_unique(),
|
orderbook: Pubkey::new_unique(),
|
||||||
asks: Pubkey::new_unique(),
|
|
||||||
event_queue: Pubkey::new_unique(),
|
event_queue: Pubkey::new_unique(),
|
||||||
quote_lot_size: 1,
|
quote_lot_size: 1,
|
||||||
base_lot_size: 1,
|
base_lot_size: 1,
|
||||||
|
@ -274,6 +274,7 @@ impl PerpMarket {
|
||||||
reserved: [0; 92],
|
reserved: [0; 92],
|
||||||
padding1: Default::default(),
|
padding1: Default::default(),
|
||||||
padding2: Default::default(),
|
padding2: Default::default(),
|
||||||
|
padding3: Default::default(),
|
||||||
registration_time: 0,
|
registration_time: 0,
|
||||||
fee_penalty: 0.0,
|
fee_penalty: 0.0,
|
||||||
trusted_market: 0,
|
trusted_market: 0,
|
||||||
|
|
|
@ -2197,8 +2197,7 @@ pub struct PerpCreateMarketInstruction {
|
||||||
pub group: Pubkey,
|
pub group: Pubkey,
|
||||||
pub admin: TestKeypair,
|
pub admin: TestKeypair,
|
||||||
pub oracle: Pubkey,
|
pub oracle: Pubkey,
|
||||||
pub asks: Pubkey,
|
pub orderbook: Pubkey,
|
||||||
pub bids: Pubkey,
|
|
||||||
pub event_queue: Pubkey,
|
pub event_queue: Pubkey,
|
||||||
pub payer: TestKeypair,
|
pub payer: TestKeypair,
|
||||||
pub settle_token_index: TokenIndex,
|
pub settle_token_index: TokenIndex,
|
||||||
|
@ -2226,11 +2225,8 @@ impl PerpCreateMarketInstruction {
|
||||||
base: &crate::mango_setup::Token,
|
base: &crate::mango_setup::Token,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
PerpCreateMarketInstruction {
|
PerpCreateMarketInstruction {
|
||||||
asks: solana
|
orderbook: solana
|
||||||
.create_account_for_type::<BookSide>(&mango_v4::id())
|
.create_account_for_type::<OrderBook>(&mango_v4::id())
|
||||||
.await,
|
|
||||||
bids: solana
|
|
||||||
.create_account_for_type::<BookSide>(&mango_v4::id())
|
|
||||||
.await,
|
.await,
|
||||||
event_queue: solana
|
event_queue: solana
|
||||||
.create_account_for_type::<EventQueue>(&mango_v4::id())
|
.create_account_for_type::<EventQueue>(&mango_v4::id())
|
||||||
|
@ -2293,8 +2289,7 @@ impl ClientInstruction for PerpCreateMarketInstruction {
|
||||||
admin: self.admin.pubkey(),
|
admin: self.admin.pubkey(),
|
||||||
oracle: self.oracle,
|
oracle: self.oracle,
|
||||||
perp_market,
|
perp_market,
|
||||||
asks: self.asks,
|
orderbook: self.orderbook,
|
||||||
bids: self.bids,
|
|
||||||
event_queue: self.event_queue,
|
event_queue: self.event_queue,
|
||||||
payer: self.payer.pubkey(),
|
payer: self.payer.pubkey(),
|
||||||
system_program: System::id(),
|
system_program: System::id(),
|
||||||
|
@ -2310,12 +2305,8 @@ impl ClientInstruction for PerpCreateMarketInstruction {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct PerpCloseMarketInstruction {
|
pub struct PerpCloseMarketInstruction {
|
||||||
pub group: Pubkey,
|
|
||||||
pub admin: TestKeypair,
|
pub admin: TestKeypair,
|
||||||
pub perp_market: Pubkey,
|
pub perp_market: Pubkey,
|
||||||
pub asks: Pubkey,
|
|
||||||
pub bids: Pubkey,
|
|
||||||
pub event_queue: Pubkey,
|
|
||||||
pub sol_destination: Pubkey,
|
pub sol_destination: Pubkey,
|
||||||
}
|
}
|
||||||
#[async_trait::async_trait(?Send)]
|
#[async_trait::async_trait(?Send)]
|
||||||
|
@ -2324,18 +2315,18 @@ impl ClientInstruction for PerpCloseMarketInstruction {
|
||||||
type Instruction = mango_v4::instruction::PerpCloseMarket;
|
type Instruction = mango_v4::instruction::PerpCloseMarket;
|
||||||
async fn to_instruction(
|
async fn to_instruction(
|
||||||
&self,
|
&self,
|
||||||
_loader: impl ClientAccountLoader + 'async_trait,
|
account_loader: impl ClientAccountLoader + 'async_trait,
|
||||||
) -> (Self::Accounts, instruction::Instruction) {
|
) -> (Self::Accounts, instruction::Instruction) {
|
||||||
let program_id = mango_v4::id();
|
let program_id = mango_v4::id();
|
||||||
let instruction = Self::Instruction {};
|
let instruction = Self::Instruction {};
|
||||||
|
let perp_market: PerpMarket = account_loader.load(&self.perp_market).await.unwrap();
|
||||||
|
|
||||||
let accounts = Self::Accounts {
|
let accounts = Self::Accounts {
|
||||||
group: self.group,
|
group: perp_market.group,
|
||||||
admin: self.admin.pubkey(),
|
admin: self.admin.pubkey(),
|
||||||
perp_market: self.perp_market,
|
perp_market: self.perp_market,
|
||||||
asks: self.asks,
|
orderbook: perp_market.orderbook,
|
||||||
bids: self.bids,
|
event_queue: perp_market.event_queue,
|
||||||
event_queue: self.event_queue,
|
|
||||||
token_program: Token::id(),
|
token_program: Token::id(),
|
||||||
sol_destination: self.sol_destination,
|
sol_destination: self.sol_destination,
|
||||||
};
|
};
|
||||||
|
@ -2407,9 +2398,9 @@ impl ClientInstruction for PerpPlaceOrderInstruction {
|
||||||
max_base_lots: self.max_base_lots,
|
max_base_lots: self.max_base_lots,
|
||||||
max_quote_lots: self.max_quote_lots,
|
max_quote_lots: self.max_quote_lots,
|
||||||
client_order_id: self.client_order_id,
|
client_order_id: self.client_order_id,
|
||||||
order_type: OrderType::Limit,
|
order_type: PlaceOrderType::Limit,
|
||||||
expiry_timestamp: 0,
|
expiry_timestamp: 0,
|
||||||
limit: 1,
|
limit: 10,
|
||||||
};
|
};
|
||||||
|
|
||||||
let perp_market: PerpMarket = account_loader.load(&self.perp_market).await.unwrap();
|
let perp_market: PerpMarket = account_loader.load(&self.perp_market).await.unwrap();
|
||||||
|
@ -2430,8 +2421,73 @@ impl ClientInstruction for PerpPlaceOrderInstruction {
|
||||||
group: account.fixed.group,
|
group: account.fixed.group,
|
||||||
account: self.account,
|
account: self.account,
|
||||||
perp_market: self.perp_market,
|
perp_market: self.perp_market,
|
||||||
asks: perp_market.asks,
|
orderbook: perp_market.orderbook,
|
||||||
bids: perp_market.bids,
|
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,
|
event_queue: perp_market.event_queue,
|
||||||
oracle: perp_market.oracle,
|
oracle: perp_market.oracle,
|
||||||
owner: self.owner.pubkey(),
|
owner: self.owner.pubkey(),
|
||||||
|
@ -2448,13 +2504,10 @@ impl ClientInstruction for PerpPlaceOrderInstruction {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct PerpCancelOrderInstruction {
|
pub struct PerpCancelOrderInstruction {
|
||||||
pub group: Pubkey,
|
|
||||||
pub account: Pubkey,
|
pub account: Pubkey,
|
||||||
pub perp_market: Pubkey,
|
pub perp_market: Pubkey,
|
||||||
pub asks: Pubkey,
|
|
||||||
pub bids: Pubkey,
|
|
||||||
pub owner: TestKeypair,
|
pub owner: TestKeypair,
|
||||||
pub order_id: i128,
|
pub order_id: u128,
|
||||||
}
|
}
|
||||||
#[async_trait::async_trait(?Send)]
|
#[async_trait::async_trait(?Send)]
|
||||||
impl ClientInstruction for PerpCancelOrderInstruction {
|
impl ClientInstruction for PerpCancelOrderInstruction {
|
||||||
|
@ -2462,18 +2515,18 @@ impl ClientInstruction for PerpCancelOrderInstruction {
|
||||||
type Instruction = mango_v4::instruction::PerpCancelOrder;
|
type Instruction = mango_v4::instruction::PerpCancelOrder;
|
||||||
async fn to_instruction(
|
async fn to_instruction(
|
||||||
&self,
|
&self,
|
||||||
_loader: impl ClientAccountLoader + 'async_trait,
|
account_loader: impl ClientAccountLoader + 'async_trait,
|
||||||
) -> (Self::Accounts, instruction::Instruction) {
|
) -> (Self::Accounts, instruction::Instruction) {
|
||||||
let program_id = mango_v4::id();
|
let program_id = mango_v4::id();
|
||||||
let instruction = Self::Instruction {
|
let instruction = Self::Instruction {
|
||||||
order_id: self.order_id,
|
order_id: self.order_id,
|
||||||
};
|
};
|
||||||
|
let perp_market: PerpMarket = account_loader.load(&self.perp_market).await.unwrap();
|
||||||
let accounts = Self::Accounts {
|
let accounts = Self::Accounts {
|
||||||
group: self.group,
|
group: perp_market.group,
|
||||||
account: self.account,
|
account: self.account,
|
||||||
perp_market: self.perp_market,
|
perp_market: self.perp_market,
|
||||||
asks: self.asks,
|
orderbook: perp_market.orderbook,
|
||||||
bids: self.bids,
|
|
||||||
owner: self.owner.pubkey(),
|
owner: self.owner.pubkey(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -2487,11 +2540,8 @@ impl ClientInstruction for PerpCancelOrderInstruction {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct PerpCancelOrderByClientOrderIdInstruction {
|
pub struct PerpCancelOrderByClientOrderIdInstruction {
|
||||||
pub group: Pubkey,
|
|
||||||
pub account: Pubkey,
|
pub account: Pubkey,
|
||||||
pub perp_market: Pubkey,
|
pub perp_market: Pubkey,
|
||||||
pub asks: Pubkey,
|
|
||||||
pub bids: Pubkey,
|
|
||||||
pub owner: TestKeypair,
|
pub owner: TestKeypair,
|
||||||
pub client_order_id: u64,
|
pub client_order_id: u64,
|
||||||
}
|
}
|
||||||
|
@ -2501,18 +2551,18 @@ impl ClientInstruction for PerpCancelOrderByClientOrderIdInstruction {
|
||||||
type Instruction = mango_v4::instruction::PerpCancelOrderByClientOrderId;
|
type Instruction = mango_v4::instruction::PerpCancelOrderByClientOrderId;
|
||||||
async fn to_instruction(
|
async fn to_instruction(
|
||||||
&self,
|
&self,
|
||||||
_loader: impl ClientAccountLoader + 'async_trait,
|
account_loader: impl ClientAccountLoader + 'async_trait,
|
||||||
) -> (Self::Accounts, instruction::Instruction) {
|
) -> (Self::Accounts, instruction::Instruction) {
|
||||||
let program_id = mango_v4::id();
|
let program_id = mango_v4::id();
|
||||||
let instruction = Self::Instruction {
|
let instruction = Self::Instruction {
|
||||||
client_order_id: self.client_order_id,
|
client_order_id: self.client_order_id,
|
||||||
};
|
};
|
||||||
|
let perp_market: PerpMarket = account_loader.load(&self.perp_market).await.unwrap();
|
||||||
let accounts = Self::Accounts {
|
let accounts = Self::Accounts {
|
||||||
group: self.group,
|
group: perp_market.group,
|
||||||
account: self.account,
|
account: self.account,
|
||||||
perp_market: self.perp_market,
|
perp_market: self.perp_market,
|
||||||
asks: self.asks,
|
orderbook: perp_market.orderbook,
|
||||||
bids: self.bids,
|
|
||||||
owner: self.owner.pubkey(),
|
owner: self.owner.pubkey(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -2545,8 +2595,7 @@ impl ClientInstruction for PerpCancelAllOrdersInstruction {
|
||||||
group: perp_market.group,
|
group: perp_market.group,
|
||||||
account: self.account,
|
account: self.account,
|
||||||
perp_market: self.perp_market,
|
perp_market: self.perp_market,
|
||||||
asks: perp_market.asks,
|
orderbook: perp_market.orderbook,
|
||||||
bids: perp_market.bids,
|
|
||||||
owner: self.owner.pubkey(),
|
owner: self.owner.pubkey(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -2598,10 +2647,7 @@ impl ClientInstruction for PerpConsumeEventsInstruction {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct PerpUpdateFundingInstruction {
|
pub struct PerpUpdateFundingInstruction {
|
||||||
pub group: Pubkey,
|
|
||||||
pub perp_market: Pubkey,
|
pub perp_market: Pubkey,
|
||||||
pub bids: Pubkey,
|
|
||||||
pub asks: Pubkey,
|
|
||||||
pub bank: Pubkey,
|
pub bank: Pubkey,
|
||||||
pub oracle: Pubkey,
|
pub oracle: Pubkey,
|
||||||
}
|
}
|
||||||
|
@ -2611,15 +2657,15 @@ impl ClientInstruction for PerpUpdateFundingInstruction {
|
||||||
type Instruction = mango_v4::instruction::PerpUpdateFunding;
|
type Instruction = mango_v4::instruction::PerpUpdateFunding;
|
||||||
async fn to_instruction(
|
async fn to_instruction(
|
||||||
&self,
|
&self,
|
||||||
_loader: impl ClientAccountLoader + 'async_trait,
|
account_loader: impl ClientAccountLoader + 'async_trait,
|
||||||
) -> (Self::Accounts, instruction::Instruction) {
|
) -> (Self::Accounts, instruction::Instruction) {
|
||||||
let program_id = mango_v4::id();
|
let program_id = mango_v4::id();
|
||||||
let instruction = Self::Instruction {};
|
let instruction = Self::Instruction {};
|
||||||
|
let perp_market: PerpMarket = account_loader.load(&self.perp_market).await.unwrap();
|
||||||
let accounts = Self::Accounts {
|
let accounts = Self::Accounts {
|
||||||
group: self.group,
|
group: perp_market.group,
|
||||||
perp_market: self.perp_market,
|
perp_market: self.perp_market,
|
||||||
bids: self.bids,
|
orderbook: perp_market.orderbook,
|
||||||
asks: self.asks,
|
|
||||||
oracle: self.oracle,
|
oracle: self.oracle,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -2781,8 +2827,7 @@ impl ClientInstruction for PerpLiqForceCancelOrdersInstruction {
|
||||||
group: account.fixed.group,
|
group: account.fixed.group,
|
||||||
perp_market: self.perp_market,
|
perp_market: self.perp_market,
|
||||||
account: self.account,
|
account: self.account,
|
||||||
bids: perp_market.bids,
|
orderbook: perp_market.orderbook,
|
||||||
asks: perp_market.asks,
|
|
||||||
oracle: perp_market.oracle,
|
oracle: perp_market.oracle,
|
||||||
};
|
};
|
||||||
let mut instruction = make_instruction(program_id, &accounts, instruction);
|
let mut instruction = make_instruction(program_id, &accounts, instruction);
|
||||||
|
|
|
@ -217,19 +217,18 @@ impl SolanaCookie {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_account_opt<T: AccountDeserialize>(&self, address: Pubkey) -> Option<T> {
|
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 data = self.get_account_data(address).await?;
|
||||||
let mut data_slice: &[u8] = &data;
|
let mut data_slice: &[u8] = &data;
|
||||||
AccountDeserialize::try_deserialize(&mut data_slice).ok()
|
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 {
|
pub async fn get_account<T: AccountDeserialize>(&self, address: Pubkey) -> T {
|
||||||
self.get_account_opt(address).await.unwrap()
|
self.get_account_opt(address).await.unwrap()
|
||||||
}
|
}
|
||||||
|
|
|
@ -203,13 +203,7 @@ async fn test_health_compute_perp() -> Result<(), TransportError> {
|
||||||
//
|
//
|
||||||
let mut perp_markets = vec![];
|
let mut perp_markets = vec![];
|
||||||
for (perp_market_index, token) in tokens[1..].iter().enumerate() {
|
for (perp_market_index, token) in tokens[1..].iter().enumerate() {
|
||||||
let mango_v4::accounts::PerpCreateMarket {
|
let mango_v4::accounts::PerpCreateMarket { perp_market, .. } = send_tx(
|
||||||
perp_market,
|
|
||||||
asks,
|
|
||||||
bids,
|
|
||||||
event_queue,
|
|
||||||
..
|
|
||||||
} = send_tx(
|
|
||||||
solana,
|
solana,
|
||||||
PerpCreateMarketInstruction {
|
PerpCreateMarketInstruction {
|
||||||
group,
|
group,
|
||||||
|
@ -231,18 +225,18 @@ async fn test_health_compute_perp() -> Result<(), TransportError> {
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
perp_markets.push((perp_market, asks, bids, event_queue));
|
perp_markets.push(perp_market);
|
||||||
}
|
}
|
||||||
|
|
||||||
let price_lots = {
|
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))
|
perp_market.native_price_to_lot(I80F48::from(1))
|
||||||
};
|
};
|
||||||
|
|
||||||
//
|
//
|
||||||
// TEST: Create a perp order for each market
|
// 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);
|
println!("adding market {}", i);
|
||||||
send_tx(
|
send_tx(
|
||||||
solana,
|
solana,
|
||||||
|
|
|
@ -13,7 +13,7 @@ use utils::assert_equal_fixed_f64 as assert_equal;
|
||||||
mod program_test;
|
mod program_test;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_perp() -> Result<(), TransportError> {
|
async fn test_perp_fixed() -> Result<(), TransportError> {
|
||||||
let context = TestContext::new().await;
|
let context = TestContext::new().await;
|
||||||
let solana = &context.solana.clone();
|
let solana = &context.solana.clone();
|
||||||
|
|
||||||
|
@ -67,9 +67,7 @@ async fn test_perp() -> Result<(), TransportError> {
|
||||||
//
|
//
|
||||||
let mango_v4::accounts::PerpCreateMarket {
|
let mango_v4::accounts::PerpCreateMarket {
|
||||||
perp_market,
|
perp_market,
|
||||||
asks,
|
orderbook,
|
||||||
bids,
|
|
||||||
event_queue,
|
|
||||||
..
|
..
|
||||||
} = send_tx(
|
} = send_tx(
|
||||||
solana,
|
solana,
|
||||||
|
@ -118,19 +116,18 @@ async fn test_perp() -> Result<(), TransportError> {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
check_prev_instruction_post_health(&solana, account_0).await;
|
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
|
let order_id_to_cancel = solana
|
||||||
.get_account::<MangoAccount>(account_0)
|
.get_account::<MangoAccount>(account_0)
|
||||||
.await
|
.await
|
||||||
.perp_open_orders[0]
|
.perp_open_orders[0]
|
||||||
.order_id;
|
.id;
|
||||||
send_tx(
|
send_tx(
|
||||||
solana,
|
solana,
|
||||||
PerpCancelOrderInstruction {
|
PerpCancelOrderInstruction {
|
||||||
group,
|
|
||||||
account: account_0,
|
account: account_0,
|
||||||
perp_market,
|
perp_market,
|
||||||
asks,
|
|
||||||
bids,
|
|
||||||
owner,
|
owner,
|
||||||
order_id: order_id_to_cancel,
|
order_id: order_id_to_cancel,
|
||||||
},
|
},
|
||||||
|
@ -163,11 +160,8 @@ async fn test_perp() -> Result<(), TransportError> {
|
||||||
send_tx(
|
send_tx(
|
||||||
solana,
|
solana,
|
||||||
PerpCancelOrderByClientOrderIdInstruction {
|
PerpCancelOrderByClientOrderIdInstruction {
|
||||||
group,
|
|
||||||
account: account_0,
|
account: account_0,
|
||||||
perp_market,
|
perp_market,
|
||||||
asks,
|
|
||||||
bids,
|
|
||||||
owner,
|
owner,
|
||||||
client_order_id: 1,
|
client_order_id: 1,
|
||||||
},
|
},
|
||||||
|
@ -198,12 +192,13 @@ async fn test_perp() -> Result<(), TransportError> {
|
||||||
check_prev_instruction_post_health(&solana, account_0).await;
|
check_prev_instruction_post_health(&solana, account_0).await;
|
||||||
send_tx(
|
send_tx(
|
||||||
solana,
|
solana,
|
||||||
PerpPlaceOrderInstruction {
|
PerpPlaceOrderPeggedInstruction {
|
||||||
account: account_0,
|
account: account_0,
|
||||||
perp_market,
|
perp_market,
|
||||||
owner,
|
owner,
|
||||||
side: Side::Bid,
|
side: Side::Bid,
|
||||||
price_lots,
|
price_offset: -1,
|
||||||
|
peg_limit: -1,
|
||||||
max_base_lots: 1,
|
max_base_lots: 1,
|
||||||
max_quote_lots: i64::MAX,
|
max_quote_lots: i64::MAX,
|
||||||
client_order_id: 3,
|
client_order_id: 3,
|
||||||
|
@ -445,12 +440,8 @@ async fn test_perp() -> Result<(), TransportError> {
|
||||||
send_tx(
|
send_tx(
|
||||||
solana,
|
solana,
|
||||||
PerpCloseMarketInstruction {
|
PerpCloseMarketInstruction {
|
||||||
group,
|
|
||||||
admin,
|
admin,
|
||||||
perp_market,
|
perp_market,
|
||||||
asks,
|
|
||||||
bids,
|
|
||||||
event_queue,
|
|
||||||
sol_destination: payer.pubkey(),
|
sol_destination: payer.pubkey(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -460,13 +451,433 @@ async fn test_perp() -> Result<(), TransportError> {
|
||||||
Ok(())
|
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) {
|
async fn assert_no_perp_orders(solana: &SolanaCookie, account_0: Pubkey) {
|
||||||
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
|
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
|
||||||
|
|
||||||
for oo in mango_account_0.perp_open_orders.iter() {
|
for oo in mango_account_0.perp_open_orders.iter() {
|
||||||
assert!(oo.order_id == 0);
|
assert!(oo.id == 0);
|
||||||
assert!(oo.order_side == Side::Bid);
|
assert!(oo.side_and_tree == SideAndOrderTree::BidFixed);
|
||||||
assert!(oo.client_order_id == 0);
|
assert!(oo.client_id == 0);
|
||||||
assert!(oo.order_market == FREE_ORDER_SLOT);
|
assert!(oo.market == FREE_ORDER_SLOT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue