Back to Community
Candlestick Patterns

I'm surprised no one built out a candlestick algorithm. For those who aren't familiar with candlesticks, it's a bit of a hokey analysis that looks at patterns between the open, close, high and low price of a stock at some arbitrary time interval to determine whether the stock is likely to go up or down during the next period.

In my opinion, its popularity comes from the fact that the analysis is subjective enough to claim a kind of accuracy that is difficult to challenge empirically. Also, it produces pretty graphs.

I replicated candle stick analysis in the algorithm below. I boiled down the patterns to 4 factors.
1. Whether the stock ended higher or lower at end of day
2. Relative size of the high wick
3. How big the body is
4. Relative size of the low wick

The patterns I used for matching are the patterns indicated in the website below. The algorithm is setup in such a way that these patterns can easily be changed or new patterns added.

http://www.swing-trade-stocks.com/candlestick-patterns.html

The inputs required, apart from the patterns you're looking to match, is what you consider a big or small body and wick size. I was thinking about putting in some relative measure (5% movement in a day is large?) but chose to keep it simple with an absolute value instead.

Obviously the results are less than impressive but if someone really believes in candle stick analysis, that person can easily build on this. Also, this can pretty easily be expanded to analyze candles and trade multiple stocks.

Does anyone know of a way to plot candlesticks on a graph as an addendum to the graph produced?

Clone Algorithm
288
Loading...
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
Returns 1 Month 3 Month 6 Month 12 Month
Alpha 1 Month 3 Month 6 Month 12 Month
Beta 1 Month 3 Month 6 Month 12 Month
Sharpe 1 Month 3 Month 6 Month 12 Month
Sortino 1 Month 3 Month 6 Month 12 Month
Volatility 1 Month 3 Month 6 Month 12 Month
Max Drawdown 1 Month 3 Month 6 Month 12 Month
class Candle(object):
    # size definitions that determine if a body/wick is big, small or medium
    size_definitions={    "body": {"big":15.0, "small":5.0},
                      "wick": {"big":5.0, "small":2.0}}
    
    def __init__(self, open_px, close_px, hi, lo):
        self.direction = self.get_direction(open_px, close_px)
        self.hi_wick_size = self.get_object_size("wick",hi,max(open_px,close_px))
        self.body_size = self.get_object_size("body",open_px,close_px)
        self.lo_wick_size = self.get_object_size("wick",lo,min(open_px,close_px))
        self.candle =  (self.direction, self.hi_wick_size, self.body_size, self.lo_wick_size) 
    def __eq__(self,other):
        """ candles are equal to each other if direction, hi_wick_size, body_size and lo_wick_size are equal
            None attributes are considered a match """
        return ((self.direction == other.direction or not self.direction or not other.direction) and 
                (self.body_size == other.body_size or not self.body_size or not other.body_size)  and
                (self.lo_wick_size == other.lo_wick_size or not self.lo_wick_size or not other.lo_wick_size)  and
                (self.hi_wick_size == other.hi_wick_size or not self.hi_wick_size or not other.hi_wick_size))
    def __hash__(self):
        return hash(self.candle) 
    def __str__(self):
        return str(self.candle)
    def __len__(self):
        return len(self.candle)
    def __getitem__(self,index):
        return self.candle[index]
    def get_direction(self, open_px, close_px):
        if close_px > open_px: return "up"
        return "down"
    def get_object_size(self, object_type, top, bottom):
        body_length = abs(top-bottom)
        if body_length > self.size_definitions[object_type]["big"]:
            return "big"
        elif body_length < self.size_definitions[object_type]["small"]:
            return "small"
        else:
            return "medium"

class Pattern(Candle):
    def __init__ ( self, direction = None, hi_wick_size = None, body_size = None, lo_wick_size = None):
        self.direction = direction
        self.hi_wick_size = hi_wick_size
        self.body_size = body_size
        self.lo_wick_size = lo_wick_size
        self.candle =  (self.direction, self.hi_wick_size,self.body_size, self.lo_wick_size) 
               
