Backtesting¶
Test trading strategies against historical data. The backtesting engine provides pre-built strategies, a strategy builder for custom logic, and comprehensive performance metrics.
Enable Feature¶
Backtesting requires the backtesting feature (which depends on indicators):
Pre-built Strategies¶
SMA Crossover¶
Dual Simple Moving Average crossover:
use finance_query::{Ticker, Interval, TimeRange};
use finance_query::backtesting::SmaCrossover;
let ticker = Ticker::new("AAPL").await?;
let result = ticker.backtest(
SmaCrossover::new(10, 20), // fast=10, slow=20
Interval::OneDay,
TimeRange::OneYear,
None,
).await?;
println!("Total Return: {:.2}%", result.metrics.total_return_pct);
println!("Sharpe Ratio: {:.2}", result.metrics.sharpe_ratio);
println!("Max Drawdown: {:.2}%", result.metrics.max_drawdown_pct * 100.0);
RSI Mean Reversion¶
Reversal strategy using Relative Strength Index:
use finance_query::backtesting::RsiReversal;
let result = ticker.backtest(
RsiReversal::new(14), // period (uses default thresholds: 30/70)
Interval::OneDay,
TimeRange::OneYear,
None,
).await?;
// Or with custom thresholds:
let result = ticker.backtest(
RsiReversal::new(14).with_thresholds(30, 70),
Interval::OneDay,
TimeRange::OneYear,
None,
).await?;
MACD Signal Crossover¶
MACD line crosses signal line:
use finance_query::backtesting::MacdSignal;
let result = ticker.backtest(
MacdSignal::new(12, 26, 9), // fast, slow, signal
Interval::OneDay,
TimeRange::OneYear,
None,
).await?;
Bollinger Band Mean Reversion¶
Buy at lower band, sell at upper band:
use finance_query::backtesting::BollingerMeanReversion;
let result = ticker.backtest(
BollingerMeanReversion::new(20, 2.0), // period, std_dev
Interval::OneDay,
TimeRange::OneYear,
None,
).await?;
SuperTrend Trend Following¶
Follow trends using ATR-based SuperTrend:
use finance_query::backtesting::SuperTrendFollow;
let result = ticker.backtest(
SuperTrendFollow::new(10, 3.0), // period, multiplier
Interval::OneDay,
TimeRange::OneYear,
None,
).await?;
Donchian Breakout¶
Channel breakout strategy:
use finance_query::backtesting::DonchianBreakout;
let result = ticker.backtest(
DonchianBreakout::new(20), // lookback period
Interval::OneDay,
TimeRange::OneYear,
None,
).await?;
Custom Strategies¶
Build custom strategies with the StrategyBuilder:
use finance_query::backtesting::StrategyBuilder;
use finance_query::backtesting::refs::*;
use finance_query::backtesting::condition::*;
let strategy = StrategyBuilder::new("RSI Mean Reversion")
.entry(
rsi(14)
.crosses_below(30.0)
.and(price().above_ref(sma(200)))
)
.exit(
rsi(14)
.crosses_above(70.0)
.or(stop_loss(0.05))
)
.build();
let result = ticker.backtest(
strategy,
Interval::OneDay,
TimeRange::OneYear,
None,
).await?;
Configuration¶
Customize backtesting behavior:
use finance_query::backtesting::BacktestConfig;
let config = BacktestConfig::builder()
.initial_capital(50_000.0) // Starting balance
.commission_pct(0.001) // 0.1% per trade
.slippage_pct(0.0005) // 0.05% slippage
.stop_loss_pct(0.05) // 5% stop-loss
.take_profit_pct(0.15) // 15% take-profit
.allow_short(true) // Allow short selling
.build()?;
let result = ticker.backtest(
SmaCrossover::new(10, 20),
Interval::OneDay,
TimeRange::OneYear,
Some(config),
).await?;
Performance Metrics¶
Access comprehensive backtest results:
let result = ticker.backtest(...).await?;
// Overall returns
println!("Total Return: {:.2}%", result.metrics.total_return_pct);
println!("Annualized Return: {:.2}%", result.metrics.annualized_return_pct);
// Risk metrics
println!("Sharpe Ratio: {:.2}", result.metrics.sharpe_ratio);
println!("Sortino Ratio: {:.2}", result.metrics.sortino_ratio);
println!("Max Drawdown: {:.2}%", result.metrics.max_drawdown_pct * 100.0);
println!("Calmar Ratio: {:.2}", result.metrics.calmar_ratio);
// Trade statistics
println!("Total Trades: {}", result.metrics.total_trades);
println!("Win Rate: {:.2}%", result.metrics.win_rate * 100.0);
println!("Profit Factor: {:.2}", result.metrics.profit_factor);
println!("Average Win: {:.2}%", result.metrics.avg_win_pct);
println!("Average Loss: {:.2}%", result.metrics.avg_loss_pct);
// Trading activity
println!("Long Trades: {}", result.metrics.long_trades);
println!("Short Trades: {}", result.metrics.short_trades);
Available Indicators¶
Use any of 40+ indicators in strategy conditions:
Moving Averages:
sma, ema, wma, dema, tema, hma, vwma, alma, mcginley
Oscillators:
rsi, stochastic, stochastic_rsi, cci, williams_r, cmo, awesome_oscillator
Trend:
macd, adx, aroon, supertrend, ichimoku, parabolic_sar
Volatility:
atr, bollinger, keltner, donchian, choppiness_index
Volume:
obv, vwap, mfi, cmf, chaikin_oscillator, accumulation_distribution, balance_of_power
Available Conditions¶
Comparisons:
above(threshold)- Price above valuebelow(threshold)- Price below valuecrosses_above(threshold)- Indicator crosses above valuecrosses_below(threshold)- Indicator crosses below valuebetween(lower, upper)- Indicator between valuesequals(value)- Indicator equals value
Composites:
and()- Both conditions trueor()- Either condition truenot()- Negate condition
Position Management:
stop_loss(pct)- Exit on loss percentagetake_profit(pct)- Exit on profit percentagetrailing_stop(pct)- Exit if price retracestrailing_take_profit(pct)- Exit if profit retraces
Position State:
has_position()- Currently holding positionno_position()- Not holding positionis_long()- Currently longis_short()- Currently shortin_profit()- Position is profitablein_loss()- Position is in loss
Reference Signals¶
Access price and indicator values in conditions:
price()- Close priceopen()- Open pricehigh()- High pricelow()- Low pricevolume()- Volume
Trading Modes¶
Strategies support both long and short:
// Long only (default)
let strategy = SmaCrossover::new(10, 20);
// With configuration for short selling
let config = BacktestConfig::builder()
.allow_short(true)
.build()?;
let result = ticker.backtest(strategy, interval, range, Some(config)).await?;
Example: Complete Strategy¶
use finance_query::{Ticker, Interval, TimeRange};
use finance_query::backtesting::{StrategyBuilder, BacktestConfig};
use finance_query::backtesting::refs::*;
use finance_query::backtesting::condition::*;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let ticker = Ticker::new("AAPL").await?;
// Custom momentum strategy
let strategy = StrategyBuilder::new("Momentum with Risk Management")
.entry(
{
let m = macd(12, 26, 9);
m.line().crosses_above_ref(m.signal_line())
.and(price().above_ref(ema(50)))
.and(volume().above_ref(sma(20)))
}
)
.exit(
{
let m = macd(12, 26, 9);
m.line().crosses_below_ref(m.signal_line())
.or(stop_loss(0.08))
.or(take_profit(0.15))
}
)
.build();
// Configuration
let config = BacktestConfig::builder()
.initial_capital(100_000.0)
.commission_pct(0.001)
.slippage_pct(0.0005)
.allow_short(false)
.build()?;
// Run backtest
let result = ticker.backtest(
strategy,
Interval::OneDay,
TimeRange::OneYear,
Some(config),
).await?;
// Print results
println!("Backtest Results for AAPL");
println!("=========================");
println!("Total Return: {:.2}%", result.metrics.total_return_pct);
println!("Sharpe Ratio: {:.2}", result.metrics.sharpe_ratio);
println!("Win Rate: {:.2}%", result.metrics.win_rate * 100.0);
println!("Total Trades: {}", result.metrics.total_trades);
println!("Max Drawdown: {:.2}%", result.metrics.max_drawdown_pct * 100.0);
Ok(())
}
Best Practices¶
Design Robust Strategies
- Test multiple timeframes - Validate strategies on different intervals and date ranges to avoid overfitting
- Use realistic assumptions - Set appropriate commission, slippage, and position sizing
- Avoid lookahead bias - Only use data that would have been available at the time of each trade
- Validate with walk-forward testing - Test on out-of-sample data to ensure strategy generalizes
- Combine indicators - Use multiple confirming signals rather than single indicator strategies
// Good: Realistic configuration with multiple confirmations
let config = BacktestConfig::builder()
.initial_capital(10_000.0)
.commission_pct(0.001) // 0.1% per trade (realistic for retail)
.slippage_pct(0.0005) // 0.05% slippage
.allow_short(false) // Match your actual trading permissions
.build()?;
let strategy = StrategyBuilder::new("Validated Strategy")
.entry(
rsi(14).crosses_below(30.0)
.and(price().above_ref(sma(200))) // Trend filter
.and(volume().above_ref(sma(20))) // Volume confirmation
)
.exit(
rsi(14).crosses_above(70.0)
.or(stop_loss(0.05)) // Risk management
.or(take_profit(0.15))
)
.build();
// Test on multiple periods
let test1 = ticker.backtest(strategy.clone(), Interval::OneDay, TimeRange::OneYear, Some(config.clone())).await?;
let test2 = ticker.backtest(strategy.clone(), Interval::OneDay, TimeRange::TwoYears, Some(config.clone())).await?;
// Less robust: Unrealistic assumptions, single indicator
let bad_config = BacktestConfig::builder()
.initial_capital(10_000.0)
.commission_pct(0.0) // No commission (unrealistic)
.slippage_pct(0.0) // No slippage (unrealistic)
.build()?;
let simple_strategy = StrategyBuilder::new("Simple")
.entry(rsi(14).crosses_below(30.0)) // Single indicator, no confirmation
.exit(rsi(14).crosses_above(70.0))
.build();
Common Pitfalls
- Overfitting - Strategies that work perfectly on historical data often fail in live trading. Use simple rules and validate on multiple periods.
- Ignoring costs - Commission and slippage significantly impact returns, especially for high-frequency strategies.
- Position sizing - Default 100% capital allocation is aggressive. Consider using smaller position sizes.
- Survivor bias - Backtesting on current index constituents ignores delisted/bankrupt companies.
- Data quality - Yahoo Finance data may have gaps or inaccuracies. Validate important results.
Next Steps¶
- Technical Indicators - Complete reference for all 52+ available indicators
- Ticker API - Learn how to fetch data and run backtests
- DataFrame Support - Analyze backtest results in Polars DataFrames
- Models Reference - Understanding backtest result structures