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(
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 PerContract(PerContractFeeModel),
44}
45
46impl FeeModel for FeeModelAny {
47 fn get_commission(
48 &self,
49 order: &OrderAny,
50 fill_quantity: Quantity,
51 fill_px: Price,
52 instrument: &InstrumentAny,
53 ) -> anyhow::Result<Money> {
54 match self {
55 Self::Fixed(model) => model.get_commission(order, fill_quantity, fill_px, instrument),
56 Self::MakerTaker(model) => {
57 model.get_commission(order, fill_quantity, fill_px, instrument)
58 }
59 Self::PerContract(model) => {
60 model.get_commission(order, fill_quantity, fill_px, instrument)
61 }
62 }
63 }
64}
65
66impl Default for FeeModelAny {
67 fn default() -> Self {
68 Self::MakerTaker(MakerTakerFeeModel)
69 }
70}
71
72#[derive(Debug, Clone)]
73#[cfg_attr(
74 feature = "python",
75 pyo3::pyclass(
76 module = "nautilus_trader.core.nautilus_pyo3.execution",
77 from_py_object
78 )
79)]
80#[cfg_attr(
81 feature = "python",
82 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.execution")
83)]
84pub struct FixedFeeModel {
85 commission: Money,
86 zero_commission: Money,
87 change_commission_once: bool,
88}
89
90impl FixedFeeModel {
91 pub fn new(commission: Money, change_commission_once: Option<bool>) -> anyhow::Result<Self> {
97 if commission.raw < 0 {
98 anyhow::bail!("Commission must be greater than or equal to zero")
99 }
100 let zero_commission = Money::zero(commission.currency);
101 Ok(Self {
102 commission,
103 zero_commission,
104 change_commission_once: change_commission_once.unwrap_or(true),
105 })
106 }
107}
108
109impl FeeModel for FixedFeeModel {
110 fn get_commission(
111 &self,
112 order: &OrderAny,
113 _fill_quantity: Quantity,
114 _fill_px: Price,
115 _instrument: &InstrumentAny,
116 ) -> anyhow::Result<Money> {
117 if !self.change_commission_once || order.filled_qty().is_zero() {
118 Ok(self.commission)
119 } else {
120 Ok(self.zero_commission)
121 }
122 }
123}
124
125#[derive(Debug, Clone)]
126#[cfg_attr(
127 feature = "python",
128 pyo3::pyclass(
129 module = "nautilus_trader.core.nautilus_pyo3.execution",
130 from_py_object
131 )
132)]
133#[cfg_attr(
134 feature = "python",
135 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.execution")
136)]
137pub struct PerContractFeeModel {
138 commission: Money,
139}
140
141impl PerContractFeeModel {
142 pub fn new(commission: Money) -> anyhow::Result<Self> {
148 if commission.raw < 0 {
149 anyhow::bail!("Commission must be greater than or equal to zero")
150 }
151 Ok(Self { commission })
152 }
153}
154
155impl FeeModel for PerContractFeeModel {
156 fn get_commission(
157 &self,
158 _order: &OrderAny,
159 fill_quantity: Quantity,
160 _fill_px: Price,
161 _instrument: &InstrumentAny,
162 ) -> anyhow::Result<Money> {
163 let total = self.commission.as_f64() * fill_quantity.as_f64();
164 Ok(Money::new(total, self.commission.currency))
165 }
166}
167
168#[derive(Debug, Clone)]
169#[cfg_attr(
170 feature = "python",
171 pyo3::pyclass(
172 module = "nautilus_trader.core.nautilus_pyo3.execution",
173 from_py_object
174 )
175)]
176#[cfg_attr(
177 feature = "python",
178 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.execution")
179)]
180pub struct MakerTakerFeeModel;
181
182impl FeeModel for MakerTakerFeeModel {
183 fn get_commission(
184 &self,
185 order: &OrderAny,
186 fill_quantity: Quantity,
187 fill_px: Price,
188 instrument: &InstrumentAny,
189 ) -> anyhow::Result<Money> {
190 let notional = instrument.calculate_notional_value(fill_quantity, fill_px, Some(false));
191 let commission = match order.liquidity_side() {
192 Some(LiquiditySide::Maker) => notional * instrument.maker_fee().to_f64().unwrap(),
193 Some(LiquiditySide::Taker) => notional * instrument.taker_fee().to_f64().unwrap(),
194 Some(LiquiditySide::NoLiquiditySide) | None => anyhow::bail!("Liquidity side not set"),
195 };
196
197 if instrument.is_inverse() {
198 Ok(Money::new(commission, instrument.base_currency().unwrap()))
199 } else {
200 Ok(Money::new(commission, instrument.quote_currency()))
201 }
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use nautilus_model::{
208 enums::{LiquiditySide, OrderSide, OrderType},
209 instruments::{Instrument, InstrumentAny, stubs::audusd_sim},
210 orders::{
211 Order,
212 builder::OrderTestBuilder,
213 stubs::{TestOrderEventStubs, TestOrderStubs},
214 },
215 types::{Currency, Money, Price, Quantity},
216 };
217 use rstest::rstest;
218
219 use super::{FeeModel, FixedFeeModel, MakerTakerFeeModel, PerContractFeeModel};
220
221 #[rstest]
222 fn test_fixed_model_single_fill() {
223 let expected_commission = Money::new(1.0, Currency::USD());
224 let aud_usd = InstrumentAny::CurrencyPair(audusd_sim());
225 let fee_model = FixedFeeModel::new(expected_commission, None).unwrap();
226 let market_order = OrderTestBuilder::new(OrderType::Market)
227 .instrument_id(aud_usd.id())
228 .side(OrderSide::Buy)
229 .quantity(Quantity::from(100_000))
230 .build();
231 let accepted_order = TestOrderStubs::make_accepted_order(&market_order);
232 let commission = fee_model
233 .get_commission(
234 &accepted_order,
235 Quantity::from(100_000),
236 Price::from("1.0"),
237 &aud_usd,
238 )
239 .unwrap();
240 assert_eq!(commission, expected_commission);
241 }
242
243 #[rstest]
244 #[case(OrderSide::Buy, true, Money::from("1 USD"), Money::from("0 USD"))]
245 #[case(OrderSide::Sell, true, Money::from("1 USD"), Money::from("0 USD"))]
246 #[case(OrderSide::Buy, false, Money::from("1 USD"), Money::from("1 USD"))]
247 #[case(OrderSide::Sell, false, Money::from("1 USD"), Money::from("1 USD"))]
248 fn test_fixed_model_multiple_fills(
249 #[case] order_side: OrderSide,
250 #[case] charge_commission_once: bool,
251 #[case] expected_first_fill: Money,
252 #[case] expected_next_fill: Money,
253 ) {
254 let aud_usd = InstrumentAny::CurrencyPair(audusd_sim());
255 let fee_model =
256 FixedFeeModel::new(expected_first_fill, Some(charge_commission_once)).unwrap();
257 let market_order = OrderTestBuilder::new(OrderType::Market)
258 .instrument_id(aud_usd.id())
259 .side(order_side)
260 .quantity(Quantity::from(100_000))
261 .build();
262 let mut accepted_order = TestOrderStubs::make_accepted_order(&market_order);
263 let commission_first_fill = fee_model
264 .get_commission(
265 &accepted_order,
266 Quantity::from(50_000),
267 Price::from("1.0"),
268 &aud_usd,
269 )
270 .unwrap();
271 let fill = TestOrderEventStubs::filled(
272 &accepted_order,
273 &aud_usd,
274 None,
275 None,
276 None,
277 Some(Quantity::from(50_000)),
278 None,
279 None,
280 None,
281 None,
282 );
283 accepted_order.apply(fill).unwrap();
284 let commission_next_fill = fee_model
285 .get_commission(
286 &accepted_order,
287 Quantity::from(50_000),
288 Price::from("1.0"),
289 &aud_usd,
290 )
291 .unwrap();
292 assert_eq!(commission_first_fill, expected_first_fill);
293 assert_eq!(commission_next_fill, expected_next_fill);
294 }
295
296 #[rstest]
297 fn test_maker_taker_fee_model_maker_commission() {
298 let fee_model = MakerTakerFeeModel;
299 let aud_usd = InstrumentAny::CurrencyPair(audusd_sim());
300 let maker_fee = aud_usd.maker_fee();
301 let price = Price::from("1.0");
302 let limit_order = OrderTestBuilder::new(OrderType::Limit)
303 .instrument_id(aud_usd.id())
304 .side(OrderSide::Sell)
305 .price(price)
306 .quantity(Quantity::from(100_000))
307 .build();
308 let fill = TestOrderStubs::make_filled_order(&limit_order, &aud_usd, LiquiditySide::Maker);
309 let expected_commission = fill.quantity().as_decimal() * price.as_decimal() * maker_fee;
310 let commission = fee_model
311 .get_commission(&fill, Quantity::from(100_000), Price::from("1.0"), &aud_usd)
312 .unwrap();
313 assert_eq!(commission.as_decimal(), expected_commission);
314 }
315
316 #[rstest]
317 fn test_maker_taker_fee_model_taker_commission() {
318 let fee_model = MakerTakerFeeModel;
319 let aud_usd = InstrumentAny::CurrencyPair(audusd_sim());
320 let taker_fee = aud_usd.taker_fee();
321 let price = Price::from("1.0");
322 let limit_order = OrderTestBuilder::new(OrderType::Limit)
323 .instrument_id(aud_usd.id())
324 .side(OrderSide::Sell)
325 .price(price)
326 .quantity(Quantity::from(100_000))
327 .build();
328
329 let fill = TestOrderStubs::make_filled_order(&limit_order, &aud_usd, LiquiditySide::Taker);
330 let expected_commission = fill.quantity().as_decimal() * price.as_decimal() * taker_fee;
331 let commission = fee_model
332 .get_commission(&fill, Quantity::from(100_000), Price::from("1.0"), &aud_usd)
333 .unwrap();
334 assert_eq!(commission.as_decimal(), expected_commission);
335 }
336
337 #[rstest]
338 fn test_per_contract_fee_model() {
339 let commission_per_contract = Money::new(0.50, Currency::USD());
340 let aud_usd = InstrumentAny::CurrencyPair(audusd_sim());
341 let fee_model = PerContractFeeModel::new(commission_per_contract).unwrap();
342 let market_order = OrderTestBuilder::new(OrderType::Market)
343 .instrument_id(aud_usd.id())
344 .side(OrderSide::Buy)
345 .quantity(Quantity::from(100))
346 .build();
347 let accepted_order = TestOrderStubs::make_accepted_order(&market_order);
348 let commission = fee_model
349 .get_commission(
350 &accepted_order,
351 Quantity::from(100),
352 Price::from("1.0"),
353 &aud_usd,
354 )
355 .unwrap();
356 assert_eq!(commission, Money::new(50.0, Currency::USD()));
357 }
358
359 #[rstest]
360 fn test_per_contract_fee_model_partial_fill() {
361 let commission_per_contract = Money::new(1.25, Currency::USD());
362 let aud_usd = InstrumentAny::CurrencyPair(audusd_sim());
363 let fee_model = PerContractFeeModel::new(commission_per_contract).unwrap();
364 let market_order = OrderTestBuilder::new(OrderType::Market)
365 .instrument_id(aud_usd.id())
366 .side(OrderSide::Sell)
367 .quantity(Quantity::from(1000))
368 .build();
369 let accepted_order = TestOrderStubs::make_accepted_order(&market_order);
370 let commission = fee_model
371 .get_commission(
372 &accepted_order,
373 Quantity::from(400),
374 Price::from("1.0"),
375 &aud_usd,
376 )
377 .unwrap();
378 assert_eq!(commission, Money::new(500.0, Currency::USD()));
379 }
380
381 #[rstest]
382 fn test_per_contract_fee_model_negative_commission_fails() {
383 let result = PerContractFeeModel::new(Money::new(-1.0, Currency::USD()));
384 assert!(result.is_err());
385 }
386}