-
Notifications
You must be signed in to change notification settings - Fork 75
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
Limit order support in driver #713
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR is not done yet but because prices are tricky I wanted to get a few eyes on the implementation to avoid spending time on writing tests for incorrect math.
Also I'm not sure where (if at all) I have to take buy
vs sell
orders into account.
188122f
to
7450e81
Compare
/// `compute_synthetic_order_amounts_if_limit_order()`). | ||
/// Returns an error if the UCP doesn't contain the `sell_token` or if under- or overflows | ||
/// happen during the computation. | ||
fn custom_price_for_limit_order(&self, order: &Order) -> Result<U256> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just noticed that this function name might get confusing together with compute_limit_buy_price()
(used for liquidity orders). I'm open for naming suggestions to make the distinction clearer.
// But actually this is the equation the solution has to satisfy: | ||
// uniform_sell_price * sell_amount = custom_buy_price * buy_amount | ||
// That means we can solve for the custom_buy_price like this: | ||
// custom_buy_price = (uniform_sell_price * sell_amount) / buy_amount |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Something feels off here since the the buy price is independent of the uniform_buy_price
...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this make more sense?
- Use UCP buy and sell price to compute the actual
buy_amount
adjusted for thefee_surplus
(it should be lower) - Use the existing formula but with the new
reduced_buy_amount
custom_buy_price = (uniform_sell_price * sell_amount) / reduced_buy_amount
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, and I think you also need to account for order side (sell/buy). I.e., compute the buy_amount for sell orders, and sell_amount for buy orders.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@harisang... do you mind double-checking our logic here 😅
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is how I understand the problem after the last discussion:
fn custom_price_for_limit_order(&self, order: &Order) -> Result<U256> {
let uniform_buy_price = self
.clearing_prices
.get(&order.data.buy_token)
.context("buy token price is missing")?;
let uniform_sell_price = self
.clearing_prices
.get(&order.data.sell_token)
.context("sell token price is missing")?;
let (sell_amount, buy_amount) = match order.data.kind {
// This means sell as much `sell_token` as needed to buy exactly the expected
// `buy_amount`. Therefore we need to solve for `sell_amount`.
OrderKind::Buy => {
let sell_amount = order.data.buy_amount * uniform_buy_price / uniform_sell_price;
// We have to sell slightly more `sell_token` to capture the `surplus_fee`
let sell_amount_adjusted_for_fees = sell_amount + order.metadata.surplus_fee;
(sell_amount_adjusted_for_fees, order.data.buy_amount)
}
// This means sell ALL the `sell_token` and get as much `buy_token` as possible.
// Therefore we need to solve for `buy_amount`.
OrderKind::Sell => {
// Solver actually used this `sell_amount` to compute prices.
let sell_amount = order.data.sell_amount - order.metadata.surplus_fee;
let buy_amount = sell_amount * uniform_sell_price / uniform_buy_price;
(order.data.sell_amount, buy_amount)
}
};
let custom_buy_price = sell_amount * uniform_sell_price / buy_amount;
Ok(custom_buy_price)
}
I didn't use checked math operations to keep it succinct for this comment.
Also in the second to last line I still have to add order.data.fee_amount
in case we want to use a non 0 fee at some point in the future, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks correct to me now.
But... its price related so 🤷.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did some napkin math and the equations look correct to me ✅.
dbadb44
to
836b7f7
Compare
647bf63
to
12387d4
Compare
Co-authored-by: Nicholas Rodrigues Lordello <[email protected]>
Sorry, just got to see this. Will try to catch up and review this. Also, is terminology defined anywhere (e.g., some issue in the repo, or notion etc?) |
I tried to implement what is described in this notion page. But I didn't consciously try to adhere to the terminology used there so it might not be that helpful. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!
I'm not super confident about custom_price_for_limit_order
(as it confuses me as well), but the rest looks good! Maybe a unit test that verifies that the additional prices for limit orders is correct would be good?
.checked_add(order.data.fee_amount) | ||
.context("surplus_fee adjustment would overflow sell_amount")? | ||
.checked_sub(order.metadata.surplus_fee) | ||
.context("surplus_fee adjustment would underflow sell_amount")?; | ||
return Ok((sell_amount, order.metadata.surplus_fee)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It makes sense that surplus_fee is subtracted from sell_amount. But why is fee_amount added? Maybe was explained in the discussion on Github but when I read this code here it is unclear.
Naively I'd think that surplus_fee already takes into account that the order was a real fee too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Naively I'd think that surplus_fee already takes into account that the order was a real fee too.
Then it would need to be a signed amount to account for setting a too high fee_amount
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Basically, surplus_fee
is the exact fee amount that needs to be taken for the order for that auction (regardless of what the actually set fee_amount
is). This leads to the following system of equations:
(1)
synthetic_order.fee_amount = surplus_fee
(2)
synthetic_order.sell_amount + synthetic_order.fee_amount
= order.sell_amount + order.fee_amount
// Solving for synthetic_order.sell_amount:
(3)
synthetic_order.sell_amount
= order.sell_amount + order.fee_amount - surplus_fee
For limit orders, you can imagine they are an order without pro-rated fee. Because the settlement contract always transfers in order.sell_amount + order.fee_amount
, we need to account for non-0 fee amounts here.
/// `compute_synthetic_order_amounts_if_limit_order()`). | ||
/// Returns an error if the UCP doesn't contain the traded tokens or if under- or overflows | ||
/// happen during the computation. | ||
fn custom_price_for_limit_order(&self, order: &Order) -> Result<U256> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe to make this easier to test we can pass in the uniform prices you get from self
so that we have a freestanding function. Then the standalone tests also makes the behavior easier to understand and can be combined with compute_synthetic_order_amounts_if_limit_order
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@vkgnosis This is the exact thing I'm trying to argue against 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a saying that "testable code == clean code" and this is true in some cases and false in others. This is one case where it's false. I see this elsewhere in the codebase as well where functions feel like they should be methods, and they accept a bunch of fields from self
, but they were written as functions just so they could be tested.
This is testing internal APIs (! I keep saying that), which makes them harder to change. Not only that, but the code is also more awkward because the reader wonders "huh, why isn't this a method...?"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I recognize it's something we disagree on.
@harisang I added a few unit tests. Could you please check if they make sense to you. |
}, | ||
encoder.custom_price_trades[0] | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code looks good but I did not think about the math in detail.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some nits/code quality comments. The code does make sense to me with my limited domain knowledge.
@@ -71,14 +74,32 @@ struct OrderSettlementHandler { | |||
scaled_unsubsidized_fee_amount: U256, | |||
} | |||
|
|||
/// Returns (`sell_amount`, `fee_amount`) for the given order and adjusts the values accordingly | |||
/// for limit orders. | |||
fn compute_synthetic_order_amounts_if_limit_order(order: &Order) -> Result<(U256, U256)> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe, though I'm still not a fan of this name
fn compute_synthetic_order_amounts_if_limit_order(order: &Order) -> Result<(U256, U256)> { | |
fn compute_synthetic_order_amounts_for_limit_order(order: &Order) -> Result<(U256, U256)> { |
The confusing part is that "limit order" now might refer to either LimitOrder
or Order
with OrderClass::Limit
. 😕
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It'd be better if the if
could be hoisted out of the function. Generally if you have a function named do_a_if_b
it's better to split into two functions, do_a
and b
:
if b() {
do_a();
}
let sell_amount = order | ||
.data | ||
.sell_amount | ||
.checked_add(order.data.fee_amount) | ||
.context("surplus_fee adjustment would overflow sell_amount")? | ||
.checked_sub(order.metadata.surplus_fee) | ||
.context("surplus_fee adjustment would underflow sell_amount")?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How common will it be for surplus_fee
to be higher than sell_amount
? I imagine it could happen but I'm not sure if such an order even makes sense. The way this code is written now, such an order would result in an error here, but I'm not sure how that error is handled. Is it logged as an error?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would be surprised if that happened often. If it does we will get an alert and the order will be omitted from the auction.
@@ -195,13 +195,89 @@ impl SettlementEncoder { | |||
)? | |||
} | |||
OrderClass::Limit => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This branch doesn't use the executed_amount
parameter. That's pointing to a mistake in how this code is organized. Not necessarily on you, but this needs to be refactored.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comes from the fact that the executed_amount
is kind of meaningless for this order type because solvers can't know the actual executed amount. After all we gave them a synthetic order with adjusted sell_amount
.
We could assert that the solver at least used the fake sell_amount
we gave them but that wouldn't help much.
The two tests look good to me! |
Fixes #705
To not require any special handling on the solver side to settle limit orders we have to manage them a bit under the hood:
LimitOrder
type in the driver we deduct thesurplus_fee
from thesell_amount
in theOrderConverter
OrderClass::Ordinary
orderbuy_price
which takes the originalsell_amount
into account (SettlementEncoder
)Test Plan
Added unit tests for
OrderConverter
and price adjustment logic inSettlementEncoder