Candlestick Patterns, They Really Work? Conducting a Massive Event Study

Federico M. Glancszpigel
Nerd For Tech
Published in
10 min readJan 11, 2021

Candlestick patterns are an ancient form of technical analysis developed in Japan around the 18th century. The invention is attributed to Japanese rice trader, Munehisa Homma, but it was not until the early 90s that the technique was popularized by Steve Nilson with his book, Japanese Candlestick Charting Techniques. This article aims to conduct an Event Study over all the S&P500 constituents to determine the real effectiveness of candlesticks as predictors.

In the first section, we explain the basics of candlesticks. Then, in the second section, we detail the procedure to conduct an event study. In the third section, we go over the Python code used to conduct the study. In the fourth section, we show the results of the experiment and finally, in the fifth section, we conclude.

Candlesticks Basics

The aim of this article is not to dive in detail on how all candlestick patterns work, but we will briefly explain how a single candle is read.

The visualization of a candle does not diverge much from a bar chart, both show the same information. The main difference falls in a matter of terminology and vocabulary. The center of the candle is called the “real body, which represents the price range between the open and close price. If the real body is black, it means that the close was lower than the open, while a white body indicates the opposite. Then, above and below the real body are the so-called “shadows”, upper and lower, which show the high and low prices of that trading day. The thing about candlesticks is that they sometimes form several patterns which serve as predictors. Patterns are divided into bullish and bearish, whether they predict a rise or a fall of the asset’s price.

The most difficult part of working with candlesticks is recognizing the patterns. Fortunately for us, Python offers a library that makes this task easier, called TA-Lib. The package also has several features for conducting technical analysis, which we invite you to check in the References section.

Event Studies

In this section, we will explain the methodology implemented to conduct the analysis. First, a necessary definition: What is an Event Study?

An Event Study is a statistical technique to measure the impact of a given event on the value of a firm or asset. The method relies on Fama’s Market Efficiency Hypothesis (EMH) and the Capital Asset Pricing Model (CAPM). Below, we detail the process of an event study:

1 ) Define the event: the first step is selecting the event (and the firm or asset) on which the study will be conducted. Usually, event studies are made over stock splits, M&A announcements, earnings announcements, macroeconomic announcements, etc. In this line, an event is defined as a non-anticipated shock in a well-defined moment in time.

2 ) Split the data into windows: after choosing the event carefully, we have to determine an Estimation Window, an Event Window and a Control Window. We can observe the lengths of each window in the figure below. In the Estimation Window, we will fit a model with the data available before the Event Window to compute a series of parameters. In this line, the window must be long enough (this is related to the behavior of the event) for the estimated parameters to be robust. Then, in the Control Window (Post-event window), we will apply the estimated parameters to compute the Abnormal Returns. The Event Window can be divided into two phases: the previous moments to the event (T1 to 0), the event day (t=0) and 3-the immediate post-event (0 to T2). In our case, T2=0.

3 ) Choose a model: the next step is to select a model to compute the Estimation Window’s parameters, we will focus on two: Mean-Return Model and Market Model.

· Mean-Return: is a single-factor model that assumes returns behavior can be explained by its expected value (mu) plus a white noise (epsilon).

Ri,t denotes the return of a firm (or asset) i in the trading day t.

· Market Model or CAPM: assumes that two risk factors explain the behavior of returns. CAPM states that an asset’s returns can be explained partially by the returns of the market (Rm) and, on the other hand, by the firm’s idiosyncratic risk (epsilon). For the scope of this article, we will focus particularly on this model.

Alfa and beta are obtained from an OLS regression.

4) Compute Abnormal Returns: after choosing the model, we are ready to calculate Abnormal Returns (ARs) over the Control Window. ARs are defined as the difference between the current price of an asset and the predicted or “fair” price estimated by a model (in this case, the Market Model).

The procedure is simple: 1- we estimate alfa and beta with the data available in the Estimation Window, 2- we compute the returns’ expected value over the Control Window and 3- we calculate ARs.

5 ) Perform the T-test: the final step is to compute the statistic and perform a T-student test. For this step, we need to calculate the Cumulative Abnormal Returns (CAR) over the Control Window.

In this line, the statistic is distributed as a T-student with L1–2 degrees of freedom. For our case, we conduct a single tail t-test depending on the trend associate to the candlestick pattern (bullish or bearish). In this line, a test for a bullish and bearish pattern states:

The Code

In this section, we will go over the code implemented to conduct the experiment. We define a class called Events that contains the whole code:

