
The strategy trades 11 commodity futures, creating a zero-investment portfolio by buying past overnight losers and selling winners, with daily rebalancing and adjusted weights to manage excessive volatility.
ASSET CLASS: CFDs, futures | REGION: Global | FREQUENCY:
Intraday | MARKET: commodities | KEYWORD: Overnight-Intraday, Daily, Reversal, Commodities
I. STRATEGY IN A NUTSHELL
The strategy targets 11 commodity futures, including Corn, Ethanol CBOT, Lean Hogs, Live Cattle, and others, selecting the most liquid contracts. A zero-investment portfolio is constructed by buying the previous day’s overnight losers and selling overnight winners. Commodity weights are determined using formulas from the academic paper, with 1/2 of the calculated weight used to estimate portfolio return and volatility, as full weights indicate excessive volatility (60% annually). The portfolio is rebalanced daily, with trades executed from the next open-to-close period after the formation period. This approach aims to exploit short-term price movements in commodities.
II. ECONOMIC RATIONALE
Price reversals often occur due to investor overreaction to news, followed by a price correction. Market closures, like overnight or weekend breaks, typically feature low liquidity and trading activity. Research by Nagel (2012) shows that uncertainty, measured by the VIX index, plays a significant role in explaining profits from the Close-to-Open (CO-OC) reversal strategy. This effect is particularly strong in futures markets. The CO-OC reversal pattern aligns with Hong and Wang’s (2000) continuous-time model, suggesting that hedging demands during market closures contribute to these reversals, leading to predictable profit opportunities.
III. SOURCE PAPER
Market Closure and Short-Term Reversal [Click to Open PDF]
Corte, Kosowski, Wang
<Abstract>
A strategy that buys securities with low past overnight returns and sells securities with high past overnight returns generates sizeable out-of-sample intraday returns and Sharpe ratios in all major asset classes. This strategy, labeled as overnight-intraday reversal, delivers an average return that is about two to five times larger than those generated by the conventional reversal strategy. Investor heterogeneity, sentiment, market uncertainty and market-wide illiquidity fail to explain this overnight-intraday reversal return. Our findings are consistent with an asset class-specific market maker liquidity provision mechanism, and we find that cross-sectional return dispersion can predict the strategy returns in every asset class. A global two-factor model, consisting of the market and overnight-intraday reversal factor, well explains the intraday return variation of diversified portfolios across asset classes.


IV. BACKTEST PERFORMANCE
| Annualised Return | 45.75% |
| Volatility | 31.02% |
| Beta | -0.012 |
| Sharpe Ratio | 1.47 |
| Sortino Ratio | -2.897 |
| Maximum Drawdown | N/A |
| Win Rate | 44% |
V. FULL PYTHON CODE
from AlgorithmImports import *
#endregion
class OvernightIntradayDailyReversalinCommodities(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2015, 1, 1)
self.SetCash(1000000)
symbols:List[str] = [
Futures.Grains.Corn,
Futures.Meats.LeanHogs,
Futures.Meats.LiveCattle,
Futures.Forestry.Lumber,
Futures.Grains.Oats,
Futures.Grains.SoybeanMeal,
Futures.Grains.Soybeans,
Futures.Grains.Wheat,
]
self.traded_percentage:float = 0.2
self.futures:List[Symbol] = []
self.recent_close:Dict[Symbol, float] = {}
for symbol in symbols:
future = self.AddFuture(symbol, Resolution.Minute, dataNormalizationMode=DataNormalizationMode.BackwardsRatio, contractDepthOffset=0)
self.futures.append(future.Symbol)
self.day_close_flag:bool = False
self.day_open_flag:bool = False
self.Schedule.On(self.DateRules.EveryDay(self.futures[3]), self.TimeRules.BeforeMarketClose(self.futures[3], 1), self.DayClose)
self.Schedule.On(self.DateRules.EveryDay(self.futures[3]), self.TimeRules.AfterMarketOpen(self.futures[3], 1), self.DayOpen)
def OnData(self, data: Slice) -> None:
if self.day_open_flag:
self.day_open_flag = False
returns:Dict[Symbol, float] = { symbol : data[symbol].Open / self.recent_close[symbol] - 1 for symbol in self.futures if symbol in self.recent_close and symbol in data and data[symbol] }
self.recent_close.clear()
long:List[Symbol] = [x[0] for x in returns.items() if x[1] < 0]
short:List[Symbol] = [x[0] for x in returns.items() if x[1] > 0]
for i, portfolio in enumerate([long, short]):
for symbol in portfolio:
# notional value = asset price * contract multiplier
notional_value:float = (self.Securities[symbol].Price * self.Securities[symbol].SymbolProperties.ContractMultiplier)
quantity:int = int((((-1) ** i) * self.Portfolio.TotalPortfolioValue) / len(portfolio) * self.traded_percentage) // notional_value
self.MarketOrder(self.Securities[symbol].Mapped, quantity)
if self.day_close_flag:
self.day_close_flag = False
self.Liquidate()
self.recent_close = { symbol : data[symbol].Close for symbol in self.futures if symbol in data and data[symbol] }
def DayClose(self) -> None:
self.day_close_flag = True
def DayOpen(self) -> None:
self.day_open_flag = True
VI. Backtest Performance