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