# Necessary Imports
import talib as tb
import yfinance as yf
import pandas as pd
from dateutil.relativedelta import relativedelta
import numpy as np
from sklearn.linear_model import LinearRegression
class Events:
def __init__(self,df,l1,l2,l3,model,patterns):
"""
df: DataFrame with price data: Open, Close, High & Low.
l1: length of estimation window.
l2: length of event window
l3: length of control window
model: market or mean return model.
patterns: candlestick patterns to conduct the event study.
"""
self.df=df
self.l1=l1
self.l2=l2
self.l3=l3
self.model=model
self.patterns=patternsGetting Events

As we mentioned before, the TA-Lib library offers an attribute to identify candlestick patterns very easily. Currently, TA-Lib’s recognition algorithm supports 61 patterns. The function’s syntax is quite simple: PATTERN_NAME (Open, High, Low, Close). In this line, we can use Python’s built-in getattr, passing the library name (or abbreviation), the pattern’s name and the four inputs. The pattern recognition function outputs a 100 whenever it finds a bullish pattern, a -100 for a bearish pattern and 0 when it does not recognize any pattern. For more information about TA-Lib’s attributes, you can find the library’s documentation in the References section.

def candle_events(self,stock,pattern):   

#Define the events
ind=self.df['Adj Close'][stock].dropna().index.tolist()
df=pd.DataFrame(index=ind)
hi=self.df['High'][stock].dropna().values
lo=self.df['Low'][stock].dropna().values
op=self.df['Open'][stock].dropna().values
cl=self.df['Close'][stock].dropna().values
df[pattern] = getattr(tb, pattern)(op, hi, lo, cl)
events=df[df[pattern]!=0][pattern]

Building The Windows

The following step in the code is to define the Estimation and Control Windows for each event in the events data frame. The key in this part of the code is that events can’t overlap. If we select an event that overlaps with the Estimation Window of another event, we will obtain biased results.

#Dates list
dates=events.index.tolist()
dates.insert(0,df.index[1])
dates.append(df.index[-1])
#Compute Windows
estimation_window=[]
control_window=[]
event_day=[]
bullish_bearish=[]
for i in range(1,len(dates)-1):
if ind.index(dates[i])-ind.index(dates[i-1])>=self.l1+self.l2 and ind.index(dates[i+1])-ind.index(dates[i])>=self.l3+1:
estimation_window.append((ind[ind.index(dates[i])-(self.l1+self.l2)],ind[ind.index(dates[i])-self.l2]))
control_window.append((ind[ind.index(dates[i])+1],ind[ind.index(dates[i])+self.l3]))
event_day.append(str(dates[i])[:-9])
bullish_bearish.append(events.loc[dates[i]])
return estimation_window,control_window,event_day,bullish_bearish

Selecting the Model

As we described in the previous section, the third step is to define a model to estimate the expected returns. Our code is prepared for the two models mentioned before: Mean-Return and Market Model. For the OLS regression, we use Scikit Learn’s regression tools.

def get_CAR(self,stock,pattern):

reference_index=self.df['Adj Close'].columns[-1]
estimation_window, control_window, event_day, bullish_bearish=self.candle_events(stock,pattern)
log_returns=np.log(abs(self.df['Adj Close'][[stock,reference_index]])/abs(self.df['Adj Close'][[stock,reference_index]].shift(1))).dropna()
CAR, V_CAR, SCAR={},{},{}

if event_day==[]:
return {}, {}, {}, {}

else:
for t in range(len(estimation_window)):
#Linear Regression
Rm_estimation=log_returns[reference_index].loc[estimation_window[t][0]:estimation_window[t][1]].values.reshape(-1,1)
Ri_estimation=log_returns[stock].loc[estimation_window[t][0]:estimation_window[t][1]].values.reshape(-1,1)
Rm_event=log_returns[reference_index].loc[control_window[t][0]:control_window[t][1]].values.reshape(-1,1)
Ri_event=log_returns[stock].loc[control_window[t][0]:control_window[t][1]].values

#Calculate expected returns (Market or Mean Return model)
if self.model=='market':
lm=LinearRegression()
lm.fit(Rm_estimation,Ri_estimation)
event_preds=lm.predict(Rm_event).reshape(Rm_event.shape[0])
est_fitted=lm.predict(Rm_estimation).reshape(Rm_estimation.shape[0])
elif self.model=='mean return':
event_preds=est_fitted=Ri_estimation.reshape(Ri_estimation.shape[0]).mean()

