Looking more into quantitative trading strategies and determining returns

Learn all three interactively throughdataquest.io

InPython for Finance, Part I, we focused on using Python and Pandas to

retrieve financial time-series from free online sources (Yahoo),

format the data by filling missing observations and aligning them,

calculate some simple indicators such as rolling moving averages and

As a reminder, the dataframe containing the three cleaned price timeseries has the following format:

import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns sns.set(style=darkgrid, context=talk, palette=Dark2) data = pd.read_pickle(./data.pkl) data.head(10)

We have also calculated the rolling moving averages of these three timeseries as follows. Note that when calculating the $M$ days moving average, the first $M-1$ are not valid, as $M$ prices are required for the first moving average data point.

Calculating the short-window moving average short_rolling = an() short_rolling.head()

Calculating the short-window moving average long_rolling = data.rolling(window=100).mean() long_rolling.tail()

Building on these results, our ultimate goal will be to design a simple yet realistic trading strategy. However, first we need to go through some of the basic concepts related to quantitative trading strategies, as well as the tools and techniques in the process.

There are several ways one can go about when a trading strategy is to be developed. One approach would be to use the price time-series directly and work with numbers that correspond to some monetary value.

For example, a researcher could be working with time-series expressing the price of a given stock, like the time-series we used in the previous article. Similarly, if working with fixed income instruments, e.g. bonds, one could be using a time-series expressing the price of the bond as a percentage of a given reference value, in this case the par value of the bond. Working with this type of time-series can be more intuitive as people are used to thinking in terms of prices. However, price time-series have some drawbacks. Prices are usually only positive, which makes it harder to use models and approaches which require or produce negative numbers. In addition, price time-series are usually non-stationary, that is their statistical properties are less stable over time.

An alternative approach is to use time-series which correspond not to actual values but changes in the monetary value of the asset. These time-series can and do assume negative values and also, their statistical properties are usually more stable than the ones of price time-series. The most frequently used forms used are relative returns defined as

\beginequationr_\textrelative\left(t\right) = \fracp\left(t\right) – p\left(t-1\right)p\left(t-1\right)\endequation

$$\beginequationr\left(t\right) = \log\left( \fracp\left(t\right)p\left(t-1\right) \right)\endequation$$

where $p\left(t\right)$ is the price of the asset at time $t$. For example, if $p\left(t\right) = 101$ and $p\left(t-1\right) = 100$ then $r_\textrelative\left(t\right) = \frac101 – 100100 = 1\%$.

There are several reasons why log-returns are being used in the industry and some of them are related to long-standing assumptions about the behaviour of asset returns and are out of our scope. However, what we need to point out are two quite interesting properties. Log-returns are additive and this facilitates treatment of our time-series, relative returns are not. We can see the additivity of log-returns in the following equation.

\beginequationr\left(t_1\right) + r\left(t_2\right) = \log\left( \fracp\left(t_1\right)p\left(t_0\right) \right) + \log\left( \fracp\left(t_2\right)p\left(t_1\right) \right) = \log\left( \fracp\left(t_2\right)p\left(t_0\right) \right)\endequation

which is simply the log-return from $t_0$ to $t_2$. Secondly, log-returns are approximately equal to the relative returns for values of $\fracp\left(t\right)p\left(t-1\right)$ sufficiently close to $1$. By taking the 1st order Taylor expansion of $\log\left( \fracp\left(t\right)p\left(t-1\right) \right)$ around $1$, we get

\beginequation\log\left( \fracp\left(t\right)p\left(t-1\right) \right) \simeq \log\left(1\right) + \fracp\left(t\right)p\left(t-1\right) – 1 = r_\textrelative\left(t\right)\endequation

Both of these are trivially calculated using Pandas:

Relative returns returns = data.pct_change(1) returns.head()

Log returns – First the logarithm of the prices is taken and the the difference of consecutive (log) observations log_returns = np.log(data).diff() log_returns.head()

Since log-returns are additive, we can create the time-series of cumulative log-returns defined as

\beginequationc\left(t\right) = \sum_k=1^t r\left(t\right)\endequation

The cumulative log-returns and the total relative returns from 2000/01/01 for the three time-series can be seen below. Note that although log-returns are easy to manipulate, investors are accustomed to using relative returns. For example, a log-return of $1$ does not mean an investor has doubled the value of his portfolio. A relative return of $1 = 100\%$ does! Converting between the cumulative log-return $c\left(t\right)$ and the total relative return $c_\textrelative\left(t\right) = \fracp\left(t\right) – p\left(t_o\right)p\left(t_o\right)$ is simple

$$ c_\textrelative\left(t\right) = e^c\left(t\right) – 1 $$

