Keltner Channels (KC)

References

Definition

  • The Keltner Channels (KC) indicator is a banded indicator similar to Bollinger Bands and Moving Average Envelopes.
  • They consist of an Upper Envelope above a Middle Line as well as a Lower Envelope below the Middle Line.
    • The Middle Line is a moving average of price over a user-defined time period. Either a simple moving average or an exponential moving average are typically used.
    • The Upper and Lower Envelopes are set a (user-defined multiple) of a range away from the Middle Line. This can be a multiple of the daily high/low range, or more commonly a multiple of the Average True Range.

Calculation


20 Period EMA with Envelopes using Average True Range and a multiplier of 2 as example:

  • Basis = 20 Period EMA
  • Upper Envelope = 20 Period EMA + (2 X ATR)
  • Lower Envelope = 20 Period EMA - (2 X ATR)

Read the indicator

  • The Keltner Channels (KC) indicator is a lagging indicator.
  • The main occurrences to look for when using Keltner Channels are breakthroughs above the Upper Envelope or below the Lower Envelope. A breakthrough above the Upper Envelope signifies overbought conditions. A breakthrough below the Lower Envelope signifies oversold conditions.
  • Keep in mind however when using Keltner Channels, that overbought and oversold conditions are oftentimes a sign of strength. During a clearly defined trend, overbought and oversold conditions can signify strength. In this case, the current trend would strengthen and ultimately continue. It works a little bit different in a sideways market. When the market is trending sideways, overbought and oversold readings are frequently followed by price moving back towards the moving average (Middle Line).

  • Trend Confirmation
    • during a clearly defined trend, breakthrough above or below the envelopes can be a sign of underlying strength of the trend.
    • During a Bullish Trend, a breakthrough above the upper envelope can be seen as a sign of strength and the uptrend is likely to continue.
    • During a Bearish Trend, a breakthrough below the lower envelope can be seen as a sign of strength and the downtrend is likely to continue.
  • Overbought and Oversold
    • When a market is choppy or trading sideways, Keltner Channels can be useful for identifying overbought and oversold conditions. These conditions can typically lead to price corrections where price moves back towards the moving average (Middle Line).

In terms of trend identification and determining overbought and oversold levels, the Keltner Channels indicator does this effectively.

While Keltner Channels can be used independently, it is best to use them with additional technical analysis tools. Historical analysis may also be helpful when trying to determine the correct parameters when setting up the indicator. Different securities may require a different multiplier to adjust the width of the bands or envelopes.

Load basic packages
import pandas as pd
import numpy as np
import os
import gc
import copy
from pathlib import Path
from datetime import datetime, timedelta, time, date
#this package is to download equity price data from yahoo finance
#the source code of this package can be found here: https://github.com/ranaroussi/yfinance/blob/main
import yfinance as yf
pd.options.display.max_rows = 100
pd.options.display.max_columns = 100

import warnings
warnings.filterwarnings("ignore")

import pytorch_lightning as pl
random_seed=1234
pl.seed_everything(random_seed)
Global seed set to 1234





1234
#S&P 500 (^GSPC),  Dow Jones Industrial Average (^DJI), NASDAQ Composite (^IXIC)
#Russell 2000 (^RUT), Crude Oil Nov 21 (CL=F), Gold Dec 21 (GC=F)
#Treasury Yield 10 Years (^TNX)

#benchmark_tickers = ['^GSPC', '^DJI', '^IXIC', '^RUT',  'CL=F', 'GC=F', '^TNX']

benchmark_tickers = ['^GSPC']
tickers = benchmark_tickers + ['GSK', 'NVO', 'AROC', 'DAL']
#https://github.com/ranaroussi/yfinance/blob/main/yfinance/base.py
#     def history(self, period="1mo", interval="1d",
#                 start=None, end=None, prepost=False, actions=True,
#                 auto_adjust=True, back_adjust=False,
#                 proxy=None, rounding=False, tz=None, timeout=None, **kwargs):

dfs = {}

for ticker in tickers:
    cur_data = yf.Ticker(ticker)
    hist = cur_data.history(period="max", start='2000-01-01')
    print(datetime.now(), ticker, hist.shape, hist.index.min(), hist.index.max())
    dfs[ticker] = hist