def initialize(context):
    # pattern names and signal
    context.match_pattern_names = {"engulfing-up": "bull",
                             "hammer": "bull",
                             "harami-up": "bull",
                             "piercing": "bull",
                             "bullish kicker": "bull",
                             "engulfing-down": "bear",
                             "shooting star": "bear",
                             "harami-down": "bear",
                             "dark cloud cover": "bear",
                             "bearish kicker": "bear"}
    # match patterns and names
    context.match_patterns = {1:{
                                Pattern(direction="up",hi_wick_size="small",body_size="small",lo_wick_size="big"): "hammer",
                                Pattern(direction="down",hi_wick_size="big",body_size="small",lo_wick_size="small"): "shooting star",
                       },2: 
                               {Pattern(direction="down",body_size="small"):{
                                    Pattern(direction="up",body_size="big"): "engulfing-up"},
                                Pattern(direction="down",body_size="big"):{
                                    Pattern(direction="up",body_size="small"): "harami-up",
                                    Pattern(direction="up",body_size="big"): "piercing"},
                                Pattern(direction="up",body_size="small"):{
                                    Pattern(direction="down",body_size="big"):"engulfing-down"},
                                Pattern(direction="up",body_size="big"):{
                                    Pattern(direction="down",body_size="small"): "harami-down",
                                    Pattern(direction="down",body_size="big"): "dark cloud cover"}
                        },4: 
                            {Pattern(direction="down",body_size="big"):{
                                Pattern(direction="down",body_size="big"):{
                                  Pattern(direction="down",body_size="big"):{
                                    Pattern(direction="up",body_size="big"):"bullish kicker"}}},
                                Pattern(direction="up",body_size="big"):{
                                 Pattern(direction="up",body_size="big"):{
                                   Pattern(direction="up",body_size="big"):{
                                     Pattern(direction="down", body_size="big"):"bearish kicker"}}}
                              }
                        }
    context.max_patterns = max(context.match_patterns) 
    context.stock = sid(24)            # AAPL
    context.candle_patterns = []       # stores the last several candle patterns

def handle_data(context, data):
    
    # determine candle
    open_px=data[context.stock].open_price
    close_px=data[context.stock].close_price
    hi=data[context.stock].high
    lo=data[context.stock].low
    price=data[context.stock].price
    candle=Candle(open_px,close_px,hi,lo)
    
    # update historical patterns
    update_candle_patterns(context,candle)
    
    # close any open positions
    cur_amount = context.portfolio.positions[context.stock].amount
    if cur_amount != 0:
        if cur_amount < 0:
            log.info("cover @ " + str(price))
        else:
            log.info("sell @ " + str(price))
        order(context.stock, -1 * cur_amount)
        
    # match patterns
    signal = match_patterns(context)
    
    # determine trade size
    trade_size = context.portfolio.cash/price
    
    # put in trade order
    if signal == "bull":
        log.info("buy @ " + str(price))
        order(context.stock, trade_size)
    elif signal == "bear":
        log.info("short @ " + str(price))
        order(context.stock, -1 * trade_size)

    
def update_candle_patterns(context,candle):
    """ updates the candle patterns based on the max amount of sticks analyzed
        first entry in context.candle_patterns is most recent """
    context.candle_patterns.insert(0, candle)
    if len(context.candle_patterns) > context.max_patterns: 
        context.candle_patterns = context.candle_patterns[0:context.max_patterns]
    
def match_patterns(context):
    """ looks for patterns based on context.match_patterns and context.candle_patterns
        yields "bull", "bear" or None """
    num_patterns = min(context.max_patterns, len(context.candle_patterns))
    for num_pattern in xrange(1,num_patterns+1):
        try:
            patterns = context.match_patterns[num_pattern]
        except KeyError:
            continue
        for candle in context.candle_patterns:
            match = get_match(patterns, candle)
            if match is None: 
                break            # no match
            patterns = patterns[match]
            if type(patterns) == str: 
                try: 
                    signal = context.match_pattern_names[patterns]
                    log.info(patterns)
                    return signal 
                except KeyError: 
                    break       # no match on match_pattern_names
    return None
    
def get_match(pattern_dict, candle):
    """ returns the first instance of a partial key match, ignoring None values 
        example: (1,None,3,None) matches (1,2,3,4) """
    for pattern in pattern_dict.iterkeys():
        match = True
        for i in xrange(len(candle)):
            if candle[i] is not None and pattern[i] is not None and candle[i] != pattern[i]:
                match = False
                break
        if match is True:
            return pattern
    return None
    
    
This backtest was created using an older version of the backtester. Please re-run this backtest to see results using the latest backtester. Learn more about the recent changes.
There was a runtime error.
7 responses

Hello Branko,

I have played a little with plotting candle charts with matplotlib (see below) but this is not possible in Quantopian. See http://matplotlib.org/examples/pylab_examples/finance_demo.html for more information.

P.

Seems like Quantopian is now blocking use of eq, hash, len, and getitem. Any suggestions for what to replace these with?

ta-lib has candlestick pattern recognition indicators:

