Back in May 2020, in the eye of the Covid storm, we looked at overnight vs intraday returns in US equities.
Intuitively, we’d probably expect to see higher average returns overnight when the market is closed – because it’s much more difficult to hedge and manage our exposures when the cash market is closed, so we might expect to get paid a premium, on average, for taking that risk.
And that’s exactly what we found:
Most of the returns have indeed come overnight – which is quite remarkable in my opinion – but at the same time, most of the big negative returns have also come overnight, which supports the idea of a premium associated with the additional risks of holding positions overnight.
This week, I wanted to see whether that behaviour had changed since 2020.
Let’s dive in.
First, we’ll get some data for the SPY ETF from Yahoo. In the code below, I’m sourcing a script called yahoo_prices.R
, which retrives prices from Yahoo Finance. You can find the code here.
# load libraries and functions
library(tidyverse)
# set chart options
options(repr.plot.width = 14, repr.plot.height = 7, warn = -1)
theme_set(theme_bw())
theme_update(text = element_text(size = 20))
# functions for getting data
source("../../data_tools/yahoo_prices.R")
# get SPY data
spy <- single_ticker_prices_yahoo("SPY", "2000-01-01")
tail(spy)
Date | Open | High | Low | Close | Adj.Close | Volume | |
---|---|---|---|---|---|---|---|
<date> | <dbl> | <dbl> | <dbl> | <dbl> | <dbl> | <int> | |
6137 | 2024-05-22 | 530.65 | 531.38 | 527.60 | 529.83 | 529.83 | 48390000 |
6138 | 2024-05-23 | 532.96 | 533.07 | 524.72 | 525.96 | 525.96 | 57211200 |
6139 | 2024-05-24 | 527.85 | 530.27 | 526.88 | 529.44 | 529.44 | 41258400 |
6140 | 2024-05-28 | 530.27 | 530.51 | 527.11 | 529.81 | 529.81 | 36269600 |
6141 | 2024-05-29 | 525.68 | 527.31 | 525.37 | 526.10 | 526.10 | 45190300 |
6142 | 2024-05-30 | 524.52 | 525.20 | 521.33 | 522.61 | 522.61 | 46377600 |
Now we calculate:
- overnight returns as the % difference between the close price and the previous open
- intraday returns as the % difference between the open and the close
# calculate intraday and overnight returns
spy <- spy %>%
mutate(intraday = Close/Open - 1) %>%
mutate(overnight = Open/lag(Close) - 1) %>%
na.omit()
tail(spy)
Date | Open | High | Low | Close | Adj.Close | Volume | intraday | overnight | |
---|---|---|---|---|---|---|---|---|---|
<date> | <dbl> | <dbl> | <dbl> | <dbl> | <dbl> | <int> | <dbl> | <dbl> | |
6137 | 2024-05-22 | 530.65 | 531.38 | 527.60 | 529.83 | 529.83 | 48390000 | -0.0015452878 | -0.001336121 |
6138 | 2024-05-23 | 532.96 | 533.07 | 524.72 | 525.96 | 525.96 | 57211200 | -0.0131341934 | 0.005907565 |
6139 | 2024-05-24 | 527.85 | 530.27 | 526.88 | 529.44 | 529.44 | 41258400 | 0.0030122688 | 0.003593342 |
6140 | 2024-05-28 | 530.27 | 530.51 | 527.11 | 529.81 | 529.81 | 36269600 | -0.0008675241 | 0.001567728 |
6141 | 2024-05-29 | 525.68 | 527.31 | 525.37 | 526.10 | 526.10 | 45190300 | 0.0007989328 | -0.007795257 |
6142 | 2024-05-30 | 524.52 | 525.20 | 521.33 | 522.61 | 522.61 | 46377600 | -0.0036414911 | -0.003003148 |
Plot overnight and intraday returns:
spy %>%
pivot_longer(c(intraday, overnight), names_to = 'period', values_to = 'returns') %>%
group_by(period) %>%
mutate(cumreturns = cumprod(1+returns)) %>%
ggplot(aes(x=Date, y=cumreturns, color=period)) +
geom_line() +
labs(
title = "Overnight vs Intraday Returns",
y = "Cumulative Return"
)
We can see that the effect has persisted: the majority of the returns over the full cycle have come overnight. As have the most significant negative returns. No risk no reward.
I also wanted to see what the effect looked like using adjusted open and close prices:
spy %>%
mutate(Adj.Open = Open * (Adj.Close/Close)) %>%
mutate(Adj.intraday = Adj.Close/Adj.Open - 1) %>%
mutate(Adj.overnight = Adj.Open/lag(Adj.Close) - 1) %>%
na.omit() %>%
pivot_longer(c(Adj.intraday, Adj.overnight), names_to = 'period', values_to = 'returns') %>%
group_by(period) %>%
mutate(cumreturns = cumprod(1+returns)) %>%
ggplot(aes(x=Date, y=cumreturns, color=period)) +
geom_line()+
labs(
title = "Overnight vs Intraday Returns",
subtitle = "Calculated on adjusted prices",
y = "Cumulative Return"
)
The effect is even more pronounced using adjusted prices.
The overnight drift
This paper looks at the returns from equity index futures and suggests that nearly 100% of those returns have come in one hour between 2 am and 3 am.
This is an insane result if true, and something we’ve been meaning to look into for a while. We’ll do that in the near future.