Skip to content

Commit

Permalink
In/Out of market order protocol fee config (#2550)
Browse files Browse the repository at this point in the history
# Description
Separate protocol fees config for in and out of market orders.

## How to test
`protocol_fee.rs` tests work with in-market orders. Removing
`in_market_fee_policy_kind` makes the tests fail.


## Related Issues
Fixes #2548
  • Loading branch information
squadgazzz authored and fleupold committed Mar 26, 2024
1 parent 7bababd commit ac6580a
Show file tree
Hide file tree
Showing 6 changed files with 232 additions and 117 deletions.
107 changes: 61 additions & 46 deletions crates/autopilot/src/arguments.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use {
crate::{domain::fee::FeeFactor, infra},
anyhow::Context,
clap::ValueEnum,
primitive_types::{H160, U256},
shared::{
arguments::{display_list, display_option, ExternalSolver},
Expand Down Expand Up @@ -201,9 +202,9 @@ pub struct Arguments {
)]
pub solve_deadline: Duration,

/// Describes how the protocol fee should be calculated.
#[clap(flatten)]
pub fee_policy: FeePolicy,
/// Describes how the protocol fees should be calculated.
#[clap(long, env, use_value_delimiter = true)]
pub fee_policies: Vec<FeePolicy>,

/// Arguments for uploading information to S3.
#[clap(flatten)]
Expand Down Expand Up @@ -251,7 +252,7 @@ impl std::fmt::Display for Arguments {
score_cap,
shadow,
solve_deadline,
fee_policy,
fee_policies,
order_events_cleanup_interval,
order_events_cleanup_threshold,
db_url,
Expand Down Expand Up @@ -312,7 +313,7 @@ impl std::fmt::Display for Arguments {
writeln!(f, "score_cap: {}", score_cap)?;
display_option(f, "shadow", shadow)?;
writeln!(f, "solve_deadline: {:?}", solve_deadline)?;
writeln!(f, "fee_policy: {:?}", fee_policy)?;
writeln!(f, "fee_policies: {:?}", fee_policies)?;
writeln!(
f,
"order_events_cleanup_interval: {:?}",
Expand Down Expand Up @@ -340,32 +341,26 @@ impl std::fmt::Display for Arguments {
}
}

#[derive(clap::Parser, Debug, Clone)]
/// A fee policy to be used for orders base on it's class.
/// Examples:
/// - Surplus with a high enough cap for limit orders
/// surplus:0.5:0.9:limit
///
/// - Surplus with cap for market orders:
/// surplus:0.5:0.06:market
///
/// - Price improvement with a high enough cap for any order class
/// price_improvement:0.5:0.9:any
///
/// - Price improvement with cap for limit orders:
/// price_improvement:0.5:0.06:limit
///
/// - Volume based fee for any order class:
/// volume:0.1:any
#[derive(Debug, Clone)]
pub struct FeePolicy {
/// Type of fee policy to use. Examples:
///
/// - Surplus without cap
/// surplus:0.5:1.0
///
/// - Surplus with cap:
/// surplus:0.5:0.06
///
/// - Price improvement without cap:
/// price_improvement:0.5:1.0
///
/// - Price improvement with cap:
/// price_improvement:0.5:0.06
///
/// - Volume based:
/// volume:0.1
#[clap(long, env, default_value = "surplus:0.0:0.9")]
pub fee_policy_kind: FeePolicyKind,

/// Should protocol fees be collected or skipped for orders whose
/// limit price at order creation time suggests they can be immediately
/// filled.
#[clap(long, env, action = clap::ArgAction::Set, default_value = "true")]
pub fee_policy_skip_market_orders: bool,
pub fee_policy_order_class: FeePolicyOrderClass,
}

#[derive(clap::Parser, Debug, Clone)]
Expand All @@ -386,13 +381,23 @@ pub enum FeePolicyKind {
Volume { factor: FeeFactor },
}

impl FromStr for FeePolicyKind {
#[derive(clap::Parser, clap::ValueEnum, Clone, Debug)]
pub enum FeePolicyOrderClass {
/// If a fee policy needs to be applied to in-market orders.
Market,
/// If a fee policy needs to be applied to limit orders.
Limit,
/// If a fee policy needs to be applied regardless of the order class.
Any,
}

impl FromStr for FeePolicy {
type Err = anyhow::Error;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut parts = s.split(':');
let kind = parts.next().context("missing fee policy kind")?;
match kind {
let fee_policy_kind = match kind {
"surplus" => {
let factor = parts
.next()
Expand All @@ -404,7 +409,7 @@ impl FromStr for FeePolicyKind {
.context("missing max volume factor")?
.parse::<f64>()
.map_err(|e| anyhow::anyhow!("invalid max volume factor: {}", e))?;
Ok(Self::Surplus {
Ok(FeePolicyKind::Surplus {
factor: factor.try_into()?,
max_volume_factor: max_volume_factor.try_into()?,
})
Expand All @@ -422,7 +427,7 @@ impl FromStr for FeePolicyKind {
.map_err(|e| {
anyhow::anyhow!("invalid price improvement max volume factor: {}", e)
})?;
Ok(Self::PriceImprovement {
Ok(FeePolicyKind::PriceImprovement {
factor: factor.try_into()?,
max_volume_factor: max_volume_factor.try_into()?,
})
Expand All @@ -433,12 +438,22 @@ impl FromStr for FeePolicyKind {
.context("missing volume factor")?
.parse::<f64>()
.map_err(|e| anyhow::anyhow!("invalid volume factor: {}", e))?;
Ok(Self::Volume {
Ok(FeePolicyKind::Volume {
factor: factor.try_into()?,
})
}
_ => Err(anyhow::anyhow!("invalid fee policy kind: {}", kind)),
}
}?;
let fee_policy_order_class = FeePolicyOrderClass::from_str(
parts.next().context("missing fee policy order class")?,
true,
)
.map_err(|e| anyhow::anyhow!("invalid fee policy order class: {}", e))?;

Ok(FeePolicy {
fee_policy_kind,
fee_policy_order_class,
})
}
}

Expand All @@ -449,20 +464,20 @@ mod test {
#[test]
fn test_fee_factor_limits() {
let policies = vec![
"volume:1.0",
"volume:-1.0",
"surplus:1.0:0.5",
"surplus:0.5:1.0",
"surplus:0.5:-1.0",
"surplus:-1.0:0.5",
"priceImprovement:1.0:0.5",
"priceImprovement:-1.0:0.5",
"priceImprovement:0.5:1.0",
"priceImprovement:0.5:-1.0",
"volume:1.0:market",
"volume:-1.0:limit",
"surplus:1.0:0.5:any",
"surplus:0.5:1.0:limit",
"surplus:0.5:-1.0:market",
"surplus:-1.0:0.5:limit",
"priceImprovement:1.0:0.5:market",
"priceImprovement:-1.0:0.5:any",
"priceImprovement:0.5:1.0:market",
"priceImprovement:0.5:-1.0:limit",
];

for policy in policies {
assert!(FeePolicyKind::from_str(policy)
assert!(FeePolicy::from_str(policy)
.err()
.unwrap()
.to_string()
Expand Down
62 changes: 53 additions & 9 deletions crates/autopilot/src/domain/fee/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,43 @@ use {
std::str::FromStr,
};

enum OrderClass {
Market,
Limit,
Any,
}

impl From<arguments::FeePolicyOrderClass> for OrderClass {
fn from(value: arguments::FeePolicyOrderClass) -> Self {
match value {
arguments::FeePolicyOrderClass::Market => Self::Market,
arguments::FeePolicyOrderClass::Limit => Self::Limit,
arguments::FeePolicyOrderClass::Any => Self::Any,
}
}
}

/// Constructs fee policies based on the current configuration.
pub struct ProtocolFee {
policy: policy::Policy,
order_class: OrderClass,
}

impl ProtocolFee {
pub fn new(fee_policy_args: arguments::FeePolicy) -> Self {
Self {
policy: fee_policy_args.into(),
policy: fee_policy_args.fee_policy_kind.into(),
order_class: fee_policy_args.fee_policy_order_class.into(),
}
}

/// Converts an order from the boundary layer to the domain layer, applying
/// protocol fees if necessary.
pub fn apply(&self, order: boundary::Order, quote: &domain::Quote) -> domain::Order {
pub fn apply(
protocol_fees: &[ProtocolFee],
order: boundary::Order,
quote: &domain::Quote,
) -> domain::Order {
// If the partner fee is specified, it overwrites the current volume fee policy
if let Some(validated_app_data) = order
.metadata
Expand All @@ -55,13 +77,35 @@ impl ProtocolFee {
}
}

let protocol_fees = match &self.policy {
policy::Policy::Surplus(variant) => variant.apply(&order, quote),
policy::Policy::PriceImprovement(variant) => variant.apply(&order, quote),
policy::Policy::Volume(variant) => variant.apply(&order),
}
.into_iter()
.collect_vec();
let order_ = boundary::Amounts {
sell: order.data.sell_amount,
buy: order.data.buy_amount,
fee: order.data.fee_amount,
};
let quote_ = boundary::Amounts {
sell: quote.sell_amount,
buy: quote.buy_amount,
fee: quote.fee,
};
let protocol_fees = protocol_fees
.iter()
// TODO: support multiple fee policies
.find_map(|fee_policy| {
let outside_market_price = boundary::is_order_outside_market_price(&order_, &quote_);
match (outside_market_price, &fee_policy.order_class) {
(_, OrderClass::Any) => Some(&fee_policy.policy),
(true, OrderClass::Limit) => Some(&fee_policy.policy),
(false, OrderClass::Market) => Some(&fee_policy.policy),
_ => None,
}
})
.and_then(|policy| match policy {
policy::Policy::Surplus(variant) => variant.apply(&order),
policy::Policy::PriceImprovement(variant) => variant.apply(&order, quote),
policy::Policy::Volume(variant) => variant.apply(&order),
})
.into_iter()
.collect_vec();
boundary::order::to_domain(order, protocol_fees)
}
}
Expand Down
31 changes: 5 additions & 26 deletions crates/autopilot/src/domain/fee/policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ pub enum Policy {
pub struct Surplus {
factor: FeeFactor,
max_volume_factor: FeeFactor,
skip_market_orders: bool,
}

pub struct PriceImprovement {
Expand All @@ -25,16 +24,15 @@ pub struct Volume {
factor: FeeFactor,
}

impl From<arguments::FeePolicy> for Policy {
fn from(policy_arg: arguments::FeePolicy) -> Self {
match policy_arg.fee_policy_kind {
impl From<arguments::FeePolicyKind> for Policy {
fn from(policy_arg: arguments::FeePolicyKind) -> Self {
match policy_arg {
arguments::FeePolicyKind::Surplus {
factor,
max_volume_factor,
} => Policy::Surplus(Surplus {
factor,
max_volume_factor,
skip_market_orders: policy_arg.fee_policy_skip_market_orders,
}),
arguments::FeePolicyKind::PriceImprovement {
factor,
Expand All @@ -49,11 +47,7 @@ impl From<arguments::FeePolicy> for Policy {
}

impl Surplus {
pub fn apply(
&self,
order: &boundary::Order,
quote: &domain::Quote,
) -> Option<domain::fee::Policy> {
pub fn apply(&self, order: &boundary::Order) -> Option<domain::fee::Policy> {
match order.metadata.class {
boundary::OrderClass::Market => None,
boundary::OrderClass::Liquidity => None,
Expand All @@ -62,22 +56,7 @@ impl Surplus {
factor: self.factor,
max_volume_factor: self.max_volume_factor,
};
if !self.skip_market_orders {
Some(policy)
} else {
let order_ = boundary::Amounts {
sell: order.data.sell_amount,
buy: order.data.buy_amount,
fee: order.data.fee_amount,
};
let quote_ = boundary::Amounts {
sell: quote.sell_amount,
buy: quote.buy_amount,
fee: quote.fee,
};

boundary::is_order_outside_market_price(&order_, &quote_).then_some(policy)
}
Some(policy)
}
}
}
Expand Down
5 changes: 4 additions & 1 deletion crates/autopilot/src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -560,7 +560,10 @@ pub async fn run(args: Arguments) {
args.limit_order_price_factor
.try_into()
.expect("limit order price factor can't be converted to BigDecimal"),
domain::ProtocolFee::new(args.fee_policy.clone()),
args.fee_policies
.into_iter()
.map(domain::ProtocolFee::new)
.collect(),
);
solvable_orders_cache
.update(block)
Expand Down
8 changes: 4 additions & 4 deletions crates/autopilot/src/solvable_orders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ pub struct SolvableOrdersCache {
metrics: &'static Metrics,
weth: H160,
limit_order_price_factor: BigDecimal,
protocol_fee: domain::ProtocolFee,
protocol_fees: Vec<domain::ProtocolFee>,
}

type Balances = HashMap<Query, U256>;
Expand All @@ -104,7 +104,7 @@ impl SolvableOrdersCache {
update_interval: Duration,
weth: H160,
limit_order_price_factor: BigDecimal,
protocol_fee: domain::ProtocolFee,
protocol_fees: Vec<domain::ProtocolFee>,
) -> Arc<Self> {
let self_ = Arc::new(Self {
min_order_validity_period,
Expand All @@ -121,7 +121,7 @@ impl SolvableOrdersCache {
metrics: Metrics::instance(observe::metrics::get_storage_registry()).unwrap(),
weth,
limit_order_price_factor,
protocol_fee,
protocol_fees,
});
tokio::task::spawn(
update_task(Arc::downgrade(&self_), update_interval, current_block)
Expand Down Expand Up @@ -242,7 +242,7 @@ impl SolvableOrdersCache {
.into_iter()
.filter_map(|order| {
if let Some(quote) = db_solvable_orders.quotes.get(&order.metadata.uid.into()) {
Some(self.protocol_fee.apply(order, quote))
Some(domain::ProtocolFee::apply(&self.protocol_fees, order, quote))
} else {
tracing::warn!(order_uid = %order.metadata.uid, "order is skipped, quote is missing");
None
Expand Down
Loading

0 comments on commit ac6580a

Please sign in to comment.