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.

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 Return6.7%
Volatility10%
Beta0.017
Sharpe Ratio0.67
Sortino RatioN/A
Maximum DrawdownN/A
Win Rate48%

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

Leave a Reply

Discover more from Quant Buffet

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

Continue reading