Happy new year. This post will be a quick one covering the relationship between the simple moving average and time series momentum. The implication is that one can potentially derive better time series momentum indicators than the classical one applied in so many papers.
Okay, so the main idea for this post is quite simple:
I’m sure we’re all familiar with classical momentum. That is, the price now compared to the price however long ago (3 months, 10 months, 12 months, etc.). E.G. P(now) – P(10)
And I’m sure everyone is familiar with the simple moving average indicator, as well. E.G. SMA(10).
Well, as it turns out, these two quantities are actually related.
It turns out, if instead of expressing momentum as the difference of two numbers, it is expressed as the sum of returns, it can be written (for a 10 month momentum) as:
MOM_10 = return of this month + return of last month + return of 2 months ago + … + return of 9 months ago, for a total of 10 months in our little example.
This can be written as MOM_10 = (P(0) – P(1)) + (P(1) – P(2)) + … + (P(9) – P(10)). (Each difference within parentheses denotes one month’s worth of returns.)
Which can then be rewritten by associative arithmetic as: (P(0) + P(1) + … + P(9)) – (P(1) + P(2) + … + P(10)).
In other words, momentum — aka the difference between two prices, can be rewritten as the difference between two cumulative sums of prices. And what is a simple moving average? Simply a cumulative sum of prices divided by however many prices summed over.
Here’s some R code to demonstrate.
require(quantmod) require(TTR) require(PerformanceAnalytics) getSymbols('SPY', from = '1990-01-01') monthlySPY <- Ad(SPY)[endpoints(SPY, on = 'months')] monthlySPYrets <- Return.calculate(monthlySPY) #dividing by 10 since that's the moving average period for comparison signalTSMOM <- (monthlySPY - lag(monthlySPY, 10))/10 signalDiffMA <- diff(SMA(monthlySPY, 10)) # rounding just sum(round(signalTSMOM, 3)==round(signalDiffMA, 3), na.rm=TRUE)
With the resulting number of times these two signals are equal:
In short, every time.
Now, what exactly is the punchline of this little example? Here’s the punchline:
The simple moving average is…fairly simplistic as far as filters go. It works as a pedagogical example, but it has some well known weaknesses regarding lag, windowing effects, and so on.
Here’s a toy example how one can get a different momentum signal by changing the filter.
toyStrat <- monthlySPYrets * lag(signalTSMOM > 0) emaSignal <- diff(EMA(monthlySPY, 10)) emaStrat <- monthlySPYrets * lag(emaSignal > 0) comparison <- cbind(toyStrat, emaStrat) colnames(comparison) <- c("DiffSMA10", "DiffEMA10") charts.PerformanceSummary(comparison) table.AnnualizedReturns(comparison)
With the following results:
DiffSMA10 DiffEMA10 Annualized Return 0.1051 0.0937 Annualized Std Dev 0.1086 0.1076 Annualized Sharpe (Rf=0%) 0.9680 0.8706
While the difference of EMA10 strategy didn’t do better than the difference of SMA10 (aka standard 10-month momentum), that’s not the point. The point is that the momentum signal is derived from a simple moving average filter, and that by using a different filter, one can still use a momentum type of strategy.
Or, put differently, the main/general takeaway here is that momentum is the slope of a filter, and one can compute momentum in an infinite number of ways depending on the filter used, and can come up with a myriad of different momentum strategies.
Thanks for reading.
NOTE: I am currently contracting in Chicago, and am always open to networking. Contact me at my email at email@example.com or find me on my LinkedIn here.