2022-08-26 00:28:36.746272 ^GSPC (5700, 7) 1999-12-31 00:00:00 2022-08-25 00:00:00
2022-08-26 00:28:37.078984 GSK (5700, 7) 1999-12-31 00:00:00 2022-08-25 00:00:00
2022-08-26 00:28:37.360578 NVO (5700, 7) 1999-12-31 00:00:00 2022-08-25 00:00:00
2022-08-26 00:28:37.649839 AROC (3781, 7) 2007-08-21 00:00:00 2022-08-25 00:00:00
2022-08-26 00:28:37.954032 DAL (3857, 7) 2007-05-03 00:00:00 2022-08-25 00:00:00
ticker = 'DAL'
dfs[ticker].tail(5)
Open High Low Close Volume Dividends Stock Splits
Date
2022-08-19 33.880001 34.060001 33.060001 33.250000 9860900 0.0 0
2022-08-22 32.500000 32.500000 31.850000 32.380001 9246500 0.0 0
2022-08-23 32.389999 33.020000 32.270000 32.869999 7974300 0.0 0
2022-08-24 32.880001 33.410000 32.669998 33.310001 5739500 0.0 0
2022-08-25 33.580002 34.160000 33.520699 33.660000 3562483 0.0 0
Define Keltner Channels (KC) calculation function
#https://www.tradingview.com/support/solutions/43000501980-choppiness-index-chop/
def cal_tr(ohlc: pd.DataFrame) -> pd.Series:
    """True Range is the maximum of three price ranges.
    Most recent period's high minus the most recent period's low.
    Absolute value of the most recent period's high minus the previous close.
    Absolute value of the most recent period's low minus the previous close."""

    TR1 = pd.Series(ohlc["high"] - ohlc["low"]).abs()  # True Range = High less Low

    TR2 = pd.Series(
        ohlc["high"] - ohlc["close"].shift()
    ).abs()  # True Range = High less Previous Close

    TR3 = pd.Series(
        ohlc["close"].shift() - ohlc["low"]
    ).abs()  # True Range = Previous Close less Low

    _TR = pd.concat([TR1, TR2, TR3], axis=1)

    _TR["TR"] = _TR.max(axis=1)

    return pd.Series(_TR["TR"], name="TR")


def cal_atr(ohlc: pd.DataFrame, period: int = 14) -> pd.Series:
    """Average True Range is moving average of True Range."""

    TR = cal_tr(ohlc)
    return pd.Series(
        TR.rolling(center=False, window=period).mean(),
        name=f"ATR{period}",
    )
    


def cal_kc(
        ohlc: pd.DataFrame,
        period: int = 20,
        atr_period: int = 10,
        MA: pd.Series = None,
        kc_mult: float = 2,
    ) -> pd.DataFrame:
    
    """
    Keltner Channels (KC) are volatility-based envelopes set above and below an exponential moving average.
    This indicator is similar to Bollinger Bands, which use the standard deviation to set the bands.
    Instead of using the standard deviation, Keltner Channels use the Average True Range (ATR) to set channel distance.
    The channels are typically set two Average True Range values above and below the 20-day EMA.
    The exponential moving average dictates direction and the Average True Range sets channel width.
    Keltner Channels are a trend following indicator used to identify reversals with channel breakouts and channel direction.
    Channels can also be used to identify overbought and oversold levels when the trend is flat.
    """

    ohlc = ohlc.copy(deep=True)
    ohlc.columns = [c.lower() for c in ohlc.columns]
    
    
    if not isinstance(MA, pd.core.series.Series):
        middle = pd.Series(ohlc["close"].ewm(span=period).mean(), name="KC_MIDDLE")
    else:
        middle = pd.Series(MA, name="KC_MIDDLE")
    
    atr = cal_atr(ohlc, period=atr_period)
  
    up = pd.Series(middle + (kc_mult * atr), name="KC_UPPER")
    down = pd.Series( middle - (kc_mult * atr), name="KC_LOWER")

    return pd.concat([up, middle, down], axis=1)

Calculate Keltner Channels (KC)
df = dfs[ticker][['Open', 'High', 'Low', 'Close', 'Volume']]
df = df.round(2)
cal_kc
<function __main__.cal_kc(ohlc: pandas.core.frame.DataFrame, period: int = 20, atr_period: int = 10, MA: pandas.core.series.Series = None, kc_mult: float = 2) -> pandas.core.frame.DataFrame>
df_ta = cal_kc(df, period=14, atr_period=5, kc_mult=2)
df = df.merge(df_ta, left_index = True, right_index = True, how='inner' )

del df_ta
gc.collect()
122
from core.finta import TA
TA.MAMA
<function core.finta.TA.MAMA(ohlc: pandas.core.frame.DataFrame, fast_limit: float = 0.5, slow_limit: float = 0.05, column: str = 'close') -> pandas.core.series.Series>
df_ta = TA.MAMA(df, column='close')
df = df.merge(df_ta, left_index = True, right_index = True, how='inner' )

del df_ta
gc.collect()
42
df_ta = cal_kc(df, period=14, atr_period=5, MA=df['MAMA'],  kc_mult=2)
df_ta.columns = [f'MAMA_{c}' for c in df_ta.columns]
df = df.merge(df_ta, left_index = True, right_index = True, how='inner' )

