Trading Strategies, Backtesting, and Risk Analysis¶
This lab builds on Part 1. We will use the moving averages and Bollinger Bands from Part 1 to:
Implement a simple trading strategy based on moving average crossovers.
Backtest the strategy against historical data to see if it would have made money.
Measure risk using volatility, Sharpe Ratio, and maximum drawdown.
Build interactive visualizations for exploring different stocks.
Analyze a stock of your own choice.
Important Disclaimer: This lab is for educational purposes only. Stock trading involves significant risk, and past performance does not guarantee future results.
0. Setup¶
Run the two cells below to reload the stock data and recalculate the indicators from Part 1 (moving averages and Bollinger Bands). This allows Part 2 to run independently.
# Run this cell — installs and imports all required packages
try:
import yfinance as yf
except ImportError:
!pip install yfinance
import yfinance as yf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import ipywidgets as widgets
from IPython.display import display
import warnings
warnings.filterwarnings('ignore')
%matplotlib inline
print("All libraries loaded successfully!")
# Run this cell — download data and recalculate indicators from Part 1
tickers = ['AAPL', 'GOOGL', 'MSFT', 'AMZN', 'TSLA']
stock_data = {}
for ticker in tickers:
df = yf.download(ticker, period='5y', progress=False)
df.columns = df.columns.get_level_values(0)
stock_data[ticker] = df
print(f" {ticker}: {len(df):,} trading days downloaded")
aapl = stock_data['AAPL'].copy()
# Recalculate indicators from Part 1
aapl['Daily_Return'] = aapl['Close'].pct_change() * 100
aapl['MA_20'] = aapl['Close'].rolling(window=20).mean()
aapl['MA_50'] = aapl['Close'].rolling(window=50).mean()
aapl['MA_200'] = aapl['Close'].rolling(window=200).mean()
bb_window = 20
aapl['BB_Middle'] = aapl['Close'].rolling(bb_window).mean()
aapl['BB_Std'] = aapl['Close'].rolling(bb_window).std()
aapl['BB_Upper'] = aapl['BB_Middle'] + (2 * aapl['BB_Std'])
aapl['BB_Lower'] = aapl['BB_Middle'] - (2 * aapl['BB_Std'])
print(f"\nDate range: {aapl.index.min().date()} to {aapl.index.max().date()}")
print(f"Columns: {list(aapl.columns.get_level_values(0).unique())}")
print("Setup complete -- all Part 1 indicators recalculated.")
6. Trading Strategy: Moving Average Crossover¶
Now we will use the moving averages from Part 1 to build a simple trading strategy. The MA crossover rule is one of the most well-known strategies in technical analysis:
BUY when the short-term MA (20-day) crosses above the long-term MA (50-day)
SELL when the short-term MA crosses below the long-term MA
The logic is that when the short-term average rises above the long-term average, it suggests upward momentum, and vice versa.
TO-DO: Create buy/sell signals based on the MA crossover rule. Fill in the signal values.
Hint
# TO-DO: Generate trading signals
# Signal = 1 -> BUY / HOLD long position
# Signal = -1 -> SELL / SHORT position
aapl['Signal'] = 0
# When 20-day MA is above 50-day MA -> bullish -> signal = ?
aapl.loc[aapl['MA_20'] > aapl['MA_50'], 'Signal'] = ...
# When 20-day MA is below 50-day MA -> bearish -> signal = ?
aapl.loc[aapl['MA_20'] < aapl['MA_50'], 'Signal'] = ...
# Detect crossovers: where the signal changes
aapl['Crossover'] = aapl['Signal'].diff()
buy_signals = aapl[aapl['Crossover'] == 2] # -1 -> +1 = change of 2
sell_signals = aapl[aapl['Crossover'] == -2] # +1 -> -1 = change of -2
print(f"Total BUY signals: {len(buy_signals)}")
print(f"Total SELL signals: {len(sell_signals)}")
# Run this cell — visualize the buy/sell signals on the price chart (last 3 years)
last_3y = aapl.loc[aapl.index >= aapl.index.max() - pd.DateOffset(years=3)]
buys_3y = buy_signals.loc[buy_signals.index >= last_3y.index.min()]
sells_3y = sell_signals.loc[sell_signals.index >= last_3y.index.min()]
plt.figure(figsize=(14, 7))
plt.plot(last_3y.index, last_3y['Close'], label='Closing Price', color='black', linewidth=1)
plt.plot(last_3y.index, last_3y['MA_20'], label='20-day MA', color='dodgerblue', linewidth=0.9)
plt.plot(last_3y.index, last_3y['MA_50'], label='50-day MA', color='orange', linewidth=0.9)
plt.scatter(buys_3y.index, buys_3y['Close'], marker='^', color='green', s=120,
label='BUY Signal', zorder=5, edgecolors='black', linewidth=0.5)
plt.scatter(sells_3y.index, sells_3y['Close'], marker='v', color='red', s=120,
label='SELL Signal', zorder=5, edgecolors='black', linewidth=0.5)
plt.title('Moving Average Crossover -- Buy & Sell Signals (Last 3 Years)', fontsize=16, fontweight='bold')
plt.xlabel('Date', fontsize=12)
plt.ylabel('Price ($)', fontsize=12)
plt.legend(fontsize=11)
plt.grid(alpha=0.3)
plt.tight_layout()
plt.show()
Question 6: Look at the buy/sell signal chart. Are there any signals that appear to be “wrong” -- for example, a buy signal right before a price drop? Why might simple rule-based strategies produce misleading signals?
Type your answer here, replacing this text.
7. Backtesting: Did the Strategy Make Money?¶
Backtesting means running a strategy against historical data to see how it would have performed. We compare the MA crossover strategy against a simple “buy and hold” approach (buying the stock on day 1 and never selling).
# Run this cell — backtest the MA crossover strategy
# Use last 3 years for the backtest
bt = last_3y[['Close', 'Signal']].copy()
bt = bt.dropna()
# Daily returns
bt['Market_Return'] = bt['Close'].pct_change()
# Strategy return: only earn market return when Signal = 1 (long position)
# When Signal = -1, we assume we're in cash (0% return)
bt['Strategy_Return'] = bt['Market_Return'] * bt['Signal'].shift(1)
# Cumulative returns (growth of $1)
bt['Buy_Hold_Growth'] = (1 + bt['Market_Return']).cumprod()
bt['Strategy_Growth'] = (1 + bt['Strategy_Return']).cumprod()
# Final results
bh_total = (bt['Buy_Hold_Growth'].iloc[-1] - 1) * 100
st_total = (bt['Strategy_Growth'].iloc[-1] - 1) * 100
print("=" * 50)
print(" BACKTEST RESULTS (Last 3 Years)")
print("=" * 50)
print(f" Buy & Hold Return: {bh_total:+.2f}%")
print(f" MA Crossover Return: {st_total:+.2f}%")
print(f" Difference: {st_total - bh_total:+.2f}%")
print("=" * 50)
if st_total > bh_total:
print(" -> Strategy OUTPERFORMED buy & hold")
else:
print(" -> Strategy UNDERPERFORMED buy & hold")
# Run this cell — plot cumulative growth comparison
plt.figure(figsize=(14, 7))
plt.plot(bt.index, bt['Buy_Hold_Growth'], label='Buy & Hold', color='blue', linewidth=1.5)
plt.plot(bt.index, bt['Strategy_Growth'], label='MA Crossover Strategy', color='green', linewidth=1.5)
plt.axhline(y=1, color='gray', linestyle='--', linewidth=0.8)
plt.fill_between(bt.index, bt['Buy_Hold_Growth'], bt['Strategy_Growth'],
where=(bt['Strategy_Growth'] > bt['Buy_Hold_Growth']),
color='green', alpha=0.1, label='Strategy Winning')
plt.fill_between(bt.index, bt['Buy_Hold_Growth'], bt['Strategy_Growth'],
where=(bt['Strategy_Growth'] <= bt['Buy_Hold_Growth']),
color='red', alpha=0.1, label='Buy & Hold Winning')
plt.title('Growth of $1: Buy & Hold vs MA Crossover Strategy', fontsize=16, fontweight='bold')
plt.xlabel('Date', fontsize=12)
plt.ylabel('Portfolio Value ($)', fontsize=12)
plt.legend(fontsize=11)
plt.grid(alpha=0.3)
plt.tight_layout()
plt.show()
Question 7: Did the MA crossover strategy beat simply buying and holding? Why do you think simple strategies like this often struggle to beat the market?
Type your answer here, replacing this text.
8. Risk Analysis¶
Returns alone don’t tell the full story. Two strategies might have similar returns but very different risk profiles. Let’s calculate some standard risk metrics to compare the strategies.
# Run this cell — compare risk metrics
def risk_report(returns, label):
"""Calculate key risk metrics for a return series."""
annual_return = returns.mean() * 252 * 100
annual_vol = returns.std() * np.sqrt(252) * 100
sharpe = (returns.mean() / returns.std()) * np.sqrt(252) if returns.std() > 0 else 0
max_dd = ((1 + returns).cumprod() / (1 + returns).cumprod().cummax() - 1).min() * 100
return {
'Strategy': label,
'Annual Return (%)': f"{annual_return:.2f}",
'Annual Volatility (%)': f"{annual_vol:.2f}",
'Sharpe Ratio': f"{sharpe:.2f}",
'Max Drawdown (%)': f"{max_dd:.2f}"
}
bh_metrics = risk_report(bt['Market_Return'].dropna(), 'Buy & Hold')
st_metrics = risk_report(bt['Strategy_Return'].dropna(), 'MA Crossover')
risk_df = pd.DataFrame([bh_metrics, st_metrics]).set_index('Strategy')
risk_df
Key terms explained:
Annual Volatility: How much the returns fluctuate year-to-year. Lower is more stable.
Sharpe Ratio: Return per unit of risk. Higher is better. Above 1.0 is generally considered good.
Max Drawdown: The largest peak-to-trough decline. This tells you the worst-case scenario -- how much you could have lost at the worst possible time.
Question 8: Compare the Sharpe Ratios and Max Drawdowns of both strategies. Even if one strategy has a lower total return, could it still be “better” from a risk perspective? Why might an investor prefer a lower-return strategy with lower risk?
Type your answer here, replacing this text.
9. Interactive Stock Explorer¶
Now let’s put everything together into an interactive tool. Use the dropdown and slider below to analyze different stocks and time windows. The widget calculates Bollinger Bands and identifies overbought/oversold conditions automatically.
# Run this cell — build the interactive stock analyzer
def analyze_stock(ticker, years):
"""Generate Bollinger Band analysis for a given stock and time window."""
df = stock_data[ticker].copy()
cutoff = df.index.max() - pd.DateOffset(years=years)
# Calculate Bollinger Bands on full data, then filter
df['BB_Mid'] = df['Close'].rolling(20).mean()
df['BB_Std'] = df['Close'].rolling(20).std()
df['BB_Up'] = df['BB_Mid'] + 2 * df['BB_Std']
df['BB_Lo'] = df['BB_Mid'] - 2 * df['BB_Std']
df = df.loc[df.index >= cutoff]
overbought = df[df['Close'] > df['BB_Up']]
oversold = df[df['Close'] < df['BB_Lo']]
fig, ax = plt.subplots(figsize=(14, 7))
ax.plot(df.index, df['Close'], label='Close', color='blue', linewidth=1.2)
ax.plot(df.index, df['BB_Mid'], label='SMA (20)', color='black', linestyle='--', linewidth=0.9)
ax.plot(df.index, df['BB_Up'], color='red', linewidth=0.7)
ax.plot(df.index, df['BB_Lo'], color='green', linewidth=0.7)
ax.fill_between(df.index, df['BB_Up'], df['BB_Lo'], color='gray', alpha=0.12)
if len(overbought) > 0:
ax.scatter(overbought.index, overbought['Close'], color='red',
marker='v', s=60, zorder=5, label=f'Overbought ({len(overbought)})')
if len(oversold) > 0:
ax.scatter(oversold.index, oversold['Close'], color='green',
marker='^', s=60, zorder=5, label=f'Oversold ({len(oversold)})')
ax.set_title(f'{ticker} -- Bollinger Bands ({years} Year{"s" if years > 1 else ""})',
fontsize=16, fontweight='bold')
ax.set_xlabel('Date', fontsize=12)
ax.set_ylabel('Price ($)', fontsize=12)
ax.legend(fontsize=10)
ax.grid(alpha=0.3)
plt.tight_layout()
plt.show()
latest = df.iloc[-1]
print(f" Latest Close: ${latest['Close']:.2f}")
print(f" Upper Band: ${latest['BB_Up']:.2f}")
print(f" Lower Band: ${latest['BB_Lo']:.2f}")
status = "OVERBOUGHT" if latest['Close'] > latest['BB_Up'] else \
"OVERSOLD" if latest['Close'] < latest['BB_Lo'] else "NORMAL"
print(f" Current Status: {status}")
# Build widget interface
stock_dropdown = widgets.Dropdown(options=tickers, value='AAPL', description='Stock:')
years_slider = widgets.IntSlider(value=2, min=1, max=5, step=1, description='Years:')
out = widgets.Output()
def on_change(_):
with out:
out.clear_output(wait=True)
analyze_stock(stock_dropdown.value, years_slider.value)
stock_dropdown.observe(on_change, names='value')
years_slider.observe(on_change, names='value')
print("=== INTERACTIVE STOCK ANALYZER ===")
display(widgets.HBox([stock_dropdown, years_slider]))
display(out)
on_change(None)
TO-DO: Use the interactive widget to analyze a stock other than Apple. Answer the following:
Question 9: Which stock did you choose? Is it currently overbought, oversold, or in the normal range? Compare the band width to Apple’s -- which stock appears more volatile?
Type your answer here, replacing this text.
10. Your Turn: Analyze a Stock of Your Choice¶
In the cell below, download data for a stock that is not in our original list. You can find ticker symbols on Yahoo Finance.
Complete the following tasks:
Download the data using
yf.download()Calculate 20-day and 50-day moving averages
Calculate Bollinger Bands
Create at least one visualization
# TO-DO: Choose your own stock and perform the analysis
# Step 1: Download data (replace 'NFLX' with any ticker you want)
my_stock = yf.download('...', period='5y', progress=False)
# Step 2: Calculate moving averages
my_stock['MA_20'] = ...
my_stock['MA_50'] = ...
# Step 3: Calculate Bollinger Bands
my_stock['BB_Mid'] = ...
my_stock['BB_Std'] = ...
my_stock['BB_Upper'] = ...
my_stock['BB_Lower'] = ...
# Step 4: Create a plot (at least one visualization)
plt.figure(figsize=(14, 7))
# ... your plotting code here ...
plt.show()
Question 10: Describe what you found for your chosen stock. Does it show different behavior compared to Apple? Would the MA crossover strategy have worked better or worse on this stock? What might explain the differences?
Type your answer here, replacing this text.
11. Reflection: Excel vs Python for Data Analysis¶
Question 11: Based on your experience in this lab, list at least 3 advantages of using Python/pandas for stock analysis compared to doing the same work in Excel. Then list at least 1 situation where Excel might still be preferable.
Type your answer here, replacing this text.
Question 12: Think about the backtesting process. In Excel, you’d need to write formulas row-by-row, then manually calculate cumulative returns. In Python, we did it in a few lines. How does this relate to the concept of automation in data analysis?
Type your answer here, replacing this text.
Bonus Challenge (Optional)¶
Try to improve the MA crossover strategy. Some ideas:
Different MA periods: Instead of 20 and 50, try 10 and 30, or 50 and 200. Does performance change?
Add a volume filter: Only trade when volume is above its 20-day average.
Combine indicators: Use both MA crossover and Bollinger Bands. For example, only buy when the MA signal is bullish AND price is near the lower Bollinger Band.
Test multiple stocks: Run your strategy on all 5 stocks and compare results.
# Bonus: Experiment with strategy improvements here
Summary¶
In this lab, you learned to:
Implement a trading strategy using moving average crossovers
Backtest a strategy against historical data to evaluate performance
Measure risk using volatility, Sharpe Ratio, and maximum drawdown
Build interactive visualizations for exploring different stocks
Analyze a stock of your own choosing
Key takeaway: Python and pandas let you work with data as complete sets, automating repetitive analysis and testing ideas that would take hours to set up in Excel. Backtesting, risk metrics, and interactive tools are all practical applications that go well beyond what spreadsheets can do efficiently.
Important reminder: This lab is for educational purposes only. Real stock trading involves significant financial risk. Always consult a qualified financial advisor before making investment decisions.