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.as_f64() < 0.0 {
83 anyhow::bail!("Commission must be greater than or equal to zero.")
84 }
85 let zero_commission = Money::new(0.0, 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 use rust_decimal::prelude::ToPrimitive;
149
150 use super::{FeeModel, FixedFeeModel, MakerTakerFeeModel};
151
152 #[rstest]
153 fn test_fixed_model_single_fill() {
154 let expected_commission = Money::new(1.0, Currency::USD());
155 let aud_usd = InstrumentAny::CurrencyPair(audusd_sim());
156 let fee_model = FixedFeeModel::new(expected_commission, None).unwrap();
157 let market_order = OrderTestBuilder::new(OrderType::Market)
158 .instrument_id(aud_usd.id())
159 .side(OrderSide::Buy)
160 .quantity(Quantity::from(100_000))
161 .build();
162 let accepted_order = TestOrderStubs::make_accepted_order(&market_order);
163 let commission = fee_model
164 .get_commission(
165 &accepted_order,
166 Quantity::from(100_000),
167 Price::from("1.0"),
168 &aud_usd,
169 )
170 .unwrap();
171 assert_eq!(commission, expected_commission);
172 }
173
174 #[rstest]
175 #[case(OrderSide::Buy, true, Money::from("1 USD"), Money::from("0 USD"))]
176 #[case(OrderSide::Sell, true, Money::from("1 USD"), Money::from("0 USD"))]
177 #[case(OrderSide::Buy, false, Money::from("1 USD"), Money::from("1 USD"))]
178 #[case(OrderSide::Sell, false, Money::from("1 USD"), Money::from("1 USD"))]
179 fn test_fixed_model_multiple_fills(
180 #[case] order_side: OrderSide,
181 #[case] charge_commission_once: bool,
182 #[case] expected_first_fill: Money,
183 #[case] expected_next_fill: Money,
184 ) {
185 let aud_usd = InstrumentAny::CurrencyPair(audusd_sim());
186 let fee_model =
187 FixedFeeModel::new(expected_first_fill, Some(charge_commission_once)).unwrap();
188 let market_order = OrderTestBuilder::new(OrderType::Market)
189 .instrument_id(aud_usd.id())
190 .side(order_side)
191 .quantity(Quantity::from(100_000))
192 .build();
193 let mut accepted_order = TestOrderStubs::make_accepted_order(&market_order);
194 let commission_first_fill = fee_model
195 .get_commission(
196 &accepted_order,
197 Quantity::from(50_000),
198 Price::from("1.0"),
199 &aud_usd,
200 )
201 .unwrap();
202 let fill = TestOrderEventStubs::filled(
203 &accepted_order,
204 &aud_usd,
205 None,
206 None,
207 None,
208 Some(Quantity::from(50_000)),
209 None,
210 None,
211 None,
212 None,
213 );
214 accepted_order.apply(fill).unwrap();
215 let commission_next_fill = fee_model
216 .get_commission(
217 &accepted_order,
218 Quantity::from(50_000),
219 Price::from("1.0"),
220 &aud_usd,
221 )
222 .unwrap();
223 assert_eq!(commission_first_fill, expected_first_fill);
224 assert_eq!(commission_next_fill, expected_next_fill);
225 }
226
227 #[rstest]
228 fn test_maker_taker_fee_model_maker_commission() {
229 let fee_model = MakerTakerFeeModel;
230 let aud_usd = InstrumentAny::CurrencyPair(audusd_sim());
231 let maker_fee = aud_usd.maker_fee().to_f64().unwrap();
232 let price = Price::from("1.0");
233 let limit_order = OrderTestBuilder::new(OrderType::Limit)
234 .instrument_id(aud_usd.id())
235 .side(OrderSide::Sell)
236 .price(price)
237 .quantity(Quantity::from(100_000))
238 .build();
239 let fill = TestOrderStubs::make_filled_order(&limit_order, &aud_usd, LiquiditySide::Maker);
240 let expected_commission_amount = fill.quantity().as_f64() * price.as_f64() * maker_fee;
241 let commission = fee_model
242 .get_commission(&fill, Quantity::from(100_000), Price::from("1.0"), &aud_usd)
243 .unwrap();
244 assert_eq!(commission.as_f64(), expected_commission_amount);
245 }
246
247 #[rstest]
248 fn test_maker_taker_fee_model_taker_commission() {
249 let fee_model = MakerTakerFeeModel;
250 let aud_usd = InstrumentAny::CurrencyPair(audusd_sim());
251 let maker_fee = aud_usd.taker_fee().to_f64().unwrap();
252 let price = Price::from("1.0");
253 let limit_order = OrderTestBuilder::new(OrderType::Limit)
254 .instrument_id(aud_usd.id())
255 .side(OrderSide::Sell)
256 .price(price)
257 .quantity(Quantity::from(100_000))
258 .build();
259
260 let fill = TestOrderStubs::make_filled_order(&limit_order, &aud_usd, LiquiditySide::Taker);
261 let expected_commission_amount = fill.quantity().as_f64() * price.as_f64() * maker_fee;
262 let commission = fee_model
263 .get_commission(&fill, Quantity::from(100_000), Price::from("1.0"), &aud_usd)
264 .unwrap();
265 assert_eq!(commission.as_f64(), expected_commission_amount);
266 }
267}