This is a programming lesson. So there will be very less amount of explanation and more code. If you’re stuck somewhere, feel free to comment.
Technical indicators are heuristic or mathematical calculations that rely on a security’s price, volume, or open interest, often employed by traders who adhere to technical analysis principles.
These indicators serve as tools for predicting future price movements by scrutinizing historical data. Some widely used technical indicators encompass the Relative Strength Index, Money Flow Index, Stochastics, MACD, and Bollinger Bands®.
Technical analysis is a trading discipline that involves the evaluation of investments and the identification of trading opportunities by analyzing statistical trends derived from trading activities, such as price movements and trading volumes. In contrast to fundamental analysts, who assess a security’s intrinsic value based on financial and economic data, technical analysts concentrate on patterns in price movements, trading signals, and a variety of analytical charting tools to gauge a security’s strength or weakness.
This approach to analysis can be applied to any security with historical trading data, encompassing stocks, futures, commodities, fixed-income instruments, currencies, and other financial assets. While we primarily focus on stocks in our examples in this tutorial, it’s essential to recognize that these concepts are universally applicable. In fact, technical analysis finds more extensive use in commodities and forex markets, where traders are primarily concerned with short-term price fluctuations.
Technical indicators, often referred to as “technicals,” center their analysis on historical trading data, including price, volume, and open interest, rather than delving into a company’s fundamentals like earnings, revenue, or profit margins. These indicators are particularly popular among active traders because they are tailored to analyze short-term price movements. However, even long-term investors can utilize technical indicators to pinpoint optimal entry and exit points.
There are two fundamental categories of technical indicators:
Overlays: These technical indicators use the same scale as prices and are plotted directly over the price chart. Notable examples include moving averages and Bollinger Bands®.
Oscillators: Oscillators are technical indicators that fluctuate between a local minimum and maximum and are typically plotted either above or below a price chart. Prominent examples encompass the stochastic oscillator, MACD (Moving Average Convergence Divergence), and RSI (Relative Strength Index).
Note – The structure of historical data and live data from Zerodha is identical. During the development and testing of functions, it’s not feasible to wait for days or months to validate their performance using live data.
To address this, we’ll initially test and validate our functions using historical data. Once we have confirmed that the functions generate accurate signals and execute trades correctly with historical data, we can seamlessly transition to using live data for real-time trading.
				
					'''
    # Date must be present as a Pandas DataFrame with ['date', 'open', 'high', 'low', 'close', 'volume'] as columns
    df = pd.DataFrame(data["data"]["candles"], columns=['date', 'open', 'high', 'low', 'close', 'volume']) 
'''
df=pd.DataFrame(kite.historical_data(779521,"2019-04-14 15:00:00","2019-08-16 09:16:00","day",0))[["date","open","high","low","close","volume"]]
display(df.tail(10)) 
				
			
		Output –
				
					date	open	high	low	close	volume
73	2019-08-01 00:00:00+05:30	330.80	331.50	311.35	317.15	40853304
74	2019-08-02 00:00:00+05:30	315.55	322.25	307.05	308.45	64472348
75	2019-08-05 00:00:00+05:30	298.45	302.85	291.70	300.25	48815899
76	2019-08-06 00:00:00+05:30	298.80	304.25	297.25	301.40	30442970
77	2019-08-07 00:00:00+05:30	302.40	302.45	288.80	289.90	30820114
78	2019-08-08 00:00:00+05:30	290.00	295.50	285.60	294.35	32929233
79	2019-08-09 00:00:00+05:30	296.30	298.00	290.05	291.35	23377581
80	2019-08-13 00:00:00+05:30	290.90	291.55	282.50	283.35	24231272
81	2019-08-14 00:00:00+05:30	285.05	291.25	284.50	289.75	18523649
82	2019-08-16 00:00:00+05:30	287.95	292.80	284.30	290.90	20047399 
				
			
		
				
					plt.style.use('seaborn')
df['high'].plot()
df['low'].plot()
df['close'].plot()
df['open'].plot()
plt.show()
print ("  \t'---- ABOVE GRAPH OF OHLC Historical data ----- ' \n \n ") 
				
			
		Output –

				
					"""
Function to compute Average True Range (ATR)
Args :
    df : Pandas DataFrame which contains ['date', 'open', 'high', 'low', 'close', 'volume'] columns
    period : Integer indicates the period of computation in terms of number of candles
    ohlc: List defining OHLC Column names (default ['Open', 'High', 'Low', 'Close'])
Returns :
    df : Pandas DataFrame with new columns added for 
        True Range (TR)
        ATR (ATR_$period)
"""
def atr(df,period):
    df['hl']=abs(df['high']-df['low'])
    df['hpc']=abs(df['high']-df['close'].shift())
    df['lpc']=abs(df['low']-df['close'].shift())
    df['tr']=df[['hl','hpc','lpc']].max(axis=1)
    df['ATR']=pd.DataFrame.ewm(df["tr"], span=period,min_periods=period).mean()
    df.drop(["hl","hpc","lpc","tr"],axis = 1 , inplace =True)
