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