Copyright Robot Wealth Pty Ltd 2019 ©
Strategy Breakdown: Alpha Trade in Gold
This book or parts thereof may not be reproduced or distributed in any
form, either electronically or in print, without prior written permission of the
publisher.
For permission requests, write to contact@[Link].
[Link]
ny views, opinions or examples presented in this PDF are for
Disclaimer: A
educational purposes only. We do not provide financial advice. Robot Wealth
Pty Ltd accepts no liability or responsibility for any loss or damages which
may occur from your personal or professional trading.
Thanks for downloading our complete breakdown of this seasonality
alpha trade! Here you’ll find all of the research and code that went
into the strategy. Let’s get started!
Seasonal Anomalies
Despite what you might hear, alpha trading doesn't need to be complicated!
For example, simple seasonal anomalies can make for some of the best and most
reliable alpha trades around.
In this strategy breakdown, we're going to look at a day-of-the-week seasonality
effect in Gold and Gold miners.
Why are we looking at Seasonal Patterns in Gold?
Trading ideas can start in a number of places:
● Often we notice something in the markets, which leads us to form a hypothesis
about an inefficiency or effect we might be able to exploit.
● Sometimes we read something interesting in a paper or a blog.
● And occasionally we talk to other traders about what they are trading, and this
often sparks interesting ideas, too.
The idea we're going to look at today was sparked by a conversation with a trader friend
of mine over lunch. He mentioned to me that a huge proportion of the net (positive)
returns of GLD, the gold ETF, had come on Friday.
So, let's have a look and see if we might be able to build an alpha trading strategy
around the effect!
We're going to use The R Programming Language to do the analysis.
First, we're going to need some data
We're going to get our data from Alpha Vantage. You'll need to register with Alpha
Vantage to get an API key.
Store it in your R global environment as AV_API_KEY
AV_API_KEY <- 'YOUR_API_KEY_HERE'
Now we'll load the dependencies we're going to use in our analysis.
We're going to use:
● the alphavantager package to pull data from Alpha Vantage
● the t idyquant package for most of our analysis
● the t imetk package to split dates into their components (day-of-the-week etc)
The following code installs these dependencies (and installs them if necessary):
if (!require("pacman")) [Link]("pacman")
pacman::p_load(alphavantager, tidyquant, timetk)
library(tidyquant)
library(alphavantager)
library(timetk)
Next, we use the tidyquant and alphavantager packages to load daily adjusted OHLC
(open high low close) data for the ETFs GLD and GDX.
av_api_key(AV_API_KEY)
prices_df <- c('GLD', 'GDX') %>%
tq_get(get = "alphavantager", av_fun = 'TIME_SERIES_DAILY_ADJUSTED',
outputsize = 'full')
head(prices_df)
price_df now contains a data frame of daily GLD and GDX prices.
Here’s your first major takeaway from this: in analysis work, never assume things just
work!
Always plot everything and make sure.
Here, we plot the close prices of our ETFs using the ggplot library, and we inspect the
plots for bad data.
prices_df %>%
ggplot(aes(x=timestamp, y=close)) +
geom_line() +
ggtitle('GLD and GDX Prices') +
ylab('Close Price') +
xlab('Date') +
facet_wrap(~symbol, scales = 'free')
It looks pretty good... let's move on.
Calculating Returns
Prices aren't very easy to work with when we're doing seasonality analysis.
We're going to want to calculate some returns.
The code below calculates three different types of returns from our daily data:
● c2c - The percentage price change between subsequent market closes. (This
includes overnight and intraday returns)
● 02c - The intraday returns of the asset from the market open to market close.
● c20 - The overnight returns from close of the previous day to the day's open.
We calculate these returns as columns in our data frame.
return_df <- prices_df %>%
group_by(symbol) %>%
mutate(
c2c = close / lag(close) - 1,
o2c = close / open - 1,
c2o = open / lag(close) - 1
) %>%
select(symbol, timestamp, c2c, o2c, c2o) %>%
[Link]()
head(return_df)
The data frame return_df now contains daily returns for GLD and GDX.
So now we have a data frame with a row for each symbol and each date, and a column
for each of our return types.
Again, never assume things have worked properly! Let's do a basic check that we've
calculated returns correctly. We're going to plot the running cumulative product of
close-to-close returns. This should have the same "shape" as the prices we plotted
earlier but normalised to start at 1 at the beginning of our data.
return_df %>%
group_by(symbol) %>%
mutate(cumreturns = cumprod(1 + c2c)) %>%
ggplot(aes(x = timestamp, y = cumreturns, color = symbol)) +
geom_line() +
ggtitle('Cumulative total returns - GLD and GDX')
That looks good to me!
Investigate the Day of the Week Seasonality Effect
Now we've got some returns data, we want to plot average returns for each day of the
week.
Currently, our data only indexed by the date in it, not the day of the week.
We're going to use the function timetk::_augment_timseries_signature() to explode the
date into its components, including the day of the week.
return_df <- return_df %>%
tk_augment_timeseries_signature()
head(return_df)
Finally, we get to the good bit.
Let's plot mean returns for each day of the week.
We'll start with close-to-close returns. In this case:
● "Monday" is the returns from buying at the Friday close and exiting on the
Monday close
● "Tuesday" is the returns from buying at the Monday close and existing on the
Tuesday close
● Etc…
return_df %>%
group_by(symbol, [Link]) %>%
summarise(meanreturns = mean(c2c)) %>%
[Link]() %>%
ggplot(aes(x = [Link]([Link]), y = meanreturns)) +
geom_bar(stat = 'identity') +
labs(title = 'Day of the Week Seasonality - Close to close returns') +
xlab('Day of the Week') +
facet_wrap(~symbol)
Interesting!!
We see a quite significant effect, on average, over our sample.
We see what looks like:
● significant outperformance, on average, on Friday (especially in GLD)
● significant underperformance, on average, on Monday (especially in GDX)
Let's dig further...
Stability of the Seasonality Effect over Time
So far we've just looked at mean returns over all the data we have available.
Summary data can hide a lot of important context. In particular, these patterns might
result from just a few observations, or they might be concentrated in a certain subset of
the data period.
We ideally want to be any effect we trade to be consistent across time. That's useful for
a few reasons:
● it suggests the effect isn't concentrated in a few observations or a certain period.
● it gives us more confidence that the inefficiency has a cause rather than just
being a manifestation of randomness in the data.
Let's plot the day of the week mean returns for every year of our sample.
We're going to use close-to-close returns and plot GLD and GDX.
return_df %>%
group_by(year, [Link], symbol) %>%
summarise(meanreturns = mean(c2c, [Link] = TRUE)) %>%
[Link]() %>%
ggplot(aes(x = [Link], y = meanreturns, fill = symbol)) +
geom_bar(stat = 'identity', position = 'dodge') +
labs(title = 'Day of the Week Seasonality - Close to Close Returns') +
facet_wrap(~year)
We see a fairly consistent effect in Monday and Friday returns - with Friday returns
greater than Monday returns in most periods.
We’re asking a lot from this plot, though!
A month is only 252 observations, however, which isn’t much compared to the amount
of noise in market prices.
So, let’s divide the period into 4 periods of 4 years.
return_df %>%
mutate(period = ntile(year, n=4)) %>%
mutate(period_lbl = paste((period*4)+2000, '-', (period*4)+2003)) %>%
group_by(symbol, period_lbl, [Link]) %>%
summarise(meanreturns = mean(c2c)) %>%
[Link]() %>%
ggplot(aes(x = [Link], y = meanreturns, fill = symbol)) +
geom_bar(stat = 'identity', position = 'dodge') +
labs(title = 'Day of the Week Seasonality - Close to Close') +
xlab('Day of Week') +
facet_wrap(~period_lbl)
In every one of our 4-year periods, we see Friday returns being higher than
Monday returns.
This suggests a long/short alpha trading strategy in which we:
● buy these assets on Thursday night
● reverse short on Friday night
● cover the short positions on Monday night.
Is the close the best time to trade? Maybe these anomalous returns were
concentrated in market hours and entering positions on open might be
more appropriate?
Good question! Let's plot the intraday (o2c) and overnight (c2o) returns separately.
return_df %>%
gather(key = type, value = 'return', c2c, o2c, c2o) %>%
filter(type != 'c2c') %>%
group_by(symbol, [Link], type) %>%
summarise(meanreturns = mean(return)) %>%
[Link]() %>%
ggplot(aes(x = [Link]([Link]), y = meanreturns, fill = type)) +
geom_bar(stat = 'identity', position = 'dodge') +
labs(title = 'Day of the Week Seasonality') +
xlab('Day of the Week') +
facet_wrap(~symbol)
Broadly, we see that most of the effect we're looking at appears to manifest in the
intraday returns. (The teal "o2c" series.)
The large negative intraday returns on Monday in GDX is particularly interesting.
Could there be something to this? Or this is just a random manifestation?
Let's plot the data over our 4 year periods. Again, we're just looking to see if the
seasonality effect is consistently stronger for the o2c (intraday) returns - over the
sample.
This time we're going to plot GLD and GDX separately...
return_df %>%
gather(key = type, value = 'return', c2c, o2c, c2o) %>%
filter(symbol == 'GLD',
type %in% c('c2o','o2c')) %>%
mutate(period = ntile(year, n=4)) %>%
mutate(period_lbl = paste((period*4)+2000, '-', (period*4)+2003)) %>%
group_by(type, period_lbl, [Link]) %>%
summarise(meanreturns = mean(return)) %>%
[Link]() %>%
ggplot(aes(x = [Link], y = meanreturns, fill = type)) +
geom_bar(stat = 'identity', position = 'dodge') +
labs(title = 'GLD Day of the Week Seasonality - Intraday (o2c) and
Overnight (c2o) returns') +
facet_wrap(~period_lbl)
return_df %>%
gather(key = type, value = 'return', c2c, o2c, c2o) %>%
filter(symbol == 'GDX',
type %in% c('c2o','o2c')) %>%
mutate(period = ntile(year, n=4)) %>%
mutate(period_lbl = paste((period*4)+2000, '-', (period*4)+2003)) %>%
group_by(type, period_lbl, [Link]) %>%
summarise(meanreturns = mean(return)) %>%
[Link]() %>%
ggplot(aes(x = [Link], y = meanreturns, fill = type)) +
geom_bar(stat = 'identity', position = 'dodge') +
labs(title = 'GDX Day of the Week Seasonality - Intraday (o2c) and
Overnight (c2o) returns') +
facet_wrap(~period_lbl)
So there's a bit less consistency to this effect. It holds up on average, but it's not totally
consistent across the sample.
Now, obviously a backtest i s going to look a bit better if we choose to only enter a short
on Monday in GDX.... but that doesn't mean it's a good idea!
The market doesn't give us money for our backtests. It is trading in the present moment
that we get money for.
And, on the weight of this evidence, I don't think this justifies getting "cuter" by only
trading the intraday session.
Rolling and Cumulative Returns Holding each day of the
Week
It is valuable to examine things from slightly different directions.
So, before we try to put together a trading strategy based on these ideas, let's visualise
the data in a slightly different way.
We're going to plot the yearly returns we would get for holding on each day of the week.
In the charts below:
● "Monday" shows the returns of a strategy which buys on the Friday close and
closes on Monday every week for the period
● "Tuesday" shows the returns of a strategy which buys on the Monday close and
closes on Tuesday every week for the period
● etc.
First, we'll look at yearly returns:
return_df %>%
group_by(symbol, year, [Link]) %>%
summarise(meanannualreturns = mean(c2c)) %>%
[Link]() %>%
ggplot(aes(x = year, y = meanannualreturns, color = [Link])) +
geom_point() +
geom_smooth(method = 'loess', se = FALSE) +
geom_hline(yintercept = 0) +
facet_wrap(~symbol)
We've added some smoothing to make the plots easier to interpret.
What do we make of this?
● We see that Monday (the darkest colour) is consistently below Friday (yellow)
throughout the period. This is a very positive thing.
● There is some suggestion that the strength of the effect is decaying (as we would
expect it to, eventually.)
Now, let's plot cumulative returns. This essentially creates a compounded "equity curve"
for each day's "strategy".
return_df %>%
group_by(symbol, [Link]) %>%
mutate(cumreturns = cumprod(1 + c2c)) %>%
ggplot(aes(x = timestamp, y = cumreturns, color = [Link])) +
geom_line() +
facet_wrap(~symbol)
Again, we see:
● Friday is consistently one of the best days to hold GLD and GDX
● Monday is consistently one of the worst days to hold GLD and GDX.
I don't think you want to read more into this than that...
This looks like a fairly large effect (compared to other seasonal anomalies) but we can't
afford to get too cute with it.
Simple, blunt trading is the order of the day. So let's put a simple strategy together.
A Simple Seasonality Strategy for Each ETF
Let’s put together a basic strategy that:
● buys each ETF on the Thursday close
● reverses short on Friday close
● covers positions on Monday close.
First, we'll plot (rolling) annual returns:
longshort <- return_df %>%
mutate(longshort = case_when(
[Link] == 'Monday' ~ -c2c,
[Link] == 'Friday' ~ c2c,
TRUE ~ 0
))
longshort %>%
group_by(symbol, year) %>%
summarise(longshortreturns = sum(longshort)) %>%
ggplot(aes(x = year, y = longshortreturns)) +
geom_point() +
geom_smooth(method = 'loess') +
geom_hline(yintercept = 0) +
facet_wrap(~symbol) +
ggtitle('Long short Annual Returns')
It's consistently profitable throughout the period.
There is a suggestion of recent degradation of performance, and increasing uncertainty
around whether the edge holds up. (We say that because the blue smoothed line is
increasing towards the end of the sample and the grey cloud representing the standard
error bounds is widening.)
Now let's do a couple of simple backtests, assuming we trade an equal dollar amount of
each ETF each week (and ignoring any trading costs we might incur.)
longshort_returns <- longshort %>%
mutate(tradereturnsdollar = 10000 * longshort) %>%
group_by(symbol, year, month) %>%
summarise(dollarreturns = sum(tradereturnsdollar),
pctreturns = prod(1 + longshort) - 1) %>%
mutate(yearmon = ceiling_date(ymd(paste0(year, '-', month, '-', 1)),
'month') - 1) # last day of month
longshort_returns %>%
group_by(symbol) %>%
mutate(cumdollarreturns = cumsum(dollarreturns)) %>%
ggplot(aes(x = yearmon, y = cumdollarreturns)) +
geom_line() +
facet_wrap(~symbol) +
xlab('Date') +
ggtitle('Long Short Cumulative Returns - $10,000 per trade each side')
That looks very nice!
Let's use the functions in the PortfolioAnalytics package to calculate the performance of
the strategy for each asset.
spread_ret <- longshort_returns %>%
select(symbol, year, yearmon, pctreturns) %>%
spread(key = symbol, value = pctreturns)
strategy_returns_xts <- [Link](spread_ret[,c('GLD','GDX')], [Link] =
[Link](spread_ret$yearmon))
[Link](strategy_returns_xts)
Putting it All Together
We see this edge holds up on both assets.
When designing a trading strategy it is often useful to diversify and hedge our bets. So
let's trade both of the ETFs at the same time.
In what proportion should we combine them?
From the table above we see that the annualised volatility of our strategy is, on average,
about 2x bigger for GDX as it is for GLD.
So, let's create a combined strategy which trades GLD 2x bigger than GDX. This is
simply a crude way to have each asset contribute roughly equal volatility to the strategy.
strategy_returns_xts$strategy <- (strategy_returns_xts$GLD * 0.67) +
(strategy_returns_xts$GDX * 0.33)
[Link](strategy_returns_xts)
[Link](strategy_returns_xts$strategy, main = 'Gold
Seasonality Strategy')
Performance has been good!
We can see the risk-adjusted performance of the combined strategy is greater than the
performance of the trading the ETFs separately.
Trading both ETFs together was a good idea. At least in the past!
What about Costs?
This strategy turns over twice a week, so trading costs may be significant.
If we're trading with Interactive Brokers on the Fixed Tier then we'll be paying USD
0.005 per share on each trade.
We are trading 4x the total capital committed to the strategy each week. we:
● Enter long position
● Close long position
● Enter short position
● Close short position.
Given the above, let's estimate trading costs conservatively at 0.15% per month, which
should have you covered fine for 2 round trips in these instruments (taking into account
slippage and the extra costs of trading GDX, which is more expensive to trade on a
per-share basis due to its smaller share price.)
monthly_cost_pct <- 0.15 / 100
strategy_after_cost <- [Link](strategy_returns_xts$strategy) -
monthly_cost_pct
[Link](strategy_after_cost)
[Link](strategy_after_cost, main = 'Gold Seasonality
Strategy - After (conservative) cost estimate')
Would We Trade This?
Ok, so how would we trade this?
Well... that depends.
You never know what's going to work out - so a large part of the game is to diversify
across assets, trading styles and timescales.
If you're doing it well, then you're trading a lot of simple stuff.
This is a simple alpha which has shown to be a consistent effect for the last 16 years.
There is some evidence that the inefficiency is decreasing - and it may even have
disappeared - but I think it's worth a shot if you have the buying power or risk budget to
fit it into your portfolio.
We don't expect this trade to make your dreams come true, but we think it’s a good
addition to a diversified trading portfolio.
We trade a lot of stuff… and we trade this with a tiny proportion of our trading capital.
How To Trade It
Decide the amount of capital to commit to this strategy. This is the total absolute value
of the GLD and GDX exposure you are going to hold long or short. Let's say it's $1,000.
Calculate the amount of each ETF you will be trading:
● GLD: 33.3% * $1,000 = $333
● GDX: 66.6% * $1,000 = $667
Trading Plan
● On Thursday place two "market on close" orders to go long $667 of GLD and
$333 of GDX
● On Friday place two "market on close" orders to close those positions and go
short $667 of GLD and $333 of GDX
● On Monday place two "market on close" orders to cover all positions.
That's it!
You could automate it if you're already set up to do so, but trading it manually is
absolutely fine… and good for the soul!
So that’s our complete strategy teardown. Was it simpler that you anticipated? Don’t
forget to grab the code for the strategy using the link below!
Get The Code
The code shown above is available in an R Notebook file here.
Ready for a brutally-honest
breakdown of our approach to
profitable retail trading?
If you liked this strategy breakdown, chances are you’ll
love our latest ebook….
In Embrace the Mayhem, you’ll gain an understanding
of the markets usually learned only after years of trial,
error and eventual success — in just 80 pages.
This short ebook is the direct result of 20+ years of
full-time trading, both professionally and at the retail
level. You will see the reality of trading in terms of
what really works, free from BS and false-promises,
and you will learn how you can turn your passion for
trading into a capital-growing reality, like we did.
Get your copy of Embrace the Mayhem here