further work on perps

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>
This commit is contained in:
microwavedcola1 2022-03-21 20:29:28 +01:00
parent 34d14ef267
commit ec5e959804
26 changed files with 3437 additions and 141 deletions

22
Cargo.lock generated
View File

@ -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",

12
mango-macro/Cargo.toml Normal file
View File

@ -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"

20
mango-macro/src/lib.rs Normal file
View File

@ -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()
}

View File

@ -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 }

View File

@ -22,30 +22,13 @@ pub struct CreatePerpMarket<'info> {
space = 8 + std::mem::size_of::<PerpMarket>(),
)]
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::<Book>(),
)]
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::<Book>(),
)]
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::<EventQueue>(),
)]
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(())
}

View File

@ -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<PlacePerpOrder>,
// 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<PlacePerpOrder>) -> Result<()> {
Ok(())
}

View File

@ -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<PlacePerpOrder>) -> Result<()> {
instructions::place_perp_order(ctx)
pub fn place_perp_order(
ctx: Context<PlacePerpOrder>,
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,
)
}
}

View File

@ -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 {

View File

@ -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;

File diff suppressed because it is too large Load Diff

View File

@ -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<RefMut<'a, Self>> {
let state = account.load_mut::<BookSide>()?;
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<RefMut<'a, Self>> {
// // 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<LeafNode> {
self.remove_by_key(self.get(self.find_min()?)?.key()?)
}
pub fn remove_max(&mut self) -> Option<LeafNode> {
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<LeafNode> {
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<NodeHandle> {
self.find_min_max(true)
}
pub fn root(&self) -> Option<NodeHandle> {
if self.leaf_count == 0 {
None
} else {
Some(self.root_node)
}
}
pub fn find_min(&self) -> Option<NodeHandle> {
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<NodeHandle> {
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<LeafNode> {
// path of InnerNode handles that lead to the removed leaf
let mut stack: Vec<(NodeHandle, bool)> = vec![];
// special case potentially removing the root
let mut parent_h = self.root()?;
let (mut child_h, mut crit_bit) = match self.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<AnyNode> {
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<NodeHandle> {
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::<AnyNode, FreeNode>(node).next;
self.free_list_len -= 1;
*node = *val;
Ok(key)
}
pub fn insert_leaf(&mut self, new_leaf: &LeafNode) -> Result<(NodeHandle, Option<LeafNode>)> {
// path of InnerNode handles that lead to the new leaf
let mut stack: Vec<(NodeHandle, bool)> = vec![];
// deal with inserts into an empty tree
let mut root: NodeHandle = match self.root() {
Some(h) => h,
None => {
// create a new root if none exists
let handle = self.insert(new_leaf.as_ref())?;
self.root_node = handle;
self.leaf_count = 1;
return Ok((handle, None));
}
};
// walk down the tree until we find the insert location
loop {
// require if the new node will be a child of the root
let root_contents = *self.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()));
}
};
}
}
}

View File

@ -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<Self::Item> {
// 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
}
}

View File

@ -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,
}

View File

@ -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,
}
}
}

View File

@ -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;

View File

@ -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::<AnyNode>(), size_of::<InnerNode>());
const_assert_eq!(size_of::<AnyNode>(), size_of::<LeafNode>());
const_assert_eq!(size_of::<AnyNode>(), size_of::<FreeNode>());
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<i128> {
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<NodeRef> {
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<NodeRefMut> {
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<AnyNode> for InnerNode {
fn as_ref(&self) -> &AnyNode {
cast_ref(self)
}
}
impl AsRef<AnyNode> for LeafNode {
#[inline]
fn as_ref(&self) -> &AnyNode {
cast_ref(self)
}
}

View File

@ -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<T: Pod>(bytes: &mut [u8]) -> &mut [T] {
let slop = bytes.len() % size_of::<T>();
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::<H>());
(from_bytes_mut(header_bytes), remove_slop_mut(inner_bytes))
}))
}

View File

@ -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,
}

View File

@ -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<RefMut<'a, Self>> {
// let mut state = Self::load_mut(account)?;
// check!(account.owner == program_id, MangoErrorCode::InvalidOwner)?;
// check!(
// rent.is_exempt(account.lamports(), size_of::<Self>()),
// 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<Ref<'a, Self>> {
// 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<RefMut<'a, Self>> {
// 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<I80F48> {
// 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,
}

View File

@ -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
}

View File

@ -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<Self> {
let (header, buf) = strip_header_mut::<H, H::Item>(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<H::Item, ()> {
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<Item = &H::Item> {
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<Self::Item> {
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<Self> {
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<Self> {
// 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::<AnyEvent>(), size_of::<FillEvent>());
const_assert_eq!(size_of::<AnyEvent>(), size_of::<OutEvent>());
const_assert_eq!(size_of::<AnyEvent>(), size_of::<LiquidateEvent>());

View File

@ -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),
}
}
}

View File

@ -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<T: ZeroCopy + Owner>(&self) -> Result<Ref<T>>;
/// Same as load(), but mut
fn load_mut<T: ZeroCopy + Owner>(&self) -> Result<RefMut<T>>;
/// Same as load(), but doesn't check the discriminator.
fn load_unchecked<T: ZeroCopy + Owner>(&self) -> Result<Ref<T>>;
/// Same as load_unchecked(), but mut
fn load_unchecked_mut<T: ZeroCopy + Owner>(&self) -> Result<RefMut<T>>;
}
impl<'info> LoadZeroCopy for AccountInfo<'info> {
fn load_mut<T: ZeroCopy + Owner>(&self) -> Result<RefMut<T>> {
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::<T>() + 8])
}))
}
fn load<T: ZeroCopy + Owner>(&self) -> Result<Ref<T>> {
if self.owner != &T::owner() {
return Err(ErrorCode::AccountOwnedByWrongProgram.into());
@ -49,6 +73,18 @@ impl<'info> LoadZeroCopy for AccountInfo<'info> {
}))
}
fn load_unchecked_mut<T: ZeroCopy + Owner>(&self) -> Result<RefMut<T>> {
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::<T>() + 8])
}))
}
fn load_unchecked<T: ZeroCopy + Owner>(&self) -> Result<Ref<T>> {
if self.owner != &T::owner() {
return Err(ErrorCode::AccountOwnedByWrongProgram.into());

View File

@ -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]
}
}

View File

@ -83,6 +83,23 @@ impl SolanaCookie {
.newest()
}
pub async fn create_account<T>(&self, owner: &Pubkey) -> Pubkey {
let key = Keypair::new();
let len = 8 + std::mem::size_of::<T>();
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<T: AccountDeserialize>(&self, address: Pubkey) -> Option<T> {
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()

View File

@ -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::<BookSide>(&mango_v4::id())
.await,
bids: context
.solana
.create_account::<BookSide>(&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(())
}