finance_query/backtesting/config/
costs.rs1use std::fmt;
4use std::sync::Arc;
5
6use super::BacktestConfig;
7
8#[derive(Clone)]
27#[non_exhaustive]
28pub struct CommissionFn(Arc<dyn Fn(f64, f64) -> f64 + Send + Sync>);
29
30impl CommissionFn {
31 pub fn new<F>(f: F) -> Self
33 where
34 F: Fn(f64, f64) -> f64 + Send + Sync + 'static,
35 {
36 Self(Arc::new(f))
37 }
38
39 #[inline]
41 pub(crate) fn call(&self, size: f64, price: f64) -> f64 {
42 (self.0)(size, price)
43 }
44}
45
46impl fmt::Debug for CommissionFn {
47 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48 write!(f, "CommissionFn(<closure>)")
49 }
50}
51
52impl BacktestConfig {
53 pub fn calculate_commission(&self, size: f64, price: f64) -> f64 {
62 if let Some(ref f) = self.commission_fn {
63 f.call(size, price)
64 } else {
65 self.commission + (size * price * self.commission_pct)
66 }
67 }
68
69 pub fn apply_entry_slippage(&self, price: f64, is_long: bool) -> f64 {
71 if is_long {
72 price * (1.0 + self.slippage_pct)
73 } else {
74 price * (1.0 - self.slippage_pct)
75 }
76 }
77
78 pub fn apply_exit_slippage(&self, price: f64, is_long: bool) -> f64 {
80 if is_long {
81 price * (1.0 - self.slippage_pct)
82 } else {
83 price * (1.0 + self.slippage_pct)
84 }
85 }
86
87 pub fn apply_entry_spread(&self, price: f64, is_long: bool) -> f64 {
92 let half = self.spread_pct / 2.0;
93 if is_long {
94 price * (1.0 + half)
95 } else {
96 price * (1.0 - half)
97 }
98 }
99
100 pub fn apply_exit_spread(&self, price: f64, is_long: bool) -> f64 {
105 let half = self.spread_pct / 2.0;
106 if is_long {
107 price * (1.0 - half)
108 } else {
109 price * (1.0 + half)
110 }
111 }
112
113 pub fn calculate_transaction_tax(&self, trade_value: f64, is_buy: bool) -> f64 {
121 if is_buy {
122 trade_value * self.transaction_tax_pct
123 } else {
124 0.0
125 }
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 #[test]
134 fn test_commission_calculation() {
135 let config = BacktestConfig::builder()
136 .commission(5.0)
137 .commission_pct(0.01)
138 .build()
139 .unwrap();
140
141 let commission = config.calculate_commission(10.0, 100.0);
143 assert!((commission - 15.0).abs() < 0.01);
144 }
145
146 #[test]
147 fn test_slippage() {
148 let config = BacktestConfig::builder()
149 .slippage_pct(0.01) .build()
151 .unwrap();
152
153 let entry_price = config.apply_entry_slippage(100.0, true);
155 assert!((entry_price - 101.0).abs() < 0.01);
156
157 let exit_price = config.apply_exit_slippage(100.0, true);
159 assert!((exit_price - 99.0).abs() < 0.01);
160
161 let short_entry = config.apply_entry_slippage(100.0, false);
163 assert!((short_entry - 99.0).abs() < 0.01);
164
165 let short_exit = config.apply_exit_slippage(100.0, false);
167 assert!((short_exit - 101.0).abs() < 0.01);
168 }
169
170 #[test]
171 fn test_spread_entry_long() {
172 let config = BacktestConfig::builder()
173 .spread_pct(0.0004) .build()
175 .unwrap();
176 let price = config.apply_entry_spread(100.0, true);
178 assert!((price - 100.02).abs() < 1e-10);
179 }
180
181 #[test]
182 fn test_spread_exit_long() {
183 let config = BacktestConfig::builder()
184 .spread_pct(0.0004)
185 .build()
186 .unwrap();
187 let price = config.apply_exit_spread(100.0, true);
189 assert!((price - 99.98).abs() < 1e-10);
190 }
191
192 #[test]
193 fn test_spread_entry_short() {
194 let config = BacktestConfig::builder()
195 .spread_pct(0.0004)
196 .build()
197 .unwrap();
198 let price = config.apply_entry_spread(100.0, false);
200 assert!((price - 99.98).abs() < 1e-10);
201 }
202
203 #[test]
204 fn test_spread_exit_short() {
205 let config = BacktestConfig::builder()
206 .spread_pct(0.0004)
207 .build()
208 .unwrap();
209 let price = config.apply_exit_spread(100.0, false);
211 assert!((price - 100.02).abs() < 1e-10);
212 }
213
214 #[test]
215 fn test_spread_zero_is_noop() {
216 let config = BacktestConfig::default(); assert!((config.apply_entry_spread(123.45, true) - 123.45).abs() < 1e-10);
218 assert!((config.apply_exit_spread(123.45, false) - 123.45).abs() < 1e-10);
219 }
220
221 #[test]
222 fn test_transaction_tax_on_buy() {
223 let config = BacktestConfig::builder()
224 .transaction_tax_pct(0.005) .build()
226 .unwrap();
227 let tax = config.calculate_transaction_tax(10_000.0, true);
228 assert!((tax - 50.0).abs() < 1e-10);
229 }
230
231 #[test]
232 fn test_transaction_tax_not_on_sell() {
233 let config = BacktestConfig::builder()
234 .transaction_tax_pct(0.005)
235 .build()
236 .unwrap();
237 let tax = config.calculate_transaction_tax(10_000.0, false);
238 assert_eq!(tax, 0.0);
239 }
240
241 #[test]
242 fn test_transaction_tax_zero_default() {
243 let config = BacktestConfig::default();
244 assert_eq!(config.calculate_transaction_tax(100_000.0, true), 0.0);
245 }
246
247 #[test]
248 fn test_commission_fn_replaces_flat_and_pct() {
249 let config = BacktestConfig::builder()
251 .commission_fn(|size, _price| (size * 0.005_f64).max(1.00))
252 .build()
253 .unwrap();
254 let comm = config.calculate_commission(100.0, 50.0);
256 assert!((comm - 1.00).abs() < 1e-10);
257 let comm = config.calculate_commission(500.0, 50.0);
259 assert!((comm - 2.50).abs() < 1e-10);
260 }
261
262 #[test]
263 fn test_commission_fn_ignores_flat_and_pct_fields() {
264 let config = BacktestConfig::builder()
266 .commission(5.0)
267 .commission_pct(0.01)
268 .commission_fn(|size, price| size * price * 0.0005)
269 .build()
270 .unwrap();
271 let comm = config.calculate_commission(10.0, 100.0);
273 assert!((comm - 0.50).abs() < 1e-10);
274 }
275
276 #[test]
277 fn test_commission_fn_fallback_when_none() {
278 let config = BacktestConfig::builder()
280 .commission(1.0)
281 .commission_pct(0.002)
282 .build()
283 .unwrap();
284 let comm = config.calculate_commission(10.0, 100.0);
286 assert!((comm - 3.0).abs() < 1e-10);
287 }
288}