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.

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 Return5.47%
Volatility3.42%
Beta0.003
Sharpe Ratio1.6
Sortino RatioN/A
Maximum DrawdownN/A
Win Rate49%

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

Leave a Reply

Discover more from Quant Buffet

Subscribe now to keep reading and get access to the full archive.

Continue reading