1use rust_decimal::Decimal;
2use serde::{Deserialize, Serialize};
3
4pub trait FeeModel {
12 fn compute_fee(&self, price: Decimal, quantity: Decimal, contract_size: Decimal) -> Decimal;
13}
14
15#[derive(
17 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Deserialize, Serialize,
18)]
19pub struct ZeroFeeModel;
20
21impl FeeModel for ZeroFeeModel {
22 fn compute_fee(&self, _price: Decimal, _quantity: Decimal, _contract_size: Decimal) -> Decimal {
23 Decimal::ZERO
24 }
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
35pub struct PerContractFeeModel {
36 #[serde(with = "rust_decimal::serde::str")]
37 pub commission_per_contract: Decimal,
38}
39
40impl FeeModel for PerContractFeeModel {
41 fn compute_fee(&self, _price: Decimal, quantity: Decimal, _contract_size: Decimal) -> Decimal {
42 self.commission_per_contract * quantity.abs()
43 }
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
52pub struct PercentageFeeModel {
53 #[serde(with = "rust_decimal::serde::str")]
60 pub rate: Decimal,
61}
62
63impl FeeModel for PercentageFeeModel {
64 fn compute_fee(&self, price: Decimal, quantity: Decimal, _contract_size: Decimal) -> Decimal {
65 self.rate * price * quantity.abs()
66 }
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
88pub enum FeeModelConfig {
89 Zero(ZeroFeeModel),
90 PerContract(PerContractFeeModel),
91 Percentage(PercentageFeeModel),
92}
93
94impl Default for FeeModelConfig {
95 fn default() -> Self {
96 Self::Zero(ZeroFeeModel)
97 }
98}
99
100impl FeeModel for FeeModelConfig {
101 fn compute_fee(&self, price: Decimal, quantity: Decimal, contract_size: Decimal) -> Decimal {
102 match self {
103 FeeModelConfig::Zero(m) => m.compute_fee(price, quantity, contract_size),
104 FeeModelConfig::PerContract(m) => m.compute_fee(price, quantity, contract_size),
105 FeeModelConfig::Percentage(m) => m.compute_fee(price, quantity, contract_size),
106 }
107 }
108}
109
110#[cfg(test)]
111#[allow(clippy::unwrap_used)] mod tests {
113 use super::*;
114
115 fn d(s: &str) -> Decimal {
116 s.parse().unwrap()
117 }
118
119 #[test]
120 fn zero_fee_model_always_returns_zero() {
121 assert_eq!(
122 ZeroFeeModel.compute_fee(d("100"), d("5"), d("100")),
123 Decimal::ZERO
124 );
125 assert_eq!(
126 ZeroFeeModel.compute_fee(Decimal::ZERO, Decimal::ZERO, Decimal::ONE),
127 Decimal::ZERO
128 );
129 }
130
131 #[test]
132 fn per_contract_fee_charges_by_quantity() {
133 let model = PerContractFeeModel {
134 commission_per_contract: d("0.65"),
135 };
136 assert_eq!(model.compute_fee(d("100"), d("10"), d("100")), d("6.5"));
137 }
138
139 #[test]
140 fn per_contract_fee_uses_abs_quantity() {
141 let model = PerContractFeeModel {
142 commission_per_contract: d("0.65"),
143 };
144 assert_eq!(
146 model.compute_fee(d("100"), d("-10"), d("100")),
147 model.compute_fee(d("100"), d("10"), d("100")),
148 );
149 }
150
151 #[test]
154 fn fee_model_config_zero_dispatches() {
155 let cfg = FeeModelConfig::Zero(ZeroFeeModel);
156 assert_eq!(cfg.compute_fee(d("100"), d("5"), d("100")), Decimal::ZERO);
157 }
158
159 #[test]
160 fn fee_model_config_per_contract_dispatches() {
161 let model = PerContractFeeModel {
162 commission_per_contract: d("0.65"),
163 };
164 let cfg = FeeModelConfig::PerContract(model);
165 assert_eq!(
166 cfg.compute_fee(d("100"), d("10"), d("100")),
167 model.compute_fee(d("100"), d("10"), d("100")),
168 );
169 }
170
171 #[test]
172 fn fee_model_config_default_is_zero() {
173 assert_eq!(
174 FeeModelConfig::default(),
175 FeeModelConfig::Zero(ZeroFeeModel)
176 );
177 }
178
179 #[test]
182 fn percentage_fee_computes_rate_times_notional() {
183 let model = PercentageFeeModel { rate: d("0.001") };
185 assert_eq!(model.compute_fee(d("100"), d("10"), d("1")), d("1"));
187 }
188
189 #[test]
190 fn percentage_fee_uses_abs_quantity() {
191 let model = PercentageFeeModel { rate: d("0.001") };
192 assert_eq!(
193 model.compute_fee(d("100"), d("-10"), d("1")),
194 model.compute_fee(d("100"), d("10"), d("1")),
195 );
196 }
197
198 #[test]
199 fn fee_model_config_percentage_dispatches() {
200 let model = PercentageFeeModel { rate: d("0.001") };
201 let cfg = FeeModelConfig::Percentage(model);
202 assert_eq!(
203 cfg.compute_fee(d("100"), d("10"), d("1")),
204 model.compute_fee(d("100"), d("10"), d("1")),
205 );
206 }
207
208 #[test]
211 fn zero_fee_model_serde_roundtrip() {
212 let cfg = FeeModelConfig::Zero(ZeroFeeModel);
213 let json = serde_json::to_string(&cfg).unwrap();
214 assert_eq!(json, r#"{"Zero":null}"#);
215 let parsed: FeeModelConfig = serde_json::from_str(&json).unwrap();
216 assert_eq!(parsed, cfg);
217 }
218
219 #[test]
220 fn fee_model_config_default_when_field_omitted() {
221 #[derive(Deserialize)]
223 struct Wrapper {
224 #[serde(default)]
225 fee_model: FeeModelConfig,
226 }
227 let parsed: Wrapper = serde_json::from_str(r#"{}"#).unwrap();
228 assert_eq!(parsed.fee_model, FeeModelConfig::Zero(ZeroFeeModel));
229 }
230
231 #[test]
232 fn percentage_fee_model_serde_roundtrip() {
233 let cfg = FeeModelConfig::Percentage(PercentageFeeModel { rate: d("0.001") });
234 let json = serde_json::to_string(&cfg).unwrap();
235 assert_eq!(json, r#"{"Percentage":{"rate":"0.001"}}"#);
236 let parsed: FeeModelConfig = serde_json::from_str(&json).unwrap();
237 assert_eq!(parsed, cfg);
238 }
239
240 #[test]
241 fn per_contract_fee_model_serde_roundtrip() {
242 let cfg = FeeModelConfig::PerContract(PerContractFeeModel {
243 commission_per_contract: d("0.65"),
244 });
245 let json = serde_json::to_string(&cfg).unwrap();
246 assert_eq!(
247 json,
248 r#"{"PerContract":{"commission_per_contract":"0.65"}}"#
249 );
250 let parsed: FeeModelConfig = serde_json::from_str(&json).unwrap();
251 assert_eq!(parsed, cfg);
252 }
253}