Background Information

Using an Internal Bar Strength Indicator (IBSI) , the aim of this post is to show the mean reverting properties of overnight returns. The indicator measures the relative position of the daily closing price in relation to the daily price range. When the Close price is near the bottom of this range the following day’s Open price tends to be higher than average and visa versa.

We will explore this effect over three scenarios:

  • Scenario 1: The Goldilocks scenario where we are able to both observe end of day price for the purposes of generating the signal AND invest at that price (close to close)

  • Scenario 2: We observe the end of day price for the purposes of generating a signal but do not invest until the following morning (open to close return)

  • Scenario 3: We observe the end of day price for the purposes of generating a signal but do not invest until the close of the following day (t+1 close to close)

The formula for IBSI used in this report is: \[IBSI = \frac{Close_t - Low_t}{High_t - Low_t}\]

The inspiration for this post is based on this article

Set Up

To test each scenario we will need to do the following:

  1. Create a list of Equity ETFs to test

  2. For each ETF create the IBS factor

  3. For each ETF calculate the corresponding factor return in each scenario

  4. Combine ETFs into one global Factor

Equity List

To run our strategy we will be using the following etfs

Data Processing

To process our data we will use the following function

get.etf.ibs = function(etf.name , scenario = 1 , volume_filter = 1000000){
  
  # Get data and Clean
  etf = get.etf.symbol(etf.name)
  etf = na.locf.default(etf)
  etf = as.data.table(etf)
  colnames(etf) = c( "date" , "Open" , "High" , "Low" , "Close" , "Volume" , "Adjusted")
  
  # Volume Filter
  etf[ , dollar_volume := Volume*Close]
  etf[ , dollar_volume_average := rollapplyr(dollar_volume , 7 , mean , fill = NA)]
  etf[ , dollar_volume_average_shift := shift(dollar_volume_average , 1)]
  etf[ dollar_volume_average_shift > volume_filter ,  investable := T]
  etf[ is.na(investable) , investable := F]
  
  # Create factor and return series
  etf = etf[ , IBS := (Close - Low) / (High - Low) , ]
  etf = etf[ , daily.return.cc := Adjusted / shift(Adjusted) - 1 , ]
  etf = etf[ , daily.return.oc := Close / Open - 1 , ]
  etf = etf[ , daily.return.cc.lead := shift(daily.return.cc , type = "lead" , 1) , ]
  etf = etf[ , daily.return.cc.lead2 := shift(daily.return.cc , type = "lead" , 2 ) , ]
  etf = etf[ , daily.return.oc.lead := shift(daily.return.oc , type = "lead") , ]
  
  # Set return to correct scenario 
  if(scenario == 1){
    
    etf$scenario_returns = etf$daily.return.cc.lead
    
  } else {
    
    if(scenario == 2){
      
      etf$scenario_returns = etf$daily.return.oc.lead
      
    } else {
      
      etf$scenario_returns = etf$daily.return.cc.lead2
      
      
    }
    
  }

  etf = etf[year(date) > 1999, ]
  # plot returns against raw signal
  p1 = ggplot(etf[investable == T , ] , aes(x = IBS , y = scenario_returns)) + geom_point() + 
    geom_smooth(method='lm',formula=y~x) + ggtitle(label = etf.name$Name , subtitle = etf.name$Type)
  
  # generate lm
  regression = summary(lm(scenario_returns  ~ IBS , etf[investable == T , ]))
  
  # Create signal factor bins
  etf$portfolio = ifelse(etf$IBS <= 0.2 , 1 ,
                         ifelse(etf$IBS <= 0.4 , 2 ,
                                ifelse(etf$IBS <= 0.6 , 3,
                                       ifelse(etf$IBS <= 0.8 , 4, 5))))
  etf$portfolio = na.locf0(etf$portfolio)
  etf$portfolio = as.factor(etf$portfolio)
  
  
  src = calcluate_information_coefficient(etf[investable == T , ] , holding_period = 1)
  # Generate box-plox 
  p2 = ggplot(etf[investable == T , ] , aes(x = portfolio , y = scenario_returns)) + 
    geom_boxplot() + ggtitle(label = etf.name$Name , subtitle = etf.name$Type)
  
  # Calculate cumulative returns by factor bin 
  etf.cumulative.return = etf[investable == T , ][order(portfolio , date)][ , .(date , returns = scenario_returns) ,portfolio ]
  
  # Calculate buy and hold returns
  overall.cumuative.return = etf[order(date)][ , .(date , returns = daily.return.cc.lead) ,]
  overall.cumuative.return$portfolio = "overall.etf"
  etf.cumulative.return = rbind(etf.cumulative.return , overall.cumuative.return)
  
  # Calculate ls portfolio return
  ls.portfolio = etf[investable == T , ][ portfolio %in% c(1,5)][order(date)]
  ls.portfolio$scenario_returns = ifelse(ls.portfolio$portfolio == 1,ls.portfolio$scenario_returns ,  -ls.portfolio$scenario_returns)
  ls.portfolio = data.table(portfolio = "ls.portfolio" , date = ls.portfolio$date , returns = ls.portfolio$scenario_returns)
  etf.cumulative.return = rbind(etf.cumulative.return , ls.portfolio)
  etf.cumulative.return$portfolio = as.factor(etf.cumulative.return$portfolio)
  
  # Get Performance stats
  etf.cumulative.return = dcast(etf.cumulative.return , date ~ portfolio , value.var = "returns" )
  etf.cumulative.return[is.na(etf.cumulative.return)] = 0 

  stats = get_annualized_stats(etf.cumulative.return)
  
  # Plot out all return series
  etf.cumulative.return = melt(etf.cumulative.return, id.vars = "date")
  etf.cumulative.return = etf.cumulative.return[ , cum.returns := cumprod(value + 1) , variable]
  etf.cumulative.return = etf.cumulative.return[ , portfolio := variable]
  p3 = ggplot(etf.cumulative.return , aes(x = date , y = cum.returns , color = portfolio)) + geom_line()  + 
    ggtitle(label = etf.name$Name , subtitle = etf.name$Type)
  
  # Return all data
  etf$etf.name = etf.name$Name
  etf$etf.ticker = etf.name$Symbol
  output = list(p1 = p1 , regression = regression , src = src , summary_table = stats,  p2 = p2 , p3 = p3 , etf.data = etf)
  
  return(output)
}

