Perp: Track taker and maker orders in the account; test
This commit is contained in:
parent
53becd681b
commit
6f3673bcdf
|
@ -291,10 +291,17 @@ impl Default for PerpAccount {
|
|||
|
||||
impl PerpAccount {
|
||||
/// Add taker trade after it has been matched but before it has been process on EventQueue
|
||||
pub fn add_taker_trade(&mut self, base_change: i64, quote_change: i64) {
|
||||
// TODO make checked? estimate chances of overflow here
|
||||
self.taker_base_lots += base_change;
|
||||
self.taker_quote_lots += quote_change;
|
||||
pub fn add_taker_trade(&mut self, side: Side, base_lots: i64, quote_lots: i64) {
|
||||
match side {
|
||||
Side::Bid => {
|
||||
self.taker_base_lots = cm!(self.taker_base_lots + base_lots);
|
||||
self.taker_quote_lots = cm!(self.taker_quote_lots - quote_lots);
|
||||
}
|
||||
Side::Ask => {
|
||||
self.taker_base_lots = cm!(self.taker_base_lots - base_lots);
|
||||
self.taker_quote_lots = cm!(self.taker_quote_lots + quote_lots);
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Remove taker trade after it has been processed on EventQueue
|
||||
pub fn remove_taker_trade(&mut self, base_change: i64, quote_change: i64) {
|
||||
|
@ -357,6 +364,7 @@ impl MangoAccountPerps {
|
|||
pos = self.accounts.iter().position(|p| !p.is_active());
|
||||
if let Some(i) = pos {
|
||||
self.accounts[i] = PerpAccount {
|
||||
market_index: perp_market_index,
|
||||
..Default::default()
|
||||
};
|
||||
}
|
||||
|
@ -395,16 +403,10 @@ impl MangoAccountPerps {
|
|||
let mut perp_account = self.get_account_mut_or_create(perp_market_index).unwrap().0;
|
||||
match side {
|
||||
Side::Bid => {
|
||||
perp_account.bids_base_lots = perp_account
|
||||
.bids_base_lots
|
||||
.checked_add(order.quantity)
|
||||
.unwrap();
|
||||
perp_account.bids_base_lots = cm!(perp_account.bids_base_lots + order.quantity);
|
||||
}
|
||||
Side::Ask => {
|
||||
perp_account.asks_base_lots = perp_account
|
||||
.asks_base_lots
|
||||
.checked_add(order.quantity)
|
||||
.unwrap();
|
||||
perp_account.asks_base_lots = cm!(perp_account.asks_base_lots + order.quantity);
|
||||
}
|
||||
};
|
||||
let slot = order.owner_slot as usize;
|
||||
|
|
|
@ -248,8 +248,6 @@ impl<'a> Book<'a> {
|
|||
let match_quote_lots = cm!(match_base_lots * best_opposing_price);
|
||||
remaining_base_lots = cm!(remaining_base_lots - match_base_lots);
|
||||
remaining_quote_lots = cm!(remaining_quote_lots - match_quote_lots);
|
||||
// TODO: record the taker trade in the right mango_account.perp.accounts[..]
|
||||
//mango_account.perp_accounts[market_index].add_taker_trade(match_quantity, -match_quote);
|
||||
|
||||
let new_best_opposing_quantity = cm!(best_opposing.quantity - match_base_lots);
|
||||
let maker_out = new_best_opposing_quantity == 0;
|
||||
|
@ -259,6 +257,13 @@ impl<'a> Book<'a> {
|
|||
matched_order_changes.push((best_opposing_h, new_best_opposing_quantity));
|
||||
}
|
||||
|
||||
// Record the taker trade in the account already, even though it will only be
|
||||
// realized when the fill event gets executed
|
||||
let perp_account = mango_account_perps
|
||||
.get_account_mut_or_create(market.perp_market_index)?
|
||||
.0;
|
||||
perp_account.add_taker_trade(side, match_base_lots, match_quote_lots);
|
||||
|
||||
let fill = FillEvent::new(
|
||||
side,
|
||||
maker_out,
|
||||
|
@ -361,7 +366,8 @@ impl<'a> Book<'a> {
|
|||
book_base_quantity,
|
||||
price_lots
|
||||
);
|
||||
// mango_account.add_order(market_index, Side::Bid, &new_bid)?;
|
||||
|
||||
mango_account_perps.add_order(market.perp_market_index, side, &new_order)?;
|
||||
}
|
||||
|
||||
// if there were matched taker quote apply ref fees
|
||||
|
|
|
@ -437,7 +437,7 @@ impl BookSide {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::super::order_type::{OrderType, Side};
|
||||
use super::super::order_type::OrderType;
|
||||
use super::*;
|
||||
use bytemuck::Zeroable;
|
||||
|
||||
|
@ -653,131 +653,4 @@ mod tests {
|
|||
verify_bookside(&bids);
|
||||
}
|
||||
}
|
||||
|
||||
fn bookside_contains_key(bookside: &BookSide, key: i128) -> bool {
|
||||
for (_, leaf) in bookside.iter_all_including_invalid() {
|
||||
if leaf.key == key {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn bookside_contains_price(bookside: &BookSide, price: i64) -> bool {
|
||||
for (_, leaf) in bookside.iter_all_including_invalid() {
|
||||
if leaf.price() == price {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn book_bids_full() {
|
||||
use super::super::book::Book;
|
||||
use super::super::queue::EventQueue;
|
||||
use crate::state::{MangoAccountPerps, PerpMarket};
|
||||
use fixed::types::I80F48;
|
||||
use std::cell::RefCell;
|
||||
|
||||
let bids = RefCell::new(new_bookside(BookSideType::Bids));
|
||||
let asks = RefCell::new(new_bookside(BookSideType::Asks));
|
||||
let mut book = Book {
|
||||
bids: bids.borrow_mut(),
|
||||
asks: asks.borrow_mut(),
|
||||
};
|
||||
|
||||
let mut event_queue = EventQueue::zeroed();
|
||||
|
||||
let oracle_price = I80F48::from_num(5000.0);
|
||||
|
||||
let mut perp_market = PerpMarket::zeroed();
|
||||
perp_market.quote_lot_size = 1;
|
||||
perp_market.base_lot_size = 1;
|
||||
perp_market.maint_asset_weight = I80F48::ONE;
|
||||
perp_market.maint_liab_weight = I80F48::ONE;
|
||||
perp_market.init_asset_weight = I80F48::ONE;
|
||||
perp_market.init_liab_weight = I80F48::ONE;
|
||||
|
||||
let mut new_order =
|
||||
|book: &mut Book, event_queue: &mut EventQueue, side, price, now_ts| -> i128 {
|
||||
let mut account_perps = MangoAccountPerps::new();
|
||||
|
||||
let quantity = 1;
|
||||
let tif = 100;
|
||||
|
||||
book.new_order(
|
||||
side,
|
||||
&mut perp_market,
|
||||
event_queue,
|
||||
oracle_price,
|
||||
&mut account_perps,
|
||||
&Pubkey::default(),
|
||||
price,
|
||||
quantity,
|
||||
i64::MAX,
|
||||
OrderType::Limit,
|
||||
tif,
|
||||
0,
|
||||
now_ts,
|
||||
u8::MAX,
|
||||
)
|
||||
.unwrap();
|
||||
account_perps.order_id[0]
|
||||
};
|
||||
|
||||
// insert bids until book side is full
|
||||
for i in 1..10 {
|
||||
new_order(
|
||||
&mut book,
|
||||
&mut event_queue,
|
||||
Side::Bid,
|
||||
1000 + i as i64,
|
||||
1000000 + i as u64,
|
||||
);
|
||||
}
|
||||
for i in 10..1000 {
|
||||
new_order(
|
||||
&mut book,
|
||||
&mut event_queue,
|
||||
Side::Bid,
|
||||
1000 + i as i64,
|
||||
1000011 as u64,
|
||||
);
|
||||
if book.bids.is_full() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert!(book.bids.is_full());
|
||||
assert_eq!(book.bids.get_min().unwrap().price(), 1001);
|
||||
assert_eq!(
|
||||
book.bids.get_max().unwrap().price(),
|
||||
(1000 + book.bids.leaf_count) as i64
|
||||
);
|
||||
|
||||
// add another bid at a higher price before expiry, replacing the lowest-price one (1001)
|
||||
new_order(&mut book, &mut event_queue, Side::Bid, 1005, 1000000 - 1);
|
||||
assert_eq!(book.bids.get_min().unwrap().price(), 1002);
|
||||
assert_eq!(event_queue.len(), 1);
|
||||
|
||||
// adding another bid after expiry removes the soonest-expiring order (1005)
|
||||
new_order(&mut book, &mut event_queue, Side::Bid, 999, 2000000);
|
||||
assert_eq!(book.bids.get_min().unwrap().price(), 999);
|
||||
assert!(!bookside_contains_key(&book.bids, 1005));
|
||||
assert_eq!(event_queue.len(), 2);
|
||||
|
||||
// adding an ask will wipe up to three expired bids at the top of the book
|
||||
let bids_max = book.bids.get_max().unwrap().price();
|
||||
let bids_count = book.bids.leaf_count;
|
||||
new_order(&mut book, &mut event_queue, Side::Ask, 6000, 1500000);
|
||||
assert_eq!(book.bids.leaf_count, bids_count - 5);
|
||||
assert_eq!(book.asks.leaf_count, 1);
|
||||
assert_eq!(event_queue.len(), 2 + 5);
|
||||
assert!(!bookside_contains_price(&book.bids, bids_max));
|
||||
assert!(!bookside_contains_price(&book.bids, bids_max - 1));
|
||||
assert!(!bookside_contains_price(&book.bids, bids_max - 2));
|
||||
assert!(!bookside_contains_price(&book.bids, bids_max - 3));
|
||||
assert!(!bookside_contains_price(&book.bids, bids_max - 4));
|
||||
assert!(bookside_contains_price(&book.bids, bids_max - 5));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,3 +11,317 @@ pub mod bookside_iterator;
|
|||
pub mod nodes;
|
||||
pub mod order_type;
|
||||
pub mod queue;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::state::{MangoAccountPerps, PerpMarket, FREE_ORDER_SLOT};
|
||||
use bytemuck::Zeroable;
|
||||
use fixed::types::I80F48;
|
||||
use solana_program::pubkey::Pubkey;
|
||||
use std::cell::RefCell;
|
||||
|
||||
fn new_bookside(book_side_type: BookSideType) -> BookSide {
|
||||
BookSide {
|
||||
book_side_type,
|
||||
bump_index: 0,
|
||||
free_list_len: 0,
|
||||
free_list_head: 0,
|
||||
root_node: 0,
|
||||
leaf_count: 0,
|
||||
nodes: [AnyNode::zeroed(); MAX_BOOK_NODES],
|
||||
}
|
||||
}
|
||||
|
||||
fn bookside_leaf_by_key(bookside: &BookSide, key: i128) -> Option<&LeafNode> {
|
||||
for (_, leaf) in bookside.iter_all_including_invalid() {
|
||||
if leaf.key == key {
|
||||
return Some(leaf);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn bookside_contains_key(bookside: &BookSide, key: i128) -> bool {
|
||||
for (_, leaf) in bookside.iter_all_including_invalid() {
|
||||
if leaf.key == key {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn bookside_contains_price(bookside: &BookSide, price: i64) -> bool {
|
||||
for (_, leaf) in bookside.iter_all_including_invalid() {
|
||||
if leaf.price() == price {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn test_setup(
|
||||
price: f64,
|
||||
) -> (
|
||||
PerpMarket,
|
||||
I80F48,
|
||||
EventQueue,
|
||||
RefCell<BookSide>,
|
||||
RefCell<BookSide>,
|
||||
) {
|
||||
let bids = RefCell::new(new_bookside(BookSideType::Bids));
|
||||
let asks = RefCell::new(new_bookside(BookSideType::Asks));
|
||||
|
||||
let event_queue = EventQueue::zeroed();
|
||||
|
||||
let oracle_price = I80F48::from_num(price);
|
||||
|
||||
let mut perp_market = PerpMarket::zeroed();
|
||||
perp_market.quote_lot_size = 1;
|
||||
perp_market.base_lot_size = 1;
|
||||
perp_market.maint_asset_weight = I80F48::ONE;
|
||||
perp_market.maint_liab_weight = I80F48::ONE;
|
||||
perp_market.init_asset_weight = I80F48::ONE;
|
||||
perp_market.init_liab_weight = I80F48::ONE;
|
||||
|
||||
(perp_market, oracle_price, event_queue, bids, asks)
|
||||
}
|
||||
|
||||
// Check what happens when one side of the book fills up
|
||||
#[test]
|
||||
fn book_bids_full() {
|
||||
let (mut perp_market, oracle_price, mut event_queue, bids, asks) = test_setup(5000.0);
|
||||
let mut book = Book {
|
||||
bids: bids.borrow_mut(),
|
||||
asks: asks.borrow_mut(),
|
||||
};
|
||||
|
||||
let mut new_order =
|
||||
|book: &mut Book, event_queue: &mut EventQueue, side, price, now_ts| -> i128 {
|
||||
let mut account_perps = MangoAccountPerps::new();
|
||||
|
||||
let quantity = 1;
|
||||
let tif = 100;
|
||||
|
||||
book.new_order(
|
||||
side,
|
||||
&mut perp_market,
|
||||
event_queue,
|
||||
oracle_price,
|
||||
&mut account_perps,
|
||||
&Pubkey::default(),
|
||||
price,
|
||||
quantity,
|
||||
i64::MAX,
|
||||
OrderType::Limit,
|
||||
tif,
|
||||
0,
|
||||
now_ts,
|
||||
u8::MAX,
|
||||
)
|
||||
.unwrap();
|
||||
account_perps.order_id[0]
|
||||
};
|
||||
|
||||
// insert bids until book side is full
|
||||
for i in 1..10 {
|
||||
new_order(
|
||||
&mut book,
|
||||
&mut event_queue,
|
||||
Side::Bid,
|
||||
1000 + i as i64,
|
||||
1000000 + i as u64,
|
||||
);
|
||||
}
|
||||
for i in 10..1000 {
|
||||
new_order(
|
||||
&mut book,
|
||||
&mut event_queue,
|
||||
Side::Bid,
|
||||
1000 + i as i64,
|
||||
1000011 as u64,
|
||||
);
|
||||
if book.bids.is_full() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert!(book.bids.is_full());
|
||||
assert_eq!(book.bids.get_min().unwrap().price(), 1001);
|
||||
assert_eq!(
|
||||
book.bids.get_max().unwrap().price(),
|
||||
(1000 + book.bids.leaf_count) as i64
|
||||
);
|
||||
|
||||
// add another bid at a higher price before expiry, replacing the lowest-price one (1001)
|
||||
new_order(&mut book, &mut event_queue, Side::Bid, 1005, 1000000 - 1);
|
||||
assert_eq!(book.bids.get_min().unwrap().price(), 1002);
|
||||
assert_eq!(event_queue.len(), 1);
|
||||
|
||||
// adding another bid after expiry removes the soonest-expiring order (1005)
|
||||
new_order(&mut book, &mut event_queue, Side::Bid, 999, 2000000);
|
||||
assert_eq!(book.bids.get_min().unwrap().price(), 999);
|
||||
assert!(!bookside_contains_key(&book.bids, 1005));
|
||||
assert_eq!(event_queue.len(), 2);
|
||||
|
||||
// adding an ask will wipe up to three expired bids at the top of the book
|
||||
let bids_max = book.bids.get_max().unwrap().price();
|
||||
let bids_count = book.bids.leaf_count;
|
||||
new_order(&mut book, &mut event_queue, Side::Ask, 6000, 1500000);
|
||||
assert_eq!(book.bids.leaf_count, bids_count - 5);
|
||||
assert_eq!(book.asks.leaf_count, 1);
|
||||
assert_eq!(event_queue.len(), 2 + 5);
|
||||
assert!(!bookside_contains_price(&book.bids, bids_max));
|
||||
assert!(!bookside_contains_price(&book.bids, bids_max - 1));
|
||||
assert!(!bookside_contains_price(&book.bids, bids_max - 2));
|
||||
assert!(!bookside_contains_price(&book.bids, bids_max - 3));
|
||||
assert!(!bookside_contains_price(&book.bids, bids_max - 4));
|
||||
assert!(bookside_contains_price(&book.bids, bids_max - 5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn book_new_order() {
|
||||
let (mut market, oracle_price, mut event_queue, bids, asks) = test_setup(1000.0);
|
||||
let mut book = Book {
|
||||
bids: bids.borrow_mut(),
|
||||
asks: asks.borrow_mut(),
|
||||
};
|
||||
|
||||
// Add lots and fees to make sure to exercise unit conversion
|
||||
market.base_lot_size = 10;
|
||||
market.quote_lot_size = 100;
|
||||
market.maker_fee = I80F48::from_num(-0.001f64);
|
||||
market.taker_fee = I80F48::from_num(0.01f64);
|
||||
|
||||
let mut maker = MangoAccountPerps::new();
|
||||
let mut taker = MangoAccountPerps::new();
|
||||
let maker_pk = Pubkey::new_unique();
|
||||
let taker_pk = Pubkey::new_unique();
|
||||
let now_ts = 1000000;
|
||||
|
||||
// Place a maker-bid
|
||||
let price = 1000 * market.base_lot_size / market.quote_lot_size;
|
||||
let bid_quantity = 10;
|
||||
book.new_order(
|
||||
Side::Bid,
|
||||
&mut market,
|
||||
&mut event_queue,
|
||||
oracle_price,
|
||||
&mut maker,
|
||||
&maker_pk,
|
||||
price,
|
||||
bid_quantity,
|
||||
i64::MAX,
|
||||
OrderType::Limit,
|
||||
0,
|
||||
42,
|
||||
now_ts,
|
||||
u8::MAX,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(maker.order_market[0], market.perp_market_index);
|
||||
assert_eq!(maker.order_market[1], FREE_ORDER_SLOT);
|
||||
assert_ne!(maker.order_id[0], 0);
|
||||
assert_eq!(maker.order_client_id[0], 42);
|
||||
assert_eq!(maker.order_side[0], Side::Bid);
|
||||
assert!(bookside_contains_key(&book.bids, maker.order_id[0]));
|
||||
assert!(bookside_contains_price(&book.bids, price));
|
||||
assert_eq!(maker.accounts[0].bids_base_lots, bid_quantity);
|
||||
assert_eq!(maker.accounts[0].asks_base_lots, 0);
|
||||
assert_eq!(maker.accounts[0].taker_base_lots, 0);
|
||||
assert_eq!(maker.accounts[0].taker_quote_lots, 0);
|
||||
assert_eq!(maker.accounts[0].base_position_lots, 0);
|
||||
assert_eq!(maker.accounts[0].quote_position_native, 0);
|
||||
assert_eq!(event_queue.len(), 0);
|
||||
|
||||
// Take the order partially
|
||||
let match_quantity = 5;
|
||||
book.new_order(
|
||||
Side::Ask,
|
||||
&mut market,
|
||||
&mut event_queue,
|
||||
oracle_price,
|
||||
&mut taker,
|
||||
&taker_pk,
|
||||
price,
|
||||
match_quantity,
|
||||
i64::MAX,
|
||||
OrderType::Limit,
|
||||
0,
|
||||
43,
|
||||
now_ts,
|
||||
u8::MAX,
|
||||
)
|
||||
.unwrap();
|
||||
// the remainder of the maker order is still on the book
|
||||
// (the maker account is unchanged: it was not even passed in)
|
||||
let order = bookside_leaf_by_key(&book.bids, maker.order_id[0]).unwrap();
|
||||
assert_eq!(order.price(), price);
|
||||
assert_eq!(order.quantity, bid_quantity - match_quantity);
|
||||
|
||||
// fees were immediately accrued
|
||||
let match_quote = I80F48::from(match_quantity * price * market.quote_lot_size);
|
||||
assert_eq!(
|
||||
market.fees_accrued,
|
||||
match_quote * (market.maker_fee + market.taker_fee)
|
||||
);
|
||||
|
||||
// the taker account is updated
|
||||
assert_eq!(taker.order_market[0], FREE_ORDER_SLOT);
|
||||
assert_eq!(taker.accounts[0].bids_base_lots, 0);
|
||||
assert_eq!(taker.accounts[0].asks_base_lots, 0);
|
||||
assert_eq!(taker.accounts[0].taker_base_lots, -match_quantity);
|
||||
assert_eq!(taker.accounts[0].taker_quote_lots, match_quantity * price);
|
||||
assert_eq!(taker.accounts[0].base_position_lots, 0);
|
||||
assert_eq!(
|
||||
taker.accounts[0].quote_position_native,
|
||||
-match_quote * market.taker_fee
|
||||
);
|
||||
|
||||
// the fill gets added to the event queue
|
||||
assert_eq!(event_queue.len(), 1);
|
||||
let event = event_queue.peek_front().unwrap();
|
||||
assert_eq!(event.event_type, EventType::Fill as u8);
|
||||
let fill: &FillEvent = bytemuck::cast_ref(event);
|
||||
assert_eq!(fill.quantity, match_quantity);
|
||||
assert_eq!(fill.price, price);
|
||||
assert_eq!(fill.taker_client_order_id, 43);
|
||||
assert_eq!(fill.maker_client_order_id, 42);
|
||||
assert_eq!(fill.maker, maker_pk);
|
||||
assert_eq!(fill.taker, taker_pk);
|
||||
assert_eq!(fill.maker_fee, market.maker_fee);
|
||||
assert_eq!(fill.taker_fee, market.taker_fee);
|
||||
|
||||
// simulate event queue processing
|
||||
maker
|
||||
.execute_maker(market.perp_market_index, &mut market, &fill)
|
||||
.unwrap();
|
||||
taker
|
||||
.execute_taker(market.perp_market_index, &mut market, &fill)
|
||||
.unwrap();
|
||||
assert_eq!(market.open_interest, 2 * match_quantity);
|
||||
|
||||
assert_eq!(maker.order_market[0], 0);
|
||||
assert_eq!(
|
||||
maker.accounts[0].bids_base_lots,
|
||||
bid_quantity - match_quantity
|
||||
);
|
||||
assert_eq!(maker.accounts[0].asks_base_lots, 0);
|
||||
assert_eq!(maker.accounts[0].taker_base_lots, 0);
|
||||
assert_eq!(maker.accounts[0].taker_quote_lots, 0);
|
||||
assert_eq!(maker.accounts[0].base_position_lots, match_quantity);
|
||||
assert_eq!(
|
||||
maker.accounts[0].quote_position_native,
|
||||
-match_quote - match_quote * market.maker_fee
|
||||
);
|
||||
|
||||
assert_eq!(taker.accounts[0].bids_base_lots, 0);
|
||||
assert_eq!(taker.accounts[0].asks_base_lots, 0);
|
||||
assert_eq!(taker.accounts[0].taker_base_lots, 0);
|
||||
assert_eq!(taker.accounts[0].taker_quote_lots, 0);
|
||||
assert_eq!(taker.accounts[0].base_position_lots, -match_quantity);
|
||||
assert_eq!(
|
||||
taker.accounts[0].quote_position_native,
|
||||
match_quote - match_quote * market.taker_fee
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue