KDJ
References
Definition
- KDJ, also known as random index, is a technical index widely used in short-term trend analysis of futures and stock markets.
- KDJ is calculated on the basis of the highest price, the lowest price and the closing price. It can reflect the intensity of price fluctuations, overbought and oversold, and give trading signals before prices rise or fall.
- KDJ is sensitive to price changes, which may generate wrong trading signals in very volatile markets, causing prices not to rise or fall with the signals, thus causing traders to make misjudgments.
Calculation
step 1: calculate the immature random value (RSV)

- Hn denotes the highest price, Ln denotes the lowest price, C denotes the closing price
step 2: calculate the %K line:

step 3: calculate the %D line:

step 4: calculate the %J line:

Read the indicator
-
KDJ values range from 0 to 100 (J values sometimes exceed). Generally speaking, an overbought signal occurs when the D value is more than 70 and an oversell signal occurs when the D value is less than 30.
- Gold fork
- When the K line breaks through the D line on the graph, it is commonly known as the golden fork, which is a buy signal. In addition, when the K-line and D-line cross upward below 20, the short-term buy signal is more accurate; if the K value is below 50 and crosses twice above D value to form a higher golden fork “W” shape, then the stock price may rise considerably and the market prospect is promising.
- Dead fork
- When the K value gets smaller and smaller, and then falls below the D line from above, it is often called a dead fork and is regarded as a sell signal. In addition, when K-line and D-line cross downward at gate 80, the short-term sell signal is more accurate. If the K value is above 50, crossing below the D line twice in the trend, and from the low dead cross “M” shape, the market outlook may have a considerable decline in stock prices.
- Bottom and top
- J-line is a sensitive line of direction. When the J value is greater than 90, especially for more than 5 consecutive days, the stock price will form at least a short-term peak. On the contrary, when the J value is less than 10:00, especially for several consecutive days, the stock price will form at least a short-term bottom.
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']
#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-09-10 21:20:33.417662 ^GSPC (5710, 7) 1999-12-31 00:00:00 2022-09-09 00:00:00
2022-09-10 21:20:33.761677 GSK (5710, 7) 1999-12-31 00:00:00 2022-09-09 00:00:00
2022-09-10 21:20:34.092952 NVO (5710, 7) 1999-12-31 00:00:00 2022-09-09 00:00:00
2022-09-10 21:20:34.340454 AROC (3791, 7) 2007-08-21 00:00:00 2022-09-09 00:00:00
ticker = 'GSK'
dfs[ticker].tail(5)
| Open | High | Low | Close | Volume | Dividends | Stock Splits | |
|---|---|---|---|---|---|---|---|
| Date | |||||||
| 2022-09-02 | 31.600000 | 31.969999 | 31.469999 | 31.850000 | 8152600 | 0.0 | 0.0 |
| 2022-09-06 | 31.650000 | 31.760000 | 31.370001 | 31.469999 | 5613900 | 0.0 | 0.0 |
| 2022-09-07 | 31.209999 | 31.590000 | 31.160000 | 31.490000 | 4822000 | 0.0 | 0.0 |
| 2022-09-08 | 30.910000 | 31.540001 | 30.830000 | 31.510000 | 6620900 | 0.0 | 0.0 |
| 2022-09-09 | 31.950001 | 31.969999 | 31.730000 | 31.889999 | 3556800 | 0.0 | 0.0 |
Define KDJ calculation function
def cal_kdj(ohlc: pd.DataFrame, period: int = 14) -> pd.DataFrame:
"""
KDJ
reference: https://www.futunn.com/en/learn/detail-what-is-kdj-64858-0
"""
ohlc = ohlc.copy(deep=True)
ohlc.columns = [c.lower() for c in ohlc.columns]
highest_high = ohlc["high"].rolling(center=False, window=period).max()
lowest_low = ohlc["low"].rolling(center=False, window=period).min()
rsv = (ohlc["close"] - lowest_low) / (highest_high - lowest_low) * 100
rsv = rsv.values
k_ = np.zeros(len(ohlc))
d_ = np.zeros(len(ohlc))
for i in range(len(ohlc)):
if i < period:
k_[i] = 0
d_[i] = 0
else:
k_[i] = (2/3)*k_[i-1] + (1/3)*rsv[i]
d_[i] = (2/3)*d_[i-1] + (1/3)*k_[i]
j_ = 3*k_ - 2*d_
return pd.DataFrame(data={'K': k_, 'D': d_, 'J': j_}, index=ohlc.index)
def cal_kdj2(ohlc: pd.DataFrame, period: int = 14, m1: int = 3, m2: int = 3) -> pd.DataFrame:
"""
KDJ
reference:
- https://www.futunn.com/en/learn/detail-what-is-kdj-64858-0
- https://github.com/mpquant/MyTT/blob/ea4f14857ecc46a3739a75ce2e6974b9057a6102/MyTT.py#L145
"""
ohlc = ohlc.copy(deep=True)
ohlc.columns = [c.lower() for c in ohlc.columns]
highest_high = ohlc["high"].rolling(center=False, window=period).max()
lowest_low = ohlc["low"].rolling(center=False, window=period).min()
rsv = (ohlc["close"] - lowest_low) / (highest_high - lowest_low) * 100
k_ = rsv.ewm(span=m1*2-1, adjust=False).mean()
d_ = k_.ewm(span=m2*2-1, adjust=False).mean()
j_ = 3*k_ - 2*d_
return pd.DataFrame(data={'K': k_, 'D': d_, 'J': j_})
Calculate KDJ
df = dfs[ticker][['Open', 'High', 'Low', 'Close', 'Volume']]
df = df.round(2)
cal_kdjdf_ta = cal_kdj(df, period = 14) df = df.merge(df_ta, left_index = True, right_index = True, how=’inner’ )
del df_ta gc.collect()
cal_kdj2
<function __main__.cal_kdj2(ohlc: pandas.core.frame.DataFrame, period: int = 14, m1: int = 3, m2: int = 3) -> pandas.core.frame.DataFrame>
df_ta = cal_kdj2(df, period = 14, m1 = 3, m2 = 3)
df = df.merge(df_ta, left_index = True, right_index = True, how='inner' )
del df_ta
gc.collect()
143
df_ta = cal_kdj2(df, period = 9, m1 = 5, m2 = 3) df = df.merge(df_ta, left_index = True, right_index = True, how=’inner’ )
del df_ta gc.collect()
display(df.head(5))
display(df.tail(5))
| Open | High | Low | Close | Volume | K | D | J | |
|---|---|---|---|---|---|---|---|---|
| Date | ||||||||
| 1999-12-31 | 19.60 | 19.67 | 19.52 | 19.56 | 139400 | NaN | NaN | NaN |
| 2000-01-03 | 19.58 | 19.71 | 19.25 | 19.45 | 556100 | NaN | NaN | NaN |
| 2000-01-04 | 19.45 | 19.45 | 18.90 | 18.95 | 367200 | NaN | NaN | NaN |
| 2000-01-05 | 19.21 | 19.58 | 19.08 | 19.58 | 481700 | NaN | NaN | NaN |
| 2000-01-06 | 19.38 | 19.43 | 18.90 | 19.30 | 853800 | NaN | NaN | NaN |
| Open | High | Low | Close | Volume | K | D | J | |
|---|---|---|---|---|---|---|---|---|
| Date | ||||||||
| 2022-09-02 | 31.60 | 31.97 | 31.47 | 31.85 | 8152600 | 4.883618 | 3.605564 | 7.439726 |
| 2022-09-06 | 31.65 | 31.76 | 31.37 | 31.47 | 5613900 | 4.287737 | 3.832955 | 5.197301 |
| 2022-09-07 | 31.21 | 31.59 | 31.16 | 31.49 | 4822000 | 6.056166 | 4.574025 | 9.020447 |
| 2022-09-08 | 30.91 | 31.54 | 30.83 | 31.51 | 6620900 | 10.230522 | 6.459524 | 17.772518 |
| 2022-09-09 | 31.95 | 31.97 | 31.73 | 31.89 | 3556800 | 17.151732 | 10.023594 | 31.408009 |
df[['K', 'D', 'J']].hist(bins=50)
array([[<AxesSubplot:title={'center':'K'}>,
<AxesSubplot:title={'center':'D'}>],
[<AxesSubplot:title={'center':'J'}>, <AxesSubplot:>]], dtype=object)

