Building Your First Backtest in Python: A Step-by-Step Tutorial
# Building Your First Backtest in Python: A Step-by-Step Tutorial
Every systematic trader needs to know how to backtest. While GUI-based platforms exist, building backtests in Python gives you unlimited flexibility and teaches you exactly what happens under the hood. This guide walks you through creating a complete backtest from scratch using only pandas and numpy.
Prerequisites
You need basic Python knowledge and familiarity with pandas DataFrames. Install the required libraries: pandas for data manipulation, numpy for numerical operations, yfinance for downloading free historical data, and matplotlib for plotting results. A Jupyter notebook or VS Code with Python extension provides the best interactive development experience for backtesting.
Step 1: Acquiring Data
The first step is getting clean historical price data. Using yfinance, you can download adjusted OHLCV data for any stock or ETF. Always use adjusted close prices that account for dividends and stock splits. Download more data than you think you need because you will want both an in-sample development period and an out-of-sample testing period. We recommend at least 10 years for daily strategies.
Step 2: Calculating Indicators
For our example strategy (50/200 SMA crossover), calculate both moving averages using the pandas rolling function. Store them as new columns in your DataFrame. Always check for NaN values at the beginning of your series where insufficient data exists for the calculation. A 200-day moving average needs 200 days of data before it produces its first value, so your actual trading period begins after this warmup.
Step 3: Generating Signals
Create a signal column that equals 1 when the fast MA is above the slow MA and 0 otherwise. Then create a position column that shifts the signal forward by one day to avoid look-ahead bias. This shift is critical: you calculate the signal using today's close and enter the position at tomorrow's close. Without this shift, your backtest assumes you can trade on information before it is available.
Step 4: Calculating Returns
Multiply your position column by the daily percentage returns of the asset. When your position is 1, you capture the full daily return. When your position is 0, you earn nothing (or optionally a risk-free rate). Cumulate these daily returns to build your equity curve. This vectorized approach calculates the entire backtest in a single line of code, making it extremely fast.
Step 5: Accounting for Transaction Costs
Identify the days where your position changes (entries and exits) and subtract a transaction cost on those days. A reasonable assumption for liquid ETFs is 0.1% per trade (covering the bid-ask spread and any commission). For less liquid stocks, use 0.2-0.5%. This simple approach approximates real-world friction without overcomplicating the backtest. Your returns after costs represent a more realistic expectation.
Step 6: Performance Metrics
Calculate key metrics from your equity curve. The annualized return uses the compound annual growth rate formula. The Sharpe ratio divides annualized excess return by annualized standard deviation. Maximum drawdown finds the largest peak-to-trough decline. Win rate requires identifying individual trades and counting profitable ones. The profit factor divides gross profits by gross losses. These metrics together give a comprehensive picture of strategy quality.
Step 7: Visualization
Plot your equity curve against buy-and-hold for visual comparison. Add a drawdown chart below to show underwater periods. Mark entry and exit points on a price chart to visually verify that the strategy behaves as expected. Visualization often reveals bugs that raw numbers miss, such as the strategy entering during periods you intended to filter out or holding positions longer than expected.
Step 8: Robustness Checks
Before trusting your results, perform basic robustness checks. Vary your parameters slightly (try 45/195 and 55/205 instead of 50/200) and verify performance remains similar. Test on a different time period or market. Check that your sample size (number of trades) is sufficient for statistical significance. A minimum of 30 trades is needed for any confidence in the results.
Common Pitfalls for Beginners
The most dangerous bugs in backtesting are subtle. Look-ahead bias occurs when you use future data in current decisions (even accidentally through pandas operations that do not shift properly). Survivorship bias occurs when you test only on stocks that still exist today. Selection bias occurs when you test many strategies and only report the best one. These biases can make worthless strategies appear profitable.
Next Steps
Once you have built a basic vectorized backtest, consider graduating to event-driven frameworks like Backtrader for more complex strategies that require position sizing based on current equity or multi-asset portfolio logic. But the vectorized approach you built here will handle 80% of strategy ideas and executes orders of magnitude faster. Use it as your rapid prototyping tool and reserve complex frameworks for final validation.
Conclusion
Building a backtest from scratch in Python demystifies the process and ensures you understand every assumption embedded in your results. The vectorized pandas approach handles simple strategies elegantly and executes almost instantly. As you develop more sophisticated strategies, the foundation you built here serves as your starting point. Remember that a backtest is only as good as its assumptions, so always be skeptical of results that seem too good to be true.