“该策略投资于来自54个国家的10年期债券,做多回报排名前五分之一的债券,做空回报排名后五分之一的债券,采用等权重,并根据过去一个月的表现每月重新平衡。”

I. 策略概要

该投资策略涉及来自54个国家的10年期政府债券。资产根据其过去一个月的收益率分为五分位。该方法对收益率最高的五分位建立多头头寸,对收益率最低的五分位建立空头头寸。该策略采用等权重,每月重新平衡。

II. 策略合理性

研究表明,上个月的回报可以预测股票、债券、票据、商品、货币和 pooled 资产类别的未来回报。这种短期动量独立于传统的长期12个月动量,并且无法用市场贝塔、波动率、价值、偏度或季节性等常见回报预测因子来解释。它提供了超越这些因子对未来回报的增量洞察。股票、债券和票据的短期动量显示出微弱但显著的相关性,表明可能存在一个共同的潜在因子。尽管其原因尚不清楚,但该效应在各种稳健性检查下都成立,包括子周期分析、市场状况和替代实施方案。虽然动量策略在票据中表现良好,但由于融资成本,实施做多-做空策略是不可行的,这使得它更适合作为一种只做多的方法。

III. 来源论文

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

<摘要>

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

IV. 回测表现

年化回报6.04%
波动率9.69%
β值-0.037
夏普比率0.62
索提诺比率-0.305
最大回撤N/A
胜率49%

V. 完整的 Python 代码

from AlgorithmImports import *
class OneMonthMomentum(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2000, 1, 1)
        self.SetCash(100000)
        self.symbols = [
            "EUREX_FGBL1",    # Euro-Bund (10Y) Futures, Continuous Contract #1 (Germany)
            "CME_TY1",        # 10 Yr Note Futures, Continuous Contract #1 (USA)
            "MX_CGB1",        # Ten-Year Government of Canada Bond Futures, Continuous Contract #1 (Canada)
            "ASX_XT1",        # 10 Year Commonwealth Treasury Bond Futures, Continuous Contract #1 (Australia)
            "SGX_JB1",        # SGX 10-Year Mini Japanese Government Bond Futures, Continuous Contract #1 (Japan)
            "LIFFE_R1",       # Long Gilt Futures, Continuous Contract #1 (U.K.)
            "EUREX_FBTP1"     # Long-Term Euro-BTP Futures, Continuous Contract #1 (Italy)
        ]
        
        self.period = 21
        self.quantile = 5
        self.SetWarmUp(self.period)
        
        # Daily ROC data.
        self.data = {}
        
        for symbol in self.symbols:
            data = self.AddData(QuantpediaFutures, symbol, Resolution.Daily)
            data.SetFeeModel(CustomFeeModel())
            data.SetLeverage(5)
            
            self.data[symbol] = self.ROC(symbol, self.period, Resolution.Daily)
        
        self.rebalance_flag: bool = False
        self.Schedule.On(self.DateRules.MonthStart(self.symbols[0]), self.TimeRules.At(0, 0), self.Rebalance)
        self.settings.daily_precise_end_time = False
        self.settings.minimum_order_margin_portfolio_percentage = 0.
    
    def on_data(self, slice: Slice) -> None:
        if not self.rebalance_flag:
            return
        self.rebalance_flag = False
        # Return sorting.
        sorted_by_return = sorted([x for x in self.data.items() if x[1].IsReady and self.Securities[x[0]].GetLastData() and self.Time.date() < QuantpediaFutures.get_last_update_date()[x[0]] and slice.contains_key(x[0]) and slice[x[0]]], key = lambda x: x[1].Current.Value, reverse = True)
        long = []
        short = []
        if len(sorted_by_return) >= self.quantile:
            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:]]
        # Trade execution.
        targets: List[PortfolioTarget] = []
        for i, portfolio in enumerate([long, short]):
            for symbol in portfolio:
                if slice.contains_key(symbol) and slice[symbol]:
                    targets.append(PortfolioTarget(symbol, ((-1) ** i) / len(portfolio)))
        
        self.SetHoldings(targets, True)
    def Rebalance(self):
        self.rebalance_flag = True
# 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 的更多信息

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

继续阅读