The easiest way to evaluate the performance of trading strategies in Python | by Eryk Lewinson | Towards Data Science

This is the third 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)
This time, the goal of the article is to show how to quickly and efficiently evaluate the performance of our strategies using a library called pyfolio (developed by Quantopian, the creators of zipline). pyfolio can be used as a standalone library and provide performance results based only on a provided series of returns. However, it works efficiently with zipline and I present this combination in this article.
In this article, I only show a basic strategy, as the main focus is on evaluating the performance. For that purpose, I chose a strategy 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 20 shares
when the price crosses the 20-day SMA downwards — sell all the shares
we can only have a maximum of 20 shares at any given time
there is no short-selling in the strategy
I run a backtest of the strategy using IBM’s stock over the years 2016–2017. For details on how to create such a strategy and what do the particular components actually do, please refer to my previous article.
I start by loading the required libraries:
import numpy as npimport matplotlib.pyplot as pltimport pandas as pdimport ziplineimport warningsimport pyfolio as pf
Then, I run the strategy:
zipline automatically creates a performance DataFrame, which you can also see in the output of the code. For convenience, I stored the output in a pickle file called simple_moving_average.pkl. To make the analysis as smooth as possible, we can use a utility function provided by pyfolio and load the 3 most important elements of the performance DataFrame — the returns, positions, and transactions. To do so, I use pf.utils.extract_rets_pos_txn_from_zipline.
sma_results = pd.read_pickle('simple_moving_average.pkl')returns, positions, transactions = pf.utils.extract_rets_pos_txn_from_zipline(sma_results)
We can now inspect the extracted elements:
returns.head()
There could be no returns over the first 20 trading days of 2016, as it is still the warm-up period for the moving average.
positions.head()
The positions DataFrame contains entries for each day when we do have a position in the considered assets and shows the capital split between equities and cash. We can see that by buying 20 IBM shares we still keep the majority of the capital in cash. For more advanced strategies, we can allocate a percentage of the capital to each order by using order_percent.
transactions.head()
The transactions DataFrame contains all the transactions executed by the trading strategy — we see both buy and sell orders.
To evaluate the performance of strategies, portfolios or even single assets, we use pyfolio to create a tear sheet. A tear sheet is a concise document (often a single-paged one) that contains the most important information — such as financial metrics — regarding a public company. pyfolio provides much more functionalities than can be contained on a single sheet of paper, but for simplicity, we start with a simple tear sheet, which contains only the most vital information.
To create it, we run the following:
pf.create_simple_tear_sheet(returns)
What is truly remarkable about `pyfolio` is that it generates so much information with a single line of code!
The first table we see presents the dates of the test, how many months it lasted and a lot of financial metrics such as:
annualized returns/standard deviation
skewness — the third moment describes how skewed is the distribution
kurtosis — the fourth moment indicates if there is more mass in the tails of the distribution
Sharpe ratio — a very popular risk metric. It indicates the amount of excess return (over the risk-free rate) per unit of risk (measured by standard deviation).
Sortino ratio — a modified version of the Sharpe ratio, where the standard deviation is replaced by downside deviation. Downside deviation measures only the negative volatility of the series, strictly speaking below a predefined level called minimum acceptable return.
Omega ratio — another kind of risk-return performance metric. Its biggest advantage over the Sharpe ratio is that — by construction — it takes into account all statistical moments, while the Sharpe ratio only considers the first two.
Maximum drawdown — indicates the largest (expressed in %) drop between a peak and a valley
daily Value-at-Risk — another very popular risk metric. In this case, it indicates that in 95% of the cases, we will not lose more than 0.5% by keeping the position/portfolio for 1 more day.
The first 3 bullets are connected to the stylized facts of asset returns, which I described in one of the previous articles.
The next chart presents the cumulative returns on our strategy — we can observe the evolution of our portfolio’s worth over the two years of running this strategy. The flat periods represent periods when we held no assets.
We can also see the rolling Sharpe ratio, which provides more insight into the stability of our strategy. We see that the overall reported Sharpe ratio is 0.84, however, when we look at the rolling one we see how volatile it was over time (calculated using rolling 6 months of data, not entire sample!).
The last chart — the underwater plot — shows the investment from a pessimistic point of view. By pessimistic, I mean that it focuses on losses. It depicts drawdowns and shows how long it took for the portfolio’s value to recover to the previous peak, after suffering a loss. This chart makes it easy to distinguish between normal and extended periods of losses.
We created all these plots by passing only the returns object to the function. We can also pass the previously extracted positions and transactions, to automatically receive even more information on the simple tear sheet! Additionally, we can also specify a live_start_date, which splits the periods into the backtest and live-trading ones (zipline offers such a possibility). To do so, we simply run:
pf.create_simple_tear_sheet(returns, positions, transactions, live_start_date='2017-01-01')
In the table below we immediately see the changes:
there are two separate periods for analysis
providing positions and transactions related data resulted in extra metrics such as gross leverage and extra turnover
The simple tear sheet also contains more plots related to positions and transactions, however, I do not show them for brevity.
In the previous part, we only created a simple tear sheet, which gave a concise overview of the strategy’s performance. It is possible to easily obtain much more information, by creating a full tear sheet (it’s level of detail also depends on the provided information — here we only use returns).
pf.create_full_tear_sheet(returns)
I present a selection of new tables/charts included in the full tear sheet:
A table presenting top 5 worst drawdowns, together with information such as peak/valley date and the duration
Top 5 drawdown periods visualized on top of the cumulative returns. In our case, the drawdown periods cover almost the entire investment horizon. One of the reasons is that for extended periods of time we had no open positions, so the cumulative returns did not change, thus extending the recovery time.
A simple plot showing the daily returns over time. The flat periods reflect the times when we had no position in the underlying asset.
A selection of plots summarizing the strategy’s returns by breaking them down into: monthly returns (separately per each year), annual returns per each year, a histogram presenting the distribution of monthly returns and finally quantiles of returns expressed in different frequencies.
In this article, I showed how to use pyfolio to easily obtain a plethora of financial metrics and plots describing the performance of an asset/strategy/portfolio. This post gives a high-level introduction, while there are still many aspects to cover.
Some of the other functionalities of pyfolio include:
plots can be accessed manually without creating a simple/full tear sheet — for example, we can create a “top 5 worst drawdowns plot” by running pf.plot_drawdown_periods(returns, top=5)
we can account for slippage while creating the tear sheets
create Bayesian tear sheets (based on PyMC3, require separate dependencies installed)
Quantopian also released a library called empyrical, which is used to calculate all the risk metrics used in pyfolio. One possible advantage of using it is when we are purely interested in calculating one metric, such as the Omega ratio or the Sortino ratio. If that is the case, I certainly recommend empyrical for the task.
Kudos to Quantopian for creating such comprehensive libraries as zipline, pyfolio and empyrical!
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.
Below you can find the other articles in the series:
building algorithmic trading strategies based on Technical Analysis (link)
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.