You are on page 1of 13

2/4/22, 3:08 PM Empower automated stock trading with pattern recognition — Identify channels with a recursive algorithm | by J. K.

Chang | D…

Empower automated stock trading with pattern


recognition — Identify channels with a
recursive algorithm

Photo by AbsolutVision on Unsplash

During the course of developing an automated trading system, pattern recognition is one
of the most important elements. Once the system has learned to correctly identify
various patterns, more options are opened up while designing a trading strategy that
suits an individual. To enable the functionality of a trading system step by step, this
article will extend the applications of the global recursive trading pattern recognition
algorithm with channel patterns. With such an implementation or similar, a stock
trading system should be able to:

automatically generate and display channel patterns, and/or

automatically label stock data with chart patterns for the machine to learn from and
eventually automatically generate trading strategies after integrating with other
elements.

https://medium.datadriveninvestor.com/empower-automated-stock-trading-with-pattern-recognition-identify-channels-with-a-recursive-algo-55de… 1/13
2/4/22, 3:08 PM Empower automated stock trading with pattern recognition — Identify channels with a recursive algorithm | by J. K. Chang | D…

For readers who want to test the code while reading, the full code base is included near
the bottom of this article.

What are the challenges for trading systems to recognize stock trading
patterns?
When manually drawing the stock trading patterns based on a set of rules, the outcome
is never 100% objective. The rules are tweaked with human perception, as in any system.
This fact doesn’t necessarily mean the trades are negatively affected. As opposed, human
expertise in this subject increases the odds to win, on the occasion of unusual chart
patterns. However, manual creation of chart patterns imposes two challenges for the
enablement of the automation of trading systems:

The optimized/customized/tweaked chart patterns for a particular situation might


cause difficulty for the machine to learn.

An unbearable amount of careful manual work is required to generate a sufficient


amount of data in order to achieve comparable machine learning results to that using
generic programmatic algorithm-generated chart pattern labels.

More generally, batch production of stock data sets with labeled chart patterns and the
robustness of such production are two primary challenges for an automated algorithm
trading system.

An algorithmic solution to chart pattern recognition


Speaking of algorithms for trading chart pattern recognition, the first impression might
be computer vision and deep learning-related algorithms. However, they won’t help with
tackling the two aforementioned challenges since they are heavily dependent on the
labeled inputs, either labeled images or time-series data.

On the contrary, the algorithm in this article can directly visualize chart patterns and
automatically label stock data at scale, and eventually automatically generate trading
strategies after being integrated with other components in a trading system. This
algorithm is a global recursive approach to identify trading chart patterns. In the linked
article, an implementation to visualize and label support and resistance is introduced. In
this article, the application of this algorithm will be extended to recognize channels.

Empower automated stock trading with pattern recognition — a


recursive programmatic approach
A highly generalizable global recursive programmatic approach to stock
https://medium.datadriveninvestor.com/empower-automated-stock-trading-with-pattern-recognition-identify-channels-with-a-recursive-algo-55de… 2/13
2/4/22, 3:08 PM Empower automated stock trading with pattern recognition — Identify channels with a recursive algorithm | by J. K. Chang | D…
A highly generalizable global recursive programmatic approach to stock
pattern recognition and its implementation for
medium.datadriveninvestor.com

As a quick recap, the algorithm is included here.

# The targeting pattern to be recognized is called "the pattern" in


this algorithm.

1. Find the pattern on the entire data range, which is range(0, l+1)
initially, where l is the length of the data range.

2. If there are, detect the first point (P0) that breaks the global
pattern from step one. The pattern is broken whenever at least one of
the two thresholds is penetrated.
- The validity of the pattern

- The strength of the pattern

3. Split the whole data range from P0.

4. Re-evaluate the pattern for data range before point P0 by


recursively repeating step two to step four. Stop when no more breaking
point is found. The last breaking point is denoted as Pi.

5. Set the new whole data range as range(Pi, l+1), repeat step two to
step five. Stop when no more breaking point is found for the whole data
range

Channel patterns in trading charts


Before implementing this algorithm, the channel patterns need to be explicitly defined as
follows:

Channels are based on parallel trending lines, i.e. parallel support and resistance
pairs

A pair of support and resistance is considered parallel if their slope difference is less
than the threshold s.

