mango-v4/programs/mango-v4/src/state/orderbook/book.rs

465 lines
16 KiB
Rust

use std::cell::RefMut;
use crate::{
error::MangoError,
state::{
orderbook::{bookside::BookSide, nodes::LeafNode},
EventQueue, MangoAccount, MangoAccountPerps, PerpMarket, FREE_ORDER_SLOT,
MAX_PERP_OPEN_ORDERS,
},
util::LoadZeroCopy,
};
use anchor_lang::prelude::*;
use bytemuck::cast;
use fixed::types::I80F48;
use super::{
nodes::NodeHandle,
order_type::{OrderType, Side},
FillEvent, OutEvent,
};
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,
}
}
/// 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 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 {
bids: bids_ai.load_mut::<BookSide>()?,
asks: asks_ai.load_mut::<BookSide>()?,
})
}
pub fn get_bookside(&mut self, side: Side) -> &mut BookSide {
match side {
Side::Bid => &mut self.bids,
Side::Ask => &mut self.asks,
}
}
/// Returns best valid bid
pub fn get_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 get_best_ask_price(&self, now_ts: u64) -> Option<i64> {
Some(self.asks.iter_valid(now_ts).next()?.1.price())
}
pub fn get_best_price(&self, now_ts: u64, side: Side) -> Option<i64> {
match side {
Side::Bid => self.get_best_bid_price(now_ts),
Side::Ask => self.get_best_ask_price(now_ts),
}
}
/// Get the quantity of valid bids above and including the price
pub fn get_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)
}
/// Walk up the book `quantity` units and return the price at that level. If `quantity` units
/// not on book, return None
pub fn get_impact_price(&self, side: Side, quantity: i64, now_ts: u64) -> 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();
if sum >= quantity {
return Some(order.price());
}
}
None
}
/// Get the quantity of valid asks below and including the price
pub fn get_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 get_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 get_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,
perp_market: &mut PerpMarket,
event_queue: &mut EventQueue,
oracle_price: I80F48,
mango_account_perps: &mut MangoAccountPerps,
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 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.get_best_price(now_ts, other_side)
{
post_only_slide_limit(side, best_other_price, price_lots)
} else {
price_lots
};
(true, true, price)
}
};
if post_allowed {
// 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;
}
}
// generate new order id
let order_id = market.gen_order_id(side, price_lots);
// 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 number_of_dropped_expired_orders = 0;
let opposing_bookside = self.get_bookside(other_side);
for (best_opposing_h, best_opposing) in opposing_bookside.iter_all_including_invalid() {
if !best_opposing.is_valid(now_ts) {
// 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,
now_ts,
event_queue.header.seq_num,
best_opposing.owner,
best_opposing.quantity,
);
event_queue.push_back(cast(event)).unwrap();
matched_order_deletes.push(best_opposing.key);
}
continue;
}
let best_opposing_price = best_opposing.price();
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;
break; // return silently to not fail other instructions in tx
} else if limit == 0 {
msg!("Order matching limit reached");
post_allowed = false;
break;
}
let max_match_by_quote = remaining_quote_lots / best_opposing_price;
let match_base_lots = remaining_base_lots
.min(best_opposing.quantity)
.min(max_match_by_quote);
let done =
match_base_lots == max_match_by_quote || match_base_lots == remaining_base_lots;
let match_quote_lots = cm!(match_base_lots * best_opposing_price);
remaining_base_lots = cm!(remaining_base_lots - match_base_lots);
remaining_quote_lots = cm!(remaining_quote_lots - match_quote_lots);
let new_best_opposing_quantity = cm!(best_opposing.quantity - match_base_lots);
let maker_out = new_best_opposing_quantity == 0;
if maker_out {
matched_order_deletes.push(best_opposing.key);
} else {
matched_order_changes.push((best_opposing_h, new_best_opposing_quantity));
}
// Record the taker trade in the account already, even though it will only be
// realized when the fill event gets executed
let perp_account = mango_account_perps
.get_account_mut_or_create(market.perp_market_index)?
.0;
perp_account.add_taker_trade(side, match_base_lots, match_quote_lots);
let fill = FillEvent::new(
side,
maker_out,
best_opposing.owner_slot,
now_ts,
event_queue.header.seq_num,
best_opposing.owner,
best_opposing.key,
best_opposing.client_order_id,
market.maker_fee,
best_opposing.timestamp,
*mango_account_pk,
order_id,
client_order_id,
market.taker_fee,
best_opposing_price,
match_base_lots,
);
event_queue.push_back(cast(fill)).unwrap();
limit -= 1;
if done {
break;
}
}
let total_quote_lots_taken = cm!(max_quote_lots - remaining_quote_lots);
// Apply changes to matched asks (handles invalidate on delete!)
for (handle, new_quantity) in matched_order_changes {
opposing_bookside
.get_mut(handle)
.unwrap()
.as_leaf_mut()
.unwrap()
.quantity = new_quantity;
}
for key in matched_order_deletes {
let _removed_leaf = opposing_bookside.remove_by_key(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 {
// Drop an expired order if possible
let bookside = self.get_bookside(side);
if let Some(expired_order) = bookside.remove_one_expired(now_ts) {
let event = OutEvent::new(
side,
expired_order.owner_slot,
now_ts,
event_queue.header.seq_num,
expired_order.owner,
expired_order.quantity,
);
event_queue.push_back(cast(event)).unwrap();
}
if bookside.is_full() {
// If this bid is higher than lowest bid, boot that bid and insert this one
let worst_order = bookside.remove_worst().unwrap();
// MangoErrorCode::OutOfSpace
require!(
side.is_price_better(price_lots, worst_order.price()),
MangoError::SomeError
);
let event = OutEvent::new(
side,
worst_order.owner_slot,
now_ts,
event_queue.header.seq_num,
worst_order.owner,
worst_order.quantity,
);
event_queue.push_back(cast(event)).unwrap();
}
let owner_slot = mango_account_perps
.next_order_slot()
.ok_or_else(|| error!(MangoError::SomeError))?;
let new_order = LeafNode::new(
owner_slot as u8,
order_id,
*mango_account_pk,
book_base_quantity,
client_order_id,
now_ts,
order_type,
time_in_force,
);
let _result = bookside.insert_leaf(&new_order)?;
// TODO OPT remove if PlacePerpOrder needs more compute
msg!(
"{} on book order_id={} quantity={} price={}",
match side {
Side::Bid => "bid",
Side::Ask => "ask",
},
order_id,
book_base_quantity,
price_lots
);
mango_account_perps.add_order(market.perp_market_index, side, &new_order)?;
}
// if there were matched taker quote apply ref fees
// we know ref_fee_rate is not None if total_quote_taken > 0
if total_quote_lots_taken > 0 {
apply_fees(market, mango_account_perps, total_quote_lots_taken)?;
}
Ok(())
}
pub fn cancel_all_order(
&mut self,
mango_account: &mut MangoAccount,
perp_market: &mut PerpMarket,
mut limit: u8,
side_to_cancel_option: Option<Side>,
) -> Result<()> {
for i in 0..MAX_PERP_OPEN_ORDERS {
if mango_account.perps.order_market[i] == FREE_ORDER_SLOT
|| mango_account.perps.order_market[i] != perp_market.perp_market_index
{
continue;
}
let order_id = mango_account.perps.order_id[i];
let order_side = mango_account.perps.order_side[i];
if let Some(side_to_cancel) = side_to_cancel_option {
if side_to_cancel != order_side {
continue;
}
}
if let Ok(leaf_node) = self.cancel_order(order_id, order_side) {
mango_account
.perps
.remove_order(leaf_node.owner_slot as usize, leaf_node.quantity)?
};
limit -= 1;
if limit == 0 {
break;
}
}
Ok(())
}
pub fn cancel_order(&mut self, order_id: i128, side: Side) -> Result<LeafNode> {
match side {
Side::Bid => self
.bids
.remove_by_key(order_id)
.ok_or_else(|| error!(MangoError::SomeError)), // InvalidOrderId
Side::Ask => self
.asks
.remove_by_key(order_id)
.ok_or_else(|| error!(MangoError::SomeError)), // InvalidOrderId
}
}
}
/// Apply taker fees to the taker account and update the markets' fees_accrued for
/// both the maker and taker fees.
fn apply_fees(
market: &mut PerpMarket,
mango_account_perps: &mut MangoAccountPerps,
total_quote_taken: i64,
) -> Result<()> {
let taker_quote_native = I80F48::from_num(
market
.quote_lot_size
.checked_mul(total_quote_taken)
.unwrap(),
);
// Track maker fees immediately: they can be negative and applying them later
// risks that fees_accrued is settled to 0 before they apply. It going negative
// breaks assumptions.
// The maker fees apply to the maker's account only when the fill event is consumed.
let maker_fees = taker_quote_native * market.maker_fee;
let taker_fees = taker_quote_native * market.taker_fee;
let perp_account = mango_account_perps
.get_account_mut_or_create(market.perp_market_index)?
.0;
perp_account.quote_position_native -= taker_fees;
market.fees_accrued += taker_fees + maker_fees;
Ok(())
}