Intraday Closing Momentum in Futures
Log in to collectAcademic paper
Hedging Demand and Market Intraday Momentum
Guido Baltussen; Zhi Da; Sten Lammers; Martin Martens
- NLErasmus University Rotterdam
- ?Erasmus University Rotterdam (EUR)
- ?Robeco Quantitative Investments
- ARUniversity of Mendoza
- University of Notre Dame
- ?University of Notre Dame - Mendoza College of Business
- ?Erasmus University Rotterdam (EUR) - Erasmus School of Economics (ESE)
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.
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.
Backtest performance
Annualised return5.47%
Volatility3.42%
Beta0.003
Sharpe ratio1.6
Win rate49%
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()