nautilus_backtest/models/
fee.rs1use nautilus_model::{
17 enums::LiquiditySide,
18 instruments::InstrumentAny,
19 orders::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::{stubs::audusd_sim, InstrumentAny},
131 orders::{
132 builder::OrderTestBuilder,
133 stubs::{TestOrderEventStubs, TestOrderStubs},
134 },
135 types::{Currency, Money, Price, Quantity},
136 };
137 use rstest::rstest;
138 use rust_decimal::prelude::ToPrimitive;
139
140 use crate::models::fee::{FeeModel, FixedFeeModel, MakerTakerFeeModel};
141
142 #[rstest]
143 fn test_fixed_model_single_fill() {
144 let expected_commission = Money::new(1.0, Currency::USD());
145 let aud_usd = InstrumentAny::CurrencyPair(audusd_sim());
146 let fee_model = FixedFeeModel::new(expected_commission, None).unwrap();
147 let market_order = OrderTestBuilder::new(OrderType::Market)
148 .instrument_id(aud_usd.id())
149 .side(OrderSide::Buy)
150 .quantity(Quantity::from(100_000))
151 .build();
152 let accepted_order = TestOrderStubs::make_accepted_order(&market_order);
153 let commission = fee_model
154 .get_commission(
155 &accepted_order,
156 Quantity::from(100_000),
157 Price::from("1.0"),
158 &aud_usd,
159 )
160 .unwrap();
161 assert_eq!(commission, expected_commission);
162 }
163
164 #[rstest]
165 #[case(OrderSide::Buy, true, Money::from("1 USD"), Money::from("0 USD"))]
166 #[case(OrderSide::Sell, true, Money::from("1 USD"), Money::from("0 USD"))]
167 #[case(OrderSide::Buy, false, Money::from("1 USD"), Money::from("1 USD"))]
168 #[case(OrderSide::Sell, false, Money::from("1 USD"), Money::from("1 USD"))]
169 fn test_fixed_model_multiple_fills(
170 #[case] order_side: OrderSide,
171 #[case] charge_commission_once: bool,
172 #[case] expected_first_fill: Money,
173 #[case] expected_next_fill: Money,
174 ) {
175 let aud_usd = InstrumentAny::CurrencyPair(audusd_sim());
176 let fee_model =
177 FixedFeeModel::new(expected_first_fill, Some(charge_commission_once)).unwrap();
178 let market_order = OrderTestBuilder::new(OrderType::Market)
179 .instrument_id(aud_usd.id())
180 .side(order_side)
181 .quantity(Quantity::from(100_000))
182 .build();
183 let mut accepted_order = TestOrderStubs::make_accepted_order(&market_order);
184 let commission_first_fill = fee_model
185 .get_commission(
186 &accepted_order,
187 Quantity::from(50_000),
188 Price::from("1.0"),
189 &aud_usd,
190 )
191 .unwrap();
192 let fill = TestOrderEventStubs::order_filled(
193 &accepted_order,
194 &aud_usd,
195 None,
196 None,
197 None,
198 Some(Quantity::from(50_000)),
199 None,
200 None,
201 None,
202 None,
203 );
204 accepted_order.apply(fill).unwrap();
205 let commission_next_fill = fee_model
206 .get_commission(
207 &accepted_order,
208 Quantity::from(50_000),
209 Price::from("1.0"),
210 &aud_usd,
211 )
212 .unwrap();
213 assert_eq!(commission_first_fill, expected_first_fill);
214 assert_eq!(commission_next_fill, expected_next_fill);
215 }
216
217 #[rstest]
218 fn test_maker_taker_fee_model_maker_commission() {
219 let fee_model = MakerTakerFeeModel;
220 let aud_usd = InstrumentAny::CurrencyPair(audusd_sim());
221 let maker_fee = aud_usd.maker_fee().to_f64().unwrap();
222 let price = Price::from("1.0");
223 let limit_order = OrderTestBuilder::new(OrderType::Limit)
224 .instrument_id(aud_usd.id())
225 .side(OrderSide::Sell)
226 .price(price)
227 .quantity(Quantity::from(100_000))
228 .build();
229 let order_filled =
230 TestOrderStubs::make_filled_order(&limit_order, &aud_usd, LiquiditySide::Maker);
231 let expected_commission_amount =
232 order_filled.quantity().as_f64() * price.as_f64() * maker_fee;
233 let commission = fee_model
234 .get_commission(
235 &order_filled,
236 Quantity::from(100_000),
237 Price::from("1.0"),
238 &aud_usd,
239 )
240 .unwrap();
241 assert_eq!(commission.as_f64(), expected_commission_amount);
242 }
243
244 #[rstest]
245 fn test_maker_taker_fee_model_taker_commission() {
246 let fee_model = MakerTakerFeeModel;
247 let aud_usd = InstrumentAny::CurrencyPair(audusd_sim());
248 let maker_fee = aud_usd.taker_fee().to_f64().unwrap();
249 let price = Price::from("1.0");
250 let limit_order = OrderTestBuilder::new(OrderType::Limit)
251 .instrument_id(aud_usd.id())
252 .side(OrderSide::Sell)
253 .price(price)
254 .quantity(Quantity::from(100_000))
255 .build();
256
257 let order_filled =
258 TestOrderStubs::make_filled_order(&limit_order, &aud_usd, LiquiditySide::Taker);
259 let expected_commission_amount =
260 order_filled.quantity().as_f64() * price.as_f64() * maker_fee;
261 let commission = fee_model
262 .get_commission(
263 &order_filled,
264 Quantity::from(100_000),
265 Price::from("1.0"),
266 &aud_usd,
267 )
268 .unwrap();
269 assert_eq!(commission.as_f64(), expected_commission_amount);
270 }
271}