
Trade WTI futures using a predictor based on API data vs. Bloomberg consensus, taking positions 60 minutes before EIA announcements and closing 1 minute prior.
ASSET CLASS: futures | REGION: Global | FREQUENCY:
Intraday | MARKET: commodities | KEYWORD: Inventory
I. STRATEGY IN A NUTSHELL
Trades nearby WTI futures around API inventory releases. Compute the predictor: (API actual – Bloomberg median) / inventory. Go short if positive, long if negative. Positions are opened 60 minutes before the EIA report and closed 1 minute prior, capturing short-term movements from underpriced API data.
II. ECONOMIC RATIONALE
API reports are less widely followed than EIA reports, leading to underreaction in prices, especially during low liquidity or low attention periods. Traders incorporate this information gradually, allowing short-term predictability of pre-EIA price movements based on API surprises.
III. SOURCE PAPER
Market Inefficiencies Surrounding Energy Announcements [Click to Open PDF]
Sultan Alturki, King Saud University; Alexander Kurov, West Virginia University – College of Business & Economics
<Abstract>
We use sequential energy inventory announcements to shed new light on the informational efficiency of financial markets. Our findings provide clear evidence of inefficiency in crude oil futures and stock markets. This inefficiency can be exploited by sophisticated traders. We examine the effect of market liquidity on the efficient incorporation of information in this setting. We also construct a predictor that can predict inventory surprises and pre-announcement returns in-sample and out-of-sample. Finally, we develop a combination forecast that can be used as a proxy for market expectations of oil inventory announcements.


IV. BACKTEST PERFORMANCE
| Annualised Return | 6.7% |
| Volatility | 10% |
| Beta | 0.017 |
| Sharpe Ratio | 0.67 |
| Sortino Ratio | N/A |
| Maximum Drawdown | N/A |
| Win Rate | 48% |
V. FULL PYTHON CODE
from AlgorithmImports import *
#endregion
import data_tools
EXPIRY_MIN_DAYS = TimeSpan.FromDays(5)
EXPIRY_MAX_DAYS = TimeSpan.FromDays(35)
class InventoryMispricingPredictsOilReturns(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2012, 3, 27)
self.SetCash(100000)
self.max_missing_days = 14
future = self.AddFuture(Futures.Energies.CrudeOilWTI, Resolution.Minute)
future.SetFilter(EXPIRY_MIN_DAYS, EXPIRY_MAX_DAYS)
self.active_future = None
self.symbol = future.Symbol
self.api_report = self.AddData(data_tools.APIOilReport, 'APIOilReport', Resolution.Daily).Symbol # API data comes one day before EIAOilReport
self.api_report_date = None
self.eia_report = self.AddData(data_tools.EIAOilReport, 'EIAOilReport', Resolution.Daily).Symbol
def OnData(self, slice):
# check if contract isn't None and if it is about to expire in 1 day
if self.active_future and self.active_future.Expiry.date() - timedelta(days=1) <= self.Time.date():
# liquidate contract
self.Liquidate(self.active_future.Symbol)
self.active_future = None
# pick nearest contract if there is no selected contract
if not self.active_future:
for chain in slice.FutureChains:
contracts = [contract for contract in chain.Value]
# there aren't any active contracts
if len(contracts) == 0:
continue
# contract = sorted(contracts, key=lambda k : k.OpenInterest, reverse=True)[0]
near_contract = sorted(contracts, key=lambda x: x.Expiry, reverse=True)[0]
self.active_future = near_contract
# new api report data came
if self.api_report in slice and slice[self.api_report]:
self.api_report_date = self.Time.date()
# liquidate
if self.Portfolio.Invested and ((self.Time.hour == 10 and self.Time.minute >= 29) or (self.Time.hour > 10)):
self.Liquidate()
# day after api report
if self.api_report_date and self.Time.date() == self.api_report_date:
if self.active_future:
# active contract is available
# if and self.Securities.ContainsKey(self.api_report) and self.Securities.ContainsKey(self.eia_report):
if self.Time.hour == 9 and self.Time.minute == 31:
api_actual = None
eia_prior = None
inv_level = None
api_report = self.Securities[self.api_report].GetLastData()
if api_report:
api_actual = api_report['actual']
eia_report = self.Securities[self.eia_report].GetLastData() # at 9:31 EIA report 'survey' and 'prior' columns should be available
if eia_report:
eia_survey = eia_report['survey'] # Bloomberg median consensus
inv_level = eia_report['prior']
if api_actual is not None and \
eia_survey is not None and \
inv_level is not None and \
inv_level != 0:
# predictor = (api_actual - eia_survey) / inv_level
predictor = api_actual - (eia_survey / inv_level)
# Sell (buy) WTI futures contracts 60 minutes before the EIA announcement and close the position 1 minute before the EIA announcement if the predictor is positive (negative).
if self.Securities[self.active_future.Symbol].IsTradable:
if predictor > 0:
self.SetHoldings(self.active_future.Symbol, -0.5)
else:
self.SetHoldings(self.active_future.Symbol, 0.5)
else:
if not all(self.Securities[x].GetLastData() and (self.Time.date() - self.Securities[x].GetLastData().Time.date()).days <= self.max_missing_days for x in [self.eia_report, self.api_report]):
self.Liquidate()
return