From 512030f5aedf7e36c1f5ff02aed6251bbbeca610 Mon Sep 17 00:00:00 2001 From: Jarry Xiao <61092285+jarry-xiao@users.noreply.github.com> Date: Wed, 8 Mar 2023 14:13:46 -0500 Subject: [PATCH 1/4] Time In Force (#23) Draft implementation of time-in-force (TIF) orders for Phoenix v1. Added a standard test in test_market.rs. Could use more coverage Changes: - Uses the padding on the FIFORestingOrder to store and expiration slot and an expiration time - Modifies the OrderPacket struct to enable specification of expiration - Injects logic into the matching engine to skip orders that are expired --- idl/phoenix_v1.json | 80 +++++ src/program/events.rs | 43 +++ .../processor/cancel_multiple_orders.rs | 20 +- src/program/processor/new_order.rs | 122 ++++--- src/state/inflight_order.rs | 8 + src/state/markets/fifo.rs | 312 +++++++++++++++--- src/state/markets/market_events.rs | 11 + src/state/markets/market_traits.rs | 1 + src/state/markets/test_market.rs | 304 ++++++++++++++++- src/state/order_schema/order_packet.rs | 64 ++++ tests/test_phoenix.rs | 225 +++++++------ 11 files changed, 986 insertions(+), 204 deletions(-) diff --git a/idl/phoenix_v1.json b/idl/phoenix_v1.json index cc36fc3..1dcc88a 100644 --- a/idl/phoenix_v1.json +++ b/idl/phoenix_v1.json @@ -1800,6 +1800,58 @@ ] } }, + { + "name": "TimeInForceEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "index", + "type": "u16" + }, + { + "name": "orderSequenceNumber", + "type": "u64" + }, + { + "name": "lastValidSlot", + "type": "u64" + }, + { + "name": "lastValidUnixTimestampInSeconds", + "type": "u64" + } + ] + } + }, + { + "name": "ExpiredOrderEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "index", + "type": "u16" + }, + { + "name": "makerId", + "type": "publicKey" + }, + { + "name": "orderSequenceNumber", + "type": "u64" + }, + { + "name": "priceInTicks", + "type": "u64" + }, + { + "name": "baseLotsRemoved", + "type": "u64" + } + ] + } + }, { "name": "CancelUpToParams", "type": { @@ -1950,6 +2002,18 @@ { "name": "sizeInBaseLots", "type": "u64" + }, + { + "name": "lastValidSlot", + "type": { + "option": "u64" + } + }, + { + "name": "lastValidUnixTimestampInSeconds", + "type": { + "option": "u64" + } } ] } @@ -2156,6 +2220,22 @@ "defined": "FeeEvent" } ] + }, + { + "name": "TimeInForce", + "fields": [ + { + "defined": "TimeInForceEvent" + } + ] + }, + { + "name": "ExpiredOrder", + "fields": [ + { + "defined": "ExpiredOrderEvent" + } + ] } ] } diff --git a/src/program/events.rs b/src/program/events.rs index 32e434d..a7b4813 100644 --- a/src/program/events.rs +++ b/src/program/events.rs @@ -65,6 +65,23 @@ pub struct FeeEvent { pub fees_collected_in_quote_lots: u64, } +#[derive(Debug, Copy, Clone, BorshDeserialize, BorshSerialize)] +pub struct TimeInForceEvent { + pub index: u16, + pub order_sequence_number: u64, + pub last_valid_slot: u64, + pub last_valid_unix_timestamp_in_seconds: u64, +} + +#[derive(Debug, Copy, Clone, BorshDeserialize, BorshSerialize)] +pub struct ExpiredOrderEvent { + pub index: u16, + pub maker_id: Pubkey, + pub order_sequence_number: u64, + pub price_in_ticks: u64, + pub base_lots_removed: u64, +} + #[derive(Debug, Copy, Clone, BorshDeserialize, BorshSerialize)] pub enum PhoenixMarketEvent { Uninitialized, @@ -75,6 +92,8 @@ pub enum PhoenixMarketEvent { Evict(EvictEvent), FillSummary(FillSummaryEvent), Fee(FeeEvent), + TimeInForce(TimeInForceEvent), + ExpiredOrder(ExpiredOrderEvent), } impl Default for PhoenixMarketEvent { @@ -92,6 +111,8 @@ impl PhoenixMarketEvent { Self::FillSummary(FillSummaryEvent { index, .. }) => *index = i, Self::Evict(EvictEvent { index, .. }) => *index = i, Self::Fee(FeeEvent { index, .. }) => *index = i, + Self::TimeInForce(TimeInForceEvent { index, .. }) => *index = i, + Self::ExpiredOrder(ExpiredOrderEvent { index, .. }) => *index = i, _ => panic!("Cannot set index on uninitialized or header event"), } } @@ -168,6 +189,28 @@ impl From> for PhoenixMarketEvent { fees_collected_in_quote_lots: fees_collected_in_quote_lots.into(), index: 0, }), + MarketEvent::::TimeInForce { + order_sequence_number, + last_valid_slot, + last_valid_unix_timestamp_in_seconds, + } => Self::TimeInForce(TimeInForceEvent { + order_sequence_number, + last_valid_slot, + last_valid_unix_timestamp_in_seconds, + index: 0, + }), + MarketEvent::::ExpiredOrder { + maker_id, + order_sequence_number, + price_in_ticks, + base_lots_removed, + } => Self::ExpiredOrder(ExpiredOrderEvent { + maker_id, + order_sequence_number, + price_in_ticks: price_in_ticks.into(), + base_lots_removed: base_lots_removed.into(), + index: 0, + }), } } } diff --git a/src/program/processor/cancel_multiple_orders.rs b/src/program/processor/cancel_multiple_orders.rs index cc3e6ac..51339cd 100644 --- a/src/program/processor/cancel_multiple_orders.rs +++ b/src/program/processor/cancel_multiple_orders.rs @@ -1,8 +1,9 @@ use crate::{ program::{ - dispatch_market::load_with_dispatch_mut, loaders::CancelOrWithdrawContext as Cancel, - token_utils::try_withdraw, validation::checkers::phoenix_checkers::MarketAccountInfo, - MarketHeader, PhoenixMarketContext, PhoenixVaultContext, + assert_with_msg, dispatch_market::load_with_dispatch_mut, + loaders::CancelOrWithdrawContext as Cancel, token_utils::try_withdraw, + validation::checkers::phoenix_checkers::MarketAccountInfo, MarketHeader, PhoenixError, + PhoenixMarketContext, PhoenixVaultContext, }, quantities::{Ticks, WrapperU64}, state::{ @@ -279,6 +280,19 @@ pub(crate) fn process_cancel_orders<'a, 'info>( num_quote_lots_out * header.get_quote_lot_size(), num_base_lots_out * header.get_base_lot_size(), )?; + } else { + // This case is only reached if the user invoked CancelUpToWithFreeFunds + // In this case, there should be no funds to claim + assert_with_msg( + num_quote_lots_out == 0, + PhoenixError::CancelMultipleOrdersError, + "num_quote_lots_out must be 0", + )?; + assert_with_msg( + num_base_lots_out == 0, + PhoenixError::CancelMultipleOrdersError, + "num_base_lots_out must be 0", + )?; } Ok(()) diff --git a/src/program/processor/new_order.rs b/src/program/processor/new_order.rs index b69aecf..87c11e1 100644 --- a/src/program/processor/new_order.rs +++ b/src/program/processor/new_order.rs @@ -7,14 +7,14 @@ use crate::{ token_utils::{maybe_invoke_deposit, maybe_invoke_withdraw}, MarketHeader, PhoenixMarketContext, PhoenixVaultContext, }, - quantities::{BaseAtoms, BaseLots, QuoteAtoms, QuoteLots, WrapperU64}, + quantities::{BaseAtoms, BaseLots, QuoteAtoms, QuoteLots, Ticks, WrapperU64}, state::{markets::MarketEvent, OrderPacket, OrderPacketMetadata, Side}, }; use borsh::{BorshDeserialize, BorshSerialize}; use itertools::Itertools; use solana_program::{ - account_info::AccountInfo, entrypoint::ProgramResult, log::sol_log_compute_units, - program_error::ProgramError, pubkey::Pubkey, + account_info::AccountInfo, clock::Clock, entrypoint::ProgramResult, log::sol_log_compute_units, + program_error::ProgramError, pubkey::Pubkey, sysvar::Sysvar, }; use std::mem::size_of; @@ -32,51 +32,40 @@ pub struct MultipleOrderPacket { pub struct CondensedOrder { pub price_in_ticks: u64, pub size_in_base_lots: u64, + pub last_valid_slot: Option, + pub last_valid_unix_timestamp_in_seconds: Option, +} + +impl CondensedOrder { + pub fn new_default(price_in_ticks: u64, size_in_base_lots: u64) -> Self { + CondensedOrder { + price_in_ticks, + size_in_base_lots, + last_valid_slot: None, + last_valid_unix_timestamp_in_seconds: None, + } + } } impl MultipleOrderPacket { pub fn new( - bids: Vec<(u64, u64)>, - asks: Vec<(u64, u64)>, + bids: Vec, + asks: Vec, client_order_id: Option, reject_post_only: bool, ) -> Self { MultipleOrderPacket { - bids: bids - .iter() - .map(|(p, s)| CondensedOrder { - price_in_ticks: *p, - size_in_base_lots: *s, - }) - .collect(), - asks: asks - .iter() - .map(|(p, s)| CondensedOrder { - price_in_ticks: *p, - size_in_base_lots: *s, - }) - .collect(), + bids, + asks, client_order_id, reject_post_only, } } - pub fn new_default(bids: Vec<(u64, u64)>, asks: Vec<(u64, u64)>) -> Self { + pub fn new_default(bids: Vec, asks: Vec) -> Self { MultipleOrderPacket { - bids: bids - .iter() - .map(|(p, s)| CondensedOrder { - price_in_ticks: *p, - size_in_base_lots: *s, - }) - .collect(), - asks: asks - .iter() - .map(|(p, s)| CondensedOrder { - price_in_ticks: *p, - size_in_base_lots: *s, - }) - .collect(), + bids, + asks, client_order_id: None, reject_post_only: true, } @@ -303,10 +292,17 @@ fn process_new_order<'a, 'info>( base_atoms_to_withdraw, base_atoms_to_deposit, ) = { + let clock = Clock::get()?; + let mut get_clock_fn = || (clock.slot, clock.unix_timestamp as u64); let market_bytes = &mut market_info.try_borrow_mut_data()?[size_of::()..]; let market = load_with_dispatch_mut(&market_info.size_params, market_bytes)?.inner; let (_order_id, matching_engine_response) = market - .place_order(trader.key, *order_packet, record_event_fn) + .place_order( + trader.key, + *order_packet, + record_event_fn, + &mut get_clock_fn, + ) .ok_or(PhoenixError::NewOrderError)?; ( @@ -413,33 +409,53 @@ fn process_multiple_new_orders<'a, 'info>( }; { + let clock = Clock::get()?; + let mut get_clock_fn = || (clock.slot, clock.unix_timestamp as u64); let market_bytes = &mut market_info.try_borrow_mut_data()?[size_of::()..]; let market = load_with_dispatch_mut(&market_info.size_params, market_bytes)?.inner; for (book_orders, side) in [(&bids, Side::Bid), (&asks, Side::Ask)].iter() { for CondensedOrder { price_in_ticks, size_in_base_lots, + last_valid_slot, + last_valid_unix_timestamp_in_seconds, } in book_orders .iter() .sorted_by(|o1, o2| o1.price_in_ticks.cmp(&o2.price_in_ticks)) - .group_by(|o| o.price_in_ticks) - .into_iter() - .map(|(price_in_ticks, level)| CondensedOrder { - price_in_ticks, - size_in_base_lots: level.fold(0, |acc, o| acc + o.size_in_base_lots), + .group_by(|o| { + ( + o.price_in_ticks, + o.last_valid_slot, + o.last_valid_unix_timestamp_in_seconds, + ) }) + .into_iter() + .map( + |( + (price_in_ticks, last_valid_slot, last_valid_unix_timestamp_in_seconds), + level, + )| CondensedOrder { + price_in_ticks, + size_in_base_lots: level.fold(0, |acc, o| acc + o.size_in_base_lots), + last_valid_slot, + last_valid_unix_timestamp_in_seconds, + }, + ) { - let order_packet = OrderPacket::new_post_only( - *side, - price_in_ticks, - size_in_base_lots, + let order_packet = OrderPacket::PostOnly { + side: *side, + price_in_ticks: Ticks::new(price_in_ticks), + num_base_lots: BaseLots::new(size_in_base_lots), client_order_id, reject_post_only, - no_deposit, - ); + use_only_deposited_funds: no_deposit, + last_valid_slot, + last_valid_unix_timestamp_in_seconds, + }; + let matching_engine_response = { let (_order_id, matching_engine_response) = market - .place_order(trader.key, order_packet, record_event_fn) + .place_order(trader.key, order_packet, record_event_fn, &mut get_clock_fn) .ok_or(PhoenixError::NewOrderError)?; matching_engine_response }; @@ -469,6 +485,12 @@ fn process_multiple_new_orders<'a, 'info>( "e_vault, trader.as_ref(), )?; + } else { + assert_with_msg( + quote_lots_to_deposit == QuoteLots::ZERO, + PhoenixError::CancelMultipleOrdersError, + "Expected quote_lots_to_deposit to be zero", + )?; } if !asks.is_empty() { maybe_invoke_deposit( @@ -478,6 +500,12 @@ fn process_multiple_new_orders<'a, 'info>( &base_vault, trader.as_ref(), )?; + } else { + assert_with_msg( + base_lots_to_deposit == BaseLots::ZERO, + PhoenixError::CancelMultipleOrdersError, + "Expected base_lots_to_deposit to be zero", + )?; } } } else if base_lots_to_deposit > BaseLots::ZERO || quote_lots_to_deposit > QuoteLots::ZERO { diff --git a/src/state/inflight_order.rs b/src/state/inflight_order.rs index 4afe875..4d984b5 100644 --- a/src/state/inflight_order.rs +++ b/src/state/inflight_order.rs @@ -32,6 +32,10 @@ pub(crate) struct InflightOrder { /// Number of quote lots paid in fees pub quote_lot_fees: QuoteLots, + + pub last_valid_slot: Option, + + pub last_valid_unix_timestamp_in_seconds: Option, } impl InflightOrder { @@ -42,6 +46,8 @@ impl InflightOrder { match_limit: u64, base_lot_budget: BaseLots, adjusted_quote_lot_budget: AdjustedQuoteLots, + last_valid_slot: Option, + last_valid_unix_timestamp_in_seconds: Option, ) -> Self { InflightOrder { side, @@ -54,6 +60,8 @@ impl InflightOrder { matched_adjusted_quote_lots: AdjustedQuoteLots::ZERO, matched_base_lots: BaseLots::ZERO, quote_lot_fees: QuoteLots::ZERO, + last_valid_slot, + last_valid_unix_timestamp_in_seconds, } } diff --git a/src/state/markets/fifo.rs b/src/state/markets/fifo.rs index 13373c6..7c47fec 100644 --- a/src/state/markets/fifo.rs +++ b/src/state/markets/fifo.rs @@ -103,17 +103,65 @@ impl Ord for FIFOOrderId { pub struct FIFORestingOrder { pub trader_index: u64, pub num_base_lots: BaseLots, // Number of base lots quoted - _padding: [u64; 2], + pub last_valid_slot: u64, + pub last_valid_unix_timestamp_in_seconds: u64, } impl FIFORestingOrder { - pub fn new(trader_index: u64, num_base_lots: BaseLots) -> Self { + pub fn new_default(trader_index: u64, num_base_lots: BaseLots) -> Self { FIFORestingOrder { trader_index, num_base_lots, - _padding: [0; 2], + last_valid_slot: 0, + last_valid_unix_timestamp_in_seconds: 0, } } + + pub fn new( + trader_index: u64, + num_base_lots: BaseLots, + last_valid_slot: Option, + last_valid_unix_timestamp_in_seconds: Option, + ) -> Self { + FIFORestingOrder { + trader_index, + num_base_lots, + last_valid_slot: last_valid_slot.unwrap_or(0), + last_valid_unix_timestamp_in_seconds: last_valid_unix_timestamp_in_seconds.unwrap_or(0), + } + } + + pub fn new_with_last_valid_slot( + trader_index: u64, + num_base_lots: BaseLots, + last_valid_slot: u64, + ) -> Self { + FIFORestingOrder { + trader_index, + num_base_lots, + last_valid_slot, + last_valid_unix_timestamp_in_seconds: 0, + } + } + + pub fn new_with_last_valid_unix_timestamp( + trader_index: u64, + num_base_lots: BaseLots, + last_valid_unix_timestamp_in_seconds: u64, + ) -> Self { + FIFORestingOrder { + trader_index, + num_base_lots, + last_valid_slot: 0, + last_valid_unix_timestamp_in_seconds, + } + } + + pub fn is_expired(&self, current_slot: u64, current_unix_timestamp_in_seconds: u64) -> bool { + (self.last_valid_slot != 0 && self.last_valid_slot < current_slot) + || (self.last_valid_unix_timestamp_in_seconds != 0 + && self.last_valid_unix_timestamp_in_seconds < current_unix_timestamp_in_seconds) + } } impl RestingOrder for FIFORestingOrder { @@ -373,8 +421,9 @@ impl< trader_id: &MarketTraderId, order_packet: OrderPacket, record_event_fn: &mut dyn FnMut(MarketEvent), + get_clock_fn: &mut dyn FnMut() -> (u64, u64), ) -> Option<(Option, MatchingEngineResponse)> { - self.place_order_inner(trader_id, order_packet, record_event_fn) + self.place_order_inner(trader_id, order_packet, record_event_fn, get_clock_fn) } fn reduce_order( @@ -391,6 +440,7 @@ impl< order_id, side, size, + false, claim_funds, record_event_fn, ) @@ -543,16 +593,41 @@ impl< } #[inline] - /// Size with fees adjusted + /// Quote lot budget with fees adjusted (buys) /// - /// The desired result is size_in_lots / (1 + fee_bps). We approach this result by taking + /// The desired result is adjusted_quote_lots / (1 + fee_bps). We approach this result by taking /// (size_in_lots * u64::MAX) / (u64::MAX * (1 + fee_bps)) for accurate numerical precision. /// This will never overflow at any point in the calculation because all intermediate values /// will be stored in a u128. There is only a single multiplication of u64's which will be /// strictly less than u128::MAX - fn size_post_fee_adjustment(&self, size_in_adjusted_quote_lots: AdjustedQuoteLots) -> u64 { + fn adjusted_quote_lot_budget_post_fee_adjustment_for_buys( + &self, + size_in_adjusted_quote_lots: AdjustedQuoteLots, + ) -> Option { let fee_adjustment = self.compute_fee(AdjustedQuoteLots::MAX).as_u128() + u64::MAX as u128; - (size_in_adjusted_quote_lots.as_u128() * u64::MAX as u128 / fee_adjustment) as u64 + // Return an option to catch truncation from downcasting to u64 + u64::try_from(size_in_adjusted_quote_lots.as_u128() * u64::MAX as u128 / fee_adjustment) + .ok() + .map(AdjustedQuoteLots::new) + } + + #[inline] + /// Quote lot budget with fees adjusted (sells) + /// + /// The desired result is adjusted_quote_lots / (1 - fee_bps). We approach this result by taking + /// (size_in_lots * u64::MAX) / (u64::MAX * (1 - fee_bps)) for accurate numerical precision. + /// This will never overflow at any point in the calculation because all intermediate values + /// will be stored in a u128. There is only a single multiplication of u64's which will be + /// strictly less than u128::MAX + fn adjusted_quote_lot_budget_post_fee_adjustment_for_sells( + &self, + size_in_adjusted_quote_lots: AdjustedQuoteLots, + ) -> Option { + let fee_adjustment = self.compute_fee(AdjustedQuoteLots::MAX).as_u128() - u64::MAX as u128; + // Return an option to catch truncation from downcasting to u64 + u64::try_from(size_in_adjusted_quote_lots.as_u128() * u64::MAX as u128 / fee_adjustment) + .ok() + .map(AdjustedQuoteLots::new) } #[inline] @@ -579,24 +654,48 @@ impl< } /// This function determines whether a PostOnly order crosses the book. - /// If the order crosses the book, the function returns the price of the best order + /// If the order crosses the book, the function returns the price of the best unexpired order /// on the opposite side of the book in Ticks. Otherwise, it returns None. - fn check_for_cross(&mut self, side: Side, num_ticks: Ticks) -> Option { - let book = self.get_book_mut(side.opposite()); - while let Some((o_id, order)) = book.get_min() { - let crosses = match side.opposite() { - Side::Bid => o_id.price_in_ticks >= num_ticks, - Side::Ask => o_id.price_in_ticks <= num_ticks, - }; - if !crosses { - break; - } else if order.num_base_lots > BaseLots::ZERO { - return Some(o_id.price_in_ticks); + fn check_for_cross( + &mut self, + side: Side, + num_ticks: Ticks, + current_slot: u64, + current_unix_timestamp_in_seconds: u64, + record_event_fn: &mut dyn FnMut(MarketEvent), + ) -> Option { + loop { + let book_entry = self.get_book_mut(side.opposite()).get_min(); + if let Some((o_id, order)) = book_entry { + let crosses = match side.opposite() { + Side::Bid => o_id.price_in_ticks >= num_ticks, + Side::Ask => o_id.price_in_ticks <= num_ticks, + }; + if !crosses { + break; + } else if order.num_base_lots > BaseLots::ZERO { + if order.is_expired(current_slot, current_unix_timestamp_in_seconds) { + self.reduce_order_inner( + order.trader_index as u32, + &o_id, + side.opposite(), + None, + true, + false, + record_event_fn, + )?; + } else { + return Some(o_id.price_in_ticks); + } + } else { + // If the order is empty, we can remove it from the tree + // This case should never occur in v1 + phoenix_log!("WARNING: Empty order found in check_for_cross"); + self.get_book_mut(side.opposite()).remove(&o_id); + } } else { - // If the order is empty, we can remove it from the tree - // This case should never occur in v1 - phoenix_log!("WARNING: Empty order found in check_for_cross"); - book.remove(&o_id); + // Book is empty + break; } } None @@ -644,6 +743,7 @@ impl< trader_id: &MarketTraderId, mut order_packet: OrderPacket, record_event_fn: &mut dyn FnMut(MarketEvent), + get_clock_fn: &mut dyn FnMut() -> (u64, u64), ) -> Option<(Option, MatchingEngineResponse)> { if self.order_sequence_number == 0 { phoenix_log!("Market is uninitialized"); @@ -697,6 +797,13 @@ impl< } } + let (current_slot, current_unix_timestamp) = get_clock_fn(); + + if order_packet.is_expired(current_slot, current_unix_timestamp) { + phoenix_log!("Order parameters include a last_valid_slot or last_valid_unix_timestamp_in_seconds in the past, skipping matching and posting"); + return None; + } + let (resting_order, mut matching_engine_response) = if let OrderPacket::PostOnly { price_in_ticks, reject_post_only, @@ -704,7 +811,13 @@ impl< } = &mut order_packet { // Handle cases where PostOnly order would cross the book - if let Some(ticks) = self.check_for_cross(side, *price_in_ticks) { + if let Some(ticks) = self.check_for_cross( + side, + *price_in_ticks, + current_slot, + current_unix_timestamp, + record_event_fn, + ) { if *reject_post_only { phoenix_log!("PostOnly order crosses the book - order rejected"); return None; @@ -726,23 +839,37 @@ impl< } ( - FIFORestingOrder::new(trader_index as u64, order_packet.num_base_lots()), + FIFORestingOrder::new( + trader_index as u64, + order_packet.num_base_lots(), + order_packet.get_last_valid_slot(), + order_packet.get_last_valid_unix_timestamp_in_seconds(), + ), MatchingEngineResponse::default(), ) } else { let base_lot_budget = order_packet.base_lot_budget(); // Multiply the quote lot budget by the number of base lots per unit to get the number of // adjusted quote lots (quote_lots * base_lots_per_base_unit) - let adjusted_quote_lot_budget = AdjustedQuoteLots::new( - order_packet - .quote_lot_budget() - .map(|quote_lot_budget| { - self.size_post_fee_adjustment( - quote_lot_budget * self.base_lots_per_base_unit, - ) - }) - .unwrap_or(u64::MAX), - ); + let quote_lot_budget = order_packet.quote_lot_budget(); + let adjusted_quote_lot_budget = match side { + // For buys, the adjusted quote lot budget is decreased by the max fee. + // This is because the fee is added to the quote lots spent after the matching is complete. + Side::Bid => quote_lot_budget.and_then(|quote_lot_budget| { + self.adjusted_quote_lot_budget_post_fee_adjustment_for_buys( + quote_lot_budget * self.base_lots_per_base_unit, + ) + }), + // For sells, the adjusted quote lot budget is increased by the max fee. + // This is because the fee is subtracted from the quote lot received after the matching is complete. + Side::Ask => quote_lot_budget.and_then(|quote_lot_budget| { + self.adjusted_quote_lot_budget_post_fee_adjustment_for_sells( + quote_lot_budget * self.base_lots_per_base_unit, + ) + }), + } + .unwrap_or(AdjustedQuoteLots::new(u64::MAX)); + let mut inflight_order = InflightOrder::new( side, order_packet.self_trade_behavior(), @@ -750,9 +877,17 @@ impl< order_packet.match_limit(), base_lot_budget, adjusted_quote_lot_budget, + order_packet.get_last_valid_slot(), + order_packet.get_last_valid_unix_timestamp_in_seconds(), ); let resting_order = self - .match_order(&mut inflight_order, trader_index, record_event_fn) + .match_order( + &mut inflight_order, + trader_index, + record_event_fn, + current_slot, + current_unix_timestamp, + ) .map_or_else( || { phoenix_log!("Encountered error matching order"); @@ -948,6 +1083,18 @@ impl< client_order_id: order_packet.client_order_id(), }); + if resting_order.last_valid_slot != 0 + || resting_order.last_valid_unix_timestamp_in_seconds != 0 + { + // Record the time in force event + record_event_fn(MarketEvent::::TimeInForce { + order_sequence_number: order_id.order_sequence_number, + last_valid_slot: resting_order.last_valid_slot, + last_valid_unix_timestamp_in_seconds: resting_order + .last_valid_unix_timestamp_in_seconds, + }); + } + // Increment the order sequence number after successfully placing an order self.order_sequence_number += 1; } @@ -1003,11 +1150,19 @@ impl< inflight_order: &mut InflightOrder, current_trader_index: u32, record_event_fn: &mut dyn FnMut(MarketEvent), + current_slot: u64, + current_unix_timestamp: u64, ) -> Option { let mut total_matched_adjusted_quote_lots = AdjustedQuoteLots::ZERO; while inflight_order.in_progress() { // Find the first order on the opposite side of the book that matches the inflight order. - let (trader_index, order_id, num_base_lots_quoted) = { + let ( + trader_index, + order_id, + num_base_lots_quoted, + last_valid_slot, + last_valid_unix_timestamp_in_seconds, + ) = { let book = self.get_book_mut(inflight_order.side.opposite()); // Look at the top of the book to compare the book's price to the order's price let ( @@ -1016,7 +1171,8 @@ impl< FIFORestingOrder { trader_index, num_base_lots: num_base_lots_quoted, - .. + last_valid_slot, + last_valid_unix_timestamp_in_seconds, }, ) = if let Some((o_id, quote)) = book.get_min() { ( @@ -1043,9 +1199,34 @@ impl< inflight_order.match_limit -= 1; continue; } - (trader_index, order_id, num_base_lots_quoted) + ( + trader_index, + order_id, + num_base_lots_quoted, + last_valid_slot, + last_valid_unix_timestamp_in_seconds, + ) }; + // This block is entered if the order has expired. The order is removed from the book and + // the match limit is decremented. + if (last_valid_slot != 0 && last_valid_slot < current_slot) + || (last_valid_unix_timestamp_in_seconds != 0 + && last_valid_unix_timestamp_in_seconds < current_unix_timestamp) + { + self.reduce_order_inner( + trader_index as u32, + &order_id, + inflight_order.side.opposite(), + None, + true, + false, + record_event_fn, + )?; + inflight_order.match_limit -= 1; + continue; + } + // Handle self trade if trader_index == current_trader_index as u64 { match inflight_order.self_trade_behavior { @@ -1062,6 +1243,7 @@ impl< inflight_order.side.opposite(), None, false, + false, record_event_fn, )?; if inflight_order.self_trade_behavior == SelfTradeBehavior::DecrementTake { @@ -1140,14 +1322,21 @@ impl< // Increment the matched adjusted quote lots for fee calculation total_matched_adjusted_quote_lots += matched_adjusted_quote_lots; - // The fill event is recorded to be logged later - record_event_fn(MarketEvent::::Fill { - maker_id: self.get_trader_id_from_index(trader_index as u32), - order_sequence_number: order_id.order_sequence_number, - price_in_ticks: order_id.price_in_ticks, - base_lots_filled: matched_base_lots, - base_lots_remaining: order_remaining_base_lots, - }); + // If the matched base lots is zero, we don't record the fill event + if matched_base_lots != BaseLots::ZERO { + // The fill event is recorded + record_event_fn(MarketEvent::::Fill { + maker_id: self.get_trader_id_from_index(trader_index as u32), + order_sequence_number: order_id.order_sequence_number, + price_in_ticks: order_id.price_in_ticks, + base_lots_filled: matched_base_lots, + base_lots_remaining: order_remaining_base_lots, + }); + } else if !inflight_order.should_terminate { + phoenix_log!( + "WARNING: should_terminate should always be true if matched_base_lots is zero" + ); + } let base_lots_per_base_unit = self.base_lots_per_base_unit; // Update the maker's state to reflect the match @@ -1172,6 +1361,8 @@ impl< Some(FIFORestingOrder::new( current_trader_index as u64, inflight_order.base_lot_budget, + inflight_order.last_valid_slot, + inflight_order.last_valid_unix_timestamp_in_seconds, )) } @@ -1256,6 +1447,7 @@ impl< &order_id, Side::from_order_sequence_number(order_id.order_sequence_number), None, + false, claim_funds, record_event_fn, ) @@ -1290,9 +1482,11 @@ impl< order_id: &FIFOOrderId, side: Side, size: Option, + order_is_expired: bool, claim_funds: bool, record_event_fn: &mut dyn FnMut(MarketEvent), ) -> Option { + let maker_id = self.get_trader_id_from_index(trader_index); let removed_base_lots = { let book = self.get_book_mut(side); let (should_remove_order_from_book, base_lots_to_remove) = { @@ -1321,12 +1515,22 @@ impl< resting_order.num_base_lots -= base_lots_to_remove; resting_order.num_base_lots }; - record_event_fn(MarketEvent::Reduce { - order_sequence_number: order_id.order_sequence_number, - price_in_ticks: order_id.price_in_ticks, - base_lots_removed: base_lots_to_remove, - base_lots_remaining, - }); + // If the order was not cancelled by the maker, we make sure that the maker's id is logged. + if order_is_expired { + record_event_fn(MarketEvent::ExpiredOrder { + maker_id, + order_sequence_number: order_id.order_sequence_number, + price_in_ticks: order_id.price_in_ticks, + base_lots_removed: base_lots_to_remove, + }); + } else { + record_event_fn(MarketEvent::Reduce { + order_sequence_number: order_id.order_sequence_number, + price_in_ticks: order_id.price_in_ticks, + base_lots_removed: base_lots_to_remove, + base_lots_remaining, + }); + } base_lots_to_remove }; let (num_quote_lots, num_base_lots) = { diff --git a/src/state/markets/market_events.rs b/src/state/markets/market_events.rs index 9ae8f9a..427cb45 100644 --- a/src/state/markets/market_events.rs +++ b/src/state/markets/market_events.rs @@ -38,4 +38,15 @@ pub enum MarketEvent { Fee { fees_collected_in_quote_lots: QuoteLots, }, + TimeInForce { + order_sequence_number: u64, + last_valid_slot: u64, + last_valid_unix_timestamp_in_seconds: u64, + }, + ExpiredOrder { + maker_id: MarketTraderId, + order_sequence_number: u64, + price_in_ticks: Ticks, + base_lots_removed: BaseLots, + }, } diff --git a/src/state/markets/market_traits.rs b/src/state/markets/market_traits.rs index ba31ea9..810f855 100644 --- a/src/state/markets/market_traits.rs +++ b/src/state/markets/market_traits.rs @@ -175,6 +175,7 @@ pub(crate) trait WritableMarket< trader: &MarketTraderId, order_packet: MarketOrderPacket, record_event_fn: &mut dyn FnMut(MarketEvent), + get_clock_fn: &mut dyn FnMut() -> (u64, u64), ) -> Option<(Option, MatchingEngineResponse)>; fn cancel_order( diff --git a/src/state/markets/test_market.rs b/src/state/markets/test_market.rs index b43c78e..e1a7c98 100644 --- a/src/state/markets/test_market.rs +++ b/src/state/markets/test_market.rs @@ -1,4 +1,5 @@ use std::collections::VecDeque; +use std::time::{SystemTime, UNIX_EPOCH}; use crate::quantities::*; use crate::state::markets::*; @@ -33,6 +34,11 @@ fn setup_market_with_params( *dex } +/// Dummy placeholder clock function +fn get_clock_fn() -> (u64, u64) { + (0, 0) +} + #[allow(clippy::too_many_arguments)] fn layer_orders( dex: &mut Dex, @@ -78,6 +84,7 @@ fn layer_orders( &trader, OrderPacket::new_limit_order_default(side, *p, *s * adj), event_recorder, + &mut get_clock_fn, ) .unwrap(); } @@ -165,6 +172,7 @@ fn test_market_simple() { false, ), &mut record_event_fn, + &mut get_clock_fn, ) .unwrap(); assert!(o.is_none()); @@ -204,6 +212,7 @@ fn test_market_simple() { false, ), &mut record_event_fn, + &mut get_clock_fn, ) .unwrap(); println!( @@ -232,6 +241,7 @@ fn test_market_simple() { false, ), &mut record_event_fn, + &mut get_clock_fn, ) .unwrap(); @@ -274,6 +284,7 @@ fn test_market_simple() { false, ), &mut record_event_fn, + &mut get_clock_fn, ) .unwrap(); @@ -379,6 +390,7 @@ fn test_post_only_default() { &trader, OrderPacket::new_post_only_default(Side::Bid, 100, 1), &mut record_event_fn, + &mut get_clock_fn, ) .is_some()); // Cannot place post only order that would match @@ -387,6 +399,7 @@ fn test_post_only_default() { &trader, OrderPacket::new_post_only_default(Side::Ask, 100, 1), &mut record_event_fn, + &mut get_clock_fn, ) .is_none()); @@ -395,6 +408,7 @@ fn test_post_only_default() { &trader, OrderPacket::new_post_only_default(Side::Ask, 102, 1), &mut record_event_fn, + &mut get_clock_fn, ) .is_some()); assert!(market @@ -402,6 +416,7 @@ fn test_post_only_default() { &trader, OrderPacket::new_post_only_default(Side::Bid, 101, 1), &mut record_event_fn, + &mut get_clock_fn, ) .is_some()); @@ -411,6 +426,7 @@ fn test_post_only_default() { &trader, OrderPacket::new_post_only_default(Side::Bid, 102, 1), &mut record_event_fn, + &mut get_clock_fn, ) .is_none()); @@ -436,6 +452,7 @@ fn test_post_only_rejection() { &trader, OrderPacket::new_post_only(Side::Bid, 100, 1, 0, true, false), &mut record_event_fn, + &mut get_clock_fn, ) .is_some()); // Cannot place post only order that would match if reject flag is true @@ -444,6 +461,7 @@ fn test_post_only_rejection() { &trader, OrderPacket::new_post_only(Side::Ask, 100, 1, 0, true, false), &mut record_event_fn, + &mut get_clock_fn, ) .is_none()); @@ -452,6 +470,7 @@ fn test_post_only_rejection() { &trader, OrderPacket::new_post_only(Side::Ask, 102, 1, 0, true, false), &mut record_event_fn, + &mut get_clock_fn, ) .is_some()); assert!(market @@ -459,6 +478,7 @@ fn test_post_only_rejection() { &trader, OrderPacket::new_post_only(Side::Bid, 101, 1, 0, true, false), &mut record_event_fn, + &mut get_clock_fn, ) .is_some()); @@ -468,6 +488,7 @@ fn test_post_only_rejection() { &trader, OrderPacket::new_post_only(Side::Bid, 102, 1, 0, true, false), &mut record_event_fn, + &mut get_clock_fn, ) .is_none()); @@ -477,6 +498,7 @@ fn test_post_only_rejection() { &trader, OrderPacket::new_post_only(Side::Ask, 100, 1, 0, false, false), &mut record_event_fn, + &mut get_clock_fn, ) .is_some()); @@ -486,6 +508,7 @@ fn test_post_only_rejection() { &trader, OrderPacket::new_post_only(Side::Bid, 102, 1, 0, false, false), &mut record_event_fn, + &mut get_clock_fn, ) .is_some()); @@ -505,6 +528,7 @@ fn test_post_only_rejection() { &trader, OrderPacket::new_post_only(Side::Ask, 1, 1, 0, false, false), &mut record_event_fn, + &mut get_clock_fn, ) .is_some()); @@ -514,6 +538,7 @@ fn test_post_only_rejection() { &trader, OrderPacket::new_post_only(Side::Bid, 1, 1, 0, false, false), &mut record_event_fn, + &mut get_clock_fn, ) .is_none()); @@ -523,6 +548,7 @@ fn test_post_only_rejection() { &trader, OrderPacket::new_post_only(Side::Bid, 0, 1, 0, false, false), &mut record_event_fn, + &mut get_clock_fn, ) .is_none()); @@ -546,6 +572,7 @@ fn test_cancel_all() { &trader, OrderPacket::new_post_only_default(Side::Bid, 100 - i, 1), &mut record_event_fn, + &mut get_clock_fn, ) .is_some()); } @@ -557,6 +584,7 @@ fn test_cancel_all() { &trader, OrderPacket::new_post_only_default(Side::Ask, 102 + i, 1), &mut record_event_fn, + &mut get_clock_fn, ) .is_some()); } @@ -580,6 +608,7 @@ fn test_limit_orders_with_self_trade() { &trader, OrderPacket::new_limit_order_default(Side::Bid, 100, 5), &mut record_event_fn, + &mut get_clock_fn, ) .is_some()); let ladder = market.get_typed_ladder(1); @@ -591,6 +620,7 @@ fn test_limit_orders_with_self_trade() { &trader, OrderPacket::new_limit_order_default(Side::Ask, 100, 10), &mut record_event_fn, + &mut get_clock_fn, ) .unwrap(); let mut res = MatchingEngineResponse::default(); @@ -613,6 +643,7 @@ fn test_limit_orders_with_self_trade() { false, ), &mut record_event_fn, + &mut get_clock_fn, ) .is_none()); let (order, matching_engine_response) = market @@ -628,6 +659,7 @@ fn test_limit_orders_with_self_trade() { false, ), &mut record_event_fn, + &mut get_clock_fn, ) .unwrap(); assert!(order.is_some()); @@ -657,6 +689,7 @@ fn test_limit_orders_with_self_trade() { false, ), &mut record_event_fn, + &mut get_clock_fn, ) .unwrap(); assert!(order.is_some()); @@ -690,6 +723,7 @@ fn test_limit_orders_with_free_lots() { &trader, OrderPacket::new_limit_order_default(Side::Bid, 100, 5), &mut record_event_fn, + &mut get_clock_fn, ) .is_some()); @@ -698,6 +732,7 @@ fn test_limit_orders_with_free_lots() { &trader, OrderPacket::new_limit_order_default(Side::Bid, 95, 15), &mut record_event_fn, + &mut get_clock_fn, ) .is_some()); @@ -706,6 +741,7 @@ fn test_limit_orders_with_free_lots() { &taker, OrderPacket::new_limit_order_default(Side::Ask, 95, 15,), &mut record_event_fn, + &mut get_clock_fn, ) .is_some()); @@ -715,6 +751,7 @@ fn test_limit_orders_with_free_lots() { &trader, OrderPacket::new_limit_order_default(Side::Ask, 100, 5), &mut record_event_fn, + &mut get_clock_fn, ) .unwrap(); assert!(order.is_some()); @@ -729,6 +766,7 @@ fn test_limit_orders_with_free_lots() { &trader, OrderPacket::new_limit_order_default(Side::Ask, 100, 20), &mut record_event_fn, + &mut get_clock_fn, ) .unwrap(); assert!(order.is_some()); @@ -744,6 +782,7 @@ fn test_limit_orders_with_free_lots() { &trader, OrderPacket::new_limit_order_default(Side::Bid, 100, 10), &mut record_event_fn, + &mut get_clock_fn, ) .is_some()); @@ -753,6 +792,7 @@ fn test_limit_orders_with_free_lots() { &taker, OrderPacket::new_limit_order_default(Side::Bid, 101, 10), &mut record_event_fn, + &mut get_clock_fn, ) .is_some()); let (order, matching_engine_response) = market @@ -760,6 +800,7 @@ fn test_limit_orders_with_free_lots() { &trader, OrderPacket::new_limit_order_default(Side::Ask, 100, 50), &mut record_event_fn, + &mut get_clock_fn, ) .unwrap(); assert!(order.is_some()); @@ -777,6 +818,7 @@ fn test_limit_orders_with_free_lots() { &taker, OrderPacket::new_limit_order_default(Side::Bid, 105, 55,), &mut record_event_fn, + &mut get_clock_fn, ) .is_some()); @@ -786,6 +828,7 @@ fn test_limit_orders_with_free_lots() { &trader, OrderPacket::new_limit_order_default(Side::Bid, 100, 20), &mut record_event_fn, + &mut get_clock_fn, ) .unwrap(); assert!(order.is_some()); @@ -807,6 +850,7 @@ fn test_limit_orders_with_free_lots() { &trader, OrderPacket::new_limit_order_default(Side::Bid, 100, 50), &mut record_event_fn, + &mut get_clock_fn, ) .unwrap(); assert!(order.is_some()); @@ -828,6 +872,7 @@ fn test_limit_orders_with_free_lots() { &trader, OrderPacket::new_limit_order_default(Side::Bid, 120, 50), &mut record_event_fn, + &mut get_clock_fn, ) .is_some()); assert!(market @@ -835,6 +880,7 @@ fn test_limit_orders_with_free_lots() { &trader, OrderPacket::new_limit_order_default(Side::Ask, 120, 25), &mut record_event_fn, + &mut get_clock_fn, ) .is_some()); @@ -844,6 +890,7 @@ fn test_limit_orders_with_free_lots() { &taker, OrderPacket::new_limit_order_default(Side::Ask, 110, 25), &mut record_event_fn, + &mut get_clock_fn, ) .is_some()); let (order, matching_engine_response) = market @@ -851,6 +898,7 @@ fn test_limit_orders_with_free_lots() { &trader, OrderPacket::new_limit_order_default(Side::Bid, 111, 75), &mut record_event_fn, + &mut get_clock_fn, ) .unwrap(); assert!(order.is_some()); @@ -888,6 +936,7 @@ fn test_orders_with_only_free_funds() { &taker, OrderPacket::new_post_only(Side::Bid, 100, 5, 0, false, true,), &mut record_event_fn, + &mut get_clock_fn, ) .is_none()); @@ -896,6 +945,7 @@ fn test_orders_with_only_free_funds() { &trader, OrderPacket::new_post_only(Side::Bid, 100, 5, 0, false, false,), &mut record_event_fn, + &mut get_clock_fn, ) .is_some()); @@ -913,6 +963,7 @@ fn test_orders_with_only_free_funds() { true, ), &mut record_event_fn, + &mut get_clock_fn, ) .is_none()); @@ -929,6 +980,7 @@ fn test_orders_with_only_free_funds() { false, ), &mut record_event_fn, + &mut get_clock_fn, ) .is_some()); @@ -946,6 +998,7 @@ fn test_orders_with_only_free_funds() { true, ), &mut record_event_fn, + &mut get_clock_fn, ) .is_some()); @@ -962,6 +1015,7 @@ fn test_orders_with_only_free_funds() { false, ), &mut record_event_fn, + &mut get_clock_fn, ) .is_some()); @@ -978,6 +1032,7 @@ fn test_orders_with_only_free_funds() { false, ), &mut record_event_fn, + &mut get_clock_fn, ) .is_some()); @@ -995,6 +1050,7 @@ fn test_orders_with_only_free_funds() { true, ), &mut record_event_fn, + &mut get_clock_fn, ) .is_some()); } @@ -1010,6 +1066,7 @@ fn seed_market_with_orders( trader, OrderPacket::new_post_only_default(Side::Bid, 100 - i, 10), record_event_fn, + &mut get_clock_fn, ) .is_some()); assert!(market @@ -1017,6 +1074,7 @@ fn seed_market_with_orders( trader, OrderPacket::new_post_only_default(Side::Ask, 100 + i, 10), record_event_fn, + &mut get_clock_fn, ) .is_some()); } @@ -1051,6 +1109,7 @@ fn test_fok_and_ioc_limit_1() { false, ), &mut record_event_fn, + &mut get_clock_fn, ) .unwrap(); assert!(order.is_none()); @@ -1076,6 +1135,7 @@ fn test_fok_and_ioc_limit_1() { false, ), &mut record_event_fn, + &mut get_clock_fn, ) .unwrap(); assert!(order.is_none()); @@ -1110,6 +1170,7 @@ fn test_fok_and_ioc_limit_2() { false, ), &mut record_event_fn, + &mut get_clock_fn, ) .is_none()); assert!(market @@ -1124,6 +1185,7 @@ fn test_fok_and_ioc_limit_2() { false, ), &mut record_event_fn, + &mut get_clock_fn, ) .is_none()); } @@ -1158,6 +1220,7 @@ fn test_fok_and_ioc_limit_3() { false, ), &mut record_event_fn, + &mut get_clock_fn, ) .unwrap(); assert!(o.is_none()); @@ -1181,6 +1244,7 @@ fn test_fok_and_ioc_limit_3() { false, ), &mut record_event_fn, + &mut get_clock_fn, ) .unwrap(); assert!(o.is_none()); @@ -1223,6 +1287,7 @@ fn test_fok_and_ioc_limit_4() { false, ), &mut record_event_fn, + &mut get_clock_fn, ) .unwrap(); assert!(o.is_none()); @@ -1252,6 +1317,7 @@ fn test_fok_and_ioc_limit_4() { false, ), &mut record_event_fn, + &mut get_clock_fn, ) .unwrap(); assert!(o.is_none()); @@ -1294,6 +1360,7 @@ fn test_fok_and_ioc_limit_5() { false, ), &mut record_event_fn, + &mut get_clock_fn, ) .is_none()); assert!(market @@ -1308,6 +1375,7 @@ fn test_fok_and_ioc_limit_5() { false, ), &mut record_event_fn, + &mut get_clock_fn, ) .is_none()); @@ -1328,6 +1396,7 @@ fn test_fok_and_ioc_limit_5() { false, ), &mut record_event_fn, + &mut get_clock_fn, ) .is_none(), "Only one of num_base_lots or num_quote_lots should be set" @@ -1381,6 +1450,7 @@ fn test_fok_with_slippage_1() { &trader, OrderPacket::new_post_only_default(Side::Bid, 100 - i, 10000 * i), &mut record_event_fn, + &mut get_clock_fn, ) .is_some()); assert!(market @@ -1388,6 +1458,7 @@ fn test_fok_with_slippage_1() { &trader, OrderPacket::new_post_only_default(Side::Ask, 100 + i, 10000 * i), &mut record_event_fn, + &mut get_clock_fn, ) .is_some()); } @@ -1424,6 +1495,7 @@ fn test_fok_with_slippage_1() { min_base_lots_out.as_u64(), ), &mut record_event_fn, + &mut get_clock_fn, ) .unwrap(); println!("matching_engine_response: {:?}", matching_engine_response); @@ -1483,6 +1555,7 @@ fn test_fok_with_slippage_2() { &trader, OrderPacket::new_post_only_default(Side::Bid, 100 - i, 10000 * i), &mut record_event_fn, + &mut get_clock_fn, ) .is_some()); assert!(market @@ -1490,6 +1563,7 @@ fn test_fok_with_slippage_2() { &trader, OrderPacket::new_post_only_default(Side::Ask, 100 + i, 10000 * i), &mut record_event_fn, + &mut get_clock_fn, ) .is_some()); } @@ -1507,6 +1581,7 @@ fn test_fok_with_slippage_2() { .as_u64() ), // 2 full levels, 1 partial level &mut record_event_fn, + &mut get_clock_fn, ) .is_none()); } @@ -1529,6 +1604,7 @@ fn test_fok_with_slippage_3() { &trader, OrderPacket::new_post_only_default(Side::Bid, 100 - i, 10000 * i), &mut record_event_fn, + &mut get_clock_fn, ) .is_some()); assert!(market @@ -1536,6 +1612,7 @@ fn test_fok_with_slippage_3() { &trader, OrderPacket::new_post_only_default(Side::Ask, 100 + i, 10000 * i), &mut record_event_fn, + &mut get_clock_fn, ) .is_some()); } @@ -1573,6 +1650,7 @@ fn test_fok_with_slippage_3() { min_quote_lots_out.as_u64(), ), &mut record_event_fn, + &mut get_clock_fn, ) .unwrap(); assert!(order.is_none()); @@ -1636,6 +1714,7 @@ fn test_fees_basic() { &trader, OrderPacket::new_post_only_default(Side::Bid, 9900, 10), &mut record_event_fn, + &mut get_clock_fn, ) .is_some()); assert!(market @@ -1643,6 +1722,7 @@ fn test_fees_basic() { &trader, OrderPacket::new_post_only_default(Side::Ask, 10100, 10), &mut record_event_fn, + &mut get_clock_fn, ) .is_some()); @@ -1659,6 +1739,7 @@ fn test_fees_basic() { false, ), &mut record_event_fn, + &mut get_clock_fn, ) .unwrap(); assert!(o_id.is_none()); @@ -1685,6 +1766,7 @@ fn test_fees_basic() { false, ), &mut record_event_fn, + &mut get_clock_fn, ) .unwrap(); assert!(o_id.is_none()); @@ -1720,6 +1802,7 @@ fn test_evict_order() { &trader, OrderPacket::new_post_only_default(side, price.as_u64(), 1), &mut record_event_fn, + &mut get_clock_fn, ); } let direction = match side { @@ -1731,6 +1814,7 @@ fn test_evict_order() { &stink_order, OrderPacket::new_post_only_default(side, stink_price.as_u64(), 99), &mut record_event_fn, + &mut get_clock_fn, ); // Order must be more aggressive than the least aggressive order in a full book assert!(market @@ -1738,6 +1822,7 @@ fn test_evict_order() { &stink_order, OrderPacket::new_post_only_default(side, stink_price.as_u64(), 99), &mut record_event_fn, + &mut get_clock_fn, ) .is_none()); let mut event_recorder = VecDeque::new(); @@ -1751,6 +1836,7 @@ fn test_evict_order() { 99 ), &mut record_event_fn, + &mut get_clock_fn, ) .is_some()); @@ -1801,7 +1887,12 @@ fn test_reduce_order() { { let mut record_event_fn = |e: MarketEvent| event_recorder.push_back(e); market - .place_order(&maker, order_packet, &mut record_event_fn) + .place_order( + &maker, + order_packet, + &mut record_event_fn, + &mut get_clock_fn, + ) .unwrap(); } @@ -1857,7 +1948,12 @@ fn test_reduce_order() { { let mut record_event_fn = |e: MarketEvent| event_recorder.push_back(e); market - .place_order(&random_maker, order_packet, &mut record_event_fn) + .place_order( + &random_maker, + order_packet, + &mut record_event_fn, + &mut get_clock_fn, + ) .unwrap(); assert!( market @@ -1920,3 +2016,207 @@ fn test_reduce_order() { assert!(market.bids.get(&order_id).is_none()); } + +#[test] +fn test_tif() { + let mut rng = StdRng::seed_from_u64(2); + let mut market = setup_market(); + let maker = rng.gen::(); + + pub struct MockClock { + slot: u64, + timestamp: u64, + } + + let now = SystemTime::now(); + let exp = now + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() + .checked_add(1000) + .unwrap(); + + let order_packet_unix_timestamp_tif = OrderPacket::PostOnly { + side: Side::Bid, + price_in_ticks: Ticks::new(1000), + num_base_lots: BaseLots::new(100), + client_order_id: rng.gen::(), + use_only_deposited_funds: false, + reject_post_only: true, + last_valid_slot: None, + last_valid_unix_timestamp_in_seconds: Some(exp), + }; + + let order_packet_slot_tif = OrderPacket::PostOnly { + side: Side::Bid, + price_in_ticks: Ticks::new(1000), + num_base_lots: BaseLots::new(100), + client_order_id: rng.gen::(), + use_only_deposited_funds: false, + reject_post_only: true, + last_valid_slot: Some(2000), + last_valid_unix_timestamp_in_seconds: None, + }; + + for order_packet in [order_packet_unix_timestamp_tif, order_packet_slot_tif] { + let mut mock_clock = MockClock { + slot: 1000, + timestamp: now.duration_since(UNIX_EPOCH).unwrap().as_secs(), + }; + let mut event_recorder = VecDeque::new(); + let mut record_event_fn = |e: MarketEvent| event_recorder.push_back(e); + + { + let expired_mock_clock = MockClock { + slot: 3000, + timestamp: now.duration_since(UNIX_EPOCH).unwrap().as_secs() + 2000, + }; + let mut mock_clock_fn = || (expired_mock_clock.slot, expired_mock_clock.timestamp); + assert!(market + .place_order( + &maker, + order_packet, + &mut record_event_fn, + &mut mock_clock_fn, + ) + .is_none()); + } + + { + let mut mock_clock_fn = || (mock_clock.slot, mock_clock.timestamp); + market + .place_order( + &maker, + order_packet, + &mut record_event_fn, + &mut mock_clock_fn, + ) + .unwrap(); + } + + let taker = rng.gen::(); + + if order_packet.get_last_valid_slot().is_some() { + mock_clock.slot += 500; + } else { + mock_clock.timestamp += 500; + } + let (_, matching_engine_response) = { + let mut mock_clock_fn = || (mock_clock.slot, mock_clock.timestamp); + market + .place_order( + &taker, + OrderPacket::new_ioc_by_lots( + Side::Ask, + 0, + 10, + SelfTradeBehavior::Abort, + None, + rng.gen::(), + false, + ), + &mut record_event_fn, + &mut mock_clock_fn, + ) + .unwrap() + }; + + assert!(matching_engine_response.num_quote_lots_out > QuoteLots::ZERO); + + if order_packet.get_last_valid_slot().is_some() { + mock_clock.slot += 500; + } else { + mock_clock.timestamp += 500; + } + + let (_, matching_engine_response) = { + let mut mock_clock_fn = || (mock_clock.slot, mock_clock.timestamp); + market + .place_order( + &taker, + OrderPacket::new_ioc_by_lots( + Side::Ask, + 0, + 10, + SelfTradeBehavior::Abort, + None, + rng.gen::(), + false, + ), + &mut record_event_fn, + &mut mock_clock_fn, + ) + .unwrap() + }; + + assert!(matching_engine_response.num_quote_lots_out > QuoteLots::ZERO); + + if order_packet.get_last_valid_slot().is_some() { + mock_clock.slot += 1; + } else { + mock_clock.timestamp += 1; + } + + let (_, matching_engine_response) = { + let mut mock_clock_fn = || (mock_clock.slot, mock_clock.timestamp); + market + .place_order( + &taker, + OrderPacket::new_ioc_by_lots( + Side::Ask, + 0, + 10, + SelfTradeBehavior::Abort, + None, + rng.gen::(), + false, + ), + &mut record_event_fn, + &mut mock_clock_fn, + ) + .unwrap() + }; + + // Assert that TIF kicked in + assert_eq!(matching_engine_response.num_quote_lots_out, QuoteLots::ZERO); + + for (i, event) in event_recorder.iter().enumerate() { + match i { + 0 => { + assert!(matches!(event, MarketEvent::Place { .. })); + } + 1 => { + assert!(matches!(event, MarketEvent::TimeInForce { .. })); + } + 2 | 4 => { + assert!(matches!(event, MarketEvent::Fill { .. })); + } + 3 | 5 | 7 => { + assert!(matches!(event, MarketEvent::FillSummary { .. })); + } + 6 => { + if let MarketEvent::ExpiredOrder { + maker_id, + order_sequence_number, + price_in_ticks, + base_lots_removed, + } = event + { + assert_eq!(maker_id, &maker); + assert_eq!( + Side::from_order_sequence_number(*order_sequence_number), + Side::Bid + ); + assert_eq!(*price_in_ticks, Ticks::new(1000)); + assert_eq!(*base_lots_removed, BaseLots::new(80)); + } else { + panic!("Invalid event") + } + } + _ => { + panic!("Invalid event") + } + } + } + } +} diff --git a/src/state/order_schema/order_packet.rs b/src/state/order_schema/order_packet.rs index 2145db8..82adeb4 100644 --- a/src/state/order_schema/order_packet.rs +++ b/src/state/order_schema/order_packet.rs @@ -41,6 +41,12 @@ pub enum OrderPacket { /// Using only deposited funds will allow the trader to pass in less accounts per instruction and /// save transaction space as well as compute. This is only for traders who have a seat use_only_deposited_funds: bool, + + /// If this is set, the order will be invalid after the specified slot + last_valid_slot: Option, + + /// If this is set, the order will be invalid after the specified unix timestamp + last_valid_unix_timestamp_in_seconds: Option, }, /// This order type is used to place a limit order on the book @@ -68,6 +74,12 @@ pub enum OrderPacket { /// Using only deposited funds will allow the trader to pass in less accounts per instruction and /// save transaction space as well as compute. This is only for traders who have a seat use_only_deposited_funds: bool, + + /// If this is set, the order will be invalid after the specified slot + last_valid_slot: Option, + + /// If this is set, the order will be invalid after the specified unix timestamp + last_valid_unix_timestamp_in_seconds: Option, }, /// This order type is used to place an order that will be matched against existing resting orders @@ -169,6 +181,8 @@ impl OrderPacket { client_order_id: 0, reject_post_only: true, use_only_deposited_funds: false, + last_valid_slot: None, + last_valid_unix_timestamp_in_seconds: None, } } @@ -185,6 +199,8 @@ impl OrderPacket { client_order_id, reject_post_only: true, use_only_deposited_funds: false, + last_valid_slot: None, + last_valid_unix_timestamp_in_seconds: None, } } @@ -201,6 +217,8 @@ impl OrderPacket { client_order_id, reject_post_only: false, use_only_deposited_funds: false, + last_valid_slot: None, + last_valid_unix_timestamp_in_seconds: None, } } @@ -219,6 +237,8 @@ impl OrderPacket { client_order_id, reject_post_only, use_only_deposited_funds, + last_valid_slot: None, + last_valid_unix_timestamp_in_seconds: None, } } @@ -268,6 +288,8 @@ impl OrderPacket { match_limit, client_order_id, use_only_deposited_funds, + last_valid_slot: None, + last_valid_unix_timestamp_in_seconds: None, } } @@ -548,4 +570,46 @@ impl OrderPacket { } => *old_price_in_ticks = Some(price_in_ticks), } } + + pub fn get_last_valid_slot(&self) -> Option { + match self { + Self::PostOnly { + last_valid_slot, .. + } => *last_valid_slot, + Self::Limit { + last_valid_slot, .. + } => *last_valid_slot, + Self::ImmediateOrCancel { .. } => None, + } + } + + pub fn get_last_valid_unix_timestamp_in_seconds(&self) -> Option { + match self { + Self::PostOnly { + last_valid_unix_timestamp_in_seconds, + .. + } => *last_valid_unix_timestamp_in_seconds, + Self::Limit { + last_valid_unix_timestamp_in_seconds, + .. + } => *last_valid_unix_timestamp_in_seconds, + Self::ImmediateOrCancel { .. } => None, + } + } + + pub fn is_expired(&self, current_slot: u64, current_unix_timestamp_in_seconds: u64) -> bool { + if let Some(last_valid_slot) = self.get_last_valid_slot() { + if current_slot > last_valid_slot { + return true; + } + } + if let Some(last_valid_unix_timestamp_in_seconds) = + self.get_last_valid_unix_timestamp_in_seconds() + { + if current_unix_timestamp_in_seconds > last_valid_unix_timestamp_in_seconds { + return true; + } + } + false + } } diff --git a/tests/test_phoenix.rs b/tests/test_phoenix.rs index 73bc818..ea89ad3 100644 --- a/tests/test_phoenix.rs +++ b/tests/test_phoenix.rs @@ -3,6 +3,7 @@ use ellipsis_client::EllipsisClient; use phoenix::phoenix_log_authority; use phoenix::program::deposit::DepositParams; use phoenix::program::instruction_builders::*; +use phoenix::program::new_order::CondensedOrder; use phoenix::program::new_order::MultipleOrderPacket; use phoenix::program::MarketHeader; use phoenix::quantities::WrapperU64; @@ -2280,24 +2281,32 @@ async fn test_phoenix_place_multiple_limit_orders() { // Place multiple post only orders successfully let multiple_order_packet = MultipleOrderPacket::new_default( vec![ - ( - sdk.float_price_to_ticks(8.0), - sdk.raw_base_units_to_base_lots(10.0), - ), - ( - sdk.float_price_to_ticks(9.0), - sdk.raw_base_units_to_base_lots(10.0), - ), + CondensedOrder { + price_in_ticks: sdk.float_price_to_ticks(8.0), + size_in_base_lots: sdk.raw_base_units_to_base_lots(10.0), + last_valid_slot: None, + last_valid_unix_timestamp_in_seconds: None, + }, + CondensedOrder { + price_in_ticks: sdk.float_price_to_ticks(9.0), + size_in_base_lots: sdk.raw_base_units_to_base_lots(10.0), + last_valid_slot: None, + last_valid_unix_timestamp_in_seconds: None, + }, ], vec![ - ( - sdk.float_price_to_ticks(10.0), - sdk.raw_base_units_to_base_lots(10.0), - ), - ( - sdk.float_price_to_ticks(11.0), - sdk.raw_base_units_to_base_lots(10.0), - ), + CondensedOrder { + price_in_ticks: sdk.float_price_to_ticks(10.0), + size_in_base_lots: sdk.raw_base_units_to_base_lots(10.0), + last_valid_slot: None, + last_valid_unix_timestamp_in_seconds: None, + }, + CondensedOrder { + price_in_ticks: sdk.float_price_to_ticks(11.0), + size_in_base_lots: sdk.raw_base_units_to_base_lots(10.0), + last_valid_slot: None, + last_valid_unix_timestamp_in_seconds: None, + }, ], ); @@ -2332,25 +2341,25 @@ async fn test_phoenix_place_multiple_limit_orders() { // Ensure free funds order doesnt place if not enough base lots but enough quote lots let multiple_order_packet = MultipleOrderPacket::new_default( vec![ - ( + CondensedOrder::new_default( sdk.float_price_to_ticks(8.0), - sdk.raw_base_units_to_base_lots(10.0), + sdk.raw_base_units_to_base_lots(9.0), ), - ( - sdk.float_price_to_ticks(9.0), + CondensedOrder::new_default( + sdk.float_price_to_ticks(11.0), sdk.raw_base_units_to_base_lots(10.0), ), ], vec![ - ( + CondensedOrder::new_default( sdk.float_price_to_ticks(10.0), sdk.raw_base_units_to_base_lots(10.0), ), - ( + CondensedOrder::new_default( sdk.float_price_to_ticks(11.0), sdk.raw_base_units_to_base_lots(10.0), ), - ( + CondensedOrder::new_default( sdk.float_price_to_ticks(12.0), sdk.raw_base_units_to_base_lots(4.0), ), @@ -2373,28 +2382,38 @@ async fn test_phoenix_place_multiple_limit_orders() { let multiple_order_packet = MultipleOrderPacket::new_default( vec![ - ( - sdk.float_price_to_ticks(8.0), - sdk.raw_base_units_to_base_lots(10.0), - ), - ( - sdk.float_price_to_ticks(9.0), - sdk.raw_base_units_to_base_lots(10.0), - ), - ( - sdk.float_price_to_ticks(3.0), - sdk.raw_base_units_to_base_lots(1.0), - ), + CondensedOrder { + price_in_ticks: sdk.float_price_to_ticks(8.0), + size_in_base_lots: sdk.raw_base_units_to_base_lots(10.0), + last_valid_slot: None, + last_valid_unix_timestamp_in_seconds: None, + }, + CondensedOrder { + price_in_ticks: sdk.float_price_to_ticks(9.0), + size_in_base_lots: sdk.raw_base_units_to_base_lots(10.0), + last_valid_slot: None, + last_valid_unix_timestamp_in_seconds: None, + }, + CondensedOrder { + price_in_ticks: sdk.float_price_to_ticks(3.0), + size_in_base_lots: sdk.raw_base_units_to_base_lots(1.0), + last_valid_slot: None, + last_valid_unix_timestamp_in_seconds: None, + }, ], vec![ - ( - sdk.float_price_to_ticks(10.0), - sdk.raw_base_units_to_base_lots(10.0), - ), - ( - sdk.float_price_to_ticks(11.0), - sdk.raw_base_units_to_base_lots(10.0), - ), + CondensedOrder { + price_in_ticks: sdk.float_price_to_ticks(10.0), + size_in_base_lots: sdk.raw_base_units_to_base_lots(10.0), + last_valid_slot: None, + last_valid_unix_timestamp_in_seconds: None, + }, + CondensedOrder { + price_in_ticks: sdk.float_price_to_ticks(11.0), + size_in_base_lots: sdk.raw_base_units_to_base_lots(10.0), + last_valid_slot: None, + last_valid_unix_timestamp_in_seconds: None, + }, ], ); @@ -2413,28 +2432,38 @@ async fn test_phoenix_place_multiple_limit_orders() { // place multiple post only orders successfully with free funds let multiple_order_packet = MultipleOrderPacket::new_default( vec![ - ( - sdk.float_price_to_ticks(8.0), - sdk.raw_base_units_to_base_lots(10.0), - ), - ( - sdk.float_price_to_ticks(9.0), - sdk.raw_base_units_to_base_lots(10.0), - ), + CondensedOrder { + price_in_ticks: sdk.float_price_to_ticks(8.0), + size_in_base_lots: sdk.raw_base_units_to_base_lots(10.0), + last_valid_slot: None, + last_valid_unix_timestamp_in_seconds: None, + }, + CondensedOrder { + price_in_ticks: sdk.float_price_to_ticks(9.0), + size_in_base_lots: sdk.raw_base_units_to_base_lots(10.0), + last_valid_slot: None, + last_valid_unix_timestamp_in_seconds: None, + }, ], vec![ - ( - sdk.float_price_to_ticks(17.0), - sdk.raw_base_units_to_base_lots(10.0), - ), - ( - sdk.float_price_to_ticks(17.0), - sdk.raw_base_units_to_base_lots(5.0), - ), - ( - sdk.float_price_to_ticks(12.0), - sdk.raw_base_units_to_base_lots(5.0), - ), + CondensedOrder { + price_in_ticks: sdk.float_price_to_ticks(17.0), + size_in_base_lots: sdk.raw_base_units_to_base_lots(10.0), + last_valid_slot: None, + last_valid_unix_timestamp_in_seconds: None, + }, + CondensedOrder { + price_in_ticks: sdk.float_price_to_ticks(17.0), + size_in_base_lots: sdk.raw_base_units_to_base_lots(5.0), + last_valid_slot: None, + last_valid_unix_timestamp_in_seconds: None, + }, + CondensedOrder { + price_in_ticks: sdk.float_price_to_ticks(12.0), + size_in_base_lots: sdk.raw_base_units_to_base_lots(5.0), + last_valid_slot: None, + last_valid_unix_timestamp_in_seconds: None, + }, ], ); let new_order_ix = create_new_multiple_order_with_free_funds_instruction( @@ -2474,21 +2503,21 @@ async fn test_phoenix_place_multiple_limit_orders() { // Ensure we can't place orders in cross against themselves let multiple_order_packet = MultipleOrderPacket::new_default( vec![ - ( + CondensedOrder::new_default( sdk.float_price_to_ticks(8.0), sdk.raw_base_units_to_base_lots(10.0), ), - ( + CondensedOrder::new_default( sdk.float_price_to_ticks(9.0), sdk.raw_base_units_to_base_lots(10.0), ), ], vec![ - ( + CondensedOrder::new_default( sdk.float_price_to_ticks(9.0), sdk.raw_base_units_to_base_lots(10.0), ), - ( + CondensedOrder::new_default( sdk.float_price_to_ticks(11.0), sdk.raw_base_units_to_base_lots(10.0), ), @@ -2512,25 +2541,25 @@ async fn test_phoenix_place_multiple_limit_orders() { // Ensure we can't place orders in cross against themselves, different variation let multiple_order_packet = MultipleOrderPacket::new_default( vec![ - ( + CondensedOrder::new_default( sdk.float_price_to_ticks(29.0), sdk.raw_base_units_to_base_lots(1.0), ), - ( + CondensedOrder::new_default( sdk.float_price_to_ticks(9.0), sdk.raw_base_units_to_base_lots(10.0), ), - ( + CondensedOrder::new_default( sdk.float_price_to_ticks(19.0), sdk.raw_base_units_to_base_lots(10.0), ), ], vec![ - ( + CondensedOrder::new_default( sdk.float_price_to_ticks(30.0), sdk.raw_base_units_to_base_lots(10.0), ), - ( + CondensedOrder::new_default( sdk.float_price_to_ticks(25.0), sdk.raw_base_units_to_base_lots(10.0), ), @@ -2589,21 +2618,21 @@ async fn test_phoenix_place_multiple_limit_orders() { // Ensure we can't place orders in cross against the existing book let multiple_order_packet = MultipleOrderPacket::new_default( vec![ - ( + CondensedOrder::new_default( sdk.float_price_to_ticks(8.0), sdk.raw_base_units_to_base_lots(10.0), ), - ( + CondensedOrder::new_default( sdk.float_price_to_ticks(9.0), sdk.raw_base_units_to_base_lots(10.0), ), ], vec![ - ( + CondensedOrder::new_default( sdk.float_price_to_ticks(10.0), sdk.raw_base_units_to_base_lots(10.0), ), - ( + CondensedOrder::new_default( sdk.float_price_to_ticks(11.0), sdk.raw_base_units_to_base_lots(10.0), ), @@ -2626,11 +2655,11 @@ async fn test_phoenix_place_multiple_limit_orders() { let multiple_order_packet = MultipleOrderPacket::new_default( vec![ - ( + CondensedOrder::new_default( sdk.float_price_to_ticks(20.0), sdk.raw_base_units_to_base_lots(10.0), ), - ( + CondensedOrder::new_default( sdk.float_price_to_ticks(9.0), sdk.raw_base_units_to_base_lots(10.0), ), @@ -2656,46 +2685,46 @@ async fn test_phoenix_place_multiple_limit_orders() { // Currently have 20 base units and 170 quote units available let multiple_order_packet = MultipleOrderPacket::new_default( vec![ - ( + CondensedOrder::new_default( sdk.float_price_to_ticks(5.0), sdk.raw_base_units_to_base_lots(10.0), ), - ( + CondensedOrder::new_default( sdk.float_price_to_ticks(4.0), sdk.raw_base_units_to_base_lots(10.0), ), - ( + CondensedOrder::new_default( sdk.float_price_to_ticks(3.0), sdk.raw_base_units_to_base_lots(10.0), ), - ( + CondensedOrder::new_default( sdk.float_price_to_ticks(5.0), sdk.raw_base_units_to_base_lots(10.0), ), - ( + CondensedOrder::new_default( //this order is all of the extra quote lots we need to deposit sdk.float_price_to_ticks(4.0), sdk.raw_base_units_to_base_lots(10.0), ), ], vec![ - ( + CondensedOrder::new_default( sdk.float_price_to_ticks(100.0), sdk.raw_base_units_to_base_lots(5.0), ), - ( + CondensedOrder::new_default( sdk.float_price_to_ticks(105.0), sdk.raw_base_units_to_base_lots(5.0), ), - ( + CondensedOrder::new_default( sdk.float_price_to_ticks(100.0), sdk.raw_base_units_to_base_lots(5.0), ), - ( + CondensedOrder::new_default( sdk.float_price_to_ticks(103.0), sdk.raw_base_units_to_base_lots(5.0), ), - ( + CondensedOrder::new_default( //this order is all of the extra base lots we need to deposit sdk.float_price_to_ticks(102.0), sdk.raw_base_units_to_base_lots(5.0), @@ -2751,7 +2780,7 @@ async fn test_phoenix_place_multiple_limit_orders() { // Send 100 orders on each side to verify there is enough compute to do so let bids = (1..101) .map(|i| { - ( + CondensedOrder::new_default( sdk.float_price_to_ticks(100.0 - (i as f64 * 0.1)), sdk.raw_base_units_to_base_lots(10.0), ) @@ -2759,7 +2788,7 @@ async fn test_phoenix_place_multiple_limit_orders() { .collect::>(); let asks = (1..101) .map(|i| { - ( + CondensedOrder::new_default( sdk.float_price_to_ticks(100.0 + (i as f64 * 0.1)), sdk.raw_base_units_to_base_lots(10.0), ) @@ -2790,7 +2819,7 @@ async fn test_phoenix_place_multiple_limit_orders() { //Send multiple orders in cross via the second maker - verify this throws an error let bids = (1..30) .map(|i| { - ( + CondensedOrder::new_default( sdk.float_price_to_ticks(101.0 - (i as f64 * 0.1)), sdk.raw_base_units_to_base_lots(10.0), ) @@ -2798,7 +2827,7 @@ async fn test_phoenix_place_multiple_limit_orders() { .collect::>(); let asks = (1..30) .map(|i| { - ( + CondensedOrder::new_default( sdk.float_price_to_ticks(99.0 + (i as f64 * 0.1)), sdk.raw_base_units_to_base_lots(10.0), ) @@ -2822,7 +2851,7 @@ async fn test_phoenix_place_multiple_limit_orders() { // Send multiple orders in cross via the second maker, this time with post only rejection set to false - verify this succeeds let bids = (1..30) .map(|i| { - ( + CondensedOrder::new_default( sdk.float_price_to_ticks(101.0 - (i as f64 * 0.1)), sdk.raw_base_units_to_base_lots(10.0), ) @@ -2830,7 +2859,7 @@ async fn test_phoenix_place_multiple_limit_orders() { .collect::>(); let asks = (1..30) .map(|i| { - ( + CondensedOrder::new_default( sdk.float_price_to_ticks(99.0 + (i as f64 * 0.1)), sdk.raw_base_units_to_base_lots(10.0), ) @@ -3015,7 +3044,7 @@ async fn test_phoenix_place_multiple_memory_management() { // Send 40 orders on each side to verify there is enough compute to do so let bids = (1..41) .map(|i| { - ( + CondensedOrder::new_default( sdk.float_price_to_ticks(100.0 - (i as f64 * 0.1)), sdk.raw_base_units_to_base_lots(1.0), ) @@ -3023,7 +3052,7 @@ async fn test_phoenix_place_multiple_memory_management() { .collect::>(); let asks = (1..41) .map(|i| { - ( + CondensedOrder::new_default( sdk.float_price_to_ticks(100.0 + (i as f64 * 0.1)), sdk.raw_base_units_to_base_lots(1.0), ) @@ -3098,10 +3127,10 @@ async fn test_phoenix_place_multiple_limit_orders_adversarial() { // Stuff the book with 1 lots loop { let bids = (start..start + 30) - .map(|_| (sdk.float_price_to_ticks(99.0), 1)) + .map(|_| CondensedOrder::new_default(sdk.float_price_to_ticks(99.0), 1)) .collect::>(); let asks = (start..start + 30) - .map(|_| (sdk.float_price_to_ticks(100.0), 1)) + .map(|_| CondensedOrder::new_default(sdk.float_price_to_ticks(100.0), 1)) .collect::>(); let multiple_order_packet = MultipleOrderPacket::new_default(bids, asks); From 6bd9152ff182d068ce02ac465be3ac162369e0df Mon Sep 17 00:00:00 2001 From: Jarry Xiao <61092285+jarry-xiao@users.noreply.github.com> Date: Tue, 14 Mar 2023 13:07:11 -0400 Subject: [PATCH 2/4] Add minor changes to matching engine logic (#24) Changes: - DecrementTake handles partial removals - Free fund accounting now occurs after the matching/order placement to ensure that checks work as intended. --- idl/generateClient.js | 16 ++- idl/phoenix_v1.json | 24 ++++ src/program/processor/withdraw.rs | 2 +- src/shank_structs.rs | 4 + src/state/markets/fifo.rs | 199 ++++++++++++++++---------- src/state/markets/market_traits.rs | 37 ++++- src/state/markets/test_market.rs | 151 ++++++++++++++++++- src/state/matching_engine_response.rs | 7 +- 8 files changed, 350 insertions(+), 90 deletions(-) diff --git a/idl/generateClient.js b/idl/generateClient.js index 0d0b765..2a3e16c 100644 --- a/idl/generateClient.js +++ b/idl/generateClient.js @@ -1,4 +1,5 @@ const { Solita } = require("@metaplex-foundation/solita"); +const { spawn } = require("child_process"); const path = require("path"); const idlDir = __dirname; @@ -8,7 +9,18 @@ const PROGRAM_NAME = "phoenix_v1"; async function main() { generateTypeScriptSDK().then(() => { - conoole.log("done"); + console.log("Running prettier on generated files..."); + // Note: prettier is not a dependency of this package, so it must be installed + // TODO: Add a prettier config file for consistent style + spawn("prettier", ["--write", sdkDir], { stdio: "inherit" }) + .on("error", (err) => { + console.error( + "Failed to lint client files. Try installing prettier (`npm install --save-dev --save-exact prettier`)" + ); + }) + .on("exit", () => { + console.log("Finished linting files."); + }); }); } @@ -18,8 +30,6 @@ async function generateTypeScriptSDK() { const idl = require(generatedIdlPath); const gen = new Solita(idl, { formatCode: true }); await gen.renderAndWriteTo(sdkDir); - console.error("Success!"); - process.exit(0); } main().catch((err) => { diff --git a/idl/phoenix_v1.json b/idl/phoenix_v1.json index 1dcc88a..27c1015 100644 --- a/idl/phoenix_v1.json +++ b/idl/phoenix_v1.json @@ -2316,6 +2316,18 @@ { "name": "use_only_deposited_funds", "type": "bool" + }, + { + "name": "last_valid_slot", + "type": { + "option": "u64" + } + }, + { + "name": "last_valid_unix_timestamp_in_seconds", + "type": { + "option": "u64" + } } ] }, @@ -2355,6 +2367,18 @@ { "name": "use_only_deposited_funds", "type": "bool" + }, + { + "name": "last_valid_slot", + "type": { + "option": "u64" + } + }, + { + "name": "last_valid_unix_timestamp_in_seconds", + "type": { + "option": "u64" + } } ] }, diff --git a/src/program/processor/withdraw.rs b/src/program/processor/withdraw.rs index 1dcf2df..511cf11 100644 --- a/src/program/processor/withdraw.rs +++ b/src/program/processor/withdraw.rs @@ -81,7 +81,7 @@ pub(crate) fn process_withdraw<'a, 'info>( base_lots_to_withdraw.map(BaseLots::new), evict_seat, ) - .ok_or(PhoenixError::ReduceOrderError)?; + .ok_or(PhoenixError::WithdrawFundsError)?; sol_log_compute_units(); if evict_seat { assert_with_msg( diff --git a/src/shank_structs.rs b/src/shank_structs.rs index 1968c97..64eff90 100644 --- a/src/shank_structs.rs +++ b/src/shank_structs.rs @@ -40,6 +40,8 @@ enum OrderPacket { client_order_id: u128, reject_post_only: bool, use_only_deposited_funds: bool, + last_valid_slot: Option, + last_valid_unix_timestamp_in_seconds: Option, }, Limit { side: Side, @@ -49,6 +51,8 @@ enum OrderPacket { match_limit: Option, client_order_id: u128, use_only_deposited_funds: bool, + last_valid_slot: Option, + last_valid_unix_timestamp_in_seconds: Option, }, ImmediateOrCancel { side: Side, diff --git a/src/state/markets/fifo.rs b/src/state/markets/fifo.rs index 7c47fec..059c5fe 100644 --- a/src/state/markets/fifo.rs +++ b/src/state/markets/fifo.rs @@ -156,18 +156,34 @@ impl FIFORestingOrder { last_valid_unix_timestamp_in_seconds, } } - - pub fn is_expired(&self, current_slot: u64, current_unix_timestamp_in_seconds: u64) -> bool { - (self.last_valid_slot != 0 && self.last_valid_slot < current_slot) - || (self.last_valid_unix_timestamp_in_seconds != 0 - && self.last_valid_unix_timestamp_in_seconds < current_unix_timestamp_in_seconds) - } } impl RestingOrder for FIFORestingOrder { fn size(&self) -> u64 { self.num_base_lots.as_u64() } + + fn last_valid_slot(&self) -> Option { + if self.last_valid_slot == 0 { + None + } else { + Some(self.last_valid_slot) + } + } + + fn last_valid_unix_timestamp_in_seconds(&self) -> Option { + if self.last_valid_unix_timestamp_in_seconds == 0 { + None + } else { + Some(self.last_valid_unix_timestamp_in_seconds) + } + } + + fn is_expired(&self, current_slot: u64, current_unix_timestamp_in_seconds: u64) -> bool { + (self.last_valid_slot != 0 && self.last_valid_slot < current_slot) + || (self.last_valid_unix_timestamp_in_seconds != 0 + && self.last_valid_unix_timestamp_in_seconds < current_unix_timestamp_in_seconds) + } } #[repr(C)] @@ -775,6 +791,11 @@ impl< self.get_or_register_trader(trader_id)? }; + if order_packet.num_base_lots() == 0 && order_packet.num_quote_lots() == 0 { + phoenix_log!("Either num_base_lots or num_quote_lots must be nonzero"); + return None; + } + // For IOC order types exactly one of num_quote_lots or num_base_lots needs to be specified. if let OrderPacket::ImmediateOrCancel { num_base_lots, @@ -868,7 +889,7 @@ impl< ) }), } - .unwrap_or(AdjustedQuoteLots::new(u64::MAX)); + .unwrap_or_else(|| AdjustedQuoteLots::new(u64::MAX)); let mut inflight_order = InflightOrder::new( side, @@ -912,7 +933,7 @@ impl< - inflight_order.quote_lot_fees } }; - let mut matching_engine_response = match side { + let matching_engine_response = match side { Side::Bid => MatchingEngineResponse::new_from_buy( matched_quote_lots, inflight_order.matched_base_lots, @@ -923,44 +944,6 @@ impl< ), }; - // If the trader is a registered trader, check if they have free lots - if trader_index != u32::MAX { - let trader_state = self.get_trader_state_from_index_mut(trader_index); - match side { - Side::Bid => { - let quote_lots_free_to_use = - trader_state.quote_lots_free.min(matched_quote_lots); - trader_state.use_free_quote_lots(quote_lots_free_to_use); - matching_engine_response.use_free_quote_lots(quote_lots_free_to_use); - } - Side::Ask => { - let base_lots_free_to_use = trader_state - .base_lots_free - .min(inflight_order.matched_base_lots); - trader_state.use_free_base_lots(base_lots_free_to_use); - matching_engine_response.use_free_base_lots(base_lots_free_to_use); - } - } - - // If the order crosses and only uses deposited funds, then add the matched funds back to the trader's free funds - // Set the matching_engine_response lots_out to zero to set token withdrawals to zero - if order_packet.no_deposit_or_withdrawal() { - match side { - Side::Bid => { - trader_state - .deposit_free_base_lots(matching_engine_response.num_base_lots_out); - matching_engine_response.num_base_lots_out = BaseLots::ZERO; - } - Side::Ask => { - trader_state.deposit_free_quote_lots( - matching_engine_response.num_quote_lots_out, - ); - matching_engine_response.num_quote_lots_out = QuoteLots::ZERO; - } - } - } - } - record_event_fn(MarketEvent::FillSummary { client_order_id: order_packet.client_order_id(), total_base_lots_filled: inflight_order.matched_base_lots, @@ -997,15 +980,6 @@ impl< ); return None; } - - // Check the trader had enough deposited funds to process the order - if order_packet.no_deposit_or_withdrawal() - && trader_index != u32::MAX - && !matching_engine_response.verify_no_deposit_or_withdrawal() - { - phoenix_log!("Insufficient deposited funds to process order"); - return None; - } } else { let price_in_ticks = order_packet.get_price_in_ticks(); let (order_id, book_full) = match side { @@ -1066,15 +1040,6 @@ impl< } } - // Check if trader has enough deposited funds to process the order - if order_packet.no_deposit_or_withdrawal() - && trader_index != u32::MAX - && !matching_engine_response.verify_no_deposit_or_withdrawal() - { - phoenix_log!("Insufficient deposited funds to process order"); - return None; - } - // Record the place event record_event_fn(MarketEvent::::Place { order_sequence_number: order_id.order_sequence_number, @@ -1099,6 +1064,57 @@ impl< self.order_sequence_number += 1; } } + + // If the trader is a registered trader, check if they have free lots + if trader_index != u32::MAX { + let trader_state = self.get_trader_state_from_index_mut(trader_index); + match side { + Side::Bid => { + let quote_lots_free_to_use = trader_state + .quote_lots_free + .min(matching_engine_response.num_quote_lots()); + trader_state.use_free_quote_lots(quote_lots_free_to_use); + matching_engine_response.use_free_quote_lots(quote_lots_free_to_use); + } + Side::Ask => { + let base_lots_free_to_use = trader_state + .base_lots_free + .min(matching_engine_response.num_base_lots()); + trader_state.use_free_base_lots(base_lots_free_to_use); + matching_engine_response.use_free_base_lots(base_lots_free_to_use); + } + } + + // If the order crosses and only uses deposited funds, then add the matched funds back to the trader's free funds + // Set the matching_engine_response lots_out to zero to set token withdrawals to zero + if order_packet.no_deposit_or_withdrawal() { + match side { + Side::Bid => { + trader_state + .deposit_free_base_lots(matching_engine_response.num_base_lots_out); + matching_engine_response.num_base_lots_out = BaseLots::ZERO; + } + Side::Ask => { + trader_state + .deposit_free_quote_lots(matching_engine_response.num_quote_lots_out); + matching_engine_response.num_quote_lots_out = QuoteLots::ZERO; + } + } + + // Check if trader has enough deposited funds to process the order + if !matching_engine_response.verify_no_deposit() { + phoenix_log!("Trader does not have enough deposited funds to process order"); + return None; + } + + // Check that the matching engine response does not withdraw any base or quote lots + if !matching_engine_response.verify_no_withdrawal() { + phoenix_log!("Matching engine response withdraws base or quote lots"); + return None; + } + } + } + Some((placed_order_id, matching_engine_response)) } @@ -1231,9 +1247,9 @@ impl< if trader_index == current_trader_index as u64 { match inflight_order.self_trade_behavior { SelfTradeBehavior::Abort => return None, - SelfTradeBehavior::CancelProvide | SelfTradeBehavior::DecrementTake => { + SelfTradeBehavior::CancelProvide => { // This block is entered if the self trade behavior for the crossing order is - // CancelProvide or DecrementTake + // CancelProvide // // We cancel the order from the book and free up the locked quote_lots or base_lots, but // we do not claim them as part of the match @@ -1246,21 +1262,46 @@ impl< false, record_event_fn, )?; - if inflight_order.self_trade_behavior == SelfTradeBehavior::DecrementTake { - // In the case that the self trade behavior is DecrementTake, we decrement the - // the base lot and adjusted quote lot budgets accordingly - inflight_order.base_lot_budget = inflight_order - .base_lot_budget - .saturating_sub(num_base_lots_quoted); - inflight_order.adjusted_quote_lot_budget = - inflight_order.adjusted_quote_lot_budget.saturating_sub( - self.tick_size_in_quote_lots_per_base_unit - * order_id.price_in_ticks - * num_base_lots_quoted, - ); - } + inflight_order.match_limit -= 1; + } + SelfTradeBehavior::DecrementTake => { + let base_lots_removed = inflight_order + .base_lot_budget + .min( + inflight_order + .adjusted_quote_lot_budget + .unchecked_div::( + order_id.price_in_ticks + * self.tick_size_in_quote_lots_per_base_unit, + ), + ) + .min(num_base_lots_quoted); + + self.reduce_order_inner( + current_trader_index, + &order_id, + inflight_order.side.opposite(), + Some(base_lots_removed), + false, + false, + record_event_fn, + )?; + // In the case that the self trade behavior is DecrementTake, we decrement the + // the base lot and adjusted quote lot budgets accordingly + inflight_order.base_lot_budget = inflight_order + .base_lot_budget + .saturating_sub(base_lots_removed); + inflight_order.adjusted_quote_lot_budget = + inflight_order.adjusted_quote_lot_budget.saturating_sub( + self.tick_size_in_quote_lots_per_base_unit + * order_id.price_in_ticks + * base_lots_removed, + ); // Self trades will count towards the match limit inflight_order.match_limit -= 1; + // If base_lots_removed < num_base_lots_quoted, then the order budget must be fully + // exhausted + inflight_order.should_terminate = base_lots_removed < num_base_lots_quoted; } } continue; diff --git a/src/state/markets/market_traits.rs b/src/state/markets/market_traits.rs index 810f855..1fc608f 100644 --- a/src/state/markets/market_traits.rs +++ b/src/state/markets/market_traits.rs @@ -43,6 +43,9 @@ pub trait OrderId { pub trait RestingOrder { fn size(&self) -> u64; + fn last_valid_slot(&self) -> Option; + fn last_valid_unix_timestamp_in_seconds(&self) -> Option; + fn is_expired(&self, current_slot: u64, current_unix_timestamp_in_seconds: u64) -> bool; } /// A wrapper around an matching algorithm implementation that allows arbitrary structs to be @@ -65,7 +68,20 @@ pub trait Market< } fn get_ladder(&self, levels: u64) -> Ladder { - let ladder = self.get_typed_ladder(levels); + self.get_ladder_with_expiration(levels, None, None) + } + + fn get_ladder_with_expiration( + &self, + levels: u64, + last_valid_slot: Option, + last_valid_unix_timestamp_in_seconds: Option, + ) -> Ladder { + let ladder = self.get_typed_ladder_with_expiration( + levels, + last_valid_slot, + last_valid_unix_timestamp_in_seconds, + ); Ladder { bids: ladder .bids @@ -87,6 +103,17 @@ pub trait Market< } fn get_typed_ladder(&self, levels: u64) -> TypedLadder { + self.get_typed_ladder_with_expiration(levels, None, None) + } + + fn get_typed_ladder_with_expiration( + &self, + levels: u64, + last_valid_slot: Option, + last_valid_unix_timestamp_in_seconds: Option, + ) -> TypedLadder { + let slot_expiration = last_valid_slot.unwrap_or_else(|| 0); + let unix_timestamp_expiration = last_valid_unix_timestamp_in_seconds.unwrap_or_else(|| 0); let mut bids = vec![]; let mut asks = vec![]; for (side, book) in [(Side::Bid, &mut bids), (Side::Ask, &mut asks)].iter_mut() { @@ -94,8 +121,12 @@ pub trait Market< &self .get_book(*side) .iter() - .map(|(order_id, resting_order)| { - (order_id.price_in_ticks(), resting_order.size()) + .filter_map(|(order_id, resting_order)| { + if resting_order.is_expired(slot_expiration, unix_timestamp_expiration) { + None + } else { + Some((order_id.price_in_ticks(), resting_order.size())) + } }) .group_by(|(price_in_ticks, _)| *price_in_ticks) .into_iter() diff --git a/src/state/markets/test_market.rs b/src/state/markets/test_market.rs index e1a7c98..336fb5c 100644 --- a/src/state/markets/test_market.rs +++ b/src/state/markets/test_market.rs @@ -676,6 +676,8 @@ fn test_limit_orders_with_self_trade() { assert!(matching_engine_response == res); let ladder = market.get_typed_ladder(1); assert!(ladder.bids[0].size_in_base_lots == BaseLots::new(5)); + + // Try to trade with DecrementTake for more than the order size let (order, matching_engine_response) = market .place_order( &taker, @@ -693,18 +695,39 @@ fn test_limit_orders_with_self_trade() { ) .unwrap(); assert!(order.is_some()); - println!("released quantities: {:?}", matching_engine_response); let mut res = MatchingEngineResponse::new_from_sell( BaseLots::new(5), Ticks::new(100) * market.tick_size_in_quote_lots_per_base_unit * BaseLots::new(5) / market.base_lots_per_base_unit, ); res.post_base_lots(BaseLots::new(5)); - println!("Matching engine response: {:?}", matching_engine_response); - println!("Res: {:?}", res); assert!(matching_engine_response == res); let ladder = market.get_typed_ladder(1); assert!(ladder.asks[0].size_in_base_lots == BaseLots::new(5)); + + // Try to trade with DecrementTake for less than the order size + let (order, matching_engine_response) = market + .place_order( + &taker, + OrderPacket::new_limit_order( + Side::Bid, + 100, + 1, + SelfTradeBehavior::DecrementTake, + None, + rng.gen::(), + false, + ), + &mut record_event_fn, + &mut get_clock_fn, + ) + .unwrap(); + + let res = MatchingEngineResponse::default(); + assert!(order.is_none()); + assert!(matching_engine_response == res); + let ladder = market.get_typed_ladder(1); + assert!(ladder.asks[0].size_in_base_lots == BaseLots::new(4)); } #[test] @@ -1403,6 +1426,126 @@ fn test_fok_and_ioc_limit_5() { ); } +#[test] +fn test_fok_and_ioc_with_free_funds() { + let mut rng = StdRng::seed_from_u64(2); + let mut market = Box::new(setup_market()); + let mut event_recorder = VecDeque::new(); + let mut record_event_fn = |e: MarketEvent| event_recorder.push_back(e); + + let trader = rng.gen::(); + let taker = rng.gen::(); + + seed_market_with_orders(&trader, &mut market, &mut record_event_fn); + + market.get_or_register_trader(&taker).unwrap(); + + let tick_size = market.tick_size_in_quote_lots_per_base_unit; + let base_lots_per_base_unit = market.base_lots_per_base_unit; + { + let trader_state = market.get_trader_state_mut(&taker).unwrap(); + trader_state.base_lots_free += BaseLots::new(29); + trader_state.quote_lots_free += + Ticks::new(103) * tick_size * BaseLots::new(1) / base_lots_per_base_unit; + } + assert!(market + .place_order( + &taker, + OrderPacket::new_fok_sell_with_limit_price( + 99, + 10, + SelfTradeBehavior::Abort, + None, + rng.gen::(), + false, + ), + &mut record_event_fn, + &mut get_clock_fn, + ) + .is_some()); + assert!(market + .place_order( + &taker, + OrderPacket::new_fok_sell_with_limit_price( + 98, + 10, + SelfTradeBehavior::Abort, + None, + rng.gen::(), + true, + ), + &mut record_event_fn, + &mut get_clock_fn, + ) + .is_some()); + assert!(market + .place_order( + &taker, + OrderPacket::new_fok_sell_with_limit_price( + 97, + 10, + SelfTradeBehavior::Abort, + None, + rng.gen::(), + true, + ), + &mut record_event_fn, + &mut get_clock_fn, + ) + .is_none()); + + assert!(market + .place_order( + &taker, + OrderPacket::new_fok_buy_with_limit_price( + 101, + 10, + SelfTradeBehavior::Abort, + None, + rng.gen::(), + false, + ), + &mut record_event_fn, + &mut get_clock_fn, + ) + .is_some()); + + assert!(market + .place_order( + &taker, + OrderPacket::new_fok_buy_with_limit_price( + 102, + 10, + SelfTradeBehavior::Abort, + None, + rng.gen::(), + true, + ), + &mut record_event_fn, + &mut get_clock_fn, + ) + .is_some()); + + let trader_state = market.get_trader_state_mut(&taker).unwrap(); + println!("trader_state: {:?}", trader_state); + + assert!(market + .place_order( + &taker, + OrderPacket::new_fok_buy_with_limit_price( + 103, + 10, + SelfTradeBehavior::Abort, + None, + rng.gen::(), + true, + ), + &mut record_event_fn, + &mut get_clock_fn, + ) + .is_none()); +} + // Base lots = (quote lots * base lots per base unit) / (tick size in quote lots per base unit * price in ticks) // Then adjust for fees. fn get_min_base_lots_out( @@ -2123,6 +2266,7 @@ fn test_tif() { assert!(matching_engine_response.num_quote_lots_out > QuoteLots::ZERO); + // Check that order are still not expired on the boundary if order_packet.get_last_valid_slot().is_some() { mock_clock.slot += 500; } else { @@ -2180,6 +2324,7 @@ fn test_tif() { // Assert that TIF kicked in assert_eq!(matching_engine_response.num_quote_lots_out, QuoteLots::ZERO); + // Verify that the events are released in the expected order for (i, event) in event_recorder.iter().enumerate() { match i { 0 => { diff --git a/src/state/matching_engine_response.rs b/src/state/matching_engine_response.rs index da87c65..b591119 100644 --- a/src/state/matching_engine_response.rs +++ b/src/state/matching_engine_response.rs @@ -94,8 +94,13 @@ impl MatchingEngineResponse { } #[inline(always)] - pub fn verify_no_deposit_or_withdrawal(&self) -> bool { + pub fn verify_no_deposit(&self) -> bool { self.num_base_lots_in + self.num_base_lots_posted == self.num_free_base_lots_used && self.num_quote_lots_in + self.num_quote_lots_posted == self.num_free_quote_lots_used } + + #[inline(always)] + pub fn verify_no_withdrawal(&self) -> bool { + self.num_base_lots_out == BaseLots::ZERO && self.num_quote_lots_out == QuoteLots::ZERO + } } From 5b8faa38afa53686b4251650c2220481daa68cf7 Mon Sep 17 00:00:00 2001 From: Rahul Jain Date: Mon, 13 Mar 2023 15:57:43 -0400 Subject: [PATCH 3/4] update tests for SDK changes as active market key is now an option --- tests/test_phoenix.rs | 200 +++++++++++++++++++++--------------------- 1 file changed, 100 insertions(+), 100 deletions(-) diff --git a/tests/test_phoenix.rs b/tests/test_phoenix.rs index ea89ad3..ad27ce6 100644 --- a/tests/test_phoenix.rs +++ b/tests/test_phoenix.rs @@ -275,13 +275,13 @@ async fn get_new_maker(sdk: &SDKClient, context: &PhoenixTestContext) -> Phoenix vec![ system_instruction::transfer( &sdk.client.payer.pubkey(), - &get_seat_address(&sdk.active_market_key, &maker.user.pubkey()).0, + &get_seat_address(&sdk.active_market_key.unwrap(), &maker.user.pubkey()).0, 5000, ), create_request_seat_authorized_instruction( &sdk.client.payer.pubkey(), &sdk.client.payer.pubkey(), - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &maker.user.pubkey(), ), ], @@ -294,7 +294,7 @@ async fn get_new_maker(sdk: &SDKClient, context: &PhoenixTestContext) -> Phoenix .sign_send_instructions( vec![create_change_seat_status_instruction( &sdk.client.payer.pubkey(), - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &maker.user.pubkey(), SeatApprovalStatus::Approved, )], @@ -316,7 +316,7 @@ async fn test_phoenix_request_seats() { let quote_mint = &meta.quote_mint; let base_mint = &meta.base_mint; - let market = &sdk.core.active_market_key; + let market = &sdk.core.active_market_key.unwrap(); // Don't use the default_maker since we are testing the request_seats instruction let maker = Keypair::new(); airdrop(&sdk.client, &maker.pubkey(), sol(20.0)) @@ -529,7 +529,7 @@ async fn test_phoenix_orders() { OrderPacket::new_limit_order_default(Side::Bid, sdk.float_price_to_ticks(100.0), 1); orders.push(create_new_order_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &default_maker.user.pubkey(), base_mint, quote_mint, @@ -540,7 +540,7 @@ async fn test_phoenix_orders() { OrderPacket::new_limit_order_default(Side::Bid, sdk.float_price_to_ticks(99.0), 1); orders.push(create_new_order_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &default_maker.user.pubkey(), base_mint, quote_mint, @@ -550,7 +550,7 @@ async fn test_phoenix_orders() { let limit_order = OrderPacket::new_limit_order_default(Side::Ask, sdk.float_price_to_ticks(101.0), 1); orders.push(create_new_order_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &default_maker.user.pubkey(), base_mint, quote_mint, @@ -561,7 +561,7 @@ async fn test_phoenix_orders() { let limit_order = OrderPacket::new_limit_order_default(Side::Ask, sdk.float_price_to_ticks(102.0), 1); orders.push(create_new_order_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &default_maker.user.pubkey(), base_mint, quote_mint, @@ -574,7 +574,7 @@ async fn test_phoenix_orders() { .client .sign_send_instructions( vec![create_new_order_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &default_maker.user.pubkey(), base_mint, quote_mint, @@ -591,7 +591,7 @@ async fn test_phoenix_orders() { .client .sign_send_instructions( vec![create_new_order_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &default_maker.user.pubkey(), base_mint, quote_mint, @@ -611,7 +611,7 @@ async fn test_phoenix_orders() { sdk.client .sign_send_instructions( vec![create_cancel_multiple_orders_by_id_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &default_maker.user.pubkey(), base_mint, quote_mint, @@ -635,7 +635,7 @@ async fn test_phoenix_orders() { assert_eq!(base_start, 999999998000000); assert_eq!(quote_start, 999999801000); - let sequence_number = get_sequence_number(&sdk.client, &sdk.core.active_market_key).await; + let sequence_number = get_sequence_number(&sdk.client, &sdk.core.active_market_key.unwrap()).await; let cancel_orders = vec![ CancelOrderParams { @@ -653,7 +653,7 @@ async fn test_phoenix_orders() { sdk.client .sign_send_instructions( vec![create_cancel_multiple_orders_by_id_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &default_maker.user.pubkey(), base_mint, quote_mint, @@ -668,7 +668,7 @@ async fn test_phoenix_orders() { let mut base_end = get_token_balance(&sdk.client, default_maker.base_ata).await; let mut quote_end = get_token_balance(&sdk.client, default_maker.quote_ata).await; - let new_sequence_number = get_sequence_number(&sdk.client, &sdk.core.active_market_key).await; + let new_sequence_number = get_sequence_number(&sdk.client, &sdk.core.active_market_key.unwrap()).await; // maker receives base tokens assert_eq!(base_end, 1000000000000000); @@ -681,7 +681,7 @@ async fn test_phoenix_orders() { sdk.client .sign_send_instructions( vec![create_cancel_multiple_orders_by_id_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &default_maker.user.pubkey(), base_mint, quote_mint, @@ -715,7 +715,7 @@ async fn test_phoenix_orders() { sdk.client .sign_send_instructions( vec![create_cancel_multiple_orders_by_id_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &default_maker.user.pubkey(), base_mint, quote_mint, @@ -774,7 +774,7 @@ async fn test_phoenix_cancel_all_orders() { let limit_order = OrderPacket::new_limit_order_default(Side::Bid, sdk.float_price_to_ticks(100.0), 1); orders.push(create_new_order_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &default_maker.user.pubkey(), base_mint, quote_mint, @@ -785,7 +785,7 @@ async fn test_phoenix_cancel_all_orders() { OrderPacket::new_limit_order_default(Side::Bid, sdk.float_price_to_ticks(99.0), 1); orders.push(create_new_order_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &default_maker.user.pubkey(), base_mint, quote_mint, @@ -795,7 +795,7 @@ async fn test_phoenix_cancel_all_orders() { let limit_order = OrderPacket::new_limit_order_default(Side::Ask, sdk.float_price_to_ticks(101.0), 1); orders.push(create_new_order_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &default_maker.user.pubkey(), base_mint, quote_mint, @@ -806,7 +806,7 @@ async fn test_phoenix_cancel_all_orders() { let limit_order = OrderPacket::new_limit_order_default(Side::Ask, sdk.float_price_to_ticks(102.0), 1); orders.push(create_new_order_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &default_maker.user.pubkey(), base_mint, quote_mint, @@ -814,12 +814,12 @@ async fn test_phoenix_cancel_all_orders() { )); sdk.client.set_payer(&payer_key).unwrap(); - let sequence_number = get_sequence_number(&sdk.client, &sdk.core.active_market_key).await; + let sequence_number = get_sequence_number(&sdk.client, &sdk.core.active_market_key.unwrap()).await; sdk.client .sign_send_instructions( vec![create_cancel_all_orders_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &default_maker.user.pubkey(), base_mint, quote_mint, @@ -831,7 +831,7 @@ async fn test_phoenix_cancel_all_orders() { let base_end = get_token_balance(&sdk.client, default_maker.base_ata).await; let quote_end = get_token_balance(&sdk.client, default_maker.quote_ata).await; - let new_sequence_number = get_sequence_number(&sdk.client, &sdk.core.active_market_key).await; + let new_sequence_number = get_sequence_number(&sdk.client, &sdk.core.active_market_key.unwrap()).await; // maker receives base tokens assert_eq!(base_end, 1000000000000000); @@ -857,14 +857,14 @@ async fn test_phoenix_admin() { let meta = *sdk.get_active_market_metadata(); let quote_mint = &meta.quote_mint; let base_mint = &meta.base_mint; - let market = sdk.active_market_key; + let market = sdk.active_market_key.unwrap(); let mut orders = vec![]; let payer_key = sdk.client.payer.pubkey(); sdk.set_payer(clone_keypair(&default_maker.user)); orders.push(create_new_order_instruction( - &sdk.active_market_key, + &sdk.active_market_key.unwrap(), &default_maker.user.pubkey(), base_mint, quote_mint, @@ -872,7 +872,7 @@ async fn test_phoenix_admin() { )); // Place a bid at 99 orders.push(create_new_order_instruction( - &sdk.active_market_key, + &sdk.active_market_key.unwrap(), &default_maker.user.pubkey(), base_mint, quote_mint, @@ -880,7 +880,7 @@ async fn test_phoenix_admin() { )); // Place an ask at 101 orders.push(create_new_order_instruction( - &sdk.active_market_key, + &sdk.active_market_key.unwrap(), &default_maker.user.pubkey(), base_mint, quote_mint, @@ -889,7 +889,7 @@ async fn test_phoenix_admin() { // Place an ask at 102 orders.push(create_new_order_instruction( - &sdk.active_market_key, + &sdk.active_market_key.unwrap(), &default_maker.user.pubkey(), base_mint, quote_mint, @@ -987,7 +987,7 @@ async fn test_phoenix_admin() { sdk.client .sign_send_instructions( vec![create_new_order_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &default_taker.user.pubkey(), base_mint, quote_mint, @@ -1005,7 +1005,7 @@ async fn test_phoenix_admin() { .sign_send_instructions( vec![create_change_market_status_instruction( &successor.pubkey(), - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), MarketStatus::Closed )], vec![&successor], @@ -1020,7 +1020,7 @@ async fn test_phoenix_admin() { .sign_send_instructions( vec![create_change_market_status_instruction( &admin.pubkey(), - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), MarketStatus::Paused )], vec![&admin], @@ -1035,7 +1035,7 @@ async fn test_phoenix_admin() { .sign_send_instructions( vec![create_change_market_status_instruction( &successor.pubkey(), - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), MarketStatus::Paused )], vec![&successor], @@ -1049,7 +1049,7 @@ async fn test_phoenix_admin() { sdk.client .sign_send_instructions( vec![create_cancel_up_to_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &default_maker.user.pubkey(), base_mint, quote_mint, @@ -1071,7 +1071,7 @@ async fn test_phoenix_admin() { .sign_send_instructions( vec![create_change_market_status_instruction( &successor.pubkey(), - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), MarketStatus::Active )], vec![&successor], @@ -1085,7 +1085,7 @@ async fn test_phoenix_admin() { .sign_send_instructions( vec![create_change_market_status_instruction( &successor.pubkey(), - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), MarketStatus::Paused )], vec![&successor], @@ -1102,7 +1102,7 @@ async fn test_phoenix_admin() { sdk.client .sign_send_instructions( vec![create_new_order_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &default_taker.user.pubkey(), base_mint, quote_mint, @@ -1119,7 +1119,7 @@ async fn test_phoenix_admin() { .sign_send_instructions( vec![create_change_market_status_instruction( &successor.pubkey(), - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), MarketStatus::Closed, )], vec![&successor], @@ -1133,7 +1133,7 @@ async fn test_phoenix_admin() { .sign_send_instructions( vec![create_change_market_status_instruction( &successor.pubkey(), - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), MarketStatus::Tombstoned, )], vec![&successor], @@ -1148,7 +1148,7 @@ async fn test_phoenix_admin() { .sign_send_instructions( vec![ create_cancel_up_to_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &default_maker.user.pubkey(), base_mint, quote_mint, @@ -1160,7 +1160,7 @@ async fn test_phoenix_admin() { }, ), create_cancel_up_to_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &default_maker.user.pubkey(), base_mint, quote_mint, @@ -1187,7 +1187,7 @@ async fn test_phoenix_admin() { // call withdraw create_evict_seat_instruction( &successor.pubkey(), - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &default_maker.user.pubkey(), base_mint, quote_mint, @@ -1204,7 +1204,7 @@ async fn test_phoenix_admin() { .sign_send_instructions( vec![create_change_seat_status_instruction( &successor.pubkey(), - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &default_maker.user.pubkey(), SeatApprovalStatus::NotApproved, )], @@ -1221,7 +1221,7 @@ async fn test_phoenix_admin() { // call withdraw create_evict_seat_instruction( &successor.pubkey(), - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &default_maker.user.pubkey(), base_mint, quote_mint, @@ -1239,7 +1239,7 @@ async fn test_phoenix_admin() { .sign_send_instructions( vec![create_change_market_status_instruction( &successor.pubkey(), - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), MarketStatus::Tombstoned )], vec![&successor], @@ -1253,7 +1253,7 @@ async fn test_phoenix_admin() { sdk.client .sign_send_instructions( vec![create_collect_fees_instruction_default( - &sdk.active_market_key, + &sdk.active_market_key.unwrap(), &sdk.client.payer.pubkey(), &sdk.client.payer.pubkey(), // Fee collector is the market creator in this case quote_mint, @@ -1267,7 +1267,7 @@ async fn test_phoenix_admin() { .sign_send_instructions( vec![create_change_market_status_instruction( &successor.pubkey(), - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), MarketStatus::Tombstoned, )], vec![&successor], @@ -1327,7 +1327,7 @@ async fn test_phoenix_basic() { let quote_start = get_token_balance(&sdk.client, default_taker.quote_ata).await; let new_order_ix = create_new_order_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &default_taker.user.pubkey(), base_mint, quote_mint, @@ -1350,7 +1350,7 @@ async fn test_phoenix_basic() { let quote_start = get_token_balance(&sdk.client, default_maker.quote_ata).await; let withdraw_ix = create_withdraw_funds_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &default_maker.user.pubkey(), base_mint, quote_mint, @@ -1372,7 +1372,7 @@ async fn test_phoenix_basic() { }; let cancel_multiple_ix = create_cancel_up_to_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &default_maker.user.pubkey(), base_mint, quote_mint, @@ -1413,7 +1413,7 @@ async fn test_phoenix_basic() { let quote_after_cancel = get_token_balance(&sdk.client, default_maker.quote_ata).await; assert!(quote_after_cancel == 1_000_000_000_000 - 398750000); let deposit_ix = create_deposit_funds_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &default_maker.user.pubkey(), base_mint, quote_mint, @@ -1439,7 +1439,7 @@ async fn test_phoenix_basic() { let base_before_withdraw = base_after_deposit; let quote_before_withdraw = quote_after_deposit; let withdraw_ix = create_withdraw_funds_with_custom_amounts_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &default_maker.user.pubkey(), base_mint, quote_mint, @@ -1480,7 +1480,7 @@ async fn test_phoenix_fees() { let limit_order = OrderPacket::new_limit_order_default(Side::Bid, sdk.float_price_to_ticks(100.0), 1000); let make_ix = create_new_order_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &default_maker.user.pubkey(), base_mint, quote_mint, @@ -1502,7 +1502,7 @@ async fn test_phoenix_fees() { false, ); let take_ix = create_new_order_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &default_taker.user.pubkey(), base_mint, quote_mint, @@ -1525,7 +1525,7 @@ async fn test_phoenix_fees() { let change_fee_recipient_ix = create_change_fee_recipient_instruction( &admin.pubkey(), - &sdk.active_market_key, + &sdk.active_market_key.unwrap(), &new_fee_recipient.user.pubkey(), ); @@ -1539,7 +1539,7 @@ async fn test_phoenix_fees() { let change_fee_recipient_ix = create_change_fee_recipient_with_unclaimed_fees_instruction( &admin.pubkey(), - &sdk.active_market_key, + &sdk.active_market_key.unwrap(), &new_fee_recipient.user.pubkey(), &admin.pubkey(), ); @@ -1553,14 +1553,14 @@ async fn test_phoenix_fees() { ); let collect_fees_ix = create_collect_fees_instruction_default( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &admin.pubkey(), &new_fee_recipient.user.pubkey(), quote_mint, ); let fee_ata = get_associated_token_address(&new_fee_recipient.user.pubkey(), quote_mint); let fee_dest_start = get_token_balance(&sdk.client, fee_ata).await; - let quote_vault = get_vault_address(&sdk.core.active_market_key, quote_mint).0; + let quote_vault = get_vault_address(&sdk.core.active_market_key.unwrap(), quote_mint).0; let quote_balance_start = get_token_balance(&sdk.client, quote_vault).await; sdk.client @@ -1579,7 +1579,7 @@ async fn test_phoenix_fees() { assert_eq!(quote_balance_end, 0); assert_eq!(fee_dest_balance - fee_dest_start, 50000); - let market_account_data = (sdk.client.get_account_data(&sdk.core.active_market_key)) + let market_account_data = (sdk.client.get_account_data(&sdk.core.active_market_key.unwrap())) .await .unwrap(); let (header_bytes, bytes) = market_account_data.split_at(size_of::()); @@ -1598,7 +1598,7 @@ async fn test_phoenix_fees() { let change_fee_recipient_ix = create_change_fee_recipient_instruction( &admin.pubkey(), - &sdk.active_market_key, + &sdk.active_market_key.unwrap(), &Keypair::new().pubkey(), ); @@ -1616,7 +1616,7 @@ async fn test_phoenix_cancel_with_free_funds() { let (PhoenixTestClient { mut sdk, .. }, ctx) = bootstrap_default(0).await; let PhoenixTestContext { default_maker, .. } = &ctx; let meta = *sdk.get_active_market_metadata(); - let market = sdk.active_market_key; + let market = sdk.active_market_key.unwrap(); sdk.client.set_payer(&default_maker.user.pubkey()).unwrap(); let quote_lots_to_deposit = sdk.quote_units_to_quote_lots(10000.0); let base_lots_to_deposit = sdk.raw_base_units_to_base_lots(100.0); @@ -1832,7 +1832,7 @@ async fn test_phoenix_orders_with_free_funds() { ); let new_order_ix = create_new_order_with_free_funds_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &default_taker.user.pubkey(), &sell_params, ); @@ -1889,7 +1889,7 @@ async fn test_phoenix_orders_with_free_funds() { for param in taker_params { let new_order_ix = create_new_order_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &default_taker.user.pubkey(), base_mint, quote_mint, @@ -1903,7 +1903,7 @@ async fn test_phoenix_orders_with_free_funds() { for param in maker_params { let new_order_ix = create_new_order_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &default_maker.user.pubkey(), base_mint, quote_mint, @@ -1927,7 +1927,7 @@ async fn test_phoenix_orders_with_free_funds() { //Attempt to send a LimitOrderWithFreeFunds with the second maker that will fail due to insufficient funds sdk.client.payer = clone_keypair(&second_maker.user); let new_order_ix = create_new_order_with_free_funds_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &second_maker.user.pubkey(), &OrderPacket::new_post_only_default( Side::Bid, @@ -1977,7 +1977,7 @@ async fn test_phoenix_orders_with_free_funds() { let maker_ioc_params = vec![ioc_buy_params, ioc_sell_params]; for param in second_maker_params { let new_order_ix = create_new_order_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &second_maker.user.pubkey(), base_mint, quote_mint, @@ -1992,7 +1992,7 @@ async fn test_phoenix_orders_with_free_funds() { sdk.set_payer(clone_keypair(&default_maker.user)); for param in maker_ioc_params { let new_order_ix = create_new_order_with_free_funds_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &default_maker.user.pubkey(), ¶m, ); @@ -2025,7 +2025,7 @@ async fn test_phoenix_orders_with_free_funds() { for param in maker_params { let new_order_ix = create_new_order_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &default_maker.user.pubkey(), base_mint, quote_mint, @@ -2050,7 +2050,7 @@ async fn test_phoenix_orders_with_free_funds() { let second_maker_quote_balance_start = get_token_balance(&sdk.client, second_maker.quote_ata).await; let new_order_ix = create_new_order_with_free_funds_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &second_maker.user.pubkey(), &OrderPacket::new_ioc_by_lots( Side::Bid, @@ -2091,7 +2091,7 @@ async fn test_phoenix_orders_with_free_funds() { for params in [limit_buy_params, limit_sell_params] { let new_order_ix = create_new_order_with_free_funds_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &second_maker.user.pubkey(), ¶ms, ); @@ -2137,7 +2137,7 @@ async fn test_phoenix_orders_with_free_funds() { for params in [limit_buy_params, limit_sell_params] { let new_order_ix = create_new_order_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &second_maker.user.pubkey(), base_mint, quote_mint, @@ -2163,7 +2163,7 @@ async fn test_phoenix_orders_with_free_funds() { // Cancel all to free up some funds let cancel_all_ix = create_cancel_all_order_with_free_funds_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &second_maker.user.pubkey(), ); @@ -2195,7 +2195,7 @@ async fn test_phoenix_orders_with_free_funds() { //Check that sending an orderpacket with free funds set to true fails if we send via the wrong instruction type for params in [limit_buy_params, limit_sell_params] { let new_order_ix = create_new_order_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &second_maker.user.pubkey(), base_mint, quote_mint, @@ -2211,7 +2211,7 @@ async fn test_phoenix_orders_with_free_funds() { // Free funds order packet succeeds with correct instruction type for params in [limit_buy_params, limit_sell_params] { let new_order_ix = create_new_order_with_free_funds_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &second_maker.user.pubkey(), ¶ms, ); @@ -2244,7 +2244,7 @@ async fn test_phoenix_orders_with_free_funds() { // Order packet with free funds set to false fails if we send via the free funds instruction type for params in [limit_buy_params, limit_sell_params] { let new_order_ix = create_new_order_with_free_funds_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &second_maker.user.pubkey(), ¶ms, ); @@ -2311,7 +2311,7 @@ async fn test_phoenix_place_multiple_limit_orders() { ); let new_order_ix = create_new_multiple_order_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &default_maker.user.pubkey(), base_mint, quote_mint, @@ -2329,7 +2329,7 @@ async fn test_phoenix_place_multiple_limit_orders() { assert_eq!(quote_balance_start - quote_balance_end, 170000000); let cancel_order_ix = create_cancel_all_order_with_free_funds_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &default_maker.user.pubkey(), ); @@ -2367,7 +2367,7 @@ async fn test_phoenix_place_multiple_limit_orders() { ); let new_order_ix = create_new_multiple_order_with_free_funds_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &default_maker.user.pubkey(), &multiple_order_packet, ); @@ -2418,7 +2418,7 @@ async fn test_phoenix_place_multiple_limit_orders() { ); let new_order_ix = create_new_multiple_order_with_free_funds_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &default_maker.user.pubkey(), &multiple_order_packet, ); @@ -2467,7 +2467,7 @@ async fn test_phoenix_place_multiple_limit_orders() { ], ); let new_order_ix = create_new_multiple_order_with_free_funds_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &default_maker.user.pubkey(), &multiple_order_packet, ); @@ -2491,7 +2491,7 @@ async fn test_phoenix_place_multiple_limit_orders() { // Cancel orders to return the orderbook to empty let cancel_order_ix = create_cancel_all_order_with_free_funds_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &default_maker.user.pubkey(), ); @@ -2525,7 +2525,7 @@ async fn test_phoenix_place_multiple_limit_orders() { ); let new_order_ix = create_new_multiple_order_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &default_maker.user.pubkey(), base_mint, quote_mint, @@ -2567,7 +2567,7 @@ async fn test_phoenix_place_multiple_limit_orders() { ); let new_order_ix = create_new_multiple_order_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &default_maker.user.pubkey(), base_mint, quote_mint, @@ -2603,7 +2603,7 @@ async fn test_phoenix_place_multiple_limit_orders() { for params in [limit_buy_params, limit_sell_params] { let new_order_ix = create_new_order_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &second_maker.user.pubkey(), base_mint, quote_mint, @@ -2640,7 +2640,7 @@ async fn test_phoenix_place_multiple_limit_orders() { ); let new_order_ix = create_new_multiple_order_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &default_maker.user.pubkey(), base_mint, quote_mint, @@ -2668,7 +2668,7 @@ async fn test_phoenix_place_multiple_limit_orders() { ); let new_order_ix = create_new_multiple_order_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &default_maker.user.pubkey(), base_mint, quote_mint, @@ -2733,7 +2733,7 @@ async fn test_phoenix_place_multiple_limit_orders() { ); let new_order_ix = create_new_multiple_order_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &default_maker.user.pubkey(), base_mint, quote_mint, @@ -2758,7 +2758,7 @@ async fn test_phoenix_place_multiple_limit_orders() { // Cancel orders for both makers to return the orderbook to empty let cancel_order_ix = create_cancel_all_order_with_free_funds_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &default_maker.user.pubkey(), ); @@ -2768,7 +2768,7 @@ async fn test_phoenix_place_multiple_limit_orders() { .unwrap(); let cancel_order_ix = create_cancel_all_order_with_free_funds_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &second_maker.user.pubkey(), ); @@ -2798,7 +2798,7 @@ async fn test_phoenix_place_multiple_limit_orders() { let multiple_order_packet = MultipleOrderPacket::new_default(bids, asks); let new_order_ix = create_new_multiple_order_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &default_maker.user.pubkey(), base_mint, quote_mint, @@ -2835,7 +2835,7 @@ async fn test_phoenix_place_multiple_limit_orders() { .collect::>(); let new_order_ix = create_new_multiple_order_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &second_maker.user.pubkey(), base_mint, quote_mint, @@ -2867,7 +2867,7 @@ async fn test_phoenix_place_multiple_limit_orders() { .collect::>(); let new_order_ix = create_new_multiple_order_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &second_maker.user.pubkey(), base_mint, quote_mint, @@ -2922,7 +2922,7 @@ async fn layer_orders( for (p, s) in prices.iter().zip(sizes.iter()) { let params = OrderPacket::new_limit_order_default(side, *p, *s); let new_order_ix = create_new_order_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &sdk.get_trader(), &meta.base_mint, &meta.quote_mint, @@ -3062,7 +3062,7 @@ async fn test_phoenix_place_multiple_memory_management() { let multiple_order_packet = MultipleOrderPacket::new_default(bids, asks); let new_order_ix = create_new_multiple_order_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &default_maker.user.pubkey(), &sdk.base_mint, &sdk.quote_mint, @@ -3085,7 +3085,7 @@ async fn test_phoenix_place_multiple_memory_management() { vec![ ComputeBudgetInstruction::set_compute_unit_limit(1_400_000), create_new_order_instruction( - &sdk.active_market_key, + &sdk.active_market_key.unwrap(), &default_taker.user.pubkey(), &sdk.base_mint, &sdk.quote_mint, @@ -3136,7 +3136,7 @@ async fn test_phoenix_place_multiple_limit_orders_adversarial() { let multiple_order_packet = MultipleOrderPacket::new_default(bids, asks); let new_order_ix = create_new_multiple_order_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &default_maker.user.pubkey(), base_mint, quote_mint, @@ -3172,7 +3172,7 @@ async fn test_phoenix_place_multiple_limit_orders_adversarial() { false, ); let ix = create_new_order_instruction( - &sdk.active_market_key, + &sdk.active_market_key.unwrap(), &default_taker.user.pubkey(), &sdk.base_mint, &sdk.quote_mint, @@ -3265,7 +3265,7 @@ async fn test_phoenix_basic_with_raw_base_unit_adjustment() { ); let bid_ix = create_new_order_instruction( - &sdk.active_market_key, + &sdk.active_market_key.unwrap(), &default_maker.user.pubkey(), base_mint, quote_mint, @@ -3273,7 +3273,7 @@ async fn test_phoenix_basic_with_raw_base_unit_adjustment() { ); let ask_ix = create_new_order_instruction( - &sdk.active_market_key, + &sdk.active_market_key.unwrap(), &default_maker.user.pubkey(), base_mint, quote_mint, @@ -3317,7 +3317,7 @@ async fn test_phoenix_basic_with_raw_base_unit_adjustment() { ); let new_order_ix = create_new_order_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &default_taker.user.pubkey(), base_mint, quote_mint, @@ -3347,7 +3347,7 @@ async fn test_phoenix_basic_with_raw_base_unit_adjustment() { let quote_start = get_token_balance(&sdk.client, default_maker.quote_ata).await; let withdraw_ix = create_withdraw_funds_instruction( - &sdk.core.active_market_key, + &sdk.core.active_market_key.unwrap(), &default_maker.user.pubkey(), base_mint, quote_mint, From 69578d9963326ac60ba70e3f115dde9f77939f5b Mon Sep 17 00:00:00 2001 From: Rahul Jain Date: Tue, 14 Mar 2023 13:30:57 -0400 Subject: [PATCH 4/4] update sdk --- Cargo.lock | 104 +++++++++++++++++------------------------------------ Cargo.toml | 2 +- 2 files changed, 34 insertions(+), 72 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3e3e5b7..6cb36f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2099,7 +2099,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a53d79ba8304ac1c4f9eb3b9d281f21f7be9d4626f72ce7df4ad8fbde4f38a74" dependencies = [ "modular-bitfield-impl", - "static_assertions 1.1.0", + "static_assertions", ] [[package]] @@ -2477,30 +2477,10 @@ dependencies = [ "num", ] -[[package]] -name = "phoenix" -version = "0.1.0" -source = "git+https://github.com/Ellipsis-Labs/phoenix-v1?rev=640bcdf#640bcdf553e9513fb3dcae4adc27806e2b97e4f8" -dependencies = [ - "borsh 0.9.3", - "bytemuck", - "ellipsis-macros", - "itertools", - "lib-sokoban", - "num_enum", - "shank 0.0.9", - "solana-program", - "solana-security-txt", - "spl-associated-token-account", - "spl-token", - "static_assertions 0.1.1", - "thiserror", -] - [[package]] name = "phoenix-sdk" version = "0.1.0" -source = "git+https://github.com/Ellipsis-Labs/phoenix-sdk?rev=adb01a3#adb01a3780f61ad7ac281a3797864a8194df7266" +source = "git+https://github.com/Ellipsis-Labs/phoenix-sdk?branch=update-rust-sdk#fce13e4d7ea9e7b6eb51f1e7b36b0257fd929b5b" dependencies = [ "anyhow", "async-trait", @@ -2512,11 +2492,12 @@ dependencies = [ "futures 0.3.26", "itertools", "num-traits", - "phoenix", "phoenix-sdk-core", + "phoenix-v1 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.7.3", "rust_decimal", "rust_decimal_macros", + "serde", "solana-client", "solana-program", "solana-sdk", @@ -2528,13 +2509,13 @@ dependencies = [ [[package]] name = "phoenix-sdk-core" version = "0.1.0" -source = "git+https://github.com/Ellipsis-Labs/phoenix-sdk?rev=adb01a3#adb01a3780f61ad7ac281a3797864a8194df7266" +source = "git+https://github.com/Ellipsis-Labs/phoenix-sdk?branch=update-rust-sdk#fce13e4d7ea9e7b6eb51f1e7b36b0257fd929b5b" dependencies = [ "anyhow", "borsh 0.9.3", "itertools", "num-traits", - "phoenix", + "phoenix-v1 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.7.3", "rust_decimal", "rust_decimal_macros", @@ -2555,17 +2536,38 @@ dependencies = [ "num_enum", "phoenix-sdk", "rand 0.7.3", - "shank 0.0.12", + "shank", "solana-program", "solana-sdk", "solana-security-txt", "spl-associated-token-account", "spl-token", - "static_assertions 1.1.0", + "static_assertions", "thiserror", "tokio", ] +[[package]] +name = "phoenix-v1" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64366b3a050af1ad0511ed9f247851bbfeb92ad8a6305e99e2f40f3405136b98" +dependencies = [ + "borsh 0.9.3", + "bytemuck", + "ellipsis-macros", + "itertools", + "lib-sokoban", + "num_enum", + "shank", + "solana-program", + "solana-security-txt", + "spl-associated-token-account", + "spl-token", + "static_assertions", + "thiserror", +] + [[package]] name = "pin-project" version = "1.0.12" @@ -3403,34 +3405,13 @@ dependencies = [ "keccak", ] -[[package]] -name = "shank" -version = "0.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b54c657cbe18aaff6d5042a4d48f643fdd2a826dfc7161de98ee28e5bd7e85e0" -dependencies = [ - "shank_macro 0.0.9", -] - [[package]] name = "shank" version = "0.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439c00542aa8b4c777750b3130ce36fcff86ba215d54006d47d67359513b70be" dependencies = [ - "shank_macro 0.0.12", -] - -[[package]] -name = "shank_macro" -version = "0.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b43a02ae007b64b177f4dbb21d322f276458e4860f9fefbd8b9b791c21644ab" -dependencies = [ - "proc-macro2 1.0.51", - "quote 1.0.23", - "shank_macro_impl 0.0.9", - "syn 1.0.107", + "shank_macro", ] [[package]] @@ -3441,24 +3422,11 @@ checksum = "3498d6ea2ba012f26ad3d79a19773ba8e1c7a69f14dec67e3ed51c723cc9f30a" dependencies = [ "proc-macro2 1.0.51", "quote 1.0.23", - "shank_macro_impl 0.0.12", + "shank_macro_impl", "shank_render", "syn 1.0.107", ] -[[package]] -name = "shank_macro_impl" -version = "0.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d36cdf68202db080a13ef0300c369fc691695265e3d0ab0fa08d4734b0cfb66" -dependencies = [ - "anyhow", - "proc-macro2 1.0.51", - "quote 1.0.23", - "serde", - "syn 1.0.107", -] - [[package]] name = "shank_macro_impl" version = "0.0.12" @@ -3480,7 +3448,7 @@ checksum = "142e11124c70d1702424011209621551adf775988033dedea428ce4a21d3acdf" dependencies = [ "proc-macro2 1.0.51", "quote 1.0.23", - "shank_macro_impl 0.0.12", + "shank_macro_impl", ] [[package]] @@ -4418,12 +4386,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "static_assertions" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f406d6ee68db6796e11ffd7b4d171864c58b7451e79ef9460ea33c287a1f89a7" - [[package]] name = "static_assertions" version = "1.1.0" @@ -4535,7 +4497,7 @@ dependencies = [ "pin-project", "rand 0.8.5", "serde", - "static_assertions 1.1.0", + "static_assertions", "tarpc-plugins", "thiserror", "tokio", diff --git a/Cargo.toml b/Cargo.toml index ab4835b..5f584ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,4 +45,4 @@ rand = "0.7.3" ellipsis-client = "0.1.15" tokio = { version = "1.8.4", features = ["full"] } solana-sdk = "=1.14.9" -phoenix-sdk = { git = "https://github.com/Ellipsis-Labs/phoenix-sdk", rev = "adb01a3" } +phoenix-sdk = { git = "https://github.com/Ellipsis-Labs/phoenix-sdk", branch = "update-rust-sdk" }