From 0c4a4baf4c74b19c0dd1820dce10bf6b1e047fcf Mon Sep 17 00:00:00 2001 From: lil perp Date: Mon, 20 Feb 2023 15:13:31 -0500 Subject: [PATCH] program: limit order auctions (#355) * new is_amm_available_liquidity_source * tweak how trigger orders work * tweak get_limit_price to allow limit orders to have auction price * allow limit order to pass auction params * do order validation * make the resting limit order logic based on auction being complete * dlob trigger order matches on chain * ts client reflects price changes * fix ts dlob tests * add rust tests * add failing test * prototype of updateRestingLimitOrders * address pr feedback * program: tweak calculate_size_premium_liability_weight to have smaller effect on initial margin (#350) * bigz/improve-calculate_size_premium_liability_weight * fmt fix * margin.ts: sync w/ contract * CHANGELOG --------- Co-authored-by: Chris Heaney * v2.16.0-beta.2 * sdk: fix borrow limit calc (#356) * update CHANGELOG.md * sdk: new squareRootBN implementation using bit shifting * sdk: change modify order params to be object (#353) * sdk: change modify order params to be object * Update CHANGELOG.md * sdk: DLOB matching logic accounts for zero-price spot market orders not matching resting limit orders * v2.16.0-beta.3 * sdk: add market lookup table (#359) * sdk: add look up table to config * add ability to send version tx with retry sender * tweak LOOK UP to LOOKUP * add fetchMarketLookupTableAccount to driftClient * CHANGELOG * v2.16.0 * getMarketBids/Asks becomes getTakingBids/Asks * remove updateRestingLimitOrder from getbestNode * tests working * update isFallbackAvailableLiquiditySource * tweak is_maker_for_taker * tweaks * fix updateRestingLimitOrders * add test for updating resting limit ordres * add tests for is_maker_for_taker * fix broken ts test * tweak some syntax choices * simplify is_maker_for_taker * dont let auction prices be zero for market or limit orders * tweak findJitAuctionNodesToFill * CHANGELOG --------- Co-authored-by: bigzPubkey Co-authored-by: wphan Co-authored-by: Evan Pipta <3pipta@gmail.com> Co-authored-by: 0xbigz <83473873+0xbigz@users.noreply.github.com> --- CHANGELOG.md | 2 + programs/drift/src/controller/orders.rs | 95 +-- .../src/controller/orders/amm_jit_tests.rs | 12 + programs/drift/src/controller/orders/tests.rs | 286 +++++++- programs/drift/src/math/auction.rs | 10 +- programs/drift/src/math/fulfillment.rs | 5 +- programs/drift/src/math/fulfillment/tests.rs | 11 + programs/drift/src/math/matching.rs | 22 +- programs/drift/src/math/matching/tests.rs | 172 +++++ programs/drift/src/state/user.rs | 12 +- programs/drift/src/validation/order.rs | 107 ++- sdk/src/dlob/DLOB.ts | 523 +++++++++---- sdk/src/dlob/DLOBNode.ts | 27 +- sdk/src/examples/loadDlob.ts | 2 +- sdk/src/math/auction.ts | 14 +- sdk/src/math/orders.ts | 15 +- sdk/tests/dlob/test.ts | 688 ++++++++++++++++-- tests/liquidateBorrowForPerpPnl.ts | 2 +- 18 files changed, 1674 insertions(+), 331 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e1170e197..a50907b04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Features + +- program: allow limit orders to go through auction ([#355](https://github.com/drift-labs/protocol-v2/pull/355)) - program: improve conditions for withdraw/borrow guard ([#354](https://github.com/drift-labs/protocol-v2/pull/354)) ### Fixes diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index f939b842c..27c574813 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -1,5 +1,4 @@ use std::cell::RefMut; -use std::cmp::max; use std::num::NonZeroU64; use std::ops::DerefMut; @@ -29,7 +28,7 @@ use crate::get_struct_values; use crate::get_then_update_id; use crate::instructions::OrderParams; use crate::load_mut; -use crate::math::auction::{calculate_auction_prices, is_auction_complete}; +use crate::math::auction::{calculate_auction_prices, is_amm_available_liquidity_source}; use crate::math::casting::Cast; use crate::math::constants::{ BASE_PRECISION_U64, FEE_POOL_TO_REVENUE_POOL_THRESHOLD, FIVE_MINUTE, ONE_HOUR, PERP_DECIMALS, @@ -207,8 +206,12 @@ pub fn place_perp_order( }; let oracle_price_data = oracle_map.get_price_data(&market.amm.oracle)?; - let (auction_start_price, auction_end_price) = - get_auction_prices(¶ms, oracle_price_data, market.amm.order_tick_size)?; + let (auction_start_price, auction_end_price, auction_duration) = get_auction_params( + ¶ms, + oracle_price_data, + market.amm.order_tick_size, + state.min_perp_auction_duration, + )?; validate!( params.market_type == MarketType::Perp, @@ -216,11 +219,6 @@ pub fn place_perp_order( "must be perp order" )?; - let auction_duration = max( - params.auction_duration.unwrap_or(0), - state.min_perp_auction_duration, - ); - let new_order = Order { status: OrderStatus::Open, order_type: params.order_type, @@ -371,13 +369,31 @@ pub fn place_perp_order( Ok(()) } -fn get_auction_prices( +fn get_auction_params( params: &OrderParams, oracle_price_data: &OraclePriceData, tick_size: u64, -) -> DriftResult<(i64, i64)> { - if !matches!(params.order_type, OrderType::Market | OrderType::Oracle) { - return Ok((0_i64, 0_i64)); + min_auction_duration: u8, +) -> DriftResult<(i64, i64, u8)> { + if !matches!( + params.order_type, + OrderType::Market | OrderType::Oracle | OrderType::Limit + ) { + return Ok((0_i64, 0_i64, 0_u8)); + } + + let auction_duration = params + .auction_duration + .unwrap_or(0) + .max(min_auction_duration); + + if params.order_type == OrderType::Limit { + return match (params.auction_start_price, params.auction_end_price) { + (Some(auction_start_price), Some(auction_end_price)) => { + Ok((auction_start_price, auction_end_price, auction_duration)) + } + _ => Ok((0_i64, 0_i64, 0_u8)), + }; } let (auction_start_price, auction_end_price) = @@ -395,6 +411,7 @@ fn get_auction_prices( Ok(( standardize_price_i64(auction_start_price, tick_size.cast()?, params.direction)?, standardize_price_i64(auction_end_price, tick_size.cast()?, params.direction)?, + auction_duration, )) } @@ -856,6 +873,7 @@ pub fn fill_perp_order( valid_oracle_price, now, slot, + state.min_perp_auction_duration, amm_is_available, )?; @@ -1076,11 +1094,12 @@ fn sanitize_maker_order<'a>( let maker_order_price = *maker_order_price; let maker_order = &maker.orders[maker_order_index]; - if !is_maker_for_taker(maker_order, taker_order)? { + if !is_maker_for_taker(maker_order, taker_order, slot)? { continue; } - if !maker_order.is_resting_limit_order(slot)? || maker_order.is_jit_maker() { + // dont use maker if order is < 45 slots old and cross amm + if slot.safe_sub(maker_order.slot)? < 45 { match maker_direction { PositionDirection::Long => { if maker_order_price >= amm_ask_price { @@ -1258,6 +1277,7 @@ fn fulfill_perp_order( valid_oracle_price: Option, now: i64, slot: u64, + min_auction_duration: u8, amm_is_available: bool, ) -> DriftResult<(u64, bool, bool)> { let market_index = user.orders[user_order_index].market_index; @@ -1287,6 +1307,7 @@ fn fulfill_perp_order( Some(oracle_price), amm_is_available, slot, + min_auction_duration, )? }; @@ -1347,6 +1368,7 @@ fn fulfill_perp_order( valid_oracle_price, now, slot, + min_auction_duration, fee_structure, oracle_map, &mut order_records, @@ -1764,6 +1786,7 @@ pub fn fulfill_perp_order_with_match( valid_oracle_price: Option, now: i64, slot: u64, + min_auction_duration: u8, fee_structure: &FeeStructure, oracle_map: &mut OracleMap, order_records: &mut Vec, @@ -1816,7 +1839,11 @@ pub fn fulfill_perp_order_with_match( // if the auction isn't complete, cant fill against vamm yet // use the vamm price to guard against bad fill for taker if taker.orders[taker_order_index].is_limit_order() - && !taker.orders[taker_order_index].is_auction_complete(slot)? + && !is_amm_available_liquidity_source( + &taker.orders[taker_order_index], + min_auction_duration, + slot, + )? { taker_price = match taker_direction { PositionDirection::Long => { @@ -2298,14 +2325,6 @@ pub fn trigger_order( let oracle_price = oracle_price_data.price; - let order_slot = user.orders[order_index].slot; - let auction_duration = user.orders[order_index].auction_duration; - validate!( - is_auction_complete(order_slot, auction_duration, slot)?, - ErrorCode::OrderDidNotSatisfyTriggerCondition, - "Auction duration must elapse before triggering" - )?; - let can_trigger = order_satisfies_trigger_condition( &user.orders[order_index], oracle_price.unsigned_abs().cast()?, @@ -2328,6 +2347,7 @@ pub fn trigger_order( user.orders[order_index].slot = slot; let order_type = user.orders[order_index].order_type; if let OrderType::TriggerMarket = order_type { + user.orders[order_index].auction_duration = state.min_perp_auction_duration; let (auction_start_price, auction_end_price) = calculate_auction_prices(oracle_price_data, direction, 0)?; user.orders[order_index].auction_start_price = auction_start_price; @@ -2712,8 +2732,12 @@ pub fn place_spot_order( ) }; - let (auction_start_price, auction_end_price) = - get_auction_prices(¶ms, &oracle_price_data, spot_market.order_tick_size)?; + let (auction_start_price, auction_end_price, auction_duration) = get_auction_params( + ¶ms, + &oracle_price_data, + spot_market.order_tick_size, + state.default_spot_auction_duration, + )?; validate!(spot_market.orders_enabled, ErrorCode::SpotOrdersDisabled)?; @@ -2729,10 +2753,6 @@ pub fn place_spot_order( "must be spot order" )?; - let auction_duration = params - .auction_duration - .unwrap_or(state.default_spot_auction_duration); - let new_order = Order { status: OrderStatus::Open, order_type: params.order_type, @@ -3153,11 +3173,7 @@ fn sanitize_spot_maker_order<'a>( { let maker_order = &maker.orders[maker_order_index]; - if !is_maker_for_taker(maker_order, taker_order)? { - return Ok((None, None, None, None)); - } - - if !maker_order.is_resting_limit_order(slot)? { + if !is_maker_for_taker(maker_order, taker_order, slot)? { return Ok((None, None, None, None)); } @@ -4282,14 +4298,6 @@ pub fn trigger_spot_order( let oracle_price = oracle_price_data.price; - let order_slot = user.orders[order_index].slot; - let auction_duration = user.orders[order_index].auction_duration; - validate!( - is_auction_complete(order_slot, auction_duration, slot)?, - ErrorCode::OrderDidNotSatisfyTriggerCondition, - "Auction duration must elapse before triggering" - )?; - let can_trigger = order_satisfies_trigger_condition( &user.orders[order_index], oracle_price.unsigned_abs().cast()?, @@ -4311,6 +4319,7 @@ pub fn trigger_spot_order( user.orders[order_index].slot = slot; let order_type = user.orders[order_index].order_type; if let OrderType::TriggerMarket = order_type { + user.orders[order_index].auction_duration = state.default_spot_auction_duration; let (auction_start_price, auction_end_price) = calculate_auction_prices(oracle_price_data, direction, 0)?; user.orders[order_index].auction_start_price = auction_start_price; diff --git a/programs/drift/src/controller/orders/amm_jit_tests.rs b/programs/drift/src/controller/orders/amm_jit_tests.rs index 1822a54e4..c27736fac 100644 --- a/programs/drift/src/controller/orders/amm_jit_tests.rs +++ b/programs/drift/src/controller/orders/amm_jit_tests.rs @@ -294,6 +294,7 @@ pub mod amm_jit { Some(market.amm.historical_oracle_data.last_oracle_price), now, slot, + 0, true, ) .unwrap(); @@ -469,6 +470,7 @@ pub mod amm_jit { Some(PRICE_PRECISION_I64), now, slot, + 10, true, ) .unwrap(); @@ -651,6 +653,7 @@ pub mod amm_jit { Some(PRICE_PRECISION_I64), now, slot, + 10, true, ) .unwrap(); @@ -833,6 +836,7 @@ pub mod amm_jit { Some(200 * PRICE_PRECISION_I64), now, slot, + 10, true, ) .unwrap(); @@ -1023,6 +1027,7 @@ pub mod amm_jit { Some(market.amm.historical_oracle_data.last_oracle_price), now, slot, + 0, true, ) .unwrap(); @@ -1221,6 +1226,7 @@ pub mod amm_jit { Some(market.amm.historical_oracle_data.last_oracle_price), now, slot, + 0, true, ) .unwrap(); @@ -1419,6 +1425,7 @@ pub mod amm_jit { Some(market.amm.historical_oracle_data.last_oracle_price), now, slot, + 0, true, ) .unwrap(); @@ -1593,6 +1600,7 @@ pub mod amm_jit { Some(1), now, slot, + 10, true, ) .unwrap(); @@ -1778,6 +1786,7 @@ pub mod amm_jit { Some(200 * PRICE_PRECISION_I64), now, slot, + 10, true, ) .unwrap(); @@ -2014,6 +2023,7 @@ pub mod amm_jit { Some(1), now, slot, + auction_duration, true, ) .unwrap(); @@ -2284,6 +2294,7 @@ pub mod amm_jit { Some(200 * PRICE_PRECISION_I64), now, slot, + 10, true, ) .unwrap(); @@ -2496,6 +2507,7 @@ pub mod amm_jit { Some(1), now, slot, + 0, true, ) .unwrap(); diff --git a/programs/drift/src/controller/orders/tests.rs b/programs/drift/src/controller/orders/tests.rs index 13608ac07..56e533aa1 100644 --- a/programs/drift/src/controller/orders/tests.rs +++ b/programs/drift/src/controller/orders/tests.rs @@ -124,6 +124,7 @@ pub mod fulfill_order_with_maker_order { None, now, slot, + 10, &fee_structure, &mut get_oracle_map(), &mut order_records, @@ -241,6 +242,7 @@ pub mod fulfill_order_with_maker_order { None, now, slot, + 10, &fee_structure, &mut get_oracle_map(), &mut order_records, @@ -358,6 +360,7 @@ pub mod fulfill_order_with_maker_order { None, now, slot, + 10, &fee_structure, &mut get_oracle_map(), &mut order_records, @@ -475,6 +478,7 @@ pub mod fulfill_order_with_maker_order { None, now, slot, + 10, &fee_structure, &mut get_oracle_map(), &mut order_records, @@ -592,6 +596,7 @@ pub mod fulfill_order_with_maker_order { None, now, slot, + 10, &fee_structure, &mut get_oracle_map(), &mut order_records, @@ -675,6 +680,7 @@ pub mod fulfill_order_with_maker_order { None, now, slot, + 10, &fee_structure, &mut get_oracle_map(), &mut order_records, @@ -759,6 +765,7 @@ pub mod fulfill_order_with_maker_order { None, now, slot, + 10, &fee_structure, &mut get_oracle_map(), &mut order_records, @@ -843,6 +850,7 @@ pub mod fulfill_order_with_maker_order { None, now, slot, + 10, &fee_structure, &mut get_oracle_map(), &mut order_records, @@ -927,6 +935,7 @@ pub mod fulfill_order_with_maker_order { None, now, slot, + 10, &fee_structure, &mut get_oracle_map(), &mut order_records, @@ -1031,6 +1040,7 @@ pub mod fulfill_order_with_maker_order { None, now, slot, + 10, &fee_structure, &mut get_oracle_map(), &mut order_records, @@ -1138,6 +1148,7 @@ pub mod fulfill_order_with_maker_order { None, now, slot, + 10, &fee_structure, &mut get_oracle_map(), &mut order_records, @@ -1252,6 +1263,7 @@ pub mod fulfill_order_with_maker_order { None, now, slot, + 0, &fee_structure, &mut get_oracle_map(), &mut order_records, @@ -1367,6 +1379,7 @@ pub mod fulfill_order_with_maker_order { None, now, slot, + 10, &fee_structure, &mut get_oracle_map(), &mut order_records, @@ -1506,6 +1519,7 @@ pub mod fulfill_order_with_maker_order { None, now, slot, + 10, &fee_structure, &mut get_oracle_map(), &mut order_records, @@ -1620,6 +1634,7 @@ pub mod fulfill_order_with_maker_order { None, now, slot, + 10, &fee_structure, &mut get_oracle_map(), &mut order_records, @@ -1718,6 +1733,7 @@ pub mod fulfill_order_with_maker_order { Some(oracle_price), now, slot, + 10, &fee_structure, &mut oracle_map, &mut order_records, @@ -1860,6 +1876,7 @@ pub mod fulfill_order_with_maker_order { Some(oracle_price), now, slot, + 10, &fee_structure, &mut oracle_map, &mut order_records, @@ -1990,6 +2007,7 @@ pub mod fulfill_order_with_maker_order { None, now, slot, + 10, &fee_structure, &mut oracle_map, &mut order_records, @@ -2131,6 +2149,7 @@ pub mod fulfill_order_with_maker_order { None, now, slot, + 10, &fee_structure, &mut oracle_map, &mut order_records, @@ -2262,6 +2281,7 @@ pub mod fulfill_order_with_maker_order { Some(oracle_price), now, slot, + 10, &fee_structure, &mut oracle_map, &mut order_records, @@ -2360,6 +2380,7 @@ pub mod fulfill_order_with_maker_order { Some(oracle_price), now, slot, + 10, &fee_structure, &mut oracle_map, &mut order_records, @@ -2368,6 +2389,257 @@ pub mod fulfill_order_with_maker_order { assert_eq!(base_asset_amount, 0); } + + #[test] + fn limit_auction_crosses_maker_bid() { + let mut maker = User { + orders: get_orders(Order { + market_index: 0, + post_only: true, + order_type: OrderType::Limit, + direction: PositionDirection::Long, + base_asset_amount: BASE_PRECISION_U64, + slot: 0, + price: 100 * PRICE_PRECISION_U64, + ..Order::default() + }), + perp_positions: get_positions(PerpPosition { + market_index: 0, + open_orders: 1, + open_bids: BASE_PRECISION_I64, + ..PerpPosition::default() + }), + ..User::default() + }; + + let mut taker = User { + orders: get_orders(Order { + market_index: 0, + order_type: OrderType::Limit, + direction: PositionDirection::Short, + base_asset_amount: BASE_PRECISION_U64, + price: 10 * PRICE_PRECISION_U64, + auction_end_price: 10 * PRICE_PRECISION_I64, + auction_start_price: 100 * PRICE_PRECISION_I64, + auction_duration: 10, + ..Order::default() + }), + perp_positions: get_positions(PerpPosition { + market_index: 0, + open_orders: 1, + open_asks: -BASE_PRECISION_I64, + ..PerpPosition::default() + }), + ..User::default() + }; + + let mut market = PerpMarket::default_test(); + + let now = 5_i64; + let slot = 5_u64; + + assert_eq!( + taker.orders[0] + .get_limit_price(None, None, slot, market.amm.order_tick_size) + .unwrap(), + Some(55000000) + ); + + let fee_structure = get_fee_structure(); + + let (maker_key, taker_key, filler_key) = get_user_keys(); + + let mut order_records = vec![]; + + let mut taker_stats = UserStats::default(); + let mut maker_stats = UserStats::default(); + + fulfill_perp_order_with_match( + &mut market, + &mut taker, + &mut taker_stats, + 0, + &taker_key, + &mut maker, + &mut Some(&mut maker_stats), + 0, + &maker_key, + &mut None, + &mut None, + &filler_key, + &mut None, + &mut None, + 0, + None, + now, + slot, + 10, + &fee_structure, + &mut get_oracle_map(), + &mut order_records, + ) + .unwrap(); + + let maker_position = &maker.perp_positions[0]; + assert_eq!(maker_position.base_asset_amount, BASE_PRECISION_I64); + assert_eq!(maker_position.quote_asset_amount, -99970000); + assert_eq!( + maker_position.quote_entry_amount, + -100 * QUOTE_PRECISION_I64 + ); + assert_eq!(maker_position.quote_break_even_amount, -99970000); + assert_eq!(maker_position.open_orders, 0); + assert_eq!(maker_position.open_bids, 0); + assert_eq!(maker_stats.fees.total_fee_rebate, 30000); + assert_eq!(maker.orders[0], Order::default()); + assert_eq!(maker_stats.maker_volume_30d, 100 * QUOTE_PRECISION_U64); + + let taker_position = &taker.perp_positions[0]; + assert_eq!(taker_position.base_asset_amount, -BASE_PRECISION_I64); + assert_eq!(taker_position.quote_asset_amount, 99950000); + assert_eq!(taker_position.quote_entry_amount, 100 * QUOTE_PRECISION_I64); + assert_eq!(taker_position.quote_break_even_amount, 99950000); + assert_eq!(taker_position.open_asks, 0); + assert_eq!(taker_position.open_orders, 0); + assert_eq!(taker_stats.fees.total_fee_paid, 50000); + assert_eq!(taker_stats.fees.total_referee_discount, 0); + assert_eq!(taker_stats.fees.total_token_discount, 0); + assert_eq!(taker_stats.taker_volume_30d, 100 * QUOTE_PRECISION_U64); + assert_eq!(taker.orders[0], Order::default()); + + assert_eq!(market.amm.base_asset_amount_with_amm, 0); + assert_eq!(market.amm.base_asset_amount_long, BASE_PRECISION_I128); + assert_eq!(market.amm.base_asset_amount_short, -BASE_PRECISION_I128); + assert_eq!(market.amm.quote_asset_amount, -20000); + assert_eq!(market.amm.total_fee, 20000); + assert_eq!(market.amm.total_fee_minus_distributions, 20000); + assert_eq!(market.amm.net_revenue_since_last_funding, 20000); + } + + #[test] + fn limit_auction_crosses_maker_ask() { + let mut maker = User { + orders: get_orders(Order { + market_index: 0, + post_only: true, + order_type: OrderType::Limit, + direction: PositionDirection::Short, + base_asset_amount: BASE_PRECISION_U64, + slot: 0, + price: 100 * PRICE_PRECISION_U64, + ..Order::default() + }), + perp_positions: get_positions(PerpPosition { + market_index: 0, + open_orders: 1, + open_asks: -BASE_PRECISION_I64, + ..PerpPosition::default() + }), + ..User::default() + }; + + let mut taker = User { + orders: get_orders(Order { + market_index: 0, + order_type: OrderType::Limit, + direction: PositionDirection::Long, + base_asset_amount: BASE_PRECISION_U64, + price: 150 * PRICE_PRECISION_U64, + auction_start_price: 50 * PRICE_PRECISION_I64, + auction_end_price: 150 * PRICE_PRECISION_I64, + auction_duration: 10, + ..Order::default() + }), + perp_positions: get_positions(PerpPosition { + market_index: 0, + open_orders: 1, + open_bids: BASE_PRECISION_I64, + ..PerpPosition::default() + }), + ..User::default() + }; + + let mut market = PerpMarket::default_test(); + + let now = 5_i64; + let slot = 5_u64; + + assert_eq!( + taker.orders[0] + .get_limit_price(None, None, slot, market.amm.order_tick_size) + .unwrap(), + Some(100000000) + ); + + let fee_structure = get_fee_structure(); + let (maker_key, taker_key, filler_key) = get_user_keys(); + + let mut order_records = vec![]; + + let mut taker_stats = UserStats::default(); + let mut maker_stats = UserStats::default(); + + fulfill_perp_order_with_match( + &mut market, + &mut taker, + &mut taker_stats, + 0, + &taker_key, + &mut maker, + &mut Some(&mut maker_stats), + 0, + &maker_key, + &mut None, + &mut None, + &filler_key, + &mut None, + &mut None, + 0, + None, + now, + slot, + 0, + &fee_structure, + &mut get_oracle_map(), + &mut order_records, + ) + .unwrap(); + + let maker_position = &maker.perp_positions[0]; + assert_eq!(maker_position.base_asset_amount, -BASE_PRECISION_I64); + assert_eq!(maker_position.quote_asset_amount, 100030000); + assert_eq!(maker_position.quote_entry_amount, 100 * QUOTE_PRECISION_I64); + assert_eq!(maker_position.quote_break_even_amount, 100030000); + assert_eq!(maker_position.open_orders, 0); + assert_eq!(maker_position.open_asks, 0); + assert_eq!(maker_stats.fees.total_fee_rebate, 30000); + assert_eq!(maker_stats.maker_volume_30d, 100 * QUOTE_PRECISION_U64); + assert_eq!(maker.orders[0], Order::default()); + + let taker_position = &taker.perp_positions[0]; + assert_eq!(taker_position.base_asset_amount, BASE_PRECISION_I64); + assert_eq!(taker_position.quote_asset_amount, -100050000); + assert_eq!( + taker_position.quote_entry_amount, + -100 * QUOTE_PRECISION_I64 + ); + assert_eq!(taker_position.quote_break_even_amount, -100050000); + assert_eq!(taker_position.open_bids, 0); + assert_eq!(taker_position.open_orders, 0); + assert_eq!(taker_stats.fees.total_fee_paid, 50000); + assert_eq!(taker_stats.fees.total_referee_discount, 0); + assert_eq!(taker_stats.fees.total_token_discount, 0); + assert_eq!(taker_stats.taker_volume_30d, 100 * QUOTE_PRECISION_U64); + assert_eq!(taker.orders[0], Order::default()); + + assert_eq!(market.amm.base_asset_amount_with_amm, 0); + assert_eq!(market.amm.base_asset_amount_long, BASE_PRECISION_I128); + assert_eq!(market.amm.base_asset_amount_short, -BASE_PRECISION_I128); + assert_eq!(market.amm.quote_asset_amount, -20000); + assert_eq!(market.amm.total_fee, 20000); + assert_eq!(market.amm.total_fee_minus_distributions, 20000); + assert_eq!(market.amm.net_revenue_since_last_funding, 20000); + } } pub mod fulfill_order { @@ -2654,6 +2926,7 @@ pub mod fulfill_order { Some(market.amm.historical_oracle_data.last_oracle_price), now, slot, + 0, true, ) .unwrap(); @@ -2879,6 +3152,7 @@ pub mod fulfill_order { Some(market.amm.historical_oracle_data.last_oracle_price), now, slot, + 10, true, ) .unwrap(); @@ -3053,6 +3327,7 @@ pub mod fulfill_order { Some(market.amm.historical_oracle_data.last_oracle_price), now, slot, + 0, true, ) .unwrap(); @@ -3242,6 +3517,7 @@ pub mod fulfill_order { None, now, slot, + 10, true, ) .unwrap(); @@ -3402,6 +3678,7 @@ pub mod fulfill_order { Some(market.amm.historical_oracle_data.last_oracle_price), now, slot, + 0, true, ) .unwrap(); @@ -3792,6 +4069,7 @@ pub mod fulfill_order { None, now, slot, + 10, true, ) .unwrap(); @@ -3905,7 +4183,7 @@ pub mod fill_order { #[test] fn maker_order_canceled_for_breaching_oracle_price_band() { let clock = Clock { - slot: 6, + slot: 56, epoch_start_timestamp: 0, epoch: 0, leader_schedule_epoch: 0, @@ -4105,7 +4383,7 @@ pub mod fill_order { #[test] fn fallback_maker_order_id() { let clock = Clock { - slot: 6, + slot: 56, epoch_start_timestamp: 0, epoch: 0, leader_schedule_epoch: 0, @@ -8047,7 +8325,7 @@ pub mod sanitize_maker_orders { #[test] fn one_maker_order_canceled_for_breaching_oracle_price_band() { let clock = Clock { - slot: 6, + slot: 56, epoch_start_timestamp: 0, epoch: 0, leader_schedule_epoch: 0, @@ -8246,7 +8524,7 @@ pub mod sanitize_maker_orders { #[test] fn one_maker_order_canceled_for_being_expired() { let clock = Clock { - slot: 6, + slot: 56, epoch_start_timestamp: 0, epoch: 0, leader_schedule_epoch: 0, diff --git a/programs/drift/src/math/auction.rs b/programs/drift/src/math/auction.rs index aec354062..87d593e51 100644 --- a/programs/drift/src/math/auction.rs +++ b/programs/drift/src/math/auction.rs @@ -85,7 +85,7 @@ pub fn calculate_auction_price( valid_oracle_price: Option, ) -> DriftResult { match order.order_type { - OrderType::Market | OrderType::TriggerMarket => { + OrderType::Market | OrderType::TriggerMarket | OrderType::Limit => { calculate_auction_price_for_fixed_auction(order, slot, tick_size) } OrderType::Oracle => calculate_auction_price_for_oracle_offset_auction( @@ -221,3 +221,11 @@ pub fn is_auction_complete(order_slot: u64, auction_duration: u8, slot: u64) -> Ok(slots_elapsed > auction_duration.cast()?) } + +pub fn is_amm_available_liquidity_source( + order: &Order, + min_auction_duration: u8, + slot: u64, +) -> DriftResult { + is_auction_complete(order.slot, min_auction_duration, slot) +} diff --git a/programs/drift/src/math/fulfillment.rs b/programs/drift/src/math/fulfillment.rs index aa8c357f7..5474ff431 100644 --- a/programs/drift/src/math/fulfillment.rs +++ b/programs/drift/src/math/fulfillment.rs @@ -1,6 +1,6 @@ use crate::controller::position::PositionDirection; use crate::error::DriftResult; -use crate::math::auction::is_auction_complete; +use crate::math::auction::is_amm_available_liquidity_source; use crate::math::matching::do_orders_cross; use crate::state::fulfillment::{PerpFulfillmentMethod, SpotFulfillmentMethod}; use crate::state::perp_market::AMM; @@ -17,12 +17,13 @@ pub fn determine_perp_fulfillment_methods( valid_oracle_price: Option, amm_is_available: bool, slot: u64, + min_auction_duration: u8, ) -> DriftResult> { let mut fulfillment_methods = vec![]; let can_fill_with_amm = amm_is_available && valid_oracle_price.is_some() - && is_auction_complete(taker_order.slot, taker_order.auction_duration, slot)?; + && is_amm_available_liquidity_source(taker_order, min_auction_duration, slot)?; let taker_price = taker_order.get_limit_price(valid_oracle_price, None, slot, amm.order_tick_size)?; diff --git a/programs/drift/src/math/fulfillment/tests.rs b/programs/drift/src/math/fulfillment/tests.rs index aea60b05c..de0023ff0 100644 --- a/programs/drift/src/math/fulfillment/tests.rs +++ b/programs/drift/src/math/fulfillment/tests.rs @@ -60,6 +60,7 @@ mod determine_perp_fulfillment_methods { Some(oracle_price), true, 0, + 0, ) .unwrap(); @@ -116,6 +117,7 @@ mod determine_perp_fulfillment_methods { Some(oracle_price), true, 0, + 0, ) .unwrap(); @@ -184,6 +186,7 @@ mod determine_perp_fulfillment_methods { Some(oracle_price), true, 0, + 0, ) .unwrap(); @@ -250,6 +253,7 @@ mod determine_perp_fulfillment_methods { Some(oracle_price), true, 0, + 0, ) .unwrap(); @@ -317,6 +321,7 @@ mod determine_perp_fulfillment_methods { Some(oracle_price), true, 0, + 0, ) .unwrap(); @@ -383,6 +388,7 @@ mod determine_perp_fulfillment_methods { Some(oracle_price), true, 0, + 0, ) .unwrap(); @@ -451,6 +457,7 @@ mod determine_perp_fulfillment_methods { Some(oracle_price), true, 0, + 0, ) .unwrap(); @@ -518,6 +525,7 @@ mod determine_perp_fulfillment_methods { Some(oracle_price), true, 0, + 0, ) .unwrap(); @@ -584,6 +592,7 @@ mod determine_perp_fulfillment_methods { Some(oracle_price), true, 0, + 0, ) .unwrap(); @@ -652,6 +661,7 @@ mod determine_perp_fulfillment_methods { Some(oracle_price), true, 0, + 0, ) .unwrap(); @@ -711,6 +721,7 @@ mod determine_perp_fulfillment_methods { Some(oracle_price), true, 0, + 0, ) .unwrap(); diff --git a/programs/drift/src/math/matching.rs b/programs/drift/src/math/matching.rs index a48e827e7..7791585a8 100644 --- a/programs/drift/src/math/matching.rs +++ b/programs/drift/src/math/matching.rs @@ -1,7 +1,7 @@ use std::cmp::min; use crate::controller::position::PositionDirection; -use crate::error::{DriftResult, ErrorCode}; +use crate::error::DriftResult; use crate::math::casting::Cast; use crate::math::constants::{BID_ASK_SPREAD_PRECISION_I128, TEN_BPS_I64}; use crate::math::orders::calculate_quote_asset_amount_for_maker_order; @@ -12,16 +12,18 @@ use crate::state::user::Order; #[cfg(test)] mod tests; -#[allow(clippy::if_same_then_else)] -pub fn is_maker_for_taker(maker_order: &Order, taker_order: &Order) -> DriftResult { - if taker_order.post_only { - Err(ErrorCode::CantMatchTwoPostOnlys) - } else if maker_order.post_only && !taker_order.post_only { - Ok(true) - } else if maker_order.is_limit_order() && taker_order.is_market_order() { - Ok(true) - } else if maker_order.is_market_order() { +pub fn is_maker_for_taker( + maker_order: &Order, + taker_order: &Order, + slot: u64, +) -> DriftResult { + // taker cant be post only and maker must be resting limit order + if taker_order.post_only || !maker_order.is_resting_limit_order(slot)? { Ok(false) + // can make if taker order isn't resting (market order or limit going through auction) or if maker is post only + } else if !taker_order.is_resting_limit_order(slot)? || maker_order.post_only { + Ok(true) + // otherwise the maker must be older than the taker order } else { Ok(maker_order.slot < taker_order.slot) } diff --git a/programs/drift/src/math/matching/tests.rs b/programs/drift/src/math/matching/tests.rs index 4dc01830a..9aaf2dcd1 100644 --- a/programs/drift/src/math/matching/tests.rs +++ b/programs/drift/src/math/matching/tests.rs @@ -2,6 +2,178 @@ use crate::controller::position::PositionDirection; use crate::math::constants::{PRICE_PRECISION_I64, PRICE_PRECISION_U64}; use crate::math::matching::*; +mod is_maker_for_taker { + use crate::math::matching::is_maker_for_taker; + use crate::state::user::{Order, OrderType}; + + #[test] + fn taker_is_post_only() { + let taker = Order { + post_only: true, + ..Default::default() + }; + let maker = Order { + post_only: false, + ..Default::default() + }; + assert_eq!(is_maker_for_taker(&maker, &taker, 0).unwrap(), false); + } + + #[test] + fn maker_is_market_order() { + let taker = Order { + post_only: false, + order_type: OrderType::Market, + ..Default::default() + }; + let maker = Order { + post_only: false, + order_type: OrderType::Market, + ..Default::default() + }; + assert_eq!(is_maker_for_taker(&maker, &taker, 0).unwrap(), false); + } + + #[test] + fn maker_is_limit_order_in_auction() { + // market order + let taker = Order { + post_only: false, + order_type: OrderType::Market, + ..Default::default() + }; + let maker = Order { + post_only: false, + order_type: OrderType::Limit, + auction_duration: 10, + slot: 0, + ..Default::default() + }; + assert_eq!(is_maker_for_taker(&maker, &taker, 0).unwrap(), false); + + // limit order in auction + let taker = Order { + post_only: false, + order_type: OrderType::Limit, + auction_duration: 10, + ..Default::default() + }; + assert_eq!(is_maker_for_taker(&maker, &taker, 0).unwrap(), false); + } + + #[test] + fn maker_is_post_only() { + // market order + let taker = Order { + post_only: false, + order_type: OrderType::Market, + ..Default::default() + }; + let maker = Order { + post_only: true, + order_type: OrderType::Limit, + ..Default::default() + }; + assert_eq!(is_maker_for_taker(&maker, &taker, 0).unwrap(), true); + + // limit order in auction + let taker = Order { + post_only: false, + order_type: OrderType::Limit, + auction_duration: 10, + ..Default::default() + }; + assert_eq!(is_maker_for_taker(&maker, &taker, 0).unwrap(), true); + } + + #[test] + fn maker_is_resting_limit_order_after_auction() { + // market order + let taker = Order { + post_only: false, + order_type: OrderType::Market, + ..Default::default() + }; + let maker = Order { + post_only: false, + order_type: OrderType::Limit, + auction_duration: 10, + ..Default::default() + }; + let slot = 11; + assert_eq!(maker.is_resting_limit_order(slot).unwrap(), true); + assert_eq!(is_maker_for_taker(&maker, &taker, slot).unwrap(), true); + + // limit order in auction + let taker = Order { + post_only: false, + order_type: OrderType::Limit, + slot, + auction_duration: 10, + ..Default::default() + }; + assert_eq!(taker.is_resting_limit_order(slot).unwrap(), false); + assert_eq!(is_maker_for_taker(&maker, &taker, slot).unwrap(), true); + } + + #[test] + fn maker_is_post_only_for_resting_taker_limit() { + let slot = 11; + + let taker = Order { + post_only: false, + order_type: OrderType::Limit, + slot: 0, + auction_duration: 10, + ..Default::default() + }; + assert_eq!(taker.is_resting_limit_order(slot).unwrap(), true); + + let maker = Order { + post_only: true, + order_type: OrderType::Limit, + ..Default::default() + }; + assert_eq!(is_maker_for_taker(&maker, &taker, slot).unwrap(), true); + } + + #[test] + fn maker_and_taker_resting_limit_orders() { + let slot = 15; + + let taker = Order { + post_only: false, + order_type: OrderType::Limit, + slot: 0, + auction_duration: 10, + ..Default::default() + }; + assert_eq!(taker.is_resting_limit_order(slot).unwrap(), true); + + let maker = Order { + post_only: false, + order_type: OrderType::Limit, + slot: 1, + auction_duration: 10, + ..Default::default() + }; + assert_eq!(taker.is_resting_limit_order(slot).unwrap(), true); + + assert_eq!(is_maker_for_taker(&maker, &taker, slot).unwrap(), false); + + let taker = Order { + post_only: false, + order_type: OrderType::Limit, + slot: 2, + auction_duration: 10, + ..Default::default() + }; + assert_eq!(taker.is_resting_limit_order(slot).unwrap(), true); + + assert_eq!(is_maker_for_taker(&maker, &taker, slot).unwrap(), true); + } +} + #[test] fn filler_multiplier_maker_long() { let direction = PositionDirection::Long; diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index ae22996a4..dded9e0e8 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -643,15 +643,19 @@ impl Order { is_auction_complete(self.slot, self.auction_duration, slot) } + pub fn has_auction(&self) -> bool { + self.auction_duration != 0 + } + pub fn has_auction_price( &self, order_slot: u64, auction_duration: u8, slot: u64, ) -> DriftResult { - let has_auction_price = - self.is_market_order() && !is_auction_complete(order_slot, auction_duration, slot)?; - Ok(has_auction_price) + let auction_complete = is_auction_complete(order_slot, auction_duration, slot)?; + let has_auction_prices = self.auction_start_price != 0 || self.auction_end_price != 0; + Ok(!auction_complete && has_auction_prices) } /// Passing in an existing_position forces the function to consider the order's reduce only status @@ -742,7 +746,7 @@ impl Order { } pub fn is_resting_limit_order(&self, slot: u64) -> DriftResult { - Ok(self.is_limit_order() && (self.post_only || slot.safe_sub(self.slot)? >= 45)) + Ok(self.is_limit_order() && (self.post_only || self.is_auction_complete(slot)?)) } } diff --git a/programs/drift/src/validation/order.rs b/programs/drift/src/validation/order.rs index 208ec0907..a0e88d685 100644 --- a/programs/drift/src/validation/order.rs +++ b/programs/drift/src/validation/order.rs @@ -50,25 +50,7 @@ fn validate_market_order(order: &Order, step_size: u64, min_order_size: u64) -> "Auction start and end price must be greater than 0" )?; - match order.direction { - PositionDirection::Long if order.auction_start_price >= order.auction_end_price => { - msg!( - "Auction start price ({}) was greater than auction end price ({})", - order.auction_start_price, - order.auction_end_price - ); - return Err(ErrorCode::InvalidOrderAuction); - } - PositionDirection::Short if order.auction_start_price <= order.auction_end_price => { - msg!( - "Auction start price ({}) was less than auction end price ({})", - order.auction_start_price, - order.auction_end_price - ); - return Err(ErrorCode::InvalidOrderAuction); - } - _ => {} - } + validate_auction_params(order)?; if order.trigger_price > 0 { msg!("Market should not have trigger price"); @@ -193,6 +175,12 @@ fn validate_limit_order( } if order.post_only { + validate!( + !order.has_auction(), + ErrorCode::InvalidOrder, + "post only limit order cant have auction" + )?; + validate_post_only_order(order, market, valid_oracle_price, slot)?; let order_breaches_oracle_price_limits = order_breaches_oracle_price_bands( @@ -209,6 +197,22 @@ fn validate_limit_order( } } + validate_limit_order_auction_params(order)?; + + Ok(()) +} + +fn validate_limit_order_auction_params(order: &Order) -> DriftResult { + if order.has_auction() { + validate!( + !order.has_oracle_price_offset(), + ErrorCode::InvalidOrder, + "limit order with auction can not have an oracle price offset" + )?; + + validate_auction_params(order)?; + } + Ok(()) } @@ -365,6 +369,63 @@ fn validate_base_asset_amount( Ok(()) } +fn validate_auction_params(order: &Order) -> DriftResult { + validate!( + order.auction_start_price != 0, + ErrorCode::InvalidOrderAuction, + "Auction start price was 0" + )?; + + validate!( + order.auction_end_price != 0, + ErrorCode::InvalidOrderAuction, + "Auction end price was 0" + )?; + + match order.direction { + PositionDirection::Long => { + if order.auction_start_price >= order.auction_end_price { + msg!( + "Auction start price ({}) was greater than auction end price ({})", + order.auction_start_price, + order.auction_end_price + ); + return Err(ErrorCode::InvalidOrderAuction); + } + + if order.price != 0 && order.price < order.auction_end_price.cast()? { + msg!( + "Order price ({}) was less than auction end price ({})", + order.price, + order.auction_end_price + ); + return Err(ErrorCode::InvalidOrderAuction); + } + } + PositionDirection::Short => { + if order.auction_start_price <= order.auction_end_price { + msg!( + "Auction start price ({}) was less than auction end price ({})", + order.auction_start_price, + order.auction_end_price + ); + return Err(ErrorCode::InvalidOrderAuction); + } + + if order.price != 0 && order.price > order.auction_end_price.cast()? { + msg!( + "Order price ({}) was greater than auction end price ({})", + order.price, + order.auction_end_price + ); + return Err(ErrorCode::InvalidOrderAuction); + } + } + } + + Ok(()) +} + pub fn validate_spot_order( order: &Order, valid_oracle_price: Option, @@ -425,6 +486,12 @@ fn validate_spot_limit_order( } if order.post_only { + validate!( + !order.has_auction(), + ErrorCode::InvalidOrder, + "post only limit order cant have auction" + )?; + let order_breaches_oracle_price_limits = order_breaches_oracle_price_bands( order, valid_oracle_price.ok_or(ErrorCode::InvalidOracle)?, @@ -439,5 +506,7 @@ fn validate_spot_limit_order( } } + validate_limit_order_auction_params(order)?; + Ok(()) } diff --git a/sdk/src/dlob/DLOB.ts b/sdk/src/dlob/DLOB.ts index 8dd85f6fd..f8cef8363 100644 --- a/sdk/src/dlob/DLOB.ts +++ b/sdk/src/dlob/DLOB.ts @@ -19,7 +19,6 @@ import { SlotSubscriber, MarketTypeStr, StateAccount, - isMarketOrder, mustBeTriggered, isTriggered, getLimitPrice, @@ -28,6 +27,9 @@ import { OrderActionRecord, ZERO, BN_MAX, + isRestingLimitOrder, + isTakingOrder, + isFallbackAvailableLiquiditySource, } from '..'; import { PublicKey } from '@solana/web3.js'; import { DLOBNode, DLOBNodeType, TriggerOrderNode } from '..'; @@ -35,14 +37,18 @@ import { ammPaused, exchangePaused, fillPaused } from '../math/exchangeStatus'; import { DLOBOrders } from './DLOBOrders'; export type MarketNodeLists = { - limit: { - ask: NodeList<'limit'>; - bid: NodeList<'limit'>; + restingLimit: { + ask: NodeList<'restingLimit'>; + bid: NodeList<'restingLimit'>; }; floatingLimit: { ask: NodeList<'floatingLimit'>; bid: NodeList<'floatingLimit'>; }; + takingLimit: { + ask: NodeList<'takingLimit'>; + bid: NodeList<'takingLimit'>; + }; market: { ask: NodeList<'market'>; bid: NodeList<'market'>; @@ -75,6 +81,7 @@ const SUPPORTED_ORDER_TYPES = [ export class DLOB { openOrders = new Map>(); orderLists = new Map>(); + maxSlotForRestingLimitOrders = 0; initialized = false; @@ -109,6 +116,8 @@ export class DLOB { } this.orderLists.clear(); + this.maxSlotForRestingLimitOrders = 0; + this.init(); } @@ -117,7 +126,10 @@ export class DLOB { * * @returns a promise that resolves when the DLOB is initialized */ - public async initFromUserMap(userMap: UserMap): Promise { + public async initFromUserMap( + userMap: UserMap, + slot: number + ): Promise { if (this.initialized) { return false; } @@ -128,7 +140,7 @@ export class DLOB { const userAccountPubkey = user.getUserAccountPublicKey(); for (const order of userAccount.orders) { - this.insertOrder(order, userAccountPubkey); + this.insertOrder(order, userAccountPubkey, slot); } } @@ -136,24 +148,27 @@ export class DLOB { return true; } - public initFromOrders(dlobOrders: DLOBOrders): boolean { + public initFromOrders(dlobOrders: DLOBOrders, slot: number): boolean { if (this.initialized) { return false; } for (const { user, order } of dlobOrders) { - this.insertOrder(order, user); + this.insertOrder(order, user, slot); } this.initialized = true; return true; } - public handleOrderRecord(record: OrderRecord): void { - this.insertOrder(record.order, record.user); + public handleOrderRecord(record: OrderRecord, slot: number): void { + this.insertOrder(record.order, record.user, slot); } - public handleOrderActionRecord(record: OrderActionRecord): void { + public handleOrderActionRecord( + record: OrderActionRecord, + slot: number + ): void { if (isOneOfVariant(record.action, ['place', 'expire'])) { return; } @@ -162,14 +177,14 @@ export class DLOB { if (record.taker !== null) { const takerOrder = this.getOrder(record.takerOrderId, record.taker); if (takerOrder) { - this.trigger(takerOrder, record.taker); + this.trigger(takerOrder, record.taker, slot); } } if (record.maker !== null) { const makerOrder = this.getOrder(record.makerOrderId, record.maker); if (makerOrder) { - this.trigger(makerOrder, record.maker); + this.trigger(makerOrder, record.maker, slot); } } } else if (isVariant(record.action, 'fill')) { @@ -179,6 +194,7 @@ export class DLOB { this.updateOrder( takerOrder, record.taker, + slot, record.takerOrderCumulativeBaseAssetAmountFilled ); } @@ -190,6 +206,7 @@ export class DLOB { this.updateOrder( makerOrder, record.maker, + slot, record.makerOrderCumulativeBaseAssetAmountFilled ); } @@ -198,14 +215,14 @@ export class DLOB { if (record.taker !== null) { const takerOrder = this.getOrder(record.takerOrderId, record.taker); if (takerOrder) { - this.delete(takerOrder, record.taker); + this.delete(takerOrder, record.taker, slot); } } if (record.maker !== null) { const makerOrder = this.getOrder(record.makerOrderId, record.maker); if (makerOrder) { - this.delete(makerOrder, record.maker); + this.delete(makerOrder, record.maker, slot); } } } @@ -214,6 +231,7 @@ export class DLOB { public insertOrder( order: Order, userAccount: PublicKey, + slot: number, onInsert?: OrderBookCallback ): void { if (isVariant(order.status, 'init')) { @@ -235,7 +253,7 @@ export class DLOB { .get(marketType) .add(getOrderSignature(order.orderId, userAccount)); } - this.getListForOrder(order)?.insert(order, marketType, userAccount); + this.getListForOrder(order, slot)?.insert(order, marketType, userAccount); if (onInsert) { onInsert(); @@ -244,14 +262,18 @@ export class DLOB { addOrderList(marketType: MarketTypeStr, marketIndex: number): void { this.orderLists.get(marketType).set(marketIndex, { - limit: { - ask: new NodeList('limit', 'asc'), - bid: new NodeList('limit', 'desc'), + restingLimit: { + ask: new NodeList('restingLimit', 'asc'), + bid: new NodeList('restingLimit', 'desc'), }, floatingLimit: { ask: new NodeList('floatingLimit', 'asc'), bid: new NodeList('floatingLimit', 'desc'), }, + takingLimit: { + ask: new NodeList('takingLimit', 'asc'), + bid: new NodeList('takingLimit', 'asc'), // always sort ascending for market orders + }, market: { ask: new NodeList('market', 'asc'), bid: new NodeList('market', 'asc'), // always sort ascending for market orders @@ -266,11 +288,14 @@ export class DLOB { public updateOrder( order: Order, userAccount: PublicKey, + slot: number, cumulativeBaseAssetAmountFilled: BN, onUpdate?: OrderBookCallback ): void { + this.updateRestingLimitOrders(slot); + if (order.baseAssetAmount.eq(cumulativeBaseAssetAmountFilled)) { - this.delete(order, userAccount); + this.delete(order, userAccount, slot); return; } @@ -283,7 +308,7 @@ export class DLOB { }; newOrder.baseAssetAmountFilled = cumulativeBaseAssetAmountFilled; - this.getListForOrder(order)?.update(newOrder, userAccount); + this.getListForOrder(order, slot)?.update(newOrder, userAccount); if (onUpdate) { onUpdate(); @@ -293,12 +318,15 @@ export class DLOB { public trigger( order: Order, userAccount: PublicKey, + slot: number, onTrigger?: OrderBookCallback ): void { if (isVariant(order.status, 'init')) { return; } + this.updateRestingLimitOrders(slot); + if (isTriggered(order)) { return; } @@ -309,7 +337,7 @@ export class DLOB { .trigger[isVariant(order.triggerCondition, 'above') ? 'above' : 'below']; triggerList.remove(order, userAccount); - this.getListForOrder(order)?.insert(order, marketType, userAccount); + this.getListForOrder(order, slot)?.insert(order, marketType, userAccount); if (onTrigger) { onTrigger(); } @@ -318,19 +346,25 @@ export class DLOB { public delete( order: Order, userAccount: PublicKey, + slot: number, onDelete?: OrderBookCallback ): void { if (isVariant(order.status, 'init')) { return; } - this.getListForOrder(order)?.remove(order, userAccount); + this.updateRestingLimitOrders(slot); + + this.getListForOrder(order, slot)?.remove(order, userAccount); if (onDelete) { onDelete(); } } - public getListForOrder(order: Order): NodeList | undefined { + public getListForOrder( + order: Order, + slot: number + ): NodeList | undefined { const isInactiveTriggerOrder = mustBeTriggered(order) && !isTriggered(order); @@ -344,7 +378,8 @@ export class DLOB { } else if (order.oraclePriceOffset !== 0) { type = 'floatingLimit'; } else { - type = 'limit'; + const isResting = isRestingLimitOrder(order, slot); + type = isResting ? 'restingLimit' : 'takingLimit'; } let subType: string; @@ -365,6 +400,58 @@ export class DLOB { ]; } + public updateRestingLimitOrders(slot: number): void { + if (slot <= this.maxSlotForRestingLimitOrders) { + return; + } + + this.maxSlotForRestingLimitOrders = slot; + + this.updateRestingLimitOrdersForMarketType(slot, 'perp'); + + this.updateRestingLimitOrdersForMarketType(slot, 'spot'); + } + + updateRestingLimitOrdersForMarketType( + slot: number, + marketTypeStr: MarketTypeStr + ): void { + for (const [_, nodeLists] of this.orderLists.get(marketTypeStr)) { + const nodesToUpdate = []; + for (const node of nodeLists.takingLimit.ask.getGenerator()) { + if (!isRestingLimitOrder(node.order, slot)) { + continue; + } + + nodesToUpdate.push({ + side: 'ask', + node, + }); + } + + for (const node of nodeLists.takingLimit.bid.getGenerator()) { + if (!isRestingLimitOrder(node.order, slot)) { + continue; + } + + nodesToUpdate.push({ + side: 'bid', + node, + }); + } + + for (const nodeToUpdate of nodesToUpdate) { + const { side, node } = nodeToUpdate; + nodeLists.takingLimit[side].remove(node.order, node.userAccount); + nodeLists.restingLimit[side].insert( + node.order, + marketTypeStr, + node.userAccount + ); + } + } + } + public getOrder(orderId: number, userAccount: PublicKey): Order | undefined { for (const nodeList of this.getNodeLists()) { const node = nodeList.get(orderId, userAccount); @@ -393,24 +480,30 @@ export class DLOB { const isAmmPaused = ammPaused(stateAccount, marketAccount); - const marketOrderNodesToFill: Array = - this.findMarketNodesToFill( + const minAuctionDuration = isVariant(marketType, 'perp') + ? stateAccount.minPerpAuctionDuration + : 0; + + const restingLimitOrderNodesToFill: Array = + this.findRestingLimitOrderNodesToFill( marketIndex, slot, marketType, oraclePriceData, isAmmPaused, + minAuctionDuration, fallbackAsk, fallbackBid ); - const limitOrderNodesToFill: Array = - this.findLimitOrderNodesToFill( + const takingOrderNodesToFill: Array = + this.findTakingNodesToFill( marketIndex, slot, marketType, oraclePriceData, isAmmPaused, + minAuctionDuration, fallbackAsk, fallbackBid ); @@ -421,28 +514,30 @@ export class DLOB { ts, marketType ); - return marketOrderNodesToFill.concat( - limitOrderNodesToFill, + return restingLimitOrderNodesToFill.concat( + takingOrderNodesToFill, expiredNodesToFill ); } - public findLimitOrderNodesToFill( + public findRestingLimitOrderNodesToFill( marketIndex: number, slot: number, marketType: MarketType, oraclePriceData: OraclePriceData, isAmmPaused: boolean, + minAuctionDuration: number, fallbackAsk: BN | undefined, fallbackBid: BN | undefined ): NodeToFill[] { const nodesToFill = new Array(); - const crossingNodes = this.findCrossingLimitOrders( + const crossingNodes = this.findCrossingRestingLimitOrders( marketIndex, slot, marketType, oraclePriceData, + minAuctionDuration, fallbackAsk, fallbackBid ); @@ -452,7 +547,7 @@ export class DLOB { } if (fallbackBid && !isAmmPaused) { - const askGenerator = this.getLimitAsks( + const askGenerator = this.getRestingLimitAsks( marketIndex, slot, marketType, @@ -466,7 +561,8 @@ export class DLOB { fallbackBid, (askPrice, fallbackPrice) => { return askPrice.lte(fallbackPrice); - } + }, + minAuctionDuration ); for (const askCrossingFallback of asksCrossingFallback) { @@ -475,7 +571,7 @@ export class DLOB { } if (fallbackAsk && !isAmmPaused) { - const bidGenerator = this.getLimitBids( + const bidGenerator = this.getRestingLimitBids( marketIndex, slot, marketType, @@ -489,7 +585,8 @@ export class DLOB { fallbackAsk, (bidPrice, fallbackPrice) => { return bidPrice.gte(fallbackPrice); - } + }, + minAuctionDuration ); for (const bidCrossingFallback of bidsCrossingFallback) { @@ -500,25 +597,31 @@ export class DLOB { return nodesToFill; } - public findMarketNodesToFill( + public findTakingNodesToFill( marketIndex: number, slot: number, marketType: MarketType, oraclePriceData: OraclePriceData, isAmmPaused: boolean, + minAuctionDuration: number, fallbackAsk: BN | undefined, fallbackBid?: BN | undefined ): NodeToFill[] { const nodesToFill = new Array(); - let marketOrderGenerator = this.getMarketAsks(marketIndex, marketType); + let takingOrderGenerator = this.getTakingAsks( + marketIndex, + marketType, + slot, + oraclePriceData + ); - const marketAsksCrossingBids = this.findMarketNodesCrossingLimitNodes( + const takingAsksCrossingBids = this.findTakingNodesCrossingMakerNodes( marketIndex, slot, marketType, oraclePriceData, - marketOrderGenerator, + takingOrderGenerator, this.getMakerLimitBids.bind(this), (takerPrice, makerPrice) => { if (isVariant(marketType, 'spot')) { @@ -534,37 +637,48 @@ export class DLOB { }, fallbackAsk ); - for (const marketAskCrossingBid of marketAsksCrossingBids) { - nodesToFill.push(marketAskCrossingBid); + for (const takingAskCrossingBid of takingAsksCrossingBids) { + nodesToFill.push(takingAskCrossingBid); } if (fallbackBid && !isAmmPaused) { - marketOrderGenerator = this.getMarketAsks(marketIndex, marketType); - const marketAsksCrossingFallback = + takingOrderGenerator = this.getTakingAsks( + marketIndex, + marketType, + slot, + oraclePriceData + ); + const takingAsksCrossingFallback = this.findNodesCrossingFallbackLiquidity( marketType, slot, oraclePriceData, - marketOrderGenerator, + takingOrderGenerator, fallbackBid, (takerPrice, fallbackPrice) => { return takerPrice === undefined || takerPrice.lte(fallbackPrice); - } + }, + minAuctionDuration ); - for (const marketAskCrossingFallback of marketAsksCrossingFallback) { - nodesToFill.push(marketAskCrossingFallback); + for (const takingAskCrossingFallback of takingAsksCrossingFallback) { + nodesToFill.push(takingAskCrossingFallback); } } - marketOrderGenerator = this.getMarketBids(marketIndex, marketType); + takingOrderGenerator = this.getTakingBids( + marketIndex, + marketType, + slot, + oraclePriceData + ); - const marketBidsToFill = this.findMarketNodesCrossingLimitNodes( + const takingBidsToFill = this.findTakingNodesCrossingMakerNodes( marketIndex, slot, marketType, oraclePriceData, - marketOrderGenerator, + takingOrderGenerator, this.getMakerLimitAsks.bind(this), (takerPrice, makerPrice) => { if (isVariant(marketType, 'spot')) { @@ -582,24 +696,30 @@ export class DLOB { fallbackBid ); - for (const marketBidToFill of marketBidsToFill) { - nodesToFill.push(marketBidToFill); + for (const takingBidToFill of takingBidsToFill) { + nodesToFill.push(takingBidToFill); } if (fallbackAsk && !isAmmPaused) { - marketOrderGenerator = this.getMarketBids(marketIndex, marketType); - const marketBidsCrossingFallback = + takingOrderGenerator = this.getTakingBids( + marketIndex, + marketType, + slot, + oraclePriceData + ); + const takingBidsCrossingFallback = this.findNodesCrossingFallbackLiquidity( marketType, slot, oraclePriceData, - marketOrderGenerator, + takingOrderGenerator, fallbackAsk, (takerPrice, fallbackPrice) => { return takerPrice === undefined || takerPrice.gte(fallbackPrice); - } + }, + minAuctionDuration ); - for (const marketBidCrossingFallback of marketBidsCrossingFallback) { + for (const marketBidCrossingFallback of takingBidsCrossingFallback) { nodesToFill.push(marketBidCrossingFallback); } } @@ -607,7 +727,7 @@ export class DLOB { return nodesToFill; } - public findMarketNodesCrossingLimitNodes( + public findTakingNodesCrossingMakerNodes( marketIndex: number, slot: number, marketType: MarketType, @@ -671,7 +791,7 @@ export class DLOB { const newMakerOrder = { ...makerOrder }; newMakerOrder.baseAssetAmountFilled = makerOrder.baseAssetAmountFilled.add(baseFilled); - this.getListForOrder(newMakerOrder).update( + this.getListForOrder(newMakerOrder, slot).update( newMakerOrder, makerNode.userAccount ); @@ -679,7 +799,7 @@ export class DLOB { const newTakerOrder = { ...takerOrder }; newTakerOrder.baseAssetAmountFilled = takerOrder.baseAssetAmountFilled.add(baseFilled); - this.getListForOrder(newTakerOrder).update( + this.getListForOrder(newTakerOrder, slot).update( newTakerOrder, takerNode.userAccount ); @@ -701,7 +821,8 @@ export class DLOB { oraclePriceData: OraclePriceData, nodeGenerator: Generator, fallbackPrice: BN, - doesCross: (nodePrice: BN | undefined, fallbackPrice: BN) => boolean + doesCross: (nodePrice: BN | undefined, fallbackPrice: BN) => boolean, + minAuctionDuration: number ): NodeToFill[] { const nodesToFill = new Array(); @@ -721,7 +842,12 @@ export class DLOB { // fallback is available if auction is complete or it's a spot order const fallbackAvailable = - isVariant(marketType, 'spot') || isAuctionComplete(node.order, slot); + isVariant(marketType, 'spot') || + isFallbackAvailableLiquiditySource( + node.order, + minAuctionDuration, + slot + ); if (crosses && fallbackAvailable) { nodesToFill.push({ @@ -752,12 +878,14 @@ export class DLOB { // All bids/asks that can expire const bidGenerators = [ - nodeLists.limit.bid.getGenerator(), + nodeLists.takingLimit.bid.getGenerator(), + nodeLists.restingLimit.bid.getGenerator(), nodeLists.floatingLimit.bid.getGenerator(), nodeLists.market.bid.getGenerator(), ]; const askGenerators = [ - nodeLists.limit.ask.getGenerator(), + nodeLists.takingLimit.ask.getGenerator(), + nodeLists.restingLimit.ask.getGenerator(), nodeLists.floatingLimit.ask.getGenerator(), nodeLists.market.ask.getGenerator(), ]; @@ -788,31 +916,40 @@ export class DLOB { public findJitAuctionNodesToFill( marketIndex: number, slot: number, + oraclePriceData: OraclePriceData, marketType: MarketType ): NodeToFill[] { const nodesToFill = new Array(); // Then see if there are orders still in JIT auction - for (const marketBid of this.getMarketBids(marketIndex, marketType)) { - if (!isAuctionComplete(marketBid.order, slot)) { - nodesToFill.push({ - node: marketBid, - }); - } + for (const marketBid of this.getTakingBids( + marketIndex, + marketType, + slot, + oraclePriceData + )) { + nodesToFill.push({ + node: marketBid, + }); } - for (const marketAsk of this.getMarketAsks(marketIndex, marketType)) { - if (!isAuctionComplete(marketAsk.order, slot)) { - nodesToFill.push({ - node: marketAsk, - }); - } + for (const marketAsk of this.getTakingAsks( + marketIndex, + marketType, + slot, + oraclePriceData + )) { + nodesToFill.push({ + node: marketAsk, + }); } return nodesToFill; } - *getMarketBids( + *getTakingBids( marketIndex: number, - marketType: MarketType + marketType: MarketType, + slot: number, + oraclePriceData: OraclePriceData ): Generator { const marketTypeStr = getVariant(marketType) as MarketTypeStr; const orderLists = this.orderLists.get(marketTypeStr).get(marketIndex); @@ -820,18 +957,28 @@ export class DLOB { return; } - const generator = orderLists.market.bid.getGenerator(); - for (const marketBidNode of generator) { - if (marketBidNode.isBaseFilled()) { - continue; + this.updateRestingLimitOrders(slot); + + const generatorList = [ + orderLists.market.bid.getGenerator(), + orderLists.takingLimit.bid.getGenerator(), + ]; + + yield* this.getBestNode( + generatorList, + oraclePriceData, + slot, + (bestNode, currentNode) => { + return bestNode.order.slot.lt(currentNode.order.slot); } - yield marketBidNode; - } + ); } - *getMarketAsks( + *getTakingAsks( marketIndex: number, - marketType: MarketType + marketType: MarketType, + slot: number, + oraclePriceData: OraclePriceData ): Generator { const marketTypeStr = getVariant(marketType) as MarketTypeStr; const orderLists = this.orderLists.get(marketTypeStr).get(marketIndex); @@ -839,20 +986,33 @@ export class DLOB { return; } - const generator = orderLists.market.ask.getGenerator(); - for (const marketAskNode of generator) { - if (marketAskNode.isBaseFilled()) { - continue; + this.updateRestingLimitOrders(slot); + + const generatorList = [ + orderLists.market.ask.getGenerator(), + orderLists.takingLimit.ask.getGenerator(), + ]; + + yield* this.getBestNode( + generatorList, + oraclePriceData, + slot, + (bestNode, currentNode) => { + return bestNode.order.slot.lt(currentNode.order.slot); } - yield marketAskNode; - } + ); } private *getBestNode( generatorList: Array>, oraclePriceData: OraclePriceData, slot: number, - compareFcn: (bestPrice: BN, currentPrice: BN) => boolean + compareFcn: ( + bestDLOBNode: DLOBNode, + currentDLOBNode: DLOBNode, + slot: number, + oraclePriceData: OraclePriceData + ) => boolean ): Generator { const generators = generatorList.map((generator) => { return { @@ -876,18 +1036,7 @@ export class DLOB { const bestValue = bestGenerator.next.value as DLOBNode; const currentValue = currentGenerator.next.value as DLOBNode; - // always return the market orders first - if (bestValue.order && isMarketOrder(bestValue.order)) { - return bestGenerator; - } - if (currentValue.order && isMarketOrder(currentValue.order)) { - return currentGenerator; - } - - const bestPrice = bestValue.getPrice(oraclePriceData, slot); - const currentPrice = currentValue.getPrice(oraclePriceData, slot); - - return compareFcn(bestPrice, currentPrice) + return compareFcn(bestValue, currentValue, slot, oraclePriceData) ? bestGenerator : currentGenerator; } @@ -908,7 +1057,7 @@ export class DLOB { } } - *getLimitAsks( + *getRestingLimitAsks( marketIndex: number, slot: number, marketType: MarketType, @@ -917,6 +1066,9 @@ export class DLOB { if (isVariant(marketType, 'spot') && !oraclePriceData) { throw new Error('Must provide OraclePriceData to get spot asks'); } + + this.updateRestingLimitOrders(slot); + const marketTypeStr = getVariant(marketType) as MarketTypeStr; const nodeLists = this.orderLists.get(marketTypeStr).get(marketIndex); @@ -925,7 +1077,7 @@ export class DLOB { } const generatorList = [ - nodeLists.limit.ask.getGenerator(), + nodeLists.restingLimit.ask.getGenerator(), nodeLists.floatingLimit.ask.getGenerator(), ]; @@ -933,15 +1085,17 @@ export class DLOB { generatorList, oraclePriceData, slot, - (bestPrice, currentPrice) => { - return bestPrice.lt(currentPrice); + (bestNode, currentNode, slot, oraclePriceData) => { + return bestNode + .getPrice(oraclePriceData, slot) + .lt(currentNode.getPrice(oraclePriceData, slot)); } ); } /** - * Filters the limit asks that are post only, have been place for sufficiently long or are above the fallback bid - * Market orders can only fill against orders that meet this criteria + * Filters the limit asks that are resting and do not cross fallback bid + * Taking orders can only fill against orders that meet this criteria * * @returns */ @@ -952,28 +1106,23 @@ export class DLOB { oraclePriceData: OraclePriceData, fallbackBid?: BN ): Generator { - for (const node of this.getLimitAsks( + for (const node of this.getRestingLimitAsks( marketIndex, slot, marketType, oraclePriceData )) { - if (this.isRestingLimitOrder(node.order, slot)) { - yield node; - } else if ( + if ( fallbackBid && - node.getPrice(oraclePriceData, slot).gt(fallbackBid) + node.getPrice(oraclePriceData, slot).lte(fallbackBid) ) { - yield node; + continue; } + yield node; } } - isRestingLimitOrder(order: Order, slot: number): boolean { - return order.postOnly || new BN(slot).sub(order.slot).gte(new BN(45)); - } - - *getLimitBids( + *getRestingLimitBids( marketIndex: number, slot: number, marketType: MarketType, @@ -983,6 +1132,8 @@ export class DLOB { throw new Error('Must provide OraclePriceData to get spot bids'); } + this.updateRestingLimitOrders(slot); + const marketTypeStr = getVariant(marketType) as MarketTypeStr; const nodeLists = this.orderLists.get(marketTypeStr).get(marketIndex); @@ -991,7 +1142,7 @@ export class DLOB { } const generatorList = [ - nodeLists.limit.bid.getGenerator(), + nodeLists.restingLimit.bid.getGenerator(), nodeLists.floatingLimit.bid.getGenerator(), ]; @@ -999,8 +1150,10 @@ export class DLOB { generatorList, oraclePriceData, slot, - (bestPrice, currentPrice) => { - return bestPrice.gt(currentPrice); + (bestNode, currentNode, slot, oraclePriceData) => { + return bestNode + .getPrice(oraclePriceData, slot) + .gt(currentNode.getPrice(oraclePriceData, slot)); } ); } @@ -1018,20 +1171,19 @@ export class DLOB { oraclePriceData: OraclePriceData, fallbackAsk?: BN ): Generator { - for (const node of this.getLimitBids( + for (const node of this.getRestingLimitBids( marketIndex, slot, marketType, oraclePriceData )) { - if (this.isRestingLimitOrder(node.order, slot)) { - yield node; - } else if ( + if ( fallbackAsk && - node.getPrice(oraclePriceData, slot).lt(fallbackAsk) + node.getPrice(oraclePriceData, slot).gte(fallbackAsk) ) { - yield node; + continue; } + yield node; } } @@ -1047,8 +1199,8 @@ export class DLOB { } const generatorList = [ - this.getMarketAsks(marketIndex, marketType), - this.getLimitAsks(marketIndex, slot, marketType, oraclePriceData), + this.getTakingAsks(marketIndex, marketType, slot, oraclePriceData), + this.getRestingLimitAsks(marketIndex, slot, marketType, oraclePriceData), ]; const marketTypeStr = getVariant(marketType) as MarketTypeStr; @@ -1060,8 +1212,29 @@ export class DLOB { generatorList, oraclePriceData, slot, - (bestPrice, currentPrice) => { - return bestPrice.lt(currentPrice); + (bestNode, currentNode, slot, oraclePriceData) => { + const bestNodeTaking = bestNode.order + ? isTakingOrder(bestNode.order, slot) + : false; + const currentNodeTaking = currentNode.order + ? isTakingOrder(currentNode.order, slot) + : false; + + if (bestNodeTaking && currentNodeTaking) { + return bestNode.order.slot.lt(currentNode.order.slot); + } + + if (bestNodeTaking) { + return true; + } + + if (currentNodeTaking) { + return false; + } + + return bestNode + .getPrice(oraclePriceData, slot) + .lt(currentNode.getPrice(oraclePriceData, slot)); } ); } @@ -1078,8 +1251,8 @@ export class DLOB { } const generatorList = [ - this.getMarketBids(marketIndex, marketType), - this.getLimitBids(marketIndex, slot, marketType, oraclePriceData), + this.getTakingBids(marketIndex, marketType, slot, oraclePriceData), + this.getRestingLimitBids(marketIndex, slot, marketType, oraclePriceData), ]; const marketTypeStr = getVariant(marketType) as MarketTypeStr; @@ -1091,29 +1264,51 @@ export class DLOB { generatorList, oraclePriceData, slot, - (bestPrice, currentPrice) => { - return bestPrice.gt(currentPrice); + (bestNode, currentNode, slot, oraclePriceData) => { + const bestNodeTaking = bestNode.order + ? isTakingOrder(bestNode.order, slot) + : false; + const currentNodeTaking = currentNode.order + ? isTakingOrder(currentNode.order, slot) + : false; + + if (bestNodeTaking && currentNodeTaking) { + return bestNode.order.slot.lt(currentNode.order.slot); + } + + if (bestNodeTaking) { + return true; + } + + if (currentNodeTaking) { + return false; + } + + return bestNode + .getPrice(oraclePriceData, slot) + .gt(currentNode.getPrice(oraclePriceData, slot)); } ); } - findCrossingLimitOrders( + findCrossingRestingLimitOrders( marketIndex: number, slot: number, marketType: MarketType, oraclePriceData: OraclePriceData, + minAuctionDuration: number, fallbackAsk: BN | undefined, fallbackBid: BN | undefined ): NodeToFill[] { const nodesToFill = new Array(); - for (const askNode of this.getLimitAsks( + for (const askNode of this.getRestingLimitAsks( marketIndex, slot, marketType, oraclePriceData )) { - for (const bidNode of this.getLimitBids( + for (const bidNode of this.getRestingLimitBids( marketIndex, slot, marketType, @@ -1142,7 +1337,13 @@ export class DLOB { ); // extra guard against bad fills for limit orders where auction is incomplete - if (!isAuctionComplete(takerNode.order, slot)) { + if ( + !isFallbackAvailableLiquiditySource( + takerNode.order, + minAuctionDuration, + slot + ) + ) { let bidPrice: BN; let askPrice: BN; if (isVariant(takerNode.order.direction, 'long')) { @@ -1176,7 +1377,7 @@ export class DLOB { const newBidOrder = { ...bidOrder }; newBidOrder.baseAssetAmountFilled = bidOrder.baseAssetAmountFilled.add(baseFilled); - this.getListForOrder(newBidOrder).update( + this.getListForOrder(newBidOrder, slot).update( newBidOrder, bidNode.userAccount ); @@ -1185,7 +1386,7 @@ export class DLOB { const newAskOrder = { ...askOrder }; newAskOrder.baseAssetAmountFilled = askOrder.baseAssetAmountFilled.add(baseFilled); - this.getListForOrder(newAskOrder).update( + this.getListForOrder(newAskOrder, slot).update( newAskOrder, askNode.userAccount ); @@ -1288,11 +1489,9 @@ export class DLOB { if (triggerAboveList) { for (const node of triggerAboveList.getGenerator()) { if (oraclePrice.gt(node.order.triggerPrice)) { - if (isAuctionComplete(node.order, slot)) { - nodesToTrigger.push({ - node: node, - }); - } + nodesToTrigger.push({ + node: node, + }); } else { break; } @@ -1305,11 +1504,9 @@ export class DLOB { if (triggerBelowList) { for (const node of triggerBelowList.getGenerator()) { if (oraclePrice.lt(node.order.triggerPrice)) { - if (isAuctionComplete(node.order, slot)) { - nodesToTrigger.push({ - node: node, - }); - } + nodesToTrigger.push({ + node: node, + }); } else { break; } @@ -1438,8 +1635,10 @@ export class DLOB { *getNodeLists(): Generator> { for (const [_, nodeLists] of this.orderLists.get('perp')) { - yield nodeLists.limit.bid; - yield nodeLists.limit.ask; + yield nodeLists.restingLimit.bid; + yield nodeLists.restingLimit.ask; + yield nodeLists.takingLimit.bid; + yield nodeLists.takingLimit.ask; yield nodeLists.market.bid; yield nodeLists.market.ask; yield nodeLists.floatingLimit.bid; @@ -1449,8 +1648,10 @@ export class DLOB { } for (const [_, nodeLists] of this.orderLists.get('spot')) { - yield nodeLists.limit.bid; - yield nodeLists.limit.ask; + yield nodeLists.restingLimit.bid; + yield nodeLists.restingLimit.ask; + yield nodeLists.takingLimit.bid; + yield nodeLists.takingLimit.ask; yield nodeLists.market.bid; yield nodeLists.market.ask; yield nodeLists.floatingLimit.bid; diff --git a/sdk/src/dlob/DLOBNode.ts b/sdk/src/dlob/DLOBNode.ts index 5ab440f7b..b6dcf245d 100644 --- a/sdk/src/dlob/DLOBNode.ts +++ b/sdk/src/dlob/DLOBNode.ts @@ -77,9 +77,18 @@ export abstract class OrderNode implements DLOBNode { } } -export class LimitOrderNode extends OrderNode { - next?: LimitOrderNode; - previous?: LimitOrderNode; +export class TakingLimitOrderNode extends OrderNode { + next?: TakingLimitOrderNode; + previous?: TakingLimitOrderNode; + + getSortValue(order: Order): BN { + return order.slot; + } +} + +export class RestingLimitOrderNode extends OrderNode { + next?: RestingLimitOrderNode; + previous?: RestingLimitOrderNode; getSortValue(order: Order): BN { return order.price; @@ -114,14 +123,16 @@ export class TriggerOrderNode extends OrderNode { } export type DLOBNodeMap = { - limit: LimitOrderNode; + restingLimit: RestingLimitOrderNode; + takingLimit: TakingLimitOrderNode; floatingLimit: FloatingLimitOrderNode; market: MarketOrderNode; trigger: TriggerOrderNode; }; export type DLOBNodeType = - | 'limit' + | 'restingLimit' + | 'takingLimit' | 'floatingLimit' | 'market' | ('trigger' & keyof DLOBNodeMap); @@ -134,8 +145,10 @@ export function createNode( switch (nodeType) { case 'floatingLimit': return new FloatingLimitOrderNode(order, userAccount); - case 'limit': - return new LimitOrderNode(order, userAccount); + case 'restingLimit': + return new RestingLimitOrderNode(order, userAccount); + case 'takingLimit': + return new TakingLimitOrderNode(order, userAccount); case 'market': return new MarketOrderNode(order, userAccount); case 'trigger': diff --git a/sdk/src/examples/loadDlob.ts b/sdk/src/examples/loadDlob.ts index effdc8b29..afa1e28df 100644 --- a/sdk/src/examples/loadDlob.ts +++ b/sdk/src/examples/loadDlob.ts @@ -64,7 +64,7 @@ const main = async () => { console.log('Loading dlob from user map...'); const dlob = new DLOB(); - await dlob.initFromUserMap(userMap); + await dlob.initFromUserMap(userMap, bulkAccountLoader.mostRecentSlot); console.log('number of orders', dlob.getDLOBOrders().length); diff --git a/sdk/src/math/auction.ts b/sdk/src/math/auction.ts index 5220cfd94..90b72b20a 100644 --- a/sdk/src/math/auction.ts +++ b/sdk/src/math/auction.ts @@ -9,12 +9,24 @@ export function isAuctionComplete(order: Order, slot: number): boolean { return new BN(slot).sub(order.slot).gt(new BN(order.auctionDuration)); } +export function isFallbackAvailableLiquiditySource( + order: Order, + minAuctionDuration: number, + slot: number +): boolean { + if (minAuctionDuration === 0) { + return true; + } + + return new BN(slot).sub(order.slot).gt(new BN(minAuctionDuration)); +} + export function getAuctionPrice( order: Order, slot: number, oraclePrice: BN ): BN { - if (isOneOfVariant(order.orderType, ['market', 'triggerMarket'])) { + if (isOneOfVariant(order.orderType, ['market', 'triggerMarket', 'limit'])) { return getAuctionPriceForFixedAuction(order, slot); } else if (isVariant(order.orderType, 'oracle')) { return getAuctionPriceForOracleOffsetAuction(order, slot, oraclePrice); diff --git a/sdk/src/math/orders.ts b/sdk/src/math/orders.ts index a081f13d1..54e315924 100644 --- a/sdk/src/math/orders.ts +++ b/sdk/src/math/orders.ts @@ -153,7 +153,10 @@ export function hasLimitPrice(order: Order, slot: number): boolean { } export function hasAuctionPrice(order: Order, slot: number): boolean { - return isMarketOrder(order) && !isAuctionComplete(order, slot); + return ( + !isAuctionComplete(order, slot) && + (!order.auctionStartPrice.eq(ZERO) || !order.auctionEndPrice.eq(ZERO)) + ); } export function isFillableByVAMM( @@ -280,3 +283,13 @@ export function isTriggered(order: Order): boolean { 'triggeredBelow', ]); } + +export function isRestingLimitOrder(order: Order, slot: number): boolean { + return ( + isLimitOrder(order) && (order.postOnly || isAuctionComplete(order, slot)) + ); +} + +export function isTakingOrder(order: Order, slot: number): boolean { + return isMarketOrder(order) || !isRestingLimitOrder(order, slot); +} diff --git a/sdk/tests/dlob/test.ts b/sdk/tests/dlob/test.ts index 6372c2a06..6e68af5da 100644 --- a/sdk/tests/dlob/test.ts +++ b/sdk/tests/dlob/test.ts @@ -41,14 +41,16 @@ function insertOrderToDLOB( slot?: BN, maxTs = ZERO, oraclePriceOffset = new BN(0), - postOnly = false + postOnly = false, + auctionDuration = 10 ) { + slot = slot || new BN(1); dlob.insertOrder( { status: OrderStatus.OPEN, orderType, marketType, - slot: slot || new BN(1), + slot, orderId, userOrderId: 0, marketIndex, @@ -65,12 +67,13 @@ function insertOrderToDLOB( postOnly, immediateOrCancel: false, oraclePriceOffset: oraclePriceOffset.toNumber(), - auctionDuration: 10, + auctionDuration, auctionStartPrice, auctionEndPrice, maxTs, }, - userAccount + userAccount, + slot.toNumber() ); } @@ -92,12 +95,13 @@ function insertTriggerOrderToDLOB( maxTs = ZERO, oraclePriceOffset = new BN(0) ) { + slot = slot || new BN(1); dlob.insertOrder( { status: OrderStatus.OPEN, orderType, marketType, - slot: slot || new BN(1), + slot, orderId, userOrderId: 0, marketIndex, @@ -119,7 +123,8 @@ function insertTriggerOrderToDLOB( auctionEndPrice, maxTs, }, - userAccount + userAccount, + slot.toNumber() ); } @@ -545,6 +550,266 @@ describe('DLOB Tests', () => { true ); }); + + it('DLOB update resting limit orders bids', () => { + const vAsk = new BN(15); + const vBid = new BN(10); + + let slot = 1; + const oracle = { + price: vBid.add(vAsk).div(new BN(2)), + slot: new BN(slot), + confidence: new BN(1), + hasSufficientNumberOfDataPoints: true, + }; + + const user0 = Keypair.generate(); + const user1 = Keypair.generate(); + const user2 = Keypair.generate(); + + const dlob = new DLOB(); + const marketIndex = 0; + const marketType = MarketType.PERP; + + insertOrderToDLOB( + dlob, + user0.publicKey, + OrderType.LIMIT, + MarketType.PERP, + 1, // orderId + marketIndex, + new BN(11), // price + BASE_PRECISION, // quantity + PositionDirection.LONG, + vBid, + vAsk, + new BN(1) + ); + insertOrderToDLOB( + dlob, + user1.publicKey, + OrderType.LIMIT, + MarketType.PERP, + 2, // orderId + marketIndex, + new BN(12), // price + BASE_PRECISION, // quantity + PositionDirection.LONG, + vBid, + vAsk, + new BN(11) + ); + insertOrderToDLOB( + dlob, + user2.publicKey, + OrderType.LIMIT, + MarketType.PERP, + 3, // orderId + marketIndex, + new BN(13), // price + BASE_PRECISION, // quantity + PositionDirection.LONG, + vBid, + vAsk, + new BN(21) + ); + + let takingBids = Array.from( + dlob.getTakingBids(marketIndex, marketType, slot, oracle) + ); + + expect(takingBids.length).to.equal(3); + expect(takingBids[0].order.orderId).to.equal(1); + expect(takingBids[1].order.orderId).to.equal(2); + expect(takingBids[2].order.orderId).to.equal(3); + + let restingBids = Array.from( + dlob.getRestingLimitBids(marketIndex, slot, marketType, oracle) + ); + + expect(restingBids.length).to.equal(0); + + slot += 11; + + takingBids = Array.from( + dlob.getTakingBids(marketIndex, marketType, slot, oracle) + ); + + expect(takingBids.length).to.equal(2); + expect(takingBids[0].order.orderId).to.equal(2); + expect(takingBids[1].order.orderId).to.equal(3); + + restingBids = Array.from( + dlob.getRestingLimitBids(marketIndex, slot, marketType, oracle) + ); + + expect(restingBids.length).to.equal(1); + expect(restingBids[0].order.orderId).to.equal(1); + + slot += 11; + + takingBids = Array.from( + dlob.getTakingBids(marketIndex, marketType, slot, oracle) + ); + + expect(takingBids.length).to.equal(1); + expect(takingBids[0].order.orderId).to.equal(3); + + restingBids = Array.from( + dlob.getRestingLimitBids(marketIndex, slot, marketType, oracle) + ); + + expect(restingBids.length).to.equal(2); + expect(restingBids[0].order.orderId).to.equal(2); + expect(restingBids[1].order.orderId).to.equal(1); + + slot += 11; + + takingBids = Array.from( + dlob.getTakingBids(marketIndex, marketType, slot, oracle) + ); + + expect(takingBids.length).to.equal(0); + + restingBids = Array.from( + dlob.getRestingLimitBids(marketIndex, slot, marketType, oracle) + ); + + expect(restingBids.length).to.equal(3); + expect(restingBids[0].order.orderId).to.equal(3); + expect(restingBids[1].order.orderId).to.equal(2); + expect(restingBids[2].order.orderId).to.equal(1); + }); + + it('DLOB update resting limit orders asks', () => { + const vAsk = new BN(15); + const vBid = new BN(10); + + let slot = 1; + const oracle = { + price: vBid.add(vAsk).div(new BN(2)), + slot: new BN(slot), + confidence: new BN(1), + hasSufficientNumberOfDataPoints: true, + }; + + const user0 = Keypair.generate(); + const user1 = Keypair.generate(); + const user2 = Keypair.generate(); + + const dlob = new DLOB(); + const marketIndex = 0; + const marketType = MarketType.PERP; + + insertOrderToDLOB( + dlob, + user0.publicKey, + OrderType.LIMIT, + MarketType.PERP, + 1, // orderId + marketIndex, + new BN(13), // price + BASE_PRECISION, // quantity + PositionDirection.SHORT, + vBid, + vAsk, + new BN(1) + ); + insertOrderToDLOB( + dlob, + user1.publicKey, + OrderType.LIMIT, + MarketType.PERP, + 2, // orderId + marketIndex, + new BN(12), // price + BASE_PRECISION, // quantity + PositionDirection.SHORT, + vBid, + vAsk, + new BN(11) + ); + insertOrderToDLOB( + dlob, + user2.publicKey, + OrderType.LIMIT, + MarketType.PERP, + 3, // orderId + marketIndex, + new BN(11), // price + BASE_PRECISION, // quantity + PositionDirection.SHORT, + vBid, + vAsk, + new BN(21) + ); + + let takingBids = Array.from( + dlob.getTakingAsks(marketIndex, marketType, slot, oracle) + ); + + expect(takingBids.length).to.equal(3); + expect(takingBids[0].order.orderId).to.equal(1); + expect(takingBids[1].order.orderId).to.equal(2); + expect(takingBids[2].order.orderId).to.equal(3); + + let restingBids = Array.from( + dlob.getRestingLimitAsks(marketIndex, slot, marketType, oracle) + ); + + expect(restingBids.length).to.equal(0); + + slot += 11; + + takingBids = Array.from( + dlob.getTakingAsks(marketIndex, marketType, slot, oracle) + ); + + expect(takingBids.length).to.equal(2); + expect(takingBids[0].order.orderId).to.equal(2); + expect(takingBids[1].order.orderId).to.equal(3); + + restingBids = Array.from( + dlob.getRestingLimitAsks(marketIndex, slot, marketType, oracle) + ); + + expect(restingBids.length).to.equal(1); + expect(restingBids[0].order.orderId).to.equal(1); + + slot += 11; + + takingBids = Array.from( + dlob.getTakingAsks(marketIndex, marketType, slot, oracle) + ); + + expect(takingBids.length).to.equal(1); + expect(takingBids[0].order.orderId).to.equal(3); + + restingBids = Array.from( + dlob.getRestingLimitAsks(marketIndex, slot, marketType, oracle) + ); + + expect(restingBids.length).to.equal(2); + expect(restingBids[0].order.orderId).to.equal(2); + expect(restingBids[1].order.orderId).to.equal(1); + + slot += 11; + + takingBids = Array.from( + dlob.getTakingAsks(marketIndex, marketType, slot, oracle) + ); + + expect(takingBids.length).to.equal(0); + + restingBids = Array.from( + dlob.getRestingLimitAsks(marketIndex, slot, marketType, oracle) + ); + + expect(restingBids.length).to.equal(3); + expect(restingBids[0].order.orderId).to.equal(3); + expect(restingBids[1].order.orderId).to.equal(2); + expect(restingBids[2].order.orderId).to.equal(1); + }); }); describe('DLOB Perp Tests', () => { @@ -569,6 +834,8 @@ describe('DLOB Perp Tests', () => { price: new BN(0), direction: PositionDirection.LONG, orderType: OrderType.MARKET, + slot: new BN(0), + postOnly: false, }, { expectedIdx: 1, @@ -577,6 +844,8 @@ describe('DLOB Perp Tests', () => { price: new BN(0), direction: PositionDirection.LONG, orderType: OrderType.MARKET, + slot: new BN(1), + postOnly: false, }, { expectedIdx: 2, @@ -585,6 +854,8 @@ describe('DLOB Perp Tests', () => { price: new BN(0), direction: PositionDirection.LONG, orderType: OrderType.MARKET, + slot: new BN(2), + postOnly: false, }, { expectedIdx: 3, @@ -593,6 +864,8 @@ describe('DLOB Perp Tests', () => { price: new BN(12), direction: PositionDirection.LONG, orderType: OrderType.LIMIT, + slot: new BN(3), + postOnly: false, }, { expectedIdx: 4, @@ -601,6 +874,8 @@ describe('DLOB Perp Tests', () => { price: new BN(11), direction: PositionDirection.LONG, orderType: OrderType.LIMIT, + slot: new BN(4), + postOnly: false, }, { expectedIdx: 7, @@ -609,6 +884,8 @@ describe('DLOB Perp Tests', () => { price: new BN(8), direction: PositionDirection.LONG, orderType: OrderType.LIMIT, + slot: new BN(5), + postOnly: true, }, { expectedIdx: 5, @@ -617,6 +894,8 @@ describe('DLOB Perp Tests', () => { price: undefined, direction: undefined, orderType: undefined, + slot: undefined, + postOnly: false, }, { expectedIdx: 6, @@ -625,6 +904,8 @@ describe('DLOB Perp Tests', () => { price: new BN(9), direction: PositionDirection.LONG, orderType: OrderType.LIMIT, + slot: new BN(6), + postOnly: true, }, ]; @@ -645,8 +926,12 @@ describe('DLOB Perp Tests', () => { t.price || new BN(0), // price BASE_PRECISION, // quantity t.direction || PositionDirection.LONG, - vBid, - vAsk + !t.postOnly ? vBid : ZERO, + !t.postOnly ? vAsk : ZERO, + t.slot, + undefined, + undefined, + t.postOnly ); } @@ -682,29 +967,34 @@ describe('DLOB Perp Tests', () => { } expect(countBids).to.equal(testCases.length); - const marketBids = dlob.getMarketBids(marketIndex, MarketType.PERP); + const takingBids = dlob.getTakingBids( + marketIndex, + MarketType.PERP, + slot, + oracle + ); countBids = 0; - for (const bid of marketBids) { + for (const bid of takingBids) { expect(bid.isVammNode(), `expected vAMM node`).to.be.eq( - expectedTestCase.slice(0, 3)[countBids].isVamm + expectedTestCase.slice(0, 5)[countBids].isVamm ); expect(bid.order?.orderId, `expected orderId`).to.equal( - expectedTestCase.slice(0, 3)[countBids].orderId + expectedTestCase.slice(0, 5)[countBids].orderId ); expect(bid.order?.price.toNumber(), `expected price`).to.equal( - expectedTestCase.slice(0, 3)[countBids].price?.toNumber() + expectedTestCase.slice(0, 5)[countBids].price?.toNumber() ); expect(bid.order?.direction, `expected order direction`).to.equal( - expectedTestCase.slice(0, 3)[countBids].direction + expectedTestCase.slice(0, 5)[countBids].direction ); expect(bid.order?.orderType, `expected order type`).to.equal( - expectedTestCase.slice(0, 3)[countBids].orderType + expectedTestCase.slice(0, 5)[countBids].orderType ); countBids++; } - expect(countBids).to.equal(expectedTestCase.slice(0, 3).length); + expect(countBids).to.equal(expectedTestCase.slice(0, 5).length); - const limitBids = dlob.getLimitBids( + const limitBids = dlob.getRestingLimitBids( marketIndex, slot, MarketType.PERP, @@ -713,28 +1003,28 @@ describe('DLOB Perp Tests', () => { countBids = 0; let idx = 0; for (const bid of limitBids) { - if (expectedTestCase.slice(3)[idx].isVamm) { + if (expectedTestCase.slice(5)[idx].isVamm) { idx++; } expect(bid.isVammNode(), `expected vAMM node`).to.be.eq( - expectedTestCase.slice(3)[idx].isVamm + expectedTestCase.slice(5)[idx].isVamm ); expect(bid.order?.orderId, `expected orderId`).to.equal( - expectedTestCase.slice(3)[idx].orderId + expectedTestCase.slice(5)[idx].orderId ); expect(bid.order?.price.toNumber(), `expected price`).to.equal( - expectedTestCase.slice(3)[idx].price?.toNumber() + expectedTestCase.slice(5)[idx].price?.toNumber() ); expect(bid.order?.direction, `expected order direction`).to.equal( - expectedTestCase.slice(3)[idx].direction + expectedTestCase.slice(5)[idx].direction ); expect(bid.order?.orderType, `expected order type`).to.equal( - expectedTestCase.slice(3)[idx].orderType + expectedTestCase.slice(5)[idx].orderType ); countBids++; idx++; } - expect(countBids).to.equal(expectedTestCase.slice(3).length - 1); // subtract one since test case 5 is vAMM node + expect(countBids).to.equal(expectedTestCase.slice(5).length - 1); // subtract one since test case 5 is vAMM node }); it('Test proper bids on multiple markets', () => { @@ -904,6 +1194,8 @@ describe('DLOB Perp Tests', () => { price: new BN(0), direction: PositionDirection.SHORT, orderType: OrderType.MARKET, + slot: new BN(0), + postOnly: false, }, { expectedIdx: 1, @@ -912,6 +1204,8 @@ describe('DLOB Perp Tests', () => { price: new BN(0), direction: PositionDirection.SHORT, orderType: OrderType.MARKET, + slot: new BN(1), + postOnly: false, }, { expectedIdx: 2, @@ -920,6 +1214,8 @@ describe('DLOB Perp Tests', () => { price: new BN(0), direction: PositionDirection.SHORT, orderType: OrderType.MARKET, + slot: new BN(2), + postOnly: false, }, { expectedIdx: 3, @@ -928,6 +1224,8 @@ describe('DLOB Perp Tests', () => { price: new BN(13), direction: PositionDirection.SHORT, orderType: OrderType.LIMIT, + slot: new BN(3), + postOnly: false, }, { expectedIdx: 6, @@ -936,6 +1234,8 @@ describe('DLOB Perp Tests', () => { price: new BN(16), direction: PositionDirection.SHORT, orderType: OrderType.LIMIT, + slot: new BN(4), + postOnly: true, }, { expectedIdx: 5, @@ -944,6 +1244,8 @@ describe('DLOB Perp Tests', () => { price: undefined, direction: undefined, orderType: undefined, + slot: new BN(0), + postOnly: false, }, { expectedIdx: 7, @@ -952,6 +1254,8 @@ describe('DLOB Perp Tests', () => { price: new BN(17), direction: PositionDirection.SHORT, orderType: OrderType.LIMIT, + slot: new BN(4), + postOnly: true, }, { expectedIdx: 4, @@ -960,6 +1264,8 @@ describe('DLOB Perp Tests', () => { price: new BN(14), direction: PositionDirection.SHORT, orderType: OrderType.LIMIT, + slot: new BN(4), + postOnly: true, }, ]; @@ -980,8 +1286,12 @@ describe('DLOB Perp Tests', () => { t.price || new BN(0), // price BASE_PRECISION, // quantity t.direction || PositionDirection.SHORT, - vBid, - vAsk + !t.postOnly ? vBid : ZERO, + !t.postOnly ? vAsk : ZERO, + t.slot, + undefined, + undefined, + t.postOnly ); } @@ -1008,29 +1318,34 @@ describe('DLOB Perp Tests', () => { } expect(countAsks).to.equal(testCases.length); - const marketAsks = dlob.getMarketAsks(marketIndex, MarketType.PERP); + const takingAsks = dlob.getTakingAsks( + marketIndex, + MarketType.PERP, + slot, + oracle + ); countAsks = 0; - for (const ask of marketAsks) { + for (const ask of takingAsks) { expect(ask.isVammNode()).to.be.eq( - expectedTestCase.slice(0, 3)[countAsks].isVamm + expectedTestCase.slice(0, 4)[countAsks].isVamm ); expect(ask.order?.orderId).to.equal( - expectedTestCase.slice(0, 3)[countAsks].orderId + expectedTestCase.slice(0, 4)[countAsks].orderId ); expect(ask.order?.price.toNumber()).to.equal( - expectedTestCase.slice(0, 3)[countAsks].price?.toNumber() + expectedTestCase.slice(0, 4)[countAsks].price?.toNumber() ); expect(ask.order?.direction).to.equal( - expectedTestCase.slice(0, 3)[countAsks].direction + expectedTestCase.slice(0, 4)[countAsks].direction ); expect(ask.order?.orderType).to.equal( - expectedTestCase.slice(0, 3)[countAsks].orderType + expectedTestCase.slice(0, 4)[countAsks].orderType ); countAsks++; } - expect(countAsks).to.equal(expectedTestCase.slice(0, 3).length); + expect(countAsks).to.equal(expectedTestCase.slice(0, 4).length); - const limitAsks = dlob.getLimitAsks( + const limitAsks = dlob.getRestingLimitAsks( marketIndex, slot, MarketType.PERP, @@ -1039,26 +1354,26 @@ describe('DLOB Perp Tests', () => { countAsks = 0; let idx = 0; for (const ask of limitAsks) { - if (expectedTestCase.slice(3)[idx].isVamm) { + if (expectedTestCase.slice(4)[idx].isVamm) { idx++; } - expect(ask.isVammNode()).to.be.eq(expectedTestCase.slice(3)[idx].isVamm); + expect(ask.isVammNode()).to.be.eq(expectedTestCase.slice(4)[idx].isVamm); expect(ask.order?.orderId).to.equal( - expectedTestCase.slice(3)[idx].orderId + expectedTestCase.slice(4)[idx].orderId ); expect(ask.order?.price.toNumber()).to.equal( - expectedTestCase.slice(3)[idx].price?.toNumber() + expectedTestCase.slice(4)[idx].price?.toNumber() ); expect(ask.order?.direction).to.equal( - expectedTestCase.slice(3)[idx].direction + expectedTestCase.slice(4)[idx].direction ); expect(ask.order?.orderType).to.equal( - expectedTestCase.slice(3)[idx].orderType + expectedTestCase.slice(4)[idx].orderType ); countAsks++; idx++; } - expect(countAsks).to.equal(expectedTestCase.slice(3).length - 1); // subtract one since test case includes vAMM node + expect(countAsks).to.equal(expectedTestCase.slice(4).length - 1); // subtract one since test case includes vAMM node }); it('Test insert market orders', () => { @@ -1185,7 +1500,12 @@ describe('DLOB Perp Tests', () => { BASE_PRECISION, PositionDirection.LONG, vBid, - vAsk + vAsk, + undefined, + undefined, + undefined, + undefined, + 0 ); insertOrderToDLOB( @@ -1199,7 +1519,12 @@ describe('DLOB Perp Tests', () => { BASE_PRECISION, PositionDirection.LONG, vBid, - vAsk + vAsk, + undefined, + undefined, + undefined, + undefined, + 0 ); insertOrderToDLOB( @@ -1213,7 +1538,12 @@ describe('DLOB Perp Tests', () => { BASE_PRECISION, PositionDirection.LONG, vBid, - vAsk + vAsk, + undefined, + undefined, + undefined, + undefined, + 0 ); insertOrderToDLOB( @@ -1227,7 +1557,12 @@ describe('DLOB Perp Tests', () => { BASE_PRECISION, PositionDirection.SHORT, vBid, - vAsk + vAsk, + undefined, + undefined, + undefined, + undefined, + 0 ); insertOrderToDLOB( @@ -1241,7 +1576,12 @@ describe('DLOB Perp Tests', () => { BASE_PRECISION, PositionDirection.SHORT, vBid, - vAsk + vAsk, + undefined, + undefined, + undefined, + undefined, + 0 ); insertOrderToDLOB( @@ -1255,7 +1595,12 @@ describe('DLOB Perp Tests', () => { BASE_PRECISION, PositionDirection.SHORT, vBid, - vAsk + vAsk, + undefined, + undefined, + undefined, + undefined, + 0 ); let asks = 0; @@ -1552,7 +1897,7 @@ describe('DLOB Perp Tests', () => { ); // should have no crossing orders - const nodesToFillBefore = dlob.findLimitOrderNodesToFill( + const nodesToFillBefore = dlob.findRestingLimitOrderNodesToFill( marketIndex, 12, // auction over MarketType.PERP, @@ -1563,6 +1908,7 @@ describe('DLOB Perp Tests', () => { hasSufficientNumberOfDataPoints: true, }, false, + 10, undefined, undefined ); @@ -1696,7 +2042,7 @@ describe('DLOB Perp Tests', () => { const endSlot = 12; // should have no crossing orders - const nodesToFillBefore = dlob.findLimitOrderNodesToFill( + const nodesToFillBefore = dlob.findRestingLimitOrderNodesToFill( marketIndex, endSlot, MarketType.PERP, @@ -1707,6 +2053,7 @@ describe('DLOB Perp Tests', () => { hasSufficientNumberOfDataPoints: true, }, false, + 10, undefined, undefined ); @@ -2293,7 +2640,8 @@ describe('DLOB Perp Tests', () => { new BN(slot), ZERO, new BN(1).mul(PRICE_PRECISION), - true + true, + 0 ); insertOrderToDLOB( dlob, @@ -2310,7 +2658,8 @@ describe('DLOB Perp Tests', () => { new BN(slot), ZERO, new BN(1).mul(PRICE_PRECISION), - true + true, + 0 ); insertOrderToDLOB( dlob, @@ -2327,7 +2676,8 @@ describe('DLOB Perp Tests', () => { new BN(slot), ZERO, new BN(1).mul(PRICE_PRECISION), - true + true, + 0 ); // should have no crossing orders @@ -2496,12 +2846,13 @@ describe('DLOB Perp Tests', () => { // should have no crossing orders const auctionOverSlot = slot * 10; const auctionOverTs = ts * 10; - const nodesToFillBefore = dlob.findLimitOrderNodesToFill( + const nodesToFillBefore = dlob.findRestingLimitOrderNodesToFill( marketIndex, auctionOverSlot, // auction over MarketType.PERP, oracle, false, + 10, undefined, undefined ); @@ -3012,7 +3363,6 @@ describe('DLOB Perp Tests', () => { printCrossedNodes(n, afterAuctionSlot); } - // taker should fill first order completely with best maker (1/1) expect( nodesToFillAfter[0].node.order?.orderId, 'wrong taker orderId' @@ -3022,7 +3372,6 @@ describe('DLOB Perp Tests', () => { 'wrong maker orderId' ).to.equal(undefined); - // taker should fill second order completely with vamm expect( nodesToFillAfter[1].node.order?.orderId, 'wrong taker orderId' @@ -3070,7 +3419,8 @@ describe('DLOB Perp Tests', () => { new BN(slot), new BN(200), undefined, - true + true, + 0 ); // insert a buy above the vBid insertOrderToDLOB( @@ -3086,7 +3436,10 @@ describe('DLOB Perp Tests', () => { vBid, vAsk, new BN(slot - 1), // later order becomes taker - new BN(200) + new BN(200), + undefined, + undefined, + 0 ); console.log(`Book state before fill:`); @@ -3242,11 +3595,13 @@ describe('DLOB Perp Tests', () => { vBid.sub(PRICE_PRECISION), new BN(1).mul(BASE_PRECISION), // quantity PositionDirection.SHORT, - vAsk, - vBid, + ZERO, + ZERO, new BN(slot), new BN(200), - undefined + undefined, + undefined, + 0 ); // Market buy right above amm bid. crosses limit sell but can't be used @@ -3277,9 +3632,13 @@ describe('DLOB Perp Tests', () => { vAsk.add(PRICE_PRECISION), // price, new BN(8768).mul(BASE_PRECISION).div(new BN(10000)), // quantity PositionDirection.LONG, - vBid, - vAsk, - new BN(slot) + ZERO, + ZERO, + new BN(slot), + undefined, + undefined, + undefined, + 0 ); // Market sell right below amm ask. crosses limit buy but can't be used @@ -3302,12 +3661,13 @@ describe('DLOB Perp Tests', () => { console.log(`Book state before fill:`); printBookState(dlob, marketIndex, vBid, vAsk, slot, oracle); - const nodesToFillBefore = dlob.findMarketNodesToFill( + const nodesToFillBefore = dlob.findTakingNodesToFill( marketIndex, slot, MarketType.PERP, oracle, false, + 10, vAsk, vBid ); @@ -3325,10 +3685,13 @@ describe('DLOB Perp Tests', () => { vBid.add(PRICE_PRECISION.div(TWO)), new BN(1).mul(BASE_PRECISION), // quantity PositionDirection.SHORT, - vAsk, - vBid, + ZERO, + ZERO, new BN(slot), - new BN(200) + new BN(200), + undefined, + undefined, + 0 ); // insert a buy below the amm ask @@ -3342,17 +3705,22 @@ describe('DLOB Perp Tests', () => { vAsk.sub(PRICE_PRECISION.div(TWO)), // price, new BN(8768).mul(BASE_PRECISION).div(new BN(10000)), // quantity PositionDirection.LONG, - vBid, - vAsk, - new BN(slot) + ZERO, + ZERO, + new BN(slot), + undefined, + undefined, + undefined, + 0 ); - const nodesToFillAfter = dlob.findMarketNodesToFill( + const nodesToFillAfter = dlob.findTakingNodesToFill( marketIndex, slot, MarketType.PERP, oracle, false, + 10, vAsk, vBid ); @@ -3377,6 +3745,138 @@ describe('DLOB Perp Tests', () => { 'wrong maker orderId' ).to.equal(5); }); + + it('Test limit bid fills during auction', () => { + const vAsk = new BN(20).mul(PRICE_PRECISION); + const vBid = new BN(5).mul(PRICE_PRECISION); + + const user0 = Keypair.generate(); + const user1 = Keypair.generate(); + const user2 = Keypair.generate(); + const user3 = Keypair.generate(); + + const dlob = new DLOB(); + const marketIndex = 0; + + const slot = 9; + const ts = 9; + const oracle = { + price: vBid.add(vAsk).div(new BN(2)), // 11.5 + slot: new BN(slot), + confidence: new BN(1), + hasSufficientNumberOfDataPoints: true, + }; + + insertOrderToDLOB( + dlob, + user3.publicKey, + OrderType.LIMIT, + MarketType.PERP, + 2, // orderId + marketIndex, + vAsk, // price + new BN(1).mul(BASE_PRECISION), // quantity + PositionDirection.LONG, + vBid.add(PRICE_PRECISION), + vAsk, + new BN(0), + new BN(200), + undefined, + undefined, + 10 + ); + + insertOrderToDLOB( + dlob, + user2.publicKey, + OrderType.LIMIT, + MarketType.PERP, + 4, // orderId + marketIndex, + vBid, // price + new BN(1).mul(BASE_PRECISION), // quantity + PositionDirection.SHORT, + vAsk.sub(PRICE_PRECISION), + vBid, + new BN(0), + new BN(200), + undefined, + undefined, + 10 + ); + + // insert a sell right above amm bid + insertOrderToDLOB( + dlob, + user0.publicKey, + OrderType.LIMIT, + MarketType.PERP, + 5, // orderId + marketIndex, + oracle.price, + new BN(1).mul(BASE_PRECISION), // quantity + PositionDirection.SHORT, + ZERO, + ZERO, + new BN(slot), + new BN(200), + undefined, + true, + 0 + ); + + // insert a buy below the amm ask + insertOrderToDLOB( + dlob, + user1.publicKey, + OrderType.LIMIT, + MarketType.PERP, + 6, // orderId + marketIndex, + oracle.price, // price, + new BN(8768).mul(BASE_PRECISION).div(new BN(10000)), // quantity + PositionDirection.LONG, + ZERO, + ZERO, + new BN(slot), + undefined, + undefined, + true, + 0 + ); + + const nodesToFillAfter = dlob.findNodesToFill( + marketIndex, + vBid, + vAsk, + slot, + ts, + MarketType.PERP, + oracle, + mockStateAccount, + mockPerpMarkets[marketIndex] + ); + + expect(nodesToFillAfter.length).to.equal(2); + + expect( + nodesToFillAfter[0].node.order?.orderId, + 'wrong taker orderId' + ).to.equal(4); + expect( + nodesToFillAfter[0].makerNode?.order?.orderId, + 'wrong maker orderId' + ).to.equal(6); + + expect( + nodesToFillAfter[1].node.order?.orderId, + 'wrong taker orderId' + ).to.equal(2); + expect( + nodesToFillAfter[1].makerNode?.order?.orderId, + 'wrong maker orderId' + ).to.equal(5); + }); }); describe('DLOB Spot Tests', () => { @@ -3400,6 +3900,8 @@ describe('DLOB Spot Tests', () => { price: new BN(0), // will calc 108 direction: PositionDirection.LONG, orderType: OrderType.MARKET, + slot: new BN(0), + postOnly: false, }, { expectedIdx: 1, @@ -3407,6 +3909,8 @@ describe('DLOB Spot Tests', () => { price: new BN(0), // will calc 108 direction: PositionDirection.LONG, orderType: OrderType.MARKET, + slot: new BN(1), + postOnly: false, }, { expectedIdx: 2, @@ -3414,6 +3918,8 @@ describe('DLOB Spot Tests', () => { price: new BN(0), // will calc 108 direction: PositionDirection.LONG, orderType: OrderType.MARKET, + slot: new BN(2), + postOnly: false, }, { expectedIdx: 4, @@ -3421,6 +3927,8 @@ describe('DLOB Spot Tests', () => { price: new BN(110), direction: PositionDirection.LONG, orderType: OrderType.LIMIT, + slot: new BN(0), + postOnly: true, }, { expectedIdx: 5, @@ -3428,6 +3936,8 @@ describe('DLOB Spot Tests', () => { price: new BN(109), direction: PositionDirection.LONG, orderType: OrderType.LIMIT, + slot: new BN(0), + postOnly: true, }, { expectedIdx: 6, @@ -3435,6 +3945,8 @@ describe('DLOB Spot Tests', () => { price: new BN(107), direction: PositionDirection.LONG, orderType: OrderType.LIMIT, + slot: new BN(0), + postOnly: true, }, { expectedIdx: 7, @@ -3442,6 +3954,8 @@ describe('DLOB Spot Tests', () => { price: new BN(106), direction: PositionDirection.LONG, orderType: OrderType.LIMIT, + slot: new BN(0), + postOnly: true, }, ]; @@ -3458,8 +3972,12 @@ describe('DLOB Spot Tests', () => { t.price || new BN(0), // price BASE_PRECISION, // quantity t.direction || PositionDirection.LONG, - vBid, - vAsk + !t.postOnly ? vBid : ZERO, + !t.postOnly ? vAsk : ZERO, + t.slot, + undefined, + undefined, + t.postOnly ); } @@ -3651,6 +4169,8 @@ describe('DLOB Spot Tests', () => { price: new BN(0), direction: PositionDirection.SHORT, orderType: OrderType.MARKET, + slot: new BN(0), + postOnly: false, }, { expectedIdx: 1, @@ -3658,6 +4178,8 @@ describe('DLOB Spot Tests', () => { price: new BN(0), direction: PositionDirection.SHORT, orderType: OrderType.MARKET, + slot: new BN(1), + postOnly: false, }, { expectedIdx: 2, @@ -3665,6 +4187,8 @@ describe('DLOB Spot Tests', () => { price: new BN(0), direction: PositionDirection.SHORT, orderType: OrderType.MARKET, + slot: new BN(2), + postOnly: false, }, { expectedIdx: 3, @@ -3672,6 +4196,8 @@ describe('DLOB Spot Tests', () => { price: new BN(13), direction: PositionDirection.SHORT, orderType: OrderType.LIMIT, + slot: new BN(3), + postOnly: false, }, { expectedIdx: 6, @@ -3679,6 +4205,8 @@ describe('DLOB Spot Tests', () => { price: new BN(16), direction: PositionDirection.SHORT, orderType: OrderType.LIMIT, + slot: new BN(0), + postOnly: true, }, { expectedIdx: 7, @@ -3686,6 +4214,8 @@ describe('DLOB Spot Tests', () => { price: new BN(17), direction: PositionDirection.SHORT, orderType: OrderType.LIMIT, + slot: new BN(0), + postOnly: true, }, { expectedIdx: 4, @@ -3693,6 +4223,8 @@ describe('DLOB Spot Tests', () => { price: new BN(14), direction: PositionDirection.SHORT, orderType: OrderType.LIMIT, + slot: new BN(0), + postOnly: true, }, ]; @@ -3709,8 +4241,12 @@ describe('DLOB Spot Tests', () => { t.price || new BN(0), // price BASE_PRECISION, // quantity t.direction || PositionDirection.SHORT, - vBid, - vAsk + !t.postOnly ? vBid : ZERO, + !t.postOnly ? vAsk : ZERO, + t.slot, + undefined, + undefined, + t.postOnly ); } diff --git a/tests/liquidateBorrowForPerpPnl.ts b/tests/liquidateBorrowForPerpPnl.ts index c172fb565..d2c76d52f 100644 --- a/tests/liquidateBorrowForPerpPnl.ts +++ b/tests/liquidateBorrowForPerpPnl.ts @@ -132,7 +132,7 @@ describe('liquidate borrow for perp pnl', () => { const oracleGuardRails: OracleGuardRails = { priceDivergence: { markOracleDivergenceNumerator: new BN(1), - markOracleDivergenceDenominator: new BN(10), + markOracleDivergenceDenominator: new BN(1), }, validity: { slotsBeforeStaleForAmm: new BN(100),