Investing is a tough task. Anyone who has traded live markets will concur. There’s always a point when you lose money. When this happens, a lot of questions emerge: Was the investment thesis wrong? Were risks incorrectly assessed? Was the potential gain of the trade too low? Was the timing decision poor?
Investment professionals have tried to come up with answers before it’s too late. Whether you are a stock picker or study the statistical properties of time series, investing typically means coming up with a system, i.e. a set of rules that considers scenarios and triggers buy or sell actions depending on their outcomes. Your system may not be very well defined; sometimes, you might not even realise it’s a system, but it is. Backtesting is the process of testing this system on past data in order to assess its viability prior to investing in live markets.
In this article, I’ll use the Alphien platform to illustrate how to conduct robust backtesting in only a few lines of R code.
Contents
What is backtesting?
It is the process of applying an investment strategy to historical data and seeing what performance it would have generated had you traded it. In other terms, you put yourself back in the exact same informational conditions that existed in the past and generate investment decisions based on these conditions.
However, it is important to be careful about backtesting, as it can be deceiving. There are many pitfalls when it comes to it, the most common ones being:
- Survivorship bias: It means excluding securities that no longer exist, from your tests. Let’s suppose company SuperTech, a component of the Nasdaq index, goes bankrupt in 2001. If you select your investment universe - a collection of single stocks - today, you are most certainly not going to include company SuperTech as it’s no longer trading. Yet, as an investor in 4Q1999, looking at the Nasdaq skyrocketing, would you have excluded that company from your portfolio? Most certainly not. By excluding SuperTech from your investment universe, you have introduced a bias in your model.
- Look-ahead bias: It is very easy to peek into the “future” when you have all the data right in front of your eyes. When you build a strategy on the S&P index today, it could be tempting to design it so that it turns defensive when July 2007 comes. However, back then, would you have really tamed down your exposure? If we compare to what most fund managers did at the time, the answer is no. Peeking ahead is a very costly mistake that can make a poor strategy look great.
In the rest of this article, I will demonstrate how to backtest in R using the Alphien platform. The platform allows you to test a strategy quickly, performs checks to ensure it does not suffer obvious biases. Bonus point: if your strategy passes the backtest, it will not only be tested in the past; it will then be implemented on future data without changing anything to the code. In short, this is a nice tool to save you some time and focus on improving your investment idea rather than developing a testing environment.
How to run a backtest?
Let’s have a peek at Alphien R functions for backtesting. We can elect to create single asset strategies or portfolios: Here is the corresponding R code in each case:
- Single asset: Use the algoEngine class. For instance, you can define an Exponential Moving Average (EMA) Cross strategy on Gold futures. I choose 40 days short term rolling average and 260 days long term rolling average.
algoEngine("GC",type = "MM")%>% payout(emaCross, px=getBB("GC"), short=40, long=260) %>% evaluate() -> singleAssetStrategy backtest(singleAssetStrategy)
Let’s go through the code line by line:
- algoEngine("GC",type = "MM"): algoEngine() defines a strategy. It invests in “GC” (first parameter), which is the gold front future contract. The GC series is called a building block; the series is already adjusted for rolling from one contract to another; this is why it can be directly used in algoEngine. The “type” argument defines which type of strategy you are building. MM stands for “Market Momentum”. This argument is optional.
- payout(emaCross,px=getBB("GC"), short=40, long=260): payout() applies the strategy logic, i.e. when to have a long position (buy) or a short position (sell) on gold futures. payout() takes a function as first argument (here emaCross); arguments of this function are then passed by name. Here, “px”, “long” and “short” are input parameters of the emaCross() function. The interpreter will read it like: emaCross(px=getBB(“GC”), short=40, long=260).
- evaluate(): it computes the strategy allocation in time.
- backtest(): runs the backtest and outputs performance and risk metrics.
![RTENOTITLE|800x673](upload://qLoOQhbrSXfHnNoW41RDU8i0CrW.png)
single asset strategy backtesting results
- Portfolio: Use the portfolio class. To define a simple equal-weight strategy on gold and silver commodities you can do
portfolio(list("GC","SI")) %>% payout(equalWeight) %>% evaluate() -> portfolioStrategy backtest(portfolioStrategy)
![RTENOTITLE|721x435](upload://yKmr16OwabhcSnmLaPKX6yROdWu.png)
portfolio backtesting results
Why use single asset strategies you may wonder? One answer is divide and conquer: it is possible to combine single asset strategies into a portfolio. This flexibility allows you to create different market timing strategies for different assets and to combine them into one portfolio. Another answer is that some strategies can be asset specific. You wouldn’t study copper supply-demand dynamics to invest in a media company’s certificate of deposit.
In any case, the backtesting pipeline is similar and calls the same methods, making it handy to switch from one approach to another. Pretty handy as this allows us to spend time developing the strategy, not the infrastructure to test it.
The payout function
If the backtesting process only takes four lines of code, your strategy itself can be as complex as you want. The payout function defines the underlying set of investment rules of your strategy. It generates the time series of allocations (positions) in the asset. The following table illustrates the way allocations are handled at Alphien.
![RTENOTITLE|800x188](upload://faAM50wnmqZscvKGNfzTc8AslVt.png)
Note that the trading of assets is implicit; we think in position at a given time. If the position goes from 0 to -1 between t-1 and t, it means that we have sold the position at t-1 close, resulting in the short position at t. If there is no data for a date, it doesn’t mean there is no allocation at that date. By default, the position is maintained: if there is a position of 1 at date t, no data for date t+2 and -1 at date t+3, the position at date t+2 was 1 as we entered into a long position at date t. Actually, the backtesting engine automatically trades the positions your payout generates with a lag 1: the allocation at time t is traded at time t+1.
In previous examples, I have used available payouts on the platform, namely emaCross() and equalWeight(). You can decide to combine multiple pre-existing payouts in order to have an original strategy, or you can define your own custom payout function from scratch. It will then fit in the strategy pipeline like this:
portfolio(list('''asset1, asset2, ...'''))%>% payout('''customPayout, payoutParameter1, payoutParameter2, payoutParameter3,...''')%>% evaluate() -> portfolioStrategy
The only requirement for your payout function is that it generates an xts (R preferred time series data structure) of allocations for each asset in which you want to invest.
As an example let us define a custom payout function based on the exponential moving average crossover. This single asset strategy is fairly simple:
- When short term exponential moving average is greater than long term exponential moving average payout sets a long (buy) position.
- When short term exponential moving average is lower than long term exponential moving average payout sets a short (sell) position.
You can define this payout in the following way-
emaCross = function(px,short=60,long=260){ ind = ifelse(EMA(px,n=short) > EMA(px,n=long),1,-1) #EMA(px, n) computes the rolling exponential moving average over n periods. return(na.omit(ind)) }
Similarly, we can build a payout function for a portfolio that maintains an equal weight to each underlying asset.
equalWeight = function(assetReturns){ dates = index(assetReturns) nbAsset = ncol(assetReturns) nbDate = nrow(assetReturns) alloc = xts(matrix(rep(1/3, nbAsset*nbDate, nrow=nbDate), order.by=as.Dates(dates)) return(alloc) }
Is backtesting all there is?
Often, backtesting is only the first step in the making of a quantitative investment strategy. More developments are generally needed to turn the strategy systematic. However, there is no reason for segmenting these steps. On Alphien, the strategy you have backtested is good to be traded straight away. The backtesting process includes making sure the strategy will also run in the future: worst case scenario, you have terrible backtesting results and you can forget the idea you were exploring. However, if your strategy has some potential, you can push it to live in no time. This leads to significant time efficiency when doing research.
If you want your strategy to be published, i.e. to be replicated on live market data, submit it with the stressTest() function. The stressTest() function runs robustness tests on your strategy, makes sure there is no obvious bias within your payout, and lets you initialise the replication phase.
Conclusion
The article illustrated how to backtest a systematic investment strategy. On Alphien, it only takes four lines of R code. We discussed the payout function and how it can be used to implement and test custom strategies.
Stay posted for more articles where we’ll look into what financial data you can use in your backtesting. In the meantime, we hope to see you join us on the platform!
Tushar — Quant Developer @ Alphien