Skip to content

Commit

Permalink
Finish limit order implementation (#3)
Browse files Browse the repository at this point in the history
* Finish limit order implementation

* Run cargo fmt

* Fix wrong interpretation of active_tick_index = None

Saturate instead of wrapping around when we're at the high tick bounds
  • Loading branch information
die-herdplatte authored Nov 21, 2024
1 parent dd060b3 commit 07977a9
Show file tree
Hide file tree
Showing 3 changed files with 275 additions and 43 deletions.
26 changes: 17 additions & 9 deletions src/quoting/base_pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::math::uint::U256;
use crate::quoting::types::{NodeKey, Pool, Quote, QuoteParams, Tick};
use crate::quoting::util::approximate_number_of_tick_spacings_crossed;
use alloc::vec::Vec;
use core::ops::Add;
use core::ops::{Add, AddAssign};
use num_traits::Zero;

// Resources consumed during any swap execution.
Expand All @@ -15,16 +15,20 @@ pub struct BasePoolResources {
pub tick_spacings_crossed: u32,
}

impl AddAssign for BasePoolResources {
fn add_assign(&mut self, rhs: Self) {
self.no_override_price_change += rhs.no_override_price_change;
self.initialized_ticks_crossed += rhs.initialized_ticks_crossed;
self.tick_spacings_crossed += rhs.tick_spacings_crossed;
}
}

impl Add for BasePoolResources {
type Output = Self;

fn add(self, rhs: Self) -> Self::Output {
BasePoolResources {
no_override_price_change: self.no_override_price_change + rhs.no_override_price_change,
initialized_ticks_crossed: self.initialized_ticks_crossed
+ rhs.initialized_ticks_crossed,
tick_spacings_crossed: self.tick_spacings_crossed + rhs.tick_spacings_crossed,
}
fn add(mut self, rhs: Self) -> Self::Output {
self += rhs;
self
}
}

Expand Down Expand Up @@ -288,7 +292,11 @@ impl Pool for BasePool {
};
}
} else {
active_tick_index = None
active_tick_index = if is_increasing {
self.sorted_ticks.len().checked_sub(1)
} else {
None
};
}
}

Expand Down
289 changes: 257 additions & 32 deletions src/quoting/limit_order_pool.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
use crate::math::swap::is_price_increasing;
use crate::math::tick::{to_sqrt_ratio, MAX_SQRT_RATIO, MIN_SQRT_RATIO};
use crate::math::uint::U256;
use crate::quoting::base_pool::{BasePool, BasePoolQuoteError, BasePoolResources, BasePoolState};
use crate::quoting::types::{NodeKey, Pool, Quote, QuoteParams, Tick};
use crate::quoting::util::find_nearest_initialized_tick_index;
use alloc::vec::Vec;
use core::ops::Add;
use num_traits::Zero;

use super::types::TokenAmount;
use super::util::approximate_number_of_tick_spacings_crossed;

