From ec5e9598042d4ed81d5ad2b20d724290ae1b1dce Mon Sep 17 00:00:00 2001 From: microwavedcola1 Date: Mon, 21 Mar 2022 20:29:28 +0100 Subject: [PATCH] further work on perps Signed-off-by: microwavedcola1 --- Cargo.lock | 22 +- mango-macro/Cargo.toml | 12 + mango-macro/src/lib.rs | 20 + programs/mango-v4/Cargo.toml | 1 + .../src/instructions/create_perp_market.rs | 55 +- .../src/instructions/place_perp_order.rs | 98 +- programs/mango-v4/src/lib.rs | 28 +- programs/mango-v4/src/state/mango_account.rs | 1 + programs/mango-v4/src/state/mod.rs | 2 + programs/mango-v4/src/state/orderbook/book.rs | 1346 +++++++++++++++++ .../mango-v4/src/state/orderbook/bookside.rs | 468 ++++++ .../src/state/orderbook/bookside_iterator.rs | 94 ++ .../mango-v4/src/state/orderbook/datatype.rs | 18 + .../mango-v4/src/state/orderbook/metadata.rs | 42 + programs/mango-v4/src/state/orderbook/mod.rs | 20 + .../mango-v4/src/state/orderbook/nodes.rs | 272 ++++ .../mango-v4/src/state/orderbook/ob_utils.rs | 24 + .../src/state/orderbook/order_type.rs | 56 + .../src/state/orderbook/perp_market.rs | 275 ++++ .../src/state/orderbook/perp_market_info.rs | 18 + .../mango-v4/src/state/orderbook/queue.rs | 438 ++++++ programs/mango-v4/src/state/perp_market.rs | 85 +- programs/mango-v4/src/util.rs | 36 + .../tests/program_test/mango_client.rs | 84 +- .../mango-v4/tests/program_test/solana.rs | 27 + programs/mango-v4/tests/test_perp.rs | 36 +- 26 files changed, 3437 insertions(+), 141 deletions(-) create mode 100644 mango-macro/Cargo.toml create mode 100644 mango-macro/src/lib.rs create mode 100644 programs/mango-v4/src/state/orderbook/book.rs create mode 100644 programs/mango-v4/src/state/orderbook/bookside.rs create mode 100644 programs/mango-v4/src/state/orderbook/bookside_iterator.rs create mode 100644 programs/mango-v4/src/state/orderbook/datatype.rs create mode 100644 programs/mango-v4/src/state/orderbook/metadata.rs create mode 100644 programs/mango-v4/src/state/orderbook/mod.rs create mode 100644 programs/mango-v4/src/state/orderbook/nodes.rs create mode 100644 programs/mango-v4/src/state/orderbook/ob_utils.rs create mode 100644 programs/mango-v4/src/state/orderbook/order_type.rs create mode 100644 programs/mango-v4/src/state/orderbook/perp_market.rs create mode 100644 programs/mango-v4/src/state/orderbook/perp_market_info.rs create mode 100644 programs/mango-v4/src/state/orderbook/queue.rs diff --git a/Cargo.lock b/Cargo.lock index 5bc10324f..a1ec88482 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -448,9 +448,9 @@ dependencies = [ [[package]] name = "bytemuck" -version = "1.7.3" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439989e6b8c38d1b6570a384ef1e49c8848128f5a97f3914baef02920842712f" +checksum = "0e851ca7c24871e7336801608a4797d7376545b6928a10d32d75685687141ead" dependencies = [ "bytemuck_derive", ] @@ -1542,6 +1542,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "mango-macro" +version = "0.0.1" +dependencies = [ + "bytemuck", + "quote", + "syn", +] + [[package]] name = "mango-v4" version = "0.1.0" @@ -1558,6 +1567,7 @@ dependencies = [ "fixed", "fixed-macro", "log", + "mango-macro", "margin-trade", "num_enum", "pyth-client", @@ -2012,9 +2022,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.15" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" +checksum = "b4af2ec4714533fcdf07e886f17025ace8b997b9ce51204ee69b6da831c3da57" dependencies = [ "proc-macro2", ] @@ -3056,9 +3066,9 @@ checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" [[package]] name = "syn" -version = "1.0.86" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b" +checksum = "ea297be220d52398dcc07ce15a209fce436d361735ac1db700cab3b6cdfb9f54" dependencies = [ "proc-macro2", "quote", diff --git a/mango-macro/Cargo.toml b/mango-macro/Cargo.toml new file mode 100644 index 000000000..80568d031 --- /dev/null +++ b/mango-macro/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "mango-macro" +version = "0.0.1" +edition = "2018" + +[lib] +proc-macro = true + +[dependencies] +syn = "1.0.89" +bytemuck = "1.8.0" +quote = "1.0.16" diff --git a/mango-macro/src/lib.rs b/mango-macro/src/lib.rs new file mode 100644 index 000000000..6677c693b --- /dev/null +++ b/mango-macro/src/lib.rs @@ -0,0 +1,20 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, DeriveInput}; + +#[proc_macro_derive(Pod)] +pub fn pod(input: TokenStream) -> TokenStream { + let DeriveInput { ident, data, .. } = parse_macro_input!(input); + + match data { + syn::Data::Struct(_) => { + quote! { + unsafe impl bytemuck::Zeroable for #ident {} + unsafe impl bytemuck::Pod for #ident {} + } + } + + _ => panic!(), + } + .into() +} diff --git a/programs/mango-v4/Cargo.toml b/programs/mango-v4/Cargo.toml index ed2bf6a47..37e0acd95 100644 --- a/programs/mango-v4/Cargo.toml +++ b/programs/mango-v4/Cargo.toml @@ -35,6 +35,7 @@ checked_math = { path = "../../lib/checked_math" } arrayref = "0.3.6" num_enum = "0.5.1" bincode = "1.3.3" +mango-macro={ path = "../../mango-macro" } [dev-dependencies] solana-sdk = { version = "1.9.5", default-features = false } diff --git a/programs/mango-v4/src/instructions/create_perp_market.rs b/programs/mango-v4/src/instructions/create_perp_market.rs index 3fb1d1486..973029933 100644 --- a/programs/mango-v4/src/instructions/create_perp_market.rs +++ b/programs/mango-v4/src/instructions/create_perp_market.rs @@ -22,30 +22,13 @@ pub struct CreatePerpMarket<'info> { space = 8 + std::mem::size_of::(), )] pub perp_market: AccountLoader<'info, PerpMarket>, - #[account( - init, - seeds = [group.key().as_ref(), b"Asks".as_ref(), perp_market.key().as_ref()], - bump, - payer = payer, - space = 8 + std::mem::size_of::(), - )] - pub asks: AccountLoader<'info, crate::state::Book>, - #[account( - init, - seeds = [group.key().as_ref(), b"Bids".as_ref(), perp_market.key().as_ref()], - bump, - payer = payer, - space = 8 + std::mem::size_of::(), - )] - pub bids: AccountLoader<'info, Book>, - #[account( - init, - seeds = [group.key().as_ref(), b"EventQueue".as_ref(), perp_market.key().as_ref()], - bump, - payer = payer, - space = 8 + std::mem::size_of::(), - )] - pub event_queue: AccountLoader<'info, crate::state::EventQueue>, + + /// 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>, #[account(mut)] pub payer: Signer<'info>, @@ -60,11 +43,6 @@ pub fn create_perp_market( quote_token_index: TokenIndex, quote_lot_size: i64, base_lot_size: i64, - // todo - // base token index (optional) - // quote token index - // oracle - // perp market index ) -> Result<()> { let mut perp_market = ctx.accounts.perp_market.load_init()?; *perp_market = PerpMarket { @@ -72,31 +50,20 @@ pub fn create_perp_market( oracle: ctx.accounts.oracle.key(), bids: ctx.accounts.bids.key(), asks: ctx.accounts.asks.key(), - event_queue: ctx.accounts.event_queue.key(), quote_lot_size: quote_lot_size, base_lot_size: base_lot_size, - // long_funding, - // short_funding, - // last_updated, - // open_interest, seq_num: 0, - // fees_accrued, - // liquidity_mining_info, - // mngo_vault: ctx.accounts.mngo_vault.key(), - bump: *ctx.bumps.get("perp_market").ok_or(MangoError::SomeError)?, perp_market_index, base_token_index: base_token_index_opt.ok_or(TokenIndex::MAX).unwrap(), quote_token_index, + bump: *ctx.bumps.get("perp_market").ok_or(MangoError::SomeError)?, }; - let mut asks = ctx.accounts.asks.load_init()?; - *asks = Book {}; - let mut bids = ctx.accounts.bids.load_init()?; - *bids = Book {}; + bids.book_side_type = BookSideType::Bids; - let mut event_queue = ctx.accounts.event_queue.load_init()?; - *event_queue = EventQueue {}; + let mut asks = ctx.accounts.asks.load_init()?; + asks.book_side_type = BookSideType::Asks; Ok(()) } diff --git a/programs/mango-v4/src/instructions/place_perp_order.rs b/programs/mango-v4/src/instructions/place_perp_order.rs index 99108f2df..33d8c5b23 100644 --- a/programs/mango-v4/src/instructions/place_perp_order.rs +++ b/programs/mango-v4/src/instructions/place_perp_order.rs @@ -1,8 +1,100 @@ use anchor_lang::prelude::*; -#[derive(Accounts)] -pub struct PlacePerpOrder {} +use crate::state::{ + oracle_price, Book, BookSide, Group, MangoAccount, OrderType, PerpMarket, Side, +}; + +#[derive(Accounts)] +pub struct PlacePerpOrder<'info> { + pub group: AccountLoader<'info, Group>, + + #[account( + mut, + has_one = group, + has_one = owner, + )] + pub account: AccountLoader<'info, MangoAccount>, + + #[account( + mut, + has_one = group, + has_one = bids, + has_one = asks, + has_one = oracle, + )] + pub perp_market: AccountLoader<'info, PerpMarket>, + #[account(mut)] + pub asks: AccountLoader<'info, BookSide>, + #[account(mut)] + pub bids: AccountLoader<'info, BookSide>, + pub oracle: UncheckedAccount<'info>, + + pub owner: Signer<'info>, +} + +pub fn place_perp_order( + ctx: Context, + // TODO side is harcoded for now + // maybe new_bid and new_ask can be folded into one function + // side: Side, + price: i64, + max_base_quantity: i64, + max_quote_quantity: i64, + client_order_id: u64, + order_type: OrderType, + // TODO reduce_only relies on event queue + // reduce_only: bool, + expiry_timestamp: u64, + limit: u8, +) -> Result<()> { + let mut account = ctx.accounts.account.load_mut()?; + let mango_account_pk = ctx.accounts.account.key(); + + let mut perp_market = ctx.accounts.perp_market.load_mut()?; + let bids = &ctx.accounts.bids.to_account_info(); + let asks = &ctx.accounts.asks.to_account_info(); + let mut book = Book::load_checked(&bids, &asks, &perp_market)?; + + let oracle_price = oracle_price(&ctx.accounts.oracle.to_account_info())?; + + 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_bid( + // program_id: &Pubkey, + // mango_group: &MangoGroup, + // mango_group_pk: &Pubkey, + // mango_cache: &MangoCache, + // event_queue: &mut EventQueue, + &mut perp_market, + oracle_price, + &mut account, + &mango_account_pk, + // market_index: usize, + price, + max_base_quantity, + max_quote_quantity, + order_type, + time_in_force, + client_order_id, + now_ts, + // referrer_mango_account_ai: Option<&AccountInfo>, + limit, + )?; -pub fn place_perp_order(_ctx: Context) -> Result<()> { Ok(()) } diff --git a/programs/mango-v4/src/lib.rs b/programs/mango-v4/src/lib.rs index 9dcd816fe..41969c05f 100644 --- a/programs/mango-v4/src/lib.rs +++ b/programs/mango-v4/src/lib.rs @@ -15,7 +15,7 @@ pub mod instructions; mod serum3_cpi; pub mod state; -use state::{PerpMarketIndex, Serum3MarketIndex, TokenIndex}; +use state::{OrderType, PerpMarketIndex, Serum3MarketIndex, Side, TokenIndex}; declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); @@ -143,8 +143,30 @@ pub mod mango_v4 { ) } - pub fn place_perp_order(ctx: Context) -> Result<()> { - instructions::place_perp_order(ctx) + pub fn place_perp_order( + ctx: Context, + side: Side, + price: i64, + max_base_quantity: i64, + max_quote_quantity: i64, + client_order_id: u64, + order_type: OrderType, + reduce_only: bool, + expiry_timestamp: u64, + limit: u8, + ) -> Result<()> { + instructions::place_perp_order( + ctx, + // side, + price, + max_base_quantity, + max_quote_quantity, + client_order_id, + order_type, + // reduce_only, + // expiry_timestamp, + limit, + ) } } diff --git a/programs/mango-v4/src/state/mango_account.rs b/programs/mango-v4/src/state/mango_account.rs index 883c3ebf7..03d160ebb 100644 --- a/programs/mango-v4/src/state/mango_account.rs +++ b/programs/mango-v4/src/state/mango_account.rs @@ -14,6 +14,7 @@ use crate::state::*; // MangoAccount size and health compute needs. const MAX_INDEXED_POSITIONS: usize = 16; const MAX_SERUM_OPEN_ORDERS: usize = 8; +const MAX_PERP_OPEN_ORDERS: usize = 8; #[zero_copy] pub struct TokenAccount { diff --git a/programs/mango-v4/src/state/mod.rs b/programs/mango-v4/src/state/mod.rs index dc57a2554..746559e72 100644 --- a/programs/mango-v4/src/state/mod.rs +++ b/programs/mango-v4/src/state/mod.rs @@ -4,6 +4,7 @@ pub use health::*; pub use mango_account::*; pub use mint_info::*; pub use oracle::*; +pub use orderbook::*; pub use perp_market::*; pub use serum3_market::*; @@ -13,5 +14,6 @@ mod health; mod mango_account; mod mint_info; mod oracle; +pub mod orderbook; mod perp_market; mod serum3_market; diff --git a/programs/mango-v4/src/state/orderbook/book.rs b/programs/mango-v4/src/state/orderbook/book.rs new file mode 100644 index 000000000..ec622493e --- /dev/null +++ b/programs/mango-v4/src/state/orderbook/book.rs @@ -0,0 +1,1346 @@ +use std::cell::RefMut; + +use crate::{ + error::MangoError, + state::{ + orderbook::{bookside::BookSide, nodes::LeafNode}, + MangoAccount, PerpMarket, + }, +}; +use anchor_lang::prelude::*; +use bytemuck::cast; +use fixed::types::I80F48; +use fixed_macro::types::I80F48; + +use super::{ + nodes::NodeHandle, + order_type::{OrderType, Side}, + queue::{EventQueue, FillEvent, OutEvent}, +}; + +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> { + pub fn load_checked( + bids_ai: &'a AccountInfo, + asks_ai: &'a AccountInfo, + perp_market: &PerpMarket, + ) -> std::result::Result { + require!(bids_ai.key == &perp_market.bids, MangoError::SomeError); // MangoErrorCode::InvalidAccount + require!(asks_ai.key == &perp_market.asks, MangoError::SomeError); // MangoErrorCode::InvalidAccount + Ok(Self { + bids: BookSide::load_mut_checked(bids_ai, perp_market)?, + asks: BookSide::load_mut_checked(asks_ai, perp_market)?, + }) + } + + /// returns best valid bid + pub fn get_best_bid_price(&self, now_ts: u64) -> Option { + 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 { + 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 { + 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) + } + + // #[inline(never)] + // pub fn new_order( + // &mut self, + // program_id: &Pubkey, + // // mango_group: &MangoGroup, + // mango_group_pk: &Pubkey, + // // mango_cache: &MangoCache, + // event_queue: &mut EventQueue, + // market: &mut PerpMarket, + // oracle_price: I80F48, + // mango_account: &mut MangoAccount, + // mango_account_pk: &Pubkey, + // market_index: usize, + // side: Side, + // 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>, + // limit: u8, + // ) -> std::result::Result<(), Error> { + // match side { + // Side::Bid => self.new_bid( + // program_id, + // // mango_group, + // mango_group_pk, + // // mango_cache, + // event_queue, + // market, + // oracle_price, + // mango_account, + // mango_account_pk, + // market_index, + // price, + // max_base_quantity, + // max_quote_quantity, + // order_type, + // time_in_force, + // client_order_id, + // now_ts, + // referrer_mango_account_ai, + // limit, + // ), + // Side::Ask => self.new_ask( + // program_id, + // // mango_group, + // mango_group_pk, + // // mango_cache, + // event_queue, + // market, + // oracle_price, + // mango_account, + // mango_account_pk, + // market_index, + // price, + // max_base_quantity, + // max_quote_quantity, + // order_type, + // time_in_force, + // client_order_id, + // now_ts, + // referrer_mango_account_ai, + // limit, + // ), + // } + // } + + /// Iterate over the book and return + /// return changes to (taker_base, taker_quote, bids_quantity, asks_quantity) + pub fn sim_new_bid( + &self, + market: &PerpMarket, + // info: &PerpMarketInfo, + oracle_price: I80F48, + 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, + now_ts: u64, + ) -> std::result::Result<(i64, i64, i64, i64), Error> { + let (mut taker_base, mut taker_quote, mut bids_quantity, asks_quantity) = (0, 0, 0i64, 0); + + 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) + } + }; + // 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() > info.maint_liab_weight { + // msg!("Posting on book disallowed due to price limits"); + // post_allowed = false; + // } + // } + + let mut rem_base_quantity = max_base_quantity; // base lots (aka contracts) + let mut rem_quote_quantity = max_quote_quantity; + + for (_, best_ask) in self.asks.iter_valid(now_ts) { + let best_ask_price = best_ask.price(); + if price < best_ask_price { + break; + } else if post_only { + return Ok((taker_base, taker_quote, bids_quantity, asks_quantity)); + } + + 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 match_quote = match_quantity * best_ask_price; + rem_base_quantity -= match_quantity; + rem_quote_quantity -= match_quote; + + taker_base += match_quantity; + taker_quote -= match_quote; + if match_quantity == max_match_by_quote || rem_base_quantity == 0 { + break; + } + } + let book_base_quantity = rem_base_quantity.min(rem_quote_quantity / price); + if post_allowed && book_base_quantity > 0 { + bids_quantity = bids_quantity.checked_add(book_base_quantity).unwrap(); + } + Ok((taker_base, taker_quote, bids_quantity, asks_quantity)) + } + + pub fn sim_new_ask( + &self, + market: &PerpMarket, + // info: &PerpMarketInfo, + oracle_price: I80F48, + 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, + now_ts: u64, + ) -> std::result::Result<(i64, i64, i64, i64), Error> { + let (mut taker_base, mut taker_quote, bids_quantity, mut asks_quantity) = (0, 0, 0, 0i64); + + 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, 1), + OrderType::PostOnlySlide => { + let price = if let Some(best_bid_price) = self.get_best_bid_price(now_ts) { + price.max(best_bid_price.checked_add(1).ok_or(MangoError::SomeError)?) + // todo math_err + } else { + price + }; + (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); + // if native_price.checked_div(oracle_price).unwrap() < info.maint_asset_weight { + // msg!("Posting on book disallowed due to price limits"); + // post_allowed = false; + // } + // } + + let mut rem_base_quantity = max_base_quantity; // base lots (aka contracts) + let mut rem_quote_quantity = max_quote_quantity; + + for (_, best_bid) in self.bids.iter_valid(now_ts) { + let best_bid_price = best_bid.price(); + if price > best_bid_price { + break; + } else if post_only { + return Ok((taker_base, taker_quote, bids_quantity, asks_quantity)); + } + + let max_match_by_quote = rem_quote_quantity / best_bid_price; + let match_quantity = rem_base_quantity + .min(best_bid.quantity) + .min(max_match_by_quote); + + let match_quote = match_quantity * best_bid_price; + rem_base_quantity -= match_quantity; + rem_quote_quantity -= match_quote; + + taker_base -= match_quantity; + taker_quote += match_quote; + if match_quantity == max_match_by_quote || rem_base_quantity == 0 { + break; + } + } + + let book_base_quantity = rem_base_quantity.min(rem_quote_quantity / price); + if post_allowed && book_base_quantity > 0 { + asks_quantity = asks_quantity.checked_add(book_base_quantity).unwrap(); + } + Ok((taker_base, taker_quote, bids_quantity, asks_quantity)) + } + + // todo: can new_bid and new_ask be elegantly folded into one method? + #[inline(never)] + pub fn new_bid( + &mut self, + // program_id: &Pubkey, + // mango_group: &MangoGroup, + // mango_group_pk: &Pubkey, + // mango_cache: &MangoCache, + // event_queue: &mut EventQueue, + market: &mut PerpMarket, + oracle_price: I80F48, + mango_account: &mut MangoAccount, + 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) + } + }; + // let info = &mango_group.perp_markets[market_index]; + // 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() > info.maint_liab_weight { + // msg!("Posting on book disallowed due to price limits"); + // post_allowed = false; + // } + // } + + // 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 = 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; + // 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(); + // ask_deletes.push(best_ask.key); + } + 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; + + let match_quote = match_quantity * best_ask_price; + rem_base_quantity -= match_quantity; + rem_quote_quantity -= match_quote; + // mango_account.perp_accounts[market_index].add_taker_trade(match_quantity, -match_quote); + + let new_best_ask_quantity = best_ask.quantity - match_quantity; + 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; + } + } + let total_quote_taken = max_quote_quantity - rem_quote_quantity; + + // 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 + 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(); + } + + 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(()) + } + + #[inline(never)] + pub fn new_ask( + &mut self, + program_id: &Pubkey, + // mango_group: &MangoGroup, + mango_group_pk: &Pubkey, + // mango_cache: &MangoCache, + event_queue: &mut EventQueue, + market: &mut PerpMarket, + oracle_price: I80F48, + mango_account: &mut MangoAccount, + 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 + ) -> Result<()> { + 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, 1), + OrderType::PostOnlySlide => { + let price = if let Some(best_bid_price) = self.get_best_bid_price(now_ts) { + price.max(best_bid_price.checked_add(1).ok_or(MangoError::SomeError)?) + // math_err + } else { + price + }; + (true, true, price) + } + }; + + // let info = &mango_group.perp_markets[market_index]; + // 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() < info.maint_asset_weight { + // msg!("Posting on book disallowed due to price limits"); + // post_allowed = false; + // } + // } + + // 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::Ask, price); + + // Iterate through book and match against this new ask + // + // Any changes to matching bids are collected in bid_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 bid_changes: Vec<(NodeHandle, i64)> = vec![]; + let mut bid_deletes: Vec = vec![]; + let mut number_of_dropped_expired_orders = 0; + for (best_bid_h, best_bid) in self.bids.iter_all_including_invalid() { + if !best_bid.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( + Side::Bid, + best_bid.owner_slot, + now_ts, + event_queue.header.seq_num, + best_bid.owner, + best_bid.quantity, + ); + event_queue.push_back(cast(event)).unwrap(); + bid_deletes.push(best_bid.key); + } + continue; + } + + let best_bid_price = best_bid.price(); + + if price > best_bid_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_bid_price; + let match_quantity = rem_base_quantity + .min(best_bid.quantity) + .min(max_match_by_quote); + let done = match_quantity == max_match_by_quote || match_quantity == rem_base_quantity; + + let match_quote = match_quantity * best_bid_price; + rem_base_quantity -= match_quantity; + rem_quote_quantity -= match_quote; + // mango_account.perp_accounts[market_index].add_taker_trade(-match_quantity, match_quote); + + let new_best_bid_quantity = best_bid.quantity - match_quantity; + let maker_out = new_best_bid_quantity == 0; + if maker_out { + bid_deletes.push(best_bid.key); + } else { + bid_changes.push((best_bid_h, new_best_bid_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::Ask, + // best_bid.owner_slot, + // maker_out, + // now_ts, + // event_queue.header.seq_num, + // best_bid.owner, + // best_bid.key, + // best_bid.client_order_id, + // info.maker_fee, + // best_bid.best_initial, + // best_bid.timestamp, + // *mango_account_pk, + // order_id, + // client_order_id, + // info.taker_fee + ref_fee_rate.unwrap(), + // best_bid_price, + // match_quantity, + // best_bid.version, + // ); + + // event_queue.push_back(cast(fill)).unwrap(); + limit -= 1; + + if done { + break; + } + } + let total_quote_taken = max_quote_quantity - rem_quote_quantity; + + // Apply changes to matched bids (handles invalidate on delete!) + for (handle, new_quantity) in bid_changes { + self.bids + .get_mut(handle) + .unwrap() + .as_leaf_mut() + .unwrap() + .quantity = new_quantity; + } + for key in bid_deletes { + let _removed_leaf = self.bids.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 book_base_quantity > 0 && post_allowed { + // Drop an expired order if possible + if let Some(expired_ask) = self.asks.remove_one_expired(now_ts) { + let event = OutEvent::new( + Side::Ask, + expired_ask.owner_slot, + now_ts, + event_queue.header.seq_num, + expired_ask.owner, + expired_ask.quantity, + ); + event_queue.push_back(cast(event)).unwrap(); + } + + if self.asks.is_full() { + // If this asks is lower than highest ask, boot that ask and insert this one + let max_ask = self.asks.remove_max().unwrap(); + require!(price < max_ask.price(), MangoError::SomeError); // OutOfSpace + let event = OutEvent::new( + Side::Ask, + max_ask.owner_slot, + now_ts, + event_queue.header.seq_num, + max_ask.owner, + max_ask.quantity, + ); + event_queue.push_back(cast(event)).unwrap(); + } + + // let best_initial = if market.meta_data.version == 0 { + // match self.get_best_ask_price(now_ts) { + // None => price, + // Some(p) => p, + // } + // } else { + // let max_depth: i64 = market.liquidity_mining_info.max_depth_bps.to_num(); + // self.get_asks_size_below(price, max_depth, now_ts) + // }; + + // let owner_slot = mango_account + // .next_order_slot() + // .ok_or(MangoError::SomeError)?; // TooManyOpenOrders + let new_ask = 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.asks.insert_leaf(&new_ask)?; + + // TODO OPT remove if PlacePerpOrder needs more compute + msg!( + "ask on book order_id={} quantity={} price={}", + order_id, + book_base_quantity, + price + ); + + // mango_account.add_order(market_index, Side::Ask, &new_ask)?; + } + + // 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(()) + } + + // pub fn cancel_order(&mut self, order_id: i128, side: Side) -> Result<()> { + // match side { + // Side::Bid => self + // .bids + // .remove_by_key(order_id) + // .ok_or(MangoError::SomeError), // InvalidOrderId + // Side::Ask => self + // .asks + // .remove_by_key(order_id) + // .ok_or(MangoError::SomeError), // InvalidOrderId + // } + // } + + // /// Used by force cancel so does not need to give liquidity incentives + // pub fn cancel_all( + // &mut self, + // mango_account: &mut MangoAccount, + // market_index: usize, + // mut limit: u8, + // ) -> Result<()> { + // let market_index = market_index as u8; + // for i in 0..MAX_PERP_OPEN_ORDERS { + // if mango_account.order_market[i] != market_index { + // // means slot is free or belongs to different perp market + // continue; + // } + // let order_id = mango_account.orders[i]; + // match self.cancel_order(order_id, mango_account.order_side[i]) { + // Ok(order) => { + // mango_account.remove_order(order.owner_slot as usize, order.quantity)?; + // } + // Err(_) => { + // // If it's not on the book, then it has been matched and only Keeper can remove + // } + // }; + + // limit -= 1; + // if limit == 0 { + // break; + // } + // } + // Ok(()) + // } + + // pub fn cancel_all_side_with_size_incentives( + // &mut self, + // mango_account: &mut MangoAccount, + // perp_market: &mut PerpMarket, + // market_index: usize, + // side: Side, + // mut limit: u8, + // ) -> std::result::Result<(Vec, Vec), MangoError> { + // // TODO - test different limits + // let now_ts = Clock::get()?.unix_timestamp as u64; + // let max_depth: i64 = perp_market.liquidity_mining_info.max_depth_bps.to_num(); + + // let mut all_order_ids = vec![]; + // let mut canceled_order_ids = vec![]; + // let mut keys = vec![]; + // let market_index_u8 = market_index as u8; + // for i in 0..MAX_PERP_OPEN_ORDERS { + // if mango_account.order_market[i] == market_index_u8 + // && mango_account.order_side[i] == side + // { + // all_order_ids.push(mango_account.orders[i]); + // keys.push(mango_account.orders[i]) + // } + // } + // match side { + // Side::Bid => self.cancel_all_bids_with_size_incentives( + // mango_account, + // perp_market, + // market_index, + // max_depth, + // now_ts, + // &mut limit, + // keys, + // &mut canceled_order_ids, + // )?, + // Side::Ask => self.cancel_all_asks_with_size_incentives( + // mango_account, + // perp_market, + // market_index, + // max_depth, + // now_ts, + // &mut limit, + // keys, + // &mut canceled_order_ids, + // )?, + // }; + // Ok((all_order_ids, canceled_order_ids)) + // } + // pub fn cancel_all_with_size_incentives( + // &mut self, + // mango_account: &mut MangoAccount, + // perp_market: &mut PerpMarket, + // market_index: usize, + // mut limit: u8, + // ) -> std::result::Result<(Vec, Vec), Error> { + // // TODO - test different limits + // let now_ts = Clock::get()?.unix_timestamp as u64; + // let max_depth: i64 = perp_market.liquidity_mining_info.max_depth_bps.to_num(); + + // let mut all_order_ids = vec![]; + // let mut canceled_order_ids = vec![]; + + // let market_index_u8 = market_index as u8; + // let mut bids_keys = vec![]; + // let mut asks_keys = vec![]; + // for i in 0..MAX_PERP_OPEN_ORDERS { + // if mango_account.order_market[i] != market_index_u8 { + // continue; + // } + // all_order_ids.push(mango_account.orders[i]); + // match mango_account.order_side[i] { + // Side::Bid => bids_keys.push(mango_account.orders[i]), + // Side::Ask => asks_keys.push(mango_account.orders[i]), + // } + // } + // self.cancel_all_bids_with_size_incentives( + // mango_account, + // perp_market, + // market_index, + // max_depth, + // now_ts, + // &mut limit, + // bids_keys, + // &mut canceled_order_ids, + // )?; + // self.cancel_all_asks_with_size_incentives( + // mango_account, + // perp_market, + // market_index, + // max_depth, + // now_ts, + // &mut limit, + // asks_keys, + // &mut canceled_order_ids, + // )?; + // Ok((all_order_ids, canceled_order_ids)) + // } + + // /// Internal + // fn cancel_all_bids_with_size_incentives( + // &mut self, + // mango_account: &mut MangoAccount, + // perp_market: &mut PerpMarket, + // market_index: usize, + // max_depth: i64, + // now_ts: u64, + // limit: &mut u8, + // mut my_bids: Vec, + // canceled_order_ids: &mut Vec, + // ) -> Result<()> { + // my_bids.sort_unstable(); + // let mut bids_and_sizes = vec![]; + // let mut cuml_bids = 0; + + // let mut iter = self.bids.iter_all_including_invalid(); + // let mut curr = iter.next(); + // while let Some((_, bid)) = curr { + // match my_bids.last() { + // None => break, + // Some(&my_highest_bid) => { + // if bid.key > my_highest_bid { + // if bid.is_valid(now_ts) { + // // if bid is not valid, it doesn't count towards book liquidity + // cuml_bids += bid.quantity; + // } + // curr = iter.next(); + // } else if bid.key == my_highest_bid { + // bids_and_sizes.push((bid.key, cuml_bids)); + // my_bids.pop(); + // curr = iter.next(); + // } else { + // // my_highest_bid is not on the book; it must be on EventQueue waiting to be processed + // // check the next my_highest_bid against bid + // my_bids.pop(); + // } + + // if cuml_bids >= max_depth { + // for bid_key in my_bids { + // bids_and_sizes.push((bid_key, max_depth)); + // } + // break; + // } + // } + // } + // } + + // for (key, cuml_size) in bids_and_sizes { + // if *limit == 0 { + // return Ok(()); + // } else { + // *limit -= 1; + // } + + // match self.cancel_order(key, Side::Bid) { + // Ok(order) => { + // mango_account.remove_order(order.owner_slot as usize, order.quantity)?; + // canceled_order_ids.push(key); + // if order.version == perp_market.meta_data.version + // && order.version != 0 + // && order.is_valid(now_ts) + // { + // mango_account.perp_accounts[market_index].apply_size_incentives( + // perp_market, + // order.best_initial, + // cuml_size, + // order.timestamp, + // now_ts, + // order.quantity, + // )?; + // } + // } + // Err(_) => { + // msg!("Failed to cancel bid oid: {}; Either error state or bid is on EventQueue unprocessed", key) + // } + // } + // } + // Ok(()) + // } + + // /// Internal + // fn cancel_all_asks_with_size_incentives( + // &mut self, + // mango_account: &mut MangoAccount, + // perp_market: &mut PerpMarket, + // market_index: usize, + // max_depth: i64, + // now_ts: u64, + // limit: &mut u8, + // mut my_asks: Vec, + // canceled_order_ids: &mut Vec, + // ) -> Result<()> { + // my_asks.sort_unstable_by(|a, b| b.cmp(a)); + // let mut asks_and_sizes = vec![]; + // let mut cuml_asks = 0; + + // let mut iter = self.asks.iter_all_including_invalid(); + // let mut curr = iter.next(); + // while let Some((_, ask)) = curr { + // match my_asks.last() { + // None => break, + // Some(&my_lowest_ask) => { + // if ask.key < my_lowest_ask { + // if ask.is_valid(now_ts) { + // // if ask is not valid, it doesn't count towards book liquidity + // cuml_asks += ask.quantity; + // } + // curr = iter.next(); + // } else if ask.key == my_lowest_ask { + // asks_and_sizes.push((ask.key, cuml_asks)); + // my_asks.pop(); + // curr = iter.next(); + // } else { + // // my_lowest_ask is not on the book; it must be on EventQueue waiting to be processed + // // check the next my_lowest_ask against ask + // my_asks.pop(); + // } + // if cuml_asks >= max_depth { + // for key in my_asks { + // asks_and_sizes.push((key, max_depth)) + // } + // break; + // } + // } + // } + // } + + // for (key, cuml_size) in asks_and_sizes { + // if *limit == 0 { + // return Ok(()); + // } else { + // *limit -= 1; + // } + // match self.cancel_order(key, Side::Ask) { + // Ok(order) => { + // mango_account.remove_order(order.owner_slot as usize, order.quantity)?; + // canceled_order_ids.push(key); + // if order.version == perp_market.meta_data.version + // && order.version != 0 + // && order.is_valid(now_ts) + // { + // mango_account.perp_accounts[market_index].apply_size_incentives( + // perp_market, + // order.best_initial, + // cuml_size, + // order.timestamp, + // now_ts, + // order.quantity, + // )?; + // } + // } + // Err(_) => { + // msg!("Failed to cancel ask oid: {}; Either error state or ask is on EventQueue unprocessed", key); + // } + // } + // } + + // Ok(()) + // } + // /// Cancel all the orders for MangoAccount for this PerpMarket up to `limit` + // /// Only used when PerpMarket version == 0 + // pub fn cancel_all_with_price_incentives( + // &mut self, + // mango_account: &mut MangoAccount, + // perp_market: &mut PerpMarket, + // market_index: usize, + // mut limit: u8, + // ) -> Result<()> { + // let now_ts = Clock::get()?.unix_timestamp as u64; + + // for i in 0..MAX_PERP_OPEN_ORDERS { + // if mango_account.order_market[i] != market_index as u8 { + // // means slot is free or belongs to different perp market + // continue; + // } + // let order_id = mango_account.orders[i]; + // let order_side = mango_account.order_side[i]; + + // let best_final = match order_side { + // Side::Bid => self.get_best_bid_price(now_ts).unwrap(), + // Side::Ask => self.get_best_ask_price(now_ts).unwrap(), + // }; + + // match self.cancel_order(order_id, order_side) { + // Ok(order) => { + // // technically these should be the same. Can enable this check to be extra sure + // // check!(i == order.owner_slot as usize, MathError)?; + // mango_account.remove_order(order.owner_slot as usize, order.quantity)?; + // if order.version != perp_market.meta_data.version { + // continue; + // } + // mango_account.perp_accounts[market_index].apply_price_incentives( + // perp_market, + // order_side, + // order.price(), + // order.best_initial, + // best_final, + // order.timestamp, + // now_ts, + // order.quantity, + // )?; + // } + // Err(_) => { + // // If it's not on the book, then it has been matched and only Keeper can remove + // } + // }; + + // limit -= 1; + // if limit == 0 { + // break; + // } + // } + // Ok(()) + // } +} + +// fn determine_ref_vars<'a>( +// program_id: &Pubkey, +// mango_group: &MangoGroup, +// mango_group_pk: &Pubkey, +// mango_cache: &MangoCache, +// mango_account: &MangoAccount, +// referrer_mango_account_ai: Option<&'a AccountInfo>, +// now_ts: u64, +// ) -> Result<(I80F48, Option>)> { +// let mngo_index = match mango_group.find_token_index(&mngo_token::id()) { +// None => return Ok((I80F48::ZERO, None)), +// Some(i) => i, +// }; + +// let mngo_cache = &mango_cache.root_bank_cache[mngo_index]; + +// // If the user's MNGO deposit is non-zero then the rootbank cache will be checked already in `place_perp_order`. +// // If it's zero then cache may be out of date, but it doesn't matter because 0 * index = 0 +// let mngo_deposits = mango_account.get_native_deposit(mngo_cache, mngo_index)?; +// let ref_mngo_req = I80F48::from_num(mango_group.ref_mngo_required); +// if mngo_deposits >= ref_mngo_req { +// return Ok((I80F48::ZERO, None)); +// } else if let Some(referrer_mango_account_ai) = referrer_mango_account_ai { +// // If referrer_mango_account is invalid, just treat it as if it doesn't exist +// if let Ok(referrer_mango_account) = +// MangoAccount::load_mut_checked(referrer_mango_account_ai, program_id, mango_group_pk) +// { +// // Need to check if it's valid because user may not have mngo in active assets +// mngo_cache.check_valid(mango_group, now_ts)?; +// let ref_mngo_deposits = +// referrer_mango_account.get_native_deposit(mngo_cache, mngo_index)?; + +// if !referrer_mango_account.is_bankrupt +// && !referrer_mango_account.being_liquidated +// && ref_mngo_deposits >= ref_mngo_req +// { +// return Ok(( +// I80F48::from_num(mango_group.ref_share_centibps) / CENTIBPS_PER_UNIT, +// Some(referrer_mango_account), +// )); +// } +// } +// } +// Ok(( +// I80F48::from_num(mango_group.ref_surcharge_centibps) / CENTIBPS_PER_UNIT, +// None, +// )) +// } + +// /// 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, +// info: &PerpMarketInfo, +// mango_account: &mut MangoAccount, +// mango_account_pk: &Pubkey, +// market_index: usize, +// referrer_mango_account_opt: Option>, +// referrer_mango_account_ai: Option<&AccountInfo>, +// total_quote_taken: i64, +// ref_fee_rate: I80F48, +// // perp_market_cache: &PerpMarketCache, +// ) { +// let taker_quote_native = I80F48::from_num( +// market +// .quote_lot_size +// .checked_mul(total_quote_taken) +// .unwrap(), +// ); + +// if ref_fee_rate > I80F48::ZERO { +// let ref_fees = taker_quote_native * ref_fee_rate; + +// // if ref mango account is some, then we send some fees over +// if let Some(mut referrer_mango_account) = referrer_mango_account_opt { +// mango_account.perp_accounts[market_index].transfer_quote_position( +// &mut referrer_mango_account.perp_accounts[market_index], +// ref_fees, +// ); +// // todo +// // emit_perp_balances( +// // referrer_mango_account.mango_group, +// // *referrer_mango_account_ai.unwrap().key, +// // market_index as u64, +// // &referrer_mango_account.perp_accounts[market_index], +// // perp_market_cache, +// // ); +// // mango_emit_stack::<_, 200>(ReferralFeeAccrualLog { +// // mango_group: referrer_mango_account.mango_group, +// // referrer_mango_account: *referrer_mango_account_ai.unwrap().key, +// // referree_mango_account: *mango_account_pk, +// // market_index: market_index as u64, +// // referral_fee_accrual: ref_fees.to_bits(), +// // }); +// } else { +// // else user didn't have valid amount of MNGO and no valid referrer +// mango_account.perp_accounts[market_index].quote_position -= ref_fees; +// market.fees_accrued += ref_fees; +// } +// } + +// // 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 * info.maker_fee; + +// let taker_fees = taker_quote_native * info.taker_fee; +// mango_account.perp_accounts[market_index].quote_position -= taker_fees; +// market.fees_accrued += taker_fees + maker_fees; + +// // todo +// // emit_perp_balances( +// // mango_account.mango_group, +// // *mango_account_pk, +// // market_index as u64, +// // &mango_account.perp_accounts[market_index], +// // perp_market_cache, +// // ) +// } diff --git a/programs/mango-v4/src/state/orderbook/bookside.rs b/programs/mango-v4/src/state/orderbook/bookside.rs new file mode 100644 index 000000000..ab24dbe0b --- /dev/null +++ b/programs/mango-v4/src/state/orderbook/bookside.rs @@ -0,0 +1,468 @@ +use bytemuck::Zeroable; +use std::cell::RefMut; + +use anchor_lang::prelude::*; +use bytemuck::{cast, cast_mut, cast_ref}; + +use num_enum::{IntoPrimitive, TryFromPrimitive}; + +use crate::state::orderbook::bookside_iterator::BookSideIter; +use crate::state::PerpMarket; + +use crate::error::MangoError; +use crate::state::orderbook::nodes::{ + AnyNode, FreeNode, InnerNode, LeafNode, NodeHandle, NodeRef, NodeTag, +}; +use crate::util::LoadZeroCopy; + +use super::Book; + +pub const MAX_BOOK_NODES: usize = 1024; + +#[derive( + Eq, + PartialEq, + Copy, + Clone, + TryFromPrimitive, + IntoPrimitive, + Debug, + AnchorSerialize, + AnchorDeserialize, +)] +#[repr(u8)] +pub enum BookSideType { + Bids, + Asks, +} + +/// 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 bump_index: usize, + pub free_list_len: usize, + pub free_list_head: NodeHandle, + pub root_node: NodeHandle, + pub leaf_count: usize, + pub nodes: [AnyNode; MAX_BOOK_NODES], +} + +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) + } + + /// Iterate over all entries, including invalid orders + pub fn iter_all_including_invalid(&self) -> BookSideIter { + BookSideIter::new(self, 0) + } + + pub fn load_mut_checked<'a>( + account: &'a AccountInfo, + perp_market: &PerpMarket, + ) -> Result> { + let state = account.load_mut::()?; + + match state.book_side_type { + BookSideType::Bids => require!(account.key == &perp_market.bids, MangoError::SomeError), + BookSideType::Asks => require!(account.key == &perp_market.asks, MangoError::SomeError), + } + + Ok(state) + } + // + // pub fn load_and_init<'a>( + // account: &'a AccountInfo, + // program_id: &Pubkey, + // data_type: DataType, + // rent: &Rent, + // ) -> MangoResult> { + // // NOTE: require this first so we can borrow account later + // require!( + // rent.is_exempt(account.lamports(), account.data_len()), + // MangoErrorCode::AccountNotRentExempt + // )?; + // + // let mut state = Self::load_mut(account)?; + // require!(account.owner == program_id, MangoError::SomeError)?; // todo invalid owner + // require!(!state.meta_data.is_initialized, MangoError::SomeError)?; // todo + // state.meta_data = MetaData::new(data_type, 0, true); + // Ok(state) + // } + + pub fn get_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 get(&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 { + self.remove_by_key(self.get(self.find_min()?)?.key()?) + } + + pub fn remove_max(&mut self) -> Option { + self.remove_by_key(self.get(self.find_max()?)?.key()?) + } + + /// Remove the order with the lowest expiry timestamp, if that's < now_ts. + pub fn remove_one_expired(&mut self, now_ts: u64) -> Option { + let (expired_h, expires_at) = self.find_earliest_expiry()?; + if expires_at < now_ts { + self.remove_by_key(self.get(expired_h)?.key()?) + } else { + None + } + } + + pub fn find_max(&self) -> Option { + self.find_min_max(true) + } + + pub fn root(&self) -> Option { + if self.leaf_count == 0 { + None + } else { + Some(self.root_node) + } + } + + pub fn find_min(&self) -> Option { + self.find_min_max(false) + } + + #[cfg(test)] + 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.get(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]; + } + } + } + } + } + } + + fn find_min_max(&self, find_max: bool) -> Option { + let mut root: NodeHandle = self.root()?; + + let i = if find_max { 1 } else { 0 }; + loop { + let root_contents = self.get(root).unwrap(); + match root_contents.case().unwrap() { + NodeRef::Inner(&InnerNode { children, .. }) => { + root = children[i]; + } + _ => return Some(root), + } + } + } + + pub fn get_min(&self) -> Option<&LeafNode> { + self.get_min_max(false) + } + + pub fn get_max(&self) -> Option<&LeafNode> { + self.get_min_max(true) + } + pub fn get_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.get(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 { + // 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.get(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.get(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.get(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.get_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 { + let val = *self.get(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, + reserve: [0; 80], + }); + + self.free_list_len += 1; + self.free_list_head = key; + Some(val) + } + + pub fn insert(&mut self, val: &AnyNode) -> Result { + match NodeTag::try_from(val.tag) { + Ok(NodeTag::InnerNode) | Ok(NodeTag::LeafNode) => (), + _ => unreachable!(), + }; + + if self.free_list_len == 0 { + require!( + self.bump_index < self.nodes.len() && self.bump_index < (u32::MAX as usize), + MangoError::SomeError // todo + ); + + self.nodes[self.bump_index] = *val; + let key = self.bump_index as u32; + 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::(node).next; + self.free_list_len -= 1; + *node = *val; + Ok(key) + } + pub fn insert_leaf(&mut self, new_leaf: &LeafNode) -> Result<(NodeHandle, Option)> { + // 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.get(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.get_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.get_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 >= 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.get_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.get(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())); + } + }; + } + } +} diff --git a/programs/mango-v4/src/state/orderbook/bookside_iterator.rs b/programs/mango-v4/src/state/orderbook/bookside_iterator.rs new file mode 100644 index 000000000..670a86975 --- /dev/null +++ b/programs/mango-v4/src/state/orderbook/bookside_iterator.rs @@ -0,0 +1,94 @@ +use crate::state::orderbook::bookside::{BookSide, BookSideType}; +use crate::state::orderbook::nodes::{InnerNode, LeafNode, NodeHandle, NodeRef}; + +/// Iterate over orders in order (bids=descending, asks=ascending) +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, + + now_ts: u64, +} + +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, + now_ts, + }; + if book_side.leaf_count != 0 { + iter.next_leaf = iter.find_leftmost_valid_leaf(book_side.root_node); + } + iter + } + + fn find_leftmost_valid_leaf( + &mut self, + start: NodeHandle, + ) -> Option<(NodeHandle, &'a LeafNode)> { + let mut current = start; + loop { + match self.book_side.get(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]; + } + } + } + } + } + } + } +} + +impl<'a> Iterator for BookSideIter<'a> { + type Item = (NodeHandle, &'a LeafNode); + + fn next(&mut self) -> Option { + // if next leaf is None just return it + 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 valid leaf + self.find_leftmost_valid_leaf(start) + } + }; + + current_leaf + } +} diff --git a/programs/mango-v4/src/state/orderbook/datatype.rs b/programs/mango-v4/src/state/orderbook/datatype.rs new file mode 100644 index 000000000..11723f9ba --- /dev/null +++ b/programs/mango-v4/src/state/orderbook/datatype.rs @@ -0,0 +1,18 @@ +use num_enum::{IntoPrimitive, TryFromPrimitive}; + +#[repr(u8)] +#[derive(IntoPrimitive, TryFromPrimitive)] +pub enum DataType { + MangoGroup = 0, + MangoAccount, + RootBank, + NodeBank, + PerpMarket, + Bids, + Asks, + MangoCache, + EventQueue, + AdvancedOrders, + ReferrerMemory, + ReferrerIdRecord, +} diff --git a/programs/mango-v4/src/state/orderbook/metadata.rs b/programs/mango-v4/src/state/orderbook/metadata.rs new file mode 100644 index 000000000..95bf0dbeb --- /dev/null +++ b/programs/mango-v4/src/state/orderbook/metadata.rs @@ -0,0 +1,42 @@ +use mango_macro::Pod; + +use super::datatype::DataType; + +#[derive(Copy, Clone, Pod, Default)] +#[repr(C)] +/// Stores meta information about the `Account` on chain +pub struct MetaData { + // pub data_type: u8, + pub version: u8, + // pub is_initialized: bool, + // being used by PerpMarket to store liquidity mining param + pub extra_info: [u8; 7], +} + +impl MetaData { + pub fn new( + // data_type: DataType, + version: u8, + // is_initialized: bool + ) -> Self { + Self { + // data_type: data_type as u8, + version, + // is_initialized, + extra_info: [0; 7], + } + } + pub fn new_with_extra( + // data_type: DataType, + version: u8, + // is_initialized: bool, + extra_info: [u8; 7], + ) -> Self { + Self { + // data_type: data_type as u8, + version, + // is_initialized, + extra_info, + } + } +} diff --git a/programs/mango-v4/src/state/orderbook/mod.rs b/programs/mango-v4/src/state/orderbook/mod.rs new file mode 100644 index 000000000..df02a29f7 --- /dev/null +++ b/programs/mango-v4/src/state/orderbook/mod.rs @@ -0,0 +1,20 @@ +pub use book::*; +pub use bookside::*; +pub use bookside_iterator::*; +pub use datatype::*; +pub use metadata::*; +pub use nodes::*; +pub use ob_utils::*; +pub use order_type::*; +pub use order_type::*; +pub use queue::*; + +pub mod book; +pub mod bookside; +pub mod bookside_iterator; +pub mod datatype; +pub mod metadata; +pub mod nodes; +pub mod ob_utils; +pub mod order_type; +pub mod queue; diff --git a/programs/mango-v4/src/state/orderbook/nodes.rs b/programs/mango-v4/src/state/orderbook/nodes.rs new file mode 100644 index 000000000..0585520a1 --- /dev/null +++ b/programs/mango-v4/src/state/orderbook/nodes.rs @@ -0,0 +1,272 @@ +use std::mem::size_of; + +use anchor_lang::prelude::*; +use bytemuck::{cast_mut, cast_ref}; +use mango_macro::Pod; +use num_enum::{IntoPrimitive, TryFromPrimitive}; +use static_assertions::const_assert_eq; + +use super::order_type::OrderType; + +pub type NodeHandle = u32; +const NODE_SIZE: usize = 88; + +#[derive(IntoPrimitive, TryFromPrimitive)] +#[repr(u32)] +pub enum NodeTag { + Uninitialized = 0, + InnerNode = 1, + LeafNode = 2, + FreeNode = 3, + LastFreeNode = 4, +} + +/// InnerNodes and LeafNodes compose the binary tree of orders. +/// +/// Each InnerNode has exactly two children, which are either InnerNodes themselves, +/// or LeafNodes. The children share the top `prefix_len` bits of `key`. The left +/// child has a 0 in the next bit, and the right a 1. +#[derive(Copy, Clone, Pod)] +#[repr(C)] +pub struct InnerNode { + pub tag: u32, + /// number of highest `key` bits that all children share + /// e.g. if it's 2, the two highest bits of `key` will be the same on all children + pub prefix_len: u32, + + /// only the top `prefix_len` bits of `key` are relevant + pub key: i128, + + /// indexes into `BookSide::nodes` + pub children: [NodeHandle; 2], + + /// The earliest expiry timestamp for the left and right subtrees. + /// + /// Needed to be able to find and remove expired orders without having to + /// iterate through the whole bookside. + pub child_earliest_expiry: [u64; 2], + + pub reserve: [u8; NODE_SIZE - 48], +} + +impl InnerNode { + pub fn new(prefix_len: u32, key: i128) -> Self { + Self { + tag: NodeTag::InnerNode.into(), + prefix_len, + key, + children: [0; 2], + child_earliest_expiry: [u64::MAX; 2], + reserve: [0; NODE_SIZE - 48], + } + } + + /// 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); + let crit_bit = (search_key & crit_bit_mask) != 0; + (self.children[crit_bit as usize], crit_bit) + } + + /// The lowest timestamp at which one of the contained LeafNodes expires. + #[inline(always)] + pub fn earliest_expiry(&self) -> u64 { + std::cmp::min(self.child_earliest_expiry[0], self.child_earliest_expiry[1]) + } +} + +/// LeafNodes represent an order in the binary tree +#[derive(Debug, Copy, Clone, PartialEq, Eq, Pod)] +#[repr(C)] +pub struct LeafNode { + pub tag: u32, + pub owner_slot: u8, + pub order_type: OrderType, // this was added for TradingView move order + pub version: u8, + + /// Time in seconds after `timestamp` at which the order expires. + /// A value of 0 means no expiry. + pub time_in_force: u8, + + /// The binary tree key + pub key: i128, + + pub owner: Pubkey, + pub quantity: i64, + pub client_order_id: u64, + + // Liquidity incentive related parameters + // Either the best bid or best ask at the time the order was placed + pub best_initial: i64, + + // The time the order was placed + pub timestamp: u64, +} + +#[inline(always)] +fn key_to_price(key: i128) -> i64 { + (key >> 64) as i64 +} +impl LeafNode { + pub fn new( + version: u8, + owner_slot: u8, + key: i128, + owner: Pubkey, + quantity: i64, + client_order_id: u64, + timestamp: u64, + best_initial: i64, + order_type: OrderType, + time_in_force: u8, + ) -> Self { + Self { + tag: NodeTag::LeafNode.into(), + owner_slot, + order_type, + version, + time_in_force, + key, + owner, + quantity, + client_order_id, + best_initial, + timestamp, + } + } + + #[inline(always)] + pub fn price(&self) -> i64 { + key_to_price(self.key) + } + + /// Time at which this order will expire, u64::MAX if never + #[inline(always)] + pub fn expiry(&self) -> u64 { + if self.time_in_force == 0 { + u64::MAX + } else { + self.timestamp + self.time_in_force as u64 + } + } + + #[inline(always)] + pub fn is_valid(&self, now_ts: u64) -> bool { + self.time_in_force == 0 || now_ts < self.timestamp + self.time_in_force as u64 + } +} + +#[derive(Copy, Clone, Pod)] +#[repr(C)] +pub struct FreeNode { + pub(crate) tag: u32, + pub(crate) next: NodeHandle, + pub(crate) reserve: [u8; NODE_SIZE - 8], +} + +#[derive(Copy, Clone, Pod)] +#[repr(C)] +pub struct AnyNode { + pub tag: u32, + pub data: [u8; NODE_SIZE - 4], +} + +const_assert_eq!(size_of::(), size_of::()); +const_assert_eq!(size_of::(), size_of::()); +const_assert_eq!(size_of::(), size_of::()); + +pub enum NodeRef<'a> { + Inner(&'a InnerNode), + Leaf(&'a LeafNode), +} + +pub enum NodeRefMut<'a> { + Inner(&'a mut InnerNode), + Leaf(&'a mut LeafNode), +} + +impl AnyNode { + pub fn key(&self) -> Option { + match self.case()? { + NodeRef::Inner(inner) => Some(inner.key), + NodeRef::Leaf(leaf) => Some(leaf.key), + } + } + + pub(crate) fn children(&self) -> Option<[NodeHandle; 2]> { + match self.case().unwrap() { + NodeRef::Inner(&InnerNode { children, .. }) => Some(children), + NodeRef::Leaf(_) => None, + } + } + + pub(crate) fn case(&self) -> Option { + match NodeTag::try_from(self.tag) { + Ok(NodeTag::InnerNode) => Some(NodeRef::Inner(cast_ref(self))), + Ok(NodeTag::LeafNode) => Some(NodeRef::Leaf(cast_ref(self))), + _ => None, + } + } + + fn case_mut(&mut self) -> Option { + match NodeTag::try_from(self.tag) { + Ok(NodeTag::InnerNode) => Some(NodeRefMut::Inner(cast_mut(self))), + Ok(NodeTag::LeafNode) => Some(NodeRefMut::Leaf(cast_mut(self))), + _ => None, + } + } + + #[inline] + pub fn as_leaf(&self) -> Option<&LeafNode> { + match self.case() { + Some(NodeRef::Leaf(leaf_ref)) => Some(leaf_ref), + _ => None, + } + } + + #[inline] + pub fn as_leaf_mut(&mut self) -> Option<&mut LeafNode> { + match self.case_mut() { + Some(NodeRefMut::Leaf(leaf_ref)) => Some(leaf_ref), + _ => None, + } + } + + #[inline] + pub fn as_inner(&self) -> Option<&InnerNode> { + match self.case() { + Some(NodeRef::Inner(inner_ref)) => Some(inner_ref), + _ => None, + } + } + + #[inline] + pub fn as_inner_mut(&mut self) -> Option<&mut InnerNode> { + match self.case_mut() { + Some(NodeRefMut::Inner(inner_ref)) => Some(inner_ref), + _ => None, + } + } + + #[inline] + pub fn earliest_expiry(&self) -> u64 { + match self.case().unwrap() { + NodeRef::Inner(inner) => inner.earliest_expiry(), + NodeRef::Leaf(leaf) => leaf.expiry(), + } + } +} + +impl AsRef for InnerNode { + fn as_ref(&self) -> &AnyNode { + cast_ref(self) + } +} + +impl AsRef for LeafNode { + #[inline] + fn as_ref(&self) -> &AnyNode { + cast_ref(self) + } +} diff --git a/programs/mango-v4/src/state/orderbook/ob_utils.rs b/programs/mango-v4/src/state/orderbook/ob_utils.rs new file mode 100644 index 000000000..a01b99016 --- /dev/null +++ b/programs/mango-v4/src/state/orderbook/ob_utils.rs @@ -0,0 +1,24 @@ +use anchor_lang::prelude::Error; +use bytemuck::{bytes_of, cast_slice_mut, from_bytes_mut, Contiguous, Pod}; + +use solana_program::account_info::AccountInfo; +use solana_program::program_error::ProgramError; +use solana_program::pubkey::Pubkey; +use std::cell::RefMut; +use std::mem::size_of; + +#[inline] +pub fn remove_slop_mut(bytes: &mut [u8]) -> &mut [T] { + let slop = bytes.len() % size_of::(); + let new_len = bytes.len() - slop; + cast_slice_mut(&mut bytes[..new_len]) +} + +pub fn strip_header_mut<'a, H: Pod, D: Pod>( + account: &'a AccountInfo, +) -> Result<(RefMut<'a, H>, RefMut<'a, [D]>), Error> { + Ok(RefMut::map_split(account.try_borrow_mut_data()?, |data| { + let (header_bytes, inner_bytes) = data.split_at_mut(size_of::()); + (from_bytes_mut(header_bytes), remove_slop_mut(inner_bytes)) + })) +} diff --git a/programs/mango-v4/src/state/orderbook/order_type.rs b/programs/mango-v4/src/state/orderbook/order_type.rs new file mode 100644 index 000000000..ed5ce3c83 --- /dev/null +++ b/programs/mango-v4/src/state/orderbook/order_type.rs @@ -0,0 +1,56 @@ +use anchor_lang::prelude::*; +use bytemuck::{Pod, Zeroable}; +use num_enum::{IntoPrimitive, TryFromPrimitive}; + +#[derive( + Eq, + PartialEq, + Copy, + Clone, + TryFromPrimitive, + IntoPrimitive, + Debug, + AnchorSerialize, + AnchorDeserialize, +)] +#[repr(u8)] +pub enum OrderType { + /// 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, + + /// Take existing orders up to price, max_base_quantity and max_quote_quantity. + /// Never place an order on the book. + ImmediateOrCancel = 1, + + /// 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, + + /// Ignore price and take orders up to max_base_quantity and max_quote_quantity. + /// Never place an order on the book. + /// + /// Equivalent to ImmediateOrCancel with price=i64::MAX. + Market = 3, + + /// 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, + Copy, + Clone, + TryFromPrimitive, + IntoPrimitive, + Debug, + AnchorSerialize, + AnchorDeserialize, +)] +#[repr(u8)] +pub enum Side { + Bid = 0, + Ask = 1, +} diff --git a/programs/mango-v4/src/state/orderbook/perp_market.rs b/programs/mango-v4/src/state/orderbook/perp_market.rs new file mode 100644 index 000000000..555bb230c --- /dev/null +++ b/programs/mango-v4/src/state/orderbook/perp_market.rs @@ -0,0 +1,275 @@ +use super::{book::Book, metadata::MetaData, orders::Side}; +use crate::error::MangoError; +use anchor_lang::prelude::*; +use fixed::types::I80F48; +use fixed_macro::types::I80F48; +use mango_macro::Pod; + +/// This will hold top level info about the perps market +/// Likely all perps transactions on a market will be locked on this one because this will be passed in as writable +#[account(zero_copy)] +pub struct PerpMarket { + pub meta_data: MetaData, + + pub mango_group: Pubkey, + pub bids: Pubkey, + pub asks: Pubkey, + pub event_queue: Pubkey, + pub quote_lot_size: i64, // number of quote native that reresents min tick + pub base_lot_size: i64, // represents number of base native quantity; greater than 0 + + // TODO - consider just moving this into the cache + pub long_funding: I80F48, + pub short_funding: I80F48, + + pub open_interest: i64, // This is i64 to keep consistent with the units of contracts, but should always be > 0 + + pub last_updated: u64, + pub seq_num: u64, + pub fees_accrued: I80F48, // native quote currency + + pub liquidity_mining_info: LiquidityMiningInfo, + + // mngo_vault holds mango tokens to be disbursed as liquidity incentives for this perp market + pub mngo_vault: Pubkey, +} + +impl PerpMarket { + // pub fn load_and_init<'a>( + // account: &'a AccountInfo, + // program_id: &Pubkey, + // mango_group_ai: &'a AccountInfo, + // bids_ai: &'a AccountInfo, + // asks_ai: &'a AccountInfo, + // event_queue_ai: &'a AccountInfo, + // mngo_vault_ai: &'a AccountInfo, + // mango_group: &MangoGroup, + // rent: &Rent, + // base_lot_size: i64, + // quote_lot_size: i64, + // rate: I80F48, + // max_depth_bps: I80F48, + // target_period_length: u64, + // mngo_per_period: u64, + // exp: u8, + // version: u8, + // lm_size_shift: u8, // right shift the depth number to prevent overflow + // ) -> Result> { + // let mut state = Self::load_mut(account)?; + // check!(account.owner == program_id, MangoErrorCode::InvalidOwner)?; + // check!( + // rent.is_exempt(account.lamports(), size_of::()), + // MangoErrorCode::AccountNotRentExempt + // )?; + // check!(!state.meta_data.is_initialized, MangoErrorCode::Default)?; + + // state.meta_data = MetaData::new_with_extra( + // DataType::PerpMarket, + // version, + // true, + // [exp, lm_size_shift, 0, 0, 0], + // ); + // state.mango_group = *mango_group_ai.key; + // state.bids = *bids_ai.key; + // state.asks = *asks_ai.key; + // state.event_queue = *event_queue_ai.key; + // state.quote_lot_size = quote_lot_size; + // state.base_lot_size = base_lot_size; + + // let vault = Account::unpack(&mngo_vault_ai.try_borrow_data()?)?; + // check!( + // vault.owner == mango_group.signer_key, + // MangoErrorCode::InvalidOwner + // )?; + // check!(vault.delegate.is_none(), MangoErrorCode::InvalidVault)?; + // check!( + // vault.close_authority.is_none(), + // MangoErrorCode::InvalidVault + // )?; + // check!(vault.mint == mngo_token::ID, MangoErrorCode::InvalidVault)?; + // check!( + // mngo_vault_ai.owner == &spl_token::ID, + // MangoErrorCode::InvalidOwner + // )?; + // state.mngo_vault = *mngo_vault_ai.key; + + // let clock = Clock::get()?; + // let period_start = clock.unix_timestamp as u64; + // state.last_updated = period_start; + + // state.liquidity_mining_info = LiquidityMiningInfo { + // rate, + // max_depth_bps, + // period_start, + // target_period_length, + // mngo_left: mngo_per_period, + // mngo_per_period, + // }; + + // Ok(state) + // } + + // pub fn load_checked<'a>( + // account: &'a AccountInfo, + // program_id: &Pubkey, + // mango_group_pk: &Pubkey, + // ) -> MangoResult> { + // check_eq!(account.owner, program_id, MangoErrorCode::InvalidOwner)?; + // let state = Self::load(account)?; + // check!(state.meta_data.is_initialized, MangoErrorCode::Default)?; + // check!( + // state.meta_data.data_type == DataType::PerpMarket as u8, + // MangoErrorCode::Default + // )?; + // check!( + // mango_group_pk == &state.mango_group, + // MangoErrorCode::Default + // )?; + // Ok(state) + // } + + // pub fn load_mut_checked<'a>( + // account: &'a AccountInfo, + // program_id: &Pubkey, + // mango_group_pk: &Pubkey, + // ) -> MangoResult> { + // check_eq!(account.owner, program_id, MangoErrorCode::InvalidOwner)?; + // let state = Self::load_mut(account)?; + // check!( + // state.meta_data.is_initialized, + // MangoErrorCode::InvalidAccountState + // )?; + // check!( + // state.meta_data.data_type == DataType::PerpMarket as u8, + // MangoErrorCode::InvalidAccountState + // )?; + // check!( + // mango_group_pk == &state.mango_group, + // MangoErrorCode::InvalidAccountState + // )?; + // Ok(state) + // } + + pub fn gen_order_id(&mut self, side: Side, price: i64) -> i128 { + 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), + } + } + + /// Use current order book price and index price to update the instantaneous funding + pub fn update_funding( + &mut self, + mango_group: &MangoGroup, + book: &Book, + mango_cache: &MangoCache, + market_index: usize, + now_ts: u64, + ) -> Result<()> { + // Get the index price from cache, ensure it's not outdated + let price_cache = &mango_cache.price_cache[market_index]; + price_cache.check_valid(&mango_group, now_ts)?; + + let index_price = price_cache.price; + // hard-coded for now because there's no convenient place to put this; also creates breaking + // change if we make this a parameter + const IMPACT_QUANTITY: i64 = 100; + + // Get current book price & compare it to index price + let bid = book.get_impact_price(Side::Bid, IMPACT_QUANTITY, now_ts); + let ask = book.get_impact_price(Side::Ask, IMPACT_QUANTITY, now_ts); + + const MAX_FUNDING: I80F48 = I80F48!(0.05); + const MIN_FUNDING: I80F48 = I80F48!(-0.05); + + let diff = match (bid, ask) { + (Some(bid), Some(ask)) => { + // calculate mid-market rate + let book_price = self.lot_to_native_price((bid + ask) / 2); + (book_price / index_price - I80F48::ONE).clamp(MIN_FUNDING, MAX_FUNDING) + } + (Some(_bid), None) => MAX_FUNDING, + (None, Some(_ask)) => MIN_FUNDING, + (None, None) => I80F48::ZERO, + }; + + // TODO TEST consider what happens if time_factor is very small. Can funding_delta == 0 when diff != 0? + let time_factor = I80F48::from_num(now_ts - self.last_updated) / DAY; + let funding_delta: I80F48 = index_price + .checked_mul(diff) + .unwrap() + .checked_mul(I80F48::from_num(self.base_lot_size)) + .unwrap() + .checked_mul(time_factor) + .unwrap(); + + self.long_funding += funding_delta; + self.short_funding += funding_delta; + self.last_updated = now_ts; + + // Check if liquidity incentives ought to be paid out and if so pay them out + Ok(()) + } + + /// Convert from the price stored on the book to the price used in value calculations + pub fn lot_to_native_price(&self, price: i64) -> I80F48 { + I80F48::from_num(price) + .checked_mul(I80F48::from_num(self.quote_lot_size)) + .unwrap() + .checked_div(I80F48::from_num(self.base_lot_size)) + .unwrap() + } + + /// Socialize the loss in this account across all longs and shorts + pub fn socialize_loss( + &mut self, + account: &mut PerpAccount, + cache: &mut PerpMarketCache, + ) -> Result { + // TODO convert into only socializing on one side + // native USDC per contract open interest + let socialized_loss = if self.open_interest == 0 { + // This is kind of an unfortunate situation. This means socialized loss occurs on the + // last person to call settle_pnl on their profits. Any advice on better mechanism + // would be appreciated. Luckily, this will be an extremely rare situation. + I80F48::ZERO + } else { + account + .quote_position + .checked_div(I80F48::from_num(self.open_interest)) + .ok_or(MangoError::SomeError)? + }; + account.quote_position = I80F48::ZERO; + self.long_funding -= socialized_loss; + self.short_funding += socialized_loss; + + cache.short_funding = self.short_funding; + cache.long_funding = self.long_funding; + Ok(socialized_loss) + } +} + +#[derive(Copy, Clone, Pod)] +#[repr(C)] +/// Information regarding market maker incentives for a perp market +pub struct LiquidityMiningInfo { + /// Used to convert liquidity points to MNGO + pub rate: I80F48, + + pub max_depth_bps: I80F48, // instead of max depth bps, this should be max num contracts + + /// start timestamp of current liquidity incentive period; gets updated when mngo_left goes to 0 + pub period_start: u64, + + /// Target time length of a period in seconds + pub target_period_length: u64, + + /// Paper MNGO left for this period + pub mngo_left: u64, + + /// Total amount of MNGO allocated for current period + pub mngo_per_period: u64, +} diff --git a/programs/mango-v4/src/state/orderbook/perp_market_info.rs b/programs/mango-v4/src/state/orderbook/perp_market_info.rs new file mode 100644 index 000000000..a976b5221 --- /dev/null +++ b/programs/mango-v4/src/state/orderbook/perp_market_info.rs @@ -0,0 +1,18 @@ +use anchor_lang::prelude::*; +use fixed::types::I80F48; +use mango_macro::Pod; + +#[derive(Copy, Clone, Pod)] +#[repr(C)] +pub struct PerpMarketInfo { + pub perp_market: Pubkey, // One of these may be empty + pub maint_asset_weight: I80F48, + pub init_asset_weight: I80F48, + pub maint_liab_weight: I80F48, + pub init_liab_weight: I80F48, + pub liquidation_fee: I80F48, + pub maker_fee: I80F48, + pub taker_fee: I80F48, + pub base_lot_size: i64, // The lot size of the underlying + pub quote_lot_size: i64, // min tick +} diff --git a/programs/mango-v4/src/state/orderbook/queue.rs b/programs/mango-v4/src/state/orderbook/queue.rs new file mode 100644 index 000000000..0c8362af8 --- /dev/null +++ b/programs/mango-v4/src/state/orderbook/queue.rs @@ -0,0 +1,438 @@ +use std::cell::RefMut; +use std::mem::size_of; + +use crate::error::MangoError; +use crate::state::orderbook::datatype::DataType; +use crate::state::PerpMarket; +use anchor_lang::prelude::*; +use fixed::types::I80F48; +use num_enum::{IntoPrimitive, TryFromPrimitive}; +use solana_program::account_info::AccountInfo; +use solana_program::pubkey::Pubkey; +use solana_program::sysvar::rent::Rent; +use static_assertions::const_assert_eq; + +// use mango_logs::FillLog; +use mango_macro::Pod; + +use super::metadata::MetaData; +use super::ob_utils::strip_header_mut; +use super::order_type::Side; +// use safe_transmute::{self, trivial::TriviallyTransmutable}; + +// use crate::error::{check_assert, MangoErrorCode, MangoResult, SourceFileId}; +// use crate::matching::Side; +// use crate::state::{DataType, MetaData, PerpMarket}; +// use crate::utils::strip_header_mut; + +// Don't want event queue to become single threaded if it's logging liquidations +// Most common scenario will be liqors depositing USDC and withdrawing some other token +// So tying it to token deposited is not wise +// also can't tie it to token withdrawn because during bull market, liqs will be depositing all base tokens and withdrawing quote +// + +pub trait QueueHeader: bytemuck::Pod { + type Item: bytemuck::Pod + Copy; + + fn head(&self) -> usize; + fn set_head(&mut self, value: usize); + fn count(&self) -> usize; + fn set_count(&mut self, value: usize); + + fn incr_event_id(&mut self); + fn decr_event_id(&mut self, n: usize); +} + +pub struct Queue<'a, H: QueueHeader> { + pub header: RefMut<'a, H>, + pub buf: RefMut<'a, [H::Item]>, +} + +impl<'a, H: QueueHeader> Queue<'a, H> { + pub fn new(header: RefMut<'a, H>, buf: RefMut<'a, [H::Item]>) -> Self { + Self { header, buf } + } + + pub fn load_mut(account: &'a AccountInfo) -> Result { + let (header, buf) = strip_header_mut::(account)?; + Ok(Self { header, buf }) + } + + pub fn len(&self) -> usize { + self.header.count() + } + + pub fn full(&self) -> bool { + self.header.count() == self.buf.len() + } + + pub fn empty(&self) -> bool { + self.header.count() == 0 + } + + pub fn push_back(&mut self, value: H::Item) -> std::result::Result<(), H::Item> { + if self.full() { + return Err(value); + } + let slot = (self.header.head() + self.header.count()) % self.buf.len(); + self.buf[slot] = value; + + let count = self.header.count(); + self.header.set_count(count + 1); + + self.header.incr_event_id(); + Ok(()) + } + + pub fn peek_front(&self) -> Option<&H::Item> { + if self.empty() { + return None; + } + Some(&self.buf[self.header.head()]) + } + + pub fn peek_front_mut(&mut self) -> Option<&mut H::Item> { + if self.empty() { + return None; + } + Some(&mut self.buf[self.header.head()]) + } + + pub fn pop_front(&mut self) -> std::result::Result { + if self.empty() { + return Err(()); + } + let value = self.buf[self.header.head()]; + + let count = self.header.count(); + self.header.set_count(count - 1); + + let head = self.header.head(); + self.header.set_head((head + 1) % self.buf.len()); + + Ok(value) + } + + pub fn revert_pushes(&mut self, desired_len: usize) -> Result<()> { + require!(desired_len <= self.header.count(), MangoError::SomeError); + let len_diff = self.header.count() - desired_len; + self.header.set_count(desired_len); + self.header.decr_event_id(len_diff); + Ok(()) + } + + pub fn iter(&self) -> impl Iterator { + QueueIterator { + queue: self, + index: 0, + } + } +} + +struct QueueIterator<'a, 'b, H: QueueHeader> { + queue: &'b Queue<'a, H>, + index: usize, +} + +impl<'a, 'b, H: QueueHeader> Iterator for QueueIterator<'a, 'b, H> { + type Item = &'b H::Item; + fn next(&mut self) -> Option { + if self.index == self.queue.len() { + None + } else { + let item = + &self.queue.buf[(self.queue.header.head() + self.index) % self.queue.buf.len()]; + self.index += 1; + Some(item) + } + } +} + +#[account(zero_copy)] +pub struct EventQueueHeader { + pub meta_data: MetaData, + head: usize, + count: usize, + pub seq_num: usize, +} +// unsafe impl TriviallyTransmutable for EventQueueHeader {} + +impl QueueHeader for EventQueueHeader { + type Item = AnyEvent; + + fn head(&self) -> usize { + self.head + } + fn set_head(&mut self, value: usize) { + self.head = value; + } + fn count(&self) -> usize { + self.count + } + fn set_count(&mut self, value: usize) { + self.count = value; + } + fn incr_event_id(&mut self) { + self.seq_num += 1; + } + fn decr_event_id(&mut self, n: usize) { + self.seq_num -= n; + } +} + +pub type EventQueue<'a> = Queue<'a, EventQueueHeader>; + +impl<'a> EventQueue<'a> { + pub fn load_mut_checked( + account: &'a AccountInfo, + program_id: &Pubkey, + perp_market: &PerpMarket, + ) -> Result { + require!(account.owner == program_id, MangoError::SomeError); // MangoErrorCode::InvalidOwner + // require!( + // &perp_market.event_queue == account.key, + // MangoError::SomeError + // ); // MangoErrorCode::InvalidAccount + Self::load_mut(account) + } + + pub fn load_and_init( + account: &'a AccountInfo, + program_id: &Pubkey, + rent: &Rent, + ) -> Result { + // NOTE: check this first so we can borrow account later + require!( + rent.is_exempt(account.lamports(), account.data_len()), + MangoError::SomeError + ); //MangoErrorCode::AccountNotRentExempt + + let mut state = Self::load_mut(account)?; + require!(account.owner == program_id, MangoError::SomeError); // MangoErrorCode::InvalidOwner + + // require!( + // !state.header.meta_data.is_initialized, + // MangoError::SomeError + // ); + // state.header.meta_data = MetaData::new(DataType::EventQueue, 0, true); + + Ok(state) + } +} + +#[derive(Copy, Clone, IntoPrimitive, TryFromPrimitive, Eq, PartialEq)] +#[repr(u8)] +pub enum EventType { + Fill, + Out, + Liquidate, +} + +const EVENT_SIZE: usize = 200; +#[derive(Copy, Clone, Debug, Pod)] +#[repr(C)] +pub struct AnyEvent { + pub event_type: u8, + pub padding: [u8; EVENT_SIZE - 1], +} +// unsafe impl TriviallyTransmutable for AnyEvent {} + +#[derive(Copy, Clone, Debug, Pod)] +#[repr(C)] +pub struct FillEvent { + pub event_type: u8, + pub taker_side: Side, // side from the taker's POV + pub maker_slot: u8, + pub maker_out: bool, // true if maker order quantity == 0 + pub version: u8, + pub market_fees_applied: bool, + pub padding: [u8; 2], + pub timestamp: u64, + pub seq_num: usize, // note: usize same as u64 + + pub maker: Pubkey, + pub maker_order_id: i128, + pub maker_client_order_id: u64, + pub maker_fee: I80F48, + + // The best bid/ask at the time the maker order was placed. Used for liquidity incentives + pub best_initial: i64, + + // Timestamp of when the maker order was placed; copied over from the LeafNode + pub maker_timestamp: u64, + + pub taker: Pubkey, + pub taker_order_id: i128, + pub taker_client_order_id: u64, + pub taker_fee: I80F48, + + pub price: i64, + pub quantity: i64, // number of quote lots +} +// unsafe impl TriviallyTransmutable for FillEvent {} + +impl FillEvent { + pub fn new( + taker_side: Side, + maker_slot: u8, + maker_out: bool, + timestamp: u64, + seq_num: usize, + maker: Pubkey, + maker_order_id: i128, + maker_client_order_id: u64, + maker_fee: I80F48, + best_initial: i64, + maker_timestamp: u64, + + taker: Pubkey, + taker_order_id: i128, + taker_client_order_id: u64, + taker_fee: I80F48, + price: i64, + quantity: i64, + version: u8, + ) -> FillEvent { + Self { + event_type: EventType::Fill as u8, + taker_side, + maker_slot, + maker_out, + version, + market_fees_applied: true, // Since mango v3.3.5, market fees are adjusted at matching time + padding: [0u8; 2], + timestamp, + seq_num, + maker, + maker_order_id, + maker_client_order_id, + maker_fee, + best_initial, + maker_timestamp, + taker, + taker_order_id, + taker_client_order_id, + taker_fee, + price, + quantity, + } + } + + pub fn base_quote_change(&self, side: Side) -> (i64, i64) { + match side { + Side::Bid => ( + self.quantity, + -self.price.checked_mul(self.quantity).unwrap(), + ), + Side::Ask => ( + -self.quantity, + self.price.checked_mul(self.quantity).unwrap(), + ), + } + } + + // pub fn to_fill_log(&self, mango_group: Pubkey, market_index: usize) -> FillLog { + // FillLog { + // mango_group, + // market_index: market_index as u64, + // taker_side: self.taker_side as u8, + // maker_slot: self.maker_slot, + // maker_out: self.maker_out, + // timestamp: self.timestamp, + // seq_num: self.seq_num as u64, + // maker: self.maker, + // maker_order_id: self.maker_order_id, + // maker_client_order_id: self.maker_client_order_id, + // maker_fee: self.maker_fee.to_bits(), + // best_initial: self.best_initial, + // maker_timestamp: self.maker_timestamp, + // taker: self.taker, + // taker_order_id: self.taker_order_id, + // taker_client_order_id: self.taker_client_order_id, + // taker_fee: self.taker_fee.to_bits(), + // price: self.price, + // quantity: self.quantity, + // } + // } +} + +#[derive(Copy, Clone, Debug, Pod)] +#[repr(C)] +pub struct OutEvent { + pub event_type: u8, + pub side: Side, + pub slot: u8, + padding0: [u8; 5], + pub timestamp: u64, + pub seq_num: usize, + pub owner: Pubkey, + pub quantity: i64, + padding1: [u8; EVENT_SIZE - 64], +} +// unsafe impl TriviallyTransmutable for OutEvent {} +impl OutEvent { + pub fn new( + side: Side, + slot: u8, + timestamp: u64, + seq_num: usize, + owner: Pubkey, + quantity: i64, + ) -> Self { + Self { + event_type: EventType::Out.into(), + side, + slot, + padding0: [0; 5], + timestamp, + seq_num, + owner, + quantity, + padding1: [0; EVENT_SIZE - 64], + } + } +} + +#[derive(Copy, Clone, Debug, Pod)] +#[repr(C)] +/// Liquidation for the PerpMarket this EventQueue is for +pub struct LiquidateEvent { + pub event_type: u8, + padding0: [u8; 7], + pub timestamp: u64, + pub seq_num: usize, + pub liqee: Pubkey, + pub liqor: Pubkey, + pub price: I80F48, // oracle price at the time of liquidation + pub quantity: i64, // number of contracts that were moved from liqee to liqor + pub liquidation_fee: I80F48, // liq fee for this earned for this market + padding1: [u8; EVENT_SIZE - 128], +} +// unsafe impl TriviallyTransmutable for LiquidateEvent {} +impl LiquidateEvent { + pub fn new( + timestamp: u64, + seq_num: usize, + liqee: Pubkey, + liqor: Pubkey, + price: I80F48, + quantity: i64, + liquidation_fee: I80F48, + ) -> Self { + Self { + event_type: EventType::Liquidate.into(), + padding0: [0u8; 7], + timestamp, + seq_num, + liqee, + liqor, + price, + quantity, + liquidation_fee, + padding1: [0u8; EVENT_SIZE - 128], + } + } +} +const_assert_eq!(size_of::(), size_of::()); +const_assert_eq!(size_of::(), size_of::()); +const_assert_eq!(size_of::(), size_of::()); diff --git a/programs/mango-v4/src/state/perp_market.rs b/programs/mango-v4/src/state/perp_market.rs index 91cef156a..a28524a10 100644 --- a/programs/mango-v4/src/state/perp_market.rs +++ b/programs/mango-v4/src/state/perp_market.rs @@ -1,5 +1,6 @@ use anchor_lang::prelude::*; +use crate::state::orderbook::order_type::Side; use crate::state::TokenIndex; pub type PerpMarketIndex = u16; @@ -7,81 +8,63 @@ pub type PerpMarketIndex = u16; #[account(zero_copy)] pub struct EventQueue {} -#[account(zero_copy)] -pub struct Book {} - #[account(zero_copy)] pub struct PerpMarket { - // todo - /// metadata - // pub meta_data: MetaData, - - /// mango group pub group: Pubkey, - // todo better docs - /// pub oracle: Pubkey, - /// order book pub bids: Pubkey, pub asks: Pubkey, - // todo better docs - /// - pub event_queue: Pubkey, + /// Event queue of TODO + /// pub event_queue: Pubkey, - /// number of quote native that reresents min tick - /// e.g. base lot size 100, quote lot size 10, then tick i.e. price increment is 10/100 i.e. 1 - // todo: why signed? + /// Number of quote native that reresents min tick + /// e.g. when base lot size is 100, and quote lot size is 10, then tick i.e. price increment is 10/100 i.e. 0.1 pub quote_lot_size: i64, - /// represents number of base native quantity; greater than 0 - /// e.g. base decimals 6, base lot size 100, base position 10000, then + + /// Represents number of base native quantity + /// e.g. if base decimals for underlying asset are 6, base lot size is 100, and base position is 10000, then /// UI position is 1 - // todo: why signed? pub base_lot_size: i64, - // todo - /// an always increasing number (except in case of socializing losses), incremented by - /// funding delta, funding delta is difference between book and index price which needs to be paid every day, - /// funding delta is measured per day - per base lots - the larger users position the more funding - /// he pays, funding is always paid in quote - // pub long_funding: I80F48, - // pub short_funding: I80F48, - // todo - /// timestamp when funding was last updated - // pub last_updated: u64, + /// pub long_funding: I80F48, + /// pub short_funding: I80F48, + /// pub funding_last_updated: u64, - // todo - /// This is i64 to keep consistent with the units of contracts, but should always be > 0 - // todo: why signed? - // pub open_interest: i64, + /// pub open_interest: u64, - // todo - /// number of orders generated + /// Total number of orders seen pub seq_num: u64, - // todo - /// in native quote currency - // pub fees_accrued: I80F48, + /// Fees accrued in native quote currency + /// pub fees_accrued: I80F48, - // todo - /// liquidity mining - // pub liquidity_mining_info: LiquidityMiningInfo, + /// Liquidity mining metadata + /// pub liquidity_mining_info: LiquidityMiningInfo, - // todo - /// token vault which holds mango tokens to be disbursed as liquidity incentives for this perp market - // pub mngo_vault: Pubkey, + /// Token vault which holds mango tokens to be disbursed as liquidity incentives for this perp market + /// pub mngo_vault: Pubkey, - /// pda bump + /// PDA bump pub bump: u8, - /// useful for looking up respective perp account + /// Lookup indices pub perp_market_index: PerpMarketIndex, - /// useful for looking up respective base token, - /// note: is optional, since perp market can exist without a corresponding base token, - /// should be TokenIndex::MAX in that case pub base_token_index: TokenIndex, - /// useful for looking up respective quote token pub quote_token_index: TokenIndex, } + +impl PerpMarket { + /// TODO why is this based on price? + pub fn gen_order_id(&mut self, side: Side, price: i64) -> i128 { + 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), + } + } +} diff --git a/programs/mango-v4/src/util.rs b/programs/mango-v4/src/util.rs index 4a029e79a..6f2f656fb 100644 --- a/programs/mango-v4/src/util.rs +++ b/programs/mango-v4/src/util.rs @@ -1,6 +1,7 @@ use anchor_lang::prelude::*; use anchor_lang::ZeroCopy; use arrayref::array_ref; +use std::cell::RefMut; use std::{cell::Ref, mem}; #[macro_export] @@ -27,11 +28,34 @@ pub trait LoadZeroCopy { /// It checks the account owner and discriminator, then casts the data. fn load(&self) -> Result>; + /// Same as load(), but mut + fn load_mut(&self) -> Result>; + /// Same as load(), but doesn't check the discriminator. fn load_unchecked(&self) -> Result>; + + /// Same as load_unchecked(), but mut + fn load_unchecked_mut(&self) -> Result>; } impl<'info> LoadZeroCopy for AccountInfo<'info> { + fn load_mut(&self) -> Result> { + if self.owner != &T::owner() { + return Err(ErrorCode::AccountOwnedByWrongProgram.into()); + } + + let data = self.try_borrow_mut_data()?; + + let disc_bytes = array_ref![data, 0, 8]; + if disc_bytes != &T::discriminator() { + return Err(ErrorCode::AccountDiscriminatorMismatch.into()); + } + + Ok(RefMut::map(data, |data| { + bytemuck::from_bytes_mut(&mut data[8..mem::size_of::() + 8]) + })) + } + fn load(&self) -> Result> { if self.owner != &T::owner() { return Err(ErrorCode::AccountOwnedByWrongProgram.into()); @@ -49,6 +73,18 @@ impl<'info> LoadZeroCopy for AccountInfo<'info> { })) } + fn load_unchecked_mut(&self) -> Result> { + if self.owner != &T::owner() { + return Err(ErrorCode::AccountOwnedByWrongProgram.into()); + } + + let data = self.try_borrow_mut_data()?; + + Ok(RefMut::map(data, |data| { + bytemuck::from_bytes_mut(&mut data[8..mem::size_of::() + 8]) + })) + } + fn load_unchecked(&self) -> Result> { if self.owner != &T::owner() { return Err(ErrorCode::AccountOwnedByWrongProgram.into()); diff --git a/programs/mango-v4/tests/program_test/mango_client.rs b/programs/mango-v4/tests/program_test/mango_client.rs index 88c6f8347..2fe3c1fd5 100644 --- a/programs/mango-v4/tests/program_test/mango_client.rs +++ b/programs/mango-v4/tests/program_test/mango_client.rs @@ -1076,8 +1076,10 @@ impl ClientInstruction for Serum3LiqForceCancelOrdersInstruction { pub struct CreatePerpMarketInstruction<'keypair> { pub group: Pubkey, - pub mint: Pubkey, pub admin: &'keypair Keypair, + pub oracle: Pubkey, + pub asks: Pubkey, + pub bids: Pubkey, pub payer: &'keypair Keypair, pub perp_market_index: PerpMarketIndex, pub base_token_index: TokenIndex, @@ -1102,12 +1104,6 @@ impl<'keypair> ClientInstruction for CreatePerpMarketInstruction<'keypair> { base_lot_size: self.base_lot_size, }; - let oracle = Pubkey::find_program_address( - &[b"StubOracle".as_ref(), self.mint.as_ref()], - &program_id, - ) - .0; - let perp_market = Pubkey::find_program_address( &[ self.group.as_ref(), @@ -1117,34 +1113,14 @@ impl<'keypair> ClientInstruction for CreatePerpMarketInstruction<'keypair> { &program_id, ) .0; - let asks = Pubkey::find_program_address( - &[self.group.as_ref(), b"Asks".as_ref(), perp_market.as_ref()], - &program_id, - ) - .0; - let bids = Pubkey::find_program_address( - &[self.group.as_ref(), b"Bids".as_ref(), perp_market.as_ref()], - &program_id, - ) - .0; - let event_queue = Pubkey::find_program_address( - &[ - self.group.as_ref(), - b"EventQueue".as_ref(), - perp_market.as_ref(), - ], - &program_id, - ) - .0; let accounts = Self::Accounts { group: self.group, admin: self.admin.pubkey(), - oracle, + oracle: self.oracle, perp_market, - asks, - bids, - event_queue, + asks: self.asks, + bids: self.bids, payer: self.payer.pubkey(), system_program: System::id(), }; @@ -1157,3 +1133,51 @@ impl<'keypair> ClientInstruction for CreatePerpMarketInstruction<'keypair> { vec![self.admin, self.payer] } } + +pub struct PlacePerpOrderInstruction<'keypair> { + pub group: Pubkey, + pub account: Pubkey, + pub perp_market: Pubkey, + pub asks: Pubkey, + pub bids: Pubkey, + pub oracle: Pubkey, + pub owner: &'keypair Keypair, +} +#[async_trait::async_trait(?Send)] +impl<'keypair> ClientInstruction for PlacePerpOrderInstruction<'keypair> { + type Accounts = mango_v4::accounts::PlacePerpOrder; + type Instruction = mango_v4::instruction::PlacePerpOrder; + async fn to_instruction( + &self, + _loader: impl ClientAccountLoader + 'async_trait, + ) -> (Self::Accounts, instruction::Instruction) { + let program_id = mango_v4::id(); + let instruction = Self::Instruction { + side: Side::Bid, + price: 1, + max_base_quantity: 1, + max_quote_quantity: 1, + client_order_id: 0, + order_type: OrderType::Limit, + reduce_only: false, + expiry_timestamp: 0, + limit: 1, + }; + let accounts = Self::Accounts { + group: self.group, + account: self.account, + perp_market: self.perp_market, + asks: self.asks, + bids: self.bids, + oracle: self.oracle, + owner: self.owner.pubkey(), + }; + + let instruction = make_instruction(program_id, &accounts, instruction); + (accounts, instruction) + } + + fn signers(&self) -> Vec<&Keypair> { + vec![self.owner] + } +} diff --git a/programs/mango-v4/tests/program_test/solana.rs b/programs/mango-v4/tests/program_test/solana.rs index eac786683..49dd4395d 100644 --- a/programs/mango-v4/tests/program_test/solana.rs +++ b/programs/mango-v4/tests/program_test/solana.rs @@ -83,6 +83,23 @@ impl SolanaCookie { .newest() } + pub async fn create_account(&self, owner: &Pubkey) -> Pubkey { + let key = Keypair::new(); + let len = 8 + std::mem::size_of::(); + let rent = self.rent.minimum_balance(len); + let create_account_instr = solana_sdk::system_instruction::create_account( + &self.context.borrow().payer.pubkey(), + &key.pubkey(), + rent, + len as u64, + &owner, + ); + self.process_transaction(&[create_account_instr], Some(&[&key])) + .await + .unwrap(); + key.pubkey() + } + #[allow(dead_code)] pub async fn create_token_account(&self, owner: &Pubkey, mint: Pubkey) -> Pubkey { let keypair = Keypair::new(); @@ -145,6 +162,16 @@ impl SolanaCookie { #[allow(dead_code)] pub async fn get_account_opt(&self, address: Pubkey) -> Option { + let account = self + .context + .borrow_mut() + .banks_client + .get_account(address) + .await + .unwrap() + .unwrap(); + println!("{:#?}", account.owner); + let data = self.get_account_data(address).await?; let mut data_slice: &[u8] = &data; AccountDeserialize::try_deserialize(&mut data_slice).ok() diff --git a/programs/mango-v4/tests/test_perp.rs b/programs/mango-v4/tests/test_perp.rs index d33f0ee43..736f4e842 100644 --- a/programs/mango-v4/tests/test_perp.rs +++ b/programs/mango-v4/tests/test_perp.rs @@ -1,5 +1,6 @@ #![cfg(feature = "test-bpf")] +use mango_v4::state::BookSide; use solana_program_test::*; use solana_sdk::{signature::Keypair, transport::TransportError}; @@ -77,13 +78,26 @@ async fn test_perp() -> Result<(), TransportError> { // // TEST: Create a perp market // - let _perp_market = send_tx( + let mango_v4::accounts::CreatePerpMarket { + perp_market, + asks, + bids, + .. + } = send_tx( solana, CreatePerpMarketInstruction { group, + oracle: tokens[0].oracle, + asks: context + .solana + .create_account::(&mango_v4::id()) + .await, + bids: context + .solana + .create_account::(&mango_v4::id()) + .await, admin, payer, - mint: mints[0].pubkey, perp_market_index: 0, base_token_index: tokens[0].index, quote_token_index: tokens[1].index, @@ -93,8 +107,22 @@ async fn test_perp() -> Result<(), TransportError> { }, ) .await - .unwrap() - .perp_market; + .unwrap(); + + send_tx( + solana, + PlacePerpOrderInstruction { + group, + account, + perp_market, + asks, + bids, + oracle: tokens[0].oracle, + owner, + }, + ) + .await + .unwrap(); Ok(()) }