diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index 0fc1fc3850..be8249559a 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -2617,8 +2617,56 @@ pub fn handle_update_perp_bid_ask_twap<'c: 'info, 'info>( let depth = perp_market.get_market_depth_for_funding_rate()?; - let (bids, asks) = + let amm_worst_price_bid = perp_market + .amm + .get_price_for_swap(depth, PositionDirection::Short)?; + let amm_worst_price_ask = perp_market + .amm + .get_price_for_swap(depth, PositionDirection::Long)?; + + let (mut bids, mut asks) = find_bids_and_asks_from_users(perp_market, oracle_price_data, &makers, slot, now)?; + bids.retain(|level| level.price >= amm_worst_price_bid); + asks.retain(|level| level.price <= amm_worst_price_ask); + + if !perp_market.is_operation_paused(PerpOperation::AmmFill) && !state.amm_paused()? { + let base_per_level = depth.safe_div(10)?; + let amm_bids = + perp_market + .amm + .clone() + .get_levels(16, PositionDirection::Short, base_per_level)?; + let amm_asks = + perp_market + .amm + .clone() + .get_levels(16, PositionDirection::Long, base_per_level)?; + + bids.extend(amm_bids); + asks.extend(amm_asks); + bids.sort_by(|a, b| b.price.cmp(&a.price)); + asks.sort_by(|a, b| a.price.cmp(&b.price)); + let merge_same_price = |side: &mut Vec| { + if side.is_empty() { + return; + } + let mut merged: Vec = Vec::with_capacity(side.len()); + for lvl in side.drain(..) { + if let Some(last) = merged.last_mut() { + if last.price == lvl.price { + last.base_asset_amount = + last.base_asset_amount.saturating_add(lvl.base_asset_amount); + continue; + } + } + merged.push(lvl); + } + *side = merged; + }; + merge_same_price(&mut bids); + merge_same_price(&mut asks); + } + let estimated_bid = estimate_price_from_side(&bids, depth)?; let estimated_ask = estimate_price_from_side(&asks, depth)?; diff --git a/programs/drift/src/math/amm.rs b/programs/drift/src/math/amm.rs index 0407f627cf..d76ace0d1a 100644 --- a/programs/drift/src/math/amm.rs +++ b/programs/drift/src/math/amm.rs @@ -114,12 +114,12 @@ pub fn update_mark_twap_crank( let (amm_bid_price, amm_ask_price) = amm.bid_ask_price(amm_reserve_price)?; let mut best_bid_price = match best_dlob_bid_price { - Some(best_dlob_bid_price) => best_dlob_bid_price.max(amm_bid_price), + Some(best_dlob_bid_price) => best_dlob_bid_price, None => amm_bid_price, }; let mut best_ask_price = match best_dlob_ask_price { - Some(best_dlob_ask_price) => best_dlob_ask_price.min(amm_ask_price), + Some(best_dlob_ask_price) => best_dlob_ask_price, None => amm_ask_price, }; diff --git a/programs/drift/src/math/orders.rs b/programs/drift/src/math/orders.rs index b4495afd5c..2d3c390232 100644 --- a/programs/drift/src/math/orders.rs +++ b/programs/drift/src/math/orders.rs @@ -1215,7 +1215,7 @@ fn calculate_free_collateral_delta_for_spot( }) } -#[derive(Eq, PartialEq, Debug)] +#[derive(Eq, PartialEq, Debug, Clone)] pub struct Level { pub price: u64, pub base_asset_amount: u64, diff --git a/programs/drift/src/math/orders/tests.rs b/programs/drift/src/math/orders/tests.rs index 52b0e012a8..5b90f573fb 100644 --- a/programs/drift/src/math/orders/tests.rs +++ b/programs/drift/src/math/orders/tests.rs @@ -1061,6 +1061,59 @@ mod find_maker_orders { } } +pub mod amm_l2_levels { + use crate::controller::position::PositionDirection; + use crate::math::orders::Level; + use crate::state::perp_market::AMM; + + fn is_monotonic(levels: &Vec, dir: PositionDirection) -> bool { + if levels.is_empty() { + return true; + } + match dir { + PositionDirection::Long => levels.windows(2).all(|w| w[0].price <= w[1].price), + PositionDirection::Short => levels.windows(2).all(|w| w[0].price >= w[1].price), + } + } + + #[test] + fn amm_get_levels_monotonic_and_terminal_clamp() { + let amm = AMM::liquid_sol_test(); + + let (bid_price, ask_price) = amm.bid_ask_price(amm.reserve_price().unwrap()).unwrap(); + + let depth = (amm + .base_asset_amount_long + .abs() + .max(amm.base_asset_amount_short.abs()) + .unsigned_abs() + / 1000) as u64; + + // Asks monotonically increasing and greater than oracle price + let asks = amm + .get_levels(10, PositionDirection::Long, depth / 10) + .unwrap(); + assert!(!asks.is_empty()); + assert!(is_monotonic(&asks, PositionDirection::Long)); + + assert!(asks + .iter() + .all(|l| l.base_asset_amount > 0 && l.price > ask_price)); + + // Bids monotonically decreasing and less than oracle price + let bids = amm + .get_levels(10, PositionDirection::Short, depth / 10) + .unwrap(); + assert!(!bids.is_empty()); + assert!(is_monotonic(&bids, PositionDirection::Short)); + assert!(bids + .iter() + .all(|l| l.base_asset_amount > 0 && l.price < bid_price)); + + println!("Bids: {:?}", bids); + } +} + mod calculate_max_spot_order_size { use std::str::FromStr; diff --git a/programs/drift/src/state/perp_market.rs b/programs/drift/src/state/perp_market.rs index c0d5bd44cf..8d5ab290da 100644 --- a/programs/drift/src/state/perp_market.rs +++ b/programs/drift/src/state/perp_market.rs @@ -1,3 +1,8 @@ +use crate::controller::amm::SwapDirection; +use crate::math::amm::{calculate_quote_asset_amount_swapped, calculate_swap_output}; +use crate::math::amm_spread::{self, get_spread_reserves}; +use crate::math::constants::PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO; +use crate::math::orders::{standardize_base_asset_amount, standardize_price, Level}; use crate::state::fill_mode::FillMode; use crate::state::pyth_lazer_oracle::PythLazerOracle; use crate::state::user::{MarketType, Order}; @@ -1700,6 +1705,120 @@ impl AMM { self.mm_oracle_slot = mm_oracle_slot; Ok(()) } + + pub fn get_levels( + &self, + levels: u8, + taker_direction: PositionDirection, + base_swap_amount_per_level: u64, + ) -> DriftResult> { + let (mut base_reserve, mut quote_reserve) = get_spread_reserves(self, taker_direction)?; + + let (max_bids, max_asks) = amm::_calculate_market_open_bids_asks( + base_reserve, + self.min_base_asset_reserve, + self.max_base_asset_reserve, + )?; + let open_liquidity_u128: u128 = match taker_direction { + PositionDirection::Long => max_bids.unsigned_abs(), + PositionDirection::Short => max_asks.unsigned_abs(), + }; + let open_liquidity: u64 = open_liquidity_u128.min(u64::MAX as u128).cast()?; + + if open_liquidity < self.min_order_size.saturating_mul(2) { + return Ok(Vec::new()); + } + + let swap_dir = match taker_direction { + PositionDirection::Long => SwapDirection::Remove, + PositionDirection::Short => SwapDirection::Add, + }; + + let mut remaining = open_liquidity; + let standardized_base_swap = + standardize_base_asset_amount(base_swap_amount_per_level, self.order_step_size)?; + if standardized_base_swap == 0 { + return Ok(Vec::new()); + } + let mut out: Vec = Vec::with_capacity(levels as usize); + + for _ in 0..levels { + if remaining < self.order_step_size { + break; + } + + // Sim swap + let step_swap: u64 = standardized_base_swap.min(remaining); + if step_swap == 0 { + break; + } + + let (new_quote_reserve, new_base_reserve) = + calculate_swap_output(step_swap as u128, base_reserve, swap_dir, self.sqrt_k)?; + + let quote_swapped = calculate_quote_asset_amount_swapped( + quote_reserve, + new_quote_reserve, + swap_dir, + self.peg_multiplier, + )?; + + let mut price: u64 = quote_swapped + .safe_mul(PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO)? + .safe_div(step_swap.cast()?)? + .cast()?; + + price = standardize_price(price, self.order_tick_size, taker_direction)?; + + out.push(Level { + price, + base_asset_amount: step_swap, + }); + + base_reserve = new_base_reserve; + quote_reserve = new_quote_reserve; + remaining = remaining.saturating_sub(step_swap); + + if out.len() as u8 >= levels { + break; + } + } + + Ok(out) + } + + pub fn get_price_for_swap( + &self, + base_asset_amount: u64, + taker_direction: PositionDirection, + ) -> DriftResult { + let (base_reserve, quote_reserve) = amm_spread::get_spread_reserves(self, taker_direction)?; + let swap_direction = match taker_direction { + PositionDirection::Long => SwapDirection::Remove, + PositionDirection::Short => SwapDirection::Add, + }; + + let (new_quote_reserve, _new_base_reserve) = calculate_swap_output( + base_asset_amount as u128, + base_reserve, + swap_direction, + self.sqrt_k, + )?; + + let quote_swapped = calculate_quote_asset_amount_swapped( + quote_reserve, + new_quote_reserve, + swap_direction, + self.peg_multiplier, + )?; + + let price: u64 = quote_swapped + .safe_mul(PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO)? + .safe_div(base_asset_amount as u128)? + .cast()?; + + Ok(price) + } } #[cfg(test)] @@ -1764,4 +1883,105 @@ impl AMM { ..AMM::default() } } + + pub fn liquid_sol_test() -> Self { + AMM { + historical_oracle_data: HistoricalOracleData { + last_oracle_price: 190641285, + last_oracle_conf: 0, + last_oracle_delay: 17, + last_oracle_price_twap: 189914813, + last_oracle_price_twap_5min: 190656263, + last_oracle_price_twap_ts: 1761000653, + }, + base_asset_amount_per_lp: -213874721369, + quote_asset_amount_per_lp: -58962015125, + fee_pool: PoolBalance { + scaled_balance: 8575516773308741, + market_index: 0, + padding: [0, 0, 0, 0, 0, 0], + }, + base_asset_reserve: 24302266099492168, + quote_asset_reserve: 24291832241447530, + concentration_coef: 1004142, + min_base_asset_reserve: 24196060267862680, + max_base_asset_reserve: 24396915542699764, + sqrt_k: 24297048610394662, + peg_multiplier: 190724934, + terminal_quote_asset_reserve: 24297816895589961, + base_asset_amount_long: 917177880000000, + base_asset_amount_short: -923163630000000, + base_asset_amount_with_amm: -5985750000000, + base_asset_amount_with_unsettled_lp: 0, + max_open_interest: 2000000000000000, + quote_asset_amount: 15073495357350, + quote_entry_amount_long: -182456763836058, + quote_entry_amount_short: 182214483467437, + quote_break_even_amount_long: -181616323258115, + quote_break_even_amount_short: 180666910938502, + user_lp_shares: 0, + last_funding_rate: 142083, + last_funding_rate_long: 142083, + last_funding_rate_short: 142083, + last_24h_avg_funding_rate: -832430, + total_fee: 23504910735696, + total_mm_fee: 8412188362643, + total_exchange_fee: 15240376207986, + total_fee_minus_distributions: 12622783464171, + total_fee_withdrawn: 7622904850984, + total_liquidation_fee: 5153159954719, + cumulative_funding_rate_long: 48574028958, + cumulative_funding_rate_short: 48367829283, + total_social_loss: 4512659649, + ask_base_asset_reserve: 24307711375337898, + ask_quote_asset_reserve: 24286390522755450, + bid_base_asset_reserve: 24318446036975185, + bid_quote_asset_reserve: 24275670011080633, + last_oracle_normalised_price: 190641285, + last_oracle_reserve_price_spread_pct: 0, + last_bid_price_twap: 189801870, + last_ask_price_twap: 189877406, + last_mark_price_twap: 189839638, + last_mark_price_twap_5min: 190527180, + last_update_slot: 374711191, + last_oracle_conf_pct: 491, + net_revenue_since_last_funding: 9384752152, + last_funding_rate_ts: 1760997616, + funding_period: 3600, + order_step_size: 10000000, + order_tick_size: 100, + min_order_size: 10000000, + mm_oracle_slot: 374711192, + volume_24h: 114093279361263, + long_intensity_volume: 1572903262040, + short_intensity_volume: 3352472398103, + last_trade_ts: 1761000641, + mark_std: 623142, + oracle_std: 727888, + last_mark_price_twap_ts: 1761000646, + base_spread: 100, + max_spread: 20000, + long_spread: 40, + short_spread: 842, + mm_oracle_price: 190643458, + max_fill_reserve_fraction: 25000, + max_slippage_ratio: 50, + curve_update_intensity: 110, + amm_jit_intensity: 100, + last_oracle_valid: true, + target_base_asset_amount_per_lp: -565000000, + per_lp_base: 3, + taker_speed_bump_override: 5, + amm_spread_adjustment: -20, + oracle_slot_delay_override: -1, + mm_oracle_sequence_id: 1761000654650000, + net_unsettled_funding_pnl: 1042875, + quote_asset_amount_with_unsettled_lp: -112671203108, + reference_price_offset: -488, + amm_inventory_spread_adjustment: -20, + last_funding_oracle_twap: 189516656, + reference_price_offset_deadband_pct: 10, + ..AMM::default() + } + } }