use crate::logs::{FilledPerpOrderLog, PerpTakerTradeLog}; use crate::state::MangoAccountRefMut; use crate::{ error::*, state::{orderbook::bookside::*, EventQueue, PerpMarket}, }; use anchor_lang::prelude::*; use bytemuck::cast; use fixed::types::I80F48; use std::cell::RefMut; use super::*; /// 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 Orderbook<'a> { pub bids: RefMut<'a, BookSide>, pub asks: RefMut<'a, BookSide>, } impl<'a> Orderbook<'a> { pub fn init(&mut self) { self.bids.nodes.order_tree_type = OrderTreeType::Bids.into(); self.asks.nodes.order_tree_type = OrderTreeType::Asks.into(); } pub fn bookside_mut(&mut self, side: Side) -> &mut BookSide { match side { Side::Bid => &mut self.bids, Side::Ask => &mut self.asks, } } pub fn bookside(&self, side: Side) -> &BookSide { match side { Side::Bid => &self.bids, Side::Ask => &self.asks, } } #[allow(clippy::too_many_arguments)] pub fn new_order( &mut self, order: Order, perp_market: &mut PerpMarket, event_queue: &mut EventQueue, oracle_price: I80F48, mango_account: &mut MangoAccountRefMut, mango_account_pk: &Pubkey, 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 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)?; // generate new order id let order_id = market.gen_order_id(side, price_data); // IOC orders have a fee penalty applied regardless of match let fee_penalty = if order.needs_penalty_fee() { apply_penalty(market, mango_account)? } else { I80F48::ZERO }; let perp_position = mango_account.perp_position_mut(market.perp_market_index)?; // 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 = order.max_base_lots; let mut remaining_quote_lots = order.max_quote_lots; let mut decremented_quote_lots = 0i64; let mut orders_to_change: Vec<(BookSideOrderHandle, i64)> = vec![]; let mut orders_to_delete: Vec<(BookSideOrderTree, u128)> = vec![]; let mut number_of_dropped_expired_orders = 0; let opposing_bookside = self.bookside_mut(other_side); for best_opposing in opposing_bookside.iter_all_including_invalid(now_ts, oracle_price_lots) { if remaining_base_lots == 0 || remaining_quote_lots == 0 { break; } 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.node.owner_slot, now_ts, event_queue.header.seq_num, best_opposing.node.owner, best_opposing.node.quantity, ); event_queue.push_back(cast(event)).unwrap(); orders_to_delete .push((best_opposing.handle.order_tree, best_opposing.node.key)); } continue; } 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_target = None; break; // return silently to not fail other instructions in tx } else if limit == 0 { msg!("Order matching limit reached"); post_target = None; break; } let max_match_by_quote = remaining_quote_lots / best_opposing_price; if max_match_by_quote == 0 { break; } let match_base_lots = remaining_base_lots .min(best_opposing.node.quantity) .min(max_match_by_quote); let match_quote_lots = match_base_lots * best_opposing_price; let order_would_self_trade = *mango_account_pk == best_opposing.node.owner; if order_would_self_trade { match order.self_trade_behavior { SelfTradeBehavior::DecrementTake => { // remember all decremented quote lots to only charge fees on not-self-trades decremented_quote_lots += match_quote_lots; } SelfTradeBehavior::CancelProvide => { let event = OutEvent::new( other_side, best_opposing.node.owner_slot, now_ts, event_queue.header.seq_num, best_opposing.node.owner, best_opposing.node.quantity, ); event_queue.push_back(cast(event)).unwrap(); orders_to_delete .push((best_opposing.handle.order_tree, best_opposing.node.key)); // skip actual matching continue; } SelfTradeBehavior::AbortTransaction => return err!(MangoError::WouldSelfTrade), } assert!(order.self_trade_behavior == SelfTradeBehavior::DecrementTake); } remaining_base_lots -= match_base_lots; remaining_quote_lots -= match_quote_lots; assert!(remaining_quote_lots >= 0); let new_best_opposing_quantity = best_opposing.node.quantity - match_base_lots; let maker_out = new_best_opposing_quantity == 0; if maker_out { orders_to_delete.push((best_opposing.handle.order_tree, best_opposing.node.key)); } else { orders_to_change.push((best_opposing.handle, new_best_opposing_quantity)); } // order_would_self_trade is only true in the DecrementTake case, in which we don't charge fees let seq_num = event_queue.header.seq_num; let fill = FillEvent::new( side, maker_out, best_opposing.node.owner_slot, now_ts, seq_num, best_opposing.node.owner, best_opposing.node.client_order_id, if order_would_self_trade { I80F48::ZERO } else { market.maker_fee }, best_opposing.node.timestamp, *mango_account_pk, order.client_order_id, if order_would_self_trade { I80F48::ZERO } else { // NOTE: this does not include the IOC penalty, but this value is not used to calculate fees market.taker_fee }, best_opposing_price, match_base_lots, ); event_queue.push_back(cast(fill)).unwrap(); limit -= 1; emit!(FilledPerpOrderLog { mango_group: market.group.key(), perp_market_index: market.perp_market_index, seq_num, }); } let total_quote_lots_taken = order.max_quote_lots - remaining_quote_lots; let total_base_lots_taken = order.max_base_lots - remaining_base_lots; assert!(total_quote_lots_taken >= 0); assert!(total_base_lots_taken >= 0); // Record the taker trade in the account already, even though it will only be // realized when the fill event gets executed if total_quote_lots_taken > 0 || total_base_lots_taken > 0 { perp_position.add_taker_trade(side, total_base_lots_taken, total_quote_lots_taken); // reduce fees to apply by decrement take volume let taker_fees_paid = apply_fees( market, mango_account, total_quote_lots_taken - decremented_quote_lots, )?; emit!(PerpTakerTradeLog { mango_group: market.group.key(), mango_account: *mango_account_pk, perp_market_index: market.perp_market_index, taker_side: side as u8, total_base_lots_taken, total_quote_lots_taken, total_quote_lots_decremented: decremented_quote_lots, taker_fees_paid: taker_fees_paid.to_bits(), fee_penalty: fee_penalty.to_bits(), }); } // Apply changes to matched asks (handles invalidate on delete!) for (handle, new_quantity) in orders_to_change { opposing_bookside .node_mut(handle.node) .unwrap() .as_leaf_mut() .unwrap() .quantity = new_quantity; } for (component, key) in orders_to_delete { let _removed_leaf = opposing_bookside.remove_by_key(component, key).unwrap(); } // // Place remainder on the book if requested // // If there are still quantity unmatched, place on the book let book_base_quantity = remaining_base_lots.min(remaining_quote_lots / price_lots); if book_base_quantity <= 0 { post_target = None; } 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, order price {:?}, oracle price {:?}", native_price, oracle_price); post_target = None; } } if let Some(order_tree_target) = post_target { let bookside = self.bookside_mut(side); // Drop an expired order if possible if let Some(expired_order) = bookside.remove_one_expired(order_tree_target, 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, worst_price) = bookside.remove_worst(now_ts, oracle_price_lots).unwrap(); // MangoErrorCode::OutOfSpace require!( side.is_price_better(price_lots, worst_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.perp_next_order_slot()?; let new_order = LeafNode::new( owner_slot as u8, order_id, *mango_account_pk, book_base_quantity, now_ts, PostOrderType::Limit, // TODO: Support order types? needed? order.time_in_force, order.peg_limit(), order.client_order_id, ); let _result = bookside.insert_leaf(order_tree_target, &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.add_perp_order( market.perp_market_index, side, order_tree_target, &new_order, order.client_order_id, )?; } if post_target.is_some() { Ok(Some(order_id)) } else { Ok(None) } } /// Cancels up to `limit` orders that are listed on the mango account for the given perp market. /// Optionally filters by `side_to_cancel_option`. /// The orders are removed from the book and from the mango account open order list. pub fn cancel_all_orders( &mut self, mango_account: &mut MangoAccountRefMut, perp_market: &mut PerpMarket, mut limit: u8, side_to_cancel_option: Option, ) -> Result<()> { for i in 0..mango_account.header.perp_oo_count() { let oo = mango_account.perp_order_by_raw_index(i); if !oo.is_active_for_market(perp_market.perp_market_index) { continue; } 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_and_tree.side() { continue; } } let order_id = oo.id; let cancel_result = self.cancel_order(mango_account, order_id, order_side_and_tree, None); if cancel_result.is_anchor_error_with_code(MangoError::PerpOrderIdNotFound.into()) { // It's possible for the order to be filled or expired already. // There will be an event on the queue, the perp order slot is freed once // it is processed. msg!( "order {} was not found on orderbook, expired or filled already", order_id ); } else { cancel_result?; } limit -= 1; if limit == 0 { break; } } Ok(()) } /// Cancels an order on a side, removing it from the book and the mango account orders list pub fn cancel_order( &mut self, mango_account: &mut MangoAccountRefMut, order_id: u128, side_and_tree: SideAndOrderTree, expected_owner: Option, ) -> Result { let side = side_and_tree.side(); let book_component = side_and_tree.order_tree(); let leaf_node = self.bookside_mut(side). remove_by_key(book_component, order_id).ok_or_else(|| { // possibly already filled or expired? error_msg_typed!(MangoError::PerpOrderIdNotFound, "no perp order with id {order_id}, side {side:?}, component {book_component:?} found on the orderbook") })?; if let Some(owner) = expected_owner { require_keys_eq!(leaf_node.owner, owner); } mango_account.remove_perp_order(leaf_node.owner_slot as usize, leaf_node.quantity)?; Ok(leaf_node) } } /// 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, account: &mut MangoAccountRefMut, quote_lots: i64, ) -> Result { let quote_native = I80F48::from_num(market.quote_lot_size * quote_lots); // The maker fees apply to the maker's account only when the fill event is consumed. let maker_fees = quote_native * market.maker_fee; let taker_fees = quote_native * market.taker_fee; // taker fees should never be negative require_gte!(taker_fees, 0); // Part of the taker fees that go to the dao, instead of paying for maker rebates let taker_dao_fees = (taker_fees + maker_fees.min(I80F48::ZERO)).max(I80F48::ZERO); account .fixed .accrue_buyback_fees(taker_dao_fees.floor().to_num::()); let perp_position = account.perp_position_mut(market.perp_market_index)?; perp_position.record_trading_fee(taker_fees); // taker fees are applied to volume during matching, quote volume only during consume perp_position.taker_volume += taker_fees.to_num::(); // Accrue 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. market.fees_accrued += taker_fees + maker_fees; Ok(taker_fees) } /// Applies a fixed penalty fee to the account, and update the market's fees_accrued fn apply_penalty(market: &mut PerpMarket, account: &mut MangoAccountRefMut) -> Result { let fee_penalty = I80F48::from_num(market.fee_penalty); account .fixed .accrue_buyback_fees(fee_penalty.floor().to_num::()); let perp_position = account.perp_position_mut(market.perp_market_index)?; perp_position.record_trading_fee(fee_penalty); market.fees_accrued += fee_penalty; Ok(fee_penalty) }