“该策略根据12个月的动量将32种商品期货分为高4、低4和中等。投资者做多高4商品,做空低4商品,等权重,每月重新平衡。”

I. 策略概要

该策略涉及32种商品期货,每个月都使用近月第一和第二合约之间12个月动量的差异,将商品分为三组:高4、低4和中等。高4包含动量排名最高的四种商品,低4包含动量排名最低的四种商品。投资者做多高4组中的四种商品,做空低4组中的四种商品。投资组合等权重,每月重新平衡。该策略旨在利用商品之间的动量差异获利,目标是做多高表现商品,做空低表现商品。

II. 策略合理性

研究表明,结果是由特定期限套期保值者的价格压力驱动的,这导致现货和期限溢价随时间和单一商品的不同合约而变化。这种压力主要影响展期收益,因为期货市场的套期保值导致期货价格在到期时收敛于现货价格。套期保值者的价格压力影响期货曲线,导致基差动量,表现为持续的陡峭或平坦化,无论市场条件如何,如现货溢价或期货溢价。这种持续性是由期货市场中生产者、消费者和投机者的决策中嵌入的信息解释的。

III. 来源论文

基差动量 [点击查看论文]

<摘要>

我们引入了一个与期货期限结构的斜率和曲率相关的回报预测因子:基差动量。基差动量在预测商品现货和期限溢价的时间序列和横截面方面,显著优于基准特征。基差动量的风险敞口在商品排序投资组合和单个商品中都有定价。我们认为,基差动量捕捉了当投机者和中介机构的市场清算能力受损时出现的期货合约供需失衡,并且基差动量代表了定价风险的补偿。我们的发现与基于存储、库存和套期保值压力的其他解释不一致。

IV. 回测表现

年化回报18.38%
波动率19.98%
β值-0.053
夏普比率0.92
索提诺比率0.195
最大回撤N/A
胜率52%

V. 完整的 Python 代码

