1use ahash::AHashMap;
23use rust_decimal::Decimal;
24use serde::{Deserialize, Serialize};
25
26use crate::{
27 enums::LiquiditySide,
28 identifiers::InstrumentId,
29 instruments::{Instrument, InstrumentAny},
30 types::{Money, Price, Quantity},
31};
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
38pub struct MakerTakerFeeRates {
39 pub maker: Decimal,
41 pub taker: Decimal,
43}
44
45impl MakerTakerFeeRates {
46 #[must_use]
48 pub const fn new(maker: Decimal, taker: Decimal) -> Self {
49 Self { maker, taker }
50 }
51
52 #[must_use]
54 pub const fn zero() -> Self {
55 Self {
56 maker: Decimal::ZERO,
57 taker: Decimal::ZERO,
58 }
59 }
60
61 pub fn rate_for(&self, liquidity_side: LiquiditySide) -> anyhow::Result<Decimal> {
67 match liquidity_side {
68 LiquiditySide::Maker => Ok(self.maker),
69 LiquiditySide::Taker => Ok(self.taker),
70 LiquiditySide::NoLiquiditySide => {
71 anyhow::bail!("Invalid `LiquiditySide`: {liquidity_side}")
72 }
73 }
74 }
75}
76
77#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
83pub struct MakerTakerFeeSchedule {
84 pub default: MakerTakerFeeRates,
86 pub overrides: AHashMap<InstrumentId, MakerTakerFeeRates>,
88}
89
90impl MakerTakerFeeSchedule {
91 #[must_use]
93 pub fn new(maker: Decimal, taker: Decimal) -> Self {
94 Self {
95 default: MakerTakerFeeRates::new(maker, taker),
96 overrides: AHashMap::new(),
97 }
98 }
99
100 #[must_use]
102 pub fn zero() -> Self {
103 Self {
104 default: MakerTakerFeeRates::zero(),
105 overrides: AHashMap::new(),
106 }
107 }
108
109 pub fn set_override(&mut self, instrument_id: InstrumentId, rates: MakerTakerFeeRates) {
111 self.overrides.insert(instrument_id, rates);
112 }
113
114 #[must_use]
116 pub fn rates_for(&self, instrument_id: InstrumentId) -> MakerTakerFeeRates {
117 self.overrides
118 .get(&instrument_id)
119 .copied()
120 .unwrap_or(self.default)
121 }
122
123 pub fn rate_for(
129 &self,
130 instrument_id: InstrumentId,
131 liquidity_side: LiquiditySide,
132 ) -> anyhow::Result<Decimal> {
133 self.rates_for(instrument_id).rate_for(liquidity_side)
134 }
135}
136
137pub fn calculate_maker_taker_commission(
152 instrument: &InstrumentAny,
153 last_qty: Quantity,
154 last_px: Price,
155 fee_rate: Decimal,
156 use_quote_for_inverse: Option<bool>,
157) -> anyhow::Result<Money> {
158 let notional =
159 instrument.try_calculate_notional_value(last_qty, last_px, use_quote_for_inverse)?;
160 let commission = notional
161 .as_decimal()
162 .abs()
163 .checked_mul(fee_rate)
164 .ok_or_else(|| anyhow::anyhow!("commission calculation overflow"))?;
165 Money::from_decimal(commission, notional.currency).map_err(Into::into)
166}
167
168#[cfg(test)]
169mod tests {
170 use rstest::rstest;
171 use rust_decimal_macros::dec;
172
173 use super::*;
174 use crate::enums::LiquiditySide;
175
176 #[rstest]
177 fn test_rates_for_liquidity_side() {
178 let rates = MakerTakerFeeRates::new(dec!(0.0002), dec!(0.0005));
179 assert_eq!(rates.rate_for(LiquiditySide::Maker).unwrap(), dec!(0.0002));
180 assert_eq!(rates.rate_for(LiquiditySide::Taker).unwrap(), dec!(0.0005));
181 assert!(rates.rate_for(LiquiditySide::NoLiquiditySide).is_err());
182 }
183
184 #[rstest]
185 fn test_schedule_override_wins_over_default() {
186 let mut schedule = MakerTakerFeeSchedule::new(dec!(0.001), dec!(0.002));
187 let instrument_id = InstrumentId::from("BTCUSDT.BINANCE");
188 let other_id = InstrumentId::from("ETHUSDT.BINANCE");
189 schedule.set_override(
190 instrument_id,
191 MakerTakerFeeRates::new(dec!(0.0001), dec!(0.0002)),
192 );
193
194 assert_eq!(
195 schedule.rates_for(instrument_id),
196 MakerTakerFeeRates::new(dec!(0.0001), dec!(0.0002))
197 );
198 assert_eq!(
199 schedule.rates_for(other_id),
200 MakerTakerFeeRates::new(dec!(0.001), dec!(0.002))
201 );
202 }
203
204 #[rstest]
205 fn test_explicit_zero_distinguishable_from_default() {
206 let mut schedule = MakerTakerFeeSchedule::new(dec!(0.001), dec!(0.002));
207 let instrument_id = InstrumentId::from("BTCUSDT.BINANCE");
208 schedule.set_override(instrument_id, MakerTakerFeeRates::zero());
209
210 assert_eq!(
211 schedule
212 .rate_for(instrument_id, LiquiditySide::Taker)
213 .unwrap(),
214 Decimal::ZERO
215 );
216 assert_eq!(
217 schedule
218 .rate_for(InstrumentId::from("ETHUSDT.BINANCE"), LiquiditySide::Taker)
219 .unwrap(),
220 dec!(0.002)
221 );
222 }
223
224 #[rstest]
225 fn test_commission_matches_notional_arithmetic() {
226 use crate::{
227 instruments::{Instrument, stubs::audusd_sim},
228 types::{Price, Quantity},
229 };
230
231 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
232 let commission = calculate_maker_taker_commission(
233 &instrument,
234 Quantity::from(100_000),
235 Price::from("1.0"),
236 dec!(0.0002),
237 None,
238 )
239 .unwrap();
240 let notional = instrument
241 .try_calculate_notional_value(Quantity::from(100_000), Price::from("1.0"), None)
242 .unwrap();
243 assert_eq!(
244 commission.as_decimal(),
245 notional.as_decimal() * dec!(0.0002)
246 );
247 assert_eq!(commission.currency, notional.currency);
248 }
249
250 #[rstest]
251 #[case::charge(dec!(0.0002), "0.80 USD")]
252 #[case::rebate(dec!(-0.0001), "-0.40 USD")]
253 fn test_commission_sign_follows_rate_at_negative_price(
254 #[case] fee_rate: Decimal,
255 #[case] expected: &str,
256 ) {
257 use crate::{
258 instruments::stubs::futures_contract_es,
259 types::{Price, Quantity},
260 };
261
262 let instrument = InstrumentAny::FuturesContract(futures_contract_es(None, None));
264 let commission = calculate_maker_taker_commission(
265 &instrument,
266 Quantity::from(100),
267 Price::from("-40.00"),
268 fee_rate,
269 None,
270 )
271 .unwrap();
272
273 assert_eq!(commission, Money::from(expected));
274 }
275}