Perp: Always allow users to free perp open order slots (#817)

Previously, freeing would be impossible if a canceling fill or out event
was already in-flight - then the order would no longer be on the
orderbook.

Now, FillEvent and OutEvent store the order id and can check if the open
order slot on the account has been reused already. That allows canceling
orders to always free up the user slot immediately.
This commit is contained in:
Christian Kamm 2023-12-20 11:15:10 +01:00 committed by GitHub
parent 86334020e2
commit 7655a87404
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 593 additions and 96 deletions

View File

@ -18,7 +18,13 @@ pub fn perp_cancel_all_orders(ctx: Context<PerpCancelAllOrders>, limit: u8) -> R
asks: ctx.accounts.asks.load_mut()?,
};
book.cancel_all_orders(&mut account.borrow_mut(), &mut perp_market, limit, None)?;
book.cancel_all_orders(
&mut account.borrow_mut(),
ctx.accounts.account.as_ref().key,
&mut perp_market,
limit,
None,
)?;
Ok(())
}

View File

@ -24,6 +24,7 @@ pub fn perp_cancel_all_orders_by_side(
book.cancel_all_orders(
&mut account.borrow_mut(),
ctx.accounts.account.as_ref().key,
&mut perp_market,
limit,
side_option,

View File

@ -18,19 +18,17 @@ pub fn perp_cancel_order(ctx: Context<PerpCancelOrder>, order_id: u128) -> Resul
asks: ctx.accounts.asks.load_mut()?,
};
let oo = account
let (slot, _) = account
.perp_find_order_with_order_id(perp_market.perp_market_index, order_id)
.ok_or_else(|| {
error_msg!("could not find perp order with id {order_id} in user account")
})?;
let order_id = oo.id;
let order_side_and_tree = oo.side_and_tree();
book.cancel_order(
book.cancel_order_by_slot(
&mut account.borrow_mut(),
order_id,
order_side_and_tree,
Some(ctx.accounts.account.key()),
ctx.accounts.account.as_ref().key,
slot,
perp_market.perp_market_index,
)?;
Ok(())

View File

@ -21,21 +21,19 @@ pub fn perp_cancel_order_by_client_order_id(
asks: ctx.accounts.asks.load_mut()?,
};
let oo = account
let (slot, _) = account
.perp_find_order_with_client_order_id(perp_market.perp_market_index, client_order_id)
.ok_or_else(|| {
error_msg!(
"could not find perp order with client order id {client_order_id} in user account"
)
})?;
let order_id = oo.id;
let order_side_and_tree = oo.side_and_tree();
book.cancel_order(
book.cancel_order_by_slot(
&mut account.borrow_mut(),
order_id,
order_side_and_tree,
Some(ctx.accounts.account.key()),
ctx.accounts.account.as_ref().key,
slot,
perp_market.perp_market_index,
)?;
Ok(())

View File

@ -155,7 +155,13 @@ pub fn perp_consume_events(ctx: Context<PerpConsumeEvents>, limit: usize) -> Res
EventType::Out => {
let out: &OutEvent = cast_ref(event);
load_mango_account!(owner, out.owner, mango_account_ais, group, event_queue);
owner.remove_perp_order(out.owner_slot as usize, out.quantity)?;
owner.execute_perp_out_event(
perp_market_index,
out.side(),
out.owner_slot as usize,
out.quantity,
out.order_id,
)?;
}
EventType::Liquidate => {
// This is purely for record keeping. Can be removed if program logs are superior

View File

@ -40,7 +40,13 @@ pub fn perp_liq_force_cancel_orders(
asks: ctx.accounts.asks.load_mut()?,
};
book.cancel_all_orders(&mut account.borrow_mut(), &mut perp_market, limit, None)?;
book.cancel_all_orders(
&mut account.borrow_mut(),
ctx.accounts.account.as_ref().key,
&mut perp_market,
limit,
None,
)?;
let perp_position = account.perp_position(perp_market.perp_market_index)?;
health_cache.recompute_perp_info(perp_position, &perp_market)?;

View File

@ -850,18 +850,20 @@ impl<
&self,
market_index: PerpMarketIndex,
client_order_id: u64,
) -> Option<&PerpOpenOrder> {
self.all_perp_orders()
.find(|&oo| oo.is_active_for_market(market_index) && oo.client_id == client_order_id)
) -> Option<(usize, &PerpOpenOrder)> {
self.all_perp_orders().enumerate().find(|(_, &oo)| {
oo.is_active_for_market(market_index) && oo.client_id == client_order_id
})
}
pub fn perp_find_order_with_order_id(
&self,
market_index: PerpMarketIndex,
order_id: u128,
) -> Option<&PerpOpenOrder> {
) -> Option<(usize, &PerpOpenOrder)> {
self.all_perp_orders()
.find(|&oo| oo.is_active_for_market(market_index) && oo.id == order_id)
.enumerate()
.find(|(_, &oo)| oo.is_active_for_market(market_index) && oo.id == order_id)
}
pub fn being_liquidated(&self) -> bool {
@ -1200,52 +1202,38 @@ impl<
side: Side,
order_tree: BookSideOrderTree,
order: &LeafNode,
client_order_id: u64,
) -> Result<()> {
let mut perp_account = self.perp_position_mut(perp_market_index)?;
match side {
Side::Bid => {
perp_account.bids_base_lots += order.quantity;
}
Side::Ask => {
perp_account.asks_base_lots += order.quantity;
}
};
let perp_account = self.perp_position_mut(perp_market_index)?;
perp_account.adjust_maker_lots(side, order.quantity);
let slot = order.owner_slot as usize;
let mut oo = self.perp_order_mut_by_raw_index(slot);
let oo = self.perp_order_mut_by_raw_index(slot);
oo.market = perp_market_index;
oo.side_and_tree = SideAndOrderTree::new(side, order_tree).into();
oo.id = order.key;
oo.client_id = client_order_id;
oo.client_id = order.client_order_id;
oo.quantity = order.quantity;
Ok(())
}
/// Removes the perp order and updates the maker bids/asks tracking
///
/// The passed in `quantity` may differ from the quantity stored on the
/// perp open order slot, because maybe we're cancelling an order slot
/// for quantity 10 where 3 are in-flight in a FillEvent and 7 were left
/// on the book.
pub fn remove_perp_order(&mut self, slot: usize, quantity: i64) -> Result<()> {
{
let oo = self.perp_order_mut_by_raw_index(slot);
require_neq!(oo.market, FREE_ORDER_SLOT);
let order_side = oo.side_and_tree().side();
let perp_market_index = oo.market;
let perp_account = self.perp_position_mut(perp_market_index)?;
let oo = self.perp_order_by_raw_index(slot)?;
require_neq!(oo.market, FREE_ORDER_SLOT);
let perp_market_index = oo.market;
let order_side = oo.side_and_tree().side();
// accounting
match order_side {
Side::Bid => {
perp_account.bids_base_lots -= quantity;
}
Side::Ask => {
perp_account.asks_base_lots -= quantity;
}
}
}
let perp_account = self.perp_position_mut(perp_market_index)?;
perp_account.adjust_maker_lots(order_side, -quantity);
// release space
let oo = self.perp_order_mut_by_raw_index(slot);
oo.market = FREE_ORDER_SLOT;
oo.side_and_tree = SideAndOrderTree::BidFixed.into();
oo.id = 0;
oo.client_id = 0;
oo.clear();
Ok(())
}
@ -1273,19 +1261,34 @@ impl<
pa.maker_volume += quote.abs().to_num::<u64>();
if fill.maker_out() {
self.remove_perp_order(fill.maker_slot as usize, base_change.abs())
} else {
match side {
Side::Bid => {
pa.bids_base_lots -= base_change.abs();
}
Side::Ask => {
pa.asks_base_lots -= base_change.abs();
}
let quantity_filled = base_change.abs();
let maker_slot = fill.maker_slot as usize;
// Always adjust the bids/asks_base_lots for the filled amount.
// Because any early cancels only adjust it for the amount that was on the book,
// so even fill events that come after the slot was freed still need to clear
// the pending maker lots.
pa.adjust_maker_lots(side, -quantity_filled);
let oo = self.perp_order_mut_by_raw_index(maker_slot);
let is_active = oo.is_active_for_market(perp_market_index);
// Old fill events have no maker order id and match against any order.
// (this works safely because we don't allow old order's slots to be
// prematurely freed - and new orders can only have new fill events)
let is_old_fill = fill.maker_order_id == 0;
let order_id_match = is_old_fill || oo.id == fill.maker_order_id;
if is_active && order_id_match {
// Old orders have quantity=0
oo.quantity = (oo.quantity - quantity_filled).max(0);
if fill.maker_out() {
oo.clear();
}
Ok(())
}
Ok(())
}
pub fn execute_perp_taker(
@ -1309,6 +1312,37 @@ impl<
Ok(())
}
pub fn execute_perp_out_event(
&mut self,
perp_market_index: PerpMarketIndex,
side: Side,
slot: usize,
quantity: i64,
order_id: u128,
) -> Result<()> {
// Always free up the maker lots tracking, regardless of whether the
// order slot is still on the account or not
let pa = self.perp_position_mut(perp_market_index)?;
pa.adjust_maker_lots(side, -quantity);
let oo = self.perp_order_mut_by_raw_index(slot);
let is_active = oo.is_active_for_market(perp_market_index);
// Old events have no order id and match against any order.
// (this works safely because we don't allow old order's slots to be
// prematurely freed - and new orders can only have new events)
let is_old_event = order_id == 0;
let order_id_match = is_old_event || oo.id == order_id;
// This may be a delayed out event (slot may be empty or reused), so make
// sure it's the right one before canceling.
if is_active && order_id_match {
oo.clear();
}
Ok(())
}
pub fn token_conditional_swap_mut_by_index(
&mut self,
index: usize,
@ -1814,8 +1848,11 @@ impl<'a, 'info: 'a> MangoAccountLoader<'a> for &'a AccountLoader<'info, MangoAcc
#[cfg(test)]
mod tests {
use bytemuck::Zeroable;
use itertools::Itertools;
use crate::state::PostOrderType;
use super::*;
fn make_test_account() -> MangoAccountValue {
@ -2561,4 +2598,212 @@ mod tests {
}
Ok(())
}
#[test]
fn test_perp_order_events() -> Result<()> {
let group = Group::zeroed();
let perp_market_index = 0;
let mut perp_market = PerpMarket::zeroed();
let mut account = make_test_account();
account.ensure_token_position(0)?;
account.ensure_perp_position(perp_market_index, 0)?;
let owner = Pubkey::new_unique();
let slot = account.perp_next_order_slot()?;
let order_id = 127;
let quantity = 42;
let order = LeafNode::new(
slot as u8,
order_id,
owner,
quantity,
1,
PostOrderType::Limit,
0,
0,
0,
);
let side = Side::Bid;
account.add_perp_order(0, side, BookSideOrderTree::Fixed, &order)?;
let make_fill = |quantity, out, order_id| {
FillEvent::new(
side.invert_side(),
out,
slot as u8,
0,
0,
owner,
order_id,
0,
I80F48::ZERO,
0,
owner,
0,
I80F48::ZERO,
1,
quantity,
)
};
let pp = |a: &MangoAccountValue| a.perp_position(perp_market_index).unwrap().clone();
{
// full fill
let mut account = account.clone();
let fill = make_fill(quantity, true, order_id);
account.execute_perp_maker(perp_market_index, &mut perp_market, &fill, &group)?;
assert_eq!(pp(&account).bids_base_lots, 0);
assert_eq!(pp(&account).asks_base_lots, 0);
assert!(!account.perp_order_by_raw_index(0)?.is_active());
}
{
// full fill, no order id
let mut account = account.clone();
let fill = make_fill(quantity, true, 0);
account.execute_perp_maker(perp_market_index, &mut perp_market, &fill, &group)?;
assert_eq!(pp(&account).bids_base_lots, 0);
assert_eq!(pp(&account).asks_base_lots, 0);
assert!(!account.perp_order_by_raw_index(0)?.is_active());
}
{
// out event
let mut account = account.clone();
account.execute_perp_out_event(perp_market_index, side, slot, quantity, order_id)?;
assert_eq!(pp(&account).bids_base_lots, 0);
assert_eq!(pp(&account).asks_base_lots, 0);
assert!(!account.perp_order_by_raw_index(0)?.is_active());
}
{
// out event, no order id
let mut account = account.clone();
account.execute_perp_out_event(perp_market_index, side, slot, quantity, 0)?;
assert_eq!(pp(&account).bids_base_lots, 0);
assert_eq!(pp(&account).asks_base_lots, 0);
assert!(!account.perp_order_by_raw_index(0)?.is_active());
}
{
// cancel
let mut account = account.clone();
account.remove_perp_order(slot, quantity)?;
assert_eq!(pp(&account).bids_base_lots, 0);
assert_eq!(pp(&account).asks_base_lots, 0);
assert!(!account.perp_order_by_raw_index(0)?.is_active());
}
{
// partial fill event, user closes rest, following out event has no effect
let mut account = account.clone();
let fill = make_fill(quantity - 10, false, order_id);
account.execute_perp_maker(perp_market_index, &mut perp_market, &fill, &group)?;
assert_eq!(pp(&account).bids_base_lots, 10);
assert_eq!(pp(&account).asks_base_lots, 0);
assert_eq!(account.perp_order_by_raw_index(slot)?.quantity, 10);
// out event happens but is delayed
account.remove_perp_order(slot, 0)?;
assert_eq!(pp(&account).bids_base_lots, 10);
assert_eq!(pp(&account).asks_base_lots, 0);
assert!(!account.perp_order_by_raw_index(0)?.is_active());
account.execute_perp_out_event(perp_market_index, side, slot, 10, order_id)?;
assert_eq!(pp(&account).bids_base_lots, 0);
assert_eq!(pp(&account).asks_base_lots, 0);
}
{
// partial fill and out are delayed, user closes first
let mut account = account.clone();
account.remove_perp_order(slot, 0)?;
assert_eq!(pp(&account).bids_base_lots, quantity);
assert_eq!(pp(&account).asks_base_lots, 0);
assert!(!account.perp_order_by_raw_index(0)?.is_active());
let fill = make_fill(quantity - 10, false, order_id);
account.execute_perp_maker(perp_market_index, &mut perp_market, &fill, &group)?;
assert_eq!(pp(&account).bids_base_lots, 10);
assert_eq!(pp(&account).asks_base_lots, 0);
account.execute_perp_out_event(perp_market_index, side, slot, 10, order_id)?;
assert_eq!(pp(&account).bids_base_lots, 0);
assert_eq!(pp(&account).asks_base_lots, 0);
}
{
// partial fill and cancel, cancel before outevent
let mut account = account.clone();
account.remove_perp_order(slot, 10)?;
assert_eq!(pp(&account).bids_base_lots, quantity - 10);
assert_eq!(pp(&account).asks_base_lots, 0);
assert!(!account.perp_order_by_raw_index(0)?.is_active());
let fill = make_fill(quantity - 10, false, order_id);
account.execute_perp_maker(perp_market_index, &mut perp_market, &fill, &group)?;
assert_eq!(pp(&account).bids_base_lots, 0);
assert_eq!(pp(&account).asks_base_lots, 0);
}
{
// several fills
let mut account = account.clone();
let fill = make_fill(10, false, order_id);
account.execute_perp_maker(perp_market_index, &mut perp_market, &fill, &group)?;
assert_eq!(pp(&account).bids_base_lots, quantity - 10);
assert_eq!(pp(&account).asks_base_lots, 0);
assert_eq!(
account.perp_order_by_raw_index(slot)?.quantity,
quantity - 10
);
let fill = make_fill(10, false, order_id);
account.execute_perp_maker(perp_market_index, &mut perp_market, &fill, &group)?;
assert_eq!(pp(&account).bids_base_lots, quantity - 20);
assert_eq!(pp(&account).asks_base_lots, 0);
assert_eq!(
account.perp_order_by_raw_index(slot)?.quantity,
quantity - 20
);
let fill = make_fill(quantity - 20, true, order_id);
account.execute_perp_maker(perp_market_index, &mut perp_market, &fill, &group)?;
assert_eq!(pp(&account).bids_base_lots, 0);
assert_eq!(pp(&account).asks_base_lots, 0);
assert!(!account.perp_order_by_raw_index(0)?.is_active());
}
{
// mismatched fill and out
let mut account = account.clone();
let mut fill = make_fill(10, false, order_id);
fill.maker_order_id = 1;
account.execute_perp_maker(perp_market_index, &mut perp_market, &fill, &group)?;
assert_eq!(pp(&account).bids_base_lots, quantity - 10);
assert_eq!(pp(&account).asks_base_lots, 0);
assert_eq!(account.perp_order_by_raw_index(slot)?.quantity, quantity);
account.execute_perp_out_event(perp_market_index, side, slot, 10, 1)?;
assert_eq!(pp(&account).bids_base_lots, quantity - 20);
assert_eq!(pp(&account).asks_base_lots, 0);
assert_eq!(account.perp_order_by_raw_index(slot)?.quantity, quantity);
}
Ok(())
}
}

View File

@ -376,6 +376,17 @@ impl PerpPosition {
self.taker_quote_lots -= quote_change;
}
pub fn adjust_maker_lots(&mut self, side: Side, base_lots: i64) {
match side {
Side::Bid => {
self.bids_base_lots += base_lots;
}
Side::Ask => {
self.asks_base_lots += base_lots;
}
};
}
pub fn is_active(&self) -> bool {
self.market_index != PerpMarketIndex::MAX
}
@ -850,10 +861,13 @@ pub struct PerpOpenOrder {
pub client_id: u64,
pub id: u128,
pub quantity: i64,
// WARNING: When adding fields, take care of updating the clear() function
#[derivative(Debug = "ignore")]
pub reserved: [u8; 64],
pub reserved: [u8; 56],
}
const_assert_eq!(size_of::<PerpOpenOrder>(), 1 + 1 + 2 + 4 + 8 + 16 + 64);
const_assert_eq!(size_of::<PerpOpenOrder>(), 1 + 1 + 2 + 4 + 8 + 16 + 8 + 56);
const_assert_eq!(size_of::<PerpOpenOrder>(), 96);
const_assert_eq!(size_of::<PerpOpenOrder>() % 8, 0);
@ -866,7 +880,8 @@ impl Default for PerpOpenOrder {
padding2: Default::default(),
client_id: 0,
id: 0,
reserved: [0; 64],
quantity: 0,
reserved: [0; 56],
}
}
}
@ -883,6 +898,14 @@ impl PerpOpenOrder {
pub fn is_active(&self) -> bool {
self.market != FREE_ORDER_SLOT
}
pub fn clear(&mut self) {
self.market = FREE_ORDER_SLOT;
self.side_and_tree = SideAndOrderTree::BidFixed.into();
self.id = 0;
self.client_id = 0;
self.quantity = 0;
}
}
#[macro_export]

View File

@ -1,6 +1,8 @@
use crate::error::*;
use crate::logs::{emit_stack, FilledPerpOrderLog, PerpTakerTradeLog};
use crate::state::{orderbook::bookside::*, EventQueue, MangoAccountRefMut, PerpMarket};
use crate::state::{
orderbook::bookside::*, EventQueue, MangoAccountRefMut, PerpMarket, PerpMarketIndex,
};
use anchor_lang::prelude::*;
use bytemuck::cast;
use fixed::types::I80F48;
@ -91,13 +93,11 @@ impl<'a> Orderbook<'a> {
// 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(
let event = OutEvent::from_leaf_node(
other_side,
best_opposing.node.owner_slot,
now_ts,
event_queue.header.seq_num,
best_opposing.node.owner,
best_opposing.node.quantity,
best_opposing.node,
);
event_queue.push_back(cast(event)).unwrap();
orders_to_delete
@ -140,13 +140,11 @@ impl<'a> Orderbook<'a> {
decremented_base_lots += match_base_lots;
}
SelfTradeBehavior::CancelProvide => {
let event = OutEvent::new(
let event = OutEvent::from_leaf_node(
other_side,
best_opposing.node.owner_slot,
now_ts,
event_queue.header.seq_num,
best_opposing.node.owner,
best_opposing.node.quantity,
best_opposing.node,
);
event_queue.push_back(cast(event)).unwrap();
orders_to_delete
@ -181,6 +179,7 @@ impl<'a> Orderbook<'a> {
now_ts,
seq_num,
best_opposing.node.owner,
best_opposing.node.key,
best_opposing.node.client_order_id,
if order_would_self_trade {
I80F48::ZERO
@ -272,13 +271,11 @@ impl<'a> Orderbook<'a> {
// Drop an expired order if possible
if let Some(expired_order) = bookside.remove_one_expired(order_tree_target, now_ts) {
let event = OutEvent::new(
let event = OutEvent::from_leaf_node(
side,
expired_order.owner_slot,
now_ts,
event_queue.header.seq_num,
expired_order.owner,
expired_order.quantity,
&expired_order,
);
event_queue.push_back(cast(event)).unwrap();
}
@ -292,13 +289,11 @@ impl<'a> Orderbook<'a> {
side.is_price_better(price_lots, worst_price),
MangoError::SomeError
);
let event = OutEvent::new(
let event = OutEvent::from_leaf_node(
side,
worst_order.owner_slot,
now_ts,
event_queue.header.seq_num,
worst_order.owner,
worst_order.quantity,
&worst_order,
);
event_queue.push_back(cast(event)).unwrap();
}
@ -334,7 +329,6 @@ impl<'a> Orderbook<'a> {
side,
order_tree_target,
&new_order,
order.client_order_id,
)?;
}
@ -351,6 +345,7 @@ impl<'a> Orderbook<'a> {
pub fn cancel_all_orders(
&mut self,
mango_account: &mut MangoAccountRefMut,
mango_account_pk: &Pubkey,
perp_market: &mut PerpMarket,
mut limit: u8,
side_to_cancel_option: Option<Side>,
@ -371,8 +366,12 @@ impl<'a> Orderbook<'a> {
let order_id = oo.id;
let cancel_result =
self.cancel_order(mango_account, order_id, order_side_and_tree, None);
let cancel_result = self.cancel_order_by_slot(
mango_account,
mango_account_pk,
i,
perp_market.perp_market_index,
);
if cancel_result.is_anchor_error_with_code(MangoError::PerpOrderIdNotFound.into()) {
// It's possible for the order to be filled or expired already.
// There will be an event on the queue, the perp order slot is freed once
@ -394,8 +393,53 @@ impl<'a> Orderbook<'a> {
Ok(())
}
/// Cancels an order in an open order slot, removing it from open orders list
/// and from the orderbook (unless already filled/expired)
pub fn cancel_order_by_slot(
&mut self,
mango_account: &mut MangoAccountRefMut,
mango_account_pk: &Pubkey,
slot: usize,
perp_market_index: PerpMarketIndex,
) -> Result<()> {
let oo = mango_account.perp_order_by_raw_index(slot)?;
if !oo.is_active_for_market(perp_market_index) {
return Err(error_msg_typed!(
MangoError::SomeError,
"perp orders at slot {slot} is not active for perp market {perp_market_index}"
));
}
let side_and_tree = oo.side_and_tree();
let side = side_and_tree.side();
let book_component = side_and_tree.order_tree();
let order_id = oo.id;
let leaf_node_opt = self
.bookside_mut(side)
.remove_by_key(book_component, order_id);
// If the order is still on the book, cancel it without an OutEvent and free up the order
// quantity immediately. If it's not on the book, the OutEvent or FillEvent is responsible
// for freeing up quantity, even if we already free up the slot itself here.
let on_book_quantity = if let Some(leaf_node) = leaf_node_opt {
require_eq!(leaf_node.owner_slot as usize, slot);
require_keys_eq!(leaf_node.owner, *mango_account_pk);
leaf_node.quantity
} else {
// Old orders didn't keep track of `quantity` on the oo slot. They are not allowed
// to be cancelled while a canceling Fill- or OutEvent is in flight.
if oo.quantity == 0 {
return Err(error_msg_typed!(MangoError::PerpOrderIdNotFound, "no perp order with id {order_id}, side {side:?}, component {book_component:?} found on the orderbook"));
}
0
};
mango_account.remove_perp_order(slot, on_book_quantity)?;
Ok(())
}
/// Cancels an order on a side, removing it from the book and the mango account orders list
pub fn cancel_order(
pub fn cancel_order_by_id(
&mut self,
mango_account: &mut MangoAccountRefMut,
order_id: u128,

View File

@ -6,7 +6,7 @@ use num_enum::{IntoPrimitive, TryFromPrimitive};
use static_assertions::const_assert_eq;
use std::mem::size_of;
use super::Side;
use super::{LeafNode, Side};
pub const MAX_NUM_EVENTS: u32 = 488;
@ -159,7 +159,7 @@ const EVENT_SIZE: usize = 208;
#[derive(Debug)]
pub struct AnyEvent {
pub event_type: u8,
pub padding: [u8; 207],
pub padding: [u8; EVENT_SIZE - 1],
}
const_assert_eq!(size_of::<AnyEvent>(), EVENT_SIZE);
@ -194,7 +194,7 @@ pub struct FillEvent {
pub taker: Pubkey,
pub padding3: [u8; 16],
pub taker_client_order_id: u64,
pub padding4: [u8; 16],
pub maker_order_id: u128,
pub price: i64,
pub quantity: i64, // number of quote lots
@ -215,6 +215,7 @@ impl FillEvent {
timestamp: u64,
seq_num: u64,
maker: Pubkey,
maker_order_id: u128,
maker_client_order_id: u64,
maker_fee: I80F48,
maker_timestamp: u64,
@ -232,6 +233,7 @@ impl FillEvent {
timestamp,
seq_num,
maker,
maker_order_id,
maker_client_order_id,
maker_fee: maker_fee.to_num::<f32>(),
maker_timestamp,
@ -243,7 +245,6 @@ impl FillEvent {
padding: Default::default(),
padding2: Default::default(),
padding3: Default::default(),
padding4: Default::default(),
reserved: [0; 8],
}
}
@ -306,7 +307,8 @@ pub struct OutEvent {
pub seq_num: u64,
pub owner: Pubkey,
pub quantity: i64,
padding1: [u8; 144],
pub order_id: u128,
padding1: [u8; 128],
}
const_assert_eq!(size_of::<OutEvent>() % 8, 0);
const_assert_eq!(size_of::<OutEvent>(), EVENT_SIZE);
@ -319,6 +321,7 @@ impl OutEvent {
seq_num: u64,
owner: Pubkey,
quantity: i64,
order_id: u128,
) -> Self {
Self {
event_type: EventType::Out.into(),
@ -329,10 +332,23 @@ impl OutEvent {
seq_num,
owner,
quantity,
padding1: [0; EVENT_SIZE - 64],
order_id,
padding1: [0; 128],
}
}
pub fn from_leaf_node(side: Side, timestamp: u64, seq_num: u64, node: &LeafNode) -> Self {
Self::new(
side,
node.owner_slot,
timestamp,
seq_num,
node.owner,
node.quantity,
node.key,
)
}
pub fn side(&self) -> Side {
self.side.try_into().unwrap()
}

View File

@ -1439,6 +1439,160 @@ async fn test_perp_compute() -> Result<(), TransportError> {
Ok(())
}
#[tokio::test]
async fn test_perp_cancel_with_in_flight_events() -> Result<(), TransportError> {
let context = TestContext::new().await;
let solana = &context.solana.clone();
let admin = TestKeypair::new();
let owner = context.users[0].key;
let payer = context.users[1].key;
let mints = &context.mints[0..2];
//
// SETUP: Create a group and an account
//
let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
admin,
payer,
mints: mints.to_vec(),
..GroupWithTokensConfig::default()
}
.create(solana)
.await;
let deposit_amount = 1000;
let account_0 = create_funded_account(
&solana,
group,
owner,
0,
&context.users[1],
mints,
deposit_amount,
0,
)
.await;
let account_1 = create_funded_account(
&solana,
group,
owner,
1,
&context.users[1],
mints,
deposit_amount,
0,
)
.await;
//
// SETUP: Create a perp market
//
let mango_v4::accounts::PerpCreateMarket { perp_market, .. } = send_tx(
solana,
PerpCreateMarketInstruction {
group,
admin,
payer,
perp_market_index: 0,
quote_lot_size: 10,
base_lot_size: 100,
maint_base_asset_weight: 0.975,
init_base_asset_weight: 0.95,
maint_base_liab_weight: 1.025,
init_base_liab_weight: 1.05,
base_liquidation_fee: 0.012,
maker_fee: 0.0000,
taker_fee: 0.0000,
settle_pnl_limit_factor: -1.0,
settle_pnl_limit_window_size_ts: 24 * 60 * 60,
..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, &tokens[1]).await
},
)
.await
.unwrap();
let perp_market_data = solana.get_account::<PerpMarket>(perp_market).await;
let price_lots = perp_market_data.native_price_to_lot(I80F48::from(1));
//
// SETUP: Place a bid, a matching ask, generating a closing fill event
//
send_tx(
solana,
PerpPlaceOrderInstruction {
account: account_0,
perp_market,
owner,
side: Side::Bid,
price_lots,
max_base_lots: 2,
client_order_id: 5,
..PerpPlaceOrderInstruction::default()
},
)
.await
.unwrap();
send_tx(
solana,
PerpPlaceOrderInstruction {
account: account_1,
perp_market,
owner,
side: Side::Ask,
price_lots,
max_base_lots: 2,
client_order_id: 6,
..PerpPlaceOrderInstruction::default()
},
)
.await
.unwrap();
//
// TEST: it's possible to cancel, freeing up the user's oo slot
//
send_tx(
solana,
PerpCancelAllOrdersInstruction {
account: account_0,
perp_market,
owner,
limit: 10,
},
)
.await
.unwrap();
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
let perp_0 = mango_account_0.perps[0];
assert_eq!(perp_0.bids_base_lots, 2);
assert!(!mango_account_0.perp_open_orders[0].is_active());
//
// TEST: consuming the event updates the perp account state
//
send_tx(
solana,
PerpConsumeEventsInstruction {
perp_market,
mango_accounts: vec![account_0, account_1],
},
)
.await
.unwrap();
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
let perp_0 = mango_account_0.perps[0];
assert_eq!(perp_0.bids_base_lots, 0);
Ok(())
}
async fn assert_no_perp_orders(solana: &SolanaCookie, account_0: Pubkey) {
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;