atr(df,14)
df.tail() 
				
			
		Output –
				
					date	open	high	low	close	volume	ATR
78	2019-08-08 00:00:00+05:30	290.00	295.50	285.60	294.35	32929233	11.851230
79	2019-08-09 00:00:00+05:30	296.30	298.00	290.05	291.35	23377581	11.331060
80	2019-08-13 00:00:00+05:30	290.90	291.55	282.50	283.35	24231272	11.026916
81	2019-08-14 00:00:00+05:30	285.05	291.25	284.50	289.75	18523649	10.609991
82	2019-08-16 00:00:00+05:30	287.95	292.80	284.30	290.90	20047399	10.328657 
				
			
		
				
					"""
    Function to compute Relative Strength Index (RSI)
    
    Args :
        df : Pandas DataFrame which contains ['date', 'open', 'high', 'low', 'close', 'volume'] columns
        period : Integer indicates the period of computation in terms of number of candles
        
    Returns :
        df : Pandas DataFrame with new columns added for 
            Relative Strength Index (RSI_$period)
    """
rsi_period=14
chg=df["close"].diff(1)
gain=chg.mask(chg<0,0) 
loss=chg.mask(chg>0,0)
avg_gain=gain.ewm(com=rsi_period-1,min_periods=rsi_period).mean()
avg_loss=loss.ewm(com=rsi_period-1,min_periods=rsi_period).mean()
rs =abs(avg_gain / avg_loss)
rsi =100 -(100/(1+rs))
df['rsi']=rsi
display(df.tail()) 
				
			
		Output –
				
					
date	open	high	low	close	volume	ATR	rsi
78	2019-08-08 00:00:00+05:30	290.00	295.50	285.60	294.35	32929233	11.851230	26.529639
79	2019-08-09 00:00:00+05:30	296.30	298.00	290.05	291.35	23377581	11.331060	25.567214
80	2019-08-13 00:00:00+05:30	290.90	291.55	282.50	283.35	24231272	11.026916	23.154909
81	2019-08-14 00:00:00+05:30	285.05	291.25	284.50	289.75	18523649	10.609991	28.931857
82	2019-08-16 00:00:00+05:30	287.95	292.80	284.30	290.90	20047399	10.328657	29.950888 
				
			
		
				
					"""
    Function to compute Simple Moving Average (SMA)
    
    Args :
        df : Pandas DataFrame which contains ['date', 'open', 'high', 'low', 'close', 'volume'] columns
        base : String indicating the column name from which the SMA needs to be computed from
        target : String indicates the column name to which the computed data needs to be stored
        period : Integer indicates the period of computation in terms of number of candles
        
    Returns :
        df : Pandas DataFrame with new column added with name 'target'
    """
def SMA(df, column="close", period=14):
    smavg = df.close.rolling(14).mean()
    return df.join(smavg.to_frame('SMA'))
a=SMA(df)
a.tail() 
				
			
		Output –
				
					
date	open	high	low	close	volume	ATR	rsi	SMA
78	2019-08-08 00:00:00+05:30	290.00	295.50	285.60	294.35	32929233	11.851230	26.529639	323.685714
79	2019-08-09 00:00:00+05:30	296.30	298.00	290.05	291.35	23377581	11.331060	25.567214	319.435714
80	2019-08-13 00:00:00+05:30	290.90	291.55	282.50	283.35	24231272	11.026916	23.154909	315.232143
81	2019-08-14 00:00:00+05:30	285.05	291.25	284.50	289.75	18523649	10.609991	28.931857	311.671429
82	2019-08-16 00:00:00+05:30	287.95	292.80	284.30	290.90	20047399	10.328657	29.950888	308.071429 
				
			
		
				
					"""
    Function to compute Exponential Moving Average (EMA)
    Args :
        df : Pandas DataFrame which contains ['date', 'open', 'high', 'low', 'close', 'volume'] columns
        base : String indicating the column name from which the EMA needs to be computed from
        target : String indicates the column name to which the computed data needs to be stored
        period : Integer indicates the period of computation in terms of number of candles
        alpha : Boolean if True indicates to use the formula for computing EMA using alpha (default is False)
    Returns :
        df : Pandas DataFrame with new column added with name 'target'
    """
