finance_query/backtesting/
config.rs1use serde::{Deserialize, Serialize};
4
5use super::error::{BacktestError, Result};
6
7#[non_exhaustive]
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct BacktestConfig {
29 pub initial_capital: f64,
31
32 pub commission: f64,
34
35 pub commission_pct: f64,
37
38 pub slippage_pct: f64,
40
41 pub position_size_pct: f64,
43
44 pub max_positions: Option<usize>,
46
47 pub allow_short: bool,
49
50 pub min_signal_strength: f64,
52
53 pub stop_loss_pct: Option<f64>,
55
56 pub take_profit_pct: Option<f64>,
58
59 pub close_at_end: bool,
61}
62
63impl Default for BacktestConfig {
64 fn default() -> Self {
65 Self {
66 initial_capital: 10_000.0,
67 commission: 0.0,
68 commission_pct: 0.001, slippage_pct: 0.001, position_size_pct: 1.0, max_positions: Some(1), allow_short: false,
73 min_signal_strength: 0.0,
74 stop_loss_pct: None,
75 take_profit_pct: None,
76 close_at_end: true,
77 }
78 }
79}
80
81impl BacktestConfig {
82 pub fn builder() -> BacktestConfigBuilder {
84 BacktestConfigBuilder::default()
85 }
86
87 pub fn validate(&self) -> Result<()> {
89 if self.initial_capital <= 0.0 {
90 return Err(BacktestError::invalid_param(
91 "initial_capital",
92 "must be positive",
93 ));
94 }
95
96 if self.commission < 0.0 {
97 return Err(BacktestError::invalid_param(
98 "commission",
99 "cannot be negative",
100 ));
101 }
102
103 if !(0.0..=1.0).contains(&self.commission_pct) {
104 return Err(BacktestError::invalid_param(
105 "commission_pct",
106 "must be between 0.0 and 1.0",
107 ));
108 }
109
110 if !(0.0..=1.0).contains(&self.slippage_pct) {
111 return Err(BacktestError::invalid_param(
112 "slippage_pct",
113 "must be between 0.0 and 1.0",
114 ));
115 }
116
117 if !(0.0..=1.0).contains(&self.position_size_pct) {
118 return Err(BacktestError::invalid_param(
119 "position_size_pct",
120 "must be between 0.0 and 1.0",
121 ));
122 }
123
124 if !(0.0..=1.0).contains(&self.min_signal_strength) {
125 return Err(BacktestError::invalid_param(
126 "min_signal_strength",
127 "must be between 0.0 and 1.0",
128 ));
129 }
130
131 if let Some(sl) = self.stop_loss_pct
132 && !(0.0..=1.0).contains(&sl)
133 {
134 return Err(BacktestError::invalid_param(
135 "stop_loss_pct",
136 "must be between 0.0 and 1.0",
137 ));
138 }
139
140 if let Some(tp) = self.take_profit_pct
141 && !(0.0..=1.0).contains(&tp)
142 {
143 return Err(BacktestError::invalid_param(
144 "take_profit_pct",
145 "must be between 0.0 and 1.0",
146 ));
147 }
148
149 Ok(())
150 }
151
152 pub fn calculate_commission(&self, trade_value: f64) -> f64 {
154 self.commission + (trade_value * self.commission_pct)
155 }
156
157 pub fn apply_entry_slippage(&self, price: f64, is_long: bool) -> f64 {
159 if is_long {
160 price * (1.0 + self.slippage_pct)
162 } else {
163 price * (1.0 - self.slippage_pct)
165 }
166 }
167
168 pub fn apply_exit_slippage(&self, price: f64, is_long: bool) -> f64 {
170 if is_long {
171 price * (1.0 - self.slippage_pct)
173 } else {
174 price * (1.0 + self.slippage_pct)
176 }
177 }
178
179 pub fn calculate_position_size(&self, available_capital: f64, price: f64) -> f64 {
181 let capital_to_use = available_capital * self.position_size_pct;
182
183 let adjusted_capital = capital_to_use / (1.0 + self.commission_pct);
189
190 adjusted_capital / price
191 }
192}
193
194#[derive(Default)]
196pub struct BacktestConfigBuilder {
197 config: BacktestConfig,
198}
199
200impl BacktestConfigBuilder {
201 pub fn initial_capital(mut self, capital: f64) -> Self {
203 self.config.initial_capital = capital;
204 self
205 }
206
207 pub fn commission(mut self, fee: f64) -> Self {
209 self.config.commission = fee;
210 self
211 }
212
213 pub fn commission_pct(mut self, pct: f64) -> Self {
215 self.config.commission_pct = pct;
216 self
217 }
218
219 pub fn slippage_pct(mut self, pct: f64) -> Self {
221 self.config.slippage_pct = pct;
222 self
223 }
224
225 pub fn position_size_pct(mut self, pct: f64) -> Self {
227 self.config.position_size_pct = pct;
228 self
229 }
230
231 pub fn max_positions(mut self, max: usize) -> Self {
233 self.config.max_positions = Some(max);
234 self
235 }
236
237 pub fn unlimited_positions(mut self) -> Self {
239 self.config.max_positions = None;
240 self
241 }
242
243 pub fn allow_short(mut self, allow: bool) -> Self {
245 self.config.allow_short = allow;
246 self
247 }
248
249 pub fn min_signal_strength(mut self, threshold: f64) -> Self {
251 self.config.min_signal_strength = threshold;
252 self
253 }
254
255 pub fn stop_loss_pct(mut self, pct: f64) -> Self {
257 self.config.stop_loss_pct = Some(pct);
258 self
259 }
260
261 pub fn take_profit_pct(mut self, pct: f64) -> Self {
263 self.config.take_profit_pct = Some(pct);
264 self
265 }
266
267 pub fn close_at_end(mut self, close: bool) -> Self {
269 self.config.close_at_end = close;
270 self
271 }
272
273 pub fn build(self) -> Result<BacktestConfig> {
275 self.config.validate()?;
276 Ok(self.config)
277 }
278}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283
284 #[test]
285 fn test_default_config() {
286 let config = BacktestConfig::default();
287 assert_eq!(config.initial_capital, 10_000.0);
288 assert!(config.validate().is_ok());
289 }
290
291 #[test]
292 fn test_builder() {
293 let config = BacktestConfig::builder()
294 .initial_capital(50_000.0)
295 .commission_pct(0.002)
296 .allow_short(true)
297 .stop_loss_pct(0.05)
298 .take_profit_pct(0.10)
299 .build()
300 .unwrap();
301
302 assert_eq!(config.initial_capital, 50_000.0);
303 assert_eq!(config.commission_pct, 0.002);
304 assert!(config.allow_short);
305 assert_eq!(config.stop_loss_pct, Some(0.05));
306 assert_eq!(config.take_profit_pct, Some(0.10));
307 }
308
309 #[test]
310 fn test_validation_failures() {
311 assert!(
312 BacktestConfig::builder()
313 .initial_capital(-100.0)
314 .build()
315 .is_err()
316 );
317
318 assert!(
319 BacktestConfig::builder()
320 .commission_pct(1.5)
321 .build()
322 .is_err()
323 );
324
325 assert!(
326 BacktestConfig::builder()
327 .stop_loss_pct(2.0)
328 .build()
329 .is_err()
330 );
331 }
332
333 #[test]
334 fn test_commission_calculation() {
335 let config = BacktestConfig::builder()
336 .commission(5.0)
337 .commission_pct(0.01)
338 .build()
339 .unwrap();
340
341 let commission = config.calculate_commission(1000.0);
343 assert!((commission - 15.0).abs() < 0.01);
344 }
345
346 #[test]
347 fn test_slippage() {
348 let config = BacktestConfig::builder()
349 .slippage_pct(0.01) .build()
351 .unwrap();
352
353 let entry_price = config.apply_entry_slippage(100.0, true);
355 assert!((entry_price - 101.0).abs() < 0.01);
356
357 let exit_price = config.apply_exit_slippage(100.0, true);
359 assert!((exit_price - 99.0).abs() < 0.01);
360
361 let short_entry = config.apply_entry_slippage(100.0, false);
363 assert!((short_entry - 99.0).abs() < 0.01);
364
365 let short_exit = config.apply_exit_slippage(100.0, false);
367 assert!((short_exit - 101.0).abs() < 0.01);
368 }
369
370 #[test]
371 fn test_position_sizing() {
372 let config = BacktestConfig::builder()
373 .position_size_pct(0.5) .commission_pct(0.0) .build()
376 .unwrap();
377
378 let size = config.calculate_position_size(10_000.0, 100.0);
380 assert!((size - 50.0).abs() < 0.01);
381 }
382
383 #[test]
384 fn test_position_sizing_with_commission() {
385 let config = BacktestConfig::builder()
386 .position_size_pct(0.5) .commission_pct(0.001) .build()
389 .unwrap();
390
391 let size = config.calculate_position_size(10_000.0, 100.0);
395 let expected = 5000.0 / 1.001 / 100.0;
396 assert!((size - expected).abs() < 0.01);
397 }
398}