-
Notifications
You must be signed in to change notification settings - Fork 89
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
Price competition bang for buck #2145
Changes from 3 commits
545af6a
8373d4d
0dac41d
6cf959e
96c6609
4e79635
a2cd56f
4a04960
ea9ade5
ee62e5c
2a9344d
87860b6
35c1ecf
f193435
e0878ac
b14aa6f
e629885
29afcbe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -50,20 +50,23 @@ type PriceEstimationStage<T> = Vec<(String, T)>; | |
/// stage Returns a price estimation early if there is a configurable number of | ||
/// successful estimates for every query or if all price sources returned an | ||
/// estimate. | ||
pub struct RacingCompetitionEstimator<T> { | ||
pub struct RacingCompetitionEstimator<T, N> { | ||
inner: Vec<PriceEstimationStage<T>>, | ||
successful_results_for_early_return: NonZeroUsize, | ||
native: N, | ||
} | ||
|
||
impl<T: Send + Sync + 'static> RacingCompetitionEstimator<T> { | ||
impl<T: Send + Sync + 'static, N: Send + Sync + 'static> RacingCompetitionEstimator<T, N> { | ||
pub fn new( | ||
inner: Vec<PriceEstimationStage<T>>, | ||
successful_results_for_early_return: NonZeroUsize, | ||
native: N, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: can be option to not use a Noop estimator and make the 1:1 exchange rate for the native token estimates explicit? |
||
) -> Self { | ||
assert!(!inner.is_empty()); | ||
Self { | ||
inner, | ||
successful_results_for_early_return, | ||
native, | ||
} | ||
} | ||
|
||
|
@@ -182,24 +185,37 @@ fn successes<R, E>(results: &[(EstimatorIndex, Result<R, E>)]) -> usize { | |
results.iter().filter(|(_, result)| result.is_ok()).count() | ||
} | ||
|
||
impl PriceEstimating for RacingCompetitionEstimator<Arc<dyn PriceEstimating>> { | ||
impl PriceEstimating | ||
for RacingCompetitionEstimator<Arc<dyn PriceEstimating>, Arc<dyn NativePriceEstimating>> | ||
{ | ||
fn estimate(&self, query: Arc<Query>) -> futures::future::BoxFuture<'_, PriceEstimateResult> { | ||
self.estimate_generic( | ||
query.clone(), | ||
query.kind, | ||
|estimator, query| estimator.estimate(query), | ||
move |a, b| { | ||
if is_second_quote_result_preferred(query.as_ref(), a, b) { | ||
Ordering::Less | ||
} else { | ||
Ordering::Greater | ||
} | ||
}, | ||
) | ||
async { | ||
let out_token = match query.kind { | ||
OrderKind::Buy => query.sell_token, | ||
OrderKind::Sell => query.buy_token, | ||
}; | ||
let native_price = self.native.estimate_native_price(out_token).await?; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we move this await into the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can do that. There are some notes, though:
But I guess since you are in favor of making the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I gave it a go and the inner future was written so generically that we can't get the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tried to contain the damage (i.e. additional complexity) the best I could. |
||
self.estimate_generic( | ||
query.clone(), | ||
query.kind, | ||
|estimator, query| estimator.estimate(query), | ||
move |a, b| { | ||
if is_second_quote_result_preferred(query.as_ref(), a, b, native_price) { | ||
Ordering::Less | ||
} else { | ||
Ordering::Greater | ||
} | ||
}, | ||
) | ||
.await | ||
} | ||
.boxed() | ||
} | ||
} | ||
|
||
impl NativePriceEstimating for RacingCompetitionEstimator<Arc<dyn NativePriceEstimating>> { | ||
impl<N: Send + Sync + 'static> NativePriceEstimating | ||
for RacingCompetitionEstimator<Arc<dyn NativePriceEstimating>, N> | ||
{ | ||
fn estimate_native_price( | ||
&self, | ||
token: H160, | ||
|
@@ -221,22 +237,24 @@ impl NativePriceEstimating for RacingCompetitionEstimator<Arc<dyn NativePriceEst | |
|
||
/// Price estimator that pulls estimates from various sources | ||
/// and competes on the best price. | ||
pub struct CompetitionEstimator<T> { | ||
inner: RacingCompetitionEstimator<T>, | ||
pub struct CompetitionEstimator<T, N> { | ||
inner: RacingCompetitionEstimator<T, N>, | ||
} | ||
|
||
impl<T: Send + Sync + 'static> CompetitionEstimator<T> { | ||
pub fn new(inner: Vec<Vec<(String, T)>>) -> Self { | ||
impl<T: Send + Sync + 'static, N: Send + Sync + 'static> CompetitionEstimator<T, N> { | ||
pub fn new(inner: Vec<Vec<(String, T)>>, native: N) -> Self { | ||
let number_of_estimators = | ||
NonZeroUsize::new(inner.iter().fold(0, |sum, stage| sum + stage.len())) | ||
.expect("Vec of estimators should not be empty."); | ||
Self { | ||
inner: RacingCompetitionEstimator::new(inner, number_of_estimators), | ||
inner: RacingCompetitionEstimator::new(inner, number_of_estimators, native), | ||
} | ||
} | ||
} | ||
|
||
impl PriceEstimating for CompetitionEstimator<Arc<dyn PriceEstimating>> { | ||
impl PriceEstimating | ||
for CompetitionEstimator<Arc<dyn PriceEstimating>, Arc<dyn NativePriceEstimating>> | ||
{ | ||
fn estimate(&self, query: Arc<Query>) -> futures::future::BoxFuture<'_, PriceEstimateResult> { | ||
self.inner.estimate(query) | ||
} | ||
|
@@ -246,9 +264,10 @@ fn is_second_quote_result_preferred( | |
query: &Query, | ||
a: &PriceEstimateResult, | ||
b: &PriceEstimateResult, | ||
native_price: f64, | ||
) -> bool { | ||
match (a, b) { | ||
(Ok(a), Ok(b)) => is_second_estimate_preferred(query, a, b), | ||
(Ok(a), Ok(b)) => is_second_estimate_preferred(query, a, b, native_price), | ||
(Ok(_), Err(_)) => false, | ||
(Err(_), Ok(_)) => true, | ||
(Err(a), Err(b)) => is_second_error_preferred(a, b), | ||
|
@@ -267,10 +286,16 @@ fn is_second_native_result_preferred( | |
} | ||
} | ||
|
||
fn is_second_estimate_preferred(query: &Query, a: &Estimate, b: &Estimate) -> bool { | ||
fn is_second_estimate_preferred( | ||
query: &Query, | ||
a: &Estimate, | ||
b: &Estimate, | ||
native_price: f64, | ||
) -> bool { | ||
let amount_out = |estimate: &Estimate| estimate.out_amount_in_eth(native_price); | ||
match query.kind { | ||
OrderKind::Buy => b.out_amount < a.out_amount, | ||
OrderKind::Sell => a.out_amount < b.out_amount, | ||
OrderKind::Buy => amount_out(b) < amount_out(a), | ||
OrderKind::Sell => amount_out(a) < amount_out(b), | ||
} | ||
} | ||
|
||
|
@@ -321,7 +346,7 @@ fn metrics() -> &'static Metrics { | |
mod tests { | ||
use { | ||
super::*, | ||
crate::price_estimation::MockPriceEstimating, | ||
crate::price_estimation::{native::MockNativePriceEstimating, MockPriceEstimating}, | ||
anyhow::anyhow, | ||
futures::channel::oneshot::channel, | ||
model::order::OrderKind, | ||
|
@@ -331,6 +356,16 @@ mod tests { | |
tokio::time::sleep, | ||
}; | ||
|
||
/// Builds native price estimator that returns the same native price for any | ||
/// token. | ||
fn mock_native_estimator() -> Arc<dyn NativePriceEstimating> { | ||
let mut estimator = MockNativePriceEstimating::new(); | ||
estimator | ||
.expect_estimate_native_price() | ||
.returning(|_token| async { Ok(1.) }.boxed()); | ||
Arc::new(estimator) | ||
} | ||
|
||
#[tokio::test] | ||
async fn works() { | ||
let queries = [ | ||
|
@@ -418,11 +453,13 @@ mod tests { | |
}), | ||
]); | ||
|
||
let priority: CompetitionEstimator<Arc<dyn PriceEstimating>> = | ||
CompetitionEstimator::new(vec![vec![ | ||
let priority: CompetitionEstimator<Arc<dyn PriceEstimating>, _> = CompetitionEstimator::new( | ||
vec![vec![ | ||
("first".to_owned(), Arc::new(first)), | ||
("second".to_owned(), Arc::new(second)), | ||
]]); | ||
]], | ||
mock_native_estimator(), | ||
); | ||
|
||
let result = priority.estimate(queries[0].clone()).await; | ||
assert_eq!(result.as_ref().unwrap(), &estimates[0]); | ||
|
@@ -497,14 +534,15 @@ mod tests { | |
.boxed() | ||
}); | ||
|
||
let racing: RacingCompetitionEstimator<Arc<dyn PriceEstimating>> = | ||
let racing: RacingCompetitionEstimator<Arc<dyn PriceEstimating>, _> = | ||
RacingCompetitionEstimator::new( | ||
vec![vec![ | ||
("first".to_owned(), Arc::new(first)), | ||
("second".to_owned(), Arc::new(second)), | ||
("third".to_owned(), Arc::new(third)), | ||
]], | ||
NonZeroUsize::new(1).unwrap(), | ||
mock_native_estimator(), | ||
); | ||
|
||
let result = racing.estimate(query).await; | ||
|
@@ -567,7 +605,7 @@ mod tests { | |
.boxed() | ||
}); | ||
|
||
let racing: RacingCompetitionEstimator<Arc<dyn PriceEstimating>> = | ||
let racing: RacingCompetitionEstimator<Arc<dyn PriceEstimating>, _> = | ||
RacingCompetitionEstimator::new( | ||
vec![ | ||
vec![ | ||
|
@@ -580,6 +618,7 @@ mod tests { | |
], | ||
], | ||
NonZeroUsize::new(2).unwrap(), | ||
mock_native_estimator(), | ||
); | ||
|
||
let result = racing.estimate(query).await; | ||
|
@@ -638,16 +677,19 @@ mod tests { | |
let mut fourth = MockPriceEstimating::new(); | ||
fourth.expect_estimate().never(); | ||
|
||
let racing: RacingCompetitionEstimator<Arc<dyn PriceEstimating>> = | ||
RacingCompetitionEstimator { | ||
inner: vec![ | ||
vec![("first".to_owned(), Arc::new(first))], | ||
vec![("second".to_owned(), Arc::new(second))], | ||
vec![("third".to_owned(), Arc::new(third))], | ||
vec![("fourth".to_owned(), Arc::new(fourth))], | ||
], | ||
successful_results_for_early_return: NonZeroUsize::new(2).unwrap(), | ||
}; | ||
let racing: RacingCompetitionEstimator< | ||
Arc<dyn PriceEstimating>, | ||
Arc<dyn NativePriceEstimating>, | ||
> = RacingCompetitionEstimator { | ||
inner: vec![ | ||
vec![("first".to_owned(), Arc::new(first))], | ||
vec![("second".to_owned(), Arc::new(second))], | ||
vec![("third".to_owned(), Arc::new(third))], | ||
vec![("fourth".to_owned(), Arc::new(fourth))], | ||
], | ||
successful_results_for_early_return: NonZeroUsize::new(2).unwrap(), | ||
native: mock_native_estimator(), | ||
}; | ||
|
||
racing.estimate(query).await.unwrap(); | ||
} | ||
|
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'm afraid this is the gas amount not the actual gas cost in ETH 🙈 (the new types in the driver/solver engine are really helpful to avoid this confusion).
services/crates/shared/src/order_quoting.rs
Lines 577 to 581 in 01e1c51
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.
Makes sense. Will add a gas price estimator to get the actual gas used. 👍