“该策略涉及根据过去12个月商品回报的z分数在10年期债券中开立多头或空头头寸,头寸大小由z分数的绝对值决定,并每月重新平衡。”

I. 策略概要

该投资范围包括来自美国、英国、德国、日本、加拿大和澳大利亚的10年期债券。头寸根据过去12个月商品回报的z分数开立,计算方法是:近期12个月回报减去10年平均回报,然后除以标准差。为了限制极端情况,z分数上限设置为1和-1。如果z分数为正,则采取债券空头头寸;如果为负,则开立多头头寸。头寸大小基于z分数的绝对值。投资组合等权重,每月重新平衡。变体包括仅使用z分数符号或不设上限。

II. 策略合理性

该策略基于商品价格预示通胀压力的理念。商品价格下跌表明通货紧缩,导致债券回报率上升,而价格上涨则预示债券回报率下降。路透CRB总回报指数被用作通胀压力的代理。该策略的可预测性在各种经济条件下都表现稳健,包括衰退和扩张时期、高通胀和低通胀时期,以及股票牛市和熊市。这种可预测性归因于债券市场择时,而非结构性债券风险,并且在市场波动较大时表现出更强的业绩。

III. 来源论文

Predicting Bond Returns: 70 years of International Evidence [点击查看论文]

<摘要>

我们通过对主要债券市场70年国际数据的深入研究,考察了政府债券回报的可预测性。利用基于经济交易的测试框架,我们发现了债券回报可预测性的强有力经济和统计证据,自1950年以来夏普比率为0.87。这一发现对市场和时间段都具有稳健性,包括30年的国际债券市场样本外数据和另外九个国家的数据。此外,结果在各种经济环境中保持一致,包括利率长期上升或下降的时期,并且在扣除交易成本后仍可利用。可预测性与通胀和经济增长的可预测性相关。总的来说,政府债券溢价显示出可预测的动态,国际债券市场回报的择时为投资者提供了可利用的机会。

IV. 回测表现

年化回报4.4%
波动率10%
β值0.091
夏普比率0.44
索提诺比率-0.149
最大回撤N/A
胜率53%

V. 完整的 Python 代码

from AlgorithmImports import *
import numpy as np
#endregion
class PredictingBondReturnswithCommodityIndex(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2000, 1, 1)
        self.SetCash(100000)
        
        self.period = (10 * 12) * 21 + 21
        self.SetWarmUp(self.period, Resolution.Daily)
        
        data = self.AddData(QuantpediaIndices, 'SPGSCITR', Resolution.Daily)
        data.SetFeeModel(CustomFeeModel())
        self.symbol = data.Symbol
        
        # Daily price data.
        self.data = RollingWindow[float](self.period)
        self.max_missing_days:int = 5
        self.recent_month:int = -1
        
    def OnData(self, data):
        # Store daily data.
        if self.symbol in data and data[self.symbol]:
            price = data[self.symbol].Value
            self.data.Add(price)
        
        if self.recent_month == self.Time.month:
            return
        self.recent_month = self.Time.month
        self.Liquidate()
        
        # Z score calc.
        z_score = 0
        if self.data.IsReady:
            if self.Securities[self.symbol].GetLastData() and (self.Time.date() - self.Securities[self.symbol].GetLastData().Time.date()).days <= self.max_missing_days:
                closes:List[float] = list(self.data)[::-1]
                separete_yearly_returns = [Return(closes[x:x+12*21]) for x in range(0, len(closes),21)]
                
                # Return history is ready.
                if len(separete_yearly_returns) == self.period / 21:
                    return_mean = np.mean(separete_yearly_returns)
                    return_std = np.std(separete_yearly_returns)
                    z_score = (separete_yearly_returns[-1] - return_mean) / return_std
                    
                    if z_score > 1: z_score = 1
                    elif z_score < -1: z_score = -1
                    
                    z_score = -1 * z_score
        self.SetHoldings(self.symbol, z_score)
# Quantpedia bond yield data.
# NOTE: IMPORTANT: Data order must be ascending (datewise)
class QuantpediaIndices(PythonData):
    def GetSource(self, config, date, isLiveMode):
        return SubscriptionDataSource("data.quantpedia.com/backtesting_data/index/{0}.csv".format(config.Symbol.Value), SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)
    def Reader(self, config, line, date, isLiveMode):
        data = QuantpediaIndices()
        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])
        data.Value = float(split[1])
        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"))
def Return(values):
    return (values[-1] - values[0]) / values[0]

发表评论

了解 Quant Buffet 的更多信息

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

继续阅读