#[derive(Clone, Copy)]
pub struct LimitOrderPoolState {
pub base_pool_state: BasePoolState,
// the maximum active tick index we've reached after a swap of token1 for token0
// if None, then we haven't seen any swaps from token1 to token0
// if Some(None), then we have swapped through all the ticks greater than the current active tick index
// if Some(sorted_ticks.len() - 1), then we have swapped through all the ticks greater than the current active tick index
pub max_tick_index_after_swap: Option<Option<usize>>,
// the minimum active tick index we've reached after a swap of token0 for token1
// if None, then we haven't seen any swaps from token0 to token1
Expand Down Expand Up @@ -43,6 +48,7 @@ pub struct LimitOrderPool {
}

pub const LIMIT_ORDER_TICK_SPACING: u32 = 128;
pub const DOUBLE_LIMIT_ORDER_TICK_SPACING: u32 = 2 * LIMIT_ORDER_TICK_SPACING;

impl LimitOrderPool {
pub fn new(
Expand Down Expand Up @@ -96,45 +102,234 @@ impl Pool for LimitOrderPool {
&self,
params: QuoteParams<Self::State, Self::Meta>,
) -> Result<Quote<Self::Resources, Self::State>, Self::QuoteError> {
if let Some(state) = params.override_state {
let increasing = is_price_increasing(
params.token_amount.amount,
params.token_amount.token == self.get_key().token1,
);
let initial_state = params.override_state.unwrap_or_else(|| self.get_state());

let mut calculated_amount = 0;
let mut consumed_amount = 0;
let mut fees_paid = 0;
let mut base_pool_resources = BasePoolResources::default();
let mut base_pool_state = initial_state.base_pool_state;

let is_increasing = is_price_increasing(
params.token_amount.amount,
params.token_amount.token == self.get_key().token1,
);

panic!("todo");
let next_unpulled_order_tick_index = if is_increasing {
initial_state.max_tick_index_after_swap
} else {
let result = self.base_pool.quote(QuoteParams {
sqrt_ratio_limit: params.sqrt_ratio_limit,
override_state: params.override_state.map(|s| s.base_pool_state),
initial_state.min_tick_index_after_swap
};

let sorted_ticks = self.base_pool.get_sorted_ticks();

if let Some(next_unpulled_order_tick_index) = next_unpulled_order_tick_index {
let active_tick_index = base_pool_state.active_tick_index;

let active_tick_sqrt_ratio_limit = if is_increasing {
active_tick_index
.map_or_else(|| sorted_ticks.first(), |idx| sorted_ticks.get(idx + 1))
.map_or(Ok(MAX_SQRT_RATIO), |next| {
to_sqrt_ratio(next.index).ok_or(BasePoolQuoteError::InvalidTick(next.index))
})
} else {
active_tick_index.map_or(Ok(MIN_SQRT_RATIO), |idx| {
let tick = sorted_ticks[idx]; // is always valid
to_sqrt_ratio(tick.index).ok_or(BasePoolQuoteError::InvalidTick(tick.index))
})
}?;

let params_sqrt_ratio_limit = params.sqrt_ratio_limit.unwrap_or(if is_increasing {
MAX_SQRT_RATIO
} else {
MIN_SQRT_RATIO
});

let active_tick_boundary_sqrt_ratio = if is_increasing {
Ord::min(active_tick_sqrt_ratio_limit, params_sqrt_ratio_limit)
} else {
Ord::max(active_tick_sqrt_ratio_limit, params_sqrt_ratio_limit)
};

// swap to (at most) the boundary of the current active tick
let quote_to_active_tick_boundary = self.base_pool.quote(QuoteParams {
sqrt_ratio_limit: Some(active_tick_boundary_sqrt_ratio),
token_amount: params.token_amount,
override_state: Some(base_pool_state),
meta: (),
})?;

let (min_tick_index_after_swap, max_tick_index_after_swap) =
if result.is_price_increasing {
(None, Some(result.state_after.active_tick_index))
calculated_amount += quote_to_active_tick_boundary.calculated_amount;
consumed_amount += quote_to_active_tick_boundary.consumed_amount;
fees_paid += quote_to_active_tick_boundary.fees_paid;
base_pool_resources += quote_to_active_tick_boundary.execution_resources;

base_pool_state = quote_to_active_tick_boundary.state_after;

let amount_remaining = params.token_amount.amount - consumed_amount;

// skip the range of pulled orders and start from the sqrt ratio of the next unpulled order if we have some amount remaining
if !amount_remaining.is_zero() {
let skip_starting_sqrt_ratio = if is_increasing {
let next_unpulled_order_sqrt_ratio = next_unpulled_order_tick_index.map_or(
Ok(MAX_SQRT_RATIO), // note that reaching this case implies that the pool has no initialized ticks
|idx| {
let tick_index = sorted_ticks[idx].index;
to_sqrt_ratio(tick_index)
.ok_or(BasePoolQuoteError::InvalidTick(tick_index))
},
)?;

next_unpulled_order_sqrt_ratio.clamp(
base_pool_state.sqrt_ratio, // for the case that next_unpulled_order_index refers to the starting tick index which we've already crossed
params_sqrt_ratio_limit,
)
} else {
(Some(result.state_after.active_tick_index), None)
let next_unpulled_order_sqrt_ratio =
next_unpulled_order_tick_index.map_or(Ok(MIN_SQRT_RATIO), |idx| {
sorted_ticks
.get(idx + 1)
.map(|tick| {
to_sqrt_ratio(tick.index)
.ok_or(BasePoolQuoteError::InvalidTick(tick.index))
})
.unwrap_or(Ok(MAX_SQRT_RATIO))
})?;

next_unpulled_order_sqrt_ratio.clamp(
params_sqrt_ratio_limit,
base_pool_state.sqrt_ratio, // for the case that next_unpulled_order_index refers to the starting tick index which we've already crossed
)
};

Ok(Quote {
calculated_amount: result.calculated_amount,
consumed_amount: result.consumed_amount,
execution_resources: LimitOrderPoolResources {
base_pool_resources: result.execution_resources,
// todo: calculate how many orders were pulled
orders_pulled: 0,
},
fees_paid: result.fees_paid,
is_price_increasing: result.is_price_increasing,
state_after: LimitOrderPoolState {
base_pool_state: result.state_after,
max_tick_index_after_swap,
min_tick_index_after_swap,
},
})
// account for the tick spacings of the uninitialized ticks that we will skip next
base_pool_resources.tick_spacings_crossed +=
approximate_number_of_tick_spacings_crossed(
base_pool_state.sqrt_ratio,
skip_starting_sqrt_ratio,
LIMIT_ORDER_TICK_SPACING,
);

let liquidity_at_next_unpulled_order_tick_index = {
let mut current_liquidity = base_pool_state.liquidity;
let mut current_active_tick_index = base_pool_state.active_tick_index;

// apply all liquidity_deltas in between the current active tick and the next unpulled order
loop {
let (next_tick_index, liquidity_delta) = if is_increasing
&& current_active_tick_index < next_unpulled_order_tick_index
{
let next_tick_index = current_active_tick_index.map_or(
0,
|idx| idx + 1, // valid index because next_unpulled_order_tick_index is larger than current_active_tick_index
);

(
Some(next_tick_index),
sorted_ticks[next_tick_index].liquidity_delta,
)
} else if !is_increasing
&& current_active_tick_index > next_unpulled_order_tick_index
{
let current_tick_index = current_active_tick_index.unwrap(); // safe because current_active_tick_index is larger than next_unpulled_order_tick_index

(
current_tick_index.checked_sub(1),
sorted_ticks[current_tick_index].liquidity_delta,
)
} else {
break;
};

current_active_tick_index = next_tick_index;

if liquidity_delta.is_positive() == is_increasing {
current_liquidity += liquidity_delta.unsigned_abs();
} else {
current_liquidity -= liquidity_delta.unsigned_abs();
};
}

current_liquidity
};

let quote_from_next_unpulled_order = self.base_pool.quote(QuoteParams {
sqrt_ratio_limit: params.sqrt_ratio_limit,
token_amount: TokenAmount {
amount: amount_remaining,
token: params.token_amount.token,
},
override_state: Some(BasePoolState {
active_tick_index: next_unpulled_order_tick_index,
sqrt_ratio: skip_starting_sqrt_ratio,
liquidity: liquidity_at_next_unpulled_order_tick_index,
}),
meta: (),
})?;

calculated_amount += quote_from_next_unpulled_order.calculated_amount;
consumed_amount += quote_from_next_unpulled_order.consumed_amount;
fees_paid += quote_from_next_unpulled_order.fees_paid;
base_pool_resources += quote_from_next_unpulled_order.execution_resources;

base_pool_state = quote_from_next_unpulled_order.state_after;
}
} else {
let quote_simple = self.base_pool.quote(QuoteParams {
sqrt_ratio_limit: params.sqrt_ratio_limit,
override_state: Some(initial_state.base_pool_state),
token_amount: params.token_amount,
meta: (),
})?;

calculated_amount += quote_simple.calculated_amount;
consumed_amount += quote_simple.consumed_amount;
fees_paid += quote_simple.fees_paid;
base_pool_resources += quote_simple.execution_resources;

base_pool_state = quote_simple.state_after;
}

let tick_index_after = base_pool_state.active_tick_index;

let (min_tick_index_after_swap, max_tick_index_after_swap) = if is_increasing {
(
initial_state.min_tick_index_after_swap,
Some(tick_index_after),
)
} else {
(
Some(tick_index_after),
initial_state.max_tick_index_after_swap,
)
};

let from_tick_index = next_unpulled_order_tick_index
.unwrap_or(initial_state.base_pool_state.active_tick_index);
let to_tick_index = if is_increasing {
Ord::max(from_tick_index, tick_index_after)
} else {
Ord::min(from_tick_index, tick_index_after)
};

let orders_pulled =
calculate_orders_pulled(from_tick_index, to_tick_index, is_increasing, sorted_ticks);

Ok(Quote {
calculated_amount,
consumed_amount,
execution_resources: LimitOrderPoolResources {
base_pool_resources,
orders_pulled,
},
fees_paid,
is_price_increasing: is_increasing,
state_after: LimitOrderPoolState {
base_pool_state,
min_tick_index_after_swap,
max_tick_index_after_swap,
},
})
}

fn has_liquidity(&self) -> bool {
Expand All @@ -150,6 +345,36 @@ impl Pool for LimitOrderPool {
}
}

fn calculate_orders_pulled(
from: Option<usize>,
to: Option<usize>,
is_increasing: bool,
sorted_ticks: &Vec<Tick>,
) -> u32 {
let mut current = from;
let mut orders_pulled = 0;

while current != to {
let crossed_tick = if is_increasing {
let crossed_tick = current.map_or(0, |idx| idx + 1);
current = Some(crossed_tick);
crossed_tick
} else {
let crossed_tick = current.unwrap();
current = crossed_tick.checked_sub(1);
crossed_tick
};

if !(sorted_ticks[crossed_tick].index.unsigned_abs() % DOUBLE_LIMIT_ORDER_TICK_SPACING)
.is_zero()
{
orders_pulled += 1;
}
}

orders_pulled
}

#[cfg(test)]
mod tests {
use crate::math::tick::to_sqrt_ratio;
Expand Down Expand Up @@ -195,10 +420,10 @@ mod tests {

assert_eq!(quote.fees_paid, 0);
assert_eq!(quote.state_after.min_tick_index_after_swap, None);
assert_eq!(quote.state_after.max_tick_index_after_swap, Some(None));
assert_eq!(quote.state_after.max_tick_index_after_swap, Some(Some(1)));
assert_eq!(quote.consumed_amount, 641);
assert_eq!(quote.calculated_amount, 639);
assert_eq!(quote.execution_resources.orders_pulled, 0);
assert_eq!(quote.execution_resources.orders_pulled, 1);
assert_eq!(
quote
.execution_resources
Expand Down
Loading

0 comments on commit 07977a9

Please sign in to comment.