Backtest a stock using RSI and SMA indicators with detailed analysis
python
stock
Author
Aakash Basnet
Published
January 26, 2025
Backtesting a Trading Strategy: A Step-by-Step Guide
In this blog post, we will walk through the process of backtesting a simple trading strategy using Python. We’ll use the backtesting library to test a strategy based on Simple Moving Averages (SMA) and discuss how to interpret the results. This guide is suitable for beginners and intermediate users interested in algorithmic trading and quantitative analysis.
What is Backtesting?
Backtesting is the process of testing a trading strategy on historical data to evaluate its effectiveness before applying it to live markets. It helps traders understand the potential risks and rewards of their strategies and make data-driven decisions.
Installation
To get started, you need to install the required libraries. In your terminal, run:
pip install backtesting yfinance pandas
backtesting: For running and visualizing trading strategy backtests.
yfinance: To fetch historical stock data from Yahoo Finance.
pandas: For data manipulation and analysis.
Extracting S&P 500 Companies
To make our strategy more robust, you can extract a list of S&P 500 tickers from Wikipedia or other sources. For this example, we’ll focus on a single stock (Google/GOOGL) for clarity, but you can easily extend this to a basket of stocks.
Importing Libraries and Defining Indicators
This code block imports the necessary libraries for backtesting, data handling, and downloading historical stock data. It also defines helper functions for calculating the Simple Moving Average (SMA) and Relative Strength Index (RSI), which are common technical indicators used in trading strategies. Finally, it defines a custom trading strategy class that uses these indicators to generate buy and sell signals.
import pandas as pdimport yfinance as yffrom backtesting import Strategy, Backtestimport backtestingbacktesting.set_bokeh_output(notebook=True)# Helper functions for indicatorsdef SMA(array, n):"""Simple Moving Average (SMA): Smooths price data by averaging closing prices over a period."""return pd.Series(array).rolling(n).mean()def RSI(array, n):"""Relative Strength Index (RSI): Measures the speed and change of price movements to identify overbought or oversold conditions.""" gain = pd.Series(array).diff() loss = gain.copy() gain[gain <0] =0 loss[loss >0] =0 rs = gain.ewm(n).mean() / loss.abs().ewm(n).mean()return100-100/ (1+ rs)class SimpleMovingAverage(Strategy):""" Trading strategy based on multiple SMAs: - Enters a long position when short-term SMAs are above long-term SMAs and price is above the 10-day SMA. - Sets a fixed stop loss at 8% below entry price. - Exits if price falls 2% below the 10-day SMA. """def init(self):# Compute moving averagesself.ma10 =self.I(SMA, self.data.Close, 10)self.ma20 =self.I(SMA, self.data.Close, 20)self.ma50 =self.I(SMA, self.data.Close, 50)self.ma100 =self.I(SMA, self.data.Close, 100)defnext(self): price =self.data.Close[-1]# Entry condition: bullish trendif (notself.position andself.ma10[-1] >self.ma20[-1] >self.ma50[-1] >self.ma100[-1] and price >self.ma10[-1]):self.buy(sl=.92* price) # 8% stop loss# Exit condition: price drops 2% below 10-day SMAelif price <.98*self.ma10[-1]:self.position.close()# This class defines the core trading logic for our backtest.# These functions will be used as technical indicators in our strategy.
/var/folders/mc/dtq5snf15qx4_l62h8lx76jc0000gn/T/ipykernel_98519/43207852.py:1: DeprecationWarning:
Pyarrow will become a required dependency of pandas in the next major release of pandas (pandas 3.0),
(to allow more performant data types, such as the Arrow string type, and better interoperability with other libraries)
but was not found to be installed on your system.
If this would cause problems for you,
please provide us feedback at https://github.com/pandas-dev/pandas/issues/54466
import pandas as pd
/Users/aaru/miniconda/envs/blog/lib/python3.12/site-packages/backtesting/_plotting.py:55: UserWarning: Jupyter Notebook detected. Setting Bokeh output to notebook. This may not work in Jupyter clients without JavaScript support, such as old IDEs. Reset with `backtesting.set_bokeh_output(notebook=False)`.
warnings.warn('Jupyter Notebook detected. '
Loading BokehJS ...
Downloading Data and Running the Backtest
This code block downloads historical stock data for Google (GOOGL) using the yfinance library, sets up the backtest with the custom trading strategy, and runs the backtest. It then displays summary statistics such as returns, Sharpe ratio, drawdown, win rate, and number of trades. These metrics help you evaluate the performance and risk of the strategy.
# Download historical data for Google (GOOGL)TICKER ='GOOGL'START_DATE ='2025-01-01'END_DATE ='2025-12-01'COMMISSION =0.002# 0.2% commission per tradeINITIAL_CASH =1000# Starting capital for the backtestSPREAD =0.00MARGIN_LEVERAGE =1.0EXCLUSIVE_OREDES =Falsedf = yf.download(TICKER, start=START_DATE, end=END_DATE, multi_level_index=False)# Run the backtestbacktest = Backtest(data=df, strategy=SimpleMovingAverage, commission=COMMISSION, cash=INITIAL_CASH, spread=SPREAD, margin=MARGIN_LEVERAGE, exclusive_orders=EXCLUSIVE_OREDES )results = backtest.run()# Display summary statisticsresults# The results object contains key performance metrics such as:# - Return [%]# - Sharpe Ratio# - Max Drawdown [%]# - Win Rate [%]# - Trades
[*********************100%***********************] 1 of 1 completed
/var/folders/mc/dtq5snf15qx4_l62h8lx76jc0000gn/T/ipykernel_98519/1429128387.py:24: UserWarning: Some trades remain open at the end of backtest. Use `Backtest(..., finalize_trades=True)` to close them and include them in stats.
results = backtest.run()
Start 2025-01-02 00:00:00
End 2025-11-28 00:00:00
Duration 330 days 00:00:00
Exposure Time [%] 34.21053
Equity Final [$] 1640.29852
Equity Peak [$] 1656.58789
Commissions [$] 9.58176
Return [%] 64.02985
Buy & Hold Return [%] 86.15358
Return (Ann.) [%] 72.80104
Volatility (Ann.) [%] 30.9689
CAGR [%] 45.92273
Sharpe Ratio 2.35078
Sortino Ratio 10.73383
Calmar Ratio 11.82038
Alpha [%] 38.301
Beta 0.29864
Max. Drawdown [%] -6.15894
Avg. Drawdown [%] -1.4646
Max. Drawdown Duration 38 days 00:00:00
Avg. Drawdown Duration 7 days 00:00:00
# Trades 2
Win Rate [%] 100.0
Best Trade [%] 37.89776
Worst Trade [%] 11.7606
Avg. Trade [%] 24.14321
Max. Trade Duration 82 days 00:00:00
Avg. Trade Duration 55 days 00:00:00
Profit Factor NaN
Expectancy [%] 24.82918
SQN 2.63973
Kelly Criterion NaN
_strategy SimpleMovingAverage
_equity_curve ...
_trades Size EntryBa...
dtype: object
Visualizing Backtest Results
The plot generated by the backtesting library provides a comprehensive visual summary of your trading strategy’s performance. Here’s what is being plotted and what to look for:
Equity Curve: Shows how your account balance changes over time. A steadily rising curve indicates consistent profits, while sharp drops signal drawdowns.
Trades: Buy and sell signals are marked on the price chart, helping you see where the strategy entered and exited positions.
Drawdowns: Highlights periods where the strategy lost value from its peak, helping you assess risk.
Indicators: Any moving averages or other indicators used in the strategy are overlaid for reference.
What to look for: - Smooth, upward-sloping equity curve with minimal drawdowns. - Logical entry and exit points that match your strategy rules. - Reasonable frequency of trades (not too many or too few). - Periods of high volatility and how the strategy handled them.
This visualization helps you quickly assess the strengths and weaknesses of your strategy before considering live trading.
# Visualize the backtest resultsbacktest.plot()# The plot provides a visual overview of trades, equity curve, drawdowns, and more.
Entry: The strategy enters a long position when the short-term moving averages (10, 20, 50) are all above the 100-day moving average, and the price is above the 10-day SMA. This signals a strong bullish trend.
Exit: The position is closed if the price drops 2% below the 10-day SMA, protecting against downside risk. An 8% stop loss is also set at entry.
Key Metrics to Evaluate
Return [%]: Total percentage return over the backtest period.
Sharpe Ratio: Risk-adjusted return; higher is better.
Max Drawdown [%]: Largest peak-to-trough decline; lower is safer.
Win Rate [%]: Percentage of profitable trades.
Number of Trades: Indicates strategy activity and robustness.
What to Look For
Consistent Returns: A high Sharpe ratio and steady equity curve are positive signs.
Low Drawdowns: Indicates the strategy avoids large losses.
Reasonable Trade Frequency: Too many trades may mean overfitting; too few may mean missed opportunities.
Limitations & Next Steps
This is a simple trend-following strategy. Real-world trading requires more robust risk management, slippage modeling, and out-of-sample testing.
Try experimenting with different stocks, timeframes, or adding more indicators (like RSI) to improve performance.
Backtesting is a powerful tool, but always remember: past performance does not guarantee future results!