df_ta = cal_kc(df, period=14, atr_period=5, MA=df['FAMA'],  kc_mult=2)
df_ta.columns = [f'FAMA_{c}' for c in df_ta.columns]
df = df.merge(df_ta, left_index = True, right_index = True, how='inner' )

del df_ta
gc.collect()
0
display(df.head(5))
display(df.tail(5))
Open High Low Close Volume KC_UPPER KC_MIDDLE KC_LOWER MAMA FAMA MAMA_KC_UPPER MAMA_KC_MIDDLE MAMA_KC_LOWER FAMA_KC_UPPER FAMA_KC_MIDDLE FAMA_KC_LOWER
Date
2007-05-03 19.32 19.50 18.25 18.40 8052800 NaN 18.400000 NaN 18.40 18.40 NaN 18.40 NaN NaN 18.40 NaN
2007-05-04 18.88 18.96 18.39 18.64 5437300 NaN 18.528571 NaN 18.64 18.64 NaN 18.64 NaN NaN 18.64 NaN
2007-05-07 18.83 18.91 17.94 18.08 2646300 NaN 18.357216 NaN 18.08 18.08 NaN 18.08 NaN NaN 18.08 NaN
2007-05-08 17.76 17.76 17.14 17.44 4166100 NaN 18.076613 NaN 17.44 17.44 NaN 17.44 NaN NaN 17.44 NaN
2007-05-09 17.54 17.94 17.44 17.58 7541100 19.639048 17.947048 16.255048 17.58 17.58 19.272 17.58 15.888 19.272 17.58 15.888
Open High Low Close Volume KC_UPPER KC_MIDDLE KC_LOWER MAMA FAMA MAMA_KC_UPPER MAMA_KC_MIDDLE MAMA_KC_LOWER FAMA_KC_UPPER FAMA_KC_MIDDLE FAMA_KC_LOWER
Date
2022-08-19 33.88 34.06 33.06 33.25 9860900 35.871887 33.723887 31.575887 33.933994 34.197449 36.081994 33.933994 31.785994 36.345449 34.197449 32.049449
2022-08-22 32.50 32.50 31.85 32.38 9246500 35.852702 33.544702 31.236702 33.156997 33.937336 35.464997 33.156997 30.848997 36.245336 33.937336 31.629336
2022-08-23 32.39 33.02 32.27 32.87 7974300 35.630742 33.454742 31.278742 33.058943 33.787283 35.234943 33.058943 30.882943 35.963283 33.787283 31.611283
2022-08-24 32.88 33.41 32.67 33.31 5739500 35.395443 33.435443 31.475443 33.184472 33.636580 35.144472 33.184472 31.224472 35.596580 33.636580 31.676580
2022-08-25 33.58 34.16 33.52 33.66 3562483 35.533384 33.465384 31.397384 33.422236 33.582994 35.490236 33.422236 31.354236 35.650994 33.582994 31.514994
df[['KC_UPPER', 'KC_MIDDLE' , 'KC_LOWER']].hist(bins=50)
array([[<AxesSubplot:title={'center':'KC_UPPER'}>,
        <AxesSubplot:title={'center':'KC_MIDDLE'}>],
       [<AxesSubplot:title={'center':'KC_LOWER'}>, <AxesSubplot:>]],
      dtype=object)

png

#https://github.com/matplotlib/mplfinance
#this package help visualize financial data
import mplfinance as mpf
import matplotlib.colors as mcolors

# all_colors = list(mcolors.CSS4_COLORS.keys())#"CSS Colors"
# all_colors = list(mcolors.TABLEAU_COLORS.keys()) # "Tableau Palette",
# all_colors = list(mcolors.BASE_COLORS.keys()) #"Base Colors",
all_colors = ['dodgerblue', 'firebrick','limegreen','skyblue','lightgreen',  'navy','yellow','plum',  'yellowgreen']
#https://github.com/matplotlib/mplfinance/issues/181#issuecomment-667252575
#list of colors: https://matplotlib.org/stable/gallery/color/named_colors.html
#https://github.com/matplotlib/mplfinance/blob/master/examples/styles.ipynb