def EMA(df, column="close", period=14):
    ema = df[column].ewm(span=period, min_periods=period - 1).mean()
    return df.join(ema.to_frame('EMA'))
pp= ( EMA(df)["EMA"].shift(1)*13 + df['close'] ) /  14
pp.tail() 
				
			
		Output –
				
					78    321.111774
79    317.329208
80    313.293848
81    309.758441
82    307.172770
dtype: float64 
				
			
		
				
					def BollingerBand(df, column="close", period=20):
    sma = df[column].rolling(window=period, min_periods=period - 1).mean()
    std = df[column].rolling(window=period, min_periods=period - 1).std()
    up = (sma + (std * 2)).to_frame('BBANDUP')
    lower = (sma - (std * 2)).to_frame('BBANDLO')
    return df.join(up).join(lower)
c=BollingerBand(df)
c.tail() 
				
			
		Output –
				
						date	open	high	low	close	volume	ATR	rsi	BBANDUP	BBANDLO
78	2019-08-08 00:00:00+05:30	290.00	295.50	285.60	294.35	32929233	11.851230	26.529639	387.024069	284.140931
79	2019-08-09 00:00:00+05:30	296.30	298.00	290.05	291.35	23377581	11.331060	25.567214	385.242185	278.697815
80	2019-08-13 00:00:00+05:30	290.90	291.55	282.50	283.35	24231272	11.026916	23.154909	383.881457	272.388543
81	2019-08-14 00:00:00+05:30	285.05	291.25	284.50	289.75	18523649	10.609991	28.931857	379.931275	268.878725
82	2019-08-16 00:00:00+05:30	287.95	292.80	284.30	290.90	20047399	10.328657	29.950888	372.909758	267.750242 
				
			
		
				
					
