1use rust_decimal::Decimal;
2use serde::{Deserialize, Serialize};
3
4#[derive(
11 Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Deserialize, Serialize,
12)]
13pub enum Liquidity {
14 #[default]
18 Taker,
19 Maker,
22}
23
24pub trait FeeModel {
33 fn compute_fee(
34 &self,
35 price: Decimal,
36 quantity: Decimal,
37 contract_size: Decimal,
38 liquidity: Liquidity,
39 ) -> Decimal;
40}
41
42#[derive(
44 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Deserialize, Serialize,
45)]
46pub struct ZeroFeeModel;
47
48impl FeeModel for ZeroFeeModel {
49 fn compute_fee(
50 &self,
51 _price: Decimal,
52 _quantity: Decimal,
53 _contract_size: Decimal,
54 _liquidity: Liquidity,
55 ) -> Decimal {
56 Decimal::ZERO
57 }
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
68pub struct PerContractFeeModel {
69 #[serde(with = "rust_decimal::serde::str")]
70 pub commission_per_contract: Decimal,
71}
72
73impl FeeModel for PerContractFeeModel {
74 fn compute_fee(
77 &self,
78 _price: Decimal,
79 quantity: Decimal,
80 _contract_size: Decimal,
81 _liquidity: Liquidity,
82 ) -> Decimal {
83 self.commission_per_contract * quantity.abs()
84 }
85}
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
104pub struct PercentageFeeModel {
105 #[serde(with = "rust_decimal::serde::str")]
113 pub rate: Decimal,
114
115 #[serde(
122 default,
123 skip_serializing_if = "Option::is_none",
124 with = "rust_decimal::serde::str_option"
125 )]
126 pub maker_rate: Option<Decimal>,
127}
128
129impl PercentageFeeModel {
130 pub fn new(rate: Decimal) -> Self {
132 Self {
133 rate,
134 maker_rate: None,
135 }
136 }
137
138 pub fn maker_taker(maker_rate: Decimal, taker_rate: Decimal) -> Self {
142 Self {
143 rate: taker_rate,
144 maker_rate: Some(maker_rate),
145 }
146 }
147
148 pub fn rate_for(&self, liquidity: Liquidity) -> Decimal {
151 match liquidity {
152 Liquidity::Maker => self.maker_rate.unwrap_or(self.rate),
153 Liquidity::Taker => self.rate,
154 }
155 }
156}
157
158impl FeeModel for PercentageFeeModel {
159 fn compute_fee(
160 &self,
161 price: Decimal,
162 quantity: Decimal,
163 contract_size: Decimal,
164 liquidity: Liquidity,
165 ) -> Decimal {
166 self.rate_for(liquidity) * price * quantity.abs() * contract_size
167 }
168}
169
170#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
189pub enum FeeModelConfig {
190 Zero(ZeroFeeModel),
191 PerContract(PerContractFeeModel),
192 Percentage(PercentageFeeModel),
193}
194
195impl Default for FeeModelConfig {
196 fn default() -> Self {
197 Self::Zero(ZeroFeeModel)
198 }
199}
200
201impl FeeModel for FeeModelConfig {
202 fn compute_fee(
203 &self,
204 price: Decimal,
205 quantity: Decimal,
206 contract_size: Decimal,
207 liquidity: Liquidity,
208 ) -> Decimal {
209 match self {
210 FeeModelConfig::Zero(model) => {
211 model.compute_fee(price, quantity, contract_size, liquidity)
212 }
213 FeeModelConfig::PerContract(model) => {
214 model.compute_fee(price, quantity, contract_size, liquidity)
215 }
216 FeeModelConfig::Percentage(model) => {
217 model.compute_fee(price, quantity, contract_size, liquidity)
218 }
219 }
220 }
221}
222
223#[cfg(test)]
224#[allow(clippy::unwrap_used, clippy::expect_used)] mod tests {
226 use super::*;
227
228 fn d(s: &str) -> Decimal {
229 s.parse().unwrap()
230 }
231
232 #[test]
233 fn zero_fee_model_always_returns_zero() {
234 assert_eq!(
235 ZeroFeeModel.compute_fee(d("100"), d("5"), d("100"), Liquidity::Taker),
236 Decimal::ZERO
237 );
238 assert_eq!(
239 ZeroFeeModel.compute_fee(Decimal::ZERO, Decimal::ZERO, Decimal::ONE, Liquidity::Taker),
240 Decimal::ZERO
241 );
242 }
243
244 #[test]
245 fn per_contract_fee_charges_by_quantity() {
246 let model = PerContractFeeModel {
247 commission_per_contract: d("0.65"),
248 };
249 assert_eq!(
250 model.compute_fee(d("100"), d("10"), d("100"), Liquidity::Taker),
251 d("6.5")
252 );
253 }
254
255 #[test]
256 fn per_contract_fee_uses_abs_quantity() {
257 let model = PerContractFeeModel {
258 commission_per_contract: d("0.65"),
259 };
260 assert_eq!(
262 model.compute_fee(d("100"), d("-10"), d("100"), Liquidity::Taker),
263 model.compute_fee(d("100"), d("10"), d("100"), Liquidity::Taker),
264 );
265 }
266
267 #[test]
270 fn fee_model_config_zero_dispatches() {
271 let cfg = FeeModelConfig::Zero(ZeroFeeModel);
272 assert_eq!(
273 cfg.compute_fee(d("100"), d("5"), d("100"), Liquidity::Taker),
274 Decimal::ZERO
275 );
276 }
277
278 #[test]
279 fn fee_model_config_per_contract_dispatches() {
280 let model = PerContractFeeModel {
281 commission_per_contract: d("0.65"),
282 };
283 let cfg = FeeModelConfig::PerContract(model);
284 assert_eq!(
285 cfg.compute_fee(d("100"), d("10"), d("100"), Liquidity::Taker),
286 model.compute_fee(d("100"), d("10"), d("100"), Liquidity::Taker),
287 );
288 }
289
290 #[test]
291 fn fee_model_config_default_is_zero() {
292 assert_eq!(
293 FeeModelConfig::default(),
294 FeeModelConfig::Zero(ZeroFeeModel)
295 );
296 }
297
298 #[test]
301 fn percentage_fee_computes_rate_times_notional() {
302 let model = PercentageFeeModel::new(d("0.001"));
304 assert_eq!(
306 model.compute_fee(d("100"), d("10"), d("1"), Liquidity::Taker),
307 d("1")
308 );
309 }
310
311 #[test]
312 fn percentage_fee_uses_abs_quantity() {
313 let model = PercentageFeeModel::new(d("0.001"));
314 assert_eq!(
315 model.compute_fee(d("100"), d("-10"), d("1"), Liquidity::Taker),
316 model.compute_fee(d("100"), d("10"), d("1"), Liquidity::Taker),
317 );
318 }
319
320 #[test]
321 fn percentage_fee_scales_by_contract_size() {
322 let model = PercentageFeeModel::new(d("0.001"));
325 assert_eq!(
326 model.compute_fee(d("5000"), d("1"), d("25"), Liquidity::Taker),
327 d("125")
328 );
329 }
330
331 #[test]
332 fn percentage_fee_is_unchanged_for_a_unit_contract_size() {
333 let model = PercentageFeeModel::new(d("0.001"));
335 assert_eq!(
336 model.compute_fee(d("100"), d("10"), Decimal::ONE, Liquidity::Taker),
337 d("1")
338 );
339 }
340
341 #[test]
342 fn per_contract_fee_ignores_contract_size() {
343 let model = PerContractFeeModel {
347 commission_per_contract: d("0.65"),
348 };
349 assert_eq!(
350 model.compute_fee(d("5000"), d("10"), d("25"), Liquidity::Taker),
351 model.compute_fee(d("5000"), d("10"), Decimal::ONE, Liquidity::Taker),
352 );
353 }
354
355 #[test]
356 fn fee_model_config_percentage_dispatches() {
357 let model = PercentageFeeModel::new(d("0.001"));
358 let cfg = FeeModelConfig::Percentage(model);
359 assert_eq!(
360 cfg.compute_fee(d("100"), d("10"), d("1"), Liquidity::Taker),
361 model.compute_fee(d("100"), d("10"), d("1"), Liquidity::Taker),
362 );
363 }
364
365 #[test]
368 fn zero_fee_model_serde_roundtrip() {
369 let cfg = FeeModelConfig::Zero(ZeroFeeModel);
370 let json = serde_json::to_string(&cfg).unwrap();
371 assert_eq!(json, r#"{"Zero":null}"#);
372 let parsed: FeeModelConfig = serde_json::from_str(&json).unwrap();
373 assert_eq!(parsed, cfg);
374 }
375
376 #[test]
377 fn fee_model_config_default_when_field_omitted() {
378 #[derive(Deserialize)]
380 struct Wrapper {
381 #[serde(default)]
382 fee_model: FeeModelConfig,
383 }
384 let parsed: Wrapper = serde_json::from_str(r#"{}"#).unwrap();
385 assert_eq!(parsed.fee_model, FeeModelConfig::Zero(ZeroFeeModel));
386 }
387
388 #[test]
389 fn percentage_fee_model_serde_roundtrip() {
390 let cfg = FeeModelConfig::Percentage(PercentageFeeModel::new(d("0.001")));
391 let json = serde_json::to_string(&cfg).unwrap();
392 assert_eq!(json, r#"{"Percentage":{"rate":"0.001"}}"#);
393 let parsed: FeeModelConfig = serde_json::from_str(&json).unwrap();
394 assert_eq!(parsed, cfg);
395 }
396
397 #[test]
398 fn per_contract_fee_model_serde_roundtrip() {
399 let cfg = FeeModelConfig::PerContract(PerContractFeeModel {
400 commission_per_contract: d("0.65"),
401 });
402 let json = serde_json::to_string(&cfg).unwrap();
403 assert_eq!(
404 json,
405 r#"{"PerContract":{"commission_per_contract":"0.65"}}"#
406 );
407 let parsed: FeeModelConfig = serde_json::from_str(&json).unwrap();
408 assert_eq!(parsed, cfg);
409 }
410
411 #[test]
414 fn a_percentage_model_without_a_maker_rate_charges_one_rate_on_both_sides() {
415 let model = PercentageFeeModel::new(d("0.001"));
416
417 assert_eq!(
418 model.compute_fee(d("100"), d("10"), Decimal::ONE, Liquidity::Maker),
419 model.compute_fee(d("100"), d("10"), Decimal::ONE, Liquidity::Taker),
420 "an unconfigured maker rate must price exactly as the model did before the field \
421 existed"
422 );
423 }
424
425 #[test]
426 fn a_configured_maker_rate_is_charged_only_to_the_maker() {
427 let model = PercentageFeeModel::maker_taker(d("0.0002"), d("0.001"));
428
429 assert_eq!(
430 model.compute_fee(d("100"), d("10"), Decimal::ONE, Liquidity::Taker),
431 d("1"),
432 "0.1% of 1000 notional"
433 );
434 assert_eq!(
435 model.compute_fee(d("100"), d("10"), Decimal::ONE, Liquidity::Maker),
436 d("0.2"),
437 "0.02% of 1000 notional"
438 );
439 }
440
441 #[test]
443 fn a_maker_rebate_is_a_negative_fee() {
444 let model = PercentageFeeModel::maker_taker(d("-0.0001"), d("0.001"));
445
446 assert_eq!(
447 model.compute_fee(d("100"), d("10"), Decimal::ONE, Liquidity::Maker),
448 d("-0.1")
449 );
450 }
451
452 #[test]
453 fn a_per_contract_commission_does_not_vary_with_liquidity() {
454 let model = PerContractFeeModel {
455 commission_per_contract: d("0.65"),
456 };
457
458 assert_eq!(
459 model.compute_fee(d("100"), d("10"), d("100"), Liquidity::Maker),
460 model.compute_fee(d("100"), d("10"), d("100"), Liquidity::Taker)
461 );
462 }
463
464 #[test]
466 fn a_percentage_model_deserialises_without_a_maker_rate() {
467 let model: PercentageFeeModel =
468 serde_json::from_str(r#"{"rate":"0.001"}"#).expect("legacy shape must parse");
469
470 assert_eq!(model, PercentageFeeModel::new(d("0.001")));
471 assert_eq!(model.maker_rate, None);
472 }
473
474 #[test]
475 fn a_percentage_model_round_trips_a_maker_rate() {
476 let model = PercentageFeeModel::maker_taker(d("0.0002"), d("0.001"));
477 let json = serde_json::to_string(&model).expect("must serialise");
478
479 assert_eq!(
480 serde_json::from_str::<PercentageFeeModel>(&json).expect("must round-trip"),
481 model
482 );
483 }
484}