Technical Indicators for Equity Trading Using Python
Over the course of the past months, I've revisited the use of Technical Analysis.
In my early 20's I learned a significant amount about these techniques as I pursued my Chartered Market Technician designation.
While I never quite finished the requirements to earn the designation, I did learn the material through-and-through, and have recently found that much of it is still applicable to my investing/trading.
What exactly is Technical Analysis?
According to Investopedia, Technical Analysis is:
A trading discipline employed to evaluate investments and identify trading opportunities by analyzing statistical trends gathered from trading activity, such as price movement and volume. Unlike fundamental analysis, which attempts to evaluate a security's value based on business results such as sales and earnings, technical analysis focuses on the study of price and volume.
A primary component of Technical Analysis is the use of Technical Indicators.
These indicators are signals produced from the patterns observed in the movements of stock prices (high, low, open, close) and volumes.
These signals are used to predict the future price movements.
Traders who employ technical analysis look for indicators in the historical data and use them to look for buy/sell opportunities.
Technical analysts differ from fundamental analysts who evaluate a stock's performance by looking at the fundamentals and the intrinsic value.
These technical indicators that evaluate trends and momentum are generally specific calculations that can be codified for automation.
Below are a few examples of the technical indicators:
MACD
The Moving Average Convergence/Divergence oscillator (MACD) is one of the simplest and most effective momentum indicators.
MACD turns two trend-following indicators, moving averages, into a momentum oscillator by subtracting the longer moving average from the shorter one.
This results in a powerful combination of trend following and momentum.
MACD fluctuates above and below a zero line, which is known as the centerline, as the moving averages converge, cross, and diverge.
The shorter moving average (typically 12-day) reacts quicker and is responsible for most MACD movements.
The longer moving average (typically 26-day) is slower and less reactive to price changes.
Traders generally look for signal line crossovers, centerline crossovers and divergences to generate buy/sell signals.
As a cautionary note, MACD is not particularly useful for identifying overbought and oversold levels, as it is unbounded and can therefore reach extreme levels before decisive price action takes place.
Below is the Python implementation of MACD:
# Import necessary libraries
import pandas as pd
import pandas_datareader.data as pdr
import datetime
import matplotlib.pyplot as plt
# Create list of S&P 500 tickers
sp500_wiki = pd.read_html('https://en.wikipedia.org/wiki/List_of_S%26P_500_companies')
tickers = sp500_wiki[0]['Symbol'].tolist()
# Download historical data for tickers
ohlcv = pdr.get_data_yahoo(ticker, datetime.date.today() - datetime.timedelta(1825), datetime.date.today())
# Calculate MACD
def MACD(DF, a, b, c):
"""function to calculate MACD
typical values a = 12; b = 26, c = 9"""
df = DF.copy()
df["MA_Fast"] = df["Adj Close"].ewm(span=a, min_periods=a).mean()
df["MA_Slow"] = df["Adj Close"].ewm(span=b, min_periods=b).mean()
df["MACD"] = df["MA_Fast"] - df["MA_Slow"]
df["Signal"] = df["MACD"].ewm(span=c, min_periods=c).mean()
df.dropna(inplace=True)
return df
# Visualization - plotting MACD/signal along with close price and volume for last 100 data points
df = MACD(ohlcv, 12, 26, 9)
plt.subplot(311)
plt.plot(df.iloc[-100:, 4])
plt.title('Stock Price')
plt.xticks([])
plt.subplot(312)
plt.bar(df.iloc[-100:, 5].index, df.iloc[-100:, 5].values)
plt.title('Volume')
plt.xticks([])
plt.subplot(313)
plt.plot(df.iloc[-100:, [-2, -1]])
plt.title('MACD')
plt.legend(('MACD', 'Signal'), loc='lower right')
plt.show()
# Visualization - Using object oriented approach
# Get the figure and the axes
fig, (ax0, ax1) = plt.subplots(nrows=2, ncols=1, sharex=True, sharey=False,
figsize=(10, 6), gridspec_kw={'height_ratios': [2.5, 1]})
df.iloc[-100:, 4].plot(ax=ax0)
ax0.set(ylabel='Adj Close')
df.iloc[-100:, [-2, -1]].plot(ax=ax1)
ax1.set(xlabel='Date', ylabel='MACD/Signal')
# Title the figure
fig.suptitle('Stock Price with MACD', fontsize=14, fontweight='bold')
RSI
The Relative Strength Index (RSI) is a momentum oscillator that measures the speed and change of price movements.
RSI oscillates between zero and 100, with a default calculation timeframe based on 14 trading periods.
RSI is considered overbought when above 70 and oversold when below 30.
RSI can be used to identify the general trend, and signals can typically be generated by looking for divergences, failure swings and centerline crossovers.
Below is the Python implementation of RSI:
import pandas as pd
import pandas_datareader.data as pdr
import numpy as np
import datetime
# Create list of S&P 500 tickers
sp500_wiki = pd.read_html('https://en.wikipedia.org/wiki/List_of_S%26P_500_companies')
tickers = sp500_wiki[0]['Symbol'].tolist()
# Download historical data for required stocks
ohlcv = pdr.get_data_yahoo(ticker, datetime.date.today() - datetime.timedelta(364), datetime.date.today())
# Calculate RSI with loop
def RSI(DF, n):
"""function to calculate RSI"""
df = DF.copy()
df['delta'] = df['Adj Close'] - df['Adj Close'].shift(1)
df['gain'] = np.where(df['delta'] >= 0, df['delta'], 0)
df['loss'] = np.where(df['delta'] < 0, abs(df['delta']), 0)
avg_gain = []
avg_loss = []
gain = df['gain'].tolist()
loss = df['loss'].tolist()
for i in range(len(df)):
if i < n:
avg_gain.append(np.NaN)
avg_loss.append(np.NaN)
elif i == n:
avg_gain.append(df['gain'].rolling(n).mean().tolist()[n])
avg_loss.append(df['loss'].rolling(n).mean().tolist()[n])
elif i > n:
avg_gain.append(((n - 1) * avg_gain[i - 1] + gain[i]) / n)
avg_loss.append(((n - 1) * avg_loss[i - 1] + loss[i]) / n)
df['avg_gain'] = np.array(avg_gain)
df['avg_loss'] = np.array(avg_loss)
df['RS'] = df['avg_gain'] / df['avg_loss']
df['RSI'] = 100 - (100 / (1 + df['RS']))
return df['RSI']
# Calculating RSI without loop
def rsi(df, n):
"""function to calculate RSI"""
delta = df["Adj Close"].diff().dropna()
u = delta * 0
d = u.copy()
u[delta > 0] = delta[delta > 0]
d[delta < 0] = -delta[delta < 0]
u[u.index[n - 1]] = np.mean(u[:n]) # first value is sum of avg gains
u = u.drop(u.index[:(n - 1)])
d[d.index[n - 1]] = np.mean(d[:n]) # first value is sum of avg losses
d = d.drop(d.index[:(n - 1)])
rs = pd.stats.moments.ewma(u, com=n - 1, adjust=False) / \
pd.stats.moments.ewma(d, com=n - 1, adjust=False)
return 100 - 100 / (1 + rs)
OBV
Based on the theory that volume precedes price, On Balance Volume (OBV) measures buying and selling pressure as a cumulative indicator, adding volume on up days and subtracting it on down days.
The OBV line is a running total of positive and negative volume.
A trading period's volume is positive when the close is above the prior close and is negative when the close is below the prior close.
These simple calculations translate into simple buy/sell signals.
OBV rises when volume on up days outpaces volume on down days.
OBV falls when volume on down days is stronger.
A rising OBV reflects positive volume pressure that can lead to higher prices.
Conversely, falling OBV reflects negative volume pressure that can foreshadow lower prices.
Below is the Python implementation of OBV:
# Import necessary libraries
import requests
from bs4 import BeautifulSoup
import pandas as pd
import pandas_datareader.data as pdr
import numpy as np
import datetime
# Create list of S&P 500 tickers
sp500_wiki = pd.read_html('https://en.wikipedia.org/wiki/List_of_S%26P_500_companies')
tickers = sp500_wiki[0]['Symbol'].tolist()
# Download historical data for required stocks
ohlcv = pdr.get_data_yahoo(tickers, datetime.date.today() - datetime.timedelta(364), datetime.date.today())
# Calculate OBV
def OBV(DF):
"""function to calculate On Balance Volume"""
df = DF.copy()
df['daily_ret'] = df['Adj Close'].pct_change()
df['direction'] = np.where(df['daily_ret'] >= 0, 1, -1)
df['direction'][0] = 0
df['vol_adj'] = df['Volume'] * df['direction']
df['obv'] = df['vol_adj'].cumsum()
return df['obv']
ADX
The Average Directional Index (ADX), Minus Directional Indicator (-DI) and Plus Directional Indicator (+DI) represent a group of directional movement indicators that form a unique trading system.
Using these three indicators together, traders can determine both the direction and strength of the trend.
The Plus Directional Indicator (+DI) and Minus Directional Indicator (-DI) are derived from smoothed averages of these differences and measure trend direction over time.
These two indicators are often collectively referred to as the Directional Movement Indicator (DMI).
The Average Directional Index (ADX) is derived from the smoothed averages of the difference between +DI and -DI, and it measures the strength of the trend (regardless of direction) over time.
Below is the Python implementation of ADX:
# Import necessary libraries
import pandas as pd
import pandas_datareader.data as pdr
import numpy as np
import datetime
# Create list of S&P 500 tickers
sp500_wiki = pd.read_html('https://en.wikipedia.org/wiki/List_of_S%26P_500_companies')
tickers = sp500_wiki[0]['Symbol'].tolist()
# Download historical data for required stocks
ohlcv = pdr.get_data_yahoo(ticker, datetime.date.today() - datetime.timedelta(364), datetime.date.today())
# Calculate True Range and Average True Range
def ATR(DF, n):
"""function to calculate True Range and Average True Range"""
df = DF.copy()
df['H-L'] = abs(df['High'] - df['Low'])
df['H-PC'] = abs(df['High'] - df['Adj Close'].shift(1))
df['L-PC'] = abs(df['Low'] - df['Adj Close'].shift(1))
df['TR'] = df[['H-L', 'H-PC', 'L-PC']].max(axis=1, skipna=False)
df['ATR'] = df['TR'].rolling(n).mean()
# df['ATR'] = df['TR'].ewm(span=n, adjust=False, min_periods=n).mean()
df2 = df.drop(['H-L', 'H-PC', 'L-PC'], axis=1)
return df2
# Calculate ADX
def ADX(DF, n):
"""function to calculate ADX"""
df2 = DF.copy()
df2['TR'] = ATR(df2, n)['TR'] # the period parameter of ATR function does not matter because period does not influence TR calculation
df2['DMplus'] = np.where((df2['High'] - df2['High'].shift(1)) > (df2['Low'].shift(1) - df2['Low']),
df2['High'] - df2['High'].shift(1), 0)
df2['DMplus'] = np.where(df2['DMplus'] < 0, 0, df2['DMplus'])
df2['DMminus'] = np.where((df2['Low'].shift(1) - df2['Low']) > (df2['High'] - df2['High'].shift(1)),
df2['Low'].shift(1) - df2['Low'], 0)
df2['DMminus'] = np.where(df2['DMminus'] < 0, 0, df2['DMminus'])
TRn = []
DMplusN = []
DMminusN = []
TR = df2['TR'].tolist()
DMplus = df2['DMplus'].tolist()
DMminus = df2['DMminus'].tolist()
for i in range(len(df2)):
if i < n:
TRn.append(np.NaN)
DMplusN.append(np.NaN)
DMminusN.append(np.NaN)
elif i == n:
TRn.append(df2['TR'].rolling(n).sum().tolist()[n])
DMplusN.append(df2['DMplus'].rolling(n).sum().tolist()[n])
DMminusN.append(df2['DMminus'].rolling(n).sum().tolist()[n])
elif i > n:
TRn.append(TRn[i - 1] - (TRn[i - 1] / n) + TR[i])
DMplusN.append(DMplusN[i - 1] - (DMplusN[i - 1] / n) + DMplus[i])
DMminusN.append(DMminusN[i - 1] - (DMminusN[i - 1] / n) + DMminus[i])
df2['TRn'] = np.array(TRn)
df2['DMplusN'] = np.array(DMplusN)
df2['DMminusN'] = np.array(DMminusN)
df2['DIplusN'] = 100 * (df2['DMplusN'] / df2['TRn'])
df2['DIminusN'] = 100 * (df2['DMminusN'] / df2['TRn'])
df2['DIdiff'] = abs(df2['DIplusN'] - df2['DIminusN'])
df2['DIsum'] = df2['DIplusN'] + df2['DIminusN']
df2['DX'] = 100 * (df2['DIdiff'] / df2['DIsum'])
ADX = []
DX = df2['DX'].tolist()
for j in range(len(df2)):
if j < 2 * n - 1:
ADX.append(np.NaN)
elif j == 2 * n - 1:
ADX.append(df2['DX'][j - n + 1:j + 1].mean())
elif j > 2 * n - 1:
ADX.append(((n - 1) * ADX[j - 1] + DX[j]) / n)
df2['ADX'] = np.array(ADX)
return df2['ADX']
If you'd like to learn more, I highly recommend ChartSchool.
DISCLAIMER: To be brutally honest, I do not know if these indicators or strategies can consistently generate abnormal risk-adjusted rates of return. I am simply sharing these ideas so others can contest or explore their potential usefulness. The information in this post does not constitute investment advice. I will not accept liability for any loss or damage, including without limitation any loss of profit, which may arise directly or indirectly from use of or reliance on such information.
Discussion
Comments are powered by GitHub Issues. Join the conversation by opening an issue.
⊹Add Comment via GitHub