For those who are wondering if this is correct, yes it is. If someone had bought $\$1000$ worth of AAPL shares in January 2000, her/his portfolio would now be worth over $\$30,000$. If only we had a time machine…

fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(16,12)) for c in log_returns: dex, log_returns[c].cumsum(), label=str(c)) ax1.set_ylabel(Cumulative log returns) ax1.legend(loc=best) for c in log_returns: ax2.plot(log_returns.index, 100*(np.exp(log_returns[c].cumsum()) – 1), label=str(c)) ax2.set_ylabel(Total relative returns (%)) ax2.legend(loc=best) plt.show()

Our goal is to develop a toy trading strategy, but what does the term quantitative trading strategy actually mean? In this section we will give a definition that will guide us in our long-term goal.

Assume we have at our disposal a certain amount of dollars, $N$, which we are interested to invest. We have at our disposal a set of $K$ assets from which we can buy and sell freely any arbitrary amount. Our goal is to derive weights $w_i\left(t\right), i = 1, \ldots, K$ such that

$$w_i\left(t\right) \in \mathbbR \ \textand \ \sum_i=1^K w_i\left(t\right) \leq 1$$

so that an amount of dollars equal to $w_i\left(t\right) N$ is invested at time $t$ on asset $i$.

The inequality condition signifies $\sum_i=1^K w_i\left(t\right) \leq 1$ that the maximum amount we can invest is equal to amount of dollars we have, that is $N$.

For example, assume we can invest in $2$ instruments only and that $N=\$1000$. The goal is to derive two weights $w_1\left(t\right)$ and $w_2\left(t\right)$.

If at some point $w_1\left(t\right) = 0.4$ and $w_2\left(t\right) = 0.6$, this means that we have invested $w_1\left(t\right)N = \$400$ in asset $1$ and $w_2\left(t\right)N = \$600$ in asset $2$. Since we only have $\$1000$, we can only invest up to that much which means that

$$w_1\left(t\right)N + w_2\left(t\right)N \leq N \Rightarrow w_1\left(t\right) + w_2\left(t\right) 1$$.

Note that since we have allowed $w_i\left(t\right)$ to be any real number, we are implying that we are allowed to have negative weights. Negative weights imply that we have sold a given asset short. Selling short an asset means selling an asset we do not currently hold and receiving its value in cash. Selling short is different than selling an asset we already own, which is called selling long.

The mechanics behind this can be complicated and are usually subject to regulatory scrutiny. However, on a high level, it involves borrowing the asset from a third party and then selling it to the buyer. Since at some point the asset needs to be returned to the party from which it was borrowed, the short position needs to be closed. This is achieved by buying the asset back from the original buyer or any other willing seller. For the purpose of this article it will be assumed that selling an asset short can be accomplished at no added cost, an assumption which is not true.

The assuming that the weights can be unbounded is not realistic. For example, based on the definition given above we could sell short an amount of AAPL shares of value equal to $N$. This means that, for now, we have at our disposal an additional $N$ dollars to invest from the short sale. Thus, together with our original $N$ dollars, we can the purchase shares of MSFT worth $2N$ dollars. In our framework, this translates to $w_\textAAPL=-1$ and $w_\textMSFT=2$. In theory, the weights could be $-999$ and $1000$ respectively. However, an increase in the absolute values of the weights leads to an increase in the risk of our portfolio for reasons we will see further down this series of tutorials. Therefore, when developing our trading strategy, appropriate thresholds will be imposed on the weights $w_i\left(t\right)$.

A final note has to do with cash. Any portfolio will at some point in time include cash. In the aforementioned setup if at any point in time $W = \sum_i=1^K w_i\left(t\right) 1$, then it means that our portfolio includes $\left(1-W\right)N$ dollars in cash. Of course, if $W0$, our net position is short, which means we are currently holding more than $N$ dollars which is the initial value of the portfolio.

Last day returns. Make this a column vector r_t = log_returns.tail(1).transpose() r_t

Weights as defined above weights_vector = pd.DataFrame(1 / 3, dex, lumns) weights_vector

Total log_return for the portfolio is: portfolio_log_return = weights_vector.transpose().dot(r_t) portfolio_log_return

If computer memory is not an issue, a very fast way of computing the portfolio returns for all days, $t = 1, \ldots, T$ is the following:

Assume that $\mathbfR \in \mathbbR^T \times K$ is a matrix, the $t$th row of which is the row vector $\vecr\left(t\right)^T$. Similarly, $\mathbfW \in \mathbbR^T \times K$ is a matrix, the $t$th row of which is the row vector $\vecw\left(t\right)^T$. Then if $\vecr_p = \left[ r_p\left(1\right), \ldots, r_p\left(T\right) \right]^T \in \mathbfR^T \times 1$ is a column vector of all portfolio returns, we have

