Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Switch the liquidity shape to a triangle #2

Merged
merged 3 commits into from
Feb 29, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
253 changes: 184 additions & 69 deletions packages/caviarnine-v1-adapter-v1/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ macro_rules! define_error {
define_error! {
RESOURCE_DOES_NOT_BELONG_ERROR
=> "One or more of the resources do not belong to pool.";
NO_ACTIVE_AMOUNTS_ERROR => "Pool has no active amounts.";
NO_PRICE_ERROR => "Pool has no price.";
OVERFLOW_ERROR => "Overflow error.";
}
Expand Down Expand Up @@ -249,88 +248,204 @@ pub mod adapter {
let SelectedTicks {
higher_ticks,
lower_ticks,
lowest_tick,
highest_tick,
..
} = SelectedTicks::select(
active_tick,
bin_span,
PREFERRED_TOTAL_NUMBER_OF_HIGHER_AND_LOWER_BINS,
);

// This comment was quickly going out of sync with the constant that
// is defined above of the number of bins to contribute to. Thus, to
// make this simple, let's say that the number of bins to contribute
// to is defined as `m` such that we're contributing to `m` bins to
// the left and `m` to the right.
// Calculating the value of liquidity of the left side (all Y) and
// the value of liquidity of the right side (all X). Note that the
// equations used below are all derived from the following quadric
// equation:
// (sqrt(pa) / sqrt(pb) - 1) * L^2 + (x*sqrt(pa) + y / sqrt(pb)) * L + xy = 0
// The equation for the left side can be derived by using the
// knowledge that it is entirely made up of Y and therefore X is
// zero. Similarly, we can derive the equation for the right side of
// liquidity by setting Y to zero.
//
// Determine the amount of resources that we will add to each of the
// bins. We have m on the left and m on the right. But, we also
// have the active bin that is composed of both x and y. So, this
// be like contributing to m.x and m.y bins where x = 1-y. X here
// is the percentage of resources x in the active bin.
let (amount_in_active_bin_x, amount_in_active_bin_y) =
pool.get_active_amounts().expect(NO_ACTIVE_AMOUNTS_ERROR);

let percentage_in_active_bin_x = amount_in_active_bin_x
.checked_mul(price)
.and_then(|value| {
value.checked_div(
amount_in_active_bin_x
.checked_mul(price)?
.checked_add(amount_in_active_bin_y)?,
)
})
.expect(OVERFLOW_ERROR);
let percentage_in_active_bin_y = Decimal::one()
.checked_sub(percentage_in_active_bin_x)
// We can also find the equations used below in the paper
// https://atiselsts.github.io/pdfs/uniswap-v3-liquidity-math.pdf
// which are equations 5 and 9.
//
// Lets refer to the equation that finds the left side of liquidity
// as Ly and to the one that finds the right side of liquidity as
// Lx. We will use those named in some of the comments that follow.
let current_price = price;
let lowest_price = tick_to_spot(lowest_tick).expect(OVERFLOW_ERROR);
let highest_price = highest_tick
.checked_add(bin_span)
.and_then(tick_to_spot)
.expect(OVERFLOW_ERROR);

// In here, we decide the amount x by the number of higher bins plus
// the percentage of the x in the currently active bin since the
// pool starting from the current price and upward is
// entirely composed of X. Similarly, we divide amount_y
// by the number of lower positions plus the percentage
// of y in the active bin since the pool starting from
// the current price and downward is composed just of y.
let position_amount_x = Decimal::from(higher_ticks.len() as u32)
.checked_add(percentage_in_active_bin_x)
.and_then(|value| amount_x.checked_div(value))
let current_price_sqrt =
current_price.checked_sqrt().expect(OVERFLOW_ERROR);
let lowest_price_sqrt =
lowest_price.checked_sqrt().expect(OVERFLOW_ERROR);
let highest_price_sqrt =
highest_price.checked_sqrt().expect(OVERFLOW_ERROR);

let liquidity = {
// This is equation 9 from the paper I shared above. Applied
// between the current price and the lowest price which is the
// range in which there is only Y.
let liquidity_y = current_price_sqrt
.checked_sub(lowest_price_sqrt)
.and_then(|sqrt_difference| {
amount_y.checked_div(sqrt_difference)
})
.expect(OVERFLOW_ERROR);

// This is equation 5 from the paper I shared above. Applied
// between the current price and the highest price which is the
// range in which there is only X.
let liquidity_x = amount_x
.checked_mul(current_price_sqrt)
.and_then(|value| value.checked_mul(highest_price_sqrt))
.and_then(|nominator| {
let denominator = highest_price_sqrt
.checked_sub(current_price_sqrt)?;

nominator.checked_div(denominator)
})
.expect(OVERFLOW_ERROR);

// We define the liquidity as the minimum of the X and Y
// liquidity such that the position is always balanced.
min(liquidity_x, liquidity_y)
};

// At this point, we have found the Lx and the Ly. This tells the
// liquidity value that should be in each of the bins. We now
// compute the exact amount that should go into each of the bins
// based on what's been calculated above. For this, we will derive
// an equation for x from Lx and an equation for y from Ly.

// The first one that we compute is how much should add to the
// currently active bin.
let (active_bin_amount_x, active_bin_amount_y) = {
let bin_lower_tick = active_tick;
let bin_higher_tick =
bin_lower_tick.checked_add(bin_span).expect(OVERFLOW_ERROR);

let bin_lower_price =
tick_to_spot(bin_lower_tick).expect(OVERFLOW_ERROR);
let bin_higher_price =
tick_to_spot(bin_higher_tick).expect(OVERFLOW_ERROR);

let bin_lower_price_sqrt =
bin_lower_price.checked_sqrt().expect(OVERFLOW_ERROR);
let bin_higher_price_sqrt =
bin_higher_price.checked_sqrt().expect(OVERFLOW_ERROR);

let amount_y = current_price_sqrt
.checked_sub(bin_lower_price_sqrt)
.and_then(|price_sqrt_difference| {
price_sqrt_difference.checked_mul(liquidity)
})
.expect(OVERFLOW_ERROR);

let amount_x = bin_higher_price_sqrt
.checked_sub(current_price_sqrt)
.and_then(|price_sqrt_difference| {
price_sqrt_difference.checked_mul(liquidity)
})
.and_then(|nominator| {
let denominator = current_price_sqrt
.checked_mul(bin_higher_price_sqrt)?;

nominator.checked_div(denominator)
})
.expect(OVERFLOW_ERROR);

(amount_x, amount_y)
};

let mut remaining_x = amount_x
.checked_sub(active_bin_amount_x)
.expect(OVERFLOW_ERROR);
let position_amount_y = Decimal::from(lower_ticks.len() as u32)
.checked_add(percentage_in_active_bin_y)
.and_then(|value| amount_y.checked_div(value))
let mut remaining_y = amount_y
.checked_sub(active_bin_amount_y)
.expect(OVERFLOW_ERROR);
let mut positions =
vec![(active_tick, active_bin_amount_x, active_bin_amount_y)];

// Finding the amount of Y to contribute to each one of the lower
// bins (contain only Y).
for bin_lower_tick in lower_ticks.iter().copied() {
let bin_higher_tick =
bin_lower_tick.checked_add(bin_span).expect(OVERFLOW_ERROR);

let bin_lower_price =
tick_to_spot(bin_lower_tick).expect(OVERFLOW_ERROR);
let bin_higher_price =
tick_to_spot(bin_higher_tick).expect(OVERFLOW_ERROR);

let bin_lower_price_sqrt =
bin_lower_price.checked_sqrt().expect(OVERFLOW_ERROR);
let bin_higher_price_sqrt =
bin_higher_price.checked_sqrt().expect(OVERFLOW_ERROR);

// Calculating the amount - we use min here so that if any loss
// of precision happens we do not end up exceeding the amount
// that we have in total.
let amount = min(
bin_higher_price_sqrt
.checked_sub(bin_lower_price_sqrt)
.and_then(|price_sqrt_difference| {
price_sqrt_difference.checked_mul(liquidity)
})
.expect(OVERFLOW_ERROR),
remaining_y,
);
remaining_y =
Copy link
Member

@iamyulong iamyulong Feb 27, 2024

Choose a reason for hiding this comment

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

Do we have some visualization for curve y = sqrt(tick_to_price(x + 1)) - sqrt(tick_to_price(x))? Trying to relate this to the triangular shape.

Copy link
Member Author

Choose a reason for hiding this comment

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

I have graphed it out here: https://www.desmos.com/calculator/krrqdfifeg and here https://docs.google.com/spreadsheets/d/1LiD_XfjvgXuGStU2KPWhMJglLYYnuy6zJq2fgvg84Mg/edit?usp=sharing

Let me provide more context on the triangle shape and why it was chosen: Caviar based their calculations around the fact that at every price point along an xy = k curve the k is equal and doesn't change. So, they started with this premise and started working backwards to determine how much should be added to each bin such that the k in each bin is equal. They also used the fact that all bins at a lower price than the current price contain only Y and all bins above the current price contain only X.

They chose an arbitrary K to be base their calculation on, let's say 500 and used the equations below to determine how much should each bin have:

$$ L = \sqrt{k} $$

$$ x = L \frac{ \sqrt{p_b} - \sqrt{p_a} }{ \sqrt{p_a} \times \sqrt{p_b} } $$

$$ y = L ( \sqrt{p_b} - \sqrt{p_a} ) $$

In the end, after calculating the amount they got that they should be a triangle.

I follow a very similar approach in the code here where the code doesn't actually say "get me a triangle formation", rather the code is contributing the amount of resources that we need to get an equal K in all of the bins and doesn't care what shape that gives us (but it results in a triangle as seen in the second link I shared above)

remaining_y.checked_sub(amount).expect(OVERFLOW_ERROR);

let position_amount_x_in_y =
position_amount_x.checked_mul(price).expect(OVERFLOW_ERROR);
let (position_amount_x, position_amount_y) =
if position_amount_x_in_y > position_amount_y {
let position_amount_y_in_x = position_amount_y
.checked_div(price)
.expect(OVERFLOW_ERROR);
(position_amount_y_in_x, position_amount_y)
} else {
(position_amount_x, position_amount_x_in_y)
};
positions.push((bin_lower_tick, dec!(0), amount));
}

let mut positions = vec![(
active_tick,
position_amount_x
.checked_mul(percentage_in_active_bin_x)
.expect(OVERFLOW_ERROR),
position_amount_y
.checked_mul(percentage_in_active_bin_y)
.expect(OVERFLOW_ERROR),
)];
positions.extend(
lower_ticks
.into_iter()
.map(|bin_id| (bin_id, dec!(0), position_amount_y)),
);
positions.extend(
higher_ticks
.into_iter()
.map(|bin_id| (bin_id, position_amount_x, dec!(0))),
);
// Finding the amount of X to contribute to each one of the higher
// bins (contain only X).
for bin_lower_tick in higher_ticks.iter().copied() {
let bin_higher_tick =
bin_lower_tick.checked_add(bin_span).expect(OVERFLOW_ERROR);

let bin_lower_price =
tick_to_spot(bin_lower_tick).expect(OVERFLOW_ERROR);
let bin_higher_price =
tick_to_spot(bin_higher_tick).expect(OVERFLOW_ERROR);

let bin_lower_price_sqrt =
bin_lower_price.checked_sqrt().expect(OVERFLOW_ERROR);
let bin_higher_price_sqrt =
bin_higher_price.checked_sqrt().expect(OVERFLOW_ERROR);

// Calculating the amount - we use min here so that if any loss
// of precision happens we do not end up exceeding the amount
// that we have in total.
let amount = min(
bin_higher_price_sqrt
.checked_sub(bin_lower_price_sqrt)
.and_then(|price_sqrt_difference| {
price_sqrt_difference.checked_mul(liquidity)
})
.and_then(|nominator| {
let denominator = bin_lower_price_sqrt
.checked_mul(bin_higher_price_sqrt)?;

nominator.checked_div(denominator)
})
.expect(OVERFLOW_ERROR),
Copy link
Member

Choose a reason for hiding this comment

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

Any documentation for the division above?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yup, this is derived from the equation that I named $L_x$ in the comments which is as follows:

$$ L_x = x\dfrac{\sqrt{p_a}\sqrt{p_b}}{\sqrt{p_b} - \sqrt{p_a}} $$

We can modify the equation above and use it to find $x$ which will be:

$$ x = L \frac{ \sqrt{p_b} - \sqrt{p_a} }{ \sqrt{p_a} \times \sqrt{p_b} } $$

You can also find these equations here as equations 4 and 5.

remaining_x,
);
remaining_x =
remaining_x.checked_sub(amount).expect(OVERFLOW_ERROR);

positions.push((bin_lower_tick, amount, dec!(0)));
}

let (receipt, change_x, change_y) =
pool.add_liquidity(bucket_x, bucket_y, positions);
Expand Down
7 changes: 7 additions & 0 deletions packages/caviarnine-v1-adapter-v1/src/tick_selector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ const MAXIMUM_TICK_VALUE: usize = 54000;
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SelectedTicks {
pub active_tick: u32,
pub lowest_tick: u32,
pub highest_tick: u32,
pub lower_ticks: Vec<u32>,
pub higher_ticks: Vec<u32>,
}
Expand Down Expand Up @@ -147,6 +149,8 @@ impl SelectedTicks {
active_tick,
higher_ticks: vec![],
lower_ticks: vec![],
lowest_tick: active_tick,
highest_tick: active_tick,
};

let mut remaining = preferred_total_number_of_higher_and_lower_ticks;
Expand Down Expand Up @@ -182,6 +186,9 @@ impl SelectedTicks {
}
}

selected_ticks.highest_tick = forward_counter.0;
selected_ticks.lowest_tick = backward_counter.0;

selected_ticks
}
}
Expand Down
Loading
Loading