Contents

# Strategy Library

### Abstract

In this tutorial, we implement an intraday momentum strategy that trades some of the most actively traded ETFs. Specifically, we observe the return generated from the first half-hour of the trading day to predict the sign of the trading day's last half-hour return. Researchers have shown that this momentum pattern is statistically and economically significant, even after accounting for trading fees. The algorithm we design here is a recreation of the research completed by Gao, Han, Li, and Zhou (2017).

### Background

News items are usually released before the opening bell. As it takes time for traders to digest and interpret the news, the first half-hour of trading typically has relatively higher levels of volume and volatility. Additionally, as traders attempt to mitigate overnight risk by unloading positions near the close, the last half-hour of trading also sees these higher levels of volume and volatility. These characteristics can be observed from the image below, which is reproducible in the attached research notebook.

Bogousslavsky (2016) points out that some investors are late-informed or simply prefer to delay their trading until the market close. As a result, a positive correlation exists between the direction of the opening and closing periods. Gao et al (2017) find that when trading this momentum strategy, the average annual return over their sample period was 6.67% for SPY, 11.72% for IWM, and 24.22% for IYR. Equal-weighting these returns leads to a combined average annual return of 14.2%.

### Method

#### Universe Selection

We implement a manual universe selection model that supplies a subset of the proposed ETFs in the attached research paper. Gao et al (2017) select the following tickers: DIA, QQQ, IWM, EEM, FXI, EFA, VWO, XLF, IYR, and TLT. In an effort to increase the backtest performance, we narrow our universe to SPY, IWM, and IYR.

tickers = ['SPY',  # S&P 500
'IWM',  # Russell 2000
'IYR'   # Real Estate ETF
]
symbols = [ Symbol.Create(ticker, SecurityType.Equity, Market.USA) for ticker in tickers ]
self.SetUniverseSelection( ManualUniverseSelectionModel(symbols) )
self.UniverseSettings.Resolution = Resolution.Minute


#### Alpha Construction

The IntradayMomentumAlphaModel emits insights to take positions for the last return_bar_count minutes of the day in the direction of the return for the first return_bar_count minutes of the day. During construction, we create a dictionary to store IntradayMomentum data for each symbol, define a method to determine the sign of returns, and specify the value of return_bar_count. In this tutorial, we follow Gao et al (2017) in setting return_bar_count to 30 by default.

class IntradayMomentumAlphaModel(AlphaModel):
sign = lambda _, x: int(x and (1, -1)[x < 0])

def __init__(self, algorithm, return_bar_count = 30):
self.return_bar_count = return_bar_count


#### Alpha Securities Management

When a new security is added to the universe, we create an IntradayMomentum object for it to store information needed to calculate morning returns. The management of the IntradayMomentum objects occurs in the alpha model's OnSecuritiesChanged method.

def OnSecuritiesChanged(self, algorithm, changes):

for security in changes.RemovedSecurities:


The definition of the IntradayMomentum class is shown below. We save a reference to the security's exchange so we can access the market hours of the exchange when generating insights.

class IntradayMomentum:
def __init__(self, security, algorithm):
self.symbol = security.Symbol
self.exchange = security.Exchange

self.bars_seen_today = 0
self.yesterdays_close = algorithm.History(self.symbol, 1, Resolution.Daily).loc[self.symbol].close[0]
self.morning_return = 0


#### Alpha Update

With each call to the alpha model's Update method, we count the number of bars the algorithm has received for each symbol. If we've reached the end of the morning window, we calculate the morning return. If we are at the beginning of the close window, we emit an insight in the direction of the morning window's return. If we are at the end of the day, we save the closing price and reset the counter for the number of bars seen today.

def Update(self, algorithm, slice):
insights = []

if slice.ContainsKey(symbol) and slice[symbol] is not None:

# End of the morning return

## Beginning of the close
mins_to_close = int((next_close_time - slice.Time).total_seconds() / 60)

if mins_to_close == self.return_bar_count + 1:
next_close_time,
insights.append(insight)
continue

# End of the day

return insights


The attached research paper holds positions for the last 30 minutes of the trading day, exiting at the market close. In order to accomplish this, we create a custom execution model. The model defined below submits a market order for the entry while also submitting a market on close order in the same time step.

class CloseOnCloseExecutionModel(ExecutionModel):
def __init__(self):
self.targetsCollection = PortfolioTargetCollection()
self.invested_symbols = []

def Execute(self, algorithm, targets):
# for performance we check count value, OrderByMarginImpact and ClearFulfilled are expensive to call
if self.targetsCollection.Count > 0:
for target in self.targetsCollection.OrderByMarginImpact(algorithm):
# calculate remaining quantity to be ordered
quantity = OrderSizing.GetUnorderedQuantity(algorithm, target)
if quantity == 0:
continue

algorithm.MarketOrder(target.Symbol, quantity)
algorithm.MarketOnCloseOrder(target.Symbol, -quantity)

self.targetsCollection.ClearFulfilled(algorithm)


### Conclusion

We conclude that the momentum pattern documented by Gao et al (2017) produces lower returns over our testing period. Comparing the strategy to the S&P 500 benchmark, the strategy has a lower Sharpe ratio during the backtesting period and during the recovery from the 2020 stock market crash. However, the strategy greatly outperforms the benchmark during the downfall of the 2020 crash, achieving a 4.8 Sharpe ratio. Throughout all of the time periods we tested, the strategy had a lower annual standard deviation than the benchmark, meaning more consistent returns. A breakdown of the results from all of the testing periods can be seen in the table below.

Period NameStart DateEnd DateStrategySharpeASD
Backtest 1/1/2015 8/16/2020Strategy-0.764 0.05
Benchmark 0.7090.185
Fall 2015 8/10/2015 10/10/2015Strategy -0.696 0.058
Benchmark-1.2430.793
2020 Crash 2/19/2020 3/23/2020Strategy 4.818 0.266
Benchmark-1.2430.793
2020 Recovery 3/23/2020 6/8/2020Strategy0.602 0.103
Benchmark 13.7610.386

We find the lack of performance for this strategy is not largely attributed to the inclusion of transaction costs in our analysis while Gao et al (2017) decide to ignore them. Even with ignoring the transaction fees, spread costs, and slippage, the strategy still has a lower Sharpe ratio than the S&P 500 and doesn't match the results found in the original research paper. Refer to the backtest results.

Throughout their research paper, Gao et al (2017) provide several suggestions to increase the return generated by this momentum pattern. These areas of future research include:

• Trading only on days with economic news events by utilizing the TradingEconomics data set. Gae et al (2017) suggest using the Michigan Consumer Sentiment Index, and news released on gross domestic product or the consumer price index.
• Restricting trading to times of greater volatility or during financial crises.
• Incorporating a volume threshold the morning session must pass to signal a trade for the close.
• Increasing diversification by extending the universe to include ETFs from other sectors.
• Considering the return from multiple n-minute periods throughout the day to predict the return of the closing period.

### References

1. Gao, Lei and Han, Yufeng and Li, Sophia Zhengzi and Zhou, Guofu, Market Intraday Momentum (June 19, 2017). Online copy

You can also see our Documentation and Videos. You can also get in touch with us via Chat.