CDL2CROWS Two Crows
CDL3BLACKCROWS Three Black Crows
CDL3INSIDE Three Inside Up/Down
CDL3LINESTRIKE Three-Line Strike
CDL3OUTSIDE Three Outside Up/Down
CDL3STARSINSOUTH Three Stars In The South
CDL3WHITESOLDIERS Three Advancing White Soldiers
CDLABANDONEDBABY Abandoned Baby
CDLADVANCEBLOCK Advance Block
CDLBELTHOLD Belt-hold
CDLBREAKAWAY Breakaway
CDLCLOSINGMARUBOZU Closing Marubozu
CDLCONCEALBABYSWALL Concealing Baby Swallow
CDLCOUNTERATTACK Counterattack
CDLDARKCLOUDCOVER Dark Cloud Cover
CDLDOJI Doji
CDLDOJISTAR Doji Star
CDLDRAGONFLYDOJI Dragonfly Doji
CDLENGULFING Engulfing Pattern
CDLEVENINGDOJISTAR Evening Doji Star
CDLEVENINGSTAR Evening Star
CDLGAPSIDESIDEWHITE Up/Down-gap side-by-side white lines
CDLGRAVESTONEDOJI Gravestone Doji
CDLHAMMER Hammer
CDLHANGINGMAN Hanging Man
CDLHARAMI Harami Pattern
CDLHARAMICROSS Harami Cross Pattern
CDLHIGHWAVE High-Wave Candle
CDLHIKKAKE Hikkake Pattern
CDLHIKKAKEMOD Modified Hikkake Pattern
CDLHOMINGPIGEON Homing Pigeon
CDLIDENTICAL3CROWS Identical Three Crows
CDLINNECK In-Neck Pattern
CDLINVERTEDHAMMER Inverted Hammer
CDLKICKING Kicking
CDLKICKINGBYLENGTH Kicking - bull/bear determined by the longer marubozu
CDLLADDERBOTTOM Ladder Bottom
CDLLONGLEGGEDDOJI Long Legged Doji
CDLLONGLINE Long Line Candle
CDLMARUBOZU Marubozu
CDLMATCHINGLOW Matching Low
CDLMATHOLD Mat Hold
CDLMORNINGDOJISTAR Morning Doji Star
CDLMORNINGSTAR Morning Star
CDLONNECK On-Neck Pattern
CDLPIERCING Piercing Pattern
CDLRICKSHAWMAN Rickshaw Man
CDLRISEFALL3METHODS Rising/Falling Three Methods
CDLSEPARATINGLINES Separating Lines
CDLSHOOTINGSTAR Shooting Star
CDLSHORTLINE Short Line Candle
CDLSPINNINGTOP Spinning Top
CDLSTALLEDPATTERN Stalled Pattern
CDLSTICKSANDWICH Stick Sandwich
CDLTAKURI Takuri (Dragonfly Doji with very long lower shadow)
CDLTASUKIGAP Tasuki Gap
CDLTHRUSTING Thrusting Pattern
CDLTRISTAR Tristar Pattern
CDLUNIQUE3RIVER Unique 3 River
CDLUPSIDEGAP2CROWS Upside Gap Two Crows
CDLXSIDEGAP3METHODS Upside/Downside Gap Three Method

http://ta-lib.org/function.html

Great, thank you!

Hi,
I personally have been struggling to understand how exactly TA Lib defines those patterns.
Here I take the exemple of Dojis, but this can be replicated to all other indicators

From this TA-Lib Doji definition and the TA-Lib settings, it appears the definitions are often linked to the ten last candles

    const TA_CandleSetting TA_CandleDefaultSettings[] = {  
        /* real body is long when it's longer than the average of the 10 previous candles' real body */  
        { TA_BodyLong, TA_RangeType_RealBody, 10, 1.0 },  
        /* real body is very long when it's longer than 3 times the average of the 10 previous candles' real body */  
        { TA_BodyVeryLong, TA_RangeType_RealBody, 10, 3.0 },  
        /* real body is short when it's shorter than the average of the 10 previous candles' real bodies */  
        { TA_BodyShort, TA_RangeType_RealBody, 10, 1.0 },  
        /* real body is like doji's body when it's shorter than 10% the average of the 10 previous candles' high-low range */  
        { TA_BodyDoji, TA_RangeType_HighLow, 10, 0.1 },  
        /* shadow is long when it's longer than the real body */  
        { TA_ShadowLong, TA_RangeType_RealBody, 0, 1.0 },  
        /* shadow is very long when it's longer than 2 times the real body */  
        { TA_ShadowVeryLong, TA_RangeType_RealBody, 0, 2.0 },  
        /* shadow is short when it's shorter than half the average of the 10 previous candles' sum of shadows */  
        { TA_ShadowShort, TA_RangeType_Shadows, 10, 1.0 },  
        /* shadow is very short when it's shorter than 10% the average of the 10 previous candles' high-low range */  
        { TA_ShadowVeryShort, TA_RangeType_HighLow, 10, 0.1 },  
        /* when measuring distance between parts of candles or width of gaps */  
        /* "near" means "<= 20% of the average of the 5 previous candles' high-low range" */  
        { TA_Near, TA_RangeType_HighLow, 5, 0.2 },  
        /* when measuring distance between parts of candles or width of gaps */  
        /* "far" means ">= 60% of the average of the 5 previous candles' high-low range" */  
        { TA_Far, TA_RangeType_HighLow, 5, 0.6 },  
        /* when measuring distance between parts of candles or width of gaps */  
        /* "equal" means "<= 5% of the average of the 5 previous candles' high-low range" */  
        { TA_Equal, TA_RangeType_HighLow, 5, 0.05 }  
    };