Computing ARs and building the T-stat

The next step is to calculate Abnormal Returns and build the T-statistic.

#AR estimation window
AR_est=Ri_estimation.reshape(Ri_estimation.shape[0])-est_fitted
#AR for the control window
AR_event=Ri_event.reshape(Ri_event.shape[0])-event_preds
#Variance of AR
Vi=sum(AR_est**2)/(self.l1-2)
#CAR & CAR variance
if Vi!=0:
CAR[str(event_day[t])]=sum(AR_event)
V_CAR[str(event_day[t])]=self.l3*Vi
SCAR[str(event_day[t])]=CAR[str(event_day[t])]/V_CAR[str(event_day[t])]**0.5
else:
pass

return CAR, V_CAR, SCAR, bullish_bearish

Computing the Proportion of Significant Events Over a Pattern

For the study to be massive, we have to build a function that performs the study’s procedure repeatedly for a group of stocks. In this line, the function below runs get_CAR() for each stock in the self.df data frame and calculates the p-value of the t-test depending on the trend associate to the pattern selected (bullish or bearish). Then, it appends the number of events statistically significant, at a 5% level, to a dictionary and the total number of samples analyzed (events) to another dictionary.

def pattern_significance(self,pattern):

#Number of stocks in the sample
stocks=self.df['Adj Close'].columns[:-1]

one_tail_sign,N={},{}

for s in tq(stocks):
SCAR,bullish_bearish=self.get_CAR(s,pattern)[2:4]

if SCAR!={}:
#Two tail t-test
#tt_pvalue=[2*(1-t.cdf(abs(x),self.l1-2)) for x in SCAR.values()]
#two_tail_sign[s]=sum([1 for x in tt_pvalue if x<0.01])/len(tt_pvalue)

#One tail t-test depending on the pattern trend (Bullish or Bearish)
for i in bullish_bearish:
if i==100:
ot_pvalue=[t.pdf(x,self.l1-2) for x in SCAR.values()]
one_tail_sign[s]=sum([1 for x in ot_pvalue if x<0.05])
elif i==-100:
ot_pvalue=[t.cdf(x,self.l1-2) for x in SCAR.values()]
one_tail_sign[s]=sum([1 for x in ot_pvalue if x<0.05])
else:
one_tail_sign[s]=np.nan

elif SCAR=={}:
#two_tail_sign[s]=np.nan
one_tail_sign[s]=np.nan

#Number of observations for each stock
N[s]=len(SCAR)

return one_tail_sign,sum(N.values())

Performing the Study for all the Patterns Available

In the last part of the code, we simply extend the pattern_significance function to the 61 patterns available in TA-Lib. The function outputs a data frame with the stock tickers as rows and the pattern names as columns, containing the number of statistically significant events and a dictionary containing the number of samples analyzed for each pattern.

def S_overall(self):
one_tail_soverall={}
N_events={}
for p in tq(self.patterns):
ot_sign, N = self.pattern_significance(p)
N_events[p]=N
one_tail_soverall[p]=ot_sign
return pd.DataFrame(one_tail_soverall,index=self.df['Adj Close'].columns[:-1]),N_events

To visualize the whole code, you can consult the Github repo in the References section.

Results of the Experiment

The experiment was conducted over 502 constituents of the S&P500 using the Market Model with the index as the market’s benchmark. We selected L1=200, L2=2 and L3=4 trading days. The algorithm processed a total of 136,555 events. In the table below we can see the top three and the last three patterns, ranked by proportion of statistically significant events. On the other hand, we separated into another table, the patterns with more appearances and vice-versa.

On the other hand, we can analyze the distribution of the patterns:

We can observe that significance is quite low. From the 136,555 events analyzed in the experiment, only 7247 turned out to be statistically significant. In other words, 5.3% of the time the candlestick patterns predicted accurately the price’s trend.

Conclusion

So, are candlestick patterns really effective? Statistically speaking we can conclude that only 5.3% of the time, candlestick patterns predict accurately future price’s movements. In this line, despite all the mysticism surrounding these figures, they proved to be very inefficient as predictors. Furthermore, they even perform worst than a random guessing model. In conclusion, another myth of technical analysis is banished.

We hope that the results of this article encourage readers to continue studying candlesticks and other technical analysis tools from different perspectives. It will be interesting in the future to see a similar experiment with technical indicators and patterns.

References

TA-Lib’s source:

More on Candlestick Patterns:

More on Event Studies:

Github Repository for this article:

--

--