From 06b80a8f70cacd8dece0167a9626dad5cb499d0d Mon Sep 17 00:00:00 2001 From: Nour Alharithi <14929853+moosecat2@users.noreply.github.com> Date: Mon, 20 Oct 2025 15:05:37 -0700 Subject: [PATCH 1/6] add amm levels --- programs/drift/src/instructions/keeper.rs | 53 ++++++++++- programs/drift/src/math/orders.rs | 2 +- programs/drift/src/math/orders/tests.rs | 86 ++++++++++++++++++ programs/drift/src/state/perp_market.rs | 106 ++++++++++++++++++++++ 4 files changed, 245 insertions(+), 2 deletions(-) diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index 0fc1fc3850..ff1eb1f79a 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -2617,8 +2617,59 @@ 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 (mut bids, mut asks) = find_bids_and_asks_from_users(perp_market, oracle_price_data, &makers, slot, now)?; + + if !perp_market.is_operation_paused(PerpOperation::AmmFill) { + let oracle_price = mm_oracle_price_data + .get_safe_oracle_price_data() + .price + .cast::()?; + let ask_terminal_price = match asks.last() { + Some(level) => level.price, + None => oracle_price.saturating_mul(120).saturating_div(100), + }; + let bid_terminal_price = match bids.last() { + Some(level) => level.price, + None => oracle_price.saturating_mul(80).saturating_div(100), + }; + + let amm_bids = + perp_market + .amm + .clone() + .get_levels(16, PositionDirection::Short, bid_terminal_price)?; + let amm_asks = + perp_market + .amm + .clone() + .get_levels(16, PositionDirection::Long, ask_terminal_price)?; + + 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/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..302bf7e0b2 100644 --- a/programs/drift/src/math/orders/tests.rs +++ b/programs/drift/src/math/orders/tests.rs @@ -1061,6 +1061,92 @@ mod find_maker_orders { } } +// ----------------------------------------------------------------------------- +// AMM L2 generation and merging tests +// ----------------------------------------------------------------------------- +pub mod amm_l2_levels_and_merge { + use crate::controller::position::PositionDirection; + use crate::math::constants::PRICE_PRECISION_U64; + use crate::math::orders::{estimate_price_from_side, Level}; + use crate::state::perp_market::AMM; + + fn is_monotonic(levels: &Vec, dir: PositionDirection) -> bool { + if levels.is_empty() { + return true; + } + match dir { + // Long taker consumes asks: prices should be non-decreasing with depth + PositionDirection::Long => levels.windows(2).all(|w| w[0].price <= w[1].price), + // Short taker consumes bids: prices should be non-increasing with depth + PositionDirection::Short => levels.windows(2).all(|w| w[0].price >= w[1].price), + } + } + + fn merge_by_price(mut a: Vec, mut b: Vec, dir: PositionDirection) -> Vec { + // Combine and sort, then coalesce identical price levels + a.append(&mut b); + + match dir { + PositionDirection::Long => a.sort_by_key(|l| l.price), // asks ascending + PositionDirection::Short => a.sort_by(|x, y| y.price.cmp(&x.price)), // bids descending + } + + let mut merged: Vec = Vec::with_capacity(a.len()); + for lvl in a.into_iter() { + 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); + } + merged + } + + #[test] + fn amm_get_levels_monotonic_and_terminal_clamp() { + let amm = AMM::default_test(); + + // Generate asks (for a long taker): expect non-decreasing prices + let asks = amm + .get_levels(10, PositionDirection::Long, PRICE_PRECISION_U64 * 120 / 100) + .unwrap(); + assert!(!asks.is_empty()); + assert!(is_monotonic(&asks, PositionDirection::Long)); + assert!(asks.iter().all(|l| l.base_asset_amount > 0)); + + // Terminal clamp: push terminal below current best ask and ensure it clamps + let best_ask = asks[0].price; + let clamped_terminal = best_ask.saturating_sub(1); + let asks_clamped = amm + .get_levels(5, PositionDirection::Long, clamped_terminal) + .unwrap(); + assert!(!asks_clamped.is_empty()); + assert!(asks_clamped.iter().all(|l| l.price <= clamped_terminal)); + assert_eq!(asks_clamped[0].price, clamped_terminal); // first level should clamp exactly + + // Generate bids (for a short taker): expect non-increasing prices + let bids = amm + .get_levels(10, PositionDirection::Short, PRICE_PRECISION_U64 * 80 / 100) + .unwrap(); + assert!(!bids.is_empty()); + assert!(is_monotonic(&bids, PositionDirection::Short)); + assert!(bids.iter().all(|l| l.base_asset_amount > 0)); + + // Terminal clamp: raise terminal above current best bid and ensure it clamps + let best_bid = bids[0].price; + let raised_terminal = best_bid.saturating_add(1); + let bids_clamped = amm + .get_levels(5, PositionDirection::Short, raised_terminal) + .unwrap(); + assert!(!bids_clamped.is_empty()); + assert!(bids_clamped.iter().all(|l| l.price >= raised_terminal)); + assert_eq!(bids_clamped[0].price, raised_terminal); + } +} + 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..46520bc0a4 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::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,107 @@ impl AMM { self.mm_oracle_slot = mm_oracle_slot; Ok(()) } + + pub fn get_levels( + &self, + levels: u8, + direction: PositionDirection, + terminal_price: u64, + ) -> DriftResult> { + // Determine total available liquidity on the chosen side + let (max_bids, max_asks) = amm::calculate_market_open_bids_asks(self)?; + let open_liquidity: u64 = match direction { + PositionDirection::Long => { + let v: i128 = max_bids.max(0); + v.min(u64::MAX as i128).cast()? + } + PositionDirection::Short => { + // asks side depth (stored negative) + let v: i128 = max_asks.unsigned_abs().cast()?; + v.min(u64::MAX as i128).cast()? + } + }; + + // If not enough liquidity, return empty + if open_liquidity < self.min_order_size.saturating_mul(2) { + return Ok(Vec::new()); + } + + let (mut base_reserve, mut quote_reserve) = match direction { + PositionDirection::Long => get_spread_reserves(self, PositionDirection::Short)?, + PositionDirection::Short => get_spread_reserves(self, PositionDirection::Long)?, + }; + + let swap_dir = match direction { + PositionDirection::Long => SwapDirection::Add, + PositionDirection::Short => SwapDirection::Remove, + }; + + let mut remaining = open_liquidity; + let mut out: Vec = Vec::with_capacity(levels as usize); + let total_levels = levels as u64; + + for i in 0..total_levels { + if remaining < self.order_step_size { + break; + } + + let remaining_levels = total_levels.saturating_sub(i); + let target = remaining.checked_div(remaining_levels.max(1)).unwrap_or(0); + if target == 0 { + break; + } + + let mut base_swap = standardize_base_asset_amount(target, self.order_step_size)?; + if base_swap == 0 { + break; + } + if base_swap > remaining { + base_swap = standardize_base_asset_amount(remaining, self.order_step_size)?; + if base_swap == 0 { + break; + } + } + + // Sim swap + let (new_quote_reserve, new_base_reserve) = + calculate_swap_output(base_swap.cast()?, 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(base_swap.cast()?)? + .cast()?; + + price = standardize_price(price, self.order_tick_size, direction)?; + + price = match direction { + PositionDirection::Long => price.min(terminal_price), + PositionDirection::Short => price.max(terminal_price), + }; + + out.push(Level { + price, + base_asset_amount: base_swap, + }); + + base_reserve = new_base_reserve; + quote_reserve = new_quote_reserve; + remaining = remaining.saturating_sub(base_swap); + + if out.len() as u64 >= total_levels { + break; + } + } + + Ok(out) + } } #[cfg(test)] From fd4d9203d8f425bd47beea82b3ffbca72f7249ac Mon Sep 17 00:00:00 2001 From: Nour Alharithi <14929853+moosecat2@users.noreply.github.com> Date: Mon, 20 Oct 2025 16:08:53 -0700 Subject: [PATCH 2/6] tests pass --- programs/drift/src/math/orders/tests.rs | 61 ++++------ programs/drift/src/state/perp_market.rs | 150 +++++++++++++++++++----- 2 files changed, 142 insertions(+), 69 deletions(-) diff --git a/programs/drift/src/math/orders/tests.rs b/programs/drift/src/math/orders/tests.rs index 302bf7e0b2..5f65272903 100644 --- a/programs/drift/src/math/orders/tests.rs +++ b/programs/drift/src/math/orders/tests.rs @@ -1061,13 +1061,9 @@ mod find_maker_orders { } } -// ----------------------------------------------------------------------------- -// AMM L2 generation and merging tests -// ----------------------------------------------------------------------------- -pub mod amm_l2_levels_and_merge { +pub mod amm_l2_levels { use crate::controller::position::PositionDirection; - use crate::math::constants::PRICE_PRECISION_U64; - use crate::math::orders::{estimate_price_from_side, Level}; + use crate::math::orders::Level; use crate::state::perp_market::AMM; fn is_monotonic(levels: &Vec, dir: PositionDirection) -> bool { @@ -1075,49 +1071,29 @@ pub mod amm_l2_levels_and_merge { return true; } match dir { - // Long taker consumes asks: prices should be non-decreasing with depth PositionDirection::Long => levels.windows(2).all(|w| w[0].price <= w[1].price), - // Short taker consumes bids: prices should be non-increasing with depth PositionDirection::Short => levels.windows(2).all(|w| w[0].price >= w[1].price), } } - fn merge_by_price(mut a: Vec, mut b: Vec, dir: PositionDirection) -> Vec { - // Combine and sort, then coalesce identical price levels - a.append(&mut b); - - match dir { - PositionDirection::Long => a.sort_by_key(|l| l.price), // asks ascending - PositionDirection::Short => a.sort_by(|x, y| y.price.cmp(&x.price)), // bids descending - } - - let mut merged: Vec = Vec::with_capacity(a.len()); - for lvl in a.into_iter() { - 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); - } - merged - } - #[test] fn amm_get_levels_monotonic_and_terminal_clamp() { - let amm = AMM::default_test(); + let amm = AMM::liquid_sol_test(); - // Generate asks (for a long taker): expect non-decreasing prices + // Asks monotonically increasing and greater than oracle price let asks = amm - .get_levels(10, PositionDirection::Long, PRICE_PRECISION_U64 * 120 / 100) + .get_levels( + 10, + PositionDirection::Long, + (amm.historical_oracle_data.last_oracle_price as u64) * 120 / 100, + ) .unwrap(); assert!(!asks.is_empty()); assert!(is_monotonic(&asks, PositionDirection::Long)); - assert!(asks.iter().all(|l| l.base_asset_amount > 0)); + assert!(asks.iter().all(|l| l.base_asset_amount > 0 + && l.price > amm.historical_oracle_data.last_oracle_price as u64)); - // Terminal clamp: push terminal below current best ask and ensure it clamps + // Test clamping at terminal price on ask side let best_ask = asks[0].price; let clamped_terminal = best_ask.saturating_sub(1); let asks_clamped = amm @@ -1127,15 +1103,20 @@ pub mod amm_l2_levels_and_merge { assert!(asks_clamped.iter().all(|l| l.price <= clamped_terminal)); assert_eq!(asks_clamped[0].price, clamped_terminal); // first level should clamp exactly - // Generate bids (for a short taker): expect non-increasing prices + // Bids monotonically decreasing and less than oracle price let bids = amm - .get_levels(10, PositionDirection::Short, PRICE_PRECISION_U64 * 80 / 100) + .get_levels( + 10, + PositionDirection::Short, + (amm.historical_oracle_data.last_oracle_price as u64) * 80 / 100, + ) .unwrap(); assert!(!bids.is_empty()); assert!(is_monotonic(&bids, PositionDirection::Short)); - assert!(bids.iter().all(|l| l.base_asset_amount > 0)); + assert!(bids.iter().all(|l| l.base_asset_amount > 0 + && l.price < amm.historical_oracle_data.last_oracle_price as u64)); - // Terminal clamp: raise terminal above current best bid and ensure it clamps + // Test clamping at terminal price on bid side let best_bid = bids[0].price; let raised_terminal = best_bid.saturating_add(1); let bids_clamped = amm diff --git a/programs/drift/src/state/perp_market.rs b/programs/drift/src/state/perp_market.rs index 46520bc0a4..f772765eeb 100644 --- a/programs/drift/src/state/perp_market.rs +++ b/programs/drift/src/state/perp_market.rs @@ -1709,49 +1709,40 @@ impl AMM { pub fn get_levels( &self, levels: u8, - direction: PositionDirection, + taker_direction: PositionDirection, terminal_price: u64, ) -> DriftResult> { - // Determine total available liquidity on the chosen side - let (max_bids, max_asks) = amm::calculate_market_open_bids_asks(self)?; - let open_liquidity: u64 = match direction { - PositionDirection::Long => { - let v: i128 = max_bids.max(0); - v.min(u64::MAX as i128).cast()? - } - PositionDirection::Short => { - // asks side depth (stored negative) - let v: i128 = max_asks.unsigned_abs().cast()?; - v.min(u64::MAX as i128).cast()? - } + let (mut base_reserve, mut quote_reserve) = match taker_direction { + PositionDirection::Long => get_spread_reserves(self, PositionDirection::Long)?, + PositionDirection::Short => get_spread_reserves(self, PositionDirection::Short)?, }; - // If not enough liquidity, return empty + let open_liquidity_u128: u128 = match taker_direction { + PositionDirection::Long => base_reserve.safe_sub(self.min_base_asset_reserve)?, + PositionDirection::Short => self.max_base_asset_reserve.safe_sub(base_reserve)?, + }; + 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 (mut base_reserve, mut quote_reserve) = match direction { - PositionDirection::Long => get_spread_reserves(self, PositionDirection::Short)?, - PositionDirection::Short => get_spread_reserves(self, PositionDirection::Long)?, - }; - - let swap_dir = match direction { - PositionDirection::Long => SwapDirection::Add, - PositionDirection::Short => SwapDirection::Remove, + let swap_dir = match taker_direction { + PositionDirection::Long => SwapDirection::Remove, + PositionDirection::Short => SwapDirection::Add, }; let mut remaining = open_liquidity; let mut out: Vec = Vec::with_capacity(levels as usize); - let total_levels = levels as u64; - for i in 0..total_levels { + for i in 0..levels { if remaining < self.order_step_size { break; } - let remaining_levels = total_levels.saturating_sub(i); - let target = remaining.checked_div(remaining_levels.max(1)).unwrap_or(0); + let remaining_levels = levels.saturating_sub(i); + let target = remaining + .checked_div(remaining_levels.max(1) as u64) + .unwrap_or(0); if target == 0 { break; } @@ -1783,9 +1774,9 @@ impl AMM { .safe_div(base_swap.cast()?)? .cast()?; - price = standardize_price(price, self.order_tick_size, direction)?; + price = standardize_price(price, self.order_tick_size, taker_direction)?; - price = match direction { + price = match taker_direction { PositionDirection::Long => price.min(terminal_price), PositionDirection::Short => price.max(terminal_price), }; @@ -1799,7 +1790,7 @@ impl AMM { quote_reserve = new_quote_reserve; remaining = remaining.saturating_sub(base_swap); - if out.len() as u64 >= total_levels { + if out.len() as u8 >= levels { break; } } @@ -1870,4 +1861,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() + } + } } From 6ecb34f28e7e5d09cde4050b2726f78de2d95ff2 Mon Sep 17 00:00:00 2001 From: Nour Alharithi <14929853+moosecat2@users.noreply.github.com> Date: Mon, 20 Oct 2025 16:31:16 -0700 Subject: [PATCH 3/6] open liquidity change --- programs/drift/src/instructions/keeper.rs | 4 ++-- programs/drift/src/state/perp_market.rs | 28 ++++++++++++++++------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index ff1eb1f79a..bd5d725fc6 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -2627,11 +2627,11 @@ pub fn handle_update_perp_bid_ask_twap<'c: 'info, 'info>( .cast::()?; let ask_terminal_price = match asks.last() { Some(level) => level.price, - None => oracle_price.saturating_mul(120).saturating_div(100), + None => oracle_price.saturating_mul(110).saturating_div(100), }; let bid_terminal_price = match bids.last() { Some(level) => level.price, - None => oracle_price.saturating_mul(80).saturating_div(100), + None => oracle_price.saturating_mul(90).saturating_div(100), }; let amm_bids = diff --git a/programs/drift/src/state/perp_market.rs b/programs/drift/src/state/perp_market.rs index f772765eeb..9297e8736e 100644 --- a/programs/drift/src/state/perp_market.rs +++ b/programs/drift/src/state/perp_market.rs @@ -1717,11 +1717,17 @@ impl AMM { PositionDirection::Short => get_spread_reserves(self, PositionDirection::Short)?, }; + 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 => base_reserve.safe_sub(self.min_base_asset_reserve)?, - PositionDirection::Short => self.max_base_asset_reserve.safe_sub(base_reserve)?, + 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()); } @@ -1747,15 +1753,21 @@ impl AMM { break; } - let mut base_swap = standardize_base_asset_amount(target, self.order_step_size)?; + let candidate = target.min(remaining); + let mut base_swap = standardize_base_asset_amount(candidate, self.order_step_size)?; if base_swap == 0 { break; } - if base_swap > remaining { - base_swap = standardize_base_asset_amount(remaining, self.order_step_size)?; - if base_swap == 0 { - break; - } + + let allowable: u128 = match taker_direction { + PositionDirection::Long => base_reserve.safe_sub(self.min_base_asset_reserve)?, + PositionDirection::Short => self.max_base_asset_reserve.safe_sub(base_reserve)?, + }; + + let cap = allowable.min(u64::MAX as u128).cast()?; + base_swap = base_swap.min(cap); + if base_swap == 0 { + break; } // Sim swap From 0783078b60afe68685f52e5b1b6a4b4b39fc00e4 Mon Sep 17 00:00:00 2001 From: Nour Alharithi <14929853+moosecat2@users.noreply.github.com> Date: Mon, 20 Oct 2025 21:33:59 -0700 Subject: [PATCH 4/6] sipmler logic --- programs/drift/src/instructions/keeper.rs | 32 +++++---- programs/drift/src/math/orders/tests.rs | 54 ++++++-------- programs/drift/src/state/perp_market.rs | 88 +++++++++++++---------- 3 files changed, 86 insertions(+), 88 deletions(-) diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index bd5d725fc6..4f53680b76 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -11,6 +11,7 @@ use solana_program::sysvar::instructions::{ self, load_current_index_checked, load_instruction_at_checked, ID as IX_ID, }; +use crate::controller::amm::SwapDirection; use crate::controller::insurance::update_user_stats_if_stake_amount; use crate::controller::liquidation::{ liquidate_spot_with_swap_begin, liquidate_spot_with_swap_end, @@ -28,7 +29,9 @@ use crate::ids::{ use crate::instructions::constraints::*; use crate::instructions::optional_accounts::get_revenue_share_escrow_account; use crate::instructions::optional_accounts::{load_maps, AccountMaps}; +use crate::math::amm_spread; use crate::math::casting::Cast; +use crate::math::constants::PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO; use crate::math::constants::QUOTE_SPOT_MARKET_INDEX; use crate::math::margin::get_margin_calculation_for_disable_high_leverage_mode; use crate::math::margin::{calculate_user_equity, meets_settle_pnl_maintenance_margin_requirement}; @@ -89,6 +92,8 @@ use crate::state::margin_calculation::MarginContext; use super::optional_accounts::get_high_leverage_mode_config; use super::optional_accounts::get_token_interface; +use crate::math::amm; + #[access_control( fill_not_paused(&ctx.accounts.state) )] @@ -2617,33 +2622,30 @@ pub fn handle_update_perp_bid_ask_twap<'c: 'info, 'info>( let depth = perp_market.get_market_depth_for_funding_rate()?; + 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) { - let oracle_price = mm_oracle_price_data - .get_safe_oracle_price_data() - .price - .cast::()?; - let ask_terminal_price = match asks.last() { - Some(level) => level.price, - None => oracle_price.saturating_mul(110).saturating_div(100), - }; - let bid_terminal_price = match bids.last() { - Some(level) => level.price, - None => oracle_price.saturating_mul(90).saturating_div(100), - }; - + let base_per_level = depth.safe_div(10)?; let amm_bids = perp_market .amm .clone() - .get_levels(16, PositionDirection::Short, bid_terminal_price)?; + .get_levels(16, PositionDirection::Short, base_per_level)?; let amm_asks = perp_market .amm .clone() - .get_levels(16, PositionDirection::Long, ask_terminal_price)?; + .get_levels(16, PositionDirection::Long, base_per_level)?; bids.extend(amm_bids); asks.extend(amm_asks); diff --git a/programs/drift/src/math/orders/tests.rs b/programs/drift/src/math/orders/tests.rs index 5f65272903..5b90f573fb 100644 --- a/programs/drift/src/math/orders/tests.rs +++ b/programs/drift/src/math/orders/tests.rs @@ -1080,51 +1080,37 @@ pub mod amm_l2_levels { 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, - (amm.historical_oracle_data.last_oracle_price as u64) * 120 / 100, - ) + .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 > amm.historical_oracle_data.last_oracle_price as u64)); - - // Test clamping at terminal price on ask side - let best_ask = asks[0].price; - let clamped_terminal = best_ask.saturating_sub(1); - let asks_clamped = amm - .get_levels(5, PositionDirection::Long, clamped_terminal) - .unwrap(); - assert!(!asks_clamped.is_empty()); - assert!(asks_clamped.iter().all(|l| l.price <= clamped_terminal)); - assert_eq!(asks_clamped[0].price, clamped_terminal); // first level should clamp exactly + + 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, - (amm.historical_oracle_data.last_oracle_price as u64) * 80 / 100, - ) + .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 < amm.historical_oracle_data.last_oracle_price as u64)); - - // Test clamping at terminal price on bid side - let best_bid = bids[0].price; - let raised_terminal = best_bid.saturating_add(1); - let bids_clamped = amm - .get_levels(5, PositionDirection::Short, raised_terminal) - .unwrap(); - assert!(!bids_clamped.is_empty()); - assert!(bids_clamped.iter().all(|l| l.price >= raised_terminal)); - assert_eq!(bids_clamped[0].price, raised_terminal); + assert!(bids + .iter() + .all(|l| l.base_asset_amount > 0 && l.price < bid_price)); + + println!("Bids: {:?}", bids); } } diff --git a/programs/drift/src/state/perp_market.rs b/programs/drift/src/state/perp_market.rs index 9297e8736e..8d5ab290da 100644 --- a/programs/drift/src/state/perp_market.rs +++ b/programs/drift/src/state/perp_market.rs @@ -1,6 +1,6 @@ use crate::controller::amm::SwapDirection; use crate::math::amm::{calculate_quote_asset_amount_swapped, calculate_swap_output}; -use crate::math::amm_spread::get_spread_reserves; +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; @@ -1710,12 +1710,9 @@ impl AMM { &self, levels: u8, taker_direction: PositionDirection, - terminal_price: u64, + base_swap_amount_per_level: u64, ) -> DriftResult> { - let (mut base_reserve, mut quote_reserve) = match taker_direction { - PositionDirection::Long => get_spread_reserves(self, PositionDirection::Long)?, - PositionDirection::Short => get_spread_reserves(self, PositionDirection::Short)?, - }; + 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, @@ -1738,41 +1735,26 @@ impl AMM { }; 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 i in 0..levels { + for _ in 0..levels { if remaining < self.order_step_size { break; } - let remaining_levels = levels.saturating_sub(i); - let target = remaining - .checked_div(remaining_levels.max(1) as u64) - .unwrap_or(0); - if target == 0 { - break; - } - - let candidate = target.min(remaining); - let mut base_swap = standardize_base_asset_amount(candidate, self.order_step_size)?; - if base_swap == 0 { - break; - } - - let allowable: u128 = match taker_direction { - PositionDirection::Long => base_reserve.safe_sub(self.min_base_asset_reserve)?, - PositionDirection::Short => self.max_base_asset_reserve.safe_sub(base_reserve)?, - }; - - let cap = allowable.min(u64::MAX as u128).cast()?; - base_swap = base_swap.min(cap); - if base_swap == 0 { + // Sim swap + let step_swap: u64 = standardized_base_swap.min(remaining); + if step_swap == 0 { break; } - // Sim swap let (new_quote_reserve, new_base_reserve) = - calculate_swap_output(base_swap.cast()?, base_reserve, swap_dir, self.sqrt_k)?; + calculate_swap_output(step_swap as u128, base_reserve, swap_dir, self.sqrt_k)?; let quote_swapped = calculate_quote_asset_amount_swapped( quote_reserve, @@ -1783,24 +1765,19 @@ impl AMM { let mut price: u64 = quote_swapped .safe_mul(PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO)? - .safe_div(base_swap.cast()?)? + .safe_div(step_swap.cast()?)? .cast()?; price = standardize_price(price, self.order_tick_size, taker_direction)?; - price = match taker_direction { - PositionDirection::Long => price.min(terminal_price), - PositionDirection::Short => price.max(terminal_price), - }; - out.push(Level { price, - base_asset_amount: base_swap, + base_asset_amount: step_swap, }); base_reserve = new_base_reserve; quote_reserve = new_quote_reserve; - remaining = remaining.saturating_sub(base_swap); + remaining = remaining.saturating_sub(step_swap); if out.len() as u8 >= levels { break; @@ -1809,6 +1786,39 @@ impl AMM { 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)] From b8e12141d57d035ba578c1c6dc3bd751301315e2 Mon Sep 17 00:00:00 2001 From: Nour Alharithi <14929853+moosecat2@users.noreply.github.com> Date: Mon, 20 Oct 2025 21:48:06 -0700 Subject: [PATCH 5/6] remove bba from update_mark_twap_crank --- programs/drift/src/math/amm.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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, }; From 49dad0da5b318078471bcedb004487313ab148ed Mon Sep 17 00:00:00 2001 From: Nour Alharithi <14929853+moosecat2@users.noreply.github.com> Date: Mon, 20 Oct 2025 21:49:58 -0700 Subject: [PATCH 6/6] include state being paused in twap crank criteria --- programs/drift/src/instructions/keeper.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index 4f53680b76..be8249559a 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -11,7 +11,6 @@ use solana_program::sysvar::instructions::{ self, load_current_index_checked, load_instruction_at_checked, ID as IX_ID, }; -use crate::controller::amm::SwapDirection; use crate::controller::insurance::update_user_stats_if_stake_amount; use crate::controller::liquidation::{ liquidate_spot_with_swap_begin, liquidate_spot_with_swap_end, @@ -29,9 +28,7 @@ use crate::ids::{ use crate::instructions::constraints::*; use crate::instructions::optional_accounts::get_revenue_share_escrow_account; use crate::instructions::optional_accounts::{load_maps, AccountMaps}; -use crate::math::amm_spread; use crate::math::casting::Cast; -use crate::math::constants::PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO; use crate::math::constants::QUOTE_SPOT_MARKET_INDEX; use crate::math::margin::get_margin_calculation_for_disable_high_leverage_mode; use crate::math::margin::{calculate_user_equity, meets_settle_pnl_maintenance_margin_requirement}; @@ -92,8 +89,6 @@ use crate::state::margin_calculation::MarginContext; use super::optional_accounts::get_high_leverage_mode_config; use super::optional_accounts::get_token_interface; -use crate::math::amm; - #[access_control( fill_not_paused(&ctx.accounts.state) )] @@ -2634,7 +2629,7 @@ pub fn handle_update_perp_bid_ask_twap<'c: 'info, 'info>( 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) { + if !perp_market.is_operation_paused(PerpOperation::AmmFill) && !state.amm_paused()? { let base_per_level = depth.safe_div(10)?; let amm_bids = perp_market