We can then run our tickers through this function for scenario 1, 2 and then 3 and output the factor analysis and performance for each ticker under each scenario.

Scenario One

Result Overview

There are a lot of tickers in our data set so let’s start by summarizing by Information Coefficient and Performance Data for Scenario 1

In general, for factors, information coefficients are fairly low, in-fact finding ICs with values greater than +/-0.1 is fairly uncommon.

Let’s take a look at a histogram of our ICs:

You can see that almost all of our information coefficients (IC) are negative and centered around -0.06. In fact the mean and median are -0.062 and -0.058 respectively.

Next we can review the overall universe IBSI scores vs following day returns in a linear model.

  scenario_returns
Predictors Estimates CI p
(Intercept) 0.00158 0.00149 – 0.00168 <0.001
IBS -0.00243 -0.00258 – -0.00228 <0.001
Observations 372679
R2 / R2 adjusted 0.003 / 0.003

We don’t have the strongest R squared value here but it is encouraging that our coefficients are statistically significant. Furthermore a spearman rank of our entire data set of -0.05 helps confirm a pattern likely exists.

Let’s take a closer look at some interesting ETFs:

We will start with the one with the largest IC, the SHV (The Short Treasury Bond ETF).

SHV ETF under Scenario 1

Linear Model

  scenario_returns
Predictors Estimates CI p
(Intercept) 0.00011 0.00010 – 0.00012 <0.001
IBS -0.00013 -0.00014 – -0.00011 <0.001
Observations 4313
R2 / R2 adjusted 0.056 / 0.055

Boxplot

Spearman Rank Coefficient:

Portfolio Returns

Next let’s take a look at the ETF with the largest return, the PIN (India ETF)

PIN ETF under Scenario 1

Linear Model

  scenario_returns
Predictors Estimates CI p
(Intercept) 0.00407 0.00280 – 0.00535 <0.001
IBS -0.00760 -0.00963 – -0.00557 <0.001
Observations 3087
R2 / R2 adjusted 0.017 / 0.017

Boxplot

Spearman Rank Coefficient:

Portfolio Returns

And finally the one with the worst IC the ARGT (Argentina ETF)

ARGT ETF under Scenario 1

Linear Model

  scenario_returns
Predictors Estimates CI p
(Intercept) -0.00161 -0.00358 – 0.00036 0.110
IBS 0.00420 0.00090 – 0.00751 0.013
Observations 1046
R2 / R2 adjusted 0.006 / 0.005

Boxplot

Spearman Rank Coefficient:

Portfolio Returns

A quick Aside:

