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

Price improvemenet fee policy autopilot domain #2378

Merged
merged 16 commits into from
Feb 12, 2024
Merged

Conversation

squadgazzz
Copy link
Contributor

Description

Closes task n2 from #2287

@squadgazzz squadgazzz changed the base branch from main to 2287/autopilot-argument February 6, 2024 18:29
@squadgazzz squadgazzz marked this pull request as ready for review February 6, 2024 18:29
@squadgazzz squadgazzz requested a review from a team as a code owner February 6, 2024 18:29
@squadgazzz squadgazzz changed the title 2287/autopilot domain Price improvemenet fee policy autopilot domain Feb 6, 2024
Copy link
Contributor

@fleupold fleupold left a comment

Choose a reason for hiding this comment

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

I'm still not entirely happy about the factoring here (it seems somewhat convoluted and not very straightforward to me).

Here is my mental model:

  1. As far as the domain is concerned there is one component responsible for all logic of how fees are applied to orders (fee.apply(order, quote) -> OrderWithFees)
  2. This fee component itself can apply many different types of fee (policies) to a single order (UI integration fee, price improvement fee, etc) based on what the order specifies (ie in its AppData, how much CoW tokens the user holds, etc.) and what the autopilot has been configured with.
  3. For this, each fee policy (builder) can apply itself to an order policy.with(order) -> Option<Fee>
  4. The fee component collects all fees into the order and returns it.

I'm not entirely sure if factoring 3 out into a builder is overkill (we can remove it for now), but I'm worried this logic will explode in complexity as soon as we add more advanced fees (like a volume fee that is specified by the interface or fee discounts based on your CoW token holdings)

crates/autopilot/src/domain/fee/mod.rs Outdated Show resolved Hide resolved
boundary::OrderClass::Market => {
if self.fee_policy_skip_market_orders {
vec![]
} else {
vec![self.policy]
vec![self.policy_builder.build_with(quote)]
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't the information whether or not to apply a fee for market order be part of the fee policy (builder) and not part of this component?

@squadgazzz
Copy link
Contributor Author

squadgazzz commented Feb 7, 2024

Not fully following the intention and how the current structure should be changed.

I'm still not entirely happy about the factoring here (it seems somewhat convoluted and not very straightforward to me).

Here is my mental model:

  1. As far as the domain is concerned there is one component responsible for all logic of how fees are applied to orders (fee.apply(order, quote) -> OrderWithFees)

I thought that domain::fee::ProtocolFee is that component which is serving the intended functionality.

  1. This fee component itself can apply many different types of fee (policies) to a single order (UI integration fee, price improvement fee, etc) based on what the order specifies (ie in its AppData, how much CoW tokens the user holds, etc.) and what the autopilot has been configured with.

ProtocolFee::apply function does something similar, doesn't it?

  1. For this, each fee policy (builder) can apply itself to an order policy.with(order) -> Option<Fee>
  2. The fee component collects all fees into the order and returns it.

Made an attempt(c519fb8) to follow this logic. Not sure it addresses all the intentions.

Base automatically changed from 2287/autopilot-argument to main February 7, 2024 13:39
Copy link
Contributor

@fleupold fleupold left a comment

Choose a reason for hiding this comment

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

Not fully following the intention and how the current structure should be changed.

Right, I realize that I asked for a bunch of refactorings and am now not happy with it (not really constructive review performance on my end). Feel free to propose something completely different (I just feel we now have a very complex control flow for not even yet very complex fee models, but those are around the corner).

As long as we keep the details private to the domain::fee component we can also refine it later.

policy: Policy,
fee_policy_skip_market_orders: bool,
policy_builder: PolicyBuilder,
skip_market_orders: bool,
Copy link
Contributor

Choose a reason for hiding this comment

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

This boolean is bothering me here. Skip market orders is something specific to the Surplus fee (we currently charge a surplus fee but skip market orders) and should thus be specified there and considered when we are trying to apply a Surplus FeePolicy to an order.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So, should this function be also changed? Currently, it checks for this flag regardless of the policy kind.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, I think conceptually this flag applies only to the Surplus policy. Once price improvement is implemented, I think we are likely going to use PriceImprovement over Surplus for all order types. I'm not sure though how we would configure things if price improvement for swap orders has different volume factors than for out of market limit orders (I think such a config may be desirable, to be checked with @olgafetisova).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved to Surplus

}

#[derive(Debug)]
pub enum PolicyBuilder {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should make this class private and create it in the main Fee component from the infra config. It might also be nicer to have every variant be there own sub-struct so that we can implement slightly different build behaviors (e.g. surplus and volume don't need the quote).

Then the main apply function would become something like

self.fee_variants.filter_map(|variant| {
  match variant {
    FeeVariant::Surplus(variant) => variant.apply(order),
    FeeVariant::PriceImprovement(variant) => variant.apply(order, quote),
    ...
  }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Refactored

Copy link
Contributor

@fleupold fleupold left a comment

Choose a reason for hiding this comment

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

Looks good to me. Some remaining comments on how to split the code into files (to allow for succinct and nice to read naming), reducing visibility of internal structs as much as possible and avoiding to share logic which is likely not correlated, but nothing blocking.

}

#[derive(Debug)]
struct SurplusPolicy {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: we are trying to keep struct names short for lexicographic appeal. I think they could just be called Surplus (without policy) but if we want the Policy part on all subtypes, I think it would be prettier to create a new private submodule policy and reference them as

policy::Surplus, policy::PriceImprovement etc

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

}
}

fn can_policy_apply(order: &boundary::Order, quote: &domain::Quote) -> bool {
Copy link
Contributor

Choose a reason for hiding this comment

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

I wouldn't share this method across fee policies (it seems coincidental that there is some shared logic between).

If I understand the current logic correctly, volume fees can only be applied to out of market limit orders, which doesn't seem correct to me.

Also for PriceImprovement, shouldn't this be applied mainly for in market price orders?

Let's just make this logic explicit for every fee policy.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed

}

impl SurplusPolicy {
pub fn apply(&self, order: &boundary::Order, quote: &domain::Quote) -> Option<Policy> {
Copy link
Contributor

Choose a reason for hiding this comment

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

no need to be pub

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Since it was moved to its own private submodule, this requires the function to be public.

pub struct ProtocolFee {
policy: Policy,
fee_policy_skip_market_orders: bool,
policy_builder: PolicyBuilder,
Copy link
Contributor

Choose a reason for hiding this comment

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

Since this is a private type now, if we move it to a policy submodule we could even call it Policy without having a name collision (the way it turned out now it's not really following the Builder pattern anymore)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved

Copy link
Contributor

@sunce86 sunce86 left a comment

Choose a reason for hiding this comment

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

Lookin' nice!

pub struct Quote {
pub sell_amount: U256,
pub buy_amount: U256,
pub fee: U256,
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: maybe add a comment that the fee is always expressed in the sell token

@squadgazzz squadgazzz enabled auto-merge (squash) February 12, 2024 11:27
@squadgazzz squadgazzz merged commit 96f4f7b into main Feb 12, 2024
9 checks passed
@squadgazzz squadgazzz deleted the 2287/autopilot-domain branch February 12, 2024 11:32
@github-actions github-actions bot locked and limited conversation to collaborators Feb 12, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants