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 crate::error::MangoError;
|
||||
use crate::state::{AccountLoaderDynamic, Book, BookSide, Group, MangoAccount, PerpMarket};
|
||||
use crate::state::{AccountLoaderDynamic, Group, MangoAccount, OrderBook, PerpMarket};
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct PerpCancelAllOrders<'info> {
|
||||
|
@ -14,14 +14,11 @@ pub struct PerpCancelAllOrders<'info> {
|
|||
#[account(
|
||||
mut,
|
||||
has_one = group,
|
||||
has_one = bids,
|
||||
has_one = asks
|
||||
has_one = orderbook,
|
||||
)]
|
||||
pub perp_market: AccountLoader<'info, PerpMarket>,
|
||||
#[account(mut)]
|
||||
pub asks: AccountLoader<'info, BookSide>,
|
||||
#[account(mut)]
|
||||
pub bids: AccountLoader<'info, BookSide>,
|
||||
pub orderbook: AccountLoader<'info, OrderBook>,
|
||||
}
|
||||
|
||||
pub fn perp_cancel_all_orders(ctx: Context<PerpCancelAllOrders>, limit: u8) -> Result<()> {
|
||||
|
@ -32,9 +29,7 @@ pub fn perp_cancel_all_orders(ctx: Context<PerpCancelAllOrders>, limit: u8) -> R
|
|||
);
|
||||
|
||||
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
|
||||
let bids = ctx.accounts.bids.load_mut()?;
|
||||
let asks = ctx.accounts.asks.load_mut()?;
|
||||
let mut book = Book::new(bids, asks);
|
||||
let mut book = ctx.accounts.orderbook.load_mut()?;
|
||||
|
||||
book.cancel_all_orders(&mut account.borrow_mut(), &mut perp_market, limit, None)?;
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use anchor_lang::prelude::*;
|
||||
|
||||
use crate::error::MangoError;
|
||||
use crate::state::{AccountLoaderDynamic, Book, BookSide, Group, MangoAccount, PerpMarket, Side};
|
||||
use crate::state::{AccountLoaderDynamic, Group, MangoAccount, OrderBook, PerpMarket, Side};
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct PerpCancelAllOrdersBySide<'info> {
|
||||
|
@ -14,14 +14,11 @@ pub struct PerpCancelAllOrdersBySide<'info> {
|
|||
#[account(
|
||||
mut,
|
||||
has_one = group,
|
||||
has_one = bids,
|
||||
has_one = asks
|
||||
has_one = orderbook,
|
||||
)]
|
||||
pub perp_market: AccountLoader<'info, PerpMarket>,
|
||||
#[account(mut)]
|
||||
pub asks: AccountLoader<'info, BookSide>,
|
||||
#[account(mut)]
|
||||
pub bids: AccountLoader<'info, BookSide>,
|
||||
pub orderbook: AccountLoader<'info, OrderBook>,
|
||||
}
|
||||
|
||||
pub fn perp_cancel_all_orders_by_side(
|
||||
|
@ -37,9 +34,7 @@ pub fn perp_cancel_all_orders_by_side(
|
|||
);
|
||||
|
||||
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
|
||||
let bids = ctx.accounts.bids.load_mut()?;
|
||||
let asks = ctx.accounts.asks.load_mut()?;
|
||||
let mut book = Book::new(bids, asks);
|
||||
let mut book = ctx.accounts.orderbook.load_mut()?;
|
||||
|
||||
book.cancel_all_orders(
|
||||
&mut account.borrow_mut(),
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use anchor_lang::prelude::*;
|
||||
|
||||
use crate::error::*;
|
||||
use crate::state::{AccountLoaderDynamic, Book, BookSide, Group, MangoAccount, PerpMarket};
|
||||
use crate::state::{AccountLoaderDynamic, Group, MangoAccount, OrderBook, PerpMarket};
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct PerpCancelOrder<'info> {
|
||||
|
@ -14,17 +14,14 @@ pub struct PerpCancelOrder<'info> {
|
|||
#[account(
|
||||
mut,
|
||||
has_one = group,
|
||||
has_one = bids,
|
||||
has_one = asks
|
||||
has_one = orderbook,
|
||||
)]
|
||||
pub perp_market: AccountLoader<'info, PerpMarket>,
|
||||
#[account(mut)]
|
||||
pub asks: AccountLoader<'info, BookSide>,
|
||||
#[account(mut)]
|
||||
pub bids: AccountLoader<'info, BookSide>,
|
||||
pub orderbook: AccountLoader<'info, OrderBook>,
|
||||
}
|
||||
|
||||
pub fn perp_cancel_order(ctx: Context<PerpCancelOrder>, order_id: i128) -> Result<()> {
|
||||
pub fn perp_cancel_order(ctx: Context<PerpCancelOrder>, order_id: u128) -> Result<()> {
|
||||
let mut account = ctx.accounts.account.load_mut()?;
|
||||
require!(
|
||||
account.fixed.is_owner_or_delegate(ctx.accounts.owner.key()),
|
||||
|
@ -32,20 +29,20 @@ pub fn perp_cancel_order(ctx: Context<PerpCancelOrder>, order_id: i128) -> Resul
|
|||
);
|
||||
|
||||
let perp_market = ctx.accounts.perp_market.load_mut()?;
|
||||
let bids = ctx.accounts.bids.load_mut()?;
|
||||
let asks = ctx.accounts.asks.load_mut()?;
|
||||
let mut book = Book::new(bids, asks);
|
||||
let mut book = ctx.accounts.orderbook.load_mut()?;
|
||||
|
||||
let side = account
|
||||
.perp_find_order_side(perp_market.perp_market_index, order_id)
|
||||
let oo = account
|
||||
.perp_find_order_with_order_id(perp_market.perp_market_index, order_id)
|
||||
.ok_or_else(|| {
|
||||
error_msg!("could not find perp order with id {order_id} in perp market orderbook")
|
||||
})?;
|
||||
let order_id = oo.id;
|
||||
let order_side_and_tree = oo.side_and_tree;
|
||||
|
||||
book.cancel_order(
|
||||
&mut account.borrow_mut(),
|
||||
order_id,
|
||||
side,
|
||||
order_side_and_tree,
|
||||
Some(ctx.accounts.account.key()),
|
||||
)?;
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use anchor_lang::prelude::*;
|
||||
|
||||
use crate::error::*;
|
||||
use crate::state::{AccountLoaderDynamic, Book, BookSide, Group, MangoAccount, PerpMarket};
|
||||
use crate::state::{AccountLoaderDynamic, Group, MangoAccount, OrderBook, PerpMarket};
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct PerpCancelOrderByClientOrderId<'info> {
|
||||
|
@ -14,14 +14,11 @@ pub struct PerpCancelOrderByClientOrderId<'info> {
|
|||
#[account(
|
||||
mut,
|
||||
has_one = group,
|
||||
has_one = bids,
|
||||
has_one = asks
|
||||
has_one = orderbook,
|
||||
)]
|
||||
pub perp_market: AccountLoader<'info, PerpMarket>,
|
||||
#[account(mut)]
|
||||
pub asks: AccountLoader<'info, BookSide>,
|
||||
#[account(mut)]
|
||||
pub bids: AccountLoader<'info, BookSide>,
|
||||
pub orderbook: AccountLoader<'info, OrderBook>,
|
||||
}
|
||||
|
||||
pub fn perp_cancel_order_by_client_order_id(
|
||||
|
@ -35,18 +32,18 @@ pub fn perp_cancel_order_by_client_order_id(
|
|||
);
|
||||
|
||||
let perp_market = ctx.accounts.perp_market.load_mut()?;
|
||||
let bids = ctx.accounts.bids.load_mut()?;
|
||||
let asks = ctx.accounts.asks.load_mut()?;
|
||||
let mut book = Book::new(bids, asks);
|
||||
let mut book = ctx.accounts.orderbook.load_mut()?;
|
||||
|
||||
let (order_id, side) = account
|
||||
let oo = account
|
||||
.perp_find_order_with_client_order_id(perp_market.perp_market_index, client_order_id)
|
||||
.ok_or_else(|| error_msg!("could not find perp order with client order id {client_order_id} in perp order books"))?;
|
||||
let order_id = oo.id;
|
||||
let order_side_and_tree = oo.side_and_tree;
|
||||
|
||||
book.cancel_order(
|
||||
&mut account.borrow_mut(),
|
||||
order_id,
|
||||
side,
|
||||
order_side_and_tree,
|
||||
Some(ctx.accounts.account.key()),
|
||||
)?;
|
||||
|
||||
|
|
|
@ -15,8 +15,7 @@ pub struct PerpCloseMarket<'info> {
|
|||
#[account(
|
||||
mut,
|
||||
has_one = group,
|
||||
has_one = bids,
|
||||
has_one = asks,
|
||||
has_one = orderbook,
|
||||
has_one = event_queue,
|
||||
close = sol_destination
|
||||
)]
|
||||
|
@ -26,13 +25,7 @@ pub struct PerpCloseMarket<'info> {
|
|||
mut,
|
||||
close = sol_destination
|
||||
)]
|
||||
pub bids: AccountLoader<'info, BookSide>,
|
||||
|
||||
#[account(
|
||||
mut,
|
||||
close = sol_destination
|
||||
)]
|
||||
pub asks: AccountLoader<'info, BookSide>,
|
||||
pub orderbook: AccountLoader<'info, OrderBook>,
|
||||
|
||||
#[account(
|
||||
mut,
|
||||
|
|
|
@ -32,9 +32,7 @@ pub struct PerpCreateMarket<'info> {
|
|||
/// Accounts are initialised by client,
|
||||
/// anchor discriminator is set first when ix exits,
|
||||
#[account(zero)]
|
||||
pub bids: AccountLoader<'info, BookSide>,
|
||||
#[account(zero)]
|
||||
pub asks: AccountLoader<'info, BookSide>,
|
||||
pub orderbook: AccountLoader<'info, OrderBook>,
|
||||
#[account(zero)]
|
||||
pub event_queue: AccountLoader<'info, EventQueue>,
|
||||
|
||||
|
@ -92,8 +90,7 @@ pub fn perp_create_market(
|
|||
name: fill_from_str(&name)?,
|
||||
oracle: ctx.accounts.oracle.key(),
|
||||
oracle_config,
|
||||
bids: ctx.accounts.bids.key(),
|
||||
asks: ctx.accounts.asks.key(),
|
||||
orderbook: ctx.accounts.orderbook.key(),
|
||||
event_queue: ctx.accounts.event_queue.key(),
|
||||
quote_lot_size,
|
||||
base_lot_size,
|
||||
|
@ -119,6 +116,7 @@ pub fn perp_create_market(
|
|||
registration_time: Clock::get()?.unix_timestamp,
|
||||
padding1: Default::default(),
|
||||
padding2: Default::default(),
|
||||
padding3: Default::default(),
|
||||
fee_penalty,
|
||||
settle_fee_flat,
|
||||
settle_fee_amount_threshold,
|
||||
|
@ -126,11 +124,8 @@ pub fn perp_create_market(
|
|||
reserved: [0; 92],
|
||||
};
|
||||
|
||||
let mut bids = ctx.accounts.bids.load_init()?;
|
||||
bids.book_side_type = BookSideType::Bids;
|
||||
|
||||
let mut asks = ctx.accounts.asks.load_init()?;
|
||||
asks.book_side_type = BookSideType::Asks;
|
||||
let mut orderbook = ctx.accounts.orderbook.load_init()?;
|
||||
orderbook.init();
|
||||
|
||||
emit!(PerpMarketMetaDataLog {
|
||||
mango_group: ctx.accounts.group.key(),
|
||||
|
|
|
@ -14,14 +14,11 @@ pub struct PerpLiqForceCancelOrders<'info> {
|
|||
#[account(
|
||||
mut,
|
||||
has_one = group,
|
||||
has_one = bids,
|
||||
has_one = asks
|
||||
has_one = orderbook,
|
||||
)]
|
||||
pub perp_market: AccountLoader<'info, PerpMarket>,
|
||||
#[account(mut)]
|
||||
pub asks: AccountLoader<'info, BookSide>,
|
||||
#[account(mut)]
|
||||
pub bids: AccountLoader<'info, BookSide>,
|
||||
pub orderbook: AccountLoader<'info, OrderBook>,
|
||||
|
||||
/// CHECK: Oracle can have different account types, constrained by address in perp_market
|
||||
pub oracle: UncheckedAccount<'info>,
|
||||
|
@ -68,9 +65,7 @@ pub fn perp_liq_force_cancel_orders(
|
|||
//
|
||||
{
|
||||
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
|
||||
let bids = ctx.accounts.bids.load_mut()?;
|
||||
let asks = ctx.accounts.asks.load_mut()?;
|
||||
let mut book = Book::new(bids, asks);
|
||||
let mut book = ctx.accounts.orderbook.load_mut()?;
|
||||
|
||||
book.cancel_all_orders(&mut account.borrow_mut(), &mut perp_market, limit, None)?;
|
||||
|
||||
|
|
|
@ -4,8 +4,8 @@ use crate::accounts_zerocopy::*;
|
|||
use crate::error::*;
|
||||
use crate::state::MangoAccount;
|
||||
use crate::state::{
|
||||
new_fixed_order_account_retriever, new_health_cache, AccountLoaderDynamic, Book, BookSide,
|
||||
EventQueue, Group, OrderType, PerpMarket, Side,
|
||||
new_fixed_order_account_retriever, new_health_cache, AccountLoaderDynamic, EventQueue, Group,
|
||||
Order, OrderBook, PerpMarket,
|
||||
};
|
||||
|
||||
#[derive(Accounts)]
|
||||
|
@ -19,16 +19,13 @@ pub struct PerpPlaceOrder<'info> {
|
|||
#[account(
|
||||
mut,
|
||||
has_one = group,
|
||||
has_one = bids,
|
||||
has_one = asks,
|
||||
has_one = orderbook,
|
||||
has_one = event_queue,
|
||||
has_one = oracle,
|
||||
)]
|
||||
pub perp_market: AccountLoader<'info, PerpMarket>,
|
||||
#[account(mut)]
|
||||
pub asks: AccountLoader<'info, BookSide>,
|
||||
#[account(mut)]
|
||||
pub bids: AccountLoader<'info, BookSide>,
|
||||
pub orderbook: AccountLoader<'info, OrderBook>,
|
||||
#[account(mut)]
|
||||
pub event_queue: AccountLoader<'info, EventQueue>,
|
||||
|
||||
|
@ -38,43 +35,10 @@ pub struct PerpPlaceOrder<'info> {
|
|||
|
||||
// TODO
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn perp_place_order(
|
||||
ctx: Context<PerpPlaceOrder>,
|
||||
side: Side,
|
||||
pub fn perp_place_order(ctx: Context<PerpPlaceOrder>, order: Order, limit: u8) -> Result<()> {
|
||||
require_gte!(order.max_base_lots, 0);
|
||||
require_gte!(order.max_quote_lots, 0);
|
||||
|
||||
// Price in quote lots per base lots.
|
||||
//
|
||||
// Effect is based on order type, it's usually
|
||||
// - fill orders on the book up to this price or
|
||||
// - place an order on the book at this price.
|
||||
//
|
||||
// Ignored for Market orders and potentially adjusted for PostOnlySlide orders.
|
||||
price_lots: i64,
|
||||
|
||||
// Max base lots to buy/sell.
|
||||
max_base_lots: i64,
|
||||
|
||||
// Max quote lots to pay/receive (not taking fees into account).
|
||||
max_quote_lots: i64,
|
||||
|
||||
// Arbitrary user-controlled order id.
|
||||
client_order_id: u64,
|
||||
|
||||
order_type: OrderType,
|
||||
|
||||
// Timestamp of when order expires
|
||||
//
|
||||
// Send 0 if you want the order to never expire.
|
||||
// Timestamps in the past mean the instruction is skipped.
|
||||
// Timestamps in the future are reduced to now + 255s.
|
||||
expiry_timestamp: u64,
|
||||
|
||||
// Maximum number of orders from the book to fill.
|
||||
//
|
||||
// Use this to limit compute used during order matching.
|
||||
// When the limit is reached, processing stops and the instruction succeeds.
|
||||
limit: u8,
|
||||
) -> Result<()> {
|
||||
let mut account = ctx.accounts.account.load_mut()?;
|
||||
require!(
|
||||
account.fixed.is_owner_or_delegate(ctx.accounts.owner.key()),
|
||||
|
@ -111,9 +75,7 @@ pub fn perp_place_order(
|
|||
};
|
||||
|
||||
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
|
||||
let bids = ctx.accounts.bids.load_mut()?;
|
||||
let asks = ctx.accounts.asks.load_mut()?;
|
||||
let mut book = Book::new(bids, asks);
|
||||
let mut book = ctx.accounts.orderbook.load_mut()?;
|
||||
|
||||
let mut event_queue = ctx.accounts.event_queue.load_mut()?;
|
||||
|
||||
|
@ -121,35 +83,16 @@ pub fn perp_place_order(
|
|||
perp_market.oracle_price(&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?)?;
|
||||
|
||||
let now_ts = Clock::get()?.unix_timestamp as u64;
|
||||
let time_in_force = if expiry_timestamp != 0 {
|
||||
// If expiry is far in the future, clamp to 255 seconds
|
||||
let tif = expiry_timestamp.saturating_sub(now_ts).min(255);
|
||||
if tif == 0 {
|
||||
// If expiry is in the past, ignore the order
|
||||
msg!("Order is already expired");
|
||||
return Ok(());
|
||||
}
|
||||
tif as u8
|
||||
} else {
|
||||
// Never expire
|
||||
0
|
||||
};
|
||||
|
||||
// TODO reduce_only based on event queue
|
||||
|
||||
book.new_order(
|
||||
side,
|
||||
order,
|
||||
&mut perp_market,
|
||||
&mut event_queue,
|
||||
oracle_price,
|
||||
&mut account.borrow_mut(),
|
||||
&account_pk,
|
||||
price_lots,
|
||||
max_base_lots,
|
||||
max_quote_lots,
|
||||
order_type,
|
||||
time_in_force,
|
||||
client_order_id,
|
||||
now_ts,
|
||||
limit,
|
||||
)?;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use anchor_lang::prelude::*;
|
||||
|
||||
use crate::accounts_zerocopy::*;
|
||||
use crate::state::{Book, BookSide, Group, PerpMarket};
|
||||
use crate::state::{Group, OrderBook, PerpMarket};
|
||||
|
||||
use crate::logs::PerpUpdateFundingLog;
|
||||
|
||||
|
@ -11,16 +11,13 @@ pub struct PerpUpdateFunding<'info> {
|
|||
|
||||
#[account(
|
||||
mut,
|
||||
has_one = bids,
|
||||
has_one = asks,
|
||||
has_one = orderbook,
|
||||
has_one = oracle,
|
||||
constraint = perp_market.load()?.group.key() == group.key(),
|
||||
)]
|
||||
pub perp_market: AccountLoader<'info, PerpMarket>,
|
||||
#[account(mut)]
|
||||
pub asks: AccountLoader<'info, BookSide>,
|
||||
#[account(mut)]
|
||||
pub bids: AccountLoader<'info, BookSide>,
|
||||
pub orderbook: AccountLoader<'info, OrderBook>,
|
||||
|
||||
/// CHECK: The oracle can be one of several different account types and the pubkey is checked above
|
||||
pub oracle: UncheckedAccount<'info>,
|
||||
|
@ -30,9 +27,7 @@ pub fn perp_update_funding(ctx: Context<PerpUpdateFunding>) -> Result<()> {
|
|||
let now_ts = Clock::get()?.unix_timestamp;
|
||||
|
||||
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
|
||||
let bids = ctx.accounts.bids.load_mut()?;
|
||||
let asks = ctx.accounts.asks.load_mut()?;
|
||||
let book = Book::new(bids, asks);
|
||||
let book = ctx.accounts.orderbook.load_mut()?;
|
||||
|
||||
let oracle_price =
|
||||
perp_market.oracle_price(&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?)?;
|
||||
|
|
|
@ -3,6 +3,7 @@ use fixed::types::I80F48;
|
|||
#[macro_use]
|
||||
pub mod util;
|
||||
|
||||
extern crate core;
|
||||
extern crate static_assertions;
|
||||
|
||||
use anchor_lang::prelude::*;
|
||||
|
@ -19,7 +20,7 @@ pub mod serum3_cpi;
|
|||
pub mod state;
|
||||
pub mod types;
|
||||
|
||||
use state::{OracleConfig, OrderType, PerpMarketIndex, Serum3MarketIndex, Side, TokenIndex};
|
||||
use state::{OracleConfig, PerpMarketIndex, PlaceOrderType, Serum3MarketIndex, Side, TokenIndex};
|
||||
|
||||
declare_id!("m43thNJ58XCjL798ZSq6JGAG1BnWskhdq5or6kcnfsD");
|
||||
|
||||
|
@ -485,8 +486,6 @@ pub mod mango_v4 {
|
|||
instructions::perp_close_market(ctx)
|
||||
}
|
||||
|
||||
// TODO perp_change_perp_market_params
|
||||
|
||||
pub fn perp_deactivate_position(ctx: Context<PerpDeactivatePosition>) -> Result<()> {
|
||||
instructions::perp_deactivate_position(ctx)
|
||||
}
|
||||
|
@ -495,28 +494,118 @@ pub mod mango_v4 {
|
|||
pub fn perp_place_order(
|
||||
ctx: Context<PerpPlaceOrder>,
|
||||
side: Side,
|
||||
|
||||
// The price in lots (quote lots per base lots)
|
||||
// - fill orders on the book up to this price or
|
||||
// - place an order on the book at this price.
|
||||
// - ignored for Market orders and potentially adjusted for PostOnlySlide orders.
|
||||
price_lots: i64,
|
||||
|
||||
max_base_lots: i64,
|
||||
max_quote_lots: i64,
|
||||
client_order_id: u64,
|
||||
order_type: OrderType,
|
||||
order_type: PlaceOrderType,
|
||||
|
||||
// Timestamp of when order expires
|
||||
//
|
||||
// Send 0 if you want the order to never expire.
|
||||
// Timestamps in the past mean the instruction is skipped.
|
||||
// Timestamps in the future are reduced to now + 255s.
|
||||
expiry_timestamp: u64,
|
||||
|
||||
// Maximum number of orders from the book to fill.
|
||||
//
|
||||
// Use this to limit compute used during order matching.
|
||||
// When the limit is reached, processing stops and the instruction succeeds.
|
||||
limit: u8,
|
||||
) -> Result<()> {
|
||||
instructions::perp_place_order(
|
||||
ctx,
|
||||
require_gte!(price_lots, 0);
|
||||
|
||||
use crate::state::{Order, OrderParams};
|
||||
let time_in_force = match Order::tif_from_expiry(expiry_timestamp) {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
msg!("Order is already expired");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
let order = Order {
|
||||
side,
|
||||
price_lots,
|
||||
max_base_lots,
|
||||
max_quote_lots,
|
||||
client_order_id,
|
||||
order_type,
|
||||
expiry_timestamp,
|
||||
limit,
|
||||
)
|
||||
time_in_force,
|
||||
params: match order_type {
|
||||
PlaceOrderType::Market => OrderParams::Market,
|
||||
PlaceOrderType::ImmediateOrCancel => OrderParams::ImmediateOrCancel { price_lots },
|
||||
_ => OrderParams::Fixed {
|
||||
price_lots,
|
||||
order_type: order_type.to_post_order_type()?,
|
||||
},
|
||||
},
|
||||
};
|
||||
instructions::perp_place_order(ctx, order, limit)
|
||||
}
|
||||
|
||||
pub fn perp_cancel_order(ctx: Context<PerpCancelOrder>, order_id: i128) -> Result<()> {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn perp_place_order_pegged(
|
||||
ctx: Context<PerpPlaceOrder>,
|
||||
side: Side,
|
||||
|
||||
// The adjustment from the oracle price, in lots (quote lots per base lots).
|
||||
// Orders on the book may be filled at oracle + adjustment (depends on order type).
|
||||
price_offset_lots: i64,
|
||||
|
||||
// The limit at which the pegged order shall expire.
|
||||
// May be -1 to denote no peg limit.
|
||||
//
|
||||
// Example: An bid pegged to -20 with peg_limit 100 would expire if the oracle hits 121.
|
||||
peg_limit: i64,
|
||||
|
||||
max_base_lots: i64,
|
||||
max_quote_lots: i64,
|
||||
client_order_id: u64,
|
||||
order_type: PlaceOrderType,
|
||||
|
||||
// Timestamp of when order expires
|
||||
//
|
||||
// Send 0 if you want the order to never expire.
|
||||
// Timestamps in the past mean the instruction is skipped.
|
||||
// Timestamps in the future are reduced to now + 255s.
|
||||
expiry_timestamp: u64,
|
||||
|
||||
// Maximum number of orders from the book to fill.
|
||||
//
|
||||
// Use this to limit compute used during order matching.
|
||||
// When the limit is reached, processing stops and the instruction succeeds.
|
||||
limit: u8,
|
||||
) -> Result<()> {
|
||||
require_gte!(peg_limit, -1);
|
||||
|
||||
use crate::state::{Order, OrderParams};
|
||||
let time_in_force = match Order::tif_from_expiry(expiry_timestamp) {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
msg!("Order is already expired");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
let order = Order {
|
||||
side,
|
||||
max_base_lots,
|
||||
max_quote_lots,
|
||||
client_order_id,
|
||||
time_in_force,
|
||||
params: OrderParams::OraclePegged {
|
||||
price_offset_lots,
|
||||
order_type: order_type.to_post_order_type()?,
|
||||
peg_limit,
|
||||
},
|
||||
};
|
||||
instructions::perp_place_order(ctx, order, limit)
|
||||
}
|
||||
|
||||
pub fn perp_cancel_order(ctx: Context<PerpCancelOrder>, order_id: u128) -> Result<()> {
|
||||
instructions::perp_cancel_order(ctx, order_id)
|
||||
}
|
||||
|
||||
|
|
|
@ -99,7 +99,7 @@ pub struct FillLog {
|
|||
pub seq_num: u64, // note: usize same as u64
|
||||
|
||||
pub maker: Pubkey,
|
||||
pub maker_order_id: i128,
|
||||
pub maker_order_id: u128,
|
||||
pub maker_client_order_id: u64,
|
||||
pub maker_fee: i128,
|
||||
|
||||
|
@ -107,7 +107,7 @@ pub struct FillLog {
|
|||
pub maker_timestamp: u64,
|
||||
|
||||
pub taker: Pubkey,
|
||||
pub taker_order_id: i128,
|
||||
pub taker_order_id: u128,
|
||||
pub taker_client_order_id: u64,
|
||||
pub taker_fee: i128,
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ use crate::state::{
|
|||
use crate::util::checked_math as cm;
|
||||
|
||||
#[cfg(feature = "client")]
|
||||
use crate::state::orderbook::order_type::Side as PerpOrderSide;
|
||||
use crate::state::orderbook::Side as PerpOrderSide;
|
||||
|
||||
use super::MangoAccountRef;
|
||||
|
||||
|
|
|
@ -13,17 +13,18 @@ use crate::error::MangoError;
|
|||
use crate::error_msg;
|
||||
|
||||
use super::dynamic_account::*;
|
||||
use super::BookSideOrderTree;
|
||||
use super::FillEvent;
|
||||
use super::LeafNode;
|
||||
use super::PerpMarket;
|
||||
use super::PerpMarketIndex;
|
||||
use super::PerpOpenOrder;
|
||||
use super::Serum3MarketIndex;
|
||||
use super::Side;
|
||||
use super::TokenIndex;
|
||||
use super::FREE_ORDER_SLOT;
|
||||
use super::{HealthCache, HealthType};
|
||||
use super::{PerpPosition, Serum3Orders, TokenPosition};
|
||||
use super::{Side, SideAndOrderTree};
|
||||
use crate::logs::{DeactivatePerpPositionLog, DeactivateTokenPositionLog};
|
||||
use checked_math as cm;
|
||||
|
||||
|
@ -500,7 +501,7 @@ impl<
|
|||
|
||||
pub fn perp_next_order_slot(&self) -> Result<usize> {
|
||||
self.all_perp_orders()
|
||||
.position(|&oo| oo.order_market == FREE_ORDER_SLOT)
|
||||
.position(|&oo| oo.market == FREE_ORDER_SLOT)
|
||||
.ok_or_else(|| error_msg!("no free perp order index"))
|
||||
}
|
||||
|
||||
|
@ -508,23 +509,23 @@ impl<
|
|||
&self,
|
||||
market_index: PerpMarketIndex,
|
||||
client_order_id: u64,
|
||||
) -> Option<(i128, Side)> {
|
||||
) -> Option<&PerpOpenOrder> {
|
||||
for oo in self.all_perp_orders() {
|
||||
if oo.order_market == market_index && oo.client_order_id == client_order_id {
|
||||
return Some((oo.order_id, oo.order_side));
|
||||
if oo.market == market_index && oo.client_id == client_order_id {
|
||||
return Some(&oo);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn perp_find_order_side(
|
||||
pub fn perp_find_order_with_order_id(
|
||||
&self,
|
||||
market_index: PerpMarketIndex,
|
||||
order_id: i128,
|
||||
) -> Option<Side> {
|
||||
order_id: u128,
|
||||
) -> Option<&PerpOpenOrder> {
|
||||
for oo in self.all_perp_orders() {
|
||||
if oo.order_market == market_index && oo.order_id == order_id {
|
||||
return Some(oo.order_side);
|
||||
if oo.market == market_index && oo.id == order_id {
|
||||
return Some(&oo);
|
||||
}
|
||||
}
|
||||
None
|
||||
|
@ -796,6 +797,7 @@ impl<
|
|||
&mut self,
|
||||
perp_market_index: PerpMarketIndex,
|
||||
side: Side,
|
||||
order_tree: BookSideOrderTree,
|
||||
order: &LeafNode,
|
||||
) -> Result<()> {
|
||||
let mut perp_account = self.perp_position_mut(perp_market_index)?;
|
||||
|
@ -810,19 +812,19 @@ impl<
|
|||
let slot = order.owner_slot as usize;
|
||||
|
||||
let mut oo = self.perp_order_mut_by_raw_index(slot);
|
||||
oo.order_market = perp_market_index;
|
||||
oo.order_side = side;
|
||||
oo.order_id = order.key;
|
||||
oo.client_order_id = order.client_order_id;
|
||||
oo.market = perp_market_index;
|
||||
oo.side_and_tree = SideAndOrderTree::new(side, order_tree);
|
||||
oo.id = order.key;
|
||||
oo.client_id = order.client_order_id;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove_perp_order(&mut self, slot: usize, quantity: i64) -> Result<()> {
|
||||
{
|
||||
let oo = self.perp_order_mut_by_raw_index(slot);
|
||||
require_neq!(oo.order_market, FREE_ORDER_SLOT);
|
||||
let order_side = oo.order_side;
|
||||
let perp_market_index = oo.order_market;
|
||||
require_neq!(oo.market, FREE_ORDER_SLOT);
|
||||
let order_side = oo.side_and_tree.side();
|
||||
let perp_market_index = oo.market;
|
||||
let perp_account = self.perp_position_mut(perp_market_index)?;
|
||||
|
||||
// accounting
|
||||
|
@ -838,10 +840,10 @@ impl<
|
|||
|
||||
// release space
|
||||
let oo = self.perp_order_mut_by_raw_index(slot);
|
||||
oo.order_market = FREE_ORDER_SLOT;
|
||||
oo.order_side = Side::Bid;
|
||||
oo.order_id = 0i128;
|
||||
oo.client_order_id = 0u64;
|
||||
oo.market = FREE_ORDER_SLOT;
|
||||
oo.side_and_tree = SideAndOrderTree::BidFixed;
|
||||
oo.id = 0;
|
||||
oo.client_id = 0;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
@ -395,24 +395,24 @@ impl PerpPosition {
|
|||
#[zero_copy]
|
||||
#[derive(AnchorSerialize, AnchorDeserialize, Debug)]
|
||||
pub struct PerpOpenOrder {
|
||||
pub order_side: Side, // TODO: storing enums isn't POD
|
||||
pub side_and_tree: SideAndOrderTree, // TODO: storing enums isn't POD
|
||||
pub padding1: [u8; 1],
|
||||
pub order_market: PerpMarketIndex,
|
||||
pub market: PerpMarketIndex,
|
||||
pub padding2: [u8; 4],
|
||||
pub client_order_id: u64,
|
||||
pub order_id: i128,
|
||||
pub client_id: u64,
|
||||
pub id: u128,
|
||||
pub reserved: [u8; 64],
|
||||
}
|
||||
|
||||
impl Default for PerpOpenOrder {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
order_side: Side::Bid,
|
||||
side_and_tree: SideAndOrderTree::BidFixed,
|
||||
padding1: Default::default(),
|
||||
order_market: FREE_ORDER_SLOT,
|
||||
market: FREE_ORDER_SLOT,
|
||||
padding2: Default::default(),
|
||||
client_order_id: 0,
|
||||
order_id: 0,
|
||||
client_id: 0,
|
||||
id: 0,
|
||||
reserved: [0; 64],
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,250 +1,163 @@
|
|||
use std::cell::RefMut;
|
||||
|
||||
use crate::accounts_zerocopy::*;
|
||||
use crate::state::MangoAccountRefMut;
|
||||
use crate::{
|
||||
error::*,
|
||||
state::{
|
||||
orderbook::{bookside::BookSide, nodes::LeafNode},
|
||||
EventQueue, PerpMarket, FREE_ORDER_SLOT,
|
||||
},
|
||||
state::{orderbook::bookside::*, EventQueue, PerpMarket, FREE_ORDER_SLOT},
|
||||
};
|
||||
use anchor_lang::prelude::*;
|
||||
use bytemuck::cast;
|
||||
use fixed::types::I80F48;
|
||||
use static_assertions::const_assert_eq;
|
||||
|
||||
use super::{
|
||||
nodes::NodeHandle,
|
||||
order_type::{OrderType, Side},
|
||||
FillEvent, OutEvent,
|
||||
};
|
||||
use super::*;
|
||||
use crate::util::checked_math as cm;
|
||||
|
||||
/// Drop at most this many expired orders from a BookSide when trying to match orders.
|
||||
/// This exists as a guard against excessive compute use.
|
||||
const DROP_EXPIRED_ORDER_LIMIT: usize = 5;
|
||||
|
||||
/// The implicit limit price to use for market orders
|
||||
fn market_order_limit_for_side(side: Side) -> i64 {
|
||||
match side {
|
||||
Side::Bid => i64::MAX,
|
||||
Side::Ask => 1,
|
||||
}
|
||||
#[account(zero_copy)]
|
||||
pub struct OrderBook {
|
||||
pub bids: BookSide,
|
||||
pub asks: BookSide,
|
||||
}
|
||||
const_assert_eq!(
|
||||
std::mem::size_of::<OrderBook>(),
|
||||
2 * std::mem::size_of::<BookSide>()
|
||||
);
|
||||
const_assert_eq!(std::mem::size_of::<OrderBook>() % 8, 0);
|
||||
|
||||
/// The limit to use for PostOnlySlide orders: the tinyest bit better than
|
||||
/// the best opposing order
|
||||
fn post_only_slide_limit(side: Side, best_other_side: i64, limit: i64) -> i64 {
|
||||
match side {
|
||||
Side::Bid => limit.min(cm!(best_other_side - 1)),
|
||||
Side::Ask => limit.max(cm!(best_other_side + 1)),
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Book<'a> {
|
||||
pub bids: RefMut<'a, BookSide>, // todo: why refmut?
|
||||
pub asks: RefMut<'a, BookSide>,
|
||||
}
|
||||
|
||||
impl<'a> Book<'a> {
|
||||
pub fn new(bids: RefMut<'a, BookSide>, asks: RefMut<'a, BookSide>) -> Self {
|
||||
Self { bids, asks }
|
||||
impl OrderBook {
|
||||
pub fn init(&mut self) {
|
||||
self.bids.fixed.order_tree_type = OrderTreeType::Bids;
|
||||
self.bids.oracle_pegged.order_tree_type = OrderTreeType::Bids;
|
||||
self.asks.fixed.order_tree_type = OrderTreeType::Asks;
|
||||
self.asks.oracle_pegged.order_tree_type = OrderTreeType::Asks;
|
||||
}
|
||||
|
||||
pub fn load_mut(
|
||||
bids_ai: &'a AccountInfo,
|
||||
asks_ai: &'a AccountInfo,
|
||||
perp_market: &PerpMarket,
|
||||
) -> std::result::Result<Self, Error> {
|
||||
require!(bids_ai.key == &perp_market.bids, MangoError::SomeError);
|
||||
require!(asks_ai.key == &perp_market.asks, MangoError::SomeError);
|
||||
Ok(Self::new(
|
||||
bids_ai.load_mut::<BookSide>()?,
|
||||
asks_ai.load_mut::<BookSide>()?,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn bookside(&mut self, side: Side) -> &mut BookSide {
|
||||
pub fn bookside_mut(&mut self, side: Side) -> &mut BookSide {
|
||||
match side {
|
||||
Side::Bid => &mut self.bids,
|
||||
Side::Ask => &mut self.asks,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns best valid bid
|
||||
pub fn best_bid_price(&self, now_ts: u64) -> Option<i64> {
|
||||
Some(self.bids.iter_valid(now_ts).next()?.1.price())
|
||||
}
|
||||
|
||||
/// Returns best valid ask
|
||||
pub fn best_ask_price(&self, now_ts: u64) -> Option<i64> {
|
||||
Some(self.asks.iter_valid(now_ts).next()?.1.price())
|
||||
}
|
||||
|
||||
pub fn best_price(&self, now_ts: u64, side: Side) -> Option<i64> {
|
||||
pub fn bookside(&self, side: Side) -> &BookSide {
|
||||
match side {
|
||||
Side::Bid => self.best_bid_price(now_ts),
|
||||
Side::Ask => self.best_ask_price(now_ts),
|
||||
Side::Bid => &self.bids,
|
||||
Side::Ask => &self.asks,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the quantity of valid bids above and including the price
|
||||
pub fn bids_size_above(&self, price: i64, max_depth: i64, now_ts: u64) -> i64 {
|
||||
let mut sum: i64 = 0;
|
||||
for (_, bid) in self.bids.iter_valid(now_ts) {
|
||||
if price > bid.price() || sum >= max_depth {
|
||||
break;
|
||||
}
|
||||
sum = sum.checked_add(bid.quantity).unwrap();
|
||||
}
|
||||
sum.min(max_depth)
|
||||
pub fn best_price(&self, now_ts: u64, oracle_price_lots: i64, side: Side) -> Option<i64> {
|
||||
Some(
|
||||
self.bookside(side)
|
||||
.iter_valid(now_ts, oracle_price_lots)
|
||||
.next()?
|
||||
.price_lots,
|
||||
)
|
||||
}
|
||||
|
||||
/// Walk up the book `quantity` units and return the price at that level. If `quantity` units
|
||||
/// not on book, return None
|
||||
pub fn impact_price(&self, side: Side, quantity: i64, now_ts: u64) -> Option<i64> {
|
||||
pub fn impact_price(
|
||||
&self,
|
||||
side: Side,
|
||||
quantity: i64,
|
||||
now_ts: u64,
|
||||
oracle_price_lots: i64,
|
||||
) -> Option<i64> {
|
||||
let mut sum: i64 = 0;
|
||||
let book_side = match side {
|
||||
Side::Bid => self.bids.iter_valid(now_ts),
|
||||
Side::Ask => self.asks.iter_valid(now_ts),
|
||||
};
|
||||
for (_, order) in book_side {
|
||||
sum = sum.checked_add(order.quantity).unwrap();
|
||||
let bookside = self.bookside(side);
|
||||
let iter = bookside.iter_valid(now_ts, oracle_price_lots);
|
||||
for order in iter {
|
||||
cm!(sum += order.node.quantity);
|
||||
if sum >= quantity {
|
||||
return Some(order.price());
|
||||
return Some(order.price_lots);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Get the quantity of valid asks below and including the price
|
||||
pub fn asks_size_below(&self, price: i64, max_depth: i64, now_ts: u64) -> i64 {
|
||||
let mut s = 0;
|
||||
for (_, ask) in self.asks.iter_valid(now_ts) {
|
||||
if price < ask.price() || s >= max_depth {
|
||||
break;
|
||||
}
|
||||
s += ask.quantity;
|
||||
}
|
||||
s.min(max_depth)
|
||||
}
|
||||
/// Get the quantity of valid bids above this order id. Will return full size of book if order id not found
|
||||
pub fn bids_size_above_order(&self, order_id: i128, max_depth: i64, now_ts: u64) -> i64 {
|
||||
let mut s = 0;
|
||||
for (_, bid) in self.bids.iter_valid(now_ts) {
|
||||
if bid.key == order_id || s >= max_depth {
|
||||
break;
|
||||
}
|
||||
s += bid.quantity;
|
||||
}
|
||||
s.min(max_depth)
|
||||
}
|
||||
|
||||
/// Get the quantity of valid asks above this order id. Will return full size of book if order id not found
|
||||
pub fn asks_size_below_order(&self, order_id: i128, max_depth: i64, now_ts: u64) -> i64 {
|
||||
let mut s = 0;
|
||||
for (_, ask) in self.asks.iter_valid(now_ts) {
|
||||
if ask.key == order_id || s >= max_depth {
|
||||
break;
|
||||
}
|
||||
s += ask.quantity;
|
||||
}
|
||||
s.min(max_depth)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new_order(
|
||||
&mut self,
|
||||
side: Side,
|
||||
order: Order,
|
||||
perp_market: &mut PerpMarket,
|
||||
event_queue: &mut EventQueue,
|
||||
oracle_price: I80F48,
|
||||
mango_account: &mut MangoAccountRefMut,
|
||||
mango_account_pk: &Pubkey,
|
||||
price_lots: i64,
|
||||
max_base_lots: i64,
|
||||
max_quote_lots: i64,
|
||||
order_type: OrderType,
|
||||
time_in_force: u8,
|
||||
client_order_id: u64,
|
||||
now_ts: u64,
|
||||
mut limit: u8,
|
||||
) -> std::result::Result<(), Error> {
|
||||
let side = order.side;
|
||||
let other_side = side.invert_side();
|
||||
let market = perp_market;
|
||||
let (post_only, mut post_allowed, price_lots) = match order_type {
|
||||
OrderType::Limit => (false, true, price_lots),
|
||||
OrderType::ImmediateOrCancel => (false, false, price_lots),
|
||||
OrderType::PostOnly => (true, true, price_lots),
|
||||
OrderType::Market => (false, false, market_order_limit_for_side(side)),
|
||||
OrderType::PostOnlySlide => {
|
||||
let price = if let Some(best_other_price) = self.best_price(now_ts, other_side) {
|
||||
post_only_slide_limit(side, best_other_price, price_lots)
|
||||
} else {
|
||||
price_lots
|
||||
};
|
||||
(true, true, price)
|
||||
}
|
||||
};
|
||||
let oracle_price_lots = market.native_price_to_lot(oracle_price);
|
||||
let post_only = order.is_post_only();
|
||||
let mut post_target = order.post_target();
|
||||
let (price_lots, price_data) = order.price(now_ts, oracle_price_lots, self)?;
|
||||
|
||||
if post_allowed {
|
||||
if post_target.is_some() {
|
||||
// price limit check computed lazily to save CU on average
|
||||
let native_price = market.lot_to_native_price(price_lots);
|
||||
if !market.inside_price_limit(side, native_price, oracle_price) {
|
||||
msg!("Posting on book disallowed due to price limits");
|
||||
post_allowed = false;
|
||||
post_target = None;
|
||||
}
|
||||
}
|
||||
|
||||
// generate new order id
|
||||
let order_id = market.gen_order_id(side, price_lots);
|
||||
let order_id = market.gen_order_id(side, price_data);
|
||||
|
||||
// Iterate through book and match against this new order.
|
||||
//
|
||||
// Any changes to matching orders on the other side of the book are collected in
|
||||
// matched_changes/matched_deletes and then applied after this loop.
|
||||
let mut remaining_base_lots = max_base_lots;
|
||||
let mut remaining_quote_lots = max_quote_lots;
|
||||
let mut matched_order_changes: Vec<(NodeHandle, i64)> = vec![];
|
||||
let mut matched_order_deletes: Vec<i128> = vec![];
|
||||
let mut remaining_base_lots = order.max_base_lots;
|
||||
let mut remaining_quote_lots = order.max_quote_lots;
|
||||
let mut matched_order_changes: Vec<(BookSideOrderHandle, i64)> = vec![];
|
||||
let mut matched_order_deletes: Vec<(BookSideOrderTree, u128)> = vec![];
|
||||
let mut number_of_dropped_expired_orders = 0;
|
||||
let opposing_bookside = self.bookside(other_side);
|
||||
for (best_opposing_h, best_opposing) in opposing_bookside.iter_all_including_invalid() {
|
||||
if !best_opposing.is_valid(now_ts) {
|
||||
let opposing_bookside = self.bookside_mut(other_side);
|
||||
for best_opposing in opposing_bookside.iter_all_including_invalid(now_ts, oracle_price_lots)
|
||||
{
|
||||
if !best_opposing.is_valid {
|
||||
// Remove the order from the book unless we've done that enough
|
||||
if number_of_dropped_expired_orders < DROP_EXPIRED_ORDER_LIMIT {
|
||||
number_of_dropped_expired_orders += 1;
|
||||
let event = OutEvent::new(
|
||||
other_side,
|
||||
best_opposing.owner_slot,
|
||||
best_opposing.node.owner_slot,
|
||||
now_ts,
|
||||
event_queue.header.seq_num,
|
||||
best_opposing.owner,
|
||||
best_opposing.quantity,
|
||||
best_opposing.node.owner,
|
||||
best_opposing.node.quantity,
|
||||
);
|
||||
event_queue.push_back(cast(event)).unwrap();
|
||||
matched_order_deletes.push(best_opposing.key);
|
||||
matched_order_deletes
|
||||
.push((best_opposing.handle.order_tree, best_opposing.node.key));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
let best_opposing_price = best_opposing.price();
|
||||
let best_opposing_price = best_opposing.price_lots;
|
||||
|
||||
if !side.is_price_within_limit(best_opposing_price, price_lots) {
|
||||
break;
|
||||
} else if post_only {
|
||||
msg!("Order could not be placed due to PostOnly");
|
||||
post_allowed = false;
|
||||
post_target = None;
|
||||
break; // return silently to not fail other instructions in tx
|
||||
} else if limit == 0 {
|
||||
msg!("Order matching limit reached");
|
||||
post_allowed = false;
|
||||
post_target = None;
|
||||
break;
|
||||
}
|
||||
|
||||
let max_match_by_quote = remaining_quote_lots / best_opposing_price;
|
||||
let match_base_lots = remaining_base_lots
|
||||
.min(best_opposing.quantity)
|
||||
.min(best_opposing.node.quantity)
|
||||
.min(max_match_by_quote);
|
||||
let done =
|
||||
match_base_lots == max_match_by_quote || match_base_lots == remaining_base_lots;
|
||||
|
@ -253,12 +166,13 @@ impl<'a> Book<'a> {
|
|||
cm!(remaining_base_lots -= match_base_lots);
|
||||
cm!(remaining_quote_lots -= match_quote_lots);
|
||||
|
||||
let new_best_opposing_quantity = cm!(best_opposing.quantity - match_base_lots);
|
||||
let new_best_opposing_quantity = cm!(best_opposing.node.quantity - match_base_lots);
|
||||
let maker_out = new_best_opposing_quantity == 0;
|
||||
if maker_out {
|
||||
matched_order_deletes.push(best_opposing.key);
|
||||
matched_order_deletes
|
||||
.push((best_opposing.handle.order_tree, best_opposing.node.key));
|
||||
} else {
|
||||
matched_order_changes.push((best_opposing_h, new_best_opposing_quantity));
|
||||
matched_order_changes.push((best_opposing.handle, new_best_opposing_quantity));
|
||||
}
|
||||
|
||||
// Record the taker trade in the account already, even though it will only be
|
||||
|
@ -269,17 +183,17 @@ impl<'a> Book<'a> {
|
|||
let fill = FillEvent::new(
|
||||
side,
|
||||
maker_out,
|
||||
best_opposing.owner_slot,
|
||||
best_opposing.node.owner_slot,
|
||||
now_ts,
|
||||
event_queue.header.seq_num,
|
||||
best_opposing.owner,
|
||||
best_opposing.key,
|
||||
best_opposing.client_order_id,
|
||||
best_opposing.node.owner,
|
||||
best_opposing.node.key,
|
||||
best_opposing.node.client_order_id,
|
||||
market.maker_fee,
|
||||
best_opposing.timestamp,
|
||||
best_opposing.node.timestamp,
|
||||
*mango_account_pk,
|
||||
order_id,
|
||||
client_order_id,
|
||||
order.client_order_id,
|
||||
market.taker_fee,
|
||||
best_opposing_price,
|
||||
match_base_lots,
|
||||
|
@ -291,7 +205,7 @@ impl<'a> Book<'a> {
|
|||
break;
|
||||
}
|
||||
}
|
||||
let total_quote_lots_taken = cm!(max_quote_lots - remaining_quote_lots);
|
||||
let total_quote_lots_taken = cm!(order.max_quote_lots - remaining_quote_lots);
|
||||
|
||||
// Apply changes to matched asks (handles invalidate on delete!)
|
||||
for (handle, new_quantity) in matched_order_changes {
|
||||
|
@ -302,17 +216,21 @@ impl<'a> Book<'a> {
|
|||
.unwrap()
|
||||
.quantity = new_quantity;
|
||||
}
|
||||
for key in matched_order_deletes {
|
||||
let _removed_leaf = opposing_bookside.remove_by_key(key).unwrap();
|
||||
for (component, key) in matched_order_deletes {
|
||||
let _removed_leaf = opposing_bookside.remove_by_key(component, key).unwrap();
|
||||
}
|
||||
|
||||
// If there are still quantity unmatched, place on the book
|
||||
let book_base_quantity = remaining_base_lots.min(remaining_quote_lots / price_lots);
|
||||
msg!("{:?}", post_allowed);
|
||||
if post_allowed && book_base_quantity > 0 {
|
||||
if book_base_quantity <= 0 {
|
||||
post_target = None;
|
||||
}
|
||||
if let Some(order_tree_target) = post_target {
|
||||
let bookside = self.bookside_mut(side);
|
||||
let order_tree = bookside.orders_mut(order_tree_target);
|
||||
|
||||
// Drop an expired order if possible
|
||||
let bookside = self.bookside(side);
|
||||
if let Some(expired_order) = bookside.remove_one_expired(now_ts) {
|
||||
if let Some(expired_order) = order_tree.remove_one_expired(now_ts) {
|
||||
let event = OutEvent::new(
|
||||
side,
|
||||
expired_order.owner_slot,
|
||||
|
@ -324,12 +242,12 @@ impl<'a> Book<'a> {
|
|||
event_queue.push_back(cast(event)).unwrap();
|
||||
}
|
||||
|
||||
if bookside.is_full() {
|
||||
if order_tree.is_full() {
|
||||
// If this bid is higher than lowest bid, boot that bid and insert this one
|
||||
let worst_order = bookside.remove_worst().unwrap();
|
||||
let worst_order = order_tree.remove_worst().unwrap();
|
||||
// MangoErrorCode::OutOfSpace
|
||||
require!(
|
||||
side.is_price_better(price_lots, worst_order.price()),
|
||||
side.is_price_data_better(price_data, worst_order.price_data()),
|
||||
MangoError::SomeError
|
||||
);
|
||||
let event = OutEvent::new(
|
||||
|
@ -349,12 +267,13 @@ impl<'a> Book<'a> {
|
|||
order_id,
|
||||
*mango_account_pk,
|
||||
book_base_quantity,
|
||||
client_order_id,
|
||||
order.client_order_id,
|
||||
now_ts,
|
||||
order_type,
|
||||
time_in_force,
|
||||
PostOrderType::Limit, // TODO: Support order types? needed?
|
||||
order.time_in_force,
|
||||
order.peg_limit(),
|
||||
);
|
||||
let _result = bookside.insert_leaf(&new_order)?;
|
||||
let _result = order_tree.insert_leaf(&new_order)?;
|
||||
|
||||
// TODO OPT remove if PlacePerpOrder needs more compute
|
||||
msg!(
|
||||
|
@ -368,7 +287,12 @@ impl<'a> Book<'a> {
|
|||
price_lots
|
||||
);
|
||||
|
||||
mango_account.add_perp_order(market.perp_market_index, side, &new_order)?;
|
||||
mango_account.add_perp_order(
|
||||
market.perp_market_index,
|
||||
side,
|
||||
order_tree_target,
|
||||
&new_order,
|
||||
)?;
|
||||
}
|
||||
|
||||
// if there were matched taker quote apply ref fees
|
||||
|
@ -378,7 +302,7 @@ impl<'a> Book<'a> {
|
|||
}
|
||||
|
||||
// IOC orders have a fee penalty applied regardless of match
|
||||
if order_type == OrderType::ImmediateOrCancel {
|
||||
if order.needs_penalty_fee() {
|
||||
apply_penalty(market, mango_account)?;
|
||||
}
|
||||
|
||||
|
@ -397,21 +321,20 @@ impl<'a> Book<'a> {
|
|||
) -> Result<()> {
|
||||
for i in 0..mango_account.header.perp_oo_count() {
|
||||
let oo = mango_account.perp_order_by_raw_index(i);
|
||||
if oo.order_market == FREE_ORDER_SLOT
|
||||
|| oo.order_market != perp_market.perp_market_index
|
||||
{
|
||||
if oo.market == FREE_ORDER_SLOT || oo.market != perp_market.perp_market_index {
|
||||
continue;
|
||||
}
|
||||
|
||||
let order_side = oo.order_side;
|
||||
let order_side_and_tree = oo.side_and_tree;
|
||||
if let Some(side_to_cancel) = side_to_cancel_option {
|
||||
if side_to_cancel != order_side {
|
||||
if side_to_cancel != order_side_and_tree.side() {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let order_id = oo.order_id;
|
||||
self.cancel_order(mango_account, order_id, order_side, None)?;
|
||||
let order_id = oo.id;
|
||||
|
||||
self.cancel_order(mango_account, order_id, order_side_and_tree, None)?;
|
||||
|
||||
limit -= 1;
|
||||
if limit == 0 {
|
||||
|
@ -426,19 +349,16 @@ impl<'a> Book<'a> {
|
|||
pub fn cancel_order(
|
||||
&mut self,
|
||||
mango_account: &mut MangoAccountRefMut,
|
||||
order_id: i128,
|
||||
side: Side,
|
||||
order_id: u128,
|
||||
side_and_tree: SideAndOrderTree,
|
||||
expected_owner: Option<Pubkey>,
|
||||
) -> Result<LeafNode> {
|
||||
let leaf_node =
|
||||
match side {
|
||||
Side::Bid => self.bids.remove_by_key(order_id).ok_or_else(|| {
|
||||
error_msg!("invalid perp order id {order_id} for side {side:?}")
|
||||
}),
|
||||
Side::Ask => self.asks.remove_by_key(order_id).ok_or_else(|| {
|
||||
error_msg!("invalid perp order id {order_id} for side {side:?}")
|
||||
}),
|
||||
}?;
|
||||
let side = side_and_tree.side();
|
||||
let book_component = side_and_tree.order_tree();
|
||||
let leaf_node = self.bookside_mut(side).orders_mut(book_component).
|
||||
remove_by_key(order_id).ok_or_else(|| {
|
||||
error_msg!("invalid perp order id {order_id} for side {side:?} and component {book_component:?}")
|
||||
})?;
|
||||
if let Some(owner) = expected_owner {
|
||||
require_keys_eq!(leaf_node.owner, owner);
|
||||
}
|
||||
|
|
|
@ -1,17 +1,7 @@
|
|||
use anchor_lang::prelude::*;
|
||||
use bytemuck::{cast, cast_mut, cast_ref};
|
||||
|
||||
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
||||
use static_assertions::const_assert_eq;
|
||||
|
||||
use crate::state::orderbook::bookside_iterator::BookSideIter;
|
||||
|
||||
use crate::error::MangoError;
|
||||
use crate::state::orderbook::nodes::{
|
||||
AnyNode, FreeNode, InnerNode, LeafNode, NodeHandle, NodeRef, NodeTag,
|
||||
};
|
||||
|
||||
pub const MAX_BOOK_NODES: usize = 1024;
|
||||
use super::*;
|
||||
|
||||
#[derive(
|
||||
Eq,
|
||||
|
@ -25,619 +15,261 @@ pub const MAX_BOOK_NODES: usize = 1024;
|
|||
AnchorDeserialize,
|
||||
)]
|
||||
#[repr(u8)]
|
||||
pub enum BookSideType {
|
||||
Bids,
|
||||
Asks,
|
||||
pub enum BookSideOrderTree {
|
||||
Fixed,
|
||||
OraclePegged,
|
||||
}
|
||||
|
||||
/// Reference to a node in a book side component
|
||||
pub struct BookSideOrderHandle {
|
||||
pub node: NodeHandle,
|
||||
pub order_tree: BookSideOrderTree,
|
||||
}
|
||||
|
||||
/// A binary tree on AnyNode::key()
|
||||
///
|
||||
/// The key encodes the price in the top 64 bits.
|
||||
#[account(zero_copy)]
|
||||
pub struct BookSide {
|
||||
// pub meta_data: MetaData,
|
||||
// todo: do we want this type at this level?
|
||||
pub book_side_type: BookSideType,
|
||||
pub padding: [u8; 3],
|
||||
pub bump_index: u32,
|
||||
pub free_list_len: u32,
|
||||
pub free_list_head: NodeHandle,
|
||||
pub root_node: NodeHandle,
|
||||
pub leaf_count: u32,
|
||||
pub nodes: [AnyNode; MAX_BOOK_NODES],
|
||||
pub reserved: [u8; 256],
|
||||
pub fixed: OrderTree,
|
||||
pub oracle_pegged: OrderTree,
|
||||
}
|
||||
const_assert_eq!(
|
||||
std::mem::size_of::<BookSide>(),
|
||||
1 + 3 + 4 * 2 + 4 + 4 + 4 + 96 * 1024 + 256 // 98584
|
||||
);
|
||||
const_assert_eq!(std::mem::size_of::<BookSide>() % 8, 0);
|
||||
|
||||
impl BookSide {
|
||||
/// Iterate over all entries in the book filtering out invalid orders
|
||||
///
|
||||
/// smallest to highest for asks
|
||||
/// highest to smallest for bids
|
||||
pub fn iter_valid(&self, now_ts: u64) -> BookSideIter {
|
||||
BookSideIter::new(self, now_ts)
|
||||
pub fn iter_valid(
|
||||
&self,
|
||||
now_ts: u64,
|
||||
oracle_price_lots: i64,
|
||||
) -> impl Iterator<Item = BookSideIterItem> {
|
||||
BookSideIter::new(self, now_ts, oracle_price_lots).filter(|it| it.is_valid)
|
||||
}
|
||||
|
||||
/// Iterate over all entries, including invalid orders
|
||||
pub fn iter_all_including_invalid(&self) -> BookSideIter {
|
||||
BookSideIter::new(self, 0)
|
||||
pub fn iter_all_including_invalid(&self, now_ts: u64, oracle_price_lots: i64) -> BookSideIter {
|
||||
BookSideIter::new(self, now_ts, oracle_price_lots)
|
||||
}
|
||||
|
||||
pub fn node_mut(&mut self, key: NodeHandle) -> Option<&mut AnyNode> {
|
||||
let node = &mut self.nodes[key as usize];
|
||||
let tag = NodeTag::try_from(node.tag);
|
||||
match tag {
|
||||
Ok(NodeTag::InnerNode) | Ok(NodeTag::LeafNode) => Some(node),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
pub fn node(&self, key: NodeHandle) -> Option<&AnyNode> {
|
||||
let node = &self.nodes[key as usize];
|
||||
let tag = NodeTag::try_from(node.tag);
|
||||
match tag {
|
||||
Ok(NodeTag::InnerNode) | Ok(NodeTag::LeafNode) => Some(node),
|
||||
_ => None,
|
||||
pub fn orders(&self, component: BookSideOrderTree) -> &OrderTree {
|
||||
match component {
|
||||
BookSideOrderTree::Fixed => &self.fixed,
|
||||
BookSideOrderTree::OraclePegged => &self.oracle_pegged,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_min(&mut self) -> Option<LeafNode> {
|
||||
self.remove_by_key(self.min_leaf()?.key)
|
||||
}
|
||||
|
||||
pub fn remove_max(&mut self) -> Option<LeafNode> {
|
||||
self.remove_by_key(self.max_leaf()?.key)
|
||||
}
|
||||
|
||||
pub fn remove_worst(&mut self) -> Option<LeafNode> {
|
||||
match self.book_side_type {
|
||||
BookSideType::Bids => self.remove_min(),
|
||||
BookSideType::Asks => self.remove_max(),
|
||||
pub fn orders_mut(&mut self, component: BookSideOrderTree) -> &mut OrderTree {
|
||||
match component {
|
||||
BookSideOrderTree::Fixed => &mut self.fixed,
|
||||
BookSideOrderTree::OraclePegged => &mut self.oracle_pegged,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn node(&self, key: BookSideOrderHandle) -> Option<&AnyNode> {
|
||||
self.orders(key.order_tree).node(key.node)
|
||||
}
|
||||
|
||||
pub fn node_mut(&mut self, key: BookSideOrderHandle) -> Option<&mut AnyNode> {
|
||||
self.orders_mut(key.order_tree).node_mut(key.node)
|
||||
}
|
||||
|
||||
pub fn is_full(&self, component: BookSideOrderTree) -> bool {
|
||||
self.orders(component).is_full()
|
||||
}
|
||||
|
||||
pub fn remove_worst(&mut self, component: BookSideOrderTree) -> Option<LeafNode> {
|
||||
self.orders_mut(component).remove_worst()
|
||||
}
|
||||
|
||||
/// Remove the order with the lowest expiry timestamp, if that's < now_ts.
|
||||
pub fn remove_one_expired(&mut self, now_ts: u64) -> Option<LeafNode> {
|
||||
let (expired_h, expires_at) = self.find_earliest_expiry()?;
|
||||
if expires_at < now_ts {
|
||||
self.remove_by_key(self.node(expired_h)?.key()?)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn root(&self) -> Option<NodeHandle> {
|
||||
if self.leaf_count == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(self.root_node)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(dead_code)]
|
||||
fn to_price_quantity_vec(&self, reverse: bool) -> Vec<(i64, i64)> {
|
||||
let mut pqs = vec![];
|
||||
let mut current: NodeHandle = match self.root() {
|
||||
None => return pqs,
|
||||
Some(node_handle) => node_handle,
|
||||
};
|
||||
|
||||
let left = reverse as usize;
|
||||
let right = !reverse as usize;
|
||||
let mut stack = vec![];
|
||||
loop {
|
||||
let root_contents = self.node(current).unwrap(); // should never fail unless book is already fucked
|
||||
match root_contents.case().unwrap() {
|
||||
NodeRef::Inner(inner) => {
|
||||
stack.push(inner);
|
||||
current = inner.children[left];
|
||||
}
|
||||
NodeRef::Leaf(leaf) => {
|
||||
// if you hit leaf then pop stack and go right
|
||||
// all inner nodes on stack have already been visited to the left
|
||||
pqs.push((leaf.price(), leaf.quantity));
|
||||
match stack.pop() {
|
||||
None => return pqs,
|
||||
Some(inner) => {
|
||||
current = inner.children[right];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn min_leaf(&self) -> Option<&LeafNode> {
|
||||
self.leaf_min_max(false)
|
||||
}
|
||||
|
||||
pub fn max_leaf(&self) -> Option<&LeafNode> {
|
||||
self.leaf_min_max(true)
|
||||
}
|
||||
fn leaf_min_max(&self, find_max: bool) -> Option<&LeafNode> {
|
||||
let mut root: NodeHandle = self.root()?;
|
||||
|
||||
let i = if find_max { 1 } else { 0 };
|
||||
loop {
|
||||
let root_contents = self.node(root)?;
|
||||
match root_contents.case()? {
|
||||
NodeRef::Inner(inner) => {
|
||||
root = inner.children[i];
|
||||
}
|
||||
NodeRef::Leaf(leaf) => {
|
||||
return Some(leaf);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_by_key(&mut self, search_key: i128) -> Option<LeafNode> {
|
||||
// path of InnerNode handles that lead to the removed leaf
|
||||
let mut stack: Vec<(NodeHandle, bool)> = vec![];
|
||||
|
||||
// special case potentially removing the root
|
||||
let mut parent_h = self.root()?;
|
||||
let (mut child_h, mut crit_bit) = match self.node(parent_h).unwrap().case().unwrap() {
|
||||
NodeRef::Leaf(&leaf) if leaf.key == search_key => {
|
||||
assert_eq!(self.leaf_count, 1);
|
||||
self.root_node = 0;
|
||||
self.leaf_count = 0;
|
||||
let _old_root = self.remove(parent_h).unwrap();
|
||||
return Some(leaf);
|
||||
}
|
||||
NodeRef::Leaf(_) => return None,
|
||||
NodeRef::Inner(inner) => inner.walk_down(search_key),
|
||||
};
|
||||
stack.push((parent_h, crit_bit));
|
||||
|
||||
// walk down the tree until finding the key
|
||||
loop {
|
||||
match self.node(child_h).unwrap().case().unwrap() {
|
||||
NodeRef::Inner(inner) => {
|
||||
parent_h = child_h;
|
||||
let (new_child_h, new_crit_bit) = inner.walk_down(search_key);
|
||||
child_h = new_child_h;
|
||||
crit_bit = new_crit_bit;
|
||||
stack.push((parent_h, crit_bit));
|
||||
}
|
||||
NodeRef::Leaf(leaf) => {
|
||||
if leaf.key != search_key {
|
||||
return None;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// replace parent with its remaining child node
|
||||
// free child_h, replace *parent_h with *other_child_h, free other_child_h
|
||||
let other_child_h = self.node(parent_h).unwrap().children().unwrap()[!crit_bit as usize];
|
||||
let other_child_node_contents = self.remove(other_child_h).unwrap();
|
||||
let new_expiry = other_child_node_contents.earliest_expiry();
|
||||
*self.node_mut(parent_h).unwrap() = other_child_node_contents;
|
||||
self.leaf_count -= 1;
|
||||
let removed_leaf: LeafNode = cast(self.remove(child_h).unwrap());
|
||||
|
||||
// update child min expiry back up to the root
|
||||
let outdated_expiry = removed_leaf.expiry();
|
||||
stack.pop(); // the final parent has been replaced by the remaining leaf
|
||||
self.update_parent_earliest_expiry(&stack, outdated_expiry, new_expiry);
|
||||
|
||||
Some(removed_leaf)
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, key: NodeHandle) -> Option<AnyNode> {
|
||||
let val = *self.node(key)?;
|
||||
|
||||
self.nodes[key as usize] = cast(FreeNode {
|
||||
tag: if self.free_list_len == 0 {
|
||||
NodeTag::LastFreeNode.into()
|
||||
} else {
|
||||
NodeTag::FreeNode.into()
|
||||
},
|
||||
next: self.free_list_head,
|
||||
reserved: [0; 88],
|
||||
});
|
||||
|
||||
self.free_list_len += 1;
|
||||
self.free_list_head = key;
|
||||
Some(val)
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, val: &AnyNode) -> Result<NodeHandle> {
|
||||
match NodeTag::try_from(val.tag) {
|
||||
Ok(NodeTag::InnerNode) | Ok(NodeTag::LeafNode) => (),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
if self.free_list_len == 0 {
|
||||
require!(
|
||||
(self.bump_index as usize) < self.nodes.len() && self.bump_index < u32::MAX,
|
||||
MangoError::SomeError // todo
|
||||
);
|
||||
|
||||
self.nodes[self.bump_index as usize] = *val;
|
||||
let key = self.bump_index;
|
||||
self.bump_index += 1;
|
||||
return Ok(key);
|
||||
}
|
||||
|
||||
let key = self.free_list_head;
|
||||
let node = &mut self.nodes[key as usize];
|
||||
|
||||
// TODO OPT possibly unnecessary require here - remove if we need compute
|
||||
match NodeTag::try_from(node.tag) {
|
||||
Ok(NodeTag::FreeNode) => assert!(self.free_list_len > 1),
|
||||
Ok(NodeTag::LastFreeNode) => assert_eq!(self.free_list_len, 1),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
// TODO - test borrow requireer
|
||||
self.free_list_head = cast_ref::<AnyNode, FreeNode>(node).next;
|
||||
self.free_list_len -= 1;
|
||||
*node = *val;
|
||||
Ok(key)
|
||||
}
|
||||
pub fn insert_leaf(&mut self, new_leaf: &LeafNode) -> Result<(NodeHandle, Option<LeafNode>)> {
|
||||
// path of InnerNode handles that lead to the new leaf
|
||||
let mut stack: Vec<(NodeHandle, bool)> = vec![];
|
||||
|
||||
// deal with inserts into an empty tree
|
||||
let mut root: NodeHandle = match self.root() {
|
||||
Some(h) => h,
|
||||
None => {
|
||||
// create a new root if none exists
|
||||
let handle = self.insert(new_leaf.as_ref())?;
|
||||
self.root_node = handle;
|
||||
self.leaf_count = 1;
|
||||
return Ok((handle, None));
|
||||
}
|
||||
};
|
||||
|
||||
// walk down the tree until we find the insert location
|
||||
loop {
|
||||
// require if the new node will be a child of the root
|
||||
let root_contents = *self.node(root).unwrap();
|
||||
let root_key = root_contents.key().unwrap();
|
||||
if root_key == new_leaf.key {
|
||||
// This should never happen because key should never match
|
||||
if let Some(NodeRef::Leaf(&old_root_as_leaf)) = root_contents.case() {
|
||||
// clobber the existing leaf
|
||||
*self.node_mut(root).unwrap() = *new_leaf.as_ref();
|
||||
self.update_parent_earliest_expiry(
|
||||
&stack,
|
||||
old_root_as_leaf.expiry(),
|
||||
new_leaf.expiry(),
|
||||
);
|
||||
return Ok((root, Some(old_root_as_leaf)));
|
||||
}
|
||||
// InnerNodes have a random child's key, so matching can happen and is fine
|
||||
}
|
||||
let shared_prefix_len: u32 = (root_key ^ new_leaf.key).leading_zeros();
|
||||
match root_contents.case() {
|
||||
None => unreachable!(),
|
||||
Some(NodeRef::Inner(inner)) => {
|
||||
let keep_old_root = shared_prefix_len >= inner.prefix_len;
|
||||
if keep_old_root {
|
||||
let (child, crit_bit) = inner.walk_down(new_leaf.key);
|
||||
stack.push((root, crit_bit));
|
||||
root = child;
|
||||
continue;
|
||||
};
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
// implies root is a Leaf or Inner where shared_prefix_len < prefix_len
|
||||
// we'll replace root with a new InnerNode that has new_leaf and root as children
|
||||
|
||||
// change the root in place to represent the LCA of [new_leaf] and [root]
|
||||
let crit_bit_mask: i128 = 1i128 << (127 - shared_prefix_len);
|
||||
let new_leaf_crit_bit = (crit_bit_mask & new_leaf.key) != 0;
|
||||
let old_root_crit_bit = !new_leaf_crit_bit;
|
||||
|
||||
let new_leaf_handle = self.insert(new_leaf.as_ref())?;
|
||||
let moved_root_handle = match self.insert(&root_contents) {
|
||||
Ok(h) => h,
|
||||
Err(e) => {
|
||||
self.remove(new_leaf_handle).unwrap();
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
let new_root: &mut InnerNode = cast_mut(self.node_mut(root).unwrap());
|
||||
*new_root = InnerNode::new(shared_prefix_len, new_leaf.key);
|
||||
|
||||
new_root.children[new_leaf_crit_bit as usize] = new_leaf_handle;
|
||||
new_root.children[old_root_crit_bit as usize] = moved_root_handle;
|
||||
|
||||
let new_leaf_expiry = new_leaf.expiry();
|
||||
let old_root_expiry = root_contents.earliest_expiry();
|
||||
new_root.child_earliest_expiry[new_leaf_crit_bit as usize] = new_leaf_expiry;
|
||||
new_root.child_earliest_expiry[old_root_crit_bit as usize] = old_root_expiry;
|
||||
|
||||
// walk up the stack and fix up the new min if needed
|
||||
if new_leaf_expiry < old_root_expiry {
|
||||
self.update_parent_earliest_expiry(&stack, old_root_expiry, new_leaf_expiry);
|
||||
}
|
||||
|
||||
self.leaf_count += 1;
|
||||
return Ok((new_leaf_handle, None));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_full(&self) -> bool {
|
||||
self.free_list_len <= 1 && (self.bump_index as usize) >= self.nodes.len() - 1
|
||||
}
|
||||
|
||||
/// When a node changes, the parents' child_earliest_expiry may need to be updated.
|
||||
///
|
||||
/// This function walks up the `stack` of parents and applies the change where the
|
||||
/// previous child's `outdated_expiry` is replaced by `new_expiry`.
|
||||
pub fn update_parent_earliest_expiry(
|
||||
pub fn remove_one_expired(
|
||||
&mut self,
|
||||
stack: &[(NodeHandle, bool)],
|
||||
mut outdated_expiry: u64,
|
||||
mut new_expiry: u64,
|
||||
) {
|
||||
// Walk from the top of the stack to the root of the tree.
|
||||
// Since the stack grows by appending, we need to iterate the slice in reverse order.
|
||||
for (parent_h, crit_bit) in stack.iter().rev() {
|
||||
let parent = self.node_mut(*parent_h).unwrap().as_inner_mut().unwrap();
|
||||
if parent.child_earliest_expiry[*crit_bit as usize] != outdated_expiry {
|
||||
break;
|
||||
}
|
||||
outdated_expiry = parent.earliest_expiry();
|
||||
parent.child_earliest_expiry[*crit_bit as usize] = new_expiry;
|
||||
new_expiry = parent.earliest_expiry();
|
||||
}
|
||||
component: BookSideOrderTree,
|
||||
now_ts: u64,
|
||||
) -> Option<LeafNode> {
|
||||
self.orders_mut(component).remove_one_expired(now_ts)
|
||||
}
|
||||
|
||||
/// Returns the handle of the node with the lowest expiry timestamp, and this timestamp
|
||||
pub fn find_earliest_expiry(&self) -> Option<(NodeHandle, u64)> {
|
||||
let mut current: NodeHandle = match self.root() {
|
||||
Some(h) => h,
|
||||
None => return None,
|
||||
};
|
||||
pub fn remove_by_key(
|
||||
&mut self,
|
||||
component: BookSideOrderTree,
|
||||
search_key: u128,
|
||||
) -> Option<LeafNode> {
|
||||
self.orders_mut(component).remove_by_key(search_key)
|
||||
}
|
||||
|
||||
loop {
|
||||
let contents = *self.node(current).unwrap();
|
||||
match contents.case() {
|
||||
None => unreachable!(),
|
||||
Some(NodeRef::Inner(inner)) => {
|
||||
current = inner.children[(inner.child_earliest_expiry[0]
|
||||
> inner.child_earliest_expiry[1])
|
||||
as usize];
|
||||
}
|
||||
_ => {
|
||||
return Some((current, contents.earliest_expiry()));
|
||||
}
|
||||
};
|
||||
}
|
||||
pub fn remove(&mut self, key: BookSideOrderHandle) -> Option<AnyNode> {
|
||||
self.orders_mut(key.order_tree).remove(key.node)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::super::order_type::OrderType;
|
||||
use super::*;
|
||||
use bytemuck::Zeroable;
|
||||
|
||||
fn new_bookside(book_side_type: BookSideType) -> BookSide {
|
||||
BookSide {
|
||||
book_side_type,
|
||||
fn new_order_tree(order_tree_type: OrderTreeType) -> OrderTree {
|
||||
OrderTree {
|
||||
order_tree_type,
|
||||
padding: [0u8; 3],
|
||||
bump_index: 0,
|
||||
free_list_len: 0,
|
||||
free_list_head: 0,
|
||||
root_node: 0,
|
||||
leaf_count: 0,
|
||||
nodes: [AnyNode::zeroed(); MAX_BOOK_NODES],
|
||||
nodes: [AnyNode::zeroed(); MAX_ORDERTREE_NODES],
|
||||
reserved: [0; 256],
|
||||
}
|
||||
}
|
||||
|
||||
fn verify_bookside(bookside: &BookSide) {
|
||||
verify_bookside_invariant(bookside);
|
||||
verify_bookside_iteration(bookside);
|
||||
verify_bookside_expiry(bookside);
|
||||
}
|
||||
|
||||
// check that BookSide binary tree key invariant holds
|
||||
fn verify_bookside_invariant(bookside: &BookSide) {
|
||||
let r = match bookside.root() {
|
||||
Some(h) => h,
|
||||
None => return,
|
||||
};
|
||||
|
||||
fn recursive_check(bookside: &BookSide, h: NodeHandle) {
|
||||
match bookside.node(h).unwrap().case().unwrap() {
|
||||
NodeRef::Inner(&inner) => {
|
||||
let left = bookside.node(inner.children[0]).unwrap().key().unwrap();
|
||||
let right = bookside.node(inner.children[1]).unwrap().key().unwrap();
|
||||
|
||||
// the left and right keys share the InnerNode's prefix
|
||||
assert!((inner.key ^ left).leading_zeros() >= inner.prefix_len);
|
||||
assert!((inner.key ^ right).leading_zeros() >= inner.prefix_len);
|
||||
|
||||
// the left and right node key have the critbit unset and set respectively
|
||||
let crit_bit_mask: i128 = 1i128 << (127 - inner.prefix_len);
|
||||
assert!(left & crit_bit_mask == 0);
|
||||
assert!(right & crit_bit_mask != 0);
|
||||
|
||||
recursive_check(bookside, inner.children[0]);
|
||||
recursive_check(bookside, inner.children[1]);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
recursive_check(bookside, r);
|
||||
}
|
||||
|
||||
// check that iteration of bookside has the right order and misses no leaves
|
||||
fn verify_bookside_iteration(bookside: &BookSide) {
|
||||
let mut total = 0;
|
||||
let ascending = bookside.book_side_type == BookSideType::Asks;
|
||||
let mut last_key = if ascending { 0 } else { i128::MAX };
|
||||
for (_, node) in bookside.iter_all_including_invalid() {
|
||||
let key = node.key;
|
||||
if ascending {
|
||||
assert!(key >= last_key);
|
||||
} else {
|
||||
assert!(key <= last_key);
|
||||
}
|
||||
last_key = key;
|
||||
total += 1;
|
||||
}
|
||||
assert_eq!(bookside.leaf_count, total);
|
||||
}
|
||||
|
||||
// check that BookSide::child_expiry invariant holds
|
||||
fn verify_bookside_expiry(bookside: &BookSide) {
|
||||
let r = match bookside.root() {
|
||||
Some(h) => h,
|
||||
None => return,
|
||||
};
|
||||
|
||||
fn recursive_check(bookside: &BookSide, h: NodeHandle) {
|
||||
match bookside.node(h).unwrap().case().unwrap() {
|
||||
NodeRef::Inner(&inner) => {
|
||||
let left = bookside.node(inner.children[0]).unwrap().earliest_expiry();
|
||||
let right = bookside.node(inner.children[1]).unwrap().earliest_expiry();
|
||||
|
||||
// child_expiry must hold the expiry of the children
|
||||
assert_eq!(inner.child_earliest_expiry[0], left);
|
||||
assert_eq!(inner.child_earliest_expiry[1], right);
|
||||
|
||||
recursive_check(bookside, inner.children[0]);
|
||||
recursive_check(bookside, inner.children[1]);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
recursive_check(bookside, r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bookside_expiry_manual() {
|
||||
let mut bids = new_bookside(BookSideType::Bids);
|
||||
let new_expiring_leaf = |key: i128, expiry: u64| {
|
||||
LeafNode::new(
|
||||
0,
|
||||
key,
|
||||
Pubkey::default(),
|
||||
0,
|
||||
0,
|
||||
expiry - 1,
|
||||
OrderType::Limit,
|
||||
1,
|
||||
)
|
||||
};
|
||||
|
||||
assert!(bids.find_earliest_expiry().is_none());
|
||||
|
||||
bids.insert_leaf(&new_expiring_leaf(0, 5000)).unwrap();
|
||||
assert_eq!(bids.find_earliest_expiry().unwrap(), (bids.root_node, 5000));
|
||||
verify_bookside(&bids);
|
||||
|
||||
let (new4000_h, _) = bids.insert_leaf(&new_expiring_leaf(1, 4000)).unwrap();
|
||||
assert_eq!(bids.find_earliest_expiry().unwrap(), (new4000_h, 4000));
|
||||
verify_bookside(&bids);
|
||||
|
||||
let (_new4500_h, _) = bids.insert_leaf(&new_expiring_leaf(2, 4500)).unwrap();
|
||||
assert_eq!(bids.find_earliest_expiry().unwrap(), (new4000_h, 4000));
|
||||
verify_bookside(&bids);
|
||||
|
||||
let (new3500_h, _) = bids.insert_leaf(&new_expiring_leaf(3, 3500)).unwrap();
|
||||
assert_eq!(bids.find_earliest_expiry().unwrap(), (new3500_h, 3500));
|
||||
verify_bookside(&bids);
|
||||
// the first two levels of the tree are innernodes, with 0;1 on one side and 2;3 on the other
|
||||
assert_eq!(
|
||||
bids.node_mut(bids.root_node)
|
||||
.unwrap()
|
||||
.as_inner_mut()
|
||||
.unwrap()
|
||||
.child_earliest_expiry,
|
||||
[4000, 3500]
|
||||
);
|
||||
|
||||
bids.remove_by_key(3).unwrap();
|
||||
verify_bookside(&bids);
|
||||
assert_eq!(
|
||||
bids.node_mut(bids.root_node)
|
||||
.unwrap()
|
||||
.as_inner_mut()
|
||||
.unwrap()
|
||||
.child_earliest_expiry,
|
||||
[4000, 4500]
|
||||
);
|
||||
assert_eq!(bids.find_earliest_expiry().unwrap().1, 4000);
|
||||
|
||||
bids.remove_by_key(0).unwrap();
|
||||
verify_bookside(&bids);
|
||||
assert_eq!(
|
||||
bids.node_mut(bids.root_node)
|
||||
.unwrap()
|
||||
.as_inner_mut()
|
||||
.unwrap()
|
||||
.child_earliest_expiry,
|
||||
[4000, 4500]
|
||||
);
|
||||
assert_eq!(bids.find_earliest_expiry().unwrap().1, 4000);
|
||||
|
||||
bids.remove_by_key(1).unwrap();
|
||||
verify_bookside(&bids);
|
||||
assert_eq!(bids.find_earliest_expiry().unwrap().1, 4500);
|
||||
|
||||
bids.remove_by_key(2).unwrap();
|
||||
verify_bookside(&bids);
|
||||
assert!(bids.find_earliest_expiry().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bookside_expiry_random() {
|
||||
fn bookside_iteration_random_helper(side: Side) {
|
||||
use rand::Rng;
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
let mut bids = new_bookside(BookSideType::Bids);
|
||||
let new_expiring_leaf = |key: i128, expiry: u64| {
|
||||
let order_tree_type = match side {
|
||||
Side::Bid => OrderTreeType::Bids,
|
||||
Side::Ask => OrderTreeType::Asks,
|
||||
};
|
||||
|
||||
let mut fixed = new_order_tree(order_tree_type);
|
||||
let mut oracle_pegged = new_order_tree(order_tree_type);
|
||||
let new_leaf = |key: u128| {
|
||||
LeafNode::new(
|
||||
0,
|
||||
key,
|
||||
Pubkey::default(),
|
||||
0,
|
||||
0,
|
||||
expiry - 1,
|
||||
OrderType::Limit,
|
||||
1,
|
||||
PostOrderType::Limit,
|
||||
0,
|
||||
-1,
|
||||
)
|
||||
};
|
||||
|
||||
// add 200 random leaves
|
||||
// add 100 leaves to each BookSide, mostly random
|
||||
let mut keys = vec![];
|
||||
for _ in 0..200 {
|
||||
let key: i128 = rng.gen_range(0..10000); // overlap in key bits
|
||||
|
||||
// ensure at least one oracle pegged order visible even at oracle price 1
|
||||
let key = new_node_key(side, oracle_pegged_price_data(20), 0);
|
||||
keys.push(key);
|
||||
oracle_pegged.insert_leaf(&new_leaf(key)).unwrap();
|
||||
|
||||
while oracle_pegged.leaf_count < 100 {
|
||||
let price_data: u64 = oracle_pegged_price_data(rng.gen_range(-20..20));
|
||||
let seq_num: u64 = rng.gen_range(0..1000);
|
||||
let key = new_node_key(side, price_data, seq_num);
|
||||
if keys.contains(&key) {
|
||||
continue;
|
||||
}
|
||||
let expiry = rng.gen_range(1..200); // give good chance of duplicate expiry times
|
||||
keys.push(key);
|
||||
bids.insert_leaf(&new_expiring_leaf(key, expiry)).unwrap();
|
||||
verify_bookside(&bids);
|
||||
oracle_pegged.insert_leaf(&new_leaf(key)).unwrap();
|
||||
}
|
||||
|
||||
// remove 50 at random
|
||||
for _ in 0..50 {
|
||||
if keys.len() == 0 {
|
||||
break;
|
||||
while fixed.leaf_count < 100 {
|
||||
let price_data: u64 = rng.gen_range(1..50);
|
||||
let seq_num: u64 = rng.gen_range(0..1000);
|
||||
let key = new_node_key(side, price_data, seq_num);
|
||||
if keys.contains(&key) {
|
||||
continue;
|
||||
}
|
||||
keys.push(key);
|
||||
fixed.insert_leaf(&new_leaf(key)).unwrap();
|
||||
}
|
||||
|
||||
let bookside = BookSide {
|
||||
fixed,
|
||||
oracle_pegged,
|
||||
};
|
||||
|
||||
// verify iteration order for different oracle prices
|
||||
for oracle_price_lots in 1..40 {
|
||||
println!("oracle {oracle_price_lots}");
|
||||
let mut total = 0;
|
||||
let ascending = order_tree_type == OrderTreeType::Asks;
|
||||
let mut last_price = if ascending { 0 } else { i64::MAX };
|
||||
for order in bookside.iter_all_including_invalid(0, oracle_price_lots) {
|
||||
let price = order.price_lots;
|
||||
println!("{} {:?} {price}", order.node.key, order.handle.order_tree);
|
||||
if ascending {
|
||||
assert!(price >= last_price);
|
||||
} else {
|
||||
assert!(price <= last_price);
|
||||
}
|
||||
last_price = price;
|
||||
total += 1;
|
||||
}
|
||||
assert!(total >= 101); // some oracle peg orders could be skipped
|
||||
if oracle_price_lots > 20 {
|
||||
assert_eq!(total, 200);
|
||||
}
|
||||
let k = keys[rng.gen_range(0..keys.len())];
|
||||
bids.remove_by_key(k).unwrap();
|
||||
keys.retain(|v| *v != k);
|
||||
verify_bookside(&bids);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bookside_iteration_random() {
|
||||
bookside_iteration_random_helper(Side::Bid);
|
||||
bookside_iteration_random_helper(Side::Ask);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bookside_order_filtering() {
|
||||
let side = Side::Bid;
|
||||
let order_tree_type = OrderTreeType::Bids;
|
||||
|
||||
let mut fixed = new_order_tree(order_tree_type);
|
||||
let mut oracle_pegged = new_order_tree(order_tree_type);
|
||||
let new_node = |key: u128, tif: u8, peg_limit: i64| {
|
||||
LeafNode::new(
|
||||
0,
|
||||
key,
|
||||
Pubkey::default(),
|
||||
0,
|
||||
0,
|
||||
1000,
|
||||
PostOrderType::Limit,
|
||||
tif,
|
||||
peg_limit,
|
||||
)
|
||||
};
|
||||
let mut add_fixed = |price: i64, tif: u8| {
|
||||
let key = new_node_key(side, fixed_price_data(price).unwrap(), 0);
|
||||
fixed.insert_leaf(&new_node(key, tif, -1)).unwrap();
|
||||
};
|
||||
let mut add_pegged = |price_offset: i64, tif: u8, peg_limit: i64| {
|
||||
let key = new_node_key(side, oracle_pegged_price_data(price_offset), 0);
|
||||
oracle_pegged
|
||||
.insert_leaf(&new_node(key, tif, peg_limit))
|
||||
.unwrap();
|
||||
};
|
||||
|
||||
add_fixed(100, 0);
|
||||
add_fixed(120, 5);
|
||||
add_pegged(-10, 0, 100);
|
||||
add_pegged(-15, 0, -1);
|
||||
add_pegged(-20, 7, 95);
|
||||
|
||||
let bookside = BookSide {
|
||||
fixed,
|
||||
oracle_pegged,
|
||||
};
|
||||
|
||||
let order_prices = |now_ts: u64, oracle: i64| -> Vec<i64> {
|
||||
bookside
|
||||
.iter_valid(now_ts, oracle)
|
||||
.map(|it| it.price_lots)
|
||||
.collect()
|
||||
};
|
||||
|
||||
assert_eq!(order_prices(0, 100), vec![120, 100, 90, 85, 80]);
|
||||
assert_eq!(order_prices(1004, 100), vec![120, 100, 90, 85, 80]);
|
||||
assert_eq!(order_prices(1005, 100), vec![100, 90, 85, 80]);
|
||||
assert_eq!(order_prices(1006, 100), vec![100, 90, 85, 80]);
|
||||
assert_eq!(order_prices(1007, 100), vec![100, 90, 85]);
|
||||
assert_eq!(order_prices(0, 110), vec![120, 100, 100, 95, 90]);
|
||||
assert_eq!(order_prices(0, 111), vec![120, 100, 96, 91]);
|
||||
assert_eq!(order_prices(0, 115), vec![120, 100, 100, 95]);
|
||||
assert_eq!(order_prices(0, 116), vec![120, 101, 100]);
|
||||
assert_eq!(order_prices(0, 2015), vec![2000, 120, 100]);
|
||||
assert_eq!(order_prices(1010, 2015), vec![2000, 100]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,92 +1,153 @@
|
|||
use crate::state::orderbook::bookside::{BookSide, BookSideType};
|
||||
use crate::state::orderbook::nodes::{InnerNode, LeafNode, NodeHandle, NodeRef};
|
||||
use super::*;
|
||||
|
||||
/// Iterate over orders in order (bids=descending, asks=ascending)
|
||||
pub struct BookSideIterItem<'a> {
|
||||
pub handle: BookSideOrderHandle,
|
||||
pub node: &'a LeafNode,
|
||||
pub price_lots: i64,
|
||||
pub is_valid: bool,
|
||||
}
|
||||
|
||||
/// Iterates the fixed and oracle_pegged OrderTrees simultaneously, allowing users to
|
||||
/// walk the orderbook without caring about where an order came from.
|
||||
///
|
||||
/// This will skip over orders that are not currently matchable, but might be valid
|
||||
/// in the future.
|
||||
///
|
||||
/// This may return invalid orders (tif expired, peg_limit exceeded; see is_valid) which
|
||||
/// users are supposed to remove from the orderbook if they can.
|
||||
pub struct BookSideIter<'a> {
|
||||
book_side: &'a BookSide,
|
||||
/// InnerNodes where the right side still needs to be iterated on
|
||||
stack: Vec<&'a InnerNode>,
|
||||
/// To be returned on `next()`
|
||||
next_leaf: Option<(NodeHandle, &'a LeafNode)>,
|
||||
|
||||
/// either 0, 1 to iterate low-to-high, or 1, 0 to iterate high-to-low
|
||||
left: usize,
|
||||
right: usize,
|
||||
|
||||
fixed_iter: OrderTreeIter<'a>,
|
||||
oracle_pegged_iter: OrderTreeIter<'a>,
|
||||
now_ts: u64,
|
||||
oracle_price_lots: i64,
|
||||
}
|
||||
|
||||
impl<'a> BookSideIter<'a> {
|
||||
pub fn new(book_side: &'a BookSide, now_ts: u64) -> Self {
|
||||
let (left, right) = if book_side.book_side_type == BookSideType::Bids {
|
||||
(1, 0)
|
||||
} else {
|
||||
(0, 1)
|
||||
};
|
||||
let stack = vec![];
|
||||
|
||||
let mut iter = Self {
|
||||
book_side,
|
||||
stack,
|
||||
next_leaf: None,
|
||||
left,
|
||||
right,
|
||||
pub fn new(book_side: &'a BookSide, now_ts: u64, oracle_price_lots: i64) -> Self {
|
||||
Self {
|
||||
fixed_iter: book_side.fixed.iter(),
|
||||
oracle_pegged_iter: book_side.oracle_pegged.iter(),
|
||||
now_ts,
|
||||
};
|
||||
if book_side.leaf_count != 0 {
|
||||
iter.next_leaf = iter.find_leftmost_valid_leaf(book_side.root_node);
|
||||
oracle_price_lots,
|
||||
}
|
||||
iter
|
||||
}
|
||||
}
|
||||
|
||||
fn find_leftmost_valid_leaf(
|
||||
&mut self,
|
||||
start: NodeHandle,
|
||||
) -> Option<(NodeHandle, &'a LeafNode)> {
|
||||
let mut current = start;
|
||||
loop {
|
||||
match self.book_side.node(current).unwrap().case().unwrap() {
|
||||
NodeRef::Inner(inner) => {
|
||||
self.stack.push(inner);
|
||||
current = inner.children[self.left];
|
||||
}
|
||||
NodeRef::Leaf(leaf) => {
|
||||
if leaf.is_valid(self.now_ts) {
|
||||
return Some((current, leaf));
|
||||
} else {
|
||||
match self.stack.pop() {
|
||||
None => {
|
||||
return None;
|
||||
}
|
||||
Some(inner) => {
|
||||
current = inner.children[self.right];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum OrderState {
|
||||
Valid,
|
||||
Invalid,
|
||||
Skipped,
|
||||
}
|
||||
|
||||
fn oracle_pegged_price(
|
||||
oracle_price_lots: i64,
|
||||
node: &LeafNode,
|
||||
side: Side,
|
||||
) -> (OrderState, Option<i64>) {
|
||||
let price_data = node.price_data();
|
||||
let price_offset = oracle_pegged_price_offset(price_data);
|
||||
if let Some(price) = oracle_price_lots.checked_add(price_offset) {
|
||||
if price >= 1 {
|
||||
if node.peg_limit != -1 && side.is_price_better(price, node.peg_limit) {
|
||||
return (OrderState::Invalid, Some(price));
|
||||
} else {
|
||||
return (OrderState::Valid, Some(price));
|
||||
}
|
||||
}
|
||||
}
|
||||
(OrderState::Skipped, None)
|
||||
}
|
||||
|
||||
fn key_for_price(key: u128, price_lots: i64) -> u128 {
|
||||
// We know this can never fail, because oracle pegged price will always be >= 1
|
||||
assert!(price_lots >= 1);
|
||||
let price_data = fixed_price_data(price_lots).unwrap();
|
||||
let upper = (price_data as u128) << 64;
|
||||
let lower = (key as u64) as u128;
|
||||
upper | lower
|
||||
}
|
||||
|
||||
impl<'a> Iterator for BookSideIter<'a> {
|
||||
type Item = (NodeHandle, &'a LeafNode);
|
||||
type Item = BookSideIterItem<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
// if next leaf is None just return it
|
||||
self.next_leaf?;
|
||||
let side = self.fixed_iter.side();
|
||||
|
||||
// start popping from stack and get the other child
|
||||
let current_leaf = self.next_leaf;
|
||||
self.next_leaf = match self.stack.pop() {
|
||||
None => None,
|
||||
Some(inner) => {
|
||||
let start = inner.children[self.right];
|
||||
// go down the left branch as much as possible until reaching a valid leaf
|
||||
self.find_leftmost_valid_leaf(start)
|
||||
// Skip all the oracle pegged orders that aren't representable with the current oracle
|
||||
// price. Example: iterating asks, but the best ask is at offset -100 with the oracle at 50.
|
||||
// We need to skip asks until we find the first that has a price >= 1.
|
||||
let mut o_peek = self.oracle_pegged_iter.peek();
|
||||
while let Some((_, o_node)) = o_peek {
|
||||
if oracle_pegged_price(self.oracle_price_lots, o_node, side).0 != OrderState::Skipped {
|
||||
break;
|
||||
}
|
||||
};
|
||||
o_peek = self.oracle_pegged_iter.next()
|
||||
}
|
||||
|
||||
current_leaf
|
||||
match (self.fixed_iter.peek(), o_peek) {
|
||||
(Some((d_handle, d_node)), Some((o_handle, o_node))) => {
|
||||
let is_better = if side == Side::Bid {
|
||||
|a, b| a > b
|
||||
} else {
|
||||
|a, b| a < b
|
||||
};
|
||||
|
||||
let (o_valid, o_price_maybe) =
|
||||
oracle_pegged_price(self.oracle_price_lots, o_node, side);
|
||||
let o_price = o_price_maybe.unwrap(); // Skipped orders are skipped above
|
||||
if is_better(d_node.key, key_for_price(o_node.key, o_price)) {
|
||||
self.fixed_iter.next();
|
||||
Some(Self::Item {
|
||||
handle: BookSideOrderHandle {
|
||||
order_tree: BookSideOrderTree::Fixed,
|
||||
node: d_handle,
|
||||
},
|
||||
node: d_node,
|
||||
price_lots: fixed_price_lots(d_node.price_data()),
|
||||
is_valid: d_node.is_not_expired(self.now_ts),
|
||||
})
|
||||
} else {
|
||||
self.oracle_pegged_iter.next();
|
||||
Some(Self::Item {
|
||||
handle: BookSideOrderHandle {
|
||||
order_tree: BookSideOrderTree::OraclePegged,
|
||||
node: o_handle,
|
||||
},
|
||||
node: o_node,
|
||||
price_lots: o_price,
|
||||
is_valid: o_valid == OrderState::Valid
|
||||
&& o_node.is_not_expired(self.now_ts),
|
||||
})
|
||||
}
|
||||
}
|
||||
(None, Some((handle, node))) => {
|
||||
self.oracle_pegged_iter.next();
|
||||
let (valid, price_maybe) = oracle_pegged_price(self.oracle_price_lots, node, side);
|
||||
let price_lots = price_maybe.unwrap(); // Skipped orders are skipped above
|
||||
Some(Self::Item {
|
||||
handle: BookSideOrderHandle {
|
||||
order_tree: BookSideOrderTree::OraclePegged,
|
||||
node: handle,
|
||||
},
|
||||
node,
|
||||
price_lots,
|
||||
is_valid: valid == OrderState::Valid && node.is_not_expired(self.now_ts),
|
||||
})
|
||||
}
|
||||
(Some((handle, node)), None) => {
|
||||
self.fixed_iter.next();
|
||||
Some(Self::Item {
|
||||
handle: BookSideOrderHandle {
|
||||
order_tree: BookSideOrderTree::Fixed,
|
||||
node: handle,
|
||||
},
|
||||
node,
|
||||
price_lots: fixed_price_lots(node.price_data()),
|
||||
is_valid: node.is_not_expired(self.now_ts),
|
||||
})
|
||||
}
|
||||
(None, None) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,15 +2,21 @@ pub use book::*;
|
|||
pub use bookside::*;
|
||||
pub use bookside_iterator::*;
|
||||
pub use nodes::*;
|
||||
pub use order::*;
|
||||
pub use order_type::*;
|
||||
pub use ordertree::*;
|
||||
pub use ordertree_iterator::*;
|
||||
pub use queue::*;
|
||||
|
||||
pub mod book;
|
||||
pub mod bookside;
|
||||
pub mod bookside_iterator;
|
||||
pub mod nodes;
|
||||
pub mod order_type;
|
||||
pub mod queue;
|
||||
mod book;
|
||||
mod bookside;
|
||||
mod bookside_iterator;
|
||||
mod nodes;
|
||||
mod order;
|
||||
mod order_type;
|
||||
mod ordertree;
|
||||
mod ordertree_iterator;
|
||||
mod queue;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
@ -20,24 +26,9 @@ mod tests {
|
|||
use bytemuck::Zeroable;
|
||||
use fixed::types::I80F48;
|
||||
use solana_program::pubkey::Pubkey;
|
||||
use std::cell::RefCell;
|
||||
|
||||
fn new_bookside(book_side_type: BookSideType) -> BookSide {
|
||||
BookSide {
|
||||
book_side_type,
|
||||
padding: [0u8; 3],
|
||||
bump_index: 0,
|
||||
free_list_len: 0,
|
||||
free_list_head: 0,
|
||||
root_node: 0,
|
||||
leaf_count: 0,
|
||||
nodes: [AnyNode::zeroed(); MAX_BOOK_NODES],
|
||||
reserved: [0; 256],
|
||||
}
|
||||
}
|
||||
|
||||
fn bookside_leaf_by_key(bookside: &BookSide, key: i128) -> Option<&LeafNode> {
|
||||
for (_, leaf) in bookside.iter_all_including_invalid() {
|
||||
fn order_tree_leaf_by_key(order_tree: &OrderTree, key: u128) -> Option<&LeafNode> {
|
||||
for (_, leaf) in order_tree.iter() {
|
||||
if leaf.key == key {
|
||||
return Some(leaf);
|
||||
}
|
||||
|
@ -45,8 +36,8 @@ mod tests {
|
|||
None
|
||||
}
|
||||
|
||||
fn bookside_contains_key(bookside: &BookSide, key: i128) -> bool {
|
||||
for (_, leaf) in bookside.iter_all_including_invalid() {
|
||||
fn order_tree_contains_key(order_tree: &OrderTree, key: u128) -> bool {
|
||||
for (_, leaf) in order_tree.iter() {
|
||||
if leaf.key == key {
|
||||
return true;
|
||||
}
|
||||
|
@ -54,26 +45,18 @@ mod tests {
|
|||
false
|
||||
}
|
||||
|
||||
fn bookside_contains_price(bookside: &BookSide, price: i64) -> bool {
|
||||
for (_, leaf) in bookside.iter_all_including_invalid() {
|
||||
if leaf.price() == price {
|
||||
fn order_tree_contains_price(order_tree: &OrderTree, price_data: u64) -> bool {
|
||||
for (_, leaf) in order_tree.iter() {
|
||||
if leaf.price_data() == price_data {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn test_setup(
|
||||
price: f64,
|
||||
) -> (
|
||||
PerpMarket,
|
||||
I80F48,
|
||||
EventQueue,
|
||||
RefCell<BookSide>,
|
||||
RefCell<BookSide>,
|
||||
) {
|
||||
let bids = RefCell::new(new_bookside(BookSideType::Bids));
|
||||
let asks = RefCell::new(new_bookside(BookSideType::Asks));
|
||||
fn test_setup(price: f64) -> (PerpMarket, I80F48, EventQueue, Box<OrderBook>) {
|
||||
let mut book = Box::new(OrderBook::zeroed());
|
||||
book.init();
|
||||
|
||||
let event_queue = EventQueue::zeroed();
|
||||
|
||||
|
@ -87,49 +70,53 @@ mod tests {
|
|||
perp_market.init_asset_weight = I80F48::ONE;
|
||||
perp_market.init_liab_weight = I80F48::ONE;
|
||||
|
||||
(perp_market, oracle_price, event_queue, bids, asks)
|
||||
(perp_market, oracle_price, event_queue, book)
|
||||
}
|
||||
|
||||
// Check what happens when one side of the book fills up
|
||||
#[test]
|
||||
fn book_bids_full() {
|
||||
let (mut perp_market, oracle_price, mut event_queue, bids, asks) = test_setup(5000.0);
|
||||
let mut book = Book {
|
||||
bids: bids.borrow_mut(),
|
||||
asks: asks.borrow_mut(),
|
||||
};
|
||||
let (mut perp_market, oracle_price, mut event_queue, mut book) = test_setup(5000.0);
|
||||
let settle_token_index = 0;
|
||||
|
||||
let mut new_order =
|
||||
|book: &mut Book, event_queue: &mut EventQueue, side, price, now_ts| -> i128 {
|
||||
let buffer = MangoAccount::default_for_tests().try_to_vec().unwrap();
|
||||
let mut account = MangoAccountValue::from_bytes(&buffer).unwrap();
|
||||
account
|
||||
.ensure_perp_position(perp_market.perp_market_index, settle_token_index)
|
||||
.unwrap();
|
||||
|
||||
let quantity = 1;
|
||||
let tif = 100;
|
||||
|
||||
book.new_order(
|
||||
side,
|
||||
&mut perp_market,
|
||||
event_queue,
|
||||
oracle_price,
|
||||
&mut account.borrow_mut(),
|
||||
&Pubkey::default(),
|
||||
price,
|
||||
quantity,
|
||||
i64::MAX,
|
||||
OrderType::Limit,
|
||||
tif,
|
||||
0,
|
||||
now_ts,
|
||||
u8::MAX,
|
||||
)
|
||||
let mut new_order = |book: &mut OrderBook,
|
||||
event_queue: &mut EventQueue,
|
||||
side,
|
||||
price_lots,
|
||||
now_ts|
|
||||
-> u128 {
|
||||
let buffer = MangoAccount::default_for_tests().try_to_vec().unwrap();
|
||||
let mut account = MangoAccountValue::from_bytes(&buffer).unwrap();
|
||||
account
|
||||
.ensure_perp_position(perp_market.perp_market_index, settle_token_index)
|
||||
.unwrap();
|
||||
account.perp_order_by_raw_index(0).order_id
|
||||
};
|
||||
|
||||
let max_base_lots = 1;
|
||||
let time_in_force = 100;
|
||||
|
||||
book.new_order(
|
||||
Order {
|
||||
side,
|
||||
max_base_lots,
|
||||
max_quote_lots: i64::MAX,
|
||||
client_order_id: 0,
|
||||
time_in_force,
|
||||
params: OrderParams::Fixed {
|
||||
price_lots,
|
||||
order_type: PostOrderType::Limit,
|
||||
},
|
||||
},
|
||||
&mut perp_market,
|
||||
event_queue,
|
||||
oracle_price,
|
||||
&mut account.borrow_mut(),
|
||||
&Pubkey::default(),
|
||||
now_ts,
|
||||
u8::MAX,
|
||||
)
|
||||
.unwrap();
|
||||
account.perp_order_by_raw_index(0).id
|
||||
};
|
||||
|
||||
// insert bids until book side is full
|
||||
for i in 1..10 {
|
||||
|
@ -149,50 +136,46 @@ mod tests {
|
|||
1000 + i as i64,
|
||||
1000011 as u64,
|
||||
);
|
||||
if book.bids.is_full() {
|
||||
if book.bids.fixed.is_full() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert!(book.bids.is_full());
|
||||
assert_eq!(book.bids.min_leaf().unwrap().price(), 1001);
|
||||
assert!(book.bids.fixed.is_full());
|
||||
assert_eq!(book.bids.fixed.min_leaf().unwrap().price(), 1001);
|
||||
assert_eq!(
|
||||
book.bids.max_leaf().unwrap().price(),
|
||||
(1000 + book.bids.leaf_count) as i64
|
||||
book.bids.fixed.max_leaf().unwrap().price(),
|
||||
(1000 + book.bids.fixed.leaf_count) as i64
|
||||
);
|
||||
|
||||
// add another bid at a higher price before expiry, replacing the lowest-price one (1001)
|
||||
new_order(&mut book, &mut event_queue, Side::Bid, 1005, 1000000 - 1);
|
||||
assert_eq!(book.bids.min_leaf().unwrap().price(), 1002);
|
||||
assert_eq!(book.bids.fixed.min_leaf().unwrap().price(), 1002);
|
||||
assert_eq!(event_queue.len(), 1);
|
||||
|
||||
// adding another bid after expiry removes the soonest-expiring order (1005)
|
||||
new_order(&mut book, &mut event_queue, Side::Bid, 999, 2000000);
|
||||
assert_eq!(book.bids.min_leaf().unwrap().price(), 999);
|
||||
assert!(!bookside_contains_key(&book.bids, 1005));
|
||||
assert_eq!(book.bids.fixed.min_leaf().unwrap().price(), 999);
|
||||
assert!(!order_tree_contains_key(&book.bids.fixed, 1005));
|
||||
assert_eq!(event_queue.len(), 2);
|
||||
|
||||
// adding an ask will wipe up to three expired bids at the top of the book
|
||||
let bids_max = book.bids.max_leaf().unwrap().price();
|
||||
let bids_count = book.bids.leaf_count;
|
||||
let bids_max = book.bids.fixed.max_leaf().unwrap().price_data();
|
||||
let bids_count = book.bids.fixed.leaf_count;
|
||||
new_order(&mut book, &mut event_queue, Side::Ask, 6000, 1500000);
|
||||
assert_eq!(book.bids.leaf_count, bids_count - 5);
|
||||
assert_eq!(book.asks.leaf_count, 1);
|
||||
assert_eq!(book.bids.fixed.leaf_count, bids_count - 5);
|
||||
assert_eq!(book.asks.fixed.leaf_count, 1);
|
||||
assert_eq!(event_queue.len(), 2 + 5);
|
||||
assert!(!bookside_contains_price(&book.bids, bids_max));
|
||||
assert!(!bookside_contains_price(&book.bids, bids_max - 1));
|
||||
assert!(!bookside_contains_price(&book.bids, bids_max - 2));
|
||||
assert!(!bookside_contains_price(&book.bids, bids_max - 3));
|
||||
assert!(!bookside_contains_price(&book.bids, bids_max - 4));
|
||||
assert!(bookside_contains_price(&book.bids, bids_max - 5));
|
||||
assert!(!order_tree_contains_price(&book.bids.fixed, bids_max));
|
||||
assert!(!order_tree_contains_price(&book.bids.fixed, bids_max - 1));
|
||||
assert!(!order_tree_contains_price(&book.bids.fixed, bids_max - 2));
|
||||
assert!(!order_tree_contains_price(&book.bids.fixed, bids_max - 3));
|
||||
assert!(!order_tree_contains_price(&book.bids.fixed, bids_max - 4));
|
||||
assert!(order_tree_contains_price(&book.bids.fixed, bids_max - 5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn book_new_order() {
|
||||
let (mut market, oracle_price, mut event_queue, bids, asks) = test_setup(1000.0);
|
||||
let mut book = Book {
|
||||
bids: bids.borrow_mut(),
|
||||
asks: asks.borrow_mut(),
|
||||
};
|
||||
let (mut market, oracle_price, mut event_queue, mut book) = test_setup(1000.0);
|
||||
let settle_token_index = 0;
|
||||
|
||||
// Add lots and fees to make sure to exercise unit conversion
|
||||
|
@ -216,41 +199,48 @@ mod tests {
|
|||
let now_ts = 1000000;
|
||||
|
||||
// Place a maker-bid
|
||||
let price = 1000 * market.base_lot_size / market.quote_lot_size;
|
||||
let price_lots = 1000 * market.base_lot_size / market.quote_lot_size;
|
||||
let bid_quantity = 10;
|
||||
book.new_order(
|
||||
Side::Bid,
|
||||
Order {
|
||||
side: Side::Bid,
|
||||
max_base_lots: bid_quantity,
|
||||
max_quote_lots: i64::MAX,
|
||||
client_order_id: 42,
|
||||
time_in_force: 0,
|
||||
params: OrderParams::Fixed {
|
||||
price_lots,
|
||||
order_type: PostOrderType::Limit,
|
||||
},
|
||||
},
|
||||
&mut market,
|
||||
&mut event_queue,
|
||||
oracle_price,
|
||||
&mut maker.borrow_mut(),
|
||||
&maker_pk,
|
||||
price,
|
||||
bid_quantity,
|
||||
i64::MAX,
|
||||
OrderType::Limit,
|
||||
0,
|
||||
42,
|
||||
now_ts,
|
||||
u8::MAX,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
maker.perp_order_mut_by_raw_index(0).order_market,
|
||||
maker.perp_order_mut_by_raw_index(0).market,
|
||||
market.perp_market_index
|
||||
);
|
||||
assert_eq!(maker.perp_order_mut_by_raw_index(1).market, FREE_ORDER_SLOT);
|
||||
assert_ne!(maker.perp_order_mut_by_raw_index(0).id, 0);
|
||||
assert_eq!(maker.perp_order_mut_by_raw_index(0).client_id, 42);
|
||||
assert_eq!(
|
||||
maker.perp_order_mut_by_raw_index(1).order_market,
|
||||
FREE_ORDER_SLOT
|
||||
maker.perp_order_mut_by_raw_index(0).side_and_tree,
|
||||
SideAndOrderTree::BidFixed
|
||||
);
|
||||
assert_ne!(maker.perp_order_mut_by_raw_index(0).order_id, 0);
|
||||
assert_eq!(maker.perp_order_mut_by_raw_index(0).client_order_id, 42);
|
||||
assert_eq!(maker.perp_order_mut_by_raw_index(0).order_side, Side::Bid);
|
||||
assert!(bookside_contains_key(
|
||||
&book.bids,
|
||||
maker.perp_order_mut_by_raw_index(0).order_id
|
||||
assert!(order_tree_contains_key(
|
||||
&book.bids.fixed,
|
||||
maker.perp_order_mut_by_raw_index(0).id
|
||||
));
|
||||
assert!(order_tree_contains_price(
|
||||
&book.bids.fixed,
|
||||
price_lots as u64
|
||||
));
|
||||
assert!(bookside_contains_price(&book.bids, price));
|
||||
assert_eq!(
|
||||
maker.perp_position_by_raw_index(0).bids_base_lots,
|
||||
bid_quantity
|
||||
|
@ -271,18 +261,22 @@ mod tests {
|
|||
// Take the order partially
|
||||
let match_quantity = 5;
|
||||
book.new_order(
|
||||
Side::Ask,
|
||||
Order {
|
||||
side: Side::Ask,
|
||||
max_base_lots: match_quantity,
|
||||
max_quote_lots: i64::MAX,
|
||||
client_order_id: 43,
|
||||
time_in_force: 0,
|
||||
params: OrderParams::Fixed {
|
||||
price_lots,
|
||||
order_type: PostOrderType::Limit,
|
||||
},
|
||||
},
|
||||
&mut market,
|
||||
&mut event_queue,
|
||||
oracle_price,
|
||||
&mut taker.borrow_mut(),
|
||||
&taker_pk,
|
||||
price,
|
||||
match_quantity,
|
||||
i64::MAX,
|
||||
OrderType::Limit,
|
||||
0,
|
||||
43,
|
||||
now_ts,
|
||||
u8::MAX,
|
||||
)
|
||||
|
@ -290,22 +284,19 @@ mod tests {
|
|||
// the remainder of the maker order is still on the book
|
||||
// (the maker account is unchanged: it was not even passed in)
|
||||
let order =
|
||||
bookside_leaf_by_key(&book.bids, maker.perp_order_by_raw_index(0).order_id).unwrap();
|
||||
assert_eq!(order.price(), price);
|
||||
order_tree_leaf_by_key(&book.bids.fixed, maker.perp_order_by_raw_index(0).id).unwrap();
|
||||
assert_eq!(order.price(), price_lots);
|
||||
assert_eq!(order.quantity, bid_quantity - match_quantity);
|
||||
|
||||
// fees were immediately accrued
|
||||
let match_quote = I80F48::from(match_quantity * price * market.quote_lot_size);
|
||||
let match_quote = I80F48::from(match_quantity * price_lots * market.quote_lot_size);
|
||||
assert_eq!(
|
||||
market.fees_accrued,
|
||||
match_quote * (market.maker_fee + market.taker_fee)
|
||||
);
|
||||
|
||||
// the taker account is updated
|
||||
assert_eq!(
|
||||
taker.perp_order_by_raw_index(0).order_market,
|
||||
FREE_ORDER_SLOT
|
||||
);
|
||||
assert_eq!(taker.perp_order_by_raw_index(0).market, FREE_ORDER_SLOT);
|
||||
assert_eq!(taker.perp_position_by_raw_index(0).bids_base_lots, 0);
|
||||
assert_eq!(taker.perp_position_by_raw_index(0).asks_base_lots, 0);
|
||||
assert_eq!(
|
||||
|
@ -314,7 +305,7 @@ mod tests {
|
|||
);
|
||||
assert_eq!(
|
||||
taker.perp_position_by_raw_index(0).taker_quote_lots,
|
||||
match_quantity * price
|
||||
match_quantity * price_lots
|
||||
);
|
||||
assert_eq!(taker.perp_position_by_raw_index(0).base_position_lots(), 0);
|
||||
assert_eq!(
|
||||
|
@ -328,7 +319,7 @@ mod tests {
|
|||
assert_eq!(event.event_type, EventType::Fill as u8);
|
||||
let fill: &FillEvent = bytemuck::cast_ref(event);
|
||||
assert_eq!(fill.quantity, match_quantity);
|
||||
assert_eq!(fill.price, price);
|
||||
assert_eq!(fill.price, price_lots);
|
||||
assert_eq!(fill.taker_client_order_id, 43);
|
||||
assert_eq!(fill.maker_client_order_id, 42);
|
||||
assert_eq!(fill.maker, maker_pk);
|
||||
|
@ -345,7 +336,7 @@ mod tests {
|
|||
.unwrap();
|
||||
assert_eq!(market.open_interest, 2 * match_quantity);
|
||||
|
||||
assert_eq!(maker.perp_order_by_raw_index(0).order_market, 0);
|
||||
assert_eq!(maker.perp_order_by_raw_index(0).market, 0);
|
||||
assert_eq!(
|
||||
maker.perp_position_by_raw_index(0).bids_base_lots,
|
||||
bid_quantity - match_quantity
|
||||
|
@ -378,37 +369,35 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_fee_penalty_applied_only_on_limit_order() -> Result<()> {
|
||||
let (mut market, oracle_price, mut event_queue, bids, asks) = test_setup(1000.0);
|
||||
let mut book = Book {
|
||||
bids: bids.borrow_mut(),
|
||||
asks: asks.borrow_mut(),
|
||||
};
|
||||
let (mut market, oracle_price, mut event_queue, mut book) = test_setup(1000.0);
|
||||
|
||||
let buffer = MangoAccount::default_for_tests().try_to_vec().unwrap();
|
||||
let mut account = MangoAccountValue::from_bytes(&buffer).unwrap();
|
||||
let taker_pk = Pubkey::new_unique();
|
||||
let now_ts = 1000000;
|
||||
|
||||
market.base_lot_size = 1;
|
||||
market.quote_lot_size = 1;
|
||||
market.taker_fee = I80F48::from_num(0.01);
|
||||
market.fee_penalty = 5.0;
|
||||
account.ensure_perp_position(market.perp_market_index, 0)?;
|
||||
|
||||
// Passive order
|
||||
book.new_order(
|
||||
Side::Ask,
|
||||
Order {
|
||||
side: Side::Ask,
|
||||
max_base_lots: 2,
|
||||
max_quote_lots: i64::MAX,
|
||||
client_order_id: 43,
|
||||
time_in_force: 0,
|
||||
params: OrderParams::Fixed {
|
||||
price_lots: 1000,
|
||||
order_type: PostOrderType::Limit,
|
||||
},
|
||||
},
|
||||
&mut market,
|
||||
&mut event_queue,
|
||||
oracle_price,
|
||||
&mut account.borrow_mut(),
|
||||
&taker_pk,
|
||||
1000,
|
||||
2,
|
||||
i64::MAX,
|
||||
OrderType::Limit,
|
||||
0,
|
||||
43,
|
||||
now_ts,
|
||||
u8::MAX,
|
||||
)
|
||||
|
@ -416,18 +405,22 @@ mod tests {
|
|||
|
||||
// Partial taker
|
||||
book.new_order(
|
||||
Side::Bid,
|
||||
Order {
|
||||
side: Side::Bid,
|
||||
max_base_lots: 1,
|
||||
max_quote_lots: i64::MAX,
|
||||
client_order_id: 43,
|
||||
time_in_force: 0,
|
||||
params: OrderParams::Fixed {
|
||||
price_lots: 1000,
|
||||
order_type: PostOrderType::Limit,
|
||||
},
|
||||
},
|
||||
&mut market,
|
||||
&mut event_queue,
|
||||
oracle_price,
|
||||
&mut account.borrow_mut(),
|
||||
&taker_pk,
|
||||
1000,
|
||||
1,
|
||||
i64::MAX,
|
||||
OrderType::Limit,
|
||||
0,
|
||||
43,
|
||||
now_ts,
|
||||
u8::MAX,
|
||||
)
|
||||
|
@ -449,18 +442,19 @@ mod tests {
|
|||
|
||||
// Full taker
|
||||
book.new_order(
|
||||
Side::Bid,
|
||||
Order {
|
||||
side: Side::Bid,
|
||||
max_base_lots: 1,
|
||||
max_quote_lots: i64::MAX,
|
||||
client_order_id: 43,
|
||||
time_in_force: 0,
|
||||
params: OrderParams::ImmediateOrCancel { price_lots: 1000 },
|
||||
},
|
||||
&mut market,
|
||||
&mut event_queue,
|
||||
oracle_price,
|
||||
&mut account.borrow_mut(),
|
||||
&taker_pk,
|
||||
1000,
|
||||
1,
|
||||
i64::MAX,
|
||||
OrderType::ImmediateOrCancel,
|
||||
0,
|
||||
43,
|
||||
now_ts,
|
||||
u8::MAX,
|
||||
)
|
||||
|
|
|
@ -6,7 +6,7 @@ use mango_macro::Pod;
|
|||
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
||||
use static_assertions::const_assert_eq;
|
||||
|
||||
use super::order_type::OrderType;
|
||||
use super::order_type::{PostOrderType, Side};
|
||||
|
||||
pub type NodeHandle = u32;
|
||||
const NODE_SIZE: usize = 96;
|
||||
|
@ -21,6 +21,43 @@ pub enum NodeTag {
|
|||
LastFreeNode = 4,
|
||||
}
|
||||
|
||||
/// Creates a binary tree node key.
|
||||
///
|
||||
/// It's used for sorting nodes (ascending for asks, descending for bids)
|
||||
/// and encodes price data in the top 64 bits followed by an ordering number
|
||||
/// in the lower bits.
|
||||
///
|
||||
/// The `seq_num` that's passed should monotonically increase. It's used to choose
|
||||
/// the ordering number such that orders placed later for the same price data
|
||||
/// are ordered after earlier orders.
|
||||
pub fn new_node_key(side: Side, price_data: u64, seq_num: u64) -> u128 {
|
||||
let seq_num = if side == Side::Bid { !seq_num } else { seq_num };
|
||||
|
||||
let upper = (price_data as u128) << 64;
|
||||
upper | (seq_num as u128)
|
||||
}
|
||||
|
||||
pub fn oracle_pegged_price_data(price_offset_lots: i64) -> u64 {
|
||||
// Price data is used for ordering in the bookside's top bits of the u128 key.
|
||||
// Map i64::MIN to be 0 and i64::MAX to u64::MAX, that way comparisons on the
|
||||
// u64 produce the same result as on the source i64.
|
||||
(price_offset_lots as u64).wrapping_add(u64::MAX / 2 + 1)
|
||||
}
|
||||
|
||||
pub fn oracle_pegged_price_offset(price_data: u64) -> i64 {
|
||||
price_data.wrapping_sub(u64::MAX / 2 + 1) as i64
|
||||
}
|
||||
|
||||
pub fn fixed_price_data(price_lots: i64) -> Result<u64> {
|
||||
require_gte!(price_lots, 1);
|
||||
Ok(price_lots as u64)
|
||||
}
|
||||
|
||||
pub fn fixed_price_lots(price_data: u64) -> i64 {
|
||||
assert!(price_data <= i64::MAX as u64);
|
||||
price_data as i64
|
||||
}
|
||||
|
||||
/// InnerNodes and LeafNodes compose the binary tree of orders.
|
||||
///
|
||||
/// Each InnerNode has exactly two children, which are either InnerNodes themselves,
|
||||
|
@ -35,7 +72,7 @@ pub struct InnerNode {
|
|||
pub prefix_len: u32,
|
||||
|
||||
/// only the top `prefix_len` bits of `key` are relevant
|
||||
pub key: i128,
|
||||
pub key: u128,
|
||||
|
||||
/// indexes into `BookSide::nodes`
|
||||
pub children: [NodeHandle; 2],
|
||||
|
@ -52,7 +89,7 @@ const_assert_eq!(size_of::<InnerNode>() % 8, 0);
|
|||
const_assert_eq!(size_of::<InnerNode>(), NODE_SIZE);
|
||||
|
||||
impl InnerNode {
|
||||
pub fn new(prefix_len: u32, key: i128) -> Self {
|
||||
pub fn new(prefix_len: u32, key: u128) -> Self {
|
||||
Self {
|
||||
tag: NodeTag::InnerNode.into(),
|
||||
prefix_len,
|
||||
|
@ -65,8 +102,8 @@ impl InnerNode {
|
|||
|
||||
/// Returns the handle of the child that may contain the search key
|
||||
/// and 0 or 1 depending on which child it was.
|
||||
pub(crate) fn walk_down(&self, search_key: i128) -> (NodeHandle, bool) {
|
||||
let crit_bit_mask = 1i128 << (127 - self.prefix_len);
|
||||
pub(crate) fn walk_down(&self, search_key: u128) -> (NodeHandle, bool) {
|
||||
let crit_bit_mask = 1u128 << (127 - self.prefix_len);
|
||||
let crit_bit = (search_key & crit_bit_mask) != 0;
|
||||
(self.children[crit_bit as usize], crit_bit)
|
||||
}
|
||||
|
@ -84,7 +121,7 @@ impl InnerNode {
|
|||
pub struct LeafNode {
|
||||
pub tag: u32,
|
||||
pub owner_slot: u8,
|
||||
pub order_type: OrderType, // this was added for TradingView move order
|
||||
pub order_type: PostOrderType, // this was added for TradingView move order
|
||||
|
||||
pub padding: [u8; 1],
|
||||
|
||||
|
@ -93,7 +130,7 @@ pub struct LeafNode {
|
|||
pub time_in_force: u8,
|
||||
|
||||
/// The binary tree key
|
||||
pub key: i128,
|
||||
pub key: u128,
|
||||
|
||||
pub owner: Pubkey,
|
||||
pub quantity: i64,
|
||||
|
@ -102,27 +139,26 @@ pub struct LeafNode {
|
|||
// The time the order was placed
|
||||
pub timestamp: u64,
|
||||
|
||||
pub reserved: [u8; 16],
|
||||
// Only applicable in the oracle_pegged OrderTree
|
||||
pub peg_limit: i64,
|
||||
|
||||
pub reserved: [u8; 8],
|
||||
}
|
||||
const_assert_eq!(size_of::<LeafNode>() % 8, 0);
|
||||
const_assert_eq!(size_of::<LeafNode>(), NODE_SIZE);
|
||||
|
||||
#[inline(always)]
|
||||
fn key_to_price(key: i128) -> i64 {
|
||||
(key >> 64) as i64
|
||||
}
|
||||
|
||||
impl LeafNode {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
owner_slot: u8,
|
||||
key: i128,
|
||||
key: u128,
|
||||
owner: Pubkey,
|
||||
quantity: i64,
|
||||
client_order_id: u64,
|
||||
timestamp: u64,
|
||||
order_type: OrderType,
|
||||
order_type: PostOrderType,
|
||||
time_in_force: u8,
|
||||
peg_limit: i64,
|
||||
) -> Self {
|
||||
Self {
|
||||
tag: NodeTag::LeafNode.into(),
|
||||
|
@ -135,13 +171,20 @@ impl LeafNode {
|
|||
quantity,
|
||||
client_order_id,
|
||||
timestamp,
|
||||
reserved: [0; 16],
|
||||
peg_limit,
|
||||
reserved: [0; 8],
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: remove, it's not always the price
|
||||
#[inline(always)]
|
||||
pub fn price(&self) -> i64 {
|
||||
key_to_price(self.key)
|
||||
self.price_data() as i64
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn price_data(&self) -> u64 {
|
||||
(self.key >> 64) as u64
|
||||
}
|
||||
|
||||
/// Time at which this order will expire, u64::MAX if never
|
||||
|
@ -155,7 +198,7 @@ impl LeafNode {
|
|||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn is_valid(&self, now_ts: u64) -> bool {
|
||||
pub fn is_not_expired(&self, now_ts: u64) -> bool {
|
||||
self.time_in_force == 0 || now_ts < self.timestamp + self.time_in_force as u64
|
||||
}
|
||||
}
|
||||
|
@ -191,7 +234,7 @@ pub(crate) enum NodeRefMut<'a> {
|
|||
}
|
||||
|
||||
impl AnyNode {
|
||||
pub fn key(&self) -> Option<i128> {
|
||||
pub fn key(&self) -> Option<u128> {
|
||||
match self.case()? {
|
||||
NodeRef::Inner(inner) => Some(inner.key),
|
||||
NodeRef::Leaf(leaf) => Some(leaf.key),
|
||||
|
@ -274,3 +317,70 @@ impl AsRef<AnyNode> for LeafNode {
|
|||
cast_ref(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use itertools::Itertools;
|
||||
|
||||
#[test]
|
||||
fn order_tree_price_data() {
|
||||
for price in [1, 42, i64::MAX] {
|
||||
assert_eq!(price, fixed_price_lots(fixed_price_data(price).unwrap()));
|
||||
}
|
||||
|
||||
let seq = [-i64::MAX, -i64::MAX + 1, 0, i64::MAX - 1, i64::MAX];
|
||||
for price_offset in seq {
|
||||
assert_eq!(
|
||||
price_offset,
|
||||
oracle_pegged_price_offset(oracle_pegged_price_data(price_offset))
|
||||
);
|
||||
}
|
||||
for (lhs, rhs) in seq.iter().tuple_windows() {
|
||||
let l_price_data = oracle_pegged_price_data(*lhs);
|
||||
let r_price_data = oracle_pegged_price_data(*rhs);
|
||||
assert!(l_price_data < r_price_data);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn order_tree_key_ordering() {
|
||||
let bid_seq: Vec<(i64, u64)> = vec![
|
||||
(-5, 15),
|
||||
(-5, 10),
|
||||
(-4, 6),
|
||||
(-4, 5),
|
||||
(0, 20),
|
||||
(0, 1),
|
||||
(4, 6),
|
||||
(4, 5),
|
||||
(5, 3),
|
||||
];
|
||||
for (lhs, rhs) in bid_seq.iter().tuple_windows() {
|
||||
let l_price_data = oracle_pegged_price_data(lhs.0);
|
||||
let r_price_data = oracle_pegged_price_data(rhs.0);
|
||||
let l_key = new_node_key(Side::Bid, l_price_data, lhs.1);
|
||||
let r_key = new_node_key(Side::Bid, r_price_data, rhs.1);
|
||||
assert!(l_key < r_key);
|
||||
}
|
||||
|
||||
let ask_seq: Vec<(i64, u64)> = vec![
|
||||
(-5, 10),
|
||||
(-5, 15),
|
||||
(-4, 6),
|
||||
(-4, 7),
|
||||
(0, 1),
|
||||
(0, 20),
|
||||
(4, 5),
|
||||
(4, 6),
|
||||
(5, 3),
|
||||
];
|
||||
for (lhs, rhs) in ask_seq.iter().tuple_windows() {
|
||||
let l_price_data = oracle_pegged_price_data(lhs.0);
|
||||
let r_price_data = oracle_pegged_price_data(rhs.0);
|
||||
let l_key = new_node_key(Side::Ask, l_price_data, lhs.1);
|
||||
let r_key = new_node_key(Side::Ask, r_price_data, rhs.1);
|
||||
assert!(l_key < r_key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 num_enum::{IntoPrimitive, TryFromPrimitive};
|
||||
|
||||
use super::*;
|
||||
use crate::error::*;
|
||||
use crate::error_msg;
|
||||
|
||||
#[derive(
|
||||
Eq,
|
||||
PartialEq,
|
||||
|
@ -13,7 +17,7 @@ use num_enum::{IntoPrimitive, TryFromPrimitive};
|
|||
AnchorDeserialize,
|
||||
)]
|
||||
#[repr(u8)]
|
||||
pub enum OrderType {
|
||||
pub enum PlaceOrderType {
|
||||
/// Take existing orders up to price, max_base_quantity and max_quote_quantity.
|
||||
/// If any base_quantity or quote_quantity remains, place an order on the book
|
||||
Limit = 0,
|
||||
|
@ -37,6 +41,44 @@ pub enum OrderType {
|
|||
PostOnlySlide = 4,
|
||||
}
|
||||
|
||||
impl PlaceOrderType {
|
||||
pub fn to_post_order_type(&self) -> Result<PostOrderType> {
|
||||
match *self {
|
||||
Self::Market => Err(error_msg!("Market is not a PostOrderType")),
|
||||
Self::ImmediateOrCancel => Err(error_msg!("ImmediateOrCancel is not a PostOrderType")),
|
||||
Self::Limit => Ok(PostOrderType::Limit),
|
||||
Self::PostOnly => Ok(PostOrderType::PostOnly),
|
||||
Self::PostOnlySlide => Ok(PostOrderType::PostOnlySlide),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Eq,
|
||||
PartialEq,
|
||||
Copy,
|
||||
Clone,
|
||||
TryFromPrimitive,
|
||||
IntoPrimitive,
|
||||
Debug,
|
||||
AnchorSerialize,
|
||||
AnchorDeserialize,
|
||||
)]
|
||||
#[repr(u8)]
|
||||
pub enum PostOrderType {
|
||||
/// Take existing orders up to price, max_base_quantity and max_quote_quantity.
|
||||
/// If any base_quantity or quote_quantity remains, place an order on the book
|
||||
Limit = 0,
|
||||
|
||||
/// Never take any existing orders, post the order on the book if possible.
|
||||
/// If existing orders can match with this order, do nothing.
|
||||
PostOnly = 2,
|
||||
|
||||
/// If existing orders match with this order, adjust the price to just barely
|
||||
/// not match. Always places an order on the book.
|
||||
PostOnlySlide = 4,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Eq,
|
||||
PartialEq,
|
||||
|
@ -62,6 +104,14 @@ impl Side {
|
|||
}
|
||||
}
|
||||
|
||||
/// Is `lhs` is a better order for `side` than `rhs`?
|
||||
pub fn is_price_data_better(self: &Side, lhs: u64, rhs: u64) -> bool {
|
||||
match self {
|
||||
Side::Bid => lhs > rhs,
|
||||
Side::Ask => lhs < rhs,
|
||||
}
|
||||
}
|
||||
|
||||
/// Is `lhs` is a better order for `side` than `rhs`?
|
||||
pub fn is_price_better(self: &Side, lhs: i64, rhs: i64) -> bool {
|
||||
match self {
|
||||
|
@ -78,3 +128,48 @@ impl Side {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// SideAndOrderTree is a storage optimization, so we don't need two bytes for the data
|
||||
#[derive(
|
||||
Eq,
|
||||
PartialEq,
|
||||
Copy,
|
||||
Clone,
|
||||
TryFromPrimitive,
|
||||
IntoPrimitive,
|
||||
Debug,
|
||||
AnchorSerialize,
|
||||
AnchorDeserialize,
|
||||
)]
|
||||
#[repr(u8)]
|
||||
pub enum SideAndOrderTree {
|
||||
BidFixed = 0,
|
||||
AskFixed = 1,
|
||||
BidOraclePegged = 2,
|
||||
AskOraclePegged = 3,
|
||||
}
|
||||
|
||||
impl SideAndOrderTree {
|
||||
pub fn new(side: Side, order_tree: BookSideOrderTree) -> Self {
|
||||
match (side, order_tree) {
|
||||
(Side::Bid, BookSideOrderTree::Fixed) => Self::BidFixed,
|
||||
(Side::Ask, BookSideOrderTree::Fixed) => Self::AskFixed,
|
||||
(Side::Bid, BookSideOrderTree::OraclePegged) => Self::BidOraclePegged,
|
||||
(Side::Ask, BookSideOrderTree::OraclePegged) => Self::AskOraclePegged,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn side(&self) -> Side {
|
||||
match self {
|
||||
Self::BidFixed | Self::BidOraclePegged => Side::Bid,
|
||||
Self::AskFixed | Self::AskOraclePegged => Side::Ask,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn order_tree(&self) -> BookSideOrderTree {
|
||||
match self {
|
||||
Self::BidFixed | Self::AskFixed => BookSideOrderTree::Fixed,
|
||||
Self::BidOraclePegged | Self::AskOraclePegged => BookSideOrderTree::OraclePegged,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 maker: Pubkey,
|
||||
pub maker_order_id: i128,
|
||||
pub maker_order_id: u128,
|
||||
pub maker_client_order_id: u64,
|
||||
pub maker_fee: I80F48,
|
||||
|
||||
|
@ -195,7 +195,7 @@ pub struct FillEvent {
|
|||
pub maker_timestamp: u64,
|
||||
|
||||
pub taker: Pubkey,
|
||||
pub taker_order_id: i128,
|
||||
pub taker_order_id: u128,
|
||||
pub taker_client_order_id: u64,
|
||||
pub taker_fee: I80F48,
|
||||
|
||||
|
@ -215,13 +215,13 @@ impl FillEvent {
|
|||
timestamp: u64,
|
||||
seq_num: u64,
|
||||
maker: Pubkey,
|
||||
maker_order_id: i128,
|
||||
maker_order_id: u128,
|
||||
maker_client_order_id: u64,
|
||||
maker_fee: I80F48,
|
||||
maker_timestamp: u64,
|
||||
|
||||
taker: Pubkey,
|
||||
taker_order_id: i128,
|
||||
taker_order_id: u128,
|
||||
taker_client_order_id: u64,
|
||||
taker_fee: I80F48,
|
||||
price: i64,
|
||||
|
|
|
@ -6,11 +6,11 @@ use fixed::types::I80F48;
|
|||
use static_assertions::const_assert_eq;
|
||||
|
||||
use crate::accounts_zerocopy::KeyedAccountReader;
|
||||
use crate::state::orderbook::order_type::Side;
|
||||
use crate::state::orderbook::Side;
|
||||
use crate::state::{oracle, TokenIndex};
|
||||
use crate::util::checked_math as cm;
|
||||
|
||||
use super::{Book, OracleConfig, DAY_I80F48};
|
||||
use super::{orderbook, OracleConfig, OrderBook, DAY_I80F48};
|
||||
|
||||
pub type PerpMarketIndex = u16;
|
||||
|
||||
|
@ -39,8 +39,8 @@ pub struct PerpMarket {
|
|||
|
||||
pub oracle_config: OracleConfig,
|
||||
|
||||
pub bids: Pubkey,
|
||||
pub asks: Pubkey,
|
||||
pub orderbook: Pubkey,
|
||||
pub padding3: [u8; 32],
|
||||
|
||||
pub event_queue: Pubkey,
|
||||
|
||||
|
@ -133,14 +133,9 @@ impl PerpMarket {
|
|||
self.trusted_market == 1
|
||||
}
|
||||
|
||||
pub fn gen_order_id(&mut self, side: Side, price: i64) -> i128 {
|
||||
pub fn gen_order_id(&mut self, side: Side, price_data: u64) -> u128 {
|
||||
self.seq_num += 1;
|
||||
|
||||
let upper = (price as i128) << 64;
|
||||
match side {
|
||||
Side::Bid => upper | (!self.seq_num as i128),
|
||||
Side::Ask => upper | (self.seq_num as i128),
|
||||
}
|
||||
orderbook::new_node_key(side, price_data, self.seq_num)
|
||||
}
|
||||
|
||||
pub fn oracle_price(&self, oracle_acc: &impl KeyedAccountReader) -> Result<I80F48> {
|
||||
|
@ -153,12 +148,18 @@ impl PerpMarket {
|
|||
}
|
||||
|
||||
/// Use current order book price and index price to update the instantaneous funding
|
||||
pub fn update_funding(&mut self, book: &Book, oracle_price: I80F48, now_ts: u64) -> Result<()> {
|
||||
pub fn update_funding(
|
||||
&mut self,
|
||||
book: &OrderBook,
|
||||
oracle_price: I80F48,
|
||||
now_ts: u64,
|
||||
) -> Result<()> {
|
||||
let index_price = oracle_price;
|
||||
let oracle_price_lots = self.native_price_to_lot(oracle_price);
|
||||
|
||||
// Get current book price & compare it to index price
|
||||
let bid = book.impact_price(Side::Bid, self.impact_quantity, now_ts);
|
||||
let ask = book.impact_price(Side::Ask, self.impact_quantity, now_ts);
|
||||
let bid = book.impact_price(Side::Bid, self.impact_quantity, now_ts, oracle_price_lots);
|
||||
let ask = book.impact_price(Side::Ask, self.impact_quantity, now_ts, oracle_price_lots);
|
||||
|
||||
let diff_price = match (bid, ask) {
|
||||
(Some(bid), Some(ask)) => {
|
||||
|
@ -247,8 +248,7 @@ impl PerpMarket {
|
|||
oracle_config: OracleConfig {
|
||||
conf_filter: I80F48::ZERO,
|
||||
},
|
||||
bids: Pubkey::new_unique(),
|
||||
asks: Pubkey::new_unique(),
|
||||
orderbook: Pubkey::new_unique(),
|
||||
event_queue: Pubkey::new_unique(),
|
||||
quote_lot_size: 1,
|
||||
base_lot_size: 1,
|
||||
|
@ -274,6 +274,7 @@ impl PerpMarket {
|
|||
reserved: [0; 92],
|
||||
padding1: Default::default(),
|
||||
padding2: Default::default(),
|
||||
padding3: Default::default(),
|
||||
registration_time: 0,
|
||||
fee_penalty: 0.0,
|
||||
trusted_market: 0,
|
||||
|
|
|
@ -2197,8 +2197,7 @@ pub struct PerpCreateMarketInstruction {
|
|||
pub group: Pubkey,
|
||||
pub admin: TestKeypair,
|
||||
pub oracle: Pubkey,
|
||||
pub asks: Pubkey,
|
||||
pub bids: Pubkey,
|
||||
pub orderbook: Pubkey,
|
||||
pub event_queue: Pubkey,
|
||||
pub payer: TestKeypair,
|
||||
pub settle_token_index: TokenIndex,
|
||||
|
@ -2226,11 +2225,8 @@ impl PerpCreateMarketInstruction {
|
|||
base: &crate::mango_setup::Token,
|
||||
) -> Self {
|
||||
PerpCreateMarketInstruction {
|
||||
asks: solana
|
||||
.create_account_for_type::<BookSide>(&mango_v4::id())
|
||||
.await,
|
||||
bids: solana
|
||||
.create_account_for_type::<BookSide>(&mango_v4::id())
|
||||
orderbook: solana
|
||||
.create_account_for_type::<OrderBook>(&mango_v4::id())
|
||||
.await,
|
||||
event_queue: solana
|
||||
.create_account_for_type::<EventQueue>(&mango_v4::id())
|
||||
|
@ -2293,8 +2289,7 @@ impl ClientInstruction for PerpCreateMarketInstruction {
|
|||
admin: self.admin.pubkey(),
|
||||
oracle: self.oracle,
|
||||
perp_market,
|
||||
asks: self.asks,
|
||||
bids: self.bids,
|
||||
orderbook: self.orderbook,
|
||||
event_queue: self.event_queue,
|
||||
payer: self.payer.pubkey(),
|
||||
system_program: System::id(),
|
||||
|
@ -2310,12 +2305,8 @@ impl ClientInstruction for PerpCreateMarketInstruction {
|
|||
}
|
||||
|
||||
pub struct PerpCloseMarketInstruction {
|
||||
pub group: Pubkey,
|
||||
pub admin: TestKeypair,
|
||||
pub perp_market: Pubkey,
|
||||
pub asks: Pubkey,
|
||||
pub bids: Pubkey,
|
||||
pub event_queue: Pubkey,
|
||||
pub sol_destination: Pubkey,
|
||||
}
|
||||
#[async_trait::async_trait(?Send)]
|
||||
|
@ -2324,18 +2315,18 @@ impl ClientInstruction for PerpCloseMarketInstruction {
|
|||
type Instruction = mango_v4::instruction::PerpCloseMarket;
|
||||
async fn to_instruction(
|
||||
&self,
|
||||
_loader: impl ClientAccountLoader + 'async_trait,
|
||||
account_loader: impl ClientAccountLoader + 'async_trait,
|
||||
) -> (Self::Accounts, instruction::Instruction) {
|
||||
let program_id = mango_v4::id();
|
||||
let instruction = Self::Instruction {};
|
||||
let perp_market: PerpMarket = account_loader.load(&self.perp_market).await.unwrap();
|
||||
|
||||
let accounts = Self::Accounts {
|
||||
group: self.group,
|
||||
group: perp_market.group,
|
||||
admin: self.admin.pubkey(),
|
||||
perp_market: self.perp_market,
|
||||
asks: self.asks,
|
||||
bids: self.bids,
|
||||
event_queue: self.event_queue,
|
||||
orderbook: perp_market.orderbook,
|
||||
event_queue: perp_market.event_queue,
|
||||
token_program: Token::id(),
|
||||
sol_destination: self.sol_destination,
|
||||
};
|
||||
|
@ -2407,9 +2398,9 @@ impl ClientInstruction for PerpPlaceOrderInstruction {
|
|||
max_base_lots: self.max_base_lots,
|
||||
max_quote_lots: self.max_quote_lots,
|
||||
client_order_id: self.client_order_id,
|
||||
order_type: OrderType::Limit,
|
||||
order_type: PlaceOrderType::Limit,
|
||||
expiry_timestamp: 0,
|
||||
limit: 1,
|
||||
limit: 10,
|
||||
};
|
||||
|
||||
let perp_market: PerpMarket = account_loader.load(&self.perp_market).await.unwrap();
|
||||
|
@ -2430,8 +2421,73 @@ impl ClientInstruction for PerpPlaceOrderInstruction {
|
|||
group: account.fixed.group,
|
||||
account: self.account,
|
||||
perp_market: self.perp_market,
|
||||
asks: perp_market.asks,
|
||||
bids: perp_market.bids,
|
||||
orderbook: perp_market.orderbook,
|
||||
event_queue: perp_market.event_queue,
|
||||
oracle: perp_market.oracle,
|
||||
owner: self.owner.pubkey(),
|
||||
};
|
||||
let mut instruction = make_instruction(program_id, &accounts, instruction);
|
||||
instruction.accounts.extend(health_check_metas);
|
||||
|
||||
(accounts, instruction)
|
||||
}
|
||||
|
||||
fn signers(&self) -> Vec<TestKeypair> {
|
||||
vec![self.owner]
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PerpPlaceOrderPeggedInstruction {
|
||||
pub account: Pubkey,
|
||||
pub perp_market: Pubkey,
|
||||
pub owner: TestKeypair,
|
||||
pub side: Side,
|
||||
pub price_offset: i64,
|
||||
pub max_base_lots: i64,
|
||||
pub max_quote_lots: i64,
|
||||
pub client_order_id: u64,
|
||||
pub peg_limit: i64,
|
||||
}
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl ClientInstruction for PerpPlaceOrderPeggedInstruction {
|
||||
type Accounts = mango_v4::accounts::PerpPlaceOrder;
|
||||
type Instruction = mango_v4::instruction::PerpPlaceOrderPegged;
|
||||
async fn to_instruction(
|
||||
&self,
|
||||
account_loader: impl ClientAccountLoader + 'async_trait,
|
||||
) -> (Self::Accounts, instruction::Instruction) {
|
||||
let program_id = mango_v4::id();
|
||||
let instruction = Self::Instruction {
|
||||
side: self.side,
|
||||
price_offset_lots: self.price_offset,
|
||||
peg_limit: self.peg_limit,
|
||||
max_base_lots: self.max_base_lots,
|
||||
max_quote_lots: self.max_quote_lots,
|
||||
client_order_id: self.client_order_id,
|
||||
order_type: PlaceOrderType::Limit,
|
||||
expiry_timestamp: 0,
|
||||
limit: 10,
|
||||
};
|
||||
|
||||
let perp_market: PerpMarket = account_loader.load(&self.perp_market).await.unwrap();
|
||||
let account = account_loader
|
||||
.load_mango_account(&self.account)
|
||||
.await
|
||||
.unwrap();
|
||||
let health_check_metas = derive_health_check_remaining_account_metas(
|
||||
&account_loader,
|
||||
&account,
|
||||
None,
|
||||
false,
|
||||
Some(perp_market.perp_market_index),
|
||||
)
|
||||
.await;
|
||||
|
||||
let accounts = Self::Accounts {
|
||||
group: account.fixed.group,
|
||||
account: self.account,
|
||||
perp_market: self.perp_market,
|
||||
orderbook: perp_market.orderbook,
|
||||
event_queue: perp_market.event_queue,
|
||||
oracle: perp_market.oracle,
|
||||
owner: self.owner.pubkey(),
|
||||
|
@ -2448,13 +2504,10 @@ impl ClientInstruction for PerpPlaceOrderInstruction {
|
|||
}
|
||||
|
||||
pub struct PerpCancelOrderInstruction {
|
||||
pub group: Pubkey,
|
||||
pub account: Pubkey,
|
||||
pub perp_market: Pubkey,
|
||||
pub asks: Pubkey,
|
||||
pub bids: Pubkey,
|
||||
pub owner: TestKeypair,
|
||||
pub order_id: i128,
|
||||
pub order_id: u128,
|
||||
}
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl ClientInstruction for PerpCancelOrderInstruction {
|
||||
|
@ -2462,18 +2515,18 @@ impl ClientInstruction for PerpCancelOrderInstruction {
|
|||
type Instruction = mango_v4::instruction::PerpCancelOrder;
|
||||
async fn to_instruction(
|
||||
&self,
|
||||
_loader: impl ClientAccountLoader + 'async_trait,
|
||||
account_loader: impl ClientAccountLoader + 'async_trait,
|
||||
) -> (Self::Accounts, instruction::Instruction) {
|
||||
let program_id = mango_v4::id();
|
||||
let instruction = Self::Instruction {
|
||||
order_id: self.order_id,
|
||||
};
|
||||
let perp_market: PerpMarket = account_loader.load(&self.perp_market).await.unwrap();
|
||||
let accounts = Self::Accounts {
|
||||
group: self.group,
|
||||
group: perp_market.group,
|
||||
account: self.account,
|
||||
perp_market: self.perp_market,
|
||||
asks: self.asks,
|
||||
bids: self.bids,
|
||||
orderbook: perp_market.orderbook,
|
||||
owner: self.owner.pubkey(),
|
||||
};
|
||||
|
||||
|
@ -2487,11 +2540,8 @@ impl ClientInstruction for PerpCancelOrderInstruction {
|
|||
}
|
||||
|
||||
pub struct PerpCancelOrderByClientOrderIdInstruction {
|
||||
pub group: Pubkey,
|
||||
pub account: Pubkey,
|
||||
pub perp_market: Pubkey,
|
||||
pub asks: Pubkey,
|
||||
pub bids: Pubkey,
|
||||
pub owner: TestKeypair,
|
||||
pub client_order_id: u64,
|
||||
}
|
||||
|
@ -2501,18 +2551,18 @@ impl ClientInstruction for PerpCancelOrderByClientOrderIdInstruction {
|
|||
type Instruction = mango_v4::instruction::PerpCancelOrderByClientOrderId;
|
||||
async fn to_instruction(
|
||||
&self,
|
||||
_loader: impl ClientAccountLoader + 'async_trait,
|
||||
account_loader: impl ClientAccountLoader + 'async_trait,
|
||||
) -> (Self::Accounts, instruction::Instruction) {
|
||||
let program_id = mango_v4::id();
|
||||
let instruction = Self::Instruction {
|
||||
client_order_id: self.client_order_id,
|
||||
};
|
||||
let perp_market: PerpMarket = account_loader.load(&self.perp_market).await.unwrap();
|
||||
let accounts = Self::Accounts {
|
||||
group: self.group,
|
||||
group: perp_market.group,
|
||||
account: self.account,
|
||||
perp_market: self.perp_market,
|
||||
asks: self.asks,
|
||||
bids: self.bids,
|
||||
orderbook: perp_market.orderbook,
|
||||
owner: self.owner.pubkey(),
|
||||
};
|
||||
|
||||
|
@ -2545,8 +2595,7 @@ impl ClientInstruction for PerpCancelAllOrdersInstruction {
|
|||
group: perp_market.group,
|
||||
account: self.account,
|
||||
perp_market: self.perp_market,
|
||||
asks: perp_market.asks,
|
||||
bids: perp_market.bids,
|
||||
orderbook: perp_market.orderbook,
|
||||
owner: self.owner.pubkey(),
|
||||
};
|
||||
|
||||
|
@ -2598,10 +2647,7 @@ impl ClientInstruction for PerpConsumeEventsInstruction {
|
|||
}
|
||||
|
||||
pub struct PerpUpdateFundingInstruction {
|
||||
pub group: Pubkey,
|
||||
pub perp_market: Pubkey,
|
||||
pub bids: Pubkey,
|
||||
pub asks: Pubkey,
|
||||
pub bank: Pubkey,
|
||||
pub oracle: Pubkey,
|
||||
}
|
||||
|
@ -2611,15 +2657,15 @@ impl ClientInstruction for PerpUpdateFundingInstruction {
|
|||
type Instruction = mango_v4::instruction::PerpUpdateFunding;
|
||||
async fn to_instruction(
|
||||
&self,
|
||||
_loader: impl ClientAccountLoader + 'async_trait,
|
||||
account_loader: impl ClientAccountLoader + 'async_trait,
|
||||
) -> (Self::Accounts, instruction::Instruction) {
|
||||
let program_id = mango_v4::id();
|
||||
let instruction = Self::Instruction {};
|
||||
let perp_market: PerpMarket = account_loader.load(&self.perp_market).await.unwrap();
|
||||
let accounts = Self::Accounts {
|
||||
group: self.group,
|
||||
group: perp_market.group,
|
||||
perp_market: self.perp_market,
|
||||
bids: self.bids,
|
||||
asks: self.asks,
|
||||
orderbook: perp_market.orderbook,
|
||||
oracle: self.oracle,
|
||||
};
|
||||
|
||||
|
@ -2781,8 +2827,7 @@ impl ClientInstruction for PerpLiqForceCancelOrdersInstruction {
|
|||
group: account.fixed.group,
|
||||
perp_market: self.perp_market,
|
||||
account: self.account,
|
||||
bids: perp_market.bids,
|
||||
asks: perp_market.asks,
|
||||
orderbook: perp_market.orderbook,
|
||||
oracle: perp_market.oracle,
|
||||
};
|
||||
let mut instruction = make_instruction(program_id, &accounts, instruction);
|
||||
|
|
|
@ -217,19 +217,18 @@ impl SolanaCookie {
|
|||
}
|
||||
|
||||
pub async fn get_account_opt<T: AccountDeserialize>(&self, address: Pubkey) -> Option<T> {
|
||||
self.context
|
||||
.borrow_mut()
|
||||
.banks_client
|
||||
.get_account(address)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
let data = self.get_account_data(address).await?;
|
||||
let mut data_slice: &[u8] = &data;
|
||||
AccountDeserialize::try_deserialize(&mut data_slice).ok()
|
||||
}
|
||||
|
||||
// Use when accounts are too big for the stack
|
||||
pub async fn get_account_boxed<T: AccountDeserialize>(&self, address: Pubkey) -> Box<T> {
|
||||
let data = self.get_account_data(address).await.unwrap();
|
||||
let mut data_slice: &[u8] = &data;
|
||||
Box::new(AccountDeserialize::try_deserialize(&mut data_slice).unwrap())
|
||||
}
|
||||
|
||||
pub async fn get_account<T: AccountDeserialize>(&self, address: Pubkey) -> T {
|
||||
self.get_account_opt(address).await.unwrap()
|
||||
}
|
||||
|
|
|
@ -203,13 +203,7 @@ async fn test_health_compute_perp() -> Result<(), TransportError> {
|
|||
//
|
||||
let mut perp_markets = vec![];
|
||||
for (perp_market_index, token) in tokens[1..].iter().enumerate() {
|
||||
let mango_v4::accounts::PerpCreateMarket {
|
||||
perp_market,
|
||||
asks,
|
||||
bids,
|
||||
event_queue,
|
||||
..
|
||||
} = send_tx(
|
||||
let mango_v4::accounts::PerpCreateMarket { perp_market, .. } = send_tx(
|
||||
solana,
|
||||
PerpCreateMarketInstruction {
|
||||
group,
|
||||
|
@ -231,18 +225,18 @@ async fn test_health_compute_perp() -> Result<(), TransportError> {
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
perp_markets.push((perp_market, asks, bids, event_queue));
|
||||
perp_markets.push(perp_market);
|
||||
}
|
||||
|
||||
let price_lots = {
|
||||
let perp_market = solana.get_account::<PerpMarket>(perp_markets[0].0).await;
|
||||
let perp_market = solana.get_account::<PerpMarket>(perp_markets[0]).await;
|
||||
perp_market.native_price_to_lot(I80F48::from(1))
|
||||
};
|
||||
|
||||
//
|
||||
// TEST: Create a perp order for each market
|
||||
//
|
||||
for (i, &(perp_market, _asks, _bids, _event_queue)) in perp_markets.iter().enumerate() {
|
||||
for (i, &perp_market) in perp_markets.iter().enumerate() {
|
||||
println!("adding market {}", i);
|
||||
send_tx(
|
||||
solana,
|
||||
|
|
|
@ -13,7 +13,7 @@ use utils::assert_equal_fixed_f64 as assert_equal;
|
|||
mod program_test;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_perp() -> Result<(), TransportError> {
|
||||
async fn test_perp_fixed() -> Result<(), TransportError> {
|
||||
let context = TestContext::new().await;
|
||||
let solana = &context.solana.clone();
|
||||
|
||||
|
@ -67,9 +67,7 @@ async fn test_perp() -> Result<(), TransportError> {
|
|||
//
|
||||
let mango_v4::accounts::PerpCreateMarket {
|
||||
perp_market,
|
||||
asks,
|
||||
bids,
|
||||
event_queue,
|
||||
orderbook,
|
||||
..
|
||||
} = send_tx(
|
||||
solana,
|
||||
|
@ -118,19 +116,18 @@ async fn test_perp() -> Result<(), TransportError> {
|
|||
.unwrap();
|
||||
check_prev_instruction_post_health(&solana, account_0).await;
|
||||
|
||||
let orderbook_data = solana.get_account_boxed::<OrderBook>(orderbook).await;
|
||||
assert_eq!(orderbook_data.bids.fixed.leaf_count, 1);
|
||||
let order_id_to_cancel = solana
|
||||
.get_account::<MangoAccount>(account_0)
|
||||
.await
|
||||
.perp_open_orders[0]
|
||||
.order_id;
|
||||
.id;
|
||||
send_tx(
|
||||
solana,
|
||||
PerpCancelOrderInstruction {
|
||||
group,
|
||||
account: account_0,
|
||||
perp_market,
|
||||
asks,
|
||||
bids,
|
||||
owner,
|
||||
order_id: order_id_to_cancel,
|
||||
},
|
||||
|
@ -163,11 +160,8 @@ async fn test_perp() -> Result<(), TransportError> {
|
|||
send_tx(
|
||||
solana,
|
||||
PerpCancelOrderByClientOrderIdInstruction {
|
||||
group,
|
||||
account: account_0,
|
||||
perp_market,
|
||||
asks,
|
||||
bids,
|
||||
owner,
|
||||
client_order_id: 1,
|
||||
},
|
||||
|
@ -198,12 +192,13 @@ async fn test_perp() -> Result<(), TransportError> {
|
|||
check_prev_instruction_post_health(&solana, account_0).await;
|
||||
send_tx(
|
||||
solana,
|
||||
PerpPlaceOrderInstruction {
|
||||
PerpPlaceOrderPeggedInstruction {
|
||||
account: account_0,
|
||||
perp_market,
|
||||
owner,
|
||||
side: Side::Bid,
|
||||
price_lots,
|
||||
price_offset: -1,
|
||||
peg_limit: -1,
|
||||
max_base_lots: 1,
|
||||
max_quote_lots: i64::MAX,
|
||||
client_order_id: 3,
|
||||
|
@ -445,12 +440,8 @@ async fn test_perp() -> Result<(), TransportError> {
|
|||
send_tx(
|
||||
solana,
|
||||
PerpCloseMarketInstruction {
|
||||
group,
|
||||
admin,
|
||||
perp_market,
|
||||
asks,
|
||||
bids,
|
||||
event_queue,
|
||||
sol_destination: payer.pubkey(),
|
||||
},
|
||||
)
|
||||
|
@ -460,13 +451,433 @@ async fn test_perp() -> Result<(), TransportError> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_perp_oracle_peg() -> Result<(), TransportError> {
|
||||
let context = TestContext::new().await;
|
||||
let solana = &context.solana.clone();
|
||||
|
||||
let admin = TestKeypair::new();
|
||||
let owner = context.users[0].key;
|
||||
let payer = context.users[1].key;
|
||||
let mints = &context.mints[0..2];
|
||||
|
||||
//
|
||||
// SETUP: Create a group and an account
|
||||
//
|
||||
|
||||
let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
|
||||
admin,
|
||||
payer,
|
||||
mints: mints.to_vec(),
|
||||
..GroupWithTokensConfig::default()
|
||||
}
|
||||
.create(solana)
|
||||
.await;
|
||||
|
||||
let deposit_amount = 100000;
|
||||
let account_0 = create_funded_account(
|
||||
&solana,
|
||||
group,
|
||||
owner,
|
||||
0,
|
||||
&context.users[1],
|
||||
mints,
|
||||
deposit_amount,
|
||||
0,
|
||||
)
|
||||
.await;
|
||||
let account_1 = create_funded_account(
|
||||
&solana,
|
||||
group,
|
||||
owner,
|
||||
1,
|
||||
&context.users[1],
|
||||
mints,
|
||||
deposit_amount,
|
||||
0,
|
||||
)
|
||||
.await;
|
||||
|
||||
//
|
||||
// SETUP: Create a perp market
|
||||
//
|
||||
let mango_v4::accounts::PerpCreateMarket {
|
||||
perp_market,
|
||||
orderbook,
|
||||
..
|
||||
} = send_tx(
|
||||
solana,
|
||||
PerpCreateMarketInstruction {
|
||||
group,
|
||||
admin,
|
||||
payer,
|
||||
perp_market_index: 0,
|
||||
quote_lot_size: 10,
|
||||
base_lot_size: 10000,
|
||||
maint_asset_weight: 0.975,
|
||||
init_asset_weight: 0.95,
|
||||
maint_liab_weight: 1.025,
|
||||
init_liab_weight: 1.05,
|
||||
liquidation_fee: 0.012,
|
||||
maker_fee: -0.0001,
|
||||
taker_fee: 0.0002,
|
||||
..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, &tokens[0]).await
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let price_lots = {
|
||||
let perp_market = solana.get_account::<PerpMarket>(perp_market).await;
|
||||
perp_market.native_price_to_lot(I80F48!(1))
|
||||
};
|
||||
assert_eq!(price_lots, 1000);
|
||||
|
||||
//
|
||||
// TEST: Place and cancel order with order_id
|
||||
//
|
||||
send_tx(
|
||||
solana,
|
||||
PerpPlaceOrderPeggedInstruction {
|
||||
account: account_0,
|
||||
perp_market,
|
||||
owner,
|
||||
side: Side::Bid,
|
||||
price_offset: -1,
|
||||
peg_limit: -1,
|
||||
max_base_lots: 1,
|
||||
max_quote_lots: i64::MAX,
|
||||
client_order_id: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
check_prev_instruction_post_health(&solana, account_0).await;
|
||||
|
||||
let orderbook_data = solana.get_account_boxed::<OrderBook>(orderbook).await;
|
||||
assert_eq!(orderbook_data.bids.oracle_pegged.leaf_count, 1);
|
||||
let perp_order = solana
|
||||
.get_account::<MangoAccount>(account_0)
|
||||
.await
|
||||
.perp_open_orders[0];
|
||||
assert_eq!(perp_order.side_and_tree, SideAndOrderTree::BidOraclePegged);
|
||||
send_tx(
|
||||
solana,
|
||||
PerpCancelOrderInstruction {
|
||||
account: account_0,
|
||||
perp_market,
|
||||
owner,
|
||||
order_id: perp_order.id,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_no_perp_orders(solana, account_0).await;
|
||||
|
||||
//
|
||||
// TEST: Place a pegged bid, take it with a direct and pegged ask, and consume events
|
||||
//
|
||||
send_tx(
|
||||
solana,
|
||||
PerpPlaceOrderPeggedInstruction {
|
||||
account: account_0,
|
||||
perp_market,
|
||||
owner,
|
||||
side: Side::Bid,
|
||||
price_offset: 0,
|
||||
peg_limit: -1,
|
||||
max_base_lots: 2,
|
||||
max_quote_lots: i64::MAX,
|
||||
client_order_id: 5,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
check_prev_instruction_post_health(&solana, account_0).await;
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
PerpPlaceOrderInstruction {
|
||||
account: account_1,
|
||||
perp_market,
|
||||
owner,
|
||||
side: Side::Ask,
|
||||
price_lots,
|
||||
max_base_lots: 1,
|
||||
max_quote_lots: i64::MAX,
|
||||
client_order_id: 6,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
check_prev_instruction_post_health(&solana, account_1).await;
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
PerpPlaceOrderPeggedInstruction {
|
||||
account: account_1,
|
||||
perp_market,
|
||||
owner,
|
||||
side: Side::Ask,
|
||||
price_offset: 0,
|
||||
peg_limit: -1,
|
||||
max_base_lots: 1,
|
||||
max_quote_lots: i64::MAX,
|
||||
client_order_id: 7,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
check_prev_instruction_post_health(&solana, account_1).await;
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
PerpConsumeEventsInstruction {
|
||||
perp_market,
|
||||
mango_accounts: vec![account_0, account_1],
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
|
||||
assert_eq!(mango_account_0.perps[0].base_position_lots(), 2);
|
||||
assert!(assert_equal(
|
||||
mango_account_0.perps[0].quote_position_native(),
|
||||
-19998.0,
|
||||
0.001
|
||||
));
|
||||
|
||||
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
|
||||
assert_eq!(mango_account_1.perps[0].base_position_lots(), -2);
|
||||
assert!(assert_equal(
|
||||
mango_account_1.perps[0].quote_position_native(),
|
||||
19996.0,
|
||||
0.001
|
||||
));
|
||||
|
||||
//
|
||||
// TEST: Place a pegged order and check how it behaves with oracle changes
|
||||
//
|
||||
send_tx(
|
||||
solana,
|
||||
PerpPlaceOrderPeggedInstruction {
|
||||
account: account_0,
|
||||
perp_market,
|
||||
owner,
|
||||
side: Side::Bid,
|
||||
price_offset: -1,
|
||||
peg_limit: -1,
|
||||
max_base_lots: 2,
|
||||
max_quote_lots: i64::MAX,
|
||||
client_order_id: 5,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// TEST: an ask at current oracle price does not match
|
||||
send_tx(
|
||||
solana,
|
||||
PerpPlaceOrderInstruction {
|
||||
account: account_1,
|
||||
perp_market,
|
||||
owner,
|
||||
side: Side::Ask,
|
||||
price_lots,
|
||||
max_base_lots: 1,
|
||||
max_quote_lots: i64::MAX,
|
||||
client_order_id: 60,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
send_tx(
|
||||
solana,
|
||||
PerpCancelOrderByClientOrderIdInstruction {
|
||||
account: account_1,
|
||||
perp_market,
|
||||
owner,
|
||||
client_order_id: 60,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// TEST: Change the oracle, now the ask matches
|
||||
send_tx(
|
||||
solana,
|
||||
StubOracleSetInstruction {
|
||||
group,
|
||||
admin,
|
||||
mint: mints[0].pubkey,
|
||||
payer,
|
||||
price: "1.002",
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
send_tx(
|
||||
solana,
|
||||
PerpPlaceOrderInstruction {
|
||||
account: account_1,
|
||||
perp_market,
|
||||
owner,
|
||||
side: Side::Ask,
|
||||
price_lots,
|
||||
max_base_lots: 2,
|
||||
max_quote_lots: i64::MAX,
|
||||
client_order_id: 61,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
send_tx(
|
||||
solana,
|
||||
PerpConsumeEventsInstruction {
|
||||
perp_market,
|
||||
mango_accounts: vec![account_0, account_1],
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_no_perp_orders(solana, account_0).await;
|
||||
|
||||
// restore the oracle to default
|
||||
send_tx(
|
||||
solana,
|
||||
StubOracleSetInstruction {
|
||||
group,
|
||||
admin,
|
||||
mint: mints[0].pubkey,
|
||||
payer,
|
||||
price: "1.0",
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
//
|
||||
// TEST: order is cancelled when the price exceeds the peg limit
|
||||
//
|
||||
send_tx(
|
||||
solana,
|
||||
PerpPlaceOrderPeggedInstruction {
|
||||
account: account_0,
|
||||
perp_market,
|
||||
owner,
|
||||
side: Side::Bid,
|
||||
price_offset: -1,
|
||||
peg_limit: price_lots + 2,
|
||||
max_base_lots: 2,
|
||||
max_quote_lots: i64::MAX,
|
||||
client_order_id: 5,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// order is still matchable when exactly at the peg limit
|
||||
send_tx(
|
||||
solana,
|
||||
StubOracleSetInstruction {
|
||||
group,
|
||||
admin,
|
||||
mint: mints[0].pubkey,
|
||||
payer,
|
||||
price: "1.003",
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
send_tx(
|
||||
solana,
|
||||
PerpPlaceOrderInstruction {
|
||||
account: account_1,
|
||||
perp_market,
|
||||
owner,
|
||||
side: Side::Ask,
|
||||
price_lots: price_lots + 2,
|
||||
max_base_lots: 1,
|
||||
max_quote_lots: i64::MAX,
|
||||
client_order_id: 62,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(send_tx(
|
||||
solana,
|
||||
PerpCancelOrderByClientOrderIdInstruction {
|
||||
account: account_1,
|
||||
perp_market,
|
||||
owner,
|
||||
client_order_id: 62,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.is_err());
|
||||
|
||||
// but once the adjusted price is > the peg limit, it's gone
|
||||
send_tx(
|
||||
solana,
|
||||
StubOracleSetInstruction {
|
||||
group,
|
||||
admin,
|
||||
mint: mints[0].pubkey,
|
||||
payer,
|
||||
price: "1.004",
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
send_tx(
|
||||
solana,
|
||||
PerpPlaceOrderInstruction {
|
||||
account: account_1,
|
||||
perp_market,
|
||||
owner,
|
||||
side: Side::Ask,
|
||||
price_lots: price_lots + 3,
|
||||
max_base_lots: 1,
|
||||
max_quote_lots: i64::MAX,
|
||||
client_order_id: 63,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
send_tx(
|
||||
solana,
|
||||
PerpCancelOrderByClientOrderIdInstruction {
|
||||
account: account_1,
|
||||
perp_market,
|
||||
owner,
|
||||
client_order_id: 63,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
send_tx(
|
||||
solana,
|
||||
PerpConsumeEventsInstruction {
|
||||
perp_market,
|
||||
mango_accounts: vec![account_0, account_1],
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_no_perp_orders(solana, account_0).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn assert_no_perp_orders(solana: &SolanaCookie, account_0: Pubkey) {
|
||||
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
|
||||
|
||||
for oo in mango_account_0.perp_open_orders.iter() {
|
||||
assert!(oo.order_id == 0);
|
||||
assert!(oo.order_side == Side::Bid);
|
||||
assert!(oo.client_order_id == 0);
|
||||
assert!(oo.order_market == FREE_ORDER_SLOT);
|
||||
assert!(oo.id == 0);
|
||||
assert!(oo.side_and_tree == SideAndOrderTree::BidFixed);
|
||||
assert!(oo.client_id == 0);
|
||||
assert!(oo.market == FREE_ORDER_SLOT);
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue