“该策略投资于48种商品,做多表现最佳的五分位,做空表现最差的五分位,采用等权重和每月重新平衡,利用动量来利用近期表现趋势。”

I. 策略概要

该策略涉及农业、工业、能源和贵金属等类别的48种商品。商品根据其过去一个月的收益率分为五分位。对表现最佳的五分位建立多头头寸,对表现最差的五分位建立空头头寸。投资组合采用等权重,每月重新平衡,利用动量来利用商品市场近期表现趋势。

II. 策略合理性

研究发现,上个月的回报可以显著预测股票、债券、票据、商品、货币以及 pooled 资产类别的未来横截面回报。短期动量独立于传统长期动量,并且无法用市场贝塔、特殊波动率、价值、偏度或季节性等因素来解释。这种动量为未来回报提供了增量预测能力。资产类别之间微弱但显著的相关性表明可能存在驱动短期动量的共同因素。尽管其根本原因仍不清楚,但研究结果在各种测试中均表现出稳健性,包括子周期分析、市场状况、日历月份、替代实施方案和数据集,从而支持了短期动量策略的可靠性。

III. 来源论文

Short-Term Momentum (Almost) Everywhere [点击查看论文]

<摘要>

在个股之外是否存在短期反转效应?为了回答这个问题,我们调查了一个包含两个多世纪以来五大主要资产类别(股票指数、政府债券、国库券、商品和货币)回报的综合数据集。与股票层面的证据相反,我们发现了一个惊人的短期动量模式:最近一个月的收益率正向预测未来表现。这种效应无法用已建立的收益率预测因子(包括标准动量)来解释,并且对许多因素都具有鲁棒性。短期动量在具有高特殊波动性的资产中以及在收益率离散度较高的时期最为显著。此外,该策略的回报在不同资产类别之间显示出部分共性。

IV. 回测表现

年化回报21.12%
波动率20.86%
β值-0.079
夏普比率1.01
索提诺比率-0.035
最大回撤N/A
胜率52%

V. 完整的 Python 代码

from AlgorithmImports import *
class OneMonthMomentuminCommodities(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(1991, 1, 1)
        self.SetCash(100000)
        self.symbols = [
            "CME_S1",   # Soybean Futures, Continuous Contract
            "CME_W1",   # Wheat Futures, Continuous Contract
            "CME_SM1",  # Soybean Meal Futures, Continuous Contract
            "CME_BO1",  # Soybean Oil Futures, Continuous Contract
            "CME_C1",   # Corn Futures, Continuous Contract
            "CME_O1",   # Oats Futures, Continuous Contract
            "CME_LC1",  # Live Cattle Futures, Continuous Contract
            "CME_FC1",  # Feeder Cattle Futures, Continuous Contract
            "CME_LN1",  # Lean Hog Futures, Continuous Contract
            "CME_GC1",  # Gold Futures, Continuous Contract
            "CME_SI1",  # Silver Futures, Continuous Contract
            "CME_PL1",  # Platinum Futures, Continuous Contract
            "CME_CL1",  # Crude Oil Futures, Continuous Contract
            "CME_HG1",  # Copper Futures, Continuous Contract
            "CME_LB1",  # Random Length Lumber Futures, Continuous Contract
            "CME_PA1",  # Palladium Futures, Continuous Contract 
            "CME_RR1",  # Rough Rice Futures, Continuous Contract
            "ICE_RS1",  # Canola Futures, Continuous Contract
            "ICE_GO1",  # Gas Oil Futures, Continuous Contract
            "CME_RB2",  # Gasoline Futures, Continuous Contract
            "CME_KW2",  # Wheat Kansas, Continuous Contract
            "ICE_WT1",  # WTI Crude Futures, Continuous Contract
            "ICE_CC1",  # Cocoa Futures, Continuous Contract 
            "ICE_CT1",  # Cotton No. 2 Futures, Continuous Contract
            "ICE_KC1",  # Coffee C Futures, Continuous Contract
            "ICE_O1",   # Heating Oil Futures, Continuous Contract
            "ICE_OJ1",  # Orange Juice Futures, Continuous Contract
            "ICE_SB1"   # Sugar No. 11 Futures, Continuous Contract
            ]
        self.period = 21
        self.quantile = 5
        self.SetWarmUp(self.period)
        
        self.data = {}
        for symbol in self.symbols:
            data = self.AddData(QuantpediaFutures, symbol, Resolution.Daily)
            data.SetFeeModel(CustomFeeModel())
            data.SetLeverage(5)
            
            self.data[symbol] = SymbolData(self, symbol, self.period)
        
        self.rebalance_flag: bool = False
        self.Schedule.On(self.DateRules.MonthStart(self.symbols[0]), self.TimeRules.At(0, 0), self.Rebalance)
        self.settings.minimum_order_margin_portfolio_percentage = 0.
    
    def on_data(self, slice: Slice) -> None:
        if self.IsWarmingUp: return
        
        if not self.rebalance_flag:
            return
        self.rebalance_flag = False
        # Return sorting
        performance = { x : self.data[x].roc.Current.Value for x in self.data if self.data[x].is_ready() and self.Securities[x].GetLastData() and self.Time.date() < QuantpediaFutures.get_last_update_date()[x] }
        
        long = []
        short = []
        if len(performance) >= self.quantile:
            sorted_by_return = sorted(performance.items(), key = lambda x: x[1], reverse = True)
            quintile = int(len(sorted_by_return) / self.quantile)
            long = [x[0] for x in sorted_by_return[:quintile]]
            short = [x[0] for x in sorted_by_return[-quintile:]]
        targets: List[PortfolioTarget] = []
        for i, portfolio in enumerate([long, short]):
            for symbol in portfolio:
                if symbol in slice and slice[symbol]:
                    targets.append(PortfolioTarget(symbol, ((-1) ** i) / len(portfolio)))
        
        self.SetHoldings(targets, True)
    def Rebalance(self):
        self.rebalance_flag = True
class SymbolData():
    def __init__(self, algorithm, symbol, period:int) -> None:
        self.roc = algorithm.ROC(symbol, period, Resolution.Daily)
        self.algorithm = algorithm
    
    def is_ready(self) -> bool:
        return self.roc.IsReady
# Quantpedia data.
# NOTE: IMPORTANT: Data order must be ascending (datewise)
class QuantpediaFutures(PythonData):
    _last_update_date:Dict[Symbol, datetime.date] = {}
    @staticmethod
    def get_last_update_date() -> Dict[Symbol, datetime.date]:
       return QuantpediaFutures._last_update_date
    def GetSource(self, config, date, isLiveMode):
        return SubscriptionDataSource("data.quantpedia.com/backtesting_data/futures/{0}.csv".format(config.Symbol.Value), SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)
    def Reader(self, config, line, date, isLiveMode):
        data = QuantpediaFutures()
        data.Symbol = config.Symbol
        
        if not line[0].isdigit(): return None
        split = line.split(';')
        
        data.Time = datetime.strptime(split[0], "%d.%m.%Y") + timedelta(days=1)
        data['back_adjusted'] = float(split[1])
        data['spliced'] = float(split[2])
        data.Value = float(split[1])
        if config.Symbol.Value not in QuantpediaFutures._last_update_date:
            QuantpediaFutures._last_update_date[config.Symbol.Value] = datetime(1,1,1).date()
        if data.Time.date() > QuantpediaFutures._last_update_date[config.Symbol.Value]:
            QuantpediaFutures._last_update_date[config.Symbol.Value] = data.Time.date()
        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 的更多信息

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

继续阅读