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