Second Breakfast
Home
(current)
About
(current)
Github
Technical Indicators for Equity Trading Using Python
July 31, 2020, 3:13 p.m.
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 Technicain designation](https://cmtassociation.org/chartered-market-technician/ "Chartered Market Technicain designation"). While I never quite finished the requirements to earn the designation, I did learn the material through-&-through, and have recently found that much of it is still be applicable to my investing/trading. What exactly is Technical Analysis? According to [Investopedia](https://www.investopedia.com/terms/t/technicalanalysis.asp "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 evalute trends and momentum are generally specific calucations 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 result in a powerful combination of trend following and momentum. MACD fluctuates above and below a zero line, which is know as the centerline, as the moving averages converge, cross, & diverge. The shorter moving average (typically 12-day) reacts quicker and responsible for most MACD movements. The longer moving average (typically 26-day) is slower and less reactive to price changes. Traders generally look for signalline 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:** ```python # 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 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 orient 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:** ```python import pandas as pd import pandas_datareader.data as pdr import numpy as np import datetime # Create list of S&P 500 tickers 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:** ```python # 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 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:** ```python # 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 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](https://school.stockcharts.com/doku.php "ChartSchool"). >**DISCLAIMER:** To be brutally honest, I do not know if these indicators or strategies can consistantly 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.
Word Count: 1460
Please enable JavaScript to view the
comments powered by Disqus.