A channel pattern is broken if a ticker is p percentage above the value on the


resistance line or below the support line.

According to the definition, the first step is to find trend lines. We will modify
the  trendlineSeeker  class in the linked article above. The main purpose of the

https://medium.datadriveninvestor.com/empower-automated-stock-trading-with-pattern-recognition-identify-channels-with-a-recursive-algo-55de… 3/13
2/4/22, 3:08 PM Empower automated stock trading with pattern recognition — Identify channels with a recursive algorithm | by J. K. Chang | D…

modification is to add the functionality to return both support and resistance at the same
time. Besides, it will support multiple tickers as inputs. The modified code block is
included below.

class trendlineSeeker:


def __init__(self, symbols, flag='all'):

self.connectors = alpaca_trade_api.REST('configure your key',


'configure your secret', 'https://paper-api.alpaca.markets')

self.symbols = symbols

self.start = '2019-08-29'
self.end = '2021-08-21'

self.validationWindowUpper = 48
self.validationWindowLower = 48

self.distortionThresholdUpper =
0.15
self.distortionThresholdLower = 0.15


if flag == 'all':

self.flag = ['upper', 'lower']


else:

self.flag = [flag]

self.data = None

self.previousTrendlineBreakingPoint = None
self.currentTrendlineBreakingPoint = None


self.refinedTrendlines = {}

def get_data(self):

df = pd.DataFrame()

NY = 'America/New_York'

start = pd.Timestamp(self.start, tz=NY).isoformat()


end = pd.Timestamp(self.end, tz=NY).isoformat()

self.data = self.connectors.get_barset(self.symbols, 'day',


start=start, end=end, limit=1000).df

self.data = pd.concat([df, self.data], axis=0)


self.data['row number'] = np.arange(1, len(self.data)+1)

self.previousTrendlineBreakingPoint = 0

self.currentTrendlineBreakingPoint = len(self.data) + 1

def getCurrentSymbol(self):

for symbol in self.symbols:


yield symbol

https://medium.datadriveninvestor.com/empower-automated-stock-trading-with-pattern-recognition-identify-channels-with-a-recursive-algo-55de… 4/13
2/4/22, 3:08 PM Empower automated stock trading with pattern recognition — Identify channels with a recursive algorithm | by J. K. Chang | D…

def getCurrentRange(self):
df = self.data.copy().loc[(self.data['row number'] >=
self.previousTrendlineBreakingPoint) & (self.data['row number'] <
self.currentTrendlineBreakingPoint), :]
return df

def trendlineBreakingPoints(self, symbol, flag, currentdf, slope,


intercept):

if flag == 'upper':

distortionThreshold = self.distortionThresholdUpper

if flag == 'lower':

distortionThreshold = self.distortionThresholdLower

