Module 7: Technical Analysis with Python
Learning Objectives
By the end of this module, you will:
- Understand the principles and limitations of technical analysis
- Calculate and interpret moving averages (SMA, EMA, WMA)
- Build momentum indicators (RSI, MACD, Stochastic)
- Implement volatility indicators (Bollinger Bands, ATR)
- Create volume-based indicators
- Backtest simple trading strategies
- Visualize technical indicators professionally
- Recognize common chart patterns programmatically
- Build complete technical analysis dashboards
7.1 Introduction to Technical Analysis
What is Technical Analysis?
Technical analysis studies historical price and volume data to forecast future price movements. Unlike fundamental analysis (which examines financial statements and business metrics), technical analysis focuses entirely on market action.
Core Principles
- Price discounts everything: All information (fundamental, economic, psychological) is already reflected in the price
- Price moves in trends: Markets trend, and these trends persist until clear reversal signals appear
- History repeats itself: Market participants react similarly to similar situations, creating recognizable patterns
Important Disclaimer
Technical analysis is controversial. Academic research shows mixed results on its effectiveness. Markets are largely efficient, making consistent outperformance difficult. Use technical analysis as one tool among many, not as a standalone crystal ball.
Types of Technical Indicators
Trend Indicators
- Moving Averages (SMA, EMA)
- MACD
- ADX (Average Directional Index)
Momentum Indicators
- RSI (Relative Strength Index)
- Stochastic Oscillator
- ROC (Rate of Change)
Volatility Indicators
- Bollinger Bands
- ATR (Average True Range)
- Keltner Channels
Volume Indicators
- OBV (On-Balance Volume)
- Volume MA
- Accumulation/Distribution
7.2 Moving Averages
Simple Moving Average (SMA)
The most basic indicator: the average price over N periods.
import yfinance as yf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
# Download data
data = yf.download('AAPL', start='2023-01-01', end='2024-01-01', progress=False)
prices = data['Adj Close']
# Calculate SMAs
sma_20 = prices.rolling(window=20).mean()
sma_50 = prices.rolling(window=50).mean()
sma_200 = prices.rolling(window=200).mean()
# Plot
plt.figure(figsize=(14, 7))
plt.plot(prices.index, prices.values, label='Price', linewidth=2, alpha=0.7)
plt.plot(sma_20.index, sma_20.values, label='20-day SMA', linewidth=2)
plt.plot(sma_50.index, sma_50.values, label='50-day SMA', linewidth=2)
plt.plot(sma_200.index, sma_200.values, label='200-day SMA', linewidth=2)
plt.title('Apple Stock with Moving Averages', fontsize=16, fontweight='bold', pad=20)
plt.xlabel('Date', fontsize=12)
plt.ylabel('Price ($)', fontsize=12)
plt.legend(fontsize=11)
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# Identify trend
current_price = prices.iloc[-1]
current_sma20 = sma_20.iloc[-1]
current_sma50 = sma_50.iloc[-1]
print(f"Current Price: ${current_price:.2f}")
print(f"20-day SMA: ${current_sma20:.2f}")
print(f"50-day SMA: ${current_sma50:.2f}")
if current_price > current_sma20 > current_sma50:
print("\nTrend Signal: Strong Uptrend ↑")
elif current_price < current_sma20 < current_sma50:
print("\nTrend Signal: Strong Downtrend ↓")
else:
print("\nTrend Signal: Mixed/Neutral")
Exponential Moving Average (EMA)
EMA gives more weight to recent prices, making it more responsive than SMA.
# Calculate EMAs
ema_12 = prices.ewm(span=12, adjust=False).mean()
ema_26 = prices.ewm(span=26, adjust=False).mean()
# Manual EMA calculation (for understanding)
def calculate_ema(prices, period):
"""Calculate EMA manually"""
ema = prices.copy()
multiplier = 2 / (period + 1)
# Initialize with SMA
ema.iloc[period-1] = prices.iloc[:period].mean()
# Calculate EMA for remaining periods
for i in range(period, len(prices)):
ema.iloc[i] = (prices.iloc[i] * multiplier) + (ema.iloc[i-1] * (1 - multiplier))
return ema
# Compare SMA vs EMA
plt.figure(figsize=(14, 7))
plt.plot(prices.index, prices.values, label='Price', linewidth=2, alpha=0.5, color='black')
plt.plot(sma_20.index, sma_20.values, label='20-day SMA', linewidth=2)
plt.plot(ema_12.index, ema_12.values, label='12-day EMA', linewidth=2, linestyle='--')
plt.title('SMA vs EMA Comparison', fontsize=16, fontweight='bold', pad=20)
plt.xlabel('Date', fontsize=12)
plt.ylabel('Price ($)', fontsize=12)
plt.legend(fontsize=11)
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
print("EMA is more responsive to recent price changes than SMA")
Golden Cross and Death Cross
Classic trading signals based on MA crossovers.
# Identify crossovers
signal = pd.DataFrame(index=prices.index)
signal['Price'] = prices
signal['SMA_50'] = sma_50
signal['SMA_200'] = sma_200
# Create signal column
signal['Position'] = 0
signal.loc[signal['SMA_50'] > signal['SMA_200'], 'Position'] = 1 # Bullish
signal.loc[signal['SMA_50'] <= signal['SMA_200'], 'Position'] = -1 # Bearish
# Find crossover points
signal['Signal'] = signal['Position'].diff()
# Golden crosses (SMA 50 crosses above SMA 200)
golden_crosses = signal[signal['Signal'] == 2].copy()
# Death crosses (SMA 50 crosses below SMA 200)
death_crosses = signal[signal['Signal'] == -2].copy()
print(f"Golden Crosses: {len(golden_crosses)}")
print(f"Death Crosses: {len(death_crosses)}")
# Plot with crossovers
plt.figure(figsize=(14, 7))
plt.plot(prices.index, prices.values, label='Price', linewidth=2, alpha=0.7)
plt.plot(sma_50.index, sma_50.values, label='50-day SMA', linewidth=2)
plt.plot(sma_200.index, sma_200.values, label='200-day SMA', linewidth=2)
# Mark golden crosses
if len(golden_crosses) > 0:
plt.scatter(golden_crosses.index, prices.loc[golden_crosses.index],
marker='^', s=200, color='green', label='Golden Cross', zorder=5)
# Mark death crosses
if len(death_crosses) > 0:
plt.scatter(death_crosses.index, prices.loc[death_crosses.index],
marker='v', s=200, color='red', label='Death Cross', zorder=5)
plt.title('Moving Average Crossovers', fontsize=16, fontweight='bold', pad=20)
plt.xlabel('Date', fontsize=12)
plt.ylabel('Price ($)', fontsize=12)
plt.legend(fontsize=11)
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# Show recent crossovers
if len(golden_crosses) > 0:
print("\nMost Recent Golden Cross:")
print(golden_crosses.tail(1)[['Price', 'SMA_50', 'SMA_200']])
if len(death_crosses) > 0:
print("\nMost Recent Death Cross:")
print(death_crosses.tail(1)[['Price', 'SMA_50', 'SMA_200']])
7.3 Momentum Indicators
Relative Strength Index (RSI)
RSI measures the magnitude of recent price changes to evaluate overbought or oversold conditions.
- RSI > 70: Potentially overbought
- RSI < 30: Potentially oversold
def calculate_rsi(prices, period=14):
"""
Calculate RSI
RSI = 100 - (100 / (1 + RS))
where RS = Average Gain / Average Loss
"""
# Calculate price changes
delta = prices.diff()
# Separate gains and losses
gains = delta.where(delta > 0, 0)
losses = -delta.where(delta < 0, 0)
# Calculate average gains and losses
avg_gains = gains.rolling(window=period).mean()
avg_losses = losses.rolling(window=period).mean()
# Calculate RS and RSI
rs = avg_gains / avg_losses
rsi = 100 - (100 / (1 + rs))
return rsi
# Calculate RSI
data = yf.download('AAPL', start='2023-01-01', end='2024-01-01', progress=False)
prices = data['Adj Close']
rsi = calculate_rsi(prices, period=14)
# Plot price and RSI
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(14, 10), sharex=True)
# Price chart
ax1.plot(prices.index, prices.values, linewidth=2, color='#2E86AB')
ax1.set_ylabel('Price ($)', fontsize=12)
ax1.set_title('Apple Stock Price', fontsize=14, fontweight='bold')
ax1.grid(True, alpha=0.3)
# RSI chart
ax2.plot(rsi.index, rsi.values, linewidth=2, color='#A23B72')
ax2.axhline(y=70, color='red', linestyle='--', linewidth=1, alpha=0.7, label='Overbought (70)')
ax2.axhline(y=30, color='green', linestyle='--', linewidth=1, alpha=0.7, label='Oversold (30)')
ax2.fill_between(rsi.index, 30, 70, alpha=0.1, color='gray')
ax2.set_ylabel('RSI', fontsize=12)
ax2.set_xlabel('Date', fontsize=12)
ax2.set_title('RSI (14-period)', fontsize=14, fontweight='bold')
ax2.legend(fontsize=10)
ax2.grid(True, alpha=0.3)
ax2.set_ylim([0, 100])
plt.tight_layout()
plt.show()
# Identify overbought/oversold conditions
current_rsi = rsi.iloc[-1]
print(f"Current RSI: {current_rsi:.2f}")
if current_rsi > 70:
print("Signal: Overbought - potential selling opportunity")
elif current_rsi < 30:
print("Signal: Oversold - potential buying opportunity")
else:
print("Signal: Neutral")
# Count extreme RSI periods
overbought_days = (rsi > 70).sum()
oversold_days = (rsi < 30).sum()
print(f"\nDays overbought (RSI > 70): {overbought_days}")
print(f"Days oversold (RSI < 30): {oversold_days}")
MACD (Moving Average Convergence Divergence)
MACD shows the relationship between two moving averages and generates trading signals.
def calculate_macd(prices, fast=12, slow=26, signal=9):
"""
Calculate MACD
MACD Line = 12-day EMA - 26-day EMA
Signal Line = 9-day EMA of MACD Line
Histogram = MACD Line - Signal Line
"""
# Calculate EMAs
ema_fast = prices.ewm(span=fast, adjust=False).mean()
ema_slow = prices.ewm(span=slow, adjust=False).mean()
# MACD line
macd_line = ema_fast - ema_slow
# Signal line
signal_line = macd_line.ewm(span=signal, adjust=False).mean()
# Histogram
histogram = macd_line - signal_line
return macd_line, signal_line, histogram
# Calculate MACD
macd_line, signal_line, histogram = calculate_macd(prices)
# Plot
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(14, 10), sharex=True)
# Price chart
ax1.plot(prices.index, prices.values, linewidth=2, color='#2E86AB')
ax1.set_ylabel('Price ($)', fontsize=12)
ax1.set_title('Apple Stock Price', fontsize=14, fontweight='bold')
ax1.grid(True, alpha=0.3)
# MACD chart
ax2.plot(macd_line.index, macd_line.values, linewidth=2, label='MACD Line', color='blue')
ax2.plot(signal_line.index, signal_line.values, linewidth=2, label='Signal Line', color='red')
ax2.bar(histogram.index, histogram.values, label='Histogram', alpha=0.3, color='gray')
ax2.axhline(y=0, color='black', linewidth=1)
ax2.set_ylabel('MACD', fontsize=12)
ax2.set_xlabel('Date', fontsize=12)
ax2.set_title('MACD (12, 26, 9)', fontsize=14, fontweight='bold')
ax2.legend(fontsize=10)
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# Identify signals
macd_signals = pd.DataFrame({
'MACD': macd_line,
'Signal': signal_line,
'Histogram': histogram
})
macd_signals['Position'] = 0
macd_signals.loc[macd_signals['MACD'] > macd_signals['Signal'], 'Position'] = 1
macd_signals['Signal_Change'] = macd_signals['Position'].diff()
# Bullish crossovers (MACD crosses above signal)
bullish_cross = macd_signals[macd_signals['Signal_Change'] == 1]
# Bearish crossovers (MACD crosses below signal)
bearish_cross = macd_signals[macd_signals['Signal_Change'] == -1]
print(f"Bullish MACD Crossovers: {len(bullish_cross)}")
print(f"Bearish MACD Crossovers: {len(bearish_cross)}")
if len(bullish_cross) > 0:
print(f"\nMost Recent Bullish Crossover: {bullish_cross.index[-1].strftime('%Y-%m-%d')}")
if len(bearish_cross) > 0:
print(f"Most Recent Bearish Crossover: {bearish_cross.index[-1].strftime('%Y-%m-%d')}")
Stochastic Oscillator
Compares closing price to price range over a period, indicating momentum.
def calculate_stochastic(data, k_period=14, d_period=3):
"""
Calculate Stochastic Oscillator
%K = (Current Close - Lowest Low) / (Highest High - Lowest Low) * 100
%D = 3-day SMA of %K
"""
# Get high, low, close
high = data['High']
low = data['Low']
close = data['Close']
# Calculate %K
lowest_low = low.rolling(window=k_period).min()
highest_high = high.rolling(window=k_period).max()
k_percent = 100 * ((close - lowest_low) / (highest_high - lowest_low))
# Calculate %D (signal line)
d_percent = k_percent.rolling(window=d_period).mean()
return k_percent, d_percent
# Download data with OHLC
data = yf.download('AAPL', start='2023-01-01', end='2024-01-01', progress=False)
k_percent, d_percent = calculate_stochastic(data)
# Plot
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(14, 10), sharex=True)
# Price
ax1.plot(data.index, data['Adj Close'], linewidth=2, color='#2E86AB')
ax1.set_ylabel('Price ($)', fontsize=12)
ax1.set_title('Apple Stock Price', fontsize=14, fontweight='bold')
ax1.grid(True, alpha=0.3)
# Stochastic
ax2.plot(k_percent.index, k_percent.values, linewidth=2, label='%K', color='blue')
ax2.plot(d_percent.index, d_percent.values, linewidth=2, label='%D', color='red')
ax2.axhline(y=80, color='red', linestyle='--', linewidth=1, alpha=0.7, label='Overbought')
ax2.axhline(y=20, color='green', linestyle='--', linewidth=1, alpha=0.7, label='Oversold')
ax2.fill_between(k_percent.index, 20, 80, alpha=0.1, color='gray')
ax2.set_ylabel('Stochastic', fontsize=12)
ax2.set_xlabel('Date', fontsize=12)
ax2.set_title('Stochastic Oscillator (14, 3)', fontsize=14, fontweight='bold')
ax2.legend(fontsize=10)
ax2.grid(True, alpha=0.3)
ax2.set_ylim([0, 100])
plt.tight_layout()
plt.show()
# Current signal
current_k = k_percent.iloc[-1]
current_d = d_percent.iloc[-1]
print(f"Current %K: {current_k:.2f}")
print(f"Current %D: {current_d:.2f}")
if current_k > 80:
print("Signal: Overbought")
elif current_k < 20:
print("Signal: Oversold")
else:
print("Signal: Neutral")
7.4 Volatility Indicators
Bollinger Bands
Bollinger Bands show price volatility and potential reversal points.
def calculate_bollinger_bands(prices, period=20, num_std=2):
"""
Calculate Bollinger Bands
Middle Band = 20-day SMA
Upper Band = Middle Band + (2 × Standard Deviation)
Lower Band = Middle Band - (2 × Standard Deviation)
"""
# Middle band (SMA)
middle_band = prices.rolling(window=period).mean()
# Standard deviation
std = prices.rolling(window=period).std()
# Upper and lower bands
upper_band = middle_band + (num_std * std)
lower_band = middle_band - (num_std * std)
return upper_band, middle_band, lower_band
# Calculate Bollinger Bands
data = yf.download('AAPL', start='2023-01-01', end='2024-01-01', progress=False)
prices = data['Adj Close']
upper_band, middle_band, lower_band = calculate_bollinger_bands(prices)
# Plot
plt.figure(figsize=(14, 7))
plt.plot(prices.index, prices.values, linewidth=2, label='Price', color='black')
plt.plot(upper_band.index, upper_band.values, linewidth=1.5, label='Upper Band',
color='red', linestyle='--')
plt.plot(middle_band.index, middle_band.values, linewidth=1.5, label='Middle Band (SMA)',
color='blue')
plt.plot(lower_band.index, lower_band.values, linewidth=1.5, label='Lower Band',
color='green', linestyle='--')
# Fill between bands
plt.fill_between(prices.index, upper_band, lower_band, alpha=0.1, color='gray')
plt.title('Bollinger Bands (20, 2)', fontsize=16, fontweight='bold', pad=20)
plt.xlabel('Date', fontsize=12)
plt.ylabel('Price ($)', fontsize=12)
plt.legend(fontsize=11)
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# Calculate %B (position within bands)
bb_percent = (prices - lower_band) / (upper_band - lower_band)
print(f"Current Price: ${prices.iloc[-1]:.2f}")
print(f"Upper Band: ${upper_band.iloc[-1]:.2f}")
print(f"Middle Band: ${middle_band.iloc[-1]:.2f}")
print(f"Lower Band: ${lower_band.iloc[-1]:.2f}")
print(f"%B: {bb_percent.iloc[-1]:.2f}")
if bb_percent.iloc[-1] > 1:
print("\nSignal: Price above upper band - potentially overbought")
elif bb_percent.iloc[-1] < 0:
print("\nSignal: Price below lower band - potentially oversold")
else:
print("\nSignal: Price within bands - normal range")
# Bandwidth (measures volatility)
bandwidth = (upper_band - lower_band) / middle_band
current_bandwidth = bandwidth.iloc[-1]
avg_bandwidth = bandwidth.mean()
print(f"\nCurrent Bandwidth: {current_bandwidth:.4f}")
print(f"Average Bandwidth: {avg_bandwidth:.4f}")
if current_bandwidth < avg_bandwidth * 0.5:
print("Volatility: Low (Squeeze - potential breakout ahead)")
elif current_bandwidth > avg_bandwidth * 1.5:
print("Volatility: High (Expansion)")
Average True Range (ATR)
ATR measures market volatility.
def calculate_atr(data, period=14):
"""
Calculate Average True Range
True Range = max(High - Low, abs(High - Previous Close), abs(Low - Previous Close))
ATR = Moving Average of True Range
"""
high = data['High']
low = data['Low']
close = data['Close']
# Calculate True Range
tr1 = high - low
tr2 = abs(high - close.shift())
tr3 = abs(low - close.shift())
true_range = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
# ATR is the moving average of True Range
atr = true_range.rolling(window=period).mean()
return atr
# Calculate ATR
data = yf.download('AAPL', start='2023-01-01', end='2024-01-01', progress=False)
atr = calculate_atr(data, period=14)
# Plot
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(14, 10), sharex=True)
# Price
ax1.plot(data.index, data['Adj Close'], linewidth=2, color='#2E86AB')
ax1.set_ylabel('Price ($)', fontsize=12)
ax1.set_title('Apple Stock Price', fontsize=14, fontweight='bold')
ax1.grid(True, alpha=0.3)
# ATR
ax2.plot(atr.index, atr.values, linewidth=2, color='#A23B72')
ax2.fill_between(atr.index, 0, atr.values, alpha=0.3, color='#A23B72')
ax2.set_ylabel('ATR ($)', fontsize=12)
ax2.set_xlabel('Date', fontsize=12)
ax2.set_title('Average True Range (14)', fontsize=14, fontweight='bold')
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# Analyze volatility
current_atr = atr.iloc[-1]
avg_atr = atr.mean()
current_price = data['Adj Close'].iloc[-1]
atr_percent = (current_atr / current_price) * 100
print(f"Current ATR: ${current_atr:.2f}")
print(f"Average ATR: ${avg_atr:.2f}")
print(f"ATR as % of Price: {atr_percent:.2f}%")
if current_atr > avg_atr * 1.5:
print("\nVolatility: High - larger price swings expected")
elif current_atr < avg_atr * 0.5:
print("\nVolatility: Low - smaller price swings expected")
else:
print("\nVolatility: Normal")
7.5 Volume Indicators
On-Balance Volume (OBV)
OBV uses volume flow to predict price changes.
def calculate_obv(data):
"""
Calculate On-Balance Volume
If close > previous close: OBV = previous OBV + volume
If close < previous close: OBV = previous OBV - volume
If close = previous close: OBV = previous OBV
"""
obv = pd.Series(index=data.index, dtype=float)
obv.iloc[0] = data['Volume'].iloc[0]
for i in range(1, len(data)):
if data['Close'].iloc[i] > data['Close'].iloc[i-1]:
obv.iloc[i] = obv.iloc[i-1] + data['Volume'].iloc[i]
elif data['Close'].iloc[i] < data['Close'].iloc[i-1]:
obv.iloc[i] = obv.iloc[i-1] - data['Volume'].iloc[i]
else:
obv.iloc[i] = obv.iloc[i-1]
return obv
# Calculate OBV
data = yf.download('AAPL', start='2023-01-01', end='2024-01-01', progress=False)
obv = calculate_obv(data)
# Plot
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(14, 10), sharex=True)
# Price
ax1.plot(data.index, data['Adj Close'], linewidth=2, color='#2E86AB')
ax1.set_ylabel('Price ($)', fontsize=12)
ax1.set_title('Apple Stock Price', fontsize=14, fontweight='bold')
ax1.grid(True, alpha=0.3)
# OBV
ax2.plot(obv.index, obv.values, linewidth=2, color='#06A77D')
ax2.fill_between(obv.index, 0, obv.values, alpha=0.3, color='#06A77D')
ax2.set_ylabel('OBV', fontsize=12)
ax2.set_xlabel('Date', fontsize=12)
ax2.set_title('On-Balance Volume', fontsize=14, fontweight='bold')
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# Check divergence
price_change = (data['Adj Close'].iloc[-1] / data['Adj Close'].iloc[-30] - 1) * 100
obv_change = (obv.iloc[-1] / obv.iloc[-30] - 1) * 100
print(f"30-day Price Change: {price_change:+.2f}%")
print(f"30-day OBV Change: {obv_change:+.2f}%")
if price_change > 0 and obv_change < 0:
print("\nDivergence: Bearish (price up, volume down)")
elif price_change < 0 and obv_change > 0:
print("\nDivergence: Bullish (price down, volume up)")
else:
print("\nNo significant divergence")
Volume Moving Average
# Calculate volume moving average
volume_ma = data['Volume'].rolling(window=20).mean()
# Plot
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(14, 10), sharex=True)
# Price
ax1.plot(data.index, data['Adj Close'], linewidth=2, color='#2E86AB')
ax1.set_ylabel('Price ($)', fontsize=12)
ax1.set_title('Apple Stock Price', fontsize=14, fontweight='bold')
ax1.grid(True, alpha=0.3)
# Volume
ax2.bar(data.index, data['Volume'], alpha=0.5, color='#A23B72', label='Volume')
ax2.plot(volume_ma.index, volume_ma.values, linewidth=2, color='#E63946',
label='20-day MA', linestyle='--')
ax2.set_ylabel('Volume', fontsize=12)
ax2.set_xlabel('Date', fontsize=12)
ax2.set_title('Trading Volume', fontsize=14, fontweight='bold')
ax2.legend(fontsize=10)
ax2.grid(True, alpha=0.3, axis='y')
plt.tight_layout()
plt.show()
# Identify volume spikes
current_volume = data['Volume'].iloc[-1]
avg_volume = volume_ma.iloc[-1]
volume_ratio = current_volume / avg_volume
print(f"Current Volume: {current_volume:,.0f}")
print(f"20-day Average Volume: {avg_volume:,.0f}")
print(f"Volume Ratio: {volume_ratio:.2f}x")
if volume_ratio > 2:
print("\nVolume: Significantly above average (high interest)")
elif volume_ratio < 0.5:
print("\nVolume: Significantly below average (low interest)")
else:
print("\nVolume: Normal")
7.6 Building a Technical Analysis Dashboard
Complete Multi-Indicator Dashboard
import yfinance as yf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.gridspec import GridSpec
def technical_analysis_dashboard(ticker, start_date, end_date):
"""
Create comprehensive technical analysis dashboard
"""
# Download data
data = yf.download(ticker, start=start_date, end=end_date, progress=False)
prices = data['Adj Close']
# Calculate all indicators
# Moving Averages
sma_20 = prices.rolling(window=20).mean()
sma_50 = prices.rolling(window=50).mean()
ema_12 = prices.ewm(span=12, adjust=False).mean()
# Bollinger Bands
bb_middle = prices.rolling(window=20).mean()
bb_std = prices.rolling(window=20).std()
bb_upper = bb_middle + (2 * bb_std)
bb_lower = bb_middle - (2 * bb_std)
# RSI
delta = prices.diff()
gains = delta.where(delta > 0, 0).rolling(window=14).mean()
losses = -delta.where(delta < 0, 0).rolling(window=14).mean()
rs = gains / losses
rsi = 100 - (100 / (1 + rs))
# MACD
ema_fast = prices.ewm(span=12, adjust=False).mean()
ema_slow = prices.ewm(span=26, adjust=False).mean()
macd_line = ema_fast - ema_slow
signal_line = macd_line.ewm(span=9, adjust=False).mean()
macd_hist = macd_line - signal_line
# Volume
volume_ma = data['Volume'].rolling(window=20).mean()
# Create figure
fig = plt.figure(figsize=(16, 12))
gs = GridSpec(4, 1, height_ratios=[3, 1, 1, 1], hspace=0.3)
# 1. Price with Bollinger Bands and Moving Averages
ax1 = fig.add_subplot(gs[0])
ax1.plot(prices.index, prices.values, linewidth=2, label='Price', color='black', alpha=0.7)
ax1.plot(sma_20.index, sma_20.values, linewidth=1.5, label='SMA 20', alpha=0.7)
ax1.plot(sma_50.index, sma_50.values, linewidth=1.5, label='SMA 50', alpha=0.7)
ax1.plot(bb_upper.index, bb_upper.values, linewidth=1, label='BB Upper',
linestyle='--', color='red', alpha=0.5)
ax1.plot(bb_lower.index, bb_lower.values, linewidth=1, label='BB Lower',
linestyle='--', color='green', alpha=0.5)
ax1.fill_between(prices.index, bb_upper, bb_lower, alpha=0.1, color='gray')
ax1.set_ylabel('Price ($)', fontsize=11)
ax1.set_title(f'{ticker} Technical Analysis Dashboard', fontsize=16, fontweight='bold')
ax1.legend(loc='best', fontsize=9)
ax1.grid(True, alpha=0.3)
# 2. Volume
ax2 = fig.add_subplot(gs[1], sharex=ax1)
colors = ['green' if data['Close'][i] >= data['Open'][i] else 'red'
for i in range(len(data))]
ax2.bar(data.index, data['Volume'], color=colors, alpha=0.5)
ax2.plot(volume_ma.index, volume_ma.values, linewidth=2, color='blue',
label='Volume MA', linestyle='--')
ax2.set_ylabel('Volume', fontsize=11)
ax2.legend(loc='best', fontsize=9)
ax2.grid(True, alpha=0.3, axis='y')
# 3. RSI
ax3 = fig.add_subplot(gs[2], sharex=ax1)
ax3.plot(rsi.index, rsi.values, linewidth=2, color='purple')
ax3.axhline(y=70, color='red', linestyle='--', linewidth=1, alpha=0.7)
ax3.axhline(y=30, color='green', linestyle='--', linewidth=1, alpha=0.7)
ax3.fill_between(rsi.index, 30, 70, alpha=0.1, color='gray')
ax3.set_ylabel('RSI', fontsize=11)
ax3.set_ylim([0, 100])
ax3.grid(True, alpha=0.3)
# 4. MACD
ax4 = fig.add_subplot(gs[3], sharex=ax1)
ax4.plot(macd_line.index, macd_line.values, linewidth=2, label='MACD', color='blue')
ax4.plot(signal_line.index, signal_line.values, linewidth=2, label='Signal', color='red')
ax4.bar(macd_hist.index, macd_hist.values, label='Histogram', alpha=0.3, color='gray')
ax4.axhline(y=0, color='black', linewidth=1)
ax4.set_ylabel('MACD', fontsize=11)
ax4.set_xlabel('Date', fontsize=11)
ax4.legend(loc='best', fontsize=9)
ax4.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# Print summary
print("\n" + "="*70)
print(f"TECHNICAL ANALYSIS SUMMARY: {ticker}")
print("="*70)
current_price = prices.iloc[-1]
current_rsi = rsi.iloc[-1]
current_macd = macd_line.iloc[-1]
current_signal = signal_line.iloc[-1]
print(f"\nCurrent Price: ${current_price:.2f}")
print(f"20-day SMA: ${sma_20.iloc[-1]:.2f}")
print(f"50-day SMA: ${sma_50.iloc[-1]:.2f}")
print(f"\nRSI (14): {current_rsi:.2f}", end=" - ")
if current_rsi > 70:
print("OVERBOUGHT")
elif current_rsi < 30:
print("OVERSOLD")
else:
print("NEUTRAL")
print(f"\nMACD: {current_macd:.2f}")
print(f"Signal: {current_signal:.2f}")
if current_macd > current_signal:
print("MACD Signal: BULLISH")
else:
print("MACD Signal: BEARISH")
# Trend
if current_price > sma_20.iloc[-1] > sma_50.iloc[-1]:
print("\nTrend: STRONG UPTREND")
elif current_price < sma_20.iloc[-1] < sma_50.iloc[-1]:
print("\nTrend: STRONG DOWNTREND")
elif current_price > sma_50.iloc[-1]:
print("\nTrend: UPTREND")
elif current_price < sma_50.iloc[-1]:
print("\nTrend: DOWNTREND")
else:
print("\nTrend: SIDEWAYS")
print("="*70)
# Create dashboard
technical_analysis_dashboard('AAPL', '2023-01-01', '2024-01-01')
7.7 Simple Backtesting
MA Crossover Strategy Backtest
def backtest_ma_crossover(ticker, start_date, end_date, fast_period=20, slow_period=50):
"""
Backtest a simple MA crossover strategy
"""
# Download data
data = yf.download(ticker, start=start_date, end=end_date, progress=False)
prices = data['Adj Close']
# Calculate moving averages
fast_ma = prices.rolling(window=fast_period).mean()
slow_ma = prices.rolling(window=slow_period).mean()
# Generate signals
signals = pd.DataFrame(index=prices.index)
signals['Price'] = prices
signals['Fast_MA'] = fast_ma
signals['Slow_MA'] = slow_ma
signals['Position'] = 0
# Position: 1 when fast > slow, 0 otherwise
signals.loc[signals['Fast_MA'] > signals['Slow_MA'], 'Position'] = 1
# Calculate returns
signals['Market_Return'] = prices.pct_change()
signals['Strategy_Return'] = signals['Position'].shift(1) * signals['Market_Return']
# Calculate cumulative returns
signals['Market_Cumulative'] = (1 + signals['Market_Return']).cumprod()
signals['Strategy_Cumulative'] = (1 + signals['Strategy_Return']).cumprod()
# Performance metrics
total_market_return = signals['Market_Cumulative'].iloc[-1] - 1
total_strategy_return = signals['Strategy_Cumulative'].iloc[-1] - 1
strategy_volatility = signals['Strategy_Return'].std() * np.sqrt(252)
strategy_sharpe = (signals['Strategy_Return'].mean() * 252) / strategy_volatility
# Count trades
trades = signals['Position'].diff().abs().sum() / 2
# Plot
plt.figure(figsize=(14, 7))
plt.plot(signals.index, signals['Market_Cumulative'],
linewidth=2, label='Buy & Hold', alpha=0.7)
plt.plot(signals.index, signals['Strategy_Cumulative'],
linewidth=2, label='MA Crossover Strategy', alpha=0.7)
plt.title(f'{ticker} - MA Crossover Strategy Backtest ({fast_period}/{slow_period})',
fontsize=16, fontweight='bold', pad=20)
plt.xlabel('Date', fontsize=12)
plt.ylabel('Cumulative Return', fontsize=12)
plt.legend(fontsize=11)
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# Print results
print("\n" + "="*70)
print("BACKTEST RESULTS")
print("="*70)
print(f"Ticker: {ticker}")
print(f"Period: {start_date} to {end_date}")
print(f"Strategy: {fast_period}/{slow_period} MA Crossover")
print("\n" + "-"*70)
print(f"Buy & Hold Return: {total_market_return*100:.2f}%")
print(f"Strategy Return: {total_strategy_return*100:.2f}%")
print(f"Outperformance: {(total_strategy_return - total_market_return)*100:.2f}%")
print(f"\nStrategy Volatility: {strategy_volatility*100:.2f}%")
print(f"Strategy Sharpe Ratio: {strategy_sharpe:.2f}")
print(f"\nNumber of Trades: {int(trades)}")
print("="*70)
return signals
# Run backtest
results = backtest_ma_crossover('AAPL', '2020-01-01', '2024-01-01',
fast_period=50, slow_period=200)
7.8 Practice Exercises
Exercise 1: Build Your Indicator
# Your task: Create a custom indicator
# 1. Combine RSI and MACD into a single signal
# 2. Buy when: RSI < 30 AND MACD crosses above signal
# 3. Sell when: RSI > 70 OR MACD crosses below signal
# 4. Backtest on a stock of your choice
# 5. Compare to buy-and-hold
# 6. Calculate win rate and profit factor
Exercise 2: Multi-Timeframe Analysis
# Your task: Analyze trends across timeframes
# 1. Download 5 years of data
# 2. Calculate 50/200 MA on daily, weekly, and monthly charts
# 3. Identify when all timeframes align (bullish or bearish)
# 4. Visualize the multi-timeframe analysis
# 5. Determine current market regime
Exercise 3: Volatility Breakout Strategy
# Your task: Build a Bollinger Band breakout strategy
# 1. Buy when price breaks above upper band
# 2. Sell when price crosses below middle band
# 3. Backtest over 3 years
# 4. Calculate maximum drawdown
# 5. Optimize the BB parameters (period and std deviation)
# 6. Compare to other strategies
Exercise 4: Volume-Price Divergence Detector
# Your task: Find divergences between price and volume
# 1. Calculate OBV and price trends
# 2. Detect bullish divergence (price down, OBV up)
# 3. Detect bearish divergence (price up, OBV down)
# 4. Mark all divergences on a chart
# 5. Analyze if divergences predict reversals
# 6. Calculate the success rate of divergence signals
Module 7 Summary
Congratulations! You've mastered technical analysis with Python.
What You've Accomplished
Indicator Knowledge
- Calculating and interpreting moving averages (SMA, EMA, WMA)
- Building momentum indicators (RSI, MACD, Stochastic)
- Implementing volatility measures (Bollinger Bands, ATR)
- Creating volume indicators (OBV, Volume MA)
Technical Skills
- Writing indicator calculations from scratch
- Understanding the mathematics behind each indicator
- Combining multiple indicators for better signals
- Visualizing technical analysis professionally
Backtesting Ability
- Building simple trading strategies
- Testing strategies on historical data
- Calculating performance metrics
- Comparing strategy returns to benchmarks
Practical Applications
- Creating comprehensive technical dashboards
- Identifying overbought/oversold conditions
- Detecting trend changes and reversals
- Recognizing divergences between indicators
Real-World Capabilities
You can now:
- Perform professional technical analysis on any stock
- Build and test trading strategies
- Create indicator-based alerts and signals
- Combine fundamental and technical analysis
- Make more informed trading decisions
Critical Perspective
Remember: Technical analysis is a tool, not a crystal ball. Markets are largely efficient, and past patterns don't guarantee future results. Use technical analysis:
- As confirmation alongside fundamental analysis
- For timing entry and exit points
- To understand market sentiment
- With proper risk management
Never rely solely on technical indicators for investment decisions.
What's Next
In Module 8, we'll dive into financial modeling—building discounted cash flow models, valuing companies, and creating forecasts. You'll combine the data skills from earlier modules with financial theory to build complete valuation models.
Before Moving Forward
Ensure you're comfortable with:
- Calculating key technical indicators
- Interpreting indicator signals
- Building basic backtests
- Visualizing technical analysis
- Understanding indicator limitations
Practice Recommendations
- Daily Analysis: Review technical indicators on different stocks daily
- Paper Trading: Test strategies without real money
- Keep a Journal: Document what works and what doesn't
- Study Failures: Learn from false signals
- Combine Tools: Use technical analysis with other forms of analysis
The Balance
Technical analysis provides valuable insights into market psychology and timing. Combined with fundamental analysis and portfolio theory, it becomes part of a comprehensive analytical toolkit. You now have that toolkit.
Continue to Module 8: Financial Modeling Fundamentals →

