finance_query/backtesting/config/builder.rs
1//! Builder for [`BacktestConfig`].
2
3use super::{BacktestConfig, CommissionFn, PositionSizing};
4use crate::backtesting::error::Result;
5
6/// Builder for BacktestConfig
7#[derive(Default)]
8#[non_exhaustive]
9pub struct BacktestConfigBuilder {
10 config: BacktestConfig,
11}
12
13impl BacktestConfigBuilder {
14 /// Set initial capital
15 pub fn initial_capital(mut self, capital: f64) -> Self {
16 self.config.initial_capital = capital;
17 self
18 }
19
20 /// Set flat commission per trade
21 pub fn commission(mut self, fee: f64) -> Self {
22 self.config.commission = fee;
23 self
24 }
25
26 /// Set commission as percentage of trade value
27 pub fn commission_pct(mut self, pct: f64) -> Self {
28 self.config.commission_pct = pct;
29 self
30 }
31
32 /// Set slippage as percentage of price
33 pub fn slippage_pct(mut self, pct: f64) -> Self {
34 self.config.slippage_pct = pct;
35 self
36 }
37
38 /// Set position size as fraction of available equity
39 pub fn position_size_pct(mut self, pct: f64) -> Self {
40 self.config.position_size_pct = pct;
41 self
42 }
43
44 /// Set maximum concurrent positions
45 pub fn max_positions(mut self, max: usize) -> Self {
46 self.config.max_positions = Some(max);
47 self
48 }
49
50 /// Allow unlimited concurrent positions
51 pub fn unlimited_positions(mut self) -> Self {
52 self.config.max_positions = None;
53 self
54 }
55
56 /// Allow or disallow short selling
57 pub fn allow_short(mut self, allow: bool) -> Self {
58 self.config.allow_short = allow;
59 self
60 }
61
62 /// Set minimum signal strength threshold
63 pub fn min_signal_strength(mut self, threshold: f64) -> Self {
64 self.config.min_signal_strength = threshold;
65 self
66 }
67
68 /// Set stop-loss percentage (auto-exit if loss exceeds this)
69 pub fn stop_loss_pct(mut self, pct: f64) -> Self {
70 self.config.stop_loss_pct = Some(pct);
71 self
72 }
73
74 /// Set take-profit percentage (auto-exit if profit exceeds this)
75 pub fn take_profit_pct(mut self, pct: f64) -> Self {
76 self.config.take_profit_pct = Some(pct);
77 self
78 }
79
80 /// Set whether to close open positions at end of backtest
81 pub fn close_at_end(mut self, close: bool) -> Self {
82 self.config.close_at_end = close;
83 self
84 }
85
86 /// Set annual risk-free rate for Sharpe/Sortino/Calmar calculations (0.0 - 1.0)
87 ///
88 /// Use the current T-bill rate for accurate ratios (e.g. `0.05` for 5%).
89 pub fn risk_free_rate(mut self, rate: f64) -> Self {
90 self.config.risk_free_rate = rate;
91 self
92 }
93
94 /// Set trailing stop percentage (0.0 - 1.0).
95 ///
96 /// For longs: exits when price drops this fraction below its peak since entry.
97 /// For shorts: exits when price rises this fraction above its trough since entry.
98 pub fn trailing_stop_pct(mut self, pct: f64) -> Self {
99 self.config.trailing_stop_pct = Some(pct);
100 self
101 }
102
103 /// Enable or disable dividend reinvestment
104 ///
105 /// When `true`, dividend income is reinvested (added to P&L as additional hypothetical shares).
106 pub fn reinvest_dividends(mut self, reinvest: bool) -> Self {
107 self.config.reinvest_dividends = reinvest;
108 self
109 }
110
111 /// Set the number of bars per calendar year for annualisation.
112 ///
113 /// Defaults to `252.0` (US equity daily bars). Common values:
114 /// - `252.0` — daily US equity
115 /// - `52.0` — weekly
116 /// - `12.0` — monthly
117 /// - `252.0 * 6.5` (≈ 1638) — hourly (6.5-hour trading day)
118 pub fn bars_per_year(mut self, n: f64) -> Self {
119 self.config.bars_per_year = n;
120 self
121 }
122
123 /// Set symmetric bid-ask spread as a fraction of price (0.0 – 1.0).
124 ///
125 /// Half the spread is applied adversely on entry and half on exit,
126 /// independent of [`slippage_pct`](BacktestConfig::slippage_pct).
127 /// For example, `0.0002` represents a 2-basis-point spread (1 bp per side).
128 pub fn spread_pct(mut self, pct: f64) -> Self {
129 self.config.spread_pct = pct;
130 self
131 }
132
133 /// Set the transaction tax as a fraction of trade value, applied on buys only.
134 ///
135 /// Models purchase taxes such as UK Stamp Duty (0.005 = 0.5 %). Applied on
136 /// long entries and short covers; not applied on sells.
137 pub fn transaction_tax_pct(mut self, pct: f64) -> Self {
138 self.config.transaction_tax_pct = pct;
139 self
140 }
141
142 /// Set a custom commission function `f(size, price) -> commission`.
143 ///
144 /// Replaces the flat [`commission`](BacktestConfig::commission) and
145 /// percentage [`commission_pct`](BacktestConfig::commission_pct) fields.
146 /// Use this to model broker-specific fee schedules.
147 ///
148 /// # Example
149 ///
150 /// ```
151 /// use finance_query::backtesting::BacktestConfig;
152 ///
153 /// // $0.005 per share, minimum $1.00 per order
154 /// let config = BacktestConfig::builder()
155 /// .commission_fn(|size, price| (size * 0.005_f64).max(1.00))
156 /// .build()
157 /// .unwrap();
158 /// ```
159 pub fn commission_fn<F>(mut self, f: F) -> Self
160 where
161 F: Fn(f64, f64) -> f64 + Send + Sync + 'static,
162 {
163 self.config.commission_fn = Some(CommissionFn::new(f));
164 self
165 }
166
167 /// Set the maximum gross exposure as a multiple of equity.
168 ///
169 /// Values above `1.0` open a margin loan for the shortfall. Pair with
170 /// [`margin_interest_rate`](Self::margin_interest_rate); left at zero,
171 /// leverage costs nothing and inflates every levered result.
172 pub fn max_leverage(mut self, leverage: f64) -> Self {
173 self.config.max_leverage = leverage;
174 self
175 }
176
177 /// Set the equity floor as a fraction of gross exposure (0.0 - 1.0).
178 ///
179 /// The engine liquidates the position when equity falls below it.
180 /// Consulted for any levered position, and for a short at any leverage;
181 /// an unlevered long is never checked.
182 pub fn maintenance_margin_pct(mut self, pct: f64) -> Self {
183 self.config.maintenance_margin_pct = pct;
184 self
185 }
186
187 /// Set the annual borrow rate charged while a short position is open.
188 pub fn short_borrow_rate(mut self, rate: f64) -> Self {
189 self.config.short_borrow_rate = rate;
190 self
191 }
192
193 /// Set the annual interest rate charged on a debit cash balance.
194 pub fn margin_interest_rate(mut self, rate: f64) -> Self {
195 self.config.margin_interest_rate = rate;
196 self
197 }
198
199 /// Set the position sizing scheme.
200 ///
201 /// Schemes other than [`PositionSizing::FixedFraction`] size at or below
202 /// [`position_size_pct`](BacktestConfig::position_size_pct), which stays the
203 /// risk budget for the run.
204 pub fn position_sizing(mut self, sizing: PositionSizing) -> Self {
205 self.config.position_sizing = sizing;
206 self
207 }
208
209 /// Build and validate the configuration
210 pub fn build(self) -> Result<BacktestConfig> {
211 self.config.validate()?;
212 Ok(self.config)
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219
220 #[test]
221 fn test_builder() {
222 let config = BacktestConfig::builder()
223 .initial_capital(50_000.0)
224 .commission_pct(0.002)
225 .allow_short(true)
226 .stop_loss_pct(0.05)
227 .take_profit_pct(0.10)
228 .build()
229 .unwrap();
230
231 assert_eq!(config.initial_capital, 50_000.0);
232 assert_eq!(config.commission_pct, 0.002);
233 assert!(config.allow_short);
234 assert_eq!(config.stop_loss_pct, Some(0.05));
235 assert_eq!(config.take_profit_pct, Some(0.10));
236 }
237
238 #[test]
239 fn test_trailing_stop() {
240 let config = BacktestConfig::builder()
241 .trailing_stop_pct(0.05)
242 .build()
243 .unwrap();
244 assert_eq!(config.trailing_stop_pct, Some(0.05));
245
246 // Out-of-range should fail
247 assert!(
248 BacktestConfig::builder()
249 .trailing_stop_pct(1.5)
250 .build()
251 .is_err()
252 );
253 }
254}