nautilus_execution/models/
fee.rs1use nautilus_model::{
17 enums::LiquiditySide,
18 instruments::{Instrument, InstrumentAny},
19 orders::{Order, OrderAny},
20 types::{Money, Price, Quantity},
21};
22use rust_decimal::prelude::ToPrimitive;
23
24pub trait FeeModel {
25 fn get_commission(
31 &self,
32 order: &OrderAny,
33 fill_quantity: Quantity,
34 fill_px: Price,
35 instrument: &InstrumentAny,
36 ) -> anyhow::Result<Money>;
37}
38
39#[derive(Clone, Debug)]
40pub enum FeeModelAny {
41 Fixed(FixedFeeModel),
42 MakerTaker(MakerTakerFeeModel),
43}
44
45impl FeeModel for FeeModelAny {
46 fn get_commission(
47 &self,
48 order: &OrderAny,
49 fill_quantity: Quantity,
50 fill_px: Price,
51 instrument: &InstrumentAny,
52 ) -> anyhow::Result<Money> {
53 match self {
54 Self::Fixed(model) => model.get_commission(order, fill_quantity, fill_px, instrument),
55 Self::MakerTaker(model) => {
56 model.get_commission(order, fill_quantity, fill_px, instrument)
57 }
58 }
59 }
60}
61
62impl Default for FeeModelAny {
63 fn default() -> Self {
64 Self::MakerTaker(MakerTakerFeeModel)
65 }
66}
67
68#[derive(Debug, Clone)]
69pub struct FixedFeeModel {
70 commission: Money,
71 zero_commission: Money,
72 change_commission_once: bool,
73}
74
75impl FixedFeeModel {
76 pub fn new(commission: Money, change_commission_once: Option<bool>) -> anyhow::Result<Self> {
82 if commission.raw < 0 {
83 anyhow::bail!("Commission must be greater than or equal to zero")
84 }
85 let zero_commission = Money::zero(commission.currency);
86 Ok(Self {
87 commission,
88 zero_commission,
89 change_commission_once: change_commission_once.unwrap_or(true),
90 })
91 }
92}
93
94impl FeeModel for FixedFeeModel {
95 fn get_commission(
96 &self,
97 order: &OrderAny,
98 _fill_quantity: Quantity,
99 _fill_px: Price,
100 _instrument: &InstrumentAny,
101 ) -> anyhow::Result<Money> {
102 if !self.change_commission_once || order.filled_qty().is_zero() {
103 Ok(self.commission)
104 } else {
105 Ok(self.zero_commission)
106 }
107 }
108}
109
110#[derive(Debug, Clone)]
111pub struct MakerTakerFeeModel;
112
113impl FeeModel for MakerTakerFeeModel {
114 fn get_commission(
115 &self,
116 order: &OrderAny,
117 fill_quantity: Quantity,
118 fill_px: Price,
119 instrument: &InstrumentAny,
120 ) -> anyhow::Result<Money> {
121 let notional = instrument.calculate_notional_value(fill_quantity, fill_px, Some(false));
122 let commission = match order.liquidity_side() {
123 Some(LiquiditySide::Maker) => notional * instrument.maker_fee().to_f64().unwrap(),
124 Some(LiquiditySide::Taker) => notional * instrument.taker_fee().to_f64().unwrap(),
125 Some(LiquiditySide::NoLiquiditySide) | None => anyhow::bail!("Liquidity side not set"),
126 };
127 if instrument.is_inverse() {
128 Ok(Money::new(commission, instrument.base_currency().unwrap()))
129 } else {
130 Ok(Money::new(commission, instrument.quote_currency()))
131 }
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use nautilus_model::{
138 enums::{LiquiditySide, OrderSide, OrderType},
139 instruments::{Instrument, InstrumentAny, stubs::audusd_sim},
140 orders::{
141 Order,
142 builder::OrderTestBuilder,
143 stubs::{TestOrderEventStubs, TestOrderStubs},
144 },
145 types::{Currency, Money, Price, Quantity},
146 };
147 use rstest::rstest;
148
149 use super::{FeeModel, FixedFeeModel, MakerTakerFeeModel};
150
151 #[rstest]
152 fn test_fixed_model_single_fill() {
153 let expected_commission = Money::new(1.0, Currency::USD());
154 let aud_usd = InstrumentAny::CurrencyPair(audusd_sim());
155 let fee_model = FixedFeeModel::new(expected_commission, None).unwrap();
156 let market_order = OrderTestBuilder::new(OrderType::Market)
157 .instrument_id(aud_usd.id())
158 .side(OrderSide::Buy)
159 .quantity(Quantity::from(100_000))
160 .build();
161 let accepted_order = TestOrderStubs::make_accepted_order(&market_order);
162 let commission = fee_model
163 .get_commission(
164 &accepted_order,
165 Quantity::from(100_000),
166 Price::from("1.0"),
167 &aud_usd,
168 )
169 .unwrap();
170 assert_eq!(commission, expected_commission);
171 }
172
173 #[rstest]
174 #[case(OrderSide::Buy, true, Money::from("1 USD"), Money::from("0 USD"))]
175 #[case(OrderSide::Sell, true, Money::from("1 USD"), Money::from("0 USD"))]
176 #[case(OrderSide::Buy, false, Money::from("1 USD"), Money::from("1 USD"))]
177 #[case(OrderSide::Sell, false, Money::from("1 USD"), Money::from("1 USD"))]
178 fn test_fixed_model_multiple_fills(
179 #[case] order_side: OrderSide,
180 #[case] charge_commission_once: bool,
181 #[case] expected_first_fill: Money,
182 #[case] expected_next_fill: Money,
183 ) {
184 let aud_usd = InstrumentAny::CurrencyPair(audusd_sim());
185 let fee_model =
186 FixedFeeModel::new(expected_first_fill, Some(charge_commission_once)).unwrap();
187 let market_order = OrderTestBuilder::new(OrderType::Market)
188 .instrument_id(aud_usd.id())
189 .side(order_side)
190 .quantity(Quantity::from(100_000))
191 .build();
192 let mut accepted_order = TestOrderStubs::make_accepted_order(&market_order);
193 let commission_first_fill = fee_model
194 .get_commission(
195 &accepted_order,
196 Quantity::from(50_000),
197 Price::from("1.0"),
198 &aud_usd,
199 )
200 .unwrap();
201 let fill = TestOrderEventStubs::filled(
202 &accepted_order,
203 &aud_usd,
204 None,
205 None,
206 None,
207 Some(Quantity::from(50_000)),
208 None,
209 None,
210 None,
211 None,
212 );
213 accepted_order.apply(fill).unwrap();
214 let commission_next_fill = fee_model
215 .get_commission(
216 &accepted_order,
217 Quantity::from(50_000),
218 Price::from("1.0"),
219 &aud_usd,
220 )
221 .unwrap();
222 assert_eq!(commission_first_fill, expected_first_fill);
223 assert_eq!(commission_next_fill, expected_next_fill);
224 }
225
226 #[rstest]
227 fn test_maker_taker_fee_model_maker_commission() {
228 let fee_model = MakerTakerFeeModel;
229 let aud_usd = InstrumentAny::CurrencyPair(audusd_sim());
230 let maker_fee = aud_usd.maker_fee();
231 let price = Price::from("1.0");
232 let limit_order = OrderTestBuilder::new(OrderType::Limit)
233 .instrument_id(aud_usd.id())
234 .side(OrderSide::Sell)
235 .price(price)
236 .quantity(Quantity::from(100_000))
237 .build();
238 let fill = TestOrderStubs::make_filled_order(&limit_order, &aud_usd, LiquiditySide::Maker);
239 let expected_commission = fill.quantity().as_decimal() * price.as_decimal() * maker_fee;
240 let commission = fee_model
241 .get_commission(&fill, Quantity::from(100_000), Price::from("1.0"), &aud_usd)
242 .unwrap();
243 assert_eq!(commission.as_decimal(), expected_commission);
244 }
245
246 #[rstest]
247 fn test_maker_taker_fee_model_taker_commission() {
248 let fee_model = MakerTakerFeeModel;
249 let aud_usd = InstrumentAny::CurrencyPair(audusd_sim());
250 let taker_fee = aud_usd.taker_fee();
251 let price = Price::from("1.0");
252 let limit_order = OrderTestBuilder::new(OrderType::Limit)
253 .instrument_id(aud_usd.id())
254 .side(OrderSide::Sell)
255 .price(price)
256 .quantity(Quantity::from(100_000))
257 .build();
258
259 let fill = TestOrderStubs::make_filled_order(&limit_order, &aud_usd, LiquiditySide::Taker);
260 let expected_commission = fill.quantity().as_decimal() * price.as_decimal() * taker_fee;
261 let commission = fee_model
262 .get_commission(&fill, Quantity::from(100_000), Price::from("1.0"), &aud_usd)
263 .unwrap();
264 assert_eq!(commission.as_decimal(), expected_commission);
265 }
266}