1. Introduction
Backtesting is the key process in evaluating trading strategies, and Python provides a robust environment to implement and test these strategies. In this guide, we will walk you through the steps to manually backtest a simple trading strategy using Python.
We’ll explore:
- What is Manual Backtesting?: The process of applying a trading strategy to historical data.
- Step-by-Step Guide: Implementing a basic backtesting script for a simple strategy.
1.1 What is Manual Backtesting?
Manual backtesting involves coding a trading strategy and testing it against historical data without relying on automated backtesting frameworks. The trader manually defines entry and exit signals, applies them to past data, and calculates the performance.
This process allows you to:
- Understand how a strategy might perform under different market conditions.
- Evaluate the feasibility of your strategy before live trading.
- Gain insights into potential pitfalls or improvements.
1.2 Benefits of Manual Backtesting
Manual backtesting gives you more control over the strategy and can be highly educational. By coding your own backtest, you gain a deeper understanding of the logic and mechanics behind the strategy and the data.
2. The Backtesting Process
We’ll walk through a simple moving average crossover strategy. The idea behind the moving average crossover strategy is:
- Buy Signal: When a short-term moving average (e.g., 50-day) crosses above a long-term moving average (e.g., 200-day).
- Sell Signal: When the short-term moving average crosses below the long-term moving average.
Here’s a step-by-step guide to writing the backtesting script.
2.1 Set Up the Environment
Before starting, ensure that you have the required libraries. The primary ones we will use are:
yfinance
to fetch historical data.pandas
for data manipulation.matplotlib
for visualization.
To install the libraries, run:
pip install yfinance pandas matplotlib
2.2 Fetch Historical Data
We will fetch historical stock data using yfinance
. In this example, we’ll test the strategy on the stock of Apple Inc. (AAPL
).
import yfinance as yf
import pandas as pd
import matplotlib.pyplot as plt
# Fetch historical stock data for AAPL from January 2020 to December 2021
data = yf.download('AAPL', start='2020-01-01', end='2021-12-31')
# Display the first few rows of the data
print(data.head())
2.3 Calculate Moving Averages
Next, we calculate the short-term and long-term moving averages (SMA).
# Calculate 50-day and 200-day simple moving averages (SMA)
data['SMA_50'] = data['Close'].rolling(window=50).mean()
data['SMA_200'] = data['Close'].rolling(window=200).mean()
# Display the moving averages
print(data[['Close', 'SMA_50', 'SMA_200']].tail())
2.4 Define Buy and Sell Signals
Now, we need to define the buy and sell signals based on the crossover of the moving averages:
- Buy Signal: When the 50-day moving average crosses above the 200-day moving average.
- Sell Signal: When the 50-day moving average crosses below the 200-day moving average.
# Define Buy Signal: 50-day SMA crosses above 200-day SMA
data['Buy_Signal'] = (data['SMA_50'] > data['SMA_200']) & (data['SMA_50'].shift(1) <= data['SMA_200'].shift(1))
# Define Sell Signal: 50-day SMA crosses below 200-day SMA
data['Sell_Signal'] = (data['SMA_50'] < data['SMA_200']) & (data['SMA_50'].shift(1) >= data['SMA_200'].shift(1))
# Display the signals
print(data[['Close', 'SMA_50', 'SMA_200', 'Buy_Signal', 'Sell_Signal']].tail())
2.5 Simulate Trades and Calculate Returns
Now, we simulate the trades based on these buy and sell signals. We will assume:
- A starting capital of $10,000.
- Each trade will use the full capital (100% of the capital is invested on each buy signal).
For simplicity, we won’t include transaction costs in this basic example.
# Initialize capital and position
capital = 10000
position = 0
# Create columns to store trading data
data['Position'] = 0
data['Cash'] = capital
data['Portfolio_Value'] = capital
# Simulate the trades
for i in range(1, len(data)):
# If we have a buy signal and no position, buy stock
if data['Buy_Signal'].iloc[i] and position == 0:
position = data['Cash'].iloc[i] / data['Close'].iloc[i]
data['Cash'].iloc[i] = 0 # Use all capital to buy stock
data['Position'].iloc[i] = position
# If we have a sell signal and a position, sell stock
elif data['Sell_Signal'].iloc[i] and position > 0:
data['Cash'].iloc[i] = position * data['Close'].iloc[i] # Sell all stock
position = 0
data['Position'].iloc[i] = position
# Carry forward the cash value if no trades
else:
data['Cash'].iloc[i] = data['Cash'].iloc[i-1]
data['Position'].iloc[i] = position
# Calculate the portfolio value: cash + value of the position
data['Portfolio_Value'].iloc[i] = data['Cash'].iloc[i] + position * data['Close'].iloc[i]
# Display the portfolio value
print(data[['Close', 'Portfolio_Value']].tail())
2.6 Evaluate the Results
We now evaluate the performance of the strategy by calculating key metrics:
- Total Return: The total percentage return from the strategy.
- Max Drawdown: The largest peak-to-trough decline in portfolio value.
- Sharpe Ratio: A risk-adjusted return metric.
# Calculate total return
total_return = (data['Portfolio_Value'].iloc[-1] - capital) / capital * 100
# Calculate maximum drawdown
max_drawdown = (data['Portfolio_Value'] / data['Portfolio_Value'].cummax() - 1).min()
# Calculate Sharpe Ratio
daily_returns = data['Portfolio_Value'].pct_change().dropna()
sharpe_ratio = daily_returns.mean() / daily_returns.std() * (252 ** 0.5)
# Display the results
print(f"Total Return: {total_return:.2f}%")
print(f"Max Drawdown: {max_drawdown:.2f}%")
print(f"Sharpe Ratio: {sharpe_ratio:.2f}")
2.7 Visualizing the Results
Finally, we will visualize the portfolio value over time and the buy/sell signals on the price chart.
# Plot the portfolio value over time
plt.figure(figsize=(10,6))
plt.plot(data['Portfolio_Value'], label='Portfolio Value')
plt.title('Portfolio Value Over Time')
plt.xlabel('Date')
plt.ylabel('Portfolio Value')
plt.legend()
plt.show()
# Plot the stock price along with the buy and sell signals
plt.figure(figsize=(10,6))
plt.plot(data['Close'], label='Stock Price', color='blue')
plt.plot(data['SMA_50'], label='50-Day SMA', color='orange')
plt.plot(data['SMA_200'], label='200-Day SMA', color='red')
plt.scatter(data.index[data['Buy_Signal']], data['Close'][data['Buy_Signal']], marker='^', color='green', label='Buy Signal', alpha=1)
plt.scatter(data.index[data['Sell_Signal']], data['Close'][data['Sell_Signal']], marker='v', color='red', label='Sell Signal', alpha=1)
plt.title('Stock Price and Moving Average Crossover Signals')
plt.xlabel('Date')
plt.ylabel('Price')
plt.legend()
plt.show()
3. Conclusion
In this guide, you learned how to manually backtest a simple moving average crossover strategy using Python. We walked through:
- Fetching historical stock data.
- Calculating moving averages.
- Generating buy and sell signals.
- Simulating trades and evaluating the results using key performance metrics like total return, max drawdown, and Sharpe ratio.
- Visualizing the results to better understand the strategy’s performance.
*Disclaimer: The content in this post is for informational purposes only. The views expressed are those of the author and may not reflect those of any affiliated organizations. No guarantees are made regarding the accuracy or reliability of the information. Use at your own risk.