“通过API数据与彭博社共识的预测器交易WTI期货,在EIA公告前60分钟建仓,并在公告前1分钟平仓。”

I. 策略概要

投资范围包括近月WTI期货合约。预测器计算为API实际数据与彭博社中值共识之差,除以库存水平。如果预测器为正,则卖出WTI期货;如果为负,则买入WTI期货。头寸在EIA公告前60分钟建立,并在公告前1分钟平仓,利用预测器的洞察力在库存数据发布前后进行短期交易。

II. 策略合理性

市场参与者对API和EIA原油库存公告的处理方式不同,尽管两者收集的数据相似。EIA报告更广为人知,而API报告在正常交易时间后发布,流动性较低,因此交易员并未完全将其计入价格。API发布后回报对EIA发布前原油回报的预测能力仅在原油关注度和流动性较低的时期才显著。这与Chordia、Roll和Subrahmanyam(2008)的发现一致,他们发现回报预测能力在流动性高的情况下会下降。这些发现表明,市场状况,特别是低流动性和关注度,导致API报告中的信息延迟纳入油价。

III. 来源论文

Market Inefficiencies Surrounding Energy Announcements [点击查看论文]

<摘要>

我们利用连续的能源库存公告,为金融市场的信息效率提供了新的见解。我们的研究结果为原油期货和股票市场的低效率提供了明确证据。这种低效率可以被经验丰富的交易者利用。我们研究了市场流动性对信息有效纳入的影响。我们还构建了一个预测因子,可以预测库存意外和公告前回报的样本内和样本外情况。最后,我们开发了一个组合预测,可以作为市场对原油库存公告预期的替代指标。

IV. 回测表现

年化回报6.7%
波动率10%
β值0.017
夏普比率0.67
索提诺比率N/A
最大回撤N/A
胜率48%

V. 完整的 Python 代码

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

发表评论

了解 Quant Buffet 的更多信息

立即订阅以继续阅读并访问完整档案。

继续阅读