$$\vecr_p = \textdiag\left\ \mathbfW \mathbfR^T \right\$$

where $\textdiag\left\A \right\$ is the diagonal of a matrix $\mathbfA$. The diagonal extraction is required because only in the diagonal the weights and log-returns vectors are properly time-aligned.

To illustrate the concepts of the previous section, let us consider a very simple trading strategy, where the investor splits his investments equally among all three assets we have been looking at. That is:

$$ w_\textAAPL = w_\textMSFT = w_\text^GSPC = \frac13 $$

Matrix $\mathbfR$ is simply our log-returns dataframe defined before.

Thus, the portfolio returns are calculated as:

Initially the two matrices are multiplied. Note that we are only interested in the diagonal, which is where the dates in the row-index and the column-index match. temp_var = weights_matrix.dot(log_returns.transpose()) temp_var.head().iloc[:, 0:5]

The numpy np.diag function is used to extract the diagonal and then a Series is constructed using the time information from the log_returns index portfolio_log_returns = pd.Series(np.diag(temp_var), index=log_returns.index) portfolio_log_returns.tail()

2016-12-26 0.000000 2016-12-27 0.003070 2016-12-28 -0.005753 2016-12-29 -0.000660 2016-12-30 -0.008210 Freq: B, dtype: float64

Note that these returns are only estimates because of our use of log-returns instead of relative returns. However, for most practical purposes the difference is negligible. Let us see what our cumulative log returns and the total relative returns for this portfolio look.

total_relative_returns = (np.exp(portfolio_log_returns.cumsum()) – 1) fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(16,12)) ax1.plot(portfolio_log_returns.index, portfolio_log_returns.cumsum()) ax1.set_ylabel(Portfolio cumulative log returns) ax2.plot(total_relative_returns.index, 100 * total_relative_returns) ax2.set_ylabel(Portfolio total relative returns (%)) plt.show()

So this simple investing strategy would yield a total return of more than $325\%$ in the course of almost $16$ years.

How does this translate to a yearly performance? Since we have kept all weekdays in our portfolio, there are $52 \times 5 = 260$ weekdays each year. There are $4435$ days in our simulation which corresponds roughly to $16.92$ years. We will be calculating the average geometric return, that is an average return $\barr$ which when compounded for $16.92$ years will produce the total relative return of $325.14\%$. So we need to solve:

\beginequation\left(1 + \barr\right)^16.92 = 1 + 3.2514\endequation

Calculating the time-related parameters of the simulation days_per_year = 52 * 5 total_days_in_simulation = data.shape[0] number_of_years = total_days_in_simulation / days_per_year The last data point will give us the total portfolio return total_portfolio_return = total_relative_returns[-1] Average portfolio return assuming compunding of returns average_yearly_return = (1 + total_portfolio_return)**(1 / number_of_years) – 1 print(Total portfolio return is: + :5.2f.format(100 * total_portfolio_return) + %) print(Average yearly return is: + :5.2f.format(100 * average_yearly_return) + %)

Total portfolio return is: 325.14% Average yearly return is: 8.85%

Our strategy is a very simple example of a buy-and-hold strategy. The investor simply splits up the available funds in the three assets and keeps the same position throughout the period under investigation. Although simple, the strategy does produce a healthy $8.85\%$ per year.

However, the simulation is not completely accurate. Let us not forget that we have used ALL weekdays in our example, but we do know that on some days the markets are not trading. This will not affect the strategy we presented as the returns on the days the markets are closed are 0, but it may potentially affect other types of strategies. Furthermore, the weights here are constant over time. Ideally, we would like weights that change over time so that we can take advantage of price swings and other market events.

Also, we have said nothing at all about the risk of this strategy. Risk is the most important consideration in any investment strategy and is closely related to the expected returns. In what follows, we will start designing a more complex strategy, the weights of which will not be constant over time. At the same time we will start looking into the risk of the strategy and present appropriate metrics to measure it. Finally, we will look into the issue of optimizing the strategy parameters and how this can improve our return to risk profile.

See Part 3 of this series:Moving Average Trading Strategies.

Python for Financial Analysis and Algorithmic Trading

Goes over numpy, pandas, matplotlib, Quantopian, ARIMA models, statsmodels, and important metrics, like the Sharpe ratio

PhD in Applied Mathematics and Statistics. Analyst working on quantitative trading, market and credit risk management and behavioral modelling at Barclays Investment Bank. Founder and CEO of QuAnalytics Limited.