so for a Doji should be defined with body below 10% of the average of the last 10 high-low ranges, but when I test it it seems only to recognize dojis as figures where body(t-1) = body(t) (true equality, and not 'more or less equal').

In addition to this I wonder whether a more "correct" definition would not be an absolute figure (perhaps calibrated on percentiles from the whole sample, not only the last 10)

Hi,
I personally have been struggling to understand how exactly TA Lib defines those patterns.
Here I take the exemple of Dojis, but this can be replicated to all other indicators

From this TA-Lib Doji definition and the TA-Lib settings, it appears the definitions are often linked to the ten last candles

    const TA_CandleSetting TA_CandleDefaultSettings[] = {  
        /* real body is long when it's longer than the average of the 10 previous candles' real body */  
        { TA_BodyLong, TA_RangeType_RealBody, 10, 1.0 },  
        /* real body is very long when it's longer than 3 times the average of the 10 previous candles' real body */  
        { TA_BodyVeryLong, TA_RangeType_RealBody, 10, 3.0 },  
        /* real body is short when it's shorter than the average of the 10 previous candles' real bodies */  
        { TA_BodyShort, TA_RangeType_RealBody, 10, 1.0 },  
        /* real body is like doji's body when it's shorter than 10% the average of the 10 previous candles' high-low range */  
        { TA_BodyDoji, TA_RangeType_HighLow, 10, 0.1 },  
        /* shadow is long when it's longer than the real body */  
        { TA_ShadowLong, TA_RangeType_RealBody, 0, 1.0 },  
        /* shadow is very long when it's longer than 2 times the real body */  
        { TA_ShadowVeryLong, TA_RangeType_RealBody, 0, 2.0 },  
        /* shadow is short when it's shorter than half the average of the 10 previous candles' sum of shadows */  
        { TA_ShadowShort, TA_RangeType_Shadows, 10, 1.0 },  
        /* shadow is very short when it's shorter than 10% the average of the 10 previous candles' high-low range */  
        { TA_ShadowVeryShort, TA_RangeType_HighLow, 10, 0.1 },  
        /* when measuring distance between parts of candles or width of gaps */  
        /* "near" means "<= 20% of the average of the 5 previous candles' high-low range" */  
        { TA_Near, TA_RangeType_HighLow, 5, 0.2 },  
        /* when measuring distance between parts of candles or width of gaps */  
        /* "far" means ">= 60% of the average of the 5 previous candles' high-low range" */  
        { TA_Far, TA_RangeType_HighLow, 5, 0.6 },  
        /* when measuring distance between parts of candles or width of gaps */  
        /* "equal" means "<= 5% of the average of the 5 previous candles' high-low range" */  
        { TA_Equal, TA_RangeType_HighLow, 5, 0.05 }  
    };

so for a Doji should be defined with body below 10% of the average of the last 10 high-low ranges, but when I test it it seems only to recognize dojis as figures where body(t-1) = body(t) (true equality, and not 'more or less equal').

In addition to this I wonder whether a more "correct" definition would not be an absolute figure (perhaps calibrated on percentiles from the whole sample, not only the last 10)

@ Jean-Baptiste Lepetit
I personally do found that majorities of the candlesticks pattern I tested on were incorrect.
I left my comments on Ta-Lib github and waiting some feedback. If you have the same issues, please feel free to add your comments.
https://github.com/mrjbq7/ta-lib/issues/119
I found that some bitcoin back testing also using Talib for candlestick. If they do not modify the code, I doubt how come there is no people complaining about it?