
The investment universe consists of all futures contracts in the equity/commodities/fixed income/Forex derivatives market (we picked equity futures). The variables of interest are the return (ONFH) and return (ROD). The return (ONFH) today = the return generated if we were to buy the security at yesterday’s closing price and then sell it at its price 30 minutes after today’s market opening.
ASSET CLASS: CFDs, futures | REGION: United States | FREQUENCY:
Intraday | MARKET: bonds, commodities, currencies, equities | KEYWORD: Closing Momentum
I. STRATEGY IN A NUTSHELL
The strategy trades equity futures using intraday return signals. Returns in the first 30 minutes after market open (ONFH) and the last 30 minutes before market close (ROD) are used to predict returns in the final 30 minutes of the trading day (LH). If both ONFH and ROD are positive, a buy signal is generated. Futures positions are equally weighted, and the portfolio is rebalanced intraday.
II. ECONOMIC RATIONALE
Traders often hedge gamma exposure at market open and close to manage portfolio risk, reduce volatility, and incorporate available market information. This behavior creates predictable intraday return patterns, which the strategy exploits.
III. SOURCE PAPER
Hedging demand and market intraday momentum [Click to Open PDF]
Baltussen, Guido, Erasmus University Rotterdam (EUR); Northern Trust Corporation – Northern Trust Asset Management;Da, Zhi, University of Notre Dame – Mendoza College of Business;
Lammers, Sten, Erasmus University Rotterdam (EUR) – Erasmus School of Economics (ESE);
Martens, Martin, Erasmus University Rotterdam (EUR).
<Abstract>
Hedging short gamma exposure requires trading in the direction of price movements, thereby creating price momentum. Using intraday returns on over 60 futures on equities, bonds, commodities, and currencies between 1974 and 2020, we document strong “market intraday momentum” everywhere. The return during the last 30 minutes before the market close is positively predicted by the return during the rest of the day (from previous market close to the last 30 minutes). The predictive power is economically and statistically highly significant, and reverts over the next days. We provide novel evidence that links market intraday momentum to the gamma hedging demand from market participants such as market makers of options and leveraged ETFs.


IV. BACKTEST PERFORMANCE
| Annualised Return | 5.47% |
| Volatility | 3.42% |
| Beta | 0.003 |
| Sharpe Ratio | 1.6 |
| Sortino Ratio | N/A |
| Maximum Drawdown | N/A |
| Win Rate | 49% |
V. FULL PYTHON CODE
from AlgorithmImports import *
from datetime import datetime
# endregion
class IntradayClosingMomentuminFutures(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2015, 1, 1)
self.SetCash(100000)
self.tickers:list[str] = [
# equity
Futures.Indices.SP500EMini, # E-mini S&P 500 Futures
Futures.Indices.NASDAQ100EMini, # E-mini Nasdaq-100 Futures
Futures.Indices.Russell2000EMini, # E-mini Russell 2000 Index Futures
]
self.perf_period_start_time1:datetime = datetime(2000, 1, 1, 9, 31).time()
self.perf_period_start_time2:datetime = datetime(2000, 1, 1, 10, 0).time()
self.perf_period_end_time:datetime = datetime(2000, 1, 1, 15, 30).time()
self.open_trade_time:datetime = datetime(2000, 1, 1, 15, 30).time()
self.close_trade_time:datetime = datetime(2000, 1, 1, 16, 0).time()
# self.perf_period_start_hour1:int = 9
# self.perf_period_start_minute1:int = 31
self.perf_period_start_hour2:int = 10
self.perf_period_start_minute2:int = 0
self.perf_period_end_hour:int = 15
self.perf_period_end_minute:int = 30
self.open_trade_hour:int = 15
self.open_trade_minute:int = 30
self.close_trade_hour:int = 16
self.close_trade_minute:int = 0
self.stored_price_cnt:int = 3
self.portfolio_percentage:float = 0.1
self.long_leg:list[Symbol] = []
self.short_leg:list[Symbol] = []
self.open_orders:list[list[Symbol, float]] = []
self.futures_data:dict[str, FutureData] = {}
for ticker in self.tickers:
future:Symbol = self.AddFuture(ticker, Resolution.Minute)
future.SetFilter(0, 90)
self.futures_data[ticker] = FutureData(future.Symbol)
def OnData(self, data: Slice):
if (self.Time.time() == self.perf_period_start_time1) \
or (self.Time.time() == self.perf_period_start_time2) \
or (self.Time.time() == self.perf_period_end_time):
for ticker, future_data in self.futures_data.items():
contract:FuturesContract = future_data.contract
contract_symbol:Symbol|None = contract.Symbol if contract else None
if contract_symbol and self.Securities[contract_symbol].Price != 0:
hist = self.History(contract_symbol, 1, Resolution.Minute)
if not hist.empty:
price:float = hist['close'].iloc[-1]
future_data.update_prices(price)
if self.Time.time() == self.perf_period_end_time:
if contract_symbol and future_data.prices_ready() and self.Securities[contract_symbol].IsTradable \
and (contract.Expiry.date() > (self.Time.date() + timedelta(days=2))):
if future_data.buy_signal():
self.long_leg.append(contract_symbol)
future_data.reset_prices()
# find near contract
for ticker, future_data in self.futures_data.items():
curr_contract:FuturesContract|None = future_data.contract
# future_data.update_contract(None)
if curr_contract is None or ((curr_contract.Expiry.date() - timedelta(days=1)) <= self.Time.date()):
for chain in data.FuturesChains:
if chain.Key.ID.Symbol != ticker:
continue
contracts:list[FuturesContract] = [contract for contract in chain.Value]
if len(contracts) == 0:
continue
near_contract:FuturesContract = sorted(contracts, key=lambda x: x.Expiry, reverse=True)[0]
future_data.update_contract(near_contract)
# open trade
if self.Time.time() == self.open_trade_time:
long_length = len(self.long_leg)
if long_length != 0:
for contract_symbol in self.long_leg:
self.SetHoldings(contract_symbol, (self.portfolio_percentage / long_length) )
self.long_leg.clear()
self.short_leg.clear()
# close trade
if self.Time.time() == self.close_trade_time:
self.Liquidate()
class FutureData:
def __init__(self, symbol:Symbol, stored_price_cnt:int = 3) -> None:
self.symbol = symbol
self.contract = None
self.prices:RollingWindow = RollingWindow[float](stored_price_cnt)
self.stored_price_cnt = stored_price_cnt
def update_contract(self, contract) -> None:
self.contract = contract
def update_prices(self, price:float) -> None:
self.prices.Add(price)
def prices_ready(self) -> bool:
return self.prices.IsReady
def buy_signal(self) -> bool:
result:bool = (self.prices[0] / self.prices[self.stored_price_cnt-1] - 1) > 0 and (self.prices[1] / self.prices[self.stored_price_cnt-1] - 1) > 0
return result
def reset_prices(self) -> None:
self.prices.Reset()
VI. Backtest Performance