#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",
#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, mid_panel, chart_type='candle', names=None,
figratio=(14,9), fill_weights = (0, 0)):
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=2,
panel_ratios=(4,2,1), tight_layout=True, style=style, returnfig=True)
if names is None:
names = {'main_title': '', 'sub_tile': ''}
added_plots = {
'K': mpf.make_addplot(mid_panel['K'], panel=1, color='orange', width=1, secondary_y=False),
'D': mpf.make_addplot(mid_panel['D'], panel=1, color='dodgerblue', width=1, secondary_y=False),
'J': mpf.make_addplot(mid_panel['J'], panel=1, color='darkviolet', width=1, secondary_y=False),
}
fb_bbands2_ = dict(y1=fill_weights[0]*np.ones(mid_panel.shape[0]),
y2=fill_weights[1]*np.ones(mid_panel.shape[0]),color="lightskyblue",alpha=0.1,interpolate=True)
fb_bbands2_['panel'] = 1
fb_bbands= [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.1375)
axes[0].set_title(names['sub_tile'], fontsize=10, style='italic', loc='left')
axes[2].set_ylabel('KDJ')
axes[2].legend([None]*3)
handles = axes[2].get_legend().legendHandles
axes[2].legend(handles=handles,labels=list(added_plots.keys()))
return fig, axes
start = -200
end = df.shape[0]
names = {'main_title': f'{ticker}',
'sub_tile': 'KDJ: an overbought signal occurs when the D value is more than 70 and an oversell signal occurs when the D value is less than 30'}
aa_, bb_ = make_3panels2(df.iloc[start:end][['Open', 'High', 'Low', 'Close', 'Volume']],
df.iloc[start:end][['K', 'D', 'J']],
chart_type='hollow_and_filled',
names = names,
fill_weights = (30, 70)
)
