Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
53 changes: 52 additions & 1 deletion programs/drift/src/instructions/keeper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<u64>()?;
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 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<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
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
67 changes: 67 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,73 @@ 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();

// 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,
)
.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

// 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,
)
.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);
}
}

mod calculate_max_spot_order_size {
use std::str::FromStr;

Expand Down
210 changes: 210 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::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,110 @@ impl AMM {
self.mm_oracle_slot = mm_oracle_slot;
Ok(())
}

pub fn get_levels(
&self,
levels: u8,
taker_direction: PositionDirection,
terminal_price: u64,
) -> DriftResult<Vec<Level>> {
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 (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 mut out: Vec<Level> = Vec::with_capacity(levels as usize);

for i 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 {
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, 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_reserve = new_base_reserve;
quote_reserve = new_quote_reserve;
remaining = remaining.saturating_sub(base_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)
}
}

#[cfg(test)]
Expand Down Expand Up @@ -1764,4 +1873,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()
}
}
}