Backtesting stocks is the process of applying fixed trading rules to historical data, simulating trades in chronological order, including costs and slippage, and comparing the result with a benchmark.
The example in this guide tests SPY from 2015 through 2025 with 50-day and 200-day moving averages. A backtest tests a strategy, not whether one stock is "good." For example, you might test whether buying when a stock's 50-day moving average rises above its 200-day moving average would have produced acceptable returns and risk.
Backtesting Stocks at a Glance
| Step | What you need to decide | Common mistake |
|---|---|---|
| 1. Trading rules | Entry, exit, position size and holding period | Changing rules after seeing the results |
| 2. Historical data | Prices, dividends, splits and fundamentals | Using incomplete or improperly adjusted data |
| 3. Trade execution | When an order is placed and filled | Buying at today's close using information known only after the close |
| 4. Costs | Commissions, spread, slippage and taxes where applicable | Assuming every trade fills at the quoted price |
| 5. Validation | In-sample, out-of-sample and walk-forward testing | Optimising and evaluating on the same data |
| 6. Evaluation | CAGR, drawdown, volatility and benchmark performance | Looking only at total return |
1. Define the Stock-Trading Strategy Before Testing It
Write the strategy as precise rules that another person could implement without interpretation.
A basic example:
- Universe: One or more liquid US stocks
- Entry: Buy when the 50-day moving average is above the 200-day moving average
- Exit: Sell when the 50-day moving average falls below the 200-day moving average
- Position size: Invest 100% of available cash in one position
- Execution: Enter at the next trading day's open
- Dividends: Reinvest them
- Leverage: None
- Rebalancing: Check the signal daily
You should also specify:
- Whether short selling is allowed
- Whether fractional shares are allowed
- What happens when a stock is suspended or delisted
- Whether signals use closing prices
- Whether positions are rebalanced daily, weekly or monthly
- How much cash remains uninvested
A rule that changes after you see the result is no longer the same test.
2. Use Historical Data Adjusted for Stock Splits and Dividends
For a long-term stock strategy, use a consistent price series that accounts for corporate actions. Stock splits change the quoted share price, while dividends affect an investor's total return.
Data providers and backtesting platforms use different adjustment methods. QuantConnect offers raw, split-adjusted and total-return data modes. Its adjusted US equity data accounts for splits and dividends to create a continuous historical price series.
The open-source yfinance library can automatically adjust historical open, high, low and closing prices using the adjusted closing price.
Adjusted Versus Unadjusted Prices
Use adjusted prices when you are:
- Measuring historical investment returns
- Testing moving averages over long periods
- Comparing a strategy with a buy-and-hold investment
- Reinvesting dividends
Use unadjusted prices plus explicit corporate-action handling when you are:
- Modelling the exact historical execution price
- Testing stop-loss orders around stock splits
- Reconstructing orders as they would have appeared in a brokerage account
Do not mix adjusted closing prices with unadjusted open prices without understanding the consequences. That combination can produce unrealistic returns and incorrect signals.
3. Avoid Look-Ahead Bias
Look-ahead bias occurs when a backtest uses information that was not available when the historical trade would have occurred.
For example, if a moving-average signal is calculated using Monday's closing price, the backtest should not assume that the strategy bought at Monday's close. A more realistic assumption is that the signal became available after the close and the trade executed on Tuesday.
For fundamental strategies, use the date when financial information became publicly available, not simply the quarter-end date. A company may have reported its results weeks after the period ended.
A simple way to prevent same-day look-ahead bias is to shift the trading position forward by one bar:
df["position"] = df["signal"].shift(1).fillna(0)
Survivorship bias is another problem. Testing only companies that still exist today excludes firms that went bankrupt, were delisted or were acquired. That can make historical results look stronger than the strategy could have achieved at the time.
4. Model Commissions, Spreads and Slippage
A backtest should simulate the costs of entering and exiting positions.
Important costs include:
- Broker commissions, if applicable
- Bid-ask spread
- Slippage between the expected and actual fill price
- Market impact from large orders
- Borrow fees for short selling
- Financing costs for leveraged positions
- Fund expense ratios when testing ETFs
Slippage is the difference between the expected execution price and the actual fill price. Volatility, order size, liquidity and execution delays can all affect it. Backtesting engines such as QuantConnect include models for slippage, transaction fees, brokerage rules and leverage.
A simple daily-stock backtest might model costs like this:
cost_per_trade = 0.0005 # 0.05% per one-way trade
turnover = df["position"].diff().abs().fillna(0)
df["strategy_return"] = (
df["position"] * df["asset_return"]
- turnover * cost_per_trade
)
This is only an estimate. A small-cap stock or a large order may have much higher execution costs than a liquid large-cap stock.
5. Test the Strategy in Chronological Order
Do not randomly shuffle stock-market data. Time order matters.
A useful testing structure is:
- Development period: Create and refine the strategy.
- Validation period: Compare reasonable parameter choices.
- Out-of-sample period: Run the final rules on data that was not used for development.
- Paper-trading period: Observe how the strategy behaves with live or delayed market data before risking capital.
A walk-forward test repeatedly calibrates the strategy using a past window, tests it on the following period, then moves the window forward. This more closely reflects how a strategy would have been developed and operated in real time.
For example:
- Use January 2010 through December 2015 to set parameters.
- Test January 2016 through December 2016.
- Move the window forward one year.
- Repeat until the end of the data.
6. Measure More Than Total Return
Total return alone is not enough. A stock strategy that earns 50% may still be poor if it suffers a 70% drawdown or relies on one unusually profitable trade.
Track these metrics:
| Metric | What it measures |
|---|---|
| Total return | Overall gain or loss |
| CAGR | Annualised compound growth rate |
| Maximum drawdown | Largest peak-to-trough decline |
| Annualised volatility | Typical variation in returns |
| Sharpe ratio | Return relative to volatility |
| Sortino ratio | Return relative to downside volatility |
| Win rate | Percentage of profitable trades |
| Profit factor | Gross profit divided by gross loss |
| Number of trades | Whether the result depends on very few trades |
| Exposure | How much time the strategy is invested |
| Benchmark return | Whether the strategy beat a passive alternative |
Compare the strategy with a benchmark over the same dates. For a US large-cap strategy, a broad-market ETF such as SPY may be a reasonable reference point, although the best benchmark depends on the strategy's universe and risk.
A strategy that earns less than the benchmark but has much lower drawdowns may still be useful. A strategy that earns more only because it uses hidden leverage or unrealistic fills is not a reliable result.
7. Python Example: Backtest a Moving-Average Stock Strategy
The following example tests a simple long-only strategy on SPY. It buys when the 50-day moving average is above the 200-day moving average and exits when it is below.
import numpy as np
import pandas as pd
import yfinance as yf
# Download adjusted historical data
df = yf.Ticker("SPY").history(
start="2015-01-01",
end="2025-01-01",
auto_adjust=True,
actions=False
)
df = df[["Close"]].dropna()
# Calculate indicators
df["sma_50"] = df["Close"].rolling(50).mean()
df["sma_200"] = df["Close"].rolling(200).mean()
# Trading signal
df["signal"] = (
(df["sma_50"] > df["sma_200"]) &
df["sma_200"].notna()
).astype(int)
# Trade on the next bar to avoid same-day look-ahead bias
df["position"] = df["signal"].shift(1).fillna(0)
# Daily asset returns
df["asset_return"] = df["Close"].pct_change().fillna(0)
# Approximate transaction costs
cost_per_trade = 0.0005
turnover = df["position"].diff().abs().fillna(0)
# Strategy returns after costs
df["strategy_return"] = (
df["position"] * df["asset_return"]
- turnover * cost_per_trade
)
# Equity curves
df["strategy_equity"] = (1 + df["strategy_return"]).cumprod()
df["buy_hold_equity"] = (1 + df["asset_return"]).cumprod()
# Performance metrics
years = (df.index[-1] - df.index[0]).days / 365.25
cagr = df["strategy_equity"].iloc[-1] ** (1 / years) - 1
annualised_volatility = df["strategy_return"].std() * np.sqrt(252)
sharpe = (
df["strategy_return"].mean()
/ df["strategy_return"].std()
* np.sqrt(252)
)
drawdown = (
df["strategy_equity"]
/ df["strategy_equity"].cummax()
- 1
)
max_drawdown = drawdown.min()
print(f"CAGR: {cagr:.2%}")
print(f"Annualised volatility: {annualised_volatility:.2%}")
print(f"Sharpe ratio: {sharpe:.2f}")
print(f"Maximum drawdown: {max_drawdown:.2%}")
This example is for learning rather than live trading. It does not model bid-ask spreads, partial fills, trading halts, taxes, market impact or order-size constraints.
8. Check Whether the Result Holds Up
A backtest should not depend on one exact setting.
Test whether the results remain reasonable when you:
- Change the moving-average periods slightly
- Increase assumed transaction costs
- Use different start and end dates
- Exclude the best-performing trade
- Test bull, bear and high-volatility periods
- Compare several stocks rather than one winner
- Use a different but related benchmark
- Reduce the position size
- Add realistic execution delays
Do not select the best result from hundreds of parameter combinations and present it as if it were the original idea. Research on trading strategies has found that repeated searches for the most attractive historical result can create a large gap between backtest and out-of-sample performance.
Which Backtesting Method Should You Use?
Python with pandas is a practical starting point for a first project. When you test many stocks, dynamic universes, leverage, order types or detailed transaction costs, a dedicated engine such as QuantConnect LEAN provides more specialised data, portfolio, order and execution components.
| Method | Best for | Limitation |
|---|---|---|
| Spreadsheet | One stock, simple rules and daily data | Easy to make formula and timing errors |
| Python with pandas | Repeatable personal research and custom strategies | Requires programming and data cleaning |
| QuantConnect LEAN | Multiple stocks, brokerage models, universes and execution assumptions | More setup and platform complexity |
| Manual chart review | Understanding individual historical examples | Not a reliable statistical backtest |
The Most Important Backtesting Rules
Before trusting a result, confirm that:
- The strategy rules were written before reviewing the final performance.
- Prices include the correct treatment of splits and dividends.
- The historical universe includes companies that later failed or disappeared.
- Signals use only information available at the time.
- Trades execute on a realistic future price.
- Commissions, spreads and slippage are included.
- The strategy is compared with a suitable benchmark.
- The final evaluation uses out-of-sample data.
- Results survive reasonable changes to parameters and costs.
- The strategy has been paper-tested before real money is committed.
The purpose of backtesting is to discover how a strategy behaved under specified historical conditions, identify its risks and decide whether the idea deserves further testing. Past performance is not a guarantee of future results.