“该策略使用基于价格的信号交易中国商品期货,构建多空投资组合,每月重新平衡,采用逐步展期,并排除低流动性商品以确保实际可投资性。”

I. 策略概要

该策略交易郑商所、大商所和上期所的商品期货,使用基于第二近月和第三近月期货合约价格比率的排序信号。买入具有正信号(现货溢价)的商品,卖出具有负信号(期货溢价)的商品,形成一个等权重的多空投资组合,每月重新平衡。采用逐步展期方法,持有第三近月合约,直到到期前五天,然后每天将20%的头寸展期到下一个第三近月合约。月交易量低于10,000手的商品被排除在外,以确保投资组合在实际流动性限制下可投资。

II. 策略合理性

商品期货中的展期策略通过对现货溢价商品(期货价格低于现货价格)做多头寸和对期货溢价商品(期货价格高于现货价格)做空头寸来利用期限结构。该策略受益于展期收益,因为期货价格在到期时会收敛于现货价格。两种展期方法管理流动性:“逐步展期”持有第三近月合约,直到到期前五天,每天展期20%,支持120万人民币的投资组合容量,年回报率为8.07%。“动态展期”则转向未平仓合约量最高的合约,将容量增加到6.88亿人民币,但表现降至7.40%。替代权重方案可以根据投资者偏好进一步调整容量和回报。这些方法平衡了绩效和可扩展性,使该策略能够适应不同的投资需求。

III. 来源论文

Investable Commodity Premia in China [点击查看论文]

<摘要>

我们研究了中国商品风险溢价的可投资性。此前记录的标准动量、展期和基差动量因子由于中国期货曲线独特的流动性模式而无法投资。然而,动态展期和战略性投资组合权重显著提高了此类溢价的投资容量,而不会损害其统计和经济意义。同时,风格整合带来了增强的绩效和改进的机会集。此外,观察到的可投资溢价对执行滞后、止损、非流动性、子周期规范、季节性和交易成本均具有鲁棒性。它们还为投资者提供了投资组合多样化。最后,中国可投资商品溢价显示出与全球实际经济增长的强大预测能力。

IV. 回测表现

年化回报8.07%
波动率9.94%
β值-0.016
夏普比率0.81
索提诺比率N/A
最大回撤-22.81%
胜率50%

V. 完整的 Python 代码

from AlgorithmImports import *
#endregion
class CarryCommodityPremiainChina(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2017, 1, 1)
        self.SetCash(100000)
        
        # NOTE: QC max cap of 100 custom symbols added => 50 commodities x2 contracts
        self.symbols:list[str] = [
            # 'ER','ME','RO','S','TC','WS','WT',    # empty
            
            'CU', 'A', 'AG', 'AL', 'AP', 'AU',
            'B', 'BB', 'BU', 'C', 'CF', 'CS',
            'CY', 'FB', 'FG', 'FU', 'HC', 'I',
            'IC', 'IF', 'IH', 'J', 'JD', 'JM', 
            'JR', 'L', 'LR', 'M', 'MA', 'NI',
            'OI', 'P', 'PB', 'PM', 'PP', 'RB',
            'RI', 'RM', 'RS', 'RU',  'SF', 'SM',
            'SN', 'SR', 'T', 'TF', 'V',
            'WR', 'ZN', 'Y'
            #  'ZC', 'TA', 'WH'
        ]
        
        self.period:int = 12 * 21
        self.SetWarmup(self.period, Resolution.Daily)
        self.contract_range:list[int] = [2, 3]  # 2nd and 3rd futures contract
        self.latest_update_date:dict = {}   # latest price data arrival time
        for symbol in self.symbols:
            # futures data
            for i in self.contract_range:
                sym = symbol + str(i)
                data = self.AddData(QuantpediaChineseFutures, sym, Resolution.Daily)
                data.SetLeverage(5)
                data.SetFeeModel(CustomFeeModel())
            self.latest_update_date[symbol] = None
                
        self.recent_month = -1
    
    def OnData(self, data):
        signal:dict[Symbol, float] = {}
        # store daily prices
        for symbol in self.symbols:
            # both contracts data points are available
            if all(symbol+str(i) in data and data[symbol+str(i)] and data[symbol+str(i)].Value != 0 for i in self.contract_range):
                near_c_symbol:str = symbol + str(self.contract_range[0])  # 2nd contract
                dist_c_symbol:str = symbol + str(self.contract_range[1])  # 3rd contract
                self.latest_update_date[symbol] = self.Time.date()
                if self.IsWarmingUp: continue
                # rebalance date
                if self.Time.month != self.recent_month:
                    # check data arrival time
                    if (self.Time.date() - self.latest_update_date[symbol]).days > 5:
                        continue
                    
                    # calculate signal from spliced price contract data
                    signal[dist_c_symbol] = data[near_c_symbol].GetProperty('close') / data[dist_c_symbol].GetProperty('close') - 1
        # monthly rebalance
        if self.Time.month == self.recent_month:
            return
        self.recent_month = self.Time.month
        # buying the commodity futures with positive signal and selling commodity futures with a negative signal
        long:list[Symbol] = [x[0] for x in signal.items() if x[1] > 0.]
        short:list[Symbol] = [x[0] for x in signal.items() if x[1] < 0.]
        # trade execution
        invested = [x.Key for x in self.Portfolio if x.Value.Invested]
        for symbol in invested:
            if symbol not in long + short:
                self.Liquidate(symbol)
        for symbol in long:
            if symbol in data and data[symbol]:
                self.SetHoldings(symbol, 1 / len(long))
        
        for symbol in short:
            if symbol in data and data[symbol]:
                self.SetHoldings(symbol, -1 / len(short))
# Quantpedia data.
# NOTE: IMPORTANT: Data order must be ascending (datewise)
class QuantpediaChineseFutures(PythonData):
    def GetSource(self, config, date, isLiveMode):
        return SubscriptionDataSource("data.quantpedia.com/backtesting_data/futures/china/forward_ratio_rolled/{0}.csv".format(config.Symbol.Value), SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)
    def Reader(self, config, line, date, isLiveMode):
        data = QuantpediaChineseFutures()
        data.Symbol = config.Symbol
        
        if not line[0].isdigit(): 
            return None
        split = line.split(';')
        
        data.Time = datetime.strptime(split[0], "%Y-%m-%d") + timedelta(days=1)
        data['close'] = float(split[1]) if split[1] != '' else 0 # unadjusted close
        data['adj_close'] = float(split[2]) if split[2] != '' else 0
        data['last_trade_month'] = int(split[3])
        data.Value = float(split[2]) if split[2] != '' else 0
        return data
# Custom fee model.
class CustomFeeModel(FeeModel):
    def GetOrderFee(self, parameters):
        fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
        return OrderFee(CashAmount(fee, "USD"))

发表评论

了解 Quant Buffet 的更多信息

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

继续阅读