Stock Trading Strategy Backtester
Build a quant trading backtester using Python, inspired by D. E. Shaw.
Introduction
30 Second Summary
Every day, billions of dollars move through stock markets based on algorithms, not gut feelings. Quantitative investment firms like the D. E. Shaw group test trading strategies against years of historical data before risking a single dollar.
In this project, you will build a stock trading strategy backtester using Python in Google Colab. You will fetch real Apple stock data, implement a classic trading strategy, simulate a $10,000 portfolio, and measure how it performs against simply buying and holding the stock.
What You'll Build
Type a stock ticker, run a single notebook cell, and instantly see whether your quantitative trading strategy would have beaten the market over five years of real historical data.
By the end of this project, you'll have:
- A working backtester you can run on any stock ticker to instantly see whether your SMA crossover strategy beats the market, with a chart marking every buy and sell decision on the stock's price history.
- A performance dashboard that compares your strategy's portfolio value against Buy & Hold over time and prints industry-standard metrics: Sharpe ratio, max drawdown, and total return.
- The same Python-based quantitative research workflow used at firms like the D. E. Shaw group, built from scratch in Google Colab.
- Secret Mission: Add a second strategy (RSI mean reversion) and compare both strategies head-to-head on the same stock.
Are there any prerequisites?
Basic programming experience in any language is all you need. Google Colab provides the full Python environment in your browser, so there is nothing to install on your computer.
Before We Start
Before diving into code, let's lock in what you're about to build and why it matters.
Set Up Your Quant Research Environment
Every quantitative research project starts with the right environment. At firms like D. E. Shaw, researchers prototype trading strategies in Python notebook environments before anything touches production systems.
Google Colab gives you a free Python notebook in the browser with pandas, numpy, and matplotlib already installed. You only need to add one library: yfinance, which pulls real stock market data from Yahoo Finance.
In this step, get ready to:
- Open Google Colab and create your backtester notebook.
- Install yfinance and import all required libraries.
- Fetch sample stock data to confirm your data pipeline works.
Open Google Colab and create your notebook
Google Colab is a free online notebook that runs Python in your browser. No installation required.
- Open your browser and navigate to colab.research.google.com.
- Sign in with your Google account if prompted.
- Click File in the top menu bar, then click New notebook.
- Click the notebook name at the top left (it defaults to something like Untitled0.ipynb).
- Type DE Shaw Backtester and press Enter to rename it.
You should see your notebook title update to DE Shaw Backtester at the top of the page, and an empty code cell waiting for your first input.
What is Google Colab?
Google Colab is a free, cloud-hosted Jupyter notebook environment. It comes with Python and popular data science libraries pre-installed, so you can start coding immediately without any local setup.
Your notebooks are saved automatically to Google Drive. This is the same kind of interactive notebook environment that quant researchers use for rapid prototyping.
Install yfinance and import your libraries
Colab already has pandas, numpy, and matplotlib. The only library you need to install is yfinance, which connects to Yahoo Finance and downloads historical stock data.
- Click into the first code cell in your notebook and paste the following code:
!pip install yfinance
import yfinance as yf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
What does this code do?
- !pip install yfinance installs the yfinance library. The ! prefix tells Colab to run this as a terminal command instead of Python code.
- import yfinance as yf loads the Yahoo Finance library so you can fetch stock data with short calls like yf.download().
- import pandas as pd loads pandas, the library you will use to organize and analyze stock data in tables called DataFrames.
- import numpy as np loads numpy for mathematical calculations like the Sharpe ratio.
- import matplotlib.pyplot as plt loads matplotlib for creating charts and visualizations of your strategy's performance.
- Run the cell by clicking the play button (triangle icon) on the left side of the cell, or by pressing Shift+Enter.
You should see pip output scrolling through the cell as yfinance installs. Look for a line near the end that says Successfully installed followed by package names. If no error appears after the imports, all four libraries loaded correctly.
- Confirm the yfinance version by clicking the + Code button in the toolbar to add a new cell, then running:
print(yf.__version__)
You should see a version number printed, such as 0.2.40 or newer. The exact number may differ. If it prints without an error, yfinance is ready to go.
Seeing a ModuleNotFoundError?
- Make sure your first line is !pip install yfinance with the ! at the start. Without the exclamation mark, Colab tries to run it as Python code and fails.
- Check that you ran Cell 1 before the version check cell. The imports must run first.
- If Colab shows a "Runtime disconnected" message, click Reconnect in the top right and re-run the cell.
Still stuck? Help me fix a ModuleNotFoundError when importing yfinance in Google Colab.
✔️ Awesome, I've got everything!
Great. Make sure Cell 1 is saved (Colab auto-saves to Google Drive). You can delete the version check cell now since it was just for confirmation.
ⓧ I'd like to double check the full code
Here is what Cell 1 should look like. Make sure your code matches this exactly:
!pip install yfinance
import yfinance as yf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
Fetch sample stock data to confirm everything works
Libraries are installed, but can they actually pull real market data? A quick test download proves the full pipeline is connected.
Before you run the next cell, what do you think the output will look like? Think about what kind of data a stock market API would return for a company like Apple.
- Add a new code cell by clicking + Code, then fetch 5 days of Apple stock data by running:
yf.download('AAPL', period='5d')
You should see a DataFrame table appear below the cell with columns like Open, High, Low, Close, and Volume, with 5 rows of recent Apple stock prices. Each row represents one trading day.
What do these columns mean?
- Open is the price when the market opened that day.
- High and Low are the highest and lowest prices reached during the day.
- Close is the final price when the market closed. This is the column you will use most for your backtester.
- Volume is how many shares were traded that day.
Not seeing any data?
- Make sure the ticker symbol 'AAPL' is in quotes. Without quotes, Python treats it as a variable name and throws an error.
- If you see an empty DataFrame, it may be a weekend or holiday when markets are closed. Try changing the period to '1mo' to pull a wider range.
- Check that you ran Cell 1 first. The yf.download() function only works after yfinance has been imported.
Still stuck? Help me troubleshoot why yf.download is not returning data in Google Colab.
Your quant research environment is ready. You have a working notebook that can pull live stock data from Yahoo Finance. Next up, you will fetch 5 years of Apple stock history and start exploring the patterns inside it.
Fetch and Explore Historical Market Data
Your quant research environment is set up and your data pipeline is confirmed working. Now you need the raw material for your backtester: real historical stock prices.
A backtester is only as useful as the data behind it. You need years of daily prices to test whether a trading strategy actually works across different market conditions. In this step, you'll download 5 years of Apple stock data, visualize its price history, and calculate daily returns.
In this step, get ready to:
- Download 5 years of Apple (AAPL) historical data and inspect its structure.
- Plot the closing price to visualize trends, drops, and recoveries.
- Calculate daily returns to see the percentage changes your strategy will trade on.
Download 5 years of Apple stock data
In Step 1 you pulled 5 days of data to test the connection. Now you need a much larger dataset to backtest a real trading strategy.
- In your DE Shaw Backtester notebook, click + Code to add a new cell below your imports cell.
- Download 5 years of Apple stock data and inspect it by pasting this code:
# Download 5 years of Apple stock data
data = yf.download('AAPL', period='5y', auto_adjust=True)
# Flatten the multi-level column index
data = data.droplevel('Ticker', axis=1)
# Inspect the data
print(f"Data shape: {data.shape}")
print(f"Date range: {data.index[0].strftime('%Y-%m-%d')} to {data.index[-1].strftime('%Y-%m-%d')}")
data.head()
What does this code do?
- yf.download('AAPL', period='5y', auto_adjust=True) fetches 5 years of daily Apple stock data from Yahoo Finance. The auto_adjust=True parameter adjusts prices for stock splits and dividends so you get clean, comparable numbers.
- droplevel('Ticker', axis=1) flattens the column headers. Recent versions of yfinance return a multi-level index even for a single stock, so this line simplifies column access.
- data.head() prints the first 5 rows so you can see the columns: Open, High, Low, Close, and Volume.
- Run the cell by pressing Shift+Enter.
- You should see the data shape (around 1,258 rows by 5 columns), a date range spanning 5 years, and a table showing the first 5 rows with columns like Open, High, Low, Close, and Volume.
Seeing an error or empty DataFrame?
- If you see a NameError for yf, make sure you ran Cell 1 (the imports cell) first. Colab cells must run in order.
- If the download seems to hang, Yahoo Finance may be temporarily slow. Wait 30 seconds and try running the cell again.
- help me debug an error downloading AAPL data with yf.download in Google Colab
What is OHLCV data?
The five columns you see (Open, High, Low, Close, Volume) are the standard representation of stock price data. Each row is one trading day. Close is the most important column for your backtester because trading signals are calculated from closing prices.
Plot the closing price over time
A table of numbers is hard to interpret. A chart reveals the story behind the data: trends, crashes, recoveries.
- Add a new code cell below and plot Apple's closing price by pasting this code:
# Plot Apple's closing price over 5 years
plt.figure(figsize=(12, 5))
plt.plot(data.index, data['Close'], linewidth=1)
plt.title('AAPL Closing Price - 5 Year History')
plt.xlabel('Date')
plt.ylabel('Price ($)')
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
What's happening here?
- plt.figure(figsize=(12, 5)) creates a wide chart (12 inches by 5 inches) so you can see detail across 5 years of data.
- plt.plot(data.index, data['Close']) draws the closing price on each date. matplotlib automatically formats the x-axis as dates.
- plt.grid(True, alpha=0.3) adds a subtle grid so you can estimate price levels visually.
- Run the cell with Shift+Enter.
- You should see a line chart of Apple's closing price over 5 years displayed inline. Look for real price movements: rises, crashes, and recoveries across the time period.
Chart not showing up?
- Make sure you ran Cell 2 first. This cell depends on the data variable created there.
- If you see a KeyError: 'Close', double check that Cell 2 includes the droplevel line. Without it, column access fails.
- help me fix my matplotlib chart not displaying in Google Colab
Calculate daily returns
Stock prices alone do not tell you how volatile a stock is. Daily returns show the percentage change between each day's closing price. This is the raw material that every trading strategy and performance metric is built on.
- Add a new code cell below and calculate daily returns by pasting this code:
# Calculate the percentage change between each day's close
data['Daily_Return'] = data['Close'].pct_change()
# Print summary statistics
print(f"Average daily return: {data['Daily_Return'].mean():.4%}")
print(f"Daily return std dev: {data['Daily_Return'].std():.4%}")
data[['Close', 'Daily_Return']].tail(10)
What does this code do?
- pct_change() calculates the percentage difference between each row and the one before it. If the stock closed at $100 yesterday and $102 today, the daily return is 0.02 (2%).
- The average daily return tells you the typical daily gain or loss. The standard deviation tells you how much returns vary from that average. Higher standard deviation means more volatile (riskier) stock.
- tail(10) shows the last 10 rows so you can see the daily returns alongside closing prices.
Before you run this cell, what do you think the average daily return will be? A large positive number, a small positive number, or close to zero?
- Run the cell with Shift+Enter.
- You should see the average daily return (a very small percentage, close to zero) and the standard deviation. The table below shows the last 10 days of closing prices alongside their daily return values.
Why is the average daily return so small?
Stock prices change by tiny percentages each day. A 0.05% daily return might look insignificant, but compounded over 252 trading days in a year, it adds up. This is why quant researchers measure performance over years, not days.
Seeing NaN in the first row?
- The very first row of Daily_Return is always NaN (Not a Number) because there is no previous day to compare against. This is normal and expected.
- If you see all NaN values in the Daily_Return column, check that Cell 2 ran successfully and the Close column contains numeric price data.
- help me debug NaN values in my daily return calculation
✔️ Awesome, I've got everything!
Great. Double check that you've saved your notebook. Your notebook should now have 4 cells total.
ⓧ I'd like to double check the full code
Scroll through your notebook and confirm you have these 4 cells in order. Each cell's code should match what you entered above.
Cell 1: Setup and Imports
!pip install yfinance
import yfinance as yf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
Cell 2: Fetch Historical Data
data = yf.download('AAPL', period='5y', auto_adjust=True)
data = data.droplevel('Ticker', axis=1)
print(f"Data shape: {data.shape}")
print(f"Date range: {data.index[0].strftime('%Y-%m-%d')} to {data.index[-1].strftime('%Y-%m-%d')}")
data.head()
Cell 3: Plot Price History
plt.figure(figsize=(12, 5))
plt.plot(data.index, data['Close'], linewidth=1)
plt.title('AAPL Closing Price - 5 Year History')
plt.xlabel('Date')
plt.ylabel('Price ($)')
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Cell 4: Calculate Daily Returns
data['Daily_Return'] = data['Close'].pct_change()
print(f"Average daily return: {data['Daily_Return'].mean():.4%}")
print(f"Daily return std dev: {data['Daily_Return'].std():.4%}")
data[['Close', 'Daily_Return']].tail(10)
You now have 5 years of real Apple stock data loaded, visualized, and analyzed. Next up, you'll use this data to build your first quantitative trading strategy: the SMA crossover.
Implement the SMA Crossover Strategy
In the last step, you loaded five years of Apple stock data and explored its price history and daily returns. You have the raw material, but no trading logic.
A Simple Moving Average (SMA) crossover is one of the most classic quantitative trading strategies. When a short-term moving average crosses above a long-term one, the trend is shifting upward, and that crossing is your buy signal. When it crosses back below, you sell. This is exactly the kind of hypothesis quant researchers test at firms like D. E. Shaw.
In this step, get ready to:
- Calculate 20-day and 50-day simple moving averages and plot them on the price chart.
- Generate buy and sell signals from the SMA crossover points.
- Visualize the signals on a chart with green buy arrows and red sell arrows.
Calculate the moving averages
A moving average smooths out noisy daily price data into a trend line. A 20-day SMA averages the last 20 closing prices, reacting quickly to recent changes. A 50-day SMA averages 50 days, producing a slower, steadier line.
When the fast line (20-day) crosses above the slow line (50-day), the stock's recent momentum is outpacing its longer-term trend. That crossing is the core signal your strategy will trade on.
- Create a new cell below your daily returns cell and calculate both SMAs by pasting this code:
# Calculate 20-day (fast) and 50-day (slow) Simple Moving Averages
data['SMA_20'] = data['Close'].rolling(window=20).mean()
data['SMA_50'] = data['Close'].rolling(window=50).mean()
# Plot price with both SMAs overlaid
plt.figure(figsize=(12, 5))
plt.plot(data.index, data['Close'], label='Close Price', alpha=0.7)
plt.plot(data.index, data['SMA_20'], label='20-day SMA', linewidth=1.5)
plt.plot(data.index, data['SMA_50'], label='50-day SMA', linewidth=1.5)
plt.title('AAPL Price with 20-day and 50-day SMAs')
plt.xlabel('Date')
plt.ylabel('Price ($)')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
What does this code do?
- rolling(window=20).mean() slides a 20-day window across the closing prices and calculates the average at each point. This creates a smoothed trend line that reacts quickly to price changes.
- rolling(window=50).mean() does the same with a 50-day window. The longer window produces a smoother, slower-moving line that captures the broader trend.
- The first 19 values of SMA_20 and the first 49 values of SMA_50 will be NaN because there aren't enough prior days to calculate the average yet.
- Run the cell by pressing Shift+Enter.
You should see Apple's closing price in light blue with two smoother lines overlaid. The 20-day SMA hugs the price closely, while the 50-day SMA traces a gentler arc through the data.
Not seeing two SMA lines?
- Make sure your Cell 2 (which downloads the data) ran successfully before this cell. The data DataFrame must exist with a Close column.
- If the SMA lines look identical to the price, double-check that the window values are 20 and 50, not 1 or 2.
- Still stuck? Help me debug my SMA chart in Google Colab
Generate buy and sell signals
Now you need to turn those two SMA lines into concrete trading decisions. The logic is straightforward: whenever the fast SMA crosses above the slow SMA, buy. Whenever it crosses below, sell.
- Create a new cell and generate the trading signals by pasting this code:
# Set Signal to 1 when the fast SMA is above the slow SMA
data['Signal'] = 0.0
data.loc[data['SMA_20'] > data['SMA_50'], 'Signal'] = 1.0
# Detect crossover points: +1 = buy, -1 = sell
data['Position'] = data['Signal'].diff()
# Filter for buy and sell crossover events
buy_signals = data[data['Position'] == 1]
sell_signals = data[data['Position'] == -1]
What does this code do?
- Signal is set to 1.0 on every day where the 20-day SMA is above the 50-day SMA (bullish trend), and 0.0 otherwise.
- .diff() subtracts each day's signal from the previous day's. A result of +1 means the signal just flipped from 0 to 1 (a buy crossover). A result of -1 means it flipped from 1 to 0 (a sell crossover).
- buy_signals and sell_signals filter the DataFrame down to just the rows where those crossover events happen. These are the exact days your strategy would execute a trade.
Plot the buy and sell signals
The signals are computed, but you can't see them yet. A good chart makes the difference between a pile of numbers and a strategy you can actually evaluate at a glance.
- In the same cell (Cell 6), add the following code below your signal generation code:
# Plot price with SMAs and buy/sell markers
plt.figure(figsize=(12, 6))
plt.plot(data.index, data['Close'], label='Close Price', alpha=0.7)
plt.plot(data.index, data['SMA_20'], label='20-day SMA', alpha=0.8)
plt.plot(data.index, data['SMA_50'], label='50-day SMA', alpha=0.8)
plt.scatter(buy_signals.index, buy_signals['Close'], marker='^', color='green', s=100, label='Buy Signal', zorder=5)
plt.scatter(sell_signals.index, sell_signals['Close'], marker='v', color='red', s=100, label='Sell Signal', zorder=5)
plt.title('SMA Crossover Strategy - Buy/Sell Signals')
plt.xlabel('Date')
plt.ylabel('Price ($)')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# Print signal counts
print(f"Total buy signals: {len(buy_signals)}")
print(f"Total sell signals: {len(sell_signals)}")
What does this code do?
- plt.scatter() places markers at specific data points instead of drawing a continuous line. Green up arrows (marker='^') mark buy signals. Red down arrows (marker='v') mark sell signals.
- zorder=5 ensures the signal markers render on top of the price and SMA lines so they are always visible.
- The final print statements show how many buy and sell events the strategy detected across the entire five-year period.
Before you run this cell, look at your SMA chart from the previous cell. How many times do you think the fast SMA crossed the slow SMA over five years?
- Run Cell 6 by pressing Shift+Enter.
You should see a chart with Apple's closing price, both SMA lines, and green up-arrows at buy crossovers and red down-arrows at sell crossovers scattered across the timeline. Below the chart, two lines print the total number of buy and sell signals.
Not seeing buy/sell arrows on the chart?
- Make sure the signal generation code and the plotting code are in the same cell. If they are in separate cells, buy_signals and sell_signals might not be defined when the plotting code runs.
- If the total buy/sell signal counts print as 0, check that Cell 5 ran before Cell 6. The SMA_20 and SMA_50 columns must exist before the signal logic can compare them.
- Still stuck? Help me debug my SMA crossover buy/sell signal chart
✔️ Awesome, I've got everything!
Great. Double check you have both Cell 5 and Cell 6 looking correct before moving on.
ⓧ I'd like to double check the full code
Here is the complete code for Cell 5 (Calculate Moving Averages):
data['SMA_20'] = data['Close'].rolling(window=20).mean()
data['SMA_50'] = data['Close'].rolling(window=50).mean()
plt.figure(figsize=(12, 5))
plt.plot(data.index, data['Close'], label='Close Price', alpha=0.7)
plt.plot(data.index, data['SMA_20'], label='20-day SMA', linewidth=1.5)
plt.plot(data.index, data['SMA_50'], label='50-day SMA', linewidth=1.5)
plt.title('AAPL Price with 20-day and 50-day SMAs')
plt.xlabel('Date')
plt.ylabel('Price ($)')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Here is the complete code for Cell 6 (Generate Buy/Sell Signals):
data['Signal'] = 0.0
data.loc[data['SMA_20'] > data['SMA_50'], 'Signal'] = 1.0
data['Position'] = data['Signal'].diff()
buy_signals = data[data['Position'] == 1]
sell_signals = data[data['Position'] == -1]
plt.figure(figsize=(12, 6))
plt.plot(data.index, data['Close'], label='Close Price', alpha=0.7)
plt.plot(data.index, data['SMA_20'], label='20-day SMA', alpha=0.8)
plt.plot(data.index, data['SMA_50'], label='50-day SMA', alpha=0.8)
plt.scatter(buy_signals.index, buy_signals['Close'], marker='^', color='green', s=100, label='Buy Signal', zorder=5)
plt.scatter(sell_signals.index, sell_signals['Close'], marker='v', color='red', s=100, label='Sell Signal', zorder=5)
plt.title('SMA Crossover Strategy - Buy/Sell Signals')
plt.xlabel('Date')
plt.ylabel('Price ($)')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
print(f"Total buy signals: {len(buy_signals)}")
print(f"Total sell signals: {len(sell_signals)}")
Your strategy is generating buy and sell signals across five years of Apple stock data. The arrows on your chart show exactly when the strategy would have entered and exited positions. But here is the big question: would following those signals have actually made you money? In the next step, you will build the backtesting engine that simulates a $10,000 portfolio trading on these signals and find out.
Build the Backtesting Engine
You have signals telling you when to buy and sell. But signals alone don't tell you whether following them actually makes money.
This is where theory meets reality. You'll build an engine that simulates a $10,000 portfolio trading based on your SMA signals and tracks its value day by day. You'll also create a Buy & Hold benchmark to see if all that trading actually beats doing nothing.
In this step, get ready to:
- Build a portfolio simulation that trades $10,000 based on your SMA signals.
- Calculate a Buy & Hold benchmark to compare your strategy against.
- Plot both equity curves side by side to visually compare performance.
Build the portfolio simulation
At firms like D. E. Shaw, a backtester replays history to answer one question: if you had followed this strategy, would you have made money? Your engine starts with $10,000 in cash, buys shares when the signal says buy, sells when it says sell, and records the total portfolio value at the end of each trading day.
- Create a new code cell below Cell 6 and paste the simulation loop:
# Starting portfolio
initial_capital = 10000.0
shares = 0
cash = initial_capital
portfolio_values = []
# Simulate trading day by day
for i in range(len(data)):
price = data['Close'].iloc[i]
# Skip days with missing prices
if pd.isna(price):
portfolio_values.append(cash)
continue
# Buy signal: spend all cash on shares
if data['Position'].iloc[i] == 1 and cash > 0:
shares = int(cash // price)
cash = cash - shares * price
# Sell signal: sell all shares for cash
elif data['Position'].iloc[i] == -1 and shares > 0:
cash = cash + shares * price
shares = 0
# Record today's total portfolio value
portfolio_values.append(cash + shares * price)
# Store results in the DataFrame
data['Strategy_Value'] = portfolio_values
What does this code do?
The loop walks through every trading day in the dataset, one at a time.
- When Position is 1 (buy signal), the engine spends all available cash to buy as many whole shares as it can afford.
- When Position is -1 (sell signal), it sells every share and converts back to cash.
- Each day, the total portfolio value (cash + share value) is appended to portfolio_values, which becomes the Strategy_Value column.
Calculate the Buy & Hold benchmark
A strategy is only useful if it beats the simplest alternative. Buy & Hold means buying the stock on day one and never selling. If your SMA strategy can't beat that, all that trading was wasted effort.
- Add this code directly below the simulation loop in the same cell:
# Calculate Buy & Hold: buy on day one, hold forever
buy_hold_shares = int(initial_capital // data['Close'].dropna().iloc[0])
buy_hold_cash = initial_capital - buy_hold_shares * data['Close'].dropna().iloc[0]
data['BuyHold_Value'] = buy_hold_cash + buy_hold_shares * data['Close']
# Compare final values
print(f"Strategy final value: ${data['Strategy_Value'].iloc[-1]:,.2f}")
print(f"Buy & Hold final value: ${data['BuyHold_Value'].iloc[-1]:,.2f}")
What does this code do?
- buy_hold_shares calculates how many whole shares $10,000 could buy on the very first trading day.
- buy_hold_cash is the leftover cash that wasn't enough to buy another full share.
- BuyHold_Value tracks the portfolio value every day by multiplying the share count by the daily closing price, plus leftover cash.
Before you run this cell, think about it: do you expect the SMA strategy to have made more or less money than simply buying and holding AAPL for five years?
- Run the cell by pressing Shift+Enter.
You should see two printed lines showing dollar amounts for both the strategy's final portfolio value and the Buy & Hold final portfolio value. The two numbers are different, proving the simulation is working and producing distinct results from simply holding the stock.
Seeing a KeyError or empty output?
- Make sure you ran all previous cells first (Cells 1 through 6). The simulation depends on the Position column created in Cell 6.
- Check that the simulation loop and the Buy & Hold code are both in the same cell, with no blank cells between them.
- If you see a NameError for data, go to the top of your notebook and rerun Cells 1 through 6 in order.
Why is my backtester printing unexpected values or throwing a KeyError?
Plot both equity curves
Numbers alone don't tell the full story. An equity curve shows how each approach performed over time. Plotting them together reveals when the strategy outperformed Buy & Hold and when it fell behind.
- Create a new code cell below Cell 7 and paste the equity curve chart:
plt.figure(figsize=(12, 5))
plt.plot(data.index, data['Strategy_Value'], label='SMA Strategy', linewidth=1.5)
plt.plot(data.index, data['BuyHold_Value'], label='Buy & Hold', linewidth=1.5)
# Horizontal line at starting capital for reference
plt.axhline(y=initial_capital, color='gray', linestyle='--', alpha=0.5, label='Initial Capital')
plt.title('Strategy vs Buy & Hold - Equity Curves')
plt.xlabel('Date')
plt.ylabel('Portfolio Value ($)')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
What does this chart show?
- The blue line tracks your SMA strategy's portfolio value over time as it buys and sells.
- The orange line tracks the Buy & Hold approach, which simply rides the stock price up and down.
- The dashed gray line marks your $10,000 starting capital. Any time a line dips below it, that approach is losing money.
- Run the cell by pressing Shift+Enter.
You should see a line chart with two curves diverging from the $10,000 starting line. The two lines separate and reconverge at different points, proving that the SMA strategy takes a different path than simply holding the stock.
Chart is empty or only shows one line?
- Make sure Cell 7 ran successfully first. The chart depends on both Strategy_Value and BuyHold_Value columns existing in your DataFrame.
- If both lines appear but overlap perfectly, double check that the simulation loop in Cell 7 uses data['Position'] to trigger trades, not data['Signal'].
Why is my equity curve chart not displaying both strategy lines?
✔️ Awesome, I've got everything!
Great. Make sure both Cell 7 and Cell 8 are saved in your notebook.
ⓧ I'd like to double check the full code
Here is the full code for Cell 7 (Backtest the Strategy):
initial_capital = 10000.0
shares = 0
cash = initial_capital
portfolio_values = []
for i in range(len(data)):
price = data['Close'].iloc[i]
if pd.isna(price):
portfolio_values.append(cash)
continue
if data['Position'].iloc[i] == 1 and cash > 0:
shares = int(cash // price)
cash = cash - shares * price
elif data['Position'].iloc[i] == -1 and shares > 0:
cash = cash + shares * price
shares = 0
portfolio_values.append(cash + shares * price)
data['Strategy_Value'] = portfolio_values
buy_hold_shares = int(initial_capital // data['Close'].dropna().iloc[0])
buy_hold_cash = initial_capital - buy_hold_shares * data['Close'].dropna().iloc[0]
data['BuyHold_Value'] = buy_hold_cash + buy_hold_shares * data['Close']
print(f"Strategy final value: ${data['Strategy_Value'].iloc[-1]:,.2f}")
print(f"Buy & Hold final value: ${data['BuyHold_Value'].iloc[-1]:,.2f}")
And here is the full code for Cell 8 (Plot Equity Curves):
plt.figure(figsize=(12, 5))
plt.plot(data.index, data['Strategy_Value'], label='SMA Strategy', linewidth=1.5)
plt.plot(data.index, data['BuyHold_Value'], label='Buy & Hold', linewidth=1.5)
plt.axhline(y=initial_capital, color='gray', linestyle='--', alpha=0.5, label='Initial Capital')
plt.title('Strategy vs Buy & Hold - Equity Curves')
plt.xlabel('Date')
plt.ylabel('Portfolio Value ($)')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Your backtesting engine is working. You can see exactly how a $10,000 portfolio would have performed using your SMA crossover strategy versus simply holding the stock. Next up, you'll calculate the precise performance metrics that quant funds use to decide whether a strategy deserves real capital.
Calculate Performance Metrics and Final Dashboard
Your backtester now simulates a $10,000 portfolio and plots the equity curves. You can visually compare how the SMA strategy performed against Buy & Hold over five years.
A chart alone does not tell you whether a strategy is actually good. Quant funds like the D. E. Shaw group evaluate strategies using precise numerical metrics before deploying them with real capital. In this step, you'll calculate the same metrics they use and build a professional dashboard.
In this step, get ready to:
- Calculate the Sharpe ratio and max drawdown for both the SMA strategy and Buy & Hold.
- Print a formatted performance comparison table.
- Build a final two-panel dashboard chart.
Calculate the Sharpe ratio and max drawdown
Two metrics determine whether a strategy gets deployed with real capital. The Sharpe ratio measures risk-adjusted return: how much return you earn for each unit of risk you take. The max drawdown measures the worst peak-to-trough loss your portfolio suffered.
- Create a new code cell in your notebook and paste this function that calculates performance metrics for any portfolio series:
def calculate_metrics(portfolio_series, name):
daily_returns = portfolio_series.pct_change().dropna()
total_return = (portfolio_series.iloc[-1] / portfolio_series.iloc[0]) - 1
sharpe_ratio = (daily_returns.mean() / daily_returns.std()) * np.sqrt(252)
rolling_max = portfolio_series.cummax()
drawdown = (portfolio_series - rolling_max) / rolling_max
max_drawdown = drawdown.min()
return {
'Name': name,
'Total Return': f"{total_return:.2%}",
'Sharpe Ratio': f"{sharpe_ratio:.3f}",
'Max Drawdown': f"{max_drawdown:.2%}",
'Final Value': f"${portfolio_series.iloc[-1]:,.2f}"
}
What does this function do?
- pct_change().dropna() converts the portfolio's daily values into percentage returns. These daily returns are the raw material for both metrics.
- The Sharpe ratio formula is (mean daily return / std of daily returns) * sqrt(252). Multiplying by the square root of 252 annualizes it, since there are 252 trading days per year.
- cummax() tracks the highest value the portfolio has ever reached at each point in time. The drawdown at each day is how far below that peak the portfolio has fallen.
- max_drawdown captures the single worst drop. A max drawdown of -20% means the portfolio once lost 20% from its peak before recovering.
Don't run this cell yet. The next chunk adds the code that calls this function and displays the results.
Print the performance comparison
- Below the function you just pasted (in the same cell), add this code to run the metrics on both the SMA strategy and Buy & Hold:
strategy_metrics = calculate_metrics(data['Strategy_Value'], 'SMA Strategy')
buyhold_metrics = calculate_metrics(data['BuyHold_Value'].dropna(), 'Buy & Hold')
metrics_df = pd.DataFrame([strategy_metrics, buyhold_metrics])
metrics_df = metrics_df.set_index('Name')
print("\n=== PERFORMANCE COMPARISON ===\n")
print(metrics_df.to_string())
print(f"\nTotal trades: {len(buy_signals) + len(sell_signals)}")
What does this code do?
- calculate_metrics() is called once for the SMA strategy and once for Buy & Hold. Notice .dropna() on the Buy & Hold series. This removes any NaN values from the early rows before the first valid price.
- The results are combined into a pandas DataFrame and printed as a formatted table so you can compare both approaches side by side.
- The total trade count adds buy signals and sell signals together. More trades mean more transaction costs in real life.
- Run the cell.
- You should see a printed table titled === PERFORMANCE COMPARISON === with rows for SMA Strategy and Buy & Hold, showing Total Return, Sharpe Ratio, Max Drawdown, and Final Value for each.
Seeing a NameError?
- If you see NameError: name 'calculate_metrics' is not defined, make sure both the function definition and the calling code are in the same cell. The function must appear above the lines that call it.
- If you see NameError: name 'buy_signals' is not defined, go back and re-run Cell 6 (Generate Buy/Sell Signals) first. Colab cells need to be run in order.
Why is my performance comparison table not printing correctly?
What is a good Sharpe ratio?
A Sharpe ratio above 1.0 is considered acceptable. Above 2.0 is very good. Above 3.0 is excellent. Most passive index funds sit around 0.5 to 1.0.
Compare your strategy's Sharpe ratio to Buy & Hold's. If your strategy has a lower Sharpe ratio, it means you took on more risk per unit of return than simply holding the stock.
Create the final dashboard
Your metrics table gives the numbers. Now you'll bring everything together into a single two-panel dashboard. The top panel shows the stock price with SMAs and buy/sell signals. The bottom panel shows the equity curves.
- Create a new code cell and paste this dashboard code:
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(14, 10), gridspec_kw={'height_ratios': [3, 2]})
ax1.plot(data.index, data['Close'], label='Close Price', alpha=0.7)
ax1.plot(data.index, data['SMA_20'], label='20-day SMA', alpha=0.8)
ax1.plot(data.index, data['SMA_50'], label='50-day SMA', alpha=0.8)
ax1.scatter(buy_signals.index, buy_signals['Close'], marker='^', color='green', s=80, label='Buy', zorder=5)
ax1.scatter(sell_signals.index, sell_signals['Close'], marker='v', color='red', s=80, label='Sell', zorder=5)
ax1.set_title('AAPL - SMA Crossover Strategy')
ax1.set_ylabel('Price ($)')
ax1.legend(loc='upper left')
ax1.grid(True, alpha=0.3)
What does the top panel show?
- plt.subplots(2, 1) creates two charts stacked vertically. The height_ratios: [3, 2] makes the top panel taller since it has more detail.
- ax1 plots the closing price, both SMAs, and the buy/sell signal markers all in one view. This is the strategy visualization panel.
- Below the code you just pasted (in the same cell), add the bottom panel and display commands:
ax2.plot(data.index, data['Strategy_Value'], label='SMA Strategy', linewidth=1.5)
ax2.plot(data.index, data['BuyHold_Value'], label='Buy & Hold', linewidth=1.5)
ax2.axhline(y=initial_capital, color='gray', linestyle='--', alpha=0.5)
ax2.set_title('Portfolio Value Over Time')
ax2.set_xlabel('Date')
ax2.set_ylabel('Portfolio Value ($)')
ax2.legend(loc='upper left')
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
What does the bottom panel show?
- ax2 plots both equity curves on the same axes so you can see exactly when the strategy pulls ahead or falls behind Buy & Hold.
- The dashed gray line at initial_capital marks your starting $10,000. Any time a line dips below it, that approach has lost money from the start.
Before you run this cell, think about what you expect to see. Will your SMA strategy's equity curve track closely with Buy & Hold, or diverge significantly?
- Run the cell.
- You should see a large two-panel figure. The top panel shows AAPL's price with the 20-day and 50-day SMAs overlaid, plus green up-arrows for buys and red down-arrows for sells. The bottom panel shows both equity curves diverging from the $10,000 starting line.
Only seeing one panel or a blank chart?
- Make sure both the ax1 block and the ax2 block are in the same cell. If they're split across two cells, only the second panel renders.
- Check that plt.show() appears at the very end of the cell. Without it, Colab may not display the figure.
Why is my two-panel dashboard not rendering correctly in Colab?
✔️ Awesome, I've got everything!
Great. Double check you've saved your notebook.
ⓧ I'd like to double check the full code
Here is the full code for Cell 9 (Calculate Performance Metrics):
def calculate_metrics(portfolio_series, name):
daily_returns = portfolio_series.pct_change().dropna()
total_return = (portfolio_series.iloc[-1] / portfolio_series.iloc[0]) - 1
sharpe_ratio = (daily_returns.mean() / daily_returns.std()) * np.sqrt(252)
rolling_max = portfolio_series.cummax()
drawdown = (portfolio_series - rolling_max) / rolling_max
max_drawdown = drawdown.min()
return {
'Name': name,
'Total Return': f"{total_return:.2%}",
'Sharpe Ratio': f"{sharpe_ratio:.3f}",
'Max Drawdown': f"{max_drawdown:.2%}",
'Final Value': f"${portfolio_series.iloc[-1]:,.2f}"
}
strategy_metrics = calculate_metrics(data['Strategy_Value'], 'SMA Strategy')
buyhold_metrics = calculate_metrics(data['BuyHold_Value'].dropna(), 'Buy & Hold')
metrics_df = pd.DataFrame([strategy_metrics, buyhold_metrics])
metrics_df = metrics_df.set_index('Name')
print("\n=== PERFORMANCE COMPARISON ===\n")
print(metrics_df.to_string())
print(f"\nTotal trades: {len(buy_signals) + len(sell_signals)}")
And here is the full code for Cell 10 (Final Dashboard):
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(14, 10), gridspec_kw={'height_ratios': [3, 2]})
ax1.plot(data.index, data['Close'], label='Close Price', alpha=0.7)
ax1.plot(data.index, data['SMA_20'], label='20-day SMA', alpha=0.8)
ax1.plot(data.index, data['SMA_50'], label='50-day SMA', alpha=0.8)
ax1.scatter(buy_signals.index, buy_signals['Close'], marker='^', color='green', s=80, label='Buy', zorder=5)
ax1.scatter(sell_signals.index, sell_signals['Close'], marker='v', color='red', s=80, label='Sell', zorder=5)
ax1.set_title('AAPL - SMA Crossover Strategy')
ax1.set_ylabel('Price ($)')
ax1.legend(loc='upper left')
ax1.grid(True, alpha=0.3)
ax2.plot(data.index, data['Strategy_Value'], label='SMA Strategy', linewidth=1.5)
ax2.plot(data.index, data['BuyHold_Value'], label='Buy & Hold', linewidth=1.5)
ax2.axhline(y=initial_capital, color='gray', linestyle='--', alpha=0.5)
ax2.set_title('Portfolio Value Over Time')
ax2.set_xlabel('Date')
ax2.set_ylabel('Portfolio Value ($)')
ax2.legend(loc='upper left')
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Secret mission
Add an RSI Strategy and Compare Head-to-Head
Your SMA crossover strategy follows momentum, buying when the trend shifts upward. But what about the opposite approach? Implement an RSI mean-reversion strategy that buys when a stock is oversold and sells when it is overbought. Then compare both strategies head-to-head against Buy & Hold to see which philosophy wins.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely in Google Colab, so there are no ongoing costs or cloud resources to worry about.
Resources you used:
- Google Colab notebook named "DE Shaw Backtester" (saved to your Google Drive).
- Colab runtime with yfinance installed (ephemeral, resets automatically when the runtime disconnects).
Keep everything running
No action needed. Choose this if you want to keep experimenting with different stock tickers or strategy parameters.
- Your notebook stays saved in your Google Drive under Colab Notebooks.
- The Colab runtime will disconnect automatically after a period of inactivity. When you reopen the notebook, you will need to re-run all cells from the top (including the !pip install yfinance cell).
Pause - I'll come back to this later
Disconnect the Colab runtime to free up the session, but keep the notebook saved in your Google Drive for later.
- In your Colab notebook, click Runtime in the top menu bar.
- Click Disconnect and delete runtime.
- Your code and outputs remain saved in the notebook. When you return, re-run all cells from the top to restore your data and variables.
Delete - I don't want to use this again
Remove the notebook from your Google Drive entirely.
- In your Colab notebook, click File in the top menu bar.
- Click Move to trash.
- To permanently delete it, open Google Drive, click Trash in the left sidebar, find the notebook, right-click it, and select Delete forever.
- The Colab runtime and any installed packages are automatically cleaned up when the session ends. No further action is needed.
Nice Work!
Nice Work!
Nice work! You just built a quantitative trading strategy backtester from scratch using Python, the same language D. E. Shaw uses for their quantitative research.
You've learned how to:
- Fetched real historical stock data with yfinance, implemented an SMA crossover trading strategy, and generated buy/sell signals from moving average crossovers.
- Built a portfolio simulation engine that tracks positions, cash, and equity over time, starting from a $10,000 portfolio and comparing it against a Buy & Hold benchmark.
- Calculated industry-standard performance metrics including Sharpe ratio, max drawdown, and total return, then visualized everything in a final dashboard with equity curves and signal charts.
- Secret Mission: Implemented a second strategy (RSI mean reversion) and compared both approaches head-to-head against Buy & Hold.
Ready to quiz yourself?