Backtesting¶
Cargo Docs
Test trading strategies against historical data. The backtesting engine provides pre-built strategies, a custom strategy builder, ensemble composition, parameter optimization, walk-forward validation, Monte Carlo simulation, and portfolio-level backtesting.
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.0, 70.0),
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 StrategyBuilder. Entry conditions are combined with AND; exit conditions with OR (any exit triggers):
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?;
Regime Filter¶
Suppress entry signals unless a regime condition passes (e.g., only trade in uptrends):
let strategy = StrategyBuilder::new("Trend-Filtered RSI")
.entry(rsi(14).crosses_below(30.0))
.exit(rsi(14).crosses_above(70.0))
.regime_filter(price().above_ref(sma(200))) // only enter if price > SMA(200)
.build();
Separate Short Leg¶
Define independent entry/exit conditions for short positions:
let strategy = StrategyBuilder::new("Long-Short RSI")
.entry(rsi(14).crosses_below(30.0)) // long entry
.exit(rsi(14).crosses_above(70.0)) // long exit
.with_short(
rsi(14).crosses_above(70.0), // short entry
rsi(14).crosses_below(30.0), // short exit
)
.build();
Warmup Period¶
Skip the first N bars before generating signals (useful when indicators need time to stabilize):
let strategy = StrategyBuilder::new("SMA with Warmup")
.entry(price().crosses_above_ref(sma(200)))
.exit(price().crosses_below_ref(sma(200)))
.warmup(200) // skip first 200 bars
.build();
Configuration¶
Customize backtesting behavior with BacktestConfig:
use finance_query::backtesting::BacktestConfig;
let config = BacktestConfig::builder()
.initial_capital(50_000.0)
.commission_pct(0.001) // 0.1% per trade
.commission(1.0) // $1 flat fee per trade
.slippage_pct(0.0005) // 0.05% slippage
.spread_pct(0.0002) // 0.02% bid-ask spread (half each side)
.transaction_tax_pct(0.005) // 0.5% stamp duty on buys
.stop_loss_pct(0.05) // 5% global stop-loss
.take_profit_pct(0.15) // 15% global take-profit
.trailing_stop_pct(0.03) // 3% trailing stop
.allow_short(true)
.position_size_pct(0.5) // use 50% of capital per trade
.max_positions(3) // at most 3 concurrent positions
.bars_per_year(252.0) // for annualized metric calculations
.risk_free_rate(0.04) // 4% annual risk-free rate
.reinvest_dividends(true)
.close_at_end(true) // close open positions at final bar
.build()?;
let result = ticker.backtest(
SmaCrossover::new(10, 20),
Interval::OneDay,
TimeRange::OneYear,
Some(config),
).await?;
Zero-Cost Config¶
Convenience constructor with all friction zeroed — useful for theoretical comparisons:
Custom Commission Function¶
Replace flat + percentage commission with a custom function:
let config = BacktestConfig::builder()
.initial_capital(10_000.0)
.commission_fn(|size, price| {
// Example: tiered commission
let value = size * price;
if value < 1_000.0 { 1.0 } else { value * 0.0005 }
})
.build()?;
Performance Metrics¶
Access the full set of performance metrics from result.metrics:
let result = ticker.backtest(SmaCrossover::new(10, 20), Interval::OneDay, TimeRange::OneYear, None).await?;
// Returns
println!("Total Return: {:.2}%", result.metrics.total_return_pct);
println!("Annualized Return: {:.2}%", result.metrics.annualized_return_pct);
println!("Final Equity: ${:.2}", result.final_equity);
// Risk-adjusted
println!("Sharpe Ratio: {:.2}", result.metrics.sharpe_ratio);
println!("Sortino Ratio: {:.2}", result.metrics.sortino_ratio);
println!("Calmar Ratio: {:.2}", result.metrics.calmar_ratio);
println!("Max Drawdown: {:.2}%", result.metrics.max_drawdown_pct * 100.0);
// Trade statistics
println!("Total Trades: {}", result.metrics.total_trades);
println!("Winning Trades: {}", result.metrics.winning_trades);
println!("Losing Trades: {}", result.metrics.losing_trades);
println!("Win Rate: {:.2}%", result.metrics.win_rate * 100.0);
println!("Profit Factor: {:.2}", result.metrics.profit_factor);
println!("Avg Trade: {:.2}%", result.metrics.avg_trade_return_pct);
println!("Avg Win: {:.2}%", result.metrics.avg_win_pct);
println!("Avg Loss: {:.2}%", result.metrics.avg_loss_pct);
println!("Largest Win: {:.2}%", result.metrics.largest_win);
println!("Largest Loss: {:.2}%", result.metrics.largest_loss);
println!("Max Consec. Wins: {}", result.metrics.max_consecutive_wins);
println!("Max Consec. Losses: {}", result.metrics.max_consecutive_losses);
// Position breakdown
println!("Long Trades: {}", result.metrics.long_trades);
println!("Short Trades: {}", result.metrics.short_trades);
println!("Time in Market: {:.1}%", result.metrics.time_in_market_pct * 100.0);
// Signal execution
println!("Total Signals: {}", result.metrics.total_signals);
println!("Executed Signals: {}", result.metrics.executed_signals);
println!("Total Commission: ${:.2}", result.metrics.total_commission);
println!("Dividend Income: ${:.2}", result.metrics.total_dividend_income);
// Advanced statistics
println!("Kelly Criterion: {:.2}", result.metrics.kelly_criterion);
println!("SQN: {:.2}", result.metrics.sqn);
println!("Expectancy: {:.2}", result.metrics.expectancy);
println!("Omega Ratio: {:.2}", result.metrics.omega_ratio);
println!("Tail Ratio: {:.2}", result.metrics.tail_ratio);
println!("Recovery Factor: {:.2}", result.metrics.recovery_factor);
println!("Ulcer Index: {:.2}", result.metrics.ulcer_index);
println!("Serenity Ratio: {:.2}", result.metrics.serenity_ratio);
Advanced Result Analysis¶
Rolling Analytics¶
let sharpe_30 = result.rolling_sharpe(30); // rolling 30-bar Sharpe ratio
let drawdowns = result.drawdown_series(); // drawdown at each equity point
let win_rate_20 = result.rolling_win_rate(20); // rolling 20-trade win rate
Temporal Breakdown¶
// Break performance down by calendar period
let by_year = result.by_year(); // HashMap<i32, PerformanceMetrics>
let by_month = result.by_month(); // HashMap<(i32, u32), PerformanceMetrics>
let by_dow = result.by_day_of_week(); // HashMap<Weekday, PerformanceMetrics>
for (year, metrics) in &by_year {
println!("{year}: {:.2}%", metrics.total_return_pct);
}
Tag-Based Filtering¶
Tag signals and trades to analyze subsets of your strategy:
let tagged_trades = result.trades_by_tag("breakout");
let tagged_metrics = result.metrics_by_tag("breakout");
let all_tags = result.all_tags();
Diagnostics¶
Engine warnings and notes (e.g., skipped bars, insufficient capital):
Order Types¶
By default all signals fill at market. Use limit, stop, and stop-limit orders for more realistic fills:
use finance_query::backtesting::Signal;
let ts = 0i64; // use actual candle timestamp in practice
let px = 150.0; // use actual candle close in practice
// Market (default)
let market_entry = Signal::long(ts, px);
// Buy limit — fill only if price reaches limit_price (below current)
let limit_entry = Signal::buy_limit(ts, px, 148.0);
// Buy stop — fill when price breaks above stop_price
let stop_entry = Signal::buy_stop(ts, px, 152.0);
// Buy stop-limit — trigger at stop, fill at limit or better
let stop_limit = Signal::buy_stop_limit(ts, px, 152.0, 153.0);
// Sell limit / stop for exits
let limit_exit = Signal::sell_limit(ts, px, 160.0);
let stop_exit = Signal::sell_stop(ts, px, 145.0);
Order Expiry¶
Pending orders that haven't filled cancel after N bars:
let signal = Signal::buy_limit(ts, px, 148.0)
.expires_in_bars(5); // cancel if not filled within 5 bars
Per-Trade Bracket Orders¶
Override global stop-loss / take-profit / trailing-stop on a per-signal basis:
let signal = Signal::long(ts, px)
.stop_loss(0.03) // 3% stop for this trade
.take_profit(0.10) // 10% take-profit for this trade
.trailing_stop(0.02); // 2% trailing stop for this trade
Scale In / Out¶
Add to or partially exit an existing position:
let add_to_position = Signal::scale_in(0.25, ts, px); // add 25% of position size
let reduce_position = Signal::scale_out(0.50, ts, px); // exit 50% of position
Signal Tags¶
Label signals for post-backtest filtering with trades_by_tag / metrics_by_tag:
Ensemble Strategy¶
Combine multiple strategies and aggregate their signals with a voting rule:
use finance_query::backtesting::{EnsembleStrategy, EnsembleMode, SmaCrossover, RsiReversal};
let ensemble = EnsembleStrategy::new("Ensemble")
.add(SmaCrossover::new(10, 50), 0.6)
.add(RsiReversal::new(14), 0.4)
.mode(EnsembleMode::WeightedMajority)
.build();
let result = ticker.backtest(ensemble, Interval::OneDay, TimeRange::OneYear, None).await?;
Voting modes:
| Mode | Description |
|---|---|
WeightedMajority |
Entry if weighted vote share > 50% (default) |
Unanimous |
Entry only if all members agree |
AnySignal |
Entry if any member signals |
StrongestSignal |
Entry from the member with the highest signal strength |
Higher-Timeframe Conditions¶
Evaluate a condition on a coarser timeframe within a lower-timeframe strategy using htf():
use finance_query::backtesting::refs::*;
use finance_query::backtesting::condition::*;
use finance_query::backtesting::refs::htf;
use finance_query::{Interval, Region};
// Use daily RSI as a filter inside a 15-minute strategy
let strategy = StrategyBuilder::new("HTF RSI Filter")
.entry(
rsi(14).crosses_below(30.0)
.and(htf(Interval::OneDay, rsi(14).above(40.0)))
)
.exit(rsi(14).crosses_above(70.0))
.build();
HTF scope applies to computed indicators (RSI, SMA, MACD, etc.). Price-action refs (price(), volume(), etc.) always stay on the base timeframe.
Benchmark Comparison¶
Compare your strategy against a benchmark symbol:
let result = ticker.backtest_with_benchmark(
SmaCrossover::new(10, 50),
Interval::OneDay,
TimeRange::OneYear,
None,
"SPY", // benchmark symbol
).await?;
if let Some(bench) = &result.benchmark {
println!("Strategy return: {:.2}%", result.metrics.total_return_pct);
println!("Benchmark return: {:.2}%", bench.benchmark_return_pct);
println!("Buy & hold return: {:.2}%", bench.buy_and_hold_return_pct);
println!("Alpha: {:.4}", bench.alpha);
println!("Beta: {:.4}", bench.beta);
println!("Information Ratio: {:.4}", bench.information_ratio);
}
Strategy Comparison¶
Rank multiple strategy results by a chosen metric:
use finance_query::backtesting::{BacktestComparison, OptimizeMetric};
let report = BacktestComparison::new()
.add("SMA Crossover", result_sma)
.add("RSI Reversal", result_rsi)
.add("MACD Signal", result_macd)
.ranked_by(OptimizeMetric::SharpeRatio);
println!("Winner: {}", report.winner());
for row in report.table() {
println!(
"#{} {} — Sharpe {:.2}, Return {:.2}%",
row.rank, row.label, row.sharpe_ratio, row.total_return_pct,
);
}
Parameter Optimization¶
Grid Search¶
Exhaustive parallel search over all parameter combinations:
use finance_query::backtesting::{GridSearch, ParamRange, OptimizeMetric, BacktestConfig};
let config = BacktestConfig::zero_cost();
let report = GridSearch::new()
.param("fast", ParamRange::int_range(5, 20, 5))
.param("slow", ParamRange::int_range(20, 60, 10))
.optimize_for(OptimizeMetric::SharpeRatio)
.run("AAPL", &candles, &config, |params| {
SmaCrossover::new(
params["fast"].as_int() as usize,
params["slow"].as_int() as usize,
)
})?;
println!("Best Sharpe: {:.2}", report.best.result.metrics.sharpe_ratio);
println!("Best params: fast={}, slow={}",
report.best.params["fast"].as_int(),
report.best.params["slow"].as_int(),
);
println!("Evaluated {} combinations", report.n_evaluations);
Bayesian Search (SAMBO)¶
Efficient adaptive search using a surrogate model — much faster for larger parameter spaces:
use finance_query::backtesting::BayesianSearch;
let report = BayesianSearch::new()
.param("fast", ParamRange::int_bounds(5, 50))
.param("slow", ParamRange::int_bounds(20, 200))
.max_evaluations(100)
.initial_points(10)
.ucb_beta(2.0)
.seed(42)
.optimize_for(OptimizeMetric::SharpeRatio)
.run("AAPL", &candles, &config, |params| {
SmaCrossover::new(
params["fast"].as_int() as usize,
params["slow"].as_int() as usize,
)
})?;
// Convergence curve shows best score at each evaluation
println!("Convergence: {:?}", report.convergence_curve);
ParamRange constructors:
| Constructor | Description |
|---|---|
int_range(s, e, step) |
Integer grid (GridSearch) |
float_range(s, e, step) |
Float grid (GridSearch) |
int_bounds(s, e) |
Integer bounds, step=1 (BayesianSearch) |
float_bounds(s, e) |
Continuous float (BayesianSearch) |
OptimizeMetric variants: TotalReturn, SharpeRatio, SortinoRatio, CalmarRatio, ProfitFactor, WinRate, MinDrawdown
Walk-Forward Validation¶
Validate out-of-sample performance by rolling an in-sample optimization window across the data:
use finance_query::backtesting::{WalkForwardConfig, GridSearch, ParamRange, OptimizeMetric, BacktestConfig};
let grid = GridSearch::new()
.param("fast", ParamRange::int_range(5, 20, 5))
.param("slow", ParamRange::int_range(20, 60, 10))
.optimize_for(OptimizeMetric::SharpeRatio);
let config = BacktestConfig::builder()
.initial_capital(10_000.0)
.commission_pct(0.001)
.build()?;
let report = WalkForwardConfig::new(grid, config)
.in_sample_bars(252) // 1 year in-sample
.out_of_sample_bars(63) // 1 quarter out-of-sample
.run("AAPL", &candles, |params| {
SmaCrossover::new(
params["fast"].as_int() as usize,
params["slow"].as_int() as usize,
)
})?;
println!("OOS Return: {:.2}%", report.aggregate_metrics.total_return_pct);
println!("Consistency: {:.1}%", report.consistency_ratio * 100.0);
println!("Windows tested: {}", report.windows.len());
for w in &report.windows {
println!(
"Window {}: IS {:.1}% → OOS {:.1}%",
w.window,
w.in_sample.metrics.total_return_pct,
w.out_of_sample.metrics.total_return_pct,
);
}
Monte Carlo Simulation¶
Stress-test a backtest result by running thousands of randomised trade-sequence simulations:
use finance_query::backtesting::{MonteCarloConfig, MonteCarloMethod};
let mc = MonteCarloConfig::new()
.seed(42)
.num_simulations(1_000)
.method(MonteCarloMethod::IidShuffle)
.run(&result);
println!("Return p5: {:.2}%", mc.total_return.p5);
println!("Return p50: {:.2}%", mc.total_return.p50);
println!("Return p95: {:.2}%", mc.total_return.p95);
println!("Drawdown p95: {:.2}%", mc.max_drawdown.p95);
println!("Sharpe p50: {:.2}", mc.sharpe_ratio.p50);
MonteCarloMethod variants:
| Method | Description |
|---|---|
IidShuffle (default) |
Randomly shuffle trade returns (i.i.d. assumption) |
BlockBootstrap { block_size } |
Resample contiguous blocks to preserve autocorrelation |
StationaryBootstrap { mean_block_size } |
Random-length blocks (geometric distribution) |
Parametric |
Fit normal distribution to trade returns and sample |
Portfolio Backtesting¶
Run the same strategy across multiple symbols with a shared capital pool:
use finance_query::backtesting::portfolio::{PortfolioConfig, PortfolioEngine, RebalanceMode, SymbolData};
let config = PortfolioConfig::new(BacktestConfig::builder()
.initial_capital(50_000.0)
.commission_pct(0.001)
.build()?
)
.max_total_positions(3)
.rebalance(RebalanceMode::EqualWeight);
let symbol_data = vec![
SymbolData::new("AAPL", aapl_candles),
SymbolData::new("MSFT", msft_candles),
SymbolData::new("GOOGL", googl_candles),
];
let result = PortfolioEngine::new(config)
.run(&symbol_data, |_sym| Box::new(SmaCrossover::new(10, 50)))?;
println!("Portfolio Return: {:.2}%", result.portfolio_metrics.total_return_pct);
println!("Final Equity: ${:.2}", result.final_equity);
for (sym, sym_result) in &result.symbols {
println!("{}: {:.2}%", sym, sym_result.metrics.total_return_pct);
}
RebalanceMode variants:
| Mode | Description |
|---|---|
AvailableCapital (default) |
Each symbol uses position_size_pct of available cash |
EqualWeight |
Split initial capital equally among symbols |
CustomWeights(HashMap<String, f64>) |
Specify weight per symbol (fractions of initial capital) |
Via Tickers::backtest() — fetches charts and dividends automatically, then runs PortfolioEngine:
use finance_query::Tickers;
use finance_query::backtesting::{SmaCrossover, BacktestConfig};
use finance_query::backtesting::portfolio::{PortfolioConfig, RebalanceMode};
let tickers = Tickers::new(vec!["AAPL", "MSFT", "GOOGL"]).await?;
let config = PortfolioConfig::new(BacktestConfig::default())
.max_total_positions(3)
.rebalance(RebalanceMode::EqualWeight);
let result = tickers.backtest(
Interval::OneDay,
TimeRange::OneYear,
Some(config),
|_sym| SmaCrossover::new(10, 50),
).await?;
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)- Value above thresholdbelow(threshold)- Value below thresholdcrosses_above(threshold)- Crosses from below to abovecrosses_below(threshold)- Crosses from above to belowabove_ref(indicator)- Value above another indicatorcrosses_above_ref(indicator)- Crosses above another indicatorbetween(lower, upper)- Value between two thresholdsequals(value)- Value equals threshold
Composites:
and(condition)- Both conditions must be trueor(condition)- Either condition must be truenot()- Negate condition
Position Management:
stop_loss(pct)- Exit on loss percentagetake_profit(pct)- Exit on profit percentagetrailing_stop(pct)- Exit if price retraces by percentagetrailing_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
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 with regime filter and warmup
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))
}
)
.regime_filter(price().above_ref(sma(200)))
.warmup(200)
.build();
let config = BacktestConfig::builder()
.initial_capital(100_000.0)
.commission_pct(0.001)
.slippage_pct(0.0005)
.allow_short(false)
.build()?;
let result = ticker.backtest(
strategy,
Interval::OneDay,
TimeRange::TwoYears,
Some(config),
).await?;
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();
let result = ticker.backtest(strategy, Interval::OneDay, TimeRange::OneYear, Some(config)).await?;
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 40+ available indicators
- Ticker API - Fetch historical data and run single-symbol backtests
- Batch Tickers - Portfolio backtesting across multiple symbols
- Risk Analytics - Standalone VaR, Sharpe, and drawdown metrics
- DataFrame Support - Analyze backtest results in Polars DataFrames