Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 49 additions & 1 deletion programs/drift/src/instructions/keeper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Member

@crispheaney crispheaney Oct 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

technically this is average price, does that mess with things?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can get the worst case price by looking at base/quote reserves to get price of amm at end of swap

.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()? {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one thing i'd be worried about is if there are any weird cu issues here, should we think about a feature flag for this?

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<crate::math::orders::Level>| {
if side.is_empty() {
return;
}
let mut merged: Vec<crate::math::orders::Level> = 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)?;

Expand Down
4 changes: 2 additions & 2 deletions programs/drift/src/math/amm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,12 +114,12 @@ pub fn update_mark_twap_crank(
let (amm_bid_price, amm_ask_price) = amm.bid_ask_price(amm_reserve_price)?;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: we might be able to save CUs only getting bid/ask iff on of the best bid/ask are none


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,
};

Expand Down
2 changes: 1 addition & 1 deletion programs/drift/src/math/orders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
53 changes: 53 additions & 0 deletions programs/drift/src/math/orders/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Level>, 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;

Expand Down
220 changes: 220 additions & 0 deletions programs/drift/src/state/perp_market.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -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<Vec<Level>> {
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<Level> = 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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this redundant?

break;
}
}

Ok(out)
}

pub fn get_price_for_swap(
&self,
base_asset_amount: u64,
taker_direction: PositionDirection,
) -> DriftResult<u64> {
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)]
Expand Down Expand Up @@ -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()
}
}
}