A couple of things I want to make note of at this point:

  1. These results look really great, but it’s important to distinguish between market anomalies and what is actually tradable. For instance, Scenario 1 is very unrealistic; being able to observe the close price for signal purposes and then execute at that price is technically impossible. You might be able to get close if you are a high frequency trader but even then anyone who has actually traded knows that open and close are where most price action occurs.

  2. A lot of these ETFs are low volume, not all of them, but a lot of them. For instance the India Country ETF with the highest returning portfolio. Its average trading volume over the period was barely $8M a day. For Reference the SPY averages $19 Billion in volume daily. So far we haven’t accounted for slippage or fees. To try and account for the low volume, the function above does have a volume filter which will only count the signal and invest in the portfolio if the rolling 7 day dollar volume of the ETF is more than $1MM. This helps eliminate a lot of data outliers, but even this volume assumption is fairly low. (Periods of no trading due to this constraint are most evident in the ARGT equity curves above).

  3. Technically our hypothesis is that ETFs mean revert based on the IBS signal. But we’ve seen that some, like the Argentina ETF, exhibit more trend following behavior. In an actual trading model, technically we don’t need to make our model so strict. We could easily relax this assumption and instead look for strong mean-reverting OR trend following signals. You can easily see in the plots above that if we flip our long/short (ls) portfolios we would actually have a decent ls return for the ARGT.

For now though we will continue with the analysis.

Portfolio Construction

Now that we know we might have a signal we can test out a hypothetical portfolio as if we were able to trade under Scenario 1.

For this step we will keep the portfolio construction fairly simple.

  1. We will only consider portfolios with IC < 0 and
  2. We will only construct a equal weight of all ETFs

It’s important to note that we are cheating here. How?

Technically we are using an IC that has been trained over the entire period to filter our data. This is called look ahead bias. Basically we are using information to make a decision historically with data we would not have had at the time.

A more realistic implementation would be to create a rolling IC and then use the prior day value to filter our etfs. (Similar to what we did for the volume constraint).

Acknowledging that let’s proceed.

Amazing, we created a long short portfolio that returns 30% a year, has a Sharpe ratio over 3 and only drew down about 17% at its worst. And all we need was a bit of look ahead bias, a highly unrealistic entry assumptions, zero slippage and no transaction fees. (Note the sarcasm)

In all seriousness though, we might not be able to trade this portfolio but we did prove that there does seem to be a mean reverting effect between intraday ETF movement and overnight price action.

Next let’s see if we can make it a bit more realistic from a trading perspective.

Scenario Two

Result Overview

You can already see that both IC and returns are down when we change to scenario 2. This is further exemplified by our histogram.

In this case our mean and median have moved basically to 0, at -0.004 and -0.01 respectively.

You can see very quickly that buying the following morning of the signal and holding to close, does not have the same effect of buying the close the same day.

Our annualized returns now fall to 4.5%, the Sharpe ratio to .58 and Drawdown to 21.5%.

You can also see from the plot that you really haven’t made any money since 2010.

A quick side quest:

Here is an interesting market dynamic, semi-related to our current exploration.

In general for the S&P 500 (as proxied by the SPY). An overwhelming majority of returns have come by holding overnight (Close to Open) and not during the trading session (Open to Close). Note that this exercise uses un-adjusted prices so will not account for dividend reinvestment.

This could be one reason why returns in general are so low in our current scenario 2. But more analysis would need to be conducted for the other ETFs.

Scenario Three

Scenario 3 is another practical implementation from a trading perspective. The main question here is whether the signal is strong enough to still be relevant investing at the following day’s close.

Let’s take a look.

Result Overview

On an individual basis our returns are even lower in this scenario and our IC also seem to have dropped.

The histogram of ICs also shows the same story as scenario 2 as the distribution has moved a lot closer to 0 from scenario 1.

Our returns are slightly better than scenario 2, but also much worse than our original scenario. Also the benefits in scenario 3’s returns over scenario 2 is likely due to the market anomaly we discussed above. Here we are holding close to close, not just during the session.

The signal under scenario 3 also did not produce any real returns from 2005 until 2020.

Now, you might also look at this return profile and think. “Well our long portfolio outperforms the SPY, so why not just hold that?”

Here is the thing… To be honest, SPY isn’t really the best benchmark for this exercise (and definately a topic for another post). To start, if we want to benchmark our long short portfolio we certainly shouldn’t be using a long only benchmark. Secondly, even the long only “Portfolio 1” is an amalgamation of sector, country and precious metal ETFs all with their own unique factor profiles, most of which completely unrelated to the SPY.

Therefore, in order to determine that the out performance was driven by the actual IBSI factor a more extensive analysis would be required (like determining what type of factor risk Portfolio 1 is actually exposed to and which ETFs it’s most commonly holding on a look through basis).

Conclusion

In this post we demonstrated that likely most Equity ETFs exhibit mean reverting properties based on intraday returns as measured with the IBSI factor.

We also demonstrated the importance of distinguishing between a market anomaly and a tradable signal. Scenario 1 seemed very promising from a risk reward perspective but actually implementing that strategy is theoretically impossible.

We then saw how quickly our signal strength dissipates as we moved to more realistic implementations.

And finally we discussed how just because a portfolio may seem like it “outperforms”, understanding how it might differ from the chosen benchmark is imperative before any conclusions can be reached.