possibleTrendBreakingPoints = currentdf.loc[(currentdf.loc[:,
(symbol, "close")] < (1-distortionThreshold)*(slope*currentdf['row
number'] + intercept)) | (currentdf.loc[:, (symbol, "close")] >
(1+distortionThreshold)*(slope*currentdf['row number'] + intercept)),
'row number']


return possibleTrendBreakingPoints

def trendlineForCurrentRange(self, symbol, flag, currentdf):


tempdf = currentdf.copy()

if flag == 'upper':

tempdf = tempdf.loc[(tempdf.loc[:, (symbol, "close")] >


slope * tempdf['row number'] + intercept)]

if flag == 'lower':

tempdf = tempdf.loc[(tempdf.loc[:, (symbol, "close")]<


slope * tempdf['row number'] + intercept)]

return slope, intercept


def refineTrendlineForCurrentRange(self, symbol, flag,


possibleTrendlineBreakingPoints):

if flag == 'upper':
validationWindow
= self.validationWindowUpper

if flag == 'lower':
validationWindow
= self.validationWindowLower

localPossibleTrendlineBreakingPoints =
possibleTrendlineBreakingPoints

i = 1

while len(localPossibleTrendlineBreakingPoints) > 0:


self.currentTrendlineBreakingPoint =

int(localPossibleTrendlineBreakingPoints[0])

https://medium.datadriveninvestor.com/empower-automated-stock-trading-with-pattern-recognition-identify-channels-with-a-recursive-algo-55de… 5/13
2/4/22, 3:08 PM Empower automated stock trading with pattern recognition — Identify channels with a recursive algorithm | by J. K. Chang | D…

if self.currentTrendlineBreakingPoint -
self.previousTrendlineBreakingPoint < validationWindow:

self.currentTrendlineBreakingPoint = len(self.data) + 1
- i

i += 1

currentdf = self.getCurrentRange()

slope, intercept = self.trendlineForCurrentRange(symbol,


flag, currentdf)

localPossibleTrendlineBreakingPoints =
self.trendlineBreakingPoints(symbol, flag, currentdf, slope, intercept)

self.refinedTrendlines[symbol][flag]
[str(self.previousTrendlineBreakingPoint)] = {'slope': slope,
'intercept': intercept,'starting row':
self.previousTrendlineBreakingPoint, 'ending row':
self.currentTrendlineBreakingPoint - 1}

self.previousTrendlineBreakingPoint =
self.currentTrendlineBreakingPoint

self.currentTrendlineBreakingPoint = len(self.data) + 1

def vizTrend(self):

fig, axs = plt.subplots(len(self.symbols))

pltCount = 0

for symbol, values in self.refinedTrendlines.items():


for flag, trendlines in values.items():

for period, trendline in trendlines.items():

tempAxis = axs[pltCount] if len(self.symbols) > 1


else axs

tempAxis.plot(self.data.index,self.data.loc[:,
(symbol, "close")], label=f'{symbol} price action')
tempAxis.plot(

self.data.index.values[trendline['starting row']:trendline['ending
row']],trendline['slope']*self.data['row number'][trendline['starting
row']:trendline['ending row']] + trendline['intercept'],
label=f'{self.data.index.values[trendline["starting row"]]}-
{self.data.index.values[trendline["ending row"]-1]}')

pltCount += 1

plt.show()

def runTrendlineSeeker(self, viz=True):

https://medium.datadriveninvestor.com/empower-automated-stock-trading-with-pattern-recognition-identify-channels-with-a-recursive-algo-55de… 6/13
2/4/22, 3:08 PM Empower automated stock trading with pattern recognition — Identify channels with a recursive algorithm | by J. K. Chang | D…

self.get_data()

for symbol in self.symbols:


self.refinedTrendlines[symbol] = {}

for flag in self.flag:


self.refinedTrendlines[symbol][flag] = {}

while True:

currentRange = self.getCurrentRange()

if len(currentRange) <= 2: break

trendline = self.trendlineForCurrentRange(symbol,
flag, currentRange)

possibleTrendlineBreakingPoints =
self.trendlineBreakingPoints(symbol, flag, currentRange, *trendline)

if len(possibleTrendlineBreakingPoints) == 0:
self.refinedTrendlines[symbol][flag]

[str(self.previousTrendlineBreakingPoint)] = {'slope': trendline[0],


'intercept': trendline[1], 'starting row':
self.previousTrendlineBreakingPoint, 'ending row':
self.currentTrendlineBreakingPoint - 1}
break

else:

self.refineTrendlineForCurrentRange(symbol,
flag, possibleTrendlineBreakingPoints)

self.previousTrendlineBreakingPoint = 0
self.currentTrendlineBreakingPoint = len(self.data) + 1

if viz: self.vizTrend()

The following  channelSeeker  class inherits the attributes from  trendlineSeeker  to return
all support and resistance trendlines. In the demo, the threshold s in the definition for
channel pattern above is  self.slopeDiffThreshold = 8*math.pi/180  . And the channel
breaking signal p is  self.channelBreakingThreshold = 0.1  . Therefore, when a pair of
support and resistance trends has slopes with less than 8 degrees difference and no
ticker point is above or below the current channel range, a channel is recorded and
visualized.

class channelSeeker(trendlineSeeker):

def __init__(self, symbols):


i i
https://medium.datadriveninvestor.com/empower-automated-stock-trading-with-pattern-recognition-identify-channels-with-a-recursive-algo-55de… 7/13
2/4/22, 3:08 PM Empower automated stock trading with pattern recognition — Identify channels with a recursive algorithm | by J. K. Chang | D…

flag='all')
super().__init__(symbols,
self.channelValidationWindow = 48

self.slopeDiffThreshold = 8*math.pi/180
self.channelBreakingThreshold = 0.1

self.channels = {}

def runChannelSeeker(self, viz=False):


self.runTrendlineSeeker(viz=False)

trendlines = self.refinedTrendlines

for symbol in self.symbols:


self.channels[symbol] =
{}

upperTrends = trendlines[symbol]['upper']
lowerTrends = trendlines[symbol]['lower']

for ku, vu in upperTrends.items():



= vu['slope'],
uSlope, uIntercept, uStart, uEnd
vu['intercept'], vu['starting row'], vu['ending row']
for kl, vl in lowerTrends.items():

lSlope, lIntercept, lStart, lEnd = vl['slope'],


vl['intercept'], vl['starting row'], vl['ending row']

if (np.abs(uSlope-lSlope) <
self.slopeDiffThreshold) and (uIntercept-lIntercept > 0) and
range(max(uStart, lStart), min(uEnd, lEnd)+1):

start = np.min((uStart, lStart))


end = np.max((uEnd, lEnd))

upperViolation =
self.channelBreakingPoints(symbol, uSlope, uIntercept, start, end,
self.channelBreakingThreshold, flag='upper')
lowerViolation =

self.channelBreakingPoints(symbol, lSlope, lIntercept, start, end,


self.channelBreakingThreshold, flag='lower')

if upperViolation.any() or lowerViolation.any():
continue

else:

channelId = f'{start}'

self.channels[symbol][channelId] = {}

self.channels[symbol][channelId]['start'] =
start

self.channels[symbol][channelId]['end'] =
end

self.channels[symbol][channelId]
['slopeUpper'] = uSlope

self.channels[symbol][channelId]
['slopeLower'] = lSlope

self.channels[symbol][channelId]
['interceptUpper'] = uIntercept

self.channels[symbol][channelId]
['interceptLower'] = lIntercept

https://medium.datadriveninvestor.com/empower-automated-stock-trading-with-pattern-recognition-identify-channels-with-a-recursive-algo-55de… 8/13
2/4/22, 3:08 PM Empower automated stock trading with pattern recognition — Identify channels with a recursive algorithm | by J. K. Chang | D…

else:

continue

if viz == True:

print(self.channels)

self.vizChannel()

def channelBreakingPoints(self, symbol, slope, intercept, start,


end, tolerance, flag):

if flag == 'upper':

violationCondition = self.data.loc[:,
(symbol,'close')].iloc[start:end] > (1 + tolerance)*
(slope*self.data['row number'].iloc[start:end]+1.0*intercept)

if flag == 'lower':

violationCondition = self.data.loc[:,
(symbol,'close')].iloc[start:end] < (1 - tolerance)*
(slope*self.data['row number'].iloc[start:end]+1.0*intercept)

print(self.data.loc[:, (symbol, 'close')].iloc[start:end]-(1 -


tolerance)*(slope*self.data['row
number'].iloc[start:end]+1.0*intercept))

return self.data.iloc[start:end].loc[violationCondition, 'row


number']

def vizChannel(self):

fig, axs = plt.subplots(len(self.symbols))

pltCount = 0

for symbol, values in self.channels.items():


tempAxis = axs[pltCount] if len(self.symbols) > 1 else axs

tempAxis.plot(self.data.index,self.data.loc[:, (symbol,
"close")], label=f'{symbol} price action')

for channelId, channelProperties in values.items():


tempAxis.plot(

self.data.index.values[channelProperties['start']:channelProperties['end']
channelProperties['slopeUpper']*self.data['row number']
[channelProperties['start']:channelProperties['end']] +
channelProperties['interceptUpper'])

tempAxis.plot(

self.data.index.values[channelProperties['start']:channelProperties['end']
number'][channelProperties['start']:channelProperties['end']] +
channelProperties['interceptLower'])

pltCount += 1

plt.show()
https://medium.datadriveninvestor.com/empower-automated-stock-trading-with-pattern-recognition-identify-channels-with-a-recursive-algo-55de… 9/13
2/4/22, 3:08 PM Empower automated stock trading with pattern recognition — Identify channels with a recursive algorithm | by J. K. Chang | D…

For the sake of completeness, the fully functional code structure, including the
modified  trendlineSeeker  class is included below.

import math

from scipy import stats


import scipy as sp

import pandas as pd
import numpy as np

import matplotlib.pyplot as plt

class trendlineSeeker:


...

class channelSeeker:

...

if __name__ == '__main__':

# ts = trendlineSeeker(['AAPL'], flag='all')
# ts.runTrendlineSeeker(viz = True)

cs = channelSeeker(['AAPL'])

cs.runChannelSeeker(viz=True)

Test the code on AAPL


The code is tested on AAPL. The data range is from 2019–08–29 to 2021–08–21. There
was a 4 to 1 split around 2019–08–25. To add complexity, the unadjusted OHLCV data
remains unchanged.

Sample records as the output of previous code will be in the following format.

{'AAPL': {'0': {'start': 0, 'end': 122, 'slopeUpper':


1.067195290858726, 'slopeLower': 0.9481459459459458, 'interceptUpper':
214.24631024930744, 'interceptLower': 196.6427864864865}, '145':
{'start': 145, 'end': 236, 'slopeUpper': 1.6257741303282707,
'slopeLower': 1.6703351404632827, 'interceptUpper': 31.928829005389446,
'interceptLower': -10.78136684737973}, '254': {'start': 254, 'end':
281, 'slopeUpper': -0.7069965576592079, 'slopeLower':
https://medium.datadriveninvestor.com/empower-automated-stock-trading-with-pattern-recognition-identify-channels-with-a-recursive-algo-55d… 10/13
2/4/22, 3:08 PM Empower automated stock trading with pattern recognition — Identify channels with a recursive algorithm | by J. K. Chang | D…

-0.7158695652173923, 'interceptUpper': 313.3540791738381,


'interceptLower': 299.3216956521742}}}

And the visualization is illustrated below.

AAPL channel with channel breaking threshold = 10%, slope divergence threshold = 8˚

Please note that the recognized channels changes along with the parameters in the
generalized definition of trend and channel patterns. After adding more relaxations, i.e.
increasing the thresholds  distortionThresholdUpper -> 0.20, s -> 15*math.pi/180, p ->

0.2  , more support and resistance pairs meet the criteria as shown below.

https://medium.datadriveninvestor.com/empower-automated-stock-trading-with-pattern-recognition-identify-channels-with-a-recursive-algo-55d… 11/13
2/4/22, 3:08 PM Empower automated stock trading with pattern recognition — Identify channels with a recursive algorithm | by J. K. Chang | D…

AAPL channel with channel breaking threshold = 20%, slope divergence threshold = 15˚

Conclusion
This application of the recursive global approach is able to capture significant channels
in trading data. With a different set of thresholds, certain channels will be included or
excluded. Since the outputs of this application can be used further as inputs of another
learning algorithm, these thresholds can be optimized as parameters while training these
learning algorithms.

The major advantage of this application is the inherited recursiveness from the algorithm
and the added objectiveness while defining the channel pattern. They help tackle the
aforementioned two major challenges, i.e. data set generation and human biases in data
set.

The main disadvantage would be the lacking of human expertise on special occasions. In
a real-world scenario, this could mean slightly lower trading frequency or a few trades
less. However, this is not necessarily negative on the trading result since this
disadvantage, on the other side, means only patterns within control are captured and
considered in a trading strategy. Also combining multiple patterns or with indicators can
be a mediation.
https://medium.datadriveninvestor.com/empower-automated-stock-trading-with-pattern-recognition-identify-channels-with-a-recursive-algo-55d… 12/13
2/4/22, 3:08 PM Empower automated stock trading with pattern recognition — Identify channels with a recursive algorithm | by J. K. Chang | D…

Endnote
Trendlines and channels are just two types among all patterns. Moreover, pattern
recognition is merely one of many options/indications while creating a trading strategy.
Long story short, quite a journey ahead towards an automated trading system.

In my next article, instead of bumping into more technical details in patterns, indicators
or system software development, I will first write a guideline for designing a trading
system. It will introduce all the elements in an automated trading system and how they
are integrated together for designing trading strategies.

If you have any questions or any specific topics in your mind, feel free to leave a
comment.

To see more, follow and stay in tune.

https://medium.datadriveninvestor.com/empower-automated-stock-trading-with-pattern-recognition-identify-channels-with-a-recursive-algo-55d… 13/13

You might also like