def HA(df, ohlc=['Open', 'High', 'Low', 'Close']):
    """
    Function to compute Heiken Ashi Candles (HA)
    Args :
        df : Pandas DataFrame which contains ['date', 'open', 'high', 'low', 'close', 'volume'] columns
        ohlc: List defining OHLC Column names (default ['Open', 'High', 'Low', 'Close'])
    Returns :
        df : Pandas DataFrame with new columns added for 
            Heiken Ashi Close (HA_$ohlc[3])
            Heiken Ashi Open (HA_$ohlc[0])
            Heiken Ashi High (HA_$ohlc[1])
            Heiken Ashi Low (HA_$ohlc[2])
    """
    ha_open = 'HA_' + ohlc[0]
    ha_high = 'HA_' + ohlc[1]
    ha_low = 'HA_' + ohlc[2]
    ha_close = 'HA_' + ohlc[3]
    df[ha_close] = (df[ohlc[0]] + df[ohlc[1]] + df[ohlc[2]] + df[ohlc[3]]) / 4
    df[ha_open] = 0.00
    for i in range(0, len(df)):
        if i == 0:
            df[ha_open].iat[i] = (df[ohlc[0]].iat[i] + df[ohlc[3]].iat[i]) / 2
        else:
            df[ha_open].iat[i] = (df[ha_open].iat[i - 1] + df[ha_close].iat[i - 1]) / 2
    df[ha_high]=df[[ha_open, ha_close, ohlc[1]]].max(axis=1)
    df[ha_low]=df[[ha_open, ha_close, ohlc[2]]].min(axis=1)
    return df 
				
			
		
				
					def STDDEV(df, base, target, period):
    """
    Function to compute Standard Deviation (STDDEV)
    Args :
        df : Pandas DataFrame which contains ['date', 'open', 'high', 'low', 'close', 'volume'] columns
        base : String indicating the column name from which the SMA needs to be computed from
        target : String indicates the column name to which the computed data needs to be stored
        period : Integer indicates the period of computation in terms of number of candles
    Returns :
        df : Pandas DataFrame with new column added with name 'target'
    """
    df[target] = df[base].rolling(window=period).std()
    df[target].fillna(0, inplace=True)
    return df 
				
			
		
				
					def SuperTrend(df, period, multiplier, ohlc=['Open', 'High', 'Low', 'Close']):
    """
    Function to compute SuperTrend
    Args :
        df : Pandas DataFrame which contains ['date', 'open', 'high', 'low', 'close', 'volume'] columns
        period : Integer indicates the period of computation in terms of number of candles
        multiplier : Integer indicates value to multiply the ATR
        ohlc: List defining OHLC Column names (default ['Open', 'High', 'Low', 'Close'])
    Returns :
        df : Pandas DataFrame with new columns added for 
            True Range (TR), ATR (ATR_$period)
            SuperTrend (ST_$period_$multiplier)
            SuperTrend Direction (STX_$period_$multiplier)
    """
    ATR(df, period, ohlc=ohlc)
    atr = 'ATR_' + str(period)
    st = 'ST_' + str(period) + '_' + str(multiplier)
    stx = 'STX_' + str(period) + '_' + str(multiplier)
    """
    SuperTrend Algorithm :
        BASIC UPPERBAND = (HIGH + LOW) / 2 + Multiplier * ATR
        BASIC LOWERBAND = (HIGH + LOW) / 2 - Multiplier * ATR
        FINAL UPPERBAND = IF( (Current BASICUPPERBAND < Previous FINAL UPPERBAND) or (Previous Close > Previous FINAL UPPERBAND))
                            THEN (Current BASIC UPPERBAND) ELSE Previous FINALUPPERBAND)
        FINAL LOWERBAND = IF( (Current BASIC LOWERBAND > Previous FINAL LOWERBAND) or (Previous Close < Previous FINAL LOWERBAND)) 
                            THEN (Current BASIC LOWERBAND) ELSE Previous FINAL LOWERBAND)
        SUPERTREND = IF((Previous SUPERTREND = Previous FINAL UPPERBAND) and (Current Close <= Current FINAL UPPERBAND)) THEN
                        Current FINAL UPPERBAND
                    ELSE
                        IF((Previous SUPERTREND = Previous FINAL UPPERBAND) and (Current Close > Current FINAL UPPERBAND)) THEN
                            Current FINAL LOWERBAND
                        ELSE
                            IF((Previous SUPERTREND = Previous FINAL LOWERBAND) and (Current Close >= Current FINAL LOWERBAND)) THEN
                                Current FINAL LOWERBAND
                            ELSE
                                IF((Previous SUPERTREND = Previous FINAL LOWERBAND) and (Current Close < Current FINAL LOWERBAND)) THEN
                                    Current FINAL UPPERBAND
    """
    # Compute basic upper and lower bands
    df['basic_ub'] = (df[ohlc[1]] + df[ohlc[2]]) / 2 + multiplier * df[atr]
    df['basic_lb'] = (df[ohlc[1]] + df[ohlc[2]]) / 2 - multiplier * df[atr]
    # Compute final upper and lower bands
    df['final_ub'] = 0.00
    df['final_lb'] = 0.00
    for i in range(period, len(df)):
        df['final_ub'].iat[i] = df['basic_ub'].iat[i] if df['basic_ub'].iat[i] < df['final_ub'].iat[i - 1] or df[ohlc[3]].iat[i - 1] > df['final_ub'].iat[i - 1] else df['final_ub'].iat[i - 1]
        df['final_lb'].iat[i] = df['basic_lb'].iat[i] if df['basic_lb'].iat[i] > df['final_lb'].iat[i - 1] or df[ohlc[3]].iat[i - 1] < df['final_lb'].iat[i - 1] else df['final_lb'].iat[i - 1]
    # Set the Supertrend value
    df[st] = 0.00
    for i in range(period, len(df)):
        df[st].iat[i] = df['final_ub'].iat[i] if df[st].iat[i - 1] == df['final_ub'].iat[i - 1] and df[ohlc[3]].iat[i] <= df['final_ub'].iat[i] else \
                        df['final_lb'].iat[i] if df[st].iat[i - 1] == df['final_ub'].iat[i - 1] and df[ohlc[3]].iat[i] >  df['final_ub'].iat[i] else \
                        df['final_lb'].iat[i] if df[st].iat[i - 1] == df['final_lb'].iat[i - 1] and df[ohlc[3]].iat[i] >= df['final_lb'].iat[i] else \
                        df['final_ub'].iat[i] if df[st].iat[i - 1] == df['final_lb'].iat[i - 1] and df[ohlc[3]].iat[i] <  df['final_lb'].iat[i] else 0.00     
    # Mark the trend direction up/down
    df[stx] = np.where((df[st] > 0.00), np.where((df[ohlc[3]] < df[st]), 'down',  'up'), np.NaN)
    # Remove basic and final bands from the columns
    df.drop(['basic_ub', 'basic_lb', 'final_ub', 'final_lb'], inplace=True, axis=1)
    df.fillna(0, inplace=True)
    return df 
				
			
		
				
					def MACD(df, fastEMA=12, slowEMA=26, signal=9, base='Close'):
    """
    Function to compute Moving Average Convergence Divergence (MACD)
    Args :
        df : Pandas DataFrame which contains ['date', 'open', 'high', 'low', 'close', 'volume'] columns
        fastEMA : Integer indicates faster EMA
        slowEMA : Integer indicates slower EMA
        signal : Integer indicates the signal generator for MACD
        base : String indicating the column name from which the MACD needs to be computed from (Default Close)
    Returns :
        df : Pandas DataFrame with new columns added for 
            Fast EMA (ema_$fastEMA)
            Slow EMA (ema_$slowEMA)
            MACD (macd_$fastEMA_$slowEMA_$signal)
            MACD Signal (signal_$fastEMA_$slowEMA_$signal)
            MACD Histogram (MACD (hist_$fastEMA_$slowEMA_$signal)) 
    """
    fE = "ema_" + str(fastEMA)
    sE = "ema_" + str(slowEMA)
    macd = "macd_" + str(fastEMA) + "_" + str(slowEMA) + "_" + str(signal)
    sig = "signal_" + str(fastEMA) + "_" + str(slowEMA) + "_" + str(signal)
    hist = "hist_" + str(fastEMA) + "_" + str(slowEMA) + "_" + str(signal)
    # Compute fast and slow EMA    
    EMA(df, base, fE, fastEMA)
    EMA(df, base, sE, slowEMA)
    # Compute MACD
    df[macd] = np.where(np.logical_and(np.logical_not(df[fE] == 0), np.logical_not(df[sE] == 0)), df[fE] - df[sE], 0)
    # Compute MACD Signal
    EMA(df, macd, sig, signal)
    # Compute MACD Histogram
    df[hist] = np.where(np.logical_and(np.logical_not(df[macd] == 0), np.logical_not(df[sig] == 0)), df[macd] - df[sig], 0)
    return df 
				
			
		
				
					def Ichimoku(df, ohlc=['Open', 'High', 'Low', 'Close'], param=[9, 26, 52, 26]):
    """
    Function to compute Ichimoku Cloud parameter (Ichimoku)    
    Args :
        df : Pandas DataFrame which contains ['date', 'open', 'high', 'low', 'close', 'volume'] columns
        ohlc: List defining OHLC Column names (default ['Open', 'High', 'Low', 'Close'])
        param: Periods to be used in computation (default [tenkan_sen_period, kijun_sen_period, senkou_span_period, chikou_span_period] = [9, 26, 52, 26])
    Returns :
        df : Pandas DataFrame with new columns added for ['Tenkan Sen', 'Kijun Sen', 'Senkou Span A', 'Senkou Span B', 'Chikou Span']
    """
    high = df[ohlc[1]]
    low = df[ohlc[2]]
    close = df[ohlc[3]]
    tenkan_sen_period = param[0]
    kijun_sen_period = param[1]
    senkou_span_period = param[2]
    chikou_span_period = param[3]
    tenkan_sen_column = 'Tenkan Sen'
    kijun_sen_column = 'Kijun Sen'
    senkou_span_a_column = 'Senkou Span A'
    senkou_span_b_column = 'Senkou Span B'
    chikou_span_column = 'Chikou Span'
    # Tenkan-sen (Conversion Line)
    tenkan_sen_high = high.rolling(window=tenkan_sen_period).max()
    tenkan_sen_low = low.rolling(window=tenkan_sen_period).min()
    df[tenkan_sen_column] = (tenkan_sen_high + tenkan_sen_low) / 2
    # Kijun-sen (Base Line)
    kijun_sen_high = high.rolling(window=kijun_sen_period).max()
    kijun_sen_low = low.rolling(window=kijun_sen_period).min()
    df[kijun_sen_column] = (kijun_sen_high + kijun_sen_low) / 2
    # Senkou Span A (Leading Span A)
    df[senkou_span_a_column] = ((df[tenkan_sen_column] + df[kijun_sen_column]) / 2).shift(kijun_sen_period)
    # Senkou Span B (Leading Span B)
    senkou_span_high = high.rolling(window=senkou_span_period).max()
    senkou_span_low = low.rolling(window=senkou_span_period).min()
    df[senkou_span_b_column] = ((senkou_span_high + senkou_span_low) / 2).shift(kijun_sen_period)
    # The most current closing price plotted chikou_span_period time periods behind
    df[chikou_span_column] = close.shift(-1 * chikou_span_period)
    return df 
				
			
		

Thank you for this! This is really helpful as a reference as I use different broker to fetch real-time data and temporarily write to Redis.