2022-03-21 12:29:28 -07:00
|
|
|
use std::cell::RefMut;
|
|
|
|
|
|
|
|
use crate::{
|
|
|
|
error::MangoError,
|
|
|
|
state::{
|
|
|
|
orderbook::{bookside::BookSide, nodes::LeafNode},
|
2022-03-24 06:08:10 -07:00
|
|
|
EventQueue, PerpMarket,
|
2022-03-21 12:29:28 -07:00
|
|
|
},
|
2022-03-24 08:12:55 -07:00
|
|
|
util::LoadZeroCopy,
|
2022-03-21 12:29:28 -07:00
|
|
|
};
|
|
|
|
use anchor_lang::prelude::*;
|
|
|
|
use fixed::types::I80F48;
|
|
|
|
use fixed_macro::types::I80F48;
|
|
|
|
|
|
|
|
use super::{
|
|
|
|
nodes::NodeHandle,
|
|
|
|
order_type::{OrderType, Side},
|
|
|
|
};
|
2022-03-22 02:39:51 -07:00
|
|
|
use crate::util::checked_math as cm;
|
2022-03-21 12:29:28 -07:00
|
|
|
|
|
|
|
pub const CENTIBPS_PER_UNIT: I80F48 = I80F48!(1_000_000);
|
|
|
|
// todo move to a constants module or something
|
|
|
|
pub const MAX_PERP_OPEN_ORDERS: usize = 64;
|
|
|
|
|
|
|
|
/// 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;
|
|
|
|
|
|
|
|
pub struct Book<'a> {
|
|
|
|
pub bids: RefMut<'a, BookSide>, // todo: why refmut?
|
|
|
|
pub asks: RefMut<'a, BookSide>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a> Book<'a> {
|
2022-03-24 08:12:55 -07:00
|
|
|
pub fn load_mut(
|
2022-03-21 12:29:28 -07:00
|
|
|
bids_ai: &'a AccountInfo,
|
|
|
|
asks_ai: &'a AccountInfo,
|
|
|
|
perp_market: &PerpMarket,
|
|
|
|
) -> std::result::Result<Self, Error> {
|
2022-03-24 07:29:42 -07:00
|
|
|
require!(bids_ai.key == &perp_market.bids, MangoError::SomeError);
|
|
|
|
require!(asks_ai.key == &perp_market.asks, MangoError::SomeError);
|
2022-03-21 12:29:28 -07:00
|
|
|
Ok(Self {
|
2022-03-24 08:12:55 -07:00
|
|
|
bids: bids_ai.load_mut::<BookSide>()?,
|
|
|
|
asks: asks_ai.load_mut::<BookSide>()?,
|
2022-03-21 12:29:28 -07:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
/// 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())
|
|
|
|
}
|
|
|
|
|
|
|
|
/// 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 s = 0;
|
|
|
|
for (_, bid) in self.bids.iter_valid(now_ts) {
|
|
|
|
if price > bid.price() || s >= max_depth {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
s += bid.quantity;
|
|
|
|
}
|
|
|
|
s.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 s = 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 {
|
|
|
|
s += order.quantity;
|
|
|
|
if s >= 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)
|
|
|
|
}
|
|
|
|
|
|
|
|
// todo: can new_bid and new_ask be elegantly folded into one method?
|
|
|
|
#[inline(never)]
|
2022-03-23 01:33:51 -07:00
|
|
|
#[allow(clippy::too_many_arguments)]
|
2022-03-21 12:29:28 -07:00
|
|
|
pub fn new_bid(
|
|
|
|
&mut self,
|
|
|
|
// program_id: &Pubkey,
|
|
|
|
// mango_group: &MangoGroup,
|
|
|
|
// mango_group_pk: &Pubkey,
|
|
|
|
// mango_cache: &MangoCache,
|
2022-03-24 06:40:08 -07:00
|
|
|
_event_queue: &mut EventQueue,
|
2022-03-21 12:29:28 -07:00
|
|
|
market: &mut PerpMarket,
|
2022-03-24 09:29:30 -07:00
|
|
|
oracle_price: I80F48,
|
2022-03-22 03:19:12 -07:00
|
|
|
// mango_account: &mut MangoAccount,
|
2022-03-21 12:29:28 -07:00
|
|
|
mango_account_pk: &Pubkey,
|
|
|
|
// market_index: usize,
|
|
|
|
price: i64,
|
|
|
|
max_base_quantity: i64, // guaranteed to be greater than zero due to initial check
|
|
|
|
max_quote_quantity: i64, // guaranteed to be greater than zero due to initial check
|
|
|
|
order_type: OrderType,
|
|
|
|
time_in_force: u8,
|
|
|
|
client_order_id: u64,
|
|
|
|
now_ts: u64,
|
|
|
|
// referrer_mango_account_ai: Option<&AccountInfo>,
|
|
|
|
mut limit: u8, // max number of FillEvents allowed; guaranteed to be greater than 0
|
|
|
|
) -> std::result::Result<(), Error> {
|
|
|
|
// TODO proper error handling
|
|
|
|
// TODO handle the case where we run out of compute (right now just fails)
|
|
|
|
let (post_only, mut post_allowed, price) = match order_type {
|
|
|
|
OrderType::Limit => (false, true, price),
|
|
|
|
OrderType::ImmediateOrCancel => (false, false, price),
|
|
|
|
OrderType::PostOnly => (true, true, price),
|
|
|
|
OrderType::Market => (false, false, i64::MAX),
|
|
|
|
OrderType::PostOnlySlide => {
|
|
|
|
let price = if let Some(best_ask_price) = self.get_best_ask_price(now_ts) {
|
|
|
|
price.min(best_ask_price.checked_sub(1).ok_or(MangoError::SomeError)?)
|
|
|
|
// math_err
|
|
|
|
} else {
|
|
|
|
price
|
|
|
|
};
|
|
|
|
(true, true, price)
|
|
|
|
}
|
|
|
|
};
|
2022-03-24 09:29:30 -07:00
|
|
|
|
|
|
|
if post_allowed {
|
|
|
|
// price limit check computed lazily to save CU on average
|
|
|
|
let native_price = market.lot_to_native_price(price);
|
|
|
|
if native_price.checked_div(oracle_price).unwrap() > market.maint_liab_weight {
|
|
|
|
msg!("Posting on book disallowed due to price limits");
|
|
|
|
post_allowed = false;
|
|
|
|
}
|
|
|
|
}
|
2022-03-21 12:29:28 -07:00
|
|
|
|
|
|
|
// referral fee related variables
|
|
|
|
// let mut ref_fee_rate = None;
|
|
|
|
// let mut referrer_mango_account_opt = None;
|
|
|
|
|
|
|
|
// generate new order id
|
|
|
|
let order_id = market.gen_order_id(Side::Bid, price);
|
|
|
|
|
|
|
|
// Iterate through book and match against this new bid
|
|
|
|
//
|
|
|
|
// Any changes to matching asks are collected in ask_changes
|
|
|
|
// and then applied after this loop.
|
|
|
|
let mut rem_base_quantity = max_base_quantity; // base lots (aka contracts)
|
|
|
|
let mut rem_quote_quantity = max_quote_quantity;
|
|
|
|
let mut ask_changes: Vec<(NodeHandle, i64)> = vec![];
|
|
|
|
let mut ask_deletes: Vec<i128> = vec![];
|
|
|
|
let mut number_of_dropped_expired_orders = 0;
|
|
|
|
for (best_ask_h, best_ask) in self.asks.iter_all_including_invalid() {
|
|
|
|
if !best_ask.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;
|
2022-03-24 06:08:10 -07:00
|
|
|
// let event = OutEvent::new(
|
|
|
|
// Side::Ask,
|
|
|
|
// best_ask.owner_slot,
|
|
|
|
// now_ts,
|
|
|
|
// event_queue.header.seq_num,
|
|
|
|
// best_ask.owner,
|
|
|
|
// best_ask.quantity,
|
|
|
|
// );
|
|
|
|
// event_queue.push_back(cast(event)).unwrap();
|
2022-03-22 23:53:45 -07:00
|
|
|
ask_deletes.push(best_ask.key);
|
2022-03-21 12:29:28 -07:00
|
|
|
}
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
let best_ask_price = best_ask.price();
|
|
|
|
|
|
|
|
if price < best_ask_price {
|
|
|
|
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 = rem_quote_quantity / best_ask_price;
|
|
|
|
let match_quantity = rem_base_quantity
|
|
|
|
.min(best_ask.quantity)
|
|
|
|
.min(max_match_by_quote);
|
|
|
|
let done = match_quantity == max_match_by_quote || match_quantity == rem_base_quantity;
|
|
|
|
|
2022-03-22 02:39:51 -07:00
|
|
|
let match_quote = cm!(match_quantity * best_ask_price);
|
|
|
|
rem_base_quantity = cm!(rem_base_quantity - match_quantity);
|
|
|
|
rem_quote_quantity = cm!(rem_quote_quantity - match_quote);
|
2022-03-21 12:29:28 -07:00
|
|
|
// mango_account.perp_accounts[market_index].add_taker_trade(match_quantity, -match_quote);
|
|
|
|
|
2022-03-22 02:39:51 -07:00
|
|
|
let new_best_ask_quantity = cm!(best_ask.quantity - match_quantity);
|
2022-03-21 12:29:28 -07:00
|
|
|
let maker_out = new_best_ask_quantity == 0;
|
|
|
|
if maker_out {
|
|
|
|
ask_deletes.push(best_ask.key);
|
|
|
|
} else {
|
|
|
|
ask_changes.push((best_ask_h, new_best_ask_quantity));
|
|
|
|
}
|
|
|
|
|
|
|
|
// todo
|
|
|
|
// if ref_fee_rate is none, determine it
|
|
|
|
// if ref_valid, then pay into referrer, else pay to perp market
|
|
|
|
// if ref_fee_rate.is_none() {
|
|
|
|
// let (a, b) = determine_ref_vars(
|
|
|
|
// program_id,
|
|
|
|
// mango_group,
|
|
|
|
// mango_group_pk,
|
|
|
|
// mango_cache,
|
|
|
|
// mango_account,
|
|
|
|
// referrer_mango_account_ai,
|
|
|
|
// now_ts,
|
|
|
|
// )?;
|
|
|
|
// ref_fee_rate = Some(a);
|
|
|
|
// referrer_mango_account_opt = b;
|
|
|
|
// }
|
|
|
|
|
|
|
|
// let fill = FillEvent::new(
|
|
|
|
// Side::Bid,
|
|
|
|
// best_ask.owner_slot,
|
|
|
|
// maker_out,
|
|
|
|
// now_ts,
|
|
|
|
// event_queue.header.seq_num,
|
|
|
|
// best_ask.owner,
|
|
|
|
// best_ask.key,
|
|
|
|
// best_ask.client_order_id,
|
|
|
|
// info.maker_fee,
|
|
|
|
// best_ask.best_initial,
|
|
|
|
// best_ask.timestamp,
|
|
|
|
// *mango_account_pk,
|
|
|
|
// order_id,
|
|
|
|
// client_order_id,
|
|
|
|
// info.taker_fee + ref_fee_rate.unwrap(),
|
|
|
|
// best_ask_price,
|
|
|
|
// match_quantity,
|
|
|
|
// best_ask.version,
|
|
|
|
// );
|
|
|
|
// event_queue.push_back(cast(fill)).unwrap();
|
|
|
|
limit -= 1;
|
|
|
|
|
|
|
|
if done {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2022-03-22 03:19:12 -07:00
|
|
|
// let total_quote_taken = cm!(max_quote_quantity - rem_quote_quantity);
|
2022-03-21 12:29:28 -07:00
|
|
|
|
|
|
|
// Apply changes to matched asks (handles invalidate on delete!)
|
|
|
|
for (handle, new_quantity) in ask_changes {
|
|
|
|
self.asks
|
|
|
|
.get_mut(handle)
|
|
|
|
.unwrap()
|
|
|
|
.as_leaf_mut()
|
|
|
|
.unwrap()
|
|
|
|
.quantity = new_quantity;
|
|
|
|
}
|
|
|
|
for key in ask_deletes {
|
|
|
|
let _removed_leaf = self.asks.remove_by_key(key).unwrap();
|
|
|
|
}
|
|
|
|
|
|
|
|
// If there are still quantity unmatched, place on the book
|
|
|
|
let book_base_quantity = rem_base_quantity.min(rem_quote_quantity / price);
|
|
|
|
if post_allowed && book_base_quantity > 0 {
|
|
|
|
// Drop an expired order if possible
|
2022-03-22 03:19:12 -07:00
|
|
|
// if let Some(expired_bid) = self.bids.remove_one_expired(now_ts) {
|
|
|
|
// let event = OutEvent::new(
|
|
|
|
// Side::Bid,
|
|
|
|
// expired_bid.owner_slot,
|
|
|
|
// now_ts,
|
|
|
|
// event_queue.header.seq_num,
|
|
|
|
// expired_bid.owner,
|
|
|
|
// expired_bid.quantity,
|
|
|
|
// );
|
|
|
|
// event_queue.push_back(cast(event)).unwrap();
|
|
|
|
// }
|
2022-03-21 12:29:28 -07:00
|
|
|
|
|
|
|
if self.bids.is_full() {
|
|
|
|
// If this bid is higher than lowest bid, boot that bid and insert this one
|
|
|
|
let min_bid = self.bids.remove_min().unwrap();
|
|
|
|
require!(price > min_bid.price(), MangoError::SomeError); // MangoErrorCode::OutOfSpace
|
|
|
|
// let event = OutEvent::new(
|
|
|
|
// Side::Bid,
|
|
|
|
// min_bid.owner_slot,
|
|
|
|
// now_ts,
|
|
|
|
// event_queue.header.seq_num,
|
|
|
|
// min_bid.owner,
|
|
|
|
// min_bid.quantity,
|
|
|
|
// );
|
|
|
|
// event_queue.push_back(cast(event)).unwrap();
|
|
|
|
}
|
|
|
|
|
|
|
|
// iterate through book on the bid side
|
|
|
|
// let best_initial = if market.meta_data.version == 0 {
|
|
|
|
// match self.get_best_bid_price(now_ts) {
|
|
|
|
// None => price,
|
|
|
|
// Some(p) => p,
|
|
|
|
// }
|
|
|
|
// } else {
|
|
|
|
// let max_depth: i64 = market.liquidity_mining_info.max_depth_bps.to_num();
|
|
|
|
// self.get_bids_size_above(price, max_depth, now_ts)
|
|
|
|
// };
|
|
|
|
|
|
|
|
// let owner_slot = mango_account
|
|
|
|
// .next_order_slot()
|
|
|
|
// .ok_or(MangoError::SomeError)?; // TooManyOpenOrders
|
|
|
|
let new_bid = LeafNode::new(
|
|
|
|
1, // todo market.meta_data.version,
|
|
|
|
0, // todo owner_slot as u8,
|
|
|
|
order_id,
|
|
|
|
*mango_account_pk,
|
|
|
|
book_base_quantity,
|
|
|
|
client_order_id,
|
|
|
|
now_ts,
|
|
|
|
0, // todo best_initial,
|
|
|
|
order_type,
|
|
|
|
time_in_force,
|
|
|
|
);
|
|
|
|
let _result = self.bids.insert_leaf(&new_bid)?;
|
|
|
|
|
|
|
|
// TODO OPT remove if PlacePerpOrder needs more compute
|
|
|
|
msg!(
|
|
|
|
"bid on book order_id={} quantity={} price={}",
|
|
|
|
order_id,
|
|
|
|
book_base_quantity,
|
|
|
|
price
|
|
|
|
);
|
|
|
|
// mango_account.add_order(market_index, Side::Bid, &new_bid)?;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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_taken > 0 {
|
|
|
|
// apply_fees(
|
|
|
|
// market,
|
|
|
|
// info,
|
|
|
|
// mango_account,
|
|
|
|
// mango_account_pk,
|
|
|
|
// market_index,
|
|
|
|
// referrer_mango_account_opt,
|
|
|
|
// referrer_mango_account_ai,
|
|
|
|
// total_quote_taken,
|
|
|
|
// ref_fee_rate.unwrap(),
|
|
|
|
// // &mango_cache.perp_market_cache[market_index],
|
|
|
|
// );
|
|
|
|
// }
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
}
|