from AlgorithmImports import *
import data_tools
#endregion
class SpreadMomentum(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2009, 1, 1)
        self.SetCash(100000)
        self.tickers = {
            "CME_S1": Futures.Grains.Soybeans, # Soybean Futures, Continuous Contract
            "CME_W1": Futures.Grains.Wheat,   # Wheat Futures, Continuous Contract
            "CME_SM1": Futures.Grains.SoybeanMeal,  # Soybean Meal Futures, Continuous Contract
            "CME_BO1": Futures.Grains.SoybeanOil,  # Soybean Oil Futures, Continuous Contract
            "CME_C1": Futures.Grains.Corn,   # Corn Futures, Continuous Contract
            "CME_O1": Futures.Grains.Oats,   # Oats Futures, Continuous Contract
            "CME_LC1": Futures.Meats.LiveCattle,  # Live Cattle Futures, Continuous Contract
            "CME_FC1": Futures.Meats.FeederCattle,  # Feeder Cattle Futures, Continuous Contract
            "CME_LN1": Futures.Meats.LeanHogs,  # Lean Hog Futures, Continuous Contract
            "CME_GC1": Futures.Metals.Gold,  # Gold Futures, Continuous Contract
            "CME_SI1": Futures.Metals.Silver,  # Silver Futures, Continuous Contract
            "CME_PL1": Futures.Metals.Platinum,  # Platinum Futures, Continuous Contract
            "CME_HG1": Futures.Metals.Copper,  # Copper Futures, Continuous Contract
            "CME_LB1": Futures.Forestry.RandomLengthLumber,  # Random Length Lumber Futures, Continuous Contract
            "CME_PA1": Futures.Metals.Palladium,  # Palladium Futures, Continuous Contract
            "CME_RB2": Futures.Energies.Gasoline,  # Gasoline Futures, Continuous Contract
            "ICE_CC1": Futures.Softs.Cocoa,  # Cocoa Futures, Continuous Contract 
            "ICE_O1": Futures.Energies.HeatingOil,   # Heating Oil Futures, Continuous Contract
            "ICE_SB1": Futures.Softs.Sugar11CME,   # Sugar No. 11 Futures, Continuous Contract
            "ICE_WT1": Futures.Energies.CrudeOilWTI,  # WTI Crude Futures, Continuous Contract
        }
        self.futures_data:Dict[str, data_tools.FutureData] = {}
        self.max_missing_days:int = 5
        self.futures_num:int = 4
        self.lookup_period:int = 12 * 21
        min_expiration_days:int = 2
        max_expiration_days:int = 360
        
        # subscribe data
        for qp_ticker, qc_ticker in self.tickers.items():
            security = self.AddData(data_tools.QuantpediaFutures, qp_ticker, Resolution.Daily)
            security.SetFeeModel(data_tools.CustomFeeModel())
            security.SetLeverage(5)
            qp_symbol:Symbol = security.Symbol
            # QC futures
            future:Future = self.AddFuture(qc_ticker, Resolution.Daily)
            future.SetFilter(timedelta(days=min_expiration_days), timedelta(days=max_expiration_days))
            self.futures_data[future.Symbol.Value] = data_tools.FuturesData(qp_symbol, self.lookup_period)
        self.recent_month:int = -1
        self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
    def FindAndUpdateContracts(self, futures_chain, ticker) -> None:
        near_contract:FuturesContract = None
        dist_contract:FuturesContract = None
        if ticker in futures_chain:
            contracts:List[:FuturesContract] = [contract for contract in futures_chain[ticker] if contract.Expiry.date() > self.Time.date()]
            if len(contracts) >= 2:
                contracts:List[:FuturesContract] = sorted(contracts, key=lambda x: x.Expiry, reverse=False)
                near_contract = contracts[0]
                dist_contract = contracts[1]
        self.futures_data[ticker].update_contracts(near_contract, dist_contract)
    
    def OnData(self, data):
        curr_time:datetime.datetime = self.Time
        curr_date:datetime.date = curr_time.date()
        # daily update qc future data
        if data.FutureChains.Count > 0:
            for ticker, future_obj in self.futures_data.items():
                # check if near contract is expired or is not initialized
                if not future_obj.is_initialized() or \
                    (future_obj.is_initialized() and future_obj.near_contract.Expiry.date() == curr_date):
                    self.FindAndUpdateContracts(data.FutureChains, ticker)
                # update QC futures rolling return
                if future_obj.is_initialized():
                    near_c:FuturesContract = future_obj.near_contract
                    dist_c:FuturesContract = future_obj.distant_contract
                    if near_c.Symbol in data and data[near_c.Symbol] and dist_c.Symbol in data and data[dist_c.Symbol]:
                        raw_price1:float = data[near_c.Symbol].Value
                        raw_price2:float = data[dist_c.Symbol].Value
                        if raw_price1 != 0 and raw_price2 != 0:
                            future_obj.update_rate_of_change(raw_price1, raw_price2, curr_time)
        # update, when qp data still coming
        for _, future_obj in self.futures_data.items():
            qp_symbol:Symbol = future_obj.quantpedia_future
            if qp_symbol in data and data[qp_symbol]:
                future_obj.update_quantpedia_last_update(curr_date)
        # rebalance monthly
        if self.recent_month != curr_time.month:
            self.recent_month = curr_time.month
            self.Rebalance(curr_date)
                    
    def Rebalance(self, curr_date:datetime.date) -> None:
        if self.IsWarmingUp: return
        diff:Dict[Symbol, float] = {}
        last_update_date:Dict[str, datetime.date] = data_tools.QuantpediaFutures.get_last_update_date()
        # filter ready futures and reset futures, which do not recieve new data
        for _, future_obj in self.futures_data.items():
            data_ready_flag:bool = future_obj.is_ready()
            # make sure data are ready and up to date
            if data_ready_flag and self.Securities[future_obj.quantpedia_future].GetLastData() and \
                future_obj.quantpedia_future.Value in last_update_date and self.Time.date() < last_update_date[future_obj.quantpedia_future.Value]:
                diff[future_obj.quantpedia_future] = future_obj.get_difference()
            # reset future's data
            elif data_ready_flag:
                future_obj.reset_data()
            
        self.Liquidate()
        # make sure there are enough futures
        if len(diff) < (self.futures_num * 2): return
    
        sorted_by_diff:List[Symbol] = [x[0] for x in sorted(diff.items(), key=lambda item: item[1])]
        long:List[Symbol] = sorted_by_diff[-self.futures_num:]
        short:List[Symbol] = sorted_by_diff[:self.futures_num]
        # Trade execution
        for i, portfolio in enumerate([long, short]):
            for symbol in portfolio:
                self.SetHoldings(symbol, ((-1) ** i) / len(portfolio))

发表评论

了解 Quant Buffet 的更多信息

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

继续阅读