Backtesting Candlestick Patterns in Python

Backtesting Candlestick Patterns in Python

One may be interested in using candlestick patterns in their trading strategies. Thanks to libraries like TA-lib, we can automatically detect all of the most common patterns and hence automate our backtesting so we can test out more strategies without the legwork of manually labelling the candles.

Preparing our price data

If you have your own data source you can skip straight to the next section. Just make sure you have full OHLC data, otherwise TA-lib won't be able to construct the candles or recognise any patterns.

I'll be using yfinance here to quickly scrape some crypto price data

 1import pandas as pd
 2import yfinance as yf
 3from datetime import datetime, timedelta
 4
 5end_time = datetime.now()
 6start_time = end_time - timedelta(days = 5)
 7
 8# Yfinance doesn't have great minute data
 9data = yf.download(
10        "BTC-USD", 
11        start=start_time, 
12        end=end_time,
13        interval="1h")
14
15print(data)

That should leave you with a pandas dataframe full of OHLCV data. You can change the value in timedelta to get a different amount of data. The smaller timeframe that you choose, the less price history you can request. For hourly data you can request about a month's data.

If you need to scrape a significant amount of historical data, you can check out my article on scraping from Bitstamp.

Detecting Candlestick Patterns

Once we've got our OHLC data, detecting common patterns is a one-liner in TA-lib

1import talib
2hammer = talib.CDLHAMMER(data.Open, data.High, data.Low, data.Close)

In this case we're looking for a hammer pattern. This (supposedly) indicates a bullish reversal, as you can see in the bottom-most green candle on the thumbnail of this article. You can find a big list of all the candlestick patterns and their respective TA-lib functions on the TA-lib github page.

TA-lib very much can be a pain to install. Before installing with pip, you'll want to make sure that the underlying C libraries are installed. You can find instructions on the TA-lib page under dependencies

If you print out hammer you'll get a pandas series that indicates whether the pattern has been detected

 12022-07-09 19:00:00+00:00    0
 22022-07-09 20:00:00+00:00    0
 32022-07-09 21:00:00+00:00    0
 42022-07-09 22:00:00+00:00    0
 52022-07-09 23:00:00+00:00    0
 6                            ..
 72022-07-14 16:00:00+00:00    0
 82022-07-14 17:00:00+00:00    0
 92022-07-14 18:00:00+00:00    0
102022-07-14 19:00:00+00:00    0
112022-07-14 19:06:00+00:00    0

You'll notice here all of the entries are 0. To make sure it's working we can look for entries where the value is equal to 100.

1print(hammer[hammer == 100])
 12022-07-10 11:00:00+00:00    100
 22022-07-11 00:00:00+00:00    100
 32022-07-11 02:00:00+00:00    100
 42022-07-11 23:00:00+00:00    100
 52022-07-12 17:00:00+00:00    100
 62022-07-13 10:00:00+00:00    100
 72022-07-13 16:00:00+00:00    100
 82022-07-14 09:00:00+00:00    100
 92022-07-14 11:00:00+00:00    100
10dtype: int32
11Series([], dtype: bool)

If you want to find the range of values that can be returned by a TA-lib function, as well as its inputs, you can open up a regular python shell and try the following:

1Python 3.8.10 (default, Jun 22 2022, 20:18:18) 
2[GCC 9.4.0] on linux
3Type "help", "copyright", "credits" or "license" for more information.
4>>> import talib
5>>> help(talib.CDLHAMMER)
 1Help on function CDLHAMMER in module talib._ta_lib:
 2
 3CDLHAMMER(...)
 4    CDLHAMMER(open, high, low, close)
 5    
 6    Hammer (Pattern Recognition)
 7    
 8    Inputs:
 9        prices: ['open', 'high', 'low', 'close']
10    Outputs:
11        integer (values are -100, 0 or 100)

From experimentation, I've found that a value of 100 represents a bullish signal, -100 a bearish signal, and 0 neutral.

Backtesting the strategy

I'm also going to grab all the instances of the hanging man pattern to use as our sell signal. It's very similar to the hammer and is the bearish equivalent.

1hanging_man =talib.CDLHANGINGMAN(data.Open, data.High, data.Low, data.Close)

From here we can construct our buys and sells series, which we can use to feed into VectorBt.

1buys = hammer == 100 
2sells = hanging_man == -100
3pf = vbt.Portfolio.from_signals(data.Close, buys, sells, fees = 0.005)

The syntax might look a little weird there but we just want to convert our numerical value from our series into True/False values that VectorBt can accept. Make sure to import vectorbt as vbt.

At this point you can get some stats from the backtest by printing out pf.stats(), or you can plot what we've got so far with pf.plot().

You can also print out the trade records in an easy to read format

1trade_records = pf.trades.records_readable
2print(trade_records.sort_values("Entry Timestamp").to_string())

This is great for checking that order execution is as you expect. So that's everything you need to know to get started backtesting candlestick pattern based strategies. Everything from here is just composing what we've learned here with some basic pandas knowledge.

Video tutorial

If you'd prefer a video tutorial, check out my youtube channel: