Algorithmic trading based on Technical Analysis in Python | by Eryk Lewinson | Towards Data Science

This is the fourth part of a series of articles on backtesting trading strategies in Python. The previous ones described the following topics:
introducing the zipline framework and presenting how to test basic strategies (link)
importing custom data to use with zipline (link)
evaluating the performance of trading strategies (link)
This time, the goal of the article is to show how to create trading strategies based on Technical Analysis (TA in short). Quoting Wikipedia, technical analysis is a “methodology for forecasting the direction of prices through the study of past market data, primarily price, and volume”.
In this article, I show how to use a popular Python library for calculating TA indicators — TA-Lib — together with the zipline backtesting framework. I will create 5 strategies and then investigate which one performs best over the investment horizon.
For this article I use the following libraries:
pyfolio    0.9.2numpy      1.14.6matplotlib 3.0.0pandas     0.22.0json       2.0.9empyrical  0.5.0zipline    1.3.0
Before creating the strategies, I define a few helper functions (here I only describe one of them, as it is the most important one affecting the backtests).
The function is used for getting the modified start date of the backtest. That is because I would like all the strategies to start working on the same day — the first day of 2016. However, some strategies based on technical indicators require a certain number of past observations — the so-called “warm-up period”. That is why using this function I calculate the date the backtest should start so that on the first day of the investment horizon I already have enough past observations to calculate the indicators. Please bear in mind that no trading decision can happen before the true start date of the backtest!
UPDATE: Actually there is no explicit need to use this approach. When we specify the bar_count in the data.history, zipline will automatically take the previous number of bars available, even when they are from a period before the backtest. I used this approach here.
In this article we use the following problem setting:
the investor has a capital of 10000$
the investment horizon covers years 2016–2017
the investor can only invest in Tesla’s stock
we assume no transactions costs — zero-commission trading
there is no short selling (the investor can only sell what he/she currently owns)
when the investor opens a position (buys the stock), the investor goes “all in” — allocates all available resources for the purchase
One of the reasons for selecting this range of dates is the fact that from mid-2018 the Quandl dataset was not updated and we want to keep the code as simple as possible. For details on how to load custom data (including the latest stock prices) into zipline, please refer to my previous article.
We start with the most basic strategy — Buy and Hold. The idea is that we buy a certain asset and do not do anything for the entire duration of the investment horizon. So at the first possible date, we buy as much Tesla stock as we can with our capital and do nothing later.
This simple strategy can also be considered a benchmark for more advanced ones — because there is no point in using a very complex strategy that generates less money (in general or due to transaction costs) than buying once and doing nothing.
We load the performance DataFrame:
buy_and_hold_results = pd.read_pickle('buy_and_hold.pkl')
What can happen here (it did not, but it is good to be aware of the possibility) is the sudden appearance of negative ending_cash. The reason for that could be the fact that the amount of shares we want to buy is calculated at the end of the day, using that day’s (closing) price. However, the order is executed on the next day, and the price can change significantly. In zipline the order is not rejected due to insufficient funds, but we can end up with a negative balance. This can happen mostly with strategies that go “all-in”. We could come up with some ways to avoid it — for example manually calculating the number of shares we can buy the next day and also including some markup to prevent such a situation from occurring, however, for simplicity we accept that this can happen.
We use a helper function to visualize some details of the strategy: the evolution of the portfolio’s value, the transactions on top of the price series, and the daily returns.
visualize_results(buy_and_hold_results, 'Buy and Hold Strategy - TSLA')
We also create the performance summary (using another helper function), which will be used in the last section:
buy_and_hold_perf = get_performance_summary(buy_and_hold_results.returns)
For brevity, we will not show all these steps (such as loading the performance DataFrame or getting the performance summary) for each strategy, because they are done in the same manner each time.
The second strategy we consider is based on the simple moving average (SMA). The logic of the strategy can be summarized by the following:
when the price crosses the 20-day SMA upwards — buy shares
when the price crosses the 20-day SMA downwards — sell all the shares
the moving average uses 19 prior days and the current day — the trading decision is for the next day
This is the first time we need to use the previously defined helper function to calculate the adjusted starting date, which will enable the investor to make trading decisions on the first trading day of 2016.
get_start_date('TSLA', '2016-01-04', 19)# '2015-12-04'
In the strategy below, we use the adjusted date as the start date.
Note: data.current(context.asset, ‘price’) is equivalent to price_history[-1].
Below we illustrate the strategy:
The plot below shows the price series together with the 20-day moving average. We have additionally marked the orders, which are executed on the next trading day after the signal was generated.
This strategy can be considered an extension of the previous one — instead of a single moving average, we use two averages of different window sizes. The 100-day moving average is the one that takes longer to adjust to sudden price changes, while the 20-day one is much faster to account for sudden changes.
The logic of the strategy is as follows:
when the fast MA crosses the slow one upwards, we buy the asset
when the slow MA crosses the fast one upwards, we sell the asset
Bear in mind that many different window-lengths combinations defining the fast and slow MA can be considered for this strategy.
For this strategy, we need to additionally take 100 days of data in order to counter the “warm-up period”.
Below we plotted the two moving averages on top of the price series. We see that the strategy generated much fewer signals than the one based on SMA.
MACD stands for Moving Average Convergence/Divergence and is an indicator/oscillator used in technical analysis of stock prices.
MACD is a collection of three time-series calculated using historical close prices:
the MACD series — the difference between the fast (shorter period) and slow (longer period) exponential moving averages
the signal — EMA on the MACD series
the divergence — the difference between the MACD series and the signal
MACD is parametrized by the number of days used to calculate the three moving averages — MACD(a,b,c). The parameter a corresponds to the fast EMA, b to the slow EMA, and c to the MACD signal EMA. The most common setup, also used in this article, is MACD(12,26,9). Historically, these numbers corresponded to 2 weeks, 1 month and 1.5 weeks based on a 6-day working week.
One thing to remember is that MACD is a lagging indicator, as it is based on moving averages. That is why the MACD is less useful for stocks that do not exhibit a trend or are trading with erratic price action.
The strategy we use in this article can be described by:
buy shares when the MACD crosses the signal line upwards
sell shares when the MACD crosses the signal line downwards
As before, to counter the warm-up period we need to ascertain that we have 34 observations to calculate the MACD.
Below we plot the MACD and the signal lines, where the crossovers indicate buy/sell signals. Additionally, one could plot the MACD divergence in the form of a barplot (it is commonly referred to as the MACD histogram).
RSI stands for the Relative Strength Index, which is another technical indicator we can use to create trading strategies. The RSI is classified as a momentum oscillator and it measures the velocity and magnitude of directional price movements. Momentum describes the rate at which the price of the asset rises or falls.
Without going into too many technical details, the RSI measures momentum as the ratio of higher closes to lower closes. Assets with more/stronger positive changes have higher RSI than assets with more/stronger negative changes.
The output of the RSI is a number on a scale from 0 to 100 and it is typically calculated on a 14-day basis. To generate the trading signals, it is common to specify the low and high levels of the RSI at 30 and 70, respectively. The interpretation of the thresholds is that the lower one indicates that the asset is oversold, and the upper one that the asset is overbought.
Sometimes, a medium level (halfway between low and high) is also specified, for example in case of strategies which also allow for short-selling. We can also select more extreme thresholds such as 20 and 80, which would then indicate stronger momentum. However, this should be specified using domain knowledge or by running backtests.
The strategy we consider can be described as:
when the RSI crosses the lower threshold (30) — buy the asset
when the RSI crosses the upper threshold (70) — sell the asset
Below we plot the RSI together with the upper and lower threshold.
The last step involves putting all the performance metrics into one DataFrame and inspecting the results. We can see that in the case of our backtest, the strategy based on the simple moving average performed best in terms of generated returns. It also had the highest Sharpe ratio — the highest excess return (in this case return, as we do not consider a risk-free asset) per unit of risk. The second-best strategy turned out to be the one based on the MACD. It is also good to notice that only these two performed better than the benchmark buy and hold strategy.
perf_df = pd.DataFrame({'Buy and Hold': buy_and_hold_perf,                        'Simple Moving Average': sma_perf,                        'Moving Average Crossover': mac_perf,                        'MACD': macd_perf,                        'RSI': rsi_perf})perf_df.transpose()
In this short article, I showed how to combine zipline with talib in order to backtest trading strategies based on popular technical indicators such as moving averages, the MACD, the RSI, etc. But this was only the beginning, as it is possible to create much more sophisticated strategies.
Some of the possible future directions:
include multiple assets into the strategies
allow for short-selling
mix the indicators
backtest different parameters for each strategy to find the best-performing ones
We must also remember that the fact that the strategy performed well in the past is no guarantee that this will happen again in the future.
As always, any constructive feedback is welcome. You can reach out to me on Twitter or in the comments. You can find the code used for this article on my GitHub.
Liked the article? Become a Medium member to continue learning by reading without limits. If you use this link to become a member, you will support me at no extra cost to you. Thanks in advance and see you around!
Below you can find the other articles in the series:
building algorithmic trading strategies based on the mean-variance analysis (link)
I recently published a book on using Python for solving practical tasks in the financial domain. If you are interested, I posted an article introducing the contents of the book. You can get the book on Amazon or Packt’s website.