Skip to main content

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}