Quant BuffetRelax, Not Over Thinking

Intraday Closing Momentum in Futures

Log in to collect

Academic paper

Hedging Demand and Market Intraday Momentum

AuthorsGuido Baltussen; Zhi Da; Sten Lammers; Martin Martens

Institute
  • 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()