def make_3panels2(main_data, add_data, mid_panel=None, chart_type='candle', names=None, figratio=(14,9)):
    """
    main chart type: default is candle. alternatives: ohlc, line

    example:
    start = 200

    names = {'main_title': 'MAMA: MESA Adaptive Moving Average', 
             'sub_tile': 'S&P 500 (^GSPC)', 'y_tiles': ['price', 'Volume [$10^{6}$]']}


    make_candle(df.iloc[-start:, :5], df.iloc[-start:][['MAMA', 'FAMA']], names = names)
    
    """

    style = mpf.make_mpf_style(base_mpf_style='yahoo',  #charles
                               base_mpl_style = 'seaborn-whitegrid',
#                                marketcolors=mpf.make_marketcolors(up="r", down="#0000CC",inherit=True),
                               gridcolor="whitesmoke", 
                               gridstyle="--", #or None, or - for solid
                               gridaxis="both", 
                               edgecolor = 'whitesmoke',
                               facecolor = 'white', #background color within the graph edge
                               figcolor = 'white', #background color outside of the graph edge
                               y_on_right = False,
                               rc =  {'legend.fontsize': 'small',#or number
                                      #'figure.figsize': (14, 9),
                                     'axes.labelsize': 'small',
                                     'axes.titlesize':'small',
                                     'xtick.labelsize':'small',#'x-small', 'small','medium','large'
                                     'ytick.labelsize':'small'
                                     }, 
                              )   

    if (chart_type is None) or (chart_type not in ['ohlc', 'line', 'candle', 'hollow_and_filled']):
        chart_type = 'candle'
    len_dict = {'candle':2, 'ohlc':3, 'line':1, 'hollow_and_filled':2}    
        
    kwargs = dict(type=chart_type, figratio=figratio, volume=True, volume_panel=1, 
                  panel_ratios=(4,2), tight_layout=True, style=style, returnfig=True)
    
    if names is None:
        names = {'main_title': '', 'sub_tile': ''}
    



    added_plots = { }
    for name_, data_ in add_data.iteritems():
        added_plots[name_] = mpf.make_addplot(data_, panel=0, width=1, secondary_y=False)
    
    fb_bbands_ = dict(y1=add_data.iloc[:, 0].values,
                      y2=add_data.iloc[:, 2].values,color="lightskyblue",alpha=0.1,interpolate=True)
    fb_bbands_['panel'] = 0
    

    fb_bbands= [fb_bbands_]
    
    
    if mid_panel is not None:
        i = 0
        for name_, data_ in mid_panel.iteritems():
            added_plots[name_] = mpf.make_addplot(data_, panel=1, color=all_colors[i])
            i = i + 1
        fb_bbands2_ = dict(y1=np.zeros(mid_panel.shape[0]),
                      y2=0.8+np.zeros(mid_panel.shape[0]),color="lightskyblue",alpha=0.1,interpolate=True)
        fb_bbands2_['panel'] = 1
        fb_bbands.append(fb_bbands2_)


    fig, axes = mpf.plot(main_data,  **kwargs,
                         addplot=list(added_plots.values()), 
                         fill_between=fb_bbands)
    # add a new suptitle
    fig.suptitle(names['main_title'], y=1.05, fontsize=12, x=0.1285)

    axes[0].legend([None]*5)
    handles = axes[0].get_legend().legendHandles
    axes[0].legend(handles=handles[2:],labels=list(added_plots.keys()))
    axes[0].set_title(names['sub_tile'], fontsize=10, style='italic',  loc='left')
    

#     axes[0].set_ylabel(names['y_tiles'][0])
#     axes[2].set_ylabel(names['y_tiles'][1])
    return fig, axes
   

start = -200
end = -100#df.shape[0]

names = {'main_title': f'{ticker}', 
         'sub_tile': 'KC: overbought -breakthrough above the Upper Envelope;oversold - breakthrough below the Lower Envelope'}


aa_, bb_ = make_3panels2(df.iloc[start:end][['Open', 'High', 'Low', 'Close', 'Volume']], 
            df.iloc[start:end][['KC_UPPER', 'KC_MIDDLE','KC_LOWER']],
             chart_type='hollow_and_filled',names = names)

png


start = -200
end = -100#df.shape[0]

names = {'main_title': f'{ticker}', 
         'sub_tile': 'KC: overbought -breakthrough above the Upper Envelope;oversold - breakthrough below the Lower Envelope'}


aa_, bb_ = make_3panels2(df.iloc[start:end][['Open', 'High', 'Low', 'Close', 'Volume']], 
            df.iloc[start:end][['MAMA_KC_UPPER', 'MAMA_KC_MIDDLE','MAMA_KC_LOWER']],
             chart_type='hollow_and_filled',names = names)

png


start = -200
end = -100#df.shape[0]

names = {'main_title': f'{ticker}', 
         'sub_tile': 'KC: overbought -breakthrough above the Upper Envelope;oversold - breakthrough below the Lower Envelope'}


aa_, bb_ = make_3panels2(df.iloc[start:end][['Open', 'High', 'Low', 'Close', 'Volume']], 
            df.iloc[start:end][['FAMA_KC_UPPER', 'FAMA_KC_MIDDLE', 'FAMA_KC_LOWER']],
             chart_type='hollow_and_filled',names = names)

png